Skip to content

Commit e7e9cd8

Browse files
authored
fix: prevent CCTP v2 nonce manipulation (#7209)
1 parent a62b662 commit e7e9cd8

File tree

7 files changed

+880
-252
lines changed

7 files changed

+880
-252
lines changed

solidity/contracts/mock/MockCircleMessageTransmitter.sol

Lines changed: 114 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,24 @@ pragma solidity ^0.8.13;
33

44
import {IMessageTransmitter} from "../interfaces/cctp/IMessageTransmitter.sol";
55
import {IMessageTransmitterV2} from "../interfaces/cctp/IMessageTransmitterV2.sol";
6+
import {IMessageHandler} from "../interfaces/cctp/IMessageHandler.sol";
7+
import {IMessageHandlerV2} from "../interfaces/cctp/IMessageHandlerV2.sol";
68
import {MockToken} from "./MockToken.sol";
9+
import {TypedMemView} from "../libs/TypedMemView.sol";
10+
import {CctpMessageV1} from "../libs/CctpMessageV1.sol";
11+
import {CctpMessageV2} from "../libs/CctpMessageV2.sol";
12+
import {TypeCasts} from "../libs/TypeCasts.sol";
713

814
contract MockCircleMessageTransmitter is
915
IMessageTransmitter,
1016
IMessageTransmitterV2
1117
{
18+
using TypedMemView for bytes;
19+
using TypedMemView for bytes29;
20+
using CctpMessageV1 for bytes29;
21+
using CctpMessageV2 for bytes29;
22+
using TypeCasts for address;
23+
1224
mapping(bytes32 => bool) processedNonces;
1325
MockToken token;
1426
uint32 public version;
@@ -26,10 +38,66 @@ contract MockCircleMessageTransmitter is
2638
}
2739

2840
function receiveMessage(
29-
bytes memory,
41+
bytes memory message,
3042
bytes calldata
31-
) external pure returns (bool success) {
32-
success = true;
43+
) external returns (bool success) {
44+
bytes29 cctpMessage = TypedMemView.ref(message, 0);
45+
46+
// Extract nonce and source domain to check if message was already processed
47+
uint32 sourceDomain;
48+
bytes32 nonceId;
49+
if (version == 0) {
50+
sourceDomain = cctpMessage._sourceDomain();
51+
uint64 nonce = cctpMessage._nonce();
52+
nonceId = hashSourceAndNonce(sourceDomain, nonce);
53+
} else {
54+
sourceDomain = cctpMessage._getSourceDomain();
55+
bytes32 nonce = cctpMessage._getNonce();
56+
// For V2, use the nonce directly as the nonceId (it's already a bytes32)
57+
nonceId = keccak256(abi.encodePacked(sourceDomain, nonce));
58+
}
59+
60+
require(!processedNonces[nonceId], "Message already processed");
61+
processedNonces[nonceId] = true;
62+
63+
// Extract recipient based on version
64+
address recipient;
65+
bytes32 sender;
66+
bytes memory messageBody;
67+
68+
if (version == 0) {
69+
// V1
70+
recipient = _bytes32ToAddress(cctpMessage._recipient());
71+
sender = cctpMessage._sender();
72+
messageBody = cctpMessage._messageBody().clone();
73+
} else {
74+
// V2
75+
recipient = _bytes32ToAddress(cctpMessage._getRecipient());
76+
sender = cctpMessage._getSender();
77+
messageBody = cctpMessage._getMessageBody().clone();
78+
}
79+
80+
if (version == 0) {
81+
// V1: Call handleReceiveMessage
82+
success = IMessageHandler(recipient).handleReceiveMessage(
83+
sourceDomain,
84+
sender,
85+
messageBody
86+
);
87+
} else {
88+
// V2: Call handleReceiveUnfinalizedMessage
89+
success = IMessageHandlerV2(recipient)
90+
.handleReceiveUnfinalizedMessage(
91+
sourceDomain,
92+
sender,
93+
1000, // mock finality threshold
94+
messageBody
95+
);
96+
}
97+
}
98+
99+
function _bytes32ToAddress(bytes32 _buf) internal pure returns (address) {
100+
return address(uint160(uint256(_buf)));
33101
}
34102

35103
function hashSourceAndNonce(
@@ -70,11 +138,36 @@ contract MockCircleMessageTransmitter is
70138
}
71139

72140
function sendMessage(
73-
uint32,
74-
bytes32,
75-
bytes calldata message
141+
uint32 destinationDomain,
142+
bytes32 recipient,
143+
bytes calldata messageBody
76144
) public returns (uint64) {
77-
emit MessageSent(message);
145+
// Format a complete CCTP message for the event based on version
146+
bytes memory cctpMessage;
147+
if (version == 0) {
148+
cctpMessage = CctpMessageV1._formatMessage(
149+
version,
150+
0, // sourceDomain (mock localDomain returns 0)
151+
destinationDomain,
152+
0, // nonce
153+
address(this).addressToBytes32(),
154+
recipient,
155+
bytes32(0), // destinationCaller (anyone can relay)
156+
messageBody
157+
);
158+
} else {
159+
cctpMessage = CctpMessageV2._formatMessageForRelay(
160+
version,
161+
0, // sourceDomain (mock localDomain returns 0)
162+
destinationDomain,
163+
address(this).addressToBytes32(),
164+
recipient,
165+
bytes32(0), // destinationCaller (anyone can relay)
166+
1000, // mock finality threshold
167+
messageBody
168+
);
169+
}
170+
emit MessageSent(cctpMessage);
78171
return 0;
79172
}
80173

@@ -90,10 +183,21 @@ contract MockCircleMessageTransmitter is
90183
function sendMessage(
91184
uint32 destinationDomain,
92185
bytes32 recipient,
93-
bytes32,
94-
uint32,
186+
bytes32 destinationCaller,
187+
uint32 minFinalityThreshold,
95188
bytes calldata messageBody
96189
) external {
97-
sendMessage(destinationDomain, recipient, messageBody);
190+
// V2 sendMessage: format a complete CCTP V2 message
191+
bytes memory cctpMessage = CctpMessageV2._formatMessageForRelay(
192+
version,
193+
0, // sourceDomain (mock localDomain returns 0)
194+
destinationDomain,
195+
address(this).addressToBytes32(),
196+
recipient,
197+
destinationCaller,
198+
minFinalityThreshold,
199+
messageBody
200+
);
201+
emit MessageSent(cctpMessage);
98202
}
99203
}

