Skip to main content

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:
  1. Verifies the message format and structure
  2. Validates the attestation signature against trusted attesters
  3. Checks that the message hasn’t been used before (nonce uniqueness)
  4. Verifies the destination domain matches the current chain
  5. 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:
  1. Message hash is computed from the message bytes
  2. Signature is verified against trusted attester public keys
  3. Attestation proves the message was observed on the source chain
  4. 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;
    }
}

Build docs developers (and LLMs) love