Disclaimer
This repository contains example code of how a Chainlink product or service can be used. It is provided solely to demonstrate a potential integration approach and is not intended for production. This repository is provided "AS IS" without warranties of any kind, has not been audited, may be incomplete, and may be missing key checks or error handling mechanisms. You are solely responsible for testing and simulating all code and transactions, validating functionality on testnet environments, and conducting comprehensive security, technical, and engineering reviews before deploying anything to any mainnet or production environments. SmartContract Chainlink Limited SEZC (“Chainlink Labs”) disclaims all liability for any loss or damage arising from or related to your use of or reliance on this repository. Chainlink Labs does not represent or warrant that the repository will be uninterrupted, available at any particular time, or error-free.
A CLI tool to generate calldata for CCIP TokenPool contract interactions. Supports deploying tokens and pools across EVM chains, configuring cross-chain settings, and managing token permissions. Outputs raw calldata or Safe Transaction Builder JSON.
- What This Tool Does
- Prerequisites
- Installation
- Input File Reference
- Quick Start: Deploy Cross-Chain Token
- Finding Chain Selectors and Factory Addresses
- Commands
- Advanced Configuration
- Understanding CREATE2
- Output Formats
- Troubleshooting
- Development
- Project Structure
- Additional Resources
This CLI tool generates transactions in JSON format compatible with Safe wallet UI for Cross-Chain Token (CCT) operations. Projects using Safe multisig can upload the generated JSON files directly into their Safe Transaction Builder interface to execute operations securely.
Primary Use Case: Managing Cross-Chain Tokens (CCT) through Safe multisig wallets
Supported Operations:
- Deploy tokens and pools through TokenPoolFactory (uses CREATE2 for deterministic addresses)
- Register tokens in the Token Admin Registry
- Configure token pools for cross-chain transfers:
- Add/remove remote chain connections
- Configure rate limiters (transfer volume limits)
- Manage sender allow lists
- Grant/revoke roles (mint and burn permissions)
- Mint tokens for testing and operations
Output Format: Generates Safe Transaction Builder JSON files that can be imported directly into the Safe UI, or raw calldata for direct contract interaction.
- Node.js >= 22.0.0
- pnpm:
npm install -g pnpm - A Safe multisig wallet (for safe-json format)
- Chain selectors and factory addresses for your chains (see reference)
# Clone the repository
git clone https://github.com/smartcontractkit/token-pools-calldata.git
cd token-pools-calldata
# Install dependencies
pnpm install
# Verify installation
pnpm start --helpAll input files are in JSON format. Ready-to-use examples are in the examples/ directory.
| Field | Type | Description |
|---|---|---|
name |
string | Token name displayed in wallets and explorers |
symbol |
string | Token ticker symbol (e.g., ETH, USDC) |
decimals |
number | Decimal places. Use 18 for standard tokens, 6 for USDC-like |
maxSupply |
string | Maximum supply in wei (see conversion below) |
preMint |
string | Tokens minted to deployer on deployment, in wei |
remoteTokenPools |
array | Remote chain configs. Leave as [] for initial deployment |
Wei Conversion: Token amounts are specified in wei (smallest unit). Multiply the human-readable amount by 10^decimals:
| Human Amount | Decimals | Wei Value |
|---|---|---|
| 1,000,000 tokens | 18 | "1000000000000000000000000" |
| 100,000 tokens | 18 | "100000000000000000000000" |
| 1,000 tokens | 18 | "1000000000000000000000" |
| 1,000,000 tokens | 6 | "1000000000000" |
Format: [chainsToRemove, chainsToAdd]
| Field | Type | Description |
|---|---|---|
remoteChainSelector |
string | CCIP chain selector (uint64). See Finding Chain Selectors |
remotePoolAddresses |
string[] | Pool addresses on remote chain |
remoteTokenAddress |
string | Token address on remote chain |
outboundRateLimiterConfig |
object | Rate limits for tokens sent TO remote chain |
inboundRateLimiterConfig |
object | Rate limits for tokens received FROM remote chain |
remoteChainType |
string | "evm" for Ethereum-like chains, "svm" for Solana |
| Field | Type | Description |
|---|---|---|
isEnabled |
boolean | Enable/disable rate limiting |
capacity |
string | Maximum tokens transferable at once (wei) |
rate |
string | Bucket refill rate in tokens per second (wei) |
Capacity/Rate Values (for 18-decimal tokens):
| Use Case | Capacity | Rate | Capacity (wei) | Rate (wei) |
|---|---|---|---|---|
| Test/Dev | 100,000 tokens | 1,000/sec | "100000000000000000000000" |
"1000000000000000000000" |
| Low Volume | 1M tokens | 10,000/sec | "1000000000000000000000000" |
"10000000000000000000000" |
This guide deploys a cross-chain token between Base Sepolia and Ethereum Sepolia.
Gather these values - you'll use them in every command:
| Parameter | Description | Where to find |
|---|---|---|
YOUR_SAFE_ADDRESS |
Your Safe multisig address | Safe UI |
YOUR_OWNER_ADDRESS |
Address of the Safe owner signing transactions | Your wallet |
Chain Reference (used throughout this guide):
| Chain | Chain ID | Chain Selector | Factory Address |
|---|---|---|---|
| Base Sepolia | 84532 |
10344971235874465080 |
0xff170aD8f1d86eFAC90CA7a2E1204bA64aC5e0f9 |
| Ethereum Sepolia | 11155111 |
16015286601757825753 |
0xBCf47E9195A225813A629BB7580eDF338c2d8202 |
The example file examples/token-and-pool-deployment.json contains a ready-to-use token configuration. To customize, copy and edit the file (see Input File Reference).
Deploy on Base Sepolia:
pnpm start generate-token-deployment \
-i examples/token-and-pool-deployment.json \
-d 0xff170aD8f1d86eFAC90CA7a2E1204bA64aC5e0f9 \
--salt 0x0000000000000000000000000000000000000000000000000000000000000001 \
-f safe-json \
-s YOUR_SAFE_ADDRESS \
-w YOUR_OWNER_ADDRESS \
-c 84532 \
-o output/base-deployment.json| Flag | Description |
|---|---|
-i |
Path to input JSON file with token configuration |
-d |
TokenPoolFactory contract address (from Chain Reference table above) |
--salt |
32-byte hex value for deterministic deployment. Use the same salt on both chains for predictable addresses |
-f |
Output format: safe-json for Safe Transaction Builder, calldata for raw hex |
-s |
Your Safe multisig address |
-w |
Address of the Safe owner signing the transaction |
-c |
Chain ID where the transaction will execute |
-o |
Output file path |
Deploy on Ethereum Sepolia (same input file, different chain parameters):
pnpm start generate-token-deployment \
-i examples/token-and-pool-deployment.json \
-d 0xBCf47E9195A225813A629BB7580eDF338c2d8202 \
--salt 0x0000000000000000000000000000000000000000000000000000000000000001 \
-f safe-json \
-s YOUR_SAFE_ADDRESS \
-w YOUR_OWNER_ADDRESS \
-c 11155111 \
-o output/eth-deployment.jsonExecute and record outputs:
-
Import
output/base-deployment.jsoninto Safe Transaction Builder and execute -
Import
output/eth-deployment.jsoninto Safe Transaction Builder and execute -
From the transaction logs, find the two "Created" contract addresses:
- First "Created" address → Token contract
- Second "Created" address → Token Pool contract
-
Record the deployed addresses:
# Save these - you'll need them in Steps 2 and 3
BASE_TOKEN_ADDRESS=0x... # First "Created" address on Base Sepolia
BASE_POOL_ADDRESS=0x... # Second "Created" address on Base Sepolia
ETH_TOKEN_ADDRESS=0x... # First "Created" address on Ethereum Sepolia
ETH_POOL_ADDRESS=0x... # Second "Created" address on Ethereum Sepolia
The factory sets your Safe as pendingOwner on both the token and pool contracts. You must accept ownership before you can configure cross-chain connections.
Generate transactions:
# Base Sepolia: Accept ownership of token
pnpm start generate-accept-ownership \
-a BASE_TOKEN_ADDRESS \
-f safe-json \
-s YOUR_SAFE_ADDRESS \
-w YOUR_OWNER_ADDRESS \
-c 84532 \
-o output/base-token-accept-ownership.json
# Base Sepolia: Accept ownership of pool
pnpm start generate-accept-ownership \
-a BASE_POOL_ADDRESS \
-f safe-json \
-s YOUR_SAFE_ADDRESS \
-w YOUR_OWNER_ADDRESS \
-c 84532 \
-o output/base-pool-accept-ownership.json
# Ethereum Sepolia: Accept ownership of token
pnpm start generate-accept-ownership \
-a ETH_TOKEN_ADDRESS \
-f safe-json \
-s YOUR_SAFE_ADDRESS \
-w YOUR_OWNER_ADDRESS \
-c 11155111 \
-o output/eth-token-accept-ownership.json
# Ethereum Sepolia: Accept ownership of pool
pnpm start generate-accept-ownership \
-a ETH_POOL_ADDRESS \
-f safe-json \
-s YOUR_SAFE_ADDRESS \
-w YOUR_OWNER_ADDRESS \
-c 11155111 \
-o output/eth-pool-accept-ownership.json| Flag | Description |
|---|---|
-a |
Contract address to accept ownership of (token or pool) |
Execute:
Tip: To reduce the number of signatures, batch the token and pool transactions per chain. In Safe Transaction Builder, import both JSON files for the same chain, then execute them as a single batched transaction.
- Base Sepolia: Import
base-token-accept-ownership.jsonandbase-pool-accept-ownership.json, batch and execute - Ethereum Sepolia: Import
eth-token-accept-ownership.jsonandeth-pool-accept-ownership.json, batch and execute
Each pool must know about its counterpart on the other chain. This step uses addresses from both chains deployed in Step 1.
Create base-to-eth.json - configures the Base Sepolia pool to recognize the Ethereum Sepolia pool:
[
[],
[
{
"remoteChainSelector": "16015286601757825753",
"remotePoolAddresses": ["ETH_POOL_ADDRESS"],
"remoteTokenAddress": "ETH_TOKEN_ADDRESS",
"outboundRateLimiterConfig": {
"isEnabled": true,
"capacity": "100000000000000000000000",
"rate": "1000000000000000000000"
},
"inboundRateLimiterConfig": {
"isEnabled": true,
"capacity": "100000000000000000000000",
"rate": "1000000000000000000000"
},
"remoteChainType": "evm"
}
]
]Replace ETH_POOL_ADDRESS and ETH_TOKEN_ADDRESS with the Ethereum Sepolia addresses from Step 1.
Create eth-to-base.json - configures the Ethereum Sepolia pool to recognize the Base Sepolia pool:
[
[],
[
{
"remoteChainSelector": "10344971235874465080",
"remotePoolAddresses": ["BASE_POOL_ADDRESS"],
"remoteTokenAddress": "BASE_TOKEN_ADDRESS",
"outboundRateLimiterConfig": {
"isEnabled": true,
"capacity": "100000000000000000000000",
"rate": "1000000000000000000000"
},
"inboundRateLimiterConfig": {
"isEnabled": true,
"capacity": "100000000000000000000000",
"rate": "1000000000000000000000"
},
"remoteChainType": "evm"
}
]
]Replace BASE_POOL_ADDRESS and BASE_TOKEN_ADDRESS with the Base Sepolia addresses from Step 1.
Generate transactions:
# Configure Base Sepolia pool (uses BASE_POOL_ADDRESS from Step 1)
pnpm start generate-chain-update \
-i base-to-eth.json \
-p BASE_POOL_ADDRESS \
-f safe-json \
-s YOUR_SAFE_ADDRESS \
-w YOUR_OWNER_ADDRESS \
-c 84532 \
-o output/base-chain-update.json
# Configure Ethereum Sepolia pool (uses ETH_POOL_ADDRESS from Step 1)
pnpm start generate-chain-update \
-i eth-to-base.json \
-p ETH_POOL_ADDRESS \
-f safe-json \
-s YOUR_SAFE_ADDRESS \
-w YOUR_OWNER_ADDRESS \
-c 11155111 \
-o output/eth-chain-update.json| Flag | Description |
|---|---|
-i |
Path to chain update JSON file |
-p |
Pool address to configure (the local pool, not the remote one) |
Execute: Import and execute both JSON files in Safe.
Your cross-chain token is configured. Summary of what you deployed:
| Chain | Token | Pool |
|---|---|---|
| Base Sepolia | BASE_TOKEN_ADDRESS |
BASE_POOL_ADDRESS |
| Ethereum Sepolia | ETH_TOKEN_ADDRESS |
ETH_POOL_ADDRESS |
Your Safe received the preMinted tokens (100,000 tCCIP in the example config). To mint additional tokens, use the generate-mint command.
To transfer tokens cross-chain, interact with the CCIP Router contract. See the CCIP Documentation for instructions.
Chain selectors and TokenPoolFactory addresses are available from the CCIP API:
- Mainnet: https://docs.chain.link/api/ccip/v1/chains?environment=mainnet
- Testnet: https://docs.chain.link/api/ccip/v1/chains?environment=testnet
Navigate to data > evm > [chainId] to find:
selector- the chain selector (used inremoteChainSelector)tokenPoolFactory- the factory address (used in-dflag)
Common Testnet Values:
| Chain | Chain ID | Chain Selector | Factory Address |
|---|---|---|---|
| Ethereum Sepolia | 11155111 | 16015286601757825753 |
0xBCf47E9195A225813A629BB7580eDF338c2d8202 |
| Base Sepolia | 84532 | 10344971235874465080 |
0xff170aD8f1d86eFAC90CA7a2E1204bA64aC5e0f9 |
| Arbitrum Sepolia | 421614 | 3478487238524512106 |
Check API |
| Optimism Sepolia | 11155420 | 5224473277236331295 |
Check API |
Deploy a new BurnMintERC20 token and BurnMintTokenPool together using TokenPoolFactory.
Usage:
pnpm start generate-token-deployment \
-i <input-file> \
-d <factory-address> \
--salt <32-byte-hex> \
[-f calldata|safe-json] \
[-s <safe-address>] \
[-w <owner-address>] \
[-c <chain-id>] \
[-o <output-file>]Options:
-i, --input <path>: Input JSON file (required)-d, --deployer <address>: TokenPoolFactory address (required)--salt <bytes32>: 32-byte salt for CREATE2 (required)-f, --format <type>:calldataorsafe-json(default:calldata)-s, --safe <address>: Safe address (required for safe-json)-w, --owner <address>: Owner address (required for safe-json)-c, --chain-id <id>: Chain ID (required for safe-json)-o, --output <path>: Output file (optional, defaults to stdout)
Input JSON (see examples/token-and-pool-deployment.json):
{
"name": "My Token",
"symbol": "MTK",
"decimals": 18,
"maxSupply": "1000000000000000000000000",
"preMint": "100000000000000000000000",
"remoteTokenPools": []
}See Input File Reference for field descriptions and wei conversion.
remoteTokenPools: Array of remote chain configurations (optional, see Advanced Configuration)
Deploy a TokenPool for an existing ERC20 token.
Usage:
pnpm start generate-pool-deployment \
-i <input-file> \
-d <factory-address> \
--salt <32-byte-hex> \
[-f calldata|safe-json] \
[-s <safe-address>] \
[-w <owner-address>] \
[-c <chain-id>] \
[-o <output-file>]Input JSON:
{
"token": "0x779877A7B0D9E8603169DdbD7836e478b4624789",
"decimals": 18,
"poolType": "BurnMintTokenPool",
"remoteTokenPools": []
}Fields:
token: Existing token contract address (validated Ethereum address)decimals: Token decimals (number)poolType:"BurnMintTokenPool"or"LockReleaseTokenPool"remoteTokenPools: Array of remote chain configurations (optional)
Pool Types:
| Pool Type | When to Use | Requirements |
|---|---|---|
| BurnMintTokenPool | Token should be burned on source chain, minted on destination | Token must implement burn/mint methods |
| LockReleaseTokenPool | Token should be locked on source chain, released on destination | Standard ERC20 token |
Configure cross-chain connections for a TokenPool. Can add new chains, remove existing chains, or both in a single transaction.
Usage:
pnpm start generate-chain-update \
-i <input-file> \
-p <pool-address> \
[-f calldata|safe-json] \
[-s <safe-address>] \
[-w <owner-address>] \
[-c <chain-id>] \
[-o <output-file>]Options:
-i, --input <path>: Input JSON file (required)-p, --token-pool <address>: TokenPool address (optional, defaults to placeholder)-f, --format <type>:calldataorsafe-json(default:calldata)-s, --safe <address>: Safe address (required for safe-json)-w, --owner <address>: Owner address (required for safe-json)-c, --chain-id <id>: Chain ID (required for safe-json)-o, --output <path>: Output file (optional, defaults to stdout)
Input JSON (see examples/chain-update.json):
[
["12532609583862916517"],
[
{
"remoteChainSelector": "16015286601757825753",
"remotePoolAddresses": ["0x779877A7B0D9E8603169DdbD7836e478b4624789"],
"remoteTokenAddress": "0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05",
"outboundRateLimiterConfig": {
"isEnabled": true,
"capacity": "100000000000000000000000",
"rate": "1000000000000000000000"
},
"inboundRateLimiterConfig": {
"isEnabled": true,
"capacity": "100000000000000000000000",
"rate": "1000000000000000000000"
},
"remoteChainType": "evm"
}
]
]Format: [chainsToRemove, chainsToAdd]
- First array: Chain selectors to remove (empty
[]if only adding) - Second array: Chain configurations to add (empty
[]if only removing)
See Input File Reference for field descriptions. Key fields:
remoteChainSelector: Chain selector (find at CCIP API)remotePoolAddresses: Array of pool addresses on remote chainremoteTokenAddress: Token address on remote chainremoteChainType:"evm"for Ethereum-like chains,"svm"for Solana
Rate Limiter Configuration:
| Field | Type | Description |
|---|---|---|
| isEnabled | boolean | Enable/disable rate limiting |
| capacity | string | Maximum tokens transferable at once (wei) |
| rate | string | Bucket refill rate in tokens per second (wei) |
Recommended Rate Limiter Values (for 18-decimal tokens):
| Use Case | Capacity | Rate | Capacity (wei) | Rate (wei) |
|---|---|---|---|---|
| Test/Dev | 100,000 | 1,000/sec | "100000000000000000000000" |
"1000000000000000000000" |
| Low Volume | 1,000,000 | 10,000/sec | "1000000000000000000000000" |
"10000000000000000000000" |
| Medium Volume | 10,000,000 | 100,000/sec | "10000000000000000000000000" |
"100000000000000000000000" |
| High Volume | 100,000,000 | 1,000,000/sec | "100000000000000000000000000" |
"1000000000000000000000000" |
Generate a mint transaction for BurnMintERC20 tokens. Caller must have minter role.
Usage:
pnpm start generate-mint \
-t <token-address> \
-r <receiver-address> \
-a <amount> \
[-f calldata|safe-json] \
[-s <safe-address>] \
[-w <owner-address>] \
[-c <chain-id>] \
[-o <output-file>]Options:
-t, --token <address>: Token contract address (required)-r, --receiver <address>: Receiver address (required)-a, --amount <amount>: Amount to mint (string, required)-f, --format <type>:calldataorsafe-json(default:calldata)-s, --safe <address>: Safe address (required for safe-json)-w, --owner <address>: Owner address (required for safe-json)-c, --chain-id <id>: Chain ID (required for safe-json)-o, --output <path>: Output file (optional, defaults to stdout)
Example:
# Mint 1000 tokens (with 18 decimals)
pnpm start generate-mint \
-t 0x779877A7B0D9E8603169DdbD7836e478b4624789 \
-r 0x1234567890123456789012345678901234567890 \
-a 1000000000000000000000 \
-f safe-json \
-s 0xYourSafe \
-w 0xYourOwner \
-c 84532Accept ownership of a contract using the two-step ownership transfer pattern.
When to use: After deploying a token and pool via
generate-token-deployment, the TokenPoolFactory sets your Safe aspendingOwner. You must accept ownership before calling owner-only functions likeapplyChainUpdates.
Usage:
pnpm start generate-accept-ownership \
-a <contract-address> \
[-f calldata|safe-json] \
[-s <safe-address>] \
[-w <owner-address>] \
[-c <chain-id>] \
[-o <output-file>]Options:
-a, --address <address>: Contract address to accept ownership of (required)-f, --format <type>:calldataorsafe-json(default:calldata)-s, --safe <address>: Safe address (required for safe-json)-w, --owner <address>: Owner address (required for safe-json)-c, --chain-id <id>: Chain ID (required for safe-json)-o, --output <path>: Output file (optional, defaults to stdout)
Examples:
# Accept ownership of a token
pnpm start generate-accept-ownership \
-a 0x779877A7B0D9E8603169DdbD7836e478b4624789 \
-f safe-json \
-s 0xYourSafe \
-w 0xYourOwner \
-c 84532 \
-o output/accept-token-ownership.json
# Accept ownership of a pool
pnpm start generate-accept-ownership \
-a 0x1234567890123456789012345678901234567890 \
-f safe-json \
-s 0xYourSafe \
-w 0xYourOwner \
-c 84532 \
-o output/accept-pool-ownership.jsonGrant or revoke mint and/or burn permissions to/from a TokenPool.
When to use: This command is only needed when deploying a pool for an existing token (via
generate-pool-deployment). When deploying a new token + pool together (viagenerate-token-deployment), the TokenPoolFactory automatically grants mint/burn roles to the pool.
Usage:
pnpm start generate-grant-roles \
-t <token-address> \
-p <pool-address> \
[--action grant|revoke] \
[--role-type mint|burn|both] \
[-f calldata|safe-json] \
[-s <safe-address>] \
[-w <owner-address>] \
[-c <chain-id>] \
[-o <output-file>]Options:
-t, --token <address>: Token contract address (required)-p, --pool <address>: Pool contract address (required)--action <type>:grantorrevoke(default:grant)--role-type <type>:mint,burn, orboth(default:both)-f, --format <type>:calldataorsafe-json(default:calldata)-s, --safe <address>: Safe address (required for safe-json)-w, --owner <address>: Owner address (required for safe-json)-c, --chain-id <id>: Chain ID (required for safe-json)-o, --output <path>: Output file (optional, defaults to stdout)
Granting Roles:
# Grant both mint and burn roles (1 transaction)
pnpm start generate-grant-roles \
-t 0x779877A7B0D9E8603169DdbD7836e478b4624789 \
-p 0x1234567890123456789012345678901234567890 \
--action grant \
--role-type both \
-f safe-json \
-s 0xYourSafe \
-w 0xYourOwner \
-c 84532
# Grant mint role only
pnpm start generate-grant-roles \
-t 0x779877A7B0D9E8603169DdbD7836e478b4624789 \
-p 0x1234567890123456789012345678901234567890 \
--action grant \
--role-type mint \
-f safe-json \
-s 0xYourSafe \
-w 0xYourOwner \
-c 84532Revoking Roles:
# Revoke both mint and burn roles (2 transactions - executed atomically in Safe)
pnpm start generate-grant-roles \
-t 0x779877A7B0D9E8603169DdbD7836e478b4624789 \
-p 0x1234567890123456789012345678901234567890 \
--action revoke \
--role-type both \
-f safe-json \
-s 0xYourSafe \
-w 0xYourOwner \
-c 84532
# Revoke mint role only
pnpm start generate-grant-roles \
-t 0x779877A7B0D9E8603169DdbD7836e478b4624789 \
-p 0x1234567890123456789012345678901234567890 \
--action revoke \
--role-type mint \
-f safe-json \
-s 0xYourSafe \
-w 0xYourOwner \
-c 84532
# Revoke burn role only
pnpm start generate-grant-roles \
-t 0x779877A7B0D9E8603169DdbD7836e478b4624789 \
-p 0x1234567890123456789012345678901234567890 \
--action revoke \
--role-type burn \
-f safe-json \
-s 0xYourSafe \
-w 0xYourOwner \
-c 84532Important Notes:
-
Revoking both roles: When using
--action revoke --role-type both, the tool generates TWO transactions that will be executed atomically in Safe:revokeMintRole(pool)- Removes minting permissionrevokeBurnRole(pool)- Removes burning permission
This is because the BurnMintERC20 contract provides separate revoke functions but no combined
revokeMintAndBurnRoles()function (unlike grant operations which havegrantMintAndBurnRoles()). -
Backward compatibility: The
--actionflag defaults togrant, so existing scripts continue to work without modification.
Update the sender allow list for a TokenPool. The allow list restricts which addresses can initiate cross-chain transfers through the pool.
Usage:
pnpm start generate-allow-list-updates \
-i <input-json-path> \
-p <pool-address> \
[-f calldata|safe-json] \
[-s <safe-address>] \
[-w <owner-address>] \
[-c <chain-id>] \
[-o <output-file>]Options:
-i, --input <path>: Path to input JSON file (required)-p, --pool <address>: Token pool contract address (required)-f, --format <type>: Output format -calldataorsafe-json(default:calldata)-s, --safe <address>: Safe address (required for safe-json)-w, --owner <address>: Owner address (required for safe-json)-c, --chain-id <id>: Chain ID (required for safe-json)-o, --output <path>: Output file (optional, defaults to stdout)
Input JSON (see examples/allow-list-updates.json):
{
"removes": ["0x1234567890123456789012345678901234567890"],
"adds": [
"0x779877A7B0D9E8603169DdbD7836e478b4624789",
"0xa469F39796Cad956bE2E51117693880dB3E6438d"
]
}Parameters:
removes: Addresses to remove from allow list (optional, defaults to[])adds: Addresses to add to allow list (optional, defaults to[])
Example:
pnpm start generate-allow-list-updates \
-i examples/allow-list-updates.json \
-p 0x1234567890123456789012345678901234567890 \
-f safe-json \
-s 0xYourSafe \
-w 0xYourOwner \
-c 84532 \
-o output/allow-list-updates.jsonUpdate rate limiter configuration for a specific remote chain on a TokenPool.
Usage:
pnpm start generate-rate-limiter-config \
-i <input-json-path> \
-p <pool-address> \
[-f calldata|safe-json] \
[-s <safe-address>] \
[-w <owner-address>] \
[-c <chain-id>] \
[-o <output-file>]Options:
-i, --input <path>: Path to input JSON file (required)-p, --pool <address>: Token pool contract address (required)-f, --format <type>: Output format -calldataorsafe-json(default:calldata)-s, --safe <address>: Safe address (required for safe-json)-w, --owner <address>: Owner address (required for safe-json)-c, --chain-id <id>: Chain ID (required for safe-json)-o, --output <path>: Output file (optional, defaults to stdout)
Input JSON (see examples/rate-limiter-config.json):
{
"remoteChainSelector": "3478487238524512106",
"outboundConfig": {
"isEnabled": true,
"capacity": "1000000000000000000000",
"rate": "100000000000000000000"
},
"inboundConfig": {
"isEnabled": true,
"capacity": "1000000000000000000000",
"rate": "100000000000000000000"
}
}Parameters:
remoteChainSelector: Chain selector (find at CCIP API)outboundConfig: Rate limiter for tokens sent TO remote chaininboundConfig: Rate limiter for tokens received FROM remote chain
Rate Limiter Configuration:
| Field | Type | Description |
|---|---|---|
| isEnabled | boolean | Enable/disable rate limiting |
| capacity | string | Maximum tokens in bucket (wei) |
| rate | string | Bucket refill rate in tokens per second (wei) |
See Input File Reference for wei conversion and recommended values.
Example:
pnpm start generate-rate-limiter-config \
-i examples/rate-limiter-config.json \
-p 0x1234567890123456789012345678901234567890 \
-f safe-json \
-s 0xYourSafe \
-w 0xYourOwner \
-c 84532 \
-o output/rate-limiter-config.jsonYou can configure multiple pool addresses for a single remote chain. This is useful when:
- Multiple pools manage the same token on the remote chain
- Gradual migration between pool versions
- Multi-token pool architectures
Example:
{
"remoteChainSelector": "16015286601757825753",
"remotePoolAddresses": [
"0x779877A7B0D9E8603169DdbD7836e478b4624789",
"0x1234567890123456789012345678901234567890",
"0xa469F39796Cad956bE2E51117693880dB3E6438d"
],
"remoteTokenAddress": "0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05",
"outboundRateLimiterConfig": { ... },
"inboundRateLimiterConfig": { ... },
"remoteChainType": "evm"
}Configure cross-chain connections between EVM and Solana chains.
Address Formats:
- EVM: Standard 20-byte Ethereum addresses (e.g.,
0x779877A7B0D9E8603169DdbD7836e478b4624789) - SVM: 32-byte Solana public keys in base58 format (e.g.,
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA)
Example - Ethereum → Solana:
[
[],
[
{
"remoteChainSelector": "SOLANA_CHAIN_SELECTOR",
"remotePoolAddresses": ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
"remoteTokenAddress": "So11111111111111111111111111111111111111112",
"outboundRateLimiterConfig": {
"isEnabled": true,
"capacity": "100000000000000000000000",
"rate": "1000000000000000000000"
},
"inboundRateLimiterConfig": {
"isEnabled": true,
"capacity": "100000000000000000000000",
"rate": "1000000000000000000000"
},
"remoteChainType": "svm"
}
]
]Note: Set remoteChainType to "svm" for Solana chains. The tool will automatically encode Solana addresses as bytes32.
Add and remove chains in a single transaction:
[
["12532609583862916517", "3478487238524512106"],
[
{
"remoteChainSelector": "16015286601757825753",
"remotePoolAddresses": ["0x779877A7B0D9E8603169DdbD7836e478b4624789"],
"remoteTokenAddress": "0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05",
"outboundRateLimiterConfig": { ... },
"inboundRateLimiterConfig": { ... },
"remoteChainType": "evm"
}
]
]This removes chains 12532609583862916517 and 3478487238524512106 while adding chain 16015286601757825753.
Configure remote chains during initial token/pool deployment:
See examples/token-deployment-with-remote.json for a complete example with remote chain configuration.
This tool uses CREATE2 for deterministic contract deployment. Key concepts:
Salt: A 32-byte value that determines the deployment address. The same salt with the same code and deployer always produces the same address.
Salt Modification: The TokenPoolFactory modifies your salt by hashing it with the sender address:
modifiedSalt = keccak256(abi.encodePacked(salt, msg.sender))
This means the same salt produces different addresses for different senders.
Address Computation: The final address is computed as:
address = keccak256(0xff, deployer, modifiedSalt, keccak256(initCode))
The tool automatically computes and logs the deterministic address before deployment.
Why This Matters: You can predict deployment addresses before executing transactions. This is useful for:
- Pre-configuring contracts with addresses
- Coordinating multi-chain deployments
- Verifying deployments
Hex-encoded function call data. Use with:
- Web3 libraries (ethers.js, web3.js)
- Block explorers (Etherscan)
- Hardware wallets
- Direct contract interaction tools
Example:
0x4a792d70000000000000000000000000000000000000000000000000000000000000...
Structured JSON compatible with Safe Transaction Builder. Includes:
- Transaction metadata (chain ID, timestamps)
- Safe and owner addresses
- Contract method signatures with full type information
- Human-readable descriptions
Example:
{
"version": "1.0",
"chainId": "84532",
"createdAt": 1234567890,
"meta": {
"name": "Token and Pool Factory Deployment - My Token",
"description": "Deploy My Token (MTK) token and associated pool using factory",
"txBuilderVersion": "1.18.0",
"createdFromSafeAddress": "0xYourSafe",
"createdFromOwnerAddress": "0xYourOwner"
},
"transactions": [...]
}Import this file directly into Safe Transaction Builder.
Cause: Address format is incorrect.
Solution: Ensure addresses:
- Start with
0x - Are 42 characters long (20 bytes in hex)
- Use valid hex characters (0-9, a-f)
Cause: Salt is not exactly 32 bytes.
Solution: Salt must be 66 characters total (0x + 64 hex characters). Example:
0x0000000000000000000000000000000000000000000000000000000000000001
Cause: Missing required options for safe-json format.
Solution: Add all three options:
-s YOUR_SAFE_ADDRESS -w YOUR_OWNER_ADDRESS -c CHAIN_IDCause: Input JSON doesn't match expected schema.
Solution: Check that:
- JSON is valid (use
jqto validate) - All required fields are present
- Field types match (strings, numbers, booleans)
- Addresses are valid
- Amounts are strings (not numbers)
Cause: Caller doesn't have minter role.
Solution: Grant minter role first:
pnpm start generate-grant-roles \
-t TOKEN_ADDRESS \
-p POOL_OR_MINTER_ADDRESS \
--role-type mintCause: Capacity or rate exceeds uint128 max value.
Solution: Values must be ≤ 340282366920938463463374607431768211455. For reference:
- 100 million tokens (18 decimals):
100000000000000000000000000 - This is well within uint128 limits
# Build
pnpm build
# Lint
pnpm lint:check
pnpm lint:fix
# Format
pnpm format:check
pnpm format:fix
# Test
pnpm test
pnpm test:watch
pnpm test:coverage
# Generate types from ABIs
pnpm typechain.
├── abis/ # Contract ABIs
├── examples/ # Example input JSON files
│ ├── token-and-pool-deployment.json
│ ├── token-deployment-with-remote.json
│ ├── pool-deployment.json
│ ├── chain-update.json
│ ├── allow-list-updates.json
│ ├── rate-limiter-config.json
│ ├── grant-roles.json
│ ├── revoke-roles.json
│ └── mint.json
├── src/
│ ├── cli.ts # CLI entry point
│ ├── constants/ # Bytecodes and constants
│ ├── generators/ # Transaction generators
│ ├── types/ # TypeScript types and Zod schemas
│ ├── typechain/ # Generated contract types
│ └── utils/ # Utility functions
└── output/ # Generated transaction files