# Lock & Mint V1 -> V2
Source: https://docs.chain.link/ccip/evm/concepts/cross-chain-token/lock-mint-v1-to-v2

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

## Overview

This migration flow covers upgrading a **lock and mint** token handling mechanism from v1 to v2 token pools, where you
have a **LockReleaseTokenPool on Chain A** and a **BurnMintTokenPool on Chain B**. When transferring A→B, tokens are
locked on A and minted on B. When transferring B→A, tokens are burned on B and released on A. It assumes your address
is the registered administrator in the `TokenAdminRegistry` on all chains.

This guide covers **EVM-to-EVM only**.

> **NOTE: Prefer scripts and commands?**
>
> For step-by-step scripts and runnable commands, follow the [Foundry migration
> tutorial](/ccip/evm/tutorials/cross-chain-tokens/migrate-from-v1-to-v2-lock-mint-foundry).

For simplicity, the steps below illustrate a **2-chain setup (Chain A ↔ Chain B)**. If your token is deployed across
more chains (for example, one LockRelease pool on Chain A connected to BurnMint pools on Chain B, C, D), repeat the
relevant steps for each remote chain. See [Multi-chain considerations](#multi-chain-considerations).

**Out of scope**

- Customized token pools

**Definitions**

- **v1 pool**: Any pool version 1.5.x or 1.6.x (`LockReleaseTokenPool 1.5.1`, `BurnMintTokenPool 1.6.0`, etc.).
- **v2 pool**: Pool version 2.0 (`LockReleaseTokenPool 2.0` on Chain A, `BurnMintTokenPool 2.0` on Chain B) that
  you want to upgrade to.
- **ERC20LockBox**: A v2 contract that holds locked token liquidity on behalf of the Lock & Release pool. Constructor:
  `(address token)`. In v1, liquidity was held directly in the pool contract. In v2, it's held in the lockbox. The
  lockbox is a simple token vault. The `remoteChainSelector` parameter in `deposit`/`withdraw` exists for interface
  compatibility but is **unused** internally. All liquidity is pooled together.
- **Chain A**: The chain with the LockReleaseTokenPool (locks tokens on outbound, releases tokens on inbound).
- **Chain B**: The chain with the BurnMintTokenPool (mints tokens on inbound, burns tokens on outbound).
- **TokenAdminRegistry**: The CCIP contract where a token ↔ token pool mapping is registered.

**Key architectural note**: This migration is **asymmetric**. Chain A requires deploying both an ERC20LockBox and a
LockReleaseTokenPool, while Chain B only needs a BurnMintTokenPool. Additionally, there is a **liquidity migration
step** where locked tokens must be moved from the old v1 pool to the new v2 lockbox.

**v2 LockRelease pool variants**:

| v2 Pool                      | Lockbox model                                               | Use case                                             |
| :--------------------------- | :---------------------------------------------------------- | :--------------------------------------------------- |
| `LockReleaseTokenPool`       | Single shared lockbox (set in constructor)                  | Standard: all remote chains share one liquidity pool |
| `SiloedLockReleaseTokenPool` | Per-chain lockboxes (configured via `configureLockBoxes()`) | When you need isolated liquidity per remote chain    |

Unlike BurnMint pools (which have variants based on the token's burn interface), LockRelease pools don't need interface
variants, because locking and releasing use standard ERC20 `transferFrom`/`transfer`.

This guide uses `LockReleaseTokenPool` (single shared lockbox) as the standard migration path. For siloed lockboxes,
see the [Siloed liquidity](#siloed-liquidity-per-chain-isolation) section.

## Flow diagram

![Lock & Mint v1 to v2 migration flow diagram](/images/ccip/cct/migration/lock-mint-v1-to-v2-flow.png)

## Batching operations

If you control the administrator, pool owner, and lockbox owner addresses through a multisig that supports
meta-transactions (for example, Safe), you can batch multiple migration steps into a single atomic transaction.

Recommended groupings:

### Batch 1: Liquidity migration (Chain A)

Steps 3a–3f combined into one atomic transaction:

- On v1 pool: `setRebalancer` → `withdrawLiquidity`
- On v2 lockbox: `applyAuthorizedCallerUpdates(add)` → `deposit`
- On token: `approve` (for lockbox to pull tokens)
- Cleanup: `applyAuthorizedCallerUpdates(remove)` on v2 lockbox

This is the most critical batch because it eliminates the non-atomic custody window where liquidity sits in your wallet
between withdraw and deposit. You can optionally include `setPool` (Step 6) in this same batch to combine liquidity
migration and cutover on Chain A in a single atomic transaction.

### Batch 2: Configure + cutover (per chain)

`applyChainUpdates` (Step 4) + `setPool` (Step 6). Configures the v2 pool and activates it in one transaction.

### Batch 3: Cleanup (per chain)

`removeRemotePool` (Step 8a) + `revokeMintRole` + `revokeBurnRole` (Step 8b, destination only).

Each batch must be executed **per chain**. You cannot batch cross-chain operations into a single transaction. Batch 1
runs on Chain A only; Batches 2 and 3 run on each chain separately.

## Pre-flight checks

Before starting the migration, verify the following on **both chains**:

### 1. Confirm your admin status in TokenAdminRegistry

- Call `TokenAdminRegistry.getTokenConfig(tokenAddress)` on both chains. This returns:
  - `.administrator`: must match the address you control (EOA, multisig, or other account)
  - `.pendingAdministrator`: must be `address(0)` (no pending transfer in progress)
  - `.tokenPool`: the current v1 pool address
- If the administrator is a multisig or timelock, execute subsequent steps through that governance mechanism. If using
  a multisig like Safe, you can batch multiple steps into a single meta-transaction for atomicity.
- Note: even if `.pendingAdministrator` is non-zero, the current `.administrator` can still call `setPool`. However,
  resolving the pending transfer before starting is recommended. For example, if an administrator change you started
  earlier is no longer relevant, call `transferAdminRole(localToken, address(0))` to cancel it.

### 2. Confirm you can grant mint/burn roles on the Chain B token

- On **Chain B**, the new v2 BurnMintTokenPool needs mint and burn privileges on the token.
- Verify the address you control can call `grantMintAndBurnRoles` (or equivalent) on the Chain B token contract.
- **This is a hard blocker** for Chain B. If you can't grant roles, you can't migrate that side.
- On **Chain A**, the Lock & Release pool does NOT need mint/burn roles (it locks and releases; it doesn't burn or
  mint). No role grant is needed on the Chain A token.

### 3. Confirm you can withdraw liquidity from the v1 Chain A pool

- The v1 LockReleaseTokenPool holds locked token liquidity directly in its contract balance. You need to withdraw this
  liquidity and deposit it into the new v2 ERC20LockBox.
- **Only the rebalancer can call `withdrawLiquidity`**. The pool owner cannot withdraw directly. If the rebalancer is
  `address(0)` (never set), liquidity cannot be withdrawn at all.
- If the rebalancer is not the address you control, the pool owner can call `setRebalancer(address)` (`onlyOwner`) to
  change it. A common pattern is to temporarily set the rebalancer, perform the withdrawal, then restore the original
  rebalancer after (see Step 3).
- **This is a hard blocker**. If the rebalancer is `address(0)` and you are not the pool owner (and thus cannot call
  `setRebalancer`), you cannot fund the new lockbox.
- **Note**: `getRebalancer` and `setRebalancer` are v1 functions. v2 pools do NOT have a rebalancer concept. They use
  the lockbox + authorized callers pattern instead.

### 4. Record your existing v1 deployment details

Collect and export the following:

- **Token addresses** on each chain
- **v1 pool addresses** on each chain:
  - Chain A v1 pool (LockReleaseTokenPool)
  - Chain B v1 pool (BurnMintTokenPool)
- **v1 Chain A pool liquidity balance**: Check the pool's total token balance on a block explorer:
  `token.balanceOf(v1PoolAddress)`.
- **Current rate limiter settings** on the v1 pools (inbound and outbound per remote chain). You'll replicate these on
  the new v2 pools.
  - Read via two separate calls per remote chain (v1.5.x / v1.6.x):
    - `pool.getCurrentInboundRateLimiterState(remoteChainSelector)` → returns the inbound `TokenBucket`.
    - `pool.getCurrentOutboundRateLimiterState(remoteChainSelector)` → returns the outbound `TokenBucket`.
    - Each `TokenBucket` is `{ tokens, lastUpdated, isEnabled, capacity, rate }`. The struct is identical across v1.5.1,
      v1.6.1, and v2.0, so you can copy values directly.
    - Note: v2 collapses these into a single `getCurrentRateLimiterState(remoteChainSelector, fastFinality)` call
      returning both buckets, but v1 doesn't have that signature.

### 5. Confirm the token contracts are compatible

- **Chain A token**: Must be a standard ERC20 (the Lock & Release pool locks/releases, so no special interface is needed
  beyond `transfer`/`transferFrom`).
- **Chain B token**: Must implement `burn(amount)` and `mint(to, amount)` interfaces (for example, `IBurnMintERC20`).

### 6. Verify pool ownership model

- After deploying the new v2 pools, the deployer becomes the initial owner.
- `applyChainUpdates` and `removeRemotePool` require **pool owner** (`onlyOwner`) privileges.
- `setPool` on TokenAdminRegistry requires **TokenAdminRegistry administrator** (`onlyTokenAdmin`) privileges.
- If these are different addresses, coordinate accordingly.

## Step 1: Deploy new v2 token pools

This step is **asymmetric**: Chain A and Chain B require different contracts.

### Chain A: LockBox + LockReleaseTokenPool

Three sub-steps, executed in order:

#### 1a. Deploy ERC20LockBox

- Deploy an `ERC20LockBox` contract on Chain A.
- The lockbox holds locked token liquidity on behalf of the LockReleaseTokenPool. It **must be deployed before** the
  pool.
- **Constructor**:

```
constructor(address token)  // The ERC20 token address — must not be address(0)
```

- The lockbox initializes with an empty authorized callers list. Callers are added in Step 1c.
- After deployment: `export LOCK_BOX=0x...`

#### 1b. Deploy LockReleaseTokenPool

- Deploy a `LockReleaseTokenPool 2.0` on Chain A.
- **Constructor**:

```
constructor(
    IERC20 token,                // existing token address
    uint8 localTokenDecimals,    // token decimals on this chain
    address advancedPoolHooks,   // address(0) to skip, or AdvancedPoolHooks contract
    address rmnProxy,            // RMN proxy address for this chain
    address router,              // CCIP router address for this chain
    address lockBox              // ERC20LockBox address from Step 1a
)
```

- Pass `address(0)` for `advancedPoolHooks` if you don't need allowlisting, CCV configuration, or a policy engine. You
  can attach hooks later via `updateAdvancedPoolHooks`.
- Record the new v2 pool address.

#### 1c. Authorize pool on LockBox

- The new v2 pool must be an **authorized caller** on the lockbox before it can deposit/withdraw tokens.
- Call `applyAuthorizedCallerUpdates(AuthorizedCallerArgs)` on the lockbox (`onlyOwner`). The struct takes two arrays:

```
struct AuthorizedCallerArgs {
    address[] addedCallers;    // addresses to authorize
    address[] removedCallers;  // addresses to deauthorize
}
```

- The deployer is the initial lockbox owner.
- You may also add your timelock or multisig address as an authorized caller if you plan to manage liquidity through
  governance.
- Verify: call `getAllAuthorizedCallers()` on the lockbox and confirm the new pool is listed.

### Chain B: BurnMintTokenPool

- Deploy a `BurnMintTokenPool 2.0` on Chain B.
- **Constructor**: `(IBurnMintERC20 token, uint8 localTokenDecimals, address advancedPoolHooks, address rmnProxy, address router)`
- Grant mint and burn roles to the new v2 pool on the Chain B token contract.
- Record the new v2 pool address.

**Note (dual privilege window)**: From this point until Step 8b:

- On Chain B, **both** old v1 and new v2 BurnMint pools have mint/burn roles.
- On Chain A, the old v1 LockRelease pool still holds liquidity and is still registered. The new v2 pool exists but is
  empty and not yet connected.

**State after this step**

- Old v1 pools: still active, still routing all CCIP traffic.
- New v2 Chain A pool: deployed, authorized on lockbox, but **empty** (no liquidity yet) and not connected to CCIP.
- New v2 Chain B pool: deployed, has mint/burn roles, but not connected to CCIP.

## Step 2 (strongly recommended): Pause outbound transfers on old v1 pools

- Set outbound rate limiter to `isEnabled: true, capacity: 1, rate: 0` on the old v1 pools on **both chains**. This
  effectively blocks all transfers: the capacity allows at most 1 unit (smallest token denomination) and the zero rate
  means the bucket never refills. This works across all pool versions (v1.5.x through v2).
  - **Do NOT use `isEnabled: false`**. It disables rate limiting entirely and allows unlimited transfers.
- This blocks new cross-chain transfers while the migration completes, so expect a brief maintenance window.
- **Keep inbound open** so in-flight messages can still arrive and be processed.

**For Lock & Mint migrations, pausing is strongly recommended (not just optional)**. Unlike Burn & Mint, Lock & Mint has
a liquidity migration step (Step 3) that drains the v1 pool. If you don't pause:

- New tokens can get locked in the old v1 pool between Step 3 (drain) and Step 6 (cutover).
- These tokens are **stranded**. They're not in the v2 lockbox, so the v2 pool can't release them.
- This creates a **liquidity accounting mismatch**: the v2 lockbox is underfunded relative to the total locked tokens.
- Reverse (Mint→Lock) messages could fail if the lockbox doesn't have sufficient funds.

If you can batch steps atomically (for example, via a Safe multisig meta-transaction), this window is eliminated. If
you cannot batch atomically (for example, executing from a plain EOA), pausing is the safest approach.

**Trade-off**: Pausing prevents stranded liquidity and gives you clean accounting and a safe cutover. Not pausing means
zero downtime but risks stranded tokens and an underfunded lockbox.

## Step 3: Migrate liquidity to LockBox (Chain A only)

This step is unique to Lock & Mint migration. In v1, the Lock & Release pool held liquidity directly in its contract
balance. In v2, liquidity sits in the `ERC20LockBox`. You must move it.

**Warning (non-atomic execution risk)**: If you execute steps individually (not batched), the withdraw → approve →
deposit sequence is **NOT atomic**. Between withdraw and deposit, the full liquidity sits in your wallet. If the
account is compromised or the deposit fails, tokens are at risk. **Mitigations**: batch all sub-steps atomically via a
Safe multisig meta-transaction or similar account abstraction, or use a hardware wallet if executing individually.

### Migration sequence

#### 3a. Set rebalancer on old v1 pool

- Read the current rebalancer via `pool.getRebalancer()`.
- Call `setRebalancer(yourAddress)` on the old v1 pool to grant your address withdrawal rights. This requires pool
  owner privileges.
- If your address is not the current rebalancer, call `setRebalancer(yourAddress)` on the old v1 pool (`onlyOwner`) to
  grant your address withdrawal rights.

#### 3b. Withdraw liquidity from the old v1 pool

- Read the old pool's token balance: `token.balanceOf(v1PoolAddress)`. This is the total available for migration.
- Call `withdrawLiquidity(amount)` on the old v1 LockReleaseTokenPool. This function is callable only by the current
  **rebalancer** (set in Step 3a).
- **Full withdrawal** (recommended if you paused in Step 2): Withdraw the entire balance. Since outbound is paused, no
  new locks are arriving and in-flight releases have settled.
- **Partial withdrawal** (alternative if you did NOT pause): Withdraw a portion (for example, 80%) and leave the
  remainder to cover any in-flight release messages still pending on the old pool. You can perform a second migration
  later to move the remaining liquidity once in-flight messages have settled. This avoids downtime but requires
  monitoring.
- The withdrawn tokens are transferred to the rebalancer's address.

**Caution**: Any tokens left in the old v1 pool after `setPool` (Step 6) will not be accessible by the new v2 pool. If
you fully drained, verify `token.balanceOf(v1PoolAddress) = 0`.

#### 3c. Add your address as authorized caller on the lockbox (temporarily)

- Call `applyAuthorizedCallerUpdates` on the lockbox, adding your address as an authorized caller.
- This is required because only authorized callers can call `deposit`.
- This authorization is temporary. You will remove it in Step 3f after the deposit.

#### 3d. Approve the lockbox to spend tokens

- Call `token.approve(lockboxAddress, amount)` on the Chain A token contract.

#### 3e. Deposit tokens into the ERC20LockBox

- Call `lockbox.deposit(tokenAddress, 0, amount)` to deposit the tokens into the lockbox.
- The `remoteChainSelector` parameter (second arg) is **unused** by the ERC20LockBox contract, so pass `0` or any value.
  The lockbox is a simple token vault shared across all remote chains.

#### 3f. Remove your address from authorized callers (cleanup)

- Call `applyAuthorizedCallerUpdates` on the lockbox, removing your address from the authorized callers list.
- This minimizes the attack surface. Only the pool should remain as an authorized caller.

### Post-migration verification

After completing the deposit, verify:

- `token.balanceOf(v1PoolAddress) = 0` (v1 pool is drained)
- `token.balanceOf(lockboxAddress)` equals the total withdrawn amount

**State after this step**

- Old v1 Chain A pool: **drained** (no liquidity).
- New v2 lockbox: funded with the migrated liquidity.
- New v2 pools: still not connected to CCIP routing.

## Step 4: Configure new v2 pools (`applyChainUpdates`)

On each chain, call `applyChainUpdates` on the new v2 pool to configure remote chain connections.

### Function signature

```
function applyChainUpdates(
    uint64[] calldata remoteChainSelectorsToRemove,
    ChainUpdate[] calldata chainsToAdd
) external onlyOwner
```

Each `ChainUpdate` contains:

- `remoteChainSelector` (uint64)
- `remotePoolAddresses` (bytes\[]): array of remote pool addresses
- `remoteTokenAddress` (bytes): the token address on the remote chain
- `outboundRateLimiterConfig` (isEnabled, capacity, rate)
- `inboundRateLimiterConfig` (isEnabled, capacity, rate)

### Remote pool addresses: Include both old and new

The `remotePoolAddresses` array must include **both** the old v1 pool **and** the new v2 pool on the remote chain.
Multiple pools per remote chain are supported, and the contract stores them additively.

Note the **asymmetric pool types**:

- Configure Chain A pool (LockReleaseTokenPool):
  - Remote chain = Chain B
  - `remotePoolAddresses = [encode(v1ChainBBurnMintPool), encode(v2ChainBBurnMintPool)]`
- Configure Chain B pool (BurnMintTokenPool):
  - Remote chain = Chain A
  - `remotePoolAddresses = [encode(v1ChainALockReleasePool), encode(v2ChainALockReleasePool)]`

**Address encoding**: `remotePoolAddresses` is typed as `bytes[]` and `remoteTokenAddress` as `bytes`. The contract
stores whatever bytes you pass with no format validation beyond a non-zero-length check.

- **EVM remote chain**: left-pad the 20-byte address to 32 bytes. In Solidity: `abi.encode(poolAddress)`. In Go:
  `common.LeftPadBytes(addr.Bytes(), 32)`. Both produce the same 32 bytes: the 20-byte address right-aligned with 12
  leading zero bytes.

**Why both pools must be listed**: Messages sent through the old v1 pool before the cutover may still be in-flight. The
destination pool validates that the source pool address in the message matches one of its configured remote pools.
Without the old pool in the list, in-flight messages fail validation.

### Rate limiter configuration

- Rate limiters are configured atomically as part of `applyChainUpdates`.
- Replicate your existing v1 rate limiter settings (or adjust as needed for v2). The struct is identical between v1 and
  v2.
- Configure both inbound and outbound rate limits per remote chain.
- If you paused transfers in Step 2, set the v2 pool rate limits to your desired production values (not zero, because
  the v2 pool needs to be ready to handle traffic after cutover).
- **Note**: setting `isEnabled: false` means unlimited transfers (no rate limiting). Set `isEnabled: true` with
  appropriate capacity and rate to enforce limits.

### Error recovery

`applyChainUpdates` reverts with `ChainAlreadyExists` if the remote chain is already configured. You cannot call it
twice for the same chain. If you need to fix a misconfiguration after the initial call:

- **Add a missing remote pool**: call `addRemotePool(remoteChainSelector, encodedRemotePoolAddress)` (`onlyOwner`).
  The second arg is `bytes`, encoded as described above.
- **Remove a wrong remote pool**: call `removeRemotePool(remoteChainSelector, encodedRemotePoolAddress)` (`onlyOwner`).
  Pass the same bytes that were stored when adding.
- **Change rate limiters**: call `setRateLimitConfig(RateLimitConfigArgs[])` (`onlyOwner` or rate limit admin).
- **Fully reconfigure a chain**: first remove it via `applyChainUpdates([chainSelector], [])`, then re-add it with the
  correct configuration.

## Step 5: Verify new pool configuration

Before cutting over, verify the new v2 pools are correctly configured on **both chains**.

**Chain A (LockReleaseTokenPool)**:

- [ ] Remote chain configs include both old v1 and new v2 Chain B BurnMint pool addresses.
- [ ] Remote token address is correct (Chain B token).
- [ ] Inbound and outbound rate limiters match your intended settings.
- [ ] Pool is authorized caller on the lockbox (`lockbox.getAllAuthorizedCallers()` includes pool).
- [ ] Lockbox has the correct liquidity balance (`token.balanceOf(lockboxAddress)` matches expected total).
- [ ] Pool owner is the address you control (`pool.owner()`).

**Chain B (BurnMintTokenPool)**:

- [ ] Remote chain configs include both old v1 and new v2 Chain A LockRelease pool addresses.
- [ ] Remote token address is correct (Chain A token).
- [ ] Inbound and outbound rate limiters match your intended settings.
- [ ] The v2 pool has mint and burn roles on the token (verify via the token contract's role-checking functions).
- [ ] Pool owner is the address you control (`pool.owner()`).

**Both chains**:

- [ ] `TokenAdminRegistry.getTokenConfig(token).pendingAdministrator` is `address(0)`.
- [ ] v1 pool is paused (if Step 2 was executed). Verify `token.balanceOf(v1PoolAddress) = 0` (no new locks since
  drain).

Verify before proceeding, because `setPool` in the next step routes live traffic through the v2 pools.

## Step 6: Cut over to the new v2 pools (`setPool`)

- On **each chain**, call `TokenAdminRegistry.setPool(tokenAddress, newV2PoolAddress)` to point the registry to the new
  v2 pool.
- Call `setPool` directly, since your address is already the registered administrator. Do **not** call
  `proposeAdministrator` or `acceptAdminRole`; these register or transfer the administrator role and are not part of a
  pool upgrade.
- Once executed on a chain, all new CCIP transfers for this token on that chain route through the new v2 pool.
- Execute on both chains. The order does not matter because of the dual remote pool configuration from Step 4.

**What happens to in-flight messages after cutover**

- Messages sent through the v1 pool before `setPool` will still arrive at the destination.
- The destination's new v2 pool recognizes the old v1 remote pool address (configured in Step 4), so these messages
  validate and execute correctly.
- **For in-flight Lock→Mint messages**: If you paused in Step 2 and withdrew the full balance in Step 3, no residual
  tokens should be in the v1 pool. If you did NOT pause, any tokens locked between Step 3 and this cutover remain in
  the v1 pool and are NOT in the v2 lockbox, which creates a liquidity accounting mismatch (see Step 2 for why pausing
  is strongly recommended).

**Rollback**: If issues arise, you can call `TokenAdminRegistry.setPool(tokenAddress, oldV1PoolAddress)` to revert to
v1. However, for Lock & Mint:

- The v1 Chain A pool has been drained in Step 3. You would need to re-fund it via direct token transfer before it can
  process new releases. (`provideLiquidity` does not exist in the contract source.)
- Any in-flight messages sent through the v2 pool will fail validation on the v1 destination (since v1 pools were not
  configured with v2 remote pool addresses).
- **Rollback before Step 6 is simpler**: the tokens are in the v2 lockbox, and an authorized caller can recover them via
  `lockbox.withdraw()`. **Rollback after Step 6 is significantly harder** due to split liquidity state.

## Step 7: Validate end-to-end

- Send a cross-chain token transfer using `ccip-cli`: tokens should be **locked** on Chain A (in the lockbox) and
  **minted** on Chain B. `ccip-cli` uses the CCIP SDK and queries `api.ccip.chain.link` to track message status
  end-to-end.
- Verify the transfer completes successfully on Chain B.
- Confirm the transfer was routed through the new v2 pool (check transaction logs).
- Test the reverse direction (Chain B → Chain A): tokens should be **burned** on Chain B and **released** from the
  lockbox on Chain A.

## Step 8: Post-migration cleanup

**Important**: If you paused outbound in Step 2 and waited for in-flight messages to settle before draining, there
should be no pending messages by this point. Verify via [CCIP Explorer](https://ccip.chain.link/) or `ccip-cli` (which
queries `api.ccip.chain.link` and will soon support filtering by source token address) that all messages from the old
v1 pool addresses show SUCCESS status.

**Note**: Steps 8a and 8b are optional. The old pools are already inert (removed from TokenAdminRegistry) and pose
minimal risk if cleanup is deferred. If you performed a partial drain (left some liquidity in the old v1 pool for
in-flight releases), wait until those remaining messages have settled before removing old remote pool addresses.

### 8a. Remove old v1 remote pool addresses from the new v2 pools

Call `removeRemotePool(uint64 remoteChainSelector, bytes calldata remotePoolAddress)` (`onlyOwner`) on each chain:

- On Chain A pool: remove the old v1 Chain B BurnMint pool from the remote pool list.
- On Chain B pool: remove the old v1 Chain A LockRelease pool from the remote pool list.

**Caution**: If any in-flight messages from the old v1 pools are still pending, removing their remote pool address will
cause those messages to fail validation. If this happens, re-add the address using
`addRemotePool(uint64 remoteChainSelector, bytes calldata remotePoolAddress)` (`onlyOwner`) to unblock them.

### 8b. (Optional) Revoke old pool privileges

**Chain B**:

- Call `revokeMintRole(oldV1DestPoolAddress)` and `revokeBurnRole(oldV1DestPoolAddress)` on the Chain B token contract
  (if supported, for example, `BurnMintERC677`).

**Chain A**:

- The old v1 Lock & Release pool does **not** have mint/burn roles, so no revocation is needed.
- The old v1 Chain A pool is drained and removed from TokenAdminRegistry. It is inert.

**State after cleanup**

- v2 pools: active, routing all CCIP traffic, only v2 remote pools configured.
- v2 lockbox: holds all locked liquidity for Chain A.
- v1 pools: orphaned, not in TokenAdminRegistry, drained (Chain A) / no mint roles (Chain B, if revoked).

## Step 9 (optional): Explore v2 features

v2 pools support additional configuration not available in v1:

### Advanced pool hooks

- Deploy a separate `AdvancedPoolHooks` contract and attach it to your pool via
  `updateAdvancedPoolHooks(IAdvancedPoolHooks newHook)` (`onlyOwner`).
- Add your pool as an authorized caller on the hooks contract (constructor `authorizedCallers` or
  `applyAuthorizedCallerUpdates`); otherwise transfers through the pool revert.
- Alternatively, pass the hooks address in the pool constructor at deployment time (Step 1).
- If you don't need these features, leave the hooks address at `address(0)` (the default).
- Constructor: `(address[] allowlist, uint256 thresholdAmountForAdditionalCCVs, address policyEngine, address[] authorizedCallers)`
- Capabilities:
  - **Allowlisting**: Restrict which addresses can initiate transfers (moved from pool constructor in v1 to hooks in
    v2). Managed via `applyAllowListUpdates(address[] removes, address[] adds)` (`onlyOwner`). The allowlist is enabled
    only if you pass a non-empty `allowlist` to the constructor.
  - **CCV (Cross-Chain Verifier) management**: Configure per-chain verifiers for inbound and outbound transfers via
    `applyCCVConfigUpdates(CCVConfigArg[])` (`onlyOwner`).
  - **Threshold amount for additional CCVs**: Set via `setThresholdAmount(uint256)` (`onlyOwner`). When a transfer
    amount meets or exceeds this threshold, additional CCVs are required on top of the base CCVs, which gives
    high-value transfers tiered security. Set to `0` to disable (only base CCVs apply regardless of amount).
  - **Policy engine**: Attach a custom policy contract for pre-flight/post-flight validation via
    `setPolicyEngine(address)` (`onlyOwner`).

### Fast finality (FTF)

- Call `setAllowedFinalityConfig(bytes4 allowedFinality)` on the v2 pool (`onlyOwner`) to enable fast finality. The
  default `bytes4(0)` means the pool only accepts full-finality transfers; any non-zero value opts the pool into fast
  finality (either safe-head flag, minimum block-depth, or both). Read the current value via
  `getAllowedFinalityConfig()`.
- When fast finality is enabled, the pool can keep **separate rate limiter buckets per remote chain** for default
  (wait-for-finality) transfers and for fast-finality transfers. Until you enable the fast-finality buckets
  (`setRateLimitConfig` with `fastFinality: true`), fast-finality transfers use the default buckets.

### Rate limits

- v2 pools support **separate rate limits for default (wait-for-finality) and fast finality** per remote chain.
- Call `setRateLimitConfig(RateLimitConfigArgs[])` (`onlyOwner` or rate limit admin) to configure rate limits. Each
  `RateLimitConfigArgs` includes a `bool fastFinality` flag to target the default or fast-finality bucket.
- To read current rate limits, call `getCurrentRateLimiterState(remoteChainSelector, fastFinality)`. Pass `false` for
  the default (wait-for-finality) bucket and `true` for the fast-finality bucket.

### Token transfer fee configuration

- v2 pools have a **pool-level fee override** mechanism. Call `applyTokenTransferFeeConfigUpdates()` on **your pool**
  (`onlyOwner`) to configure per-destination-chain fees.
- Fees support **separate rates for default and fast finality**:
  - `finalityTransferFeeBps` / `finalityFeeUSDCents`: for default (wait-for-finality) transfers
  - `fastFinalityTransferFeeBps` / `fastFinalityFeeUSDCents`: for fast-finality transfers
- These pool-level fees override the global defaults set by Chainlink on the FeeQuoter contract. If you don't configure
  pool-level fees for a destination chain, the pool's `getFee` returns `isEnabled: false` and the FeeQuoter defaults
  apply.
- To withdraw accrued fees, call `withdrawFeeTokens(address[], address)` on the pool (`onlyOwner` or `feeAdmin`).

### Rate limit admin & fee admin delegation

- Call `setDynamicConfig(address router, address rateLimitAdmin, address feeAdmin)` on the v2 pool (`onlyOwner`).
- There is no separate `setRateLimitAdmin` function. All three are set together (pass the current value for any field
  you don't want to change; for example, router).
- The **rate limit admin** can call `setRateLimitConfig()` to modify rate limits without full pool ownership.
- The **fee admin** can call `withdrawFeeTokens(address[], address)` to withdraw accrued fees without full pool
  ownership.

### Pool & lockbox ownership transfer (recommended for production)

- Transfer pool ownership to a multisig or timelock contract after migration.
- Transfer lockbox ownership as well, and remove your address from authorized callers.
- This ensures all future owner-only configuration changes require governance approval.

## Multi-chain considerations

### 3+ chain deployments

For tokens deployed across 3+ chains, you typically have **one LockReleaseTokenPool on Chain A** and
**BurnMintTokenPools on each additional chain** (Chain B, C, D, etc.):

- The Chain A LockRelease pool must configure remote pool entries for **all** remote chains (both old v1 and new v2
  BurnMint pools on each).
- Each remote BurnMint pool must configure remote pool entries for Chain A's old v1 and new v2 LockRelease pool
  addresses.
- Liquidity migration (Step 3): The ERC20LockBox is a shared vault, so one withdrawal and one deposit cover all remote
  chains (the `remoteChainSelector` param is unused).

### Siloed liquidity (per-chain isolation)

If you need per-chain liquidity isolation, use `SiloedLockReleaseTokenPool` instead of `LockReleaseTokenPool`:

- No lockbox in the constructor. Configure per-chain lockboxes via `configureLockBoxes(LockBoxConfig[])` (`onlyOwner`).
- Each remote chain can have its own ERC20LockBox instance.
- Liquidity migration differs from the standard flow:
  - Use `withdrawSiloedLiquidity(remoteChainSelector, amount)` per remote chain (instead of `withdrawLiquidity`).
  - Each siloed chain has its own rebalancer. Set and restore it per chain via the pool's silo rebalancer functions.
  - Authorize, approve, and deposit into each chain's lockbox separately.
  - Also handle any unsiloed liquidity separately.

### Gradual migration

You can migrate remote chains sequentially. Chain A's v2 pool can serve both migrated (v2) and not-yet-migrated (v1)
destinations simultaneously, as long as all remote pool addresses are configured, including Chain A's v2 pool on each
not-yet-migrated chain's v1 pool (`addRemotePool`).

> **CAUTION: Disclaimer**
>
> Chainlink CCIP is an interoperability messaging protocol. Chainlink does not hold or transfer any assets. The
> performance and behaviour of applications using Chainlink CCIP may depend on coding, engineering, configuration, and
> other technical implementation choices made by developers, token issuers, Cross-Chain Verifiers, and other
> participants. Users remain responsible for evaluating, configuring, testing, deploying, operating, and maintaining
> their own applications and integrations, including assessing any applicable operational, security, technical, and
> legal or regulatory risks. Please review the [Chainlink Terms of Service](https://chain.link/terms) which provides
> important information and disclosures. By using Chainlink CCIP, you expressly acknowledge and agree to accept these
> terms. Cross-Chain Verifiers (CCVs) may be operated by third parties. The security, availability, governance, and
> operational profile of a CCV varies depending on the verifier selected. Users are solely responsible for evaluating
> any CCVs used in connection with their applications or integrations and determining whether they are appropriate for
> their intended use case. This code represents an example of using a Chainlink product or service. It is provided "AS
> IS" and "AS AVAILABLE" without warranties of any kind, has not been audited, and may omit checks or error handling.
> Each party intending to use this reference implementation must perform its own audits, security and code review, and
> testing before any production deployment and ensure the operation and performance of such code matches expectations.
> 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. Use of the Chainlink Network is subject to the Chainlink
> Foundation Terms of Service, which provides important information and disclosures. By using this code, you acknowledge
> and agree to these terms.