Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement governance mechanism with timelock and security council #42

Merged
merged 23 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 254 additions & 0 deletions ethereum/contracts/governance/Governance.sol
vladbochok marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.13;

import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {IGovernance} from "./IGovernance.sol";

/// @author Matter Labs
/// @custom:security-contact [email protected]
/// @notice This contract manages operations (calls with preconditions) for governance tasks.
/// The contract allows for operations to be scheduled, executed, and canceled with
/// appropriate permissions and delays. It is used for managing and coordinating upgrades
/// and changes in all zkSync Era governed contracts.
///
/// Operations can be proposed as either fully transparent upgrades with on-chain data,
/// or "shadow" upgrades where upgrade data is not published on-chain before execution. Proposed operations
/// are subject to a delay before they can be executed, but they can be executed instantly
/// with the security council’s permission.
contract Governance is IGovernance, Ownable2Step {
/// @notice A constant representing the timestamp for completed operations.
uint256 internal constant _DONE_TIMESTAMP = uint256(1);
vladbochok marked this conversation as resolved.
Show resolved Hide resolved

/// @notice The address of the security council.
/// @dev It is supposed to be multisig contract.
address public securityCouncil;

/// @notice A mapping to store timestamps where each operation will be ready for execution.
/// @dev - 0 means the operation is not created.
/// @dev - 1 (_DONE_TIMESTAMP) means the operation is already executed.
/// @dev - any other value means timestamp in seconds when the operation will be ready for execution.
mapping(bytes32 => uint256) public timestamps;

/// @notice The minimum delay in seconds for operations to be ready for execution.
uint256 public minDelay;

/// @notice Initializes the contract with the admin address, security council address, and minimum delay.
/// @param _admin The address to be assigned as the admin of the contract.
/// @param _securityCouncil The address to be assigned as the security council of the contract.
/// @param _minDelay The initial minimum delay (in seconds) to be set for operations.
constructor(
address _admin,
address _securityCouncil,
uint256 _minDelay
) {
require(_admin != address(0), "Admin should be non zero address");

_transferOwnership(_admin);

securityCouncil = _securityCouncil;
minDelay = _minDelay;
}

/*//////////////////////////////////////////////////////////////
MODIFIERS
//////////////////////////////////////////////////////////////*/

/// @notice Checks that the message sender is contract itself.
modifier onlySelf() {
require(msg.sender == address(this), "Only governance contract itself allowed to call this function");
_;
}

/// @notice Checks that the message sender is an active security council.
modifier onlySecurityCouncil() {
require(msg.sender == securityCouncil, "Only security council allowed to call this function");
_;
}

/// @notice Checks that the message sender is an active owner or an active security council.
modifier onlyOwnerOrSecurityCouncil() {
require(msg.sender == owner() || msg.sender == securityCouncil, "Only the owner and security council allowed to call this function");
_;
}

/*//////////////////////////////////////////////////////////////
OPERATION GETTERS
//////////////////////////////////////////////////////////////*/

/// @dev Returns whether an id corresponds to a registered operation. This
/// includes both Waiting, Ready, and Done operations.
function isOperation(bytes32 _id) public view returns (bool) {
return getOperationState(_id) != OperationState.Unset;
}

/// @dev Returns whether an operation is pending or not. Note that a "pending" operation may also be "ready".
function isOperationPending(bytes32 _id) public view returns (bool) {
OperationState state = getOperationState(_id);
return state == OperationState.Waiting || state == OperationState.Ready;
}

/// @dev Returns whether an operation is ready for execution. Note that a "ready" operation is also "pending".
function isOperationReady(bytes32 _id) public view returns (bool) {
return getOperationState(_id) == OperationState.Ready;
}

/// @dev Returns whether an operation is done or not.
function isOperationDone(bytes32 _id) public view returns (bool) {
return getOperationState(_id) == OperationState.Done;
}

/// @dev Returns operation state.
function getOperationState(bytes32 _id) public view returns (OperationState) {
uint256 timestamp = timestamps[_id];
if (timestamp == 0) {
return OperationState.Unset;
} else if (timestamp == _DONE_TIMESTAMP) {
return OperationState.Done;
} else if (timestamp > block.timestamp) {
return OperationState.Waiting;
} else {
return OperationState.Ready;
}
}

/*//////////////////////////////////////////////////////////////
SCHEDULING CALLS
//////////////////////////////////////////////////////////////*/

/// @notice Propose a fully transparent upgrade, providing upgrade data on-chain.
/// @notice The owner will be able to execute the proposal either:
/// - With a `delay` timelock on its own.
/// - With security council instantly.
/// @dev Only the current owner can propose an upgrade.
/// @param _operation The operation parameters will be executed with the upgrade.
/// @param _delay The delay time (in seconds) after which the proposed upgrade can be executed by the owner.
function scheduleTransparent(Operation calldata _operation, uint256 _delay) external onlyOwner {
bytes32 id = hashOperation(_operation);
_schedule(id, _delay);
}

/// @notice Propose "shadow" upgrade, upgrade data is not publishing on-chain.
/// @notice The owner will be able to execute the proposal either:
/// - With a `delay` timelock on its own.
/// - With security council instantly.
/// @dev Only the current owner can propose an upgrade.
/// @param _id The operation hash (see `hashOperation` function)
/// @param _delay The delay time (in seconds) after which the proposed upgrade may be executed by the owner.
function scheduleShadow(bytes32 _id, uint256 _delay) external onlyOwner {
_schedule(_id, _delay);
}

/*//////////////////////////////////////////////////////////////
CANCELING CALLS
//////////////////////////////////////////////////////////////*/

/// @dev Cancel the scheduled operation.
/// @dev Both the owner and security council may cancel an operation.
/// @param _id Proposal id value (see `hashOperation`)
function cancel(bytes32 _id) external onlyOwnerOrSecurityCouncil {
require(isOperationPending(_id));
delete timestamps[_id];
}

/*//////////////////////////////////////////////////////////////
EXECUTING CALLS
//////////////////////////////////////////////////////////////*/

/// @notice Executes the scheduled operation after the delay passed.
/// @dev Both the owner and security council may execute delayed operations.
/// @param _operation The operation parameters will be executed with the upgrade.
function execute(Operation calldata _operation) external onlyOwnerOrSecurityCouncil {
bytes32 id = hashOperation(_operation);
// Check if the predecessor operation is completed.
_checkPredecessorDone(_operation.predecessor);
// Ensure that the operation is ready to proceed.
require(isOperationReady(id), "Operation must be ready before execution");
// Execute operation.
_execute(_operation.calls);
// Reconfirming that the operation is still ready before execution.
vladbochok marked this conversation as resolved.
Show resolved Hide resolved
// This is needed to avoid unexpected reentrancy attacks of re-executing the same operation.
require(isOperationReady(id), "Operation must be ready after execution");
// Set operation to be done
timestamps[id] = _DONE_TIMESTAMP;
}

/// @notice Executes the scheduled operation with the security council instantly.
/// @dev Only the security council may execute an operation instantly.
/// @param _operation The operation parameters will be executed with the upgrade.
function executeInstant(Operation calldata _operation) external onlySecurityCouncil {
bytes32 id = hashOperation(_operation);
// Check if the predecessor operation is completed.
_checkPredecessorDone(_operation.predecessor);
// Ensure that the operation is in a pending state before proceeding.
require(isOperationPending(id), "Operation must be pending before execution");
// Execute operation.
_execute(_operation.calls);
// Reconfirming that the operation is still pending before execution.
// This is needed to avoid unexpected reentrancy attacks of re-executing the same operation.
require(isOperationPending(id), "Operation must be pending after execution");
// Set operation to be done
timestamps[id] = _DONE_TIMESTAMP;
}

/// @dev Returns the identifier of an operation.
/// @param _operation The operation object to compute the identifier for.
function hashOperation(Operation calldata _operation) public pure returns (bytes32) {
return keccak256(abi.encode(_operation));
}

/*//////////////////////////////////////////////////////////////
HELPERS
//////////////////////////////////////////////////////////////*/

/// @dev Schedule an operation that is to become valid after a given delay.
/// @param _id The operation hash (see `hashOperation` function)
/// @param _delay The delay time (in seconds) after which the proposed upgrade can be executed by the owner.
function _schedule(bytes32 _id, uint256 _delay) internal {
require(!isOperation(_id), "Operation with this proposal id already exists");
require(_delay >= minDelay, "Proposed delay is less than minimum delay");

timestamps[_id] = block.timestamp + _delay;
}

/// @dev Execute an operation's calls.
/// @param _calls The array of calls to be executed.
function _execute(Call[] calldata _calls) internal {
for (uint256 i = 0;i < _calls.length; ++i) {
vladbochok marked this conversation as resolved.
Show resolved Hide resolved
(bool success, bytes memory returnData) = _calls[i].target.call{value: _calls[i].value}(_calls[i].data);
if (!success) {
// Propage an error if the call fails.
assembly {
revert(add(returnData, 0x20), mload(returnData))
}
}
}
}

/// @notice Verifies if the predecessor operation is completed.
/// @param _predecessorId The hash of the operation that should be completed.
/// @dev Doesn't check the operation to be complete if the input is zero.
function _checkPredecessorDone(bytes32 _predecessorId) internal view {
require(_predecessorId == bytes32(0) || isOperationDone(_predecessorId), "Predecessor operation not completed");
}

/*//////////////////////////////////////////////////////////////
SELF UPGRADES
//////////////////////////////////////////////////////////////*/

/// @dev Changes the minimum timelock duration for future operations.
/// @param _newDelay The new minimum delay time (in seconds) for future operations.
function updateDelay(uint256 _newDelay) external onlySelf {
minDelay = _newDelay;
}

/// @dev Updates the address of the security council.
/// @param _newSecurityCouncil The address of the new security council.
function updateSecurityCouncil(address _newSecurityCouncil) external onlySelf {
securityCouncil = _newSecurityCouncil;
}

/// @dev Contract might receive/hold ETH as part of the maintenance process.
receive() external payable {}
}
63 changes: 63 additions & 0 deletions ethereum/contracts/governance/IGovernance.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.13;

