# Migrate your CCT token pools from CCIP v1 to v2 (Burn & Mint) using Hardhat
Source: https://docs.chain.link/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-hardhat
Last Updated: 2026-06-11

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

## Guide Versions

- [Foundry (Burn & Mint migration)](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-foundry)

- [Hardhat (Burn & Mint migration)](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-burn-mint-hardhat)

This migration guide helps you upgrade a deployed Burn & Mint Cross-Chain Token (CCT) setup from CCIP v1 (1.5.x / 1.6.x) to CCIP 2.0, while keeping your existing token addresses and minimizing disruption.

> **NOTE: Finality and v2 features are optional**
>
> 1. **Default finality:** Transfers use the chain's native finality unless you explicitly opt into
>    block-depth faster than finality.
> 2. **Opt-in changes:** Pool-level fees, faster than finality, and hooks only apply if you configure
>    them. This guide only focuses on a safe pool migration.

In this guide you will:

1. Confirm prerequisites and record your existing CCIP v1 deployment details.
2. Deploy new CCIP 2.0 `Burn Mint` token pools on each chain.
3. Configure the v2 pools to accept messages from both the old v1 pools and the new v2 pools.
4. Cut over CCIP routing by calling [`TokenAdminRegistry`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/tokenAdminRegistry/TokenAdminRegistry.sol).setPool on each chain.
5. Validate transfers end-to-end, then optionally clean up legacy configuration.

## 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. Clone the repository and navigate to the project directory:

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

Clone the CCIP 2.0 Hardhat template for a smoother CCT setup.

```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>

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

[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. To make sure your terminal has access to these variables, run the following command:

```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 <your_private_key_name>
```

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

> **NOTE: Note**
>
> 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 (e.g. `PRIVATE_KEY`) is what you set as `KEYSTORE_NAME` in `.env`.
> 3. Use `npx hardhat keystore list` to check previously created keystores.

### 2. What this guide assumes

- **EVM-to-EVM only**: this page is strictly for Ethereum Sepolia ↔ Arbitrum Sepolia. - **Burn & Mint pools only**:
  standard
  [`BurnMintTokenPool`](https://github.com/smartcontractkit/chainlink-ccip/tree/contracts-ccip-v2.0.0/chains/evm/contracts/pools/BurnMintTokenPool.sol)
  migrations (no custom pools). - **Admin control**: you control the `TokenAdminRegistry` administrator role for the
  token on both chains. - **Token permissions**: you can grant the required mint/burn permissions to the new v2 pools on
  each chain. - **Pool-owner actions**: you can execute pool-owner actions on both chains (for example,
  `applyChainUpdates`, `removeRemotePool`). - **Existing v1 deployment**: you are migrating an already-live CCIP v1
  setup (1.5.x or 1.6.x), not doing a new registration.

## Migration Guide

> **TIP: Set these variables once for cleaner copy/paste**
>
> This guide uses explicit addresses so you don't need local `deployments/` files. Start by exporting your
> token addresses, then we'll export the v1/v2 pool addresses as we discover them.
>
> ```bash filename="Terminal"
> # Token addresses (existing deployments)
> export ETHEREUM_SEPOLIA_TOKEN=0x...
> export ARBITRUM_SEPOLIA_TOKEN=0x...
> ```

### 1. Step 1: Confirm the live CCIP v1 pool (and record v1 addresses)

This step answers two questions, on both chains:

1. Which pool is currently live for your token? (read from `TokenAdminRegistry.getTokenConfig`)
2. Is that pool a v1.5/v1.6 pool? (read `typeAndVersion()` from the pool)

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

View the token config query task on GitHub.

1. Read the token config on Ethereum Sepolia:

```bash filename="Terminal"
npx hardhat getTokenConfig \
  --tokenaddress $ETHEREUM_SEPOLIA_TOKEN \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
📋 Get Token Config
========================================
Chain:        Ethereum Sepolia
Token:        0x9602399103Ff5F87587Ac5A28E1551A0bA0c6C0D
Action:       Read getTokenConfig
========================================

