From 854918a2443873876d603d84c25b26187187eb11 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 13 Mar 2024 11:59:25 +0300 Subject: [PATCH 01/38] tiebraker contracts --- contracts/Tiebreaker.sol | 105 +++++++++++++++++++++++++++++++++ contracts/TiebreakerNOR.sol | 112 ++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 contracts/Tiebreaker.sol create mode 100644 contracts/TiebreakerNOR.sol diff --git a/contracts/Tiebreaker.sol b/contracts/Tiebreaker.sol new file mode 100644 index 00000000..da4da86e --- /dev/null +++ b/contracts/Tiebreaker.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +interface IEmergencyExecutor { + function emergencyExecute(uint256 proposalId) external; +} + +/** + * A contract provides ability to execute locked proposals. + */ +contract Tiebreaker is IEmergencyExecutor { + error SenderIsNotMember(); + error SenderIsNotOwner(); + error IsNotMember(); + error ProposalIsNotSupported(); + error ProposalAlreadyExecuted(uint256 proposalId); + error ZeroQuorum(); + + address executor; + + mapping(address => bool) members; + address public owner; + address[] membersList; + + struct ProposalState { + address[] supportersList; + mapping(address => bool) supporters; + bool isExecuted; + } + + mapping(uint256 => ProposalState) proposals; + + constructor(address _owner, address[] memory _members, address _executor) { + owner = _owner; + membersList = _members; + executor = _executor; + } + + function emergencyExecute(uint256 _proposalId) public onlyMember { + proposals[_proposalId].supportersList.push(msg.sender); + proposals[_proposalId].supporters[msg.sender] = true; + } + + function forwardExecution(uint256 _proposalId) public { + if (!hasQuorum(_proposalId)) { + revert ProposalIsNotSupported(); + } + + if (proposals[_proposalId].isExecuted == true) { + revert ProposalAlreadyExecuted(_proposalId); + } + + IEmergencyExecutor(executor).emergencyExecute(_proposalId); + + proposals[_proposalId].isExecuted = true; + } + + function addMember(address _newMember) public onlyOwner { + membersList.push(_newMember); + members[_newMember] = true; + } + + function removeMember(address _member) public onlyOwner { + if (members[_member] == false) { + revert IsNotMember(); + } + members[_member] = false; + for (uint256 i = 0; i < membersList.length; ++i) { + if (membersList[i] == _member) { + membersList[i] = membersList[membersList.length - 1]; + membersList.pop(); + break; + } + } + } + + function hasQuorum(uint256 _proposalId) public view returns (bool) { + uint256 supportersCount = 0; + uint256 quorum = membersList.length / 2 + 1; + if (quorum == 0) { + revert ZeroQuorum(); + } + + for (uint256 i = 0; i < proposals[_proposalId].supportersList.length; ++i) { + if (members[proposals[_proposalId].supportersList[i]] == true) { + supportersCount++; + } + } + return supportersCount >= quorum; + } + + modifier onlyMember() { + if (members[msg.sender] == false) { + revert SenderIsNotMember(); + } + _; + } + + modifier onlyOwner() { + if (msg.sender != owner) { + revert SenderIsNotOwner(); + } + _; + } +} diff --git a/contracts/TiebreakerNOR.sol b/contracts/TiebreakerNOR.sol new file mode 100644 index 00000000..f8060397 --- /dev/null +++ b/contracts/TiebreakerNOR.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +interface IEmergencyExecutor { + function emergencyExecute(uint256 proposalId) external; +} + +interface INodeOperatorsRegistry { + function getNodeOperator( + uint256 _id, + bool _fullInfo + ) + external + view + returns ( + bool active, + string memory name, + address rewardAddress, + uint64 stakingLimit, + uint64 stoppedValidators, + uint64 totalSigningKeys, + uint64 usedSigningKeys + ); + + function getNodeOperatorsCount() external view returns (uint256); + function getActiveNodeOperatorsCount() external view returns (uint256); + function getNodeOperatorIsActive(uint256 _nodeOperatorId) external view returns (bool); +} + +/** + * A contract provides ability to execute locked proposals. + */ +contract TiebreakerNOR { + error SenderIsNotMember(); + error ProposalIsNotSupported(); + error ProposalSupported(); + error ProposalAlreadyExecuted(uint256 proposalId); + + address public executor; + address public nodeOperatorsRegistry; + + struct ProposalState { + uint256[] supportersList; + mapping(address => bool) supporters; + bool isExecuted; + } + + mapping(uint256 => ProposalState) proposals; + + constructor(address _nodeOperatorsRegistry, address _executor) { + nodeOperatorsRegistry = _nodeOperatorsRegistry; + executor = _executor; + } + + function emergencyExecute(uint256 _proposalId, uint256 _nodeOperatorId) public onlyNodeOperator(_nodeOperatorId) { + if (proposals[_proposalId].supporters[msg.sender] == true) { + revert ProposalSupported(); + } + proposals[_proposalId].supportersList.push(_nodeOperatorId); + proposals[_proposalId].supporters[msg.sender] = true; + } + + function forwardExecution(uint256 _proposalId) public { + if (!hasQuorum(_proposalId)) { + revert ProposalIsNotSupported(); + } + + if (proposals[_proposalId].isExecuted == true) { + revert ProposalAlreadyExecuted(_proposalId); + } + + IEmergencyExecutor(executor).emergencyExecute(_proposalId); + + proposals[_proposalId].isExecuted = true; + } + + function hasQuorum(uint256 _proposalId) public view returns (bool) { + uint256 activeNOCount = INodeOperatorsRegistry(nodeOperatorsRegistry).getActiveNodeOperatorsCount(); + uint256 quorum = activeNOCount / 2 + 1; + + uint256 supportersCount = 0; + + for (uint256 i = 0; i < proposals[_proposalId].supportersList.length; ++i) { + if ( + INodeOperatorsRegistry(nodeOperatorsRegistry).getNodeOperatorIsActive( + proposals[_proposalId].supportersList[i] + ) == true + ) { + supportersCount++; + } + } + + return supportersCount >= quorum; + } + + modifier onlyNodeOperator(uint256 _nodeOperatorId) { + ( + bool active, + , //string memory name, + address rewardAddress, + , //uint64 stakingLimit, + , //uint64 stoppedValidators, + , //uint64 totalSigningKeys, + //uint64 usedSigningKeys + ) = INodeOperatorsRegistry(nodeOperatorsRegistry).getNodeOperator(_nodeOperatorId, false); + + if (active == false || msg.sender != rewardAddress) { + revert SenderIsNotMember(); + } + _; + } +} From 0702db6ca1e3261bc82a52793bbf7718258ed1af Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Tue, 19 Mar 2024 09:23:05 +0300 Subject: [PATCH 02/38] happy path test --- contracts/Tiebreaker.sol | 13 +++- test/scenario/tiebraker.t.sol | 116 +++++++++++++++++++++++++++++++ test/utils/interfaces.sol | 22 ++++++ test/utils/mainnet-addresses.sol | 1 + 4 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 test/scenario/tiebraker.t.sol diff --git a/contracts/Tiebreaker.sol b/contracts/Tiebreaker.sol index da4da86e..bb06c81d 100644 --- a/contracts/Tiebreaker.sol +++ b/contracts/Tiebreaker.sol @@ -13,14 +13,15 @@ contract Tiebreaker is IEmergencyExecutor { error SenderIsNotOwner(); error IsNotMember(); error ProposalIsNotSupported(); + error ProposalAlreadySupported(); error ProposalAlreadyExecuted(uint256 proposalId); error ZeroQuorum(); address executor; - mapping(address => bool) members; + mapping(address => bool) public members; address public owner; - address[] membersList; + address[] public membersList; struct ProposalState { address[] supportersList; @@ -34,9 +35,17 @@ contract Tiebreaker is IEmergencyExecutor { owner = _owner; membersList = _members; executor = _executor; + + for (uint256 i = 0; i < _members.length; ++i) { + members[_members[i]] = true; + } } function emergencyExecute(uint256 _proposalId) public onlyMember { + if (proposals[_proposalId].supporters[msg.sender] == true) { + revert ProposalAlreadySupported(); + } + proposals[_proposalId].supportersList.push(msg.sender); proposals[_proposalId].supporters[msg.sender] = true; } diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol new file mode 100644 index 00000000..83f19425 --- /dev/null +++ b/test/scenario/tiebraker.t.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Test, console} from "forge-std/Test.sol"; +import {DualGovernanceDeployScript, DualGovernance, EmergencyProtectedTimelock} from "script/Deploy.s.sol"; +import {Tiebreaker} from "contracts/Tiebreaker.sol"; +import {TiebreakerNOR} from "contracts/TiebreakerNOR.sol"; + +import {Utils} from "../utils/utils.sol"; +import {INodeOperatorsRegistry} from "../utils/interfaces.sol"; +import {NODE_OPERATORS_REGISTRY} from "../utils/mainnet-addresses.sol"; + +contract TiebreakerScenarioTest is Test { + Executor__mock private _emergencyExecutor; + + Tiebreaker private _coreTiebreaker; + Tiebreaker private _efTiebraker; + TiebreakerNOR private _norTiebreaker; + + uint256 private efMembersCount = 5; + address[] private _efTiebrakerMembers; + + function setUp() external { + Utils.selectFork(); + + _emergencyExecutor = new Executor__mock(); + + _coreTiebreaker = new Tiebreaker(address(this), new address[](0), address(_emergencyExecutor)); + + for (uint256 i = 0; i < 5; i++) { + _efTiebrakerMembers.push(makeAddr(string(abi.encode(i + 65)))); + } + + _efTiebraker = new Tiebreaker(address(this), _efTiebrakerMembers, address(_coreTiebreaker)); + + _norTiebreaker = new TiebreakerNOR(NODE_OPERATORS_REGISTRY, address(_coreTiebreaker)); + + _coreTiebreaker.addMember(address(_efTiebraker)); + _coreTiebreaker.addMember(address(_norTiebreaker)); + } + + function test_proposal_execution() external { + uint256 proposalIdToExecute = 1; + + assert(_emergencyExecutor.proposals(proposalIdToExecute) == false); + + for (uint256 i = 0; i < _efTiebrakerMembers.length / 2; i++) { + vm.prank(_efTiebrakerMembers[i]); + _efTiebraker.emergencyExecute(proposalIdToExecute); + + assert(_efTiebraker.hasQuorum(proposalIdToExecute) == false); + } + + vm.prank(_efTiebrakerMembers[_efTiebrakerMembers.length - 1]); + _efTiebraker.emergencyExecute(proposalIdToExecute); + + assert(_efTiebraker.hasQuorum(proposalIdToExecute) == true); + + assert(_coreTiebreaker.hasQuorum(proposalIdToExecute) == false); + + _efTiebraker.forwardExecution(proposalIdToExecute); + + assert(_coreTiebreaker.hasQuorum(proposalIdToExecute) == false); + + uint256 participatedNOCount = 0; + uint256 requiredOperatorsCount = + INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getActiveNodeOperatorsCount() / 2 + 1; + + for (uint256 i = 0; i < INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getNodeOperatorsCount(); i++) { + ( + bool active, + , //string memory name, + address rewardAddress, + , //uint64 stakingLimit, + , //uint64 stoppedValidators, + , //uint64 totalSigningKeys, + //uint64 usedSigningKeys + ) = INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getNodeOperator(i, false); + if (active) { + vm.prank(rewardAddress); + _norTiebreaker.emergencyExecute(proposalIdToExecute, i); + + participatedNOCount++; + } + if (participatedNOCount >= requiredOperatorsCount) break; + } + + assert(_norTiebreaker.hasQuorum(proposalIdToExecute) == true); + + _norTiebreaker.forwardExecution(proposalIdToExecute); + + assert(_coreTiebreaker.hasQuorum(proposalIdToExecute) == true); + + assert(_emergencyExecutor.proposals(proposalIdToExecute) == true); + } +} + +contract Executor__mock { + error NotEmergencyCommittee(address sender); + error ProposalAlreadyExecuted(); + + mapping(uint256 => bool) public proposals; + address private committee; + + function emergencyExecute(uint256 _proposalId) public { + if (proposals[_proposalId] == true) { + revert ProposalAlreadyExecuted(); + } + + if (msg.sender != committee) { + revert NotEmergencyCommittee(msg.sender); + } + + proposals[_proposalId] = true; + } +} diff --git a/test/utils/interfaces.sol b/test/utils/interfaces.sol index 625c8e3d..f9865829 100644 --- a/test/utils/interfaces.sol +++ b/test/utils/interfaces.sol @@ -76,3 +76,25 @@ interface IWithdrawalQueue { function getLastFinalizedRequestId() external view returns (uint256); function finalize(uint256 _lastRequestIdToBeFinalized, uint256 _maxShareRate) external payable; } + +interface INodeOperatorsRegistry { + function getNodeOperator( + uint256 _id, + bool _fullInfo + ) + external + view + returns ( + bool active, + string memory name, + address rewardAddress, + uint64 stakingLimit, + uint64 stoppedValidators, + uint64 totalSigningKeys, + uint64 usedSigningKeys + ); + + function getNodeOperatorsCount() external view returns (uint256); + function getActiveNodeOperatorsCount() external view returns (uint256); + function getNodeOperatorIsActive(uint256 _nodeOperatorId) external view returns (bool); +} diff --git a/test/utils/mainnet-addresses.sol b/test/utils/mainnet-addresses.sol index 3de321ee..dda7876e 100644 --- a/test/utils/mainnet-addresses.sol +++ b/test/utils/mainnet-addresses.sol @@ -9,3 +9,4 @@ address constant WST_ETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; address constant LDO_TOKEN = 0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32; address constant WITHDRAWAL_QUEUE = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; address constant BURNER = 0xD15a672319Cf0352560eE76d9e89eAB0889046D3; +address constant NODE_OPERATORS_REGISTRY = 0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5; From aaa4c37e0d23774a04980d568cd97ad1cbe99308 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 20 Mar 2024 11:43:36 +0300 Subject: [PATCH 03/38] gp tiebraker --- contracts/Tiebreaker.sol | 110 ++++++++++++++++++++++----------------- 1 file changed, 63 insertions(+), 47 deletions(-) diff --git a/contracts/Tiebreaker.sol b/contracts/Tiebreaker.sol index bb06c81d..d7658dff 100644 --- a/contracts/Tiebreaker.sol +++ b/contracts/Tiebreaker.sol @@ -1,72 +1,74 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -interface IEmergencyExecutor { - function emergencyExecute(uint256 proposalId) external; -} - /** - * A contract provides ability to execute locked proposals. + * A contract provides ability to execute . */ -contract Tiebreaker is IEmergencyExecutor { - error SenderIsNotMember(); - error SenderIsNotOwner(); +abstract contract Tiebreaker { + event HashApproved(bytes32 indexed approvedHash, address indexed owner); + event ExecutionSuccess(bytes32 txHash); + event MemberAdded(address indexed newMember); + + error Initialized(); error IsNotMember(); - error ProposalIsNotSupported(); - error ProposalAlreadySupported(); - error ProposalAlreadyExecuted(uint256 proposalId); error ZeroQuorum(); + error NoQourum(); + error SenderIsNotMember(); + error SenderIsNotOwner(); - address executor; + bool isInitialized; - mapping(address => bool) public members; address public owner; - address[] public membersList; - struct ProposalState { - address[] supportersList; - mapping(address => bool) supporters; - bool isExecuted; - } + address[] membersList; + mapping(address => bool) members; + uint256 quorum; - mapping(uint256 => ProposalState) proposals; + mapping(address => mapping(bytes32 => bool)) public approves; + mapping(bytes32 => address[]) signers; - constructor(address _owner, address[] memory _members, address _executor) { - owner = _owner; - membersList = _members; - executor = _executor; + uint256 public nonce; - for (uint256 i = 0; i < _members.length; ++i) { - members[_members[i]] = true; + function initialize(address[] memory _members, uint256 _quorum) public { + if (isInitialized) { + revert Initialized(); } - } - function emergencyExecute(uint256 _proposalId) public onlyMember { - if (proposals[_proposalId].supporters[msg.sender] == true) { - revert ProposalAlreadySupported(); - } + isInitialized = true; - proposals[_proposalId].supportersList.push(msg.sender); - proposals[_proposalId].supporters[msg.sender] = true; - } + quorum = _quorum; - function forwardExecution(uint256 _proposalId) public { - if (!hasQuorum(_proposalId)) { - revert ProposalIsNotSupported(); + for (uint256 i = 0; i < _members.length; i++) { + _addMember(_members[i]); } + } - if (proposals[_proposalId].isExecuted == true) { - revert ProposalAlreadyExecuted(_proposalId); + function execTransaction(address _to, bytes calldata _data) public payable returns (bool, bytes memory) { + nonce++; + bytes32 txHash = getTransactionHash(_to, _data, nonce); + + if (signers[txHash].length < quorum) { + revert NoQourum(); } - IEmergencyExecutor(executor).emergencyExecute(_proposalId); + (bool success, bytes memory data) = _to.call(_data); + + emit ExecutionSuccess(txHash); + + return (success, data); + } - proposals[_proposalId].isExecuted = true; + /** + * @dev Marks a hash as approved. This can be used to validate a hash that is used by a signature. + * @param _hashToApprove The hash that should be marked as approved for signatures that are verified by this contract. + */ + function approveHash(bytes32 _hashToApprove) public onlyMember { + approves[msg.sender][_hashToApprove] = true; + emit HashApproved(_hashToApprove, msg.sender); } function addMember(address _newMember) public onlyOwner { - membersList.push(_newMember); - members[_newMember] = true; + _addMember(_newMember); } function removeMember(address _member) public onlyOwner { @@ -83,21 +85,35 @@ contract Tiebreaker is IEmergencyExecutor { } } - function hasQuorum(uint256 _proposalId) public view returns (bool) { + /// @dev Returns hash to be signed by owners. + /// @param _to Destination address. + /// @param _data Data payload. + /// @param _nonce Transaction nonce. + /// @return Transaction hash. + function getTransactionHash(address _to, bytes calldata _data, uint256 _nonce) public pure returns (bytes32) { + return keccak256(abi.encode(_to, _data, _nonce)); + } + + function hasQuorum(bytes32 _txHash) public view returns (bool) { uint256 supportersCount = 0; - uint256 quorum = membersList.length / 2 + 1; if (quorum == 0) { revert ZeroQuorum(); } - for (uint256 i = 0; i < proposals[_proposalId].supportersList.length; ++i) { - if (members[proposals[_proposalId].supportersList[i]] == true) { + for (uint256 i = 0; i < signers[_txHash].length; ++i) { + if (members[signers[_txHash][i]] == true) { supportersCount++; } } return supportersCount >= quorum; } + function _addMember(address _newMember) internal { + membersList.push(_newMember); + members[_newMember] = true; + emit MemberAdded(_newMember); + } + modifier onlyMember() { if (members[msg.sender] == false) { revert SenderIsNotMember(); From a5b22c234b7d9c7727f603f37f0352e751359773 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 20 Mar 2024 12:38:18 +0300 Subject: [PATCH 04/38] base tiebreaker test --- contracts/Tiebreaker.sol | 10 ++- test/scenario/tiebraker.t.sol | 115 +++++++++++++++++++++------------- 2 files changed, 80 insertions(+), 45 deletions(-) diff --git a/contracts/Tiebreaker.sol b/contracts/Tiebreaker.sol index d7658dff..f2292910 100644 --- a/contracts/Tiebreaker.sol +++ b/contracts/Tiebreaker.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.23; /** * A contract provides ability to execute . */ -abstract contract Tiebreaker { +contract Tiebreaker { event HashApproved(bytes32 indexed approvedHash, address indexed owner); event ExecutionSuccess(bytes32 txHash); event MemberAdded(address indexed newMember); @@ -15,6 +15,7 @@ abstract contract Tiebreaker { error NoQourum(); error SenderIsNotMember(); error SenderIsNotOwner(); + error ExecutionFailed(); bool isInitialized; @@ -29,7 +30,7 @@ abstract contract Tiebreaker { uint256 public nonce; - function initialize(address[] memory _members, uint256 _quorum) public { + function initialize(address _owner, address[] memory _members, uint256 _quorum) public { if (isInitialized) { revert Initialized(); } @@ -37,6 +38,7 @@ abstract contract Tiebreaker { isInitialized = true; quorum = _quorum; + owner = _owner; for (uint256 i = 0; i < _members.length; i++) { _addMember(_members[i]); @@ -52,6 +54,9 @@ abstract contract Tiebreaker { } (bool success, bytes memory data) = _to.call(_data); + if (success == false) { + revert ExecutionFailed(); + } emit ExecutionSuccess(txHash); @@ -64,6 +69,7 @@ abstract contract Tiebreaker { */ function approveHash(bytes32 _hashToApprove) public onlyMember { approves[msg.sender][_hashToApprove] = true; + signers[_hashToApprove].push(msg.sender); emit HashApproved(_hashToApprove, msg.sender); } diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol index 83f19425..b3802f96 100644 --- a/test/scenario/tiebraker.t.sol +++ b/test/scenario/tiebraker.t.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.23; import {Test, console} from "forge-std/Test.sol"; import {DualGovernanceDeployScript, DualGovernance, EmergencyProtectedTimelock} from "script/Deploy.s.sol"; import {Tiebreaker} from "contracts/Tiebreaker.sol"; -import {TiebreakerNOR} from "contracts/TiebreakerNOR.sol"; import {Utils} from "../utils/utils.sol"; import {INodeOperatorsRegistry} from "../utils/interfaces.sol"; @@ -15,28 +14,34 @@ contract TiebreakerScenarioTest is Test { Tiebreaker private _coreTiebreaker; Tiebreaker private _efTiebraker; - TiebreakerNOR private _norTiebreaker; + Tiebreaker private _norTiebreaker; + + uint256 private _efMembersCount = 5; + uint256 private _efQuorum = 3; - uint256 private efMembersCount = 5; address[] private _efTiebrakerMembers; + address[] private _coreTiebrakerMembers; function setUp() external { Utils.selectFork(); - _emergencyExecutor = new Executor__mock(); + _coreTiebreaker = new Tiebreaker(); - _coreTiebreaker = new Tiebreaker(address(this), new address[](0), address(_emergencyExecutor)); + _emergencyExecutor = new Executor__mock(address(_coreTiebreaker)); - for (uint256 i = 0; i < 5; i++) { + for (uint256 i = 0; i < _efMembersCount; i++) { _efTiebrakerMembers.push(makeAddr(string(abi.encode(i + 65)))); } - _efTiebraker = new Tiebreaker(address(this), _efTiebrakerMembers, address(_coreTiebreaker)); + _efTiebraker = new Tiebreaker(); + + _norTiebreaker = new Tiebreaker(); + + _efTiebraker.initialize(address(this), _efTiebrakerMembers, _efQuorum); - _norTiebreaker = new TiebreakerNOR(NODE_OPERATORS_REGISTRY, address(_coreTiebreaker)); + _coreTiebrakerMembers.push(address(_efTiebraker)); - _coreTiebreaker.addMember(address(_efTiebraker)); - _coreTiebreaker.addMember(address(_norTiebreaker)); + _coreTiebreaker.initialize(address(this), _coreTiebrakerMembers, 1); } function test_proposal_execution() external { @@ -44,55 +49,75 @@ contract TiebreakerScenarioTest is Test { assert(_emergencyExecutor.proposals(proposalIdToExecute) == false); - for (uint256 i = 0; i < _efTiebrakerMembers.length / 2; i++) { - vm.prank(_efTiebrakerMembers[i]); - _efTiebraker.emergencyExecute(proposalIdToExecute); + bytes32 execProposalHash = _prepareExecuteProposalHash(address(_emergencyExecutor), proposalIdToExecute, 1); + bytes32 execApproveHash = _prepareApproveHashHash(address(_coreTiebreaker), execProposalHash, 1); - assert(_efTiebraker.hasQuorum(proposalIdToExecute) == false); + for (uint256 i = 0; i < _efQuorum - 1; i++) { + vm.prank(_efTiebrakerMembers[i]); + _efTiebraker.approveHash(execApproveHash); + assert(_efTiebraker.hasQuorum(execApproveHash) == false); } vm.prank(_efTiebrakerMembers[_efTiebrakerMembers.length - 1]); - _efTiebraker.emergencyExecute(proposalIdToExecute); + _efTiebraker.approveHash(execApproveHash); - assert(_efTiebraker.hasQuorum(proposalIdToExecute) == true); + assert(_efTiebraker.hasQuorum(execApproveHash) == true); - assert(_coreTiebreaker.hasQuorum(proposalIdToExecute) == false); + _efTiebraker.execTransaction( + address(_coreTiebreaker), abi.encodeWithSignature("approveHash(bytes32)", execProposalHash) + ); - _efTiebraker.forwardExecution(proposalIdToExecute); + // assert(_coreTiebreaker.hasQuorum(proposalIdToExecute) == false); - assert(_coreTiebreaker.hasQuorum(proposalIdToExecute) == false); + // uint256 participatedNOCount = 0; + // uint256 requiredOperatorsCount = + // INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getActiveNodeOperatorsCount() / 2 + 1; - uint256 participatedNOCount = 0; - uint256 requiredOperatorsCount = - INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getActiveNodeOperatorsCount() / 2 + 1; + // for (uint256 i = 0; i < INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getNodeOperatorsCount(); i++) { + // ( + // bool active, + // , //string memory name, + // address rewardAddress, + // , //uint64 stakingLimit, + // , //uint64 stoppedValidators, + // , //uint64 totalSigningKeys, + // //uint64 usedSigningKeys + // ) = INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getNodeOperator(i, false); + // if (active) { + // vm.prank(rewardAddress); + // _norTiebreaker.emergencyExecute(proposalIdToExecute, i); - for (uint256 i = 0; i < INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getNodeOperatorsCount(); i++) { - ( - bool active, - , //string memory name, - address rewardAddress, - , //uint64 stakingLimit, - , //uint64 stoppedValidators, - , //uint64 totalSigningKeys, - //uint64 usedSigningKeys - ) = INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getNodeOperator(i, false); - if (active) { - vm.prank(rewardAddress); - _norTiebreaker.emergencyExecute(proposalIdToExecute, i); + // participatedNOCount++; + // } + // if (participatedNOCount >= requiredOperatorsCount) break; + // } - participatedNOCount++; - } - if (participatedNOCount >= requiredOperatorsCount) break; - } + // assert(_norTiebreaker.hasQuorum(proposalIdToExecute) == true); - assert(_norTiebreaker.hasQuorum(proposalIdToExecute) == true); + // _norTiebreaker.forwardExecution(proposalIdToExecute); - _norTiebreaker.forwardExecution(proposalIdToExecute); + assert(_coreTiebreaker.hasQuorum(execProposalHash) == true); - assert(_coreTiebreaker.hasQuorum(proposalIdToExecute) == true); + _coreTiebreaker.execTransaction( + address(_emergencyExecutor), abi.encodeWithSignature("tiebreaExecute(uint256)", proposalIdToExecute) + ); assert(_emergencyExecutor.proposals(proposalIdToExecute) == true); } + + function _prepareApproveHashHash(address _to, bytes32 _hash, uint256 _nonce) public view returns (bytes32) { + return _efTiebraker.getTransactionHash(_to, abi.encodeWithSignature("approveHash(bytes32)", _hash), _nonce); + } + + function _prepareExecuteProposalHash( + address _to, + uint256 _proposalId, + uint256 _nonce + ) public view returns (bytes32) { + return _coreTiebreaker.getTransactionHash( + _to, abi.encodeWithSignature("tiebreaExecute(uint256)", _proposalId), _nonce + ); + } } contract Executor__mock { @@ -102,7 +127,11 @@ contract Executor__mock { mapping(uint256 => bool) public proposals; address private committee; - function emergencyExecute(uint256 _proposalId) public { + constructor(address _committee) { + committee = _committee; + } + + function tiebreaExecute(uint256 _proposalId) public { if (proposals[_proposalId] == true) { revert ProposalAlreadyExecuted(); } From 9d1f90e3eefdf1bff906d50e6672e543f48cb7e6 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 20 Mar 2024 13:00:19 +0300 Subject: [PATCH 05/38] Node operators tiebreaker --- contracts/TiebreakerNOR.sol | 89 ++++++++++++++++++++--------------- test/scenario/tiebraker.t.sol | 66 ++++++++++++++------------ 2 files changed, 87 insertions(+), 68 deletions(-) diff --git a/contracts/TiebreakerNOR.sol b/contracts/TiebreakerNOR.sol index f8060397..5bb93224 100644 --- a/contracts/TiebreakerNOR.sol +++ b/contracts/TiebreakerNOR.sol @@ -1,10 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -interface IEmergencyExecutor { - function emergencyExecute(uint256 proposalId) external; -} - interface INodeOperatorsRegistry { function getNodeOperator( uint256 _id, @@ -31,61 +27,80 @@ interface INodeOperatorsRegistry { * A contract provides ability to execute locked proposals. */ contract TiebreakerNOR { + event HashApproved(bytes32 indexed approvedHash, uint256 nodeOperatorId, address indexed owner); + event ExecutionSuccess(bytes32 txHash); + event MemberAdded(address indexed newMember); + + error Initialized(); + error IsNotMember(); + error ZeroQuorum(); + error NoQourum(); error SenderIsNotMember(); - error ProposalIsNotSupported(); - error ProposalSupported(); - error ProposalAlreadyExecuted(uint256 proposalId); + error SenderIsNotOwner(); + error ExecutionFailed(); - address public executor; + bool isInitialized; address public nodeOperatorsRegistry; - struct ProposalState { - uint256[] supportersList; - mapping(address => bool) supporters; - bool isExecuted; - } + mapping(uint256 => mapping(bytes32 => bool)) public approves; + mapping(bytes32 => uint256[]) signers; - mapping(uint256 => ProposalState) proposals; + uint256 public nonce; - constructor(address _nodeOperatorsRegistry, address _executor) { + function initialize(address _nodeOperatorsRegistry) public { + if (isInitialized) { + revert Initialized(); + } + + isInitialized = true; nodeOperatorsRegistry = _nodeOperatorsRegistry; - executor = _executor; } - function emergencyExecute(uint256 _proposalId, uint256 _nodeOperatorId) public onlyNodeOperator(_nodeOperatorId) { - if (proposals[_proposalId].supporters[msg.sender] == true) { - revert ProposalSupported(); - } - proposals[_proposalId].supportersList.push(_nodeOperatorId); - proposals[_proposalId].supporters[msg.sender] = true; - } + function execTransaction(address _to, bytes calldata _data) public payable returns (bool, bytes memory) { + nonce++; + bytes32 txHash = getTransactionHash(_to, _data, nonce); - function forwardExecution(uint256 _proposalId) public { - if (!hasQuorum(_proposalId)) { - revert ProposalIsNotSupported(); + if (hasQuorum(txHash) == false) { + revert NoQourum(); } - if (proposals[_proposalId].isExecuted == true) { - revert ProposalAlreadyExecuted(_proposalId); + (bool success, bytes memory data) = _to.call(_data); + if (success == false) { + revert ExecutionFailed(); } - IEmergencyExecutor(executor).emergencyExecute(_proposalId); + emit ExecutionSuccess(txHash); + + return (success, data); + } + + /** + * @dev Marks a hash as approved. This can be used to validate a hash that is used by a signature. + * @param _hashToApprove The hash that should be marked as approved for signatures that are verified by this contract. + */ + function approveHash(bytes32 _hashToApprove, uint256 _nodeOperatorId) public onlyNodeOperator(_nodeOperatorId) { + approves[_nodeOperatorId][_hashToApprove] = true; + signers[_hashToApprove].push(_nodeOperatorId); + emit HashApproved(_hashToApprove, _nodeOperatorId, msg.sender); + } - proposals[_proposalId].isExecuted = true; + /// @dev Returns hash to be signed by owners. + /// @param _to Destination address. + /// @param _data Data payload. + /// @param _nonce Transaction nonce. + /// @return Transaction hash. + function getTransactionHash(address _to, bytes calldata _data, uint256 _nonce) public pure returns (bytes32) { + return keccak256(abi.encode(_to, _data, _nonce)); } - function hasQuorum(uint256 _proposalId) public view returns (bool) { + function hasQuorum(bytes32 _txHash) public view returns (bool) { uint256 activeNOCount = INodeOperatorsRegistry(nodeOperatorsRegistry).getActiveNodeOperatorsCount(); uint256 quorum = activeNOCount / 2 + 1; uint256 supportersCount = 0; - for (uint256 i = 0; i < proposals[_proposalId].supportersList.length; ++i) { - if ( - INodeOperatorsRegistry(nodeOperatorsRegistry).getNodeOperatorIsActive( - proposals[_proposalId].supportersList[i] - ) == true - ) { + for (uint256 i = 0; i < signers[_txHash].length; ++i) { + if (INodeOperatorsRegistry(nodeOperatorsRegistry).getNodeOperatorIsActive(signers[_txHash][i]) == true) { supportersCount++; } } diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol index b3802f96..8a82de63 100644 --- a/test/scenario/tiebraker.t.sol +++ b/test/scenario/tiebraker.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.23; import {Test, console} from "forge-std/Test.sol"; import {DualGovernanceDeployScript, DualGovernance, EmergencyProtectedTimelock} from "script/Deploy.s.sol"; import {Tiebreaker} from "contracts/Tiebreaker.sol"; +import {TiebreakerNOR} from "contracts/TiebreakerNOR.sol"; import {Utils} from "../utils/utils.sol"; import {INodeOperatorsRegistry} from "../utils/interfaces.sol"; @@ -14,7 +15,7 @@ contract TiebreakerScenarioTest is Test { Tiebreaker private _coreTiebreaker; Tiebreaker private _efTiebraker; - Tiebreaker private _norTiebreaker; + TiebreakerNOR private _norTiebreaker; uint256 private _efMembersCount = 5; uint256 private _efQuorum = 3; @@ -35,13 +36,15 @@ contract TiebreakerScenarioTest is Test { _efTiebraker = new Tiebreaker(); - _norTiebreaker = new Tiebreaker(); + _norTiebreaker = new TiebreakerNOR(); _efTiebraker.initialize(address(this), _efTiebrakerMembers, _efQuorum); - _coreTiebrakerMembers.push(address(_efTiebraker)); - _coreTiebreaker.initialize(address(this), _coreTiebrakerMembers, 1); + _norTiebreaker.initialize(NODE_OPERATORS_REGISTRY); + _coreTiebrakerMembers.push(address(_norTiebreaker)); + + _coreTiebreaker.initialize(address(this), _coreTiebrakerMembers, 2); } function test_proposal_execution() external { @@ -67,35 +70,36 @@ contract TiebreakerScenarioTest is Test { address(_coreTiebreaker), abi.encodeWithSignature("approveHash(bytes32)", execProposalHash) ); - // assert(_coreTiebreaker.hasQuorum(proposalIdToExecute) == false); - - // uint256 participatedNOCount = 0; - // uint256 requiredOperatorsCount = - // INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getActiveNodeOperatorsCount() / 2 + 1; - - // for (uint256 i = 0; i < INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getNodeOperatorsCount(); i++) { - // ( - // bool active, - // , //string memory name, - // address rewardAddress, - // , //uint64 stakingLimit, - // , //uint64 stoppedValidators, - // , //uint64 totalSigningKeys, - // //uint64 usedSigningKeys - // ) = INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getNodeOperator(i, false); - // if (active) { - // vm.prank(rewardAddress); - // _norTiebreaker.emergencyExecute(proposalIdToExecute, i); - - // participatedNOCount++; - // } - // if (participatedNOCount >= requiredOperatorsCount) break; - // } - - // assert(_norTiebreaker.hasQuorum(proposalIdToExecute) == true); + assert(_coreTiebreaker.hasQuorum(execProposalHash) == false); + + uint256 participatedNOCount = 0; + uint256 requiredOperatorsCount = + INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getActiveNodeOperatorsCount() / 2 + 1; + + for (uint256 i = 0; i < INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getNodeOperatorsCount(); i++) { + ( + bool active, + , //string memory name, + address rewardAddress, + , //uint64 stakingLimit, + , //uint64 stoppedValidators, + , //uint64 totalSigningKeys, + //uint64 usedSigningKeys + ) = INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getNodeOperator(i, false); + if (active) { + vm.prank(rewardAddress); + _norTiebreaker.approveHash(execApproveHash, i); + + participatedNOCount++; + } + if (participatedNOCount >= requiredOperatorsCount) break; + } - // _norTiebreaker.forwardExecution(proposalIdToExecute); + assert(_norTiebreaker.hasQuorum(execApproveHash) == true); + _norTiebreaker.execTransaction( + address(_coreTiebreaker), abi.encodeWithSignature("approveHash(bytes32)", execProposalHash) + ); assert(_coreTiebreaker.hasQuorum(execProposalHash) == true); _coreTiebreaker.execTransaction( From c82b350ccbc45aaa3350ed1f6b227d790953f143 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Mon, 1 Apr 2024 09:54:53 +0300 Subject: [PATCH 06/38] address lib usage --- contracts/Tiebreaker.sol | 43 ++++++++++++++++-------- contracts/TiebreakerNOR.sol | 62 +++++++++++++++++++---------------- test/scenario/tiebraker.t.sol | 15 ++++----- 3 files changed, 69 insertions(+), 51 deletions(-) diff --git a/contracts/Tiebreaker.sol b/contracts/Tiebreaker.sol index f2292910..049eb680 100644 --- a/contracts/Tiebreaker.sol +++ b/contracts/Tiebreaker.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + /** * A contract provides ability to execute . */ @@ -45,22 +47,14 @@ contract Tiebreaker { } } - function execTransaction(address _to, bytes calldata _data) public payable returns (bool, bytes memory) { + function execTransaction(address _to, bytes calldata _data, uint256 _value) public payable returns (bytes memory) { nonce++; - bytes32 txHash = getTransactionHash(_to, _data, nonce); + bytes32 txHash = getTransactionHash(_to, _data, _value, nonce); - if (signers[txHash].length < quorum) { + if (hasQuorum(txHash) == false) { revert NoQourum(); } - - (bool success, bytes memory data) = _to.call(_data); - if (success == false) { - revert ExecutionFailed(); - } - - emit ExecutionSuccess(txHash); - - return (success, data); + return Address.functionCallWithValue(_to, _data, _value); } /** @@ -73,6 +67,21 @@ contract Tiebreaker { emit HashApproved(_hashToApprove, msg.sender); } + /** + * @dev Marks a hash as approved. This can be used to validate a hash that is used by a signature. + * @param _hashToReject The hash that should be marked as approved for signatures that are verified by this contract. + */ + function rejectHash(bytes32 _hashToReject) public onlyMember { + approves[msg.sender][_hashToReject] = false; + for (uint256 i = 0; i < signers[_hashToReject].length; ++i) { + if (signers[_hashToReject][i] == msg.sender) { + signers[_hashToReject][i] = signers[_hashToReject][signers[_hashToReject].length - 1]; + signers[_hashToReject].pop(); + break; + } + } + } + function addMember(address _newMember) public onlyOwner { _addMember(_newMember); } @@ -94,10 +103,16 @@ contract Tiebreaker { /// @dev Returns hash to be signed by owners. /// @param _to Destination address. /// @param _data Data payload. + /// @param _value ETH value to transfer /// @param _nonce Transaction nonce. /// @return Transaction hash. - function getTransactionHash(address _to, bytes calldata _data, uint256 _nonce) public pure returns (bytes32) { - return keccak256(abi.encode(_to, _data, _nonce)); + function getTransactionHash( + address _to, + bytes calldata _data, + uint256 _value, + uint256 _nonce + ) public pure returns (bytes32) { + return keccak256(abi.encode(_to, _data, _value, _nonce)); } function hasQuorum(bytes32 _txHash) public view returns (bool) { diff --git a/contracts/TiebreakerNOR.sol b/contracts/TiebreakerNOR.sol index 5bb93224..18179b23 100644 --- a/contracts/TiebreakerNOR.sol +++ b/contracts/TiebreakerNOR.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + interface INodeOperatorsRegistry { function getNodeOperator( uint256 _id, @@ -27,61 +29,60 @@ interface INodeOperatorsRegistry { * A contract provides ability to execute locked proposals. */ contract TiebreakerNOR { - event HashApproved(bytes32 indexed approvedHash, uint256 nodeOperatorId, address indexed owner); + event HashApproved(address to, bytes data, uint256 nonce, address indexed member); event ExecutionSuccess(bytes32 txHash); - event MemberAdded(address indexed newMember); - error Initialized(); - error IsNotMember(); error ZeroQuorum(); error NoQourum(); error SenderIsNotMember(); error SenderIsNotOwner(); - error ExecutionFailed(); + error NonceAlreadyUsed(); - bool isInitialized; address public nodeOperatorsRegistry; - mapping(uint256 => mapping(bytes32 => bool)) public approves; - mapping(bytes32 => uint256[]) signers; + mapping(bytes32 txHash => uint256[] signers) signers; + mapping(uint256 nodeOperatorId => mapping(bytes32 txHash => bool isApproved)) approves; uint256 public nonce; - function initialize(address _nodeOperatorsRegistry) public { - if (isInitialized) { - revert Initialized(); - } - - isInitialized = true; + constructor(address _nodeOperatorsRegistry) { nodeOperatorsRegistry = _nodeOperatorsRegistry; } - function execTransaction(address _to, bytes calldata _data) public payable returns (bool, bytes memory) { + function execTransaction(address _to, bytes calldata _data, uint256 _value) public payable returns (bytes memory) { nonce++; - bytes32 txHash = getTransactionHash(_to, _data, nonce); + bytes32 txHash = getTransactionHash(_to, _data, _value, nonce); if (hasQuorum(txHash) == false) { revert NoQourum(); } - - (bool success, bytes memory data) = _to.call(_data); - if (success == false) { - revert ExecutionFailed(); - } - - emit ExecutionSuccess(txHash); - - return (success, data); + return Address.functionCallWithValue(_to, _data, _value); } /** * @dev Marks a hash as approved. This can be used to validate a hash that is used by a signature. * @param _hashToApprove The hash that should be marked as approved for signatures that are verified by this contract. + * @param _nodeOperatorId Node Operator ID of msg.sender */ function approveHash(bytes32 _hashToApprove, uint256 _nodeOperatorId) public onlyNodeOperator(_nodeOperatorId) { approves[_nodeOperatorId][_hashToApprove] = true; signers[_hashToApprove].push(_nodeOperatorId); - emit HashApproved(_hashToApprove, _nodeOperatorId, msg.sender); + } + + /** + * @dev Marks a hash as approved. This can be used to validate a hash that is used by a signature. + * @param _hashToReject The hash that should be marked as approved for signatures that are verified by this contract. + * @param _nodeOperatorId Node Operator ID of msg.sender + */ + function rejectHash(bytes32 _hashToReject, uint256 _nodeOperatorId) public onlyNodeOperator(_nodeOperatorId) { + approves[_nodeOperatorId][_hashToReject] = false; + for (uint256 i = 0; i < signers[_hashToReject].length; ++i) { + if (signers[_hashToReject][i] == _nodeOperatorId) { + signers[_hashToReject][i] = signers[_hashToReject][signers[_hashToReject].length - 1]; + signers[_hashToReject].pop(); + break; + } + } } /// @dev Returns hash to be signed by owners. @@ -89,8 +90,13 @@ contract TiebreakerNOR { /// @param _data Data payload. /// @param _nonce Transaction nonce. /// @return Transaction hash. - function getTransactionHash(address _to, bytes calldata _data, uint256 _nonce) public pure returns (bytes32) { - return keccak256(abi.encode(_to, _data, _nonce)); + function getTransactionHash( + address _to, + bytes calldata _data, + uint256 _value, + uint256 _nonce + ) public pure returns (bytes32) { + return keccak256(abi.encode(_to, _data, _value, _nonce)); } function hasQuorum(bytes32 _txHash) public view returns (bool) { diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol index 8a82de63..0d460cc3 100644 --- a/test/scenario/tiebraker.t.sol +++ b/test/scenario/tiebraker.t.sol @@ -35,13 +35,10 @@ contract TiebreakerScenarioTest is Test { } _efTiebraker = new Tiebreaker(); - - _norTiebreaker = new TiebreakerNOR(); - _efTiebraker.initialize(address(this), _efTiebrakerMembers, _efQuorum); _coreTiebrakerMembers.push(address(_efTiebraker)); - _norTiebreaker.initialize(NODE_OPERATORS_REGISTRY); + _norTiebreaker = new TiebreakerNOR(NODE_OPERATORS_REGISTRY); _coreTiebrakerMembers.push(address(_norTiebreaker)); _coreTiebreaker.initialize(address(this), _coreTiebrakerMembers, 2); @@ -67,7 +64,7 @@ contract TiebreakerScenarioTest is Test { assert(_efTiebraker.hasQuorum(execApproveHash) == true); _efTiebraker.execTransaction( - address(_coreTiebreaker), abi.encodeWithSignature("approveHash(bytes32)", execProposalHash) + address(_coreTiebreaker), abi.encodeWithSignature("approveHash(bytes32)", execProposalHash), 0 ); assert(_coreTiebreaker.hasQuorum(execProposalHash) == false); @@ -98,19 +95,19 @@ contract TiebreakerScenarioTest is Test { assert(_norTiebreaker.hasQuorum(execApproveHash) == true); _norTiebreaker.execTransaction( - address(_coreTiebreaker), abi.encodeWithSignature("approveHash(bytes32)", execProposalHash) + address(_coreTiebreaker), abi.encodeWithSignature("approveHash(bytes32)", execProposalHash), 0 ); assert(_coreTiebreaker.hasQuorum(execProposalHash) == true); _coreTiebreaker.execTransaction( - address(_emergencyExecutor), abi.encodeWithSignature("tiebreaExecute(uint256)", proposalIdToExecute) + address(_emergencyExecutor), abi.encodeWithSignature("tiebreaExecute(uint256)", proposalIdToExecute), 0 ); assert(_emergencyExecutor.proposals(proposalIdToExecute) == true); } function _prepareApproveHashHash(address _to, bytes32 _hash, uint256 _nonce) public view returns (bytes32) { - return _efTiebraker.getTransactionHash(_to, abi.encodeWithSignature("approveHash(bytes32)", _hash), _nonce); + return _efTiebraker.getTransactionHash(_to, abi.encodeWithSignature("approveHash(bytes32)", _hash), 0, _nonce); } function _prepareExecuteProposalHash( @@ -119,7 +116,7 @@ contract TiebreakerScenarioTest is Test { uint256 _nonce ) public view returns (bytes32) { return _coreTiebreaker.getTransactionHash( - _to, abi.encodeWithSignature("tiebreaExecute(uint256)", _proposalId), _nonce + _to, abi.encodeWithSignature("tiebreaExecute(uint256)", _proposalId), 0, _nonce ); } } From 146ba7c26be2a7af9cf8e73dffb2e91210dc81e8 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 4 Apr 2024 13:43:48 +0300 Subject: [PATCH 07/38] restricted multisigs --- contracts/EmergencyActivationMultisig.sol | 47 +++++++ contracts/EmergencyExecutionMultisig.sol | 74 +++++++++++ contracts/RestrictedMultisigBase.sol | 155 ++++++++++++++++++++++ contracts/Tiebreaker.sol | 151 --------------------- contracts/TiebreakerCore.sol | 51 +++++++ contracts/TiebreakerNOR.sol | 133 ------------------- contracts/TiebreakerSubDAO.sol | 51 +++++++ test/scenario/tiebraker.t.sol | 149 ++++++++++----------- 8 files changed, 446 insertions(+), 365 deletions(-) create mode 100644 contracts/EmergencyActivationMultisig.sol create mode 100644 contracts/EmergencyExecutionMultisig.sol create mode 100644 contracts/RestrictedMultisigBase.sol delete mode 100644 contracts/Tiebreaker.sol create mode 100644 contracts/TiebreakerCore.sol delete mode 100644 contracts/TiebreakerNOR.sol create mode 100644 contracts/TiebreakerSubDAO.sol diff --git a/contracts/EmergencyActivationMultisig.sol b/contracts/EmergencyActivationMultisig.sol new file mode 100644 index 00000000..a1722d26 --- /dev/null +++ b/contracts/EmergencyActivationMultisig.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {RestrictedMultisigBase} from "./RestrictedMultisigBase.sol"; + +interface IEmergencyProtectedTimelock { + function emergencyActivate() external; +} + +contract EmergencyActivationMultisig is RestrictedMultisigBase { + uint256 public constant EMERGENCY_ACTIVATE = 1; + + address emergencyProtectedTimelock; + + constructor( + address _owner, + address[] memory _members, + uint256 _quorum, + address _emergencyProtectedTimelock + ) RestrictedMultisigBase(_owner, _members, _quorum) { + emergencyProtectedTimelock = _emergencyProtectedTimelock; + } + + function voteEmergencyActivate() public onlyMember { + _vote(_buildEmergencyActivateAction(), true); + } + + function getEmergencyActivateState() public returns (uint256 support, uint256 ExecutionQuorum, bool isExecuted) { + return _getState(_buildEmergencyActivateAction()); + } + + function emergencyActivate() external { + _execute(_buildEmergencyActivateAction()); + } + + function _issueCalls(Action memory _action) internal override { + if (_action.actionType == EMERGENCY_ACTIVATE) { + IEmergencyProtectedTimelock(emergencyProtectedTimelock).emergencyActivate(); + } else { + assert(false); + } + } + + function _buildEmergencyActivateAction() internal view returns (Action memory) { + return Action(EMERGENCY_ACTIVATE, new bytes(0), false, new address[](0)); + } +} diff --git a/contracts/EmergencyExecutionMultisig.sol b/contracts/EmergencyExecutionMultisig.sol new file mode 100644 index 00000000..d0f0aa14 --- /dev/null +++ b/contracts/EmergencyExecutionMultisig.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {RestrictedMultisigBase} from "./RestrictedMultisigBase.sol"; + +interface IEmergencyProtectedTimelock { + function emergencyExecute(uint256 proposalId) external; + function emergencyReset() external; +} + +contract EmergencyExecutionMultisig is RestrictedMultisigBase { + uint256 public constant EXECUTE_PROPOSAL = 1; + uint256 public constant RESET_GOVERNANCE = 2; + + address emergencyProtectedTimelock; + + constructor( + address _owner, + address[] memory _members, + uint256 _quorum, + address _emergencyProtectedTimelock + ) RestrictedMultisigBase(_owner, _members, _quorum) { + emergencyProtectedTimelock = _emergencyProtectedTimelock; + } + + // Proposal Execution + function voteExecuteProposal(uint256 _proposalId, bool _supports) public onlyMember { + _vote(_buildExecuteProposalAction(_proposalId), _supports); + } + + function getExecuteProposalState(uint256 _proposalId) + public + returns (uint256 support, uint256 ExecutionQuorum, bool isExecuted) + { + return _getState(_buildExecuteProposalAction(_proposalId)); + } + + function executeProposal(uint256 _proposalId) public { + _execute(_buildExecuteProposalAction(_proposalId)); + } + + // Governance reset + + function voteGoveranaceReset() public onlyMember { + _vote(_buildResetGovAction(), true); + } + + function getGovernanceResetState() public returns (uint256 support, uint256 ExecutionQuorum, bool isExecuted) { + return _getState(_buildResetGovAction()); + } + + function resetGovernance() external { + _execute(_buildResetGovAction()); + } + + function _issueCalls(Action memory _action) internal override { + if (_action.actionType == EXECUTE_PROPOSAL) { + uint256 proposalIdToExecute = abi.decode(_action.data, (uint256)); + IEmergencyProtectedTimelock(emergencyProtectedTimelock).emergencyExecute(proposalIdToExecute); + } else if (_action.actionType == RESET_GOVERNANCE) { + IEmergencyProtectedTimelock(emergencyProtectedTimelock).emergencyReset(); + } else { + assert(false); + } + } + + function _buildResetGovAction() internal view returns (Action memory) { + return Action(RESET_GOVERNANCE, new bytes(0), false, new address[](0)); + } + + function _buildExecuteProposalAction(uint256 proposalId) internal view returns (Action memory) { + return Action(EXECUTE_PROPOSAL, abi.encode(proposalId), false, new address[](0)); + } +} diff --git a/contracts/RestrictedMultisigBase.sol b/contracts/RestrictedMultisigBase.sol new file mode 100644 index 00000000..019a8880 --- /dev/null +++ b/contracts/RestrictedMultisigBase.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +abstract contract RestrictedMultisigBase { + error IsNotMember(); + error SenderIsNotMember(); + error SenderIsNotOwner(); + error DataIsNotEqual(); + + struct Action { + uint256 actionType; + bytes data; + bool isExecuted; + address[] signers; + } + + address public owner; + + address[] public membersList; + mapping(address => bool) public members; + uint256 public quorum; + + mapping(bytes32 actionHash => Action) actions; + mapping(address signer => mapping(bytes32 actionHash => bool support)) public approves; + + constructor(address _owner, address[] memory _members, uint256 _quorum) { + quorum = _quorum; + owner = _owner; + + for (uint256 i = 0; i < _members.length; ++i) { + _addMember(_members[i]); + } + } + + function _vote(Action memory _action, bool _supports) internal { + bytes32 actionHash = _hashAction(_action); + if (actions[actionHash].data.length == 0) { + actions[actionHash].actionType = _action.actionType; + actions[actionHash].data = _action.data; + } else { + _checkStoredAction(_action); + } + + if (approves[msg.sender][actionHash] == _supports) { + return; + } + + approves[msg.sender][actionHash] = _supports; + if (_supports == true) { + actions[actionHash].signers.push(msg.sender); + } else { + uint256 signersLength = actions[actionHash].signers.length; + for (uint256 i = 0; i < signersLength; ++i) { + if (actions[actionHash].signers[i] == msg.sender) { + actions[actionHash].signers[i] = actions[actionHash].signers[signersLength - 1]; + actions[actionHash].signers.pop(); + break; + } + } + } + } + + function _execute(Action memory _action) internal { + _checkStoredAction(_action); + + bytes32 actionHash = _hashAction(_action); + + require(actions[actionHash].isExecuted == false); + require(_getSuport(actionHash) >= quorum); + + _issueCalls(_action); + + actions[actionHash].isExecuted = true; + } + + function _getState(Action memory _action) + public + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + { + _checkStoredAction(_action); + + bytes32 actionHash = _hashAction(_action); + support = _getSuport(actionHash); + execuitionQuorum = quorum; + isExecuted = actions[actionHash].isExecuted; + } + + function addMember(address _newMember, uint256 _quorum) public onlyOwner { + _addMember(_newMember); + quorum = _quorum; + } + + function removeMember(address _member, uint256 _quorum) public onlyOwner { + if (members[_member] == false) { + revert IsNotMember(); + } + members[_member] = false; + for (uint256 i = 0; i < membersList.length; ++i) { + if (membersList[i] == _member) { + membersList[i] = membersList[membersList.length - 1]; + membersList.pop(); + break; + } + } + quorum = _quorum; + } + + function _addMember(address _newMember) internal { + membersList.push(_newMember); + members[_newMember] = true; + } + + function _issueCalls(Action memory _action) internal virtual; + + function _getSuport(bytes32 _actionHash) internal returns (uint256 support) { + for (uint256 i = 0; i < actions[_actionHash].signers.length; ++i) { + if (members[actions[_actionHash].signers[i]] == true) { + support++; + } + } + } + + function _checkStoredAction(Action memory _action) internal { + bytes32 actionHash = _hashAction(_action); + + require(_action.actionType > 0); + require(actions[actionHash].actionType == _action.actionType); + require(actions[actionHash].isExecuted == false); + + require(actions[actionHash].data.length == _action.data.length); + for (uint256 i = 0; i < _action.data.length; ++i) { + if (actions[actionHash].data[i] != _action.data[i]) { + revert DataIsNotEqual(); + } + } + } + + function _hashAction(Action memory _action) internal pure returns (bytes32) { + return keccak256(abi.encode(_action.actionType, _action.data)); + } + + modifier onlyMember() { + if (members[msg.sender] == false) { + revert SenderIsNotMember(); + } + _; + } + + modifier onlyOwner() { + if (msg.sender != owner) { + revert SenderIsNotOwner(); + } + _; + } +} diff --git a/contracts/Tiebreaker.sol b/contracts/Tiebreaker.sol deleted file mode 100644 index 049eb680..00000000 --- a/contracts/Tiebreaker.sol +++ /dev/null @@ -1,151 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; - -/** - * A contract provides ability to execute . - */ -contract Tiebreaker { - event HashApproved(bytes32 indexed approvedHash, address indexed owner); - event ExecutionSuccess(bytes32 txHash); - event MemberAdded(address indexed newMember); - - error Initialized(); - error IsNotMember(); - error ZeroQuorum(); - error NoQourum(); - error SenderIsNotMember(); - error SenderIsNotOwner(); - error ExecutionFailed(); - - bool isInitialized; - - address public owner; - - address[] membersList; - mapping(address => bool) members; - uint256 quorum; - - mapping(address => mapping(bytes32 => bool)) public approves; - mapping(bytes32 => address[]) signers; - - uint256 public nonce; - - function initialize(address _owner, address[] memory _members, uint256 _quorum) public { - if (isInitialized) { - revert Initialized(); - } - - isInitialized = true; - - quorum = _quorum; - owner = _owner; - - for (uint256 i = 0; i < _members.length; i++) { - _addMember(_members[i]); - } - } - - function execTransaction(address _to, bytes calldata _data, uint256 _value) public payable returns (bytes memory) { - nonce++; - bytes32 txHash = getTransactionHash(_to, _data, _value, nonce); - - if (hasQuorum(txHash) == false) { - revert NoQourum(); - } - return Address.functionCallWithValue(_to, _data, _value); - } - - /** - * @dev Marks a hash as approved. This can be used to validate a hash that is used by a signature. - * @param _hashToApprove The hash that should be marked as approved for signatures that are verified by this contract. - */ - function approveHash(bytes32 _hashToApprove) public onlyMember { - approves[msg.sender][_hashToApprove] = true; - signers[_hashToApprove].push(msg.sender); - emit HashApproved(_hashToApprove, msg.sender); - } - - /** - * @dev Marks a hash as approved. This can be used to validate a hash that is used by a signature. - * @param _hashToReject The hash that should be marked as approved for signatures that are verified by this contract. - */ - function rejectHash(bytes32 _hashToReject) public onlyMember { - approves[msg.sender][_hashToReject] = false; - for (uint256 i = 0; i < signers[_hashToReject].length; ++i) { - if (signers[_hashToReject][i] == msg.sender) { - signers[_hashToReject][i] = signers[_hashToReject][signers[_hashToReject].length - 1]; - signers[_hashToReject].pop(); - break; - } - } - } - - function addMember(address _newMember) public onlyOwner { - _addMember(_newMember); - } - - function removeMember(address _member) public onlyOwner { - if (members[_member] == false) { - revert IsNotMember(); - } - members[_member] = false; - for (uint256 i = 0; i < membersList.length; ++i) { - if (membersList[i] == _member) { - membersList[i] = membersList[membersList.length - 1]; - membersList.pop(); - break; - } - } - } - - /// @dev Returns hash to be signed by owners. - /// @param _to Destination address. - /// @param _data Data payload. - /// @param _value ETH value to transfer - /// @param _nonce Transaction nonce. - /// @return Transaction hash. - function getTransactionHash( - address _to, - bytes calldata _data, - uint256 _value, - uint256 _nonce - ) public pure returns (bytes32) { - return keccak256(abi.encode(_to, _data, _value, _nonce)); - } - - function hasQuorum(bytes32 _txHash) public view returns (bool) { - uint256 supportersCount = 0; - if (quorum == 0) { - revert ZeroQuorum(); - } - - for (uint256 i = 0; i < signers[_txHash].length; ++i) { - if (members[signers[_txHash][i]] == true) { - supportersCount++; - } - } - return supportersCount >= quorum; - } - - function _addMember(address _newMember) internal { - membersList.push(_newMember); - members[_newMember] = true; - emit MemberAdded(_newMember); - } - - modifier onlyMember() { - if (members[msg.sender] == false) { - revert SenderIsNotMember(); - } - _; - } - - modifier onlyOwner() { - if (msg.sender != owner) { - revert SenderIsNotOwner(); - } - _; - } -} diff --git a/contracts/TiebreakerCore.sol b/contracts/TiebreakerCore.sol new file mode 100644 index 00000000..9fed8b09 --- /dev/null +++ b/contracts/TiebreakerCore.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {RestrictedMultisigBase} from "./RestrictedMultisigBase.sol"; + +interface IDualGovernance { + function tiebreakerApproveProposal(uint256 _proposalId) external; +} + +contract TiebreakerCore is RestrictedMultisigBase { + uint256 public constant APPROVE_PROPOSAL = 1; + + address dualGovernance; + + constructor( + address _owner, + address[] memory _members, + uint256 _quorum, + address _dualGovernance + ) RestrictedMultisigBase(_owner, _members, _quorum) { + dualGovernance = _dualGovernance; + } + + function voteApproveProposal(uint256 _proposalId, bool _supports) public onlyMember { + _vote(_buildApproveProposalAction(_proposalId), _supports); + } + + function getApproveProposalState(uint256 _proposalId) + public + returns (uint256 support, uint256 ExecutionQuorum, bool isExecuted) + { + return _getState(_buildApproveProposalAction(_proposalId)); + } + + function approveProposal(uint256 _proposalId) public { + _execute(_buildApproveProposalAction(_proposalId)); + } + + function _issueCalls(Action memory _action) internal override { + if (_action.actionType == APPROVE_PROPOSAL) { + uint256 proposalIdToApprove = abi.decode(_action.data, (uint256)); + IDualGovernance(dualGovernance).tiebreakerApproveProposal(proposalIdToApprove); + } else { + assert(false); + } + } + + function _buildApproveProposalAction(uint256 _proposalId) internal view returns (Action memory) { + return Action(APPROVE_PROPOSAL, abi.encode(_proposalId), false, new address[](0)); + } +} diff --git a/contracts/TiebreakerNOR.sol b/contracts/TiebreakerNOR.sol deleted file mode 100644 index 18179b23..00000000 --- a/contracts/TiebreakerNOR.sol +++ /dev/null @@ -1,133 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; - -interface INodeOperatorsRegistry { - function getNodeOperator( - uint256 _id, - bool _fullInfo - ) - external - view - returns ( - bool active, - string memory name, - address rewardAddress, - uint64 stakingLimit, - uint64 stoppedValidators, - uint64 totalSigningKeys, - uint64 usedSigningKeys - ); - - function getNodeOperatorsCount() external view returns (uint256); - function getActiveNodeOperatorsCount() external view returns (uint256); - function getNodeOperatorIsActive(uint256 _nodeOperatorId) external view returns (bool); -} - -/** - * A contract provides ability to execute locked proposals. - */ -contract TiebreakerNOR { - event HashApproved(address to, bytes data, uint256 nonce, address indexed member); - event ExecutionSuccess(bytes32 txHash); - - error ZeroQuorum(); - error NoQourum(); - error SenderIsNotMember(); - error SenderIsNotOwner(); - error NonceAlreadyUsed(); - - address public nodeOperatorsRegistry; - - mapping(bytes32 txHash => uint256[] signers) signers; - mapping(uint256 nodeOperatorId => mapping(bytes32 txHash => bool isApproved)) approves; - - uint256 public nonce; - - constructor(address _nodeOperatorsRegistry) { - nodeOperatorsRegistry = _nodeOperatorsRegistry; - } - - function execTransaction(address _to, bytes calldata _data, uint256 _value) public payable returns (bytes memory) { - nonce++; - bytes32 txHash = getTransactionHash(_to, _data, _value, nonce); - - if (hasQuorum(txHash) == false) { - revert NoQourum(); - } - return Address.functionCallWithValue(_to, _data, _value); - } - - /** - * @dev Marks a hash as approved. This can be used to validate a hash that is used by a signature. - * @param _hashToApprove The hash that should be marked as approved for signatures that are verified by this contract. - * @param _nodeOperatorId Node Operator ID of msg.sender - */ - function approveHash(bytes32 _hashToApprove, uint256 _nodeOperatorId) public onlyNodeOperator(_nodeOperatorId) { - approves[_nodeOperatorId][_hashToApprove] = true; - signers[_hashToApprove].push(_nodeOperatorId); - } - - /** - * @dev Marks a hash as approved. This can be used to validate a hash that is used by a signature. - * @param _hashToReject The hash that should be marked as approved for signatures that are verified by this contract. - * @param _nodeOperatorId Node Operator ID of msg.sender - */ - function rejectHash(bytes32 _hashToReject, uint256 _nodeOperatorId) public onlyNodeOperator(_nodeOperatorId) { - approves[_nodeOperatorId][_hashToReject] = false; - for (uint256 i = 0; i < signers[_hashToReject].length; ++i) { - if (signers[_hashToReject][i] == _nodeOperatorId) { - signers[_hashToReject][i] = signers[_hashToReject][signers[_hashToReject].length - 1]; - signers[_hashToReject].pop(); - break; - } - } - } - - /// @dev Returns hash to be signed by owners. - /// @param _to Destination address. - /// @param _data Data payload. - /// @param _nonce Transaction nonce. - /// @return Transaction hash. - function getTransactionHash( - address _to, - bytes calldata _data, - uint256 _value, - uint256 _nonce - ) public pure returns (bytes32) { - return keccak256(abi.encode(_to, _data, _value, _nonce)); - } - - function hasQuorum(bytes32 _txHash) public view returns (bool) { - uint256 activeNOCount = INodeOperatorsRegistry(nodeOperatorsRegistry).getActiveNodeOperatorsCount(); - uint256 quorum = activeNOCount / 2 + 1; - - uint256 supportersCount = 0; - - for (uint256 i = 0; i < signers[_txHash].length; ++i) { - if (INodeOperatorsRegistry(nodeOperatorsRegistry).getNodeOperatorIsActive(signers[_txHash][i]) == true) { - supportersCount++; - } - } - - return supportersCount >= quorum; - } - - modifier onlyNodeOperator(uint256 _nodeOperatorId) { - ( - bool active, - , //string memory name, - address rewardAddress, - , //uint64 stakingLimit, - , //uint64 stoppedValidators, - , //uint64 totalSigningKeys, - //uint64 usedSigningKeys - ) = INodeOperatorsRegistry(nodeOperatorsRegistry).getNodeOperator(_nodeOperatorId, false); - - if (active == false || msg.sender != rewardAddress) { - revert SenderIsNotMember(); - } - _; - } -} diff --git a/contracts/TiebreakerSubDAO.sol b/contracts/TiebreakerSubDAO.sol new file mode 100644 index 00000000..cbbf2cf3 --- /dev/null +++ b/contracts/TiebreakerSubDAO.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {RestrictedMultisigBase} from "./RestrictedMultisigBase.sol"; + +interface ITiebreakerCore { + function voteApproveProposal(uint256 _proposalId, bool _supports) external; +} + +contract TiebreakerSubDAO is RestrictedMultisigBase { + uint256 public constant APPROVE_PROPOSAL = 1; + + address tiebreakerCore; + + constructor( + address _owner, + address[] memory _members, + uint256 _quorum, + address _tiebreakerCore + ) RestrictedMultisigBase(_owner, _members, _quorum) { + tiebreakerCore = _tiebreakerCore; + } + + function voteApproveProposal(uint256 _proposalId, bool _supports) public onlyMember { + _vote(_buildApproveProposalAction(_proposalId), _supports); + } + + function getApproveProposalState(uint256 _proposalId) + public + returns (uint256 support, uint256 ExecutionQuorum, bool isExecuted) + { + return _getState(_buildApproveProposalAction(_proposalId)); + } + + function approveProposal(uint256 _proposalId) public { + _execute(_buildApproveProposalAction(_proposalId)); + } + + function _issueCalls(Action memory _action) internal override { + if (_action.actionType == APPROVE_PROPOSAL) { + uint256 proposalIdToExecute = abi.decode(_action.data, (uint256)); + ITiebreakerCore(tiebreakerCore).voteApproveProposal(proposalIdToExecute, true); + } else { + assert(false); + } + } + + function _buildApproveProposalAction(uint256 proposalId) internal view returns (Action memory) { + return Action(APPROVE_PROPOSAL, abi.encode(proposalId), false, new address[](0)); + } +} diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol index 0d460cc3..dedefe59 100644 --- a/test/scenario/tiebraker.t.sol +++ b/test/scenario/tiebraker.t.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.23; import {Test, console} from "forge-std/Test.sol"; import {DualGovernanceDeployScript, DualGovernance, EmergencyProtectedTimelock} from "script/Deploy.s.sol"; -import {Tiebreaker} from "contracts/Tiebreaker.sol"; -import {TiebreakerNOR} from "contracts/TiebreakerNOR.sol"; +import {TiebreakerCore} from "contracts/TiebreakerCore.sol"; +import {TiebreakerSubDAO} from "contracts/TiebreakerSubDAO.sol"; import {Utils} from "../utils/utils.sol"; import {INodeOperatorsRegistry} from "../utils/interfaces.sol"; @@ -13,111 +13,98 @@ import {NODE_OPERATORS_REGISTRY} from "../utils/mainnet-addresses.sol"; contract TiebreakerScenarioTest is Test { Executor__mock private _emergencyExecutor; - Tiebreaker private _coreTiebreaker; - Tiebreaker private _efTiebraker; - TiebreakerNOR private _norTiebreaker; + TiebreakerCore private _coreTiebreaker; + TiebreakerSubDAO private _efTiebreaker; + TiebreakerSubDAO private _nosTiebreaker; uint256 private _efMembersCount = 5; uint256 private _efQuorum = 3; - address[] private _efTiebrakerMembers; - address[] private _coreTiebrakerMembers; + uint256 private _nosMembersCount = 10; + uint256 private _nosQuorum = 7; + + address[] private _efTiebreakerMembers; + address[] private _nosTiebreakerMembers; + address[] private _coreTiebreakerMembers; function setUp() external { Utils.selectFork(); - _coreTiebreaker = new Tiebreaker(); + _emergencyExecutor = new Executor__mock(); + _coreTiebreaker = new TiebreakerCore(address(this), new address[](0), 0, address(_emergencyExecutor)); - _emergencyExecutor = new Executor__mock(address(_coreTiebreaker)); + _emergencyExecutor.setCommittee(address(_coreTiebreaker)); + // EF sub DAO + _efTiebreaker = new TiebreakerSubDAO(address(this), new address[](0), 0, address(_coreTiebreaker)); for (uint256 i = 0; i < _efMembersCount; i++) { - _efTiebrakerMembers.push(makeAddr(string(abi.encode(i + 65)))); + _efTiebreakerMembers.push(makeAddr(string(abi.encode(i + 65)))); + _efTiebreaker.addMember(_efTiebreakerMembers[i], _efQuorum); } - - _efTiebraker = new Tiebreaker(); - _efTiebraker.initialize(address(this), _efTiebrakerMembers, _efQuorum); - _coreTiebrakerMembers.push(address(_efTiebraker)); - - _norTiebreaker = new TiebreakerNOR(NODE_OPERATORS_REGISTRY); - _coreTiebrakerMembers.push(address(_norTiebreaker)); - - _coreTiebreaker.initialize(address(this), _coreTiebrakerMembers, 2); + _coreTiebreakerMembers.push(address(_efTiebreaker)); + _coreTiebreaker.addMember(address(_efTiebreaker), _efQuorum); + + // NOs sub DAO + _nosTiebreaker = new TiebreakerSubDAO(address(this), new address[](0), 0, address(_coreTiebreaker)); + for (uint256 i = 0; i < _nosMembersCount; i++) { + _nosTiebreakerMembers.push(makeAddr(string(abi.encode(i + 65)))); + _nosTiebreaker.addMember(_nosTiebreakerMembers[i], _nosQuorum); + } + _coreTiebreakerMembers.push(address(_nosTiebreaker)); + _coreTiebreaker.addMember(address(_nosTiebreaker), 2); } function test_proposal_execution() external { uint256 proposalIdToExecute = 1; + uint256 quorum; + uint256 support; + bool isExecuted; assert(_emergencyExecutor.proposals(proposalIdToExecute) == false); - bytes32 execProposalHash = _prepareExecuteProposalHash(address(_emergencyExecutor), proposalIdToExecute, 1); - bytes32 execApproveHash = _prepareApproveHashHash(address(_coreTiebreaker), execProposalHash, 1); - + // EF sub DAO for (uint256 i = 0; i < _efQuorum - 1; i++) { - vm.prank(_efTiebrakerMembers[i]); - _efTiebraker.approveHash(execApproveHash); - assert(_efTiebraker.hasQuorum(execApproveHash) == false); + vm.prank(_efTiebreakerMembers[i]); + _efTiebreaker.voteApproveProposal(proposalIdToExecute, true); + (support, quorum, isExecuted) = _efTiebreaker.getApproveProposalState(proposalIdToExecute); + assert(support < quorum); + assert(isExecuted == false); } - vm.prank(_efTiebrakerMembers[_efTiebrakerMembers.length - 1]); - _efTiebraker.approveHash(execApproveHash); - - assert(_efTiebraker.hasQuorum(execApproveHash) == true); - - _efTiebraker.execTransaction( - address(_coreTiebreaker), abi.encodeWithSignature("approveHash(bytes32)", execProposalHash), 0 - ); - - assert(_coreTiebreaker.hasQuorum(execProposalHash) == false); - - uint256 participatedNOCount = 0; - uint256 requiredOperatorsCount = - INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getActiveNodeOperatorsCount() / 2 + 1; - - for (uint256 i = 0; i < INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getNodeOperatorsCount(); i++) { - ( - bool active, - , //string memory name, - address rewardAddress, - , //uint64 stakingLimit, - , //uint64 stoppedValidators, - , //uint64 totalSigningKeys, - //uint64 usedSigningKeys - ) = INodeOperatorsRegistry(NODE_OPERATORS_REGISTRY).getNodeOperator(i, false); - if (active) { - vm.prank(rewardAddress); - _norTiebreaker.approveHash(execApproveHash, i); - - participatedNOCount++; - } - if (participatedNOCount >= requiredOperatorsCount) break; - } + vm.prank(_efTiebreakerMembers[_efTiebreakerMembers.length - 1]); + _efTiebreaker.voteApproveProposal(proposalIdToExecute, true); + (support, quorum, isExecuted) = _efTiebreaker.getApproveProposalState(proposalIdToExecute); + assert(support == quorum); + assert(isExecuted == false); - assert(_norTiebreaker.hasQuorum(execApproveHash) == true); + _efTiebreaker.approveProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _coreTiebreaker.getApproveProposalState(proposalIdToExecute); + assert(support < quorum); - _norTiebreaker.execTransaction( - address(_coreTiebreaker), abi.encodeWithSignature("approveHash(bytes32)", execProposalHash), 0 - ); - assert(_coreTiebreaker.hasQuorum(execProposalHash) == true); + // NOs sub DAO - _coreTiebreaker.execTransaction( - address(_emergencyExecutor), abi.encodeWithSignature("tiebreaExecute(uint256)", proposalIdToExecute), 0 - ); + for (uint256 i = 0; i < _nosQuorum - 1; i++) { + vm.prank(_nosTiebreakerMembers[i]); + _nosTiebreaker.voteApproveProposal(proposalIdToExecute, true); + (support, quorum, isExecuted) = _nosTiebreaker.getApproveProposalState(proposalIdToExecute); + assert(support < quorum); + assert(isExecuted == false); + } - assert(_emergencyExecutor.proposals(proposalIdToExecute) == true); - } + vm.prank(_nosTiebreakerMembers[_nosTiebreakerMembers.length - 1]); + _nosTiebreaker.voteApproveProposal(proposalIdToExecute, true); - function _prepareApproveHashHash(address _to, bytes32 _hash, uint256 _nonce) public view returns (bytes32) { - return _efTiebraker.getTransactionHash(_to, abi.encodeWithSignature("approveHash(bytes32)", _hash), 0, _nonce); - } + (support, quorum, isExecuted) = _nosTiebreaker.getApproveProposalState(proposalIdToExecute); + assert(support == quorum); + assert(isExecuted == false); - function _prepareExecuteProposalHash( - address _to, - uint256 _proposalId, - uint256 _nonce - ) public view returns (bytes32) { - return _coreTiebreaker.getTransactionHash( - _to, abi.encodeWithSignature("tiebreaExecute(uint256)", _proposalId), 0, _nonce - ); + _nosTiebreaker.approveProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _coreTiebreaker.getApproveProposalState(proposalIdToExecute); + assert(support == quorum); + + _coreTiebreaker.approveProposal(proposalIdToExecute); + + assert(_emergencyExecutor.proposals(proposalIdToExecute) == true); } } @@ -128,11 +115,11 @@ contract Executor__mock { mapping(uint256 => bool) public proposals; address private committee; - constructor(address _committee) { + function setCommittee(address _committee) public { committee = _committee; } - function tiebreaExecute(uint256 _proposalId) public { + function tiebreakerApproveProposal(uint256 _proposalId) public { if (proposals[_proposalId] == true) { revert ProposalAlreadyExecuted(); } From 393fc7b41ca914d9a02b6383843c856be53522bf Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Mon, 15 Apr 2024 13:45:29 +0300 Subject: [PATCH 08/38] fix internal calls --- contracts/EmergencyActivationMultisig.sol | 42 +++---- contracts/EmergencyExecutionMultisig.sol | 70 +++++------ contracts/RestrictedMultisigBase.sol | 134 +++++++++++----------- contracts/TiebreakerCore.sol | 42 +++---- contracts/TiebreakerSubCommittee.sol | 37 ++++++ contracts/TiebreakerSubDAO.sol | 51 -------- test/scenario/tiebraker.t.sol | 16 +-- 7 files changed, 174 insertions(+), 218 deletions(-) create mode 100644 contracts/TiebreakerSubCommittee.sol delete mode 100644 contracts/TiebreakerSubDAO.sol diff --git a/contracts/EmergencyActivationMultisig.sol b/contracts/EmergencyActivationMultisig.sol index a1722d26..76e3eafc 100644 --- a/contracts/EmergencyActivationMultisig.sol +++ b/contracts/EmergencyActivationMultisig.sol @@ -3,45 +3,35 @@ pragma solidity 0.8.23; import {RestrictedMultisigBase} from "./RestrictedMultisigBase.sol"; -interface IEmergencyProtectedTimelock { - function emergencyActivate() external; -} - contract EmergencyActivationMultisig is RestrictedMultisigBase { - uint256 public constant EMERGENCY_ACTIVATE = 1; - - address emergencyProtectedTimelock; + address public immutable EMERGENCY_PROTECTED_TIMELOCK; constructor( - address _owner, - address[] memory _members, - uint256 _quorum, - address _emergencyProtectedTimelock - ) RestrictedMultisigBase(_owner, _members, _quorum) { - emergencyProtectedTimelock = _emergencyProtectedTimelock; + address OWNER, + address[] memory multisigMembers, + uint256 executionQuorum, + address emergencyProtectedTimelock + ) RestrictedMultisigBase(OWNER, multisigMembers, executionQuorum) { + EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; } - function voteEmergencyActivate() public onlyMember { + function approveEmergencyActivate() public onlyMember { _vote(_buildEmergencyActivateAction(), true); } - function getEmergencyActivateState() public returns (uint256 support, uint256 ExecutionQuorum, bool isExecuted) { - return _getState(_buildEmergencyActivateAction()); + function getEmergencyActivateState() + public + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + { + return getActionState(_buildEmergencyActivateAction()); } - function emergencyActivate() external { + function executeEmergencyActivate() external { _execute(_buildEmergencyActivateAction()); } - function _issueCalls(Action memory _action) internal override { - if (_action.actionType == EMERGENCY_ACTIVATE) { - IEmergencyProtectedTimelock(emergencyProtectedTimelock).emergencyActivate(); - } else { - assert(false); - } - } - function _buildEmergencyActivateAction() internal view returns (Action memory) { - return Action(EMERGENCY_ACTIVATE, new bytes(0), false, new address[](0)); + return Action(EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSignature("emergencyActivate()")); } } diff --git a/contracts/EmergencyExecutionMultisig.sol b/contracts/EmergencyExecutionMultisig.sol index d0f0aa14..4e3063d2 100644 --- a/contracts/EmergencyExecutionMultisig.sol +++ b/contracts/EmergencyExecutionMultisig.sol @@ -9,66 +9,58 @@ interface IEmergencyProtectedTimelock { } contract EmergencyExecutionMultisig is RestrictedMultisigBase { - uint256 public constant EXECUTE_PROPOSAL = 1; - uint256 public constant RESET_GOVERNANCE = 2; - - address emergencyProtectedTimelock; + address public immutable EMERGENCY_PROTECTED_TIMELOCK; constructor( - address _owner, - address[] memory _members, - uint256 _quorum, - address _emergencyProtectedTimelock - ) RestrictedMultisigBase(_owner, _members, _quorum) { - emergencyProtectedTimelock = _emergencyProtectedTimelock; + address OWNER, + address[] memory multisigMembers, + uint256 executionQuorum, + address emergencyProtectedTimelock + ) RestrictedMultisigBase(OWNER, multisigMembers, executionQuorum) { + EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; } - // Proposal Execution - function voteExecuteProposal(uint256 _proposalId, bool _supports) public onlyMember { - _vote(_buildExecuteProposalAction(_proposalId), _supports); + // Emergency Execution + + function voteEmergencyExecute(uint256 _proposalId, bool _supports) public onlyMember { + _vote(_buildEmergencyExecuteAction(_proposalId), _supports); } - function getExecuteProposalState(uint256 _proposalId) + function getEmergencyExecuteState(uint256 _proposalId) public - returns (uint256 support, uint256 ExecutionQuorum, bool isExecuted) + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getState(_buildExecuteProposalAction(_proposalId)); + return getActionState(_buildEmergencyExecuteAction(_proposalId)); } - function executeProposal(uint256 _proposalId) public { - _execute(_buildExecuteProposalAction(_proposalId)); + function executeEmergencyExecute(uint256 _proposalId) public { + _execute(_buildEmergencyExecuteAction(_proposalId)); } // Governance reset - function voteGoveranaceReset() public onlyMember { - _vote(_buildResetGovAction(), true); - } - - function getGovernanceResetState() public returns (uint256 support, uint256 ExecutionQuorum, bool isExecuted) { - return _getState(_buildResetGovAction()); + function approveEmergencyReset() public onlyMember { + _vote(_buildEmergencyResetAction(), true); } - function resetGovernance() external { - _execute(_buildResetGovAction()); + function getEmergencyResetState() + public + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + { + return getActionState(_buildEmergencyResetAction()); } - function _issueCalls(Action memory _action) internal override { - if (_action.actionType == EXECUTE_PROPOSAL) { - uint256 proposalIdToExecute = abi.decode(_action.data, (uint256)); - IEmergencyProtectedTimelock(emergencyProtectedTimelock).emergencyExecute(proposalIdToExecute); - } else if (_action.actionType == RESET_GOVERNANCE) { - IEmergencyProtectedTimelock(emergencyProtectedTimelock).emergencyReset(); - } else { - assert(false); - } + function executeEmergencyReset() external { + _execute(_buildEmergencyResetAction()); } - function _buildResetGovAction() internal view returns (Action memory) { - return Action(RESET_GOVERNANCE, new bytes(0), false, new address[](0)); + function _buildEmergencyResetAction() internal view returns (Action memory) { + return Action(EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSignature("emergencyReset()")); } - function _buildExecuteProposalAction(uint256 proposalId) internal view returns (Action memory) { - return Action(EXECUTE_PROPOSAL, abi.encode(proposalId), false, new address[](0)); + function _buildEmergencyExecuteAction(uint256 proposalId) internal view returns (Action memory) { + return Action(EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSignature("emergencyExecute(uint256)", proposalId)); } } diff --git a/contracts/RestrictedMultisigBase.sol b/contracts/RestrictedMultisigBase.sol index 019a8880..015b29e7 100644 --- a/contracts/RestrictedMultisigBase.sol +++ b/contracts/RestrictedMultisigBase.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + abstract contract RestrictedMultisigBase { error IsNotMember(); error SenderIsNotMember(); @@ -8,135 +10,135 @@ abstract contract RestrictedMultisigBase { error DataIsNotEqual(); struct Action { - uint256 actionType; + address to; bytes data; + } + + struct ActionState { + Action action; bool isExecuted; address[] signers; } - address public owner; + address public immutable OWNER; address[] public membersList; mapping(address => bool) public members; uint256 public quorum; - mapping(bytes32 actionHash => Action) actions; + mapping(bytes32 actionHash => ActionState) actionsStates; mapping(address signer => mapping(bytes32 actionHash => bool support)) public approves; - constructor(address _owner, address[] memory _members, uint256 _quorum) { - quorum = _quorum; - owner = _owner; + constructor(address owner, address[] memory newMembers, uint256 executionQuorum) { + quorum = executionQuorum; + OWNER = owner; - for (uint256 i = 0; i < _members.length; ++i) { - _addMember(_members[i]); + for (uint256 i = 0; i < newMembers.length; ++i) { + _addMember(newMembers[i]); } } - function _vote(Action memory _action, bool _supports) internal { - bytes32 actionHash = _hashAction(_action); - if (actions[actionHash].data.length == 0) { - actions[actionHash].actionType = _action.actionType; - actions[actionHash].data = _action.data; + function _vote(Action memory action, bool support) internal { + bytes32 actionHash = _hashAction(action); + if (actionsStates[actionHash].action.to == address(0)) { + actionsStates[actionHash].action = action; } else { - _checkStoredAction(_action); + _getAndCheckStoredActionState(action); } - if (approves[msg.sender][actionHash] == _supports) { + if (approves[msg.sender][actionHash] == support) { return; } - approves[msg.sender][actionHash] = _supports; - if (_supports == true) { - actions[actionHash].signers.push(msg.sender); + approves[msg.sender][actionHash] = support; + if (support == true) { + actionsStates[actionHash].signers.push(msg.sender); } else { - uint256 signersLength = actions[actionHash].signers.length; + uint256 signersLength = actionsStates[actionHash].signers.length; for (uint256 i = 0; i < signersLength; ++i) { - if (actions[actionHash].signers[i] == msg.sender) { - actions[actionHash].signers[i] = actions[actionHash].signers[signersLength - 1]; - actions[actionHash].signers.pop(); + if (actionsStates[actionHash].signers[i] == msg.sender) { + actionsStates[actionHash].signers[i] = actionsStates[actionHash].signers[signersLength - 1]; + actionsStates[actionHash].signers.pop(); break; } } } } - function _execute(Action memory _action) internal { - _checkStoredAction(_action); + function _execute(Action memory action) internal { + (ActionState memory actionState, bytes32 actionHash) = _getAndCheckStoredActionState(action); - bytes32 actionHash = _hashAction(_action); + require(actionState.isExecuted == false); + require(_getState(actionHash) >= quorum); - require(actions[actionHash].isExecuted == false); - require(_getSuport(actionHash) >= quorum); + Address.functionCall(actionState.action.to, actionState.action.data); - _issueCalls(_action); - - actions[actionHash].isExecuted = true; + actionsStates[actionHash].isExecuted = true; } - function _getState(Action memory _action) + function getActionState(Action memory action) public + view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - _checkStoredAction(_action); + (ActionState memory actionState, bytes32 actionHash) = _getAndCheckStoredActionState(action); - bytes32 actionHash = _hashAction(_action); - support = _getSuport(actionHash); + support = _getState(actionHash); execuitionQuorum = quorum; - isExecuted = actions[actionHash].isExecuted; + isExecuted = actionState.isExecuted; } - function addMember(address _newMember, uint256 _quorum) public onlyOwner { - _addMember(_newMember); - quorum = _quorum; + function addMember(address newMember, uint256 newQuorum) public onlyOwner { + _addMember(newMember); + quorum = newQuorum; } - function removeMember(address _member, uint256 _quorum) public onlyOwner { - if (members[_member] == false) { + function removeMember(address memberToRemove, uint256 newQuorum) public onlyOwner { + if (members[memberToRemove] == false) { revert IsNotMember(); } - members[_member] = false; + members[memberToRemove] = false; for (uint256 i = 0; i < membersList.length; ++i) { - if (membersList[i] == _member) { + if (membersList[i] == memberToRemove) { membersList[i] = membersList[membersList.length - 1]; membersList.pop(); break; } } - quorum = _quorum; - } - function _addMember(address _newMember) internal { - membersList.push(_newMember); - members[_newMember] = true; + require(newQuorum > 0); + require(newQuorum <= membersList.length); + quorum = newQuorum; } - function _issueCalls(Action memory _action) internal virtual; + function _addMember(address newMember) internal { + membersList.push(newMember); + members[newMember] = true; + } - function _getSuport(bytes32 _actionHash) internal returns (uint256 support) { - for (uint256 i = 0; i < actions[_actionHash].signers.length; ++i) { - if (members[actions[_actionHash].signers[i]] == true) { + function _getState(bytes32 actionHash) internal view returns (uint256 support) { + for (uint256 i = 0; i < actionsStates[actionHash].signers.length; ++i) { + if (members[actionsStates[actionHash].signers[i]] == true) { support++; } } } - function _checkStoredAction(Action memory _action) internal { - bytes32 actionHash = _hashAction(_action); - - require(_action.actionType > 0); - require(actions[actionHash].actionType == _action.actionType); - require(actions[actionHash].isExecuted == false); + function _getAndCheckStoredActionState(Action memory action) + internal + view + returns (ActionState memory storedAction, bytes32 actionHash) + { + actionHash = _hashAction(action); - require(actions[actionHash].data.length == _action.data.length); - for (uint256 i = 0; i < _action.data.length; ++i) { - if (actions[actionHash].data[i] != _action.data[i]) { - revert DataIsNotEqual(); - } - } + storedAction = actionsStates[actionHash]; + require(storedAction.action.to == action.to); + require(storedAction.action.data.length == action.data.length); + require(storedAction.isExecuted == false); } - function _hashAction(Action memory _action) internal pure returns (bytes32) { - return keccak256(abi.encode(_action.actionType, _action.data)); + function _hashAction(Action memory action) internal pure returns (bytes32) { + return keccak256(abi.encode(action.to, action.data)); } modifier onlyMember() { @@ -147,7 +149,7 @@ abstract contract RestrictedMultisigBase { } modifier onlyOwner() { - if (msg.sender != owner) { + if (msg.sender != OWNER) { revert SenderIsNotOwner(); } _; diff --git a/contracts/TiebreakerCore.sol b/contracts/TiebreakerCore.sol index 9fed8b09..06dfe31a 100644 --- a/contracts/TiebreakerCore.sol +++ b/contracts/TiebreakerCore.sol @@ -3,49 +3,35 @@ pragma solidity 0.8.23; import {RestrictedMultisigBase} from "./RestrictedMultisigBase.sol"; -interface IDualGovernance { - function tiebreakerApproveProposal(uint256 _proposalId) external; -} - contract TiebreakerCore is RestrictedMultisigBase { - uint256 public constant APPROVE_PROPOSAL = 1; - - address dualGovernance; + address immutable DUAL_GOVERNANCE; constructor( - address _owner, - address[] memory _members, - uint256 _quorum, - address _dualGovernance - ) RestrictedMultisigBase(_owner, _members, _quorum) { - dualGovernance = _dualGovernance; + address owner, + address[] memory multisigMembers, + uint256 executionQuorum, + address dualGovernance + ) RestrictedMultisigBase(owner, multisigMembers, executionQuorum) { + DUAL_GOVERNANCE = dualGovernance; } - function voteApproveProposal(uint256 _proposalId, bool _supports) public onlyMember { - _vote(_buildApproveProposalAction(_proposalId), _supports); + function approveProposal(uint256 _proposalId) public onlyMember { + _vote(_buildApproveProposalAction(_proposalId), true); } function getApproveProposalState(uint256 _proposalId) public - returns (uint256 support, uint256 ExecutionQuorum, bool isExecuted) + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getState(_buildApproveProposalAction(_proposalId)); + return getActionState(_buildApproveProposalAction(_proposalId)); } - function approveProposal(uint256 _proposalId) public { + function executeApproveProposal(uint256 _proposalId) public { _execute(_buildApproveProposalAction(_proposalId)); } - function _issueCalls(Action memory _action) internal override { - if (_action.actionType == APPROVE_PROPOSAL) { - uint256 proposalIdToApprove = abi.decode(_action.data, (uint256)); - IDualGovernance(dualGovernance).tiebreakerApproveProposal(proposalIdToApprove); - } else { - assert(false); - } - } - function _buildApproveProposalAction(uint256 _proposalId) internal view returns (Action memory) { - return Action(APPROVE_PROPOSAL, abi.encode(_proposalId), false, new address[](0)); + return Action(DUAL_GOVERNANCE, abi.encodeWithSignature("tiebreakerApproveProposal(uint256)", _proposalId)); } } diff --git a/contracts/TiebreakerSubCommittee.sol b/contracts/TiebreakerSubCommittee.sol new file mode 100644 index 00000000..7f9d3181 --- /dev/null +++ b/contracts/TiebreakerSubCommittee.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {RestrictedMultisigBase} from "./RestrictedMultisigBase.sol"; + +contract TiebreakerSubCommittee is RestrictedMultisigBase { + address immutable TIEBREAKER_CORE; + + constructor( + address owner, + address[] memory multisigMembers, + uint256 executionQuorum, + address tiebreakerCore + ) RestrictedMultisigBase(owner, multisigMembers, executionQuorum) { + TIEBREAKER_CORE = tiebreakerCore; + } + + function voteApproveProposal(uint256 _proposalId, bool _supports) public onlyMember { + _vote(_buildApproveProposalAction(_proposalId), _supports); + } + + function getApproveProposalState(uint256 _proposalId) + public + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + { + return getActionState(_buildApproveProposalAction(_proposalId)); + } + + function executeApproveProposal(uint256 _proposalId) public { + _execute(_buildApproveProposalAction(_proposalId)); + } + + function _buildApproveProposalAction(uint256 _proposalId) internal view returns (Action memory) { + return Action(TIEBREAKER_CORE, abi.encodeWithSignature("approveProposal(uint256)", _proposalId)); + } +} diff --git a/contracts/TiebreakerSubDAO.sol b/contracts/TiebreakerSubDAO.sol deleted file mode 100644 index cbbf2cf3..00000000 --- a/contracts/TiebreakerSubDAO.sol +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import {RestrictedMultisigBase} from "./RestrictedMultisigBase.sol"; - -interface ITiebreakerCore { - function voteApproveProposal(uint256 _proposalId, bool _supports) external; -} - -contract TiebreakerSubDAO is RestrictedMultisigBase { - uint256 public constant APPROVE_PROPOSAL = 1; - - address tiebreakerCore; - - constructor( - address _owner, - address[] memory _members, - uint256 _quorum, - address _tiebreakerCore - ) RestrictedMultisigBase(_owner, _members, _quorum) { - tiebreakerCore = _tiebreakerCore; - } - - function voteApproveProposal(uint256 _proposalId, bool _supports) public onlyMember { - _vote(_buildApproveProposalAction(_proposalId), _supports); - } - - function getApproveProposalState(uint256 _proposalId) - public - returns (uint256 support, uint256 ExecutionQuorum, bool isExecuted) - { - return _getState(_buildApproveProposalAction(_proposalId)); - } - - function approveProposal(uint256 _proposalId) public { - _execute(_buildApproveProposalAction(_proposalId)); - } - - function _issueCalls(Action memory _action) internal override { - if (_action.actionType == APPROVE_PROPOSAL) { - uint256 proposalIdToExecute = abi.decode(_action.data, (uint256)); - ITiebreakerCore(tiebreakerCore).voteApproveProposal(proposalIdToExecute, true); - } else { - assert(false); - } - } - - function _buildApproveProposalAction(uint256 proposalId) internal view returns (Action memory) { - return Action(APPROVE_PROPOSAL, abi.encode(proposalId), false, new address[](0)); - } -} diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol index dedefe59..e5c6828a 100644 --- a/test/scenario/tiebraker.t.sol +++ b/test/scenario/tiebraker.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.23; import {Test, console} from "forge-std/Test.sol"; import {DualGovernanceDeployScript, DualGovernance, EmergencyProtectedTimelock} from "script/Deploy.s.sol"; import {TiebreakerCore} from "contracts/TiebreakerCore.sol"; -import {TiebreakerSubDAO} from "contracts/TiebreakerSubDAO.sol"; +import {TiebreakerSubCommittee} from "contracts/TiebreakerSubCommittee.sol"; import {Utils} from "../utils/utils.sol"; import {INodeOperatorsRegistry} from "../utils/interfaces.sol"; @@ -14,8 +14,8 @@ contract TiebreakerScenarioTest is Test { Executor__mock private _emergencyExecutor; TiebreakerCore private _coreTiebreaker; - TiebreakerSubDAO private _efTiebreaker; - TiebreakerSubDAO private _nosTiebreaker; + TiebreakerSubCommittee private _efTiebreaker; + TiebreakerSubCommittee private _nosTiebreaker; uint256 private _efMembersCount = 5; uint256 private _efQuorum = 3; @@ -36,7 +36,7 @@ contract TiebreakerScenarioTest is Test { _emergencyExecutor.setCommittee(address(_coreTiebreaker)); // EF sub DAO - _efTiebreaker = new TiebreakerSubDAO(address(this), new address[](0), 0, address(_coreTiebreaker)); + _efTiebreaker = new TiebreakerSubCommittee(address(this), new address[](0), 0, address(_coreTiebreaker)); for (uint256 i = 0; i < _efMembersCount; i++) { _efTiebreakerMembers.push(makeAddr(string(abi.encode(i + 65)))); _efTiebreaker.addMember(_efTiebreakerMembers[i], _efQuorum); @@ -45,7 +45,7 @@ contract TiebreakerScenarioTest is Test { _coreTiebreaker.addMember(address(_efTiebreaker), _efQuorum); // NOs sub DAO - _nosTiebreaker = new TiebreakerSubDAO(address(this), new address[](0), 0, address(_coreTiebreaker)); + _nosTiebreaker = new TiebreakerSubCommittee(address(this), new address[](0), 0, address(_coreTiebreaker)); for (uint256 i = 0; i < _nosMembersCount; i++) { _nosTiebreakerMembers.push(makeAddr(string(abi.encode(i + 65)))); _nosTiebreaker.addMember(_nosTiebreakerMembers[i], _nosQuorum); @@ -77,7 +77,7 @@ contract TiebreakerScenarioTest is Test { assert(support == quorum); assert(isExecuted == false); - _efTiebreaker.approveProposal(proposalIdToExecute); + _efTiebreaker.executeApproveProposal(proposalIdToExecute); (support, quorum, isExecuted) = _coreTiebreaker.getApproveProposalState(proposalIdToExecute); assert(support < quorum); @@ -98,11 +98,11 @@ contract TiebreakerScenarioTest is Test { assert(support == quorum); assert(isExecuted == false); - _nosTiebreaker.approveProposal(proposalIdToExecute); + _nosTiebreaker.executeApproveProposal(proposalIdToExecute); (support, quorum, isExecuted) = _coreTiebreaker.getApproveProposalState(proposalIdToExecute); assert(support == quorum); - _coreTiebreaker.approveProposal(proposalIdToExecute); + _coreTiebreaker.executeApproveProposal(proposalIdToExecute); assert(_emergencyExecutor.proposals(proposalIdToExecute) == true); } From 4ce81821635b785e285ca5f7bf330c70ce7a6585 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 17 Apr 2024 09:53:53 +0300 Subject: [PATCH 09/38] errors and events --- contracts/RestrictedMultisigBase.sol | 58 ++++++++++++++++++++++------ test/scenario/tiebraker.t.sol | 12 +++--- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/contracts/RestrictedMultisigBase.sol b/contracts/RestrictedMultisigBase.sol index 015b29e7..43f43b30 100644 --- a/contracts/RestrictedMultisigBase.sol +++ b/contracts/RestrictedMultisigBase.sol @@ -4,10 +4,21 @@ pragma solidity 0.8.23; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; abstract contract RestrictedMultisigBase { + event MemberAdded(address indexed member); + event MemberRemoved(address indexed member); + event QuorumSet(uint256 quorum); + event ActionProposed(address indexed to, bytes data); + event ActionExecuted(address indexed to, bytes data); + event ActionVoted(address indexed signer, bool support, address indexed to, bytes data); + error IsNotMember(); error SenderIsNotMember(); error SenderIsNotOwner(); error DataIsNotEqual(); + error ActionAlreadyExecuted(); + error QuorumIsNotReached(); + error InvalidQuorum(); + error ActionMismatch(); struct Action { address to; @@ -30,7 +41,12 @@ abstract contract RestrictedMultisigBase { mapping(address signer => mapping(bytes32 actionHash => bool support)) public approves; constructor(address owner, address[] memory newMembers, uint256 executionQuorum) { + if (executionQuorum == 0) { + revert InvalidQuorum(); + } quorum = executionQuorum; + emit QuorumSet(executionQuorum); + OWNER = owner; for (uint256 i = 0; i < newMembers.length; ++i) { @@ -42,6 +58,7 @@ abstract contract RestrictedMultisigBase { bytes32 actionHash = _hashAction(action); if (actionsStates[actionHash].action.to == address(0)) { actionsStates[actionHash].action = action; + emit ActionProposed(action.to, action.data); } else { _getAndCheckStoredActionState(action); } @@ -51,6 +68,7 @@ abstract contract RestrictedMultisigBase { } approves[msg.sender][actionHash] = support; + emit ActionVoted(msg.sender, support, action.to, action.data); if (support == true) { actionsStates[actionHash].signers.push(msg.sender); } else { @@ -68,12 +86,18 @@ abstract contract RestrictedMultisigBase { function _execute(Action memory action) internal { (ActionState memory actionState, bytes32 actionHash) = _getAndCheckStoredActionState(action); - require(actionState.isExecuted == false); - require(_getState(actionHash) >= quorum); + if (actionState.isExecuted == true) { + revert ActionAlreadyExecuted(); + } + if (_getSupport(actionHash) < quorum) { + revert QuorumIsNotReached(); + } Address.functionCall(actionState.action.to, actionState.action.data); actionsStates[actionHash].isExecuted = true; + + emit ActionExecuted(action.to, action.data); } function getActionState(Action memory action) @@ -83,14 +107,19 @@ abstract contract RestrictedMultisigBase { { (ActionState memory actionState, bytes32 actionHash) = _getAndCheckStoredActionState(action); - support = _getState(actionHash); + support = _getSupport(actionHash); execuitionQuorum = quorum; isExecuted = actionState.isExecuted; } function addMember(address newMember, uint256 newQuorum) public onlyOwner { _addMember(newMember); + + if (newQuorum == 0 || newQuorum > membersList.length) { + revert InvalidQuorum(); + } quorum = newQuorum; + emit QuorumSet(newQuorum); } function removeMember(address memberToRemove, uint256 newQuorum) public onlyOwner { @@ -105,18 +134,22 @@ abstract contract RestrictedMultisigBase { break; } } + emit MemberRemoved(memberToRemove); - require(newQuorum > 0); - require(newQuorum <= membersList.length); + if (newQuorum == 0 || newQuorum > membersList.length) { + revert InvalidQuorum(); + } quorum = newQuorum; + emit QuorumSet(newQuorum); } function _addMember(address newMember) internal { membersList.push(newMember); members[newMember] = true; + emit MemberAdded(newMember); } - function _getState(bytes32 actionHash) internal view returns (uint256 support) { + function _getSupport(bytes32 actionHash) internal view returns (uint256 support) { for (uint256 i = 0; i < actionsStates[actionHash].signers.length; ++i) { if (members[actionsStates[actionHash].signers[i]] == true) { support++; @@ -127,14 +160,17 @@ abstract contract RestrictedMultisigBase { function _getAndCheckStoredActionState(Action memory action) internal view - returns (ActionState memory storedAction, bytes32 actionHash) + returns (ActionState memory storedActionState, bytes32 actionHash) { actionHash = _hashAction(action); - storedAction = actionsStates[actionHash]; - require(storedAction.action.to == action.to); - require(storedAction.action.data.length == action.data.length); - require(storedAction.isExecuted == false); + storedActionState = actionsStates[actionHash]; + if (storedActionState.action.to != action.to || storedActionState.action.data.length != action.data.length) { + revert ActionMismatch(); + } + if (storedActionState.isExecuted == true) { + revert ActionAlreadyExecuted(); + } } function _hashAction(Action memory action) internal pure returns (bytes32) { diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol index e5c6828a..33cfd647 100644 --- a/test/scenario/tiebraker.t.sol +++ b/test/scenario/tiebraker.t.sol @@ -31,24 +31,24 @@ contract TiebreakerScenarioTest is Test { Utils.selectFork(); _emergencyExecutor = new Executor__mock(); - _coreTiebreaker = new TiebreakerCore(address(this), new address[](0), 0, address(_emergencyExecutor)); + _coreTiebreaker = new TiebreakerCore(address(this), new address[](0), 1, address(_emergencyExecutor)); _emergencyExecutor.setCommittee(address(_coreTiebreaker)); // EF sub DAO - _efTiebreaker = new TiebreakerSubCommittee(address(this), new address[](0), 0, address(_coreTiebreaker)); + _efTiebreaker = new TiebreakerSubCommittee(address(this), new address[](0), 1, address(_coreTiebreaker)); for (uint256 i = 0; i < _efMembersCount; i++) { _efTiebreakerMembers.push(makeAddr(string(abi.encode(i + 65)))); - _efTiebreaker.addMember(_efTiebreakerMembers[i], _efQuorum); + _efTiebreaker.addMember(_efTiebreakerMembers[i], i + 1 < _efQuorum ? i + 1 : _efQuorum); } _coreTiebreakerMembers.push(address(_efTiebreaker)); - _coreTiebreaker.addMember(address(_efTiebreaker), _efQuorum); + _coreTiebreaker.addMember(address(_efTiebreaker), 1); // NOs sub DAO - _nosTiebreaker = new TiebreakerSubCommittee(address(this), new address[](0), 0, address(_coreTiebreaker)); + _nosTiebreaker = new TiebreakerSubCommittee(address(this), new address[](0), 1, address(_coreTiebreaker)); for (uint256 i = 0; i < _nosMembersCount; i++) { _nosTiebreakerMembers.push(makeAddr(string(abi.encode(i + 65)))); - _nosTiebreaker.addMember(_nosTiebreakerMembers[i], _nosQuorum); + _nosTiebreaker.addMember(_nosTiebreakerMembers[i], i + 1 < _nosQuorum ? i + 1 : _nosQuorum); } _coreTiebreakerMembers.push(address(_nosTiebreaker)); _coreTiebreaker.addMember(address(_nosTiebreaker), 2); From a5fc85da498431ce9e3db3b03748e778ec7c5551 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 17 Apr 2024 10:29:30 +0300 Subject: [PATCH 10/38] tiebreaker unpause sealable --- contracts/EmergencyActivationMultisig.sol | 2 +- contracts/EmergencyExecutionMultisig.sol | 6 ++- contracts/RestrictedMultisigBase.sol | 3 +- contracts/TiebreakerCore.sol | 46 ++++++++++++++++++++- contracts/TiebreakerSubCommittee.sol | 49 +++++++++++++++++++---- 5 files changed, 93 insertions(+), 13 deletions(-) diff --git a/contracts/EmergencyActivationMultisig.sol b/contracts/EmergencyActivationMultisig.sol index 76e3eafc..c6d42ca9 100644 --- a/contracts/EmergencyActivationMultisig.sol +++ b/contracts/EmergencyActivationMultisig.sol @@ -32,6 +32,6 @@ contract EmergencyActivationMultisig is RestrictedMultisigBase { } function _buildEmergencyActivateAction() internal view returns (Action memory) { - return Action(EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSignature("emergencyActivate()")); + return Action(EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSignature("emergencyActivate()"), new bytes(0)); } } diff --git a/contracts/EmergencyExecutionMultisig.sol b/contracts/EmergencyExecutionMultisig.sol index 4e3063d2..4419b0bd 100644 --- a/contracts/EmergencyExecutionMultisig.sol +++ b/contracts/EmergencyExecutionMultisig.sol @@ -57,10 +57,12 @@ contract EmergencyExecutionMultisig is RestrictedMultisigBase { } function _buildEmergencyResetAction() internal view returns (Action memory) { - return Action(EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSignature("emergencyReset()")); + return Action(EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSignature("emergencyReset()"), new bytes(0)); } function _buildEmergencyExecuteAction(uint256 proposalId) internal view returns (Action memory) { - return Action(EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSignature("emergencyExecute(uint256)", proposalId)); + return Action( + EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSignature("emergencyExecute(uint256)", proposalId), new bytes(0) + ); } } diff --git a/contracts/RestrictedMultisigBase.sol b/contracts/RestrictedMultisigBase.sol index 43f43b30..6ee2dc53 100644 --- a/contracts/RestrictedMultisigBase.sol +++ b/contracts/RestrictedMultisigBase.sol @@ -23,6 +23,7 @@ abstract contract RestrictedMultisigBase { struct Action { address to; bytes data; + bytes extraData; } struct ActionState { @@ -174,7 +175,7 @@ abstract contract RestrictedMultisigBase { } function _hashAction(Action memory action) internal pure returns (bytes32) { - return keccak256(abi.encode(action.to, action.data)); + return keccak256(abi.encode(action.to, action.data, action.extraData)); } modifier onlyMember() { diff --git a/contracts/TiebreakerCore.sol b/contracts/TiebreakerCore.sol index 06dfe31a..8052e607 100644 --- a/contracts/TiebreakerCore.sol +++ b/contracts/TiebreakerCore.sol @@ -4,8 +4,12 @@ pragma solidity 0.8.23; import {RestrictedMultisigBase} from "./RestrictedMultisigBase.sol"; contract TiebreakerCore is RestrictedMultisigBase { + error ResumeSealableNonceMismatch(); + address immutable DUAL_GOVERNANCE; + mapping(address => uint256) public _sealableResumeNonces; + constructor( address owner, address[] memory multisigMembers, @@ -15,6 +19,8 @@ contract TiebreakerCore is RestrictedMultisigBase { DUAL_GOVERNANCE = dualGovernance; } + // Approve proposal + function approveProposal(uint256 _proposalId) public onlyMember { _vote(_buildApproveProposalAction(_proposalId), true); } @@ -31,7 +37,45 @@ contract TiebreakerCore is RestrictedMultisigBase { _execute(_buildApproveProposalAction(_proposalId)); } + // Resume sealable + + function getSealableResumeNonce(address sealable) public view returns (uint256) { + return _sealableResumeNonces[sealable]; + } + + function approveSealableResume(address sealable, uint256 nonce) public { + if (nonce != _sealableResumeNonces[sealable]) { + revert ResumeSealableNonceMismatch(); + } + _vote(_buildSealableResumeAction(sealable, nonce), true); + } + + function getSealableResumeState( + address sealable, + uint256 nonce + ) public view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { + return getActionState(_buildSealableResumeAction(sealable, nonce)); + } + + function executeSealableResume(address sealable, uint256 nonce) external { + if (nonce != _sealableResumeNonces[sealable]) { + revert ResumeSealableNonceMismatch(); + } + _execute(_buildSealableResumeAction(sealable, nonce)); + _sealableResumeNonces[sealable]++; + } + function _buildApproveProposalAction(uint256 _proposalId) internal view returns (Action memory) { - return Action(DUAL_GOVERNANCE, abi.encodeWithSignature("tiebreakerApproveProposal(uint256)", _proposalId)); + return Action( + DUAL_GOVERNANCE, abi.encodeWithSignature("tiebreakerApproveProposal(uint256)", _proposalId), new bytes(0) + ); + } + + function _buildSealableResumeAction(address sealable, uint256 nonce) internal view returns (Action memory) { + return Action( + DUAL_GOVERNANCE, + abi.encodeWithSignature("tiebreakerApproveSealableResume(uint256)", sealable), + abi.encode(nonce) + ); } } diff --git a/contracts/TiebreakerSubCommittee.sol b/contracts/TiebreakerSubCommittee.sol index 7f9d3181..ce20d2f7 100644 --- a/contracts/TiebreakerSubCommittee.sol +++ b/contracts/TiebreakerSubCommittee.sol @@ -3,6 +3,10 @@ pragma solidity 0.8.23; import {RestrictedMultisigBase} from "./RestrictedMultisigBase.sol"; +interface ITiebreakerCore { + function getSealableResumeNonce(address sealable) external view returns (uint256 nonce); +} + contract TiebreakerSubCommittee is RestrictedMultisigBase { address immutable TIEBREAKER_CORE; @@ -15,23 +19,52 @@ contract TiebreakerSubCommittee is RestrictedMultisigBase { TIEBREAKER_CORE = tiebreakerCore; } - function voteApproveProposal(uint256 _proposalId, bool _supports) public onlyMember { - _vote(_buildApproveProposalAction(_proposalId), _supports); + // Approve proposal + + function voteApproveProposal(uint256 proposalId, bool support) public onlyMember { + _vote(_buildApproveProposalAction(proposalId), support); + } + + function getApproveProposalState(uint256 proposalId) + public + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + { + return getActionState(_buildApproveProposalAction(proposalId)); + } + + function executeApproveProposal(uint256 proposalId) public { + _execute(_buildApproveProposalAction(proposalId)); + } + + // Approve unpause sealable + + function voteApproveSealableResume(address sealable, bool support) external { + _vote(_buildApproveSealableResumeAction(sealable), support); } - function getApproveProposalState(uint256 _proposalId) + function getApproveSealableResumeState(address sealable) public view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return getActionState(_buildApproveProposalAction(_proposalId)); + return getActionState(_buildApproveSealableResumeAction(sealable)); + } + + function executeApproveSealableResume(address sealable) external { + _execute(_buildApproveSealableResumeAction(sealable)); } - function executeApproveProposal(uint256 _proposalId) public { - _execute(_buildApproveProposalAction(_proposalId)); + function _buildApproveSealableResumeAction(address sealable) internal view returns (Action memory) { + uint256 nonce = ITiebreakerCore(TIEBREAKER_CORE).getSealableResumeNonce(sealable); + return Action( + TIEBREAKER_CORE, + abi.encodeWithSignature("approveSealableResume(address,uint256)", sealable, nonce), + new bytes(0) + ); } - function _buildApproveProposalAction(uint256 _proposalId) internal view returns (Action memory) { - return Action(TIEBREAKER_CORE, abi.encodeWithSignature("approveProposal(uint256)", _proposalId)); + function _buildApproveProposalAction(uint256 proposalId) internal view returns (Action memory) { + return Action(TIEBREAKER_CORE, abi.encodeWithSignature("approveProposal(uint256)", proposalId), new bytes(0)); } } From fd45a615ced7fc2d035ae724c40d74588e9b7e8b Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 24 Apr 2024 11:07:41 +0300 Subject: [PATCH 11/38] rename restricted multisig -> executive committee --- ...onMultisig.sol => EmergencyActivationCommittee.sol} | 6 +++--- ...ionMultisig.sol => EmergencyExecutionCommittee.sol} | 6 +++--- contracts/EmergencyProtectedTimelock.sol | 4 ++-- ...strictedMultisigBase.sol => ExecutiveCommittee.sol} | 2 +- contracts/TiebreakerCore.sol | 10 +++++----- contracts/TiebreakerSubCommittee.sol | 10 +++++----- contracts/libraries/EmergencyProtection.sol | 10 +++++----- 7 files changed, 24 insertions(+), 24 deletions(-) rename contracts/{EmergencyActivationMultisig.sol => EmergencyActivationCommittee.sol} (83%) rename contracts/{EmergencyExecutionMultisig.sol => EmergencyExecutionCommittee.sol} (90%) rename contracts/{RestrictedMultisigBase.sol => ExecutiveCommittee.sol} (99%) diff --git a/contracts/EmergencyActivationMultisig.sol b/contracts/EmergencyActivationCommittee.sol similarity index 83% rename from contracts/EmergencyActivationMultisig.sol rename to contracts/EmergencyActivationCommittee.sol index c6d42ca9..19ae1347 100644 --- a/contracts/EmergencyActivationMultisig.sol +++ b/contracts/EmergencyActivationCommittee.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {RestrictedMultisigBase} from "./RestrictedMultisigBase.sol"; +import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; -contract EmergencyActivationMultisig is RestrictedMultisigBase { +contract EmergencyActivationCommittee is ExecutiveCommittee { address public immutable EMERGENCY_PROTECTED_TIMELOCK; constructor( @@ -11,7 +11,7 @@ contract EmergencyActivationMultisig is RestrictedMultisigBase { address[] memory multisigMembers, uint256 executionQuorum, address emergencyProtectedTimelock - ) RestrictedMultisigBase(OWNER, multisigMembers, executionQuorum) { + ) ExecutiveCommittee(OWNER, multisigMembers, executionQuorum) { EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; } diff --git a/contracts/EmergencyExecutionMultisig.sol b/contracts/EmergencyExecutionCommittee.sol similarity index 90% rename from contracts/EmergencyExecutionMultisig.sol rename to contracts/EmergencyExecutionCommittee.sol index 4419b0bd..3150db0e 100644 --- a/contracts/EmergencyExecutionMultisig.sol +++ b/contracts/EmergencyExecutionCommittee.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {RestrictedMultisigBase} from "./RestrictedMultisigBase.sol"; +import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; interface IEmergencyProtectedTimelock { function emergencyExecute(uint256 proposalId) external; function emergencyReset() external; } -contract EmergencyExecutionMultisig is RestrictedMultisigBase { +contract EmergencyExecutiveCommittee is ExecutiveCommittee { address public immutable EMERGENCY_PROTECTED_TIMELOCK; constructor( @@ -16,7 +16,7 @@ contract EmergencyExecutionMultisig is RestrictedMultisigBase { address[] memory multisigMembers, uint256 executionQuorum, address emergencyProtectedTimelock - ) RestrictedMultisigBase(OWNER, multisigMembers, executionQuorum) { + ) ExecutiveCommittee(OWNER, multisigMembers, executionQuorum) { EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; } diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index e44cb2d2..6cf3b073 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -70,7 +70,7 @@ contract EmergencyProtectedTimelock is ConfigurationProvider { function emergencyExecute(uint256 proposalId) external { _emergencyProtection.checkEmergencyModeActive(true); - _emergencyProtection.checkExecutionCommittee(msg.sender); + _emergencyProtection.checkExecutiveCommittee(msg.sender); _proposals.execute(proposalId, /* afterScheduleDelay */ 0); } @@ -85,7 +85,7 @@ contract EmergencyProtectedTimelock is ConfigurationProvider { function emergencyReset() external { _emergencyProtection.checkEmergencyModeActive(true); - _emergencyProtection.checkExecutionCommittee(msg.sender); + _emergencyProtection.checkExecutiveCommittee(msg.sender); _emergencyProtection.deactivate(); _setGovernance(CONFIG.EMERGENCY_GOVERNANCE()); _proposals.cancelAll(); diff --git a/contracts/RestrictedMultisigBase.sol b/contracts/ExecutiveCommittee.sol similarity index 99% rename from contracts/RestrictedMultisigBase.sol rename to contracts/ExecutiveCommittee.sol index 6ee2dc53..7da43c7f 100644 --- a/contracts/RestrictedMultisigBase.sol +++ b/contracts/ExecutiveCommittee.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.23; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -abstract contract RestrictedMultisigBase { +abstract contract ExecutiveCommittee { event MemberAdded(address indexed member); event MemberRemoved(address indexed member); event QuorumSet(uint256 quorum); diff --git a/contracts/TiebreakerCore.sol b/contracts/TiebreakerCore.sol index 8052e607..ac0fe04b 100644 --- a/contracts/TiebreakerCore.sol +++ b/contracts/TiebreakerCore.sol @@ -1,21 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {RestrictedMultisigBase} from "./RestrictedMultisigBase.sol"; +import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; -contract TiebreakerCore is RestrictedMultisigBase { +contract TiebreakerCore is ExecutiveCommittee { error ResumeSealableNonceMismatch(); address immutable DUAL_GOVERNANCE; - mapping(address => uint256) public _sealableResumeNonces; + mapping(address => uint256) private _sealableResumeNonces; constructor( address owner, address[] memory multisigMembers, uint256 executionQuorum, address dualGovernance - ) RestrictedMultisigBase(owner, multisigMembers, executionQuorum) { + ) ExecutiveCommittee(owner, multisigMembers, executionQuorum) { DUAL_GOVERNANCE = dualGovernance; } @@ -43,7 +43,7 @@ contract TiebreakerCore is RestrictedMultisigBase { return _sealableResumeNonces[sealable]; } - function approveSealableResume(address sealable, uint256 nonce) public { + function approveSealableResume(address sealable, uint256 nonce) public onlyMember { if (nonce != _sealableResumeNonces[sealable]) { revert ResumeSealableNonceMismatch(); } diff --git a/contracts/TiebreakerSubCommittee.sol b/contracts/TiebreakerSubCommittee.sol index ce20d2f7..4c8223ca 100644 --- a/contracts/TiebreakerSubCommittee.sol +++ b/contracts/TiebreakerSubCommittee.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {RestrictedMultisigBase} from "./RestrictedMultisigBase.sol"; +import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; interface ITiebreakerCore { function getSealableResumeNonce(address sealable) external view returns (uint256 nonce); } -contract TiebreakerSubCommittee is RestrictedMultisigBase { +contract TiebreakerSubCommittee is ExecutiveCommittee { address immutable TIEBREAKER_CORE; constructor( @@ -15,7 +15,7 @@ contract TiebreakerSubCommittee is RestrictedMultisigBase { address[] memory multisigMembers, uint256 executionQuorum, address tiebreakerCore - ) RestrictedMultisigBase(owner, multisigMembers, executionQuorum) { + ) ExecutiveCommittee(owner, multisigMembers, executionQuorum) { TIEBREAKER_CORE = tiebreakerCore; } @@ -39,7 +39,7 @@ contract TiebreakerSubCommittee is RestrictedMultisigBase { // Approve unpause sealable - function voteApproveSealableResume(address sealable, bool support) external { + function voteApproveSealableResume(address sealable, bool support) public { _vote(_buildApproveSealableResumeAction(sealable), support); } @@ -51,7 +51,7 @@ contract TiebreakerSubCommittee is RestrictedMultisigBase { return getActionState(_buildApproveSealableResumeAction(sealable)); } - function executeApproveSealableResume(address sealable) external { + function executeApproveSealableResume(address sealable) public { _execute(_buildApproveSealableResumeAction(sealable)); } diff --git a/contracts/libraries/EmergencyProtection.sol b/contracts/libraries/EmergencyProtection.sol index 758525e1..e2ab8c43 100644 --- a/contracts/libraries/EmergencyProtection.sol +++ b/contracts/libraries/EmergencyProtection.sol @@ -23,7 +23,7 @@ library EmergencyProtection { event EmergencyModeDeactivated(); event EmergencyGovernanceReset(); event EmergencyActivationCommitteeSet(address indexed activationCommittee); - event EmergencyExecutionCommitteeSet(address indexed executionCommittee); + event EmergencyExecutiveCommitteeSet(address indexed executionCommittee); event EmergencyModeDurationSet(uint256 emergencyModeDuration); event EmergencyCommitteeProtectedTillSet(uint256 protectedTill); @@ -51,10 +51,10 @@ library EmergencyProtection { emit EmergencyActivationCommitteeSet(activationCommittee); } - address prevExecutionCommittee = self.executionCommittee; - if (executionCommittee != prevExecutionCommittee) { + address prevExecutiveCommittee = self.executionCommittee; + if (executionCommittee != prevExecutiveCommittee) { self.executionCommittee = executionCommittee; - emit EmergencyExecutionCommitteeSet(executionCommittee); + emit EmergencyExecutiveCommitteeSet(executionCommittee); } uint256 prevProtectedTill = self.protectedTill; @@ -117,7 +117,7 @@ library EmergencyProtection { } } - function checkExecutionCommittee(State storage self, address account) internal view { + function checkExecutiveCommittee(State storage self, address account) internal view { if (self.executionCommittee != account) { revert NotEmergencyEnactor(account); } From ab1ed75a4d142204430c9da5adec205e1d215699 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 24 Apr 2024 11:49:08 +0300 Subject: [PATCH 12/38] rename restricted multisig -> executive committee --- contracts/EmergencyActivationCommittee.sol | 4 ++-- contracts/EmergencyExecutionCommittee.sol | 4 ++-- contracts/TiebreakerCore.sol | 4 ++-- contracts/TiebreakerSubCommittee.sol | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/EmergencyActivationCommittee.sol b/contracts/EmergencyActivationCommittee.sol index 19ae1347..9acd5341 100644 --- a/contracts/EmergencyActivationCommittee.sol +++ b/contracts/EmergencyActivationCommittee.sol @@ -8,10 +8,10 @@ contract EmergencyActivationCommittee is ExecutiveCommittee { constructor( address OWNER, - address[] memory multisigMembers, + address[] memory committeeMembers, uint256 executionQuorum, address emergencyProtectedTimelock - ) ExecutiveCommittee(OWNER, multisigMembers, executionQuorum) { + ) ExecutiveCommittee(OWNER, committeeMembers, executionQuorum) { EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; } diff --git a/contracts/EmergencyExecutionCommittee.sol b/contracts/EmergencyExecutionCommittee.sol index 3150db0e..6cd9449d 100644 --- a/contracts/EmergencyExecutionCommittee.sol +++ b/contracts/EmergencyExecutionCommittee.sol @@ -13,10 +13,10 @@ contract EmergencyExecutiveCommittee is ExecutiveCommittee { constructor( address OWNER, - address[] memory multisigMembers, + address[] memory committeeMembers, uint256 executionQuorum, address emergencyProtectedTimelock - ) ExecutiveCommittee(OWNER, multisigMembers, executionQuorum) { + ) ExecutiveCommittee(OWNER, committeeMembers, executionQuorum) { EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; } diff --git a/contracts/TiebreakerCore.sol b/contracts/TiebreakerCore.sol index ac0fe04b..7ed181fb 100644 --- a/contracts/TiebreakerCore.sol +++ b/contracts/TiebreakerCore.sol @@ -12,10 +12,10 @@ contract TiebreakerCore is ExecutiveCommittee { constructor( address owner, - address[] memory multisigMembers, + address[] memory committeeMembers, uint256 executionQuorum, address dualGovernance - ) ExecutiveCommittee(owner, multisigMembers, executionQuorum) { + ) ExecutiveCommittee(owner, committeeMembers, executionQuorum) { DUAL_GOVERNANCE = dualGovernance; } diff --git a/contracts/TiebreakerSubCommittee.sol b/contracts/TiebreakerSubCommittee.sol index 4c8223ca..f5f64353 100644 --- a/contracts/TiebreakerSubCommittee.sol +++ b/contracts/TiebreakerSubCommittee.sol @@ -12,10 +12,10 @@ contract TiebreakerSubCommittee is ExecutiveCommittee { constructor( address owner, - address[] memory multisigMembers, + address[] memory committeeMembers, uint256 executionQuorum, address tiebreakerCore - ) ExecutiveCommittee(owner, multisigMembers, executionQuorum) { + ) ExecutiveCommittee(owner, committeeMembers, executionQuorum) { TIEBREAKER_CORE = tiebreakerCore; } From 322e0d06262afdf4162bbf6cc4ac243ebee4641c Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 24 Apr 2024 14:02:31 +0300 Subject: [PATCH 13/38] reseal executor and committee --- contracts/GateSealBreaker.sol | 128 ---------------- contracts/ResealCommittee.sol | 46 ++++++ contracts/ResealExecutor.sol | 61 ++++++++ contracts/interfaces/IGateSeal.sol | 3 - contracts/interfaces/ISealable.sol | 1 + docs/specification.md | 73 ++-------- test/mocks/GateSealMock.sol | 20 +-- test/scenario/gate-seal-breaker.t.sol | 202 -------------------------- test/scenario/reseal-executor.t.sol | 150 +++++++++++++++++++ test/scenario/tiebraker.t.sol | 133 ----------------- test/utils/interfaces.sol | 1 + 11 files changed, 276 insertions(+), 542 deletions(-) delete mode 100644 contracts/GateSealBreaker.sol create mode 100644 contracts/ResealCommittee.sol create mode 100644 contracts/ResealExecutor.sol delete mode 100644 test/scenario/gate-seal-breaker.t.sol create mode 100644 test/scenario/reseal-executor.t.sol delete mode 100644 test/scenario/tiebraker.t.sol diff --git a/contracts/GateSealBreaker.sol b/contracts/GateSealBreaker.sol deleted file mode 100644 index 58c75587..00000000 --- a/contracts/GateSealBreaker.sol +++ /dev/null @@ -1,128 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import {IGateSeal} from "./interfaces/IGateSeal.sol"; -import {ISealable} from "./interfaces/ISealable.sol"; -import {SealableCalls} from "./libraries/SealableCalls.sol"; - -interface IDualGovernance { - function isSchedulingEnabled() external view returns (bool); -} - -contract GateSealBreaker is Ownable { - using SafeCast for uint256; - using SealableCalls for ISealable; - - struct GateSealState { - uint40 registeredAt; - uint40 releaseStartedAt; - uint40 releaseEnactedAt; - } - - error GovernanceLocked(); - error ReleaseNotStarted(); - error GateSealNotActivated(); - error ReleaseDelayNotPassed(); - error DualGovernanceIsLocked(); - error GateSealAlreadyReleased(); - error MinSealDurationNotPassed(); - error GateSealIsNotRegistered(IGateSeal gateSeal); - error GateSealAlreadyRegistered(IGateSeal gateSeal, uint256 registeredAt); - - event ReleaseIsPausedConditionNotMet(ISealable sealable); - event ReleaseResumeCallFailed(ISealable sealable, bytes lowLevelError); - event ReleaseIsPausedCheckFailed(ISealable sealable, bytes lowLevelError); - - uint256 public immutable RELEASE_DELAY; - IDualGovernance public immutable DUAL_GOVERNANCE; - - constructor(uint256 releaseDelay, address owner, address dualGovernance) Ownable(owner) { - RELEASE_DELAY = releaseDelay; - DUAL_GOVERNANCE = IDualGovernance(dualGovernance); - } - - mapping(IGateSeal gateSeal => GateSealState) internal _gateSeals; - - function registerGateSeal(IGateSeal gateSeal) external { - _checkOwner(); - if (_gateSeals[gateSeal].registeredAt != 0) { - revert GateSealAlreadyRegistered(gateSeal, _gateSeals[gateSeal].registeredAt); - } - _gateSeals[gateSeal].registeredAt = block.timestamp.toUint40(); - } - - function startRelease(IGateSeal gateSeal) external { - _checkGateSealRegistered(gateSeal); - _checkGateSealActivated(gateSeal); - _checkMinSealDurationPassed(gateSeal); - _checkGateSealNotReleased(gateSeal); - _checkGovernanceNotLocked(); - - _gateSeals[gateSeal].releaseStartedAt = block.timestamp.toUint40(); - } - - function enactRelease(IGateSeal gateSeal) external { - _checkGateSealRegistered(gateSeal); - GateSealState memory gateSealState = _gateSeals[gateSeal]; - if (gateSealState.releaseStartedAt == 0) { - revert ReleaseNotStarted(); - } - if (block.timestamp <= gateSealState.releaseStartedAt + RELEASE_DELAY) { - revert ReleaseDelayNotPassed(); - } - - _gateSeals[gateSeal].releaseEnactedAt = block.timestamp.toUint40(); - - address[] memory sealed_ = gateSeal.sealed_sealables(); - - for (uint256 i = 0; i < sealed_.length; ++i) { - ISealable sealable = ISealable(sealed_[i]); - (bool isPausedCallSuccess, bytes memory isPausedLowLevelError, bool isPaused) = sealable.callIsPaused(); - if (!isPausedCallSuccess) { - emit ReleaseIsPausedCheckFailed(sealable, isPausedLowLevelError); - } - if (!isPaused) { - emit ReleaseIsPausedConditionNotMet(sealable); - continue; - } - (bool resumeCallSuccess, bytes memory lowLevelError) = sealable.callResume(); - if (!resumeCallSuccess) { - emit ReleaseResumeCallFailed(sealable, lowLevelError); - } - } - } - - function _checkGateSealRegistered(IGateSeal gateSeal) internal view { - if (_gateSeals[gateSeal].registeredAt == 0) { - revert GateSealIsNotRegistered(gateSeal); - } - } - - function _checkGateSealActivated(IGateSeal gateSeal) internal view { - address[] memory sealed_ = gateSeal.sealed_sealables(); - if (sealed_.length == 0) { - revert GateSealNotActivated(); - } - } - - function _checkMinSealDurationPassed(IGateSeal gateSeal) internal view { - if (block.timestamp < gateSeal.get_expiry_timestamp() + gateSeal.get_min_seal_duration()) { - revert MinSealDurationNotPassed(); - } - } - - function _checkGateSealNotReleased(IGateSeal gateSeal) internal view { - if (_gateSeals[gateSeal].releaseStartedAt != 0) { - revert GateSealAlreadyReleased(); - } - } - - function _checkGovernanceNotLocked() internal view { - if (!DUAL_GOVERNANCE.isSchedulingEnabled()) { - revert GovernanceLocked(); - } - } -} diff --git a/contracts/ResealCommittee.sol b/contracts/ResealCommittee.sol new file mode 100644 index 00000000..8f5347d7 --- /dev/null +++ b/contracts/ResealCommittee.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; + +contract ResealCommittee is ExecutiveCommittee { + address public immutable RESEAL_EXECUTOR; + + mapping(bytes32 => uint256) private _resealNonces; + + constructor( + address owner, + address[] memory committeeMembers, + uint256 executionQuorum, + address resealExecutor + ) ExecutiveCommittee(owner, committeeMembers, executionQuorum) { + RESEAL_EXECUTOR = resealExecutor; + } + + function voteReseal(address[] memory sealables, bool support) public onlyMember { + _vote(_buildResealAction(sealables), support); + } + + function getResealState(address[] memory sealables) + public + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + { + return getActionState(_buildResealAction(sealables)); + } + + function executeReseal(address[] memory sealables) external { + _execute(_buildResealAction(sealables)); + bytes32 resealNonceHash = keccak256(abi.encode(sealables)); + _resealNonces[resealNonceHash]++; + } + + function _buildResealAction(address[] memory sealables) internal view returns (Action memory) { + bytes32 resealNonceHash = keccak256(abi.encode(sealables)); + return Action( + RESEAL_EXECUTOR, + abi.encodeWithSignature("reseal(address[])", sealables), + abi.encode(_resealNonces[resealNonceHash]) + ); + } +} diff --git a/contracts/ResealExecutor.sol b/contracts/ResealExecutor.sol new file mode 100644 index 00000000..80c1b0e0 --- /dev/null +++ b/contracts/ResealExecutor.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {OwnableExecutor, Address} from "./OwnableExecutor.sol"; +import {ISealable} from "./interfaces/ISealable.sol"; + +interface IDualGovernanace { + enum GovernanceState { + Normal, + VetoSignalling, + VetoSignallingDeactivation, + VetoCooldown, + RageQuit + } + + function currentState() external view returns (GovernanceState); +} + +contract ResealExecutor is OwnableExecutor { + event ResealCommitteeSet(address indexed newResealCommittee); + + error SenderIsNotCommittee(); + error DualGovernanceInNormalState(); + error SealableWrongPauseState(); + + uint256 public constant PAUSE_INFINITELY = type(uint256).max; + address public immutable DUAL_GOVERNANCE; + + address public resealCommittee; + + constructor(address owner, address dualGovernance, address resealCommitteeAddress) OwnableExecutor(owner) { + DUAL_GOVERNANCE = dualGovernance; + resealCommittee = resealCommitteeAddress; + } + + function reseal(address[] memory sealables) public onlyCommittee { + if (IDualGovernanace(DUAL_GOVERNANCE).currentState() == IDualGovernanace.GovernanceState.Normal) { + revert DualGovernanceInNormalState(); + } + for (uint256 i = 0; i < sealables.length; ++i) { + uint256 sealableResumeSinceTimestamp = ISealable(sealables[i]).getResumeSinceTimestamp(); + if (sealableResumeSinceTimestamp < block.timestamp || sealableResumeSinceTimestamp == PAUSE_INFINITELY) { + revert SealableWrongPauseState(); + } + Address.functionCall(sealables[i], abi.encodeWithSelector(ISealable.resume.selector)); + Address.functionCall(sealables[i], abi.encodeWithSelector(ISealable.pauseFor.selector, PAUSE_INFINITELY)); + } + } + + function setResealCommittee(address newResealCommittee) public onlyOwner { + resealCommittee = newResealCommittee; + emit ResealCommitteeSet(newResealCommittee); + } + + modifier onlyCommittee() { + if (msg.sender != resealCommittee) { + revert SenderIsNotCommittee(); + } + _; + } +} diff --git a/contracts/interfaces/IGateSeal.sol b/contracts/interfaces/IGateSeal.sol index 2b2a121d..9dd6d07e 100644 --- a/contracts/interfaces/IGateSeal.sol +++ b/contracts/interfaces/IGateSeal.sol @@ -2,8 +2,5 @@ pragma solidity 0.8.23; interface IGateSeal { - function get_min_seal_duration() external view returns (uint256); - function get_expiry_timestamp() external view returns (uint256); - function sealed_sealables() external view returns (address[] memory); function seal(address[] calldata sealables) external; } diff --git a/contracts/interfaces/ISealable.sol b/contracts/interfaces/ISealable.sol index 12239b92..df924ec5 100644 --- a/contracts/interfaces/ISealable.sol +++ b/contracts/interfaces/ISealable.sol @@ -5,4 +5,5 @@ interface ISealable { function resume() external; function pauseFor(uint256 duration) external; function isPaused() external view returns (bool); + function getResumeSinceTimestamp() external view returns (uint256); } diff --git a/docs/specification.md b/docs/specification.md index e250f4b7..a9944fbf 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -23,8 +23,7 @@ The system is composed of the following main contracts: * [`EmergencyProtectedTimelock.sol`](#Contract-EmergencyProtectedTimelocksol) is a singleton that stores submitted proposals and provides an interface for their execution. In addition, it implements an optional temporary protection from a zero-day vulnerability in the dual governance contracts following the initial deployment or upgrade of the system. The protection is implemented as a timelock on proposal execution combined with two emergency committees that have the right to cooperate and disable the dual governance. * [`Executor.sol`](#Contract-Executorsol) contract instances make calls resulting from governance proposals' execution. Every protocol permission or role protected by the DG, as well as the permission to manage this role/permission, should be assigned exclusively to one of the instances of this contract (in contrast with being assigned directly to a DAO voting system). * [`Escrow.sol`](#Contract-Escrowsol) is a contract that can hold stETH, wstETH, withdrawal NFTs, and plain ETH. It can exist in two states, each serving a different purpose: either an oracle for users' opposition to DAO proposals or an immutable and ungoverned accumulator for the ETH withdrawn as a result of the [rage quit](#Rage-quit). -* [`GateSealBreaker.sol`](#contract-gatesealbreakersol) is a singleton that allows anyone to unpause the protocol contracts that were put into an emergency pause by the [GateSeal emergency protection mechanism](https://github.com/lidofinance/gate-seals), given that the minimum pause duration has passed and that the DAO execution is not currently blocked by the DG system. - +* [`ResealExecutor.sol`](#Contract-ResealExecutorsol) contract instances make calls to extend protocol withdrawals pause in case of contracts were put into an emergency pause by the [GateSeal emergency protection mechanism](https://github.com/lidofinance/gate-seals) and the DAO governance is currently blocked by the DG system. Has pause and resume roles for all protocols withdrawals contracts. ## Proposal flow @@ -902,7 +901,7 @@ Resets the `governance` address to the `EMERGENCY_GOVERNANCE` value defined in t The contract has the interface for managing the configuration related to emergency protection (`setEmergencyProtection`) and general system wiring (`transferExecutorOwnership`, `setGovernance`). These functions MUST be called by the [Admin Executor](#Administrative-actions) address, basically routing any such changes through the Dual Governance mechanics. -## Contract: GateSealBreaker.sol +## Contract: ResealExecutor.sol In the Lido protocol, specific critical components (`WithdrawalQueue` and `ValidatorsExitBus`) are safeguarded by the `GateSeal` contract instance. According to the gate seals [documentation](https://github.com/lidofinance/gate-seals?tab=readme-ov-file#what-is-a-gateseal): @@ -910,72 +909,26 @@ In the Lido protocol, specific critical components (`WithdrawalQueue` and `Valid However, the effectiveness of this approach is contingent upon the predictability of the DAO's solution adoption timeframe. With the dual governance system, proposal execution may experience significant delays based on the current state of the `DualGovernance` contract. There's a risk that `GateSeal`'s pause period may expire before the Lido DAO can implement the necessary fixes. -To address this compatibility challenge between gate seals and dual governance, the `GateSealBreaker` contract is introduced. The `GateSealBreaker` enables the trustless unpause of contracts sealed by a `GateSeal` instance, but only under specific conditions: -- The minimum delay defined in the `GateSeal` contract has elapsed. -- Proposal execution is allowed within the dual governance system. - -For seamless integration with the `DualGovernance` and `GateSealBreaker` contracts, the `GateSeal` instance will be configured as follows: - -- `MAX_SEAL_DURATION_SECONDS` and `SEAL_DURATION_SECONDS` are set to `type(uint256).max`, what equivalent to `PAUSE_INFINITELY`, for the [PausableUntil.sol](https://github.com/lidofinance/core/blob/master/contracts/0.8.9/utils/PausableUntil.sol) contract. -- `MIN_SEAL_DURATION_SECONDS` is set to a finite duration, allowing the Lido DAO sufficient time to respond and adopt proposals when the `DualGovernance` contract is in the `Normal` state. - -With such settings, the `GateSeal` instance seals the contracts indefinitely. However, anyone can initiate the process of "breaking the seal" by calling the `GateSealBreaker.startRelease(address gateSeal)` function, provided both requirements are met: - -- The `MIN_SEAL_DURATION_SECONDS` has elapsed since the committee activated the `GateSeal`. -- The `DualGovernance` is currently in the `Normal` or `VetoCooldown` state, allowing proposals scheduling. - -The `GateSealBreaker.startRelease()` function can be called only once for each activated `GateSeal` contract registered in the `GateSealBreaker`. This function effectively begins the countdown to release the seal, starting the `RELEASE_DELAY`. - -During the `RELEASE_DELAY`, the sealed contracts remain paused, providing the Lido DAO time to schedule proposals within the dual governance system (the scheduling is allowed, which is guaranteed by the governance state precondition of the `GateSealBreaker.startRelease` function). +To address this compatibility challenge between gate seals and dual governance, the `ResealExecutor` contract is introduced. The `ResealExecutor` allows to extend pause of temporarily paused contracts to permanent pause, if conditions are met: +- `ResealExecutor` has `PAUSE_ROLE` and `RESUME_ROLE` for target contracts. +- Contracts are paused until timestamp after current timestamp and not for infinite time. +- The DAO governance is blocked by `DualGovernance`. -Upon completion of the `RELEASE_DELAY`, the `GateSealBreaker.enactRelease(address gateSeal)` function can be called to unpause the sealed contracts. This function is trustless and may only be called once. It does not revert even if some or all attempts to unpause the sealed contracts fail. +It inherits `OwnableExecutor` and provides ability to extend contracts pause for committee set by DAO. -### Function GateSealBreaker.registerGateSeal +### Function ResealExecutor.reseal ```solidity -function registerGateSeal(IGateSeal gateSeal) +function reseal(address[] memory sealables) ``` -This function should be invoked by the Lido DAO during the setup of the `GateSeal` instance. Upon registration in the contract, an activated `GateSeal` instance becomes eligible for release using the `startRelease()`/`enactRelease()` methods. - -#### Preconditions - -- MUST be called by the contract owner (supposed to be set to Lido DAO). -- The `GateSeal` instance being registered MUST NOT have been previously registered. - -### Function GateSealBreaker.startRelease - -```solidity -function startRelease(IGateSeal gateSeal) -``` - -Initiates the release process for the activated `GateSeal` instance registered in the contract. Records the release initiation timestamp and starts the `RELEASE_DELAY` period for the specific `gateSeal`. - -#### Preconditions - -- The specified `gateSeal` MUST be registered in the contract. -- The `gateSeal` MUST be activated by the gate seal committee. -- The `MIN_SEAL_DURATION_SECONDS` MUST have passed since the activation of the `gateSeal`. -- The `gateSeal` MUST NOT be already released. -- The `DualGovernance` contract MUST be in either the `Normal` or `VetoCooldown` state. - -### Function GateSealBreaker.enactRelease - -```solidity -function enactRelease(IGateSeal gateSeal) -``` - -Unpauses all contracts sealed by the specified `gateSeal` once the `RELEASE_DELAY` has elapsed since the release initiation. - -Retrieves all sealed contracts via the `GateSeal.sealed_sealables()` view function and calls `IPausableUntil(sealable).resume()` for each sealed contract. - -If any call to a sealable, including the `resume()` call, fails during the execution, the transaction WILL NOT revert but will emit the `ErrorWhileResuming(sealable, lowLevelError)` event for each contract that failed to unpause. +This function extends pause of `sealables`. Can be called by committee address. #### Preconditions -- The `GateSealBreaker.startRelease()` function MUST be called for the specified `gateSeal`. -- The `RELEASE_DELAY` for the specified `gateSeal` MUST have elapsed since the release initiation. -- The `GateSealBreaker` contract SHOULD have been granted rights to unpause the sealed contracts. +- `ResealExecutor` has `PAUSE_ROLE` and `RESUME_ROLE` for target contracts. +- Contracts are paused until timestamp after current timestamp and not for infinite time. +- The DAO governance is blocked by `DualGovernance`. ## Contract: Configuration.sol diff --git a/test/mocks/GateSealMock.sol b/test/mocks/GateSealMock.sol index fbd8e069..632c2269 100644 --- a/test/mocks/GateSealMock.sol +++ b/test/mocks/GateSealMock.sol @@ -12,11 +12,11 @@ contract GateSealMock is IGateSeal { uint256 internal constant _INFINITE_DURATION = type(uint256).max; uint256 internal _expiryTimestamp; - uint256 internal _minSealDuration; + uint256 internal _seal_duration_seconds; address[] internal _sealedSealables; - constructor(uint256 minSealDuration, uint256 lifetime) { - _minSealDuration = minSealDuration; + constructor(uint256 sealDurationSeconds, uint256 lifetime) { + _seal_duration_seconds = sealDurationSeconds; _expiryTimestamp = block.timestamp + lifetime; } @@ -28,22 +28,10 @@ contract GateSealMock is IGateSeal { _expiryTimestamp = block.timestamp; for (uint256 i = 0; i < sealables.length; ++i) { - ISealable(sealables[i]).pauseFor(_INFINITE_DURATION); + ISealable(sealables[i]).pauseFor(_seal_duration_seconds); assert(ISealable(sealables[i]).isPaused()); } emit SealablesSealed(sealables); } - - function sealed_sealables() external view returns (address[] memory) { - return _sealedSealables; - } - - function get_min_seal_duration() external view returns (uint256) { - return _minSealDuration; - } - - function get_expiry_timestamp() external view returns (uint256) { - return _expiryTimestamp; - } } diff --git a/test/scenario/gate-seal-breaker.t.sol b/test/scenario/gate-seal-breaker.t.sol deleted file mode 100644 index 1b427efa..00000000 --- a/test/scenario/gate-seal-breaker.t.sol +++ /dev/null @@ -1,202 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import {percents, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; - -import {GateSealMock} from "../mocks/GateSealMock.sol"; -import {GateSealBreaker, IGateSeal} from "contracts/GateSealBreaker.sol"; - -import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; - -contract SealBreakerScenarioTest is ScenarioTestBlueprint { - uint256 private immutable _RELEASE_DELAY = 5 days; - uint256 private immutable _MIN_SEAL_DURATION = 14 days; - - address private immutable _VETOER = makeAddr("VETOER"); - - IGateSeal private _gateSeal; - address[] private _sealables; - GateSealBreaker private _sealBreaker; - - function setUp() external { - _selectFork(); - _deployTarget(); - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); - - _sealables.push(address(_WITHDRAWAL_QUEUE)); - - _gateSeal = new GateSealMock(_MIN_SEAL_DURATION, _SEALING_COMMITTEE_LIFETIME); - - _sealBreaker = new GateSealBreaker(_RELEASE_DELAY, address(this), address(_dualGovernance)); - - _sealBreaker.registerGateSeal(_gateSeal); - - // grant rights to gate seal to pause/resume the withdrawal queue - vm.startPrank(DAO_AGENT); - _WITHDRAWAL_QUEUE.grantRole(_WITHDRAWAL_QUEUE.PAUSE_ROLE(), address(_gateSeal)); - _WITHDRAWAL_QUEUE.grantRole(_WITHDRAWAL_QUEUE.RESUME_ROLE(), address(_sealBreaker)); - vm.stopPrank(); - } - - function testFork_DualGovernanceLockedThenSeal() external { - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - _assertNormalState(); - - _lockStETH(_VETOER, percents("10.0")); - _assertVetoSignalingState(); - - // sealing committee seals Withdrawal Queue - vm.prank(_SEALING_COMMITTEE); - _gateSeal.seal(_sealables); - - // seal can't be released before the min sealing duration has passed - vm.expectRevert(GateSealBreaker.MinSealDurationNotPassed.selector); - _sealBreaker.startRelease(_gateSeal); - - // validate Withdrawal Queue was paused - assertTrue(_WITHDRAWAL_QUEUE.isPaused()); - - _wait(_MIN_SEAL_DURATION + 1); - - // validate the dual governance still in the veto signaling state - _assertVetoSignalingState(); - - // seal can't be released before the governance returns to Normal state - vm.expectRevert(GateSealBreaker.GovernanceLocked.selector); - _sealBreaker.startRelease(_gateSeal); - - // wait the governance returns to normal state - _wait(14 days); - _activateNextState(); - _assertVetoSignalingDeactivationState(); - - _wait(_config.SIGNALLING_DEACTIVATION_DURATION() + 1); - _activateNextState(); - _assertVetoCooldownState(); - - // anyone may start release the seal - _sealBreaker.startRelease(_gateSeal); - - // reverts until timelock - vm.expectRevert(GateSealBreaker.ReleaseDelayNotPassed.selector); - _sealBreaker.enactRelease(_gateSeal); - - // anyone may release the seal after timelock - _wait(_RELEASE_DELAY + 1); - _sealBreaker.enactRelease(_gateSeal); - - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - } - - function testFork_SealThenDualGovernanceLocked() external { - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - _assertNormalState(); - - // sealing committee seals the Withdrawal Queue - vm.prank(_SEALING_COMMITTEE); - _gateSeal.seal(_sealables); - - // validate Withdrawal Queue was paused - assertTrue(_WITHDRAWAL_QUEUE.isPaused()); - - // wait some time, before dual governance enters veto signaling state - _wait(_MIN_SEAL_DURATION / 2); - - _lockStETH(_VETOER, percents("10.0")); - _assertVetoSignalingState(); - - // seal can't be released before the min sealing duration has passed - vm.expectRevert(GateSealBreaker.MinSealDurationNotPassed.selector); - _sealBreaker.startRelease(_gateSeal); - - _wait(_MIN_SEAL_DURATION / 2 + 1); - - // seal can't be released before the governance returns to Normal state - vm.expectRevert(GateSealBreaker.GovernanceLocked.selector); - _sealBreaker.startRelease(_gateSeal); - - // wait the governance returns to normal state - _wait(14 days); - _activateNextState(); - _assertVetoSignalingDeactivationState(); - - _wait(_dualGovernance.CONFIG().SIGNALLING_DEACTIVATION_DURATION() + 1); - _activateNextState(); - _assertVetoCooldownState(); - - // the stETH whale takes his funds back from Escrow - _unlockStETH(_VETOER); - - _wait(_dualGovernance.CONFIG().SIGNALLING_COOLDOWN_DURATION() + 1); - _activateNextState(); - _assertNormalState(); - - // now seal may be released - _sealBreaker.startRelease(_gateSeal); - - // reverts until timelock - vm.expectRevert(GateSealBreaker.ReleaseDelayNotPassed.selector); - _sealBreaker.enactRelease(_gateSeal); - - // anyone may release the seal after timelock - _wait(_RELEASE_DELAY + 1); - _sealBreaker.enactRelease(_gateSeal); - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - } - - function testFork_SealWhenDualGovernanceNotLocked() external { - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - _assertNormalState(); - - // sealing committee seals the Withdrawal Queue - vm.prank(_SEALING_COMMITTEE); - _gateSeal.seal(_sealables); - - // validate Withdrawal Queue was paused - assertTrue(_WITHDRAWAL_QUEUE.isPaused()); - - // seal can't be released before the min sealing duration has passed - vm.expectRevert(GateSealBreaker.MinSealDurationNotPassed.selector); - _sealBreaker.startRelease(_gateSeal); - - vm.warp(block.timestamp + _MIN_SEAL_DURATION + 1); - - // now seal may be released - _sealBreaker.startRelease(_gateSeal); - - // reverts until timelock - vm.expectRevert(GateSealBreaker.ReleaseDelayNotPassed.selector); - _sealBreaker.enactRelease(_gateSeal); - - // anyone may release the seal after timelock - _wait(_RELEASE_DELAY + 1); - _sealBreaker.enactRelease(_gateSeal); - - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - } - - function testFork_GateSealMayBeReleasedOnlyOnce() external { - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - _assertNormalState(); - - // sealing committee seals the Withdrawal Queue - vm.prank(_SEALING_COMMITTEE); - _gateSeal.seal(_sealables); - - // validate Withdrawal Queue was paused - assertTrue(_WITHDRAWAL_QUEUE.isPaused()); - - // seal can't be released before the min sealing duration has passed - vm.expectRevert(GateSealBreaker.MinSealDurationNotPassed.selector); - _sealBreaker.startRelease(_gateSeal); - - _wait(_MIN_SEAL_DURATION + 1); - - // now seal may be released - _sealBreaker.startRelease(_gateSeal); - - // An attempt to release same gate seal the second time fails - vm.expectRevert(GateSealBreaker.GateSealAlreadyReleased.selector); - _sealBreaker.startRelease(_gateSeal); - } -} diff --git a/test/scenario/reseal-executor.t.sol b/test/scenario/reseal-executor.t.sol new file mode 100644 index 00000000..9fbb2953 --- /dev/null +++ b/test/scenario/reseal-executor.t.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {percents, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; + +import {GateSealMock} from "../mocks/GateSealMock.sol"; +import {ResealExecutor} from "contracts/ResealExecutor.sol"; +import {ResealCommittee} from "contracts/ResealCommittee.sol"; +import {IGateSeal} from "contracts/interfaces/IGateSeal.sol"; + +import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; + +contract ResealExecutorScenarioTest is ScenarioTestBlueprint { + uint256 private immutable _RELEASE_DELAY = 5 days; + uint256 private immutable _SEAL_DURATION = 14 days; + uint256 private constant _PAUSE_INFINITELY = type(uint256).max; + + address private immutable _VETOER = makeAddr("VETOER"); + + IGateSeal private _gateSeal; + address[] private _sealables; + ResealExecutor private _resealExecutor; + ResealCommittee private _resealCommittee; + + uint256 private _resealCommitteeMembersCount = 5; + uint256 private _resealCommitteeQuorum = 3; + address[] private _resealCommitteeMembers = new address[](0); + + function setUp() external { + _selectFork(); + _deployTarget(); + _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); + + _sealables.push(address(_WITHDRAWAL_QUEUE)); + + _gateSeal = new GateSealMock(_SEAL_DURATION, _SEALING_COMMITTEE_LIFETIME); + + _resealExecutor = new ResealExecutor(address(this), address(_dualGovernance), address(this)); + for (uint256 i = 0; i < _resealCommitteeMembersCount; i++) { + _resealCommitteeMembers.push(makeAddr(string(abi.encode(i + 65)))); + } + _resealCommittee = new ResealCommittee( + address(this), _resealCommitteeMembers, _resealCommitteeQuorum, address(_resealExecutor) + ); + + _resealExecutor.setResealCommittee(address(_resealCommittee)); + + // grant rights to gate seal to pause/resume the withdrawal queue + vm.startPrank(DAO_AGENT); + _WITHDRAWAL_QUEUE.grantRole(_WITHDRAWAL_QUEUE.PAUSE_ROLE(), address(_gateSeal)); + _WITHDRAWAL_QUEUE.grantRole(_WITHDRAWAL_QUEUE.PAUSE_ROLE(), address(_resealExecutor)); + _WITHDRAWAL_QUEUE.grantRole(_WITHDRAWAL_QUEUE.RESUME_ROLE(), address(_resealExecutor)); + vm.stopPrank(); + } + + function testFork_resealingWithLockedGovernance() external { + assertFalse(_WITHDRAWAL_QUEUE.isPaused()); + _assertNormalState(); + + _lockStETH(_VETOER, percents("10.0")); + _assertVetoSignalingState(); + + // sealing committee seals Withdrawal Queue + vm.prank(_SEALING_COMMITTEE); + _gateSeal.seal(_sealables); + + // validate Withdrawal Queue was paused + assertTrue(_WITHDRAWAL_QUEUE.isPaused()); + + // validate the dual governance still in the veto signaling state + _assertVetoSignalingState(); + + //Committee votes for resealing WQ + for (uint256 i = 0; i < _resealCommitteeQuorum; i++) { + vm.prank(_resealCommitteeMembers[i]); + _resealCommittee.voteReseal(_sealables, true); + } + (uint256 support, uint256 quorum, bool isExecuted) = _resealCommittee.getResealState(_sealables); + assert(support == quorum); + assert(isExecuted == false); + + // WQ is paused for limited time before resealing + assert(_WITHDRAWAL_QUEUE.getResumeSinceTimestamp() < _PAUSE_INFINITELY); + + // Reseal execution + _resealCommittee.executeReseal(_sealables); + + // WQ is paused for infinite time after resealing + assert(_WITHDRAWAL_QUEUE.getResumeSinceTimestamp() == _PAUSE_INFINITELY); + assert(_WITHDRAWAL_QUEUE.isPaused()); + } + + function testFork_resealingWithActiveGovernance() external { + assertFalse(_WITHDRAWAL_QUEUE.isPaused()); + _assertNormalState(); + + // sealing committee seals Withdrawal Queue + vm.prank(_SEALING_COMMITTEE); + _gateSeal.seal(_sealables); + + // validate Withdrawal Queue was paused + assertTrue(_WITHDRAWAL_QUEUE.isPaused()); + + //Committee votes for resealing WQ + for (uint256 i = 0; i < _resealCommitteeQuorum; i++) { + vm.prank(_resealCommitteeMembers[i]); + _resealCommittee.voteReseal(_sealables, true); + } + (uint256 support, uint256 quorum, bool isExecuted) = _resealCommittee.getResealState(_sealables); + assert(support == quorum); + assert(isExecuted == false); + + // WQ is paused for limited time before resealing + assert(_WITHDRAWAL_QUEUE.getResumeSinceTimestamp() < _PAUSE_INFINITELY); + + // Reseal exection reverts + vm.expectRevert(); + _resealCommittee.executeReseal(_sealables); + } + + function testFork_resealingWithLockedGovernanceAndActiveWQ() external { + assertFalse(_WITHDRAWAL_QUEUE.isPaused()); + _assertNormalState(); + + _lockStETH(_VETOER, percents("10.0")); + _assertVetoSignalingState(); + + // validate Withdrawal Queue is Active + assertFalse(_WITHDRAWAL_QUEUE.isPaused()); + + // validate the dual governance still in the veto signaling state + _assertVetoSignalingState(); + + //Committee votes for resealing WQ + for (uint256 i = 0; i < _resealCommitteeQuorum; i++) { + vm.prank(_resealCommitteeMembers[i]); + _resealCommittee.voteReseal(_sealables, true); + } + (uint256 support, uint256 quorum, bool isExecuted) = _resealCommittee.getResealState(_sealables); + assert(support == quorum); + assert(isExecuted == false); + + // validate Withdrawal Queue is Active + assertFalse(_WITHDRAWAL_QUEUE.isPaused()); + + // Reseal exection reverts + vm.expectRevert(); + _resealCommittee.executeReseal(_sealables); + } +} diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol deleted file mode 100644 index 33cfd647..00000000 --- a/test/scenario/tiebraker.t.sol +++ /dev/null @@ -1,133 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import {Test, console} from "forge-std/Test.sol"; -import {DualGovernanceDeployScript, DualGovernance, EmergencyProtectedTimelock} from "script/Deploy.s.sol"; -import {TiebreakerCore} from "contracts/TiebreakerCore.sol"; -import {TiebreakerSubCommittee} from "contracts/TiebreakerSubCommittee.sol"; - -import {Utils} from "../utils/utils.sol"; -import {INodeOperatorsRegistry} from "../utils/interfaces.sol"; -import {NODE_OPERATORS_REGISTRY} from "../utils/mainnet-addresses.sol"; - -contract TiebreakerScenarioTest is Test { - Executor__mock private _emergencyExecutor; - - TiebreakerCore private _coreTiebreaker; - TiebreakerSubCommittee private _efTiebreaker; - TiebreakerSubCommittee private _nosTiebreaker; - - uint256 private _efMembersCount = 5; - uint256 private _efQuorum = 3; - - uint256 private _nosMembersCount = 10; - uint256 private _nosQuorum = 7; - - address[] private _efTiebreakerMembers; - address[] private _nosTiebreakerMembers; - address[] private _coreTiebreakerMembers; - - function setUp() external { - Utils.selectFork(); - - _emergencyExecutor = new Executor__mock(); - _coreTiebreaker = new TiebreakerCore(address(this), new address[](0), 1, address(_emergencyExecutor)); - - _emergencyExecutor.setCommittee(address(_coreTiebreaker)); - - // EF sub DAO - _efTiebreaker = new TiebreakerSubCommittee(address(this), new address[](0), 1, address(_coreTiebreaker)); - for (uint256 i = 0; i < _efMembersCount; i++) { - _efTiebreakerMembers.push(makeAddr(string(abi.encode(i + 65)))); - _efTiebreaker.addMember(_efTiebreakerMembers[i], i + 1 < _efQuorum ? i + 1 : _efQuorum); - } - _coreTiebreakerMembers.push(address(_efTiebreaker)); - _coreTiebreaker.addMember(address(_efTiebreaker), 1); - - // NOs sub DAO - _nosTiebreaker = new TiebreakerSubCommittee(address(this), new address[](0), 1, address(_coreTiebreaker)); - for (uint256 i = 0; i < _nosMembersCount; i++) { - _nosTiebreakerMembers.push(makeAddr(string(abi.encode(i + 65)))); - _nosTiebreaker.addMember(_nosTiebreakerMembers[i], i + 1 < _nosQuorum ? i + 1 : _nosQuorum); - } - _coreTiebreakerMembers.push(address(_nosTiebreaker)); - _coreTiebreaker.addMember(address(_nosTiebreaker), 2); - } - - function test_proposal_execution() external { - uint256 proposalIdToExecute = 1; - uint256 quorum; - uint256 support; - bool isExecuted; - - assert(_emergencyExecutor.proposals(proposalIdToExecute) == false); - - // EF sub DAO - for (uint256 i = 0; i < _efQuorum - 1; i++) { - vm.prank(_efTiebreakerMembers[i]); - _efTiebreaker.voteApproveProposal(proposalIdToExecute, true); - (support, quorum, isExecuted) = _efTiebreaker.getApproveProposalState(proposalIdToExecute); - assert(support < quorum); - assert(isExecuted == false); - } - - vm.prank(_efTiebreakerMembers[_efTiebreakerMembers.length - 1]); - _efTiebreaker.voteApproveProposal(proposalIdToExecute, true); - (support, quorum, isExecuted) = _efTiebreaker.getApproveProposalState(proposalIdToExecute); - assert(support == quorum); - assert(isExecuted == false); - - _efTiebreaker.executeApproveProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _coreTiebreaker.getApproveProposalState(proposalIdToExecute); - assert(support < quorum); - - // NOs sub DAO - - for (uint256 i = 0; i < _nosQuorum - 1; i++) { - vm.prank(_nosTiebreakerMembers[i]); - _nosTiebreaker.voteApproveProposal(proposalIdToExecute, true); - (support, quorum, isExecuted) = _nosTiebreaker.getApproveProposalState(proposalIdToExecute); - assert(support < quorum); - assert(isExecuted == false); - } - - vm.prank(_nosTiebreakerMembers[_nosTiebreakerMembers.length - 1]); - _nosTiebreaker.voteApproveProposal(proposalIdToExecute, true); - - (support, quorum, isExecuted) = _nosTiebreaker.getApproveProposalState(proposalIdToExecute); - assert(support == quorum); - assert(isExecuted == false); - - _nosTiebreaker.executeApproveProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _coreTiebreaker.getApproveProposalState(proposalIdToExecute); - assert(support == quorum); - - _coreTiebreaker.executeApproveProposal(proposalIdToExecute); - - assert(_emergencyExecutor.proposals(proposalIdToExecute) == true); - } -} - -contract Executor__mock { - error NotEmergencyCommittee(address sender); - error ProposalAlreadyExecuted(); - - mapping(uint256 => bool) public proposals; - address private committee; - - function setCommittee(address _committee) public { - committee = _committee; - } - - function tiebreakerApproveProposal(uint256 _proposalId) public { - if (proposals[_proposalId] == true) { - revert ProposalAlreadyExecuted(); - } - - if (msg.sender != committee) { - revert NotEmergencyCommittee(msg.sender); - } - - proposals[_proposalId] = true; - } -} diff --git a/test/utils/interfaces.sol b/test/utils/interfaces.sol index f7560fe6..5cdde994 100644 --- a/test/utils/interfaces.sol +++ b/test/utils/interfaces.sol @@ -105,6 +105,7 @@ interface IWithdrawalQueue is IERC721 { function grantRole(bytes32 role, address account) external; function hasRole(bytes32 role, address account) external view returns (bool); function isPaused() external view returns (bool); + function getResumeSinceTimestamp() external view returns (uint256); } interface INodeOperatorsRegistry { From d0ecb3d2ede8aee440303b95602b5975019b1db5 Mon Sep 17 00:00:00 2001 From: Alexandr Tarelkin Date: Wed, 24 Apr 2024 15:53:57 +0300 Subject: [PATCH 14/38] setResealCommittee spec --- docs/specification.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/specification.md b/docs/specification.md index a9944fbf..ae977736 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -930,6 +930,18 @@ This function extends pause of `sealables`. Can be called by committee address. - Contracts are paused until timestamp after current timestamp and not for infinite time. - The DAO governance is blocked by `DualGovernance`. +### Function ResealExecutor.setResealCommittee + +```solidity +function setResealCommittee(address newResealCommittee) +``` + +This function set `resealCommittee` address to `newResealCommittee`. Can be called by owner. + +#### Preconditions + +- Can be called by `OWNER`. + ## Contract: Configuration.sol `Configuration.sol` is the smart contract encompassing all the constants in the Dual Governance design & providing the interfaces for getting access to them. It implements interfaces `IAdminExecutorConfiguration`, `ITimelockConfiguration`, `IDualGovernanceConfiguration` covering for relevant "parameters domains". From 651c026807bd88a32b1b55f819710c9fde685ae7 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 25 Apr 2024 11:08:33 +0300 Subject: [PATCH 15/38] fix tiebreaker scenario --- test/scenario/tiebraker.t.sol | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol index 33cfd647..38aeaa7f 100644 --- a/test/scenario/tiebraker.t.sol +++ b/test/scenario/tiebraker.t.sol @@ -1,16 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {Test, console} from "forge-std/Test.sol"; -import {DualGovernanceDeployScript, DualGovernance, EmergencyProtectedTimelock} from "script/Deploy.s.sol"; +import {ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; + import {TiebreakerCore} from "contracts/TiebreakerCore.sol"; import {TiebreakerSubCommittee} from "contracts/TiebreakerSubCommittee.sol"; -import {Utils} from "../utils/utils.sol"; -import {INodeOperatorsRegistry} from "../utils/interfaces.sol"; -import {NODE_OPERATORS_REGISTRY} from "../utils/mainnet-addresses.sol"; - -contract TiebreakerScenarioTest is Test { +contract TiebreakerScenarioTest is ScenarioTestBlueprint { Executor__mock private _emergencyExecutor; TiebreakerCore private _coreTiebreaker; @@ -28,7 +24,9 @@ contract TiebreakerScenarioTest is Test { address[] private _coreTiebreakerMembers; function setUp() external { - Utils.selectFork(); + _selectFork(); + _deployTarget(); + _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); _emergencyExecutor = new Executor__mock(); _coreTiebreaker = new TiebreakerCore(address(this), new address[](0), 1, address(_emergencyExecutor)); From bf1c140d658c87124094104166ffeafb1a4e8f58 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 25 Apr 2024 11:10:30 +0300 Subject: [PATCH 16/38] typos --- docs/specification.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/specification.md b/docs/specification.md index dc43cc78..f1bd2214 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -205,7 +205,7 @@ The main entry point to the dual governance system. * Implements a state machine tracking the current [global governance state](#Governance-state) which, in turn, determines whether proposal submission and execution is currently allowed. * Deploys and tracks the [`Escrow`](#Contract-Escrowsol) contract instances. Tracks the current signalling escrow. -This contract is a singleton, meaning that any DG deployment includes exectly one instance of this contract. +This contract is a singleton, meaning that any DG deployment includes exactly one instance of this contract. ### Enum: DualGovernance.State @@ -577,7 +577,7 @@ To correctly calculate the rage quit support (see the `Escrow.getRageQuitSupport uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; _vetoersLockedAssets[msg.sender].withdrawalNFTShares += amountOfShares; -_totalWithdrawlNFTSharesLocked += amountOfShares; +_totalWithdrawalNFTSharesLocked += amountOfShares; ``` Finally, calls the `DualGovernance.activateNextState()` function. This action may transition the `Escrow` instance from the `SignallingEscrow` state into the `RageQuitEscrow` state. @@ -606,9 +606,9 @@ To correctly calculate the rage quit support (see the `Escrow.getRageQuitSupport uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; uint256 claimableAmount = _getClaimableEther(id); -_totalWithdrawlNFTSharesLocked -= amountOfShares; -_totalFinalizedWithdrawlNFTSharesLocked -= amountOfShares; -_totalFinalizedWithdrawlNFTAmountLocked -= claimableAmount; +_totalWithdrawalNFTSharesLocked -= amountOfShares; +_totalFinalizedWithdrawalNFTSharesLocked -= amountOfShares; +_totalFinalizedWithdrawalNFTAmountLocked -= claimableAmount; _vetoersLockedAssets[msg.sender].withdrawalNFTShares -= amountOfShares; _vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTShares -= amountOfShares; @@ -620,7 +620,7 @@ _vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTAmount -= claimableAmount ```solidity uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; -_totalWithdrawlNFTSharesLocked -= amountOfShares; +_totalWithdrawalNFTSharesLocked -= amountOfShares; _vetoersLockedAssets[msg.sender].withdrawalNFTShares -= amountOfShares; ``` @@ -653,8 +653,8 @@ For each Withdrawal NFT in the `unstETHIds`: uint256 claimableAmount = _getClaimableEther(id); uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; -_totalFinalizedWithdrawlNFTSharesLocked += amountOfShares; -_totalFinalizedWithdrawlNFTAmountLocked += claimableAmount; +_totalFinalizedWithdrawalNFTSharesLocked += amountOfShares; +_totalFinalizedWithdrawalNFTAmountLocked += claimableAmount; _vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTShares += amountOfShares; _vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTAmount += claimableAmount; @@ -748,7 +748,7 @@ function claimUnstETH(uint256[] unstETHIds, uint256[] hints) Allows users to claim the ETH associated with finalized Withdrawal NFTs with ids `unstETHIds` locked in the `Escrow` contract. Upon calling this function, the claimed ETH is transferred to the `Escrow` contract instance. -To safeguard the ETH associated with Withdrawal NFTs, this function should be invoked when the `Escrow` is in the `RageQuitEscrow` state and before the `RageQuitExtensionDelay` period ends. The ETH corresponding to unclaimed Withdrawal NFTs after this period ends would still be controlled by the code potentially afftected by pending and future DAO decisions. +To safeguard the ETH associated with Withdrawal NFTs, this function should be invoked when the `Escrow` is in the `RageQuitEscrow` state and before the `RageQuitExtensionDelay` period ends. The ETH corresponding to unclaimed Withdrawal NFTs after this period ends would still be controlled by the code potentially affected by pending and future DAO decisions. #### Preconditions @@ -772,7 +772,7 @@ Returns whether the rage quit process has been finalized. The rage quit process function withdrawStEthAsEth() ``` -Allows the caller (i.e. `msg.sender`) to withdraw all stETH they have previouusly locked into `Escrow` contract instance (while it was in the `SignallingEscrow` state) as plain ETH, given that the `RageQuit` process is completed and that the `RageQuitEthClaimTimelock` has elapsed. Upon execution, the function transfers ETH to the caller's account and marks the corresponding stETH as withdrawn for the caller. +Allows the caller (i.e. `msg.sender`) to withdraw all stETH they have previously locked into `Escrow` contract instance (while it was in the `SignallingEscrow` state) as plain ETH, given that the `RageQuit` process is completed and that the `RageQuitEthClaimTimelock` has elapsed. Upon execution, the function transfers ETH to the caller's account and marks the corresponding stETH as withdrawn for the caller. The amount of ETH sent to the caller is determined by the proportion of the user's stETH shares compared to the total amount of locked stETH and wstETH shares in the Escrow instance, calculated as follows: @@ -795,7 +795,7 @@ return _totalClaimedEthAmount * _vetoersLockedAssets[msg.sender].stETHShares function withdrawWstEthAsEth() external ``` -Allows the caller (i.e. `msg.sender`) to withdraw all wstETH they have previouusly locked into `Escrow` contract instance (while it was in the `SignallingEscrow` state) as plain ETH, given that the `RageQuit` process is completed and that the `RageQuitEthClaimTimelock` has elapsed. Upon execution, the function transfers ETH to the caller's account and marks the corresponding wstETH as withdrawn for the caller. +Allows the caller (i.e. `msg.sender`) to withdraw all wstETH they have previously locked into `Escrow` contract instance (while it was in the `SignallingEscrow` state) as plain ETH, given that the `RageQuit` process is completed and that the `RageQuitEthClaimTimelock` has elapsed. Upon execution, the function transfers ETH to the caller's account and marks the corresponding wstETH as withdrawn for the caller. The amount of ETH sent to the caller is determined by the proportion of the user's wstETH funds compared to the total amount of locked stETH and wstETH shares in the Escrow instance, calculated as follows: @@ -844,7 +844,7 @@ For a proposal to be executed, the following steps have to be performed in order The contract only allows proposal submission and scheduling by the `governance` address. Normally, this address points to the [`DualGovernance`](#Contract-DualGovernancesol) singleton instance. Proposal execution is permissionless, unless Emergency Mode is activated. -If the Emergency Committees are set up and active, the governance proposal gets a separate emergency protection timelock between submitting and scheduling. This additional timelock is implemented in the `EmergencyProtectedTimelock` contract to protect from zero-day vulnerability in the logic of `DualGovenance.sol` and other core DG contracts. If the Emergency Committees aren't set, the proposal flow is the same, but the timelock duration is zero. +If the Emergency Committees are set up and active, the governance proposal gets a separate emergency protection timelock between submitting and scheduling. This additional timelock is implemented in the `EmergencyProtectedTimelock` contract to protect from zero-day vulnerability in the logic of `DualGovernance.sol` and other core DG contracts. If the Emergency Committees aren't set, the proposal flow is the same, but the timelock duration is zero. Emergency Activation Committee, while active, can enable the Emergency Mode. This mode prohibits anyone but the Emergency Execution Committee from executing proposals. It also allows the Emergency Execution Committee to reset the governance, effectively disabling the Dual Governance subsystem. From 7a3037121eabc2dc017f61d58c34d34fda2d7261 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 25 Apr 2024 12:32:42 +0300 Subject: [PATCH 17/38] fix naming --- contracts/EmergencyExecutionCommittee.sol | 2 +- contracts/EmergencyProtectedTimelock.sol | 4 ++-- contracts/libraries/EmergencyProtection.sol | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/EmergencyExecutionCommittee.sol b/contracts/EmergencyExecutionCommittee.sol index 6cd9449d..c170d239 100644 --- a/contracts/EmergencyExecutionCommittee.sol +++ b/contracts/EmergencyExecutionCommittee.sol @@ -8,7 +8,7 @@ interface IEmergencyProtectedTimelock { function emergencyReset() external; } -contract EmergencyExecutiveCommittee is ExecutiveCommittee { +contract EmergencyExecutionCommittee is ExecutiveCommittee { address public immutable EMERGENCY_PROTECTED_TIMELOCK; constructor( diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 6cf3b073..e44cb2d2 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -70,7 +70,7 @@ contract EmergencyProtectedTimelock is ConfigurationProvider { function emergencyExecute(uint256 proposalId) external { _emergencyProtection.checkEmergencyModeActive(true); - _emergencyProtection.checkExecutiveCommittee(msg.sender); + _emergencyProtection.checkExecutionCommittee(msg.sender); _proposals.execute(proposalId, /* afterScheduleDelay */ 0); } @@ -85,7 +85,7 @@ contract EmergencyProtectedTimelock is ConfigurationProvider { function emergencyReset() external { _emergencyProtection.checkEmergencyModeActive(true); - _emergencyProtection.checkExecutiveCommittee(msg.sender); + _emergencyProtection.checkExecutionCommittee(msg.sender); _emergencyProtection.deactivate(); _setGovernance(CONFIG.EMERGENCY_GOVERNANCE()); _proposals.cancelAll(); diff --git a/contracts/libraries/EmergencyProtection.sol b/contracts/libraries/EmergencyProtection.sol index e2ab8c43..758525e1 100644 --- a/contracts/libraries/EmergencyProtection.sol +++ b/contracts/libraries/EmergencyProtection.sol @@ -23,7 +23,7 @@ library EmergencyProtection { event EmergencyModeDeactivated(); event EmergencyGovernanceReset(); event EmergencyActivationCommitteeSet(address indexed activationCommittee); - event EmergencyExecutiveCommitteeSet(address indexed executionCommittee); + event EmergencyExecutionCommitteeSet(address indexed executionCommittee); event EmergencyModeDurationSet(uint256 emergencyModeDuration); event EmergencyCommitteeProtectedTillSet(uint256 protectedTill); @@ -51,10 +51,10 @@ library EmergencyProtection { emit EmergencyActivationCommitteeSet(activationCommittee); } - address prevExecutiveCommittee = self.executionCommittee; - if (executionCommittee != prevExecutiveCommittee) { + address prevExecutionCommittee = self.executionCommittee; + if (executionCommittee != prevExecutionCommittee) { self.executionCommittee = executionCommittee; - emit EmergencyExecutiveCommitteeSet(executionCommittee); + emit EmergencyExecutionCommitteeSet(executionCommittee); } uint256 prevProtectedTill = self.protectedTill; @@ -117,7 +117,7 @@ library EmergencyProtection { } } - function checkExecutiveCommittee(State storage self, address account) internal view { + function checkExecutionCommittee(State storage self, address account) internal view { if (self.executionCommittee != account) { revert NotEmergencyEnactor(account); } From da9baf96e36726a408dd00cc41cbb88f647e842d Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 25 Apr 2024 12:34:13 +0300 Subject: [PATCH 18/38] remove nor --- test/utils/interfaces.sol | 22 ---------------------- test/utils/mainnet-addresses.sol | 1 - 2 files changed, 23 deletions(-) diff --git a/test/utils/interfaces.sol b/test/utils/interfaces.sol index f7560fe6..2d3c56f3 100644 --- a/test/utils/interfaces.sol +++ b/test/utils/interfaces.sol @@ -106,25 +106,3 @@ interface IWithdrawalQueue is IERC721 { function hasRole(bytes32 role, address account) external view returns (bool); function isPaused() external view returns (bool); } - -interface INodeOperatorsRegistry { - function getNodeOperator( - uint256 _id, - bool _fullInfo - ) - external - view - returns ( - bool active, - string memory name, - address rewardAddress, - uint64 stakingLimit, - uint64 stoppedValidators, - uint64 totalSigningKeys, - uint64 usedSigningKeys - ); - - function getNodeOperatorsCount() external view returns (uint256); - function getActiveNodeOperatorsCount() external view returns (uint256); - function getNodeOperatorIsActive(uint256 _nodeOperatorId) external view returns (bool); -} diff --git a/test/utils/mainnet-addresses.sol b/test/utils/mainnet-addresses.sol index dda7876e..3de321ee 100644 --- a/test/utils/mainnet-addresses.sol +++ b/test/utils/mainnet-addresses.sol @@ -9,4 +9,3 @@ address constant WST_ETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; address constant LDO_TOKEN = 0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32; address constant WITHDRAWAL_QUEUE = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; address constant BURNER = 0xD15a672319Cf0352560eE76d9e89eAB0889046D3; -address constant NODE_OPERATORS_REGISTRY = 0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5; From 3062c99f19017fdcfc7649d12a0d54bacf41029e Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 25 Apr 2024 17:24:20 +0300 Subject: [PATCH 19/38] move committees setup to blueprint --- test/scenario/agent-timelock.t.sol | 4 +- test/scenario/tiebraker.t.sol | 203 +++++++++++++++---------- test/utils/scenario-test-blueprint.sol | 64 +++++++- 3 files changed, 178 insertions(+), 93 deletions(-) diff --git a/test/scenario/agent-timelock.t.sol b/test/scenario/agent-timelock.t.sol index 1fed3c8a..587cd629 100644 --- a/test/scenario/agent-timelock.t.sol +++ b/test/scenario/agent-timelock.t.sol @@ -112,10 +112,10 @@ contract AgentTimelockTest is ScenarioTestBlueprint { vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2); // committee resets governance - vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); + vm.prank(address(_emergencyActivationCommittee)); _timelock.emergencyActivate(); - vm.prank(_EMERGENCY_EXECUTION_COMMITTEE); + vm.prank(address(_emergencyExecutionCommittee)); _timelock.emergencyReset(); // proposal is canceled now diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol index 38aeaa7f..f885d2ac 100644 --- a/test/scenario/tiebraker.t.sol +++ b/test/scenario/tiebraker.t.sol @@ -1,131 +1,166 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; +import { + ScenarioTestBlueprint, percents, ExecutorCall, ExecutorCallHelpers +} from "../utils/scenario-test-blueprint.sol"; import {TiebreakerCore} from "contracts/TiebreakerCore.sol"; import {TiebreakerSubCommittee} from "contracts/TiebreakerSubCommittee.sol"; +import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; -contract TiebreakerScenarioTest is ScenarioTestBlueprint { - Executor__mock private _emergencyExecutor; - - TiebreakerCore private _coreTiebreaker; - TiebreakerSubCommittee private _efTiebreaker; - TiebreakerSubCommittee private _nosTiebreaker; - - uint256 private _efMembersCount = 5; - uint256 private _efQuorum = 3; +import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; - uint256 private _nosMembersCount = 10; - uint256 private _nosQuorum = 7; - - address[] private _efTiebreakerMembers; - address[] private _nosTiebreakerMembers; - address[] private _coreTiebreakerMembers; +contract TiebreakerScenarioTest is ScenarioTestBlueprint { + address internal immutable _VETOER = makeAddr("VETOER"); + uint256 public constant PAUSE_INFINITELY = type(uint256).max; function setUp() external { _selectFork(); - _deployTarget(); _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); - - _emergencyExecutor = new Executor__mock(); - _coreTiebreaker = new TiebreakerCore(address(this), new address[](0), 1, address(_emergencyExecutor)); - - _emergencyExecutor.setCommittee(address(_coreTiebreaker)); - - // EF sub DAO - _efTiebreaker = new TiebreakerSubCommittee(address(this), new address[](0), 1, address(_coreTiebreaker)); - for (uint256 i = 0; i < _efMembersCount; i++) { - _efTiebreakerMembers.push(makeAddr(string(abi.encode(i + 65)))); - _efTiebreaker.addMember(_efTiebreakerMembers[i], i + 1 < _efQuorum ? i + 1 : _efQuorum); - } - _coreTiebreakerMembers.push(address(_efTiebreaker)); - _coreTiebreaker.addMember(address(_efTiebreaker), 1); - - // NOs sub DAO - _nosTiebreaker = new TiebreakerSubCommittee(address(this), new address[](0), 1, address(_coreTiebreaker)); - for (uint256 i = 0; i < _nosMembersCount; i++) { - _nosTiebreakerMembers.push(makeAddr(string(abi.encode(i + 65)))); - _nosTiebreaker.addMember(_nosTiebreakerMembers[i], i + 1 < _nosQuorum ? i + 1 : _nosQuorum); - } - _coreTiebreakerMembers.push(address(_nosTiebreaker)); - _coreTiebreaker.addMember(address(_nosTiebreaker), 2); } - function test_proposal_execution() external { - uint256 proposalIdToExecute = 1; + function test_proposal_approval() external { uint256 quorum; uint256 support; bool isExecuted; - assert(_emergencyExecutor.proposals(proposalIdToExecute) == false); - - // EF sub DAO - for (uint256 i = 0; i < _efQuorum - 1; i++) { - vm.prank(_efTiebreakerMembers[i]); - _efTiebreaker.voteApproveProposal(proposalIdToExecute, true); - (support, quorum, isExecuted) = _efTiebreaker.getApproveProposalState(proposalIdToExecute); + // Tiebreak activation + _assertNormalState(); + _lockStETH(_VETOER, percents("15.00")); + _wait(_config.SIGNALLING_MAX_DURATION()); + _activateNextState(); + _assertRageQuitState(); + _wait(_config.TIE_BREAK_ACTIVATION_TIMEOUT()); + _activateNextState(); + + ExecutorCall[] memory proposalCalls = ExecutorCallHelpers.create(address(0), new bytes(0)); + uint256 proposalIdToExecute = _submitProposal(_dualGovernance, "Proposal for execution", proposalCalls); + + // Tiebreaker subcommittee 0 + for (uint256 i = 0; i < _tiebreakerSubCommittees[0].quorum - 1; i++) { + vm.prank(_tiebreakerSubCommittees[0].members[i]); + _tiebreakerSubCommittees[0].committee.voteApproveProposal(proposalIdToExecute, true); + (support, quorum, isExecuted) = + _tiebreakerSubCommittees[0].committee.getApproveProposalState(proposalIdToExecute); assert(support < quorum); assert(isExecuted == false); } - vm.prank(_efTiebreakerMembers[_efTiebreakerMembers.length - 1]); - _efTiebreaker.voteApproveProposal(proposalIdToExecute, true); - (support, quorum, isExecuted) = _efTiebreaker.getApproveProposalState(proposalIdToExecute); + vm.prank(_tiebreakerSubCommittees[0].members[_tiebreakerSubCommittees[0].members.length - 1]); + _tiebreakerSubCommittees[0].committee.voteApproveProposal(proposalIdToExecute, true); + (support, quorum, isExecuted) = + _tiebreakerSubCommittees[0].committee.getApproveProposalState(proposalIdToExecute); assert(support == quorum); assert(isExecuted == false); - _efTiebreaker.executeApproveProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _coreTiebreaker.getApproveProposalState(proposalIdToExecute); + _tiebreakerSubCommittees[0].committee.executeApproveProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _tiebreakerCommittee.getApproveProposalState(proposalIdToExecute); assert(support < quorum); - // NOs sub DAO - - for (uint256 i = 0; i < _nosQuorum - 1; i++) { - vm.prank(_nosTiebreakerMembers[i]); - _nosTiebreaker.voteApproveProposal(proposalIdToExecute, true); - (support, quorum, isExecuted) = _nosTiebreaker.getApproveProposalState(proposalIdToExecute); + // Tiebreaker subcommittee 1 + for (uint256 i = 0; i < _tiebreakerSubCommittees[1].quorum - 1; i++) { + vm.prank(_tiebreakerSubCommittees[1].members[i]); + _tiebreakerSubCommittees[1].committee.voteApproveProposal(proposalIdToExecute, true); + (support, quorum, isExecuted) = + _tiebreakerSubCommittees[1].committee.getApproveProposalState(proposalIdToExecute); assert(support < quorum); assert(isExecuted == false); } - vm.prank(_nosTiebreakerMembers[_nosTiebreakerMembers.length - 1]); - _nosTiebreaker.voteApproveProposal(proposalIdToExecute, true); - - (support, quorum, isExecuted) = _nosTiebreaker.getApproveProposalState(proposalIdToExecute); + vm.prank(_tiebreakerSubCommittees[1].members[_tiebreakerSubCommittees[1].members.length - 1]); + _tiebreakerSubCommittees[1].committee.voteApproveProposal(proposalIdToExecute, true); + (support, quorum, isExecuted) = + _tiebreakerSubCommittees[1].committee.getApproveProposalState(proposalIdToExecute); assert(support == quorum); assert(isExecuted == false); - _nosTiebreaker.executeApproveProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _coreTiebreaker.getApproveProposalState(proposalIdToExecute); + // Approve proposal for scheduling + _tiebreakerSubCommittees[1].committee.executeApproveProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _tiebreakerCommittee.getApproveProposalState(proposalIdToExecute); assert(support == quorum); - _coreTiebreaker.executeApproveProposal(proposalIdToExecute); + _tiebreakerCommittee.executeApproveProposal(proposalIdToExecute); + + // Waiting for submit delay pass + _wait(_config.AFTER_SUBMIT_DELAY()); - assert(_emergencyExecutor.proposals(proposalIdToExecute) == true); + _dualGovernance.tiebreakerSchedule(proposalIdToExecute); } -} -contract Executor__mock { - error NotEmergencyCommittee(address sender); - error ProposalAlreadyExecuted(); + function test_resume_withdrawals() external { + uint256 quorum; + uint256 support; + bool isExecuted; - mapping(uint256 => bool) public proposals; - address private committee; + // Tiebreak activation + _assertNormalState(); + _lockStETH(_VETOER, percents("15.00")); + _wait(_config.SIGNALLING_MAX_DURATION()); + _activateNextState(); + _assertRageQuitState(); + vm.startPrank(DAO_AGENT); + _WITHDRAWAL_QUEUE.grantRole(_WITHDRAWAL_QUEUE.PAUSE_ROLE(), address(this)); + vm.stopPrank(); + _WITHDRAWAL_QUEUE.pauseFor(PAUSE_INFINITELY); + _activateNextState(); + + // Tiebreaker subcommittee 0 + for (uint256 i = 0; i < _tiebreakerSubCommittees[0].quorum - 1; i++) { + vm.prank(_tiebreakerSubCommittees[0].members[i]); + _tiebreakerSubCommittees[0].committee.voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); + (support, quorum, isExecuted) = + _tiebreakerSubCommittees[0].committee.getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); + assert(support < quorum); + assert(isExecuted == false); + } - function setCommittee(address _committee) public { - committee = _committee; - } + vm.prank(_tiebreakerSubCommittees[0].members[_tiebreakerSubCommittees[0].members.length - 1]); + _tiebreakerSubCommittees[0].committee.voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); + (support, quorum, isExecuted) = + _tiebreakerSubCommittees[0].committee.getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); + assert(support == quorum); + assert(isExecuted == false); - function tiebreakerApproveProposal(uint256 _proposalId) public { - if (proposals[_proposalId] == true) { - revert ProposalAlreadyExecuted(); - } + _tiebreakerSubCommittees[0].committee.executeApproveSealableResume(address(_WITHDRAWAL_QUEUE)); + (support, quorum, isExecuted) = _tiebreakerCommittee.getSealableResumeState( + address(_WITHDRAWAL_QUEUE), _tiebreakerCommittee.getSealableResumeNonce(address(_WITHDRAWAL_QUEUE)) + ); + assert(support < quorum); - if (msg.sender != committee) { - revert NotEmergencyCommittee(msg.sender); + // Tiebreaker subcommittee 1 + for (uint256 i = 0; i < _tiebreakerSubCommittees[1].quorum - 1; i++) { + vm.prank(_tiebreakerSubCommittees[1].members[i]); + _tiebreakerSubCommittees[1].committee.voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); + (support, quorum, isExecuted) = + _tiebreakerSubCommittees[1].committee.getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); + assert(support < quorum); + assert(isExecuted == false); } - proposals[_proposalId] = true; + vm.prank(_tiebreakerSubCommittees[1].members[_tiebreakerSubCommittees[1].members.length - 1]); + _tiebreakerSubCommittees[1].committee.voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); + (support, quorum, isExecuted) = + _tiebreakerSubCommittees[1].committee.getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); + assert(support == quorum); + assert(isExecuted == false); + + // Approve proposal for scheduling + _tiebreakerSubCommittees[1].committee.executeApproveSealableResume(address(_WITHDRAWAL_QUEUE)); + (support, quorum, isExecuted) = _tiebreakerCommittee.getSealableResumeState( + address(_WITHDRAWAL_QUEUE), _tiebreakerCommittee.getSealableResumeNonce(address(_WITHDRAWAL_QUEUE)) + ); + assert(support == quorum); + + uint256 lastProposalId = EmergencyProtectedTimelock(address(_dualGovernance.TIMELOCK())).getProposalsCount(); + _tiebreakerCommittee.executeSealableResume(address(_WITHDRAWAL_QUEUE)); + uint256 proposalIdToExecute = + EmergencyProtectedTimelock(address(_dualGovernance.TIMELOCK())).getProposalsCount(); + assert(lastProposalId + 1 == proposalIdToExecute); + + // Waiting for submit delay pass + _wait(_config.AFTER_SUBMIT_DELAY()); + + _dualGovernance.tiebreakerSchedule(proposalIdToExecute); } } diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index d9119dda..a81f5707 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -13,6 +13,11 @@ import {Escrow, VetoerState, LockedAssetsTotals} from "contracts/Escrow.sol"; import {IConfiguration, Configuration} from "contracts/Configuration.sol"; import {OwnableExecutor} from "contracts/OwnableExecutor.sol"; +import {EmergencyActivationCommittee} from "contracts/EmergencyActivationCommittee.sol"; +import {EmergencyExecutionCommittee} from "contracts/EmergencyExecutionCommittee.sol"; +import {TiebreakerCore} from "contracts/TiebreakerCore.sol"; +import {TiebreakerSubCommittee} from "contracts/TiebreakerSubCommittee.sol"; + import { ExecutorCall, EmergencyState, @@ -39,6 +44,12 @@ struct Balances { uint256 wstETHShares; } +struct TiebreakerSubCommitteeEntity { + TiebreakerSubCommittee committee; + address[] members; + uint256 quorum; +} + uint256 constant PERCENTS_PRECISION = 16; function countDigits(uint256 number) pure returns (uint256 digitsCount) { @@ -57,19 +68,20 @@ contract ScenarioTestBlueprint is Test { address internal immutable _ADMIN_PROPOSER = DAO_VOTING; uint256 internal immutable _EMERGENCY_MODE_DURATION = 180 days; uint256 internal immutable _EMERGENCY_PROTECTION_DURATION = 90 days; - address internal immutable _EMERGENCY_ACTIVATION_COMMITTEE = makeAddr("EMERGENCY_ACTIVATION_COMMITTEE"); - address internal immutable _EMERGENCY_EXECUTION_COMMITTEE = makeAddr("EMERGENCY_EXECUTION_COMMITTEE"); uint256 internal immutable _SEALING_DURATION = 14 days; uint256 internal immutable _SEALING_COMMITTEE_LIFETIME = 365 days; address internal immutable _SEALING_COMMITTEE = makeAddr("SEALING_COMMITTEE"); - address internal immutable _TIEBREAK_COMMITTEE = makeAddr("TIEBREAK_COMMITTEE"); - IStEth public immutable _ST_ETH = IStEth(ST_ETH); IWstETH public immutable _WST_ETH = IWstETH(WST_ETH); IWithdrawalQueue public immutable _WITHDRAWAL_QUEUE = IWithdrawalQueue(WITHDRAWAL_QUEUE); + EmergencyActivationCommittee internal _emergencyActivationCommittee; + EmergencyExecutionCommittee internal _emergencyExecutionCommittee; + TiebreakerCore internal _tiebreakerCommittee; + TiebreakerSubCommitteeEntity[] internal _tiebreakerSubCommittees; + TargetMock internal _target; IConfiguration internal _config; @@ -459,6 +471,9 @@ contract ScenarioTestBlueprint is Test { _deployEscrowMasterCopy(); _deployUngovernedTimelock(); _deployDualGovernance(); + _deployEmergencyActivationCommittee(); + _deployEmergencyExecutionCommittee(); + _deployTiebreaker(); _finishTimelockSetup(address(_dualGovernance), isEmergencyProtectionEnabled); } @@ -469,6 +484,9 @@ contract ScenarioTestBlueprint is Test { _deployEscrowMasterCopy(); _deployUngovernedTimelock(); _deploySingleGovernance(); + _deployEmergencyActivationCommittee(); + _deployEmergencyExecutionCommittee(); + _deployTiebreaker(); _finishTimelockSetup(address(_singleGovernance), isEmergencyProtectionEnabled); } @@ -507,6 +525,38 @@ contract ScenarioTestBlueprint is Test { _escrowMasterCopy = new Escrow(ST_ETH, WST_ETH, WITHDRAWAL_QUEUE, address(_config)); } + function _deployTiebreaker() internal { + uint256 subCommitteeMembersCount = 5; + uint256 subCommitteeQuorum = 5; + uint256 subCommitteesCount = 2; + + _tiebreakerCommittee = + new TiebreakerCore(address(_adminExecutor), new address[](0), 1, address(_dualGovernance)); + + for (uint256 i = 0; i < subCommitteesCount; ++i) { + address[] memory committeeMembers = new address[](subCommitteeMembersCount); + for (uint256 j = 0; j < subCommitteeMembersCount; j++) { + committeeMembers[j] = makeAddr(string(abi.encode(i + j * subCommitteeMembersCount + 65))); + } + _tiebreakerSubCommittees.push( + TiebreakerSubCommitteeEntity( + new TiebreakerSubCommittee( + address(_adminExecutor), committeeMembers, subCommitteeQuorum, address(_tiebreakerCommittee) + ), + committeeMembers, + subCommitteeQuorum + ) + ); + + vm.prank(address(_adminExecutor)); + _tiebreakerCommittee.addMember(address(_tiebreakerSubCommittees[i].committee), i + 1); + } + } + + function _deployEmergencyActivationCommittee() internal {} + + function _deployEmergencyExecutionCommittee() internal {} + function _finishTimelockSetup(address governance, bool isEmergencyProtectionEnabled) internal { if (isEmergencyProtectionEnabled) { _adminExecutor.execute( @@ -515,8 +565,8 @@ contract ScenarioTestBlueprint is Test { abi.encodeCall( _timelock.setEmergencyProtection, ( - _EMERGENCY_ACTIVATION_COMMITTEE, - _EMERGENCY_EXECUTION_COMMITTEE, + address(_emergencyActivationCommittee), + address(_emergencyExecutionCommittee), _EMERGENCY_PROTECTION_DURATION, _EMERGENCY_MODE_DURATION ) @@ -528,7 +578,7 @@ contract ScenarioTestBlueprint is Test { _adminExecutor.execute( address(_dualGovernance), 0, - abi.encodeCall(_dualGovernance.setTiebreakerProtection, (_TIEBREAK_COMMITTEE)) + abi.encodeCall(_dualGovernance.setTiebreakerProtection, (address(_tiebreakerCommittee))) ); } _adminExecutor.execute(address(_timelock), 0, abi.encodeCall(_timelock.setGovernance, (governance))); From a53d678ad76d43df1034e294c3c5f122ee197edc Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 25 Apr 2024 17:26:37 +0300 Subject: [PATCH 20/38] tiebreaker dualgovernance support --- contracts/DualGovernance.sol | 52 ++++++++++++++++++++++++--- contracts/TiebreakerCore.sol | 9 ++--- test/scenario/happy-path-plan-b.t.sol | 24 ++++++------- test/utils/interfaces.sol | 2 ++ 4 files changed, 64 insertions(+), 23 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index a195c24f..b74b6b15 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -14,14 +14,24 @@ contract DualGovernance is IGovernance, ConfigurationProvider { using DualGovernanceState for DualGovernanceState.Store; event TiebreakerSet(address tiebreakCommittee); + event ProposalApprovedForExecition(uint256 proposalId); event ProposalScheduled(uint256 proposalId); + event SealableResumeApproved(address sealable); error ProposalNotExecutable(uint256 proposalId); error NotTiebreaker(address account, address tiebreakCommittee); + error ProposalAlreadyApproved(uint256 proposalId); + error ProposalIsNotApprovedForExecution(uint256 proposalId); + error TiebreakerTimelockIsNotPassed(uint256 proposalId); + error SealableResumeAlreadyApproved(address sealable); + error TieBreakerAddressIsSame(); ITimelock public immutable TIMELOCK; address internal _tiebreaker; + uint256 internal _tiebreakerProposalApprovalTimelock; + mapping(uint256 proposalId => uint256) internal _tiebreakerProposalApprovalTimestamp; + mapping(address sealable => bool) internal _tiebreakerSealableResumeApprovals; Proposers.State internal _proposers; DualGovernanceState.Store internal _dgState; @@ -143,19 +153,51 @@ contract DualGovernance is IGovernance, ConfigurationProvider { // Tiebreaker Protection // --- - function tiebreakerSchedule(uint256 proposalId) external { + function tiebreakerApproveProposal(uint256 proposalId) external { + _checkTiebreakerCommittee(msg.sender); + _dgState.checkTiebreak(CONFIG); + if (_tiebreakerProposalApprovalTimestamp[proposalId] > 0) { + revert ProposalAlreadyApproved(proposalId); + } + + _tiebreakerProposalApprovalTimestamp[proposalId] = block.timestamp; + emit ProposalApprovedForExecition(proposalId); + } + + function tiebreakerApproveSealableResume(address sealable) external { _checkTiebreakerCommittee(msg.sender); _dgState.checkTiebreak(CONFIG); + Proposer memory proposer = _proposers.get(msg.sender); + ExecutorCall[] memory calls = new ExecutorCall[](1); + calls[0] = ExecutorCall(sealable, 0, abi.encodeWithSignature("resume()")); + uint256 proposalId = TIMELOCK.submit(proposer.executor, calls); + _tiebreakerProposalApprovalTimestamp[proposalId] = block.timestamp; + emit ProposalApprovedForExecition(proposalId); + emit SealableResumeApproved(sealable); + } + + function tiebreakerSchedule(uint256 proposalId) external { + _dgState.checkTiebreak(CONFIG); + if (_tiebreakerProposalApprovalTimestamp[proposalId] == 0) { + revert ProposalIsNotApprovedForExecution(proposalId); + } + if (_tiebreakerProposalApprovalTimestamp[proposalId] + _tiebreakerProposalApprovalTimelock > block.timestamp) { + revert TiebreakerTimelockIsNotPassed(proposalId); + } TIMELOCK.schedule(proposalId); } function setTiebreakerProtection(address newTiebreaker) external { _checkAdminExecutor(msg.sender); - address oldTiebreaker = _tiebreaker; - if (newTiebreaker != oldTiebreaker) { - _tiebreaker = newTiebreaker; - emit TiebreakerSet(newTiebreaker); + if (_tiebreaker == newTiebreaker) { + revert TieBreakerAddressIsSame(); + } + if (_tiebreaker != address(0)) { + _proposers.unregister(CONFIG, _tiebreaker); } + _tiebreaker = newTiebreaker; + _proposers.register(newTiebreaker, CONFIG.ADMIN_EXECUTOR()); // TODO: check what executor should be. Reseal executor? + emit TiebreakerSet(newTiebreaker); } // --- diff --git a/contracts/TiebreakerCore.sol b/contracts/TiebreakerCore.sol index 7ed181fb..8896f1c1 100644 --- a/contracts/TiebreakerCore.sol +++ b/contracts/TiebreakerCore.sol @@ -57,11 +57,8 @@ contract TiebreakerCore is ExecutiveCommittee { return getActionState(_buildSealableResumeAction(sealable, nonce)); } - function executeSealableResume(address sealable, uint256 nonce) external { - if (nonce != _sealableResumeNonces[sealable]) { - revert ResumeSealableNonceMismatch(); - } - _execute(_buildSealableResumeAction(sealable, nonce)); + function executeSealableResume(address sealable) external { + _execute(_buildSealableResumeAction(sealable, getSealableResumeNonce(sealable))); _sealableResumeNonces[sealable]++; } @@ -74,7 +71,7 @@ contract TiebreakerCore is ExecutiveCommittee { function _buildSealableResumeAction(address sealable, uint256 nonce) internal view returns (Action memory) { return Action( DUAL_GOVERNANCE, - abi.encodeWithSignature("tiebreakerApproveSealableResume(uint256)", sealable), + abi.encodeWithSignature("tiebreakerApproveSealableResume(address)", sealable), abi.encode(nonce) ); } diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index 782d3767..addf270e 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -72,7 +72,7 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertCanSchedule(_singleGovernance, maliciousProposalId, false); // emergency committee activates emergency mode - vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); + vm.prank(address(_emergencyActivationCommittee)); _timelock.emergencyActivate(); // emergency mode was successfully activated @@ -124,8 +124,8 @@ contract PlanBSetup is ScenarioTestBlueprint { abi.encodeCall( _timelock.setEmergencyProtection, ( - _EMERGENCY_ACTIVATION_COMMITTEE, - _EMERGENCY_EXECUTION_COMMITTEE, + address(_emergencyActivationCommittee), + address(_emergencyExecutionCommittee), _EMERGENCY_PROTECTION_DURATION, 30 days ) @@ -147,7 +147,7 @@ contract PlanBSetup is ScenarioTestBlueprint { _waitAfterScheduleDelayPassed(); // now emergency committee may execute the proposal - vm.prank(_EMERGENCY_EXECUTION_COMMITTEE); + vm.prank(address(_emergencyExecutionCommittee)); _timelock.emergencyExecute(dualGovernanceLunchProposalId); assertEq(_timelock.getGovernance(), address(_dualGovernance)); @@ -202,8 +202,8 @@ contract PlanBSetup is ScenarioTestBlueprint { abi.encodeCall( _timelock.setEmergencyProtection, ( - _EMERGENCY_ACTIVATION_COMMITTEE, - _EMERGENCY_EXECUTION_COMMITTEE, + address(_emergencyActivationCommittee), + address(_emergencyExecutionCommittee), _EMERGENCY_PROTECTION_DURATION, 30 days ) @@ -232,8 +232,8 @@ contract PlanBSetup is ScenarioTestBlueprint { assertTrue(_timelock.isEmergencyProtectionEnabled()); emergencyState = _timelock.getEmergencyState(); - assertEq(emergencyState.activationCommittee, _EMERGENCY_ACTIVATION_COMMITTEE); - assertEq(emergencyState.executionCommittee, _EMERGENCY_EXECUTION_COMMITTEE); + assertEq(emergencyState.activationCommittee, address(_emergencyActivationCommittee)); + assertEq(emergencyState.executionCommittee, address(_emergencyExecutionCommittee)); assertFalse(emergencyState.isEmergencyModeActivated); assertEq(emergencyState.emergencyModeDuration, 30 days); assertEq(emergencyState.emergencyModeEndsAfter, 0); @@ -289,7 +289,7 @@ contract PlanBSetup is ScenarioTestBlueprint { { vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2); - vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); + vm.prank(address(_emergencyActivationCommittee)); _timelock.emergencyActivate(); emergencyState = _timelock.getEmergencyState(); @@ -387,7 +387,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // emergency committee activates emergency mode EmergencyState memory emergencyState; { - vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); + vm.prank(address(_emergencyActivationCommittee)); _timelock.emergencyActivate(); emergencyState = _timelock.getEmergencyState(); @@ -400,7 +400,7 @@ contract PlanBSetup is ScenarioTestBlueprint { vm.warp(block.timestamp + _EMERGENCY_MODE_DURATION / 2); assertTrue(emergencyState.emergencyModeEndsAfter > block.timestamp); - vm.prank(_EMERGENCY_EXECUTION_COMMITTEE); + vm.prank(address(_emergencyExecutionCommittee)); _timelock.emergencyReset(); assertEq(_timelock.getGovernance(), _config.EMERGENCY_GOVERNANCE()); @@ -429,7 +429,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // attempt to activate emergency protection fails { vm.expectRevert(EmergencyProtection.EmergencyCommitteeExpired.selector); - vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); + vm.prank(address(_emergencyActivationCommittee)); _timelock.emergencyActivate(); } } diff --git a/test/utils/interfaces.sol b/test/utils/interfaces.sol index 2d3c56f3..52e11563 100644 --- a/test/utils/interfaces.sol +++ b/test/utils/interfaces.sol @@ -105,4 +105,6 @@ interface IWithdrawalQueue is IERC721 { function grantRole(bytes32 role, address account) external; function hasRole(bytes32 role, address account) external view returns (bool); function isPaused() external view returns (bool); + function resume() external; + function pauseFor(uint256 duration) external; } From cbff9c5e48b59261ef3b40b9fd975dd4f48ff051 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 25 Apr 2024 20:34:35 +0300 Subject: [PATCH 21/38] replace committee struct --- contracts/ExecutiveCommittee.sol | 4 ++ test/scenario/tiebraker.t.sol | 76 ++++++++++++++------------ test/utils/scenario-test-blueprint.sol | 18 ++---- 3 files changed, 48 insertions(+), 50 deletions(-) diff --git a/contracts/ExecutiveCommittee.sol b/contracts/ExecutiveCommittee.sol index 7da43c7f..85bff120 100644 --- a/contracts/ExecutiveCommittee.sol +++ b/contracts/ExecutiveCommittee.sol @@ -144,6 +144,10 @@ abstract contract ExecutiveCommittee { emit QuorumSet(newQuorum); } + function getMembers() public view returns (address[] memory) { + return membersList; + } + function _addMember(address newMember) internal { membersList.push(newMember); members[newMember] = true; diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol index f885d2ac..f07fab67 100644 --- a/test/scenario/tiebraker.t.sol +++ b/test/scenario/tiebraker.t.sol @@ -25,6 +25,8 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { uint256 support; bool isExecuted; + address[] memory members; + // Tiebreak activation _assertNormalState(); _lockStETH(_VETOER, percents("15.00")); @@ -38,45 +40,43 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { uint256 proposalIdToExecute = _submitProposal(_dualGovernance, "Proposal for execution", proposalCalls); // Tiebreaker subcommittee 0 - for (uint256 i = 0; i < _tiebreakerSubCommittees[0].quorum - 1; i++) { - vm.prank(_tiebreakerSubCommittees[0].members[i]); - _tiebreakerSubCommittees[0].committee.voteApproveProposal(proposalIdToExecute, true); - (support, quorum, isExecuted) = - _tiebreakerSubCommittees[0].committee.getApproveProposalState(proposalIdToExecute); + members = _tiebreakerSubCommittees[0].getMembers(); + for (uint256 i = 0; i < _tiebreakerSubCommittees[0].quorum() - 1; i++) { + vm.prank(members[i]); + _tiebreakerSubCommittees[0].voteApproveProposal(proposalIdToExecute, true); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getApproveProposalState(proposalIdToExecute); assert(support < quorum); assert(isExecuted == false); } - vm.prank(_tiebreakerSubCommittees[0].members[_tiebreakerSubCommittees[0].members.length - 1]); - _tiebreakerSubCommittees[0].committee.voteApproveProposal(proposalIdToExecute, true); - (support, quorum, isExecuted) = - _tiebreakerSubCommittees[0].committee.getApproveProposalState(proposalIdToExecute); + vm.prank(members[members.length - 1]); + _tiebreakerSubCommittees[0].voteApproveProposal(proposalIdToExecute, true); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getApproveProposalState(proposalIdToExecute); assert(support == quorum); assert(isExecuted == false); - _tiebreakerSubCommittees[0].committee.executeApproveProposal(proposalIdToExecute); + _tiebreakerSubCommittees[0].executeApproveProposal(proposalIdToExecute); (support, quorum, isExecuted) = _tiebreakerCommittee.getApproveProposalState(proposalIdToExecute); assert(support < quorum); // Tiebreaker subcommittee 1 - for (uint256 i = 0; i < _tiebreakerSubCommittees[1].quorum - 1; i++) { - vm.prank(_tiebreakerSubCommittees[1].members[i]); - _tiebreakerSubCommittees[1].committee.voteApproveProposal(proposalIdToExecute, true); - (support, quorum, isExecuted) = - _tiebreakerSubCommittees[1].committee.getApproveProposalState(proposalIdToExecute); + members = _tiebreakerSubCommittees[1].getMembers(); + for (uint256 i = 0; i < _tiebreakerSubCommittees[1].quorum() - 1; i++) { + vm.prank(members[i]); + _tiebreakerSubCommittees[1].voteApproveProposal(proposalIdToExecute, true); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getApproveProposalState(proposalIdToExecute); assert(support < quorum); assert(isExecuted == false); } - vm.prank(_tiebreakerSubCommittees[1].members[_tiebreakerSubCommittees[1].members.length - 1]); - _tiebreakerSubCommittees[1].committee.voteApproveProposal(proposalIdToExecute, true); - (support, quorum, isExecuted) = - _tiebreakerSubCommittees[1].committee.getApproveProposalState(proposalIdToExecute); + vm.prank(members[members.length - 1]); + _tiebreakerSubCommittees[1].voteApproveProposal(proposalIdToExecute, true); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getApproveProposalState(proposalIdToExecute); assert(support == quorum); assert(isExecuted == false); // Approve proposal for scheduling - _tiebreakerSubCommittees[1].committee.executeApproveProposal(proposalIdToExecute); + _tiebreakerSubCommittees[1].executeApproveProposal(proposalIdToExecute); (support, quorum, isExecuted) = _tiebreakerCommittee.getApproveProposalState(proposalIdToExecute); assert(support == quorum); @@ -93,6 +93,8 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { uint256 support; bool isExecuted; + address[] memory members; + // Tiebreak activation _assertNormalState(); _lockStETH(_VETOER, percents("15.00")); @@ -106,47 +108,49 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { _activateNextState(); // Tiebreaker subcommittee 0 - for (uint256 i = 0; i < _tiebreakerSubCommittees[0].quorum - 1; i++) { - vm.prank(_tiebreakerSubCommittees[0].members[i]); - _tiebreakerSubCommittees[0].committee.voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); + members = _tiebreakerSubCommittees[0].getMembers(); + for (uint256 i = 0; i < _tiebreakerSubCommittees[0].quorum() - 1; i++) { + vm.prank(members[i]); + _tiebreakerSubCommittees[0].voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); (support, quorum, isExecuted) = - _tiebreakerSubCommittees[0].committee.getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[0].getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); assert(support < quorum); assert(isExecuted == false); } - vm.prank(_tiebreakerSubCommittees[0].members[_tiebreakerSubCommittees[0].members.length - 1]); - _tiebreakerSubCommittees[0].committee.voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); + vm.prank(members[members.length - 1]); + _tiebreakerSubCommittees[0].voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); (support, quorum, isExecuted) = - _tiebreakerSubCommittees[0].committee.getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[0].getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); assert(support == quorum); assert(isExecuted == false); - _tiebreakerSubCommittees[0].committee.executeApproveSealableResume(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[0].executeApproveSealableResume(address(_WITHDRAWAL_QUEUE)); (support, quorum, isExecuted) = _tiebreakerCommittee.getSealableResumeState( address(_WITHDRAWAL_QUEUE), _tiebreakerCommittee.getSealableResumeNonce(address(_WITHDRAWAL_QUEUE)) ); assert(support < quorum); // Tiebreaker subcommittee 1 - for (uint256 i = 0; i < _tiebreakerSubCommittees[1].quorum - 1; i++) { - vm.prank(_tiebreakerSubCommittees[1].members[i]); - _tiebreakerSubCommittees[1].committee.voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); + members = _tiebreakerSubCommittees[1].getMembers(); + for (uint256 i = 0; i < _tiebreakerSubCommittees[1].quorum() - 1; i++) { + vm.prank(members[i]); + _tiebreakerSubCommittees[1].voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); (support, quorum, isExecuted) = - _tiebreakerSubCommittees[1].committee.getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[1].getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); assert(support < quorum); assert(isExecuted == false); } - vm.prank(_tiebreakerSubCommittees[1].members[_tiebreakerSubCommittees[1].members.length - 1]); - _tiebreakerSubCommittees[1].committee.voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); + vm.prank(members[members.length - 1]); + _tiebreakerSubCommittees[1].voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); (support, quorum, isExecuted) = - _tiebreakerSubCommittees[1].committee.getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[1].getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); assert(support == quorum); assert(isExecuted == false); // Approve proposal for scheduling - _tiebreakerSubCommittees[1].committee.executeApproveSealableResume(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[1].executeApproveSealableResume(address(_WITHDRAWAL_QUEUE)); (support, quorum, isExecuted) = _tiebreakerCommittee.getSealableResumeState( address(_WITHDRAWAL_QUEUE), _tiebreakerCommittee.getSealableResumeNonce(address(_WITHDRAWAL_QUEUE)) ); diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index a81f5707..e0a2cc9b 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -44,12 +44,6 @@ struct Balances { uint256 wstETHShares; } -struct TiebreakerSubCommitteeEntity { - TiebreakerSubCommittee committee; - address[] members; - uint256 quorum; -} - uint256 constant PERCENTS_PRECISION = 16; function countDigits(uint256 number) pure returns (uint256 digitsCount) { @@ -80,7 +74,7 @@ contract ScenarioTestBlueprint is Test { EmergencyActivationCommittee internal _emergencyActivationCommittee; EmergencyExecutionCommittee internal _emergencyExecutionCommittee; TiebreakerCore internal _tiebreakerCommittee; - TiebreakerSubCommitteeEntity[] internal _tiebreakerSubCommittees; + TiebreakerSubCommittee[] internal _tiebreakerSubCommittees; TargetMock internal _target; @@ -539,17 +533,13 @@ contract ScenarioTestBlueprint is Test { committeeMembers[j] = makeAddr(string(abi.encode(i + j * subCommitteeMembersCount + 65))); } _tiebreakerSubCommittees.push( - TiebreakerSubCommitteeEntity( - new TiebreakerSubCommittee( - address(_adminExecutor), committeeMembers, subCommitteeQuorum, address(_tiebreakerCommittee) - ), - committeeMembers, - subCommitteeQuorum + new TiebreakerSubCommittee( + address(_adminExecutor), committeeMembers, subCommitteeQuorum, address(_tiebreakerCommittee) ) ); vm.prank(address(_adminExecutor)); - _tiebreakerCommittee.addMember(address(_tiebreakerSubCommittees[i].committee), i + 1); + _tiebreakerCommittee.addMember(address(_tiebreakerSubCommittees[i]), i + 1); } } From 27344bd01ac0088162ec5e2cb06abe0f42abde46 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 25 Apr 2024 22:11:38 +0300 Subject: [PATCH 22/38] emergency committee tests --- contracts/DualGovernance.sol | 2 -- test/scenario/agent-timelock.t.sol | 7 ++-- test/scenario/happy-path-plan-b.t.sol | 15 +++----- test/scenario/tiebraker.t.sol | 2 -- test/utils/scenario-test-blueprint.sol | 49 ++++++++++++++++++++++++-- 5 files changed, 54 insertions(+), 21 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index b74b6b15..63bf0d56 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -6,7 +6,6 @@ import {ITimelock, IGovernance} from "./interfaces/ITimelock.sol"; import {ConfigurationProvider} from "./ConfigurationProvider.sol"; import {Proposers, Proposer} from "./libraries/Proposers.sol"; import {ExecutorCall} from "./libraries/Proposals.sol"; -import {EmergencyProtection} from "./libraries/EmergencyProtection.sol"; import {DualGovernanceState, State as GovernanceState} from "./libraries/DualGovernanceState.sol"; contract DualGovernance is IGovernance, ConfigurationProvider { @@ -35,7 +34,6 @@ contract DualGovernance is IGovernance, ConfigurationProvider { Proposers.State internal _proposers; DualGovernanceState.Store internal _dgState; - EmergencyProtection.State internal _emergencyProtection; mapping(uint256 proposalId => uint256 executableAfter) internal _scheduledProposals; constructor( diff --git a/test/scenario/agent-timelock.t.sol b/test/scenario/agent-timelock.t.sol index 587cd629..60383099 100644 --- a/test/scenario/agent-timelock.t.sol +++ b/test/scenario/agent-timelock.t.sol @@ -112,11 +112,8 @@ contract AgentTimelockTest is ScenarioTestBlueprint { vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2); // committee resets governance - vm.prank(address(_emergencyActivationCommittee)); - _timelock.emergencyActivate(); - - vm.prank(address(_emergencyExecutionCommittee)); - _timelock.emergencyReset(); + _executeEmergencyActivate(); + _executeEmergencyReset(); // proposal is canceled now vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2 + 1); diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index addf270e..14d33b29 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -72,8 +72,7 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertCanSchedule(_singleGovernance, maliciousProposalId, false); // emergency committee activates emergency mode - vm.prank(address(_emergencyActivationCommittee)); - _timelock.emergencyActivate(); + _executeEmergencyActivate(); // emergency mode was successfully activated uint256 expectedEmergencyModeEndTimestamp = block.timestamp + _EMERGENCY_MODE_DURATION; @@ -147,8 +146,7 @@ contract PlanBSetup is ScenarioTestBlueprint { _waitAfterScheduleDelayPassed(); // now emergency committee may execute the proposal - vm.prank(address(_emergencyExecutionCommittee)); - _timelock.emergencyExecute(dualGovernanceLunchProposalId); + _executeEmergencyExecute(dualGovernanceLunchProposalId); assertEq(_timelock.getGovernance(), address(_dualGovernance)); // TODO: check emergency protection also was applied @@ -289,8 +287,7 @@ contract PlanBSetup is ScenarioTestBlueprint { { vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2); - vm.prank(address(_emergencyActivationCommittee)); - _timelock.emergencyActivate(); + _executeEmergencyActivate(); emergencyState = _timelock.getEmergencyState(); assertTrue(emergencyState.isEmergencyModeActivated); @@ -387,8 +384,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // emergency committee activates emergency mode EmergencyState memory emergencyState; { - vm.prank(address(_emergencyActivationCommittee)); - _timelock.emergencyActivate(); + _executeEmergencyActivate(); emergencyState = _timelock.getEmergencyState(); assertTrue(emergencyState.isEmergencyModeActivated); @@ -400,8 +396,7 @@ contract PlanBSetup is ScenarioTestBlueprint { vm.warp(block.timestamp + _EMERGENCY_MODE_DURATION / 2); assertTrue(emergencyState.emergencyModeEndsAfter > block.timestamp); - vm.prank(address(_emergencyExecutionCommittee)); - _timelock.emergencyReset(); + _executeEmergencyReset(); assertEq(_timelock.getGovernance(), _config.EMERGENCY_GOVERNANCE()); diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol index f07fab67..0e89e033 100644 --- a/test/scenario/tiebraker.t.sol +++ b/test/scenario/tiebraker.t.sol @@ -5,8 +5,6 @@ import { ScenarioTestBlueprint, percents, ExecutorCall, ExecutorCallHelpers } from "../utils/scenario-test-blueprint.sol"; -import {TiebreakerCore} from "contracts/TiebreakerCore.sol"; -import {TiebreakerSubCommittee} from "contracts/TiebreakerSubCommittee.sol"; import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index e0a2cc9b..886fc2e9 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -543,9 +543,27 @@ contract ScenarioTestBlueprint is Test { } } - function _deployEmergencyActivationCommittee() internal {} + function _deployEmergencyActivationCommittee() internal { + uint256 quorum = 3; + uint256 membersCount = 5; + address[] memory committeeMembers = new address[](membersCount); + for (uint256 i = 0; i < membersCount; ++i) { + committeeMembers[i] = makeAddr(string(abi.encode(0xFE + i * membersCount + 65))); + } + _emergencyActivationCommittee = + new EmergencyActivationCommittee(address(_adminExecutor), committeeMembers, quorum, address(_timelock)); + } - function _deployEmergencyExecutionCommittee() internal {} + function _deployEmergencyExecutionCommittee() internal { + uint256 quorum = 3; + uint256 membersCount = 5; + address[] memory committeeMembers = new address[](membersCount); + for (uint256 i = 0; i < membersCount; ++i) { + committeeMembers[i] = makeAddr(string(abi.encode(0xFD + i * membersCount + 65))); + } + _emergencyExecutionCommittee = + new EmergencyExecutionCommittee(address(_adminExecutor), committeeMembers, quorum, address(_timelock)); + } function _finishTimelockSetup(address governance, bool isEmergencyProtectionEnabled) internal { if (isEmergencyProtectionEnabled) { @@ -591,6 +609,33 @@ contract ScenarioTestBlueprint is Test { _wait(_config.AFTER_SCHEDULE_DELAY() + 1); } + function _executeEmergencyActivate() internal { + address[] memory members = _emergencyActivationCommittee.getMembers(); + for (uint256 i = 0; i < _emergencyActivationCommittee.quorum(); ++i) { + vm.prank(members[i]); + _emergencyActivationCommittee.approveEmergencyActivate(); + } + _emergencyActivationCommittee.executeEmergencyActivate(); + } + + function _executeEmergencyExecute(uint256 proposalId) internal { + address[] memory members = _emergencyExecutionCommittee.getMembers(); + for (uint256 i = 0; i < _emergencyExecutionCommittee.quorum(); ++i) { + vm.prank(members[i]); + _emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); + } + _emergencyExecutionCommittee.executeEmergencyExecute(proposalId); + } + + function _executeEmergencyReset() internal { + address[] memory members = _emergencyExecutionCommittee.getMembers(); + for (uint256 i = 0; i < _emergencyExecutionCommittee.quorum(); ++i) { + vm.prank(members[i]); + _emergencyExecutionCommittee.approveEmergencyReset(); + } + _emergencyExecutionCommittee.executeEmergencyReset(); + } + struct Duration { uint256 _days; uint256 _hours; From 40373a5a00560102b78f98da5b1318b667b7498b Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 25 Apr 2024 22:42:50 +0300 Subject: [PATCH 23/38] spec --- docs/specification.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/specification.md b/docs/specification.md index f1bd2214..2b63dd26 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -42,7 +42,10 @@ The system is composed of the following main contracts: * [`EmergencyProtectedTimelock.sol`](#Contract-EmergencyProtectedTimelocksol) is a singleton that stores submitted proposals and provides an interface for their execution. In addition, it implements an optional temporary protection from a zero-day vulnerability in the dual governance contracts following the initial deployment or upgrade of the system. The protection is implemented as a timelock on proposal execution combined with two emergency committees that have the right to cooperate and disable the dual governance. * [`Executor.sol`](#Contract-Executorsol) contract instances make calls resulting from governance proposals' execution. Every protocol permission or role protected by the DG, as well as the permission to manage this role/permission, should be assigned exclusively to one of the instances of this contract (in contrast with being assigned directly to a DAO voting system). * [`Escrow.sol`](#Contract-Escrowsol) is a contract that can hold stETH, wstETH, withdrawal NFTs, and plain ETH. It can exist in two states, each serving a different purpose: either an oracle for users' opposition to DAO proposals or an immutable and ungoverned accumulator for the ETH withdrawn as a result of the [rage quit](#Rage-quit). - +* [`TiebreakerCore.sol`](#contract-tiebreakercoresol) allows to approve proposals for execution and release protocol withdrawals in case of DAO execution ability is locked by `DualGovernance`. Consists of set of `TiebreakerSubCommittee` appointed by the DAO. +* [`TiebreakerSubCommittee.sol`](#contract-tiebreakersubcommitteesol) provides ability to participate in `TiebreakerCore` for external actors. +* [`EmergencyActivationCommittee`](#contract-emergencyactivationcommitteesol) contract that can activate the Emergency Mode, while only `EmergencyExecutionCommittee` can perform proposal execution. Requires to get quorum from committee members. +* [`EmergencyExecutionCommittee`](#contract-emergencyexecutioncommitteesol) contract provides ability to execute proposals in case of the Emergency Mode or renounce renounce further execution rights, by getting quorum of committee members. ## Proposal flow @@ -973,6 +976,10 @@ The contract has the interface for managing the configuration related to emergen `Configuration.sol` is the smart contract encompassing all the constants in the Dual Governance design & providing the interfaces for getting access to them. It implements interfaces `IAdminExecutorConfiguration`, `ITimelockConfiguration`, `IDualGovernanceConfiguration` covering for relevant "parameters domains". +## Contract: TiebreakerCore.sol +## Contract: TiebreakerSubCommittee.sol +## Contract: EmergencyActivationCommittee.sol +## Contract: EmergencyExecutionCommittee.sol ## Upgrade flow description From 9110ea6408993df739f70987cc6741b1aa60776b Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Thu, 13 Jun 2024 16:36:51 +0300 Subject: [PATCH 24/38] fix: review fixes --- contracts/DualGovernance.sol | 9 +++-- contracts/ExecutiveCommittee.sol | 64 ++++++++++++++++---------------- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 63bf0d56..f8eaadf4 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.23; import {ITimelock, IGovernance} from "./interfaces/ITimelock.sol"; +import {ISealable} from "./interfaces/ISealable.sol"; import {ConfigurationProvider} from "./ConfigurationProvider.sol"; import {Proposers, Proposer} from "./libraries/Proposers.sol"; @@ -13,7 +14,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { using DualGovernanceState for DualGovernanceState.Store; event TiebreakerSet(address tiebreakCommittee); - event ProposalApprovedForExecition(uint256 proposalId); + event ProposalApprovedForExecution(uint256 proposalId); event ProposalScheduled(uint256 proposalId); event SealableResumeApproved(address sealable); @@ -159,7 +160,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { } _tiebreakerProposalApprovalTimestamp[proposalId] = block.timestamp; - emit ProposalApprovedForExecition(proposalId); + emit ProposalApprovedForExecution(proposalId); } function tiebreakerApproveSealableResume(address sealable) external { @@ -167,10 +168,10 @@ contract DualGovernance is IGovernance, ConfigurationProvider { _dgState.checkTiebreak(CONFIG); Proposer memory proposer = _proposers.get(msg.sender); ExecutorCall[] memory calls = new ExecutorCall[](1); - calls[0] = ExecutorCall(sealable, 0, abi.encodeWithSignature("resume()")); + calls[0] = ExecutorCall(sealable, 0, abi.encodeWithSelector(ISealable.resume.selector)); uint256 proposalId = TIMELOCK.submit(proposer.executor, calls); _tiebreakerProposalApprovalTimestamp[proposalId] = block.timestamp; - emit ProposalApprovedForExecition(proposalId); + emit ProposalApprovedForExecution(proposalId); emit SealableResumeApproved(sealable); } diff --git a/contracts/ExecutiveCommittee.sol b/contracts/ExecutiveCommittee.sol index 85bff120..1ab992b9 100644 --- a/contracts/ExecutiveCommittee.sol +++ b/contracts/ExecutiveCommittee.sol @@ -2,8 +2,11 @@ pragma solidity 0.8.23; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; abstract contract ExecutiveCommittee { + using EnumerableSet for EnumerableSet.AddressSet; + event MemberAdded(address indexed member); event MemberRemoved(address indexed member); event QuorumSet(uint256 quorum); @@ -19,11 +22,12 @@ abstract contract ExecutiveCommittee { error QuorumIsNotReached(); error InvalidQuorum(); error ActionMismatch(); + error DuplicatedMember(address member); struct Action { address to; bytes data; - bytes extraData; + bytes salt; } struct ActionState { @@ -34,11 +38,10 @@ abstract contract ExecutiveCommittee { address public immutable OWNER; - address[] public membersList; - mapping(address => bool) public members; + EnumerableSet.AddressSet private members; uint256 public quorum; - mapping(bytes32 actionHash => ActionState) actionsStates; + mapping(bytes32 actionHash => ActionState) public actionsStates; mapping(address signer => mapping(bytes32 actionHash => bool support)) public approves; constructor(address owner, address[] memory newMembers, uint256 executionQuorum) { @@ -51,33 +54,36 @@ abstract contract ExecutiveCommittee { OWNER = owner; for (uint256 i = 0; i < newMembers.length; ++i) { + if (members.contains(newMembers[i])) { + revert DuplicatedMember(newMembers[i]); + } _addMember(newMembers[i]); } } function _vote(Action memory action, bool support) internal { - bytes32 actionHash = _hashAction(action); - if (actionsStates[actionHash].action.to == address(0)) { - actionsStates[actionHash].action = action; + bytes32 digest = _hashAction(action); + if (actionsStates[digest].action.to == address(0)) { + actionsStates[digest].action = action; emit ActionProposed(action.to, action.data); } else { _getAndCheckStoredActionState(action); } - if (approves[msg.sender][actionHash] == support) { + if (approves[msg.sender][digest] == support) { return; } - approves[msg.sender][actionHash] = support; + approves[msg.sender][digest] = support; emit ActionVoted(msg.sender, support, action.to, action.data); if (support == true) { - actionsStates[actionHash].signers.push(msg.sender); + actionsStates[digest].signers.push(msg.sender); } else { - uint256 signersLength = actionsStates[actionHash].signers.length; + uint256 signersLength = actionsStates[digest].signers.length; for (uint256 i = 0; i < signersLength; ++i) { - if (actionsStates[actionHash].signers[i] == msg.sender) { - actionsStates[actionHash].signers[i] = actionsStates[actionHash].signers[signersLength - 1]; - actionsStates[actionHash].signers.pop(); + if (actionsStates[digest].signers[i] == msg.sender) { + actionsStates[digest].signers[i] = actionsStates[digest].signers[signersLength - 1]; + actionsStates[digest].signers.pop(); break; } } @@ -94,10 +100,10 @@ abstract contract ExecutiveCommittee { revert QuorumIsNotReached(); } - Address.functionCall(actionState.action.to, actionState.action.data); - actionsStates[actionHash].isExecuted = true; + Address.functionCall(actionState.action.to, actionState.action.data); + emit ActionExecuted(action.to, action.data); } @@ -116,7 +122,7 @@ abstract contract ExecutiveCommittee { function addMember(address newMember, uint256 newQuorum) public onlyOwner { _addMember(newMember); - if (newQuorum == 0 || newQuorum > membersList.length) { + if (newQuorum == 0 || newQuorum > members.length()) { revert InvalidQuorum(); } quorum = newQuorum; @@ -124,20 +130,13 @@ abstract contract ExecutiveCommittee { } function removeMember(address memberToRemove, uint256 newQuorum) public onlyOwner { - if (members[memberToRemove] == false) { + if (!members.contains(memberToRemove)) { revert IsNotMember(); } - members[memberToRemove] = false; - for (uint256 i = 0; i < membersList.length; ++i) { - if (membersList[i] == memberToRemove) { - membersList[i] = membersList[membersList.length - 1]; - membersList.pop(); - break; - } - } + members.remove(memberToRemove); emit MemberRemoved(memberToRemove); - if (newQuorum == 0 || newQuorum > membersList.length) { + if (newQuorum == 0 || newQuorum > members.length()) { revert InvalidQuorum(); } quorum = newQuorum; @@ -145,18 +144,17 @@ abstract contract ExecutiveCommittee { } function getMembers() public view returns (address[] memory) { - return membersList; + return members.values(); } function _addMember(address newMember) internal { - membersList.push(newMember); - members[newMember] = true; + members.add(newMember); emit MemberAdded(newMember); } function _getSupport(bytes32 actionHash) internal view returns (uint256 support) { for (uint256 i = 0; i < actionsStates[actionHash].signers.length; ++i) { - if (members[actionsStates[actionHash].signers[i]] == true) { + if (members.contains(actionsStates[actionHash].signers[i])) { support++; } } @@ -179,11 +177,11 @@ abstract contract ExecutiveCommittee { } function _hashAction(Action memory action) internal pure returns (bytes32) { - return keccak256(abi.encode(action.to, action.data, action.extraData)); + return keccak256(abi.encode(action.to, action.data, action.salt)); } modifier onlyMember() { - if (members[msg.sender] == false) { + if (!members.contains(msg.sender)) { revert SenderIsNotMember(); } _; From 4c83cb1bf690fa4fe43b6d184acb5b8a66f71618 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Thu, 13 Jun 2024 18:18:27 +0300 Subject: [PATCH 25/38] feat: move tiebreaker logic from dual governance to lib --- contracts/DualGovernance.sol | 66 ++++--------------- contracts/libraries/TiebreakerProtection.sol | 67 ++++++++++++++++++++ 2 files changed, 78 insertions(+), 55 deletions(-) create mode 100644 contracts/libraries/TiebreakerProtection.sol diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index f8eaadf4..7c4fda46 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -8,34 +8,20 @@ import {ConfigurationProvider} from "./ConfigurationProvider.sol"; import {Proposers, Proposer} from "./libraries/Proposers.sol"; import {ExecutorCall} from "./libraries/Proposals.sol"; import {DualGovernanceState, State as GovernanceState} from "./libraries/DualGovernanceState.sol"; +import {TiebreakerProtection} from "./libraries/TiebreakerProtection.sol"; contract DualGovernance is IGovernance, ConfigurationProvider { using Proposers for Proposers.State; using DualGovernanceState for DualGovernanceState.Store; + using TiebreakerProtection for TiebreakerProtection.Tiebreaker; - event TiebreakerSet(address tiebreakCommittee); - event ProposalApprovedForExecution(uint256 proposalId); event ProposalScheduled(uint256 proposalId); - event SealableResumeApproved(address sealable); - - error ProposalNotExecutable(uint256 proposalId); - error NotTiebreaker(address account, address tiebreakCommittee); - error ProposalAlreadyApproved(uint256 proposalId); - error ProposalIsNotApprovedForExecution(uint256 proposalId); - error TiebreakerTimelockIsNotPassed(uint256 proposalId); - error SealableResumeAlreadyApproved(address sealable); - error TieBreakerAddressIsSame(); ITimelock public immutable TIMELOCK; - address internal _tiebreaker; - uint256 internal _tiebreakerProposalApprovalTimelock; - mapping(uint256 proposalId => uint256) internal _tiebreakerProposalApprovalTimestamp; - mapping(address sealable => bool) internal _tiebreakerSealableResumeApprovals; - + TiebreakerProtection.Tiebreaker internal _tiebreaker; Proposers.State internal _proposers; DualGovernanceState.Store internal _dgState; - mapping(uint256 proposalId => uint256 executableAfter) internal _scheduledProposals; constructor( address config, @@ -74,10 +60,6 @@ contract DualGovernance is IGovernance, ConfigurationProvider { return address(_dgState.signallingEscrow); } - function isScheduled(uint256 proposalId) external view returns (bool) { - return _scheduledProposals[proposalId] != 0; - } - function canSchedule(uint256 proposalId) external view returns (bool) { return _dgState.isProposalsAdoptionAllowed() && TIMELOCK.canSchedule(proposalId); } @@ -153,59 +135,33 @@ contract DualGovernance is IGovernance, ConfigurationProvider { // --- function tiebreakerApproveProposal(uint256 proposalId) external { - _checkTiebreakerCommittee(msg.sender); + _tiebreaker.checkTiebreakerCommittee(msg.sender); _dgState.checkTiebreak(CONFIG); - if (_tiebreakerProposalApprovalTimestamp[proposalId] > 0) { - revert ProposalAlreadyApproved(proposalId); - } - - _tiebreakerProposalApprovalTimestamp[proposalId] = block.timestamp; - emit ProposalApprovedForExecution(proposalId); + _tiebreaker.approveProposal(proposalId); } function tiebreakerApproveSealableResume(address sealable) external { - _checkTiebreakerCommittee(msg.sender); + _tiebreaker.checkTiebreakerCommittee(msg.sender); _dgState.checkTiebreak(CONFIG); Proposer memory proposer = _proposers.get(msg.sender); ExecutorCall[] memory calls = new ExecutorCall[](1); calls[0] = ExecutorCall(sealable, 0, abi.encodeWithSelector(ISealable.resume.selector)); uint256 proposalId = TIMELOCK.submit(proposer.executor, calls); - _tiebreakerProposalApprovalTimestamp[proposalId] = block.timestamp; - emit ProposalApprovedForExecution(proposalId); - emit SealableResumeApproved(sealable); + _tiebreaker.approveSealableResume(proposalId, sealable); } function tiebreakerSchedule(uint256 proposalId) external { _dgState.checkTiebreak(CONFIG); - if (_tiebreakerProposalApprovalTimestamp[proposalId] == 0) { - revert ProposalIsNotApprovedForExecution(proposalId); - } - if (_tiebreakerProposalApprovalTimestamp[proposalId] + _tiebreakerProposalApprovalTimelock > block.timestamp) { - revert TiebreakerTimelockIsNotPassed(proposalId); - } + _tiebreaker.canSchedule(proposalId); TIMELOCK.schedule(proposalId); } function setTiebreakerProtection(address newTiebreaker) external { _checkAdminExecutor(msg.sender); - if (_tiebreaker == newTiebreaker) { - revert TieBreakerAddressIsSame(); + if (_tiebreaker.tiebreaker != address(0)) { + _proposers.unregister(CONFIG, _tiebreaker.tiebreaker); } - if (_tiebreaker != address(0)) { - _proposers.unregister(CONFIG, _tiebreaker); - } - _tiebreaker = newTiebreaker; _proposers.register(newTiebreaker, CONFIG.ADMIN_EXECUTOR()); // TODO: check what executor should be. Reseal executor? - emit TiebreakerSet(newTiebreaker); - } - - // --- - // Internal Helper Methods - // --- - - function _checkTiebreakerCommittee(address account) internal view { - if (account != _tiebreaker) { - revert NotTiebreaker(account, _tiebreaker); - } + _tiebreaker.setTiebreaker(newTiebreaker); } } diff --git a/contracts/libraries/TiebreakerProtection.sol b/contracts/libraries/TiebreakerProtection.sol new file mode 100644 index 00000000..4ce4dd24 --- /dev/null +++ b/contracts/libraries/TiebreakerProtection.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +library TiebreakerProtection { + struct Tiebreaker { + address tiebreaker; + uint256 tiebreakerProposalApprovalTimelock; + mapping(uint256 proposalId => uint256) tiebreakerProposalApprovalTimestamp; + } + + event TiebreakerSet(address tiebreakCommittee); + event ProposalApprovedForExecution(uint256 proposalId); + event SealableResumeApproved(address sealable); + + error ProposalNotExecutable(uint256 proposalId); + error NotTiebreaker(address account, address tiebreakCommittee); + error ProposalAlreadyApproved(uint256 proposalId); + error ProposalIsNotApprovedForExecution(uint256 proposalId); + error TiebreakerTimelockIsNotPassed(uint256 proposalId); + error SealableResumeAlreadyApproved(address sealable); + error TieBreakerAddressIsSame(); + + function approveProposal(Tiebreaker storage self, uint256 proposalId) internal { + if (self.tiebreakerProposalApprovalTimestamp[proposalId] > 0) { + revert ProposalAlreadyApproved(proposalId); + } + + _approveProposal(self, proposalId); + } + + function approveSealableResume(Tiebreaker storage self, uint256 proposalId, address sealable) internal { + _approveProposal(self, proposalId); + emit SealableResumeApproved(sealable); + } + + function canSchedule(Tiebreaker storage self, uint256 proposalId) internal view { + if (self.tiebreakerProposalApprovalTimestamp[proposalId] == 0) { + revert ProposalIsNotApprovedForExecution(proposalId); + } + if ( + self.tiebreakerProposalApprovalTimestamp[proposalId] + self.tiebreakerProposalApprovalTimelock + > block.timestamp + ) { + revert TiebreakerTimelockIsNotPassed(proposalId); + } + } + + function setTiebreaker(Tiebreaker storage self, address tiebreaker) internal { + if (self.tiebreaker == tiebreaker) { + revert TieBreakerAddressIsSame(); + } + + self.tiebreaker = tiebreaker; + emit TiebreakerSet(tiebreaker); + } + + function checkTiebreakerCommittee(Tiebreaker storage self, address account) internal view { + if (account != self.tiebreaker) { + revert NotTiebreaker(account, self.tiebreaker); + } + } + + function _approveProposal(Tiebreaker storage self, uint256 proposalId) internal { + self.tiebreakerProposalApprovalTimestamp[proposalId] = block.timestamp; + emit ProposalApprovedForExecution(proposalId); + } +} From 59dcc3473a373ef5a5f4772d04c62e13d91a6b41 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Mon, 17 Jun 2024 17:56:56 +0300 Subject: [PATCH 26/38] review fixes and tests --- contracts/EmergencyActivationCommittee.sol | 2 +- contracts/EmergencyExecutionCommittee.sol | 4 +- contracts/ExecutiveCommittee.sol | 28 ++-- contracts/TiebreakerCore.sol | 4 +- contracts/TiebreakerSubCommittee.sol | 4 +- test/unit/EmergencyActivationCommittee.t.sol | 21 +++ test/unit/ExecutiveCommittee.t.sol | 160 +++++++++++++++++++ test/utils/unit-test.sol | 12 ++ 8 files changed, 211 insertions(+), 24 deletions(-) create mode 100644 test/unit/EmergencyActivationCommittee.t.sol create mode 100644 test/unit/ExecutiveCommittee.t.sol create mode 100644 test/utils/unit-test.sol diff --git a/contracts/EmergencyActivationCommittee.sol b/contracts/EmergencyActivationCommittee.sol index 9acd5341..f2714271 100644 --- a/contracts/EmergencyActivationCommittee.sol +++ b/contracts/EmergencyActivationCommittee.sol @@ -24,7 +24,7 @@ contract EmergencyActivationCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return getActionState(_buildEmergencyActivateAction()); + return _getActionState(_buildEmergencyActivateAction()); } function executeEmergencyActivate() external { diff --git a/contracts/EmergencyExecutionCommittee.sol b/contracts/EmergencyExecutionCommittee.sol index c170d239..866d61d7 100644 --- a/contracts/EmergencyExecutionCommittee.sol +++ b/contracts/EmergencyExecutionCommittee.sol @@ -31,7 +31,7 @@ contract EmergencyExecutionCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return getActionState(_buildEmergencyExecuteAction(_proposalId)); + return _getActionState(_buildEmergencyExecuteAction(_proposalId)); } function executeEmergencyExecute(uint256 _proposalId) public { @@ -49,7 +49,7 @@ contract EmergencyExecutionCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return getActionState(_buildEmergencyResetAction()); + return _getActionState(_buildEmergencyResetAction()); } function executeEmergencyReset() external { diff --git a/contracts/ExecutiveCommittee.sol b/contracts/ExecutiveCommittee.sol index 1ab992b9..887d42f6 100644 --- a/contracts/ExecutiveCommittee.sol +++ b/contracts/ExecutiveCommittee.sol @@ -33,7 +33,6 @@ abstract contract ExecutiveCommittee { struct ActionState { Action action; bool isExecuted; - address[] signers; } address public immutable OWNER; @@ -76,18 +75,6 @@ abstract contract ExecutiveCommittee { approves[msg.sender][digest] = support; emit ActionVoted(msg.sender, support, action.to, action.data); - if (support == true) { - actionsStates[digest].signers.push(msg.sender); - } else { - uint256 signersLength = actionsStates[digest].signers.length; - for (uint256 i = 0; i < signersLength; ++i) { - if (actionsStates[digest].signers[i] == msg.sender) { - actionsStates[digest].signers[i] = actionsStates[digest].signers[signersLength - 1]; - actionsStates[digest].signers.pop(); - break; - } - } - } } function _execute(Action memory action) internal { @@ -107,8 +94,8 @@ abstract contract ExecutiveCommittee { emit ActionExecuted(action.to, action.data); } - function getActionState(Action memory action) - public + function _getActionState(Action memory action) + internal view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { @@ -147,14 +134,21 @@ abstract contract ExecutiveCommittee { return members.values(); } + function isMember(address member) public view returns (bool) { + return members.contains(member); + } + function _addMember(address newMember) internal { + if (members.contains(newMember)) { + revert DuplicatedMember(newMember); + } members.add(newMember); emit MemberAdded(newMember); } function _getSupport(bytes32 actionHash) internal view returns (uint256 support) { - for (uint256 i = 0; i < actionsStates[actionHash].signers.length; ++i) { - if (members.contains(actionsStates[actionHash].signers[i])) { + for (uint256 i = 0; i < members.length(); ++i) { + if (approves[members.at(i)][actionHash]) { support++; } } diff --git a/contracts/TiebreakerCore.sol b/contracts/TiebreakerCore.sol index 8896f1c1..616ff98c 100644 --- a/contracts/TiebreakerCore.sol +++ b/contracts/TiebreakerCore.sol @@ -30,7 +30,7 @@ contract TiebreakerCore is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return getActionState(_buildApproveProposalAction(_proposalId)); + return _getActionState(_buildApproveProposalAction(_proposalId)); } function executeApproveProposal(uint256 _proposalId) public { @@ -54,7 +54,7 @@ contract TiebreakerCore is ExecutiveCommittee { address sealable, uint256 nonce ) public view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return getActionState(_buildSealableResumeAction(sealable, nonce)); + return _getActionState(_buildSealableResumeAction(sealable, nonce)); } function executeSealableResume(address sealable) external { diff --git a/contracts/TiebreakerSubCommittee.sol b/contracts/TiebreakerSubCommittee.sol index f5f64353..aa86f68d 100644 --- a/contracts/TiebreakerSubCommittee.sol +++ b/contracts/TiebreakerSubCommittee.sol @@ -30,7 +30,7 @@ contract TiebreakerSubCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return getActionState(_buildApproveProposalAction(proposalId)); + return _getActionState(_buildApproveProposalAction(proposalId)); } function executeApproveProposal(uint256 proposalId) public { @@ -48,7 +48,7 @@ contract TiebreakerSubCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return getActionState(_buildApproveSealableResumeAction(sealable)); + return _getActionState(_buildApproveSealableResumeAction(sealable)); } function executeApproveSealableResume(address sealable) public { diff --git a/test/unit/EmergencyActivationCommittee.t.sol b/test/unit/EmergencyActivationCommittee.t.sol new file mode 100644 index 00000000..d151dd14 --- /dev/null +++ b/test/unit/EmergencyActivationCommittee.t.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {EmergencyActivationCommittee} from "../../contracts/EmergencyActivationCommittee.sol"; + +import {ExecutiveCommitteeUnitTest, ExecutiveCommittee} from "./ExecutiveCommittee.t.sol"; + +contract EmergencyActivationCommitteeUnitTest is ExecutiveCommitteeUnitTest { + EmergencyActivationCommittee internal _emergencyActivationCommittee; + + EmergencyProtectedTimelockMock internal _emergencyProtectedTimelock; + + function setUp() public { + _emergencyProtectedTimelock = new EmergencyProtectedTimelockMock(); + _emergencyActivationCommittee = + new EmergencyActivationCommittee(_owner, _committeeMembers, _quorum, address(_emergencyProtectedTimelock)); + _executiveCommittee = ExecutiveCommittee(_emergencyActivationCommittee); + } +} + +contract EmergencyProtectedTimelockMock {} diff --git a/test/unit/ExecutiveCommittee.t.sol b/test/unit/ExecutiveCommittee.t.sol new file mode 100644 index 00000000..d51e3247 --- /dev/null +++ b/test/unit/ExecutiveCommittee.t.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {UnitTest} from "test/utils/unit-test.sol"; + +import {ExecutiveCommittee} from "../../contracts/ExecutiveCommittee.sol"; + +abstract contract ExecutiveCommitteeUnitTest is UnitTest { + ExecutiveCommittee internal _executiveCommittee; + + address internal _owner = makeAddr("COMMITTEE_OWNER"); + + address internal _stranger = makeAddr("STRANGER"); + + uint256 internal _membersCount = 13; + uint256 internal _quorum = 7; + address[] internal _committeeMembers = new address[](_membersCount); + + constructor() { + for (uint256 i = 0; i < _membersCount; ++i) { + _committeeMembers[i] = makeAddr(string(abi.encode(0xFE + i * _membersCount + 65))); + } + } + + function test_isMember() public { + for (uint256 i = 0; i < _membersCount; ++i) { + assertEq(_executiveCommittee.isMember(_committeeMembers[i]), true); + } + + assertEq(_executiveCommittee.isMember(_owner), false); + assertEq(_executiveCommittee.isMember(_stranger), false); + } + + function test_getMembers() public { + address[] memory committeeMembers = _executiveCommittee.getMembers(); + + assertEq(committeeMembers.length, _committeeMembers.length); + + for (uint256 i = 0; i < _membersCount; ++i) { + assertEq(committeeMembers[i], _committeeMembers[i]); + } + } + + function test_addMember_stranger_call() public { + address newMember = makeAddr("NEW_MEMBER"); + assertEq(_executiveCommittee.isMember(newMember), false); + + vm.prank(_stranger); + vm.expectRevert(abi.encodeWithSignature("SenderIsNotOwner()")); + _executiveCommittee.addMember(newMember, _quorum); + + for (uint256 i = 0; i < _membersCount; ++i) { + vm.prank(_committeeMembers[i]); + vm.expectRevert(abi.encodeWithSignature("SenderIsNotOwner()")); + _executiveCommittee.addMember(newMember, _quorum); + } + } + + function test_addMember_reverts_on_duplicate() public { + address existedMember = _committeeMembers[0]; + assertEq(_executiveCommittee.isMember(existedMember), true); + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSignature("DuplicatedMember(address)", existedMember)); + _executiveCommittee.addMember(existedMember, _quorum); + } + + function test_addMember_reverts_on_invalid_quorum() public { + address newMember = makeAddr("NEW_MEMBER"); + assertEq(_executiveCommittee.isMember(newMember), false); + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); + _executiveCommittee.addMember(newMember, 0); + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); + _executiveCommittee.addMember(newMember, _membersCount + 2); + } + + function test_addMember() public { + address newMember = makeAddr("NEW_MEMBER"); + uint256 newQuorum = _quorum + 1; + + assertEq(_executiveCommittee.isMember(newMember), false); + + vm.prank(_owner); + vm.expectEmit(address(_executiveCommittee)); + emit ExecutiveCommittee.MemberAdded(newMember); + vm.expectEmit(address(_executiveCommittee)); + emit ExecutiveCommittee.QuorumSet(newQuorum); + _executiveCommittee.addMember(newMember, newQuorum); + + assertEq(_executiveCommittee.isMember(newMember), true); + + address[] memory committeeMembers = _executiveCommittee.getMembers(); + + assertEq(committeeMembers.length, _membersCount + 1); + assertEq(committeeMembers[committeeMembers.length - 1], newMember); + } + + function test_removeMember_stranger_call() public { + address memberToRemove = _committeeMembers[0]; + assertEq(_executiveCommittee.isMember(memberToRemove), true); + + vm.prank(_stranger); + vm.expectRevert(abi.encodeWithSignature("SenderIsNotOwner()")); + _executiveCommittee.removeMember(memberToRemove, _quorum); + + for (uint256 i = 0; i < _membersCount; ++i) { + vm.prank(_committeeMembers[i]); + vm.expectRevert(abi.encodeWithSignature("SenderIsNotOwner()")); + _executiveCommittee.removeMember(memberToRemove, _quorum); + } + } + + function test_removeMember_reverts_on_member_is_not_exist() public { + assertEq(_executiveCommittee.isMember(_stranger), false); + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSignature("IsNotMember()")); + _executiveCommittee.removeMember(_stranger, _quorum); + } + + function test_removeMember_reverts_on_invalid_quorum() public { + address memberToRemove = _committeeMembers[0]; + assertEq(_executiveCommittee.isMember(memberToRemove), true); + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); + _executiveCommittee.removeMember(memberToRemove, 0); + + vm.prank(_owner); + vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); + _executiveCommittee.removeMember(memberToRemove, _membersCount); + } + + function test_removeMember() public { + address memberToRemove = _committeeMembers[0]; + uint256 newQuorum = _quorum - 1; + + assertEq(_executiveCommittee.isMember(memberToRemove), true); + + vm.prank(_owner); + vm.expectEmit(address(_executiveCommittee)); + emit ExecutiveCommittee.MemberRemoved(memberToRemove); + vm.expectEmit(address(_executiveCommittee)); + emit ExecutiveCommittee.QuorumSet(newQuorum); + _executiveCommittee.removeMember(memberToRemove, newQuorum); + + assertEq(_executiveCommittee.isMember(memberToRemove), false); + + address[] memory committeeMembers = _executiveCommittee.getMembers(); + + assertEq(committeeMembers.length, _membersCount - 1); + for (uint256 i = 0; i < committeeMembers.length; ++i) { + assertNotEq(committeeMembers[i], memberToRemove); + } + } +} diff --git a/test/utils/unit-test.sol b/test/utils/unit-test.sol new file mode 100644 index 00000000..bec9d6de --- /dev/null +++ b/test/utils/unit-test.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// solhint-disable-next-line +import {Test} from "forge-std/Test.sol"; +import {ExecutorCall} from "contracts/libraries/Proposals.sol"; + +contract UnitTest is Test { + function _wait(uint256 duration) internal { + vm.warp(block.timestamp + duration); + } +} From ab594d706c4baeacc4a50bef0fe421bb4dc4c675 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 19 Jun 2024 14:20:34 +0300 Subject: [PATCH 27/38] executive committee tests --- test/unit/ExecutiveCommittee.t.sol | 267 +++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) diff --git a/test/unit/ExecutiveCommittee.t.sol b/test/unit/ExecutiveCommittee.t.sol index d51e3247..aefbf4ef 100644 --- a/test/unit/ExecutiveCommittee.t.sol +++ b/test/unit/ExecutiveCommittee.t.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.23; import {UnitTest} from "test/utils/unit-test.sol"; +import {Vm} from "forge-std/Test.sol"; + import {ExecutiveCommittee} from "../../contracts/ExecutiveCommittee.sol"; abstract contract ExecutiveCommitteeUnitTest is UnitTest { @@ -158,3 +160,268 @@ abstract contract ExecutiveCommitteeUnitTest is UnitTest { } } } + +contract ExecutiveCommitteeWrapper is ExecutiveCommittee { + constructor( + address owner, + address[] memory newMembers, + uint256 executionQuorum + ) ExecutiveCommittee(owner, newMembers, executionQuorum) {} + + function vote(Action memory action, bool support) public { + _vote(action, support); + } + + function execute(Action memory action) public { + _execute(action); + } + + function getActionState(Action memory action) + public + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + { + return _getActionState(action); + } + + function getSupport(bytes32 actionHash) public view returns (uint256 support) { + return _getSupport(actionHash); + } + + function getAndCheckStoredActionState(Action memory action) + public + view + returns (ActionState memory storedActionState, bytes32 actionHash) + { + return _getAndCheckStoredActionState(action); + } + + function hashAction(Action memory action) public pure returns (bytes32) { + return _hashAction(action); + } +} + +contract Target { + event Executed(); + + function trigger() public { + emit Executed(); + } +} + +contract ExecutiveCommitteeInternalUnitTest is ExecutiveCommitteeUnitTest { + ExecutiveCommitteeWrapper internal _executiveCommitteeWrapper; + Target internal _target; + + function setUp() public { + _target = new Target(); + _executiveCommitteeWrapper = new ExecutiveCommitteeWrapper(_owner, _committeeMembers, _quorum); + _executiveCommittee = ExecutiveCommittee(_executiveCommitteeWrapper); + } + + function test_hashAction() public { + ExecutiveCommittee.Action memory action = ExecutiveCommittee.Action(address(1), new bytes(10), new bytes(100)); + + bytes32 actionHash = keccak256(abi.encode(action.to, action.data, action.salt)); + + assertEq(_executiveCommitteeWrapper.hashAction(action), actionHash); + } + + function test_getSupport() public { + ExecutiveCommittee.Action memory action = ExecutiveCommittee.Action(address(1), new bytes(10), new bytes(100)); + bytes32 actionHash = keccak256(abi.encode(action.to, action.data, action.salt)); + + assertEq(_executiveCommitteeWrapper.getSupport(actionHash), 0); + + for (uint256 i = 0; i < _membersCount; ++i) { + assertEq(_executiveCommitteeWrapper.getSupport(actionHash), i); + vm.prank(_committeeMembers[i]); + _executiveCommitteeWrapper.vote(action, true); + assertEq(_executiveCommitteeWrapper.getSupport(actionHash), i + 1); + } + + assertEq(_executiveCommitteeWrapper.getSupport(actionHash), _membersCount); + + for (uint256 i = 0; i < _membersCount; ++i) { + assertEq(_executiveCommitteeWrapper.getSupport(actionHash), _membersCount - i); + vm.prank(_committeeMembers[i]); + _executiveCommitteeWrapper.vote(action, false); + assertEq(_executiveCommitteeWrapper.getSupport(actionHash), _membersCount - i - 1); + } + + assertEq(_executiveCommitteeWrapper.getSupport(actionHash), 0); + } + + function test_getAndCheckActionState() public { + address to = address(_target); + bytes memory data = abi.encodeWithSelector(Target.trigger.selector); + bytes memory salt = abi.encodePacked(hex"beaf"); + + ExecutiveCommittee.Action memory action = ExecutiveCommittee.Action(to, data, salt); + bytes32 actionHash = keccak256(abi.encode(action.to, action.data, action.salt)); + + ExecutiveCommittee.ActionState memory storedActionStateFromContract; + bytes32 actionHashFromContract; + + vm.expectRevert(abi.encodeWithSignature("ActionMismatch()")); + _executiveCommitteeWrapper.getAndCheckStoredActionState(action); + + vm.prank(_committeeMembers[0]); + _executiveCommitteeWrapper.vote(action, false); + + (storedActionStateFromContract, actionHashFromContract) = + _executiveCommitteeWrapper.getAndCheckStoredActionState(action); + assertEq(storedActionStateFromContract.isExecuted, false); + assertEq(storedActionStateFromContract.action.to, to); + assertEq(storedActionStateFromContract.action.data, data); + assertEq(storedActionStateFromContract.action.salt, salt); + assertEq(actionHashFromContract, actionHash); + + for (uint256 i = 0; i < _membersCount; ++i) { + vm.prank(_committeeMembers[i]); + _executiveCommitteeWrapper.vote(action, true); + } + + _executiveCommitteeWrapper.execute(action); + + vm.expectRevert(abi.encodeWithSignature("ActionAlreadyExecuted()")); + _executiveCommitteeWrapper.getAndCheckStoredActionState(action); + } + + function test_getActionState() public { + address to = address(_target); + bytes memory data = abi.encodeWithSelector(Target.trigger.selector); + bytes memory salt = abi.encodePacked(hex"beaf"); + + ExecutiveCommittee.Action memory action = ExecutiveCommittee.Action(to, data, salt); + + vm.prank(_committeeMembers[0]); + _executiveCommitteeWrapper.vote(action, false); + + uint256 support; + uint256 execuitionQuorum; + bool isExecuted; + + (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getActionState(action); + assertEq(support, 0); + assertEq(execuitionQuorum, _quorum); + assertEq(isExecuted, false); + + for (uint256 i = 0; i < _membersCount; ++i) { + (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getActionState(action); + assertEq(support, i); + assertEq(execuitionQuorum, _quorum); + assertEq(isExecuted, false); + + vm.prank(_committeeMembers[i]); + _executiveCommitteeWrapper.vote(action, true); + + (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getActionState(action); + assertEq(support, i + 1); + assertEq(execuitionQuorum, _quorum); + assertEq(isExecuted, false); + } + + (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getActionState(action); + assertEq(support, _membersCount); + assertEq(execuitionQuorum, _quorum); + assertEq(isExecuted, false); + + _executiveCommitteeWrapper.execute(action); + + vm.expectRevert(abi.encodeWithSignature("ActionAlreadyExecuted()")); + _executiveCommitteeWrapper.getActionState(action); + } + + function test_vote() public { + address to = address(_target); + bytes memory data = abi.encodeWithSelector(Target.trigger.selector); + bytes memory salt = abi.encodePacked(hex"beaf"); + + ExecutiveCommittee.Action memory action = ExecutiveCommittee.Action(to, data, salt); + bytes32 actionHash = keccak256(abi.encode(action.to, action.data, action.salt)); + + assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], actionHash), false); + + vm.prank(_committeeMembers[0]); + vm.expectEmit(address(_executiveCommitteeWrapper)); + emit ExecutiveCommittee.ActionVoted(_committeeMembers[0], true, to, data); + _executiveCommitteeWrapper.vote(action, true); + assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], actionHash), true); + + vm.prank(_committeeMembers[0]); + vm.recordLogs(); + _executiveCommitteeWrapper.vote(action, true); + Vm.Log[] memory logs = vm.getRecordedLogs(); + assertEq(logs.length, 0); + assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], actionHash), true); + + vm.prank(_committeeMembers[0]); + vm.expectEmit(address(_executiveCommitteeWrapper)); + emit ExecutiveCommittee.ActionVoted(_committeeMembers[0], false, to, data); + _executiveCommitteeWrapper.vote(action, false); + assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], actionHash), false); + + vm.prank(_committeeMembers[0]); + vm.recordLogs(); + _executiveCommitteeWrapper.vote(action, false); + logs = vm.getRecordedLogs(); + assertEq(logs.length, 0); + assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], actionHash), false); + } + + function test_vote_reverts_on_executed() public { + address to = address(_target); + bytes memory data = abi.encodeWithSelector(Target.trigger.selector); + bytes memory salt = abi.encodePacked(hex"beaf"); + + ExecutiveCommittee.Action memory action = ExecutiveCommittee.Action(to, data, salt); + + for (uint256 i = 0; i < _quorum; ++i) { + vm.prank(_committeeMembers[i]); + _executiveCommitteeWrapper.vote(action, true); + } + + _executiveCommitteeWrapper.execute(action); + + vm.prank(_committeeMembers[0]); + vm.expectRevert(abi.encodeWithSignature("ActionAlreadyExecuted()")); + _executiveCommitteeWrapper.vote(action, true); + } + + function test_execute_events() public { + address to = address(_target); + bytes memory data = abi.encodeWithSelector(Target.trigger.selector); + bytes memory salt = abi.encodePacked(hex"beaf"); + + ExecutiveCommittee.Action memory action = ExecutiveCommittee.Action(to, data, salt); + + vm.prank(_stranger); + vm.expectRevert(abi.encodeWithSignature("ActionMismatch()")); + _executiveCommitteeWrapper.execute(action); + + vm.prank(_committeeMembers[0]); + _executiveCommitteeWrapper.vote(action, true); + + vm.prank(_stranger); + vm.expectRevert(abi.encodeWithSignature("QuorumIsNotReached()")); + _executiveCommitteeWrapper.execute(action); + + for (uint256 i = 0; i < _quorum; ++i) { + vm.prank(_committeeMembers[i]); + _executiveCommitteeWrapper.vote(action, true); + } + + vm.prank(_stranger); + vm.expectEmit(address(_target)); + emit Target.Executed(); + vm.expectEmit(address(_executiveCommitteeWrapper)); + emit ExecutiveCommittee.ActionExecuted(to, data); + + _executiveCommitteeWrapper.execute(action); + + vm.prank(_stranger); + vm.expectRevert(abi.encodeWithSignature("ActionAlreadyExecuted()")); + _executiveCommitteeWrapper.execute(action); + } +} From a84afd9343e17cdd3a5b81d3ad01ed1c60a8f985 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 20 Jun 2024 13:11:52 +0300 Subject: [PATCH 28/38] WIP: committee refactoring --- contracts/ResealExecutor.sol | 35 ++----------------- .../EmergencyActivationCommittee.sol | 0 .../EmergencyExecutionCommittee.sol | 0 .../{ => committees}/ExecutiveCommittee.sol | 8 ++--- .../{ => committees}/ResealCommittee.sol | 0 contracts/{ => committees}/TiebreakerCore.sol | 0 .../TiebreakerSubCommittee.sol | 0 7 files changed, 5 insertions(+), 38 deletions(-) rename contracts/{ => committees}/EmergencyActivationCommittee.sol (100%) rename contracts/{ => committees}/EmergencyExecutionCommittee.sol (100%) rename contracts/{ => committees}/ExecutiveCommittee.sol (94%) rename contracts/{ => committees}/ResealCommittee.sol (100%) rename contracts/{ => committees}/TiebreakerCore.sol (100%) rename contracts/{ => committees}/TiebreakerSubCommittee.sol (100%) diff --git a/contracts/ResealExecutor.sol b/contracts/ResealExecutor.sol index 80c1b0e0..c9931444 100644 --- a/contracts/ResealExecutor.sol +++ b/contracts/ResealExecutor.sol @@ -1,24 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {OwnableExecutor, Address} from "./OwnableExecutor.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ISealable} from "./interfaces/ISealable.sol"; -interface IDualGovernanace { - enum GovernanceState { - Normal, - VetoSignalling, - VetoSignallingDeactivation, - VetoCooldown, - RageQuit - } - - function currentState() external view returns (GovernanceState); -} - -contract ResealExecutor is OwnableExecutor { - event ResealCommitteeSet(address indexed newResealCommittee); - +contract ResealExecutor is Ownable { error SenderIsNotCommittee(); error DualGovernanceInNormalState(); error SealableWrongPauseState(); @@ -28,15 +14,12 @@ contract ResealExecutor is OwnableExecutor { address public resealCommittee; - constructor(address owner, address dualGovernance, address resealCommitteeAddress) OwnableExecutor(owner) { + constructor(address owner, address dualGovernance) OwnableExecutor(owner) { DUAL_GOVERNANCE = dualGovernance; resealCommittee = resealCommitteeAddress; } function reseal(address[] memory sealables) public onlyCommittee { - if (IDualGovernanace(DUAL_GOVERNANCE).currentState() == IDualGovernanace.GovernanceState.Normal) { - revert DualGovernanceInNormalState(); - } for (uint256 i = 0; i < sealables.length; ++i) { uint256 sealableResumeSinceTimestamp = ISealable(sealables[i]).getResumeSinceTimestamp(); if (sealableResumeSinceTimestamp < block.timestamp || sealableResumeSinceTimestamp == PAUSE_INFINITELY) { @@ -46,16 +29,4 @@ contract ResealExecutor is OwnableExecutor { Address.functionCall(sealables[i], abi.encodeWithSelector(ISealable.pauseFor.selector, PAUSE_INFINITELY)); } } - - function setResealCommittee(address newResealCommittee) public onlyOwner { - resealCommittee = newResealCommittee; - emit ResealCommitteeSet(newResealCommittee); - } - - modifier onlyCommittee() { - if (msg.sender != resealCommittee) { - revert SenderIsNotCommittee(); - } - _; - } } diff --git a/contracts/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol similarity index 100% rename from contracts/EmergencyActivationCommittee.sol rename to contracts/committees/EmergencyActivationCommittee.sol diff --git a/contracts/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol similarity index 100% rename from contracts/EmergencyExecutionCommittee.sol rename to contracts/committees/EmergencyExecutionCommittee.sol diff --git a/contracts/ExecutiveCommittee.sol b/contracts/committees/ExecutiveCommittee.sol similarity index 94% rename from contracts/ExecutiveCommittee.sol rename to contracts/committees/ExecutiveCommittee.sol index 887d42f6..ea6f97d5 100644 --- a/contracts/ExecutiveCommittee.sol +++ b/contracts/committees/ExecutiveCommittee.sol @@ -77,7 +77,7 @@ abstract contract ExecutiveCommittee { emit ActionVoted(msg.sender, support, action.to, action.data); } - function _execute(Action memory action) internal { + function _markExecuted(Action memory action) internal { (ActionState memory actionState, bytes32 actionHash) = _getAndCheckStoredActionState(action); if (actionState.isExecuted == true) { @@ -89,8 +89,6 @@ abstract contract ExecutiveCommittee { actionsStates[actionHash].isExecuted = true; - Address.functionCall(actionState.action.to, actionState.action.data); - emit ActionExecuted(action.to, action.data); } @@ -162,9 +160,7 @@ abstract contract ExecutiveCommittee { actionHash = _hashAction(action); storedActionState = actionsStates[actionHash]; - if (storedActionState.action.to != action.to || storedActionState.action.data.length != action.data.length) { - revert ActionMismatch(); - } + if (storedActionState.isExecuted == true) { revert ActionAlreadyExecuted(); } diff --git a/contracts/ResealCommittee.sol b/contracts/committees/ResealCommittee.sol similarity index 100% rename from contracts/ResealCommittee.sol rename to contracts/committees/ResealCommittee.sol diff --git a/contracts/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol similarity index 100% rename from contracts/TiebreakerCore.sol rename to contracts/committees/TiebreakerCore.sol diff --git a/contracts/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol similarity index 100% rename from contracts/TiebreakerSubCommittee.sol rename to contracts/committees/TiebreakerSubCommittee.sol From d09a26308e9936510799e78b95a3d0aac0747632 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 20 Jun 2024 14:03:14 +0300 Subject: [PATCH 29/38] refactor committees --- contracts/ResealExecutor.sol | 8 +- .../EmergencyActivationCommittee.sol | 15 ++-- contracts/committees/ExecutiveCommittee.sol | 80 +++++-------------- test/scenario/reseal-executor.t.sol | 2 +- test/unit/EmergencyActivationCommittee.t.sol | 2 +- test/unit/ExecutiveCommittee.t.sol | 2 +- test/utils/scenario-test-blueprint.sol | 8 +- 7 files changed, 40 insertions(+), 77 deletions(-) diff --git a/contracts/ResealExecutor.sol b/contracts/ResealExecutor.sol index c9931444..e3411e75 100644 --- a/contracts/ResealExecutor.sol +++ b/contracts/ResealExecutor.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ISealable} from "./interfaces/ISealable.sol"; @@ -12,14 +13,11 @@ contract ResealExecutor is Ownable { uint256 public constant PAUSE_INFINITELY = type(uint256).max; address public immutable DUAL_GOVERNANCE; - address public resealCommittee; - - constructor(address owner, address dualGovernance) OwnableExecutor(owner) { + constructor(address owner, address dualGovernance) Ownable(owner) { DUAL_GOVERNANCE = dualGovernance; - resealCommittee = resealCommitteeAddress; } - function reseal(address[] memory sealables) public onlyCommittee { + function reseal(address[] memory sealables) public onlyOwner { for (uint256 i = 0; i < sealables.length; ++i) { uint256 sealableResumeSinceTimestamp = ISealable(sealables[i]).getResumeSinceTimestamp(); if (sealableResumeSinceTimestamp < block.timestamp || sealableResumeSinceTimestamp == PAUSE_INFINITELY) { diff --git a/contracts/committees/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol index f2714271..93e76930 100644 --- a/contracts/committees/EmergencyActivationCommittee.sol +++ b/contracts/committees/EmergencyActivationCommittee.sol @@ -3,6 +3,10 @@ pragma solidity 0.8.23; import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; +interface IEmergencyProtectedTimelock { + function emergencyActivate() external; +} + contract EmergencyActivationCommittee is ExecutiveCommittee { address public immutable EMERGENCY_PROTECTED_TIMELOCK; @@ -16,7 +20,7 @@ contract EmergencyActivationCommittee is ExecutiveCommittee { } function approveEmergencyActivate() public onlyMember { - _vote(_buildEmergencyActivateAction(), true); + _vote(_hashEmergencyActivateAction(), true); } function getEmergencyActivateState() @@ -24,14 +28,15 @@ contract EmergencyActivationCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getActionState(_buildEmergencyActivateAction()); + return _getActionState(_hashEmergencyActivateAction()); } function executeEmergencyActivate() external { - _execute(_buildEmergencyActivateAction()); + _markExecute(_hashEmergencyActivateAction()); + IEmergencyProtectedTimelock(EMERGENCY_PROTECTED_TIMELOCK).emergencyActivate(); } - function _buildEmergencyActivateAction() internal view returns (Action memory) { - return Action(EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSignature("emergencyActivate()"), new bytes(0)); + function _hashEmergencyActivateAction() internal view returns (Action memory) { + return keccak256("EMERGENCY_ACTIVATE"); } } diff --git a/contracts/committees/ExecutiveCommittee.sol b/contracts/committees/ExecutiveCommittee.sol index ea6f97d5..27d2b410 100644 --- a/contracts/committees/ExecutiveCommittee.sol +++ b/contracts/committees/ExecutiveCommittee.sol @@ -10,38 +10,24 @@ abstract contract ExecutiveCommittee { event MemberAdded(address indexed member); event MemberRemoved(address indexed member); event QuorumSet(uint256 quorum); - event ActionProposed(address indexed to, bytes data); - event ActionExecuted(address indexed to, bytes data); - event ActionVoted(address indexed signer, bool support, address indexed to, bytes data); + event VoteExecuted(address indexed to, bytes data); + event Voted(address indexed signer, bool support, address indexed to, bytes data); error IsNotMember(); error SenderIsNotMember(); error SenderIsNotOwner(); - error DataIsNotEqual(); - error ActionAlreadyExecuted(); + error VoteAlreadyExecuted(); error QuorumIsNotReached(); error InvalidQuorum(); - error ActionMismatch(); error DuplicatedMember(address member); - struct Action { - address to; - bytes data; - bytes salt; - } - - struct ActionState { - Action action; - bool isExecuted; - } - address public immutable OWNER; EnumerableSet.AddressSet private members; uint256 public quorum; - mapping(bytes32 actionHash => ActionState) public actionsStates; - mapping(address signer => mapping(bytes32 actionHash => bool support)) public approves; + mapping(bytes32 digest => bool isEecuted) public voteStates; + mapping(address signer => mapping(bytes32 digest => bool support)) public approves; constructor(address owner, address[] memory newMembers, uint256 executionQuorum) { if (executionQuorum == 0) { @@ -60,13 +46,9 @@ abstract contract ExecutiveCommittee { } } - function _vote(Action memory action, bool support) internal { - bytes32 digest = _hashAction(action); - if (actionsStates[digest].action.to == address(0)) { - actionsStates[digest].action = action; - emit ActionProposed(action.to, action.data); - } else { - _getAndCheckStoredActionState(action); + function _vote(bytes32 digest, bool support) internal { + if (voteStates[digest] == true) { + revert VoteAlreadyExecuted(); } if (approves[msg.sender][digest] == support) { @@ -74,34 +56,30 @@ abstract contract ExecutiveCommittee { } approves[msg.sender][digest] = support; - emit ActionVoted(msg.sender, support, action.to, action.data); + emit Voted(msg.sender, digest); } - function _markExecuted(Action memory action) internal { - (ActionState memory actionState, bytes32 actionHash) = _getAndCheckStoredActionState(action); - - if (actionState.isExecuted == true) { - revert ActionAlreadyExecuted(); + function _markExecuted(bytes32 digest) internal { + if (voteStates[digest] == true) { + revert VoteAlreadyExecuted(); } - if (_getSupport(actionHash) < quorum) { + if (_getSupport(digest) < quorum) { revert QuorumIsNotReached(); } - actionsStates[actionHash].isExecuted = true; + voteStates[digest] = true; - emit ActionExecuted(action.to, action.data); + emit VoteExecuted(digest); } - function _getActionState(Action memory action) + function _getVoteState(bytes32 digest) internal view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - (ActionState memory actionState, bytes32 actionHash) = _getAndCheckStoredActionState(action); - - support = _getSupport(actionHash); + support = _getSupport(digest); execuitionQuorum = quorum; - isExecuted = actionState.isExecuted; + isExecuted = voteStates[digest]; } function addMember(address newMember, uint256 newQuorum) public onlyOwner { @@ -144,32 +122,14 @@ abstract contract ExecutiveCommittee { emit MemberAdded(newMember); } - function _getSupport(bytes32 actionHash) internal view returns (uint256 support) { + function _getSupport(bytes32 digest) internal view returns (uint256 support) { for (uint256 i = 0; i < members.length(); ++i) { - if (approves[members.at(i)][actionHash]) { + if (approves[members.at(i)][digest]) { support++; } } } - function _getAndCheckStoredActionState(Action memory action) - internal - view - returns (ActionState memory storedActionState, bytes32 actionHash) - { - actionHash = _hashAction(action); - - storedActionState = actionsStates[actionHash]; - - if (storedActionState.isExecuted == true) { - revert ActionAlreadyExecuted(); - } - } - - function _hashAction(Action memory action) internal pure returns (bytes32) { - return keccak256(abi.encode(action.to, action.data, action.salt)); - } - modifier onlyMember() { if (!members.contains(msg.sender)) { revert SenderIsNotMember(); diff --git a/test/scenario/reseal-executor.t.sol b/test/scenario/reseal-executor.t.sol index 9fbb2953..6d8701f0 100644 --- a/test/scenario/reseal-executor.t.sol +++ b/test/scenario/reseal-executor.t.sol @@ -5,7 +5,7 @@ import {percents, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint. import {GateSealMock} from "../mocks/GateSealMock.sol"; import {ResealExecutor} from "contracts/ResealExecutor.sol"; -import {ResealCommittee} from "contracts/ResealCommittee.sol"; +import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; import {IGateSeal} from "contracts/interfaces/IGateSeal.sol"; import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; diff --git a/test/unit/EmergencyActivationCommittee.t.sol b/test/unit/EmergencyActivationCommittee.t.sol index d151dd14..aecc9036 100644 --- a/test/unit/EmergencyActivationCommittee.t.sol +++ b/test/unit/EmergencyActivationCommittee.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {EmergencyActivationCommittee} from "../../contracts/EmergencyActivationCommittee.sol"; +import {EmergencyActivationCommittee} from "../../contracts/committees/EmergencyActivationCommittee.sol"; import {ExecutiveCommitteeUnitTest, ExecutiveCommittee} from "./ExecutiveCommittee.t.sol"; diff --git a/test/unit/ExecutiveCommittee.t.sol b/test/unit/ExecutiveCommittee.t.sol index aefbf4ef..1e6bab5b 100644 --- a/test/unit/ExecutiveCommittee.t.sol +++ b/test/unit/ExecutiveCommittee.t.sol @@ -5,7 +5,7 @@ import {UnitTest} from "test/utils/unit-test.sol"; import {Vm} from "forge-std/Test.sol"; -import {ExecutiveCommittee} from "../../contracts/ExecutiveCommittee.sol"; +import {ExecutiveCommittee} from "../../contracts/committees/ExecutiveCommittee.sol"; abstract contract ExecutiveCommitteeUnitTest is UnitTest { ExecutiveCommittee internal _executiveCommittee; diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 886fc2e9..b7338b2a 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -13,10 +13,10 @@ import {Escrow, VetoerState, LockedAssetsTotals} from "contracts/Escrow.sol"; import {IConfiguration, Configuration} from "contracts/Configuration.sol"; import {OwnableExecutor} from "contracts/OwnableExecutor.sol"; -import {EmergencyActivationCommittee} from "contracts/EmergencyActivationCommittee.sol"; -import {EmergencyExecutionCommittee} from "contracts/EmergencyExecutionCommittee.sol"; -import {TiebreakerCore} from "contracts/TiebreakerCore.sol"; -import {TiebreakerSubCommittee} from "contracts/TiebreakerSubCommittee.sol"; +import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; +import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; +import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; +import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; import { ExecutorCall, From ff2eefc09cc11320c9c69027b8ebcd1b44d51c54 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Mon, 24 Jun 2024 14:39:35 +0300 Subject: [PATCH 30/38] executive committee: move execution up --- contracts/DualGovernance.sol | 9 +- .../EmergencyActivationCommittee.sol | 10 +- .../EmergencyExecutionCommittee.sol | 24 +- contracts/committees/ExecutiveCommittee.sol | 39 ++- contracts/committees/ResealCommittee.sol | 14 +- contracts/committees/TiebreakerCore.sol | 39 +-- .../committees/TiebreakerSubCommittee.sol | 29 +-- test/unit/ExecutiveCommittee.t.sol | 222 ++++++------------ 8 files changed, 163 insertions(+), 223 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index fbaf4cf3..2af09f4f 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -18,6 +18,8 @@ contract DualGovernance is IGovernance, ConfigurationProvider { event ProposalScheduled(uint256 proposalId); + error NotTiebreaker(address account, address tiebreakCommittee); + ITimelock public immutable TIMELOCK; TiebreakerProtection.Tiebreaker internal _tiebreaker; @@ -139,9 +141,8 @@ contract DualGovernance is IGovernance, ConfigurationProvider { // Tiebreaker Protection // --- - function tiebreakerScheduleProposal(uint256 proposalId) external { - _checkTiebreakerCommittee(msg.sender); - _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); + function tiebreakerApproveProposal(uint256 proposalId) external { + _tiebreaker.checkTiebreakerCommittee(msg.sender); _dgState.checkTiebreak(CONFIG); _tiebreaker.approveProposal(proposalId); } @@ -162,7 +163,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { TIMELOCK.schedule(proposalId); } - function setTiebreakerCommittee(address newTiebreaker) external { + function setTiebreakerProtection(address newTiebreaker) external { _checkAdminExecutor(msg.sender); if (_tiebreaker.tiebreaker != address(0)) { _proposers.unregister(CONFIG, _tiebreaker.tiebreaker); diff --git a/contracts/committees/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol index 93e76930..f7ecdcfc 100644 --- a/contracts/committees/EmergencyActivationCommittee.sol +++ b/contracts/committees/EmergencyActivationCommittee.sol @@ -20,7 +20,7 @@ contract EmergencyActivationCommittee is ExecutiveCommittee { } function approveEmergencyActivate() public onlyMember { - _vote(_hashEmergencyActivateAction(), true); + _vote(_encodeEmergencyActivateData(), true); } function getEmergencyActivateState() @@ -28,15 +28,15 @@ contract EmergencyActivationCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getActionState(_hashEmergencyActivateAction()); + return _getVoteState(_encodeEmergencyActivateData()); } function executeEmergencyActivate() external { - _markExecute(_hashEmergencyActivateAction()); + _markExecuted(_encodeEmergencyActivateData()); IEmergencyProtectedTimelock(EMERGENCY_PROTECTED_TIMELOCK).emergencyActivate(); } - function _hashEmergencyActivateAction() internal view returns (Action memory) { - return keccak256("EMERGENCY_ACTIVATE"); + function _encodeEmergencyActivateData() internal pure returns (bytes memory data) { + data = bytes("EMERGENCY_ACTIVATE"); } } diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol index 866d61d7..c7b7a337 100644 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -23,7 +23,7 @@ contract EmergencyExecutionCommittee is ExecutiveCommittee { // Emergency Execution function voteEmergencyExecute(uint256 _proposalId, bool _supports) public onlyMember { - _vote(_buildEmergencyExecuteAction(_proposalId), _supports); + _vote(_encodeEmergencyExecuteData(_proposalId), _supports); } function getEmergencyExecuteState(uint256 _proposalId) @@ -31,17 +31,18 @@ contract EmergencyExecutionCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getActionState(_buildEmergencyExecuteAction(_proposalId)); + return _getVoteState(_encodeEmergencyExecuteData(_proposalId)); } function executeEmergencyExecute(uint256 _proposalId) public { - _execute(_buildEmergencyExecuteAction(_proposalId)); + _markExecuted(_encodeEmergencyExecuteData(_proposalId)); + IEmergencyProtectedTimelock(EMERGENCY_PROTECTED_TIMELOCK).emergencyExecute(_proposalId); } // Governance reset function approveEmergencyReset() public onlyMember { - _vote(_buildEmergencyResetAction(), true); + _vote(_dataEmergencyResetData(), true); } function getEmergencyResetState() @@ -49,20 +50,19 @@ contract EmergencyExecutionCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getActionState(_buildEmergencyResetAction()); + return _getVoteState(_dataEmergencyResetData()); } function executeEmergencyReset() external { - _execute(_buildEmergencyResetAction()); + _markExecuted(_dataEmergencyResetData()); + IEmergencyProtectedTimelock(EMERGENCY_PROTECTED_TIMELOCK).emergencyReset(); } - function _buildEmergencyResetAction() internal view returns (Action memory) { - return Action(EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSignature("emergencyReset()"), new bytes(0)); + function _dataEmergencyResetData() internal pure returns (bytes memory data) { + data = bytes("EMERGENCY_RESET"); } - function _buildEmergencyExecuteAction(uint256 proposalId) internal view returns (Action memory) { - return Action( - EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSignature("emergencyExecute(uint256)", proposalId), new bytes(0) - ); + function _encodeEmergencyExecuteData(uint256 proposalId) internal pure returns (bytes memory data) { + data = abi.encode(proposalId); } } diff --git a/contracts/committees/ExecutiveCommittee.sol b/contracts/committees/ExecutiveCommittee.sol index 27d2b410..7cd42352 100644 --- a/contracts/committees/ExecutiveCommittee.sol +++ b/contracts/committees/ExecutiveCommittee.sol @@ -10,8 +10,8 @@ abstract contract ExecutiveCommittee { event MemberAdded(address indexed member); event MemberRemoved(address indexed member); event QuorumSet(uint256 quorum); - event VoteExecuted(address indexed to, bytes data); - event Voted(address indexed signer, bool support, address indexed to, bytes data); + event VoteExecuted(bytes data); + event Voted(address indexed signer, bytes data, bool support); error IsNotMember(); error SenderIsNotMember(); @@ -26,7 +26,12 @@ abstract contract ExecutiveCommittee { EnumerableSet.AddressSet private members; uint256 public quorum; - mapping(bytes32 digest => bool isEecuted) public voteStates; + struct VoteState { + bytes data; + bool isExecuted; + } + + mapping(bytes32 digest => VoteState) public voteStates; mapping(address signer => mapping(bytes32 digest => bool support)) public approves; constructor(address owner, address[] memory newMembers, uint256 executionQuorum) { @@ -46,8 +51,14 @@ abstract contract ExecutiveCommittee { } } - function _vote(bytes32 digest, bool support) internal { - if (voteStates[digest] == true) { + function _vote(bytes memory data, bool support) internal { + bytes32 digest = keccak256(data); + + if (voteStates[digest].data.length == 0) { + voteStates[digest].data = data; + } + + if (voteStates[digest].isExecuted == true) { revert VoteAlreadyExecuted(); } @@ -56,30 +67,34 @@ abstract contract ExecutiveCommittee { } approves[msg.sender][digest] = support; - emit Voted(msg.sender, digest); + emit Voted(msg.sender, data, support); } - function _markExecuted(bytes32 digest) internal { - if (voteStates[digest] == true) { + function _markExecuted(bytes memory data) internal { + bytes32 digest = keccak256(data); + + if (voteStates[digest].isExecuted == true) { revert VoteAlreadyExecuted(); } if (_getSupport(digest) < quorum) { revert QuorumIsNotReached(); } - voteStates[digest] = true; + voteStates[digest].isExecuted = true; - emit VoteExecuted(digest); + emit VoteExecuted(data); } - function _getVoteState(bytes32 digest) + function _getVoteState(bytes memory data) internal view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { + bytes32 digest = keccak256(data); + support = _getSupport(digest); execuitionQuorum = quorum; - isExecuted = voteStates[digest]; + isExecuted = voteStates[digest].isExecuted; } function addMember(address newMember, uint256 newQuorum) public onlyOwner { diff --git a/contracts/committees/ResealCommittee.sol b/contracts/committees/ResealCommittee.sol index 8f5347d7..a1fcb602 100644 --- a/contracts/committees/ResealCommittee.sol +++ b/contracts/committees/ResealCommittee.sol @@ -18,7 +18,7 @@ contract ResealCommittee is ExecutiveCommittee { } function voteReseal(address[] memory sealables, bool support) public onlyMember { - _vote(_buildResealAction(sealables), support); + _vote(_encodeResealData(sealables), support); } function getResealState(address[] memory sealables) @@ -26,21 +26,17 @@ contract ResealCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return getActionState(_buildResealAction(sealables)); + return _getVoteState(_encodeResealData(sealables)); } function executeReseal(address[] memory sealables) external { - _execute(_buildResealAction(sealables)); + _markExecuted(_encodeResealData(sealables)); bytes32 resealNonceHash = keccak256(abi.encode(sealables)); _resealNonces[resealNonceHash]++; } - function _buildResealAction(address[] memory sealables) internal view returns (Action memory) { + function _encodeResealData(address[] memory sealables) internal view returns (bytes memory data) { bytes32 resealNonceHash = keccak256(abi.encode(sealables)); - return Action( - RESEAL_EXECUTOR, - abi.encodeWithSignature("reseal(address[])", sealables), - abi.encode(_resealNonces[resealNonceHash]) - ); + data = abi.encode(sealables, _resealNonces[resealNonceHash]); } } diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol index 616ff98c..e9f5ebbe 100644 --- a/contracts/committees/TiebreakerCore.sol +++ b/contracts/committees/TiebreakerCore.sol @@ -3,6 +3,11 @@ pragma solidity 0.8.23; import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; +interface IDualGovernance { + function tiebreakerApproveProposal(uint256 proposalId) external; + function tiebreakerApproveSealableResume(address sealable) external; +} + contract TiebreakerCore is ExecutiveCommittee { error ResumeSealableNonceMismatch(); @@ -21,20 +26,21 @@ contract TiebreakerCore is ExecutiveCommittee { // Approve proposal - function approveProposal(uint256 _proposalId) public onlyMember { - _vote(_buildApproveProposalAction(_proposalId), true); + function approveProposal(uint256 proposalId) public onlyMember { + _vote(_encodeAproveProposalData(proposalId), true); } - function getApproveProposalState(uint256 _proposalId) + function getApproveProposalState(uint256 proposalId) public view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getActionState(_buildApproveProposalAction(_proposalId)); + return _getVoteState(_encodeAproveProposalData(proposalId)); } - function executeApproveProposal(uint256 _proposalId) public { - _execute(_buildApproveProposalAction(_proposalId)); + function executeApproveProposal(uint256 proposalId) public { + _markExecuted(_encodeAproveProposalData(proposalId)); + IDualGovernance(DUAL_GOVERNANCE).tiebreakerApproveProposal(proposalId); } // Resume sealable @@ -47,32 +53,27 @@ contract TiebreakerCore is ExecutiveCommittee { if (nonce != _sealableResumeNonces[sealable]) { revert ResumeSealableNonceMismatch(); } - _vote(_buildSealableResumeAction(sealable, nonce), true); + _vote(_encodeSealableResumeData(sealable, nonce), true); } function getSealableResumeState( address sealable, uint256 nonce ) public view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getActionState(_buildSealableResumeAction(sealable, nonce)); + return _getVoteState(_encodeSealableResumeData(sealable, nonce)); } function executeSealableResume(address sealable) external { - _execute(_buildSealableResumeAction(sealable, getSealableResumeNonce(sealable))); + _markExecuted(_encodeSealableResumeData(sealable, _sealableResumeNonces[sealable])); _sealableResumeNonces[sealable]++; + IDualGovernance(DUAL_GOVERNANCE).tiebreakerApproveSealableResume(sealable); } - function _buildApproveProposalAction(uint256 _proposalId) internal view returns (Action memory) { - return Action( - DUAL_GOVERNANCE, abi.encodeWithSignature("tiebreakerApproveProposal(uint256)", _proposalId), new bytes(0) - ); + function _encodeAproveProposalData(uint256 proposalId) internal pure returns (bytes memory data) { + data = abi.encode(proposalId); } - function _buildSealableResumeAction(address sealable, uint256 nonce) internal view returns (Action memory) { - return Action( - DUAL_GOVERNANCE, - abi.encodeWithSignature("tiebreakerApproveSealableResume(address)", sealable), - abi.encode(nonce) - ); + function _encodeSealableResumeData(address sealable, uint256 nonce) internal pure returns (bytes memory data) { + data = abi.encode(sealable, nonce); } } diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol index aa86f68d..6b4bafb0 100644 --- a/contracts/committees/TiebreakerSubCommittee.sol +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -5,6 +5,8 @@ import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; interface ITiebreakerCore { function getSealableResumeNonce(address sealable) external view returns (uint256 nonce); + function approveProposal(uint256 _proposalId) external; + function approveSealableResume(address sealable, uint256 nonce) external; } contract TiebreakerSubCommittee is ExecutiveCommittee { @@ -22,7 +24,7 @@ contract TiebreakerSubCommittee is ExecutiveCommittee { // Approve proposal function voteApproveProposal(uint256 proposalId, bool support) public onlyMember { - _vote(_buildApproveProposalAction(proposalId), support); + _vote(_encodeApproveProposalData(proposalId), support); } function getApproveProposalState(uint256 proposalId) @@ -30,17 +32,18 @@ contract TiebreakerSubCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getActionState(_buildApproveProposalAction(proposalId)); + return _getVoteState(_encodeApproveProposalData(proposalId)); } function executeApproveProposal(uint256 proposalId) public { - _execute(_buildApproveProposalAction(proposalId)); + _markExecuted(_encodeApproveProposalData(proposalId)); + ITiebreakerCore(TIEBREAKER_CORE).approveProposal(proposalId); } // Approve unpause sealable function voteApproveSealableResume(address sealable, bool support) public { - _vote(_buildApproveSealableResumeAction(sealable), support); + _vote(_encodeApproveSealableResumeData(sealable), support); } function getApproveSealableResumeState(address sealable) @@ -48,23 +51,21 @@ contract TiebreakerSubCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getActionState(_buildApproveSealableResumeAction(sealable)); + return _getVoteState(_encodeApproveSealableResumeData(sealable)); } function executeApproveSealableResume(address sealable) public { - _execute(_buildApproveSealableResumeAction(sealable)); + _markExecuted(_encodeApproveSealableResumeData(sealable)); + uint256 nonce = ITiebreakerCore(TIEBREAKER_CORE).getSealableResumeNonce(sealable); + ITiebreakerCore(TIEBREAKER_CORE).approveSealableResume(sealable, nonce); } - function _buildApproveSealableResumeAction(address sealable) internal view returns (Action memory) { + function _encodeApproveSealableResumeData(address sealable) internal view returns (bytes memory data) { uint256 nonce = ITiebreakerCore(TIEBREAKER_CORE).getSealableResumeNonce(sealable); - return Action( - TIEBREAKER_CORE, - abi.encodeWithSignature("approveSealableResume(address,uint256)", sealable, nonce), - new bytes(0) - ); + data = abi.encode(sealable, nonce); } - function _buildApproveProposalAction(uint256 proposalId) internal view returns (Action memory) { - return Action(TIEBREAKER_CORE, abi.encodeWithSignature("approveProposal(uint256)", proposalId), new bytes(0)); + function _encodeApproveProposalData(uint256 proposalId) internal pure returns (bytes memory data) { + data = abi.encode(proposalId); } } diff --git a/test/unit/ExecutiveCommittee.t.sol b/test/unit/ExecutiveCommittee.t.sol index 1e6bab5b..a3601cfb 100644 --- a/test/unit/ExecutiveCommittee.t.sol +++ b/test/unit/ExecutiveCommittee.t.sol @@ -161,51 +161,45 @@ abstract contract ExecutiveCommitteeUnitTest is UnitTest { } } +contract Target { + event Executed(); + + function trigger() public { + emit Executed(); + } +} + contract ExecutiveCommitteeWrapper is ExecutiveCommittee { + Target internal _target; + constructor( address owner, address[] memory newMembers, - uint256 executionQuorum - ) ExecutiveCommittee(owner, newMembers, executionQuorum) {} - - function vote(Action memory action, bool support) public { - _vote(action, support); + uint256 executionQuorum, + Target target + ) ExecutiveCommittee(owner, newMembers, executionQuorum) { + _target = target; } - function execute(Action memory action) public { - _execute(action); + function vote(bytes calldata data, bool support) public { + _vote(data, support); } - function getActionState(Action memory action) - public - view - returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) - { - return _getActionState(action); + function execute(bytes calldata data) public { + _markExecuted(data); + _target.trigger(); } - function getSupport(bytes32 actionHash) public view returns (uint256 support) { - return _getSupport(actionHash); - } - - function getAndCheckStoredActionState(Action memory action) + function getVoteState(bytes calldata data) public view - returns (ActionState memory storedActionState, bytes32 actionHash) + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getAndCheckStoredActionState(action); + return _getVoteState(data); } - function hashAction(Action memory action) public pure returns (bytes32) { - return _hashAction(action); - } -} - -contract Target { - event Executed(); - - function trigger() public { - emit Executed(); + function getSupport(bytes32 voteHash) public view returns (uint256 support) { + return _getSupport(voteHash); } } @@ -215,213 +209,145 @@ contract ExecutiveCommitteeInternalUnitTest is ExecutiveCommitteeUnitTest { function setUp() public { _target = new Target(); - _executiveCommitteeWrapper = new ExecutiveCommitteeWrapper(_owner, _committeeMembers, _quorum); + _executiveCommitteeWrapper = new ExecutiveCommitteeWrapper(_owner, _committeeMembers, _quorum, _target); _executiveCommittee = ExecutiveCommittee(_executiveCommitteeWrapper); } - function test_hashAction() public { - ExecutiveCommittee.Action memory action = ExecutiveCommittee.Action(address(1), new bytes(10), new bytes(100)); - - bytes32 actionHash = keccak256(abi.encode(action.to, action.data, action.salt)); - - assertEq(_executiveCommitteeWrapper.hashAction(action), actionHash); - } - function test_getSupport() public { - ExecutiveCommittee.Action memory action = ExecutiveCommittee.Action(address(1), new bytes(10), new bytes(100)); - bytes32 actionHash = keccak256(abi.encode(action.to, action.data, action.salt)); + bytes memory data = abi.encode(address(_target)); + bytes32 dataHash = keccak256(data); - assertEq(_executiveCommitteeWrapper.getSupport(actionHash), 0); + assertEq(_executiveCommitteeWrapper.getSupport(dataHash), 0); for (uint256 i = 0; i < _membersCount; ++i) { - assertEq(_executiveCommitteeWrapper.getSupport(actionHash), i); + assertEq(_executiveCommitteeWrapper.getSupport(dataHash), i); vm.prank(_committeeMembers[i]); - _executiveCommitteeWrapper.vote(action, true); - assertEq(_executiveCommitteeWrapper.getSupport(actionHash), i + 1); + _executiveCommitteeWrapper.vote(data, true); + assertEq(_executiveCommitteeWrapper.getSupport(dataHash), i + 1); } - assertEq(_executiveCommitteeWrapper.getSupport(actionHash), _membersCount); + assertEq(_executiveCommitteeWrapper.getSupport(dataHash), _membersCount); for (uint256 i = 0; i < _membersCount; ++i) { - assertEq(_executiveCommitteeWrapper.getSupport(actionHash), _membersCount - i); + assertEq(_executiveCommitteeWrapper.getSupport(dataHash), _membersCount - i); vm.prank(_committeeMembers[i]); - _executiveCommitteeWrapper.vote(action, false); - assertEq(_executiveCommitteeWrapper.getSupport(actionHash), _membersCount - i - 1); + _executiveCommitteeWrapper.vote(data, false); + assertEq(_executiveCommitteeWrapper.getSupport(dataHash), _membersCount - i - 1); } - assertEq(_executiveCommitteeWrapper.getSupport(actionHash), 0); - } - - function test_getAndCheckActionState() public { - address to = address(_target); - bytes memory data = abi.encodeWithSelector(Target.trigger.selector); - bytes memory salt = abi.encodePacked(hex"beaf"); - - ExecutiveCommittee.Action memory action = ExecutiveCommittee.Action(to, data, salt); - bytes32 actionHash = keccak256(abi.encode(action.to, action.data, action.salt)); - - ExecutiveCommittee.ActionState memory storedActionStateFromContract; - bytes32 actionHashFromContract; - - vm.expectRevert(abi.encodeWithSignature("ActionMismatch()")); - _executiveCommitteeWrapper.getAndCheckStoredActionState(action); - - vm.prank(_committeeMembers[0]); - _executiveCommitteeWrapper.vote(action, false); - - (storedActionStateFromContract, actionHashFromContract) = - _executiveCommitteeWrapper.getAndCheckStoredActionState(action); - assertEq(storedActionStateFromContract.isExecuted, false); - assertEq(storedActionStateFromContract.action.to, to); - assertEq(storedActionStateFromContract.action.data, data); - assertEq(storedActionStateFromContract.action.salt, salt); - assertEq(actionHashFromContract, actionHash); - - for (uint256 i = 0; i < _membersCount; ++i) { - vm.prank(_committeeMembers[i]); - _executiveCommitteeWrapper.vote(action, true); - } - - _executiveCommitteeWrapper.execute(action); - - vm.expectRevert(abi.encodeWithSignature("ActionAlreadyExecuted()")); - _executiveCommitteeWrapper.getAndCheckStoredActionState(action); + assertEq(_executiveCommitteeWrapper.getSupport(dataHash), 0); } - function test_getActionState() public { - address to = address(_target); - bytes memory data = abi.encodeWithSelector(Target.trigger.selector); - bytes memory salt = abi.encodePacked(hex"beaf"); - - ExecutiveCommittee.Action memory action = ExecutiveCommittee.Action(to, data, salt); - - vm.prank(_committeeMembers[0]); - _executiveCommitteeWrapper.vote(action, false); + function test_getVoteState() public { + bytes memory data = abi.encode(address(_target)); uint256 support; uint256 execuitionQuorum; bool isExecuted; - (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getActionState(action); + (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getVoteState(data); assertEq(support, 0); assertEq(execuitionQuorum, _quorum); assertEq(isExecuted, false); for (uint256 i = 0; i < _membersCount; ++i) { - (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getActionState(action); + (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getVoteState(data); assertEq(support, i); assertEq(execuitionQuorum, _quorum); assertEq(isExecuted, false); vm.prank(_committeeMembers[i]); - _executiveCommitteeWrapper.vote(action, true); + _executiveCommitteeWrapper.vote(data, true); - (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getActionState(action); + (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getVoteState(data); assertEq(support, i + 1); assertEq(execuitionQuorum, _quorum); assertEq(isExecuted, false); } - (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getActionState(action); + (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getVoteState(data); assertEq(support, _membersCount); assertEq(execuitionQuorum, _quorum); assertEq(isExecuted, false); - _executiveCommitteeWrapper.execute(action); + _executiveCommitteeWrapper.execute(data); - vm.expectRevert(abi.encodeWithSignature("ActionAlreadyExecuted()")); - _executiveCommitteeWrapper.getActionState(action); + (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getVoteState(data); + assertEq(support, _membersCount); + assertEq(execuitionQuorum, _quorum); + assertEq(isExecuted, true); } function test_vote() public { - address to = address(_target); - bytes memory data = abi.encodeWithSelector(Target.trigger.selector); - bytes memory salt = abi.encodePacked(hex"beaf"); + bytes memory data = abi.encode(address(_target)); - ExecutiveCommittee.Action memory action = ExecutiveCommittee.Action(to, data, salt); - bytes32 actionHash = keccak256(abi.encode(action.to, action.data, action.salt)); + bytes32 dataHash = keccak256(data); - assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], actionHash), false); + assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], dataHash), false); vm.prank(_committeeMembers[0]); vm.expectEmit(address(_executiveCommitteeWrapper)); - emit ExecutiveCommittee.ActionVoted(_committeeMembers[0], true, to, data); - _executiveCommitteeWrapper.vote(action, true); - assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], actionHash), true); + emit ExecutiveCommittee.Voted(_committeeMembers[0], data, true); + _executiveCommitteeWrapper.vote(data, true); + assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], dataHash), true); vm.prank(_committeeMembers[0]); vm.recordLogs(); - _executiveCommitteeWrapper.vote(action, true); + _executiveCommitteeWrapper.vote(data, true); Vm.Log[] memory logs = vm.getRecordedLogs(); assertEq(logs.length, 0); - assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], actionHash), true); + assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], dataHash), true); vm.prank(_committeeMembers[0]); vm.expectEmit(address(_executiveCommitteeWrapper)); - emit ExecutiveCommittee.ActionVoted(_committeeMembers[0], false, to, data); - _executiveCommitteeWrapper.vote(action, false); - assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], actionHash), false); + emit ExecutiveCommittee.Voted(_committeeMembers[0], data, false); + _executiveCommitteeWrapper.vote(data, false); + assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], dataHash), false); vm.prank(_committeeMembers[0]); vm.recordLogs(); - _executiveCommitteeWrapper.vote(action, false); + _executiveCommitteeWrapper.vote(data, false); logs = vm.getRecordedLogs(); assertEq(logs.length, 0); - assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], actionHash), false); + assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], dataHash), false); } function test_vote_reverts_on_executed() public { - address to = address(_target); - bytes memory data = abi.encodeWithSelector(Target.trigger.selector); - bytes memory salt = abi.encodePacked(hex"beaf"); - - ExecutiveCommittee.Action memory action = ExecutiveCommittee.Action(to, data, salt); + bytes memory data = abi.encode(address(_target)); for (uint256 i = 0; i < _quorum; ++i) { vm.prank(_committeeMembers[i]); - _executiveCommitteeWrapper.vote(action, true); + _executiveCommitteeWrapper.vote(data, true); } - _executiveCommitteeWrapper.execute(action); + _executiveCommitteeWrapper.execute(data); vm.prank(_committeeMembers[0]); - vm.expectRevert(abi.encodeWithSignature("ActionAlreadyExecuted()")); - _executiveCommitteeWrapper.vote(action, true); + vm.expectRevert(abi.encodeWithSignature("VoteAlreadyExecuted()")); + _executiveCommitteeWrapper.vote(data, true); } function test_execute_events() public { - address to = address(_target); - bytes memory data = abi.encodeWithSelector(Target.trigger.selector); - bytes memory salt = abi.encodePacked(hex"beaf"); - - ExecutiveCommittee.Action memory action = ExecutiveCommittee.Action(to, data, salt); - - vm.prank(_stranger); - vm.expectRevert(abi.encodeWithSignature("ActionMismatch()")); - _executiveCommitteeWrapper.execute(action); - - vm.prank(_committeeMembers[0]); - _executiveCommitteeWrapper.vote(action, true); + bytes memory data = abi.encode(address(_target)); vm.prank(_stranger); vm.expectRevert(abi.encodeWithSignature("QuorumIsNotReached()")); - _executiveCommitteeWrapper.execute(action); + _executiveCommitteeWrapper.execute(data); for (uint256 i = 0; i < _quorum; ++i) { vm.prank(_committeeMembers[i]); - _executiveCommitteeWrapper.vote(action, true); + _executiveCommitteeWrapper.vote(data, true); } vm.prank(_stranger); + vm.expectEmit(address(_executiveCommitteeWrapper)); + emit ExecutiveCommittee.VoteExecuted(data); vm.expectEmit(address(_target)); emit Target.Executed(); - vm.expectEmit(address(_executiveCommitteeWrapper)); - emit ExecutiveCommittee.ActionExecuted(to, data); - - _executiveCommitteeWrapper.execute(action); + _executiveCommitteeWrapper.execute(data); vm.prank(_stranger); - vm.expectRevert(abi.encodeWithSignature("ActionAlreadyExecuted()")); - _executiveCommitteeWrapper.execute(action); + vm.expectRevert(abi.encodeWithSignature("VoteAlreadyExecuted()")); + _executiveCommitteeWrapper.execute(data); } } From 714bf5acb5cfd34c0585140ecaf9eb9aec794f41 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Tue, 25 Jun 2024 12:06:21 +0300 Subject: [PATCH 31/38] Committee timlock --- .../{ResealExecutor.sol => ResealManager.sol} | 36 +++++++++++++++---- .../EmergencyActivationCommittee.sol | 2 +- .../EmergencyExecutionCommittee.sol | 2 +- contracts/committees/ExecutiveCommittee.sol | 22 +++++++++++- contracts/committees/ResealCommittee.sol | 5 +-- contracts/committees/TiebreakerCore.sol | 2 +- .../committees/TiebreakerSubCommittee.sol | 2 +- test/unit/ExecutiveCommittee.t.sol | 7 ++-- 8 files changed, 63 insertions(+), 15 deletions(-) rename contracts/{ResealExecutor.sol => ResealManager.sol} (50%) diff --git a/contracts/ResealExecutor.sol b/contracts/ResealManager.sol similarity index 50% rename from contracts/ResealExecutor.sol rename to contracts/ResealManager.sol index e3411e75..32d3671d 100644 --- a/contracts/ResealExecutor.sol +++ b/contracts/ResealManager.sol @@ -6,18 +6,22 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ISealable} from "./interfaces/ISealable.sol"; contract ResealExecutor is Ownable { - error SenderIsNotCommittee(); - error DualGovernanceInNormalState(); error SealableWrongPauseState(); + error SenderIsNotManager(); + + event ManagerSet(address newManager); uint256 public constant PAUSE_INFINITELY = type(uint256).max; - address public immutable DUAL_GOVERNANCE; - constructor(address owner, address dualGovernance) Ownable(owner) { - DUAL_GOVERNANCE = dualGovernance; + address public manager; + + constructor(address owner, address managerAddress) Ownable(owner) { + manager = managerAddress; + + emit ManagerSet(managerAddress); } - function reseal(address[] memory sealables) public onlyOwner { + function reseal(address[] memory sealables) public onlyManager { for (uint256 i = 0; i < sealables.length; ++i) { uint256 sealableResumeSinceTimestamp = ISealable(sealables[i]).getResumeSinceTimestamp(); if (sealableResumeSinceTimestamp < block.timestamp || sealableResumeSinceTimestamp == PAUSE_INFINITELY) { @@ -27,4 +31,24 @@ contract ResealExecutor is Ownable { Address.functionCall(sealables[i], abi.encodeWithSelector(ISealable.pauseFor.selector, PAUSE_INFINITELY)); } } + + function resume(address sealable) public onlyManager { + uint256 sealableResumeSinceTimestamp = ISealable(sealable).getResumeSinceTimestamp(); + if (sealableResumeSinceTimestamp < block.timestamp) { + revert SealableWrongPauseState(); + } + Address.functionCall(sealable, abi.encodeWithSelector(ISealable.resume.selector)); + } + + function setManager(address newManager) public onlyOwner { + manager = newManager; + emit ManagerSet(newManager); + } + + modifier onlyManager() { + if (msg.sender != manager) { + revert SenderIsNotManager(); + } + _; + } } diff --git a/contracts/committees/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol index f7ecdcfc..81935c62 100644 --- a/contracts/committees/EmergencyActivationCommittee.sol +++ b/contracts/committees/EmergencyActivationCommittee.sol @@ -15,7 +15,7 @@ contract EmergencyActivationCommittee is ExecutiveCommittee { address[] memory committeeMembers, uint256 executionQuorum, address emergencyProtectedTimelock - ) ExecutiveCommittee(OWNER, committeeMembers, executionQuorum) { + ) ExecutiveCommittee(OWNER, committeeMembers, executionQuorum, 0) { EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; } diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol index c7b7a337..83a23de0 100644 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -16,7 +16,7 @@ contract EmergencyExecutionCommittee is ExecutiveCommittee { address[] memory committeeMembers, uint256 executionQuorum, address emergencyProtectedTimelock - ) ExecutiveCommittee(OWNER, committeeMembers, executionQuorum) { + ) ExecutiveCommittee(OWNER, committeeMembers, executionQuorum, 0) { EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; } diff --git a/contracts/committees/ExecutiveCommittee.sol b/contracts/committees/ExecutiveCommittee.sol index 7cd42352..084e82fa 100644 --- a/contracts/committees/ExecutiveCommittee.sol +++ b/contracts/committees/ExecutiveCommittee.sol @@ -12,6 +12,7 @@ abstract contract ExecutiveCommittee { event QuorumSet(uint256 quorum); event VoteExecuted(bytes data); event Voted(address indexed signer, bytes data, bool support); + event TimelockDurationSet(uint256 timelockDuration); error IsNotMember(); error SenderIsNotMember(); @@ -20,21 +21,24 @@ abstract contract ExecutiveCommittee { error QuorumIsNotReached(); error InvalidQuorum(); error DuplicatedMember(address member); + error TimelockNotPassed(); address public immutable OWNER; EnumerableSet.AddressSet private members; uint256 public quorum; + uint256 public timelockDuration; struct VoteState { bytes data; + uint256 quorumAt; bool isExecuted; } mapping(bytes32 digest => VoteState) public voteStates; mapping(address signer => mapping(bytes32 digest => bool support)) public approves; - constructor(address owner, address[] memory newMembers, uint256 executionQuorum) { + constructor(address owner, address[] memory newMembers, uint256 executionQuorum, uint256 timelock) { if (executionQuorum == 0) { revert InvalidQuorum(); } @@ -43,6 +47,9 @@ abstract contract ExecutiveCommittee { OWNER = owner; + timelockDuration = timelock; + emit TimelockDurationSet(timelock); + for (uint256 i = 0; i < newMembers.length; ++i) { if (members.contains(newMembers[i])) { revert DuplicatedMember(newMembers[i]); @@ -66,6 +73,11 @@ abstract contract ExecutiveCommittee { return; } + uint256 heads = _getSupport(digest); + if (heads == quorum - 1 && support == true) { + voteStates[digest].quorumAt = block.timestamp; + } + approves[msg.sender][digest] = support; emit Voted(msg.sender, data, support); } @@ -79,6 +91,9 @@ abstract contract ExecutiveCommittee { if (_getSupport(digest) < quorum) { revert QuorumIsNotReached(); } + if (block.timestamp < voteStates[digest].quorumAt + timelockDuration) { + revert TimelockNotPassed(); + } voteStates[digest].isExecuted = true; @@ -129,6 +144,11 @@ abstract contract ExecutiveCommittee { return members.contains(member); } + function setTimelockDuration(uint256 timelock) public onlyOwner { + timelockDuration = timelock; + emit TimelockDurationSet(timelock); + } + function _addMember(address newMember) internal { if (members.contains(newMember)) { revert DuplicatedMember(newMember); diff --git a/contracts/committees/ResealCommittee.sol b/contracts/committees/ResealCommittee.sol index a1fcb602..d984211d 100644 --- a/contracts/committees/ResealCommittee.sol +++ b/contracts/committees/ResealCommittee.sol @@ -12,8 +12,9 @@ contract ResealCommittee is ExecutiveCommittee { address owner, address[] memory committeeMembers, uint256 executionQuorum, - address resealExecutor - ) ExecutiveCommittee(owner, committeeMembers, executionQuorum) { + address resealExecutor, + uint256 timelock + ) ExecutiveCommittee(owner, committeeMembers, executionQuorum, timelock) { RESEAL_EXECUTOR = resealExecutor; } diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol index e9f5ebbe..214316f3 100644 --- a/contracts/committees/TiebreakerCore.sol +++ b/contracts/committees/TiebreakerCore.sol @@ -20,7 +20,7 @@ contract TiebreakerCore is ExecutiveCommittee { address[] memory committeeMembers, uint256 executionQuorum, address dualGovernance - ) ExecutiveCommittee(owner, committeeMembers, executionQuorum) { + ) ExecutiveCommittee(owner, committeeMembers, executionQuorum, 0) { DUAL_GOVERNANCE = dualGovernance; } diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol index 6b4bafb0..f61811d1 100644 --- a/contracts/committees/TiebreakerSubCommittee.sol +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -17,7 +17,7 @@ contract TiebreakerSubCommittee is ExecutiveCommittee { address[] memory committeeMembers, uint256 executionQuorum, address tiebreakerCore - ) ExecutiveCommittee(owner, committeeMembers, executionQuorum) { + ) ExecutiveCommittee(owner, committeeMembers, executionQuorum, 0) { TIEBREAKER_CORE = tiebreakerCore; } diff --git a/test/unit/ExecutiveCommittee.t.sol b/test/unit/ExecutiveCommittee.t.sol index a3601cfb..74a64dfa 100644 --- a/test/unit/ExecutiveCommittee.t.sol +++ b/test/unit/ExecutiveCommittee.t.sol @@ -176,8 +176,9 @@ contract ExecutiveCommitteeWrapper is ExecutiveCommittee { address owner, address[] memory newMembers, uint256 executionQuorum, + uint256 timelock, Target target - ) ExecutiveCommittee(owner, newMembers, executionQuorum) { + ) ExecutiveCommittee(owner, newMembers, executionQuorum, timelock) { _target = target; } @@ -206,10 +207,12 @@ contract ExecutiveCommitteeWrapper is ExecutiveCommittee { contract ExecutiveCommitteeInternalUnitTest is ExecutiveCommitteeUnitTest { ExecutiveCommitteeWrapper internal _executiveCommitteeWrapper; Target internal _target; + uint256 _timelock = 3600; function setUp() public { _target = new Target(); - _executiveCommitteeWrapper = new ExecutiveCommitteeWrapper(_owner, _committeeMembers, _quorum, _target); + _executiveCommitteeWrapper = + new ExecutiveCommitteeWrapper(_owner, _committeeMembers, _quorum, _timelock, _target); _executiveCommittee = ExecutiveCommittee(_executiveCommitteeWrapper); } From cf183f930223734a67cf7871f4d2b359f543a0f0 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Tue, 25 Jun 2024 12:15:17 +0300 Subject: [PATCH 32/38] Ownable Executive committee --- contracts/committees/ExecutiveCommittee.sol | 18 +++--------------- test/unit/ExecutiveCommittee.t.sol | 8 ++++---- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/contracts/committees/ExecutiveCommittee.sol b/contracts/committees/ExecutiveCommittee.sol index 084e82fa..d09e5b75 100644 --- a/contracts/committees/ExecutiveCommittee.sol +++ b/contracts/committees/ExecutiveCommittee.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -abstract contract ExecutiveCommittee { +abstract contract ExecutiveCommittee is Ownable { using EnumerableSet for EnumerableSet.AddressSet; event MemberAdded(address indexed member); @@ -16,15 +16,12 @@ abstract contract ExecutiveCommittee { error IsNotMember(); error SenderIsNotMember(); - error SenderIsNotOwner(); error VoteAlreadyExecuted(); error QuorumIsNotReached(); error InvalidQuorum(); error DuplicatedMember(address member); error TimelockNotPassed(); - address public immutable OWNER; - EnumerableSet.AddressSet private members; uint256 public quorum; uint256 public timelockDuration; @@ -38,15 +35,13 @@ abstract contract ExecutiveCommittee { mapping(bytes32 digest => VoteState) public voteStates; mapping(address signer => mapping(bytes32 digest => bool support)) public approves; - constructor(address owner, address[] memory newMembers, uint256 executionQuorum, uint256 timelock) { + constructor(address owner, address[] memory newMembers, uint256 executionQuorum, uint256 timelock) Ownable(owner) { if (executionQuorum == 0) { revert InvalidQuorum(); } quorum = executionQuorum; emit QuorumSet(executionQuorum); - OWNER = owner; - timelockDuration = timelock; emit TimelockDurationSet(timelock); @@ -171,11 +166,4 @@ abstract contract ExecutiveCommittee { } _; } - - modifier onlyOwner() { - if (msg.sender != OWNER) { - revert SenderIsNotOwner(); - } - _; - } } diff --git a/test/unit/ExecutiveCommittee.t.sol b/test/unit/ExecutiveCommittee.t.sol index 74a64dfa..b3e29d94 100644 --- a/test/unit/ExecutiveCommittee.t.sol +++ b/test/unit/ExecutiveCommittee.t.sol @@ -48,12 +48,12 @@ abstract contract ExecutiveCommitteeUnitTest is UnitTest { assertEq(_executiveCommittee.isMember(newMember), false); vm.prank(_stranger); - vm.expectRevert(abi.encodeWithSignature("SenderIsNotOwner()")); + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", _stranger)); _executiveCommittee.addMember(newMember, _quorum); for (uint256 i = 0; i < _membersCount; ++i) { vm.prank(_committeeMembers[i]); - vm.expectRevert(abi.encodeWithSignature("SenderIsNotOwner()")); + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", _committeeMembers[i])); _executiveCommittee.addMember(newMember, _quorum); } } @@ -106,12 +106,12 @@ abstract contract ExecutiveCommitteeUnitTest is UnitTest { assertEq(_executiveCommittee.isMember(memberToRemove), true); vm.prank(_stranger); - vm.expectRevert(abi.encodeWithSignature("SenderIsNotOwner()")); + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", _stranger)); _executiveCommittee.removeMember(memberToRemove, _quorum); for (uint256 i = 0; i < _membersCount; ++i) { vm.prank(_committeeMembers[i]); - vm.expectRevert(abi.encodeWithSignature("SenderIsNotOwner()")); + vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", _committeeMembers[i])); _executiveCommittee.removeMember(memberToRemove, _quorum); } } From d0d7f368da4230d1e3b3484921596e045196e7a5 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Tue, 25 Jun 2024 12:31:14 +0300 Subject: [PATCH 33/38] use Address call --- contracts/ResealManager.sol | 1 - .../EmergencyActivationCommittee.sol | 5 ++++- .../EmergencyExecutionCommittee.sol | 22 ++++++++++++------- contracts/committees/ResealCommittee.sol | 14 +++++++++--- contracts/committees/TiebreakerCore.sol | 9 ++++++-- .../committees/TiebreakerSubCommittee.sol | 9 ++++++-- 6 files changed, 43 insertions(+), 17 deletions(-) diff --git a/contracts/ResealManager.sol b/contracts/ResealManager.sol index 32d3671d..306f73fd 100644 --- a/contracts/ResealManager.sol +++ b/contracts/ResealManager.sol @@ -17,7 +17,6 @@ contract ResealExecutor is Ownable { constructor(address owner, address managerAddress) Ownable(owner) { manager = managerAddress; - emit ManagerSet(managerAddress); } diff --git a/contracts/committees/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol index 81935c62..2f3004ce 100644 --- a/contracts/committees/EmergencyActivationCommittee.sol +++ b/contracts/committees/EmergencyActivationCommittee.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; interface IEmergencyProtectedTimelock { @@ -33,7 +34,9 @@ contract EmergencyActivationCommittee is ExecutiveCommittee { function executeEmergencyActivate() external { _markExecuted(_encodeEmergencyActivateData()); - IEmergencyProtectedTimelock(EMERGENCY_PROTECTED_TIMELOCK).emergencyActivate(); + Address.functionCall( + EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyActivate.selector) + ); } function _encodeEmergencyActivateData() internal pure returns (bytes memory data) { diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol index 83a23de0..d30ad320 100644 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; interface IEmergencyProtectedTimelock { @@ -22,21 +23,24 @@ contract EmergencyExecutionCommittee is ExecutiveCommittee { // Emergency Execution - function voteEmergencyExecute(uint256 _proposalId, bool _supports) public onlyMember { - _vote(_encodeEmergencyExecuteData(_proposalId), _supports); + function voteEmergencyExecute(uint256 proposalId, bool _supports) public onlyMember { + _vote(_encodeEmergencyExecuteData(proposalId), _supports); } - function getEmergencyExecuteState(uint256 _proposalId) + function getEmergencyExecuteState(uint256 proposalId) public view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getVoteState(_encodeEmergencyExecuteData(_proposalId)); + return _getVoteState(_encodeEmergencyExecuteData(proposalId)); } - function executeEmergencyExecute(uint256 _proposalId) public { - _markExecuted(_encodeEmergencyExecuteData(_proposalId)); - IEmergencyProtectedTimelock(EMERGENCY_PROTECTED_TIMELOCK).emergencyExecute(_proposalId); + function executeEmergencyExecute(uint256 proposalId) public { + _markExecuted(_encodeEmergencyExecuteData(proposalId)); + Address.functionCall( + EMERGENCY_PROTECTED_TIMELOCK, + abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyExecute.selector, proposalId) + ); } // Governance reset @@ -55,7 +59,9 @@ contract EmergencyExecutionCommittee is ExecutiveCommittee { function executeEmergencyReset() external { _markExecuted(_dataEmergencyResetData()); - IEmergencyProtectedTimelock(EMERGENCY_PROTECTED_TIMELOCK).emergencyReset(); + Address.functionCall( + EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyReset.selector) + ); } function _dataEmergencyResetData() internal pure returns (bytes memory data) { diff --git a/contracts/committees/ResealCommittee.sol b/contracts/committees/ResealCommittee.sol index d984211d..6ff782e4 100644 --- a/contracts/committees/ResealCommittee.sol +++ b/contracts/committees/ResealCommittee.sol @@ -1,10 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; +interface IDualGovernance { + function reseal(address[] memory sealables) external; +} + contract ResealCommittee is ExecutiveCommittee { - address public immutable RESEAL_EXECUTOR; + address public immutable DUAL_GOVERNANCE; mapping(bytes32 => uint256) private _resealNonces; @@ -12,10 +17,10 @@ contract ResealCommittee is ExecutiveCommittee { address owner, address[] memory committeeMembers, uint256 executionQuorum, - address resealExecutor, + address dualGovernance, uint256 timelock ) ExecutiveCommittee(owner, committeeMembers, executionQuorum, timelock) { - RESEAL_EXECUTOR = resealExecutor; + DUAL_GOVERNANCE = dualGovernance; } function voteReseal(address[] memory sealables, bool support) public onlyMember { @@ -32,6 +37,9 @@ contract ResealCommittee is ExecutiveCommittee { function executeReseal(address[] memory sealables) external { _markExecuted(_encodeResealData(sealables)); + + Address.functionCall(DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.reseal.selector, sealables)); + bytes32 resealNonceHash = keccak256(abi.encode(sealables)); _resealNonces[resealNonceHash]++; } diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol index 214316f3..64704001 100644 --- a/contracts/committees/TiebreakerCore.sol +++ b/contracts/committees/TiebreakerCore.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; interface IDualGovernance { @@ -40,7 +41,9 @@ contract TiebreakerCore is ExecutiveCommittee { function executeApproveProposal(uint256 proposalId) public { _markExecuted(_encodeAproveProposalData(proposalId)); - IDualGovernance(DUAL_GOVERNANCE).tiebreakerApproveProposal(proposalId); + Address.functionCall( + DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.tiebreakerApproveProposal.selector, proposalId) + ); } // Resume sealable @@ -66,7 +69,9 @@ contract TiebreakerCore is ExecutiveCommittee { function executeSealableResume(address sealable) external { _markExecuted(_encodeSealableResumeData(sealable, _sealableResumeNonces[sealable])); _sealableResumeNonces[sealable]++; - IDualGovernance(DUAL_GOVERNANCE).tiebreakerApproveSealableResume(sealable); + Address.functionCall( + DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.tiebreakerApproveSealableResume.selector, sealable) + ); } function _encodeAproveProposalData(uint256 proposalId) internal pure returns (bytes memory data) { diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol index f61811d1..e73cee3c 100644 --- a/contracts/committees/TiebreakerSubCommittee.sol +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; interface ITiebreakerCore { @@ -37,7 +38,9 @@ contract TiebreakerSubCommittee is ExecutiveCommittee { function executeApproveProposal(uint256 proposalId) public { _markExecuted(_encodeApproveProposalData(proposalId)); - ITiebreakerCore(TIEBREAKER_CORE).approveProposal(proposalId); + Address.functionCall( + TIEBREAKER_CORE, abi.encodeWithSelector(ITiebreakerCore.approveProposal.selector, proposalId) + ); } // Approve unpause sealable @@ -57,7 +60,9 @@ contract TiebreakerSubCommittee is ExecutiveCommittee { function executeApproveSealableResume(address sealable) public { _markExecuted(_encodeApproveSealableResumeData(sealable)); uint256 nonce = ITiebreakerCore(TIEBREAKER_CORE).getSealableResumeNonce(sealable); - ITiebreakerCore(TIEBREAKER_CORE).approveSealableResume(sealable, nonce); + Address.functionCall( + TIEBREAKER_CORE, abi.encodeWithSelector(ITiebreakerCore.approveSealableResume.selector, sealable, nonce) + ); } function _encodeApproveSealableResumeData(address sealable) internal view returns (bytes memory data) { From 43bfcdab1af864eddb28ff5398a89134d796381d Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Tue, 25 Jun 2024 12:40:27 +0300 Subject: [PATCH 34/38] resume sealable via dg --- contracts/DualGovernance.sol | 8 ++------ contracts/committees/TiebreakerCore.sol | 4 ++-- contracts/libraries/TiebreakerProtection.sol | 13 +++++++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 2af09f4f..8ac5ad9b 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -147,14 +147,10 @@ contract DualGovernance is IGovernance, ConfigurationProvider { _tiebreaker.approveProposal(proposalId); } - function tiebreakerApproveSealableResume(address sealable) external { + function tiebreakerResumeSealable(address sealable) external { _tiebreaker.checkTiebreakerCommittee(msg.sender); _dgState.checkTiebreak(CONFIG); - Proposer memory proposer = _proposers.get(msg.sender); - ExecutorCall[] memory calls = new ExecutorCall[](1); - calls[0] = ExecutorCall(sealable, 0, abi.encodeWithSelector(ISealable.resume.selector)); - uint256 proposalId = TIMELOCK.submit(proposer.executor, calls); - _tiebreaker.approveSealableResume(proposalId, sealable); + _tiebreaker.resumeSealable(sealable); } function tiebreakerSchedule(uint256 proposalId) external { diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol index 64704001..5a3071b1 100644 --- a/contracts/committees/TiebreakerCore.sol +++ b/contracts/committees/TiebreakerCore.sol @@ -6,7 +6,7 @@ import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; interface IDualGovernance { function tiebreakerApproveProposal(uint256 proposalId) external; - function tiebreakerApproveSealableResume(address sealable) external; + function tiebreakerResumeSealable(address sealable) external; } contract TiebreakerCore is ExecutiveCommittee { @@ -70,7 +70,7 @@ contract TiebreakerCore is ExecutiveCommittee { _markExecuted(_encodeSealableResumeData(sealable, _sealableResumeNonces[sealable])); _sealableResumeNonces[sealable]++; Address.functionCall( - DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.tiebreakerApproveSealableResume.selector, sealable) + DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.tiebreakerResumeSealable.selector, sealable) ); } diff --git a/contracts/libraries/TiebreakerProtection.sol b/contracts/libraries/TiebreakerProtection.sol index 4ce4dd24..0f323dd5 100644 --- a/contracts/libraries/TiebreakerProtection.sol +++ b/contracts/libraries/TiebreakerProtection.sol @@ -1,16 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +interface IResealManger { + function resume(address sealable) external; +} + library TiebreakerProtection { struct Tiebreaker { address tiebreaker; + address resealManager; uint256 tiebreakerProposalApprovalTimelock; mapping(uint256 proposalId => uint256) tiebreakerProposalApprovalTimestamp; } event TiebreakerSet(address tiebreakCommittee); event ProposalApprovedForExecution(uint256 proposalId); - event SealableResumeApproved(address sealable); + event SealableResumed(address sealable); error ProposalNotExecutable(uint256 proposalId); error NotTiebreaker(address account, address tiebreakCommittee); @@ -28,9 +33,9 @@ library TiebreakerProtection { _approveProposal(self, proposalId); } - function approveSealableResume(Tiebreaker storage self, uint256 proposalId, address sealable) internal { - _approveProposal(self, proposalId); - emit SealableResumeApproved(sealable); + function resumeSealable(Tiebreaker storage self, address sealable) internal { + IResealManger(self.resealManager).resume(sealable); + emit SealableResumed(sealable); } function canSchedule(Tiebreaker storage self, uint256 proposalId) internal view { From b49abf327527c1396e1808154656604aee03b2b7 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Tue, 25 Jun 2024 13:19:01 +0300 Subject: [PATCH 35/38] reseal committee via DG --- contracts/DualGovernance.sol | 29 ++++++++++++++++---- contracts/interfaces/IResealManager.sol | 7 +++++ contracts/libraries/DualGovernanceState.sol | 7 +++++ contracts/libraries/TiebreakerProtection.sol | 10 +++++-- 4 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 contracts/interfaces/IResealManager.sol diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 8ac5ad9b..a2afbb28 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.23; import {ITimelock, IGovernance} from "./interfaces/ITimelock.sol"; import {ISealable} from "./interfaces/ISealable.sol"; +import {IResealManager} from "./interfaces/IResealManager.sol"; import {ConfigurationProvider} from "./ConfigurationProvider.sol"; import {Proposers, Proposer} from "./libraries/Proposers.sol"; @@ -18,7 +19,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { event ProposalScheduled(uint256 proposalId); - error NotTiebreaker(address account, address tiebreakCommittee); + error NotResealCommitttee(address account); ITimelock public immutable TIMELOCK; @@ -26,6 +27,8 @@ contract DualGovernance is IGovernance, ConfigurationProvider { Proposers.State internal _proposers; DualGovernanceState.Store internal _dgState; EmergencyProtection.State internal _emergencyProtection; + address internal _resealCommittee; + IResealManager internal _resealManager; constructor( address config, @@ -159,12 +162,26 @@ contract DualGovernance is IGovernance, ConfigurationProvider { TIMELOCK.schedule(proposalId); } - function setTiebreakerProtection(address newTiebreaker) external { + function setTiebreakerProtection(address newTiebreaker, address resealManager) external { _checkAdminExecutor(msg.sender); - if (_tiebreaker.tiebreaker != address(0)) { - _proposers.unregister(CONFIG, _tiebreaker.tiebreaker); + _tiebreaker.setTiebreaker(newTiebreaker, resealManager); + } + + // --- + // Reseal executor + // --- + + function resealSealables(address[] memory sealables) external { + if (msg.sender != _resealCommittee) { + revert NotResealCommitttee(msg.sender); } - _proposers.register(newTiebreaker, CONFIG.ADMIN_EXECUTOR()); // TODO: check what executor should be. Reseal executor? - _tiebreaker.setTiebreaker(newTiebreaker); + _dgState.checkResealState(); + _resealManager.reseal(sealables); + } + + function setReseal(address resealManager, address resealCommittee) external { + _checkAdminExecutor(msg.sender); + _resealCommittee = resealCommittee; + _resealManager = IResealManager(resealManager); } } diff --git a/contracts/interfaces/IResealManager.sol b/contracts/interfaces/IResealManager.sol new file mode 100644 index 00000000..ca1f2ee4 --- /dev/null +++ b/contracts/interfaces/IResealManager.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +interface IResealManager { + function resume(address sealable) external; + function reseal(address[] memory sealables) external; +} diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol index b55499f4..19efcbc0 100644 --- a/contracts/libraries/DualGovernanceState.sol +++ b/contracts/libraries/DualGovernanceState.sol @@ -37,6 +37,7 @@ library DualGovernanceState { error AlreadyInitialized(); error ProposalsCreationSuspended(); error ProposalsAdoptionSuspended(); + error ResealIsNotAllowedInNormalState(); event NewSignallingEscrowDeployed(address indexed escrow); event DualGovernanceStateChanged(State oldState, State newState); @@ -102,6 +103,12 @@ library DualGovernanceState { } } + function checkResealState(Store storage self) internal view { + if (self.state == State.Normal) { + revert ResealIsNotAllowedInNormalState(); + } + } + function currentState(Store storage self) internal view returns (State) { return self.state; } diff --git a/contracts/libraries/TiebreakerProtection.sol b/contracts/libraries/TiebreakerProtection.sol index 0f323dd5..ce9d3084 100644 --- a/contracts/libraries/TiebreakerProtection.sol +++ b/contracts/libraries/TiebreakerProtection.sol @@ -8,7 +8,7 @@ interface IResealManger { library TiebreakerProtection { struct Tiebreaker { address tiebreaker; - address resealManager; + IResealManger resealManager; uint256 tiebreakerProposalApprovalTimelock; mapping(uint256 proposalId => uint256) tiebreakerProposalApprovalTimestamp; } @@ -16,6 +16,7 @@ library TiebreakerProtection { event TiebreakerSet(address tiebreakCommittee); event ProposalApprovedForExecution(uint256 proposalId); event SealableResumed(address sealable); + event ResealManagerSet(address resealManager); error ProposalNotExecutable(uint256 proposalId); error NotTiebreaker(address account, address tiebreakCommittee); @@ -34,7 +35,7 @@ library TiebreakerProtection { } function resumeSealable(Tiebreaker storage self, address sealable) internal { - IResealManger(self.resealManager).resume(sealable); + self.resealManager.resume(sealable); emit SealableResumed(sealable); } @@ -50,13 +51,16 @@ library TiebreakerProtection { } } - function setTiebreaker(Tiebreaker storage self, address tiebreaker) internal { + function setTiebreaker(Tiebreaker storage self, address tiebreaker, address resealManager) internal { if (self.tiebreaker == tiebreaker) { revert TieBreakerAddressIsSame(); } self.tiebreaker = tiebreaker; emit TiebreakerSet(tiebreaker); + + self.resealManager = IResealManger(resealManager); + emit ResealManagerSet(resealManager); } function checkTiebreakerCommittee(Tiebreaker storage self, address account) internal view { From d3a87da919f3ed71ea3fc75b82a10ca77b666eb3 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Tue, 2 Jul 2024 14:46:44 +0300 Subject: [PATCH 36/38] Committee with proposals list --- contracts/DualGovernance.sol | 10 +- contracts/ResealManager.sol | 2 +- .../EmergencyActivationCommittee.sol | 20 +- .../EmergencyExecutionCommittee.sol | 51 ++- ...ecutiveCommittee.sol => HashConsensus.sol} | 109 +++--- contracts/committees/ProposalsList.sol | 41 ++ contracts/committees/ResealCommittee.sol | 20 +- contracts/committees/TiebreakerCore.sol | 66 ++-- .../committees/TiebreakerSubCommittee.sol | 75 ++-- contracts/interfaces/IWithdrawalQueue.sol | 4 + contracts/libraries/EnumerableProposals.sol | 83 ++++ contracts/libraries/TiebreakerProtection.sol | 39 +- test/scenario/agent-timelock.t.sol | 4 +- test/scenario/happy-path-plan-b.t.sol | 8 +- test/scenario/reseal-executor.t.sol | 150 -------- test/scenario/tiebraker.t.sol | 83 ++-- test/unit/EmergencyActivationCommittee.t.sol | 21 -- test/unit/ExecutiveCommittee.t.sol | 356 ------------------ test/utils/scenario-test-blueprint.sol | 23 +- test/utils/utils.sol | 2 +- 20 files changed, 399 insertions(+), 768 deletions(-) rename contracts/committees/{ExecutiveCommittee.sol => HashConsensus.sol} (53%) create mode 100644 contracts/committees/ProposalsList.sol create mode 100644 contracts/libraries/EnumerableProposals.sol delete mode 100644 test/scenario/reseal-executor.t.sol delete mode 100644 test/unit/EmergencyActivationCommittee.t.sol delete mode 100644 test/unit/ExecutiveCommittee.t.sol diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index a2afbb28..2ef821fc 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -144,21 +144,15 @@ contract DualGovernance is IGovernance, ConfigurationProvider { // Tiebreaker Protection // --- - function tiebreakerApproveProposal(uint256 proposalId) external { - _tiebreaker.checkTiebreakerCommittee(msg.sender); - _dgState.checkTiebreak(CONFIG); - _tiebreaker.approveProposal(proposalId); - } - function tiebreakerResumeSealable(address sealable) external { _tiebreaker.checkTiebreakerCommittee(msg.sender); _dgState.checkTiebreak(CONFIG); _tiebreaker.resumeSealable(sealable); } - function tiebreakerSchedule(uint256 proposalId) external { + function tiebreakerScheduleProposal(uint256 proposalId) external { + _tiebreaker.checkTiebreakerCommittee(msg.sender); _dgState.checkTiebreak(CONFIG); - _tiebreaker.canSchedule(proposalId); TIMELOCK.schedule(proposalId); } diff --git a/contracts/ResealManager.sol b/contracts/ResealManager.sol index 306f73fd..add4b9a0 100644 --- a/contracts/ResealManager.sol +++ b/contracts/ResealManager.sol @@ -5,7 +5,7 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ISealable} from "./interfaces/ISealable.sol"; -contract ResealExecutor is Ownable { +contract ResealManager is Ownable { error SealableWrongPauseState(); error SenderIsNotManager(); diff --git a/contracts/committees/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol index 2f3004ce..038dd46b 100644 --- a/contracts/committees/EmergencyActivationCommittee.sol +++ b/contracts/committees/EmergencyActivationCommittee.sol @@ -2,26 +2,28 @@ pragma solidity 0.8.23; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; +import {HashConsensus} from "./HashConsensus.sol"; interface IEmergencyProtectedTimelock { function emergencyActivate() external; } -contract EmergencyActivationCommittee is ExecutiveCommittee { +contract EmergencyActivationCommittee is HashConsensus { address public immutable EMERGENCY_PROTECTED_TIMELOCK; + bytes32 private constant EMERGENCY_ACTIVATION_HASH = keccak256("EMERGENCY_ACTIVATE"); + constructor( - address OWNER, + address owner, address[] memory committeeMembers, uint256 executionQuorum, address emergencyProtectedTimelock - ) ExecutiveCommittee(OWNER, committeeMembers, executionQuorum, 0) { + ) HashConsensus(owner, committeeMembers, executionQuorum, 0) { EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; } function approveEmergencyActivate() public onlyMember { - _vote(_encodeEmergencyActivateData(), true); + _vote(EMERGENCY_ACTIVATION_HASH, true); } function getEmergencyActivateState() @@ -29,17 +31,13 @@ contract EmergencyActivationCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getVoteState(_encodeEmergencyActivateData()); + return _getHashState(EMERGENCY_ACTIVATION_HASH); } function executeEmergencyActivate() external { - _markExecuted(_encodeEmergencyActivateData()); + _markUsed(EMERGENCY_ACTIVATION_HASH); Address.functionCall( EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyActivate.selector) ); } - - function _encodeEmergencyActivateData() internal pure returns (bytes memory data) { - data = bytes("EMERGENCY_ACTIVATE"); - } } diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol index d30ad320..6586dfc3 100644 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -2,29 +2,37 @@ pragma solidity 0.8.23; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; +import {HashConsensus} from "./HashConsensus.sol"; +import {ProposalsList} from "./ProposalsList.sol"; interface IEmergencyProtectedTimelock { function emergencyExecute(uint256 proposalId) external; function emergencyReset() external; } -contract EmergencyExecutionCommittee is ExecutiveCommittee { +enum ProposalType { + EmergencyExecute, + EmergencyReset +} + +contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { address public immutable EMERGENCY_PROTECTED_TIMELOCK; constructor( - address OWNER, + address owner, address[] memory committeeMembers, uint256 executionQuorum, address emergencyProtectedTimelock - ) ExecutiveCommittee(OWNER, committeeMembers, executionQuorum, 0) { + ) HashConsensus(owner, committeeMembers, executionQuorum, 0) { EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; } // Emergency Execution function voteEmergencyExecute(uint256 proposalId, bool _supports) public onlyMember { - _vote(_encodeEmergencyExecuteData(proposalId), _supports); + (bytes memory proposalData, bytes32 key) = _encodeEmergencyExecute(proposalId); + _vote(key, _supports); + _pushProposal(key, uint256(ProposalType.EmergencyExecute), proposalData); } function getEmergencyExecuteState(uint256 proposalId) @@ -32,21 +40,34 @@ contract EmergencyExecutionCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getVoteState(_encodeEmergencyExecuteData(proposalId)); + (, bytes32 key) = _encodeEmergencyExecute(proposalId); + return _getHashState(key); } function executeEmergencyExecute(uint256 proposalId) public { - _markExecuted(_encodeEmergencyExecuteData(proposalId)); + (, bytes32 key) = _encodeEmergencyExecute(proposalId); + _markUsed(key); Address.functionCall( EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyExecute.selector, proposalId) ); } + function _encodeEmergencyExecute(uint256 proposalId) + private + view + returns (bytes memory proposalData, bytes32 key) + { + proposalData = abi.encode(ProposalType.EmergencyExecute, bytes32(proposalId)); + key = keccak256(proposalData); + } + // Governance reset function approveEmergencyReset() public onlyMember { - _vote(_dataEmergencyResetData(), true); + bytes32 proposalKey = _encodeEmergencyResetProposalKey(); + _vote(proposalKey, true); + _pushProposal(proposalKey, uint256(ProposalType.EmergencyReset), bytes("")); } function getEmergencyResetState() @@ -54,21 +75,19 @@ contract EmergencyExecutionCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getVoteState(_dataEmergencyResetData()); + bytes32 proposalKey = _encodeEmergencyResetProposalKey(); + return _getHashState(proposalKey); } function executeEmergencyReset() external { - _markExecuted(_dataEmergencyResetData()); + bytes32 proposalKey = _encodeEmergencyResetProposalKey(); + _markUsed(proposalKey); Address.functionCall( EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyReset.selector) ); } - function _dataEmergencyResetData() internal pure returns (bytes memory data) { - data = bytes("EMERGENCY_RESET"); - } - - function _encodeEmergencyExecuteData(uint256 proposalId) internal pure returns (bytes memory data) { - data = abi.encode(proposalId); + function _encodeEmergencyResetProposalKey() internal view returns (bytes32) { + return keccak256(abi.encode(ProposalType.EmergencyReset, bytes32(0))); } } diff --git a/contracts/committees/ExecutiveCommittee.sol b/contracts/committees/HashConsensus.sol similarity index 53% rename from contracts/committees/ExecutiveCommittee.sol rename to contracts/committees/HashConsensus.sol index d09e5b75..fb4a2d65 100644 --- a/contracts/committees/ExecutiveCommittee.sol +++ b/contracts/committees/HashConsensus.sol @@ -4,36 +4,35 @@ pragma solidity 0.8.23; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -abstract contract ExecutiveCommittee is Ownable { +abstract contract HashConsensus is Ownable { using EnumerableSet for EnumerableSet.AddressSet; event MemberAdded(address indexed member); event MemberRemoved(address indexed member); event QuorumSet(uint256 quorum); - event VoteExecuted(bytes data); - event Voted(address indexed signer, bytes data, bool support); + event HashUsed(bytes32 hash); + event Voted(address indexed signer, bytes32 hash, bool support); event TimelockDurationSet(uint256 timelockDuration); error IsNotMember(); error SenderIsNotMember(); - error VoteAlreadyExecuted(); + error HashAlreadyUsed(); error QuorumIsNotReached(); error InvalidQuorum(); error DuplicatedMember(address member); error TimelockNotPassed(); - EnumerableSet.AddressSet private members; + struct HashState { + uint40 quorumAt; + uint40 usedAt; + } + uint256 public quorum; uint256 public timelockDuration; - struct VoteState { - bytes data; - uint256 quorumAt; - bool isExecuted; - } - - mapping(bytes32 digest => VoteState) public voteStates; - mapping(address signer => mapping(bytes32 digest => bool support)) public approves; + mapping(bytes32 => HashState) private _hashStates; + EnumerableSet.AddressSet private _members; + mapping(address signer => mapping(bytes32 => bool)) public approves; constructor(address owner, address[] memory newMembers, uint256 executionQuorum, uint256 timelock) Ownable(owner) { if (executionQuorum == 0) { @@ -46,71 +45,58 @@ abstract contract ExecutiveCommittee is Ownable { emit TimelockDurationSet(timelock); for (uint256 i = 0; i < newMembers.length; ++i) { - if (members.contains(newMembers[i])) { - revert DuplicatedMember(newMembers[i]); - } _addMember(newMembers[i]); } } - function _vote(bytes memory data, bool support) internal { - bytes32 digest = keccak256(data); - - if (voteStates[digest].data.length == 0) { - voteStates[digest].data = data; - } - - if (voteStates[digest].isExecuted == true) { - revert VoteAlreadyExecuted(); + function _vote(bytes32 hash, bool support) internal { + if (_hashStates[hash].usedAt > 0) { + revert HashAlreadyUsed(); } - if (approves[msg.sender][digest] == support) { + if (approves[msg.sender][hash] == support) { return; } - uint256 heads = _getSupport(digest); + uint256 heads = _getSupport(hash); if (heads == quorum - 1 && support == true) { - voteStates[digest].quorumAt = block.timestamp; + _hashStates[hash].quorumAt = uint40(block.timestamp); } - approves[msg.sender][digest] = support; - emit Voted(msg.sender, data, support); + approves[msg.sender][hash] = support; + emit Voted(msg.sender, hash, support); } - function _markExecuted(bytes memory data) internal { - bytes32 digest = keccak256(data); - - if (voteStates[digest].isExecuted == true) { - revert VoteAlreadyExecuted(); + function _markUsed(bytes32 hash) internal { + if (_hashStates[hash].usedAt > 0) { + revert HashAlreadyUsed(); } - if (_getSupport(digest) < quorum) { + if (_getSupport(hash) < quorum) { revert QuorumIsNotReached(); } - if (block.timestamp < voteStates[digest].quorumAt + timelockDuration) { + if (block.timestamp < _hashStates[hash].quorumAt + timelockDuration) { revert TimelockNotPassed(); } - voteStates[digest].isExecuted = true; + _hashStates[hash].usedAt = uint40(block.timestamp); - emit VoteExecuted(data); + emit HashUsed(hash); } - function _getVoteState(bytes memory data) + function _getHashState(bytes32 hash) internal view - returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + returns (uint256 support, uint256 execuitionQuorum, bool isUsed) { - bytes32 digest = keccak256(data); - - support = _getSupport(digest); + support = _getSupport(hash); execuitionQuorum = quorum; - isExecuted = voteStates[digest].isExecuted; + isUsed = _hashStates[hash].usedAt > 0; } function addMember(address newMember, uint256 newQuorum) public onlyOwner { _addMember(newMember); - if (newQuorum == 0 || newQuorum > members.length()) { + if (newQuorum == 0 || newQuorum > _members.length()) { revert InvalidQuorum(); } quorum = newQuorum; @@ -118,13 +104,13 @@ abstract contract ExecutiveCommittee is Ownable { } function removeMember(address memberToRemove, uint256 newQuorum) public onlyOwner { - if (!members.contains(memberToRemove)) { + if (!_members.contains(memberToRemove)) { revert IsNotMember(); } - members.remove(memberToRemove); + _members.remove(memberToRemove); emit MemberRemoved(memberToRemove); - if (newQuorum == 0 || newQuorum > members.length()) { + if (newQuorum == 0 || newQuorum > _members.length()) { revert InvalidQuorum(); } quorum = newQuorum; @@ -132,11 +118,11 @@ abstract contract ExecutiveCommittee is Ownable { } function getMembers() public view returns (address[] memory) { - return members.values(); + return _members.values(); } function isMember(address member) public view returns (bool) { - return members.contains(member); + return _members.contains(member); } function setTimelockDuration(uint256 timelock) public onlyOwner { @@ -144,24 +130,33 @@ abstract contract ExecutiveCommittee is Ownable { emit TimelockDurationSet(timelock); } + function setQuorum(uint256 newQuorum) public onlyOwner { + if (newQuorum == 0 || newQuorum > _members.length()) { + revert InvalidQuorum(); + } + + quorum = newQuorum; + emit QuorumSet(newQuorum); + } + function _addMember(address newMember) internal { - if (members.contains(newMember)) { + if (_members.contains(newMember)) { revert DuplicatedMember(newMember); } - members.add(newMember); + _members.add(newMember); emit MemberAdded(newMember); } - function _getSupport(bytes32 digest) internal view returns (uint256 support) { - for (uint256 i = 0; i < members.length(); ++i) { - if (approves[members.at(i)][digest]) { + function _getSupport(bytes32 hash) internal view returns (uint256 support) { + for (uint256 i = 0; i < _members.length(); ++i) { + if (approves[_members.at(i)][hash]) { support++; } } } modifier onlyMember() { - if (!members.contains(msg.sender)) { + if (!_members.contains(msg.sender)) { revert SenderIsNotMember(); } _; diff --git a/contracts/committees/ProposalsList.sol b/contracts/committees/ProposalsList.sol new file mode 100644 index 00000000..ecdf9786 --- /dev/null +++ b/contracts/committees/ProposalsList.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {EnumerableProposals, Proposal} from "../libraries/EnumerableProposals.sol"; + +contract ProposalsList { + using EnumerableProposals for EnumerableProposals.Bytes32ToProposalMap; + + EnumerableProposals.Bytes32ToProposalMap internal _proposals; + + function getProposals(uint256 offset, uint256 limit) public returns (Proposal[] memory proposals) { + bytes32[] memory keys = _proposals.orederedKeys(offset, limit); + + uint256 length = keys.length; + proposals = new Proposal[](keys.length); + + for (uint256 i = 0; i < keys.length; ++i) { + proposals[i] = _proposals.get(keys[i]); + } + } + + function getProposalAt(uint256 index) public returns (Proposal memory) { + return _proposals.at(index); + } + + function getProposal(bytes32 key) public returns (Proposal memory) { + return _proposals.get(key); + } + + function proposalsLength() public returns (uint256) { + return _proposals.length(); + } + + function orederedKeys(uint256 offset, uint256 limit) public returns (bytes32[] memory) { + return _proposals.orederedKeys(offset, limit); + } + + function _pushProposal(bytes32 key, uint256 proposalType, bytes memory data) internal { + _proposals.push(key, proposalType, data); + } +} diff --git a/contracts/committees/ResealCommittee.sol b/contracts/committees/ResealCommittee.sol index 6ff782e4..1b40a5d6 100644 --- a/contracts/committees/ResealCommittee.sol +++ b/contracts/committees/ResealCommittee.sol @@ -2,13 +2,14 @@ pragma solidity 0.8.23; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; +import {HashConsensus} from "./HashConsensus.sol"; +import {ProposalsList} from "./ProposalsList.sol"; interface IDualGovernance { function reseal(address[] memory sealables) external; } -contract ResealCommittee is ExecutiveCommittee { +contract ResealCommittee is HashConsensus, ProposalsList { address public immutable DUAL_GOVERNANCE; mapping(bytes32 => uint256) private _resealNonces; @@ -19,12 +20,14 @@ contract ResealCommittee is ExecutiveCommittee { uint256 executionQuorum, address dualGovernance, uint256 timelock - ) ExecutiveCommittee(owner, committeeMembers, executionQuorum, timelock) { + ) HashConsensus(owner, committeeMembers, executionQuorum, timelock) { DUAL_GOVERNANCE = dualGovernance; } function voteReseal(address[] memory sealables, bool support) public onlyMember { - _vote(_encodeResealData(sealables), support); + (bytes memory proposalData, bytes32 key) = _encodeResealProposal(sealables); + _vote(key, support); + _pushProposal(key, 0, proposalData); } function getResealState(address[] memory sealables) @@ -32,11 +35,13 @@ contract ResealCommittee is ExecutiveCommittee { view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getVoteState(_encodeResealData(sealables)); + (, bytes32 key) = _encodeResealProposal(sealables); + return _getHashState(key); } function executeReseal(address[] memory sealables) external { - _markExecuted(_encodeResealData(sealables)); + (, bytes32 key) = _encodeResealProposal(sealables); + _markUsed(key); Address.functionCall(DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.reseal.selector, sealables)); @@ -44,8 +49,9 @@ contract ResealCommittee is ExecutiveCommittee { _resealNonces[resealNonceHash]++; } - function _encodeResealData(address[] memory sealables) internal view returns (bytes memory data) { + 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]); + key = keccak256(data); } } diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol index 5a3071b1..3f45e90a 100644 --- a/contracts/committees/TiebreakerCore.sol +++ b/contracts/committees/TiebreakerCore.sol @@ -2,14 +2,20 @@ pragma solidity 0.8.23; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; +import {HashConsensus} from "./HashConsensus.sol"; +import {ProposalsList} from "./ProposalsList.sol"; interface IDualGovernance { - function tiebreakerApproveProposal(uint256 proposalId) external; + function tiebreakerScheduleProposal(uint256 proposalId) external; function tiebreakerResumeSealable(address sealable) external; } -contract TiebreakerCore is ExecutiveCommittee { +enum ProposalType { + ScheduleProposal, + ResumeSelable +} + +contract TiebreakerCore is HashConsensus, ProposalsList { error ResumeSealableNonceMismatch(); address immutable DUAL_GOVERNANCE; @@ -20,65 +26,79 @@ contract TiebreakerCore is ExecutiveCommittee { address owner, address[] memory committeeMembers, uint256 executionQuorum, - address dualGovernance - ) ExecutiveCommittee(owner, committeeMembers, executionQuorum, 0) { + address dualGovernance, + uint256 timelock + ) HashConsensus(owner, committeeMembers, executionQuorum, timelock) { DUAL_GOVERNANCE = dualGovernance; } - // Approve proposal + // Schedule proposal - function approveProposal(uint256 proposalId) public onlyMember { - _vote(_encodeAproveProposalData(proposalId), true); + function scheduleProposal(uint256 proposalId) public onlyMember { + (bytes memory proposalData, bytes32 key) = _encodeScheduleProposal(proposalId); + _vote(key, true); + _pushProposal(key, uint256(ProposalType.ScheduleProposal), proposalData); } - function getApproveProposalState(uint256 proposalId) + function getScheduleProposalState(uint256 proposalId) public view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getVoteState(_encodeAproveProposalData(proposalId)); + (, bytes32 key) = _encodeScheduleProposal(proposalId); + return _getHashState(key); } - function executeApproveProposal(uint256 proposalId) public { - _markExecuted(_encodeAproveProposalData(proposalId)); + function executeScheduleProposal(uint256 proposalId) public { + (, bytes32 key) = _encodeScheduleProposal(proposalId); + _markUsed(key); Address.functionCall( - DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.tiebreakerApproveProposal.selector, proposalId) + DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.tiebreakerScheduleProposal.selector, proposalId) ); } + function _encodeScheduleProposal(uint256 proposalId) internal pure returns (bytes memory data, bytes32 key) { + data = abi.encode(ProposalType.ScheduleProposal, data); + key = keccak256(data); + } + // Resume sealable function getSealableResumeNonce(address sealable) public view returns (uint256) { return _sealableResumeNonces[sealable]; } - function approveSealableResume(address sealable, uint256 nonce) public onlyMember { + function sealableResume(address sealable, uint256 nonce) public onlyMember { if (nonce != _sealableResumeNonces[sealable]) { revert ResumeSealableNonceMismatch(); } - _vote(_encodeSealableResumeData(sealable, nonce), true); + (bytes memory proposalData, bytes32 key) = _encodeSealableResume(sealable, nonce); + _vote(key, true); + _pushProposal(key, uint256(ProposalType.ResumeSelable), proposalData); } function getSealableResumeState( address sealable, uint256 nonce ) public view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getVoteState(_encodeSealableResumeData(sealable, nonce)); + (, bytes32 key) = _encodeSealableResume(sealable, nonce); + return _getHashState(key); } function executeSealableResume(address sealable) external { - _markExecuted(_encodeSealableResumeData(sealable, _sealableResumeNonces[sealable])); + (, bytes32 key) = _encodeSealableResume(sealable, _sealableResumeNonces[sealable]); + _markUsed(key); _sealableResumeNonces[sealable]++; Address.functionCall( DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.tiebreakerResumeSealable.selector, sealable) ); } - function _encodeAproveProposalData(uint256 proposalId) internal pure returns (bytes memory data) { - data = abi.encode(proposalId); - } - - function _encodeSealableResumeData(address sealable, uint256 nonce) internal pure returns (bytes memory data) { - data = abi.encode(sealable, nonce); + function _encodeSealableResume( + address sealable, + uint256 nonce + ) private pure returns (bytes memory data, bytes32 key) { + data = abi.encode(ProposalType.ResumeSelable, sealable, nonce); + key = keccak256(data); } } diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol index e73cee3c..1ceb40fc 100644 --- a/contracts/committees/TiebreakerSubCommittee.sol +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -2,15 +2,21 @@ pragma solidity 0.8.23; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {ExecutiveCommittee} from "./ExecutiveCommittee.sol"; +import {HashConsensus} from "./HashConsensus.sol"; +import {ProposalsList} from "./ProposalsList.sol"; interface ITiebreakerCore { function getSealableResumeNonce(address sealable) external view returns (uint256 nonce); - function approveProposal(uint256 _proposalId) external; - function approveSealableResume(address sealable, uint256 nonce) external; + function scheduleProposal(uint256 _proposalId) external; + function sealableResume(address sealable, uint256 nonce) external; } -contract TiebreakerSubCommittee is ExecutiveCommittee { +enum ProposalType { + ScheduleProposal, + ResumeSelable +} + +contract TiebreakerSubCommittee is HashConsensus, ProposalsList { address immutable TIEBREAKER_CORE; constructor( @@ -18,59 +24,72 @@ contract TiebreakerSubCommittee is ExecutiveCommittee { address[] memory committeeMembers, uint256 executionQuorum, address tiebreakerCore - ) ExecutiveCommittee(owner, committeeMembers, executionQuorum, 0) { + ) HashConsensus(owner, committeeMembers, executionQuorum, 0) { TIEBREAKER_CORE = tiebreakerCore; } - // Approve proposal + // Schedule proposal - function voteApproveProposal(uint256 proposalId, bool support) public onlyMember { - _vote(_encodeApproveProposalData(proposalId), support); + function scheduleProposal(uint256 proposalId) public onlyMember { + (bytes memory proposalData, bytes32 key) = _encodeAproveProposal(proposalId); + _vote(key, true); + _pushProposal(key, uint256(ProposalType.ScheduleProposal), proposalData); } - function getApproveProposalState(uint256 proposalId) + function getScheduleProposalState(uint256 proposalId) public view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getVoteState(_encodeApproveProposalData(proposalId)); + (, bytes32 key) = _encodeAproveProposal(proposalId); + return _getHashState(key); } - function executeApproveProposal(uint256 proposalId) public { - _markExecuted(_encodeApproveProposalData(proposalId)); + function executeScheduleProposal(uint256 proposalId) public { + (, bytes32 key) = _encodeAproveProposal(proposalId); + _markUsed(key); Address.functionCall( - TIEBREAKER_CORE, abi.encodeWithSelector(ITiebreakerCore.approveProposal.selector, proposalId) + TIEBREAKER_CORE, abi.encodeWithSelector(ITiebreakerCore.scheduleProposal.selector, proposalId) ); } - // Approve unpause sealable + function _encodeAproveProposal(uint256 proposalId) internal pure returns (bytes memory data, bytes32 key) { + data = abi.encode(ProposalType.ScheduleProposal, data); + key = keccak256(data); + } + + // Sealable resume - function voteApproveSealableResume(address sealable, bool support) public { - _vote(_encodeApproveSealableResumeData(sealable), support); + function sealableResume(address sealable) public { + (bytes memory proposalData, bytes32 key,) = _encodeSealableResume(sealable); + _vote(key, true); + _pushProposal(key, uint256(ProposalType.ResumeSelable), proposalData); } - function getApproveSealableResumeState(address sealable) + function getSealableResumeState(address sealable) public view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { - return _getVoteState(_encodeApproveSealableResumeData(sealable)); + (, bytes32 key,) = _encodeSealableResume(sealable); + return _getHashState(key); } - function executeApproveSealableResume(address sealable) public { - _markExecuted(_encodeApproveSealableResumeData(sealable)); - uint256 nonce = ITiebreakerCore(TIEBREAKER_CORE).getSealableResumeNonce(sealable); + function executeSealableResume(address sealable) public { + (, bytes32 key, uint256 nonce) = _encodeSealableResume(sealable); + _markUsed(key); Address.functionCall( - TIEBREAKER_CORE, abi.encodeWithSelector(ITiebreakerCore.approveSealableResume.selector, sealable, nonce) + TIEBREAKER_CORE, abi.encodeWithSelector(ITiebreakerCore.sealableResume.selector, sealable, nonce) ); } - function _encodeApproveSealableResumeData(address sealable) internal view returns (bytes memory data) { - uint256 nonce = ITiebreakerCore(TIEBREAKER_CORE).getSealableResumeNonce(sealable); + function _encodeSealableResume(address sealable) + internal + view + returns (bytes memory data, bytes32 key, uint256 nonce) + { + nonce = ITiebreakerCore(TIEBREAKER_CORE).getSealableResumeNonce(sealable); data = abi.encode(sealable, nonce); - } - - function _encodeApproveProposalData(uint256 proposalId) internal pure returns (bytes memory data) { - data = abi.encode(proposalId); + key = keccak256(data); } } diff --git a/contracts/interfaces/IWithdrawalQueue.sol b/contracts/interfaces/IWithdrawalQueue.sol index e3beea17..0ca85366 100644 --- a/contracts/interfaces/IWithdrawalQueue.sol +++ b/contracts/interfaces/IWithdrawalQueue.sol @@ -48,4 +48,8 @@ interface IWithdrawalQueue { uint256[] calldata _amounts, address _owner ) external returns (uint256[] memory requestIds); + + function grantRole(bytes32 role, address account) external; + function pauseFor(uint256 duration) external; + function isPaused() external returns (bool); } diff --git a/contracts/libraries/EnumerableProposals.sol b/contracts/libraries/EnumerableProposals.sol new file mode 100644 index 00000000..d5954f9e --- /dev/null +++ b/contracts/libraries/EnumerableProposals.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +struct Proposal { + uint40 submittedAt; + uint256 proposalType; + bytes data; +} + +library EnumerableProposals { + using EnumerableSet for EnumerableSet.Bytes32Set; + + error ProposalDoesNotExist(bytes32 key); + error OffsetOutOfBounds(); + + struct Bytes32ToProposalMap { + bytes32[] _orderedKeys; + EnumerableSet.Bytes32Set _keys; + mapping(bytes32 key => Proposal) _proposals; + } + + function push( + Bytes32ToProposalMap storage map, + bytes32 key, + uint256 proposalType, + bytes memory data + ) internal returns (bool) { + if (!contains(map, key)) { + Proposal memory proposal = Proposal(uint40(block.timestamp), proposalType, data); + map._proposals[key] = proposal; + map._orderedKeys.push(key); + map._keys.add(key); + return true; + } + return false; + } + + function contains(Bytes32ToProposalMap storage map, bytes32 key) internal view returns (bool) { + return map._keys.contains(key); + } + + function length(Bytes32ToProposalMap storage map) internal view returns (uint256) { + return map._orderedKeys.length; + } + + function at(Bytes32ToProposalMap storage map, uint256 index) internal view returns (Proposal memory) { + bytes32 key = map._orderedKeys[index]; + return map._proposals[key]; + } + + function get(Bytes32ToProposalMap storage map, bytes32 key) internal view returns (Proposal memory value) { + if (!contains(map, key)) { + revert ProposalDoesNotExist(key); + } + value = map._proposals[key]; + } + + function orederedKeys(Bytes32ToProposalMap storage map) internal view returns (bytes32[] memory) { + return map._orderedKeys; + } + + function orederedKeys( + Bytes32ToProposalMap storage map, + uint256 offset, + uint256 limit + ) internal view returns (bytes32[] memory keys) { + if (offset >= map._orderedKeys.length) { + revert OffsetOutOfBounds(); + } + + uint256 keysLength = limit; + if (keysLength > map._orderedKeys.length - offset) { + keysLength = map._orderedKeys.length - offset; + } + + keys = new bytes32[](keysLength); + for (uint256 i = 0; i < keysLength; ++i) { + keys[i] = map._orderedKeys[offset + i]; + } + } +} diff --git a/contracts/libraries/TiebreakerProtection.sol b/contracts/libraries/TiebreakerProtection.sol index ce9d3084..fb30649a 100644 --- a/contracts/libraries/TiebreakerProtection.sol +++ b/contracts/libraries/TiebreakerProtection.sol @@ -9,58 +9,28 @@ library TiebreakerProtection { struct Tiebreaker { address tiebreaker; IResealManger resealManager; - uint256 tiebreakerProposalApprovalTimelock; - mapping(uint256 proposalId => uint256) tiebreakerProposalApprovalTimestamp; } - event TiebreakerSet(address tiebreakCommittee); - event ProposalApprovedForExecution(uint256 proposalId); + event TiebreakerSet(address tiebreakCommittee, address resealManager); event SealableResumed(address sealable); - event ResealManagerSet(address resealManager); error ProposalNotExecutable(uint256 proposalId); error NotTiebreaker(address account, address tiebreakCommittee); - error ProposalAlreadyApproved(uint256 proposalId); - error ProposalIsNotApprovedForExecution(uint256 proposalId); - error TiebreakerTimelockIsNotPassed(uint256 proposalId); - error SealableResumeAlreadyApproved(address sealable); error TieBreakerAddressIsSame(); - function approveProposal(Tiebreaker storage self, uint256 proposalId) internal { - if (self.tiebreakerProposalApprovalTimestamp[proposalId] > 0) { - revert ProposalAlreadyApproved(proposalId); - } - - _approveProposal(self, proposalId); - } - function resumeSealable(Tiebreaker storage self, address sealable) internal { self.resealManager.resume(sealable); emit SealableResumed(sealable); } - function canSchedule(Tiebreaker storage self, uint256 proposalId) internal view { - if (self.tiebreakerProposalApprovalTimestamp[proposalId] == 0) { - revert ProposalIsNotApprovedForExecution(proposalId); - } - if ( - self.tiebreakerProposalApprovalTimestamp[proposalId] + self.tiebreakerProposalApprovalTimelock - > block.timestamp - ) { - revert TiebreakerTimelockIsNotPassed(proposalId); - } - } - function setTiebreaker(Tiebreaker storage self, address tiebreaker, address resealManager) internal { if (self.tiebreaker == tiebreaker) { revert TieBreakerAddressIsSame(); } self.tiebreaker = tiebreaker; - emit TiebreakerSet(tiebreaker); - self.resealManager = IResealManger(resealManager); - emit ResealManagerSet(resealManager); + emit TiebreakerSet(tiebreaker, resealManager); } function checkTiebreakerCommittee(Tiebreaker storage self, address account) internal view { @@ -68,9 +38,4 @@ library TiebreakerProtection { revert NotTiebreaker(account, self.tiebreaker); } } - - function _approveProposal(Tiebreaker storage self, uint256 proposalId) internal { - self.tiebreakerProposalApprovalTimestamp[proposalId] = block.timestamp; - emit ProposalApprovedForExecution(proposalId); - } } diff --git a/test/scenario/agent-timelock.t.sol b/test/scenario/agent-timelock.t.sol index f209a1e9..6cd3cb3b 100644 --- a/test/scenario/agent-timelock.t.sol +++ b/test/scenario/agent-timelock.t.sol @@ -98,10 +98,10 @@ contract AgentTimelockTest is ScenarioTestBlueprint { vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2); // committee resets governance - vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); + vm.prank(address(_emergencyActivationCommittee)); _timelock.activateEmergencyMode(); - vm.prank(_EMERGENCY_EXECUTION_COMMITTEE); + vm.prank(address(_emergencyExecutionCommittee)); _timelock.emergencyReset(); // proposal is canceled now diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index fe239efe..72484ed0 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -72,7 +72,7 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertCanSchedule(_singleGovernance, maliciousProposalId, false); // emergency committee activates emergency mode - vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); + vm.prank(address(_emergencyActivationCommittee)); _timelock.activateEmergencyMode(); // emergency mode was successfully activated @@ -288,7 +288,7 @@ contract PlanBSetup is ScenarioTestBlueprint { { vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2); - vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); + vm.prank(address(_emergencyActivationCommittee)); _timelock.activateEmergencyMode(); emergencyState = _timelock.getEmergencyState(); @@ -386,7 +386,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // emergency committee activates emergency mode EmergencyState memory emergencyState; { - vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); + vm.prank(address(_emergencyActivationCommittee)); _timelock.activateEmergencyMode(); emergencyState = _timelock.getEmergencyState(); @@ -435,7 +435,7 @@ contract PlanBSetup is ScenarioTestBlueprint { emergencyState.protectedTill ) ); - vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); + vm.prank(address(_emergencyActivationCommittee)); _timelock.activateEmergencyMode(); } } diff --git a/test/scenario/reseal-executor.t.sol b/test/scenario/reseal-executor.t.sol deleted file mode 100644 index 6d8701f0..00000000 --- a/test/scenario/reseal-executor.t.sol +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import {percents, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; - -import {GateSealMock} from "../mocks/GateSealMock.sol"; -import {ResealExecutor} from "contracts/ResealExecutor.sol"; -import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; -import {IGateSeal} from "contracts/interfaces/IGateSeal.sol"; - -import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; - -contract ResealExecutorScenarioTest is ScenarioTestBlueprint { - uint256 private immutable _RELEASE_DELAY = 5 days; - uint256 private immutable _SEAL_DURATION = 14 days; - uint256 private constant _PAUSE_INFINITELY = type(uint256).max; - - address private immutable _VETOER = makeAddr("VETOER"); - - IGateSeal private _gateSeal; - address[] private _sealables; - ResealExecutor private _resealExecutor; - ResealCommittee private _resealCommittee; - - uint256 private _resealCommitteeMembersCount = 5; - uint256 private _resealCommitteeQuorum = 3; - address[] private _resealCommitteeMembers = new address[](0); - - function setUp() external { - _selectFork(); - _deployTarget(); - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); - - _sealables.push(address(_WITHDRAWAL_QUEUE)); - - _gateSeal = new GateSealMock(_SEAL_DURATION, _SEALING_COMMITTEE_LIFETIME); - - _resealExecutor = new ResealExecutor(address(this), address(_dualGovernance), address(this)); - for (uint256 i = 0; i < _resealCommitteeMembersCount; i++) { - _resealCommitteeMembers.push(makeAddr(string(abi.encode(i + 65)))); - } - _resealCommittee = new ResealCommittee( - address(this), _resealCommitteeMembers, _resealCommitteeQuorum, address(_resealExecutor) - ); - - _resealExecutor.setResealCommittee(address(_resealCommittee)); - - // grant rights to gate seal to pause/resume the withdrawal queue - vm.startPrank(DAO_AGENT); - _WITHDRAWAL_QUEUE.grantRole(_WITHDRAWAL_QUEUE.PAUSE_ROLE(), address(_gateSeal)); - _WITHDRAWAL_QUEUE.grantRole(_WITHDRAWAL_QUEUE.PAUSE_ROLE(), address(_resealExecutor)); - _WITHDRAWAL_QUEUE.grantRole(_WITHDRAWAL_QUEUE.RESUME_ROLE(), address(_resealExecutor)); - vm.stopPrank(); - } - - function testFork_resealingWithLockedGovernance() external { - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - _assertNormalState(); - - _lockStETH(_VETOER, percents("10.0")); - _assertVetoSignalingState(); - - // sealing committee seals Withdrawal Queue - vm.prank(_SEALING_COMMITTEE); - _gateSeal.seal(_sealables); - - // validate Withdrawal Queue was paused - assertTrue(_WITHDRAWAL_QUEUE.isPaused()); - - // validate the dual governance still in the veto signaling state - _assertVetoSignalingState(); - - //Committee votes for resealing WQ - for (uint256 i = 0; i < _resealCommitteeQuorum; i++) { - vm.prank(_resealCommitteeMembers[i]); - _resealCommittee.voteReseal(_sealables, true); - } - (uint256 support, uint256 quorum, bool isExecuted) = _resealCommittee.getResealState(_sealables); - assert(support == quorum); - assert(isExecuted == false); - - // WQ is paused for limited time before resealing - assert(_WITHDRAWAL_QUEUE.getResumeSinceTimestamp() < _PAUSE_INFINITELY); - - // Reseal execution - _resealCommittee.executeReseal(_sealables); - - // WQ is paused for infinite time after resealing - assert(_WITHDRAWAL_QUEUE.getResumeSinceTimestamp() == _PAUSE_INFINITELY); - assert(_WITHDRAWAL_QUEUE.isPaused()); - } - - function testFork_resealingWithActiveGovernance() external { - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - _assertNormalState(); - - // sealing committee seals Withdrawal Queue - vm.prank(_SEALING_COMMITTEE); - _gateSeal.seal(_sealables); - - // validate Withdrawal Queue was paused - assertTrue(_WITHDRAWAL_QUEUE.isPaused()); - - //Committee votes for resealing WQ - for (uint256 i = 0; i < _resealCommitteeQuorum; i++) { - vm.prank(_resealCommitteeMembers[i]); - _resealCommittee.voteReseal(_sealables, true); - } - (uint256 support, uint256 quorum, bool isExecuted) = _resealCommittee.getResealState(_sealables); - assert(support == quorum); - assert(isExecuted == false); - - // WQ is paused for limited time before resealing - assert(_WITHDRAWAL_QUEUE.getResumeSinceTimestamp() < _PAUSE_INFINITELY); - - // Reseal exection reverts - vm.expectRevert(); - _resealCommittee.executeReseal(_sealables); - } - - function testFork_resealingWithLockedGovernanceAndActiveWQ() external { - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - _assertNormalState(); - - _lockStETH(_VETOER, percents("10.0")); - _assertVetoSignalingState(); - - // validate Withdrawal Queue is Active - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - - // validate the dual governance still in the veto signaling state - _assertVetoSignalingState(); - - //Committee votes for resealing WQ - for (uint256 i = 0; i < _resealCommitteeQuorum; i++) { - vm.prank(_resealCommitteeMembers[i]); - _resealCommittee.voteReseal(_sealables, true); - } - (uint256 support, uint256 quorum, bool isExecuted) = _resealCommittee.getResealState(_sealables); - assert(support == quorum); - assert(isExecuted == false); - - // validate Withdrawal Queue is Active - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - - // Reseal exection reverts - vm.expectRevert(); - _resealCommittee.executeReseal(_sealables); - } -} diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol index 0e89e033..1c465fbf 100644 --- a/test/scenario/tiebraker.t.sol +++ b/test/scenario/tiebraker.t.sol @@ -16,6 +16,7 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { function setUp() external { _selectFork(); _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); + _depositStETH(_VETOER, 1 ether); } function test_proposal_approval() external { @@ -27,8 +28,9 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { // Tiebreak activation _assertNormalState(); - _lockStETH(_VETOER, percents("15.00")); - _wait(_config.SIGNALLING_MAX_DURATION()); + _lockStETH(_VETOER, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, 1 gwei); + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() + 1); _activateNextState(); _assertRageQuitState(); _wait(_config.TIE_BREAK_ACTIVATION_TIMEOUT()); @@ -41,49 +43,47 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { members = _tiebreakerSubCommittees[0].getMembers(); for (uint256 i = 0; i < _tiebreakerSubCommittees[0].quorum() - 1; i++) { vm.prank(members[i]); - _tiebreakerSubCommittees[0].voteApproveProposal(proposalIdToExecute, true); - (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getApproveProposalState(proposalIdToExecute); + _tiebreakerSubCommittees[0].scheduleProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getScheduleProposalState(proposalIdToExecute); assert(support < quorum); assert(isExecuted == false); } vm.prank(members[members.length - 1]); - _tiebreakerSubCommittees[0].voteApproveProposal(proposalIdToExecute, true); - (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getApproveProposalState(proposalIdToExecute); + _tiebreakerSubCommittees[0].scheduleProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getScheduleProposalState(proposalIdToExecute); assert(support == quorum); assert(isExecuted == false); - _tiebreakerSubCommittees[0].executeApproveProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _tiebreakerCommittee.getApproveProposalState(proposalIdToExecute); + _tiebreakerSubCommittees[0].executeScheduleProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _tiebreakerCommittee.getScheduleProposalState(proposalIdToExecute); assert(support < quorum); // Tiebreaker subcommittee 1 members = _tiebreakerSubCommittees[1].getMembers(); for (uint256 i = 0; i < _tiebreakerSubCommittees[1].quorum() - 1; i++) { vm.prank(members[i]); - _tiebreakerSubCommittees[1].voteApproveProposal(proposalIdToExecute, true); - (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getApproveProposalState(proposalIdToExecute); + _tiebreakerSubCommittees[1].scheduleProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getScheduleProposalState(proposalIdToExecute); assert(support < quorum); assert(isExecuted == false); } vm.prank(members[members.length - 1]); - _tiebreakerSubCommittees[1].voteApproveProposal(proposalIdToExecute, true); - (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getApproveProposalState(proposalIdToExecute); + _tiebreakerSubCommittees[1].scheduleProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getScheduleProposalState(proposalIdToExecute); assert(support == quorum); assert(isExecuted == false); // Approve proposal for scheduling - _tiebreakerSubCommittees[1].executeApproveProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _tiebreakerCommittee.getApproveProposalState(proposalIdToExecute); + _tiebreakerSubCommittees[1].executeScheduleProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _tiebreakerCommittee.getScheduleProposalState(proposalIdToExecute); assert(support == quorum); - _tiebreakerCommittee.executeApproveProposal(proposalIdToExecute); - // Waiting for submit delay pass _wait(_config.AFTER_SUBMIT_DELAY()); - _dualGovernance.tiebreakerSchedule(proposalIdToExecute); + _tiebreakerCommittee.executeScheduleProposal(proposalIdToExecute); } function test_resume_withdrawals() external { @@ -93,37 +93,42 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { address[] memory members; + vm.prank(DAO_AGENT); + _WITHDRAWAL_QUEUE.grantRole( + 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(DAO_AGENT) + ); + vm.prank(DAO_AGENT); + _WITHDRAWAL_QUEUE.pauseFor(type(uint256).max); + assertEq(_WITHDRAWAL_QUEUE.isPaused(), true); + // Tiebreak activation _assertNormalState(); - _lockStETH(_VETOER, percents("15.00")); - _wait(_config.SIGNALLING_MAX_DURATION()); + _lockStETH(_VETOER, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, 1 gwei); + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() + 1); _activateNextState(); _assertRageQuitState(); - vm.startPrank(DAO_AGENT); - _WITHDRAWAL_QUEUE.grantRole(_WITHDRAWAL_QUEUE.PAUSE_ROLE(), address(this)); - vm.stopPrank(); - _WITHDRAWAL_QUEUE.pauseFor(PAUSE_INFINITELY); + _wait(_config.TIE_BREAK_ACTIVATION_TIMEOUT()); _activateNextState(); // Tiebreaker subcommittee 0 members = _tiebreakerSubCommittees[0].getMembers(); for (uint256 i = 0; i < _tiebreakerSubCommittees[0].quorum() - 1; i++) { vm.prank(members[i]); - _tiebreakerSubCommittees[0].voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); + _tiebreakerSubCommittees[0].sealableResume(address(_WITHDRAWAL_QUEUE)); (support, quorum, isExecuted) = - _tiebreakerSubCommittees[0].getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[0].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); assert(support < quorum); assert(isExecuted == false); } vm.prank(members[members.length - 1]); - _tiebreakerSubCommittees[0].voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); - (support, quorum, isExecuted) = - _tiebreakerSubCommittees[0].getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[0].sealableResume(address(_WITHDRAWAL_QUEUE)); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); assert(support == quorum); assert(isExecuted == false); - _tiebreakerSubCommittees[0].executeApproveSealableResume(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[0].executeSealableResume(address(_WITHDRAWAL_QUEUE)); (support, quorum, isExecuted) = _tiebreakerCommittee.getSealableResumeState( address(_WITHDRAWAL_QUEUE), _tiebreakerCommittee.getSealableResumeNonce(address(_WITHDRAWAL_QUEUE)) ); @@ -133,22 +138,20 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { members = _tiebreakerSubCommittees[1].getMembers(); for (uint256 i = 0; i < _tiebreakerSubCommittees[1].quorum() - 1; i++) { vm.prank(members[i]); - _tiebreakerSubCommittees[1].voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); + _tiebreakerSubCommittees[1].sealableResume(address(_WITHDRAWAL_QUEUE)); (support, quorum, isExecuted) = - _tiebreakerSubCommittees[1].getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[1].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); assert(support < quorum); assert(isExecuted == false); } vm.prank(members[members.length - 1]); - _tiebreakerSubCommittees[1].voteApproveSealableResume(address(_WITHDRAWAL_QUEUE), true); - (support, quorum, isExecuted) = - _tiebreakerSubCommittees[1].getApproveSealableResumeState(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[1].sealableResume(address(_WITHDRAWAL_QUEUE)); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); assert(support == quorum); assert(isExecuted == false); - // Approve proposal for scheduling - _tiebreakerSubCommittees[1].executeApproveSealableResume(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[1].executeSealableResume(address(_WITHDRAWAL_QUEUE)); (support, quorum, isExecuted) = _tiebreakerCommittee.getSealableResumeState( address(_WITHDRAWAL_QUEUE), _tiebreakerCommittee.getSealableResumeNonce(address(_WITHDRAWAL_QUEUE)) ); @@ -156,13 +159,7 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { uint256 lastProposalId = EmergencyProtectedTimelock(address(_dualGovernance.TIMELOCK())).getProposalsCount(); _tiebreakerCommittee.executeSealableResume(address(_WITHDRAWAL_QUEUE)); - uint256 proposalIdToExecute = - EmergencyProtectedTimelock(address(_dualGovernance.TIMELOCK())).getProposalsCount(); - assert(lastProposalId + 1 == proposalIdToExecute); - - // Waiting for submit delay pass - _wait(_config.AFTER_SUBMIT_DELAY()); - _dualGovernance.tiebreakerSchedule(proposalIdToExecute); + assertEq(_WITHDRAWAL_QUEUE.isPaused(), false); } } diff --git a/test/unit/EmergencyActivationCommittee.t.sol b/test/unit/EmergencyActivationCommittee.t.sol deleted file mode 100644 index aecc9036..00000000 --- a/test/unit/EmergencyActivationCommittee.t.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import {EmergencyActivationCommittee} from "../../contracts/committees/EmergencyActivationCommittee.sol"; - -import {ExecutiveCommitteeUnitTest, ExecutiveCommittee} from "./ExecutiveCommittee.t.sol"; - -contract EmergencyActivationCommitteeUnitTest is ExecutiveCommitteeUnitTest { - EmergencyActivationCommittee internal _emergencyActivationCommittee; - - EmergencyProtectedTimelockMock internal _emergencyProtectedTimelock; - - function setUp() public { - _emergencyProtectedTimelock = new EmergencyProtectedTimelockMock(); - _emergencyActivationCommittee = - new EmergencyActivationCommittee(_owner, _committeeMembers, _quorum, address(_emergencyProtectedTimelock)); - _executiveCommittee = ExecutiveCommittee(_emergencyActivationCommittee); - } -} - -contract EmergencyProtectedTimelockMock {} diff --git a/test/unit/ExecutiveCommittee.t.sol b/test/unit/ExecutiveCommittee.t.sol deleted file mode 100644 index b3e29d94..00000000 --- a/test/unit/ExecutiveCommittee.t.sol +++ /dev/null @@ -1,356 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import {UnitTest} from "test/utils/unit-test.sol"; - -import {Vm} from "forge-std/Test.sol"; - -import {ExecutiveCommittee} from "../../contracts/committees/ExecutiveCommittee.sol"; - -abstract contract ExecutiveCommitteeUnitTest is UnitTest { - ExecutiveCommittee internal _executiveCommittee; - - address internal _owner = makeAddr("COMMITTEE_OWNER"); - - address internal _stranger = makeAddr("STRANGER"); - - uint256 internal _membersCount = 13; - uint256 internal _quorum = 7; - address[] internal _committeeMembers = new address[](_membersCount); - - constructor() { - for (uint256 i = 0; i < _membersCount; ++i) { - _committeeMembers[i] = makeAddr(string(abi.encode(0xFE + i * _membersCount + 65))); - } - } - - function test_isMember() public { - for (uint256 i = 0; i < _membersCount; ++i) { - assertEq(_executiveCommittee.isMember(_committeeMembers[i]), true); - } - - assertEq(_executiveCommittee.isMember(_owner), false); - assertEq(_executiveCommittee.isMember(_stranger), false); - } - - function test_getMembers() public { - address[] memory committeeMembers = _executiveCommittee.getMembers(); - - assertEq(committeeMembers.length, _committeeMembers.length); - - for (uint256 i = 0; i < _membersCount; ++i) { - assertEq(committeeMembers[i], _committeeMembers[i]); - } - } - - function test_addMember_stranger_call() public { - address newMember = makeAddr("NEW_MEMBER"); - assertEq(_executiveCommittee.isMember(newMember), false); - - vm.prank(_stranger); - vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", _stranger)); - _executiveCommittee.addMember(newMember, _quorum); - - for (uint256 i = 0; i < _membersCount; ++i) { - vm.prank(_committeeMembers[i]); - vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", _committeeMembers[i])); - _executiveCommittee.addMember(newMember, _quorum); - } - } - - function test_addMember_reverts_on_duplicate() public { - address existedMember = _committeeMembers[0]; - assertEq(_executiveCommittee.isMember(existedMember), true); - - vm.prank(_owner); - vm.expectRevert(abi.encodeWithSignature("DuplicatedMember(address)", existedMember)); - _executiveCommittee.addMember(existedMember, _quorum); - } - - function test_addMember_reverts_on_invalid_quorum() public { - address newMember = makeAddr("NEW_MEMBER"); - assertEq(_executiveCommittee.isMember(newMember), false); - - vm.prank(_owner); - vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); - _executiveCommittee.addMember(newMember, 0); - - vm.prank(_owner); - vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); - _executiveCommittee.addMember(newMember, _membersCount + 2); - } - - function test_addMember() public { - address newMember = makeAddr("NEW_MEMBER"); - uint256 newQuorum = _quorum + 1; - - assertEq(_executiveCommittee.isMember(newMember), false); - - vm.prank(_owner); - vm.expectEmit(address(_executiveCommittee)); - emit ExecutiveCommittee.MemberAdded(newMember); - vm.expectEmit(address(_executiveCommittee)); - emit ExecutiveCommittee.QuorumSet(newQuorum); - _executiveCommittee.addMember(newMember, newQuorum); - - assertEq(_executiveCommittee.isMember(newMember), true); - - address[] memory committeeMembers = _executiveCommittee.getMembers(); - - assertEq(committeeMembers.length, _membersCount + 1); - assertEq(committeeMembers[committeeMembers.length - 1], newMember); - } - - function test_removeMember_stranger_call() public { - address memberToRemove = _committeeMembers[0]; - assertEq(_executiveCommittee.isMember(memberToRemove), true); - - vm.prank(_stranger); - vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", _stranger)); - _executiveCommittee.removeMember(memberToRemove, _quorum); - - for (uint256 i = 0; i < _membersCount; ++i) { - vm.prank(_committeeMembers[i]); - vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", _committeeMembers[i])); - _executiveCommittee.removeMember(memberToRemove, _quorum); - } - } - - function test_removeMember_reverts_on_member_is_not_exist() public { - assertEq(_executiveCommittee.isMember(_stranger), false); - - vm.prank(_owner); - vm.expectRevert(abi.encodeWithSignature("IsNotMember()")); - _executiveCommittee.removeMember(_stranger, _quorum); - } - - function test_removeMember_reverts_on_invalid_quorum() public { - address memberToRemove = _committeeMembers[0]; - assertEq(_executiveCommittee.isMember(memberToRemove), true); - - vm.prank(_owner); - vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); - _executiveCommittee.removeMember(memberToRemove, 0); - - vm.prank(_owner); - vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); - _executiveCommittee.removeMember(memberToRemove, _membersCount); - } - - function test_removeMember() public { - address memberToRemove = _committeeMembers[0]; - uint256 newQuorum = _quorum - 1; - - assertEq(_executiveCommittee.isMember(memberToRemove), true); - - vm.prank(_owner); - vm.expectEmit(address(_executiveCommittee)); - emit ExecutiveCommittee.MemberRemoved(memberToRemove); - vm.expectEmit(address(_executiveCommittee)); - emit ExecutiveCommittee.QuorumSet(newQuorum); - _executiveCommittee.removeMember(memberToRemove, newQuorum); - - assertEq(_executiveCommittee.isMember(memberToRemove), false); - - address[] memory committeeMembers = _executiveCommittee.getMembers(); - - assertEq(committeeMembers.length, _membersCount - 1); - for (uint256 i = 0; i < committeeMembers.length; ++i) { - assertNotEq(committeeMembers[i], memberToRemove); - } - } -} - -contract Target { - event Executed(); - - function trigger() public { - emit Executed(); - } -} - -contract ExecutiveCommitteeWrapper is ExecutiveCommittee { - Target internal _target; - - constructor( - address owner, - address[] memory newMembers, - uint256 executionQuorum, - uint256 timelock, - Target target - ) ExecutiveCommittee(owner, newMembers, executionQuorum, timelock) { - _target = target; - } - - function vote(bytes calldata data, bool support) public { - _vote(data, support); - } - - function execute(bytes calldata data) public { - _markExecuted(data); - _target.trigger(); - } - - function getVoteState(bytes calldata data) - public - view - returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) - { - return _getVoteState(data); - } - - function getSupport(bytes32 voteHash) public view returns (uint256 support) { - return _getSupport(voteHash); - } -} - -contract ExecutiveCommitteeInternalUnitTest is ExecutiveCommitteeUnitTest { - ExecutiveCommitteeWrapper internal _executiveCommitteeWrapper; - Target internal _target; - uint256 _timelock = 3600; - - function setUp() public { - _target = new Target(); - _executiveCommitteeWrapper = - new ExecutiveCommitteeWrapper(_owner, _committeeMembers, _quorum, _timelock, _target); - _executiveCommittee = ExecutiveCommittee(_executiveCommitteeWrapper); - } - - function test_getSupport() public { - bytes memory data = abi.encode(address(_target)); - bytes32 dataHash = keccak256(data); - - assertEq(_executiveCommitteeWrapper.getSupport(dataHash), 0); - - for (uint256 i = 0; i < _membersCount; ++i) { - assertEq(_executiveCommitteeWrapper.getSupport(dataHash), i); - vm.prank(_committeeMembers[i]); - _executiveCommitteeWrapper.vote(data, true); - assertEq(_executiveCommitteeWrapper.getSupport(dataHash), i + 1); - } - - assertEq(_executiveCommitteeWrapper.getSupport(dataHash), _membersCount); - - for (uint256 i = 0; i < _membersCount; ++i) { - assertEq(_executiveCommitteeWrapper.getSupport(dataHash), _membersCount - i); - vm.prank(_committeeMembers[i]); - _executiveCommitteeWrapper.vote(data, false); - assertEq(_executiveCommitteeWrapper.getSupport(dataHash), _membersCount - i - 1); - } - - assertEq(_executiveCommitteeWrapper.getSupport(dataHash), 0); - } - - function test_getVoteState() public { - bytes memory data = abi.encode(address(_target)); - - uint256 support; - uint256 execuitionQuorum; - bool isExecuted; - - (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getVoteState(data); - assertEq(support, 0); - assertEq(execuitionQuorum, _quorum); - assertEq(isExecuted, false); - - for (uint256 i = 0; i < _membersCount; ++i) { - (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getVoteState(data); - assertEq(support, i); - assertEq(execuitionQuorum, _quorum); - assertEq(isExecuted, false); - - vm.prank(_committeeMembers[i]); - _executiveCommitteeWrapper.vote(data, true); - - (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getVoteState(data); - assertEq(support, i + 1); - assertEq(execuitionQuorum, _quorum); - assertEq(isExecuted, false); - } - - (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getVoteState(data); - assertEq(support, _membersCount); - assertEq(execuitionQuorum, _quorum); - assertEq(isExecuted, false); - - _executiveCommitteeWrapper.execute(data); - - (support, execuitionQuorum, isExecuted) = _executiveCommitteeWrapper.getVoteState(data); - assertEq(support, _membersCount); - assertEq(execuitionQuorum, _quorum); - assertEq(isExecuted, true); - } - - function test_vote() public { - bytes memory data = abi.encode(address(_target)); - - bytes32 dataHash = keccak256(data); - - assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], dataHash), false); - - vm.prank(_committeeMembers[0]); - vm.expectEmit(address(_executiveCommitteeWrapper)); - emit ExecutiveCommittee.Voted(_committeeMembers[0], data, true); - _executiveCommitteeWrapper.vote(data, true); - assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], dataHash), true); - - vm.prank(_committeeMembers[0]); - vm.recordLogs(); - _executiveCommitteeWrapper.vote(data, true); - Vm.Log[] memory logs = vm.getRecordedLogs(); - assertEq(logs.length, 0); - assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], dataHash), true); - - vm.prank(_committeeMembers[0]); - vm.expectEmit(address(_executiveCommitteeWrapper)); - emit ExecutiveCommittee.Voted(_committeeMembers[0], data, false); - _executiveCommitteeWrapper.vote(data, false); - assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], dataHash), false); - - vm.prank(_committeeMembers[0]); - vm.recordLogs(); - _executiveCommitteeWrapper.vote(data, false); - logs = vm.getRecordedLogs(); - assertEq(logs.length, 0); - assertEq(_executiveCommitteeWrapper.approves(_committeeMembers[0], dataHash), false); - } - - function test_vote_reverts_on_executed() public { - bytes memory data = abi.encode(address(_target)); - - for (uint256 i = 0; i < _quorum; ++i) { - vm.prank(_committeeMembers[i]); - _executiveCommitteeWrapper.vote(data, true); - } - - _executiveCommitteeWrapper.execute(data); - - vm.prank(_committeeMembers[0]); - vm.expectRevert(abi.encodeWithSignature("VoteAlreadyExecuted()")); - _executiveCommitteeWrapper.vote(data, true); - } - - function test_execute_events() public { - bytes memory data = abi.encode(address(_target)); - - vm.prank(_stranger); - vm.expectRevert(abi.encodeWithSignature("QuorumIsNotReached()")); - _executiveCommitteeWrapper.execute(data); - - for (uint256 i = 0; i < _quorum; ++i) { - vm.prank(_committeeMembers[i]); - _executiveCommitteeWrapper.vote(data, true); - } - - vm.prank(_stranger); - vm.expectEmit(address(_executiveCommitteeWrapper)); - emit ExecutiveCommittee.VoteExecuted(data); - vm.expectEmit(address(_target)); - emit Target.Executed(); - _executiveCommitteeWrapper.execute(data); - - vm.prank(_stranger); - vm.expectRevert(abi.encodeWithSignature("VoteAlreadyExecuted()")); - _executiveCommitteeWrapper.execute(data); - } -} diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index fa9042ea..0a7ac71e 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -18,6 +18,8 @@ import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecuti import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; +import {ResealManager} from "contracts/ResealManager.sol"; + import { ExecutorCall, EmergencyState, @@ -42,7 +44,7 @@ import { import {ExecutorCallHelpers} from "../utils/executor-calls.sol"; import {Utils, TargetMock, console} from "../utils/utils.sol"; -import {DAO_VOTING, ST_ETH, WST_ETH, WITHDRAWAL_QUEUE} from "../utils/mainnet-addresses.sol"; +import {DAO_VOTING, ST_ETH, WST_ETH, WITHDRAWAL_QUEUE, DAO_AGENT} from "../utils/mainnet-addresses.sol"; struct Balances { uint256 stETHAmount; @@ -92,6 +94,8 @@ contract ScenarioTestBlueprint is Test { SingleGovernance internal _singleGovernance; DualGovernance internal _dualGovernance; + ResealManager internal _resealManager; + address[] internal _sealableWithdrawalBlockers = [WITHDRAWAL_QUEUE]; // --- @@ -540,7 +544,7 @@ contract ScenarioTestBlueprint is Test { uint256 subCommitteesCount = 2; _tiebreakerCommittee = - new TiebreakerCore(address(_adminExecutor), new address[](0), 1, address(_dualGovernance)); + new TiebreakerCore(address(_adminExecutor), new address[](0), 1, address(_dualGovernance), 0); for (uint256 i = 0; i < subCommitteesCount; ++i) { address[] memory committeeMembers = new address[](subCommitteeMembersCount); @@ -597,11 +601,24 @@ contract ScenarioTestBlueprint is Test { ); } + _resealManager = new ResealManager(address(_adminExecutor), address(_dualGovernance)); + + vm.prank(DAO_AGENT); + _WITHDRAWAL_QUEUE.grantRole( + 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(_resealManager) + ); + vm.prank(DAO_AGENT); + _WITHDRAWAL_QUEUE.grantRole( + 0x2fc10cc8ae19568712f7a176fb4978616a610650813c9d05326c34abb62749c7, address(_resealManager) + ); + if (governance == address(_dualGovernance)) { _adminExecutor.execute( address(_dualGovernance), 0, - abi.encodeCall(_dualGovernance.setTiebreakerCommittee, (_TIEBREAK_COMMITTEE)) + abi.encodeCall( + _dualGovernance.setTiebreakerProtection, (address(_tiebreakerCommittee), address(_resealManager)) + ) ); } _adminExecutor.execute(address(_timelock), 0, abi.encodeCall(_timelock.setGovernance, (governance))); diff --git a/test/utils/utils.sol b/test/utils/utils.sol index 9072e0ce..15224876 100644 --- a/test/utils/utils.sol +++ b/test/utils/utils.sol @@ -54,7 +54,7 @@ library Utils { function selectFork() internal { vm.createSelectFork(vm.envString("MAINNET_RPC_URL")); - vm.rollFork(18984396); + vm.rollFork(20218312); } function encodeEvmCallScript(address target, bytes memory data) internal pure returns (bytes memory) { From 157c9ff36398779c7eec5870d8a65322b06718e7 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 5 Jul 2024 12:46:43 +0300 Subject: [PATCH 37/38] governance reset handling --- contracts/ResealManager.sol | 36 ++++++++++++-------------- test/utils/scenario-test-blueprint.sol | 2 +- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/contracts/ResealManager.sol b/contracts/ResealManager.sol index add4b9a0..5fdb2137 100644 --- a/contracts/ResealManager.sol +++ b/contracts/ResealManager.sol @@ -2,25 +2,25 @@ pragma solidity 0.8.23; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ISealable} from "./interfaces/ISealable.sol"; -contract ResealManager is Ownable { - error SealableWrongPauseState(); - error SenderIsNotManager(); +interface IEmergencyProtectedTimelock { + function getGovernance() external view returns (address); +} - event ManagerSet(address newManager); +contract ResealManager { + error SealableWrongPauseState(); + error SenderIsNotGovernance(); + error NotAllowed(); uint256 public constant PAUSE_INFINITELY = type(uint256).max; + address public immutable EMERGENCY_PROTECTED_TIMELOCK; - address public manager; - - constructor(address owner, address managerAddress) Ownable(owner) { - manager = managerAddress; - emit ManagerSet(managerAddress); + constructor(address emergencyProtectedTimelock) { + EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; } - function reseal(address[] memory sealables) public onlyManager { + function reseal(address[] memory sealables) public onlyGovernance { for (uint256 i = 0; i < sealables.length; ++i) { uint256 sealableResumeSinceTimestamp = ISealable(sealables[i]).getResumeSinceTimestamp(); if (sealableResumeSinceTimestamp < block.timestamp || sealableResumeSinceTimestamp == PAUSE_INFINITELY) { @@ -31,7 +31,7 @@ contract ResealManager is Ownable { } } - function resume(address sealable) public onlyManager { + function resume(address sealable) public onlyGovernance { uint256 sealableResumeSinceTimestamp = ISealable(sealable).getResumeSinceTimestamp(); if (sealableResumeSinceTimestamp < block.timestamp) { revert SealableWrongPauseState(); @@ -39,14 +39,10 @@ contract ResealManager is Ownable { Address.functionCall(sealable, abi.encodeWithSelector(ISealable.resume.selector)); } - function setManager(address newManager) public onlyOwner { - manager = newManager; - emit ManagerSet(newManager); - } - - modifier onlyManager() { - if (msg.sender != manager) { - revert SenderIsNotManager(); + modifier onlyGovernance() { + address governance = IEmergencyProtectedTimelock(EMERGENCY_PROTECTED_TIMELOCK).getGovernance(); + if (msg.sender != governance) { + revert SenderIsNotGovernance(); } _; } diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 0a7ac71e..fab9202d 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -601,7 +601,7 @@ contract ScenarioTestBlueprint is Test { ); } - _resealManager = new ResealManager(address(_adminExecutor), address(_dualGovernance)); + _resealManager = new ResealManager(address(_timelock)); vm.prank(DAO_AGENT); _WITHDRAWAL_QUEUE.grantRole( From 01943be25e15a075587c170e43aa1291a0b781f7 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 5 Jul 2024 13:09:28 +0300 Subject: [PATCH 38/38] fix miss --- .../committees/EmergencyExecutionCommittee.sol | 6 +++--- contracts/committees/ProposalsList.sol | 14 +++++++------- contracts/committees/TiebreakerCore.sol | 2 +- test/scenario/tiebraker.t.sol | 1 - 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol index 6586dfc3..1c670227 100644 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -55,10 +55,10 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { function _encodeEmergencyExecute(uint256 proposalId) private - view + pure returns (bytes memory proposalData, bytes32 key) { - proposalData = abi.encode(ProposalType.EmergencyExecute, bytes32(proposalId)); + proposalData = abi.encode(ProposalType.EmergencyExecute, proposalId); key = keccak256(proposalData); } @@ -87,7 +87,7 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { ); } - function _encodeEmergencyResetProposalKey() internal view returns (bytes32) { + function _encodeEmergencyResetProposalKey() internal pure returns (bytes32) { return keccak256(abi.encode(ProposalType.EmergencyReset, bytes32(0))); } } diff --git a/contracts/committees/ProposalsList.sol b/contracts/committees/ProposalsList.sol index ecdf9786..625a5841 100644 --- a/contracts/committees/ProposalsList.sol +++ b/contracts/committees/ProposalsList.sol @@ -8,30 +8,30 @@ contract ProposalsList { EnumerableProposals.Bytes32ToProposalMap internal _proposals; - function getProposals(uint256 offset, uint256 limit) public returns (Proposal[] memory proposals) { + function getProposals(uint256 offset, uint256 limit) public view returns (Proposal[] memory proposals) { bytes32[] memory keys = _proposals.orederedKeys(offset, limit); uint256 length = keys.length; - proposals = new Proposal[](keys.length); + proposals = new Proposal[](length); - for (uint256 i = 0; i < keys.length; ++i) { + for (uint256 i = 0; i < length; ++i) { proposals[i] = _proposals.get(keys[i]); } } - function getProposalAt(uint256 index) public returns (Proposal memory) { + function getProposalAt(uint256 index) public view returns (Proposal memory) { return _proposals.at(index); } - function getProposal(bytes32 key) public returns (Proposal memory) { + function getProposal(bytes32 key) public view returns (Proposal memory) { return _proposals.get(key); } - function proposalsLength() public returns (uint256) { + function proposalsLength() public view returns (uint256) { return _proposals.length(); } - function orederedKeys(uint256 offset, uint256 limit) public returns (bytes32[] memory) { + function orederedKeys(uint256 offset, uint256 limit) public view returns (bytes32[] memory) { return _proposals.orederedKeys(offset, limit); } diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol index 3f45e90a..a380036a 100644 --- a/contracts/committees/TiebreakerCore.sol +++ b/contracts/committees/TiebreakerCore.sol @@ -58,7 +58,7 @@ contract TiebreakerCore is HashConsensus, ProposalsList { } function _encodeScheduleProposal(uint256 proposalId) internal pure returns (bytes memory data, bytes32 key) { - data = abi.encode(ProposalType.ScheduleProposal, data); + data = abi.encode(ProposalType.ScheduleProposal, proposalId); key = keccak256(data); } diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol index 1c465fbf..43890ccf 100644 --- a/test/scenario/tiebraker.t.sol +++ b/test/scenario/tiebraker.t.sol @@ -157,7 +157,6 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { ); assert(support == quorum); - uint256 lastProposalId = EmergencyProtectedTimelock(address(_dualGovernance.TIMELOCK())).getProposalsCount(); _tiebreakerCommittee.executeSealableResume(address(_WITHDRAWAL_QUEUE)); assertEq(_WITHDRAWAL_QUEUE.isPaused(), false);