Skip to main content

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

This guide demonstrates how to integrate CCTP into your smart contracts using hooks, message handlers, and relayers.

Core Interfaces

CCTP provides several interfaces for integration:

IMessageHandler

Implement IMessageHandler to receive cross-chain messages:
interface IMessageHandler {
    function handleReceiveMessage(
        uint32 sourceDomain,
        bytes32 sender,
        bytes calldata messageBody
    ) external returns (bool);
}
Parameters:
  • sourceDomain: The source chain’s domain ID
  • sender: The message sender address as bytes32
  • messageBody: The message payload
Returns:
  • true if message handling succeeded

IReceiver

The IReceiver interface is implemented by MessageTransmitter:
interface IReceiver {
    function receiveMessage(
        bytes calldata message,
        bytes calldata signature
    ) external returns (bool success);
}
This validates incoming messages and forwards them to the appropriate handler.

IRelayer

The IRelayer interface is implemented by MessageTransmitter for sending messages:
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);
}

Implementation Examples

Basic Message Handler

Implement a simple contract that receives cross-chain messages:
pragma solidity 0.7.6;

import "../interfaces/IMessageHandler.sol";
import "../roles/Ownable2Step.sol";

contract MyMessageHandler is IMessageHandler, Ownable2Step {
    // Address of the MessageTransmitter
    address public immutable messageTransmitter;
    
    // Mapping of authorized senders by domain
    mapping(uint32 => bytes32) public authorizedSenders;
    
    event MessageReceived(
        uint32 sourceDomain,
        bytes32 sender,
        bytes messageBody
    );
    
    constructor(address _messageTransmitter) Ownable2Step() {
        require(
            _messageTransmitter != address(0),
            "Invalid transmitter address"
        );
        messageTransmitter = _messageTransmitter;
    }
    
    function handleReceiveMessage(
        uint32 sourceDomain,
        bytes32 sender,
        bytes calldata messageBody
    ) external override returns (bool) {
        // Only allow calls from MessageTransmitter
        require(
            msg.sender == messageTransmitter,
            "Unauthorized caller"
        );
        
        // Verify sender is authorized
        require(
            authorizedSenders[sourceDomain] == sender,
            "Unauthorized sender"
        );
        
        // Process message
        _processMessage(sourceDomain, sender, messageBody);
        
        emit MessageReceived(sourceDomain, sender, messageBody);
        return true;
    }
    
    function _processMessage(
        uint32 sourceDomain,
        bytes32 sender,
        bytes calldata messageBody
    ) internal virtual {
        // Implement your message processing logic
    }
    
    function setAuthorizedSender(
        uint32 domain,
        bytes32 sender
    ) external onlyOwner {
        authorizedSenders[domain] = sender;
    }
}

Cross-Chain Token Bridge

Build a custom token bridge on top of CCTP:
pragma solidity 0.7.6;

import "../interfaces/IRelayer.sol";
import "../interfaces/IMessageHandler.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract TokenBridge is IMessageHandler {
    IRelayer public immutable messageTransmitter;
    address public immutable messageHandler;
    IERC20 public immutable token;
    
    mapping(uint32 => bytes32) public remoteBridges;
    mapping(address => mapping(uint32 => uint256)) public lockedBalances;
    
    event TokensLocked(
        address indexed sender,
        uint32 destinationDomain,
        uint256 amount
    );
    
    event TokensUnlocked(
        address indexed recipient,
        uint32 sourceDomain,
        uint256 amount
    );
    
    constructor(
        address _messageTransmitter,
        address _token
    ) {
        messageTransmitter = IRelayer(_messageTransmitter);
        messageHandler = address(this);
        token = IERC20(_token);
    }
    
    function bridgeTokens(
        uint32 destinationDomain,
        address recipient,
        uint256 amount
    ) external returns (uint64) {
        // Transfer tokens from sender
        require(
            token.transferFrom(msg.sender, address(this), amount),
            "Transfer failed"
        );
        
        // Update locked balance
        lockedBalances[msg.sender][destinationDomain] += amount;
        
        // Encode message
        bytes memory messageBody = abi.encode(
            recipient,
            amount
        );
        
        // Send cross-chain message
        uint64 nonce = messageTransmitter.sendMessage(
            destinationDomain,
            remoteBridges[destinationDomain],
            messageBody
        );
        
        emit TokensLocked(msg.sender, destinationDomain, amount);
        return nonce;
    }
    
    function handleReceiveMessage(
        uint32 sourceDomain,
        bytes32 sender,
        bytes calldata messageBody
    ) external override returns (bool) {
        require(
            msg.sender == address(messageTransmitter),
            "Unauthorized"
        );
        
        require(
            sender == remoteBridges[sourceDomain],
            "Invalid sender"
        );
        
        // Decode message
        (address recipient, uint256 amount) = abi.decode(
            messageBody,
            (address, uint256)
        );
        
        // Transfer tokens to recipient
        require(
            token.transfer(recipient, amount),
            "Transfer failed"
        );
        
        emit TokensUnlocked(recipient, sourceDomain, amount);
        return true;
    }
    
    function setRemoteBridge(
        uint32 domain,
        bytes32 bridge
    ) external {
        remoteBridges[domain] = bridge;
    }
}

