> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/circlefin/evm-cctp-contracts/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrating from V1 to V2

> Step-by-step guide for migrating your integration from V1 to V2 contracts

## Overview

This guide helps you migrate your CCTP integration from V1 to V2 contracts. V2 introduces breaking changes in function signatures and event structures that require code updates.

<Warning>
  V1 and V2 are separate protocol deployments. You cannot send V1 messages to V2 contracts or vice versa. Both versions can coexist on the same chain.
</Warning>

## Migration Checklist

* [ ] Review new V2 features and determine which to use
* [ ] Update `depositForBurn` function calls with new parameters
* [ ] Replace `depositForBurnWithCaller` usage
* [ ] Update event listeners for new event signatures
* [ ] Add fee calculation logic
* [ ] Configure new role addresses
* [ ] Update message handler interface (if applicable)
* [ ] Deploy or connect to V2 contracts
* [ ] Test integration thoroughly

## API Changes

### depositForBurn Function

The `depositForBurn` function signature has changed significantly.

#### V1 to V2 Comparison

<CodeGroup>
  ```solidity V1 theme={null}
  // Approve USDC
  usdc.approve(address(tokenMessenger), amount);

  // Deposit for burn
  uint64 nonce = tokenMessenger.depositForBurn(
      amount,
      destinationDomain,
      mintRecipient,
      burnToken
  );
  ```

  ```solidity V2 theme={null}
  // Approve USDC
  usdc.approve(address(tokenMessenger), amount);

  // Deposit for burn with new parameters
  tokenMessenger.depositForBurn(
      amount,
      destinationDomain,
      mintRecipient,
      burnToken,
      bytes32(0),              // destinationCaller (new)
      maxFee,                  // maxFee (new)
      minFinalityThreshold     // minFinalityThreshold (new)
  );
  // Note: V2 does not return nonce
  ```
</CodeGroup>

#### Parameter Mapping

| Parameter              | V1             | V2   | Notes                                      |
| ---------------------- | -------------- | ---- | ------------------------------------------ |
| `amount`               | ✅              | ✅    | Same                                       |
| `destinationDomain`    | ✅              | ✅    | Same                                       |
| `mintRecipient`        | ✅              | ✅    | Same                                       |
| `burnToken`            | ✅              | ✅    | Same                                       |
| `destinationCaller`    | ❌              | ✅    | New - use `bytes32(0)` for any caller      |
| `maxFee`               | ❌              | ✅    | New - must be \< amount and >= minFee      |
| `minFinalityThreshold` | ❌              | ✅    | New - minimum 500, use 1000 for most cases |
| **Return value**       | `uint64 nonce` | none | V2 doesn't return nonce                    |

#### Migration Example

```solidity theme={null}
// V1 code
function bridgeUSDC(uint256 amount, uint32 destDomain, bytes32 recipient) external {
    usdc.approve(address(tokenMessengerV1), amount);
    
    uint64 nonce = tokenMessengerV1.depositForBurn(
        amount,
        destDomain,
        recipient,
        address(usdc)
    );
    
    emit BridgeInitiated(nonce, amount);
}

// V2 code
function bridgeUSDC(uint256 amount, uint32 destDomain, bytes32 recipient) external {
    usdc.approve(address(tokenMessengerV2), amount);
    
    // Calculate required fee
    uint256 minFeeAmount = tokenMessengerV2.getMinFeeAmount(amount);
    uint256 maxFee = minFeeAmount + 1000; // Add buffer
    
    tokenMessengerV2.depositForBurn(
        amount,
        destDomain,
        recipient,
        address(usdc),
        bytes32(0),                           // Any caller can relay
        maxFee,                               // Maximum fee willing to pay
        1000                                  // Confirmed finality
    );
    
    // Note: Cannot use nonce anymore, use event filtering instead
    emit BridgeInitiated(amount);
}
```

### depositForBurnWithCaller Removal

`depositForBurnWithCaller` is removed in V2. Use `depositForBurn` with the `destinationCaller` parameter instead.

<CodeGroup>
  ```solidity V1 theme={null}
  // Specify authorized caller
  tokenMessenger.depositForBurnWithCaller(
      amount,
      destinationDomain,
      mintRecipient,
      burnToken,
      destinationCaller
  );
  ```

  ```solidity V2 theme={null}
  // Use main depositForBurn function
  tokenMessenger.depositForBurn(
      amount,
      destinationDomain,
      mintRecipient,
      burnToken,
      destinationCaller,    // Moved to main function
      maxFee,
      minFinalityThreshold
  );
  ```
