Skip to content

docs: add CCTP V2 integration guide for Arc Testnet (closes #110) - #127

Open
zkasuran wants to merge 2 commits into
circlefin:mainfrom
zkasuran:docs/cctp-v2-integration
Open

docs: add CCTP V2 integration guide for Arc Testnet (closes #110)#127
zkasuran wants to merge 2 commits into
circlefin:mainfrom
zkasuran:docs/cctp-v2-integration

Conversation

@zkasuran

@zkasuran zkasuran commented Jun 8, 2026

Copy link
Copy Markdown

What this does

Adds a CCTP V2 integration guide at docs/cctp-v2-integration.md and 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:

  • Arc CCTP domain is 26. Required as destinationDomain in depositForBurn.
  • All Arc CCTP contracts are V2. The V1 depositForBurn selector (0x6fd3504e) reverts against the V2 TokenMessenger. The V2 selector is 0x8e0250ee. This trips up code ported from older Circle examples.
  • Use minFinalityThreshold: 2000 for burns sourced from Arc. 1000 (Fast Transfer) can leave the attestation stuck pending.
  • eth_estimateGas can return data: null for depositForBurn, so pass an explicit gasLimit (covered with the typical gas range).
  • How to fetch the attestation from the Iris API and check route fees.

How I verified the values

  • Domain 26 and the source-chain domains (Ethereum 0, Base 6, Avalanche 1): Circle's supported blockchains.
  • Contract addresses: Circle's EVM smart contracts reference. I also confirmed the TokenMessenger 0x8FE6…2DAA returns contract code (2175 bytes) on Arc Testnet via eth_getCode.
  • Selectors: computed keccak256 of both signatures. V2 depositForBurn(uint256,uint32,bytes32,address,bytes32,uint256,uint32) = 0x8e0250ee, V1 depositForBurn(uint256,uint32,bytes32,address) = 0x6fd3504e.
  • minFinalityThreshold semantics (1000 = Fast/confirmed, 2000 = Standard/finalized): Circle's contract interfaces. The "use 2000 on Arc" guidance comes from the field report in docs: CCTP V2 integration on Arc Testnet — domain 26, minFinalityThreshold 2000, V1 selector incompatible #110; I documented it as Arc behavior, not as a Circle spec.
  • Iris endpoints: the /v2/messages/{sourceDomain} and /v2/burn/USDC/fees/{src}/{dst} paths return well-formed responses against the sandbox host.

Docs-only change, no code touched.

AI disclosure

This guide was prepared with help from Claude (Anthropic), which drafted the prose. Every value (domain, contract addresses, selectors, finality thresholds, API paths) was verified against Circle's official documentation, an on-chain eth_getCode call, a local keccak computation of the selectors, and live calls to the Iris sandbox API before submitting. The verification steps are listed above so they can be rechecked.

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.
@memosr

memosr commented Jun 15, 2026

Copy link
Copy Markdown

Independently verified the technical values in this guide and they all check out:

  • Domain 26, USDC 0x3600000000000000000000000000000000000000, and the four V2 contract addresses (TokenMessengerV2 0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA, MessageTransmitterV2 0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275, TokenMinterV2 0xb43db544E2c27092c107639Ad201b3dEfAbcF192, MessageV2 0xbaC0179bB358A8936169a63408C8481D582390C4) match Circle's evm-smart-contracts reference and the official Arc skill values.
  • Selectors are correct by construction: cast sig gives V2 depositForBurn(uint256,uint32,bytes32,address,bytes32,uint256,uint32) = 0x8e0250ee and V1 depositForBurn(uint256,uint32,bytes32,address) = 0x6fd3504e.
  • The minFinalityThreshold 1000 (fast) / 2000 (finalized) semantics match the technical guide.

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 10011999 also round up to 2000. Might be worth rephrasing to avoid implying e.g. 1500 stays 1500.

The gas-limit (600k) and USDC-system-address gotchas raised on #110 are already covered here. LGTM.

@ZhiyuCircle ZhiyuCircle added documentation Improvements or additions to documentation ecosystem Component: ecosystem labels Jul 23, 2026
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.
@zkasuran

Copy link
Copy Markdown
Author

Good catch, fixed in 428dd65.

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.

@osr21

osr21 commented Jul 29, 2026

Copy link
Copy Markdown

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 trap

The guide correctly states USDC on Arc lives at 0x3600000000000000000000000000000000000000, but there is a related trap worth calling out: Arc's native gas accounting for USDC uses 18-decimal precision internally, but the ERC-20 decimals() call on that address returns 6, consistent with USDC everywhere else. Any CCTP amount calculation must use the 6-decimal ERC-20 interface, not the native-gas precision. Example: bridging 1 USDC → amount = 1_000_000n (6 decimals), not 1_000_000_000_000_000_000n.


mintRecipient must be left-padded to 32 bytes

mintRecipient is typed bytes32, not address. Ethers.js and viem do not auto-pad it to 32 bytes the way they would for an address parameter. Passing a bare 20-byte hex address silently packs garbage into the leading bytes and the mint either reverts or goes to the wrong address. Pad explicitly:

// ethers.js v6
const mintRecipient = ethers.zeroPadValue(recipientAddress, 32);

// viem
const mintRecipient = pad(recipientAddress as Hex, { size: 32 });

Attestation timing (real-world, testnet)

From live polling in production:

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation ecosystem Component: ecosystem

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs: CCTP V2 integration on Arc Testnet — domain 26, minFinalityThreshold 2000, V1 selector incompatible

4 participants