The BurnMessage library defines the format for the message body used in CCTP token transfers. This message is embedded within the main Message’s messageBody field.
BurnMessage Structure
Message body version (4 bytes)
Address of the burned token on the source domain (32 bytes, starting at index 4)
Address that will receive minted tokens on destination domain (32 bytes, starting at index 36)
Amount of tokens burned (32 bytes, starting at index 68)
Address that initiated the burn (32 bytes, starting at index 100)
Memory Layout (V1)
Field Bytes Type Index
-----------------------------------------------
version 4 uint32 0
burnToken 32 bytes32 4
mintRecipient 32 bytes32 36
amount 32 uint256 68
messageSender 32 bytes32 100
Total: 132 bytes (fixed size)
Source: ~/workspace/source/src/messages/BurnMessage.sol:23-30
Version 2 adds fee handling and hook support:
Field Bytes Type Index
-----------------------------------------------
version 4 uint32 0
burnToken 32 bytes32 4
mintRecipient 32 bytes32 36
amount 32 uint256 68
messageSender 32 bytes32 100
maxFee 32 uint256 132
feeExecuted 32 uint256 164
expirationBlock 32 uint256 196
hookData dynamic bytes 228
Source: ~/workspace/source/src/messages/v2/BurnMessageV2.sol:26-37
V2 Additions
Maximum fee the sender is willing to pay on the destination domain (32 bytes, starting at index 132)
Actual fee charged during execution (set to 0 during message creation, populated on destination)
Block number after which the message expires (set to 0 during message creation)
Optional dynamic data passed to hooks for custom processing on destination domain
Encoding Functions
Source: ~/workspace/source/src/messages/BurnMessage.sol:57-72
function _formatMessage(
uint32 _version,
bytes32 _burnToken,
bytes32 _mintRecipient,
uint256 _amount,
bytes32 _messageSender
) internal pure returns (bytes memory)
Creates a V1 burn message with exactly 132 bytes.
Parameters:
_version: Message body version
_burnToken: Burned token address on source domain (as bytes32)
_mintRecipient: Recipient address on destination domain (as bytes32)
_amount: Amount burned
_messageSender: Address that initiated the burn (as bytes32)
Returns: Formatted 132-byte burn message
Source: ~/workspace/source/src/messages/v2/BurnMessageV2.sol:68-89
function _formatMessageForRelay(
uint32 _version,
bytes32 _burnToken,
bytes32 _mintRecipient,
uint256 _amount,
bytes32 _messageSender,
uint256 _maxFee,
bytes calldata _hookData
) internal pure returns (bytes memory)
Creates a V2 burn message with fee and hook support.
Parameters:
_version: Message body version
_burnToken: Burned token address (as bytes32)
_mintRecipient: Recipient address (as bytes32)
_amount: Amount burned
_messageSender: Initiator address (as bytes32)
_maxFee: Maximum fee for destination processing
_hookData: Optional hook data for custom logic
Returns: Formatted burn message (minimum 228 bytes + hookData length)
feeExecuted and expirationBlock are set to 0 during message creation. These fields are populated during attestation and execution.
Decoding Functions
V1 Getters
Source: ~/workspace/source/src/messages/BurnMessage.sol
// Get message version
function _getVersion(bytes29 _message) internal pure returns (uint32)
// Get burned token address
function _getBurnToken(bytes29 _message) internal pure returns (bytes32)
// Get mint recipient address
function _getMintRecipient(bytes29 _message) internal pure returns (bytes32)
// Get burned amount
function _getAmount(bytes29 _message) internal pure returns (uint256)
// Get message sender address
function _getMessageSender(bytes29 _message) internal pure returns (bytes32)
V2 Getters
V2 inherits V1 getters and adds:
Source: ~/workspace/source/src/messages/v2/BurnMessageV2.sol
// Inherited V1 getters
function _getVersion(bytes29 _message) internal pure returns (uint32)
function _getBurnToken(bytes29 _message) internal pure returns (bytes32)
function _getMintRecipient(bytes29 _message) internal pure returns (bytes32)
function _getAmount(bytes29 _message) internal pure returns (uint256)
function _getMessageSender(bytes29 _message) internal pure returns (bytes32)
// V2-specific getters
function _getMaxFee(bytes29 _message) internal pure returns (uint256)
function _getFeeExecuted(bytes29 _message) internal pure returns (uint256)
function _getExpirationBlock(bytes29 _message) internal pure returns (uint256)
function _getHookData(bytes29 _message) internal pure returns (bytes29)
V2 uses BurnMessage as a library dependency, calling V1 functions internally (e.g., _message._getVersion()).
Validation
V1 Validation
Source: ~/workspace/source/src/messages/BurnMessage.sol:131-134
function _validateBurnMessageFormat(bytes29 _message) internal pure {
require(_message.isValid(), "Malformed message");
require(_message.len() == BURN_MESSAGE_LEN, "Invalid message length");
}
Enforces exactly 132 bytes for V1 messages.
V2 Validation
Source: ~/workspace/source/src/messages/v2/BurnMessageV2.sol:151-157
function _validateBurnMessageFormat(bytes29 _message) internal pure {
require(_message.isValid(), "Malformed message");
require(
_message.len() >= HOOK_DATA_INDEX,
"Invalid burn message: too short"
);
}
Enforces minimum 228 bytes (allows variable-length hookData).
TypedMemView Usage
BurnMessage uses TypedMemView for efficient parsing:
using TypedMemView for bytes;
using TypedMemView for bytes29;
- indexUint(offset, length): Reads uint256/uint32 at byte offset
- index(offset, length): Reads bytes32 at byte offset
- slice(offset, length, type): Extracts subview for hookData
See TypedMemView documentation for details.
Example Usage
Encoding a V1 BurnMessage
import {BurnMessage} from "./messages/BurnMessage.sol";
import {AddressUtils} from "./messages/v2/AddressUtils.sol";
// Convert addresses
bytes32 tokenBytes = AddressUtils.toBytes32(usdcAddress);
bytes32 recipientBytes = AddressUtils.toBytes32(destinationAddress);
bytes32 senderBytes = AddressUtils.toBytes32(msg.sender);
// Format burn message
bytes memory burnMsg = BurnMessage._formatMessage(
0, // version
tokenBytes, // burn token
recipientBytes, // mint recipient
1000000, // amount (1 USDC with 6 decimals)
senderBytes // message sender
);
// burnMsg is exactly 132 bytes
Encoding a V2 BurnMessage with Hooks
import {BurnMessageV2} from "./messages/v2/BurnMessageV2.sol";
import {AddressUtils} from "./messages/v2/AddressUtils.sol";
// Prepare hook data
bytes memory hookData = abi.encode(
recipientContract,
"customFunction(uint256)",
additionalParams
);
// Format V2 burn message
bytes memory burnMsg = BurnMessageV2._formatMessageForRelay(
1, // version 1
tokenBytes, // burn token
recipientBytes, // mint recipient
1000000, // amount
senderBytes, // message sender
5000, // max fee (0.005 USDC)
hookData // hook data
);
// burnMsg is 228 + hookData.length bytes
Decoding a BurnMessage
import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol";
import {BurnMessage} from "./messages/BurnMessage.sol";
using TypedMemView for bytes;
using BurnMessage for bytes29;
// Parse message body from main message
bytes29 messageBody = message._messageBody();
// Validate format
messageBody._validateBurnMessageFormat();
// Extract fields
uint32 version = messageBody._getVersion();
bytes32 burnToken = messageBody._getBurnToken();
bytes32 mintRecipient = messageBody._getMintRecipient();
uint256 amount = messageBody._getAmount();
bytes32 sender = messageBody._getMessageSender();
// Convert to addresses
address tokenAddress = AddressUtils.toAddress(burnToken);
address recipientAddress = AddressUtils.toAddress(mintRecipient);
Decoding V2 with Fee and Hook Data
import {BurnMessageV2} from "./messages/v2/BurnMessageV2.sol";
using TypedMemView for bytes;
using BurnMessageV2 for bytes29;
bytes29 burnMessageView = messageBody.ref(0);
// Validate V2 format
burnMessageView._validateBurnMessageFormat();
// Extract V2-specific fields
uint256 maxFee = burnMessageView._getMaxFee();
uint256 feeExecuted = burnMessageView._getFeeExecuted();
uint256 expirationBlock = burnMessageView._getExpirationBlock();
bytes29 hookData = burnMessageView._getHookData();
// Process hook data if present
if (hookData.len() > 0) {
// Decode and execute hook logic
bytes memory hookBytes = hookData.clone();
(address target, bytes memory callData) = abi.decode(hookBytes, (address, bytes));
// Execute hook...
}
Integration with Message
BurnMessage is embedded as the messageBody field in Message:
import {Message} from "./messages/Message.sol";
import {BurnMessage} from "./messages/BurnMessage.sol";
// Create burn message
bytes memory burnMsg = BurnMessage._formatMessage(...);
// Embed in main message
bytes memory fullMessage = Message._formatMessage(
0, // message version
sourceDomain,
destinationDomain,
nonce,
sender,
recipient,
destinationCaller,
burnMsg // burn message as body
);
See Also