</CodeGroup>

### replaceDepositForBurn

<Warning>
  `replaceDepositForBurn()` is not available in V2. Message replacement functionality has been removed.
</Warning>

If you rely on message replacement:

1. **Alternative 1**: Send a new message with updated parameters
2. **Alternative 2**: Implement application-level message invalidation logic
3. **Alternative 3**: Use hook data to encode conditional logic

## Event Changes

### DepositForBurn Event

The event signature has changed significantly.

#### Event Structure Comparison

<CodeGroup>
  ```solidity V1 theme={null}
  event DepositForBurn(
      uint64 indexed nonce,
      address indexed burnToken,
      uint256 amount,
      address indexed depositor,
      bytes32 mintRecipient,
      uint32 destinationDomain,
      bytes32 destinationTokenMessenger,
      bytes32 destinationCaller
  );
  ```

  ```solidity V2 theme={null}
  event DepositForBurn(
      address indexed burnToken,
      uint256 amount,
      address indexed depositor,
      bytes32 mintRecipient,
      uint32 destinationDomain,
      bytes32 destinationTokenMessenger,
      bytes32 destinationCaller,
      uint256 maxFee,                      // NEW
      uint32 indexed minFinalityThreshold, // NEW
      bytes hookData                       // NEW
  );
  ```
</CodeGroup>

#### Migration Impact

**Key Changes**:

* ❌ `nonce` removed (was indexed)
* ✅ `minFinalityThreshold` added (indexed)
* ✅ `maxFee` added
* ✅ `hookData` added

#### Event Listener Migration

<CodeGroup>
  ```javascript V1 Event Listener theme={null}
  // V1 - Listen by nonce
  const filter = tokenMessenger.filters.DepositForBurn(
      nonce,              // indexed
      null,               // burnToken
      null                // depositor
  );

  const events = await tokenMessenger.queryFilter(filter);
  console.log('Amount:', events[0].args.amount);
  ```

  ```javascript V2 Event Listener theme={null}
  // V2 - Listen by finality threshold or burnToken
  const filter = tokenMessenger.filters.DepositForBurn(
      null,               // burnToken
      null,               // depositor
      1000                // minFinalityThreshold (indexed)
  );

  const events = await tokenMessenger.queryFilter(filter);
  console.log('Amount:', events[0].args.amount);
  console.log('Max Fee:', events[0].args.maxFee);
  console.log('Hook Data:', events[0].args.hookData);
  ```
</CodeGroup>

### MintAndWithdraw Event

<CodeGroup>
  ```solidity V1 theme={null}
  event MintAndWithdraw(
      address indexed mintRecipient,
      uint256 amount,
      address indexed mintToken
  );
  ```

  ```solidity V2 theme={null}
  event MintAndWithdraw(
      address indexed mintRecipient,
      uint256 amount,
      address indexed mintToken,
      uint256 feeCollected  // NEW
  );
  ```
</CodeGroup>

**Migration**: Update event listeners to include `feeCollected` field.

## Fee Management

### Calculating Fees

V2 requires fee calculation before calling `depositForBurn`.

```solidity theme={null}
// Get minimum fee amount
uint256 minFeeAmount = tokenMessenger.getMinFeeAmount(amount);

// Add buffer for fee fluctuation (optional)
uint256 maxFee = minFeeAmount.mul(110).div(100); // 10% buffer

// Ensure maxFee < amount
require(maxFee < amount, "Fee exceeds amount");

tokenMessenger.depositForBurn(
    amount,
    destinationDomain,
    mintRecipient,
    address(usdc),
    bytes32(0),
    maxFee,
    minFinalityThreshold
);
```

### Fee Recipient

Set up fee collection:

```solidity theme={null}
// Owner sets fee recipient
tokenMessenger.setFeeRecipient(treasuryAddress);

// Fees are automatically minted to feeRecipient
// when messages are processed as unfinalized
```

### Zero Fee Transfers

To avoid fees entirely, use finalized messages:

```solidity theme={null}
// Use finalized threshold (2000) for zero-fee transfers
tokenMessenger.depositForBurn(
    amount,
    destinationDomain,
    mintRecipient,
    address(usdc),
    bytes32(0),
    0,      // maxFee can be 0 for finalized
    2000    // FINALITY_THRESHOLD_FINALIZED
);
```

<Note>
  Finalized messages take longer to process but don't incur fees.
</Note>

