Cross-Chain Token (CCT) Operational Security (EVM)

This page covers the best practices the Chainlink team recommends for Cross-Chain Token (CCT) deployments on EVM chains. It is written for token developers who are enabling a token for CCIP or already operating one in production. Follow these practices for any CCT in production.

Guidance here assumes CCIP v1.6.1 token pools. You are responsible for assessing which items apply to your own deployment and risk profile.

Critical

Secure the roles that control your token and pools

The token administrator, the token pool owner, and any address holding mint or burn rights can change how your token moves across chains. Each of these roles is held by whatever address you assign to it:

  • an externally owned account (EOA),
  • a multi-signature smart account,
  • or another contract.

Risk: A single compromised private key is enough to redirect transfers, mint unbacked supply, or take control of your pools.

What should you do:

  • Assign contract ownership, the CCIP administrator role, and mint and burn rights to a multi-signature smart account such as a Safe Smart Account, using a threshold of at least 3-of-n, commensurate with the value at risk. We recommend a minimum of 5 signers to ensure sufficient decentralization and security.
  • Add a timelock so that critical actions may be reviewed and cancelled with a sufficient time buffer.
  • The multi-signature account and timelock should own the token, the pools, and any related contracts from day one, rather than being retrofitted after launch.
  • Remove departed or compromised signers promptly, and confirm routinely that every remaining signer still controls their key.
  • Distribute signers so a quorum is reachable quickly during an incident, rather than concentrating them in one team, location, or time zone.
  • Assign and transfer the administrator role through the TokenAdminRegistry. See Registration and Administration and the TokenAdminRegistry API reference.
  • For executing configuration changes from a multi-signature account, see Executing with a Multisig and Prerequisites and Permissions.
RiskSetup
🟢 LowestMulti-signature smart account (3-of-n or higher) with a timelock providing a propose, review, cancel window
🟢 LowMulti-signature smart account, 3-of-n or higher, without a timelock
🟡 MediumSmart contract with unclassified governance
🔴 HighEOA (single private key control)
🔴 CriticalEOA holding mint rights

Keep locked supply at or above remote minted supply

In a Lock and Mint architecture, tokens are locked in a LockRelease pool on the issuing chain and minted on remote chains. The locked balance is the only backing for that remote supply.

Risk: If locked supply falls below the total minted across remote chains, transfers back to the issuing chain fail and users cannot withdraw until the shortfall is covered.

What should you do:

  • Continuously compare the locked balance held by the pool on the issuing chain against the sum of total supply across every remote burn and mint chain.
  • Alert on the ratio between them, not only on the absolute balance, and set thresholds that trigger action well before the pool is exhausted.
  • See Tokens and the LockReleaseTokenPool API reference.

Configure both sides of every lane symmetrically

A lane works only when both pools work in tandem with each other. The configuration for the remote pool on the other end is applied independently on each chain, so the two sides can drift apart without any error being raised at the time of the change.

Risk: Mismatched configuration causes transfers to revert, or to become stuck in flight until the configuration is corrected.

What should you do:

  • On the source pool, confirm the configured remote pool addresses (getRemotePools) resolve to the actual destination pool address.
  • On the source pool, confirm the configured remote token address (getRemoteToken) matches the token the destination pool reports (getToken).
  • Confirm the destination chain has a pool registered against the token in the TokenAdminRegistry (setPool, verified with getPool).
  • Apply chain updates on both pools (applyChainUpdates), with matching chain selectors, before enabling traffic on the lane.
  • See Registration and Administration, the TokenPool API reference, and the TokenAdminRegistry API reference.

Keep siloed lanes isolated from non-hub chains

A siloed liquidity design isolates each remote chain's liquidity to a designated hub chain. Connecting a siloed lane to any network other than its hub breaks that isolation.

Risk: Cross-silo connections corrupt accounting and can result in stuck transfers or loss of funds.

What should you do:

  • Review every remote chain configured on a siloed pool and remove any chain that is not the designated hub, using applyChainUpdates with the chain selector in the removal list.

Account for decimal differences between chains

Token decimals are set independently on each chain where your token is deployed. CCIP converts amounts during transfer, and rate limit values are denominated in the token's smallest unit on the chain the bucket applies to.

Risk: Mismatched decimals can cause transfers to fail, or cause a rate limit to throttle at a value orders of magnitude away from the one you intended to set.

What should you do:

  • Use matching decimals on every chain where your token is deployed whenever you can.
  • Where decimals differ, configure every rate limit in the smallest unit of the chain that bucket applies to, and verify the resulting value against the amount you intended.
  • See Token Units and Decimals and Token Pools.

