5/5
## Crafting the `ConfigurePool.s.sol` Script for CCIP Token Pool Configuration This lesson guides you through creating a Foundry script, `ConfigurePool.s.sol`, designed to configure your deployed `RebaseTokenPool` contracts for Cross-Chain Interoperability Protocol (CCIP) operations. This script is executed *after* the initial deployment of token pools on both source and destination chains (typically handled by a script like `Deployer.s.sol`). Its primary function is to establish the connection details and rate-limiting parameters, enabling seamless cross-chain token transfers. This process mirrors the configuration logic you might have previously implemented within test environments. ### Core Concepts in Play Before diving into the script, let's review the key concepts involved: 1. **Foundry Scripts:** We'll leverage Foundry's scripting capabilities (`forge script`) to automate contract interactions. These scripts inherit from `forge-std/Script.sol` and use a `run()` function as their entry point. Foundry cheatcodes, such as `vm.startBroadcast()` and `vm.stopBroadcast()`, allow the script to send transactions as if they were initiated by the script runner's address. 2. **CCIP Token Pools:** These specialized smart contracts are central to CCIP. They manage the locking/releasing or burning/minting of tokens during cross-chain transfers. Crucially, they must be configured with information about their counterpart pools on other chains. 3. **Chain Configuration (`applyChainUpdates`):** The `TokenPool` contract exposes an `applyChainUpdates` function, which is the workhorse for our configuration. It accepts two main arguments: * An array of chain selectors to *remove* (this is often an empty array during initial setup). * An array of `TokenPool.ChainUpdate` structs, detailing the chains to *add* or update. 4. **The `ChainUpdate` Struct:** Defined within `TokenPool.sol`, this struct bundles all necessary information to link a local pool to a remote one: * `remoteChainSelector` (uint64): The unique identifier of the target blockchain. * `remotePoolAddresses` (bytes[]): An array of ABI-encoded addresses of the pool(s) on the remote chain. * `remoteTokenAddress` (bytes): The ABI-encoded address of the token contract on the remote chain. * `outboundRateLimiterConfig` (RateLimiter.Config): Configuration for rate-limiting tokens leaving the current pool. * `inboundRateLimiterConfig` (RateLimiter.Config): Configuration for rate-limiting tokens arriving at the current pool. 5. **Rate Limiting (`RateLimiter.Config`):** CCIP employs rate limiting to manage token flow and enhance security. The `RateLimiter.Config` struct, defined in the `RateLimiter.sol` library, specifies: * `isEnabled` (bool): A flag to activate or deactivate rate limiting. * `capacity` (uint128): The maximum token amount the "bucket" can hold. * `rate` (uint128): The rate (tokens per second) at which the bucket refills. 6. **ABI Encoding:** The `ChainUpdate` struct requires `remotePoolAddresses` and `remoteTokenAddress` to be `bytes` or `bytes[]` types, respectively. Therefore, `address` values must be converted to this format using `abi.encode()`. 7. **Separation of Deployment and Configuration:** This lesson follows a common and recommended pattern: contract deployment (creating instances) is handled by one script, while inter-contract state setup (configuration) is managed by a separate script, like the `ConfigurePool.s.sol` we are building. ### Building the `ConfigurePool.s.sol` Script Let's construct the script step-by-step. **1. File Creation and Initial Setup** First, create a new file named `ConfigurePool.s.sol` within your Foundry project's `script/` directory. Add the standard SPDX license identifier and Solidity pragma: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; ``` **2. Importing Necessary Contracts and Libraries** Next, import the required modules: `Script` from `forge-std`, `TokenPool` and its associated structs, and `RateLimiter` for its configuration struct. ```solidity import {Script} from "forge-std/Script.sol"; import {TokenPool} from "@ccip/contracts/src/v0.8/ccip/pools/TokenPool.sol"; import {RateLimiter} from "@ccip/contracts/src/v0.8/ccip/libraries/RateLimiter.sol"; ``` **3. Defining the Script Contract and `run` Function** Define a contract, `ConfigurePoolScript`, that inherits from `Script`. The core logic will reside in the `run` function. This function will accept parameters for all necessary configuration values, providing flexibility. ```solidity contract ConfigurePoolScript is Script { function run( address localPool, uint64 remoteChainSelector, address remotePool, address remoteToken, bool outboundRateLimiterIsEnabled, uint128 outboundRateLimiterCapacity, uint128 outboundRateLimiterRate, bool inboundRateLimiterIsEnabled, uint128 inboundRateLimiterCapacity, uint128 inboundRateLimiterRate ) public { // Configuration logic will go here } } ``` Passing rate limiter settings as parameters, rather than hardcoding, allows for easier adjustments per deployment or chain. **4. Preparing `ChainUpdate` Data within the `run` Function** Inside the `run` function, we'll prepare the data structure needed for the `applyChainUpdates` call. * **Start Transaction Broadcasting:** Begin by telling Foundry to broadcast subsequent state-changing calls as transactions. ```solidity vm.startBroadcast(); ``` * **Prepare ABI-Encoded Addresses:** The `ChainUpdate` struct expects the remote pool and token addresses to be ABI-encoded. ```solidity // Prepare remotePoolAddresses (needs to be bytes[]) bytes[] memory remotePoolAddresses_ = new bytes[](1); remotePoolAddresses_[0] = abi.encode(remotePool); // Prepare remoteTokenAddress (needs to be bytes) bytes memory remoteTokenAddress_ = abi.encode(remoteToken); ``` We use an array of size one for `remotePoolAddresses_` as we are typically configuring a single corresponding pool. * **Create and Populate the `ChainUpdate` Struct:** Instantiate an array for `ChainUpdate` structs (again, typically size one for a single remote chain configuration) and populate it. ```solidity TokenPool.ChainUpdate[] memory chainsToAdd = new TokenPool.ChainUpdate[](1); chainsToAdd[0] = TokenPool.ChainUpdate({ chainSelector: remoteChainSelector, // Renamed from remoteChainSelector for clarity within struct remotePoolAddresses: remotePoolAddresses_, remoteTokenAddress: remoteTokenAddress_, outboundRateLimiterConfig: RateLimiter.Config({ isEnabled: outboundRateLimiterIsEnabled, capacity: outboundRateLimiterCapacity, rate: outboundRateLimiterRate }), inboundRateLimiterConfig: RateLimiter.Config({ isEnabled: inboundRateLimiterIsEnabled, capacity: inboundRateLimiterCapacity, rate: inboundRateLimiterRate }) }); ``` **5. Calling `applyChainUpdates`** With the `chainsToAdd` data prepared, call `applyChainUpdates` on the `localPool` contract. Cast the `localPool` address to the `TokenPool` interface to access its functions. We'll provide an empty `uint64` array for the `chainsToRemove` parameter, as we are only adding/updating a configuration. ```solidity // Cast localPool address to TokenPool contract type TokenPool(localPool).applyChainUpdates( new uint64[](0), // Chains to remove (empty array) chainsToAdd // Chains to add/update ); ``` * **Stop Transaction Broadcasting:** Conclude the transaction block. ```solidity vm.stopBroadcast(); ``` The complete `run` function will look like this: ```solidity contract ConfigurePoolScript is Script { function run( address localPool, uint64 remoteChainSelector, address remotePool, address remoteToken, bool outboundRateLimiterIsEnabled, uint128 outboundRateLimiterCapacity, uint128 outboundRateLimiterRate, bool inboundRateLimiterIsEnabled, uint128 inboundRateLimiterCapacity, uint128 inboundRateLimiterRate ) public { vm.startBroadcast(); // Prepare remotePoolAddresses (needs to be bytes[]) bytes[] memory remotePoolAddresses_ = new bytes[](1); remotePoolAddresses_[0] = abi.encode(remotePool); // Prepare remoteTokenAddress (needs to be bytes) bytes memory remoteTokenAddress_ = abi.encode(remoteToken); TokenPool.ChainUpdate[] memory chainsToAdd = new TokenPool.ChainUpdate[](1); chainsToAdd[0] = TokenPool.ChainUpdate({ chainSelector: remoteChainSelector, remotePoolAddresses: remotePoolAddresses_, remoteTokenAddress: remoteTokenAddress_, outboundRateLimiterConfig: RateLimiter.Config({ isEnabled: outboundRateLimiterIsEnabled, capacity: outboundRateLimiterCapacity, rate: outboundRateLimiterRate }), inboundRateLimiterConfig: RateLimiter.Config({ isEnabled: inboundRateLimiterIsEnabled, capacity: inboundRateLimiterCapacity, rate: inboundRateLimiterRate }) }); // Cast localPool address to TokenPool contract type TokenPool(localPool).applyChainUpdates( new uint64[](0), // Chains to remove (empty array) chainsToAdd // Chains to add/update ); vm.stopBroadcast(); } } ``` ### Important Considerations * **Execution Order:** This `ConfigurePool.s.sol` script is intended to be run *after* the `RebaseTokenPool` contracts have been deployed on both the source and destination chains. * **Reciprocal Configuration:** For CCIP to function correctly, configuration must be reciprocal. You will need to run this script (or apply equivalent logic) on *both* participating chains, each pointing to the other as the remote chain with its respective pool and token details. * **Why `abi.encode()`?** The `TokenPool` contract's `applyChainUpdates` function, via the `ChainUpdate` struct, specifically expects remote pool and token addresses in a `bytes` (or `bytes[]`) format, not as raw `address` types. `abi.encode()` performs this necessary conversion. * **Foundry Compilation:** If your project involves complex CCIP contract dependencies or deep call stacks, you might encounter stack too deep errors during compilation. Using the `--via-ir` flag with Foundry (`forge build --via-ir` or `forge script --via-ir ...`) can often resolve these by leveraging the IR-based compilation pipeline. This `ConfigurePool.s.sol` script provides a robust and reusable method for configuring your CCIP token pools after their initial deployment, laying the groundwork for cross-chain token transfers.
ConfigurePool.s.sol
Script for CCIP Token Pool ConfigurationThis lesson guides you through creating a Foundry script, ConfigurePool.s.sol
, designed to configure your deployed RebaseTokenPool
contracts for Cross-Chain Interoperability Protocol (CCIP) operations. This script is executed after the initial deployment of token pools on both source and destination chains (typically handled by a script like Deployer.s.sol
). Its primary function is to establish the connection details and rate-limiting parameters, enabling seamless cross-chain token transfers. This process mirrors the configuration logic you might have previously implemented within test environments.
Before diving into the script, let's review the key concepts involved:
Foundry Scripts: We'll leverage Foundry's scripting capabilities (forge script
) to automate contract interactions. These scripts inherit from forge-std/Script.sol
and use a run()
function as their entry point. Foundry cheatcodes, such as vm.startBroadcast()
and vm.stopBroadcast()
, allow the script to send transactions as if they were initiated by the script runner's address.
CCIP Token Pools: These specialized smart contracts are central to CCIP. They manage the locking/releasing or burning/minting of tokens during cross-chain transfers. Crucially, they must be configured with information about their counterpart pools on other chains.
Chain Configuration (applyChainUpdates
): The TokenPool
contract exposes an applyChainUpdates
function, which is the workhorse for our configuration. It accepts two main arguments:
An array of chain selectors to remove (this is often an empty array during initial setup).
An array of TokenPool.ChainUpdate
structs, detailing the chains to add or update.
The ChainUpdate
Struct: Defined within TokenPool.sol
, this struct bundles all necessary information to link a local pool to a remote one:
remoteChainSelector
(uint64): The unique identifier of the target blockchain.
remotePoolAddresses
(bytes[]): An array of ABI-encoded addresses of the pool(s) on the remote chain.
remoteTokenAddress
(bytes): The ABI-encoded address of the token contract on the remote chain.
outboundRateLimiterConfig
(RateLimiter.Config): Configuration for rate-limiting tokens leaving the current pool.
inboundRateLimiterConfig
(RateLimiter.Config): Configuration for rate-limiting tokens arriving at the current pool.
Rate Limiting (RateLimiter.Config
): CCIP employs rate limiting to manage token flow and enhance security. The RateLimiter.Config
struct, defined in the RateLimiter.sol
library, specifies:
isEnabled
(bool): A flag to activate or deactivate rate limiting.
capacity
(uint128): The maximum token amount the "bucket" can hold.
rate
(uint128): The rate (tokens per second) at which the bucket refills.
ABI Encoding: The ChainUpdate
struct requires remotePoolAddresses
and remoteTokenAddress
to be bytes
or bytes[]
types, respectively. Therefore, address
values must be converted to this format using abi.encode()
.
Separation of Deployment and Configuration: This lesson follows a common and recommended pattern: contract deployment (creating instances) is handled by one script, while inter-contract state setup (configuration) is managed by a separate script, like the ConfigurePool.s.sol
we are building.
ConfigurePool.s.sol
ScriptLet's construct the script step-by-step.
1. File Creation and Initial Setup
First, create a new file named ConfigurePool.s.sol
within your Foundry project's script/
directory. Add the standard SPDX license identifier and Solidity pragma:
2. Importing Necessary Contracts and Libraries
Next, import the required modules: Script
from forge-std
, TokenPool
and its associated structs, and RateLimiter
for its configuration struct.
3. Defining the Script Contract and run
Function
Define a contract, ConfigurePoolScript
, that inherits from Script
. The core logic will reside in the run
function. This function will accept parameters for all necessary configuration values, providing flexibility.
Passing rate limiter settings as parameters, rather than hardcoding, allows for easier adjustments per deployment or chain.
4. Preparing ChainUpdate
Data within the run
Function
Inside the run
function, we'll prepare the data structure needed for the applyChainUpdates
call.
Start Transaction Broadcasting: Begin by telling Foundry to broadcast subsequent state-changing calls as transactions.
Prepare ABI-Encoded Addresses:
The ChainUpdate
struct expects the remote pool and token addresses to be ABI-encoded.
We use an array of size one for remotePoolAddresses_
as we are typically configuring a single corresponding pool.
Create and Populate the ChainUpdate
Struct:
Instantiate an array for ChainUpdate
structs (again, typically size one for a single remote chain configuration) and populate it.
5. Calling applyChainUpdates
With the chainsToAdd
data prepared, call applyChainUpdates
on the localPool
contract. Cast the localPool
address to the TokenPool
interface to access its functions. We'll provide an empty uint64
array for the chainsToRemove
parameter, as we are only adding/updating a configuration.
Stop Transaction Broadcasting: Conclude the transaction block.
The complete run
function will look like this:
Execution Order: This ConfigurePool.s.sol
script is intended to be run after the RebaseTokenPool
contracts have been deployed on both the source and destination chains.
Reciprocal Configuration: For CCIP to function correctly, configuration must be reciprocal. You will need to run this script (or apply equivalent logic) on both participating chains, each pointing to the other as the remote chain with its respective pool and token details.
Why abi.encode()
? The TokenPool
contract's applyChainUpdates
function, via the ChainUpdate
struct, specifically expects remote pool and token addresses in a bytes
(or bytes[]
) format, not as raw address
types. abi.encode()
performs this necessary conversion.
Foundry Compilation: If your project involves complex CCIP contract dependencies or deep call stacks, you might encounter stack too deep errors during compilation. Using the --via-ir
flag with Foundry (forge build --via-ir
or forge script --via-ir ...
) can often resolve these by leveraging the IR-based compilation pipeline.
This ConfigurePool.s.sol
script provides a robust and reusable method for configuring your CCIP token pools after their initial deployment, laying the groundwork for cross-chain token transfers.
An essential tutorial to Crafting the `ConfigurePool.s.sol` Script for CCIP Token Pool Configuration - Master building a Foundry script (`ConfigurePool.s.sol`) to configure deployed `TokenPool` contracts for CCIP operations. You'll learn to use `applyChainUpdates`, prepare `ChainUpdate` structs with rate limiters, and ABI-encode addresses for robust cross-chain setup.
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)
Web3 engineer, educator, and Cyfrin co-founder. Patrick's smart contract development and security courses have helped hundreds of thousands of engineers kickstarting their careers into web3.
Guest lecturers:
Last updated on July 10, 2025
Duration: 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)
Web3 engineer, educator, and Cyfrin co-founder. Patrick's smart contract development and security courses have helped hundreds of thousands of engineers kickstarting their careers into web3.
Guest lecturers:
Last updated on July 10, 2025