// Copyright 2021-2022, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.4; import { AlreadyInit, NotRollup, ProofTooLong, PathNotMinimal, UnknownRoot, AlreadySpent, BridgeCallFailed, HadZeroInit, BadPostUpgradeInit, RollupNotChanged } from "../libraries/Error.sol"; import "./IBridge.sol"; import "./IOutbox.sol"; import "../libraries/MerkleLib.sol"; import "../libraries/DelegateCallAware.sol"; /// @dev this error is thrown since certain functions are only expected to be used in simulations, not in actual txs error SimulationOnlyEntrypoint(); abstract contract AbsOutbox is DelegateCallAware, IOutbox { address public rollup; // the rollup contract IBridge public bridge; // the bridge contract mapping(uint256 => bytes32) public spent; // packed spent bitmap mapping(bytes32 => bytes32) public roots; // maps root hashes => L2 block hash // we're packing this struct into 4 storage slots // 1st slot: timestamp, l2Block (128 bits each, max ~3.4*10^38) // 2nd slot: outputId (256 bits) // 3rd slot: l1Block (96 bits, max ~7.9*10^28), sender (address 160 bits) // 4th slot: withdrawalAmount (256 bits) struct L2ToL1Context { uint128 l2Block; uint128 timestamp; bytes32 outputId; address sender; uint96 l1Block; uint256 withdrawalAmount; } // Note, these variables are set and then wiped during a single transaction. // Therefore their values don't need to be maintained, and their slots will // hold default values (which are interpreted as empty values) outside of transactions L2ToL1Context internal context; // default context values to be used in storage instead of zero, to save on storage refunds // it is assumed that arb-os never assigns these values to a valid leaf to be redeemed uint128 private constant L2BLOCK_DEFAULT_CONTEXT = type(uint128).max; uint96 private constant L1BLOCK_DEFAULT_CONTEXT = type(uint96).max; uint128 private constant TIMESTAMP_DEFAULT_CONTEXT = type(uint128).max; bytes32 private constant OUTPUTID_DEFAULT_CONTEXT = bytes32(type(uint256).max); address private constant SENDER_DEFAULT_CONTEXT = address(type(uint160).max); uint128 public constant OUTBOX_VERSION = 2; function initialize(IBridge _bridge) external onlyDelegated { if (address(_bridge) == address(0)) revert HadZeroInit(); if (address(bridge) != address(0)) revert AlreadyInit(); // address zero is returned if no context is set, but the values used in storage // are non-zero to save users some gas (as storage refunds are usually maxed out) // EIP-1153 would help here context = L2ToL1Context({ l2Block: L2BLOCK_DEFAULT_CONTEXT, l1Block: L1BLOCK_DEFAULT_CONTEXT, timestamp: TIMESTAMP_DEFAULT_CONTEXT, outputId: OUTPUTID_DEFAULT_CONTEXT, sender: SENDER_DEFAULT_CONTEXT, withdrawalAmount: _defaultContextAmount() }); bridge = _bridge; rollup = address(_bridge.rollup()); } function postUpgradeInit() external onlyDelegated onlyProxyOwner { // prevent postUpgradeInit within a withdrawal if (context.l2Block != L2BLOCK_DEFAULT_CONTEXT) revert BadPostUpgradeInit(); context = L2ToL1Context({ l2Block: L2BLOCK_DEFAULT_CONTEXT, l1Block: L1BLOCK_DEFAULT_CONTEXT, timestamp: TIMESTAMP_DEFAULT_CONTEXT, outputId: OUTPUTID_DEFAULT_CONTEXT, sender: SENDER_DEFAULT_CONTEXT, withdrawalAmount: _defaultContextAmount() }); } /// @notice Allows the rollup owner to sync the rollup address function updateRollupAddress() external { if (msg.sender != IOwnable(rollup).owner()) revert NotOwner(msg.sender, IOwnable(rollup).owner()); address newRollup = address(bridge.rollup()); if (rollup == newRollup) revert RollupNotChanged(); rollup = newRollup; } function updateSendRoot(bytes32 root, bytes32 l2BlockHash) external { if (msg.sender != rollup) revert NotRollup(msg.sender, rollup); roots[root] = l2BlockHash; emit SendRootUpdated(root, l2BlockHash); } /// @inheritdoc IOutbox function l2ToL1Sender() external view returns (address) { address sender = context.sender; // we don't return the default context value to avoid a breaking change in the API if (sender == SENDER_DEFAULT_CONTEXT) return address(0); return sender; } /// @inheritdoc IOutbox function l2ToL1Block() external view returns (uint256) { uint128 l2Block = context.l2Block; // we don't return the default context value to avoid a breaking change in the API if (l2Block == L2BLOCK_DEFAULT_CONTEXT) return uint256(0); return uint256(l2Block); } /// @inheritdoc IOutbox function l2ToL1EthBlock() external view returns (uint256) { uint96 l1Block = context.l1Block; // we don't return the default context value to avoid a breaking change in the API if (l1Block == L1BLOCK_DEFAULT_CONTEXT) return uint256(0); return uint256(l1Block); } /// @inheritdoc IOutbox function l2ToL1Timestamp() external view returns (uint256) { uint128 timestamp = context.timestamp; // we don't return the default context value to avoid a breaking change in the API if (timestamp == TIMESTAMP_DEFAULT_CONTEXT) return uint256(0); return uint256(timestamp); } /// @notice batch number is deprecated and now always returns 0 function l2ToL1BatchNum() external pure returns (uint256) { return 0; } /// @inheritdoc IOutbox function l2ToL1OutputId() external view returns (bytes32) { bytes32 outputId = context.outputId; // we don't return the default context value to avoid a breaking change in the API if (outputId == OUTPUTID_DEFAULT_CONTEXT) return bytes32(0); return outputId; } /// @inheritdoc IOutbox function executeTransaction( bytes32[] calldata proof, uint256 index, address l2Sender, address to, uint256 l2Block, uint256 l1Block, uint256 l2Timestamp, uint256 value, bytes calldata data ) external { bytes32 userTx = calculateItemHash( l2Sender, to, l2Block, l1Block, l2Timestamp, value, data ); recordOutputAsSpent(proof, index, userTx); executeTransactionImpl(index, l2Sender, to, l2Block, l1Block, l2Timestamp, value, data); } /// @inheritdoc IOutbox function executeTransactionSimulation( uint256 index, address l2Sender, address to, uint256 l2Block, uint256 l1Block, uint256 l2Timestamp, uint256 value, bytes calldata data ) external { if (msg.sender != address(0)) revert SimulationOnlyEntrypoint(); executeTransactionImpl(index, l2Sender, to, l2Block, l1Block, l2Timestamp, value, data); } function executeTransactionImpl( uint256 outputId, address l2Sender, address to, uint256 l2Block, uint256 l1Block, uint256 l2Timestamp, uint256 value, bytes calldata data ) internal { emit OutBoxTransactionExecuted(to, l2Sender, 0, outputId); // we temporarily store the previous values so the outbox can naturally // unwind itself when there are nested calls to `executeTransaction` L2ToL1Context memory prevContext = context; context = L2ToL1Context({ sender: l2Sender, l2Block: uint128(l2Block), l1Block: uint96(l1Block), timestamp: uint128(l2Timestamp), outputId: bytes32(outputId), withdrawalAmount: _amountToSetInContext(value) }); // set and reset vars around execution so they remain valid during call executeBridgeCall(to, value, data); context = prevContext; } function _calcSpentIndexOffset(uint256 index) internal view returns ( uint256, uint256, bytes32 ) { uint256 spentIndex = index / 255; // Note: Reserves the MSB. uint256 bitOffset = index % 255; bytes32 replay = spent[spentIndex]; return (spentIndex, bitOffset, replay); } function _isSpent(uint256 bitOffset, bytes32 replay) internal pure returns (bool) { return ((replay >> bitOffset) & bytes32(uint256(1))) != bytes32(0); } /// @inheritdoc IOutbox function isSpent(uint256 index) external view returns (bool) { (, uint256 bitOffset, bytes32 replay) = _calcSpentIndexOffset(index); return _isSpent(bitOffset, replay); } function recordOutputAsSpent( bytes32[] memory proof, uint256 index, bytes32 item ) internal { if (proof.length >= 256) revert ProofTooLong(proof.length); if (index >= 2**proof.length) revert PathNotMinimal(index, 2**proof.length); // Hash the leaf an extra time to prove it's a leaf bytes32 calcRoot = calculateMerkleRoot(proof, index, item); if (roots[calcRoot] == bytes32(0)) revert UnknownRoot(calcRoot); (uint256 spentIndex, uint256 bitOffset, bytes32 replay) = _calcSpentIndexOffset(index); if (_isSpent(bitOffset, replay)) revert AlreadySpent(index); spent[spentIndex] = (replay | bytes32(1 << bitOffset)); } function executeBridgeCall( address to, uint256 value, bytes memory data ) internal { (bool success, bytes memory returndata) = bridge.executeCall(to, value, data); if (!success) { if (returndata.length > 0) { // solhint-disable-next-line no-inline-assembly assembly { let returndata_size := mload(returndata) revert(add(32, returndata), returndata_size) } } else { revert BridgeCallFailed(); } } } function calculateItemHash( address l2Sender, address to, uint256 l2Block, uint256 l1Block, uint256 l2Timestamp, uint256 value, bytes calldata data ) public pure returns (bytes32) { return keccak256(abi.encodePacked(l2Sender, to, l2Block, l1Block, l2Timestamp, value, data)); } function calculateMerkleRoot( bytes32[] memory proof, uint256 path, bytes32 item ) public pure returns (bytes32) { return MerkleLib.calculateRoot(proof, path, keccak256(abi.encodePacked(item))); } /// @notice default value to be used for 'amount' field in L2ToL1Context outside of transaction execution. /// @return default 'amount' in case of ERC20-based rollup is type(uint256).max, or 0 in case of ETH-based rollup function _defaultContextAmount() internal pure virtual returns (uint256); /// @notice value to be set for 'amount' field in L2ToL1Context during L2 to L1 transaction execution. /// In case of ERC20-based rollup this is the amount of native token being withdrawn. In case of standard ETH-based /// rollup this amount shall always be 0, because amount of ETH being withdrawn can be read from msg.value. /// @return amount of native token being withdrawn in case of ERC20-based rollup, or 0 in case of ETH-based rollup function _amountToSetInContext(uint256 value) internal pure virtual returns (uint256); /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ uint256[42] private __gap; }