Skip to main content
The TokenController contract provides token configuration management for CCTP, including mapping local tokens to remote tokens across domains and setting per-message burn limits. Contract: src/roles/TokenController.sol

Key Concepts

  • Token Pairs: Links between local tokens and their corresponding remote domain tokens
  • Burn Limits: Maximum amount of each token that can be burned per message
  • Token Controller Role: Authorized address that manages token configuration

State Variables

burnLimitsPerMessage

mapping(address => uint256) public burnLimitsPerMessage
Maps local token addresses to their maximum burn amount per message. Key: Local token address
Value: Maximum burn amount per message (in token’s smallest unit)
Usage: A burn limit of 0 means the token is not supported for burning.

remoteTokensToLocalTokens

mapping(bytes32 => address) public remoteTokensToLocalTokens
Maps remote domain and token pairs to local token addresses. Key: keccak256(abi.encodePacked(remoteDomain, remoteToken))
Value: Local token address
Usage: Enables validation and minting when receiving messages from remote domains.

_tokenController

address private _tokenController
Private state variable storing the address of the token controller.

Functions

linkTokenPair

function linkTokenPair(
    address localToken,
    uint32 remoteDomain,
    bytes32 remoteToken
) external onlyTokenController
Links a pair of local and remote tokens to be supported by the TokenMinter. Parameters:
  • localToken: Address of the local token
  • remoteDomain: Domain ID of the remote chain
  • remoteToken: Address of the token on the remote domain (as bytes32)
Requirements:
  • Caller must be the token controller
  • Remote token must not already be linked to a local token
Emits: TokenPairLinked(address localToken, uint32 remoteDomain, bytes32 remoteToken) Source: TokenController.sol:126 Note:
  • A remote token can only map to one local token
  • Multiple remote tokens can map to the same local token
  • Linking does not enable deposits (must also call setMaxBurnAmountPerMessage)

unlinkTokenPair

function unlinkTokenPair(
    address localToken,
    uint32 remoteDomain,
    bytes32 remoteToken
) external onlyTokenController
Removes the link between a local token and a remote token. Parameters:
  • localToken: Address of the local token
  • remoteDomain: Domain ID of the remote chain
  • remoteToken: Address of the token on the remote domain (as bytes32)
Requirements:
  • Caller must be the token controller
  • Token pair must currently be linked
Emits: TokenPairUnlinked(address localToken, uint32 remoteDomain, bytes32 remoteToken) Source: TokenController.sol:157 Note: Unlinking does not disable burning the local token (must separately call setMaxBurnAmountPerMessage with 0)

setMaxBurnAmountPerMessage

function setMaxBurnAmountPerMessage(
    address localToken,
    uint256 burnLimitPerMessage
) external onlyTokenController
Sets the maximum burn amount per message for a given local token. Parameters:
  • localToken: Address of the local token
  • burnLimitPerMessage: Maximum burn amount per message (0 to disable burning)
Requirements:
  • Caller must be the token controller
Emits: SetBurnLimitPerMessage(address indexed token, uint256 burnLimitPerMessage) Source: TokenController.sol:186 Important:
  • Burns exceeding this limit will revert
  • Mints do not respect this limit
  • Reducing the limit does not affect previously burned tokens (they can still be minted)
  • Setting to 0 disables burning for that token

tokenController

function tokenController() external view returns (address)
Returns the address of the token controller. Returns: Address of the current token controller Source: TokenController.sol:113

Internal Functions

_setTokenController

function _setTokenController(address newTokenController) internal
Internal function to set the token controller address. Parameters:
  • newTokenController: Address of the new token controller
Requirements:
  • New token controller must be non-zero address
Emits: SetTokenController(address tokenController) Source: TokenController.sol:202

_getLocalToken

function _getLocalToken(
    uint32 remoteDomain,
    bytes32 remoteToken
) internal view returns (address)
Gets the local token associated with a remote domain and token. Parameters:
  • remoteDomain: Domain ID of the remote chain
  • remoteToken: Address of the token on the remote domain (as bytes32)
Returns: Local token address, or zero address if not linked Source: TokenController.sol:217

_hashRemoteDomainAndToken

function _hashRemoteDomainAndToken(
    uint32 remoteDomain,
    bytes32 remoteToken
) internal pure returns (bytes32)
Hashes a remote domain and token address together. Parameters:
  • remoteDomain: Domain ID of the remote chain
  • remoteToken: Address of the token on the remote domain (as bytes32)
