Layer 1: service-level denylist
Every transfer on this blockchain—outbound and inbound, regardless ofTokenManagerType—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.senderandtx.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
receiveMessageto revert withDenylistedAddress(recipient).
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 theCrossChainToken’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.