Skip to content
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) {
_initializeEraMultisig(_validatorTimelock);
}

/// @dev Shared initialization logic for EIP-712 and the validator timelock address.
function _initializeEraMultisig(address _validatorTimelock) internal {
__EIP712_init("EraMultisigValidator", "1");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment

if (_validatorTimelock.code.length == 0) {
revert AddressHasNoCode(_validatorTimelock);
}
validatorTimelock = _validatorTimelock;
}

/// @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

View workflow job for this annotation

GitHub Actions / lint

GC: For [ i ] variable, increment/decrement by 1 using: [ ++variable ] to save gas

Check failure on line 92 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: For [ i ] variable, increment/decrement by 1 using: [ ++variable ] to save gas

Check failure on line 92 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: For [ i ] variable, increment/decrement by 1 using: [ ++variable ] to save gas
if (executionMultisigMember[approvers[i]]) {
count += 1;

Check failure on line 94 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: For [ count ] variable, increment/decrement by 1 using: [ ++variable ] to save gas

Check failure on line 94 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: For [ count ] variable, increment/decrement by 1 using: [ ++variable ] to save gas

Check failure on line 94 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: For [ count ] variable, increment/decrement by 1 using: [ ++variable ] to save gas
}
}
return count;
}

/// @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

View workflow job for this annotation

GitHub Actions / lint

GC: [_addressesToRemove] argument on Function [changeExecutionMultisigMember] could be [calldata] if it's not being updated

Check failure on line 107 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: [_addressesToAdd] argument on Function [changeExecutionMultisigMember] could be [calldata] if it's not being updated

Check failure on line 107 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: [_addressesToRemove] argument on Function [changeExecutionMultisigMember] could be [calldata] if it's not being updated

Check failure on line 107 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: [_addressesToAdd] argument on Function [changeExecutionMultisigMember] could be [calldata] if it's not being updated

Check failure on line 107 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: [_addressesToRemove] argument on Function [changeExecutionMultisigMember] could be [calldata] if it's not being updated

Check failure on line 107 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: [_addressesToAdd] argument on Function [changeExecutionMultisigMember] could be [calldata] if it's not being updated
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

View workflow job for this annotation

GitHub Actions / lint

GC: For [ i ] variable, increment/decrement by 1 using: [ ++variable ] to save gas

Check failure on line 111 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: Found [ .length ] property in Loop condition. Suggestion: assign it to a variable

Check failure on line 111 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: For [ i ] variable, increment/decrement by 1 using: [ ++variable ] to save gas

Check failure on line 111 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: Found [ .length ] property in Loop condition. Suggestion: assign it to a variable

Check failure on line 111 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: For [ i ] variable, increment/decrement by 1 using: [ ++variable ] to save gas

Check failure on line 111 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: Found [ .length ] property in Loop condition. Suggestion: assign it to a variable
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

View workflow job for this annotation

GitHub Actions / lint

GC: For [ i ] variable, increment/decrement by 1 using: [ ++variable ] to save gas

Check failure on line 115 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: Found [ .length ] property in Loop condition. Suggestion: assign it to a variable

Check failure on line 115 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: For [ i ] variable, increment/decrement by 1 using: [ ++variable ] to save gas

Check failure on line 115 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: Found [ .length ] property in Loop condition. Suggestion: assign it to a variable

Check failure on line 115 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: For [ i ] variable, increment/decrement by 1 using: [ ++variable ] to save gas

Check failure on line 115 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

GC: Found [ .length ] property in Loop condition. Suggestion: assign it to a variable
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

View workflow job for this annotation

GitHub Actions / lint

Variable "_l2BlockNumber" is unused

Check failure on line 124 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

Variable "_l2BlockNumber" is unused

Check failure on line 124 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

Variable "_l2BlockNumber" is unused
bytes calldata _l2Block

Check failure on line 125 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

Variable "_l2Block" is unused

Check failure on line 125 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

Variable "_l2Block" is unused

Check failure on line 125 in l1-contracts/contracts/state-transition/validators/EraMultisigValidator.sol

View workflow job for this annotation

GitHub Actions / lint

Variable "_l2Block" is unused
) 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();
}

/// @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
Expand Up @@ -197,7 +197,7 @@ contract ValidatorTimelock is
address _chainAddress,
uint256, // _l2BlockNumber (unused in this specific implementation)
bytes calldata // _l2Block (unused in this specific implementation)
) public onlyRole(_chainAddress, PRECOMMITTER_ROLE) {
) public virtual onlyRole(_chainAddress, PRECOMMITTER_ROLE) {
_propagateToZKChain(_chainAddress);
}

Expand Down Expand Up @@ -233,7 +233,7 @@ contract ValidatorTimelock is
function revertBatchesSharedBridge(
address _chainAddress,
uint256 /*_newLastBatch*/
) external onlyRole(_chainAddress, REVERTER_ROLE) {
) public virtual onlyRole(_chainAddress, REVERTER_ROLE) {
_propagateToZKChain(_chainAddress);
}

Expand All @@ -243,7 +243,7 @@ contract ValidatorTimelock is
uint256, // _processBatchFrom (unused in this specific implementation)
uint256, // _processBatchTo (unused in this specific implementation)
bytes calldata // _proofData (unused in this specific implementation)
) external onlyRole(_chainAddress, PROVER_ROLE) {
) public virtual onlyRole(_chainAddress, PROVER_ROLE) {
_propagateToZKChain(_chainAddress);
}

Expand All @@ -253,7 +253,7 @@ contract ValidatorTimelock is
uint256 _processBatchFrom,
uint256 _processBatchTo,
bytes calldata // _batchData (unused in this specific implementation)
) external onlyRole(_chainAddress, EXECUTOR_ROLE) {
) public virtual onlyRole(_chainAddress, EXECUTOR_ROLE) {
uint256 delay = executionDelay; // uint32
unchecked {
for (uint256 i = _processBatchFrom; i <= _processBatchTo; ++i) {
Expand Down
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.
/// @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);
}
Loading
Loading