Skip to main content

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