Token Config:
  administrator:        0xYourAdminAddress
  pendingAdministrator: 0x0000000000000000000000000000000000000000
  tokenPool:            0xOldSepoliaV1PoolAddress
```

Export the v1 pool address:

```bash filename="Terminal"
export ETHEREUM_SEPOLIA_V1_POOL=0xOldSepoliaV1PoolAddress
```

2. Repeat on Arbitrum Sepolia:

```bash filename="Terminal"
npx hardhat getTokenConfig \
  --tokenaddress $ARBITRUM_SEPOLIA_TOKEN \
  --network arbitrumSepolia
```

Your output should look something like this:

```bash filename="Terminal"
Token Config:
  administrator:        0xYourAdminAddress
  pendingAdministrator: 0x0000000000000000000000000000000000000000
  tokenPool:            0xOldArbitrumV1PoolAddress
```

Export the v1 pool address:

```bash filename="Terminal"
export ARBITRUM_SEPOLIA_V1_POOL=0xOldArbitrumV1PoolAddress
```

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

View the type/version query task on GitHub.

3. Confirm the v1 pool version on Ethereum Sepolia:

```bash filename="Terminal"
npx hardhat getTypeAndVersion \
  --address $ETHEREUM_SEPOLIA_V1_POOL \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
typeAndVersion: BurnMintTokenPool 1.6.1
```

4. Confirm the v1 pool version on Arbitrum Sepolia:

```bash filename="Terminal"
npx hardhat getTypeAndVersion \
  --address $ARBITRUM_SEPOLIA_V1_POOL \
  --network arbitrumSepolia
```

Your output should look something like this:

```bash filename="Terminal"
typeAndVersion: BurnMintTokenPool 1.6.1
```

> **NOTE: What to check before continuing**
>
> - **administrator** should match the account/multisig you control (this account must execute `setPool` during cutover).
> - **pendingAdministrator** should typically be `0x0`. If it's set, resolve it before
>   migrating to avoid confusion.
> - **typeAndVersion** should indicate a v1.5.x or v1.6.x `BurnMintTokenPool`.

### 2. Step 2: Record your v1 lane configuration (remote pools + rate limits)

Before deploying anything, snapshot your current v1 configuration so you can keep behavior consistent after migration.

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

View the supported-chains query task on GitHub.

1. List configured remote chains on the Sepolia v1 pool:

```bash filename="Terminal"
npx hardhat getSupportedChains \
  --tokenpool $ETHEREUM_SEPOLIA_V1_POOL \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
Supported Remote Chains: 1

  [0] Arbitrum Sepolia (3478487238524512106)
       Remote Pools: 1
         [0] 0xOldArbitrumV1PoolAddress
```

2. List configured remote chains on the Arbitrum v1 pool:

```bash filename="Terminal"
npx hardhat getSupportedChains \
  --tokenpool $ARBITRUM_SEPOLIA_V1_POOL \
  --network arbitrumSepolia
```

Your output should look something like this:

```bash filename="Terminal"
Supported Remote Chains: 1

  [0] Ethereum Sepolia (16015286601757825753)
       Remote Pools: 1
         [0] 0xOldSepoliaV1PoolAddress
```

[getCurrentRateLimits.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/rate-limiter/getCurrentRateLimits.ts)

View the rate limiter query task on GitHub.

3. Snapshot current v1 rate limits on the Sepolia → Arbitrum lane:

```bash filename="Terminal"
npx hardhat getCurrentRateLimits \
  --tokenpool $ETHEREUM_SEPOLIA_V1_POOL \
  --destchain arbitrumSepolia \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
Pool Version: v1

  Outbound Enabled:  true
  Outbound Capacity: 1000000000000000000000
  Outbound Rate:     100000000000000000
  Inbound  Enabled:  true
  Inbound  Capacity: 1000000000000000000000
  Inbound  Rate:     100000000000000000
