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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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(