125 lines
8.2 KiB
Markdown
125 lines
8.2 KiB
Markdown
|
|
# 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
|
|||
|
|
|
|||
|
|
1. **Not from our bridge:** `CCIPWETH9Bridge.sol` uses `require(..., "string")` and does not define custom errors with that selector.
|
|||
|
|
2. **Not from repo CCIPRouter:** The in-repo `contracts/ccip/CCIPRouter.sol` also uses `require` strings.
|
|||
|
|
3. **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’s `feeToken` against 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
|
|||
|
|
|
|||
|
|
1. **Use a fee token accepted on Chain 138**
|
|||
|
|
Check the [CCIP Directory](https://docs.chain.link/ccip/supported-networks) (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 with `feeToken = address(0)` and the user paying fees in native ETH.
|
|||
|
|
|
|||
|
|
2. **Ensure the bridge uses the correct LINK (or fee token) address**
|
|||
|
|
On Chain 138 the bridge uses LINK at `0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03`. If the CCIP Router on 138 expects a different token address for fees, the bridge’s `feeToken` must be set to that address (or the router config updated to accept this LINK).
|
|||
|
|
|
|||
|
|
3. **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:
|
|||
|
|
```bash
|
|||
|
|
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.
|
|||
|
|
|
|||
|
|
4. **Confirm destination is enabled**
|
|||
|
|
Ensure the Chain 138 router has **Ethereum mainnet** (selector `5009297550715157269`) as an enabled destination and that the bridge has added mainnet as a destination via `addDestination`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 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 `--value` when `feeToken` is 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](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](https://docs.chain.link/ccip/api-reference/evm/v1.6.0/errors) — FeeTokenNotSupported, InsufficientFeeTokenAmount, etc.
|
|||
|
|
- [scripts/README.md § Send ETH to mainnet](../../scripts/README.md) — exact send command and env.
|
|||
|
|
- [CONTRACT_ADDRESSES_REFERENCE.md](../11-references/CONTRACT_ADDRESSES_REFERENCE.md) — Chain 138 LINK and bridge addresses.
|