# Manage Token Transfer Fees Using Hardhat
Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-hardhat
Last Updated: 2026-05-05

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

## Guide Versions

- [Foundry](/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-foundry)

- [Hardhat](/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-hardhat)

CCIP 2.0 token pools let pool owners opt into **pool-level token transfer fee overrides** per destination lane. When an override is disabled, the [OnRamp](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/onRamp/OnRamp.sol) falls back to [`FeeQuoter`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/FeeQuoter.sol) defaults for that lane. In this tutorial you will:

1. Review the current fee configuration for both configured lanes.
2. Optionally set a fee admin who can withdraw accrued fees.
3. Configure lane-specific flat fees, transfer fees, destination gas overhead, and destination bytes overhead.
4. Send a LINK-paid transfer and inspect accrued fees in the source token pool.
5. Withdraw accrued fee tokens.
6. Disable the fee configuration on both lanes to return to `FeeQuoter` defaults.

> **NOTE: Defaults apply unless you opt in**
>
> 1. **Fee overrides are per lane:** each direction is configured independently (Ethereum Sepolia →
>    Arbitrum Sepolia vs. Arbitrum Sepolia → Ethereum Sepolia).
> 2. **FeeQuoter is the default:** when `isEnabled: false` for a lane, the OnRamp uses
>    FeeQuoter defaults for that lane.
> 3. **Faster Than Finality fees are opt-in:** the `fastFinality*` fields only apply when you
>    request block-depth finality (for example, `--extra finality=32`).

## 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 the [Node.js documentation](https://nodejs.org/en/download/).
   - `npm` is bundled with Node.js. If you can't run `npm`, reinstall or update Node.js from the official installer.

2. Install/Update Chainlink `ccip-cli`:

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

3. Clone the repository and navigate to the project directory:

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

Clone docs-cct-hardhat for the CCT Hardhat scripts used in this tutorial.

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

4. 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 `npx hardhat keystore set`)
KEYSTORE_NAME=<your_private_key_name>

# Optional: keystore name for a configured fee admin
FEE_ADMIN_KEYSTORE_NAME=your_fee_admin_keystore_name

# RPC URLs
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
```

Use the [CCIP Directory](/ccip/directory/testnet) or `helper-config.ts` to confirm the router and LINK addresses for your network.

> **TIP: Funding**
>
> 1. Configuration and withdrawal transactions require native gas on the source chain where you broadcast.
> 2. The `ccip-cli --fee-token LINK` examples require LINK on the source chain. You can get testnet LINK
>    from the [Chainlink faucet](https://faucets.chain.link/).
> 3. Token transfer examples require the sender to hold the source-chain token being transferred.

[Complete List of Supported Chains](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/helper-config.ts)

View the complete list of supported chains in the `helper-config.ts` file.

5. Load your environment variables:

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

6. Build the project:

```bash filename="Terminal"
npm install && npx hardhat compile
```

7. Create an encrypted Hardhat keystore, if you haven't already:

```bash filename="Terminal"
npx hardhat keystore set PRIVATE_KEY
```

Optionally, create an encrypted Hardhat keystore for the fee admin, if you haven't already:

```bash filename="Terminal"
npx hardhat keystore set FEE_ADMIN_PRIVATE_KEY
```

> **CAUTION: Private key format**
>
> When prompted for your private key, include the `0x` prefix.

> **NOTE: Hardhat keystore**
>
> 1. Hardhat prompts for a keystore password: set it on first use, or enter it to unlock on subsequent uses. Then it
>    asks you to enter your private key as hidden text.
> 2. The name you choose, such as `PRIVATE_KEY`, is what you set as `KEYSTORE_NAME` in `.env`.
> 3. Use `npx hardhat keystore list` to check previously created keystores.

## Tutorial

> **TIP: Prerequisites**
>
> This tutorial assumes you have already deployed tokens and token pools on both chains. If not, complete one of the two
> tutorials first:
>
> - [Burn & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat)
> - [Lock & Mint](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-lock-mint-hardhat)
>
> Use the token pool address for the chain you are configuring. In a Burn & Mint setup, both pools are `BurnMintTokenPool` contracts. In a Lock & Mint setup, the source chain can use a `LockReleaseTokenPool` backed by an `ERC20LockBox`, while the destination chain uses a `BurnMintTokenPool`. This tutorial configures the token pool, not the lockbox.
>
> ```bash filename="Terminal"
> export ETHEREUM_SEPOLIA_TOKEN=0x...
> export ARBITRUM_SEPOLIA_TOKEN=0x...
>
> export ETHEREUM_SEPOLIA_TOKEN_POOL=0x...
> export ARBITRUM_SEPOLIA_TOKEN_POOL=0x...
> ```
>
> 1. `KEYSTORE_NAME` must resolve to the owner of the source-chain token pool when you set dynamic config or
>    fee config.
> 2. A configured fee admin can withdraw accrued fee tokens, but cannot set fee config or set another fee admin.

### 1. Verify and Set Fee Admin

The fee admin is part of the token pool dynamic configuration. Use `getDynamicConfig` to inspect the current router, rate limit admin, and fee admin before making any changes.

[getDynamicConfig.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/dynamic-config/getDynamicConfig.ts)

View the dynamic config query task on GitHub.

```bash filename="Terminal"
npx hardhat getDynamicConfig \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
⚙️  Get Dynamic Config
========================================
Chain:        Ethereum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       View dynamic config
========================================

