Skip to content

Commit

Permalink
Merge pull request #65 from lidofinance/feature/committees-natspec
Browse files Browse the repository at this point in the history
Committees NatSpec and Reseal Manager spec fix
  • Loading branch information
Psirex authored Jul 24, 2024
2 parents fa34169 + 778807d commit 46f3d71
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 17 deletions.
11 changes: 11 additions & 0 deletions contracts/committees/EmergencyActivationCommittee.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ interface IEmergencyProtectedTimelock {
function emergencyActivate() external;
}

/// @title Emergency Activation Committee Contract
/// @notice This contract allows a committee to approve and execute an emergency activation
/// @dev Inherits from HashConsensus to utilize voting and consensus mechanisms
contract EmergencyActivationCommittee is HashConsensus {
address public immutable EMERGENCY_PROTECTED_TIMELOCK;

Expand All @@ -22,10 +25,16 @@ contract EmergencyActivationCommittee is HashConsensus {
EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock;
}

/// @notice Approves the emergency activation by casting a vote
/// @dev Only callable by committee members
function approveEmergencyActivate() public onlyMember {
_vote(EMERGENCY_ACTIVATION_HASH, true);
}

/// @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 isExecuted Whether the activation has been executed
function getEmergencyActivateState()
public
view
Expand All @@ -34,6 +43,8 @@ contract EmergencyActivationCommittee is HashConsensus {
return _getHashState(EMERGENCY_ACTIVATION_HASH);
}

/// @notice Executes the emergency activation if the quorum is reached
/// @dev Calls the emergencyActivate function on the Emergency Protected Timelock contract
function executeEmergencyActivate() external {
_markUsed(EMERGENCY_ACTIVATION_HASH);
Address.functionCall(
Expand Down
31 changes: 31 additions & 0 deletions contracts/committees/EmergencyExecutionCommittee.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ enum ProposalType {
EmergencyReset
}

/// @title Emergency Execution Committee Contract
/// @notice This contract allows a committee to vote on and execute emergency proposals
/// @dev Inherits from HashConsensus for voting mechanisms and ProposalsList for proposal management
contract EmergencyExecutionCommittee is HashConsensus, ProposalsList {
address public immutable EMERGENCY_PROTECTED_TIMELOCK;

Expand All @@ -27,14 +30,25 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList {
EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock;
}

// ---
// Emergency Execution
// ---

/// @notice Votes on an emergency execution proposal
/// @dev Only callable by committee members
/// @param proposalId The ID of the proposal to vote on
/// @param _supports Indicates whether the member supports the proposal execution
function voteEmergencyExecute(uint256 proposalId, bool _supports) public onlyMember {
(bytes memory proposalData, bytes32 key) = _encodeEmergencyExecute(proposalId);
_vote(key, _supports);
_pushProposal(key, uint256(ProposalType.EmergencyExecute), proposalData);
}

/// @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 isExecuted Whether the proposal has been executed
function getEmergencyExecuteState(uint256 proposalId)
public
view
Expand All @@ -44,6 +58,8 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList {
return _getHashState(key);
}

/// @notice Executes an approved emergency execution proposal
/// @param proposalId The ID of the proposal to execute
function executeEmergencyExecute(uint256 proposalId) public {
(, bytes32 key) = _encodeEmergencyExecute(proposalId);
_markUsed(key);
Expand All @@ -53,6 +69,10 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList {
);
}

/// @dev Encodes the proposal data and generates the proposal key for an emergency execution
/// @param proposalId The ID of the proposal to encode
/// @return proposalData The encoded proposal data
/// @return key The generated proposal key
function _encodeEmergencyExecute(uint256 proposalId)
private
pure
Expand All @@ -62,14 +82,22 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList {
key = keccak256(proposalData);
}

// ---
// Governance reset
// ---

/// @notice Approves an emergency reset proposal
/// @dev Only callable by committee members
function approveEmergencyReset() public onlyMember {
bytes32 proposalKey = _encodeEmergencyResetProposalKey();
_vote(proposalKey, true);
_pushProposal(proposalKey, uint256(ProposalType.EmergencyReset), bytes(""));
}

/// @notice Gets the current state of an emergency reset opprosal
/// @return support The number of votes in support of the proposal
/// @return execuitionQuorum The required number of votes for execution
/// @return isExecuted Whether the proposal has been executed
function getEmergencyResetState()
public
view
Expand All @@ -79,6 +107,7 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList {
return _getHashState(proposalKey);
}

/// @notice Executes an approved emergency reset proposal
function executeEmergencyReset() external {
bytes32 proposalKey = _encodeEmergencyResetProposalKey();
_markUsed(proposalKey);
Expand All @@ -87,6 +116,8 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList {
);
}

/// @notice Encodes the proposal key for an emergency reset
/// @return The generated proposal key
function _encodeEmergencyResetProposalKey() internal pure returns (bytes32) {
return keccak256(abi.encode(ProposalType.EmergencyReset, bytes32(0)));
}
Expand Down
46 changes: 46 additions & 0 deletions contracts/committees/HashConsensus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ pragma solidity 0.8.26;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

/// @title HashConsensus Contract
/// @notice This contract provides a consensus mechanism based on hash voting among members
/// @dev Inherits from Ownable for access control and uses EnumerableSet for member management
abstract contract HashConsensus is Ownable {
using EnumerableSet for EnumerableSet.AddressSet;

Expand Down Expand Up @@ -49,6 +52,10 @@ abstract contract HashConsensus is Ownable {
}
}

/// @notice Casts a vote on a given hash if hash has not been used
/// @dev Only callable by members
/// @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) {
revert HashAlreadyUsed();
Expand All @@ -67,6 +74,9 @@ abstract contract HashConsensus is Ownable {
emit Voted(msg.sender, hash, support);
}

/// @notice Marks a hash as used if quorum is reached and timelock has passed
/// @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) {
revert HashAlreadyUsed();
Expand All @@ -83,6 +93,12 @@ abstract contract HashConsensus is Ownable {
emit HashUsed(hash);
}

/// @notice Gets the state of a given hash
/// @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 isUsed Whether the hash has been used
function _getHashState(bytes32 hash)
internal
view
Expand All @@ -93,6 +109,10 @@ abstract contract HashConsensus is Ownable {
isUsed = _hashStates[hash].usedAt > 0;
}

/// @notice Adds a new member to the committee and updates the quorum
/// @dev Only callable by the owner
/// @param newMember The address of the new member
/// @param newQuorum The new quorum value
function addMember(address newMember, uint256 newQuorum) public onlyOwner {
_addMember(newMember);

Expand All @@ -103,6 +123,10 @@ abstract contract HashConsensus is Ownable {
emit QuorumSet(newQuorum);
}

/// @notice Removes a member from the committee and updates the quorum
/// @dev Only callable by the owner
/// @param memberToRemove The address of the member to remove
/// @param newQuorum The new quorum value
function removeMember(address memberToRemove, uint256 newQuorum) public onlyOwner {
if (!_members.contains(memberToRemove)) {
revert IsNotMember();
Expand All @@ -117,19 +141,32 @@ abstract contract HashConsensus is Ownable {
emit QuorumSet(newQuorum);
}

/// @notice Gets the list of committee members
/// @dev Public function to return the list of members
/// @return An array of addresses representing the committee members
function getMembers() public view returns (address[] memory) {
return _members.values();
}

/// @notice Checks if an address is a member of the committee
/// @dev Public function to check membership status
/// @param member The address to check
/// @return A boolean indicating whether the address is a member
function isMember(address member) public view returns (bool) {
return _members.contains(member);
}

/// @notice Sets the timelock duration
/// @dev Only callable by the owner
/// @param timelock The new timelock duration in seconds
function setTimelockDuration(uint256 timelock) public onlyOwner {
timelockDuration = timelock;
emit TimelockDurationSet(timelock);
}

/// @notice Sets the quorum value
/// @dev Only callable by the owner
/// @param newQuorum The new quorum value
function setQuorum(uint256 newQuorum) public onlyOwner {
if (newQuorum == 0 || newQuorum > _members.length()) {
revert InvalidQuorum();
Expand All @@ -139,6 +176,9 @@ abstract contract HashConsensus is Ownable {
emit QuorumSet(newQuorum);
}

/// @notice Adds a new member to the committee
/// @dev Internal function to add a new member
/// @param newMember The address of the new member
function _addMember(address newMember) internal {
if (_members.contains(newMember)) {
revert DuplicatedMember(newMember);
Expand All @@ -147,6 +187,10 @@ abstract contract HashConsensus is Ownable {
emit MemberAdded(newMember);
}

/// @notice Gets the number of votes in support of a given hash
/// @dev Internal function to count the votes in support of a hash
/// @param hash The hash to check
/// @return support The number of votes in support of the hash
function _getSupport(bytes32 hash) internal view returns (uint256 support) {
for (uint256 i = 0; i < _members.length(); ++i) {
if (approves[_members.at(i)][hash]) {
Expand All @@ -155,6 +199,8 @@ abstract contract HashConsensus is Ownable {
}
}

/// @notice Restricts access to only committee members
/// @dev Modifier to ensure that only members can call a function
modifier onlyMember() {
if (!_members.contains(msg.sender)) {
revert SenderIsNotMember();
Expand Down
29 changes: 29 additions & 0 deletions contracts/committees/ProposalsList.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@ pragma solidity 0.8.26;

import {EnumerableProposals, Proposal} from "../libraries/EnumerableProposals.sol";

/// @title Proposals List Contract
/// @notice This contract manages a list of proposals using an enumerable map
/// @dev Uses the EnumerableProposals library for managing proposals
contract ProposalsList {
using EnumerableProposals for EnumerableProposals.Bytes32ToProposalMap;

EnumerableProposals.Bytes32ToProposalMap internal _proposals;

/// @notice Retrieves a list of proposals with pagination
/// @dev Fetches an ordered list of proposals based on the offset and limit
/// @param offset The starting index for the list of proposals
/// @param limit The maximum number of proposals to return
/// @return proposals An array of Proposal structs
function getProposals(uint256 offset, uint256 limit) public view returns (Proposal[] memory proposals) {
bytes32[] memory keys = _proposals.getOrderedKeys(offset, limit);

Expand All @@ -19,22 +27,43 @@ contract ProposalsList {
}
}

/// @notice Retrieves a proposal at a specific index
/// @dev Fetches the proposal located at the specified index in the map
/// @param index The index of the proposal to retrieve
/// @return The Proposal struct at the given index
function getProposalAt(uint256 index) public view returns (Proposal memory) {
return _proposals.at(index);
}

/// @notice Retrieves a proposal by its key
/// @dev Fetches the proposal associated with the given key
/// @param key The key of the proposal to retrieve
/// @return The Proposal struct associated with the given key
function getProposal(bytes32 key) public view returns (Proposal memory) {
return _proposals.get(key);
}

/// @notice Retrieves the total number of proposals
/// @dev Fetches the length of the proposals map
/// @return The total number of proposals
function getProposalsLength() public view returns (uint256) {
return _proposals.length();
}

/// @notice Retrieves an ordered list of proposal keys with pagination
/// @dev Fetches the keys of the proposals based on the offset and limit
/// @param offset The starting index for the list of keys
/// @param limit The maximum number of keys to return
/// @return An array of proposal keys
function getOrderedKeys(uint256 offset, uint256 limit) public view returns (bytes32[] memory) {
return _proposals.getOrderedKeys(offset, limit);
}

/// @notice Adds a new proposal to the list
/// @dev Internal function to push a new proposal into the map
/// @param key The key of the proposal
/// @param proposalType The type of the proposal
/// @param data The data associated with the proposal
function _pushProposal(bytes32 key, uint256 proposalType, bytes memory data) internal {
_proposals.push(key, proposalType, data);
}
Expand Down
21 changes: 21 additions & 0 deletions contracts/committees/ResealCommittee.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ interface IDualGovernance {
function reseal(address[] memory sealables) external;
}

/// @title Reseal Committee Contract
/// @notice This contract allows a committee to vote on and execute resealing proposals
/// @dev Inherits from HashConsensus for voting mechanisms and ProposalsList for proposal management
contract ResealCommittee is HashConsensus, ProposalsList {
address public immutable DUAL_GOVERNANCE;

Expand All @@ -24,12 +27,22 @@ contract ResealCommittee is HashConsensus, ProposalsList {
DUAL_GOVERNANCE = dualGovernance;
}

/// @notice Votes on a reseal proposal
/// @dev Allows committee members to vote on resealing a set of addresses
/// @param sealables The addresses to reseal
/// @param support Indicates whether the member supports the proposal
function voteReseal(address[] memory sealables, bool support) public onlyMember {
(bytes memory proposalData, bytes32 key) = _encodeResealProposal(sealables);
_vote(key, support);
_pushProposal(key, 0, proposalData);
}

/// @notice Gets the current state of a reseal proposal
/// @dev Retrieves the state of the reseal proposal for a set of addresses
/// @param sealables 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 isExecuted Whether the proposal has been executed
function getResealState(address[] memory sealables)
public
view
Expand All @@ -39,6 +52,9 @@ contract ResealCommittee is HashConsensus, ProposalsList {
return _getHashState(key);
}

/// @notice Executes an approved reseal proposal
/// @dev Executes the reseal proposal by calling the reseal function on the Dual Governance contract
/// @param sealables The addresses to reseal
function executeReseal(address[] memory sealables) external {
(, bytes32 key) = _encodeResealProposal(sealables);
_markUsed(key);
Expand All @@ -49,6 +65,11 @@ contract ResealCommittee is HashConsensus, ProposalsList {
_resealNonces[resealNonceHash]++;
}

/// @notice Encodes a reseal proposal
/// @dev Internal function to encode the proposal data and generate the proposal key
/// @param sealables The addresses to reseal
/// @return data The encoded proposal data
/// @return key The generated proposal key
function _encodeResealProposal(address[] memory sealables) internal view returns (bytes memory data, bytes32 key) {
bytes32 resealNonceHash = keccak256(abi.encode(sealables));
data = abi.encode(sealables, _resealNonces[resealNonceHash]);
Expand Down
Loading

0 comments on commit 46f3d71

Please sign in to comment.