@arbitrum/nitro-contracts 1.0.0-beta.4 → 1.0.0-beta.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. package/package.json +2 -1
  2. package/src/bridge/Bridge.sol +127 -32
  3. package/src/bridge/IBridge.sol +40 -6
  4. package/src/bridge/{IMessageProvider.sol → IDelayedMessageProvider.sol} +4 -1
  5. package/src/bridge/IInbox.sol +23 -2
  6. package/src/bridge/IOwnable.sol +9 -0
  7. package/src/bridge/ISequencerInbox.sol +36 -9
  8. package/src/bridge/Inbox.sol +131 -34
  9. package/src/bridge/Outbox.sol +134 -31
  10. package/src/bridge/SequencerInbox.sol +159 -60
  11. package/src/challenge/ChallengeLib.sol +0 -2
  12. package/src/challenge/ChallengeManager.sol +4 -8
  13. package/src/challenge/IChallengeManager.sol +1 -1
  14. package/src/libraries/Error.sol +6 -0
  15. package/src/libraries/IGasRefunder.sol +13 -13
  16. package/src/libraries/MerkleLib.sol +11 -2
  17. package/src/libraries/MessageTypes.sol +1 -0
  18. package/src/mocks/BridgeStub.sol +67 -21
  19. package/src/mocks/SequencerInboxStub.sol +10 -8
  20. package/src/mocks/Simple.sol +8 -0
  21. package/src/node-interface/NodeInterface.sol +32 -5
  22. package/src/osp/IOneStepProver.sol +1 -2
  23. package/src/osp/OneStepProver0.sol +1 -87
  24. package/src/osp/OneStepProverHostIo.sol +5 -6
  25. package/src/osp/OneStepProverMath.sol +37 -27
  26. package/src/osp/OneStepProverMemory.sol +3 -4
  27. package/src/precompiles/ArbAggregator.sol +23 -33
  28. package/src/precompiles/ArbBLS.sol +1 -43
  29. package/src/precompiles/ArbGasInfo.sol +9 -18
  30. package/src/precompiles/ArbOwner.sol +18 -15
  31. package/src/precompiles/ArbRetryableTx.sol +13 -1
  32. package/src/precompiles/ArbSys.sol +15 -2
  33. package/src/precompiles/ArbosActs.sol +9 -2
  34. package/src/rollup/BridgeCreator.sol +23 -28
  35. package/src/rollup/IRollupCore.sol +3 -3
  36. package/src/rollup/{IRollupEventBridge.sol → IRollupEventInbox.sol} +2 -2
  37. package/src/rollup/IRollupLogic.sol +21 -18
  38. package/src/rollup/RollupAdminLogic.sol +30 -34
  39. package/src/rollup/RollupCore.sol +15 -7
  40. package/src/rollup/RollupCreator.sol +21 -11
  41. package/src/rollup/{RollupEventBridge.sol → RollupEventInbox.sol} +10 -10
  42. package/src/rollup/RollupLib.sol +20 -5
  43. package/src/rollup/RollupUserLogic.sol +10 -18
  44. package/src/rollup/ValidatorWallet.sol +125 -8
  45. package/src/rollup/ValidatorWalletCreator.sol +11 -6
  46. package/src/state/Deserialize.sol +3 -22
  47. package/src/state/Instructions.sol +2 -10
  48. package/src/state/Machine.sol +0 -4
  49. package/src/state/ModuleMemory.sol +2 -1
  50. package/src/state/Value.sol +2 -3
  51. package/src/test-helpers/BridgeTester.sol +223 -0
  52. package/src/test-helpers/OutboxWithoutOptTester.sol +188 -0
  53. package/src/test-helpers/RollupMock.sol +21 -0
  54. package/src/state/PcStack.sol +0 -32
@@ -5,6 +5,7 @@
5
5
  pragma solidity ^0.8.4;
6
6
 
7
7
  import "./IInbox.sol";
8
+ import "./ISequencerInbox.sol";
8
9
  import "./IBridge.sol";
9
10
 
10
11
  import "./Messages.sol";
@@ -20,7 +21,6 @@ import {
20
21
  } from "../libraries/MessageTypes.sol";
21
22
  import {MAX_DATA_SIZE} from "../libraries/Constants.sol";
22
23
 
23
- import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
24
24
  import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
25
25
  import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
26
26
 
@@ -31,27 +31,73 @@ import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
31
31
  */