Dynamic Configuration:
  Router:           0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59
  Rate Limit Admin: 0xYourAddressForRateLimitAdmin
  Fee Admin:        0xYourAddressForFeeAdmin

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

```bash filename="Terminal"
npx hardhat getDynamicConfig \
  --network arbitrumSepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
⚙️  Get Dynamic Config
========================================
Chain:        Arbitrum Sepolia
Token Pool:   0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Action:       View dynamic config
========================================

Dynamic Configuration:
  Router:           0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165
  Rate Limit Admin: 0xYourAddressForRateLimitAdmin
  Fee Admin:        0xYourAddressForFeeAdmin

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

Use `setDynamicConfig` to set a fee admin if you want to. This task sets the router, rate limit admin, and fee admin together, so pass the current router and rate limit admin from the previous output unless you intend to change them.

[setDynamicConfig.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/dynamic-config/setDynamicConfig.ts)

View the dynamic config update task on GitHub.

| Input              | Required | Description                                                                                                   |
| ------------------ | -------- | ------------------------------------------------------------------------------------------------------------- |
| `--router`         | No       | Router to keep or set. Use the current router from `getDynamicConfig` unless intentionally changing it.       |
| `--ratelimitadmin` | No       | Rate limit admin to keep or set. Use the current value unless intentionally changing it.                      |
| `--feeadmin`       | No       | Address allowed to withdraw accrued fee tokens. Set to `address(0)` to restrict withdrawal to the pool owner. |
| `KEYSTORE_NAME`    | Yes      | Hardhat keystore name for the pool owner (set in `.env`).                                                     |

1. Set the fee admin on the Ethereum Sepolia pool:

```bash filename="Terminal"
npx hardhat setDynamicConfig \
  --router 0xCurrentRouterFromOutput \
  --ratelimitadmin 0xYourAddressForRateLimitAdmin \
  --feeadmin 0xYourFeeAdminAddress \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
⚙️  Set Dynamic Config
========================================
Chain:        Ethereum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       Set dynamic config
========================================

Current Configuration:
  Router:           0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59
  Rate Limit Admin: 0xYourAddressForRateLimitAdmin
  Fee Admin:        0xYourAddressForFeeAdmin

New Configuration:
  Router:           0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59
  Rate Limit Admin: 0xYourAddressForRateLimitAdmin
  Fee Admin:        0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994

[Step 1] Setting dynamic config on Ethereum Sepolia
⏳ Tx: https://sepolia.etherscan.io/tx/0x0f4fda7e9e39f35d2bb6d6b7967db7f1b8d2d99fc1f7a64dfe8a3f0d8c7a2b31
✅ Dynamic config updated successfully!

========================================
✅ Configuration Complete on Ethereum Sepolia!
========================================
Token Pool:       https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Router:           0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59
Rate Limit Admin: 0xYourAddressForRateLimitAdmin
Fee Admin:        0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
Transaction:      https://sepolia.etherscan.io/tx/0x0f4fda7e9e39f35d2bb6d6b7967db7f1b8d2d99fc1f7a64dfe8a3f0d8c7a2b31
========================================
```

2. Set the fee admin on the Arbitrum Sepolia pool:

```bash filename="Terminal"
npx hardhat setDynamicConfig \
  --router 0xCurrentRouterFromOutput \
  --ratelimitadmin 0xYourAddressForRateLimitAdmin \
  --feeadmin 0xYourFeeAdminAddress \
  --network arbitrumSepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
⚙️  Set Dynamic Config
========================================
Chain:        Arbitrum Sepolia
Token Pool:   0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Action:       Set dynamic config
========================================

Current Configuration:
  Router:           0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165
  Rate Limit Admin: 0xYourAddressForRateLimitAdmin
  Fee Admin:        0xYourAddressForFeeAdmin

New Configuration:
  Router:           0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165
  Rate Limit Admin: 0xYourAddressForRateLimitAdmin
  Fee Admin:        0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994

