diff --git a/.gas-snapshot b/.gas-snapshot index e3b7058..e17c338 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,7 +1,6 @@ -HelpersTest:test_signature() (gas: 6587) -ZenithTest:test_badSequence() (gas: 77167) -ZenithTest:test_badSignature() (gas: 66732) -ZenithTest:test_blockExpired() (gas: 55352) -ZenithTest:test_notSequencer() (gas: 58463) -ZenithTest:test_onePerBlock() (gas: 123114) -ZenithTest:test_submitBlock() (gas: 90231) \ No newline at end of file +ZenithTest:test_badSequence() (gas: 77339) +ZenithTest:test_badSignature() (gas: 66856) +ZenithTest:test_blockExpired() (gas: 55471) +ZenithTest:test_notSequencer() (gas: 58576) +ZenithTest:test_onePerBlock() (gas: 123118) +ZenithTest:test_submitBlock() (gas: 90078) \ No newline at end of file diff --git a/script/Zenith.s.sol b/script/Zenith.s.sol index 47bc1c0..84cb49e 100644 --- a/script/Zenith.s.sol +++ b/script/Zenith.s.sol @@ -4,11 +4,34 @@ pragma solidity ^0.8.24; import {Script} from "forge-std/Script.sol"; import {Zenith} from "../src/Zenith.sol"; -contract DeployZenith is Script { +contract ZenithScript is Script { // deploy: - // forge script DeployZenith --sig "run()" --rpc-url $RPC_URL --etherscan-api-key $ETHERSCAN_API_KEY --private-key $PRIVATE_KEY --broadcast --verify - function run() public { - vm.broadcast(); - new Zenith(block.chainid + 1, msg.sender); + // 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 $SEQUENCER_ADMIN_ADDRESS + function deploy(uint256 defaultRollupChainId, address withdrawalAdmin, address sequencerAdmin) + public + returns (Zenith z) + { + vm.startBroadcast(); + z = new Zenith(defaultRollupChainId, withdrawalAdmin, sequencerAdmin); + // send some ETH to newly deployed Zenith to populate some rollup state + payable(address(z)).transfer(0.00123 ether); + } + + // NOTE: script must be run using SequencerAdmin key + // set sequencer: + // forge script ZenithScript --sig "setSequencerRole(address,address)" --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast $ZENITH_ADDRESS $SEQUENCER_ADDRESS + function setSequencerRole(address payable z, address sequencer) public { + vm.startBroadcast(); + Zenith zenith = Zenith(z); + zenith.addSequencer(sequencer); + } + + // NOTE: script must be run using SequencerAdmin key + // revoke sequencer: + // forge script ZenithScript --sig "revokeSequencerRole(address,address)" --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast $ZENITH_ADDRESS $SEQUENCER_ADDRESS + function revokeSequencerRole(address payable z, address sequencer) public { + vm.startBroadcast(); + Zenith zenith = Zenith(z); + zenith.removeSequencer(sequencer); } } diff --git a/src/Passage.sol b/src/Passage.sol index 691d608..bf81319 100644 --- a/src/Passage.sol +++ b/src/Passage.sol @@ -1,20 +1,23 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -// import IERC20 from OpenZeppelin import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {AccessControlDefaultAdminRules} from - "openzeppelin-contracts/contracts/access/extensions/AccessControlDefaultAdminRules.sol"; /// @notice A contract deployed to Host chain that allows tokens to enter the rollup, /// and enables Builders to fulfill requests to exchange tokens on the Rollup for tokens on the Host. -contract Passage is AccessControlDefaultAdminRules { +contract Passage { /// @notice The chainId of rollup that Ether will be sent to by default when entering the rollup via fallback() or receive(). uint256 immutable defaultRollupChainId; + /// @notice The address that is allowed to withdraw funds from the contract. + address public immutable withdrawalAdmin; + /// @notice Thrown when attempting to fulfill an exit order with a deadline that has passed. error OrderExpired(); + /// @notice Thrown when attempting to withdraw funds if not withdrawal admin. + error OnlyWithdrawalAdmin(); + /// @notice Emitted when tokens enter the rollup. /// @param token - The address of the token entering the rollup. /// @param rollupRecipient - The recipient of the token on the rollup. @@ -57,15 +60,11 @@ contract Passage is AccessControlDefaultAdminRules { uint256 amount; } - /// @notice Initializes the Admin role. - /// @dev See `AccessControlDefaultAdminRules` for information on contract administration. - /// - Admin role can grant and revoke Sequencer roles. - /// - Admin role can be transferred via two-step process with a 1 day timelock. /// @param _defaultRollupChainId - the chainId of the rollup that Ether will be sent to by default /// when entering the rollup via fallback() or receive() fns. - /// @param admin - the address that will be the initial admin. - constructor(uint256 _defaultRollupChainId, address admin) AccessControlDefaultAdminRules(1 days, admin) { + constructor(uint256 _defaultRollupChainId, address _withdrawalAdmin) { defaultRollupChainId = _defaultRollupChainId; + withdrawalAdmin = _withdrawalAdmin; } /// @notice Allows native Ether to enter the rollup by being sent directly to the contract. @@ -139,7 +138,8 @@ contract Passage is AccessControlDefaultAdminRules { /// @notice Allows the admin to withdraw tokens from the contract. /// @dev Only the admin can call this function. /// @param withdrawals - The withdrawals to process. See Withdrawal struct docs for details. - function withdraw(Withdrawal[] calldata withdrawals) external onlyRole(DEFAULT_ADMIN_ROLE) { + function withdraw(Withdrawal[] calldata withdrawals) external { + if (msg.sender != withdrawalAdmin) revert OnlyWithdrawalAdmin(); for (uint256 i = 0; i < withdrawals.length; i++) { // transfer ether if (withdrawals[i].ethAmount > 0) { diff --git a/src/Zenith.sol b/src/Zenith.sol index 388334c..c503c6e 100644 --- a/src/Zenith.sol +++ b/src/Zenith.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; -// import openzeppelin Role contracts import {Passage} from "./Passage.sol"; contract Zenith is Passage { + /// @notice The address that is allowed to set/remove sequencers. + address public immutable sequencerAdmin; + /// @notice Block header information for the rollup block, signed by the sequencer. /// @param rollupChainId - the chainId of the rollup chain. Any chainId is accepted by the contract. /// @param sequence - the sequence number of the rollup block. Must be monotonically increasing. Enforced by the contract. @@ -19,9 +21,6 @@ contract Zenith is Passage { address rewardAddress; } - /// @notice Role that allows a key to sign commitments to rollup blocks. - bytes32 public constant SEQUENCER_ROLE = bytes32("SEQUENCER_ROLE"); - /// @notice The sequence number of the next block that can be submitted for a given rollup chainId. /// rollupChainId => nextSequence number mapping(uint256 => uint256) public nextSequence; @@ -30,6 +29,10 @@ contract Zenith is Passage { /// rollupChainId => host blockNumber that block was last submitted at mapping(uint256 => uint256) public lastSubmittedAtBlock; + /// @notice Registry of permissioned sequencers. + /// address => TRUE if it's a permissioned sequencer + mapping(address => bool) public isSequencer; + /// @notice Thrown when a block submission is attempted with a sequence number that is not the next block for the rollup chainId. /// @dev Blocks must be submitted in strict monotonic increasing order. /// @param expected - the correct next sequence number for the given rollup chainId. @@ -46,6 +49,9 @@ contract Zenith is Passage { /// @notice Thrown when attempting to submit more than one rollup block per host block error OneRollupBlockPerHostBlock(); + /// @notice Thrown when attempting to modify sequencer roles if not sequencerAdmin. + error OnlySequencerAdmin(); + /// @notice Emitted when a new rollup block is successfully submitted. /// @param sequencer - the address of the sequencer that signed the block. /// @param rollupChainId - the chainId of the rollup chain. @@ -67,12 +73,36 @@ contract Zenith is Passage { /// @notice Emit the entire block data for easy visibility event BlockData(bytes blockData); - /// @notice Initializes the Admin role. - /// @dev See `AccessControlDefaultAdminRules` for information on contract administration. - /// - Admin role can grant and revoke Sequencer roles. - /// - Admin role can be transferred via two-step process with a 1 day timelock. - /// @param admin - the address that will be the initial admin. - constructor(uint256 defaultRollupChainId, address admin) Passage(defaultRollupChainId, admin) {} + /// @notice Emitted when a sequencer is added or removed. + event SequencerSet(address indexed sequencer, bool indexed permissioned); + + constructor(uint256 _defaultRollupChainId, address _withdrawalAdmin, address _sequencerAdmin) + Passage(_defaultRollupChainId, _withdrawalAdmin) + { + sequencerAdmin = _sequencerAdmin; + } + + /// @notice Add a sequencer to the permissioned sequencer list. + /// @param sequencer - the address of the sequencer to add. + /// @custom:emits SequencerSet if the sequencer is added. + /// @custom:reverts OnlySequencerAdmin if the caller is not the sequencerAdmin. + function addSequencer(address sequencer) external { + if (msg.sender != sequencerAdmin) revert OnlySequencerAdmin(); + if (isSequencer[sequencer]) return; + isSequencer[sequencer] = true; + emit SequencerSet(sequencer, true); + } + + /// @notice Remove a sequencer from the permissioned sequencer list. + /// @param sequencer - the address of the sequencer to remove. + /// @custom:emits SequencerSet if the sequencer is removed. + /// @custom:reverts OnlySequencerAdmin if the caller is not the sequencerAdmin. + function removeSequencer(address sequencer) external { + if (msg.sender != sequencerAdmin) revert OnlySequencerAdmin(); + if (!isSequencer[sequencer]) return; + delete isSequencer[sequencer]; + emit SequencerSet(sequencer, false); + } /// @notice Submit a rollup block with block data submitted via calldata. /// @dev Blocks are submitted by Builders, with an attestation to the block data signed by a Sequencer. @@ -115,7 +145,7 @@ contract Zenith is Passage { address sequencer = ecrecover(blockCommit, v, r, s); // assert that signature is valid && sequencer is permissioned - if (sequencer == address(0) || !hasRole(SEQUENCER_ROLE, sequencer)) revert BadSignature(sequencer); + if (sequencer == address(0) || !isSequencer[sequencer]) revert BadSignature(sequencer); // assert this is the first rollup block submitted for this host block if (lastSubmittedAtBlock[header.rollupChainId] == block.number) revert OneRollupBlockPerHostBlock(); diff --git a/test/Helpers.t.sol b/test/Helpers.t.sol index 84b9e60..bdd1d77 100644 --- a/test/Helpers.t.sol +++ b/test/Helpers.t.sol @@ -9,10 +9,12 @@ contract HelpersTest is Test { function setUp() public { vm.createSelectFork("https://rpc.holesky.ethpandaops.io"); - target = new Zenith(block.chainid + 1, 0x0a53e650c6f015eF70a15Da7B18fa95F051465aB); + target = new Zenith( + block.chainid + 1, 0x11Aa4EBFbf7a481617c719a2Df028c9DA1a219aa, 0x29403F107781ea45Bf93710abf8df13F67f2008f + ); } - function test_signature() public { + function check_signature() public { bytes32 hash = 0xdcd0af9a45fa82dcdd1e4f9ef703d8cd459b6950c0638154c67117e86facf9c1; uint8 v = 28; bytes32 r = 0xb89764d107f812dbbebb925711b320d336ff8d03f08570f051123df86334f3f5; diff --git a/test/Zenith.t.sol b/test/Zenith.t.sol index 19d3f37..ced630f 100644 --- a/test/Zenith.t.sol +++ b/test/Zenith.t.sol @@ -27,8 +27,8 @@ contract ZenithTest is Test { ); function setUp() public { - target = new Zenith(block.chainid + 1, address(this)); - target.grantRole(target.SEQUENCER_ROLE(), vm.addr(sequencerKey)); + target = new Zenith(block.chainid + 1, address(this), address(this)); + target.addSequencer(vm.addr(sequencerKey)); // set default block values header.rollupChainId = block.chainid + 1;