Skip to main content
The following tables index contracts, types, errors, and sentinels, and document behaviors that aren’t obvious from reading the source. For contract addresses, see Contract Addresses.

Contracts

Types

QuoteClaim

Passed to every payable entrypoint to attach a signed fee quote and a refund address.
Solidity
  • signedQuote: version-prefixed, ABI-encoded fee quote returned by the Iris fee-quote endpoint (POST /v1/quote/cctpx/{tokenId}/{sourceDomain}/{destinationDomain}). Pass the bytes value verbatim.
  • refundAddress: receives fee refunds. Set to address(0) to opt out of refund attribution. Non-zero values are screened against the USDC denylist on collection; the call reverts if denied.

DeploymentParams

Passed when registering or deploying a token.
Solidity
Address fields are bytes rather than address so the struct can carry non-EVM addresses for non-EVM domains. Some deployments omit initialSupply from the onchain ABI (for example current sandbox registerCustomToken). Encoding a field the contract does not expect produces an empty revert. Always verify the live ABI or function selector before copying struct layouts from docs.

TokenManagerType

Solidity
See Token manager types for when each applies.

ConnectionType

Solidity
Distinguishes the two integration models. See Ownerless and custom tokens.

Errors

Integrators should monitor for these reverts:

Sentinels

EXECUTE_SUCCESS

Solidity
The value that executeWithCrossChainToken must return. The service verifies this on every hook call. Implement the public entrypoint on your receiver, restrict msg.sender to the local CrossChainTokenService, and return this sentinel. See Build a hook receiver. When the contracts are open-sourced, an optional CrossChainTokenExecutable base can wrap the same pattern (external entrypoint returns the sentinel; subclasses override an internal hook). Until then, implement the write ABI directly.

Non-obvious behaviors

These behaviors aren’t apparent from reading the source but commonly trip up integrators.

approve goes to the TokenManager, not the service

For BURN_MINT and LOCK_UNLOCK transfers, the source token is moved by the per-token TokenManager. ERC-20 approvals must target the TokenManager address (returned by resolveTokenManager(tokenId)), not the CrossChainTokenService.

destinationAddress is packed address bytes, not ABI-encoded

crossChainTransfer takes destinationAddress as bytes. On EVM, pass the raw 20-byte address (abi.encodePacked(recipient) / encodePacked(["address"], [recipient])). Do not use abi.encode / encodeAbiParameters([{ type: "address" }], …), which produces a 32-byte left-padded word and breaks destination delivery. This is separate from application hookData, which may still be ABI-encoded.

Custom tokenId uses customTokenDeploySalt

For custom tokens, the tokenId is crossChainTokenId(deployer, customTokenDeploySalt(deployer, salt)). Calling crossChainTokenId(deployer, salt) alone yields a different id. Native crosschain tokens use crossChainTokenId(deployer, salt) directly.

resolveTokenManager and resolveTokenAddress revert on miss

Both revert with TokenNotRegistered(tokenId) when the predicted address has no deployed code. They do not return address(0). To probe for existence without throwing, wrap the call in try/catch or check extcodesize on the deterministic CREATE3-predicted address.

CrossChainToken denylist provider replaces, it does not chain

CrossChainToken holds a single denylist provider slot. Calling updateDenylistProvider overwrites the previous value rather than adding to it. To combine multiple lists, deploy a composite provider that internally calls each upstream. The service-level denylist still runs in addition to the per-token layer.

Ownerless deploys inherit Circle’s denylist provider

When deployRemoteOwnerlessToken lands on a remote domain, the new CrossChainToken’s denylistProvider is initialized to the service’s provider. The fields are typed bytes instead of address so the same struct works for non-EVM domains in the future. On EVM, pack the address with abi.encodePacked(addr) / encodePacked(["address"], [addr]) (20 bytes). Do not use encodeAbiParameters([{ type: "address" }], [addr]), which ABI-pads to 32 bytes and is not equivalent. For an existing ERC-20 registered with registerCustomToken, pass empty bytes (0x) for tokenOwner and tokenMinter; grant the TokenManager minter role on the token in a separate step.

sourceAddress in hook callbacks is bytes

Same reason as the deployment params: the hook signature carries the source address as bytes so receivers can decode senders from non-EVM blockchains.

Hook execution can be reorged on fast transfers

When a hook executes with finalityThresholdExecuted < 2000, the source blockchain transaction is still pre-finality and could be reorged. Receivers that opt into fast hook execution must design their logic to tolerate rollback by deferring irreversible side-effects until finality is confirmed.

Function index

CrossChainTokenService

TokenManager

CrossChainToken

CrossChainTokenExecutable

Optional helper (when published). Integrators may instead implement the public entrypoint directly.