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.
| Risk | Setup |
|---|---|
| 🟢 Lowest | Multi-signature smart account (3-of-n or higher) with a timelock providing a propose, review, cancel window |
| 🟢 Low | Multi-signature smart account, 3-of-n or higher, without a timelock |
| 🟡 Medium | Smart contract with unclassified governance |
| 🔴 High | EOA (single private key control) |
| 🔴 Critical | EOA 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 withgetPool). - 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
applyChainUpdateswith 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:
- Enable both inbound and outbound rate limits on every production lane, and confirm the deployed values match what you intended.
- See Rate Limit Management and Inspect Current Rate Limits.
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:
- Give the rate limit admin role to signers who can execute within minutes. See Delegate rate limit changes to the rate limit admin role.
- The role can raise limits as well as lower them, so scope what your fast path can do.
- Prepare the logistics in advance: which lanes, which values for your deployed pool version (see Distinguish an intentional pause from a misconfiguration), who executes, and how the decision to lift it is made.
- Rehearse the runbook on a testnet lane regularly.
Strongly recommended
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.1or later, pause a lane by settingisEnabled: true,capacity: 0,rate: 0on the affected direction viasetChainRateLimiterConfig(orsetChainRateLimiterConfigsfor 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: 0with 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: truewhen pausing. A disabled bucket (which requirescapacity: 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:
- Read the guidance for each network before you enable a lane to it. See Network Specific and Service Limits.
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.0release reports1.5.1, and some releases report development stamps such as1.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.