Set Token Pool Rate Limits Using Hardhat
Guide Versions
This guide is available in multiple versions. Choose the one that matches your needs.
In this tutorial you will:
- Review current standard rate limiter configurations for your deployed token pool.
- Update outbound and inbound standard rate limiter configurations.
- Verify the updated configurations.
- Test standard rate limiter enforcement with a cross-chain token transfer.
- Optionally configure and test rate limits for faster than finality transfers.
Before You Begin
1 Set up your development environment
-
Install Node.js and npm:
- Make sure you have
Node.js v22.10.0or above installed. If not, installNode.js v22.10.0using their documentation. npmis bundled with Node.js. If you can’t runnpm, reinstall/update Node.js from the official installer.
- Make sure you have
-
Install/Update Chainlink CCIP-CLI. You can also find the GitHub repository here.
npm install -g @chainlink/ccip-cli
Verify the installation by running the following command:
ccip-cli --version
- Clone the repository and navigate to the project directory:
Clone the CCIP 2.0 Hardhat template for a smoother CCT setup.
git clone https://github.com/smartcontractkit/docs-cct-hardhat.git
cd docs-cct-hardhat
- Create a
.envfile by copying the.env.examplefile, and fill in the required values:
cp .env.example .env
# Keystore name (created via `npx hardhat keystore set`)
KEYSTORE_NAME=<your_private_key_name>
# RPC URLs (add the ones you need)
ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc
ARBITRUM_SEPOLIA_RPC_URL=your_arbitrum_sepolia_rpc
# Etherscan API key (required only if you pass --verify to deployment tasks)
ETHERSCAN_API_KEY=your_etherscan_api_key
View the complete list of supported chains in the helper-config.ts file.
- To make sure your terminal has access to these variables, run the following command:
source .env
- Build the project:
npm install && npx hardhat compile
- Create an encrypted Hardhat keystore, if you haven't already:
npx hardhat keystore set <your_private_key_name>
Tutorial
1 Review Current Rate Limiter Configurations
Use the getCurrentRateLimits task to read the current rate limiter configuration state for a specific lane from your token pool. The task reports whether rate limiting is enabled, the maximum capacity (bucket size), the refill rate (tokens per second), and the current token count in each bucket.
View the rate limiter query task on GitHub.
--destchain | arbitrumSepolia) | |
--fastfinality |
- Check the Ethereum Sepolia pool (lane to Arbitrum Sepolia):
npx hardhat getCurrentRateLimits \
--destchain arbitrumSepolia \
--network sepolia
Your output should look something like this:
========================================
📊 Get Current Rate Limits
========================================
Chain: Ethereum Sepolia
Remote Chain: Arbitrum Sepolia
Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action: View rate limits
Bucket: Standard finality
========================================
Pool Version: v2
Outbound Enabled: false
Outbound Capacity: 0
Outbound Rate: 0
Outbound Tokens: 0
Inbound Enabled: false
Inbound Capacity: 0
Inbound Rate: 0
Inbound Tokens: 0
========================================
Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
========================================
- Check the Arbitrum Sepolia pool (lane to Ethereum Sepolia):
npx hardhat getCurrentRateLimits \
--destchain sepolia \
--network arbitrumSepolia
Your output should look something like this:
========================================
📊 Get Current Rate Limits
========================================
Chain: Arbitrum Sepolia
Remote Chain: Ethereum Sepolia
Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Action: View rate limits
Bucket: Standard finality
========================================
Pool Version: v2
Outbound Enabled: false
Outbound Capacity: 0
Outbound Rate: 0
Outbound Tokens: 0
Inbound Enabled: false
Inbound Capacity: 0
Inbound Rate: 0
Inbound Tokens: 0
========================================
Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
========================================
2 Update Rate Limiter Configurations
Use the updateRateLimiters task to update rate limiter configurations for an existing lane in your token pool. The direction to update is inferred automatically from whichever --outbound* / --inbound* flags you set.
Only the directions you specify are updated; any direction you don't specify is left unchanged. Outbound and inbound are relative to the pool you're configuring:
outboundis tokens leaving this pool toward the--destchainremote chain;inboundis tokens arriving from the--destchainremote chain into this pool.
View the rate limiter update task on GitHub.
--destchain | ||
--outboundcapacity | ||
--outboundrate | ||
--outboundenabled | isEnabled explicitly (true/false; defaults to true when capacity or rate are set) | |
--inboundcapacity | ||
--inboundrate | ||
--inboundenabled | isEnabled explicitly (true/false; defaults to true when capacity or rate are set) | |
--fastfinality |
In this example, we configure an 18-decimal token with outbound capacity of 10 tokens (refilling at 0.1 tokens/sec) and inbound capacity of 11 tokens (refilling at 0.11 tokens/sec, a 10% buffer):
- Update the Ethereum Sepolia pool (lane to Arbitrum Sepolia):
npx hardhat updateRateLimiters \
--destchain arbitrumSepolia \
--outboundcapacity 10000000000000000000 \
--outboundrate 100000000000000000 \
--inboundcapacity 11000000000000000000 \
--inboundrate 110000000000000000 \
--network sepolia
Your output should look something like this:
========================================
⚡️ Update Rate Limiters
========================================
Chain: Ethereum Sepolia
Remote Chain: Arbitrum Sepolia
Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action: Update rate limits
Direction: Both
Bucket: Standard finality
========================================
Pool Version: v2 (setRateLimitConfig)
Current Rate Limiter State:
Outbound Enabled: false
Outbound Capacity: 0
Outbound Rate: 0
Outbound Tokens: 0
Inbound Enabled: false
Inbound Capacity: 0
Inbound Rate: 0
Inbound Tokens: 0
New Configuration:
Outbound Enabled: true
Outbound Capacity: 10000000000000000000
Outbound Rate: 100000000000000000
Inbound Enabled: true
Inbound Capacity: 11000000000000000000
Inbound Rate: 110000000000000000
[Step 1] Applying rate limit update on Ethereum Sepolia
⏳ Tx: 0xddeeff00112233445566778899aabbccddeeff00112233445566778899aabbcc
========================================
✅ Rate limiter update complete on Ethereum Sepolia!
========================================
Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Transaction: 0xddeeff00112233445566778899aabbccddeeff00112233445566778899aabbcc
========================================
- Update the Arbitrum Sepolia pool (lane to Ethereum Sepolia):
npx hardhat updateRateLimiters \
--destchain sepolia \
--outboundcapacity 10000000000000000000 \
--outboundrate 100000000000000000 \
--inboundcapacity 11000000000000000000 \
--inboundrate 110000000000000000 \
--network arbitrumSepolia
Your output should look something like this:
========================================
⚡️ Update Rate Limiters
========================================
Chain: Arbitrum Sepolia
Remote Chain: Ethereum Sepolia
Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Action: Update rate limits
Direction: Both
Bucket: Standard finality
========================================
Pool Version: v2 (setRateLimitConfig)
Current Rate Limiter State:
Outbound Enabled: false
Outbound Capacity: 0
Outbound Rate: 0
Outbound Tokens: 0
Inbound Enabled: false
Inbound Capacity: 0
Inbound Rate: 0
Inbound Tokens: 0
New Configuration:
Outbound Enabled: true
Outbound Capacity: 10000000000000000000
Outbound Rate: 100000000000000000
Inbound Enabled: true
Inbound Capacity: 11000000000000000000
Inbound Rate: 110000000000000000
[Step 1] Applying rate limit update on Arbitrum Sepolia
⏳ Tx: 0x2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f70812
========================================
✅ Rate limiter update complete on Arbitrum Sepolia!
========================================
Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Transaction: 0x2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f70812
========================================
- To disable rate limiters for a lane, pass
--outboundenabled falseand--inboundenabled false:
npx hardhat updateRateLimiters \
--destchain arbitrumSepolia \
--outboundenabled false \
--inboundenabled false \
--network sepolia
3 Verify the Updated Rate Limiter Configurations
Re-run the getCurrentRateLimits task on both chains to confirm the update was applied correctly.
- Verify the Ethereum Sepolia pool:
npx hardhat getCurrentRateLimits \
--destchain arbitrumSepolia \
--network sepolia
Your output should look something like this:
========================================
📊 Get Current Rate Limits
========================================
Chain: Ethereum Sepolia
Remote Chain: Arbitrum Sepolia
Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action: View rate limits
Bucket: Standard finality
========================================
Pool Version: v2
Outbound Enabled: true
Outbound Capacity: 10000000000000000000
Outbound Rate: 100000000000000000
Outbound Tokens: 10000000000000000000
Inbound Enabled: true
Inbound Capacity: 11000000000000000000
Inbound Rate: 110000000000000000
Inbound Tokens: 11000000000000000000
========================================
Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
========================================
- Verify the Arbitrum Sepolia pool:
npx hardhat getCurrentRateLimits \
--destchain sepolia \
--network arbitrumSepolia
Your output should look something like this:
========================================
📊 Get Current Rate Limits
========================================
Chain: Arbitrum Sepolia
Remote Chain: Ethereum Sepolia
Token Pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Action: View rate limits
Bucket: Standard finality
========================================
Pool Version: v2
Outbound Enabled: true
Outbound Capacity: 10000000000000000000
Outbound Rate: 100000000000000000
Outbound Tokens: 10000000000000000000
Inbound Enabled: true
Inbound Capacity: 11000000000000000000
Inbound Rate: 110000000000000000
Inbound Tokens: 11000000000000000000
========================================
Token Pool: https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
========================================
4 Test the Rate Limiter Configurations
Initiate a cross-chain transfer to verify that the rate limiter enforces the configured limits.
Before testing, export the router and token addresses for the source chain:
export ETHEREUM_SEPOLIA_ROUTER=0x...
export ETHEREUM_SEPOLIA_TOKEN=0x...
Note:
- Add
--fee-token LINKto pay CCIP fees in LINK. If you omit this flag, fees are paid in the native gas token.- Use
--receiver <address>for a custom recipient. If omitted on EVM-to-EVM, the receiver defaults to the sender.
Test 1: Exceed capacity
Transfer one wei more than the outbound capacity (10 tokens + 1 wei). This should fail because the requested amount exceeds the bucket size.
ccip-cli send \
--source ethereum-testnet-sepolia \
--router $ETHEREUM_SEPOLIA_ROUTER \
--dest ethereum-testnet-sepolia-arbitrum-1 \
--transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=10.000000000000000001 \
--wallet hardhat:$KEYSTORE_NAME \
--rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
--rpc "$ARBITRUM_SEPOLIA_RPC_URL"
Your output should look something like this:
Fee: 130129888907619n = 0.000130129888907619 ETH
✔ Enter password for Hardhat keystore 'PRIVATE_KEY'
error: Failed to call "ccipSend"
Error = execution reverted: TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress)
Revert.error = TokenMaxCapacityExceeded(uint256,uint256,address)
Revert.capacity = 10000000000000000000n
Revert.requested = 10000000000000000001n
Revert.tokenAddress = 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
The transfer should fail with TokenMaxCapacityExceeded because the requested amount is larger than the bucket capacity.
Test 2: Drain the bucket, then hit the rate
- First transfer (succeeds: drains the outbound bucket):
ccip-cli send \
--source ethereum-testnet-sepolia \
--router $ETHEREUM_SEPOLIA_ROUTER \
--dest ethereum-testnet-sepolia-arbitrum-1 \
--transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=10 \
--wallet hardhat:$KEYSTORE_NAME \
--rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
--rpc "$ARBITRUM_SEPOLIA_RPC_URL"
Your output should look something like this:
Fee: 130129888907619n = 0.000130129888907619 ETH
✔ Enter password for Hardhat keystore 'PRIVATE_KEY'
🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293
CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293
- Second transfer (fails: bucket has not refilled):
Run the same command immediately:
ccip-cli send \
--source ethereum-testnet-sepolia \
--router $ETHEREUM_SEPOLIA_ROUTER \
--dest ethereum-testnet-sepolia-arbitrum-1 \
--transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=10 \
--wallet hardhat:$KEYSTORE_NAME \
--rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
--rpc "$ARBITRUM_SEPOLIA_RPC_URL"
Your output should look something like this:
Fee: 130129888907619n = 0.000130129888907619 ETH
✔ Enter password for Hardhat keystore 'PRIVATE_KEY'
error: Failed to call "ccipSend"
Error = execution reverted: TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress)
Revert.error = TokenRateLimitReached(uint256,uint256,address)
Revert.minWaitInSeconds = 100n
Revert.available = 0n
Revert.tokenAddress = 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
The transfer should fail with TokenRateLimitReached because the bucket has not yet refilled. With a capacity of 10 tokens and a rate of 0.1 tokens per second, it takes 100 seconds to fully replenish.
5 (Optional) Configure Rate Limits for Faster Than Finality Transfers
This section applies to TokenPool v2.0 or later only. CCIP 2.0 token pools maintain two independent rate limiter buckets per lane:
- Standard finality bucket: Used for standard finality transfers (
finality=finalized, which is the default when you omit--extra finality=...). This is the bucket you configured in the previous steps. - Faster Than Finality bucket: Used for transfers that request faster than finality with
finality=<N>(numeric block depth). If this bucket is disabled for a direction, faster than finality transfers for that direction fall back to the standard bucket (enforced on-chain by the token pool).
Check the pool's allowed finality config
Use getFinalityConfig to confirm which faster than finality modes the pool currently allows.
View the finality config query task on GitHub.
- Check the Ethereum Sepolia pool:
npx hardhat getFinalityConfig --network sepolia
Your output should look something like this:
========================================
⏱️ Get Finality Config
========================================
Chain: Ethereum Sepolia
Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action: View finality config
========================================
Allowed Finality Config (raw): 0x00000000
Mode: WAIT_FOR_FINALITY (default)
Full finality is required. Faster Than Finality transfers are disabled.
========================================
Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
========================================
BLOCK_DEPTH (32) sets a minimum for numeric requests: --extra finality=32 (or higher) is allowed; --extra finality=10 reverts with InvalidRequestedFinality(requestedFinality, allowedFinality).
- Check the Arbitrum Sepolia pool:
npx hardhat getFinalityConfig --network arbitrumSepolia
Approach 1: Allow Faster Than Finality and Configure the Faster Than Finality Bucket in One Command
Use setFinalityConfig with rate limit flags to allow faster than finality and configure the faster than finality bucket in a single run.
View the finality config update task on GitHub.
This example configures the Ethereum Sepolia pool for transfers to Arbitrum Sepolia. To configure the reverse direction, run the same task on Arbitrum Sepolia with --destchain sepolia --network arbitrumSepolia.
npx hardhat setFinalityConfig \
--blockdepth 32 \
--destchain arbitrumSepolia \
--outboundcapacity 5000000000000000000 \
--outboundrate 50000000000000000 \
--inboundcapacity 5500000000000000000 \
--inboundrate 55000000000000000 \
--network sepolia
Your output should look something like this:
========================================
⏱️ Set Finality Config
========================================
Chain: Ethereum Sepolia
Remote Chain: Arbitrum Sepolia
Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action: Set finality config
========================================
Current Finality Config: 0x00000000
New Finality Config: 0x00000020
Mode: BLOCK_DEPTH (32 blocks)
[Step 1] Setting finality config on Ethereum Sepolia
⏳ Tx: 0x280399cd3dce1815a30ca3763736c7cbc4a8018bdb3ba1b650e728479640f5b6
✅ Finality config set successfully!
[Step 2] Updating rate limits (faster than finality bucket) on Ethereum Sepolia → Arbitrum Sepolia
New Configuration:
Outbound Enabled: true
Outbound Capacity: 5000000000000000000
Outbound Rate: 50000000000000000
Inbound Enabled: true
Inbound Capacity: 5500000000000000000
Inbound Rate: 55000000000000000
⏳ Tx: 0xa69245f229f41313d597ea452fa7c9d8ead9e05bfe36aad5a997a6850d198ac5
✅ Rate limits updated successfully!
========================================
✅ Configuration Complete on Ethereum Sepolia!
========================================
Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Finality Config: 0x00000020
Mode: BLOCK_DEPTH (32 blocks)
========================================
Approach 2: Update the Faster Than Finality Bucket Directly
If the pool already allows faster than finality, you can update the faster than finality bucket directly using updateRateLimiters with --fastfinality:
npx hardhat updateRateLimiters \
--destchain arbitrumSepolia \
--fastfinality \
--outboundcapacity 5000000000000000000 \
--outboundrate 50000000000000000 \
--inboundcapacity 5500000000000000000 \
--inboundrate 55000000000000000 \
--network sepolia
Verify the faster than finality bucket
Pass --fastfinality to getCurrentRateLimits. Each direction is shown independently: the faster than finality bucket for directions where it is enabled, and the standard bucket fallback for directions where it is not.
npx hardhat getCurrentRateLimits \
--destchain arbitrumSepolia \
--fastfinality \
--network sepolia
Your output should look something like this:
========================================
📊 Get Current Rate Limits
========================================
Chain: Ethereum Sepolia
Remote Chain: Arbitrum Sepolia
Token Pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action: View rate limits
Bucket: Faster Than Finality (standard finality fallback per direction if not enabled)
========================================
Pool Version: v2
Outbound [faster than finality]:
Enabled: true
Capacity: 5000000000000000000
Rate: 50000000000000000
Tokens: 5000000000000000000
Inbound [faster than finality]:
Enabled: true
Capacity: 5500000000000000000
Rate: 55000000000000000
Tokens: 5500000000000000000
========================================
Token Pool: https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
========================================
Test a faster than finality transfer
After the pool allows faster than finality and the faster than finality bucket is configured, send a small transfer with numeric block depth (for example --extra finality=32):
ccip-cli send \
--source ethereum-testnet-sepolia \
--router $ETHEREUM_SEPOLIA_ROUTER \
--dest ethereum-testnet-sepolia-arbitrum-1 \
--transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=1 \
--receiver 0xYourReceiverAddress \
--extra finality=32 \
--wallet hardhat:$KEYSTORE_NAME \
--rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
--rpc "$ARBITRUM_SEPOLIA_RPC_URL"
Your output should look something like this:
Fee: 130129888907619n = 0.000130129888907619 ETH
✔ Enter password for Hardhat keystore 'PRIVATE_KEY'
🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x5f1c2d3e4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e , messageId => 0x3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2c4d
CCIP Explorer: https://ccip.chain.link/msg/0x3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2c4d
Re-run getCurrentRateLimits with --fastfinality to confirm the outbound faster than finality bucket changed after the transfer:
npx hardhat getCurrentRateLimits \
--destchain arbitrumSepolia \
--fastfinality \
--network sepolia