From 179b53778e5ed86d03993634b6101c8c341bd76f Mon Sep 17 00:00:00 2001 From: osr21 Date: Mon, 25 May 2026 02:47:06 +0930 Subject: [PATCH 1/6] docs: add Arc Relay Bridge ecosystem documentation --- docs/ecosystem/arc-relay-bridge.md | 121 +++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 docs/ecosystem/arc-relay-bridge.md diff --git a/docs/ecosystem/arc-relay-bridge.md b/docs/ecosystem/arc-relay-bridge.md new file mode 100644 index 0000000..bc97a31 --- /dev/null +++ b/docs/ecosystem/arc-relay-bridge.md @@ -0,0 +1,121 @@ +# Arc Relay Bridge + + A cross-chain USDC bridge built on [Circle's CCTP V2](https://developers.circle.com/stablecoins/cctp-getting-started), deployable to Arc Testnet. Burn USDC on a source chain and natively mint it on the destination — no wrapped tokens, no liquidity pools. + + > **Live testnet app:** deployed on Replit + > **Protocol:** Circle CCTP V2 (all chains) + > **Status:** Testnet — supports Arc Testnet ↔ Ethereum Sepolia, Base Sepolia, Avalanche Fuji + + --- + + ## Overview + + Arc Relay Bridge is a frontend-only dapp that implements the full CCTP V2 burn-and-mint flow client-side via ethers.js and MetaMask. Users connect their wallet, choose source and destination chains, enter a USDC amount, and execute a 4-step bridge: + + 1. **Approve** — ERC-20 approval of the fee router (or TokenMessenger directly if no fee) + 2. **Burn** — `depositForBurn` on the source chain TokenMessenger + 3. **Attest** — Poll Circle Iris API until attestation is ready + 4. **Mint** — `receiveMessage` on the destination chain MessageTransmitter + + The UI shows real-time step progress and links each transaction to the appropriate block explorer. + + --- + + ## Supported Chains (Testnet) + + | Chain | CCTP Domain | Chain ID | Explorer | + |---|---|---|---| + | Arc Testnet | 26 | 5042002 | [testnet.arcscan.app](https://testnet.arcscan.app) | + | Ethereum Sepolia | 0 | 11155111 | [sepolia.etherscan.io](https://sepolia.etherscan.io) | + | Base Sepolia | 6 | 84532 | [sepolia.basescan.org](https://sepolia.basescan.org) | + | Avalanche Fuji | 1 | 43113 | [testnet.snowtrace.io](https://testnet.snowtrace.io) | + + --- + + ## Contract Addresses + + All chains use the same CCTP V2 contract addresses: + + | Contract | Address | + |---|---| + | TokenMessenger (CCTP V2) | `0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA` | + | MessageTransmitter (CCTP V2) | `0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275` | + + USDC addresses per chain: + + | Chain | USDC Address | + |---|---| + | Arc Testnet | `0x3600000000000000000000000000000000000000` | + | Ethereum Sepolia | `0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238` | + | Base Sepolia | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` | + | Avalanche Fuji | `0x5425890298aed601595a70AB815c96711a31Bc65` | + + --- + + ## Technical Notes + + ### CCTP V2 on Arc + + Arc implements CCTP V2 with a few important characteristics: + + - **All calls use the 7-parameter `depositForBurn`** (selector `0x8e0250ee`). The V1 4-parameter variant will revert. + - **Nonce encoding:** Arc's MessageTransmitter encodes the nonce as a 32-byte field (always `0x00…00` in practice). Replay protection uses `keccak256(messageBytes)` as the `usedNonces` key — not `keccak256(sourceDomain, nonce)` as in CCTP V1. + - **Message length:** 376 bytes (140-byte header + 236-byte body). + - **`depositForBurn` parameters:** `destinationCaller = bytes32(0)` (permissionless relay), `maxFee = 0`, `minFinalityThreshold` varies by chain (Arc: 2000 finalized; others: 1000 safe). + - **USDC decimals:** Arc's native USDC at `0x360…0` uses 18 decimals for gas accounting but exposes a standard 6-decimal ERC-20 interface — always use the ERC-20 interface. + + ### Attestation + + The bridge polls [Circle Iris API (sandbox)](https://iris-api-sandbox.circle.com) every 5 seconds for up to 20 minutes. Attestation timing: + + - Arc → other chains: ~1–3 minutes (finalized finality) + - Other chains → Arc: ~2–5 minutes (safe finality) + + --- + + ## Stack + + - **Frontend:** React 19 + Vite + Tailwind CSS (shadcn/ui) + - **Web3:** ethers.js v6 (BrowserProvider + Contract) + - **Protocol:** Circle CCTP V2 + - **Attestation:** Circle Iris API (sandbox endpoint) + - **No backend:** all bridge logic runs client-side + + --- + + ## Fee Router + + The bridge includes an optional protocol fee (0.3%) collected via an immutable FeeRouter contract deployed on each chain. Security properties: + + - Immutable `usdc` and `tokenMessenger` addresses (no caller-supplied addresses) + - Reentrancy lock + - Zero `mintRecipient` check + - `rescueTokens` return-value check + + | Chain | FeeRouter Address | + |---|---| + | Arc Testnet | `0x8256a1e1f8971448b49dA0F55b8A1BB6557eA8FC` | + | Ethereum Sepolia | `0x5B1F511ed4dF76f369671BF1c4aCF0dD84CC0804` | + | Base Sepolia | `0x8d4B57eD464df10414Dde3ADC2E403a01ebc50d8` | + | Avalanche Fuji | `0x64D160b7E91e78e52dFc0e8829640E32A919164C` | + + --- + + ## Getting Started + + 1. Get testnet USDC from the [Circle Faucet](https://faucet.circle.com) + 2. Connect MetaMask to the source chain + 3. Select source and destination chains + 4. Enter an amount and click **Bridge** + 5. Approve and sign each step as prompted + 6. Monitor real-time progress — the bridge will auto-switch your wallet to the destination chain for the final mint + + --- + + ## Links + + - [Arc Developer Docs](https://docs.arc.network) + - [CCTP Documentation](https://developers.circle.com/stablecoins/cctp-getting-started) + - [Circle Faucet](https://faucet.circle.com) + - [Arc Testnet Explorer](https://testnet.arcscan.app) + \ No newline at end of file From 2da33becbf1347b63ba29d0024702bd010bdf1a2 Mon Sep 17 00:00:00 2001 From: osr21 Date: Mon, 25 May 2026 02:56:29 +0930 Subject: [PATCH 2/6] docs: add CCTP V2 integration guide with Arc-specific findings --- docs/cctp-v2.md | 219 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 docs/cctp-v2.md diff --git a/docs/cctp-v2.md b/docs/cctp-v2.md new file mode 100644 index 0000000..dd7b972 --- /dev/null +++ b/docs/cctp-v2.md @@ -0,0 +1,219 @@ +# CCTP V2 Integration Guide for Arc + + This guide documents Arc-specific behaviors when integrating [Circle's Cross-Chain Transfer Protocol V2 (CCTP V2)](https://developers.circle.com/stablecoins/cctp-getting-started). These findings come from building and testing against the live Arc testnet and are not fully covered in Circle's general CCTP documentation. + + --- + + ## Quick Reference + + | Parameter | Arc Value | + |---|---| + | CCTP Domain | 26 | + | Chain ID | 5042002 | + | TokenMessenger | `0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA` | + | MessageTransmitter | `0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275` | + | USDC | `0x3600000000000000000000000000000000000000` | + | `minFinalityThreshold` | `2000` (finalized) | + | Message length | 376 bytes | + | Attestation time | ~1–3 minutes | + + --- + + ## Critical: Always Use the V2 `depositForBurn` Selector + + Arc's TokenMessenger **only** supports the 7-parameter CCTP V2 variant of `depositForBurn`. The V1 4-parameter variant will revert. + + ### V2 (correct) — selector `0x8e0250ee` + + ```solidity + function depositForBurn( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller, // bytes32(0) = permissionless relay + uint256 maxFee, // 0 for no fee cap + uint32 minFinalityThreshold // 2000 on Arc (finalized); 1000 on others (safe) + ) external returns (uint64 nonce); + ``` + + ### V1 (wrong — will revert on Arc) — selector `0x6fd3504e` + + ```solidity + function depositForBurn( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken + ) external returns (uint64 nonce); + ``` + + --- + + ## Nonce Encoding: bytes32, Always Zero + + Arc's MessageTransmitter encodes the nonce field as **`bytes32`** (32 bytes, always `0x00…00`) in the CCTP message header — not the `uint64` used in CCTP V1 or standard CCTP V2 documentation. + + ### Message layout (376 bytes total) + + | Offset | Length | Field | + |---|---|---| + | 0 | 4 | `version` (uint32) | + | 4 | 4 | `sourceDomain` (uint32) | + | 8 | 4 | `destinationDomain` (uint32) | + | 12 | 32 | `nonce` (bytes32, always 0x00) | + | 44 | 32 | `sender` (bytes32) | + | 76 | 32 | `recipient` (bytes32) | + | 108 | 32 | `destinationCaller` (bytes32) | + | 140 | 236 | message body | + + This was verified by scanning 221 live `MessageSent` events on Arc testnet — all had nonce = `0x0000…0000`. + + --- + + ## Replay Protection: keccak256(messageBytes) + + Because the nonce is always zero, Arc's `usedNonces` mapping uses **`keccak256(messageBytes)`** as the key — not the CCTP V1 key of `keccak256(sourceDomain, nonce)`. + + ### Correct check (CCTP V2 on Arc) + + ```typescript + const messageHash = ethers.keccak256(messageBytes); + const result: bigint = await messageTransmitter.usedNonces(messageHash); + const alreadyMinted = result !== 0n; + ``` + + ### Wrong check (CCTP V1 style — silently broken on Arc) + + ```typescript + // ❌ DO NOT USE — nonce is always 0 on Arc, this returns the same hash for every message + const key = ethers.keccak256( + ethers.solidityPacked(["uint32", "uint64"], [sourceDomain, nonce]) + ); + ``` + + Using the V1 key means every bridge after the first would be incorrectly flagged as "already minted" once any single message with nonce=0 from that domain had been received. + + --- + + ## minFinalityThreshold + + The `minFinalityThreshold` parameter controls when Circle will issue an attestation after a burn. + + | Chain | Recommended value | Meaning | + |---|---|---| + | Arc Testnet | `2000` | Finalized finality (~1–3 min attestation) | + | Ethereum Sepolia | `1000` | Safe finality (~2–5 min attestation) | + | Base Sepolia | `1000` | Safe finality | + | Avalanche Fuji | `1000` | Safe finality | + + Using `1000` on Arc works but may result in longer attestation waits. Using `2000` is recommended for Arc-originated burns. + + --- + + ## USDC Decimals + + Arc's native USDC at `0x3600000000000000000000000000000000000000` is a system address with a special dual-decimal representation: + + - **18 decimals** — used internally for gas accounting + - **6 decimals** — exposed via the standard ERC-20 interface (`decimals()` returns 6) + + **Always use the ERC-20 interface with 6 decimals** for all token operations (approve, transfer, balance queries). Using 18 decimals will result in 10^12x over/underestimates. + + ```typescript + // ✅ Correct — use 6 decimals via ERC-20 interface + const amount = ethers.parseUnits("10.00", 6); // 10 USDC = 10_000_000n + + // ❌ Wrong — do not use 18 decimals + const amount = ethers.parseUnits("10.00", 18); + ``` + + --- + + ## Attestation + + After a burn on Arc, poll the Circle Iris API (sandbox endpoint for testnet): + + ``` + GET https://iris-api-sandbox.circle.com/v1/attestations/{messageHash} + ``` + + Where `messageHash` is `keccak256(messageBytes)` from the `MessageSent` event. + + Expected response when ready: + + ```json + { + "attestation": "0x...", + "status": "complete" + } + ``` + + **Timing:** + - Arc → other chains: ~1–3 minutes + - Other chains → Arc: ~2–5 minutes + + Poll every 5 seconds. If the attestation is not ready within 20 minutes, the burn may not have been indexed — verify the transaction was confirmed on-chain first. + + --- + + ## Gas on Arc + + Arc uses USDC as its native gas token. When estimating gas for `depositForBurn` or `receiveMessage` calls on Arc, use a higher gas limit than you might expect: + + ```typescript + // Recommended gas limit for Arc write transactions + const gasLimit = 600_000n; + ``` + + Standard EVM gas estimation (`eth_estimateGas`) may underestimate on Arc due to the USDC-as-gas mechanics. Setting an explicit override prevents out-of-gas reverts. + + --- + + ## Complete Example (ethers.js v6) + + ```typescript + import { ethers } from "ethers"; + + const TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA"; + const ARC_DOMAIN = 26; + const ARC_USDC = "0x3600000000000000000000000000000000000000"; + + async function burnOnArc( + signer: ethers.Signer, + amount: bigint, // in 6-decimal units + destinationDomain: number, + mintRecipient: string // EVM address of recipient + ) { + const messenger = new ethers.Contract(TOKEN_MESSENGER, [ + "function depositForBurn(uint256,uint32,bytes32,address,bytes32,uint256,uint32) returns (uint64)" + ], signer); + + const recipientBytes32 = ethers.zeroPadValue(mintRecipient, 32); + + const tx = await messenger.depositForBurn( + amount, + destinationDomain, + recipientBytes32, + ARC_USDC, + ethers.ZeroHash, // destinationCaller = bytes32(0) = permissionless + 0n, // maxFee = 0 + 2000, // minFinalityThreshold (Arc-originated burns) + { gasLimit: 600_000n } + ); + + const receipt = await tx.wait(); + return receipt; + } + ``` + + --- + + ## Resources + + - [Circle CCTP Documentation](https://developers.circle.com/stablecoins/cctp-getting-started) + - [Arc Developer Docs](https://docs.arc.network) + - [Arc Testnet Explorer](https://testnet.arcscan.app) + - [Circle Iris API (sandbox)](https://iris-api-sandbox.circle.com) + - [Circle Faucet](https://faucet.circle.com) + \ No newline at end of file From ca1dfa95d2796cfeacd3528f2243c20bee90a1c8 Mon Sep 17 00:00:00 2001 From: osr21 Date: Mon, 25 May 2026 02:56:47 +0930 Subject: [PATCH 3/6] docs: add ecosystem README listing community tools and dapps --- docs/ecosystem/README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docs/ecosystem/README.md diff --git a/docs/ecosystem/README.md b/docs/ecosystem/README.md new file mode 100644 index 0000000..94ae56d --- /dev/null +++ b/docs/ecosystem/README.md @@ -0,0 +1,39 @@ +# Arc Ecosystem + + Community-built tools, dapps, and integrations on Arc Testnet. + + > **Want to add your project?** Open a pull request adding an entry to this file. + + --- + + ## Bridges + + ### Arc Relay Bridge + + A cross-chain USDC bridge built on Circle's CCTP V2. + + - **Chains:** Arc Testnet ↔ Ethereum Sepolia, Base Sepolia, Avalanche Fuji + - **Protocol:** CCTP V2 burn-and-mint (no wrapped tokens, no liquidity pools) + - **Stack:** React 19 + Vite + ethers.js v6 + - **Docs:** [docs/ecosystem/arc-relay-bridge.md](arc-relay-bridge.md) + + --- + + ## Node Tooling + + ### Arc Node Health Monitor *(in progress)* + + Public Arc node checker and operator health monitor. + + - See [issue #49](https://github.com/circlefin/arc-node/issues/49) for discussion. + + --- + + ## Contributing + + To list your project here, open a pull request with an entry under the relevant category. Include: + + - Project name and one-line description + - Supported chains or use case + - Link to source code or documentation + \ No newline at end of file From d3814a19d088804a13347b4ae933c57aede39879 Mon Sep 17 00:00:00 2001 From: osr21 Date: Mon, 25 May 2026 13:38:05 +0930 Subject: [PATCH 4/6] docs(cctp-v2): add gas estimation bug, RPC reliability findings, and ethers.js chain-switch issue --- docs/cctp-v2.md | 173 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 127 insertions(+), 46 deletions(-) diff --git a/docs/cctp-v2.md b/docs/cctp-v2.md index dd7b972..8f29cca 100644 --- a/docs/cctp-v2.md +++ b/docs/cctp-v2.md @@ -1,6 +1,6 @@ # CCTP V2 Integration Guide for Arc - This guide documents Arc-specific behaviors when integrating [Circle's Cross-Chain Transfer Protocol V2 (CCTP V2)](https://developers.circle.com/stablecoins/cctp-getting-started). These findings come from building and testing against the live Arc testnet and are not fully covered in Circle's general CCTP documentation. + This guide documents Arc-specific behaviors when integrating [Circle's Cross-Chain Transfer Protocol V2 (CCTP V2)](https://developers.circle.com/stablecoins/cctp-getting-started). These findings come from building and testing a production bridge dapp against the live Arc testnet and are not fully covered in Circle's general CCTP documentation. --- @@ -16,12 +16,13 @@ | `minFinalityThreshold` | `2000` (finalized) | | Message length | 376 bytes | | Attestation time | ~1–3 minutes | + | Recommended gas limit | 600,000 (see §Gas Estimation Bug) | --- ## Critical: Always Use the V2 `depositForBurn` Selector - Arc's TokenMessenger **only** supports the 7-parameter CCTP V2 variant of `depositForBurn`. The V1 4-parameter variant will revert. + Arc's TokenMessenger **only** supports the 7-parameter CCTP V2 variant of `depositForBurn`. The V1 4-parameter variant will revert silently. ### V2 (correct) — selector `0x8e0250ee` @@ -96,9 +97,97 @@ --- - ## minFinalityThreshold + ## ⚠️ Known Bug: eth_estimateGas Unreliable on Arc Testnet + + **`eth_estimateGas` consistently fails or returns incorrect values for all USDC write transactions on Arc Testnet.** This affects ERC-20 `approve`, ERC-20 `transfer`, `depositForBurn`, and `receiveMessage`. + + ### Observed errors (without explicit gas limit) + + ``` + Error: missing revert data + Error: could not estimate gas; transaction may fail or may require manual gas limit + Error: execution reverted (no reason string) + ``` + + ### Root cause hypothesis + + Arc's USDC-as-gas model may cause the EVM's gas simulation to incorrectly predict reverts when it cannot account for the ERC-20 gas token balance check. The transactions themselves succeed on-chain when submitted with a fixed gas limit. + + ### Workaround — always provide an explicit gasLimit + + ```typescript + const tx = await contract.someMethod(args, { gasLimit: 600_000n }); + ``` + + 600,000 is comfortably above actual gas usage (typically 50,000–150,000 for CCTP ops) and is safe to hard-code until gas estimation is fixed. This issue has been reported in [arc-node#80](https://github.com/circlefin/arc-node/issues/80). + + --- + + ## ⚠️ Known Issue: RPC Endpoint Reliability + + Two public testnet RPC endpoints are available, with significantly different reliability for transaction submission: + + | Endpoint | Read calls | `eth_sendRawTransaction` | + |---|---|---| + | `https://rpc.drpc.testnet.arc.network` | ✅ Reliable | ✅ Reliable | + | `https://rpc.testnet.arc.network` | ✅ Reliable | ⚠️ Intermittently fails with `"error sending request"` | + + **Use `rpc.drpc.testnet.arc.network` as your primary endpoint**, with `rpc.testnet.arc.network` as a fallback. The unreliable forwarding behaviour of the second endpoint is related to [arc-node#59](https://github.com/circlefin/arc-node/issues/59). + + ```typescript + // Recommended RPC config for Arc Testnet + const RPC_PRIMARY = "https://rpc.drpc.testnet.arc.network"; + const RPC_FALLBACK = "https://rpc.testnet.arc.network"; + ``` + + --- + + ## ⚠️ Known Issue: ethers.js BrowserProvider + Chain Switch + + When building a cross-chain dapp that switches the user's wallet between Arc and another chain (e.g. Arc → Sepolia for the mint step), **ethers v6 `BrowserProvider` can throw `"underlying network changed"` during `tx.wait()`** — even when the transaction was already confirmed on-chain. + + ### Why this happens + + After MetaMask switches chains, ethers v6 detects the network change and aborts any in-flight receipt polling with a "network changed" error. This is a false negative — the transaction succeeded, but the dapp sees an error. - The `minFinalityThreshold` parameter controls when Circle will issue an attestation after a burn. + ### Workaround — retry with a static provider + + After catching a network-flavoured error from `tx.wait()`, retry by fetching the receipt via a `JsonRpcProvider` (static, independent of MetaMask): + + ```typescript + async function waitWithRetry( + tx: ethers.TransactionResponse, + rpcUrls: string[], + retries = 4 + ): Promise { + const isNetworkErr = (e: unknown) => + /(network|could not detect|connection|timeout)/i.test( + e instanceof Error ? e.message : String(e) + ); + + for (let i = 0; i <= retries; i++) { + try { + const receipt = await tx.wait(); + if (receipt) return receipt; + } catch (err) { + if (!isNetworkErr(err) || i === retries) throw err; + } + await new Promise(r => setTimeout(r, 4000)); + for (const url of rpcUrls) { + try { + const receipt = await new ethers.JsonRpcProvider(url) + .getTransactionReceipt(tx.hash); + if (receipt) return receipt; + } catch { /* try next */ } + } + } + throw new Error("Transaction not confirmed after retries"); + } + ``` + + --- + + ## minFinalityThreshold | Chain | Recommended value | Meaning | |---|---|---| @@ -107,66 +196,59 @@ | Base Sepolia | `1000` | Safe finality | | Avalanche Fuji | `1000` | Safe finality | - Using `1000` on Arc works but may result in longer attestation waits. Using `2000` is recommended for Arc-originated burns. + Using `1000` on Arc works but may result in longer attestation waits. `2000` (finalized) is recommended for Arc-originated burns. --- ## USDC Decimals - Arc's native USDC at `0x3600000000000000000000000000000000000000` is a system address with a special dual-decimal representation: + Arc's native USDC at `0x3600000000000000000000000000000000000000` has a dual-decimal representation: - **18 decimals** — used internally for gas accounting - **6 decimals** — exposed via the standard ERC-20 interface (`decimals()` returns 6) - **Always use the ERC-20 interface with 6 decimals** for all token operations (approve, transfer, balance queries). Using 18 decimals will result in 10^12x over/underestimates. + **Always use the ERC-20 interface with 6 decimals** for all token operations. ```typescript - // ✅ Correct — use 6 decimals via ERC-20 interface + // ✅ Correct const amount = ethers.parseUnits("10.00", 6); // 10 USDC = 10_000_000n - // ❌ Wrong — do not use 18 decimals + // ❌ Wrong const amount = ethers.parseUnits("10.00", 18); ``` --- - ## Attestation - - After a burn on Arc, poll the Circle Iris API (sandbox endpoint for testnet): + ## Block Explorer - ``` - GET https://iris-api-sandbox.circle.com/v1/attestations/{messageHash} - ``` + The canonical Arc Testnet block explorer is **[testnet.arcscan.app](https://testnet.arcscan.app)**. - Where `messageHash` is `keccak256(messageBytes)` from the `MessageSent` event. + Transaction URL format: `https://testnet.arcscan.app/tx/{txHash}` - Expected response when ready: + > ⚠️ `explorer.testnet.arc.network` and `explorer.arc.io` are both dead. See [arc-node#81](https://github.com/circlefin/arc-node/issues/81). - ```json - { - "attestation": "0x...", - "status": "complete" - } + When adding Arc Testnet to MetaMask via `wallet_addEthereumChain`, use: + ```typescript + blockExplorerUrls: ["https://testnet.arcscan.app"] ``` - **Timing:** - - Arc → other chains: ~1–3 minutes - - Other chains → Arc: ~2–5 minutes - - Poll every 5 seconds. If the attestation is not ready within 20 minutes, the burn may not have been indexed — verify the transaction was confirmed on-chain first. - --- - ## Gas on Arc + ## Attestation - Arc uses USDC as its native gas token. When estimating gas for `depositForBurn` or `receiveMessage` calls on Arc, use a higher gas limit than you might expect: + After a burn on Arc, poll the Circle Iris API (sandbox endpoint for testnet): - ```typescript - // Recommended gas limit for Arc write transactions - const gasLimit = 600_000n; + ``` + GET https://iris-api-sandbox.circle.com/v1/attestations/{messageHash} ``` - Standard EVM gas estimation (`eth_estimateGas`) may underestimate on Arc due to the USDC-as-gas mechanics. Setting an explicit override prevents out-of-gas reverts. + Where `messageHash = keccak256(messageBytes)` from the `MessageSent` event. + + **Timing:** + - Arc → other chains: ~1–3 minutes (finalized finality) + - Other chains → Arc: ~2–5 minutes (safe finality) + + Poll every 5 seconds for up to 20 minutes. If the attestation is not ready, verify the burn transaction was confirmed on-chain first. --- @@ -176,34 +258,31 @@ import { ethers } from "ethers"; const TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA"; - const ARC_DOMAIN = 26; - const ARC_USDC = "0x3600000000000000000000000000000000000000"; + const ARC_USDC = "0x3600000000000000000000000000000000000000"; async function burnOnArc( signer: ethers.Signer, - amount: bigint, // in 6-decimal units + amount: bigint, // 6-decimal units destinationDomain: number, - mintRecipient: string // EVM address of recipient + mintRecipient: string // EVM address ) { const messenger = new ethers.Contract(TOKEN_MESSENGER, [ "function depositForBurn(uint256,uint32,bytes32,address,bytes32,uint256,uint32) returns (uint64)" ], signer); - const recipientBytes32 = ethers.zeroPadValue(mintRecipient, 32); - + // Note: always provide explicit gasLimit — eth_estimateGas is unreliable on Arc const tx = await messenger.depositForBurn( amount, destinationDomain, - recipientBytes32, + ethers.zeroPadValue(mintRecipient, 32), ARC_USDC, - ethers.ZeroHash, // destinationCaller = bytes32(0) = permissionless - 0n, // maxFee = 0 - 2000, // minFinalityThreshold (Arc-originated burns) + ethers.ZeroHash, // destinationCaller = anyone may relay + 0n, // maxFee + 2000, // minFinalityThreshold (Arc: finalized) { gasLimit: 600_000n } ); - const receipt = await tx.wait(); - return receipt; + return tx.wait(); } ``` @@ -216,4 +295,6 @@ - [Arc Testnet Explorer](https://testnet.arcscan.app) - [Circle Iris API (sandbox)](https://iris-api-sandbox.circle.com) - [Circle Faucet](https://faucet.circle.com) + - [arc-node#80](https://github.com/circlefin/arc-node/issues/80) — eth_estimateGas bug report + - [arc-node#81](https://github.com/circlefin/arc-node/issues/81) — Dead explorer URLs report \ No newline at end of file From 7d441965b409126d11d15747e8d36e96141c381d Mon Sep 17 00:00:00 2001 From: osr21 Date: Tue, 26 May 2026 00:46:06 +0930 Subject: [PATCH 5/6] =?UTF-8?q?docs:=20add=20contract-verification.md=20?= =?UTF-8?q?=E2=80=94=20practical=20dApp=20dev=20guide=20from=20real=20test?= =?UTF-8?q?net=20experience?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contract-verification.md | 209 ++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 docs/contract-verification.md diff --git a/docs/contract-verification.md b/docs/contract-verification.md new file mode 100644 index 0000000..0ce4da7 --- /dev/null +++ b/docs/contract-verification.md @@ -0,0 +1,209 @@ +# Contract Verification on ArcScan + + ArcScan (https://testnet.arcscan.app) is powered by Blockscout. Verified contracts display their + source code, ABI, and read/write interfaces — significantly improving the developer experience + for anyone integrating your contract. + + --- + + ## Hardhat — `@nomicfoundation/hardhat-verify` + + ### Installation + + ```bash + npm install --save-dev @nomicfoundation/hardhat-verify + ``` + + ### Configuration + + Add the following to your `hardhat.config.ts`. The `etherscan` block tells `hardhat-verify` + where Blockscout's verification API is. The dummy API key is required — Blockscout ignores the + value but `hardhat-verify` rejects an empty string. + + ```typescript + import "@nomicfoundation/hardhat-verify"; + import { HardhatUserConfig } from "hardhat/types"; + + const config: HardhatUserConfig = { + solidity: { + version: "0.8.20", + settings: { + optimizer: { enabled: true, runs: 200 }, + }, + }, + networks: { + arc: { + url: process.env.ARC_RPC_URL ?? "https://arc-testnet.drpc.org", + chainId: 5042002, + accounts: [process.env.PRIVATE_KEY!], + }, + arcMainnet: { + url: process.env.ARC_MAINNET_RPC_URL ?? "", + chainId: 5042, + accounts: [process.env.PRIVATE_KEY!], + }, + }, + etherscan: { + apiKey: { + arc: "empty", // Blockscout ignores API keys — any non-empty string works + arcMainnet: "empty", + }, + customChains: [ + { + network: "arc", + chainId: 5042002, // Arc Testnet + urls: { + apiURL: "https://testnet.arcscan.app/api", + browserURL: "https://testnet.arcscan.app", + }, + }, + { + network: "arcMainnet", + chainId: 5042, // Arc Mainnet + urls: { + apiURL: "https://arcscan.app/api", + browserURL: "https://arcscan.app", + }, + }, + ], + }, + sourcify: { + enabled: false, // Prevents conflicting verification attempts via Sourcify + }, + }; + + export default config; + ``` + + ### Verify a deployed contract + + ```bash + npx hardhat verify --network arc + ``` + + For a contract with no constructor arguments: + + ```bash + npx hardhat verify --network arc + ``` + + --- + + ## Foundry — `forge verify-contract` + + ### Configuration (`foundry.toml`) + + ```toml + [profile.default] + src = "src" + out = "out" + libs = ["lib"] + optimizer = true + optimizer_runs = 200 + + [etherscan] + arc = { key = "empty", url = "https://testnet.arcscan.app/api" } + arcMainnet = { key = "empty", url = "https://arcscan.app/api" } + ``` + + ### Verify command + + ```bash + forge verify-contract \ + \ + src/MyContract.sol:MyContract \ + --chain-id 5042002 \ + --etherscan-api-key empty \ + --verifier-url https://testnet.arcscan.app/api \ + --constructor-args $(cast abi-encode "constructor(address,uint256)" ) + ``` + + For a no-argument constructor: + + ```bash + forge verify-contract \ + \ + src/MyContract.sol:MyContract \ + --chain-id 5042002 \ + --etherscan-api-key empty \ + --verifier-url https://testnet.arcscan.app/api + ``` + + --- + + ## Manual Verification via ArcScan UI + + 1. Navigate to your contract on ArcScan: + `https://testnet.arcscan.app/address/?tab=contract` + 2. Click **"Verify & Publish"** + 3. Select **"Via flattened source code"** + 4. Fill in: + - **Contract name**: exact name from the Solidity file + - **Compiler version**: e.g., `v0.8.20+commit.a1b79de6` + - **Optimization**: Yes / No (must match your actual compilation settings) + - **Runs**: e.g., 200 (must match exactly) + - **EVM version**: e.g., `shanghai` or `paris` (must match your compilation settings) + 5. Paste the flattened source code + + Flatten with Hardhat: + ```bash + npx hardhat flatten src/MyContract.sol > MyContract.flat.sol + ``` + + Or with Foundry: + ```bash + forge flatten src/MyContract.sol > MyContract.flat.sol + ``` + + --- + + ## Important: Bytecode Must Match Exactly + + Blockscout verification requires the compiled bytecode to match the on-chain deployed bytecode + **exactly**, including the CBOR metadata suffix. This means: + + - **Compiler version** must match to the commit hash (e.g., `v0.8.20+commit.a1b79de6`) + - **Optimizer settings** (enabled, runs) must match exactly + - **EVM version target** must match (check `evmVersion` in your `hardhat.config.ts` or `foundry.toml`) + - **Source code** must be identical to what was compiled — even whitespace-only changes + invalidate the CBOR metadata hash and cause a mismatch + + If your source has been modified since deployment (e.g., you upgraded the contract logic but + deployed an old compiled binary), the original source is effectively lost. The only path forward + is to redeploy with the current source and verify the new contract. + + --- + + ## Blockscout Verification API + + For programmatic or scripted verification, the Blockscout v2 REST API accepts flattened + source code directly: + + ```bash + curl -X POST https://testnet.arcscan.app/api/v2/smart-contracts/
/verification/via/flattened-code \ + -H "Content-Type: application/json" \ + -d '{ + "compiler_version": "v0.8.20+commit.a1b79de6", + "source_code": "", + "is_optimization_enabled": true, + "optimization_runs": 200, + "contract_name": "MyContract", + "evm_version": "default", + "autodetect_constructor_args": true + }' + ``` + + A `200` response with `{"message":"Smart-contract verification started"}` means the request was + accepted. Poll the contract endpoint to check whether verification succeeded: + + ```bash + curl https://testnet.arcscan.app/api/v2/smart-contracts/
+ # Look for "is_verified": true in the response + ``` + + --- + + ## Related Issues + + - [arc-node#84](https://github.com/circlefin/arc-node/issues/84) — Tracking the missing official documentation + \ No newline at end of file From 08ebc151e0e31127d823ebc1f69ed16b57dc1434 Mon Sep 17 00:00:00 2001 From: osr21 Date: Tue, 26 May 2026 00:47:17 +0930 Subject: [PATCH 6/6] =?UTF-8?q?docs:=20add=20dapp-development.md=20?= =?UTF-8?q?=E2=80=94=20practical=20guide=20from=20real=20Arc=20Testnet=20i?= =?UTF-8?q?ntegration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/dapp-development.md | 226 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 docs/dapp-development.md diff --git a/docs/dapp-development.md b/docs/dapp-development.md new file mode 100644 index 0000000..acc4ff6 --- /dev/null +++ b/docs/dapp-development.md @@ -0,0 +1,226 @@ +# DApp Development on Arc Testnet + + A practical reference for developers building browser-based DApps on Arc Testnet. These notes reflect + real-world integration experience — each section documents a pain point encountered during development, + with a working workaround. + + --- + + ## Network Configuration + + | Parameter | Value | + |---|---| + | Chain ID | `5042002` (`0x4cef52`) | + | Public RPC | `https://rpc.testnet.arc.network` | + | Browser-safe RPC | `https://arc-testnet.drpc.org` (has CORS headers) | + | Block explorer | `https://testnet.arcscan.app` | + | USDC | `0x3600000000000000000000000000000000000000` | + | Chain ID (mainnet) | `5042` | + + --- + + ## MetaMask Integration + + ### ⚠️ Always use `wallet_addEthereumChain`, never `wallet_switchEthereumChain` + + `wallet_switchEthereumChain` fails silently or throws error `4902` for Arc Testnet — even when + the network was previously added. `wallet_addEthereumChain` is the only reliable method: it both + adds the network if missing *and* switches to it if already present. See [arc-node#89](https://github.com/circlefin/arc-node/issues/89). + + ```typescript + await window.ethereum.request({ + method: "wallet_addEthereumChain", + params: [{ + chainId: "0x4CEF52", // 5042002 + chainName: "Arc Testnet", + nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 }, + rpcUrls: ["https://rpc.testnet.arc.network"], + blockExplorerUrls: ["https://testnet.arcscan.app"], + }], + }); + ``` + + ### Gas display note + + Arc uses USDC as its native gas token, but MetaMask requires `nativeCurrency.decimals: 18` and + treats it as ETH. MetaMask will show gas costs in "ETH" (18-decimal units) — this is cosmetic and + cannot be changed from application code. + + --- + + ## RPC Endpoints + + ### CORS limitation + + `https://rpc.testnet.arc.network` does **not** return `Access-Control-Allow-Origin` headers. + Browser fetch() calls fail with a CORS error. Tracked in [arc-node#90](https://github.com/circlefin/arc-node/issues/90). + + **Options:** + + | Approach | How | + |---|---| + | Use dRPC endpoint | `https://arc-testnet.drpc.org` — has CORS headers, works in browser | + | Use MetaMask provider | `window.ethereum` proxies RPC, bypassing CORS | + | Proxy backend | Forward `/rpc` → `rpc.testnet.arc.network` from your server | + + ```typescript + import { ethers } from "ethers"; + + // ✅ Works in browser — MetaMask handles CORS + const provider = new ethers.BrowserProvider(window.ethereum); + + // ✅ Works in browser — dRPC has CORS headers + const staticProvider = new ethers.JsonRpcProvider("https://arc-testnet.drpc.org"); + + // ❌ CORS error in browser + // const broken = new ethers.JsonRpcProvider("https://rpc.testnet.arc.network"); + ``` + + ### Reliability comparison + + | Endpoint | Browser CORS | Reads | `eth_sendRawTransaction` | + |---|---|---|---| + | `https://arc-testnet.drpc.org` | ✅ | ✅ | ✅ Reliable | + | `https://rpc.testnet.arc.network` | ❌ | ✅ | ⚠️ Intermittent | + + Tracked in [arc-node#92](https://github.com/circlefin/arc-node/issues/92) and [arc-node#59](https://github.com/circlefin/arc-node/issues/59). + + --- + + ## USDC Decimals + + Arc USDC has **two decimal representations** depending on context: + + | Context | Decimals | 1 USDC encoded as | + |---|---|---| + | ERC-20 (transfer, approve, balanceOf) | **6** | `1_000_000` | + | Native gas (eth_gasPrice, fee history) | **18** | `1_000_000_000_000_000_000` | + + **Always use 6 decimals for token operations:** + + ```typescript + // ✅ Correct + const amount = ethers.parseUnits("10.00", 6); // 10_000_000n + + // ❌ Wrong — overdraws by 10^12 + const amount = ethers.parseUnits("10.00", 18); + ``` + + Calling `usdc.decimals()` returns `6`, which is correct for all ERC-20 operations. + The 18-decimal representation only appears in RPC gas-price responses. + Tracked in [arc-node#91](https://github.com/circlefin/arc-node/issues/91). + + --- + + ## Gas — Two Known Issues + + ### Issue 1: `eth_estimateGas` unreliable + + Gas estimation fails or returns incorrect values for USDC write transactions (approve, transfer, + depositForBurn). Always provide an explicit `gasLimit`. Tracked in [arc-node#80](https://github.com/circlefin/arc-node/issues/80). + + ```typescript + // 600_000 is safe and well above actual usage (~50k–150k for CCTP operations) + const tx = await contract.method(args, { gasLimit: 600_000n }); + ``` + + ### Issue 2: Stale `eth_gasPrice` baseline + + Submitting two transactions in rapid succession may fail with **"replacement transaction underpriced"** + unless a ≥30% gas price premium is applied. Tracked in [arc-node#87](https://github.com/circlefin/arc-node/issues/87). + + ```typescript + const feeData = await provider.getFeeData(); + const gasPrice = (feeData.gasPrice! * 130n) / 100n; // +30% premium + + const tx = await contract.method(args, { gasPrice, gasLimit: 600_000n }); + ``` + + --- + + ## `eth_getLogs` Block Range Limit + + Wide-range `eth_getLogs` calls (e.g., fromBlock: 0 → toBlock: latest) hang silently for 60+ + seconds and never return. Paginate with a bounded range. Tracked in [arc-node#83](https://github.com/circlefin/arc-node/issues/83). + + ```typescript + async function getLogsInBatches(provider, filter, batchSize = 10_000) { + const latest = await provider.getBlockNumber(); + const logs: ethers.Log[] = []; + for (let from = 0; from <= latest; from += batchSize) { + const chunk = await provider.getLogs({ + ...filter, + fromBlock: from, + toBlock: Math.min(from + batchSize - 1, latest), + }); + logs.push(...chunk); + } + return logs; + } + ``` + + --- + + ## EIP-2612 Permit (Gasless Approvals) + + Arc Testnet USDC supports EIP-2612 `permit` — off-chain approval via a typed-data signature. + This removes the need for a separate `approve()` transaction. Tracked in [arc-node#93](https://github.com/circlefin/arc-node/issues/93). + + ```typescript + import { ethers } from "ethers"; + + const USDC = "0x3600000000000000000000000000000000000000"; + const ABI = [ + "function nonces(address) view returns (uint256)", + "function name() view returns (string)", + "function version() view returns (string)", + "function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)", + ]; + + async function signPermit(signer: ethers.Signer, spender: string, amount: bigint, deadline: bigint) { + const usdc = new ethers.Contract(USDC, ABI, signer); + const owner = await signer.getAddress(); + const nonce = await usdc.nonces(owner); + const chainId = (await signer.provider!.getNetwork()).chainId; + + const sig = await signer.signTypedData( + { name: await usdc.name(), version: await usdc.version(), chainId, verifyingContract: USDC }, + { Permit: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + { name: "value", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ]}, + { owner, spender, value: amount, nonce, deadline } + ); + return { ...ethers.Signature.from(sig), deadline }; + } + ``` + + --- + + ## Cross-Chain Bridging (CCTP V2) + + For bridging USDC to/from Arc Testnet via Circle's CCTP V2, see + [docs/cctp-v2.md](./cctp-v2.md) — it covers Arc-specific nonce encoding, the gas estimation + bug, RPC endpoint selection, attestation timing, and the ethers.js chain-switch workaround. + + --- + + ## Smart Contract Verification + + See [docs/contract-verification.md](./contract-verification.md) for Hardhat and Foundry configuration + to verify contracts on ArcScan. Tracked in [arc-node#84](https://github.com/circlefin/arc-node/issues/84). + + --- + + ## Testnet Resources + + | Resource | URL | + |---|---| + | Block explorer | https://testnet.arcscan.app | + | Circle USDC Faucet | https://faucet.circle.com | + | Arc Developer Docs | https://docs.arc.network | + | CCTP Docs | https://developers.circle.com/stablecoins/cctp-getting-started | + \ No newline at end of file