Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 114 additions & 10 deletions solidity/contracts/mock/MockCircleMessageTransmitter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,24 @@ pragma solidity ^0.8.13;

import {IMessageTransmitter} from "../interfaces/cctp/IMessageTransmitter.sol";
import {IMessageTransmitterV2} from "../interfaces/cctp/IMessageTransmitterV2.sol";
import {IMessageHandler} from "../interfaces/cctp/IMessageHandler.sol";
import {IMessageHandlerV2} from "../interfaces/cctp/IMessageHandlerV2.sol";
import {MockToken} from "./MockToken.sol";
import {TypedMemView} from "../libs/TypedMemView.sol";
import {CctpMessageV1} from "../libs/CctpMessageV1.sol";
import {CctpMessageV2} from "../libs/CctpMessageV2.sol";
import {TypeCasts} from "../libs/TypeCasts.sol";

contract MockCircleMessageTransmitter is
IMessageTransmitter,
IMessageTransmitterV2
{
using TypedMemView for bytes;
using TypedMemView for bytes29;
using CctpMessageV1 for bytes29;
using CctpMessageV2 for bytes29;
using TypeCasts for address;

mapping(bytes32 => bool) processedNonces;
MockToken token;
uint32 public version;
Expand All @@ -26,10 +38,66 @@ contract MockCircleMessageTransmitter is
}

function receiveMessage(
bytes memory,
bytes memory message,
bytes calldata
) external pure returns (bool success) {
success = true;
) external returns (bool success) {
bytes29 cctpMessage = TypedMemView.ref(message, 0);

// Extract nonce and source domain to check if message was already processed
uint32 sourceDomain;
bytes32 nonceId;
if (version == 0) {
sourceDomain = cctpMessage._sourceDomain();
uint64 nonce = cctpMessage._nonce();
nonceId = hashSourceAndNonce(sourceDomain, nonce);
} else {
sourceDomain = cctpMessage._getSourceDomain();
bytes32 nonce = cctpMessage._getNonce();
// For V2, use the nonce directly as the nonceId (it's already a bytes32)
nonceId = keccak256(abi.encodePacked(sourceDomain, nonce));
}

require(!processedNonces[nonceId], "Message already processed");
processedNonces[nonceId] = true;

// Extract recipient based on version
address recipient;
bytes32 sender;
bytes memory messageBody;

if (version == 0) {
// V1
recipient = _bytes32ToAddress(cctpMessage._recipient());
sender = cctpMessage._sender();
messageBody = cctpMessage._messageBody().clone();
} else {
// V2
recipient = _bytes32ToAddress(cctpMessage._getRecipient());
sender = cctpMessage._getSender();
messageBody = cctpMessage._getMessageBody().clone();
}

if (version == 0) {
// V1: Call handleReceiveMessage
success = IMessageHandler(recipient).handleReceiveMessage(
sourceDomain,
sender,
messageBody
);
} else {
// V2: Call handleReceiveUnfinalizedMessage
success = IMessageHandlerV2(recipient)
.handleReceiveUnfinalizedMessage(
sourceDomain,
sender,
1000, // mock finality threshold
messageBody
);
}
}

function _bytes32ToAddress(bytes32 _buf) internal pure returns (address) {
return address(uint160(uint256(_buf)));
}

function hashSourceAndNonce(
Expand Down Expand Up @@ -70,11 +138,36 @@ contract MockCircleMessageTransmitter is
}

function sendMessage(
uint32,
bytes32,
bytes calldata message
uint32 destinationDomain,
bytes32 recipient,
bytes calldata messageBody
) public returns (uint64) {
emit MessageSent(message);
// Format a complete CCTP message for the event based on version
bytes memory cctpMessage;
if (version == 0) {
cctpMessage = CctpMessageV1._formatMessage(
version,
0, // sourceDomain (mock localDomain returns 0)
destinationDomain,
0, // nonce
address(this).addressToBytes32(),
recipient,
bytes32(0), // destinationCaller (anyone can relay)
messageBody
);
} else {
cctpMessage = CctpMessageV2._formatMessageForRelay(
version,
0, // sourceDomain (mock localDomain returns 0)
destinationDomain,
address(this).addressToBytes32(),
recipient,
bytes32(0), // destinationCaller (anyone can relay)
1000, // mock finality threshold
messageBody
);
}
emit MessageSent(cctpMessage);
return 0;
}

Expand All @@ -90,10 +183,21 @@ contract MockCircleMessageTransmitter is
function sendMessage(
uint32 destinationDomain,
bytes32 recipient,
bytes32,
uint32,
bytes32 destinationCaller,
uint32 minFinalityThreshold,
bytes calldata messageBody
) external {
sendMessage(destinationDomain, recipient, messageBody);
// V2 sendMessage: format a complete CCTP V2 message
bytes memory cctpMessage = CctpMessageV2._formatMessageForRelay(
version,
0, // sourceDomain (mock localDomain returns 0)
destinationDomain,
address(this).addressToBytes32(),
recipient,
destinationCaller,
minFinalityThreshold,
messageBody
);
emit MessageSent(cctpMessage);
}
}
37 changes: 36 additions & 1 deletion solidity/contracts/mock/MockCircleTokenMessenger.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@ pragma solidity ^0.8.13;

import {ITokenMessenger, ITokenMessengerV1} from "../interfaces/cctp/ITokenMessenger.sol";
import {ITokenMessengerV2} from "../interfaces/cctp/ITokenMessengerV2.sol";
import {IMessageHandler} from "../interfaces/cctp/IMessageHandler.sol";
import {IMessageHandlerV2} from "../interfaces/cctp/IMessageHandlerV2.sol";
import {MockToken} from "./MockToken.sol";

