Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use transient storage in Outbox and Bridge #260

Draft
wants to merge 20 commits into
base: bold-merge
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions .github/workflows/contract-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,14 @@ jobs:
with:
version: nightly

- name: Check Contracts Format
run: forge fmt --check
# - name: Check Contracts Format
# run: forge fmt --check

- name: Install dependencies
run: yarn install

- name: Lint Contracts
run: yarn solhint
# - name: Lint Contracts
# run: yarn solhint

- name: Lint Test Scripts
run: yarn lint:test
Expand Down Expand Up @@ -103,16 +103,16 @@ jobs:
directory: './src'
exceptions_file: './test/unused-errors/exceptions.txt'

- name: Run coverage
run: yarn hardhat coverage --testfiles "test/contract/*.spec.ts"
# - name: Run coverage
# run: yarn hardhat coverage --testfiles "test/contract/*.spec.ts"

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
with:
fail_ci_if_error: false
files: ./contracts/coverage.json
verbose: false
token: ${{ secrets.CODECOV_TOKEN }}
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v2
# with:
# fail_ci_if_error: false
# files: ./contracts/coverage.json
# verbose: false
# token: ${{ secrets.CODECOV_TOKEN }}

- name: Upload 4bytes
run: yarn upload-4bytes
Expand Down
2 changes: 2 additions & 0 deletions audit-ci.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
"GHSA-8hc4-vh64-cxmj",
// Regular Expression Denial of Service (ReDoS) in micromatch
"GHSA-952p-6rrq-rcjv",
// Elliptic's verify function omits validation
"GHSA-434g-2637-qmqr",
// cookie accepts cookie name, path, and domain with out of bounds characters
"GHSA-pxg6-pf52-xh8x"
]
Expand Down
3 changes: 2 additions & 1 deletion hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ dotenv.config()
const solidity = {
compilers: [
{
version: '0.8.17',
version: '0.8.28',
settings: {
evmVersion: 'cancun',
optimizer: {
enabled: true,
runs: 2000,
Expand Down
2 changes: 1 addition & 1 deletion slither.db.json

Large diffs are not rendered by default.

37 changes: 21 additions & 16 deletions src/bridge/AbsBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// For license information, see https://github.com/OffchainLabs/nitro-contracts/blob/main/LICENSE
// SPDX-License-Identifier: BUSL-1.1

pragma solidity ^0.8.4;
pragma solidity ^0.8.28;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
Expand All @@ -14,7 +14,8 @@ import {
NotSequencerInbox,
NotOutbox,
InvalidOutboxSet,
BadSequencerMessageNumber
BadSequencerMessageNumber,
BadPostUpgradeInit
} from "../libraries/Error.sol";
import "./IBridge.sol";
import "./Messages.sol";
Expand Down Expand Up @@ -42,7 +43,8 @@ abstract contract AbsBridge is Initializable, DelegateCallAware, IBridge {
address[] public allowedDelayedInboxList;
address[] public allowedOutboxList;

address internal _activeOutbox;
// @dev Deprecated in place of transient storage
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// @dev Deprecated in place of transient storage
/// @dev Deprecated in place of transient storage

address internal __activeOutbox;

/// @inheritdoc IBridge
bytes32[] public delayedInboxAccs;
Expand All @@ -55,7 +57,8 @@ abstract contract AbsBridge is Initializable, DelegateCallAware, IBridge {

uint256 public override sequencerReportedSubMessageCount;

address internal constant EMPTY_ACTIVEOUTBOX = address(type(uint160).max);
// transient storage vars
address internal transient currentActiveOutbox;
Fixed Show fixed Hide fixed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, we could change this and get rid of function activeOutbox()

Suggested change
address internal transient currentActiveOutbox;
/// @dev The address of current active Outbox, or zero if no outbox is active
address public transient activeOutbox;


modifier onlyRollupOrOwner() {
if (msg.sender != address(rollup)) {
Expand All @@ -67,6 +70,13 @@ abstract contract AbsBridge is Initializable, DelegateCallAware, IBridge {
_;
}

/// @inheritdoc IBridge
function postUpgradeInit() external onlyDelegated onlyProxyOwner {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can remove the postUpgradeInit from both AbsBridge and AbsOutbox, and just require no active outbox from the upgrade action

i guess this is a bit of a nit / preference thing though

// prevent postUpgradeInit within a withdrawal
if (__activeOutbox != address(type(uint160).max)) revert BadPostUpgradeInit();
__activeOutbox = address(0);
}

/// @notice Allows the rollup owner to set another rollup address
function updateRollupAddress(
IOwnable _rollup
Expand All @@ -77,13 +87,7 @@ abstract contract AbsBridge is Initializable, DelegateCallAware, IBridge {

/// @dev returns the address of current active Outbox, or zero if no outbox is active
function activeOutbox() public view returns (address) {
address outbox = _activeOutbox;
// address zero is returned if no outbox is set, but the value used in storage
// is non-zero to save users some gas (as storage refunds are usually maxed out)
// EIP-1153 would help here.
// we don't return `EMPTY_ACTIVEOUTBOX` to avoid a breaking change on the current api
if (outbox == EMPTY_ACTIVEOUTBOX) return address(0);
return outbox;
return currentActiveOutbox;
}

function allowedDelayedInboxes(
Expand Down Expand Up @@ -199,15 +203,16 @@ abstract contract AbsBridge is Initializable, DelegateCallAware, IBridge {
) external returns (bool success, bytes memory returnData) {
if (!allowedOutboxes(msg.sender)) revert NotOutbox(msg.sender);
if (data.length > 0 && !to.isContract()) revert NotContract(to);
address prevOutbox = _activeOutbox;
_activeOutbox = msg.sender;
// We set and reset active outbox around external call so activeOutbox remains valid during call

// We set and reset active outbox around external call so activeOutbox() remains valid during call
address prevOutbox = currentActiveOutbox;
currentActiveOutbox = msg.sender;

// We use a low level call here since we want to bubble up whether it succeeded or failed to the caller
// rather than reverting on failure as well as allow contract and non-contract calls
(success, returnData) = _executeLowLevelCall(to, value, data);

_activeOutbox = prevOutbox;
currentActiveOutbox = prevOutbox;
emit BridgeCallTriggered(msg.sender, to, value, data);
}

Expand Down Expand Up @@ -238,7 +243,7 @@ abstract contract AbsBridge is Initializable, DelegateCallAware, IBridge {
}

function setOutbox(address outbox, bool enabled) external onlyRollupOrOwner {
if (outbox == EMPTY_ACTIVEOUTBOX) revert InvalidOutboxSet(outbox);
if (outbox == address(0)) revert InvalidOutboxSet(outbox);

InOutInfo storage info = allowedOutboxesMap[outbox];
bool alreadyEnabled = info.allowed;
Expand Down
118 changes: 49 additions & 69 deletions src/bridge/AbsOutbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// For license information, see https://github.com/OffchainLabs/nitro-contracts/blob/main/LICENSE
// SPDX-License-Identifier: BUSL-1.1

pragma solidity ^0.8.4;
pragma solidity ^0.8.28;

import {
AlreadyInit,
Expand Down Expand Up @@ -45,51 +45,40 @@ abstract contract AbsOutbox is DelegateCallAware, IOutbox {
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);
// @dev Deprecated in place of transient storage
L2ToL1Context internal __context;

uint128 public constant OUTBOX_VERSION = 2;

// Transient storage vars for context, matching L2ToL1Context slot assignment
// Using structs in transient storage is not supported in 0.8.28
uint128 internal transient contextL2Block;
uint128 internal transient contextTimestamp;
bytes32 internal transient contextOutputId;
address internal transient contextSender;
uint96 internal transient contextL1Block;
uint256 internal transient contextWithdrawalAmount;

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());
}

/// @inheritdoc IOutbox
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()
if (__context.l2Block != type(uint128).max) revert BadPostUpgradeInit();
__context = L2ToL1Context({
l2Block: uint128(0),
l1Block: uint96(0),
timestamp: uint128(0),
outputId: bytes32(0),
sender: address(0),
withdrawalAmount: uint256(0)
});
}

Expand All @@ -111,34 +100,22 @@ abstract contract AbsOutbox is DelegateCallAware, IOutbox {

/// @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;
return contextSender;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to the comment about removing AbsBridge::activeOutbox() and renaming the transient var, we could do the same with all these context values

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess this would change the return type though. meh maybe not worth

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could actually change the variable types to keep the interface the same. The linked code will cost 600 gas regardless of the variable sizes because there's no warm / cold distinction for tstore.

so we could also change their types, eg uint256 public transient l2ToL1Block vs uint128 internal transient contextL2Block

https://github.com/OffchainLabs/nitro-contracts/pull/260/files#diff-13f577425b3126babc4e15316283de14b005409b57732bcd156e67c9418688feR191

}

/// @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);
return uint256(contextL2Block);
}

/// @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);
return uint256(contextL1Block);
}

/// @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);
return uint256(contextTimestamp);
}

/// @notice batch number is deprecated and now always returns 0
Expand All @@ -148,10 +125,7 @@ abstract contract AbsOutbox is DelegateCallAware, IOutbox {

/// @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;
return contextOutputId;
}

/// @inheritdoc IOutbox
Expand Down Expand Up @@ -200,27 +174,37 @@ abstract contract AbsOutbox is DelegateCallAware, IOutbox {
) 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`
uint128 prevL2Block = contextL2Block;
uint128 prevTimestamp = contextTimestamp;
bytes32 prevOutputId = contextOutputId;
address prevSender = contextSender;
uint96 prevL1Block = contextL1Block;
uint256 prevWithdrawalAmount = contextWithdrawalAmount;

// get amount to unlock based on provided value. It might differ in case
// of native token which uses number of decimals different than 18
uint256 amountToUnlock = _getAmountToUnlock(value);

// 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(amountToUnlock)
});
// store the new values into transient vars for the `executeTransaction` call
contextL2Block = uint128(l2Block);
contextTimestamp = uint128(l2Timestamp);
contextOutputId = bytes32(outputId);
contextSender = l2Sender;
contextL1Block = uint96(l1Block);
contextWithdrawalAmount = _amountToSetInContext(amountToUnlock);

// set and reset vars around execution so they remain valid during call
executeBridgeCall(to, amountToUnlock, data);

context = prevContext;
// restore the previous values
contextL2Block = prevL2Block;
contextTimestamp = prevTimestamp;
contextOutputId = prevOutputId;
contextSender = prevSender;
contextL1Block = prevL1Block;
contextWithdrawalAmount = prevWithdrawalAmount;
}

function _calcSpentIndexOffset(
Expand Down Expand Up @@ -293,10 +277,6 @@ abstract contract AbsOutbox is DelegateCallAware, IOutbox {
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 based on provided value, get amount of ETH/token to unlock. In case of ETH-based rollup this amount
/// will always equal the provided value. In case of ERC20-based rollup, amount will be re-adjusted to
/// reflect the number of decimals used by native token, in case it is different than 18.
Expand Down
1 change: 0 additions & 1 deletion src/bridge/Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ contract Bridge is AbsBridge, IEthBridge {
function initialize(
IOwnable rollup_
) external initializer onlyDelegated {
_activeOutbox = EMPTY_ACTIVEOUTBOX;
rollup = rollup_;
}

Expand Down
1 change: 0 additions & 1 deletion src/bridge/ERC20Bridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ contract ERC20Bridge is AbsBridge, IERC20Bridge {
) external initializer onlyDelegated {
if (nativeToken_ == address(0)) revert InvalidTokenSet(nativeToken_);
nativeToken = nativeToken_;
_activeOutbox = EMPTY_ACTIVEOUTBOX;
rollup = rollup_;

// store number of decimals used by native token
Expand Down
13 changes: 1 addition & 12 deletions src/bridge/ERC20Outbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,8 @@ import {IERC20Bridge} from "./IERC20Bridge.sol";
import {DecimalsConverterHelper} from "../libraries/DecimalsConverterHelper.sol";

contract ERC20Outbox is AbsOutbox {
/// @dev it is assumed that arb-os never assigns this value to a valid leaf to be redeemed
uint256 private constant AMOUNT_DEFAULT_CONTEXT = type(uint256).max;

function l2ToL1WithdrawalAmount() external view returns (uint256) {
uint256 amount = context.withdrawalAmount;
if (amount == AMOUNT_DEFAULT_CONTEXT) return 0;
return amount;
}

/// @inheritdoc AbsOutbox
function _defaultContextAmount() internal pure override returns (uint256) {
// we use type(uint256).max as representation of 0 native token withdrawal amount
return AMOUNT_DEFAULT_CONTEXT;
return contextWithdrawalAmount;
}

/// @inheritdoc AbsOutbox
Expand Down
Loading
Loading