```

4. Snapshot current v1 rate limits on the Arbitrum → Sepolia lane:

```bash filename="Terminal"
npx hardhat getCurrentRateLimits \
  --tokenpool $ARBITRUM_SEPOLIA_V1_POOL \
  --destchain sepolia \
  --network arbitrumSepolia
```

Your output should look something like this:

```bash filename="Terminal"
Pool Version: v1

  Outbound Enabled:  true
  Outbound Capacity: 1000000000000000000000
  Outbound Rate:     100000000000000000
  Inbound  Enabled:  true
  Inbound  Capacity: 1000000000000000000000
  Inbound  Rate:     100000000000000000
```

Record these values, you'll reuse them when configuring the v2 pools in Step 5.

### 3. Step 3: Deploy new v2 pools (per chain)

Deploy a new v2 `BurnMintTokenPool` on each chain, pointing to your existing token address on that chain.

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

View the Burn & Mint pool deployment task on GitHub.

1. Deploy the v2 pool on Ethereum Sepolia:

```bash filename="Terminal"
npx hardhat deployTokenPool \
  --tokenaddress $ETHEREUM_SEPOLIA_TOKEN \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
🔥⚒️  Deploy Burn & Mint Token Pool

[Step 1] Deploying BurnMintTokenPool on Ethereum Sepolia
Token Pool deployed at: 0xNewSepoliaV2PoolAddress

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

Export the new v2 pool address:

```bash filename="Terminal"
export ETHEREUM_SEPOLIA_V2_POOL=0xNewSepoliaV2PoolAddress
```

2. Deploy the v2 pool on Arbitrum Sepolia:

```bash filename="Terminal"
npx hardhat deployTokenPool \
  --tokenaddress $ARBITRUM_SEPOLIA_TOKEN \
  --network arbitrumSepolia
```

Your output should look something like this:

```bash filename="Terminal"
🔥⚒️  Deploy Burn & Mint Token Pool

[Step 1] Deploying BurnMintTokenPool on Arbitrum Sepolia
Token Pool deployed at: 0xNewArbitrumV2PoolAddress

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

Export the new v2 pool address:

```bash filename="Terminal"
export ARBITRUM_SEPOLIA_V2_POOL=0xNewArbitrumV2PoolAddress
```

> **NOTE: Dual mint/burn window**
>
> Until post-migration cleanup, **both** v1 and v2 pools may hold mint/burn roles on your tokens. Your CCIP
> routing still points to the v1 pool until Step 7.

### 4. Step 4 (Recommended): Pause outbound transfers on the v1 pools

This recommended step throttles new outbound traffic on v1 while you cut over. Use capacity = 2 and rate = 1
as the most restrictive (near-zero) configuration we document for v1 pools while keeping the lane configured.

[updateRateLimiters.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/rate-limiter/updateRateLimiters.ts)

View the rate limiter update task on GitHub.

1. Pause outbound on the Sepolia v1 pool (lane to Arbitrum Sepolia):

```bash filename="Terminal"
npx hardhat updateRateLimiters \
  --tokenpool $ETHEREUM_SEPOLIA_V1_POOL \
  --destchain arbitrumSepolia \
  --outboundcapacity 2 \
  --outboundrate 1 \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
⚡️ Update Rate Limiters
========================================
Pool Version: v1 (setChainRateLimiterConfig)

New Configuration:
  Outbound Enabled:  true
  Outbound Capacity: 2
  Outbound Rate:     1
```

2. Pause outbound on the Arbitrum v1 pool (lane to Ethereum Sepolia):

```bash filename="Terminal"
npx hardhat updateRateLimiters \
  --tokenpool $ARBITRUM_SEPOLIA_V1_POOL \
  --destchain sepolia \
  --outboundcapacity 2 \
  --outboundrate 1 \
  --network arbitrumSepolia
