From 9e8e46073ae3612a75b0095e780f13fb9ad67ae7 Mon Sep 17 00:00:00 2001 From: Anna Carroll Date: Mon, 15 Jul 2024 13:53:24 +0200 Subject: [PATCH] feat: configurable transact gas limits (#61) * feat: move Transact to its own contract * feat: non-configurable gas limits * feat: configurable gas limits --- .gas-snapshot | 31 +++++----- script/Zenith.s.sol | 10 ++-- src/Passage.sol | 59 ------------------- src/Transact.sol | 136 ++++++++++++++++++++++++++++++++++++++++++++ test/Passage.t.sol | 30 ---------- test/Transact.t.sol | 115 +++++++++++++++++++++++++++++++++++++ 6 files changed, 275 insertions(+), 106 deletions(-) create mode 100644 src/Transact.sol create mode 100644 test/Transact.t.sol diff --git a/.gas-snapshot b/.gas-snapshot index 60969ec..cfa160e 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -14,23 +14,28 @@ OrdersTest:test_sweepERC20() (gas: 60446) OrdersTest:test_sweepETH() (gas: 81940) OrdersTest:test_underflowETH() (gas: 63528) PassageTest:test_configureEnter() (gas: 82311) -PassageTest:test_disallowedEnter() (gas: 17916) -PassageTest:test_enter() (gas: 25563) -PassageTest:test_enterToken() (gas: 64332) -PassageTest:test_enterToken_defaultChain() (gas: 62915) -PassageTest:test_enterTransact() (gas: 60890) -PassageTest:test_enter_defaultChain() (gas: 24033) -PassageTest:test_fallback() (gas: 21534) -PassageTest:test_onlyTokenAdmin() (gas: 16926) -PassageTest:test_receive() (gas: 21384) -PassageTest:test_setUp() (gas: 16968) -PassageTest:test_transact() (gas: 58562) -PassageTest:test_transact_defaultChain() (gas: 57475) -PassageTest:test_withdraw() (gas: 59166) +PassageTest:test_disallowedEnter() (gas: 17938) +PassageTest:test_enter() (gas: 25507) +PassageTest:test_enterToken() (gas: 64354) +PassageTest:test_enterToken_defaultChain() (gas: 62870) +PassageTest:test_enter_defaultChain() (gas: 24011) +PassageTest:test_fallback() (gas: 21445) +PassageTest:test_onlyTokenAdmin() (gas: 16881) +PassageTest:test_receive() (gas: 21339) +PassageTest:test_setUp() (gas: 16901) +PassageTest:test_withdraw() (gas: 59188) RollupPassageTest:test_exit() (gas: 22347) RollupPassageTest:test_exitToken() (gas: 50183) RollupPassageTest:test_fallback() (gas: 19883) RollupPassageTest:test_receive() (gas: 19844) +TransactTest:test_configureGas() (gas: 22828) +TransactTest:test_enterTransact() (gas: 103961) +TransactTest:test_onlyGasAdmin() (gas: 8810) +TransactTest:test_setUp() (gas: 17494) +TransactTest:test_transact() (gas: 101431) +TransactTest:test_transact_defaultChain() (gas: 100544) +TransactTest:test_transact_globalGasLimit() (gas: 105063) +TransactTest:test_transact_perTransactGasLimit() (gas: 32774) ZenithTest:test_addSequencer() (gas: 88121) ZenithTest:test_badSignature() (gas: 37241) ZenithTest:test_incorrectHostBlock() (gas: 35086) diff --git a/script/Zenith.s.sol b/script/Zenith.s.sol index 4322cb7..8bc7622 100644 --- a/script/Zenith.s.sol +++ b/script/Zenith.s.sol @@ -4,20 +4,22 @@ pragma solidity ^0.8.24; import {Script} from "forge-std/Script.sol"; import {Zenith} from "../src/Zenith.sol"; import {Passage, RollupPassage} from "../src/Passage.sol"; +import {Transactor} from "../src/Transact.sol"; import {HostOrders, RollupOrders} from "../src/Orders.sol"; contract ZenithScript is Script { // deploy: - // forge script ZenithScript --sig "deploy(uint256,address,address)" --rpc-url $RPC_URL --etherscan-api-key $ETHERSCAN_API_KEY --private-key $PRIVATE_KEY --broadcast --verify $ROLLUP_CHAIN_ID $WITHDRAWAL_ADMIN_ADDRESS $INITIAL_ENTER_TOKENS_ARRAY $SEQUENCER_ADMIN_ADDRESS + // forge script ZenithScript --sig "deploy(uint256,address,address)" --rpc-url $RPC_URL --etherscan-api-key $ETHERSCAN_API_KEY --private-key $PRIVATE_KEY --broadcast --verify $ROLLUP_CHAIN_ID $WITHDRAWAL_ADMIN_ADDRESS $INITIAL_ENTER_TOKENS_ARRAY $SEQUENCER_AND_GAS_ADMIN_ADDRESS function deploy( uint256 defaultRollupChainId, address withdrawalAdmin, address[] memory initialEnterTokens, - address sequencerAdmin - ) public returns (Zenith z, Passage p, HostOrders m) { + address sequencerAndGasAdmin + ) public returns (Zenith z, Passage p, Transactor t, HostOrders m) { vm.startBroadcast(); - z = new Zenith(sequencerAdmin); + z = new Zenith(sequencerAndGasAdmin); p = new Passage(defaultRollupChainId, withdrawalAdmin, initialEnterTokens); + t = new Transactor(defaultRollupChainId, sequencerAndGasAdmin, p, 30_000_000, 5_000_000); m = new HostOrders(); } diff --git a/src/Passage.sol b/src/Passage.sol index 6b9eb15..3077559 100644 --- a/src/Passage.sol +++ b/src/Passage.sol @@ -36,17 +36,6 @@ contract Passage { uint256 indexed rollupChainId, address indexed rollupRecipient, address indexed token, uint256 amount ); - /// @notice Emitted to send a special transaction to the rollup. - event Transact( - uint256 indexed rollupChainId, - address indexed sender, - address indexed to, - bytes data, - uint256 value, - uint256 gas, - uint256 maxFeePerGas - ); - /// @notice Emitted when the admin withdraws tokens from the contract. event Withdrawal(address indexed token, address indexed recipient, uint256 amount); @@ -106,54 +95,6 @@ contract Passage { enterToken(defaultRollupChainId, rollupRecipient, token, amount); } - /// @notice Allows a special transaction to be sent to the rollup with sender == L1 msg.sender. - /// @dev Transaction is processed after normal rollup block execution. - /// @dev See `enterTransact` for docs. - function transact( - uint256 rollupChainId, - address to, - bytes calldata data, - uint256 value, - uint256 gas, - uint256 maxFeePerGas - ) public payable { - enterTransact(rollupChainId, msg.sender, to, data, value, gas, maxFeePerGas); - } - - /// @dev See `transact` for docs. - function transact(address to, bytes calldata data, uint256 value, uint256 gas, uint256 maxFeePerGas) - external - payable - { - enterTransact(defaultRollupChainId, msg.sender, to, data, value, gas, maxFeePerGas); - } - - /// @notice Send Ether on the rollup, send a special transaction to be sent to the rollup with sender == L1 msg.sender. - /// @dev Enter and Transact are processed after normal rollup block execution. - /// @dev See `enter` for Enter docs. - /// @param rollupChainId - The rollup chain to send the transaction to. - /// @param etherRecipient - The recipient of the ether. - /// @param to - The address to call on the rollup. - /// @param data - The data to send to the rollup. - /// @param value - The amount of Ether to send on the rollup. - /// @param gas - The gas limit for the transaction. - /// @param maxFeePerGas - The maximum fee per gas for the transaction (per EIP-1559). - /// @custom:emits Transact indicating the transaction to mine on the rollup. - function enterTransact( - uint256 rollupChainId, - address etherRecipient, - address to, - bytes calldata data, - uint256 value, - uint256 gas, - uint256 maxFeePerGas - ) public payable { - // if msg.value is attached, Enter - enter(rollupChainId, etherRecipient); - // emit Transact event - emit Transact(rollupChainId, msg.sender, to, data, value, gas, maxFeePerGas); - } - /// @notice Alow/Disallow a given ERC20 token to enter the rollup. function configureEnter(address token, bool _canEnter) external { if (msg.sender != tokenAdmin) revert OnlyTokenAdmin(); diff --git a/src/Transact.sol b/src/Transact.sol new file mode 100644 index 0000000..3c738a3 --- /dev/null +++ b/src/Transact.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {Passage} from "./Passage.sol"; + +/// @notice A contract deployed to Host chain that enables transactions from L1 to be sent on an L2. +contract Transactor { + /// @notice The chainId of rollup that Ether will be sent to by default when entering the rollup via fallback() or receive(). + uint256 public immutable defaultRollupChainId; + + /// @notice The address that is allowed to configure `transact` gas limits. + address public immutable gasAdmin; + + /// @notice The address of the Passage contract, to enable transact + enter. + Passage public immutable passage; + + /// @notice The sum of `transact` calls in a block cannot use more than this limit. + uint256 public perBlockGasLimit; + + /// @notice Each `transact` call cannot use more than this limit. + uint256 public perTransactGasLimit; + + /// @notice The total gas used by `transact` so far in this block. + /// rollupChainId => block number => `transasct` gasLimit used so far. + mapping(uint256 => mapping(uint256 => uint256)) public transactGasUsed; + + /// @notice Emitted to send a special transaction to the rollup. + event Transact( + uint256 indexed rollupChainId, + address indexed sender, + address indexed to, + bytes data, + uint256 value, + uint256 gas, + uint256 maxFeePerGas + ); + + /// @notice Emitted when the admin configures gas limits. + event GasConfigured(uint256 perBlock, uint256 perTransact); + + /// @notice Thrown when attempting to use more then the current global `transact` gasLimit for the block. + error PerBlockTransactGasLimit(); + + /// @notice Thrown when attempting to use too much gas per single `transact` call. + error PerTransactGasLimit(); + + /// @notice Thrown when attempting to configure gas if not the admin. + error OnlyGasAdmin(); + + /// @param _defaultRollupChainId - the chainId of the rollup that Ether will be sent to by default + /// when entering the rollup via fallback() or receive() fns. + constructor( + uint256 _defaultRollupChainId, + address _gasAdmin, + Passage _passage, + uint256 _perBlockGasLimit, + uint256 _perTransactGasLimit + ) { + defaultRollupChainId = _defaultRollupChainId; + gasAdmin = _gasAdmin; + passage = _passage; + _configureGas(_perBlockGasLimit, _perTransactGasLimit); + } + + /// @notice Configure the `transact` gas limits. + function configureGas(uint256 perBlock, uint256 perTransact) external { + if (msg.sender != gasAdmin) revert OnlyGasAdmin(); + _configureGas(perBlock, perTransact); + } + + /// @notice Allows a special transaction to be sent to the rollup with sender == L1 msg.sender. + /// @dev Transaction is processed after normal rollup block execution. + /// @dev See `enterTransact` for docs. + function transact( + uint256 rollupChainId, + address to, + bytes calldata data, + uint256 value, + uint256 gas, + uint256 maxFeePerGas + ) public payable { + enterTransact(rollupChainId, msg.sender, to, data, value, gas, maxFeePerGas); + } + + /// @dev See `transact` for docs. + function transact(address to, bytes calldata data, uint256 value, uint256 gas, uint256 maxFeePerGas) + external + payable + { + enterTransact(defaultRollupChainId, msg.sender, to, data, value, gas, maxFeePerGas); + } + + /// @notice Send Ether on the rollup, send a special transaction to be sent to the rollup with sender == L1 msg.sender. + /// @dev Enter and Transact are processed after normal rollup block execution. + /// @dev See `enter` for Enter docs. + /// @param rollupChainId - The rollup chain to send the transaction to. + /// @param etherRecipient - The recipient of the ether. + /// @param to - The address to call on the rollup. + /// @param data - The data to send to the rollup. + /// @param value - The amount of Ether to send on the rollup. + /// @param gas - The gas limit for the transaction. + /// @param maxFeePerGas - The maximum fee per gas for the transaction (per EIP-1559). + /// @custom:emits Transact indicating the transaction to mine on the rollup. + function enterTransact( + uint256 rollupChainId, + address etherRecipient, + address to, + bytes calldata data, + uint256 value, + uint256 gas, + uint256 maxFeePerGas + ) public payable { + // if msg.value is attached, Enter + if (msg.value > 0) { + passage.enter{value: msg.value}(rollupChainId, etherRecipient); + } + + // ensure per-transact gas limit is respected + if (gas > perTransactGasLimit) revert PerTransactGasLimit(); + + // ensure global transact gas limit is respected + uint256 gasUsed = transactGasUsed[rollupChainId][block.number]; + if (gasUsed + gas > perBlockGasLimit) revert PerBlockTransactGasLimit(); + transactGasUsed[rollupChainId][block.number] = gasUsed + gas; + + // emit Transact event + emit Transact(rollupChainId, msg.sender, to, data, value, gas, maxFeePerGas); + } + + /// @notice Helper to configure gas limits on deploy & via admin function + function _configureGas(uint256 perBlock, uint256 perTransact) internal { + perBlockGasLimit = perBlock; + perTransactGasLimit = perTransact; + emit GasConfigured(perBlock, perTransact); + } +} diff --git a/test/Passage.t.sol b/test/Passage.t.sol index 82ee802..cf04dc8 100644 --- a/test/Passage.t.sol +++ b/test/Passage.t.sol @@ -151,36 +151,6 @@ contract PassageTest is Test { target.enterToken(recipient, token, amount); } - function test_transact() public { - vm.expectEmit(); - emit Transact(chainId, address(this), to, data, value, gas, maxFeePerGas); - target.transact(chainId, to, data, value, gas, maxFeePerGas); - - vm.expectEmit(); - emit Enter(chainId, address(this), amount); - target.transact{value: amount}(chainId, to, data, value, gas, maxFeePerGas); - } - - function test_transact_defaultChain() public { - vm.expectEmit(); - emit Transact(target.defaultRollupChainId(), address(this), to, data, value, gas, maxFeePerGas); - target.transact(to, data, value, gas, maxFeePerGas); - - vm.expectEmit(); - emit Enter(target.defaultRollupChainId(), address(this), amount); - target.transact{value: amount}(to, data, value, gas, maxFeePerGas); - } - - function test_enterTransact() public { - vm.expectEmit(); - emit Transact(chainId, address(this), to, data, value, gas, maxFeePerGas); - target.enterTransact(chainId, recipient, to, data, value, gas, maxFeePerGas); - - vm.expectEmit(); - emit Enter(chainId, recipient, amount); - target.enterTransact{value: amount}(chainId, recipient, to, data, value, gas, maxFeePerGas); - } - function test_withdraw() public { TestERC20(token).mint(address(target), amount); diff --git a/test/Transact.t.sol b/test/Transact.t.sol new file mode 100644 index 0000000..f875092 --- /dev/null +++ b/test/Transact.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {Test, console2} from "forge-std/Test.sol"; +import {Passage} from "../src/Passage.sol"; +import {Transactor} from "../src/Transact.sol"; + +contract TransactTest is Test { + Passage public passage; + Transactor public target; + uint256 chainId = 3; + address recipient = address(0x123); + uint256 amount = 200; + + address to = address(0x01); + bytes data = abi.encodeWithSelector(Passage.withdraw.selector, address(this), recipient, amount); + uint256 value = 100; + uint256 gas = 5_000_000; + uint256 maxFeePerGas = 50; + + event Transact( + uint256 indexed rollupChainId, + address indexed sender, + address indexed to, + bytes data, + uint256 value, + uint256 gas, + uint256 maxFeePerGas + ); + + event GasConfigured(uint256 perBlock, uint256 perTransact); + + // Passage event + event Enter(uint256 indexed rollupChainId, address indexed rollupRecipient, uint256 amount); + + function setUp() public { + // deploy target + passage = new Passage(block.chainid + 1, address(this), new address[](0)); + target = new Transactor(block.chainid + 1, address(this), passage, gas * 6, gas); + } + + function test_setUp() public { + assertEq(target.defaultRollupChainId(), block.chainid + 1); + assertEq(target.gasAdmin(), address(this)); + assertEq(address(target.passage()), address(passage)); + assertEq(target.perBlockGasLimit(), gas * 6); + assertEq(target.perTransactGasLimit(), gas); + } + + function test_transact() public { + vm.expectEmit(address(target)); + emit Transact(chainId, address(this), to, data, value, gas, maxFeePerGas); + target.transact(chainId, to, data, value, gas, maxFeePerGas); + + vm.expectEmit(address(passage)); + emit Enter(chainId, address(this), amount); + target.transact{value: amount}(chainId, to, data, value, gas, maxFeePerGas); + } + + function test_transact_defaultChain() public { + vm.expectEmit(address(target)); + emit Transact(target.defaultRollupChainId(), address(this), to, data, value, gas, maxFeePerGas); + target.transact(to, data, value, gas, maxFeePerGas); + + vm.expectEmit(address(passage)); + emit Enter(target.defaultRollupChainId(), address(this), amount); + target.transact{value: amount}(to, data, value, gas, maxFeePerGas); + } + + function test_enterTransact() public { + vm.expectEmit(address(target)); + emit Transact(chainId, address(this), to, data, value, gas, maxFeePerGas); + target.enterTransact(chainId, recipient, to, data, value, gas, maxFeePerGas); + + vm.expectEmit(address(passage)); + emit Enter(chainId, recipient, amount); + target.enterTransact{value: amount}(chainId, recipient, to, data, value, gas, maxFeePerGas); + } + + function test_transact_perTransactGasLimit() public { + // attempt transact with 5M + 1 gas. + vm.expectRevert(Transactor.PerTransactGasLimit.selector); + target.transact(chainId, to, data, value, gas + 1, maxFeePerGas); + } + + function test_transact_globalGasLimit() public { + // submit 6x transacts with 5M gas, consuming the total 30M global limit + for (uint256 i; i < 6; i++) { + target.transact(to, data, value, gas, maxFeePerGas); + } + + // attempt to submit another transact with 1 gas - should revert. + vm.expectRevert(abi.encodeWithSelector(Transactor.PerBlockTransactGasLimit.selector)); + target.transact(to, data, value, 1, maxFeePerGas); + } + + function test_onlyGasAdmin() public { + vm.startPrank(address(0x01)); + vm.expectRevert(Transactor.OnlyGasAdmin.selector); + target.configureGas(0, 0); + } + + function test_configureGas() public { + uint256 newPerBlock = 40_000_000; + uint256 newPerTransact = 2_000_000; + + // configure gas + vm.expectEmit(); + emit GasConfigured(newPerBlock, newPerTransact); + target.configureGas(newPerBlock, newPerTransact); + + assertEq(target.perBlockGasLimit(), newPerBlock); + assertEq(target.perTransactGasLimit(), newPerTransact); + } +}