[Step 1] Setting dynamic config on Arbitrum Sepolia
⏳ Tx: https://sepolia.arbiscan.io/tx/0xe5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4
✅ Dynamic config updated successfully!

========================================
✅ Configuration Complete on Arbitrum Sepolia!
========================================
Token Pool:       https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Router:           0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165
Rate Limit Admin: 0xYourAddressForRateLimitAdmin
Fee Admin:        0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
Transaction:      https://sepolia.arbiscan.io/tx/0xe5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4
========================================
```

> **NOTE: Fee admin permissions**
>
> 1. The pool owner can set dynamic config and token transfer fee config.
> 2. The pool owner (or a configured fee admin) can withdraw accrued fee tokens. Anyone can inspect fee token balances.

### 2. Review Current Fee Configuration

Use `getTokenTransferFeeConfig` to read the current fee configuration for a destination lane from the source-chain token pool.

[getTokenTransferFeeConfig.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/fee-config/getTokenTransferFeeConfig.ts)

View the fee config query task on GitHub.

| Input               | Required | Description                                                                                                                               |
| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `--destchain`       | Yes      | Remote chain whose lane is being queried.                                                                                                 |
| Source pool env var | Yes      | `ETHEREUM_SEPOLIA_TOKEN_POOL`, `ARBITRUM_SEPOLIA_TOKEN_POOL`, or the `--tokenpool` override for the source chain selected by `--network`. |

1. Check the Ethereum Sepolia pool for the lane to Arbitrum Sepolia:

```bash filename="Terminal"
npx hardhat getTokenTransferFeeConfig \
  --destchain arbitrumSepolia \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
💰 Get Token Transfer Fee Config
========================================
Chain:        Ethereum Sepolia
Remote Chain: Arbitrum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       View fee config
========================================

Dest Chain Selector: 3478487238524512106

Fee Configuration:
  isEnabled:                   false
  destGasOverhead:             0
  destBytesOverhead:           0
  finalityFeeUSDCents:         0
  fastFinalityFeeUSDCents:     0
  finalityTransferFeeBps:      0
  fastFinalityTransferFeeBps:  0

⚠️  Fee config is disabled for this lane.
   The OnRamp will fall back to FeeQuoter defaults for this destination.

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

2. Check the Arbitrum Sepolia pool for the lane to Ethereum Sepolia:

```bash filename="Terminal"
npx hardhat getTokenTransferFeeConfig \
  --destchain sepolia \
  --network arbitrumSepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
💰 Get Token Transfer Fee Config
========================================
Chain:        Arbitrum Sepolia
Remote Chain: Ethereum Sepolia
Token Pool:   0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Action:       View fee config
========================================

Dest Chain Selector: 16015286601757825753

Fee Configuration:
  isEnabled:                   false
  destGasOverhead:             0
  destBytesOverhead:           0
  finalityFeeUSDCents:         0
  fastFinalityFeeUSDCents:     0
  finalityTransferFeeBps:      0
  fastFinalityTransferFeeBps:  0

⚠️  Fee config is disabled for this lane.
   The OnRamp will fall back to FeeQuoter defaults for this destination.

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

> **NOTE: Fee config disabled by default**
>
> 1. When fee config is disabled, the OnRamp uses FeeQuoter defaults for token transfer fee calculation on that source -> destination lane.
> 2. If you have already configured fees, the task will show non-zero values and `isEnabled: true`.

### 3. Set Token Transfer Fee Configuration

Use `updateTokenTransferFeeConfig` to configure fee parameters for a destination lane on the source-chain token pool.

[updateTokenTransferFeeConfig.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/fee-config/updateTokenTransferFeeConfig.ts)

View the fee config update task on GitHub.

| Input                          | Required | Description                                                                                                                               |
| ------------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `--destchain`                  | Yes      | Remote chain to configure fees for.                                                                                                       |
| Source pool env var            | Yes      | `ETHEREUM_SEPOLIA_TOKEN_POOL`, `ARBITRUM_SEPOLIA_TOKEN_POOL`, or the `--tokenpool` override for the source chain selected by `--network`. |
| `KEYSTORE_NAME`                | Yes      | Hardhat keystore name for the pool owner (set in `.env`).                                                                                 |
| `--destgasoverhead`            | No       | Gas overhead charged in the fee model. Must be greater than `0` when enabled.                                                             |
| `--destbytesoverhead`          | No       | Data availability bytes overhead. Use at least `32` for Burn & Mint and Lock & Release pools.                                             |
| `--finalityfeeusdcents`        | No       | Flat fee in 0.01 USD units for default-finality transfers.                                                                                |
| `--fastfinalityfeeusdcents`    | No       | Flat fee in 0.01 USD units for fast-finality transfers.                                                                                   |
| `--finalitytransferfeebps`     | No       | Basis-point fee deducted from the transferred amount for default-finality transfers. Must be below `10000`.                               |
| `--fastfinalitytransferfeebps` | No       | Basis-point fee deducted from the transferred amount for fast-finality transfers. Must be below `10000`.                                  |
| `--disable`                    | No       | Set to disable the fee config for this lane.                                                                                              |
| `--tokenpool`                  | No       | Token pool address. Overrides the `{CHAIN}_TOKEN_POOL` env var.                                                                           |

> **NOTE: Fee units: basis points vs flat fee**
>
> - **Flat fee:** values are in `0.01 USD (1 cent)` units. For example, `1` means
>   $0.01 and `100` means $1.00.
> - **Basis points (bps):** `1 basis points = 0.01%`, `100 basis points = 1%`, and `10000 basis points = 100%`. Values must be below `10000`.

> **CAUTION: Validation rules**
>
> 1. `--destgasoverhead` must be greater than `0` when the config is enabled.
> 2. `--destbytesoverhead` must be at least `32` for Burn & Mint and Lock & Release pools.
> 3. Basis-point fee values must be below `10000`.
> 4. First-time setup should pass every fee field shown in this tutorial. Later updates can omit fields you do not want
>    to change because the task defaults omitted fields to the current on-chain values.

This section uses a **two-step update demo** for the Ethereum Sepolia → Arbitrum Sepolia lane so you can see that **omitting fields does not reset them**.

- **Step 1** sets a basis-point fee for default-finality transfers.
- **Step 2** adds a flat fee for fast-finality transfers, while intentionally omitting the basis points flag so it carries forward.

Fee behavior in this tutorial:

- **bps fields** are deducted from the transferred token amount and retained by the source pool as the pool token.
- **Flat fee fields** are charged in the selected fee token (LINK in this tutorial) and credited to the source pool.

#### Step 1: Configure a basis points fee for default-finality transfers (ETH Sepolia → Arbitrum Sepolia)

```bash filename="Terminal"
npx hardhat updateTokenTransferFeeConfig \
  --destchain arbitrumSepolia \
  --destgasoverhead 50000 \
  --destbytesoverhead 32 \
  --finalityfeeusdcents 0 \
  --finalitytransferfeebps 25 \
  --fastfinalityfeeusdcents 0 \
  --fastfinalitytransferfeebps 0 \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
