# 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 CCIP v1 to v2 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 — locking and releasing uses 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 — 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 was intended
  in the past but no longer relevant, `transferAdminRole(localToken, address(0))` can be called to cancel the previous
  transfer.

### 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 — values can be copied 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, no special interface 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 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 (required): 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`** — this disables rate limiting entirely, allowing unlimited transfers.
- This blocks new cross-chain transfers while the migration completes, creating 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, clean accounting, safe cutover. Not pausing = zero downtime but
risk of stranded tokens and 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 — 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 — 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
OffRamp validates that the source pool address in the message matches a configured remote pool on the destination.
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 — 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`) —
  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 — `setPool` in the next step routes live traffic through the v2 pools.

## Step 6: Activate new v2 pools (`setPool`) — the cutover

- On **each chain**, call `TokenAdminRegistry.setPool(tokenAddress, newV2PoolAddress)` to point the registry to the new
  v2 pool.
- Call `setPool` directly — your address is already the registered administrator. Do **not** call `proposeAdministrator`
  or `acceptAdminRole`; these are only for initial registration, not for pool upgrades.
- 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 — creating 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 can be recovered 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` — 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 it.

### 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 — no revocation 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`).
- 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). AdvancedPoolHooks is fully
  optional.
- 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`).
  - **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 — providing tiered
    security for high-value transfers. 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 maintains **two separate rate limiter buckets per remote chain**: one for
  default (wait-for-finality) transfers and one for fast-finality transfers.

### 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: `getCurrentRateLimiterState(remoteChainSelector, fastFinality)` — pass `false` for the
  default (wait-for-finality) bucket, `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 (`isEnabled: false`), 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 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 — one withdrawal and one deposit covers 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 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 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.

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