# Enable your tokens in CCIP (Burn & Mint): Register from an EOA using Foundry
Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry
Last Updated: 2025-10-30

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

## Guide Versions

- [Foundry (Burn & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry)

- [Hardhat (Burn & Mint)](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-hardhat)

In this tutorial you will:

1. Deploy a token on two chains using [Foundry](https://book.getfoundry.sh/).
2. Set up a `Burn Mint` token pool on each chain.
3. Configure and activate the pools in CCIP so your token becomes a Cross-Chain Token (CCT).
4. (Optional) Test the setup by minting and transferring tokens across networks.

> **NOTE: If you do not explicitly configure these features, default behavior applies**
>
> 1. **Default finality:** Transfers use the chain's native finality (equivalent to omitting `finality` or using `finality=finalized`) unless you opt into faster than finality via `finality` (for example, `finality=<N>` for block-depth faster than finality).
> 2. **Opt-in changes.** New v2 behavior only applies when you explicitly configure it (for example,
>    pool-level fees or faster than finality).

## 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 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 for --verify on Ethereum Sepolia)
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
```

### 2. How to configure deployment parameters?

Deployment parameters can be configured in two ways:

1. Edit the `.json` files in `script/input/`.
2. Pass environment variables inline at runtime.

> **Note:** Environment variables take precedence over `.json` file values.

#### Token Deployment Configuration

Default values live in `script/input/token.json`:

```json filename="script/input/token.json"
{
  "name": "BnM Test",
  "symbol": "BnM-T",
  "decimals": 18,
  "maxSupply": 0,
  "preMint": 0,
  "tokenAmountToMint": 1000000000000000000000,
  "tokenAmountToTransfer": 1000000000000000000
}
```

Deployment fields can be overridden with inline env vars:

| Env var                    | Overrides (`token.json`) | Optional | Description                                                                                                  |
| -------------------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------ |
| `TOKEN_NAME`               | `.name`                  | Yes      | Token name                                                                                                   |
| `TOKEN_SYMBOL`             | `.symbol`                | Yes      | Token symbol                                                                                                 |
| `TOKEN_DECIMALS`           | `.decimals`              | Yes      | Decimal places (usually `18`)                                                                                |
| `TOKEN_MAX_SUPPLY`         | `.maxSupply`             | Yes      | Max supply in smallest unit (`0` = unlimited)                                                                |
| `TOKEN_PRE_MINT`           | `.preMint`               | Yes      | Amount pre-minted to the configured recipient (`0` = none)                                                   |
| `TOKEN_PRE_MINT_RECIPIENT` | N/A (env-only)           | Yes      | Address receiving pre-minted tokens. If omitted and `TOKEN_PRE_MINT > 0`, the script defaults to broadcaster |
| `CCIP_ADMIN_ADDRESS`       | N/A (env-only)           | Yes      | Address assigned as CCIP admin on the token. If omitted, broadcaster is used                                 |

#### (Optional) Advanced Pool Hooks

> **NOTE: What are advanced pool hooks?**
>
> [Advanced Pool
> Hooks](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/AdvancedPoolHooks.sol)
> are an optional security layer you can attach to a token pool. They run checks:
>
> 1. **before** tokens are locked/burned (source chain)
> 2. **before** tokens are released/minted (destination chain)
>
> Most deployments do **not** need a hook. You would attach one only if you want to:
>
> - Make it so that only certain addresses can transfer your token cross-chain.
> - Put in place additional checks for large transfers above a threshold amount.
> - Plug in an external policy engine for custom compliance checks.

Default values live in `script/input/advanced-pool-hooks.json`:

```json filename="script/input/advanced-pool-hooks.json"
{
  "allowlist": [],
  "thresholdAmount": 0,
  "policyEngine": "0x0000000000000000000000000000000000000000",
  "authorizedCallers": []
}
```

All fields can be overridden with inline env vars:

| Env var              | Overrides (`advanced-pool-hooks.json`) | Optional | Description                                                                              |
| -------------------- | -------------------------------------- | -------- | ---------------------------------------------------------------------------------------- |
| `ALLOWLIST`          | `.allowlist`                           | Yes      | Addresses allowed to transfer (CSV or JSON array)                                        |
| `THRESHOLD_AMOUNT`   | `.thresholdAmount`                     | Yes      | Threshold amount after which additional hook checks/policy checks may apply (`0` = none) |
| `POLICY_ENGINE`      | `.policyEngine`                        | Yes      | Policy engine contract address                                                           |
| `AUTHORIZED_CALLERS` | `.authorizedCallers`                   | Yes      | Addresses authorized to call the hooks (CSV or JSON array)                               |

> **Note:** To configure an `AdvancedPoolHooks` sender allowlist, follow [Configure a sender allowlist with AdvancedPoolHooks using Foundry](/ccip/evm/tutorials/cross-chain-tokens/configure-sender-allowlist-advanced-pool-hooks-foundry). For an overview of the other hook capabilities, read [Advanced Pool Hooks](/ccip/concepts/cross-chain-token/advanced-pool-hooks).

## Tutorial

### Prerequisites

> **CAUTION: Before you continue**
>
> - **Sepolia ETH**: required for Sepolia transactions (deploy/configure/send).
> - **Arbitrum Sepolia ETH**: required for Arbitrum Sepolia transactions (deploy/configure).
> - **LINK (optional)**: if you plan to pay for the cross-chain transfer with `LINK`, fund the sender
>   wallet with testnet LINK on the **source chain**.
> - You can use our [faucet](https://faucets.chain.link/) to get testnet LINK.

**Already have deployments? Skip only what you already have:**

- If you already have tokens deployed on **both** chains, skip **Deploy Tokens**.
- If you have deployed tokens but not the corresponding pools, you must still run **Deploy Token Pools** before continuing.
- If you already have tokens and **V2-compatible** token pools deployed on **both** chains, skip both sections and move straight on to [Enablement](#enablement).

**If you already have a V1 token pool deployed and want to use newer features or prepare for lane migration:**

- You will need to deploy a **V2-compatible token pool**
- Your existing V1 pool can continue to support current transfers
- Follow the migration guide to upgrade your setup without disrupting your existing deployment

> **Read**: [Migrate from V1 to V2 token pools](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-foundry)

Before continuing, export the relevant addresses so subsequent scripts can find them:

```bash filename="Terminal"
export ETHEREUM_SEPOLIA_TOKEN=<your_token_address_on_sepolia>
export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=<your_token_address_on_arbitrum_sepolia>

export ETHEREUM_SEPOLIA_TOKEN_POOL=<your_token_pool_address_on_sepolia>
export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL=<your_token_pool_address_on_arbitrum_sepolia>
```

#### 1. Deploy Tokens

> **TIP: Already have tokens deployed?**
>
> - Your tokens must support burn and mint operations (this tutorial deploys Burn & Mint pools).
> - They must also implement either `getCCIPAdmin()` or OpenZeppelin's `owner()` so the `Claim Admin Role` step can
>   identify the token administrator.

Use the `DeployToken.s.sol` script to deploy [burn-mint ERC-20 tokens](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/interfaces/IBurnMintERC20.sol) on two testnets, Ethereum Sepolia and Arbitrum Sepolia. The deployed token implements the `IBurnMintERC20` interface, which exposes the `mint` and `burn` functions that the token pool later calls during cross-chain transfers.

> **Note:** The script reads the `script/input/token.json` file to get the token name, symbol, decimals, maximum supply, and pre-mint amount. \
> You can also override these values by providing inline environment variables at runtime. \
> See [how to configure deployment parameters](/ccip/evm/tutorials/cross-chain-tokens/register-from-eoa-burn-mint-foundry#token-deployment-configuration) for more details.

[DeployToken.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/deploy/DeployToken.s.sol)

View the deployment script on GitHub.

Optional: If you want to grant mint and burn roles to an address other than the deployer, export `ROLES_RECIPIENT` once:

```bash filename="Terminal"
export ROLES_RECIPIENT=<your_roles_recipient_address>
```

1. Deploy token on Ethereum Sepolia:

```bash filename="Terminal"
forge script \
  script/deploy/DeployToken.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast \
  --verify
```

> **NOTE: Please note**
>
> 1. `--verify` requires `ETHERSCAN_API_KEY` to be set.
> 2. `ROLES_RECIPIENT` is optional, defaults to deployer's address.
> 3. `CCIP_ADMIN_ADDRESS` is optional, defaults to broadcaster, reused in [Claim Admin Role](#claim-admin-role) step.
> 4. `TOKEN_PRE_MINT_RECIPIENT` is optional, applies only when `TOKEN_PRE_MINT > 0`, defaults
>    to broadcaster.
> 5. `TOKEN_PRE_MINT` is required to be set to a value greater than 0, only if `TOKEN_PRE_MINT_RECIPIENT` is set.

Your output should look something like this:

```
  ========================================
  🪙 Deploy Token
  ========================================
  Chain:        Ethereum Sepolia
  Action:       Deploy token
  ========================================

  Token Parameters:
    Name:                         BnM Test
    Symbol:                       BnM-T
    Decimals:                     18
    Max Supply:                   0
    Pre-mint:                     0

    Pre-mint Recipient:           0xYourDesignatedPreMintRecipient
    CCIP Admin:                   0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994

[Step 1] Deploying BnM Test (BnM-T) on Ethereum Sepolia
  Token deployed at: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
  https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D

[Step 2] Granting mint and burn roles to: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
  ✅ Roles granted successfully!

  ========================================
  ✅ Deployment Complete on Ethereum Sepolia!
  ========================================
  Token Address: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
  https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D

  Run this command to set the environment variable:
  export ETHEREUM_SEPOLIA_TOKEN=0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
  ========================================
```

After deployment, the token address is automatically saved to:

```bash
script/deployments/tokens/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-Token.json
```

1. The file uses the env var name as the key (for example, `ETHEREUM_SEPOLIA_TOKEN`).

2. You can copy the key and value directly into an `export` command.

3. The `script/deployments/` directory is ignored by `.gitignore`, so these files stay local to each user.

4. Deploy token on Arbitrum Sepolia:

```bash filename="Terminal"
forge script \
  script/deploy/DeployToken.s.sol \
  --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast \
  --verify
```

Your output should look something like this:

```
  ========================================
  🪙 Deploy Token
  ========================================
  Chain:        Arbitrum Sepolia
  Action:       Deploy token
  ========================================

  Token Parameters:
    Name:                         BnM Test
    Symbol:                       BnM-T
    Decimals:                     18
    Max Supply:                   0
    Pre-mint:                     0

    Pre-mint Recipient:           0xYourDesignatedPreMintRecipient
    CCIP Admin:                   0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994

[Step 1] Deploying BnM Test (BnM-T) on Arbitrum Sepolia
  Token deployed at: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c
  https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c

[Step 2] Granting mint and burn roles to: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
  ✅ Roles granted successfully!

  ========================================
  ✅ Deployment Complete on Arbitrum Sepolia!
  ========================================
  Token Address: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c
  https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c

  Run this command to set the environment variable:
  export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN=0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c
  ========================================
```

After deployment, the token address is automatically saved to:

```bash
script/deployments/tokens/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-Token.json
```

3. Set the environment variables for the deployed token addresses:

> **NOTE: Why do I need to export these?**
>
> 1. Deployment addresses are persisted locally under `script/deployments/`, but each script still reads
>    values from your current shell environment.
> 2. Run the `export` commands so subsequent scripts in this terminal session can discover the correct
>    addresses.
> 3. You can also pass the addresses directly as inline environment variables `TOKEN=0x...` / `TOKEN_POOL=0x...` on any script that requires them.

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

#### 2. Deploy Token Pools

Use the `DeployBurnMintTokenPool.s.sol` script to [deploy Burn & Mint token pools](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/BurnMintTokenPool.sol) for the tokens on both testnets, Ethereum Sepolia and Arbitrum Sepolia, and grant mint and burn roles to the token pools.

A `BurnMintTokenPool` burns tokens on the source chain and mints the equivalent amount on the destination chain. For this to work, the pool must hold the token's mint and burn roles, which the script grants automatically after deployment.

[DeployBurnMintTokenPool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/deploy/DeployBurnMintTokenPool.s.sol)

View the token pool deployment script on GitHub.

> **NOTE: Note**
>
> These scripts deploy `V2` token pools that unlock new features as part of CCIP 2.0. Check out how to use these new
> features in the following sections.

1. Deploy a Burn & Mint token pool for the deployed token on Ethereum Sepolia:

```bash filename="Terminal"
forge script \
  script/deploy/DeployBurnMintTokenPool.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast \
  --verify
```

Your output should look something like this:

```
  ========================================
  🔥⚒️ Deploy Burn & Mint Token Pool
  ========================================
  Chain:        Ethereum Sepolia
  Action:       Deploy burn & mint token pool
  ========================================

  Token Pool Parameters:
    Token:                        0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
    Decimals:                     18
    Router:                       0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59
    RMN Proxy:                    0xba3f6251de62dED61Ff98590cB2fDf6871FbB991
    AdvancedPoolHooks:            None (0x0)

[Step 1] Deploying BurnMintTokenPool on Ethereum Sepolia
  Token Pool deployed at: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
  https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9

[Step 2] Granting mint and burn roles to token pool: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
  ✅ Roles granted successfully!

  ========================================
  ✅ Deployment Complete on Ethereum Sepolia!
  ========================================
  Token Pool Address: 0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
  https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9

  Run this command to set the environment variable:
  export ETHEREUM_SEPOLIA_TOKEN_POOL=0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
  ========================================
```

After deployment, the pool address is automatically saved to:

```bash
script/deployments/token-pools/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-BurnMintTokenPool.json
```

2. Deploy a Burn & Mint token pool for the deployed token on Arbitrum Sepolia:

```bash filename="Terminal"
forge script \
  script/deploy/DeployBurnMintTokenPool.s.sol \
  --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast \
  --verify
```

Your output should look something like this:

```
  ========================================
  🔥⚒️ Deploy Burn & Mint Token Pool
  ========================================
  Chain:        Arbitrum Sepolia
  Action:       Deploy burn & mint token pool
  ========================================

  Token Pool Parameters:
    Token:                        0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c
    Decimals:                     18
    Router:                       0x2a9C5afB0d0e4BAb2BCdaE109EC4b0c4Be15a165
    RMN Proxy:                    0x9527E2d01A3064ef6b50c1Da1C0cC523803BCFF2
    AdvancedPoolHooks:            None (0x0)

[Step 1] Deploying BurnMintTokenPool on Arbitrum Sepolia
  Token Pool deployed at: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
  https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d

[Step 2] Granting mint and burn roles to token pool: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
  ✅ Roles granted successfully!

  ========================================
  ✅ Deployment Complete on Arbitrum Sepolia!
  ========================================
  Token Pool Address: 0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
  https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d

  Run this command to set the environment variable:
  export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL=0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
  ========================================
```

After deployment, the pool address is automatically saved to:

```bash
script/deployments/token-pools/{CHAIN_NAME_IDENTIFIER}/{timestamp}-{SYMBOL}-BurnMintTokenPool.json
```

> **NOTE: Token role-grant fallback**
>
> 1. The script attempts to call `grantMintAndBurnRoles` on the token to grant the pool mint and burn
>    rights.
> 2. If the token does not implement this function, the script prints instructions to grant the roles manually.

3. Set the environment variables for the deployed token pool addresses:

```bash filename="Terminal"
export ETHEREUM_SEPOLIA_TOKEN_POOL=0x...
export ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_TOKEN_POOL=0x...
```

### Enablement

These steps configure your token pools and activate them under the CCIP protocol so your token becomes a fully registered Cross-Chain Token (CCT).

#### 1. Claim Admin Role

Use the `ClaimAdmin.s.sol` script to register your EOA as the administrator for the deployed tokens on both chains. \
This process involves interacting with the [`RegistryModuleOwnerCustom`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/RegistryModuleOwnerCustom.sol) contract to set up your EOA as the admin.

> **NOTE: Supported admin patterns**
>
> The script auto-detects how your token exposes its admin and uses the appropriate registration method:
>
> 1. If your token implements `getCCIPAdmin()`, the script calls `registerAdminViaGetCCIPAdmin()`
>    .
> 2. If your token uses OpenZeppelin's `Ownable` pattern (`owner()`), the script calls `registerAdminViaOwner()` instead.
>
> No extra configuration is needed; both patterns are fully supported.

[ClaimAdmin.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/ClaimAdmin.s.sol)

View the admin claim script on GitHub.

1. Claim the Admin role for the token on Ethereum Sepolia:

> **NOTE: Note**
>
> 1. If you do not set `CCIP_ADMIN_ADDRESS`, the script defaults it to the broadcaster (the signer used in `--account`).
> 2. If you set `CCIP_ADMIN_ADDRESS` to a different address, the `ClaimAdmin.s.sol` transaction
>    must be signed by that same address (use that address's keystore in `--account`), or the claim step
>    will revert.

```bash filename="Terminal"
CCIP_ADMIN_ADDRESS=<your_ccip_admin_address> \
  forge script \
  script/setup/ClaimAdmin.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

Your output should look something like this:

```
  ========================================
  👑 Claim Token Admin
  ========================================
  Chain:        Ethereum Sepolia
  Action:       Claim token admin
  ========================================

  Claim Admin Parameters:
    Token:                        0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
    Current Admin:                0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
    Expected Admin:               0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
    Registry Module:              0xa3c796d480638d7476792230da1E2ADa86e031b0
    Admin Method:                 getCCIPAdmin()

[Step 1] Claiming admin for token via getCCIPAdmin() on Ethereum Sepolia
  ✅ Admin claimed successfully!

  ========================================
  ✅ Admin Claim Complete on Ethereum Sepolia!
  ========================================
  Token Address: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
  Token Address: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
  Admin Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
  ========================================
```

2. Claim the Admin role for the token on Arbitrum Sepolia:

```bash filename="Terminal"
CCIP_ADMIN_ADDRESS=<your_ccip_admin_address> \
  forge script \
  script/setup/ClaimAdmin.s.sol \
  --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

Your output should look something like this:

```
  ========================================
  👑 Claim Token Admin
  ========================================
  Chain:        Arbitrum Sepolia
  Action:       Claim token admin
  ========================================

  Claim Admin Parameters:
    Token:                        0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c
    Current Admin:                0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
    Expected Admin:               0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
    Registry Module:              0xaD417c0611dBD225471D31F056b8B6beC1CBC153
    Admin Method:                 getCCIPAdmin()

[Step 1] Claiming admin for token via getCCIPAdmin() on Arbitrum Sepolia
  ✅ Admin claimed successfully!

  ========================================
  ✅ Admin Claim Complete on Arbitrum Sepolia!
  ========================================
  Token Address: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c
  Token Address: https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c
  Admin Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
  ========================================
```

#### 2. Accept Admin Role

Use the `AcceptAdminRole.s.sol` script to accept the admin role for the deployed tokens on both chains. \
Once you have claimed the role, accepting it finalizes your control over the token administration via the [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol).

[AcceptAdminRole.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/AcceptAdminRole.s.sol)

View the admin acceptance script on GitHub.

1. Accept the admin role for the token on Ethereum Sepolia:

```bash filename="Terminal"
forge script \
  script/setup/AcceptAdminRole.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

Your output should look something like this:

```
  ========================================
  👑 Accept Admin Role
  ========================================
  Chain:        Ethereum Sepolia
  Action:       Accept admin role
  ========================================

  Accept Admin Role Parameters:
    Token:                        0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
    Token Admin Registry:         0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82
    Pending Administrator:        0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
    Signer:                       0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994

[Step 1] Accepting admin role for token on Ethereum Sepolia
  ✅ Admin role accepted successfully!

  ========================================
  ✅ Admin Role Accepted on Ethereum Sepolia!
  ========================================
  Token Address: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
  Token Address: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
  New Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
  ========================================
```

2. Accept the admin role for the token on Arbitrum Sepolia:

```bash filename="Terminal"
forge script \
  script/setup/AcceptAdminRole.s.sol \
  --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

Your output should look something like this:

```
  ========================================
  👑 Accept Admin Role
  ========================================
  Chain:        Arbitrum Sepolia
  Action:       Accept admin role
  ========================================

  Accept Admin Role Parameters:
    Token:                        0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c
    Token Admin Registry:         0x8126bE56454B628a88C17849B9ED99dd5a11Bd2f
    Pending Administrator:        0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
    Signer:                       0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994

[Step 1] Accepting admin role for token on Arbitrum Sepolia
  ✅ Admin role accepted successfully!

  ========================================
  ✅ Admin Role Accepted on Arbitrum Sepolia!
  ========================================
  Token Address: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c
  Token Address: https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c
  New Administrator: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
  ========================================
```

#### 3. Configure Token Pools

Use the `ApplyChainUpdates.s.sol` script to initialize the token pool configuration on each blockchain to enable cross-chain transfers between the two chains. \
This involves linking the token pool on one blockchain to its corresponding pool on the other blockchain, allowing cross-chain token transfers.\
You will interact with the [`TokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol) contract by calling the `applyChainUpdates()` function to set up cross-chain parameters and rate limits.

`applyChainUpdates()` registers the destination chain and the destination pool on the local pool by recording:

- The remote chain selector (to identify the exact chain),
- The remote pool address (to identify the exact pool on that exact chain),
- The remote token address (to identify the exact token on the remote pool),
- And per-direction rate limit buckets (outbound and inbound).

You must run it on **both** pools so each side knows its counterpart.

[ApplyChainUpdates.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/ApplyChainUpdates.s.sol)

View the pool configuration script on GitHub.

> **CAUTION: Rate Limiting**
>
> 1. Rate limiting is disabled by default (`isEnabled: false`). With rate limiting disabled, there are **no
>    restrictions** on the volume of tokens that can be transferred through the pool in any direction.
> 2. To protect against large, unexpected outflows, set `isEnabled` to `true` and configure `capacity` (maximum bucket
>    size) and `rate` (tokens refilled per second).
> 3. See the [Update Rate Limiters](/ccip/evm/tutorials/cross-chain-tokens/update-rate-limiters-foundry) tutorial for
>    more details.

| Env var                        | Required | Description                                                                                        |
| ------------------------------ | -------- | -------------------------------------------------------------------------------------------------- |
| `OUTBOUND_RATE_LIMIT_CAPACITY` | No       | Token bucket capacity for outbound transfers                                                       |
| `OUTBOUND_RATE_LIMIT_RATE`     | No       | Token bucket refill rate (tokens/second) for outbound transfers                                    |
| `OUTBOUND_RATE_LIMIT_ENABLED`  | No       | Override `isEnabled` explicitly (`true`/`false`; defaults to `true` when CAPACITY or RATE are set) |
| `INBOUND_RATE_LIMIT_CAPACITY`  | No       | Token bucket capacity for inbound transfers                                                        |
| `INBOUND_RATE_LIMIT_RATE`      | No       | Token bucket refill rate (tokens/second) for inbound transfers                                     |
| `INBOUND_RATE_LIMIT_ENABLED`   | No       | Override `isEnabled` explicitly (`true`/`false`; defaults to `true` when CAPACITY or RATE are set) |

1. Configure the token pool on Ethereum Sepolia:

```bash filename="Terminal"
DEST_CHAIN=ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1 \
  OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
  OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \
  INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
  INBOUND_RATE_LIMIT_RATE=100000000000000000 \
  forge script \
  script/setup/ApplyChainUpdates.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

Your output should look something like this:

```
  ========================================
  🔗 Apply Chain Updates
  ========================================
  Chain:        Ethereum Sepolia
  Remote Chain: Arbitrum Sepolia
  Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
  Action:       Configure cross-chain lane
  ========================================

  Chain Update Parameters:
    Source Pool:                  0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
    Destination Chain Selector:   3478487238524512106
    Destination Chain Family:     evm
    Destination Pool:             0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
    Destination Token:            0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c
    Outbound Rate Limit Enabled:  true
    Outbound Rate Limit Rate:     100000000000000000
    Inbound Rate Limit Enabled:   true
    Inbound Rate Limit Capacity:  1000000000000000000000
    Inbound Rate Limit Rate:      100000000000000000


[Step 1] Applying chain updates to pool on Ethereum Sepolia
  No existing config for destination chain selector; adding new one.
  ✅ Chain updates applied successfully!

  ========================================
  ✅ Chain Updates Complete on Ethereum Sepolia!
  ========================================
  Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
  Remote Chain: Arbitrum Sepolia (Selector: 3478487238524512106)
  Remote Pool:  0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
  Explorer:     https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
  ========================================
```

2. Configure the token pool on Arbitrum Sepolia:

```bash filename="Terminal"
DEST_CHAIN=ETHEREUM_SEPOLIA \
  OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
  OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \
  INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
  INBOUND_RATE_LIMIT_RATE=100000000000000000 \
  forge script \
  script/setup/ApplyChainUpdates.s.sol \
  --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

Your output should look something like this:

```
  ========================================
  🔗 Apply Chain Updates
  ========================================
  Chain:        Arbitrum Sepolia
  Remote Chain: Ethereum Sepolia
  Token Pool:   0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
  Action:       Configure cross-chain lane
  ========================================

  Chain Update Parameters:
    Source Pool:                  0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
    Destination Chain Selector:   16015286601757825753
    Destination Chain Family:     evm
    Destination Pool:             0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
    Destination Token:            0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
    Outbound Rate Limit Enabled:  true
    Outbound Rate Limit Rate:     100000000000000000
    Inbound Rate Limit Enabled:   true
    Inbound Rate Limit Capacity:  1000000000000000000000
    Inbound Rate Limit Rate:      100000000000000000


[Step 1] Applying chain updates to pool on Arbitrum Sepolia
  No existing config for destination chain selector; adding new one.
  ✅ Chain updates applied successfully!

  ========================================
  ✅ Chain Updates Complete on Arbitrum Sepolia!
  ========================================
  Token Pool:   0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
  Remote Chain: Ethereum Sepolia (Selector: 16015286601757825753)
  Remote Pool:  0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
  Explorer:     https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
  ========================================
```

3. (Optional) Read the list of supported chains and currently configured remote pools:

```bash filename="Terminal"
forge script \
  script/setup/GetSupportedChains.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL
```

#### 4. (Optional) Configure Faster Than Finality for the Token Pool

1. CCIP 2.0 supports **faster than finality** as an opt-in feature, letting token issuers control the speed vs. security
   tradeoff for cross-chain transfers of their token.

2. By default, a newly deployed token pool only allows transfers at default finality (equivalent to omitting `finality`
   or using `finality=finalized`).

3. To enable faster than finality, the pool owner must configure which faster than finality modes are allowed. Senders then request
   one mode per transfer using `finality` (either `finality=finalized` or `finality=<N>` for block-depth faster than finality).

> This section applies only to [**token pools with versions ≥ 2.0**](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/TokenPool.sol). Existing v1 pools do not support fast
> finality.

> **NOTE: Pool-level finality vs. transaction-level finality**
>
> 1. **Pool-level finality** (allowed finality config): A setting the pool owner configures via `setAllowedFinalityConfig(bytes4)`.
>
>    - Default = faster than finality is **disabled**. The pool only allows transfers that use default
>      finality (omit `finality` or use `finality=finalized`).
>    - The pool can opt in to faster than finality by allowing a minimum block depth (`finality=<N>`
>      ).
> 2. **Transaction-level finality** (`finality`): A per-transfer setting the sender specifies
>    via extra args when sending a CCIP message.
>
>    - `finality=finalized` (or omit `finality`) = default finality. This is always allowed.
>    - `finality=<N>` = faster than finality using block depth (block confirmations). Only succeeds
>      if the pool allows block-depth mode and the requested `N` is ≥ the pool's configured minimum.
>
>    Each transfer requests exactly **one** mode (either default finality or block depth).
>
> Faster Than Finality transfers use the **faster than finality** rate limit bucket when configured. If the
> faster than finality bucket isn’t configured for a lane, the pool falls back to the standard rate limit bucket.

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

View the pool configuration script on GitHub.

1. Set the allowed finality config for the token pool on Ethereum Sepolia:

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

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)

  ----------------------------------------
  📊 Current Rate Limits (faster than finality where enabled, standard otherwise):
  ----------------------------------------
    Outbound [standard fallback]:
      Enabled:  true
      Capacity: 1000000000000000000000
      Rate:     100000000000000000
      Tokens:   1000000000000000000000
    Inbound [standard fallback]:
      Enabled:  true
      Capacity: 1000000000000000000000
      Rate:     100000000000000000
      Tokens:   1000000000000000000000

  [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: 1000000000000000000000
    Outbound Rate:     100000000000000000
    Inbound Enabled:   true
    Inbound Capacity:  1000000000000000000000
    Inbound Rate:      100000000000000000

  ✅ Rate limits updated successfully!

  ----------------------------------------
  📊 Updated Rate Limits (faster than finality where enabled, standard otherwise):
  ----------------------------------------
    Outbound [custom finality]:
      Enabled:  true
      Capacity: 1000000000000000000000
      Rate:     100000000000000000
      Tokens:   1000000000000000000000
    Inbound [custom finality]:
      Enabled:  true
      Capacity: 1000000000000000000000
      Rate:     100000000000000000
      Tokens:   1000000000000000000000


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

2. Set the allowed finality config for the token pool on Arbitrum Sepolia:

```bash filename="Terminal"
BLOCK_DEPTH=32 \
  DEST_CHAIN=ETHEREUM_SEPOLIA \
  OUTBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
  OUTBOUND_RATE_LIMIT_RATE=100000000000000000 \
  INBOUND_RATE_LIMIT_CAPACITY=1000000000000000000000 \
  INBOUND_RATE_LIMIT_RATE=100000000000000000 \
  forge script \
  script/configure/finality-config/SetFinalityConfig.s.sol \
  --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

Your output should look something like this:

```
  ========================================
  ⏱️  Set Finality Config
  ========================================
  Chain:        Arbitrum Sepolia
  Remote Chain: Ethereum Sepolia
  Token Pool:   0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
  Action:       Set finality config
  ========================================

    Current Finality Config:      0x00000000
    New Finality Config:          0x00000020
    Mode:                         BLOCK_DEPTH (32 blocks)

  ----------------------------------------
  📊 Current Rate Limits (faster than finality where enabled, standard otherwise):
  ----------------------------------------
    Outbound [standard fallback]:
      Enabled:  true
      Capacity: 1000000000000000000000
      Rate:     100000000000000000
      Tokens:   1000000000000000000000
    Inbound [standard fallback]:
      Enabled:  true
      Capacity: 1000000000000000000000
      Rate:     100000000000000000
      Tokens:   1000000000000000000000

  [Step 1] Setting finality config on Arbitrum Sepolia
  ✅ Finality config set successfully!

  [Step 2] Updating rate limits (faster than finality bucket) on Arbitrum Sepolia -> Ethereum Sepolia
  New Configuration:
    Outbound Enabled:  true
    Outbound Capacity: 1000000000000000000000
    Outbound Rate:     100000000000000000
    Inbound Enabled:   true
    Inbound Capacity:  1000000000000000000000
    Inbound Rate:      100000000000000000

  ✅ Rate limits updated successfully!

  ----------------------------------------
  📊 Updated Rate Limits (faster than finality where enabled, standard otherwise):
  ----------------------------------------
    Outbound [custom finality]:
      Enabled:  true
      Capacity: 1000000000000000000000
      Rate:     100000000000000000
      Tokens:   1000000000000000000000
    Inbound [custom finality]:
      Enabled:  true
      Capacity: 1000000000000000000000
      Rate:     100000000000000000
      Tokens:   1000000000000000000000


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

#### 5. Activate the Token Pool

Use the `SetPool.s.sol` script to register each token pool in the CCIP [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol), making it the official pool for your token. This is the step that activates the pool in the CCIP protocol.

[SetPool.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/SetPool.s.sol)

View the pool linking script on GitHub.

1. Link the token to its respective token pool on Ethereum Sepolia:

```bash filename="Terminal"
forge script \
  script/setup/SetPool.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

Your output should look something like this:

```
  ========================================
  🏊‍♂️ Set Token Pool
  ========================================
  Chain:        Ethereum Sepolia
  Token Pool:   0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
  Action:       Set token pool
  ========================================

  Set Pool Parameters:
    Token:                        0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
    Pool:                         0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
    Token Admin Registry:         0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82
    Token Administrator:          0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994


[Step 1] Setting pool for token on Ethereum Sepolia
  ✅ Pool set successfully!

  ========================================
  ✅ Pool Set Complete on Ethereum Sepolia!
  ========================================
  Token Address: 0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
  Token Address: https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
  Pool Address:  0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
  Pool Address:  https://sepolia.etherscan.io/address/0x147D4625b71f58D7CFA54BB705DC7741B94e0CE9
  ========================================
```

2. Link the token to its respective token pool on Arbitrum Sepolia:

```bash filename="Terminal"
forge script \
  script/setup/SetPool.s.sol \
  --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

Your output should look something like this:

```
  ========================================
  🏊‍♂️ Set Token Pool
  ========================================
  Chain:        Arbitrum Sepolia
  Token Pool:   0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
  Action:       Set token pool
  ========================================

  Set Pool Parameters:
    Token:                        0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c
    Pool:                         0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
    Token Admin Registry:         0x8126bE56454B628a88C17849B9ED99dd5a11Bd2f
    Token Administrator:          0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994


[Step 1] Setting pool for token on Arbitrum Sepolia
  ✅ Pool set successfully!

  ========================================
  ✅ Pool Set Complete on Arbitrum Sepolia!
  ========================================
  Token Address: 0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c
  Token Address: https://sepolia.arbiscan.io/address/0x5B3c8F2a9D4e7A1c6F0b3E8d5C9a2F4b7D1e6A3c
  Pool Address:  0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
  Pool Address:  https://sepolia.arbiscan.io/address/0x7D2f4A6c8E1b3D5a9F0c2E4b6A8d1C3e5F7a9B2d
  ========================================
```

### Cross-chain Token Operations

If all the previous steps were successful, your CCT token is now fully registered and activated under the CCIP protocol.
Use these next steps to mint and transfer your tokens across networks.

#### 1. Mint Tokens

Use the `MintTokens.s.sol` script to mint tokens to your Externally Owned Account (EOA) on Ethereum Sepolia. \
Since you granted mint and burn privileges to your EOA during the token deployment in the first step, you are authorized to mint tokens for testing purposes. \
This ensures that your EOA has sufficient tokens to perform cross-chain transfers in the next step.

[MintTokens.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/operations/MintTokens.s.sol)

View the minting script on GitHub.

1. Mint tokens for your EOA on Ethereum Sepolia:

> **NOTE: Note**
>
> 1. If you don't set `AMOUNT` as an environment variable, the script will use the amount from the `tokenAmountToMint`
>    field in your `script/input/token.json` file.
> 2. Set `MINT_RECEIVER` to mint to a different address (defaults to the EOA broadcasting the transaction).

```bash filename="Terminal"
AMOUNT=1000000000000000000000 \
  forge script \
  script/operations/MintTokens.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

Your output should look something like this:

```
  ========================================
  💰 Mint Tokens
  ========================================
  Chain:        Ethereum Sepolia
  Action:       Mint tokens
  ========================================

  Mint Parameters:
    Token:                        0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
    Token Symbol:                 BnM-T
    Amount:                       1000000000000000000000
    Receiver:                     0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994


[Step 1] Minting 1000000000000000000000 BnM-T to 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
  ✅ Tokens minted successfully!

  ========================================
  ✅ Minting Complete on Ethereum Sepolia!
  ========================================
  Receiver Address: 0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
  Receiver Address: https://sepolia.etherscan.io/address/0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
  New Balance: 1000000000000000000000 BnM-T
  ========================================
```

#### 2. Transfer Tokens Across Networks

Use the `ccip-cli` command to securely transfer tokens across networks from your terminal.
Tokens will be:

1. **burned** on the source chain (Ethereum Sepolia), and,
2. **minted** on the destination chain (Arbitrum Sepolia). \\

You can choose to pay CCIP fees using LINK tokens or the native gas token.

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

The CLI command expects the router address to be set in the environment variables. You can set it using the following command:

```bash filename="Terminal"
export ETHEREUM_SEPOLIA_ROUTER=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: Token amounts in this tutorial**
>
> Earlier Foundry scripts use smallest-unit amounts (wei-like). In the `ccip-cli` examples below we use a
> compact, human-readable token amount (for example, `1.23`).

1. Transfer tokens from Ethereum Sepolia to Arbitrum Sepolia on **default finality**:

```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 \
  --wallet foundry:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL"
```

Your output should look something like this:

```
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. Transfer tokens from Ethereum Sepolia to Arbitrum Sepolia using **faster than finality**:

This assumes you enabled faster than finality on the pool in the optional finality configuration accordion above.
When you pass `--extra finality=...`, `ccip-cli` encodes it into the CCIP message extra args.

Transfer with explicit finalized finality (`finality=finalized`):

```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 \
  --extra finality=finalized \
  --wallet foundry:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL"
```

Your output should look something like this:

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

Faster Than Finality using numeric block depth (`finality=<N>`):

```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 \
  --extra finality=7 \
  --wallet foundry:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL"
```

> **NOTE: Understand Minimum Block Depth Requirements 🧑‍🏫**
>
> 1. The last command failed because the pool's **allowed finality config** requires a larger block depth
>    for numeric faster than finality (for example, if you configured `BLOCK_DEPTH=32`) but we requested `finality=7`.
> 2. Try running the command again with an appropriate value for `finality` (for example, `--extra finality=20`), or omit `finality` to use default finality (
>    `finality=finalized`).
> 3. If you see a revert like `InvalidRequestedFinality(requestedFinality, allowedFinality)`, it means the
>    requested mode (or depth) is not permitted by the pool.

#### 3. (Optional) Hand Off Token Administration After Enablement

1. The process to set up your tokens for cross-chain transfers is complete. At this stage, you might want to hand off token administration to a different address, for example, transferring control to a multisig or a dedicated operations wallet
   This new admin will be able to mint and transfer tokens as needed.
2. Use the `TransferTokenAdminRole.s.sol` script to achieve this.
   This is a **two-step process**: the current admin initiates the transfer, and the new admin must run `AcceptAdminRole.s.sol` to complete it. Until the new admin accepts, the current admin retains full control.

[TransferTokenAdminRole.s.sol](https://github.com/smartcontractkit/docs-cct-foundry/blob/main/script/setup/TransferTokenAdminRole.s.sol)

View the transfer admin script on GitHub.

1. Initiate the transfer on Ethereum Sepolia:

```bash filename="Terminal"
NEW_ADMIN=0x... \
  forge script \
  script/setup/TransferTokenAdminRole.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

Your output should look something like this:

```
  ========================================
  🔄 Transfer Token Admin Role
  ========================================
  Chain:        Ethereum Sepolia
  Action:       Transfer admin role
  ========================================

  Transfer Admin Role Parameters:
    Token:                        0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
    Token Admin Registry:         0x95F29FEE11c5C55d26cCcf1DB6772DE953B37B82
    Current Administrator:        0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994
    Pending Administrator:        0x0000000000000000000000000000000000000000
    New Admin:                    0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3
    Signer:                       0x3A34637a41aB08519d30Fdb65344aBa8E9b2e994

[Step 1] Transferring admin role for token on Ethereum Sepolia
  ✅ Admin role transfer initiated successfully!

  ========================================
  ✅ Admin Role Transfer Initiated on Ethereum Sepolia!
  ========================================
  Token:       https://sepolia.etherscan.io/address/0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
  New Admin:   0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3
  ========================================

  ℹ️  The new admin (0xE23Fc63F47F08F58B9d7448d4CCE0bCDcc96d7F3) must run AcceptAdminRole to complete the transfer.
```

> **NOTE: Note**
>
> 1. Set `TOKEN=0x...` to specify the token explicitly. If omitted, the script reads the deployed token
>    address from the environment variable for the current chain.
> 2. Only the current admin can initiate this transfer. If the signer is not the admin, the script will revert with an
>    error.

2. Repeat the same command on Arbitrum Sepolia if you also want to transfer admin rights on the destination chain:

```bash filename="Terminal"
NEW_ADMIN=0x... \
  forge script \
  script/setup/TransferTokenAdminRole.s.sol \
  --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \
  --account $KEYSTORE_NAME \
  --broadcast
```

3. The new admin completes the transfer by running the `AcceptAdminRole` script for both chains.
   Set `NEW_ADMIN_KEYSTORE_NAME` to the Foundry keystore name that the new admin created via `cast wallet import`:

```bash filename="Terminal"
export NEW_ADMIN_KEYSTORE_NAME=new_admin_keystore_name

forge script \
  script/setup/AcceptAdminRole.s.sol \
  --rpc-url $ETHEREUM_SEPOLIA_RPC_URL \
  --account $NEW_ADMIN_KEYSTORE_NAME \
  --broadcast

forge script \
  script/setup/AcceptAdminRole.s.sol \
  --rpc-url $ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1_RPC_URL \
  --account $NEW_ADMIN_KEYSTORE_NAME \
  --broadcast
```

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