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 IReceiver interface defines the entry point for receiving cross-chain messages on the destination domain. It validates message signatures and attestations before forwarding the message to the appropriate message handler for processing.
Interface Definition
IReceiver (V1)
interface IReceiver {
function receiveMessage(bytes calldata message, bytes calldata signature)
external
returns (bool success);
}
Source: src/interfaces/IReceiver.sol:22
IReceiverV2
V2 maintains the same interface but adds support for finality-aware message handling:
interface IReceiverV2 is IReceiver {
// Inherits receiveMessage from IReceiver
// Internal handling routes to IMessageHandlerV2 based on finality
}
Source: src/interfaces/v2/IReceiverV2.sol:27
Functions
receiveMessage
function receiveMessage(bytes calldata message, bytes calldata signature)
external
returns (bool success);
Receives an incoming cross-chain message, validates the signature/attestation, and forwards it to the message handler.
Parameters:
message - The complete message bytes containing header and body
signature - The attestation signature proving the message was validated
Returns:
success - True if the message was received and processed successfully
Message Validation:
- Verifies the message format and structure
- Validates the attestation signature against trusted attesters
- Checks that the message hasn’t been used before (nonce uniqueness)
- Verifies the destination domain matches the current chain
- Forwards to the appropriate message handler
How Receivers Work with CCTP
Message Flow
┌─────────────┐
│ Source Chain│
│ (Relayer) │
└──────┬──────┘
│ 1. Call receiveMessage(message, attestation)
▼
┌─────────────────┐
│ IReceiver │
│ (MessageReceiver)│
└────────┬────────┘
│ 2. Validate signature
│ 3. Parse message header
│ 4. Check nonce uniqueness
▼
┌─────────────────┐
│ IMessageHandler │
│ (TokenMessenger)│
└─────────────────┘
│ 5. Process message body
│ 6. Execute action (mint tokens, etc.)
▼
┌─────────────────┐
│ IReceiver │
│ (Optional) │
└─────────────────┘
│ 7. Execute receiver hook (if specified)
└──────────────────────────────┐
▼
┌──────────────────┐
│ End user contract│
│ onReceive() hook │
└──────────────────┘
Message Structure
The message parameter contains:
// Message header (fixed size)
- version (4 bytes)
- sourceDomain (4 bytes)
- destinationDomain (4 bytes)
- nonce (8 bytes)
- sender (32 bytes)
- recipient (32 bytes)
- destinationCaller (32 bytes)
// Message body (variable size)
- Application-specific payload
Signature Validation
The receiver validates signatures from Circle’s attestation service:
- Message hash is computed from the message bytes
- Signature is verified against trusted attester public keys
- Attestation proves the message was observed on the source chain
- Multiple attesters provide redundancy and security
Implementation in CCTP
The MessageTransmitter contract implements IReceiver:
contract MessageTransmitter is IReceiver {
mapping(bytes32 => uint256) public usedNonces;
mapping(address => bool) public attesters;
function receiveMessage(
bytes calldata message,
bytes calldata attestation
) external returns (bool) {
// Parse message header
bytes29 _msg = message.ref(0);
_msg.validateMessageFormat();
// Validate destination domain
require(
_msg.destinationDomain() == localDomain,
"Invalid destination"
);
// Verify nonce not used
bytes32 nonceId = _getMessageNonceId(_msg);
require(usedNonces[nonceId] == 0, "Nonce already used");
// Verify attestation signature
require(_verifyAttestation(message, attestation), "Invalid attestation");
// Mark nonce as used
usedNonces[nonceId] = 1;
// Forward to message handler
address handler = _msg.recipient();
bytes calldata messageBody = _msg.messageBody();
bool success = IMessageHandler(handler).handleReceiveMessage(
_msg.sourceDomain(),
_msg.sender(),
messageBody
);
return success;
}
}
Destination Caller Restriction
Messages can optionally specify a destinationCaller:
// In receiveMessage implementation
if (destinationCaller != bytes32(0)) {
require(
msg.sender == bytes32ToAddress(destinationCaller),
"Caller not authorized"
);
}
Use Cases:
- Restrict message delivery to specific relayers
- Implement custom relaying logic
- Prevent front-running of message delivery
- Control who can trigger message execution
V2 Hook Execution Pattern
Version 2 introduces enhanced receiver hooks with finality awareness:
Finality-Based Routing
IReceiverV2 routes messages to different handlers based on finality:
contract MessageTransmitterV2 is IReceiverV2 {
function receiveMessage(
bytes calldata message,
bytes calldata attestation
) external returns (bool) {
// Standard validation...
// Extract finality threshold from attestation
uint32 finalityThreshold = _extractFinalityThreshold(attestation);
// Route based on finality
if (finalityThreshold >= 2000) {
// Finalized message - full confidence
return IMessageHandlerV2(handler).handleReceiveFinalizedMessage(
sourceDomain,
sender,
finalityThreshold,
messageBody
);
} else {
// Unfinalized message - probabilistic finality
return IMessageHandlerV2(handler).handleReceiveUnfinalizedMessage(
sourceDomain,
sender,
finalityThreshold,
messageBody
);
}
}
}
Receiver Hook Integration
V2 enables end-user contracts to implement receiver hooks:
// User contract implements IReceiver for hooks
contract MyDApp is IReceiver {
function onReceive(
uint32 sourceDomain,
bytes32 sender,
bytes calldata data
) external returns (bool) {
// Custom logic after receiving tokens
// e.g., stake tokens, execute trade, etc.
return true;
}
}
// Specify receiver in mint recipient field
bytes32 receiver = addressToBytes32(myDAppAddress);
tokenMessenger.depositForBurn(
amount,
destinationDomain,
receiver, // Will trigger onReceive hook
burnToken
);
Finality-Aware Hooks
Applications can implement different behavior based on finality:
contract FinancialDApp is IReceiverV2 {
function receiveMessage(
bytes calldata message,
bytes calldata attestation
) external returns (bool) {
uint32 finality = _extractFinality(attestation);
if (finality >= 2000) {
// High confidence - execute immediately
_executeTradeImmediately(message);
} else {
// Lower confidence - add to pending
_addToPendingQueue(message, finality);
}
return true;
}
}
Security Considerations
Nonce Management
- Each message has a unique nonce to prevent replay attacks
- Nonces are marked as used after successful processing
- Cannot reuse the same message on the destination chain
Attestation Validation
- Only signatures from registered attesters are accepted
- Attesters are managed by Circle and rotated as needed
- Multiple attesters provide redundancy
Destination Validation
- Messages must be intended for the current domain
- Prevents messages from being replayed on wrong chains
Handler Security
- Message handlers should validate the caller is the registered receiver
- Handlers should validate message format and content
- Handlers should implement proper access controls
Integration Example
Relayer Integration
// Relayer observes message on source chain
function relayMessage(
uint32 sourceDomain,
uint32 destDomain,
bytes memory message
) external {
// Get attestation from Circle API
bytes memory attestation = _getAttestation(message);
// Call receiver on destination chain
IReceiver receiver = IReceiver(destinationReceiver);
bool success = receiver.receiveMessage(message, attestation);
require(success, "Message delivery failed");
}
Handler Integration
contract MyMessageHandler is IMessageHandler {
address public immutable receiver;
modifier onlyReceiver() {
require(msg.sender == receiver, "Only receiver");
_;
}
function handleReceiveMessage(
uint32 sourceDomain,
bytes32 sender,
bytes calldata messageBody
) external override onlyReceiver returns (bool) {
// Process message
return true;
}
}