```

### 5. Step 5: Configure the v2 pools (applyChainUpdates)

On each chain, configure the v2 pool to recognize the lane to the remote chain.

Critical migration rule: the `destPools` list must include both the old v1 remote pool address and the new
v2 remote pool address. This allows in-flight messages sent via v1 (before cutover) to still validate after you cut
over to v2.

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

View the applyChainUpdates task (JSON mode recommended) on GitHub.

1. Configure the Sepolia v2 pool (remote chain: Arbitrum Sepolia)

Update `input/apply-chain-updates.json`:

```json filename="input/apply-chain-updates.json"
{
  "_comment": "Configure Sepolia v2 pool → Arbitrum Sepolia. Replace 0x... placeholders with your real addresses.",
  "sourcePool": "0xNewSepoliaV2PoolAddress",
  "remoteChains": [
    {
      "destChain": "ARBITRUM_SEPOLIA",
      "destPools": ["0xOldArbitrumV1PoolAddress", "0xNewArbitrumV2PoolAddress"],
      "destToken": "0xYourArbitrumTokenAddress",
      "outboundRateLimit": { "enabled": false, "capacity": "0", "rate": "0" },
      "inboundRateLimit": { "enabled": false, "capacity": "0", "rate": "0" }
    }
  ]
}
```

If you had v1 rate limits enabled, copy the values you recorded in Step 2 into `outboundRateLimit` and
`inboundRateLimit` so the v2 pool behavior stays consistent.

Run the task on Ethereum Sepolia:

```bash filename="Terminal"
npx hardhat applyChainUpdates --viajsonfile --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
🔗 Apply Chain Updates (JSON mode)
========================================
Source Chain:  Ethereum Sepolia
Token Pool:    0xNewSepoliaV2PoolAddress
Remote Chains: 1

  [0] Arbitrum Sepolia
      Pools: 2
        [0] 0xOldArbitrumV1PoolAddress
        [1] 0xNewArbitrumV2PoolAddress

✅ Chain updates applied successfully!
```

2. Configure the Arbitrum v2 pool (remote chain: Ethereum Sepolia)

Update `input/apply-chain-updates.json`:

```json filename="input/apply-chain-updates.json"
{
  "_comment": "Configure Arbitrum v2 pool → Ethereum Sepolia. Replace 0x... placeholders with your real addresses.",
  "sourcePool": "0xNewArbitrumV2PoolAddress",
  "remoteChains": [
    {
      "destChain": "ETHEREUM_SEPOLIA",
      "destPools": ["0xOldSepoliaV1PoolAddress", "0xNewSepoliaV2PoolAddress"],
      "destToken": "0xYourSepoliaTokenAddress",
      "outboundRateLimit": { "enabled": false, "capacity": "0", "rate": "0" },
      "inboundRateLimit": { "enabled": false, "capacity": "0", "rate": "0" }
    }
  ]
}
```

Run the task on Arbitrum Sepolia:

```bash filename="Terminal"
npx hardhat applyChainUpdates --viajsonfile --network arbitrumSepolia
```

Your output should look something like this:

```bash filename="Terminal"
Source Chain:  Arbitrum Sepolia
Token Pool:    0xNewArbitrumV2PoolAddress
Remote Chains: 1

  [0] Ethereum Sepolia
      Pools: 2
        [0] 0xOldSepoliaV1PoolAddress
        [1] 0xNewSepoliaV2PoolAddress

✅ Chain updates applied successfully!
```

### 6. Step 6: Verify v2 configuration (before cutover)

Verify that each v2 pool has two remote pools configured for the lane: `[remoteV1Pool, remoteV2Pool]`.

[getRemotePools.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/remote-pools/getRemotePools.ts)

View the remote pool query task on GitHub.

1. Check remote pools on the Sepolia v2 pool:

```bash filename="Terminal"
npx hardhat getRemotePools \
  --tokenpool $ETHEREUM_SEPOLIA_V2_POOL \
  --destchain arbitrumSepolia \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
Chain Supported: Yes
Remote Pools:    2
  [0] 0xOldArbitrumV1PoolAddress
  [1] 0xNewArbitrumV2PoolAddress
