Skip to main content
The Message library defines the format for cross-chain messages in CCTP. Messages use a fixed-size header with a dynamic message body, leveraging TypedMemView for efficient memory operations.

Message Structure

Messages contain metadata about the cross-chain transfer and routing information:
version
uint32
required
Message format version (4 bytes)
sourceDomain
uint32
required
Domain identifier of the source chain (4 bytes)
destinationDomain
uint32
required
Domain identifier of the destination chain (4 bytes)
nonce
uint64
required
Destination-specific nonce for replay protection (8 bytes)
In V2, this is expanded to bytes32 (32 bytes)
sender
bytes32
required
Address of sender on source chain as bytes32 (32 bytes, starting at index 20)
recipient
bytes32
required
Address of recipient on destination chain as bytes32 (32 bytes, starting at index 52)
destinationCaller
bytes32
required
Address authorized to call receiveMessage on destination chain (32 bytes, starting at index 84)Set to bytes32(0) to allow any caller
messageBody
bytes
required
Dynamic bytes payload containing the burn message or custom data (starting at index 116)

Memory Layout (V1)

Field                 Bytes      Type       Index
-----------------------------------------------
version               4          uint32     0
sourceDomain          4          uint32     4
destinationDomain     4          uint32     8
nonce                 8          uint64     12
sender                32         bytes32    20
recipient             32         bytes32    52
destinationCaller     32         bytes32    84
messageBody           dynamic    bytes      116
Fields use specific padding: uintNN fields are left-padded, and bytesNN fields are right-padded. This ensures fixed-size encoding and prevents hash collisions.

Message V2 Format

Version 2 introduces additional fields for finality control:
Field                        Bytes      Type       Index
----------------------------------------------------------
version                      4          uint32     0
sourceDomain                 4          uint32     4
destinationDomain            4          uint32     8
nonce                        32         bytes32    12
sender                       32         bytes32    44
recipient                    32         bytes32    76
destinationCaller            32         bytes32    108
minFinalityThreshold         4          uint32     140
finalityThresholdExecuted    4          uint32     144
messageBody                  dynamic    bytes      148

V2 Key Differences

  • nonce: Expanded from uint64 (8 bytes) to bytes32 (32 bytes)
  • minFinalityThreshold: Minimum finality level required for attestation
  • finalityThresholdExecuted: Finality level at which message was executed

Encoding Functions

Format Message (V1)

Source: ~/workspace/source/src/messages/Message.sol:66-87
function _formatMessage(
    uint32 _msgVersion,
    uint32 _msgSourceDomain,
    uint32 _msgDestinationDomain,
    uint64 _msgNonce,
    bytes32 _msgSender,
    bytes32 _msgRecipient,
    bytes32 _msgDestinationCaller,
    bytes memory _msgRawBody
) internal pure returns (bytes memory)
Packs all message fields into a single bytes array using abi.encodePacked.

Format Message for Relay (V2)

Source: ~/workspace/source/src/messages/v2/MessageV2.sol:78-101
function _formatMessageForRelay(
    uint32 _version,
    uint32 _sourceDomain,
    uint32 _destinationDomain,
    bytes32 _sender,
    bytes32 _recipient,
    bytes32 _destinationCaller,
    uint32 _minFinalityThreshold,
    bytes calldata _messageBody
) internal pure returns (bytes memory)
Formats a V2 message with empty nonce and finalityThresholdExecuted (set during attestation).

Decoding Functions

All getter functions use TypedMemView for zero-copy operations:

V1 Getters

// Extract version
function _version(bytes29 _message) internal pure returns (uint32)

// Extract source domain
function _sourceDomain(bytes29 _message) internal pure returns (uint32)

// Extract destination domain
function _destinationDomain(bytes29 _message) internal pure returns (uint32)

// Extract nonce
function _nonce(bytes29 _message) internal pure returns (uint64)

// Extract sender address
function _sender(bytes29 _message) internal pure returns (bytes32)

// Extract recipient address
function _recipient(bytes29 _message) internal pure returns (bytes32)

// Extract destination caller
function _destinationCaller(bytes29 _message) internal pure returns (bytes32)

