Skip to main content

New Features

Fee Support

V2 introduces a comprehensive fee mechanism for cross-chain transfers.

Fee Parameters

maxFee (New in depositForBurn) Specifies the maximum fee willing to pay on the destination domain:
// V1 - No fee parameter
function depositForBurn(
    uint256 amount,
    uint32 destinationDomain,
    bytes32 mintRecipient,
    address burnToken
) external returns (uint64 _nonce)

// V2 - Includes maxFee
function depositForBurn(
    uint256 amount,
    uint32 destinationDomain,
    bytes32 mintRecipient,
    address burnToken,
    bytes32 destinationCaller,
    uint256 maxFee,              // NEW
    uint32 minFinalityThreshold  // NEW
) external
Reference: src/v2/TokenMessengerV2.sol:166

Fee Recipient

Configurable address to receive collected fees:
// Set fee recipient (owner only)
tokenMessenger.setFeeRecipient(feeRecipientAddress);
Fees are automatically minted to the feeRecipient when processing unfinalized messages. Reference: src/v2/BaseTokenMessenger.sol:222

Minimum Fee Control

Protocol can enforce minimum fee requirements:
// Set minimum fee controller (owner only)
tokenMessenger.setMinFeeController(controllerAddress);

// Set minimum fee in 1/1000 basis points (controller only)
tokenMessenger.setMinFee(5000); // 0.05%
The minimum fee is validated on the source domain:
require(
    _maxFee >= _calcMinFeeAmount(_amount),
    "Insufficient max fee"
);
Reference: src/v2/BaseTokenMessenger.sol:232-244

Fee Calculation

function getMinFeeAmount(uint256 amount) external view returns (uint256) {
    if (minFee == 0) return 0;
    require(amount > 1, "Amount too low");
    return _calcMinFeeAmount(amount);
}

function _calcMinFeeAmount(uint256 _amount) internal view returns (uint256) {
    uint256 _minFeeAmount = _amount.mul(minFee) / MIN_FEE_MULTIPLIER;
    return _minFeeAmount == 0 ? 1 : _minFeeAmount;
}
Where MIN_FEE_MULTIPLIER = 10_000_000 for 1/1000 basis point precision. Reference: src/v2/TokenMessengerV2.sol:299-319

Hook Execution

V2 introduces the hookData parameter for custom logic execution on the destination domain.

depositForBurnWithHook

function depositForBurnWithHook(
    uint256 amount,
    uint32 destinationDomain,
    bytes32 mintRecipient,
    address burnToken,
    bytes32 destinationCaller,
    uint256 maxFee,
    uint32 minFinalityThreshold,
    bytes calldata hookData  // NEW
) external
Reference: src/v2/TokenMessengerV2.sol:210

Hook Data Requirements

  • Must be non-empty (length > 0)
  • Included in burn message body
  • Interpreted by destination domain recipient or relayer
  • Application-specific format

Hook Use Cases

// Encode swap parameters
bytes memory hookData = abi.encode(
    "SWAP",
    dexAddress,
    outputToken,
    minOutputAmount
);

tokenMessenger.depositForBurnWithHook(
    amount,
    destinationDomain,
    recipientAddress,
    address(usdc),
    bytes32(0),
    maxFee,
    1000,
    hookData
);

Finality Thresholds

V2 introduces configurable finality requirements for flexible security/speed tradeoffs.

Finality Constants

// The threshold at which (and above) messages are considered finalized
uint32 constant FINALITY_THRESHOLD_FINALIZED = 2000;

// The threshold at which (and above) messages are considered confirmed
uint32 constant FINALITY_THRESHOLD_CONFIRMED = 1000;

// The minimum allowed level of finality accepted by TokenMessenger
uint32 constant TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD = 500;
Reference: src/v2/FinalityThresholds.sol

Message Handlers

V2 introduces two message handler functions:
// For finalized messages (no fees)
function handleReceiveFinalizedMessage(
    uint32 remoteDomain,
    bytes32 sender,
    uint32 finalityThresholdExecuted,
    bytes calldata messageBody
) external returns (bool)

// For unfinalized messages (fees apply)
function handleReceiveUnfinalizedMessage(
    uint32 remoteDomain,
    bytes32 sender,
    uint32 finalityThresholdExecuted,
    bytes calldata messageBody
) external returns (bool)
Reference: src/v2/TokenMessengerV2.sol:245-292

Finality Validation

require(
    finalityThresholdExecuted >= TOKEN_MESSENGER_MIN_FINALITY_THRESHOLD,
    "Unsupported finality threshold"
);
TokenMessenger rejects messages attested below the minimum threshold (500). Reference: src/v2/TokenMessengerV2.sol:286-289

Denylist Functionality

V2 adds protocol-level denylist capabilities.

Denylistable Role

New Denylistable contract provides denylist functionality:
abstract contract Denylistable is Ownable2Step {
    address public denylister;
    mapping(address => bool) internal denylisted;

    modifier notDenylistedCallers() {
        require(!denylisted[msg.sender], "Caller is denylisted");
        _;
    }
}
Reference: src/roles/v2/Denylistable.sol

Denylist Management

// Set denylister (owner only)
tokenMessenger.updateDenylister(denylisterAddress);

// Add to denylist (denylister only)
tokenMessenger.addToDenylist(maliciousAddress);

// Remove from denylist (denylister only)
tokenMessenger.removeFromDenylist(rehabilitatedAddress);