💰 Update Token Transfer Fee Config
========================================
Chain:        Ethereum Sepolia
Remote Chain: Arbitrum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       Set fee config
========================================

Dest Chain Selector: 3478487238524512106

Current On-Chain Fee Configuration:
  isEnabled:                   false
  destGasOverhead:             0
  destBytesOverhead:           0
  finalityFeeUSDCents:         0
  fastFinalityFeeUSDCents:     0
  finalityTransferFeeBps:      0
  fastFinalityTransferFeeBps:  0

Fee Configuration to Apply:
  destGasOverhead:             50000
  destBytesOverhead:           32
  finalityFeeUSDCents:         0
  fastFinalityFeeUSDCents:     0
  finalityTransferFeeBps:      25
  fastFinalityTransferFeeBps:  0

[Step 1] Applying fee config for lane to Arbitrum Sepolia
⏳ Tx: https://sepolia.etherscan.io/tx/0x...
✅ Fee config applied successfully!

========================================
✅ Operation Complete!
========================================
Token Pool:   https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
========================================
```

#### Step 2: Add a flat fee for fast-finality transfers (ETH Sepolia → Arbitrum Sepolia)

This command intentionally omits `--finalitytransferfeebps`. The task defaults omitted fields to the current on-chain values, so the 25 basis points default-finality fee carries forward.

```bash filename="Terminal"
npx hardhat updateTokenTransferFeeConfig \
  --destchain arbitrumSepolia \
  --fastfinalityfeeusdcents 150 \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
💰 Update Token Transfer Fee Config
========================================
Chain:        Ethereum Sepolia
Remote Chain: Arbitrum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       Set fee config
========================================

Dest Chain Selector: 3478487238524512106

Current On-Chain Fee Configuration:
  isEnabled:                   true
  destGasOverhead:             50000
  destBytesOverhead:           32
  finalityFeeUSDCents:         0
  fastFinalityFeeUSDCents:     0
  finalityTransferFeeBps:      25
  fastFinalityTransferFeeBps:  0

Fee Configuration to Apply:
  destGasOverhead:             50000
  destBytesOverhead:           32
  finalityFeeUSDCents:         0
  fastFinalityFeeUSDCents:     150
  finalityTransferFeeBps:      25
  fastFinalityTransferFeeBps:  0

[Step 1] Applying fee config for lane to Arbitrum Sepolia
⏳ Tx: https://sepolia.etherscan.io/tx/0x...
✅ Fee config applied successfully!

