Skip to main content
Configuring an existing ERC-20 as a crosschain token requires calling registerCustomToken on each blockchain where your token is deployed, using the same deployer wallet and the same bytes32 salt on every call. That pair deterministically computes your token’s crosschain identifier (tokenId), which links your token deployments across blockchains. The resulting connection is deployer-owned: you set rate limits, max transfer amount, and hold the pauser role. After configuration, two additional steps are required before any transfer succeeds: granting the TokenManager the minter role on your ERC-20 on each blockchain, and setting a non-zero rate limit. For denylist behavior on existing ERC-20 custom configurations, see Denylist layers.
Three conditions silently block or break configuration if not met:
  • Same wallet and salt on every blockchain: The tokenId is derived from (deployer address, salt). Using a different wallet or a different salt on any blockchain produces a different tokenId—no error is thrown, and the bridge connection is silently broken.
  • Rate limit starts at zero: Every TokenManager deploys with rateLimit = 0, which blocks all transfers. You must call setRateLimit with a non-zero value on each blockchain before any transfer can proceed.
  • Minter role required on every blockchain: The TokenManager burns on the source blockchain and mints on the destination blockchain. You must grant it the minter role on your ERC-20 on each blockchain before transfers can succeed.

Prerequisites

Before you begin, ensure that you’ve:
  • Installed Node.js v22+
  • Deployed your ERC-20 token on both Ethereum Sepolia and Base Sepolia, and noted both token contract addresses
  • Verified that your token uses the same number of decimals on every blockchain
  • Prepared an Ethereum-compatible testnet wallet whose private key you have available—this is the deployer wallet, and it must be the same wallet used on every blockchain
  • Funded the wallet with Sepolia ETH and Base Sepolia ETH for gas

Step 1: Set up the project

1.1. Create the project and install dependencies

1.2. Configure TypeScript (optional)

This step is optional. It helps prevent missing types in your IDE or editor.
Create a tsconfig.json file:
Then, update the tsconfig.json file:

1.3. Set environment variables

Open .env in your editor and add:
PRIVATE_KEY is the private key for the wallet that deploys the TokenManager contracts. You must use this same wallet on every blockchain where you configure the token.
Open .env in your editor rather than writing values with shell commands, and add .env to your .gitignore. This prevents credentials from leaking into your shell history or version control.
The npm run start command loads variables from .env using Node.js native env-file support.
This example uses one or more private keys for local testing. In production, use a secure key management solution and never expose or share private keys.

Step 2: Configure clients and choose a salt

Create index.ts. Set up public and wallet clients for both blockchains, then define a bytes32 salt. The salt, together with your deployer address, determines your token’s tokenId—it must be identical on every blockchain where you configure the token.

Step 3: Configure on Ethereum Sepolia

3.1. Compute your tokenId

The tokenId is purely deterministic: it derives from your deployer address and salt using two chained view calls. Computing it before submitting any transactions lets you note the value and confirm it is identical after Base Sepolia configuration.

3.2. Call registerCustomToken

Verify the live ABI before copying this struct. The sandbox contract’s DeploymentParams may omit initialSupply while the production contract includes it, or vice versa. A struct with the wrong number of fields causes the transaction to revert. Fetch the ABI from the contract source for the blockchain you are targeting before submitting, and add or remove initialSupply accordingly. The following code includes it based on the documented production struct.
registerCustomToken is payable. No configuration fee is charged on testnet, so you can omit value from both calls in this quickstart. If you are configuring on mainnet, verify whether a fee applies before submitting.

Step 4: Configure on Base Sepolia

Call registerCustomToken on Base Sepolia using the same deployer wallet and the same salt. Pass the Base Sepolia token address. The tokenId is derived from (deployer address, salt)—not from blockchain state—so it is identical to the one computed in step 3.

Step 5: Retrieve the TokenManager addresses

Call resolveTokenManager on each blockchain to get the deployed TokenManager address. You need these addresses to grant the minter role in the next step and to call setRateLimit in step 7.

Step 6: Grant the minter role

The TokenManager burns your token on the source blockchain and mints it on the destination blockchain. You must grant it the minter role on your ERC-20 on each blockchain before transfers can succeed. The exact call depends on your token’s access-control interface. For FiatToken-compatible contracts (such as USDC-style tokens), call configureMinter from the masterMinter address. Replace the function name and ABI with whatever your contract exposes.
If your token uses a different access-control pattern (such as OpenZeppelin AccessControl or a custom owner-managed role), call the equivalent grant function on each deployment.

Step 7: Set a non-zero rate limit

Every TokenManager deploys with rateLimit = 0. A zero rate limit blocks all outbound and inbound transfers for that token on that blockchain. Call setRateLimit on each TokenManager with a non-zero value. Only the operator can call setRateLimit. If you encoded your address as the operator in DeploymentParams, you can call it directly. If the operator field was left empty ("0x"), only the service operator can set the rate limit—contact Circle to adjust it.

Step 8: Verify the configuration

Call resolveTokenAddress on each blockchain to confirm that the tokenId resolves to your token addresses.
Both addresses should match the token addresses you configured. Your token is now configured for crosschain transfers on Ethereum Sepolia and Base Sepolia. To initiate your first transfer, see Transfer EURC from Ethereum to Arc—the same pattern applies to your configured token: swap in your tokenId and adjust the source and destination domains as needed.