Skip to content

smartcontractkit/docs-cct-foundry

Repository files navigation

CCIP Cross-Chain Token Deployment and Registration Scripts

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.

Prerequisites

  1. Install Foundry

  2. Install dependencies:

    npm install
  3. Create an encrypted Foundry keystore (if you don't have one already):

    cast wallet import your_keystore_name --interactive
  4. Set up environment variables in .env. You can copy .env.example to .env and 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
  5. Load environment variables:

    source .env
  6. Build the project:

    forge build

Deployment Flow

Step 1: Deploy Token (on both chains)

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 \
  --verify

Note: --verify requires ETHERSCAN_API_KEY to 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 --broadcast

Step 2: Deploy Token Pools (on both chains)

Burn & Mint Pool

Tokens 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 \
  --verify

After 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.

Lock & Release Pool

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:

Pattern A — Lock on Source, Mint on Destination

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 \
  --verify
Pattern B — Lock on Source, Release on Destination

Token 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 \
  --broadcast

The 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 --broadcast

Each 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

Step 3: Claim Admin (on both chains)

# 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 \
  --broadcast

Optional: Set CCIP_ADMIN_ADDRESS=0x... to specify the token's current admin address (defaults to the EOA broadcasting the transaction).

Step 4: Accept Admin Role (on both chains)

# 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

Step 5: Apply Chain Updates (configure cross-chain routes)

# 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 \
  --broadcast

Non-EVM destinations: For non-EVM chains like Solana Devnet, supply the destination pool and token addresses via {DEST_CHAIN}_TOKEN_POOL and {DEST_CHAIN}_TOKEN (e.g. SOLANA_DEVNET_TOKEN_POOL, SOLANA_DEVNET_TOKEN). These are base58-encoded addresses — not 0x-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: ApplyChainUpdates only configures the standard finality rate limit bucket. To configure the fast finality bucket, run UpdateRateLimiters with FAST_FINALITY=true after 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

Step 6: Set Pool (on both chains)

# 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 \
  --broadcast

Ownership Management (Optional)

The 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.

Transfer Ownership

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 \
  --broadcast

Step 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 \
  --broadcast

For BurnMintERC20 v1, step 2 exits early — the transfer was already atomic. For plain Ownable, step 1 completes immediately — step 2 will revert on-chain.

Transfer Token Admin Role

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 \
  --broadcast

Optional: Set TOKEN=0x... to override the token address (defaults to the {CHAIN}_TOKEN env var).

Token Operations

Mint Tokens

forge script \
  script/operations/MintTokens.s.sol \
  --rpc-url \
  $ETHEREUM_SEPOLIA_RPC_URL \
  --account \
  $KEYSTORE_NAME \
  --broadcast

Optional: 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).

Transfer Tokens Cross-Chain

Install ccip-cli globally if not already installed:

npm install -g @chainlink/ccip-cli

Minimum 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_NAME

Finality options (--extra finality=...):

  • finality=finalized (default): Wait for full finality.
  • finality=safe: Use Fast Confirmation Rule (wait for the safe head).
  • 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.

Deposit to LockBox

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 \
  --broadcast

Optional: 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.

Withdraw from 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 \
  --broadcast

By 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.

Get Fee Token Balances

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_URL

Withdraw Fee Tokens

Withdraws 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.

Optional Configuration

Manage Dynamic Config

Reads or updates the dynamic configuration on a token pool: the CCIP router, rate limit admin, and fee admin.

View Dynamic Config
forge script script/configure/dynamic-config/GetDynamicConfig.s.sol --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
Set Dynamic Config
ROUTER=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

Manage Finality Config

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.

View Finality Config
forge script script/configure/finality-config/GetFinalityConfig.s.sol --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
Set Finality Config

Best 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 \
  --broadcast

When 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)

Manage Remote Pools

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.

View Remote Pools
DEST_CHAIN=MANTLE_SEPOLIA \
  forge script \
  script/configure/remote-pools/GetRemotePools.s.sol \
  --rpc-url \
  $ETHEREUM_SEPOLIA_RPC_URL
Add a Remote Pool

Use 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 \
  --broadcast
Remove a Remote Pool

Warning: 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 \
  --broadcast

Deploy Advanced Pool Hooks

Use 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 allowlistEnabled flag is set immutably at deploy time based on whether ALLOWLIST is non-empty. If you deploy with an empty allowlist (the default), allowlist functionality is permanently disabled — subsequent calls to UpdateAllowList will always revert. To enable allowlisting, pass at least one address via ALLOWLIST at deploy time.

forge script \
  script/configure/allowlist/DeployAdvancedPoolHooks.s.sol \
  --rpc-url \
  $ETHEREUM_SEPOLIA_RPC_URL \
  --account \
  $KEYSTORE_NAME \
  --broadcast \
  --verify

After 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.

Get Advanced Pool Hooks

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_URL

Connect Advanced Pool Hooks to a Token Pool

TOKEN_POOL=0x... \
  NEW_HOOK=0x... \
  forge script \
  script/configure/allowlist/UpdateAdvancedPoolHooks.s.sol \
  --rpc-url \
  $ETHEREUM_SEPOLIA_RPC_URL \
  --account \
  $KEYSTORE_NAME \
  --broadcast

Manage Allowlist

Add/Remove Addresses

Supports 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 \
  --broadcast

If POOL_HOOKS is not set, the script falls back to calling directly on the TOKEN_POOL (v1 pools only).

View Current Allowlist
POOL_HOOKS=0x... \
  forge script \
  script/configure/allowlist/GetAllowList.s.sol \
  --rpc-url \
  $ETHEREUM_SEPOLIA_RPC_URL \
  --account \
  $KEYSTORE_NAME
Check if an Address Is Allowlisted
POOL_HOOKS=0x... \
  CHECK_ADDRESS=0x... \
  forge script \
  script/configure/allowlist/IsAllowListed.s.sol \
  --rpc-url \
  $ETHEREUM_SEPOLIA_RPC_URL \
  --account \
  $KEYSTORE_NAME

Manage Authorized Callers

AuthorizedCallers is used in two places:

  • AdvancedPoolHooks — authorized callers are the token pools permitted to invoke the hooks.
  • ERC20LockBox — authorized callers are the LockReleaseTokenPool contracts permitted to call deposit/withdraw.

Both use the same scripts, passing either POOL_HOOKS=<hooksAddress> or LOCK_BOX=<lockBoxAddress>.

Add/Remove Authorized Callers

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
View Current Authorized Callers
# 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_NAME

Manage Rate Limiters

View Current Rate Limits

Reads 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_URL

Optional: 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.

Update Rate Limits

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)

Manage Token Transfer Fee Config

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.

View Fee Config

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_URL
Set or Update Fee Config

All 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)
Disable Fee Config

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 \
  --broadcast

Supported Networks

EVM 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)

Configuration Files

Token Deployment Configuration

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
}

Advanced Pool Hooks Configuration

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": []
}

Environment Variable Reference

Session exports — export VAR=0x... after each deployment

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 to address(0) — scripts will revert when they receive it. If sourced from a stale .env value, 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

CLI inline vars — VAR=value forge script ...

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 .env would 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

Testing

Run the test suite with:

forge test

No 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 test

The 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.

About

Foundry scripts for deploying and managing cross-chain tokens using Chainlink CCIP

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors