Documentation Index
Fetch the complete documentation index at: https://mintlify.com/circlefin/evm-cctp-contracts/llms.txt
Use this file to discover all available pages before exploring further.
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
-
Message Observation
- Monitor
MessageSent events on source chains
- Track messages that need delivery
-
Attestation Retrieval
- Query Circle’s attestation API
- Wait for attestation to be available
- Verify attestation validity
-
Message Delivery
- Call
receiveMessage() on destination chain
- Include message and attestation as parameters
- Pay gas costs for destination transaction
-
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