Do not let source outbound capacity exceed destination inbound capacity

Outbound capacity is consumed on the source chain when tokens are locked or burned. Inbound capacity is consumed on the destination chain when tokens are released or minted. They are separate buckets and are configured independently.

Risk: A transfer that passes the source outbound limit but exceeds the destination inbound limit is stuck in flight until inbound capacity becomes available.

What should you do:

  • Size destination inbound capacity to absorb the full outbound capacity of every source lane pointing at it, and re-verify the relationship after every change to either side.

  • See How Rate Limits Work and Update Rate Limits.

Enable rate limits on every production lane

Token pool rate limits are the primary in-protocol control over how much value can move on a lane within a given window. A disabled bucket applies no volume ceiling in that direction.

Risk: Without rate limits, a compromise elsewhere in your system can move the full transferable supply through CCIP before you are able to intervene.

What should you do:

Prepare an emergency pause path before you need it

Rate limit changes are your main lever during an incident, and they are only useful if you can execute them quickly. The owner path behind a multi-signature account and timelock is slow by design.

Risk: Without a prepared fast path, pausing a lane takes as long as your slowest governance process while an incident drains value in minutes.

What should you do:

Remove stale remote pool addresses after a migration

Zero-downtime pool upgrades temporarily configure more than one remote pool address for a chain so that in-flight messages from the previous pool can still be processed.

Risk: A deprecated pool left configured continues to be accepted as a message source, and may contain faulty or unintended logic.

What should you do:

  • Once all in-flight messages from the old pool have settled, remove the stale remote pool address with removeRemotePool.
  • Confirm there are no pending or failed transactions referencing the old pool in the CCIP Explorer before removing it.
  • Seen Upgradability and the TokenPool API reference.

Clear an unintended pending token administrator or owner

Transferring the token administrator or owner role is a two-step process. Until the proposed address accepts, the transfer sits pending and can be accepted at any time.

Risk: An address proposed in error, or one that is no longer trusted, can take the administrator or owner role whenever it chooses.

What should you do:

  • Cancel an unintended transfer by calling transferAdminRole(localToken, address(0)) on the TokenAdminRegistry. This clears the pending administrator without changing the active one. See the TokenAdminRegistry API reference.
  • Cancel an unintended transfer of the owner role by calling transferOwnership(address(0)) on the relevant contract. This clears the pending owner without changing the active one. See the Ownable API reference.

Restrict burn and mint rights to the active pool

Mint and burn permissions on your token determine who can change the supply. In a CCIP deployment, only the active token pool needs them for cross-chain operation.

Risk: Any additional address holding mint rights can create unbacked supply; any address holding burn rights can destroy user balances.

What should you do:

  • Enumerate every address holding mint or burn permissions and revoke each one that is not the active token pool or an owner-like administrative role you have deliberately retained. Re-run this audit after every pool migration. See Tokens.

Remove lanes pointing at chains the router does not support

A pool can be configured with a remote chain selector that the CCIP router does not currently serve.

Risk: The lane may appear configured while transfers revert. If that chain is later added to the router, the lane becomes live without a deliberate decision to enable it.

What should you do:

  • Validate every configured chain selector against the CCIP Directory, and remove any remote chain that was not intentionally configured.

Keep destGasOverhead consistent across lanes

destGasOverhead covers the gas your token pool consumes on the destination chain. It is part of the token transfer fee configuration that CCIP maintains for your token; for current pool versions (<2.0), non-default values are applied by Chainlink Labs on your behalf. Using a non-default value is justified only when your pool or token costs more to execute.

Risk: An insufficient value causes transfers to fail automatic execution and require manual execution. An unnecessarily high value causes users to overpay. Inconsistent values across lanes pointing at the same destination usually indicate a configuration error.

What should you do:

  • Test that token transfers execute automatically on every configured lane before going to production.
  • Include first-time receivers in that test. An account that has never held your token can cost more gas.
  • Use consistent values across lanes pointing at the same destination unless you have a documented reason not to.
  • See Token Pools and Network Specific.

Give inbound capacity headroom over outbound

Matching destination inbound capacity exactly to source outbound capacity leaves no margin. While a transfer is in flight, the destination bucket is also being consumed by other traffic and refilling at its own rate.

Risk: Transfers that arrive without enough inbound capacity require manual execution, or wait for the bucket to refill before they can be executed.