Hook Execution Pattern

CCTP V2 supports hooks that execute custom logic after message receipt.

Hook Data Format

Field           Bytes    Type       Index
target          20       address    0
hookCallData    dynamic  bytes      20

CCTPHookWrapper Example

The CCTPHookWrapper demonstrates hook integration:
pragma solidity 0.7.6;

import {IReceiverV2} from "../interfaces/v2/IReceiverV2.sol";
import {Ownable2Step} from "../roles/Ownable2Step.sol";

contract CCTPHookWrapper is Ownable2Step {
    IReceiverV2 public immutable messageTransmitter;
    
    constructor(address _messageTransmitter) Ownable2Step() {
        require(
            _messageTransmitter != address(0),
            "Invalid address"
        );
        messageTransmitter = IReceiverV2(_messageTransmitter);
    }
    
    function relay(
        bytes calldata message,
        bytes calldata attestation
    )
        external
        returns (
            bool relaySuccess,
            bool hookSuccess,
            bytes memory hookReturnData
        )
    {
        _checkOwner();
        
        // Relay message to MessageTransmitter
        relaySuccess = messageTransmitter.receiveMessage(
            message,
            attestation
        );
        require(relaySuccess, "Receive failed");
        
        // Extract and execute hook if present
        bytes memory hookData = _extractHookData(message);
        if (hookData.length >= 20) {
            address target = _bytesToAddress(hookData);
            bytes memory callData = _slice(hookData, 20);
            
            (hookSuccess, hookReturnData) = target.call(callData);
        }
    }
    
    function _extractHookData(
        bytes calldata message
    ) internal pure returns (bytes memory) {
        // Parse message and extract hook data
        // Implementation details...
    }
}

Custom Hook Handler

Implement a contract that receives hook callbacks:
pragma solidity 0.7.6;

contract MyHookHandler {
    event HookExecuted(
        address indexed caller,
        uint256 amount,
        bytes data
    );
    
    function handleHook(
        uint256 amount,
        address recipient,
        bytes calldata data
    ) external {
        // Verify caller
        require(
            msg.sender == TRUSTED_HOOK_WRAPPER,
            "Unauthorized"
        );
        
        // Execute custom logic
        _processHook(amount, recipient, data);
        
        emit HookExecuted(msg.sender, amount, data);
    }
    
    function _processHook(
        uint256 amount,
        address recipient,
        bytes calldata data
    ) internal {
        // Your custom hook logic
    }
}

Integration Best Practices

1. Access Control

Always verify the message sender:
function handleReceiveMessage(
    uint32 sourceDomain,
    bytes32 sender,
    bytes calldata messageBody
) external override returns (bool) {
    // Only accept calls from MessageTransmitter
    require(
        msg.sender == address(messageTransmitter),
        "Unauthorized caller"
    );
    
    // Only accept messages from authorized remote contracts
    require(
        authorizedSenders[sourceDomain] == sender,
        "Unauthorized sender"
    );
    
    // Process message
}

2. Message Validation

