Skip to content

Commit 1c1070d

Browse files
ams9198grantmike
authored andcommitted
Add sample CCTP v2 wrapper contract (circlefin#42)
1 parent 7622d78 commit 1c1070d

File tree

5 files changed

+601
-1
lines changed

5 files changed

+601
-1
lines changed

src/examples/CCTPHookWrapper.sol

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* Copyright 2024 Circle Internet Group, Inc. All rights reserved.
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
pragma solidity 0.7.6;
19+
20+
import {IReceiverV2} from "../interfaces/v2/IReceiverV2.sol";
21+
import {TypedMemView} from "@memview-sol/contracts/TypedMemView.sol";
22+
import {MessageV2} from "../messages/v2/MessageV2.sol";
23+
import {BurnMessageV2} from "../messages/v2/BurnMessageV2.sol";
24+
25+
/**
26+
* @title CCTPHookWrapper
27+
* @notice A sample wrapper around CCTP v2 that relays a message and
28+
* optionally executes the hook contained in the Burn Message.
29+
* @dev This is intended to only work with CCTP v2 message formats and interfaces.
30+
*/
31+
contract CCTPHookWrapper {
32+
// ============ State Variables ============
33+
// Address of the local message transmitter
34+
IReceiverV2 public immutable messageTransmitter;
35+
36+
// The supported Message Format version
37+
uint32 public immutable supportedMessageVersion;
38+
39+
// The supported Message Body version
40+
uint32 public immutable supportedMessageBodyVersion;
41+
42+
// Byte-length of an address
43+
uint256 internal constant ADDRESS_BYTE_LENGTH = 20;
44+
45+
// ============ Libraries ============
46+
using TypedMemView for bytes;
47+
using TypedMemView for bytes29;
48+
49+
// ============ Modifiers ============
50+
/**
51+
* @notice A modifier to enable access control
52+
* @dev Can be overridden to customize the behavior
53+
*/
54+
modifier onlyAllowed() virtual {
55+
_;
56+
}
57+
58+
// ============ Constructor ============
59+
/**
60+
* @param _messageTransmitter The address of the local message transmitter
61+
* @param _messageVersion The required CCTP message version. For CCTP v2, this is 1.
62+
* @param _messageBodyVersion The required message body (Burn Message) version. For CCTP v2, this is 1.
63+
*/
64+
constructor(
65+
address _messageTransmitter,
66+
uint32 _messageVersion,
67+
uint32 _messageBodyVersion
68+
) {
69+
require(
70+
_messageTransmitter != address(0),
71+
"Message transmitter is the zero address"
72+
);
73+
74+
messageTransmitter = IReceiverV2(_messageTransmitter);
75+
supportedMessageVersion = _messageVersion;
76+
supportedMessageBodyVersion = _messageBodyVersion;
77+
}
78+
79+
// ============ External Functions ============
80+
/**
81+
* @notice Relays a burn message to a local message transmitter
82+
* and executes the hook, if present.
83+
*
84+
* @dev The hook data contained in the Burn Message is expected to follow this format:
85+
* Field Bytes Type Index
86+
* target 20 address 0
87+
* hookCallData dynamic bytes 20
88+
*
89+
* The hook handler will call the target address with the hookCallData, even if hookCallData
90+
* is zero-length. Additional data about the burn message is not passed in this call.
91+
*
92+
* WARNING: this implementation does NOT enforce atomicity in the hook call. If atomicity is
93+
* required, a new wrapper contract can be created, possibly by overriding this behavior in `_handleHook`,
94+
* or by introducing a different format for the hook data that includes more information about
95+
* the desired handling.
96+
*
97+
* WARNING: in a permissionless context, it is important not to view this wrapper implementation as a trusted
98+
* caller of a hook, as others can craft messages containing hooks that look identical, that are
99+
* similarly executed from this wrapper, either by setting this contract as the destination caller,
100+
* or by setting the destination caller to be bytes32(0). Alternate implementations may extract more information
101+
* from the burn message, such as the mintRecipient or the amount, to include in the hook call to allow recipients
102+
* to further filter their receiving actions.
103+
*
104+
* WARNING: re-entrant behavior is allowed in this implementation. Relay() can be overridden to disable this.
105+
*
106+
* @dev Reverts if the receiveMessage() call to the local message transmitter reverts, or returns false.
107+
* @param message The message to relay, as bytes
108+
* @param attestation The attestation corresponding to the message, as bytes
109+
* @return relaySuccess True if the call to the local message transmitter succeeded.
110+
* @return hookSuccess True if the call to the hook target succeeded. False if the hook call failed,
111+
* or if no hook was present.
112+
* @return hookReturnData The data returned from the call to the hook target. This will be empty
113+
* if there was no hook in the message.
114+
*/
115+
function relay(
116+
bytes calldata message,
117+
bytes calldata attestation
118+
)
119+
external
120+
virtual
121+
onlyAllowed
122+
returns (
123+
bool relaySuccess,
124+
bool hookSuccess,
125+
bytes memory hookReturnData
126+
)
127+
{
128+
bytes29 _msg = message.ref(0);
129+
bytes29 _msgBody = MessageV2._getMessageBody(_msg);
130+
131+
// Perform message validation
132+
_validateMessage(_msg, _msgBody);
133+
134+
// Relay message
135+
require(
136+
messageTransmitter.receiveMessage(message, attestation),
137+
"Receive message failed"
138+
);
139+
140+
relaySuccess = true;
141+
142+
// Handle hook
143+
bytes29 _hookData = BurnMessageV2._getHookData(_msgBody);
144+
(hookSuccess, hookReturnData) = _handleHook(_hookData);
145+
}
146+
147+
// ============ Internal Functions ============
148+
/**
149+
* @notice Validates a message and its message body
150+
* @dev Can be overridden to customize the validation
151+
* @dev Reverts if the message format version or message body version
152+
* do not match the supported versions.
153+
*/
154+
function _validateMessage(
155+
bytes29 _message,
156+
bytes29 _messageBody
157+
) internal virtual {
158+
require(
159+
MessageV2._getVersion(_message) == supportedMessageVersion,
160+
"Invalid message version"
161+
);
162+
require(
163+
BurnMessageV2._getVersion(_messageBody) ==
164+
supportedMessageBodyVersion,
165+
"Invalid message body version"
166+
);
167+
}
168+
169+
/**
170+
* @notice Handles hook data by executing a call to a target address
171+
* @dev Can be overridden to customize the execution behavior
172+
* @param _hookData The hook data contained in the Burn Message
173+
* @return _success True if the call to the encoded hook target succeeds
174+
* @return _returnData The data returned from the call to the hook target
175+
*/
176+
function _handleHook(
177+
bytes29 _hookData
178+
) internal virtual returns (bool _success, bytes memory _returnData) {
179+
uint256 _hookDataLength = _hookData.len();
180+
181+
if (_hookDataLength >= ADDRESS_BYTE_LENGTH) {
182+
address _target = _hookData.indexAddress(0);
183+
bytes memory _hookCalldata = _hookData
184+
.postfix(_hookDataLength - ADDRESS_BYTE_LENGTH, 0)
185+
.clone();
186+
187+
(_success, _returnData) = address(_target).call(_hookCalldata);
188+
}
189+
}
190+
}

src/messages/v2/BurnMessageV2.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,16 @@ library BurnMessageV2 {
134134
return _message.indexUint(EXPIRATION_BLOCK_INDEX, 32);
135135
}
136136

137+
// @notice Returns _message's hookData field
138+
function _getHookData(bytes29 _message) internal pure returns (bytes29) {
139+
return
140+
_message.slice(
141+
HOOK_DATA_INDEX,
142+
_message.len() - HOOK_DATA_INDEX,
143+
0
144+
);
145+
}
146+
137147
/**
138148
* @notice Reverts if burn message is malformed or invalid length
139149
* @param _message The burn message as bytes29

0 commit comments

Comments
 (0)