What should you do:

  • Set destination inbound capacity above the corresponding source outbound capacity, sized against the destination refill rate and the latency of the lane. See Common Scenarios.

Tune the refill rate for both availability and throttling

The refill rate determines how quickly a bucket returns to full capacity after being consumed.

Risk: A refill rate that is too slow blocks legitimate transfers for extended periods. One that is too fast fails to throttle abnormal flows.

What should you do:

  • Choose a refill rate that restores capacity fast enough for normal user activity but slow enough to constrain a sustained abnormal flow.
  • See Common Scenarios and How Rate Limits Work.
  • The exact numbers will vary upon network choices and traffic patterns for your specific protocol. It is the responsibility of the protocol to designate sensible numbers.

Distinguish an intentional pause from a misconfiguration

Understand how rate limit configurations differ across versions

Risk: An accidental pause blocks users indefinitely, and a pause applied with the wrong values either fails to stop transfers or removes rate limiting entirely.

What should you do:

  • On pools reporting 1.6.1 or later, pause a lane by setting isEnabled: true, capacity: 0, rate: 0 on the affected direction via setChainRateLimiterConfig (or setChainRateLimiterConfigs for several lanes at once). This blocks all transfers in that direction (an inbound pause intentionally holds in-flight transfers) until you restore normal values.
  • On these same pools, do not set rate: 0 with a non-zero capacity: validation accepts it, but once the bucket depletes, transfers fail with an arithmetic panic instead of a clean rate-limit error.
  • Keep isEnabled: true when pausing. A disabled bucket (which requires capacity: 0, rate: 0) turns rate limiting off entirely.
  • On pools reporting versions before 1.6.1, a full stop through rate limits is not possible: use the smallest accepted configuration, capacity: 2, rate: 1, and account for the small trickle that remains possible, which can be material for tokens with few decimals.
  • Record every intentional pause, including who applied it and the condition for lifting it, and review paused lanes on a fixed interval.

See Emergency Actions.

Review network-specific requirements before enabling a chain

Some networks have configuration requirements, gas behavior, RPC characteristics, or service limits that differ from the defaults.

Risk: A configuration that is correct on one chain can cause failed execution or an outage on another.

What should you do:

Settle in-flight transfers before reducing inbound capacity

Reducing a destination inbound capacity affects messages that are already in flight toward that chain, not only future ones.

Risk: In-flight transfers whose amounts exceed the new inbound capacity stall until an administrator intervenes or capacity refills.

What should you do:

  • Set the new outbound capacities on the source chains first.
  • Check the CCIP Explorer for in-flight messages whose amounts exceed the inbound capacities you intend to set.
  • Wait for those transfers to settle on their destination chains.
  • Then reduce the inbound capacities.

See Update Rate Limits.

Run the latest available TokenPool release

We recommend using our latest pool versions for improved security, reliability, and access to the latest features.

Risk: Operating on an older release exposes you to issues that may have been resolved on newer versions.

What should you do:

  • Confirm what you are running with typeAndVersion() on each pool, compare against the published releases of @chainlink/contracts-ccip, and upgrade when feasible.
  • The on-chain version string does not always match the package release: a pool built from the 1.6.0 release reports 1.5.1, and some releases report development stamps such as 1.6.x-dev. See Upgradability.

Shut down lanes that carry little or no traffic

Every enabled lane is an active path into and out of your token.

Risk: An unused lane contributes no benefit while remaining available during an incident on either of the chains it connects.

What should you do:

  • Review traffic per lane periodically and remove remote chains serving lanes that do not justify the exposure. Where possible, consolidate toward a hub-and-spoke topology rather than connecting every chain to every other chain.

Delegate rate limit changes to the rate limit admin role

The token pool owner can assign a separate rate limit admin (setRateLimitAdmin), an address permitted to change rate limits but nothing else about the pool configuration.

Risk: Rate limit changes are made relatively often, including under time pressure during an incident. Performing them with the pool owner key exposes your highest-privilege credential to routine use.

What should you do: Assign the rate limit admin role to a dedicated multi-party signer, or to a contract if you want to constrain it further to specific pre-defined adjustments. Keep the pool owner key reserved for changes that require it. See the TokenPool API reference and Prerequisites and Permissions.

Get help

If you are assessing an existing CCT deployment, or want a second opinion on your authority structure or rate limit configuration, contact Chainlink Labs. For the division of responsibilities between token developers, application developers, and Chainlink, see CCIP Service Responsibility.

Get the latest Chainlink content straight to your inbox.