// Extract message body
function _messageBody(bytes29 _message) internal pure returns (bytes29)

V2 Getters

V2 uses _get prefix and includes additional fields:
// All V1 fields with _get prefix
function _getVersion(bytes29 _message) internal pure returns (uint32)
function _getSourceDomain(bytes29 _message) internal pure returns (uint32)
function _getDestinationDomain(bytes29 _message) internal pure returns (uint32)
function _getNonce(bytes29 _message) internal pure returns (bytes32) // Returns bytes32
function _getSender(bytes29 _message) internal pure returns (bytes32)
function _getRecipient(bytes29 _message) internal pure returns (bytes32)
function _getDestinationCaller(bytes29 _message) internal pure returns (bytes32)
function _getMessageBody(bytes29 _message) internal pure returns (bytes29)

// V2-specific fields
function _getMinFinalityThreshold(bytes29 _message) internal pure returns (uint32)
function _getFinalityThresholdExecuted(bytes29 _message) internal pure returns (uint32)

Address Conversion

Convert between EVM addresses and bytes32:

Address to Bytes32

Source: ~/workspace/source/src/messages/Message.sol:146-148
function addressToBytes32(address addr) external pure returns (bytes32) {
    return bytes32(uint256(uint160(addr)));
}
Left-pads the address with zeros to create a bytes32 value.

Bytes32 to Address

Source: ~/workspace/source/src/messages/Message.sol:156-158
function bytes32ToAddress(bytes32 _buf) public pure returns (address) {
    return address(uint160(uint256(_buf)));
}
Different bytes32 values can map to the same address due to truncation. If uniqueness is required, validate that the first 12 bytes are zero-padding.

Validation

V1 Validation

Source: ~/workspace/source/src/messages/Message.sol:164-170
function _validateMessageFormat(bytes29 _message) internal pure {
    require(_message.isValid(), "Malformed message");
    require(
        _message.len() >= MESSAGE_BODY_INDEX,
        "Invalid message: too short"
    );
}
Ensures message is at least 116 bytes (minimum header size).

V2 Validation

Source: ~/workspace/source/src/messages/v2/MessageV2.sol:170-176
function _validateMessageFormat(bytes29 _message) internal pure {
    require(_message.isValid(), "Malformed message");
    require(
        _message.len() >= MESSAGE_BODY_INDEX,
        "Invalid message: too short"
    );
}
Ensures message is at least 148 bytes (V2 minimum header size).

TypedMemView Usage

Messages use the TypedMemView library for efficient, zero-copy memory operations:
using TypedMemView for bytes;
using TypedMemView for bytes29;
  • bytes29: Efficient memory view type that avoids copying data
  • indexUint(): Reads uint values at specific byte offsets
  • index(): Reads bytes32 values at specific byte offsets
  • slice(): Creates sub-views without copying data

Example Usage

Encoding a Message

import {Message} from "./messages/Message.sol";
import {AddressUtils} from "./messages/v2/AddressUtils.sol";

// Convert addresses to bytes32
bytes32 senderBytes32 = AddressUtils.toBytes32(msg.sender);
bytes32 recipientBytes32 = AddressUtils.toBytes32(recipientAddress);

// Format message
bytes memory message = Message._formatMessage(
    0,                          // version
    0,                          // source domain
    1,                          // destination domain
    12345,                      // nonce
    senderBytes32,              // sender
    recipientBytes32,           // recipient
    bytes32(0),                 // any destination caller
    burnMessageBytes            // message body
);

Decoding a Message

import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol";
import {Message} from "./messages/Message.sol";

using TypedMemView for bytes;
using Message for bytes29;

// Convert to TypedMemView
bytes29 messageView = receivedMessage.ref(0);

// Validate format
messageView._validateMessageFormat();

// Extract fields
uint32 version = messageView._version();
uint32 sourceDomain = messageView._sourceDomain();
bytes32 sender = messageView._sender();
bytes29 body = messageView._messageBody();

// Convert bytes32 back to address
address senderAddress = Message.bytes32ToAddress(sender);

See Also

Build docs developers (and LLMs) love