From fd4aebcfe8833b26e096e87e142a5e7e4744f3fa Mon Sep 17 00:00:00 2001 From: Bence Haromi <56651250+benceharomi@users.noreply.github.com> Date: Thu, 15 Aug 2024 17:36:25 +0900 Subject: [PATCH 01/13] fix: zksync-ethers v5 dependency (#702) --- gas-bound-caller/package.json | 4 ++-- l1-contracts/package.json | 2 +- l1-contracts/src.ts/deploy-test-process.ts | 2 +- l1-contracts/test/unit_tests/l2-upgrade.test.spec.ts | 2 +- l1-contracts/test/unit_tests/utils.ts | 4 ++-- system-contracts/package.json | 4 ++-- system-contracts/scripts/verify-on-explorer.ts | 2 +- system-contracts/test/BootloaderUtilities.spec.ts | 2 +- system-contracts/test/Create2Factory.spec.ts | 2 +- yarn.lock | 7 ++++--- 10 files changed, 16 insertions(+), 15 deletions(-) diff --git a/gas-bound-caller/package.json b/gas-bound-caller/package.json index 8943b4b2b..db6b3d8bf 100644 --- a/gas-bound-caller/package.json +++ b/gas-bound-caller/package.json @@ -15,7 +15,7 @@ "fast-glob": "^3.3.2", "hardhat": "^2.18.3", "preprocess": "^3.2.0", - "zksync-ethers": "https://github.com/zksync-sdk/zksync-ethers#ethers-v5-feat/bridgehub" + "zksync-ethers": "^5.9.0" }, "devDependencies": { "@matterlabs/hardhat-zksync-chai-matchers": "^0.1.4", @@ -39,7 +39,7 @@ "ts-node": "^10.1.0", "typechain": "^4.0.0", "typescript": "^4.6.4", - "zksync-ethers": "https://github.com/zksync-sdk/zksync-ethers#ethers-v5-feat/bridgehub" + "zksync-ethers": "^5.9.0" }, "mocha": { "timeout": 240000, diff --git a/l1-contracts/package.json b/l1-contracts/package.json index d30c93950..de33649f8 100644 --- a/l1-contracts/package.json +++ b/l1-contracts/package.json @@ -50,7 +50,7 @@ "ts-node": "^10.1.0", "typechain": "^4.0.0", "typescript": "^4.6.4", - "zksync-ethers": "https://github.com/zksync-sdk/zksync-ethers#ethers-v5-feat/bridgehub" + "zksync-ethers": "^5.9.0" }, "scripts": { "build": "hardhat compile", diff --git a/l1-contracts/src.ts/deploy-test-process.ts b/l1-contracts/src.ts/deploy-test-process.ts index 4762973fd..b8af27b34 100644 --- a/l1-contracts/src.ts/deploy-test-process.ts +++ b/l1-contracts/src.ts/deploy-test-process.ts @@ -7,7 +7,7 @@ import * as ethers from "ethers"; import type { BigNumberish, Wallet } from "ethers"; import { Interface } from "ethers/lib/utils"; import * as zkethers from "zksync-ethers"; -import { ETH_ADDRESS_IN_CONTRACTS } from "zksync-ethers/build/src/utils"; +import { ETH_ADDRESS_IN_CONTRACTS } from "zksync-ethers/build/utils"; import * as fs from "fs"; import type { FacetCut } from "./diamondCut"; diff --git a/l1-contracts/test/unit_tests/l2-upgrade.test.spec.ts b/l1-contracts/test/unit_tests/l2-upgrade.test.spec.ts index 7352ce8cc..4ea71d99d 100644 --- a/l1-contracts/test/unit_tests/l2-upgrade.test.spec.ts +++ b/l1-contracts/test/unit_tests/l2-upgrade.test.spec.ts @@ -3,7 +3,7 @@ import type { BigNumberish } from "ethers"; import { Wallet } from "ethers"; import * as ethers from "ethers"; import * as hardhat from "hardhat"; -import { hashBytecode } from "zksync-ethers/build/src/utils"; +import { hashBytecode } from "zksync-ethers/build/utils"; import type { AdminFacet, ExecutorFacet, GettersFacet, StateTransitionManager } from "../../typechain"; import { diff --git a/l1-contracts/test/unit_tests/utils.ts b/l1-contracts/test/unit_tests/utils.ts index c42b55c19..2bbf51733 100644 --- a/l1-contracts/test/unit_tests/utils.ts +++ b/l1-contracts/test/unit_tests/utils.ts @@ -1,8 +1,8 @@ import * as hardhat from "hardhat"; import type { BigNumberish, BytesLike } from "ethers"; import { BigNumber, ethers } from "ethers"; -import type { Address } from "zksync-ethers/build/src/types"; -import { REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT } from "zksync-ethers/build/src/utils"; +import type { Address } from "zksync-ethers/build/types"; +import { REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT } from "zksync-ethers/build/utils"; import type { IBridgehub } from "../../typechain/IBridgehub"; import type { IL1ERC20Bridge } from "../../typechain/IL1ERC20Bridge"; diff --git a/system-contracts/package.json b/system-contracts/package.json index d7d5ad1b2..95900f654 100644 --- a/system-contracts/package.json +++ b/system-contracts/package.json @@ -15,7 +15,7 @@ "fast-glob": "^3.3.2", "hardhat": "=2.22.2", "preprocess": "^3.2.0", - "zksync-ethers": "https://github.com/zksync-sdk/zksync-ethers#ethers-v5-feat/bridgehub" + "zksync-ethers": "^5.9.0" }, "devDependencies": { "@matterlabs/hardhat-zksync-chai-matchers": "^0.1.4", @@ -38,7 +38,7 @@ "ts-node": "^10.1.0", "typechain": "^4.0.0", "typescript": "^4.6.4", - "zksync-ethers": "https://github.com/zksync-sdk/zksync-ethers#ethers-v5-feat/bridgehub" + "zksync-ethers": "^5.9.0" }, "mocha": { "timeout": 240000, diff --git a/system-contracts/scripts/verify-on-explorer.ts b/system-contracts/scripts/verify-on-explorer.ts index 95fa65218..9aa37e3e6 100644 --- a/system-contracts/scripts/verify-on-explorer.ts +++ b/system-contracts/scripts/verify-on-explorer.ts @@ -6,7 +6,7 @@ import { SYSTEM_CONTRACTS } from "./constants"; import { query } from "./utils"; import { Command } from "commander"; import * as fs from "fs"; -import { sleep } from "zksync-ethers/build/src/utils"; +import { sleep } from "zksync-ethers/build/utils"; const VERIFICATION_URL = hre.network?.config?.verifyURL; diff --git a/system-contracts/test/BootloaderUtilities.spec.ts b/system-contracts/test/BootloaderUtilities.spec.ts index 2c4176a17..998b98e8b 100644 --- a/system-contracts/test/BootloaderUtilities.spec.ts +++ b/system-contracts/test/BootloaderUtilities.spec.ts @@ -2,7 +2,7 @@ import { expect } from "chai"; import { ethers } from "hardhat"; import type { Wallet } from "zksync-ethers"; import * as zksync from "zksync-ethers"; -import { serialize } from "zksync-ethers/build/src/utils"; +import { serialize } from "zksync-ethers/build/utils"; import type { BootloaderUtilities } from "../typechain"; import { BootloaderUtilitiesFactory } from "../typechain"; import { TEST_BOOTLOADER_UTILITIES_ADDRESS } from "./shared/constants"; diff --git a/system-contracts/test/Create2Factory.spec.ts b/system-contracts/test/Create2Factory.spec.ts index 7e84478e0..fc2e689f6 100644 --- a/system-contracts/test/Create2Factory.spec.ts +++ b/system-contracts/test/Create2Factory.spec.ts @@ -3,7 +3,7 @@ import { ethers } from "hardhat"; import type { Wallet } from "zksync-ethers"; import type { Create2Factory } from "../typechain"; import { deployContract, getWallets, loadArtifact } from "./shared/utils"; -import { create2Address, getDeployedContracts, hashBytecode } from "zksync-ethers/build/src/utils"; +import { create2Address, getDeployedContracts, hashBytecode } from "zksync-ethers/build/utils"; describe("Create2Factory tests", function () { let wallet: Wallet; diff --git a/yarn.lock b/yarn.lock index 01159ecf0..f83d0f620 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7654,9 +7654,10 @@ zksync-ethers@^5.0.0: dependencies: ethers "~5.7.0" -"zksync-ethers@https://github.com/zksync-sdk/zksync-ethers#ethers-v5-feat/bridgehub": - version "5.1.0" - resolved "https://github.com/zksync-sdk/zksync-ethers#28ccbe7d67b170c202b17475e06a82002e6e3acc" +zksync-ethers@^5.9.0: + version "5.9.2" + resolved "https://registry.yarnpkg.com/zksync-ethers/-/zksync-ethers-5.9.2.tgz#1c5f34cb25ac0b040fd1a6118f2ba1c2c3bda090" + integrity sha512-Y2Mx6ovvxO6UdC2dePLguVzvNToOY8iLWeq5ne+jgGSJxAi/f4He/NF6FNsf6x1aWX0o8dy4Df8RcOQXAkj5qw== dependencies: ethers "~5.7.0" From 5853c3a7ea80ede2a65c264f6a2239c87873b0de Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Tue, 20 Aug 2024 11:58:38 +0200 Subject: [PATCH 02/13] feat(consensus): add L2 registry contract (BFT-434) (#555) (#718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Moshe Shababo <17073733+moshababo@users.noreply.github.com> Co-authored-by: Akosh Farkash Co-authored-by: Bruno França Co-authored-by: Vlad Bochok <41153528+vladbochok@users.noreply.github.com> Co-authored-by: Roman Brodetski Co-authored-by: vladbochok --- l2-contracts/contracts/ConsensusRegistry.sol | 486 +++++++++++++++++ .../interfaces/IConsensusRegistry.sol | 161 ++++++ l2-contracts/package.json | 3 +- l2-contracts/src/deploy-consensus-registry.ts | 90 ++++ l2-contracts/src/utils.ts | 21 + l2-contracts/test/consensusRegistry.test.ts | 499 ++++++++++++++++++ 6 files changed, 1259 insertions(+), 1 deletion(-) create mode 100644 l2-contracts/contracts/ConsensusRegistry.sol create mode 100644 l2-contracts/contracts/interfaces/IConsensusRegistry.sol create mode 100644 l2-contracts/src/deploy-consensus-registry.ts create mode 100644 l2-contracts/test/consensusRegistry.test.ts diff --git a/l2-contracts/contracts/ConsensusRegistry.sol b/l2-contracts/contracts/ConsensusRegistry.sol new file mode 100644 index 000000000..514a4f205 --- /dev/null +++ b/l2-contracts/contracts/ConsensusRegistry.sol @@ -0,0 +1,486 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {IConsensusRegistry} from "./interfaces/IConsensusRegistry.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @title ConsensusRegistry +/// @dev Manages consensus nodes and committees for the L2 consensus protocol, +/// owned by Matter Labs Multisig. Nodes act as both validators and attesters, +/// each playing a distinct role in the consensus process. This contract facilitates +/// the rotation of validator and attester committees, which represent a subset of nodes +/// expected to actively participate in the consensus process during a specific time window. +/// @dev Designed for use with a proxy for upgradability. +contract ConsensusRegistry is IConsensusRegistry, Initializable, Ownable2StepUpgradeable { + /// @dev An array to keep track of node owners. + address[] public nodeOwners; + /// @dev A mapping of node owners => nodes. + mapping(address => Node) public nodes; + /// @dev A mapping for enabling efficient lookups when checking whether a given attester public key exists. + mapping(bytes32 => bool) public attesterPubKeyHashes; + /// @dev A mapping for enabling efficient lookups when checking whether a given validator public key exists. + mapping(bytes32 => bool) public validatorPubKeyHashes; + /// @dev Counter that increments with each new commit to the attester committee. + uint32 public attestersCommit; + /// @dev Counter that increments with each new commit to the validator committee. + uint32 public validatorsCommit; + + modifier onlyOwnerOrNodeOwner(address _nodeOwner) { + if (owner() != msg.sender && _nodeOwner != msg.sender) { + revert UnauthorizedOnlyOwnerOrNodeOwner(); + } + _; + } + + function initialize(address _initialOwner) external initializer { + if (_initialOwner == address(0)) { + revert InvalidInputNodeOwnerAddress(); + } + _transferOwnership(_initialOwner); + } + + /// @notice Adds a new node to the registry. + /// @dev Fails if node owner already exists. + /// @dev Fails if a validator/attester with the same public key already exists. + /// @param _nodeOwner The address of the new node's owner. + /// @param _validatorWeight The voting weight of the validator. + /// @param _validatorPubKey The BLS12-381 public key of the validator. + /// @param _validatorPoP The proof-of-possession (PoP) of the validator's public key. + /// @param _attesterWeight The voting weight of the attester. + /// @param _attesterPubKey The ECDSA public key of the attester. + function add( + address _nodeOwner, + uint32 _validatorWeight, + BLS12_381PublicKey calldata _validatorPubKey, + BLS12_381Signature calldata _validatorPoP, + uint32 _attesterWeight, + Secp256k1PublicKey calldata _attesterPubKey + ) external onlyOwner { + // Verify input. + _verifyInputAddress(_nodeOwner); + _verifyInputBLS12_381PublicKey(_validatorPubKey); + _verifyInputBLS12_381Signature(_validatorPoP); + _verifyInputSecp256k1PublicKey(_attesterPubKey); + + // Verify storage. + _verifyNodeOwnerDoesNotExist(_nodeOwner); + bytes32 attesterPubKeyHash = _hashAttesterPubKey(_attesterPubKey); + _verifyAttesterPubKeyDoesNotExist(attesterPubKeyHash); + bytes32 validatorPubKeyHash = _hashValidatorPubKey(_validatorPubKey); + _verifyValidatorPubKeyDoesNotExist(validatorPubKeyHash); + + uint32 nodeOwnerIdx = uint32(nodeOwners.length); + nodeOwners.push(_nodeOwner); + nodes[_nodeOwner] = Node({ + attesterLatest: AttesterAttr({ + active: true, + removed: false, + weight: _attesterWeight, + pubKey: _attesterPubKey + }), + attesterSnapshot: AttesterAttr({ + active: false, + removed: false, + weight: 0, + pubKey: Secp256k1PublicKey({tag: bytes1(0), x: bytes32(0)}) + }), + attesterLastUpdateCommit: attestersCommit, + validatorLatest: ValidatorAttr({ + active: true, + removed: false, + weight: _validatorWeight, + pubKey: _validatorPubKey, + proofOfPossession: _validatorPoP + }), + validatorSnapshot: ValidatorAttr({ + active: false, + removed: false, + weight: 0, + pubKey: BLS12_381PublicKey({a: bytes32(0), b: bytes32(0), c: bytes32(0)}), + proofOfPossession: BLS12_381Signature({a: bytes32(0), b: bytes16(0)}) + }), + validatorLastUpdateCommit: validatorsCommit, + nodeOwnerIdx: nodeOwnerIdx + }); + attesterPubKeyHashes[attesterPubKeyHash] = true; + validatorPubKeyHashes[validatorPubKeyHash] = true; + + emit NodeAdded({ + nodeOwner: _nodeOwner, + validatorWeight: _validatorWeight, + validatorPubKey: _validatorPubKey, + validatorPoP: _validatorPoP, + attesterWeight: _attesterWeight, + attesterPubKey: _attesterPubKey + }); + } + + /// @notice Deactivates a node, preventing it from participating in committees. + /// @dev Only callable by the contract owner or the node owner. + /// @dev Verifies that the node owner exists in the registry. + /// @param _nodeOwner The address of the node's owner to be inactivated. + function deactivate(address _nodeOwner) external onlyOwnerOrNodeOwner(_nodeOwner) { + _verifyNodeOwnerExists(_nodeOwner); + (Node storage node, bool deleted) = _getNodeAndDeleteIfRequired(_nodeOwner); + if (deleted) { + return; + } + + _ensureAttesterSnapshot(node); + node.attesterLatest.active = false; + _ensureValidatorSnapshot(node); + node.validatorLatest.active = false; + + emit NodeDeactivated(_nodeOwner); + } + + /// @notice Activates a previously inactive node, allowing it to participate in committees. + /// @dev Only callable by the contract owner or the node owner. + /// @dev Verifies that the node owner exists in the registry. + /// @param _nodeOwner The address of the node's owner to be activated. + function activate(address _nodeOwner) external onlyOwnerOrNodeOwner(_nodeOwner) { + _verifyNodeOwnerExists(_nodeOwner); + (Node storage node, bool deleted) = _getNodeAndDeleteIfRequired(_nodeOwner); + if (deleted) { + return; + } + + _ensureAttesterSnapshot(node); + node.attesterLatest.active = true; + _ensureValidatorSnapshot(node); + node.validatorLatest.active = true; + + emit NodeActivated(_nodeOwner); + } + + /// @notice Removes a node from the registry. + /// @dev Only callable by the contract owner. + /// @dev Verifies that the node owner exists in the registry. + /// @param _nodeOwner The address of the node's owner to be removed. + function remove(address _nodeOwner) external onlyOwner { + _verifyNodeOwnerExists(_nodeOwner); + (Node storage node, bool deleted) = _getNodeAndDeleteIfRequired(_nodeOwner); + if (deleted) { + return; + } + + _ensureAttesterSnapshot(node); + node.attesterLatest.removed = true; + _ensureValidatorSnapshot(node); + node.validatorLatest.removed = true; + + emit NodeRemoved(_nodeOwner); + } + + /// @notice Changes the validator weight of a node in the registry. + /// @dev Only callable by the contract owner. + /// @dev Verifies that the node owner exists in the registry. + /// @param _nodeOwner The address of the node's owner whose validator weight will be changed. + /// @param _weight The new validator weight to assign to the node. + function changeValidatorWeight(address _nodeOwner, uint32 _weight) external onlyOwner { + _verifyNodeOwnerExists(_nodeOwner); + (Node storage node, bool deleted) = _getNodeAndDeleteIfRequired(_nodeOwner); + if (deleted) { + return; + } + + _ensureValidatorSnapshot(node); + node.validatorLatest.weight = _weight; + + emit NodeValidatorWeightChanged(_nodeOwner, _weight); + } + + /// @notice Changes the attester weight of a node in the registry. + /// @dev Only callable by the contract owner. + /// @dev Verifies that the node owner exists in the registry. + /// @param _nodeOwner The address of the node's owner whose attester weight will be changed. + /// @param _weight The new attester weight to assign to the node. + function changeAttesterWeight(address _nodeOwner, uint32 _weight) external onlyOwner { + _verifyNodeOwnerExists(_nodeOwner); + (Node storage node, bool deleted) = _getNodeAndDeleteIfRequired(_nodeOwner); + if (deleted) { + return; + } + + _ensureAttesterSnapshot(node); + node.attesterLatest.weight = _weight; + + emit NodeAttesterWeightChanged(_nodeOwner, _weight); + } + + /// @notice Changes the validator's public key and proof-of-possession in the registry. + /// @dev Only callable by the contract owner or the node owner. + /// @dev Verifies that the node owner exists in the registry. + /// @param _nodeOwner The address of the node's owner whose validator key and PoP will be changed. + /// @param _pubKey The new BLS12-381 public key to assign to the node's validator. + /// @param _pop The new proof-of-possession (PoP) to assign to the node's validator. + function changeValidatorKey( + address _nodeOwner, + BLS12_381PublicKey calldata _pubKey, + BLS12_381Signature calldata _pop + ) external onlyOwnerOrNodeOwner(_nodeOwner) { + _verifyInputBLS12_381PublicKey(_pubKey); + _verifyInputBLS12_381Signature(_pop); + _verifyNodeOwnerExists(_nodeOwner); + (Node storage node, bool deleted) = _getNodeAndDeleteIfRequired(_nodeOwner); + if (deleted) { + return; + } + + bytes32 prevHash = _hashValidatorPubKey(node.validatorLatest.pubKey); + delete validatorPubKeyHashes[prevHash]; + bytes32 newHash = _hashValidatorPubKey(_pubKey); + _verifyValidatorPubKeyDoesNotExist(newHash); + validatorPubKeyHashes[newHash] = true; + _ensureValidatorSnapshot(node); + node.validatorLatest.pubKey = _pubKey; + node.validatorLatest.proofOfPossession = _pop; + + emit NodeValidatorKeyChanged(_nodeOwner, _pubKey, _pop); + } + + /// @notice Changes the attester's public key of a node in the registry. + /// @dev Only callable by the contract owner or the node owner. + /// @dev Verifies that the node owner exists in the registry. + /// @param _nodeOwner The address of the node's owner whose attester public key will be changed. + /// @param _pubKey The new ECDSA public key to assign to the node's attester. + function changeAttesterKey( + address _nodeOwner, + Secp256k1PublicKey calldata _pubKey + ) external onlyOwnerOrNodeOwner(_nodeOwner) { + _verifyInputSecp256k1PublicKey(_pubKey); + _verifyNodeOwnerExists(_nodeOwner); + (Node storage node, bool deleted) = _getNodeAndDeleteIfRequired(_nodeOwner); + if (deleted) { + return; + } + + bytes32 prevHash = _hashAttesterPubKey(node.attesterLatest.pubKey); + delete attesterPubKeyHashes[prevHash]; + bytes32 newHash = _hashAttesterPubKey(_pubKey); + _verifyAttesterPubKeyDoesNotExist(newHash); + attesterPubKeyHashes[newHash] = true; + + _ensureAttesterSnapshot(node); + node.attesterLatest.pubKey = _pubKey; + + emit NodeAttesterKeyChanged(_nodeOwner, _pubKey); + } + + /// @notice Adds a new commit to the attester committee. + /// @dev Implicitly updates the attester committee by affecting readers based on the current state of a node's attester attributes: + /// - If "attestersCommit" > "node.attesterLastUpdateCommit", read "node.attesterLatest". + /// - If "attestersCommit" == "node.attesterLastUpdateCommit", read "node.attesterSnapshot". + /// @dev Only callable by the contract owner. + function commitAttesterCommittee() external onlyOwner { + ++attestersCommit; + + emit AttestersCommitted(attestersCommit); + } + + /// @notice Adds a new commit to the validator committee. + /// @dev Implicitly updates the validator committee by affecting readers based on the current state of a node's validator attributes: + /// - If "validatorsCommit" > "node.validatorLastUpdateCommit", read "node.validatorLatest". + /// - If "validatorsCommit" == "node.validatorLastUpdateCommit", read "node.validatorSnapshot". + /// @dev Only callable by the contract owner. + function commitValidatorCommittee() external onlyOwner { + ++validatorsCommit; + + emit ValidatorsCommitted(validatorsCommit); + } + + /// @notice Returns an array of `AttesterAttr` structs representing the current attester committee. + /// @dev Collects active and non-removed attesters based on the latest commit to the committee. + function getAttesterCommittee() public view returns (CommitteeAttester[] memory) { + uint256 len = nodeOwners.length; + CommitteeAttester[] memory committee = new CommitteeAttester[](len); + uint256 count = 0; + + for (uint256 i = 0; i < len; ++i) { + Node storage node = nodes[nodeOwners[i]]; + AttesterAttr memory attester = attestersCommit > node.attesterLastUpdateCommit + ? node.attesterLatest + : node.attesterSnapshot; + if (attester.active && !attester.removed) { + committee[count] = CommitteeAttester({weight: attester.weight, pubKey: attester.pubKey}); + ++count; + } + } + + // Resize the array. + assembly { + mstore(committee, count) + } + return committee; + } + + /// @notice Returns an array of `ValidatorAttr` structs representing the current attester committee. + /// @dev Collects active and non-removed validators based on the latest commit to the committee. + function getValidatorCommittee() public view returns (CommitteeValidator[] memory) { + uint256 len = nodeOwners.length; + CommitteeValidator[] memory committee = new CommitteeValidator[](len); + uint256 count = 0; + + for (uint256 i = 0; i < len; ++i) { + Node storage node = nodes[nodeOwners[i]]; + ValidatorAttr memory validator = validatorsCommit > node.validatorLastUpdateCommit + ? node.validatorLatest + : node.validatorSnapshot; + if (validator.active && !validator.removed) { + committee[count] = CommitteeValidator({ + weight: validator.weight, + pubKey: validator.pubKey, + proofOfPossession: validator.proofOfPossession + }); + ++count; + } + } + + // Resize the array. + assembly { + mstore(committee, count) + } + return committee; + } + + function numNodes() public view returns (uint256) { + return nodeOwners.length; + } + + function _getNodeAndDeleteIfRequired(address _nodeOwner) private returns (Node storage, bool) { + Node storage node = nodes[_nodeOwner]; + bool pendingDeletion = _isNodePendingDeletion(node); + if (pendingDeletion) { + _deleteNode(_nodeOwner, node); + } + return (node, pendingDeletion); + } + + function _isNodePendingDeletion(Node storage _node) private returns (bool) { + bool attesterRemoved = (attestersCommit > _node.attesterLastUpdateCommit) + ? _node.attesterLatest.removed + : _node.attesterSnapshot.removed; + bool validatorRemoved = (validatorsCommit > _node.validatorLastUpdateCommit) + ? _node.validatorLatest.removed + : _node.validatorSnapshot.removed; + return attesterRemoved && validatorRemoved; + } + + function _deleteNode(address _nodeOwner, Node storage _node) private { + // Delete from array by swapping the last node owner (gas-efficient, not preserving order). + address lastNodeOwner = nodeOwners[nodeOwners.length - 1]; + nodeOwners[_node.nodeOwnerIdx] = lastNodeOwner; + nodeOwners.pop(); + // Update the node owned by the last node owner. + nodes[lastNodeOwner].nodeOwnerIdx = _node.nodeOwnerIdx; + + // Delete from the remaining mapping. + delete attesterPubKeyHashes[_hashAttesterPubKey(_node.attesterLatest.pubKey)]; + delete validatorPubKeyHashes[_hashValidatorPubKey(_node.validatorLatest.pubKey)]; + delete nodes[_nodeOwner]; + + emit NodeDeleted(_nodeOwner); + } + + function _ensureAttesterSnapshot(Node storage _node) private { + if (_node.attesterLastUpdateCommit < attestersCommit) { + _node.attesterSnapshot = _node.attesterLatest; + _node.attesterLastUpdateCommit = attestersCommit; + } + } + + function _ensureValidatorSnapshot(Node storage _node) private { + if (_node.validatorLastUpdateCommit < validatorsCommit) { + _node.validatorSnapshot = _node.validatorLatest; + _node.validatorLastUpdateCommit = validatorsCommit; + } + } + + function _isNodeOwnerExists(address _nodeOwner) private view returns (bool) { + BLS12_381PublicKey storage pubKey = nodes[_nodeOwner].validatorLatest.pubKey; + if (pubKey.a == bytes32(0) && pubKey.b == bytes32(0) && pubKey.c == bytes32(0)) { + return false; + } + return true; + } + + function _verifyNodeOwnerExists(address _nodeOwner) private view { + if (!_isNodeOwnerExists(_nodeOwner)) { + revert NodeOwnerDoesNotExist(); + } + } + + function _verifyNodeOwnerDoesNotExist(address _nodeOwner) private view { + if (_isNodeOwnerExists(_nodeOwner)) { + revert NodeOwnerExists(); + } + } + + function _hashAttesterPubKey(Secp256k1PublicKey storage _pubKey) private view returns (bytes32) { + return keccak256(abi.encode(_pubKey.tag, _pubKey.x)); + } + + function _hashAttesterPubKey(Secp256k1PublicKey calldata _pubKey) private pure returns (bytes32) { + return keccak256(abi.encode(_pubKey.tag, _pubKey.x)); + } + + function _hashValidatorPubKey(BLS12_381PublicKey storage _pubKey) private view returns (bytes32) { + return keccak256(abi.encode(_pubKey.a, _pubKey.b, _pubKey.c)); + } + + function _hashValidatorPubKey(BLS12_381PublicKey calldata _pubKey) private pure returns (bytes32) { + return keccak256(abi.encode(_pubKey.a, _pubKey.b, _pubKey.c)); + } + + function _verifyInputAddress(address _nodeOwner) private pure { + if (_nodeOwner == address(0)) { + revert InvalidInputNodeOwnerAddress(); + } + } + + function _verifyAttesterPubKeyDoesNotExist(bytes32 _hash) private view { + if (attesterPubKeyHashes[_hash]) { + revert AttesterPubKeyExists(); + } + } + + function _verifyValidatorPubKeyDoesNotExist(bytes32 _hash) private { + if (validatorPubKeyHashes[_hash]) { + revert ValidatorPubKeyExists(); + } + } + + function _verifyInputBLS12_381PublicKey(BLS12_381PublicKey calldata _pubKey) private pure { + if (_isEmptyBLS12_381PublicKey(_pubKey)) { + revert InvalidInputBLS12_381PublicKey(); + } + } + + function _verifyInputBLS12_381Signature(BLS12_381Signature calldata _pop) private pure { + if (_isEmptyBLS12_381Signature(_pop)) { + revert InvalidInputBLS12_381Signature(); + } + } + + function _verifyInputSecp256k1PublicKey(Secp256k1PublicKey calldata _pubKey) private pure { + if (_isEmptySecp256k1PublicKey(_pubKey)) { + revert InvalidInputSecp256k1PublicKey(); + } + } + + function _isEmptyBLS12_381PublicKey(BLS12_381PublicKey calldata _pubKey) private pure returns (bool) { + return _pubKey.a == bytes32(0) && _pubKey.b == bytes32(0) && _pubKey.c == bytes32(0); + } + + function _isEmptyBLS12_381Signature(BLS12_381Signature calldata _pop) private pure returns (bool) { + return _pop.a == bytes32(0) && _pop.b == bytes16(0); + } + + function _isEmptySecp256k1PublicKey(Secp256k1PublicKey calldata _pubKey) private pure returns (bool) { + return _pubKey.tag == bytes1(0) && _pubKey.x == bytes32(0); + } +} diff --git a/l2-contracts/contracts/interfaces/IConsensusRegistry.sol b/l2-contracts/contracts/interfaces/IConsensusRegistry.sol new file mode 100644 index 000000000..e3ddd118a --- /dev/null +++ b/l2-contracts/contracts/interfaces/IConsensusRegistry.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.20; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +/// @title ConsensusRegistry contract interface +interface IConsensusRegistry { + /// @dev Represents a consensus node. + /// @param attesterLastUpdateCommit The latest `attestersCommit` where the node's attester attributes were updated. + /// @param attesterLatest Attester attributes to read if `node.attesterLastUpdateCommit` < `attestersCommit`. + /// @param attesterSnapshot Attester attributes to read if `node.attesterLastUpdateCommit` == `attestersCommit`. + /// @param validatorLastUpdateCommit The latest `validatorsCommit` where the node's validator attributes were updated. + /// @param validatorLatest Validator attributes to read if `node.validatorLastUpdateCommit` < `validatorsCommit`. + /// @param validatorSnapshot Validator attributes to read if `node.validatorLastUpdateCommit` == `validatorsCommit`. + /// @param nodeOwnerIdx Index of the node owner within the array of node owners. + struct Node { + uint32 attesterLastUpdateCommit; + uint32 validatorLastUpdateCommit; + uint32 nodeOwnerIdx; + AttesterAttr attesterLatest; + AttesterAttr attesterSnapshot; + ValidatorAttr validatorLatest; + ValidatorAttr validatorSnapshot; + } + + /// @dev Represents the attester attributes of a consensus node. + /// @param active A flag stating if the attester is active. + /// @param removed A flag stating if the attester has been removed (and is pending a deletion). + /// @param weight Attester's voting weight. + /// @param pubKey Attester's Secp256k1 public key. + struct AttesterAttr { + bool active; + bool removed; + uint32 weight; + Secp256k1PublicKey pubKey; + } + + /// @dev Represents an attester within a committee. + /// @param weight Attester's voting weight. + /// @param pubKey Attester's Secp256k1 public key. + struct CommitteeAttester { + uint32 weight; + Secp256k1PublicKey pubKey; + } + + /// @dev Represents the validator attributes of a consensus node. + /// @param active A flag stating if the validator is active. + /// @param removed A flag stating if the validator has been removed (and is pending a deletion). + /// @param weight Validator's voting weight. + /// @param pubKey Validator's BLS12-381 public key. + /// @param proofOfPossession Validator's Proof-of-possession (a signature over the public key). + struct ValidatorAttr { + bool active; + bool removed; + uint32 weight; + BLS12_381PublicKey pubKey; + BLS12_381Signature proofOfPossession; + } + + /// @dev Represents a validator within a committee. + /// @param weight Validator's voting weight. + /// @param pubKey Validator's BLS12-381 public key. + /// @param proofOfPossession Validator's Proof-of-possession (a signature over the public key). + struct CommitteeValidator { + uint32 weight; + BLS12_381PublicKey pubKey; + BLS12_381Signature proofOfPossession; + } + + /// @dev Represents BLS12_381 public key. + /// @param a First component of the BLS12-381 public key. + /// @param b Second component of the BLS12-381 public key. + /// @param c Third component of the BLS12-381 public key. + struct BLS12_381PublicKey { + bytes32 a; + bytes32 b; + bytes32 c; + } + + /// @dev Represents BLS12_381 signature. + /// @param a First component of the BLS12-381 signature. + /// @param b Second component of the BLS12-381 signature. + struct BLS12_381Signature { + bytes32 a; + bytes16 b; + } + + /// @dev Represents Secp256k1 public key. + /// @param tag Y-coordinate's even/odd indicator of the Secp256k1 public key. + /// @param x X-coordinate component of the Secp256k1 public key. + struct Secp256k1PublicKey { + bytes1 tag; + bytes32 x; + } + + error UnauthorizedOnlyOwnerOrNodeOwner(); + error NodeOwnerExists(); + error NodeOwnerDoesNotExist(); + error NodeOwnerNotFound(); + error ValidatorPubKeyExists(); + error AttesterPubKeyExists(); + error InvalidInputNodeOwnerAddress(); + error InvalidInputBLS12_381PublicKey(); + error InvalidInputBLS12_381Signature(); + error InvalidInputSecp256k1PublicKey(); + + event NodeAdded( + address indexed nodeOwner, + uint32 validatorWeight, + BLS12_381PublicKey validatorPubKey, + BLS12_381Signature validatorPoP, + uint32 attesterWeight, + Secp256k1PublicKey attesterPubKey + ); + event NodeDeactivated(address indexed nodeOwner); + event NodeActivated(address indexed nodeOwner); + event NodeRemoved(address indexed nodeOwner); + event NodeDeleted(address indexed nodeOwner); + event NodeValidatorWeightChanged(address indexed nodeOwner, uint32 newWeight); + event NodeAttesterWeightChanged(address indexed nodeOwner, uint32 newWeight); + event NodeValidatorKeyChanged(address indexed nodeOwner, BLS12_381PublicKey newPubKey, BLS12_381Signature newPoP); + event NodeAttesterKeyChanged(address indexed nodeOwner, Secp256k1PublicKey newPubKey); + event ValidatorsCommitted(uint32 commit); + event AttestersCommitted(uint32 commit); + + function add( + address _nodeOwner, + uint32 _validatorWeight, + BLS12_381PublicKey calldata _validatorPubKey, + BLS12_381Signature calldata _validatorPoP, + uint32 _attesterWeight, + Secp256k1PublicKey calldata _attesterPubKey + ) external; + + function deactivate(address _nodeOwner) external; + + function activate(address _nodeOwner) external; + + function remove(address _nodeOwner) external; + + function changeValidatorWeight(address _nodeOwner, uint32 _weight) external; + + function changeAttesterWeight(address _nodeOwner, uint32 _weight) external; + + function changeValidatorKey( + address _nodeOwner, + BLS12_381PublicKey calldata _pubKey, + BLS12_381Signature calldata _pop + ) external; + + function changeAttesterKey(address _nodeOwner, Secp256k1PublicKey calldata _pubKey) external; + + function commitAttesterCommittee() external; + + function commitValidatorCommittee() external; + + function getAttesterCommittee() external view returns (CommitteeAttester[] memory); + + function getValidatorCommittee() external view returns (CommitteeValidator[] memory); +} diff --git a/l2-contracts/package.json b/l2-contracts/package.json index 2f5907461..891b348a3 100644 --- a/l2-contracts/package.json +++ b/l2-contracts/package.json @@ -42,7 +42,8 @@ "deploy-l2-weth": "ts-node src/deploy-l2-weth.ts", "upgrade-bridge-contracts": "ts-node src/upgrade-bridge-impl.ts", "update-l2-erc20-metadata": "ts-node src/update-l2-erc20-metadata.ts", - "upgrade-consistency-checker": "ts-node src/upgrade-consistency-checker.ts" + "upgrade-consistency-checker": "ts-node src/upgrade-consistency-checker.ts", + "deploy-consensus-registry": "ts-node src/deploy-consensus-registry.ts" }, "dependencies": { "dotenv": "^16.0.3" diff --git a/l2-contracts/src/deploy-consensus-registry.ts b/l2-contracts/src/deploy-consensus-registry.ts new file mode 100644 index 000000000..ffbf903f9 --- /dev/null +++ b/l2-contracts/src/deploy-consensus-registry.ts @@ -0,0 +1,90 @@ +import { Command } from "commander"; +import { ethers } from "ethers"; +import { computeL2Create2Address, create2DeployFromL2 } from "./utils"; +import { Interface } from "ethers/lib/utils"; +import { ethTestConfig } from "./deploy-utils"; + +import * as hre from "hardhat"; +import { Provider, Wallet } from "zksync-ethers"; + +const I_TRANSPARENT_UPGRADEABLE_PROXY_ARTIFACT = hre.artifacts.readArtifactSync("ITransparentUpgradeableProxy"); +const TRANSPARENT_UPGRADEABLE_PROXY_ARTIFACT = hre.artifacts.readArtifactSync("TransparentUpgradeableProxy"); +const CONSENSUS_REGISTRY_ARTIFACT = hre.artifacts.readArtifactSync("ConsensusRegistry"); +const PROXY_ADMIN_ARTIFACT = hre.artifacts.readArtifactSync("ConsensusRegistry"); + +const CONSENSUS_REGISTRY_INTERFACE = new Interface(CONSENSUS_REGISTRY_ARTIFACT.abi); +const I_TRANSPARENT_UPGRADEABLE_PROXY_INTERFACE = new Interface(I_TRANSPARENT_UPGRADEABLE_PROXY_ARTIFACT.abi); + +// Script to deploy the consensus registry contract and output its address. +// Note, that this script expects that the L2 contracts have been compiled PRIOR +// to running this script. +async function main() { + const program = new Command(); + + program + .version("0.1.0") + .name("deploy-consensus-registry") + .description("Deploys the consensus registry contract to L2"); + + program.option("--private-key ").action(async (cmd) => { + const zksProvider = new Provider(process.env.API_WEB3_JSON_RPC_HTTP_URL); + const deployWallet = cmd.privateKey + ? new Wallet(cmd.privateKey, zksProvider) + : Wallet.fromMnemonic( + process.env.MNEMONIC ? process.env.MNEMONIC : ethTestConfig.mnemonic, + "m/44'/60'/0'/0/1" + ).connect(zksProvider); + console.log(`Using deployer wallet: ${deployWallet.address}`); + + // Deploy Consensus Registry contract + const consensusRegistryImplementation = await computeL2Create2Address( + deployWallet, + CONSENSUS_REGISTRY_ARTIFACT.bytecode, + "0x", + ethers.constants.HashZero + ); + await create2DeployFromL2(deployWallet, CONSENSUS_REGISTRY_ARTIFACT.bytecode, "0x", ethers.constants.HashZero); + + // Deploy Proxy Admin contract + const proxyAdminContract = await computeL2Create2Address( + deployWallet, + PROXY_ADMIN_ARTIFACT.bytecode, + "0x", + ethers.constants.HashZero + ); + await create2DeployFromL2(deployWallet, PROXY_ADMIN_ARTIFACT.bytecode, "0x", ethers.constants.HashZero); + + const proxyInitializationParams = CONSENSUS_REGISTRY_INTERFACE.encodeFunctionData("initialize", [ + deployWallet.address, + ]); + const proxyConstructor = I_TRANSPARENT_UPGRADEABLE_PROXY_INTERFACE.encodeDeploy([ + consensusRegistryImplementation, + proxyAdminContract, + proxyInitializationParams, + ]); + + await create2DeployFromL2( + deployWallet, + TRANSPARENT_UPGRADEABLE_PROXY_ARTIFACT.bytecode, + proxyConstructor, + ethers.constants.HashZero + ); + + const address = computeL2Create2Address( + deployWallet, + TRANSPARENT_UPGRADEABLE_PROXY_ARTIFACT.bytecode, + proxyConstructor, + ethers.constants.HashZero + ); + console.log(`CONTRACTS_L2_CONSENSUS_REGISTRY_ADDR=${address}`); + }); + + await program.parseAsync(process.argv); +} + +main() + .then(() => process.exit(0)) + .catch((err) => { + console.error("Error:", err); + process.exit(1); + }); diff --git a/l2-contracts/src/utils.ts b/l2-contracts/src/utils.ts index 30fe0876e..b4e7d5c1e 100644 --- a/l2-contracts/src/utils.ts +++ b/l2-contracts/src/utils.ts @@ -143,6 +143,27 @@ export async function create2DeployFromL1( ); } +export async function create2DeployFromL2( + wallet: ethers.Wallet, + bytecode: ethers.BytesLike, + constructor: ethers.BytesLike, + create2Salt: ethers.BytesLike, + extraFactoryDeps?: ethers.BytesLike[] +) { + const deployerSystemContracts = new Interface(artifacts.readArtifactSync("IContractDeployer").abi); + const bytecodeHash = hashL2Bytecode(bytecode); + const calldata = deployerSystemContracts.encodeFunctionData("create2", [create2Salt, bytecodeHash, constructor]); + + const factoryDeps = extraFactoryDeps ? [bytecode, ...extraFactoryDeps] : [bytecode]; + return await wallet.call({ + to: DEPLOYER_SYSTEM_CONTRACT_ADDRESS, + data: calldata, + customData: { + factoryDeps, + }, + }); +} + export async function publishBytecodeFromL1( chainId: ethers.BigNumberish, wallet: ethers.Wallet, diff --git a/l2-contracts/test/consensusRegistry.test.ts b/l2-contracts/test/consensusRegistry.test.ts new file mode 100644 index 000000000..66c0309bd --- /dev/null +++ b/l2-contracts/test/consensusRegistry.test.ts @@ -0,0 +1,499 @@ +import { Deployer } from "@matterlabs/hardhat-zksync-deploy"; +import * as hre from "hardhat"; +import { Provider, Wallet } from "zksync-ethers"; +import type { ConsensusRegistry } from "../typechain"; +import { ConsensusRegistryFactory } from "../typechain"; +import { expect } from "chai"; +import { ethers } from "ethers"; +import { Interface } from "ethers/lib/utils"; + +const richAccount = { + address: "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049", + privateKey: "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110", +}; + +const gasLimit = 100_000_000; + +const CONSENSUS_REGISTRY_ARTIFACT = hre.artifacts.readArtifactSync("ConsensusRegistry"); +const CONSENSUS_REGISTRY_INTERFACE = new Interface(CONSENSUS_REGISTRY_ARTIFACT.abi); + +describe("ConsensusRegistry", function () { + const provider = new Provider(hre.config.networks.localhost.url); + const owner = new Wallet(richAccount.privateKey, provider); + const nonOwner = new Wallet(Wallet.createRandom().privateKey, provider); + const nodes = []; + const nodeEntries = []; + let registry: ConsensusRegistry; + + before("Initialize", async function () { + // Deploy. + const deployer = new Deployer(hre, owner); + const registryInstance = await deployer.deploy(await deployer.loadArtifact("ConsensusRegistry"), []); + const proxyAdmin = await deployer.deploy(await deployer.loadArtifact("ProxyAdmin"), []); + const proxyInitializationParams = CONSENSUS_REGISTRY_INTERFACE.encodeFunctionData("initialize", [owner.address]); + const proxyInstance = await deployer.deploy(await deployer.loadArtifact("TransparentUpgradeableProxy"), [ + registryInstance.address, + proxyAdmin.address, + proxyInitializationParams, + ]); + registry = ConsensusRegistryFactory.connect(proxyInstance.address, owner); + + // Fund nonOwner. + await ( + await owner.sendTransaction({ + to: nonOwner.address, + value: ethers.utils.parseEther("100"), + }) + ).wait(); + + // Prepare the node list. + const numNodes = 10; + for (let i = 0; i < numNodes; i++) { + const node = makeRandomNode(provider); + const nodeEntry = makeRandomNodeEntry(node, i); + nodes.push(node); + nodeEntries.push(nodeEntry); + } + + // Fund the first node owner. + await ( + await owner.sendTransaction({ + to: nodes[0].ownerKey.address, + value: ethers.utils.parseEther("100"), + }) + ).wait(); + }); + + it("Should set the owner as provided in constructor", async function () { + expect(await registry.owner()).to.equal(owner.address); + }); + + it("Should add nodes to both registries", async function () { + for (let i = 0; i < nodes.length; i++) { + await ( + await registry.add( + nodeEntries[i].ownerAddr, + nodeEntries[i].validatorWeight, + nodeEntries[i].validatorPubKey, + nodeEntries[i].validatorPoP, + nodeEntries[i].attesterWeight, + nodeEntries[i].attesterPubKey + ) + ).wait(); + } + + expect(await registry.numNodes()).to.equal(nodes.length); + + for (let i = 0; i < nodes.length; i++) { + const nodeOwner = await registry.nodeOwners(i); + expect(nodeOwner).to.equal(nodeEntries[i].ownerAddr); + const node = await registry.nodes(nodeOwner); + expect(node.attesterLastUpdateCommit).to.equal(0); + expect(node.validatorLastUpdateCommit).to.equal(0); + + // 'Latest' is expected to match the added node's attributes. + expect(node.attesterLatest.active).to.equal(true); + expect(node.attesterLatest.removed).to.equal(false); + expect(node.attesterLatest.weight).to.equal(nodeEntries[i].attesterWeight); + expect(node.attesterLatest.pubKey.tag).to.equal(nodeEntries[i].attesterPubKey.tag); + expect(node.attesterLatest.pubKey.x).to.equal(nodeEntries[i].attesterPubKey.x); + expect(node.validatorLastUpdateCommit).to.equal(0); + expect(node.validatorLatest.active).to.equal(true); + expect(node.validatorLatest.removed).to.equal(false); + expect(node.validatorLatest.weight).to.equal(nodeEntries[i].attesterWeight); + expect(node.validatorLatest.pubKey.a).to.equal(nodeEntries[i].validatorPubKey.a); + expect(node.validatorLatest.pubKey.b).to.equal(nodeEntries[i].validatorPubKey.b); + expect(node.validatorLatest.pubKey.c).to.equal(nodeEntries[i].validatorPubKey.c); + expect(node.validatorLatest.proofOfPossession.a).to.equal(nodeEntries[i].validatorPoP.a); + expect(node.validatorLatest.proofOfPossession.b).to.equal(nodeEntries[i].validatorPoP.b); + + // 'Snapshot' is expected to have zero values. + expect(node.attesterSnapshot.active).to.equal(false); + expect(node.attesterSnapshot.removed).to.equal(false); + expect(node.attesterSnapshot.weight).to.equal(0); + expect(ethers.utils.arrayify(node.attesterSnapshot.pubKey.tag)).to.deep.equal(new Uint8Array(1)); + expect(ethers.utils.arrayify(node.attesterSnapshot.pubKey.x)).to.deep.equal(new Uint8Array(32)); + expect(node.validatorSnapshot.active).to.equal(false); + expect(node.validatorSnapshot.removed).to.equal(false); + expect(node.validatorSnapshot.weight).to.equal(0); + expect(ethers.utils.arrayify(node.validatorSnapshot.pubKey.a)).to.deep.equal(new Uint8Array(32)); + expect(ethers.utils.arrayify(node.validatorSnapshot.pubKey.b)).to.deep.equal(new Uint8Array(32)); + expect(ethers.utils.arrayify(node.validatorSnapshot.pubKey.c)).to.deep.equal(new Uint8Array(32)); + expect(ethers.utils.arrayify(node.validatorSnapshot.proofOfPossession.a)).to.deep.equal(new Uint8Array(32)); + expect(ethers.utils.arrayify(node.validatorSnapshot.proofOfPossession.b)).to.deep.equal(new Uint8Array(16)); + } + }); + + it("Should not allow nonOwner to add", async function () { + await expect( + registry + .connect(nonOwner) + .add( + ethers.Wallet.createRandom().address, + 0, + { a: new Uint8Array(32), b: new Uint8Array(32), c: new Uint8Array(32) }, + { a: new Uint8Array(32), b: new Uint8Array(16) }, + 0, + { tag: new Uint8Array(1), x: new Uint8Array(32) }, + { gasLimit } + ) + ).to.be.reverted; + }); + + it("Should allow owner to deactivate", async function () { + const nodeOwner = nodeEntries[0].ownerAddr; + expect((await registry.nodes(nodeOwner)).validatorLatest.active).to.equal(true); + + await (await registry.connect(owner).deactivate(nodeOwner, { gasLimit })).wait(); + expect((await registry.nodes(nodeOwner)).validatorLatest.active).to.equal(false); + + // Restore state. + await (await registry.connect(owner).activate(nodeOwner, { gasLimit })).wait(); + }); + + it("Should not allow nonOwner, nonNodeOwner to deactivate", async function () { + const nodeOwner = nodeEntries[0].ownerAddr; + await expect(registry.connect(nonOwner).deactivate(nodeOwner, { gasLimit })).to.be.reverted; + }); + + it("Should change validator weight", async function () { + const entry = nodeEntries[0]; + expect((await registry.nodes(entry.ownerAddr)).validatorLatest.weight).to.equal(entry.validatorWeight); + + const baseWeight = entry.validatorWeight; + const newWeight = getRandomNumber(100, 1000); + await (await registry.changeValidatorWeight(entry.ownerAddr, newWeight, { gasLimit })).wait(); + expect((await registry.nodes(entry.ownerAddr)).validatorLatest.weight).to.equal(newWeight); + expect((await registry.nodes(entry.ownerAddr)).attesterLatest.weight).to.equal(entry.attesterWeight); + + // Restore state. + await (await registry.changeValidatorWeight(entry.ownerAddr, baseWeight, { gasLimit })).wait(); + }); + + it("Should not allow nodeOwner to change validator weight", async function () { + const node = nodes[0]; + await expect(registry.connect(node.ownerKey).changeValidatorWeight(node.ownerKey.address, 0, { gasLimit })).to.be + .reverted; + }); + + it("Should not allow nonOwner to change validator weight", async function () { + const node = nodes[0]; + await expect(registry.connect(nonOwner).changeValidatorWeight(node.ownerKey.address, 0, { gasLimit })).to.be + .reverted; + }); + + it("Should change attester weight", async function () { + const entry = nodeEntries[0]; + expect((await registry.nodes(entry.ownerAddr)).attesterLatest.weight).to.equal(entry.attesterWeight); + + const baseWeight = entry.attesterWeight; + const newWeight = getRandomNumber(100, 1000); + await (await registry.changeAttesterWeight(entry.ownerAddr, newWeight, { gasLimit })).wait(); + expect((await registry.nodes(entry.ownerAddr)).attesterLatest.weight).to.equal(newWeight); + expect((await registry.nodes(entry.ownerAddr)).validatorLatest.weight).to.equal(entry.validatorWeight); + + // Restore state. + await (await registry.changeAttesterWeight(entry.ownerAddr, baseWeight, { gasLimit })).wait(); + }); + + it("Should not allow nodeOwner to change attester weight", async function () { + const node = nodes[0]; + await expect(registry.connect(node.ownerKey).changeAttesterWeight(node.ownerKey.address, 0, { gasLimit })).to.be + .reverted; + }); + + it("Should not allow nonOwner to change attester weight", async function () { + const node = nodes[0]; + await expect(registry.connect(nonOwner).changeAttesterWeight(node.ownerKey.address, 0, { gasLimit })).to.be + .reverted; + }); + + it("Should not allow to add a node with a validator public key which already exist", async function () { + const newEntry = makeRandomNodeEntry(makeRandomNode(), 0); + await expect( + registry.add( + newEntry.ownerAddr, + newEntry.validatorWeight, + nodeEntries[0].validatorPubKey, + newEntry.validatorPoP, + newEntry.attesterWeight, + newEntry.attesterPubKey, + { gasLimit } + ) + ).to.be.reverted; + }); + + it("Should not allow to add a node with an attester public key which already exist", async function () { + const newEntry = makeRandomNodeEntry(makeRandomNode(), 0); + await expect( + registry.add( + newEntry.ownerAddr, + newEntry.validatorWeight, + newEntry.validatorPubKey, + newEntry.validatorPoP, + newEntry.attesterWeight, + nodeEntries[0].attesterPubKey, + { gasLimit } + ) + ).to.be.reverted; + }); + + it("Should return attester committee once committed to", async function () { + // Verify that committee was not committed to. + expect((await registry.getAttesterCommittee()).length).to.equal(0); + + // Commit. + await (await registry.commitAttesterCommittee({ gasLimit })).wait(); + + // Read committee. + const attesterCommittee = await registry.getAttesterCommittee(); + expect(attesterCommittee.length).to.equal(nodes.length); + for (let i = 0; i < attesterCommittee.length; i++) { + const entry = nodeEntries[i]; + const attester = attesterCommittee[i]; + expect(attester.weight).to.equal(entry.attesterWeight); + expect(attester.pubKey.tag).to.equal(entry.attesterPubKey.tag); + expect(attester.pubKey.x).to.equal(entry.attesterPubKey.x); + } + }); + + it("Should return validator committee once committed to", async function () { + // Verify that committee was not committed to. + expect((await registry.getValidatorCommittee()).length).to.equal(0); + + // Commit. + await (await registry.commitValidatorCommittee({ gasLimit })).wait(); + + // Read committee. + const validatorCommittee = await registry.getValidatorCommittee(); + expect(validatorCommittee.length).to.equal(nodes.length); + for (let i = 0; i < validatorCommittee.length; i++) { + const entry = nodeEntries[i]; + const validator = validatorCommittee[i]; + expect(validator.weight).to.equal(entry.validatorWeight); + expect(validator.pubKey.a).to.equal(entry.validatorPubKey.a); + expect(validator.pubKey.b).to.equal(entry.validatorPubKey.b); + expect(validator.pubKey.c).to.equal(entry.validatorPubKey.c); + expect(validator.proofOfPossession.a).to.equal(entry.validatorPoP.a); + expect(validator.proofOfPossession.b).to.equal(entry.validatorPoP.b); + } + }); + + it("Should not include inactive nodes in attester and validator committees when committed to", async function () { + const idx = nodeEntries.length - 1; + const entry = nodeEntries[idx]; + + // Deactivate attribute. + await (await registry.deactivate(entry.ownerAddr, { gasLimit })).wait(); + + // Verify no change. + expect((await registry.getAttesterCommittee()).length).to.equal(nodes.length); + expect((await registry.getValidatorCommittee()).length).to.equal(nodes.length); + + // Commit attester committee and verify. + await (await registry.commitAttesterCommittee({ gasLimit })).wait(); + expect((await registry.getAttesterCommittee()).length).to.equal(nodes.length - 1); + expect((await registry.getValidatorCommittee()).length).to.equal(nodes.length); + + // Commit validator committee and verify. + await (await registry.commitValidatorCommittee({ gasLimit })).wait(); + expect((await registry.getAttesterCommittee()).length).to.equal(nodes.length - 1); + expect((await registry.getValidatorCommittee()).length).to.equal(nodes.length - 1); + + // Restore state. + await (await registry.activate(entry.ownerAddr, { gasLimit })).wait(); + await (await registry.commitAttesterCommittee({ gasLimit })).wait(); + await (await registry.commitValidatorCommittee({ gasLimit })).wait(); + }); + + it("Should not include removed nodes in attester and validator committees when committed to", async function () { + const idx = nodeEntries.length - 1; + const entry = nodeEntries[idx]; + + // Remove node. + await (await registry.remove(entry.ownerAddr, { gasLimit })).wait(); + + // Verify no change. + expect((await registry.getAttesterCommittee()).length).to.equal(nodes.length); + expect((await registry.getValidatorCommittee()).length).to.equal(nodes.length); + + // Commit attester committee and verify. + await (await registry.commitAttesterCommittee({ gasLimit })).wait(); + expect((await registry.getAttesterCommittee()).length).to.equal(nodes.length - 1); + expect((await registry.getValidatorCommittee()).length).to.equal(nodes.length); + + // Commit validator committee and verify. + await (await registry.commitValidatorCommittee({ gasLimit })).wait(); + expect((await registry.getAttesterCommittee()).length).to.equal(nodes.length - 1); + expect((await registry.getValidatorCommittee()).length).to.equal(nodes.length - 1); + + // Restore state. + await (await registry.remove(entry.ownerAddr, { gasLimit })).wait(); + await ( + await registry.add( + entry.ownerAddr, + entry.validatorWeight, + entry.validatorPubKey, + entry.validatorPoP, + entry.attesterWeight, + entry.attesterPubKey + ) + ).wait(); + await (await registry.commitAttesterCommittee({ gasLimit })).wait(); + await (await registry.commitValidatorCommittee({ gasLimit })).wait(); + }); + + it("Should not include node attribute change in attester committee before committed to", async function () { + const idx = nodeEntries.length - 1; + const entry = nodeEntries[idx]; + + // Change attribute. + await (await registry.changeAttesterWeight(entry.ownerAddr, entry.attesterWeight + 1, { gasLimit })).wait(); + + // Verify no change. + const attester = (await registry.getAttesterCommittee())[idx]; + expect(attester.weight).to.equal(entry.attesterWeight); + + // Commit. + await (await registry.commitAttesterCommittee({ gasLimit })).wait(); + + // Verify change. + const committedAttester = (await registry.getAttesterCommittee())[idx]; + expect(committedAttester.weight).to.equal(entry.attesterWeight + 1); + + // Restore state. + await (await registry.changeAttesterWeight(entry.ownerAddr, entry.attesterWeight, { gasLimit })).wait(); + await (await registry.commitAttesterCommittee({ gasLimit })).wait(); + }); + + it("Should not include node attribute change in validator committee before committed to", async function () { + const idx = nodeEntries.length - 1; + const entry = nodeEntries[idx]; + + // Change attribute. + await (await registry.changeValidatorWeight(entry.ownerAddr, entry.attesterWeight + 1, { gasLimit })).wait(); + + // Verify no change. + const validator = (await registry.getValidatorCommittee())[idx]; + expect(validator.weight).to.equal(entry.validatorWeight); + + // Commit. + await (await registry.commitValidatorCommittee({ gasLimit })).wait(); + + // Verify change. + const committedValidator = (await registry.getValidatorCommittee())[idx]; + expect(committedValidator.weight).to.equal(entry.validatorWeight + 1); + + // Restore state. + await (await registry.changeValidatorWeight(entry.ownerAddr, entry.validatorWeight, { gasLimit })).wait(); + await (await registry.commitValidatorCommittee({ gasLimit })).wait(); + }); + + it("Should finalize node removal by fully deleting it from storage", async function () { + const idx = nodeEntries.length - 1; + const entry = nodeEntries[idx]; + + // Remove. + expect((await registry.nodes(entry.ownerAddr)).attesterLatest.removed).to.equal(false); + expect((await registry.nodes(entry.ownerAddr)).validatorLatest.removed).to.equal(false); + await (await registry.remove(entry.ownerAddr, { gasLimit })).wait(); + expect((await registry.nodes(entry.ownerAddr)).attesterLatest.removed).to.equal(true); + expect((await registry.nodes(entry.ownerAddr)).validatorLatest.removed).to.equal(true); + + // Commit committees. + await (await registry.commitAttesterCommittee({ gasLimit })).wait(); + await (await registry.commitValidatorCommittee({ gasLimit })).wait(); + + // Verify node was not yet deleted. + expect(await registry.numNodes()).to.equal(nodes.length); + const attesterPubKeyHash = hashAttesterPubKey(entry.attesterPubKey); + expect(await registry.attesterPubKeyHashes(attesterPubKeyHash)).to.be.equal(true); + const validatorPubKeyHash = hashValidatorPubKey(entry.validatorPubKey); + expect(await registry.validatorPubKeyHashes(validatorPubKeyHash)).to.be.equal(true); + + // Trigger node deletion. + await (await registry.remove(entry.ownerAddr, { gasLimit })).wait(); + + // Verify the deletion. + expect(await registry.numNodes()).to.equal(nodes.length - 1); + expect(await registry.attesterPubKeyHashes(attesterPubKeyHash)).to.be.equal(false); + expect(await registry.validatorPubKeyHashes(attesterPubKeyHash)).to.be.equal(false); + const node = await registry.nodes(entry.ownerAddr, { gasLimit }); + expect(ethers.utils.arrayify(node.attesterLatest.pubKey.tag)).to.deep.equal(new Uint8Array(1)); + expect(ethers.utils.arrayify(node.attesterLatest.pubKey.x)).to.deep.equal(new Uint8Array(32)); + + // Restore state. + await ( + await registry.add( + entry.ownerAddr, + entry.validatorWeight, + entry.validatorPubKey, + entry.validatorPoP, + entry.attesterWeight, + entry.attesterPubKey + ) + ).wait(); + await (await registry.commitAttesterCommittee({ gasLimit })).wait(); + await (await registry.commitValidatorCommittee({ gasLimit })).wait(); + }); + + function makeRandomNode() { + return { + ownerKey: new Wallet(Wallet.createRandom().privateKey, provider), + validatorKey: Wallet.createRandom(), + attesterKey: Wallet.createRandom(), + }; + } + + function makeRandomNodeEntry(node, weight: number) { + return { + ownerAddr: node.ownerKey.address, + validatorWeight: weight, + validatorPubKey: getRandomValidatorPubKey(), + validatorPoP: getRandomValidatorPoP(), + attesterWeight: weight, + attesterPubKey: getRandomAttesterPubKey(), + }; + } +}); + +function getRandomNumber(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function getRandomValidatorPubKey() { + return { + a: ethers.utils.hexlify(ethers.utils.randomBytes(32)), + b: ethers.utils.hexlify(ethers.utils.randomBytes(32)), + c: ethers.utils.hexlify(ethers.utils.randomBytes(32)), + }; +} + +function getRandomValidatorPoP() { + return { + a: ethers.utils.hexlify(ethers.utils.randomBytes(32)), + b: ethers.utils.hexlify(ethers.utils.randomBytes(16)), + }; +} + +function getRandomAttesterPubKey() { + return { + tag: ethers.utils.hexlify(ethers.utils.randomBytes(1)), + x: ethers.utils.hexlify(ethers.utils.randomBytes(32)), + }; +} + +function hashAttesterPubKey(attesterPubKey) { + return ethers.utils.keccak256( + ethers.utils.defaultAbiCoder.encode(["bytes1", "bytes32"], [attesterPubKey.tag, attesterPubKey.x]) + ); +} + +function hashValidatorPubKey(validatorPubKey) { + return ethers.utils.keccak256( + ethers.utils.defaultAbiCoder.encode( + ["bytes32", "bytes32", "bytes32"], + [validatorPubKey.a, validatorPubKey.b, validatorPubKey.c] + ) + ); +} From 446d391d34bdb48255d5f8fef8a8248925fc98b9 Mon Sep 17 00:00:00 2001 From: Stanislav Bezkorovainyi Date: Wed, 21 Aug 2024 18:43:27 +0200 Subject: [PATCH 03/13] Add admin role to shared bridge (#727) --- .../contracts/bridge/L1SharedBridge.sol | 53 ++++++++++++++++++- .../bridge/interfaces/IL1SharedBridge.sol | 15 ++++++ .../L1SharedBridge/L1SharedBridgeAdmin.t.sol | 26 +++++++++ .../L1SharedBridge/L1SharedBridgeFails.t.sol | 6 +-- .../_L1SharedBridge_Shared.t.sol | 8 ++- 5 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 l1-contracts/test/foundry/unit/concrete/Bridges/L1SharedBridge/L1SharedBridgeAdmin.t.sol diff --git a/l1-contracts/contracts/bridge/L1SharedBridge.sol b/l1-contracts/contracts/bridge/L1SharedBridge.sol index f74a771b0..42058fe33 100644 --- a/l1-contracts/contracts/bridge/L1SharedBridge.sol +++ b/l1-contracts/contracts/bridge/L1SharedBridge.sol @@ -89,6 +89,12 @@ contract L1SharedBridge is IL1SharedBridge, ReentrancyGuard, Ownable2StepUpgrade /// NOTE: this function may be removed in the future, don't rely on it! mapping(uint256 chainId => mapping(address l1Token => uint256 balance)) public chainBalance; + /// @dev Admin has the ability to register new chains within the shared bridge. + address public admin; + + /// @dev The pending admin, i.e. the candidate to the admin role. + address public pendingAdmin; + /// @notice Checks that the message sender is the bridgehub. modifier onlyBridgehub() { require(msg.sender == address(BRIDGE_HUB), "ShB not BH"); @@ -116,6 +122,12 @@ contract L1SharedBridge is IL1SharedBridge, ReentrancyGuard, Ownable2StepUpgrade _; } + /// @notice Checks that the message sender is either the owner or admin. + modifier onlyOwnerOrAdmin() { + require(msg.sender == owner() || msg.sender == admin, "ShB not owner or admin"); + _; + } + /// @dev Contract is expected to be used as proxy implementation. /// @dev Initialize the implementation to prevent Parity hack. constructor( @@ -139,6 +151,31 @@ contract L1SharedBridge is IL1SharedBridge, ReentrancyGuard, Ownable2StepUpgrade _transferOwnership(_owner); } + /// @inheritdoc IL1SharedBridge + /// @dev Please note, if the owner wants to enforce the admin change it must execute both `setPendingAdmin` and + /// `acceptAdmin` atomically. Otherwise `admin` can set different pending admin and so fail to accept the admin rights. + function setPendingAdmin(address _newPendingAdmin) external onlyOwnerOrAdmin { + // Save previous value into the stack to put it into the event later + address oldPendingAdmin = pendingAdmin; + // Change pending admin + pendingAdmin = _newPendingAdmin; + emit NewPendingAdmin(oldPendingAdmin, _newPendingAdmin); + } + + /// @inheritdoc IL1SharedBridge + /// @notice Accepts transfer of admin rights. Only pending admin can accept the role. + function acceptAdmin() external { + address currentPendingAdmin = pendingAdmin; + require(msg.sender == currentPendingAdmin, "ShB not pending admin"); // Only proposed by current admin address can claim the admin rights + + address previousAdmin = admin; + admin = currentPendingAdmin; + delete pendingAdmin; + + emit NewPendingAdmin(currentPendingAdmin, address(0)); + emit NewAdmin(previousAdmin, currentPendingAdmin); + } + /// @dev This sets the first post diamond upgrade batch for era, used to check old eth withdrawals /// @param _eraPostDiamondUpgradeFirstBatch The first batch number on the zkSync Era Diamond Proxy that was settled after diamond proxy upgrade. function setEraPostDiamondUpgradeFirstBatch(uint256 _eraPostDiamondUpgradeFirstBatch) external onlyOwner { @@ -207,7 +244,21 @@ contract L1SharedBridge is IL1SharedBridge, ReentrancyGuard, Ownable2StepUpgrade } /// @dev Initializes the l2Bridge address by governance for a specific chain. - function initializeChainGovernance(uint256 _chainId, address _l2BridgeAddress) external onlyOwner { + /// @param _chainId The chain ID for which the l2Bridge address is being initialized. + /// @param _l2BridgeAddress The address of the L2 bridge contract. + function initializeChainGovernance(uint256 _chainId, address _l2BridgeAddress) external onlyOwnerOrAdmin { + require(l2BridgeAddress[_chainId] == address(0), "ShB: l2 bridge already set"); + require(_l2BridgeAddress != address(0), "ShB: l2 bridge 0"); + l2BridgeAddress[_chainId] = _l2BridgeAddress; + } + + /// @dev Reinitializes the l2Bridge address by governance for a specific chain. + /// @dev Only accessible to the owner of the bridge to prevent malicious admin from changing the bridge address for + /// an existing chain. + /// @param _chainId The chain ID for which the l2Bridge address is being initialized. + /// @param _l2BridgeAddress The address of the L2 bridge contract. + function reinitializeChainGovernance(uint256 _chainId, address _l2BridgeAddress) external onlyOwner { + require(l2BridgeAddress[_chainId] != address(0), "ShB: l2 bridge not yet set"); l2BridgeAddress[_chainId] = _l2BridgeAddress; } diff --git a/l1-contracts/contracts/bridge/interfaces/IL1SharedBridge.sol b/l1-contracts/contracts/bridge/interfaces/IL1SharedBridge.sol index 45db2d02a..ef7e74165 100644 --- a/l1-contracts/contracts/bridge/interfaces/IL1SharedBridge.sol +++ b/l1-contracts/contracts/bridge/interfaces/IL1SharedBridge.sol @@ -10,6 +10,13 @@ import {IL1ERC20Bridge} from "./IL1ERC20Bridge.sol"; /// @author Matter Labs /// @custom:security-contact security@matterlabs.dev interface IL1SharedBridge { + /// @notice pendingAdmin is changed + /// @dev Also emitted when new admin is accepted and in this case, `newPendingAdmin` would be zero address + event NewPendingAdmin(address indexed oldPendingAdmin, address indexed newPendingAdmin); + + /// @notice Admin changed + event NewAdmin(address indexed oldAdmin, address indexed newAdmin); + event LegacyDepositInitiated( uint256 indexed chainId, bytes32 indexed l2DepositTxHash, @@ -151,4 +158,12 @@ interface IL1SharedBridge { function bridgehubConfirmL2Transaction(uint256 _chainId, bytes32 _txDataHash, bytes32 _txHash) external; function receiveEth(uint256 _chainId) external payable; + + /// @notice Starts the transfer of admin rights. Only the current admin can propose a new pending one. + /// @notice New admin can accept admin rights by calling `acceptAdmin` function. + /// @param _newPendingAdmin Address of the new admin + function setPendingAdmin(address _newPendingAdmin) external; + + /// @notice Accepts transfer of admin rights. Only pending admin can accept the role. + function acceptAdmin() external; } diff --git a/l1-contracts/test/foundry/unit/concrete/Bridges/L1SharedBridge/L1SharedBridgeAdmin.t.sol b/l1-contracts/test/foundry/unit/concrete/Bridges/L1SharedBridge/L1SharedBridgeAdmin.t.sol new file mode 100644 index 000000000..af97e3ed2 --- /dev/null +++ b/l1-contracts/test/foundry/unit/concrete/Bridges/L1SharedBridge/L1SharedBridgeAdmin.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {L1SharedBridgeTest} from "./_L1SharedBridge_Shared.t.sol"; + +/// We are testing all the specified revert and require cases. +contract L1SharedBridgeAdminTest is L1SharedBridgeTest { + uint256 internal randomChainId = 123456; + + function testAdminCanInitializeChainGovernance() public { + address randomL2Bridge = makeAddr("randomL2Bridge"); + + vm.prank(admin); + sharedBridge.initializeChainGovernance(randomChainId, randomL2Bridge); + + assertEq(sharedBridge.l2BridgeAddress(randomChainId), randomL2Bridge); + } + + function testAdminCanNotReinitializeChainGovernance() public { + address randomNewBridge = makeAddr("randomNewBridge"); + + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(admin); + sharedBridge.reinitializeChainGovernance(randomChainId, randomNewBridge); + } +} diff --git a/l1-contracts/test/foundry/unit/concrete/Bridges/L1SharedBridge/L1SharedBridgeFails.t.sol b/l1-contracts/test/foundry/unit/concrete/Bridges/L1SharedBridge/L1SharedBridgeFails.t.sol index 15ef94080..97bbe2ec2 100644 --- a/l1-contracts/test/foundry/unit/concrete/Bridges/L1SharedBridge/L1SharedBridgeFails.t.sol +++ b/l1-contracts/test/foundry/unit/concrete/Bridges/L1SharedBridge/L1SharedBridgeFails.t.sol @@ -21,7 +21,7 @@ contract L1SharedBridgeFailTest is L1SharedBridgeTest { vm.expectRevert("ShB owner 0"); new TransparentUpgradeableProxy( address(sharedBridgeImpl), - admin, + proxyAdmin, // solhint-disable-next-line func-named-parameters abi.encodeWithSelector(L1SharedBridge.initialize.selector, address(0), eraPostUpgradeFirstBatch) ); @@ -59,7 +59,7 @@ contract L1SharedBridgeFailTest is L1SharedBridgeTest { function test_bridgehubDeposit_Eth_l2BridgeNotDeployed() public { vm.prank(owner); - sharedBridge.initializeChainGovernance(chainId, address(0)); + sharedBridge.reinitializeChainGovernance(chainId, address(0)); vm.deal(bridgehubAddress, amount); vm.prank(bridgehubAddress); vm.mockCall( @@ -551,7 +551,7 @@ contract L1SharedBridgeFailTest is L1SharedBridgeTest { address refundRecipient = address(0); vm.prank(owner); - sharedBridge.initializeChainGovernance(eraChainId, address(0)); + sharedBridge.reinitializeChainGovernance(eraChainId, address(0)); vm.expectRevert("ShB b. n dep"); vm.prank(l1ERC20BridgeAddress); diff --git a/l1-contracts/test/foundry/unit/concrete/Bridges/L1SharedBridge/_L1SharedBridge_Shared.t.sol b/l1-contracts/test/foundry/unit/concrete/Bridges/L1SharedBridge/_L1SharedBridge_Shared.t.sol index cad8482fb..4554e4a36 100644 --- a/l1-contracts/test/foundry/unit/concrete/Bridges/L1SharedBridge/_L1SharedBridge_Shared.t.sol +++ b/l1-contracts/test/foundry/unit/concrete/Bridges/L1SharedBridge/_L1SharedBridge_Shared.t.sol @@ -69,6 +69,7 @@ contract L1SharedBridgeTest is Test { address owner; address admin; + address proxyAdmin; address zkSync; address alice; address bob; @@ -90,6 +91,7 @@ contract L1SharedBridgeTest is Test { function setUp() public { owner = makeAddr("owner"); admin = makeAddr("admin"); + proxyAdmin = makeAddr("proxyAdmin"); // zkSync = makeAddr("zkSync"); bridgehubAddress = makeAddr("bridgehub"); alice = makeAddr("alice"); @@ -119,7 +121,7 @@ contract L1SharedBridgeTest is Test { }); TransparentUpgradeableProxy sharedBridgeProxy = new TransparentUpgradeableProxy( address(sharedBridgeImpl), - admin, + proxyAdmin, abi.encodeWithSelector(L1SharedBridge.initialize.selector, owner) ); sharedBridge = L1SharedBridge(payable(sharedBridgeProxy)); @@ -135,6 +137,10 @@ contract L1SharedBridgeTest is Test { sharedBridge.initializeChainGovernance(chainId, l2SharedBridge); vm.prank(owner); sharedBridge.initializeChainGovernance(eraChainId, l2SharedBridge); + vm.prank(owner); + sharedBridge.setPendingAdmin(admin); + vm.prank(admin); + sharedBridge.acceptAdmin(); } function _setSharedBridgeDepositHappened(uint256 _chainId, bytes32 _txHash, bytes32 _txDataHash) internal { From d3687694f71d83fa286b9c186b4c3ea173028f83 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Mon, 2 Sep 2024 14:53:58 +0100 Subject: [PATCH 04/13] feat: Deploy ConsensusRegistry through L1 to L2 transaction (BFT-504) (#735) --- .../config-deploy-l2-config.toml | 1 + .../deploy-scripts/DeployL2Contracts.sol | 79 ++++++++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/l1-contracts/deploy-script-config-template/config-deploy-l2-config.toml b/l1-contracts/deploy-script-config-template/config-deploy-l2-config.toml index 67e46ae38..fae9cc907 100644 --- a/l1-contracts/deploy-script-config-template/config-deploy-l2-config.toml +++ b/l1-contracts/deploy-script-config-template/config-deploy-l2-config.toml @@ -4,3 +4,4 @@ l1_shared_bridge = "0x2ae37d8130b82c7e79b3863a39027178e073eedb" bridgehub = "0xea785a9c91a07ed69b83eb165f4ce2c30ecb4c0b" governance = "0x6a08d69675af7755569a1a25ef37e795493473a1" erc20_bridge = "0x84fbda16bd5f2d66d7fbaec5e8d816e7b7014595" +consensus_registry_owner = "0xD64e136566a9E04eb05B30184fF577F52682D182" diff --git a/l1-contracts/deploy-scripts/DeployL2Contracts.sol b/l1-contracts/deploy-scripts/DeployL2Contracts.sol index 906ae831c..b22c091ae 100644 --- a/l1-contracts/deploy-scripts/DeployL2Contracts.sol +++ b/l1-contracts/deploy-scripts/DeployL2Contracts.sol @@ -19,10 +19,15 @@ contract DeployL2Script is Script { address l1SharedBridgeProxy; address governance; address erc20BridgeProxy; + // The owner of the contract sets the validator/attester weights. + // Can be the developer multisig wallet on mainnet. + address consensusRegistryOwner; uint256 chainId; uint256 eraChainId; address l2SharedBridgeImplementation; address l2SharedBridgeProxy; + address consensusRegistryImplementation; + address consensusRegistryProxy; address forceDeployUpgraderAddress; } @@ -32,6 +37,8 @@ contract DeployL2Script is Script { bytes l2StandardErc20Bytecode; bytes l2SharedBridgeBytecode; bytes l2SharedBridgeProxyBytecode; + bytes consensusRegistryBytecode; + bytes consensusRegistryProxyBytecode; bytes forceDeployUpgrader; } @@ -44,6 +51,8 @@ contract DeployL2Script is Script { deploySharedBridgeProxy(); initializeChain(); deployForceDeployer(); + deployConsensusRegistry(); + deployConsensusRegistryProxy(); saveOutput(); } @@ -69,6 +78,16 @@ contract DeployL2Script is Script { saveOutput(); } + function runDeployConsensusRegistry() public { + initializeConfig(); + loadContracts(); + + deployConsensusRegistry(); + deployConsensusRegistryProxy(); + + saveOutput(); + } + function loadContracts() internal { //HACK: Meanwhile we are not integrated foundry zksync we use contracts that has been built using hardhat contracts.l2StandardErc20FactoryBytecode = Utils.readHardhatBytecode( @@ -84,10 +103,17 @@ contract DeployL2Script is Script { contracts.l2SharedBridgeBytecode = Utils.readHardhatBytecode( "/../l2-contracts/artifacts-zk/contracts/bridge/L2SharedBridge.sol/L2SharedBridge.json" ); - contracts.l2SharedBridgeProxyBytecode = Utils.readHardhatBytecode( "/../l2-contracts/artifacts-zk/@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol/TransparentUpgradeableProxy.json" ); + + contracts.consensusRegistryBytecode = Utils.readHardhatBytecode( + "/../l2-contracts/artifacts-zk/contracts/ConsensusRegistry.sol/ConsensusRegistry.json" + ); + contracts.consensusRegistryProxyBytecode = Utils.readHardhatBytecode( + "/../l2-contracts/artifacts-zk/@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol/TransparentUpgradeableProxy.json" + ); + contracts.forceDeployUpgrader = Utils.readHardhatBytecode( "/../l2-contracts/artifacts-zk/contracts/ForceDeployUpgrader.sol/ForceDeployUpgrader.json" ); @@ -101,6 +127,7 @@ contract DeployL2Script is Script { config.governance = toml.readAddress("$.governance"); config.l1SharedBridgeProxy = toml.readAddress("$.l1_shared_bridge"); config.erc20BridgeProxy = toml.readAddress("$.erc20_bridge"); + config.consensusRegistryOwner = toml.readAddress("$.consensus_registry_owner"); config.chainId = toml.readUint("$.chain_id"); config.eraChainId = toml.readUint("$.era_chain_id"); } @@ -108,6 +135,8 @@ contract DeployL2Script is Script { function saveOutput() internal { vm.serializeAddress("root", "l2_shared_bridge_implementation", config.l2SharedBridgeImplementation); vm.serializeAddress("root", "l2_shared_bridge_proxy", config.l2SharedBridgeProxy); + vm.serializeAddress("root", "consensus_registry_implementation", config.consensusRegistryImplementation); + vm.serializeAddress("root", "consensus_registry_proxy", config.consensusRegistryProxy); string memory toml = vm.serializeAddress("root", "l2_default_upgrader", config.forceDeployUpgraderAddress); string memory root = vm.projectRoot(); string memory path = string.concat(root, "/script-out/output-deploy-l2-contracts.toml"); @@ -185,6 +214,54 @@ contract DeployL2Script is Script { }); } + // Deploy the ConsensusRegistry implementation and save its address into the config. + function deployConsensusRegistry() internal { + // ConsensusRegistry.sol doesn't have a constructor, just an initializer. + bytes memory constructorData = ""; + + config.consensusRegistryImplementation = Utils.deployThroughL1({ + bytecode: contracts.consensusRegistryBytecode, + constructorargs: constructorData, + create2salt: "", + l2GasLimit: Utils.MAX_PRIORITY_TX_GAS, + factoryDeps: new bytes[](0), + chainId: config.chainId, + bridgehubAddress: config.bridgehubAddress, + l1SharedBridgeProxy: config.l1SharedBridgeProxy + }); + } + + // Deploy a transparent upgradable proxy for the already deployed consensus registry + // implementation and save its address into the config. + function deployConsensusRegistryProxy() internal { + // Admin for the proxy + address l2GovernorAddress = AddressAliasHelper.applyL1ToL2Alias(config.governance); + + // Call ConsensusRegistry::initialize with the initial owner. + // solhint-disable-next-line func-named-parameters + bytes memory proxyInitializationParams = abi.encodeWithSignature( + "initialize(address)", + config.consensusRegistryOwner + ); + + bytes memory consensusRegistryProxyConstructorData = abi.encode( + config.consensusRegistryImplementation, // _logic + l2GovernorAddress, // admin_ + proxyInitializationParams // _data + ); + + config.consensusRegistryProxy = Utils.deployThroughL1({ + bytecode: contracts.consensusRegistryProxyBytecode, + constructorargs: consensusRegistryProxyConstructorData, + create2salt: "", + l2GasLimit: Utils.MAX_PRIORITY_TX_GAS, + factoryDeps: new bytes[](0), + chainId: config.chainId, + bridgehubAddress: config.bridgehubAddress, + l1SharedBridgeProxy: config.l1SharedBridgeProxy + }); + } + function initializeChain() internal { L1SharedBridge bridge = L1SharedBridge(config.l1SharedBridgeProxy); From 6892ccc93bfbc36686809fd80d20fa6afabfc169 Mon Sep 17 00:00:00 2001 From: Vlad Bochok <41153528+vladbochok@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:24:21 +0200 Subject: [PATCH 05/13] Add admin permission to add custom base tokens (#776) Co-authored-by: Stanislav Breadless --- l1-contracts/contracts/bridgehub/Bridgehub.sol | 2 +- .../unit/concrete/Bridgehub/experimental_bridge.t.sol | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/l1-contracts/contracts/bridgehub/Bridgehub.sol b/l1-contracts/contracts/bridgehub/Bridgehub.sol index 539af6a0c..7b9ecf43b 100644 --- a/l1-contracts/contracts/bridgehub/Bridgehub.sol +++ b/l1-contracts/contracts/bridgehub/Bridgehub.sol @@ -101,7 +101,7 @@ contract Bridgehub is IBridgehub, ReentrancyGuard, Ownable2StepUpgradeable, Paus } /// @notice token can be any contract with the appropriate interface/functionality - function addToken(address _token) external onlyOwner { + function addToken(address _token) external onlyOwnerOrAdmin { require(!tokenIsRegistered[_token], "Bridgehub: token already registered"); tokenIsRegistered[_token] = true; } diff --git a/l1-contracts/test/foundry/unit/concrete/Bridgehub/experimental_bridge.t.sol b/l1-contracts/test/foundry/unit/concrete/Bridgehub/experimental_bridge.t.sol index 9ef3ae0fb..ff747ddac 100644 --- a/l1-contracts/test/foundry/unit/concrete/Bridgehub/experimental_bridge.t.sol +++ b/l1-contracts/test/foundry/unit/concrete/Bridgehub/experimental_bridge.t.sol @@ -249,11 +249,12 @@ contract ExperimentalBridgeTest is Test { } function test_addToken_cannotBeCalledByRandomAddress(address randomAddress, address randomCaller) public { - if (randomCaller != bridgeOwner) { - vm.prank(randomCaller); - vm.expectRevert(bytes("Ownable: caller is not the owner")); - bridgeHub.addToken(randomAddress); - } + vm.assume(randomAddress != bridgeOwner); + vm.assume(randomAddress != bridgeHub.admin()); + + vm.prank(randomCaller); + vm.expectRevert(bytes("Bridgehub: not owner or admin")); + bridgeHub.addToken(randomAddress); assertTrue(!bridgeHub.tokenIsRegistered(randomAddress), "This random address is not registered as a token"); From ec8e9daec4f4228d04f76ac21d81c962369a6a44 Mon Sep 17 00:00:00 2001 From: Danil Date: Tue, 10 Sep 2024 15:10:39 +0200 Subject: [PATCH 06/13] Deploy legacy bridge inside foundry (#787) Signed-off-by: Danil Co-authored-by: Vlad Bochok <41153528+vladbochok@users.noreply.github.com> --- .../deploy-scripts/DeployL2Contracts.sol | 54 ++++-- .../dev/SetupLegacyBridge.s.sol | 155 ++++++++++++++++++ 2 files changed, 197 insertions(+), 12 deletions(-) create mode 100644 l1-contracts/deploy-scripts/dev/SetupLegacyBridge.s.sol diff --git a/l1-contracts/deploy-scripts/DeployL2Contracts.sol b/l1-contracts/deploy-scripts/DeployL2Contracts.sol index b22c091ae..dddd26b25 100644 --- a/l1-contracts/deploy-scripts/DeployL2Contracts.sol +++ b/l1-contracts/deploy-scripts/DeployL2Contracts.sol @@ -43,12 +43,20 @@ contract DeployL2Script is Script { } function run() public { + deploy(false); + } + + function runWithLegacyBridge() public { + deploy(true); + } + + function deploy(bool legacyBridge) public { initializeConfig(); - loadContracts(); + loadContracts(legacyBridge); deployFactoryDeps(); deploySharedBridge(); - deploySharedBridgeProxy(); + deploySharedBridgeProxy(legacyBridge); initializeChain(); deployForceDeployer(); deployConsensusRegistry(); @@ -57,13 +65,21 @@ contract DeployL2Script is Script { saveOutput(); } + function runDeployLegacySharedBridge() public { + deploySharedBridge(true); + } + function runDeploySharedBridge() public { + deploySharedBridge(false); + } + + function deploySharedBridge(bool legacyBridge) internal { initializeConfig(); - loadContracts(); + loadContracts(legacyBridge); deployFactoryDeps(); deploySharedBridge(); - deploySharedBridgeProxy(); + deploySharedBridgeProxy(legacyBridge); initializeChain(); saveOutput(); @@ -71,7 +87,7 @@ contract DeployL2Script is Script { function runDefaultUpgrader() public { initializeConfig(); - loadContracts(); + loadContracts(false); deployForceDeployer(); @@ -80,7 +96,7 @@ contract DeployL2Script is Script { function runDeployConsensusRegistry() public { initializeConfig(); - loadContracts(); + loadContracts(false); deployConsensusRegistry(); deployConsensusRegistryProxy(); @@ -88,7 +104,7 @@ contract DeployL2Script is Script { saveOutput(); } - function loadContracts() internal { + function loadContracts(bool legacyBridge) internal { //HACK: Meanwhile we are not integrated foundry zksync we use contracts that has been built using hardhat contracts.l2StandardErc20FactoryBytecode = Utils.readHardhatBytecode( "/../l2-contracts/artifacts-zk/@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol/UpgradeableBeacon.json" @@ -100,9 +116,16 @@ contract DeployL2Script is Script { "/../l2-contracts/artifacts-zk/contracts/bridge/L2StandardERC20.sol/L2StandardERC20.json" ); - contracts.l2SharedBridgeBytecode = Utils.readHardhatBytecode( - "/../l2-contracts/artifacts-zk/contracts/bridge/L2SharedBridge.sol/L2SharedBridge.json" - ); + if (legacyBridge) { + contracts.l2SharedBridgeBytecode = Utils.readHardhatBytecode( + "/../l2-contracts/artifacts-zk/contracts/dev-contracts/DevL2SharedBridge.sol/DevL2SharedBridge.json" + ); + } else { + contracts.l2SharedBridgeBytecode = Utils.readHardhatBytecode( + "/../l2-contracts/artifacts-zk/contracts/bridge/L2SharedBridge.sol/L2SharedBridge.json" + ); + } + contracts.l2SharedBridgeProxyBytecode = Utils.readHardhatBytecode( "/../l2-contracts/artifacts-zk/@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol/TransparentUpgradeableProxy.json" ); @@ -183,13 +206,20 @@ contract DeployL2Script is Script { }); } - function deploySharedBridgeProxy() internal { + function deploySharedBridgeProxy(bool legacyBridge) internal { address l2GovernorAddress = AddressAliasHelper.applyL1ToL2Alias(config.governance); bytes32 l2StandardErc20BytecodeHash = L2ContractHelper.hashL2Bytecode(contracts.beaconProxy); + string memory functionSignature; + + if (legacyBridge) { + functionSignature = "initializeDevBridge(address,address,bytes32,address)"; + } else { + functionSignature = "initialize(address,address,bytes32,address)"; + } // solhint-disable-next-line func-named-parameters bytes memory proxyInitializationParams = abi.encodeWithSignature( - "initialize(address,address,bytes32,address)", + functionSignature, config.l1SharedBridgeProxy, config.erc20BridgeProxy, l2StandardErc20BytecodeHash, diff --git a/l1-contracts/deploy-scripts/dev/SetupLegacyBridge.s.sol b/l1-contracts/deploy-scripts/dev/SetupLegacyBridge.s.sol new file mode 100644 index 000000000..301bfd2c8 --- /dev/null +++ b/l1-contracts/deploy-scripts/dev/SetupLegacyBridge.s.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {Script} from "forge-std/Script.sol"; +import {stdToml} from "forge-std/StdToml.sol"; +import {Utils} from "./../Utils.sol"; +import {L1SharedBridge} from "contracts/bridge/L1SharedBridge.sol"; +import {DummyL1ERC20Bridge} from "contracts/dev-contracts/DummyL1ERC20Bridge.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {L2ContractHelper} from "contracts/common/libraries/L2ContractHelper.sol"; + +/// This scripts is only for developer +contract SetupLegacyBridge is Script { + using stdToml for string; + + Config internal config; + Addresses internal addresses; + + struct Config { + uint256 chainId; + address l2SharedBridgeAddress; + bytes32 create2FactorySalt; + } + + struct Addresses { + address create2FactoryAddr; + address bridgehub; + address diamondProxy; + address sharedBridgeProxy; + address transparentProxyAdmin; + address erc20BridgeProxy; + address tokenWethAddress; + address erc20BridgeProxyImpl; + address sharedBridgeProxyImpl; + } + + function run() public { + initializeConfig(); + deploySharedBridgeImplementation(); + upgradeImplementation(addresses.sharedBridgeProxy, addresses.sharedBridgeProxyImpl); + deployDummyErc20Bridge(); + upgradeImplementation(addresses.erc20BridgeProxy, addresses.erc20BridgeProxyImpl); + setParamsForDummyBridge(); + } + + function initializeConfig() internal { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/script-config/setup-legacy-bridge.toml"); + string memory toml = vm.readFile(path); + + addresses.bridgehub = toml.readAddress("$.bridgehub"); + addresses.diamondProxy = toml.readAddress("$.diamond_proxy"); + addresses.sharedBridgeProxy = toml.readAddress("$.shared_bridge_proxy"); + addresses.transparentProxyAdmin = toml.readAddress("$.transparent_proxy_admin"); + addresses.erc20BridgeProxy = toml.readAddress("$.erc20bridge_proxy"); + addresses.tokenWethAddress = toml.readAddress("$.token_weth_address"); + addresses.create2FactoryAddr = toml.readAddress("$.create2factory_addr"); + config.chainId = toml.readUint("$.chain_id"); + config.l2SharedBridgeAddress = toml.readAddress("$.l2shared_bridge_address"); + config.create2FactorySalt = toml.readBytes32("$.create2factory_salt"); + } + + // We need to deploy new shared bridge for changing chain id and diamond proxy address + function deploySharedBridgeImplementation() internal { + bytes memory bytecode = abi.encodePacked( + type(L1SharedBridge).creationCode, + // solhint-disable-next-line func-named-parameters + abi.encode(addresses.tokenWethAddress, addresses.bridgehub, config.chainId, addresses.diamondProxy) + ); + + address contractAddress = deployViaCreate2(bytecode); + addresses.sharedBridgeProxyImpl = contractAddress; + } + + function deployDummyErc20Bridge() internal { + bytes memory bytecode = abi.encodePacked( + type(DummyL1ERC20Bridge).creationCode, + // solhint-disable-next-line func-named-parameters + abi.encode(addresses.sharedBridgeProxy) + ); + address contractAddress = deployViaCreate2(bytecode); + addresses.erc20BridgeProxyImpl = contractAddress; + } + + function upgradeImplementation(address proxy, address implementation) internal { + bytes memory proxyAdminUpgradeData = abi.encodeCall( + ProxyAdmin.upgrade, + (ITransparentUpgradeableProxy(proxy), implementation) + ); + ProxyAdmin _proxyAdmin = ProxyAdmin(addresses.transparentProxyAdmin); + address governance = _proxyAdmin.owner(); + + Utils.executeUpgrade({ + _governor: address(governance), + _salt: bytes32(0), + _target: address(addresses.transparentProxyAdmin), + _data: proxyAdminUpgradeData, + _value: 0, + _delay: 0 + }); + } + + function setParamsForDummyBridge() internal { + (address l2TokenBeacon, bytes32 l2TokenBeaconHash) = calculateTokenBeaconAddress(); + DummyL1ERC20Bridge bridge = DummyL1ERC20Bridge(addresses.erc20BridgeProxy); + bridge.setValues(config.l2SharedBridgeAddress, l2TokenBeacon, l2TokenBeaconHash); + } + + function calculateTokenBeaconAddress() + internal + returns (address tokenBeaconAddress, bytes32 tokenBeaconBytecodeHash) + { + bytes memory l2StandardTokenCode = Utils.readHardhatBytecode( + "/../l2-contracts/artifacts-zk/contracts/bridge/L2StandardERC20.sol/L2StandardERC20.json" + ); + (address l2StandardToken, ) = calculateL2Create2Address( + config.l2SharedBridgeAddress, + l2StandardTokenCode, + bytes32(0), + "" + ); + + bytes memory beaconProxy = Utils.readHardhatBytecode( + "/../l2-contracts/artifacts-zk/@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol/BeaconProxy.json" + ); + + (tokenBeaconAddress, tokenBeaconBytecodeHash) = calculateL2Create2Address( + config.l2SharedBridgeAddress, + beaconProxy, + bytes32(0), + abi.encode(l2StandardToken) + ); + } + + function calculateL2Create2Address( + address sender, + bytes memory bytecode, + bytes32 create2salt, + bytes memory constructorargs + ) internal returns (address create2Address, bytes32 bytecodeHash) { + bytecodeHash = L2ContractHelper.hashL2Bytecode(bytecode); + + create2Address = L2ContractHelper.computeCreate2Address( + sender, + create2salt, + bytecodeHash, + keccak256(constructorargs) + ); + } + + function deployViaCreate2(bytes memory _bytecode) internal returns (address) { + return Utils.deployViaCreate2(_bytecode, config.create2FactorySalt, addresses.create2FactoryAddr); + } +} From 73b20c4b972f575613b4054d238332f93f2685cc Mon Sep 17 00:00:00 2001 From: Danil Date: Thu, 12 Sep 2024 10:36:06 +0200 Subject: [PATCH 07/13] deploy legacy bridge (#795) Signed-off-by: Danil Co-authored-by: Vlad Bochok <41153528+vladbochok@users.noreply.github.com> --- .../deploy-scripts/dev/SetupLegacyBridge.s.sol | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/l1-contracts/deploy-scripts/dev/SetupLegacyBridge.s.sol b/l1-contracts/deploy-scripts/dev/SetupLegacyBridge.s.sol index 301bfd2c8..e178824b1 100644 --- a/l1-contracts/deploy-scripts/dev/SetupLegacyBridge.s.sol +++ b/l1-contracts/deploy-scripts/dev/SetupLegacyBridge.s.sol @@ -104,6 +104,7 @@ contract SetupLegacyBridge is Script { function setParamsForDummyBridge() internal { (address l2TokenBeacon, bytes32 l2TokenBeaconHash) = calculateTokenBeaconAddress(); DummyL1ERC20Bridge bridge = DummyL1ERC20Bridge(addresses.erc20BridgeProxy); + vm.broadcast(); bridge.setValues(config.l2SharedBridgeAddress, l2TokenBeacon, l2TokenBeaconHash); } @@ -124,10 +125,15 @@ contract SetupLegacyBridge is Script { bytes memory beaconProxy = Utils.readHardhatBytecode( "/../l2-contracts/artifacts-zk/@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol/BeaconProxy.json" ); + tokenBeaconBytecodeHash = L2ContractHelper.hashL2Bytecode(beaconProxy); - (tokenBeaconAddress, tokenBeaconBytecodeHash) = calculateL2Create2Address( + bytes memory upgradableBeacon = Utils.readHardhatBytecode( + "/../l2-contracts/artifacts-zk/@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol/UpgradeableBeacon.json" + ); + + (tokenBeaconAddress, ) = calculateL2Create2Address( config.l2SharedBridgeAddress, - beaconProxy, + upgradableBeacon, bytes32(0), abi.encode(l2StandardToken) ); From 3a1b5d4b94ffb00f03d436a7db7e48589eb74d39 Mon Sep 17 00:00:00 2001 From: Danil Date: Thu, 12 Sep 2024 14:44:04 +0200 Subject: [PATCH 08/13] use chain admin for bridgehub manipulations (#799) Signed-off-by: Danil Co-authored-by: Vlad Bochok <41153528+vladbochok@users.noreply.github.com> --- l1-contracts/deploy-scripts/DeployL1.s.sol | 4 +++ .../deploy-scripts/RegisterHyperchain.s.sol | 26 +++++-------------- l1-contracts/deploy-scripts/Utils.sol | 10 +++++++ 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/l1-contracts/deploy-scripts/DeployL1.s.sol b/l1-contracts/deploy-scripts/DeployL1.s.sol index 8c071090b..f4de831a8 100644 --- a/l1-contracts/deploy-scripts/DeployL1.s.sol +++ b/l1-contracts/deploy-scripts/DeployL1.s.sol @@ -586,12 +586,15 @@ contract DeployL1Script is Script { Bridgehub bridgehub = Bridgehub(addresses.bridgehub.bridgehubProxy); bridgehub.transferOwnership(addresses.governance); + bridgehub.setPendingAdmin(addresses.chainAdmin); L1SharedBridge sharedBridge = L1SharedBridge(addresses.bridges.sharedBridgeProxy); sharedBridge.transferOwnership(addresses.governance); + sharedBridge.setPendingAdmin(addresses.chainAdmin); StateTransitionManager stm = StateTransitionManager(addresses.stateTransition.stateTransitionProxy); stm.transferOwnership(addresses.governance); + stm.setPendingAdmin(addresses.chainAdmin); vm.stopBroadcast(); console.log("Owners updated"); @@ -702,6 +705,7 @@ contract DeployL1Script is Script { addresses.blobVersionedHashRetriever ); vm.serializeAddress("deployed_addresses", "validator_timelock_addr", addresses.validatorTimelock); + vm.serializeAddress("deployed_addresses", "chain_admin", addresses.chainAdmin); vm.serializeString("deployed_addresses", "bridgehub", bridgehub); vm.serializeString("deployed_addresses", "state_transition", stateTransition); string memory deployedAddresses = vm.serializeString("deployed_addresses", "bridges", bridges); diff --git a/l1-contracts/deploy-scripts/RegisterHyperchain.s.sol b/l1-contracts/deploy-scripts/RegisterHyperchain.s.sol index e7615b598..f5e23cf8d 100644 --- a/l1-contracts/deploy-scripts/RegisterHyperchain.s.sol +++ b/l1-contracts/deploy-scripts/RegisterHyperchain.s.sol @@ -7,8 +7,7 @@ import {Script, console2 as console} from "forge-std/Script.sol"; import {Vm} from "forge-std/Vm.sol"; import {stdToml} from "forge-std/StdToml.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {IBridgehub} from "contracts/bridgehub/IBridgehub.sol"; +import {Bridgehub} from "contracts/bridgehub/Bridgehub.sol"; import {IZkSyncHyperchain} from "contracts/state-transition/chain-interfaces/IZkSyncHyperchain.sol"; import {ValidatorTimelock} from "contracts/state-transition/ValidatorTimelock.sol"; import {Governance} from "contracts/governance/Governance.sol"; @@ -118,20 +117,17 @@ contract RegisterHyperchainScript is Script { } function registerTokenOnBridgehub() internal { - IBridgehub bridgehub = IBridgehub(config.bridgehub); - Ownable ownable = Ownable(config.bridgehub); + Bridgehub bridgehub = Bridgehub(config.bridgehub); if (bridgehub.tokenIsRegistered(config.baseToken)) { console.log("Token already registered on Bridgehub"); } else { bytes memory data = abi.encodeCall(bridgehub.addToken, (config.baseToken)); - Utils.executeUpgrade({ - _governor: ownable.owner(), - _salt: bytes32(config.bridgehubCreateNewChainSalt), + Utils.chainAdminMulticall({ + _chainAdmin: bridgehub.admin(), _target: config.bridgehub, _data: data, - _value: 0, - _delay: 0 + _value: 0 }); console.log("Token registered on Bridgehub"); } @@ -156,8 +152,7 @@ contract RegisterHyperchainScript is Script { } function registerHyperchain() internal { - IBridgehub bridgehub = IBridgehub(config.bridgehub); - Ownable ownable = Ownable(config.bridgehub); + Bridgehub bridgehub = Bridgehub(config.bridgehub); vm.recordLogs(); bytes memory data = abi.encodeCall( @@ -172,14 +167,7 @@ contract RegisterHyperchainScript is Script { ) ); - Utils.executeUpgrade({ - _governor: ownable.owner(), - _salt: bytes32(config.bridgehubCreateNewChainSalt), - _target: config.bridgehub, - _data: data, - _value: 0, - _delay: 0 - }); + Utils.chainAdminMulticall({_chainAdmin: bridgehub.admin(), _target: config.bridgehub, _data: data, _value: 0}); console.log("Hyperchain registered"); // Get new diamond proxy address from emitted events diff --git a/l1-contracts/deploy-scripts/Utils.sol b/l1-contracts/deploy-scripts/Utils.sol index 23bcf0ff8..fb56bce08 100644 --- a/l1-contracts/deploy-scripts/Utils.sol +++ b/l1-contracts/deploy-scripts/Utils.sol @@ -10,6 +10,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {REQUIRED_L2_GAS_PRICE_PER_PUBDATA} from "contracts/common/Config.sol"; import {L2_DEPLOYER_SYSTEM_CONTRACT_ADDR} from "contracts/common/L2ContractAddresses.sol"; import {L2ContractHelper} from "contracts/common/libraries/L2ContractHelper.sol"; +import {IChainAdmin} from "contracts/governance/IChainAdmin.sol"; library Utils { // Cheatcodes address, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D. @@ -288,6 +289,15 @@ library Utils { return bytecode; } + function chainAdminMulticall(address _chainAdmin, address _target, bytes memory _data, uint256 _value) internal { + IChainAdmin chainAdmin = IChainAdmin(_chainAdmin); + + IChainAdmin.Call[] memory calls = new IChainAdmin.Call[](1); + calls[0] = IChainAdmin.Call({target: _target, value: _value, data: _data}); + vm.broadcast(); + chainAdmin.multicall(calls, true); + } + function executeUpgrade( address _governor, bytes32 _salt, From 9ebf9be53bb3629585a55cd524fb78936817d77a Mon Sep 17 00:00:00 2001 From: Danil Date: Fri, 13 Sep 2024 16:12:47 +0200 Subject: [PATCH 09/13] fix(deploy): Initilize chain using ChainAdmin (#807) Signed-off-by: Danil --- l1-contracts/deploy-scripts/DeployL2Contracts.sol | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/l1-contracts/deploy-scripts/DeployL2Contracts.sol b/l1-contracts/deploy-scripts/DeployL2Contracts.sol index dddd26b25..b361e221d 100644 --- a/l1-contracts/deploy-scripts/DeployL2Contracts.sol +++ b/l1-contracts/deploy-scripts/DeployL2Contracts.sol @@ -295,13 +295,11 @@ contract DeployL2Script is Script { function initializeChain() internal { L1SharedBridge bridge = L1SharedBridge(config.l1SharedBridgeProxy); - Utils.executeUpgrade({ - _governor: bridge.owner(), - _salt: bytes32(0), + Utils.chainAdminMulticall({ + _chainAdmin: bridge.admin(), _target: config.l1SharedBridgeProxy, _data: abi.encodeCall(bridge.initializeChainGovernance, (config.chainId, config.l2SharedBridgeProxy)), - _value: 0, - _delay: 0 + _value: 0 }); } } From bce4b2d0f34bd87f1aaadd291772935afb1c3bd6 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Sat, 14 Sep 2024 10:52:47 +0200 Subject: [PATCH 10/13] feat: multicall3 on L2 (#805) --- .../deploy-scripts/DeployL2Contracts.sol | 24 ++ .../contracts/dev-contracts/Multicall3.sol | 237 ++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 l2-contracts/contracts/dev-contracts/Multicall3.sol diff --git a/l1-contracts/deploy-scripts/DeployL2Contracts.sol b/l1-contracts/deploy-scripts/DeployL2Contracts.sol index b361e221d..9e78f05e5 100644 --- a/l1-contracts/deploy-scripts/DeployL2Contracts.sol +++ b/l1-contracts/deploy-scripts/DeployL2Contracts.sol @@ -28,6 +28,7 @@ contract DeployL2Script is Script { address l2SharedBridgeProxy; address consensusRegistryImplementation; address consensusRegistryProxy; + address multicall3; address forceDeployUpgraderAddress; } @@ -39,6 +40,7 @@ contract DeployL2Script is Script { bytes l2SharedBridgeProxyBytecode; bytes consensusRegistryBytecode; bytes consensusRegistryProxyBytecode; + bytes multicall3Bytecode; bytes forceDeployUpgrader; } @@ -61,6 +63,7 @@ contract DeployL2Script is Script { deployForceDeployer(); deployConsensusRegistry(); deployConsensusRegistryProxy(); + deployMulticall3(); saveOutput(); } @@ -137,6 +140,10 @@ contract DeployL2Script is Script { "/../l2-contracts/artifacts-zk/@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol/TransparentUpgradeableProxy.json" ); + contracts.multicall3Bytecode = Utils.readHardhatBytecode( + "/../l2-contracts/artifacts-zk/contracts/dev-contracts/Multicall3.sol/Multicall3.json" + ); + contracts.forceDeployUpgrader = Utils.readHardhatBytecode( "/../l2-contracts/artifacts-zk/contracts/ForceDeployUpgrader.sol/ForceDeployUpgrader.json" ); @@ -160,6 +167,7 @@ contract DeployL2Script is Script { vm.serializeAddress("root", "l2_shared_bridge_proxy", config.l2SharedBridgeProxy); vm.serializeAddress("root", "consensus_registry_implementation", config.consensusRegistryImplementation); vm.serializeAddress("root", "consensus_registry_proxy", config.consensusRegistryProxy); + vm.serializeAddress("root", "multicall3", config.multicall3); string memory toml = vm.serializeAddress("root", "l2_default_upgrader", config.forceDeployUpgraderAddress); string memory root = vm.projectRoot(); string memory path = string.concat(root, "/script-out/output-deploy-l2-contracts.toml"); @@ -261,6 +269,22 @@ contract DeployL2Script is Script { }); } + function deployMulticall3() internal { + // Multicall3 doesn't have a constructor. + bytes memory constructorData = ""; + + config.multicall3 = Utils.deployThroughL1({ + bytecode: contracts.multicall3Bytecode, + constructorargs: constructorData, + create2salt: "", + l2GasLimit: Utils.MAX_PRIORITY_TX_GAS, + factoryDeps: new bytes[](0), + chainId: config.chainId, + bridgehubAddress: config.bridgehubAddress, + l1SharedBridgeProxy: config.l1SharedBridgeProxy + }); + } + // Deploy a transparent upgradable proxy for the already deployed consensus registry // implementation and save its address into the config. function deployConsensusRegistryProxy() internal { diff --git a/l2-contracts/contracts/dev-contracts/Multicall3.sol b/l2-contracts/contracts/dev-contracts/Multicall3.sol new file mode 100644 index 000000000..9a6c69340 --- /dev/null +++ b/l2-contracts/contracts/dev-contracts/Multicall3.sol @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.20; + +/// @title Multicall3 +/// @notice Aggregate results from multiple function calls +/// @dev Multicall & Multicall2 backwards-compatible +/// @dev Aggregate methods are marked `payable` to save 24 gas per call +/// @author Michael Elliot +/// @author Joshua Levine +/// @author Nick Johnson +/// @author Andreas Bigger +/// @author Matt Solomon +contract Multicall3 { + // add this to be excluded from coverage report + function test() internal virtual {} + + struct Call { + address target; + bytes callData; + } + + struct Call3 { + address target; + bool allowFailure; + bytes callData; + } + + struct Call3Value { + address target; + bool allowFailure; + uint256 value; + bytes callData; + } + + struct Result { + bool success; + bytes returnData; + } + + /// @notice Backwards-compatible call aggregation with Multicall + /// @param calls An array of Call structs + /// @return blockNumber The block number where the calls were executed + /// @return returnData An array of bytes containing the responses + function aggregate(Call[] calldata calls) public payable returns (uint256 blockNumber, bytes[] memory returnData) { + blockNumber = block.number; + uint256 length = calls.length; + returnData = new bytes[](length); + Call calldata call; + for (uint256 i = 0; i < length; ) { + bool success; + call = calls[i]; + (success, returnData[i]) = call.target.call(call.callData); + require(success, "Multicall3: call failed"); + unchecked { + ++i; + } + } + } + + /// @notice Backwards-compatible with Multicall2 + /// @notice Aggregate calls without requiring success + /// @param requireSuccess If true, require all calls to succeed + /// @param calls An array of Call structs + /// @return returnData An array of Result structs + function tryAggregate( + bool requireSuccess, + Call[] calldata calls + ) public payable returns (Result[] memory returnData) { + uint256 length = calls.length; + returnData = new Result[](length); + Call calldata call; + for (uint256 i = 0; i < length; ) { + Result memory result = returnData[i]; + call = calls[i]; + (result.success, result.returnData) = call.target.call(call.callData); + if (requireSuccess) require(result.success, "Multicall3: call failed"); + unchecked { + ++i; + } + } + } + + /// @notice Backwards-compatible with Multicall2 + /// @notice Aggregate calls and allow failures using tryAggregate + /// @param calls An array of Call structs + /// @return blockNumber The block number where the calls were executed + /// @return blockHash The hash of the block where the calls were executed + /// @return returnData An array of Result structs + function tryBlockAndAggregate( + bool requireSuccess, + Call[] calldata calls + ) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) { + blockNumber = block.number; + blockHash = blockhash(block.number); + returnData = tryAggregate(requireSuccess, calls); + } + + /// @notice Backwards-compatible with Multicall2 + /// @notice Aggregate calls and allow failures using tryAggregate + /// @param calls An array of Call structs + /// @return blockNumber The block number where the calls were executed + /// @return blockHash The hash of the block where the calls were executed + /// @return returnData An array of Result structs + function blockAndAggregate( + Call[] calldata calls + ) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) { + (blockNumber, blockHash, returnData) = tryBlockAndAggregate(true, calls); + } + + /// @notice Aggregate calls, ensuring each returns success if required + /// @param calls An array of Call3 structs + /// @return returnData An array of Result structs + function aggregate3(Call3[] calldata calls) public payable returns (Result[] memory returnData) { + uint256 length = calls.length; + returnData = new Result[](length); + Call3 calldata calli; + for (uint256 i = 0; i < length; ) { + Result memory result = returnData[i]; + calli = calls[i]; + (result.success, result.returnData) = calli.target.call(calli.callData); + assembly { + // Revert if the call fails and failure is not allowed + // `allowFailure := calldataload(add(calli, 0x20))` and `success := mload(result)` + if iszero(or(calldataload(add(calli, 0x20)), mload(result))) { + // set "Error(string)" signature: bytes32(bytes4(keccak256("Error(string)"))) + mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000) + // set data offset + mstore(0x04, 0x0000000000000000000000000000000000000000000000000000000000000020) + // set length of revert string + mstore(0x24, 0x0000000000000000000000000000000000000000000000000000000000000017) + // set revert string: bytes32(abi.encodePacked("Multicall3: call failed")) + mstore(0x44, 0x4d756c746963616c6c333a2063616c6c206661696c6564000000000000000000) + revert(0x00, 0x64) + } + } + unchecked { + ++i; + } + } + } + + /// @notice Aggregate calls with a msg value + /// @notice Reverts if msg.value is less than the sum of the call values + /// @param calls An array of Call3Value structs + /// @return returnData An array of Result structs + function aggregate3Value(Call3Value[] calldata calls) public payable returns (Result[] memory returnData) { + uint256 valAccumulator; + uint256 length = calls.length; + returnData = new Result[](length); + Call3Value calldata calli; + for (uint256 i = 0; i < length; ) { + Result memory result = returnData[i]; + calli = calls[i]; + uint256 val = calli.value; + // Humanity will be a Type V Kardashev Civilization before this overflows - andreas + // ~ 10^25 Wei in existence << ~ 10^76 size uint fits in a uint256 + unchecked { + valAccumulator += val; + } + (result.success, result.returnData) = calli.target.call{value: val}(calli.callData); + assembly { + // Revert if the call fails and failure is not allowed + // `allowFailure := calldataload(add(calli, 0x20))` and `success := mload(result)` + if iszero(or(calldataload(add(calli, 0x20)), mload(result))) { + // set "Error(string)" signature: bytes32(bytes4(keccak256("Error(string)"))) + mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000) + // set data offset + mstore(0x04, 0x0000000000000000000000000000000000000000000000000000000000000020) + // set length of revert string + mstore(0x24, 0x0000000000000000000000000000000000000000000000000000000000000017) + // set revert string: bytes32(abi.encodePacked("Multicall3: call failed")) + mstore(0x44, 0x4d756c746963616c6c333a2063616c6c206661696c6564000000000000000000) + revert(0x00, 0x84) + } + } + unchecked { + ++i; + } + } + // Finally, make sure the msg.value = SUM(call[0...i].value) + require(msg.value == valAccumulator, "Multicall3: value mismatch"); + } + + /// @notice Returns the block hash for the given block number + /// @param blockNumber The block number + function getBlockHash(uint256 blockNumber) public view returns (bytes32 blockHash) { + blockHash = blockhash(blockNumber); + } + + /// @notice Returns the block number + function getBlockNumber() public view returns (uint256 blockNumber) { + blockNumber = block.number; + } + + /// @notice Returns the block coinbase + function getCurrentBlockCoinbase() public view returns (address coinbase) { + coinbase = block.coinbase; + } + + /// @notice Returns the block difficulty + function getCurrentBlockDifficulty() public view returns (uint256 difficulty) { + difficulty = block.prevrandao; + } + + /// @notice Returns the block gas limit + function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) { + gaslimit = block.gaslimit; + } + + /// @notice Returns the block timestamp + function getCurrentBlockTimestamp() public view returns (uint256 timestamp) { + timestamp = block.timestamp; + } + + /// @notice Returns the (ETH) balance of a given address + function getEthBalance(address addr) public view returns (uint256 balance) { + balance = addr.balance; + } + + /// @notice Returns the block hash of the last block + function getLastBlockHash() public view returns (bytes32 blockHash) { + unchecked { + blockHash = blockhash(block.number - 1); + } + } + + /// @notice Gets the base fee of the given block + /// @notice Can revert if the BASEFEE opcode is not implemented by the given chain + function getBasefee() public view returns (uint256 basefee) { + basefee = block.basefee; + } + + /// @notice Returns the chain id + function getChainId() public view returns (uint256 chainid) { + chainid = block.chainid; + } +} From 939151eb1ee74805abfc85e4a7e180da6fcb456a Mon Sep 17 00:00:00 2001 From: Vlad Bochok <41153528+vladbochok@users.noreply.github.com> Date: Wed, 2 Oct 2024 19:34:48 +0400 Subject: [PATCH 11/13] Add utils for DecentralizeGovernance transaction (#829) --- l1-contracts/deploy-scripts/EIP712Utils.sol | 30 ++++ l1-contracts/deploy-scripts/Utils.sol | 133 ++++++++++++++++++ .../interfaces/IEmergencyUpgrageBoard.sol | 23 +++ .../deploy-scripts/interfaces/IMultisig.sol | 9 ++ .../interfaces/IProtocolUpgradeHandler.sol | 33 +++++ .../deploy-scripts/interfaces/ISafe.sol | 9 ++ 6 files changed, 237 insertions(+) create mode 100644 l1-contracts/deploy-scripts/EIP712Utils.sol create mode 100644 l1-contracts/deploy-scripts/interfaces/IEmergencyUpgrageBoard.sol create mode 100644 l1-contracts/deploy-scripts/interfaces/IMultisig.sol create mode 100644 l1-contracts/deploy-scripts/interfaces/IProtocolUpgradeHandler.sol create mode 100644 l1-contracts/deploy-scripts/interfaces/ISafe.sol diff --git a/l1-contracts/deploy-scripts/EIP712Utils.sol b/l1-contracts/deploy-scripts/EIP712Utils.sol new file mode 100644 index 000000000..5f1aeb958 --- /dev/null +++ b/l1-contracts/deploy-scripts/EIP712Utils.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +library EIP712Utils { + bytes32 private constant TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + function buildDomainHash( + address _verifyingContract, + string memory _name, + string memory _version + ) internal view returns (bytes32) { + return + keccak256( + // solhint-disable-next-line func-named-parameters + abi.encode( + TYPE_HASH, + keccak256(bytes(_name)), + keccak256(bytes(_version)), + block.chainid, + _verifyingContract + ) + ); + } + + function buildDigest(bytes32 _domainHash, bytes32 _message) internal view returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x01", _domainHash, _message)); + } +} diff --git a/l1-contracts/deploy-scripts/Utils.sol b/l1-contracts/deploy-scripts/Utils.sol index fb56bce08..4f5892ef2 100644 --- a/l1-contracts/deploy-scripts/Utils.sol +++ b/l1-contracts/deploy-scripts/Utils.sol @@ -11,6 +11,26 @@ import {REQUIRED_L2_GAS_PRICE_PER_PUBDATA} from "contracts/common/Config.sol"; import {L2_DEPLOYER_SYSTEM_CONTRACT_ADDR} from "contracts/common/L2ContractAddresses.sol"; import {L2ContractHelper} from "contracts/common/libraries/L2ContractHelper.sol"; import {IChainAdmin} from "contracts/governance/IChainAdmin.sol"; +import {EIP712Utils} from "./EIP712Utils.sol"; +import {IProtocolUpgradeHandler} from "./interfaces/IProtocolUpgradeHandler.sol"; +import {IEmergencyUpgrageBoard} from "./interfaces/IEmergencyUpgrageBoard.sol"; +import {IMultisig} from "./interfaces/IMultisig.sol"; +import {ISafe} from "./interfaces/ISafe.sol"; + +/// @dev EIP-712 TypeHash for the emergency protocol upgrade execution approved by the guardians. +bytes32 constant EXECUTE_EMERGENCY_UPGRADE_GUARDIANS_TYPEHASH = keccak256( + "ExecuteEmergencyUpgradeGuardians(bytes32 id)" +); + +/// @dev EIP-712 TypeHash for the emergency protocol upgrade execution approved by the Security Council. +bytes32 constant EXECUTE_EMERGENCY_UPGRADE_SECURITY_COUNCIL_TYPEHASH = keccak256( + "ExecuteEmergencyUpgradeSecurityCouncil(bytes32 id)" +); + +/// @dev EIP-712 TypeHash for the emergency protocol upgrade execution approved by the ZK Foundation. +bytes32 constant EXECUTE_EMERGENCY_UPGRADE_ZK_FOUNDATION_TYPEHASH = keccak256( + "ExecuteEmergencyUpgradeZKFoundation(bytes32 id)" +); library Utils { // Cheatcodes address, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D. @@ -324,4 +344,117 @@ library Utils { } vm.stopBroadcast(); } + + function executeEmergencyProtocolUpgrade( + IProtocolUpgradeHandler _protocolUpgradeHandler, + Vm.Wallet memory _governorWallet, + IProtocolUpgradeHandler.Call[] memory _calls, + bytes32 _salt + ) internal returns (bytes memory) { + bytes32 upgradeId; + bytes32 emergencyUpgradeBoardDigest; + { + address emergencyUpgradeBoard = _protocolUpgradeHandler.emergencyUpgradeBoard(); + IProtocolUpgradeHandler.UpgradeProposal memory upgradeProposal = IProtocolUpgradeHandler.UpgradeProposal({ + calls: _calls, + salt: _salt, + executor: emergencyUpgradeBoard + }); + upgradeId = keccak256(abi.encode(upgradeProposal)); + emergencyUpgradeBoardDigest = EIP712Utils.buildDomainHash( + emergencyUpgradeBoard, + "EmergencyUpgradeBoard", + "1" + ); + } + + bytes memory guardiansSignatures; + { + address[] memory guardiansMembers = new address[](8); + { + IMultisig guardians = IMultisig(_protocolUpgradeHandler.guardians()); + for (uint256 i = 0; i < 8; i++) { + guardiansMembers[i] = guardians.members(i); + } + } + bytes[] memory guardiansRawSignatures = new bytes[](8); + for (uint256 i = 0; i < 8; i++) { + bytes32 safeDigest; + { + bytes32 guardiansDigest = EIP712Utils.buildDigest( + emergencyUpgradeBoardDigest, + keccak256(abi.encode(EXECUTE_EMERGENCY_UPGRADE_GUARDIANS_TYPEHASH, upgradeId)) + ); + safeDigest = ISafe(guardiansMembers[i]).getMessageHash(abi.encode(guardiansDigest)); + } + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_governorWallet, safeDigest); + guardiansRawSignatures[i] = abi.encodePacked(r, s, v); + } + guardiansSignatures = abi.encode(guardiansMembers, guardiansRawSignatures); + } + + bytes memory securityCouncilSignatures; + { + address[] memory securityCouncilMembers = new address[](12); + { + IMultisig securityCouncil = IMultisig(_protocolUpgradeHandler.securityCouncil()); + for (uint256 i = 0; i < 12; i++) { + securityCouncilMembers[i] = securityCouncil.members(i); + } + } + bytes[] memory securityCouncilRawSignatures = new bytes[](12); + for (uint256 i = 0; i < securityCouncilMembers.length; i++) { + bytes32 safeDigest; + { + bytes32 securityCouncilDigest = EIP712Utils.buildDigest( + emergencyUpgradeBoardDigest, + keccak256(abi.encode(EXECUTE_EMERGENCY_UPGRADE_SECURITY_COUNCIL_TYPEHASH, upgradeId)) + ); + safeDigest = ISafe(securityCouncilMembers[i]).getMessageHash(abi.encode(securityCouncilDigest)); + } + { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_governorWallet, safeDigest); + securityCouncilRawSignatures[i] = abi.encodePacked(r, s, v); + } + } + securityCouncilSignatures = abi.encode(securityCouncilMembers, securityCouncilRawSignatures); + } + + bytes memory zkFoundationSignature; + { + ISafe zkFoundation; + { + IEmergencyUpgrageBoard emergencyUpgradeBoard = IEmergencyUpgrageBoard( + _protocolUpgradeHandler.emergencyUpgradeBoard() + ); + zkFoundation = ISafe(emergencyUpgradeBoard.ZK_FOUNDATION_SAFE()); + } + bytes32 zkFoundationDigest = EIP712Utils.buildDigest( + emergencyUpgradeBoardDigest, + keccak256(abi.encode(EXECUTE_EMERGENCY_UPGRADE_ZK_FOUNDATION_TYPEHASH, upgradeId)) + ); + bytes32 safeDigest = ISafe(zkFoundation).getMessageHash(abi.encode(zkFoundationDigest)); + { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_governorWallet, safeDigest); + zkFoundationSignature = abi.encodePacked(r, s, v); + } + } + + { + vm.startBroadcast(); + IEmergencyUpgrageBoard emergencyUpgradeBoard = IEmergencyUpgrageBoard( + _protocolUpgradeHandler.emergencyUpgradeBoard() + ); + // solhint-disable-next-line func-named-parameters + emergencyUpgradeBoard.executeEmergencyUpgrade( + _calls, + _salt, + guardiansSignatures, + securityCouncilSignatures, + zkFoundationSignature + ); + vm.stopBroadcast(); + } + } } diff --git a/l1-contracts/deploy-scripts/interfaces/IEmergencyUpgrageBoard.sol b/l1-contracts/deploy-scripts/interfaces/IEmergencyUpgrageBoard.sol new file mode 100644 index 000000000..1f3d0d3d6 --- /dev/null +++ b/l1-contracts/deploy-scripts/interfaces/IEmergencyUpgrageBoard.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IProtocolUpgradeHandler} from "./IProtocolUpgradeHandler.sol"; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface IEmergencyUpgrageBoard { + function GUARDIANS() external view returns (address); + + function SECURITY_COUNCIL() external view returns (address); + + function ZK_FOUNDATION_SAFE() external view returns (address); + + function executeEmergencyUpgrade( + IProtocolUpgradeHandler.Call[] calldata _calls, + bytes32 _salt, + bytes calldata _guardiansSignatures, + bytes calldata _securityCouncilSignatures, + bytes calldata _zkFoundationSignatures + ) external; +} diff --git a/l1-contracts/deploy-scripts/interfaces/IMultisig.sol b/l1-contracts/deploy-scripts/interfaces/IMultisig.sol new file mode 100644 index 000000000..2a1dd955d --- /dev/null +++ b/l1-contracts/deploy-scripts/interfaces/IMultisig.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface IMultisig { + function members(uint256) external view returns (address); +} diff --git a/l1-contracts/deploy-scripts/interfaces/IProtocolUpgradeHandler.sol b/l1-contracts/deploy-scripts/interfaces/IProtocolUpgradeHandler.sol new file mode 100644 index 000000000..baa48f43f --- /dev/null +++ b/l1-contracts/deploy-scripts/interfaces/IProtocolUpgradeHandler.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface IProtocolUpgradeHandler { + /// @dev Represents a call to be made during an upgrade. + /// @param target The address to which the call will be made. + /// @param value The amount of Ether (in wei) to be sent along with the call. + /// @param data The calldata to be executed on the `target` address. + struct Call { + address target; + uint256 value; + bytes data; + } + + /// @dev Defines the structure of an upgrade that is executed by Protocol Upgrade Handler. + /// @param executor The L1 address that is authorized to perform the upgrade execution (if address(0) then anyone). + /// @param calls An array of `Call` structs, each representing a call to be made during the upgrade execution. + /// @param salt A bytes32 value used for creating unique upgrade proposal hashes. + struct UpgradeProposal { + Call[] calls; + address executor; + bytes32 salt; + } + + function emergencyUpgradeBoard() external view returns (address); + + function guardians() external view returns (address); + + function securityCouncil() external view returns (address); +} diff --git a/l1-contracts/deploy-scripts/interfaces/ISafe.sol b/l1-contracts/deploy-scripts/interfaces/ISafe.sol new file mode 100644 index 000000000..82877b3b3 --- /dev/null +++ b/l1-contracts/deploy-scripts/interfaces/ISafe.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +/// @author Matter Labs +/// @custom:security-contact security@matterlabs.dev +interface ISafe { + function getMessageHash(bytes memory _message) external view returns (bytes32); +} From aafee035db892689df3f7afe4b89fd6467a39313 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Thu, 3 Oct 2024 11:56:45 +0200 Subject: [PATCH 12/13] feat(zk_toolbox): dedicated command for deploying multicall3 on L2 (#835) --- l1-contracts/deploy-scripts/DeployL2Contracts.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/l1-contracts/deploy-scripts/DeployL2Contracts.sol b/l1-contracts/deploy-scripts/DeployL2Contracts.sol index 9e78f05e5..3525b66b4 100644 --- a/l1-contracts/deploy-scripts/DeployL2Contracts.sol +++ b/l1-contracts/deploy-scripts/DeployL2Contracts.sol @@ -107,6 +107,15 @@ contract DeployL2Script is Script { saveOutput(); } + function runDeployMulticall3() public { + initializeConfig(); + loadContracts(false); + + deployMulticall3(); + + saveOutput(); + } + function loadContracts(bool legacyBridge) internal { //HACK: Meanwhile we are not integrated foundry zksync we use contracts that has been built using hardhat contracts.l2StandardErc20FactoryBytecode = Utils.readHardhatBytecode( From b75fd10ae9888e6aae9e7e5e01abaa344b8b314e Mon Sep 17 00:00:00 2001 From: Zach Kolodny Date: Thu, 3 Oct 2024 21:26:13 -0400 Subject: [PATCH 13/13] remove duplicate identifier --- l1-contracts/contracts/bridge/L1SharedBridge.sol | 6 ------ 1 file changed, 6 deletions(-) diff --git a/l1-contracts/contracts/bridge/L1SharedBridge.sol b/l1-contracts/contracts/bridge/L1SharedBridge.sol index 10ba5e8a4..eaddcccc7 100644 --- a/l1-contracts/contracts/bridge/L1SharedBridge.sol +++ b/l1-contracts/contracts/bridge/L1SharedBridge.sol @@ -134,12 +134,6 @@ contract L1SharedBridge is IL1SharedBridge, ReentrancyGuard, Ownable2StepUpgrade _; } - /// @notice Checks that the message sender is either the owner or admin. - modifier onlyOwnerOrAdmin() { - require(msg.sender == owner() || msg.sender == admin, "ShB not owner or admin"); - _; - } - /// @dev Contract is expected to be used as proxy implementation. /// @dev Initialize the implementation to prevent Parity hack. constructor(