========================================
✅ Operation Complete!
========================================
Token Pool:   https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
========================================
```

#### Repeat for the reverse lane (Arbitrum Sepolia → Ethereum Sepolia)

Repeat the same two steps on the Arbitrum Sepolia token pool by switching the network and destination chain.

Step 1:

```bash filename="Terminal"
npx hardhat updateTokenTransferFeeConfig \
  --destchain sepolia \
  --destgasoverhead 50000 \
  --destbytesoverhead 32 \
  --finalityfeeusdcents 0 \
  --finalitytransferfeebps 25 \
  --fastfinalityfeeusdcents 0 \
  --fastfinalitytransferfeebps 0 \
  --network arbitrumSepolia
```

Step 2:

```bash filename="Terminal"
npx hardhat updateTokenTransferFeeConfig \
  --destchain sepolia \
  --fastfinalityfeeusdcents 150 \
  --network arbitrumSepolia
```

Both steps should complete successfully and print the token pool address and transaction hash for the update.

### 4. Verify the Updated Fee Configuration

Re-run `getTokenTransferFeeConfig` to confirm the final (Step 2) configuration for the Ethereum Sepolia → Arbitrum Sepolia lane.

1. Verify the Ethereum Sepolia pool for the lane to Arbitrum Sepolia:

```bash filename="Terminal"
npx hardhat getTokenTransferFeeConfig \
  --destchain arbitrumSepolia \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
💰 Get Token Transfer Fee Config
========================================
Chain:        Ethereum Sepolia
Remote Chain: Arbitrum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       View fee config
========================================

Dest Chain Selector: 3478487238524512106

Fee Configuration:
  isEnabled:                   true
  destGasOverhead:             50000
  destBytesOverhead:           32
  finalityFeeUSDCents:         0
  fastFinalityFeeUSDCents:     150
  finalityTransferFeeBps:      25
  fastFinalityTransferFeeBps:  0

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

2. Repeat for the reverse lane (Arbitrum Sepolia → Ethereum Sepolia):

```bash filename="Terminal"
npx hardhat getTokenTransferFeeConfig \
  --destchain sepolia \
  --network arbitrumSepolia
```

Success criteria:

- Ethereum Sepolia → Arbitrum Sepolia shows `isEnabled: true`.
- Ethereum Sepolia → Arbitrum Sepolia shows `destGasOverhead: 50000`.
- Ethereum Sepolia → Arbitrum Sepolia shows `destBytesOverhead: 32`.
- Ethereum Sepolia → Arbitrum Sepolia shows `finalityFeeUSDCents: 0` and `finalityTransferFeeBps: 25`.
- Ethereum Sepolia → Arbitrum Sepolia shows `fastFinalityFeeUSDCents: 150` and `fastFinalityTransferFeeBps: 0`.
- The reverse lane should show the same values after you repeat the two-step update.

### 5. Send Transfers That Use the Fee Config

This section demonstrates Ethereum Sepolia → Arbitrum Sepolia cross-chain transfers. Repeat the same pattern from Arbitrum Sepolia if you want the Arbitrum Sepolia source pool to accrue fees.

Before sending, inspect the source pool balances for LINK and the source token:

[getFeeTokenBalances.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/operations/getFeeTokenBalances.ts)

View the fee-token balance inspection task on GitHub.