Validate message format and data:
function _validateMessage(
    bytes calldata messageBody
) internal pure {
    require(messageBody.length >= 32, "Invalid length");
    
    (address recipient, uint256 amount) = abi.decode(
        messageBody,
        (address, uint256)
    );
    
    require(recipient != address(0), "Invalid recipient");
    require(amount > 0, "Invalid amount");
}

3. Reentrancy Protection

Protect against reentrancy attacks:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract MyHandler is IMessageHandler, ReentrancyGuard {
    function handleReceiveMessage(
        uint32 sourceDomain,
        bytes32 sender,
        bytes calldata messageBody
    ) external override nonReentrant returns (bool) {
        // Safe from reentrancy
    }
}

4. Error Handling

Handle errors gracefully:
function handleReceiveMessage(
    uint32 sourceDomain,
    bytes32 sender,
    bytes calldata messageBody
) external override returns (bool) {
    try this._processMessage(messageBody) {
        return true;
    } catch Error(string memory reason) {
        emit MessageFailed(sourceDomain, sender, reason);
        return false;
    } catch {
        emit MessageFailed(sourceDomain, sender, "Unknown error");
        return false;
    }
}

5. Gas Optimization

Optimize gas usage:
// Use immutable for constants
address public immutable messageTransmitter;

// Pack structs efficiently
struct Transfer {
    uint64 nonce;        // 8 bytes
    uint32 domain;       // 4 bytes
    uint96 amount;       // 12 bytes
    address recipient;   // 20 bytes
}  // Total: 44 bytes (fits in 2 slots)

// Use unchecked for safe operations
unchecked {
    counter += 1;
}

Automated Relaying

Implement automated message relaying:
const { Web3 } = require('web3');

class CCTPRelayer {
    constructor(sourceRpc, destRpc) {
        this.sourceWeb3 = new Web3(sourceRpc);
        this.destWeb3 = new Web3(destRpc);
    }
    
    async monitorAndRelay() {
        // Subscribe to MessageSent events
        this.sourceWeb3.eth.subscribe('logs', {
            address: MESSAGE_TRANSMITTER_ADDRESS,
            topics: [web3.utils.keccak256('MessageSent(bytes)')]
        }, async (error, log) => {
            if (error) {
                console.error('Error:', error);
                return;
            }
            
            await this.relayMessage(log);
        });
    }
    
    async relayMessage(log) {
        // Extract message bytes
        const messageBytes = this.sourceWeb3.eth.abi
            .decodeParameters(['bytes'], log.data)[0];
        
        const messageHash = this.sourceWeb3.utils
            .keccak256(messageBytes);
        
        // Fetch attestation
        const attestation = await this.fetchAttestation(messageHash);
        
        // Relay to destination
        await this.destMessageTransmitter.methods
            .receiveMessage(messageBytes, attestation)
            .send();
        
        console.log(`Relayed message: ${messageHash}`);
    }
    
    async fetchAttestation(messageHash) {
        let response = { status: 'pending' };
        
        while (response.status !== 'complete') {
            const res = await fetch(
                `https://iris-api-sandbox.circle.com/attestations/${messageHash}`
            );
            response = await res.json();
            await new Promise(r => setTimeout(r, 2000));
        }
        
        return response.attestation;
    }
}

// Usage
const relayer = new CCTPRelayer(ETH_RPC, AVAX_RPC);
relayer.monitorAndRelay();

Testing Integration

Test your integration thoroughly:
contract MyHandlerTest is Test {
    MyMessageHandler handler;
    MockMessageTransmitter transmitter;
    
    function setUp() public {
        transmitter = new MockMessageTransmitter();
        handler = new MyMessageHandler(address(transmitter));
    }
    
    function testHandleMessage() public {
        uint32 sourceDomain = 0;
        bytes32 sender = bytes32(uint256(uint160(address(this))));
        bytes memory messageBody = abi.encode(
            address(0x123),
            1000e6
        );
        
        // Set authorized sender
        handler.setAuthorizedSender(sourceDomain, sender);
        
        // Simulate message receipt
        vm.prank(address(transmitter));
        bool success = handler.handleReceiveMessage(
            sourceDomain,
            sender,
            messageBody
        );
        
        assertTrue(success);
    }
}

Next Steps

Build docs developers (and LLMs) love