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
Automated Swap
Conditional Transfer
Multi-Recipient
// Encode swap parameters
bytes memory hookData = abi . encode (
"SWAP" ,
dexAddress,
outputToken,
minOutputAmount
);
tokenMessenger. depositForBurnWithHook (
amount,
destinationDomain,
recipientAddress,
address (usdc),
bytes32 ( 0 ),
maxFee,
1000 ,
hookData
);
// Encode release conditions
bytes memory hookData = abi . encode (
"ESCROW" ,
releaseTimestamp,
validatorAddress
);
tokenMessenger. depositForBurnWithHook (
amount,
destinationDomain,
escrowContract,
address (usdc),
bytes32 ( uint256 ( uint160 (validatorAddress))),
maxFee,
1000 ,
hookData
);
// Encode distribution plan
address [] memory recipients = new address []( 3 );
uint256 [] memory amounts = new uint256 []( 3 );
// ... populate arrays
bytes memory hookData = abi . encode (
"DISTRIBUTE" ,
recipients,
amounts
);
tokenMessenger. depositForBurnWithHook (
totalAmount,
destinationDomain,
distributorContract,
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
V1 (IMessageHandler)
V2 (IMessageHandlerV2)
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
V2 uses a new burn message format:
V1 BurnMessage
V2 BurnMessage
// V1 Burn Message Layout
// [0:4] - version
// [4:36] - burnToken
// [36:68] - mintRecipient
// [68:100] - amount
// [100:132] - messageSender
// V2 Burn Message Layout
// [0:4] - version
// [4:36] - burnToken
// [36:68] - mintRecipient
// [68:100] - amount
// [100:132] - messageSender
// [132:164] - maxFee // NEW
// [164:196] - expirationBlock // NEW
// [196:228] - feeExecuted // NEW
// [228:260] - hookDataLength // NEW
// [260:...] - hookData // NEW (variable length)
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