docs: add CCTP V2 integration guide for Arc Testnet (closes #110) - #127
docs: add CCTP V2 integration guide for Arc Testnet (closes #110)#127zkasuran wants to merge 4 commits into
Conversation
Documents the Arc-specific details needed to move native USDC on and off Arc with CCTP V2, addressing circlefin#110: - Arc CCTP domain is 26 (verified against Circle's supported-blockchains list; the testnet shares the mainnet domain). - All Arc CCTP contracts are V2. The V1 depositForBurn selector (0x6fd3504e) reverts against the V2 TokenMessenger; the V2 selector is 0x8e0250ee. Both were checked by computing keccak of the signatures. - Burns sourced from Arc should use minFinalityThreshold 2000 (finalized); 1000 can leave the attestation stuck pending. - eth_estimateGas can return data:null for depositForBurn, so pass an explicit gasLimit. - How to fetch the attestation from the Iris API and the fee endpoint. Contract addresses are from Circle's EVM smart contracts reference and the TokenMessenger was confirmed to have code on Arc Testnet. Adds a link from the README documentation section.
|
Independently verified the technical values in this guide and they all check out:
One small wording nit: the guide says "a value above 2000 is capped to 2000," but per the technical guide the rule is "any value below 1000 is treated as 1000, and any value above 1000 is treated as 2000" — so The gas-limit (600k) and USDC-system-address gotchas raised on #110 are already covered here. LGTM. |
The guide said a value above 2000 is capped to 2000, which reads as if 1500 stays 1500. Circle's technical guide states the real rule: any value below 1000 is treated as 1000 and any value above 1000 is treated as 2000, so everything between rounds up to a finalized transfer.
|
Good catch, fixed in The line now states the rule from Circle's technical guide: any value below 1000 is treated as 1000 and any value above 1000 is treated as 2000. So 1500 rounds up to a finalized transfer instead of staying at 1500. I pulled the guide again before rewording, because "capped to 2000" implies a scale that does not exist. Thanks for the independent check on the domain, the four V2 addresses and the selectors. |
|
Good guide — the domain, contract addresses, and selector split are correct. A few additions from having run this integration end-to-end on Arc Testnet that might be worth folding in: USDC decimal trapThe guide correctly states USDC on Arc lives at
|
| Source | Destination | Typical wait (minFinalityThreshold 2000) |
|---|---|---|
| Arc | any | 1–3 min |
| Ethereum Sepolia | Arc | 2–5 min |
| Base Sepolia | Arc | 2–5 min |
| Avalanche Fuji | Arc | 2–5 min |
Practical polling recipe: 5-second interval, 20-minute timeout. Attestations that have not completed after 20 minutes almost always indicate an on-chain revert or an incorrect source domain passed to the Iris endpoint — inspect the burn receipt before re-polling.
destinationCaller semantics (non-zero case)
The guide shows bytes32(0) (anyone may relay), which is the right default for most integrations. For completeness: if destinationCaller is set to a specific address (left-padded to 32 bytes), only that address can call receiveMessage on the destination chain. This is useful for atomic relayers that need to prevent front-running, but requires the caller address to also be left-padded to 32 bytes (same padding rule as mintRecipient).
RPC reliability (Sepolia)
If the guide grows to include testnet setup: rpc.sepolia.org is unreliable. https://ethereum-sepolia-rpc.publicnode.com has been stable for high-volume CCTP testing.
Arc Testnet block explorer
For any burn/mint transaction links in examples: the canonical Arc Testnet explorer is https://testnet.arcscan.app with the standard /tx/{hash} path. Earlier URLs (explorer.testnet.arc.network, explorer.arc.io) are dead.
Overall this fills a real gap — the V1/V2 selector split and the minFinalityThreshold two-tier behaviour are the two most common failure modes for developers porting older CCTP code to Arc, and neither was documented anywhere before this.
Upstream moved the README links from arc.network to arc.io. That line sits directly under the documentation list, so the CCTP guide bullet added here conflicted with it. Kept the bullet and took the arc.io URL.
Two months on, a recheck of every hard value in the guide against Circle's docs, the CCTP V2 contract source and Arc Testnet itself, plus the review points raised on the PR. Corrections: - Burns from domain 26 submitted with minFinalityThreshold 1000 are no longer stuck. Iris attests them at finalityThresholdExecuted 2000 and the message completes, which matches Circle listing Arc testnet as a standard transfer source with fast transfer N/A. The section now explains why 2000 is still the value to pass rather than claiming 1000 hangs. - Circle lists domain 26 as "Arc testnet" and supports Arc testnet only, so the claim that the testnet shares a mainnet domain is gone. - The fees endpoint returns basis points, not token units. maxFee is in burnToken units and the on-chain floor comes from getMinFeeAmount. - Three of the four reference links moved. Updated to their current paths. Additions: - USDC amounts are 6 decimals. The ERC-20 view uses 6 while native accounting uses 18, so the same balance reads a factor of 10^12 apart. - mintRecipient and destinationCaller are bytes32 and need left-padding, with the viem and ethers helpers and what each throws if you skip it. - A V1-selector revert carries no data at all, which is the tell. - Explicit gas applies to receiveMessage when Arc is the destination, and the 1559 base fee race on other destinations points at circlefin#153. - Iris polling cadence, the normal 404, and the explorer check for a message that stalls.
|
Rebuilt on current The conflict4657ade ("docs: update outdated arc.network domain references to arc.io", #70) rewrote the I merged What moved since JuneTwo claims in the guide did not survive a recheck. Burns from Arc at Arc has no CCTP mainnet domain. Circle lists 26 as "Arc testnet" and states that CCTP supports Arc testnet only, so the parenthetical about the testnet sharing a mainnet domain is gone. Three of the four reference links now 308 to new paths ( One correction to my own text. The Iris fees endpoint returns basis points (1 = 0.01%), not token units, so "your maxFee must be greater than or equal to the minimum fee for the route" was misleading. @osr21's suggestionsTaken:
Left out:
Every value, and where it was checked
Not verified, and said so
ChecksPublic CI and Build Docker sit at Nothing in the repo lints markdown ( Thanks @memosr for the independent check and @osr21 for the field notes. Both of them changed the guide. |
osr21
left a comment
There was a problem hiding this comment.
Re-read the full guide at 34a0eb3 against the earlier review and re-checked the values. Approving.
What the last push resolved
- Attestation polling edge cases — the new "Two responses that look like failures are not" section covers the pre-index 404 (
Message not found for provided parameters) and pending status, plus the 35 req/s limit and the 5-minute lockout after a 429. This was the biggest operational gap in the previous revision; polling code that treats the 404 as terminal is the most common way integrators break here. - Explorer URL —
https://testnet.arcscan.app/tx/<burnTxHash>is the live explorer; the olderexplorer.testnet.arc.network/explorer.arc.iohosts are dead. Correct as written. - References — all seven links now point at Circle's current information architecture (
/concepts/,/references/,/howtos/paths); the two dead pre-restructure URLs are gone. minFinalityThresholdclamping (from428dd65) — the below-1000→1000 / above-1000→2000 rule now matches Circle's technical guide, and thefinalityThresholdExecuted: 2000observation from live Arc-sourced messages is consistent with the capability table marking fast transfer N/A for domain 26.
Spot-checks on the rest
- V2/V1 selector split (
0x8e0250ee/0x6fd3504e) matches the 7-arg vs 4-argdepositForBurnsignatures, and the "revert with no data is the tell" diagnostic is right — an unknown selector on the V2 TokenMessenger hits no function and reverts bare, while V2 ABI errors are decodable. - The
bytes32padding examples are correct for both libraries (pad(recipient, { size: 32 })viem,ethers.zeroPadValue(recipient, 32)v6), and the guide is right that both throw at encode time — the residual risk being hand-rolled encoding is exactly the right caveat. Invalid caller for messageis the correct MessageTransmitterV2 revert for adestinationCallermismatch.- The finalized-handler routing note (2000 →
handleReceiveFinalizedMessage) is a subtle detail most guides miss and matters for contract recipients of Arc-sourced transfers. getMinFeeAmount(1000000) → 0on Arc Testnet reproduces, and the basis-point conversion example (10_000_000n * 1n / 10_000n) is arithmetically right.- The gas-limit guidance (explicit 600k, actual 180–260k, cite to #108/#80) matches the linked issues, and the new #153 cite for the stale
maxFeePerGason destination mints is a good addition.
Not blocking
Two optional items from the earlier review weren't folded in — the typical attestation-latency table (Arc→any ~1–3 min, other testnets→Arc ~2–5 min) and the Sepolia RPC reliability note. Both are enhancements, not corrections; the guide's "about 20 minutes, then investigate" bound is the load-bearing number and it's present. Fine to leave for a follow-up if ever.
One note for anyone reading the commit-range diff (428dd65..34a0eb3): it shows CI workflow and scripts/release-config.sh changes, but those arrived via the b761731 merge of upstream main — the PR's effective diff against main is still just the two documented files (README +1, guide +280). Verified via the PR file list.
|
Approved above; two data points for the items the write-up marks as not verified, from having run this integration on Arc Testnet: The gas figures hold up. With a funded account and a live USDC allowance, my The Also confirming the The one optional item from my earlier comment that didn't land — typical attestation latency (Arc→any roughly 1–3 min, other testnets→Arc 2–5 min, against the guide's 20-minute investigate bound) — is fine as a follow-up if a maintainer wants it; it's a nice-to-have next to the polling guidance that did land. |
What this does
Adds a CCTP V2 integration guide at
docs/cctp-v2-integration.mdand links it from the README documentation section. It resolves #110.The guide documents the Arc-specific details a developer needs to move native USDC on and off Arc with Cross-Chain Transfer Protocol V2, which are not currently in this repo or obvious from Circle's general docs:
destinationDomainindepositForBurn. Circle lists that domain as "Arc testnet" and supports Arc testnet only.depositForBurnselector (0x6fd3504e) reverts against the V2 TokenMessenger, and that revert carries no data at all, which is the tell. The V2 selector is0x8e0250ee. This trips up code ported from older Circle examples.mintRecipientanddestinationCallerarebytes32. A 20-byte address has to be left-padded, and current viem and ethers throw while encoding if it is not.minFinalityThreshold: 2000for burns sourced from Arc. Circle lists Arc testnet as a standard transfer source with fast transferN/A, so Iris attests an Arc burn at the finalized level whatever the burn asked for.maxFeeis inburnTokenunits. The route fee the Iris fees endpoint returns is in basis points, and the on-chain floor comes fromgetMinFeeAmount(amount), which is 0 on Arc Testnet today.eth_estimateGasis unreliable for CCTP writes on Arc Testnet (bug: eth_estimateGas returns data:null for CCTP depositForBurn — hardcoded gasLimit required #108, bug: eth_estimateGas unreliable on Arc Testnet — all CCTP/USDC write transactions require explicit gasLimit override #80), so pass an explicitgasLimit. The same applies toreceiveMessagewhen Arc is the destination.How the values were verified
Every hard value was rechecked against a primary source before the latest push, and the guide only states what came back.
MessageTransmitterV2.localDomain()returns 26 on Arc Testnet.eth_getCode, and they cross-reference each other:localMessageTransmitter()andlocalMinter()on the TokenMessenger return the MessageTransmitter and TokenMinter in the table, andlocalTokenMessenger()on the TokenMinter points back.decimals()returns 6 andsymbol()returns USDC. The 18-decimal native view is what this repo's own e2e test describes ("NativeFiatToken uses 6 decimals; the precompile operates in 18-decimal native units").cast sigand an ethersInterfaceindependently give V20x8e0250eeand V10x6fd3504e. Aneth_callwith the V1 selector against Arc's TokenMessenger comes back as a bareexecution revertedwith no data, where the V2 ABI returns a decodable reason.minFinalityThresholdrule: Circle's technical guide states that any value below 1000 is treated as 1000 and any value above 1000 is treated as 2000. Only 1000 and 2000 exist.minFinalityThreshold: 1000and came back from Iris withfinalityThresholdExecuted: 2000andstatus: complete. That matches the capability table, which marks fast transferN/Afor Arc testnet as a source.amount * minFee / MIN_FEE_MULTIPLIERfrom the TokenMessengerV2 source.minFee()andgetMinFeeAmount(1000000)both return 0 on Arc Testnet./v2/messages/{sourceDomain}and/v2/burn/USDC/fees/{src}/{dst}were called against the sandbox host. The 35 requests per second limit is from Circle's resolve attestation issues guide.Two figures are field reports rather than my measurements, and the guide keeps them attributed. The
data: nullresponse frometh_estimateGasis cited to #108 and #80 because it did not reproduce in my probe, and the 600,000 gas limit with a 180,000 to 260,000 actual range comes from #110.Docs-only change, no code touched. Every link in the guide resolves, and the repo's pre-commit whitespace hooks pass on the changed file.
AI disclosure
AI assistance (Claude, Anthropic) was used in developing this change. It drafted and revised the prose and ran the verification steps listed above. The author owns the change and can walk through any part of it. What was verified before this push: every value against Circle's documentation, the CCTP V2 contract source, live
eth_callandeth_getCodeagainst Arc Testnet, live Iris API calls, locally computed selectors, and a link check over the guide. The two values that are field reports rather than measurements are called out as such rather than presented as verified.