-
Notifications
You must be signed in to change notification settings - Fork 32
/
ExecutionHub.sol
401 lines (375 loc) · 21.6 KB
/
ExecutionHub.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
// ══════════════════════════════ LIBRARY IMPORTS ══════════════════════════════
import {Attestation} from "../libs/memory/Attestation.sol";
import {BaseMessage, BaseMessageLib, MemView} from "../libs/memory/BaseMessage.sol";
import {ByteString, CallData} from "../libs/memory/ByteString.sol";
import {ORIGIN_TREE_HEIGHT, SNAPSHOT_TREE_HEIGHT} from "../libs/Constants.sol";
import {
AlreadyExecuted,
AlreadyFailed,
DisputeTimeoutNotOver,
DuplicatedSnapshotRoot,
IncorrectDestinationDomain,
IncorrectMagicValue,
IncorrectOriginDomain,
IncorrectSnapshotRoot,
GasLimitTooLow,
GasSuppliedTooLow,
MessageOptimisticPeriod,
NotaryInDispute
} from "../libs/Errors.sol";
import {SafeCall} from "../libs/SafeCall.sol";
import {MerkleMath} from "../libs/merkle/MerkleMath.sol";
import {Header, Message, MessageFlag, MessageLib} from "../libs/memory/Message.sol";
import {Receipt, ReceiptLib} from "../libs/memory/Receipt.sol";
import {Request} from "../libs/stack/Request.sol";
import {SnapshotLib} from "../libs/memory/Snapshot.sol";
import {AgentFlag, AgentStatus, MessageStatus} from "../libs/Structures.sol";
import {Tips} from "../libs/stack/Tips.sol";
import {ChainContext} from "../libs/ChainContext.sol";
import {TypeCasts} from "../libs/TypeCasts.sol";
// ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════
import {AgentSecured} from "../base/AgentSecured.sol";
import {ExecutionHubEvents} from "../events/ExecutionHubEvents.sol";
import {InterfaceInbox} from "../interfaces/InterfaceInbox.sol";
import {IExecutionHub} from "../interfaces/IExecutionHub.sol";
import {IMessageRecipient} from "../interfaces/IMessageRecipient.sol";
// ═════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
/// @notice `ExecutionHub` is a parent contract for `Destination`. It is responsible for the following:
/// - Executing the messages that are proven against the saved Snapshot Merkle Roots.
/// - Base messages are forwarded to the specified message recipient, ensuring that the original
/// execution request is fulfilled correctly.
/// - Manager messages are forwarded to the local `AgentManager` contract.
/// - Keeping track of the saved Snapshot Merkle Roots (which are accepted in `Destination`).
/// - Keeping track of message execution Receipts, as well as verify their validity.
abstract contract ExecutionHub is AgentSecured, ReentrancyGuardUpgradeable, ExecutionHubEvents, IExecutionHub {
using Address for address;
using BaseMessageLib for MemView;
using ByteString for MemView;
using MessageLib for bytes;
using ReceiptLib for bytes;
using SafeCall for address;
using SafeCast for uint256;
using TypeCasts for bytes32;
/// @notice Struct representing stored data for the snapshot root
/// @param notaryIndex Index of Notary who submitted the statement with the snapshot root
/// @param attNonce Nonce of the attestation for this snapshot root
/// @param attBN Summit block number of the attestation for this snapshot root
/// @param attTS Summit timestamp of the attestation for this snapshot root
/// @param index Index of snapshot root in `_roots`
/// @param submittedAt Timestamp when the statement with the snapshot root was submitted
/// @param notaryV V-value from the Notary signature for the attestation
// TODO: tight pack this
struct SnapRootData {
uint32 notaryIndex;
uint32 attNonce;
uint40 attBN;
uint40 attTS;
uint32 index;
uint40 submittedAt;
uint256 sigIndex;
}
/// @notice Struct representing stored receipt data for the message in Execution Hub.
/// @param origin Domain where message originated
/// @param rootIndex Index of snapshot root used for proving the message
/// @param stateIndex Index of state used for the snapshot proof
/// @param executor Executor who successfully executed the message
struct ReceiptData {
uint32 origin;
uint32 rootIndex;
uint8 stateIndex;
address executor;
}
// TODO: include nonce?
// 24 bits available for tight packing
// ══════════════════════════════════════════════════ STORAGE ══════════════════════════════════════════════════════
/// @notice (messageHash => status)
/// @dev Messages coming from different origins will always have a different hash
/// as origin domain is encoded into the formatted message.
/// Thus we can use hash as a key instead of an (origin, hash) tuple.
mapping(bytes32 => ReceiptData) private _receiptData;
/// @notice First executor who made a valid attempt of executing a message.
/// Note: stored only for messages that had Failed status at some point of time
mapping(bytes32 => address) private _firstExecutor;
/// @dev All saved snapshot roots
bytes32[] internal _roots;
/// @dev Tracks data for all saved snapshot roots
mapping(bytes32 => SnapRootData) internal _rootData;
/// @dev gap for upgrade safety
uint256[46] private __GAP; // solhint-disable-line var-name-mixedcase
// ═════════════════════════════════════════════ MESSAGE EXECUTION ═════════════════════════════════════════════════
/// @inheritdoc IExecutionHub
function execute(
bytes memory msgPayload,
bytes32[] calldata originProof,
bytes32[] calldata snapProof,
uint8 stateIndex,
uint64 gasLimit
) external nonReentrant {
// This will revert if payload is not a formatted message payload
Message message = msgPayload.castToMessage();
Header header = message.header();
bytes32 msgLeaf = message.leaf();
// Ensure message was meant for this domain
if (header.destination() != localDomain) revert IncorrectDestinationDomain();
// Ensure message was not sent from this domain
if (header.origin() == localDomain) revert IncorrectOriginDomain();
// Check that message has not been executed before
ReceiptData memory rcptData = _receiptData[msgLeaf];
if (rcptData.executor != address(0)) revert AlreadyExecuted();
// Check proofs validity
SnapRootData memory rootData = _proveAttestation(header, msgLeaf, originProof, snapProof, stateIndex);
// Check if optimistic period has passed
uint256 proofMaturity = block.timestamp - rootData.submittedAt;
if (proofMaturity < header.optimisticPeriod()) revert MessageOptimisticPeriod();
uint256 paddedTips;
bool success;
// Only Base/Manager message flags exist
if (header.flag() == MessageFlag.Base) {
// This will revert if message body is not a formatted BaseMessage payload
BaseMessage baseMessage = message.body().castToBaseMessage();
success = _executeBaseMessage(header, proofMaturity, gasLimit, baseMessage);
paddedTips = Tips.unwrap(baseMessage.tips());
} else {
// gasLimit is ignored when executing manager messages
success = _executeManagerMessage(header, proofMaturity, message.body());
}
if (rcptData.origin == 0) {
// This is the first valid attempt to execute the message => save origin and snapshot proof
rcptData.origin = header.origin();
rcptData.rootIndex = rootData.index;
rcptData.stateIndex = stateIndex;
if (success) {
// This is the successful attempt to execute the message => save the executor
rcptData.executor = msg.sender;
} else {
// Save as the "first executor", if execution failed
_firstExecutor[msgLeaf] = msg.sender;
}
_receiptData[msgLeaf] = rcptData;
} else {
if (!success) revert AlreadyFailed();
// There has been a failed attempt to execute the message before => don't touch origin and snapshot root
// This is the successful attempt to execute the message => save the executor
rcptData.executor = msg.sender;
_receiptData[msgLeaf] = rcptData;
}
emit Executed(header.origin(), msgLeaf, success);
if (!_passReceipt(rootData.notaryIndex, rootData.attNonce, msgLeaf, paddedTips, rcptData)) {
// Emit event with the recorded tips so that Notaries could form a receipt to submit to Summit
emit TipsRecorded(msgLeaf, paddedTips);
}
}
// ═══════════════════════════════════════════════════ VIEWS ═══════════════════════════════════════════════════════
/// @inheritdoc IExecutionHub
function getAttestationNonce(bytes32 snapRoot) external view returns (uint32 attNonce) {
return _rootData[snapRoot].attNonce;
}
/// @inheritdoc IExecutionHub
function isValidReceipt(bytes memory rcptPayload) external view returns (bool isValid) {
// This will revert if payload is not a receipt
// This will revert if receipt refers to another domain
return _isValidReceipt(rcptPayload.castToReceipt());
}
/// @inheritdoc IExecutionHub
function messageStatus(bytes32 messageHash) external view returns (MessageStatus status) {
ReceiptData memory rcptData = _receiptData[messageHash];
if (rcptData.executor != address(0)) {
return MessageStatus.Success;
} else if (_firstExecutor[messageHash] != address(0)) {
return MessageStatus.Failed;
} else {
return MessageStatus.None;
}
}
/// @inheritdoc IExecutionHub
function messageReceipt(bytes32 messageHash) external view returns (bytes memory rcptPayload) {
ReceiptData memory rcptData = _receiptData[messageHash];
// Return empty payload if there has been no attempt to execute the message
if (rcptData.origin == 0) return "";
return _messageReceipt(messageHash, rcptData);
}
// ══════════════════════════════════════════════ INTERNAL LOGIC ═══════════════════════════════════════════════════
/// @dev Passes message content to recipient that conforms to IMessageRecipient interface.
function _executeBaseMessage(Header header, uint256 proofMaturity, uint64 gasLimit, BaseMessage baseMessage)
internal
returns (bool)
{
// Check that gas limit covers the one requested by the sender.
// We let the executor specify gas limit higher than requested to guarantee the execution of
// messages with gas limit set too low.
Request request = baseMessage.request();
if (gasLimit < request.gasLimit()) revert GasLimitTooLow();
// TODO: check that the discarded bits are empty
address recipient = baseMessage.recipient().bytes32ToAddress();
// Forward message content to the recipient, and limit the amount of forwarded gas
if (gasleft() <= gasLimit) revert GasSuppliedTooLow();
// receiveBaseMessage(origin, nonce, sender, proofMaturity, version, content)
bytes memory payload = abi.encodeCall(
IMessageRecipient.receiveBaseMessage,
(
header.origin(),
header.nonce(),
baseMessage.sender(),
proofMaturity,
request.version(),
baseMessage.content().clone()
)
);
// Pass the base message to the recipient, return the success status of the call
return recipient.safeCall({gasLimit: gasLimit, msgValue: 0, payload: payload});
}
/// @dev Uses message body for a call to AgentManager, and checks the returned magic value to ensure that
/// only "remoteX" functions could be called this way.
function _executeManagerMessage(Header header, uint256 proofMaturity, MemView body) internal returns (bool) {
// TODO: introduce incentives for executing Manager Messages?
CallData callData = body.castToCallData();
// Add the (origin, proofMaturity) values to the calldata
bytes memory payload = callData.addPrefix(abi.encode(header.origin(), proofMaturity));
// functionCall() calls AgentManager and bubbles the revert from the external call
bytes memory magicValue = address(agentManager).functionCall(payload);
// We check the returned value here to ensure that only "remoteX" functions could be called this way.
// This is done to prevent an attack by a malicious Notary trying to force Destination to call an arbitrary
// function in a local AgentManager. Any other function will not return the required selector,
// while the "remoteX" functions will perform the proofMaturity check that will make impossible to
// submit an attestation and execute a malicious Manager Message immediately, preventing this attack vector.
if (magicValue.length != 32 || bytes32(magicValue) != callData.callSelector()) revert IncorrectMagicValue();
return true;
}
/// @dev Passes the message receipt to the Inbox contract, if it is deployed on Synapse Chain.
/// This ensures that the message receipts for the messages executed on Synapse Chain are passed to Summit
/// without a Notary having to sign them.
function _passReceipt(
uint32 attNotaryIndex,
uint32 attNonce,
bytes32 messageHash,
uint256 paddedTips,
ReceiptData memory rcptData
) internal returns (bool) {
// Do nothing if contract is not deployed on Synapse Chain
if (localDomain != synapseDomain) return false;
// Do nothing for messages with no tips (TODO: introduce incentives for manager messages?)
if (paddedTips == 0) return false;
return InterfaceInbox(inbox).passReceipt({
attNotaryIndex: attNotaryIndex,
attNonce: attNonce,
paddedTips: paddedTips,
rcptPayload: _messageReceipt(messageHash, rcptData)
});
}
/// @dev Saves a snapshot root with the attestation data provided by a Notary.
/// It is assumed that the Notary signature has been checked outside of this contract.
function _saveAttestation(Attestation att, uint32 notaryIndex, uint256 sigIndex) internal {
bytes32 root = att.snapRoot();
if (_rootData[root].submittedAt != 0) revert DuplicatedSnapshotRoot();
// TODO: consider using more than 32 bits for the root index
_rootData[root] = SnapRootData({
notaryIndex: notaryIndex,
attNonce: att.nonce(),
attBN: att.blockNumber(),
attTS: att.timestamp(),
index: _roots.length.toUint32(),
submittedAt: ChainContext.blockTimestamp(),
sigIndex: sigIndex
});
_roots.push(root);
}
// ══════════════════════════════════════════════ INTERNAL VIEWS ═══════════════════════════════════════════════════
/// @dev Checks if receipt body matches the saved data for the referenced message.
/// Reverts if destination domain doesn't match the local domain.
function _isValidReceipt(Receipt rcpt) internal view returns (bool) {
// Check if receipt refers to this chain
if (rcpt.destination() != localDomain) revert IncorrectDestinationDomain();
bytes32 messageHash = rcpt.messageHash();
ReceiptData memory rcptData = _receiptData[messageHash];
// Check if there has been a single attempt to execute the message
if (rcptData.origin == 0) return false;
// Check that origin and state index fields match
if (rcpt.origin() != rcptData.origin || rcpt.stateIndex() != rcptData.stateIndex) return false;
// Check that snapshot root and notary who submitted it match in the Receipt
bytes32 snapRoot = rcpt.snapshotRoot();
(address attNotary,) = _getAgent(_rootData[snapRoot].notaryIndex);
if (snapRoot != _roots[rcptData.rootIndex] || rcpt.attNotary() != attNotary) return false;
// Check if message was executed from the first attempt
address firstExecutor = _firstExecutor[messageHash];
if (firstExecutor == address(0)) {
// Both first and final executors are saved in receipt data
return rcpt.firstExecutor() == rcptData.executor && rcpt.finalExecutor() == rcptData.executor;
} else {
// Message was Failed at some point of time, so both receipts are valid:
// "Failed": finalExecutor is ZERO
// "Success": finalExecutor matches executor from saved receipt data
address finalExecutor = rcpt.finalExecutor();
return rcpt.firstExecutor() == firstExecutor
&& (finalExecutor == address(0) || finalExecutor == rcptData.executor);
}
}
/**
* @notice Attempts to prove the validity of the cross-chain message.
* First, the origin Merkle Root is reconstructed using the origin proof.
* Then the origin state's "left leaf" is reconstructed using the origin domain.
* After that the snapshot Merkle Root is reconstructed using the snapshot proof.
* The snapshot root needs to have been submitted by an undisputed Notary.
* @dev Reverts if any of the checks fail.
* @param header Memory view over the message header
* @param msgLeaf Message Leaf that was inserted in the Origin Merkle Tree
* @param originProof Proof of inclusion of Message Leaf in the Origin Merkle Tree
* @param snapProof Proof of inclusion of Origin State Left Leaf into Snapshot Merkle Tree
* @param stateIndex Index of Origin State in the Snapshot
* @return rootData Data for the derived snapshot root
*/
function _proveAttestation(
Header header,
bytes32 msgLeaf,
bytes32[] calldata originProof,
bytes32[] calldata snapProof,
uint8 stateIndex
) internal view returns (SnapRootData memory rootData) {
// Reconstruct Origin Merkle Root using the origin proof
// Message index in the tree is (nonce - 1), as nonce starts from 1
// This will revert if origin proof length exceeds Origin Tree height
bytes32 originRoot = MerkleMath.proofRoot(header.nonce() - 1, msgLeaf, originProof, ORIGIN_TREE_HEIGHT);
// Reconstruct Snapshot Merkle Root using the snapshot proof
// This will revert if:
// - State index is out of range.
// - Snapshot Proof length exceeds Snapshot tree Height.
bytes32 snapshotRoot = SnapshotLib.proofSnapRoot(originRoot, header.origin(), snapProof, stateIndex);
// Fetch the attestation data for the snapshot root
rootData = _rootData[snapshotRoot];
// Check if snapshot root has been submitted
if (rootData.submittedAt == 0) revert IncorrectSnapshotRoot();
// Check that Notary who submitted the attestation is not in dispute
if (_notaryDisputeExists(rootData.notaryIndex)) revert NotaryInDispute();
// Check that Notary who submitted the attestation isn't in post-dispute timeout
if (_notaryDisputeTimeout(rootData.notaryIndex)) revert DisputeTimeoutNotOver();
}
/// @dev Formats the message execution receipt payload for the given hash and receipt data.
function _messageReceipt(bytes32 messageHash, ReceiptData memory rcptData)
internal
view
returns (bytes memory rcptPayload)
{
// Determine the first executor who tried to execute the message
address firstExecutor = _firstExecutor[messageHash];
if (firstExecutor == address(0)) firstExecutor = rcptData.executor;
// Determine the snapshot root that was used for proving the message
bytes32 snapRoot = _roots[rcptData.rootIndex];
(address attNotary,) = _getAgent(_rootData[snapRoot].notaryIndex);
// ExecutionHub does not store the tips,
// the Notary will have to derive the proof of tips from the message payload.
return ReceiptLib.formatReceipt({
origin_: rcptData.origin,
destination_: localDomain,
messageHash_: messageHash,
snapshotRoot_: snapRoot,
stateIndex_: rcptData.stateIndex,
attNotary_: attNotary,
firstExecutor_: firstExecutor,
finalExecutor_: rcptData.executor
});
}
}