contract MockCircleTokenMessenger is ITokenMessengerV1, ITokenMessengerV2 {
contract MockCircleTokenMessenger is
ITokenMessengerV1,
ITokenMessengerV2,
IMessageHandler,
IMessageHandlerV2
{
uint64 public nextNonce = 0;
MockToken token;
uint32 public version;
Expand Down Expand Up @@ -56,4 +63,32 @@ contract MockCircleTokenMessenger is ITokenMessengerV1, ITokenMessengerV2 {
) external {
depositForBurn(_amount, 0, 0, _burnToken);
}

// V1 handler
function handleReceiveMessage(
uint32,
bytes32,
bytes calldata
) external pure override returns (bool) {
return true;
}

// V2 handlers
function handleReceiveFinalizedMessage(
uint32,
bytes32,
uint32,
bytes calldata
) external pure override returns (bool) {
return true;
}

function handleReceiveUnfinalizedMessage(
uint32,
bytes32,
uint32,
bytes calldata
) external pure override returns (bool) {
return true;
}
}
101 changes: 101 additions & 0 deletions solidity/contracts/token/CCTP.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,104 @@ flowchart LR
class MT_O,MT_D,TM_O,TM_D,Iris,USDC_O,USDC_D cctp
class M_O,M_D,Relayer hyperlane
```

## Destination Chain Sequence Diagrams

### 1. Token Message with Hyperlane Relayer

```mermaid
sequenceDiagram
participant HR as Hyperlane Relayer
participant Mailbox as Mailbox
participant TBCCTP as TokenBridgeCctp
participant MT as MessageTransmitter
participant TM as TokenMessenger
participant USDC as USDC
participant Recipient as Recipient

HR->>Mailbox: process([burnMessage, attestation], tokenMessage)
Mailbox->>TBCCTP: verify([burnMessage, attestation], tokenMessage)
TBCCTP->>MT: receiveMessage(burnMessage, attestation)
MT->>TM: handleReceiveMessage(burnMessage)
TM->>USDC: mint(amount, recipient)
USDC-->>Recipient: amount transferred

Note over Mailbox: Marks message as delivered<br/>in delivered mapping
Mailbox->>TBCCTP: handle(tokenMessage)
TBCCTP-->>Recipient: emit event reflecting tokens were transferred
```

### 2. Token Message with CCTP Relayer and Hyperlane Relayer

```mermaid
sequenceDiagram
participant CR as CCTP Relayer
participant HR as Hyperlane Relayer
participant Mailbox as Mailbox
participant TBCCTP as TokenBridgeCctp
participant MT as MessageTransmitter
participant TM as TokenMessenger
participant USDC as USDC
participant Recipient as Recipient

Note over CR: CCTP Relayer submits<br/>burn message first
CR->>TBCCTP: receiveMessage(burnMessage, attestation)
TBCCTP->>MT: receiveMessage(burnMessage, attestation)
MT->>TM: handleReceiveMessage(burnMessage)
TM->>USDC: mint(amount, recipient)
USDC-->>Recipient: amount minted

Note over HR: Hyperlane Relayer delivers<br/>token message
HR->>Mailbox: process([], tokenMessage)
Mailbox->>TBCCTP: verify([], tokenMessage)
TBCCTP-xMailbox: REVERT: Burn message already processed
Note over Mailbox: Transaction reverts,<br/>handle never called
```

### 3. GMP Message with Hyperlane Relayer

```mermaid
sequenceDiagram
participant HR as Hyperlane Relayer
participant Mailbox as Mailbox
participant TBCCTP as TokenBridgeCctp (ISM)
participant MT as MessageTransmitter
participant Recipient as Recipient App

HR->>Mailbox: process([cctpMessage, attestation], hyperlaneMessage)
Mailbox->>TBCCTP: verify([cctpMessage, attestation], hyperlaneMessage)
TBCCTP->>MT: receiveMessage(cctpMessage, attestation)
MT->>TBCCTP: handleReceiveMessage(cctpMessage)
Note over TBCCTP: Verifies message ID matches

Note over Mailbox: Marks message as delivered<br/>in delivered mapping
Mailbox->>Recipient: handle(hyperlaneMessage)
Note over Recipient: Application receives message
```

### 4. GMP Message with CCTP Relayer and Hyperlane Relayer

```mermaid
sequenceDiagram
participant CR as CCTP Relayer
participant HR as Hyperlane Relayer
participant Mailbox as Mailbox
participant TBCCTP as TokenBridgeCctp (ISM)
participant MT as MessageTransmitter
participant Recipient as Recipient App

Note over CR: CCTP Relayer submits<br/>message first
CR->>TBCCTP: receiveMessage(cctpMessage, attestation)
TBCCTP->>MT: receiveMessage(cctpMessage, attestation)
MT->>TBCCTP: handleReceiveMessage(cctpMessage)
Note over TBCCTP: Stores message ID

Note over HR: Hyperlane Relayer delivers<br/>GMP message
HR->>Mailbox: process([], hyperlaneMessage)
Mailbox->>TBCCTP: verify([], hyperlaneMessage)
Note over TBCCTP: CCTP message already processed,<br/>verifies message ID

Note over Mailbox: Marks message as delivered<br/>in delivered mapping
Mailbox->>Recipient: handle(hyperlaneMessage)
Note over Recipient: Application receives message
```
Loading
Loading