0/5
## Chainlink CCIP: Configuring Token Pools in Tests Before executing cross-chain token transfers using Chainlink CCIP within your Hardhat or Foundry test environment, a crucial final configuration step is required. After deploying your tokens and their corresponding Token Pool contracts on each participating chain, you must explicitly configure these pools to recognize and interact with each other. This lesson details how to perform this configuration using the `applyChainUpdates` function. ## Understanding Token Pool Configuration The primary goal of configuring a Token Pool is to enable secure and reliable cross-chain token movements, whether using a Burn & Mint or Lock & Unlock mechanism. Each pool deployed on a specific chain needs to be explicitly informed about the destination chains it can send tokens to and receive tokens from. **When to Configure:** This configuration is essential *after* deploying the token and token pool contracts on both the source and destination chains but *before* attempting any cross-chain minting or transfer operations. **How to Configure:** The configuration is achieved by invoking the `applyChainUpdates` function on the `TokenPool` contract deployed on the local chain. This function updates the pool's internal registry, adding or removing supported remote chains and their associated contract addresses and parameters. ## The `applyChainUpdates` Function The `applyChainUpdates` function is the core mechanism for managing a Token Pool's knowledge of other chains. * **Source Contract:** This function is defined within the base `TokenPool` contract provided by Chainlink CCIP (`@ccip/contracts/src/v0.8/ccip/pools/TokenPool.sol`), which your specific pool implementations (like `RebaseTokenPool` in the example) typically inherit from. * **Access Control:** Critically, this function includes an `onlyOwner` modifier, meaning only the address designated as the owner during the `TokenPool` contract deployment can successfully call it. * **Function Signature:** ```solidity function applyChainUpdates( uint64[] calldata remoteChainSelectorsToRemove, TokenPool.ChainUpdate[] calldata chainsToAdd ) external virtual onlyOwner; ``` * **Arguments:** 1. `remoteChainSelectorsToRemove (uint64[])`: An array containing the unique blockchain identifiers (chain selectors) for any remote chains you wish to *remove* from the pool's supported list. For initial configuration, this array is typically empty (`new uint64[](0)`). 2. `chainsToAdd (TokenPool.ChainUpdate[])`: An array of `TokenPool.ChainUpdate` structs. Each struct in this array defines the configuration parameters for a *new* remote chain being added or updated in the pool's registry. ## Essential Data Structures: `ChainUpdate` and `RateLimiter.Config` To call `applyChainUpdates`, you need to construct the `chainsToAdd` array using the `TokenPool.ChainUpdate` struct. **`TokenPool.ChainUpdate` Struct:** This struct bundles the necessary information about a single remote chain configuration. * **Definition:** ```solidity struct ChainUpdate { uint64 remoteChainSelector; // Remote chain selector bytes remotePoolAddresses; // Address of the remote pool, ABI encoded bytes remoteTokenAddress; // Address of the remote token, ABI encoded RateLimiter.Config outboundRateLimiterConfig; // Outbound rate limited config RateLimiter.Config inboundRateLimiterConfig; // Inbound rate limited config } ``` * **Member Details:** * `remoteChainSelector`: The unique identifier for the destination chain you are enabling communication with. * `remotePoolAddresses`: The contract address of the corresponding `TokenPool` on the remote chain. **Crucially, this address must be ABI-encoded before being passed:** `abi.encode(remotePoolAddress)`. * `remoteTokenAddress`: The contract address of the corresponding Token contract on the remote chain. **This address must also be ABI-encoded:** `abi.encode(remoteTokenAddress)`. * `outboundRateLimiterConfig`: A `RateLimiter.Config` struct defining the rate limits for tokens flowing *out* of this local pool *to* the specified remote chain. * `inboundRateLimiterConfig`: A `RateLimiter.Config` struct defining the rate limits for tokens flowing *into* this local pool *from* the specified remote chain. **`RateLimiter.Config` Struct:** This struct, defined in `@ccip/contracts/src/v0.8/ccip/libraries/RateLimiter.sol`, controls the token transfer rate limits. * **Definition:** ```solidity struct Config { bool isEnabled; // Indication whether the rate limiting should be enabled uint128 capacity; // Specifies the capacity of the rate limiter (total bucket size) uint128 rate; // Specifies the rate of the rate limiter (refill rate per second) } ``` * **Usage in Example:** For the test scenario described, rate limiting is disabled. Both `outboundRateLimiterConfig` and `inboundRateLimiterConfig` within the `ChainUpdate` struct are populated with default values indicating disabled limits: ```solidity RateLimiter.Config({ isEnabled: false, capacity: 0, rate: 0 }) ``` ## Implementing Configuration in Hardhat/Foundry Tests Within a Hardhat/Foundry test file (e.g., `CrossChain.t.sol`), managing the configuration for multiple chains can be streamlined using a helper function. **Required Imports:** Ensure you import the necessary contracts and libraries: ```solidity import {TokenPool} from "@ccip/contracts/src/v0.8/ccip/pools/TokenPool.sol"; import {RateLimiter} from "@ccip/contracts/src/v0.8/ccip/libraries/RateLimiter.sol"; ``` **Helper Function: `configureTokenPool`** This function encapsulates the logic for configuring a single local pool to interact with one remote chain. * **Purpose:** To simplify the repetitive task of calling `applyChainUpdates` for each direction of the cross-chain interaction. * **Parameters:** * `fork (uint256)`: The identifier of the blockchain fork (e.g., Sepolia testnet fork) to switch the test environment's context to, using `vm.selectFork`. * `localPool (address)`: The address of the `TokenPool` contract on the chain currently being configured (the target of the `applyChainUpdates` call). * `remoteChainSelector (uint64)`: The chain selector ID of the *other* chain. * `remotePool (address)`: The address of the `TokenPool` contract on the *other* chain. * `remoteTokenAddress (address)`: The address of the Token contract on the *other* chain. * **Implementation:** ```solidity function configureTokenPool( uint256 fork, address localPool, uint64 remoteChainSelector, address remotePool, address remoteTokenAddress ) internal { // Switch to the context of the chain being configured vm.selectFork(fork); // Prepare the update data for the remote chain TokenPool.ChainUpdate[] memory chainsToAdd = new TokenPool.ChainUpdate[](1); chainsToAdd[0] = TokenPool.ChainUpdate({ remoteChainSelector: remoteChainSelector, // ABI encode remote addresses remotePoolAddresses: abi.encode(remotePool), remoteTokenAddress: abi.encode(remoteTokenAddress), // Disable rate limiting for the test outboundRateLimiterConfig: RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}), inboundRateLimiterConfig: RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}) }); // Prepare an empty array for removals (initial config) uint64[] memory chainsToRemove = new uint64[](0); // Impersonate the owner for the next call only vm.prank(owner); // Assumes 'owner' variable holds the pool owner address // Call the configuration function on the local pool TokenPool(localPool).applyChainUpdates(chainsToRemove, chainsToAdd); } ``` **Calling the Helper in `setUp`** After deploying all contracts in your test's `setUp` function, call the `configureTokenPool` helper twice – once for each direction of communication. This should typically be done while impersonating the contract owner using `vm.startPrank(owner)` and before `vm.stopPrank()`. 1. **Configure the Sepolia Pool:** Make the Sepolia pool aware of the Arbitrum Sepolia pool and token. ```solidity // Assuming sepoliaFork, sepoliaPool, arbSepoliaNetworkDetails.chainSelector, // arbSepoliaPool, and arbSepoliaToken are defined variables. configureTokenPool( sepoliaFork, // Switch to Sepolia context address(sepoliaPool), // Local pool to configure arbSepoliaNetworkDetails.chainSelector, // Remote chain ID (Arb Sepolia) address(arbSepoliaPool), // Remote pool address (Arb Sepolia) address(arbSepoliaToken) // Remote token address (Arb Sepolia) ); ``` 2. **Configure the Arbitrum Sepolia Pool:** Make the Arbitrum Sepolia pool aware of the Sepolia pool and token. ```solidity // Assuming arbSepoliaFork, arbSepoliaPool, sepoliaNetworkDetails.chainSelector, // sepoliaPool, and sepoliaToken are defined variables. configureTokenPool( arbSepoliaFork, // Switch to Arb Sepolia context address(arbSepoliaPool), // Local pool to configure sepoliaNetworkDetails.chainSelector, // Remote chain ID (Sepolia) address(sepoliaPool), // Remote pool address (Sepolia) address(sepoliaToken) // Remote token address (Sepolia) ); ``` *(Note: Casting contract variables like `sepoliaPool` to `address` using `address(...)` is often necessary when passing them as arguments).* ## Verification and Next Steps After implementing this configuration logic within your `setUp` function, run your test suite's build command (e.g., `forge build`) to ensure the code compiles without errors. With the token pools now correctly configured to recognize each other, you are ready to proceed to the next stage: writing the actual test functions that simulate sending CCIP messages to trigger cross-chain minting and transfers.
A crucial guide to Chainlink CCIP: Configuring Token Pools in Tests - Learn the vital configuration step after deploying Token Pools but before testing cross-chain transfers. Understand how to use `applyChainUpdates` in Hardhat/Foundry to link pools across chains.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Guest lecturers:
Juliette Chevalier
Lead Developer relations at Aragon
Nader Dabit
Director of developer relations at EigenLayer
Ally Haire
Developer relations at Protocol Labs
Harrison
Founder at GasliteGG
Last updated on April 4, 2025
Solidity Developer
Advanced FoundryDuration: 36min
Duration: 3h 06min
Duration: 5h 02min
Duration: 6h 02min
Duration: 2h 47min
Duration: 1h 23min
Duration: 4h 28min
Duration: 1h 19min
Duration: 1h 10min
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Guest lecturers:
Juliette Chevalier
Lead Developer relations at Aragon
Nader Dabit
Director of developer relations at EigenLayer
Ally Haire
Developer relations at Protocol Labs
Harrison
Founder at GasliteGG
Last updated on April 4, 2025
Testimonials
Read what our students have to say about this course.
Chainlink
Chainlink
Gustavo Gonzalez
Solutions Engineer at OpenZeppelin
Francesco Andreoli
Lead Devrel at Metamask
Albert Hu
DeForm Founding Engineer
Radek
Senior Developer Advocate at Ceramic
Boidushya
WalletConnect
Idris
Developer Relations Engineer at Axelar