// Check denylist status
bool isDenylisted = tokenMessenger.isDenylisted(address);

Protected Functions

The notDenylistedCallers modifier protects:
  • depositForBurn()
  • depositForBurnWithHook()
Denylisted addresses cannot initiate cross-chain burns. Reference: src/v2/TokenMessengerV2.sol:174

API Changes

depositForBurn Signature

function depositForBurn(
    uint256 amount,
    uint32 destinationDomain,
    bytes32 mintRecipient,
    address burnToken
) external returns (uint64 _nonce)
Breaking Change: V2 requires additional parameters. Use bytes32(0) for destinationCaller to allow any caller.

depositForBurnWithCaller

depositForBurnWithCaller() is removed in V2. Use depositForBurn() with destinationCaller parameter instead.
// V1 approach
tokenMessenger.depositForBurnWithCaller(
    amount,
    destinationDomain,
    mintRecipient,
    burnToken,
    destinationCaller
);

// V2 equivalent
tokenMessenger.depositForBurn(
    amount,
    destinationDomain,
    mintRecipient,
    burnToken,
    destinationCaller,  // Directly in main function
    maxFee,
    minFinalityThreshold
);

Event Signature Changes

DepositForBurn Event

event DepositForBurn(
    uint64 indexed nonce,
    address indexed burnToken,
    uint256 amount,
    address indexed depositor,
    bytes32 mintRecipient,
    uint32 destinationDomain,
    bytes32 destinationTokenMessenger,
    bytes32 destinationCaller
);
Key Differences:
  • nonce removed from V2 event (no longer indexed)
  • maxFee added
  • minFinalityThreshold added (indexed)
  • hookData added
  • Index changes: V1 indexes nonce, V2 indexes minFinalityThreshold
Reference: src/v2/TokenMessengerV2.sol:62-73

MintAndWithdraw Event

event MintAndWithdraw(
    address indexed mintRecipient,
    uint256 amount,
    address indexed mintToken
);
Reference: src/v2/BaseTokenMessenger.sol:84-89

Message Handler Interface

interface IMessageHandler {
    function handleReceiveMessage(
        uint32 sourceDomain,
        bytes32 sender,
        bytes calldata messageBody
    ) external returns (bool);
}
Reference: src/interfaces/v2/IMessageHandlerV2.sol

Contract Architecture Changes

Base Contracts

V2 introduces base contracts for shared functionality:
// V2 structure
contract TokenMessengerV2 is IMessageHandlerV2, BaseTokenMessenger {
    // ...
}

abstract contract BaseTokenMessenger is Rescuable, Denylistable, Initializable {
    // Shared admin functions
    // Fee management
    // Remote messenger management
}
Reference: src/v2/BaseTokenMessenger.sol

Initializable Pattern

V2 uses OpenZeppelin’s initializer pattern instead of direct constructor initialization:
// V1 - Direct setup in constructor
constructor(address _messageTransmitter, uint32 _messageBodyVersion) {
    localMessageTransmitter = IMessageTransmitter(_messageTransmitter);
    messageBodyVersion = _messageBodyVersion;
}

// V2 - Proxy initialization
constructor(address _messageTransmitter, uint32 _messageBodyVersion) 
    BaseTokenMessenger(_messageTransmitter, _messageBodyVersion) 
{
    _disableInitializers();
}

function initialize(
    TokenMessengerV2Roles calldata roles,
    uint256 minFee_,
    uint32[] calldata remoteDomains_,
    bytes32[] calldata remoteTokenMessengers_
) external initializer
Reference: src/v2/TokenMessengerV2.sol:84-142

CREATE2 Deployment

V2 uses CREATE2 for deterministic addresses:
contract Create2Factory is Ownable2Step {
    function create2Deploy(
        bytes32 salt,
        bytes memory bytecode
    ) external onlyOwner returns (address) {
        address addr;
        assembly {
            addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
        }
        require(addr != address(0), "Create2: Failed on deploy");
        return addr;
    }
}
Reference: src/v2/Create2Factory.sol

TokenMinter Changes

Dual-Recipient Mint

V2 TokenMinter supports minting to two recipients (for fee collection):
// V1 - Single recipient
function mint(
    uint32 sourceDomain,
    bytes32 burnToken,
    address to,
    uint256 amount
) external returns (address mintToken)

// V2 - Dual recipient (NEW)
function mint(
    uint32 sourceDomain,
    bytes32 burnToken,
    address recipientOne,
    address recipientTwo,
    uint256 amountOne,
    uint256 amountTwo
) external returns (address mintToken)
Reference: src/v2/TokenMinterV2.sol:54

Message Format Changes

V2 uses a new burn message format:
// V1 Burn Message Layout
// [0:4]    - version
// [4:36]   - burnToken
// [36:68]  - mintRecipient  
// [68:100] - amount
// [100:132] - messageSender
Reference: src/messages/v2/BurnMessageV2.sol

State Variables

New State Variables

// Fee management
address public feeRecipient;
address public minFeeController;
uint256 public minFee;
uint256 public constant MIN_FEE_MULTIPLIER = 10_000_000;
Reference: src/v2/BaseTokenMessenger.sol:104-114

Next Steps

Migration Guide

Learn how to migrate from V1 to V2

Deployment

Deploy V2 contracts to your network

TokenMessengerV2 API

Complete API reference

Integration Guide

Integrate V2 into your application

Build docs developers (and LLMs) love