-
Notifications
You must be signed in to change notification settings - Fork 409
EN 2FA validator for Era #2037
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: draft-v31
Are you sure you want to change the base?
EN 2FA validator for Era #2037
Changes from 2 commits
1f99d0a
4b92171
431df16
0b04097
ba61e82
dc4a7fc
bb514aa
afe6536
5f902cf
0cdbcab
ab76538
b0cd0c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity 0.8.28; | ||
|
|
||
| import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable-v4/utils/cryptography/EIP712Upgradeable.sol"; | ||
| import {AddressHasNoCode} from "../../common/L1ContractErrors.sol"; | ||
| import {ValidatorTimelock} from "./ValidatorTimelock.sol"; | ||
| import {IValidatorTimelock} from "./interfaces/IValidatorTimelock.sol"; | ||
| import {IEraMultisigValidator} from "./interfaces/IEraMultisigValidator.sol"; | ||
|
|
||
| /// @author Matter Labs | ||
| /// @custom:security-contact security@matterlabs.dev | ||
| /// @notice A multisig wrapper around `ValidatorTimelock` that requires a threshold of approvals | ||
| /// before batch execution can proceed. Designed for Era chains (not ZKsync OS chains) that want | ||
| /// additional security through 2FA: independent nodes verify the execution and sign off on the | ||
| /// state transition before it can be finalized on L1. | ||
| /// @dev This contract sits between the executor EOA and the `ValidatorTimelock`. Commit and prove | ||
| /// calls are forwarded directly, while execute calls require that enough multisig members have | ||
| /// pre-approved the exact execution parameters via `approveHash`. | ||
| /// @dev Expected to be deployed as a TransparentUpgradeableProxy. | ||
| contract EraMultisigValidator is IEraMultisigValidator, ValidatorTimelock, EIP712Upgradeable { | ||
| /// @dev EIP-712 typehash for the ExecuteBatches struct. | ||
| bytes32 internal constant EXECUTE_BATCHES_TYPEHASH = | ||
| keccak256( | ||
| "ExecuteBatches(address chainAddress,uint256 processBatchFrom,uint256 processBatchTo,bytes batchData)" | ||
| ); | ||
|
|
||
| /// @inheritdoc IEraMultisigValidator | ||
| address public override validatorTimelock; | ||
|
|
||
| /// @inheritdoc IEraMultisigValidator | ||
| mapping(address => bool) public override executionMultisigMember; | ||
|
|
||
| /// @inheritdoc IEraMultisigValidator | ||
| mapping(address => mapping(bytes32 => bool)) public override individualApprovals; | ||
|
|
||
| /// @dev Addresses that have approved a given hash. Iterated at execution time | ||
| /// to count only current members. | ||
| mapping(bytes32 => address[]) internal hashApprovers; | ||
|
|
||
| /// @inheritdoc IEraMultisigValidator | ||
| uint256 public override threshold; | ||
|
|
||
| /// @dev Reserved storage space to allow for layout changes in future upgrades. | ||
| uint256[44] private __gap; | ||
|
|
||
| constructor(address _bridgeHub) ValidatorTimelock(_bridgeHub) { | ||
| _disableInitializers(); | ||
| } | ||
|
|
||
| /// @inheritdoc IEraMultisigValidator | ||
| function initializeV2( | ||
| address _initialOwner, | ||
| uint32 _initialExecutionDelay, | ||
| address _validatorTimelock | ||
| ) external reinitializer(2) { | ||
| _validatorTimelockInit(_initialOwner, _initialExecutionDelay); | ||
| _initializeEraMultisig(_validatorTimelock); | ||
| } | ||
|
|
||
| /// @inheritdoc IEraMultisigValidator | ||
| function reinitializeV2(address _validatorTimelock) external reinitializer(2) { | ||
vladbochok marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| _initializeEraMultisig(_validatorTimelock); | ||
| } | ||
|
|
||
| /// @dev Shared initialization logic for EIP-712 and the validator timelock address. | ||
| function _initializeEraMultisig(address _validatorTimelock) internal { | ||
| __EIP712_init("EraMultisigValidator", "1"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. comment |
||
| if (_validatorTimelock.code.length == 0) { | ||
| revert AddressHasNoCode(_validatorTimelock); | ||
| } | ||
| validatorTimelock = _validatorTimelock; | ||
| } | ||
vladbochok marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// @inheritdoc IEraMultisigValidator | ||
| function approveHash(bytes32 _hash) external { | ||
| if (!executionMultisigMember[msg.sender]) { | ||
| revert NotSigner(); | ||
| } | ||
| if (individualApprovals[msg.sender][_hash]) { | ||
| revert AlreadySigned(); | ||
| } | ||
| individualApprovals[msg.sender][_hash] = true; | ||
| hashApprovers[_hash].push(msg.sender); | ||
| emit HashApproved(msg.sender, _hash); | ||
| } | ||
|
|
||
| /// @inheritdoc IEraMultisigValidator | ||
| function getApprovals(bytes32 _hash) public view returns (uint256) { | ||
| uint256 count = 0; | ||
| address[] storage approvers = hashApprovers[_hash]; | ||
| uint256 length = approvers.length; | ||
| for (uint256 i = 0; i < length; i++) { | ||
|
Check failure on line 92 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol
|
||
| if (executionMultisigMember[approvers[i]]) { | ||
| count += 1; | ||
|
Check failure on line 94 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol
|
||
| } | ||
| } | ||
| return count; | ||
| } | ||
vladbochok marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// @inheritdoc IEraMultisigValidator | ||
| function changeThreshold(uint256 _newThreshold) external onlyOwner { | ||
| threshold = _newThreshold; | ||
| emit ThresholdChanged(_newThreshold); | ||
| } | ||
|
|
||
| /// @inheritdoc IEraMultisigValidator | ||
| function changeExecutionMultisigMember( | ||
|
Check failure on line 107 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol
|
||
| address[] memory _addressesToAdd, | ||
| address[] memory _addressesToRemove | ||
| ) external onlyOwner { | ||
| for (uint256 i = 0; i < _addressesToAdd.length; i++) { | ||
|
Check failure on line 111 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol
|
||
vladbochok marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| executionMultisigMember[_addressesToAdd[i]] = true; | ||
| emit MultisigMemberChanged(_addressesToAdd[i], true); | ||
| } | ||
| for (uint256 i = 0; i < _addressesToRemove.length; i++) { | ||
|
Check failure on line 115 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol
|
||
| executionMultisigMember[_addressesToRemove[i]] = false; | ||
| emit MultisigMemberChanged(_addressesToRemove[i], false); | ||
| } | ||
| } | ||
|
|
||
| /// @inheritdoc IValidatorTimelock | ||
| function precommitSharedBridge( | ||
| address _chainAddress, | ||
| uint256 _l2BlockNumber, | ||
|
Check failure on line 124 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol
|
||
| bytes calldata _l2Block | ||
|
Check failure on line 125 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol
|
||
| ) public onlyRole(_chainAddress, PRECOMMITTER_ROLE) override(ValidatorTimelock, IValidatorTimelock) { | ||
| _propagateToValidatorTimelock(); | ||
| } | ||
|
|
||
| /// @inheritdoc IValidatorTimelock | ||
| function revertBatchesSharedBridge( | ||
| address _chainAddress, | ||
| uint256 _newLastBatch | ||
| ) public onlyRole(_chainAddress, REVERTER_ROLE) override(ValidatorTimelock, IValidatorTimelock) { | ||
| _propagateToValidatorTimelock(); | ||
| } | ||
|
|
||
| /// @inheritdoc IValidatorTimelock | ||
| function commitBatchesSharedBridge( | ||
| address _chainAddress, | ||
| uint256 _processBatchFrom, | ||
| uint256 _processBatchTo, | ||
| bytes calldata _batchData | ||
| ) public onlyRole(_chainAddress, COMMITTER_ROLE) override(ValidatorTimelock, IValidatorTimelock) { | ||
| _propagateToValidatorTimelock(); | ||
vladbochok marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /// @inheritdoc IValidatorTimelock | ||
| function proveBatchesSharedBridge( | ||
| address _chainAddress, | ||
| uint256 _processBatchFrom, | ||
| uint256 _processBatchTo, | ||
| bytes calldata _batchData | ||
| ) public onlyRole(_chainAddress, PROVER_ROLE) override(ValidatorTimelock, IValidatorTimelock) { | ||
| _propagateToValidatorTimelock(); | ||
| } | ||
|
|
||
| /// @inheritdoc IValidatorTimelock | ||
| /// @dev In addition to the base role check, this override requires that the execution parameters | ||
| /// have been approved by at least `threshold` multisig members before forwarding. | ||
| function executeBatchesSharedBridge( | ||
| address _chainAddress, | ||
| uint256 _processBatchFrom, | ||
| uint256 _processBatchTo, | ||
| bytes calldata _batchData | ||
| ) public onlyRole(_chainAddress, EXECUTOR_ROLE) override(ValidatorTimelock, IValidatorTimelock) { | ||
| bytes32 approvedHash = calculateHash(_chainAddress, _processBatchFrom, _processBatchTo, _batchData); | ||
| if (getApprovals(approvedHash) < threshold) { | ||
| revert NotEnoughSignatures(); | ||
| } | ||
| _propagateToValidatorTimelock(); | ||
| } | ||
|
|
||
| /// @inheritdoc IEraMultisigValidator | ||
| function calculateHash( | ||
| address _chainAddress, | ||
| uint256 _processBatchFrom, | ||
| uint256 _processBatchTo, | ||
| bytes calldata _batchData | ||
| ) public view returns (bytes32) { | ||
| return | ||
| _hashTypedDataV4( | ||
| keccak256( | ||
| abi.encode( | ||
| EXECUTE_BATCHES_TYPEHASH, | ||
| _chainAddress, | ||
| _processBatchFrom, | ||
| _processBatchTo, | ||
| keccak256(_batchData) | ||
| ) | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| /// @dev Forwards the current calldata to the downstream `ValidatorTimelock`. | ||
| function _propagateToValidatorTimelock() internal { | ||
| address validatorTimelock_ = validatorTimelock; | ||
| assembly { | ||
| // Copy function signature and arguments from calldata at zero position into memory at pointer position | ||
| calldatacopy(0, 0, calldatasize()) | ||
| // Call the ValidatorTimelock contract, returns 0 on error | ||
| let result := call(gas(), validatorTimelock_, 0, 0, calldatasize(), 0, 0) | ||
| // Get the size of the last return data | ||
| let size := returndatasize() | ||
| // Copy the size length of bytes from return data at zero position to pointer position | ||
| returndatacopy(0, 0, size) | ||
| // Depending on the result value | ||
| switch result | ||
| case 0 { | ||
| // End execution and revert state changes | ||
| revert(0, size) | ||
| } | ||
| default { | ||
| // Return data with length of size at pointers position | ||
| return(0, size) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity 0.8.28; | ||
|
|
||
| import {IValidatorTimelock} from "./IValidatorTimelock.sol"; | ||
|
|
||
| /// @author Matter Labs | ||
| /// @custom:security-contact security@matterlabs.dev | ||
| interface IEraMultisigValidator is IValidatorTimelock { | ||
| /// @notice Emitted when a multisig member approves an batch execution hash. | ||
vladbochok marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// @param member The address of the approving member. | ||
| /// @param hash The approved batch execution hash. | ||
| event HashApproved(address indexed member, bytes32 indexed hash); | ||
|
|
||
| /// @notice Emitted when the approval threshold is changed. | ||
| /// @param newThreshold The new threshold value. | ||
| event ThresholdChanged(uint256 newThreshold); | ||
|
|
||
| /// @notice Emitted when a multisig member is added or removed. | ||
| /// @param member The address of the member being modified. | ||
| /// @param isMember Whether the address is now a member. | ||
| event MultisigMemberChanged(address indexed member, bool isMember); | ||
|
|
||
| /// @notice Thrown when an execution is attempted without meeting the approval threshold. | ||
| error NotEnoughSignatures(); | ||
|
|
||
| /// @notice Thrown when a non-member attempts to approve a hash. | ||
| error NotSigner(); | ||
|
|
||
| /// @notice Thrown when a member attempts to approve the same hash twice. | ||
| error AlreadySigned(); | ||
|
|
||
| /// @notice The downstream `ValidatorTimelock` to which calls are forwarded. | ||
| function validatorTimelock() external view returns (address); | ||
|
|
||
| /// @notice Whether an address is a member of the execution multisig. | ||
| function executionMultisigMember(address _member) external view returns (bool); | ||
|
|
||
| /// @notice Whether a specific member has approved a given execution hash. | ||
| function individualApprovals(address _member, bytes32 _hash) external view returns (bool); | ||
|
|
||
| /// @notice Returns the number of approvals for a given hash from addresses that are | ||
| /// currently multisig members. Approvals from removed members are not counted. | ||
| /// @param _hash The execution hash to query. | ||
| function getApprovals(bytes32 _hash) external view returns (uint256); | ||
|
|
||
| /// @notice The number of approvals required before `executeBatchesSharedBridge` can proceed. | ||
| function threshold() external view returns (uint256); | ||
|
|
||
| /// @notice Initializer for a fresh proxy deployment. | ||
| /// @param _initialOwner The initial owner of this contract. | ||
| /// @param _initialExecutionDelay The initial execution delay for the timelock. | ||
| /// @param _validatorTimelock The address of the downstream `ValidatorTimelock` (must be a deployed contract). | ||
| function initializeV2( | ||
| address _initialOwner, | ||
| uint32 _initialExecutionDelay, | ||
| address _validatorTimelock | ||
| ) external; | ||
|
|
||
| /// @notice Reinitializer when upgrading an existing `ValidatorTimelock` proxy. | ||
| /// @dev Owner and execution delay are already set from the previous version. | ||
| /// @param _validatorTimelock The address of the downstream `ValidatorTimelock` (must be a deployed contract). | ||
| function reinitializeV2(address _validatorTimelock) external; | ||
|
|
||
| /// @notice Registers the caller's approval for a given execution hash. | ||
| /// @dev Reverts if the caller is not a multisig member or has already approved this hash. | ||
| /// @param _hash The execution hash to approve (computed via `calculateHash`). | ||
| function approveHash(bytes32 _hash) external; | ||
|
|
||
| /// @notice Updates the number of approvals required for execution. | ||
| /// @param _newThreshold The new approval threshold. | ||
| function changeThreshold(uint256 _newThreshold) external; | ||
|
|
||
| /// @notice Adds and/or removes members of the execution multisig. | ||
| /// @param _addressesToAdd Addresses to grant multisig membership. | ||
| /// @param _addressesToRemove Addresses to revoke multisig membership. | ||
| function changeExecutionMultisigMember( | ||
| address[] memory _addressesToAdd, | ||
| address[] memory _addressesToRemove | ||
| ) external; | ||
|
|
||
| /// @notice Computes the EIP-712 digest used for multisig approval of a batch execution. | ||
| /// @param _chainAddress The address of the ZK chain. | ||
| /// @param _processBatchFrom The first batch number in the range. | ||
| /// @param _processBatchTo The last batch number in the range. | ||
| /// @param _batchData The batch execution data. | ||
| /// @return The EIP-712 typed data hash of the execution parameters. | ||
| function calculateHash( | ||
| address _chainAddress, | ||
| uint256 _processBatchFrom, | ||
| uint256 _processBatchTo, | ||
| bytes calldata _batchData | ||
| ) external view returns (bytes32); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.