# Set Token Pool Rate Limits Using Foundry
Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-foundry
Last Updated: 2026-04-27

> For the complete documentation index, see [llms.txt](/llms.txt).

## Guide Versions

- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-foundry)

- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-hardhat)

In this tutorial you will:

1. Review current standard rate limiter configurations for your deployed token pool.
2. Update outbound and inbound standard rate limiter configurations.
3. Verify the updated configurations.
4. Test standard rate limiter enforcement with a cross-chain token transfer.
5. Optionally configure and test rate limits for faster than finality transfers.

## Before You Begin

### 1. Set up your development environment

1. Install Node.js and npm:
   - Make sure you have `Node.js v22.10.0` or above installed. If not, install **`Node.js v22.10.0`** using their [documentation](https://nodejs.org/en/download/).
   - `npm` is bundled with Node.js. If you can't run `npm`, reinstall/update Node.js from the official installer.

2. Install/Update [Chainlink CCIP-CLI](https://docs.chain.link/ccip/tools/cli/). You can also find the [GitHub repository here](https://github.com/smartcontractkit/ccip-cli).

```bash filename="Terminal"
npm install -g @chainlink/ccip-cli
```

Verify the installation by running the following command:

```bash filename="Terminal"
ccip-cli --version
```

3. **Install Foundry**:
   If you haven't already, follow the instructions in the [Foundry documentation](https://book.getfoundry.sh/getting-started/installation) to install Foundry. \
   Verify the installation by running the following command:

```bash filename="Terminal"
forge --version
```

4. **Clone the repository and navigate to the project directory**:

[CCIP 2.0 template](https://github.com/smartcontractkit/docs-cct-foundry)

Clone the CCIP 2.0 template for a smoother CCT setup.

```bash filename="Terminal"
git clone https://github.com/smartcontractkit/docs-cct-foundry.git
cd docs-cct-foundry
```

5. Create an encrypted Foundry keystore, if you haven't already:

```bash filename="Terminal"
cast wallet import your_keystore_name --interactive
```

> **NOTE: Note**
>
> 1. For more information on how to create an encrypted Foundry keystore, refer to the [Foundry documentation](https://www.getfoundry.sh/cast/wallet-operations#wallet-operations).
> 2. `cast wallet import` will throw an error if a keystore with the same name already exists.
> 3. Use `cast wallet list` to check previously created keystores.

![Foundry keystore list command output](/images/ccip/tutorials/ccip-getting-started-evm-2.png)

6. Create a `.env` file by copying the `.env.example` file, and fill in the required values:

```bash filename="Terminal"
cp .env.example .env
```

```bash filename=".env"
# Keystore name (created via `cast wallet import`)
KEYSTORE_NAME=your_keystore_name

# RPC URLs (add the ones you need)
ETHEREUM_SEPOLIA_RPC_URL=your_eth_sepolia_rpc
ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL=your_arbitrum_sepolia_rpc

# Etherscan API key (required only if you pass --verify to deployment scripts)
ETHERSCAN_API_KEY=your_etherscan_api_key
```

[Complete List of Supported Chains](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/HelperConfig.s.sol)

View the complete list of supported chains in the `HelperConfig.s.sol` file.

7. To make sure your terminal has access to these variables, run the following command:

```bash filename="Terminal"
source .env
```

8. Build the project:

```bash filename="Terminal"
npm install && forge build
```

## Tutorial

> **TIP: Prerequisites**
>
> This tutorial assumes you have already deployed tokens and token pools on both chains. If not, complete one of the
> registration tutorials first:
>
> - [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry)
> - [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry)
>
> Make sure your token and token pool addresses are exported as environment variables:
>
> ```bash
> export ETHEREUM_SEPOLIA_TOKEN=0x...
> export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x...
>
> export ETHEREUM_SEPOLIA_TOKEN_POOL=0x...
> export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL=0x...
> ```
>
> This tutorial supports both CCT registration models:
>
> - In the Burn & Mint model, rate limiters are configured on the `BurnMintTokenPool` on each chain.
> - In the Lock & Mint model, rate limiters are configured on the token pools: the source-chain `LockReleaseTokenPool` and the destination-chain `BurnMintTokenPool`. The `ERC20LockBox` is not rate-limited and is not updated by the scripts in this tutorial.

### 1. Review Current Rate Limiter Configurations

Use the `GetCurrentRateLimits.s.sol` script to read the current rate limiter configuration state for a specific lane from your token pool. The script 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.

[GetCurrentRateLimits.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/rate-limiter/GetCurrentRateLimits.s.sol)

View the rate limiter query script on GitHub.

| Env var         | Required | Description                                                                                                                     |
| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `DEST_CHAIN`    | Yes      | Remote chain whose lane is being queried (e.g. `ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1`)                                           |
| `FAST_FINALITY` | No       | Set to `true` to read the faster than finality bucket instead of the standard finality bucket (v2 pools only, default: `false`) |

1. Check the Ethereum Sepolia pool (lane to Arbitrum Sepolia):

```bash filename="Terminal"
DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \
  forge script \
  script/configure/rate-limiter/GetCurrentRateLimits.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
```

Your output should look something like this:

```bash filename="Terminal"
========================================
📊 Get Current Rate Limits
========================================
Chain:        Ethereum Sepolia
Remote Chain: Arbitrum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       View rate limits
Bucket:       Standard finality
========================================

Pool Version: v2

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

========================================
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Token Pool:   https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
========================================
```

2. Check the Arbitrum Sepolia pool (lane to Ethereum Sepolia):

```bash filename="Terminal"
DEST_CHAIN=ETHEREUM_SEPOLIA \
  forge script \
  script/configure/rate-limiter/GetCurrentRateLimits.s.sol \
  --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL
```

Your output should look something like this:

```bash filename="Terminal"
========================================
📊 Get Current Rate Limits
========================================
Chain:        Arbitrum Sepolia
Remote Chain: Ethereum Sepolia
Token Pool:   0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Action:       View rate limits
Bucket:       Standard finality
========================================

Pool Version: v2

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

========================================
Token Pool:   0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Token Pool:   https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
========================================
```

> **NOTE: Rate Limiters Disabled**
>
> 1. Rate limiting is disabled by default when no rate-limiting parameters are provided during
>    `ApplyChainUpdates`. In that case, both outbound and inbound capacities are `0`, meaning
>    there are no restrictions on token flow.
> 2. In the next step you will enable and configure these rate limiters.

### 2. Update Rate Limiter Configurations

Use the `UpdateRateLimiters.s.sol` script to update rate limiter configurations for an existing lane in your token pool. The direction to update is inferred automatically from whichever `OUTBOUND_*` / `INBOUND_*` environment variables 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:

1. `outbound` is tokens leaving this pool toward `DEST_CHAIN`;
2. `inbound` is tokens arriving from `DEST_CHAIN` into this pool.

> **NOTE: Lane must exist**
>
> 1. Rate limiters can only be updated for lanes that already exist.
> 2. If you haven't configured the lane yet, complete one of the two tutorials first:
>
>    - [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry)
>    - [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry)
> 3. Run `ApplyChainUpdates.s.sol` to configure the lane.

[UpdateRateLimiters.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/rate-limiter/UpdateRateLimiters.s.sol)

View the rate limiter update script on GitHub.

| 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 faster than finality bucket instead of the standard finality bucket (v2 only, default: `false`) |

> **NOTE: Access control and compatibility**
>
> 1. The pool owner and the designated `rateLimitAdmin` can update rate limiters.
> 2. The `rateLimitAdmin` is configured via `SetDynamicConfig.s.sol`.
> 3. This script supports both v1 and v2 token pools: on v2 pools it calls `setRateLimitConfig()`; on v1
>    pools it falls back to `setChainRateLimiterConfig()`.
> 4. The `FAST_FINALITY` flag is v2-only and is ignored on v1 pools.

> **NOTE: Configuration best practices**
>
> 1. Set inbound rate limits a bit higher than outbound limits on the same lane. Many chains finalize blocks in
>    batches, so multiple outbound messages may become executable on the destination simultaneously, requiring slightly
>    more inbound capacity.
> 2. The exact additional capacity required depends on the refill rate and the size of the source chain epochs and CCIP
>    round time. In most cases, a 5-10% buffer should be sufficient.
> 3. `rate` must be less than or equal to `capacity`, and when disabling rate limiting (
>    `isEnabled: false`), both `capacity` and `rate` must be `0`.

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

1. Update the Ethereum Sepolia pool (lane to Arbitrum Sepolia):

```bash filename="Terminal"
DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \
  OUTBOUND_RATE_LIMIT_CAPACITY=10000000000000000000 \
  OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \
  INBOUND_RATE_LIMIT_CAPACITY=11000000000000000000 \
  INBOUND_RATE_LIMIT_RATE=110000000000000000 \
  forge script \
  script/configure/rate-limiter/UpdateRateLimiters.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

Your output should look something like this:

```bash filename="Terminal"
========================================
⚡️ Update Rate Limiters
========================================
Chain:        Ethereum Sepolia
Remote Chain: Arbitrum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       Update rate limits
Direction:    Outbound + Inbound
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

========================================
✅ Rate limiter update complete on Ethereum Sepolia!
========================================
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Token Pool:   https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
========================================
```

2. Update the Arbitrum Sepolia pool (lane to Ethereum Sepolia):

```bash filename="Terminal"
DEST_CHAIN=ETHEREUM_SEPOLIA \
  OUTBOUND_RATE_LIMIT_CAPACITY=10000000000000000000 \
  OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \
  INBOUND_RATE_LIMIT_CAPACITY=11000000000000000000 \
  INBOUND_RATE_LIMIT_RATE=110000000000000000 \
  forge script \
  script/configure/rate-limiter/UpdateRateLimiters.s.sol \
  --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

Your output should look something like this:

```bash filename="Terminal"
========================================
⚡️ Update Rate Limiters
========================================
Chain:        Arbitrum Sepolia
Remote Chain: Ethereum Sepolia
Token Pool:   0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Action:       Update rate limits
Direction:    Outbound + Inbound
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

========================================
✅ Rate limiter update complete on Arbitrum Sepolia!
========================================
Token Pool:   0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Token Pool:   https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
========================================
```

3. To **Disable** rate limiters for a lane, set `OUTBOUND_RATE_LIMIT_ENABLED=false` and `INBOUND_RATE_LIMIT_ENABLED=false`:

> **CAUTION: Disabling rate limiters**
>
> 1. We highly recommend that devs have at least some limits set up. We advise against completely disabling rate
>    limiters unless absolutely necessary.
> 2. **Informational-only for this tutorial:** If you disable rate limiters here, the later **Test the Rate Limiter Configurations** section will not behave as described (it will not revert
>    with `TokenMaxCapacityExceeded` / `TokenRateLimitReached`).

```bash filename="Terminal"
DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \
  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
```

### 3. Verify the Updated Rate Limiter Configurations

Re-run the `GetCurrentRateLimits.s.sol` script on both chains to confirm the update was applied correctly.

1. Verify the Ethereum Sepolia pool:

```bash filename="Terminal"
DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \
  forge script \
  script/configure/rate-limiter/GetCurrentRateLimits.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
```

> **NOTE: Note**
>
> Your output might differ based on whether or not you disabled rate limiters in the previous step.

Your output should look something like this:

```bash filename="Terminal"
========================================
📊 Get Current Rate Limits
========================================
Chain:        Ethereum Sepolia
Remote Chain: Arbitrum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       View rate limits
Bucket:       Standard finality
========================================

Pool Version: v2

Current Rate Limiter State:
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:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Token Pool:   https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
========================================
```

2. Verify the Arbitrum Sepolia pool:

```bash filename="Terminal"
DEST_CHAIN=ETHEREUM_SEPOLIA \
  forge script \
  script/configure/rate-limiter/GetCurrentRateLimits.s.sol \
  --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL
```

Your output should look something like this:

```bash filename="Terminal"
========================================
📊 Get Current Rate Limits
========================================
Chain:        Arbitrum Sepolia
Remote Chain: Ethereum Sepolia
Token Pool:   0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Action:       View rate limits
Bucket:       Standard finality
========================================

Pool Version: v2

Current Rate Limiter State:
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:   0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Token Pool:   https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
========================================
```

> **NOTE: Token bucket values**
>
> The `Tokens` field shows the current fill level of the bucket. Immediately after an update the bucket
> resets to full capacity. Over time, as transfers consume tokens, this value decreases and refills at the configured
> rate.

### 4. Test the Rate Limiter Configurations

Initiate a cross-chain transfer to verify that the rate limiter enforces the configured limits.

> **NOTE: Token balance**
>
> Ensure your EOA has a sufficient balance of the cross-chain token on the source chain. If you need more tokens, use
> the `MintTokens.s.sol` script or transfer tokens from another account.

Before testing, export the router and token addresses for the source chain:

```bash filename="Terminal"
export ETHEREUM_SEPOLIA_ROUTER=0x...
export ETHEREUM_SEPOLIA_TOKEN=0x...
```

> **NOTE: Router address**
>
> You can find the router address for your chain in the [CCIP
> Directory](https://docs.chain.link/ccip/directory/testnet).

> **Note:**
>
> 1. Add `--fee-token LINK` to pay CCIP fees in LINK. If you omit this flag, fees are paid in the native gas token.
> 2. 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.

```bash filename="Terminal"
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 foundry:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL"
```

Your output should look something like this:

```bash filename="Terminal"
Fee: 130129888907619n = 0.000130129888907619 ETH
✔ Enter password for Foundry 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.

> **NOTE: How does the token bucket work?**
>
> CCIP rate limiters use a  model. Think of the bucket as a virtual allowance that controls how
> many tokens can flow through the pool in a given window:
>
> 1. The bucket starts full at its configured **capacity**.
> 2. Each transfer **consumes** its amount from the bucket.
> 3. The bucket **refills** continuously at the configured **rate** (tokens per second).
>
> In this tutorial, the outbound bucket has a capacity of 10 tokens and refills at 0.1 tokens/sec. Transferring all 10
> tokens in one go "drains" the bucket to zero. Any immediate follow-up transfer will fail because the bucket needs 100
> seconds (10 ÷ 0.1) to fully replenish.

#### Test 2: Drain the bucket, then hit the rate

1. **First transfer** (succeeds: drains the outbound bucket):

```bash filename="Terminal"
ccip-cli send \
  --source ethereum-testnet-sepolia \
  --router $ETHEREUM_SEPOLIA_ROUTER \
  --dest ethereum-testnet-sepolia-arbitrum-1 \
  --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=10 \
  --wallet foundry:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL"
```

Your output should look something like this:

```bash filename="Terminal"
Fee: 130129888907619n = 0.000130129888907619 ETH
✔ Enter password for Foundry keystore 'PRIVATE_KEY'
🚀 Sending message to 0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3 @ ethereum-testnet-sepolia-arbitrum-1 , tx => 0x3dc40bea29f3e3fc93ff8fce0dda45fd7f55a07ace5089646e018874c8b6745e , messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293
CCIP Explorer: https://ccip.chain.link/msg/0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293
```

2. **Second transfer** (fails: bucket has not refilled):

Run the same command immediately:

```bash filename="Terminal"
ccip-cli send \
  --source ethereum-testnet-sepolia \
  --router $ETHEREUM_SEPOLIA_ROUTER \
  --dest ethereum-testnet-sepolia-arbitrum-1 \
  --transfer-tokens $ETHEREUM_SEPOLIA_TOKEN=10 \
  --wallet foundry:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL"
```

Your output should look something like this:

```bash filename="Terminal"
Fee: 130129888907619n = 0.000130129888907619 ETH
✔ Enter password for Foundry 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`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol) 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).

> **NOTE: Pool type and lockbox scope**
>
> Rate limiters are configured on token pools, regardless of whether you registered your token with Burn & Mint or
> Lock & Mint. In Lock & Mint, the `ERC20LockBox` is not rate-limited and is not updated by these
> scripts.

> **CAUTION: Faster Than Finality must be allowed first**
>
> 1. Updating the faster than finality bucket alone does **not** enable faster than finality transfers.
>    The pool owner must also configure the pool's **allowed finality config** with `SetFinalityConfig.s.sol`.
> 2. Setting the allowed finality config is owner-only. Updating rate limiters can be done by the pool owner or the
>    configured `rateLimitAdmin`.
> 3. For the full faster than finality setup flow, see the Configure Faster Than Finality section in either
>    registration tutorial:
>
>    - [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry#optional-configure-faster-than-finality-for-the-token-pool)
>    - [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-foundry#optional-configure-fast-finality-for-the-token-pool)

#### Check the pool's allowed finality config

Use `GetFinalityConfig.s.sol` to confirm which faster than finality modes the pool currently allows.

[GetFinalityConfig.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/finality-config/GetFinalityConfig.s.sol)

View the finality config query script on GitHub.

1. Check the Ethereum Sepolia pool:

```bash filename="Terminal"
forge script \
  script/configure/finality-config/GetFinalityConfig.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
```

Your output should look something like this:

```bash filename="Terminal"
========================================
⏱️  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)`.

2. Check the Arbitrum Sepolia pool:

```bash filename="Terminal"
forge script \
  script/configure/finality-config/GetFinalityConfig.s.sol \
  --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL
```

#### Approach 1: Allow Faster Than Finality and Configure the Faster Than Finality Bucket in One Command

Use `SetFinalityConfig.s.sol` with the rate limit environment variables to allow faster than finality and configure the faster than finality bucket in a single broadcast.

[SetFinalityConfig.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/configure/finality-config/SetFinalityConfig.s.sol)

View the finality config update script on GitHub.

This example configures the Ethereum Sepolia pool for transfers to Arbitrum Sepolia. To configure the reverse direction, run the same script on Arbitrum Sepolia with `DEST_CHAIN=ETHEREUM_SEPOLIA`.

```bash filename="Terminal"
BLOCK_DEPTH=32 \
  DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \
  OUTBOUND_RATE_LIMIT_CAPACITY=5000000000000000000 \
  OUTBOUND_RATE_LIMIT_RATE=50000000000000000 \
  INBOUND_RATE_LIMIT_CAPACITY=5500000000000000000 \
  INBOUND_RATE_LIMIT_RATE=55000000000000000 \
  forge script \
  script/configure/finality-config/SetFinalityConfig.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

Your output should look something like this:

> **NOTE: Output abbreviated**
>
> `SetFinalityConfig.s.sol` also prints the lane's current and updated rate limits (faster than finality
> where enabled, standard otherwise). Those sections are omitted here for brevity.

```bash filename="Terminal"
========================================
⏱️  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
✅ 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

✅ Rate limits updated successfully!

========================================
✅ Configuration Complete on Ethereum Sepolia!
========================================
Token Pool:      0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Finality Config: 0x00000020
Mode:            BLOCK_DEPTH (32 blocks)
Token Pool:      https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
========================================
```

#### 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.s.sol` with `FAST_FINALITY=true`:

```bash filename="Terminal"
DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \
  FAST_FINALITY=true \
  OUTBOUND_RATE_LIMIT_CAPACITY=5000000000000000000 \
  OUTBOUND_RATE_LIMIT_RATE=50000000000000000 \
  INBOUND_RATE_LIMIT_CAPACITY=5500000000000000000 \
  INBOUND_RATE_LIMIT_RATE=55000000000000000 \
  forge script \
  script/configure/rate-limiter/UpdateRateLimiters.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

#### Verify the faster than finality bucket

Pass `FAST_FINALITY=true` to `GetCurrentRateLimits.s.sol`. 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.

> **NOTE: Foundry output label**
>
> Some current Foundry output labels this bucket as `Custom finality`. In this tutorial, that label refers to
> the faster than finality bucket selected by `FAST_FINALITY=true`.

```bash filename="Terminal"
DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \
  FAST_FINALITY=true \
  forge script \
  script/configure/rate-limiter/GetCurrentRateLimits.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
```

Your output should look something like this:

```bash filename="Terminal"
========================================
📊 Get Current Rate Limits
========================================
Chain:        Ethereum Sepolia
Remote Chain: Arbitrum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       View rate limits
Bucket:       Custom finality (standard finality fallback per direction if not enabled)
========================================

Pool Version: v2

Outbound [custom finality]:
Enabled:  true
Capacity: 5000000000000000000
Rate:     50000000000000000
Tokens:   5000000000000000000
Inbound [custom finality]:
Enabled:  true
Capacity: 5500000000000000000
Rate:     55000000000000000
Tokens:   5500000000000000000

========================================
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
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`):

```bash filename="Terminal"
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 foundry:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL"
```

Your output should look something like this:

```bash filename="Terminal"
Fee: 130129888907619n = 0.000130129888907619 ETH
✔ Enter password for Foundry 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.s.sol` with `FAST_FINALITY=true` to confirm the outbound faster than finality bucket changed after the transfer:

```bash filename="Terminal"
DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \
  FAST_FINALITY=true \
  forge script \
  script/configure/rate-limiter/GetCurrentRateLimits.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
```

> **NOTE: Faster Than Finality troubleshooting**
>
> 1. This test assumes the pool's allowed finality config allows numeric block depth. If the transfer reverts with `InvalidRequestedFinality(requestedFinality, allowedFinality)`, run `GetFinalityConfig.s.sol` and confirm the configured `BLOCK_DEPTH`, then retry with `--extra finality=32` (or higher than the configured minimum).
> 2. If you request a numeric value lower than the configured minimum block depth, the token pool will revert.

> **CAUTION: Educational Example Disclaimer**
>
> Please note, this page contains community examples only — these are not Chainlink products or services and are not
> supported or maintained by Chainlink. This code represents an example of using a Chainlink product or service, and is
> intended for demonstration and educational purposes only. It is provided "AS IS" and "AS AVAILABLE" without warranties
> of any kind, may not have been audited, and may omit checks or error handling. Each party intending to use this
> example code does so entirely at their own risk and must perform its own audits, security and code review, key
> management, and testing before any production deployment and ensure the operation and performance of such code matches
> expectations. Neither Chainlink Labs nor the Chainlink Foundation deploys, operates, monitors, maintains or endorses
> any deployment of this code. Note that this is not a Chainlink product, feature or service, and there are no
> commitments made with respect to the code, including compatibility with future Chainlink releases. You should not rely
> on this code without first conducting your own technical, engineering, and security review. This code is also outside
> the scope of any Chainlink bug bounty programs. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node
> operators are responsible for outcomes due to errors in this example or how it is deployed or operated, or liable for
> any resulting claims or damages. Use of the Chainlink Network is subject to the Chainlink Foundation [Terms of
> Service](https://chain.link/terms), which provides important information and disclosures. By using this code, you
> acknowledge and agree to these terms.