interface IGovernance {
/// @dev This enumeration includes the following states:
/// @param Unset Default state, indicating the operation has not been set.
/// @param Waiting The operation is scheduled but not yet ready to be executed.
/// @param Ready The operation is ready to be executed.
/// @param Done The operation has been successfully executed.
enum OperationState {
Unset,
Waiting,
Ready,
Done
}

/// @dev Represents a call to be made during an operation.
/// @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 call data to be executed on the `target` address.
struct Call {
address target;
uint256 value;
bytes data;
}

/// @dev Defines the structure of an operation that Governance executes.
/// @param calls An array of `Call` structs, each representing a call to be made during the operation.
/// @param predecessor The hash of the predecessor operation, that should be executed before this operation.
/// @param salt A bytes32 value used for creating unique operation hashes.
struct Operation {
Call[] calls;
bytes32 predecessor;
bytes32 salt;
}

function isOperation(bytes32 _id) external view returns (bool);

function isOperationPending(bytes32 _id) external view returns (bool);

function isOperationReady(bytes32 _id) external view returns (bool);

function isOperationDone(bytes32 _id) external view returns (bool);

function getOperationState(bytes32 _id) external view returns (OperationState);

function scheduleTransparent(Operation calldata _operation, uint256 _delay) external;

function scheduleShadow(bytes32 _id, uint256 _delay) external;

function cancel(bytes32 _id) external;

function execute(Operation calldata _operation) external;

function executeInstant(Operation calldata _operation) external;

function hashOperation(Operation calldata _operation) external pure returns (bytes32);

function updateDelay(uint256 _newDelay) external;

function updateSecurityCouncil(address _newSecurityCouncil) external;
}
2 changes: 1 addition & 1 deletion ethereum/contracts/zksync/Storage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ struct AppStorage {
/// without overhead for proving the block.
uint256 priorityTxMaxGasLimit;
/// @dev Storage of variables needed for upgrade facet
UpgradeStorage upgrades;
UpgradeStorage __DEPRECATED_upgrades;
/// @dev A mapping L2 block number => message number => flag.
/// @dev The L2 -> L1 log is sent for every withdrawal, so this mapping is serving as
/// a flag to indicate that the message was already processed.
Expand Down
16 changes: 10 additions & 6 deletions ethereum/contracts/zksync/facets/Base.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import "../Storage.sol";
import "../../common/ReentrancyGuard.sol";
import "../../common/AllowListed.sol";

import "@openzeppelin/contracts/access/Ownable.sol";

/// @title Base contract containing functions accessible to the other facets.
/// @author Matter Labs
contract Base is ReentrancyGuard, AllowListed {
Expand All @@ -17,15 +19,17 @@ contract Base is ReentrancyGuard, AllowListed {
_;
}

/// @notice Checks if validator is active
modifier onlyValidator() {
require(s.validators[msg.sender], "1h"); // validator is not active
/// @notice Checks that the message sender is an active governor or its owner
vladbochok marked this conversation as resolved.
Show resolved Hide resolved
modifier onlyGovernorOrItsOwner() {
address governorAddr = s.governor;
address ownerAddr = Ownable(governorAddr).owner();
require(msg.sender == ownerAddr || msg.sender == governorAddr, "Only by governor owner");
_;
}

/// @notice Checks if `msg.sender` is the security council
modifier onlySecurityCouncil() {
require(msg.sender == s.upgrades.securityCouncil, "a9"); // not a security council
/// @notice Checks if validator is active
modifier onlyValidator() {
require(s.validators[msg.sender], "1h"); // validator is not active
_;
}
}
Loading
Loading