Summary
After a successful wallet_addEthereumChain or wallet_switchEthereumChain, MetaMask's eth_chainId briefly reports the new chain ID before its internal JSON-RPC router has fully switched to the new endpoint. Any transaction submitted in this ~200–500ms window is routed to the old chain, not the new one.
This is distinct from #89 (where wallet_switchEthereumChain silently returns without actually switching). Here, the switch does appear to succeed and #89's workaround — polling eth_chainId until it matches — is already in place, but there is still a brief window where eth_chainId matches the new chain while the transaction submission still goes to the old RPC endpoint.
Reproduction
Observed consistently when bridging USDC Base Sepolia → Arc Testnet via CCTP V2 in the Arc Relay Bridge:
- Burn completes on Base Sepolia via ERC-4337 UserOp
- Attestation received from Circle Iris (source domain 6 → destination domain 26)
- Code calls
wallet_addEthereumChain to switch to Arc Testnet, then polls eth_chainId until it returns 0x4CEF52
- Code acquires
BrowserProvider → getSigner() and submits MessageTransmitter.receiveMessage(message, attestation)
Expected: receiveMessage lands on Arc Testnet (domain 26) ✅
Actual: receiveMessage lands on Base Sepolia (domain 6) → reverts with "Invalid destination domain" ❌
On-chain evidence
| Tx |
Chain |
Result |
Notes |
0x1008a1d8e2209e776d852684deece36780eba747167c093a863ab7229d1cbd6b |
Base Sepolia |
❌ Reverted |
receiveMessage submitted to wrong chain; message dest domain = 26, chain domain = 6 |
0x0fc42da8bd52440dc7391bf141e2e5822090d6d654ea0d4ed0cb9054b203adb2 |
Arc Testnet |
✅ Success |
Same attestation, correct chain |
Decoded the CCTP message embedded in the failing calldata:
- Source domain: 6 (Base Sepolia)
- Destination domain: 26 (Arc Testnet)
- The message and attestation were correct — only the chain the tx was submitted to was wrong.
Root cause
wallet_addEthereumChain("Arc Testnet")
→ MetaMask: ok (eth_chainId now returns 0x4CEF52)
→ [~200–500ms gap]
→ MetaMask: RPC router fully switched to Arc Testnet RPC endpoint
There is a timing gap between when eth_chainId reflects the new chain and when the internal RPC transport has caught up. Polling eth_chainId — as the standard #89 workaround does — is not sufficient to guarantee the next eth_sendTransaction hits the correct RPC endpoint.
Fix / Workaround
After the chain switch (and after eth_chainId confirms the new chain), verify that the provider itself reports the correct network via provider.getNetwork() before submitting any transaction. Unlike eth_chainId, getNetwork() goes through the same RPC transport that eth_sendTransaction will use.
async function getSignerOnChain(
expectedChainId: number,
maxRetries = 6
): Promise<ethers.JsonRpcSigner> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
if (attempt > 0) await new Promise(r => setTimeout(r, 500 * attempt));
const provider = await getProvider(); // fresh BrowserProvider
const signer = await provider.getSigner();
const network = await provider.getNetwork();
if (Number(network.chainId) === expectedChainId) return signer;
}
throw new Error(
`Wallet is still on the wrong network after ${maxRetries} attempts. ` +
`Please switch to the destination chain manually in MetaMask and try again.`
);
}
The key difference from polling window.ethereum.request({ method: "eth_chainId" }) directly: provider.getNetwork() sends the eth_chainId call through the same underlying transport that eth_sendTransaction uses, catching the race that direct polling misses.
Affected environments
- MetaMask browser extension with Arc Testnet (chain ID 5042002,
0x4CEF52)
- Reproduced on Base Sepolia → Arc Testnet CCTP V2 bridge
- Likely affects any dApp that switches to a custom EVM network and immediately submits a transaction
Related
Summary
After a successful
wallet_addEthereumChainorwallet_switchEthereumChain, MetaMask'seth_chainIdbriefly reports the new chain ID before its internal JSON-RPC router has fully switched to the new endpoint. Any transaction submitted in this ~200–500ms window is routed to the old chain, not the new one.This is distinct from #89 (where
wallet_switchEthereumChainsilently returns without actually switching). Here, the switch does appear to succeed and #89's workaround — pollingeth_chainIduntil it matches — is already in place, but there is still a brief window whereeth_chainIdmatches the new chain while the transaction submission still goes to the old RPC endpoint.Reproduction
Observed consistently when bridging USDC Base Sepolia → Arc Testnet via CCTP V2 in the Arc Relay Bridge:
wallet_addEthereumChainto switch to Arc Testnet, then pollseth_chainIduntil it returns0x4CEF52BrowserProvider → getSigner()and submitsMessageTransmitter.receiveMessage(message, attestation)Expected:
receiveMessagelands on Arc Testnet (domain 26) ✅Actual:
receiveMessagelands on Base Sepolia (domain 6) → reverts with"Invalid destination domain"❌On-chain evidence
0x1008a1d8e2209e776d852684deece36780eba747167c093a863ab7229d1cbd6breceiveMessagesubmitted to wrong chain; message dest domain = 26, chain domain = 60x0fc42da8bd52440dc7391bf141e2e5822090d6d654ea0d4ed0cb9054b203adb2Decoded the CCTP message embedded in the failing calldata:
Root cause
There is a timing gap between when
eth_chainIdreflects the new chain and when the internal RPC transport has caught up. Pollingeth_chainId— as the standard #89 workaround does — is not sufficient to guarantee the nexteth_sendTransactionhits the correct RPC endpoint.Fix / Workaround
After the chain switch (and after
eth_chainIdconfirms the new chain), verify that the provider itself reports the correct network viaprovider.getNetwork()before submitting any transaction. Unlikeeth_chainId,getNetwork()goes through the same RPC transport thateth_sendTransactionwill use.The key difference from polling
window.ethereum.request({ method: "eth_chainId" })directly:provider.getNetwork()sends theeth_chainIdcall through the same underlying transport thateth_sendTransactionuses, catching the race that direct polling misses.Affected environments
0x4CEF52)Related
wallet_switchEthereumChainfails silently for Arc Testnet; must usewallet_addEthereumChainunconditionally