Skip to main content
CCTP for non-USDC enforces two independent address-screening layers on every non-USDC crosschain transfer. Both must clear before a transfer succeeds. They differ in scope, in who manages them, and in where they execute.

Layer 1: service-level denylist

Every transfer on this blockchain—outbound and inbound, regardless of TokenManagerType—is screened against an IDenylistProvider configured on the local CrossChainTokenService. The provider exposes a single view:
Solidity
  • Outbound: every payable transfer entrypoint screens both msg.sender and tx.origin. Denied addresses cause the call to revert before the burn or lock happens.
  • Inbound: the service screens the transfer recipient before delivering tokens. Denied recipients cause the inbound receiveMessage to revert with DenylistedAddress(recipient).
The provider is set by the service operator using setDenylistProvider. The check is fail-closed: if the provider’s staticcall reverts, the transfer reverts. This layer applies to every token on this blockchain. There is no opt-out at the token level.

Layer 2: per-token denylist on the CrossChainToken

Each CrossChainToken (CCT) deployed as NATIVE_CROSSCHAIN_TOKEN can carry its own IDenylistProvider. The CCT owner sets it by calling updateDenylistProvider. Setting address(0) removes per-token screening; the service-level layer still applies. The check runs inside the ERC-20 _update and screens both from and to on every transfer, mint, and burn. transferFrom additionally screens the spender at the entrypoint. Denied accounts cause AccountDenylisted(account) to revert. Layer 2 exists only for protocol-deployed tokens. BURN_MINT and LOCK_UNLOCK configurations of pre-existing ERC-20 tokens do not have this layer; whatever screening the underlying token implements applies independently.

Initial provider on a new CrossChainToken

When the service deploys a CrossChainToken, the new contract’s initial denylistProvider value depends on the deploy path: In other words: ownerless crosschain tokens get Circle’s screening list out of the box on every remote domain. Custom tokens always start with no per-token screening; the issuer opts in by calling updateDenylistProvider themselves.

Comparison

Composition

The two layers are independent. Layer 1 fires at the service boundary; layer 2 fires inside the CrossChainToken’s _update. A transfer must clear both. Layer 2 has a single provider slot. Setting a new provider replaces the previous one—it does not chain. An owner who wants both Circle’s screening and a custom list must implement a composite provider that calls both upstream lists. The service-level layer at the boundary is unaffected by what the owner does on the CCT; layer 1 always runs.