> **NOTE: Note**
>
> 1. Export the `LINK` token address for the source chain. For example, `export ETHEREUM_SEPOLIA_LINK=0x...`.
> 2. Export the Router address for the source chain. For example, `export ETHEREUM_SEPOLIA_ROUTER=0x...`.
> 3. You can find a complete list of all addresses inside the [CCIP
>    Directory](https://docs.chain.link/ccip/directory/testnet).

```bash filename="Terminal"
npx hardhat getFeeTokenBalances \
  --feetokens "$ETHEREUM_SEPOLIA_LINK,$ETHEREUM_SEPOLIA_TOKEN" \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
🔍 Get Fee Token Balances
========================================
Chain:        Ethereum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       Inspect fee token balances
========================================

Pool Token:   0xD73D06d36cEa36817186779fF877e418Ab759F09 (for reference)

Fee Token Balances:
  [0] 0x779877A7B0D9E8603169DdbD7836e478b4624789  →  balance: 0 ⚠️  (skipping)
  [1] 0xD73D06d36cEa36817186779fF877e418Ab759F09  →  balance: 0 ⚠️  (skipping)

ℹ️  No fee tokens have a non-zero balance in the pool. Nothing to withdraw.
========================================
```

1. Send a default-finality transfer and pay CCIP fees in LINK:

```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.23 \
  --receiver 0xYourReceiverAddress \
  --fee-token LINK \
  --wallet hardhat:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ARBITRUM_SEPOLIA_RPC_URL"
```

Your output should look something like this:

```bash filename="Terminal"
Fee: 119317588427515265n = 0.119317588427515265 LINK
Request (source):
┌────────────────────────────────┬──────────────────────────────────────────────────────────────────────┐
│ (index)                        │ Values                                                               │
├────────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
│ fee                            │ '0.119317588427515265 LINK'                                          │
│ tokens                         │ '1.226925 BnM-T'                                                     │
│ tokenAmountBeforeTokenPoolFees │ 1230000000000000000n                                                 │
│ receipts[1].issuer             │ '0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9'                         │
│ receipts[1].feeTokenAmount     │ 93786750729051300n                                                   │
│ finality                       │ 'finalized'                                                          │
└────────────────────────────────┴──────────────────────────────────────────────────────────────────────┘
```

This send uses `finalityTransferFeeBps` (25 bps). In this demo, `finalityFeeUSDCents` is `0`.

2. Send a fast-finality transfer with block-depth finality and pay CCIP fees in LINK:

```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.23 \
  --receiver 0xYourReceiverAddress \
  --fee-token LINK \
  --extra finality=32 \
  --wallet hardhat:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ARBITRUM_SEPOLIA_RPC_URL"
```

Your output should look something like this:

```bash filename="Terminal"
Request (source):
┌────────────────────────────────┬──────────────────────────────────────────────────────────────────────┐
│ (index)                        │ Values                                                               │
├────────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
│ tokenAmountBeforeTokenPoolFees │ 1230000000000000000n                                                 │
│ tokens                         │ '1.23 BnM-T'                                                         │
└────────────────────────────────┴──────────────────────────────────────────────────────────────────────┘
```

This send uses `fastFinalityFeeUSDCents` (150). In this demo, `fastFinalityTransferFeeBps` is `0`.

> **CAUTION: Faster Than Finality must be allowed**
>
> 1. This send uses `fastFinalityFeeUSDCents` and `fastFinalityTransferFeeBps`. Use `--extra finality=<N>` only when the pool allows block-depth finality.
> 2. If the pool does not allow block-depth finality, or if the requested `finality=<N>` is below the pool minimum, the send reverts.

#### How to read fees from the `ccip-cli` output

- The top-level `fee: <X> LINK` is the total fee paid by the sender. It is not necessarily the amount credited to the token pool.
- For the fee-token amount credited to the pool, find the receipt where `receipts[i].issuer` equals the source token pool address. The corresponding `receipts[i].feeTokenAmount` is denominated in the fee token (LINK) in wei.
- For basis points deductions, compare `tokenAmountBeforeTokenPoolFees` with the `tokens` field. Their difference is the token amount retained by the source pool.

### 6. Inspect and Withdraw Accrued Fees

After the transfers complete, inspect the source pool balances again:

```bash filename="Terminal"
npx hardhat getFeeTokenBalances \
  --feetokens "$ETHEREUM_SEPOLIA_LINK,$ETHEREUM_SEPOLIA_TOKEN" \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
🔍 Get Fee Token Balances
========================================
Chain:        Ethereum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       Inspect fee token balances
========================================

Pool Token:   0xD73D06d36cEa36817186779fF877e418Ab759F09 (for reference)

Fee Token Balances:
  [0] 0x779877A7B0D9E8603169DdbD7836e478b4624789  →  balance: <LINK_BALANCE_WEI>
  [1] 0xD73D06d36cEa36817186779fF877e418Ab759F09  →  balance: 3075000000000000

✅ 2 token(s) with non-zero balances are ready for withdrawal.

To withdraw, run:
  npx hardhat withdrawFeeTokens --feetokens "0x779877A7B0D9E8603169DdbD7836e478b4624789,0xD73D06d36cEa36817186779fF877e418Ab759F09" --network sepolia
  (Add --recipient 0x... to send to a different address; defaults to the deployer wallet.)
========================================
```

Expected balances:

- **Source token (bps fee)**: The default-finality send uses `finalityTransferFeeBps = 25`. With a `1.23` token transfer:
  - Retained by the pool: (1.23 \times 25 / 10000 = 0.003075) tokens
  - Verify in `ccip-cli` output: `tokenAmountBeforeTokenPoolFees (1.23)` - `tokens received (1.226925)` = `0.003075`
- **LINK (fee token credited to the pool)**: The fee-token amount credited to the token pool is visible in the send output receipt where `receipts[i].issuer` equals the source token pool address. Sum `receipts[i].feeTokenAmount` across the default-finality and fast-finality sends, then convert from wei to LINK by dividing by (10^). Confirm it matches `getFeeTokenBalances` for LINK.

Set a value for the `FEE_ADMIN_KEYSTORE_NAME` variable inside your `.env`, using the instructions covered inside [Set Up Your Development Environment](/ccip/evm/tutorials/cross-chain-tokens/set-transfer-fee-config-hardhat#set-up-your-development-environment).

Use `withdrawFeeTokens` to withdraw accrued fee balances to a recipient. Withdraw as the configured fee admin:

[withdrawFeeTokens.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/operations/withdrawFeeTokens.ts)

View the fee-token withdrawal task on GitHub.

```bash filename="Terminal"
KEYSTORE_NAME=$FEE_ADMIN_KEYSTORE_NAME \
  npx hardhat withdrawFeeTokens \
  --recipient 0xYourRecipientAddress \
  --feetokens "$ETHEREUM_SEPOLIA_LINK,$ETHEREUM_SEPOLIA_TOKEN" \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
💸 Withdraw Fee Tokens
========================================
Chain:        Ethereum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       Withdraw fee tokens
========================================

Pool Token:   0xD73D06d36cEa36817186779fF877e418Ab759F09 (for reference)
Recipient:    0x368b6c4218Cd5B8feF04eFDb207Bd52f0566c411

Tokens to Withdraw:
  [0] 0x779877A7B0D9E8603169DdbD7836e478b4624789  →  balance: <LINK_BALANCE_WEI>
  [1] 0xD73D06d36cEa36817186779fF877e418Ab759F09  →  balance: 3075000000000000

[Step 1] Withdrawing fee tokens on Ethereum Sepolia
⏳ Tx: https://sepolia.etherscan.io/tx/0x5e1c0d9a8b7f6e5d4c3b2a190f1e2d3c4b5a69788796a5b4c3d2e1f0a9b8c7d6
✅ Fee tokens withdrawn successfully!

========================================
✅ Withdrawal complete on Ethereum Sepolia!
========================================
Token Pool:   https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Recipient:    https://sepolia.etherscan.io/address/0x368b6c4218Cd5B8feF04eFDb207Bd52f0566c411
========================================
```

Or withdraw as the pool owner:

```bash filename="Terminal"
npx hardhat withdrawFeeTokens \
  --recipient 0xYourRecipientAddress \
  --feetokens "$ETHEREUM_SEPOLIA_LINK,$ETHEREUM_SEPOLIA_TOKEN" \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
💸 Withdraw Fee Tokens
========================================
Chain:        Ethereum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       Withdraw fee tokens
========================================

Pool Token:   0xD73D06d36cEa36817186779fF877e418Ab759F09 (for reference)
Recipient:    0x368b6c4218Cd5B8feF04eFDb207Bd52f0566c411

Tokens to Withdraw:
  [0] 0x779877A7B0D9E8603169DdbD7836e478b4624789  →  balance: <LINK_BALANCE_WEI>
  [1] 0xD73D06d36cEa36817186779fF877e418Ab759F09  →  balance: 3075000000000000

[Step 1] Withdrawing fee tokens on Ethereum Sepolia
⏳ Tx: https://sepolia.etherscan.io/tx/0x9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d
✅ Fee tokens withdrawn successfully!

========================================
✅ Withdrawal complete on Ethereum Sepolia!
========================================
Token Pool:   https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Recipient:    https://sepolia.etherscan.io/address/0x368b6c4218Cd5B8feF04eFDb207Bd52f0566c411
========================================
```

Verify the balances after withdrawal:

```bash filename="Terminal"
npx hardhat getFeeTokenBalances \
  --feetokens "$ETHEREUM_SEPOLIA_LINK,$ETHEREUM_SEPOLIA_TOKEN" \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
🔍 Get Fee Token Balances
========================================
Chain:        Ethereum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       Inspect fee token balances
========================================

Pool Token:   0xD73D06d36cEa36817186779fF877e418Ab759F09 (for reference)

Fee Token Balances:
  [0] 0x779877A7B0D9E8603169DdbD7836e478b4624789  →  balance: 0 ⚠️  (skipping)
  [1] 0xD73D06d36cEa36817186779fF877e418Ab759F09  →  balance: 0 ⚠️  (skipping)

ℹ️  No fee tokens have a non-zero balance in the pool. Nothing to withdraw.
========================================
```

> **NOTE: ERC20 fee tokens only**
>
> - `withdrawFeeTokens` accepts ERC20 token addresses. It does not withdraw native gas tokens.
> - This tutorial uses LINK for the fee-token portion so the accrued flat fee component can be inspected and withdrawn
>   with the existing task.

### 7. Disable the Fee Configuration

Disabling fee configuration for a lane removes the lane-specific override and returns that lane to `FeeQuoter` defaults. Disable both lanes that you configured earlier.

1. Disable the fee config on the Ethereum Sepolia pool for the lane to Arbitrum Sepolia:

```bash filename="Terminal"
npx hardhat updateTokenTransferFeeConfig \
  --destchain arbitrumSepolia \
  --disable \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
💰 Update Token Transfer Fee Config
========================================
Chain:        Ethereum Sepolia
Remote Chain: Arbitrum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       Disable fee config
========================================

Dest Chain Selector: 3478487238524512106

[Step 1] Disabling fee config for lane to Arbitrum Sepolia
⏳ Tx: https://sepolia.etherscan.io/tx/0x6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b
✅ Fee config disabled for this lane.
   The OnRamp will now use FeeQuoter defaults for this destination.

========================================
✅ Operation Complete!
========================================
Token Pool:   https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
========================================
```

2. Verify the Ethereum Sepolia pool for the lane to Arbitrum Sepolia:

```bash filename="Terminal"
npx hardhat getTokenTransferFeeConfig \
  --destchain arbitrumSepolia \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
💰 Get Token Transfer Fee Config
========================================
Chain:        Ethereum Sepolia
Remote Chain: Arbitrum Sepolia
Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
Action:       View fee config
========================================

Dest Chain Selector: 3478487238524512106

Fee Configuration:
  isEnabled:                   false
  destGasOverhead:             0
  destBytesOverhead:           0
  finalityFeeUSDCents:         0
  fastFinalityFeeUSDCents:     0
  finalityTransferFeeBps:      0
  fastFinalityTransferFeeBps:  0

⚠️  Fee config is disabled for this lane.
   The OnRamp will fall back to FeeQuoter defaults for this destination.

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

3. Disable the fee config on the Arbitrum Sepolia pool for the lane to Ethereum Sepolia:

```bash filename="Terminal"
npx hardhat updateTokenTransferFeeConfig \
  --destchain sepolia \
  --disable \
  --network arbitrumSepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
💰 Update Token Transfer Fee Config
========================================
Chain:        Arbitrum Sepolia
Remote Chain: Ethereum Sepolia
Token Pool:   0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Action:       Disable fee config
========================================

Dest Chain Selector: 16015286601757825753

[Step 1] Disabling fee config for lane to Ethereum Sepolia
⏳ Tx: https://sepolia.arbiscan.io/tx/0xf60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5
✅ Fee config disabled for this lane.
   The OnRamp will now use FeeQuoter defaults for this destination.

========================================
✅ Operation Complete!
========================================
Token Pool:   https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
========================================
```

4. Verify the Arbitrum Sepolia pool for the lane to Ethereum Sepolia:

```bash filename="Terminal"
npx hardhat getTokenTransferFeeConfig \
  --destchain sepolia \
  --network arbitrumSepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
💰 Get Token Transfer Fee Config
========================================
Chain:        Arbitrum Sepolia
Remote Chain: Ethereum Sepolia
Token Pool:   0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
Action:       View fee config
========================================

Dest Chain Selector: 16015286601757825753

Fee Configuration:
  isEnabled:                   false
  destGasOverhead:             0
  destBytesOverhead:           0
  finalityFeeUSDCents:         0
  fastFinalityFeeUSDCents:     0
  finalityTransferFeeBps:      0
  fastFinalityTransferFeeBps:  0

⚠️  Fee config is disabled for this lane.
   The OnRamp will fall back to FeeQuoter defaults for this destination.

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

Both lanes should show `isEnabled: false` and zeroed fee fields. The OnRamp will use `FeeQuoter` defaults for both directions again.

> **CAUTION: Troubleshooting**
>
> - **Owner revert:** confirm `KEYSTORE_NAME` is the owner of the source-chain token pool.
> - **Fee admin withdrawal revert:** confirm the fee admin was set, the correct fee admin keystore is
>   used, and the action is withdrawal rather than owner-only configuration.
> - **Missing gas:** confirm the broadcaster has native gas on the source chain.
> - **Missing LINK:** confirm the sender has source-chain LINK when using `--fee-token LINK`.
> - **Missing lane:** confirm the pool lane was configured with `applyChainUpdates`.
> - **Wrong lane or pool:** confirm `--network`, `--destchain`, and the source pool
>   env var (or `--tokenpool`) match the intended source -> destination lane.
> - **Invalid config:** confirm `--destgasoverhead` is not `0`, `--destbytesoverhead` is at least `32`, and basis points values are below `10000`.
> - **Fast-finality revert:** confirm the pool allows block-depth finality and the requested `finality=<N>` is not below the pool minimum.
> - **No accrued fees:** confirm `--feetokens` includes LINK and the source token, the send
>   completed, native fee token was not used, and flat or basis points fee values are non-zero.

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