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
ImplementIMessageHandler to receive cross-chain messages:
interface IMessageHandler {
function handleReceiveMessage(
uint32 sourceDomain,
bytes32 sender,
bytes calldata messageBody
) external returns (bool);
}
sourceDomain: The source chain’s domain IDsender: The message sender address as bytes32messageBody: The message payload
trueif message handling succeeded
IReceiver
TheIReceiver interface is implemented by MessageTransmitter:
interface IReceiver {
function receiveMessage(
bytes calldata message,
bytes calldata signature
) external returns (bool success);
}
IRelayer
TheIRelayer 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
TheCCTPHookWrapper 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);
}
}