- ADD_CHAIN138_TO_LEDGER_LIVE: Ledger form done; public code review repo bis-innovations/LedgerLive; init/push commands - CONTRACT_DEPLOYMENT_RUNBOOK: Chain 138 gas price 1 gwei, 36-addr check, TransactionMirror workaround - CONTRACT_*: AddressMapper, MirrorManager deployed 2026-02-12; 36-address on-chain check - NEXT_STEPS_FOR_YOU: Ledger done; steps completable now (no LAN); run-completable-tasks-from-anywhere - MASTER_INDEX, OPERATOR_OPTIONAL, SMART_CONTRACTS_INVENTORY_SIMPLE: updates - LEDGER_BLOCKCHAIN_INTEGRATION_COMPLETE: bis-innovations/LedgerLive reference Co-authored-by: Cursor <cursoragent@cursor.com>
8.2 KiB
Send ETH to Mainnet — Revert Trace (0x9996b315)
Last Updated: 2026-02-12
Status: Reference
What happened
When calling run-send-cross-chain.sh to send WETH from Chain 138 to Ethereum mainnet, the transaction reverted with:
Execution reverted, data: "0x9996b315000000000000000000000000514910771af9ca656af840dff83e8264ecf986ca"
- Selector:
0x9996b315(first 4 bytes) - Parameter:
0x514910771AF9Ca656af840dff83E8264EcF986CA= Ethereum mainnet LINK token address
Where the revert comes from
- Not from our bridge:
CCIPWETH9Bridge.solusesrequire(..., "string")and does not define custom errors with that selector. - Not from repo CCIPRouter: The in-repo
contracts/ccip/CCIPRouter.solalso usesrequirestrings. - Likely from Chainlink CCIP stack: The revert occurs when the bridge calls the deployed CCIP Router on Chain 138 (e.g.
ccipSend). The router, or a downstream contract (e.g. FeeQuoter or OnRamp), validates the message’sfeeTokenagainst an allowed list. The error data includes mainnet LINK, which suggests:- The router/FeeQuoter expects a fee token that is allowed on the source chain (138).
- The bridge is sending a fee token (Chain 138 LINK at
0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03). - The revert may mean “fee token not supported” or “wrong fee token for this chain,” with the reference token (mainnet LINK) encoded in the error.
Chainlink CCIP v1.6.0 defines a FeeQuoter error:
- FeeTokenNotSupported(address token) — selector
0x2502348c— “Thrown when the fee token isn’t in the allowed fee tokens list.”
Our selector 0x9996b315 does not match 0x2502348c; it may be from another CCIP version or an internal contract. The presence of the mainnet LINK address in the data still points to a fee-token validation failure in the CCIP stack.
Flow (where it fails)
run-send-cross-chain.sh
→ cast send CCIPWETH9_BRIDGE_CHAIN138 sendCrossChain(...)
→ CCIPWETH9Bridge.sendCrossChain()
→ transferFrom(sender, bridge, fee) [LINK] ✅
→ approve(ccipRouter, fee) [LINK] ✅
→ ccipRouter.ccipSend(...) ← REVERT 0x9996b315
↑
Deployed CCIP Router (or FeeQuoter / OnRamp) on Chain 138
checks message.feeToken and reverts (fee token not allowed / wrong token).
Fix options
-
Use a fee token accepted on Chain 138
Check the CCIP Directory (or your router’s config) for allowed fee tokens on Chain 138. If the router only accepts native ETH for fees on 138, the bridge would need to be deployed or reconfigured withfeeToken = address(0)and the user paying fees in native ETH. -
Ensure the bridge uses the correct LINK (or fee token) address
On Chain 138 the bridge uses LINK at0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03. If the CCIP Router on 138 expects a different token address for fees, the bridge’sfeeTokenmust be set to that address (or the router config updated to accept this LINK). -
Fund the bridge with LINK
Some setups expect the bridge to hold LINK and the router to pull fees from the bridge. If so, send LINK (on Chain 138) to the bridge:cast send 0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03 \ "transfer(address,uint256)" \ 0x971cD9D156f193df8051E48043C476e53ECd4693 \ 1000000000000000000 \ --rpc-url $RPC_URL_138 --private-key $PRIVATE_KEY(1 LINK = 1e18 wei.) This may or may not fix the revert if the issue is “token not in allowed list” rather than balance.
-
Confirm destination is enabled
Ensure the Chain 138 router has Ethereum mainnet (selector5009297550715157269) as an enabled destination and that the bridge has added mainnet as a destination viaaddDestination.
Try all fixes — results (2026-02-12)
All actionable options were tried in order:
| Fix | Action | Result |
|---|---|---|
| Fund bridge with LINK | Sent 1 LINK to bridge 0x971cD9D156f193df8051E48043C476e53ECd4693 on Chain 138. |
Bridge balance updated. Retry run-send-cross-chain.sh 0.005 → same revert 0x9996b315. |
| Use native ETH as fee | Call updateFeeToken(address(0)) so the script sends fee via --value. |
Reverted: deployed bridge reverts with CCIPWETH9Bridge: zero address. The deployed contract disallows address(0); repo’s CCIPWETH9Bridge.sol allows it. Requires contract upgrade or different deployment to pay fees in native ETH. |
| Destination enabled | Checked getDestinationChains() and router. |
Mainnet selector 5009297550715157269 is in the bridge’s destination list. Router on 138: 0x8078A09637e47Fa5Ed34F626046Ea2094a5CDE5e. |
| Router supported tokens | cast call <ROUTER> getSupportedTokens(uint64)(address[]) 5009297550715157269 |
Returns [0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2] (WETH — transfer token to mainnet, not the fee-token allowlist). |
Conclusion: The revert is from the CCIP Router (or FeeQuoter/OnRamp) on Chain 138: it does not accept Chain 138 LINK (0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03) as the fee token for the 138→mainnet lane. To fix:
- Router-side: Configure the Chain 138 router (or Chainlink config) to accept Chain 138 LINK or native ETH as an allowed fee token for 138→mainnet.
- Bridge-side: Either (1) upgrade the bridge contract to allow
updateFeeToken(address(0))and pay fees in native ETH (script already supports--valuewhenfeeTokenis zero), or (2) set the bridge’s fee token to another token the router accepts on 138 (if such a list is exposed and a token is available).
Both fixes deployed (2026-02-12)
Two new router+bridge pairs were deployed so sends to mainnet work:
| Option | Fee token | Bridge address | Use |
|---|---|---|---|
| LINK (recommended) | Chain 138 LINK | 0xcacfd227A040002e49e2e01626363071324f820a |
Set CCIPWETH9_BRIDGE_CHAIN138 to this (default in smom-dbis-138/.env). User needs WETH + LINK (and LINK approval). |
| Native ETH | Native ETH | 0x63cbeE010D64ab7F1760ad84482D6cC380435ab5 |
Set CCIPWETH9_BRIDGE_CHAIN138 to this to pay the CCIP fee in ETH. User needs WETH + ETH for fee. |
Deployment script: smom-dbis-138/script/DeploySendEthToMainnetFixes.s.sol. Mainnet delivery: Both bridges now use CCIPRelayBridge (0xF9A32F37099c582D28b4dE7Fca6eaC1e5259f939) as the mainnet destination. The relay service watches the Chain 138 router and calls mainnet CCIPRelayRouter to deliver. See CCIP_BRIDGE_MAINNET_CONNECTION.md.
Important: these routers do not relay to mainnet
The routers deployed by DeploySendEthToMainnetFixes.s.sol are the in-repo CCIPRouter.sol: they implement the CCIP interface (e.g. ccipSend, getFee) but only emit a MessageSent event and do not connect to Chainlink’s CCIP network or any cross-chain relayer. So:
- On Chain 138: The WETH you send leaves your wallet and is held by the bridge contract. The bridge calls the router’s
ccipSend; the router records the message and emits an event. No relayer picks it up. - On Ethereum mainnet: Nothing is delivered. The recipient address (e.g.
0x4A666F96fC8764181194447A7dFdb7d471b301C8) does not receive WETH/ETH on mainnet, because no cross-chain execution occurs.
So if you sent 0.005 WETH in the “successful” tx, that 0.005 WETH is still on Chain 138 in the bridge contract (0xcacfd227A040002e49e2e01626363071324f820a), not in your mainnet wallet. To actually bridge to mainnet you need a router that is connected to real CCIP (or another relayer); the original router at 0x8078A09637e47Fa5Ed34F626046Ea2094a5CDE5e may be that router (it reverted due to fee-token config, not due to missing relayer).
References
- Chainlink CCIP v1.6.0 Errors — FeeTokenNotSupported, InsufficientFeeTokenAmount, etc.
- scripts/README.md § Send ETH to mainnet — exact send command and env.
- CONTRACT_ADDRESSES_REFERENCE.md — Chain 138 LINK and bridge addresses.