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 toaddress(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
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
ConnectionType
Solidity
Errors
Integrators should monitor for these reverts:Sentinels
EXECUTE_SUCCESS
Solidity
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
WhendeployRemoteOwnerlessToken lands on a remote domain, the new
CrossChainToken’s denylistProvider is initialized to the service’s provider.
tokenManagerOwner, tokenManagerOperator, and related fields are bytes
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 withfinalityThresholdExecuted < 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.