From 854918a2443873876d603d84c25b26187187eb11 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 13 Mar 2024 11:59:25 +0300 Subject: [PATCH 001/134] 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 002/134] 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 003/134] 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 004/134] 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 005/134] 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 006/134] 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 007/134] 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 eace4d0d56fb9f654059aa1f24a3be5def1665dd Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Thu, 11 Apr 2024 00:31:48 +0300 Subject: [PATCH 008/134] docs: add mechanism design changelog --- docs/mechanism.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/docs/mechanism.md b/docs/mechanism.md index 9de600ff..4109b2fb 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -266,6 +266,7 @@ RageQuitEthClaimTimelockGrowthStartSeqNumber = 2 RageQuitEthClaimTimelockGrowthCoeffs = (0, TODO, TODO) ``` + ### Gate Seal behavior and Tiebreaker Committee The [Gate Seal](https://docs.lido.fi/contracts/gate-seal) is an existing circuit breaker mechanism designed to be activated in the event of a zero-day vulnerability in the protocol contracts being found or exploited and empowering a DAO-elected committee to pause certain protocol functionality, including withdrawals, for a predefined duration enough for the DAO to vote for and execute a remediation. When this happens, the committee immediately loses its power. If this never happens, the committee's power also expires after a pre-configured amount of time passes since its election. @@ -308,6 +309,7 @@ TiebreakerExecutionTimelock = 1 month TieBreakerActivationTimeout = 1 year ``` + ## Dual governance scope Dual governance should cover any DAO proposal that could potentially affect the protocol users, including: @@ -327,3 +329,84 @@ Dual governance should not cover: * Emergency actions triggered by circuit breaker committees and contracts, including activation of any Gate Seal. These actions must be limited in scope and time and must be unable to change any protocol code. * DAO decisions related to spending and managing the DAO treasury. + + +## Changelog + +### 2024-04-02 + +* Added a minimum lock time of (w)stETH/wNFTs in the signalling escrow to prevent triggering state transitions using flash loans. + + > An example attack: + > 1. Take a flash loan of `FirstSealRageQuitSupport` stETH, lock into the signalling escrow, and unlock in the same transaction. + > 2. This triggers the Veto Signalling state that will last for `VetoSignallingMinDuration` followed by the Veto Cooldown. + > 3. At the block the Veto Cooldown transitions to Normal, frontrun any transition-triggering transaction with a bundle that performs the transition and includes the transaction from step 1. Go to 2. + +* Added a lower limit on the Normal state duration to prevent `Veto Signalling -> Normal -> Veto Signalling` cycling attacks by front-running the DAO execution. + + > An example attack: the same steps as in the previous item but use own/borrowed capital instead of the flash-borrowed one. + +* Added a lower limit on the time between exiting the Veto Signalling Deactivation sub-state and re-entering it (as well as transitioning to the Rage Quit) to prevent `(Veto Signalling, Deactivation) -> Veto Signalling -> (Veto Signalling, Deactivation)` cycling attacks by gradually locking stETH in the signalling escrow and thus blocking submission of new DAO proposals. + + > An example attack: + > 1. An attacker controls `SecondSealRageQuitSupport` stETH. They divide these tokens into N parts so that the first part generates the `FirstSealRageQuitSupport` rage quit support and with each next part added, the Veto Signalling duration is increased by exactly `VetoSignallingDeactivationMinDuration` plus one block. + > 2. The attacker locks the first part of stETH into signalling escrow, triggering transition to Veto Signalling. + > 3. The attacker waits until the Veto Signalling Deactivation sub-state gets entered, and then waits for `VetoSignallingDeactivationMinDuration` minus one block. + > 4. The attacker locks the next stETH part into the signalling escrow, exiting the Deactivation state. After one block, the Deactivation state gets entered once again. Go to 3 (if not all stETH parts are locked yet). + + > This way, the attacker can deprive the DAO of the ability to submit proposals (which is impossible in the Deactivation sub-state) for almost the whole `VetoSignallingMaxDuration` except a limited number of blocks. + +* Replaced the `Rage Quit -> Normal` transition with the `Rage Quit -> Veto Cooldown` transition to prevent front-running the DAO to sequentially enter Veto Signalling without incrementing the rage quit sequence number and thus increasing the rage quit ETH claim lock time. + + > An example attack: + > + > 1. An attacker controls `2 * SecondSealRageQuitSupport` stETH. They lock the first half into the signalling escrow, triggering Rage Quit. + > 2. In the beginning of the block the Rage Quit ends, the attacker includes a bundle with two transaction: the first triggers the Rage Quit -> Normal transition, the second locks the unused half of stETH into the signalling escrow, triggering the Normal -> Rage Quit transition. + > 3. While Rage Quit is in progress, wait until ETH claim timelock ends for the other half of stETH, claim them to ETH and stake/swap to stETH. Go to 2. + +* Decreased the proposed Veto Cooldown state duration from 1 day to 5 hours. + + > The new proposed time should be enough for the DAO (or anyone since execution is permissionless) to trigger proposal execution. At the same time, given the changes from the previous item, the DAO submitting a proposal at the end of Rage Quit now leads to the proposal's min execution timelock being effectively reduced by the veto cooldown duration so the latter should be set to a minumally viable value. + > + > An example attack: + > + > 0. If `VetoCooldownDuration >= ProposalExecutionMinTimelock`, the DAO can submit a proposal at the block preceding the one in which the Rage Quit ends. Since Rage Quit transitions to Veto Cooldown that allows proposal execution, the proposal will inevitably become executable after `ProposalExecutionMinTimelock` without stakers having the ability to extend its timelock and potentially leave before the proposal becomes executable. + > 1. If `VetoCooldownDuration < ProposalExecutionMinTimelock`, the DAO can submit a proposal `ProposalExecutionMinTimelock - VetoCooldownDuration - one_block` seconds before the Rage Quit ends. + > 2. Now, in order to extend the execution timelock of this proposal, stakers have the time until Rage Quit ends to lock enough stETH into the signalling escrow to generate `FirstSealRageQuitThreshold`. Otherwise, the proposal will become executable while the Veto Cooldown state lasts without stakers having the ability to extend its timelock and potentially leave before the proposal becomes executable. + > 3. This way, the effective execution timelock of the proposal is reduced by `VetoCooldownDuration`. That's ok if `VetoCooldownDuration << ProposalExecutionMinTimelock` but is not ok if they're comparable. + +### [2024-03-22](https://github.com/skozin/hackmd-notes/blob/35a2190a130ed6c92c231255be9bffd1f0d07a6c/dual-governance-design.md) + +* Made the Veto Signalling Deactivation state duration dependent on the last time a proposal was submitted during Veto Signalling. + + > An example attack: + > 1. A malicious DAO actor locks `SecondSealRageQuitSupport` tokens in the veto signalling escrow, triggering the transition to Veto Signalling. + > 2. Waits until the `VetoSignallingMaxDuration` minus one block passes, submits a malicious proposal, and withdraws their tokens. This triggers the entrance of the Deactivation state. + > 3. Now, honest stakers have only the `VetoSignallingDeactivationDuration` to enter the signalling escrow with no less than the `SecondSealRageQuitSupport` tokens and trigger rage quit. Otherwise, the Veto Cooldown state gets activated and the malicious proposal becomes executable. + +* Added the Tiebreaker execution timelock. +* Renamed parameters and changed some terms for clarity. + +### [2024-02-09](https://github.com/skozin/hackmd-notes/blob/22dbd2f22a35de44cca8d9cdfc556e6d0ce17c25/dual-governance-design.md) + +* Removed the Rage Quit Accumulation state since it allowed a sophisticated actor to bypass locking (w)stETH in the escrow while still blocking the DAO execution (which, in turn, significantly reduced the cost of the "constant veto" DoS attack on the governance). +* Added details on veto signalling and rage quit escrows. +* Changed the post-rage quit ETH withdrawal timelock to be dynamic instead of static to further increase the cost of the "constant veto" DoS attack while keeping the default timelock adequate. + +### [2023-12-05](https://github.com/skozin/hackmd-notes/blob/9a4eba7eb48de2915321e7875fbb7285ebb46949/dual-governance-design.md) + +* Removed the stETH balance snapshotting mechanism since the Tiebreaker Committee already allows recovering from an infinite stETH mint vulnerability. +* Added support for using withdrawal NFTs in the veto escrow. + +### [2023-10-23](https://github.com/skozin/hackmd-notes/blob/e84727ec658983761fa9c6a897de00a11f42edfe/dual-governance-design.md) + +A major re-work of the DG mechanism (previous version [here](https://hackmd.io/@lido/BJKmFkM-i)). + +* Replaced the global settlement mechanism with the rage quit mechanism (i.e. local settlement, a protected exit from the protocol). Removed states: Global Settlement; added states: Rage Quit Accumulation, Rage Quit. +* Removed the veto lift voting mechanism. +* Re-worked the DG activation and negotiation mechanism, replacing Veto Voting and Veto Negotiation states with the Veto Signalling state. +* Added the Veto Cooldown state. +* Added the $KillAllPendingProposals$ DAO decision. +* Added stETH balance snapshotting mechanism. +* Specified inter-operation between the Gate Seal mechanism and DG. +* Added the Tiebreaker Committee. From 8d87e394e9ca8b4cf488af4588546e9033fb029d Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Thu, 11 Apr 2024 00:44:34 +0300 Subject: [PATCH 009/134] docs: update the mechanism design (see the changelog) --- docs/mechanism.md | 130 +++++++++++++++++++++++++++++++--------------- 1 file changed, 87 insertions(+), 43 deletions(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index 4109b2fb..237f4f81 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -1,4 +1,4 @@ -**Working draft**, the latest version is published at https://hackmd.io/@skozin/rkD1eUzja. +**Working draft** --- @@ -82,7 +82,7 @@ where * $\text{shares}(N_i)$ is the stETH shares amount corresponding to the unfinalized withdrawal NFT $N_i$, * $\text{eth}(N_i)$ is the withdrawn ETH amount associated with the finalized withdrawal NFT $N_i$. -Changes in the rage quit support act as the main driver for the global governance state transitions. +All of the above values implicitly depend on the **current block time** $t$, thus $R = R(t)$. Changes of $R$ act as the main driver for the global governance state transitions. ```env # Proposed values, to be modeled and refined @@ -106,12 +106,20 @@ The DG mechanism can be described as a state machine defining the global governa Let's now define these states and transitions. +> Note: when a state has multiple outgoing transitions, their conditions are avaluated in the order they're listed in the text. If a condition evaluates to true, the further evaluation stops and the corresponsing transition is triggered. + ### Normal state -The Normal state is the state the mechanism is designed to spend the most time within. The DAO can submit the approved proposals to the DG and execute them after the standard timelock of `ProposalExecutionMinTimelock` days. +The Normal state is the state the mechanism is designed to spend the most time within. The DAO can submit the approved proposals to the DG and execute them after the standard timelock of `ProposalExecutionMinTimelock` days passes since the proposal's submission. + +**Transition to Veto Signalling**. If, while the state is active, the following condition becomes true: + +```math +\big( R(t) > R_1 \big) \, \land \, \big( t - t^N_{act} > T^N_{min} \big) +``` -If, while the state is active, the [rage quit support](#Signalling-Escrow) exceeds `FirstSealRageQuitSupport`, and at least `NormalStateMinDuration` seconds passed since the moment of the state activation, the governance is transferred into the Veto Signalling state. +where $R_1$ is `FirstSealRageQuitSupport`, $t^N_{act}$ is the time the Normal state was entered, and $T^N_{min}$ is `NormalStateMinDuration`, the Normal state is exited and the Veto Signalling state is entered. ```env # Proposed values, to be modeled and refined @@ -132,46 +140,57 @@ In this state, the DAO can submit approved proposals to the DG but cannot execut The only proposal that can be executed by the DAO is the special $CancelAllPendingProposals$ action that cancels all proposals that were pending at the moment of this execution, making them forever unexecutable. This mechanism provides a way for the DAO and stakers to negotiate and de-escalate if consensus is reached. -The **current duration** $T^S(t)$ of the Veto Signalling state is the time passed since the activation of the state. The time spent in the [Deactivation](#Deactivation-sub-state) sub-state is counted towards the current Veto Signalling duration (since the parent state remains active). +To define transitions from this state, let's first introduce several time values. -The **target duration** $T^S_{target}(R)$ of the state depends on the current rage quit support $R$ and can be calculated as follows: +The **time of activation** of the Veto Signalling state $t^S_{act}$ is the time the state was entered. Entering and exiting the Deactivation sub-state doesn't affect this value. + +The **time of re-activation** of the Veto Signalling state $t^S_{react}$ is the time the Deactivation sub-state was last exited without exiting the parent Veto Signalling state. + +The **time of last proposal submission** $t_{prop}$ is the time the last proposal was submitted to the DG subsystem. + +The **dynamic timelock duration** $T_{lock}(R)$ depends on the current rage quit support $R = R(t)$ and can be calculated as follows: ```math -T^S_{target}(R) = +T_{lock}(R) = \left\{ \begin{array}{lr} 0, & \text{if } R \lt R_1 \\ L(R), & \text{if } R_1 \leq R \leq R_2 \\ - T^S_{max}, & \text{if } R \gt R_2 + L_{max}, & \text{if } R \gt R_2 \end{array} \right. ``` ```math -L(R) = T^S_{min} + \frac{(R - R_1)} {R_2 - R_1} (T^S_{max} - T^S_{min}) +L(R) = L_{min} + \frac{(R - R_1)} {R_2 - R_1} (L_{max} - L_{min}) ``` -where $R_1$ is `FirstSealRageQuitSupport`, $R_2$ is `SecondSealRageQuitSupport`, $T^S_{min}$ is `VetoSignallingMinDuration`, $T^S_{max}$ is `VetoSignallingMaxDuration`. The dependence of the target duration on the rage quit support can be illustrated by the following graph: +where $R_1$ is `FirstSealRageQuitSupport`, $R_2$ is `SecondSealRageQuitSupport`, $L_{min}$ is `DynamicTimelockMinDuration`, $L_{max}$ is `DynamicTimelockMaxDuration`. The dependence of the dynamic timelock on the rage quit support $R$ can be illustrated by the following graph: -![image](https://github.com/lidofinance/dual-governance/assets/1699593/e118e856-7cb1-438a-9bda-bcb6b3960cf0) +![image](https://github.com/lidofinance/dual-governance/assets/1699593/b98dd9f1-1e55-4b5d-8ce1-56539f4cc3f8) -When the current rage quit support changes due to stakers locking or unlocking tokens into/out of the signalling escrow or the total stETH supply changing, the target duration is re-evaluated. +When the current rage quit support changes due to stakers locking or unlocking tokens into/out of the signalling escrow or the total stETH supply changing, the dynamic timelock duration is re-evaluated. -The **time since re-activation** $T^{Sr}(t)$ is the time passed since the Deactivation sub-state was exited the last time, leaving only the parent Veto Signalling state active. +Let's now define the outgoing transitions. -If, while Veto Signalling is active, the following condition becomes true: +**Transition to Rage Quit**. If, while Veto Signalling is active (including while the Deactivation sub-state is active), the following expression becomes true: ```math -\left(T^S(t) > T^S_{target}(R)\right) \, \land \, \left(T^{Sr}(t) > T^{Sr}_{min}\right) +\big( t - t^S_{act} > L_{max} \big) \, \land \, \big( R > R_2 \big) ``` -where $T^{Sr}_{min}$ is `VetoSignallingMinReactivationDuration`, then either of the following happens: +the Veto Signalling state is exited and the Rage Quit state is entered. -1. if the rage quit support $R$ is below the `SecondSealRageQuitSupport`, the Deactivation sub-state of the Veto Signalling state is entered without exiting the parent Veto Signalling state, -2. otherwise, the Veto Signalling state is exited and the governance is transferred to the Rage Quit state (this can happen iff the governance has already spent `VetoSignallingMaxDuration` in this state). +**Transition to Deactivation**. If, while Veto Signalling is active and the Deactivation sub-state is not active, the following expression becomes true: + +```math +\left( t - \max \left\{ t^S_{act} \,,\, t_{prop} \right\} > T_{lock}(R) \right) \, \land \, \left( t - t^S_{react} > T^{Sr}_{min} \right) +``` + +where $T^{Sr}_{min}$ is `VetoSignallingMinReactivationDuration`, then the Deactivation sub-state of the Veto Signalling state is entered without exiting the parent Veto Signalling state. ```env # Proposed values, to be modeled and refined -VetoSignallingMinDuration = 5 days -VetoSignallingMaxDuration = 45 days +DynamicTimelockMinDuration = 5 days +DynamicTimelockMaxDuration = 45 days VetoSignallingMinReactivationDuration = 5 hours SecondSealRageQuitSupport = 0.1 ``` @@ -180,38 +199,46 @@ SecondSealRageQuitSupport = 0.1 The sub-state's purpose is to allow all stakers to observe the Veto Signalling being deactivated and react accordingly before non-cancelled proposals can be executed. In this sub-state, the DAO cannot submit proposals to the DG or execute pending proposals. -Since this is a sub-state, the time it's being active counts towards the parent Veto Signalling state duration. - -The maximum time this sub-state can remain active, $T^D_{max}$, is calculated at the moment it gets entered as follows: if there were no proposals submitted to the DG since the last activation of the Veto Signalling state, then +**Transition to the parent state**. If, while the sub-state is active, the following condition becomes true: ```math -T^D_{max} = T^D_{min} +t - \max \left\{ t^S_{act} \,,\, t_{prop} \right\} \leq \, T_{lock}(R) ``` -where $T^D_{min}$ is `VetoSignallingDeactivationMinDuration`. Otherwise, it's defined by the following expression: +then the Deactivation sub-state is exited so only the parent Veto Signalling state remains active. + +**Transition to Veto Cooldown**. If, while the sub-state is active, the following condition becomes true: ```math -T^D_{max} = \max \left\{ T^D_{min}, \; t_{prop} + T^S_{max} - t \right\} +t - t^{SD}_{act} > \, T^{SD}_{max} ``` -where $t_{prop}$ is the moment the last proposal was submitted to the DG, $T^S_{max}$ is `VetoSignallingMaxDuration`, $t$ is the current time, i.e. the moment the Deactivation state is entered at. - -If the current duration of the Deactivation state becomes larger than $T^D_{max}$, the Deactivation sub-state is exited along with its parent Veto Signalling state and the governance is transferred to the Veto Cooldown state. - -If, while the sub-state is active and as the result of the rage quit support changing, the target duration of the Veto Signalling state becomes more than its current duration (which includes the time the Deactivation sub-state is being active), the Deactivation sub-state is exited so only the main Veto Signalling state remains active. - -If, while the sub-state is active, the rage quit support exceeds the `SecondSealRageQuitSupport` AND the current duration of the Veto Signalling state exceeds the `VetoSignallingMaxDuration`, the Deactivation sub-state and its parent Veto Signalling state are exited and the governance is transferred to the Rage Quit state. +where $t^{SD}_{act}$ is the time the Deactivation sub-state was entered and $T^{SD}_{max}$ is `VetoSignallingDeactivationMaxDuration`, then the Deactivation sub-state is exited along with its parent Veto Signalling state and the Veto Cooldown state is entered. ```env # Proposed values, to be modeled and refined -VetoSignallingDeactivationMinDuration = 3 days +VetoSignallingDeactivationMaxDuration = 3 days ``` ### Veto Cooldown state -In the Veto Cooldown state, the DAO cannot submit proposals to the DG but can execute pending non-cancelled proposals. It exists to guarantee that no staker possessing `FirstSealRageQuitSupport` stETH can lock the governance indefinitely without rage quitting the protocol. +In the Veto Cooldown state, the DAO cannot submit proposals to the DG but can execute pending non-cancelled proposals. It exists to guarantee that no staker possessing enough stETH to generate `FirstSealRageQuitSupport` can lock the governance indefinitely without rage quitting the protocol. -The state duration is fixed at `VetoCooldownDuration`. After this time passes since the state activation, the state is exited and the governance is transferred either to the Normal state (if the rage quit support at that moment is less than `FirstSealRageQuitSupport`) or to the Veto Signalling state (otherwise). +**Transition to Veto Signalling**. If, while the state is active, the following condition becomes true: + +```math +\big( t - t^C_{act} > T^C \big) \,\land\, \big( R(t) > R_1 \big) +``` + +where $t^{C}_{act}$ is the time the Veto Cooldown state was entered and $T^{C}$ is `VetoCooldownDuration`, then the Veto Cooldown state is exited and the Veto Signalling state is entered. + +**Transition to Normal**. If, while the state is active, the following condition becomes true: + +```math +\big( t - t^C_{act} > T^C \big) \,\land\, \big( R(t) \leq R_1 \big) +``` + +then the Veto Cooldown state is exited and the Normal state is entered. ```env # Proposed values, to be modeled and refined @@ -233,16 +260,18 @@ In this state, the DAO is allowed submit proposals to the DG but cannot execute The state lasts until the withdrawal started in 2) is complete, i.e. until all batch withdrawal NFTs generated from (w)stETH that was locked in the escrow are fulfilled and claimed, plus `RageQuitExtensionDelay` days. -If, prior to the Rage Quit state being entered, a staker locked a withdrawal NFT into the signalling escrow, this NFT remains locked in the rage quit escrow. When such an NFT becomes fulfilled, the staker is allowed to burn this NFT and convert it to plain ETH, although still locked in the escrow. This allows stakers to derisk their ETH as early as possible by removing any dependence on the DAO decisions (remember that the withdrawal NFT contract is potentially upgradeable by the DAO but the rage quit escrow is immutable). +If, prior to the Rage Quit state being entered, a staker locked a withdrawal NFT into the signalling escrow, this NFT remains locked in the rage quit escrow. When such an NFT becomes fulfilled, the staker is allowed to burn this NFT and convert it to plain ETH, although still locked in the escrow. This allows stakers to derisk their ETH as early as possible by removing any dependence on the DAO-controlled code (remember that the withdrawal NFT contract is potentially upgradeable by the DAO but the rage quit escrow is immutable). -Since batch withdrawal NFTs are generated after the NFTs that were locked by stakers into the escrow directly, the withdrawal queue mechanism guarantees that, by the time batch NFTs are fulfilled, all individually locked NFTs are fulfilled as well and can be claimed. Together with the extension delay, this guarantees that any staker having a withdrawal NFT locked in the rage quit escrow has at least `RageQuitExtensionDelay` days to convert it to escrow-locked ETH before the DAO execution is unblocked. +Since batch withdrawal NFTs are generated after the NFTs that were locked by stakers into the escrow directly, the withdrawal queue mechanism (external to the DG) guarantees that, by the time batch NFTs are fulfilled, all individually locked NFTs are fulfilled as well and can be claimed. Together with the extension delay, this guarantees that any staker having a withdrawal NFT locked in the rage quit escrow has at least `RageQuitExtensionDelay` days to convert it to escrow-locked ETH before the DAO execution is unblocked. When the withdrawal is complete and the extension delay elapses, two things happen simultaneously: 1. A timelock lasting $W(i)$ days is started, during which the withdrawn ETH remains locked in the rage quit escrow. After the timelock elapses, stakers who participated in the rage quit can obtain their ETH from the rage quit escrow. -2. The governance exits the Rage Quit state. +2. The Rage Quit state is exited. -The next state depends on the current rage quit support (which depends on the amount of tokens locked in the veto signalling escrow deployed in 3): if it exceeds `FirstSealRageQuitSupport`, the governance is transferred to the Veto Signalling state; otherwise, to the Veto Cooldown state. +**Transition to Veto Signalling**. If, at the moment of the Rage Quit state exit, $R(t) > R_1$, the Veto Signalling state is entered. + +**Transition to Veto Cooldown**. If, at the moment of the Rage Quit state exit, $R(t) \leq R_1$, the Veto Cooldown state is entered. The duration of the ETH claim timelock $W(i)$ is a non-linear function that depends on the rage quit sequence number $i$ (see below): @@ -277,7 +306,7 @@ The DG mechanism introduces a dynamic timelock on DAO proposals dependent on sta #### Gate Seal behaviour (updated) -If, at any moment in time, two predicates become true simultaneously: +If, at any moment in time, two conditions become true simultaneously: 1. any DAO-managed contract functionality is paused by a Gate Seal; 2. the DAO execution is blocked by the DG mechanism (i.e. the global governance state is Veto Signalling, Veto Signalling Deactivation, or Rage Quit), @@ -296,8 +325,8 @@ To resolve the potential deadlock, the mechanism contains a third-party arbiter Specifically, the Tiebreaker committee can execute any pending proposal submitted by the DAO to DG, subject to a timelock of `TiebreakerExecutionTimelock` days, iff any of the following two conditions is true: -* **Tiebreaker Condition A**: (governance state is Rage Quit) AND (protocol withdrawals are paused by a Gate Seal). -* **Tiebreaker Condition B**: (governance state is Rage Quit) AND (last time governance exited Normal or Veto Cooldown state was more than `TiebreakerActivationTimeout` days ago). +* **Tiebreaker Condition A**: (governance state is Rage Quit) $\land$ (protocol withdrawals are paused by a Gate Seal). +* **Tiebreaker Condition B**: (governance state is Rage Quit) $\land$ (last time governance exited Normal or Veto Cooldown state was more than `TiebreakerActivationTimeout` days ago). The Tiebreaker committee should be composed of multiple sub-committees covering different interest groups within the Ethereum community (e.g. largest DAOs, EF, L2s, node operators, OGs) and should require approval from a supermajority of sub-committees in order to execute a pending proposal. The approval by each sub-committee should require the majority support within the sub-committee. No sub-committee should contain more than $1/4$ of the members that are also members of the withdrawals Gate Seal committee. @@ -333,6 +362,21 @@ Dual governance should not cover: ## Changelog +### 2024-04-10 + +* Redesigned the Veto Signalling exit conditions and the Deactivation phase transitions: the Deactivation duration is now constant but Veto Signalling duration gets extended each time a new proposal is submitted. + + > The extension of Veto Signalling better expresses one of the first design principles of the DG: stakers should have adequate time to react to any DAO proposal, disagree with the DAO and potentially exit the protocol, and the more stakers disagree with the DAO, the longer the allowed reaction time should be. + > + > Also, these changes prevent the following DoS attack: + > + > 1. A malicious actor front-runs submission of a proposal to the DG with a transaction that locks enough tokens in the signalling escrow to generate `FirstSealRageQuitSupport`, and immediately initiates the unlock. The governance is transitioned into the Veto Signalling state. + > 2. As soon as the `SignallingEscrowMinLockTime` passes, the actor unlocks their tokens from the escrow. This transitions the governance into the Veto Signalling Deactivation state that lasts `VetoSignallingMaxDuration`. + + > As one can see, the actor was able to delay the governance execution by `SignallingEscrowMinLockTime + VetoSignallingMaxDuration` while controlling tokens only enough to generate `FirstSealRageQuitSupport` and locking them for only the `SignallingEscrowMinLockTime` which makes for a rather cheap and efficient DoS attack. + +* Specified the transition conditions more rigorously. + ### 2024-04-02 * Added a minimum lock time of (w)stETH/wNFTs in the signalling escrow to prevent triggering state transitions using flash loans. From 21d3460a85a7e47a22bdad37c4fa1bf39d97e5e9 Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Thu, 11 Apr 2024 00:52:08 +0300 Subject: [PATCH 010/134] docs: fix spelling in the mechanism design --- docs/mechanism.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index 237f4f81..f67a668d 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -8,7 +8,7 @@ A proposal by [sam](https://twitter.com/_skozin), [pshe](https://twitter.com/Psh Currently, the Lido protocol governance consists of the Lido DAO that uses LDO voting to approve DAO proposals, along with an optimistic voting subsystem called Easy Tracks that is used for routine changes of low-impact parameters and falls back to LDO voting given any objection from LDO holders. -Additionally, there is a Gate Seal emergency committee that allows pausing certain protocol functionality (e.g. withdrawals) for a pre-configured amount of time sufficient for the DAO to vote on and execute a proposal. Gate Seal committee can only enact a pause once before losing its power (so it has to be re-elected by the DAO after that). +Additionally, there is a Gate Seal emergency committee that allows pausing certain protocol functionality (e.g. withdrawals) for a pre-configured amount of time sufficient for the DAO to vote on and execute a proposal. The Gate Seal committee can only enact a pause once before losing its power (so it has to be re-elected by the DAO after that). The Dual governance mechanism (DG) is an iteration on the protocol governance that gives stakers a say by allowing them to block DAO decisions and providing a negotiation device between stakers and the DAO. @@ -21,11 +21,11 @@ Another way of looking at dual governance is that it implements 1) a dynamic use 2. a fungibility layer distributing ETH between node operators and issuing stakers a fungible deposit receipt token (stETH). * **Protocol governance:** the mechanism allowing to change the Lido protocol parameters and upgrade non-ossified (mutable) parts of the protocol code. * **LDO:** the fungible governance token of the Lido DAO. -* **Lido DAO:** code deployed on the Ethereum blockchain implementing a DAO that receives a fee taken from the staking rewards to its treasury and allows LDO holders to collectively vote on spending the treasury, changing parameters of the Lido protocol and upgrading the non-ossified parts of the Lido protocol code. Referred to as just **DAO** thoughout this document. +* **Lido DAO:** code deployed on the Ethereum blockchain implementing a DAO that receives a fee taken from the staking rewards to its treasury and allows LDO holders to collectively vote on spending the treasury, changing parameters of the Lido protocol and upgrading the non-ossified parts of the Lido protocol code. Referred to as just **DAO** throughout this document. * **DAO proposal:** a specific change in the onchain state of the Lido protocol or the Lido DAO proposed by LDO holders. Proposals have to be approved via onchain voting between LDO holders to become executable. * **stETH:** the fungible deposit receipt token of the Lido protocol. Allows the holder to withdraw the deposited ETH plus all accrued rewards (minus the fees) and penalties. Rewards/penalties accrual is expressed by periodic rebases of the token balances. * **wstETH:** a non-rebasable, immutable, and trustless wrapper around stETH deployed as an integral part of the Lido protocol. At any moment in time, there is a fixed wstETH/stETH rate effective for wrapping and unwrapping. The rate changes on each stETH rebase. -* **Withdrawal NFT:** a non-fungible token minted by the Lido withdrawal queue contract as part of the (w)stETH withdrawal to ETH, parametrized by the underlying (w)stETH amount and the position in queue. Gives holder the right to claim the corresponding ETH amount after the withdrawal is complete. Doesn't entitle the holder to receive staking rewards. +* **Withdrawal NFT:** a non-fungible token minted by the Lido withdrawal queue contract as part of the (w)stETH withdrawal to ETH, parametrized by the underlying (w)stETH amount and the position in the queue. Gives the holder the right to claim the corresponding ETH amount after the withdrawal is complete. Doesn't entitle the holder to receive staking rewards. * **Stakers:** EOAs and smart contract wallets that hold stETH, wstETH tokens, and withdrawal NFTs or deposit them into various deFi protocols and ceFi platforms: DEXes, CEXes, lending and stablecoin protocols, custodies, etc. * **Node operators:** parties registered in the Lido protocol willing to run Ethereum validators using the delegated ETH in exchange for a fee taken from the staking rewards. Node operators generate validator keys and at any time remain their sole holders, having full and exclusive control over Ethereum validators. Node operators are required to set their validators' withdrawal credentials to point to the specific Lido protocol smart contract. @@ -42,7 +42,7 @@ Instead of making the in-scope changes directly, the DAO voting script should su After submission to the DG, a proposal can exist in one of the following states: * **Pending**: a proposal approved by the DAO was submitted to the DG subsystem, starting the dynamic execution timelock. -* **Cancelled**: the DAO votes for cancelling the pending proposal. This is the terminal state. +* **Cancelled**: the DAO votes for canceling the pending proposal. This is the terminal state. * **Executed**: the dynamic timelock of a pending proposal has elapsed and the proposal was executed. This is the terminal state. @@ -106,7 +106,7 @@ The DG mechanism can be described as a state machine defining the global governa Let's now define these states and transitions. -> Note: when a state has multiple outgoing transitions, their conditions are avaluated in the order they're listed in the text. If a condition evaluates to true, the further evaluation stops and the corresponsing transition is triggered. +> Note: when a state has multiple outgoing transitions, their conditions are evaluated in the order they're listed in the text. If a condition evaluates to true, the further evaluation stops and the corresponding transition is triggered. ### Normal state @@ -138,7 +138,7 @@ The Veto Signalling state's purpose is two-fold: In this state, the DAO can submit approved proposals to the DG but cannot execute them, including the proposals that were pending prior to the governance entering this state, effectively extending the timelock on all such proposals. -The only proposal that can be executed by the DAO is the special $CancelAllPendingProposals$ action that cancels all proposals that were pending at the moment of this execution, making them forever unexecutable. This mechanism provides a way for the DAO and stakers to negotiate and de-escalate if consensus is reached. +The only proposal that can be executed by the DAO is the special $CancelAllPendingProposals$ action that cancels all proposals that were pending at the moment of this execution, making them forever unexecutable. This mechanism provides a way for the DAO and stakers to negotiate and de-escalate if a consensus is reached. To define transitions from this state, let's first introduce several time values. @@ -146,7 +146,7 @@ The **time of activation** of the Veto Signalling state $t^S_{act}$ is the time The **time of re-activation** of the Veto Signalling state $t^S_{react}$ is the time the Deactivation sub-state was last exited without exiting the parent Veto Signalling state. -The **time of last proposal submission** $t_{prop}$ is the time the last proposal was submitted to the DG subsystem. +The **time of last proposal submission** $t_{prop}$ is the last time a proposal was submitted to the DG subsystem. The **dynamic timelock duration** $T_{lock}(R)$ depends on the current rage quit support $R = R(t)$ and can be calculated as follows: @@ -197,7 +197,7 @@ SecondSealRageQuitSupport = 0.1 #### Deactivation sub-state -The sub-state's purpose is to allow all stakers to observe the Veto Signalling being deactivated and react accordingly before non-cancelled proposals can be executed. In this sub-state, the DAO cannot submit proposals to the DG or execute pending proposals. +The sub-state's purpose is to allow all stakers to observe the Veto Signalling being deactivated and react accordingly before non-canceled proposals can be executed. In this sub-state, the DAO cannot submit proposals to the DG or execute pending proposals. **Transition to the parent state**. If, while the sub-state is active, the following condition becomes true: @@ -248,7 +248,7 @@ VetoCooldownDuration = 5 hours ### Rage Quit state -The Rage Quit state allows all stakers who elected to leave the protocol via rage quit to fully withdraw their ETH without being subject to any new or pending DAO decisions. Entering this state means that stakers and the DAO weren't able to resolve the dispute so the DAO is misaligned with a significant part of stakers. +The Rage Quit state allows all stakers who elected to leave the protocol via rage quit to fully withdraw their ETH without being subject to any new or pending DAO decisions. Entering this state means that the stakers and the DAO weren't able to resolve the dispute so the DAO is misaligned with a significant part of the stakers. Upon entry into the Rage Quit state, three things happen: @@ -256,13 +256,13 @@ Upon entry into the Rage Quit state, three things happen: 2. All stETH and wstETH held by the rage quit escrow are sent for withdrawal via the regular Lido Withdrawal Queue mechanism, generating a set of batch withdrawal NFTs held by the rage quit escrow. 3. A new instance of the veto signalling escrow smart contract is deployed. This way, at any point in time, there is only one veto signalling escrow but there may be multiple rage quit escrows from previous rage quits. -In this state, the DAO is allowed submit proposals to the DG but cannot execute any pending proposals. Stakers are not allowed to lock (w)stETH or withdrawal NFTs into the rage quit escrow so joining the ongoing rage quit is not possible. However, they can lock their tokens that are not part of the ongoing rage quit process to the newly-deployed veto signalling escrow to potentially trigger a new rage quit later. +In this state, the DAO is allowed to submit proposals to the DG but cannot execute any pending proposals. Stakers are not allowed to lock (w)stETH or withdrawal NFTs into the rage quit escrow so joining the ongoing rage quit is not possible. However, they can lock their tokens that are not part of the ongoing rage quit process to the newly-deployed veto signalling escrow to potentially trigger a new rage quit later. The state lasts until the withdrawal started in 2) is complete, i.e. until all batch withdrawal NFTs generated from (w)stETH that was locked in the escrow are fulfilled and claimed, plus `RageQuitExtensionDelay` days. -If, prior to the Rage Quit state being entered, a staker locked a withdrawal NFT into the signalling escrow, this NFT remains locked in the rage quit escrow. When such an NFT becomes fulfilled, the staker is allowed to burn this NFT and convert it to plain ETH, although still locked in the escrow. This allows stakers to derisk their ETH as early as possible by removing any dependence on the DAO-controlled code (remember that the withdrawal NFT contract is potentially upgradeable by the DAO but the rage quit escrow is immutable). +If a staker locks a withdrawal NFT into the signalling escrow before the Rage Quit state is entered, this NFT remains locked in the rage quit escrow. When such an NFT becomes fulfilled, the staker is allowed to burn this NFT and convert it to plain ETH, although still locked in the escrow. This allows stakers to derisk their ETH as early as possible by removing any dependence on the DAO-controlled code (remember that the withdrawal NFT contract is potentially upgradeable by the DAO but the rage quit escrow is immutable). -Since batch withdrawal NFTs are generated after the NFTs that were locked by stakers into the escrow directly, the withdrawal queue mechanism (external to the DG) guarantees that, by the time batch NFTs are fulfilled, all individually locked NFTs are fulfilled as well and can be claimed. Together with the extension delay, this guarantees that any staker having a withdrawal NFT locked in the rage quit escrow has at least `RageQuitExtensionDelay` days to convert it to escrow-locked ETH before the DAO execution is unblocked. +Since batch withdrawal NFTs are generated after the NFTs that were locked by stakers into the escrow directly, the withdrawal queue mechanism (external to the DG) guarantees that by the time batch NFTs are fulfilled, all individually locked NFTs are fulfilled as well and can be claimed. Together with the extension delay, this guarantees that any staker having a withdrawal NFT locked in the rage quit escrow has at least `RageQuitExtensionDelay` days to convert it to escrow-locked ETH before the DAO execution is unblocked. When the withdrawal is complete and the extension delay elapses, two things happen simultaneously: @@ -296,7 +296,7 @@ RageQuitEthClaimTimelockGrowthCoeffs = (0, TODO, TODO) ``` -### Gate Seal behavior and Tiebreaker Committee +### Gate Seal behaviour and Tiebreaker Committee The [Gate Seal](https://docs.lido.fi/contracts/gate-seal) is an existing circuit breaker mechanism designed to be activated in the event of a zero-day vulnerability in the protocol contracts being found or exploited and empowering a DAO-elected committee to pause certain protocol functionality, including withdrawals, for a predefined duration enough for the DAO to vote for and execute a remediation. When this happens, the committee immediately loses its power. If this never happens, the committee's power also expires after a pre-configured amount of time passes since its election. @@ -328,7 +328,7 @@ Specifically, the Tiebreaker committee can execute any pending proposal submitte * **Tiebreaker Condition A**: (governance state is Rage Quit) $\land$ (protocol withdrawals are paused by a Gate Seal). * **Tiebreaker Condition B**: (governance state is Rage Quit) $\land$ (last time governance exited Normal or Veto Cooldown state was more than `TiebreakerActivationTimeout` days ago). -The Tiebreaker committee should be composed of multiple sub-committees covering different interest groups within the Ethereum community (e.g. largest DAOs, EF, L2s, node operators, OGs) and should require approval from a supermajority of sub-committees in order to execute a pending proposal. The approval by each sub-committee should require the majority support within the sub-committee. No sub-committee should contain more than $1/4$ of the members that are also members of the withdrawals Gate Seal committee. +The Tiebreaker committee should be composed of multiple sub-committees covering different interest groups within the Ethereum community (e.g. largest DAOs, EF, L2s, node operators, OGs) and should require approval from a supermajority of sub-committees to execute a pending proposal. The approval by each sub-committee should require the majority support within the sub-committee. No sub-committee should contain more than $1/4$ of the members that are also members of the withdrawals Gate Seal committee. The composition of the Tiebreaker committee should be set by a DAO vote (subject to DG) and reviewed at least once a year. @@ -347,7 +347,7 @@ Dual governance should cover any DAO proposal that could potentially affect the * Changing the global protocol parameters and safety limits. * Changing the parameters of: * The withdrawal queue. - * The staking router, including addition and removal of staking modules. + * The staking router, including the addition and removal of staking modules. * Staking modules, including addition and removal of node operators within the curated staking module. * Adding, removing, and replacing the oracle committee members. * Adding, removing, and replacing the deposit security committee members. @@ -364,9 +364,9 @@ Dual governance should not cover: ### 2024-04-10 -* Redesigned the Veto Signalling exit conditions and the Deactivation phase transitions: the Deactivation duration is now constant but Veto Signalling duration gets extended each time a new proposal is submitted. +* Redesigned the Veto Signalling exit conditions and the Deactivation phase transitions: the Deactivation duration is now constant but the Veto Signalling duration gets extended each time a new proposal is submitted. - > The extension of Veto Signalling better expresses one of the first design principles of the DG: stakers should have adequate time to react to any DAO proposal, disagree with the DAO and potentially exit the protocol, and the more stakers disagree with the DAO, the longer the allowed reaction time should be. + > The extension of Veto Signalling better expresses one of the first design principles of the DG: stakers should have adequate time to react to any DAO proposal, disagree with the DAO, and potentially exit the protocol, and the more stakers disagree with the DAO, the longer the allowed reaction time should be. > > Also, these changes prevent the following DoS attack: > From c42c874ff9624ba254a080a87a8ee38df5c144b7 Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Thu, 11 Apr 2024 01:25:23 +0300 Subject: [PATCH 011/134] docs: update the spec according to the latest mechanism design changes --- docs/specification.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/specification.md b/docs/specification.md index e250f4b7..eba65117 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -1,4 +1,4 @@ -**Working draft**, the latest version is published at https://hackmd.io/@skozin/SkjuZAuip. +**Working draft** --- @@ -8,9 +8,9 @@ Dual Governance (DG) is a governance subsystem that sits between the Lido DAO, r This document provides the system description on the code architecture level. A detailed description on the mechanism level can be found in the [Dual Governance mechanism design overview][mech design] document which should be considered an integral part of this specification. -[mech design]: https://hackmd.io/@skozin/rkD1eUzja +[mech design]: mechanism.md -[mech design - tiebreaker]: https://hackmd.io/@skozin/rkD1eUzja#Tiebreaker-Committee +[mech design - tiebreaker]: mechanism.md#Tiebreaker-Committee ## System overview @@ -64,7 +64,7 @@ By the time the dynamic timelock described above elapses, one of the following o The proposal execution flow comes after the dynamic timelock elapses and the proposal is scheduled for execution. The system can function in two deployment modes which affect the flow. -![image](https://github.com/lidofinance/dual-governance/assets/1699593/6252bc65-269f-447d-b215-6a59188b8a94) +![image](https://github.com/lidofinance/dual-governance/assets/1699593/7a0f0330-6ef5-4985-8fd4-ac8f1f95d229) #### Regular deployment mode @@ -116,7 +116,7 @@ These transitions are enabled by three processes (see the [mechanism design docu 2. Protocol withdrawals processing (in the `RageQuit` state); 3. Time passing. -![image](https://github.com/lidofinance/dual-governance/assets/1699593/570141e8-4d66-45a2-9d18-3435806f1831) +![image](https://github.com/lidofinance/dual-governance/assets/1699593/118c26ef-5187-469f-a5ab-aea945fdb6aa) ## Rage quit @@ -131,7 +131,7 @@ At any time, only one instance of the rage quit process can be active. From the stakers' point of view, opposition to the DAO and the rage quit process can be described by the following diagram: -![image](https://github.com/lidofinance/dual-governance/assets/1699593/419f5621-7f83-4360-8f81-d3ced27b9fcc) +![image](https://github.com/lidofinance/dual-governance/assets/1699593/f0f3647d-e251-458c-8556-2c481c2df35b) ## Tiebreaker committee From 6126bc8dcd873928cc3e65e984751d904fc7795d Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Thu, 11 Apr 2024 13:42:20 +0300 Subject: [PATCH 012/134] docs: fix a changelog item in mechanism.md --- docs/mechanism.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index f67a668d..868ea9c9 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -373,7 +373,7 @@ Dual governance should not cover: > 1. A malicious actor front-runs submission of a proposal to the DG with a transaction that locks enough tokens in the signalling escrow to generate `FirstSealRageQuitSupport`, and immediately initiates the unlock. The governance is transitioned into the Veto Signalling state. > 2. As soon as the `SignallingEscrowMinLockTime` passes, the actor unlocks their tokens from the escrow. This transitions the governance into the Veto Signalling Deactivation state that lasts `VetoSignallingMaxDuration`. - > As one can see, the actor was able to delay the governance execution by `SignallingEscrowMinLockTime + VetoSignallingMaxDuration` while controlling tokens only enough to generate `FirstSealRageQuitSupport` and locking them for only the `SignallingEscrowMinLockTime` which makes for a rather cheap and efficient DoS attack. + > As one can see, the actor was able to delay the governance execution by `VetoSignallingMaxDuration` while controlling tokens only enough to generate `FirstSealRageQuitSupport` and locking them for only the `SignallingEscrowMinLockTime` which makes for a rather cheap and efficient DoS attack. * Specified the transition conditions more rigorously. From 1de628ab3d0e0ede51b5dc92435bfa5b828d8686 Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Thu, 11 Apr 2024 15:02:26 +0300 Subject: [PATCH 013/134] docs: fix inline math rendering in mechanism.md --- docs/mechanism.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index 868ea9c9..013a9f59 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -213,7 +213,7 @@ then the Deactivation sub-state is exited so only the parent Veto Signalling sta t - t^{SD}_{act} > \, T^{SD}_{max} ``` -where $t^{SD}_{act}$ is the time the Deactivation sub-state was entered and $T^{SD}_{max}$ is `VetoSignallingDeactivationMaxDuration`, then the Deactivation sub-state is exited along with its parent Veto Signalling state and the Veto Cooldown state is entered. +where $`t^{SD}_{act}`$ is the time the Deactivation sub-state was entered and $`T^{SD}_{max}`$ is `VetoSignallingDeactivationMaxDuration`, then the Deactivation sub-state is exited along with its parent Veto Signalling state and the Veto Cooldown state is entered. ```env # Proposed values, to be modeled and refined From c4b29364990b81a780cb65e7d601910ad7007cb1 Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Thu, 11 Apr 2024 15:17:34 +0300 Subject: [PATCH 014/134] docs: add inline navigation --- docs/mechanism.md | 17 +++++++++++++++++ docs/specification.md | 20 ++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/docs/mechanism.md b/docs/mechanism.md index 013a9f59..f635b3de 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -14,6 +14,23 @@ The Dual governance mechanism (DG) is an iteration on the protocol governance th Another way of looking at dual governance is that it implements 1) a dynamic user-extensible timelock on DAO decisions and 2) a rage quit mechanism for stakers taking into account the specifics of how Ethereum withdrawals work. + +## Navigation + +* [Definitions](#definitions) +* [Mechanism description](#mechanism-description) + + [Proposal lifecycle](#proposal-lifecycle) + + [Signalling Escrow](#signalling-escrow) + + [Global governance state](#global-governance-state) + + [Normal state](#normal-state) + + [Veto Signalling state](#veto-signalling-state) + + [Veto Cooldown state](#veto-cooldown-state) + + [Rage Quit state](#rage-quit-state) + + [Gate Seal behaviour and Tiebreaker Committee](#gate-seal-behaviour-and-tiebreaker-committee) +* [Dual governance scope](#dual-governance-scope) +* [Changelog](#changelog) + + ## Definitions * **Lido protocol:** code deployed on the Ethereum blockchain implementing: diff --git a/docs/specification.md b/docs/specification.md index eba65117..3908bbd5 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -13,6 +13,26 @@ This document provides the system description on the code architecture level. A [mech design - tiebreaker]: mechanism.md#Tiebreaker-Committee +## Navigation + +* [System overview](#system-overview) +* [Proposal flow](#proposal-flow) + + [Dynamic timelock](#dynamic-timelock) + + [Proposal execution and deployment modes](#proposal-execution-and-deployment-modes) +* [Governance state](#governance-state) +* [Rage quit](#rage-quit) +* [Tiebreaker committee](#tiebreaker-committee) +* [Administrative actions](#administrative-actions) +* [Common types](#common-types) +* [Contract: DualGovernance.sol](#contract-dualgovernancesol) +* [Contract: Executor.sol](#contract-executorsol) +* [Contract: Escrow.sol](#contract-escrowsol) +* [Contract: EmergencyProtectedTimelock.sol](#contract-emergencyprotectedtimelocksol) +* [Contract: GateSealBreaker.sol](#contract-gatesealbreakersol) +* [Contract: Configuration.sol](#contract-configurationsol) +* [Upgrade flow description](#upgrade-flow-description) + + ## System overview ![image](https://github.com/lidofinance/dual-governance/assets/1699593/8b1f119c-2a61-4d66-969c-acab2b66c16e) From 0c70337c8e61ddf8c26c14268f3a45eb78212b2f Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 12 Apr 2024 13:09:22 +0400 Subject: [PATCH 015/134] The draft of possible VetoSignalling change --- contracts/libraries/DualGovernanceState.sol | 116 +++++++++++------- .../last-moment-malicious-proposal.t.sol | 24 ++-- 2 files changed, 80 insertions(+), 60 deletions(-) diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol index f363f500..aa84c6b6 100644 --- a/contracts/libraries/DualGovernanceState.sol +++ b/contracts/libraries/DualGovernanceState.sol @@ -24,13 +24,17 @@ library DualGovernanceState { struct Store { State state; uint40 enteredAt; - uint40 signallingActivatedAt; + // + uint40 vetoSignallingFirstActivation; + uint40 vetoSignallingLastActivation; + // uint40 lastAdoptableStateExitedAt; IEscrow signallingEscrow; IEscrow rageQuitEscrow; - uint40 lastProposalCreatedAt; uint8 rageQuitRound; } + // uint40 vetoAccumulationDuration; + // uint40 vetoDeactivationDuration; error NotTie(); error AlreadyInitialized(); @@ -74,8 +78,41 @@ library DualGovernanceState { } } + // TODO: Consider this code as possible option. Delete if not needed + // function onNewProposal(Store storage self, IConfiguration config) internal { + // uint256 accumulationMaxDuration = config.SIGNALLING_MAX_DURATION(); + // uint256 deactivationMinDuration = config.SIGNALLING_DEACTIVATION_DURATION(); + + // if (self.state == State.Normal || self.state == State.RageQuit) { + // self.vetoAccumulationDuration = TimeUtils.timestamp(accumulationMaxDuration); + // self.vetoDeactivationDuration = TimeUtils.timestamp(deactivationMinDuration); + // } else if (self.state == State.VetoAccumulation) { + // // when the proposal submitted during the veto accumulation phase + // uint256 enteredAt = self.enteredAt; + // uint256 vetoAccumulationDurationPassed = block.timestamp - enteredAt; + // // now we have to decrease veto accumulation duration on passed time and increase the + // // deactivation duration + // uint256 vetoAccumulationDurationNew = self.vetoAccumulationDuration > vetoAccumulationDurationPassed + // ? self.vetoAccumulationDuration - vetoAccumulationDurationPassed + // : 0; + // uint256 vetoDeactivationDurationNew = + // deactivationMinDuration + accumulationMaxDuration - vetoAccumulationDurationNew; + + // self.vetoAccumulationDuration = TimeUtils.timestamp(vetoAccumulationDurationNew); + // self.vetoDeactivationDuration = TimeUtils.timestamp(vetoDeactivationDurationNew); + // // when the durations were updated, assuming that vet signalling was reactivated + // // at this point. + // self.enteredAt = TimeUtils.timestamp(); + // } else { + // // in any other cases, proposal can't be submitted + // assert(false); + // } + // } + function setLastProposalCreationTimestamp(Store storage self) internal { - self.lastProposalCreatedAt = TimeUtils.timestamp(); + if (self.state == State.VetoSignalling) { + self.vetoSignallingLastActivation = TimeUtils.timestamp(); + } } function checkProposalsCreationAllowed(Store storage self) internal view { @@ -132,7 +169,7 @@ library DualGovernanceState { isActive = self.state == State.VetoSignalling; duration = isActive ? getVetoSignallingDuration(self, config) : 0; enteredAt = isActive ? self.enteredAt : 0; - activatedAt = isActive ? self.signallingActivatedAt : 0; + activatedAt = isActive ? self.vetoSignallingLastActivation : 0; } function getVetoSignallingDuration(Store storage self, IConfiguration config) internal view returns (uint256) { @@ -150,19 +187,10 @@ library DualGovernanceState { IConfiguration config ) internal view returns (bool isActive, uint256 duration, uint256 enteredAt) { isActive = self.state == State.VetoSignallingDeactivation; - duration = getVetoSignallingDeactivationDuration(self, config); + duration = config.SIGNALLING_DEACTIVATION_DURATION(); enteredAt = isActive ? self.enteredAt : 0; } - function getVetoSignallingDeactivationDuration( - Store storage self, - IConfiguration config - ) internal view returns (uint256) { - return self.lastProposalCreatedAt >= self.signallingActivatedAt - ? config.SIGNALLING_MIN_PROPOSAL_REVIEW_DURATION() - : config.SIGNALLING_DEACTIVATION_DURATION(); - } - // --- // Store Transitions // --- @@ -173,40 +201,47 @@ library DualGovernanceState { } function _fromVetoSignallingState(Store storage self, IConfiguration config) private view returns (State) { - uint256 totalSupport = self.signallingEscrow.getRageQuitSupport(); + uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - if (totalSupport < config.FIRST_SEAL_THRESHOLD()) { + if (rageQuitSupport < config.FIRST_SEAL_THRESHOLD()) { return State.VetoSignallingDeactivation; } - uint256 currentDuration = block.timestamp - self.signallingActivatedAt; - uint256 targetDuration = _calcVetoSignallingTargetDuration(config, totalSupport); + uint256 vetoSignallingTotalDuration = block.timestamp - self.vetoSignallingFirstActivation; - if (currentDuration < targetDuration) { - return State.VetoSignalling; + if (vetoSignallingTotalDuration >= config.SIGNALLING_MAX_DURATION() && _isSecondThresholdReached(self, config)) + { + return State.RageQuit; + } + + uint256 vetoSignallingCurrentDuration = block.timestamp - self.vetoSignallingLastActivation; + uint256 targetDuration = _calcVetoSignallingTargetDuration(config, rageQuitSupport); + + // spent in the VetoAccumulation state longer than needed + if (vetoSignallingCurrentDuration >= targetDuration) { + return State.VetoSignallingDeactivation; } - return _isSecondThresholdReached(self, config) ? State.RageQuit : State.VetoSignallingDeactivation; + return State.VetoSignalling; } function _fromVetoSignallingDeactivationState( Store storage self, IConfiguration config ) private view returns (State) { - if (_isVetoSignallingDeactivationPhasePassed(self, config)) return State.VetoCooldown; - uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - uint256 currentSignallingDuration = block.timestamp - self.signallingActivatedAt; - uint256 targetSignallingDuration = _calcVetoSignallingTargetDuration(config, rageQuitSupport); - - if (currentSignallingDuration >= targetSignallingDuration) { - if (rageQuitSupport >= config.SECOND_SEAL_THRESHOLD()) { - return State.RageQuit; - } - } else if (rageQuitSupport >= config.FIRST_SEAL_THRESHOLD()) { + uint256 vetoSignallingCurrentDuration = block.timestamp - self.vetoSignallingLastActivation; + uint256 targetDuration = _calcVetoSignallingTargetDuration(config, rageQuitSupport); + + if (targetDuration > vetoSignallingCurrentDuration) { return State.VetoSignalling; } - return State.VetoSignallingDeactivation; + + if (block.timestamp - self.enteredAt <= config.SIGNALLING_DEACTIVATION_DURATION()) { + return State.VetoSignallingDeactivation; + } + + return _isSecondThresholdReached(self, config) ? State.RageQuit : State.VetoCooldown; } function _fromVetoCooldownState(Store storage self, IConfiguration config) private view returns (State) { @@ -249,7 +284,8 @@ library DualGovernanceState { self.rageQuitRound = 0; } if (newState == State.VetoSignalling && oldState != State.VetoSignallingDeactivation) { - self.signallingActivatedAt = currentTime; + self.vetoSignallingFirstActivation = currentTime; + self.vetoSignallingLastActivation = currentTime; } if (newState == State.RageQuit) { IEscrow signallingEscrow = self.signallingEscrow; @@ -295,20 +331,6 @@ library DualGovernanceState { + (totalSupport - firstSealThreshold) * (maxDuration - minDuration) / (secondSealThreshold - firstSealThreshold); } - function _isVetoSignallingDeactivationPhasePassed( - Store storage self, - IConfiguration config - ) private view returns (bool) { - uint256 currentDeactivationDuration = block.timestamp - self.enteredAt; - - if (currentDeactivationDuration < config.SIGNALLING_DEACTIVATION_DURATION()) return false; - - uint256 lastProposalCreatedAt = self.lastProposalCreatedAt; - return lastProposalCreatedAt >= self.signallingActivatedAt - ? block.timestamp - lastProposalCreatedAt >= config.SIGNALLING_MIN_PROPOSAL_REVIEW_DURATION() - : true; - } - function _deployNewSignallingEscrow(Store storage self, address escrowMasterCopy) private { IEscrow clone = IEscrow(Clones.clone(escrowMasterCopy)); clone.initialize(address(this)); diff --git a/test/scenario/last-moment-malicious-proposal.t.sol b/test/scenario/last-moment-malicious-proposal.t.sol index 630aae83..9c091d34 100644 --- a/test/scenario/last-moment-malicious-proposal.t.sol +++ b/test/scenario/last-moment-malicious-proposal.t.sol @@ -78,33 +78,31 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { } // --- - // ACT 5. STETH HOLDERS TRY ACQUIRE QUORUM, DURING THE DEACTIVATION PERIOD BUT UNSUCCESSFULLY + // ACT 5. STETH HOLDERS MAY ACQUIRE QUORUM BECAUSE THE VETO SIGNALLING PERIOD RESTARTED // --- address stEthWhale = makeAddr("STETH_WHALE"); { + _wait(_config.SIGNALLING_DEACTIVATION_DURATION() / 2); _lockStETH(stEthWhale, percents("10.0")); _logVetoSignallingDeactivationState(); - _assertVetoSignalingDeactivationState(); + _assertVetoSignalingState(); + _logVetoSignallingState(); } // --- - // ACT 6. BUT THE DEACTIVATION PHASE IS PROLONGED BECAUSE THE MALICIOUS VOTE - // WAS SUBMITTED ON VETO SIGNALLING PHASE + // ACT 6. STETH HOLDER MAY EXIT TO RAGE QUIT WHEN THE SECOND SEAL THRESHOLD REACHED // --- { - vm.warp(block.timestamp + _config.SIGNALLING_DEACTIVATION_DURATION() + 1); - - // the veto signalling deactivation duration is passed, but proposal will be executed - // only when the _config.SIGNALLING_MIN_PROPOSAL_REVIEW_DURATION() from the last proposal - // submission is passed. + _wait(_config.SIGNALLING_DEACTIVATION_DURATION() / 2 + 1); _activateNextState(); - _assertVetoSignalingDeactivationState(); - - vm.warp(block.timestamp + _config.SIGNALLING_MIN_PROPOSAL_REVIEW_DURATION() / 2); + _assertVetoSignalingState(); // stEth holders reach the rage quit threshold - _lockStETH(maliciousActor, percents("10.0")); + _lockStETH(stEthWhale, percents("10.0")); + + _wait(_config.SIGNALLING_DEACTIVATION_DURATION()); + _activateNextState(); // the dual governance immediately transfers to the Rage Quit state _assertRageQuitState(); From f3407f759812af12d881eca74db35f4fd84821e1 Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Fri, 12 Apr 2024 17:38:21 +0300 Subject: [PATCH 016/134] docs: add clarification to the state machine spec --- docs/mechanism.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/mechanism.md b/docs/mechanism.md index f635b3de..d9323517 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -124,6 +124,8 @@ The DG mechanism can be described as a state machine defining the global governa Let's now define these states and transitions. > Note: when a state has multiple outgoing transitions, their conditions are evaluated in the order they're listed in the text. If a condition evaluates to true, the further evaluation stops and the corresponding transition is triggered. +> +> Multiple state transitions can be sequentially triggered at the same timestamp, provided that each subsequent transition's condition is re-evaluated and holds true after the preceding transition is triggered. ### Normal state From 98e8ea076ecc129e0bffd2dfa8b9c6fb0f53661d Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Fri, 12 Apr 2024 18:50:51 +0300 Subject: [PATCH 017/134] docs: few clarifications in mechanism.md --- docs/mechanism.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index d9323517..dbee30eb 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -130,7 +130,7 @@ Let's now define these states and transitions. ### Normal state -The Normal state is the state the mechanism is designed to spend the most time within. The DAO can submit the approved proposals to the DG and execute them after the standard timelock of `ProposalExecutionMinTimelock` days passes since the proposal's submission. +The Normal state is the state the mechanism is designed to spend the most time within. The DAO can submit the approved proposals to the DG and execute them provided that the proposal being executed is not cancelled and was submitted more than `ProposalExecutionMinTimelock` days ago. **Transition to Veto Signalling**. If, while the state is active, the following condition becomes true: @@ -241,7 +241,7 @@ VetoSignallingDeactivationMaxDuration = 3 days ### Veto Cooldown state -In the Veto Cooldown state, the DAO cannot submit proposals to the DG but can execute pending non-cancelled proposals. It exists to guarantee that no staker possessing enough stETH to generate `FirstSealRageQuitSupport` can lock the governance indefinitely without rage quitting the protocol. +In the Veto Cooldown state, the DAO cannot submit proposals to the DG but can execute pending non-cancelled proposals, provided that the proposal being executed was submitted more than `ProposalExecutionMinTimelock` days ago. This state exists to guarantee that no staker possessing enough stETH to generate `FirstSealRageQuitSupport` can lock the governance indefinitely without rage quitting the protocol. **Transition to Veto Signalling**. If, while the state is active, the following condition becomes true: From 6144a3008bd3467d089b905f612af980ac4233bb Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Fri, 12 Apr 2024 18:54:46 +0300 Subject: [PATCH 018/134] spec: remove lower bound on Normal state duration not needed anymore: flash loan attacks are prevented by the signalling escrow min lock time and governance liveliness is ensured by the Veto Cooldown --- docs/mechanism.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index dbee30eb..683d590c 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -132,19 +132,18 @@ Let's now define these states and transitions. The Normal state is the state the mechanism is designed to spend the most time within. The DAO can submit the approved proposals to the DG and execute them provided that the proposal being executed is not cancelled and was submitted more than `ProposalExecutionMinTimelock` days ago. -**Transition to Veto Signalling**. If, while the state is active, the following condition becomes true: +**Transition to Veto Signalling**. If, while the state is active, the following expression becomes true: ```math -\big( R(t) > R_1 \big) \, \land \, \big( t - t^N_{act} > T^N_{min} \big) +R > R_1 ``` -where $R_1$ is `FirstSealRageQuitSupport`, $t^N_{act}$ is the time the Normal state was entered, and $T^N_{min}$ is `NormalStateMinDuration`, the Normal state is exited and the Veto Signalling state is entered. +where $R_1$ is `FirstSealRageQuitSupport`, the Normal state is exited and the Veto Signalling state is entered. ```env # Proposed values, to be modeled and refined ProposalExecutionMinTimelock = 3 days FirstSealRageQuitSupport = 0.01 -NormalStateMinDuration = 5 hours ``` @@ -381,6 +380,10 @@ Dual governance should not cover: ## Changelog +### 2024-04-12 + +* Removed the lower boundary on the Normal state duration since it's not needed anymore: flash loan attacks are prevented by the signalling escrow min lock time and governance liveliness is ensured by the Veto Cooldown. + ### 2024-04-10 * Redesigned the Veto Signalling exit conditions and the Deactivation phase transitions: the Deactivation duration is now constant but the Veto Signalling duration gets extended each time a new proposal is submitted. From c11227d885e99c6c608ce5adeb10a91035d7b968 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 15 Apr 2024 13:16:56 +0400 Subject: [PATCH 019/134] Rename Configuration constants --- contracts/Configuration.sol | 30 +++++++++---------- contracts/Escrow.sol | 6 ++-- contracts/interfaces/IConfiguration.sol | 14 ++++----- contracts/libraries/DualGovernanceState.sol | 18 +++++------ test/scenario/escrow.t.sol | 8 ++--- test/scenario/gate-seal-breaker.t.sol | 6 ++-- test/scenario/gov-state-transitions.t.sol | 22 +++++++------- .../last-moment-malicious-proposal.t.sol | 6 ++-- 8 files changed, 55 insertions(+), 55 deletions(-) diff --git a/contracts/Configuration.sol b/contracts/Configuration.sol index 58b134d5..69754366 100644 --- a/contracts/Configuration.sol +++ b/contracts/Configuration.sol @@ -16,15 +16,15 @@ contract Configuration is IConfiguration { uint256 public immutable RAGE_QUIT_ETH_WITHDRAWAL_TIMELOCK = 30 days; - uint256 public immutable SIGNALLING_COOLDOWN_DURATION = 4 days; - uint256 public immutable SIGNALLING_DEACTIVATION_DURATION = 5 days; + uint256 public immutable VETO_COOLDOWN_DURATION = 4 days; + uint256 public immutable VETO_SIGNALLING_DEACTIVATION_DURATION = 5 days; uint256 public immutable SIGNALLING_MIN_PROPOSAL_REVIEW_DURATION = 30 days; - uint256 public immutable SIGNALLING_MIN_DURATION = 3 days; - uint256 public immutable SIGNALLING_MAX_DURATION = 30 days; + uint256 public immutable DYNAMIC_TIMELOCK_MIN_DURATION = 3 days; + uint256 public immutable DYNAMIC_TIMELOCK_MAX_DURATION = 30 days; - uint256 public immutable FIRST_SEAL_THRESHOLD = 3 * PERCENT; - uint256 public immutable SECOND_SEAL_THRESHOLD = 15 * PERCENT; + uint256 public immutable FIRST_SEAL_RAGE_QUIT_SUPPORT = 3 * PERCENT; + uint256 public immutable SECOND_SEAL_RAGE_QUIT_SUPPORT = 15 * PERCENT; uint256 public immutable TIE_BREAK_ACTIVATION_TIMEOUT = 365 days; @@ -32,7 +32,7 @@ contract Configuration is IConfiguration { uint256 public immutable RAGE_QUIT_EXTENSION_DELAY = 7 days; uint256 public immutable RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK = 60 days; uint256 public immutable MIN_STATE_DURATION = 5 hours; - uint256 public immutable ESCROW_ASSETS_UNLOCK_DELAY = 5 hours; + uint256 public immutable SIGNALLING_ESCROW_MIN_LOCK_TIME = 5 hours; // Sealables Array Representation uint256 private immutable MAX_SELABLES_COUNT = 5; @@ -74,15 +74,15 @@ contract Configuration is IConfiguration { external view returns ( - uint256 firstSealThreshold, - uint256 secondSealThreshold, - uint256 signallingMinDuration, - uint256 signallingMaxDuration + uint256 firstSealRageQuitSupport, + uint256 secondSealRageQuitSupport, + uint256 dynamicTimelockMinDuration, + uint256 dynamicTimelockMaxDuration ) { - firstSealThreshold = FIRST_SEAL_THRESHOLD; - secondSealThreshold = SECOND_SEAL_THRESHOLD; - signallingMinDuration = SIGNALLING_MIN_DURATION; - signallingMaxDuration = SIGNALLING_MAX_DURATION; + firstSealRageQuitSupport = FIRST_SEAL_RAGE_QUIT_SUPPORT; + secondSealRageQuitSupport = SECOND_SEAL_RAGE_QUIT_SUPPORT; + dynamicTimelockMinDuration = DYNAMIC_TIMELOCK_MIN_DURATION; + dynamicTimelockMaxDuration = DYNAMIC_TIMELOCK_MAX_DURATION; } } diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 8e8bd608..94c14d84 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -91,7 +91,7 @@ contract Escrow is IEscrow { } function unlockStETH() external { - uint256 sharesUnlocked = _accounting.accountStETHUnlock(CONFIG.ESCROW_ASSETS_UNLOCK_DELAY(), msg.sender); + uint256 sharesUnlocked = _accounting.accountStETHUnlock(CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME(), msg.sender); ST_ETH.transferShares(msg.sender, sharesUnlocked); _activateNextGovernanceState(); } @@ -107,7 +107,7 @@ contract Escrow is IEscrow { } function unlockWstETH() external returns (uint256 wstETHUnlocked) { - wstETHUnlocked = _accounting.accountWstETHUnlock(CONFIG.ESCROW_ASSETS_UNLOCK_DELAY(), msg.sender); + wstETHUnlocked = _accounting.accountWstETHUnlock(CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME(), msg.sender); WST_ETH.transfer(msg.sender, wstETHUnlocked); _activateNextGovernanceState(); } @@ -126,7 +126,7 @@ contract Escrow is IEscrow { } function unlockUnstETH(uint256[] memory unstETHIds) external { - _accounting.accountUnstETHUnlock(CONFIG.ESCROW_ASSETS_UNLOCK_DELAY(), msg.sender, unstETHIds); + _accounting.accountUnstETHUnlock(CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME(), msg.sender, unstETHIds); uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { WITHDRAWAL_QUEUE.transferFrom(address(this), msg.sender, unstETHIds[i]); diff --git a/contracts/interfaces/IConfiguration.sol b/contracts/interfaces/IConfiguration.sol index 7c1f87b8..d3546d40 100644 --- a/contracts/interfaces/IConfiguration.sol +++ b/contracts/interfaces/IConfiguration.sol @@ -14,15 +14,15 @@ interface ITimelockConfiguration { interface IDualGovernanceConfiguration { function RAGE_QUIT_ETH_WITHDRAWAL_TIMELOCK() external view returns (uint256); - function SIGNALLING_COOLDOWN_DURATION() external view returns (uint256); - function SIGNALLING_DEACTIVATION_DURATION() external view returns (uint256); + function VETO_COOLDOWN_DURATION() external view returns (uint256); + function VETO_SIGNALLING_DEACTIVATION_DURATION() external view returns (uint256); function SIGNALLING_MIN_PROPOSAL_REVIEW_DURATION() external view returns (uint256); - function SIGNALLING_MIN_DURATION() external view returns (uint256); - function SIGNALLING_MAX_DURATION() external view returns (uint256); + function DYNAMIC_TIMELOCK_MIN_DURATION() external view returns (uint256); + function DYNAMIC_TIMELOCK_MAX_DURATION() external view returns (uint256); - function FIRST_SEAL_THRESHOLD() external view returns (uint256); - function SECOND_SEAL_THRESHOLD() external view returns (uint256); + function FIRST_SEAL_RAGE_QUIT_SUPPORT() external view returns (uint256); + function SECOND_SEAL_RAGE_QUIT_SUPPORT() external view returns (uint256); function TIE_BREAK_ACTIVATION_TIMEOUT() external view returns (uint256); @@ -31,7 +31,7 @@ interface IDualGovernanceConfiguration { function RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK() external view returns (uint256); function MIN_STATE_DURATION() external view returns (uint256); - function ESCROW_ASSETS_UNLOCK_DELAY() external view returns (uint256); + function SIGNALLING_ESCROW_MIN_LOCK_TIME() external view returns (uint256); function sealableWithdrawalBlockers() external view returns (address[] memory); diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol index f363f500..25e07917 100644 --- a/contracts/libraries/DualGovernanceState.sol +++ b/contracts/libraries/DualGovernanceState.sol @@ -160,7 +160,7 @@ library DualGovernanceState { ) internal view returns (uint256) { return self.lastProposalCreatedAt >= self.signallingActivatedAt ? config.SIGNALLING_MIN_PROPOSAL_REVIEW_DURATION() - : config.SIGNALLING_DEACTIVATION_DURATION(); + : config.VETO_SIGNALLING_DEACTIVATION_DURATION(); } // --- @@ -169,13 +169,13 @@ library DualGovernanceState { function _fromNormalState(Store storage self, IConfiguration config) private view returns (State) { uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - return rageQuitSupport >= config.FIRST_SEAL_THRESHOLD() ? State.VetoSignalling : State.Normal; + return rageQuitSupport >= config.FIRST_SEAL_RAGE_QUIT_SUPPORT() ? State.VetoSignalling : State.Normal; } function _fromVetoSignallingState(Store storage self, IConfiguration config) private view returns (State) { uint256 totalSupport = self.signallingEscrow.getRageQuitSupport(); - if (totalSupport < config.FIRST_SEAL_THRESHOLD()) { + if (totalSupport < config.FIRST_SEAL_RAGE_QUIT_SUPPORT()) { return State.VetoSignallingDeactivation; } @@ -200,10 +200,10 @@ library DualGovernanceState { uint256 targetSignallingDuration = _calcVetoSignallingTargetDuration(config, rageQuitSupport); if (currentSignallingDuration >= targetSignallingDuration) { - if (rageQuitSupport >= config.SECOND_SEAL_THRESHOLD()) { + if (rageQuitSupport >= config.SECOND_SEAL_RAGE_QUIT_SUPPORT()) { return State.RageQuit; } - } else if (rageQuitSupport >= config.FIRST_SEAL_THRESHOLD()) { + } else if (rageQuitSupport >= config.FIRST_SEAL_RAGE_QUIT_SUPPORT()) { return State.VetoSignalling; } return State.VetoSignallingDeactivation; @@ -211,7 +211,7 @@ library DualGovernanceState { function _fromVetoCooldownState(Store storage self, IConfiguration config) private view returns (State) { uint256 duration_ = block.timestamp - self.enteredAt; - if (duration_ < config.SIGNALLING_COOLDOWN_DURATION()) { + if (duration_ < config.VETO_COOLDOWN_DURATION()) { return State.VetoCooldown; } return _isFirstThresholdReached(self, config) ? State.VetoSignalling : State.Normal; @@ -268,12 +268,12 @@ library DualGovernanceState { function _isFirstThresholdReached(Store storage self, IConfiguration config) private view returns (bool) { uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - return rageQuitSupport >= config.FIRST_SEAL_THRESHOLD(); + return rageQuitSupport >= config.FIRST_SEAL_RAGE_QUIT_SUPPORT(); } function _isSecondThresholdReached(Store storage self, IConfiguration config) private view returns (bool) { uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - return rageQuitSupport >= config.SECOND_SEAL_THRESHOLD(); + return rageQuitSupport >= config.SECOND_SEAL_RAGE_QUIT_SUPPORT(); } function _calcVetoSignallingTargetDuration( @@ -301,7 +301,7 @@ library DualGovernanceState { ) private view returns (bool) { uint256 currentDeactivationDuration = block.timestamp - self.enteredAt; - if (currentDeactivationDuration < config.SIGNALLING_DEACTIVATION_DURATION()) return false; + if (currentDeactivationDuration < config.VETO_SIGNALLING_DEACTIVATION_DURATION()) return false; uint256 lastProposalCreatedAt = self.lastProposalCreatedAt; return lastProposalCreatedAt >= self.signallingActivatedAt diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 994465a4..cf66dbed 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -99,7 +99,7 @@ contract EscrowHappyPath is TestHelpers { _lockStETH(_VETOER_2, 3 * 10 ** 18); _lockWstETH(_VETOER_2, 5 * 10 ** 18); - _wait(_config.ESCROW_ASSETS_UNLOCK_DELAY() + 1); + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); _unlockStETH(_VETOER_1); _unlockWstETH(_VETOER_1); @@ -136,7 +136,7 @@ contract EscrowHappyPath is TestHelpers { uint256 secondVetoerStETHSharesAfterRebase = _ST_ETH.sharesOf(_VETOER_2); uint256 secondVetoerWstETHBalanceAfterRebase = _WST_ETH.balanceOf(_VETOER_2); - _wait(_config.ESCROW_ASSETS_UNLOCK_DELAY() + 1); + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); _unlockStETH(_VETOER_1); _unlockWstETH(_VETOER_1); @@ -180,7 +180,7 @@ contract EscrowHappyPath is TestHelpers { rebase(-100); - _wait(_config.ESCROW_ASSETS_UNLOCK_DELAY() + 1); + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); _unlockStETH(_VETOER_1); _unlockWstETH(_VETOER_1); @@ -206,7 +206,7 @@ contract EscrowHappyPath is TestHelpers { _lockUnstETH(_VETOER_1, unstETHIds); - _wait(_config.ESCROW_ASSETS_UNLOCK_DELAY() + 1); + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); _unlockUnstETH(_VETOER_1, unstETHIds); } diff --git a/test/scenario/gate-seal-breaker.t.sol b/test/scenario/gate-seal-breaker.t.sol index 1b427efa..f54d2937 100644 --- a/test/scenario/gate-seal-breaker.t.sol +++ b/test/scenario/gate-seal-breaker.t.sol @@ -70,7 +70,7 @@ contract SealBreakerScenarioTest is ScenarioTestBlueprint { _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_config.SIGNALLING_DEACTIVATION_DURATION() + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_DURATION() + 1); _activateNextState(); _assertVetoCooldownState(); @@ -120,14 +120,14 @@ contract SealBreakerScenarioTest is ScenarioTestBlueprint { _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_dualGovernance.CONFIG().SIGNALLING_DEACTIVATION_DURATION() + 1); + _wait(_dualGovernance.CONFIG().VETO_SIGNALLING_DEACTIVATION_DURATION() + 1); _activateNextState(); _assertVetoCooldownState(); // the stETH whale takes his funds back from Escrow _unlockStETH(_VETOER); - _wait(_dualGovernance.CONFIG().SIGNALLING_COOLDOWN_DURATION() + 1); + _wait(_dualGovernance.CONFIG().VETO_COOLDOWN_DURATION() + 1); _activateNextState(); _assertNormalState(); diff --git a/test/scenario/gov-state-transitions.t.sol b/test/scenario/gov-state-transitions.t.sol index d3f6d079..e6c4f06d 100644 --- a/test/scenario/gov-state-transitions.t.sol +++ b/test/scenario/gov-state-transitions.t.sol @@ -17,12 +17,12 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _lockStETH(_VETOER, percents("3.00")); _assertVetoSignalingState(); - _wait(_config.SIGNALLING_MIN_DURATION() / 2); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() / 2); _activateNextState(); _assertVetoSignalingState(); - _wait(_config.SIGNALLING_MIN_DURATION() / 2 + 1); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() / 2 + 1); _activateNextState(); _assertVetoSignalingDeactivationState(); @@ -35,12 +35,12 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _assertVetoSignalingState(); - _wait(_config.SIGNALLING_MAX_DURATION() / 2); + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() / 2); _activateNextState(); _assertVetoSignalingState(); - _wait(_config.SIGNALLING_MAX_DURATION() / 2 + 1); + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() / 2 + 1); _activateNextState(); _assertRageQuitState(); @@ -53,12 +53,12 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _assertVetoSignalingState(); - _wait(_config.SIGNALLING_MIN_DURATION()); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION()); _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_config.SIGNALLING_DEACTIVATION_DURATION()); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_DURATION()); _activateNextState(); _assertVetoCooldownState(); @@ -67,7 +67,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _getSignallingEscrow().unlockStETH(); vm.stopPrank(); - _wait(_config.SIGNALLING_COOLDOWN_DURATION()); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_DURATION()); _activateNextState(); _assertNormalState(); @@ -80,17 +80,17 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _assertVetoSignalingState(); - _wait(_config.SIGNALLING_MIN_DURATION()); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION()); _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_config.SIGNALLING_DEACTIVATION_DURATION()); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_DURATION()); _activateNextState(); _assertVetoCooldownState(); - _wait(_config.SIGNALLING_COOLDOWN_DURATION()); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_DURATION()); _activateNextState(); _assertVetoSignalingState(); @@ -102,7 +102,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _lockStETH(_VETOER, percents("15.00")); _assertVetoSignalingState(); - _wait(_config.SIGNALLING_MAX_DURATION()); + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION()); _activateNextState(); _assertRageQuitState(); diff --git a/test/scenario/last-moment-malicious-proposal.t.sol b/test/scenario/last-moment-malicious-proposal.t.sol index 630aae83..717f0f72 100644 --- a/test/scenario/last-moment-malicious-proposal.t.sol +++ b/test/scenario/last-moment-malicious-proposal.t.sol @@ -92,7 +92,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { // WAS SUBMITTED ON VETO SIGNALLING PHASE // --- { - vm.warp(block.timestamp + _config.SIGNALLING_DEACTIVATION_DURATION() + 1); + vm.warp(block.timestamp + _config.VETO_SIGNALLING_DEACTIVATION_DURATION() + 1); // the veto signalling deactivation duration is passed, but proposal will be executed // only when the _config.SIGNALLING_MIN_PROPOSAL_REVIEW_DURATION() from the last proposal @@ -152,10 +152,10 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { } // --- - // ACT 3. THE VETO SIGNALLING DEACTIVATION DURATION EQUALS TO "SIGNALLING_DEACTIVATION_DURATION" DAYS + // ACT 3. THE VETO SIGNALLING DEACTIVATION DURATION EQUALS TO "VETO_SIGNALLING_DEACTIVATION_DURATION" DAYS // --- { - vm.warp(block.timestamp + _config.SIGNALLING_DEACTIVATION_DURATION() + 1); + vm.warp(block.timestamp + _config.VETO_SIGNALLING_DEACTIVATION_DURATION() + 1); _activateNextState(); _assertVetoCooldownState(); From b5a3d589174e8af6e0ecca9342ac7d0a176ff4a3 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 15 Apr 2024 13:18:09 +0400 Subject: [PATCH 020/134] agent-timelock tests renamings --- test/scenario/agent-timelock.t.sol | 35 ++++++++++---------------- test/utils/scenario-test-blueprint.sol | 2 +- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/test/scenario/agent-timelock.t.sol b/test/scenario/agent-timelock.t.sol index 1fed3c8a..323466ad 100644 --- a/test/scenario/agent-timelock.t.sol +++ b/test/scenario/agent-timelock.t.sol @@ -13,57 +13,48 @@ contract AgentTimelockTest is ScenarioTestBlueprint { function testFork_AgentTimelockHappyPath() external { ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); // --- - // ACT 1. The proposal is submitted via Aragon voting + // 1. THE PROPOSAL IS SUBMITTED // --- uint256 proposalId; { proposalId = _submitProposal( _dualGovernance, "Propose to doSmth on target passing dual governance", regularStaffCalls ); - _assertSubmittedProposalData(proposalId, _config.ADMIN_EXECUTOR(), regularStaffCalls); - // proposal can't be scheduled until the AFTER_SUBMIT_DELAY has passed + _assertSubmittedProposalData(proposalId, _config.ADMIN_EXECUTOR(), regularStaffCalls); _assertCanSchedule(_dualGovernance, proposalId, false); } // --- - // ACT 2. THE PROPOSAL IS SCHEDULED + // 2. THE PROPOSAL IS SCHEDULED // --- { - // wait until the delay has passed - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() + 1); - - // when the first delay is passed and the is no opposition from the stETH holders - // the proposal can be scheduled + _waitAfterSubmitDelayPassed(); _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); - // proposal can't be executed until the second delay has ended _assertProposalScheduled(proposalId); _assertCanExecute(proposalId, false); } // --- - // ACT 3. THE PROPOSAL CAN BE EXECUTED + // 3. THE PROPOSAL CAN BE EXECUTED // --- { // wait until the second delay has passed - vm.warp(block.timestamp + _config.AFTER_SCHEDULE_DELAY() + 1); + _waitAfterScheduleDelayPassed(); // Now proposal can be executed - _assertProposalScheduled(proposalId); _assertCanExecute(proposalId, true); - // before the proposal is executed there are no calls to target - _assertNoTargetCalls(); + _assertNoTargetMockCalls(); _executeProposal(proposalId); - - // check the proposal was executed correctly _assertProposalExecuted(proposalId); - _assertCanSchedule(_dualGovernance, proposalId, false); + _assertCanExecute(proposalId, false); + _assertCanSchedule(_dualGovernance, proposalId, false); + _assertTargetMockCalls(_config.ADMIN_EXECUTOR(), regularStaffCalls); } } @@ -72,7 +63,7 @@ contract AgentTimelockTest is ScenarioTestBlueprint { ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); // --- - // ACT 1. THE PROPOSAL IS CREATED + // 1. THE PROPOSAL IS SUBMITTED // --- uint256 proposalId; { @@ -86,7 +77,7 @@ contract AgentTimelockTest is ScenarioTestBlueprint { } // --- - // ACT 2. THE PROPOSAL IS SCHEDULED + // 2. THE PROPOSAL IS SCHEDULED // --- { // wait until the delay has passed @@ -104,7 +95,7 @@ contract AgentTimelockTest is ScenarioTestBlueprint { } // --- - // ACT 3. EMERGENCY MODE ACTIVATED & GOVERNANCE RESET + // 3. EMERGENCY MODE ACTIVATED & GOVERNANCE RESET // --- { // some time passes and emergency committee activates emergency mode diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index d9119dda..754fc277 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -388,7 +388,7 @@ contract ScenarioTestBlueprint is Test { assertEq(uint256(_dualGovernance.currentState()), uint256(GovernanceState.VetoCooldown)); } - function _assertNoTargetCalls() internal { + function _assertNoTargetMockCalls() internal { assertEq(_target.getCalls().length, 0, "Unexpected target calls count"); } From 393fc7b41ca914d9a02b6383843c856be53522bf Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Mon, 15 Apr 2024 13:45:29 +0300 Subject: [PATCH 021/134] 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 bb54f623cd91d56acfd6a20d747b1fbf387431eb Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Mon, 15 Apr 2024 18:25:51 +0300 Subject: [PATCH 022/134] docs/spec: limit the time between Veto Signalling and Deactivation entrance --- docs/mechanism.md | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index 683d590c..a8a6cbf3 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -200,16 +200,18 @@ the Veto Signalling state is exited and the Rage Quit state is entered. **Transition to Deactivation**. If, while Veto Signalling is active and the Deactivation sub-state is not active, the following expression becomes true: ```math -\left( t - \max \left\{ t^S_{act} \,,\, t_{prop} \right\} > T_{lock}(R) \right) \, \land \, \left( t - t^S_{react} > T^{Sr}_{min} \right) +\left( t - \max \left\{ t^S_{act} \,,\, t_{prop} \right\} > T_{lock}(R) \right) \, \land \, \left( t - \max \left\{ t^S_{act} \,,\, t^S_{react} \right\} > T^{Sa}_{min} \right) ``` -where $T^{Sr}_{min}$ is `VetoSignallingMinReactivationDuration`, then the Deactivation sub-state of the Veto Signalling state is entered without exiting the parent Veto Signalling state. +where $T^{Sa}_{min}$ is `VetoSignallingMinActiveDuration`, then the Deactivation sub-state of the Veto Signalling state is entered without exiting the parent Veto Signalling state. + +The right part of the condition limits how fast the Deactivation sub-state can be entered and re-entered. It's needed make it impossible to keep the governance in the Deactivation sub-state (where the DAO cannot submit proposals) by front-running state transitions and locking/unlocking tokens from the signalling escrow. ```env # Proposed values, to be modeled and refined DynamicTimelockMinDuration = 5 days DynamicTimelockMaxDuration = 45 days -VetoSignallingMinReactivationDuration = 5 hours +VetoSignallingMinActiveDuration = 5 hours SecondSealRageQuitSupport = 0.1 ``` @@ -382,6 +384,24 @@ Dual governance should not cover: ### 2024-04-12 +* Limited the time between Veto Signalling state entrance and the Deactivation sub-state entrance to prevent the front-running attack allowing to keep the governance in the Deactivation sub-state (`T` is `FirstSealRageQuitSupport` and `P` is `SignallingEscrowMinLockTime`): + + ``` + 1. t = 0: lock T/2, initiate unlock + 2. t = P: execute bundle: + - lock T/2, initiate unlock => Veto Signalling is entered + - unlock T/2 => Deactivation is entered + 3. t = 2P: unlock T/2 + 4. t = [Veto Cooldown end] - P: lock T/2, initiate unlock + 5. t = X = [Veto Cooldown end]: execute bundle: + - state transition: Normal is entered + - lock T/2, initiate unlock => Veto Signalling is entered + - unlock T/2 from prev lock => Deactivation is entered + 6. t = X + P: unlock T/2 + + repeat from 4 + ``` + * Removed the lower boundary on the Normal state duration since it's not needed anymore: flash loan attacks are prevented by the signalling escrow min lock time and governance liveliness is ensured by the Veto Cooldown. ### 2024-04-10 From bfe8cee7d2c81cc9d94552c7d721a7dabf5f4a77 Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Mon, 15 Apr 2024 17:05:45 +0300 Subject: [PATCH 023/134] docs/spec: allow the Tiebreaker committee to unpause protocol contracts --- docs/mechanism.md | 11 ++++++-- docs/specification.md | 59 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index a8a6cbf3..c0f86e86 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -341,9 +341,12 @@ Given the updated Gate Seal behaviour, the system allows for reaching a deadlock Apart from the Gate Seal being activated, withdrawals can become dysfunctional due to a bug in the protocol code. If this happens while the Rage Quit state is active, it would also trigger the deadlock since a DAO proposal fixing the bug cannot be executed until the Rage Quit state is exited. -To resolve the potential deadlock, the mechanism contains a third-party arbiter **Tiebreaker Committee** elected by the DAO. The committee gains its power only under the specific conditions of the deadlock (see below), and the power is limited by bypassing the DG dynamic timelock for pending proposals approved by the DAO. +To resolve the potential deadlock, the mechanism contains a third-party arbiter **Tiebreaker Committee** elected by the DAO. The committee gains its power only under the specific conditions of the deadlock (see below), and can only perform the following actions: -Specifically, the Tiebreaker committee can execute any pending proposal submitted by the DAO to DG, subject to a timelock of `TiebreakerExecutionTimelock` days, iff any of the following two conditions is true: +* Execute any pending proposal submitted by the DAO to DG (i.e. bypass the DG dynamic timelock). +* Unpause any of the paused protocol contracts. + +The Tiebreaker committee can perform the above actions, subject to a timelock of `TiebreakerExecutionTimelock` days, iff any of the following two conditions is true: * **Tiebreaker Condition A**: (governance state is Rage Quit) $\land$ (protocol withdrawals are paused by a Gate Seal). * **Tiebreaker Condition B**: (governance state is Rage Quit) $\land$ (last time governance exited Normal or Veto Cooldown state was more than `TiebreakerActivationTimeout` days ago). @@ -384,6 +387,10 @@ Dual governance should not cover: ### 2024-04-12 +* Allowed the Tiebreaker committee to unpause protocol contracts. + + > Without this, a collusion between a malicious DAO and the Gate Seal committee (which has to be fast and thus limited in the number of participants) would allow them to indefinitely delay a rage quit by pausing certain contracts (e.g. withdrawal queue or validator exit bus) and blackmail rage quit participants. + * Limited the time between Veto Signalling state entrance and the Deactivation sub-state entrance to prevent the front-running attack allowing to keep the governance in the Deactivation sub-state (`T` is `FirstSealRageQuitSupport` and `P` is `SignallingEscrowMinLockTime`): ``` diff --git a/docs/specification.md b/docs/specification.md index 3908bbd5..c57f3dc7 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -35,7 +35,7 @@ This document provides the system description on the code architecture level. A ## System overview -![image](https://github.com/lidofinance/dual-governance/assets/1699593/8b1f119c-2a61-4d66-969c-acab2b66c16e) +![image](https://github.com/lidofinance/dual-governance/assets/1699593/e043530a-d898-4089-8e2d-c58f00436264) The system is composed of the following main contracts: @@ -158,11 +158,14 @@ From the stakers' point of view, opposition to the DAO and the rage quit process The mechanism design allows for a deadlock where the system is stuck in the `RageQuit` state while protocol withdrawals are paused or dysfunctional and require a DAO vote to resume, and includes a third-party arbiter Tiebreaker committee for resolving it. -The committee gains the power to bypass the DG dynamic timelock and execute pending proposals under the specific conditions of the deadlock. The detailed Tiebreaker mechanism design can be found in the [Dual Governance mechanism design overview][mech design - tiebreaker] document. +The committee gains the power to execute pending proposals, bypassing the DG dynamic timelock, and unpause withdrawals (if they're paused by a Gate Seal) under the specific conditions of the deadlock. The detailed Tiebreaker mechanism design can be found in the [Dual Governance mechanism design overview][mech design - tiebreaker] document. The Tiebreaker committee is represented in the system by its address which can be configured via the admin executor calling the [`DualGovernance.setTiebreakerCommittee`](#Function-DualGovernancesetTiebreakerCommittee) function. -While the deadlock conditions are met, the tiebreaker committee address is allowed to approve execution of any pending proposal by calling [`DualGovernance.tiebreakerApproveProposal`](#Function-DualGovernancetiebreakerApproveProposal) so that its execution can be scheduled after the tiebreaker execution timelock passes by calling [`DualGovernance.tiebreakerScheduleProposal`](#Function-DualGovernancetiebreakerScheduleProposal). +While the deadlock conditions are met, the tiebreaker committee address is allowed to: + +1. Approve execution of any pending proposal by calling [`DualGovernance.tiebreakerApproveProposal`] so that its execution can be scheduled by calling [`DualGovernance.tiebreakerScheduleProposal`] after the tiebreaker execution timelock passes. +2. Approve the unpause of a pausable ("sealable") protocol contract by calling [`DualGovernance.tiebreakerApproveSealableResume`] so that it can be unpaused by calling [`DualGovernance.tiebreakerScheduleSealableResume`] after the tiebreaker execution timelock passes. ## Administrative actions @@ -255,7 +258,7 @@ Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimeloc #### Preconditions -* The proposal with the given id MUST be already submitted. +* The proposal with the given id MUST be already submitted using the `DualGovernance.submitProposal` call (the proposal MUST NOT be submitted as the result of the [`DualGovernance.tiebreakerApproveSealableResume`] call). * The proposal MUST NOT be scheduled. * The proposal's dynamic timelock MUST have elapsed. * The proposal MUST NOT be cancelled. @@ -265,6 +268,8 @@ Triggers a transition of the current governance state (if one is possible) befor ### Function: DualGovernance.tiebreakerApproveProposal +[`DualGovernance.tiebreakerApproveProposal`]: #Function-DualGovernancetiebreakerApproveProposal + ```solidity function tiebreakerApproveProposal(uint256 proposalId) ``` @@ -284,6 +289,8 @@ Triggers a transition of the current governance state (if one is possible) befor ### Function: DualGovernance.tiebreakerScheduleProposal +[`DualGovernance.tiebreakerScheduleProposal`]: #Function-DualGovernancetiebreakerScheduleProposal + ```solidity function tiebreakerScheduleProposal(uint256 proposalId) ``` @@ -293,7 +300,7 @@ Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimeloc #### Preconditions * Either the Tiebreaker Condition A or the Tiebreaker Condition B MUST be met (see the [mechanism design document][mech design - tiebreaker]). -* The proposal MUST be already submitted. +* The proposal with the given id MUST be already submitted using the `DualGovernance.submitProposal` call (the proposal MUST NOT be submitted as the result of the [`DualGovernance.tiebreakerApproveSealableResume`] call). * The proposal MUST NOT be cancelled. * The proposal with the specified id MUST be approved by the Tiebreaker committee. * The current block timestamp MUST be at least `TIEBREAKER_EXECUTION_TIMELOCK` seconds greater than the timestamp of the block in which the proposal was approved by the Tiebreaker committee. @@ -301,6 +308,47 @@ Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimeloc Triggers a transition of the current governance state (if one is possible) before checking the preconditions. +### Function: DualGovernance.tiebreakerApproveSealableResume + +[`DualGovernance.tiebreakerApproveSealableResume`]: #Function-DualGovernancetiebreakerApproveSealableResume + +```solidity +function tiebreakerApproveSealableResume(address sealable) +``` + +Submits a proposal on issuing the `ISealable(sealable).resume()` call from the admin executor contract by calling `EmergencyProtectedTimelock.submit` on the `EmergencyProtectedTimelock` singleton instance. Starts a timelock with the `TIEBREAKER_EXECUTION_TIMELOCK` duration on scheduling this proposal. + +#### Branches + +If the last proposal submitted by calling this function with the same `sealable` parameter is not executed and not cancelled, then does nothing. + +#### Preconditions + +* MUST be called by the [Tiebreaker committee address](#Function-DualGovernancesetTiebreakerCommittee). +* Either the Tiebreaker Condition A or the Tiebreaker Condition B MUST be met (see the [mechanism design document][mech design - tiebreaker]). + +Triggers a transition of the current governance state (if one is possible) before checking the preconditions. + + +### Function: DualGovernance.tiebreakerScheduleSealableResume + +[`DualGovernance.tiebreakerScheduleSealableResume`]: #Function-DualGovernancetiebreakerScheduleSealableResume + +```solidity +function tiebreakerScheduleSealableResume(address sealable) +``` + +Schedules the proposal on issuing the `ISealable(sealable).resume()` call that was previously submitted by calling the [`DualGovernance.tiebreakerApproveSealableResume`] function, given that the timelock on its scheduling has elapsed. + +#### Preconditions + +* Either the Tiebreaker Condition A or the Tiebreaker Condition B MUST be met (see the [mechanism design document][mech design - tiebreaker]). +* The last proposal submitted by calling the [`DualGovernance.tiebreakerApproveSealableResume`] function with the same `sealable` parameter MUST be pending, i.e. not scheduled, not executed, and not cancelled. +* The timelock on scheduling that proposal MUST be elapsed. + +Triggers a transition of the current governance state (if one is possible) before checking the preconditions. + + ### Function: DualGovernance.cancelAllPendingProposals ```solidity @@ -314,6 +362,7 @@ Triggers a transition of the current governance state, if one is possible. #### Preconditions * MUST be called by an [admin proposer](#Administrative-actions). +* The current governance state MUST NOT equal `Normal`, `VetoCooldown`, or `RageQuit`. ### Function: DualGovernance.registerProposer From 4ce81821635b785e285ca5f7bf330c70ce7a6585 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 17 Apr 2024 09:53:53 +0300 Subject: [PATCH 024/134] 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 025/134] 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 496f7bf86ba1d8d53c3a08600883cfb01802101f Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Thu, 18 Apr 2024 16:34:08 +0300 Subject: [PATCH 026/134] spec: clarify rules about the atomicity of state transitions --- docs/mechanism.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index c0f86e86..644d513d 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -121,11 +121,11 @@ The DG mechanism can be described as a state machine defining the global governa ![image](https://github.com/lidofinance/dual-governance/assets/1699593/862b3f11-ea79-4e75-8c56-ff56f94d0a6f) -Let's now define these states and transitions. +When a state has multiple outgoing transitions, their conditions are evaluated in the order they're listed in the text. If a condition evaluates to true, the further evaluation stops and the corresponding transition is triggered. + +If a state transition A, being triggered, makes a condition for state transition B from the new state hold true at the same timestamp, the transition B must be triggered together with A. In other words, **if there's a sequence of state transitions where each preceding transition enables each subsequent one, the whole sequence must be triggered atomically**. -> Note: when a state has multiple outgoing transitions, their conditions are evaluated in the order they're listed in the text. If a condition evaluates to true, the further evaluation stops and the corresponding transition is triggered. -> -> Multiple state transitions can be sequentially triggered at the same timestamp, provided that each subsequent transition's condition is re-evaluated and holds true after the preceding transition is triggered. +Let's now define these states and transitions. ### Normal state From 221bdceaed1480814a14f3e78932cc737c463a1c Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Fri, 19 Apr 2024 14:21:25 +0300 Subject: [PATCH 027/134] spec: fix dynamic timelock time boundary value --- docs/mechanism.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index 644d513d..b565288f 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -171,9 +171,9 @@ The **dynamic timelock duration** $T_{lock}(R)$ depends on the current rage quit ```math T_{lock}(R) = \left\{ \begin{array}{lr} - 0, & \text{if } R \lt R_1 \\ - L(R), & \text{if } R_1 \leq R \leq R_2 \\ - L_{max}, & \text{if } R \gt R_2 + 0, & \text{if } R \leq R_1 \\ + L(R), & \text{if } R_1 < R < R_2 \\ + L_{max}, & \text{if } R \geq R_2 \end{array} \right. ``` From 6c3b4cdde72820ae4256f2966055e1c8caf9926e Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Fri, 19 Apr 2024 14:22:57 +0300 Subject: [PATCH 028/134] spec: clarify the Veto Signalling -> Rage Quit transition --- docs/mechanism.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index b565288f..8bdf8f3a 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -189,7 +189,7 @@ When the current rage quit support changes due to stakers locking or unlocking t Let's now define the outgoing transitions. -**Transition to Rage Quit**. If, while Veto Signalling is active (including while the Deactivation sub-state is active), the following expression becomes true: +**Transition to Rage Quit**. If, while Veto Signalling is active and the Deactivation sub-state is not active, the following expression becomes true: ```math \big( t - t^S_{act} > L_{max} \big) \, \land \, \big( R > R_2 \big) @@ -222,7 +222,7 @@ The sub-state's purpose is to allow all stakers to observe the Veto Signalling b **Transition to the parent state**. If, while the sub-state is active, the following condition becomes true: ```math -t - \max \left\{ t^S_{act} \,,\, t_{prop} \right\} \leq \, T_{lock}(R) +\big( t - \max \left\{ t^S_{act} \,,\, t_{prop} \right\} \leq \, T_{lock}(R) \big) \,\lor\, \big( R > R_2 \big) ``` then the Deactivation sub-state is exited so only the parent Veto Signalling state remains active. From fc908b4515b090a19e2065faca49c1c7c424c2db Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Fri, 19 Apr 2024 19:12:43 +0300 Subject: [PATCH 029/134] spec: fix spelling --- docs/mechanism.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index 8bdf8f3a..b6e7e7e5 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -205,7 +205,7 @@ the Veto Signalling state is exited and the Rage Quit state is entered. where $T^{Sa}_{min}$ is `VetoSignallingMinActiveDuration`, then the Deactivation sub-state of the Veto Signalling state is entered without exiting the parent Veto Signalling state. -The right part of the condition limits how fast the Deactivation sub-state can be entered and re-entered. It's needed make it impossible to keep the governance in the Deactivation sub-state (where the DAO cannot submit proposals) by front-running state transitions and locking/unlocking tokens from the signalling escrow. +The right part of the condition limits how fast the Deactivation sub-state can be entered and re-entered. It's needed to make it impossible to keep the governance in the Deactivation sub-state (where the DAO cannot submit proposals) by front-running state transitions and locking/unlocking tokens from the signalling escrow. ```env # Proposed values, to be modeled and refined From b4c32dcb1a63972a37fc7ab0c22c8548d27e2a13 Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Fri, 19 Apr 2024 19:12:12 +0300 Subject: [PATCH 030/134] spec: replace dynamic Gate Seals with the Reseal committee --- docs/mechanism.md | 46 ++++++++++++++++-------- docs/specification.md | 83 ++----------------------------------------- 2 files changed, 35 insertions(+), 94 deletions(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index b6e7e7e5..82389b25 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -26,7 +26,7 @@ Another way of looking at dual governance is that it implements 1) a dynamic use + [Veto Signalling state](#veto-signalling-state) + [Veto Cooldown state](#veto-cooldown-state) + [Rage Quit state](#rage-quit-state) - + [Gate Seal behaviour and Tiebreaker Committee](#gate-seal-behaviour-and-tiebreaker-committee) + + [Contracts pausability and Tiebreaker Committee](#contracts-pausability-and-tiebreaker-committee) * [Dual governance scope](#dual-governance-scope) * [Changelog](#changelog) @@ -316,30 +316,42 @@ RageQuitEthClaimTimelockGrowthCoeffs = (0, TODO, TODO) ``` -### Gate Seal behaviour and Tiebreaker Committee +### Contracts pausability and Tiebreaker Committee -The [Gate Seal](https://docs.lido.fi/contracts/gate-seal) is an existing circuit breaker mechanism designed to be activated in the event of a zero-day vulnerability in the protocol contracts being found or exploited and empowering a DAO-elected committee to pause certain protocol functionality, including withdrawals, for a predefined duration enough for the DAO to vote for and execute a remediation. When this happens, the committee immediately loses its power. If this never happens, the committee's power also expires after a pre-configured amount of time passes since its election. +#### Gate Seal + +The [Gate Seal](https://docs.lido.fi/contracts/gate-seal) is an existing circuit breaker mechanism designed to be activated in the event of a zero-day vulnerability in the protocol contracts being found or exploited and empowering a DAO-elected committee to pause certain protocol functionality, including withdrawals, for a predefined duration enough for the DAO to vote for and execute a remediation (let's call this state an "ephemeral pause"). When this happens, the committee immediately loses its power. If this never happens, the committee's power also expires after a pre-configured amount of time passes since its election. The pre-defined pause duration currently works since all DAO proposals have a fixed execution timelock so it's possible to configure the pause in a way that would ensure the DAO has enough time to vote on a fix, wait until the execution timelock expires, and execute the proposal before the pause ends. -The DG mechanism introduces a dynamic timelock on DAO proposals dependent on stakers' actions and protocol withdrawals processing which, in turn, requires making the Gate Seal pause duration also dynamic for the Gate Seal to remain an efficient circuit-breaker. +The DG mechanism introduces a dynamic timelock on DAO proposals dependent on stakers' actions and protocol withdrawals processing which, in turn, requires either modifying the Gate Seal mechanism to make its pause dynamic or introducing an additional mechanism for extending the pause. + +Making the Gate Seal pause dynamic has several downsides. First, it significantly increases the damage a malicious Gate Seal committee can do to the protocol, from pausing contracts for a few days to potentially pausing them for a very long duration. Second, the dynamic pause would require an explicit unpause transaction, making the mechanism significantly more complex and fragile. Thus, the Gate Seal mechanism is kept intact but an additional Reseal Committee is introduced. + +#### Reseal Committee + +The **Reseal Committee** is a multisig that has exactly one right: given the DAO proposal submission or execution is currently blocked by the Dual Governance mechanism, the committee is allowed to turn an ephemeral pause of a protocol contract into a full one, i.e. until the DAO explicitly unpases the contract. -#### Gate Seal behaviour (updated) +Specifically, the Reseal Committee has the right to pause an [ephemerally pausable contract](https://github.com/lidofinance/lido-dao/blob/master/contracts/0.8.9/utils/PausableUntil.sol) for an indefinite duration if two conditions become true simultaneously: -If, at any moment in time, two conditions become true simultaneously: +1. the contract is ephemerally paused, i.e. its unpause time is above the current block timestamp and below the `PAUSE_INFINITELY` value ($2^{256} - 1$); +2. the current governance state is different from Normal. -1. any DAO-managed contract functionality is paused by a Gate Seal; -2. the DAO execution is blocked by the DG mechanism (i.e. the global governance state is Veto Signalling, Veto Signalling Deactivation, or Rage Quit), +The committee should have more members and a higher quorum value than the Gate Seal committee due to the higher potential damage to the protocol in the case of misuse. -then the Gate Seal-induced pause is prolonged until the DAO execution is unblocked by the DG, i.e. until the global governance state becomes Normal or Veto Cooldown. +The intended scenario for this committee is the following: -Otherwise, the Gate Seal-induced pause lasts for a pre-defined fixed duration. +1. A vulnerability in a protocol contract is discovered and communicated to the Gate Seal and Reseal committees. +2. The Gate Seal committee pauses the contract for a fixed duration. +3. The governance occurs in a non-Normal state, either because it was in a non-Normal state at the moment the pause was triggered or because it exited the Normal state during the pause. +4. The Reseal committee pauses the contract for an indefinite duration. +5. When the DAO execution is unblocked, the DAO votes for and executes a proposal that fixes the vulnerability and unpauses the contract. #### Tiebreaker Committee -Given the updated Gate Seal behaviour, the system allows for reaching a deadlock state: if the protocol withdrawals functionality gets paused by the Gate Seal committee while the governance state is Rage Quit, or if it gets paused before and remains paused until the Rage Quit starts, then withdrawals should remain paused until the Rage Quit state is exited and the DAO execution is unblocked. But the Rage Quit state lasts until all staked ETH participating in the rage quit is withdrawn to ETH, which cannot happen while withdrawals are paused. +Given the pausability of protocol contracts, the system allows for reaching a deadlock state: if the protocol withdrawals functionality gets permanently paused before the Rage Quit state is entered or while it's active, the rage quit process won't be able to finish until the pause is lifted. But a permanently paused contract can only be unpaused by the DAO, and the DAO execution is blocked until the rage quit process is finished and the Rage Quit state is exited. -Apart from the Gate Seal being activated, withdrawals can become dysfunctional due to a bug in the protocol code. If this happens while the Rage Quit state is active, it would also trigger the deadlock since a DAO proposal fixing the bug cannot be executed until the Rage Quit state is exited. +Apart from being paused, withdrawals can become dysfunctional due to a bug in the protocol code. If this happens while the Rage Quit state is active, it would also trigger the deadlock since a DAO proposal fixing the bug cannot be executed until the Rage Quit state is exited. To resolve the potential deadlock, the mechanism contains a third-party arbiter **Tiebreaker Committee** elected by the DAO. The committee gains its power only under the specific conditions of the deadlock (see below), and can only perform the following actions: @@ -348,10 +360,10 @@ To resolve the potential deadlock, the mechanism contains a third-party arbiter The Tiebreaker committee can perform the above actions, subject to a timelock of `TiebreakerExecutionTimelock` days, iff any of the following two conditions is true: -* **Tiebreaker Condition A**: (governance state is Rage Quit) $\land$ (protocol withdrawals are paused by a Gate Seal). +* **Tiebreaker Condition A**: (governance state is Rage Quit) $\land$ (protocol withdrawals are paused for a duration exceeding `TiebreakerActivationTimeout`). * **Tiebreaker Condition B**: (governance state is Rage Quit) $\land$ (last time governance exited Normal or Veto Cooldown state was more than `TiebreakerActivationTimeout` days ago). -The Tiebreaker committee should be composed of multiple sub-committees covering different interest groups within the Ethereum community (e.g. largest DAOs, EF, L2s, node operators, OGs) and should require approval from a supermajority of sub-committees to execute a pending proposal. The approval by each sub-committee should require the majority support within the sub-committee. No sub-committee should contain more than $1/4$ of the members that are also members of the withdrawals Gate Seal committee. +The Tiebreaker committee should be composed of multiple sub-committees covering different interest groups within the Ethereum community (e.g. largest DAOs, EF, L2s, node operators, OGs) and should require approval from a supermajority of sub-committees to execute a pending proposal. The approval by each sub-committee should require the majority support within the sub-committee. No sub-committee should contain more than $1/4$ of the members that are also members of the Reseal committee. The composition of the Tiebreaker committee should be set by a DAO vote (subject to DG) and reviewed at least once a year. @@ -385,6 +397,12 @@ Dual governance should not cover: ## Changelog +### 2024-04-19 + +* Replaced the dynamic Gate Seal pause mechanism with the Reseal Committee. + + > A dynamic Gate Seal pause has several issues, including the conflict between the requirement for a very fast committee reaction time and the requirement for its increased safety due to higher potential damage under the DG, as well as increased complexity and operational fragility of the resulting mechanism. Keeping the Gate Seal pause static and introducing the additional committee provides for both the quick and impact-limited Gate Seal committee and a safer Reseal Committee by allowing the latter for a slower reaction time (and thus a larger quorum value) and imposing limitations on the conditions under which it can be activated. + ### 2024-04-12 * Allowed the Tiebreaker committee to unpause protocol contracts. diff --git a/docs/specification.md b/docs/specification.md index c57f3dc7..dc43cc78 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -28,14 +28,13 @@ This document provides the system description on the code architecture level. A * [Contract: Executor.sol](#contract-executorsol) * [Contract: Escrow.sol](#contract-escrowsol) * [Contract: EmergencyProtectedTimelock.sol](#contract-emergencyprotectedtimelocksol) -* [Contract: GateSealBreaker.sol](#contract-gatesealbreakersol) * [Contract: Configuration.sol](#contract-configurationsol) * [Upgrade flow description](#upgrade-flow-description) ## System overview -![image](https://github.com/lidofinance/dual-governance/assets/1699593/e043530a-d898-4089-8e2d-c58f00436264) +![image](https://github.com/lidofinance/dual-governance/assets/1699593/0ca7686c-63bb-489a-bc6a-59d8b9982969) The system is composed of the following main contracts: @@ -43,7 +42,6 @@ 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. ## Proposal flow @@ -158,11 +156,11 @@ From the stakers' point of view, opposition to the DAO and the rage quit process The mechanism design allows for a deadlock where the system is stuck in the `RageQuit` state while protocol withdrawals are paused or dysfunctional and require a DAO vote to resume, and includes a third-party arbiter Tiebreaker committee for resolving it. -The committee gains the power to execute pending proposals, bypassing the DG dynamic timelock, and unpause withdrawals (if they're paused by a Gate Seal) under the specific conditions of the deadlock. The detailed Tiebreaker mechanism design can be found in the [Dual Governance mechanism design overview][mech design - tiebreaker] document. +The committee gains the power to execute pending proposals, bypassing the DG dynamic timelock, and unpause any protocol contract under the specific conditions of the deadlock. The detailed Tiebreaker mechanism design can be found in the [Dual Governance mechanism design overview][mech design - tiebreaker] document. The Tiebreaker committee is represented in the system by its address which can be configured via the admin executor calling the [`DualGovernance.setTiebreakerCommittee`](#Function-DualGovernancesetTiebreakerCommittee) function. -While the deadlock conditions are met, the tiebreaker committee address is allowed to: +While the deadlock conditions are met, the Tiebreaker committee address is allowed to: 1. Approve execution of any pending proposal by calling [`DualGovernance.tiebreakerApproveProposal`] so that its execution can be scheduled by calling [`DualGovernance.tiebreakerScheduleProposal`] after the tiebreaker execution timelock passes. 2. Approve the unpause of a pausable ("sealable") protocol contract by calling [`DualGovernance.tiebreakerApproveSealableResume`] so that it can be unpaused by calling [`DualGovernance.tiebreakerScheduleSealableResume`] after the tiebreaker execution timelock passes. @@ -971,81 +969,6 @@ 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 - -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): - ->*"A GateSeal is a contract that allows the designated account to instantly put a set of contracts on pause (i.e. seal) for a limited duration. This will give the Lido DAO the time to come up with a solution, hold a vote, implement changes, etc.".* - -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). - -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. - -### Function GateSealBreaker.registerGateSeal - -```solidity -function registerGateSeal(IGateSeal gateSeal) -``` - -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. - -#### 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. - ## 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 eaba5ea7c02025487f76d14faf2883437a953746 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Sun, 21 Apr 2024 03:11:49 +0400 Subject: [PATCH 031/134] Dual Governance State refactoring & spec compliance --- contracts/Configuration.sol | 49 +- contracts/DualGovernance.sol | 26 +- contracts/interfaces/IConfiguration.sol | 27 +- contracts/libraries/DualGovernanceState.sol | 449 +++++++++--------- test/scenario/gate-seal-breaker.t.sol | 4 +- test/scenario/gov-state-transitions.t.sol | 47 +- .../last-moment-malicious-proposal.t.sol | 22 +- test/utils/percents.sol | 5 + test/utils/scenario-test-blueprint.sol | 37 +- test/utils/utils.sol | 40 +- 10 files changed, 408 insertions(+), 298 deletions(-) diff --git a/contracts/Configuration.sol b/contracts/Configuration.sol index 69754366..e957a29c 100644 --- a/contracts/Configuration.sol +++ b/contracts/Configuration.sol @@ -1,39 +1,43 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {IConfiguration} from "./interfaces/IConfiguration.sol"; +import {IConfiguration, DualGovernanceConfig} from "./interfaces/IConfiguration.sol"; uint256 constant PERCENT = 10 ** 16; contract Configuration is IConfiguration { error MaxSealablesLimitOverflow(uint256 count, uint256 limit); - address public immutable ADMIN_EXECUTOR; - address public immutable EMERGENCY_GOVERNANCE; - - uint256 public immutable AFTER_SUBMIT_DELAY = 3 days; - uint256 public immutable AFTER_SCHEDULE_DELAY = 2 days; - - uint256 public immutable RAGE_QUIT_ETH_WITHDRAWAL_TIMELOCK = 30 days; - - uint256 public immutable VETO_COOLDOWN_DURATION = 4 days; - uint256 public immutable VETO_SIGNALLING_DEACTIVATION_DURATION = 5 days; - uint256 public immutable SIGNALLING_MIN_PROPOSAL_REVIEW_DURATION = 30 days; + // --- + // Dual Governance State Properties + // --- + uint256 public immutable FIRST_SEAL_RAGE_QUIT_SUPPORT = 3 * PERCENT; + uint256 public immutable SECOND_SEAL_RAGE_QUIT_SUPPORT = 15 * PERCENT; uint256 public immutable DYNAMIC_TIMELOCK_MIN_DURATION = 3 days; uint256 public immutable DYNAMIC_TIMELOCK_MAX_DURATION = 30 days; - uint256 public immutable FIRST_SEAL_RAGE_QUIT_SUPPORT = 3 * PERCENT; - uint256 public immutable SECOND_SEAL_RAGE_QUIT_SUPPORT = 15 * PERCENT; + uint256 public immutable VETO_SIGNALLING_MIN_ACTIVE_DURATION = 5 hours; + uint256 public immutable VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = 5 days; + uint256 public immutable RAGE_QUIT_ACCUMULATION_MAX_DURATION = 3 days; - uint256 public immutable TIE_BREAK_ACTIVATION_TIMEOUT = 365 days; + uint256 public immutable VETO_COOLDOWN_DURATION = 4 days; uint256 public immutable RAGE_QUIT_EXTRA_TIMELOCK = 14 days; uint256 public immutable RAGE_QUIT_EXTENSION_DELAY = 7 days; uint256 public immutable RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK = 60 days; - uint256 public immutable MIN_STATE_DURATION = 5 hours; + // --- + + address public immutable ADMIN_EXECUTOR; + address public immutable EMERGENCY_GOVERNANCE; + + uint256 public immutable AFTER_SUBMIT_DELAY = 3 days; + uint256 public immutable AFTER_SCHEDULE_DELAY = 2 days; + uint256 public immutable SIGNALLING_ESCROW_MIN_LOCK_TIME = 5 hours; + uint256 public immutable TIE_BREAK_ACTIVATION_TIMEOUT = 365 days; + // Sealables Array Representation uint256 private immutable MAX_SELABLES_COUNT = 5; @@ -85,4 +89,17 @@ contract Configuration is IConfiguration { dynamicTimelockMinDuration = DYNAMIC_TIMELOCK_MIN_DURATION; dynamicTimelockMaxDuration = DYNAMIC_TIMELOCK_MAX_DURATION; } + + function getDualGovernanceConfig() external view returns (DualGovernanceConfig memory config) { + config.firstSealRageQuitSupport = FIRST_SEAL_RAGE_QUIT_SUPPORT; + config.secondSealRageQuitSupport = SECOND_SEAL_RAGE_QUIT_SUPPORT; + config.dynamicTimelockMinDuration = DYNAMIC_TIMELOCK_MIN_DURATION; + config.dynamicTimelockMaxDuration = DYNAMIC_TIMELOCK_MAX_DURATION; + config.vetoSignallingMinActiveDuration = VETO_SIGNALLING_MIN_ACTIVE_DURATION; + config.vetoSignallingDeactivationMaxDuration = VETO_SIGNALLING_DEACTIVATION_MAX_DURATION; + config.vetoCooldownDuration = VETO_COOLDOWN_DURATION; + config.rageQuitExtraTimelock = RAGE_QUIT_EXTRA_TIMELOCK; + config.rageQuitExtensionDelay = RAGE_QUIT_EXTENSION_DELAY; + config.rageQuitEthClaimMinTimelock = RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK; + } } diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index a195c24f..7a41324c 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -7,11 +7,17 @@ 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"; +import { + State, + DualGovernanceState, + DualGovernanceStateTransitions, + DualGovernanceStateViews +} from "./libraries/DualGovernanceState.sol"; contract DualGovernance is IGovernance, ConfigurationProvider { using Proposers for Proposers.State; - using DualGovernanceState for DualGovernanceState.Store; + using DualGovernanceStateViews for DualGovernanceState; + using DualGovernanceStateTransitions for DualGovernanceState; event TiebreakerSet(address tiebreakCommittee); event ProposalScheduled(uint256 proposalId); @@ -24,7 +30,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { address internal _tiebreaker; Proposers.State internal _proposers; - DualGovernanceState.Store internal _dgState; + DualGovernanceState internal _dgState; EmergencyProtection.State internal _emergencyProtection; mapping(uint256 proposalId => uint256 executableAfter) internal _scheduledProposals; @@ -42,7 +48,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function submit(ExecutorCall[] calldata calls) external returns (uint256 proposalId) { _proposers.checkProposer(msg.sender); - _dgState.activateNextState(CONFIG); + _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); _dgState.checkProposalsCreationAllowed(); _dgState.setLastProposalCreationTimestamp(); Proposer memory proposer = _proposers.get(msg.sender); @@ -50,7 +56,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { } function schedule(uint256 proposalId) external { - _dgState.activateNextState(CONFIG); + _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); _dgState.checkProposalsAdoptionAllowed(); TIMELOCK.schedule(proposalId); emit ProposalScheduled(proposalId); @@ -78,10 +84,10 @@ contract DualGovernance is IGovernance, ConfigurationProvider { // --- function activateNextState() external { - _dgState.activateNextState(CONFIG); + _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); } - function currentState() external view returns (GovernanceState) { + function currentState() external view returns (State) { return _dgState.currentState(); } @@ -90,7 +96,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { view returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) { - (isActive, duration, activatedAt, enteredAt) = _dgState.getVetoSignallingState(CONFIG); + (isActive, duration, activatedAt, enteredAt) = _dgState.getVetoSignallingState(CONFIG.getDualGovernanceConfig()); } function getVetoSignallingDeactivationState() @@ -98,11 +104,11 @@ contract DualGovernance is IGovernance, ConfigurationProvider { view returns (bool isActive, uint256 duration, uint256 enteredAt) { - (isActive, duration, enteredAt) = _dgState.getVetoSignallingDeactivationState(CONFIG); + (isActive, duration, enteredAt) = _dgState.getVetoSignallingDeactivationState(CONFIG.getDualGovernanceConfig()); } function getVetoSignallingDuration() external view returns (uint256) { - return _dgState.getVetoSignallingDuration(CONFIG); + return _dgState.getVetoSignallingDuration(CONFIG.getDualGovernanceConfig()); } function isSchedulingEnabled() external view returns (bool) { diff --git a/contracts/interfaces/IConfiguration.sol b/contracts/interfaces/IConfiguration.sol index d3546d40..19de5116 100644 --- a/contracts/interfaces/IConfiguration.sol +++ b/contracts/interfaces/IConfiguration.sol @@ -1,6 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +struct DualGovernanceConfig { + uint256 firstSealRageQuitSupport; + uint256 secondSealRageQuitSupport; + // TODO: consider dynamicDelayMaxDuration + uint256 dynamicTimelockMaxDuration; + uint256 dynamicTimelockMinDuration; + uint256 vetoSignallingMinActiveDuration; + uint256 vetoSignallingDeactivationMaxDuration; + uint256 vetoCooldownDuration; + uint256 rageQuitExtraTimelock; + uint256 rageQuitExtensionDelay; + uint256 rageQuitEthClaimMinTimelock; +} + interface IAdminExecutorConfiguration { function ADMIN_EXECUTOR() external view returns (address); } @@ -12,11 +26,12 @@ interface ITimelockConfiguration { } interface IDualGovernanceConfiguration { - function RAGE_QUIT_ETH_WITHDRAWAL_TIMELOCK() external view returns (uint256); + function TIE_BREAK_ACTIVATION_TIMEOUT() external view returns (uint256); function VETO_COOLDOWN_DURATION() external view returns (uint256); - function VETO_SIGNALLING_DEACTIVATION_DURATION() external view returns (uint256); - function SIGNALLING_MIN_PROPOSAL_REVIEW_DURATION() external view returns (uint256); + function VETO_SIGNALLING_MIN_ACTIVE_DURATION() external view returns (uint256); + + function VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() external view returns (uint256); function DYNAMIC_TIMELOCK_MIN_DURATION() external view returns (uint256); function DYNAMIC_TIMELOCK_MAX_DURATION() external view returns (uint256); @@ -24,13 +39,11 @@ interface IDualGovernanceConfiguration { function FIRST_SEAL_RAGE_QUIT_SUPPORT() external view returns (uint256); function SECOND_SEAL_RAGE_QUIT_SUPPORT() external view returns (uint256); - function TIE_BREAK_ACTIVATION_TIMEOUT() external view returns (uint256); - function RAGE_QUIT_EXTRA_TIMELOCK() external view returns (uint256); function RAGE_QUIT_EXTENSION_DELAY() external view returns (uint256); function RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK() external view returns (uint256); + function RAGE_QUIT_ACCUMULATION_MAX_DURATION() external view returns (uint256); - function MIN_STATE_DURATION() external view returns (uint256); function SIGNALLING_ESCROW_MIN_LOCK_TIME() external view returns (uint256); function sealableWithdrawalBlockers() external view returns (address[] memory); @@ -44,6 +57,8 @@ interface IDualGovernanceConfiguration { uint256 signallingMinDuration, uint256 signallingMaxDuration ); + + function getDualGovernanceConfig() external view returns (DualGovernanceConfig memory config); } interface IConfiguration is IAdminExecutorConfiguration, ITimelockConfiguration, IDualGovernanceConfiguration {} diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol index 5d9ab8dd..c8bc4621 100644 --- a/contracts/libraries/DualGovernanceState.sol +++ b/contracts/libraries/DualGovernanceState.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {IEscrow} from "../interfaces/IEscrow.sol"; -import {IDualGovernanceConfiguration as IConfiguration} from "../interfaces/IConfiguration.sol"; +import {IDualGovernanceConfiguration as IConfiguration, DualGovernanceConfig} from "../interfaces/IConfiguration.sol"; import {TimeUtils} from "../utils/time.sol"; @@ -20,44 +21,62 @@ enum State { RageQuit } -library DualGovernanceState { - struct Store { - State state; - uint40 enteredAt; - // - uint40 vetoSignallingFirstActivation; - uint40 vetoSignallingLastActivation; - // - uint40 lastAdoptableStateExitedAt; - IEscrow signallingEscrow; - IEscrow rageQuitEscrow; - uint8 rageQuitRound; +struct DualGovernanceState { + State state; + uint8 rageQuitRound; + uint40 enteredAt; + // the time the veto signalling state was entered + uint40 vetoSignallingActivationTime; + // the time the Deactivation sub-state was last exited without exiting the parent Veto Signalling state + uint40 vetoSignallingReactivationTime; + IEscrow signallingEscrow; + // the last time a proposal was submitted to the DG subsystem + uint40 lastProposalSubmissionTime; + uint40 lastAdoptableStateExitedAt; + IEscrow rageQuitEscrow; +} + +function dynamicTimelockDuration( + DualGovernanceConfig memory config, + uint256 rageQuitSupport +) pure returns (uint256 duration_) { + uint256 firstSealRageQuitSupport = config.firstSealRageQuitSupport; + uint256 secondSealRageQuitSupport = config.secondSealRageQuitSupport; + uint256 dynamicTimelockMinDuration = config.dynamicTimelockMinDuration; + uint256 dynamicTimelockMaxDuration = config.dynamicTimelockMaxDuration; + + if (rageQuitSupport < firstSealRageQuitSupport) { + return 0; } - // uint40 vetoAccumulationDuration; - // uint40 vetoDeactivationDuration; - error NotTie(); + if (rageQuitSupport >= secondSealRageQuitSupport) { + return dynamicTimelockMaxDuration; + } + + duration_ = dynamicTimelockMinDuration + + (rageQuitSupport - firstSealRageQuitSupport) * (dynamicTimelockMaxDuration - dynamicTimelockMinDuration) + / (secondSealRageQuitSupport - firstSealRageQuitSupport); +} + +library DualGovernanceStateTransitions { error AlreadyInitialized(); - error ProposalsCreationSuspended(); - error ProposalsAdoptionSuspended(); event NewSignallingEscrowDeployed(address indexed escrow); event DualGovernanceStateChanged(State oldState, State newState); - function initialize(Store storage self, address escrowMasterCopy) internal { + function initialize(DualGovernanceState storage self, address escrowMasterCopy) internal { if (address(self.signallingEscrow) != address(0)) { revert AlreadyInitialized(); } _deployNewSignallingEscrow(self, escrowMasterCopy); } - function activateNextState(Store storage self, IConfiguration config) internal returns (State newState) { + function activateNextState( + DualGovernanceState storage self, + DualGovernanceConfig memory config + ) internal returns (State newState) { State oldState = self.state; - // TODO: Currently doesn't match spec precisely because not only Normal or VetoSignalling states are bounded. - // But it seems like there are no states that may last shorter than MIN_STATE_DURATION - if (block.timestamp < self.enteredAt + config.MIN_STATE_DURATION()) { - newState = oldState; - } else if (oldState == State.Normal) { + if (oldState == State.Normal) { newState = _fromNormalState(self, config); } else if (oldState == State.VetoSignalling) { newState = _fromVetoSignallingState(self, config); @@ -72,227 +91,123 @@ library DualGovernanceState { } if (oldState != newState) { - _setState(self, oldState, newState); + self.state = newState; _handleStateTransitionSideEffects(self, config, oldState, newState); emit DualGovernanceStateChanged(oldState, newState); } } - // TODO: Consider this code as possible option. Delete if not needed - // function onNewProposal(Store storage self, IConfiguration config) internal { - // uint256 accumulationMaxDuration = config.SIGNALLING_MAX_DURATION(); - // uint256 deactivationMinDuration = config.SIGNALLING_DEACTIVATION_DURATION(); - - // if (self.state == State.Normal || self.state == State.RageQuit) { - // self.vetoAccumulationDuration = TimeUtils.timestamp(accumulationMaxDuration); - // self.vetoDeactivationDuration = TimeUtils.timestamp(deactivationMinDuration); - // } else if (self.state == State.VetoAccumulation) { - // // when the proposal submitted during the veto accumulation phase - // uint256 enteredAt = self.enteredAt; - // uint256 vetoAccumulationDurationPassed = block.timestamp - enteredAt; - // // now we have to decrease veto accumulation duration on passed time and increase the - // // deactivation duration - // uint256 vetoAccumulationDurationNew = self.vetoAccumulationDuration > vetoAccumulationDurationPassed - // ? self.vetoAccumulationDuration - vetoAccumulationDurationPassed - // : 0; - // uint256 vetoDeactivationDurationNew = - // deactivationMinDuration + accumulationMaxDuration - vetoAccumulationDurationNew; - - // self.vetoAccumulationDuration = TimeUtils.timestamp(vetoAccumulationDurationNew); - // self.vetoDeactivationDuration = TimeUtils.timestamp(vetoDeactivationDurationNew); - // // when the durations were updated, assuming that vet signalling was reactivated - // // at this point. - // self.enteredAt = TimeUtils.timestamp(); - // } else { - // // in any other cases, proposal can't be submitted - // assert(false); - // } - // } - - function setLastProposalCreationTimestamp(Store storage self) internal { - if (self.state == State.VetoSignalling) { - self.vetoSignallingLastActivation = TimeUtils.timestamp(); - } - } - - function checkProposalsCreationAllowed(Store storage self) internal view { - if (!isProposalsCreationAllowed(self)) { - revert ProposalsCreationSuspended(); - } - } - - function checkProposalsAdoptionAllowed(Store storage self) internal view { - if (!isProposalsAdoptionAllowed(self)) { - revert ProposalsAdoptionSuspended(); - } - } - - function checkTiebreak(Store storage self, IConfiguration config) internal view { - if (!isTiebreak(self, config)) { - revert NotTie(); - } - } - - function currentState(Store storage self) internal view returns (State) { - return self.state; - } - - function isProposalsCreationAllowed(Store storage self) internal view returns (bool) { - State state = self.state; - return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; - } - - function isProposalsAdoptionAllowed(Store storage self) internal view returns (bool) { - State state = self.state; - return state == State.Normal || state == State.VetoCooldown; - } - - function isTiebreak(Store storage self, IConfiguration config) internal view returns (bool) { - if (isProposalsAdoptionAllowed(self)) return false; - - // for the governance is locked for long period of time - if (block.timestamp - self.lastAdoptableStateExitedAt >= config.TIE_BREAK_ACTIVATION_TIMEOUT()) return true; - - if (self.state != State.RageQuit) return false; - - address[] memory sealableWithdrawalBlockers = config.sealableWithdrawalBlockers(); - for (uint256 i = 0; i < sealableWithdrawalBlockers.length; ++i) { - if (IPausableUntil(sealableWithdrawalBlockers[i]).isPaused()) return true; - } - return false; - } - - function getVetoSignallingState( - Store storage self, - IConfiguration config - ) internal view returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) { - isActive = self.state == State.VetoSignalling; - duration = isActive ? getVetoSignallingDuration(self, config) : 0; - enteredAt = isActive ? self.enteredAt : 0; - activatedAt = isActive ? self.vetoSignallingLastActivation : 0; - } - - function getVetoSignallingDuration(Store storage self, IConfiguration config) internal view returns (uint256) { - uint256 totalSupport = self.signallingEscrow.getRageQuitSupport(); - return _calcVetoSignallingTargetDuration(config, totalSupport); - } - - struct VetoSignallingDeactivationState { - uint256 duration; - uint256 enteredAt; - } - - function getVetoSignallingDeactivationState( - Store storage self, - IConfiguration config - ) internal view returns (bool isActive, uint256 duration, uint256 enteredAt) { - isActive = self.state == State.VetoSignallingDeactivation; - duration = config.VETO_SIGNALLING_DEACTIVATION_DURATION(); - enteredAt = isActive ? self.enteredAt : 0; + function setLastProposalCreationTimestamp(DualGovernanceState storage self) internal { + self.lastProposalSubmissionTime = TimeUtils.timestamp(); } // --- - // Store Transitions + // State Transitions // --- - function _fromNormalState(Store storage self, IConfiguration config) private view returns (State) { - uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - return rageQuitSupport >= config.FIRST_SEAL_RAGE_QUIT_SUPPORT() ? State.VetoSignalling : State.Normal; + function _fromNormalState( + DualGovernanceState storage self, + DualGovernanceConfig memory config + ) private view returns (State) { + return _isFirstSealRageQuitSupportCrossed(config, self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; } - function _fromVetoSignallingState(Store storage self, IConfiguration config) private view returns (State) { + function _fromVetoSignallingState( + DualGovernanceState storage self, + DualGovernanceConfig memory config + ) private view returns (State) { uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - if (rageQuitSupport < config.FIRST_SEAL_RAGE_QUIT_SUPPORT()) { - return State.VetoSignallingDeactivation; - } - - uint256 vetoSignallingTotalDuration = block.timestamp - self.vetoSignallingFirstActivation; - - if ( - vetoSignallingTotalDuration >= config.DYNAMIC_TIMELOCK_MAX_DURATION() - && _isSecondThresholdReached(self, config) - ) { - return State.RageQuit; + // 1. Transition to RageQuitAccumulation + if (_isDynamicTimelockMaxDurationPassed(self, config)) { + if (_isSecondSealRageQuitSupportCrossed(config, rageQuitSupport)) { + return State.RageQuit; + } } - uint256 vetoSignallingCurrentDuration = block.timestamp - self.vetoSignallingLastActivation; - uint256 targetDuration = _calcVetoSignallingTargetDuration(config, rageQuitSupport); + // 2. Transition to VetoSignallingDeactivation - // spent in the VetoAccumulation state longer than needed - if (vetoSignallingCurrentDuration >= targetDuration) { - return State.VetoSignallingDeactivation; + if (_isDynamicTimelockDurationPassed(self, config, rageQuitSupport)) { + if (_isVetoSignallingReactivationDurationPassed(self, config)) { + return State.VetoSignallingDeactivation; + } } return State.VetoSignalling; } function _fromVetoSignallingDeactivationState( - Store storage self, - IConfiguration config + DualGovernanceState storage self, + DualGovernanceConfig memory config ) private view returns (State) { uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - uint256 vetoSignallingCurrentDuration = block.timestamp - self.vetoSignallingLastActivation; - uint256 targetDuration = _calcVetoSignallingTargetDuration(config, rageQuitSupport); - if (targetDuration > vetoSignallingCurrentDuration) { + if (!_isDynamicTimelockDurationPassed(self, config, rageQuitSupport)) { return State.VetoSignalling; } - if (block.timestamp - self.enteredAt <= config.VETO_SIGNALLING_DEACTIVATION_DURATION()) { - return State.VetoSignallingDeactivation; + if (_isVetoSignallingDeactivationMaxDurationPassed(self, config)) { + return State.VetoCooldown; } - return _isSecondThresholdReached(self, config) ? State.RageQuit : State.VetoCooldown; + return State.VetoSignallingDeactivation; } - function _fromVetoCooldownState(Store storage self, IConfiguration config) private view returns (State) { - uint256 duration_ = block.timestamp - self.enteredAt; - if (duration_ < config.VETO_COOLDOWN_DURATION()) { + function _fromVetoCooldownState( + DualGovernanceState storage self, + DualGovernanceConfig memory config + ) private view returns (State) { + if (!_isVetoCooldownDurationPassed(self, config)) { return State.VetoCooldown; } - return _isFirstThresholdReached(self, config) ? State.VetoSignalling : State.Normal; + return _isFirstSealRageQuitSupportCrossed(config, self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.Normal; } - function _fromRageQuitState(Store storage self, IConfiguration config) private view returns (State) { + function _fromRageQuitState( + DualGovernanceState storage self, + DualGovernanceConfig memory config + ) private view returns (State) { if (!self.rageQuitEscrow.isRageQuitFinalized()) { return State.RageQuit; } - return _isFirstThresholdReached(self, config) ? State.VetoSignalling : State.VetoCooldown; + return _isFirstSealRageQuitSupportCrossed(config, self.signallingEscrow.getRageQuitSupport()) + ? State.VetoSignalling + : State.VetoCooldown; } - function _setState(Store storage self, State oldState, State newState) private { - assert(oldState != newState); - assert(self.state == oldState); - - self.state = newState; - - uint40 currentTime = TimeUtils.timestamp(); - self.enteredAt = currentTime; - } + // --- + // Helper Methods + // --- function _handleStateTransitionSideEffects( - Store storage self, - IConfiguration config, + DualGovernanceState storage self, + DualGovernanceConfig memory config, State oldState, State newState ) private { - uint40 currentTime = TimeUtils.timestamp(); + uint40 timestamp = TimeUtils.timestamp(); + self.enteredAt = timestamp; // track the time when the governance state allowed execution if (oldState == State.Normal || oldState == State.VetoCooldown) { - self.lastAdoptableStateExitedAt = currentTime; + self.lastAdoptableStateExitedAt = timestamp; } if (newState == State.Normal && self.rageQuitRound != 0) { self.rageQuitRound = 0; } if (newState == State.VetoSignalling && oldState != State.VetoSignallingDeactivation) { - self.vetoSignallingFirstActivation = currentTime; - self.vetoSignallingLastActivation = currentTime; + self.vetoSignallingActivationTime = timestamp; + } + if (oldState == State.VetoSignallingDeactivation && newState == State.VetoSignalling) { + self.vetoSignallingReactivationTime = timestamp; } if (newState == State.RageQuit) { IEscrow signallingEscrow = self.signallingEscrow; signallingEscrow.startRageQuit( - config.RAGE_QUIT_EXTRA_TIMELOCK(), _calcRageQuitWithdrawalsTimelock(config, self.rageQuitRound) + config.rageQuitExtraTimelock, _calcRageQuitWithdrawalsTimelock(config, self.rageQuitRound) ); self.rageQuitEscrow = signallingEscrow; _deployNewSignallingEscrow(self, signallingEscrow.MASTER_COPY()); @@ -300,40 +215,59 @@ library DualGovernanceState { } } - // --- - // Helper Methods - // --- + function _isFirstSealRageQuitSupportCrossed( + DualGovernanceConfig memory config, + uint256 rageQuitSupport + ) private pure returns (bool) { + return rageQuitSupport > config.firstSealRageQuitSupport; + } - function _isFirstThresholdReached(Store storage self, IConfiguration config) private view returns (bool) { - uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - return rageQuitSupport >= config.FIRST_SEAL_RAGE_QUIT_SUPPORT(); + function _isSecondSealRageQuitSupportCrossed( + DualGovernanceConfig memory config, + uint256 rageQuitSupport + ) private pure returns (bool) { + return rageQuitSupport > config.secondSealRageQuitSupport; } - function _isSecondThresholdReached(Store storage self, IConfiguration config) private view returns (bool) { - uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - return rageQuitSupport >= config.SECOND_SEAL_RAGE_QUIT_SUPPORT(); + function _isDynamicTimelockMaxDurationPassed( + DualGovernanceState storage self, + DualGovernanceConfig memory config + ) private view returns (bool) { + return block.timestamp - self.vetoSignallingActivationTime > config.dynamicTimelockMaxDuration; } - function _calcVetoSignallingTargetDuration( - IConfiguration config, - uint256 totalSupport - ) private view returns (uint256 duration_) { - (uint256 firstSealThreshold, uint256 secondSealThreshold, uint256 minDuration, uint256 maxDuration) = - config.getSignallingThresholdData(); + function _isDynamicTimelockDurationPassed( + DualGovernanceState storage self, + DualGovernanceConfig memory config, + uint256 rageQuitSupport + ) private view returns (bool) { + uint256 vetoSignallingDurationPassed = + block.timestamp - Math.max(self.vetoSignallingActivationTime, self.lastProposalSubmissionTime); + return vetoSignallingDurationPassed > dynamicTimelockDuration(config, rageQuitSupport); + } - if (totalSupport < firstSealThreshold) { - return 0; - } + function _isVetoSignallingReactivationDurationPassed( + DualGovernanceState storage self, + DualGovernanceConfig memory config + ) private view returns (bool) { + return block.timestamp - self.vetoSignallingReactivationTime > config.vetoSignallingMinActiveDuration; + } - if (totalSupport >= secondSealThreshold) { - return maxDuration; - } + function _isVetoSignallingDeactivationMaxDurationPassed( + DualGovernanceState storage self, + DualGovernanceConfig memory config + ) private view returns (bool) { + return block.timestamp - self.enteredAt > config.vetoSignallingDeactivationMaxDuration; + } - duration_ = minDuration - + (totalSupport - firstSealThreshold) * (maxDuration - minDuration) / (secondSealThreshold - firstSealThreshold); + function _isVetoCooldownDurationPassed( + DualGovernanceState storage self, + DualGovernanceConfig memory config + ) private view returns (bool) { + return block.timestamp - self.enteredAt > config.vetoCooldownDuration; } - function _deployNewSignallingEscrow(Store storage self, address escrowMasterCopy) private { + function _deployNewSignallingEscrow(DualGovernanceState storage self, address escrowMasterCopy) private { IEscrow clone = IEscrow(Clones.clone(escrowMasterCopy)); clone.initialize(address(this)); self.signallingEscrow = clone; @@ -341,10 +275,95 @@ library DualGovernanceState { } function _calcRageQuitWithdrawalsTimelock( - IConfiguration config, + DualGovernanceConfig memory config, uint256 rageQuitRound - ) private view returns (uint256) { + ) private pure returns (uint256) { // TODO: implement proper function - return config.RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK() * config.RAGE_QUIT_EXTENSION_DELAY() * rageQuitRound; + return config.rageQuitEthClaimMinTimelock + config.rageQuitExtensionDelay * rageQuitRound; + } +} + +library DualGovernanceStateViews { + error NotTie(); + error ProposalsCreationSuspended(); + error ProposalsAdoptionSuspended(); + + function checkProposalsCreationAllowed(DualGovernanceState storage self) internal view { + if (!isProposalsCreationAllowed(self)) { + revert ProposalsCreationSuspended(); + } + } + + function checkProposalsAdoptionAllowed(DualGovernanceState storage self) internal view { + if (!isProposalsAdoptionAllowed(self)) { + revert ProposalsAdoptionSuspended(); + } + } + + function checkTiebreak(DualGovernanceState storage self, IConfiguration config) internal view { + if (!isTiebreak(self, config)) { + revert NotTie(); + } + } + + function currentState(DualGovernanceState storage self) internal view returns (State) { + return self.state; + } + + function isProposalsCreationAllowed(DualGovernanceState storage self) internal view returns (bool) { + State state = self.state; + return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; + } + + function isProposalsAdoptionAllowed(DualGovernanceState storage self) internal view returns (bool) { + State state = self.state; + return state == State.Normal || state == State.VetoCooldown; + } + + function isTiebreak(DualGovernanceState storage self, IConfiguration config) internal view returns (bool) { + if (isProposalsAdoptionAllowed(self)) return false; + + // for the governance is locked for long period of time + if (block.timestamp - self.lastAdoptableStateExitedAt >= config.TIE_BREAK_ACTIVATION_TIMEOUT()) return true; + + if (self.state != State.RageQuit) return false; + + address[] memory sealableWithdrawalBlockers = config.sealableWithdrawalBlockers(); + for (uint256 i = 0; i < sealableWithdrawalBlockers.length; ++i) { + if (IPausableUntil(sealableWithdrawalBlockers[i]).isPaused()) return true; + } + return false; + } + + function getVetoSignallingState( + DualGovernanceState storage self, + DualGovernanceConfig memory config + ) internal view returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) { + isActive = self.state == State.VetoSignalling; + duration = isActive ? getVetoSignallingDuration(self, config) : 0; + enteredAt = isActive ? self.enteredAt : 0; + activatedAt = isActive ? self.vetoSignallingActivationTime : 0; + } + + function getVetoSignallingDuration( + DualGovernanceState storage self, + DualGovernanceConfig memory config + ) internal view returns (uint256) { + uint256 totalSupport = self.signallingEscrow.getRageQuitSupport(); + return dynamicTimelockDuration(config, totalSupport); + } + + struct VetoSignallingDeactivationState { + uint256 duration; + uint256 enteredAt; + } + + function getVetoSignallingDeactivationState( + DualGovernanceState storage self, + DualGovernanceConfig memory config + ) internal view returns (bool isActive, uint256 duration, uint256 enteredAt) { + isActive = self.state == State.VetoSignallingDeactivation; + duration = config.vetoSignallingDeactivationMaxDuration; + enteredAt = isActive ? self.enteredAt : 0; } } diff --git a/test/scenario/gate-seal-breaker.t.sol b/test/scenario/gate-seal-breaker.t.sol index f54d2937..4f0dea97 100644 --- a/test/scenario/gate-seal-breaker.t.sol +++ b/test/scenario/gate-seal-breaker.t.sol @@ -70,7 +70,7 @@ contract SealBreakerScenarioTest is ScenarioTestBlueprint { _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_DURATION() + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); _activateNextState(); _assertVetoCooldownState(); @@ -120,7 +120,7 @@ contract SealBreakerScenarioTest is ScenarioTestBlueprint { _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_dualGovernance.CONFIG().VETO_SIGNALLING_DEACTIVATION_DURATION() + 1); + _wait(_dualGovernance.CONFIG().VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); _activateNextState(); _assertVetoCooldownState(); diff --git a/test/scenario/gov-state-transitions.t.sol b/test/scenario/gov-state-transitions.t.sol index e6c4f06d..1978a94b 100644 --- a/test/scenario/gov-state-transitions.t.sol +++ b/test/scenario/gov-state-transitions.t.sol @@ -9,12 +9,16 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function setUp() external { _selectFork(); _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); + _depositStETH(_VETOER, 1 ether); } function test_signalling_state_min_duration() public { _assertNormalState(); - _lockStETH(_VETOER, percents("3.00")); + _lockStETH(_VETOER, percents(_config.FIRST_SEAL_RAGE_QUIT_SUPPORT())); + _assertNormalState(); + + _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() / 2); @@ -31,7 +35,12 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_state_max_duration() public { _assertNormalState(); - _lockStETH(_VETOER, percents("15.0")); + _lockStETH(_VETOER, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + + _assertVetoSignalingState(); + + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() / 2); + _activateNextState(); _assertVetoSignalingState(); @@ -40,7 +49,9 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() / 2 + 1); + _lockStETH(_VETOER, 1 gwei); + + _wait(1 seconds); _activateNextState(); _assertRageQuitState(); @@ -49,16 +60,19 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_to_normal() public { _assertNormalState(); - _lockStETH(_VETOER, percents("3.00")); + _lockStETH(_VETOER, percents(_config.FIRST_SEAL_RAGE_QUIT_SUPPORT())); + _assertNormalState(); + + _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION()); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() + 1); _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_DURATION()); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); _activateNextState(); _assertVetoCooldownState(); @@ -67,7 +81,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _getSignallingEscrow().unlockStETH(); vm.stopPrank(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_DURATION()); + _wait(_config.VETO_COOLDOWN_DURATION() + 1); _activateNextState(); _assertNormalState(); @@ -76,21 +90,23 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_non_stop() public { _assertNormalState(); - _lockStETH(_VETOER, percents("3.00")); + _lockStETH(_VETOER, percents(_config.FIRST_SEAL_RAGE_QUIT_SUPPORT())); + _assertNormalState(); + _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION()); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() + 1); _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_DURATION()); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); _activateNextState(); _assertVetoCooldownState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_DURATION()); + _wait(_config.VETO_COOLDOWN_DURATION() + 1); _activateNextState(); _assertVetoSignalingState(); @@ -99,12 +115,19 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_to_rage_quit() public { _assertNormalState(); - _lockStETH(_VETOER, percents("15.00")); + _lockStETH(_VETOER, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT())); _assertVetoSignalingState(); _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION()); _activateNextState(); + _assertVetoSignalingState(); + + _lockStETH(_VETOER, 1 gwei); + _assertVetoSignalingState(); + + _wait(1 seconds); + _activateNextState(); _assertRageQuitState(); } } diff --git a/test/scenario/last-moment-malicious-proposal.t.sol b/test/scenario/last-moment-malicious-proposal.t.sol index 090e6aee..ddb91985 100644 --- a/test/scenario/last-moment-malicious-proposal.t.sol +++ b/test/scenario/last-moment-malicious-proposal.t.sol @@ -44,7 +44,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _logVetoSignallingState(); // almost all veto signalling period has passed - vm.warp(block.timestamp + 20 days); + _wait(20 days); _activateNextState(); _assertVetoSignalingState(); _logVetoSignallingState(); @@ -72,8 +72,8 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { // ACT 4. MALICIOUS ACTOR UNLOCK FUNDS FROM ESCROW // --- { + _wait(12 seconds); _unlockStETH(maliciousActor); - _logVetoSignallingDeactivationState(); _assertVetoSignalingDeactivationState(); } @@ -82,8 +82,9 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { // --- address stEthWhale = makeAddr("STETH_WHALE"); { - _wait(_config.VETO_SIGNALLING_DEACTIVATION_DURATION() / 2); - _lockStETH(stEthWhale, percents("10.0")); + _depositStETH(stEthWhale, 1 ether); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION()); + _lockStETH(stEthWhale, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT())); _logVetoSignallingDeactivationState(); _assertVetoSignalingState(); _logVetoSignallingState(); @@ -93,15 +94,10 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { // ACT 6. STETH HOLDER MAY EXIT TO RAGE QUIT WHEN THE SECOND SEAL THRESHOLD REACHED // --- { - _wait(_config.VETO_SIGNALLING_DEACTIVATION_DURATION() / 2 + 1); - - _activateNextState(); + _lockStETH(stEthWhale, 1 gwei); _assertVetoSignalingState(); - // stEth holders reach the rage quit threshold - _lockStETH(stEthWhale, percents("10.0")); - - _wait(_config.VETO_SIGNALLING_DEACTIVATION_DURATION()); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); _activateNextState(); // the dual governance immediately transfers to the Rage Quit state @@ -150,10 +146,10 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { } // --- - // ACT 3. THE VETO SIGNALLING DEACTIVATION DURATION EQUALS TO "VETO_SIGNALLING_DEACTIVATION_DURATION" DAYS + // ACT 3. THE VETO SIGNALLING DEACTIVATION DURATION EQUALS TO "VETO_SIGNALLING_DEACTIVATION_MAX_DURATION" DAYS // --- { - vm.warp(block.timestamp + _config.VETO_SIGNALLING_DEACTIVATION_DURATION() + 1); + vm.warp(block.timestamp + _config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); _activateNextState(); _assertVetoCooldownState(); diff --git a/test/utils/percents.sol b/test/utils/percents.sol index 0b5da1b0..9862be56 100644 --- a/test/utils/percents.sol +++ b/test/utils/percents.sol @@ -10,6 +10,11 @@ struct Percents { uint256 constant PRECISION = 16; +function percents(uint256 value) pure returns (Percents memory result) { + result.value = value; + result.precision = PRECISION; +} + function percents(string memory value) pure returns (Percents memory result) { result = percents(value, PRECISION); } diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 754fc277..59ec292f 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -21,7 +21,7 @@ import { } from "contracts/EmergencyProtectedTimelock.sol"; import {SingleGovernance, IGovernance} from "contracts/SingleGovernance.sol"; -import {DualGovernance, GovernanceState} from "contracts/DualGovernance.sol"; +import {DualGovernance, DualGovernanceState, State} from "contracts/DualGovernance.sol"; import {Proposal, Status as ProposalStatus} from "contracts/libraries/Proposals.sol"; @@ -117,14 +117,24 @@ contract ScenarioTestBlueprint is Test { // Balances Manipulation // --- - function _setupStETHWhale(address vetoer) internal { + function _depositStETH( + address account, + uint256 amountToMint + ) internal returns (uint256 sharesMinted, uint256 amountMinted) { + return Utils.depositStETH(account, amountToMint); + } + + function _setupStETHWhale(address vetoer) internal returns (uint256 shares, uint256 amount) { Utils.removeLidoStakingLimit(); - Utils.setupStETHWhale(vetoer, percents("10.0").value); + return Utils.setupStETHWhale(vetoer, percents("10.0")); } - function _setupStETHWhale(address vetoer, Percents memory vetoPowerInPercents) internal { + function _setupStETHWhale( + address vetoer, + Percents memory vetoPowerInPercents + ) internal returns (uint256 shares, uint256 amount) { Utils.removeLidoStakingLimit(); - Utils.setupStETHWhale(vetoer, vetoPowerInPercents.value); + return Utils.setupStETHWhale(vetoer, vetoPowerInPercents); } function _getBalances(address vetoer) internal view returns (Balances memory balances) { @@ -142,9 +152,8 @@ contract ScenarioTestBlueprint is Test { // Escrow Manipulation // --- function _lockStETH(address vetoer, Percents memory vetoPowerInPercents) internal { - Utils.removeLidoStakingLimit(); - Utils.setupStETHWhale(vetoer, vetoPowerInPercents.value); - _lockStETH(vetoer, IERC20(ST_ETH).balanceOf(vetoer)); + (, uint256 amount) = _setupStETHWhale(vetoer, vetoPowerInPercents); + _lockStETH(vetoer, amount); } function _lockStETH(address vetoer, uint256 amount) internal { @@ -369,23 +378,23 @@ contract ScenarioTestBlueprint is Test { } function _assertNormalState() internal { - assertEq(uint256(_dualGovernance.currentState()), uint256(GovernanceState.Normal)); + assertEq(uint256(_dualGovernance.currentState()), uint256(State.Normal)); } function _assertVetoSignalingState() internal { - assertEq(uint256(_dualGovernance.currentState()), uint256(GovernanceState.VetoSignalling)); + assertEq(uint256(_dualGovernance.currentState()), uint256(State.VetoSignalling)); } function _assertVetoSignalingDeactivationState() internal { - assertEq(uint256(_dualGovernance.currentState()), uint256(GovernanceState.VetoSignallingDeactivation)); + assertEq(uint256(_dualGovernance.currentState()), uint256(State.VetoSignallingDeactivation)); } function _assertRageQuitState() internal { - assertEq(uint256(_dualGovernance.currentState()), uint256(GovernanceState.RageQuit)); + assertEq(uint256(_dualGovernance.currentState()), uint256(State.RageQuit)); } function _assertVetoCooldownState() internal { - assertEq(uint256(_dualGovernance.currentState()), uint256(GovernanceState.VetoCooldown)); + assertEq(uint256(_dualGovernance.currentState()), uint256(State.VetoCooldown)); } function _assertNoTargetMockCalls() internal { @@ -589,7 +598,7 @@ contract ScenarioTestBlueprint is Test { assertEq(uint256(a), uint256(b), message); } - function assertEq(GovernanceState a, GovernanceState b) internal { + function assertEq(State a, State b) internal { assertEq(uint256(a), uint256(b)); } diff --git a/test/utils/utils.sol b/test/utils/utils.sol index 15c23c60..9072e0ce 100644 --- a/test/utils/utils.sol +++ b/test/utils/utils.sol @@ -7,6 +7,8 @@ import "forge-std/Test.sol"; import {stdStorage, StdStorage} from "forge-std/Test.sol"; +import {Percents, percents} from "../utils/percents.sol"; + import "./mainnet-addresses.sol"; import "./interfaces.sol"; @@ -87,24 +89,42 @@ library Utils { vm.warp(block.timestamp + 15); } - function setupStETHWhale(address addr) internal { + function setupStETHWhale(address addr) internal returns (uint256 shares, uint256 balance) { // 15% of total stETH supply - setupStETHWhale(addr, 30 * 10 ** 16); + return setupStETHWhale(addr, percents("30.00")); } - function setupStETHWhale(address addr, uint256 totalSupplyPercentage) internal { - uint256 ST_ETH_TRANSFERS_SHARE_LOST_COMPENSATION = 8; // TODO: evaluate min enough value + function setupStETHWhale( + address addr, + Percents memory totalSupplyPercentage + ) internal returns (uint256 shares, uint256 balance) { + uint256 ST_ETH_TRANSFERS_SHARE_LOSS_COMPENSATION = 8; // TODO: evaluate min enough value // bal / (totalSupply + bal) = percentage => bal = totalSupply * percentage / (1 - percentage) - uint256 shares = IStEth(ST_ETH).getTotalShares() * totalSupplyPercentage / (10 ** 18 - totalSupplyPercentage); + shares = ST_ETH_TRANSFERS_SHARE_LOSS_COMPENSATION + + IStEth(ST_ETH).getTotalShares() * totalSupplyPercentage.value + / (100 * 10 ** totalSupplyPercentage.precision - totalSupplyPercentage.value); // to compensate StETH wei lost on submit/transfers, generate slightly larger eth amount - uint256 ethBalance = IStEth(ST_ETH).getPooledEthByShares(shares + ST_ETH_TRANSFERS_SHARE_LOST_COMPENSATION); + return depositStETH(addr, IStEth(ST_ETH).getPooledEthByShares(shares)); + } + + function depositStETH( + address addr, + uint256 amountToMint + ) internal returns (uint256 sharesMinted, uint256 amountMinted) { + uint256 sharesBalanceBefore = IStEth(ST_ETH).sharesOf(addr); + uint256 amountBalanceBefore = IStEth(ST_ETH).balanceOf(addr); + // solhint-disable-next-line - console.log("setting ETH balance of address %x to %d ETH", addr, ethBalance / 10 ** 18); - vm.deal(addr, ethBalance); + console.log("setting ETH balance of address %x to %d ETH", addr, amountToMint / 10 ** 18); + vm.deal(addr, amountToMint); vm.prank(addr); - IStEth(ST_ETH).submit{value: ethBalance}(address(0)); + IStEth(ST_ETH).submit{value: amountToMint}(address(0)); + + sharesMinted = IStEth(ST_ETH).sharesOf(addr) - sharesBalanceBefore; + amountMinted = IStEth(ST_ETH).balanceOf(addr) - amountBalanceBefore; + // solhint-disable-next-line - console.log("stETH balance of address %x: %d stETH", addr, IERC20(ST_ETH).balanceOf(addr) / 10 ** 18); + console.log("stETH balance of address %x: %d stETH", addr, (amountMinted) / 10 ** 18); } function removeLidoStakingLimit() external { From bc776c24022e2835366a76e50e63a045c430d628 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Sun, 21 Apr 2024 23:55:53 +0400 Subject: [PATCH 032/134] Add methods to request withdrawals for locked stETH/wstETH --- contracts/Escrow.sol | 30 +++++++++- contracts/interfaces/IWithdrawalQueue.sol | 7 ++- contracts/libraries/AssetsAccounting.sol | 61 +++++++++++++++------ contracts/utils/arrays.sol | 7 +++ test/scenario/escrow.t.sol | 67 +++++++++++++++++++++++ 5 files changed, 151 insertions(+), 21 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 94c14d84..387ad41c 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -13,6 +13,8 @@ import {IWithdrawalQueue, WithdrawalRequestStatus} from "./interfaces/IWithdrawa import {AssetsAccounting, LockedAssetsStats, LockedAssetsTotals} from "./libraries/AssetsAccounting.sol"; +import {ArrayUtils} from "./utils/arrays.sol"; + interface IDualGovernance { function activateNextState() external; } @@ -77,6 +79,9 @@ contract Escrow is IEscrow { _escrowState = EscrowState.SignallingEscrow; _dualGovernance = IDualGovernance(dualGovernance); + + ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); + WST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); } // --- @@ -91,11 +96,24 @@ contract Escrow is IEscrow { } function unlockStETH() external { - uint256 sharesUnlocked = _accounting.accountStETHUnlock(CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME(), msg.sender); + _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); + uint256 sharesUnlocked = _accounting.accountStETHUnlock(msg.sender); ST_ETH.transferShares(msg.sender, sharesUnlocked); _activateNextGovernanceState(); } + function requestWithdrawalsStETH(uint256[] calldata amounts) external returns (uint256[] memory unstETHIds) { + unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(amounts, address(this)); + WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + + uint256 sharesTotal = 0; + for (uint256 i = 0; i < statuses.length; ++i) { + sharesTotal += statuses[i].amountOfShares; + } + _accounting.accountStETHUnlock(msg.sender, sharesTotal); + _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); + } + // --- // Lock / Unlock wstETH // --- @@ -107,11 +125,19 @@ contract Escrow is IEscrow { } function unlockWstETH() external returns (uint256 wstETHUnlocked) { - wstETHUnlocked = _accounting.accountWstETHUnlock(CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME(), msg.sender); + _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); + wstETHUnlocked = _accounting.accountWstETHUnlock(msg.sender); WST_ETH.transfer(msg.sender, wstETHUnlocked); _activateNextGovernanceState(); } + function requestWithdrawalsWstETH(uint256[] calldata amounts) external returns (uint256[] memory unstETHIds) { + uint256 totalAmount = ArrayUtils.sum(amounts); + _accounting.accountWstETHUnlock(msg.sender, totalAmount); + unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawalsWstETH(amounts, address(this)); + _accounting.accountUnstETHLock(msg.sender, unstETHIds, WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds)); + } + // --- // Lock / Unlock unstETH // --- diff --git a/contracts/interfaces/IWithdrawalQueue.sol b/contracts/interfaces/IWithdrawalQueue.sol index 2b79d759..e3beea17 100644 --- a/contracts/interfaces/IWithdrawalQueue.sol +++ b/contracts/interfaces/IWithdrawalQueue.sol @@ -14,8 +14,6 @@ interface IWithdrawalQueue { function MIN_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256); function MAX_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256); - function requestWithdrawalsWstETH(uint256[] calldata amounts, address owner) external returns (uint256[] memory); - function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external; function getLastFinalizedRequestId() external view returns (uint256); @@ -45,4 +43,9 @@ interface IWithdrawalQueue { uint256[] calldata _amounts, address _owner ) external returns (uint256[] memory requestIds); + + function requestWithdrawalsWstETH( + uint256[] calldata _amounts, + address _owner + ) external returns (uint256[] memory requestIds); } diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index d067da0a..afee69ec 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -83,6 +83,7 @@ library AssetsAccounting { error InvalidWithdrawalBatchesOffset(uint256 actual, uint256 expected); error InvalidWithdrawalBatchesCount(uint256 actual, uint256 expected); error AssetsUnlockDelayNotPassed(uint256 unlockTimelockExpiresAt); + error NotEnoughStETHToUnlock(uint256 requested, uint256 sharesBalance); struct State { LockedAssetsTotals totals; @@ -107,16 +108,19 @@ library AssetsAccounting { emit StETHLocked(vetoer, shares); } + function accountStETHUnlock(State storage self, address vetoer) internal returns (uint128 sharesUnlocked) { + sharesUnlocked = accountStETHUnlock(self, vetoer, self.assets[vetoer].stETHShares); + } + function accountStETHUnlock( State storage self, - uint256 assetsUnlockDelay, - address vetoer + address vetoer, + uint256 shares ) internal returns (uint128 sharesUnlocked) { - sharesUnlocked = self.assets[vetoer].stETHShares; - _checkNonZeroSharesUnlock(vetoer, sharesUnlocked); - _checkAssetsUnlockDelayPassed(self, assetsUnlockDelay, vetoer); - self.assets[vetoer].stETHShares = 0; + _checkStETHSharesUnlock(self, vetoer, shares); + sharesUnlocked = shares.toUint128(); self.totals.shares -= sharesUnlocked; + self.assets[vetoer].stETHShares -= sharesUnlocked; emit StETHUnlocked(vetoer, sharesUnlocked); } @@ -132,6 +136,10 @@ library AssetsAccounting { // wstETH Operations Accounting // --- + function checkAssetsUnlockDelayPassed(State storage self, address vetoer, uint256 delay) internal view { + _checkAssetsUnlockDelayPassed(self, delay, vetoer); + } + function accountWstETHLock(State storage self, address vetoer, uint256 shares) internal { _checkNonZeroSharesLock(vetoer, shares); uint128 sharesUint128 = shares.toUint128(); @@ -141,16 +149,19 @@ library AssetsAccounting { emit WstETHLocked(vetoer, shares); } + function accountWstETHUnlock(State storage self, address vetoer) internal returns (uint128 sharesUnlocked) { + sharesUnlocked = accountWstETHUnlock(self, vetoer, self.assets[vetoer].wstETHShares); + } + function accountWstETHUnlock( State storage self, - uint256 assetsUnlockDelay, - address vetoer + address vetoer, + uint256 shares ) internal returns (uint128 sharesUnlocked) { - sharesUnlocked = self.assets[vetoer].wstETHShares; - _checkNonZeroSharesUnlock(vetoer, sharesUnlocked); - _checkAssetsUnlockDelayPassed(self, assetsUnlockDelay, vetoer); + _checkNonZeroSharesUnlock(vetoer, shares); + sharesUnlocked = shares.toUint128(); self.totals.shares -= sharesUnlocked; - self.assets[vetoer].wstETHShares = 0; + self.assets[vetoer].wstETHShares -= sharesUnlocked; emit WstETHUnlocked(vetoer, sharesUnlocked); } @@ -202,6 +213,7 @@ library AssetsAccounting { for (uint256 i = 0; i < unstETHIdsCount; ++i) { (uint256 sharesUnlocked, uint256 finalizedSharesUnlocked, uint256 finalizedAmountUnlocked) = _removeWithdrawalRequest(self, vetoer, unstETHIds[i]); + totalUnstETHSharesUnlocked += sharesUnlocked; totalFinalizedSharesUnlocked += finalizedSharesUnlocked; totalFinalizedAmountUnlocked += finalizedAmountUnlocked; @@ -216,8 +228,8 @@ library AssetsAccounting { self.assets[vetoer].amountFinalized -= totalFinalizedAmountUnlockedUint128; self.totals.shares -= totalUnstETHSharesUnlockedUint128; - self.totals.amountFinalized -= totalFinalizedSharesUnlockedUint128; - self.totals.sharesFinalized -= totalFinalizedAmountUnlockedUint128; + self.totals.sharesFinalized -= totalFinalizedSharesUnlockedUint128; + self.totals.amountFinalized -= totalFinalizedAmountUnlockedUint128; emit UnstETHUnlocked( vetoer, unstETHIds, totalUnstETHSharesUnlocked, totalFinalizedSharesUnlocked, totalFinalizedAmountUnlocked @@ -236,8 +248,12 @@ library AssetsAccounting { uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { - (uint256 sharesFinalized, uint256 amountFinalized) = + (address owner, uint256 sharesFinalized, uint256 amountFinalized) = _finalizeWithdrawalRequest(self, unstETHIds[i], claimableAmounts[i]); + + self.assets[owner].sharesFinalized += sharesFinalized.toUint128(); + self.assets[owner].amountFinalized += amountFinalized.toUint128(); + totalSharesFinalized += sharesFinalized; totalAmountFinalized += amountFinalized; } @@ -427,11 +443,12 @@ library AssetsAccounting { State storage self, uint256 unstETHId, uint256 claimableAmount - ) private returns (uint256 sharesFinalized, uint256 amountFinalized) { + ) private returns (address owner, uint256 sharesFinalized, uint256 amountFinalized) { WithdrawalRequest storage request = self.requests[unstETHId]; if (claimableAmount == 0 || request.state != WithdrawalRequestState.Locked) { - return (0, 0); + return (request.owner, 0, 0); } + owner = request.owner; request.state = WithdrawalRequestState.Finalized; request.claimableAmount = claimableAmount.toUint96(); @@ -516,6 +533,16 @@ library AssetsAccounting { } } + function _checkStETHSharesUnlock(State storage self, address vetoer, uint256 shares) private view { + if (shares == 0) { + revert InvalidSharesUnlock(vetoer, 0); + } + + if (self.assets[vetoer].stETHShares < shares) { + revert NotEnoughStETHToUnlock(shares, self.assets[vetoer].stETHShares); + } + } + function _checkNonZeroSharesWithdraw(address vetoer, uint256 shares) private pure { if (shares == 0) { revert InvalidSharesWithdraw(vetoer, 0); diff --git a/contracts/utils/arrays.sol b/contracts/utils/arrays.sol index c64ce2f3..960189bb 100644 --- a/contracts/utils/arrays.sol +++ b/contracts/utils/arrays.sol @@ -2,6 +2,13 @@ pragma solidity 0.8.23; library ArrayUtils { + function sum(uint256[] calldata values) internal pure returns (uint256 res) { + uint256 valuesCount = values.length; + for (uint256 i = 0; i < valuesCount; ++i) { + res += values[i]; + } + } + function seed(uint256 length, uint256 value) internal pure returns (uint256[] memory res) { res = new uint256[](length); for (uint256 i = 0; i < length; ++i) { diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index cf66dbed..d17f7928 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -428,6 +428,73 @@ contract EscrowHappyPath is TestHelpers { vm.stopPrank(); } + function test_request_st_eth_wst_eth_withdrawals() external { + uint256 firstVetoerStETHAmount = 10 ether; + uint256 firstVetoerWstETHAmount = 11 ether; + + uint256 firstVetoerStETHShares = _ST_ETH.getSharesByPooledEth(firstVetoerStETHAmount); + uint256 totalSharesLocked = firstVetoerWstETHAmount + firstVetoerStETHShares; + + _lockStETH(_VETOER_1, firstVetoerStETHAmount); + assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).stETHShares, firstVetoerStETHShares, 1); + assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, firstVetoerStETHShares, 1); + + _lockWstETH(_VETOER_1, firstVetoerWstETHAmount); + assertEq(escrow.getVetoerState(_VETOER_1).wstETHShares, firstVetoerWstETHAmount); + assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); + + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); + + uint256[] memory stETHWithdrawalRequestAmounts = new uint256[](1); + stETHWithdrawalRequestAmounts[0] = firstVetoerStETHAmount; + + vm.prank(_VETOER_1); + uint256[] memory stETHWithdrawalRequestIds = escrow.requestWithdrawalsStETH(stETHWithdrawalRequestAmounts); + + assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, firstVetoerStETHShares, 1); + assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); + + uint256[] memory wstETHWithdrawalRequestAmounts = new uint256[](1); + wstETHWithdrawalRequestAmounts[0] = firstVetoerWstETHAmount; + + vm.prank(_VETOER_1); + uint256[] memory wstETHWithdrawalRequestIds = escrow.requestWithdrawalsWstETH(wstETHWithdrawalRequestAmounts); + + assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 1); + assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); + + finalizeWQ(wstETHWithdrawalRequestIds[0]); + + escrow.markUnstETHFinalized( + stETHWithdrawalRequestIds, + _WITHDRAWAL_QUEUE.findCheckpointHints( + stETHWithdrawalRequestIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex() + ) + ); + assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 1); + assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); + + escrow.markUnstETHFinalized( + wstETHWithdrawalRequestIds, + _WITHDRAWAL_QUEUE.findCheckpointHints( + wstETHWithdrawalRequestIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex() + ) + ); + assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 1); + assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); + + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); + + vm.prank(_VETOER_1); + escrow.unlockUnstETH(stETHWithdrawalRequestIds); + + assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, firstVetoerWstETHAmount, 1); + assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, firstVetoerWstETHAmount, 1); + + vm.prank(_VETOER_1); + escrow.unlockUnstETH(wstETHWithdrawalRequestIds); + } + function externalLockUnstETH(address vetoer, uint256[] memory unstETHIds) external { _lockUnstETH(vetoer, unstETHIds); } From 0933e8bb6d6174aae61dd38a50fb6d352b14699c Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 22 Apr 2024 00:57:59 +0400 Subject: [PATCH 033/134] DualGovernance methods renaming (cancelAllPending, vetoSignallingEscrow) --- contracts/DualGovernance.sol | 6 +++--- contracts/EmergencyProtectedTimelock.sol | 2 +- contracts/SingleGovernance.sol | 4 ++-- contracts/interfaces/ITimelock.sol | 4 ++-- test/scenario/escrow.t.sol | 2 +- test/scenario/gov-state-transitions.t.sol | 2 +- test/utils/scenario-test-blueprint.sol | 16 ++++++++-------- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 7a41324c..de3722e9 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -62,12 +62,12 @@ contract DualGovernance is IGovernance, ConfigurationProvider { emit ProposalScheduled(proposalId); } - function cancelAll() external { + function cancelAllPendingProposals() external { _proposers.checkAdminProposer(CONFIG, msg.sender); - TIMELOCK.cancelAll(); + TIMELOCK.cancelAllNonExecutedProposals(); } - function signallingEscrow() external view returns (address) { + function vetoSignallingEscrow() external view returns (address) { return address(_dgState.signallingEscrow); } diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index e44cb2d2..af90cc7f 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -43,7 +43,7 @@ contract EmergencyProtectedTimelock is ConfigurationProvider { _proposals.execute(proposalId, CONFIG.AFTER_SCHEDULE_DELAY()); } - function cancelAll() external { + function cancelAllNonExecutedProposals() external { _checkGovernance(msg.sender); _proposals.cancelAll(); } diff --git a/contracts/SingleGovernance.sol b/contracts/SingleGovernance.sol index 35a347f5..5f01f4bc 100644 --- a/contracts/SingleGovernance.sol +++ b/contracts/SingleGovernance.sol @@ -30,9 +30,9 @@ contract SingleGovernance is IGovernance, ConfigurationProvider { return TIMELOCK.canSchedule(proposalId); } - function cancelAll() external { + function cancelAllPendingProposals() external { _checkGovernance(msg.sender); - TIMELOCK.cancelAll(); + TIMELOCK.cancelAllNonExecutedProposals(); } function _checkGovernance(address account) internal view { diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index 5ed72a0c..380de7fe 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -6,7 +6,7 @@ import {ExecutorCall} from "./IExecutor.sol"; interface IGovernance { function submit(ExecutorCall[] calldata calls) external returns (uint256 proposalId); function schedule(uint256 proposalId) external; - function cancelAll() external; + function cancelAllPendingProposals() external; function canSchedule(uint256 proposalId) external view returns (bool); } @@ -15,7 +15,7 @@ interface ITimelock { function submit(address executor, ExecutorCall[] calldata calls) external returns (uint256 newProposalId); function schedule(uint256 proposalId) external; function execute(uint256 proposalId) external; - function cancelAll() external; + function cancelAllNonExecutedProposals() external; function canSchedule(uint256 proposalId) external view returns (bool); function canExecute(uint256 proposalId) external view returns (bool); diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index d17f7928..bf374c72 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -60,7 +60,7 @@ contract EscrowHappyPath is TestHelpers { _selectFork(); _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); - escrow = _getSignallingEscrow(); + escrow = _getVetoSignallingEscrow(); _setupStETHWhale(_VETOER_1); vm.startPrank(_VETOER_1); diff --git a/test/scenario/gov-state-transitions.t.sol b/test/scenario/gov-state-transitions.t.sol index 1978a94b..90b35b4e 100644 --- a/test/scenario/gov-state-transitions.t.sol +++ b/test/scenario/gov-state-transitions.t.sol @@ -78,7 +78,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _assertVetoCooldownState(); vm.startPrank(_VETOER); - _getSignallingEscrow().unlockStETH(); + _getVetoSignallingEscrow().unlockStETH(); vm.stopPrank(); _wait(_config.VETO_COOLDOWN_DURATION() + 1); diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 59ec292f..3b7885bf 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -90,8 +90,8 @@ contract ScenarioTestBlueprint is Test { // --- // Helper Getters // --- - function _getSignallingEscrow() internal view returns (Escrow) { - return Escrow(payable(_dualGovernance.signallingEscrow())); + function _getVetoSignallingEscrow() internal view returns (Escrow) { + return Escrow(payable(_dualGovernance.vetoSignallingEscrow())); } function _getTargetRegularStaffCalls() internal view returns (ExecutorCall[] memory) { @@ -157,7 +157,7 @@ contract ScenarioTestBlueprint is Test { } function _lockStETH(address vetoer, uint256 amount) internal { - Escrow escrow = _getSignallingEscrow(); + Escrow escrow = _getVetoSignallingEscrow(); vm.startPrank(vetoer); if (_ST_ETH.allowance(vetoer, address(escrow)) < amount) { _ST_ETH.approve(address(escrow), amount); @@ -168,12 +168,12 @@ contract ScenarioTestBlueprint is Test { function _unlockStETH(address vetoer) internal { vm.startPrank(vetoer); - _getSignallingEscrow().unlockStETH(); + _getVetoSignallingEscrow().unlockStETH(); vm.stopPrank(); } function _lockWstETH(address vetoer, uint256 amount) internal { - Escrow escrow = _getSignallingEscrow(); + Escrow escrow = _getVetoSignallingEscrow(); vm.startPrank(vetoer); if (_WST_ETH.allowance(vetoer, address(escrow)) < amount) { _WST_ETH.approve(address(escrow), amount); @@ -183,7 +183,7 @@ contract ScenarioTestBlueprint is Test { } function _unlockWstETH(address vetoer) internal { - Escrow escrow = _getSignallingEscrow(); + Escrow escrow = _getVetoSignallingEscrow(); uint256 wstETHBalanceBefore = _WST_ETH.balanceOf(vetoer); uint256 vetoerWstETHSharesBefore = escrow.getVetoerState(vetoer).wstETHShares; @@ -196,7 +196,7 @@ contract ScenarioTestBlueprint is Test { } function _lockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { - Escrow escrow = _getSignallingEscrow(); + Escrow escrow = _getVetoSignallingEscrow(); uint256 vetoerUnstETHSharesBefore = escrow.getVetoerState(vetoer).unstETHShares; uint256 totalSharesBefore = escrow.getLockedAssetsTotals().shares; @@ -221,7 +221,7 @@ contract ScenarioTestBlueprint is Test { } function _unlockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { - Escrow escrow = _getSignallingEscrow(); + Escrow escrow = _getVetoSignallingEscrow(); uint256 vetoerUnstETHSharesBefore = escrow.getVetoerState(vetoer).unstETHShares; uint256 totalSharesBefore = escrow.getLockedAssetsTotals().shares; From f7c882eb6d24973e3020f17299d55239701ffb72 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 22 Apr 2024 02:37:02 +0400 Subject: [PATCH 034/134] Add quadratic rage quit extension function --- contracts/Configuration.sol | 13 +++++++++++-- contracts/interfaces/IConfiguration.sol | 8 +++++++- contracts/libraries/DualGovernanceState.sol | 13 ++++++++++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/contracts/Configuration.sol b/contracts/Configuration.sol index e957a29c..44cfc9bd 100644 --- a/contracts/Configuration.sol +++ b/contracts/Configuration.sol @@ -23,9 +23,13 @@ contract Configuration is IConfiguration { uint256 public immutable VETO_COOLDOWN_DURATION = 4 days; - uint256 public immutable RAGE_QUIT_EXTRA_TIMELOCK = 14 days; uint256 public immutable RAGE_QUIT_EXTENSION_DELAY = 7 days; uint256 public immutable RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK = 60 days; + uint256 public immutable RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_START_SEQ_NUMBER = 2; + + uint256 public immutable RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_A = 0; + uint256 public immutable RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_B = 0; + uint256 public immutable RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_C = 0; // --- address public immutable ADMIN_EXECUTOR; @@ -98,8 +102,13 @@ contract Configuration is IConfiguration { config.vetoSignallingMinActiveDuration = VETO_SIGNALLING_MIN_ACTIVE_DURATION; config.vetoSignallingDeactivationMaxDuration = VETO_SIGNALLING_DEACTIVATION_MAX_DURATION; config.vetoCooldownDuration = VETO_COOLDOWN_DURATION; - config.rageQuitExtraTimelock = RAGE_QUIT_EXTRA_TIMELOCK; config.rageQuitExtensionDelay = RAGE_QUIT_EXTENSION_DELAY; config.rageQuitEthClaimMinTimelock = RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK; + config.rageQuitEthClaimTimelockGrowthStartSeqNumber = RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_START_SEQ_NUMBER; + config.rageQuitEthClaimTimelockGrowthCoeffs = [ + RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_A, + RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_B, + RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_C + ]; } } diff --git a/contracts/interfaces/IConfiguration.sol b/contracts/interfaces/IConfiguration.sol index 19de5116..04aa9f14 100644 --- a/contracts/interfaces/IConfiguration.sol +++ b/contracts/interfaces/IConfiguration.sol @@ -13,6 +13,8 @@ struct DualGovernanceConfig { uint256 rageQuitExtraTimelock; uint256 rageQuitExtensionDelay; uint256 rageQuitEthClaimMinTimelock; + uint256 rageQuitEthClaimTimelockGrowthStartSeqNumber; + uint256[3] rageQuitEthClaimTimelockGrowthCoeffs; } interface IAdminExecutorConfiguration { @@ -39,10 +41,14 @@ interface IDualGovernanceConfiguration { function FIRST_SEAL_RAGE_QUIT_SUPPORT() external view returns (uint256); function SECOND_SEAL_RAGE_QUIT_SUPPORT() external view returns (uint256); - function RAGE_QUIT_EXTRA_TIMELOCK() external view returns (uint256); function RAGE_QUIT_EXTENSION_DELAY() external view returns (uint256); function RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK() external view returns (uint256); function RAGE_QUIT_ACCUMULATION_MAX_DURATION() external view returns (uint256); + function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_START_SEQ_NUMBER() external view returns (uint256); + + function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_A() external view returns (uint256); + function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_B() external view returns (uint256); + function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_C() external view returns (uint256); function SIGNALLING_ESCROW_MIN_LOCK_TIME() external view returns (uint256); diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol index c8bc4621..aa93fafe 100644 --- a/contracts/libraries/DualGovernanceState.sol +++ b/contracts/libraries/DualGovernanceState.sol @@ -207,7 +207,7 @@ library DualGovernanceStateTransitions { if (newState == State.RageQuit) { IEscrow signallingEscrow = self.signallingEscrow; signallingEscrow.startRageQuit( - config.rageQuitExtraTimelock, _calcRageQuitWithdrawalsTimelock(config, self.rageQuitRound) + config.rageQuitExtensionDelay, _calcRageQuitWithdrawalsTimelock(config, self.rageQuitRound) ); self.rageQuitEscrow = signallingEscrow; _deployNewSignallingEscrow(self, signallingEscrow.MASTER_COPY()); @@ -278,8 +278,15 @@ library DualGovernanceStateTransitions { DualGovernanceConfig memory config, uint256 rageQuitRound ) private pure returns (uint256) { - // TODO: implement proper function - return config.rageQuitEthClaimMinTimelock + config.rageQuitExtensionDelay * rageQuitRound; + if (rageQuitRound < config.rageQuitEthClaimTimelockGrowthStartSeqNumber) { + return config.rageQuitEthClaimMinTimelock; + } + return config.rageQuitEthClaimMinTimelock + + ( + config.rageQuitEthClaimTimelockGrowthCoeffs[0] * rageQuitRound * rageQuitRound + + config.rageQuitEthClaimTimelockGrowthCoeffs[1] * rageQuitRound + + config.rageQuitEthClaimTimelockGrowthCoeffs[2] + ) / 10 ** 18; // TODO: rewrite in a prettier way } } From bf6112038e4cb4dcf9adbd44f098aec880a13da9 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 22 Apr 2024 02:41:13 +0400 Subject: [PATCH 035/134] OwnableExecutor -> Executor --- contracts/{OwnableExecutor.sol => Executor.sol} | 2 +- test/utils/scenario-test-blueprint.sol | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename contracts/{OwnableExecutor.sol => Executor.sol} (92%) diff --git a/contracts/OwnableExecutor.sol b/contracts/Executor.sol similarity index 92% rename from contracts/OwnableExecutor.sol rename to contracts/Executor.sol index b9c8c8ae..106c0a6b 100644 --- a/contracts/OwnableExecutor.sol +++ b/contracts/Executor.sol @@ -6,7 +6,7 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IExecutor} from "./interfaces/IExecutor.sol"; -contract OwnableExecutor is IExecutor, Ownable { +contract Executor is IExecutor, Ownable { constructor(address owner) Ownable(owner) {} function execute( diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 3b7885bf..8d7196e4 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -11,7 +11,7 @@ import { import {Escrow, VetoerState, LockedAssetsTotals} from "contracts/Escrow.sol"; import {IConfiguration, Configuration} from "contracts/Configuration.sol"; -import {OwnableExecutor} from "contracts/OwnableExecutor.sol"; +import {Executor} from "contracts/Executor.sol"; import { ExecutorCall, @@ -79,7 +79,7 @@ contract ScenarioTestBlueprint is Test { Escrow internal _escrowMasterCopy; - OwnableExecutor internal _adminExecutor; + Executor internal _adminExecutor; EmergencyProtectedTimelock internal _timelock; SingleGovernance internal _singleGovernance; @@ -486,7 +486,7 @@ contract ScenarioTestBlueprint is Test { } function _deployAdminExecutor(address owner) internal { - _adminExecutor = new OwnableExecutor(owner); + _adminExecutor = new Executor(owner); } function _deployConfigImpl() internal { From ef3b668682ed4672a0e26ea67b4eede6517a7c14 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 22 Apr 2024 04:09:50 +0400 Subject: [PATCH 036/134] Add VetoSignallingDeactivation -> RageQuit transition --- contracts/libraries/DualGovernanceState.sol | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol index aa93fafe..8caebe3d 100644 --- a/contracts/libraries/DualGovernanceState.sol +++ b/contracts/libraries/DualGovernanceState.sol @@ -120,7 +120,7 @@ library DualGovernanceStateTransitions { ) private view returns (State) { uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - // 1. Transition to RageQuitAccumulation + // 1. Transition to RageQuit if (_isDynamicTimelockMaxDurationPassed(self, config)) { if (_isSecondSealRageQuitSupportCrossed(config, rageQuitSupport)) { return State.RageQuit; @@ -148,6 +148,12 @@ library DualGovernanceStateTransitions { return State.VetoSignalling; } + if (_isDynamicTimelockMaxDurationPassed(self, config)) { + if (_isSecondSealRageQuitSupportCrossed(config, rageQuitSupport)) { + return State.RageQuit; + } + } + if (_isVetoSignallingDeactivationMaxDurationPassed(self, config)) { return State.VetoCooldown; } From b27bd2541ea2a5cddc9259c629fcaf6ed495b603 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 22 Apr 2024 04:11:27 +0400 Subject: [PATCH 037/134] setTiebreakerProtection() -> setTiebreakerCommittee() --- contracts/DualGovernance.sol | 2 +- test/utils/scenario-test-blueprint.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index de3722e9..ddbbd7dd 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -155,7 +155,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { TIMELOCK.schedule(proposalId); } - function setTiebreakerProtection(address newTiebreaker) external { + function setTiebreakerCommittee(address newTiebreaker) external { _checkAdminExecutor(msg.sender); address oldTiebreaker = _tiebreaker; if (newTiebreaker != oldTiebreaker) { diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 8d7196e4..c0446466 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -537,7 +537,7 @@ contract ScenarioTestBlueprint is Test { _adminExecutor.execute( address(_dualGovernance), 0, - abi.encodeCall(_dualGovernance.setTiebreakerProtection, (_TIEBREAK_COMMITTEE)) + abi.encodeCall(_dualGovernance.setTiebreakerCommittee, (_TIEBREAK_COMMITTEE)) ); } _adminExecutor.execute(address(_timelock), 0, abi.encodeCall(_timelock.setGovernance, (governance))); From 3186ad47d63927136007bbe26f7b7d98f52e7a3e Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 22 Apr 2024 04:13:12 +0400 Subject: [PATCH 038/134] tiebreakerSchedule() -> tiebreakerScheduleProposal() --- contracts/DualGovernance.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index ddbbd7dd..f4f8fbdb 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -149,7 +149,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { // Tiebreaker Protection // --- - function tiebreakerSchedule(uint256 proposalId) external { + function tiebreakerScheduleProposal(uint256 proposalId) external { _checkTiebreakerCommittee(msg.sender); _dgState.checkTiebreak(CONFIG); TIMELOCK.schedule(proposalId); From 01bce3d1bdb71613535336c03c7a9368ebe6e5b0 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 22 Apr 2024 04:16:44 +0400 Subject: [PATCH 039/134] DG.submit() -> DG,submitProposal() --- contracts/DualGovernance.sol | 2 +- contracts/SingleGovernance.sol | 2 +- contracts/interfaces/ITimelock.sol | 2 +- test/utils/scenario-test-blueprint.sol | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index f4f8fbdb..17bf4e9a 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -46,7 +46,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { _proposers.register(adminProposer, CONFIG.ADMIN_EXECUTOR()); } - function submit(ExecutorCall[] calldata calls) external returns (uint256 proposalId) { + function submitProposal(ExecutorCall[] calldata calls) external returns (uint256 proposalId) { _proposers.checkProposer(msg.sender); _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); _dgState.checkProposalsCreationAllowed(); diff --git a/contracts/SingleGovernance.sol b/contracts/SingleGovernance.sol index 5f01f4bc..08c18490 100644 --- a/contracts/SingleGovernance.sol +++ b/contracts/SingleGovernance.sol @@ -17,7 +17,7 @@ contract SingleGovernance is IGovernance, ConfigurationProvider { TIMELOCK = ITimelock(timelock); } - function submit(ExecutorCall[] calldata calls) external returns (uint256 proposalId) { + function submitProposal(ExecutorCall[] calldata calls) external returns (uint256 proposalId) { _checkGovernance(msg.sender); return TIMELOCK.submit(CONFIG.ADMIN_EXECUTOR(), calls); } diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index 380de7fe..312b579c 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.23; import {ExecutorCall} from "./IExecutor.sol"; interface IGovernance { - function submit(ExecutorCall[] calldata calls) external returns (uint256 proposalId); + function submitProposal(ExecutorCall[] calldata calls) external returns (uint256 proposalId); function schedule(uint256 proposalId) external; function cancelAllPendingProposals() external; diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index c0446466..df359b5c 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -260,7 +260,7 @@ contract ScenarioTestBlueprint is Test { uint256 proposalsCountBefore = _timelock.getProposalsCount(); bytes memory script = - Utils.encodeEvmCallScript(address(governance), abi.encodeCall(IGovernance.submit, (calls))); + Utils.encodeEvmCallScript(address(governance), abi.encodeCall(IGovernance.submitProposal, (calls))); uint256 voteId = Utils.adoptVote(DAO_VOTING, description, script); // The scheduled calls count is the same until the vote is enacted From 279747bff5bbfc87ddc9cb067b04a1d2beea5de9 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 22 Apr 2024 04:21:47 +0400 Subject: [PATCH 040/134] DG.schedule() -> DG.scheduleProposal() --- contracts/DualGovernance.sol | 3 ++- contracts/SingleGovernance.sol | 2 +- contracts/interfaces/ITimelock.sol | 2 +- test/utils/scenario-test-blueprint.sol | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 17bf4e9a..e65869eb 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -55,7 +55,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { proposalId = TIMELOCK.submit(proposer.executor, calls); } - function schedule(uint256 proposalId) external { + function scheduleProposal(uint256 proposalId) external { _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); _dgState.checkProposalsAdoptionAllowed(); TIMELOCK.schedule(proposalId); @@ -151,6 +151,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function tiebreakerScheduleProposal(uint256 proposalId) external { _checkTiebreakerCommittee(msg.sender); + _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); _dgState.checkTiebreak(CONFIG); TIMELOCK.schedule(proposalId); } diff --git a/contracts/SingleGovernance.sol b/contracts/SingleGovernance.sol index 08c18490..93da9847 100644 --- a/contracts/SingleGovernance.sol +++ b/contracts/SingleGovernance.sol @@ -22,7 +22,7 @@ contract SingleGovernance is IGovernance, ConfigurationProvider { return TIMELOCK.submit(CONFIG.ADMIN_EXECUTOR(), calls); } - function schedule(uint256 proposalId) external { + function scheduleProposal(uint256 proposalId) external { TIMELOCK.schedule(proposalId); } diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index 312b579c..8054ad9e 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -5,7 +5,7 @@ import {ExecutorCall} from "./IExecutor.sol"; interface IGovernance { function submitProposal(ExecutorCall[] calldata calls) external returns (uint256 proposalId); - function schedule(uint256 proposalId) external; + function scheduleProposal(uint256 proposalId) external; function cancelAllPendingProposals() external; function canSchedule(uint256 proposalId) external view returns (bool); diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index df359b5c..c2e711d7 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -275,7 +275,7 @@ contract ScenarioTestBlueprint is Test { } function _scheduleProposal(IGovernance governance, uint256 proposalId) internal { - governance.schedule(proposalId); + governance.scheduleProposal(proposalId); } function _executeProposal(uint256 proposalId) internal { From 5a4442b36d53eec5d7b1bd7f442f9629b90a1946 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 22 Apr 2024 04:26:23 +0400 Subject: [PATCH 041/134] Escrow.requestWithdrawalsBatch() -> Escrow.requestNextWithdrawalsBatch() --- contracts/Escrow.sol | 2 +- test/scenario/escrow.t.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 387ad41c..d3f59f8b 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -185,7 +185,7 @@ contract Escrow is IEscrow { ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); } - function requestWithdrawalsBatch(uint256 maxWithdrawalRequestsCount) external { + function requestNextWithdrawalsBatch(uint256 maxWithdrawalRequestsCount) external { _checkEscrowState(EscrowState.RageQuitEscrow); uint256[] memory requestAmounts = _accounting.formWithdrawalBatch( diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index bf374c72..1926995d 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -329,11 +329,11 @@ contract EscrowHappyPath is TestHelpers { uint256 expectedWithdrawalBatchesCount = escrowStETHBalance / requestAmount + 1; assertEq(_WITHDRAWAL_QUEUE.balanceOf(address(escrow)), 10); - escrow.requestWithdrawalsBatch(10); + escrow.requestNextWithdrawalsBatch(10); assertEq(_WITHDRAWAL_QUEUE.balanceOf(address(escrow)), 20); - escrow.requestWithdrawalsBatch(200); + escrow.requestNextWithdrawalsBatch(200); assertEq(_WITHDRAWAL_QUEUE.balanceOf(address(escrow)), 10 + expectedWithdrawalBatchesCount); assertEq(escrow.isRageQuitFinalized(), false); From 5be440444d12adda9bff8e926f0edde51dd4a815 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 22 Apr 2024 04:28:02 +0400 Subject: [PATCH 042/134] claimWithdrawalRequests() -> claimUnstETH() --- contracts/Escrow.sol | 2 +- test/scenario/escrow.t.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index d3f59f8b..dfbabcb0 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -214,7 +214,7 @@ contract Escrow is IEscrow { } } - function claimWithdrawalRequests(uint256[] calldata unstETHIds, uint256[] calldata hints) external { + function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external { _checkEscrowState(EscrowState.RageQuitEscrow); uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints); diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 1926995d..66a07544 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -365,7 +365,7 @@ contract EscrowHappyPath is TestHelpers { { uint256[] memory hints = _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); - escrow.claimWithdrawalRequests(unstETHIds, hints); + escrow.claimUnstETH(unstETHIds, hints); // but it can't be withdrawn before withdrawal timelock has passed vm.expectRevert(); @@ -414,7 +414,7 @@ contract EscrowHappyPath is TestHelpers { uint256[] memory hints = _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); - escrow.claimWithdrawalRequests(unstETHIds, hints); + escrow.claimUnstETH(unstETHIds, hints); assertEq(escrow.isRageQuitFinalized(), false); From 162734243a221aed962d365dc525516ec42b0459 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 22 Apr 2024 04:29:20 +0400 Subject: [PATCH 043/134] Escrow.withdrawWstETH() -> Escrow.withdrawWstETHAsETH() --- contracts/Escrow.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index dfbabcb0..2488acb5 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -236,7 +236,7 @@ contract Escrow is IEscrow { Address.sendValue(payable(msg.sender), _accounting.accountStETHWithdraw(msg.sender)); } - function withdrawWstETH() external { + function withdrawWstETHAsETH() external { _checkEscrowState(EscrowState.RageQuitEscrow); _checkWithdrawalsTimelockPassed(); Address.sendValue(payable(msg.sender), _accounting.accountWstETHWithdraw(msg.sender)); From 24a9b5128c3b3f822ef1b573a5c16eeb3ca1421c Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 22 Apr 2024 04:31:40 +0400 Subject: [PATCH 044/134] EmergencyProtectedTimelock emergency methods rename --- contracts/EmergencyProtectedTimelock.sol | 4 ++-- test/scenario/agent-timelock.t.sol | 2 +- test/scenario/happy-path-plan-b.t.sol | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index af90cc7f..c4f314cc 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -62,7 +62,7 @@ contract EmergencyProtectedTimelock is ConfigurationProvider { // Emergency Protection Functionality // --- - function emergencyActivate() external { + function activateEmergencyMode() external { _emergencyProtection.checkActivationCommittee(msg.sender); _emergencyProtection.checkEmergencyModeActive(false); _emergencyProtection.activate(); @@ -74,7 +74,7 @@ contract EmergencyProtectedTimelock is ConfigurationProvider { _proposals.execute(proposalId, /* afterScheduleDelay */ 0); } - function emergencyDeactivate() external { + function deactivateEmergencyMode() external { _emergencyProtection.checkEmergencyModeActive(true); if (!_emergencyProtection.isEmergencyModePassed()) { _checkAdminExecutor(msg.sender); diff --git a/test/scenario/agent-timelock.t.sol b/test/scenario/agent-timelock.t.sol index 323466ad..b13e2895 100644 --- a/test/scenario/agent-timelock.t.sol +++ b/test/scenario/agent-timelock.t.sol @@ -104,7 +104,7 @@ contract AgentTimelockTest is ScenarioTestBlueprint { // committee resets governance vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); - _timelock.emergencyActivate(); + _timelock.activateEmergencyMode(); vm.prank(_EMERGENCY_EXECUTION_COMMITTEE); _timelock.emergencyReset(); diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index 782d3767..7424b8d6 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -73,7 +73,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // emergency committee activates emergency mode vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); - _timelock.emergencyActivate(); + _timelock.activateEmergencyMode(); // emergency mode was successfully activated uint256 expectedEmergencyModeEndTimestamp = block.timestamp + _EMERGENCY_MODE_DURATION; @@ -119,7 +119,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // Only Dual Governance contract can call the Timelock contract abi.encodeCall(_timelock.setGovernance, (address(_dualGovernance))), // Now the emergency mode may be deactivated (all scheduled calls will be canceled) - abi.encodeCall(_timelock.emergencyDeactivate, ()), + abi.encodeCall(_timelock.deactivateEmergencyMode, ()), // Setup emergency committee for some period of time until the Dual Governance is battle tested abi.encodeCall( _timelock.setEmergencyProtection, @@ -290,7 +290,7 @@ contract PlanBSetup is ScenarioTestBlueprint { vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2); vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); - _timelock.emergencyActivate(); + _timelock.activateEmergencyMode(); emergencyState = _timelock.getEmergencyState(); assertTrue(emergencyState.isEmergencyModeActivated); @@ -357,7 +357,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // anyone can deactivate emergency mode when it's over { - _timelock.emergencyDeactivate(); + _timelock.deactivateEmergencyMode(); emergencyState = _timelock.getEmergencyState(); assertFalse(emergencyState.isEmergencyModeActivated); @@ -388,7 +388,7 @@ contract PlanBSetup is ScenarioTestBlueprint { EmergencyState memory emergencyState; { vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); - _timelock.emergencyActivate(); + _timelock.activateEmergencyMode(); emergencyState = _timelock.getEmergencyState(); assertTrue(emergencyState.isEmergencyModeActivated); @@ -430,7 +430,7 @@ contract PlanBSetup is ScenarioTestBlueprint { { vm.expectRevert(EmergencyProtection.EmergencyCommitteeExpired.selector); vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); - _timelock.emergencyActivate(); + _timelock.activateEmergencyMode(); } } } From 0891ce829b4606a9c7ac8ccec9cb727e02db35aa Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Mon, 22 Apr 2024 20:18:08 +0700 Subject: [PATCH 045/134] feat: setGovernance tested --- test/unit/EmergencyProtectedTimelock.t.sol | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 test/unit/EmergencyProtectedTimelock.t.sol diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol new file mode 100644 index 00000000..11c075d9 --- /dev/null +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Test, Vm} from "forge-std/Test.sol"; + +import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; +import {IConfiguration, Configuration} from "contracts/Configuration.sol"; +import {ConfigurationProvider} from "contracts/ConfigurationProvider.sol"; + +import "forge-std/console.sol"; + +contract EmergencyProtectedTimelockUnitTests is Test { + EmergencyProtectedTimelock private _timelock; + Configuration private _config; + + address private _emergencyGovernance = makeAddr("emergencyGovernance"); + address private _dualGovernance = makeAddr("dualGovernance"); + address private _executor = makeAddr("executor"); + + function setUp() external { + _config = new Configuration(_executor, _emergencyGovernance, new address[](0)); + _timelock = new EmergencyProtectedTimelock(address(_config)); + } + + function test_admin_executor_can_set_governance() external { + vm.recordLogs(); + + assertEq(_timelock.getGovernance(), address(0)); + + vm.prank(_executor); + _timelock.setGovernance(_dualGovernance); + + assertEq(_timelock.getGovernance(), _dualGovernance); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 1); + assertEq(entries[0].topics[0], EmergencyProtectedTimelock.GovernanceSet.selector); + assertEq(abi.decode(entries[0].data, (address)), _dualGovernance); + } + + function test_cannot_set_governance_to_zero() external { + vm.prank(_executor); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidGovernance.selector, address(0))); + _timelock.setGovernance(address(0)); + } + + function test_cannot_set_governance_to_the_same_address() external { + vm.startPrank(_executor); + + _timelock.setGovernance(_dualGovernance); + assertEq(_timelock.getGovernance(), _dualGovernance); + + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidGovernance.selector, _dualGovernance)); + _timelock.setGovernance(_dualGovernance); + assertEq(_timelock.getGovernance(), _dualGovernance); + + vm.stopPrank(); + } + + function testFuzz_stranger_cannot_set_governance(address stranger) external { + vm.assume(stranger != _executor); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(ConfigurationProvider.NotAdminExecutor.selector, stranger)); + _timelock.setGovernance(makeAddr("newGovernance")); + } +} From 976a1d7c1a3ca4de378f4b830c74cabaeb72445e Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Mon, 22 Apr 2024 21:16:26 +0700 Subject: [PATCH 046/134] feat: transferExecutorOwnership tested --- test/unit/EmergencyProtectedTimelock.t.sol | 60 +++++++++++++++++++--- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 11c075d9..da891896 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -2,7 +2,9 @@ pragma solidity 0.8.23; import {Test, Vm} from "forge-std/Test.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Executor} from "contracts/Executor.sol"; import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; import {IConfiguration, Configuration} from "contracts/Configuration.sol"; import {ConfigurationProvider} from "contracts/ConfigurationProvider.sol"; @@ -15,37 +17,43 @@ contract EmergencyProtectedTimelockUnitTests is Test { address private _emergencyGovernance = makeAddr("emergencyGovernance"); address private _dualGovernance = makeAddr("dualGovernance"); - address private _executor = makeAddr("executor"); + address private _adminExecutor = makeAddr("executor"); function setUp() external { - _config = new Configuration(_executor, _emergencyGovernance, new address[](0)); + _config = new Configuration(_adminExecutor, _emergencyGovernance, new address[](0)); _timelock = new EmergencyProtectedTimelock(address(_config)); } - function test_admin_executor_can_set_governance() external { - vm.recordLogs(); + // EmergencyProtectedTimelock.setGovernance() + function test_admin_executor_can_set_governance() external { assertEq(_timelock.getGovernance(), address(0)); - vm.prank(_executor); + vm.recordLogs(); + + vm.prank(_adminExecutor); _timelock.setGovernance(_dualGovernance); assertEq(_timelock.getGovernance(), _dualGovernance); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 1); + assertEq(entries[0].emitter, address(_timelock)); assertEq(entries[0].topics[0], EmergencyProtectedTimelock.GovernanceSet.selector); + // There is no topic with value in the event (foundry bug??) + assertEq(entries[0].topics.length, 1); + assertEq(abi.decode(entries[0].data, (address)), _dualGovernance); } function test_cannot_set_governance_to_zero() external { - vm.prank(_executor); + vm.prank(_adminExecutor); vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidGovernance.selector, address(0))); _timelock.setGovernance(address(0)); } function test_cannot_set_governance_to_the_same_address() external { - vm.startPrank(_executor); + vm.startPrank(_adminExecutor); _timelock.setGovernance(_dualGovernance); assertEq(_timelock.getGovernance(), _dualGovernance); @@ -58,10 +66,46 @@ contract EmergencyProtectedTimelockUnitTests is Test { } function testFuzz_stranger_cannot_set_governance(address stranger) external { - vm.assume(stranger != _executor); + vm.assume(stranger != _adminExecutor); vm.prank(stranger); vm.expectRevert(abi.encodeWithSelector(ConfigurationProvider.NotAdminExecutor.selector, stranger)); _timelock.setGovernance(makeAddr("newGovernance")); } + + // EmergencyProtectedTimelock.transferExecutorOwnership() + + function testFuzz_admin_executor_can_transfer_executor_ownership(address newOwner) external { + vm.assume(newOwner != _adminExecutor); + vm.assume(newOwner != address(0)); + + Executor executor = new Executor(address(_timelock)); + + assertEq(executor.owner(), address(_timelock)); + + vm.recordLogs(); + + vm.prank(_adminExecutor); + _timelock.transferExecutorOwnership(address(executor), newOwner); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 1); + assertEq(entries[0].emitter, address(executor)); + assertEq(entries[0].topics[0], Ownable.OwnershipTransferred.selector); + assertEq(entries[0].topics[1], bytes32(uint256(uint160(address(_timelock))))); + assertEq(entries[0].topics[2], bytes32(uint256(uint160(newOwner)))); + + // There is no data in the event (foundry bug??) + assertEq(bytes32(entries[0].data), bytes32(uint256(0))); + + assertEq(executor.owner(), newOwner); + } + + function test_stranger_cannot_transfer_executor_ownership(address stranger) external { + vm.assume(stranger != _adminExecutor); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(ConfigurationProvider.NotAdminExecutor.selector, stranger)); + _timelock.transferExecutorOwnership(_adminExecutor, makeAddr("newOwner")); + } } From fd45a615ced7fc2d035ae724c40d74588e9b7e8b Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 24 Apr 2024 11:07:41 +0300 Subject: [PATCH 047/134] 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 048/134] 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 4f395d3f445f481328fb59cf0914388de72f2166 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 24 Apr 2024 14:00:08 +0400 Subject: [PATCH 049/134] VetoCooldown allows executing proposals submitted only before VetoSignalling --- contracts/DualGovernance.sol | 5 +- contracts/EmergencyProtectedTimelock.sol | 4 +- contracts/interfaces/ITimelock.sol | 2 +- contracts/libraries/DualGovernanceState.sol | 91 +++++--- contracts/libraries/Proposals.sol | 12 +- test/scenario/agent-timelock.t.sol | 13 +- .../last-moment-malicious-proposal.t.sol | 202 +++++++++++++++--- test/utils/scenario-test-blueprint.sol | 19 +- 8 files changed, 262 insertions(+), 86 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index e65869eb..618e2109 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -50,15 +50,14 @@ contract DualGovernance is IGovernance, ConfigurationProvider { _proposers.checkProposer(msg.sender); _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); _dgState.checkProposalsCreationAllowed(); - _dgState.setLastProposalCreationTimestamp(); Proposer memory proposer = _proposers.get(msg.sender); proposalId = TIMELOCK.submit(proposer.executor, calls); } function scheduleProposal(uint256 proposalId) external { _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); - _dgState.checkProposalsAdoptionAllowed(); - TIMELOCK.schedule(proposalId); + uint256 proposalSubmissionTime = TIMELOCK.schedule(proposalId); + _dgState.checkCanScheduleProposal(proposalSubmissionTime); emit ProposalScheduled(proposalId); } diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index c4f314cc..64aecdda 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -33,9 +33,9 @@ contract EmergencyProtectedTimelock is ConfigurationProvider { emit ProposalLaunched(msg.sender, executor, newProposalId); } - function schedule(uint256 proposalId) external { + function schedule(uint256 proposalId) external returns (uint256 submittedAt) { _checkGovernance(msg.sender); - _proposals.schedule(proposalId, CONFIG.AFTER_SUBMIT_DELAY()); + submittedAt = _proposals.schedule(proposalId, CONFIG.AFTER_SUBMIT_DELAY()); } function execute(uint256 proposalId) external { diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index 8054ad9e..0f080e8d 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -13,7 +13,7 @@ interface IGovernance { interface ITimelock { function submit(address executor, ExecutorCall[] calldata calls) external returns (uint256 newProposalId); - function schedule(uint256 proposalId) external; + function schedule(uint256 proposalId) external returns (uint256 submittedAt); function execute(uint256 proposalId) external; function cancelAllNonExecutedProposals() external; diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol index 8caebe3d..1246f37a 100644 --- a/contracts/libraries/DualGovernanceState.sol +++ b/contracts/libraries/DualGovernanceState.sol @@ -21,19 +21,36 @@ enum State { RageQuit } +/* +// The previous version of the state (3 slots) struct DualGovernanceState { - State state; - uint8 rageQuitRound; - uint40 enteredAt; + State state; // [0, 8] + uint8 rageQuitRound; // [0, 16] + uint40 enteredAt; // [0, 56] // the time the veto signalling state was entered - uint40 vetoSignallingActivationTime; + uint40 vetoSignallingActivationTime; // [0, 96] + IEscrow signallingEscrow; // [0, 256] // the time the Deactivation sub-state was last exited without exiting the parent Veto Signalling state - uint40 vetoSignallingReactivationTime; - IEscrow signallingEscrow; + uint40 vetoSignallingReactivationTime; // [1, 40] // the last time a proposal was submitted to the DG subsystem - uint40 lastProposalSubmissionTime; - uint40 lastAdoptableStateExitedAt; - IEscrow rageQuitEscrow; + uint40 lastProposalSubmissionTime; // [1, 80] + uint40 lastAdoptableStateExitedAt; // [1, 120] + IEscrow rageQuitEscrow; // [2, 160] +} +*/ + +struct DualGovernanceState { + State state; // [0, 8] + uint8 rageQuitRound; // [0, 16] + uint40 enteredAt; // [0, 56] + // the time the veto signalling state was entered + uint40 vetoSignallingActivationTime; // [0, 96] + IEscrow signallingEscrow; // [0, 256] + // the time the Deactivation sub-state was last exited without exiting the parent Veto Signalling state + uint40 vetoSignallingReactivationTime; // [1, 40] + // the last time a proposal was submitted to the DG subsystem + uint40 lastAdoptableStateExitedAt; // [1, 80] + IEscrow rageQuitEscrow; // [1, 240] } function dynamicTimelockDuration( @@ -97,10 +114,6 @@ library DualGovernanceStateTransitions { } } - function setLastProposalCreationTimestamp(DualGovernanceState storage self) internal { - self.lastProposalSubmissionTime = TimeUtils.timestamp(); - } - // --- // State Transitions // --- @@ -120,22 +133,17 @@ library DualGovernanceStateTransitions { ) private view returns (State) { uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - // 1. Transition to RageQuit - if (_isDynamicTimelockMaxDurationPassed(self, config)) { - if (_isSecondSealRageQuitSupportCrossed(config, rageQuitSupport)) { - return State.RageQuit; - } + if (!_isDynamicTimelockDurationPassed(self, config, rageQuitSupport)) { + return State.VetoSignalling; } - // 2. Transition to VetoSignallingDeactivation - - if (_isDynamicTimelockDurationPassed(self, config, rageQuitSupport)) { - if (_isVetoSignallingReactivationDurationPassed(self, config)) { - return State.VetoSignallingDeactivation; - } + if (_isSecondSealRageQuitSupportCrossed(config, rageQuitSupport)) { + return State.RageQuit; } - return State.VetoSignalling; + return _isVetoSignallingReactivationDurationPassed(self, config) + ? State.VetoSignallingDeactivation + : State.VetoSignalling; } function _fromVetoSignallingDeactivationState( @@ -148,10 +156,8 @@ library DualGovernanceStateTransitions { return State.VetoSignalling; } - if (_isDynamicTimelockMaxDurationPassed(self, config)) { - if (_isSecondSealRageQuitSupportCrossed(config, rageQuitSupport)) { - return State.RageQuit; - } + if (_isSecondSealRageQuitSupportCrossed(config, rageQuitSupport)) { + return State.RageQuit; } if (_isVetoSignallingDeactivationMaxDurationPassed(self, config)) { @@ -201,15 +207,23 @@ library DualGovernanceStateTransitions { if (oldState == State.Normal || oldState == State.VetoCooldown) { self.lastAdoptableStateExitedAt = timestamp; } + if (newState == State.Normal && self.rageQuitRound != 0) { self.rageQuitRound = 0; } + if (newState == State.VetoSignalling && oldState != State.VetoSignallingDeactivation) { self.vetoSignallingActivationTime = timestamp; } + if (oldState == State.VetoSignallingDeactivation && newState == State.VetoSignalling) { self.vetoSignallingReactivationTime = timestamp; } + + if (oldState == State.VetoCooldown && newState == State.Normal) { + self.vetoSignallingActivationTime = 0; + } + if (newState == State.RageQuit) { IEscrow signallingEscrow = self.signallingEscrow; signallingEscrow.startRageQuit( @@ -247,8 +261,7 @@ library DualGovernanceStateTransitions { DualGovernanceConfig memory config, uint256 rageQuitSupport ) private view returns (bool) { - uint256 vetoSignallingDurationPassed = - block.timestamp - Math.max(self.vetoSignallingActivationTime, self.lastProposalSubmissionTime); + uint256 vetoSignallingDurationPassed = block.timestamp - self.vetoSignallingActivationTime; return vetoSignallingDurationPassed > dynamicTimelockDuration(config, rageQuitSupport); } @@ -313,6 +326,12 @@ library DualGovernanceStateViews { } } + function checkCanScheduleProposal(DualGovernanceState storage self, uint256 proposalSubmittedAt) internal view { + if (!canScheduleProposal(self, proposalSubmittedAt)) { + revert ProposalsAdoptionSuspended(); + } + } + function checkTiebreak(DualGovernanceState storage self, IConfiguration config) internal view { if (!isTiebreak(self, config)) { revert NotTie(); @@ -323,6 +342,16 @@ library DualGovernanceStateViews { return self.state; } + function canScheduleProposal( + DualGovernanceState storage self, + uint256 proposalSubmissionTime + ) internal view returns (bool) { + if (!isProposalsAdoptionAllowed(self)) { + return false; + } + return self.vetoSignallingActivationTime == 0 || proposalSubmissionTime <= self.vetoSignallingActivationTime; + } + function isProposalsCreationAllowed(DualGovernanceState storage self) internal view returns (bool) { State state = self.state; return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; diff --git a/contracts/libraries/Proposals.sol b/contracts/libraries/Proposals.sol index 0cbb4317..3f5b742a 100644 --- a/contracts/libraries/Proposals.sol +++ b/contracts/libraries/Proposals.sol @@ -82,10 +82,18 @@ library Proposals { emit ProposalSubmitted(newProposalId, executor, calls); } - function schedule(State storage self, uint256 proposalId, uint256 afterSubmitDelay) internal { + function schedule( + State storage self, + uint256 proposalId, + uint256 afterSubmitDelay + ) internal returns (uint256 submittedAt) { _checkProposalSubmitted(self, proposalId); _checkAfterSubmitDelayPassed(self, proposalId, afterSubmitDelay); - _packed(self, proposalId).scheduledAt = TimeUtils.timestamp(); + ProposalPacked storage proposal = _packed(self, proposalId); + + submittedAt = proposal.submittedAt; + proposal.scheduledAt = TimeUtils.timestamp(); + emit ProposalScheduled(proposalId); } diff --git a/test/scenario/agent-timelock.t.sol b/test/scenario/agent-timelock.t.sol index b13e2895..f209a1e9 100644 --- a/test/scenario/agent-timelock.t.sol +++ b/test/scenario/agent-timelock.t.sol @@ -12,10 +12,9 @@ contract AgentTimelockTest is ScenarioTestBlueprint { function testFork_AgentTimelockHappyPath() external { ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); - // --- - // 1. THE PROPOSAL IS SUBMITTED - // --- + uint256 proposalId; + _step("1. THE PROPOSAL IS SUBMITTED"); { proposalId = _submitProposal( _dualGovernance, "Propose to doSmth on target passing dual governance", regularStaffCalls @@ -25,9 +24,7 @@ contract AgentTimelockTest is ScenarioTestBlueprint { _assertCanSchedule(_dualGovernance, proposalId, false); } - // --- - // 2. THE PROPOSAL IS SCHEDULED - // --- + _step("2. THE PROPOSAL IS SCHEDULED"); { _waitAfterSubmitDelayPassed(); _assertCanSchedule(_dualGovernance, proposalId, true); @@ -37,9 +34,7 @@ contract AgentTimelockTest is ScenarioTestBlueprint { _assertCanExecute(proposalId, false); } - // --- - // 3. THE PROPOSAL CAN BE EXECUTED - // --- + _step("3. THE PROPOSAL CAN BE EXECUTED"); { // wait until the second delay has passed _waitAfterScheduleDelayPassed(); diff --git a/test/scenario/last-moment-malicious-proposal.t.sol b/test/scenario/last-moment-malicious-proposal.t.sol index ddb91985..36e99eeb 100644 --- a/test/scenario/last-moment-malicious-proposal.t.sol +++ b/test/scenario/last-moment-malicious-proposal.t.sol @@ -2,7 +2,11 @@ pragma solidity 0.8.23; import { - percents, ScenarioTestBlueprint, ExecutorCall, ExecutorCallHelpers + percents, + ScenarioTestBlueprint, + ExecutorCall, + ExecutorCallHelpers, + DualGovernanceStateViews } from "../utils/scenario-test-blueprint.sol"; interface IDangerousContract { @@ -22,9 +26,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); uint256 proposalId; - // --- - // ACT 1. DAO SUBMITS PROPOSAL WITH REGULAR STAFF - // --- + _step("1. DAO SUBMITS PROPOSAL WITH REGULAR STAFF"); { proposalId = _submitProposal( _dualGovernance, "DAO does regular staff on potentially dangerous contract", regularStaffCalls @@ -34,10 +36,8 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _logVetoSignallingState(); } - // --- - // ACT 2. MALICIOUS ACTOR STARTS ACQUIRE VETO SIGNALLING DURATION - // --- address maliciousActor = makeAddr("MALICIOUS_ACTOR"); + _step("2. MALICIOUS ACTOR STARTS ACQUIRE VETO SIGNALLING DURATION"); { _lockStETH(maliciousActor, percents("12.0")); _assertVetoSignalingState(); @@ -50,10 +50,8 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _logVetoSignallingState(); } - // --- - // ACT 3. MALICIOUS ACTOR SUBMITS MALICIOUS PROPOSAL - // --- uint256 maliciousProposalId; + _step("3. MALICIOUS ACTOR SUBMITS MALICIOUS PROPOSAL"); { _assertVetoSignalingState(); maliciousProposalId = _submitProposal( @@ -68,43 +66,61 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _logVetoSignallingState(); } - // --- - // ACT 4. MALICIOUS ACTOR UNLOCK FUNDS FROM ESCROW - // --- + _step("4. MALICIOUS ACTOR UNLOCK FUNDS FROM ESCROW"); { _wait(12 seconds); _unlockStETH(maliciousActor); _assertVetoSignalingDeactivationState(); + _logVetoSignallingDeactivationState(); } - // --- - // ACT 5. STETH HOLDERS MAY ACQUIRE QUORUM BECAUSE THE VETO SIGNALLING PERIOD RESTARTED - // --- - address stEthWhale = makeAddr("STETH_WHALE"); + address stEthHolders = makeAddr("STETH_WHALE"); + _step("5. STETH HOLDERS ACQUIRING QUORUM TO VETO MALICIOUS PROPOSAL"); { - _depositStETH(stEthWhale, 1 ether); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION()); - _lockStETH(stEthWhale, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() / 2); + _lockStETH(stEthHolders, percents(_config.FIRST_SEAL_RAGE_QUIT_SUPPORT() + 1)); + _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); - _assertVetoSignalingState(); - _logVetoSignallingState(); + + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() / 2 + 1); + _activateNextState(); + _assertVetoCooldownState(); } - // --- - // ACT 6. STETH HOLDER MAY EXIT TO RAGE QUIT WHEN THE SECOND SEAL THRESHOLD REACHED - // --- + _step("6. MALICIOUS PROPOSAL CAN'T BE EXECUTED IN THE VETO COOLDOWN STATE"); { - _lockStETH(stEthWhale, 1 gwei); - _assertVetoSignalingState(); + // the regular proposal can be executed + _scheduleProposal(_dualGovernance, proposalId); + _waitAfterScheduleDelayPassed(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _executeProposal(proposalId); + _assertProposalExecuted(proposalId); + + vm.expectRevert(DualGovernanceStateViews.ProposalsAdoptionSuspended.selector); + this.scheduleProposalExternal(maliciousProposalId); + + _assertProposalSubmitted(maliciousProposalId); + } + + _step("7. NEW VETO SIGNALLING ROUND FOR MALICIOUS PROPOSAL IS STARTED"); + { + _wait(_config.VETO_COOLDOWN_DURATION() + 1); _activateNextState(); + _assertVetoSignalingState(); + _logVetoSignallingState(); + + // the second seal rage quit support is reached + _lockStETH(stEthHolders, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _assertVetoSignalingState(); + _logVetoSignallingState(); - // the dual governance immediately transfers to the Rage Quit state + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() + 1); + _logVetoSignallingState(); + _activateNextState(); _assertRageQuitState(); - // the malicious call still not executable - _assertProposalSubmitted(maliciousProposalId); + vm.expectRevert(DualGovernanceStateViews.ProposalsAdoptionSuspended.selector); + this.scheduleProposalExternal(maliciousProposalId); } } @@ -166,4 +182,128 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _assertTargetMockCalls(_config.ADMIN_EXECUTOR(), regularStaffCalls); } } + + function testFork_VetoSignallingToNormalState() external { + address maliciousActor = makeAddr("MALICIOUS_ACTOR"); + _step("2. MALICIOUS ACTOR LOCKS FIRST SEAL THRESHOLD TO ACTIVATE VETO SIGNALLING BEFORE PROPOSAL SUBMISSION"); + { + _lockStETH(maliciousActor, percents("3.50")); + _assertVetoSignalingState(); + _logVetoSignallingState(); + } + + uint256 proposalId; + ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + _step("2. DAO SUBMITS PROPOSAL WITH REGULAR STAFF"); + { + proposalId = _submitProposal( + _dualGovernance, "DAO does regular staff on potentially dangerous contract", regularStaffCalls + ); + _assertProposalSubmitted(proposalId); + _assertSubmittedProposalData(proposalId, regularStaffCalls); + _logVetoSignallingState(); + } + + _step("3. THE VETO SIGNALLING & DEACTIVATION PASSED BUT PROPOSAL STILL NOT EXECUTABLE"); + { + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() + 1); + _activateNextState(); + _assertVetoSignalingDeactivationState(); + _logVetoSignallingDeactivationState(); + + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _activateNextState(); + _assertVetoCooldownState(); + + vm.expectRevert(DualGovernanceStateViews.ProposalsAdoptionSuspended.selector); + this.scheduleProposalExternal(proposalId); + } + + _step("4. AFTER THE VETO COOLDOWN GOVERNANCE TRANSITIONS INTO NORMAL STATE"); + { + _unlockStETH(maliciousActor); + _wait(_config.VETO_COOLDOWN_DURATION() + 1); + _activateNextState(); + _assertNormalState(); + } + + _step("5. PROPOSAL EXECUTABLE IN THE NORMAL STATE"); + { + _scheduleProposal(_dualGovernance, proposalId); + _assertProposalScheduled(proposalId); + _waitAfterScheduleDelayPassed(); + _executeProposal(proposalId); + _assertProposalExecuted(proposalId); + } + } + + function testFork_ProposalSubmissionFrontRunning() external { + address maliciousActor = makeAddr("MALICIOUS_ACTOR"); + _step("2. MALICIOUS ACTOR LOCKS FIRST SEAL THRESHOLD TO ACTIVATE VETO SIGNALLING BEFORE PROPOSAL SUBMISSION"); + { + _lockStETH(maliciousActor, percents("3.50")); + _assertVetoSignalingState(); + _logVetoSignallingState(); + } + + uint256 proposalId; + ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + _step("2. DAO SUBMITS PROPOSAL WITH REGULAR STAFF"); + { + proposalId = _submitProposal( + _dualGovernance, "DAO does regular staff on potentially dangerous contract", regularStaffCalls + ); + _assertProposalSubmitted(proposalId); + _assertSubmittedProposalData(proposalId, regularStaffCalls); + _logVetoSignallingState(); + } + + _step("3. THE VETO SIGNALLING & DEACTIVATION PASSED BUT PROPOSAL STILL NOT EXECUTABLE"); + { + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() + 1); + _activateNextState(); + _assertVetoSignalingDeactivationState(); + _logVetoSignallingDeactivationState(); + + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _activateNextState(); + _assertVetoCooldownState(); + + vm.expectRevert(DualGovernanceStateViews.ProposalsAdoptionSuspended.selector); + this.scheduleProposalExternal(proposalId); + } + + _step("4. AFTER THE VETO COOLDOWN NEW VETO SIGNALLING ROUND STARTED"); + { + _wait(_config.VETO_COOLDOWN_DURATION() + 1); + _activateNextState(); + _assertVetoSignalingState(); + _logVetoSignallingState(); + + vm.expectRevert(DualGovernanceStateViews.ProposalsAdoptionSuspended.selector); + this.scheduleProposalExternal(proposalId); + } + + _step("5. PROPOSAL EXECUTABLE IN THE NEXT VETO COOLDOWN"); + { + _wait(2 * _config.DYNAMIC_TIMELOCK_MIN_DURATION()); + _activateNextState(); + _assertVetoSignalingDeactivationState(); + _logVetoSignallingDeactivationState(); + + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _activateNextState(); + _assertVetoCooldownState(); + + _scheduleProposal(_dualGovernance, proposalId); + _assertProposalScheduled(proposalId); + _waitAfterScheduleDelayPassed(); + _executeProposal(proposalId); + _assertProposalExecuted(proposalId); + } + } + + function scheduleProposalExternal(uint256 proposalId) external { + _scheduleProposal(_dualGovernance, proposalId); + } } diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index c2e711d7..04f8985d 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -21,7 +21,7 @@ import { } from "contracts/EmergencyProtectedTimelock.sol"; import {SingleGovernance, IGovernance} from "contracts/SingleGovernance.sol"; -import {DualGovernance, DualGovernanceState, State} from "contracts/DualGovernance.sol"; +import {DualGovernance, DualGovernanceState, DualGovernanceStateViews, State} from "contracts/DualGovernance.sol"; import {Proposal, Status as ProposalStatus} from "contracts/libraries/Proposals.sol"; @@ -410,7 +410,7 @@ contract ScenarioTestBlueprint is Test { _dualGovernance.getVetoSignallingState(); if (!isActive) { - console.log("VetoSignalling state is not active"); + console.log("VetoSignalling state is not active\n"); return; } @@ -418,12 +418,12 @@ contract ScenarioTestBlueprint is Test { console.log("Veto signalling entered at %d (activated at %d)", enteredAt, activatedAt); if (block.timestamp > activatedAt + duration) { console.log( - "Veto signalling has ended %s ago", + "Veto signalling has ended %s ago\n", _formatDuration(_toDuration(block.timestamp - activatedAt - duration)) ); } else { console.log( - "Veto signalling will end after %s", + "Veto signalling will end after %s\n", _formatDuration(_toDuration(activatedAt + duration - block.timestamp)) ); } @@ -435,7 +435,7 @@ contract ScenarioTestBlueprint is Test { (bool isActive, uint256 duration, uint256 enteredAt) = _dualGovernance.getVetoSignallingDeactivationState(); if (!isActive) { - console.log("VetoSignallingDeactivation state is not active"); + console.log("VetoSignallingDeactivation state is not active\n"); return; } @@ -445,12 +445,12 @@ contract ScenarioTestBlueprint is Test { console.log("VetoSignallingDeactivation entered at %d", enteredAt); if (block.timestamp > enteredAt + duration) { console.log( - "VetoSignallingDeactivation has ended %s ago", + "VetoSignallingDeactivation has ended %s ago\n", _formatDuration(_toDuration(block.timestamp - enteredAt - duration)) ); } else { console.log( - "VetoSignallingDeactivation will end after %s", + "VetoSignallingDeactivation will end after %s\n", _formatDuration(_toDuration(enteredAt + duration - block.timestamp)) ); } @@ -548,6 +548,11 @@ contract ScenarioTestBlueprint is Test { // Utils Methods // --- + function _step(string memory text) internal { + // solhint-disable-next-line + console.log(string.concat(">>> ", text, " <<<")); + } + function _wait(uint256 duration) internal { vm.warp(block.timestamp + duration); } From 322e0d06262afdf4162bbf6cc4ac243ebee4641c Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 24 Apr 2024 14:02:31 +0300 Subject: [PATCH 050/134] 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 051/134] 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 fd066faf042126601e8d3cd229c7fe966343807c Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Wed, 24 Apr 2024 21:56:36 +0700 Subject: [PATCH 052/134] feat: emergency protected timelock tested --- contracts/EmergencyProtectedTimelock.sol | 4 - contracts/libraries/Proposals.sol | 15 +- test/unit/EmergencyProtectedTimelock.t.sol | 844 +++++++++++++++++++-- test/utils/interfaces.sol | 6 + test/utils/scenario-test-blueprint.sol | 10 +- 5 files changed, 817 insertions(+), 62 deletions(-) diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index c4f314cc..3686e0ab 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -14,11 +14,8 @@ contract EmergencyProtectedTimelock is ConfigurationProvider { error InvalidGovernance(address governance); error NotGovernance(address account, address governance); - error SchedulingDisabled(); - error UnscheduledExecutionForbidden(); event GovernanceSet(address governance); - event ProposalLaunched(address indexed proposer, address indexed executor, uint256 indexed proposalId); address internal _governance; @@ -30,7 +27,6 @@ contract EmergencyProtectedTimelock is ConfigurationProvider { function submit(address executor, ExecutorCall[] calldata calls) external returns (uint256 newProposalId) { _checkGovernance(msg.sender); newProposalId = _proposals.submit(executor, calls); - emit ProposalLaunched(msg.sender, executor, newProposalId); } function schedule(uint256 proposalId) external { diff --git a/contracts/libraries/Proposals.sol b/contracts/libraries/Proposals.sol index 0cbb4317..7b0e1ee0 100644 --- a/contracts/libraries/Proposals.sol +++ b/contracts/libraries/Proposals.sol @@ -10,7 +10,7 @@ enum Status { Submitted, Scheduled, Executed, - Canceled + Cancelled } struct Proposal { @@ -34,12 +34,12 @@ library Proposals { struct State { // any proposals with ids less or equal to the given one cannot be executed - uint256 lastCanceledProposalId; + uint256 lastCancelledProposalId; ProposalPacked[] proposals; } error EmptyCalls(); - error ProposalCanceled(uint256 proposalId); + error ProposalCancelled(uint256 proposalId); error ProposalNotFound(uint256 proposalId); error ProposalNotScheduled(uint256 proposalId); error ProposalNotSubmitted(uint256 proposalId); @@ -49,7 +49,7 @@ library Proposals { event ProposalScheduled(uint256 indexed id); event ProposalSubmitted(uint256 indexed id, address indexed executor, ExecutorCall[] calls); event ProposalExecuted(uint256 indexed id, bytes[] callResults); - event ProposalsCanceledTill(uint256 proposalId); + event ProposalsCancelledTill(uint256 proposalId); // The id of the first proposal uint256 private constant PROPOSAL_ID_OFFSET = 1; @@ -97,8 +97,8 @@ library Proposals { function cancelAll(State storage self) internal { uint256 lastProposalId = self.proposals.length; - self.lastCanceledProposalId = lastProposalId; - emit ProposalsCanceledTill(lastProposalId); + self.lastCancelledProposalId = lastProposalId; + emit ProposalsCancelledTill(lastProposalId); } function get(State storage self, uint256 proposalId) internal view returns (Proposal memory proposal) { @@ -109,6 +109,7 @@ library Proposals { proposal.status = _getProposalStatus(self, proposalId); proposal.executor = packed.executor; proposal.submittedAt = packed.submittedAt; + proposal.scheduledAt = packed.scheduledAt; proposal.executedAt = packed.executedAt; proposal.calls = packed.calls; } @@ -202,7 +203,7 @@ library Proposals { ProposalPacked storage packed = _packed(self, proposalId); if (packed.executedAt != 0) return Status.Executed; - if (proposalId <= self.lastCanceledProposalId) return Status.Canceled; + if (proposalId <= self.lastCancelledProposalId) return Status.Cancelled; if (packed.scheduledAt != 0) return Status.Scheduled; if (packed.submittedAt != 0) return Status.Submitted; assert(false); diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index da891896..3179fe0d 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -8,42 +8,231 @@ import {Executor} from "contracts/Executor.sol"; import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; import {IConfiguration, Configuration} from "contracts/Configuration.sol"; import {ConfigurationProvider} from "contracts/ConfigurationProvider.sol"; +import {Executor} from "contracts/Executor.sol"; +import {Proposal, Proposals, ExecutorCall, Status} from "contracts/libraries/Proposals.sol"; +import {EmergencyProtection, EmergencyState} from "contracts/libraries/EmergencyProtection.sol"; -import "forge-std/console.sol"; +import {TargetMock} from "test/utils/utils.sol"; +import {ExecutorCallHelpers} from "test/utils/executor-calls.sol"; +import {IDangerousContract} from "test/utils/interfaces.sol"; contract EmergencyProtectedTimelockUnitTests is Test { EmergencyProtectedTimelock private _timelock; Configuration private _config; + TargetMock private _targetMock; + Executor private _executor; + + address private _emergencyActivator = makeAddr("EMERGENCY_ACTIVATION_COMMITTEE"); + address private _emergencyEnactor = makeAddr("EMERGENCY_EXECUTION_COMMITTEE"); + uint256 private _emergencyModeDuration = 180 days; + uint256 private _emergencyProtectionDuration = 90 days; - address private _emergencyGovernance = makeAddr("emergencyGovernance"); - address private _dualGovernance = makeAddr("dualGovernance"); - address private _adminExecutor = makeAddr("executor"); + address private _emergencyGovernance = makeAddr("EMERGENCY_GOVERNANCE"); + address private _dualGovernance = makeAddr("DUAL_GOVERNANCE"); + address private _adminExecutor; function setUp() external { - _config = new Configuration(_adminExecutor, _emergencyGovernance, new address[](0)); + _executor = new Executor(address(this)); + _config = new Configuration(address(_executor), _emergencyGovernance, new address[](0)); _timelock = new EmergencyProtectedTimelock(address(_config)); + _targetMock = new TargetMock(); + + _executor.transferOwnership(address(_timelock)); + _adminExecutor = address(_executor); + + vm.startPrank(_adminExecutor); + _timelock.setGovernance(_dualGovernance); + _timelock.setEmergencyProtection( + _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + ); + vm.stopPrank(); + } + + // EmergencyProtectedTimelock.submit() + + function testFuzz_stranger_cannot_submit_proposal(address stranger) external { + vm.assume(stranger != _dualGovernance); + + vm.prank(stranger); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtectedTimelock.NotGovernance.selector, [stranger, _dualGovernance]) + ); + _timelock.submit(_adminExecutor, new ExecutorCall[](0)); + assertEq(_timelock.getProposalsCount(), 0); + } + + function test_governance_can_submit_proposal() external { + vm.prank(_dualGovernance); + _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls()); + + assertEq(_timelock.getProposalsCount(), 1); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Submitted); + } + + // EmergencyProtectedTimelock.schedule() + + function test_governance_can_schedule_proposal() external { + _submitProposal(); + + assertEq(_timelock.getProposalsCount(), 1); + + vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + + _scheduleProposal(1); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Scheduled); + } + + function testFuzz_stranger_cannot_schedule_proposal(address stranger) external { + vm.assume(stranger != _dualGovernance); + vm.assume(stranger != address(0)); + + _submitProposal(); + + vm.prank(stranger); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtectedTimelock.NotGovernance.selector, [stranger, _dualGovernance]) + ); + _timelock.schedule(1); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Submitted); + } + + // EmergencyProtectedTimelock.execute() + + function testFuzz_anyone_can_execute_proposal(address stranger) external { + vm.assume(stranger != _dualGovernance); + vm.assume(stranger != address(0)); + + _submitProposal(); + assertEq(_timelock.getProposalsCount(), 1); + + vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + + _scheduleProposal(1); + + vm.warp(block.timestamp + _config.AFTER_SCHEDULE_DELAY()); + + vm.prank(stranger); + _timelock.execute(1); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Executed); + } + + function test_cannot_execute_proposal_if_emergency_mode_active() external { + _submitProposal(); + + assertEq(_timelock.getProposalsCount(), 1); + + vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + _scheduleProposal(1); + + vm.warp(block.timestamp + _config.AFTER_SCHEDULE_DELAY()); + + _activateEmergencyMode(); + + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [true, false]) + ); + _timelock.execute(1); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Scheduled); + } + + // EmergencyProtectedTimelock.cancelAllNonExecutedProposals() + + function test_governance_can_cancel_all_non_executed_proposals() external { + ExecutorCall[] memory executorCalls = _getTargetRegularStaffCalls(); + + _submitProposal(); + _submitProposal(); + + assertEq(_timelock.getProposalsCount(), 2); + + vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + + _scheduleProposal(1); + + Proposal memory proposal1 = _timelock.getProposal(1); + Proposal memory proposal2 = _timelock.getProposal(2); + + assert(proposal1.status == Status.Scheduled); + assert(proposal2.status == Status.Submitted); + + vm.prank(_dualGovernance); + _timelock.cancelAllNonExecutedProposals(); + + proposal1 = _timelock.getProposal(1); + proposal2 = _timelock.getProposal(2); + + assertEq(_timelock.getProposalsCount(), 2); + assert(proposal1.status == Status.Cancelled); + assert(proposal2.status == Status.Cancelled); + } + + function testFuzz_stranger_cannot_cancel_all_non_executed_proposals(address stranger) external { + vm.assume(stranger != _dualGovernance); + vm.assume(stranger != address(0)); + + vm.prank(stranger); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtectedTimelock.NotGovernance.selector, [stranger, _dualGovernance]) + ); + _timelock.cancelAllNonExecutedProposals(); + } + + // EmergencyProtectedTimelock.transferExecutorOwnership() + + function testFuzz_admin_executor_can_transfer_executor_ownership(address newOwner) external { + vm.assume(newOwner != _adminExecutor); + vm.assume(newOwner != address(0)); + + Executor executor = new Executor(address(_timelock)); + + assertEq(executor.owner(), address(_timelock)); + + vm.prank(_adminExecutor); + + vm.expectEmit(address(executor)); + emit Ownable.OwnershipTransferred(address(_timelock), newOwner); + + _timelock.transferExecutorOwnership(address(executor), newOwner); + + assertEq(executor.owner(), newOwner); + } + + function test_stranger_cannot_transfer_executor_ownership(address stranger) external { + vm.assume(stranger != _adminExecutor); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(ConfigurationProvider.NotAdminExecutor.selector, stranger)); + _timelock.transferExecutorOwnership(_adminExecutor, makeAddr("newOwner")); } // EmergencyProtectedTimelock.setGovernance() - function test_admin_executor_can_set_governance() external { - assertEq(_timelock.getGovernance(), address(0)); + function testFuzz_admin_executor_can_set_governance(address newGovernance) external { + vm.assume(newGovernance != _dualGovernance); + vm.assume(newGovernance != address(0)); - vm.recordLogs(); + vm.expectEmit(address(_timelock)); + emit EmergencyProtectedTimelock.GovernanceSet(newGovernance); + vm.recordLogs(); vm.prank(_adminExecutor); - _timelock.setGovernance(_dualGovernance); + _timelock.setGovernance(newGovernance); - assertEq(_timelock.getGovernance(), _dualGovernance); + assertEq(_timelock.getGovernance(), newGovernance); Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 1); - assertEq(entries[0].emitter, address(_timelock)); - assertEq(entries[0].topics[0], EmergencyProtectedTimelock.GovernanceSet.selector); - // There is no topic with value in the event (foundry bug??) - assertEq(entries[0].topics.length, 1); - assertEq(abi.decode(entries[0].data, (address)), _dualGovernance); + assertEq(entries.length, 1); } function test_cannot_set_governance_to_zero() external { @@ -53,16 +242,12 @@ contract EmergencyProtectedTimelockUnitTests is Test { } function test_cannot_set_governance_to_the_same_address() external { - vm.startPrank(_adminExecutor); - - _timelock.setGovernance(_dualGovernance); - assertEq(_timelock.getGovernance(), _dualGovernance); - + address currentGovernance = _timelock.getGovernance(); + vm.prank(_adminExecutor); vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidGovernance.selector, _dualGovernance)); - _timelock.setGovernance(_dualGovernance); - assertEq(_timelock.getGovernance(), _dualGovernance); + _timelock.setGovernance(currentGovernance); - vm.stopPrank(); + assertEq(_timelock.getGovernance(), currentGovernance); } function testFuzz_stranger_cannot_set_governance(address stranger) external { @@ -73,39 +258,612 @@ contract EmergencyProtectedTimelockUnitTests is Test { _timelock.setGovernance(makeAddr("newGovernance")); } - // EmergencyProtectedTimelock.transferExecutorOwnership() + // EmergencyProtectedTimelock.activateEmergencyMode() - function testFuzz_admin_executor_can_transfer_executor_ownership(address newOwner) external { - vm.assume(newOwner != _adminExecutor); - vm.assume(newOwner != address(0)); + function test_emergency_activator_can_activate_emergency_mode() external { + vm.prank(_emergencyActivator); + _timelock.activateEmergencyMode(); - Executor executor = new Executor(address(_timelock)); + assertEq(_isEmergencyStateActivated(), true); + } - assertEq(executor.owner(), address(_timelock)); + function testFuzz_stranger_cannot_activate_emergency_mode(address stranger) external { + vm.assume(stranger != _emergencyActivator); + vm.assume(stranger != address(0)); - vm.recordLogs(); + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyActivator.selector, stranger)); + _timelock.activateEmergencyMode(); + + assertEq(_isEmergencyStateActivated(), false); + } + + function test_cannot_activate_emergency_mode_if_already_active() external { + _activateEmergencyMode(); + + assertEq(_isEmergencyStateActivated(), true); + + vm.prank(_emergencyActivator); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [true, false]) + ); + _timelock.activateEmergencyMode(); + + assertEq(_isEmergencyStateActivated(), true); + } + + // EmergencyProtectedTimelock.emergencyExecute() + + function test_emergency_executior_can_execute_proposal() external { + _submitProposal(); + + assertEq(_timelock.getProposalsCount(), 1); + + vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + + _scheduleProposal(1); + + vm.warp(block.timestamp + _config.AFTER_SCHEDULE_DELAY()); + + _activateEmergencyMode(); + + assertEq(_isEmergencyStateActivated(), true); + + vm.prank(_emergencyEnactor); + _timelock.emergencyExecute(1); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Executed); + } + + function test_cannot_emergency_execute_proposal_if_mode_not_activated() external { + vm.startPrank(_dualGovernance); + _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls()); + + assertEq(_timelock.getProposalsCount(), 1); + + vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + _timelock.schedule(1); + + vm.warp(block.timestamp + _config.AFTER_SCHEDULE_DELAY()); + vm.stopPrank(); + + EmergencyState memory state = _timelock.getEmergencyState(); + assertEq(state.isEmergencyModeActivated, false); + + vm.prank(_emergencyActivator); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [false, true]) + ); + _timelock.emergencyExecute(1); + } + + function testFuzz_stranger_cannot_emergency_execute_proposal(address stranger) external { + vm.assume(stranger != _emergencyEnactor); + vm.assume(stranger != address(0)); + + _submitProposal(); + + assertEq(_timelock.getProposalsCount(), 1); + + vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + + _scheduleProposal(1); + + vm.warp(block.timestamp + _config.AFTER_SCHEDULE_DELAY()); + + _activateEmergencyMode(); + + assertEq(_isEmergencyStateActivated(), true); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, stranger)); + _timelock.emergencyExecute(1); + } + + // EmergencyProtectedTimelock.deactivateEmergencyMode() + + function test_admin_executor_can_deactivate_emergency_mode_if_delay_not_passed() external { + _activateEmergencyMode(); vm.prank(_adminExecutor); - _timelock.transferExecutorOwnership(address(executor), newOwner); + _timelock.deactivateEmergencyMode(); - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 1); - assertEq(entries[0].emitter, address(executor)); - assertEq(entries[0].topics[0], Ownable.OwnershipTransferred.selector); - assertEq(entries[0].topics[1], bytes32(uint256(uint160(address(_timelock))))); - assertEq(entries[0].topics[2], bytes32(uint256(uint160(newOwner)))); + assertEq(_isEmergencyStateActivated(), false); + } - // There is no data in the event (foundry bug??) - assertEq(bytes32(entries[0].data), bytes32(uint256(0))); + function test_after_deactivation_all_proposals_are_cancelled() external { + _submitProposal(); - assertEq(executor.owner(), newOwner); + assertEq(_timelock.getProposalsCount(), 1); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Submitted); + + _activateEmergencyMode(); + + _deactivateEmergencyMode(); + + proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Cancelled); } - function test_stranger_cannot_transfer_executor_ownership(address stranger) external { + function testFuzz_stranger_can_deactivate_emergency_mode_if_passed(address stranger) external { vm.assume(stranger != _adminExecutor); + _activateEmergencyMode(); + + EmergencyState memory state = _timelock.getEmergencyState(); + assertEq(_isEmergencyStateActivated(), true); + + vm.warp(state.emergencyModeEndsAfter + 1); + + vm.prank(stranger); + _timelock.deactivateEmergencyMode(); + + state = _timelock.getEmergencyState(); + assertEq(_isEmergencyStateActivated(), false); + } + + function testFuzz_cannot_deactivate_emergency_mode_if_not_activated(address stranger) external { + vm.assume(stranger != _adminExecutor); + + vm.prank(stranger); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [false, true]) + ); + _timelock.deactivateEmergencyMode(); + + vm.prank(_adminExecutor); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [false, true]) + ); + _timelock.deactivateEmergencyMode(); + } + + function testFuzz_stranger_cannot_deactivate_emergency_mode_if_not_passed(address stranger) external { + vm.assume(stranger != _adminExecutor); + + _activateEmergencyMode(); + assertEq(_isEmergencyStateActivated(), true); + vm.prank(stranger); vm.expectRevert(abi.encodeWithSelector(ConfigurationProvider.NotAdminExecutor.selector, stranger)); - _timelock.transferExecutorOwnership(_adminExecutor, makeAddr("newOwner")); + _timelock.deactivateEmergencyMode(); + } + + // EmergencyProtectedTimelock.emergencyReset() + + function test_execution_committee_can_emergency_reset() external { + _activateEmergencyMode(); + assertEq(_isEmergencyStateActivated(), true); + assertEq(_timelock.isEmergencyProtectionEnabled(), true); + + vm.prank(_emergencyEnactor); + _timelock.emergencyReset(); + + EmergencyState memory newState = _timelock.getEmergencyState(); + + assertEq(_isEmergencyStateActivated(), false); + assertEq(_timelock.getGovernance(), _emergencyGovernance); + assertEq(_timelock.isEmergencyProtectionEnabled(), false); + + assertEq(newState.activationCommittee, address(0)); + assertEq(newState.executionCommittee, address(0)); + assertEq(newState.protectedTill, 0); + assertEq(newState.emergencyModeDuration, 0); + assertEq(newState.emergencyModeEndsAfter, 0); + } + + function test_after_emergency_reset_all_proposals_are_cancelled() external { + _submitProposal(); + _activateEmergencyMode(); + + Proposal memory proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Submitted); + + vm.prank(_emergencyEnactor); + _timelock.emergencyReset(); + + proposal = _timelock.getProposal(1); + assert(proposal.status == Status.Cancelled); + } + + function testFuzz_stranger_cannot_emergency_reset_governance(address stranger) external { + vm.assume(stranger != _emergencyEnactor); + vm.assume(stranger != address(0)); + + _activateEmergencyMode(); + + assertEq(_isEmergencyStateActivated(), true); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, stranger)); + _timelock.emergencyReset(); + + assertEq(_isEmergencyStateActivated(), true); + } + + function test_cannot_emergency_reset_if_emergency_mode_not_activated() external { + assertEq(_isEmergencyStateActivated(), false); + + EmergencyState memory state = _timelock.getEmergencyState(); + + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [false, true]) + ); + vm.prank(_emergencyEnactor); + _timelock.emergencyReset(); + + EmergencyState memory newState = _timelock.getEmergencyState(); + + assertEq(newState.executionCommittee, state.executionCommittee); + assertEq(newState.activationCommittee, state.activationCommittee); + assertEq(newState.protectedTill, state.protectedTill); + assertEq(newState.emergencyModeEndsAfter, state.emergencyModeEndsAfter); + assertEq(newState.emergencyModeDuration, state.emergencyModeDuration); + assertEq(newState.isEmergencyModeActivated, state.isEmergencyModeActivated); + } + + // EmergencyProtectedTimelock.setEmergencyProtection() + + function test_admin_executor_can_set_emenrgency_protection() external { + EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + + vm.prank(_adminExecutor); + _localTimelock.setEmergencyProtection( + _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + ); + + EmergencyState memory state = _localTimelock.getEmergencyState(); + + assertEq(state.activationCommittee, _emergencyActivator); + assertEq(state.executionCommittee, _emergencyEnactor); + assertEq(state.protectedTill, block.timestamp + _emergencyProtectionDuration); + assertEq(state.emergencyModeDuration, _emergencyModeDuration); + assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.isEmergencyModeActivated, false); + } + + function testFuzz_stranger_cannot_set_emergency_protection(address stranger) external { + vm.assume(stranger != _adminExecutor); + vm.assume(stranger != address(0)); + + EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(ConfigurationProvider.NotAdminExecutor.selector, stranger)); + _localTimelock.setEmergencyProtection( + _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + ); + + EmergencyState memory state = _localTimelock.getEmergencyState(); + + assertEq(state.activationCommittee, address(0)); + assertEq(state.executionCommittee, address(0)); + assertEq(state.protectedTill, 0); + assertEq(state.emergencyModeDuration, 0); + assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.isEmergencyModeActivated, false); + } + + // EmergencyProtectedTimelock.isEmergencyProtectionEnabled() + + function test_is_emergency_protection_enabled_deactivate() external { + EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); + + vm.prank(_adminExecutor); + _localTimelock.setEmergencyProtection( + _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + ); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); + + vm.prank(_emergencyActivator); + _localTimelock.activateEmergencyMode(); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); + + vm.prank(_adminExecutor); + _localTimelock.deactivateEmergencyMode(); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); + } + + function test_is_emergency_protection_enabled_reset() external { + EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); + + vm.prank(_adminExecutor); + _localTimelock.setEmergencyProtection( + _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + ); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); + + vm.prank(_emergencyActivator); + _localTimelock.activateEmergencyMode(); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); + + vm.prank(_emergencyEnactor); + _localTimelock.emergencyReset(); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); + } + + // EmergencyProtectedTimelock.getEmergencyState() + + function test_get_emergency_state_deactivate() external { + EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + + EmergencyState memory state = _localTimelock.getEmergencyState(); + + assertEq(state.isEmergencyModeActivated, false); + assertEq(state.activationCommittee, address(0)); + assertEq(state.executionCommittee, address(0)); + assertEq(state.protectedTill, 0); + assertEq(state.emergencyModeDuration, 0); + assertEq(state.emergencyModeEndsAfter, 0); + + vm.prank(_adminExecutor); + _localTimelock.setEmergencyProtection( + _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + ); + + state = _localTimelock.getEmergencyState(); + + assertEq(_localTimelock.getEmergencyState().isEmergencyModeActivated, false); + assertEq(state.activationCommittee, _emergencyActivator); + assertEq(state.executionCommittee, _emergencyEnactor); + assertEq(state.protectedTill, block.timestamp + _emergencyProtectionDuration); + assertEq(state.emergencyModeDuration, _emergencyModeDuration); + assertEq(state.emergencyModeEndsAfter, 0); + + vm.prank(_emergencyActivator); + _localTimelock.activateEmergencyMode(); + + state = _localTimelock.getEmergencyState(); + + assertEq(_localTimelock.getEmergencyState().isEmergencyModeActivated, true); + assertEq(state.activationCommittee, _emergencyActivator); + assertEq(state.executionCommittee, _emergencyEnactor); + assertEq(state.protectedTill, block.timestamp + _emergencyProtectionDuration); + assertEq(state.emergencyModeDuration, _emergencyModeDuration); + assertEq(state.emergencyModeEndsAfter, block.timestamp + _emergencyModeDuration); + + vm.prank(_adminExecutor); + _localTimelock.deactivateEmergencyMode(); + + state = _localTimelock.getEmergencyState(); + + assertEq(state.isEmergencyModeActivated, false); + assertEq(state.activationCommittee, address(0)); + assertEq(state.executionCommittee, address(0)); + assertEq(state.protectedTill, 0); + assertEq(state.emergencyModeDuration, 0); + assertEq(state.emergencyModeEndsAfter, 0); + } + + function test_get_emergency_state_reset() external { + EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + + vm.prank(_adminExecutor); + _localTimelock.setEmergencyProtection( + _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration + ); + + vm.prank(_emergencyActivator); + _localTimelock.activateEmergencyMode(); + + vm.prank(_emergencyEnactor); + _localTimelock.emergencyReset(); + + EmergencyState memory state = _localTimelock.getEmergencyState(); + + assertEq(state.isEmergencyModeActivated, false); + assertEq(state.activationCommittee, address(0)); + assertEq(state.executionCommittee, address(0)); + assertEq(state.protectedTill, 0); + assertEq(state.emergencyModeDuration, 0); + assertEq(state.emergencyModeEndsAfter, 0); + } + + // EmergencyProtectedTimelock.getGovernance() + + function testFuzz_get_governance(address governance) external { + vm.assume(governance != address(0)); + vm.prank(_adminExecutor); + _timelock.setGovernance(governance); + assertEq(_timelock.getGovernance(), governance); + } + + // EmergencyProtectedTimelock.getProposal() + + function test_get_proposal() external { + assertEq(_timelock.getProposalsCount(), 0); + + vm.startPrank(_dualGovernance); + ExecutorCall[] memory executorCalls = _getTargetRegularStaffCalls(); + _timelock.submit(_adminExecutor, executorCalls); + _timelock.submit(_adminExecutor, executorCalls); + + Proposal memory submittedProposal = _timelock.getProposal(1); + + uint256 submitTimestamp = block.timestamp; + + assertEq(submittedProposal.id, 1); + assertEq(submittedProposal.executor, _adminExecutor); + assertEq(submittedProposal.submittedAt, submitTimestamp); + assertEq(submittedProposal.scheduledAt, 0); + assertEq(submittedProposal.executedAt, 0); + // assertEq doesn't support comparing enumerables so far + assert(submittedProposal.status == Status.Submitted); + assertEq(submittedProposal.calls.length, 1); + assertEq(submittedProposal.calls[0].value, executorCalls[0].value); + assertEq(submittedProposal.calls[0].target, executorCalls[0].target); + assertEq(submittedProposal.calls[0].payload, executorCalls[0].payload); + + vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + + _timelock.schedule(1); + uint256 scheduleTimestamp = block.timestamp; + + Proposal memory scheduledProposal = _timelock.getProposal(1); + + assertEq(scheduledProposal.id, 1); + assertEq(scheduledProposal.executor, _adminExecutor); + assertEq(scheduledProposal.submittedAt, submitTimestamp); + assertEq(scheduledProposal.scheduledAt, scheduleTimestamp); + assertEq(scheduledProposal.executedAt, 0); + // // assertEq doesn't support comparing enumerables so far + assert(scheduledProposal.status == Status.Scheduled); + assertEq(scheduledProposal.calls.length, 1); + assertEq(scheduledProposal.calls[0].value, executorCalls[0].value); + assertEq(scheduledProposal.calls[0].target, executorCalls[0].target); + assertEq(scheduledProposal.calls[0].payload, executorCalls[0].payload); + + vm.warp(block.timestamp + _config.AFTER_SCHEDULE_DELAY()); + + _timelock.execute(1); + + Proposal memory executedProposal = _timelock.getProposal(1); + uint256 executeTimestamp = block.timestamp; + + assertEq(executedProposal.id, 1); + assertEq(executedProposal.executor, _adminExecutor); + assertEq(executedProposal.submittedAt, submitTimestamp); + assertEq(executedProposal.scheduledAt, scheduleTimestamp); + assertEq(executedProposal.executedAt, executeTimestamp); + // assertEq doesn't support comparing enumerables so far + assert(executedProposal.status == Status.Executed); + assertEq(executedProposal.calls.length, 1); + assertEq(executedProposal.calls[0].value, executorCalls[0].value); + assertEq(executedProposal.calls[0].target, executorCalls[0].target); + assertEq(executedProposal.calls[0].payload, executorCalls[0].payload); + + _timelock.cancelAllNonExecutedProposals(); + + Proposal memory cancelledProposal = _timelock.getProposal(2); + + assertEq(cancelledProposal.id, 2); + assertEq(cancelledProposal.executor, _adminExecutor); + assertEq(cancelledProposal.submittedAt, submitTimestamp); + assertEq(cancelledProposal.scheduledAt, 0); + assertEq(cancelledProposal.executedAt, 0); + // assertEq doesn't support comparing enumerables so far + assert(cancelledProposal.status == Status.Cancelled); + assertEq(cancelledProposal.calls.length, 1); + assertEq(cancelledProposal.calls[0].value, executorCalls[0].value); + assertEq(cancelledProposal.calls[0].target, executorCalls[0].target); + assertEq(cancelledProposal.calls[0].payload, executorCalls[0].payload); + } + + function test_get_not_existing_proposal() external { + assertEq(_timelock.getProposalsCount(), 0); + + vm.expectRevert(); + _timelock.getProposal(1); + } + + // EmergencyProtectedTimelock.getProposalsCount() + + function testFuzz_get_proposals_count(uint256 count) external { + vm.assume(count > 0); + vm.assume(count <= type(uint8).max); + assertEq(_timelock.getProposalsCount(), 0); + + for (uint256 i = 1; i <= count; i++) { + _submitProposal(); + assertEq(_timelock.getProposalsCount(), i); + } + } + + // EmergencyProtectedTimelock.canExecute() + + function test_can_execute() external { + assertEq(_timelock.canExecute(1), false); + + _submitProposal(); + + Proposal memory proposal = _timelock.getProposal(1); + + assertEq(_timelock.canExecute(1), false); + + vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + + _scheduleProposal(1); + + assertEq(_timelock.canExecute(1), false); + + vm.warp(block.timestamp + _config.AFTER_SCHEDULE_DELAY()); + + assertEq(_timelock.canExecute(1), true); + + vm.prank(_dualGovernance); + _timelock.cancelAllNonExecutedProposals(); + + assertEq(_timelock.canExecute(1), false); + } + + // EmergencyProtectedTimelock.canSchedule() + + function test_can_schedule() external { + assertEq(_timelock.canExecute(1), false); + + _submitProposal(); + + Proposal memory proposal = _timelock.getProposal(1); + + assertEq(_timelock.canSchedule(1), false); + + vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + + assertEq(_timelock.canSchedule(1), true); + + _scheduleProposal(1); + + assertEq(_timelock.canSchedule(1), false); + + vm.prank(_dualGovernance); + _timelock.cancelAllNonExecutedProposals(); + + assertEq(_timelock.canSchedule(1), false); + } + + // Utils + + function _submitProposal() internal { + vm.prank(_dualGovernance); + _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls()); + } + + function _scheduleProposal(uint256 proposalId) internal { + vm.prank(_dualGovernance); + _timelock.schedule(proposalId); + } + + function _isEmergencyStateActivated() internal view returns (bool) { + EmergencyState memory state = _timelock.getEmergencyState(); + return state.isEmergencyModeActivated; + } + + function _activateEmergencyMode() internal { + vm.prank(_emergencyActivator); + _timelock.activateEmergencyMode(); + assertEq(_isEmergencyStateActivated(), true); + } + + function _deactivateEmergencyMode() internal { + vm.prank(_adminExecutor); + _timelock.deactivateEmergencyMode(); + assertEq(_isEmergencyStateActivated(), false); + } + + function _getTargetRegularStaffCalls() internal view returns (ExecutorCall[] memory) { + return ExecutorCallHelpers.create(address(_targetMock), abi.encodeCall(IDangerousContract.doRegularStaff, (42))); } } diff --git a/test/utils/interfaces.sol b/test/utils/interfaces.sol index 2d3c56f3..40cd495f 100644 --- a/test/utils/interfaces.sol +++ b/test/utils/interfaces.sol @@ -106,3 +106,9 @@ interface IWithdrawalQueue is IERC721 { function hasRole(bytes32 role, address account) external view returns (bool); function isPaused() external view returns (bool); } + +interface IDangerousContract { + function doRegularStaff(uint256 magic) external; + function doRugPool() external; + function doControversialStaff() external; +} \ No newline at end of file diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index c2e711d7..6ccbcde2 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -26,7 +26,7 @@ import {DualGovernance, DualGovernanceState, State} from "contracts/DualGovernan import {Proposal, Status as ProposalStatus} from "contracts/libraries/Proposals.sol"; import {Percents, percents} from "../utils/percents.sol"; -import {IERC20, IStEth, IWstETH, IWithdrawalQueue, WithdrawalRequestStatus} from "../utils/interfaces.sol"; +import {IERC20, IStEth, IWstETH, IWithdrawalQueue, WithdrawalRequestStatus, IDangerousContract} from "../utils/interfaces.sol"; import {ExecutorCallHelpers} from "../utils/executor-calls.sol"; import {Utils, TargetMock, console} from "../utils/utils.sol"; @@ -47,12 +47,6 @@ function countDigits(uint256 number) pure returns (uint256 digitsCount) { } while (number / 10 != 0); } -interface IDangerousContract { - function doRegularStaff(uint256 magic) external; - function doRugPool() external; - function doControversialStaff() external; -} - contract ScenarioTestBlueprint is Test { address internal immutable _ADMIN_PROPOSER = DAO_VOTING; uint256 internal immutable _EMERGENCY_MODE_DURATION = 180 days; @@ -374,7 +368,7 @@ contract ScenarioTestBlueprint is Test { } function _assertProposalCanceled(uint256 proposalId) internal { - assertEq(_timelock.getProposal(proposalId).status, ProposalStatus.Canceled, "Proposal not in 'Canceled' state"); + assertEq(_timelock.getProposal(proposalId).status, ProposalStatus.Cancelled, "Proposal not in 'Canceled' state"); } function _assertNormalState() internal { From e852305f1f5a4e078ecc70d26cd1a9250e9d298b Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 24 Apr 2024 20:42:08 +0400 Subject: [PATCH 053/134] Simplify the canScheduleProposal() check --- contracts/libraries/DualGovernanceState.sol | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol index 1246f37a..a22698dd 100644 --- a/contracts/libraries/DualGovernanceState.sol +++ b/contracts/libraries/DualGovernanceState.sol @@ -220,10 +220,6 @@ library DualGovernanceStateTransitions { self.vetoSignallingReactivationTime = timestamp; } - if (oldState == State.VetoCooldown && newState == State.Normal) { - self.vetoSignallingActivationTime = 0; - } - if (newState == State.RageQuit) { IEscrow signallingEscrow = self.signallingEscrow; signallingEscrow.startRageQuit( @@ -346,10 +342,12 @@ library DualGovernanceStateViews { DualGovernanceState storage self, uint256 proposalSubmissionTime ) internal view returns (bool) { - if (!isProposalsAdoptionAllowed(self)) { - return false; + State state = self.state; + if (state == State.Normal) return true; + if (state == State.VetoCooldown) { + return proposalSubmissionTime <= self.vetoSignallingActivationTime; } - return self.vetoSignallingActivationTime == 0 || proposalSubmissionTime <= self.vetoSignallingActivationTime; + return false; } function isProposalsCreationAllowed(DualGovernanceState storage self) internal view returns (bool) { From 066810d3ca52b01f3eb6172c1a7ff9b498b0c9ad Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 24 Apr 2024 22:40:25 +0400 Subject: [PATCH 054/134] Update the mechanism doc --- docs/mechanism.md | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index 82389b25..f6e23f4a 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -164,8 +164,6 @@ The **time of activation** of the Veto Signalling state $t^S_{act}$ is the time The **time of re-activation** of the Veto Signalling state $t^S_{react}$ is the time the Deactivation sub-state was last exited without exiting the parent Veto Signalling state. -The **time of last proposal submission** $t_{prop}$ is the last time a proposal was submitted to the DG subsystem. - The **dynamic timelock duration** $T_{lock}(R)$ depends on the current rage quit support $R = R(t)$ and can be calculated as follows: ```math @@ -200,7 +198,7 @@ the Veto Signalling state is exited and the Rage Quit state is entered. **Transition to Deactivation**. If, while Veto Signalling is active and the Deactivation sub-state is not active, the following expression becomes true: ```math -\left( t - \max \left\{ t^S_{act} \,,\, t_{prop} \right\} > T_{lock}(R) \right) \, \land \, \left( t - \max \left\{ t^S_{act} \,,\, t^S_{react} \right\} > T^{Sa}_{min} \right) +\left( t - t^S_{act} > T_{lock}(R) \right) \, \land \, \left( t - \max \left\{ t^S_{act} \,,\, t^S_{react} \right\} > T^{Sa}_{min} \right) ``` where $T^{Sa}_{min}$ is `VetoSignallingMinActiveDuration`, then the Deactivation sub-state of the Veto Signalling state is entered without exiting the parent Veto Signalling state. @@ -222,7 +220,7 @@ The sub-state's purpose is to allow all stakers to observe the Veto Signalling b **Transition to the parent state**. If, while the sub-state is active, the following condition becomes true: ```math -\big( t - \max \left\{ t^S_{act} \,,\, t_{prop} \right\} \leq \, T_{lock}(R) \big) \,\lor\, \big( R > R_2 \big) +\big( t - t^S_{act} \leq \, T_{lock}(R) \big) \,\lor\, \big( R > R_2 \big) ``` then the Deactivation sub-state is exited so only the parent Veto Signalling state remains active. @@ -242,7 +240,7 @@ VetoSignallingDeactivationMaxDuration = 3 days ### Veto Cooldown state -In the Veto Cooldown state, the DAO cannot submit proposals to the DG but can execute pending non-cancelled proposals, provided that the proposal being executed was submitted more than `ProposalExecutionMinTimelock` days ago. This state exists to guarantee that no staker possessing enough stETH to generate `FirstSealRageQuitSupport` can lock the governance indefinitely without rage quitting the protocol. +In the Veto Cooldown state, the DAO cannot submit proposals to the DG but can execute pending non-canceled proposals, provided that the proposal being executed was submitted more than `ProposalExecutionMinTimelock` days ago and before the Veto Signalling state was entered the last time. This state exists to guarantee that no staker possessing enough stETH to generate `FirstSealRageQuitSupport` can lock the governance indefinitely without rage quitting the protocol. **Transition to Veto Signalling**. If, while the state is active, the following condition becomes true: @@ -397,6 +395,26 @@ Dual governance should not cover: ## Changelog +### 2024-04-24 + +* Removed the logic with the extension of the Veto Signalling duration upon new proposal submission. +* Added new condition for proposal execution in the Veto Cooldown state: the proposal must be submitted before the Veto Signalling state was entered for the last time. + + > This change simplifies the transition conditions for Veto Signalling and mitigates the main disadvantage of the extendable Veto Signalling approach: the potentially "unexpected" transition from Veto Signalling to the Rage Quit state. + A possible scenario how such transition may be triggered: + > - A malicious DAO submits a "non-malicious" proposal and transitions the DG into the Veto Signalling state, locking the `SecondSealRageQuitSupport` amount of the funds. + > - Before the end of the `DynamicTimelockMaxDuration`, the malicious DAO submits a malicious proposal and withdraws funds from the Veto Signalling Escrow. + > + > Previously, the submission of any proposal during the Veto Signalling phase would prolong this phase, allowing up to `DynamicTimelockMaxDuration` for users to gather the `SecondSealRageQuitSupport` amount. However, the transition to the Rage Quit state may occur before the `DynamicTimelockMaxDuration` time has passed, as soon as the `SecondSealRageQuitSupport` is accumulated. This potentially decreases the number of users who may join the initiated Rage Quit round but still allows for the accumulation of stETH in the veto signalling escrow contract to begin a new Rage Quit after the current one ends. + > + > With the new approach, after the submission of a malicious proposal, it will not be executable after the end of the Veto Signalling initiated by the malicious DAO due to the addition of new restriction on proposals that may be executed in the Veto Cooldown state. + > + > In the new design, users will still have a `max(SignallingEscrowMinLockTime, VetoSignallingDeactivationMaxDuration + VetoCooldownDuration)` to accumulate enough funds to transfer the system into the Veto Signalling state and eventually trigger the Rage Quit. In contrast to the previous approach, the Rage Quit will not start before the `DynamicTimelockMaxDuration` time has passed, allowing more users to join the Rage Quit. + > + > A notable limitation identified with this new approach is the potential to double the maximum delay of proposal execution, facilitated by the combined duration of `DynamicTimelockMaxDuration` and `VetoDeactivationMaxDuration`. A malicious actor with a significant portion of the stETH total supply may lock at least the `FirstSealRageQuitSupport` in the Veto Signalling Escrow just before the proposal is submitted to DualGovernance. After this, the submitted proposal will be affected by two sequences of transitions: Veto Signalling -> Veto Signalling Deactivation -> Veto Cooldown. Such an action may effectively increase the duration of the DualGovernance lock with the same amount of funds. Therefore, it must be considered during the parameter selection process. + > + > Considering the limitations and benefits of both approaches, the newer model was chosen as it provides better guarantees for Lido stakers to exit the protocol during Rage Quit, a more crucial factor than the potential delay in proposal execution, which can be partially mitigated by optimizing system parameters. + ### 2024-04-19 * Replaced the dynamic Gate Seal pause mechanism with the Reseal Committee. From 651c026807bd88a32b1b55f819710c9fde685ae7 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 25 Apr 2024 11:08:33 +0300 Subject: [PATCH 055/134] 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 056/134] 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 057/134] 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 058/134] 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 059/134] 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 060/134] 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 061/134] 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 062/134] 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 063/134] 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 6b7bd1185b907707071719280a325cbaa6f03e2c Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Thu, 2 May 2024 15:21:42 +0700 Subject: [PATCH 064/134] feat: add unit test util --- test/unit/EmergencyProtectedTimelock.t.sol | 55 +++++++++------------- test/utils/unit-test.sol | 11 +++++ 2 files changed, 34 insertions(+), 32 deletions(-) create mode 100644 test/utils/unit-test.sol diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 3179fe0d..9e10ef98 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {Test, Vm} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Test.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Executor} from "contracts/Executor.sol"; @@ -12,11 +12,12 @@ import {Executor} from "contracts/Executor.sol"; import {Proposal, Proposals, ExecutorCall, Status} from "contracts/libraries/Proposals.sol"; import {EmergencyProtection, EmergencyState} from "contracts/libraries/EmergencyProtection.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; import {TargetMock} from "test/utils/utils.sol"; import {ExecutorCallHelpers} from "test/utils/executor-calls.sol"; import {IDangerousContract} from "test/utils/interfaces.sol"; -contract EmergencyProtectedTimelockUnitTests is Test { +contract EmergencyProtectedTimelockUnitTests is UnitTest { EmergencyProtectedTimelock private _timelock; Configuration private _config; TargetMock private _targetMock; @@ -78,7 +79,7 @@ contract EmergencyProtectedTimelockUnitTests is Test { assertEq(_timelock.getProposalsCount(), 1); - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + _wait(_config.AFTER_SUBMIT_DELAY()); _scheduleProposal(1); @@ -111,11 +112,11 @@ contract EmergencyProtectedTimelockUnitTests is Test { _submitProposal(); assertEq(_timelock.getProposalsCount(), 1); - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + _wait(_config.AFTER_SUBMIT_DELAY()); _scheduleProposal(1); - vm.warp(block.timestamp + _config.AFTER_SCHEDULE_DELAY()); + _wait(_config.AFTER_SCHEDULE_DELAY()); vm.prank(stranger); _timelock.execute(1); @@ -129,10 +130,10 @@ contract EmergencyProtectedTimelockUnitTests is Test { assertEq(_timelock.getProposalsCount(), 1); - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + _wait(_config.AFTER_SUBMIT_DELAY()); _scheduleProposal(1); - vm.warp(block.timestamp + _config.AFTER_SCHEDULE_DELAY()); + _wait(_config.AFTER_SCHEDULE_DELAY()); _activateEmergencyMode(); @@ -148,14 +149,12 @@ contract EmergencyProtectedTimelockUnitTests is Test { // EmergencyProtectedTimelock.cancelAllNonExecutedProposals() function test_governance_can_cancel_all_non_executed_proposals() external { - ExecutorCall[] memory executorCalls = _getTargetRegularStaffCalls(); - _submitProposal(); _submitProposal(); assertEq(_timelock.getProposalsCount(), 2); - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + _wait(_config.AFTER_SUBMIT_DELAY()); _scheduleProposal(1); @@ -299,11 +298,11 @@ contract EmergencyProtectedTimelockUnitTests is Test { assertEq(_timelock.getProposalsCount(), 1); - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + _wait(_config.AFTER_SUBMIT_DELAY()); _scheduleProposal(1); - vm.warp(block.timestamp + _config.AFTER_SCHEDULE_DELAY()); + _wait(_config.AFTER_SCHEDULE_DELAY()); _activateEmergencyMode(); @@ -322,10 +321,10 @@ contract EmergencyProtectedTimelockUnitTests is Test { assertEq(_timelock.getProposalsCount(), 1); - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + _wait(_config.AFTER_SUBMIT_DELAY()); _timelock.schedule(1); - vm.warp(block.timestamp + _config.AFTER_SCHEDULE_DELAY()); + _wait(_config.AFTER_SCHEDULE_DELAY()); vm.stopPrank(); EmergencyState memory state = _timelock.getEmergencyState(); @@ -346,11 +345,11 @@ contract EmergencyProtectedTimelockUnitTests is Test { assertEq(_timelock.getProposalsCount(), 1); - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + _wait(_config.AFTER_SUBMIT_DELAY()); _scheduleProposal(1); - vm.warp(block.timestamp + _config.AFTER_SCHEDULE_DELAY()); + _wait(_config.AFTER_SCHEDULE_DELAY()); _activateEmergencyMode(); @@ -396,7 +395,7 @@ contract EmergencyProtectedTimelockUnitTests is Test { EmergencyState memory state = _timelock.getEmergencyState(); assertEq(_isEmergencyStateActivated(), true); - vm.warp(state.emergencyModeEndsAfter + 1); + _wait(state.emergencyModeDuration + 1); vm.prank(stranger); _timelock.deactivateEmergencyMode(); @@ -707,7 +706,7 @@ contract EmergencyProtectedTimelockUnitTests is Test { assertEq(submittedProposal.calls[0].target, executorCalls[0].target); assertEq(submittedProposal.calls[0].payload, executorCalls[0].payload); - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + _wait(_config.AFTER_SUBMIT_DELAY()); _timelock.schedule(1); uint256 scheduleTimestamp = block.timestamp; @@ -726,7 +725,7 @@ contract EmergencyProtectedTimelockUnitTests is Test { assertEq(scheduledProposal.calls[0].target, executorCalls[0].target); assertEq(scheduledProposal.calls[0].payload, executorCalls[0].payload); - vm.warp(block.timestamp + _config.AFTER_SCHEDULE_DELAY()); + _wait(_config.AFTER_SCHEDULE_DELAY()); _timelock.execute(1); @@ -786,20 +785,16 @@ contract EmergencyProtectedTimelockUnitTests is Test { function test_can_execute() external { assertEq(_timelock.canExecute(1), false); - - _submitProposal(); - - Proposal memory proposal = _timelock.getProposal(1); - + _submitProposal(); assertEq(_timelock.canExecute(1), false); - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + _wait(_config.AFTER_SUBMIT_DELAY()); _scheduleProposal(1); assertEq(_timelock.canExecute(1), false); - vm.warp(block.timestamp + _config.AFTER_SCHEDULE_DELAY()); + _wait(_config.AFTER_SCHEDULE_DELAY()); assertEq(_timelock.canExecute(1), true); @@ -813,14 +808,10 @@ contract EmergencyProtectedTimelockUnitTests is Test { function test_can_schedule() external { assertEq(_timelock.canExecute(1), false); - - _submitProposal(); - - Proposal memory proposal = _timelock.getProposal(1); - + _submitProposal(); assertEq(_timelock.canSchedule(1), false); - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY()); + _wait(_config.AFTER_SUBMIT_DELAY()); assertEq(_timelock.canSchedule(1), true); diff --git a/test/utils/unit-test.sol b/test/utils/unit-test.sol new file mode 100644 index 00000000..6fdb5cfd --- /dev/null +++ b/test/utils/unit-test.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// solhint-disable-next-line +import {Test} from "forge-std/Test.sol"; + +contract UnitTest is Test { + function _wait(uint256 duration) internal { + vm.warp(block.timestamp + duration); + } +} \ No newline at end of file From 51ea69138d5520612228cd05c4d9169bd4e57cfb Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Thu, 2 May 2024 17:26:45 +0700 Subject: [PATCH 065/134] feat: add tests for EmergencyProtection library --- contracts/libraries/EmergencyProtection.sol | 14 +- test/unit/EmergencyProtectedTimelock.t.sol | 10 +- test/unit/libraries/EmergencyProtection.t.sol | 391 ++++++++++++++++++ 3 files changed, 402 insertions(+), 13 deletions(-) create mode 100644 test/unit/libraries/EmergencyProtection.t.sol diff --git a/contracts/libraries/EmergencyProtection.sol b/contracts/libraries/EmergencyProtection.sol index 758525e1..bbbee390 100644 --- a/contracts/libraries/EmergencyProtection.sol +++ b/contracts/libraries/EmergencyProtection.sol @@ -15,13 +15,11 @@ struct EmergencyState { library EmergencyProtection { error NotEmergencyActivator(address account); error NotEmergencyEnactor(address account); - error EmergencyPeriodFinished(); - error EmergencyCommitteeExpired(); + error EmergencyCommitteeExpired(uint256 timestamp, uint256 protectedTill); error InvalidEmergencyModeActiveValue(bool actual, bool expected); - event EmergencyModeActivated(); - event EmergencyModeDeactivated(); - event EmergencyGovernanceReset(); + event EmergencyModeActivated(uint256 timestamp); + event EmergencyModeDeactivated(uint256 timestamp); event EmergencyActivationCommitteeSet(address indexed activationCommittee); event EmergencyExecutionCommitteeSet(address indexed executionCommittee); event EmergencyModeDurationSet(uint256 emergencyModeDuration); @@ -74,10 +72,10 @@ library EmergencyProtection { function activate(State storage self) internal { if (block.timestamp > self.protectedTill) { - revert EmergencyCommitteeExpired(); + revert EmergencyCommitteeExpired(block.timestamp, self.protectedTill); } self.emergencyModeEndsAfter = SafeCast.toUint40(block.timestamp + self.emergencyModeDuration); - emit EmergencyModeActivated(); + emit EmergencyModeActivated(block.timestamp); } function deactivate(State storage self) internal { @@ -86,7 +84,7 @@ library EmergencyProtection { self.protectedTill = 0; self.emergencyModeDuration = 0; self.emergencyModeEndsAfter = 0; - emit EmergencyModeDeactivated(); + emit EmergencyModeDeactivated(block.timestamp); } function getEmergencyState(State storage self) internal view returns (EmergencyState memory res) { diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 9e10ef98..a3a97f15 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -550,7 +550,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_is_emergency_protection_enabled_deactivate() external { EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); - + assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); vm.prank(_adminExecutor); @@ -573,7 +573,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_is_emergency_protection_enabled_reset() external { EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); - + assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); vm.prank(_adminExecutor); @@ -600,7 +600,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); EmergencyState memory state = _localTimelock.getEmergencyState(); - + assertEq(state.isEmergencyModeActivated, false); assertEq(state.activationCommittee, address(0)); assertEq(state.executionCommittee, address(0)); @@ -785,7 +785,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_can_execute() external { assertEq(_timelock.canExecute(1), false); - _submitProposal(); + _submitProposal(); assertEq(_timelock.canExecute(1), false); _wait(_config.AFTER_SUBMIT_DELAY()); @@ -808,7 +808,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_can_schedule() external { assertEq(_timelock.canExecute(1), false); - _submitProposal(); + _submitProposal(); assertEq(_timelock.canSchedule(1), false); _wait(_config.AFTER_SUBMIT_DELAY()); diff --git a/test/unit/libraries/EmergencyProtection.t.sol b/test/unit/libraries/EmergencyProtection.t.sol new file mode 100644 index 00000000..8f05b257 --- /dev/null +++ b/test/unit/libraries/EmergencyProtection.t.sol @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Test, Vm} from "forge-std/Test.sol"; + +import {EmergencyProtection, EmergencyState} from "contracts/libraries/EmergencyProtection.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; + +contract EmergencyProtectionUnitTests is UnitTest { + EmergencyProtection.State internal _emergencyProtection; + + function testFuzz_setup_emergency_protection( + address activationCommittee, + address executionCommittee, + uint256 protectedTill, + uint256 duration + ) external { + vm.assume(protectedTill > 0 && protectedTill < type(uint40).max); + vm.assume(duration > 0 && duration < type(uint32).max); + vm.assume(activationCommittee != address(0)); + vm.assume(executionCommittee != address(0)); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyActivationCommitteeSet(activationCommittee); + vm.expectEmit(); + emit EmergencyProtection.EmergencyExecutionCommitteeSet(executionCommittee); + vm.expectEmit(); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + protectedTill); + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeDurationSet(duration); + + vm.recordLogs(); + + EmergencyProtection.setup( + _emergencyProtection, activationCommittee, executionCommittee, protectedTill, duration + ); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 4); + + assertEq(_emergencyProtection.activationCommittee, activationCommittee); + assertEq(_emergencyProtection.executionCommittee, executionCommittee); + assertEq(_emergencyProtection.protectedTill, block.timestamp + protectedTill); + assertEq(_emergencyProtection.emergencyModeDuration, duration); + assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + } + + function test_setup_same_activation_committee() external { + address activationCommittee = makeAddr("activationCommittee"); + + EmergencyProtection.setup(_emergencyProtection, activationCommittee, address(0x2), 100, 100); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x3)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + 200); + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeDurationSet(200); + + vm.recordLogs(); + EmergencyProtection.setup(_emergencyProtection, activationCommittee, address(0x3), 200, 200); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 3); + + assertEq(_emergencyProtection.activationCommittee, activationCommittee); + assertEq(_emergencyProtection.executionCommittee, address(0x3)); + assertEq(_emergencyProtection.protectedTill, block.timestamp + 200); + assertEq(_emergencyProtection.emergencyModeDuration, 200); + assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + } + + function test_setup_same_execution_committee() external { + address executionCommittee = makeAddr("executionCommittee"); + + EmergencyProtection.setup(_emergencyProtection, address(0x1), executionCommittee, 100, 100); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x2)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + 200); + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeDurationSet(200); + + vm.recordLogs(); + EmergencyProtection.setup(_emergencyProtection, address(0x2), executionCommittee, 200, 200); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 3); + + assertEq(_emergencyProtection.activationCommittee, address(0x2)); + assertEq(_emergencyProtection.executionCommittee, executionCommittee); + assertEq(_emergencyProtection.protectedTill, block.timestamp + 200); + assertEq(_emergencyProtection.emergencyModeDuration, 200); + assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + } + + function test_setup_same_protected_till() external { + EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), 100, 100); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x4)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeDurationSet(200); + + vm.recordLogs(); + EmergencyProtection.setup(_emergencyProtection, address(0x3), address(0x4), 100, 200); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 3); + + assertEq(_emergencyProtection.activationCommittee, address(0x3)); + assertEq(_emergencyProtection.executionCommittee, address(0x4)); + assertEq(_emergencyProtection.protectedTill, block.timestamp + 100); + assertEq(_emergencyProtection.emergencyModeDuration, 200); + assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + } + + function test_setup_same_emergency_mode_duration() external { + EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), 100, 100); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x4)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + 200); + + vm.recordLogs(); + EmergencyProtection.setup(_emergencyProtection, address(0x3), address(0x4), 200, 100); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 3); + + assertEq(_emergencyProtection.activationCommittee, address(0x3)); + assertEq(_emergencyProtection.executionCommittee, address(0x4)); + assertEq(_emergencyProtection.protectedTill, block.timestamp + 200); + assertEq(_emergencyProtection.emergencyModeDuration, 100); + assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + } + + function test_activate_emergency_mode() external { + EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), 100, 100); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeActivated(block.timestamp); + + vm.recordLogs(); + + EmergencyProtection.activate(_emergencyProtection); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1); + assertEq(_emergencyProtection.emergencyModeEndsAfter, block.timestamp + 100); + } + + function test_cannot_activate_emergency_mode_if_protected_till_expired() external { + uint256 protectedTill = 100; + EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), protectedTill, 100); + + _wait(protectedTill + 1); + + vm.expectRevert( + abi.encodeWithSelector( + EmergencyProtection.EmergencyCommitteeExpired.selector, + [block.timestamp, _emergencyProtection.protectedTill] + ) + ); + EmergencyProtection.activate(_emergencyProtection); + } + + function testFuzz_deactivate_emergency_mode( + address activationCommittee, + address executionCommittee, + uint256 protectedTill, + uint256 duration + ) external { + vm.assume(protectedTill > 0 && protectedTill < type(uint40).max); + vm.assume(duration > 0 && duration < type(uint32).max); + vm.assume(activationCommittee != address(0)); + vm.assume(executionCommittee != address(0)); + + EmergencyProtection.setup( + _emergencyProtection, activationCommittee, executionCommittee, protectedTill, duration + ); + EmergencyProtection.activate(_emergencyProtection); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeDeactivated(block.timestamp); + + vm.recordLogs(); + + EmergencyProtection.deactivate(_emergencyProtection); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 1); + + assertEq(_emergencyProtection.activationCommittee, address(0)); + assertEq(_emergencyProtection.executionCommittee, address(0)); + assertEq(_emergencyProtection.protectedTill, 0); + assertEq(_emergencyProtection.emergencyModeDuration, 0); + assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + } + + function test_get_emergency_state() external { + EmergencyState memory state = EmergencyProtection.getEmergencyState(_emergencyProtection); + + assertEq(state.activationCommittee, address(0)); + assertEq(state.executionCommittee, address(0)); + assertEq(state.protectedTill, 0); + assertEq(state.emergencyModeDuration, 0); + assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.isEmergencyModeActivated, false); + + EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), 100, 100); + + state = EmergencyProtection.getEmergencyState(_emergencyProtection); + + assertEq(state.activationCommittee, address(0x1)); + assertEq(state.executionCommittee, address(0x2)); + assertEq(state.protectedTill, block.timestamp + 100); + assertEq(state.emergencyModeDuration, 100); + assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.isEmergencyModeActivated, false); + + EmergencyProtection.activate(_emergencyProtection); + + state = EmergencyProtection.getEmergencyState(_emergencyProtection); + + assertEq(state.activationCommittee, address(0x1)); + assertEq(state.executionCommittee, address(0x2)); + assertEq(state.protectedTill, block.timestamp + 100); + assertEq(state.emergencyModeDuration, 100); + assertEq(state.emergencyModeEndsAfter, block.timestamp + 100); + assertEq(state.isEmergencyModeActivated, true); + + EmergencyProtection.deactivate(_emergencyProtection); + + state = EmergencyProtection.getEmergencyState(_emergencyProtection); + + assertEq(state.activationCommittee, address(0)); + assertEq(state.executionCommittee, address(0)); + assertEq(state.protectedTill, 0); + assertEq(state.emergencyModeDuration, 0); + assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.isEmergencyModeActivated, false); + } + + function test_is_emergency_mode_activated() external { + assertEq(EmergencyProtection.isEmergencyModeActivated(_emergencyProtection), false); + + EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), 100, 100); + + assertEq(EmergencyProtection.isEmergencyModeActivated(_emergencyProtection), false); + + EmergencyProtection.activate(_emergencyProtection); + + assertEq(EmergencyProtection.isEmergencyModeActivated(_emergencyProtection), true); + + EmergencyProtection.deactivate(_emergencyProtection); + + assertEq(EmergencyProtection.isEmergencyModeActivated(_emergencyProtection), false); + } + + function test_is_emergency_mode_passed() external { + assertEq(EmergencyProtection.isEmergencyModePassed(_emergencyProtection), false); + + uint256 duration = 100; + + EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), 100, duration); + + assertEq(EmergencyProtection.isEmergencyModePassed(_emergencyProtection), false); + + EmergencyProtection.activate(_emergencyProtection); + + assertEq(EmergencyProtection.isEmergencyModePassed(_emergencyProtection), false); + + _wait(duration + 1); + + assertEq(EmergencyProtection.isEmergencyModePassed(_emergencyProtection), true); + + EmergencyProtection.deactivate(_emergencyProtection); + + assertEq(EmergencyProtection.isEmergencyModePassed(_emergencyProtection), false); + } + + function test_is_emergency_protection_enabled() external { + uint256 protectedTill = 100; + uint256 duration = 100; + + assertEq(EmergencyProtection.isEmergencyProtectionEnabled(_emergencyProtection), false); + + EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), protectedTill, duration); + + assertEq(EmergencyProtection.isEmergencyProtectionEnabled(_emergencyProtection), true); + + _wait(protectedTill - block.timestamp); + + EmergencyProtection.activate(_emergencyProtection); + + _wait(duration); + + assertEq(EmergencyProtection.isEmergencyProtectionEnabled(_emergencyProtection), true); + + _wait(100); + + assertEq(EmergencyProtection.isEmergencyProtectionEnabled(_emergencyProtection), true); + + EmergencyProtection.deactivate(_emergencyProtection); + + assertEq(EmergencyProtection.isEmergencyProtectionEnabled(_emergencyProtection), false); + } + + function testFuzz_check_activation_committee(address committee, address stranger) external { + vm.assume(committee != address(0)); + vm.assume(stranger != address(0) && stranger != committee); + + vm.expectRevert( + abi.encodeWithSelector( + EmergencyProtection.NotEmergencyActivator.selector, + [stranger] + ) + ); + EmergencyProtection.checkActivationCommittee(_emergencyProtection, stranger); + EmergencyProtection.checkActivationCommittee(_emergencyProtection, address(0)); + + EmergencyProtection.setup(_emergencyProtection, committee, address(0x2), 100, 100); + + EmergencyProtection.checkActivationCommittee(_emergencyProtection, committee); + + vm.expectRevert( + abi.encodeWithSelector( + EmergencyProtection.NotEmergencyActivator.selector, + [stranger] + ) + ); + EmergencyProtection.checkActivationCommittee(_emergencyProtection, stranger); + } + + function testFuzz_check_execution_committee(address committee, address stranger) external { + vm.assume(committee != address(0)); + vm.assume(stranger != address(0) && stranger != committee); + + vm.expectRevert( + abi.encodeWithSelector( + EmergencyProtection.NotEmergencyEnactor.selector, + [stranger] + ) + ); + EmergencyProtection.checkExecutionCommittee(_emergencyProtection, stranger); + EmergencyProtection.checkExecutionCommittee(_emergencyProtection, address(0)); + + EmergencyProtection.setup(_emergencyProtection, address(0x1), committee, 100, 100); + + EmergencyProtection.checkExecutionCommittee(_emergencyProtection, committee); + + vm.expectRevert( + abi.encodeWithSelector( + EmergencyProtection.NotEmergencyEnactor.selector, + [stranger] + ) + ); + EmergencyProtection.checkExecutionCommittee(_emergencyProtection, stranger); + } + + function test_check_emergency_mode_active() external { + vm.expectRevert( + abi.encodeWithSelector( + EmergencyProtection.InvalidEmergencyModeActiveValue.selector, + [false, true] + ) + ); + EmergencyProtection.checkEmergencyModeActive(_emergencyProtection, true); + EmergencyProtection.checkEmergencyModeActive(_emergencyProtection, false); + + EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), 100, 100); + EmergencyProtection.activate(_emergencyProtection); + + EmergencyProtection.checkEmergencyModeActive(_emergencyProtection, true); + vm.expectRevert( + abi.encodeWithSelector( + EmergencyProtection.InvalidEmergencyModeActiveValue.selector, + [true, false] + ) + ); + } +} From 11ef654400d9b6a01f96e89855e4c719b84437a4 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Fri, 3 May 2024 00:33:57 +0700 Subject: [PATCH 066/134] feat: add tests for Proposal library --- contracts/libraries/Proposals.sol | 11 +- test/unit/libraries/Proposals.t.sol | 417 ++++++++++++++++++++++++++++ 2 files changed, 422 insertions(+), 6 deletions(-) create mode 100644 test/unit/libraries/Proposals.t.sol diff --git a/contracts/libraries/Proposals.sol b/contracts/libraries/Proposals.sol index 7b0e1ee0..dc63086b 100644 --- a/contracts/libraries/Proposals.sol +++ b/contracts/libraries/Proposals.sol @@ -57,7 +57,7 @@ library Proposals { function submit( State storage self, address executor, - ExecutorCall[] calldata calls + ExecutorCall[] memory calls ) internal returns (uint256 newProposalId) { if (calls.length == 0) { revert EmptyCalls(); @@ -67,9 +67,8 @@ library Proposals { self.proposals.push(); ProposalPacked storage newProposal = self.proposals[newProposalIndex]; - newProposal.executor = executor; - newProposal.executedAt = 0; + newProposal.executor = executor; newProposal.submittedAt = TimeUtils.timestamp(); // copying of arrays of custom types from calldata to storage has not been supported by the @@ -136,7 +135,7 @@ library Proposals { && block.timestamp >= _packed(self, proposalId).submittedAt + afterSubmitDelay; } - function _executeProposal(State storage self, uint256 proposalId) private returns (bytes[] memory results) { + function _executeProposal(State storage self, uint256 proposalId) private { ProposalPacked storage packed = _packed(self, proposalId); packed.executedAt = TimeUtils.timestamp(); @@ -146,7 +145,7 @@ library Proposals { assert(callsCount > 0); address executor = packed.executor; - results = new bytes[](callsCount); + bytes[] memory results = new bytes[](callsCount); for (uint256 i = 0; i < callsCount; ++i) { results[i] = IExecutor(payable(executor)).execute(calls[i].target, calls[i].value, calls[i].payload); } @@ -197,7 +196,7 @@ library Proposals { } } - function _getProposalStatus(State storage self, uint256 proposalId) private view returns (Status) { + function _getProposalStatus(State storage self, uint256 proposalId) private view returns (Status status) { if (proposalId < PROPOSAL_ID_OFFSET || proposalId > self.proposals.length) return Status.NotExist; ProposalPacked storage packed = _packed(self, proposalId); diff --git a/test/unit/libraries/Proposals.t.sol b/test/unit/libraries/Proposals.t.sol new file mode 100644 index 00000000..2f61e0e4 --- /dev/null +++ b/test/unit/libraries/Proposals.t.sol @@ -0,0 +1,417 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Vm} from "forge-std/Test.sol"; + +import {Executor} from "contracts/Executor.sol"; +import {Proposals, ExecutorCall, Proposal, Status} from "contracts/libraries/Proposals.sol"; + +import {TargetMock} from "test/utils/utils.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; +import {IDangerousContract} from "test/utils/interfaces.sol"; +import {ExecutorCallHelpers} from "test/utils/executor-calls.sol"; + +contract ProposalsUnitTests is UnitTest { + using Proposals for Proposals.State; + + TargetMock private _targetMock; + Proposals.State internal _proposals; + Executor private _executor; + + uint256 private constant PROPOSAL_ID_OFFSET = 1; + + function setUp() external { + _targetMock = new TargetMock(); + _executor = new Executor(address(this)); + } + + function test_submit_reverts_if_empty_proposals() external { + vm.expectRevert(Proposals.EmptyCalls.selector); + Proposals.submit(_proposals, address(0), new ExecutorCall[](0)); + } + + function test_submit_proposal() external { + uint256 proposalsCount = _proposals.count(); + + ExecutorCall[] memory calls = _getTargetRegularStaffCalls(); + + vm.expectEmit(); + emit Proposals.ProposalSubmitted(proposalsCount + PROPOSAL_ID_OFFSET, address(_executor), calls); + + vm.recordLogs(); + + Proposals.submit(_proposals, address(_executor), calls); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 1); + + proposalsCount = _proposals.count(); + + Proposals.ProposalPacked memory proposal = _proposals.proposals[proposalsCount - PROPOSAL_ID_OFFSET]; + + assertEq(proposal.executor, address(_executor)); + assertEq(proposal.submittedAt, block.timestamp); + assertEq(proposal.executedAt, 0); + assertEq(proposal.scheduledAt, 0); + assertEq(proposal.calls.length, 1); + + for (uint256 i = 0; i < proposal.calls.length; i++) { + assertEq(proposal.calls[i].target, address(_targetMock)); + assertEq(proposal.calls[i].value, calls[i].value); + assertEq(proposal.calls[i].payload, calls[i].payload); + } + } + + function testFuzz_schedule_proposal(uint256 delay) external { + vm.assume(delay > 0 && delay < type(uint40).max); + + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.ProposalPacked memory proposal = _proposals.proposals[0]; + + uint256 submittedAt = block.timestamp; + + assertEq(proposal.submittedAt, submittedAt); + assertEq(proposal.scheduledAt, 0); + assertEq(proposal.executedAt, 0); + + uint256 proposalId = _proposals.count(); + + _wait(delay); + + vm.expectEmit(); + emit Proposals.ProposalScheduled(proposalId); + Proposals.schedule(_proposals, proposalId, delay); + + proposal = _proposals.proposals[0]; + + assertEq(proposal.submittedAt, submittedAt); + assertEq(proposal.scheduledAt, block.timestamp); + assertEq(proposal.executedAt, 0); + } + + function testFuzz_cannot_schedule_unsubmitted_proposal(uint256 proposalId) external { + vm.assume(proposalId > 0); + + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotSubmitted.selector, proposalId)); + Proposals.schedule(_proposals, proposalId, 0); + } + + function test_cannot_schedule_proposal_twice() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + uint256 proposalId = 1; + Proposals.schedule(_proposals, proposalId, 0); + + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotSubmitted.selector, proposalId)); + Proposals.schedule(_proposals, proposalId, 0); + } + + function testFuzz_cannot_schedule_proposal_before_delay_passed(uint256 delay) external { + vm.assume(delay > 0 && delay < type(uint40).max); + + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + + _wait(delay - 1); + + vm.expectRevert(abi.encodeWithSelector(Proposals.AfterSubmitDelayNotPassed.selector, PROPOSAL_ID_OFFSET)); + Proposals.schedule(_proposals, PROPOSAL_ID_OFFSET, delay); + } + + function test_cannot_schedule_cancelled_proposal() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.cancelAll(_proposals); + + uint256 proposalId = _proposals.count(); + + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotSubmitted.selector, proposalId)); + Proposals.schedule(_proposals, proposalId, 0); + } + + function testFuzz_execute_proposal(uint256 delay) external { + vm.assume(delay > 0 && delay < type(uint40).max); + + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + uint256 proposalId = _proposals.count(); + Proposals.schedule(_proposals, proposalId, 0); + + uint256 submittedAndScheduledAt = block.timestamp; + + assertEq(_proposals.proposals[0].submittedAt, submittedAndScheduledAt); + assertEq(_proposals.proposals[0].scheduledAt, submittedAndScheduledAt); + assertEq(_proposals.proposals[0].executedAt, 0); + + _wait(delay); + + // TODO: figure out why event is not emitted + // vm.expectEmit(); + // emit Proposals.ProposalExecuted(); + Proposals.execute(_proposals, proposalId, delay); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 0); + + Proposals.ProposalPacked memory proposal = _proposals.proposals[0]; + + assertEq(_proposals.proposals[0].submittedAt, submittedAndScheduledAt); + assertEq(_proposals.proposals[0].scheduledAt, submittedAndScheduledAt); + assertEq(proposal.executedAt, block.timestamp); + } + + function testFuzz_cannot_execute_unsubmitted_proposal(uint256 proposalId) external { + vm.assume(proposalId > 0); + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); + Proposals.execute(_proposals, proposalId, 0); + } + + function test_cannot_execute_unscheduled_proposal() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + uint256 proposalId = _proposals.count(); + + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); + Proposals.execute(_proposals, proposalId, 0); + } + + function test_cannot_execute_twice() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + uint256 proposalId = _proposals.count(); + Proposals.schedule(_proposals, proposalId, 0); + Proposals.execute(_proposals, proposalId, 0); + + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); + Proposals.execute(_proposals, proposalId, 0); + } + + function test_cannot_execute_cancelled_proposal() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + uint256 proposalId = _proposals.count(); + Proposals.schedule(_proposals, proposalId, 0); + Proposals.cancelAll(_proposals); + + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); + Proposals.execute(_proposals, proposalId, 0); + } + + function testFuzz_cannot_execute_before_delay_passed(uint256 delay) external { + vm.assume(delay > 0 && delay < type(uint40).max); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + uint256 proposalId = _proposals.count(); + Proposals.schedule(_proposals, proposalId, 0); + + _wait(delay - 1); + + vm.expectRevert(abi.encodeWithSelector(Proposals.AfterScheduleDelayNotPassed.selector, proposalId)); + Proposals.execute(_proposals, proposalId, delay); + } + + function test_cancel_all_proposals() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + + uint256 proposalsCount = _proposals.count(); + + Proposals.schedule(_proposals, proposalsCount, 0); + + vm.expectEmit(); + emit Proposals.ProposalsCancelledTill(proposalsCount); + Proposals.cancelAll(_proposals); + + assertEq(_proposals.lastCancelledProposalId, proposalsCount); + } + + function test_get_proposal() external { + ExecutorCall[] memory calls = _getTargetRegularStaffCalls(); + Proposals.submit(_proposals, address(_executor), calls); + uint256 proposalId = _proposals.count(); + + Proposal memory proposal = _proposals.get(proposalId); + + uint256 submittedAt = block.timestamp; + + assertEq(proposal.id, proposalId); + assertEq(proposal.executor, address(_executor)); + assertEq(proposal.submittedAt, submittedAt); + assertEq(proposal.scheduledAt, 0); + assertEq(proposal.executedAt, 0); + assertEq(proposal.calls.length, 1); + assert(proposal.status == Status.Submitted); + + for (uint256 i = 0; i < proposal.calls.length; i++) { + assertEq(proposal.calls[i].target, address(_targetMock)); + assertEq(proposal.calls[i].value, calls[i].value); + assertEq(proposal.calls[i].payload, calls[i].payload); + } + + Proposals.schedule(_proposals, proposalId, 0); + + uint256 scheduledAt = block.timestamp; + + proposal = _proposals.get(proposalId); + + assertEq(proposal.id, proposalId); + assertEq(proposal.executor, address(_executor)); + assertEq(proposal.submittedAt, submittedAt); + assertEq(proposal.scheduledAt, scheduledAt); + assertEq(proposal.executedAt, 0); + assertEq(proposal.calls.length, 1); + assert(proposal.status == Status.Scheduled); + + for (uint256 i = 0; i < proposal.calls.length; i++) { + assertEq(proposal.calls[i].target, address(_targetMock)); + assertEq(proposal.calls[i].value, calls[i].value); + assertEq(proposal.calls[i].payload, calls[i].payload); + } + + Proposals.execute(_proposals, proposalId, 0); + + uint256 executedAt = block.timestamp; + + proposal = _proposals.get(proposalId); + + assertEq(proposal.id, proposalId); + assertEq(proposal.executor, address(_executor)); + assertEq(proposal.submittedAt, submittedAt); + assertEq(proposal.scheduledAt, scheduledAt); + assertEq(proposal.executedAt, executedAt); + assertEq(proposal.calls.length, 1); + assert(proposal.status == Status.Executed); + + for (uint256 i = 0; i < proposal.calls.length; i++) { + assertEq(proposal.calls[i].target, address(_targetMock)); + assertEq(proposal.calls[i].value, calls[i].value); + assertEq(proposal.calls[i].payload, calls[i].payload); + } + } + + function test_get_cancelled_proposal() external { + ExecutorCall[] memory calls = _getTargetRegularStaffCalls(); + Proposals.submit(_proposals, address(_executor), calls); + uint256 proposalId = _proposals.count(); + + Proposal memory proposal = _proposals.get(proposalId); + + uint256 submittedAt = block.timestamp; + + assertEq(proposal.id, proposalId); + assertEq(proposal.executor, address(_executor)); + assertEq(proposal.submittedAt, submittedAt); + assertEq(proposal.scheduledAt, 0); + assertEq(proposal.executedAt, 0); + assertEq(proposal.calls.length, 1); + assert(proposal.status == Status.Submitted); + + for (uint256 i = 0; i < proposal.calls.length; i++) { + assertEq(proposal.calls[i].target, address(_targetMock)); + assertEq(proposal.calls[i].value, calls[i].value); + assertEq(proposal.calls[i].payload, calls[i].payload); + } + + Proposals.cancelAll(_proposals); + + proposal = _proposals.get(proposalId); + + assertEq(proposal.id, proposalId); + assertEq(proposal.executor, address(_executor)); + assertEq(proposal.submittedAt, submittedAt); + assertEq(proposal.scheduledAt, 0); + assertEq(proposal.executedAt, 0); + assertEq(proposal.calls.length, 1); + assert(proposal.status == Status.Cancelled); + + for (uint256 i = 0; i < proposal.calls.length; i++) { + assertEq(proposal.calls[i].target, address(_targetMock)); + assertEq(proposal.calls[i].value, calls[i].value); + assertEq(proposal.calls[i].payload, calls[i].payload); + } + } + + function testFuzz_get_not_existing_proposal(uint256 proposalId) external { + vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotFound.selector, proposalId)); + _proposals.get(proposalId); + } + + function test_count_proposals() external { + assertEq(_proposals.count(), 0); + + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + assertEq(_proposals.count(), 1); + + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + assertEq(_proposals.count(), 2); + + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + assertEq(_proposals.count(), 3); + + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + assertEq(_proposals.count(), 4); + + Proposals.schedule(_proposals, 1, 0); + assertEq(_proposals.count(), 4); + + Proposals.schedule(_proposals, 2, 0); + assertEq(_proposals.count(), 4); + + Proposals.execute(_proposals, 1, 0); + assertEq(_proposals.count(), 4); + + Proposals.cancelAll(_proposals); + assertEq(_proposals.count(), 4); + } + + function test_can_execute_proposal() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + uint256 proposalId = _proposals.count(); + + assert(!_proposals.canExecute(proposalId, 0)); + + Proposals.schedule(_proposals, proposalId, 0); + + assert(!_proposals.canExecute(proposalId, 100)); + + _wait(100); + + assert(_proposals.canExecute(proposalId, 100)); + + Proposals.execute(_proposals, proposalId, 0); + + assert(!_proposals.canExecute(proposalId, 100)); + } + + function test_can_not_execute_cancelled_proposal() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + uint256 proposalId = _proposals.count(); + Proposals.schedule(_proposals, proposalId, 0); + + assert(_proposals.canExecute(proposalId, 0)); + Proposals.cancelAll(_proposals); + + assert(!_proposals.canExecute(proposalId, 0)); + } + + function test_can_schedule_proposal() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + uint256 proposalId = _proposals.count(); + assert(!_proposals.canSchedule(proposalId, 100)); + + _wait(100); + + assert(_proposals.canSchedule(proposalId, 100)); + + Proposals.schedule(_proposals, proposalId, 100); + Proposals.execute(_proposals, proposalId, 0); + + assert(!_proposals.canSchedule(proposalId, 100)); + } + + function test_can_not_schedule_cancelled_proposal() external { + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + uint256 proposalId = _proposals.count(); + assert(_proposals.canSchedule(proposalId, 0)); + + Proposals.cancelAll(_proposals); + + assert(!_proposals.canSchedule(proposalId, 0)); + } + + function _getTargetRegularStaffCalls() internal view returns (ExecutorCall[] memory) { + return ExecutorCallHelpers.create(address(_targetMock), abi.encodeCall(IDangerousContract.doRegularStaff, (42))); + } +} From 2715c44adbc1e2c47cef0b1dd00fed1efba894a6 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 7 May 2024 19:32:15 +0700 Subject: [PATCH 067/134] feat: add unit tests for SingleGovernance --- contracts/SingleGovernance.sol | 4 + test/unit/EmergencyProtectedTimelock.t.sol | 12 +- test/unit/SingleGovernance.t.sol | 126 +++++++++++++++++++++ test/unit/libraries/Proposals.t.sol | 48 ++++---- test/unit/mocks/TimelockMock.sol | 55 +++++++++ test/utils/unit-test.sol | 7 ++ 6 files changed, 218 insertions(+), 34 deletions(-) create mode 100644 test/unit/SingleGovernance.t.sol create mode 100644 test/unit/mocks/TimelockMock.sol diff --git a/contracts/SingleGovernance.sol b/contracts/SingleGovernance.sol index 93da9847..c1034658 100644 --- a/contracts/SingleGovernance.sol +++ b/contracts/SingleGovernance.sol @@ -26,6 +26,10 @@ contract SingleGovernance is IGovernance, ConfigurationProvider { TIMELOCK.schedule(proposalId); } + function executeProposal(uint256 proposalId) external { + TIMELOCK.execute(proposalId); + } + function canSchedule(uint256 proposalId) external view returns (bool) { return TIMELOCK.canSchedule(proposalId); } diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index a3a97f15..7216c248 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -64,7 +64,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_governance_can_submit_proposal() external { vm.prank(_dualGovernance); - _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls()); + _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls(address(_targetMock))); assertEq(_timelock.getProposalsCount(), 1); @@ -317,7 +317,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_cannot_emergency_execute_proposal_if_mode_not_activated() external { vm.startPrank(_dualGovernance); - _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls()); + _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls(address(_targetMock))); assertEq(_timelock.getProposalsCount(), 1); @@ -686,7 +686,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getProposalsCount(), 0); vm.startPrank(_dualGovernance); - ExecutorCall[] memory executorCalls = _getTargetRegularStaffCalls(); + ExecutorCall[] memory executorCalls = _getTargetRegularStaffCalls(address(_targetMock)); _timelock.submit(_adminExecutor, executorCalls); _timelock.submit(_adminExecutor, executorCalls); @@ -829,7 +829,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function _submitProposal() internal { vm.prank(_dualGovernance); - _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls()); + _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls(address(_targetMock))); } function _scheduleProposal(uint256 proposalId) internal { @@ -853,8 +853,4 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.deactivateEmergencyMode(); assertEq(_isEmergencyStateActivated(), false); } - - function _getTargetRegularStaffCalls() internal view returns (ExecutorCall[] memory) { - return ExecutorCallHelpers.create(address(_targetMock), abi.encodeCall(IDangerousContract.doRegularStaff, (42))); - } } diff --git a/test/unit/SingleGovernance.t.sol b/test/unit/SingleGovernance.t.sol new file mode 100644 index 00000000..489370bf --- /dev/null +++ b/test/unit/SingleGovernance.t.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Vm} from "forge-std/Test.sol"; + +import {Executor} from "contracts/Executor.sol"; +import {SingleGovernance} from "contracts/SingleGovernance.sol"; +import {IConfiguration, Configuration} from "contracts/Configuration.sol"; +import {ExecutorCall} from "contracts/libraries/Proposals.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; +import {TargetMock} from "test/utils/utils.sol"; + +import {TimelockMock} from "./mocks/TimelockMock.sol"; + +contract SingleGovernanceUnitTests is UnitTest { + TimelockMock private _timelock; + SingleGovernance private _singleGovernance; + Configuration private _config; + + address private _emergencyGovernance = makeAddr("EMERGENCY_GOVERNANCE"); + address private _governance = makeAddr("GOVERNANCE"); + + function setUp() external { + Executor _executor = new Executor(address(this)); + _config = new Configuration(address(_executor), _emergencyGovernance, new address[](0)); + _timelock = new TimelockMock(); + _singleGovernance = new SingleGovernance(address(_config), _governance, address(_timelock)); + } + + function testFuzz_constructor(address governance, address timelock) external { + SingleGovernance instance = new SingleGovernance(address(_config), governance, timelock); + + assertEq(instance.GOVERNANCE(), governance); + assertEq(address(instance.TIMELOCK()), address(timelock)); + } + + function test_submit_proposal() external { + assertEq(_timelock.getSubmittedProposals().length, 0); + + vm.prank(_governance); + _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + + assertEq(_timelock.getSubmittedProposals().length, 1); + } + + function testFuzz_stranger_cannot_submit_proposal(address stranger) external { + vm.assume(stranger != address(0) && stranger != _governance); + + assertEq(_timelock.getSubmittedProposals().length, 0); + + vm.startPrank(stranger); + vm.expectRevert( + abi.encodeWithSelector(SingleGovernance.NotGovernance.selector, [stranger]) + ); + _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + + assertEq(_timelock.getSubmittedProposals().length, 0); + } + + function test_schedule_proposal() external { + assertEq(_timelock.getScheduledProposals().length, 0); + + vm.prank(_governance); + _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + + _timelock.setSchedule(1); + _singleGovernance.scheduleProposal(1); + + assertEq(_timelock.getScheduledProposals().length, 1); + } + + function test_execute_proposal() external { + assertEq(_timelock.getExecutedProposals().length, 0); + + vm.prank(_governance); + _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + + _timelock.setSchedule(1); + _singleGovernance.scheduleProposal(1); + + _singleGovernance.executeProposal(1); + + assertEq(_timelock.getExecutedProposals().length, 1); + } + + function test_cancel_all_pending_proposals() external { + assertEq(_timelock.getLastCancelledProposalId(), 0); + + vm.startPrank(_governance); + _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + + _timelock.setSchedule(1); + _singleGovernance.scheduleProposal(1); + + _singleGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getLastCancelledProposalId(), 2); + } + + function testFuzz_stranger_cannot_cancel_all_pending_proposals(address stranger) external { + vm.assume(stranger != address(0) && stranger != _governance); + + assertEq(_timelock.getLastCancelledProposalId(), 0); + + vm.startPrank(stranger); + vm.expectRevert( + abi.encodeWithSelector(SingleGovernance.NotGovernance.selector, [stranger]) + ); + _singleGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getLastCancelledProposalId(), 0); + } + + function test_can_schedule() external { + vm.prank(_governance); + _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + + assertFalse(_singleGovernance.canSchedule(1)); + + _timelock.setSchedule(1); + + assertTrue(_singleGovernance.canSchedule(1)); + } +} \ No newline at end of file diff --git a/test/unit/libraries/Proposals.t.sol b/test/unit/libraries/Proposals.t.sol index 2f61e0e4..3b4ed280 100644 --- a/test/unit/libraries/Proposals.t.sol +++ b/test/unit/libraries/Proposals.t.sol @@ -33,7 +33,7 @@ contract ProposalsUnitTests is UnitTest { function test_submit_proposal() external { uint256 proposalsCount = _proposals.count(); - ExecutorCall[] memory calls = _getTargetRegularStaffCalls(); + ExecutorCall[] memory calls = _getTargetRegularStaffCalls(address(_targetMock)); vm.expectEmit(); emit Proposals.ProposalSubmitted(proposalsCount + PROPOSAL_ID_OFFSET, address(_executor), calls); @@ -65,7 +65,7 @@ contract ProposalsUnitTests is UnitTest { function testFuzz_schedule_proposal(uint256 delay) external { vm.assume(delay > 0 && delay < type(uint40).max); - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); Proposals.ProposalPacked memory proposal = _proposals.proposals[0]; uint256 submittedAt = block.timestamp; @@ -97,7 +97,7 @@ contract ProposalsUnitTests is UnitTest { } function test_cannot_schedule_proposal_twice() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = 1; Proposals.schedule(_proposals, proposalId, 0); @@ -108,7 +108,7 @@ contract ProposalsUnitTests is UnitTest { function testFuzz_cannot_schedule_proposal_before_delay_passed(uint256 delay) external { vm.assume(delay > 0 && delay < type(uint40).max); - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); _wait(delay - 1); @@ -117,7 +117,7 @@ contract ProposalsUnitTests is UnitTest { } function test_cannot_schedule_cancelled_proposal() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); Proposals.cancelAll(_proposals); uint256 proposalId = _proposals.count(); @@ -129,7 +129,7 @@ contract ProposalsUnitTests is UnitTest { function testFuzz_execute_proposal(uint256 delay) external { vm.assume(delay > 0 && delay < type(uint40).max); - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); Proposals.schedule(_proposals, proposalId, 0); @@ -163,7 +163,7 @@ contract ProposalsUnitTests is UnitTest { } function test_cannot_execute_unscheduled_proposal() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); @@ -171,7 +171,7 @@ contract ProposalsUnitTests is UnitTest { } function test_cannot_execute_twice() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); Proposals.schedule(_proposals, proposalId, 0); Proposals.execute(_proposals, proposalId, 0); @@ -181,7 +181,7 @@ contract ProposalsUnitTests is UnitTest { } function test_cannot_execute_cancelled_proposal() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); Proposals.schedule(_proposals, proposalId, 0); Proposals.cancelAll(_proposals); @@ -192,7 +192,7 @@ contract ProposalsUnitTests is UnitTest { function testFuzz_cannot_execute_before_delay_passed(uint256 delay) external { vm.assume(delay > 0 && delay < type(uint40).max); - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); Proposals.schedule(_proposals, proposalId, 0); @@ -203,8 +203,8 @@ contract ProposalsUnitTests is UnitTest { } function test_cancel_all_proposals() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalsCount = _proposals.count(); @@ -218,7 +218,7 @@ contract ProposalsUnitTests is UnitTest { } function test_get_proposal() external { - ExecutorCall[] memory calls = _getTargetRegularStaffCalls(); + ExecutorCall[] memory calls = _getTargetRegularStaffCalls(address(_targetMock)); Proposals.submit(_proposals, address(_executor), calls); uint256 proposalId = _proposals.count(); @@ -282,7 +282,7 @@ contract ProposalsUnitTests is UnitTest { } function test_get_cancelled_proposal() external { - ExecutorCall[] memory calls = _getTargetRegularStaffCalls(); + ExecutorCall[] memory calls = _getTargetRegularStaffCalls(address(_targetMock)); Proposals.submit(_proposals, address(_executor), calls); uint256 proposalId = _proposals.count(); @@ -331,16 +331,16 @@ contract ProposalsUnitTests is UnitTest { function test_count_proposals() external { assertEq(_proposals.count(), 0); - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.count(), 1); - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.count(), 2); - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.count(), 3); - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.count(), 4); Proposals.schedule(_proposals, 1, 0); @@ -357,7 +357,7 @@ contract ProposalsUnitTests is UnitTest { } function test_can_execute_proposal() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); assert(!_proposals.canExecute(proposalId, 0)); @@ -376,7 +376,7 @@ contract ProposalsUnitTests is UnitTest { } function test_can_not_execute_cancelled_proposal() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); Proposals.schedule(_proposals, proposalId, 0); @@ -387,7 +387,7 @@ contract ProposalsUnitTests is UnitTest { } function test_can_schedule_proposal() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); assert(!_proposals.canSchedule(proposalId, 100)); @@ -402,7 +402,7 @@ contract ProposalsUnitTests is UnitTest { } function test_can_not_schedule_cancelled_proposal() external { - Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls()); + Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); assert(_proposals.canSchedule(proposalId, 0)); @@ -410,8 +410,4 @@ contract ProposalsUnitTests is UnitTest { assert(!_proposals.canSchedule(proposalId, 0)); } - - function _getTargetRegularStaffCalls() internal view returns (ExecutorCall[] memory) { - return ExecutorCallHelpers.create(address(_targetMock), abi.encodeCall(IDangerousContract.doRegularStaff, (42))); - } } diff --git a/test/unit/mocks/TimelockMock.sol b/test/unit/mocks/TimelockMock.sol new file mode 100644 index 00000000..1ff6f631 --- /dev/null +++ b/test/unit/mocks/TimelockMock.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {ExecutorCall} from "contracts/libraries/Proposals.sol"; + +contract TimelockMock { + uint8 public constant OFFSET = 1; + + mapping(uint256 => bool) public canScheduleProposal; + + uint256[] public submittedProposals; + uint256[] public scheduledProposals; + uint256[] public executedProposals; + + uint256 public lastCancelledProposalId; + + function submit(address, ExecutorCall[] calldata) external returns (uint256 newProposalId) { + newProposalId = submittedProposals.length + OFFSET; + submittedProposals.push(newProposalId); + canScheduleProposal[newProposalId] = false; + return newProposalId; + } + function schedule(uint256 proposalId) external { + if (canScheduleProposal[proposalId] == false) { + revert(); + } + + scheduledProposals.push(proposalId); + } + function execute(uint256 proposalId) external { + executedProposals.push(proposalId); + } + function canSchedule(uint256 proposalId) external view returns (bool) { + return canScheduleProposal[proposalId]; + } + function cancelAllNonExecutedProposals() external { + lastCancelledProposalId = submittedProposals[submittedProposals.length - 1]; + } + function setSchedule(uint256 proposalId) external { + canScheduleProposal[proposalId] = true; + } + + function getSubmittedProposals() external view returns (uint256[] memory) { + return submittedProposals; + } + function getScheduledProposals() external view returns (uint256[] memory) { + return scheduledProposals; + } + function getExecutedProposals() external view returns (uint256[] memory) { + return executedProposals; + } + function getLastCancelledProposalId() external view returns (uint256) { + return lastCancelledProposalId; + } +} \ No newline at end of file diff --git a/test/utils/unit-test.sol b/test/utils/unit-test.sol index 6fdb5cfd..6ff279a4 100644 --- a/test/utils/unit-test.sol +++ b/test/utils/unit-test.sol @@ -3,9 +3,16 @@ pragma solidity 0.8.23; // solhint-disable-next-line import {Test} from "forge-std/Test.sol"; +import {ExecutorCall} from "contracts/libraries/Proposals.sol"; +import {ExecutorCallHelpers} from "test/utils/executor-calls.sol"; +import {IDangerousContract} from "test/utils/interfaces.sol"; contract UnitTest is Test { function _wait(uint256 duration) internal { vm.warp(block.timestamp + duration); } + + function _getTargetRegularStaffCalls(address targetMock) internal pure returns (ExecutorCall[] memory) { + return ExecutorCallHelpers.create(address(targetMock), abi.encodeCall(IDangerousContract.doRegularStaff, (42))); + } } \ No newline at end of file From 4a83572f23ffa68a7bca89dfa2626b165d27598a Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 8 May 2024 14:27:51 +0400 Subject: [PATCH 068/134] Add scenario test for proposals submitted during the Rage Quit --- contracts/DualGovernance.sol | 4 + contracts/libraries/DualGovernanceState.sol | 35 ++---- test/scenario/veto-cooldown-mechanics.t.sol | 132 ++++++++++++++++++++ test/utils/scenario-test-blueprint.sol | 9 +- 4 files changed, 151 insertions(+), 29 deletions(-) create mode 100644 test/scenario/veto-cooldown-mechanics.t.sol diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 618e2109..789f40ee 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -70,6 +70,10 @@ contract DualGovernance is IGovernance, ConfigurationProvider { return address(_dgState.signallingEscrow); } + function rageQuitEscrow() external view returns (address) { + return address(_dgState.rageQuitEscrow); + } + function isScheduled(uint256 proposalId) external view returns (bool) { return _scheduledProposals[proposalId] != 0; } diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol index a22698dd..8d02fe3b 100644 --- a/contracts/libraries/DualGovernanceState.sol +++ b/contracts/libraries/DualGovernanceState.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {IEscrow} from "../interfaces/IEscrow.sol"; @@ -21,36 +20,18 @@ enum State { RageQuit } -/* -// The previous version of the state (3 slots) struct DualGovernanceState { - State state; // [0, 8] - uint8 rageQuitRound; // [0, 16] - uint40 enteredAt; // [0, 56] + State state; + uint8 rageQuitRound; + uint40 enteredAt; // the time the veto signalling state was entered - uint40 vetoSignallingActivationTime; // [0, 96] - IEscrow signallingEscrow; // [0, 256] + uint40 vetoSignallingActivationTime; + IEscrow signallingEscrow; // the time the Deactivation sub-state was last exited without exiting the parent Veto Signalling state - uint40 vetoSignallingReactivationTime; // [1, 40] + uint40 vetoSignallingReactivationTime; // the last time a proposal was submitted to the DG subsystem - uint40 lastProposalSubmissionTime; // [1, 80] - uint40 lastAdoptableStateExitedAt; // [1, 120] - IEscrow rageQuitEscrow; // [2, 160] -} -*/ - -struct DualGovernanceState { - State state; // [0, 8] - uint8 rageQuitRound; // [0, 16] - uint40 enteredAt; // [0, 56] - // the time the veto signalling state was entered - uint40 vetoSignallingActivationTime; // [0, 96] - IEscrow signallingEscrow; // [0, 256] - // the time the Deactivation sub-state was last exited without exiting the parent Veto Signalling state - uint40 vetoSignallingReactivationTime; // [1, 40] - // the last time a proposal was submitted to the DG subsystem - uint40 lastAdoptableStateExitedAt; // [1, 80] - IEscrow rageQuitEscrow; // [1, 240] + uint40 lastAdoptableStateExitedAt; + IEscrow rageQuitEscrow; } function dynamicTimelockDuration( diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol new file mode 100644 index 00000000..6afc8951 --- /dev/null +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import { + Escrow, + percents, + ExecutorCall, + ExecutorCallHelpers, + ScenarioTestBlueprint, + DualGovernanceStateViews +} from "../utils/scenario-test-blueprint.sol"; + +interface IDangerousContract { + function doRegularStaff(uint256 magic) external; + function doRugPool() external; + function doControversialStaff() external; +} + +contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { + function setUp() external { + _selectFork(); + _deployTarget(); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); + } + + function testFork_ProposalSubmittedInRageQuitNonExecutableInTheNextVetoCooldown() external { + ExecutorCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + + uint256 proposalId; + _step("1. THE PROPOSAL IS SUBMITTED"); + { + proposalId = _submitProposal( + _dualGovernance, "Propose to doSmth on target passing dual governance", regularStaffCalls + ); + + _assertSubmittedProposalData(proposalId, _config.ADMIN_EXECUTOR(), regularStaffCalls); + _assertCanSchedule(_dualGovernance, proposalId, false); + } + + uint256 vetoedStETHAmount; + address vetoer = makeAddr("MALICIOUS_ACTOR"); + _step("2. THE SECOND SEAL RAGE QUIT SUPPORT IS ACQUIRED"); + { + vetoedStETHAmount = _lockStETH(vetoer, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT() + 1)); + _assertVetoSignalingState(); + + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() + 1); + _activateNextState(); + _assertRageQuitState(); + } + + uint256 anotherProposalId; + _step("3. ANOTHER PROPOSAL IS SUBMITTED DURING THE RAGE QUIT STATE"); + { + _activateNextState(); + _assertRageQuitState(); + anotherProposalId = _submitProposal( + _dualGovernance, + "Another Proposal", + ExecutorCallHelpers.create(address(_target), abi.encodeCall(IDangerousContract.doRugPool, ())) + ); + } + + _step("4. RAGE QUIT IS FINALIZED"); + { + // request withdrawals batches + Escrow rageQuitEscrow = _getRageQuitEscrow(); + uint256 requestAmount = _WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); + uint256 maxRequestsCount = vetoedStETHAmount / requestAmount + 1; + + rageQuitEscrow.requestNextWithdrawalsBatch(maxRequestsCount); + + vm.deal(address(_WITHDRAWAL_QUEUE), vetoedStETHAmount); + _finalizeWQ(); + + uint256 batchSizeLimit = 200; + + while (true) { + (uint256 offset, uint256 total, uint256[] memory unstETHIds) = + rageQuitEscrow.getNextWithdrawalBatches(batchSizeLimit); + if (offset == total) { + break; + } + uint256[] memory hints = + _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); + + rageQuitEscrow.claimNextWithdrawalsBatch(offset, hints); + } + + _wait(_config.RAGE_QUIT_EXTENSION_DELAY() + 1); + assertTrue(rageQuitEscrow.isRageQuitFinalized()); + } + + _step("5. PROPOSAL SUBMITTED BEFORE RAGE QUIT IS EXECUTABLE"); + { + _activateNextState(); + _assertVetoCooldownState(); + + this.scheduleProposalExternal(proposalId); + _assertProposalScheduled(proposalId); + } + + _step("6. PROPOSAL SUBMITTED DURING RAGE QUIT IS NOT EXECUTABLE"); + { + _activateNextState(); + _assertVetoCooldownState(); + + vm.expectRevert(DualGovernanceStateViews.ProposalsAdoptionSuspended.selector); + this.scheduleProposalExternal(anotherProposalId); + } + } + + function scheduleProposalExternal(uint256 proposalId) external { + _scheduleProposal(_dualGovernance, proposalId); + } + + function _finalizeWQ() internal { + uint256 lastRequestId = _WITHDRAWAL_QUEUE.getLastRequestId(); + _finalizeWQ(lastRequestId); + } + + function _finalizeWQ(uint256 id) internal { + uint256 finalizationShareRate = _ST_ETH.getPooledEthByShares(1e27) + 1e9; // TODO check finalization rate + address lido = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; + vm.prank(lido); + _WITHDRAWAL_QUEUE.finalize(id, finalizationShareRate); + + bytes32 LOCKED_ETHER_AMOUNT_POSITION = 0x0e27eaa2e71c8572ab988fef0b54cd45bbd1740de1e22343fb6cda7536edc12f; // keccak256("lido.WithdrawalQueue.lockedEtherAmount"); + + vm.store(address(_WITHDRAWAL_QUEUE), LOCKED_ETHER_AMOUNT_POSITION, bytes32(address(_WITHDRAWAL_QUEUE).balance)); + } +} diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 04f8985d..99ed749d 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -94,6 +94,11 @@ contract ScenarioTestBlueprint is Test { return Escrow(payable(_dualGovernance.vetoSignallingEscrow())); } + function _getRageQuitEscrow() internal view returns (Escrow) { + address rageQuitEscrow = _dualGovernance.rageQuitEscrow(); + return Escrow(payable(rageQuitEscrow)); + } + function _getTargetRegularStaffCalls() internal view returns (ExecutorCall[] memory) { return ExecutorCallHelpers.create(address(_target), abi.encodeCall(IDangerousContract.doRegularStaff, (42))); } @@ -151,8 +156,8 @@ contract ScenarioTestBlueprint is Test { // --- // Escrow Manipulation // --- - function _lockStETH(address vetoer, Percents memory vetoPowerInPercents) internal { - (, uint256 amount) = _setupStETHWhale(vetoer, vetoPowerInPercents); + function _lockStETH(address vetoer, Percents memory vetoPowerInPercents) internal returns (uint256 amount) { + (, amount) = _setupStETHWhale(vetoer, vetoPowerInPercents); _lockStETH(vetoer, amount); } From 37388fae092dcc58f9d5af535a54f2e3de3d6798 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 28 May 2024 15:02:07 +0300 Subject: [PATCH 069/134] fix: review fixes --- test/unit/libraries/EmergencyProtection.t.sol | 180 ++++++++---------- 1 file changed, 76 insertions(+), 104 deletions(-) diff --git a/test/unit/libraries/EmergencyProtection.t.sol b/test/unit/libraries/EmergencyProtection.t.sol index 8f05b257..442c708f 100644 --- a/test/unit/libraries/EmergencyProtection.t.sol +++ b/test/unit/libraries/EmergencyProtection.t.sol @@ -8,6 +8,8 @@ import {EmergencyProtection, EmergencyState} from "contracts/libraries/Emergency import {UnitTest} from "test/utils/unit-test.sol"; contract EmergencyProtectionUnitTests is UnitTest { + using EmergencyProtection for EmergencyProtection.State; + EmergencyProtection.State internal _emergencyProtection; function testFuzz_setup_emergency_protection( @@ -32,9 +34,7 @@ contract EmergencyProtectionUnitTests is UnitTest { vm.recordLogs(); - EmergencyProtection.setup( - _emergencyProtection, activationCommittee, executionCommittee, protectedTill, duration - ); + _emergencyProtection.setup(activationCommittee, executionCommittee, protectedTill, duration); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 4); @@ -49,17 +49,17 @@ contract EmergencyProtectionUnitTests is UnitTest { function test_setup_same_activation_committee() external { address activationCommittee = makeAddr("activationCommittee"); - EmergencyProtection.setup(_emergencyProtection, activationCommittee, address(0x2), 100, 100); + _emergencyProtection.setup(activationCommittee, address(0x2), 100, 100); vm.expectEmit(); emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x3)); vm.expectEmit(); emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + 200); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDurationSet(200); + emit EmergencyProtection.EmergencyModeDurationSet(300); vm.recordLogs(); - EmergencyProtection.setup(_emergencyProtection, activationCommittee, address(0x3), 200, 200); + _emergencyProtection.setup(activationCommittee, address(0x3), 200, 300); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); @@ -67,24 +67,24 @@ contract EmergencyProtectionUnitTests is UnitTest { assertEq(_emergencyProtection.activationCommittee, activationCommittee); assertEq(_emergencyProtection.executionCommittee, address(0x3)); assertEq(_emergencyProtection.protectedTill, block.timestamp + 200); - assertEq(_emergencyProtection.emergencyModeDuration, 200); + assertEq(_emergencyProtection.emergencyModeDuration, 300); assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); } function test_setup_same_execution_committee() external { address executionCommittee = makeAddr("executionCommittee"); - EmergencyProtection.setup(_emergencyProtection, address(0x1), executionCommittee, 100, 100); + _emergencyProtection.setup(address(0x1), executionCommittee, 100, 100); vm.expectEmit(); emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x2)); vm.expectEmit(); emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + 200); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDurationSet(200); + emit EmergencyProtection.EmergencyModeDurationSet(300); vm.recordLogs(); - EmergencyProtection.setup(_emergencyProtection, address(0x2), executionCommittee, 200, 200); + _emergencyProtection.setup(address(0x2), executionCommittee, 200, 300); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); @@ -92,12 +92,12 @@ contract EmergencyProtectionUnitTests is UnitTest { assertEq(_emergencyProtection.activationCommittee, address(0x2)); assertEq(_emergencyProtection.executionCommittee, executionCommittee); assertEq(_emergencyProtection.protectedTill, block.timestamp + 200); - assertEq(_emergencyProtection.emergencyModeDuration, 200); + assertEq(_emergencyProtection.emergencyModeDuration, 300); assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); } function test_setup_same_protected_till() external { - EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), 100, 100); + _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); vm.expectEmit(); emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); @@ -107,7 +107,7 @@ contract EmergencyProtectionUnitTests is UnitTest { emit EmergencyProtection.EmergencyModeDurationSet(200); vm.recordLogs(); - EmergencyProtection.setup(_emergencyProtection, address(0x3), address(0x4), 100, 200); + _emergencyProtection.setup(address(0x3), address(0x4), 100, 200); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); @@ -120,7 +120,7 @@ contract EmergencyProtectionUnitTests is UnitTest { } function test_setup_same_emergency_mode_duration() external { - EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), 100, 100); + _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); vm.expectEmit(); emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); @@ -130,7 +130,7 @@ contract EmergencyProtectionUnitTests is UnitTest { emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + 200); vm.recordLogs(); - EmergencyProtection.setup(_emergencyProtection, address(0x3), address(0x4), 200, 100); + _emergencyProtection.setup(address(0x3), address(0x4), 200, 100); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); @@ -143,14 +143,14 @@ contract EmergencyProtectionUnitTests is UnitTest { } function test_activate_emergency_mode() external { - EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), 100, 100); + _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); vm.expectEmit(); emit EmergencyProtection.EmergencyModeActivated(block.timestamp); vm.recordLogs(); - EmergencyProtection.activate(_emergencyProtection); + _emergencyProtection.activate(); Vm.Log[] memory entries = vm.getRecordedLogs(); @@ -160,7 +160,7 @@ contract EmergencyProtectionUnitTests is UnitTest { function test_cannot_activate_emergency_mode_if_protected_till_expired() external { uint256 protectedTill = 100; - EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), protectedTill, 100); + _emergencyProtection.setup(address(0x1), address(0x2), protectedTill, 100); _wait(protectedTill + 1); @@ -170,7 +170,7 @@ contract EmergencyProtectionUnitTests is UnitTest { [block.timestamp, _emergencyProtection.protectedTill] ) ); - EmergencyProtection.activate(_emergencyProtection); + _emergencyProtection.activate(); } function testFuzz_deactivate_emergency_mode( @@ -184,17 +184,15 @@ contract EmergencyProtectionUnitTests is UnitTest { vm.assume(activationCommittee != address(0)); vm.assume(executionCommittee != address(0)); - EmergencyProtection.setup( - _emergencyProtection, activationCommittee, executionCommittee, protectedTill, duration - ); - EmergencyProtection.activate(_emergencyProtection); + _emergencyProtection.setup(activationCommittee, executionCommittee, protectedTill, duration); + _emergencyProtection.activate(); vm.expectEmit(); emit EmergencyProtection.EmergencyModeDeactivated(block.timestamp); vm.recordLogs(); - EmergencyProtection.deactivate(_emergencyProtection); + _emergencyProtection.deactivate(); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 1); @@ -207,7 +205,7 @@ contract EmergencyProtectionUnitTests is UnitTest { } function test_get_emergency_state() external { - EmergencyState memory state = EmergencyProtection.getEmergencyState(_emergencyProtection); + EmergencyState memory state = _emergencyProtection.getEmergencyState(); assertEq(state.activationCommittee, address(0)); assertEq(state.executionCommittee, address(0)); @@ -216,31 +214,31 @@ contract EmergencyProtectionUnitTests is UnitTest { assertEq(state.emergencyModeEndsAfter, 0); assertEq(state.isEmergencyModeActivated, false); - EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), 100, 100); + _emergencyProtection.setup(address(0x1), address(0x2), 100, 200); - state = EmergencyProtection.getEmergencyState(_emergencyProtection); + state = _emergencyProtection.getEmergencyState(); assertEq(state.activationCommittee, address(0x1)); assertEq(state.executionCommittee, address(0x2)); assertEq(state.protectedTill, block.timestamp + 100); - assertEq(state.emergencyModeDuration, 100); + assertEq(state.emergencyModeDuration, 200); assertEq(state.emergencyModeEndsAfter, 0); assertEq(state.isEmergencyModeActivated, false); - EmergencyProtection.activate(_emergencyProtection); + _emergencyProtection.activate(); - state = EmergencyProtection.getEmergencyState(_emergencyProtection); + state = _emergencyProtection.getEmergencyState(); assertEq(state.activationCommittee, address(0x1)); assertEq(state.executionCommittee, address(0x2)); assertEq(state.protectedTill, block.timestamp + 100); - assertEq(state.emergencyModeDuration, 100); - assertEq(state.emergencyModeEndsAfter, block.timestamp + 100); + assertEq(state.emergencyModeDuration, 200); + assertEq(state.emergencyModeEndsAfter, block.timestamp + 200); assertEq(state.isEmergencyModeActivated, true); - EmergencyProtection.deactivate(_emergencyProtection); + _emergencyProtection.deactivate(); - state = EmergencyProtection.getEmergencyState(_emergencyProtection); + state = _emergencyProtection.getEmergencyState(); assertEq(state.activationCommittee, address(0)); assertEq(state.executionCommittee, address(0)); @@ -251,52 +249,52 @@ contract EmergencyProtectionUnitTests is UnitTest { } function test_is_emergency_mode_activated() external { - assertEq(EmergencyProtection.isEmergencyModeActivated(_emergencyProtection), false); + assertEq(_emergencyProtection.isEmergencyModeActivated(), false); - EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), 100, 100); + _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); - assertEq(EmergencyProtection.isEmergencyModeActivated(_emergencyProtection), false); + assertEq(_emergencyProtection.isEmergencyModeActivated(), false); - EmergencyProtection.activate(_emergencyProtection); + _emergencyProtection.activate(); - assertEq(EmergencyProtection.isEmergencyModeActivated(_emergencyProtection), true); + assertEq(_emergencyProtection.isEmergencyModeActivated(), true); - EmergencyProtection.deactivate(_emergencyProtection); + _emergencyProtection.deactivate(); - assertEq(EmergencyProtection.isEmergencyModeActivated(_emergencyProtection), false); + assertEq(_emergencyProtection.isEmergencyModeActivated(), false); } function test_is_emergency_mode_passed() external { - assertEq(EmergencyProtection.isEmergencyModePassed(_emergencyProtection), false); + assertEq(_emergencyProtection.isEmergencyModePassed(), false); - uint256 duration = 100; + uint256 duration = 200; - EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), 100, duration); + _emergencyProtection.setup(address(0x1), address(0x2), 100, duration); - assertEq(EmergencyProtection.isEmergencyModePassed(_emergencyProtection), false); + assertEq(_emergencyProtection.isEmergencyModePassed(), false); - EmergencyProtection.activate(_emergencyProtection); + _emergencyProtection.activate(); - assertEq(EmergencyProtection.isEmergencyModePassed(_emergencyProtection), false); + assertEq(_emergencyProtection.isEmergencyModePassed(), false); _wait(duration + 1); - assertEq(EmergencyProtection.isEmergencyModePassed(_emergencyProtection), true); + assertEq(_emergencyProtection.isEmergencyModePassed(), true); - EmergencyProtection.deactivate(_emergencyProtection); + _emergencyProtection.deactivate(); - assertEq(EmergencyProtection.isEmergencyModePassed(_emergencyProtection), false); + assertEq(_emergencyProtection.isEmergencyModePassed(), false); } function test_is_emergency_protection_enabled() external { uint256 protectedTill = 100; - uint256 duration = 100; + uint256 duration = 200; - assertEq(EmergencyProtection.isEmergencyProtectionEnabled(_emergencyProtection), false); + assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), false); - EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), protectedTill, duration); + _emergencyProtection.setup(address(0x1), address(0x2), protectedTill, duration); - assertEq(EmergencyProtection.isEmergencyProtectionEnabled(_emergencyProtection), true); + assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); _wait(protectedTill - block.timestamp); @@ -304,88 +302,62 @@ contract EmergencyProtectionUnitTests is UnitTest { _wait(duration); - assertEq(EmergencyProtection.isEmergencyProtectionEnabled(_emergencyProtection), true); + assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); _wait(100); - assertEq(EmergencyProtection.isEmergencyProtectionEnabled(_emergencyProtection), true); + assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); EmergencyProtection.deactivate(_emergencyProtection); - assertEq(EmergencyProtection.isEmergencyProtectionEnabled(_emergencyProtection), false); + assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), false); } function testFuzz_check_activation_committee(address committee, address stranger) external { vm.assume(committee != address(0)); vm.assume(stranger != address(0) && stranger != committee); - vm.expectRevert( - abi.encodeWithSelector( - EmergencyProtection.NotEmergencyActivator.selector, - [stranger] - ) - ); - EmergencyProtection.checkActivationCommittee(_emergencyProtection, stranger); - EmergencyProtection.checkActivationCommittee(_emergencyProtection, address(0)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyActivator.selector, [stranger])); + _emergencyProtection.checkActivationCommittee(stranger); + _emergencyProtection.checkActivationCommittee(address(0)); - EmergencyProtection.setup(_emergencyProtection, committee, address(0x2), 100, 100); + _emergencyProtection.setup(committee, address(0x2), 100, 100); - EmergencyProtection.checkActivationCommittee(_emergencyProtection, committee); + _emergencyProtection.checkActivationCommittee(committee); - vm.expectRevert( - abi.encodeWithSelector( - EmergencyProtection.NotEmergencyActivator.selector, - [stranger] - ) - ); - EmergencyProtection.checkActivationCommittee(_emergencyProtection, stranger); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyActivator.selector, [stranger])); + _emergencyProtection.checkActivationCommittee(stranger); } function testFuzz_check_execution_committee(address committee, address stranger) external { vm.assume(committee != address(0)); vm.assume(stranger != address(0) && stranger != committee); - vm.expectRevert( - abi.encodeWithSelector( - EmergencyProtection.NotEmergencyEnactor.selector, - [stranger] - ) - ); - EmergencyProtection.checkExecutionCommittee(_emergencyProtection, stranger); - EmergencyProtection.checkExecutionCommittee(_emergencyProtection, address(0)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, [stranger])); + _emergencyProtection.checkExecutionCommittee(stranger); + _emergencyProtection.checkExecutionCommittee(address(0)); - EmergencyProtection.setup(_emergencyProtection, address(0x1), committee, 100, 100); + _emergencyProtection.setup(address(0x1), committee, 100, 100); - EmergencyProtection.checkExecutionCommittee(_emergencyProtection, committee); + _emergencyProtection.checkExecutionCommittee(committee); - vm.expectRevert( - abi.encodeWithSelector( - EmergencyProtection.NotEmergencyEnactor.selector, - [stranger] - ) - ); - EmergencyProtection.checkExecutionCommittee(_emergencyProtection, stranger); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, [stranger])); + _emergencyProtection.checkExecutionCommittee(stranger); } function test_check_emergency_mode_active() external { vm.expectRevert( - abi.encodeWithSelector( - EmergencyProtection.InvalidEmergencyModeActiveValue.selector, - [false, true] - ) + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [false, true]) ); - EmergencyProtection.checkEmergencyModeActive(_emergencyProtection, true); - EmergencyProtection.checkEmergencyModeActive(_emergencyProtection, false); + _emergencyProtection.checkEmergencyModeActive(true); + _emergencyProtection.checkEmergencyModeActive(false); - EmergencyProtection.setup(_emergencyProtection, address(0x1), address(0x2), 100, 100); - EmergencyProtection.activate(_emergencyProtection); + _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + _emergencyProtection.activate(); - EmergencyProtection.checkEmergencyModeActive(_emergencyProtection, true); + _emergencyProtection.checkEmergencyModeActive(true); vm.expectRevert( - abi.encodeWithSelector( - EmergencyProtection.InvalidEmergencyModeActiveValue.selector, - [true, false] - ) + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, [true, false]) ); } } From a082f9f1e8a28d9643329b87fd19b90ff47a5ea7 Mon Sep 17 00:00:00 2001 From: lucasmt Date: Tue, 28 May 2024 17:55:16 -0500 Subject: [PATCH 070/134] Add simplified abstract model --- contracts/model/DualGovernance.sol | 301 ++++++++++++++++++ .../model/EmergencyProtectedTimelock.sol | 203 ++++++++++++ contracts/model/Escrow.sol | 188 +++++++++++ 3 files changed, 692 insertions(+) create mode 100644 contracts/model/DualGovernance.sol create mode 100644 contracts/model/EmergencyProtectedTimelock.sol create mode 100644 contracts/model/Escrow.sol diff --git a/contracts/model/DualGovernance.sol b/contracts/model/DualGovernance.sol new file mode 100644 index 00000000..09d6258a --- /dev/null +++ b/contracts/model/DualGovernance.sol @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./EmergencyProtectedTimelock.sol"; +import "./Escrow.sol"; + +/** + * @title Dual Governance Mechanism + * Based on the Lido protocol desgin documents. + * This document describes the module of the Dual Governance in a high-level. + */ + +// DualGovernance contract to handle proposal submissions and lifecycle management. +contract DualGovernance { + enum State { + Normal, + VetoSignalling, + VetoSignallingDeactivation, + VetoCooldown, + RageQuit + } + + EmergencyProtectedTimelock public emergencyProtectedTimelock; + Escrow public signallingEscrow; + Escrow public rageQuitEscrow; + address public fakeETH; + + // State Variables + State public currentState; + mapping(address => bool) public proposers; + mapping(address => bool) public admin_proposers; + uint256 public lastStateChangeTime; + uint256 public lastSubStateActivationTime; + uint256 public lastStateReactivationTime; + uint256 public lastVetoSignallingTime; + uint256 public rageQuitSequenceNumber; + + // Constants + uint256 public constant FIRST_SEAL_RAGE_QUIT_SUPPORT = 1; // Threshold required for transition from Normal to Veto Signalling state. + uint256 public constant SECOND_SEAL_RAGE_QUIT_SUPPORT = 10; // Transition to Rage Quit occurs if t - t^S_{act} > DynamicTimelockMaxDuration and R > SecondSealRageQuitSupport. + uint256 public constant DYNAMIC_TIMELOCK_MIN_DURATION = 5 days; // L_min; minimum duration for the dynamic timelock, which extends based on the level of dissent or rage quit support. + uint256 public constant DYNAMIC_TIMELOCK_MAX_DURATION = 45 days; // L_max; maximum possible duration for dynamic timelocks, applied under conditions of extreme dissent to delay proposal execution. + uint256 public constant VETO_SIGNALLING_MIN_ACTIVE_DURATION = 5 hours; // Minimum time Veto Signalling must be active before before transitioning to the Deactivation sub-state can be considered. + uint256 public constant VETO_COOLDOWN_DURATION = 5 hours; // Cooling period following the Veto Signalling state to prevent immediate re-signalling. + uint256 public constant VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = 3 days; // Maximum duration that the Veto Signalling can remain in Deactivation before advancing to Veto Cooldown or reverting to Veto Signalling. + uint256 public constant RAGE_QUIT_EXTENSION_DELAY = 7 days; // The delay follows the completion of the withdrawal process in Rage Quit state. + + // Constructor to initialize the governance contract in the Normal state. + constructor(address _fakeETH, uint256 emergencyProtectionTimelock) { + currentState = State.Normal; + lastStateChangeTime = block.timestamp; + fakeETH = _fakeETH; + emergencyProtectedTimelock = new EmergencyProtectedTimelock(address(this), emergencyProtectionTimelock); + signallingEscrow = new Escrow(address(this), _fakeETH); + } + + // Operations + /** + * Submits a proposal for consideration within the governance model. + * Proposals can be submitted when in the Normal state or during Veto Signalling; however they cannot be executed in Veto Signalling. + */ + function submitProposal(ExecutorCall[] calldata calls) external returns (uint256 proposalId) { + activateNextState(); + + require(proposers[msg.sender], "Caller is not authorized to submit proposals."); + require(calls.length != 0, "Empty calls."); + require( + currentState == State.Normal || currentState == State.VetoSignalling || currentState == State.RageQuit, + "Cannot submit in current state." + ); + + proposalId = emergencyProtectedTimelock.submit(msg.sender, calls); + } + + /** + * Schedules a proposal for execution, ensuring that all conditions for governance are met. + * Scheduling is allowed in Normal and Veto Cooldown states to prepare proposals for decision-making. + */ + function scheduleProposal(uint256 proposalId) external { + activateNextState(); + + require( + currentState == State.Normal || currentState == State.VetoCooldown, + "Proposals can only be scheduled in Normal or Veto Cooldown states." + ); + if (currentState == State.VetoCooldown) { + require( + block.timestamp < lastVetoSignallingTime, + "Proposal submitted after the last time Veto Signalling state was entered." + ); + } + + emergencyProtectedTimelock.schedule(proposalId); + } + + // Cancel all non-executed proposals. + function cancelAllPendingProposals() external { + activateNextState(); + + require(admin_proposers[msg.sender], "Caller is not admin proposers."); + require( + currentState != State.Normal || currentState != State.VetoCooldown || currentState != State.RageQuit, + "Cannot cancel all pending proposals in the current state." + ); + + emergencyProtectedTimelock.cancelAllNonExecutedProposals(); + } + + /** + * Calculate the dynamic timelock T_lock(R) based on the current rage quit support. + * Ajusting the timelock duration to reflect community sentiment and stake involvement, + * ranging from immediate to maximum delay based on the specified thresholds. + */ + function calculateDynamicTimelock(uint256 rageQuitSupport) public pure returns (uint256) { + if (rageQuitSupport <= FIRST_SEAL_RAGE_QUIT_SUPPORT) { + return 0; + } else if (rageQuitSupport < SECOND_SEAL_RAGE_QUIT_SUPPORT) { + return linearInterpolation(rageQuitSupport); + } else { + return DYNAMIC_TIMELOCK_MAX_DURATION; + } + } + + /** + * Implement linear interpolation to calculate dynamic timelocks based on current rage quit support. + * Linear interpolation is used to smoothly transition between DYNAMIC_TIMELOCK_MIN_DURATION and DYNAMIC_TIMELOCK_MAX_DURATION, + * proportional to the current rage quit support within the defined thresholds. + */ + function linearInterpolation(uint256 rageQuitSupport) private pure returns (uint256) { + uint256 L_min = DYNAMIC_TIMELOCK_MIN_DURATION; + uint256 L_max = DYNAMIC_TIMELOCK_MAX_DURATION; + return L_min + + ((rageQuitSupport - FIRST_SEAL_RAGE_QUIT_SUPPORT) * (L_max - L_min)) + / (SECOND_SEAL_RAGE_QUIT_SUPPORT - FIRST_SEAL_RAGE_QUIT_SUPPORT); + } + + // Function to manage transitions between states, based on rage quit support and timing. + function transitionState(State newState) private { + require(newState != currentState, "New state must be different from current state."); + + if (newState == State.Normal) { + rageQuitSequenceNumber = 0; + } else if (newState == State.VetoSignalling) { + lastVetoSignallingTime = block.timestamp; + } else if (newState == State.RageQuit) { + signallingEscrow.startRageQuit(); + rageQuitSequenceNumber++; + rageQuitEscrow = signallingEscrow; + signallingEscrow = new Escrow(address(this), fakeETH); + } + + lastStateChangeTime = block.timestamp; + lastStateReactivationTime = 0; + currentState = newState; + } + + // Function to manage transitions from parent states to sub-states. + function enterSubState(State subState) private { + require( + currentState == State.VetoSignalling && subState == State.VetoSignallingDeactivation, + "New state must be a sub-state of current state." + ); + lastSubStateActivationTime = block.timestamp; + currentState = subState; + } + + // Function to manage transitions from sub-states back to parent states. + function exitSubState(State parentState) private { + require( + currentState == State.VetoSignallingDeactivation && parentState == State.VetoSignalling, + "New state must be a parent state of current state." + ); + lastStateReactivationTime = block.timestamp; + currentState = parentState; + } + + function max(uint256 a, uint256 b) private pure returns (uint256) { + return a > b ? a : b; + } + + // State Transitions + + function activateNextState() public { + uint256 rageQuitSupport = signallingEscrow.getRageQuitSupport(); + + State previousState; + + // Make multiple transitions in sequence if the transition conditions are satisfied + do { + previousState = currentState; + + if (currentState == State.Normal) { + fromNormal(rageQuitSupport); + } else if (currentState == State.VetoSignalling) { + fromVetoSignalling(rageQuitSupport); + } else if (currentState == State.VetoSignallingDeactivation) { + fromVetoSignallingDeactivation(rageQuitSupport); + } else if (currentState == State.VetoCooldown) { + fromVetoCooldown(rageQuitSupport); + } else { + fromRageQuit(rageQuitSupport); + } + } while (currentState != previousState); + } + + /** + * Manages the state transition logic from Normal. + * Transitions from Normal to Veto Signalling occurs if rage quit support exceeds FIRST_SEAL_RAGE_QUIT_SUPPORT. + */ + function fromNormal(uint256 rageQuitSupport) private { + require(currentState == State.Normal, "Must be in Normal state."); + + if (rageQuitSupport > FIRST_SEAL_RAGE_QUIT_SUPPORT) { + transitionState(State.VetoSignalling); + } + } + + /** + * Manages the state transition logic from VetoSignalling. + * Transitions to Rage Quit if both the max timelock duration is exceeded and rage quit support exceeds SECOND_SEAL_RAGE_QUIT_SUPPORT. + * Transitions to Veto Deactivation occurs when the time elapsed since the last state change or proposal exceeds the dynamic timelock and minimum active duration. + */ + function fromVetoSignalling(uint256 rageQuitSupport) private { + require(currentState == State.VetoSignalling, "Must be in Veto Signalling state."); + + // Check the conditions for transitioning to RageQuit or Veto Deactivation based on the time elapsed and support level. + if ( + block.timestamp - lastStateChangeTime > DYNAMIC_TIMELOCK_MAX_DURATION + && rageQuitSupport > SECOND_SEAL_RAGE_QUIT_SUPPORT + ) { + transitionState(State.RageQuit); + } else if ( + block.timestamp - lastStateChangeTime > calculateDynamicTimelock(rageQuitSupport) + && block.timestamp - max(lastStateChangeTime, lastStateReactivationTime) + > VETO_SIGNALLING_MIN_ACTIVE_DURATION + ) { + enterSubState(State.VetoSignallingDeactivation); + } + } + + /** + * Manages the state transition logic from VetoSignallingDeactivation. + * Checks if enough time has passed and evaluates current rage quit support to determine the next state. + */ + function fromVetoSignallingDeactivation(uint256 rageQuitSupport) private { + require(currentState == State.VetoSignallingDeactivation, "Must be in Deactivation sub-state."); + + uint256 elapsed = block.timestamp - lastSubStateActivationTime; + // Check the conditions for transitioning to VetoCooldown or back to VetoSignalling + if ( + block.timestamp - lastStateChangeTime <= calculateDynamicTimelock(rageQuitSupport) + || rageQuitSupport > SECOND_SEAL_RAGE_QUIT_SUPPORT + ) { + exitSubState(State.VetoSignalling); + } else if (elapsed > VETO_SIGNALLING_DEACTIVATION_MAX_DURATION) { + transitionState(State.VetoCooldown); + } + } + + /** + * Manages the state transition logic from VetoCooldown. + * Checks if the cooldown period has elapsed before making any state transitions based on the rageQuitSupport levels. + */ + function fromVetoCooldown(uint256 rageQuitSupport) private { + require(currentState == State.VetoCooldown, "Must be in Veto Cooldown state."); + + // Ensure the Veto Cooldown has lasted for at least the minimum duration. + if (block.timestamp - lastStateChangeTime > VETO_COOLDOWN_DURATION) { + // Depending on the level of rage quit support, transition to Normal or Veto Signalling. + if (rageQuitSupport <= FIRST_SEAL_RAGE_QUIT_SUPPORT) { + transitionState(State.Normal); + } else { + transitionState(State.VetoSignalling); + } + } + } + + /** + * Manages the state transition logic from RageQuit based on cooldown expiration and rage support evaluation. + * Checks if withdrawal process is complete, cooldown period expired. + * Transitions to VetoCooldown if support has decreased below the threshold; otherwise, transitions to VetoSignalling. + */ + function fromRageQuit(uint256 rageQuitSupport) private { + require(currentState == State.RageQuit, "Must be in Rage Quit state."); + + // Check if the withdrawal process is completed and if the RageQuitExtensionDelay has elapsed + if (rageQuitEscrow.isRageQuitFinalized()) { + // Start ETH claim timelock period + rageQuitEscrow.startEthClaimTimelock(rageQuitSequenceNumber); + + // Depending on the level of rage quit support, transition to Veto Cooldown or Veto Signalling. + // Transition to Veto Cooldown if support has decreased below the critical threshold. + // Otherwise, return to Veto Signalling if support is still above a lower threshold. + if (rageQuitSupport <= FIRST_SEAL_RAGE_QUIT_SUPPORT) { + transitionState(State.VetoCooldown); + } else { + transitionState(State.VetoSignalling); + } + } + } +} diff --git a/contracts/model/EmergencyProtectedTimelock.sol b/contracts/model/EmergencyProtectedTimelock.sol new file mode 100644 index 00000000..df673108 --- /dev/null +++ b/contracts/model/EmergencyProtectedTimelock.sol @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Struct to represent executor calls +struct ExecutorCall { + address target; + uint96 value; + bytes payload; +} + +enum ProposalStatus { + Pending, + Scheduled, + Executed, + Canceled +} + +struct Proposal { + uint256 id; + address proposer; + ExecutorCall[] calls; + uint256 submissionTime; + uint256 schedulingTime; + ProposalStatus status; +} + +// This contract manages the timelocking of proposals with emergency intervention capabilities. +// It provides controls for entering and managing emergency states as well as executing proposals under normal and emergency conditions. +contract EmergencyProtectedTimelock { + // Addresses associated with governance roles and permissions. + address public governance; + address public emergencyGovernance; + address public adminExecutor; + address public emergencyActivationCommittee; + address public emergencyExecutionCommittee; + + // State Variables + mapping(uint256 => Proposal) public proposals; // Maps to keep track of proposals and their states. + uint256 public nextProposalId; // ID to be assigned to the next proposal. + bool public emergencyModeActive; // Indicates if the contract is currently in emergency mode. + bool public protectedModeActive; // Indicates if the contract is in a protected deployment mode. + uint256 public emergencyActivatedTimestamp; // Timestamp for when emergency mode was activated. + uint256 public emergencyProtectionTimelock; // Timelock settings for emergency and proposal management. Set to 0 in regular deployment mode. + + // Constants + uint256 public constant EMERGENCY_MODE_MAX_DURATION = 1 days; // Maximum duration emergency mode can be active. + uint256 public constant PROPOSAL_EXECUTION_MIN_TIMELOCK = 3 days; // Minimum delay before an executed proposal becomes effective, during normal operation. + + constructor(address _governance, uint256 _emergencyProtectionTimelock) { + governance = _governance; + emergencyProtectionTimelock = _emergencyProtectionTimelock; + } + + // Submits a new proposal, initializing its timelock and storing its calls. + function submit(address executor, ExecutorCall[] memory calls) external returns (uint256 proposalId) { + // Ensure that only the governance can submit new proposals. + require(msg.sender == governance, "Only governance can submit proposal."); + // Establish the minimum timelock duration for the proposal's execution. + uint256 executionDelay = PROPOSAL_EXECUTION_MIN_TIMELOCK; + + proposals[nextProposalId].id = nextProposalId; + proposals[nextProposalId].proposer = executor; + proposals[nextProposalId].submissionTime = block.timestamp; + proposals[nextProposalId].schedulingTime = 0; + proposals[nextProposalId].status = ProposalStatus.Pending; + + for (uint256 i = 0; i < calls.length; i++) { + proposals[nextProposalId].calls.push(calls[i]); + } + + proposalId = nextProposalId; + nextProposalId++; + } + + // Schedules a proposal if it has been submitted for at least AFTER_SUBMIT_DELAY days. + function schedule(uint256 proposalId) external { + // The proposal MUST be already submitted. + require(proposalId < nextProposalId, "Proposal does not exist."); + Proposal storage proposal = proposals[proposalId]; + require(proposal.status == ProposalStatus.Pending, "Proposal must be in Pending status."); + // Ensure that only the governance can schedule proposals. + require(msg.sender == governance, "Only governance can schedule proposal."); + // Ensure the mandatory delay after submission has passed before allowing scheduling. + require( + block.timestamp >= proposal.submissionTime + PROPOSAL_EXECUTION_MIN_TIMELOCK, + "Required time since submission has not yet elapsed." + ); + proposal.status = ProposalStatus.Scheduled; + proposal.schedulingTime = block.timestamp; + } + + // Executes a scheduled proposal after a defined delay. + function execute(uint256 proposalId) external { + Proposal storage proposal = proposals[proposalId]; + // Ensure the emergency mode is not active to proceed with normal execution. + require(!emergencyModeActive, "Emergency mode must not be active to execute a proposal."); + // Check that the proposal is in the Scheduled state, ready for execution. + require(proposal.status == ProposalStatus.Scheduled, "Proposal must be scheduled before it can be executed."); + // Check that the required time delay after scheduling has passed to allow for sufficient time. + require( + block.timestamp >= proposal.schedulingTime + emergencyProtectionTimelock, + "Scheduled time plus delay must pass before execution." + ); + // Execute the proposal by calling `executeProposalCalls`, which handles the execution of all calls within the proposal. + executeProposalCalls(proposalId); + } + + /** + * Contains the logic for executing the calls within a proposal. + * Each call within the proposal must execute successfully. + */ + function executeProposalCalls(uint256 proposalId) internal { + Proposal storage proposal = proposals[proposalId]; + // Iterate over all calls in the proposal. + for (uint256 i = 0; i < proposal.calls.length; i++) { + (bool success,) = proposal.calls[i].target.call{value: proposal.calls[i].value}(proposal.calls[i].payload); + require(success, "Execution failed."); + } + proposal.status = ProposalStatus.Executed; + } + + /** + * Cancels all proposals that have not yet been executed. + * It iterates through the list of all proposals and cancels each that has not been executed. + */ + function cancelAllNonExecutedProposals() public { + require(msg.sender == governance, "Caller is not authorized to cancel proposal."); + + // Loop through all the proposals stored in the contract. + for (uint256 i = 0; i < nextProposalId; i++) { + // Ensure that only proposals in 'Submitted' or 'Scheduled' status are canceled. + if (proposals[i].status != ProposalStatus.Executed) { + proposals[i].status = ProposalStatus.Canceled; + } + } + } + + // Emergency protection functions + /** + * Activates the emergency mode, restricting new proposals and allowing emergency interventions. + * Can only be activated by the emergency activation committee. + */ + function activateEmergencyMode() external { + require(msg.sender == emergencyActivationCommittee, "Must be called by the Emergency Activation Committee."); + require(!emergencyModeActive, "Emergency mode is already active."); + // Activate the emergency mode. + emergencyModeActive = true; + // Record the timestamp of activation to manage the duration of the emergency state accurately. + emergencyActivatedTimestamp = block.timestamp; + } + + /** + * Deactivates the emergency mode, resuming normal operations. + * This function is a crucial step in the recovery process from an emergency state, + * allowing the system to return to standard operational mode after addressing the emergency situation. + */ + function deactivateEmergencyMode() external { + // Ensure the emergency mode is currently active before attempting to deactivate. + require(emergencyModeActive, "Emergency mode is not active."); + + // If within the duration, only the Admin Executor can deactivate to prevent premature termination of emergency procedures. + if (block.timestamp - emergencyActivatedTimestamp < EMERGENCY_MODE_MAX_DURATION) { + require(msg.sender == adminExecutor, "Only the Admin Executor can deactivate emergency mode prematurely."); + } + // Deactivate the emergency mode. + emergencyModeActive = false; + // Clearing both the Emergency Activation and Execution Committees. + emergencyActivationCommittee = address(0); + emergencyExecutionCommittee = address(0); + cancelAllNonExecutedProposals(); + emergencyProtectionTimelock = 0; + } + + // Executes the scheduled proposal. Emergency execution allows bypassing the normal timelock in critical situations. + function emergencyExecute(uint256 proposalId) external { + Proposal storage proposal = proposals[proposalId]; + + require(msg.sender == emergencyExecutionCommittee, "Caller is not the Emergency Execution Committee."); + require(emergencyModeActive, "Emergency mode is not active."); + require(proposal.status == ProposalStatus.Scheduled, "Proposal is not scheduled."); + + executeProposalCalls(proposalId); + } + + /** + * Executes an emergency reset of the governance system to the pre-configured emergency governance address, + * cancels all non-executed proposals, and resets both emergency committees. + */ + function emergencyReset() external { + require(msg.sender == emergencyExecutionCommittee, "Caller is not the Emergency Execution Committee."); + require(emergencyModeActive, "Emergency mode must be active."); + + // Deactivate the emergency mode. + emergencyModeActive = false; + // Clearing both the Emergency Activation and Execution Committees. + emergencyActivationCommittee = address(0); + emergencyExecutionCommittee = address(0); + // Setting the governance address to a pre-configured Emergency Governance address. + governance = emergencyGovernance; + cancelAllNonExecutedProposals(); + emergencyProtectionTimelock = 0; + } +} diff --git a/contracts/model/Escrow.sol b/contracts/model/Escrow.sol new file mode 100644 index 00000000..0ebc639a --- /dev/null +++ b/contracts/model/Escrow.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * Simplified abstract model of the Escrow contract. Includes the following simplifications: + * + * - To make calculations simpler and focus on the core logic, this model uses a single, + * non-rebasable fakeETH token to signal support instead of stETH, wstETH and unstETH. + * + * - The model does not interact with the WithdrawalQueue, instead only simulates it by + * keeping track of withdrawal requests internally. + * + * - Replaces requestNextWithdrawalBatch and claimNextWithdrawalBatch with simpler non-batch + * requestNextWithdrawal and claimNextWithdrawal functions that process a single request. + */ +contract Escrow { + enum State { + SignallingEscrow, + RageQuitEscrow + } + + enum WithdrawalRequestStatus { + Requested, + Finalized, + Claimed + } + + State public currentState; + + address public dualGovernance; + IERC20 public fakeETH; + mapping(address => uint256) public balances; + uint256 public totalStaked; + uint256 public totalWithdrawalRequestAmount; + uint256 public totalClaimedEthAmount; + uint256 public totalWithdrawnPostRageQuit; + mapping(address => uint256) public lastLockedTimes; // Track the last time tokens were locked by each user + uint256 public withdrawalRequestCount; + mapping(uint256 => WithdrawalRequestStatus) public withdrawalRequestStatus; + mapping(uint256 => uint256) public withdrawalRequestAmount; + uint256 public rageQuitExtensionDelayPeriodEnd; + uint256 public rageQuitSequenceNumber; + uint256 public rageQuitEthClaimTimelockStart; + + // Constants + uint256 public constant RAGE_QUIT_EXTENSION_DELAY = 7 days; + uint256 public constant RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK = 60 days; + uint256 public constant RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_START_SEQ_NUMBER = 2; + uint256 public constant RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFFS_0 = 0; + uint256 public constant RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFFS_1 = 1; // Placeholder value + uint256 public constant RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFFS_2 = 2; // Placeholder value + uint256 public constant SIGNALLING_ESCROW_MIN_LOCK_TIME = 5 hours; // Minimum time that funds must be locked before they can be unlocked + + uint256 public constant MIN_WITHDRAWAL_AMOUNT = 100; + uint256 public constant MAX_WITHDRAWAL_AMOUNT = 1000 * 1e18; + + constructor(address _dualGovernance, address _fakeETH) { + currentState = State.SignallingEscrow; + dualGovernance = _dualGovernance; + fakeETH = IERC20(_fakeETH); + } + + // Locks a specified amount of tokens. + function lock(uint256 amount) external { + require(currentState == State.SignallingEscrow, "Cannot lock in current state."); + require(amount > 0, "Amount must be greater than zero."); + require(fakeETH.allowance(msg.sender, address(this)) >= amount, "Need allowance to transfer tokens."); + require(fakeETH.balanceOf(msg.sender) <= amount, "Not enough balance."); + fakeETH.transferFrom(msg.sender, address(this), amount); + balances[msg.sender] += amount; + totalStaked += amount; + lastLockedTimes[msg.sender] = block.timestamp; + } + + // Unlocks a specified amount of tokens. + function unlock(uint256 amount) external { + require(currentState == State.SignallingEscrow, "Cannot unlock in current state."); + require( + block.timestamp >= lastLockedTimes[msg.sender] + SIGNALLING_ESCROW_MIN_LOCK_TIME, "Lock period not expired." + ); + require(balances[msg.sender] >= amount, "Insufficient balances."); + fakeETH.transfer(msg.sender, amount); + balances[msg.sender] -= amount; + totalStaked -= amount; + } + + // Returns total rage quit support as a percentage of the total supply. + function getRageQuitSupport() external view returns (uint256) { + return totalStaked / fakeETH.totalSupply(); + } + + // Transitions the escrow to the RageQuitEscrow state and initiates withdrawal processes. + function startRageQuit() external { + require(msg.sender == dualGovernance, "Only DualGovernance can start rage quit."); + require(currentState == State.SignallingEscrow, "Already in RageQuit or invalid state."); + currentState = State.RageQuitEscrow; + } + + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + + // Initiates a withdrawal request. + function requestNextWithdrawal() external { + require(currentState == State.RageQuitEscrow, "Withdrawal only allowed in RageQuit state."); + require( + totalStaked - totalWithdrawalRequestAmount >= MIN_WITHDRAWAL_AMOUNT, + "Withdrawal requests already concluded." + ); + + uint256 amount = min(totalStaked, MAX_WITHDRAWAL_AMOUNT); + + withdrawalRequestStatus[withdrawalRequestCount] = WithdrawalRequestStatus.Requested; + withdrawalRequestAmount[withdrawalRequestCount] = amount; + withdrawalRequestCount++; + + totalWithdrawalRequestAmount += amount; + } + + // Claims the ETH associated with a finalized withdrawal request. + function claimNextWithdrawal(uint256 requestId, uint256 hint) external { + require(currentState == State.RageQuitEscrow, "Withdrawal only allowed in RageQuit state."); + require( + withdrawalRequestStatus[requestId] == WithdrawalRequestStatus.Finalized, + "Withdrawal request must be finalized and not claimed." + ); + + withdrawalRequestStatus[requestId] = WithdrawalRequestStatus.Claimed; + totalClaimedEthAmount += withdrawalRequestAmount[requestId]; + + if (totalStaked - totalClaimedEthAmount < MIN_WITHDRAWAL_AMOUNT) { + rageQuitExtensionDelayPeriodEnd = block.timestamp + RAGE_QUIT_EXTENSION_DELAY; + } + } + + // Check if the RageQuitExtensionDelay has passed since all withdrawals were finalized. + function isRageQuitFinalized() public view returns (bool) { + return currentState == State.RageQuitEscrow && totalStaked - totalClaimedEthAmount < MIN_WITHDRAWAL_AMOUNT + && rageQuitExtensionDelayPeriodEnd < block.timestamp; + } + + // Called by the governance to initiate ETH claim timelock. + function startEthClaimTimelock(uint256 _rageQuitSequenceNumber) external { + require(msg.sender == dualGovernance, "Only DualGovernance can start ETH claim timelock."); + + rageQuitSequenceNumber = _rageQuitSequenceNumber; + rageQuitEthClaimTimelockStart = block.timestamp; + } + + // Timelock between exit from Rage Quit state and when stakers are allowed to withdraw funds. + // Quadratic on the rage quit sequence number. + function rageQuitEthClaimTimelock() public view returns (uint256) { + uint256 ethClaimTimelock = RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK; + + if (rageQuitSequenceNumber >= RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_START_SEQ_NUMBER) { + uint256 c0 = RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFFS_0; + uint256 c1 = RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFFS_1; + uint256 c2 = RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFFS_2; + + uint256 x = rageQuitSequenceNumber - RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_START_SEQ_NUMBER; + + ethClaimTimelock += c0 + c1 * x + c2 * (x ** 2); + } + + return ethClaimTimelock; + } + + // Withdraws all locked funds after the RageQuit delay has passed. + function withdraw() public { + require(currentState == State.RageQuitEscrow, "Withdrawal only allowed in RageQuit state."); + require(isRageQuitFinalized(), "Rage quit process not yet finalized."); + require( + rageQuitEthClaimTimelockStart + rageQuitEthClaimTimelock() < block.timestamp, + "Rage quit ETH claim timelock has not elapsed." + ); + uint256 stakedAmount = balances[msg.sender]; + require(stakedAmount > 0, "No funds to withdraw."); + uint256 totalEth = address(this).balance; // Total ETH held by contract + require(totalEth > stakedAmount, "Not enough balance."); + + balances[msg.sender] = 0; + + // Transfer ETH equivalent + payable(msg.sender).transfer(stakedAmount); + } +} From 516ff5fb01ab9efa826e12db25bf0d4a04439308 Mon Sep 17 00:00:00 2001 From: lucasmt Date: Wed, 29 May 2024 14:46:04 -0500 Subject: [PATCH 071/134] Fix rage quit support computation in model --- contracts/model/DualGovernance.sol | 4 ++-- contracts/model/Escrow.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/model/DualGovernance.sol b/contracts/model/DualGovernance.sol index 09d6258a..5ab56511 100644 --- a/contracts/model/DualGovernance.sol +++ b/contracts/model/DualGovernance.sol @@ -36,8 +36,8 @@ contract DualGovernance { uint256 public rageQuitSequenceNumber; // Constants - uint256 public constant FIRST_SEAL_RAGE_QUIT_SUPPORT = 1; // Threshold required for transition from Normal to Veto Signalling state. - uint256 public constant SECOND_SEAL_RAGE_QUIT_SUPPORT = 10; // Transition to Rage Quit occurs if t - t^S_{act} > DynamicTimelockMaxDuration and R > SecondSealRageQuitSupport. + uint256 public constant FIRST_SEAL_RAGE_QUIT_SUPPORT = 10 ** 16; // Threshold required for transition from Normal to Veto Signalling state (1%). + uint256 public constant SECOND_SEAL_RAGE_QUIT_SUPPORT = 10 ** 17; // Transition to Rage Quit occurs if t - t^S_{act} > DynamicTimelockMaxDuration and R > SecondSealRageQuitSupport (10%). uint256 public constant DYNAMIC_TIMELOCK_MIN_DURATION = 5 days; // L_min; minimum duration for the dynamic timelock, which extends based on the level of dissent or rage quit support. uint256 public constant DYNAMIC_TIMELOCK_MAX_DURATION = 45 days; // L_max; maximum possible duration for dynamic timelocks, applied under conditions of extreme dissent to delay proposal execution. uint256 public constant VETO_SIGNALLING_MIN_ACTIVE_DURATION = 5 hours; // Minimum time Veto Signalling must be active before before transitioning to the Deactivation sub-state can be considered. diff --git a/contracts/model/Escrow.sol b/contracts/model/Escrow.sol index 0ebc639a..0df4f505 100644 --- a/contracts/model/Escrow.sol +++ b/contracts/model/Escrow.sol @@ -88,7 +88,7 @@ contract Escrow { // Returns total rage quit support as a percentage of the total supply. function getRageQuitSupport() external view returns (uint256) { - return totalStaked / fakeETH.totalSupply(); + return totalStaked * 10 ** 18 / fakeETH.totalSupply(); } // Transitions the escrow to the RageQuitEscrow state and initiates withdrawal processes. From 406f40436c37d5259ce0e766281c4d7128386f7f Mon Sep 17 00:00:00 2001 From: lucasmt Date: Tue, 28 May 2024 18:32:08 -0500 Subject: [PATCH 072/134] Add files for Kontrol CI --- .github/workflows/lido-ci.yml | 46 ++++++ test/kontrol/scripts/common.sh | 211 ++++++++++++++++++++++++++++ test/kontrol/scripts/run-kontrol.sh | 176 +++++++++++++++++++++++ test/kontrol/scripts/versions.json | 4 + 4 files changed, 437 insertions(+) create mode 100644 .github/workflows/lido-ci.yml create mode 100755 test/kontrol/scripts/common.sh create mode 100755 test/kontrol/scripts/run-kontrol.sh create mode 100644 test/kontrol/scripts/versions.json diff --git a/.github/workflows/lido-ci.yml b/.github/workflows/lido-ci.yml new file mode 100644 index 00000000..c8204248 --- /dev/null +++ b/.github/workflows/lido-ci.yml @@ -0,0 +1,46 @@ +--- + name: "Test Proofs" + on: + workflow_dispatch: + pull_request: + branches: + - main + jobs: + test: + runs-on: [self-hosted, linux, flyweight] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Run Proofs in KaaS + run: | + sha=$(git rev-parse HEAD) + branch_name=$(git rev-parse --abbrev-ref HEAD) + response=$(curl -X POST \ + -w "%{http_code}" \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.KAAS_COMPUTE_TOKEN }}" \ + https://api.github.com/repos/runtimeverification/_kaas_lidofinance_dual-governance/actions/workflows/lido-ci.yml/dispatches \ + -d '{ + "ref": "master", + "inputs": { + "branch_name": "'"${{ github.event.pull_request.head.sha || github.sha }}"'", + "extra_args": "script", + "statuses_sha": "'$sha'", + "org": "runtimeverification", + "repository": "_audits_lidofinance_dual-governance", + "auth_token": "'"${{ secrets.KAAS_COMPUTE_TOKEN }}"'" + } + }') + + if [ "$response" -ge 200 ] && [ "$response" -lt 300 ]; then + echo "The request was successful" + elif [ "$response" -ge 400 ] && [ "$response" -lt 500 ]; then + echo "There was a client error: $response" + exit 1 + else + echo "There was a server error: $response" + exit 1 + fi diff --git a/test/kontrol/scripts/common.sh b/test/kontrol/scripts/common.sh new file mode 100755 index 00000000..7a631a7b --- /dev/null +++ b/test/kontrol/scripts/common.sh @@ -0,0 +1,211 @@ +#!/bin/bash +# Common functions and variables for run-kontrol.sh and make-summary-deployment.sh + +notif() { echo "== $0: $*" >&2 ; } + +# usage function for the run-kontrol.sh script +usage_run_kontrol() { + echo "Usage: $0 [-h|--help] [container|local|dev] [script|tests]" 1>&2 + echo "" 1>&2 + echo " -h, --help Display this help message." 1>&2 + echo "" 1>&2 + echo "Execution modes:" + echo " container Run in docker container. Reproduce CI execution. (Default)" 1>&2 + echo " local Run locally, enforces registered versions.json version for better reproducibility. (Recommended)" 1>&2 + echo " dev Run locally, does NOT enforce registered version. (Useful for developing with new versions and features)" 1>&2 + echo "" 1>&2 + echo "Tests executed:" + echo " script Execute the tests recorded in run-kontrol.sh" 1>&2 + echo " tests Execute the tests provided as arguments" 1>&2 + exit 0 +} + +# placeholder usage function for any other scripts +usage_other() { + echo "Usage: $0 [-h|--help] OPTIONS" 1>&2 + echo "" 1>&2 + echo " -h, --help Display this help message." 1>&2 + echo "" 1>&2 + echo "Further Options:" + echo " option1 Description1. (Default)" 1>&2 + echo " Option2 Description2. (Recommended)" 1>&2 + exit 0 +} + +# Set Run Directory /packages/contracts-bedrock +WORKSPACE_DIR=$( cd "$SCRIPT_HOME/../../.." >/dev/null 2>&1 && pwd ) +pushd "$WORKSPACE_DIR" > /dev/null || exit + +# Variables +export CONTAINER_NAME=kontrol-tests +KONTROLRC=$(jq -r .kontrol < "$WORKSPACE_DIR/test/kontrol/scripts/versions.json") +export KONTROL_RELEASE=$KONTROLRC +export LOCAL=false +export SCRIPT_TESTS=false +SCRIPT_OPTION=false +export CUSTOM_TESTS=0 # Store the position where custom tests start, interpreting 0 as no tests +CUSTOM_OPTION=0 +export RUN_KONTROL=false # true if any functions are called from run-kontrol.sh + +# General usage function, which discerns from which script is being called and displays the appropriate message +usage() { + if [ "$RUN_KONTROL" = "true" ]; then + usage_run_kontrol + else + usage_other + fi +} + + +# Argument Parsing +# The logic behind argument parsing is the following (in order): +# - Execution mode argument: container (or empty), local, dev +# - Tests arguments (first if execution mode empty): script, specific test names +parse_args() { + if [ $# -eq 0 ]; then + export LOCAL=false + export SCRIPT_TESTS=false + export CUSTOM_TESTS=0 + # `script` argument caps the total possible arguments to its position + elif { [ $# -gt 1 ] && [ "$1" == "script" ]; } || { [ $# -gt 2 ] && [ "$2" == "script" ]; }; then + usage + elif [ $# -eq 1 ]; then + SCRIPT_OPTION=false + CUSTOM_OPTION=0 + parse_first_arg "$1" + elif [ $# -eq 2 ] && [ "$2" == "script" ]; then + if [ "$1" != "container" ] && [ "$1" != "local" ] && [ "$1" != "dev" ]; then + notif "Invalid first argument. Must be \`container\`, \`local\` or \`dev\`" + exit 1 + fi + SCRIPT_OPTION=true + CUSTOM_OPTION=0 + parse_first_arg "$1" + else + SCRIPT_OPTION=false + CUSTOM_OPTION=2 + parse_first_arg "$1" + fi +} + +# Parse the first argument passed to `run-kontrol.sh` +parse_first_arg() { + if [ "$1" == "container" ]; then + notif "Running in docker container (DEFAULT)" + export LOCAL=false + export SCRIPT_TESTS=$SCRIPT_OPTION + export CUSTOM_TESTS=$CUSTOM_OPTION + elif [ "$1" == "-h" ] || [ "$1" == "--help" ]; then + usage + elif [ "$1" == "local" ]; then + notif "Running with LOCAL install, .kontrolrc CI version ENFORCED" + export SCRIPT_TESTS=$SCRIPT_OPTION + export CUSTOM_TESTS=$CUSTOM_OPTION + check_kontrol_version + elif [ "$1" == "dev" ]; then + notif "Running with LOCAL install, IGNORING .kontrolrc version" + export LOCAL=true + export SCRIPT_TESTS=$SCRIPT_OPTION + export CUSTOM_TESTS=$CUSTOM_OPTION + elif [ "$1" == "script" ]; then + notif "Running in docker container (DEFAULT)" + export LOCAL=false + NEGATED_SCRIPT_TESTS=$([[ "${SCRIPT_OPTION}" == "true" ]] && echo false || echo true) + export SCRIPT_TESTS=$NEGATED_SCRIPT_TESTS + export CUSTOM_TESTS=$CUSTOM_OPTION + else + notif "Running in docker container (DEFAULT)" + export LOCAL=false + export SCRIPT_TESTS=$SCRIPT_OPTION + export CUSTOM_TESTS=1 # Store the position where custom tests start + fi +} + +check_kontrol_version() { + if [ "$(kontrol version | awk -F': ' '{print$2}')" == "$KONTROLRC" ]; then + notif "Kontrol version matches $KONTROLRC" + export LOCAL=true + else + notif "Kontrol version does NOT match $KONTROLRC" + notif "Please run 'kup install kontrol --version v$KONTROLRC'" + exit 1 + fi +} + +conditionally_start_docker() { + if [ "$LOCAL" == false ]; then + # Is old docker container running? + if [ "$(docker ps -q -f name="$CONTAINER_NAME")" ]; then + # Stop old docker container + notif "Stopping old docker container" + clean_docker + fi + start_docker + fi +} + +start_docker () { + docker run \ + --name "$CONTAINER_NAME" \ + --rm \ + --interactive \ + --detach \ + --env FOUNDRY_PROFILE="$FOUNDRY_PROFILE" \ + --workdir /home/user/workspace \ + runtimeverificationinc/kontrol:ubuntu-jammy-"$KONTROL_RELEASE" + + copy_to_docker +} + +copy_to_docker() { + # Copy test content to container + if [ "$LOCAL" == false ]; then + TMP_DIR=$(mktemp -d) + cp -r "$WORKSPACE_DIR/." "$TMP_DIR" + docker cp --follow-link "$TMP_DIR/." $CONTAINER_NAME:/home/user/workspace + rm -rf "$TMP_DIR" + + docker exec --user root "$CONTAINER_NAME" chown -R user:user /home/user + fi +} + +clean_docker(){ + trap + if [ "$LOCAL" = false ]; then + notif "Cleaning Docker Container" + docker stop "$CONTAINER_NAME" > /dev/null 2>&1 || true + docker rm "$CONTAINER_NAME" > /dev/null 2>&1 || true + sleep 2 # Give time for system to clean up container + else + notif "Not Running in Container. Done." + fi +} + + + +docker_exec () { + docker exec --user user --workdir /home/user/workspace $CONTAINER_NAME "${@}" +} + +run () { + if [ "$LOCAL" = true ]; then + notif "Running local" + # shellcheck disable=SC2086 + "${@}" + else + notif "Running in docker" + docker_exec "${@}" + fi +} + +# Define the function to run on failure +on_failure() { + get_log_results + + if [ "$LOCAL" = false ]; then + clean_docker + fi + + notif "Failure Cleanup Complete." + exit 1 +} diff --git a/test/kontrol/scripts/run-kontrol.sh b/test/kontrol/scripts/run-kontrol.sh new file mode 100755 index 00000000..d649f2d8 --- /dev/null +++ b/test/kontrol/scripts/run-kontrol.sh @@ -0,0 +1,176 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_HOME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +# shellcheck source=/dev/null +source "$SCRIPT_HOME/common.sh" +export RUN_KONTROL=true +CUSTOM_FOUNDRY_PROFILE=default +export FOUNDRY_PROFILE=$CUSTOM_FOUNDRY_PROFILE +export OUT_DIR=out # out dir of $FOUNDRY_PROFILE +parse_args "$@" + +############# +# Functions # +############# +kontrol_build() { + notif "Kontrol Build" + # shellcheck disable=SC2086 + run kontrol build \ + --verbose \ + --require $lemmas \ + --module-import $module \ + $rekompile + return $? +} + +kontrol_prove() { + notif "Kontrol Prove" + # shellcheck disable=SC2086 + run kontrol prove \ + --max-depth $max_depth \ + --max-iterations $max_iterations \ + --smt-timeout $smt_timeout \ + --workers $workers \ + $reinit \ + $bug_report \ + $break_on_calls \ + $break_every_step \ + $auto_abstract \ + $tests \ + $use_booster + return $? +} + +get_log_results(){ + trap clean_docker ERR + RESULTS_FILE="results-$(date +'%Y-%m-%d-%H-%M-%S').tar.gz" + LOG_PATH="$SCRIPT_HOME/logs" + RESULTS_LOG="$LOG_PATH/$RESULTS_FILE" + + if [ ! -d "$LOG_PATH" ]; then + mkdir "$LOG_PATH" + fi + + notif "Generating Results Log: $LOG_PATH" + + run tar -czvf results.tar.gz "$OUT_DIR" > /dev/null 2>&1 + if [ "$LOCAL" = true ]; then + mv results.tar.gz "$RESULTS_LOG" + else + docker cp "$CONTAINER_NAME:/home/user/workspace/results.tar.gz" "$RESULTS_LOG" + tar -xzvf "$RESULTS_LOG" + fi + if [ -f "$RESULTS_LOG" ]; then + cp "$RESULTS_LOG" "$LOG_PATH/kontrol-results_latest.tar.gz" + else + notif "Results Log: $RESULTS_LOG not found, skipping.." + fi + # Report where the file was generated and placed + notif "Results Log: $(dirname "$RESULTS_LOG") generated" + + if [ "$LOCAL" = false ]; then + notif "Results Log: $RESULTS_LOG generated" + RUN_LOG="run-kontrol-$(date +'%Y-%m-%d-%H-%M-%S').log" + docker logs "$CONTAINER_NAME" > "$LOG_PATH/$RUN_LOG" + fi +} + +######################### +# kontrol build options # +######################### +# NOTE: This script has a recurring pattern of setting and unsetting variables, +# such as `rekompile`. Such a pattern is intended for easy use while locally +# developing and executing the proofs via this script. Comment/uncomment the +# empty assignment to activate/deactivate the corresponding flag +lemmas=test/kontrol/lido-lemmas.k +base_module=LIDO-LEMMAS +module=VetoSignallingTest:$base_module +rekompile=--rekompile +rekompile= +regen=--regen +# shellcheck disable=SC2034 +regen= + +################################# +# Tests to symbolically execute # +################################# +test_list=() +if [ "$SCRIPT_TESTS" == true ]; then + # Here go the list of tests to execute with the `script` option + test_list=( "VetoSignallingTest.testTransitionNormalToVetoSignalling" ) +elif [ "$CUSTOM_TESTS" != 0 ]; then + test_list=( "${@:${CUSTOM_TESTS}}" ) +fi +tests="" +# If test_list is empty, tests will be empty as well +# This will make kontrol execute any `test`, `prove` or `check` prefixed-function +# under the foundry-defined `test` directory +for test_name in "${test_list[@]}"; do + tests+="--match-test $test_name " +done + +######################### +# kontrol prove options # +######################### +max_depth=10000 +max_iterations=10000 +smt_timeout=100000 +max_workers=16 # Should be at most (M - 8) / 8 in a machine with M GB of RAM +# workers is the minimum between max_workers and the length of test_list +# unless no test arguments are provided, in which case we default to max_workers +if [ "$CUSTOM_TESTS" == 0 ] && [ "$SCRIPT_TESTS" == false ]; then + workers=${max_workers} +else + workers=$((${#test_list[@]}>max_workers ? max_workers : ${#test_list[@]})) +fi +reinit=--reinit +reinit= +break_on_calls=--no-break-on-calls +break_on_calls= +break_every_step=--no-break-every-step +break_every_step= +auto_abstract=--auto-abstract-gas +auto_abstract= +bug_report=--bug-report +bug_report= +use_booster=--no-use-booster +use_booster= + + +############# +# RUN TESTS # +############# +# Set up the trap to run the function on failure +trap on_failure ERR INT +trap clean_docker EXIT +conditionally_start_docker + +results=() +# Run kontrol_build and store the result +kontrol_build +results[0]=$? + +# Run kontrol_prove and store the result +kontrol_prove +results[1]=$? + +get_log_results + +# Now you can use ${results[0]} and ${results[1]} +# to check the results of kontrol_build and kontrol_prove, respectively +if [ ${results[0]} -ne 0 ] && [ ${results[1]} -ne 0 ]; then + echo "Kontrol Build and Prove Failed" + exit 1 +elif [ ${results[0]} -ne 0 ]; then + echo "Kontrol Build Failed" + exit 1 +elif [ ${results[1]} -ne 0 ]; then + echo "Kontrol Prove Failed" + exit 2 + # Handle failure +else + echo "Kontrol Passed" +fi + +notif "DONE" diff --git a/test/kontrol/scripts/versions.json b/test/kontrol/scripts/versions.json new file mode 100644 index 00000000..38350d43 --- /dev/null +++ b/test/kontrol/scripts/versions.json @@ -0,0 +1,4 @@ +{ + "kontrol": "0.1.242", + "kontrol-cheatcodes": "master" +} From 23e2cfdd61624c6341fd4f91625b34b5ed6fecff Mon Sep 17 00:00:00 2001 From: lucasmt Date: Tue, 28 May 2024 18:24:51 -0500 Subject: [PATCH 073/134] Add Kontrol symbolic tests --- test/kontrol/VetoSignalling.t.sol | 398 ++++++++++++++++++++++++++++++ test/kontrol/lido-lemmas.k | 239 ++++++++++++++++++ 2 files changed, 637 insertions(+) create mode 100644 test/kontrol/VetoSignalling.t.sol create mode 100644 test/kontrol/lido-lemmas.k diff --git a/test/kontrol/VetoSignalling.t.sol b/test/kontrol/VetoSignalling.t.sol new file mode 100644 index 00000000..b69e60f8 --- /dev/null +++ b/test/kontrol/VetoSignalling.t.sol @@ -0,0 +1,398 @@ +pragma solidity 0.8.23; + +import "forge-std/Vm.sol"; +import "forge-std/Test.sol"; +import "kontrol-cheatcodes/KontrolCheats.sol"; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/utils/math/Math.sol"; + +import "contracts/model/DualGovernance.sol"; +import "contracts/model/EmergencyProtectedTimelock.sol"; +import "contracts/model/Escrow.sol"; + +contract FakeETH is ERC20("fakeETH", "fETH") {} + +contract VetoSignallingTest is Test, KontrolCheats { + DualGovernance dualGovernance; + EmergencyProtectedTimelock timelock; + ERC20 fakeETH; + Escrow signallingEscrow; + Escrow rageQuitEscrow; + + uint256 constant CURRENT_STATE_SLOT = 3; + uint256 constant CURRENT_STATE_OFFSET = 160; + + // Note: there are lemmas dependent on `ethUpperBound` + uint256 constant ethMaxWidth = 96; + uint256 constant ethUpperBound = 2 ** ethMaxWidth; + uint256 constant timeUpperBound = 2 ** 40 - 1; + + enum Mode { + Assume, + Assert + } + + function _establish(Mode mode, bool condition) internal view { + if (mode == Mode.Assume) { + vm.assume(condition); + } else { + assert(condition); + } + } + + function setUp() public { + fakeETH = new FakeETH(); + uint256 emergencyProtectionTimelock = 0; // Regular deployment mode + dualGovernance = new DualGovernance(address(fakeETH), emergencyProtectionTimelock); + timelock = dualGovernance.emergencyProtectedTimelock(); + signallingEscrow = dualGovernance.signallingEscrow(); + rageQuitEscrow = new Escrow(address(dualGovernance), address(fakeETH)); + + _fakeETHStorageSetup(); + _dualGovernanceStorageSetup(); + _signallingEscrowStorageSetup(); + _rageQuitEscrowStorageSetup(); + kevm.symbolicStorage(address(timelock)); // ?STORAGE3 + } + + function _fakeETHStorageSetup() internal { + kevm.symbolicStorage(address(fakeETH)); // ?STORAGE + // Slot 2 + uint256 totalSupply = kevm.freshUInt(32); // ?WORD + vm.assume(0 < totalSupply); + _storeUInt256(address(fakeETH), 2, totalSupply); + } + + function _dualGovernanceStorageSetup() internal { + kevm.symbolicStorage(address(dualGovernance)); // ?STORAGE0 + // Slot 0 + _storeAddress(address(dualGovernance), 0, address(timelock)); + // Slot 1 + _storeAddress(address(dualGovernance), 1, address(signallingEscrow)); + // Slot 2 + _storeAddress(address(dualGovernance), 2, address(rageQuitEscrow)); + // Slot 3 + uint8 state = uint8(kevm.freshUInt(1)); // ?WORD0 + vm.assume(state <= 4); + bytes memory slot_3_abi_encoding = abi.encodePacked(uint88(0), state, address(fakeETH)); + bytes32 slot_3_for_storage; + assembly { + slot_3_for_storage := mload(add(slot_3_abi_encoding, 0x20)) + } + _storeBytes32(address(dualGovernance), 3, slot_3_for_storage); + // Slot 6 + uint256 lastStateChangeTime = kevm.freshUInt(32); // ?WORD1 + vm.assume(lastStateChangeTime <= block.timestamp); + _storeUInt256(address(dualGovernance), 6, lastStateChangeTime); + // Slot 7 + uint256 lastSubStateActivationTime = kevm.freshUInt(32); // ?WORD2 + vm.assume(lastSubStateActivationTime <= block.timestamp); + _storeUInt256(address(dualGovernance), 7, lastSubStateActivationTime); + // Slot 8 + uint256 lastStateReactivationTime = kevm.freshUInt(32); // ?WORD3 + vm.assume(lastStateReactivationTime <= block.timestamp); + _storeUInt256(address(dualGovernance), 8, lastStateReactivationTime); + // Slot 9 + uint256 lastVetoSignallingTime = kevm.freshUInt(32); // ?WORD4 + vm.assume(lastVetoSignallingTime <= block.timestamp); + _storeUInt256(address(dualGovernance), 9, lastVetoSignallingTime); + // Slot 10 + uint256 rageQuitSequenceNumber = kevm.freshUInt(32); // ?WORD5 + vm.assume(rageQuitSequenceNumber < type(uint256).max); + _storeUInt256(address(dualGovernance), 10, rageQuitSequenceNumber); + } + + function _signallingEscrowStorageSetup() internal { + kevm.symbolicStorage(address(signallingEscrow)); // ?STORAGE1 + // Slot 0: currentState == 0 (SignallingEscrow), dualGovernance + uint8 currentState = 0; + bytes memory slot_0_abi_encoding = abi.encodePacked(uint88(0), address(dualGovernance), currentState); + bytes32 slot_0_for_storage; + assembly { + slot_0_for_storage := mload(add(slot_0_abi_encoding, 0x20)) + } + _storeBytes32(address(signallingEscrow), 0, slot_0_for_storage); + // Slot 1 + _storeAddress(address(signallingEscrow), 1, address(fakeETH)); + // Slot 3 + uint256 totalStaked = kevm.freshUInt(32); // ?WORD6 + _storeUInt256(address(signallingEscrow), 3, totalStaked); + // Slot 5 + uint256 totalClaimedEthAmount = kevm.freshUInt(32); // ?WORD7 + vm.assume(totalClaimedEthAmount <= totalStaked); + _storeUInt256(address(signallingEscrow), 5, totalClaimedEthAmount); + } + + function _rageQuitEscrowStorageSetup() internal { + kevm.symbolicStorage(address(rageQuitEscrow)); // ?STORAGE2 + // Slot 0: currentState == 1 (RageQuitEscrow), dualGovernance + uint8 currentState = 1; + bytes memory slot_0_abi_encoding = abi.encodePacked(uint88(0), address(dualGovernance), currentState); + bytes32 slot_0_for_storage; + assembly { + slot_0_for_storage := mload(add(slot_0_abi_encoding, 0x20)) + } + _storeBytes32(address(rageQuitEscrow), 0, slot_0_for_storage); + // Slot 1 + _storeAddress(address(rageQuitEscrow), 1, address(fakeETH)); + // Slot 3 + uint256 totalStaked = kevm.freshUInt(32); // ?WORD8 + _storeUInt256(address(rageQuitEscrow), 3, totalStaked); + // Slot 5 + uint256 totalClaimedEthAmount = kevm.freshUInt(32); // ?WORD9 + vm.assume(totalClaimedEthAmount <= totalStaked); + _storeUInt256(address(rageQuitEscrow), 5, totalClaimedEthAmount); + } + + function _storeBytes32(address contractAddress, uint256 slot, bytes32 value) internal { + vm.store(contractAddress, bytes32(slot), value); + } + + function _storeUInt256(address contractAddress, uint256 slot, uint256 value) internal { + vm.store(contractAddress, bytes32(slot), bytes32(value)); + } + + function _storeAddress(address contractAddress, uint256 slot, address value) internal { + vm.store(contractAddress, bytes32(slot), bytes32(uint256(uint160(value)))); + } + + struct StateRecord { + DualGovernance.State state; + uint256 timestamp; + uint256 rageQuitSupport; + uint256 maxRageQuitSupport; + uint256 activationTime; + uint256 reactivationTime; + } + + /** + * Invariants that should hold while in the Veto Signalling state + * (including Deactivation sub-state) + */ + function _vetoSignallingInvariants(Mode mode, StateRecord memory sr) internal view { + require( + sr.state != DualGovernance.State.Normal && sr.state != DualGovernance.State.VetoCooldown + && sr.state != DualGovernance.State.RageQuit, + "Invariants only apply to the Veto Signalling states." + ); + + _vetoSignallingTimesInvariant(mode, sr); + _vetoSignallingRageQuitInvariant(mode, sr); + _vetoSignallingDeactivationInvariant(mode, sr); + _vetoSignallingMaxDelayInvariant(mode, sr); + } + + /** + * Veto Signalling Invariant: At any given point t up to the present time, + * the Veto Signalling activation and reactivation times must be before t. + */ + function _vetoSignallingTimesInvariant(Mode mode, StateRecord memory sr) internal view { + _establish(mode, sr.timestamp <= block.timestamp); + _establish(mode, sr.activationTime <= sr.timestamp); + _establish(mode, sr.reactivationTime <= sr.timestamp); + } + + /** + * Veto Signalling Invariant: The rage quit support cannot be greater than + * the maximum rage quit support since entering the Veto Signalling state, + * and the maximum rage quit support must be greater than the first seal + * threshold. + */ + function _vetoSignallingRageQuitInvariant(Mode mode, StateRecord memory sr) internal view { + _establish(mode, sr.rageQuitSupport <= sr.maxRageQuitSupport); + _establish(mode, dualGovernance.FIRST_SEAL_RAGE_QUIT_SUPPORT() < sr.maxRageQuitSupport); + } + + /** + * Veto Signalling Invariant: If at a given time both the dynamic timelock + * for the current rage quit support AND the minimum active duration since + * the Deactivation sub-state was last exited have passed, the protocol is + * in the Deactivation sub-state. Otherwise, it is in the parent state. + */ + function _vetoSignallingDeactivationInvariant(Mode mode, StateRecord memory sr) internal view { + uint256 dynamicTimelock = dualGovernance.calculateDynamicTimelock(sr.rageQuitSupport); + + // Note: creates three branches in symbolic execution + if (sr.timestamp <= sr.activationTime + dynamicTimelock) { + _establish(mode, sr.state == DualGovernance.State.VetoSignalling); + } else if ( + sr.timestamp + <= Math.max(sr.reactivationTime, sr.activationTime) + dualGovernance.VETO_SIGNALLING_MIN_ACTIVE_DURATION() + ) { + _establish(mode, sr.state == DualGovernance.State.VetoSignalling); + } else { + _establish(mode, sr.state == DualGovernance.State.VetoSignallingDeactivation); + } + } + + /** + * Veto Signalling Invariant: If the maximum deactivation delay has passed, + * then the protocol must be in the Deactivation sub-state. + * + * The maximum deactivation delay is defined as T + D, where + * - T is the dynamic timelock for the maximum rage quit support obtained + * since entering the Veto Signalling state, and + * - D is the minimum active duration before the Deactivation sub-state can + * be re-entered. + */ + function _vetoSignallingMaxDelayInvariant(Mode mode, StateRecord memory sr) internal view { + // Note: creates two branches in symbolic execution + if (_maxDeactivationDelayPassed(sr)) { + _establish(mode, sr.state == DualGovernance.State.VetoSignallingDeactivation); + } + } + + function _maxDeactivationDelayPassed(StateRecord memory sr) internal view returns (bool) { + uint256 maxDeactivationDelay = dualGovernance.calculateDynamicTimelock(sr.maxRageQuitSupport) + + dualGovernance.VETO_SIGNALLING_MIN_ACTIVE_DURATION(); + + return sr.activationTime + maxDeactivationDelay < sr.timestamp; + } + + function _recordPreviousState( + uint256 lastInteractionTimestamp, + uint256 previousRageQuitSupport, + uint256 maxRageQuitSupport + ) internal view returns (StateRecord memory sr) { + sr.state = dualGovernance.currentState(); + sr.timestamp = lastInteractionTimestamp; + sr.rageQuitSupport = previousRageQuitSupport; + sr.maxRageQuitSupport = maxRageQuitSupport; + sr.activationTime = dualGovernance.lastStateChangeTime(); + sr.reactivationTime = dualGovernance.lastStateReactivationTime(); + } + + function _recordCurrentState(uint256 previousMaxRageQuitSupport) internal view returns (StateRecord memory sr) { + sr.state = dualGovernance.currentState(); + sr.timestamp = block.timestamp; + sr.rageQuitSupport = signallingEscrow.getRageQuitSupport(); + sr.maxRageQuitSupport = + previousMaxRageQuitSupport < sr.rageQuitSupport ? sr.rageQuitSupport : previousMaxRageQuitSupport; + sr.activationTime = dualGovernance.lastStateChangeTime(); + sr.reactivationTime = dualGovernance.lastStateReactivationTime(); + } + + /** + * Together, the three tests below verify the following: + * + * 1. After entering the Veto Signalling state, the Deactivation sub-state + * will be entered in at most time proportional to the maximum rage quit + * support observed since entering the Veto Signalling state. + * + * 2. If a new maximum rage quit support is not observed after this time, + * the Deactivation sub-state will not exit back to the parent state, and + * therefore the Veto Cooldown state will be entered after the maximum + * deactivation duration has elapsed. + * + * This places a bound on the maximum time that the protocol can be forced + * to stay in the Veto Signalling state. + */ + + /** + * Test that the Veto Signalling invariants hold when the Veto Signalling + * state is first entered. + */ + function testVetoSignallingInvariantsHoldInitially() external { + vm.assume(block.timestamp < timeUpperBound); + + vm.assume(dualGovernance.currentState() != DualGovernance.State.VetoSignalling); + vm.assume(dualGovernance.currentState() != DualGovernance.State.VetoSignallingDeactivation); + + dualGovernance.activateNextState(); + + StateRecord memory sr = _recordCurrentState(0); + + // Consider only the case where we have transitioned to Veto Signalling + vm.assume(sr.state == DualGovernance.State.VetoSignalling); + + _vetoSignallingInvariants(Mode.Assert, sr); + } + + /** + * Assuming that the previous state of the protocol is consistent with the + * Veto Signalling invariants, test that when we call activateNextState() + * the state remains consistent with the invariants. + */ + function testVetoSignallingInvariantsArePreserved( + uint256 lastInteractionTimestamp, + uint256 previousRageQuitSupport, + uint256 maxRageQuitSupport + ) external { + vm.assume(block.timestamp < timeUpperBound); + + vm.assume(lastInteractionTimestamp < timeUpperBound); + vm.assume(previousRageQuitSupport < ethUpperBound); + vm.assume(maxRageQuitSupport < ethUpperBound); + + // Temporary assumptions + vm.assume(previousRageQuitSupport <= 1); + vm.assume(10 <= maxRageQuitSupport); + vm.assume( + lastInteractionTimestamp + <= dualGovernance.lastStateChangeTime() + dualGovernance.calculateDynamicTimelock(previousRageQuitSupport) + ); + // Temparary assumptions + + StateRecord memory previous = + _recordPreviousState(lastInteractionTimestamp, previousRageQuitSupport, maxRageQuitSupport); + + vm.assume(previous.state != DualGovernance.State.Normal); + vm.assume(previous.state != DualGovernance.State.VetoCooldown); + vm.assume(previous.state != DualGovernance.State.RageQuit); + + _vetoSignallingInvariants(Mode.Assume, previous); + dualGovernance.activateNextState(); + + StateRecord memory current = _recordCurrentState(maxRageQuitSupport); + + if ( + current.state != DualGovernance.State.Normal && current.state != DualGovernance.State.VetoCooldown + && current.state != DualGovernance.State.RageQuit + ) { + _vetoSignallingInvariants(Mode.Assert, current); + } + } + + /** + * Test that, given the Veto Signalling invariants, then if + * a) the maximum deactivation delay passes, and + * b) we don't observe a new maximum rage quit support, + * then the protocol cannot have exited the Deactivation sub-state. + */ + function testDeactivationNotCancelled( + uint256 lastInteractionTimestamp, + uint256 previousRageQuitSupport, + uint256 maxRageQuitSupport + ) external { + StateRecord memory previous = + _recordPreviousState(lastInteractionTimestamp, previousRageQuitSupport, maxRageQuitSupport); + + vm.assume(_maxDeactivationDelayPassed(previous)); + vm.assume(signallingEscrow.getRageQuitSupport() <= previous.maxRageQuitSupport); + + vm.assume( + previous.state == DualGovernance.State.VetoSignalling + || previous.state == DualGovernance.State.VetoSignallingDeactivation + ); + + _vetoSignallingInvariants(Mode.Assume, previous); + + assert(previous.state == DualGovernance.State.VetoSignallingDeactivation); + + dualGovernance.activateNextState(); + + StateRecord memory current = _recordCurrentState(maxRageQuitSupport); + + uint256 deactivationStartTime = dualGovernance.lastSubStateActivationTime(); + uint256 deactivationEndTime = deactivationStartTime + dualGovernance.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION(); + + // The protocol is either in the Deactivation sub-state, or, if the + // maximum deactivation duration has passed, in the Veto Cooldown state + if (deactivationEndTime < block.timestamp) { + assert(current.state == DualGovernance.State.VetoCooldown); + } else { + assert(current.state == DualGovernance.State.VetoSignallingDeactivation); + } + } +} diff --git a/test/kontrol/lido-lemmas.k b/test/kontrol/lido-lemmas.k new file mode 100644 index 00000000..65da0a19 --- /dev/null +++ b/test/kontrol/lido-lemmas.k @@ -0,0 +1,239 @@ +requires "foundry.md" + +module LIDO-LEMMAS + imports FOUNDRY + imports INT-SYMBOLIC + imports MAP-SYMBOLIC + imports SET-SYMBOLIC + + syntax StepSort ::= Int + | Bool + | Bytes + | Set + // ------------------------- + + syntax KItem ::= runLemma ( StepSort ) + | doneLemma( StepSort ) + // -------------------------------------- + rule runLemma(T) => doneLemma(T) ... + + syntax Int ::= "ethMaxWidth" [macro] + syntax Int ::= "ethUpperBound" [macro] + // -------------------------------------- + rule ethMaxWidth => 96 + rule ethUpperBound => 2 ^Int ethMaxWidth + // ---------------------------------------- + + // /Int to byte representation + rule X /Int pow160 => #asWord ( #range ( #buf ( 32 , X ) , 0 , 12 ) ) + requires #rangeUInt(256, X) + [simplification, preserves-definedness] + + // Deconstruction of mask + rule 115792089237316195423570984636004990333889740523700931696805413995650331181055 &Int X => + #asWord ( #range(#buf(32, X), 0, 11) +Bytes #buf(1, 0) +Bytes #range(#buf(32, X), 12, 20) ) + requires #rangeUInt(256, X) + [simplification] + + // |Int distributivity over #asWord and +Bytes, v1 + rule A |Int #asWord ( BA1 +Bytes BA2 ) => + #asWord ( BA1 +Bytes #buf ( lengthBytes(BA2), A |Int #asWord ( BA2 ) ) ) + requires 0 <=Int A andBool A + #asWord ( + #buf ( lengthBytes(BA1), (A >>Int (8 *Int lengthBytes(BA2))) |Int #asWord ( BA1 ) ) + +Bytes + #buf ( lengthBytes(BA2), (A modInt (2 ^Int (8 *Int lengthBytes(BA2)))) |Int #asWord ( BA2 ) ) + ) + requires #rangeUInt(256, A) + [simplification, concrete(A, BA1)] + + // |Int and #asWord + rule #range ( #buf ( A, X |Int Y) , 0, B ) => + #buf ( B, X >>Int (8 *Int (A -Int B)) ) + requires B <=Int A + andBool 0 <=Int X andBool X A ==Int (-1) *Int B + requires #rangeUInt(256, A) andBool #rangeUInt(256, (-1) *Int B) + [concrete(B), simplification, comm] + + // *Int + rule A *Int B ==Int 0 => A ==Int 0 orBool B ==Int 0 [simplification] + + // /Int + rule 0 /Int B => 0 requires B =/=Int 0 [simplification, preserves-definedness] + rule A /Int B ==Int 0 => A ==Int 0 requires B =/=Int 0 [simplification, preserves-definedness] + + // /Word + rule _ /Word W1 => 0 requires W1 ==Int 0 [simplification] + rule W0 /Word W1 => W0 /Int W1 requires W1 =/=Int 0 [simplification, preserves-definedness] + + // Further arithmetic + rule ( X *Int Y ) /Int Y => X requires Y =/=Int 0 [simplification, preserves-definedness] + rule ( X ==Int ( X *Int Y ) /Word Y ) orBool Y ==Int 0 => true [simplification, preserves-definedness] + + rule A <=Int B /Int C => A *Int C <=Int B requires 0 (A +Int 1) *Int C <=Int B requires 0 Int B /Int C => A *Int C >Int B requires 0 =Int B /Int C => (A +Int 1) *Int C >Int B requires 0 =Int A => A *Int C <=Int B requires 0 Int A => (A +Int 1) *Int C <=Int B requires 0 A *Int C >Int B requires 0 (A +Int 1) *Int C >Int B requires 0 #asWord ( #range(BA, lengthBytes(BA) -Int (log2Int(X +Int 1) /Int 8), log2Int(X +Int 1) /Int 8) ) + requires #rangeUInt(256, X) + andBool X +Int 1 ==Int 2 ^Int log2Int(X +Int 1) + andBool log2Int (X +Int 1) modInt 8 ==Int 0 + andBool (log2Int (X +Int 1)) /Int 8 <=Int lengthBytes(BA) andBool lengthBytes(BA) <=Int 32 + [simplification, concrete(X), preserves-definedness] + + // &Int distributivity + rule X &Int ( Y |Int Z ) => ( X &Int Y ) |Int ( X &Int Z ) [simplification, concrete(X)] + rule X &Int ( Y &Int Z ) => ( X &Int Y ) &Int ( X &Int Z ) [simplification, concrete(X)] + + // KEVM simplification + rule #asWord(WS) >>Int M => #asWord(#range(WS, 0, lengthBytes(WS) -Int (M /Int 8) )) + requires 0 <=Int M andBool M modInt 8 ==Int 0 + [simplification, preserves-definedness] + + // + // .Bytes + // + rule .Bytes ==K b"" => true [simplification, comm] + + rule b"" ==K #buf(X, _) +Bytes _ => false requires 0 false requires 0 false requires 0 false requires 0 B:Bytes [simplification] + rule [concat-neutral-right]: B:Bytes +Bytes b"" => B:Bytes [simplification] + + // + // Alternative memory update + // + rule [memUpdate-concat-in-right]: (B1 +Bytes B2) [ S := B ] => B1 +Bytes (B2 [ S -Int lengthBytes(B1) := B ]) + requires lengthBytes(B1) <=Int S + [simplification(40)] + + rule [memUpdate-concat-in-left]: (B1 +Bytes B2) [ S := B ] => (B1 [S := B]) +Bytes B2 + requires 0 <=Int S andBool S +Int lengthBytes(B) <=Int lengthBytes(B1) + [simplification(45)] + + // + // Specific simplifications + // + rule X &Int #asWord ( BA ) ==Int Y:Int => true + requires 0 <=Int X andBool X false + requires 0 <=Int X andBool X true + requires 0 <=Int X andBool X false + requires 0 <=Int X andBool X true + requires 0 <=Int X andBool X false + requires 0 <=Int X andBool X 0 + requires 0 <=Int X andBool 0 <=Int Y andBool 0 <=Int Z + andBool X +Int 1 ==Int 2 ^Int log2Int(X +Int 1) + andBool Y ==Int 2 ^Int log2Int(Y) + andBool log2Int(X +Int 1) <=Int log2Int(Y) + [simplification, concrete(X, Y), preserves-definedness] + + rule X &Int ( Y *Int Z ) => 0 + requires 0 <=Int X andBool 0 <=Int Y andBool 0 <=Int Z + andBool X +Int 1 ==Int 2 ^Int log2Int(X +Int 1) + andBool Z ==Int 2 ^Int log2Int(Z) + andBool log2Int(X +Int 1) <=Int log2Int(Z) + [simplification, concrete(X, Z), preserves-definedness] + + rule chop ( X *Int Y ) => X *Int Y + requires 0 <=Int X andBool X X *Int Y { true #Equals X *Int Y true [simplification, smt-lemma] + + rule X >>Int N => X /Int (2 ^Int N) [simplification, concrete(N)] + + rule #asWord ( BUF1 +Bytes BUF2 ) => #asWord ( BUF2 ) + requires #asWord ( BUF1 ) ==Int 0 + [simplification, concrete(BUF1)] + +endmodule + +module LIDO-LEMMAS-SPEC + imports LIDO-LEMMAS + + claim [storage-offset]: runLemma ( ( #lookup ( STORAGE3:Map , 2 ) /Int pow160 ) ) => doneLemma ( #asWord ( #range ( #buf ( 32 , #lookup ( STORAGE3:Map , 2 ) ) , 0 , 12 ) ) ) ... + + claim [chop-simplify]: runLemma ( + notBool chop ( WORD7:Int +Int ( WORD12:Int *Int ( ( WORD5:Int -Int WORD6:Int ) /Int WORD11:Int ) ) ) ==Int + chop ( chop ( WORD7:Int +Int ( WORD12:Int *Int ( ( WORD5:Int -Int WORD6:Int ) /Int WORD11:Int ) ) ) *Int 1000000000000000000 ) /Int 1000000000000000000 + ) => runLemma ( false ) ... + requires 0 <=Int WORD5:Int + andBool 0 <=Int WORD6:Int + andBool 0 <=Int WORD7:Int + andBool 0 <=Int WORD11:Int + andBool 0 <=Int WORD12:Int + andBool WORD11:Int =/=Int 0 + andBool WORD12:Int =/=Int 0 + andBool WORD6:Int <=Int WORD5:Int + andBool WORD5:Int Date: Wed, 29 May 2024 12:31:10 -0500 Subject: [PATCH 074/134] forge install: kontrol-cheatcodes --- .gitmodules | 3 +++ lib/kontrol-cheatcodes | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/kontrol-cheatcodes diff --git a/.gitmodules b/.gitmodules index 888d42dc..413cbca7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/kontrol-cheatcodes"] + path = lib/kontrol-cheatcodes + url = https://github.com/runtimeverification/kontrol-cheatcodes diff --git a/lib/kontrol-cheatcodes b/lib/kontrol-cheatcodes new file mode 160000 index 00000000..0048278e --- /dev/null +++ b/lib/kontrol-cheatcodes @@ -0,0 +1 @@ +Subproject commit 0048278ebdfb04f452a448bf1fe19a06205efae3 From 8b61c358af745c45f931846eb072565f05752e73 Mon Sep 17 00:00:00 2001 From: lucasmt Date: Wed, 29 May 2024 12:43:19 -0500 Subject: [PATCH 075/134] Add simple testTransitionNormalToVetoSignalling test --- test/kontrol/VetoSignalling.t.sol | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/kontrol/VetoSignalling.t.sol b/test/kontrol/VetoSignalling.t.sol index b69e60f8..e53acd32 100644 --- a/test/kontrol/VetoSignalling.t.sol +++ b/test/kontrol/VetoSignalling.t.sol @@ -157,6 +157,18 @@ contract VetoSignallingTest is Test, KontrolCheats { vm.store(contractAddress, bytes32(slot), bytes32(uint256(uint160(value)))); } + /** + * Test that the Normal state transitions to VetoSignalling if the total + * veto power in the signalling escrow exceeds the first seal threshold. + */ + function testTransitionNormalToVetoSignalling() external { + uint256 rageQuitSupport = signallingEscrow.getRageQuitSupport(); + vm.assume(rageQuitSupport > dualGovernance.FIRST_SEAL_RAGE_QUIT_SUPPORT()); + vm.assume(dualGovernance.currentState() == DualGovernance.State.Normal); + dualGovernance.activateNextState(); + assert(dualGovernance.currentState() == DualGovernance.State.VetoSignalling); + } + struct StateRecord { DualGovernance.State state; uint256 timestamp; From a9ba1a18f040bba8a11986eb7cd8f301a16a371b Mon Sep 17 00:00:00 2001 From: lucasmt Date: Wed, 29 May 2024 16:31:46 -0500 Subject: [PATCH 076/134] Bound totalStaked in VetoSignallingTest --- test/kontrol/VetoSignalling.t.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/kontrol/VetoSignalling.t.sol b/test/kontrol/VetoSignalling.t.sol index e53acd32..514ca2e7 100644 --- a/test/kontrol/VetoSignalling.t.sol +++ b/test/kontrol/VetoSignalling.t.sol @@ -117,6 +117,7 @@ contract VetoSignallingTest is Test, KontrolCheats { _storeAddress(address(signallingEscrow), 1, address(fakeETH)); // Slot 3 uint256 totalStaked = kevm.freshUInt(32); // ?WORD6 + vm.assume(totalStaked < ethUpperBound); _storeUInt256(address(signallingEscrow), 3, totalStaked); // Slot 5 uint256 totalClaimedEthAmount = kevm.freshUInt(32); // ?WORD7 @@ -138,6 +139,7 @@ contract VetoSignallingTest is Test, KontrolCheats { _storeAddress(address(rageQuitEscrow), 1, address(fakeETH)); // Slot 3 uint256 totalStaked = kevm.freshUInt(32); // ?WORD8 + vm.assume(totalStaked < ethUpperBound); _storeUInt256(address(rageQuitEscrow), 3, totalStaked); // Slot 5 uint256 totalClaimedEthAmount = kevm.freshUInt(32); // ?WORD9 From 9e8c20a333f7e55d3f728e9de5eb870825f14eff Mon Sep 17 00:00:00 2001 From: lucasmt Date: Thu, 30 May 2024 15:15:28 -0500 Subject: [PATCH 077/134] Tweaks to lido-ci.yml --- .github/workflows/lido-ci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lido-ci.yml b/.github/workflows/lido-ci.yml index c8204248..8e66ddee 100644 --- a/.github/workflows/lido-ci.yml +++ b/.github/workflows/lido-ci.yml @@ -1,13 +1,12 @@ --- name: "Test Proofs" on: - workflow_dispatch: pull_request: branches: - main jobs: test: - runs-on: [self-hosted, linux, flyweight] + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 @@ -21,7 +20,7 @@ response=$(curl -X POST \ -w "%{http_code}" \ -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ secrets.KAAS_COMPUTE_TOKEN }}" \ + -H "Authorization: Bearer ${{ secrets.RV_COMPUTE_TOKEN }}" \ https://api.github.com/repos/runtimeverification/_kaas_lidofinance_dual-governance/actions/workflows/lido-ci.yml/dispatches \ -d '{ "ref": "master", @@ -31,7 +30,7 @@ "statuses_sha": "'$sha'", "org": "runtimeverification", "repository": "_audits_lidofinance_dual-governance", - "auth_token": "'"${{ secrets.KAAS_COMPUTE_TOKEN }}"'" + "auth_token": "'"${{ secrets.LIDO_STATUS_TOKEN }}"'" } }') From 7edbf4b9f247c03c8ec5f265facc7164b300f01f Mon Sep 17 00:00:00 2001 From: lucasmt Date: Fri, 31 May 2024 11:46:10 -0500 Subject: [PATCH 078/134] Change IERC20 to ERC20 in the Escrow model To facilitate Compositional Symbolic Execution --- contracts/model/Escrow.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/model/Escrow.sol b/contracts/model/Escrow.sol index 0df4f505..900ebe71 100644 --- a/contracts/model/Escrow.sol +++ b/contracts/model/Escrow.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; /** * Simplified abstract model of the Escrow contract. Includes the following simplifications: @@ -30,7 +30,7 @@ contract Escrow { State public currentState; address public dualGovernance; - IERC20 public fakeETH; + ERC20 public fakeETH; mapping(address => uint256) public balances; uint256 public totalStaked; uint256 public totalWithdrawalRequestAmount; @@ -59,7 +59,7 @@ contract Escrow { constructor(address _dualGovernance, address _fakeETH) { currentState = State.SignallingEscrow; dualGovernance = _dualGovernance; - fakeETH = IERC20(_fakeETH); + fakeETH = ERC20(_fakeETH); } // Locks a specified amount of tokens. From 2e2e1731d18c98a7d55593039119da2fdea16596 Mon Sep 17 00:00:00 2001 From: lucasmt Date: Fri, 31 May 2024 11:57:50 -0500 Subject: [PATCH 079/134] Tweaks to VetoSignallingTest --- test/kontrol/VetoSignalling.t.sol | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/kontrol/VetoSignalling.t.sol b/test/kontrol/VetoSignalling.t.sol index 514ca2e7..2451de91 100644 --- a/test/kontrol/VetoSignalling.t.sol +++ b/test/kontrol/VetoSignalling.t.sol @@ -123,6 +123,9 @@ contract VetoSignallingTest is Test, KontrolCheats { uint256 totalClaimedEthAmount = kevm.freshUInt(32); // ?WORD7 vm.assume(totalClaimedEthAmount <= totalStaked); _storeUInt256(address(signallingEscrow), 5, totalClaimedEthAmount); + // Slot 11 + uint256 rageQuitExtensionDelayPeriodEnd = 0; // since SignallingEscrow + _storeUInt256(address(signallingEscrow), 11, rageQuitExtensionDelayPeriodEnd); } function _rageQuitEscrowStorageSetup() internal { @@ -145,6 +148,9 @@ contract VetoSignallingTest is Test, KontrolCheats { uint256 totalClaimedEthAmount = kevm.freshUInt(32); // ?WORD9 vm.assume(totalClaimedEthAmount <= totalStaked); _storeUInt256(address(rageQuitEscrow), 5, totalClaimedEthAmount); + // Slot 11 + uint256 rageQuitExtensionDelayPeriodEnd = kevm.freshUInt(32); // ?WORD10 + _storeUInt256(address(signallingEscrow), 11, rageQuitExtensionDelayPeriodEnd); } function _storeBytes32(address contractAddress, uint256 slot, bytes32 value) internal { @@ -318,9 +324,9 @@ contract VetoSignallingTest is Test, KontrolCheats { StateRecord memory sr = _recordCurrentState(0); // Consider only the case where we have transitioned to Veto Signalling - vm.assume(sr.state == DualGovernance.State.VetoSignalling); - - _vetoSignallingInvariants(Mode.Assert, sr); + if (sr.state == DualGovernance.State.VetoSignalling) { + _vetoSignallingInvariants(Mode.Assert, sr); + } } /** From f6233ec65caff64c6218388340dc5aa84f0ec97d Mon Sep 17 00:00:00 2001 From: lucasmt Date: Fri, 31 May 2024 15:34:22 -0500 Subject: [PATCH 080/134] Further tweaks to lido-ci.yml --- .github/workflows/lido-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lido-ci.yml b/.github/workflows/lido-ci.yml index 8e66ddee..9d60dea3 100644 --- a/.github/workflows/lido-ci.yml +++ b/.github/workflows/lido-ci.yml @@ -28,8 +28,8 @@ "branch_name": "'"${{ github.event.pull_request.head.sha || github.sha }}"'", "extra_args": "script", "statuses_sha": "'$sha'", - "org": "runtimeverification", - "repository": "_audits_lidofinance_dual-governance", + "org": "lidofinance", + "repository": "dual-governance", "auth_token": "'"${{ secrets.LIDO_STATUS_TOKEN }}"'" } }') From 741f39f0bcc6aa330e14e9a91d1cb72131c4e60e Mon Sep 17 00:00:00 2001 From: F-WRunTime Date: Wed, 12 Jun 2024 14:31:40 -0600 Subject: [PATCH 081/134] Committing KaaS Compute Setup for RV Testing / Development --- .github/workflows/lido-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lido-ci.yml b/.github/workflows/lido-ci.yml index 9d60dea3..7e3386e7 100644 --- a/.github/workflows/lido-ci.yml +++ b/.github/workflows/lido-ci.yml @@ -23,13 +23,13 @@ -H "Authorization: Bearer ${{ secrets.RV_COMPUTE_TOKEN }}" \ https://api.github.com/repos/runtimeverification/_kaas_lidofinance_dual-governance/actions/workflows/lido-ci.yml/dispatches \ -d '{ - "ref": "master", + "ref": "rvdevelop", "inputs": { "branch_name": "'"${{ github.event.pull_request.head.sha || github.sha }}"'", "extra_args": "script", "statuses_sha": "'$sha'", - "org": "lidofinance", - "repository": "dual-governance", + "org": "runtimeverification", + "repository": "_audits_lidofinance_dual-governance_fork", "auth_token": "'"${{ secrets.LIDO_STATUS_TOKEN }}"'" } }') From 7a51acf740d23c89894032ac4516919fe7639298 Mon Sep 17 00:00:00 2001 From: F-WRunTime Date: Wed, 12 Jun 2024 14:39:01 -0600 Subject: [PATCH 082/134] Test using variables to get org name / repo name --- .github/workflows/lido-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lido-ci.yml b/.github/workflows/lido-ci.yml index 7e3386e7..264805d3 100644 --- a/.github/workflows/lido-ci.yml +++ b/.github/workflows/lido-ci.yml @@ -28,8 +28,8 @@ "branch_name": "'"${{ github.event.pull_request.head.sha || github.sha }}"'", "extra_args": "script", "statuses_sha": "'$sha'", - "org": "runtimeverification", - "repository": "_audits_lidofinance_dual-governance_fork", + "org": "${{ github.repository_owner }}", + "repository": "${{ github.event.repository.name }}", "auth_token": "'"${{ secrets.LIDO_STATUS_TOKEN }}"'" } }') From 60eebbb5aa6e8c790b0f9c5dbdc83a1dceda5e79 Mon Sep 17 00:00:00 2001 From: Freeman <105403280+F-WRunTime@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:55:02 -0600 Subject: [PATCH 083/134] Update lido-ci.yml --- .github/workflows/lido-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lido-ci.yml b/.github/workflows/lido-ci.yml index 264805d3..ebcf2963 100644 --- a/.github/workflows/lido-ci.yml +++ b/.github/workflows/lido-ci.yml @@ -3,7 +3,7 @@ on: pull_request: branches: - - main + - develop jobs: test: runs-on: ubuntu-latest From 5070a16404bb21a6e1762405f09b25f58f5a9d48 Mon Sep 17 00:00:00 2001 From: F-WRunTime Date: Wed, 12 Jun 2024 15:12:10 -0600 Subject: [PATCH 084/134] Merge conflict resolution --- .github/workflows/lido-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lido-ci.yml b/.github/workflows/lido-ci.yml index ebcf2963..1ff7f9f5 100644 --- a/.github/workflows/lido-ci.yml +++ b/.github/workflows/lido-ci.yml @@ -1,9 +1,10 @@ --- name: "Test Proofs" on: - pull_request: + push: branches: - develop + - rvdevelop jobs: test: runs-on: ubuntu-latest From fb685aa6839fcc304ebc1a8739da0d82e8c6ddc8 Mon Sep 17 00:00:00 2001 From: lucasmt Date: Wed, 12 Jun 2024 13:37:22 -0500 Subject: [PATCH 085/134] Add separate profile for kontrol proofs --- foundry.toml | 6 ++++++ test/kontrol/scripts/run-kontrol.sh | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/foundry.toml b/foundry.toml index 168aec6d..196e3fca 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,6 +5,12 @@ libs = ['node_modules', 'lib'] test = 'test' cache_path = 'cache_forge' solc-version = "0.8.23" +no-match-path = 'test/kontrol/*' + +[profile.kprove] +src = 'test/kontrol' +out = 'kout' +test = 'test/kontrol' [fmt] multiline_func_header = 'params_first' diff --git a/test/kontrol/scripts/run-kontrol.sh b/test/kontrol/scripts/run-kontrol.sh index d649f2d8..cd055976 100755 --- a/test/kontrol/scripts/run-kontrol.sh +++ b/test/kontrol/scripts/run-kontrol.sh @@ -5,9 +5,9 @@ SCRIPT_HOME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" # shellcheck source=/dev/null source "$SCRIPT_HOME/common.sh" export RUN_KONTROL=true -CUSTOM_FOUNDRY_PROFILE=default +CUSTOM_FOUNDRY_PROFILE=kprove export FOUNDRY_PROFILE=$CUSTOM_FOUNDRY_PROFILE -export OUT_DIR=out # out dir of $FOUNDRY_PROFILE +export OUT_DIR=kout # out dir of $FOUNDRY_PROFILE parse_args "$@" ############# From 0e48f528cd6677ead9c9c7113b653261e40bdec6 Mon Sep 17 00:00:00 2001 From: lucasmt Date: Wed, 12 Jun 2024 15:28:49 -0500 Subject: [PATCH 086/134] Update model --- contracts/model/DualGovernance.sol | 11 +++++------ contracts/model/EmergencyProtectedTimelock.sol | 2 -- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/contracts/model/DualGovernance.sol b/contracts/model/DualGovernance.sol index 5ab56511..490378ac 100644 --- a/contracts/model/DualGovernance.sol +++ b/contracts/model/DualGovernance.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import "@openzeppelin/contracts/utils/math/Math.sol"; + import "./EmergencyProtectedTimelock.sol"; import "./Escrow.sol"; @@ -84,8 +86,9 @@ contract DualGovernance { "Proposals can only be scheduled in Normal or Veto Cooldown states." ); if (currentState == State.VetoCooldown) { + uint256 submissionTime = emergencyProtectedTimelock.proposals(proposalId).submissionTime; require( - block.timestamp < lastVetoSignallingTime, + submissionTime < lastVetoSignallingTime, "Proposal submitted after the last time Veto Signalling state was entered." ); } @@ -174,10 +177,6 @@ contract DualGovernance { currentState = parentState; } - function max(uint256 a, uint256 b) private pure returns (uint256) { - return a > b ? a : b; - } - // State Transitions function activateNextState() public { @@ -231,7 +230,7 @@ contract DualGovernance { transitionState(State.RageQuit); } else if ( block.timestamp - lastStateChangeTime > calculateDynamicTimelock(rageQuitSupport) - && block.timestamp - max(lastStateChangeTime, lastStateReactivationTime) + && block.timestamp - Math.max(lastStateChangeTime, lastStateReactivationTime) > VETO_SIGNALLING_MIN_ACTIVE_DURATION ) { enterSubState(State.VetoSignallingDeactivation); diff --git a/contracts/model/EmergencyProtectedTimelock.sol b/contracts/model/EmergencyProtectedTimelock.sol index df673108..f63bd6c9 100644 --- a/contracts/model/EmergencyProtectedTimelock.sol +++ b/contracts/model/EmergencyProtectedTimelock.sol @@ -55,8 +55,6 @@ contract EmergencyProtectedTimelock { function submit(address executor, ExecutorCall[] memory calls) external returns (uint256 proposalId) { // Ensure that only the governance can submit new proposals. require(msg.sender == governance, "Only governance can submit proposal."); - // Establish the minimum timelock duration for the proposal's execution. - uint256 executionDelay = PROPOSAL_EXECUTION_MIN_TIMELOCK; proposals[nextProposalId].id = nextProposalId; proposals[nextProposalId].proposer = executor; From 246d5f85f3ebdada5664783e5b0e92f6fba93b3b Mon Sep 17 00:00:00 2001 From: lucasmt Date: Wed, 12 Jun 2024 14:17:20 -0500 Subject: [PATCH 087/134] Fix VetoSignalling tests --- test/kontrol/VetoSignalling.t.sol | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/test/kontrol/VetoSignalling.t.sol b/test/kontrol/VetoSignalling.t.sol index 2451de91..0db58bd3 100644 --- a/test/kontrol/VetoSignalling.t.sol +++ b/test/kontrol/VetoSignalling.t.sol @@ -26,7 +26,7 @@ contract VetoSignallingTest is Test, KontrolCheats { // Note: there are lemmas dependent on `ethUpperBound` uint256 constant ethMaxWidth = 96; uint256 constant ethUpperBound = 2 ** ethMaxWidth; - uint256 constant timeUpperBound = 2 ** 40 - 1; + uint256 constant timeUpperBound = 2 ** 40; enum Mode { Assume, @@ -150,7 +150,7 @@ contract VetoSignallingTest is Test, KontrolCheats { _storeUInt256(address(rageQuitEscrow), 5, totalClaimedEthAmount); // Slot 11 uint256 rageQuitExtensionDelayPeriodEnd = kevm.freshUInt(32); // ?WORD10 - _storeUInt256(address(signallingEscrow), 11, rageQuitExtensionDelayPeriodEnd); + _storeUInt256(address(rageQuitEscrow), 11, rageQuitExtensionDelayPeriodEnd); } function _storeBytes32(address contractAddress, uint256 slot, bytes32 value) internal { @@ -345,15 +345,6 @@ contract VetoSignallingTest is Test, KontrolCheats { vm.assume(previousRageQuitSupport < ethUpperBound); vm.assume(maxRageQuitSupport < ethUpperBound); - // Temporary assumptions - vm.assume(previousRageQuitSupport <= 1); - vm.assume(10 <= maxRageQuitSupport); - vm.assume( - lastInteractionTimestamp - <= dualGovernance.lastStateChangeTime() + dualGovernance.calculateDynamicTimelock(previousRageQuitSupport) - ); - // Temparary assumptions - StateRecord memory previous = _recordPreviousState(lastInteractionTimestamp, previousRageQuitSupport, maxRageQuitSupport); @@ -385,9 +376,12 @@ contract VetoSignallingTest is Test, KontrolCheats { uint256 previousRageQuitSupport, uint256 maxRageQuitSupport ) external { + vm.assume(block.timestamp < timeUpperBound); + StateRecord memory previous = _recordPreviousState(lastInteractionTimestamp, previousRageQuitSupport, maxRageQuitSupport); + vm.assume(previous.maxRageQuitSupport <= dualGovernance.SECOND_SEAL_RAGE_QUIT_SUPPORT()); vm.assume(_maxDeactivationDelayPassed(previous)); vm.assume(signallingEscrow.getRageQuitSupport() <= previous.maxRageQuitSupport); From 699ed04da1508b6997ed4c7fcea0abdcad4f4750 Mon Sep 17 00:00:00 2001 From: lucasmt Date: Wed, 12 Jun 2024 17:30:59 -0500 Subject: [PATCH 088/134] Fix check in Escrow model --- contracts/model/Escrow.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/model/Escrow.sol b/contracts/model/Escrow.sol index 900ebe71..cb256e30 100644 --- a/contracts/model/Escrow.sol +++ b/contracts/model/Escrow.sol @@ -67,7 +67,7 @@ contract Escrow { require(currentState == State.SignallingEscrow, "Cannot lock in current state."); require(amount > 0, "Amount must be greater than zero."); require(fakeETH.allowance(msg.sender, address(this)) >= amount, "Need allowance to transfer tokens."); - require(fakeETH.balanceOf(msg.sender) <= amount, "Not enough balance."); + require(fakeETH.balanceOf(msg.sender) >= amount, "Not enough balance."); fakeETH.transferFrom(msg.sender, address(this), amount); balances[msg.sender] += amount; totalStaked += amount; From ac35908ee6f6e6057a6ac99ce0f94dc04f7223f4 Mon Sep 17 00:00:00 2001 From: lucasmt Date: Wed, 12 Jun 2024 16:40:58 -0500 Subject: [PATCH 089/134] Revert changes to lido-ci.yml for merge into parent repo --- .github/workflows/lido-ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/lido-ci.yml b/.github/workflows/lido-ci.yml index 1ff7f9f5..2b4f13ad 100644 --- a/.github/workflows/lido-ci.yml +++ b/.github/workflows/lido-ci.yml @@ -4,7 +4,6 @@ push: branches: - develop - - rvdevelop jobs: test: runs-on: ubuntu-latest @@ -24,7 +23,7 @@ -H "Authorization: Bearer ${{ secrets.RV_COMPUTE_TOKEN }}" \ https://api.github.com/repos/runtimeverification/_kaas_lidofinance_dual-governance/actions/workflows/lido-ci.yml/dispatches \ -d '{ - "ref": "rvdevelop", + "ref": "develop", "inputs": { "branch_name": "'"${{ github.event.pull_request.head.sha || github.sha }}"'", "extra_args": "script", From e32250942b15d04c39aa7cb53512cd1da2ef0ac9 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Thu, 13 Jun 2024 16:27:41 +0300 Subject: [PATCH 090/134] fix: dual governance model proposals getter fix --- contracts/model/DualGovernance.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/model/DualGovernance.sol b/contracts/model/DualGovernance.sol index 490378ac..730c272c 100644 --- a/contracts/model/DualGovernance.sol +++ b/contracts/model/DualGovernance.sol @@ -86,7 +86,7 @@ contract DualGovernance { "Proposals can only be scheduled in Normal or Veto Cooldown states." ); if (currentState == State.VetoCooldown) { - uint256 submissionTime = emergencyProtectedTimelock.proposals(proposalId).submissionTime; + (,,,uint256 submissionTime,) = emergencyProtectedTimelock.proposals(proposalId); require( submissionTime < lastVetoSignallingTime, "Proposal submitted after the last time Veto Signalling state was entered." From 9110ea6408993df739f70987cc6741b1aa60776b Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Thu, 13 Jun 2024 16:36:51 +0300 Subject: [PATCH 091/134] 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 092/134] 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 093/134] 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 094/134] 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 e864e0987ed4bc6b541258aebe73f5ad71bc7a9f Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 20 Jun 2024 01:10:23 +0400 Subject: [PATCH 095/134] DualGovernanceState undo split in two libs --- contracts/DualGovernance.sol | 12 +- contracts/libraries/DualGovernanceState.sol | 304 +++++++++--------- .../last-moment-malicious-proposal.t.sol | 12 +- test/scenario/veto-cooldown-mechanics.t.sol | 6 +- test/utils/scenario-test-blueprint.sol | 2 +- 5 files changed, 160 insertions(+), 176 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 789f40ee..10e24db6 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -7,17 +7,11 @@ 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 { - State, - DualGovernanceState, - DualGovernanceStateTransitions, - DualGovernanceStateViews -} from "./libraries/DualGovernanceState.sol"; +import {State, DualGovernanceState} from "./libraries/DualGovernanceState.sol"; contract DualGovernance is IGovernance, ConfigurationProvider { using Proposers for Proposers.State; - using DualGovernanceStateViews for DualGovernanceState; - using DualGovernanceStateTransitions for DualGovernanceState; + using DualGovernanceState for DualGovernanceState.Store; event TiebreakerSet(address tiebreakCommittee); event ProposalScheduled(uint256 proposalId); @@ -30,7 +24,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { address internal _tiebreaker; Proposers.State internal _proposers; - DualGovernanceState internal _dgState; + DualGovernanceState.Store internal _dgState; EmergencyProtection.State internal _emergencyProtection; mapping(uint256 proposalId => uint256 executableAfter) internal _scheduledProposals; diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol index 8d02fe3b..b55499f4 100644 --- a/contracts/libraries/DualGovernanceState.sol +++ b/contracts/libraries/DualGovernanceState.sol @@ -4,14 +4,11 @@ pragma solidity 0.8.23; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {IEscrow} from "../interfaces/IEscrow.sol"; +import {ISealable} from "../interfaces/ISealable.sol"; import {IDualGovernanceConfiguration as IConfiguration, DualGovernanceConfig} from "../interfaces/IConfiguration.sol"; import {TimeUtils} from "../utils/time.sol"; -interface IPausableUntil { - function isPaused() external view returns (bool); -} - enum State { Normal, VetoSignalling, @@ -20,49 +17,31 @@ enum State { RageQuit } -struct DualGovernanceState { - State state; - uint8 rageQuitRound; - uint40 enteredAt; - // the time the veto signalling state was entered - uint40 vetoSignallingActivationTime; - IEscrow signallingEscrow; - // the time the Deactivation sub-state was last exited without exiting the parent Veto Signalling state - uint40 vetoSignallingReactivationTime; - // the last time a proposal was submitted to the DG subsystem - uint40 lastAdoptableStateExitedAt; - IEscrow rageQuitEscrow; -} - -function dynamicTimelockDuration( - DualGovernanceConfig memory config, - uint256 rageQuitSupport -) pure returns (uint256 duration_) { - uint256 firstSealRageQuitSupport = config.firstSealRageQuitSupport; - uint256 secondSealRageQuitSupport = config.secondSealRageQuitSupport; - uint256 dynamicTimelockMinDuration = config.dynamicTimelockMinDuration; - uint256 dynamicTimelockMaxDuration = config.dynamicTimelockMaxDuration; - - if (rageQuitSupport < firstSealRageQuitSupport) { - return 0; - } - - if (rageQuitSupport >= secondSealRageQuitSupport) { - return dynamicTimelockMaxDuration; +library DualGovernanceState { + // TODO: Optimize storage layout efficiency + struct Store { + State state; + uint40 enteredAt; + // the time the veto signalling state was entered + uint40 vetoSignallingActivationTime; + IEscrow signallingEscrow; + // the time the Deactivation sub-state was last exited without exiting the parent Veto Signalling state + uint40 vetoSignallingReactivationTime; + // the last time a proposal was submitted to the DG subsystem + uint40 lastAdoptableStateExitedAt; + IEscrow rageQuitEscrow; + uint8 rageQuitRound; } - duration_ = dynamicTimelockMinDuration - + (rageQuitSupport - firstSealRageQuitSupport) * (dynamicTimelockMaxDuration - dynamicTimelockMinDuration) - / (secondSealRageQuitSupport - firstSealRageQuitSupport); -} - -library DualGovernanceStateTransitions { + error NotTie(); error AlreadyInitialized(); + error ProposalsCreationSuspended(); + error ProposalsAdoptionSuspended(); event NewSignallingEscrowDeployed(address indexed escrow); event DualGovernanceStateChanged(State oldState, State newState); - function initialize(DualGovernanceState storage self, address escrowMasterCopy) internal { + function initialize(Store storage self, address escrowMasterCopy) internal { if (address(self.signallingEscrow) != address(0)) { revert AlreadyInitialized(); } @@ -70,7 +49,7 @@ library DualGovernanceStateTransitions { } function activateNextState( - DualGovernanceState storage self, + Store storage self, DualGovernanceConfig memory config ) internal returns (State newState) { State oldState = self.state; @@ -96,20 +75,115 @@ library DualGovernanceStateTransitions { } // --- - // State Transitions + // View Methods // --- - function _fromNormalState( - DualGovernanceState storage self, + function checkProposalsCreationAllowed(Store storage self) internal view { + if (!isProposalsCreationAllowed(self)) { + revert ProposalsCreationSuspended(); + } + } + + function checkProposalsAdoptionAllowed(Store storage self) internal view { + if (!isProposalsAdoptionAllowed(self)) { + revert ProposalsAdoptionSuspended(); + } + } + + function checkCanScheduleProposal(Store storage self, uint256 proposalSubmittedAt) internal view { + if (!canScheduleProposal(self, proposalSubmittedAt)) { + revert ProposalsAdoptionSuspended(); + } + } + + function checkTiebreak(Store storage self, IConfiguration config) internal view { + if (!isTiebreak(self, config)) { + revert NotTie(); + } + } + + function currentState(Store storage self) internal view returns (State) { + return self.state; + } + + function canScheduleProposal(Store storage self, uint256 proposalSubmissionTime) internal view returns (bool) { + State state = self.state; + if (state == State.Normal) return true; + if (state == State.VetoCooldown) { + return proposalSubmissionTime <= self.vetoSignallingActivationTime; + } + return false; + } + + function isProposalsCreationAllowed(Store storage self) internal view returns (bool) { + State state = self.state; + return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; + } + + function isProposalsAdoptionAllowed(Store storage self) internal view returns (bool) { + State state = self.state; + return state == State.Normal || state == State.VetoCooldown; + } + + function isTiebreak(Store storage self, IConfiguration config) internal view returns (bool) { + if (isProposalsAdoptionAllowed(self)) return false; + + // for the governance is locked for long period of time + if (block.timestamp - self.lastAdoptableStateExitedAt >= config.TIE_BREAK_ACTIVATION_TIMEOUT()) return true; + + if (self.state != State.RageQuit) return false; + + address[] memory sealableWithdrawalBlockers = config.sealableWithdrawalBlockers(); + for (uint256 i = 0; i < sealableWithdrawalBlockers.length; ++i) { + if (ISealable(sealableWithdrawalBlockers[i]).isPaused()) return true; + } + return false; + } + + function getVetoSignallingState( + Store storage self, DualGovernanceConfig memory config - ) private view returns (State) { + ) internal view returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) { + isActive = self.state == State.VetoSignalling; + duration = isActive ? getVetoSignallingDuration(self, config) : 0; + enteredAt = isActive ? self.enteredAt : 0; + activatedAt = isActive ? self.vetoSignallingActivationTime : 0; + } + + function getVetoSignallingDuration( + Store storage self, + DualGovernanceConfig memory config + ) internal view returns (uint256) { + uint256 totalSupport = self.signallingEscrow.getRageQuitSupport(); + return _calcDynamicTimelockDuration(config, totalSupport); + } + + struct VetoSignallingDeactivationState { + uint256 duration; + uint256 enteredAt; + } + + function getVetoSignallingDeactivationState( + Store storage self, + DualGovernanceConfig memory config + ) internal view returns (bool isActive, uint256 duration, uint256 enteredAt) { + isActive = self.state == State.VetoSignallingDeactivation; + duration = config.vetoSignallingDeactivationMaxDuration; + enteredAt = isActive ? self.enteredAt : 0; + } + + // --- + // State Transitions + // --- + + function _fromNormalState(Store storage self, DualGovernanceConfig memory config) private view returns (State) { return _isFirstSealRageQuitSupportCrossed(config, self.signallingEscrow.getRageQuitSupport()) ? State.VetoSignalling : State.Normal; } function _fromVetoSignallingState( - DualGovernanceState storage self, + Store storage self, DualGovernanceConfig memory config ) private view returns (State) { uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); @@ -128,7 +202,7 @@ library DualGovernanceStateTransitions { } function _fromVetoSignallingDeactivationState( - DualGovernanceState storage self, + Store storage self, DualGovernanceConfig memory config ) private view returns (State) { uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); @@ -149,7 +223,7 @@ library DualGovernanceStateTransitions { } function _fromVetoCooldownState( - DualGovernanceState storage self, + Store storage self, DualGovernanceConfig memory config ) private view returns (State) { if (!_isVetoCooldownDurationPassed(self, config)) { @@ -160,10 +234,7 @@ library DualGovernanceStateTransitions { : State.Normal; } - function _fromRageQuitState( - DualGovernanceState storage self, - DualGovernanceConfig memory config - ) private view returns (State) { + function _fromRageQuitState(Store storage self, DualGovernanceConfig memory config) private view returns (State) { if (!self.rageQuitEscrow.isRageQuitFinalized()) { return State.RageQuit; } @@ -177,7 +248,7 @@ library DualGovernanceStateTransitions { // --- function _handleStateTransitionSideEffects( - DualGovernanceState storage self, + Store storage self, DualGovernanceConfig memory config, State oldState, State newState @@ -227,43 +298,43 @@ library DualGovernanceStateTransitions { } function _isDynamicTimelockMaxDurationPassed( - DualGovernanceState storage self, + Store storage self, DualGovernanceConfig memory config ) private view returns (bool) { return block.timestamp - self.vetoSignallingActivationTime > config.dynamicTimelockMaxDuration; } function _isDynamicTimelockDurationPassed( - DualGovernanceState storage self, + Store storage self, DualGovernanceConfig memory config, uint256 rageQuitSupport ) private view returns (bool) { uint256 vetoSignallingDurationPassed = block.timestamp - self.vetoSignallingActivationTime; - return vetoSignallingDurationPassed > dynamicTimelockDuration(config, rageQuitSupport); + return vetoSignallingDurationPassed > _calcDynamicTimelockDuration(config, rageQuitSupport); } function _isVetoSignallingReactivationDurationPassed( - DualGovernanceState storage self, + Store storage self, DualGovernanceConfig memory config ) private view returns (bool) { return block.timestamp - self.vetoSignallingReactivationTime > config.vetoSignallingMinActiveDuration; } function _isVetoSignallingDeactivationMaxDurationPassed( - DualGovernanceState storage self, + Store storage self, DualGovernanceConfig memory config ) private view returns (bool) { return block.timestamp - self.enteredAt > config.vetoSignallingDeactivationMaxDuration; } function _isVetoCooldownDurationPassed( - DualGovernanceState storage self, + Store storage self, DualGovernanceConfig memory config ) private view returns (bool) { return block.timestamp - self.enteredAt > config.vetoCooldownDuration; } - function _deployNewSignallingEscrow(DualGovernanceState storage self, address escrowMasterCopy) private { + function _deployNewSignallingEscrow(Store storage self, address escrowMasterCopy) private { IEscrow clone = IEscrow(Clones.clone(escrowMasterCopy)); clone.initialize(address(this)); self.signallingEscrow = clone; @@ -284,107 +355,26 @@ library DualGovernanceStateTransitions { + config.rageQuitEthClaimTimelockGrowthCoeffs[2] ) / 10 ** 18; // TODO: rewrite in a prettier way } -} -library DualGovernanceStateViews { - error NotTie(); - error ProposalsCreationSuspended(); - error ProposalsAdoptionSuspended(); - - function checkProposalsCreationAllowed(DualGovernanceState storage self) internal view { - if (!isProposalsCreationAllowed(self)) { - revert ProposalsCreationSuspended(); - } - } - - function checkProposalsAdoptionAllowed(DualGovernanceState storage self) internal view { - if (!isProposalsAdoptionAllowed(self)) { - revert ProposalsAdoptionSuspended(); - } - } - - function checkCanScheduleProposal(DualGovernanceState storage self, uint256 proposalSubmittedAt) internal view { - if (!canScheduleProposal(self, proposalSubmittedAt)) { - revert ProposalsAdoptionSuspended(); - } - } - - function checkTiebreak(DualGovernanceState storage self, IConfiguration config) internal view { - if (!isTiebreak(self, config)) { - revert NotTie(); - } - } - - function currentState(DualGovernanceState storage self) internal view returns (State) { - return self.state; - } - - function canScheduleProposal( - DualGovernanceState storage self, - uint256 proposalSubmissionTime - ) internal view returns (bool) { - State state = self.state; - if (state == State.Normal) return true; - if (state == State.VetoCooldown) { - return proposalSubmissionTime <= self.vetoSignallingActivationTime; + function _calcDynamicTimelockDuration( + DualGovernanceConfig memory config, + uint256 rageQuitSupport + ) internal pure returns (uint256 duration_) { + uint256 firstSealRageQuitSupport = config.firstSealRageQuitSupport; + uint256 secondSealRageQuitSupport = config.secondSealRageQuitSupport; + uint256 dynamicTimelockMinDuration = config.dynamicTimelockMinDuration; + uint256 dynamicTimelockMaxDuration = config.dynamicTimelockMaxDuration; + + if (rageQuitSupport < firstSealRageQuitSupport) { + return 0; } - return false; - } - function isProposalsCreationAllowed(DualGovernanceState storage self) internal view returns (bool) { - State state = self.state; - return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; - } - - function isProposalsAdoptionAllowed(DualGovernanceState storage self) internal view returns (bool) { - State state = self.state; - return state == State.Normal || state == State.VetoCooldown; - } - - function isTiebreak(DualGovernanceState storage self, IConfiguration config) internal view returns (bool) { - if (isProposalsAdoptionAllowed(self)) return false; - - // for the governance is locked for long period of time - if (block.timestamp - self.lastAdoptableStateExitedAt >= config.TIE_BREAK_ACTIVATION_TIMEOUT()) return true; - - if (self.state != State.RageQuit) return false; - - address[] memory sealableWithdrawalBlockers = config.sealableWithdrawalBlockers(); - for (uint256 i = 0; i < sealableWithdrawalBlockers.length; ++i) { - if (IPausableUntil(sealableWithdrawalBlockers[i]).isPaused()) return true; + if (rageQuitSupport >= secondSealRageQuitSupport) { + return dynamicTimelockMaxDuration; } - return false; - } - function getVetoSignallingState( - DualGovernanceState storage self, - DualGovernanceConfig memory config - ) internal view returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) { - isActive = self.state == State.VetoSignalling; - duration = isActive ? getVetoSignallingDuration(self, config) : 0; - enteredAt = isActive ? self.enteredAt : 0; - activatedAt = isActive ? self.vetoSignallingActivationTime : 0; - } - - function getVetoSignallingDuration( - DualGovernanceState storage self, - DualGovernanceConfig memory config - ) internal view returns (uint256) { - uint256 totalSupport = self.signallingEscrow.getRageQuitSupport(); - return dynamicTimelockDuration(config, totalSupport); - } - - struct VetoSignallingDeactivationState { - uint256 duration; - uint256 enteredAt; - } - - function getVetoSignallingDeactivationState( - DualGovernanceState storage self, - DualGovernanceConfig memory config - ) internal view returns (bool isActive, uint256 duration, uint256 enteredAt) { - isActive = self.state == State.VetoSignallingDeactivation; - duration = config.vetoSignallingDeactivationMaxDuration; - enteredAt = isActive ? self.enteredAt : 0; + duration_ = dynamicTimelockMinDuration + + (rageQuitSupport - firstSealRageQuitSupport) * (dynamicTimelockMaxDuration - dynamicTimelockMinDuration) + / (secondSealRageQuitSupport - firstSealRageQuitSupport); } } diff --git a/test/scenario/last-moment-malicious-proposal.t.sol b/test/scenario/last-moment-malicious-proposal.t.sol index 36e99eeb..d40e4df3 100644 --- a/test/scenario/last-moment-malicious-proposal.t.sol +++ b/test/scenario/last-moment-malicious-proposal.t.sol @@ -6,7 +6,7 @@ import { ScenarioTestBlueprint, ExecutorCall, ExecutorCallHelpers, - DualGovernanceStateViews + DualGovernanceState } from "../utils/scenario-test-blueprint.sol"; interface IDangerousContract { @@ -96,7 +96,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _executeProposal(proposalId); _assertProposalExecuted(proposalId); - vm.expectRevert(DualGovernanceStateViews.ProposalsAdoptionSuspended.selector); + vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); this.scheduleProposalExternal(maliciousProposalId); _assertProposalSubmitted(maliciousProposalId); @@ -119,7 +119,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _activateNextState(); _assertRageQuitState(); - vm.expectRevert(DualGovernanceStateViews.ProposalsAdoptionSuspended.selector); + vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); this.scheduleProposalExternal(maliciousProposalId); } } @@ -215,7 +215,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _activateNextState(); _assertVetoCooldownState(); - vm.expectRevert(DualGovernanceStateViews.ProposalsAdoptionSuspended.selector); + vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); this.scheduleProposalExternal(proposalId); } @@ -269,7 +269,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _activateNextState(); _assertVetoCooldownState(); - vm.expectRevert(DualGovernanceStateViews.ProposalsAdoptionSuspended.selector); + vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); this.scheduleProposalExternal(proposalId); } @@ -280,7 +280,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _assertVetoSignalingState(); _logVetoSignallingState(); - vm.expectRevert(DualGovernanceStateViews.ProposalsAdoptionSuspended.selector); + vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); this.scheduleProposalExternal(proposalId); } diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index 6afc8951..38e75c0e 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -6,8 +6,8 @@ import { percents, ExecutorCall, ExecutorCallHelpers, - ScenarioTestBlueprint, - DualGovernanceStateViews + DualGovernanceState, + ScenarioTestBlueprint } from "../utils/scenario-test-blueprint.sol"; interface IDangerousContract { @@ -105,7 +105,7 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { _activateNextState(); _assertVetoCooldownState(); - vm.expectRevert(DualGovernanceStateViews.ProposalsAdoptionSuspended.selector); + vm.expectRevert(DualGovernanceState.ProposalsAdoptionSuspended.selector); this.scheduleProposalExternal(anotherProposalId); } } diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 99ed749d..1565afec 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -21,7 +21,7 @@ import { } from "contracts/EmergencyProtectedTimelock.sol"; import {SingleGovernance, IGovernance} from "contracts/SingleGovernance.sol"; -import {DualGovernance, DualGovernanceState, DualGovernanceStateViews, State} from "contracts/DualGovernance.sol"; +import {DualGovernance, DualGovernanceState, State} from "contracts/DualGovernance.sol"; import {Proposal, Status as ProposalStatus} from "contracts/libraries/Proposals.sol"; From 3faa735b2a1f419b1abedf2ed665b3b2002b153b Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 20 Jun 2024 01:40:26 +0400 Subject: [PATCH 096/134] Remove unused DG.isScheduled() method --- contracts/DualGovernance.sol | 5 ----- 1 file changed, 5 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 10e24db6..3a6c7a5a 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -26,7 +26,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( address config, @@ -68,10 +67,6 @@ contract DualGovernance is IGovernance, ConfigurationProvider { return address(_dgState.rageQuitEscrow); } - 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); } From d2c02dddbb76460de9572d4b883c101b7e88fc77 Mon Sep 17 00:00:00 2001 From: lucasmt Date: Wed, 19 Jun 2024 16:47:20 -0500 Subject: [PATCH 097/134] Add design review report --- ...ming_Analysis_of_Dual_Governance_States.md | 76 +++++++++++++ .../2_Overall_Bounds_on_Proposal_Execution.md | 100 ++++++++++++++++++ .../3_Key_Properties_of_Dual_Governance.md | 50 +++++++++ docs/RV Design Review Report/4_Proofs.md | 64 +++++++++++ 4 files changed, 290 insertions(+) create mode 100644 docs/RV Design Review Report/1_Timing_Analysis_of_Dual_Governance_States.md create mode 100644 docs/RV Design Review Report/2_Overall_Bounds_on_Proposal_Execution.md create mode 100644 docs/RV Design Review Report/3_Key_Properties_of_Dual_Governance.md create mode 100644 docs/RV Design Review Report/4_Proofs.md diff --git a/docs/RV Design Review Report/1_Timing_Analysis_of_Dual_Governance_States.md b/docs/RV Design Review Report/1_Timing_Analysis_of_Dual_Governance_States.md new file mode 100644 index 00000000..3b303024 --- /dev/null +++ b/docs/RV Design Review Report/1_Timing_Analysis_of_Dual_Governance_States.md @@ -0,0 +1,76 @@ +# Timing Analysis of Dual Governance States + +There are two opposing ways in which a malicious adversary might attack the protocol by manipulating proposal execution delays: + +1. Delay the execution of a legitimate proposal for a significant amount of time or, in the worst case, indefinitely. +2. Trigger an early execution of a malicious proposal, without giving the stakers the chance to delay it or exit the protocol. + +As these two attack vectors are diametrically opposed, any change in the protocol made to prevent one has the potential of enabling the other. Therefore, it's important to know both the minimum and maximum time that the execution of a proposal can be delayed for in the current version of the protocol. The first step is to analyze how much time can be spent inside each Dual Governance state, since the current state is one of the main factors that determine whether a proposal can be executed. + +Below, we give upper and lower bounds on the time spent in each state, given as the difference between the time the state was entered ($t_{enter}$) and exited ($t_{exit}$). + +**Note:** For simplicity, this analysis assumes that transitions happen immediately as soon as they are enabled. Since in practice they need to be triggered by a call to `activateNextState`, it's possible for there to be a delay between when the transition becomes enabled and when it actually happens. However, we can assume this delay will be small since any interested agent can call the function as soon as it becomes possible. + +## Normal + +The Normal state can only transition to the Veto Signalling state, and this transition happens immediately as soon as the rage quit support surpasses `FirstSealRageQuitSupport` ($R_1$). On the other hand, if this never happens the protocol can remain in the Normal state indefinitely. Therefore, the time between activating the Normal state and transitioning to the Veto Signalling state can have any duration: + +$$ +0 \leq t_{exit} - t_{enter} < \infty +$$ + +## Veto Signalling + +Once the Veto Signalling state is entered, it can be exited in two ways: either to the Rage Quit state or the Veto Cooldown state. It also can enter and exit the Deactivation sub-state, making this the hardest state to analyze. + +### To Veto Cooldown + +While in the Veto Signalling state, the protocol can enter and exit the Deactivation sub-state depending on the current value of the dynamic timelock duration $T_{lock}(R)$, a monotonic function on the current rage quit support $R$. + +When first entering the Veto Signalling state, and again whenever the Deactivation sub-state is exited, there is a waiting period of `VetoSignallingMinActiveDuration` ($T^{Sa}_{min}$) when the Deactivation sub-state cannot be entered. Outside of this window (as long as a rage quit is not triggered), the protocol will be in the Deactivation sub-state if the time $\Delta t$ since entering the Veto Signalling state is greater than the current value of $T_{lock}(R)$, and will be in the Veto Signalling parent state otherwise. If the Deactivation sub-state is not exited within `VetoSignallingDeactivationMaxDuration` ($T^{SD}_{max}$) of being entered, it transitions to the Veto Cooldown state. + +With this, we can calculate bounds on the time spent in the Veto Signalling state (including the Deactivation sub-state) before transitioning into Veto Cooldown: + +* For the lower bound, the earliest we can transition to the Deactivation sub-state is $T^{Sa}_{min} + 1$ after Veto Signalling is entered, and then the transition to Veto Cooldown happens $T^{SD}_{max} + 1$ after that, giving us $T^{Sa}_{min} + T^{SD}_{max} + 2$. +* For the upper bound, we can use the insight that, if $R_{max}$ is the highest rage quit support during the time we are in the Veto Signalling state, then it's impossible to exit the Deactivation sub-state (without triggering a rage quit) after $T_{lock}(R_{max})$ has passed since entering Veto Signalling (see "Veto Signalling Maximum Timelock" in the "Proofs" section for details). Therefore, for a given $R_{max}$, the longest delay happens in the following scenario: + 1. $R_{max}$ is locked in escrow, making the dynamic timelock duration $T_{lock}(R_{max})$. + 2. Shortly before $\Delta t = T_{lock}(R_{max})$ has passed, the rage quit support decreases, and the Deactivation sub-state is entered. + 3. At exactly $\Delta t = T_{lock}(R_{max})$, the rage quit support returns to $R_{max}$, and the Deactivation sub-state is exited. + 4. At $\Delta t = T_{lock}(R_{max}) + T^{Sa}_{min} + 1$, the waiting period ends and the Deactivation sub-state is entered again. + 5. At $\Delta t = T_{lock}(R_{max}) + T^{Sa}_{min} + 1 + T^{SD}_{max} + 1$, the state transitions to Veto Cooldown. + +In summary, the above analysis gives us the following bounds: + +$$ +T^{Sa}_{min} + T^{SD}_{max} + 2 \leq t_{exit} - t_{enter} \leq T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2 +$$ + +Note that the maximum value of $T_{lock}(R)$ is `DynamicTimelockMaxDuration` ($L_{max}$), so the upper bound can be at most $L_{max} + T^{Sa}_{min} + T^{SD}_{max} + 2$. However, writing it in terms of $R_{max}$ highlights an important security property: the delay in deactivating the Veto Signalling state depends only on the *highest* value of the rage quit support, and cannot be increased further by locking and unlocking funds in the signalling escrow at different times. In other words, the amount of delay an attacker is able to achieve is limited by the amount of stETH they control. + +### To Rage Quit + +The Veto Signalling state can transition to the Rage Quit state at any point after $L_{max}$ has passed, as long as the rage quit support surpasses `SecondSealRageQuitSupport` ($R_2$). This gives us a lower bound of $L_{max}$. For the upper bound, we can adapt the analysis of the Veto Cooldown transition above. Note that if $R_{max} = R_2$, it's possible to delay the transition to the Veto Cooldown state for the maximum time of $L_{max} + T^{Sa}_{min} + T^{SD}_{max} + 2$ before triggering a rage quit at the last possible moment by increasing the rage quit support above $R_2$. Therefore, + +$$ +L_{max} < t_{exit} - t_{enter} \leq L_{max} + T^{Sa}_{min} + T^{SD}_{max} + 2 +$$ + +## Veto Cooldown + +Depending on whether the rage quit support is above or below $R_1$, the Veto Cooldown state can transition to the Veto Signalling state or the Normal state, but regardless this transition always happens after `VetoCooldownDuration` ($T^C$) has passed. Therefore, the time between entering and exiting the state is always the same: + +$$ +t_{exit} - t_{enter} = T^C + 1 +$$ + +## Rage Quit + +The Rage Quit state differs from the others due to having a dependence on a mechanism external to the Dual Governance protocol, namely the withdrawal procedure for unstaking ETH. After transitioning to the Rage Quit state, anyone can call the `requestNextWithdrawalsBatch` function, sending a portion of the funds in the rage quit escrow to the Lido Withdrawal Queue. Once a withdrawal request is finalized, anyone can transfer the ETH from the queue to the rage quit escrow by calling the `claimNextWithdrawalsBatch` function. After all the withdrawals are claimed, a period lasting `RageQuitExtensionDelay` ($T^R$) starts, where any stakers who had submitted Withdrawal NFTs as rage quit support and haven't done so already can claim those as well before the Rage Quit state is exited. + +Therefore, if $T^W$ is the time from when the Rage Quit state is entered until the last withdrawal request is claimed, then the total duration of the Rage Quit state is + +$$ +t_{exit} - t_{enter} = T^W + T^R + 1 +$$ + +Unlike other values in this analysis, the time duration $T^W$ cannot be deterministically predicted or bounded based on the internal state of the Dual Governance protocol, as withdrawal time is dependent on a number of factors related to Ethereum's validator network and other parts of the Lido system. However, triggering a rage quit in bad faith would come at a higher cost than normal to an attacker, as it would not only require them to remove their stake from the protocol, but also keep their ETH locked in the rage quit escrow until a dynamic `RageQuitEthClaimTimelock` has passed after exiting the Rage Quit state. diff --git a/docs/RV Design Review Report/2_Overall_Bounds_on_Proposal_Execution.md b/docs/RV Design Review Report/2_Overall_Bounds_on_Proposal_Execution.md new file mode 100644 index 00000000..077053e1 --- /dev/null +++ b/docs/RV Design Review Report/2_Overall_Bounds_on_Proposal_Execution.md @@ -0,0 +1,100 @@ +# Overall Bounds on Proposal Execution + +Using the bounds from the previous section on the duration of each Dual Governance state, we can now set bounds on the total time between when a proposal is submitted ($t_{sub}$) and when it becomes executable ($t_{exe}$). For this analysis, we need to take into account the following rules of the protocol: + +1. Proposal submission is only allowed in the Normal, Veto Signalling (only the parent state, not the Deactivation sub-state) and Rage Quit states. +2. Proposal execution is only allowed in the Normal and Veto Cooldown states. +3. Regardless of state, a proposal can only be executed after `ProposalExecutionMinTimelock` ($T_{min}$) has passed since its submission. +4. In the Veto Cooldown state, a proposal can only be executed if it was submitted before the last time the Veto Signalling state was entered. + +Rule 4 is meant to guarantee that if a proposal is submitted during the Veto Signalling or Rage Quit states, the stakers will have the time to react and the benefit of a full Veto Signalling dynamic timelock before the proposal becomes executable. + +## Accounting for Rage Quits + +Note that it is technically possible to delay execution of a proposal indefinitely if the protocol transitions continuously between the Veto Signalling and Rage Quit states. However, as mentioned above, triggering a Rage Quit is unlikely to be cost-efficient to an attacker. Furthermore, doing this repeatedly is even more unlikely to be viable, as after exiting the Rage Quit state the funds remain locked in the rage quit escrow for `RageQuitEthClaimTimelock`, which starts as a lengthy duration (60 days with the current proposed values) and increases quadratically with each subsequent rage quit until the protocol returns to the Normal state. + +With this in mind, in the rest of this section we consider the bounds on proposal execution assuming no rage quit is triggered. If the Rage Quit state *is* entered, every entry will delay proposal execution for an additional $T^W + T^R + 1$ (keeping in mind that $T^W$ is a non-deterministic duration that depends on external factors and might be different each time the rage quit is triggered), plus a further delay between $T^{Sa}_{min} + T^{SD}_{max} + 2$ and $T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2$ if the Rage Quit state then transitions back to Veto Signalling. However, note that, after this re-entry to Veto Signalling, if the protocol then transitions to Veto Cooldown, any proposals submitted during the previous Rage Quit state or the Veto Signalling state before that will become executable immediately, without needing another round of Veto Signalling. + +More precisely, the following bounds apply to any proposal submitted in the Rage Quit state or the preceding Veto Signalling state, for the time between when the Rage Quit state is exited ($t^R_{exit}$) and when the proposal becomes executable (assuming no further rage quit happens and the minimum timelock $T_{min}$ has already passed): + +* If the Rage Quit state exits to Veto Cooldown and then Normal (becomes executable as soon as the Normal state is entered): + +$$ +t_{exe} - t^R_{exit} = T^C + 1 +$$ + +* If the Rage Quit state exits to Veto Cooldown, then Veto Signalling and Veto Cooldown again (becomes executable as soon as the Veto Cooldown state is entered for the second time): + +$$ +T^C + T^{Sa}_{min} + T^{SD}_{max} + 3 \leq t_{exe} - t^R_{exit} \leq T^C + T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 3 +$$ + +* If the Rage Quit state exits to Veto Signalling and then Veto Cooldown (becomes executable as soon as the Veto Cooldown state is entered): + +$$ +T^{Sa}_{min} + T^{SD}_{max} + 2 \leq t_{exe} - t^R_{exit} \leq T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2 +$$ + +## Proposals Submitted in the Normal State + +If a proposal is submitted in the Normal state and no transition happens before then, it become executable as soon as $T_{min} + 1$ passes: + +$$ +t_{exe} - t_{sub} = T_{min} + 1 +$$ + +On the other hand, if the protocol transitions to Veto Signalling before this time due to rage quit support surpassing $R_1$, it will become subject to the Veto Signalling dynamic timelock. The shortest possible time for execution in this case happens if the transition happens immediately after the proposal is submitted (at the same timestamp), and soon after that the rage quit support drops below $R_1$ again, making the dynamic timelock 0. In this scenario, the Deactivation sub-state will be entered at $T^{Sa}_{min} + 1$, and then exited to the Veto Cooldown state at $T^{SD}_{max} + 1$, giving the previously-stated $T^{Sa}_{min} + T^{SD}_{max} + 2$ lower bound on the duration of the Veto Signalling state. Note, however, that it's possible, depending on the parameter values, that at this point the minimum timelock $T_{min}$ hasn't passed, so the true lower bound is the highest between the minimum timelock and the minimum duration of the Veto Signalling state: + +$$ +\max \{ T_{min} + 1, T^{Sa}_{min} + T^{SD}_{max} + 2 \} \leq t_{exe} - t_{sub} +$$ + +Note that for the current proposed values, the minimum Veto Signalling duration is slightly higher: + +* $T_{min}$ is 3 days. +* $T^{Sa}_{min}$ is 5 hours. +* $T^{SD}_{max}$ is 3 days. + +For the upper bound, the longest possible delay happens when the Veto Signalling state is entered at $T_{min}$ (the last possible moment before the proposal becomes executable in the Normal state) and lasts as long as possible ($T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2$, according to our previous analysis): + +$$ +t_{exe} - t_{sub} \leq T_{min} + T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2 +$$ + +**Note:** With the current proposed values for the parameters, we have the guarantee that $T_{min}$ will have expired by the time the Veto Cooldown state is entered. However, if $T_{min}$ were greater than $T^{Sa}_{min} + T^{SD}_{max}$, it would be possible to enter and exit the Veto Signalling state before the minimum timelock expired. If it were greater than $T^{Sa}_{min} + T^{SD}_{max} + T^C$, it would even be possible to exit Veto Cooldown and re-enter Veto Signalling before it expired. Nevertheless, the above upper bound would still work in those cases. In the first case, the proposal would become executable in the Veto Cooldown state at $T_{min} + 1$, which is less than the upper bound. In the second case, the Veto Signalling state would have to be re-entered at $T_{min}$ at the latest, giving rise to the same bound as above. + +## Proposals Submitted in the Veto Signalling State + +Proposals submitted while in the Veto Signalling state are not immediately executable after transitioning to Veto Cooldown. Instead, they will either become executable when Veto Cooldown transitions to the Normal state, or, if it transitions back to Veto Signalling instead, once it exits that state again and returns to Veto Cooldown. + +In the first case, the shortest time between proposal submission and execution happens in the following scenario: + +1. The proposal is submitted immediately before entering the Deactivation sub-state (recall that submissions are only allowed in the parent state). +2. After $T^{SD}_{max} + 1$, the Deactivation sub-state transitions to Veto Cooldown. +3. After $T^C + 1$, Veto Cooldown transitions to the Normal state and the proposal becomes executable. + +Meanwhile, the longest time to execution happens in the following scenario: + +1. The proposal is submitted as soon as the Veto Signalling state is first entered. +2. The longest possible duration of $T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2$ passes before transitioning to Veto Cooldown. +3. After $T^C + 1$, Veto Cooldown transitions to the Normal state and the proposal becomes executable. + +However, if either of these scenarios takes less time than $T_{min}$ (again, not the case for the current proposed values), then we'll have to wait for the minimum timelock to pass before executing the proposal, giving us the following final bounds: + +$$ +\max \{ T_{min} + 1, T^{SD}_{max} + T^C + 2 \} \leq t_{exe} - t_{sub} +$$ + +$$ +t_{exe} - t_{sub} \leq \max \{ T_{min} + 1, T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + T^C + 3 \} +$$ + +In the second case, where Veto Cooldown transitions back to Veto Signalling instead of the Normal state, we need to add the duration of the Veto Signalling state again to the bounds. For the lower bound, we add the minimum duration of $T^{Sa}_{min} + T^{SD}_{max} + 2$, giving us $T^{Sa}_{min} + 2T^{SD}_{max} + T^C + 4$. For the upper bound, we add the maximum duration of $T_{lock}(R'_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2$, where $R'_{max}$, the maximum rage quit support during the second time through the Veto Signalling state, might be different from $R_{max}$. This leaves us with the following bounds: + +$$ +\max \{ T_{min} + 1, T^{Sa}_{min} + 2T^{SD}_{max} + T^C + 4 \} \leq t_{exe} - t_{sub} +$$ + +$$ +t_{exe} - t_{sub} \leq \max \{ T_{min} + 1, T_{lock}(R_{max}) + T_{lock}(R'_{max}) + 2T^{Sa}_{min} + 2T^{SD}_{max} + T^C + 5 \} +$$ diff --git a/docs/RV Design Review Report/3_Key_Properties_of_Dual_Governance.md b/docs/RV Design Review Report/3_Key_Properties_of_Dual_Governance.md new file mode 100644 index 00000000..7eaa553b --- /dev/null +++ b/docs/RV Design Review Report/3_Key_Properties_of_Dual_Governance.md @@ -0,0 +1,50 @@ +# Key Properties of Dual Governance + +## Implementation Properties + +These are properties that must be guaranteed by the implementation of the contracts that comprise the Dual Governance mechanism. + +### DualGovernance + +* Proposals cannot be executed in the Veto Signalling (both parent state and Deactivation sub-state) and Rage Quit states. +* Proposals cannot be submitted in the Veto Signalling Deactivation sub-state or in the Veto Cooldown state. +* If a proposal was submitted after the last time the Veto Signalling state was activated, then it cannot be executed in the Veto Cooldown state. +* One rage quit cannot start until the previous rage quit has finalized. In other words, there can only be at most one active rage quit escrow at a time. + +### EmergencyProtectedTimelock + +* A proposal cannot be scheduled for execution before at least `ProposalExecutionMinTimelock` has passed since its submission. +* A proposal cannot be executed until the emergency protection timelock has passed since it was scheduled. +* The emergency protection timelock is greater than 0 if and only if the protocol is in protected deployment mode. + +### Escrow + +* Ignoring imprecisions due to fixed-point arithmetic, the rage quit support of an escrow is equal to $$\frac{(S + W + U + F)}{(T + F)}$$ where + * $S$ is the ETH amount locked in the escrow in the form of stETH. + * $W$ is the ETH amount locked in the escrow in the form of wstETH. + * $U$ is the ETH amount locked in the escrow in the form of unfinalized Withdrawal NFTs. + * $F$ is the ETH amount locked in the escrow in the form of finalized Withdrawal NFTs. + * $T$ is the total supply of stETH. +* The amount of each token accounted for in the above calculation must be less than or equal to the balance of the escrow in the token. +* It's not possible to lock funds in or unlock funds from an escrow that is already in the rage quit state. +* An agent cannot unlock their funds from the signalling escrow until `SignallingEscrowMinLockTime` has passed since this user last locked funds. + +## Protocol Properties + +These are emergent properties that are derived from the design of the protocol, and give guarantees of protection against certain attacks. + +* Regardless of the state in which a proposal is submitted, if the stakers are able to amass and maintain a certain amount of rage quit support before the `ProposalExecutionMinTimelock` expires, they can extend the timelock for a proportional time, according to the dynamic timelock calculation. + +The proof for this property is presented in the "Proofs" section, under "Staker Reaction Time". However, note that it depends on the assumption that the minimum duration of the Veto Signalling state is greater than or equal to `ProposalExecutionMinTimelock` (which, as pointed out in the previous section, is true for the current proposed parameter values). + +* It's not possible to prevent a proposal from being executed indefinitely without triggering a rage quit. + +This property is guaranteed by the upper bounds on proposal execution presented in the previous section, "Overall Bounds on Proposal Execution". + +* It's not possible to block proposal submission indefinitely. + +This property is guaranteed by the fact that the only states that forbid proposal submission (Veto Cooldown and Veto Signalling Deactivation) have a fixed maximum duration, and that they cannot transition back-and-forth without a period in between when proposal submission is allowed: if the Veto Cooldown state transitions to the Veto Signalling state, it must remain in the parent state for at least `VetoSignallingMinActiveDuration` before it can transition to the Deactivation sub-state. + +* Until the Veto Signalling Deactivation sub-state transitions to Veto Cooldown, there is always a possibility (given enough rage quit support) of cancelling Deactivation and returning to the parent state (possibly triggering a rage quit immediately afterwards). + +This property is guaranteed by the transition function of the Deactivation sub-state, which always exits to the parent state when the current rage quit support is greater than `SecondSealRageQuitSupport`, regardless of how much time has passed. However, if `DynamicTimelockMaxDuration` has passed since the Veto Signalling state was entered, it will immediately trigger a transition from Veto Signalling to the Rage Quit state. diff --git a/docs/RV Design Review Report/4_Proofs.md b/docs/RV Design Review Report/4_Proofs.md new file mode 100644 index 00000000..5e3b0c3f --- /dev/null +++ b/docs/RV Design Review Report/4_Proofs.md @@ -0,0 +1,64 @@ +# Proofs + +## Veto Signalling Maximum Timelock + +### Property + +Suppose that at time $t^S_{act}$ the Dual Governance protocol enters the Veto Signalling state. Then, if a rage quit isn't triggered, and assuming that state transitions are activated as soon as they becomes enabled, the protocol will transition to the Veto Cooldown state at a time $t^C_{act} > t^S_{act}$ such that +$$ +t^C_{act} \leq t^S_{act} + T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2 +$$ +where $R_{max}$ is the maximum rage quit support between $t^S_{act}$ and $t^C_{act}$. + +### Proof + +**Observation 1:** First note that if we are in the Deactivation sub-state at any time after $t^S_{act} + T_{lock}(R_{max})$, we will not exit the sub-state until we transition to Veto Cooldown. To exit back to the parent state, one of the following would have to be true for some $t > t^S_{act} + T_{lock}(R_{max})$: +1. $t \leq t^S_{act} + T_{lock}(R(t))$. Since $R_{max} \geq R(t)$ for all $t$ between $t^S_{act}$ and $t^{C}_{act}$, and $T_{lock}(R)$ is monotonic, this is not possible once $t > t^S_{act} + T_{lock}(R_{max})$. +2. $R(t) > R_2$. This would imply $R_{max} \geq R(t) > R_2$, and therefore $T_{lock}(R_{max}) = L_{max}$. Since $t - t^S_{act} > T_{lock}(R_{max}) = L_{max}$ and $R(t) > R_2$, after exiting the Deactivation sub-state a rage quit would be immediately triggered. Since we are only considering scenarios where no rage quit happens, this case is also impossible. + +Now, let $t^{SD}_{act}$ be the last time the Deactivation sub-state is entered before $t^C_{act}$. We will first prove that $t^{SD}_{act} \leq t^S_{act} + T_{lock}(R_{max}) + T^{Sa}_{min} + 1$: +* **Case 1:** If $t^{SD}_{act} \leq t^S_{act} + T_{lock}(R_{max}) + 1$, we are done. +* **Case 2:** Otherwise, $t^S_{act} + T_{lock}(R_{max}) + 1 < t^{SD}_{act}$. + * Then, from Observation 1 above, at $t_1 = t^S_{act} + T_{lock}(R_{max}) + 1$ we cannot be in the Deactivation sub-state (since we wouldn't be able to exit the sub-state to enter again at $t^{SD}_{act}$). + * Since this is the case even though $t_1 - t^S_{act} > T_{lock}(R(t_1))$, it must be because $t_1 - \max \{ t^S_{act}, t^S_{react} \} \leq T^{Sa}_{min}$, where $t^S_{react} < t_1$ was the last time the Deactivation sub-state was exited, or 0 if it has never been entered (note that $t_1$ must be strictly greater than $t^S_{react}$, since it would impossible to transition back to the parent state at $t^S_{act} + T_{lock}(R_{max}) + 1$). + * In this case, as $t - t^S_{act} > T_{lock}(R(t))$ will remain true for any future $t > t_1$, the transition at $t^{SD}_{act}$ must happen as soon as $t - \max \{ t^S_{act}, t^S_{react} \} > T^{Sa}_{min}$ becomes true. + * Since $t_1$ is strictly greater than $\max \{ t^S_{act}, t^S_{react} \}$, the latest this can happen is at $t_1 + T^{Sa}_{min} = t^S_{act} + T_{lock}(R_{max}) + T^{Sa}_{min} + 1$. + +Finally, since the Deactivation sub-state does not return to the parent state after $t^{SD}_{act}$, and no rage quit is triggered, it will transition to Veto Cooldown as soon as $t - t^{SD}_{act} > T^{SD}_{max}$. Therefore, $t^C_{act} = t^{SD}_{act} + T^{SD}_{max} + 1 \leq t^S_{act} + T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2$. + +### Caveats + +* This proof assumes that the transitions at $t^{SD}_{act}$ and $t^C_{act}$ happen immediately as soon as they are enabled. Any delay $d$ in performing either of these transitions gets added to the upper bound. For instance, if the last transition to the Deactivation sub-state is only performed at $t^{SD}_{act} + d$, then the earliest that Veto Cooldown can be entered is $t^{SD}_{act} + d + T^{SD}_{max} + 1$. On the other hand, any delay in performing previous transitions between the Deactivation sub-state and the parent state (before the last one at $t^{SD}_{act}$) does not increase the upper bound, since it does not change the time $t_0 + T_{lock}(R_{max}) + 1$. + +## Staker Reaction Time + +The following property can be interpreted to say that, regardless of the state in which a proposal is submitted, if the stakers are able to amass and maintain rage quit support $R_{min}$ before the `ProposalExecutionMinTimelock` expires, they can extend the timelock to at least $T_{lock}(R_{min})$. + +### Property + +Suppose that a proposal is submitted at time $t_{prop}$, and let $t_1$ and $t_2$ be such that + +1. $t_{prop} \leq t_1 \leq t_{prop} + T_{min} \leq t_2$, where $T_{min}$ is `ProposalExecutionMinTimelock`. +2. Between $t_1$ and $t_2$, the rage quit support is never lower than some value $R_{min}$. +3. $t_2 \leq t_{prop} + T_{lock}(R_{min})$. + +Then, assuming that $T_{min}$ is less than the minimum duration of the Veto Signalling state, the proposal cannot be executed at any time less than or equal to $t_2$. + +### Proof + +First, note that if $R_{min} \leq R_1$, then $T_{lock}(R_{min}) = 0$ and $t_2 = t_{prop}$. Since this cannot be the case when $t_{prop} + T_{min} \leq t_2$, we only need to consider cases where $R_{min} > R_1$. + +At $t_{prop}$, the protocol must be in one of the three states that allow proposal submission. We'll consider each case individually. + +**Normal state:** Since $T_{min}$ is less than the minimum duration of the Veto Signalling state, immediately before $t_1$ the protocol must be in either the Normal state or the Veto Signalling state. Regardless, since $R(t_1) \geq R_{min} > R_1$, at $t_1$ the protocol will be in the Veto Signalling state. Then, given that, for every $t_1 \leq t \leq t_2$, +* $t \leq t_{prop} + T_{lock}(R_{min})$ +* $t_{prop} \leq t^S_{act}$ +* $T_{lock}(R_{min}) \leq T_{lock}(R(t))$ + +we have that $t \leq t^S_{act} + T_{lock}(R(t))$, or $t - t^S_{act} \leq T_{lock}(R(t)$. Therefore, the protocol cannot be in the Deactivation sub-state at any $t$ between $t_1$ and $t_2$. Since $T_{lock}(R(t)) \leq L_{max}$, the Rage Quit state cannot be entered either. Therefore, the Veto Signalling state cannot be exited, and consequently the proposal is not executable during this time. + +**Veto Signalling state:** If immediately before $t_1$ the protocol is either in the Normal state or the Veto Signalling state, the same reasoning as the previous case applies. Otherwise, it must be in either the Veto Cooldown or Rage Quit states. Since $T_{min}$ is less than the minimum duration of the Veto Signalling state, it cannot be the case that the Veto Signalling state was entered and exited again between $t_{prop}$ and $t_1$. Therefore, at $t_1$ the proposal is not executable in either the Veto Cooldown or Rage Quit state. Then, there are two cases: +* If we remain in the same state until $t_2$, the proposal remains not executable. +* If we transition to another state before $t_2$, it will be to the Veto Signalling state, since $R_{min} > R_1$. Using the same reasoning as above, we can conclude that we cannot exit the Veto Signalling state for any $t_1 \leq t \leq t_2$, and therefore the proposal will remain not executable until at least $t_2$ as well. + +**Rage Quit state:** The same reasoning as the previous case applies. From 7dcb8621046efbd8b7fa10e39d23abcbd9a90172 Mon Sep 17 00:00:00 2001 From: lucasmt <36549752+lucasmt@users.noreply.github.com> Date: Wed, 19 Jun 2024 16:57:56 -0500 Subject: [PATCH 098/134] Edit design review report to fix math markdown syntax --- ...iming_Analysis_of_Dual_Governance_States.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/RV Design Review Report/1_Timing_Analysis_of_Dual_Governance_States.md b/docs/RV Design Review Report/1_Timing_Analysis_of_Dual_Governance_States.md index 3b303024..e415c6b3 100644 --- a/docs/RV Design Review Report/1_Timing_Analysis_of_Dual_Governance_States.md +++ b/docs/RV Design Review Report/1_Timing_Analysis_of_Dual_Governance_States.md @@ -27,33 +27,33 @@ Once the Veto Signalling state is entered, it can be exited in two ways: either While in the Veto Signalling state, the protocol can enter and exit the Deactivation sub-state depending on the current value of the dynamic timelock duration $T_{lock}(R)$, a monotonic function on the current rage quit support $R$. -When first entering the Veto Signalling state, and again whenever the Deactivation sub-state is exited, there is a waiting period of `VetoSignallingMinActiveDuration` ($T^{Sa}_{min}$) when the Deactivation sub-state cannot be entered. Outside of this window (as long as a rage quit is not triggered), the protocol will be in the Deactivation sub-state if the time $\Delta t$ since entering the Veto Signalling state is greater than the current value of $T_{lock}(R)$, and will be in the Veto Signalling parent state otherwise. If the Deactivation sub-state is not exited within `VetoSignallingDeactivationMaxDuration` ($T^{SD}_{max}$) of being entered, it transitions to the Veto Cooldown state. +When first entering the Veto Signalling state, and again whenever the Deactivation sub-state is exited, there is a waiting period of `VetoSignallingMinActiveDuration` ($`T^{Sa}_{min}`$) when the Deactivation sub-state cannot be entered. Outside of this window (as long as a rage quit is not triggered), the protocol will be in the Deactivation sub-state if the time $\Delta t$ since entering the Veto Signalling state is greater than the current value of $T_{lock}(R)$, and will be in the Veto Signalling parent state otherwise. If the Deactivation sub-state is not exited within `VetoSignallingDeactivationMaxDuration` ($T^{SD}_{max}$) of being entered, it transitions to the Veto Cooldown state. With this, we can calculate bounds on the time spent in the Veto Signalling state (including the Deactivation sub-state) before transitioning into Veto Cooldown: -* For the lower bound, the earliest we can transition to the Deactivation sub-state is $T^{Sa}_{min} + 1$ after Veto Signalling is entered, and then the transition to Veto Cooldown happens $T^{SD}_{max} + 1$ after that, giving us $T^{Sa}_{min} + T^{SD}_{max} + 2$. +* For the lower bound, the earliest we can transition to the Deactivation sub-state is $`T^{Sa}_{min} + 1`$ after Veto Signalling is entered, and then the transition to Veto Cooldown happens $T^{SD}_{max} + 1$ after that, giving us $`T^{Sa}_{min} + T^{SD}_{max} + 2`$. * For the upper bound, we can use the insight that, if $R_{max}$ is the highest rage quit support during the time we are in the Veto Signalling state, then it's impossible to exit the Deactivation sub-state (without triggering a rage quit) after $T_{lock}(R_{max})$ has passed since entering Veto Signalling (see "Veto Signalling Maximum Timelock" in the "Proofs" section for details). Therefore, for a given $R_{max}$, the longest delay happens in the following scenario: 1. $R_{max}$ is locked in escrow, making the dynamic timelock duration $T_{lock}(R_{max})$. 2. Shortly before $\Delta t = T_{lock}(R_{max})$ has passed, the rage quit support decreases, and the Deactivation sub-state is entered. 3. At exactly $\Delta t = T_{lock}(R_{max})$, the rage quit support returns to $R_{max}$, and the Deactivation sub-state is exited. 4. At $\Delta t = T_{lock}(R_{max}) + T^{Sa}_{min} + 1$, the waiting period ends and the Deactivation sub-state is entered again. - 5. At $\Delta t = T_{lock}(R_{max}) + T^{Sa}_{min} + 1 + T^{SD}_{max} + 1$, the state transitions to Veto Cooldown. + 5. At $`\Delta t = T_{lock}(R_{max}) + T^{Sa}_{min} + 1 + T^{SD}_{max} + 1`$, the state transitions to Veto Cooldown. In summary, the above analysis gives us the following bounds: -$$ +```math T^{Sa}_{min} + T^{SD}_{max} + 2 \leq t_{exit} - t_{enter} \leq T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2 -$$ +``` -Note that the maximum value of $T_{lock}(R)$ is `DynamicTimelockMaxDuration` ($L_{max}$), so the upper bound can be at most $L_{max} + T^{Sa}_{min} + T^{SD}_{max} + 2$. However, writing it in terms of $R_{max}$ highlights an important security property: the delay in deactivating the Veto Signalling state depends only on the *highest* value of the rage quit support, and cannot be increased further by locking and unlocking funds in the signalling escrow at different times. In other words, the amount of delay an attacker is able to achieve is limited by the amount of stETH they control. +Note that the maximum value of $T_{lock}(R)$ is `DynamicTimelockMaxDuration` ($L_{max}$), so the upper bound can be at most $`L_{max} + T^{Sa}_{min} + T^{SD}_{max} + 2`$. However, writing it in terms of $R_{max}$ highlights an important security property: the delay in deactivating the Veto Signalling state depends only on the *highest* value of the rage quit support, and cannot be increased further by locking and unlocking funds in the signalling escrow at different times. In other words, the amount of delay an attacker is able to achieve is limited by the amount of stETH they control. ### To Rage Quit -The Veto Signalling state can transition to the Rage Quit state at any point after $L_{max}$ has passed, as long as the rage quit support surpasses `SecondSealRageQuitSupport` ($R_2$). This gives us a lower bound of $L_{max}$. For the upper bound, we can adapt the analysis of the Veto Cooldown transition above. Note that if $R_{max} = R_2$, it's possible to delay the transition to the Veto Cooldown state for the maximum time of $L_{max} + T^{Sa}_{min} + T^{SD}_{max} + 2$ before triggering a rage quit at the last possible moment by increasing the rage quit support above $R_2$. Therefore, +The Veto Signalling state can transition to the Rage Quit state at any point after $L_{max}$ has passed, as long as the rage quit support surpasses `SecondSealRageQuitSupport` ($R_2$). This gives us a lower bound of $L_{max}$. For the upper bound, we can adapt the analysis of the Veto Cooldown transition above. Note that if $R_{max} = R_2$, it's possible to delay the transition to the Veto Cooldown state for the maximum time of $`L_{max} + T^{Sa}_{min} + T^{SD}_{max} + 2`$ before triggering a rage quit at the last possible moment by increasing the rage quit support above $R_2$. Therefore, -$$ +```math L_{max} < t_{exit} - t_{enter} \leq L_{max} + T^{Sa}_{min} + T^{SD}_{max} + 2 -$$ +``` ## Veto Cooldown From cb77c9f69bbf2777f0f8f2d9c77bbff4c06bb3e8 Mon Sep 17 00:00:00 2001 From: lucasmt <36549752+lucasmt@users.noreply.github.com> Date: Wed, 19 Jun 2024 17:07:35 -0500 Subject: [PATCH 099/134] Edit design review report to fix math markdown syntax --- .../2_Overall_Bounds_on_Proposal_Execution.md | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/RV Design Review Report/2_Overall_Bounds_on_Proposal_Execution.md b/docs/RV Design Review Report/2_Overall_Bounds_on_Proposal_Execution.md index 077053e1..cfd60d78 100644 --- a/docs/RV Design Review Report/2_Overall_Bounds_on_Proposal_Execution.md +++ b/docs/RV Design Review Report/2_Overall_Bounds_on_Proposal_Execution.md @@ -13,7 +13,7 @@ Rule 4 is meant to guarantee that if a proposal is submitted during the Veto Sig Note that it is technically possible to delay execution of a proposal indefinitely if the protocol transitions continuously between the Veto Signalling and Rage Quit states. However, as mentioned above, triggering a Rage Quit is unlikely to be cost-efficient to an attacker. Furthermore, doing this repeatedly is even more unlikely to be viable, as after exiting the Rage Quit state the funds remain locked in the rage quit escrow for `RageQuitEthClaimTimelock`, which starts as a lengthy duration (60 days with the current proposed values) and increases quadratically with each subsequent rage quit until the protocol returns to the Normal state. -With this in mind, in the rest of this section we consider the bounds on proposal execution assuming no rage quit is triggered. If the Rage Quit state *is* entered, every entry will delay proposal execution for an additional $T^W + T^R + 1$ (keeping in mind that $T^W$ is a non-deterministic duration that depends on external factors and might be different each time the rage quit is triggered), plus a further delay between $T^{Sa}_{min} + T^{SD}_{max} + 2$ and $T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2$ if the Rage Quit state then transitions back to Veto Signalling. However, note that, after this re-entry to Veto Signalling, if the protocol then transitions to Veto Cooldown, any proposals submitted during the previous Rage Quit state or the Veto Signalling state before that will become executable immediately, without needing another round of Veto Signalling. +With this in mind, in the rest of this section we consider the bounds on proposal execution assuming no rage quit is triggered. If the Rage Quit state *is* entered, every entry will delay proposal execution for an additional $T^W + T^R + 1$ (keeping in mind that $T^W$ is a non-deterministic duration that depends on external factors and might be different each time the rage quit is triggered), plus a further delay between $`T^{Sa}_{min} + T^{SD}_{max} + 2`$ and $`T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2`$ if the Rage Quit state then transitions back to Veto Signalling. However, note that, after this re-entry to Veto Signalling, if the protocol then transitions to Veto Cooldown, any proposals submitted during the previous Rage Quit state or the Veto Signalling state before that will become executable immediately, without needing another round of Veto Signalling. More precisely, the following bounds apply to any proposal submitted in the Rage Quit state or the preceding Veto Signalling state, for the time between when the Rage Quit state is exited ($t^R_{exit}$) and when the proposal becomes executable (assuming no further rage quit happens and the minimum timelock $T_{min}$ has already passed): @@ -25,15 +25,15 @@ $$ * If the Rage Quit state exits to Veto Cooldown, then Veto Signalling and Veto Cooldown again (becomes executable as soon as the Veto Cooldown state is entered for the second time): -$$ +```math T^C + T^{Sa}_{min} + T^{SD}_{max} + 3 \leq t_{exe} - t^R_{exit} \leq T^C + T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 3 -$$ +``` * If the Rage Quit state exits to Veto Signalling and then Veto Cooldown (becomes executable as soon as the Veto Cooldown state is entered): -$$ +```math T^{Sa}_{min} + T^{SD}_{max} + 2 \leq t_{exe} - t^R_{exit} \leq T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2 -$$ +``` ## Proposals Submitted in the Normal State @@ -43,11 +43,11 @@ $$ t_{exe} - t_{sub} = T_{min} + 1 $$ -On the other hand, if the protocol transitions to Veto Signalling before this time due to rage quit support surpassing $R_1$, it will become subject to the Veto Signalling dynamic timelock. The shortest possible time for execution in this case happens if the transition happens immediately after the proposal is submitted (at the same timestamp), and soon after that the rage quit support drops below $R_1$ again, making the dynamic timelock 0. In this scenario, the Deactivation sub-state will be entered at $T^{Sa}_{min} + 1$, and then exited to the Veto Cooldown state at $T^{SD}_{max} + 1$, giving the previously-stated $T^{Sa}_{min} + T^{SD}_{max} + 2$ lower bound on the duration of the Veto Signalling state. Note, however, that it's possible, depending on the parameter values, that at this point the minimum timelock $T_{min}$ hasn't passed, so the true lower bound is the highest between the minimum timelock and the minimum duration of the Veto Signalling state: +On the other hand, if the protocol transitions to Veto Signalling before this time due to rage quit support surpassing $R_1$, it will become subject to the Veto Signalling dynamic timelock. The shortest possible time for execution in this case happens if the transition happens immediately after the proposal is submitted (at the same timestamp), and soon after that the rage quit support drops below $R_1$ again, making the dynamic timelock 0. In this scenario, the Deactivation sub-state will be entered at $`T^{Sa}_{min} + 1`$, and then exited to the Veto Cooldown state at $`T^{SD}_{max} + 1`$, giving the previously-stated $`T^{Sa}_{min} + T^{SD}_{max} + 2`$ lower bound on the duration of the Veto Signalling state. Note, however, that it's possible, depending on the parameter values, that at this point the minimum timelock $T_{min}$ hasn't passed, so the true lower bound is the highest between the minimum timelock and the minimum duration of the Veto Signalling state: -$$ +```math \max \{ T_{min} + 1, T^{Sa}_{min} + T^{SD}_{max} + 2 \} \leq t_{exe} - t_{sub} -$$ +``` Note that for the current proposed values, the minimum Veto Signalling duration is slightly higher: @@ -55,13 +55,13 @@ Note that for the current proposed values, the minimum Veto Signalling duration * $T^{Sa}_{min}$ is 5 hours. * $T^{SD}_{max}$ is 3 days. -For the upper bound, the longest possible delay happens when the Veto Signalling state is entered at $T_{min}$ (the last possible moment before the proposal becomes executable in the Normal state) and lasts as long as possible ($T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2$, according to our previous analysis): +For the upper bound, the longest possible delay happens when the Veto Signalling state is entered at $T_{min}$ (the last possible moment before the proposal becomes executable in the Normal state) and lasts as long as possible ($`T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2`$, according to our previous analysis): -$$ +```math t_{exe} - t_{sub} \leq T_{min} + T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2 -$$ +``` -**Note:** With the current proposed values for the parameters, we have the guarantee that $T_{min}$ will have expired by the time the Veto Cooldown state is entered. However, if $T_{min}$ were greater than $T^{Sa}_{min} + T^{SD}_{max}$, it would be possible to enter and exit the Veto Signalling state before the minimum timelock expired. If it were greater than $T^{Sa}_{min} + T^{SD}_{max} + T^C$, it would even be possible to exit Veto Cooldown and re-enter Veto Signalling before it expired. Nevertheless, the above upper bound would still work in those cases. In the first case, the proposal would become executable in the Veto Cooldown state at $T_{min} + 1$, which is less than the upper bound. In the second case, the Veto Signalling state would have to be re-entered at $T_{min}$ at the latest, giving rise to the same bound as above. +**Note:** With the current proposed values for the parameters, we have the guarantee that $T_{min}$ will have expired by the time the Veto Cooldown state is entered. However, if $T_{min}$ were greater than $`T^{Sa}_{min} + T^{SD}_{max}`$, it would be possible to enter and exit the Veto Signalling state before the minimum timelock expired. If it were greater than $`T^{Sa}_{min} + T^{SD}_{max} + T^C`$, it would even be possible to exit Veto Cooldown and re-enter Veto Signalling before it expired. Nevertheless, the above upper bound would still work in those cases. In the first case, the proposal would become executable in the Veto Cooldown state at $T_{min} + 1$, which is less than the upper bound. In the second case, the Veto Signalling state would have to be re-entered at $T_{min}$ at the latest, giving rise to the same bound as above. ## Proposals Submitted in the Veto Signalling State @@ -76,25 +76,25 @@ In the first case, the shortest time between proposal submission and execution h Meanwhile, the longest time to execution happens in the following scenario: 1. The proposal is submitted as soon as the Veto Signalling state is first entered. -2. The longest possible duration of $T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2$ passes before transitioning to Veto Cooldown. +2. The longest possible duration of $`T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2`$ passes before transitioning to Veto Cooldown. 3. After $T^C + 1$, Veto Cooldown transitions to the Normal state and the proposal becomes executable. However, if either of these scenarios takes less time than $T_{min}$ (again, not the case for the current proposed values), then we'll have to wait for the minimum timelock to pass before executing the proposal, giving us the following final bounds: -$$ +```math \max \{ T_{min} + 1, T^{SD}_{max} + T^C + 2 \} \leq t_{exe} - t_{sub} -$$ +``` -$$ +```math t_{exe} - t_{sub} \leq \max \{ T_{min} + 1, T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + T^C + 3 \} -$$ +``` -In the second case, where Veto Cooldown transitions back to Veto Signalling instead of the Normal state, we need to add the duration of the Veto Signalling state again to the bounds. For the lower bound, we add the minimum duration of $T^{Sa}_{min} + T^{SD}_{max} + 2$, giving us $T^{Sa}_{min} + 2T^{SD}_{max} + T^C + 4$. For the upper bound, we add the maximum duration of $T_{lock}(R'_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2$, where $R'_{max}$, the maximum rage quit support during the second time through the Veto Signalling state, might be different from $R_{max}$. This leaves us with the following bounds: +In the second case, where Veto Cooldown transitions back to Veto Signalling instead of the Normal state, we need to add the duration of the Veto Signalling state again to the bounds. For the lower bound, we add the minimum duration of $`T^{Sa}_{min} + T^{SD}_{max} + 2`$, giving us $`T^{Sa}_{min} + 2T^{SD}_{max} + T^C + 4`$. For the upper bound, we add the maximum duration of $`T_{lock}(R'_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2`$, where $`R'_{max}`$, the maximum rage quit support during the second time through the Veto Signalling state, might be different from $R_{max}$. This leaves us with the following bounds: -$$ +```math \max \{ T_{min} + 1, T^{Sa}_{min} + 2T^{SD}_{max} + T^C + 4 \} \leq t_{exe} - t_{sub} -$$ +``` -$$ +```math t_{exe} - t_{sub} \leq \max \{ T_{min} + 1, T_{lock}(R_{max}) + T_{lock}(R'_{max}) + 2T^{Sa}_{min} + 2T^{SD}_{max} + T^C + 5 \} -$$ +``` From b41ffe26e958f2ce150f77571e910c5b43ec7238 Mon Sep 17 00:00:00 2001 From: lucasmt <36549752+lucasmt@users.noreply.github.com> Date: Wed, 19 Jun 2024 17:15:40 -0500 Subject: [PATCH 100/134] Edit design review report to fix math markdown syntax --- docs/RV Design Review Report/4_Proofs.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/RV Design Review Report/4_Proofs.md b/docs/RV Design Review Report/4_Proofs.md index 5e3b0c3f..3c460c21 100644 --- a/docs/RV Design Review Report/4_Proofs.md +++ b/docs/RV Design Review Report/4_Proofs.md @@ -5,30 +5,30 @@ ### Property Suppose that at time $t^S_{act}$ the Dual Governance protocol enters the Veto Signalling state. Then, if a rage quit isn't triggered, and assuming that state transitions are activated as soon as they becomes enabled, the protocol will transition to the Veto Cooldown state at a time $t^C_{act} > t^S_{act}$ such that -$$ +```math t^C_{act} \leq t^S_{act} + T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2 -$$ +``` where $R_{max}$ is the maximum rage quit support between $t^S_{act}$ and $t^C_{act}$. ### Proof **Observation 1:** First note that if we are in the Deactivation sub-state at any time after $t^S_{act} + T_{lock}(R_{max})$, we will not exit the sub-state until we transition to Veto Cooldown. To exit back to the parent state, one of the following would have to be true for some $t > t^S_{act} + T_{lock}(R_{max})$: -1. $t \leq t^S_{act} + T_{lock}(R(t))$. Since $R_{max} \geq R(t)$ for all $t$ between $t^S_{act}$ and $t^{C}_{act}$, and $T_{lock}(R)$ is monotonic, this is not possible once $t > t^S_{act} + T_{lock}(R_{max})$. +1. $t \leq t^S_{act} + T_{lock}(R(t))$. Since $R_{max} \geq R(t)$ for all $t$ between $t^S_{act}$ and $`t^{C}_{act}`$, and $T_{lock}(R)$ is monotonic, this is not possible once $t > t^S_{act} + T_{lock}(R_{max})$. 2. $R(t) > R_2$. This would imply $R_{max} \geq R(t) > R_2$, and therefore $T_{lock}(R_{max}) = L_{max}$. Since $t - t^S_{act} > T_{lock}(R_{max}) = L_{max}$ and $R(t) > R_2$, after exiting the Deactivation sub-state a rage quit would be immediately triggered. Since we are only considering scenarios where no rage quit happens, this case is also impossible. -Now, let $t^{SD}_{act}$ be the last time the Deactivation sub-state is entered before $t^C_{act}$. We will first prove that $t^{SD}_{act} \leq t^S_{act} + T_{lock}(R_{max}) + T^{Sa}_{min} + 1$: -* **Case 1:** If $t^{SD}_{act} \leq t^S_{act} + T_{lock}(R_{max}) + 1$, we are done. +Now, let $`t^{SD}_{act}`$ be the last time the Deactivation sub-state is entered before $t^C_{act}$. We will first prove that $`t^{SD}_{act} \leq t^S_{act} + T_{lock}(R_{max}) + T^{Sa}_{min} + 1`$: +* **Case 1:** If $`t^{SD}_{act} \leq t^S_{act} + T_{lock}(R_{max}) + 1`$, we are done. * **Case 2:** Otherwise, $t^S_{act} + T_{lock}(R_{max}) + 1 < t^{SD}_{act}$. * Then, from Observation 1 above, at $t_1 = t^S_{act} + T_{lock}(R_{max}) + 1$ we cannot be in the Deactivation sub-state (since we wouldn't be able to exit the sub-state to enter again at $t^{SD}_{act}$). - * Since this is the case even though $t_1 - t^S_{act} > T_{lock}(R(t_1))$, it must be because $t_1 - \max \{ t^S_{act}, t^S_{react} \} \leq T^{Sa}_{min}$, where $t^S_{react} < t_1$ was the last time the Deactivation sub-state was exited, or 0 if it has never been entered (note that $t_1$ must be strictly greater than $t^S_{react}$, since it would impossible to transition back to the parent state at $t^S_{act} + T_{lock}(R_{max}) + 1$). - * In this case, as $t - t^S_{act} > T_{lock}(R(t))$ will remain true for any future $t > t_1$, the transition at $t^{SD}_{act}$ must happen as soon as $t - \max \{ t^S_{act}, t^S_{react} \} > T^{Sa}_{min}$ becomes true. - * Since $t_1$ is strictly greater than $\max \{ t^S_{act}, t^S_{react} \}$, the latest this can happen is at $t_1 + T^{Sa}_{min} = t^S_{act} + T_{lock}(R_{max}) + T^{Sa}_{min} + 1$. + * Since this is the case even though $t_1 - t^S_{act} > T_{lock}(R(t_1))$, it must be because $`t_1 - \max \{ t^S_{act}, t^S_{react} \} \leq T^{Sa}_{min}`$, where $t^S_{react} < t_1$ was the last time the Deactivation sub-state was exited, or 0 if it has never been entered (note that $t_1$ must be strictly greater than $t^S_{react}$, since it would impossible to transition back to the parent state at $t^S_{act} + T_{lock}(R_{max}) + 1$). + * In this case, as $t - t^S_{act} > T_{lock}(R(t))$ will remain true for any future $t > t_1$, the transition at $`t^{SD}_{act}`$ must happen as soon as $t - \max \{ t^S_{act}, t^S_{react} \} > T^{Sa}_{min}$ becomes true. + * Since $t_1$ is strictly greater than $\max \{ t^S_{act}, t^S_{react} \}$, the latest this can happen is at $`t_1 + T^{Sa}_{min} = t^S_{act} + T_{lock}(R_{max}) + T^{Sa}_{min} + 1`$. -Finally, since the Deactivation sub-state does not return to the parent state after $t^{SD}_{act}$, and no rage quit is triggered, it will transition to Veto Cooldown as soon as $t - t^{SD}_{act} > T^{SD}_{max}$. Therefore, $t^C_{act} = t^{SD}_{act} + T^{SD}_{max} + 1 \leq t^S_{act} + T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2$. +Finally, since the Deactivation sub-state does not return to the parent state after $`t^{SD}_{act}`$, and no rage quit is triggered, it will transition to Veto Cooldown as soon as $`t - t^{SD}_{act} > T^{SD}_{max}`$. Therefore, $`t^C_{act} = t^{SD}_{act} + T^{SD}_{max} + 1 \leq t^S_{act} + T_{lock}(R_{max}) + T^{Sa}_{min} + T^{SD}_{max} + 2`$. ### Caveats -* This proof assumes that the transitions at $t^{SD}_{act}$ and $t^C_{act}$ happen immediately as soon as they are enabled. Any delay $d$ in performing either of these transitions gets added to the upper bound. For instance, if the last transition to the Deactivation sub-state is only performed at $t^{SD}_{act} + d$, then the earliest that Veto Cooldown can be entered is $t^{SD}_{act} + d + T^{SD}_{max} + 1$. On the other hand, any delay in performing previous transitions between the Deactivation sub-state and the parent state (before the last one at $t^{SD}_{act}$) does not increase the upper bound, since it does not change the time $t_0 + T_{lock}(R_{max}) + 1$. +* This proof assumes that the transitions at $`t^{SD}_{act}`$ and $t^C_{act}$ happen immediately as soon as they are enabled. Any delay $d$ in performing either of these transitions gets added to the upper bound. For instance, if the last transition to the Deactivation sub-state is only performed at $`t^{SD}_{act} + d`$, then the earliest that Veto Cooldown can be entered is $`t^{SD}_{act} + d + T^{SD}_{max} + 1`$. On the other hand, any delay in performing previous transitions between the Deactivation sub-state and the parent state (before the last one at $`t^{SD}_{act}`$) does not increase the upper bound, since it does not change the time $t_0 + T_{lock}(R_{max}) + 1$. ## Staker Reaction Time From 7c33ef647ac9489184667eeeb519631027b0df92 Mon Sep 17 00:00:00 2001 From: lucasmt Date: Wed, 19 Jun 2024 17:25:53 -0500 Subject: [PATCH 101/134] Add report pdf file --- .../Dual_Governance_Design_Review.pdf | Bin 0 -> 554786 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/RV Design Review Report/Dual_Governance_Design_Review.pdf diff --git a/docs/RV Design Review Report/Dual_Governance_Design_Review.pdf b/docs/RV Design Review Report/Dual_Governance_Design_Review.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9ab9704a59d10f24d47d84f1cd7d432ef1a6cd52 GIT binary patch literal 554786 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 z ziVBr~tq@?C!=MUqYg%RsvoC7B`fG*JC;J?_v*x6Q$CGThr?QqtEj3M;?5Zxu<>dI= z8-1krTPkYY{z{GW4fm=@*^ot}_CK5F{dE<~^YYA%)%#IoLFx{S|C$#%ce_`OySv#( z0|(nnN)||CroY-5?B*UhxNT8Me#u;EC$QQxT?ZY4#fQq5$T=2h)?D+d$vrZW68^(n#rUxbRS(BD21_&(IkX`^kkm2^V{p)jJBg~SW;0S zy8?YUt=Aq)97V`wW@sU`NJf(L1AKY1pYqZ7s2Hr=3qp+c5^c>{r1cMl`>6ACCT~<2 zn1B%+c~Pr(vJj7=?2vM#3^f9iKhA5GgLjD;@%rj2%HH%5kqwL`x&7Q1=zDFev#vkQO zY;su>iV`k)Dw`&+M=vfbG$<}vV18kX+>TF1GDHLjUe-b)jiP048_Tv~_oWodNs
3hk38~%ohr*Jl4r15%B8Sq?^Nl>C&tSnId?l1 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 z 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 z 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�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 Dxd4!k literal 0 HcmV?d00001 From a84afd9343e17cdd3a5b81d3ad01ed1c60a8f985 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 20 Jun 2024 13:11:52 +0300 Subject: [PATCH 102/134] 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 95bde2694dca5ecfa53a01237f47d974c3bbc333 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 20 Jun 2024 14:20:12 +0400 Subject: [PATCH 103/134] Update tests to match new method and error signatures --- contracts/EmergencyProtectedTimelock.sol | 3 ++- test/scenario/happy-path-plan-b.t.sol | 10 +++++++++- test/unit/mocks/TimelockMock.sol | 25 ++++++++++++++++++------ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index b82f8272..019feb72 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -2,13 +2,14 @@ pragma solidity 0.8.23; import {IOwnable} from "./interfaces/IOwnable.sol"; +import {ITimelock} from "./interfaces/ITimelock.sol"; import {Proposal, Proposals, ExecutorCall} from "./libraries/Proposals.sol"; import {EmergencyProtection, EmergencyState} from "./libraries/EmergencyProtection.sol"; import {ConfigurationProvider} from "./ConfigurationProvider.sol"; -contract EmergencyProtectedTimelock is ConfigurationProvider { +contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { using Proposals for Proposals.State; using EmergencyProtection for EmergencyProtection.State; diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index 7424b8d6..33f7fef9 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -426,9 +426,17 @@ contract PlanBSetup is ScenarioTestBlueprint { vm.warp(block.timestamp + _EMERGENCY_PROTECTION_DURATION + 1); } + EmergencyState memory emergencyState = _timelock.getEmergencyState(); + // attempt to activate emergency protection fails { - vm.expectRevert(EmergencyProtection.EmergencyCommitteeExpired.selector); + vm.expectRevert( + abi.encodeWithSelector( + EmergencyProtection.EmergencyCommitteeExpired.selector, + block.timestamp, + emergencyState.protectedTill + ) + ); vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); _timelock.activateEmergencyMode(); } diff --git a/test/unit/mocks/TimelockMock.sol b/test/unit/mocks/TimelockMock.sol index 1ff6f631..b754e09f 100644 --- a/test/unit/mocks/TimelockMock.sol +++ b/test/unit/mocks/TimelockMock.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {ITimelock} from "contracts/interfaces/ITimelock.sol"; import {ExecutorCall} from "contracts/libraries/Proposals.sol"; -contract TimelockMock { +contract TimelockMock is ITimelock { uint8 public constant OFFSET = 1; - + mapping(uint256 => bool) public canScheduleProposal; uint256[] public submittedProposals; @@ -20,22 +21,31 @@ contract TimelockMock { canScheduleProposal[newProposalId] = false; return newProposalId; } - function schedule(uint256 proposalId) external { + + function schedule(uint256 proposalId) external returns (uint256 submittedAt) { if (canScheduleProposal[proposalId] == false) { revert(); } scheduledProposals.push(proposalId); } + function execute(uint256 proposalId) external { executedProposals.push(proposalId); } - function canSchedule(uint256 proposalId) external view returns (bool) { + + function canExecute(uint256 proposalId) external view returns (bool) { + revert("Not Implemented"); + } + + function canSchedule(uint256 proposalId) external view returns (bool) { return canScheduleProposal[proposalId]; - } + } + function cancelAllNonExecutedProposals() external { lastCancelledProposalId = submittedProposals[submittedProposals.length - 1]; } + function setSchedule(uint256 proposalId) external { canScheduleProposal[proposalId] = true; } @@ -43,13 +53,16 @@ contract TimelockMock { function getSubmittedProposals() external view returns (uint256[] memory) { return submittedProposals; } + function getScheduledProposals() external view returns (uint256[] memory) { return scheduledProposals; } + function getExecutedProposals() external view returns (uint256[] memory) { return executedProposals; } + function getLastCancelledProposalId() external view returns (uint256) { return lastCancelledProposalId; } -} \ No newline at end of file +} From e7e03cae76c02becaebebb1965216a0fdc0cf676 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Thu, 20 Jun 2024 13:40:29 +0300 Subject: [PATCH 104/134] fix: mock fix --- test/unit/mocks/TimelockMock.sol | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/test/unit/mocks/TimelockMock.sol b/test/unit/mocks/TimelockMock.sol index 1ff6f631..680dc98e 100644 --- a/test/unit/mocks/TimelockMock.sol +++ b/test/unit/mocks/TimelockMock.sol @@ -5,7 +5,7 @@ import {ExecutorCall} from "contracts/libraries/Proposals.sol"; contract TimelockMock { uint8 public constant OFFSET = 1; - + mapping(uint256 => bool) public canScheduleProposal; uint256[] public submittedProposals; @@ -20,22 +20,28 @@ contract TimelockMock { canScheduleProposal[newProposalId] = false; return newProposalId; } - function schedule(uint256 proposalId) external { + + function schedule(uint256 proposalId) external returns (uint256 submittedAt) { if (canScheduleProposal[proposalId] == false) { revert(); } scheduledProposals.push(proposalId); + return 0; } + function execute(uint256 proposalId) external { executedProposals.push(proposalId); } - function canSchedule(uint256 proposalId) external view returns (bool) { + + function canSchedule(uint256 proposalId) external view returns (bool) { return canScheduleProposal[proposalId]; - } + } + function cancelAllNonExecutedProposals() external { lastCancelledProposalId = submittedProposals[submittedProposals.length - 1]; } + function setSchedule(uint256 proposalId) external { canScheduleProposal[proposalId] = true; } @@ -43,13 +49,16 @@ contract TimelockMock { function getSubmittedProposals() external view returns (uint256[] memory) { return submittedProposals; } + function getScheduledProposals() external view returns (uint256[] memory) { return scheduledProposals; } + function getExecutedProposals() external view returns (uint256[] memory) { return executedProposals; } + function getLastCancelledProposalId() external view returns (uint256) { return lastCancelledProposalId; } -} \ No newline at end of file +} From d09a26308e9936510799e78b95a3d0aac0747632 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 20 Jun 2024 14:03:14 +0300 Subject: [PATCH 105/134] 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 691032433dba5e000abfd126868e525c3f896582 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 20 Jun 2024 21:04:56 +0400 Subject: [PATCH 106/134] Accounting in Escrow using only stETH --- contracts/Escrow.sol | 62 +++++------- contracts/libraries/AssetsAccounting.sol | 48 +-------- test/scenario/escrow.t.sol | 124 +++++++++++++---------- test/utils/scenario-test-blueprint.sol | 17 +++- 4 files changed, 114 insertions(+), 137 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 2488acb5..7b967e9b 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -27,7 +27,6 @@ enum EscrowState { struct VetoerState { uint256 stETHShares; - uint256 wstETHShares; uint256 unstETHShares; } @@ -80,6 +79,7 @@ contract Escrow is IEscrow { _escrowState = EscrowState.SignallingEscrow; _dualGovernance = IDualGovernance(dualGovernance); + ST_ETH.approve(address(WST_ETH), type(uint256).max); ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); WST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); } @@ -90,54 +90,37 @@ contract Escrow is IEscrow { function lockStETH(uint256 amount) external { uint256 shares = ST_ETH.getSharesByPooledEth(amount); - _accounting.accountStETHLock(msg.sender, shares); + _accounting.accountStETHSharesLock(msg.sender, shares); ST_ETH.transferSharesFrom(msg.sender, address(this), shares); _activateNextGovernanceState(); } function unlockStETH() external { _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); - uint256 sharesUnlocked = _accounting.accountStETHUnlock(msg.sender); + uint256 sharesUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); ST_ETH.transferShares(msg.sender, sharesUnlocked); _activateNextGovernanceState(); } - function requestWithdrawalsStETH(uint256[] calldata amounts) external returns (uint256[] memory unstETHIds) { - unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(amounts, address(this)); - WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); - - uint256 sharesTotal = 0; - for (uint256 i = 0; i < statuses.length; ++i) { - sharesTotal += statuses[i].amountOfShares; - } - _accounting.accountStETHUnlock(msg.sender, sharesTotal); - _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); - } - // --- // Lock / Unlock wstETH // --- function lockWstETH(uint256 amount) external { - _accounting.accountWstETHLock(msg.sender, amount); WST_ETH.transferFrom(msg.sender, address(this), amount); + uint256 stETHAmount = WST_ETH.unwrap(amount); + _accounting.accountStETHSharesLock(msg.sender, ST_ETH.getSharesByPooledEth(stETHAmount)); _activateNextGovernanceState(); } function unlockWstETH() external returns (uint256 wstETHUnlocked) { _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); - wstETHUnlocked = _accounting.accountWstETHUnlock(msg.sender); - WST_ETH.transfer(msg.sender, wstETHUnlocked); + wstETHUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); + uint256 wstETHAmount = WST_ETH.wrap(ST_ETH.getPooledEthByShares(wstETHUnlocked)); + WST_ETH.transfer(msg.sender, wstETHAmount); _activateNextGovernanceState(); } - function requestWithdrawalsWstETH(uint256[] calldata amounts) external returns (uint256[] memory unstETHIds) { - uint256 totalAmount = ArrayUtils.sum(amounts); - _accounting.accountWstETHUnlock(msg.sender, totalAmount); - unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawalsWstETH(amounts, address(this)); - _accounting.accountUnstETHLock(msg.sender, unstETHIds, WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds)); - } - // --- // Lock / Unlock unstETH // --- @@ -166,6 +149,22 @@ contract Escrow is IEscrow { _accounting.accountUnstETHFinalized(unstETHIds, claimableAmounts); } + // --- + // Convert to NFT + // --- + + function requestWithdrawals(uint256[] calldata stEthAmounts) external returns (uint256[] memory unstETHIds) { + unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(stEthAmounts, address(this)); + WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + + uint256 sharesTotal = 0; + for (uint256 i = 0; i < statuses.length; ++i) { + sharesTotal += statuses[i].amountOfShares; + } + _accounting.accountStETHSharesUnlock(msg.sender, sharesTotal); + _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); + } + // --- // State Updates // --- @@ -230,19 +229,13 @@ contract Escrow is IEscrow { // Withdraw Logic // --- - function withdrawStETHAsETH() external { - _checkEscrowState(EscrowState.RageQuitEscrow); - _checkWithdrawalsTimelockPassed(); - Address.sendValue(payable(msg.sender), _accounting.accountStETHWithdraw(msg.sender)); - } - - function withdrawWstETHAsETH() external { + function withdrawETH() external { _checkEscrowState(EscrowState.RageQuitEscrow); _checkWithdrawalsTimelockPassed(); - Address.sendValue(payable(msg.sender), _accounting.accountWstETHWithdraw(msg.sender)); + Address.sendValue(payable(msg.sender), _accounting.accountStETHSharesWithdraw(msg.sender)); } - function withdrawUnstETHAsETH(uint256[] calldata unstETHIds) external { + function withdrawETH(uint256[] calldata unstETHIds) external { _checkEscrowState(EscrowState.RageQuitEscrow); _checkWithdrawalsTimelockPassed(); Address.sendValue(payable(msg.sender), _accounting.accountUnstETHWithdraw(msg.sender, unstETHIds)); @@ -259,7 +252,6 @@ contract Escrow is IEscrow { function getVetoerState(address vetoer) external view returns (VetoerState memory vetoerState) { LockedAssetsStats memory stats = _accounting.assets[vetoer]; vetoerState.stETHShares = stats.stETHShares; - vetoerState.wstETHShares = stats.wstETHShares; vetoerState.unstETHShares = stats.unstETHShares; } diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index afee69ec..acbfefc5 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -26,7 +26,6 @@ struct WithdrawalRequest { struct LockedAssetsStats { uint128 stETHShares; - uint128 wstETHShares; uint128 unstETHShares; uint128 sharesFinalized; uint128 amountFinalized; @@ -47,10 +46,6 @@ library AssetsAccounting { event StETHUnlocked(address indexed vetoer, uint256 shares); event StETHWithdrawn(address indexed vetoer, uint256 stETHShares, uint256 ethAmount); - event WstETHLocked(address indexed vetoer, uint256 shares); - event WstETHUnlocked(address indexed vetoer, uint256 shares); - event WstETHWithdrawn(address indexed vetoer, uint256 wstETHShares, uint256 ethAmount); - event UnstETHLocked(address indexed vetoer, uint256[] ids, uint256 shares); event UnstETHUnlocked( address indexed vetoer, @@ -99,7 +94,7 @@ library AssetsAccounting { // stETH Operations Accounting // --- - function accountStETHLock(State storage self, address vetoer, uint256 shares) internal { + function accountStETHSharesLock(State storage self, address vetoer, uint256 shares) internal { _checkNonZeroSharesLock(vetoer, shares); uint128 sharesUint128 = shares.toUint128(); self.assets[vetoer].stETHShares += sharesUint128; @@ -108,11 +103,11 @@ library AssetsAccounting { emit StETHLocked(vetoer, shares); } - function accountStETHUnlock(State storage self, address vetoer) internal returns (uint128 sharesUnlocked) { - sharesUnlocked = accountStETHUnlock(self, vetoer, self.assets[vetoer].stETHShares); + function accountStETHSharesUnlock(State storage self, address vetoer) internal returns (uint128 sharesUnlocked) { + sharesUnlocked = accountStETHSharesUnlock(self, vetoer, self.assets[vetoer].stETHShares); } - function accountStETHUnlock( + function accountStETHSharesUnlock( State storage self, address vetoer, uint256 shares @@ -124,7 +119,7 @@ library AssetsAccounting { emit StETHUnlocked(vetoer, sharesUnlocked); } - function accountStETHWithdraw(State storage self, address vetoer) internal returns (uint256 ethAmount) { + function accountStETHSharesWithdraw(State storage self, address vetoer) internal returns (uint256 ethAmount) { uint256 stETHShares = self.assets[vetoer].stETHShares; _checkNonZeroSharesWithdraw(vetoer, stETHShares); self.assets[vetoer].stETHShares = 0; @@ -140,39 +135,6 @@ library AssetsAccounting { _checkAssetsUnlockDelayPassed(self, delay, vetoer); } - function accountWstETHLock(State storage self, address vetoer, uint256 shares) internal { - _checkNonZeroSharesLock(vetoer, shares); - uint128 sharesUint128 = shares.toUint128(); - self.assets[vetoer].wstETHShares += sharesUint128; - self.assets[vetoer].lastAssetsLockTimestamp = TimeUtils.timestamp(); - self.totals.shares += sharesUint128; - emit WstETHLocked(vetoer, shares); - } - - function accountWstETHUnlock(State storage self, address vetoer) internal returns (uint128 sharesUnlocked) { - sharesUnlocked = accountWstETHUnlock(self, vetoer, self.assets[vetoer].wstETHShares); - } - - function accountWstETHUnlock( - State storage self, - address vetoer, - uint256 shares - ) internal returns (uint128 sharesUnlocked) { - _checkNonZeroSharesUnlock(vetoer, shares); - sharesUnlocked = shares.toUint128(); - self.totals.shares -= sharesUnlocked; - self.assets[vetoer].wstETHShares -= sharesUnlocked; - emit WstETHUnlocked(vetoer, sharesUnlocked); - } - - function accountWstETHWithdraw(State storage self, address vetoer) internal returns (uint256 ethAmount) { - uint256 wstETHShares = self.assets[vetoer].wstETHShares; - _checkNonZeroSharesWithdraw(vetoer, wstETHShares); - self.assets[vetoer].wstETHShares = 0; - ethAmount = self.totals.amountClaimed * wstETHShares / self.totals.shares; - emit WstETHWithdrawn(vetoer, wstETHShares, ethAmount); - } - // --- // unstETH Operations Accounting // --- diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 66a07544..10d590dd 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -88,29 +88,35 @@ contract EscrowHappyPath is TestHelpers { function test_lock_unlock() public { uint256 firstVetoerStETHBalanceBefore = _ST_ETH.balanceOf(_VETOER_1); - uint256 firstVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_1); - - uint256 secondVetoerStETHBalanceBefore = _ST_ETH.balanceOf(_VETOER_2); uint256 secondVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_2); - _lockStETH(_VETOER_1, 10 ** 18); - _lockWstETH(_VETOER_1, 2 * 10 ** 18); + uint256 firstVetoerLockStETHAmount = 1 ether; + uint256 firstVetoerLockWstETHAmount = 2 ether; + + uint256 secondVetoerLockStETHAmount = 3 ether; + uint256 secondVetoerLockWstETHAmount = 5 ether; + + _lockStETH(_VETOER_1, firstVetoerLockStETHAmount); + _lockWstETH(_VETOER_1, firstVetoerLockWstETHAmount); - _lockStETH(_VETOER_2, 3 * 10 ** 18); - _lockWstETH(_VETOER_2, 5 * 10 ** 18); + _lockStETH(_VETOER_2, secondVetoerLockStETHAmount); + _lockWstETH(_VETOER_2, secondVetoerLockWstETHAmount); _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); _unlockStETH(_VETOER_1); - _unlockWstETH(_VETOER_1); - _unlockStETH(_VETOER_2); - _unlockWstETH(_VETOER_2); - - assertEq(firstVetoerWstETHBalanceBefore, _WST_ETH.balanceOf(_VETOER_1)); - assertApproxEqAbs(firstVetoerStETHBalanceBefore, _ST_ETH.balanceOf(_VETOER_1), 1); + assertApproxEqAbs( + _ST_ETH.balanceOf(_VETOER_1), + firstVetoerStETHBalanceBefore + _ST_ETH.getPooledEthByShares(firstVetoerLockWstETHAmount), + 1 + ); - assertEq(secondVetoerWstETHBalanceBefore, _WST_ETH.balanceOf(_VETOER_2)); - assertApproxEqAbs(secondVetoerStETHBalanceBefore, _ST_ETH.balanceOf(_VETOER_2), 1); + _unlockWstETH(_VETOER_2); + assertApproxEqAbs( + secondVetoerWstETHBalanceBefore, + _WST_ETH.balanceOf(_VETOER_2), + secondVetoerWstETHBalanceBefore + _ST_ETH.getSharesByPooledEth(secondVetoerLockWstETHAmount) + ); } function test_lock_unlock_w_rebase() public { @@ -122,6 +128,9 @@ contract EscrowHappyPath is TestHelpers { uint256 secondVetoerStETHShares = _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount); uint256 secondVetoerWstETHAmount = 17 * 10 ** 18; + uint256 firstVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_1); + uint256 secondVetoerStETHSharesBefore = _ST_ETH.sharesOf(_VETOER_2); + _lockStETH(_VETOER_1, firstVetoerStETHAmount); _lockWstETH(_VETOER_1, firstVetoerWstETHAmount); @@ -138,25 +147,24 @@ contract EscrowHappyPath is TestHelpers { _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); - _unlockStETH(_VETOER_1); _unlockWstETH(_VETOER_1); - - _unlockStETH(_VETOER_2); - _unlockWstETH(_VETOER_2); - assertApproxEqAbs( - _ST_ETH.getPooledEthByShares(firstVetoerStETHSharesAfterRebase + firstVetoerStETHShares), - _ST_ETH.balanceOf(_VETOER_1), - 1 + firstVetoerWstETHBalanceBefore + firstVetoerStETHShares, + _WST_ETH.balanceOf(_VETOER_1), + // Even though the wstETH itself doesn't have rounding issues, the Escrow contract wraps stETH into wstETH + // so the the rounding issue may happen because of it. Another rounding may happen on the converting stETH amount + // into shares via _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount) + 2 ); - assertEq(firstVetoerWstETHBalanceAfterRebase + firstVetoerWstETHAmount, _WST_ETH.balanceOf(_VETOER_1)); + + _unlockStETH(_VETOER_2); assertApproxEqAbs( - _ST_ETH.getPooledEthByShares(secondVetoerStETHSharesAfterRebase + secondVetoerStETHShares), + // all locked stETH and wstETH was withdrawn as stETH + _ST_ETH.getPooledEthByShares(secondVetoerStETHSharesBefore + secondVetoerWstETHAmount), _ST_ETH.balanceOf(_VETOER_2), 1 ); - assertEq(secondVetoerWstETHBalanceAfterRebase + secondVetoerWstETHAmount, _WST_ETH.balanceOf(_VETOER_2)); } function test_lock_unlock_w_negative_rebase() public { @@ -165,11 +173,9 @@ contract EscrowHappyPath is TestHelpers { uint256 secondVetoerStETHAmount = 13 * 10 ** 18; uint256 secondVetoerWstETHAmount = 17 * 10 ** 18; + uint256 secondVetoerStETHShares = _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount); uint256 firstVetoerStETHSharesBefore = _ST_ETH.sharesOf(_VETOER_1); - uint256 firstVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_1); - - uint256 secondVetoerStETHSharesBefore = _ST_ETH.sharesOf(_VETOER_2); uint256 secondVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_2); _lockStETH(_VETOER_1, firstVetoerStETHAmount); @@ -183,16 +189,23 @@ contract EscrowHappyPath is TestHelpers { _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); _unlockStETH(_VETOER_1); - _unlockWstETH(_VETOER_1); + assertApproxEqAbs( + // all locked stETH and wstETH was withdrawn as stETH + _ST_ETH.getPooledEthByShares(firstVetoerStETHSharesBefore + firstVetoerWstETHAmount), + _ST_ETH.balanceOf(_VETOER_1), + 1 + ); - _unlockStETH(_VETOER_2); _unlockWstETH(_VETOER_2); - assertApproxEqAbs(_ST_ETH.getPooledEthByShares(firstVetoerStETHSharesBefore), _ST_ETH.balanceOf(_VETOER_1), 1); - assertEq(firstVetoerWstETHBalanceBefore, _WST_ETH.balanceOf(_VETOER_1)); - - assertApproxEqAbs(_ST_ETH.getPooledEthByShares(secondVetoerStETHSharesBefore), _ST_ETH.balanceOf(_VETOER_2), 1); - assertEq(secondVetoerWstETHBalanceBefore, _WST_ETH.balanceOf(_VETOER_2)); + assertApproxEqAbs( + secondVetoerWstETHBalanceBefore + secondVetoerStETHShares, + _WST_ETH.balanceOf(_VETOER_2), + // Even though the wstETH itself doesn't have rounding issues, the Escrow contract wraps stETH into wstETH + // so the the rounding issue may happen because of it. Another rounding may happen on the converting stETH amount + // into shares via _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount) + 2 + ); } function test_lock_unlock_withdrawal_nfts() public { @@ -275,8 +288,7 @@ contract EscrowHappyPath is TestHelpers { _lockUnstETH(_VETOER_1, unstETHIds); VetoerState memory vetoerState = escrow.getVetoerState(_VETOER_1); - assertApproxEqAbs(vetoerState.stETHShares, sharesToLock, 1); - assertEq(vetoerState.wstETHShares, sharesToLock); + assertApproxEqAbs(vetoerState.stETHShares, 2 * sharesToLock, 1); assertApproxEqAbs(vetoerState.unstETHShares, _ST_ETH.getSharesByPooledEth(2e18), 1); uint256 rageQuitSupport = escrow.getRageQuitSupport(); @@ -370,12 +382,12 @@ contract EscrowHappyPath is TestHelpers { // but it can't be withdrawn before withdrawal timelock has passed vm.expectRevert(); vm.prank(_VETOER_1); - escrow.withdrawUnstETHAsETH(unstETHIds); + escrow.withdrawETH(unstETHIds); } vm.expectRevert(); vm.prank(_VETOER_1); - escrow.withdrawStETHAsETH(); + escrow.withdrawETH(); _wait(_RAGE_QUIT_EXTRA_TIMELOCK + 1); assertEq(escrow.isRageQuitFinalized(), true); @@ -383,8 +395,8 @@ contract EscrowHappyPath is TestHelpers { _wait(_RAGE_QUIT_WITHDRAWALS_TIMELOCK + 1); vm.startPrank(_VETOER_1); - escrow.withdrawStETHAsETH(); - escrow.withdrawUnstETHAsETH(unstETHIds); + escrow.withdrawETH(); + escrow.withdrawETH(unstETHIds); vm.stopPrank(); } @@ -424,7 +436,7 @@ contract EscrowHappyPath is TestHelpers { _wait(_RAGE_QUIT_WITHDRAWALS_TIMELOCK + 1); vm.startPrank(_VETOER_1); - escrow.withdrawUnstETHAsETH(unstETHIds); + escrow.withdrawETH(unstETHIds); vm.stopPrank(); } @@ -440,8 +452,10 @@ contract EscrowHappyPath is TestHelpers { assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, firstVetoerStETHShares, 1); _lockWstETH(_VETOER_1, firstVetoerWstETHAmount); - assertEq(escrow.getVetoerState(_VETOER_1).wstETHShares, firstVetoerWstETHAmount); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); + assertApproxEqAbs( + escrow.getVetoerState(_VETOER_1).stETHShares, firstVetoerWstETHAmount + firstVetoerStETHShares, 2 + ); + assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 2); _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); @@ -449,19 +463,19 @@ contract EscrowHappyPath is TestHelpers { stETHWithdrawalRequestAmounts[0] = firstVetoerStETHAmount; vm.prank(_VETOER_1); - uint256[] memory stETHWithdrawalRequestIds = escrow.requestWithdrawalsStETH(stETHWithdrawalRequestAmounts); + uint256[] memory stETHWithdrawalRequestIds = escrow.requestWithdrawals(stETHWithdrawalRequestAmounts); assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, firstVetoerStETHShares, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); + assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 2); uint256[] memory wstETHWithdrawalRequestAmounts = new uint256[](1); - wstETHWithdrawalRequestAmounts[0] = firstVetoerWstETHAmount; + wstETHWithdrawalRequestAmounts[0] = _ST_ETH.getPooledEthByShares(firstVetoerWstETHAmount); vm.prank(_VETOER_1); - uint256[] memory wstETHWithdrawalRequestIds = escrow.requestWithdrawalsWstETH(wstETHWithdrawalRequestAmounts); + uint256[] memory wstETHWithdrawalRequestIds = escrow.requestWithdrawals(wstETHWithdrawalRequestAmounts); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); + assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 2); finalizeWQ(wstETHWithdrawalRequestIds[0]); @@ -471,8 +485,8 @@ contract EscrowHappyPath is TestHelpers { stETHWithdrawalRequestIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex() ) ); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); + assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 2); escrow.markUnstETHFinalized( wstETHWithdrawalRequestIds, @@ -480,8 +494,8 @@ contract EscrowHappyPath is TestHelpers { wstETHWithdrawalRequestIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex() ) ); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); + assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 2); _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index fabbb898..629551ff 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -26,7 +26,14 @@ import {DualGovernance, DualGovernanceState, State} from "contracts/DualGovernan import {Proposal, Status as ProposalStatus} from "contracts/libraries/Proposals.sol"; import {Percents, percents} from "../utils/percents.sol"; -import {IERC20, IStEth, IWstETH, IWithdrawalQueue, WithdrawalRequestStatus, IDangerousContract} from "../utils/interfaces.sol"; +import { + IERC20, + IStEth, + IWstETH, + IWithdrawalQueue, + WithdrawalRequestStatus, + IDangerousContract +} from "../utils/interfaces.sol"; import {ExecutorCallHelpers} from "../utils/executor-calls.sol"; import {Utils, TargetMock, console} from "../utils/utils.sol"; @@ -184,14 +191,16 @@ contract ScenarioTestBlueprint is Test { function _unlockWstETH(address vetoer) internal { Escrow escrow = _getVetoSignallingEscrow(); uint256 wstETHBalanceBefore = _WST_ETH.balanceOf(vetoer); - uint256 vetoerWstETHSharesBefore = escrow.getVetoerState(vetoer).wstETHShares; + uint256 vetoerWstETHSharesBefore = escrow.getVetoerState(vetoer).stETHShares; vm.startPrank(vetoer); uint256 wstETHUnlocked = escrow.unlockWstETH(); vm.stopPrank(); - assertEq(wstETHUnlocked, vetoerWstETHSharesBefore); - assertEq(_WST_ETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerWstETHSharesBefore); + // 1 wei rounding issue may arise because of the wrapping stETH into wstETH before + // sending funds to the user + assertApproxEqAbs(wstETHUnlocked, vetoerWstETHSharesBefore, 1); + assertApproxEqAbs(_WST_ETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerWstETHSharesBefore, 1); } function _lockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { From ff2eefc09cc11320c9c69027b8ebcd1b44d51c54 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Mon, 24 Jun 2024 14:39:35 +0300 Subject: [PATCH 107/134] 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 108/134] 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 3e95f3e0f583dcb6e9b0337fe7257eb5c6a5f342 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 25 Jun 2024 13:08:14 +0400 Subject: [PATCH 109/134] Use custom types for timestamps and durations --- contracts/Configuration.sol | 29 +-- contracts/DualGovernance.sol | 10 +- contracts/EmergencyProtectedTimelock.sol | 11 +- contracts/Escrow.sol | 26 ++- contracts/interfaces/IConfiguration.sol | 46 ++-- contracts/interfaces/IEscrow.sol | 4 +- contracts/interfaces/ITimelock.sol | 3 +- contracts/libraries/AssetsAccounting.sol | 25 ++- contracts/libraries/DualGovernanceState.sol | 78 ++++--- contracts/libraries/EmergencyProtection.sol | 68 +++--- contracts/libraries/Proposals.sol | 51 ++--- contracts/types/Duration.sol | 121 +++++++++++ contracts/types/Timestamp.sol | 84 ++++++++ contracts/utils/time.sol | 18 -- test/scenario/agent-timelock.t.sol | 6 +- test/scenario/escrow.t.sol | 29 +-- test/scenario/gate-seal-breaker.t.sol | 38 ++-- test/scenario/gov-state-transitions.t.sol | 26 +-- test/scenario/happy-path-plan-b.t.sol | 54 ++--- test/scenario/happy-path.t.sol | 8 +- .../last-moment-malicious-proposal.t.sol | 39 ++-- test/scenario/veto-cooldown-mechanics.t.sol | 4 +- test/unit/EmergencyProtectedTimelock.t.sol | 70 +++--- test/unit/libraries/EmergencyProtection.t.sol | 200 +++++++++++------- test/unit/libraries/Proposals.t.sol | 144 ++++++------- test/unit/mocks/TimelockMock.sol | 4 +- test/utils/scenario-test-blueprint.sol | 70 ++++-- test/utils/unit-test.sol | 19 +- 28 files changed, 804 insertions(+), 481 deletions(-) create mode 100644 contracts/types/Duration.sol create mode 100644 contracts/types/Timestamp.sol delete mode 100644 contracts/utils/time.sol diff --git a/contracts/Configuration.sol b/contracts/Configuration.sol index 44cfc9bd..aae54c51 100644 --- a/contracts/Configuration.sol +++ b/contracts/Configuration.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Durations, Duration} from "./types/Duration.sol"; import {IConfiguration, DualGovernanceConfig} from "./interfaces/IConfiguration.sol"; uint256 constant PERCENT = 10 ** 16; @@ -14,17 +15,17 @@ contract Configuration is IConfiguration { uint256 public immutable FIRST_SEAL_RAGE_QUIT_SUPPORT = 3 * PERCENT; uint256 public immutable SECOND_SEAL_RAGE_QUIT_SUPPORT = 15 * PERCENT; - uint256 public immutable DYNAMIC_TIMELOCK_MIN_DURATION = 3 days; - uint256 public immutable DYNAMIC_TIMELOCK_MAX_DURATION = 30 days; + Duration public immutable DYNAMIC_TIMELOCK_MIN_DURATION = Durations.from(3 days); + Duration public immutable DYNAMIC_TIMELOCK_MAX_DURATION = Durations.from(30 days); - uint256 public immutable VETO_SIGNALLING_MIN_ACTIVE_DURATION = 5 hours; - uint256 public immutable VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = 5 days; - uint256 public immutable RAGE_QUIT_ACCUMULATION_MAX_DURATION = 3 days; + Duration public immutable VETO_SIGNALLING_MIN_ACTIVE_DURATION = Durations.from(5 hours); + Duration public immutable VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = Durations.from(5 days); + Duration public immutable RAGE_QUIT_ACCUMULATION_MAX_DURATION = Durations.from(3 days); - uint256 public immutable VETO_COOLDOWN_DURATION = 4 days; + Duration public immutable VETO_COOLDOWN_DURATION = Durations.from(4 days); - uint256 public immutable RAGE_QUIT_EXTENSION_DELAY = 7 days; - uint256 public immutable RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK = 60 days; + Duration public immutable RAGE_QUIT_EXTENSION_DELAY = Durations.from(7 days); + Duration public immutable RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK = Durations.from(60 days); uint256 public immutable RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_START_SEQ_NUMBER = 2; uint256 public immutable RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_A = 0; @@ -35,12 +36,12 @@ contract Configuration is IConfiguration { address public immutable ADMIN_EXECUTOR; address public immutable EMERGENCY_GOVERNANCE; - uint256 public immutable AFTER_SUBMIT_DELAY = 3 days; - uint256 public immutable AFTER_SCHEDULE_DELAY = 2 days; + Duration public immutable AFTER_SUBMIT_DELAY = Durations.from(3 days); + Duration public immutable AFTER_SCHEDULE_DELAY = Durations.from(2 days); - uint256 public immutable SIGNALLING_ESCROW_MIN_LOCK_TIME = 5 hours; + Duration public immutable SIGNALLING_ESCROW_MIN_LOCK_TIME = Durations.from(5 hours); - uint256 public immutable TIE_BREAK_ACTIVATION_TIMEOUT = 365 days; + Duration public immutable TIE_BREAK_ACTIVATION_TIMEOUT = Durations.from(365 days); // Sealables Array Representation uint256 private immutable MAX_SELABLES_COUNT = 5; @@ -84,8 +85,8 @@ contract Configuration is IConfiguration { returns ( uint256 firstSealRageQuitSupport, uint256 secondSealRageQuitSupport, - uint256 dynamicTimelockMinDuration, - uint256 dynamicTimelockMaxDuration + Duration dynamicTimelockMinDuration, + Duration dynamicTimelockMaxDuration ) { firstSealRageQuitSupport = FIRST_SEAL_RAGE_QUIT_SUPPORT; diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 3a6c7a5a..7e82e1d6 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; import {ITimelock, IGovernance} from "./interfaces/ITimelock.sol"; import {ConfigurationProvider} from "./ConfigurationProvider.sol"; @@ -49,7 +51,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function scheduleProposal(uint256 proposalId) external { _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); - uint256 proposalSubmissionTime = TIMELOCK.schedule(proposalId); + Timestamp proposalSubmissionTime = TIMELOCK.schedule(proposalId); _dgState.checkCanScheduleProposal(proposalSubmissionTime); emit ProposalScheduled(proposalId); } @@ -86,7 +88,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function getVetoSignallingState() external view - returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) + returns (bool isActive, Duration duration, Timestamp activatedAt, Timestamp enteredAt) { (isActive, duration, activatedAt, enteredAt) = _dgState.getVetoSignallingState(CONFIG.getDualGovernanceConfig()); } @@ -94,12 +96,12 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function getVetoSignallingDeactivationState() external view - returns (bool isActive, uint256 duration, uint256 enteredAt) + returns (bool isActive, Duration duration, Timestamp enteredAt) { (isActive, duration, enteredAt) = _dgState.getVetoSignallingDeactivationState(CONFIG.getDualGovernanceConfig()); } - function getVetoSignallingDuration() external view returns (uint256) { + function getVetoSignallingDuration() external view returns (Duration) { return _dgState.getVetoSignallingDuration(CONFIG.getDualGovernanceConfig()); } diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 019feb72..1069fea3 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; + import {IOwnable} from "./interfaces/IOwnable.sol"; import {ITimelock} from "./interfaces/ITimelock.sol"; @@ -30,7 +33,7 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { newProposalId = _proposals.submit(executor, calls); } - function schedule(uint256 proposalId) external returns (uint256 submittedAt) { + function schedule(uint256 proposalId) external returns (Timestamp submittedAt) { _checkGovernance(msg.sender); submittedAt = _proposals.schedule(proposalId, CONFIG.AFTER_SUBMIT_DELAY()); } @@ -68,7 +71,7 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { function emergencyExecute(uint256 proposalId) external { _emergencyProtection.checkEmergencyModeActive(true); _emergencyProtection.checkExecutionCommittee(msg.sender); - _proposals.execute(proposalId, /* afterScheduleDelay */ 0); + _proposals.execute(proposalId, /* afterScheduleDelay */ Duration.wrap(0)); } function deactivateEmergencyMode() external { @@ -91,8 +94,8 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { function setEmergencyProtection( address activator, address enactor, - uint256 protectionDuration, - uint256 emergencyModeDuration + Duration protectionDuration, + Duration emergencyModeDuration ) external { _checkAdminExecutor(msg.sender); _emergencyProtection.setup(activator, enactor, protectionDuration, emergencyModeDuration); diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 2488acb5..5075598f 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -4,6 +4,9 @@ pragma solidity 0.8.23; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Duration} from "./types/Duration.sol"; +import {Timestamp, Timestamps} from "./types/Timestamp.sol"; + import {IEscrow} from "./interfaces/IEscrow.sol"; import {IConfiguration} from "./interfaces/IConfiguration.sol"; @@ -59,9 +62,9 @@ contract Escrow is IEscrow { uint256[] internal _withdrawalUnstETHIds; - uint256 internal _rageQuitExtraTimelock; - uint256 internal _rageQuitWithdrawalsTimelock; - uint256 internal _rageQuitTimelockStartedAt; + Duration internal _rageQuitExtraTimelock; + Duration internal _rageQuitWithdrawalsTimelock; + Timestamp internal _rageQuitTimelockStartedAt; constructor(address stETH, address wstETH, address withdrawalQueue, address config) { ST_ETH = IStETH(stETH); @@ -170,7 +173,7 @@ contract Escrow is IEscrow { // State Updates // --- - function startRageQuit(uint256 rageQuitExtraTimelock, uint256 rageQuitWithdrawalsTimelock) external { + function startRageQuit(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock) external { _checkDualGovernance(msg.sender); _checkEscrowState(EscrowState.SignallingEscrow); @@ -210,7 +213,7 @@ contract Escrow is IEscrow { _accounting.accountClaimedETH(ethAmountClaimed); } if (_accounting.getIsWithdrawalsClaimed()) { - _rageQuitTimelockStartedAt = block.timestamp; + _rageQuitTimelockStartedAt = Timestamps.now(); } } @@ -284,7 +287,7 @@ contract Escrow is IEscrow { return _accounting.getIsWithdrawalsClaimed(); } - function getRageQuitTimelockStartedAt() external view returns (uint256) { + function getRageQuitTimelockStartedAt() external view returns (Timestamp) { return _rageQuitTimelockStartedAt; } @@ -295,8 +298,10 @@ contract Escrow is IEscrow { } function isRageQuitFinalized() external view returns (bool) { - return _escrowState == EscrowState.RageQuitEscrow && _accounting.getIsWithdrawalsClaimed() - && _rageQuitTimelockStartedAt != 0 && block.timestamp > _rageQuitTimelockStartedAt + _rageQuitExtraTimelock; + if (_escrowState != EscrowState.RageQuitEscrow) return false; + if (!_accounting.getIsWithdrawalsClaimed()) return false; + if (_rageQuitTimelockStartedAt.isZero()) return false; + return Timestamps.now() > _rageQuitExtraTimelock.addTo(_rageQuitTimelockStartedAt); } // --- @@ -330,10 +335,11 @@ contract Escrow is IEscrow { } function _checkWithdrawalsTimelockPassed() internal view { - if (_rageQuitTimelockStartedAt == 0) { + if (_rageQuitTimelockStartedAt.isZero()) { revert RageQuitExtraTimelockNotStarted(); } - if (block.timestamp <= _rageQuitTimelockStartedAt + _rageQuitExtraTimelock + _rageQuitWithdrawalsTimelock) { + Duration withdrawalsTimelock = _rageQuitExtraTimelock + _rageQuitWithdrawalsTimelock; + if (Timestamps.now() <= withdrawalsTimelock.addTo(_rageQuitTimelockStartedAt)) { revert WithdrawalsTimelockNotPassed(); } } diff --git a/contracts/interfaces/IConfiguration.sol b/contracts/interfaces/IConfiguration.sol index 04aa9f14..26826dd5 100644 --- a/contracts/interfaces/IConfiguration.sol +++ b/contracts/interfaces/IConfiguration.sol @@ -1,18 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Duration} from "../types/Duration.sol"; + struct DualGovernanceConfig { uint256 firstSealRageQuitSupport; uint256 secondSealRageQuitSupport; // TODO: consider dynamicDelayMaxDuration - uint256 dynamicTimelockMaxDuration; - uint256 dynamicTimelockMinDuration; - uint256 vetoSignallingMinActiveDuration; - uint256 vetoSignallingDeactivationMaxDuration; - uint256 vetoCooldownDuration; - uint256 rageQuitExtraTimelock; - uint256 rageQuitExtensionDelay; - uint256 rageQuitEthClaimMinTimelock; + Duration dynamicTimelockMaxDuration; + Duration dynamicTimelockMinDuration; + Duration vetoSignallingMinActiveDuration; + Duration vetoSignallingDeactivationMaxDuration; + Duration vetoCooldownDuration; + Duration rageQuitExtraTimelock; + Duration rageQuitExtensionDelay; + Duration rageQuitEthClaimMinTimelock; uint256 rageQuitEthClaimTimelockGrowthStartSeqNumber; uint256[3] rageQuitEthClaimTimelockGrowthCoeffs; } @@ -22,35 +24,35 @@ interface IAdminExecutorConfiguration { } interface ITimelockConfiguration { - function AFTER_SUBMIT_DELAY() external view returns (uint256); - function AFTER_SCHEDULE_DELAY() external view returns (uint256); + function AFTER_SUBMIT_DELAY() external view returns (Duration); + function AFTER_SCHEDULE_DELAY() external view returns (Duration); function EMERGENCY_GOVERNANCE() external view returns (address); } interface IDualGovernanceConfiguration { - function TIE_BREAK_ACTIVATION_TIMEOUT() external view returns (uint256); + function TIE_BREAK_ACTIVATION_TIMEOUT() external view returns (Duration); - function VETO_COOLDOWN_DURATION() external view returns (uint256); - function VETO_SIGNALLING_MIN_ACTIVE_DURATION() external view returns (uint256); + function VETO_COOLDOWN_DURATION() external view returns (Duration); + function VETO_SIGNALLING_MIN_ACTIVE_DURATION() external view returns (Duration); - function VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() external view returns (uint256); + function VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() external view returns (Duration); - function DYNAMIC_TIMELOCK_MIN_DURATION() external view returns (uint256); - function DYNAMIC_TIMELOCK_MAX_DURATION() external view returns (uint256); + function DYNAMIC_TIMELOCK_MIN_DURATION() external view returns (Duration); + function DYNAMIC_TIMELOCK_MAX_DURATION() external view returns (Duration); function FIRST_SEAL_RAGE_QUIT_SUPPORT() external view returns (uint256); function SECOND_SEAL_RAGE_QUIT_SUPPORT() external view returns (uint256); - function RAGE_QUIT_EXTENSION_DELAY() external view returns (uint256); - function RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK() external view returns (uint256); - function RAGE_QUIT_ACCUMULATION_MAX_DURATION() external view returns (uint256); + function RAGE_QUIT_EXTENSION_DELAY() external view returns (Duration); + function RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK() external view returns (Duration); + function RAGE_QUIT_ACCUMULATION_MAX_DURATION() external view returns (Duration); function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_START_SEQ_NUMBER() external view returns (uint256); function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_A() external view returns (uint256); function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_B() external view returns (uint256); function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_C() external view returns (uint256); - function SIGNALLING_ESCROW_MIN_LOCK_TIME() external view returns (uint256); + function SIGNALLING_ESCROW_MIN_LOCK_TIME() external view returns (Duration); function sealableWithdrawalBlockers() external view returns (address[] memory); @@ -60,8 +62,8 @@ interface IDualGovernanceConfiguration { returns ( uint256 firstSealThreshold, uint256 secondSealThreshold, - uint256 signallingMinDuration, - uint256 signallingMaxDuration + Duration signallingMinDuration, + Duration signallingMaxDuration ); function getDualGovernanceConfig() external view returns (DualGovernanceConfig memory config); diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol index d8c44087..3586d311 100644 --- a/contracts/interfaces/IEscrow.sol +++ b/contracts/interfaces/IEscrow.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Duration} from "../types/Duration.sol"; + interface IEscrow { function initialize(address dualGovernance) external; - function startRageQuit(uint256 rageQuitExtraTimelock, uint256 rageQuitWithdrawalsTimelock) external; + function startRageQuit(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock) external; function MASTER_COPY() external view returns (address); function isRageQuitFinalized() external view returns (bool); diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index 0f080e8d..9ca71745 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Timestamp} from "../types/Timestamp.sol"; import {ExecutorCall} from "./IExecutor.sol"; interface IGovernance { @@ -13,7 +14,7 @@ interface IGovernance { interface ITimelock { function submit(address executor, ExecutorCall[] calldata calls) external returns (uint256 newProposalId); - function schedule(uint256 proposalId) external returns (uint256 submittedAt); + function schedule(uint256 proposalId) external returns (Timestamp submittedAt); function execute(uint256 proposalId) external; function cancelAllNonExecutedProposals() external; diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index afee69ec..2a695620 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -5,7 +5,9 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {WithdrawalRequestStatus} from "../interfaces/IWithdrawalQueue.sol"; -import {TimeUtils} from "../utils/time.sol"; +import {Duration} from "../types/Duration.sol"; +import {Timestamps, Timestamp} from "../types/Timestamp.sol"; + import {ArrayUtils} from "../utils/arrays.sol"; enum WithdrawalRequestState { @@ -30,7 +32,7 @@ struct LockedAssetsStats { uint128 unstETHShares; uint128 sharesFinalized; uint128 amountFinalized; - uint40 lastAssetsLockTimestamp; + Timestamp lastAssetsLockTimestamp; } struct LockedAssetsTotals { @@ -82,7 +84,7 @@ library AssetsAccounting { error InvalidWithdrawlRequestState(uint256 id, WithdrawalRequestState actual, WithdrawalRequestState expected); error InvalidWithdrawalBatchesOffset(uint256 actual, uint256 expected); error InvalidWithdrawalBatchesCount(uint256 actual, uint256 expected); - error AssetsUnlockDelayNotPassed(uint256 unlockTimelockExpiresAt); + error AssetsUnlockDelayNotPassed(Timestamp unlockTimelockExpiresAt); error NotEnoughStETHToUnlock(uint256 requested, uint256 sharesBalance); struct State { @@ -103,7 +105,7 @@ library AssetsAccounting { _checkNonZeroSharesLock(vetoer, shares); uint128 sharesUint128 = shares.toUint128(); self.assets[vetoer].stETHShares += sharesUint128; - self.assets[vetoer].lastAssetsLockTimestamp = TimeUtils.timestamp(); + self.assets[vetoer].lastAssetsLockTimestamp = Timestamps.now(); self.totals.shares += sharesUint128; emit StETHLocked(vetoer, shares); } @@ -136,7 +138,7 @@ library AssetsAccounting { // wstETH Operations Accounting // --- - function checkAssetsUnlockDelayPassed(State storage self, address vetoer, uint256 delay) internal view { + function checkAssetsUnlockDelayPassed(State storage self, address vetoer, Duration delay) internal view { _checkAssetsUnlockDelayPassed(self, delay, vetoer); } @@ -144,7 +146,7 @@ library AssetsAccounting { _checkNonZeroSharesLock(vetoer, shares); uint128 sharesUint128 = shares.toUint128(); self.assets[vetoer].wstETHShares += sharesUint128; - self.assets[vetoer].lastAssetsLockTimestamp = TimeUtils.timestamp(); + self.assets[vetoer].lastAssetsLockTimestamp = Timestamps.now(); self.totals.shares += sharesUint128; emit WstETHLocked(vetoer, shares); } @@ -192,14 +194,14 @@ library AssetsAccounting { } uint128 totalUnstETHSharesLockedUint128 = totalUnstETHSharesLocked.toUint128(); self.assets[vetoer].unstETHShares += totalUnstETHSharesLockedUint128; - self.assets[vetoer].lastAssetsLockTimestamp = TimeUtils.timestamp(); + self.assets[vetoer].lastAssetsLockTimestamp = Timestamps.now(); self.totals.shares += totalUnstETHSharesLockedUint128; emit UnstETHLocked(vetoer, unstETHIds, totalUnstETHSharesLocked); } function accountUnstETHUnlock( State storage self, - uint256 assetsUnlockDelay, + Duration assetsUnlockDelay, address vetoer, uint256[] memory unstETHIds ) internal { @@ -551,11 +553,12 @@ library AssetsAccounting { function _checkAssetsUnlockDelayPassed( State storage self, - uint256 assetsUnlockDelay, + Duration assetsUnlockDelay, address vetoer ) private view { - if (block.timestamp <= self.assets[vetoer].lastAssetsLockTimestamp + assetsUnlockDelay) { - revert AssetsUnlockDelayNotPassed(self.assets[vetoer].lastAssetsLockTimestamp + assetsUnlockDelay); + Timestamp assetsUnlockAllowedAfter = assetsUnlockDelay.addTo(self.assets[vetoer].lastAssetsLockTimestamp); + if (Timestamps.now() <= assetsUnlockAllowedAfter) { + revert AssetsUnlockDelayNotPassed(assetsUnlockAllowedAfter); } } } diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol index b55499f4..6aa9255b 100644 --- a/contracts/libraries/DualGovernanceState.sol +++ b/contracts/libraries/DualGovernanceState.sol @@ -7,7 +7,8 @@ import {IEscrow} from "../interfaces/IEscrow.sol"; import {ISealable} from "../interfaces/ISealable.sol"; import {IDualGovernanceConfiguration as IConfiguration, DualGovernanceConfig} from "../interfaces/IConfiguration.sol"; -import {TimeUtils} from "../utils/time.sol"; +import {Duration, Durations} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; enum State { Normal, @@ -21,14 +22,14 @@ library DualGovernanceState { // TODO: Optimize storage layout efficiency struct Store { State state; - uint40 enteredAt; + Timestamp enteredAt; // the time the veto signalling state was entered - uint40 vetoSignallingActivationTime; - IEscrow signallingEscrow; + Timestamp vetoSignallingActivationTime; + IEscrow signallingEscrow; // 248 // the time the Deactivation sub-state was last exited without exiting the parent Veto Signalling state - uint40 vetoSignallingReactivationTime; + Timestamp vetoSignallingReactivationTime; // the last time a proposal was submitted to the DG subsystem - uint40 lastAdoptableStateExitedAt; + Timestamp lastAdoptableStateExitedAt; IEscrow rageQuitEscrow; uint8 rageQuitRound; } @@ -90,7 +91,7 @@ library DualGovernanceState { } } - function checkCanScheduleProposal(Store storage self, uint256 proposalSubmittedAt) internal view { + function checkCanScheduleProposal(Store storage self, Timestamp proposalSubmittedAt) internal view { if (!canScheduleProposal(self, proposalSubmittedAt)) { revert ProposalsAdoptionSuspended(); } @@ -106,7 +107,7 @@ library DualGovernanceState { return self.state; } - function canScheduleProposal(Store storage self, uint256 proposalSubmissionTime) internal view returns (bool) { + function canScheduleProposal(Store storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { State state = self.state; if (state == State.Normal) return true; if (state == State.VetoCooldown) { @@ -129,7 +130,9 @@ library DualGovernanceState { if (isProposalsAdoptionAllowed(self)) return false; // for the governance is locked for long period of time - if (block.timestamp - self.lastAdoptableStateExitedAt >= config.TIE_BREAK_ACTIVATION_TIMEOUT()) return true; + if (Timestamps.now() >= config.TIE_BREAK_ACTIVATION_TIMEOUT().addTo(self.lastAdoptableStateExitedAt)) { + return true; + } if (self.state != State.RageQuit) return false; @@ -143,17 +146,17 @@ library DualGovernanceState { function getVetoSignallingState( Store storage self, DualGovernanceConfig memory config - ) internal view returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) { + ) internal view returns (bool isActive, Duration duration, Timestamp activatedAt, Timestamp enteredAt) { isActive = self.state == State.VetoSignalling; - duration = isActive ? getVetoSignallingDuration(self, config) : 0; - enteredAt = isActive ? self.enteredAt : 0; - activatedAt = isActive ? self.vetoSignallingActivationTime : 0; + duration = isActive ? getVetoSignallingDuration(self, config) : Duration.wrap(0); + enteredAt = isActive ? self.enteredAt : Timestamps.ZERO; + activatedAt = isActive ? self.vetoSignallingActivationTime : Timestamps.ZERO; } function getVetoSignallingDuration( Store storage self, DualGovernanceConfig memory config - ) internal view returns (uint256) { + ) internal view returns (Duration) { uint256 totalSupport = self.signallingEscrow.getRageQuitSupport(); return _calcDynamicTimelockDuration(config, totalSupport); } @@ -166,10 +169,10 @@ library DualGovernanceState { function getVetoSignallingDeactivationState( Store storage self, DualGovernanceConfig memory config - ) internal view returns (bool isActive, uint256 duration, uint256 enteredAt) { + ) internal view returns (bool isActive, Duration duration, Timestamp enteredAt) { isActive = self.state == State.VetoSignallingDeactivation; duration = config.vetoSignallingDeactivationMaxDuration; - enteredAt = isActive ? self.enteredAt : 0; + enteredAt = isActive ? self.enteredAt : Timestamps.ZERO; } // --- @@ -253,7 +256,7 @@ library DualGovernanceState { State oldState, State newState ) private { - uint40 timestamp = TimeUtils.timestamp(); + Timestamp timestamp = Timestamps.now(); self.enteredAt = timestamp; // track the time when the governance state allowed execution if (oldState == State.Normal || oldState == State.VetoCooldown) { @@ -301,7 +304,7 @@ library DualGovernanceState { Store storage self, DualGovernanceConfig memory config ) private view returns (bool) { - return block.timestamp - self.vetoSignallingActivationTime > config.dynamicTimelockMaxDuration; + return Timestamps.now() > config.dynamicTimelockMaxDuration.addTo(self.vetoSignallingActivationTime); } function _isDynamicTimelockDurationPassed( @@ -309,29 +312,29 @@ library DualGovernanceState { DualGovernanceConfig memory config, uint256 rageQuitSupport ) private view returns (bool) { - uint256 vetoSignallingDurationPassed = block.timestamp - self.vetoSignallingActivationTime; - return vetoSignallingDurationPassed > _calcDynamicTimelockDuration(config, rageQuitSupport); + Duration dynamicTimelock = _calcDynamicTimelockDuration(config, rageQuitSupport); + return Timestamps.now() > dynamicTimelock.addTo(self.vetoSignallingActivationTime); } function _isVetoSignallingReactivationDurationPassed( Store storage self, DualGovernanceConfig memory config ) private view returns (bool) { - return block.timestamp - self.vetoSignallingReactivationTime > config.vetoSignallingMinActiveDuration; + return Timestamps.now() > config.vetoSignallingMinActiveDuration.addTo(self.vetoSignallingReactivationTime); } function _isVetoSignallingDeactivationMaxDurationPassed( Store storage self, DualGovernanceConfig memory config ) private view returns (bool) { - return block.timestamp - self.enteredAt > config.vetoSignallingDeactivationMaxDuration; + return Timestamps.now() > config.vetoSignallingDeactivationMaxDuration.addTo(self.enteredAt); } function _isVetoCooldownDurationPassed( Store storage self, DualGovernanceConfig memory config ) private view returns (bool) { - return block.timestamp - self.enteredAt > config.vetoCooldownDuration; + return Timestamps.now() > config.vetoCooldownDuration.addTo(self.enteredAt); } function _deployNewSignallingEscrow(Store storage self, address escrowMasterCopy) private { @@ -344,29 +347,31 @@ library DualGovernanceState { function _calcRageQuitWithdrawalsTimelock( DualGovernanceConfig memory config, uint256 rageQuitRound - ) private pure returns (uint256) { + ) private pure returns (Duration) { if (rageQuitRound < config.rageQuitEthClaimTimelockGrowthStartSeqNumber) { return config.rageQuitEthClaimMinTimelock; } return config.rageQuitEthClaimMinTimelock - + ( - config.rageQuitEthClaimTimelockGrowthCoeffs[0] * rageQuitRound * rageQuitRound - + config.rageQuitEthClaimTimelockGrowthCoeffs[1] * rageQuitRound - + config.rageQuitEthClaimTimelockGrowthCoeffs[2] - ) / 10 ** 18; // TODO: rewrite in a prettier way + + Durations.from( + ( + config.rageQuitEthClaimTimelockGrowthCoeffs[0] * rageQuitRound * rageQuitRound + + config.rageQuitEthClaimTimelockGrowthCoeffs[1] * rageQuitRound + + config.rageQuitEthClaimTimelockGrowthCoeffs[2] + ) / 10 ** 18 + ); // TODO: rewrite in a prettier way } function _calcDynamicTimelockDuration( DualGovernanceConfig memory config, uint256 rageQuitSupport - ) internal pure returns (uint256 duration_) { + ) internal pure returns (Duration duration_) { uint256 firstSealRageQuitSupport = config.firstSealRageQuitSupport; uint256 secondSealRageQuitSupport = config.secondSealRageQuitSupport; - uint256 dynamicTimelockMinDuration = config.dynamicTimelockMinDuration; - uint256 dynamicTimelockMaxDuration = config.dynamicTimelockMaxDuration; + Duration dynamicTimelockMinDuration = config.dynamicTimelockMinDuration; + Duration dynamicTimelockMaxDuration = config.dynamicTimelockMaxDuration; if (rageQuitSupport < firstSealRageQuitSupport) { - return 0; + return Durations.ZERO; } if (rageQuitSupport >= secondSealRageQuitSupport) { @@ -374,7 +379,10 @@ library DualGovernanceState { } duration_ = dynamicTimelockMinDuration - + (rageQuitSupport - firstSealRageQuitSupport) * (dynamicTimelockMaxDuration - dynamicTimelockMinDuration) - / (secondSealRageQuitSupport - firstSealRageQuitSupport); + + Durations.from( + (rageQuitSupport - firstSealRageQuitSupport) + * (dynamicTimelockMaxDuration - dynamicTimelockMinDuration).toSeconds() + / (secondSealRageQuitSupport - firstSealRageQuitSupport) + ); } } diff --git a/contracts/libraries/EmergencyProtection.sol b/contracts/libraries/EmergencyProtection.sol index bbbee390..f52bb4f3 100644 --- a/contracts/libraries/EmergencyProtection.sol +++ b/contracts/libraries/EmergencyProtection.sol @@ -1,37 +1,38 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {Duration, Durations} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; struct EmergencyState { address executionCommittee; address activationCommittee; - uint256 protectedTill; + Timestamp protectedTill; bool isEmergencyModeActivated; - uint256 emergencyModeDuration; - uint256 emergencyModeEndsAfter; + Duration emergencyModeDuration; + Timestamp emergencyModeEndsAfter; } library EmergencyProtection { error NotEmergencyActivator(address account); error NotEmergencyEnactor(address account); - error EmergencyCommitteeExpired(uint256 timestamp, uint256 protectedTill); + error EmergencyCommitteeExpired(Timestamp timestamp, Timestamp protectedTill); error InvalidEmergencyModeActiveValue(bool actual, bool expected); - event EmergencyModeActivated(uint256 timestamp); - event EmergencyModeDeactivated(uint256 timestamp); + event EmergencyModeActivated(Timestamp timestamp); + event EmergencyModeDeactivated(Timestamp timestamp); event EmergencyActivationCommitteeSet(address indexed activationCommittee); event EmergencyExecutionCommitteeSet(address indexed executionCommittee); - event EmergencyModeDurationSet(uint256 emergencyModeDuration); - event EmergencyCommitteeProtectedTillSet(uint256 protectedTill); + event EmergencyModeDurationSet(Duration emergencyModeDuration); + event EmergencyCommitteeProtectedTillSet(Timestamp newProtectedTill); struct State { // has rights to activate emergency mode address activationCommittee; - uint40 protectedTill; + Timestamp protectedTill; // till this time, the committee may activate the emergency mode - uint40 emergencyModeEndsAfter; - uint32 emergencyModeDuration; + Timestamp emergencyModeEndsAfter; + Duration emergencyModeDuration; // has rights to execute proposals in emergency mode address executionCommittee; } @@ -40,8 +41,8 @@ library EmergencyProtection { State storage self, address activationCommittee, address executionCommittee, - uint256 protectionDuration, - uint256 emergencyModeDuration + Duration protectionDuration, + Duration emergencyModeDuration ) internal { address prevActivationCommittee = self.activationCommittee; if (activationCommittee != prevActivationCommittee) { @@ -55,36 +56,37 @@ library EmergencyProtection { emit EmergencyExecutionCommitteeSet(executionCommittee); } - uint256 prevProtectedTill = self.protectedTill; - uint256 protectedTill = block.timestamp + protectionDuration; + Timestamp prevProtectedTill = self.protectedTill; + Timestamp newProtectedTill = protectionDuration.addTo(Timestamps.now()); - if (protectedTill != prevProtectedTill) { - self.protectedTill = SafeCast.toUint40(protectedTill); - emit EmergencyCommitteeProtectedTillSet(protectedTill); + if (newProtectedTill != prevProtectedTill) { + self.protectedTill = newProtectedTill; + emit EmergencyCommitteeProtectedTillSet(newProtectedTill); } - uint256 prevEmergencyModeDuration = self.emergencyModeDuration; + Duration prevEmergencyModeDuration = self.emergencyModeDuration; if (emergencyModeDuration != prevEmergencyModeDuration) { - self.emergencyModeDuration = SafeCast.toUint32(emergencyModeDuration); + self.emergencyModeDuration = emergencyModeDuration; emit EmergencyModeDurationSet(emergencyModeDuration); } } function activate(State storage self) internal { - if (block.timestamp > self.protectedTill) { - revert EmergencyCommitteeExpired(block.timestamp, self.protectedTill); + Timestamp timestamp = Timestamps.now(); + if (timestamp > self.protectedTill) { + revert EmergencyCommitteeExpired(timestamp, self.protectedTill); } - self.emergencyModeEndsAfter = SafeCast.toUint40(block.timestamp + self.emergencyModeDuration); - emit EmergencyModeActivated(block.timestamp); + self.emergencyModeEndsAfter = self.emergencyModeDuration.addTo(timestamp); + emit EmergencyModeActivated(timestamp); } function deactivate(State storage self) internal { self.activationCommittee = address(0); self.executionCommittee = address(0); - self.protectedTill = 0; - self.emergencyModeDuration = 0; - self.emergencyModeEndsAfter = 0; - emit EmergencyModeDeactivated(block.timestamp); + self.protectedTill = Timestamps.ZERO; + self.emergencyModeEndsAfter = Timestamps.ZERO; + self.emergencyModeDuration = Durations.ZERO; + emit EmergencyModeDeactivated(Timestamps.now()); } function getEmergencyState(State storage self) internal view returns (EmergencyState memory res) { @@ -97,16 +99,16 @@ library EmergencyProtection { } function isEmergencyModeActivated(State storage self) internal view returns (bool) { - return self.emergencyModeEndsAfter != 0; + return self.emergencyModeEndsAfter.isNotZero(); } function isEmergencyModePassed(State storage self) internal view returns (bool) { - uint256 endsAfter = self.emergencyModeEndsAfter; - return endsAfter != 0 && block.timestamp > endsAfter; + Timestamp endsAfter = self.emergencyModeEndsAfter; + return endsAfter.isNotZero() && Timestamps.now() > endsAfter; } function isEmergencyProtectionEnabled(State storage self) internal view returns (bool) { - return block.timestamp <= self.protectedTill || self.emergencyModeEndsAfter != 0; + return Timestamps.now() <= self.protectedTill || self.emergencyModeEndsAfter.isNotZero(); } function checkActivationCommittee(State storage self, address account) internal view { diff --git a/contracts/libraries/Proposals.sol b/contracts/libraries/Proposals.sol index e404a93f..3771c183 100644 --- a/contracts/libraries/Proposals.sol +++ b/contracts/libraries/Proposals.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {IExecutor, ExecutorCall} from "../interfaces/IExecutor.sol"; +import {Duration} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; -import {TimeUtils} from "../utils/time.sol"; +import {IExecutor, ExecutorCall} from "../interfaces/IExecutor.sol"; enum Status { NotExist, @@ -17,18 +18,18 @@ struct Proposal { uint256 id; Status status; address executor; - uint256 submittedAt; - uint256 scheduledAt; - uint256 executedAt; + Timestamp submittedAt; + Timestamp scheduledAt; + Timestamp executedAt; ExecutorCall[] calls; } library Proposals { struct ProposalPacked { address executor; - uint40 submittedAt; - uint40 scheduledAt; - uint40 executedAt; + Timestamp submittedAt; + Timestamp scheduledAt; + Timestamp executedAt; ExecutorCall[] calls; } @@ -69,7 +70,7 @@ library Proposals { ProposalPacked storage newProposal = self.proposals[newProposalIndex]; newProposal.executor = executor; - newProposal.submittedAt = TimeUtils.timestamp(); + newProposal.submittedAt = Timestamps.now(); // copying of arrays of custom types from calldata to storage has not been supported by the // Solidity compiler yet, so insert item by item @@ -84,19 +85,19 @@ library Proposals { function schedule( State storage self, uint256 proposalId, - uint256 afterSubmitDelay - ) internal returns (uint256 submittedAt) { + Duration afterSubmitDelay + ) internal returns (Timestamp submittedAt) { _checkProposalSubmitted(self, proposalId); _checkAfterSubmitDelayPassed(self, proposalId, afterSubmitDelay); ProposalPacked storage proposal = _packed(self, proposalId); submittedAt = proposal.submittedAt; - proposal.scheduledAt = TimeUtils.timestamp(); + proposal.scheduledAt = Timestamps.now(); emit ProposalScheduled(proposalId); } - function execute(State storage self, uint256 proposalId, uint256 afterScheduleDelay) internal { + function execute(State storage self, uint256 proposalId, Duration afterScheduleDelay) internal { _checkProposalScheduled(self, proposalId); _checkAfterScheduleDelayPassed(self, proposalId, afterScheduleDelay); _executeProposal(self, proposalId); @@ -128,24 +129,24 @@ library Proposals { function canExecute( State storage self, uint256 proposalId, - uint256 afterScheduleDelay + Duration afterScheduleDelay ) internal view returns (bool) { return _getProposalStatus(self, proposalId) == Status.Scheduled - && block.timestamp >= _packed(self, proposalId).scheduledAt + afterScheduleDelay; + && Timestamps.now() >= afterScheduleDelay.addTo(_packed(self, proposalId).scheduledAt); } function canSchedule( State storage self, uint256 proposalId, - uint256 afterSubmitDelay + Duration afterSubmitDelay ) internal view returns (bool) { return _getProposalStatus(self, proposalId) == Status.Submitted - && block.timestamp >= _packed(self, proposalId).submittedAt + afterSubmitDelay; + && Timestamps.now() >= afterSubmitDelay.addTo(_packed(self, proposalId).submittedAt); } function _executeProposal(State storage self, uint256 proposalId) private { ProposalPacked storage packed = _packed(self, proposalId); - packed.executedAt = TimeUtils.timestamp(); + packed.executedAt = Timestamps.now(); ExecutorCall[] memory calls = packed.calls; uint256 callsCount = calls.length; @@ -187,9 +188,9 @@ library Proposals { function _checkAfterSubmitDelayPassed( State storage self, uint256 proposalId, - uint256 afterSubmitDelay + Duration afterSubmitDelay ) private view { - if (block.timestamp < _packed(self, proposalId).submittedAt + afterSubmitDelay) { + if (Timestamps.now() < afterSubmitDelay.addTo(_packed(self, proposalId).submittedAt)) { revert AfterSubmitDelayNotPassed(proposalId); } } @@ -197,9 +198,9 @@ library Proposals { function _checkAfterScheduleDelayPassed( State storage self, uint256 proposalId, - uint256 afterScheduleDelay + Duration afterScheduleDelay ) private view { - if (block.timestamp < _packed(self, proposalId).scheduledAt + afterScheduleDelay) { + if (Timestamps.now() < afterScheduleDelay.addTo(_packed(self, proposalId).scheduledAt)) { revert AfterScheduleDelayNotPassed(proposalId); } } @@ -209,10 +210,10 @@ library Proposals { ProposalPacked storage packed = _packed(self, proposalId); - if (packed.executedAt != 0) return Status.Executed; + if (packed.executedAt.isNotZero()) return Status.Executed; if (proposalId <= self.lastCancelledProposalId) return Status.Cancelled; - if (packed.scheduledAt != 0) return Status.Scheduled; - if (packed.submittedAt != 0) return Status.Submitted; + if (packed.scheduledAt.isNotZero()) return Status.Scheduled; + if (packed.submittedAt.isNotZero()) return Status.Submitted; assert(false); } } diff --git a/contracts/types/Duration.sol b/contracts/types/Duration.sol new file mode 100644 index 00000000..0a599061 --- /dev/null +++ b/contracts/types/Duration.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Timestamp, Timestamps} from "./Timestamp.sol"; + +type Duration is uint32; + +error DurationOverflow(); +error DurationUnderflow(); + +// the max possible duration is ~ 106 years +uint256 constant MAX_VALUE = type(uint32).max; + +using {lt as <} for Duration global; +using {lte as <=} for Duration global; +using {gt as >} for Duration global; +using {eq as ==} for Duration global; +using {notEq as !=} for Duration global; + +using {plus as +} for Duration global; +using {minus as -} for Duration global; + +using {addTo} for Duration global; +using {plusSeconds} for Duration global; +using {minusSeconds} for Duration global; +using {multipliedBy} for Duration global; +using {dividedBy} for Duration global; +using {toSeconds} for Duration global; + +// --- +// Comparison Ops +// --- + +function lt(Duration d1, Duration d2) pure returns (bool) { + return Duration.unwrap(d1) < Duration.unwrap(d2); +} + +function lte(Duration d1, Duration d2) pure returns (bool) { + return Duration.unwrap(d1) <= Duration.unwrap(d2); +} + +function gt(Duration d1, Duration d2) pure returns (bool) { + return Duration.unwrap(d1) > Duration.unwrap(d2); +} + +function eq(Duration d1, Duration d2) pure returns (bool) { + return Duration.unwrap(d1) == Duration.unwrap(d2); +} + +function notEq(Duration d1, Duration d2) pure returns (bool) { + return !(d1 == d2); +} + +// --- +// Arithmetic Operations +// --- + +function plus(Duration d1, Duration d2) pure returns (Duration) { + return toDuration(Duration.unwrap(d1) + Duration.unwrap(d2)); +} + +function minus(Duration d1, Duration d2) pure returns (Duration) { + if (d1 < d2) { + revert DurationUnderflow(); + } + return Duration.wrap(Duration.unwrap(d1) - Duration.unwrap(d2)); +} + +function plusSeconds(Duration d, uint256 seconds_) pure returns (Duration) { + return toDuration(Duration.unwrap(d) + seconds_); +} + +function minusSeconds(Duration d, uint256 seconds_) pure returns (Duration) { + uint256 durationValue = Duration.unwrap(d); + if (durationValue < seconds_) { + revert DurationUnderflow(); + } + return Duration.wrap(uint32(durationValue - seconds_)); +} + +function dividedBy(Duration d, uint256 divisor) pure returns (Duration) { + return Duration.wrap(uint32(Duration.unwrap(d) / divisor)); +} + +function multipliedBy(Duration d, uint256 multiplicand) pure returns (Duration) { + return toDuration(Duration.unwrap(d) * multiplicand); +} + +function addTo(Duration d, Timestamp t) pure returns (Timestamp) { + return Timestamps.from(t.toSeconds() + d.toSeconds()); +} + +// --- +// Conversion Ops +// --- + +function toDuration(uint256 value) pure returns (Duration) { + if (value > MAX_VALUE) { + revert DurationOverflow(); + } + return Duration.wrap(uint32(value)); +} + +function toSeconds(Duration d) pure returns (uint256) { + return Duration.unwrap(d); +} + +library Durations { + Duration internal constant ZERO = Duration.wrap(0); + + Duration internal constant MIN = ZERO; + Duration internal constant MAX = Duration.wrap(uint32(MAX_VALUE)); + + function from(uint256 seconds_) internal pure returns (Duration res) { + res = toDuration(seconds_); + } + + function between(Timestamp t1, Timestamp t2) internal pure returns (Duration res) { + res = toDuration(t1.toSeconds() - t2.toSeconds()); + } +} diff --git a/contracts/types/Timestamp.sol b/contracts/types/Timestamp.sol new file mode 100644 index 00000000..ab06379f --- /dev/null +++ b/contracts/types/Timestamp.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +type Timestamp is uint40; + +error TimestampOverflow(); +error TimestampUnderflow(); + +uint256 constant MAX_TIMESTAMP_VALUE = type(uint40).max; + +using {lt as <} for Timestamp global; +using {gt as >} for Timestamp global; +using {gte as >=} for Timestamp global; +using {lte as <=} for Timestamp global; +using {eq as ==} for Timestamp global; +using {notEq as !=} for Timestamp global; + +using {isZero} for Timestamp global; +using {isNotZero} for Timestamp global; +using {toSeconds} for Timestamp global; + +// --- +// Comparison Ops +// --- + +function lt(Timestamp t1, Timestamp t2) pure returns (bool) { + return Timestamp.unwrap(t1) < Timestamp.unwrap(t2); +} + +function gt(Timestamp t1, Timestamp t2) pure returns (bool) { + return Timestamp.unwrap(t1) > Timestamp.unwrap(t2); +} + +function gte(Timestamp t1, Timestamp t2) pure returns (bool) { + return Timestamp.unwrap(t1) >= Timestamp.unwrap(t2); +} + +function lte(Timestamp t1, Timestamp t2) pure returns (bool) { + return Timestamp.unwrap(t1) <= Timestamp.unwrap(t2); +} + +function eq(Timestamp t1, Timestamp t2) pure returns (bool) { + return Timestamp.unwrap(t1) == Timestamp.unwrap(t2); +} + +function notEq(Timestamp t1, Timestamp t2) pure returns (bool) { + return !(t1 == t2); +} + +function isZero(Timestamp t) pure returns (bool) { + return Timestamp.unwrap(t) == 0; +} + +function isNotZero(Timestamp t) pure returns (bool) { + return Timestamp.unwrap(t) != 0; +} + +// --- +// Conversion Ops +// --- + +function toSeconds(Timestamp t) pure returns (uint256) { + return Timestamp.unwrap(t); +} + +uint256 constant MAX_VALUE = type(uint40).max; + +library Timestamps { + Timestamp internal constant ZERO = Timestamp.wrap(0); + + Timestamp internal constant MIN = ZERO; + Timestamp internal constant MAX = Timestamp.wrap(uint40(MAX_TIMESTAMP_VALUE)); + + function now() internal view returns (Timestamp res) { + res = Timestamp.wrap(uint40(block.timestamp)); + } + + function from(uint256 value) internal pure returns (Timestamp res) { + if (value > MAX_TIMESTAMP_VALUE) { + revert TimestampOverflow(); + } + return Timestamp.wrap(uint40(value)); + } +} diff --git a/contracts/utils/time.sol b/contracts/utils/time.sol deleted file mode 100644 index 05bc93b3..00000000 --- a/contracts/utils/time.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -library TimeUtils { - function timestamp() internal view returns (uint40) { - return timestamp(block.timestamp); - } - - function timestamp(uint256 value) internal pure returns (uint40) { - return SafeCast.toUint40(value); - } - - function duration(uint256 value) internal pure returns (uint32) { - return SafeCast.toUint32(value); - } -} diff --git a/test/scenario/agent-timelock.t.sol b/test/scenario/agent-timelock.t.sol index f209a1e9..bfac8585 100644 --- a/test/scenario/agent-timelock.t.sol +++ b/test/scenario/agent-timelock.t.sol @@ -76,7 +76,7 @@ contract AgentTimelockTest is ScenarioTestBlueprint { // --- { // wait until the delay has passed - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() + 1); + _wait(_config.AFTER_SUBMIT_DELAY().plusSeconds(1)); // when the first delay is passed and the is no opposition from the stETH holders // the proposal can be scheduled @@ -95,7 +95,7 @@ contract AgentTimelockTest is ScenarioTestBlueprint { { // some time passes and emergency committee activates emergency mode // and resets the controller - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); // committee resets governance vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); @@ -105,7 +105,7 @@ contract AgentTimelockTest is ScenarioTestBlueprint { _timelock.emergencyReset(); // proposal is canceled now - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2 + 1); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2).plusSeconds(1)); // remove canceled call from the timelock _assertCanExecute(proposalId, false); diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 66a07544..99ca0fca 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -2,14 +2,15 @@ pragma solidity 0.8.23; import {WithdrawalRequestStatus} from "../utils/interfaces.sol"; - +import {Duration as DurationType} from "contracts/types/Duration.sol"; import { Escrow, Balances, VetoerState, LockedAssetsTotals, WITHDRAWAL_QUEUE, - ScenarioTestBlueprint + ScenarioTestBlueprint, + Durations } from "../utils/scenario-test-blueprint.sol"; contract TestHelpers is ScenarioTestBlueprint { @@ -47,8 +48,8 @@ contract TestHelpers is ScenarioTestBlueprint { contract EscrowHappyPath is TestHelpers { Escrow internal escrow; - uint256 internal immutable _RAGE_QUIT_EXTRA_TIMELOCK = 14 days; - uint256 internal immutable _RAGE_QUIT_WITHDRAWALS_TIMELOCK = 7 days; + DurationType internal immutable _RAGE_QUIT_EXTRA_TIMELOCK = Durations.from(14 days); + DurationType internal immutable _RAGE_QUIT_WITHDRAWALS_TIMELOCK = Durations.from(7 days); address internal immutable _VETOER_1 = makeAddr("VETOER_1"); address internal immutable _VETOER_2 = makeAddr("VETOER_2"); @@ -99,7 +100,7 @@ contract EscrowHappyPath is TestHelpers { _lockStETH(_VETOER_2, 3 * 10 ** 18); _lockWstETH(_VETOER_2, 5 * 10 ** 18); - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); _unlockStETH(_VETOER_1); _unlockWstETH(_VETOER_1); @@ -136,7 +137,7 @@ contract EscrowHappyPath is TestHelpers { uint256 secondVetoerStETHSharesAfterRebase = _ST_ETH.sharesOf(_VETOER_2); uint256 secondVetoerWstETHBalanceAfterRebase = _WST_ETH.balanceOf(_VETOER_2); - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); _unlockStETH(_VETOER_1); _unlockWstETH(_VETOER_1); @@ -180,7 +181,7 @@ contract EscrowHappyPath is TestHelpers { rebase(-100); - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); _unlockStETH(_VETOER_1); _unlockWstETH(_VETOER_1); @@ -206,7 +207,7 @@ contract EscrowHappyPath is TestHelpers { _lockUnstETH(_VETOER_1, unstETHIds); - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); _unlockUnstETH(_VETOER_1, unstETHIds); } @@ -377,10 +378,10 @@ contract EscrowHappyPath is TestHelpers { vm.prank(_VETOER_1); escrow.withdrawStETHAsETH(); - _wait(_RAGE_QUIT_EXTRA_TIMELOCK + 1); + _wait(_RAGE_QUIT_EXTRA_TIMELOCK.plusSeconds(1)); assertEq(escrow.isRageQuitFinalized(), true); - _wait(_RAGE_QUIT_WITHDRAWALS_TIMELOCK + 1); + _wait(_RAGE_QUIT_WITHDRAWALS_TIMELOCK.plusSeconds(1)); vm.startPrank(_VETOER_1); escrow.withdrawStETHAsETH(); @@ -418,10 +419,10 @@ contract EscrowHappyPath is TestHelpers { assertEq(escrow.isRageQuitFinalized(), false); - _wait(_RAGE_QUIT_EXTRA_TIMELOCK + 1); + _wait(_RAGE_QUIT_EXTRA_TIMELOCK.plusSeconds(1)); assertEq(escrow.isRageQuitFinalized(), true); - _wait(_RAGE_QUIT_WITHDRAWALS_TIMELOCK + 1); + _wait(_RAGE_QUIT_WITHDRAWALS_TIMELOCK.plusSeconds(1)); vm.startPrank(_VETOER_1); escrow.withdrawUnstETHAsETH(unstETHIds); @@ -443,7 +444,7 @@ contract EscrowHappyPath is TestHelpers { assertEq(escrow.getVetoerState(_VETOER_1).wstETHShares, firstVetoerWstETHAmount); assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); uint256[] memory stETHWithdrawalRequestAmounts = new uint256[](1); stETHWithdrawalRequestAmounts[0] = firstVetoerStETHAmount; @@ -483,7 +484,7 @@ contract EscrowHappyPath is TestHelpers { assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 1); assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); vm.prank(_VETOER_1); escrow.unlockUnstETH(stETHWithdrawalRequestIds); diff --git a/test/scenario/gate-seal-breaker.t.sol b/test/scenario/gate-seal-breaker.t.sol index 4f0dea97..ee925d80 100644 --- a/test/scenario/gate-seal-breaker.t.sol +++ b/test/scenario/gate-seal-breaker.t.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {percents, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; +import { + percents, ScenarioTestBlueprint, DurationType, Timestamps, Durations +} from "../utils/scenario-test-blueprint.sol"; import {GateSealMock} from "../mocks/GateSealMock.sol"; import {GateSealBreaker, IGateSeal} from "contracts/GateSealBreaker.sol"; @@ -9,8 +11,8 @@ 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; + DurationType private immutable _RELEASE_DELAY = Durations.from(5 days); + DurationType private immutable _MIN_SEAL_DURATION = Durations.from(14 days); address private immutable _VETOER = makeAddr("VETOER"); @@ -25,9 +27,9 @@ contract SealBreakerScenarioTest is ScenarioTestBlueprint { _sealables.push(address(_WITHDRAWAL_QUEUE)); - _gateSeal = new GateSealMock(_MIN_SEAL_DURATION, _SEALING_COMMITTEE_LIFETIME); + _gateSeal = new GateSealMock(_MIN_SEAL_DURATION.toSeconds(), _SEALING_COMMITTEE_LIFETIME.toSeconds()); - _sealBreaker = new GateSealBreaker(_RELEASE_DELAY, address(this), address(_dualGovernance)); + _sealBreaker = new GateSealBreaker(_RELEASE_DELAY.toSeconds(), address(this), address(_dualGovernance)); _sealBreaker.registerGateSeal(_gateSeal); @@ -56,7 +58,7 @@ contract SealBreakerScenarioTest is ScenarioTestBlueprint { // validate Withdrawal Queue was paused assertTrue(_WITHDRAWAL_QUEUE.isPaused()); - _wait(_MIN_SEAL_DURATION + 1); + _wait(_MIN_SEAL_DURATION.plusSeconds(1)); // validate the dual governance still in the veto signaling state _assertVetoSignalingState(); @@ -66,11 +68,11 @@ contract SealBreakerScenarioTest is ScenarioTestBlueprint { _sealBreaker.startRelease(_gateSeal); // wait the governance returns to normal state - _wait(14 days); + _wait(Durations.from(14 days)); _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); @@ -82,7 +84,7 @@ contract SealBreakerScenarioTest is ScenarioTestBlueprint { _sealBreaker.enactRelease(_gateSeal); // anyone may release the seal after timelock - _wait(_RELEASE_DELAY + 1); + _wait(_RELEASE_DELAY.plusSeconds(1)); _sealBreaker.enactRelease(_gateSeal); assertFalse(_WITHDRAWAL_QUEUE.isPaused()); @@ -100,7 +102,7 @@ contract SealBreakerScenarioTest is ScenarioTestBlueprint { assertTrue(_WITHDRAWAL_QUEUE.isPaused()); // wait some time, before dual governance enters veto signaling state - _wait(_MIN_SEAL_DURATION / 2); + _wait(_MIN_SEAL_DURATION.dividedBy(2)); _lockStETH(_VETOER, percents("10.0")); _assertVetoSignalingState(); @@ -109,25 +111,25 @@ contract SealBreakerScenarioTest is ScenarioTestBlueprint { vm.expectRevert(GateSealBreaker.MinSealDurationNotPassed.selector); _sealBreaker.startRelease(_gateSeal); - _wait(_MIN_SEAL_DURATION / 2 + 1); + _wait(_MIN_SEAL_DURATION.dividedBy(2).plusSeconds(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); + _wait(Durations.from(14 days)); _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_dualGovernance.CONFIG().VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _wait(_dualGovernance.CONFIG().VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); // the stETH whale takes his funds back from Escrow _unlockStETH(_VETOER); - _wait(_dualGovernance.CONFIG().VETO_COOLDOWN_DURATION() + 1); + _wait(_dualGovernance.CONFIG().VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertNormalState(); @@ -139,7 +141,7 @@ contract SealBreakerScenarioTest is ScenarioTestBlueprint { _sealBreaker.enactRelease(_gateSeal); // anyone may release the seal after timelock - _wait(_RELEASE_DELAY + 1); + _wait(_RELEASE_DELAY.plusSeconds(1)); _sealBreaker.enactRelease(_gateSeal); assertFalse(_WITHDRAWAL_QUEUE.isPaused()); } @@ -159,7 +161,7 @@ contract SealBreakerScenarioTest is ScenarioTestBlueprint { vm.expectRevert(GateSealBreaker.MinSealDurationNotPassed.selector); _sealBreaker.startRelease(_gateSeal); - vm.warp(block.timestamp + _MIN_SEAL_DURATION + 1); + _wait(_MIN_SEAL_DURATION.plusSeconds(1)); // now seal may be released _sealBreaker.startRelease(_gateSeal); @@ -169,7 +171,7 @@ contract SealBreakerScenarioTest is ScenarioTestBlueprint { _sealBreaker.enactRelease(_gateSeal); // anyone may release the seal after timelock - _wait(_RELEASE_DELAY + 1); + _wait(_RELEASE_DELAY.plusSeconds(1)); _sealBreaker.enactRelease(_gateSeal); assertFalse(_WITHDRAWAL_QUEUE.isPaused()); @@ -190,7 +192,7 @@ contract SealBreakerScenarioTest is ScenarioTestBlueprint { vm.expectRevert(GateSealBreaker.MinSealDurationNotPassed.selector); _sealBreaker.startRelease(_gateSeal); - _wait(_MIN_SEAL_DURATION + 1); + _wait(_MIN_SEAL_DURATION.plusSeconds(1)); // now seal may be released _sealBreaker.startRelease(_gateSeal); diff --git a/test/scenario/gov-state-transitions.t.sol b/test/scenario/gov-state-transitions.t.sol index 90b35b4e..efb12696 100644 --- a/test/scenario/gov-state-transitions.t.sol +++ b/test/scenario/gov-state-transitions.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {ScenarioTestBlueprint, percents} from "../utils/scenario-test-blueprint.sol"; +import {ScenarioTestBlueprint, percents, Durations} from "../utils/scenario-test-blueprint.sol"; contract GovernanceStateTransitions is ScenarioTestBlueprint { address internal immutable _VETOER = makeAddr("VETOER"); @@ -21,12 +21,12 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() / 2); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().dividedBy(2)); _activateNextState(); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() / 2 + 1); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().dividedBy(2).plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); @@ -39,19 +39,19 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() / 2); + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().dividedBy(2)); _activateNextState(); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() / 2); + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().dividedBy(2)); _activateNextState(); _assertVetoSignalingState(); _lockStETH(_VETOER, 1 gwei); - _wait(1 seconds); + _wait(Durations.from(1 seconds)); _activateNextState(); _assertRageQuitState(); @@ -67,12 +67,12 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() + 1); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); @@ -81,7 +81,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _getVetoSignallingEscrow().unlockStETH(); vm.stopPrank(); - _wait(_config.VETO_COOLDOWN_DURATION() + 1); + _wait(_config.VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertNormalState(); @@ -96,17 +96,17 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() + 1); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); - _wait(_config.VETO_COOLDOWN_DURATION() + 1); + _wait(_config.VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingState(); @@ -126,7 +126,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(1 seconds); + _wait(Durations.from(1 seconds)); _activateNextState(); _assertRageQuitState(); } diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index 33f7fef9..3083c0c3 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -8,7 +8,10 @@ import { ScenarioTestBlueprint, ExecutorCall, ExecutorCallHelpers, - DualGovernance + DualGovernance, + Timestamp, + Timestamps, + Durations } from "../utils/scenario-test-blueprint.sol"; import {Proposals} from "contracts/libraries/Proposals.sol"; @@ -66,7 +69,7 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertCanSchedule(_singleGovernance, maliciousProposalId, false); // some time required to assemble the emergency committee and activate emergency mode - _wait(_config.AFTER_SUBMIT_DELAY() / 2); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); // malicious call still can't be scheduled _assertCanSchedule(_singleGovernance, maliciousProposalId, false); @@ -76,14 +79,14 @@ contract PlanBSetup is ScenarioTestBlueprint { _timelock.activateEmergencyMode(); // emergency mode was successfully activated - uint256 expectedEmergencyModeEndTimestamp = block.timestamp + _EMERGENCY_MODE_DURATION; + Timestamp expectedEmergencyModeEndTimestamp = _EMERGENCY_MODE_DURATION.addTo(Timestamps.now()); emergencyState = _timelock.getEmergencyState(); assertTrue(emergencyState.isEmergencyModeActivated); assertEq(emergencyState.emergencyModeEndsAfter, expectedEmergencyModeEndTimestamp); // after the submit delay has passed, the call still may be scheduled, but executed // only the emergency committee - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2 + 1); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2).plusSeconds(1)); _assertCanSchedule(_singleGovernance, maliciousProposalId, true); _scheduleProposal(_singleGovernance, maliciousProposalId); @@ -105,7 +108,7 @@ contract PlanBSetup is ScenarioTestBlueprint { { // Lido contributors work hard to implement and ship the Dual Governance mechanism // before the emergency mode is over - vm.warp(block.timestamp + _EMERGENCY_PROTECTION_DURATION / 2); + _wait(_EMERGENCY_PROTECTION_DURATION.dividedBy(2)); // Time passes but malicious proposal still on hold _assertCanExecute(maliciousProposalId, false); @@ -127,7 +130,7 @@ contract PlanBSetup is ScenarioTestBlueprint { _EMERGENCY_ACTIVATION_COMMITTEE, _EMERGENCY_EXECUTION_COMMITTEE, _EMERGENCY_PROTECTION_DURATION, - 30 days + Durations.from(30 days) ) ) ] @@ -161,7 +164,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // ACT 4. 🫡 EMERGENCY COMMITTEE LIFETIME IS ENDED // --- { - vm.warp(block.timestamp + _EMERGENCY_PROTECTION_DURATION + 1); + _wait(_EMERGENCY_PROTECTION_DURATION.plusSeconds(1)); assertFalse(_timelock.isEmergencyProtectionEnabled()); uint256 proposalId = _submitProposal( @@ -189,7 +192,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // --- { // some time later, the major Dual Governance update release is ready to be launched - vm.warp(block.timestamp + 365 days); + _wait(Durations.from(365 days)); DualGovernance dualGovernanceV2 = new DualGovernance(address(_config), address(_timelock), address(_escrowMasterCopy), _ADMIN_PROPOSER); @@ -205,7 +208,7 @@ contract PlanBSetup is ScenarioTestBlueprint { _EMERGENCY_ACTIVATION_COMMITTEE, _EMERGENCY_EXECUTION_COMMITTEE, _EMERGENCY_PROTECTION_DURATION, - 30 days + Durations.from(30 days) ) ) ] @@ -235,8 +238,8 @@ contract PlanBSetup is ScenarioTestBlueprint { assertEq(emergencyState.activationCommittee, _EMERGENCY_ACTIVATION_COMMITTEE); assertEq(emergencyState.executionCommittee, _EMERGENCY_EXECUTION_COMMITTEE); assertFalse(emergencyState.isEmergencyModeActivated); - assertEq(emergencyState.emergencyModeDuration, 30 days); - assertEq(emergencyState.emergencyModeEndsAfter, 0); + assertEq(emergencyState.emergencyModeDuration, Durations.from(30 days)); + assertEq(emergencyState.emergencyModeEndsAfter, Timestamps.ZERO); // use the new version of the dual governance in the future calls _dualGovernance = dualGovernanceV2; @@ -287,7 +290,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // activate emergency mode EmergencyState memory emergencyState; { - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); _timelock.activateEmergencyMode(); @@ -299,12 +302,13 @@ contract PlanBSetup is ScenarioTestBlueprint { // delay for malicious proposal has passed, but it can't be executed because of emergency mode was activated { // the after submit delay has passed, and proposal can be scheduled, but not executed - _wait(_config.AFTER_SUBMIT_DELAY() + 1); + _wait(_config.AFTER_SCHEDULE_DELAY() + Durations.from(1 seconds)); + _wait(_config.AFTER_SUBMIT_DELAY().plusSeconds(1)); _assertCanSchedule(_singleGovernance, maliciousProposalId, true); _scheduleProposal(_singleGovernance, maliciousProposalId); - _wait(_config.AFTER_SCHEDULE_DELAY() + 1); + _wait(_config.AFTER_SCHEDULE_DELAY().plusSeconds(1)); _assertCanExecute(maliciousProposalId, false); vm.expectRevert( @@ -316,10 +320,10 @@ contract PlanBSetup is ScenarioTestBlueprint { // another malicious call is scheduled during the emergency mode also can't be executed uint256 anotherMaliciousProposalId; { - vm.warp(block.timestamp + _EMERGENCY_MODE_DURATION / 2); + _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); // emergency mode still active - assertTrue(emergencyState.emergencyModeEndsAfter > block.timestamp); + assertTrue(emergencyState.emergencyModeEndsAfter > Timestamps.now()); anotherMaliciousProposalId = _submitProposal(_singleGovernance, "Another Rug Pool attempt", maliciousCalls); @@ -327,10 +331,10 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertCanExecute(anotherMaliciousProposalId, false); // the after submit delay has passed, and proposal can not be executed - _wait(_config.AFTER_SUBMIT_DELAY() + 1); + _wait(_config.AFTER_SUBMIT_DELAY().plusSeconds(1)); _assertCanSchedule(_singleGovernance, anotherMaliciousProposalId, true); - _wait(_config.AFTER_SCHEDULE_DELAY() + 1); + _wait(_config.AFTER_SCHEDULE_DELAY().plusSeconds(1)); _assertCanExecute(anotherMaliciousProposalId, false); vm.expectRevert( @@ -341,8 +345,8 @@ contract PlanBSetup is ScenarioTestBlueprint { // emergency mode is over but proposals can't be executed until the emergency mode turned off manually { - vm.warp(block.timestamp + _EMERGENCY_MODE_DURATION / 2); - assertTrue(emergencyState.emergencyModeEndsAfter < block.timestamp); + _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); + assertTrue(emergencyState.emergencyModeEndsAfter < Timestamps.now()); vm.expectRevert( abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, true, false) @@ -397,8 +401,8 @@ contract PlanBSetup is ScenarioTestBlueprint { // before the end of the emergency mode emergency committee can reset the controller to // disable dual governance { - vm.warp(block.timestamp + _EMERGENCY_MODE_DURATION / 2); - assertTrue(emergencyState.emergencyModeEndsAfter > block.timestamp); + _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); + assertTrue(emergencyState.emergencyModeEndsAfter > Timestamps.now()); vm.prank(_EMERGENCY_EXECUTION_COMMITTEE); _timelock.emergencyReset(); @@ -408,8 +412,8 @@ contract PlanBSetup is ScenarioTestBlueprint { emergencyState = _timelock.getEmergencyState(); assertEq(emergencyState.activationCommittee, address(0)); assertEq(emergencyState.executionCommittee, address(0)); - assertEq(emergencyState.emergencyModeDuration, 0); - assertEq(emergencyState.emergencyModeEndsAfter, 0); + assertEq(emergencyState.emergencyModeDuration, Durations.ZERO); + assertEq(emergencyState.emergencyModeEndsAfter, Timestamps.ZERO); assertFalse(emergencyState.isEmergencyModeActivated); } } @@ -423,7 +427,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // wait till the protection duration passes { - vm.warp(block.timestamp + _EMERGENCY_PROTECTION_DURATION + 1); + _wait(_EMERGENCY_PROTECTION_DURATION.plusSeconds(1)); } EmergencyState memory emergencyState = _timelock.getEmergencyState(); diff --git a/test/scenario/happy-path.t.sol b/test/scenario/happy-path.t.sol index 3cde7bd0..fd55104b 100644 --- a/test/scenario/happy-path.t.sol +++ b/test/scenario/happy-path.t.sol @@ -29,14 +29,14 @@ contract HappyPathTest is ScenarioTestBlueprint { _assertProposalSubmitted(proposalId); _assertSubmittedProposalData(proposalId, regularStaffCalls); - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); // the min execution delay hasn't elapsed yet vm.expectRevert(abi.encodeWithSelector(Proposals.AfterSubmitDelayNotPassed.selector, (proposalId))); _scheduleProposal(_dualGovernance, proposalId); // wait till the first phase of timelock passes - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2 + 1); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2).plusSeconds(1)); _assertCanSchedule(_dualGovernance, proposalId, true); _scheduleProposal(_dualGovernance, proposalId); @@ -67,7 +67,7 @@ contract HappyPathTest is ScenarioTestBlueprint { uint256 proposalId = _submitProposal(_dualGovernance, "Multiple items", multipleCalls); - _wait(_config.AFTER_SUBMIT_DELAY() / 2); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); // proposal can't be scheduled before the after submit delay has passed _assertCanSchedule(_dualGovernance, proposalId, false); @@ -77,7 +77,7 @@ contract HappyPathTest is ScenarioTestBlueprint { _scheduleProposal(_dualGovernance, proposalId); // wait till the DG-enforced timelock elapses - _wait(_config.AFTER_SUBMIT_DELAY() / 2 + 1); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2).plusSeconds(1)); _assertCanSchedule(_dualGovernance, proposalId, true); _scheduleProposal(_dualGovernance, proposalId); diff --git a/test/scenario/last-moment-malicious-proposal.t.sol b/test/scenario/last-moment-malicious-proposal.t.sol index d40e4df3..99bf4e47 100644 --- a/test/scenario/last-moment-malicious-proposal.t.sol +++ b/test/scenario/last-moment-malicious-proposal.t.sol @@ -6,7 +6,8 @@ import { ScenarioTestBlueprint, ExecutorCall, ExecutorCallHelpers, - DualGovernanceState + DualGovernanceState, + Durations } from "../utils/scenario-test-blueprint.sol"; interface IDangerousContract { @@ -44,7 +45,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _logVetoSignallingState(); // almost all veto signalling period has passed - _wait(20 days); + _wait(Durations.from(20 days)); _activateNextState(); _assertVetoSignalingState(); _logVetoSignallingState(); @@ -68,7 +69,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("4. MALICIOUS ACTOR UNLOCK FUNDS FROM ESCROW"); { - _wait(12 seconds); + _wait(Durations.from(12 seconds)); _unlockStETH(maliciousActor); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); @@ -77,12 +78,12 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { address stEthHolders = makeAddr("STETH_WHALE"); _step("5. STETH HOLDERS ACQUIRING QUORUM TO VETO MALICIOUS PROPOSAL"); { - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() / 2); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().dividedBy(2)); _lockStETH(stEthHolders, percents(_config.FIRST_SEAL_RAGE_QUIT_SUPPORT() + 1)); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() / 2 + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().dividedBy(2).plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); } @@ -104,7 +105,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("7. NEW VETO SIGNALLING ROUND FOR MALICIOUS PROPOSAL IS STARTED"); { - _wait(_config.VETO_COOLDOWN_DURATION() + 1); + _wait(_config.VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingState(); _logVetoSignallingState(); @@ -114,7 +115,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _assertVetoSignalingState(); _logVetoSignallingState(); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() + 1); + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); _logVetoSignallingState(); _activateNextState(); _assertRageQuitState(); @@ -145,17 +146,17 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { // --- address maliciousActor = makeAddr("MALICIOUS_ACTOR"); { - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); _lockStETH(maliciousActor, percents("12.0")); _assertVetoSignalingState(); - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2 + 1); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2).plusSeconds(1)); _assertProposalSubmitted(proposalId); (, uint256 currentVetoSignallingDuration,,) = _getVetoSignallingState(); - vm.warp(block.timestamp + currentVetoSignallingDuration + 1); + _wait(Durations.from(currentVetoSignallingDuration + 1)); _activateNextState(); _assertVetoSignalingDeactivationState(); @@ -165,7 +166,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { // ACT 3. THE VETO SIGNALLING DEACTIVATION DURATION EQUALS TO "VETO_SIGNALLING_DEACTIVATION_MAX_DURATION" DAYS // --- { - vm.warp(block.timestamp + _config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); @@ -206,12 +207,12 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("3. THE VETO SIGNALLING & DEACTIVATION PASSED BUT PROPOSAL STILL NOT EXECUTABLE"); { - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() + 1); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); @@ -222,7 +223,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("4. AFTER THE VETO COOLDOWN GOVERNANCE TRANSITIONS INTO NORMAL STATE"); { _unlockStETH(maliciousActor); - _wait(_config.VETO_COOLDOWN_DURATION() + 1); + _wait(_config.VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertNormalState(); } @@ -260,12 +261,12 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("3. THE VETO SIGNALLING & DEACTIVATION PASSED BUT PROPOSAL STILL NOT EXECUTABLE"); { - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() + 1); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); @@ -275,7 +276,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("4. AFTER THE VETO COOLDOWN NEW VETO SIGNALLING ROUND STARTED"); { - _wait(_config.VETO_COOLDOWN_DURATION() + 1); + _wait(_config.VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingState(); _logVetoSignallingState(); @@ -286,12 +287,12 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("5. PROPOSAL EXECUTABLE IN THE NEXT VETO COOLDOWN"); { - _wait(2 * _config.DYNAMIC_TIMELOCK_MIN_DURATION()); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().multipliedBy(2)); _activateNextState(); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index 38e75c0e..9ad3eca0 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -44,7 +44,7 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { vetoedStETHAmount = _lockStETH(vetoer, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT() + 1)); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() + 1); + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertRageQuitState(); } @@ -87,7 +87,7 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { rageQuitEscrow.claimNextWithdrawalsBatch(offset, hints); } - _wait(_config.RAGE_QUIT_EXTENSION_DELAY() + 1); + _wait(_config.RAGE_QUIT_EXTENSION_DELAY().plusSeconds(1)); assertTrue(rageQuitEscrow.isRageQuitFinalized()); } diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 7216c248..55ad0a24 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -12,7 +12,7 @@ import {Executor} from "contracts/Executor.sol"; import {Proposal, Proposals, ExecutorCall, Status} from "contracts/libraries/Proposals.sol"; import {EmergencyProtection, EmergencyState} from "contracts/libraries/EmergencyProtection.sol"; -import {UnitTest} from "test/utils/unit-test.sol"; +import {UnitTest, Duration, Timestamp, Timestamps, Durations, console} from "test/utils/unit-test.sol"; import {TargetMock} from "test/utils/utils.sol"; import {ExecutorCallHelpers} from "test/utils/executor-calls.sol"; import {IDangerousContract} from "test/utils/interfaces.sol"; @@ -25,8 +25,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { address private _emergencyActivator = makeAddr("EMERGENCY_ACTIVATION_COMMITTEE"); address private _emergencyEnactor = makeAddr("EMERGENCY_EXECUTION_COMMITTEE"); - uint256 private _emergencyModeDuration = 180 days; - uint256 private _emergencyProtectionDuration = 90 days; + Duration private _emergencyModeDuration = Durations.from(180 days); + Duration private _emergencyProtectionDuration = Durations.from(90 days); address private _emergencyGovernance = makeAddr("EMERGENCY_GOVERNANCE"); address private _dualGovernance = makeAddr("DUAL_GOVERNANCE"); @@ -395,7 +395,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { EmergencyState memory state = _timelock.getEmergencyState(); assertEq(_isEmergencyStateActivated(), true); - _wait(state.emergencyModeDuration + 1); + _wait(state.emergencyModeDuration.plusSeconds(1)); vm.prank(stranger); _timelock.deactivateEmergencyMode(); @@ -449,9 +449,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(newState.activationCommittee, address(0)); assertEq(newState.executionCommittee, address(0)); - assertEq(newState.protectedTill, 0); - assertEq(newState.emergencyModeDuration, 0); - assertEq(newState.emergencyModeEndsAfter, 0); + assertEq(newState.protectedTill, Timestamps.ZERO); + assertEq(newState.emergencyModeDuration, Durations.ZERO); + assertEq(newState.emergencyModeEndsAfter, Timestamps.ZERO); } function test_after_emergency_reset_all_proposals_are_cancelled() external { @@ -518,9 +518,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(state.activationCommittee, _emergencyActivator); assertEq(state.executionCommittee, _emergencyEnactor); - assertEq(state.protectedTill, block.timestamp + _emergencyProtectionDuration); + assertEq(state.protectedTill, _emergencyProtectionDuration.addTo(Timestamps.now())); assertEq(state.emergencyModeDuration, _emergencyModeDuration); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); assertEq(state.isEmergencyModeActivated, false); } @@ -540,9 +540,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(state.activationCommittee, address(0)); assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, 0); - assertEq(state.emergencyModeDuration, 0); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.protectedTill, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); assertEq(state.isEmergencyModeActivated, false); } @@ -604,9 +604,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(state.isEmergencyModeActivated, false); assertEq(state.activationCommittee, address(0)); assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, 0); - assertEq(state.emergencyModeDuration, 0); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.protectedTill, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); vm.prank(_adminExecutor); _localTimelock.setEmergencyProtection( @@ -618,9 +618,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_localTimelock.getEmergencyState().isEmergencyModeActivated, false); assertEq(state.activationCommittee, _emergencyActivator); assertEq(state.executionCommittee, _emergencyEnactor); - assertEq(state.protectedTill, block.timestamp + _emergencyProtectionDuration); + assertEq(state.protectedTill, _emergencyProtectionDuration.addTo(Timestamps.now())); assertEq(state.emergencyModeDuration, _emergencyModeDuration); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); vm.prank(_emergencyActivator); _localTimelock.activateEmergencyMode(); @@ -628,11 +628,11 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { state = _localTimelock.getEmergencyState(); assertEq(_localTimelock.getEmergencyState().isEmergencyModeActivated, true); - assertEq(state.activationCommittee, _emergencyActivator); assertEq(state.executionCommittee, _emergencyEnactor); - assertEq(state.protectedTill, block.timestamp + _emergencyProtectionDuration); + assertEq(state.activationCommittee, _emergencyActivator); assertEq(state.emergencyModeDuration, _emergencyModeDuration); - assertEq(state.emergencyModeEndsAfter, block.timestamp + _emergencyModeDuration); + assertEq(state.protectedTill, _emergencyProtectionDuration.addTo(Timestamps.now())); + assertEq(state.emergencyModeEndsAfter, _emergencyModeDuration.addTo(Timestamps.now())); vm.prank(_adminExecutor); _localTimelock.deactivateEmergencyMode(); @@ -642,9 +642,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(state.isEmergencyModeActivated, false); assertEq(state.activationCommittee, address(0)); assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, 0); - assertEq(state.emergencyModeDuration, 0); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.protectedTill, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); } function test_get_emergency_state_reset() external { @@ -666,15 +666,15 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(state.isEmergencyModeActivated, false); assertEq(state.activationCommittee, address(0)); assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, 0); - assertEq(state.emergencyModeDuration, 0); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.protectedTill, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); } // EmergencyProtectedTimelock.getGovernance() function testFuzz_get_governance(address governance) external { - vm.assume(governance != address(0)); + vm.assume(governance != address(0) && governance != _timelock.getGovernance()); vm.prank(_adminExecutor); _timelock.setGovernance(governance); assertEq(_timelock.getGovernance(), governance); @@ -692,13 +692,13 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { Proposal memory submittedProposal = _timelock.getProposal(1); - uint256 submitTimestamp = block.timestamp; + Timestamp submitTimestamp = Timestamps.now(); assertEq(submittedProposal.id, 1); assertEq(submittedProposal.executor, _adminExecutor); assertEq(submittedProposal.submittedAt, submitTimestamp); - assertEq(submittedProposal.scheduledAt, 0); - assertEq(submittedProposal.executedAt, 0); + assertEq(submittedProposal.scheduledAt, Timestamps.ZERO); + assertEq(submittedProposal.executedAt, Timestamps.ZERO); // assertEq doesn't support comparing enumerables so far assert(submittedProposal.status == Status.Submitted); assertEq(submittedProposal.calls.length, 1); @@ -709,7 +709,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _wait(_config.AFTER_SUBMIT_DELAY()); _timelock.schedule(1); - uint256 scheduleTimestamp = block.timestamp; + Timestamp scheduleTimestamp = Timestamps.now(); Proposal memory scheduledProposal = _timelock.getProposal(1); @@ -717,7 +717,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(scheduledProposal.executor, _adminExecutor); assertEq(scheduledProposal.submittedAt, submitTimestamp); assertEq(scheduledProposal.scheduledAt, scheduleTimestamp); - assertEq(scheduledProposal.executedAt, 0); + assertEq(scheduledProposal.executedAt, Timestamps.ZERO); // // assertEq doesn't support comparing enumerables so far assert(scheduledProposal.status == Status.Scheduled); assertEq(scheduledProposal.calls.length, 1); @@ -730,7 +730,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.execute(1); Proposal memory executedProposal = _timelock.getProposal(1); - uint256 executeTimestamp = block.timestamp; + Timestamp executeTimestamp = Timestamps.now(); assertEq(executedProposal.id, 1); assertEq(executedProposal.executor, _adminExecutor); @@ -751,8 +751,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(cancelledProposal.id, 2); assertEq(cancelledProposal.executor, _adminExecutor); assertEq(cancelledProposal.submittedAt, submitTimestamp); - assertEq(cancelledProposal.scheduledAt, 0); - assertEq(cancelledProposal.executedAt, 0); + assertEq(cancelledProposal.scheduledAt, Timestamps.ZERO); + assertEq(cancelledProposal.executedAt, Timestamps.ZERO); // assertEq doesn't support comparing enumerables so far assert(cancelledProposal.status == Status.Cancelled); assertEq(cancelledProposal.calls.length, 1); diff --git a/test/unit/libraries/EmergencyProtection.t.sol b/test/unit/libraries/EmergencyProtection.t.sol index 442c708f..d3681e08 100644 --- a/test/unit/libraries/EmergencyProtection.t.sol +++ b/test/unit/libraries/EmergencyProtection.t.sol @@ -5,7 +5,7 @@ import {Test, Vm} from "forge-std/Test.sol"; import {EmergencyProtection, EmergencyState} from "contracts/libraries/EmergencyProtection.sol"; -import {UnitTest} from "test/utils/unit-test.sol"; +import {UnitTest, Duration, Durations, Timestamp, Timestamps} from "test/utils/unit-test.sol"; contract EmergencyProtectionUnitTests is UnitTest { using EmergencyProtection for EmergencyProtection.State; @@ -15,11 +15,11 @@ contract EmergencyProtectionUnitTests is UnitTest { function testFuzz_setup_emergency_protection( address activationCommittee, address executionCommittee, - uint256 protectedTill, - uint256 duration + Duration protectionDuration, + Duration duration ) external { - vm.assume(protectedTill > 0 && protectedTill < type(uint40).max); - vm.assume(duration > 0 && duration < type(uint32).max); + vm.assume(protectionDuration > Durations.ZERO); + vm.assume(duration > Durations.ZERO); vm.assume(activationCommittee != address(0)); vm.assume(executionCommittee != address(0)); @@ -28,125 +28,150 @@ contract EmergencyProtectionUnitTests is UnitTest { vm.expectEmit(); emit EmergencyProtection.EmergencyExecutionCommitteeSet(executionCommittee); vm.expectEmit(); - emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + protectedTill); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(protectionDuration.addTo(Timestamps.now())); vm.expectEmit(); emit EmergencyProtection.EmergencyModeDurationSet(duration); vm.recordLogs(); - _emergencyProtection.setup(activationCommittee, executionCommittee, protectedTill, duration); + _emergencyProtection.setup(activationCommittee, executionCommittee, protectionDuration, duration); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 4); assertEq(_emergencyProtection.activationCommittee, activationCommittee); assertEq(_emergencyProtection.executionCommittee, executionCommittee); - assertEq(_emergencyProtection.protectedTill, block.timestamp + protectedTill); + assertEq(_emergencyProtection.protectedTill, protectionDuration.addTo(Timestamps.now())); assertEq(_emergencyProtection.emergencyModeDuration, duration); - assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } function test_setup_same_activation_committee() external { + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); address activationCommittee = makeAddr("activationCommittee"); - _emergencyProtection.setup(activationCommittee, address(0x2), 100, 100); + _emergencyProtection.setup(activationCommittee, address(0x2), protectionDuration, emergencyModeDuration); + + Duration newProtectionDuration = Durations.from(200 seconds); + Duration newEmergencyModeDuration = Durations.from(300 seconds); vm.expectEmit(); emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x3)); vm.expectEmit(); - emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + 200); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(newProtectionDuration.addTo(Timestamps.now())); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDurationSet(300); + emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); vm.recordLogs(); - _emergencyProtection.setup(activationCommittee, address(0x3), 200, 300); + _emergencyProtection.setup(activationCommittee, address(0x3), newProtectionDuration, newEmergencyModeDuration); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); assertEq(_emergencyProtection.activationCommittee, activationCommittee); assertEq(_emergencyProtection.executionCommittee, address(0x3)); - assertEq(_emergencyProtection.protectedTill, block.timestamp + 200); - assertEq(_emergencyProtection.emergencyModeDuration, 300); - assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + assertEq(_emergencyProtection.protectedTill, newProtectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } function test_setup_same_execution_committee() external { + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); address executionCommittee = makeAddr("executionCommittee"); - _emergencyProtection.setup(address(0x1), executionCommittee, 100, 100); + _emergencyProtection.setup(address(0x1), executionCommittee, protectionDuration, emergencyModeDuration); + + Duration newProtectionDuration = Durations.from(200 seconds); + Duration newEmergencyModeDuration = Durations.from(300 seconds); vm.expectEmit(); emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x2)); vm.expectEmit(); - emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + 200); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(newProtectionDuration.addTo(Timestamps.now())); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDurationSet(300); + emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); vm.recordLogs(); - _emergencyProtection.setup(address(0x2), executionCommittee, 200, 300); + _emergencyProtection.setup(address(0x2), executionCommittee, newProtectionDuration, newEmergencyModeDuration); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); assertEq(_emergencyProtection.activationCommittee, address(0x2)); assertEq(_emergencyProtection.executionCommittee, executionCommittee); - assertEq(_emergencyProtection.protectedTill, block.timestamp + 200); - assertEq(_emergencyProtection.emergencyModeDuration, 300); - assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + assertEq(_emergencyProtection.protectedTill, newProtectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } function test_setup_same_protected_till() external { - _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + + Duration newProtectionDuration = protectionDuration; // the new value is the same as previous one + Duration newEmergencyModeDuration = Durations.from(200 seconds); vm.expectEmit(); emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); vm.expectEmit(); emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x4)); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDurationSet(200); + emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); vm.recordLogs(); - _emergencyProtection.setup(address(0x3), address(0x4), 100, 200); + _emergencyProtection.setup(address(0x3), address(0x4), newProtectionDuration, newEmergencyModeDuration); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); assertEq(_emergencyProtection.activationCommittee, address(0x3)); assertEq(_emergencyProtection.executionCommittee, address(0x4)); - assertEq(_emergencyProtection.protectedTill, block.timestamp + 100); - assertEq(_emergencyProtection.emergencyModeDuration, 200); - assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + assertEq(_emergencyProtection.protectedTill, protectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } function test_setup_same_emergency_mode_duration() external { - _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + + Duration newProtectionDuration = Durations.from(200 seconds); + Duration newEmergencyModeDuration = emergencyModeDuration; // the new value is the same as previous one vm.expectEmit(); emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); vm.expectEmit(); emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x4)); vm.expectEmit(); - emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + 200); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(newProtectionDuration.addTo(Timestamps.now())); vm.recordLogs(); - _emergencyProtection.setup(address(0x3), address(0x4), 200, 100); + _emergencyProtection.setup(address(0x3), address(0x4), newProtectionDuration, newEmergencyModeDuration); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); assertEq(_emergencyProtection.activationCommittee, address(0x3)); assertEq(_emergencyProtection.executionCommittee, address(0x4)); - assertEq(_emergencyProtection.protectedTill, block.timestamp + 200); - assertEq(_emergencyProtection.emergencyModeDuration, 100); - assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + assertEq(_emergencyProtection.protectedTill, newProtectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } function test_activate_emergency_mode() external { - _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeActivated(block.timestamp); + emit EmergencyProtection.EmergencyModeActivated(Timestamps.now()); vm.recordLogs(); @@ -155,19 +180,22 @@ contract EmergencyProtectionUnitTests is UnitTest { Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 1); - assertEq(_emergencyProtection.emergencyModeEndsAfter, block.timestamp + 100); + assertEq(_emergencyProtection.emergencyModeEndsAfter, emergencyModeDuration.addTo(Timestamps.now())); } function test_cannot_activate_emergency_mode_if_protected_till_expired() external { - uint256 protectedTill = 100; - _emergencyProtection.setup(address(0x1), address(0x2), protectedTill, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); - _wait(protectedTill + 1); + _wait(protectionDuration.plusSeconds(1)); vm.expectRevert( abi.encodeWithSelector( EmergencyProtection.EmergencyCommitteeExpired.selector, - [block.timestamp, _emergencyProtection.protectedTill] + Timestamps.now(), + _emergencyProtection.protectedTill ) ); _emergencyProtection.activate(); @@ -176,19 +204,17 @@ contract EmergencyProtectionUnitTests is UnitTest { function testFuzz_deactivate_emergency_mode( address activationCommittee, address executionCommittee, - uint256 protectedTill, - uint256 duration + Duration protectionDuration, + Duration emergencyModeDuration ) external { - vm.assume(protectedTill > 0 && protectedTill < type(uint40).max); - vm.assume(duration > 0 && duration < type(uint32).max); vm.assume(activationCommittee != address(0)); vm.assume(executionCommittee != address(0)); - _emergencyProtection.setup(activationCommittee, executionCommittee, protectedTill, duration); + _emergencyProtection.setup(activationCommittee, executionCommittee, protectionDuration, emergencyModeDuration); _emergencyProtection.activate(); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDeactivated(block.timestamp); + emit EmergencyProtection.EmergencyModeDeactivated(Timestamps.now()); vm.recordLogs(); @@ -199,9 +225,9 @@ contract EmergencyProtectionUnitTests is UnitTest { assertEq(_emergencyProtection.activationCommittee, address(0)); assertEq(_emergencyProtection.executionCommittee, address(0)); - assertEq(_emergencyProtection.protectedTill, 0); - assertEq(_emergencyProtection.emergencyModeDuration, 0); - assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + assertEq(_emergencyProtection.protectedTill, Timestamps.ZERO); + assertEq(_emergencyProtection.emergencyModeDuration, Durations.ZERO); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } function test_get_emergency_state() external { @@ -209,20 +235,23 @@ contract EmergencyProtectionUnitTests is UnitTest { assertEq(state.activationCommittee, address(0)); assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, 0); - assertEq(state.emergencyModeDuration, 0); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.protectedTill, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); assertEq(state.isEmergencyModeActivated, false); - _emergencyProtection.setup(address(0x1), address(0x2), 100, 200); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(200 seconds); + + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); state = _emergencyProtection.getEmergencyState(); assertEq(state.activationCommittee, address(0x1)); assertEq(state.executionCommittee, address(0x2)); - assertEq(state.protectedTill, block.timestamp + 100); - assertEq(state.emergencyModeDuration, 200); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.protectedTill, protectionDuration.addTo(Timestamps.now())); + assertEq(state.emergencyModeDuration, emergencyModeDuration); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); assertEq(state.isEmergencyModeActivated, false); _emergencyProtection.activate(); @@ -231,9 +260,9 @@ contract EmergencyProtectionUnitTests is UnitTest { assertEq(state.activationCommittee, address(0x1)); assertEq(state.executionCommittee, address(0x2)); - assertEq(state.protectedTill, block.timestamp + 100); - assertEq(state.emergencyModeDuration, 200); - assertEq(state.emergencyModeEndsAfter, block.timestamp + 200); + assertEq(state.protectedTill, protectionDuration.addTo(Timestamps.now())); + assertEq(state.emergencyModeDuration, emergencyModeDuration); + assertEq(state.emergencyModeEndsAfter, emergencyModeDuration.addTo(Timestamps.now())); assertEq(state.isEmergencyModeActivated, true); _emergencyProtection.deactivate(); @@ -242,16 +271,19 @@ contract EmergencyProtectionUnitTests is UnitTest { assertEq(state.activationCommittee, address(0)); assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, 0); - assertEq(state.emergencyModeDuration, 0); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.protectedTill, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); assertEq(state.isEmergencyModeActivated, false); } function test_is_emergency_mode_activated() external { assertEq(_emergencyProtection.isEmergencyModeActivated(), false); - _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); assertEq(_emergencyProtection.isEmergencyModeActivated(), false); @@ -267,9 +299,10 @@ contract EmergencyProtectionUnitTests is UnitTest { function test_is_emergency_mode_passed() external { assertEq(_emergencyProtection.isEmergencyModePassed(), false); - uint256 duration = 200; + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(200 seconds); - _emergencyProtection.setup(address(0x1), address(0x2), 100, duration); + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); assertEq(_emergencyProtection.isEmergencyModePassed(), false); @@ -277,7 +310,7 @@ contract EmergencyProtectionUnitTests is UnitTest { assertEq(_emergencyProtection.isEmergencyModePassed(), false); - _wait(duration + 1); + _wait(emergencyModeDuration.plusSeconds(1)); assertEq(_emergencyProtection.isEmergencyModePassed(), true); @@ -287,24 +320,28 @@ contract EmergencyProtectionUnitTests is UnitTest { } function test_is_emergency_protection_enabled() external { - uint256 protectedTill = 100; - uint256 duration = 200; + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(200 seconds); assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), false); - _emergencyProtection.setup(address(0x1), address(0x2), protectedTill, duration); + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); - _wait(protectedTill - block.timestamp); + EmergencyState memory emergencyState = _emergencyProtection.getEmergencyState(); + + _wait(Durations.between(emergencyState.protectedTill, Timestamps.now())); + + // _wait(emergencyState.protectedTill.absDiff(Timestamps.now())); EmergencyProtection.activate(_emergencyProtection); - _wait(duration); + _wait(emergencyModeDuration); assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); - _wait(100); + _wait(protectionDuration); assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); @@ -321,7 +358,10 @@ contract EmergencyProtectionUnitTests is UnitTest { _emergencyProtection.checkActivationCommittee(stranger); _emergencyProtection.checkActivationCommittee(address(0)); - _emergencyProtection.setup(committee, address(0x2), 100, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(committee, address(0x2), protectionDuration, emergencyModeDuration); _emergencyProtection.checkActivationCommittee(committee); @@ -337,7 +377,10 @@ contract EmergencyProtectionUnitTests is UnitTest { _emergencyProtection.checkExecutionCommittee(stranger); _emergencyProtection.checkExecutionCommittee(address(0)); - _emergencyProtection.setup(address(0x1), committee, 100, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(address(0x1), committee, protectionDuration, emergencyModeDuration); _emergencyProtection.checkExecutionCommittee(committee); @@ -352,7 +395,10 @@ contract EmergencyProtectionUnitTests is UnitTest { _emergencyProtection.checkEmergencyModeActive(true); _emergencyProtection.checkEmergencyModeActive(false); - _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); _emergencyProtection.activate(); _emergencyProtection.checkEmergencyModeActive(true); diff --git a/test/unit/libraries/Proposals.t.sol b/test/unit/libraries/Proposals.t.sol index 3b4ed280..0b70209a 100644 --- a/test/unit/libraries/Proposals.t.sol +++ b/test/unit/libraries/Proposals.t.sol @@ -7,9 +7,7 @@ import {Executor} from "contracts/Executor.sol"; import {Proposals, ExecutorCall, Proposal, Status} from "contracts/libraries/Proposals.sol"; import {TargetMock} from "test/utils/utils.sol"; -import {UnitTest} from "test/utils/unit-test.sol"; -import {IDangerousContract} from "test/utils/interfaces.sol"; -import {ExecutorCallHelpers} from "test/utils/executor-calls.sol"; +import {UnitTest, Timestamps, Timestamp, Durations, Duration} from "test/utils/unit-test.sol"; contract ProposalsUnitTests is UnitTest { using Proposals for Proposals.State; @@ -50,9 +48,9 @@ contract ProposalsUnitTests is UnitTest { Proposals.ProposalPacked memory proposal = _proposals.proposals[proposalsCount - PROPOSAL_ID_OFFSET]; assertEq(proposal.executor, address(_executor)); - assertEq(proposal.submittedAt, block.timestamp); - assertEq(proposal.executedAt, 0); - assertEq(proposal.scheduledAt, 0); + assertEq(proposal.submittedAt, Timestamps.now()); + assertEq(proposal.executedAt, Timestamps.ZERO); + assertEq(proposal.scheduledAt, Timestamps.ZERO); assertEq(proposal.calls.length, 1); for (uint256 i = 0; i < proposal.calls.length; i++) { @@ -62,17 +60,17 @@ contract ProposalsUnitTests is UnitTest { } } - function testFuzz_schedule_proposal(uint256 delay) external { - vm.assume(delay > 0 && delay < type(uint40).max); + function testFuzz_schedule_proposal(Duration delay) external { + vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); Proposals.ProposalPacked memory proposal = _proposals.proposals[0]; - uint256 submittedAt = block.timestamp; + Timestamp submittedAt = Timestamps.now(); assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, 0); - assertEq(proposal.executedAt, 0); + assertEq(proposal.scheduledAt, Timestamps.ZERO); + assertEq(proposal.executedAt, Timestamps.ZERO); uint256 proposalId = _proposals.count(); @@ -85,32 +83,32 @@ contract ProposalsUnitTests is UnitTest { proposal = _proposals.proposals[0]; assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, block.timestamp); - assertEq(proposal.executedAt, 0); + assertEq(proposal.scheduledAt, Timestamps.now()); + assertEq(proposal.executedAt, Timestamps.ZERO); } function testFuzz_cannot_schedule_unsubmitted_proposal(uint256 proposalId) external { vm.assume(proposalId > 0); vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotSubmitted.selector, proposalId)); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); } function test_cannot_schedule_proposal_twice() external { Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = 1; - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotSubmitted.selector, proposalId)); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); } - function testFuzz_cannot_schedule_proposal_before_delay_passed(uint256 delay) external { - vm.assume(delay > 0 && delay < type(uint40).max); + function testFuzz_cannot_schedule_proposal_before_delay_passed(Duration delay) external { + vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - _wait(delay - 1); + _wait(delay.minusSeconds(1 seconds)); vm.expectRevert(abi.encodeWithSelector(Proposals.AfterSubmitDelayNotPassed.selector, PROPOSAL_ID_OFFSET)); Proposals.schedule(_proposals, PROPOSAL_ID_OFFSET, delay); @@ -123,21 +121,21 @@ contract ProposalsUnitTests is UnitTest { uint256 proposalId = _proposals.count(); vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotSubmitted.selector, proposalId)); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); } - function testFuzz_execute_proposal(uint256 delay) external { - vm.assume(delay > 0 && delay < type(uint40).max); + function testFuzz_execute_proposal(Duration delay) external { + vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); - uint256 submittedAndScheduledAt = block.timestamp; + Timestamp submittedAndScheduledAt = Timestamps.now(); assertEq(_proposals.proposals[0].submittedAt, submittedAndScheduledAt); assertEq(_proposals.proposals[0].scheduledAt, submittedAndScheduledAt); - assertEq(_proposals.proposals[0].executedAt, 0); + assertEq(_proposals.proposals[0].executedAt, Timestamps.ZERO); _wait(delay); @@ -153,13 +151,13 @@ contract ProposalsUnitTests is UnitTest { assertEq(_proposals.proposals[0].submittedAt, submittedAndScheduledAt); assertEq(_proposals.proposals[0].scheduledAt, submittedAndScheduledAt); - assertEq(proposal.executedAt, block.timestamp); + assertEq(proposal.executedAt, Timestamps.now()); } function testFuzz_cannot_execute_unsubmitted_proposal(uint256 proposalId) external { vm.assume(proposalId > 0); vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); - Proposals.execute(_proposals, proposalId, 0); + Proposals.execute(_proposals, proposalId, Durations.ZERO); } function test_cannot_execute_unscheduled_proposal() external { @@ -167,36 +165,36 @@ contract ProposalsUnitTests is UnitTest { uint256 proposalId = _proposals.count(); vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); - Proposals.execute(_proposals, proposalId, 0); + Proposals.execute(_proposals, proposalId, Durations.ZERO); } function test_cannot_execute_twice() external { Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - Proposals.schedule(_proposals, proposalId, 0); - Proposals.execute(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); + Proposals.execute(_proposals, proposalId, Durations.ZERO); vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); - Proposals.execute(_proposals, proposalId, 0); + Proposals.execute(_proposals, proposalId, Durations.ZERO); } function test_cannot_execute_cancelled_proposal() external { Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); Proposals.cancelAll(_proposals); vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); - Proposals.execute(_proposals, proposalId, 0); + Proposals.execute(_proposals, proposalId, Durations.ZERO); } - function testFuzz_cannot_execute_before_delay_passed(uint256 delay) external { - vm.assume(delay > 0 && delay < type(uint40).max); + function testFuzz_cannot_execute_before_delay_passed(Duration delay) external { + vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); - _wait(delay - 1); + _wait(delay.minusSeconds(1 seconds)); vm.expectRevert(abi.encodeWithSelector(Proposals.AfterScheduleDelayNotPassed.selector, proposalId)); Proposals.execute(_proposals, proposalId, delay); @@ -208,7 +206,7 @@ contract ProposalsUnitTests is UnitTest { uint256 proposalsCount = _proposals.count(); - Proposals.schedule(_proposals, proposalsCount, 0); + Proposals.schedule(_proposals, proposalsCount, Durations.ZERO); vm.expectEmit(); emit Proposals.ProposalsCancelledTill(proposalsCount); @@ -224,13 +222,13 @@ contract ProposalsUnitTests is UnitTest { Proposal memory proposal = _proposals.get(proposalId); - uint256 submittedAt = block.timestamp; + Timestamp submittedAt = Timestamps.now(); assertEq(proposal.id, proposalId); assertEq(proposal.executor, address(_executor)); assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, 0); - assertEq(proposal.executedAt, 0); + assertEq(proposal.scheduledAt, Timestamps.ZERO); + assertEq(proposal.executedAt, Timestamps.ZERO); assertEq(proposal.calls.length, 1); assert(proposal.status == Status.Submitted); @@ -240,9 +238,9 @@ contract ProposalsUnitTests is UnitTest { assertEq(proposal.calls[i].payload, calls[i].payload); } - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); - uint256 scheduledAt = block.timestamp; + Timestamp scheduledAt = Timestamps.now(); proposal = _proposals.get(proposalId); @@ -250,7 +248,7 @@ contract ProposalsUnitTests is UnitTest { assertEq(proposal.executor, address(_executor)); assertEq(proposal.submittedAt, submittedAt); assertEq(proposal.scheduledAt, scheduledAt); - assertEq(proposal.executedAt, 0); + assertEq(proposal.executedAt, Timestamps.ZERO); assertEq(proposal.calls.length, 1); assert(proposal.status == Status.Scheduled); @@ -260,9 +258,9 @@ contract ProposalsUnitTests is UnitTest { assertEq(proposal.calls[i].payload, calls[i].payload); } - Proposals.execute(_proposals, proposalId, 0); + Proposals.execute(_proposals, proposalId, Durations.ZERO); - uint256 executedAt = block.timestamp; + Timestamp executedAt = Timestamps.now(); proposal = _proposals.get(proposalId); @@ -288,13 +286,13 @@ contract ProposalsUnitTests is UnitTest { Proposal memory proposal = _proposals.get(proposalId); - uint256 submittedAt = block.timestamp; + Timestamp submittedAt = Timestamps.now(); assertEq(proposal.id, proposalId); assertEq(proposal.executor, address(_executor)); assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, 0); - assertEq(proposal.executedAt, 0); + assertEq(proposal.scheduledAt, Timestamps.ZERO); + assertEq(proposal.executedAt, Timestamps.ZERO); assertEq(proposal.calls.length, 1); assert(proposal.status == Status.Submitted); @@ -311,8 +309,8 @@ contract ProposalsUnitTests is UnitTest { assertEq(proposal.id, proposalId); assertEq(proposal.executor, address(_executor)); assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, 0); - assertEq(proposal.executedAt, 0); + assertEq(proposal.scheduledAt, Timestamps.ZERO); + assertEq(proposal.executedAt, Timestamps.ZERO); assertEq(proposal.calls.length, 1); assert(proposal.status == Status.Cancelled); @@ -343,13 +341,13 @@ contract ProposalsUnitTests is UnitTest { Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.count(), 4); - Proposals.schedule(_proposals, 1, 0); + Proposals.schedule(_proposals, 1, Durations.ZERO); assertEq(_proposals.count(), 4); - Proposals.schedule(_proposals, 2, 0); + Proposals.schedule(_proposals, 2, Durations.ZERO); assertEq(_proposals.count(), 4); - Proposals.execute(_proposals, 1, 0); + Proposals.execute(_proposals, 1, Durations.ZERO); assertEq(_proposals.count(), 4); Proposals.cancelAll(_proposals); @@ -357,57 +355,59 @@ contract ProposalsUnitTests is UnitTest { } function test_can_execute_proposal() external { + Duration delay = Durations.from(100 seconds); Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - assert(!_proposals.canExecute(proposalId, 0)); + assert(!_proposals.canExecute(proposalId, Durations.ZERO)); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); - assert(!_proposals.canExecute(proposalId, 100)); + assert(!_proposals.canExecute(proposalId, delay)); - _wait(100); + _wait(delay); - assert(_proposals.canExecute(proposalId, 100)); + assert(_proposals.canExecute(proposalId, delay)); - Proposals.execute(_proposals, proposalId, 0); + Proposals.execute(_proposals, proposalId, Durations.ZERO); - assert(!_proposals.canExecute(proposalId, 100)); + assert(!_proposals.canExecute(proposalId, delay)); } function test_can_not_execute_cancelled_proposal() external { Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); - assert(_proposals.canExecute(proposalId, 0)); + assert(_proposals.canExecute(proposalId, Durations.ZERO)); Proposals.cancelAll(_proposals); - assert(!_proposals.canExecute(proposalId, 0)); + assert(!_proposals.canExecute(proposalId, Durations.ZERO)); } function test_can_schedule_proposal() external { + Duration delay = Durations.from(100 seconds); Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - assert(!_proposals.canSchedule(proposalId, 100)); + assert(!_proposals.canSchedule(proposalId, delay)); - _wait(100); + _wait(delay); - assert(_proposals.canSchedule(proposalId, 100)); + assert(_proposals.canSchedule(proposalId, delay)); - Proposals.schedule(_proposals, proposalId, 100); - Proposals.execute(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, delay); + Proposals.execute(_proposals, proposalId, Durations.ZERO); - assert(!_proposals.canSchedule(proposalId, 100)); + assert(!_proposals.canSchedule(proposalId, delay)); } function test_can_not_schedule_cancelled_proposal() external { Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - assert(_proposals.canSchedule(proposalId, 0)); + assert(_proposals.canSchedule(proposalId, Durations.ZERO)); Proposals.cancelAll(_proposals); - assert(!_proposals.canSchedule(proposalId, 0)); + assert(!_proposals.canSchedule(proposalId, Durations.ZERO)); } } diff --git a/test/unit/mocks/TimelockMock.sol b/test/unit/mocks/TimelockMock.sol index 63f58cc9..39838ca4 100644 --- a/test/unit/mocks/TimelockMock.sol +++ b/test/unit/mocks/TimelockMock.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Timestamp} from "contracts/types/Timestamp.sol"; import {ITimelock} from "contracts/interfaces/ITimelock.sol"; import {ExecutorCall} from "contracts/libraries/Proposals.sol"; @@ -22,13 +23,12 @@ contract TimelockMock is ITimelock { return newProposalId; } - function schedule(uint256 proposalId) external returns (uint256 submittedAt) { + function schedule(uint256 proposalId) external returns (Timestamp submittedAt) { if (canScheduleProposal[proposalId] == false) { revert(); } scheduledProposals.push(proposalId); - return 0; } function execute(uint256 proposalId) external { diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index fabbb898..b1cb9103 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.23; import {Test} from "forge-std/Test.sol"; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; +import {Durations, Duration as DurationType} from "contracts/types/Duration.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import { @@ -26,7 +28,14 @@ import {DualGovernance, DualGovernanceState, State} from "contracts/DualGovernan import {Proposal, Status as ProposalStatus} from "contracts/libraries/Proposals.sol"; import {Percents, percents} from "../utils/percents.sol"; -import {IERC20, IStEth, IWstETH, IWithdrawalQueue, WithdrawalRequestStatus, IDangerousContract} from "../utils/interfaces.sol"; +import { + IERC20, + IStEth, + IWstETH, + IWithdrawalQueue, + WithdrawalRequestStatus, + IDangerousContract +} from "../utils/interfaces.sol"; import {ExecutorCallHelpers} from "../utils/executor-calls.sol"; import {Utils, TargetMock, console} from "../utils/utils.sol"; @@ -47,15 +56,17 @@ function countDigits(uint256 number) pure returns (uint256 digitsCount) { } while (number / 10 != 0); } +DurationType constant ONE_SECOND = DurationType.wrap(1); + 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; + DurationType internal immutable _EMERGENCY_MODE_DURATION = Durations.from(180 days); + DurationType internal immutable _EMERGENCY_PROTECTION_DURATION = Durations.from(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; + DurationType internal immutable _SEALING_DURATION = Durations.from(14 days); + DurationType internal immutable _SEALING_COMMITTEE_LIFETIME = Durations.from(365 days); address internal immutable _SEALING_COMMITTEE = makeAddr("SEALING_COMMITTEE"); address internal immutable _TIEBREAK_COMMITTEE = makeAddr("TIEBREAK_COMMITTEE"); @@ -102,7 +113,25 @@ contract ScenarioTestBlueprint is Test { view returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) { - return _dualGovernance.getVetoSignallingState(); + DurationType duration_; + Timestamp activatedAt_; + Timestamp enteredAt_; + (isActive, duration_, activatedAt_, enteredAt_) = _dualGovernance.getVetoSignallingState(); + duration = DurationType.unwrap(duration_); + enteredAt = Timestamp.unwrap(enteredAt_); + activatedAt = Timestamp.unwrap(activatedAt_); + } + + function _getVetoSignallingDeactivationState() + internal + view + returns (bool isActive, uint256 duration, uint256 enteredAt) + { + Timestamp enteredAt_; + DurationType duration_; + (isActive, duration_, enteredAt_) = _dualGovernance.getVetoSignallingDeactivationState(); + duration = DurationType.unwrap(duration_); + enteredAt = Timestamp.unwrap(enteredAt_); } // --- @@ -299,8 +328,8 @@ contract ScenarioTestBlueprint is Test { assertEq(proposal.id, proposalId, "unexpected proposal id"); assertEq(uint256(proposal.status), uint256(ProposalStatus.Submitted), "unexpected status value"); assertEq(proposal.executor, executor, "unexpected executor"); - assertEq(proposal.submittedAt, block.timestamp, "unexpected scheduledAt"); - assertEq(proposal.executedAt, 0, "unexpected executedAt"); + assertEq(Timestamp.unwrap(proposal.submittedAt), block.timestamp, "unexpected scheduledAt"); + assertEq(Timestamp.unwrap(proposal.executedAt), 0, "unexpected executedAt"); assertEq(proposal.calls.length, calls.length, "unexpected calls length"); for (uint256 i = 0; i < proposal.calls.length; ++i) { @@ -405,8 +434,7 @@ contract ScenarioTestBlueprint is Test { // --- function _logVetoSignallingState() internal { /* solhint-disable no-console */ - (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) = - _dualGovernance.getVetoSignallingState(); + (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) = _getVetoSignallingState(); if (!isActive) { console.log("VetoSignalling state is not active\n"); @@ -431,7 +459,7 @@ contract ScenarioTestBlueprint is Test { function _logVetoSignallingDeactivationState() internal { /* solhint-disable no-console */ - (bool isActive, uint256 duration, uint256 enteredAt) = _dualGovernance.getVetoSignallingDeactivationState(); + (bool isActive, uint256 duration, uint256 enteredAt) = _getVetoSignallingDeactivationState(); if (!isActive) { console.log("VetoSignallingDeactivation state is not active\n"); @@ -552,16 +580,16 @@ contract ScenarioTestBlueprint is Test { console.log(string.concat(">>> ", text, " <<<")); } - function _wait(uint256 duration) internal { - vm.warp(block.timestamp + duration); + function _wait(DurationType duration) internal { + vm.warp(duration.addTo(Timestamps.now()).toSeconds()); } function _waitAfterSubmitDelayPassed() internal { - _wait(_config.AFTER_SUBMIT_DELAY() + 1); + _wait(_config.AFTER_SUBMIT_DELAY() + ONE_SECOND); } function _waitAfterScheduleDelayPassed() internal { - _wait(_config.AFTER_SCHEDULE_DELAY() + 1); + _wait(_config.AFTER_SCHEDULE_DELAY() + ONE_SECOND); } struct Duration { @@ -594,6 +622,18 @@ contract ScenarioTestBlueprint is Test { ); } + function assertEq(uint40 a, uint40 b) internal { + assertEq(uint256(a), uint256(b)); + } + + function assertEq(Timestamp a, Timestamp b) internal { + assertEq(uint256(Timestamp.unwrap(a)), uint256(Timestamp.unwrap(b))); + } + + function assertEq(DurationType a, DurationType b) internal { + assertEq(uint256(DurationType.unwrap(a)), uint256(DurationType.unwrap(b))); + } + function assertEq(ProposalStatus a, ProposalStatus b) internal { assertEq(uint256(a), uint256(b)); } diff --git a/test/utils/unit-test.sol b/test/utils/unit-test.sol index 6ff279a4..3ec9b59c 100644 --- a/test/utils/unit-test.sol +++ b/test/utils/unit-test.sol @@ -1,18 +1,29 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; +import {Duration, Durations} from "contracts/types/Duration.sol"; + // solhint-disable-next-line -import {Test} from "forge-std/Test.sol"; +import {Test, console} from "forge-std/Test.sol"; import {ExecutorCall} from "contracts/libraries/Proposals.sol"; import {ExecutorCallHelpers} from "test/utils/executor-calls.sol"; import {IDangerousContract} from "test/utils/interfaces.sol"; contract UnitTest is Test { - function _wait(uint256 duration) internal { - vm.warp(block.timestamp + duration); + function _wait(Duration duration) internal { + vm.warp(block.timestamp + Duration.unwrap(duration)); } function _getTargetRegularStaffCalls(address targetMock) internal pure returns (ExecutorCall[] memory) { return ExecutorCallHelpers.create(address(targetMock), abi.encodeCall(IDangerousContract.doRegularStaff, (42))); } -} \ No newline at end of file + + function assertEq(Timestamp a, Timestamp b) internal { + assertEq(uint256(Timestamp.unwrap(a)), uint256(Timestamp.unwrap(b))); + } + + function assertEq(Duration a, Duration b) internal { + assertEq(uint256(Duration.unwrap(a)), uint256(Duration.unwrap(b))); + } +} From cf183f930223734a67cf7871f4d2b359f543a0f0 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Tue, 25 Jun 2024 12:15:17 +0300 Subject: [PATCH 110/134] 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 111/134] 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 112/134] 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 113/134] 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 1a5e43abbd1fa652f02f38553ddfea8ed8837ccb Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 25 Jun 2024 14:44:40 +0400 Subject: [PATCH 114/134] Use unstETH as Withdrawal NFT shortcut instead of wNFT --- docs/mechanism.md | 3 +++ docs/specification.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index f6e23f4a..ea805dc6 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -395,6 +395,9 @@ Dual governance should not cover: ## Changelog +### 2024-06-25 +- Instead of using the "wNFT" shortcut for the "Lido: stETH Withdrawal NFT" token, the official symbol "unstETH" is now used. + ### 2024-04-24 * Removed the logic with the extension of the Veto Signalling duration upon new proposal submission. diff --git a/docs/specification.md b/docs/specification.md index dc43cc78..51199dd6 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -64,7 +64,7 @@ The general proposal flow is the following: Each submitted proposal requires a minimum timelock before it can be scheduled for execution. -At any time, including while a proposal's timelock is lasting, stakers can signal their opposition to the DAO by locking their (w)stETH or withdrawal NFTs (wNFTs) into the [signalling escrow contract](#Contract-Escrowsol). If the opposition exceeds some minimum threshold, the [global governance state](#Governance-state) gets changed, blocking any DAO execution and thus effectively extending the timelock of all pending (i.e. submitted but not scheduled for execution) proposals. +At any time, including while a proposal's timelock is lasting, stakers can signal their opposition to the DAO by locking their (w)stETH or stETH Withdrawal NFTs (unstETH) into the [signalling escrow contract](#Contract-Escrowsol). If the opposition exceeds some minimum threshold, the [global governance state](#Governance-state) gets changed, blocking any DAO execution and thus effectively extending the timelock of all pending (i.e. submitted but not scheduled for execution) proposals. ![image](https://github.com/lidofinance/dual-governance/assets/1699593/98273df0-f3fd-4149-929d-3315a8e81aa8) From de758a89074a65acb8ff841eee9919c6b357dcda Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 25 Jun 2024 22:52:54 +0400 Subject: [PATCH 115/134] Use timelock instead of delay with emergency protection. Note config's constants --- docs/specification.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/specification.md b/docs/specification.md index 51199dd6..de31e0c8 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -86,11 +86,11 @@ The proposal execution flow comes after the dynamic timelock elapses and the pro #### Regular deployment mode -In the regular deployment mode, the emergency protection delay is set to zero and all calls from scheduled proposals are immediately executable by anyone via calling the [`EmergencyProtectedTimelock.execute`](#Function-EmergencyProtectedTimelockexecute) function. +In the regular deployment mode, the **emergency protection delay** is set to zero and all calls from scheduled proposals are immediately executable by anyone via calling the [`EmergencyProtectedTimelock.execute`](#Function-EmergencyProtectedTimelockexecute) function. #### Protected deployment mode -The protected deployment mode is a temporary mode designed to be active during an initial period after the deployment or upgrade of the DG contracts. In this mode, scheduled proposals cannot be executed immediately; instead, before calling [`EmergencyProtectedTimelock.execute`](#Funtion-EmergencyProtectedTimelockexecute), one has to wait until an **emergency protection timelock** elapses since the proposal scheduling time. +The protected deployment mode is a temporary mode designed to be active during an initial period after the deployment or upgrade of the DG contracts. In this mode, scheduled proposals cannot be executed immediately; instead, before calling [`EmergencyProtectedTimelock.execute`](#Funtion-EmergencyProtectedTimelockexecute), one has to wait until an emergency protection delay elapses since the proposal scheduling time. ![image](https://github.com/lidofinance/dual-governance/assets/1699593/38cb2371-bdb0-4681-9dfd-356fa1ed7959) @@ -373,7 +373,7 @@ Registers the `proposer` address in the system as a valid proposer and associate #### Preconditions -* MUST be called by the admin executor contract (see `Config.sol`). +* MUST be called by the admin executor contract (see `Configuration.sol`). * The `proposer` address MUST NOT be already registered in the system. * The `executor` instance SHOULD be owned by the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance. @@ -837,14 +837,14 @@ Allows the caller (i.e. `msg.sender`) to withdraw the claimed ETH from the Withd For a proposal to be executed, the following steps have to be performed in order: 1. The proposal must be submitted using the `EmergencyProtectedTimelock.submit` function. -2. The configured post-submit timelock must elapse. +2. The configured post-submit timelock (`Configuration.AFTER_SUBMIT_DELAY()`) must elapse. 3. The proposal must be scheduled using the `EmergencyProtectedTimelock.schedule` function. -4. The configured emergency protection timelock must elapse (can be zero, see below). +4. The configured emergency protection delay (`Configuration.AFTER_SCHEDULE_DELAY()`) must elapse (can be zero, see below). 5. The proposal must be executed using the `EmergencyProtectedTimelock.execute` function. 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 delay 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. 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 a57cf8cbfcfafd5c63aacf589f58b51f778dc626 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 25 Jun 2024 22:53:57 +0400 Subject: [PATCH 116/134] Rage Quit withdrawals clarification --- docs/mechanism.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index ea805dc6..adba5e1f 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -271,7 +271,7 @@ The Rage Quit state allows all stakers who elected to leave the protocol via rag Upon entry into the Rage Quit state, three things happen: 1. The veto signalling escrow is irreversibly transformed into the **rage quit escrow**, an immutable smart contract that holds all tokens that are part of the rage quit withdrawal process, i.e. stETH, wstETH, withdrawal NFTs, and the withdrawn ETH, and allows stakers to claim the withdrawn ETH after a certain timelock following the completion of the withdrawal process (with the timelock being determined at the moment of the Rage Quit state entry). -2. All stETH and wstETH held by the rage quit escrow are sent for withdrawal via the regular Lido Withdrawal Queue mechanism, generating a set of batch withdrawal NFTs held by the rage quit escrow. +2. All stETH and wstETH held by the rage quit escrow will be processed for withdrawals through the regular Lido Withdrawal Queue mechanism, generating a set of batch withdrawal NFTs held by the rage quit escrow. 3. A new instance of the veto signalling escrow smart contract is deployed. This way, at any point in time, there is only one veto signalling escrow but there may be multiple rage quit escrows from previous rage quits. In this state, the DAO is allowed to submit proposals to the DG but cannot execute any pending proposals. Stakers are not allowed to lock (w)stETH or withdrawal NFTs into the rage quit escrow so joining the ongoing rage quit is not possible. However, they can lock their tokens that are not part of the ongoing rage quit process to the newly-deployed veto signalling escrow to potentially trigger a new rage quit later. From ed3e9e4f64b5a252e0ee268b2610425cc47fb30c Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 26 Jun 2024 01:36:54 +0400 Subject: [PATCH 117/134] Rename params RageQuitEthClaim* -> RageQuitEthWithdrawals* --- contracts/Configuration.sol | 23 +++++++++++---------- contracts/interfaces/IConfiguration.sol | 17 +++++++-------- contracts/libraries/DualGovernanceState.sol | 12 +++++------ docs/mechanism.md | 13 ++++++------ docs/specification.md | 18 ++++++++-------- 5 files changed, 42 insertions(+), 41 deletions(-) diff --git a/contracts/Configuration.sol b/contracts/Configuration.sol index 44cfc9bd..9aa03156 100644 --- a/contracts/Configuration.sol +++ b/contracts/Configuration.sol @@ -24,12 +24,12 @@ contract Configuration is IConfiguration { uint256 public immutable VETO_COOLDOWN_DURATION = 4 days; uint256 public immutable RAGE_QUIT_EXTENSION_DELAY = 7 days; - uint256 public immutable RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK = 60 days; - uint256 public immutable RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_START_SEQ_NUMBER = 2; + uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK = 60 days; + uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER = 2; - uint256 public immutable RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_A = 0; - uint256 public immutable RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_B = 0; - uint256 public immutable RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_C = 0; + uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_A = 0; + uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_B = 0; + uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C = 0; // --- address public immutable ADMIN_EXECUTOR; @@ -103,12 +103,13 @@ contract Configuration is IConfiguration { config.vetoSignallingDeactivationMaxDuration = VETO_SIGNALLING_DEACTIVATION_MAX_DURATION; config.vetoCooldownDuration = VETO_COOLDOWN_DURATION; config.rageQuitExtensionDelay = RAGE_QUIT_EXTENSION_DELAY; - config.rageQuitEthClaimMinTimelock = RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK; - config.rageQuitEthClaimTimelockGrowthStartSeqNumber = RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_START_SEQ_NUMBER; - config.rageQuitEthClaimTimelockGrowthCoeffs = [ - RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_A, - RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_B, - RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_C + config.rageQuitEthWithdrawalsMinTimelock = RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK; + config.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber = + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER; + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs = [ + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_A, + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_B, + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C ]; } } diff --git a/contracts/interfaces/IConfiguration.sol b/contracts/interfaces/IConfiguration.sol index 04aa9f14..e307f21d 100644 --- a/contracts/interfaces/IConfiguration.sol +++ b/contracts/interfaces/IConfiguration.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.23; struct DualGovernanceConfig { uint256 firstSealRageQuitSupport; uint256 secondSealRageQuitSupport; - // TODO: consider dynamicDelayMaxDuration uint256 dynamicTimelockMaxDuration; uint256 dynamicTimelockMinDuration; uint256 vetoSignallingMinActiveDuration; @@ -12,9 +11,9 @@ struct DualGovernanceConfig { uint256 vetoCooldownDuration; uint256 rageQuitExtraTimelock; uint256 rageQuitExtensionDelay; - uint256 rageQuitEthClaimMinTimelock; - uint256 rageQuitEthClaimTimelockGrowthStartSeqNumber; - uint256[3] rageQuitEthClaimTimelockGrowthCoeffs; + uint256 rageQuitEthWithdrawalsMinTimelock; + uint256 rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber; + uint256[3] rageQuitEthWithdrawalsTimelockGrowthCoeffs; } interface IAdminExecutorConfiguration { @@ -42,13 +41,13 @@ interface IDualGovernanceConfiguration { function SECOND_SEAL_RAGE_QUIT_SUPPORT() external view returns (uint256); function RAGE_QUIT_EXTENSION_DELAY() external view returns (uint256); - function RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK() external view returns (uint256); + function RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK() external view returns (uint256); function RAGE_QUIT_ACCUMULATION_MAX_DURATION() external view returns (uint256); - function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_START_SEQ_NUMBER() external view returns (uint256); + function RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER() external view returns (uint256); - function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_A() external view returns (uint256); - function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_B() external view returns (uint256); - function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_C() external view returns (uint256); + function RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_A() external view returns (uint256); + function RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_B() external view returns (uint256); + function RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C() external view returns (uint256); function SIGNALLING_ESCROW_MIN_LOCK_TIME() external view returns (uint256); diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol index b55499f4..084bbe97 100644 --- a/contracts/libraries/DualGovernanceState.sol +++ b/contracts/libraries/DualGovernanceState.sol @@ -345,14 +345,14 @@ library DualGovernanceState { DualGovernanceConfig memory config, uint256 rageQuitRound ) private pure returns (uint256) { - if (rageQuitRound < config.rageQuitEthClaimTimelockGrowthStartSeqNumber) { - return config.rageQuitEthClaimMinTimelock; + if (rageQuitRound < config.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber) { + return config.rageQuitEthWithdrawalsMinTimelock; } - return config.rageQuitEthClaimMinTimelock + return config.rageQuitEthWithdrawalsMinTimelock + ( - config.rageQuitEthClaimTimelockGrowthCoeffs[0] * rageQuitRound * rageQuitRound - + config.rageQuitEthClaimTimelockGrowthCoeffs[1] * rageQuitRound - + config.rageQuitEthClaimTimelockGrowthCoeffs[2] + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[0] * rageQuitRound * rageQuitRound + + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[1] * rageQuitRound + + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[2] ) / 10 ** 18; // TODO: rewrite in a prettier way } diff --git a/docs/mechanism.md b/docs/mechanism.md index adba5e1f..af81e266 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -270,7 +270,7 @@ The Rage Quit state allows all stakers who elected to leave the protocol via rag Upon entry into the Rage Quit state, three things happen: -1. The veto signalling escrow is irreversibly transformed into the **rage quit escrow**, an immutable smart contract that holds all tokens that are part of the rage quit withdrawal process, i.e. stETH, wstETH, withdrawal NFTs, and the withdrawn ETH, and allows stakers to claim the withdrawn ETH after a certain timelock following the completion of the withdrawal process (with the timelock being determined at the moment of the Rage Quit state entry). +1. The veto signalling escrow is irreversibly transformed into the **rage quit escrow**, an immutable smart contract that holds all tokens that are part of the rage quit withdrawal process, i.e. stETH, wstETH, withdrawal NFTs, and the withdrawn ETH, and allows stakers to retrieve the withdrawn ETH after a certain timelock following the completion of the withdrawal process (with the timelock being determined at the moment of the Rage Quit state entry). 2. All stETH and wstETH held by the rage quit escrow will be processed for withdrawals through the regular Lido Withdrawal Queue mechanism, generating a set of batch withdrawal NFTs held by the rage quit escrow. 3. A new instance of the veto signalling escrow smart contract is deployed. This way, at any point in time, there is only one veto signalling escrow but there may be multiple rage quit escrows from previous rage quits. @@ -291,7 +291,7 @@ When the withdrawal is complete and the extension delay elapses, two things happ **Transition to Veto Cooldown**. If, at the moment of the Rage Quit state exit, $R(t) \leq R_1$, the Veto Cooldown state is entered. -The duration of the ETH claim timelock $W(i)$ is a non-linear function that depends on the rage quit sequence number $i$ (see below): +The duration of the ETH withdraw timelock $W(i)$ is a non-linear function that depends on the rage quit sequence number $i$ (see below): ```math W(i) = W_{min} + @@ -301,16 +301,16 @@ W(i) = W_{min} + \end{array} \right. ``` -where $W_{min}$ is `RageQuitEthClaimMinTimelock`, $i_{min}$ is `RageQuitEthClaimTimelockGrowthStartSeqNumber`, and $g_W(x)$ is a quadratic polynomial function with coefficients `RageQuitEthClaimTimelockGrowthCoeffs` (a list of length 3). +where $W_{min}$ is `RageQuitEthWithdrawalsMinTimelock`, $i_{min}$ is `RageQuitEthWithdrawalsTimelockGrowthStartSeqNumber`, and $g_W(x)$ is a quadratic polynomial function with coefficients `RageQuitEthWithdrawalsTimelockGrowthCoeffs` (a list of length 3). The rage quit sequence number is calculated as follows: each time the Normal state is entered, the sequence number is set to 0; each time the Rage Quit state is entered, the number is incremented by 1. ```env # Proposed values, to be modeled and refined RageQuitExtensionDelay = 7 days -RageQuitEthClaimMinTimelock = 60 days -RageQuitEthClaimTimelockGrowthStartSeqNumber = 2 -RageQuitEthClaimTimelockGrowthCoeffs = (0, TODO, TODO) +RageQuitEthWithdrawalsMinTimelock = 60 days +RageQuitEthWithdrawalsTimelockGrowthStartSeqNumber = 2 +RageQuitEthWithdrawalsTimelockGrowthCoeffs = (0, TODO, TODO) ``` @@ -397,6 +397,7 @@ Dual governance should not cover: ### 2024-06-25 - Instead of using the "wNFT" shortcut for the "Lido: stETH Withdrawal NFT" token, the official symbol "unstETH" is now used. +- For the consistency with the codebase, the `RageQuitEthClaimMinTimelock`, `RageQuitEthClaimTimelockGrowthStartSeqNumber`, `RageQuitEthClaimTimelockGrowthCoeffs` parameters were renamed into `RageQuitEthWithdrawalsMinTimelock`, `RageQuitEthWithdrawalsTimelockGrowthStartSeqNumber`, `RageQuitEthWithdrawalsTimelockGrowthCoeffs`. ### 2024-04-24 diff --git a/docs/specification.md b/docs/specification.md index de31e0c8..8152a4fd 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -96,7 +96,7 @@ The protected deployment mode is a temporary mode designed to be active during a In this mode, an **emergency activation committee** has the one-off and time-limited right to activate an adversarial **emergency mode** if they see a scheduled proposal that was created or altered due to a vulnerability in the DG contracts or if governance execution is prevented by such a vulnerability. Once the emergency mode is activated, the emergency activation committee is disabled, i.e. loses the ability to activate the emergency mode again. If the emergency activation committee doesn't activate the emergency mode within the duration of the **emergency protection duration** since the committee was configured by the DAO, it gets automatically disabled as well. -The emergency mode lasts up to the **emergency mode max duration** counting from the moment of its activation. While it's active, 1) only the **emergency execution committee** has the right to execute scheduled proposals, and 2) the same committee has the one-off right to **disable the DG subsystem**, i.e. disconnect executor contracts from the DG contracts and reconnect them to the Lido DAO Voting/Agent contract. The latter also disables the emergency mode and the emergency execution committee, so any proposal can be executed by the DAO without cooperation from any other actors. +The emergency mode lasts up to the **emergency mode max duration** counting from the moment of its activation. While it's active, 1) only the **emergency execution committee** has the right to execute scheduled proposals, and 2) the same committee has the one-off right to **disable the DG subsystem**, i.e. disconnect the `EmergencyProtectedTimelock` contract and its associated executor contracts from the DG contracts and reconnect it to the Lido DAO Voting/Agent contract. The latter also disables the emergency mode and the emergency execution committee, so any proposal can be executed by the DAO without cooperation from any other actors. If the emergency execution committee doesn't disable the DG until the emergency mode max duration elapses, anyone gets the right to deactivate the emergency mode, switching the system back to the protected mode and disabling the emergency committee. @@ -462,9 +462,9 @@ Once all funds locked in the `Escrow` instance are converted into withdrawal NFT The purpose of the `RageQuitExtensionDelay` phase is to provide sufficient time to participants who locked withdrawal NFTs to claim them before Lido DAO's proposal execution is unblocked. As soon as a withdrawal NFT is claimed, the user's ETH is no longer affected by any code controlled by the DAO. -When the `RageQuitExtensionDelay` period elapses, the `DualGovernance.activateNextState()` function exits the `RageQuit` state and initiates the `RageQuitEthClaimTimelock`. Throughout this timelock, tokens remain locked within the `Escrow` instance and are inaccessible for withdrawal. Once the timelock expires, participants in the rage quit process can retrieve their ETH by withdrawing it from the `Escrow` instance. +When the `RageQuitExtensionDelay` period elapses, the `DualGovernance.activateNextState()` function exits the `RageQuit` state and initiates the `RageQuitEthWithdrawalsTimelock`. Throughout this timelock, tokens remain locked within the `Escrow` instance and are inaccessible for withdrawal. Once the timelock expires, participants in the rage quit process can retrieve their ETH by withdrawing it from the `Escrow` instance. -The duration of the `RageQuitEthClaimTimelock` is dynamic and varies based on the number of "continuous" rage quits. A pair of rage quits is considered continuous when `DualGovernance` has not transitioned to the `Normal` or `VetoCooldown` state between them. +The duration of the `RageQuitEthWithdrawalsTimelock` is dynamic and varies based on the number of "continuous" rage quits. A pair of rage quits is considered continuous when `DualGovernance` has not transitioned to the `Normal` or `VetoCooldown` state between them. ### Function: Escrow.lockStETH @@ -701,7 +701,7 @@ return 10 ** 18 * ( function startRageQuit() ``` -Transits the `Escrow` instance from the `SignallingEscrow` state to the `RageQuitEscrow` state. Following this transition, locked funds become unwithdrawable and are accessible to users only as plain ETH after the completion of the full `RageQuit` process, including the `RageQuitExtensionDelay` and `RageQuitEthClaimTimelock` stages. +Transits the `Escrow` instance from the `SignallingEscrow` state to the `RageQuitEscrow` state. Following this transition, locked funds become unwithdrawable and are accessible to users only as plain ETH after the completion of the full `RageQuit` process, including the `RageQuitExtensionDelay` and `RageQuitEthWithdrawalsTimelock` stages. As the initial step of transitioning to the `RageQuitEscrow` state, all locked wstETH is converted into stETH, and the maximum stETH allowance is granted to the `WithdrawalQueue` contract for the upcoming creation of Withdrawal NFTs. @@ -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 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 `RageQuitEthWithdrawalsTimelock` 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: @@ -785,7 +785,7 @@ return _totalClaimedEthAmount * _vetoersLockedAssets[msg.sender].stETHShares - The `Escrow` instance MUST be in the `RageQuitEscrow` state. - The rage quit process MUST be completed, including the expiration of the `RageQuitExtensionDelay` duration. -- The `RageQuitEthClaimTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. +- The `RageQuitEthWithdrawalsTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. - The caller MUST have a non-zero amount of stETH to withdraw. - The caller MUST NOT have previously withdrawn stETH. @@ -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 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 `RageQuitEthWithdrawalsTimelock` 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: @@ -808,7 +808,7 @@ return _totalClaimedEthAmount * #### Preconditions - The `Escrow` instance MUST be in the `RageQuitEscrow` state. - The rage quit process MUST be completed, including the expiration of the `RageQuitExtensionDelay` duration. -- The `RageQuitEthClaimTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. +- The `RageQuitEthWithdrawalsTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. - The caller MUST have a non-zero amount of wstETH to withdraw. - The caller MUST NOT have previously withdrawn wstETH. @@ -824,7 +824,7 @@ Allows the caller (i.e. `msg.sender`) to withdraw the claimed ETH from the Withd - The `Escrow` instance MUST be in the `RageQuitEscrow` state. - The rage quit process MUST be completed, including the expiration of the `RageQuitExtensionDelay` duration. -- The `RageQuitEthClaimTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. +- The `RageQuitEthWithdrawalsTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. - The caller MUST be set as the owner of the provided NFTs. - Each Withdrawal NFT MUST have been claimed using the `Escrow.claimUnstETH()` function. - Withdrawal NFTs must not have been withdrawn previously. From d3a87da919f3ed71ea3fc75b82a10ca77b666eb3 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Tue, 2 Jul 2024 14:46:44 +0300 Subject: [PATCH 118/134] 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 9d0f355abf2e015896f86844dddfa8609bcd22cb Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 2 Jul 2024 15:04:40 +0300 Subject: [PATCH 119/134] refactor: new proposals getter --- contracts/DualGovernance.sol | 6 +++++- contracts/EmergencyProtectedTimelock.sol | 8 ++++++-- contracts/interfaces/ITimelock.sol | 4 +++- contracts/libraries/Proposals.sol | 10 ++++++++-- test/unit/EmergencyProtectedTimelock.t.sol | 9 +++++++++ test/unit/mocks/TimelockMock.sol | 7 +++++-- 6 files changed, 36 insertions(+), 8 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 3a6c7a5a..8223330b 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -49,8 +49,12 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function scheduleProposal(uint256 proposalId) external { _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); - uint256 proposalSubmissionTime = TIMELOCK.schedule(proposalId); + + uint256 proposalSubmissionTime = TIMELOCK.getProposalSubmissionTime(proposalId); _dgState.checkCanScheduleProposal(proposalSubmissionTime); + + TIMELOCK.schedule(proposalId); + emit ProposalScheduled(proposalId); } diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 019feb72..0f9ca933 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -30,9 +30,9 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { newProposalId = _proposals.submit(executor, calls); } - function schedule(uint256 proposalId) external returns (uint256 submittedAt) { + function schedule(uint256 proposalId) external { _checkGovernance(msg.sender); - submittedAt = _proposals.schedule(proposalId, CONFIG.AFTER_SUBMIT_DELAY()); + _proposals.schedule(proposalId, CONFIG.AFTER_SUBMIT_DELAY()); } function execute(uint256 proposalId) external { @@ -122,6 +122,10 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { count = _proposals.count(); } + function getProposalSubmissionTime(uint256 proposalId) external view returns (uint256 submittedAt) { + submittedAt = _proposals.getProposalSubmissionTime(proposalId); + } + // --- // Proposals Lifecycle View Methods // --- diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index 0f080e8d..bf8cbae7 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -13,10 +13,12 @@ interface IGovernance { interface ITimelock { function submit(address executor, ExecutorCall[] calldata calls) external returns (uint256 newProposalId); - function schedule(uint256 proposalId) external returns (uint256 submittedAt); + function schedule(uint256 proposalId) external; function execute(uint256 proposalId) external; function cancelAllNonExecutedProposals() external; function canSchedule(uint256 proposalId) external view returns (bool); function canExecute(uint256 proposalId) external view returns (bool); + + function getProposalSubmissionTime(uint256 proposalId) external view returns (uint256 submittedAt); } diff --git a/contracts/libraries/Proposals.sol b/contracts/libraries/Proposals.sol index e404a93f..56a8a589 100644 --- a/contracts/libraries/Proposals.sol +++ b/contracts/libraries/Proposals.sol @@ -85,12 +85,11 @@ library Proposals { State storage self, uint256 proposalId, uint256 afterSubmitDelay - ) internal returns (uint256 submittedAt) { + ) internal { _checkProposalSubmitted(self, proposalId); _checkAfterSubmitDelayPassed(self, proposalId, afterSubmitDelay); ProposalPacked storage proposal = _packed(self, proposalId); - submittedAt = proposal.submittedAt; proposal.scheduledAt = TimeUtils.timestamp(); emit ProposalScheduled(proposalId); @@ -121,6 +120,13 @@ library Proposals { proposal.calls = packed.calls; } + function getProposalSubmissionTime(State storage self, uint256 proposalId) internal view returns (uint256 submittedAt) { + _checkProposalExists(self, proposalId); + ProposalPacked storage packed = _packed(self, proposalId); + + submittedAt = packed.submittedAt; + } + function count(State storage self) internal view returns (uint256 count_) { count_ = self.proposals.length; } diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 7216c248..987f8fb5 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -825,6 +825,15 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.canSchedule(1), false); } + // EmergencyProtectedTimelock.getProposalSubmissionTime() + + function test_get_proposal_submission_time() external { + _submitProposal(); + uint256 submitTimestamp = block.timestamp; + + assertEq(_timelock.getProposalSubmissionTime(1), submitTimestamp); + } + // Utils function _submitProposal() internal { diff --git a/test/unit/mocks/TimelockMock.sol b/test/unit/mocks/TimelockMock.sol index 63f58cc9..467a02b1 100644 --- a/test/unit/mocks/TimelockMock.sol +++ b/test/unit/mocks/TimelockMock.sol @@ -22,13 +22,12 @@ contract TimelockMock is ITimelock { return newProposalId; } - function schedule(uint256 proposalId) external returns (uint256 submittedAt) { + function schedule(uint256 proposalId) external { if (canScheduleProposal[proposalId] == false) { revert(); } scheduledProposals.push(proposalId); - return 0; } function execute(uint256 proposalId) external { @@ -66,4 +65,8 @@ contract TimelockMock is ITimelock { function getLastCancelledProposalId() external view returns (uint256) { return lastCancelledProposalId; } + + function getProposalSubmissionTime(uint256 proposalId) external view returns (uint256 submittedAt) { + revert("Not Implemented"); + } } From 4ca353613959858f1664ab5e6848758fdaa148c6 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 3 Jul 2024 13:19:44 +0400 Subject: [PATCH 120/134] AssetsAccounting refactoring --- contracts/Escrow.sol | 202 ++++-- contracts/interfaces/IWithdrawalQueue.sol | 7 + contracts/libraries/AssetsAccounting.sol | 618 +++++++----------- .../libraries/WithdrawalBatchesQueue.sol | 179 +++++ contracts/types/ETHValue.sol | 59 ++ contracts/types/IndexOneBased.sol | 32 + contracts/types/SharesValue.sol | 50 ++ test/scenario/escrow.t.sol | 91 +-- test/scenario/veto-cooldown-mechanics.t.sol | 16 +- test/utils/scenario-test-blueprint.sol | 39 +- 10 files changed, 787 insertions(+), 506 deletions(-) create mode 100644 contracts/libraries/WithdrawalBatchesQueue.sol create mode 100644 contracts/types/ETHValue.sol create mode 100644 contracts/types/IndexOneBased.sol create mode 100644 contracts/types/SharesValue.sol diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 7b967e9b..97aeafef 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.23; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {IEscrow} from "./interfaces/IEscrow.sol"; import {IConfiguration} from "./interfaces/IConfiguration.sol"; @@ -11,9 +10,17 @@ import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; import {IWithdrawalQueue, WithdrawalRequestStatus} from "./interfaces/IWithdrawalQueue.sol"; -import {AssetsAccounting, LockedAssetsStats, LockedAssetsTotals} from "./libraries/AssetsAccounting.sol"; - -import {ArrayUtils} from "./utils/arrays.sol"; +import { + ETHValue, + ETHValues, + SharesValue, + SharesValues, + HolderAssets, + StETHAccounting, + UnstETHAccounting, + AssetsAccounting +} from "./libraries/AssetsAccounting.sol"; +import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalBatchesQueue.sol"; interface IDualGovernance { function activateNextState() external; @@ -25,16 +32,27 @@ enum EscrowState { RageQuitEscrow } +struct LockedAssetsTotals { + uint256 stETHLockedShares; + uint256 stETHClaimedETH; + uint256 unstETHUnfinalizedShares; + uint256 unstETHFinalizedETH; +} + struct VetoerState { - uint256 stETHShares; - uint256 unstETHShares; + uint256 stETHLockedShares; + uint256 unstETHLockedShares; + uint256 unstETHIdsCount; + uint256 lastAssetsLockTimestamp; } contract Escrow is IEscrow { using AssetsAccounting for AssetsAccounting.State; + using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.State; error EmptyBatch(); error ZeroWithdraw(); + error InvalidBatchSize(uint256 size); error WithdrawalsTimelockNotPassed(); error InvalidETHSender(address actual, address expected); error NotDualGovernance(address actual, address expected); @@ -43,6 +61,13 @@ contract Escrow is IEscrow { error InvalidState(EscrowState actual, EscrowState expected); error RageQuitExtraTimelockNotStarted(); + // TODO: move to config + uint256 public immutable MIN_BATCH_SIZE = 8; + uint256 public immutable MAX_BATCH_SIZE = 128; + + uint256 public immutable MIN_WITHDRAWAL_REQUEST_AMOUNT; + uint256 public immutable MAX_WITHDRAWAL_REQUEST_AMOUNT; + uint256 public immutable RAGE_QUIT_TIMELOCK = 30 days; address public immutable MASTER_COPY; @@ -55,6 +80,7 @@ contract Escrow is IEscrow { EscrowState internal _escrowState; IDualGovernance private _dualGovernance; AssetsAccounting.State private _accounting; + WithdrawalsBatchesQueue.State private _batchesQueue; uint256[] internal _withdrawalUnstETHIds; @@ -65,9 +91,11 @@ contract Escrow is IEscrow { constructor(address stETH, address wstETH, address withdrawalQueue, address config) { ST_ETH = IStETH(stETH); WST_ETH = IWstETH(wstETH); - WITHDRAWAL_QUEUE = IWithdrawalQueue(withdrawalQueue); MASTER_COPY = address(this); CONFIG = IConfiguration(config); + WITHDRAWAL_QUEUE = IWithdrawalQueue(withdrawalQueue); + MIN_WITHDRAWAL_REQUEST_AMOUNT = WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT(); + MAX_WITHDRAWAL_REQUEST_AMOUNT = WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); } function initialize(address dualGovernance) external { @@ -90,15 +118,15 @@ contract Escrow is IEscrow { function lockStETH(uint256 amount) external { uint256 shares = ST_ETH.getSharesByPooledEth(amount); - _accounting.accountStETHSharesLock(msg.sender, shares); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(shares)); ST_ETH.transferSharesFrom(msg.sender, address(this), shares); _activateNextGovernanceState(); } function unlockStETH() external { _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); - uint256 sharesUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); - ST_ETH.transferShares(msg.sender, sharesUnlocked); + SharesValue sharesUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); + ST_ETH.transferShares(msg.sender, sharesUnlocked.toUint256()); _activateNextGovernanceState(); } @@ -109,16 +137,17 @@ contract Escrow is IEscrow { function lockWstETH(uint256 amount) external { WST_ETH.transferFrom(msg.sender, address(this), amount); uint256 stETHAmount = WST_ETH.unwrap(amount); - _accounting.accountStETHSharesLock(msg.sender, ST_ETH.getSharesByPooledEth(stETHAmount)); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(ST_ETH.getSharesByPooledEth(stETHAmount))); _activateNextGovernanceState(); } - function unlockWstETH() external returns (uint256 wstETHUnlocked) { + function unlockWstETH() external returns (uint256) { _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); - wstETHUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); - uint256 wstETHAmount = WST_ETH.wrap(ST_ETH.getPooledEthByShares(wstETHUnlocked)); + SharesValue wstETHUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); + uint256 wstETHAmount = WST_ETH.wrap(ST_ETH.getPooledEthByShares(wstETHUnlocked.toUint256())); WST_ETH.transfer(msg.sender, wstETHAmount); _activateNextGovernanceState(); + return wstETHAmount; } // --- @@ -135,7 +164,8 @@ contract Escrow is IEscrow { } function unlockUnstETH(uint256[] memory unstETHIds) external { - _accounting.accountUnstETHUnlock(CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME(), msg.sender, unstETHIds); + _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); + _accounting.accountUnstETHUnlock(msg.sender, unstETHIds); uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { WITHDRAWAL_QUEUE.transferFrom(address(this), msg.sender, unstETHIds[i]); @@ -161,7 +191,7 @@ contract Escrow is IEscrow { for (uint256 i = 0; i < statuses.length; ++i) { sharesTotal += statuses[i].amountOfShares; } - _accounting.accountStETHSharesUnlock(msg.sender, sharesTotal); + _accounting.accountStETHSharesUnlock(msg.sender, SharesValues.from(sharesTotal)); _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); } @@ -184,31 +214,67 @@ contract Escrow is IEscrow { ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); } - function requestNextWithdrawalsBatch(uint256 maxWithdrawalRequestsCount) external { - _checkEscrowState(EscrowState.RageQuitEscrow); + function requestNextWithdrawalsBatch(uint256 maxBatchSize) external { + _batchesQueue.checkNotFinalized(); - uint256[] memory requestAmounts = _accounting.formWithdrawalBatch( - WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT(), - WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(), - ST_ETH.balanceOf(address(this)), - maxWithdrawalRequestsCount - ); - uint256[] memory unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(requestAmounts, address(this)); - _accounting.accountWithdrawalBatch(unstETHIds); + if (maxBatchSize < MIN_BATCH_SIZE || maxBatchSize > MAX_BATCH_SIZE) { + revert InvalidBatchSize(maxBatchSize); + } + + uint256 stETHRemaining = ST_ETH.balanceOf(address(this)); + if (stETHRemaining < MIN_WITHDRAWAL_REQUEST_AMOUNT) { + return _batchesQueue.finalize(); + } + + uint256[] memory requestAmounts = WithdrawalsBatchesQueue.calcRequestAmounts({ + minRequestAmount: MIN_WITHDRAWAL_REQUEST_AMOUNT, + requestAmount: MAX_WITHDRAWAL_REQUEST_AMOUNT, + amount: Math.min(stETHRemaining, MAX_WITHDRAWAL_REQUEST_AMOUNT * maxBatchSize) + }); + + _batchesQueue.add(WITHDRAWAL_QUEUE.requestWithdrawals(requestAmounts, address(this))); } - function claimNextWithdrawalsBatch(uint256 offset, uint256[] calldata hints) external { + function claimWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { _checkEscrowState(EscrowState.RageQuitEscrow); - uint256[] memory unstETHIds = _accounting.accountWithdrawalBatchClaimed(offset, hints.length); + _batchesQueue.checkClaimingInProgress(); + + if (_batchesQueue.isClaimingFinished()) { + _rageQuitTimelockStartedAt = block.timestamp; + return; + } + + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(maxUnstETHIdsCount); + uint256[] memory hints = + WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, WITHDRAWAL_QUEUE.getLastCheckpointIndex()); + + uint256 ethBalanceBefore = address(this).balance; + WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); + uint256 ethAmountClaimed = address(this).balance - ethBalanceBefore; + + _accounting.accountClaimedStETH(ETHValues.from(ethAmountClaimed)); + if (_batchesQueue.isClaimingFinished()) { + _rageQuitTimelockStartedAt = block.timestamp; + } + } - if (unstETHIds.length > 0) { - uint256 ethBalanceBefore = address(this).balance; - WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); - uint256 ethAmountClaimed = address(this).balance - ethBalanceBefore; + function claimWithdrawalsBatch(uint256 unstETHId, uint256[] calldata hints) external { + _checkEscrowState(EscrowState.RageQuitEscrow); + _batchesQueue.checkClaimingInProgress(); - _accounting.accountClaimedETH(ethAmountClaimed); + if (_batchesQueue.isClaimingFinished()) { + _rageQuitTimelockStartedAt = block.timestamp; + return; } - if (_accounting.getIsWithdrawalsClaimed()) { + + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(unstETHId, hints.length); + + uint256 ethBalanceBefore = address(this).balance; + WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); + uint256 ethAmountClaimed = address(this).balance - ethBalanceBefore; + + _accounting.accountClaimedStETH(ETHValues.from(ethAmountClaimed)); + if (_batchesQueue.isClaimingFinished()) { _rageQuitTimelockStartedAt = block.timestamp; } } @@ -221,8 +287,8 @@ contract Escrow is IEscrow { WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); uint256 ethBalanceAfter = address(this).balance; - uint256 totalAmountClaimed = _accounting.accountUnstETHClaimed(unstETHIds, claimableAmounts); - assert(totalAmountClaimed == ethBalanceAfter - ethBalanceBefore); + ETHValue totalAmountClaimed = _accounting.accountUnstETHClaimed(unstETHIds, claimableAmounts); + assert(totalAmountClaimed == ETHValues.from(ethBalanceAfter - ethBalanceBefore)); } // --- @@ -232,13 +298,15 @@ contract Escrow is IEscrow { function withdrawETH() external { _checkEscrowState(EscrowState.RageQuitEscrow); _checkWithdrawalsTimelockPassed(); - Address.sendValue(payable(msg.sender), _accounting.accountStETHSharesWithdraw(msg.sender)); + ETHValue ethToWithdraw = _accounting.accountStETHSharesWithdraw(msg.sender); + ethToWithdraw.sendTo(payable(msg.sender)); } function withdrawETH(uint256[] calldata unstETHIds) external { _checkEscrowState(EscrowState.RageQuitEscrow); _checkWithdrawalsTimelockPassed(); - Address.sendValue(payable(msg.sender), _accounting.accountUnstETHWithdraw(msg.sender, unstETHIds)); + ETHValue ethToWithdraw = _accounting.accountUnstETHWithdraw(msg.sender, unstETHIds); + ethToWithdraw.sendTo(payable(msg.sender)); } // --- @@ -246,34 +314,35 @@ contract Escrow is IEscrow { // --- function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory totals) { - totals = _accounting.totals; + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + totals.stETHClaimedETH = stETHTotals.claimedETH.toUint256(); + totals.stETHLockedShares = stETHTotals.lockedShares.toUint256(); + + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + totals.unstETHUnfinalizedShares = unstETHTotals.unfinalizedShares.toUint256(); + totals.unstETHFinalizedETH = unstETHTotals.finalizedETH.toUint256(); } - function getVetoerState(address vetoer) external view returns (VetoerState memory vetoerState) { - LockedAssetsStats memory stats = _accounting.assets[vetoer]; - vetoerState.stETHShares = stats.stETHShares; - vetoerState.unstETHShares = stats.unstETHShares; + function getVetoerState(address vetoer) external view returns (VetoerState memory state) { + HolderAssets storage assets = _accounting.assets[vetoer]; + + state.unstETHIdsCount = assets.unstETHIds.length; + state.stETHLockedShares = assets.stETHLockedShares.toUint256(); + state.unstETHLockedShares = assets.stETHLockedShares.toUint256(); + state.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp; } - function getNextWithdrawalBatches(uint256 limit) - external - view - returns (uint256 offset, uint256 total, uint256[] memory unstETHIds) - { - offset = _accounting.claimedBatchesCount; - total = _accounting.withdrawalBatchIds.length; - if (total == offset) { - return (offset, total, unstETHIds); - } - uint256 count = Math.min(limit, total - offset); - unstETHIds = new uint256[](count); - for (uint256 i = 0; i < count; ++i) { - unstETHIds[i] = _accounting.withdrawalBatchIds[offset + i]; - } + function getNextWithdrawalBatches(uint256 limit) external view returns (uint256[] memory unstETHIds) { + _batchesQueue.checkClaimingInProgress(); + return _batchesQueue.getNextWithdrawalsBatches(limit); + } + + function getIsWithdrawalsBatchesFinalized() external view returns (bool) { + return _batchesQueue.isFinalized; } function getIsWithdrawalsClaimed() external view returns (bool) { - return _accounting.getIsWithdrawalsClaimed(); + return _batchesQueue.isClaimingFinished(); } function getRageQuitTimelockStartedAt() external view returns (uint256) { @@ -281,13 +350,20 @@ contract Escrow is IEscrow { } function getRageQuitSupport() external view returns (uint256 rageQuitSupport) { - (uint256 rebaseableShares, uint256 finalizedAmount) = _accounting.getLocked(); - uint256 rebaseableAmount = ST_ETH.getPooledEthByShares(rebaseableShares); - rageQuitSupport = (10 ** 18 * (rebaseableAmount + finalizedAmount)) / (ST_ETH.totalSupply() + finalizedAmount); + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + + uint256 finalizedETH = unstETHTotals.finalizedETH.toUint256(); + uint256 ufinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); + + rageQuitSupport = ( + 10 ** 18 * (ST_ETH.getPooledEthByShares(ufinalizedShares) + finalizedETH) + / (ST_ETH.totalSupply() + finalizedETH) + ); } function isRageQuitFinalized() external view returns (bool) { - return _escrowState == EscrowState.RageQuitEscrow && _accounting.getIsWithdrawalsClaimed() + return _escrowState == EscrowState.RageQuitEscrow && _batchesQueue.isClaimingFinished() && _rageQuitTimelockStartedAt != 0 && block.timestamp > _rageQuitTimelockStartedAt + _rageQuitExtraTimelock; } diff --git a/contracts/interfaces/IWithdrawalQueue.sol b/contracts/interfaces/IWithdrawalQueue.sol index e3beea17..f6ae3a6c 100644 --- a/contracts/interfaces/IWithdrawalQueue.sol +++ b/contracts/interfaces/IWithdrawalQueue.sol @@ -37,6 +37,13 @@ interface IWithdrawalQueue { uint256[] calldata _hints ) external view returns (uint256[] memory claimableEthValues); + function findCheckpointHints( + uint256[] calldata _requestIds, + uint256 _firstIndex, + uint256 _lastIndex + ) external view returns (uint256[] memory hintIds); + function getLastCheckpointIndex() external view returns (uint256); + function balanceOf(address owner) external view returns (uint256); function requestWithdrawals( diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index acbfefc5..5737b155 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -1,14 +1,40 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {ETHValue, ETHValues} from "../types/ETHValue.sol"; +import {SharesValue, SharesValues} from "../types/SharesValue.sol"; +import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; import {WithdrawalRequestStatus} from "../interfaces/IWithdrawalQueue.sol"; import {TimeUtils} from "../utils/time.sol"; -import {ArrayUtils} from "../utils/arrays.sol"; -enum WithdrawalRequestState { +struct HolderAssets { + // The total shares amount of stETH/wstETH accounted to the holder + SharesValue stETHLockedShares; + // The total shares amount of unstETH NFTs accounted to the holder + SharesValue unstETHLockedShares; + // The timestamp when the last time was accounted lock of shares or unstETHs + uint40 lastAssetsLockTimestamp; + // The ids of the unstETH NFTs accounted to the holder + uint256[] unstETHIds; +} + +struct UnstETHAccounting { + // The cumulative amount of unfinalized unstETH shares locked in the Escrow + SharesValue unfinalizedShares; + // The total amount of ETH claimable from the finalized unstETH locked in the Escrow + ETHValue finalizedETH; +} + +struct StETHAccounting { + // The total amount of shares of locked stETH and wstETH tokens + SharesValue lockedShares; + // The total amount of ETH received during the claiming of the locked stETH + ETHValue claimedETH; +} + +enum UnstETHRecordStatus { NotLocked, Locked, Finalized, @@ -16,186 +42,148 @@ enum WithdrawalRequestState { Withdrawn } -struct WithdrawalRequest { - address owner; - uint96 claimableAmount; - uint128 shares; - uint64 vetoerUnstETHIndexOneBased; - WithdrawalRequestState state; -} - -struct LockedAssetsStats { - uint128 stETHShares; - uint128 unstETHShares; - uint128 sharesFinalized; - uint128 amountFinalized; - uint40 lastAssetsLockTimestamp; -} - -struct LockedAssetsTotals { - uint128 shares; - uint128 sharesFinalized; - uint128 amountFinalized; - uint128 amountClaimed; +struct UnstETHRecord { + // The one based index of the unstETH record in the UnstETHAccounting.unstETHIds list + IndexOneBased index; + // The address of the holder who locked unstETH + address lockedBy; + // The current status of the unstETH + UnstETHRecordStatus status; + // The amount of shares contained in the unstETH + SharesValue shares; + // The amount of ETH contained in the unstETH (this value equals to 0 until NFT is mark as finalized or claimed) + ETHValue claimableAmount; } library AssetsAccounting { - using SafeCast for uint256; + struct State { + StETHAccounting stETHTotals; + UnstETHAccounting unstETHTotals; + mapping(address account => HolderAssets) assets; + mapping(uint256 unstETHId => UnstETHRecord) unstETHRecords; + } - event StETHLocked(address indexed vetoer, uint256 shares); - event StETHUnlocked(address indexed vetoer, uint256 shares); - event StETHWithdrawn(address indexed vetoer, uint256 stETHShares, uint256 ethAmount); + // --- + // Events + // --- - event UnstETHLocked(address indexed vetoer, uint256[] ids, uint256 shares); + event ETHWithdrawn(address indexed holder, SharesValue shares, ETHValue value); + event StETHSharesLocked(address indexed holder, SharesValue shares); + event StETHSharesUnlocked(address indexed holder, SharesValue shares); + event UnstETHFinalized(uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement); event UnstETHUnlocked( - address indexed vetoer, - uint256[] ids, - uint256 sharesDecrement, - uint256 finalizedSharesDecrement, - uint256 finalizedAmountDecrement + address indexed holder, uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement ); - event UnstETHFinalized(uint256[] ids, uint256 finalizedSharesIncrement, uint256 finalizedAmountIncrement); - event UnstETHClaimed(uint256[] ids, uint256 ethAmount); - event UnstETHWithdrawn(uint256[] ids, uint256 ethAmount); - - event WithdrawalBatchCreated(uint256[] ids); - event WithdrawalBatchesClaimed(uint256 offset, uint256 count); - - error NoBatchesToClaim(); - error EmptyWithdrawalBatch(); - error WithdrawalBatchesFormed(); - error NotWithdrawalRequestOwner(uint256 id, address actual, address expected); - error InvalidSharesLock(address vetoer, uint256 shares); - error InvalidSharesUnlock(address vetoer, uint256 shares); - error InvalidSharesWithdraw(address vetoer, uint256 shares); - error WithdrawalRequestFinalized(uint256 id); - error ClaimableAmountChanged(uint256 id, uint256 actual, uint256 expected); - error WithdrawalRequestNotClaimable(uint256 id, WithdrawalRequestState state); - error WithdrawalRequestWasNotLocked(uint256 id); - error WithdrawalRequestAlreadyLocked(uint256 id); - error InvalidUnstETHOwner(address actual, address expected); - error InvalidWithdrawlRequestState(uint256 id, WithdrawalRequestState actual, WithdrawalRequestState expected); - error InvalidWithdrawalBatchesOffset(uint256 actual, uint256 expected); - error InvalidWithdrawalBatchesCount(uint256 actual, uint256 expected); - error AssetsUnlockDelayNotPassed(uint256 unlockTimelockExpiresAt); - error NotEnoughStETHToUnlock(uint256 requested, uint256 sharesBalance); + event UnstETHLocked(address indexed holder, uint256[] ids, SharesValue shares); + event UnstETHClaimed(uint256[] unstETHIds, ETHValue totalAmountClaimed); + event UnstETHWithdrawn(uint256[] unstETHIds, ETHValue amountWithdrawn); - struct State { - LockedAssetsTotals totals; - mapping(address vetoer => LockedAssetsStats) assets; - mapping(uint256 unstETHId => WithdrawalRequest) requests; - mapping(address vetoer => uint256[] unstETHIds) vetoersUnstETHIds; - uint256[] withdrawalBatchIds; - uint256 claimedBatchesCount; - bool isAllWithdrawalBatchesFormed; - } + event ETHClaimed(ETHValue amount); // --- - // stETH Operations Accounting + // Errors // --- - function accountStETHSharesLock(State storage self, address vetoer, uint256 shares) internal { - _checkNonZeroSharesLock(vetoer, shares); - uint128 sharesUint128 = shares.toUint128(); - self.assets[vetoer].stETHShares += sharesUint128; - self.assets[vetoer].lastAssetsLockTimestamp = TimeUtils.timestamp(); - self.totals.shares += sharesUint128; - emit StETHLocked(vetoer, shares); - } + error InvalidSharesValue(SharesValue value); + error InvalidUnstETHStatus(uint256 unstETHId, UnstETHRecordStatus status); + error InvalidUnstETHHolder(uint256 unstETHId, address actual, address expected); + error AssetsUnlockDelayNotPassed(uint256 unlockTimelockExpiresAt); + error InvalidClaimableAmount(uint256 unstETHId, ETHValue expected, ETHValue actual); + + // --- + // stETH shares operations accounting + // --- - function accountStETHSharesUnlock(State storage self, address vetoer) internal returns (uint128 sharesUnlocked) { - sharesUnlocked = accountStETHSharesUnlock(self, vetoer, self.assets[vetoer].stETHShares); + function accountStETHSharesLock(State storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares + shares; + HolderAssets storage assets = self.assets[holder]; + assets.stETHLockedShares = assets.stETHLockedShares + shares; + assets.lastAssetsLockTimestamp = TimeUtils.timestamp(); + emit StETHSharesLocked(holder, shares); } - function accountStETHSharesUnlock( - State storage self, - address vetoer, - uint256 shares - ) internal returns (uint128 sharesUnlocked) { - _checkStETHSharesUnlock(self, vetoer, shares); - sharesUnlocked = shares.toUint128(); - self.totals.shares -= sharesUnlocked; - self.assets[vetoer].stETHShares -= sharesUnlocked; - emit StETHUnlocked(vetoer, sharesUnlocked); + function accountStETHSharesUnlock(State storage self, address holder) internal returns (SharesValue shares) { + shares = self.assets[holder].stETHLockedShares; + accountStETHSharesUnlock(self, holder, shares); } - function accountStETHSharesWithdraw(State storage self, address vetoer) internal returns (uint256 ethAmount) { - uint256 stETHShares = self.assets[vetoer].stETHShares; - _checkNonZeroSharesWithdraw(vetoer, stETHShares); - self.assets[vetoer].stETHShares = 0; - ethAmount = self.totals.amountClaimed * stETHShares / self.totals.shares; - emit StETHWithdrawn(vetoer, stETHShares, ethAmount); + function accountStETHSharesUnlock(State storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); + + HolderAssets storage assets = self.assets[holder]; + if (assets.stETHLockedShares < shares) { + revert InvalidSharesValue(shares); + } + + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares - shares; + assets.stETHLockedShares = assets.stETHLockedShares - shares; + emit StETHSharesUnlocked(holder, shares); } - // --- - // wstETH Operations Accounting - // --- + function accountStETHSharesWithdraw(State storage self, address holder) internal returns (ETHValue ethWithdrawn) { + HolderAssets storage assets = self.assets[holder]; + SharesValue stETHSharesToWithdraw = assets.stETHLockedShares; + + _checkNonZeroShares(stETHSharesToWithdraw); - function checkAssetsUnlockDelayPassed(State storage self, address vetoer, uint256 delay) internal view { - _checkAssetsUnlockDelayPassed(self, delay, vetoer); + assets.stETHLockedShares = SharesValues.ZERO; + ethWithdrawn = + SharesValues.calcETHValue(self.stETHTotals.claimedETH, stETHSharesToWithdraw, self.stETHTotals.lockedShares); + + emit ETHWithdrawn(holder, stETHSharesToWithdraw, ethWithdrawn); + } + + function accountClaimedStETH(State storage self, ETHValue amount) internal { + self.stETHTotals.claimedETH = self.stETHTotals.claimedETH + amount; + emit ETHClaimed(amount); } // --- - // unstETH Operations Accounting + // unstETH operations accounting // --- function accountUnstETHLock( State storage self, - address vetoer, + address holder, uint256[] memory unstETHIds, WithdrawalRequestStatus[] memory statuses ) internal { assert(unstETHIds.length == statuses.length); - uint256 totalUnstETHSharesLocked; + SharesValue totalUnstETHLocked; uint256 unstETHcount = unstETHIds.length; for (uint256 i = 0; i < unstETHcount; ++i) { - totalUnstETHSharesLocked += _addWithdrawalRequest(self, vetoer, unstETHIds[i], statuses[i]); + totalUnstETHLocked = totalUnstETHLocked + _addUnstETHRecord(self, holder, unstETHIds[i], statuses[i]); } - uint128 totalUnstETHSharesLockedUint128 = totalUnstETHSharesLocked.toUint128(); - self.assets[vetoer].unstETHShares += totalUnstETHSharesLockedUint128; - self.assets[vetoer].lastAssetsLockTimestamp = TimeUtils.timestamp(); - self.totals.shares += totalUnstETHSharesLockedUint128; - emit UnstETHLocked(vetoer, unstETHIds, totalUnstETHSharesLocked); - } + self.assets[holder].lastAssetsLockTimestamp = TimeUtils.timestamp(); + self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares + totalUnstETHLocked; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares + totalUnstETHLocked; - function accountUnstETHUnlock( - State storage self, - uint256 assetsUnlockDelay, - address vetoer, - uint256[] memory unstETHIds - ) internal { - _checkAssetsUnlockDelayPassed(self, assetsUnlockDelay, vetoer); + emit UnstETHLocked(holder, unstETHIds, totalUnstETHLocked); + } - uint256 totalUnstETHSharesUnlocked; - uint256 totalFinalizedSharesUnlocked; - uint256 totalFinalizedAmountUnlocked; + function accountUnstETHUnlock(State storage self, address holder, uint256[] memory unstETHIds) internal { + SharesValue totalSharesUnlocked; + SharesValue totalFinalizedSharesUnlocked; + ETHValue totalFinalizedAmountUnlocked; uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { - (uint256 sharesUnlocked, uint256 finalizedSharesUnlocked, uint256 finalizedAmountUnlocked) = - _removeWithdrawalRequest(self, vetoer, unstETHIds[i]); - - totalUnstETHSharesUnlocked += sharesUnlocked; - totalFinalizedSharesUnlocked += finalizedSharesUnlocked; - totalFinalizedAmountUnlocked += finalizedAmountUnlocked; + (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) = + _removeUnstETHRecord(self, holder, unstETHIds[i]); + if (finalizedAmountUnlocked > ETHValues.ZERO) { + totalFinalizedAmountUnlocked = totalFinalizedAmountUnlocked + finalizedAmountUnlocked; + totalFinalizedSharesUnlocked = totalFinalizedSharesUnlocked + sharesUnlocked; + } + totalSharesUnlocked = totalSharesUnlocked + sharesUnlocked; } + self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares - totalSharesUnlocked; + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH - totalFinalizedAmountUnlocked; + self.unstETHTotals.unfinalizedShares = + self.unstETHTotals.unfinalizedShares - (totalSharesUnlocked - totalFinalizedSharesUnlocked); - uint128 totalUnstETHSharesUnlockedUint128 = totalUnstETHSharesUnlocked.toUint128(); - uint128 totalFinalizedSharesUnlockedUint128 = totalFinalizedSharesUnlocked.toUint128(); - uint128 totalFinalizedAmountUnlockedUint128 = totalFinalizedAmountUnlocked.toUint128(); - - self.assets[vetoer].unstETHShares -= totalUnstETHSharesUnlockedUint128; - self.assets[vetoer].sharesFinalized -= totalFinalizedSharesUnlockedUint128; - self.assets[vetoer].amountFinalized -= totalFinalizedAmountUnlockedUint128; - - self.totals.shares -= totalUnstETHSharesUnlockedUint128; - self.totals.sharesFinalized -= totalFinalizedSharesUnlockedUint128; - self.totals.amountFinalized -= totalFinalizedAmountUnlockedUint128; - - emit UnstETHUnlocked( - vetoer, unstETHIds, totalUnstETHSharesUnlocked, totalFinalizedSharesUnlocked, totalFinalizedAmountUnlocked - ); + emit UnstETHUnlocked(holder, unstETHIds, totalSharesUnlocked, totalFinalizedAmountUnlocked); } function accountUnstETHFinalized( @@ -205,26 +193,19 @@ library AssetsAccounting { ) internal { assert(claimableAmounts.length == unstETHIds.length); - uint256 totalSharesFinalized; - uint256 totalAmountFinalized; + ETHValue totalAmountFinalized; + SharesValue totalSharesFinalized; uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { - (address owner, uint256 sharesFinalized, uint256 amountFinalized) = - _finalizeWithdrawalRequest(self, unstETHIds[i], claimableAmounts[i]); - - self.assets[owner].sharesFinalized += sharesFinalized.toUint128(); - self.assets[owner].amountFinalized += amountFinalized.toUint128(); - - totalSharesFinalized += sharesFinalized; - totalAmountFinalized += amountFinalized; + (SharesValue sharesFinalized, ETHValue amountFinalized) = + _finalizeUnstETHRecord(self, unstETHIds[i], claimableAmounts[i]); + totalSharesFinalized = totalSharesFinalized + sharesFinalized; + totalAmountFinalized = totalAmountFinalized + amountFinalized; } - uint128 totalSharesFinalizedUint128 = totalSharesFinalized.toUint128(); - uint128 totalAmountFinalizedUint128 = totalAmountFinalized.toUint128(); - - self.totals.sharesFinalized += totalSharesFinalizedUint128; - self.totals.amountFinalized += totalAmountFinalizedUint128; + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH + totalAmountFinalized; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares - totalSharesFinalized; emit UnstETHFinalized(unstETHIds, totalSharesFinalized, totalAmountFinalized); } @@ -232,292 +213,173 @@ library AssetsAccounting { State storage self, uint256[] memory unstETHIds, uint256[] memory claimableAmounts - ) internal returns (uint256 totalAmountClaimed) { + ) internal returns (ETHValue totalAmountClaimed) { uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { - totalAmountClaimed += _claimWithdrawalRequest(self, unstETHIds[i], claimableAmounts[i]); + ETHValue claimableAmount = ETHValues.from(claimableAmounts[i]); + totalAmountClaimed = totalAmountClaimed + claimableAmount; + _claimUnstETHRecord(self, unstETHIds[i], claimableAmount); } - self.totals.amountClaimed += totalAmountClaimed.toUint128(); emit UnstETHClaimed(unstETHIds, totalAmountClaimed); } function accountUnstETHWithdraw( State storage self, - address vetoer, + address holder, uint256[] calldata unstETHIds - ) internal returns (uint256 amountWithdrawn) { + ) internal returns (ETHValue amountWithdrawn) { uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { - amountWithdrawn += _withdrawWithdrawalRequest(self, vetoer, unstETHIds[i]); + amountWithdrawn = amountWithdrawn + _withdrawUnstETHRecord(self, holder, unstETHIds[i]); } emit UnstETHWithdrawn(unstETHIds, amountWithdrawn); } - // --- - // Withdraw Batches - // --- - - function formWithdrawalBatch( - State storage self, - uint256 minRequestAmount, - uint256 maxRequestAmount, - uint256 stETHBalance, - uint256 requestAmountsCountLimit - ) internal returns (uint256[] memory requestAmounts) { - if (self.isAllWithdrawalBatchesFormed) { - revert WithdrawalBatchesFormed(); - } - if (requestAmountsCountLimit == 0) { - revert EmptyWithdrawalBatch(); - } - - uint256 maxAmount = maxRequestAmount * requestAmountsCountLimit; - if (stETHBalance >= maxAmount) { - return ArrayUtils.seed(requestAmountsCountLimit, maxRequestAmount); - } - - self.isAllWithdrawalBatchesFormed = true; - - uint256 requestsCount = stETHBalance / maxRequestAmount; - uint256 lastRequestAmount = stETHBalance % maxRequestAmount; - - if (lastRequestAmount < minRequestAmount) { - return ArrayUtils.seed(requestsCount, maxRequestAmount); - } - - requestAmounts = ArrayUtils.seed(requestsCount + 1, maxRequestAmount); - requestAmounts[requestsCount] = lastRequestAmount; - } - - function accountWithdrawalBatch(State storage self, uint256[] memory unstETHIds) internal { - uint256 unstETHIdsCount = unstETHIds.length; - for (uint256 i = 0; i < unstETHIdsCount; ++i) { - self.withdrawalBatchIds.push(unstETHIds[i]); - } - emit WithdrawalBatchCreated(unstETHIds); - } - - function accountWithdrawalBatchClaimed( - State storage self, - uint256 offset, - uint256 count - ) internal returns (uint256[] memory unstETHIds) { - if (count == 0) { - return unstETHIds; - } - uint256 batchesCount = self.withdrawalBatchIds.length; - uint256 claimedBatchesCount = self.claimedBatchesCount; - if (claimedBatchesCount == batchesCount) { - revert NoBatchesToClaim(); - } - if (claimedBatchesCount != offset) { - revert InvalidWithdrawalBatchesOffset(offset, claimedBatchesCount); - } - if (count > batchesCount - claimedBatchesCount) { - revert InvalidWithdrawalBatchesCount(count, batchesCount - claimedBatchesCount); - } - - unstETHIds = new uint256[](count); - for (uint256 i = 0; i < count; ++i) { - unstETHIds[i] = self.withdrawalBatchIds[claimedBatchesCount + i]; - } - self.claimedBatchesCount += count; - emit WithdrawalBatchesClaimed(offset, count); - } - - function accountClaimedETH(State storage self, uint256 amount) internal { - self.totals.amountClaimed += amount.toUint128(); - } - // --- // Getters // --- - function getLocked(State storage self) internal view returns (uint256 rebaseableShares, uint256 finalizedAmount) { - rebaseableShares = self.totals.shares - self.totals.sharesFinalized; - finalizedAmount = self.totals.amountFinalized; + function getLockedAssetsTotals(State storage self) + internal + view + returns (SharesValue ufinalizedShares, ETHValue finalizedETH) + { + finalizedETH = self.unstETHTotals.finalizedETH; + ufinalizedShares = self.stETHTotals.lockedShares + self.unstETHTotals.unfinalizedShares; } - function getIsWithdrawalsClaimed(State storage self) internal view returns (bool) { - return self.claimedBatchesCount == self.withdrawalBatchIds.length; - } - - function _checkWithdrawalRequestStatusOwner(WithdrawalRequestStatus memory status, address account) private pure { - if (status.owner != account) { - revert InvalidUnstETHOwner(account, status.owner); + function checkAssetsUnlockDelayPassed( + State storage self, + address holder, + uint256 assetsUnlockDelay + ) internal view { + if (block.timestamp <= self.assets[holder].lastAssetsLockTimestamp + assetsUnlockDelay) { + revert AssetsUnlockDelayNotPassed(self.assets[holder].lastAssetsLockTimestamp + assetsUnlockDelay); } } // --- - // Private Methods + // Helper methods // --- - function _addWithdrawalRequest( + function _addUnstETHRecord( State storage self, - address vetoer, + address holder, uint256 unstETHId, WithdrawalRequestStatus memory status - ) private returns (uint256 amountOfShares) { - amountOfShares = status.amountOfShares; - WithdrawalRequest storage request = self.requests[unstETHId]; + ) private returns (SharesValue shares) { + if (status.isFinalized) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.Finalized); + } + // must never be true, for unfinalized requests + assert(!status.isClaimed); - _checkWithdrawalRequestNotLocked(request, unstETHId); - _checkWithdrawalRequestStatusNotFinalized(status, unstETHId); + if (self.unstETHRecords[unstETHId].status != UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, self.unstETHRecords[unstETHId].status); + } - self.vetoersUnstETHIds[vetoer].push(unstETHId); + HolderAssets storage assets = self.assets[holder]; + assets.unstETHIds.push(unstETHId); - request.owner = vetoer; - request.state = WithdrawalRequestState.Locked; - request.vetoerUnstETHIndexOneBased = self.vetoersUnstETHIds[vetoer].length.toUint64(); - request.shares = amountOfShares.toUint128(); - assert(request.claimableAmount == 0); + shares = SharesValues.from(status.amountOfShares); + self.unstETHRecords[unstETHId] = UnstETHRecord({ + lockedBy: holder, + status: UnstETHRecordStatus.Locked, + index: IndicesOneBased.from(assets.unstETHIds.length), + shares: shares, + claimableAmount: ETHValues.ZERO + }); } - function _removeWithdrawalRequest( + function _removeUnstETHRecord( State storage self, - address vetoer, + address holder, uint256 unstETHId - ) private returns (uint256 sharesUnlocked, uint256 finalizedSharesUnlocked, uint256 finalizedAmountUnlocked) { - WithdrawalRequest storage request = self.requests[unstETHId]; + ) private returns (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; - _checkWithdrawalRequestOwner(request, vetoer); - _checkWithdrawalRequestWasLocked(request, unstETHId); + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); + } - sharesUnlocked = request.shares; - if (request.state == WithdrawalRequestState.Finalized) { - finalizedSharesUnlocked = sharesUnlocked; - finalizedAmountUnlocked = request.claimableAmount; + if (unstETHRecord.status == UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.NotLocked); } - uint256[] storage vetoerUnstETHIds = self.vetoersUnstETHIds[vetoer]; - uint256 unstETHIdIndex = request.vetoerUnstETHIndexOneBased - 1; - uint256 lastUnstETHIdIndex = vetoerUnstETHIds.length - 1; + sharesUnlocked = unstETHRecord.shares; + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + finalizedAmountUnlocked = unstETHRecord.claimableAmount; + } + + HolderAssets storage assets = self.assets[holder]; + IndexOneBased unstETHIdIndex = unstETHRecord.index; + IndexOneBased lastUnstETHIdIndex = IndicesOneBased.from(assets.unstETHIds.length); + if (lastUnstETHIdIndex != unstETHIdIndex) { - uint256 lastUnstETHId = vetoerUnstETHIds[lastUnstETHIdIndex]; - vetoerUnstETHIds[unstETHIdIndex] = lastUnstETHId; - self.requests[lastUnstETHId].vetoerUnstETHIndexOneBased = (unstETHIdIndex + 1).toUint64(); + uint256 lastUnstETHId = assets.unstETHIds[lastUnstETHIdIndex.value()]; + assets.unstETHIds[unstETHIdIndex.value()] = lastUnstETHId; + self.unstETHRecords[lastUnstETHId].index = unstETHIdIndex; } - vetoerUnstETHIds.pop(); - delete self.requests[unstETHId]; + assets.unstETHIds.pop(); + delete self.unstETHRecords[unstETHId]; } - function _finalizeWithdrawalRequest( + function _finalizeUnstETHRecord( State storage self, uint256 unstETHId, uint256 claimableAmount - ) private returns (address owner, uint256 sharesFinalized, uint256 amountFinalized) { - WithdrawalRequest storage request = self.requests[unstETHId]; - if (claimableAmount == 0 || request.state != WithdrawalRequestState.Locked) { - return (request.owner, 0, 0); + ) private returns (SharesValue sharesFinalized, ETHValue amountFinalized) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (claimableAmount == 0 || unstETHRecord.status != UnstETHRecordStatus.Locked) { + return (sharesFinalized, amountFinalized); } - owner = request.owner; - request.state = WithdrawalRequestState.Finalized; - request.claimableAmount = claimableAmount.toUint96(); + sharesFinalized = unstETHRecord.shares; + amountFinalized = ETHValues.from(claimableAmount); - sharesFinalized = request.shares; - amountFinalized = claimableAmount; - } + unstETHRecord.status = UnstETHRecordStatus.Finalized; + unstETHRecord.claimableAmount = amountFinalized; - function _claimWithdrawalRequest( - State storage self, - uint256 unstETHId, - uint256 claimableAmount - ) private returns (uint256 amountClaimed) { - WithdrawalRequest storage request = self.requests[unstETHId]; + self.unstETHRecords[unstETHId] = unstETHRecord; + } - if (request.state != WithdrawalRequestState.Locked && request.state != WithdrawalRequestState.Finalized) { - revert WithdrawalRequestNotClaimable(unstETHId, request.state); + function _claimUnstETHRecord(State storage self, uint256 unstETHId, ETHValue claimableAmount) private { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (unstETHRecord.status != UnstETHRecordStatus.Locked && unstETHRecord.status != UnstETHRecordStatus.Finalized) + { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); } - if (request.state == WithdrawalRequestState.Finalized && request.claimableAmount != claimableAmount) { - revert ClaimableAmountChanged(unstETHId, claimableAmount, request.claimableAmount); + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + // if the unstETH was marked finalized earlier, it's claimable amount must stay the same + if (unstETHRecord.claimableAmount != claimableAmount) { + revert InvalidClaimableAmount(unstETHId, claimableAmount, unstETHRecord.claimableAmount); + } } else { - request.claimableAmount = claimableAmount.toUint96(); + unstETHRecord.claimableAmount = claimableAmount; } - request.state = WithdrawalRequestState.Claimed; - amountClaimed = claimableAmount; + unstETHRecord.status = UnstETHRecordStatus.Claimed; + self.unstETHRecords[unstETHId] = unstETHRecord; } - function _withdrawWithdrawalRequest( + function _withdrawUnstETHRecord( State storage self, - address vetoer, + address holder, uint256 unstETHId - ) private returns (uint256 amountWithdrawn) { - WithdrawalRequest storage request = self.requests[unstETHId]; - - if (request.owner != vetoer) { - revert NotWithdrawalRequestOwner(unstETHId, vetoer, request.owner); - } - if (request.state != WithdrawalRequestState.Claimed) { - revert InvalidWithdrawlRequestState(unstETHId, request.state, WithdrawalRequestState.Claimed); - } - request.state = WithdrawalRequestState.Withdrawn; - amountWithdrawn = request.claimableAmount; - } - - function _checkWithdrawalRequestOwner(WithdrawalRequest storage request, address account) private view { - if (request.owner != account) { - revert InvalidUnstETHOwner(account, request.owner); - } - } - - function _checkWithdrawalRequestStatusNotFinalized( - WithdrawalRequestStatus memory status, - uint256 id - ) private pure { - if (status.isFinalized) { - revert WithdrawalRequestFinalized(id); - } - // it can't be claimed without finalization - assert(!status.isClaimed); - } - - function _checkWithdrawalRequestNotLocked(WithdrawalRequest storage request, uint256 unstETHId) private view { - if (request.vetoerUnstETHIndexOneBased != 0) { - revert WithdrawalRequestAlreadyLocked(unstETHId); - } - } - - function _checkWithdrawalRequestWasLocked(WithdrawalRequest storage request, uint256 id) private view { - if (request.vetoerUnstETHIndexOneBased == 0) { - revert WithdrawalRequestWasNotLocked(id); - } - } + ) private returns (ETHValue amountWithdrawn) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; - function _checkNonZeroSharesLock(address vetoer, uint256 shares) private pure { - if (shares == 0) { - revert InvalidSharesLock(vetoer, 0); + if (unstETHRecord.status != UnstETHRecordStatus.Claimed) { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); } - } - - function _checkNonZeroSharesUnlock(address vetoer, uint256 shares) private pure { - if (shares == 0) { - revert InvalidSharesUnlock(vetoer, 0); + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); } + unstETHRecord.status = UnstETHRecordStatus.Withdrawn; + amountWithdrawn = unstETHRecord.claimableAmount; } - function _checkStETHSharesUnlock(State storage self, address vetoer, uint256 shares) private view { - if (shares == 0) { - revert InvalidSharesUnlock(vetoer, 0); - } - - if (self.assets[vetoer].stETHShares < shares) { - revert NotEnoughStETHToUnlock(shares, self.assets[vetoer].stETHShares); - } - } - - function _checkNonZeroSharesWithdraw(address vetoer, uint256 shares) private pure { - if (shares == 0) { - revert InvalidSharesWithdraw(vetoer, 0); - } - } - - function _checkAssetsUnlockDelayPassed( - State storage self, - uint256 assetsUnlockDelay, - address vetoer - ) private view { - if (block.timestamp <= self.assets[vetoer].lastAssetsLockTimestamp + assetsUnlockDelay) { - revert AssetsUnlockDelayNotPassed(self.assets[vetoer].lastAssetsLockTimestamp + assetsUnlockDelay); + function _checkNonZeroShares(SharesValue shares) private pure { + if (shares == SharesValues.ZERO) { + revert InvalidSharesValue(SharesValues.ZERO); } } } diff --git a/contracts/libraries/WithdrawalBatchesQueue.sol b/contracts/libraries/WithdrawalBatchesQueue.sol new file mode 100644 index 00000000..dee11a1d --- /dev/null +++ b/contracts/libraries/WithdrawalBatchesQueue.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import {ArrayUtils} from "../utils/arrays.sol"; + +enum WithdrawalsBatchesQueueStatus { + // The default status of the WithdrawalsBatchesQueue. In the closed state the only action allowed + // to be called is open(), which transfers it into Opened state. + Closed, + // In the Opened state WithdrawalsBatchesQueue allows to add batches into the queue + Opened, + // When the WithdrawalsBatchesQueue enters Filled queue - it's not allowed to add batches and + // only allowed to mark batches claimed + Filled, + // The final state of the WithdrawalsBatchesQueue. This state means that all withdrawal batches + // were claimed + Claimed +} + +struct WithdrawalsBatch { + uint16 size; + uint240 fromUnstETHId; +} + +library WithdrawalsBatchesQueue { + using SafeCast for uint256; + + struct State { + bool isFinalized; + uint16 batchIndex; + uint16 unstETHIndex; + uint48 totalUnstETHCount; + uint48 totalUnstETHClaimed; + WithdrawalsBatch[] batches; + } + + error AllBatchesAlreadyFormed(); + error InvalidUnstETHId(uint256 unstETHId); + error NotFinalizable(uint256 stETHBalance); + error ClaimingNotStarted(); + error ClaimingIsFinished(); + error EmptyWithdrawalsBatch(); + + function calcRequestAmounts( + uint256 minRequestAmount, + uint256 requestAmount, + uint256 amount + ) internal pure returns (uint256[] memory requestAmounts) { + uint256 requestsCount = amount / requestAmount; + // last request amount will be equal to zero when it's multiple requestAmount + // when it's in the range [0, minRequestAmount) - it will not be included in the result + uint256 lastRequestAmount = amount - requestsCount * requestAmount; + if (lastRequestAmount >= minRequestAmount) { + requestsCount += 1; + } + requestAmounts = ArrayUtils.seed(requestsCount, requestAmount); + if (lastRequestAmount >= minRequestAmount) { + requestAmounts[requestsCount - 1] = lastRequestAmount; + } + } + + function add(State storage self, uint256[] memory unstETHIds) internal { + uint256 newUnstETHIdsCount = unstETHIds.length; + if (newUnstETHIdsCount == 0) { + revert EmptyWithdrawalsBatch(); + } + + uint256 firstAddedUnstETHId = unstETHIds[0]; + if (self.batches.length == 0) { + self.batches.push( + WithdrawalsBatch({fromUnstETHId: firstAddedUnstETHId.toUint240(), size: newUnstETHIdsCount.toUint16()}) + ); + return; + } + + WithdrawalsBatch memory lastBatch = self.batches[self.batches.length - 1]; + uint256 lastCreatedUnstETHId = lastBatch.fromUnstETHId + lastBatch.size; + // when there is no gap between the lastly added unstETHId and the new one + // then the batch may not be created, and added to the last one + if (firstAddedUnstETHId == lastCreatedUnstETHId) { + // but it may be done only when the batch max capacity is allowed to do it + if (lastBatch.size + newUnstETHIdsCount <= type(uint16).max) { + self.batches[self.batches.length - 1].size = (lastBatch.size + newUnstETHIdsCount).toUint16(); + } + } else { + self.batches.push( + WithdrawalsBatch({fromUnstETHId: firstAddedUnstETHId.toUint240(), size: newUnstETHIdsCount.toUint16()}) + ); + } + lastBatch = self.batches[self.batches.length - 1]; + self.totalUnstETHCount += newUnstETHIdsCount.toUint48(); + } + + function claimNextBatch(State storage self, uint256 maxUnstETHIdsCount) internal returns (uint256[] memory) { + uint256 batchId = self.batchIndex; + WithdrawalsBatch memory batch = self.batches[batchId]; + uint256 unstETHId = batch.fromUnstETHId + self.unstETHIndex; + return claimNextBatch(self, unstETHId, maxUnstETHIdsCount); + } + + function claimNextBatch( + State storage self, + uint256 unstETHId, + uint256 maxUnstETHIdsCount + ) internal returns (uint256[] memory result) { + uint256 expectedUnstETHId = self.batches[self.batchIndex].fromUnstETHId + self.unstETHIndex; + if (expectedUnstETHId != unstETHId) { + revert InvalidUnstETHId(unstETHId); + } + + uint256 unclaimedUnstETHIdsCount = self.totalUnstETHCount - self.totalUnstETHClaimed; + uint256 unstETHIdsCountToClaim = Math.min(unclaimedUnstETHIdsCount, maxUnstETHIdsCount); + + uint256 batchIndex = self.batchIndex; + uint256 unstETHIndex = self.unstETHIndex; + result = new uint256[](unstETHIdsCountToClaim); + self.totalUnstETHClaimed += unstETHIdsCountToClaim.toUint48(); + + uint256 index = 0; + while (unstETHIdsCountToClaim > 0) { + WithdrawalsBatch memory batch = self.batches[batchIndex]; + uint256 unstETHIdsToClaimInBatch = Math.min(unstETHIdsCountToClaim, batch.size - unstETHIndex); + for (uint256 i = 0; i < unstETHIdsToClaimInBatch; ++i) { + result[i] = batch.fromUnstETHId + unstETHIndex + i; + } + index += unstETHIdsToClaimInBatch; + unstETHIndex += unstETHIdsToClaimInBatch; + unstETHIdsCountToClaim -= unstETHIdsToClaimInBatch; + if (unstETHIndex == batch.size) { + batchIndex += 1; + unstETHIndex = 0; + } + } + self.batchIndex = batchIndex.toUint16(); + self.unstETHIndex = unstETHIndex.toUint16(); + } + + function getNextWithdrawalsBatches( + State storage self, + uint256 limit + ) internal view returns (uint256[] memory unstETHIds) { + uint256 batchId = self.batchIndex; + uint256 unstETHindex = self.unstETHIndex; + WithdrawalsBatch memory batch = self.batches[batchId]; + uint256 unstETHId = batch.fromUnstETHId + self.unstETHIndex; + uint256 unstETHIdsCount = Math.min(batch.size - unstETHindex, limit); + + unstETHIds = new uint256[](unstETHIdsCount); + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + unstETHIds[i] = unstETHId + i; + } + } + + function checkNotFinalized(State storage self) internal view { + if (self.isFinalized) { + revert AllBatchesAlreadyFormed(); + } + } + + function finalize(State storage self) internal { + self.isFinalized = true; + } + + function isClaimingFinished(State storage self) internal view returns (bool) { + return self.totalUnstETHClaimed == self.totalUnstETHCount; + } + + function checkClaimingInProgress(State storage self) internal view { + if (!self.isFinalized) { + revert ClaimingNotStarted(); + } + if (self.totalUnstETHCount > 0 && self.totalUnstETHCount == self.totalUnstETHClaimed) { + revert ClaimingIsFinished(); + } + } +} diff --git a/contracts/types/ETHValue.sol b/contracts/types/ETHValue.sol new file mode 100644 index 00000000..3fb11785 --- /dev/null +++ b/contracts/types/ETHValue.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +type ETHValue is uint128; + +error ETHValueOverflow(); +error ETHValueUnderflow(); + +using {plus as +, minus as -, lt as <, gt as >, eq as ==, neq as !=} for ETHValue global; +using {toUint256} for ETHValue global; +using {sendTo} for ETHValue global; + +function sendTo(ETHValue value, address payable recipient) { + Address.sendValue(recipient, value.toUint256()); +} + +function plus(ETHValue v1, ETHValue v2) pure returns (ETHValue) { + return ETHValues.from(ETHValue.unwrap(v1) + ETHValue.unwrap(v2)); +} + +function minus(ETHValue v1, ETHValue v2) pure returns (ETHValue) { + if (v1 < v2) { + revert ETHValueUnderflow(); + } + return ETHValues.from(ETHValue.unwrap(v1) + ETHValue.unwrap(v2)); +} + +function lt(ETHValue v1, ETHValue v2) pure returns (bool) { + return ETHValue.unwrap(v1) < ETHValue.unwrap(v2); +} + +function gt(ETHValue v1, ETHValue v2) pure returns (bool) { + return ETHValue.unwrap(v1) > ETHValue.unwrap(v2); +} + +function eq(ETHValue v1, ETHValue v2) pure returns (bool) { + return ETHValue.unwrap(v1) == ETHValue.unwrap(v2); +} + +function neq(ETHValue v1, ETHValue v2) pure returns (bool) { + return ETHValue.unwrap(v1) != ETHValue.unwrap(v2); +} + +function toUint256(ETHValue value) pure returns (uint256) { + return ETHValue.unwrap(value); +} + +library ETHValues { + ETHValue internal constant ZERO = ETHValue.wrap(0); + + function from(uint256 value) internal pure returns (ETHValue) { + if (value > type(uint128).max) { + revert ETHValueOverflow(); + } + return ETHValue.wrap(uint128(value)); + } +} diff --git a/contracts/types/IndexOneBased.sol b/contracts/types/IndexOneBased.sol new file mode 100644 index 00000000..201ce080 --- /dev/null +++ b/contracts/types/IndexOneBased.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +type IndexOneBased is uint32; + +error IndexOneBasedOverflow(); +error IndexOneBasedUnderflow(); + +using {neq as !=} for IndexOneBased global; +using {value} for IndexOneBased global; + +function neq(IndexOneBased i1, IndexOneBased i2) pure returns (bool) { + return IndexOneBased.unwrap(i1) != IndexOneBased.unwrap(i2); +} + +function value(IndexOneBased index) pure returns (uint256) { + if (IndexOneBased.unwrap(index) == 0) { + revert IndexOneBasedUnderflow(); + } + unchecked { + return IndexOneBased.unwrap(index) - 1; + } +} + +library IndicesOneBased { + function from(uint256 value) internal pure returns (IndexOneBased) { + if (value > type(uint32).max) { + revert IndexOneBasedOverflow(); + } + return IndexOneBased.wrap(uint32(value)); + } +} diff --git a/contracts/types/SharesValue.sol b/contracts/types/SharesValue.sol new file mode 100644 index 00000000..f5048477 --- /dev/null +++ b/contracts/types/SharesValue.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {ETHValue, ETHValues} from "./ETHValue.sol"; + +type SharesValue is uint128; + +error SharesValueOverflow(); + +using {plus as +, minus as -, eq as ==, lt as <} for SharesValue global; +using {toUint256} for SharesValue global; + +function plus(SharesValue v1, SharesValue v2) pure returns (SharesValue) { + return SharesValue.wrap(SharesValue.unwrap(v1) + SharesValue.unwrap(v2)); +} + +function minus(SharesValue v1, SharesValue v2) pure returns (SharesValue) { + return SharesValue.wrap(SharesValue.unwrap(v1) - SharesValue.unwrap(v2)); +} + +function lt(SharesValue v1, SharesValue v2) pure returns (bool) { + return SharesValue.unwrap(v1) < SharesValue.unwrap(v2); +} + +function eq(SharesValue v1, SharesValue v2) pure returns (bool) { + return SharesValue.unwrap(v1) == SharesValue.unwrap(v2); +} + +function toUint256(SharesValue v) pure returns (uint256) { + return SharesValue.unwrap(v); +} + +library SharesValues { + SharesValue internal constant ZERO = SharesValue.wrap(0); + + function from(uint256 value) internal pure returns (SharesValue) { + if (value > type(uint128).max) { + revert SharesValueOverflow(); + } + return SharesValue.wrap(uint128(value)); + } + + function calcETHValue( + ETHValue totalPooled, + SharesValue share, + SharesValue total + ) internal pure returns (ETHValue) { + return ETHValues.from(totalPooled.toUint256() * share.toUint256() / total.toUint256()); + } +} diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 10d590dd..3b34656b 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -6,10 +6,10 @@ import {WithdrawalRequestStatus} from "../utils/interfaces.sol"; import { Escrow, Balances, - VetoerState, - LockedAssetsTotals, WITHDRAWAL_QUEUE, - ScenarioTestBlueprint + ScenarioTestBlueprint, + VetoerState, + LockedAssetsTotals } from "../utils/scenario-test-blueprint.sol"; contract TestHelpers is ScenarioTestBlueprint { @@ -240,33 +240,39 @@ contract EscrowHappyPath is TestHelpers { } function test_check_finalization() public { - uint256 totalSharesLocked = _ST_ETH.getSharesByPooledEth(2 * 1e18); - uint256 expectedSharesFinalized = _ST_ETH.getSharesByPooledEth(1 * 1e18); + uint256 totalAmountLocked = 2 ether; uint256[] memory amounts = new uint256[](2); for (uint256 i = 0; i < 2; ++i) { - amounts[i] = 1e18; + amounts[i] = 1 ether; } vm.prank(_VETOER_1); uint256[] memory unstETHIds = _WITHDRAWAL_QUEUE.requestWithdrawals(amounts, _VETOER_1); + uint256 totalSharesLocked; + WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + for (uint256 i = 0; i < unstETHIds.length; ++i) { + totalSharesLocked += statuses[i].amountOfShares; + } + _lockUnstETH(_VETOER_1, unstETHIds); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); - assertEq(escrow.getLockedAssetsTotals().sharesFinalized, 0); + VetoerState memory vetoerState = escrow.getVetoerState(_VETOER_1); + assertEq(vetoerState.unstETHIdsCount, 2); + + LockedAssetsTotals memory totals = escrow.getLockedAssetsTotals(); + assertEq(totals.unstETHFinalizedETH, 0); + assertEq(totals.unstETHUnfinalizedShares, totalSharesLocked); finalizeWQ(unstETHIds[0]); uint256[] memory hints = _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); escrow.markUnstETHFinalized(unstETHIds, hints); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); - - assertApproxEqAbs(escrow.getLockedAssetsTotals().sharesFinalized, expectedSharesFinalized, 1); + totals = escrow.getLockedAssetsTotals(); + assertEq(totals.unstETHUnfinalizedShares, statuses[0].amountOfShares); uint256 ethAmountFinalized = _WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints)[0]; - assertApproxEqAbs(escrow.getLockedAssetsTotals().amountFinalized, ethAmountFinalized, 1); + assertApproxEqAbs(totals.unstETHFinalizedETH, ethAmountFinalized, 1); } function test_get_rage_quit_support() public { @@ -287,9 +293,8 @@ contract EscrowHappyPath is TestHelpers { _lockWstETH(_VETOER_1, sharesToLock); _lockUnstETH(_VETOER_1, unstETHIds); - VetoerState memory vetoerState = escrow.getVetoerState(_VETOER_1); - assertApproxEqAbs(vetoerState.stETHShares, 2 * sharesToLock, 1); - assertApproxEqAbs(vetoerState.unstETHShares, _ST_ETH.getSharesByPooledEth(2e18), 1); + assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).stETHLockedShares, 2 * sharesToLock, 1); + assertEq(escrow.getVetoerState(_VETOER_1).unstETHIdsCount, 2); uint256 rageQuitSupport = escrow.getRageQuitSupport(); assertEq(rageQuitSupport, 4 * 1e18 * 1e18 / totalSupply); @@ -299,11 +304,9 @@ contract EscrowHappyPath is TestHelpers { _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); escrow.markUnstETHFinalized(unstETHIds, hints); - LockedAssetsTotals memory totals = escrow.getLockedAssetsTotals(); - - assertApproxEqAbs(totals.sharesFinalized, sharesToLock, 1); + assertEq(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, sharesToLock); uint256 ethAmountFinalized = _WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints)[0]; - assertApproxEqAbs(totals.amountFinalized, ethAmountFinalized, 1); + assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHFinalizedETH, ethAmountFinalized, 1); rageQuitSupport = escrow.getRageQuitSupport(); assertEq( @@ -345,7 +348,9 @@ contract EscrowHappyPath is TestHelpers { assertEq(_WITHDRAWAL_QUEUE.balanceOf(address(escrow)), 20); - escrow.requestNextWithdrawalsBatch(200); + while (!escrow.getIsWithdrawalsBatchesFinalized()) { + escrow.requestNextWithdrawalsBatch(96); + } assertEq(_WITHDRAWAL_QUEUE.balanceOf(address(escrow)), 10 + expectedWithdrawalBatchesCount); assertEq(escrow.isRageQuitFinalized(), false); @@ -353,9 +358,8 @@ contract EscrowHappyPath is TestHelpers { vm.deal(WITHDRAWAL_QUEUE, 1000 * requestAmount); finalizeWQ(); - (uint256 offset, uint256 total, uint256[] memory unstETHIdsToClaim) = - escrow.getNextWithdrawalBatches(expectedWithdrawalBatchesCount); - assertEq(total, expectedWithdrawalBatchesCount); + uint256[] memory unstETHIdsToClaim = escrow.getNextWithdrawalBatches(expectedWithdrawalBatchesCount); + // assertEq(total, expectedWithdrawalBatchesCount); WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIdsToClaim); @@ -367,7 +371,9 @@ contract EscrowHappyPath is TestHelpers { uint256[] memory hints = _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIdsToClaim, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); - escrow.claimNextWithdrawalsBatch(offset, hints); + while (!escrow.getIsWithdrawalsClaimed()) { + escrow.claimWithdrawalsBatch(128); + } assertEq(escrow.isRageQuitFinalized(), false); @@ -419,7 +425,9 @@ contract EscrowHappyPath is TestHelpers { vm.deal(WITHDRAWAL_QUEUE, 100 * requestAmount); finalizeWQ(); - escrow.claimNextWithdrawalsBatch(0, new uint256[](0)); + escrow.requestNextWithdrawalsBatch(96); + + escrow.claimWithdrawalsBatch(0, new uint256[](0)); assertEq(escrow.isRageQuitFinalized(), false); @@ -448,14 +456,14 @@ contract EscrowHappyPath is TestHelpers { uint256 totalSharesLocked = firstVetoerWstETHAmount + firstVetoerStETHShares; _lockStETH(_VETOER_1, firstVetoerStETHAmount); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).stETHShares, firstVetoerStETHShares, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, firstVetoerStETHShares, 1); + assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).stETHLockedShares, firstVetoerStETHShares, 1); + assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, firstVetoerStETHShares, 1); _lockWstETH(_VETOER_1, firstVetoerWstETHAmount); assertApproxEqAbs( - escrow.getVetoerState(_VETOER_1).stETHShares, firstVetoerWstETHAmount + firstVetoerStETHShares, 2 + escrow.getVetoerState(_VETOER_1).stETHLockedShares, firstVetoerWstETHAmount + firstVetoerStETHShares, 2 ); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, totalSharesLocked, 2); _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); @@ -465,8 +473,8 @@ contract EscrowHappyPath is TestHelpers { vm.prank(_VETOER_1); uint256[] memory stETHWithdrawalRequestIds = escrow.requestWithdrawals(stETHWithdrawalRequestAmounts); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, firstVetoerStETHShares, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, firstVetoerWstETHAmount, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, firstVetoerStETHShares, 2); uint256[] memory wstETHWithdrawalRequestAmounts = new uint256[](1); wstETHWithdrawalRequestAmounts[0] = _ST_ETH.getPooledEthByShares(firstVetoerWstETHAmount); @@ -474,8 +482,8 @@ contract EscrowHappyPath is TestHelpers { vm.prank(_VETOER_1); uint256[] memory wstETHWithdrawalRequestIds = escrow.requestWithdrawals(wstETHWithdrawalRequestAmounts); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 2); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, 0, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, totalSharesLocked, 2); finalizeWQ(wstETHWithdrawalRequestIds[0]); @@ -485,8 +493,9 @@ contract EscrowHappyPath is TestHelpers { stETHWithdrawalRequestIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex() ) ); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 2); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, 0, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, firstVetoerWstETHAmount, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHFinalizedETH, firstVetoerStETHAmount, 2); escrow.markUnstETHFinalized( wstETHWithdrawalRequestIds, @@ -494,16 +503,16 @@ contract EscrowHappyPath is TestHelpers { wstETHWithdrawalRequestIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex() ) ); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 2); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, 0, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, 0, 2); _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); vm.prank(_VETOER_1); escrow.unlockUnstETH(stETHWithdrawalRequestIds); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, firstVetoerWstETHAmount, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, firstVetoerWstETHAmount, 1); + // // assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, firstVetoerWstETHAmount, 1); + // assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, firstVetoerWstETHAmount, 1); vm.prank(_VETOER_1); escrow.unlockUnstETH(wstETHWithdrawalRequestIds); diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index 38e75c0e..e1612568 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -68,23 +68,17 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { uint256 requestAmount = _WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); uint256 maxRequestsCount = vetoedStETHAmount / requestAmount + 1; - rageQuitEscrow.requestNextWithdrawalsBatch(maxRequestsCount); + while (!rageQuitEscrow.getIsWithdrawalsBatchesFinalized()) { + rageQuitEscrow.requestNextWithdrawalsBatch(96); + } vm.deal(address(_WITHDRAWAL_QUEUE), vetoedStETHAmount); _finalizeWQ(); uint256 batchSizeLimit = 200; - while (true) { - (uint256 offset, uint256 total, uint256[] memory unstETHIds) = - rageQuitEscrow.getNextWithdrawalBatches(batchSizeLimit); - if (offset == total) { - break; - } - uint256[] memory hints = - _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); - - rageQuitEscrow.claimNextWithdrawalsBatch(offset, hints); + while (!rageQuitEscrow.getIsWithdrawalsClaimed()) { + rageQuitEscrow.claimWithdrawalsBatch(128); } _wait(_config.RAGE_QUIT_EXTENSION_DELAY() + 1); diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 629551ff..a955d28a 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -191,7 +191,7 @@ contract ScenarioTestBlueprint is Test { function _unlockWstETH(address vetoer) internal { Escrow escrow = _getVetoSignallingEscrow(); uint256 wstETHBalanceBefore = _WST_ETH.balanceOf(vetoer); - uint256 vetoerWstETHSharesBefore = escrow.getVetoerState(vetoer).stETHShares; + VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); vm.startPrank(vetoer); uint256 wstETHUnlocked = escrow.unlockWstETH(); @@ -199,14 +199,14 @@ contract ScenarioTestBlueprint is Test { // 1 wei rounding issue may arise because of the wrapping stETH into wstETH before // sending funds to the user - assertApproxEqAbs(wstETHUnlocked, vetoerWstETHSharesBefore, 1); - assertApproxEqAbs(_WST_ETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerWstETHSharesBefore, 1); + assertApproxEqAbs(wstETHUnlocked, vetoerStateBefore.stETHLockedShares, 1); + assertApproxEqAbs(_WST_ETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerStateBefore.stETHLockedShares, 1); } function _lockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - uint256 vetoerUnstETHSharesBefore = escrow.getVetoerState(vetoer).unstETHShares; - uint256 totalSharesBefore = escrow.getLockedAssetsTotals().shares; + VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); uint256 unstETHTotalSharesLocked = 0; WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); @@ -224,19 +224,25 @@ contract ScenarioTestBlueprint is Test { assertEq(_WITHDRAWAL_QUEUE.ownerOf(unstETHIds[i]), address(escrow)); } - assertEq(escrow.getVetoerState(vetoer).unstETHShares, vetoerUnstETHSharesBefore + unstETHTotalSharesLocked); - assertEq(escrow.getLockedAssetsTotals().shares, totalSharesBefore + unstETHTotalSharesLocked); + VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); + assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount + unstETHIds.length); + + LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + assertEq( + lockedAssetsTotalsAfter.unstETHUnfinalizedShares, + lockedAssetsTotalsBefore.unstETHUnfinalizedShares + unstETHTotalSharesLocked + ); } function _unlockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - uint256 vetoerUnstETHSharesBefore = escrow.getVetoerState(vetoer).unstETHShares; - uint256 totalSharesBefore = escrow.getLockedAssetsTotals().shares; + VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); - uint256 unstETHTotalSharesLocked = 0; + uint256 unstETHTotalSharesUnlocked = 0; WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); for (uint256 i = 0; i < unstETHIds.length; ++i) { - unstETHTotalSharesLocked += statuses[i].amountOfShares; + unstETHTotalSharesUnlocked += statuses[i].amountOfShares; } vm.prank(vetoer); @@ -246,8 +252,15 @@ contract ScenarioTestBlueprint is Test { assertEq(_WITHDRAWAL_QUEUE.ownerOf(unstETHIds[i]), vetoer); } - assertEq(escrow.getVetoerState(vetoer).unstETHShares, vetoerUnstETHSharesBefore - unstETHTotalSharesLocked); - assertEq(escrow.getLockedAssetsTotals().shares, totalSharesBefore - unstETHTotalSharesLocked); + VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); + assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount - unstETHIds.length); + + // TODO: implement correct assert. It must consider was unstETH finalized or not + LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + assertEq( + lockedAssetsTotalsAfter.unstETHUnfinalizedShares, + lockedAssetsTotalsBefore.unstETHUnfinalizedShares - unstETHTotalSharesUnlocked + ); } // --- From 986299c331e516ed0ac61f966160d17d39f669da Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 5 Jul 2024 13:33:59 +0400 Subject: [PATCH 121/134] Optimized WithdrawalsBatches logic --- contracts/Escrow.sol | 94 ++++----- .../libraries/WithdrawalBatchesQueue.sol | 184 ++++++++---------- contracts/types/SequentialBatches.sol | 78 ++++++++ test/scenario/veto-cooldown-mechanics.t.sol | 4 +- 4 files changed, 199 insertions(+), 161 deletions(-) create mode 100644 contracts/types/SequentialBatches.sol diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 97aeafef..46abb629 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -50,17 +50,19 @@ contract Escrow is IEscrow { using AssetsAccounting for AssetsAccounting.State; using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.State; - error EmptyBatch(); - error ZeroWithdraw(); + error UnexpectedUnstETHId(); + error InvalidHintsLength(uint256 actual, uint256 expected); + error ClaimingIsFinished(); error InvalidBatchSize(uint256 size); error WithdrawalsTimelockNotPassed(); error InvalidETHSender(address actual, address expected); error NotDualGovernance(address actual, address expected); - error InvalidNextBatch(uint256 actualRequestId, uint256 expectedRequestId); error MasterCopyCallForbidden(); error InvalidState(EscrowState actual, EscrowState expected); error RageQuitExtraTimelockNotStarted(); + address public immutable MASTER_COPY; + // TODO: move to config uint256 public immutable MIN_BATCH_SIZE = 8; uint256 public immutable MAX_BATCH_SIZE = 128; @@ -68,9 +70,6 @@ contract Escrow is IEscrow { uint256 public immutable MIN_WITHDRAWAL_REQUEST_AMOUNT; uint256 public immutable MAX_WITHDRAWAL_REQUEST_AMOUNT; - uint256 public immutable RAGE_QUIT_TIMELOCK = 30 days; - address public immutable MASTER_COPY; - IStETH public immutable ST_ETH; IWstETH public immutable WST_ETH; IWithdrawalQueue public immutable WITHDRAWAL_QUEUE; @@ -82,11 +81,9 @@ contract Escrow is IEscrow { AssetsAccounting.State private _accounting; WithdrawalsBatchesQueue.State private _batchesQueue; - uint256[] internal _withdrawalUnstETHIds; - uint256 internal _rageQuitExtraTimelock; - uint256 internal _rageQuitWithdrawalsTimelock; uint256 internal _rageQuitTimelockStartedAt; + uint256 internal _rageQuitWithdrawalsTimelock; constructor(address stETH, address wstETH, address withdrawalQueue, address config) { ST_ETH = IStETH(stETH); @@ -109,7 +106,6 @@ contract Escrow is IEscrow { ST_ETH.approve(address(WST_ETH), type(uint256).max); ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); - WST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); } // --- @@ -203,19 +199,15 @@ contract Escrow is IEscrow { _checkDualGovernance(msg.sender); _checkEscrowState(EscrowState.SignallingEscrow); + _batchesQueue.open(); _escrowState = EscrowState.RageQuitEscrow; _rageQuitExtraTimelock = rageQuitExtraTimelock; _rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; - - uint256 wstETHBalance = WST_ETH.balanceOf(address(this)); - if (wstETHBalance > 0) { - WST_ETH.unwrap(wstETHBalance); - } - ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); } function requestNextWithdrawalsBatch(uint256 maxBatchSize) external { - _batchesQueue.checkNotFinalized(); + _checkEscrowState(EscrowState.RageQuitEscrow); + _batchesQueue.checkOpened(); if (maxBatchSize < MIN_BATCH_SIZE || maxBatchSize > MAX_BATCH_SIZE) { revert InvalidBatchSize(maxBatchSize); @@ -223,7 +215,7 @@ contract Escrow is IEscrow { uint256 stETHRemaining = ST_ETH.balanceOf(address(this)); if (stETHRemaining < MIN_WITHDRAWAL_REQUEST_AMOUNT) { - return _batchesQueue.finalize(); + return _batchesQueue.close(); } uint256[] memory requestAmounts = WithdrawalsBatchesQueue.calcRequestAmounts({ @@ -237,46 +229,33 @@ contract Escrow is IEscrow { function claimWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { _checkEscrowState(EscrowState.RageQuitEscrow); - _batchesQueue.checkClaimingInProgress(); - - if (_batchesQueue.isClaimingFinished()) { - _rageQuitTimelockStartedAt = block.timestamp; - return; + if (_rageQuitTimelockStartedAt != 0) { + revert ClaimingIsFinished(); } uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(maxUnstETHIdsCount); - uint256[] memory hints = - WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, WITHDRAWAL_QUEUE.getLastCheckpointIndex()); - - uint256 ethBalanceBefore = address(this).balance; - WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); - uint256 ethAmountClaimed = address(this).balance - ethBalanceBefore; - _accounting.accountClaimedStETH(ETHValues.from(ethAmountClaimed)); - if (_batchesQueue.isClaimingFinished()) { - _rageQuitTimelockStartedAt = block.timestamp; - } + _claimWithdrawalsBatch( + unstETHIds, WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, WITHDRAWAL_QUEUE.getLastCheckpointIndex()) + ); } - function claimWithdrawalsBatch(uint256 unstETHId, uint256[] calldata hints) external { + function claimWithdrawalsBatch(uint256 fromUnstETHIds, uint256[] calldata hints) external { _checkEscrowState(EscrowState.RageQuitEscrow); - _batchesQueue.checkClaimingInProgress(); - - if (_batchesQueue.isClaimingFinished()) { - _rageQuitTimelockStartedAt = block.timestamp; - return; + if (_rageQuitTimelockStartedAt != 0) { + revert ClaimingIsFinished(); } - uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(unstETHId, hints.length); - - uint256 ethBalanceBefore = address(this).balance; - WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); - uint256 ethAmountClaimed = address(this).balance - ethBalanceBefore; + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(hints.length); - _accounting.accountClaimedStETH(ETHValues.from(ethAmountClaimed)); - if (_batchesQueue.isClaimingFinished()) { - _rageQuitTimelockStartedAt = block.timestamp; + if (unstETHIds.length > 0 && fromUnstETHIds != unstETHIds[0]) { + revert UnexpectedUnstETHId(); } + if (hints.length != unstETHIds.length) { + revert InvalidHintsLength(hints.length, unstETHIds.length); + } + + _claimWithdrawalsBatch(unstETHIds, hints); } function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external { @@ -333,16 +312,15 @@ contract Escrow is IEscrow { } function getNextWithdrawalBatches(uint256 limit) external view returns (uint256[] memory unstETHIds) { - _batchesQueue.checkClaimingInProgress(); return _batchesQueue.getNextWithdrawalsBatches(limit); } function getIsWithdrawalsBatchesFinalized() external view returns (bool) { - return _batchesQueue.isFinalized; + return _batchesQueue.isClosed(); } function getIsWithdrawalsClaimed() external view returns (bool) { - return _batchesQueue.isClaimingFinished(); + return _rageQuitTimelockStartedAt != 0; } function getRageQuitTimelockStartedAt() external view returns (uint256) { @@ -363,8 +341,8 @@ contract Escrow is IEscrow { } function isRageQuitFinalized() external view returns (bool) { - return _escrowState == EscrowState.RageQuitEscrow && _batchesQueue.isClaimingFinished() - && _rageQuitTimelockStartedAt != 0 && block.timestamp > _rageQuitTimelockStartedAt + _rageQuitExtraTimelock; + return _escrowState == EscrowState.RageQuitEscrow && _batchesQueue.isClosed() && _rageQuitTimelockStartedAt != 0 + && block.timestamp > _rageQuitTimelockStartedAt + _rageQuitExtraTimelock; } // --- @@ -381,6 +359,18 @@ contract Escrow is IEscrow { // Internal Methods // --- + function _claimWithdrawalsBatch(uint256[] memory unstETHIds, uint256[] memory hints) internal { + uint256 ethBalanceBefore = address(this).balance; + WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); + uint256 ethAmountClaimed = address(this).balance - ethBalanceBefore; + + _accounting.accountClaimedStETH(ETHValues.from(ethAmountClaimed)); + + if (_batchesQueue.isClosed() && _batchesQueue.isAllUnstETHClaimed()) { + _rageQuitTimelockStartedAt = block.timestamp; + } + } + function _activateNextGovernanceState() internal { _dualGovernance.activateNextState(); } diff --git a/contracts/libraries/WithdrawalBatchesQueue.sol b/contracts/libraries/WithdrawalBatchesQueue.sol index dee11a1d..6c096be8 100644 --- a/contracts/libraries/WithdrawalBatchesQueue.sol +++ b/contracts/libraries/WithdrawalBatchesQueue.sol @@ -5,44 +5,39 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {ArrayUtils} from "../utils/arrays.sol"; +import {SequentialBatch, SequentialBatches} from "../types/SequentialBatches.sol"; -enum WithdrawalsBatchesQueueStatus { +enum Status { // The default status of the WithdrawalsBatchesQueue. In the closed state the only action allowed // to be called is open(), which transfers it into Opened state. - Closed, + Empty, // In the Opened state WithdrawalsBatchesQueue allows to add batches into the queue Opened, // When the WithdrawalsBatchesQueue enters Filled queue - it's not allowed to add batches and // only allowed to mark batches claimed - Filled, - // The final state of the WithdrawalsBatchesQueue. This state means that all withdrawal batches - // were claimed - Claimed + Closed } -struct WithdrawalsBatch { - uint16 size; - uint240 fromUnstETHId; +struct QueueIndex { + uint32 batchIndex; + uint16 valueIndex; } library WithdrawalsBatchesQueue { using SafeCast for uint256; struct State { - bool isFinalized; - uint16 batchIndex; - uint16 unstETHIndex; + Status status; + QueueIndex lastClaimedUnstETHIdIndex; uint48 totalUnstETHCount; uint48 totalUnstETHClaimed; - WithdrawalsBatch[] batches; + SequentialBatch[] batches; } - error AllBatchesAlreadyFormed(); - error InvalidUnstETHId(uint256 unstETHId); - error NotFinalizable(uint256 stETHBalance); - error ClaimingNotStarted(); - error ClaimingIsFinished(); - error EmptyWithdrawalsBatch(); + event UnstETHIdsAdded(uint256[] unstETHIds); + event UnstETHIdsClaimed(uint256[] unstETHIds); + + error InvalidWithdrawalsBatchesQueueStatus(Status status); function calcRequestAmounts( uint256 minRequestAmount, @@ -62,118 +57,95 @@ library WithdrawalsBatchesQueue { } } + function open(State storage self) internal { + _checkStatus(self, Status.Empty); + // insert empty batch as a stub for first item + self.batches.push(SequentialBatches.create({seed: 0, count: 1})); + self.status = Status.Opened; + } + + function close(State storage self) internal { + _checkStatus(self, Status.Opened); + self.status = Status.Closed; + } + + function isClosed(State storage self) internal view returns (bool) { + return self.status == Status.Closed; + } + + function isAllUnstETHClaimed(State storage self) internal view returns (bool) { + return self.totalUnstETHClaimed == self.totalUnstETHCount; + } + + function checkOpened(State storage self) internal view { + _checkStatus(self, Status.Opened); + } + function add(State storage self, uint256[] memory unstETHIds) internal { - uint256 newUnstETHIdsCount = unstETHIds.length; - if (newUnstETHIdsCount == 0) { - revert EmptyWithdrawalsBatch(); + uint256 unstETHIdsCount = unstETHIds.length; + if (unstETHIdsCount == 0) { + return; } - uint256 firstAddedUnstETHId = unstETHIds[0]; - if (self.batches.length == 0) { - self.batches.push( - WithdrawalsBatch({fromUnstETHId: firstAddedUnstETHId.toUint240(), size: newUnstETHIdsCount.toUint16()}) - ); - return; + // before creating the batch, assert that the unstETHIds is sequential + for (uint256 i = 0; i < unstETHIdsCount - 1; ++i) { + assert(unstETHIds[i + 1] == unstETHIds[i] + 1); } - WithdrawalsBatch memory lastBatch = self.batches[self.batches.length - 1]; - uint256 lastCreatedUnstETHId = lastBatch.fromUnstETHId + lastBatch.size; - // when there is no gap between the lastly added unstETHId and the new one - // then the batch may not be created, and added to the last one - if (firstAddedUnstETHId == lastCreatedUnstETHId) { - // but it may be done only when the batch max capacity is allowed to do it - if (lastBatch.size + newUnstETHIdsCount <= type(uint16).max) { - self.batches[self.batches.length - 1].size = (lastBatch.size + newUnstETHIdsCount).toUint16(); - } + uint256 lastBatchIndex = self.batches.length - 1; + SequentialBatch lastWithdrawalsBatch = self.batches[lastBatchIndex]; + SequentialBatch newWithdrawalsBatch = SequentialBatches.create({seed: unstETHIds[0], count: unstETHIdsCount}); + + if (SequentialBatches.canMerge(lastWithdrawalsBatch, newWithdrawalsBatch)) { + self.batches[lastBatchIndex] = SequentialBatches.merge(lastWithdrawalsBatch, newWithdrawalsBatch); } else { - self.batches.push( - WithdrawalsBatch({fromUnstETHId: firstAddedUnstETHId.toUint240(), size: newUnstETHIdsCount.toUint16()}) - ); + self.batches.push(newWithdrawalsBatch); } - lastBatch = self.batches[self.batches.length - 1]; - self.totalUnstETHCount += newUnstETHIdsCount.toUint48(); - } - function claimNextBatch(State storage self, uint256 maxUnstETHIdsCount) internal returns (uint256[] memory) { - uint256 batchId = self.batchIndex; - WithdrawalsBatch memory batch = self.batches[batchId]; - uint256 unstETHId = batch.fromUnstETHId + self.unstETHIndex; - return claimNextBatch(self, unstETHId, maxUnstETHIdsCount); + self.totalUnstETHCount += newWithdrawalsBatch.size().toUint48(); + emit UnstETHIdsAdded(unstETHIds); } function claimNextBatch( State storage self, - uint256 unstETHId, uint256 maxUnstETHIdsCount - ) internal returns (uint256[] memory result) { - uint256 expectedUnstETHId = self.batches[self.batchIndex].fromUnstETHId + self.unstETHIndex; - if (expectedUnstETHId != unstETHId) { - revert InvalidUnstETHId(unstETHId); - } - - uint256 unclaimedUnstETHIdsCount = self.totalUnstETHCount - self.totalUnstETHClaimed; - uint256 unstETHIdsCountToClaim = Math.min(unclaimedUnstETHIdsCount, maxUnstETHIdsCount); - - uint256 batchIndex = self.batchIndex; - uint256 unstETHIndex = self.unstETHIndex; - result = new uint256[](unstETHIdsCountToClaim); - self.totalUnstETHClaimed += unstETHIdsCountToClaim.toUint48(); - - uint256 index = 0; - while (unstETHIdsCountToClaim > 0) { - WithdrawalsBatch memory batch = self.batches[batchIndex]; - uint256 unstETHIdsToClaimInBatch = Math.min(unstETHIdsCountToClaim, batch.size - unstETHIndex); - for (uint256 i = 0; i < unstETHIdsToClaimInBatch; ++i) { - result[i] = batch.fromUnstETHId + unstETHIndex + i; - } - index += unstETHIdsToClaimInBatch; - unstETHIndex += unstETHIdsToClaimInBatch; - unstETHIdsCountToClaim -= unstETHIdsToClaimInBatch; - if (unstETHIndex == batch.size) { - batchIndex += 1; - unstETHIndex = 0; - } - } - self.batchIndex = batchIndex.toUint16(); - self.unstETHIndex = unstETHIndex.toUint16(); + ) internal returns (uint256[] memory unstETHIds) { + (unstETHIds, self.lastClaimedUnstETHIdIndex) = _getNextClaimableUnstETHIds(self, maxUnstETHIdsCount); + self.totalUnstETHClaimed += unstETHIds.length.toUint48(); + emit UnstETHIdsClaimed(unstETHIds); } function getNextWithdrawalsBatches( State storage self, uint256 limit ) internal view returns (uint256[] memory unstETHIds) { - uint256 batchId = self.batchIndex; - uint256 unstETHindex = self.unstETHIndex; - WithdrawalsBatch memory batch = self.batches[batchId]; - uint256 unstETHId = batch.fromUnstETHId + self.unstETHIndex; - uint256 unstETHIdsCount = Math.min(batch.size - unstETHindex, limit); - - unstETHIds = new uint256[](unstETHIdsCount); - for (uint256 i = 0; i < unstETHIdsCount; ++i) { - unstETHIds[i] = unstETHId + i; - } + (unstETHIds,) = _getNextClaimableUnstETHIds(self, limit); } - function checkNotFinalized(State storage self) internal view { - if (self.isFinalized) { - revert AllBatchesAlreadyFormed(); - } - } + function _getNextClaimableUnstETHIds( + State storage self, + uint256 maxUnstETHIdsCount + ) private view returns (uint256[] memory unstETHIds, QueueIndex memory lastClaimedUnstETHIdIndex) { + uint256 unstETHIdsCount = Math.min(self.totalUnstETHCount - self.totalUnstETHClaimed, maxUnstETHIdsCount); - function finalize(State storage self) internal { - self.isFinalized = true; - } + unstETHIds = new uint256[](unstETHIdsCount); + lastClaimedUnstETHIdIndex = self.lastClaimedUnstETHIdIndex; + SequentialBatch currentBatch = self.batches[lastClaimedUnstETHIdIndex.batchIndex]; - function isClaimingFinished(State storage self) internal view returns (bool) { - return self.totalUnstETHClaimed == self.totalUnstETHCount; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + lastClaimedUnstETHIdIndex.valueIndex += 1; + if (currentBatch.size() == lastClaimedUnstETHIdIndex.valueIndex) { + lastClaimedUnstETHIdIndex.batchIndex += 1; + lastClaimedUnstETHIdIndex.valueIndex = 0; + currentBatch = self.batches[lastClaimedUnstETHIdIndex.batchIndex]; + } + unstETHIds[i] = currentBatch.valueAt(lastClaimedUnstETHIdIndex.valueIndex); + } } - function checkClaimingInProgress(State storage self) internal view { - if (!self.isFinalized) { - revert ClaimingNotStarted(); - } - if (self.totalUnstETHCount > 0 && self.totalUnstETHCount == self.totalUnstETHClaimed) { - revert ClaimingIsFinished(); + function _checkStatus(State storage self, Status expectedStatus) private view { + if (self.status != expectedStatus) { + revert InvalidWithdrawalsBatchesQueueStatus(self.status); } } } diff --git a/contracts/types/SequentialBatches.sol b/contracts/types/SequentialBatches.sol new file mode 100644 index 00000000..6d944497 --- /dev/null +++ b/contracts/types/SequentialBatches.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +uint256 constant BATCH_SIZE_LENGTH = 16; +uint256 constant BATCH_SIZE_MASK = 2 ** BATCH_SIZE_LENGTH - 1; + +uint256 constant MAX_BATCH_SIZE = BATCH_SIZE_MASK; +uint256 constant MAX_BATCH_VALUE = 2 ** (256 - BATCH_SIZE_LENGTH) - 1; + +// Stores the info about the withdrawals batch encoded as single uint256 +// The 230 MST bits stores the id of the UnstETH id +// the 16 LST bits stores the size of the batch (max size is 2 ^ 16 - 1= 65535) +type SequentialBatch is uint256; + +error BatchValueOverflow(); +error InvalidBatchSize(uint256 size); +error IndexOutOfBounds(uint256 index); + +using {size} for SequentialBatch global; +using {last} for SequentialBatch global; +using {first} for SequentialBatch global; +using {valueAt} for SequentialBatch global; +using {capacity} for SequentialBatch global; + +function capacity(SequentialBatch) pure returns (uint256) { + return MAX_BATCH_SIZE; +} + +function size(SequentialBatch batch) pure returns (uint256) { + unchecked { + return SequentialBatch.unwrap(batch) & BATCH_SIZE_MASK; + } +} + +function first(SequentialBatch batch) pure returns (uint256) { + unchecked { + return SequentialBatch.unwrap(batch) >> BATCH_SIZE_LENGTH; + } +} + +function last(SequentialBatch batch) pure returns (uint256) { + unchecked { + return batch.first() + batch.size() - 1; + } +} + +function valueAt(SequentialBatch batch, uint256 index) pure returns (uint256) { + if (index >= batch.size()) { + revert IndexOutOfBounds(index); + } + unchecked { + return batch.first() + index; + } +} + +library SequentialBatches { + function create(uint256 seed, uint256 count) internal pure returns (SequentialBatch) { + if (seed > MAX_BATCH_VALUE) { + revert BatchValueOverflow(); + } + if (count == 0 || count > MAX_BATCH_SIZE) { + revert InvalidBatchSize(count); + } + unchecked { + return SequentialBatch.wrap(seed << BATCH_SIZE_LENGTH | count); + } + } + + function canMerge(SequentialBatch b1, SequentialBatch b2) internal pure returns (bool) { + unchecked { + return b1.last() == b2.first() && b1.capacity() - b1.size() > 0; + } + } + + function merge(SequentialBatch b1, SequentialBatch b2) internal pure returns (SequentialBatch b3) { + return create(b1.first(), b1.size() + b2.size()); + } +} diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index e1612568..2a19a373 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -72,11 +72,9 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { rageQuitEscrow.requestNextWithdrawalsBatch(96); } - vm.deal(address(_WITHDRAWAL_QUEUE), vetoedStETHAmount); + vm.deal(address(_WITHDRAWAL_QUEUE), 2 * vetoedStETHAmount); _finalizeWQ(); - uint256 batchSizeLimit = 200; - while (!rageQuitEscrow.getIsWithdrawalsClaimed()) { rageQuitEscrow.claimWithdrawalsBatch(128); } From 157c9ff36398779c7eec5870d8a65322b06718e7 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 5 Jul 2024 12:46:43 +0300 Subject: [PATCH 122/134] 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 123/134] 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); From 09faaa3b107d14d05e2857f15b7193d7cdb490df Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 5 Jul 2024 17:22:12 +0400 Subject: [PATCH 124/134] Update config for Escrow contract --- contracts/Configuration.sol | 3 +++ contracts/Escrow.sol | 12 ++++-------- contracts/interfaces/IConfiguration.sol | 15 ++++++++++++--- contracts/libraries/AssetsAccounting.sol | 2 -- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/contracts/Configuration.sol b/contracts/Configuration.sol index aae54c51..a6ca9e89 100644 --- a/contracts/Configuration.sol +++ b/contracts/Configuration.sol @@ -9,6 +9,9 @@ uint256 constant PERCENT = 10 ** 16; contract Configuration is IConfiguration { error MaxSealablesLimitOverflow(uint256 count, uint256 limit); + uint256 public immutable MIN_WITHDRAWALS_BATCH_SIZE = 8; + uint256 public immutable MAX_WITHDRAWALS_BATCH_SIZE = 128; + // --- // Dual Governance State Properties // --- diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index e5d30f71..3ab11198 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -7,7 +7,7 @@ import {Duration} from "./types/Duration.sol"; import {Timestamp, Timestamps} from "./types/Timestamp.sol"; import {IEscrow} from "./interfaces/IEscrow.sol"; -import {IConfiguration} from "./interfaces/IConfiguration.sol"; +import {IEscrowConfigration} from "./interfaces/IConfiguration.sol"; import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; @@ -66,10 +66,6 @@ contract Escrow is IEscrow { address public immutable MASTER_COPY; - // TODO: move to config - uint256 public immutable MIN_BATCH_SIZE = 8; - uint256 public immutable MAX_BATCH_SIZE = 128; - uint256 public immutable MIN_WITHDRAWAL_REQUEST_AMOUNT; uint256 public immutable MAX_WITHDRAWAL_REQUEST_AMOUNT; @@ -77,7 +73,7 @@ contract Escrow is IEscrow { IWstETH public immutable WST_ETH; IWithdrawalQueue public immutable WITHDRAWAL_QUEUE; - IConfiguration public immutable CONFIG; + IEscrowConfigration public immutable CONFIG; EscrowState internal _escrowState; IDualGovernance private _dualGovernance; @@ -92,7 +88,7 @@ contract Escrow is IEscrow { ST_ETH = IStETH(stETH); WST_ETH = IWstETH(wstETH); MASTER_COPY = address(this); - CONFIG = IConfiguration(config); + CONFIG = IEscrowConfigration(config); WITHDRAWAL_QUEUE = IWithdrawalQueue(withdrawalQueue); MIN_WITHDRAWAL_REQUEST_AMOUNT = WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT(); MAX_WITHDRAWAL_REQUEST_AMOUNT = WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); @@ -212,7 +208,7 @@ contract Escrow is IEscrow { _checkEscrowState(EscrowState.RageQuitEscrow); _batchesQueue.checkOpened(); - if (maxBatchSize < MIN_BATCH_SIZE || maxBatchSize > MAX_BATCH_SIZE) { + if (maxBatchSize < CONFIG.MIN_WITHDRAWALS_BATCH_SIZE() || maxBatchSize > CONFIG.MAX_WITHDRAWALS_BATCH_SIZE()) { revert InvalidBatchSize(maxBatchSize); } diff --git a/contracts/interfaces/IConfiguration.sol b/contracts/interfaces/IConfiguration.sol index 26826dd5..14d65504 100644 --- a/contracts/interfaces/IConfiguration.sol +++ b/contracts/interfaces/IConfiguration.sol @@ -19,6 +19,12 @@ struct DualGovernanceConfig { uint256[3] rageQuitEthClaimTimelockGrowthCoeffs; } +interface IEscrowConfigration { + function MIN_WITHDRAWALS_BATCH_SIZE() external view returns (uint256); + function MAX_WITHDRAWALS_BATCH_SIZE() external view returns (uint256); + function SIGNALLING_ESCROW_MIN_LOCK_TIME() external view returns (Duration); +} + interface IAdminExecutorConfiguration { function ADMIN_EXECUTOR() external view returns (address); } @@ -52,8 +58,6 @@ interface IDualGovernanceConfiguration { function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_B() external view returns (uint256); function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_C() external view returns (uint256); - function SIGNALLING_ESCROW_MIN_LOCK_TIME() external view returns (Duration); - function sealableWithdrawalBlockers() external view returns (address[] memory); function getSignallingThresholdData() @@ -69,4 +73,9 @@ interface IDualGovernanceConfiguration { function getDualGovernanceConfig() external view returns (DualGovernanceConfig memory config); } -interface IConfiguration is IAdminExecutorConfiguration, ITimelockConfiguration, IDualGovernanceConfiguration {} +interface IConfiguration is + IEscrowConfigration, + ITimelockConfiguration, + IAdminExecutorConfiguration, + IDualGovernanceConfiguration +{} diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index bdceef3d..2de7288d 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -10,8 +10,6 @@ import {WithdrawalRequestStatus} from "../interfaces/IWithdrawalQueue.sol"; import {Duration} from "../types/Duration.sol"; import {Timestamps, Timestamp} from "../types/Timestamp.sol"; -import {ArrayUtils} from "../utils/arrays.sol"; - struct HolderAssets { // The total shares amount of stETH/wstETH accounted to the holder SharesValue stETHLockedShares; From fa1b3f12403f9040ebab67b285c5856276ac96ea Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Thu, 4 Jul 2024 13:48:10 +0300 Subject: [PATCH 125/134] feat: add natspec for timelock --- contracts/EmergencyProtectedTimelock.sol | 125 ++++++++++++++++++++--- 1 file changed, 109 insertions(+), 16 deletions(-) diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 0f9ca933..d68a3e96 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -9,6 +9,14 @@ import {EmergencyProtection, EmergencyState} from "./libraries/EmergencyProtecti import {ConfigurationProvider} from "./ConfigurationProvider.sol"; +/** + * @title EmergencyProtectedTimelock + * @dev A timelock contract with emergency protection functionality. + * The contract allows for submitting, scheduling, and executing proposals, + * while providing emergency protection features to prevent unauthorized + * execution during emergency situations. + */ + contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { using Proposals for Proposals.State; using EmergencyProtection for EmergencyProtection.State; @@ -25,52 +33,93 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { constructor(address config) ConfigurationProvider(config) {} + /** + * @dev Submits a new proposal to execute a series of calls through an executor. + * Only the governance contract can call this function. + * @param executor The address of the executor contract that will execute the calls. + * @param calls An array of `ExecutorCall` structs representing the calls to be executed. + * @return newProposalId The ID of the newly created proposal. + */ function submit(address executor, ExecutorCall[] calldata calls) external returns (uint256 newProposalId) { _checkGovernance(msg.sender); newProposalId = _proposals.submit(executor, calls); } + /** + * @dev Schedules a proposal for execution after a specified delay. + * Only the governance contract can call this function. + * @param proposalId The ID of the proposal to be scheduled. + */ function schedule(uint256 proposalId) external { _checkGovernance(msg.sender); _proposals.schedule(proposalId, CONFIG.AFTER_SUBMIT_DELAY()); } + /** + * @dev Executes a scheduled proposal. + * Checks if emergency mode is active and prevents execution if it is. + * @param proposalId The ID of the proposal to be executed. + */ function execute(uint256 proposalId) external { _emergencyProtection.checkEmergencyModeActive(false); _proposals.execute(proposalId, CONFIG.AFTER_SCHEDULE_DELAY()); } + /** + * @dev Cancels all non-executed proposals. + * Only the governance contract can call this function. + */ function cancelAllNonExecutedProposals() external { _checkGovernance(msg.sender); _proposals.cancelAll(); } + /** + * @dev Transfers ownership of the executor contract to a new owner. + * Only the admin executor can call this function. + * @param executor The address of the executor contract. + * @param owner The address of the new owner. + */ function transferExecutorOwnership(address executor, address owner) external { _checkAdminExecutor(msg.sender); IOwnable(executor).transferOwnership(owner); } + /** + * @dev Sets a new governance contract address. + * Only the admin executor can call this function. + * @param newGovernance The address of the new governance contract. + */ function setGovernance(address newGovernance) external { _checkAdminExecutor(msg.sender); _setGovernance(newGovernance); } - // --- - // Emergency Protection Functionality - // --- - + /** + * @dev Activates the emergency mode. + * Only the activation committee can call this function. + */ function activateEmergencyMode() external { _emergencyProtection.checkActivationCommittee(msg.sender); _emergencyProtection.checkEmergencyModeActive(false); _emergencyProtection.activate(); } + /** + * @dev Executes a proposal during emergency mode. + * Checks if emergency mode is active and if the caller is part of the execution committee. + * @param proposalId The ID of the proposal to be executed. + */ function emergencyExecute(uint256 proposalId) external { _emergencyProtection.checkEmergencyModeActive(true); _emergencyProtection.checkExecutionCommittee(msg.sender); _proposals.execute(proposalId, /* afterScheduleDelay */ 0); } + /** + * @dev Deactivates the emergency mode. + * If the emergency mode has not passed, only the admin executor can call this function. + */ function deactivateEmergencyMode() external { _emergencyProtection.checkEmergencyModeActive(true); if (!_emergencyProtection.isEmergencyModePassed()) { @@ -80,6 +129,10 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { _proposals.cancelAll(); } + /** + * @dev Resets the system after entering the emergency mode. + * Only the execution committee can call this function. + */ function emergencyReset() external { _emergencyProtection.checkEmergencyModeActive(true); _emergencyProtection.checkExecutionCommittee(msg.sender); @@ -88,6 +141,14 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { _proposals.cancelAll(); } + /** + * @dev Sets the parameters for the emergency protection functionality. + * Only the admin executor can call this function. + * @param activator The address of the activation committee. + * @param enactor The address of the execution committee. + * @param protectionDuration The duration of the protection period. + * @param emergencyModeDuration The duration of the emergency mode. + */ function setEmergencyProtection( address activator, address enactor, @@ -98,51 +159,79 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { _emergencyProtection.setup(activator, enactor, protectionDuration, emergencyModeDuration); } + /** + * @dev Checks if the emergency protection functionality is enabled. + * @return A boolean indicating if the emergency protection is enabled. + */ function isEmergencyProtectionEnabled() external view returns (bool) { return _emergencyProtection.isEmergencyProtectionEnabled(); } + /** + * @dev Retrieves the current emergency state. + * @return res The EmergencyState struct containing the current emergency state. + */ function getEmergencyState() external view returns (EmergencyState memory res) { res = _emergencyProtection.getEmergencyState(); } - // --- - // Timelock View Methods - // --- - + /** + * @dev Retrieves the address of the current governance contract. + * @return The address of the current governance contract. + */ function getGovernance() external view returns (address) { return _governance; } + /** + * @dev Retrieves the details of a proposal. + * @param proposalId The ID of the proposal. + * @return proposal The Proposal struct containing the details of the proposal. + */ function getProposal(uint256 proposalId) external view returns (Proposal memory proposal) { proposal = _proposals.get(proposalId); } + /** + * @dev Retrieves the total number of proposals. + * @return count The total number of proposals. + */ function getProposalsCount() external view returns (uint256 count) { count = _proposals.count(); } + /** + * @dev Retrieves the submission time of a proposal. + * @param proposalId The ID of the proposal. + * @return submittedAt The submission time of the proposal. + */ function getProposalSubmissionTime(uint256 proposalId) external view returns (uint256 submittedAt) { submittedAt = _proposals.getProposalSubmissionTime(proposalId); } - // --- - // Proposals Lifecycle View Methods - // --- - + /** + * @dev Checks if a proposal can be executed. + * @param proposalId The ID of the proposal. + * @return A boolean indicating if the proposal can be executed. + */ function canExecute(uint256 proposalId) external view returns (bool) { return !_emergencyProtection.isEmergencyModeActivated() && _proposals.canExecute(proposalId, CONFIG.AFTER_SCHEDULE_DELAY()); } + /** + * @dev Checks if a proposal can be scheduled. + * @param proposalId The ID of the proposal. + * @return A boolean indicating if the proposal can be scheduled. + */ function canSchedule(uint256 proposalId) external view returns (bool) { return _proposals.canSchedule(proposalId, CONFIG.AFTER_SUBMIT_DELAY()); } - // --- - // Internal Methods - // --- - + /** + * @dev Internal function to set the governance contract address. + * @param newGovernance The address of the new governance contract. + */ function _setGovernance(address newGovernance) internal { address prevGovernance = _governance; if (newGovernance == prevGovernance || newGovernance == address(0)) { @@ -152,6 +241,10 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { emit GovernanceSet(newGovernance); } + /** + * @dev Internal function to check if the caller is the governance contract. + * @param account The address to check. + */ function _checkGovernance(address account) internal view { if (_governance != account) { revert NotGovernance(account, _governance); From 2f967c9dc49416d04725ca749bbb94813e8e3883 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 8 Jul 2024 02:25:21 +0400 Subject: [PATCH 126/134] Update the Escrow section in the spec. Tweak vars and methods names --- contracts/DualGovernance.sol | 6 +- contracts/Escrow.sol | 66 +++--- docs/specification.md | 235 ++++++++++---------- test/scenario/escrow.t.sol | 10 +- test/scenario/veto-cooldown-mechanics.t.sol | 6 +- test/utils/scenario-test-blueprint.sol | 14 +- 6 files changed, 169 insertions(+), 168 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 7e82e1d6..dd81ff7b 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -61,11 +61,11 @@ contract DualGovernance is IGovernance, ConfigurationProvider { TIMELOCK.cancelAllNonExecutedProposals(); } - function vetoSignallingEscrow() external view returns (address) { + function getVetoSignallingEscrow() external view returns (address) { return address(_dgState.signallingEscrow); } - function rageQuitEscrow() external view returns (address) { + function getRageQuitEscrow() external view returns (address) { return address(_dgState.rageQuitEscrow); } @@ -81,7 +81,7 @@ contract DualGovernance is IGovernance, ConfigurationProvider { _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); } - function currentState() external view returns (State) { + function getCurrentState() external view returns (State) { return _dgState.currentState(); } diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 3ab11198..a0d274b1 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -80,7 +80,7 @@ contract Escrow is IEscrow { AssetsAccounting.State private _accounting; WithdrawalsBatchesQueue.State private _batchesQueue; - Duration internal _rageQuitExtraTimelock; + Duration internal _rageQuitExtensionDelay; Duration internal _rageQuitWithdrawalsTimelock; Timestamp internal _rageQuitTimelockStartedAt; @@ -111,17 +111,18 @@ contract Escrow is IEscrow { // Lock & Unlock stETH // --- - function lockStETH(uint256 amount) external { - uint256 shares = ST_ETH.getSharesByPooledEth(amount); - _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(shares)); - ST_ETH.transferSharesFrom(msg.sender, address(this), shares); + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { + lockedStETHShares = ST_ETH.getSharesByPooledEth(amount); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); + ST_ETH.transferSharesFrom(msg.sender, address(this), lockedStETHShares); _activateNextGovernanceState(); } - function unlockStETH() external { + function unlockStETH() external returns (uint256 unlockedStETHShares) { + _activateNextGovernanceState(); _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); - SharesValue sharesUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); - ST_ETH.transferShares(msg.sender, sharesUnlocked.toUint256()); + unlockedStETHShares = _accounting.accountStETHSharesUnlock(msg.sender).toUint256(); + ST_ETH.transferShares(msg.sender, unlockedStETHShares); _activateNextGovernanceState(); } @@ -129,20 +130,20 @@ contract Escrow is IEscrow { // Lock / Unlock wstETH // --- - function lockWstETH(uint256 amount) external { + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { WST_ETH.transferFrom(msg.sender, address(this), amount); - uint256 stETHAmount = WST_ETH.unwrap(amount); - _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(ST_ETH.getSharesByPooledEth(stETHAmount))); + lockedStETHShares = ST_ETH.getSharesByPooledEth(WST_ETH.unwrap(amount)); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); _activateNextGovernanceState(); } - function unlockWstETH() external returns (uint256) { + function unlockWstETH() external returns (uint256 unlockedStETHShares) { + _activateNextGovernanceState(); _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); SharesValue wstETHUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); - uint256 wstETHAmount = WST_ETH.wrap(ST_ETH.getPooledEthByShares(wstETHUnlocked.toUint256())); - WST_ETH.transfer(msg.sender, wstETHAmount); + unlockedStETHShares = WST_ETH.wrap(ST_ETH.getPooledEthByShares(wstETHUnlocked.toUint256())); + WST_ETH.transfer(msg.sender, unlockedStETHShares); _activateNextGovernanceState(); - return wstETHAmount; } // --- @@ -156,15 +157,18 @@ contract Escrow is IEscrow { for (uint256 i = 0; i < unstETHIdsCount; ++i) { WITHDRAWAL_QUEUE.transferFrom(msg.sender, address(this), unstETHIds[i]); } + _activateNextGovernanceState(); } function unlockUnstETH(uint256[] memory unstETHIds) external { + _activateNextGovernanceState(); _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); _accounting.accountUnstETHUnlock(msg.sender, unstETHIds); uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { WITHDRAWAL_QUEUE.transferFrom(address(this), msg.sender, unstETHIds[i]); } + _activateNextGovernanceState(); } function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external { @@ -194,13 +198,13 @@ contract Escrow is IEscrow { // State Updates // --- - function startRageQuit(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock) external { + function startRageQuit(Duration rageQuitExtensionDelay, Duration rageQuitWithdrawalsTimelock) external { _checkDualGovernance(msg.sender); _checkEscrowState(EscrowState.SignallingEscrow); _batchesQueue.open(); _escrowState = EscrowState.RageQuitEscrow; - _rageQuitExtraTimelock = rageQuitExtraTimelock; + _rageQuitExtensionDelay = rageQuitExtensionDelay; _rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; } @@ -226,7 +230,7 @@ contract Escrow is IEscrow { _batchesQueue.add(WITHDRAWAL_QUEUE.requestWithdrawals(requestAmounts, address(this))); } - function claimWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { _checkEscrowState(EscrowState.RageQuitEscrow); if (!_rageQuitTimelockStartedAt.isZero()) { revert ClaimingIsFinished(); @@ -234,12 +238,12 @@ contract Escrow is IEscrow { uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(maxUnstETHIdsCount); - _claimWithdrawalsBatch( + _claimNextWithdrawalsBatch( unstETHIds, WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, WITHDRAWAL_QUEUE.getLastCheckpointIndex()) ); } - function claimWithdrawalsBatch(uint256 fromUnstETHIds, uint256[] calldata hints) external { + function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external { _checkEscrowState(EscrowState.RageQuitEscrow); if (!_rageQuitTimelockStartedAt.isZero()) { revert ClaimingIsFinished(); @@ -247,14 +251,14 @@ contract Escrow is IEscrow { uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(hints.length); - if (unstETHIds.length > 0 && fromUnstETHIds != unstETHIds[0]) { + if (unstETHIds.length > 0 && fromUnstETHId != unstETHIds[0]) { revert UnexpectedUnstETHId(); } if (hints.length != unstETHIds.length) { revert InvalidHintsLength(hints.length, unstETHIds.length); } - _claimWithdrawalsBatch(unstETHIds, hints); + _claimNextWithdrawalsBatch(unstETHIds, hints); } function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external { @@ -310,15 +314,15 @@ contract Escrow is IEscrow { state.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp.toSeconds(); } - function getNextWithdrawalBatches(uint256 limit) external view returns (uint256[] memory unstETHIds) { + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds) { return _batchesQueue.getNextWithdrawalsBatches(limit); } - function getIsWithdrawalsBatchesFinalized() external view returns (bool) { + function isWithdrawalsBatchesFinalized() external view returns (bool) { return _batchesQueue.isClosed(); } - function getIsWithdrawalsClaimed() external view returns (bool) { + function isWithdrawalsClaimed() external view returns (bool) { return !_rageQuitTimelockStartedAt.isZero(); } @@ -343,12 +347,12 @@ contract Escrow is IEscrow { return ( _escrowState == EscrowState.RageQuitEscrow && _batchesQueue.isClosed() && !_rageQuitTimelockStartedAt.isZero() - && Timestamps.now() > _rageQuitExtraTimelock.addTo(_rageQuitTimelockStartedAt) + && Timestamps.now() > _rageQuitExtensionDelay.addTo(_rageQuitTimelockStartedAt) ); } // --- - // RECEIVE + // Receive ETH // --- receive() external payable { @@ -361,12 +365,14 @@ contract Escrow is IEscrow { // Internal Methods // --- - function _claimWithdrawalsBatch(uint256[] memory unstETHIds, uint256[] memory hints) internal { + function _claimNextWithdrawalsBatch(uint256[] memory unstETHIds, uint256[] memory hints) internal { uint256 ethBalanceBefore = address(this).balance; WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); uint256 ethAmountClaimed = address(this).balance - ethBalanceBefore; - _accounting.accountClaimedStETH(ETHValues.from(ethAmountClaimed)); + if (ethAmountClaimed > 0) { + _accounting.accountClaimedStETH(ETHValues.from(ethAmountClaimed)); + } if (_batchesQueue.isClosed() && _batchesQueue.isAllUnstETHClaimed()) { _rageQuitTimelockStartedAt = Timestamps.now(); @@ -393,7 +399,7 @@ contract Escrow is IEscrow { if (_rageQuitTimelockStartedAt.isZero()) { revert RageQuitExtraTimelockNotStarted(); } - Duration withdrawalsTimelock = _rageQuitExtraTimelock + _rageQuitWithdrawalsTimelock; + Duration withdrawalsTimelock = _rageQuitExtensionDelay + _rageQuitWithdrawalsTimelock; if (Timestamps.now() <= withdrawalsTimelock.addTo(_rageQuitTimelockStartedAt)) { revert WithdrawalsTimelockNotPassed(); } diff --git a/docs/specification.md b/docs/specification.md index 8152a4fd..8e402254 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -64,7 +64,7 @@ The general proposal flow is the following: Each submitted proposal requires a minimum timelock before it can be scheduled for execution. -At any time, including while a proposal's timelock is lasting, stakers can signal their opposition to the DAO by locking their (w)stETH or stETH Withdrawal NFTs (unstETH) into the [signalling escrow contract](#Contract-Escrowsol). If the opposition exceeds some minimum threshold, the [global governance state](#Governance-state) gets changed, blocking any DAO execution and thus effectively extending the timelock of all pending (i.e. submitted but not scheduled for execution) proposals. +At any time, including while a proposal's timelock is lasting, stakers can signal their opposition to the DAO by locking their (w)stETH or stETH withdrawal NFTs (unstETH) into the [signalling escrow contract](#Contract-Escrowsol). If the opposition exceeds some minimum threshold, the [global governance state](#Governance-state) gets changed, blocking any DAO execution and thus effectively extending the timelock of all pending (i.e. submitted but not scheduled for execution) proposals. ![image](https://github.com/lidofinance/dual-governance/assets/1699593/98273df0-f3fd-4149-929d-3315a8e81aa8) @@ -442,17 +442,17 @@ The result of the call. ## Contract: Escrow.sol -The `Escrow` contract serves as an accumulator of users' (w)stETH, withdrawal NFTs, and ETH. It has two internal states and serves a different purpose depending on its state: +The `Escrow` contract serves as an accumulator of users' (w)stETH, withdrawal NFTs (unstETH), and ETH. It has two internal states and serves a different purpose depending on its state: * The initial state is the `SignallingEscrow` state. In this state, the contract serves as an oracle for users' opposition to DAO proposals. It allows users to lock and unlock (unlocking is permitted only for the caller after the `SignallingEscrowMinLockTime` duration has passed since their last funds locking operation) stETH, wstETH, and withdrawal NFTs, potentially changing the global governance state. The `SignallingEscrowMinLockTime` duration, measured in hours, safeguards against manipulating the dual governance state through instant lock/unlock actions within the `Escrow` contract instance. * The final state is the `RageQuitEscrow` state. In this state, the contract serves as an immutable and ungoverned accumulator for the ETH withdrawn as a result of the [rage quit](#Rage-quit) and enforces a timelock on reclaiming this ETH by users. -The `DualGovernance` contract tracks the current signalling escrow contract using the `DualGovernance.signallingEscrow` pointer. Upon the initial deployment of the system, an instance of `Escrow` is deployed in the `SignallingEscrow` state by the `DualGovernance` contract and the `DualGovernance.signallingEscrow` pointer is set to this contract. +The `DualGovernance` contract tracks the current signalling escrow contract using the `DualGovernance.getVetoSignallingEscrow()` pointer. Upon the initial deployment of the system, an instance of `Escrow` is deployed in the `SignallingEscrow` state by the `DualGovernance` contract and the `DualGovernance.getVetoSignallingEscrow()` pointer is set to this contract. Each time the governance enters the global `RageQuit` state, two things happen simultaneously: -1. The `Escrow` instance currently stored in the `DualGovernance.signallingEscrow` pointer changes its state from `SignallingEscrow` to `RageQuitEscrow`. This is the only possible (and thus irreversible) state transition. -2. The `DualGovernance` contract deploys a new instance of `Escrow` in the `SignallingEscrow` state and resets the `DualGovernance.signallingEscrow` pointer to this newly-deployed contract. +1. The `Escrow` instance currently stored in the `DualGovernance.getVetoSignallingEscrow()` pointer changes its state from `SignallingEscrow` to `RageQuitEscrow`. This is the only possible (and thus irreversible) state transition. +2. The `DualGovernance` contract deploys a new instance of `Escrow` in the `SignallingEscrow` state and resets the `DualGovernance.getVetoSignallingEscrow()` pointer to this newly-deployed contract. At any point in time, there can be only one instance of the contract in the `SignallingEscrow` state (so the contract in this state is a singleton) but multiple instances of the contract in the `RageQuitEscrow` state. @@ -469,7 +469,7 @@ The duration of the `RageQuitEthWithdrawalsTimelock` is dynamic and varies based ### Function: Escrow.lockStETH ```solidity! -function lockStETH(uint256 amount) +function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) ``` Transfers the specified `amount` of stETH from the caller's (i.e., `msg.sender`) account into the `SignallingEscrow` instance of the `Escrow` contract. @@ -477,15 +477,19 @@ Transfers the specified `amount` of stETH from the caller's (i.e., `msg.sender`) The total rage quit support is updated proportionally to the number of shares corresponding to the locked stETH (see the `Escrow.getRageQuitSupport()` function for the details). For the correct rage quit support calculation, the function updates the number of locked stETH shares in the protocol as follows: ```solidity -uint256 amountInShares = stETH.getSharesByPooledEther(amount); +amountInShares = stETH.getSharesByPooledEther(amount); -_vetoersLockedAssets[msg.sender].stETHShares += amountInShares; -_totalStEthSharesLocked += amountInShares; +_assets[msg.sender].stETHLockedShares += amountInShares; +_stETHTotals.lockedShares += amountInShares; ``` The rage quit support will be dynamically updated to reflect changes in the stETH balance due to protocol rewards or validators slashing. -Finally, calls the `DualGovernance.activateNextState()` function. This action may transit the `Escrow` instance from the `SignallingEscrow` state into the `RageQuitEscrow` state. +Finally, the function calls `DualGovernance.activateNextState()`, which may transition the `Escrow` instance from the `SignallingEscrow` state to the `RageQuitEscrow` state. + +#### Returns + +The amount of stETH shares locked by the caller during the current method call. #### Preconditions @@ -496,42 +500,54 @@ Finally, calls the `DualGovernance.activateNextState()` function. This action ma ### Function: Escrow.unlockStETH ```solidity -function unlockStETH() +function unlockStETH() external returns (uint256 unlockedStETHShares) ``` -Allows the caller (i.e. `msg.sender`) to unlock the previously locked stETH in the `SignallingEscrow` instance of the `Escrow` contract. The locked stETH balance may change due to protocol rewards or validators slashing, potentially altering the original locked amount. The total unlocked stETH equals the sum of all previously locked stETH by the caller, accounting for any changes during the locking period. +Allows the caller (i.e., `msg.sender`) to unlock all previously locked stETH and wstETH in the `SignallingEscrow` instance of the `Escrow` contract as stETH. The locked balance may change due to protocol rewards or validator slashing, potentially altering the original locked amount. The total unlocked stETH amount equals the sum of all previously locked stETH and wstETH by the caller, accounting for any changes during the locking period. -For the correct rage quit support calculation, the function updates the number of locked stETH shares in the protocol as follows: +For accurate rage quit support calculation, the function updates the number of locked stETH shares in the protocol as follows: ```solidity -_totalStEthSharesLocked -= _vetoersLockedAssets[msg.sender].stETHShares; -_vetoersLockedAssets[msg.sender].stETHShares = 0; +_stETHTotals.lockedShares -= _assets[msg.sender].stETHLockedShares; +_assets[msg.sender].stETHLockedShares = 0; ``` Additionally, the function triggers the `DualGovernance.activateNextState()` function at the beginning and end of the execution. +#### Returns + +The amount of stETH shares unlocked by the caller. + #### Preconditions - The `Escrow` instance MUST be in the `SignallingEscrow` state. - The caller MUST have a non-zero amount of previously locked stETH in the `Escrow` instance using the `Escrow.lockStETH` function. -- At least the duration of the `SignallingEscrowMinLockTime` MUST have passed since the caller last invoked any of the methods `Escrow.lockStETH`, `Escrow.lockWstETH`, or `Escrow.lockUnstETH`. +- The duration of the `SignallingEscrowMinLockTime` MUST have passed since the caller last invoked any of the methods `Escrow.lockStETH`, `Escrow.lockWstETH`, or `Escrow.lockUnstETH`. ### Function: Escrow.lockWstETH ```solidity -function lockWstETH(uint256 amount) +function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) ``` -Transfers the specified `amount` of wstETH from the caller's (i.e., `msg.sender`) account into the `SignallingEscrow` instance of the `Escrow` contract. +Transfers the specified `amount` of wstETH from the caller's (i.e., `msg.sender`) account into the `SignallingEscrow` instance of the `Escrow` contract and unwraps it into the stETH. -The total rage quit support is updated proportionally to the `amount` of locked wstETH (see the `Escrow.getRageQuitSupport()` function for the details). For the correct rage quit support calculation, the function updates the number of locked wstETH in the protocol as follows: +The total rage quit support is updated proportionally to the `amount` of locked wstETH (see the `Escrow.getRageQuitSupport()` function for details). For accurate rage quit support calculation, the function updates the number of locked stETH shares in the protocol as follows: ```solidity -_vetoersLockedAssets[msg.sender].wstETHShares += amount; -_totalStEthSharesLocked += amount; +stETHAmount = WST_ETH.unwrap(amount); +// Use getSharesByPooledEther(), because unwrap() method may transfer 1 wei less amount of stETH +stETHShares = ST_ETH.getSharesByPooledEth(stETHAmount); + +_assets[msg.sender].stETHLockedShares += stETHShares; +_stETHTotals.lockedShares += stETHShares; ``` -Finally, calls the `DualGovernance.activateNextState()` function. This action may transit the `Escrow` instance from the `SignallingEscrow` state into the `RageQuitEscrow` state. +Finally, the function calls the `DualGovernance.activateNextState()`. This action may transition the `Escrow` instance from the `SignallingEscrow` state into the `RageQuitEscrow` state. + +#### Returns + +The amount of stETH shares locked by the caller during the current method call. #### Preconditions @@ -542,20 +558,24 @@ Finally, calls the `DualGovernance.activateNextState()` function. This action ma ### Function: Escrow.unlockWstETH ```solidity -function unlockWstETH() +function unlockWstETH() external returns (uint256 unlockedStETHShares) ``` -Allows the caller (i.e. `msg.sender`) to unlock previously locked wstETH from the `SignallingEscrow` instance of the `Escrow` contract. The total unlocked wstETH equals the sum of all previously locked wstETH by the caller. +Allows the caller (i.e. `msg.sender`) to unlock previously locked wstETH and stETH from the `SignallingEscrow` instance of the `Escrow` contract as wstETH. The locked balance may change due to protocol rewards or validator slashing, potentially altering the original locked amount. The total unlocked wstETH equals the sum of all previously locked wstETH and stETH by the caller. -For the correct rage quit support calculation, the function updates the number of locked wstETH shares in the protocol as follows: +For the correct rage quit support calculation, the function updates the number of locked stETH shares in the protocol as follows: ```solidity -_totalStEthSharesLocked -= _vetoersLockedAssets[msg.sender].wstETHShares; -_vetoersLockedAssets[msg.sender].wstETHShares = 0; +_stETHTotals.lockedShares -= _assets[msg.sender].stETHLockedShares; +_assets[msg.sender].stETHLockedShares = 0; ``` Additionally, the function triggers the `DualGovernance.activateNextState()` function at the beginning and end of the execution. +#### Returns + +The amount of stETH shares unlocked by the caller. + #### Preconditions - The `Escrow` instance MUST be in the `SignallingEscrow` state. @@ -569,15 +589,16 @@ Additionally, the function triggers the `DualGovernance.activateNextState()` fun function lockUnstETH(uint256[] unstETHIds) ``` -Transfers the WIthdrawal NFTs with ids contained in the `unstETHIds` from the caller's (i.e. `msg.sender`) account into the `SignallingEscrow` instance of the `Escrow` contract. +Transfers the withdrawal NFTs with ids contained in the `unstETHIds` from the caller's (i.e. `msg.sender`) account into the `SignallingEscrow` instance of the `Escrow` contract. + -To correctly calculate the rage quit support (see the `Escrow.getRageQuitSupport()` function for the details), updates the number of locked Withdrawal NFT shares in the protocol for each withdrawal NFT in the `unstETHIds`, as follows: +To correctly calculate the rage quit support (see the `Escrow.getRageQuitSupport()` function for the details), updates the number of locked withdrawal NFT shares in the protocol for each withdrawal NFT in the `unstETHIds`, as follows: ```solidity -uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; +uint256 amountOfShares = withdrawalRequests[id].amountOfShares; -_vetoersLockedAssets[msg.sender].withdrawalNFTShares += amountOfShares; -_totalWithdrawlNFTSharesLocked += amountOfShares; +_assets[msg.sender].unstETHLockedShares += amountOfShares; +_unstETHTotals.unfinalizedShares += amountOfShares; ``` Finally, calls the `DualGovernance.activateNextState()` function. This action may transition the `Escrow` instance from the `SignallingEscrow` state into the `RageQuitEscrow` state. @@ -587,7 +608,7 @@ Finally, calls the `DualGovernance.activateNextState()` function. This action ma - The `Escrow` instance MUST be in the `SignallingEscrow` state. - The caller MUST be the owner of all withdrawal NFTs with the given ids. - The caller MUST grant permission to the `SignallingEscrow` instance to transfer tokens with the given ids (`approve()` or `setApprovalForAll()`). -- The passed ids MUST NOT contain the finalized or claimed Withdrawal NFTs. +- The passed ids MUST NOT contain the finalized or claimed withdrawal NFTs. - The passed ids MUST NOT contain duplicates. ### Function: Escrow.unlockUnstETH @@ -596,32 +617,28 @@ Finally, calls the `DualGovernance.activateNextState()` function. This action ma function unlockUnstETH(uint256[] unstETHIds) ``` -Allows the caller (i.e. `msg.sender`) to unlock a set of previously locked Withdrawal NFTs with ids `unstETHIds` from the `SignallingEscrow` instance of the `Escrow` contract. +Allows the caller (i.e. `msg.sender`) to unlock a set of previously locked withdrawal NFTs with ids `unstETHIds` from the `SignallingEscrow` instance of the `Escrow` contract. -To correctly calculate the rage quit support (see the `Escrow.getRageQuitSupport()` function for details), updates the number of locked Withdrawal NFT shares in the protocol for each withdrawal NFT in the `unstETHIds`, as follows: +To correctly calculate the rage quit support (see the `Escrow.getRageQuitSupport()` function for details), updates the number of locked withdrawal NFT shares in the protocol for each withdrawal NFT in the `unstETHIds`, as follows: -- If the Withdrawal NFT was marked as finalized (see the `Escrow.markUnstETHFinalized()` function for details): +- If the withdrawal NFT was marked as finalized (see the `Escrow.markUnstETHFinalized()` function for details): ```solidity -uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; +uint256 amountOfShares = withdrawalRequests[id].amountOfShares; uint256 claimableAmount = _getClaimableEther(id); -_totalWithdrawlNFTSharesLocked -= amountOfShares; -_totalFinalizedWithdrawlNFTSharesLocked -= amountOfShares; -_totalFinalizedWithdrawlNFTAmountLocked -= claimableAmount; - -_vetoersLockedAssets[msg.sender].withdrawalNFTShares -= amountOfShares; -_vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTShares -= amountOfShares; -_vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTAmount -= claimableAmount; +assets[msg.sender].unstETHLockedShares -= amountOfShares; +unstETHTotals.finalizedETH -= claimableAmount; +unstETHTotals.unfinalizedShares -= amountOfShares; ``` - if the Withdrawal NFT wasn't marked as finalized: ```solidity -uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; +uint256 amountOfShares = withdrawalRequests[id].amountOfShares; -_totalWithdrawlNFTSharesLocked -= amountOfShares; -_vetoersLockedAssets[msg.sender].withdrawalNFTShares -= amountOfShares; +assets[msg.sender].unstETHLockedShares -= amountOfShares; +unstETHTotals.unfinalizedShares -= amountOfShares; ``` Additionally, the function triggers the `DualGovernance.activateNextState()` function at the beginning and end of the execution. @@ -629,7 +646,7 @@ Additionally, the function triggers the `DualGovernance.activateNextState()` fun #### Preconditions - The `Escrow` instance MUST be in the `SignallingEscrow` state. -- Each provided Withdrawal NFT MUST have been previously locked by the caller. +- Each provided withdrawal NFT MUST have been previously locked by the caller. - At least the duration of the `SignallingEscrowMinLockTime` MUST have passed since the caller last invoked any of the methods `Escrow.lockStETH`, `Escrow.lockWstETH`, or `Escrow.lockUnstETH`. ### Function Escrow.markUnstETHFinalized @@ -638,31 +655,28 @@ Additionally, the function triggers the `DualGovernance.activateNextState()` fun function markUnstETHFinalized(uint256[] unstETHIds, uint256[] hints) ``` -Marks the provided Withdrawal NFTs with ids `unstETHIds` as finalized to accurately calculate their rage quit support. +Marks the provided withdrawal NFTs with ids `unstETHIds` as finalized to accurately calculate their rage quit support. -The finalization of the Withdrawal NFT leads to the following events: +The finalization of the withdrawal NFT leads to the following events: -- The value of the Withdrawal NFT is no longer affected by stETH token rebases. -- The total supply of stETH is adjusted based on the value of the finalized Withdrawal NFT. +- The value of the withdrawal NFT is no longer affected by stETH token rebases. +- The total supply of stETH is adjusted based on the value of the finalized withdrawal NFT. -As both of these events affect the rage quit support value, this function updates the number of finalized Withdrawal NFTs for the correct rage quit support accounting. +As both of these events affect the rage quit support value, this function updates the number of finalized withdrawal NFTs for the correct rage quit support accounting. -For each Withdrawal NFT in the `unstETHIds`: +For each withdrawal NFT in the `unstETHIds`: ```solidity uint256 claimableAmount = _getClaimableEther(id); -uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; +uint256 amountOfShares = withdrawalRequests[id].amountOfShares; -_totalFinalizedWithdrawlNFTSharesLocked += amountOfShares; -_totalFinalizedWithdrawlNFTAmountLocked += claimableAmount; - -_vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTShares += amountOfShares; -_vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTAmount += claimableAmount; +unstETHTotals.finalizedETH += claimableAmount; +unstETHTotals.unfinalizedShares -= amountOfShares; ``` Withdrawal NFTs belonging to any of the following categories are excluded from the rage quit support update: -- Claimed or unfinalized Withdrawal NFTs +- Claimed or unfinalized withdrawal NFTs - Withdrawal NFTs already marked as finalized - Withdrawal NFTs not locked in the `Escrow` instance @@ -676,35 +690,32 @@ Withdrawal NFTs belonging to any of the following categories are excluded from t function getRageQuitSupport() view returns (uint256) ``` -Calculates and returns the total rage quit support as a percentage of the stETH total supply locked in the instance of the `Escrow` contract. It considers contributions from stETH, wstETH, and non-finalized Withdrawal NFTs while adjusting for the impact of locked finalized Withdrawal NFTs. +Calculates and returns the total rage quit support as a percentage of the stETH total supply locked in the instance of the `Escrow` contract. It considers contributions from stETH, wstETH, and non-finalized withdrawal NFTs while adjusting for the impact of locked finalized withdrawal NFTs. The returned value represents the total rage quit support expressed as a percentage with a precision of 16 decimals. It is computed using the following formula: ```solidity -uint256 rebaseableAmount = stETH.getPooledEthByShares( - _totalStEthSharesLocked + - _totalWstEthSharesLocked + - _totalWithdrawalNFTSharesLocked - - _totalFinalizedWithdrawalNFTSharesLocked -); +uint256 finalizedETH = unstETHTotals.finalizedETH; +uint256 ufinalizedShares = stETHTotals.lockedShares + unstETHTotals.unfinalizedShares; return 10 ** 18 * ( - rebaseableAmount + _totalFinalizedWithdrawalNFTAmountLocked + ST_ETH.getPooledEtherByShares(unfinalizedShares) + finalizedETH ) / ( - stETH.totalSupply() + _totalFinalizedWithdrawalNFTAmountLocked + stETH.totalSupply() + finalizedETH ); ``` ### Function Escrow.startRageQuit ```solidity -function startRageQuit() +function startRageQuit( + Duration rageQuitExtensionDelay, + Duration rageQuitWithdrawalsTimelock +) ``` Transits the `Escrow` instance from the `SignallingEscrow` state to the `RageQuitEscrow` state. Following this transition, locked funds become unwithdrawable and are accessible to users only as plain ETH after the completion of the full `RageQuit` process, including the `RageQuitExtensionDelay` and `RageQuitEthWithdrawalsTimelock` stages. -As the initial step of transitioning to the `RageQuitEscrow` state, all locked wstETH is converted into stETH, and the maximum stETH allowance is granted to the `WithdrawalQueue` contract for the upcoming creation of Withdrawal NFTs. - #### Preconditions - Method MUST be called by the `DualGovernance` contract. @@ -713,32 +724,40 @@ As the initial step of transitioning to the `RageQuitEscrow` state, all locked w ### Function Escrow.requestNextWithdrawalsBatch ```solidity -function requestNextWithdrawalsBatch(uint256 maxWithdrawalRequestsCount) +function requestNextWithdrawalsBatch(uint256 maxBatchSize) ``` -Transfers stETH held in the `RageQuitEscrow` instance into the `WithdrawalQueue`. The function may be invoked multiple times until all stETH is converted into Withdrawal NFTs. For each Withdrawal NFT, the owner is set to `Escrow` contract instance. Each call creates up to `maxWithdrawalRequestsCount` withdrawal requests, where each withdrawal request size equals `WithdrawalQueue.MAX_STETH_WITHDRAWAL_AMOUNT()`, except for potentially the last batch, which may have a smaller size. +Transfers stETH held in the `RageQuitEscrow` instance into the `WithdrawalQueue`. The function may be invoked multiple times until all stETH is converted into withdrawal NFTs. For each withdrawal NFT, the owner is set to `Escrow` contract instance. Each call creates up to `maxBatchSize` withdrawal requests, where each withdrawal request size equals `WithdrawalQueue.MAX_STETH_WITHDRAWAL_AMOUNT()`, except for potentially the last batch, which may have a smaller size. -Upon execution, the function updates the count of withdrawal requests generated by all invocations. When the remaining stETH balance on the contract falls below `WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT()`, the generation of withdrawal batches is concluded, and subsequent function calls will revert. +Upon execution, the function tracks the ids of the withdrawal requests generated by all invocations. When the remaining stETH balance on the contract falls below `WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT()`, the generation of withdrawal batches is concluded, and subsequent function calls will revert. #### Preconditions - The `Escrow` instance MUST be in the `RageQuitEscrow` state. -- The `maxWithdrawalRequestsCount` MUST be greater than 0 -- The generation of WithdrawalRequest batches MUST not be concluded +- The `maxBatchSize` MUST be greater than or equal to `CONFIG.MIN_WITHDRAWALS_BATCH_SIZE()` and less than or equal to `CONFIG.MAX_WITHDRAWALS_BATCH_SIZE()`. +- The generation of withdrawal request batches MUST not be concluded -### Function Escrow.claimNextWithdrawalsBatch +### Function Escrow.claimNextWithdrawalsBatch(uint256, uint256[]) ```solidity -function claimNextWithdrawalsBatch(uint256[] withdrawalRequestIds, uint256[] hints) +function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] hints) ``` -Allows users to claim finalized Withdrawal NFTs generated by the `Escrow.requestNextWithdrawalsBatch()` function. -Tracks the total amount of claimed ETH updating the `_totalClaimedEthAmount` variable. Upon claiming the last batch, the `RageQuitExtensionDelay` period commences. +Allows users to claim finalized withdrawal NFTs generated by the `Escrow.requestNextWithdrawalsBatch()` function. +Tracks the total amount of claimed ETH updating the `stETHTotals.claimedETH` variable. Upon claiming the last batch, the `RageQuitExtensionDelay` period commences. #### Preconditions - The `Escrow` instance MUST be in the `RageQuitEscrow` state. -- The `withdrawalRequestIds` array MUST contain only the ids of finalized but unclaimed withdrawal requests generated by the `Escrow.requestNextWithdrawalsBatch()` function. +- The `fromUnstETHId` MUST be equal to the id of the first unclaimed withdrawal NFT locked in the `Escrow`. The ids of the unclaimed withdrawal NFTs can be retrieved via the `getNextWithdrawalBatch()` method. + +### Function Escrow.claimNextWithdrawalsBatch(uint256) + +```solidity +function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) +``` + +This is an overload version of `Escrow.claimNextWithdrawalsBatch(uint256, uint256[])`. It retrieves hints for processing the withdrawal NFTs on-chain. ### Function Escrow.claimUnstETH @@ -746,9 +765,9 @@ Tracks the total amount of claimed ETH updating the `_totalClaimedEthAmount` var 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. +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 afftected by pending and future DAO decisions. #### Preconditions @@ -766,19 +785,19 @@ Returns whether the rage quit process has been finalized. The rage quit process - All withdrawal request batches have been claimed. - The duration of the `RageQuitExtensionDelay` has elapsed. -### Function Escrow.withdrawStEthAsEth +### Function Escrow.withdrawETH ```solidity -function withdrawStEthAsEth() +function withdrawETH() ``` -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 `RageQuitEthWithdrawalsTimelock` 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 and 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 `RageQuitEthWithdrawalsTimelock` has elapsed. Upon execution, the function transfers ETH to the caller's account and marks the corresponding stETH and wstETH 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: +The amount of ETH sent to the caller is determined by the proportion of the user's stETH and wstETH shares compared to the total amount of locked stETH and wstETH shares in the Escrow instance, calculated as follows: ```solidity -return _totalClaimedEthAmount * _vetoersLockedAssets[msg.sender].stETHShares - / (_totalStEthSharesLocked + _totalWstEthSharesLocked); +return stETHTotals.claimedETH * assets[msg.sender].stETHLockedShares + / stETHTotals.lockedShares; ``` #### Preconditions @@ -786,39 +805,15 @@ return _totalClaimedEthAmount * _vetoersLockedAssets[msg.sender].stETHShares - The `Escrow` instance MUST be in the `RageQuitEscrow` state. - The rage quit process MUST be completed, including the expiration of the `RageQuitExtensionDelay` duration. - The `RageQuitEthWithdrawalsTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. -- The caller MUST have a non-zero amount of stETH to withdraw. -- The caller MUST NOT have previously withdrawn stETH. - -### Function Escrow.withdrawWstEthAsEth - -```solidity -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 `RageQuitEthWithdrawalsTimelock` 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: - -```solidity -return _totalClaimedEthAmount * - _vetoersLockedAssets[msg.sender].wstETHShares / - (_totalStEthSharesLocked + _totalWstEthSharesLocked); -``` - -#### Preconditions -- The `Escrow` instance MUST be in the `RageQuitEscrow` state. -- The rage quit process MUST be completed, including the expiration of the `RageQuitExtensionDelay` duration. -- The `RageQuitEthWithdrawalsTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. -- The caller MUST have a non-zero amount of wstETH to withdraw. -- The caller MUST NOT have previously withdrawn wstETH. +- The caller MUST have a non-zero amount of stETH shares to withdraw. -### Function Escrow.withdrawUnstETHAsEth +### Function Escrow.withdrawETH() ```solidity -function withdrawUnstETHAsEth(uint256[] unstETHIds) +function withdrawETH(uint256[] unstETHIds) ``` -Allows the caller (i.e. `msg.sender`) to withdraw the claimed ETH from the Withdrawal NFTs with ids `unstETHIds` locked by the caller in the `Escrow` contract while the latter was in the `SignallingEscrow` state. Upon execution, all ETH previously claimed from the NFTs is transferred to the caller's account, and the NFTs are marked as withdrawn. +Allows the caller (i.e. `msg.sender`) to withdraw the claimed ETH from the withdrawal NFTs with ids `unstETHIds` locked by the caller in the `Escrow` contract while the latter was in the `SignallingEscrow` state. Upon execution, all ETH previously claimed from the NFTs is transferred to the caller's account, and the NFTs are marked as withdrawn. #### Preconditions @@ -826,7 +821,7 @@ Allows the caller (i.e. `msg.sender`) to withdraw the claimed ETH from the Withd - The rage quit process MUST be completed, including the expiration of the `RageQuitExtensionDelay` duration. - The `RageQuitEthWithdrawalsTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. - The caller MUST be set as the owner of the provided NFTs. -- Each Withdrawal NFT MUST have been claimed using the `Escrow.claimUnstETH()` function. +- Each withdrawal NFT MUST have been claimed using the `Escrow.claimUnstETH()` function. - Withdrawal NFTs must not have been withdrawn previously. diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 4bd9a3c2..77ea81e0 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -349,7 +349,7 @@ contract EscrowHappyPath is TestHelpers { assertEq(_WITHDRAWAL_QUEUE.balanceOf(address(escrow)), 20); - while (!escrow.getIsWithdrawalsBatchesFinalized()) { + while (!escrow.isWithdrawalsBatchesFinalized()) { escrow.requestNextWithdrawalsBatch(96); } @@ -359,7 +359,7 @@ contract EscrowHappyPath is TestHelpers { vm.deal(WITHDRAWAL_QUEUE, 1000 * requestAmount); finalizeWQ(); - uint256[] memory unstETHIdsToClaim = escrow.getNextWithdrawalBatches(expectedWithdrawalBatchesCount); + uint256[] memory unstETHIdsToClaim = escrow.getNextWithdrawalBatch(expectedWithdrawalBatchesCount); // assertEq(total, expectedWithdrawalBatchesCount); WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIdsToClaim); @@ -372,8 +372,8 @@ contract EscrowHappyPath is TestHelpers { uint256[] memory hints = _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIdsToClaim, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); - while (!escrow.getIsWithdrawalsClaimed()) { - escrow.claimWithdrawalsBatch(128); + while (!escrow.isWithdrawalsClaimed()) { + escrow.claimNextWithdrawalsBatch(128); } assertEq(escrow.isRageQuitFinalized(), false); @@ -428,7 +428,7 @@ contract EscrowHappyPath is TestHelpers { escrow.requestNextWithdrawalsBatch(96); - escrow.claimWithdrawalsBatch(0, new uint256[](0)); + escrow.claimNextWithdrawalsBatch(0, new uint256[](0)); assertEq(escrow.isRageQuitFinalized(), false); diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index 288a2dee..e7ae5a69 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -68,15 +68,15 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { uint256 requestAmount = _WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); uint256 maxRequestsCount = vetoedStETHAmount / requestAmount + 1; - while (!rageQuitEscrow.getIsWithdrawalsBatchesFinalized()) { + while (!rageQuitEscrow.isWithdrawalsBatchesFinalized()) { rageQuitEscrow.requestNextWithdrawalsBatch(96); } vm.deal(address(_WITHDRAWAL_QUEUE), 2 * vetoedStETHAmount); _finalizeWQ(); - while (!rageQuitEscrow.getIsWithdrawalsClaimed()) { - rageQuitEscrow.claimWithdrawalsBatch(128); + while (!rageQuitEscrow.isWithdrawalsClaimed()) { + rageQuitEscrow.claimNextWithdrawalsBatch(128); } _wait(_config.RAGE_QUIT_EXTENSION_DELAY().plusSeconds(1)); diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index d0de04a2..d1e53f2c 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -96,11 +96,11 @@ contract ScenarioTestBlueprint is Test { // Helper Getters // --- function _getVetoSignallingEscrow() internal view returns (Escrow) { - return Escrow(payable(_dualGovernance.vetoSignallingEscrow())); + return Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); } function _getRageQuitEscrow() internal view returns (Escrow) { - address rageQuitEscrow = _dualGovernance.rageQuitEscrow(); + address rageQuitEscrow = _dualGovernance.getRageQuitEscrow(); return Escrow(payable(rageQuitEscrow)); } @@ -421,23 +421,23 @@ contract ScenarioTestBlueprint is Test { } function _assertNormalState() internal { - assertEq(uint256(_dualGovernance.currentState()), uint256(State.Normal)); + assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.Normal)); } function _assertVetoSignalingState() internal { - assertEq(uint256(_dualGovernance.currentState()), uint256(State.VetoSignalling)); + assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.VetoSignalling)); } function _assertVetoSignalingDeactivationState() internal { - assertEq(uint256(_dualGovernance.currentState()), uint256(State.VetoSignallingDeactivation)); + assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.VetoSignallingDeactivation)); } function _assertRageQuitState() internal { - assertEq(uint256(_dualGovernance.currentState()), uint256(State.RageQuit)); + assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.RageQuit)); } function _assertVetoCooldownState() internal { - assertEq(uint256(_dualGovernance.currentState()), uint256(State.VetoCooldown)); + assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.VetoCooldown)); } function _assertNoTargetMockCalls() internal { From cca2785b37ab7d1053a1cbefed85624a31f4213b Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 8 Jul 2024 03:13:10 +0400 Subject: [PATCH 127/134] Update the NatSpec comments format in the EmergencyProtectedTimelock --- contracts/EmergencyProtectedTimelock.sol | 188 ++++++++++------------- 1 file changed, 80 insertions(+), 108 deletions(-) diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index b4be7020..e8a84dc3 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -12,13 +12,11 @@ import {EmergencyProtection, EmergencyState} from "./libraries/EmergencyProtecti import {ConfigurationProvider} from "./ConfigurationProvider.sol"; -/** - * @title EmergencyProtectedTimelock - * @dev A timelock contract with emergency protection functionality. - * The contract allows for submitting, scheduling, and executing proposals, - * while providing emergency protection features to prevent unauthorized - * execution during emergency situations. - */ +/// @title EmergencyProtectedTimelock +/// @dev A timelock contract with emergency protection functionality. +/// The contract allows for submitting, scheduling, and executing proposals, +/// while providing emergency protection features to prevent unauthorized +/// execution during emergency situations. contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { using Proposals for Proposals.State; using EmergencyProtection for EmergencyProtection.State; @@ -35,93 +33,83 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { constructor(address config) ConfigurationProvider(config) {} - /** - * @dev Submits a new proposal to execute a series of calls through an executor. - * Only the governance contract can call this function. - * @param executor The address of the executor contract that will execute the calls. - * @param calls An array of `ExecutorCall` structs representing the calls to be executed. - * @return newProposalId The ID of the newly created proposal. - */ + // --- + // Main Timelock Functionality + // --- + + /// @dev Submits a new proposal to execute a series of calls through an executor. + /// Only the governance contract can call this function. + /// @param executor The address of the executor contract that will execute the calls. + /// @param calls An array of `ExecutorCall` structs representing the calls to be executed. + /// @return newProposalId The ID of the newly created proposal. function submit(address executor, ExecutorCall[] calldata calls) external returns (uint256 newProposalId) { _checkGovernance(msg.sender); newProposalId = _proposals.submit(executor, calls); } - /** - * @dev Schedules a proposal for execution after a specified delay. - * Only the governance contract can call this function. - * @param proposalId The ID of the proposal to be scheduled. - */ + /// @dev Schedules a proposal for execution after a specified delay. + /// Only the governance contract can call this function. + /// @param proposalId The ID of the proposal to be scheduled. function schedule(uint256 proposalId) external { _checkGovernance(msg.sender); _proposals.schedule(proposalId, CONFIG.AFTER_SUBMIT_DELAY()); } - /** - * @dev Executes a scheduled proposal. - * Checks if emergency mode is active and prevents execution if it is. - * @param proposalId The ID of the proposal to be executed. - */ + /// @dev Executes a scheduled proposal. + /// Checks if emergency mode is active and prevents execution if it is. + /// @param proposalId The ID of the proposal to be executed. function execute(uint256 proposalId) external { _emergencyProtection.checkEmergencyModeActive(false); _proposals.execute(proposalId, CONFIG.AFTER_SCHEDULE_DELAY()); } - /** - * @dev Cancels all non-executed proposals. - * Only the governance contract can call this function. - */ + /// @dev Cancels all non-executed proposals. + /// Only the governance contract can call this function. function cancelAllNonExecutedProposals() external { _checkGovernance(msg.sender); _proposals.cancelAll(); } - /** - * @dev Transfers ownership of the executor contract to a new owner. - * Only the admin executor can call this function. - * @param executor The address of the executor contract. - * @param owner The address of the new owner. - */ + /// @dev Transfers ownership of the executor contract to a new owner. + /// Only the admin executor can call this function. + /// @param executor The address of the executor contract. + /// @param owner The address of the new owner. function transferExecutorOwnership(address executor, address owner) external { _checkAdminExecutor(msg.sender); IOwnable(executor).transferOwnership(owner); } - /** - * @dev Sets a new governance contract address. - * Only the admin executor can call this function. - * @param newGovernance The address of the new governance contract. - */ + /// @dev Sets a new governance contract address. + /// Only the admin executor can call this function. + /// @param newGovernance The address of the new governance contract. function setGovernance(address newGovernance) external { _checkAdminExecutor(msg.sender); _setGovernance(newGovernance); } - /** - * @dev Activates the emergency mode. - * Only the activation committee can call this function. - */ + // --- + // Emergency Protection Functionality + // --- + + /// @dev Activates the emergency mode. + /// Only the activation committee can call this function. function activateEmergencyMode() external { _emergencyProtection.checkActivationCommittee(msg.sender); _emergencyProtection.checkEmergencyModeActive(false); _emergencyProtection.activate(); } - /** - * @dev Executes a proposal during emergency mode. - * Checks if emergency mode is active and if the caller is part of the execution committee. - * @param proposalId The ID of the proposal to be executed. - */ + /// @dev Executes a proposal during emergency mode. + /// Checks if emergency mode is active and if the caller is part of the execution committee. + /// @param proposalId The ID of the proposal to be executed. function emergencyExecute(uint256 proposalId) external { _emergencyProtection.checkEmergencyModeActive(true); _emergencyProtection.checkExecutionCommittee(msg.sender); _proposals.execute(proposalId, /* afterScheduleDelay */ Duration.wrap(0)); } - /** - * @dev Deactivates the emergency mode. - * If the emergency mode has not passed, only the admin executor can call this function. - */ + /// @dev Deactivates the emergency mode. + /// If the emergency mode has not passed, only the admin executor can call this function. function deactivateEmergencyMode() external { _emergencyProtection.checkEmergencyModeActive(true); if (!_emergencyProtection.isEmergencyModePassed()) { @@ -131,10 +119,8 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { _proposals.cancelAll(); } - /** - * @dev Resets the system after entering the emergency mode. - * Only the execution committee can call this function. - */ + /// @dev Resets the system after entering the emergency mode. + /// Only the execution committee can call this function. function emergencyReset() external { _emergencyProtection.checkEmergencyModeActive(true); _emergencyProtection.checkExecutionCommittee(msg.sender); @@ -143,14 +129,12 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { _proposals.cancelAll(); } - /** - * @dev Sets the parameters for the emergency protection functionality. - * Only the admin executor can call this function. - * @param activator The address of the activation committee. - * @param enactor The address of the execution committee. - * @param protectionDuration The duration of the protection period. - * @param emergencyModeDuration The duration of the emergency mode. - */ + /// @dev Sets the parameters for the emergency protection functionality. + /// Only the admin executor can call this function. + /// @param activator The address of the activation committee. + /// @param enactor The address of the execution committee. + /// @param protectionDuration The duration of the protection period. + /// @param emergencyModeDuration The duration of the emergency mode. function setEmergencyProtection( address activator, address enactor, @@ -161,79 +145,69 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { _emergencyProtection.setup(activator, enactor, protectionDuration, emergencyModeDuration); } - /** - * @dev Checks if the emergency protection functionality is enabled. - * @return A boolean indicating if the emergency protection is enabled. - */ + /// @dev Checks if the emergency protection functionality is enabled. + /// @return A boolean indicating if the emergency protection is enabled. function isEmergencyProtectionEnabled() external view returns (bool) { return _emergencyProtection.isEmergencyProtectionEnabled(); } - /** - * @dev Retrieves the current emergency state. - * @return res The EmergencyState struct containing the current emergency state. - */ + /// @dev Retrieves the current emergency state. + /// @return res The EmergencyState struct containing the current emergency state. function getEmergencyState() external view returns (EmergencyState memory res) { res = _emergencyProtection.getEmergencyState(); } - /** - * @dev Retrieves the address of the current governance contract. - * @return The address of the current governance contract. - */ + // --- + // Timelock View Methods + // --- + + /// @dev Retrieves the address of the current governance contract. + /// @return The address of the current governance contract. function getGovernance() external view returns (address) { return _governance; } - /** - * @dev Retrieves the details of a proposal. - * @param proposalId The ID of the proposal. - * @return proposal The Proposal struct containing the details of the proposal. - */ + /// @dev Retrieves the details of a proposal. + /// @param proposalId The ID of the proposal. + /// @return proposal The Proposal struct containing the details of the proposal. function getProposal(uint256 proposalId) external view returns (Proposal memory proposal) { proposal = _proposals.get(proposalId); } - /** - * @dev Retrieves the total number of proposals. - * @return count The total number of proposals. - */ + /// @dev Retrieves the total number of proposals. + /// @return count The total number of proposals. function getProposalsCount() external view returns (uint256 count) { count = _proposals.count(); } - /** - * @dev Retrieves the submission time of a proposal. - * @param proposalId The ID of the proposal. - * @return submittedAt The submission time of the proposal. - */ + /// @dev Retrieves the submission time of a proposal. + /// @param proposalId The ID of the proposal. + /// @return submittedAt The submission time of the proposal. function getProposalSubmissionTime(uint256 proposalId) external view returns (Timestamp submittedAt) { submittedAt = _proposals.getProposalSubmissionTime(proposalId); } - /** - * @dev Checks if a proposal can be executed. - * @param proposalId The ID of the proposal. - * @return A boolean indicating if the proposal can be executed. - */ + /// @dev Checks if a proposal can be executed. + /// @param proposalId The ID of the proposal. + /// @return A boolean indicating if the proposal can be executed. function canExecute(uint256 proposalId) external view returns (bool) { return !_emergencyProtection.isEmergencyModeActivated() && _proposals.canExecute(proposalId, CONFIG.AFTER_SCHEDULE_DELAY()); } - /** - * @dev Checks if a proposal can be scheduled. - * @param proposalId The ID of the proposal. - * @return A boolean indicating if the proposal can be scheduled. - */ + /// @dev Checks if a proposal can be scheduled. + /// @param proposalId The ID of the proposal. + /// @return A boolean indicating if the proposal can be scheduled. function canSchedule(uint256 proposalId) external view returns (bool) { return _proposals.canSchedule(proposalId, CONFIG.AFTER_SUBMIT_DELAY()); } - /** - * @dev Internal function to set the governance contract address. - * @param newGovernance The address of the new governance contract. - */ + // --- + // Internal Methods + // --- + + /// @dev Internal function to set the governance contract address. + /// @param newGovernance The address of the new governance contract. function _setGovernance(address newGovernance) internal { address prevGovernance = _governance; if (newGovernance == prevGovernance || newGovernance == address(0)) { @@ -243,10 +217,8 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { emit GovernanceSet(newGovernance); } - /** - * @dev Internal function to check if the caller is the governance contract. - * @param account The address to check. - */ + /// @dev Internal function to check if the caller is the governance contract. + /// @param account The address to check. function _checkGovernance(address account) internal view { if (_governance != account) { revert NotGovernance(account, _governance); From 73bb50adef060c656d14098ff76d69b2b1b0a8d8 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Mon, 8 Jul 2024 18:57:35 +0300 Subject: [PATCH 128/134] committees spec --- contracts/committees/ProposalsList.sol | 8 +- contracts/libraries/EnumerableProposals.sol | 4 +- docs/specification.md | 633 +++++++++++++++++--- 3 files changed, 546 insertions(+), 99 deletions(-) diff --git a/contracts/committees/ProposalsList.sol b/contracts/committees/ProposalsList.sol index 625a5841..f14e12a0 100644 --- a/contracts/committees/ProposalsList.sol +++ b/contracts/committees/ProposalsList.sol @@ -9,7 +9,7 @@ contract ProposalsList { EnumerableProposals.Bytes32ToProposalMap internal _proposals; function getProposals(uint256 offset, uint256 limit) public view returns (Proposal[] memory proposals) { - bytes32[] memory keys = _proposals.orederedKeys(offset, limit); + bytes32[] memory keys = _proposals.getOrderedKeys(offset, limit); uint256 length = keys.length; proposals = new Proposal[](length); @@ -27,12 +27,12 @@ contract ProposalsList { return _proposals.get(key); } - function proposalsLength() public view returns (uint256) { + function getProposalsLength() public view returns (uint256) { return _proposals.length(); } - function orederedKeys(uint256 offset, uint256 limit) public view returns (bytes32[] memory) { - return _proposals.orederedKeys(offset, limit); + function getOrderedKeys(uint256 offset, uint256 limit) public view returns (bytes32[] memory) { + return _proposals.getOrderedKeys(offset, limit); } function _pushProposal(bytes32 key, uint256 proposalType, bytes memory data) internal { diff --git a/contracts/libraries/EnumerableProposals.sol b/contracts/libraries/EnumerableProposals.sol index d5954f9e..1e56fee8 100644 --- a/contracts/libraries/EnumerableProposals.sol +++ b/contracts/libraries/EnumerableProposals.sol @@ -57,11 +57,11 @@ library EnumerableProposals { value = map._proposals[key]; } - function orederedKeys(Bytes32ToProposalMap storage map) internal view returns (bytes32[] memory) { + function getOrderedKeys(Bytes32ToProposalMap storage map) internal view returns (bytes32[] memory) { return map._orderedKeys; } - function orederedKeys( + function getOrderedKeys( Bytes32ToProposalMap storage map, uint256 offset, uint256 limit diff --git a/docs/specification.md b/docs/specification.md index c2c88a3d..86cec8f4 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -34,20 +34,19 @@ This document provides the system description on the code architecture level. A ## System overview -![image](https://github.com/lidofinance/dual-governance/assets/1699593/0ca7686c-63bb-489a-bc6a-59d8b9982969) +![image](https://github.com/lidofinance/dual-governance/assets/870356/6beb05c4-3b7a-407a-b840-18e368a1d8c9) The system is composed of the following main contracts: * [`DualGovernance.sol`](#Contract-DualGovernancesol) is a singleton that provides an interface for submitting governance proposals and scheduling their execution, as well as managing the list of supported proposers (DAO voting systems). Implements a state machine tracking the current global governance state which, in turn, determines whether proposal submission and execution is currently allowed. * [`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). -* [`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. * [`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. -* [`ResealExecutor`] +* [`ResealManager.sol`](#Contract-ResealManagersol) contract instances make calls to extend pause or resume sealables 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 @@ -168,8 +167,8 @@ The Tiebreaker committee is represented in the system by its address which can b While the deadlock conditions are met, the Tiebreaker committee address is allowed to: -1. Approve execution of any pending proposal by calling [`DualGovernance.tiebreakerApproveProposal`] so that its execution can be scheduled by calling [`DualGovernance.tiebreakerScheduleProposal`] after the tiebreaker execution timelock passes. -2. Approve the unpause of a pausable ("sealable") protocol contract by calling [`DualGovernance.tiebreakerApproveSealableResume`] so that it can be unpaused by calling [`DualGovernance.tiebreakerScheduleSealableResume`] after the tiebreaker execution timelock passes. +1. Schedule execution of any pending proposal by calling [`DualGovernance.tiebreakerScheduleProposal`] after the tiebreaker execution timelock passes. +2. Unpause of a pausable ("sealable") protocol contract by calling [`DualGovernance.tiebreakerResumeSealable`] after the tiebreaker execution timelock passes. ## Administrative actions @@ -251,46 +250,6 @@ The id of the successfully registered proposal. Triggers a transition of the current governance state (if one is possible) before checking the preconditions. - -### Function: DualGovernance.scheduleProposal - -```solidity -function scheduleProposal(uint256 proposalId) -``` - -Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance to schedule the proposal with id `proposalId` for execution. - -#### Preconditions - -* The proposal with the given id MUST be already submitted using the `DualGovernance.submitProposal` call (the proposal MUST NOT be submitted as the result of the [`DualGovernance.tiebreakerApproveSealableResume`] call). -* The proposal MUST NOT be scheduled. -* The proposal's dynamic timelock MUST have elapsed. -* The proposal MUST NOT be cancelled. -* The current governance state MUST be either `Normal` or `VetoCooldown`. - -Triggers a transition of the current governance state (if one is possible) before checking the preconditions. - -### Function: DualGovernance.tiebreakerApproveProposal - -[`DualGovernance.tiebreakerApproveProposal`]: #Function-DualGovernancetiebreakerApproveProposal - -```solidity -function tiebreakerApproveProposal(uint256 proposalId) -``` - -Marks the proposal with id `proposalId` as approved by the [Tiebreaker committee](#Tiebreaker-committee), given that the DG system is in a deadlock. - -#### Preconditions - -* MUST be called by the [Tiebreaker committee address](#Function-DualGovernancesetTiebreakerCommittee). -* Either the Tiebreaker Condition A or the Tiebreaker Condition B MUST be met (see the [mechanism design document][mech design - tiebreaker]). -* The proposal MUST be already submitted. -* The proposal MUST NOT be cancelled. -* The proposal with the specified id MUST NOT be already approved by the Tiebreaker committee. - -Triggers a transition of the current governance state (if one is possible) before checking the preconditions. - - ### Function: DualGovernance.tiebreakerScheduleProposal [`DualGovernance.tiebreakerScheduleProposal`]: #Function-DualGovernancetiebreakerScheduleProposal @@ -303,54 +262,28 @@ Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimeloc #### Preconditions +* MUST be called by the [Tiebreaker committee address] * Either the Tiebreaker Condition A or the Tiebreaker Condition B MUST be met (see the [mechanism design document][mech design - tiebreaker]). * The proposal with the given id MUST be already submitted using the `DualGovernance.submitProposal` call (the proposal MUST NOT be submitted as the result of the [`DualGovernance.tiebreakerApproveSealableResume`] call). * The proposal MUST NOT be cancelled. -* The proposal with the specified id MUST be approved by the Tiebreaker committee. * The current block timestamp MUST be at least `TIEBREAKER_EXECUTION_TIMELOCK` seconds greater than the timestamp of the block in which the proposal was approved by the Tiebreaker committee. Triggers a transition of the current governance state (if one is possible) before checking the preconditions. +### Function: DualGovernance.tiebreakerResumeSealable -### Function: DualGovernance.tiebreakerApproveSealableResume - -[`DualGovernance.tiebreakerApproveSealableResume`]: #Function-DualGovernancetiebreakerApproveSealableResume - -```solidity -function tiebreakerApproveSealableResume(address sealable) -``` - -Submits a proposal on issuing the `ISealable(sealable).resume()` call from the admin executor contract by calling `EmergencyProtectedTimelock.submit` on the `EmergencyProtectedTimelock` singleton instance. Starts a timelock with the `TIEBREAKER_EXECUTION_TIMELOCK` duration on scheduling this proposal. - -#### Branches - -If the last proposal submitted by calling this function with the same `sealable` parameter is not executed and not cancelled, then does nothing. - -#### Preconditions - -* MUST be called by the [Tiebreaker committee address](#Function-DualGovernancesetTiebreakerCommittee). -* Either the Tiebreaker Condition A or the Tiebreaker Condition B MUST be met (see the [mechanism design document][mech design - tiebreaker]). - -Triggers a transition of the current governance state (if one is possible) before checking the preconditions. - - -### Function: DualGovernance.tiebreakerScheduleSealableResume - -[`DualGovernance.tiebreakerScheduleSealableResume`]: #Function-DualGovernancetiebreakerScheduleSealableResume +[`DualGovernance.tiebreakerResumeSealable`]: #Function-DualGovernancetiebreakerResumeSealable ```solidity -function tiebreakerScheduleSealableResume(address sealable) +function tiebreakerResumeSealable(address sealable) ``` -Schedules the proposal on issuing the `ISealable(sealable).resume()` call that was previously submitted by calling the [`DualGovernance.tiebreakerApproveSealableResume`] function, given that the timelock on its scheduling has elapsed. +Calls the `ResealManager.resumeSealable(address sealable)` if all preconditions met. #### Preconditions +* MUST be called by the [Tiebreaker committee address] * Either the Tiebreaker Condition A or the Tiebreaker Condition B MUST be met (see the [mechanism design document][mech design - tiebreaker]). -* The last proposal submitted by calling the [`DualGovernance.tiebreakerApproveSealableResume`] function with the same `sealable` parameter MUST be pending, i.e. not scheduled, not executed, and not cancelled. -* The timelock on scheduling that proposal MUST be elapsed. - -Triggers a transition of the current governance state (if one is possible) before checking the preconditions. ### Function: DualGovernance.cancelAllPendingProposals @@ -401,7 +334,7 @@ Removes the registered `proposer` address from the list of valid proposers and d ### Function: DualGovernance.setTiebreakerCommittee ```solidity -function setTiebreakerCommittee(address newTiebreaker) +function setTiebreakerCommittee(address newTiebreaker, address resealManager) ``` Updates the address of the [Tiebreaker committee](#Tiebreaker-committee). @@ -445,7 +378,7 @@ The result of the call. * MUST be called by the contract owner (which SHOULD be the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance). -## Contract: ResealExecutor.sol +## Contract: ResealManager.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): @@ -453,38 +386,39 @@ 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 `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. +To address this compatibility challenge between gate seals and dual governance, the `ResealManager` contract is introduced. The `ResealManager` allows to extend pause of temporarily paused contracts to permanent pause or resume it, if conditions are met: +- `ResealManager` 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`. - -It inherits `OwnableExecutor` and provides ability to extend contracts pause for committee set by DAO. +- The DAO governance is blocked by `DualGovernance` -### Function ResealExecutor.reseal +### Function ResealManager.reseal ```solidity function reseal(address[] memory sealables) ``` -This function extends pause of `sealables`. Can be called by committee address. +This function extends pause of `sealables`. Can be called by governance address defined in Emergency Protected Timelock. #### Preconditions -- `ResealExecutor` has `PAUSE_ROLE` and `RESUME_ROLE` for target contracts. +- `ResealManager` 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`. +- Called by governance address defined in `EmergencyProtectedTimelock` -### Function ResealExecutor.setResealCommittee +### Function ResealManager.resume ```solidity -function setResealCommittee(address newResealCommittee) +function resume(address sealable) ``` -This function set `resealCommittee` address to `newResealCommittee`. Can be called by owner. +This function provides ability of unpause of `sealable`. Can be called by governance address defined in Emergency Protected Timelock. #### Preconditions -- Can be called by `OWNER`. +- `ResealManager` has `RESUME_ROLE` for target contracts. +- Target contracts are paused. +- Called by governance address defined in `EmergencyProtectedTimelock` + ## Contract: Escrow.sol @@ -1019,11 +953,524 @@ 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: ProposalsList.sol + +`ProposalsList` implements storage for list of `Proposal`s with public interface to access. + +### Function: ProposalsList.getProposals + +```solidity +function getProposals(uint256 offset, uint256 limit) public view returns (Proposal[] memory proposals) +``` + +Returns the limited list of `Proposal`s with offset. + +### Function: ProposalsList.getProposalAt + +```solidity +function getProposalAt(uint256 index) public view returns (Proposal memory) +``` + +Returns `Proposal` at position `index` + +### Function: ProposalsList.getProposal + +```solidity +function getProposal(bytes32 key) public view returns (Proposal memory) +``` + +Returns `Proposal` by it's `key` + +### Function: ProposalsList.getProposalsLength + +```solidity +function getProposalsLength() public view returns (uint256) +``` + +Returns total number of created `Proposal`s. + +### Function: ProposalsList.getOrderedKeys + +```solidity +function getOrderedKeys(uint256 offset, uint256 limit) public view returns (bytes32[] memory) +``` + +Returns total list of `Proposal`s keys with offset and limit. + +## Contract: HashConsensus.sol + +`HashConsensus` is an abstract contract that allows for consensus-based decision-making among a set of members. The consensus is achieved by members voting on a specific hash, and decisions can only be executed if a quorum is reached and a timelock period has elapsed. + +### Function: HashConsensus.addMember + +```solidity +function addMember(address newMember, uint256 newQuorum) public onlyOwner +``` + +Adds a new member and updates the quorum. + +#### Preconditions + +* Only the owner can call this function. +* `newQuorum` MUST be greater than 0 and less than or equal to the number of members. + +### Function: HashConsensus.removeMember + +```solidity +function removeMember(address memberToRemove, uint256 newQuorum) public onlyOwner +``` + +Removes a member and updates the quorum. + +#### Preconditions + +* Only the owner can call this function. +* Member MUST be part of the set. +* `newQuorum` MUST be greater than 0 and less than or equal to the number of remaining members. + +### Function: HashConsensus.getMembers + +```solidity +function getMembers() public view returns (address[] memory) +``` + +Returns the list of current members. + +### Function: HashConsensus.isMember + +```solidity +function isMember(address member) public view returns (bool) +``` + +Checks if an address is a member. + +### Function: HashConsensus.setTimelockDuration + +```solidity +function setTimelockDuration(uint256 timelock) public onlyOwner +``` + +Sets the timelock duration. + +#### Preconditions + +* Only the owner can call this function. + +### Function: HashConsensus.setQuorum + +```solidity +function setQuorum(uint256 newQuorum) public onlyOwner +``` + +Sets the quorum required for decision execution. + +#### Preconditions + +* Only the owner can call this function. +* `newQuorum` MUST be greater than 0 and less than or equal to the number of members. + + +### Admin functions + +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: TiebreakerCore.sol + +`TiebreakerCore` is a smart contract that extends the `HashConsensus` and `ProposalsList` contracts to manage the scheduling of proposals and the resuming of sealable contracts through a consensus-based mechanism. It interacts with a DualGovernance contract to execute decisions once consensus is reached. + +Constructor + +```solidity +constructor( + address owner, + address[] memory committeeMembers, + uint256 executionQuorum, + address dualGovernance, + uint256 timelock +) +``` + +Initializes the contract with an owner, committee members, a quorum, the address of the DualGovernance contract, and a timelock duration. + +#### Preconditions + +* `executionQuorum` MUST be greater than 0. +* `dualGovernance` MUST be a valid address. + + +### Function: TiebreakerCore.scheduleProposal + +```solidity +function scheduleProposal(uint256 proposalId) public onlyMember +``` + +Schedules a proposal for execution by voting on it and adding it to the proposal list. + +#### Preconditions + +* MUST be called by a member. + +### Function: TiebreakerCore.getScheduleProposalState + +```solidity +function getScheduleProposalState(uint256 proposalId) + public + view + returns (uint256 support, uint256 executionQuorum, bool isExecuted) +``` + +Returns the state of a scheduled proposal including support count, quorum, and execution status. + +### Function: TiebreakerCore.executeScheduleProposal + +```solidity +function executeScheduleProposal(uint256 proposalId) public +``` + +Executes a scheduled proposal by calling the tiebreakerScheduleProposal function on the DualGovernance contract. + +#### Preconditions + +* Proposal MUST have reached quorum and passed the timelock duration. + +### Function: TiebreakerCore.getSealableResumeNonce + +```solidity +function getSealableResumeNonce(address sealable) public view returns (uint256) +``` + +Returns the current nonce for resuming operations of a sealable contract. + +### Function: TiebreakerCore.sealableResume + +```solidity +function sealableResume(address sealable, uint256 nonce) public onlyMember +``` + +Submits a request to resume operations of a sealable contract by voting on it and adding it to the proposal list. + +#### Preconditions + +* MUST be called by a member. +* The provided nonce MUST match the current nonce of the sealable contract. + +### Function: TiebreakerCore.getSealableResumeState + +```solidity +function getSealableResumeState(address sealable, uint256 nonce) + public + view + returns (uint256 support, uint256 executionQuorum, bool isExecuted) +``` + +Returns the state of a sealable resume request including support count, quorum, and execution status. + +### Function: TiebreakerCore.executeSealableResume + +```solidity +function executeSealableResume(address sealable) external +``` + +Executes a sealable resume request by calling the tiebreakerResumeSealable function on the DualGovernance contract and increments the nonce. + +#### Preconditions + +* Resume request MUST have reached quorum and passed the timelock duration. + ## Contract: TiebreakerSubCommittee.sol + +`TiebreakerSubCommittee` is a smart contract that extends the functionalities of `HashConsensus` and `ProposalsList` to manage the scheduling of proposals and the resumption of sealable contracts through a consensus mechanism. It interacts with the `TiebreakerCore` contract to execute decisions once consensus is reached. + +```solidity +constructor( + address owner, + address[] memory committeeMembers, + uint256 executionQuorum, + address tiebreakerCore +) +``` + +Initializes the contract with an owner, committee members, a quorum, and the address of the TiebreakerCore contract. + +#### Preconditions +* `executionQuorum` MUST be greater than 0. +* `tiebreakerCore` MUST be a valid address. + +### Function: TiebreakerSubCommittee.scheduleProposal + +```solidity +function scheduleProposal(uint256 proposalId) public onlyMember +``` + +Schedules a proposal for execution by voting on it and adding it to the proposal list. + +#### Preconditions +* MUST be called by a member. + +### Function: TiebreakerSubCommittee.getScheduleProposalState + +```solidity +function getScheduleProposalState(uint256 proposalId) + public + view + returns (uint256 support, uint256 executionQuorum, bool isExecuted) +``` + +Returns the state of a scheduled proposal including support count, quorum, and execution status. + +### Function: TiebreakerSubCommittee.executeScheduleProposal + +```solidity +function executeScheduleProposal(uint256 proposalId) public +``` + +Executes a scheduled proposal by calling the scheduleProposal function on the TiebreakerCore contract. + +#### Preconditions + +* Proposal MUST have reached quorum and passed the timelock duration. + +### Function: TiebreakerSubCommittee.sealableResume + +```solidity +function sealableResume(address sealable) public +``` + +Submits a request to resume operations of a sealable contract by voting on it and adding it to the proposal list. + +#### Preconditions + +* MUST be called by a member. +* getSealableResumeState + +```solidity +function getSealableResumeState(address sealable) + public + view + returns (uint256 support, uint256 executionQuorum, bool isExecuted) +``` + +Returns the state of a sealable resume request including support count, quorum, and execution status. + +### Function: TiebreakerSubCommittee.executeSealableResume + +```solidity +function executeSealableResume(address sealable) public +``` + +Executes a sealable resume request by calling the sealableResume function on the TiebreakerCore contract and increments the nonce. + +#### Preconditions + +* Resume request MUST have reached quorum and passed the timelock duration. + ## Contract: EmergencyActivationCommittee.sol + +`EmergencyActivationCommittee` is a smart contract that extends the functionalities of ё to manage the emergency activation process. It allows committee members to vote on and execute the activation of emergency protocols in the ё contract. + +```solidity +constructor( + address owner, + address[] memory committeeMembers, + uint256 executionQuorum, + address emergencyProtectedTimelock +) +``` + +Initializes the contract with an owner, committee members, a quorum, and the address of the EmergencyProtectedTimelock contract. + +#### Preconditions +executionQuorum MUST be greater than 0. +emergencyProtectedTimelock MUST be a valid address. + + +### Function: EmergencyActivationCommittee.approveEmergencyActivate + +```solidity +function approveEmergencyActivate() public onlyMember +``` + +Approves the emergency activation by voting on the EMERGENCY_ACTIVATION_HASH. + +#### Preconditions + +* MUST be called by a member. + +### Function: EmergencyActivationCommittee.getEmergencyActivateState + +```solidity +function getEmergencyActivateState() + public + view + returns (uint256 support, uint256 executionQuorum, bool isExecuted) +``` + +Returns the state of the emergency activation proposal including support count, quorum, and execution status. + +### Function: EmergencyActivationCommittee.executeEmergencyActivate + +```solidity +function executeEmergencyActivate() external +``` + +Executes the emergency activation by calling the emergencyActivate function on the EmergencyProtectedTimelock contract. + +#### Preconditions + +* Emergency activation proposal MUST have reached quorum and passed the timelock duration. + + ## Contract: EmergencyExecutionCommittee.sol -## Contract: ResealCommittee.sol + +`EmergencyExecutionCommittee` is a smart contract that extends the functionalities of `HashConsensus` and `ProposalsList` to manage emergency execution and governance reset proposals through a consensus mechanism. It interacts with the `EmergencyProtectedTimelock` contract to execute critical emergency proposals. + +```solidity +constructor( + address owner, + address[] memory committeeMembers, + uint256 executionQuorum, + address emergencyProtectedTimelock +) +``` + +Initializes the contract with an owner, committee members, a quorum, and the address of the EmergencyProtectedTimelock contract. + +#### Preconditions + +* executionQuorum MUST be greater than 0. +* emergencyProtectedTimelock MUST be a valid address. + +### Function: EmergencyExecutionCommittee.voteEmergencyExecute + +```solidity +function voteEmergencyExecute(uint256 proposalId, bool _supports) public onlyMember +``` + +Allows committee members to vote on an emergency execution proposal. + +#### Preconditions + +* MUST be called by a member. + +### Function: EmergencyExecutionCommittee.getEmergencyExecuteState + +```solidity +function getEmergencyExecuteState(uint256 proposalId) + public + view + returns (uint256 support, uint256 executionQuorum, bool isExecuted) +``` + +Returns the state of an emergency execution proposal including support count, quorum, and execution status. + +### Function: EmergencyExecutionCommittee. executeEmergencyExecute + +```solidity +function executeEmergencyExecute(uint256 proposalId) public +``` + +Executes an emergency execution proposal by calling the emergencyExecute function on the EmergencyProtectedTimelock contract. + +#### Preconditions +Emergency execution proposal MUST have reached quorum and passed the timelock duration. + + +### Function: EmergencyExecutionCommittee.approveEmergencyReset + +```solidity +function approveEmergencyReset() public onlyMember +``` + +Approves the governance reset by voting on the reset proposal. + +#### Preconditions + +* MUST be called by a member. + +### Function: EmergencyExecutionCommittee.getEmergencyResetState + +```solidity +function getEmergencyResetState() + public + view + returns (uint256 support, uint256 executionQuorum, bool isExecuted) +``` + +Returns the state of the governance reset proposal including support count, quorum, and execution status. + +### Function: EmergencyExecutionCommittee.executeEmergencyReset + +```solidity +function executeEmergencyReset() external +``` + +Executes the governance reset by calling the emergencyReset function on the EmergencyProtectedTimelock contract. + +#### Preconditions + +* Governance reset proposal MUST have reached quorum and passed the timelock duration. + + +## Contract: ResealManager.sol + +`ResealManager` is a smart contract designed to manage the resealing and resuming of sealable contracts in emergency situations. It queries `EmergencyProtectedTimelock` to ensure only actual governance can trigger these actions. + +```solidity +constructor(address emergencyProtectedTimelock) +``` + +Initializes the contract with the address of the EmergencyProtectedTimelock contract. + +#### Preconditions + +* emergencyProtectedTimelock MUST be a valid address. + +### Function: ResealManager.reseal + +```solidity +function reseal(address[] memory sealables) public onlyGovernance +``` + +Pauses the specified sealable contracts indefinitely. + +#### Preconditions + +* MUST be called by the governance address. +* Each sealable contract MUST NOT be paused infinitely already and MUST be scheduled to resume in the future. + +#### Errors +`SealableWrongPauseState`: Thrown if the sealable contract is in the wrong pause state. +`SenderIsNotGovernance`: Thrown if the sender is not the governance address. + +### Function: ResealManager.resume + +```solidity +function resume(address sealable) public onlyGovernance +``` + +Resumes the specified sealable contract if it is scheduled to resume in the future. + +#### Preconditions + +* MUST be called by the governance address. + +#### Errors +`SealableWrongPauseState`: Thrown if the sealable contract is in the wrong pause state. +`SenderIsNotGovernance`: Thrown if the sender is not the governance address. + +### Modifier: ResealManager.onlyGovernance + +```solidity +modifier onlyGovernance() +``` + +Ensures that the function can only be called by the governance address. + +#### Preconditions + +* The sender MUST be the governance address obtained from the EmergencyProtectedTimelock contract. + ## Upgrade flow description From 2c28ed109ec6e132ab885d68b635169e24160dc6 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 9 Jul 2024 05:30:39 +0400 Subject: [PATCH 129/134] Update diagram in the specification --- docs/specification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specification.md b/docs/specification.md index db1c5f03..a8271137 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -34,7 +34,7 @@ This document provides the system description on the code architecture level. A ## System overview -![image](https://github.com/lidofinance/dual-governance/assets/870356/6beb05c4-3b7a-407a-b840-18e368a1d8c9) +![image](https://github.com/lidofinance/dual-governance/assets/14151334/b7498050-e04c-415e-9f45-3ed9c24f1417) The system is composed of the following main contracts: From adc11b6f2557d06e1b7c9d1fe851f233c4f5de14 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 10 Jul 2024 11:57:51 +0300 Subject: [PATCH 130/134] solidity version bump --- contracts/Configuration.sol | 2 +- contracts/ConfigurationProvider.sol | 2 +- contracts/DualGovernance.sol | 2 +- contracts/EmergencyProtectedTimelock.sol | 2 +- contracts/Escrow.sol | 2 +- contracts/Executor.sol | 2 +- contracts/ResealManager.sol | 2 +- contracts/SingleGovernance.sol | 2 +- contracts/committees/EmergencyActivationCommittee.sol | 2 +- contracts/committees/EmergencyExecutionCommittee.sol | 2 +- contracts/committees/HashConsensus.sol | 2 +- contracts/committees/ProposalsList.sol | 2 +- contracts/committees/ResealCommittee.sol | 2 +- contracts/committees/TiebreakerCore.sol | 2 +- contracts/committees/TiebreakerSubCommittee.sol | 2 +- contracts/interfaces/IConfiguration.sol | 2 +- contracts/interfaces/IEscrow.sol | 2 +- contracts/interfaces/IExecutor.sol | 2 +- contracts/interfaces/IGateSeal.sol | 2 +- contracts/interfaces/IOwnable.sol | 2 +- contracts/interfaces/IResealManager.sol | 2 +- contracts/interfaces/ISealable.sol | 2 +- contracts/interfaces/IStETH.sol | 2 +- contracts/interfaces/ITimelock.sol | 2 +- contracts/interfaces/IWithdrawalQueue.sol | 2 +- contracts/interfaces/IWstETH.sol | 2 +- contracts/libraries/AssetsAccounting.sol | 2 +- contracts/libraries/DualGovernanceState.sol | 2 +- contracts/libraries/EmergencyProtection.sol | 2 +- contracts/libraries/EnumerableProposals.sol | 2 +- contracts/libraries/Proposals.sol | 2 +- contracts/libraries/Proposers.sol | 2 +- contracts/libraries/SealableCalls.sol | 2 +- contracts/libraries/TiebreakerProtection.sol | 2 +- contracts/libraries/WithdrawalBatchesQueue.sol | 2 +- contracts/types/Duration.sol | 2 +- contracts/types/ETHValue.sol | 2 +- contracts/types/IndexOneBased.sol | 2 +- contracts/types/SequentialBatches.sol | 2 +- contracts/types/SharesValue.sol | 2 +- contracts/types/Timestamp.sol | 2 +- contracts/utils/arrays.sol | 2 +- foundry.toml | 2 +- test/kontrol/VetoSignalling.t.sol | 2 +- test/mocks/GateSealMock.sol | 2 +- test/scenario/agent-timelock.t.sol | 2 +- test/scenario/escrow.t.sol | 2 +- test/scenario/gov-state-transitions.t.sol | 2 +- test/scenario/happy-path-plan-b.t.sol | 2 +- test/scenario/happy-path.t.sol | 2 +- test/scenario/last-moment-malicious-proposal.t.sol | 2 +- test/scenario/tiebraker.t.sol | 2 +- test/scenario/veto-cooldown-mechanics.t.sol | 2 +- test/unit/EmergencyProtectedTimelock.t.sol | 2 +- test/unit/SingleGovernance.t.sol | 2 +- test/unit/libraries/EmergencyProtection.t.sol | 2 +- test/unit/libraries/Proposals.t.sol | 2 +- test/unit/mocks/TimelockMock.sol | 2 +- test/utils/executor-calls.sol | 2 +- test/utils/interfaces.sol | 2 +- test/utils/mainnet-addresses.sol | 2 +- test/utils/percents.sol | 2 +- test/utils/scenario-test-blueprint.sol | 2 +- test/utils/unit-test.sol | 2 +- test/utils/utils.sol | 2 +- 65 files changed, 65 insertions(+), 65 deletions(-) diff --git a/contracts/Configuration.sol b/contracts/Configuration.sol index 17f3b6dc..d4b4b57e 100644 --- a/contracts/Configuration.sol +++ b/contracts/Configuration.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Durations, Duration} from "./types/Duration.sol"; import {IConfiguration, DualGovernanceConfig} from "./interfaces/IConfiguration.sol"; diff --git a/contracts/ConfigurationProvider.sol b/contracts/ConfigurationProvider.sol index aae4369a..a8d9af22 100644 --- a/contracts/ConfigurationProvider.sol +++ b/contracts/ConfigurationProvider.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {IConfiguration} from "./interfaces/IConfiguration.sol"; diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index dfc8a733..5d8936dd 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Duration} from "./types/Duration.sol"; import {Timestamp} from "./types/Timestamp.sol"; diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index e8a84dc3..b01d761f 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Duration} from "./types/Duration.sol"; import {Timestamp} from "./types/Timestamp.sol"; diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index a0d274b1..a92de85b 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; diff --git a/contracts/Executor.sol b/contracts/Executor.sol index 106c0a6b..d58cb2b0 100644 --- a/contracts/Executor.sol +++ b/contracts/Executor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; diff --git a/contracts/ResealManager.sol b/contracts/ResealManager.sol index 5fdb2137..1c5593ff 100644 --- a/contracts/ResealManager.sol +++ b/contracts/ResealManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {ISealable} from "./interfaces/ISealable.sol"; diff --git a/contracts/SingleGovernance.sol b/contracts/SingleGovernance.sol index c1034658..476b251c 100644 --- a/contracts/SingleGovernance.sol +++ b/contracts/SingleGovernance.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {IGovernance, ITimelock} from "./interfaces/ITimelock.sol"; diff --git a/contracts/committees/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol index 038dd46b..d9710ce5 100644 --- a/contracts/committees/EmergencyActivationCommittee.sol +++ b/contracts/committees/EmergencyActivationCommittee.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {HashConsensus} from "./HashConsensus.sol"; diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol index 1c670227..25a8b868 100644 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {HashConsensus} from "./HashConsensus.sol"; diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index fb4a2d65..0318b2ca 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; diff --git a/contracts/committees/ProposalsList.sol b/contracts/committees/ProposalsList.sol index f14e12a0..e4934ba0 100644 --- a/contracts/committees/ProposalsList.sol +++ b/contracts/committees/ProposalsList.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {EnumerableProposals, Proposal} from "../libraries/EnumerableProposals.sol"; diff --git a/contracts/committees/ResealCommittee.sol b/contracts/committees/ResealCommittee.sol index 1b40a5d6..8d495eec 100644 --- a/contracts/committees/ResealCommittee.sol +++ b/contracts/committees/ResealCommittee.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {HashConsensus} from "./HashConsensus.sol"; diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol index a380036a..e340d759 100644 --- a/contracts/committees/TiebreakerCore.sol +++ b/contracts/committees/TiebreakerCore.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {HashConsensus} from "./HashConsensus.sol"; diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol index 1ceb40fc..abd5b356 100644 --- a/contracts/committees/TiebreakerSubCommittee.sol +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {HashConsensus} from "./HashConsensus.sol"; diff --git a/contracts/interfaces/IConfiguration.sol b/contracts/interfaces/IConfiguration.sol index 1b572e87..1ccee4ef 100644 --- a/contracts/interfaces/IConfiguration.sol +++ b/contracts/interfaces/IConfiguration.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Duration} from "../types/Duration.sol"; diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol index 3586d311..e4474b73 100644 --- a/contracts/interfaces/IEscrow.sol +++ b/contracts/interfaces/IEscrow.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Duration} from "../types/Duration.sol"; diff --git a/contracts/interfaces/IExecutor.sol b/contracts/interfaces/IExecutor.sol index 511dcd26..f54f433c 100644 --- a/contracts/interfaces/IExecutor.sol +++ b/contracts/interfaces/IExecutor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; struct ExecutorCall { address target; diff --git a/contracts/interfaces/IGateSeal.sol b/contracts/interfaces/IGateSeal.sol index 9dd6d07e..44b0b2a0 100644 --- a/contracts/interfaces/IGateSeal.sol +++ b/contracts/interfaces/IGateSeal.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; interface IGateSeal { function seal(address[] calldata sealables) external; diff --git a/contracts/interfaces/IOwnable.sol b/contracts/interfaces/IOwnable.sol index c0224f1b..8ef5a4fa 100644 --- a/contracts/interfaces/IOwnable.sol +++ b/contracts/interfaces/IOwnable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; interface IOwnable { function transferOwnership(address newOwner) external; diff --git a/contracts/interfaces/IResealManager.sol b/contracts/interfaces/IResealManager.sol index ca1f2ee4..bea725ef 100644 --- a/contracts/interfaces/IResealManager.sol +++ b/contracts/interfaces/IResealManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; interface IResealManager { function resume(address sealable) external; diff --git a/contracts/interfaces/ISealable.sol b/contracts/interfaces/ISealable.sol index df924ec5..4d616d80 100644 --- a/contracts/interfaces/ISealable.sol +++ b/contracts/interfaces/ISealable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; interface ISealable { function resume() external; diff --git a/contracts/interfaces/IStETH.sol b/contracts/interfaces/IStETH.sol index 764dc145..fc1eecc4 100644 --- a/contracts/interfaces/IStETH.sol +++ b/contracts/interfaces/IStETH.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index 1b2118ae..e7926a11 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Timestamp} from "../types/Timestamp.sol"; import {ExecutorCall} from "./IExecutor.sol"; diff --git a/contracts/interfaces/IWithdrawalQueue.sol b/contracts/interfaces/IWithdrawalQueue.sol index 0d2ac472..dab8edd0 100644 --- a/contracts/interfaces/IWithdrawalQueue.sol +++ b/contracts/interfaces/IWithdrawalQueue.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; struct WithdrawalRequestStatus { uint256 amountOfStETH; diff --git a/contracts/interfaces/IWstETH.sol b/contracts/interfaces/IWstETH.sol index 2b5622e0..6b6d6e17 100644 --- a/contracts/interfaces/IWstETH.sol +++ b/contracts/interfaces/IWstETH.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index 2de7288d..f8c2d0c9 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {ETHValue, ETHValues} from "../types/ETHValue.sol"; import {SharesValue, SharesValues} from "../types/SharesValue.sol"; diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol index 5b030899..664697ef 100644 --- a/contracts/libraries/DualGovernanceState.sol +++ b/contracts/libraries/DualGovernanceState.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; diff --git a/contracts/libraries/EmergencyProtection.sol b/contracts/libraries/EmergencyProtection.sol index f52bb4f3..e911977a 100644 --- a/contracts/libraries/EmergencyProtection.sol +++ b/contracts/libraries/EmergencyProtection.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Duration, Durations} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; diff --git a/contracts/libraries/EnumerableProposals.sol b/contracts/libraries/EnumerableProposals.sol index 1e56fee8..31610365 100644 --- a/contracts/libraries/EnumerableProposals.sol +++ b/contracts/libraries/EnumerableProposals.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; diff --git a/contracts/libraries/Proposals.sol b/contracts/libraries/Proposals.sol index 6d1ae5d1..cf0b0b06 100644 --- a/contracts/libraries/Proposals.sol +++ b/contracts/libraries/Proposals.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Duration} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; diff --git a/contracts/libraries/Proposers.sol b/contracts/libraries/Proposers.sol index 77012f20..746dac07 100644 --- a/contracts/libraries/Proposers.sol +++ b/contracts/libraries/Proposers.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; diff --git a/contracts/libraries/SealableCalls.sol b/contracts/libraries/SealableCalls.sol index 72b4e331..e472faad 100644 --- a/contracts/libraries/SealableCalls.sol +++ b/contracts/libraries/SealableCalls.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {ISealable} from "../interfaces/ISealable.sol"; diff --git a/contracts/libraries/TiebreakerProtection.sol b/contracts/libraries/TiebreakerProtection.sol index fb30649a..ffdb5665 100644 --- a/contracts/libraries/TiebreakerProtection.sol +++ b/contracts/libraries/TiebreakerProtection.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; interface IResealManger { function resume(address sealable) external; diff --git a/contracts/libraries/WithdrawalBatchesQueue.sol b/contracts/libraries/WithdrawalBatchesQueue.sol index 6c096be8..9f882b01 100644 --- a/contracts/libraries/WithdrawalBatchesQueue.sol +++ b/contracts/libraries/WithdrawalBatchesQueue.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; diff --git a/contracts/types/Duration.sol b/contracts/types/Duration.sol index 0a599061..f061273b 100644 --- a/contracts/types/Duration.sol +++ b/contracts/types/Duration.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Timestamp, Timestamps} from "./Timestamp.sol"; diff --git a/contracts/types/ETHValue.sol b/contracts/types/ETHValue.sol index 3fb11785..44160dfd 100644 --- a/contracts/types/ETHValue.sol +++ b/contracts/types/ETHValue.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; diff --git a/contracts/types/IndexOneBased.sol b/contracts/types/IndexOneBased.sol index 201ce080..1bde6542 100644 --- a/contracts/types/IndexOneBased.sol +++ b/contracts/types/IndexOneBased.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; type IndexOneBased is uint32; diff --git a/contracts/types/SequentialBatches.sol b/contracts/types/SequentialBatches.sol index 6d944497..35edef11 100644 --- a/contracts/types/SequentialBatches.sol +++ b/contracts/types/SequentialBatches.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; uint256 constant BATCH_SIZE_LENGTH = 16; uint256 constant BATCH_SIZE_MASK = 2 ** BATCH_SIZE_LENGTH - 1; diff --git a/contracts/types/SharesValue.sol b/contracts/types/SharesValue.sol index f5048477..1ff60455 100644 --- a/contracts/types/SharesValue.sol +++ b/contracts/types/SharesValue.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {ETHValue, ETHValues} from "./ETHValue.sol"; diff --git a/contracts/types/Timestamp.sol b/contracts/types/Timestamp.sol index ab06379f..87d280a2 100644 --- a/contracts/types/Timestamp.sol +++ b/contracts/types/Timestamp.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; type Timestamp is uint40; diff --git a/contracts/utils/arrays.sol b/contracts/utils/arrays.sol index 960189bb..c6daf147 100644 --- a/contracts/utils/arrays.sol +++ b/contracts/utils/arrays.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; library ArrayUtils { function sum(uint256[] calldata values) internal pure returns (uint256 res) { diff --git a/foundry.toml b/foundry.toml index 196e3fca..f609c46e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,7 @@ out = 'out' libs = ['node_modules', 'lib'] test = 'test' cache_path = 'cache_forge' -solc-version = "0.8.23" +solc-version = "0.8.26" no-match-path = 'test/kontrol/*' [profile.kprove] diff --git a/test/kontrol/VetoSignalling.t.sol b/test/kontrol/VetoSignalling.t.sol index 0db58bd3..bb0094c0 100644 --- a/test/kontrol/VetoSignalling.t.sol +++ b/test/kontrol/VetoSignalling.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.23; +pragma solidity 0.8.26; import "forge-std/Vm.sol"; import "forge-std/Test.sol"; diff --git a/test/mocks/GateSealMock.sol b/test/mocks/GateSealMock.sol index 632c2269..e926a572 100644 --- a/test/mocks/GateSealMock.sol +++ b/test/mocks/GateSealMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {ISealable} from "contracts/interfaces/ISealable.sol"; import {IGateSeal} from "contracts/interfaces/IGateSeal.sol"; diff --git a/test/scenario/agent-timelock.t.sol b/test/scenario/agent-timelock.t.sol index 75e365c8..b49cf865 100644 --- a/test/scenario/agent-timelock.t.sol +++ b/test/scenario/agent-timelock.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {ExecutorCall, ScenarioTestBlueprint, ExecutorCall} from "../utils/scenario-test-blueprint.sol"; diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 77ea81e0..e5b1414d 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {WithdrawalRequestStatus} from "../utils/interfaces.sol"; import {Duration as DurationType} from "contracts/types/Duration.sol"; diff --git a/test/scenario/gov-state-transitions.t.sol b/test/scenario/gov-state-transitions.t.sol index efb12696..acaaa1cd 100644 --- a/test/scenario/gov-state-transitions.t.sol +++ b/test/scenario/gov-state-transitions.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {ScenarioTestBlueprint, percents, Durations} from "../utils/scenario-test-blueprint.sol"; diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index ac52bc20..ed3afd4b 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import { EmergencyState, diff --git a/test/scenario/happy-path.t.sol b/test/scenario/happy-path.t.sol index fd55104b..4bc1b336 100644 --- a/test/scenario/happy-path.t.sol +++ b/test/scenario/happy-path.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import { Utils, diff --git a/test/scenario/last-moment-malicious-proposal.t.sol b/test/scenario/last-moment-malicious-proposal.t.sol index 99bf4e47..bf90b73f 100644 --- a/test/scenario/last-moment-malicious-proposal.t.sol +++ b/test/scenario/last-moment-malicious-proposal.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import { percents, diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol index 4af73b90..4cae4b0e 100644 --- a/test/scenario/tiebraker.t.sol +++ b/test/scenario/tiebraker.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import { ScenarioTestBlueprint, percents, ExecutorCall, ExecutorCallHelpers diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index e7ae5a69..b44837e0 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import { Escrow, diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 094c2781..3aa063ab 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Vm} from "forge-std/Test.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; diff --git a/test/unit/SingleGovernance.t.sol b/test/unit/SingleGovernance.t.sol index 50619594..f211165f 100644 --- a/test/unit/SingleGovernance.t.sol +++ b/test/unit/SingleGovernance.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Vm} from "forge-std/Test.sol"; diff --git a/test/unit/libraries/EmergencyProtection.t.sol b/test/unit/libraries/EmergencyProtection.t.sol index d3681e08..d588a9a7 100644 --- a/test/unit/libraries/EmergencyProtection.t.sol +++ b/test/unit/libraries/EmergencyProtection.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Test, Vm} from "forge-std/Test.sol"; diff --git a/test/unit/libraries/Proposals.t.sol b/test/unit/libraries/Proposals.t.sol index 0b70209a..ff8a544f 100644 --- a/test/unit/libraries/Proposals.t.sol +++ b/test/unit/libraries/Proposals.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Vm} from "forge-std/Test.sol"; diff --git a/test/unit/mocks/TimelockMock.sol b/test/unit/mocks/TimelockMock.sol index bea86f22..a513ecef 100644 --- a/test/unit/mocks/TimelockMock.sol +++ b/test/unit/mocks/TimelockMock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Timestamp} from "contracts/types/Timestamp.sol"; import {ITimelock} from "contracts/interfaces/ITimelock.sol"; diff --git a/test/utils/executor-calls.sol b/test/utils/executor-calls.sol index e6a87a93..3233d4ba 100644 --- a/test/utils/executor-calls.sol +++ b/test/utils/executor-calls.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {ExecutorCall} from "contracts/interfaces/IExecutor.sol"; diff --git a/test/utils/interfaces.sol b/test/utils/interfaces.sol index 5a2e90f3..56660cf4 100644 --- a/test/utils/interfaces.sol +++ b/test/utils/interfaces.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; import {IERC721} from "@openzeppelin/contracts/interfaces/IERC721.sol"; diff --git a/test/utils/mainnet-addresses.sol b/test/utils/mainnet-addresses.sol index 3de321ee..b5db4b7f 100644 --- a/test/utils/mainnet-addresses.sol +++ b/test/utils/mainnet-addresses.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.23; +pragma solidity 0.8.26; address constant DAO_ACL = 0x9895F0F17cc1d1891b6f18ee0b483B6f221b37Bb; address constant DAO_AGENT = 0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c; diff --git a/test/utils/percents.sol b/test/utils/percents.sol index 9862be56..43eff5cd 100644 --- a/test/utils/percents.sol +++ b/test/utils/percents.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; error InvalidPercentsString(string value); diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 6354155d..62199d75 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Test} from "forge-std/Test.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; diff --git a/test/utils/unit-test.sol b/test/utils/unit-test.sol index 3ec9b59c..8e4ff80e 100644 --- a/test/utils/unit-test.sol +++ b/test/utils/unit-test.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; diff --git a/test/utils/utils.sol b/test/utils/utils.sol index 15224876..a259cfd1 100644 --- a/test/utils/utils.sol +++ b/test/utils/utils.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.23; +pragma solidity 0.8.26; // solhint-disable-next-line import "forge-std/console2.sol"; From 1fe57dba1547832710e47753199f70b732a980eb Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 10 Jul 2024 12:59:45 +0300 Subject: [PATCH 131/134] add oz add git module --- .gitmodules | 3 +++ lib/openzeppelin-contracts | 1 + package.json | 3 --- remappings.txt | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) create mode 160000 lib/openzeppelin-contracts create mode 100644 remappings.txt diff --git a/.gitmodules b/.gitmodules index 413cbca7..5f8d4d17 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/kontrol-cheatcodes"] path = lib/kontrol-cheatcodes url = https://github.com/runtimeverification/kontrol-cheatcodes +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 00000000..dbb6104c --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 diff --git a/package.json b/package.json index 1a70b964..13453cac 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,5 @@ "solhint": "^4.1.1", "solhint-plugin-lido": "^0.0.4", "solidity-coverage": "^0.8.4" - }, - "dependencies": { - "@openzeppelin/contracts": "5.0.1" } } diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 00000000..add52da9 --- /dev/null +++ b/remappings.txt @@ -0,0 +1 @@ +@openzeppelin/=lib/openzeppelin-contracts/ \ No newline at end of file From 330ea4815ccf2c1018c4fb233ecdba70e1233499 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 15 Jul 2024 12:57:19 +0400 Subject: [PATCH 132/134] Update Navigation. Fix typos --- docs/specification.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/specification.md b/docs/specification.md index a8271137..43ed5608 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -26,12 +26,20 @@ This document provides the system description on the code architecture level. A * [Common types](#common-types) * [Contract: DualGovernance.sol](#contract-dualgovernancesol) * [Contract: Executor.sol](#contract-executorsol) +* [Contract: ResealManager.sol](#contract-resealmanagersol) * [Contract: Escrow.sol](#contract-escrowsol) * [Contract: EmergencyProtectedTimelock.sol](#contract-emergencyprotectedtimelocksol) * [Contract: Configuration.sol](#contract-configurationsol) +* [Contract: ProposalsList.sol](#contract-proposalslistsol) +* [Contract: HashConsensus.sol](#contract-hashconsensussol) +* [Contract: TiebreakerCore.sol](#contract-tiebreakercoresol) +* [Contract: TiebreakerSubCommittee.sol](#contract-tiebreakersubcommitteesol) +* [Contract: EmergencyActivationCommittee.sol](#contract-emergencyactivationcommitteesol) +* [Contract: EmergencyExecutionCommittee.sol](#contract-emergencyexecutioncommitteesol) * [Upgrade flow description](#upgrade-flow-description) + ## System overview ![image](https://github.com/lidofinance/dual-governance/assets/14151334/b7498050-e04c-415e-9f45-3ed9c24f1417) @@ -1262,7 +1270,7 @@ Executes a sealable resume request by calling the sealableResume function on the ## Contract: EmergencyActivationCommittee.sol -`EmergencyActivationCommittee` is a smart contract that extends the functionalities of ё to manage the emergency activation process. It allows committee members to vote on and execute the activation of emergency protocols in the ё contract. +`EmergencyActivationCommittee` is a smart contract that extends the functionalities of `HashConsensus` to manage the emergency activation process. It allows committee members to vote on and execute the activation of emergency protocols in the `HashConsensus` contract. ```solidity constructor( From 83199290f2cd7c97df4b1cd1c181eb3ebe3229e8 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 16 Jul 2024 14:45:42 +0400 Subject: [PATCH 133/134] Remove repeated ResealManager section --- docs/specification.md | 131 +++++++++++++++++------------------------- 1 file changed, 53 insertions(+), 78 deletions(-) diff --git a/docs/specification.md b/docs/specification.md index 43ed5608..bfe2af9c 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -394,38 +394,68 @@ 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 `ResealManager` contract is introduced. The `ResealManager` allows to extend pause of temporarily paused contracts to permanent pause or resume it, if conditions are met: -- `ResealManager` 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` +To address the compatibility challenge between gate seals and dual governance, the `ResealManager` contract is introduced. This smart contract is designed to manage the resealing and resuming of sealable contracts during emergencies. The `ResealManager` can extend the pause of temporarily paused contracts to a permanent pause or resume them if the following conditions are met: -### Function ResealManager.reseal +- The `ResealManager` holds the `PAUSE_ROLE` and `RESUME_ROLE` for the target contracts. +- Contracts are paused until a specific timestamp that is in the future and not indefinitely. +- DAO governance is blocked by `DualGovernance`. + +### Constructor + +```solidity +constructor(address emergencyProtectedTimelock) +``` + +Initializes the contract with the address of the EmergencyProtectedTimelock contract. + +#### Preconditions + +* emergencyProtectedTimelock MUST be a valid address. + +### Function: ResealManager.reseal + +```solidity +function reseal(address[] memory sealables) external onlyGovernance +``` + +Extends the pause of the specified `sealables` contracts. This function can be called by the governance address defined in the `EmergencyProtectedTimelock`. + +#### Preconditions + +- The `ResealManager` must have `PAUSE_ROLE` and `RESUME_ROLE` for the target contracts. +- The target contracts must be paused until a future timestamp and not indefinitely. +- The function must be called by the governance address defined in `EmergencyProtectedTimelock`. + +### Function: ResealManager.resume ```solidity -function reseal(address[] memory sealables) +function resume(address sealable) external onlyGovernance ``` -This function extends pause of `sealables`. Can be called by governance address defined in Emergency Protected Timelock. +Resumes the specified `sealable` contract if it is scheduled to resume in the future. #### Preconditions -- `ResealManager` has `PAUSE_ROLE` and `RESUME_ROLE` for target contracts. -- Contracts are paused until timestamp after current timestamp and not for infinite time. -- Called by governance address defined in `EmergencyProtectedTimelock` +- The `ResealManager` must have the `RESUME_ROLE` for the target contract. +- The target contract must be paused. +- The function must be called by the governance address defined in `EmergencyProtectedTimelock`. -### Function ResealManager.resume +### Modifier: ResealManager.onlyGovernance ```solidity -function resume(address sealable) +modifier onlyGovernance() ``` -This function provides ability of unpause of `sealable`. Can be called by governance address defined in Emergency Protected Timelock. +Ensures that the function can only be called by the governance address. #### Preconditions -- `ResealManager` has `RESUME_ROLE` for target contracts. -- Target contracts are paused. -- Called by governance address defined in `EmergencyProtectedTimelock` +- The sender must be the governance address obtained from the `EmergencyProtectedTimelock` contract. + +### Errors + +- `SealableWrongPauseState`: Thrown if the sealable contract is in the wrong pause state. +- `SenderIsNotGovernance`: Thrown if the sender is not the governance address. ## Contract: Escrow.sol @@ -1082,7 +1112,7 @@ The contract has the interface for managing the configuration related to emergen `TiebreakerCore` is a smart contract that extends the `HashConsensus` and `ProposalsList` contracts to manage the scheduling of proposals and the resuming of sealable contracts through a consensus-based mechanism. It interacts with a DualGovernance contract to execute decisions once consensus is reached. -Constructor +### Constructor ```solidity constructor( @@ -1185,6 +1215,8 @@ Executes a sealable resume request by calling the tiebreakerResumeSealable funct `TiebreakerSubCommittee` is a smart contract that extends the functionalities of `HashConsensus` and `ProposalsList` to manage the scheduling of proposals and the resumption of sealable contracts through a consensus mechanism. It interacts with the `TiebreakerCore` contract to execute decisions once consensus is reached. +### Constructor + ```solidity constructor( address owner, @@ -1272,6 +1304,8 @@ Executes a sealable resume request by calling the sealableResume function on the `EmergencyActivationCommittee` is a smart contract that extends the functionalities of `HashConsensus` to manage the emergency activation process. It allows committee members to vote on and execute the activation of emergency protocols in the `HashConsensus` contract. +### Constructor + ```solidity constructor( address owner, @@ -1328,6 +1362,8 @@ Executes the emergency activation by calling the emergencyActivate function on t `EmergencyExecutionCommittee` is a smart contract that extends the functionalities of `HashConsensus` and `ProposalsList` to manage emergency execution and governance reset proposals through a consensus mechanism. It interacts with the `EmergencyProtectedTimelock` contract to execute critical emergency proposals. +### Constructor + ```solidity constructor( address owner, @@ -1414,67 +1450,6 @@ Executes the governance reset by calling the emergencyReset function on the Emer * Governance reset proposal MUST have reached quorum and passed the timelock duration. - -## Contract: ResealManager.sol - -`ResealManager` is a smart contract designed to manage the resealing and resuming of sealable contracts in emergency situations. It queries `EmergencyProtectedTimelock` to ensure only actual governance can trigger these actions. - -```solidity -constructor(address emergencyProtectedTimelock) -``` - -Initializes the contract with the address of the EmergencyProtectedTimelock contract. - -#### Preconditions - -* emergencyProtectedTimelock MUST be a valid address. - -### Function: ResealManager.reseal - -```solidity -function reseal(address[] memory sealables) public onlyGovernance -``` - -Pauses the specified sealable contracts indefinitely. - -#### Preconditions - -* MUST be called by the governance address. -* Each sealable contract MUST NOT be paused infinitely already and MUST be scheduled to resume in the future. - -#### Errors -`SealableWrongPauseState`: Thrown if the sealable contract is in the wrong pause state. -`SenderIsNotGovernance`: Thrown if the sender is not the governance address. - -### Function: ResealManager.resume - -```solidity -function resume(address sealable) public onlyGovernance -``` - -Resumes the specified sealable contract if it is scheduled to resume in the future. - -#### Preconditions - -* MUST be called by the governance address. - -#### Errors -`SealableWrongPauseState`: Thrown if the sealable contract is in the wrong pause state. -`SenderIsNotGovernance`: Thrown if the sender is not the governance address. - -### Modifier: ResealManager.onlyGovernance - -```solidity -modifier onlyGovernance() -``` - -Ensures that the function can only be called by the governance address. - -#### Preconditions - -* The sender MUST be the governance address obtained from the EmergencyProtectedTimelock contract. - - ## Upgrade flow description In designing the dual governance system, ensuring seamless updates while maintaining the contracts' immutability was a primary consideration. To achieve this, the system was divided into three key components: `DualGovernance`, `EmergencyProtectedTimelock`, and `Executor`. From 372b3cb9a4785a3585a7812148fa60b463ed297b Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 16 Jul 2024 14:51:47 +0400 Subject: [PATCH 134/134] Fix ResealManager section formatting --- docs/specification.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/specification.md b/docs/specification.md index bfe2af9c..7d01471f 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -406,11 +406,11 @@ To address the compatibility challenge between gate seals and dual governance, t constructor(address emergencyProtectedTimelock) ``` -Initializes the contract with the address of the EmergencyProtectedTimelock contract. +Initializes the contract with the address of the `EmergencyProtectedTimelock` contract. #### Preconditions -* emergencyProtectedTimelock MUST be a valid address. +* `emergencyProtectedTimelock` MUST be a valid address. ### Function: ResealManager.reseal @@ -422,9 +422,9 @@ Extends the pause of the specified `sealables` contracts. This function can be c #### Preconditions -- The `ResealManager` must have `PAUSE_ROLE` and `RESUME_ROLE` for the target contracts. -- The target contracts must be paused until a future timestamp and not indefinitely. -- The function must be called by the governance address defined in `EmergencyProtectedTimelock`. +- The `ResealManager` MUST have `PAUSE_ROLE` and `RESUME_ROLE` for the target contracts. +- The target contracts MUST be paused until a future timestamp and not indefinitely. +- The function MUST be called by the governance address defined in `EmergencyProtectedTimelock`. ### Function: ResealManager.resume @@ -436,9 +436,9 @@ Resumes the specified `sealable` contract if it is scheduled to resume in the fu #### Preconditions -- The `ResealManager` must have the `RESUME_ROLE` for the target contract. -- The target contract must be paused. -- The function must be called by the governance address defined in `EmergencyProtectedTimelock`. +- The `ResealManager` MUST have the `RESUME_ROLE` for the target contract. +- The target contract MUST be paused. +- The function MUST be called by the governance address defined in `EmergencyProtectedTimelock`. ### Modifier: ResealManager.onlyGovernance @@ -450,7 +450,7 @@ Ensures that the function can only be called by the governance address. #### Preconditions -- The sender must be the governance address obtained from the `EmergencyProtectedTimelock` contract. +- The sender MUST be the governance address obtained from the `EmergencyProtectedTimelock` contract. ### Errors