Returns: Keccak256 hash of packed remote domain and token Source: TokenController.sol:235

Modifiers

onlyTokenController

modifier onlyTokenController()
Restricts function access to the token controller only. Reverts: “Caller is not tokenController” if caller is not the token controller Source: TokenController.sol:82

onlyWithinBurnLimit

modifier onlyWithinBurnLimit(address token, uint256 amount)
Ensures that a burn operation does not exceed the per-message burn limit. Parameters:
  • token: Address of the token to burn
  • amount: Amount to burn
Reverts:
  • “Burn token not supported” if burn limit is 0
  • “Burn amount exceeds per tx limit” if amount exceeds limit
Source: TokenController.sol:98

Events

TokenPairLinked

event TokenPairLinked(
    address localToken,
    uint32 remoteDomain,
    bytes32 remoteToken
)
Emitted when a token pair is linked. Parameters:
  • localToken: Local token address
  • remoteDomain: Remote domain ID
  • remoteToken: Remote token address as bytes32

TokenPairUnlinked

event TokenPairUnlinked(
    address localToken,
    uint32 remoteDomain,
    bytes32 remoteToken
)
Emitted when a token pair is unlinked.

SetBurnLimitPerMessage

event SetBurnLimitPerMessage(
    address indexed token,
    uint256 burnLimitPerMessage
)
Emitted when a burn limit is set for a token. Parameters:
  • token: Local token address
  • burnLimitPerMessage: New burn limit per message

SetTokenController

event SetTokenController(address tokenController)
Emitted when the token controller address is set. Parameters:
  • tokenController: New token controller address

Usage Examples

Setting Up a New Token

// Step 1: Link token pair (as token controller)
// Link local USDC to Ethereum USDC
tokenController.linkTokenPair(
    0xLOCAL_USDC_ADDRESS,
    0, // Ethereum domain ID
    0x000000000000000000000000ETHEREUM_USDC_ADDRESS
);

// Step 2: Set burn limit to enable deposits
// Allow burning up to 1 million USDC per message
tokenController.setMaxBurnAmountPerMessage(
    0xLOCAL_USDC_ADDRESS,
    1_000_000 * 10**6 // 1M USDC (6 decimals)
);

Updating Configuration

// Reduce burn limit
tokenController.setMaxBurnAmountPerMessage(
    0xLOCAL_USDC_ADDRESS,
    100_000 * 10**6 // 100K USDC
);

// Temporarily disable burning (set to 0)
tokenController.setMaxBurnAmountPerMessage(
    0xLOCAL_USDC_ADDRESS,
    0
);

// Unlink token pair
tokenController.unlinkTokenPair(
    0xLOCAL_USDC_ADDRESS,
    0, // Ethereum domain ID
    0x000000000000000000000000ETHEREUM_USDC_ADDRESS
);

Checking Configuration

// Check burn limit
uint256 limit = tokenMinter.burnLimitsPerMessage(0xLOCAL_USDC_ADDRESS);

// Check if remote token is linked
bytes32 key = keccak256(abi.encodePacked(
    uint32(0), // Ethereum domain
    bytes32(uint256(uint160(0xETHEREUM_USDC_ADDRESS)))
));
address localToken = tokenMinter.remoteTokensToLocalTokens(key);

Security Considerations

  • Token controller should be a trusted address (e.g., multisig or DAO)
  • Burn limits should be set based on liquidity and risk assessment
  • A token pair can only be linked once (prevents conflicting mappings)
  • Minting is not limited by burn limits (asymmetric by design)
  • Setting burn limit to 0 effectively disables deposits for that token
  • Unlinking a token pair does not automatically disable burning
  • Remote tokens are stored as bytes32 to support non-EVM chains
  • Multiple remote tokens can map to the same local token (useful for wrapped tokens)

Integration with TokenMinter

The TokenMinter contract inherits from TokenController and uses these functions in:
  • depositForBurn(): Checks burn limit via onlyWithinBurnLimit modifier
  • handleReceiveMessage(): Uses _getLocalToken() to determine which token to mint
  • Administrative setup: Owner uses inherited functions to configure supported tokens

Remote Token Address Format

Remote tokens are represented as bytes32 to support both EVM and non-EVM chains:
// Convert EVM address to bytes32
bytes32 remoteToken = bytes32(uint256(uint160(evmAddress)));

// Extract EVM address from bytes32
address evmAddress = address(uint160(uint256(remoteToken)));
For non-EVM chains, the bytes32 format is used directly without conversion.

Build docs developers (and LLMs) love