NOTE: This repository represents an educational example to use a Chainlink system, product, or service and is provided to demonstrate how to interact with Chainlink’s systems, products, and services to integrate them into your own. This template is provided “AS IS” and “AS AVAILABLE” without warranties of any kind, it has not been audited, and it may be missing key checks or error handling to make the usage of the system, product or service more clear. Do not use the code in this example in a production environment without completing your own audits and application of best practices. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for unintended outputs that are generated due to errors in code.
Foundry scripts for deploying and managing cross-chain tokens using Chainlink CCIP.
-
Install Foundry
-
Install dependencies:
npm install
-
Create an encrypted Foundry keystore (if you don't have one already):
cast wallet import your_keystore_name --interactive
-
Set up environment variables in
.env. You can copy.env.exampleto.envand fill in your values:cp .env.example .env
# Keystore name (created via `cast wallet import`) KEYSTORE_NAME=your_keystore_name # RPC URLs ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc MANTLE_SEPOLIA_RPC_URL=your_mantle_sepolia_rpc # Etherscan API key (required only if you pass --verify to deployment scripts) ETHERSCAN_API_KEY=your_etherscan_api_key
-
Load environment variables:
source .env -
Build the project:
forge build
Configure token parameters in script/input/token.json (see Configuration Files section), or override any field with environment variables:
| Env var | Default (from token.json) |
|---|---|
TOKEN_NAME |
.name |
TOKEN_SYMBOL |
.symbol |
TOKEN_DECIMALS |
.decimals |
TOKEN_MAX_SUPPLY |
.maxSupply |
TOKEN_PRE_MINT |
.preMint |
TOKEN_PRE_MINT_RECIPIENT |
broadcaster (if TOKEN_PRE_MINT > 0) |
CCIP_ADMIN_ADDRESS |
msg.sender (broadcaster) |
# Deploy on Ethereum Sepolia
forge script \
script/deploy/DeployToken.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast \
--verify
# Deploy on Mantle Sepolia
forge script \
script/deploy/DeployToken.s.sol \
--rpc-url \
$MANTLE_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast \
--verifyNote:
--verifyrequiresETHERSCAN_API_KEYto be set (see Prerequisites). Etherscan API v2 supports all chains with a single key.
Optional: Set ROLES_RECIPIENT to grant mint/burn roles to a specific address (defaults to the deployer).
After each deployment, the token address is automatically saved to:
script/deployments/tokens/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-Token.json
The file uses the env var name as the key (e.g. ETHEREUM_SEPOLIA_TOKEN). If you need to retrieve the deployed address later, open the file — the key is the env var name and the value is the address, so you can copy both directly into an export command. The script/deployments/ directory is ignored by .gitignore — files are local to each user.
Set the token address so subsequent scripts can find it — choose one approach:
# Option A: export for the session (persists across all commands in the current terminal)
export ETHEREUM_SEPOLIA_TOKEN=0x...
export MANTLE_SEPOLIA_TOKEN=0x...
# Option B: inline alias per command (no export needed; applies to that one command only)
TOKEN=0x... forge script script/setup/ClaimAdmin.s.sol --rpc-url $ETHEREUM_SEPOLIA_RPC_URL --account $KEYSTORE_NAME --broadcastTokens are burned on source, minted on destination.
# Deploy pool on Ethereum Sepolia
forge script \
script/deploy/DeployBurnMintTokenPool.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast \
--verify
# Deploy pool on Mantle Sepolia
forge script \
script/deploy/DeployBurnMintTokenPool.s.sol \
--rpc-url \
$MANTLE_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast \
--verifyAfter each deployment, the pool address is automatically saved to:
script/deployments/token-pools/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-BurnMintTokenPool.json
The file records the pool address under {CHAIN_NAME_IDENTIFIER}_TOKEN_POOL and its bound token address under {CHAIN_NAME_IDENTIFIER}_TOKEN.
Optional: Set POOL_HOOKS=0x... to attach an AdvancedPoolHooks contract at deploy time. Set DECIMALS=<n> if your token does not implement the optional decimals() ERC20 function — the script will fall back to this value and fail if neither is available.
The script also attempts to call grantMintAndBurnRoles on the token to grant the pool mint and burn rights. If the token does not implement this function, the script will print instructions to grant the roles manually.
Use the following when you don't have burn/mint rights on the source chain token (e.g. it was issued by a third party). Two patterns are supported:
Token was originally issued on one chain; you control the token on the destination and can grant mint rights.
The ERC20LockBox is only needed on the chain where tokens are locked. The destination chain uses a standard BurnMintTokenPool, which requires mint/burn rights on the destination token.
# 1. Deploy ERC20LockBox first (pool address isn't known yet)
forge script \
script/deploy/DeployERC20LockBox.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast \
--verify
# Optional: add AUTHORIZED_CALLERS=<DEPLOYER_OR_TOKEN_ISSUER_EOA> to the command above to authorize initial liquidity deposits/withdrawals
# 2. Deploy LockRelease pool, passing the lockbox address from step 1
LOCK_BOX=<LOCKBOX_ADDRESS> \
forge script \
script/deploy/DeployLockReleaseTokenPool.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast \
--verify
# 3. Authorize the pool to call the lockbox (deposit/withdraw)
LOCK_BOX=<LOCKBOX_ADDRESS> \
ADD_ADDRESSES=<POOL_ADDRESS> \
forge script \
script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# 4. BurnMint pool on Mantle Sepolia (minting side)
forge script \
script/deploy/DeployBurnMintTokenPool.s.sol \
--rpc-url \
$MANTLE_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast \
--verifyToken already exists on both chains independently.
Each chain needs its own ERC20LockBox and LockReleaseTokenPool.
# 1. Deploy ERC20LockBox on Ethereum Sepolia
forge script \
script/deploy/DeployERC20LockBox.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast \
--verify
# 2. Deploy ERC20LockBox on Mantle Sepolia
forge script \
script/deploy/DeployERC20LockBox.s.sol \
--rpc-url \
$MANTLE_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast \
--verify
# Optional: add AUTHORIZED_CALLERS=<DEPLOYER_OR_TOKEN_ISSUER_EOA> to either or both commands below for initial liquidity management
# 3. Deploy LockRelease pool on Ethereum Sepolia, passing its lockbox address from step 1
LOCK_BOX=<ETHEREUM_SEPOLIA_LOCKBOX_ADDRESS> \
forge script \
script/deploy/DeployLockReleaseTokenPool.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast \
--verify
# 4. Authorize the Ethereum Sepolia pool on its lockbox
LOCK_BOX=<ETHEREUM_SEPOLIA_LOCKBOX_ADDRESS> \
ADD_ADDRESSES=<ETHEREUM_SEPOLIA_POOL_ADDRESS> \
forge script \
script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# 5. Deploy LockRelease pool on Mantle Sepolia, passing its lockbox address from step 2
LOCK_BOX=<MANTLE_SEPOLIA_LOCKBOX_ADDRESS> \
forge script \
script/deploy/DeployLockReleaseTokenPool.s.sol \
--rpc-url \
$MANTLE_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast \
--verify
# 6. Authorize the Mantle Sepolia pool on its lockbox
LOCK_BOX=<MANTLE_SEPOLIA_LOCKBOX_ADDRESS> \
ADD_ADDRESSES=<MANTLE_SEPOLIA_POOL_ADDRESS> \
forge script \
script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol \
--rpc-url \
$MANTLE_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcastThe LockReleaseTokenPool requires the ERC20LockBox at deploy time, and the lockbox must authorize the pool (via UpdateAuthorizedCallers) before it can deposit/withdraw tokens. The deployment order is always: lockbox → pool → authorize pool on lockbox.
LOCK_BOX is required and must be the address of a deployed ERC20LockBox for the token. Optional: Set POOL_HOOKS=0x... to attach an already-deployed AdvancedPoolHooks contract at deploy time. Set DECIMALS=<n> if your token does not implement the optional decimals() ERC20 function. When deploying the lockbox, you can optionally set AUTHORIZED_CALLERS (CSV or JSON array) to authorize addresses immediately — useful for authorizing the deployer or token issuer to deposit/withdraw liquidity initially.
Set the pool address so subsequent scripts can find it — choose one approach:
# Option A: export for the session (persists across all commands in the current terminal)
export ETHEREUM_SEPOLIA_TOKEN_POOL=0x...
export MANTLE_SEPOLIA_TOKEN_POOL=0x...
# Option B: inline alias per command (no export needed; applies to that one command only)
TOKEN_POOL=0x... forge script script/setup/SetPool.s.sol --rpc-url $ETHEREUM_SEPOLIA_RPC_URL --account $KEYSTORE_NAME --broadcastEach deployment is automatically saved:
- ERC20LockBox →
script/deployments/lock-boxes/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-LockBox.json— keys:LOCK_BOX,{CHAIN_NAME_IDENTIFIER}_TOKEN - LockReleaseTokenPool →
script/deployments/token-pools/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-LockReleaseTokenPool.json— keys:{CHAIN_NAME_IDENTIFIER}_TOKEN_POOL,LOCK_BOX,{CHAIN_NAME_IDENTIFIER}_TOKEN
To verify the lockbox address is correctly attached to the pool:
forge script script/configure/GetLockBox.s.sol --rpc-url $MANTLE_SEPOLIA_RPC_URL# On Ethereum Sepolia
forge script \
script/setup/ClaimAdmin.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# On Mantle Sepolia
forge script \
script/setup/ClaimAdmin.s.sol \
--rpc-url \
$MANTLE_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcastOptional: Set CCIP_ADMIN_ADDRESS=0x... to specify the token's current admin address (defaults to the EOA broadcasting the transaction).
# On Ethereum Sepolia
forge script \
script/setup/AcceptAdminRole.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# On Mantle Sepolia
forge script \
script/setup/AcceptAdminRole.s.sol \
--rpc-url \
$MANTLE_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast# Configure Ethereum Sepolia → Mantle Sepolia
DEST_CHAIN=MANTLE_SEPOLIA \
forge script \
script/setup/ApplyChainUpdates.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Configure Mantle Sepolia → Ethereum Sepolia
DEST_CHAIN=ETHEREUM_SEPOLIA \
forge script \
script/setup/ApplyChainUpdates.s.sol \
--rpc-url \
$MANTLE_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Configure Ethereum Sepolia → Solana Devnet (non-EVM destination)
DEST_CHAIN=SOLANA_DEVNET \
SOLANA_DEVNET_TOKEN_POOL=<SOLANA_TOKEN_POOL_ADDRESS> \
SOLANA_DEVNET_TOKEN=<SOLANA_TOKEN_ADDRESS> \
forge script \
script/setup/ApplyChainUpdates.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcastNon-EVM destinations: For non-EVM chains like Solana Devnet, supply the destination pool and token addresses via
{DEST_CHAIN}_TOKEN_POOLand{DEST_CHAIN}_TOKEN(e.g.SOLANA_DEVNET_TOKEN_POOL,SOLANA_DEVNET_TOKEN). These are base58-encoded addresses — not0x-prefixed EVM addresses. Rate limiting is not applicable for non-EVM destinations and is ignored.
This script is idempotent — if the destination chain is already configured on the pool, the existing config is removed and replaced automatically.
Rate limiting is disabled by default. To enable it, pass the capacity and rate — isEnabled is automatically set to true when either value is provided:
# Ethereum Sepolia → Mantle Sepolia: enable both directions
DEST_CHAIN=MANTLE_SEPOLIA \
OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \
INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
INBOUND_RATE_LIMIT_RATE=100000000000000000 \
forge script \
script/setup/ApplyChainUpdates.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Mantle Sepolia → Ethereum Sepolia: enable both directions
DEST_CHAIN=ETHEREUM_SEPOLIA \
OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \
INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
INBOUND_RATE_LIMIT_RATE=100000000000000000 \
forge script \
script/setup/ApplyChainUpdates.s.sol \
--rpc-url \
$MANTLE_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Enable outbound only (Sepolia → Mantle)
DEST_CHAIN=MANTLE_SEPOLIA \
OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \
forge script \
script/setup/ApplyChainUpdates.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Enable inbound only (Sepolia → Mantle)
DEST_CHAIN=MANTLE_SEPOLIA \
INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
INBOUND_RATE_LIMIT_RATE=100000000000000000 \
forge script \
script/setup/ApplyChainUpdates.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast| Env var | Required | Description |
|---|---|---|
DEST_CHAIN |
Yes | Destination chain name (e.g. MANTLE_SEPOLIA) |
TOKEN_POOL |
No | Inline alias for the source chain pool address. Takes priority over {CHAIN}_TOKEN_POOL. |
DEST_TOKEN_POOL |
No | Inline alias for the destination chain pool address. Takes priority over {DEST_CHAIN}_TOKEN_POOL. |
DEST_TOKEN |
No | Inline alias for the destination chain token address. Takes priority over {DEST_CHAIN}_TOKEN. |
OUTBOUND_RATE_LIMIT_CAPACITY |
No | Token bucket capacity for outbound transfers |
OUTBOUND_RATE_LIMIT_RATE |
No | Token bucket refill rate (tokens/second) for outbound transfers |
OUTBOUND_RATE_LIMIT_ENABLED |
No | Override isEnabled explicitly (true/false; defaults to true when CAPACITY or RATE are set) |
INBOUND_RATE_LIMIT_CAPACITY |
No | Token bucket capacity for inbound transfers |
INBOUND_RATE_LIMIT_RATE |
No | Token bucket refill rate (tokens/second) for inbound transfers |
INBOUND_RATE_LIMIT_ENABLED |
No | Override isEnabled explicitly (true/false; defaults to true when CAPACITY or RATE are set) |
Note:
ApplyChainUpdatesonly configures the standard finality rate limit bucket. To configure the fast finality bucket, runUpdateRateLimiterswithFAST_FINALITY=trueafter the lane is set up.
To read the list of supported chains and their remote pool addresses:
forge script script/setup/GetSupportedChains.s.sol --rpc-url $ETHEREUM_SEPOLIA_RPC_URL# On Ethereum Sepolia
forge script \
script/setup/SetPool.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# On Mantle Sepolia
forge script \
script/setup/SetPool.s.sol \
--rpc-url \
$MANTLE_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcastThe following scripts are not required for the core deployment flow but are useful when handing off control to a multisig or a different EOA after initial setup. All token ownership scripts auto-detect the correct ownership pattern — no configuration needed.
Initiates an ownership transfer for a token, token pool, pool hooks, or lockbox. Use ENTITY_TYPE to specify which entity to transfer, and ADDRESS to specify the contract address. If ENTITY_TYPE is omitted, the contract is treated as a generic IOwnable — the same path used for tokenPool, poolHooks, and lockBox.
For token transfers, the script auto-detects the token type and calls the appropriate function:
| Detection | Token type | Transfer action | Accept required? |
|---|---|---|---|
pendingDefaultAdmin() succeeds |
CrossChainToken | beginDefaultAdminTransfer |
Yes — run AcceptOwnership |
pendingOwner() + owner() succeed |
OZ Ownable2Step |
transferOwnership |
Yes — run AcceptOwnership |
owner() only (no pendingOwner()) |
ConfirmedOwner or plain Ownable |
transferOwnership |
Yes for ConfirmedOwner; plain Ownable transfers immediately |
| Neither | BurnMintERC20 v1 (plain AccessControl) |
grantRole + revokeRole (atomic, 1-step) |
No |
For tokenPool, poolHooks, and lockBox: uses Chainlink's ConfirmedOwner (two-step) — always requires AcceptOwnership.
Step 1 — initiate (run as current owner/admin):
# Omit ENTITY_TYPE to use the generic IOwnable path (works for any tokenPool/poolHooks/lockBox)
ADDRESS=0xYourPool NEW_OWNER=0xNewOwner \
forge script \
script/setup/transfer-ownership/TransferOwnership.s.sol \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
--account $KEYSTORE_NAME \
--broadcast
# Or specify ENTITY_TYPE for a named label in the output
# Token pool
ENTITY_TYPE=tokenPool ADDRESS=0xYourPool NEW_OWNER=0xNewOwner \
forge script \
script/setup/transfer-ownership/TransferOwnership.s.sol \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
--account $KEYSTORE_NAME \
--broadcast
# Token
ENTITY_TYPE=token ADDRESS=0xYourToken NEW_OWNER=0xNewOwner \
forge script \
script/setup/transfer-ownership/TransferOwnership.s.sol \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
--account $KEYSTORE_NAME \
--broadcast
# Pool hooks
ENTITY_TYPE=poolHooks ADDRESS=0xYourHooks NEW_OWNER=0xNewOwner \
forge script \
script/setup/transfer-ownership/TransferOwnership.s.sol \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
--account $KEYSTORE_NAME \
--broadcast
# LockBox
ENTITY_TYPE=lockBox ADDRESS=0xYourLockBox NEW_OWNER=0xNewOwner \
forge script \
script/setup/transfer-ownership/TransferOwnership.s.sol \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
--account $KEYSTORE_NAME \
--broadcastStep 2 — accept (run as NEW_OWNER):
# Omit ENTITY_TYPE to use the generic IOwnable path
ADDRESS=0xYourPool \
forge script \
script/setup/transfer-ownership/AcceptOwnership.s.sol \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
--account $KEYSTORE_NAME \
--broadcast
# Or specify ENTITY_TYPE for a named label in the output
# Token pool
ENTITY_TYPE=tokenPool ADDRESS=0xYourPool \
forge script \
script/setup/transfer-ownership/AcceptOwnership.s.sol \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
--account $KEYSTORE_NAME \
--broadcast
# Token
ENTITY_TYPE=token ADDRESS=0xYourToken \
forge script \
script/setup/transfer-ownership/AcceptOwnership.s.sol \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
--account $KEYSTORE_NAME \
--broadcast
# Pool hooks
ENTITY_TYPE=poolHooks ADDRESS=0xYourHooks \
forge script \
script/setup/transfer-ownership/AcceptOwnership.s.sol \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
--account $KEYSTORE_NAME \
--broadcast
# LockBox
ENTITY_TYPE=lockBox ADDRESS=0xYourLockBox \
forge script \
script/setup/transfer-ownership/AcceptOwnership.s.sol \
--rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
--account $KEYSTORE_NAME \
--broadcastFor
BurnMintERC20v1, step 2 exits early — the transfer was already atomic. For plainOwnable, step 1 completes immediately — step 2 will revert on-chain.
Initiates a transfer of the CCIP token admin role to a new address. This is step 1 of a two-step process — the new admin must run AcceptAdminRole to complete it.
NEW_ADMIN=0xNewAdminAddress \
forge script \
script/setup/TransferTokenAdminRole.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcastOptional: Set TOKEN=0x... to override the token address (defaults to the {CHAIN}_TOKEN env var).
forge script \
script/operations/MintTokens.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcastOptional: Set AMOUNT to override the amount to mint (defaults to tokenAmountToMint from script/input/token.json). Set MINT_RECEIVER to mint to a different address (defaults to the EOA broadcasting the transaction).
Install ccip-cli globally if not already installed:
npm install -g @chainlink/ccip-cliMinimum version: This README assumes
@chainlink/ccip-cli >= 1.5.0(supports--extra finality=...). Verify your version with:ccip-cli --version
First, export the router address for the source chain — find it in HelperConfig.s.sol or the CCIP Directory, for example:
export ETHEREUM_SEPOLIA_ROUTER=0x...# Receiver address
export RECEIVER=0xYourReceiverAddress
# The Foundry scripts use smallest-unit amounts (wei-like). In the ccip-cli example below, we use a compact, human-readable token amount
ccip-cli send \
--source ethereum-testnet-sepolia \
--router $ETHEREUM_SEPOLIA_ROUTER \
--dest ethereum-testnet-sepolia-mantle-1 \
--transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1.23 \
--receiver $RECEIVER \
--wallet foundry:$KEYSTORE_NAMEFinality options (--extra finality=...):
finality=finalized(default): Wait for full finality.finality=safe: Use Fast Confirmation Rule (wait for thesafehead).finality=<blockDepth>: Wait for N block confirmations (example:finality=5).
Omit -x finality=<n> to use default finality. When set, <n> must be greater than or equal to the pool's configured block depth (see Set Finality Config). Pass --fee-token LINK to pay fees with LINK (defaults to the native network token).
See the CCIP CLI docs for more details.
Manually deposit tokens into an ERC20LockBox. Useful for token issuers managing liquidity.
LOCK_BOX=0x... \
forge script \
script/operations/DepositToLockBox.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcastOptional: Set AMOUNT to override the amount to deposit (defaults to tokenAmountToTransfer from script/input/token.json). Requires the broadcaster to be an authorized caller on the lockbox.
Manually withdraw tokens from an ERC20LockBox. Useful for token issuers managing liquidity.
LOCK_BOX=0x... \
forge script \
script/operations/WithdrawFromLockBox.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcastBy default, withdraws the entire lockbox balance. Optional: Set AMOUNT to withdraw a specific amount instead. Set RECIPIENT=0x... to send withdrawn tokens to a different address (defaults to broadcaster). Requires the broadcaster to be an authorized caller on the lockbox.
Inspect the fee token balances held by a token pool. Run this before WithdrawFeeTokens — it prints each token's balance and a pre-filled withdrawal command for any non-zero tokens.
FEE_TOKENS="0xTokenA,0xTokenB" \
forge script \
script/operations/GetFeeTokenBalances.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URLWithdraws accrued fee token balances from a token pool to a specified recipient. Only callable by the pool owner or the designated fee admin.
Note: Pool-level fee accrual and withdrawal are introduced in TokenPool v2.0. If run against a v1 pool, the script will exit with an informative message and suggest using FeeQuoter instead.
This script makes no assumptions about which token(s) have accumulated as fees — you must explicitly specify them via FEE_TOKENS. Accepts a comma-separated list or JSON array.
# Single fee token
RECIPIENT=0xYourAddress \
FEE_TOKENS="0xTokenThatAccruedFees" \
forge script \
script/operations/WithdrawFeeTokens.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Multiple fee tokens
RECIPIENT=0xYourAddress \
FEE_TOKENS="0xFirstFeeToken,0xSecondFeeToken" \
forge script \
script/operations/WithdrawFeeTokens.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast| Env var | Required | Description |
|---|---|---|
FEE_TOKENS |
Yes | CSV or JSON array of ERC20 token addresses to withdraw |
RECIPIENT |
No | Address to receive the withdrawn fee tokens (defaults to the broadcaster) |
The pool token address is printed at runtime for reference so you can identify whether to include it in FEE_TOKENS.
Reads or updates the dynamic configuration on a token pool: the CCIP router, rate limit admin, and fee admin.
forge script script/configure/dynamic-config/GetDynamicConfig.s.sol --rpc-url $ETHEREUM_SEPOLIA_RPC_URLROUTER=0xYourRouterAddress \
forge script \
script/configure/dynamic-config/SetDynamicConfig.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast| Env var | Required | Description |
|---|---|---|
ROUTER |
No | The CCIP router address to set on the pool (default: current on-chain value) |
RATE_LIMIT_ADMIN |
No | Rate limit admin address (default: current on-chain value, then broadcaster) |
FEE_ADMIN |
No | Fee admin address (default: current on-chain value, then broadcaster). Set to address(0) to restrict fee withdrawal to the owner only |
Note: Requires TokenPool v2.0 or later. The finality config controls which fast finality modes are accepted for cross-chain transfers. Setting it to
WAIT_FOR_FINALITY(no env vars, the default) disables fast finality transfers.
forge script script/configure/finality-config/GetFinalityConfig.s.sol --rpc-url $ETHEREUM_SEPOLIA_RPC_URLBest practice: When enabling fast finality, consider configuring the fast finality bucket rate limits at the same time. If the fast finality bucket is not configured, fast finality transfers fall back to the standard finality bucket. Configuring it explicitly gives you isolated, independently tuned rate limits for fast finality transfers — useful when their volume or risk profile differs from standard finality transfers.
# Set block depth and configure the fast finality rate limit bucket:
BLOCK_DEPTH=5 \
DEST_CHAIN=MANTLE_SEPOLIA \
OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \
INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
INBOUND_RATE_LIMIT_RATE=100000000000000000 \
forge script \
script/configure/finality-config/SetFinalityConfig.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Set block depth only (no rate limit changes):
BLOCK_DEPTH=5 \
forge script \
script/configure/finality-config/SetFinalityConfig.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Set WAIT_FOR_SAFE mode and view current rate limits for a lane (no update):
WAIT_FOR_SAFE=true \
DEST_CHAIN=MANTLE_SEPOLIA \
forge script \
script/configure/finality-config/SetFinalityConfig.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Combine BLOCK_DEPTH and WAIT_FOR_SAFE (pool accepts either mode simultaneously):
BLOCK_DEPTH=5 \
WAIT_FOR_SAFE=true \
forge script \
script/configure/finality-config/SetFinalityConfig.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Reset to default finality (disables fast finality transfers):
forge script \
script/configure/finality-config/SetFinalityConfig.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcastWhen DEST_CHAIN is provided, the script logs the current rate limits before applying any changes, and the updated state after. Each direction is shown independently: the fast finality bucket is displayed for directions where it is enabled; the standard finality bucket (fallback) is displayed for directions where it is not.
| Env var | Required | Description |
|---|---|---|
BLOCK_DEPTH |
No | Number of block confirmations for fast finality (1–65535). Can be combined with WAIT_FOR_SAFE to allow both modes simultaneously. Omit both to reset to default finality. |
WAIT_FOR_SAFE |
No | Set to true to use the safe head for fast finality. Can be combined with BLOCK_DEPTH to allow both modes simultaneously. |
DEST_CHAIN |
No | Remote chain whose lane is queried/updated (e.g. MANTLE_SEPOLIA). Required when any rate limit var is set; if omitted, the rate limiter section is skipped entirely |
OUTBOUND_RATE_LIMIT_CAPACITY |
No | uint128, outbound token bucket capacity (fast finality bucket) |
OUTBOUND_RATE_LIMIT_RATE |
No | uint128, outbound token bucket refill rate (tokens/second) |
OUTBOUND_RATE_LIMIT_ENABLED |
No | Override isEnabled explicitly (true/false; defaults to true when CAPACITY or RATE are set) |
INBOUND_RATE_LIMIT_CAPACITY |
No | uint128, inbound token bucket capacity (fast finality bucket) |
INBOUND_RATE_LIMIT_RATE |
No | uint128, inbound token bucket refill rate (tokens/second) |
INBOUND_RATE_LIMIT_ENABLED |
No | Override isEnabled explicitly (true/false; defaults to true when CAPACITY or RATE are set) |
Remote pools represent the pool addresses registered on a given chain for each supported remote chain. When a pool is upgraded on a remote chain, the old address should be kept active until all inflight messages have completed, then removed.
DEST_CHAIN=MANTLE_SEPOLIA \
forge script \
script/configure/remote-pools/GetRemotePools.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URLUse after upgrading a pool on a remote chain. Both the old and new pool addresses can be active simultaneously to allow inflight messages to complete.
DEST_CHAIN=MANTLE_SEPOLIA \
REMOTE_POOL_ADDRESS=0xNewRemotePoolAddress \
forge script \
script/configure/remote-pools/AddRemotePool.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcastWarning: All inflight transactions from the removed pool will be rejected after removal. Ensure there are no inflight transactions before proceeding.
DEST_CHAIN=MANTLE_SEPOLIA \
REMOTE_POOL_ADDRESS=0xOldRemotePoolAddress \
forge script \
script/configure/remote-pools/RemoveRemotePool.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcastUse this script for enhanced security features like allowlists, CCV management, policy engine integration, and threshold-based validation.
Configure defaults in script/input/advanced-pool-hooks.json (see Configuration Files section), or override any field with environment variables:
| Env var | Type | Default (from advanced-pool-hooks.json) |
|---|---|---|
ALLOWLIST |
CSV or JSON array | .allowlist |
AUTHORIZED_CALLERS |
CSV or JSON array | .authorizedCallers |
THRESHOLD_AMOUNT |
uint256 | .thresholdAmount |
POLICY_ENGINE |
address | .policyEngine |
Important: The
allowlistEnabledflag is set immutably at deploy time based on whetherALLOWLISTis non-empty. If you deploy with an empty allowlist (the default), allowlist functionality is permanently disabled — subsequent calls toUpdateAllowListwill always revert. To enable allowlisting, pass at least one address viaALLOWLISTat deploy time.
forge script \
script/configure/allowlist/DeployAdvancedPoolHooks.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast \
--verifyAfter deployment, the hooks address is automatically saved to:
script/deployments/advanced-pool-hooks/{CHAIN_NAME_IDENTIFIER}/{timestamp}-AdvancedPoolHooks.json
The file records the address under the POOL_HOOKS key.
Then pass the hooks address as POOL_HOOKS when deploying a new token pool, or connect it to an existing pool via UpdateAdvancedPoolHooks.
Reads and displays the AdvancedPoolHooks contract address currently attached to a token pool.
forge script script/configure/allowlist/GetAdvancedPoolHooks.s.sol --rpc-url $ETHEREUM_SEPOLIA_RPC_URLTOKEN_POOL=0x... \
NEW_HOOK=0x... \
forge script \
script/configure/allowlist/UpdateAdvancedPoolHooks.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcastSupports CSV or JSON array.
# Via AdvancedPoolHooks
POOL_HOOKS=0x... \
ADD_ADDRESSES="0xAAA...,0xBBB..." \
forge script \
script/configure/allowlist/UpdateAllowList.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Remove addresses
POOL_HOOKS=0x... \
REMOVE_ADDRESSES=0xAAA... \
forge script \
script/configure/allowlist/UpdateAllowList.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcastIf
POOL_HOOKSis not set, the script falls back to calling directly on theTOKEN_POOL(v1 pools only).
POOL_HOOKS=0x... \
forge script \
script/configure/allowlist/GetAllowList.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAMEPOOL_HOOKS=0x... \
CHECK_ADDRESS=0x... \
forge script \
script/configure/allowlist/IsAllowListed.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAMEAuthorizedCallers is used in two places:
AdvancedPoolHooks— authorized callers are the token pools permitted to invoke the hooks.ERC20LockBox— authorized callers are theLockReleaseTokenPoolcontracts permitted to calldeposit/withdraw.
Both use the same scripts, passing either POOL_HOOKS=<hooksAddress> or LOCK_BOX=<lockBoxAddress>.
Supports CSV or JSON array.
# Add callers — AdvancedPoolHooks
POOL_HOOKS=0x... \
ADD_ADDRESSES="0xAAA...,0xBBB..." \
forge script \
script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Add callers — ERC20LockBox
LOCK_BOX=0x... \
ADD_ADDRESSES=0xAAA... \
forge script \
script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Remove callers — AdvancedPoolHooks
POOL_HOOKS=0x... \
REMOVE_ADDRESSES=0xAAA... \
forge script \
script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Remove callers — ERC20LockBox
LOCK_BOX=0x... \
REMOVE_ADDRESSES=0xAAA... \
forge script \
script/configure/authorized-callers/UpdateAuthorizedCallers.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast# AdvancedPoolHooks
POOL_HOOKS=0x... \
forge script \
script/configure/authorized-callers/GetAuthorizedCallers.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME
# ERC20LockBox
LOCK_BOX=0x... \
forge script \
script/configure/authorized-callers/GetAuthorizedCallers.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAMEReads and displays the current rate limiter state for a token pool lane. Compatible with both v1 and v2 pools.
DEST_CHAIN=MANTLE_SEPOLIA \
forge script \
script/configure/rate-limiter/GetCurrentRateLimits.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URLOptional: Set FAST_FINALITY=true to query the fast finality bucket (v2 pools only). Each direction is shown independently: the fast finality bucket is displayed where it is enabled; the standard finality bucket (fallback) is displayed where it is not.
Updates rate limiter configuration for a specific lane. Compatible with both v1 and v2 pools. The direction is inferred automatically from whichever OUTBOUND_* / INBOUND_* vars are set — no need to pass ENABLED separately. isEnabled defaults to true when CAPACITY or RATE are provided; pass ENABLED=false to explicitly disable.
# Enable both directions (ENABLED is optional — defaults to true when CAPACITY/RATE are set)
DEST_CHAIN=MANTLE_SEPOLIA \
OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \
INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
INBOUND_RATE_LIMIT_RATE=100000000000000000 \
forge script \
script/configure/rate-limiter/UpdateRateLimiters.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Disable outbound only
DEST_CHAIN=MANTLE_SEPOLIA \
OUTBOUND_RATE_LIMIT_ENABLED=false \
forge script \
script/configure/rate-limiter/UpdateRateLimiters.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Disable inbound only
DEST_CHAIN=MANTLE_SEPOLIA \
INBOUND_RATE_LIMIT_ENABLED=false \
forge script \
script/configure/rate-limiter/UpdateRateLimiters.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast
# Disable both directions
DEST_CHAIN=MANTLE_SEPOLIA \
OUTBOUND_RATE_LIMIT_ENABLED=false \
INBOUND_RATE_LIMIT_ENABLED=false \
forge script \
script/configure/rate-limiter/UpdateRateLimiters.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast| Env var | Required | Description |
|---|---|---|
DEST_CHAIN |
Yes | Remote chain whose lane is being updated |
OUTBOUND_RATE_LIMIT_CAPACITY |
To update outbound | Token bucket capacity for outbound transfers |
OUTBOUND_RATE_LIMIT_RATE |
To update outbound | Token bucket refill rate (tokens/second) for outbound transfers |
OUTBOUND_RATE_LIMIT_ENABLED |
No | Override isEnabled explicitly (true/false; defaults to true when CAPACITY or RATE are set) |
INBOUND_RATE_LIMIT_CAPACITY |
To update inbound | Token bucket capacity for inbound transfers |
INBOUND_RATE_LIMIT_RATE |
To update inbound | Token bucket refill rate (tokens/second) for inbound transfers |
INBOUND_RATE_LIMIT_ENABLED |
No | Override isEnabled explicitly (true/false; defaults to true when CAPACITY or RATE are set) |
FAST_FINALITY |
No | true to update the fast finality bucket instead of the standard finality bucket (v2 only, default: false) |
Token pools v2.0 and later allow token issuers to configure fee parameters directly on the pool, overriding FeeQuoter defaults. If run against a v1 pool, these scripts will exit with an informative message — on v1, fee configuration is managed entirely by FeeQuoter and must be requested from the Chainlink team.
Reads the raw stored fee configuration for a destination lane.
DEST_CHAIN=MANTLE_SEPOLIA \
forge script \
script/configure/fee-config/GetTokenTransferFeeConfig.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URLAll fee config env vars are optional — unset fields default to the current on-chain values, so you only need to pass the fields you want to change. When setting a fee config for the first time (no existing on-chain config), any unset fields default to 0.
DEST_CHAIN=MANTLE_SEPOLIA \
DEST_GAS_OVERHEAD=50000 \
DEST_BYTES_OVERHEAD=32 \
FINALITY_FEE_USD_CENTS=0 \
FAST_FINALITY_FEE_USD_CENTS=1000 \
FINALITY_TRANSFER_FEE_BPS=0 \
FAST_FINALITY_TRANSFER_FEE_BPS=1000 \
forge script \
script/configure/fee-config/UpdateTokenTransferFeeConfig.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcast| Env var | Required | Description |
|---|---|---|
DEST_CHAIN |
Yes | Remote chain to configure fees for |
DEST_GAS_OVERHEAD |
No | Gas overhead charged on the destination chain (must be > 0; defaults to current on-chain value) |
DEST_BYTES_OVERHEAD |
No | Data availability bytes overhead (defaults to current on-chain value) |
FINALITY_FEE_USD_CENTS |
No | Flat fee in 0.01 USD units for finality transfers (defaults to current on-chain value) |
FAST_FINALITY_FEE_USD_CENTS |
No | Flat fee in 0.01 USD units for fast finality transfers (defaults to current on-chain value) |
FINALITY_TRANSFER_FEE_BPS |
No | Fee in basis points deducted from the transferred amount for finality transfers [0–9999] (defaults to current on-chain value) |
FAST_FINALITY_TRANSFER_FEE_BPS |
No | Fee in basis points deducted from the transferred amount for fast finality transfers [0–9999] (defaults to current on-chain value) |
DISABLE |
No | Set to true to disable the fee config for this lane, reverting the OnRamp to FeeQuoter defaults (default: false) |
Disabling the fee config for a lane causes the OnRamp to fall back to FeeQuoter defaults.
DEST_CHAIN=MANTLE_SEPOLIA \
DISABLE=true \
forge script \
script/configure/fee-config/UpdateTokenTransferFeeConfig.s.sol \
--rpc-url \
$ETHEREUM_SEPOLIA_RPC_URL \
--account \
$KEYSTORE_NAME \
--broadcastEVM chains (source or destination):
- Ethereum Sepolia (
ETHEREUM_SEPOLIA) - Mantle Sepolia (
MANTLE_SEPOLIA)
Non-EVM chains (destination only):
Non-EVM chains can only be used as the destination chain in
ApplyChainUpdates— i.e. to register a non-EVM token pool on an EVM source chain. They cannot be used as source chains in this repo.
- Solana Devnet (
SOLANA_DEVNET)
Default token parameters are in script/input/token.json. Deployment fields can be overridden with env vars (see Step 1). tokenAmountToMint and tokenAmountToTransfer are used as defaults by the Token Operations scripts:
{
"name": "BnM Test",
"symbol": "BnM-T",
"decimals": 18,
"maxSupply": 0,
"preMint": 0,
"tokenAmountToMint": 1000000000000000000000,
"tokenAmountToTransfer": 1000000000000000000
}Default hooks parameters are in script/input/advanced-pool-hooks.json. All fields can be overridden with env vars (see Deploy Advanced Pool Hooks):
{
"allowlist": [],
"thresholdAmount": 0,
"policyEngine": "0x0000000000000000000000000000000000000000",
"authorizedCallers": []
}These are not stored in .env. After each deployment, run export in your terminal — or use the chain-agnostic TOKEN / TOKEN_POOL inline aliases (see CLI inline vars) to pass an address to a single command without exporting. Session exports last for the current terminal — re-export after opening a new one, or recover values from script/deployments/.
Note: Do not add these to
.env. If unset,vm.envOr()falls back toaddress(0)— scripts will revert when they receive it. If sourced from a stale.envvalue, scripts will silently target the wrong contract after a redeployment.
Token addresses — exported after Step 1: Deploy Token:
| Variable | Chain |
|---|---|
ETHEREUM_SEPOLIA_TOKEN |
Ethereum Sepolia |
MANTLE_SEPOLIA_TOKEN |
Mantle Sepolia |
Non-EVM destination token addresses — set before running Step 5: Apply Chain Updates when targeting a non-EVM chain. These are base58-encoded addresses, not 0x-prefixed:
| Variable | Chain |
|---|---|
SOLANA_DEVNET_TOKEN |
Solana Devnet |
Token pool addresses — exported after Step 2: Deploy Token Pools:
| Variable | Chain |
|---|---|
ETHEREUM_SEPOLIA_TOKEN_POOL |
Ethereum Sepolia |
MANTLE_SEPOLIA_TOKEN_POOL |
Mantle Sepolia |
Non-EVM destination token pool addresses — set before running Step 5: Apply Chain Updates when targeting a non-EVM chain. These are base58-encoded addresses, not 0x-prefixed:
| Variable | Chain |
|---|---|
SOLANA_DEVNET_TOKEN_POOL |
Solana Devnet |
Prepended directly to a single forge script command. They apply to that one invocation only and do not affect the shell environment. Each script section documents its own inline vars in full — this table is an index.
Note: Do not add these to
.env. The same stale-value problem applies: scripts check for zero/empty values to trigger fallback behavior, and a value sourced from.envwould silently suppress that.
| Variable | Documented in | Config file default |
|---|---|---|
TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS, TOKEN_MAX_SUPPLY, TOKEN_PRE_MINT, TOKEN_PRE_MINT_RECIPIENT, CCIP_ADMIN_ADDRESS |
Step 1: Deploy Token | script/input/token.json |
ROLES_RECIPIENT |
Step 1: Deploy Token | |
TOKEN |
Chain-agnostic inline alias for {CHAIN}_TOKEN. Accepted by all scripts that resolve a deployed token address. Takes priority over the session export. |
|
TOKEN_POOL |
Chain-agnostic inline alias for {CHAIN}_TOKEN_POOL. Accepted by all scripts that resolve a deployed pool address. Takes priority over the session export. |
|
DEST_TOKEN_POOL |
Destination-chain pool alias used by ApplyChainUpdates. Takes priority over {DEST_CHAIN}_TOKEN_POOL. |
|
DEST_TOKEN |
Destination-chain token alias used by ApplyChainUpdates. Takes priority over {DEST_CHAIN}_TOKEN. |
|
POOL_HOOKS |
Burn & Mint Pool, Lock & Release Pool, Manage Allowlist, Manage Authorized Callers | |
LOCK_BOX |
Lock & Release Pool, Deposit to LockBox, Withdraw from LockBox, Manage Authorized Callers | |
DECIMALS |
Burn & Mint Pool, Lock & Release Pool | |
AUTHORIZED_CALLERS |
Lock & Release Pool, Deploy Advanced Pool Hooks | script/input/advanced-pool-hooks.json (deploy only) |
CCIP_ADMIN_ADDRESS |
Step 3: Claim Admin | |
DEST_CHAIN |
Step 5: Apply Chain Updates, Manage Remote Pools, Manage Rate Limiters, Manage Finality Config, Manage Token Transfer Fee Config | |
OUTBOUND_RATE_LIMIT_CAPACITY, OUTBOUND_RATE_LIMIT_RATE, OUTBOUND_RATE_LIMIT_ENABLED |
Step 5: Apply Chain Updates, Manage Rate Limiters, Manage Finality Config | |
INBOUND_RATE_LIMIT_CAPACITY, INBOUND_RATE_LIMIT_RATE, INBOUND_RATE_LIMIT_ENABLED |
Step 5: Apply Chain Updates, Manage Rate Limiters, Manage Finality Config | |
FAST_FINALITY |
Manage Rate Limiters, Manage Finality Config | |
AMOUNT |
Mint Tokens, Deposit to LockBox, Withdraw from LockBox | script/input/token.json |
MINT_RECEIVER |
Mint Tokens | |
RECIPIENT |
Withdraw from LockBox, Withdraw Fee Tokens | |
FEE_TOKENS |
Get Fee Token Balances, Withdraw Fee Tokens | |
ROUTER, RATE_LIMIT_ADMIN, FEE_ADMIN |
Manage Dynamic Config | |
BLOCK_DEPTH, WAIT_FOR_SAFE |
Manage Finality Config | |
REMOTE_POOL_ADDRESS |
Manage Remote Pools | |
ALLOWLIST, THRESHOLD_AMOUNT, POLICY_ENGINE |
Deploy Advanced Pool Hooks | script/input/advanced-pool-hooks.json |
NEW_HOOK |
Connect Advanced Pool Hooks to a Token Pool | |
ENTITY_TYPE |
Transfer Ownership | |
ADDRESS |
Transfer Ownership | |
NEW_OWNER |
Transfer Ownership | |
NEW_ADMIN |
Transfer Token Admin Role | |
ADD_ADDRESSES, REMOVE_ADDRESSES |
Manage Allowlist, Manage Authorized Callers | |
CHECK_ADDRESS |
Manage Allowlist | |
DEST_GAS_OVERHEAD, DEST_BYTES_OVERHEAD, FINALITY_FEE_USD_CENTS, FAST_FINALITY_FEE_USD_CENTS, FINALITY_TRANSFER_FEE_BPS, FAST_FINALITY_TRANSFER_FEE_BPS, DISABLE |
Manage Token Transfer Fee Config |
Run the test suite with:
forge testNo configuration is needed: the fork tests default to public Ethereum Sepolia RPC endpoints (trying several in order, so a single unavailable provider does not fail the suite). To use a private or paid endpoint instead, set ETHEREUM_SEPOLIA_RPC_URL (the same variable used by the deployment scripts):
ETHEREUM_SEPOLIA_RPC_URL=<your-sepolia-rpc-url> forge testThe fork tests deploy the token and pool fixtures by running the repo's own deploy scripts, so they exercise the same code paths as the commands documented above.