## Message Handler Interface

If you implement custom message handlers, update your interface.

### Interface Changes

<CodeGroup>
  ```solidity V1 Handler theme={null}
  contract MyHandler is IMessageHandler {
      function handleReceiveMessage(
          uint32 sourceDomain,
          bytes32 sender,
          bytes calldata messageBody
      ) external override returns (bool) {
          // Process message
          return true;
      }
  }
  ```

  ```solidity V2 Handler theme={null}
  contract MyHandler is IMessageHandlerV2 {
      function handleReceiveFinalizedMessage(
          uint32 sourceDomain,
          bytes32 sender,
          uint32 finalityThresholdExecuted,
          bytes calldata messageBody
      ) external override returns (bool) {
          // Process finalized message (no fee deduction)
          return true;
      }

      function handleReceiveUnfinalizedMessage(
          uint32 sourceDomain,
          bytes32 sender,
          uint32 finalityThresholdExecuted,
          bytes calldata messageBody
      ) external override returns (bool) {
          // Process unfinalized message (fee deducted)
          require(
              finalityThresholdExecuted >= 500,
              "Finality too low"
          );
          return true;
      }
  }
  ```
</CodeGroup>

## Role Configuration

V2 introduces new administrative roles.

### Required Role Addresses

<Tabs>
  <Tab title="V1 Roles">
    ```bash theme={null}
    # V1 .env configuration
    MESSAGE_TRANSMITTER_PAUSER_ADDRESS=<address>
    TOKEN_MINTER_PAUSER_ADDRESS=<address>
    MESSAGE_TRANSMITTER_RESCUER_ADDRESS=<address>
    TOKEN_MESSENGER_RESCUER_ADDRESS=<address>
    TOKEN_MINTER_RESCUER_ADDRESS=<address>
    TOKEN_CONTROLLER_ADDRESS=<address>
    ```
  </Tab>

  <Tab title="V2 Roles">
    ```bash theme={null}
    # V2 .env configuration (all V1 roles plus:)

    # Existing V1 roles
    MESSAGE_TRANSMITTER_V2_PAUSER_ADDRESS=<address>
    TOKEN_MINTER_V2_PAUSER_ADDRESS=<address>
    MESSAGE_TRANSMITTER_V2_RESCUER_ADDRESS=<address>
    TOKEN_MESSENGER_V2_RESCUER_ADDRESS=<address>
    TOKEN_MINTER_V2_RESCUER_ADDRESS=<address>
    TOKEN_CONTROLLER_ADDRESS=<address>

    # NEW V2 roles
    TOKEN_MESSENGER_V2_FEE_RECIPIENT_ADDRESS=<address>
    TOKEN_MESSENGER_V2_DENYLISTER_ADDRESS=<address>
    TOKEN_MESSENGER_V2_MIN_FEE_CONTROLLER_ADDRESS=<address>
    ```
  </Tab>
</Tabs>

### Role Responsibilities

| Role                 | Description                   | When to Update            |
| -------------------- | ----------------------------- | ------------------------- |
| **feeRecipient**     | Receives collected fees       | Set during initialization |
| **denylister**       | Manages protocol denylist     | Set during initialization |
| **minFeeController** | Sets minimum fee requirements | Set during initialization |

## Contract Interface Differences

### New Functions in V2

```solidity theme={null}
// Fee management
function setFeeRecipient(address _feeRecipient) external;
function setMinFeeController(address _minFeeController) external;
function setMinFee(uint256 _minFee) external;
function getMinFeeAmount(uint256 amount) external view returns (uint256);

// Denylist management
function addToDenylist(address account) external;
function removeFromDenylist(address account) external;
function isDenylisted(address account) external view returns (bool);

// Hook support
function depositForBurnWithHook(
    uint256 amount,
    uint32 destinationDomain,
    bytes32 mintRecipient,
    address burnToken,
    bytes32 destinationCaller,
    uint256 maxFee,
    uint32 minFinalityThreshold,
    bytes calldata hookData
) external;
```

### Removed Functions in V2

```solidity theme={null}
// ❌ Removed - Use depositForBurn with destinationCaller parameter
function depositForBurnWithCaller(...) external returns (uint64 nonce);

// ❌ Removed - Message replacement not supported
function replaceDepositForBurn(...) external;
```

## Smart Contract Migration Example

Complete example showing V1 to V2 migration:

<CodeGroup>
  ```solidity V1 Integration theme={null}
  contract MyBridgeV1 {
      ITokenMessenger public tokenMessenger;
      IERC20 public usdc;
      
      constructor(address _tokenMessenger, address _usdc) {
          tokenMessenger = ITokenMessenger(_tokenMessenger);
          usdc = IERC20(_usdc);
      }
      
      function bridge(
          uint256 amount,
          uint32 destDomain,
          bytes32 recipient
      ) external {
          usdc.transferFrom(msg.sender, address(this), amount);
          usdc.approve(address(tokenMessenger), amount);
          
          uint64 nonce = tokenMessenger.depositForBurn(
              amount,
              destDomain,
              recipient,
              address(usdc)
          );
          
          emit Bridged(nonce, msg.sender, amount);
      }
      
      event Bridged(uint64 indexed nonce, address indexed user, uint256 amount);
  }
  ```

  ```solidity V2 Integration theme={null}
  contract MyBridgeV2 {
      ITokenMessengerV2 public tokenMessenger;
      IERC20 public usdc;
      uint32 public defaultFinality;
      
      constructor(
          address _tokenMessenger,
          address _usdc,
          uint32 _defaultFinality
      ) {
          tokenMessenger = ITokenMessengerV2(_tokenMessenger);
          usdc = IERC20(_usdc);
          defaultFinality = _defaultFinality;
      }
      
      function bridge(
          uint256 amount,
          uint32 destDomain,
          bytes32 recipient
      ) external {
          usdc.transferFrom(msg.sender, address(this), amount);
          usdc.approve(address(tokenMessenger), amount);
          
          // Calculate minimum fee
          uint256 minFeeAmount = tokenMessenger.getMinFeeAmount(amount);
          uint256 maxFee = minFeeAmount > 0 
              ? minFeeAmount + (minFeeAmount / 10)  // 10% buffer
              : 0;
          
          require(maxFee < amount, "Fee too high");
          
          tokenMessenger.depositForBurn(
              amount,
              destDomain,
              recipient,
              address(usdc),
              bytes32(0),          // Any caller
              maxFee,
              defaultFinality
          );
          
          emit Bridged(msg.sender, amount, maxFee);
      }
      
      function bridgeWithHook(
          uint256 amount,
          uint32 destDomain,
          bytes32 recipient,
          bytes calldata hookData
      ) external {
          usdc.transferFrom(msg.sender, address(this), amount);
          usdc.approve(address(tokenMessenger), amount);
          
          uint256 minFeeAmount = tokenMessenger.getMinFeeAmount(amount);
          uint256 maxFee = minFeeAmount > 0
              ? minFeeAmount + (minFeeAmount / 10)
              : 0;
          
          require(maxFee < amount, "Fee too high");
          
          tokenMessenger.depositForBurnWithHook(
              amount,
              destDomain,
              recipient,
              address(usdc),
              bytes32(0),
              maxFee,
              defaultFinality,
              hookData
          );
          
          emit BridgedWithHook(msg.sender, amount, maxFee, hookData);
      }
      
      event Bridged(
          address indexed user,
          uint256 amount,
          uint256 maxFee
      );
      
      event BridgedWithHook(
          address indexed user,
          uint256 amount,
          uint256 maxFee,
          bytes hookData
      );
  }
  ```
</CodeGroup>

## Frontend Integration Changes

### ethers.js v6 Example

<CodeGroup>
  ```typescript V1 Frontend theme={null}
  import { ethers } from 'ethers';

  // V1 deposit
  async function depositForBurnV1(
    tokenMessenger: Contract,
    amount: bigint,
    destinationDomain: number,
    mintRecipient: string,
    burnToken: string
  ) {
    const tx = await tokenMessenger.depositForBurn(
      amount,
      destinationDomain,
      mintRecipient,
      burnToken
    );
    
    const receipt = await tx.wait();
    const event = receipt.logs
      .map(log => tokenMessenger.interface.parseLog(log))
      .find(e => e?.name === 'DepositForBurn');
    
    return event?.args.nonce;  // Get nonce
  }
  ```

  ```typescript V2 Frontend theme={null}
  import { ethers } from 'ethers';

  // V2 deposit
  async function depositForBurnV2(
    tokenMessenger: Contract,
    amount: bigint,
    destinationDomain: number,
    mintRecipient: string,
    burnToken: string,
    minFinalityThreshold: number = 1000
  ) {
    // Calculate minimum fee
    const minFee = await tokenMessenger.getMinFeeAmount(amount);
    const maxFee = minFee > 0n ? minFee + (minFee / 10n) : 0n;
    
    if (maxFee >= amount) {
      throw new Error('Amount too low for fee');
    }
    
    const tx = await tokenMessenger.depositForBurn(
      amount,
      destinationDomain,
      mintRecipient,
      burnToken,
      ethers.ZeroHash,        // destinationCaller (any)
      maxFee,
      minFinalityThreshold
    );
    
    const receipt = await tx.wait();
    const event = receipt.logs
      .map(log => tokenMessenger.interface.parseLog(log))
      .find(e => e?.name === 'DepositForBurn');
    
    // Note: No nonce in V2
    return {
      txHash: receipt.hash,
      maxFee: event?.args.maxFee,
      hookData: event?.args.hookData
    };
  }
  ```
