Skip to content

Commit

Permalink
Merge pull request lidofinance#93 from lidofinance/feature/hashconsen…
Browse files Browse the repository at this point in the history
…sus-quorum

Proposal scheduling and types usage for committees
  • Loading branch information
rkolpakov authored Aug 26, 2024
2 parents 4108d4f + dc75ebd commit 6bf5739
Show file tree
Hide file tree
Showing 11 changed files with 268 additions and 122 deletions.
9 changes: 6 additions & 3 deletions contracts/committees/EmergencyActivationCommittee.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol";

import {HashConsensus} from "./HashConsensus.sol";
import {ITimelock} from "../interfaces/ITimelock.sol";
import {Duration, Durations} from "../types/Duration.sol";
import {Timestamp} from "../types/Timestamp.sol";

/// @title Emergency Activation Committee Contract
/// @notice This contract allows a committee to approve and execute an emergency activation
Expand All @@ -19,7 +21,7 @@ contract EmergencyActivationCommittee is HashConsensus {
address[] memory committeeMembers,
uint256 executionQuorum,
address emergencyProtectedTimelock
) HashConsensus(owner, 0) {
) HashConsensus(owner, Durations.from(0)) {
EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock;

_addMembers(committeeMembers, executionQuorum);
Expand All @@ -34,12 +36,13 @@ contract EmergencyActivationCommittee is HashConsensus {

/// @notice Gets the current state of the emergency activation vote
/// @return support The number of votes in support of the activation
/// @return execuitionQuorum The required number of votes for execution
/// @return executionQuorum The required number of votes for execution
/// @return quorumAt The timestamp when the quorum was reached
/// @return isExecuted Whether the activation has been executed
function getActivateEmergencyModeState()
public
view
returns (uint256 support, uint256 execuitionQuorum, bool isExecuted)
returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted)
{
return _getHashState(EMERGENCY_ACTIVATION_HASH);
}
Expand Down
16 changes: 10 additions & 6 deletions contracts/committees/EmergencyExecutionCommittee.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {HashConsensus} from "./HashConsensus.sol";
import {ProposalsList} from "./ProposalsList.sol";
import {ITimelock} from "../interfaces/ITimelock.sol";
import {Timestamp} from "../types/Timestamp.sol";
import {Durations} from "../types/Duration.sol";

enum ProposalType {
EmergencyExecute,
Expand All @@ -22,7 +24,7 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList {
address[] memory committeeMembers,
uint256 executionQuorum,
address emergencyProtectedTimelock
) HashConsensus(owner, 0) {
) HashConsensus(owner, Durations.from(0)) {
EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock;

_addMembers(committeeMembers, executionQuorum);
Expand All @@ -46,12 +48,13 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList {
/// @notice Gets the current state of an emergency execution proposal
/// @param proposalId The ID of the proposal
/// @return support The number of votes in support of the proposal
/// @return execuitionQuorum The required number of votes for execution
/// @return executionQuorum The required number of votes for execution
/// @return quorumAt The timestamp when the quorum was reached
/// @return isExecuted Whether the proposal has been executed
function getEmergencyExecuteState(uint256 proposalId)
public
view
returns (uint256 support, uint256 execuitionQuorum, bool isExecuted)
returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted)
{
(, bytes32 key) = _encodeEmergencyExecute(proposalId);
return _getHashState(key);
Expand Down Expand Up @@ -93,14 +96,15 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList {
_pushProposal(proposalKey, uint256(ProposalType.EmergencyReset), bytes(""));
}

/// @notice Gets the current state of an emergency reset opprosal
/// @notice Gets the current state of an emergency reset proposal
/// @return support The number of votes in support of the proposal
/// @return execuitionQuorum The required number of votes for execution
/// @return executionQuorum The required number of votes for execution
/// @return quorumAt The timestamp when the quorum was reached
/// @return isExecuted Whether the proposal has been executed
function getEmergencyResetState()
public
view
returns (uint256 support, uint256 execuitionQuorum, bool isExecuted)
returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted)
{
bytes32 proposalKey = _encodeEmergencyResetProposalKey();
return _getHashState(proposalKey);
Expand Down
60 changes: 43 additions & 17 deletions contracts/committees/HashConsensus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pragma solidity 0.8.26;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {Duration} from "../types/Duration.sol";
import {Timestamp, Timestamps} from "../types/Timestamp.sol";

/// @title HashConsensus Contract
/// @notice This contract provides a consensus mechanism based on hash voting among members
Expand All @@ -15,30 +17,31 @@ abstract contract HashConsensus is Ownable {
event QuorumSet(uint256 quorum);
event HashUsed(bytes32 hash);
event Voted(address indexed signer, bytes32 hash, bool support);
event TimelockDurationSet(uint256 timelockDuration);
event TimelockDurationSet(Duration timelockDuration);

error DuplicatedMember(address account);
error AccountIsNotMember(address account);
error CallerIsNotMember(address caller);
error HashAlreadyUsed(bytes32 hash);
error QuorumIsNotReached();
error InvalidQuorum();
error InvalidTimelockDuration(uint256 timelock);
error InvalidTimelockDuration(Duration timelock);
error TimelockNotPassed();
error ProposalAlreadyScheduled(bytes32 hash);

struct HashState {
uint40 quorumAt;
uint40 usedAt;
Timestamp scheduledAt;
Timestamp usedAt;
}

uint256 public quorum;
uint256 public timelockDuration;
Duration public timelockDuration;

mapping(bytes32 => HashState) private _hashStates;
EnumerableSet.AddressSet private _members;
mapping(address signer => mapping(bytes32 => bool)) public approves;

constructor(address owner, uint256 timelock) Ownable(owner) {
constructor(address owner, Duration timelock) Ownable(owner) {
timelockDuration = timelock;
emit TimelockDurationSet(timelock);
}
Expand All @@ -48,7 +51,7 @@ abstract contract HashConsensus is Ownable {
/// @param hash The hash to vote on
/// @param support Indicates whether the member supports the hash
function _vote(bytes32 hash, bool support) internal {
if (_hashStates[hash].usedAt > 0) {
if (_hashStates[hash].usedAt > Timestamps.from(0)) {
revert HashAlreadyUsed(hash);
}

Expand All @@ -57,8 +60,9 @@ abstract contract HashConsensus is Ownable {
}

uint256 heads = _getSupport(hash);
if (heads == quorum - 1 && support == true) {
_hashStates[hash].quorumAt = uint40(block.timestamp);
// heads compares to quorum - 1 because the current vote is not counted yet
if (heads >= quorum - 1 && support == true && _hashStates[hash].scheduledAt == Timestamps.from(0)) {
_hashStates[hash].scheduledAt = Timestamps.from(block.timestamp);
}

approves[msg.sender][hash] = support;
Expand All @@ -69,7 +73,7 @@ abstract contract HashConsensus is Ownable {
/// @dev Internal function that handles marking a hash as used
/// @param hash The hash to mark as used
function _markUsed(bytes32 hash) internal {
if (_hashStates[hash].usedAt > 0) {
if (_hashStates[hash].usedAt > Timestamps.from(0)) {
revert HashAlreadyUsed(hash);
}

Expand All @@ -78,11 +82,11 @@ abstract contract HashConsensus is Ownable {
if (support == 0 || support < quorum) {
revert QuorumIsNotReached();
}
if (block.timestamp < _hashStates[hash].quorumAt + timelockDuration) {
if (timelockDuration.addTo(_hashStates[hash].scheduledAt) > Timestamps.from(block.timestamp)) {
revert TimelockNotPassed();
}

_hashStates[hash].usedAt = uint40(block.timestamp);
_hashStates[hash].usedAt = Timestamps.from(block.timestamp);

emit HashUsed(hash);
}
Expand All @@ -91,16 +95,18 @@ abstract contract HashConsensus is Ownable {
/// @dev Internal function to retrieve the state of a hash
/// @param hash The hash to get the state for
/// @return support The number of votes in support of the hash
/// @return execuitionQuorum The required number of votes for execution
/// @return executionQuorum The required number of votes for execution
/// @return scheduledAt The timestamp when the quorum was reached or scheduleProposal was called
/// @return isUsed Whether the hash has been used
function _getHashState(bytes32 hash)
internal
view
returns (uint256 support, uint256 execuitionQuorum, bool isUsed)
returns (uint256 support, uint256 executionQuorum, Timestamp scheduledAt, bool isUsed)
{
support = _getSupport(hash);
execuitionQuorum = quorum;
isUsed = _hashStates[hash].usedAt > 0;
executionQuorum = quorum;
scheduledAt = _hashStates[hash].scheduledAt;
isUsed = _hashStates[hash].usedAt > Timestamps.from(0);
}

/// @notice Adds new members to the contract and sets the execution quorum.
Expand Down Expand Up @@ -154,7 +160,7 @@ abstract contract HashConsensus is Ownable {
/// @notice Sets the timelock duration
/// @dev Only callable by the owner
/// @param timelock The new timelock duration in seconds
function setTimelockDuration(uint256 timelock) public {
function setTimelockDuration(Duration timelock) public {
_checkOwner();
if (timelock == timelockDuration) {
revert InvalidTimelockDuration(timelock);
Expand All @@ -171,6 +177,26 @@ abstract contract HashConsensus is Ownable {
_setQuorum(newQuorum);
}

/// @notice Schedules a proposal for execution if quorum is reached and it has not been scheduled yet.
/// @dev This function schedules a proposal for execution if the quorum is reached and
/// the proposal has not been scheduled yet. Could happen when execution quorum was set to the same value as
/// current support of the proposal.
/// @param hash The hash of the proposal to be scheduled
function schedule(bytes32 hash) public {
if (_hashStates[hash].usedAt > Timestamps.from(0)) {
revert HashAlreadyUsed(hash);
}

if (_getSupport(hash) < quorum) {
revert QuorumIsNotReached();
}
if (_hashStates[hash].scheduledAt > Timestamps.from(0)) {
revert ProposalAlreadyScheduled(hash);
}

_hashStates[hash].scheduledAt = Timestamps.from(block.timestamp);
}

/// @notice Sets the execution quorum required for certain operations.
/// @dev The quorum value must be greater than zero and not exceed the current number of members.
/// @param executionQuorum The new quorum value to be set.
Expand Down
9 changes: 6 additions & 3 deletions contracts/committees/ResealCommittee.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {IDualGovernance} from "../interfaces/IDualGovernance.sol";
import {HashConsensus} from "./HashConsensus.sol";
import {ProposalsList} from "./ProposalsList.sol";
import {Timestamp} from "../types/Timestamp.sol";
import {Duration} from "../types/Duration.sol";

/// @title Reseal Committee Contract
/// @notice This contract allows a committee to vote on and execute resealing proposals
Expand All @@ -20,7 +22,7 @@ contract ResealCommittee is HashConsensus, ProposalsList {
address[] memory committeeMembers,
uint256 executionQuorum,
address dualGovernance,
uint256 timelock
Duration timelock
) HashConsensus(owner, timelock) {
DUAL_GOVERNANCE = dualGovernance;

Expand All @@ -42,12 +44,13 @@ contract ResealCommittee is HashConsensus, ProposalsList {
/// @dev Retrieves the state of the reseal proposal for a sealed address
/// @param sealable The addresses for the reseal proposal
/// @return support The number of votes in support of the proposal
/// @return execuitionQuorum The required number of votes for execution
/// @return executionQuorum The required number of votes for execution
/// @return quorumAt The timestamp when the quorum was reached
/// @return isExecuted Whether the proposal has been executed
function getResealState(address sealable)
public
view
returns (uint256 support, uint256 execuitionQuorum, bool isExecuted)
returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted)
{
(, bytes32 key) = _encodeResealProposal(sealable);
return _getHashState(key);
Expand Down
20 changes: 12 additions & 8 deletions contracts/committees/TiebreakerCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import {ITiebreakerCore} from "../interfaces/ITiebreaker.sol";
import {IDualGovernance} from "../interfaces/IDualGovernance.sol";
import {HashConsensus} from "./HashConsensus.sol";
import {ProposalsList} from "./ProposalsList.sol";
import {Timestamp} from "../types/Timestamp.sol";
import {Duration} from "../types/Duration.sol";

enum ProposalType {
ScheduleProposal,
ResumeSelable
ResumeSealable
}

/// @title Tiebreaker Core Contract
Expand All @@ -23,7 +25,7 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList {

mapping(address => uint256) private _sealableResumeNonces;

constructor(address owner, address dualGovernance, uint256 timelock) HashConsensus(owner, timelock) {
constructor(address owner, address dualGovernance, Duration timelock) HashConsensus(owner, timelock) {
DUAL_GOVERNANCE = dualGovernance;
}

Expand All @@ -45,12 +47,13 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList {
/// @dev Retrieves the state of the schedule proposal for a given proposal ID
/// @param proposalId The ID of the proposal
/// @return support The number of votes in support of the proposal
/// @return execuitionQuorum The required number of votes for execution
/// @return executionQuorum The required number of votes for execution
/// @return quorumAt The timestamp when the quorum was reached
/// @return isExecuted Whether the proposal has been executed
function getScheduleProposalState(uint256 proposalId)
public
view
returns (uint256 support, uint256 execuitionQuorum, bool isExecuted)
returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted)
{
(, bytes32 key) = _encodeScheduleProposal(proposalId);
return _getHashState(key);
Expand Down Expand Up @@ -96,20 +99,21 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList {
}
(bytes memory proposalData, bytes32 key) = _encodeSealableResume(sealable, nonce);
_vote(key, true);
_pushProposal(key, uint256(ProposalType.ResumeSelable), proposalData);
_pushProposal(key, uint256(ProposalType.ResumeSealable), proposalData);
}

/// @notice Gets the current state of a resume sealable proposal
/// @dev Retrieves the state of the resume sealable proposal for a given address and nonce
/// @param sealable The address to resume
/// @param nonce The nonce for the resume proposal
/// @return support The number of votes in support of the proposal
/// @return execuitionQuorum The required number of votes for execution
/// @return executionQuorum The required number of votes for execution
/// @return quorumAt The timestamp when the quorum was reached
/// @return isExecuted Whether the proposal has been executed
function getSealableResumeState(
address sealable,
uint256 nonce
) public view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) {
) public view returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) {
(, bytes32 key) = _encodeSealableResume(sealable, nonce);
return _getHashState(key);
}
Expand All @@ -136,7 +140,7 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList {
address sealable,
uint256 nonce
) private pure returns (bytes memory data, bytes32 key) {
data = abi.encode(ProposalType.ResumeSelable, sealable, nonce);
data = abi.encode(ProposalType.ResumeSealable, sealable, nonce);
key = keccak256(data);
}
}
Loading

0 comments on commit 6bf5739

Please sign in to comment.