Skip to content

Commit 3342e64

Browse files
Amxxfrangio
andauthored
ERC-7786 N-of-M Aggregator (#82)
Co-authored-by: Francisco Giordano <[email protected]>
1 parent 0dffdf0 commit 3342e64

File tree

5 files changed

+544
-25
lines changed

5 files changed

+544
-25
lines changed
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
6+
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
7+
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
8+
import {CAIP2} from "@openzeppelin/contracts/utils/CAIP2.sol";
9+
import {CAIP10} from "@openzeppelin/contracts/utils/CAIP10.sol";
10+
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
11+
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
12+
import {IERC7786GatewaySource, IERC7786Receiver} from "../interfaces/IERC7786.sol";
13+
14+
/**
15+
* @dev N of M gateway: Sends your message through M independent gateways. It will be delivered to the receiver by an
16+
* equivalent aggregator on the destination chain if N of the M gateways agree.
17+
*/
18+
contract ERC7786Aggregator is IERC7786GatewaySource, IERC7786Receiver, Ownable, Pausable {
19+
using EnumerableSet for *;
20+
using Strings for *;
21+
22+
struct Outbox {
23+
address gateway;
24+
bytes32 id;
25+
}
26+
27+
struct Tracker {
28+
mapping(address => bool) receivedBy;
29+
uint8 countReceived;
30+
bool executed;
31+
}
32+
33+
event OutboxDetails(bytes32 indexed outboxId, Outbox[] outbox);
34+
event Received(bytes32 indexed receiveId, address gateway);
35+
event ExecutionSuccess(bytes32 indexed receiveId);
36+
event ExecutionFailed(bytes32 indexed receiveId);
37+
event GatewayAdded(address indexed gateway);
38+
event GatewayRemoved(address indexed gateway);
39+
event ThresholdUpdated(uint8 threshold);
40+
41+
error ERC7786AggregatorValueNotSupported();
42+
error ERC7786AggregatorInvalidCrosschainSender();
43+
error ERC7786AggregatorAlreadyExecuted();
44+
error ERC7786AggregatorRemoteNotRegistered(string caip2);
45+
error ERC7786AggregatorGatewayAlreadyRegistered(address gateway);
46+
error ERC7786AggregatorGatewayNotRegistered(address gateway);
47+
error ERC7786AggregatorThresholdViolation();
48+
error ERC7786AggregatorInvalidExecutionReturnValue();
49+
50+
/****************************************************************************************************************
51+
* S T A T E V A R I A B L E S *
52+
****************************************************************************************************************/
53+
54+
/// @dev address of the matching aggregator for a given CAIP2 chain
55+
mapping(string caip2 => string) private _remotes;
56+
57+
/// @dev Tracking of the received message pending final delivery
58+
mapping(bytes32 id => Tracker) private _trackers;
59+
60+
/// @dev List of authorized IERC7786 gateways (M is the length of this set)
61+
EnumerableSet.AddressSet private _gateways;
62+
63+
/// @dev Threshold for message reception
64+
uint8 private _threshold;
65+
66+
/// @dev Nonce for message deduplication (internal)
67+
uint256 private _nonce;
68+
69+
/****************************************************************************************************************
70+
* E V E N T S & E R R O R S *
71+
****************************************************************************************************************/
72+
event RemoteRegistered(string chainId, string aggregator);
73+
error RemoteAlreadyRegistered(string chainId);
74+
75+
/****************************************************************************************************************
76+
* F U N C T I O N S *
77+
****************************************************************************************************************/
78+
constructor(address owner_, address[] memory gateways_, uint8 threshold_) Ownable(owner_) {
79+
for (uint256 i = 0; i < gateways_.length; ++i) {
80+
_addGateway(gateways_[i]);
81+
}
82+
_setThreshold(threshold_);
83+
}
84+
85+
// ============================================ IERC7786GatewaySource ============================================
86+
87+
/// @inheritdoc IERC7786GatewaySource
88+
function supportsAttribute(bytes4 /*selector*/) public view virtual returns (bool) {
89+
return false;
90+
}
91+
92+
/// @inheritdoc IERC7786GatewaySource
93+
/// @dev Using memory instead of calldata avoids stack too deep errors
94+
function sendMessage(
95+
string calldata destinationChain,
96+
string memory receiver,
97+
bytes memory payload,
98+
bytes[] memory attributes
99+
) public payable virtual whenNotPaused returns (bytes32 outboxId) {
100+
if (attributes.length > 0) revert UnsupportedAttribute(bytes4(attributes[0]));
101+
if (msg.value > 0) revert ERC7786AggregatorValueNotSupported();
102+
// address of the remote aggregator, revert if not registered
103+
string memory aggregator = getRemoteAggregator(destinationChain);
104+
105+
// wrapping the payload
106+
bytes memory wrappedPayload = abi.encode(++_nonce, msg.sender.toChecksumHexString(), receiver, payload);
107+
108+
// Post on all gateways
109+
Outbox[] memory outbox = new Outbox[](_gateways.length());
110+
bool needsId = false;
111+
for (uint256 i = 0; i < outbox.length; ++i) {
112+
address gateway = _gateways.at(i);
113+
// send message
114+
bytes32 id = IERC7786GatewaySource(gateway).sendMessage(
115+
destinationChain,
116+
aggregator,
117+
wrappedPayload,
118+
attributes
119+
);
120+
// if ID, track it
121+
if (id != bytes32(0)) {
122+
outbox[i] = Outbox(gateway, id);
123+
needsId = true;
124+
}
125+
}
126+
127+
if (needsId) {
128+
outboxId = keccak256(abi.encode(outbox));
129+
emit OutboxDetails(outboxId, outbox);
130+
}
131+
132+
emit MessagePosted(
133+
outboxId,
134+
CAIP10.local(msg.sender),
135+
CAIP10.format(destinationChain, receiver),
136+
payload,
137+
attributes
138+
);
139+
}
140+
141+
// ============================================== IERC7786Receiver ===============================================
142+
143+
/**
144+
* @inheritdoc IERC7786Receiver
145+
*
146+
* @dev This function serves a dual purpose:
147+
*
148+
* It will be called by ERC-7786 gateways with message coming from the the corresponding aggregator on the source
149+
* chain. These "signals" are tracked until the threshold is reached. At that point the message is sent to the
150+
* destination.
151+
*
152+
* It can also be called by anyone (including an ERC-7786 gateway) to retry the execution. This can be useful if
153+
* the automatic execution (that is triggered when the threshold is reached) fails, and someone wants to retry it.
154+
*
155+
* When a message is forwarded by a known gateway, a {Received} event is emitted. If a known gateway calls this
156+
* function more than once (for a given message), only the first call is counts toward the threshold and emits an
157+
* {Received} event.
158+
*
159+
* This function revert if:
160+
* * the message is not properly formatted or does not originate from the registered aggregator on the source
161+
* chain.
162+
* * someone tries re-execute a message that was already successfully delivered. This includes gateways that call
163+
* this function a second time with a message that was already executed.
164+
* * the execution of the message (on the {IERC7786Receiver} receiver) is successful but fails to return the
165+
* executed value.
166+
*
167+
* This function does not revert if:
168+
* * A known gateway delivers a message for the first time, and that message was already executed. In that case
169+
* the message is NOT re-executed, and the correct "magic value" is returned.
170+
* * The execution of the message (on the {IERC7786Receiver} receiver) reverts. In that case a {ExecutionFailed}
171+
* event is emitted.
172+
*
173+
* This function emits:
174+
* * {Received} when a known ERC-7786 gateway delivers a message for the first time.
175+
* * {ExecutionSuccess} when a message is successfully delivered to the receiver.
176+
* * {ExecutionFailed} when a message delivery to the receiver reverted (for example because of OOG error).
177+
*
178+
* NOTE: interface requires this function to be payable. Even if we don't expect any value, a gateway may pass
179+
* some value for unknown reason. In that case we want to register this gateway having delivered the message and
180+
* not revert. Any value accrued that way can be recovered by the admin using the {sweep} function.
181+
*/
182+
function executeMessage(
183+
string calldata sourceChain, // CAIP-2 chain identifier
184+
string calldata sender, // CAIP-10 account address (does not include the chain identifier)
185+
bytes calldata payload,
186+
bytes[] calldata attributes
187+
) public payable virtual whenNotPaused returns (bytes4) {
188+
// Check sender is a trusted remote aggregator
189+
if (!_remotes[sourceChain].equal(sender)) revert ERC7786AggregatorInvalidCrosschainSender();
190+
191+
// Message reception tracker
192+
bytes32 id = keccak256(abi.encode(sourceChain, sender, payload, attributes));
193+
Tracker storage tracker = _trackers[id];
194+
195+
// If call is first from a trusted gateway
196+
if (_gateways.contains(msg.sender) && !tracker.receivedBy[msg.sender]) {
197+
// Count number of time received
198+
tracker.receivedBy[msg.sender] = true;
199+
++tracker.countReceived;
200+
emit Received(id, msg.sender);
201+
202+
// if already executed, leave gracefully
203+
if (tracker.executed) return IERC7786Receiver.executeMessage.selector;
204+
} else if (tracker.executed) {
205+
revert ERC7786AggregatorAlreadyExecuted();
206+
}
207+
208+
// Parse payload
209+
(, string memory originalSender, string memory receiver, bytes memory unwrappedPayload) = abi.decode(
210+
payload,
211+
(uint256, string, string, bytes)
212+
);
213+
214+
// If ready to execute, and not yet executed
215+
if (tracker.countReceived >= getThreshold()) {
216+
// prevent re-entry
217+
tracker.executed = true;
218+
219+
bytes memory call = abi.encodeCall(
220+
IERC7786Receiver.executeMessage,
221+
(sourceChain, originalSender, unwrappedPayload, attributes)
222+
);
223+
// slither-disable-next-line reentrancy-no-eth
224+
(bool success, bytes memory returndata) = receiver.parseAddress().call(call);
225+
226+
if (!success) {
227+
// rollback to enable retry
228+
tracker.executed = false;
229+
emit ExecutionFailed(id);
230+
} else if (bytes32(returndata) == bytes32(IERC7786Receiver.executeMessage.selector)) {
231+
// call successful and correct value returned
232+
emit ExecutionSuccess(id);
233+
} else {
234+
// call successful but invalid value returned, we need to revert the subcall
235+
revert ERC7786AggregatorInvalidExecutionReturnValue();
236+
}
237+
}
238+
239+
return IERC7786Receiver.executeMessage.selector;
240+
}
241+
242+
// =================================================== Getters ===================================================
243+
244+
function getGateways() public view virtual returns (address[] memory) {
245+
return _gateways.values();
246+
}
247+
248+
function getThreshold() public view virtual returns (uint8) {
249+
return _threshold;
250+
}
251+
252+
function getRemoteAggregator(string calldata caip2) public view virtual returns (string memory) {
253+
string memory aggregator = _remotes[caip2];
254+
if (bytes(aggregator).length == 0) revert ERC7786AggregatorRemoteNotRegistered(caip2);
255+
return aggregator;
256+
}
257+
258+
// =================================================== Setters ===================================================
259+
260+
function addGateway(address gateway) public virtual onlyOwner {
261+
_addGateway(gateway);
262+
}
263+
264+
function removeGateway(address gateway) public virtual onlyOwner {
265+
_removeGateway(gateway);
266+
}
267+
268+
function setThreshold(uint8 newThreshold) public virtual onlyOwner {
269+
_setThreshold(newThreshold);
270+
}
271+
272+
function registerRemoteAggregator(string memory caip2, string memory aggregator) public virtual onlyOwner {
273+
_registerRemoteAggregator(caip2, aggregator);
274+
}
275+
276+
function pause() public virtual onlyOwner {
277+
_pause();
278+
}
279+
280+
function unpause() public virtual onlyOwner {
281+
_unpause();
282+
}
283+
284+
/// @dev Recovery method in case value is ever received through {executeMessage}
285+
function sweep(address payable to) public virtual onlyOwner {
286+
Address.sendValue(to, address(this).balance);
287+
}
288+
289+
// ================================================== Internal ===================================================
290+
291+
function _addGateway(address gateway) internal virtual {
292+
if (!_gateways.add(gateway)) revert ERC7786AggregatorGatewayAlreadyRegistered(gateway);
293+
emit GatewayAdded(gateway);
294+
}
295+
296+
function _removeGateway(address gateway) internal virtual {
297+
if (!_gateways.remove(gateway)) revert ERC7786AggregatorGatewayNotRegistered(gateway);
298+
if (_threshold > _gateways.length()) revert ERC7786AggregatorThresholdViolation();
299+
emit GatewayRemoved(gateway);
300+
}
301+
302+
function _setThreshold(uint8 newThreshold) internal virtual {
303+
if (newThreshold == 0 || _threshold > _gateways.length()) revert ERC7786AggregatorThresholdViolation();
304+
_threshold = newThreshold;
305+
emit ThresholdUpdated(newThreshold);
306+
}
307+
308+
function _registerRemoteAggregator(string memory caip2, string memory aggregator) internal virtual {
309+
_remotes[caip2] = aggregator;
310+
311+
emit RemoteRegistered(caip2, aggregator);
312+
}
313+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.0;
4+
5+
import {IERC7786Receiver} from "../../interfaces/IERC7786.sol";
6+
7+
contract ERC7786ReceiverRevertMock is IERC7786Receiver {
8+
function executeMessage(
9+
string calldata,
10+
string calldata,
11+
bytes calldata,
12+
bytes[] calldata
13+
) public payable virtual returns (bytes4) {
14+
revert();
15+
}
16+
}

0 commit comments

Comments
 (0)