```

2. Check remote pools on the Arbitrum v2 pool:

```bash filename="Terminal"
npx hardhat getRemotePools \
  --tokenpool $ARBITRUM_SEPOLIA_V2_POOL \
  --destchain sepolia \
  --network arbitrumSepolia
```

Your output should look something like this:

```bash filename="Terminal"
Chain Supported: Yes
Remote Pools:    2
  [0] 0xOldSepoliaV1PoolAddress
  [1] 0xNewSepoliaV2PoolAddress
```

[getCurrentRateLimits.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/rate-limiter/getCurrentRateLimits.ts)

View the rate limiter query task on GitHub.

3. Re-check rate limits on the v2 pools (optional but recommended)

Sepolia v2 pool (lane to Arbitrum):

```bash filename="Terminal"
npx hardhat getCurrentRateLimits \
  --tokenpool $ETHEREUM_SEPOLIA_V2_POOL \
  --destchain arbitrumSepolia \
  --network sepolia
```

Arbitrum v2 pool (lane to Sepolia):

```bash filename="Terminal"
npx hardhat getCurrentRateLimits \
  --tokenpool $ARBITRUM_SEPOLIA_V2_POOL \
  --destchain sepolia \
  --network arbitrumSepolia
```

### 7. Step 7: Cut over routing (TokenAdminRegistry.setPool)

This is the migration cutover step. On each chain, update `TokenAdminRegistry` so your token routes through the new v2
pool.

> **NOTE: Who must run this step**
>
> The signer running `setPool` must be the token's `TokenAdminRegistry` **administrator** on that chain (as shown by `getTokenConfig` in Step 1).

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

View the setPool task on GitHub.

1. Cut over on Ethereum Sepolia:

```bash filename="Terminal"
npx hardhat setPool \
  --tokenaddress $ETHEREUM_SEPOLIA_TOKEN \
  --tokenpool $ETHEREUM_SEPOLIA_V2_POOL \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
========================================
🏊 Set Token Pool
========================================
Chain:        Ethereum Sepolia
Token Pool:   0xNewSepoliaV2PoolAddress
Action:       Set token pool
========================================

✅ Pool Set Complete on Ethereum Sepolia!
```

2. Cut over on Arbitrum Sepolia:

```bash filename="Terminal"
npx hardhat setPool \
  --tokenaddress $ARBITRUM_SEPOLIA_TOKEN \
  --tokenpool $ARBITRUM_SEPOLIA_V2_POOL \
  --network arbitrumSepolia
```

> **NOTE: Rollback**
>
> If you discover an issue immediately after cutover, you can roll back by calling `setPool` again and
> pointing back to your v1 pool on that chain. Keep the dual-remote-pool configuration in place until you're stable.

### 8. Step 8: Validate end-to-end (Sepolia → Arbitrum) with ccip-cli

Send a small test transfer from Ethereum Sepolia → Arbitrum Sepolia and confirm it succeeds.

> **NOTE: Prerequisites for the send**
>
> 1. You need some of your token on Ethereum Sepolia to send.
> 2. You need Sepolia ETH for gas (and optionally LINK if paying fees in LINK).
> 3. You need Arbitrum Sepolia ETH for gas to run any Arbitrum-side verification commands.

Set the router address for Ethereum Sepolia (from the [CCIP Directory](https://docs.chain.link/ccip/directory/testnet) or `helper-config.ts` in the template):

```bash filename="Terminal"
export ETHEREUM_SEPOLIA_ROUTER=0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59
```

1. Send a transfer 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 hardhat:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ARBITRUM_SEPOLIA_RPC_URL"
```

Your output should look something like this:

```bash filename="Terminal"
Fee: 130129888907619n = 0.000130129888907619 ETH
🚀 Sending message ... messageId => 0x4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293
```

2. (Optional) Request block-depth faster than finality with depth 32