32
32
  contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
33
33
  IBridge public override bridge;
34
+ ISequencerInbox public sequencerInbox;
34
35
 
35
- modifier onlyOwner() {
36
- // whoevever owns the Bridge, also owns the Inbox. this is usually the rollup contract
37
- address bridgeOwner = OwnableUpgradeable(address(bridge)).owner();
38
- if (msg.sender != bridgeOwner) revert NotOwner(msg.sender, bridgeOwner);
36
+ /// ------------------------------------ allow list start ------------------------------------ ///
37
+
38
+ bool public allowListEnabled;
39
+ mapping(address => bool) public isAllowed;
40
+
41
+ event AllowListAddressSet(address indexed user, bool val);
42
+ event AllowListEnabledUpdated(bool isEnabled);
43
+
44
+ function setAllowList(address[] memory user, bool[] memory val) external onlyRollupOrOwner {
45
+ require(user.length == val.length, "INVALID_INPUT");
46
+
47
+ for (uint256 i = 0; i < user.length; i++) {
48
+ isAllowed[user[i]] = val[i];
49
+ emit AllowListAddressSet(user[i], val[i]);
50
+ }
51
+ }
52
+
53
+ function setAllowListEnabled(bool _allowListEnabled) external onlyRollupOrOwner {
54
+ require(_allowListEnabled != allowListEnabled, "ALREADY_SET");
55
+ allowListEnabled = _allowListEnabled;
56
+ emit AllowListEnabledUpdated(_allowListEnabled);
57
+ }
58
+
59
+ /// @dev this modifier checks the tx.origin instead of msg.sender for convenience (ie it allows
60
+ /// allowed users to interact with the token bridge without needing the token bridge to be allowList aware).
61
+ /// this modifier is not intended to use to be used for security (since this opens the allowList to
62
+ /// a smart contract phishing risk).
63
+ modifier onlyAllowed() {
64
+ // solhint-disable-next-line avoid-tx-origin
65
+ if (allowListEnabled && !isAllowed[tx.origin]) revert NotAllowedOrigin(tx.origin);
66
+ _;
67
+ }
68
+
69
+ /// ------------------------------------ allow list end ------------------------------------ ///
70
+
71
+ modifier onlyRollupOrOwner() {
72
+ IOwnable rollup = bridge.rollup();
73
+ if (msg.sender != address(rollup)) {
74
+ address rollupOwner = rollup.owner();
75
+ if (msg.sender != rollupOwner) {
76
+ revert NotRollupOrOwner(msg.sender, address(rollup), rollupOwner);
77
+ }
78
+ }
39
79
  _;
40
80
  }
41
81
 
42
82
  /// @notice pauses all inbox functionality
43
- function pause() external onlyOwner {
83
+ function pause() external onlyRollupOrOwner {
44
84
  _pause();
45
85
  }
46
86
 
47
87
  /// @notice unpauses all inbox functionality
48
- function unpause() external onlyOwner {
88
+ function unpause() external onlyRollupOrOwner {
49
89
  _unpause();
50
90
  }
51
91
 
52
- function initialize(IBridge _bridge) external initializer onlyDelegated {
92
+ function initialize(IBridge _bridge, ISequencerInbox _sequencerInbox)
93
+ external
94
+ initializer
95
+ onlyDelegated
96
+ {
53
97
  if (address(bridge) != address(0)) revert AlreadyInit();
54
98
  bridge = _bridge;
99
+ sequencerInbox = _sequencerInbox;
100
+ allowListEnabled = false;
55
101
  __Pausable_init();
56
102
  }
57
103
 
@@ -64,6 +110,7 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
64
110
  sstore(i, 0)
65
111
  }
66
112
  }
113
+ allowListEnabled = false;
67
114
  bridge = _bridge;
68
115
  }
69
116
 
@@ -75,6 +122,7 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
75
122
  function sendL2MessageFromOrigin(bytes calldata messageData)
76
123
  external
77
124
  whenNotPaused
125
+ onlyAllowed
78
126
  returns (uint256)
79
127
  {
80
128
  // solhint-disable-next-line avoid-tx-origin
@@ -95,6 +143,7 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
95
143
  external
96
144
  override
97
145
  whenNotPaused
146
+ onlyAllowed
98
147
  returns (uint256)
99
148
  {
100
149
  return _deliverMessage(L2_MSG, msg.sender, messageData);
@@ -106,7 +155,7 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
106
155
  uint256 nonce,
107
156
  address to,
108
157
  bytes calldata data
109
- ) external payable virtual override whenNotPaused returns (uint256) {
158
+ ) external payable virtual override whenNotPaused onlyAllowed returns (uint256) {
110
159
  return
111
160
  _deliverMessage(
112
161
  L1MessageType_L2FundedByL1,
@@ -128,7 +177,7 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
128
177
  uint256 maxFeePerGas,
129
178
  address to,
130
179
  bytes calldata data
131
- ) external payable virtual override whenNotPaused returns (uint256) {
180
+ ) external payable virtual override whenNotPaused onlyAllowed returns (uint256) {
132
181
  return
133
182
  _deliverMessage(
134
183
  L1MessageType_L2FundedByL1,
@@ -151,7 +200,7 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
151
200
  address to,
152
201
  uint256 value,
153
202
  bytes calldata data
154
- ) external virtual override whenNotPaused returns (uint256) {
203
+ ) external virtual override whenNotPaused onlyAllowed returns (uint256) {
155
204
  return
156
205
  _deliverMessage(
157
206
  L2_MSG,
@@ -174,7 +223,7 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
174
223
  address to,
175
224
  uint256 value,
176
225
  bytes calldata data
177
- ) external virtual override whenNotPaused returns (uint256) {
226
+ ) external virtual override whenNotPaused onlyAllowed returns (uint256) {
178
227
  return
179
228
  _deliverMessage(
180
229
  L2_MSG,
@@ -209,43 +258,51 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
209
258
  /// @dev this does not trigger the fallback function when receiving in the L2 side.
210
259
  /// Look into retryable tickets if you are interested in this functionality.
211
260
  /// @dev this function should not be called inside contract constructors
212
- function depositEth() public payable override whenNotPaused returns (uint256) {
213
- address sender = msg.sender;
261
+ function depositEth() public payable override whenNotPaused onlyAllowed returns (uint256) {
262
+ address dest = msg.sender;
214
263
 
215
264
  // solhint-disable-next-line avoid-tx-origin
216
- if (!AddressUpgradeable.isContract(sender) && tx.origin == msg.sender) {
265
+ if (AddressUpgradeable.isContract(msg.sender) || tx.origin != msg.sender) {
217
266
  // isContract check fails if this function is called during a contract's constructor.
218
267
  // We don't adjust the address for calls coming from L1 contracts since their addresses get remapped
219
268
  // If the caller is an EOA, we adjust the address.
220
269
  // This is needed because unsigned messages to the L2 (such as retryables)
221
270
  // have the L1 sender address mapped.
222
- // Here we preemptively reverse the mapping for EOAs so deposits work as expected
223
- sender = AddressAliasHelper.undoL1ToL2Alias(sender);
271
+ dest = AddressAliasHelper.applyL1ToL2Alias(msg.sender);
224
272
  }
225
273
 
226
274
  return
227
275
  _deliverMessage(
228
276
  L1MessageType_ethDeposit,
229
- sender, // arb-os will add the alias to this value
230
- abi.encodePacked(msg.value)
277
+ msg.sender,
278
+ abi.encodePacked(dest, msg.value)
231
279
  );
232
280
  }
233
281
 
234
282
  /// @notice deprecated in favour of depositEth with no parameters
235
- function depositEth(uint256) external payable virtual override whenNotPaused returns (uint256) {
283
+ function depositEth(uint256)
284
+ external
285
+ payable
286
+ virtual
287
+ override
288
+ whenNotPaused
289
+ onlyAllowed
290
+ returns (uint256)
291
+ {
236
292
  return depositEth();
237
293
  }
238
294
 
239
295
  /**
240
296
  * @notice deprecated in favour of unsafeCreateRetryableTicket
241
297
  * @dev deprecated in favour of unsafeCreateRetryableTicket
298
+ * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error
242
299
  * @param to destination L2 contract address
243
300
  * @param l2CallValue call value for retryable L2 message
244
301
  * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee
245
302
  * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L2 balance
246
303
  * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled
247
- * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution
248
- * @param maxFeePerGas price bid for L2 execution
304
+ * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error)
305
+ * @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error)
249
306
  * @param data ABI encoded data of L2 message
250
307
  * @return unique id for retryable transaction (keccak256(requestID, uint(0) )
251
308
  */
@@ -258,7 +315,7 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
258
315
  uint256 gasLimit,
259
316
  uint256 maxFeePerGas,
260
317
  bytes calldata data
261
- ) external payable virtual whenNotPaused returns (uint256) {
318
+ ) external payable virtual whenNotPaused onlyAllowed returns (uint256) {
262
319
  return
263
320
  unsafeCreateRetryableTicket(
264
321
  to,
@@ -275,13 +332,14 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
275
332
  /**
276
333
  * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts
277
334
  * @dev all msg.value will deposited to callValueRefundAddress on L2
335
+ * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error
278
336
  * @param to destination L2 contract address
279
337
  * @param l2CallValue call value for retryable L2 message
280
338
  * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee
281
339
  * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L2 balance
282
340
  * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled
283
- * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution
284
- * @param maxFeePerGas price bid for L2 execution
341
+ * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error)
342
+ * @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error)
285
343
  * @param data ABI encoded data of L2 message
286
344
  * @return unique id for retryable transaction (keccak256(requestID, uint(0) )
287
345
  */
@@ -294,10 +352,14 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
294
352
  uint256 gasLimit,
295
353
  uint256 maxFeePerGas,
296
354
  bytes calldata data
297
- ) external payable virtual override whenNotPaused returns (uint256) {
355
+ ) external payable virtual override whenNotPaused onlyAllowed returns (uint256) {
298
356
  // ensure the user's deposit alone will make submission succeed
299
- if (msg.value < maxSubmissionCost + l2CallValue)
300
- revert InsufficientValue(maxSubmissionCost + l2CallValue, msg.value);
357
+ if (msg.value < (maxSubmissionCost + l2CallValue + gasLimit * maxFeePerGas)) {
358
+ revert InsufficientValue(
359
+ maxSubmissionCost + l2CallValue + gasLimit * maxFeePerGas,
360
+ msg.value
361
+ );
362
+ }
301
363
 
302
364
  // if a refund address is a contract, we apply the alias to it
303
365
  // so that it can access its funds on the L2
@@ -311,7 +373,7 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
311
373
  }
312
374
 
313
375
  return
314
- unsafeCreateRetryableTicket(
376
+ unsafeCreateRetryableTicketInternal(
315
377
  to,
316
378
  l2CallValue,
317
379
  maxSubmissionCost,
@@ -329,17 +391,18 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
329
391
  * come from the deposit alone, rather than falling back on the user's L2 balance
330
392
  * @dev Advanced usage only (does not rewrite aliases for excessFeeRefundAddress and callValueRefundAddress).
331
393
  * createRetryableTicket method is the recommended standard.
394
+ * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error
332
395
  * @param to destination L2 contract address
333
396
  * @param l2CallValue call value for retryable L2 message
334
397
  * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee
335
398
  * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L2 balance
336
399
  * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled
337
- * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution
338
- * @param maxFeePerGas price bid for L2 execution
400
+ * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error)
401
+ * @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error)
339
402
  * @param data ABI encoded data of L2 message
340
403
  * @return unique id for retryable transaction (keccak256(requestID, uint(0) )
341
404
  */
342
- function unsafeCreateRetryableTicket(
405
+ function unsafeCreateRetryableTicketInternal(
343
406
  address to,
344
407
  uint256 l2CallValue,
345
408
  uint256 maxSubmissionCost,
@@ -348,7 +411,23 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
348
411
  uint256 gasLimit,
349
412
  uint256 maxFeePerGas,
350
413
  bytes calldata data
351
- ) public payable virtual override whenNotPaused returns (uint256) {
414
+ ) internal virtual whenNotPaused onlyAllowed returns (uint256) {
415
+ // gas price and limit of 1 should never be a valid input, so instead they are used as
416
+ // magic values to trigger a revert in eth calls that surface data without requiring a tx trace
417
+ if (gasLimit == 1 || maxFeePerGas == 1)
418
+ revert RetryableData(
419
+ msg.sender,
420
+ to,
421
+ l2CallValue,
422
+ msg.value,
423
+ maxSubmissionCost,
424
+ excessFeeRefundAddress,
425
+ callValueRefundAddress,
426
+ gasLimit,
427
+ maxFeePerGas,
428
+ data
429
+ );
430
+
352
431
  uint256 submissionFee = calculateRetryableSubmissionFee(data.length, block.basefee);
353
432
  if (maxSubmissionCost < submissionFee)
354
433
  revert InsufficientSubmissionCost(submissionFee, maxSubmissionCost);
@@ -372,6 +451,19 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
372
451
  );
373
452
  }
374
453
 
454
+ function unsafeCreateRetryableTicket(
455
+ address,
456
+ uint256,
457
+ uint256,
458
+ address,
459
+ address,
460
+ uint256,
461
+ uint256,
462
+ bytes calldata
463
+ ) public payable override returns (uint256) {
464
+ revert("UNSAFE_RETRYABLES_TEMPORARILY_DISABLED");
465
+ }
466
+
375
467
  function _deliverMessage(
376
468
  uint8 _kind,
377
469
  address _sender,
@@ -389,6 +481,11 @@ contract Inbox is DelegateCallAware, PausableUpgradeable, IInbox {
389
481
  address sender,
390
482
  bytes32 messageDataHash
391
483
  ) internal returns (uint256) {
392
- return bridge.enqueueDelayedMessage{value: msg.value}(kind, sender, messageDataHash);
484
+ return
485
+ bridge.enqueueDelayedMessage{value: msg.value}(
486
+ kind,
487
+ AddressAliasHelper.applyL1ToL2Alias(sender),
488
+ messageDataHash
489
+ );
393
490
  }
394
491
  }
@@ -9,11 +9,14 @@ import "./IOutbox.sol";
9
9
  import "../libraries/MerkleLib.sol";
10
10
  import "../libraries/DelegateCallAware.sol";
11
11
 
12
+ /// @dev this error is thrown since certain functions are only expected to be used in simulations, not in actual txs
13
+ error SimulationOnlyEntrypoint();
14
+
12
15
  contract Outbox is DelegateCallAware, IOutbox {
13
16
  address public rollup; // the rollup contract
14
17
  IBridge public bridge; // the bridge contract
15
18
 
16
- mapping(uint256 => bool) public spent; // maps leaf number => if spent
19
+ mapping(uint256 => bytes32) public spent; // packed spent bitmap
17
20
  mapping(bytes32 => bytes32) public roots; // maps root hashes => L2 block hash
18
21
 
19
22
  struct L2ToL1Context {
@@ -27,12 +30,31 @@ contract Outbox is DelegateCallAware, IOutbox {
27
30
  // Therefore their values don't need to be maintained, and their slots will
28
31
  // be empty outside of transactions
29
32
  L2ToL1Context internal context;
33
+
34
+ // default context values to be used in storage instead of zero, to save on storage refunds
35
+ // it is assumed that arb-os never assigns these values to a valid leaf to be redeemed
36
+ uint128 private constant L2BLOCK_DEFAULT_CONTEXT = type(uint128).max;
37
+ uint128 private constant L1BLOCK_DEFAULT_CONTEXT = type(uint128).max;
38
+ uint128 private constant TIMESTAMP_DEFAULT_CONTEXT = type(uint128).max;
39
+ bytes32 private constant OUTPUTID_DEFAULT_CONTEXT = bytes32(type(uint256).max);
40
+ address private constant SENDER_DEFAULT_CONTEXT = address(type(uint160).max);
41
+
30
42
  uint128 public constant OUTBOX_VERSION = 2;
31
43
 
32
- function initialize(address _rollup, IBridge _bridge) external onlyDelegated {
33
- if (rollup != address(0)) revert AlreadyInit();
34
- rollup = _rollup;
44
+ function initialize(IBridge _bridge) external onlyDelegated {
45
+ if (address(bridge) != address(0)) revert AlreadyInit();
46
+ // address zero is returned if no context is set, but the values used in storage
47
+ // are non-zero to save users some gas (as storage refunds are usually maxed out)
48
+ // EIP-1153 would help here
49
+ context = L2ToL1Context({
50
+ l2Block: L2BLOCK_DEFAULT_CONTEXT,
51
+ l1Block: L1BLOCK_DEFAULT_CONTEXT,
52
+ timestamp: TIMESTAMP_DEFAULT_CONTEXT,
53
+ outputId: OUTPUTID_DEFAULT_CONTEXT,
54
+ sender: SENDER_DEFAULT_CONTEXT
55
+ });
35
56
  bridge = _bridge;
57
+ rollup = address(_bridge.rollup());
36
58
  }
37
59
 
38
60
  function updateSendRoot(bytes32 root, bytes32 l2BlockHash) external override {
@@ -45,28 +67,51 @@ contract Outbox is DelegateCallAware, IOutbox {
45
67
  /// When the return value is zero, that means this is a system message
46
68
  /// @dev the l2ToL1Sender behaves as the tx.origin, the msg.sender should be validated to protect against reentrancies
47
69
  function l2ToL1Sender() external view override returns (address) {
48
- return context.sender;
70
+ address sender = context.sender;
71
+ // we don't return the default context value to avoid a breaking change in the API
72
+ if (sender == SENDER_DEFAULT_CONTEXT) return address(0);
73
+ return sender;
49
74
  }
50
75
 
76
+ /// @return l2Block return L2 block when the L2 tx was initiated or zero
77
+ /// if no L2 to L1 transaction is active
51
78
  function l2ToL1Block() external view override returns (uint256) {
52
- return uint256(context.l2Block);
79
+ uint128 l2Block = context.l2Block;
80
+ // we don't return the default context value to avoid a breaking change in the API
81
+ if (l2Block == L1BLOCK_DEFAULT_CONTEXT) return uint256(0);
82
+ return uint256(l2Block);
53
83
  }
54
84
 
85
+ /// @return l1Block return L1 block when the L2 tx was initiated or zero
86
+ /// if no L2 to L1 transaction is active
55
87
  function l2ToL1EthBlock() external view override returns (uint256) {
56
- return uint256(context.l1Block);
88
+ uint128 l1Block = context.l1Block;
89
+ // we don't return the default context value to avoid a breaking change in the API
90
+ if (l1Block == L1BLOCK_DEFAULT_CONTEXT) return uint256(0);
91
+ return uint256(l1Block);
57
92
  }
58
93
 
94
+ /// @return timestamp return L2 timestamp when the L2 tx was initiated or zero
95
+ /// if no L2 to L1 transaction is active
59
96
  function l2ToL1Timestamp() external view override returns (uint256) {
60
- return uint256(context.timestamp);
97
+ uint128 timestamp = context.timestamp;
98
+ // we don't return the default context value to avoid a breaking change in the API
99
+ if (timestamp == TIMESTAMP_DEFAULT_CONTEXT) return uint256(0);
100
+ return uint256(timestamp);
61
101
  }
62
102
 
63
- // @deprecated batch number is now always 0
103
+ /// @notice batch number is deprecated and now always returns 0
64
104
  function l2ToL1BatchNum() external pure override returns (uint256) {
65
105
  return 0;
66
106
  }
67
107
 
108
+ /// @return outputId returns the unique output identifier of the L2 to L1 tx or
109
+ /// zero if no L2 to L1 transaction is active
68
110
  function l2ToL1OutputId() external view override returns (bytes32) {
69
- return context.outputId;
111
+ bytes32 outputId = context.outputId;
112
+ // we don't return the default context value to avoid a breaking change in the API
113
+ if (outputId == OUTPUTID_DEFAULT_CONTEXT) return bytes32(0);
114
+ return outputId;
70
115
  }
71
116
 
72
117
  /**
@@ -93,22 +138,56 @@ contract Outbox is DelegateCallAware, IOutbox {
93
138
  uint256 l2Timestamp,
94
139
  uint256 value,
95
140
  bytes calldata data
96
- ) external virtual {
97
- bytes32 outputId;
98
- {
99
- bytes32 userTx = calculateItemHash(
100
- l2Sender,
101
- to,
102
- l2Block,
103
- l1Block,
104
- l2Timestamp,
105
- value,
106
- data
107
- );
108
-
109
- outputId = recordOutputAsSpent(proof, index, userTx);
110
- emit OutBoxTransactionExecuted(to, l2Sender, 0, index);
111
- }
141
+ ) external {
142
+ bytes32 userTx = calculateItemHash(
143
+ l2Sender,
144
+ to,
145
+ l2Block,
146
+ l1Block,
147
+ l2Timestamp,
148
+ value,
149
+ data
150
+ );
151
+
152
+ recordOutputAsSpent(proof, index, userTx);
153
+
154
+ executeTransactionImpl(index, l2Sender, to, l2Block, l1Block, l2Timestamp, value, data);
155
+ }
156
+
157
+ /// @dev function used to simulate the result of a particular function call from the outbox
158
+ /// it is useful for things such as gas estimates. This function includes all costs except for
159
+ /// proof validation (which can be considered offchain as a somewhat of a fixed cost - it's
160
+ /// not really a fixed cost, but can be treated as so with a fixed overhead for gas estimation).
161
+ /// We can't include the cost of proof validation since this is intended to be used to simulate txs
162
+ /// that are included in yet-to-be confirmed merkle roots. The simulation entrypoint could instead pretend
163
+ /// to confirm a pending merkle root, but that would be less pratical for integrating with tooling.
164
+ /// It is only possible to trigger it when the msg sender is address zero, which should be impossible
165
+ /// unless under simulation in an eth_call or eth_estimateGas
166
+ function executeTransactionSimulation(
167
+ uint256 index,
168
+ address l2Sender,
169
+ address to,
170
+ uint256 l2Block,
171
+ uint256 l1Block,
172
+ uint256 l2Timestamp,
173
+ uint256 value,
174
+ bytes calldata data
175
+ ) external {
176
+ if (msg.sender != address(0)) revert SimulationOnlyEntrypoint();
177
+ executeTransactionImpl(index, l2Sender, to, l2Block, l1Block, l2Timestamp, value, data);
178
+ }
179
+
180
+ function executeTransactionImpl(
181
+ uint256 outputId,
182
+ address l2Sender,
183
+ address to,
184
+ uint256 l2Block,
185
+ uint256 l1Block,
186
+ uint256 l2Timestamp,
187
+ uint256 value,
188
+ bytes calldata data
189
+ ) internal {
190
+ emit OutBoxTransactionExecuted(to, l2Sender, 0, outputId);
112
191
 
113
192
  // we temporarily store the previous values so the outbox can naturally
114
193
  // unwind itself when there are nested calls to `executeTransaction`
@@ -119,7 +198,7 @@ contract Outbox is DelegateCallAware, IOutbox {
119
198
  l2Block: uint128(l2Block),
120
199
  l1Block: uint128(l1Block),
121
200
  timestamp: uint128(l2Timestamp),
122
- outputId: outputId
201
+ outputId: bytes32(outputId)
123
202
  });
124
203
 
125
204
  // set and reset vars around execution so they remain valid during call
@@ -128,11 +207,35 @@ contract Outbox is DelegateCallAware, IOutbox {
128
207
  context = prevContext;
129
208
  }
130
209
 
210
+ function _calcSpentIndexOffset(uint256 index)
211
+ internal
212
+ view
213
+ returns (
214
+ uint256,
215
+ uint256,
216
+ bytes32
217
+ )
218
+ {
219
+ uint256 spentIndex = index / 255; // Note: Reserves the MSB.
220
+ uint256 bitOffset = index % 255;
221
+ bytes32 replay = spent[spentIndex];
222
+ return (spentIndex, bitOffset, replay);
223
+ }
224
+
225
+ function _isSpent(uint256 bitOffset, bytes32 replay) internal pure returns (bool) {
226
+ return ((replay >> bitOffset) & bytes32(uint256(1))) != bytes32(0);
227
+ }
228
+
229
+ function isSpent(uint256 index) external view returns (bool) {
230
+ (, uint256 bitOffset, bytes32 replay) = _calcSpentIndexOffset(index);
231
+ return _isSpent(bitOffset, replay);
232
+ }
233
+
131
234
  function recordOutputAsSpent(
132
235
  bytes32[] memory proof,
133
236
  uint256 index,
134
237
  bytes32 item
135
- ) internal returns (bytes32) {
238
+ ) internal {
136
239
  if (proof.length >= 256) revert ProofTooLong(proof.length);
137
240
  if (index >= 2**proof.length) revert PathNotMinimal(index, 2**proof.length);
138
241
 
@@ -140,10 +243,10 @@ contract Outbox is DelegateCallAware, IOutbox {
140
243
  bytes32 calcRoot = calculateMerkleRoot(proof, index, item);
141
244
  if (roots[calcRoot] == bytes32(0)) revert UnknownRoot(calcRoot);
142
245
 
143
- if (spent[index]) revert AlreadySpent(index);
144
- spent[index] = true;
246
+ (uint256 spentIndex, uint256 bitOffset, bytes32 replay) = _calcSpentIndexOffset(index);
145
247
 
146
- return bytes32(index);
248
+ if (_isSpent(bitOffset, replay)) revert AlreadySpent(index);
249
+ spent[spentIndex] = (replay | bytes32(1 << bitOffset));
147
250
  }
148
251
 
149
252
  function executeBridgeCall(