solidity/contracts/mock/MockCircleTokenMessenger.sol

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@ pragma solidity ^0.8.13;
33

44
import {ITokenMessenger, ITokenMessengerV1} from "../interfaces/cctp/ITokenMessenger.sol";
55
import {ITokenMessengerV2} from "../interfaces/cctp/ITokenMessengerV2.sol";
6+
import {IMessageHandler} from "../interfaces/cctp/IMessageHandler.sol";
7+
import {IMessageHandlerV2} from "../interfaces/cctp/IMessageHandlerV2.sol";
68
import {MockToken} from "./MockToken.sol";
79

8-
contract MockCircleTokenMessenger is ITokenMessengerV1, ITokenMessengerV2 {
10+
contract MockCircleTokenMessenger is
11+
ITokenMessengerV1,
12+
ITokenMessengerV2,
13+
IMessageHandler,
14+
IMessageHandlerV2
15+
{
916
uint64 public nextNonce = 0;
1017
MockToken token;
1118
uint32 public version;
@@ -56,4 +63,32 @@ contract MockCircleTokenMessenger is ITokenMessengerV1, ITokenMessengerV2 {
5663
) external {
5764
depositForBurn(_amount, 0, 0, _burnToken);
5865
}
66+
67+
// V1 handler
68+
function handleReceiveMessage(
69+
uint32,
70+
bytes32,
71+
bytes calldata
72+
) external pure override returns (bool) {
73+
return true;
74+
}
75+
76+
// V2 handlers
77+
function handleReceiveFinalizedMessage(
78+
uint32,
79+
bytes32,
80+
uint32,
81+
bytes calldata
82+
) external pure override returns (bool) {
83+
return true;
84+
}
85+
86+
function handleReceiveUnfinalizedMessage(
87+
uint32,
88+
bytes32,
89+
uint32,
90+
bytes calldata
91+
) external pure override returns (bool) {
92+
return true;
93+
}
5994
}

solidity/contracts/token/CCTP.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,104 @@ flowchart LR
107107
class MT_O,MT_D,TM_O,TM_D,Iris,USDC_O,USDC_D cctp
108108
class M_O,M_D,Relayer hyperlane
109109
```
110+
111+
## Destination Chain Sequence Diagrams
112+
113+
### 1. Token Message with Hyperlane Relayer
114+
115+
```mermaid
116+
sequenceDiagram
117+
participant HR as Hyperlane Relayer
118+
participant Mailbox as Mailbox
119+
participant TBCCTP as TokenBridgeCctp
120+
participant MT as MessageTransmitter
121+
participant TM as TokenMessenger
122+
participant USDC as USDC
123+
participant Recipient as Recipient
124+
125+
HR->>Mailbox: process([burnMessage, attestation], tokenMessage)
126+
Mailbox->>TBCCTP: verify([burnMessage, attestation], tokenMessage)
127+
TBCCTP->>MT: receiveMessage(burnMessage, attestation)
128+
MT->>TM: handleReceiveMessage(burnMessage)
129+
TM->>USDC: mint(amount, recipient)
130+
USDC-->>Recipient: amount transferred
131+
132+
Note over Mailbox: Marks message as delivered<br/>in delivered mapping
133+
Mailbox->>TBCCTP: handle(tokenMessage)
134+
TBCCTP-->>Recipient: emit event reflecting tokens were transferred
135+
```
136+
137+
### 2. Token Message with CCTP Relayer and Hyperlane Relayer
138+
139+
```mermaid
140+
sequenceDiagram
141+
participant CR as CCTP Relayer
142+
participant HR as Hyperlane Relayer
143+
participant Mailbox as Mailbox
144+
participant TBCCTP as TokenBridgeCctp
145+
participant MT as MessageTransmitter
146+
participant TM as TokenMessenger
147+
participant USDC as USDC
148+
participant Recipient as Recipient
149+
150+
Note over CR: CCTP Relayer submits<br/>burn message first
151+
CR->>TBCCTP: receiveMessage(burnMessage, attestation)
152+
TBCCTP->>MT: receiveMessage(burnMessage, attestation)
153+
MT->>TM: handleReceiveMessage(burnMessage)
154+
TM->>USDC: mint(amount, recipient)
155+
USDC-->>Recipient: amount minted
156+
157+
Note over HR: Hyperlane Relayer delivers<br/>token message
158+
HR->>Mailbox: process([], tokenMessage)
159+
Mailbox->>TBCCTP: verify([], tokenMessage)
160+
TBCCTP-xMailbox: REVERT: Burn message already processed
161+
Note over Mailbox: Transaction reverts,<br/>handle never called
162+
```
163+
164+
### 3. GMP Message with Hyperlane Relayer
165+
166+
```mermaid
167+
sequenceDiagram
168+
participant HR as Hyperlane Relayer
169+
participant Mailbox as Mailbox
170+
participant TBCCTP as TokenBridgeCctp (ISM)
171+
participant MT as MessageTransmitter
172+
participant Recipient as Recipient App
173+
174+
HR->>Mailbox: process([cctpMessage, attestation], hyperlaneMessage)
175+
Mailbox->>TBCCTP: verify([cctpMessage, attestation], hyperlaneMessage)
176+
TBCCTP->>MT: receiveMessage(cctpMessage, attestation)
177+
MT->>TBCCTP: handleReceiveMessage(cctpMessage)
178+
Note over TBCCTP: Verifies message ID matches
179+
180+
Note over Mailbox: Marks message as delivered<br/>in delivered mapping
181+
Mailbox->>Recipient: handle(hyperlaneMessage)
182+
Note over Recipient: Application receives message
183+
```
184+
185+
### 4. GMP Message with CCTP Relayer and Hyperlane Relayer
186+
187+
```mermaid
188+
sequenceDiagram
189+
participant CR as CCTP Relayer
190+
participant HR as Hyperlane Relayer
191+
participant Mailbox as Mailbox
192+
participant TBCCTP as TokenBridgeCctp (ISM)
193+
participant MT as MessageTransmitter
194+
participant Recipient as Recipient App
195+
196+
Note over CR: CCTP Relayer submits<br/>message first
197+
CR->>TBCCTP: receiveMessage(cctpMessage, attestation)
198+
TBCCTP->>MT: receiveMessage(cctpMessage, attestation)
199+
MT->>TBCCTP: handleReceiveMessage(cctpMessage)
200+
Note over TBCCTP: Stores message ID
201+
202+
Note over HR: Hyperlane Relayer delivers<br/>GMP message
203+
HR->>Mailbox: process([], hyperlaneMessage)
204+
Mailbox->>TBCCTP: verify([], hyperlaneMessage)
205+
Note over TBCCTP: CCTP message already processed,<br/>verifies message ID
206+
207+
Note over Mailbox: Marks message as delivered<br/>in delivered mapping
208+
Mailbox->>Recipient: handle(hyperlaneMessage)
209+
Note over Recipient: Application receives message
210+
```

0 commit comments

Comments
 (0)