- Institutional / JVMTM / reserve-provenance / GRU transport + standards JSON - Validation and verify scripts (Blockscout labels, x402, GRU preflight, P1 local path) - Wormhole wiring in AGENTS, MCP_SETUP, MASTER_INDEX, 04-configuration README - Meta docs, integration gaps, live verification log, architecture updates - CI validate-config workflow updates Operator/LAN items, submodule working trees, and public token-aggregation edge routes remain follow-up (see TODOS_CONSOLIDATED P1). Made-with: Cursor
299 lines
16 KiB
Markdown
299 lines
16 KiB
Markdown
# CCIP Bridge ↔ Ethereum Mainnet Connection
|
||
|
||
**Last Updated:** 2026-03-29
|
||
**Status:** Active
|
||
|
||
---
|
||
|
||
## Overview
|
||
|
||
Chain 138 does not use Chainlink’s public CCIP network (custom chain). Cross-chain sends from Chain 138 to Ethereum mainnet use:
|
||
|
||
1. **Chain 138:** Custom router + WETH9 bridge (emits `MessageSent`).
|
||
2. **Mainnet:** Deployed **CCIPRelayRouter** and **CCIPRelayBridge** that accept relayed messages.
|
||
3. **Relay service:** Off-chain process that watches Chain 138 for `MessageSent` and calls mainnet relay router to deliver.
|
||
|
||
---
|
||
|
||
## Mainnet Contracts (Ethereum)
|
||
|
||
| Contract | Address | Role |
|
||
|--------------------|---------|------|
|
||
| **CCIPRelayRouter** | `0xAd9A228CcEB4cbB612cD165FFB72fE090ff10Afb` | Receives relayed messages; calls bridge `ccipReceive`. Relayer must have `RELAYER_ROLE`. |
|
||
| **CCIPRelayBridge** | `0xF9A32F37099c582D28b4dE7Fca6eaC1e5259f939` | Holds WETH; releases to recipient when relay router calls `ccipReceive`. **Must be funded with WETH** for payouts. **WETH9-only** — no other tokens accepted. |
|
||
| WETH9 | `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` | Canonical mainnet WETH. |
|
||
|
||
**Token mapping:** Chain 138 → Mainnet address mapping and which tokens the relay bridge supports are in [TOKEN_MAPPING_AND_MAINNET_ADDRESSES.md](TOKEN_MAPPING_AND_MAINNET_ADDRESSES.md). Source of truth: `config/token-mapping.json`.
|
||
|
||
---
|
||
|
||
## Chain 138 Setup
|
||
|
||
| Role | Address | Notes |
|
||
|--------|---------|------|
|
||
| **Router** (LINK fee) | `0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817` | Emits `MessageSent`; relay service listens here. |
|
||
| **Bridge** (LINK fee) | `0xcacfd227A040002e49e2e01626363071324f820a` | Pay fee in Chain 138 LINK. Default in `CCIPWETH9_BRIDGE_CHAIN138`. |
|
||
| **Bridge** (native ETH fee) | `0x63cbeE010D64ab7F1760ad84482D6cC380435ab5` | Pay fee in native ETH. |
|
||
|
||
Both bridges have **mainnet destination** set to **CCIPRelayBridge** (`0xF9A32F37099c582D28b4dE7Fca6eaC1e5259f939`), so all 138→mainnet sends are delivered via the relay.
|
||
|
||
---
|
||
|
||
## End-to-End Flow
|
||
|
||
1. User on Chain 138 calls bridge `sendCrossChain(mainnetSelector, recipient, amount)` (e.g. via `scripts/bridge/run-send-cross-chain.sh`).
|
||
2. Bridge pulls WETH from user, calls router `ccipSend(...)` with `receiver = abi.encode(CCIPRelayBridge)`.
|
||
3. Router emits `MessageSent` (no Chainlink relayer).
|
||
4. **Relay service** (Node) watches the Chain 138 router for `MessageSent`, builds `Any2EVMMessage`, and calls mainnet **CCIPRelayRouter.relayMessage(CCIPRelayBridge, message)**.
|
||
5. Relay router calls **CCIPRelayBridge.ccipReceive(message)**; bridge transfers WETH to `recipient` on mainnet.
|
||
|
||
---
|
||
|
||
## Running the Relay Service
|
||
|
||
1. **Fund mainnet CCIPRelayBridge** with WETH so it can pay recipients:
|
||
```bash
|
||
# Option A: Script (transfers deployer's full WETH balance by default)
|
||
./scripts/bridge/fund-mainnet-relay-bridge.sh
|
||
# Option B: Specific amount (wei)
|
||
./scripts/bridge/fund-mainnet-relay-bridge.sh 1000000000000000000
|
||
|
||
# Or manually:
|
||
cast send 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 \
|
||
"transfer(address,uint256)" \
|
||
0xF9A32F37099c582D28b4dE7Fca6eaC1e5259f939 \
|
||
<amount_wei> \
|
||
--rpc-url $ETHEREUM_MAINNET_RPC --private-key $PRIVATE_KEY --legacy
|
||
```
|
||
If the default RPC rate-limits (429), set `ETHEREUM_MAINNET_RPC` to Infura or Alchemy in `smom-dbis-138/.env`.
|
||
|
||
2. **Grant relayer role** (if not already): The relay tx will revert with "transaction execution reverted" (no revert data) until the relayer address has `RELAYER_ROLE` on the mainnet router. As the router's admin (deployer), run:
|
||
```bash
|
||
./scripts/bridge/grant-relayer-role-mainnet.sh
|
||
```
|
||
Or manually: `cast send 0xAd9A228CcEB4cbB612cD165FFB72fE090ff10Afb "grantRelayerRole(address)" 0x4A666F96fC8764181194447A7dFdb7d471b301C8 --rpc-url $ETHEREUM_MAINNET_RPC --private-key $PRIVATE_KEY --legacy`
|
||
|
||
3. **Start the relay service:**
|
||
```bash
|
||
cd smom-dbis-138/services/relay
|
||
# .env: RPC_URL_138, RPC_URL_MAINNET or ETHEREUM_MAINNET_RPC (Infura/Alchemy recommended to avoid 429), PRIVATE_KEY (relayer), CCIP_RELAY_*
|
||
npm start
|
||
```
|
||
|
||
For mainnet RPC, set `RPC_URL_MAINNET` in `services/relay/.env` or `ETHEREUM_MAINNET_RPC` in `smom-dbis-138/.env`. Prefer Infura (`https://mainnet.infura.io/v3/<PROJECT_ID>`) or Alchemy; see [RPC_ENDPOINTS_MASTER.md](../04-configuration/RPC_ENDPOINTS_MASTER.md).
|
||
|
||
Config defaults in `services/relay/src/config.js` point to the router and bridges above; override with env vars if needed.
|
||
|
||
### If relay tx reverts with "transaction execution reverted"
|
||
|
||
1. **Relayer role:** Ensure the relayer has `RELAYER_ROLE`: run `./scripts/bridge/grant-relayer-role-mainnet.sh` (use `RPC_URL_MAINNET=https://ethereum.publicnode.com` if Infura returns 403).
|
||
2. **Bridge WETH:** The mainnet CCIPRelayBridge must hold at least the amount being relayed. If the bridge balance is lower than the transfer amount, fund it:
|
||
```bash
|
||
RPC_URL_MAINNET=https://ethereum.publicnode.com ./scripts/bridge/fund-mainnet-relay-bridge.sh 1000000000000000
|
||
```
|
||
(1e15 wei = 0.001 WETH.)
|
||
|
||
## Live Execution Evidence
|
||
|
||
### 2026-03-29 — Chain 138 to Ethereum mainnet test send
|
||
|
||
**Source-chain send**
|
||
|
||
- Chain 138 bridge: `0xcacfd227A040002e49e2e01626363071324f820a`
|
||
- Source router: `0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817`
|
||
- Source tx hash: `0x5c4aab3d425c8d85b5f64eba595f6a107e1034009ae74a8e5647ad6639032566`
|
||
- Chain 138 block: `3403748`
|
||
- Message ID: `0x19656fe758fc0e36ce5ce16ad9101e76c9eae19e5ed6bea08335dfb664215edc`
|
||
- Recipient: `0x4A666F96fC8764181194447A7dFdb7d471b301C8`
|
||
- Amount: `10000000000000000` wei (`0.01 WETH`)
|
||
- Status: source-chain tx `success`
|
||
|
||
**Verified source evidence**
|
||
|
||
- `MessageSent` was emitted by the Chain 138 router in source tx `0x5c4aab3d425c8d85b5f64eba595f6a107e1034009ae74a8e5647ad6639032566`.
|
||
- The router event encoded the expected destination bridge `0xF9A32F37099c582D28b4dE7Fca6eaC1e5259f939`, recipient `0x4A666F96fC8764181194447A7dFdb7d471b301C8`, and amount `0.01 WETH`.
|
||
|
||
**Initial destination-chain verification**
|
||
|
||
- Destination bridge checked: `0xF9A32F37099c582D28b4dE7Fca6eaC1e5259f939`
|
||
- Ethereum receiver bridge has live code and reports `weth9() = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2`.
|
||
- `processedTransfers(0x19656fe758fc0e36ce5ce16ad9101e76c9eae19e5ed6bea08335dfb664215edc)` remained `false` across repeated checks at:
|
||
- `2026-03-30T02:44:48Z`
|
||
- `2026-03-30T02:45:48Z`
|
||
- `2026-03-30T02:46:50Z`
|
||
- `2026-03-30T02:47:52Z`
|
||
- `2026-03-30T02:48:53Z`
|
||
- No `CrossChainTransferCompleted` logs were found on the destination bridge over Ethereum block range `24765000..latest` during the initial verification window.
|
||
|
||
**Relay repair and replay execution**
|
||
|
||
- Host repaired: `r630-01`
|
||
- Local repo relay implementation was newer than the deployed host version:
|
||
- replaced deployed `services/relay/src/config.js`
|
||
- replaced deployed `services/relay/src/RelayService.js`
|
||
- Disabled stale relay override file on the host:
|
||
- prior `services/relay/.env.local` was pointing at an old Chain 138 router and bridge
|
||
- relay now uses the current source router `0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817`
|
||
- relay now uses the current source bridge `0xcacfd227A040002e49e2e01626363071324f820a`
|
||
- Replay window used for repair:
|
||
- temporarily set `START_BLOCK=3403747`
|
||
- restarted `ccip-relay.service`
|
||
- Verified relay recovery from journal:
|
||
- relay re-detected the historical `MessageSent`
|
||
- relay queued message ID `0x19656fe758fc0e36ce5ce16ad9101e76c9eae19e5ed6bea08335dfb664215edc`
|
||
- relay submitted destination tx `0x87e3d401c498781fabb1289be283af2244add3ab768dfca00027e9d4e270318d`
|
||
|
||
**Destination replay transaction result**
|
||
|
||
- Destination relay tx: `0x87e3d401c498781fabb1289be283af2244add3ab768dfca00027e9d4e270318d`
|
||
- Ethereum block: `24767607`
|
||
- Status: `0 (failed)`
|
||
- Gas used: `65823`
|
||
- Router-level revert surfaced by receipt:
|
||
- `CCIPRelayRouter: relay failed`
|
||
- After the failed replay:
|
||
- `processedTransfers(0x19656fe758fc0e36ce5ce16ad9101e76c9eae19e5ed6bea08335dfb664215edc)` still `false`
|
||
- relay bridge WETH balance remained `2634280582011289` wei (`0.002634280582011289 WETH`)
|
||
- no `CrossChainTransferCompleted` event was emitted
|
||
|
||
**SwapRouter / EnhancedSwapRouter / pool findings**
|
||
|
||
- `SwapRouter` (`0xC2FA05F12a75Ac84ea778AF9D6935cA807275E55`)
|
||
- WETH balance: `0`
|
||
- ETH balance: `0`
|
||
- `EnhancedSwapRouter` (`0x53Bb0218483A189eBd6AE8Ec87139aeb93423E00`)
|
||
- WETH balance: `0`
|
||
- ETH balance: `0`
|
||
- `LiquidityPoolETH` (`0x603e078eb5Cca4F5c817A2F76D073f924D7272d3`) showed accounting drift on Ethereum mainnet:
|
||
- actual native ETH balance: `0`
|
||
- actual WETH balance: `500125031257814` wei (`0.000500125031257814 WETH`)
|
||
- contract accounting still reported:
|
||
- ETH available liquidity: `15000000000000000` wei (`0.015 ETH`)
|
||
- WETH available liquidity: `1000000000000000` wei (`0.001 WETH`)
|
||
- The relayer address `0x4A666F96fC8764181194447A7dFdb7d471b301C8` is recorded as the LP for those pool balances, but direct withdrawals proved the accounting drift is real:
|
||
- ETH-side withdrawal reverted with `LiquidityPoolETH: ETH transfer failed`
|
||
- WETH-side withdrawal reverted with `FailedInnerCall`
|
||
- Operational consequence:
|
||
- neither `SwapRouter` nor `EnhancedSwapRouter` is a funding source for this payout
|
||
- the trustless pool path cannot currently self-fund the missing relay-bridge WETH because the live balances do not match the pool’s own accounting
|
||
|
||
**Verified blockers**
|
||
|
||
1. **Insufficient relay-bridge liquidity on Ethereum mainnet**
|
||
- Mainnet relay bridge WETH balance at verification time:
|
||
- `2634280582011289` wei (`0.002634280582011289 WETH`)
|
||
- Required payout for this message:
|
||
- `10000000000000000` wei (`0.01 WETH`)
|
||
- Shortfall:
|
||
- `7365719417988711` wei (`0.007365719417988711 WETH`)
|
||
- Deployer liquidity on mainnet at verification time:
|
||
- WETH balance: `0`
|
||
- ETH balance: `3345428710812742` wei (`0.003345428710812742 ETH`)
|
||
- Result: the message cannot be paid out on Ethereum mainnet until the relay bridge is funded by another mainnet wallet.
|
||
|
||
2. **Relay service source polling instability**
|
||
- Host: `r630-01`
|
||
- Service: `ccip-relay.service`
|
||
- Status during verification: `active (running)`
|
||
- Journal showed repeated source filter errors:
|
||
- `eth_getFilterChanges ... Filter not found`
|
||
- This was repaired on `2026-03-29`: the deployed relay code was updated, the stale host `.env.local` override was disabled, and the message was successfully replayed into a destination tx.
|
||
- The source-side relay issue is no longer the active blocker for this message.
|
||
|
||
3. **Destination-side execution is still blocked after relay repair**
|
||
- The repaired relay successfully replayed the historical message and submitted destination tx `0x87e3d401c498781fabb1289be283af2244add3ab768dfca00027e9d4e270318d`.
|
||
- That tx mined and reverted in block `24767607` with router-level revert `CCIPRelayRouter: relay failed`.
|
||
- Because the relay bridge balance stayed unchanged and `processedTransfers(messageId)` remained `false`, the live failure is still downstream of source detection and upstream of payout completion.
|
||
|
||
**Operational conclusion**
|
||
|
||
- The Chain 138 send is verified.
|
||
- The original Ethereum receive was blocked by **destination execution + liquidity/accounting**, not by the source send.
|
||
|
||
### 2026-03-29 — Recovery path and successful completion
|
||
|
||
**Bootstrap funding path that was actually used**
|
||
|
||
1. `138 -> Gnosis` was tested first but is not a live lane because Chain 138 emits through the custom router `0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817` while the native Gnosis bridge expects the public Chainlink router.
|
||
2. `138 -> Avalanche` and `138 -> BSC` native-bridge onward paths were evaluated next; Avalanche and BSC native CCIP continuation were not usable for direct onward mainnet forwarding because fee quoting on the official bridge path reverted.
|
||
3. The working recovery path was:
|
||
- send WETH from Chain 138 to the **BSC relay-backed receiver**
|
||
- unwrap received WETH into native BNB
|
||
- bridge BNB to Ethereum mainnet using LiFi / Across
|
||
- wrap enough ETH into WETH on mainnet
|
||
- fund the mainnet relay bridge
|
||
- manually replay the original historical message into `CCIPRelayRouter`
|
||
|
||
**Chain 138 to BSC recovery send**
|
||
|
||
- Chain 138 WETH approve: `0xba583659911a3cc1a59bea74b5b80bdfb7298532a755cd8e9ac358abd220f11d`
|
||
- Chain 138 LINK approve: `0x5d285fa6e00dbdcb85ae31acfd99fd46f521a6b4939d5bfc65d4ee0f922dd7f5`
|
||
- Chain 138 send tx: `0xe129f55a6c39988938fcb33e670bfe35d9ee8d8c46d9c5ffea4264db04587b23`
|
||
- Chain 138 block: `3407959`
|
||
- Recovery message ID on the BSC lane: `0x55a733ad50c86cb835726bcd77b9e8a8d8bae373bb9acf86f3aa0f2b9776b1fe`
|
||
- Send amount: `8000000000000000` wei (`0.008 WETH`)
|
||
- BSC destination completion tx on the relay bridge: `0xed9aee1eb6b2d7b8cd469cc462ce596b567430506fdee210106766c3c0313b6c`
|
||
- BSC destination block: `89548802`
|
||
- Post-delivery verification:
|
||
- `processedTransfers(0x55a733ad50c86cb835726bcd77b9e8a8d8bae373bb9acf86f3aa0f2b9776b1fe) = true`
|
||
- BSC deployer wrapped balance became `8000000000000000`
|
||
|
||
**BSC unwrap and external bridge to Ethereum mainnet**
|
||
|
||
- BSC unwrap tx: `0x55f2ae4804a958c0c66277c5f68f54ea7cc67d97f17eacd375dec2c63b853257`
|
||
- BSC unwrap block: `89549047`
|
||
- Unwrapped amount: `8000000000000000` wei into native BNB
|
||
- External bridge route used: LiFi `AcrossV4`
|
||
- LiFi route id: `7ea2e28a-e54e-4575-8682-5f6ffb99ed73:0`
|
||
- BSC external bridge tx: `0x5b55384778e40b4a603c9fe827d4fd49931ce5347835f5869c62d64a7f49c9f4`
|
||
- BSC external bridge block: `89549501`
|
||
- Bridged value: `15000000000000000` wei native BNB
|
||
- Quoted mainnet receive: `4485724498682976` wei ETH
|
||
|
||
**Mainnet relay-bridge funding**
|
||
|
||
- Mainnet WETH wrap tx: `0x7d78e415ab876263100039d77475ccdfae71c06cbcaece3fbfae63f53248637d`
|
||
- Mainnet relay-bridge funding tx: `0x604a999ccf95ab915caa7b3d2175d5b61391f915441b388a6fe33a67c77ba841`
|
||
- Mainnet funding blocks: `24768164` and `24768165`
|
||
- Exact shortfall funded: `7365719417988711` wei (`0.007365719417988711 WETH`)
|
||
- Relay bridge balance after funding: `10000000000000000` wei (`0.01 WETH`)
|
||
|
||
**Manual replay that completed the original stuck message**
|
||
|
||
- Manual replay tx: `0x7d1302d1e63c6e5957e3476e370a071797c4d5870cfbc81f2a55f4cf83dcb07d`
|
||
- Ethereum block: `24768179`
|
||
- Status: `1 (success)`
|
||
- Gas used: `93643`
|
||
- Transfer path observed in logs:
|
||
- WETH transferred from `0xF9A32F37099c582D28b4dE7Fca6eaC1e5259f939`
|
||
- WETH received by `0x4A666F96fC8764181194447A7dFdb7d471b301C8`
|
||
- `CrossChainTransferCompleted` emitted by the relay bridge
|
||
- `MessageRelayed` emitted by the relay router
|
||
|
||
**Final completion checks**
|
||
|
||
- `processedTransfers(0x19656fe758fc0e36ce5ce16ad9101e76c9eae19e5ed6bea08335dfb664215edc) = true`
|
||
- Recipient mainnet WETH balance: `10000000000000000` wei (`0.01 WETH`)
|
||
- Relay bridge WETH balance after payout: `0`
|
||
- Mainnet recipient ETH balance remained positive for gas after completion.
|
||
|
||
**Operational conclusion**
|
||
|
||
- The original Chain 138 `0.01 WETH` send to Ethereum mainnet is now **fully completed**.
|
||
- The failure mode was recoverable by sourcing missing liquidity from a relay-supported public-chain lane and manually replaying the original message after funding.
|
||
- For future incidents of this specific class, the fastest working runbook is:
|
||
1. verify the original message ID is still unprocessed on mainnet
|
||
2. source missing WETH through a relay-supported public-chain route
|
||
3. fund the mainnet relay bridge with the exact shortfall
|
||
4. replay the historical message directly into `CCIPRelayRouter.relayMessage(...)`
|
||
|
||
---
|
||
|
||
## References
|
||
|
||
- [TOKEN_MAPPING_AND_MAINNET_ADDRESSES.md](TOKEN_MAPPING_AND_MAINNET_ADDRESSES.md) — Full token mapping (138↔Mainnet), relay-supported tokens, and recommendations.
|
||
- [SEND_ETH_TO_MAINNET_REVERT_TRACE.md](SEND_ETH_TO_MAINNET_REVERT_TRACE.md) — Revert history and deployed LINK/native-ETH bridges.
|
||
- [scripts/README.md §8](../../scripts/README.md) — Send command and env.
|
||
- [services/relay/README.md](../../smom-dbis-138/services/relay/README.md) — Relay service deployment and config.
|
||
- [CONTRACT_ADDRESSES_REFERENCE.md](../11-references/CONTRACT_ADDRESSES_REFERENCE.md) — Chain 138 addresses.
|