</CodeGroup>

## Testing Your Migration

### Test Checklist

<Steps>
  <Step title="Test Fee Calculation">
    ```solidity theme={null}
    // Test minimum fee calculation
    uint256 amount = 1000000; // 1 USDC
    uint256 minFee = tokenMessenger.getMinFeeAmount(amount);
    assert(minFee > 0 && minFee < amount);
    ```
  </Step>

  <Step title="Test Basic Transfer">
    ```solidity theme={null}
    // Test basic V2 transfer
    usdc.approve(address(tokenMessenger), amount);
    tokenMessenger.depositForBurn(
        amount,
        destinationDomain,
        recipientBytes32,
        address(usdc),
        bytes32(0),
        minFee,
        1000
    );
    ```
  </Step>

  <Step title="Test Hook Transfer">
    ```solidity theme={null}
    // Test transfer with hook data
    bytes memory hookData = abi.encode("TEST", address(this));
    tokenMessenger.depositForBurnWithHook(
        amount,
        destinationDomain,
        recipientBytes32,
        address(usdc),
        bytes32(0),
        minFee,
        1000,
        hookData
    );
    ```
  </Step>

  <Step title="Test Event Parsing">
    ```typescript theme={null}
    // Verify event structure
    const events = await tokenMessenger.queryFilter(
      tokenMessenger.filters.DepositForBurn()
    );
    const event = events[0];
    assert(event.args.maxFee !== undefined);
    assert(event.args.minFinalityThreshold === 1000);
    ```
  </Step>
</Steps>

## Common Migration Issues

### Issue: "Insufficient max fee" Error

**Cause**: `maxFee` is less than the minimum required fee.

**Solution**: Call `getMinFeeAmount()` before `depositForBurn()`:

```solidity theme={null}
uint256 minFee = tokenMessenger.getMinFeeAmount(amount);
uint256 maxFee = minFee + buffer;
tokenMessenger.depositForBurn(..., maxFee, ...);
```

### Issue: Missing Nonce in V2

**Cause**: V2 doesn't return nonce from `depositForBurn()`.

**Solution**: Use transaction hash or event filtering instead:

```typescript theme={null}
const tx = await tokenMessenger.depositForBurn(...);
const receipt = await tx.wait();
const identifier = receipt.hash;  // Use tx hash instead of nonce
```

### Issue: "Caller is denylisted" Error

**Cause**: Calling address is on the protocol denylist.

**Solution**: Check denylist status:

```solidity theme={null}
bool denylisted = tokenMessenger.isDenylisted(msg.sender);
if (denylisted) {
    revert("Address is denylisted");
}
```

### Issue: Wrong destinationCaller Format

**Cause**: Using address instead of bytes32.

**Solution**: Convert address to bytes32:

```solidity theme={null}
// Wrong
tokenMessenger.depositForBurn(..., destinationCallerAddress, ...);

// Correct
bytes32 destinationCaller = bytes32(uint256(uint160(destinationCallerAddress)));
tokenMessenger.depositForBurn(..., destinationCaller, ...);

// Or use any caller
tokenMessenger.depositForBurn(..., bytes32(0), ...);
```

## Next Steps

<CardGroup cols={2}>
  <Card title="V2 Deployment" icon="rocket" href="/v2/deployment">
    Deploy V2 contracts to your network
  </Card>

  <Card title="TokenMessengerV2 API" icon="code" href="/api/v2/token-messenger-v2">
    Complete V2 API reference
  </Card>

  <Card title="Integration Guide" icon="plug" href="/guides/integration">
    Build on CCTP V2
  </Card>

  <Card title="Testing Guide" icon="flask" href="/guides/testing">
    Test your V2 integration
  </Card>
</CardGroup>