Only use this if your v2 pools explicitly allow block-depth faster than finality. If this fails, omit `--extra` and retry with
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 \
  --extra finality=32 \
  --wallet hardhat:$KEYSTORE_NAME \
  --rpc "$ETHEREUM_SEPOLIA_RPC_URL" \
  --rpc "$ARBITRUM_SEPOLIA_RPC_URL"
```

### 9. Step 9: Confirm your token now routes through v2 pools

Re-run the Step 1 checks. You should now see the v2 pool addresses as the live `tokenPool` on both chains, and
`typeAndVersion` should report v2.

1. Ethereum Sepolia:

```bash filename="Terminal"
npx hardhat getTokenConfig --tokenaddress $ETHEREUM_SEPOLIA_TOKEN --network sepolia
npx hardhat getTypeAndVersion --address $ETHEREUM_SEPOLIA_V2_POOL --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
Token Config:
  tokenPool:            0xNewSepoliaV2PoolAddress

typeAndVersion: BurnMintTokenPool 2.0.0
```

2. Arbitrum Sepolia:

```bash filename="Terminal"
npx hardhat getTokenConfig --tokenaddress $ARBITRUM_SEPOLIA_TOKEN --network arbitrumSepolia
npx hardhat getTypeAndVersion --address $ARBITRUM_SEPOLIA_V2_POOL --network arbitrumSepolia
```

Your output should look something like this:

```bash filename="Terminal"
Token Config:
  tokenPool:            0xNewArbitrumV2PoolAddress

typeAndVersion: BurnMintTokenPool 2.0.0
```

### 10. Step 10: Safety checks and cleanup (after v1 inflight settles)

After cutover, keep the v1 remote pool addresses configured on the v2 pools until you are confident there are no more
in-flight v1 messages. There is no cost to waiting longer.

Once you're ready, remove the old v1 remote pool
addresses from the v2 pools.

[removeRemotePool.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/remote-pools/removeRemotePool.ts)

View the remote pool removal task on GitHub.

1. On the Sepolia v2 pool, remove the old Arbitrum v1 pool address:

```bash filename="Terminal"
npx hardhat removeRemotePool \
  --tokenpool $ETHEREUM_SEPOLIA_V2_POOL \
  --destchain arbitrumSepolia \
  --remotepooladdress $ARBITRUM_SEPOLIA_V1_POOL \
  --network sepolia
```

Your output should look something like this:

```bash filename="Terminal"
➖ Remove Remote Pool

⚠️  WARNING: All inflight transactions from this pool will be rejected after removal.

Current Remote Pools: 2
  [0] 0xOldArbitrumV1PoolAddress
  [1] 0xNewArbitrumV2PoolAddress

✅ Remote pool removed successfully!
```

2. On the Arbitrum v2 pool, remove the old Sepolia v1 pool address:

```bash filename="Terminal"
npx hardhat removeRemotePool \
  --tokenpool $ARBITRUM_SEPOLIA_V2_POOL \
  --destchain sepolia \
  --remotepooladdress $ETHEREUM_SEPOLIA_V1_POOL \
  --network arbitrumSepolia
```

> **CAUTION: If you removed too early**
>
> If a v1-sent message is still in-flight and you removed its source pool address, it can fail validation on the
> destination. You can recover by adding the pool address back with `addRemotePool`, then retry cleanup
> later.

[addRemotePool.ts](https://github.com/smartcontractkit/docs-cct-hardhat/blob/main/tasks/configure/remote-pools/addRemotePool.ts)

View the remote pool add task on GitHub (recovery).

Recovery example (re-add the old pool address, then retry cleanup later):

```bash filename="Terminal"
npx hardhat addRemotePool \
  --tokenpool $ETHEREUM_SEPOLIA_V2_POOL \
  --destchain arbitrumSepolia \
  --remotepooladdress $ARBITRUM_SEPOLIA_V1_POOL \
  --network sepolia
```

> **NOTE: Optional hardening**
>
> After migration and cleanup, you can also revoke mint/burn roles from the old v1 pools (if your token's access control
> allows it). This is token-specific and may require calling role revocation functions directly on your token contract.

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