Token bridge troubleshooting
This guide covers problems that operators encounter when deploying, verifying, and registering contracts for the canonical token bridge. For the deployment procedure itself, see Deploy a token bridge using the Chain SDK. To learn how the bridge is designed, see Token bridging.
Most routing problems in this guide trace back to a single behavior: the gateway router falls back to a default gateway instead of failing when a token has no registration. We’ll first cover issues related to how a gateway router chooses a specific gateway for a given token.
Concept: How the TokenBridge router picks a gateway
Each side of the bridge has a router (L1GatewayRouter on the parent chain, L2GatewayRouter on the child chain) holding an l1TokenToGateway mapping from a parent chain token address to the gateway that handles it.
The mapping isn't a list of every bridgeable token. It holds only the tokens that someone registered to a custom gateway, so a given ERC-20 token may or may not have an entry. When you bridge a token, the router calls getGateway, which resolves in this order:
- If
l1TokenToGatewayhas an entry for the token, use that gateway. - If it has no entry, fall back to
defaultGateway—the standard ERC-20 gateway. - If the resolved gateway is explicitly disabled, return the zero address.
Step 2 is the reason misrouting is so hard to spot. A token you believe is registered to a custom gateway—but isn't—still bridges successfully through the standard gateway, minting a different child chain token than the one you deployed. Nothing reverts and no event signals the mistake. If a bridge transfer succeeds but the tokens land at an unexpected address, suspect a missing registration before you suspect a bug.
Bridged USDC routes to the standard gateway
You deployed a bridged USDC gateway following the bridged USDC standard, but the SDK and bridge UI keep sending USDC through the standard ERC-20 gateway, producing the default bridged token rather than the Circle-standard one.
What causes this problem
As part of the USDC bridge deployment script, the custom USDC gateway is registered in the TokenBridge router. If that process fails, the TokenBridge can't choose the right gateway for USDC, so it falls back to the default gateway.
Resolution
- Query the router directly. Call
getGatewayonL1GatewayRouterwith your parent chain USDC address. If it returns the standard ERC-20 gateway rather than your USDC gateway, registration never landed. - Check both sides. Registration crosses chains via a retryable ticket, so confirm the child chain router resolves correctly too. See Registration succeeded on one chain only.
- Execute
registerUsdcGatewayTx.json. If your chain owner is theUpgradeExecutorcontract, make sure an account with theEXECUTORrole sent the transaction to register the gateways, which should've been generated by the USDC bridge deployment script.
Custom gateway registration fails
Registering a token or gateway involves sending specific transactions to the TokenBridge router or gateway. This section will cover common troubleshooting, for a deeper understanding refer to the Generic custom gateway page.
Wrong caller for the registration path
registerTokenToL2 and setGateway are designed to be called by the token. They revert with NOT_ARB_ENABLED unless the caller implements the custom token interface and returns the expected value from isArbitrumEnabled(). Calling either from an externally owned account fails no matter how that account is funded or permissioned.
For a token that can't be modified, use the chain-owner-side path instead. forceRegisterTokenToL2 takes parallel arrays and bypasses the token-side check.
Because it's onlyOwner and the owner is the UpgradeExecutor on chains deployed by the creator, the call must be routed through the executor rather than sent from the chain owner's account directly.
Registration succeeded on one chain only
Because registration is asynchronous, the parent chain transaction can succeed while the child chain retryable fails or expires unredeemed. The result is a half-applied registration: one router resolves your custom gateway, and the other falls back to its default. Deposits and withdrawals then disagree about which gateway owns the token.
After any registration, confirm getGateway returns the same gateway on both routers. If the child chain side is missing, locate the retryable ticket from the parent chain transaction and redeem it. To learn how redemption works, see Parent to child chain messaging.
Array length and remapping errors
NO_UPDATE_TO_DIFFERENT_ADDRmeans the token already maps to a different child chain address. Registration is not a general-purpose update mechanism—re-registering with the same address is accepted, but pointing an already-registered token at a new child chain address is rejected. Confirm the intended mapping before the first registration.NOT_TO_CONTRACTmeans the gateway address you supplied has no code.TOKEN_NOT_HANDLED_BY_GATEWAYmeans the gateway doesn't recognize the token you're pointing it at. This is an ordering problem: register the token with the gateway first, then register the gateway with the router.
A WETH gateway is missing
Your chain has no WETH gateway, no child chain aeWETH, and no WETH bridging path.
If your chain uses a custom gas token, it doesn't need a WETH gateway since ETH is not the native token of the chain and doesn't require specific handling. In a custom gas token chain, WETH can be bridge through the default gateway.
On an ETH-based chain, the gateway is wired to the WETH address stored by the creator you deployed through, not to one you choose at chain creation time. To learn which creator you used, see Canonical factory contracts.
network does not support ENS when deploying the creator
Deploying the token bridge creator against a non-Ethereum parent chain fails with output like this:
Deploying token bridge creator...
Error: network does not support ENS (operation="getResolver", network="unknown", code=UNSUPPORTED_OPERATION, version=providers/5.7.2)
What causes this error
A malformed address. The deploy script hands two configuration values to the provider as addresses, and anything that isn't valid hex gets treated as a name to look up instead. A custom parent chain has no ENS registry, so the lookup fails rather than reporting the bad value.
Check these two variables:
BASECHAIN_WETH, the parent chain WETH address the creator stores.ROLLUP_ADDRESS, read only when you leaveGAS_LIMIT_FOR_L2_FACTORY_DEPLOYMENTunset.
A missing variable produces a clear error instead, such as Missing BASECHAIN_WETH in env vars. Seeing the ENS error means the variable is set but malformed.
Resolution
Verify that each address is a bare, checksummed hex string: no quotes, no whitespace, 0x prefixed, 42 characters. Placeholder text, a missing 0x prefix, and a truncated address are the common defects.
Calling outboundTransfer directly on the child chain gateway reverts or moves the wrong tokens
Scripts that call outboundTransfer directly on a child chain gateway, bypassing the router, may not work. L2ArbitrumGateway.outboundTransfer branches on whether the caller is the router:
if (isRouter(msg.sender)) {
(_from, _extraData) = GatewayMessageHandler.parseFromRouterToGateway(_data);
} else {
_from = msg.sender;
_extraData = _data;
}
require(_extraData.length == 0, "EXTRA_DATA_DISABLED");
Direct callers take the else branch, which produces three distinct problems:
- Direct calls must pass empty bytes as
_data, otherwise the call reverts withEXTRA_DATA_DISABLED. - With no router,
_frombecomesmsg.sender. A contract calling the gateway on a user's behalf has its own balance debited, not the user's._l1Tokenis always the parent chain token address, even when calling the child chain gateway. Passing the child chain token address resolves to the wrong token or no token at all.
Two further details are easy to miss:
_l1Tokenis always the parent chain token address, even when calling the child chain gateway. Passing the child chain token address resolves to the wrong token or no token at all.ONLY_COUNTERPART_GATEWAYcomes fromfinalizeInboundTransfer, which only accepts calls from the aliased counterpart gateway. It's not a function to call yourself.
Resolution
The recommended path is to transfer tokens through the TokenBridge router, not the gateway directly. If you're bypassing the router because it resolves the wrong gateway, verify whether the token has been correctly registered in the router. If it is (for example, because a new gateway has been deployed for that token), follow the recommendation above to send your transaction.