Skip to main content

Overview

The IRelayer interface defines the contract for sending cross-chain messages from the source domain to destination domains. It manages message formatting, nonce assignment, and event emission to enable off-chain attestation and delivery.

Interface Definition

IRelayer (V1)

interface IRelayer {
    function sendMessage(
        uint32 destinationDomain,
        bytes32 recipient,
        bytes calldata messageBody
    ) external returns (uint64);

    function sendMessageWithCaller(
        uint32 destinationDomain,
        bytes32 recipient,
        bytes32 destinationCaller,
        bytes calldata messageBody
    ) external returns (uint64);

    function replaceMessage(
        bytes calldata originalMessage,
        bytes calldata originalAttestation,
        bytes calldata newMessageBody,
        bytes32 newDestinationCaller
    ) external;
}
Source: src/interfaces/IRelayer.sol:22

IRelayerV2

V2 simplifies the interface with a single unified send function that includes finality parameters:
interface IRelayerV2 {
    function sendMessage(
        uint32 destinationDomain,
        bytes32 recipient,
        bytes32 destinationCaller,
        uint32 minFinalityThreshold,
        bytes calldata messageBody
    ) external;
}
Source: src/interfaces/v2/IRelayerV2.sol:24

Functions

sendMessage (V1)

function sendMessage(
    uint32 destinationDomain,
    bytes32 recipient,
    bytes calldata messageBody
) external returns (uint64);
Sends an outgoing message to the destination domain, allowing any caller to deliver it. Parameters:
  • destinationDomain - The domain ID of the destination chain
  • recipient - The address of the message recipient on the destination domain (as bytes32)
  • messageBody - The raw bytes content of the message payload
Returns:
  • The nonce reserved for this message
Behavior:
  • Increments the nonce counter
  • Formats the complete message with header
  • Emits MessageSent event for attesters to observe
  • Any address can call receiveMessage on the destination

sendMessageWithCaller (V1)

function sendMessageWithCaller(
    uint32 destinationDomain,
    bytes32 recipient,
    bytes32 destinationCaller,
    bytes calldata messageBody
) external returns (uint64);
Sends an outgoing message with a restricted destination caller. Parameters:
  • destinationDomain - The domain ID of the destination chain
  • recipient - The address of the message recipient (as bytes32)
  • destinationCaller - The only address allowed to deliver this message (as bytes32)
  • messageBody - The raw bytes content of the message
Returns:
  • The nonce reserved for this message
Warning: If destinationCaller does not represent a valid address, the message cannot be delivered on the destination domain. Use the standard sendMessage() when a specific caller is not required.

replaceMessage (V1)

function replaceMessage(
    bytes calldata originalMessage,
    bytes calldata originalAttestation,
    bytes calldata newMessageBody,
    bytes32 newDestinationCaller
) external;
Replaces a previously sent message with a new message body and/or destination caller. Parameters:
  • originalMessage - The original message bytes to replace
  • originalAttestation - Valid attestation of the original message
  • newMessageBody - The new message body content
  • newDestinationCaller - The new destination caller (or bytes32(0) for any caller)
Requirements:
  • originalAttestation must be a valid attestation of originalMessage
  • Original message must not have been received yet on destination
  • Replacement reuses the original message’s nonce
Use Cases:
  • Update recipient address before message is delivered
  • Change destination caller permissions
  • Modify message content while preserving nonce

sendMessage (V2)

function sendMessage(
    uint32 destinationDomain,
    bytes32 recipient,
    bytes32 destinationCaller,
    uint32 minFinalityThreshold,
    bytes calldata messageBody
) external;
Sends an outgoing message with finality requirements. Parameters:
  • destinationDomain - The domain ID of the destination chain
  • recipient - The message recipient address (as bytes32)
  • destinationCaller - Allowed caller on destination (bytes32(0) for any caller)
  • minFinalityThreshold - Minimum finality threshold for attestation
  • messageBody - The message payload
Warning: If destinationCaller is not a valid address as bytes32, the message cannot be delivered. Use bytes32(0) to allow any caller.

Relayer Role in Message Delivery

Message Lifecycle

┌──────────────┐
│ Source Chain │
└──────┬───────┘

       │ 1. sendMessage()

┌─────────────────┐
│    IRelayer     │
│(MessageTransmitter)
└────────┬────────┘
         │ 2. Emit MessageSent event


┌─────────────────┐
│  Circle API     │
│  (Attesters)    │
└────────┬────────┘
         │ 3. Observe event
         │ 4. Generate attestation

┌─────────────────┐
│  Relayer/User   │
│  (Off-chain)    │
└────────┬────────┘
         │ 5. Fetch attestation
         │ 6. Call receiveMessage()

┌─────────────────┐
│ Destination     │
│     Chain       │
└─────────────────┘

Relayer Responsibilities

  1. Message Observation
    • Monitor MessageSent events on source chains
    • Track messages that need delivery
  2. Attestation Retrieval
    • Query Circle’s attestation API
    • Wait for attestation to be available
    • Verify attestation validity
  3. Message Delivery
    • Call receiveMessage() on destination chain
    • Include message and attestation as parameters
    • Pay gas costs for destination transaction
  4. Status Tracking
    • Monitor delivery success/failure
    • Handle retries if needed
    • Track delivery receipts

MessageSent Event

event MessageSent(
    bytes message
);
Emitted when a message is sent, containing the complete formatted message:
// Message structure in event
struct Message {
    uint32 version;
    uint32 sourceDomain;
    uint32 destinationDomain;
    uint64 nonce;
    bytes32 sender;
    bytes32 recipient;
    bytes32 destinationCaller;
    bytes messageBody;
}
Relayers parse this event to:
  • Extract message hash for attestation API
  • Get destination domain for routing
  • Identify recipient and caller restrictions

Fee Handling in V2

Relayer Fee Model

V2 introduces built-in support for relayer fees through multi-recipient minting:
// On source chain - user pays amount + fee
uint256 userAmount = 100e6;  // 100 USDC to recipient
uint256 relayerFee = 5e6;    // 5 USDC to relayer

// Burn total amount
tokenMessenger.depositForBurn(
    userAmount + relayerFee,
    destinationDomain,
    recipientBytes32,
    usdcAddress
);

// Message body encodes split
bytes memory messageBody = encodeMessageWithFee(
    recipient,
    userAmount,
    relayerAddress,
    relayerFee
);

relayer.sendMessage(
    destinationDomain,
    tokenMessengerBytes32,
    bytes32(0),  // Any caller
    2000,        // Finalized threshold
    messageBody
);

Fee Distribution on Destination

// TokenMessengerV2 handles split
function handleReceiveMessage(
    uint32 sourceDomain,
    bytes32 sender,
    bytes calldata messageBody
) external returns (bool) {
    // Decode message with fee split
    (address recipient, uint256 amount, address relayer, uint256 fee) = 
        _decodeMessageWithFee(messageBody);
    
    // Mint to both recipients atomically
    tokenMinterV2.mint(
        sourceDomain,
        burnToken,
        recipient,  // User receives 100 USDC
        relayer,    // Relayer receives 5 USDC
        amount,
        fee
    );
    
    return true;
}

Dynamic Fee Calculation

Relayers can implement dynamic fee models:
function calculateRelayerFee(
    uint32 destDomain,
    uint256 amount,
    uint32 priorityLevel
) external view returns (uint256 fee) {
    // Base fee by destination
    uint256 baseFee = destinationBaseFees[destDomain];
    
    // Percentage fee
    uint256 percentFee = (amount * feePercent) / 10000;
    
    // Priority multiplier
    uint256 priorityMultiplier = priorityLevel > 0 ? 2 : 1;
    
    return (baseFee + percentFee) * priorityMultiplier;
}

Fee Payment Options

Option 1: Pre-paid (V1 & V2)
// User pays relayer off-chain (fiat, other tokens)
// Relayer delivers message for free
relayer.sendMessage(destDomain, recipient, messageBody);
Option 2: Fee-on-delivery (V2)
// User includes fee in burn amount
// Relayer receives payment on destination
tokenMessengerV2.depositForBurnWithFee(
    amount,
    fee,
    destDomain,
    recipient,
    relayerAddress,
    burnToken
);
Option 3: Sponsored (V1 & V2)
// Application sponsors user transactions
// Application acts as relayer
myDApp.sponsoredTransfer(user, amount, destDomain);

Usage Examples

Basic Message Send (V1)

// Send USDC burn message
IRelayer relayer = IRelayer(messageTransmitter);

bytes memory burnMessage = encodeBurnMessage(
    burnToken,
    recipient,
    amount,
    sender
);

uint64 nonce = relayer.sendMessage(
    0,           // Ethereum destination
    tokenMessengerBytes32,
    burnMessage
);

Restricted Caller (V1)

// Only specific relayer can deliver
uint64 nonce = relayer.sendMessageWithCaller(
    0,           // Ethereum destination
    tokenMessengerBytes32,
    authorizedRelayerBytes32,
    burnMessage
);

Message Replacement (V1)

// Change recipient before delivery
relayer.replaceMessage(
    originalMessage,
    originalAttestation,
    newBurnMessageWithUpdatedRecipient,
    bytes32(0)  // Keep caller restriction unchanged
);

Finality-Aware Send (V2)

// Require high finality for large transfer
relayer.sendMessage(
    0,           // Ethereum destination
    recipient,
    bytes32(0),  // Any caller
    2500,        // High finality threshold
    messageBody
);

// Allow fast delivery for small transfer
relayer.sendMessage(
    0,
    recipient,
    bytes32(0),
    1000,        // Lower finality threshold
    messageBody
);

Security Considerations

Nonce Management

  • Each message receives a unique, incrementing nonce
  • Nonces prevent replay attacks
  • Nonce is included in message hash for attestation

Destination Caller Validation

// Validate caller if specified
if (destinationCaller != bytes32(0)) {
    require(
        destinationCaller == addressToBytes32(msg.sender),
        "Unauthorized caller"
    );
}

Message Immutability

  • Once attested, message content is cryptographically sealed
  • Replacement creates new attestation for same nonce
  • First delivered message at a nonce wins

Domain Validation

  • Destination domain must be registered and supported
  • Source domain is automatically set to local domain
  • Cross-domain validation prevents misrouting

Build docs developers (and LLMs) love