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

feat: immutable admin structure #29

Merged
merged 3 commits into from
May 17, 2024
Merged
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
13 changes: 6 additions & 7 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -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)
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)
33 changes: 28 additions & 5 deletions script/Zenith.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
22 changes: 11 additions & 11 deletions src/Passage.sol
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down
52 changes: 41 additions & 11 deletions src/Zenith.sol
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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;
prestwich marked this conversation as resolved.
Show resolved Hide resolved
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;
prestwich marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down Expand Up @@ -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();
Expand Down
6 changes: 4 additions & 2 deletions test/Helpers.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions test/Zenith.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down