From e8a7a9f6ef5e561dbdaacf64fc2897329d4d3e18 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 14 Aug 2024 02:11:51 +0400 Subject: [PATCH 01/86] Prevent executed proposals from changing state after cancelAll is called --- contracts/libraries/ExecutableProposals.sol | 2 +- test/unit/libraries/ExecutableProposals.t.sol | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/contracts/libraries/ExecutableProposals.sol b/contracts/libraries/ExecutableProposals.sol index bd65908e..5b868ca9 100644 --- a/contracts/libraries/ExecutableProposals.sol +++ b/contracts/libraries/ExecutableProposals.sol @@ -217,6 +217,6 @@ library ExecutableProposals { uint256 proposalId, ProposalData memory proposalData ) private view returns (bool) { - return proposalId <= self.lastCancelledProposalId || proposalData.status == Status.Cancelled; + return proposalId <= self.lastCancelledProposalId && proposalData.status != Status.Executed; } } diff --git a/test/unit/libraries/ExecutableProposals.t.sol b/test/unit/libraries/ExecutableProposals.t.sol index eb4be119..6f205777 100644 --- a/test/unit/libraries/ExecutableProposals.t.sol +++ b/test/unit/libraries/ExecutableProposals.t.sol @@ -401,6 +401,54 @@ contract ExecutableProposalsUnitTests is UnitTest { assert(!_proposals.canExecute(proposalId, Durations.ZERO)); } + function test_cancelAll_DoesNotModifyStateOfExecutedProposals() external { + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + assertEq(_proposals.getProposalsCount(), 1); + uint256 executedProposalId = 1; + _proposals.schedule(executedProposalId, Durations.ZERO); + _proposals.execute(executedProposalId, Durations.ZERO); + + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + assertEq(_proposals.getProposalsCount(), 2); + uint256 scheduledProposalId = 2; + _proposals.schedule(scheduledProposalId, Durations.ZERO); + + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + assertEq(_proposals.getProposalsCount(), 3); + uint256 submittedProposalId = 3; + + // Validate the state of the proposals is correct before proceeding with cancellation. + + (ProposalStatus executedProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = + _proposals.getProposalInfo(executedProposalId); + assertEq(executedProposalStatus, ProposalStatus.Executed); + + (ProposalStatus scheduledProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = + _proposals.getProposalInfo(scheduledProposalId); + assertEq(scheduledProposalStatus, ProposalStatus.Scheduled); + + (ProposalStatus submittedProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = + _proposals.getProposalInfo(submittedProposalId); + assertEq(submittedProposalStatus, ProposalStatus.Submitted); + + // After canceling the proposals, both submitted and scheduled proposals should transition to the Cancelled state. + // However, executed proposals should remain in the Executed state. + + _proposals.cancelAll(); + + (executedProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = + _proposals.getProposalInfo(executedProposalId); + assertEq(executedProposalStatus, ProposalStatus.Executed); + + (scheduledProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = + _proposals.getProposalInfo(scheduledProposalId); + assertEq(scheduledProposalStatus, ProposalStatus.Cancelled); + + (submittedProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = + _proposals.getProposalInfo(submittedProposalId); + assertEq(submittedProposalStatus, ProposalStatus.Cancelled); + } + function test_can_schedule_proposal() external { Duration delay = Durations.from(100 seconds); _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); From 92e9d49ba4f757f464e08ff6576e6c592bc92c03 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Wed, 14 Aug 2024 13:33:26 +0300 Subject: [PATCH 02/86] feat: tiebreaker improvements and unit tests --- contracts/libraries/Tiebreaker.sol | 69 ++++++++-- test/unit/libraries/Tiebreaker.t.sol | 193 +++++++++++++++++++++++++++ 2 files changed, 249 insertions(+), 13 deletions(-) create mode 100644 test/unit/libraries/Tiebreaker.t.sol diff --git a/contracts/libraries/Tiebreaker.sol b/contracts/libraries/Tiebreaker.sol index 137a089c..37a547cc 100644 --- a/contracts/libraries/Tiebreaker.sol +++ b/contracts/libraries/Tiebreaker.sol @@ -2,23 +2,27 @@ pragma solidity 0.8.26; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; - import {Duration} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Duration.sol"; - import {ISealable} from "../interfaces/ISealable.sol"; - import {SealableCalls} from "./SealableCalls.sol"; import {State as DualGovernanceState} from "./DualGovernanceStateMachine.sol"; +/// @title Tiebreaker Library +/// @dev 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. Tiebreaker gains +/// the power to execute pending proposals, bypassing the DG dynamic timelock, and unpause any +/// protocol contract under the specific conditions of the deadlock. library Tiebreaker { using SealableCalls for ISealable; using EnumerableSet for EnumerableSet.AddressSet; - error TiebreakDisallowed(); - error InvalidSealable(address value); - error InvalidTiebreakerCommittee(address value); - error InvalidTiebreakerActivationTimeout(Duration value); + error TiebreakNotAllowed(); + error InvalidSealable(address sealable); + error InvalidTiebreakerCommittee(address account); + error InvalidTiebreakerActivationTimeout(Duration timeout); + error CallerIsNotTiebreakerCommittee(address caller); error SealableWithdrawalBlockersLimitReached(); event SealableWithdrawalBlockerAdded(address sealable); @@ -26,6 +30,10 @@ library Tiebreaker { event TiebreakerCommitteeSet(address newTiebreakerCommittee); event TiebreakerActivationTimeoutSet(Duration newTiebreakerActivationTimeout); + /// @dev Context struct to store tiebreaker-related data. + /// @param tiebreakerCommittee Address of the tiebreaker committee. + /// @param tiebreakerActivationTimeout Duration for tiebreaker activation timeout. + /// @param sealableWithdrawalBlockers Set of addresses that are sealable withdrawal blockers. struct Context { /// @dev slot0 [0..159] address tiebreakerCommittee; @@ -39,6 +47,11 @@ library Tiebreaker { // Setup functionality // --- + /// @notice Adds a sealable withdrawal blocker. + /// @dev Reverts if the maximum number of sealable withdrawal blockers is reached or if the sealable is invalid. + /// @param self The context storage. + /// @param sealableWithdrawalBlocker The address of the sealable withdrawal blocker to add. + /// @param maxSealableWithdrawalBlockersCount The maximum number of sealable withdrawal blockers allowed. function addSealableWithdrawalBlocker( Context storage self, address sealableWithdrawalBlocker, @@ -48,7 +61,6 @@ library Tiebreaker { if (sealableWithdrawalBlockersCount == maxSealableWithdrawalBlockersCount) { revert SealableWithdrawalBlockersLimitReached(); } - (bool isCallSucceed, /* lowLevelError */, /* isPaused */ ) = ISealable(sealableWithdrawalBlocker).callIsPaused(); if (!isCallSucceed) { revert InvalidSealable(sealableWithdrawalBlocker); @@ -60,6 +72,9 @@ library Tiebreaker { } } + /// @notice Removes a sealable withdrawal blocker. + /// @param self The context storage. + /// @param sealableWithdrawalBlocker The address of the sealable withdrawal blocker to remove. function removeSealableWithdrawalBlocker(Context storage self, address sealableWithdrawalBlocker) internal { bool isSuccessfullyRemoved = self.sealableWithdrawalBlockers.remove(sealableWithdrawalBlocker); if (isSuccessfullyRemoved) { @@ -67,6 +82,10 @@ library Tiebreaker { } } + /// @notice Sets the tiebreaker committee. + /// @dev Reverts if the new tiebreaker committee address is invalid. + /// @param self The context storage. + /// @param newTiebreakerCommittee The address of the new tiebreaker committee. function setTiebreakerCommittee(Context storage self, address newTiebreakerCommittee) internal { if (newTiebreakerCommittee == address(0)) { revert InvalidTiebreakerCommittee(newTiebreakerCommittee); @@ -78,6 +97,12 @@ library Tiebreaker { emit TiebreakerCommitteeSet(newTiebreakerCommittee); } + /// @notice Sets the tiebreaker activation timeout. + /// @dev Reverts if the new timeout is outside the allowed range. + /// @param self The context storage. + /// @param minTiebreakerActivationTimeout The minimum allowed tiebreaker activation timeout. + /// @param newTiebreakerActivationTimeout The new tiebreaker activation timeout. + /// @param maxTiebreakerActivationTimeout The maximum allowed tiebreaker activation timeout. function setTiebreakerActivationTimeout( Context storage self, Duration minTiebreakerActivationTimeout, @@ -102,19 +127,27 @@ library Tiebreaker { // Checks // --- + /// @notice Checks if the caller is the tiebreaker committee. + /// @dev Reverts if the caller is not the tiebreaker committee. + /// @param self The context storage. function checkCallerIsTiebreakerCommittee(Context storage self) internal view { if (msg.sender != self.tiebreakerCommittee) { - revert InvalidTiebreakerCommittee(msg.sender); + revert CallerIsNotTiebreakerCommittee(msg.sender); } } + /// @notice Checks if a tie exists. + /// @dev Reverts if no tie exists. + /// @param self The context storage. + /// @param state The current state of dual governance. + /// @param normalOrVetoCooldownExitedAt The timestamp when normal or veto cooldown exited. function checkTie( Context storage self, DualGovernanceState state, Timestamp normalOrVetoCooldownExitedAt ) internal view { if (!isTie(self, state, normalOrVetoCooldownExitedAt)) { - revert TiebreakDisallowed(); + revert TiebreakNotAllowed(); } } @@ -122,6 +155,11 @@ library Tiebreaker { // Getters // --- + /// @notice Determines if a tie exists. + /// @param self The context storage. + /// @param state The current state of dual governance. + /// @param normalOrVetoCooldownExitedAt The timestamp when normal or veto cooldown exited. + /// @return True if a tie exists, false otherwise. function isTie( Context storage self, DualGovernanceState state, @@ -129,7 +167,6 @@ library Tiebreaker { ) internal view returns (bool) { if (state == DualGovernanceState.Normal || state == DualGovernanceState.VetoCooldown) return false; - // when the governance is locked for long period of time if (Timestamps.now() >= self.tiebreakerActivationTimeout.addTo(normalOrVetoCooldownExitedAt)) { return true; } @@ -137,19 +174,25 @@ library Tiebreaker { return state == DualGovernanceState.RageQuit && isSomeSealableWithdrawalBlockerPaused(self); } + /// @notice Checks if any sealable withdrawal blocker is paused. + /// @param self The context storage. + /// @return True if any sealable withdrawal blocker is paused, false otherwise. function isSomeSealableWithdrawalBlockerPaused(Context storage self) internal view returns (bool) { uint256 sealableWithdrawalBlockersCount = self.sealableWithdrawalBlockers.length(); for (uint256 i = 0; i < sealableWithdrawalBlockersCount; ++i) { (bool isCallSucceed, /* lowLevelError */, bool isPaused) = ISealable(self.sealableWithdrawalBlockers.at(i)).callIsPaused(); - // in normal condition this call must never fail, so if some sealable withdrawal blocker - // started behave unexpectedly tiebreaker action may be the last hope for the protocol saving if (isPaused || !isCallSucceed) return true; } return false; } + /// @notice Gets the tiebreaker information. + /// @param self The context storage. + /// @return tiebreakerCommittee The address of the tiebreaker committee. + /// @return tiebreakerActivationTimeout The duration of the tiebreaker activation timeout. + /// @return sealableWithdrawalBlockers The addresses of the sealable withdrawal blockers. function getTiebreakerInfo(Context storage self) internal view diff --git a/test/unit/libraries/Tiebreaker.t.sol b/test/unit/libraries/Tiebreaker.t.sol new file mode 100644 index 00000000..e213d3df --- /dev/null +++ b/test/unit/libraries/Tiebreaker.t.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {State as DualGovernanceState} from "contracts/libraries/DualGovernanceStateMachine.sol"; +import {Tiebreaker} from "contracts/libraries/Tiebreaker.sol"; +import {Duration, Durations, Timestamp, Timestamps} from "contracts/types/Duration.sol"; +import {ISealable} from "contracts/interfaces/ISealable.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; +import {SealableMock} from "../../mocks/SealableMock.sol"; + +contract TiebreakerTest is UnitTest { + using EnumerableSet for EnumerableSet.AddressSet; + + Tiebreaker.Context private context; + SealableMock private mockSealable1; + SealableMock private mockSealable2; + + function setUp() external { + mockSealable1 = new SealableMock(); + mockSealable2 = new SealableMock(); + } + + function test_addSealableWithdrawalBlocker() external { + vm.expectEmit(); + emit Tiebreaker.SealableWithdrawalBlockerAdded(address(mockSealable1)); + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); + + assertTrue(context.sealableWithdrawalBlockers.contains(address(mockSealable1))); + } + + function test_AddSealableWithdrawalBlocker_RevertOn_LimitReached() external { + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); + + vm.expectRevert(Tiebreaker.SealableWithdrawalBlockersLimitReached.selector); + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable2), 1); + } + + function test_AddSealableWithdrawalBlocker_RevertOn_InvalidSealable() external { + mockSealable1.setShouldRevertIsPaused(true); + + vm.expectRevert(abi.encodeWithSelector(Tiebreaker.InvalidSealable.selector, address(mockSealable1))); + // external call should be used to intercept the revert + this.external__addSealableWithdrawalBlocker(address(mockSealable1)); + + vm.expectRevert(); + // external call should be used to intercept the revert + this.external__addSealableWithdrawalBlocker(address(0x123)); + } + + function test_RemoveSealableWithdrawalBlocker() external { + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); + assertTrue(context.sealableWithdrawalBlockers.contains(address(mockSealable1))); + + vm.expectEmit(); + emit Tiebreaker.SealableWithdrawalBlockerRemoved(address(mockSealable1)); + + Tiebreaker.removeSealableWithdrawalBlocker(context, address(mockSealable1)); + assertFalse(context.sealableWithdrawalBlockers.contains(address(mockSealable1))); + } + + function test_SetTiebreakerCommittee() external { + address newCommittee = address(0x123); + + vm.expectEmit(); + emit Tiebreaker.TiebreakerCommitteeSet(newCommittee); + Tiebreaker.setTiebreakerCommittee(context, newCommittee); + + assertEq(context.tiebreakerCommittee, newCommittee); + } + + function test_SetTiebreakerCommittee_WithExistingCommitteeAddress() external { + address newCommittee = address(0x123); + + Tiebreaker.setTiebreakerCommittee(context, newCommittee); + Tiebreaker.setTiebreakerCommittee(context, newCommittee); + } + + function test_SetTiebreakerCommittee_RevertOn_ZeroAddress() external { + vm.expectRevert(abi.encodeWithSelector(Tiebreaker.InvalidTiebreakerCommittee.selector, address(0))); + Tiebreaker.setTiebreakerCommittee(context, address(0)); + } + + function testFuzz_SetTiebreakerActivationTimeout(uint32 minTimeout, uint32 maxTimeout, uint32 timeout) external { + vm.assume(minTimeout < timeout && timeout < maxTimeout); + + Duration min = Duration.wrap(minTimeout); + Duration max = Duration.wrap(maxTimeout); + Duration newTimeout = Duration.wrap(timeout); + + vm.expectEmit(); + emit Tiebreaker.TiebreakerActivationTimeoutSet(newTimeout); + + Tiebreaker.setTiebreakerActivationTimeout(context, min, newTimeout, max); + assertEq(context.tiebreakerActivationTimeout, newTimeout); + } + + function test_SetTiebreakerActivationTimeout_RevertOn_InvalidTimeout() external { + Duration minTimeout = Duration.wrap(1 days); + Duration maxTimeout = Duration.wrap(10 days); + Duration newTimeout = Duration.wrap(15 days); + + vm.expectRevert(abi.encodeWithSelector(Tiebreaker.InvalidTiebreakerActivationTimeout.selector, newTimeout)); + Tiebreaker.setTiebreakerActivationTimeout(context, minTimeout, newTimeout, maxTimeout); + + newTimeout = Duration.wrap(0 days); + + vm.expectRevert(abi.encodeWithSelector(Tiebreaker.InvalidTiebreakerActivationTimeout.selector, newTimeout)); + Tiebreaker.setTiebreakerActivationTimeout(context, minTimeout, newTimeout, maxTimeout); + } + + function test_IsSomeSealableWithdrawalBlockerPaused() external { + mockSealable1.pauseFor(1 days); + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 2); + + bool result = Tiebreaker.isSomeSealableWithdrawalBlockerPaused(context); + assertTrue(result); + + mockSealable1.resume(); + + result = Tiebreaker.isSomeSealableWithdrawalBlockerPaused(context); + assertFalse(result); + + mockSealable1.setShouldRevertIsPaused(true); + + result = Tiebreaker.isSomeSealableWithdrawalBlockerPaused(context); + assertTrue(result); + } + + function test_CheckTie() external { + Timestamp cooldownExitedAt = Timestamps.from(block.timestamp); + + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); + Tiebreaker.setTiebreakerActivationTimeout( + context, Duration.wrap(1 days), Duration.wrap(3 days), Duration.wrap(10 days) + ); + + mockSealable1.pauseFor(1 days); + Tiebreaker.checkTie(context, DualGovernanceState.RageQuit, cooldownExitedAt); + + _wait(Duration.wrap(3 days)); + Tiebreaker.checkTie(context, DualGovernanceState.VetoSignalling, cooldownExitedAt); + } + + function test_CheckTie_RevertOn_NormalOrVetoCooldownState() external { + Timestamp cooldownExitedAt = Timestamps.from(block.timestamp); + + vm.expectRevert(Tiebreaker.TiebreakNotAllowed.selector); + Tiebreaker.checkTie(context, DualGovernanceState.Normal, cooldownExitedAt); + + vm.expectRevert(Tiebreaker.TiebreakNotAllowed.selector); + Tiebreaker.checkTie(context, DualGovernanceState.VetoCooldown, cooldownExitedAt); + } + + function test_CheckCallerIsTiebreakerCommittee() external { + context.tiebreakerCommittee = address(this); + + vm.expectRevert(abi.encodeWithSelector(Tiebreaker.CallerIsNotTiebreakerCommittee.selector, address(0x456))); + vm.prank(address(0x456)); + this.external__checkCallerIsTiebreakerCommittee(); + + this.external__checkCallerIsTiebreakerCommittee(); + } + + function test_GetTimebreakerInfo() external { + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); + + Duration minTimeout = Duration.wrap(1 days); + Duration maxTimeout = Duration.wrap(10 days); + Duration timeout = Duration.wrap(5 days); + + context.tiebreakerActivationTimeout = timeout; + context.tiebreakerCommittee = address(0x123); + + (address committee, Duration activationTimeout, address[] memory blockers) = + Tiebreaker.getTiebreakerInfo(context); + + assertEq(committee, context.tiebreakerCommittee); + assertEq(activationTimeout, context.tiebreakerActivationTimeout); + assertEq(blockers[0], address(mockSealable1)); + assertEq(blockers.length, 1); + } + + function external__checkCallerIsTiebreakerCommittee() external { + Tiebreaker.checkCallerIsTiebreakerCommittee(context); + } + + function external__addSealableWithdrawalBlocker(address sealable) external { + Tiebreaker.addSealableWithdrawalBlocker(context, sealable, 1); + } +} From 04a34edcf44fed3bae595fab86761e3d6e9dba9d Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 19 Aug 2024 04:26:07 +0400 Subject: [PATCH 03/86] Align cancelAllPendingProposals method with specification --- contracts/DualGovernance.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 5fb93df8..26287f2c 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -42,6 +42,8 @@ contract DualGovernance is IDualGovernance { // Events // --- + event CancelAllPendingProposalsSkipped(); + event CancelAllPendingProposalsExecuted(); event EscrowMasterCopyDeployed(address escrowMasterCopy); event ConfigProviderSet(IDualGovernanceConfigProvider newConfigProvider); @@ -138,11 +140,28 @@ contract DualGovernance is IDualGovernance { } function cancelAllPendingProposals() external { + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); if (proposer.executor != TIMELOCK.getAdminExecutor()) { revert NotAdminProposer(); } + + State currentState = _stateMachine.getCurrentState(); + if (currentState != State.VetoSignalling && currentState != State.VetoSignallingDeactivation) { + /// @dev Early return to prevent "hanging" cancelPendingProposals() requests that could become unexpectedly + /// executable in the future. + /// + /// Some proposer contracts, such as Aragon Voting, may not support canceling already-consensed decisions. + /// This could lead to situations where a proposer’s cancelAllPendingProposals() call becomes unexecutable + /// if the Dual Governance state changes. However, it could become executable again if the system state + /// reverts to VetoSignalling or VetoSignallingDeactivation. + emit CancelAllPendingProposalsSkipped(); + return; + } + TIMELOCK.cancelAllNonExecutedProposals(); + emit CancelAllPendingProposalsExecuted(); } function canSubmitProposal() public view returns (bool) { From 8831953d2284aa35dbfb36960202f8c7331671a8 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 27 Aug 2024 16:24:52 +0300 Subject: [PATCH 04/86] fix: review fixes --- test/unit/libraries/Tiebreaker.t.sol | 47 ++++++++++++++-------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/test/unit/libraries/Tiebreaker.t.sol b/test/unit/libraries/Tiebreaker.t.sol index e213d3df..c3bccddb 100644 --- a/test/unit/libraries/Tiebreaker.t.sol +++ b/test/unit/libraries/Tiebreaker.t.sol @@ -23,7 +23,7 @@ contract TiebreakerTest is UnitTest { mockSealable2 = new SealableMock(); } - function test_addSealableWithdrawalBlocker() external { + function test_addSealableWithdrawalBlocker_HappyPath() external { vm.expectEmit(); emit Tiebreaker.SealableWithdrawalBlockerAdded(address(mockSealable1)); Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); @@ -31,14 +31,14 @@ contract TiebreakerTest is UnitTest { assertTrue(context.sealableWithdrawalBlockers.contains(address(mockSealable1))); } - function test_AddSealableWithdrawalBlocker_RevertOn_LimitReached() external { + function test_addSealableWithdrawalBlocker_RevertOn_LimitReached() external { Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); vm.expectRevert(Tiebreaker.SealableWithdrawalBlockersLimitReached.selector); Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable2), 1); } - function test_AddSealableWithdrawalBlocker_RevertOn_InvalidSealable() external { + function test_addSealableWithdrawalBlocker_RevertOn_InvalidSealable() external { mockSealable1.setShouldRevertIsPaused(true); vm.expectRevert(abi.encodeWithSelector(Tiebreaker.InvalidSealable.selector, address(mockSealable1))); @@ -50,7 +50,7 @@ contract TiebreakerTest is UnitTest { this.external__addSealableWithdrawalBlocker(address(0x123)); } - function test_RemoveSealableWithdrawalBlocker() external { + function test_removeSealableWithdrawalBlocker_HappyPath() external { Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); assertTrue(context.sealableWithdrawalBlockers.contains(address(mockSealable1))); @@ -61,7 +61,7 @@ contract TiebreakerTest is UnitTest { assertFalse(context.sealableWithdrawalBlockers.contains(address(mockSealable1))); } - function test_SetTiebreakerCommittee() external { + function test_setTiebreakerCommittee_HappyPath() external { address newCommittee = address(0x123); vm.expectEmit(); @@ -71,33 +71,34 @@ contract TiebreakerTest is UnitTest { assertEq(context.tiebreakerCommittee, newCommittee); } - function test_SetTiebreakerCommittee_WithExistingCommitteeAddress() external { + function test_setTiebreakerCommittee_WithExistingCommitteeAddress() external { address newCommittee = address(0x123); Tiebreaker.setTiebreakerCommittee(context, newCommittee); + vm.expectRevert(abi.encodeWithSelector(Tiebreaker.InvalidTiebreakerCommittee.selector, newCommittee)); Tiebreaker.setTiebreakerCommittee(context, newCommittee); } - function test_SetTiebreakerCommittee_RevertOn_ZeroAddress() external { + function test_setTiebreakerCommittee_RevertOn_ZeroAddress() external { vm.expectRevert(abi.encodeWithSelector(Tiebreaker.InvalidTiebreakerCommittee.selector, address(0))); Tiebreaker.setTiebreakerCommittee(context, address(0)); } - function testFuzz_SetTiebreakerActivationTimeout(uint32 minTimeout, uint32 maxTimeout, uint32 timeout) external { + function testFuzz_SetTiebreakerActivationTimeout( + Duration minTimeout, + Duration maxTimeout, + Duration timeout + ) external { vm.assume(minTimeout < timeout && timeout < maxTimeout); - Duration min = Duration.wrap(minTimeout); - Duration max = Duration.wrap(maxTimeout); - Duration newTimeout = Duration.wrap(timeout); - vm.expectEmit(); - emit Tiebreaker.TiebreakerActivationTimeoutSet(newTimeout); + emit Tiebreaker.TiebreakerActivationTimeoutSet(timeout); - Tiebreaker.setTiebreakerActivationTimeout(context, min, newTimeout, max); - assertEq(context.tiebreakerActivationTimeout, newTimeout); + Tiebreaker.setTiebreakerActivationTimeout(context, minTimeout, timeout, timeout); + assertEq(context.tiebreakerActivationTimeout, timeout); } - function test_SetTiebreakerActivationTimeout_RevertOn_InvalidTimeout() external { + function test_setTiebreakerActivationTimeout_RevertOn_InvalidTimeout() external { Duration minTimeout = Duration.wrap(1 days); Duration maxTimeout = Duration.wrap(10 days); Duration newTimeout = Duration.wrap(15 days); @@ -111,7 +112,7 @@ contract TiebreakerTest is UnitTest { Tiebreaker.setTiebreakerActivationTimeout(context, minTimeout, newTimeout, maxTimeout); } - function test_IsSomeSealableWithdrawalBlockerPaused() external { + function test_isSomeSealableWithdrawalBlockerPaused_HappyPath() external { mockSealable1.pauseFor(1 days); Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 2); @@ -129,7 +130,7 @@ contract TiebreakerTest is UnitTest { assertTrue(result); } - function test_CheckTie() external { + function test_checkTie_HappyPath() external { Timestamp cooldownExitedAt = Timestamps.from(block.timestamp); Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); @@ -144,7 +145,7 @@ contract TiebreakerTest is UnitTest { Tiebreaker.checkTie(context, DualGovernanceState.VetoSignalling, cooldownExitedAt); } - function test_CheckTie_RevertOn_NormalOrVetoCooldownState() external { + function test_checkTie_RevertOn_NormalOrVetoCooldownState() external { Timestamp cooldownExitedAt = Timestamps.from(block.timestamp); vm.expectRevert(Tiebreaker.TiebreakNotAllowed.selector); @@ -154,7 +155,7 @@ contract TiebreakerTest is UnitTest { Tiebreaker.checkTie(context, DualGovernanceState.VetoCooldown, cooldownExitedAt); } - function test_CheckCallerIsTiebreakerCommittee() external { + function test_checkCallerIsTiebreakerCommittee_HappyPath() external { context.tiebreakerCommittee = address(this); vm.expectRevert(abi.encodeWithSelector(Tiebreaker.CallerIsNotTiebreakerCommittee.selector, address(0x456))); @@ -164,11 +165,9 @@ contract TiebreakerTest is UnitTest { this.external__checkCallerIsTiebreakerCommittee(); } - function test_GetTimebreakerInfo() external { + function test_getTimebreakerInfo_HappyPath() external { Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); - Duration minTimeout = Duration.wrap(1 days); - Duration maxTimeout = Duration.wrap(10 days); Duration timeout = Duration.wrap(5 days); context.tiebreakerActivationTimeout = timeout; @@ -183,7 +182,7 @@ contract TiebreakerTest is UnitTest { assertEq(blockers.length, 1); } - function external__checkCallerIsTiebreakerCommittee() external { + function external__checkCallerIsTiebreakerCommittee() external view { Tiebreaker.checkCallerIsTiebreakerCommittee(context); } From 81e23ec85f69786a83f83f1287ee51835fc42f9d Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 28 Aug 2024 03:05:29 +0400 Subject: [PATCH 05/86] Add unit tests for cancelAllPendingProposals() --- contracts/DualGovernance.sol | 13 +- test/mocks/StETHMock.sol | 40 +++++ test/mocks/TimelockMock.sol | 4 + test/mocks/WithdrawalQueueMock.sol | 135 ++++++++++++++++ test/unit/DualGovernance.t.sol | 242 +++++++++++++++++++++++++++++ 5 files changed, 427 insertions(+), 7 deletions(-) create mode 100644 test/mocks/StETHMock.sol create mode 100644 test/mocks/WithdrawalQueueMock.sol create mode 100644 test/unit/DualGovernance.t.sol diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 26287f2c..bf4cad64 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -149,13 +149,12 @@ contract DualGovernance is IDualGovernance { State currentState = _stateMachine.getCurrentState(); if (currentState != State.VetoSignalling && currentState != State.VetoSignallingDeactivation) { - /// @dev Early return to prevent "hanging" cancelPendingProposals() requests that could become unexpectedly - /// executable in the future. - /// - /// Some proposer contracts, such as Aragon Voting, may not support canceling already-consensed decisions. - /// This could lead to situations where a proposer’s cancelAllPendingProposals() call becomes unexecutable - /// if the Dual Governance state changes. However, it could become executable again if the system state - /// reverts to VetoSignalling or VetoSignallingDeactivation. + /// @dev Some proposer contracts, like Aragon Voting, may not support canceling decisions that have already + /// reached consensus. This could lead to a situation where a proposer’s cancelAllPendingProposals() call + /// becomes unexecutable if the Dual Governance state changes. However, it might become executable again if + /// the system state shifts back to VetoSignalling or VetoSignallingDeactivation. + /// To avoid such a scenario, an early return is used instead of a revert when proposals cannot be canceled + /// due to an unsuitable Dual Governance state. emit CancelAllPendingProposalsSkipped(); return; } diff --git a/test/mocks/StETHMock.sol b/test/mocks/StETHMock.sol new file mode 100644 index 00000000..dcc5813a --- /dev/null +++ b/test/mocks/StETHMock.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; +import {IStETH} from "contracts/interfaces/IStETH.sol"; + +/* solhint-disable no-unused-vars,custom-errors */ +contract StETHMock is ERC20Mock, IStETH { + uint256 public __shareRate = 1 gwei; + + constructor() { + /// @dev the total supply of the stETH always > 0 + _mint(address(this), 100 wei); + } + + function __setShareRate(uint256 newShareRate) public { + __shareRate = newShareRate; + } + + function getSharesByPooledEth(uint256 ethAmount) external view returns (uint256) { + return ethAmount / __shareRate; + } + + function getPooledEthByShares(uint256 sharesAmount) external view returns (uint256) { + return __shareRate * sharesAmount; + } + + function transferShares(address to, uint256 sharesAmount) external { + transfer(to, sharesAmount * __shareRate); + } + + function transferSharesFrom( + address _sender, + address _recipient, + uint256 _sharesAmount + ) external returns (uint256 tokensAmount) { + tokensAmount = _sharesAmount * __shareRate; + transferFrom(_sender, _recipient, tokensAmount); + } +} diff --git a/test/mocks/TimelockMock.sol b/test/mocks/TimelockMock.sol index 314caed3..276726e0 100644 --- a/test/mocks/TimelockMock.sol +++ b/test/mocks/TimelockMock.sol @@ -107,6 +107,10 @@ contract TimelockMock is ITimelock { revert("Not Implemented"); } + function getProposalsCount() external view returns (uint256 count) { + return submittedProposals.length; + } + function getAdminExecutor() external view returns (address) { return _ADMIN_EXECUTOR; } diff --git a/test/mocks/WithdrawalQueueMock.sol b/test/mocks/WithdrawalQueueMock.sol new file mode 100644 index 00000000..57290346 --- /dev/null +++ b/test/mocks/WithdrawalQueueMock.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +// import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; /*, ERC721("test", "test")*/ +import {IWithdrawalQueue, WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; + +/* solhint-disable no-unused-vars,custom-errors */ +contract WithdrawalQueueMock is IWithdrawalQueue { + uint256 private _lastRequestId; + uint256 private _lastFinalizedRequestId; + uint256 private _minStETHWithdrawalAmount; + uint256 private _maxStETHWithdrawalAmount; + uint256[] private _requestWithdrawalsResult; + + constructor() {} + + function MIN_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256) { + return _minStETHWithdrawalAmount; + } + + function MAX_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256) { + return _maxStETHWithdrawalAmount; + } + + function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external { + revert("Not Implemented"); + } + + function getLastRequestId() external view returns (uint256) { + return _lastRequestId; + } + + function getLastFinalizedRequestId() external view returns (uint256) { + return _lastFinalizedRequestId; + } + + function getWithdrawalStatus(uint256[] calldata _requestIds) + external + view + returns (WithdrawalRequestStatus[] memory statuses) + { + revert("Not Implemented"); + } + + /// @notice Returns amount of ether available for claim for each provided request id + /// @param _requestIds array of request ids + /// @param _hints checkpoint hints. can be found with `findCheckpointHints(_requestIds, 1, getLastCheckpointIndex())` + /// @return claimableEthValues amount of claimable ether for each request, amount is equal to 0 if request + /// is not finalized or already claimed + function getClaimableEther( + uint256[] calldata _requestIds, + uint256[] calldata _hints + ) external view returns (uint256[] memory claimableEthValues) { + revert("Not Implemented"); + } + + function findCheckpointHints( + uint256[] calldata _requestIds, + uint256 _firstIndex, + uint256 _lastIndex + ) external view returns (uint256[] memory hintIds) { + revert("Not Implemented"); + } + + function getLastCheckpointIndex() external view returns (uint256) { + revert("Not Implemented"); + } + + function requestWithdrawals( + uint256[] calldata _amounts, + address _owner + ) external returns (uint256[] memory requestIds) { + return _requestWithdrawalsResult; + } + + function balanceOf(address owner) external view returns (uint256 balance) { + revert("Not Implemented"); + } + + function ownerOf(uint256 tokenId) external view returns (address owner) { + revert("Not Implemented"); + } + + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external { + revert("Not Implemented"); + } + + function safeTransferFrom(address from, address to, uint256 tokenId) external { + revert("Not Implemented"); + } + + function transferFrom(address from, address to, uint256 tokenId) external { + revert("Not Implemented"); + } + + function approve(address to, uint256 tokenId) external { + revert("Not Implemented"); + } + + function setApprovalForAll(address operator, bool approved) external { + revert("Not Implemented"); + } + + function getApproved(uint256 tokenId) external view returns (address operator) { + revert("Not Implemented"); + } + + function isApprovedForAll(address owner, address operator) external view returns (bool) { + revert("Not Implemented"); + } + + function supportsInterface(bytes4 interfaceId) external view returns (bool) { + revert("Not Implemented"); + } + + function setLastRequestId(uint256 id) public { + _lastRequestId = id; + } + + function setLastFinalizedRequestId(uint256 id) public { + _lastFinalizedRequestId = id; + } + + function setMinStETHWithdrawalAmount(uint256 amount) public { + _minStETHWithdrawalAmount = amount; + } + + function setMaxStETHWithdrawalAmount(uint256 amount) public { + _maxStETHWithdrawalAmount = amount; + } + + function setRequestWithdrawalsResult(uint256[] memory requestIds) public { + _requestWithdrawalsResult = requestIds; + } +} diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol new file mode 100644 index 00000000..032f043f --- /dev/null +++ b/test/unit/DualGovernance.t.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Durations} from "contracts/types/Duration.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; + +import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; + +import {Escrow} from "contracts/Escrow.sol"; +import {Executor} from "contracts/Executor.sol"; +import {DualGovernance, State} from "contracts/DualGovernance.sol"; +import {IResealManager} from "contracts/interfaces/IResealManager.sol"; +import { + DualGovernanceConfig, + IDualGovernanceConfigProvider, + ImmutableDualGovernanceConfigProvider +} from "contracts/DualGovernanceConfigProvider.sol"; + +import {IWstETH} from "contracts/interfaces/IWstETH.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; +import {StETHMock} from "test/mocks/StETHMock.sol"; +import {TimelockMock} from "test/mocks/TimelockMock.sol"; +import {WithdrawalQueueMock} from "test/mocks/WithdrawalQueueMock.sol"; + +contract DualGovernanceUnitTests is UnitTest { + Executor private _executor = new Executor(address(this)); + + StETHMock private immutable _STETH_MOCK = new StETHMock(); + IWithdrawalQueue private immutable _WITHDRAWAL_QUEUE_MOCK = new WithdrawalQueueMock(); + + // TODO: Replace with mocks + IWstETH private immutable _WSTETH_STUB = IWstETH(makeAddr("WSTETH_STUB")); + IResealManager private immutable _RESEAL_MANAGER_STUB = IResealManager(makeAddr("RESEAL_MANAGER_STUB")); + + TimelockMock internal _timelock = new TimelockMock(address(_executor)); + ImmutableDualGovernanceConfigProvider internal _configProvider = new ImmutableDualGovernanceConfigProvider( + DualGovernanceConfig.Context({ + firstSealRageQuitSupport: PercentsD16.fromBasisPoints(3_00), // 3% + secondSealRageQuitSupport: PercentsD16.fromBasisPoints(15_00), // 15% + // + minAssetsLockDuration: Durations.from(5 hours), + dynamicTimelockMinDuration: Durations.from(3 days), + dynamicTimelockMaxDuration: Durations.from(30 days), + // + vetoSignallingMinActiveDuration: Durations.from(5 hours), + vetoSignallingDeactivationMaxDuration: Durations.from(5 days), + vetoCooldownDuration: Durations.from(4 days), + // + rageQuitExtensionDelay: Durations.from(7 days), + rageQuitEthWithdrawalsMinTimelock: Durations.from(60 days), + rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: 2, + rageQuitEthWithdrawalsTimelockGrowthCoeffs: [uint256(0), 0, 0] + }) + ); + + DualGovernance internal _dualGovernance = new DualGovernance({ + dependencies: DualGovernance.ExternalDependencies({ + stETH: _STETH_MOCK, + wstETH: _WSTETH_STUB, + withdrawalQueue: _WITHDRAWAL_QUEUE_MOCK, + timelock: _timelock, + resealManager: _RESEAL_MANAGER_STUB, + configProvider: _configProvider + }), + sanityCheckParams: DualGovernance.SanityCheckParams({ + minWithdrawalsBatchSize: 4, + minTiebreakerActivationTimeout: Durations.from(30 days), + maxTiebreakerActivationTimeout: Durations.from(180 days), + maxSealableWithdrawalBlockersCount: 128 + }) + }); + + function setUp() external { + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(this), address(_executor)) + ); + } + + // --- + // cancelAllPendingProposals() + // --- + + function test_cancelAllPendingProposals_HappyPath_SkippedInNormalState() external { + _submitMockProposal(); + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + assertEq(_dualGovernance.getCurrentState(), State.Normal); + + vm.expectEmit(); + emit DualGovernance.CancelAllPendingProposalsSkipped(); + + _dualGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + } + + function test_cancelAllPendingProposals_HappyPath_SkippedInVetoCooldownState() external { + _submitMockProposal(); + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + assertEq(_dualGovernance.getCurrentState(), State.Normal); + + Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); + + address vetoer = makeAddr("VETOER"); + _STETH_MOCK.mint(vetoer, 10 ether); + + vm.startPrank(vetoer); + _STETH_MOCK.approve(address(signallingEscrow), 10 ether); + signallingEscrow.lockStETH(5 ether); + vm.stopPrank(); + + assertEq(_dualGovernance.getCurrentState(), State.VetoSignalling); + + _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); + + vm.prank(vetoer); + signallingEscrow.unlockStETH(); + + assertEq(_dualGovernance.getCurrentState(), State.VetoSignallingDeactivation); + + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getCurrentState(), State.VetoCooldown); + + vm.expectEmit(); + emit DualGovernance.CancelAllPendingProposalsSkipped(); + + _dualGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + } + + function test_cancelAllPendingProposals_HappyPath_SkippedInRageQuitState() external { + _submitMockProposal(); + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + assertEq(_dualGovernance.getCurrentState(), State.Normal); + + Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); + + address vetoer = makeAddr("VETOER"); + _STETH_MOCK.mint(vetoer, 10 ether); + + vm.startPrank(vetoer); + _STETH_MOCK.approve(address(signallingEscrow), 10 ether); + signallingEscrow.lockStETH(5 ether); + vm.stopPrank(); + + assertEq(_dualGovernance.getCurrentState(), State.VetoSignalling); + + _wait(_configProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getCurrentState(), State.RageQuit); + + vm.expectEmit(); + emit DualGovernance.CancelAllPendingProposalsSkipped(); + + _dualGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + } + + function test_cancelAllPendingProposals_HappyPath_ExecutedInVetoSignallingState() external { + _submitMockProposal(); + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + assertEq(_dualGovernance.getCurrentState(), State.Normal); + + Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); + + address vetoer = makeAddr("VETOER"); + _STETH_MOCK.mint(vetoer, 10 ether); + + vm.startPrank(vetoer); + _STETH_MOCK.approve(address(signallingEscrow), 10 ether); + signallingEscrow.lockStETH(5 ether); + vm.stopPrank(); + + assertEq(_dualGovernance.getCurrentState(), State.VetoSignalling); + + vm.expectEmit(); + emit DualGovernance.CancelAllPendingProposalsExecuted(); + + _dualGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 1); + } + + function test_cancelAllPendingProposals_HappyPath_ExecutedInVetoSignallingDeactivationState() external { + _submitMockProposal(); + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + assertEq(_dualGovernance.getCurrentState(), State.Normal); + + Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); + + address vetoer = makeAddr("VETOER"); + _STETH_MOCK.mint(vetoer, 10 ether); + + vm.startPrank(vetoer); + _STETH_MOCK.approve(address(signallingEscrow), 10 ether); + signallingEscrow.lockStETH(5 ether); + vm.stopPrank(); + + assertEq(_dualGovernance.getCurrentState(), State.VetoSignalling); + + _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); + + vm.prank(vetoer); + signallingEscrow.unlockStETH(); + + assertEq(_dualGovernance.getCurrentState(), State.VetoSignallingDeactivation); + + vm.expectEmit(); + emit DualGovernance.CancelAllPendingProposalsExecuted(); + + _dualGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 1); + } + + // --- + // Helper methods + // --- + + function _submitMockProposal() internal { + // mock timelock doesn't uses proposal data + _timelock.submit(address(0), new ExternalCall[](0)); + } +} From dab02d6898c1403b0fd3350ad0aec4b75e0dbbdd Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 28 Aug 2024 03:09:32 +0400 Subject: [PATCH 06/86] Fix typo in the unstETHLockedShares value of getVetoerState method --- contracts/Escrow.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 6d1190cc..f71d5f54 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -403,7 +403,7 @@ contract Escrow is IEscrow { state.unstETHIdsCount = assets.unstETHIds.length; state.stETHLockedShares = assets.stETHLockedShares.toUint256(); - state.unstETHLockedShares = assets.stETHLockedShares.toUint256(); + state.unstETHLockedShares = assets.unstETHLockedShares.toUint256(); state.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp.toSeconds(); } From 16631755e1bb535e0ee146a4636f82c295511c56 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 28 Aug 2024 04:47:39 +0400 Subject: [PATCH 07/86] Fix the overflow of the rage quit round --- .../libraries/DualGovernanceStateMachine.sol | 14 +++- test/mocks/EscrowMock.sol | 43 ++++++++++ .../DualGovernanceStateMachine.t.sol | 80 +++++++++++++++++++ 3 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 test/mocks/EscrowMock.sol create mode 100644 test/unit/libraries/DualGovernanceStateMachine.t.sol diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 59a9c810..d783d55f 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -64,6 +64,8 @@ library DualGovernanceStateMachine { event NewSignallingEscrowDeployed(IEscrow indexed escrow); event DualGovernanceStateChanged(State from, State to, Context state); + uint256 internal constant MAX_RAGE_QUIT_ROUND = type(uint8).max; + function initialize( Context storage self, DualGovernanceConfig.Context memory config, @@ -108,10 +110,16 @@ library DualGovernanceStateMachine { } } else if (newState == State.RageQuit) { IEscrow signallingEscrow = self.signallingEscrow; - uint256 rageQuitRound = Math.min(self.rageQuitRound + 1, type(uint8).max); - self.rageQuitRound = uint8(rageQuitRound); + + uint256 currentRageQuitRound = self.rageQuitRound; + + /// @dev Limits the maximum value of the rage quit round to prevent failures due to arithmetic overflow + /// if the number of consecutive rage quits reaches MAX_RAGE_QUIT_ROUND. + uint256 newRageQuitRound = Math.min(currentRageQuitRound + 1, MAX_RAGE_QUIT_ROUND); + self.rageQuitRound = uint8(newRageQuitRound); + signallingEscrow.startRageQuit( - config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(rageQuitRound) + config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(newRageQuitRound) ); self.rageQuitEscrow = signallingEscrow; _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); diff --git a/test/mocks/EscrowMock.sol b/test/mocks/EscrowMock.sol new file mode 100644 index 00000000..fe3e0798 --- /dev/null +++ b/test/mocks/EscrowMock.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "contracts/types/Duration.sol"; +import {PercentD16} from "contracts/types/PercentD16.sol"; + +import {IEscrow} from "contracts/interfaces/IEscrow.sol"; + +contract EscrowMock is IEscrow { + event __RageQuitStarted(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock); + + Duration public __minAssetsLockDuration; + PercentD16 public __rageQuitSupport; + bool public __isRageQuitFinalized; + + function __setRageQuitSupport(PercentD16 newRageQuitSupport) external { + __rageQuitSupport = newRageQuitSupport; + } + + function __setIsRageQuitFinalized(bool newIsRageQuitFinalized) external { + __isRageQuitFinalized = newIsRageQuitFinalized; + } + + function initialize(Duration minAssetsLockDuration) external { + __minAssetsLockDuration = minAssetsLockDuration; + } + + function startRageQuit(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock) external { + emit __RageQuitStarted(rageQuitExtraTimelock, rageQuitWithdrawalsTimelock); + } + + function isRageQuitFinalized() external view returns (bool) { + return __isRageQuitFinalized; + } + + function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport) { + return __rageQuitSupport; + } + + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { + __minAssetsLockDuration = newMinAssetsLockDuration; + } +} diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol new file mode 100644 index 00000000..8aafa5c8 --- /dev/null +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {Durations} from "contracts/types/Duration.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; + +import {DualGovernanceStateMachine, State} from "contracts/libraries/DualGovernanceStateMachine.sol"; +import {DualGovernanceConfig, ImmutableDualGovernanceConfigProvider} from "contracts/DualGovernanceConfigProvider.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; +import {EscrowMock} from "test/mocks/EscrowMock.sol"; + +contract DualGovernanceStateMachineUnitTests is UnitTest { + using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; + + address private immutable _ESCROW_MASTER_COPY = address(new EscrowMock()); + ImmutableDualGovernanceConfigProvider internal immutable _CONFIG_PROVIDER = new ImmutableDualGovernanceConfigProvider( + DualGovernanceConfig.Context({ + firstSealRageQuitSupport: PercentsD16.fromBasisPoints(3_00), // 3% + secondSealRageQuitSupport: PercentsD16.fromBasisPoints(15_00), // 15% + // + minAssetsLockDuration: Durations.from(5 hours), + dynamicTimelockMinDuration: Durations.from(3 days), + dynamicTimelockMaxDuration: Durations.from(30 days), + // + vetoSignallingMinActiveDuration: Durations.from(5 hours), + vetoSignallingDeactivationMaxDuration: Durations.from(5 days), + vetoCooldownDuration: Durations.from(4 days), + // + rageQuitExtensionDelay: Durations.from(7 days), + rageQuitEthWithdrawalsMinTimelock: Durations.from(60 days), + rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: 2, + rageQuitEthWithdrawalsTimelockGrowthCoeffs: [uint256(0), 0, 0] + }) + ); + + DualGovernanceStateMachine.Context private _stateMachine; + + function setUp() external { + _stateMachine.initialize(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + } + + function test_activateNextState_HappyPath_MaxRageQuitsRound() external { + assertEq(_stateMachine.state, State.Normal); + + for (uint256 i = 0; i < 2 * DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND; ++i) { + address signallingEscrow = address(_stateMachine.signallingEscrow); + EscrowMock(signallingEscrow).__setRageQuitSupport( + _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); + assertTrue( + _stateMachine.signallingEscrow.getRageQuitSupport() > _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + ); + assertEq(_stateMachine.rageQuitRound, Math.min(i, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND)); + + // wait here the full duration of the veto cooldown to make sure it's over from the previous iteration + _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); + + _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + assertEq(_stateMachine.state, State.VetoSignalling); + + _wait(_CONFIG_PROVIDER.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + + assertEq(_stateMachine.state, State.RageQuit); + assertEq(_stateMachine.rageQuitRound, Math.min(i + 1, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND)); + + EscrowMock(signallingEscrow).__setIsRageQuitFinalized(true); + _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + assertEq(_stateMachine.state, State.VetoCooldown); + } + + // after the sequential rage quits chain is broken, the rage quit resets to 0 + _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); + _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + assertEq(_stateMachine.state, State.Normal); + } +} From 51ad0efe33a2fee1f0239295c5ed35d5ec0d9b5c Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 28 Aug 2024 17:03:57 +0400 Subject: [PATCH 08/86] Add check for rageQuitRound into the unit test --- test/unit/libraries/DualGovernanceStateMachine.t.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 8aafa5c8..3e5d6cdf 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -75,6 +75,8 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { // after the sequential rage quits chain is broken, the rage quit resets to 0 _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + + assertEq(_stateMachine.rageQuitRound, 0); assertEq(_stateMachine.state, State.Normal); } } From 7cc76cbcd72863006ed22c142f98aadcf6df5ae8 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Fri, 30 Aug 2024 14:50:27 +0300 Subject: [PATCH 09/86] fix: tiebreaker getter in dualgovernance --- contracts/DualGovernance.sol | 15 +++-------- contracts/committees/TiebreakerCore.sol | 8 +++--- .../committees/TiebreakerSubCommittee.sol | 2 +- contracts/interfaces/IDualGovernance.sol | 6 ++--- contracts/interfaces/ITiebreaker.sol | 19 ++++++++++--- contracts/interfaces/ITiebreakerCore.sol | 8 ++++++ contracts/libraries/Tiebreaker.sol | 27 +++++++------------ test/scenario/tiebreaker.t.sol | 4 +-- 8 files changed, 44 insertions(+), 45 deletions(-) create mode 100644 contracts/interfaces/ITiebreakerCore.sol diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index ffc5d822..6be3dad8 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -10,6 +10,7 @@ import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; +import {ITiebreaker} from "./interfaces/ITiebreaker.sol"; import {IResealManager} from "./interfaces/IResealManager.sol"; import {Proposers} from "./libraries/Proposers.sol"; @@ -290,18 +291,8 @@ contract DualGovernance is IDualGovernance { TIMELOCK.schedule(proposalId); } - struct TiebreakerState { - address tiebreakerCommittee; - Duration tiebreakerActivationTimeout; - address[] sealableWithdrawalBlockers; - } - - function getTiebreakerState() external view returns (TiebreakerState memory tiebreakerState) { - ( - tiebreakerState.tiebreakerCommittee, - tiebreakerState.tiebreakerActivationTimeout, - tiebreakerState.sealableWithdrawalBlockers - ) = _tiebreaker.getTiebreakerInfo(); + function getTiebreakerContext() external view returns (ITiebreaker.Context memory tiebreakerState) { + return _tiebreaker.getTiebreakerContext(); } // --- diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol index bcb75d7a..ea5d6aa7 100644 --- a/contracts/committees/TiebreakerCore.sol +++ b/contracts/committees/TiebreakerCore.sol @@ -3,8 +3,8 @@ pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {ITiebreakerCore} from "../interfaces/ITiebreaker.sol"; -import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; +import {ITiebreakerCore} from "../interfaces/ITiebreakerCore.sol"; +import {ITiebreaker} from "../interfaces/ITiebreaker.sol"; import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; import {Timestamp} from "../types/Timestamp.sol"; @@ -66,7 +66,7 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { (, bytes32 key) = _encodeScheduleProposal(proposalId); _markUsed(key); Address.functionCall( - DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.tiebreakerScheduleProposal.selector, proposalId) + DUAL_GOVERNANCE, abi.encodeWithSelector(ITiebreaker.tiebreakerScheduleProposal.selector, proposalId) ); } @@ -126,7 +126,7 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { _markUsed(key); _sealableResumeNonces[sealable]++; Address.functionCall( - DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.tiebreakerResumeSealable.selector, sealable) + DUAL_GOVERNANCE, abi.encodeWithSelector(ITiebreaker.tiebreakerResumeSealable.selector, sealable) ); } diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol index e4113143..c6ed3f88 100644 --- a/contracts/committees/TiebreakerSubCommittee.sol +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {ITiebreakerCore} from "../interfaces/ITiebreaker.sol"; +import {ITiebreakerCore} from "../interfaces/ITiebreakerCore.sol"; import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; import {Timestamp} from "../types/Timestamp.sol"; diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 1ee4e000..ce156606 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -2,12 +2,10 @@ pragma solidity 0.8.26; import {IGovernance} from "./IGovernance.sol"; +import {ITiebreaker} from "./ITiebreaker.sol"; -interface IDualGovernance is IGovernance { +interface IDualGovernance is IGovernance, ITiebreaker { function activateNextState() external; function resealSealable(address sealables) external; - - function tiebreakerScheduleProposal(uint256 proposalId) external; - function tiebreakerResumeSealable(address sealable) external; } diff --git a/contracts/interfaces/ITiebreaker.sol b/contracts/interfaces/ITiebreaker.sol index 0993fc31..90be1e2c 100644 --- a/contracts/interfaces/ITiebreaker.sol +++ b/contracts/interfaces/ITiebreaker.sol @@ -1,8 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -interface ITiebreakerCore { - function getSealableResumeNonce(address sealable) external view returns (uint256 nonce); - function scheduleProposal(uint256 _proposalId) external; - function sealableResume(address sealable, uint256 nonce) external; +import {Duration} from "../types/Duration.sol"; + +interface ITiebreaker { + struct Context { + address tiebreakerCommittee; + Duration tiebreakerActivationTimeout; + address[] sealableWithdrawalBlockers; + } + + function addTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external; + function removeTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external; + function setTiebreakerCommittee(address tiebreakerCommittee) external; + function setTiebreakerActivationTimeout(Duration tiebreakerActivationTimeout) external; + function tiebreakerScheduleProposal(uint256 proposalId) external; + function tiebreakerResumeSealable(address sealable) external; } diff --git a/contracts/interfaces/ITiebreakerCore.sol b/contracts/interfaces/ITiebreakerCore.sol new file mode 100644 index 00000000..0993fc31 --- /dev/null +++ b/contracts/interfaces/ITiebreakerCore.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +interface ITiebreakerCore { + function getSealableResumeNonce(address sealable) external view returns (uint256 nonce); + function scheduleProposal(uint256 _proposalId) external; + function sealableResume(address sealable, uint256 nonce) external; +} diff --git a/contracts/libraries/Tiebreaker.sol b/contracts/libraries/Tiebreaker.sol index 6051164f..b559baf2 100644 --- a/contracts/libraries/Tiebreaker.sol +++ b/contracts/libraries/Tiebreaker.sol @@ -5,6 +5,7 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {Duration} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Duration.sol"; import {ISealable} from "../interfaces/ISealable.sol"; +import {ITiebreaker} from "../interfaces/ITiebreaker.sol"; import {SealableCalls} from "./SealableCalls.sol"; import {State as DualGovernanceState} from "./DualGovernanceStateMachine.sol"; @@ -182,28 +183,18 @@ library Tiebreaker { return false; } - /// @notice Gets the tiebreaker information. - /// @param self The context storage. - /// @return tiebreakerCommittee The address of the tiebreaker committee. - /// @return tiebreakerActivationTimeout The duration of the tiebreaker activation timeout. - /// @return sealableWithdrawalBlockers The addresses of the sealable withdrawal blockers. - function getTiebreakerInfo(Context storage self) - internal - view - returns ( - address tiebreakerCommittee, - Duration tiebreakerActivationTimeout, - address[] memory sealableWithdrawalBlockers - ) - { - tiebreakerCommittee = self.tiebreakerCommittee; - tiebreakerActivationTimeout = self.tiebreakerActivationTimeout; + /// @dev Retrieves the tiebreaker context from the storage. + /// @param self The storage context. + /// @return context The tiebreaker context containing the tiebreaker committee, tiebreaker activation timeout, and sealable withdrawal blockers. + function getTiebreakerContext(Context storage self) internal view returns (ITiebreaker.Context memory context) { + context.tiebreakerCommittee = self.tiebreakerCommittee; + context.tiebreakerActivationTimeout = self.tiebreakerActivationTimeout; uint256 sealableWithdrawalBlockersCount = self.sealableWithdrawalBlockers.length(); - sealableWithdrawalBlockers = new address[](sealableWithdrawalBlockersCount); + context.sealableWithdrawalBlockers = new address[](sealableWithdrawalBlockersCount); for (uint256 i = 0; i < sealableWithdrawalBlockersCount; ++i) { - sealableWithdrawalBlockers[i] = self.sealableWithdrawalBlockers.at(i); + context.sealableWithdrawalBlockers[i] = self.sealableWithdrawalBlockers.at(i); } } } diff --git a/test/scenario/tiebreaker.t.sol b/test/scenario/tiebreaker.t.sol index a7062717..e56e169c 100644 --- a/test/scenario/tiebreaker.t.sol +++ b/test/scenario/tiebreaker.t.sol @@ -31,7 +31,7 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertRageQuitState(); - _wait(_dualGovernance.getTiebreakerState().tiebreakerActivationTimeout); + _wait(_dualGovernance.getTiebreakerContext().tiebreakerActivationTimeout); _activateNextState(); ExternalCall[] memory proposalCalls = ExternalCallHelpers.create(address(0), new bytes(0)); @@ -106,7 +106,7 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertRageQuitState(); - _wait(_dualGovernance.getTiebreakerState().tiebreakerActivationTimeout); + _wait(_dualGovernance.getTiebreakerContext().tiebreakerActivationTimeout); _activateNextState(); // Tiebreaker subcommittee 0 From c169036940525c541092669e1b4a80703e45e93f Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Fri, 30 Aug 2024 15:27:23 +0300 Subject: [PATCH 10/86] fix: make add/remove sealable revertable --- contracts/libraries/Tiebreaker.sol | 14 ++++++++------ test/unit/libraries/Tiebreaker.t.sol | 28 ++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/contracts/libraries/Tiebreaker.sol b/contracts/libraries/Tiebreaker.sol index 6051164f..9b91bc0d 100644 --- a/contracts/libraries/Tiebreaker.sol +++ b/contracts/libraries/Tiebreaker.sol @@ -24,6 +24,8 @@ library Tiebreaker { error InvalidTiebreakerActivationTimeout(Duration timeout); error CallerIsNotTiebreakerCommittee(address caller); error SealableWithdrawalBlockersLimitReached(); + error SealableWithdrawalBlockerNotFound(address sealable); + error SealableWithdrawalBlockerAlreadyAdded(address sealable); event SealableWithdrawalBlockerAdded(address sealable); event SealableWithdrawalBlockerRemoved(address sealable); @@ -66,20 +68,20 @@ library Tiebreaker { revert InvalidSealable(sealableWithdrawalBlocker); } - bool isSuccessfullyAdded = self.sealableWithdrawalBlockers.add(sealableWithdrawalBlocker); - if (isSuccessfullyAdded) { - emit SealableWithdrawalBlockerAdded(sealableWithdrawalBlocker); + if (!self.sealableWithdrawalBlockers.add(sealableWithdrawalBlocker)) { + revert SealableWithdrawalBlockerAlreadyAdded(sealableWithdrawalBlocker); } + emit SealableWithdrawalBlockerAdded(sealableWithdrawalBlocker); } /// @notice Removes a sealable withdrawal blocker. /// @param self The context storage. /// @param sealableWithdrawalBlocker The address of the sealable withdrawal blocker to remove. function removeSealableWithdrawalBlocker(Context storage self, address sealableWithdrawalBlocker) internal { - bool isSuccessfullyRemoved = self.sealableWithdrawalBlockers.remove(sealableWithdrawalBlocker); - if (isSuccessfullyRemoved) { - emit SealableWithdrawalBlockerRemoved(sealableWithdrawalBlocker); + if (!self.sealableWithdrawalBlockers.remove(sealableWithdrawalBlocker)) { + revert SealableWithdrawalBlockerNotFound(sealableWithdrawalBlocker); } + emit SealableWithdrawalBlockerRemoved(sealableWithdrawalBlocker); } /// @notice Sets the tiebreaker committee. diff --git a/test/unit/libraries/Tiebreaker.t.sol b/test/unit/libraries/Tiebreaker.t.sol index c3bccddb..0d322b02 100644 --- a/test/unit/libraries/Tiebreaker.t.sol +++ b/test/unit/libraries/Tiebreaker.t.sol @@ -38,16 +38,25 @@ contract TiebreakerTest is UnitTest { Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable2), 1); } + function test_addSealableWithdrawalBlocker_RevertOn_AlreadyAdded() external { + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 2); + + vm.expectRevert( + abi.encodeWithSelector(Tiebreaker.SealableWithdrawalBlockerAlreadyAdded.selector, address(mockSealable1)) + ); + this.external__addSealableWithdrawalBlocker(address(mockSealable1), 2); + } + function test_addSealableWithdrawalBlocker_RevertOn_InvalidSealable() external { mockSealable1.setShouldRevertIsPaused(true); vm.expectRevert(abi.encodeWithSelector(Tiebreaker.InvalidSealable.selector, address(mockSealable1))); // external call should be used to intercept the revert - this.external__addSealableWithdrawalBlocker(address(mockSealable1)); + this.external__addSealableWithdrawalBlocker(address(mockSealable1), 2); vm.expectRevert(); // external call should be used to intercept the revert - this.external__addSealableWithdrawalBlocker(address(0x123)); + this.external__addSealableWithdrawalBlocker(address(0x123), 2); } function test_removeSealableWithdrawalBlocker_HappyPath() external { @@ -61,6 +70,13 @@ contract TiebreakerTest is UnitTest { assertFalse(context.sealableWithdrawalBlockers.contains(address(mockSealable1))); } + function test_removeSealableWithdrawalBlocker_RevertOn_NotFound() external { + vm.expectRevert( + abi.encodeWithSelector(Tiebreaker.SealableWithdrawalBlockerNotFound.selector, address(mockSealable1)) + ); + this.external__removeSealableWithdrawalBlocker(address(mockSealable1)); + } + function test_setTiebreakerCommittee_HappyPath() external { address newCommittee = address(0x123); @@ -186,7 +202,11 @@ contract TiebreakerTest is UnitTest { Tiebreaker.checkCallerIsTiebreakerCommittee(context); } - function external__addSealableWithdrawalBlocker(address sealable) external { - Tiebreaker.addSealableWithdrawalBlocker(context, sealable, 1); + function external__addSealableWithdrawalBlocker(address sealable, uint256 count) external { + Tiebreaker.addSealableWithdrawalBlocker(context, sealable, count); + } + + function external__removeSealableWithdrawalBlocker(address sealable) external { + Tiebreaker.removeSealableWithdrawalBlocker(context, sealable); } } From 0e81a4ab01e2b8bd2c598ac0acee9eea3e660d76 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Mon, 2 Sep 2024 17:30:23 +0300 Subject: [PATCH 11/86] fix: emergency protection missed tests --- test/unit/libraries/EmergencyProtection.t.sol | 72 ++++++++++++++----- 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/test/unit/libraries/EmergencyProtection.t.sol b/test/unit/libraries/EmergencyProtection.t.sol index 0f5faf9b..4789953d 100644 --- a/test/unit/libraries/EmergencyProtection.t.sol +++ b/test/unit/libraries/EmergencyProtection.t.sol @@ -23,7 +23,7 @@ contract EmergencyProtectionTest is UnitTest { ctx.emergencyProtectionEndsAfter = Timestamps.from(block.timestamp + 86400); } - function test_ActivateEmergencyMode() external { + function test_activateEmergencyMode_HappyPath() external { vm.expectEmit(); emit EmergencyProtection.EmergencyModeActivated(); EmergencyProtection.activateEmergencyMode(ctx); @@ -32,7 +32,7 @@ contract EmergencyProtectionTest is UnitTest { assertEq(Timestamp.unwrap(ctx.emergencyModeEndsAfter), block.timestamp + 3600); } - function test_ActivateEmergencyMode_RevertOn_ProtectionExpired() external { + function test_activateEmergencyMode_RevertOn_ProtectionExpired() external { Duration untilExpiration = Durations.between(ctx.emergencyProtectionEndsAfter, Timestamps.from(block.timestamp)).plusSeconds(1); @@ -46,7 +46,7 @@ contract EmergencyProtectionTest is UnitTest { EmergencyProtection.activateEmergencyMode(ctx); } - function test_DeactivateEmergencyMode() external { + function test_deactivateEmergencyMode_HappyPath() external { EmergencyProtection.activateEmergencyMode(ctx); vm.expectEmit(); @@ -61,7 +61,7 @@ contract EmergencyProtectionTest is UnitTest { assertEq(Duration.unwrap(ctx.emergencyModeDuration), 0); } - function test_SetEmergencyGovernance() external { + function test_setEmergencyGovernance_HappyPath() external { address newGovernance = address(0x4); vm.expectEmit(); @@ -71,14 +71,14 @@ contract EmergencyProtectionTest is UnitTest { assertEq(ctx.emergencyGovernance, newGovernance); } - function test_SetEmergencyGovernance_RevertOn_SameAddress() external { + function test_setEmergencyGovernance_RevertOn_SameAddress() external { vm.expectRevert( abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyGovernance.selector, emergencyGovernance) ); EmergencyProtection.setEmergencyGovernance(ctx, emergencyGovernance); } - function test_SetEmergencyProtectionEndDate() external { + function test_setEmergencyProtectionEndDate_HappyPath() external { Timestamp newEndDate = Timestamps.from(block.timestamp + 43200); vm.expectEmit(); @@ -88,7 +88,7 @@ contract EmergencyProtectionTest is UnitTest { assertEq(Timestamp.unwrap(ctx.emergencyProtectionEndsAfter), block.timestamp + 43200); } - function test_SetEmergencyProtectionEndDate_RevertOn_InvalidValue() external { + function test_setEmergencyProtectionEndDate_RevertOn_InvalidValue() external { Timestamp invalidEndDate = Timestamps.from(block.timestamp + 90000); vm.expectRevert( @@ -104,7 +104,7 @@ contract EmergencyProtectionTest is UnitTest { EmergencyProtection.setEmergencyProtectionEndDate(ctx, ctx.emergencyProtectionEndsAfter, Duration.wrap(86400)); } - function test_SetEmergencyModeDuration() external { + function test_setEmergencyModeDuration() external { Duration newDuration = Duration.wrap(7200); vm.expectEmit(); @@ -114,7 +114,7 @@ contract EmergencyProtectionTest is UnitTest { assertEq(Duration.unwrap(ctx.emergencyModeDuration), 7200); } - function test_SetEmergencyModeDuration_RevertOn_InvalidValue() external { + function test_setEmergencyModeDuration_RevertOn_InvalidValue() external { Duration invalidDuration = Duration.wrap(90000); vm.expectRevert( @@ -128,12 +128,46 @@ contract EmergencyProtectionTest is UnitTest { EmergencyProtection.setEmergencyModeDuration(ctx, ctx.emergencyModeDuration, Duration.wrap(86400)); } - function test_CheckCallerIsEmergencyActivationCommittee() external { + function testFuzz_setEmergencyActivationCommittee_HappyPath(address committee) external { + vm.assume(committee != emergencyActivationCommittee); + vm.expectEmit(); + emit EmergencyProtection.EmergencyActivationCommitteeSet(committee); + + EmergencyProtection.setEmergencyActivationCommittee(ctx, committee); + } + + function test_setEmergencyActivationCommittee_RevertOn_SameAddress() external { + address committee = address(0x123); + EmergencyProtection.setEmergencyActivationCommittee(ctx, committee); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyActivationCommittee.selector, committee) + ); + EmergencyProtection.setEmergencyActivationCommittee(ctx, committee); + } + + function testFuzz_setEmergencyExecutionCommittee_HappyPath(address committee) external { + vm.assume(committee != emergencyExecutionCommittee); + vm.expectEmit(); + emit EmergencyProtection.EmergencyExecutionCommitteeSet(committee); + + EmergencyProtection.setEmergencyExecutionCommittee(ctx, committee); + } + + function test_setEmergencyExecutionCommittee_RevertOn_SameAddress() external { + address committee = address(0x123); + EmergencyProtection.setEmergencyExecutionCommittee(ctx, committee); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyExecutionCommittee.selector, committee) + ); + EmergencyProtection.setEmergencyExecutionCommittee(ctx, committee); + } + + function test_checkCallerIsEmergencyActivationCommittee_HappyPath() external { vm.prank(emergencyActivationCommittee); this.external__checkCallerIsEmergencyActivationCommittee(); } - function test_CheckCallerIsEmergencyActivationCommittee_RevertOn_Stranger() external { + function test_checkCallerIsEmergencyActivationCommittee_RevertOn_Stranger() external { vm.expectRevert( abi.encodeWithSelector(EmergencyProtection.CallerIsNotEmergencyActivationCommittee.selector, address(0x5)) ); @@ -141,12 +175,12 @@ contract EmergencyProtectionTest is UnitTest { this.external__checkCallerIsEmergencyActivationCommittee(); } - function test_CheckCallerIsEmergencyExecutionCommittee() external { + function test_checkCallerIsEmergencyExecutionCommittee_HappyPath() external { vm.prank(emergencyExecutionCommittee); this.external__checkCallerIsEmergencyExecutionCommittee(); } - function test_CheckCallerIsEmergencyExecutionCommittee_RevertOn_Stranger() external { + function test_checkCallerIsEmergencyExecutionCommittee_RevertOn_Stranger() external { vm.expectRevert( abi.encodeWithSelector(EmergencyProtection.CallerIsNotEmergencyExecutionCommittee.selector, address(0x5)) ); @@ -154,23 +188,23 @@ contract EmergencyProtectionTest is UnitTest { this.external__checkCallerIsEmergencyExecutionCommittee(); } - function test_CheckEmergencyMode() external { + function test_checkEmergencyMode_HappyPath() external { EmergencyProtection.activateEmergencyMode(ctx); EmergencyProtection.checkEmergencyMode(ctx, true); } - function test_CheckEmergencyMode_RevertOn_NotInEmergencyMode() external { + function test_checkEmergencyMode_RevertOn_NotInEmergencyMode() external { vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); EmergencyProtection.checkEmergencyMode(ctx, true); } - function test_IsEmergencyModeActive() public { + function test_isEmergencyModeActive_HappyPath() public { assertFalse(EmergencyProtection.isEmergencyModeActive(ctx)); EmergencyProtection.activateEmergencyMode(ctx); assertTrue(EmergencyProtection.isEmergencyModeActive(ctx)); } - function test_IsEmergencyModeDurationPassed() public { + function test_isEmergencyModeDurationPassed_HappyPath() public { assertFalse(EmergencyProtection.isEmergencyModeDurationPassed(ctx)); EmergencyProtection.activateEmergencyMode(ctx); assertFalse(EmergencyProtection.isEmergencyModeDurationPassed(ctx)); @@ -182,7 +216,7 @@ contract EmergencyProtectionTest is UnitTest { assertTrue(EmergencyProtection.isEmergencyModeDurationPassed(ctx)); } - function test_IsEmergencyProtectionEnabled() public { + function test_isEmergencyProtectionEnabled_HappyPath() public { assertTrue(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); Duration untilExpiration = @@ -192,7 +226,7 @@ contract EmergencyProtectionTest is UnitTest { assertFalse(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); } - function test_IsEmergencyProtectionEnabled_WhenEmergencyModeActive() public { + function test_isEmergencyProtectionEnabled_WhenEmergencyModeActive() public { assertTrue(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); EmergencyProtection.activateEmergencyMode(ctx); assertTrue(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); From 262d7668ec7b358f22857786ce3ab12667a9a806 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 3 Sep 2024 13:25:06 +0300 Subject: [PATCH 12/86] feat: add tests for timelock state --- test/unit/libraries/TimelockState.t.sol | 115 ++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 test/unit/libraries/TimelockState.t.sol diff --git a/test/unit/libraries/TimelockState.t.sol b/test/unit/libraries/TimelockState.t.sol new file mode 100644 index 00000000..68758e86 --- /dev/null +++ b/test/unit/libraries/TimelockState.t.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {TimelockState} from "contracts/libraries/TimelockState.sol"; +import {Duration, Durations} from "contracts/types/Duration.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; + +contract TimelockStateUnitTests is UnitTest { + using TimelockState for TimelockState.Context; + + TimelockState.Context internal _timelockState; + + address internal governance = makeAddr("governance"); + Duration internal afterSubmitDelay = Durations.from(1 days); + Duration internal afterScheduleDelay = Durations.from(2 days); + + Duration internal maxAfterSubmitDelay = Durations.from(10 days); + Duration internal maxAfterScheduleDelay = Durations.from(20 days); + + function setUp() external { + TimelockState.setGovernance(_timelockState, governance); + TimelockState.setAfterSubmitDelay(_timelockState, afterSubmitDelay, maxAfterSubmitDelay); + TimelockState.setAfterScheduleDelay(_timelockState, afterScheduleDelay, maxAfterScheduleDelay); + } + + function testFuzz_setGovernance_HappyPath(address newGovernance) external { + vm.assume(newGovernance != address(0) && newGovernance != governance); + + vm.expectEmit(); + emit TimelockState.GovernanceSet(newGovernance); + + TimelockState.setGovernance(_timelockState, newGovernance); + } + + function test_setGovernance_RevertOn_ZeroAddress() external { + vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidGovernance.selector, address(0))); + TimelockState.setGovernance(_timelockState, address(0)); + } + + function test_setGovernance_RevertOn_SameAddress() external { + vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidGovernance.selector, governance)); + TimelockState.setGovernance(_timelockState, governance); + } + + function testFuzz_setAfterSubmitDelay_HappyPath(Duration newAfterSubmitDelay) external { + vm.assume(newAfterSubmitDelay <= maxAfterSubmitDelay && newAfterSubmitDelay != afterSubmitDelay); + + vm.expectEmit(); + emit TimelockState.AfterSubmitDelaySet(newAfterSubmitDelay); + + TimelockState.setAfterSubmitDelay(_timelockState, newAfterSubmitDelay, maxAfterSubmitDelay); + } + + function test_setAfterSubmitDelay_RevertOn_GreaterThanMax() external { + vm.expectRevert( + abi.encodeWithSelector(TimelockState.InvalidAfterSubmitDelay.selector, maxAfterSubmitDelay.plusSeconds(1)) + ); + TimelockState.setAfterSubmitDelay(_timelockState, maxAfterSubmitDelay.plusSeconds(1), maxAfterSubmitDelay); + } + + function test_setAfterSubmitDelay_RevertOn_SameValue() external { + vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidAfterSubmitDelay.selector, afterSubmitDelay)); + TimelockState.setAfterSubmitDelay(_timelockState, afterSubmitDelay, maxAfterSubmitDelay); + } + + function testFuzz_setAfterScheduleDelay_HappyPath(Duration newAfterScheduleDelay) external { + vm.assume(newAfterScheduleDelay <= maxAfterScheduleDelay && newAfterScheduleDelay != afterScheduleDelay); + + vm.expectEmit(); + emit TimelockState.AfterScheduleDelaySet(newAfterScheduleDelay); + + TimelockState.setAfterScheduleDelay(_timelockState, newAfterScheduleDelay, maxAfterScheduleDelay); + } + + function test_setAfterScheduleDelay_RevertOn_GreaterThanMax() external { + vm.expectRevert( + abi.encodeWithSelector( + TimelockState.InvalidAfterScheduleDelay.selector, maxAfterScheduleDelay.plusSeconds(1) + ) + ); + TimelockState.setAfterScheduleDelay(_timelockState, maxAfterScheduleDelay.plusSeconds(1), maxAfterScheduleDelay); + } + + function test_setAfterScheduleDelay_RevertOn_SameValue() external { + vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidAfterScheduleDelay.selector, afterScheduleDelay)); + TimelockState.setAfterScheduleDelay(_timelockState, afterScheduleDelay, maxAfterScheduleDelay); + } + + function testFuzz_getAfterSubmitDelay_HappyPath(Duration newAfterSubmitDelay) external { + TimelockState.setAfterSubmitDelay(_timelockState, newAfterSubmitDelay, newAfterSubmitDelay); + assertEq(TimelockState.getAfterSubmitDelay(_timelockState), newAfterSubmitDelay); + } + + function testFuzz_getAfterScheduleDelay_HappyPath(Duration newAfterScheduleDelay) external { + TimelockState.setAfterScheduleDelay(_timelockState, newAfterScheduleDelay, newAfterScheduleDelay); + assertEq(TimelockState.getAfterScheduleDelay(_timelockState), newAfterScheduleDelay); + } + + function test_checkCallerIsGovernance_HappyPath() external { + vm.prank(governance); + this.external__checkCallerIsGovernance(); + } + + function testFuzz_checkCallerIsGovernance_RevertOn_NonGovernance(address caller) external { + vm.assume(caller != governance); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotGovernance.selector, caller)); + vm.prank(caller); + this.external__checkCallerIsGovernance(); + } + + function external__checkCallerIsGovernance() external { + TimelockState.checkCallerIsGovernance(_timelockState); + } +} From 08503670a8953b6edefb45fafb55f52a6a0b5774 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 3 Sep 2024 14:30:11 +0300 Subject: [PATCH 13/86] feat: add missing tests for withdrawalbatchesqueue --- .../libraries/WithdrawalBatchesQueue.t.sol | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/test/unit/libraries/WithdrawalBatchesQueue.t.sol b/test/unit/libraries/WithdrawalBatchesQueue.t.sol index e823f77d..01b5979e 100644 --- a/test/unit/libraries/WithdrawalBatchesQueue.t.sol +++ b/test/unit/libraries/WithdrawalBatchesQueue.t.sol @@ -285,6 +285,13 @@ contract WithdrawalsBatchesQueueTest is UnitTest { } } + function test_claimNextBatch_RevertOn_EmptyBatch() external { + _openBatchesQueue(); + + vm.expectRevert(WithdrawalsBatchesQueue.EmptyBatch.selector); + _batchesQueue.claimNextBatch(1); + } + // --- // close() // --- @@ -551,6 +558,61 @@ contract WithdrawalsBatchesQueueTest is UnitTest { _batchesQueue.getLastClaimedOrBoundaryUnstETHId(); } + // --- + // getTotalUnclaimedUnstETHIdsCount() + // --- + + function test_getTotalUnclaimedUnstETHIdsCount_HappyPath() external { + _openBatchesQueue(); + + uint256 firstBatchCount = 3; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory firstBatch = _generateFakeUnstETHIds({length: firstBatchCount, firstUnstETHId: firstUnstETHId}); + + _batchesQueue.addUnstETHIds(firstBatch); + + uint256 secondBatchCount = 2; + uint256[] memory secondBatch = + _generateFakeUnstETHIds({length: secondBatchCount, firstUnstETHId: firstUnstETHId + firstBatchCount}); + + _batchesQueue.addUnstETHIds(secondBatch); + + uint256 totalUnclaimed = _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + assertEq(totalUnclaimed, 5); + + uint256 claimLimit = 2; + _batchesQueue.claimNextBatch(claimLimit); + + totalUnclaimed = _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + assertEq(totalUnclaimed, 3); + + _batchesQueue.claimNextBatch(claimLimit); + _batchesQueue.claimNextBatch(claimLimit); + + totalUnclaimed = _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + assertEq(totalUnclaimed, 0); + } + + // --- + // isAllBatchesClaimed() + // --- + + function test_isAllBatchesClaimed_HappyPath() external { + _openBatchesQueue(); + + assertEq(_batchesQueue.isAllBatchesClaimed(), true); + + uint256 unstETHIdsCount = 5; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); + _batchesQueue.addUnstETHIds(unstETHIds); + + assertEq(_batchesQueue.isAllBatchesClaimed(), false); + + _batchesQueue.claimNextBatch(5); + assertEq(_batchesQueue.isAllBatchesClaimed(), true); + } + // --- // isClosed() // --- From d5054019e88a3fab19a24d47207d1d7398325542 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 3 Sep 2024 15:48:52 +0300 Subject: [PATCH 14/86] fix: reseal committee fix & unit tests --- contracts/committees/ResealCommittee.sol | 5 +- test/unit/committees/ResealCommittee.t.sol | 127 +++++++++++++++++++++ 2 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 test/unit/committees/ResealCommittee.t.sol diff --git a/contracts/committees/ResealCommittee.sol b/contracts/committees/ResealCommittee.sol index a6e4a99b..0c7b3a0e 100644 --- a/contracts/committees/ResealCommittee.sol +++ b/contracts/committees/ResealCommittee.sol @@ -46,14 +46,13 @@ contract ResealCommittee is HashConsensus, ProposalsList { /// @return support The number of votes in support of the proposal /// @return executionQuorum The required number of votes for execution /// @return quorumAt The timestamp when the quorum was reached - /// @return isExecuted Whether the proposal has been executed function getResealState(address sealable) public view - returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) + returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt) { (, bytes32 key) = _encodeResealProposal(sealable); - return _getHashState(key); + (support, executionQuorum, quorumAt,) = _getHashState(key); } /// @notice Executes an approved reseal proposal diff --git a/test/unit/committees/ResealCommittee.t.sol b/test/unit/committees/ResealCommittee.t.sol new file mode 100644 index 00000000..b990eb54 --- /dev/null +++ b/test/unit/committees/ResealCommittee.t.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; +import {Durations} from "contracts/types/Duration.sol"; +import {Timestamp} from "contracts/types/Timestamp.sol"; +import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; +import {HashConsensus} from "contracts/committees/HashConsensus.sol"; + +import {TargetMock} from "test/utils/target-mock.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; + +contract ResealCommitteeUnitTest is UnitTest { + ResealCommittee internal resealCommittee; + + uint256 internal quorum = 2; + address internal owner = makeAddr("owner"); + address[] internal committeeMembers = [address(0x1), address(0x2), address(0x3)]; + address internal sealable = makeAddr("sealable"); + address internal dualGovernance; + + function setUp() external { + dualGovernance = address(new TargetMock()); + resealCommittee = new ResealCommittee(owner, committeeMembers, quorum, dualGovernance, Durations.from(0)); + } + + function test_constructor_HappyPath() external { + ResealCommittee resealCommitteeLocal = + new ResealCommittee(owner, committeeMembers, quorum, dualGovernance, Durations.from(0)); + assertEq(resealCommitteeLocal.DUAL_GOVERNANCE(), dualGovernance); + } + + function test_voteReseal_HappyPath() external { + vm.prank(committeeMembers[0]); + resealCommittee.voteReseal(sealable, true); + + (uint256 partialSupport,,) = resealCommittee.getResealState(sealable); + assertEq(partialSupport, 1); + + vm.prank(committeeMembers[1]); + resealCommittee.voteReseal(sealable, true); + + (uint256 support, uint256 executionQuorum, Timestamp quorumAt) = resealCommittee.getResealState(sealable); + + assertEq(support, quorum); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); + } + + function testFuzz_voteReseal_RevertOn_NotMember(address caller) external { + vm.assume(caller != committeeMembers[0] && caller != committeeMembers[1] && caller != committeeMembers[2]); + vm.prank(caller); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.CallerIsNotMember.selector, caller)); + resealCommittee.voteReseal(sealable, true); + } + + function test_executeReseal_HappyPath() external { + vm.prank(committeeMembers[0]); + resealCommittee.voteReseal(sealable, true); + vm.prank(committeeMembers[1]); + resealCommittee.voteReseal(sealable, true); + + vm.prank(committeeMembers[2]); + vm.expectCall(dualGovernance, abi.encodeWithSelector(IDualGovernance.resealSealable.selector, sealable)); + resealCommittee.executeReseal(sealable); + + (uint256 support, uint256 executionQuorum, Timestamp quorumAt) = resealCommittee.getResealState(sealable); + assertEq(support, 0); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(0)); + } + + function test_executeReseal_RevertOn_QuorumNotReached() external { + vm.prank(committeeMembers[0]); + resealCommittee.voteReseal(sealable, true); + + vm.prank(committeeMembers[2]); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.QuorumIsNotReached.selector)); + resealCommittee.executeReseal(sealable); + } + + function test_getResealState_HappyPath() external { + (uint256 support, uint256 executionQuorum, Timestamp quorumAt) = resealCommittee.getResealState(sealable); + assertEq(support, 0); + assertEq(executionQuorum, 2); + assertEq(quorumAt, Timestamp.wrap(0)); + + vm.prank(committeeMembers[0]); + resealCommittee.voteReseal(sealable, true); + + (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); + assertEq(support, 1); + assertEq(executionQuorum, 2); + assertEq(quorumAt, Timestamp.wrap(0)); + + vm.prank(committeeMembers[1]); + resealCommittee.voteReseal(sealable, true); + + (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); + Timestamp quorumAtExpected = Timestamp.wrap(uint40(block.timestamp)); + assertEq(support, 2); + assertEq(executionQuorum, 2); + assertEq(quorumAt, quorumAtExpected); + + _wait(Durations.from(1)); + + vm.prank(committeeMembers[1]); + resealCommittee.voteReseal(sealable, false); + + (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); + assertEq(support, 1); + assertEq(executionQuorum, 2); + assertEq(quorumAt, quorumAtExpected); + + vm.prank(committeeMembers[1]); + resealCommittee.voteReseal(sealable, true); + + vm.prank(committeeMembers[2]); + vm.expectCall(dualGovernance, abi.encodeWithSelector(IDualGovernance.resealSealable.selector, sealable)); + resealCommittee.executeReseal(sealable); + + (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); + assertEq(support, 0); + assertEq(executionQuorum, 2); + assertEq(quorumAt, Timestamp.wrap(0)); + } +} From c6e74dee9def8c4374797f60822d79d8835b55cd Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 3 Sep 2024 16:14:38 +0300 Subject: [PATCH 15/86] feat: add tests for emergency activation committee --- .../EmergencyActivationCommittee.t.sol | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 test/unit/committees/EmergencyActivationCommittee.t.sol diff --git a/test/unit/committees/EmergencyActivationCommittee.t.sol b/test/unit/committees/EmergencyActivationCommittee.t.sol new file mode 100644 index 00000000..c6e48de4 --- /dev/null +++ b/test/unit/committees/EmergencyActivationCommittee.t.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; +import {HashConsensus} from "contracts/committees/HashConsensus.sol"; +import {Durations} from "contracts/types/Duration.sol"; +import {Timestamp} from "contracts/types/Timestamp.sol"; +import {ITimelock} from "contracts/interfaces/ITimelock.sol"; + +import {TargetMock} from "test/utils/target-mock.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; + +contract EmergencyActivationCommitteeUnitTest is UnitTest { + EmergencyActivationCommittee internal emergencyActivationCommittee; + uint256 internal quorum = 2; + address internal owner = makeAddr("owner"); + address[] internal committeeMembers = [address(0x1), address(0x2), address(0x3)]; + address internal emergencyProtectedTimelock; + + function setUp() external { + emergencyProtectedTimelock = address(new TargetMock()); + emergencyActivationCommittee = + new EmergencyActivationCommittee(owner, committeeMembers, quorum, emergencyProtectedTimelock); + } + + function test_constructor_HappyPath() external { + EmergencyActivationCommittee localCommittee = + new EmergencyActivationCommittee(owner, committeeMembers, quorum, emergencyProtectedTimelock); + assertEq(localCommittee.EMERGENCY_PROTECTED_TIMELOCK(), emergencyProtectedTimelock); + } + + function test_approveActivateEmergencyMode_HappyPath() external { + vm.prank(committeeMembers[0]); + emergencyActivationCommittee.approveActivateEmergencyMode(); + + (uint256 partialSupport,,,) = emergencyActivationCommittee.getActivateEmergencyModeState(); + assertEq(partialSupport, 1); + + vm.prank(committeeMembers[1]); + emergencyActivationCommittee.approveActivateEmergencyMode(); + + (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = + emergencyActivationCommittee.getActivateEmergencyModeState(); + assertEq(support, quorum); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); + assertEq(isExecuted, false); + } + + function testFuzz_approveActivateEmergencyMode_RevertOn_NotMember(address caller) external { + vm.assume(caller != committeeMembers[0] && caller != committeeMembers[1] && caller != committeeMembers[2]); + vm.prank(caller); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.CallerIsNotMember.selector, caller)); + emergencyActivationCommittee.approveActivateEmergencyMode(); + } + + function test_executeActivateEmergencyMode_HappyPath() external { + vm.prank(committeeMembers[0]); + emergencyActivationCommittee.approveActivateEmergencyMode(); + vm.prank(committeeMembers[1]); + emergencyActivationCommittee.approveActivateEmergencyMode(); + + vm.prank(committeeMembers[2]); + vm.expectCall(emergencyProtectedTimelock, abi.encodeWithSelector(ITimelock.activateEmergencyMode.selector)); + emergencyActivationCommittee.executeActivateEmergencyMode(); + + (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = + emergencyActivationCommittee.getActivateEmergencyModeState(); + assertEq(support, 2); + assertEq(executionQuorum, 2); + assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); + assertEq(isExecuted, true); + } + + function test_executeActivateEmergencyMode_RevertOn_QuorumNotReached() external { + vm.prank(committeeMembers[0]); + emergencyActivationCommittee.approveActivateEmergencyMode(); + + vm.prank(committeeMembers[2]); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.QuorumIsNotReached.selector)); + emergencyActivationCommittee.executeActivateEmergencyMode(); + } + + function test_getActivateEmergencyModeState_HappyPath() external { + (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = + emergencyActivationCommittee.getActivateEmergencyModeState(); + assertEq(support, 0); + assertEq(executionQuorum, 2); + assertEq(quorumAt, Timestamp.wrap(0)); + + vm.prank(committeeMembers[0]); + emergencyActivationCommittee.approveActivateEmergencyMode(); + + (support, executionQuorum, quorumAt, isExecuted) = emergencyActivationCommittee.getActivateEmergencyModeState(); + assertEq(support, 1); + assertEq(executionQuorum, 2); + assertEq(quorumAt, Timestamp.wrap(0)); + assertEq(isExecuted, false); + + vm.prank(committeeMembers[1]); + emergencyActivationCommittee.approveActivateEmergencyMode(); + + (support, executionQuorum, quorumAt, isExecuted) = emergencyActivationCommittee.getActivateEmergencyModeState(); + Timestamp quorumAtExpected = Timestamp.wrap(uint40(block.timestamp)); + assertEq(support, 2); + assertEq(executionQuorum, 2); + assertEq(quorumAt, quorumAtExpected); + assertEq(isExecuted, false); + + vm.prank(committeeMembers[2]); + vm.expectCall(emergencyProtectedTimelock, abi.encodeWithSelector(ITimelock.activateEmergencyMode.selector)); + emergencyActivationCommittee.executeActivateEmergencyMode(); + + (support, executionQuorum, quorumAt, isExecuted) = emergencyActivationCommittee.getActivateEmergencyModeState(); + assertEq(support, 2); + assertEq(executionQuorum, 2); + assertEq(quorumAt, quorumAtExpected); + assertEq(isExecuted, true); + } +} From 309570c5a6149483f9e56a52d74993ae471c2292 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 3 Sep 2024 16:25:57 +0300 Subject: [PATCH 16/86] feat: add tests for emergency execution committee --- .../EmergencyExecutionCommittee.t.sol | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 test/unit/committees/EmergencyExecutionCommittee.t.sol diff --git a/test/unit/committees/EmergencyExecutionCommittee.t.sol b/test/unit/committees/EmergencyExecutionCommittee.t.sol new file mode 100644 index 00000000..e111db80 --- /dev/null +++ b/test/unit/committees/EmergencyExecutionCommittee.t.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; +import {HashConsensus} from "contracts/committees/HashConsensus.sol"; +import {Durations} from "contracts/types/Duration.sol"; +import {Timestamp} from "contracts/types/Timestamp.sol"; +import {ITimelock} from "contracts/interfaces/ITimelock.sol"; + +import {TargetMock} from "test/utils/target-mock.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; + +contract EmergencyExecutionCommitteeUnitTest is UnitTest { + EmergencyExecutionCommittee internal emergencyExecutionCommittee; + uint256 internal quorum = 2; + address internal owner = makeAddr("owner"); + address[] internal committeeMembers = [address(0x1), address(0x2), address(0x3)]; + address internal emergencyProtectedTimelock; + uint256 internal proposalId = 1; + + function setUp() external { + emergencyProtectedTimelock = address(new TargetMock()); + emergencyExecutionCommittee = + new EmergencyExecutionCommittee(owner, committeeMembers, quorum, emergencyProtectedTimelock); + } + + function test_constructor_HappyPath() external { + EmergencyExecutionCommittee localCommittee = + new EmergencyExecutionCommittee(owner, committeeMembers, quorum, emergencyProtectedTimelock); + assertEq(localCommittee.EMERGENCY_PROTECTED_TIMELOCK(), emergencyProtectedTimelock); + } + + function test_voteEmergencyExecute_HappyPath() external { + vm.prank(committeeMembers[0]); + emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); + + (uint256 partialSupport,,,) = emergencyExecutionCommittee.getEmergencyExecuteState(proposalId); + assertEq(partialSupport, 1); + + vm.prank(committeeMembers[1]); + emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); + + (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = + emergencyExecutionCommittee.getEmergencyExecuteState(proposalId); + assertEq(support, quorum); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); + assertFalse(isExecuted); + } + + function testFuzz_voteEmergencyExecute_RevertOn_NotMember(address caller) external { + vm.assume(caller != committeeMembers[0] && caller != committeeMembers[1] && caller != committeeMembers[2]); + vm.prank(caller); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.CallerIsNotMember.selector, caller)); + emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); + } + + function test_executeEmergencyExecute_HappyPath() external { + vm.prank(committeeMembers[0]); + emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); + vm.prank(committeeMembers[1]); + emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); + + vm.prank(committeeMembers[2]); + vm.expectCall( + emergencyProtectedTimelock, abi.encodeWithSelector(ITimelock.emergencyExecute.selector, proposalId) + ); + emergencyExecutionCommittee.executeEmergencyExecute(proposalId); + + (,,, bool isExecuted) = emergencyExecutionCommittee.getEmergencyExecuteState(proposalId); + assertTrue(isExecuted); + } + + function test_executeEmergencyExecute_RevertOn_QuorumNotReached() external { + vm.prank(committeeMembers[0]); + emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); + + vm.prank(committeeMembers[2]); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.QuorumIsNotReached.selector)); + emergencyExecutionCommittee.executeEmergencyExecute(proposalId); + } + + function test_getEmergencyExecuteState_HappyPath() external { + (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = + emergencyExecutionCommittee.getEmergencyExecuteState(proposalId); + assertEq(support, 0); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(0)); + assertFalse(isExecuted); + + vm.prank(committeeMembers[0]); + emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); + (support, executionQuorum, quorumAt, isExecuted) = + emergencyExecutionCommittee.getEmergencyExecuteState(proposalId); + assertEq(support, 1); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(0)); + assertFalse(isExecuted); + + vm.prank(committeeMembers[1]); + emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); + (support, executionQuorum, quorumAt, isExecuted) = + emergencyExecutionCommittee.getEmergencyExecuteState(proposalId); + assertEq(support, 2); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); + assertFalse(isExecuted); + + vm.prank(committeeMembers[2]); + emergencyExecutionCommittee.executeEmergencyExecute(proposalId); + (support, executionQuorum, quorumAt, isExecuted) = + emergencyExecutionCommittee.getEmergencyExecuteState(proposalId); + assertEq(support, 2); + assertTrue(isExecuted); + } + + function test_approveEmergencyReset_HappyPath() external { + vm.prank(committeeMembers[0]); + emergencyExecutionCommittee.approveEmergencyReset(); + + (uint256 partialSupport,,,) = emergencyExecutionCommittee.getEmergencyResetState(); + assertEq(partialSupport, 1); + + vm.prank(committeeMembers[1]); + emergencyExecutionCommittee.approveEmergencyReset(); + + (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = + emergencyExecutionCommittee.getEmergencyResetState(); + assertEq(support, quorum); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); + assertFalse(isExecuted); + } + + function test_executeEmergencyReset_HappyPath() external { + vm.prank(committeeMembers[0]); + emergencyExecutionCommittee.approveEmergencyReset(); + vm.prank(committeeMembers[1]); + emergencyExecutionCommittee.approveEmergencyReset(); + + vm.prank(committeeMembers[2]); + vm.expectCall(emergencyProtectedTimelock, abi.encodeWithSelector(ITimelock.emergencyReset.selector)); + emergencyExecutionCommittee.executeEmergencyReset(); + + (,,, bool isExecuted) = emergencyExecutionCommittee.getEmergencyResetState(); + assertTrue(isExecuted); + } + + function test_executeEmergencyReset_RevertOn_QuorumNotReached() external { + vm.prank(committeeMembers[0]); + emergencyExecutionCommittee.approveEmergencyReset(); + + vm.prank(committeeMembers[2]); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.QuorumIsNotReached.selector)); + emergencyExecutionCommittee.executeEmergencyReset(); + } + + function test_getEmergencyResetState_HappyPath() external { + (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = + emergencyExecutionCommittee.getEmergencyResetState(); + assertEq(support, 0); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(0)); + assertFalse(isExecuted); + + vm.prank(committeeMembers[0]); + emergencyExecutionCommittee.approveEmergencyReset(); + (support, executionQuorum, quorumAt, isExecuted) = emergencyExecutionCommittee.getEmergencyResetState(); + assertEq(support, 1); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(0)); + assertFalse(isExecuted); + + vm.prank(committeeMembers[1]); + emergencyExecutionCommittee.approveEmergencyReset(); + (support, executionQuorum, quorumAt, isExecuted) = emergencyExecutionCommittee.getEmergencyResetState(); + assertEq(support, 2); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); + assertFalse(isExecuted); + + vm.prank(committeeMembers[2]); + emergencyExecutionCommittee.executeEmergencyReset(); + (support, executionQuorum, quorumAt, isExecuted) = emergencyExecutionCommittee.getEmergencyResetState(); + assertEq(support, 2); + assertTrue(isExecuted); + } +} From 4a14ac2286fc752dd2ccca423a7dd16c0963b7fc Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 3 Sep 2024 16:43:20 +0300 Subject: [PATCH 17/86] feat: add tests for ProposalsList --- test/unit/committees/ProposalsList.t.sol | 89 ++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 test/unit/committees/ProposalsList.t.sol diff --git a/test/unit/committees/ProposalsList.t.sol b/test/unit/committees/ProposalsList.t.sol new file mode 100644 index 00000000..738539b0 --- /dev/null +++ b/test/unit/committees/ProposalsList.t.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ProposalsList} from "contracts/committees/ProposalsList.sol"; +import {Proposal} from "contracts/libraries/EnumerableProposals.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; + +contract ProposalsListUnitTest is UnitTest, ProposalsList { + ProposalsListWrapper internal proposalsList; + + bytes32 internal proposalKey1 = keccak256(abi.encodePacked("proposal1")); + bytes32 internal proposalKey2 = keccak256(abi.encodePacked("proposal2")); + bytes32 internal proposalKey3 = keccak256(abi.encodePacked("proposal3")); + uint256 internal proposalType1 = 1; + uint256 internal proposalType2 = 2; + bytes internal proposalData1 = abi.encodePacked("data1"); + bytes internal proposalData2 = abi.encodePacked("data2"); + bytes internal proposalData3 = abi.encodePacked("data3"); + + function setUp() public { + proposalsList = new ProposalsListWrapper(); + + proposalsList.pushProposal(proposalKey1, proposalType1, proposalData1); + proposalsList.pushProposal(proposalKey2, proposalType2, proposalData2); + proposalsList.pushProposal(proposalKey3, proposalType1, proposalData3); + } + + function test_getProposalsLength_HappyPath() external { + uint256 length = proposalsList.getProposalsLength(); + assertEq(length, 3); + } + + function test_getProposal_HappyPath() external { + Proposal memory proposal = proposalsList.getProposal(proposalKey1); + assertEq(proposal.proposalType, proposalType1); + assertEq(proposal.data, proposalData1); + } + + function test_getProposals_HappyPath() external { + Proposal[] memory proposals = proposalsList.getProposals(0, 2); + assertEq(proposals.length, 2); + assertEq(proposals[0].proposalType, proposalType1); + assertEq(proposals[0].data, proposalData1); + assertEq(proposals[1].proposalType, proposalType2); + assertEq(proposals[1].data, proposalData2); + } + + function test_getProposalAt_HappyPath() external { + Proposal memory proposal = proposalsList.getProposalAt(1); + assertEq(proposal.proposalType, proposalType2); + assertEq(proposal.data, proposalData2); + } + + function test_getOrderedKeys_HappyPath() external { + bytes32[] memory keys = proposalsList.getOrderedKeys(0, 3); + assertEq(keys.length, 3); + assertEq(keys[0], proposalKey1); + assertEq(keys[1], proposalKey2); + assertEq(keys[2], proposalKey3); + } + + function test_getProposals_Pagination() external { + Proposal[] memory proposals = proposalsList.getProposals(1, 2); + assertEq(proposals.length, 2); + assertEq(proposals[0].proposalType, proposalType2); + assertEq(proposals[0].data, proposalData2); + assertEq(proposals[1].proposalType, proposalType1); + assertEq(proposals[1].data, proposalData3); + } + + function test_pushProposal_HappyPath() external { + bytes32 proposalKey4 = keccak256(abi.encodePacked("proposal4")); + uint256 proposalType4 = 4; + bytes memory proposalData4 = abi.encodePacked("data4"); + + proposalsList.pushProposal(proposalKey4, proposalType4, proposalData4); + + Proposal memory proposal = proposalsList.getProposal(proposalKey4); + assertEq(proposal.proposalType, proposalType4); + assertEq(proposal.data, proposalData4); + } +} + +contract ProposalsListWrapper is ProposalsList { + function pushProposal(bytes32 key, uint256 proposalType, bytes memory data) public { + _pushProposal(key, proposalType, data); + } +} From c552a249b3abaac5ce49e3dcc4f1aea06c57141a Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 3 Sep 2024 17:05:15 +0300 Subject: [PATCH 18/86] feat: tiebreaker core unit tests --- test/unit/committees/TiebreakerCore.t.sol | 168 ++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 test/unit/committees/TiebreakerCore.t.sol diff --git a/test/unit/committees/TiebreakerCore.t.sol b/test/unit/committees/TiebreakerCore.t.sol new file mode 100644 index 00000000..c4142716 --- /dev/null +++ b/test/unit/committees/TiebreakerCore.t.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; +import {HashConsensus} from "contracts/committees/HashConsensus.sol"; +import {Durations, Duration} from "contracts/types/Duration.sol"; +import {Timestamp} from "contracts/types/Timestamp.sol"; +import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; + +import {TargetMock} from "test/utils/target-mock.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; + +contract TiebreakerCoreUnitTest is UnitTest { + TiebreakerCore internal tiebreakerCore; + uint256 internal quorum = 2; + address internal owner = makeAddr("owner"); + address[] internal committeeMembers = [address(0x1), address(0x2), address(0x3)]; + address internal dualGovernance; + uint256 internal proposalId = 1; + address internal sealable = makeAddr("sealable"); + Duration internal timelock = Durations.from(1 days); + + function setUp() external { + dualGovernance = address(new TargetMock()); + tiebreakerCore = new TiebreakerCore(owner, dualGovernance, timelock); + + vm.prank(owner); + tiebreakerCore.addMembers(committeeMembers, quorum); + } + + function testFuzz_constructor_HappyPath(address _owner, address _dualGovernance, Duration _timelock) external { + new TiebreakerCore(_owner, _dualGovernance, _timelock); + } + + function test_scheduleProposal_HappyPath() external { + vm.prank(committeeMembers[0]); + tiebreakerCore.scheduleProposal(proposalId); + + (uint256 partialSupport,,,) = tiebreakerCore.getScheduleProposalState(proposalId); + assertEq(partialSupport, 1); + + vm.prank(committeeMembers[1]); + tiebreakerCore.scheduleProposal(proposalId); + + (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = + tiebreakerCore.getScheduleProposalState(proposalId); + assertEq(support, quorum); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); + assertFalse(isExecuted); + } + + function testFuzz_scheduleProposal_RevertOn_NotMember(address caller) external { + vm.assume(caller != committeeMembers[0] && caller != committeeMembers[1] && caller != committeeMembers[2]); + + vm.prank(caller); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.CallerIsNotMember.selector, caller)); + tiebreakerCore.scheduleProposal(proposalId); + } + + function test_executeScheduleProposal_HappyPath() external { + vm.prank(committeeMembers[0]); + tiebreakerCore.scheduleProposal(proposalId); + vm.prank(committeeMembers[1]); + tiebreakerCore.scheduleProposal(proposalId); + + _wait(timelock); + + vm.prank(committeeMembers[2]); + vm.expectCall( + dualGovernance, abi.encodeWithSelector(IDualGovernance.tiebreakerScheduleProposal.selector, proposalId) + ); + tiebreakerCore.executeScheduleProposal(proposalId); + + (,,, bool isExecuted) = tiebreakerCore.getScheduleProposalState(proposalId); + assertTrue(isExecuted); + } + + function test_sealableResume_HappyPath() external { + uint256 nonce = tiebreakerCore.getSealableResumeNonce(sealable); + + vm.prank(committeeMembers[0]); + tiebreakerCore.sealableResume(sealable, nonce); + + (uint256 partialSupport,,,) = tiebreakerCore.getSealableResumeState(sealable, nonce); + assertEq(partialSupport, 1); + + vm.prank(committeeMembers[1]); + tiebreakerCore.sealableResume(sealable, nonce); + + (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = + tiebreakerCore.getSealableResumeState(sealable, nonce); + assertEq(support, quorum); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); + assertFalse(isExecuted); + } + + function test_sealableResume_RevertOn_NonceMismatch() external { + uint256 wrongNonce = 999; + + vm.prank(committeeMembers[0]); + vm.expectRevert(abi.encodeWithSelector(TiebreakerCore.ResumeSealableNonceMismatch.selector)); + tiebreakerCore.sealableResume(sealable, wrongNonce); + } + + function test_executeSealableResume_HappyPath() external { + uint256 nonce = tiebreakerCore.getSealableResumeNonce(sealable); + + vm.prank(committeeMembers[0]); + tiebreakerCore.sealableResume(sealable, nonce); + vm.prank(committeeMembers[1]); + tiebreakerCore.sealableResume(sealable, nonce); + + _wait(timelock); + + vm.prank(committeeMembers[2]); + vm.expectCall( + dualGovernance, abi.encodeWithSelector(IDualGovernance.tiebreakerResumeSealable.selector, sealable) + ); + tiebreakerCore.executeSealableResume(sealable); + + (,,, bool isExecuted) = tiebreakerCore.getSealableResumeState(sealable, nonce); + assertTrue(isExecuted); + + uint256 newNonce = tiebreakerCore.getSealableResumeNonce(sealable); + assertEq(newNonce, nonce + 1); + } + + function test_getScheduleProposalState_HappyPath() external { + (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = + tiebreakerCore.getScheduleProposalState(proposalId); + assertEq(support, 0); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(0)); + assertFalse(isExecuted); + + vm.prank(committeeMembers[0]); + tiebreakerCore.scheduleProposal(proposalId); + + (support, executionQuorum, quorumAt, isExecuted) = tiebreakerCore.getScheduleProposalState(proposalId); + assertEq(support, 1); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(0)); + assertFalse(isExecuted); + + vm.prank(committeeMembers[1]); + tiebreakerCore.scheduleProposal(proposalId); + + (support, executionQuorum, quorumAt, isExecuted) = tiebreakerCore.getScheduleProposalState(proposalId); + Timestamp quorumAtExpected = Timestamp.wrap(uint40(block.timestamp)); + assertEq(support, quorum); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, quorumAtExpected); + assertFalse(isExecuted); + + _wait(timelock); + + vm.prank(committeeMembers[2]); + tiebreakerCore.executeScheduleProposal(proposalId); + + (support, executionQuorum, quorumAt, isExecuted) = tiebreakerCore.getScheduleProposalState(proposalId); + assertEq(support, quorum); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, quorumAtExpected); + assertTrue(isExecuted); + } +} From 4b4511a582e63a8a38417e20cc9cdaa194861997 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 3 Sep 2024 17:10:12 +0300 Subject: [PATCH 19/86] fix: small test fixes --- .../committees/EmergencyActivationCommittee.t.sol | 11 ++++++++--- .../unit/committees/EmergencyExecutionCommittee.t.sol | 11 ++++++++--- test/unit/{ => committees}/HashConsensus.t.sol | 6 +++--- 3 files changed, 19 insertions(+), 9 deletions(-) rename test/unit/{ => committees}/HashConsensus.t.sol (99%) diff --git a/test/unit/committees/EmergencyActivationCommittee.t.sol b/test/unit/committees/EmergencyActivationCommittee.t.sol index c6e48de4..e9b5247c 100644 --- a/test/unit/committees/EmergencyActivationCommittee.t.sol +++ b/test/unit/committees/EmergencyActivationCommittee.t.sol @@ -23,10 +23,15 @@ contract EmergencyActivationCommitteeUnitTest is UnitTest { new EmergencyActivationCommittee(owner, committeeMembers, quorum, emergencyProtectedTimelock); } - function test_constructor_HappyPath() external { + function testFuzz_constructor_HappyPath( + address _owner, + uint256 _quorum, + address _emergencyProtectedTimelock + ) external { + vm.assume(_quorum > 0 && _quorum <= committeeMembers.length); EmergencyActivationCommittee localCommittee = - new EmergencyActivationCommittee(owner, committeeMembers, quorum, emergencyProtectedTimelock); - assertEq(localCommittee.EMERGENCY_PROTECTED_TIMELOCK(), emergencyProtectedTimelock); + new EmergencyActivationCommittee(_owner, committeeMembers, _quorum, _emergencyProtectedTimelock); + assertEq(localCommittee.EMERGENCY_PROTECTED_TIMELOCK(), _emergencyProtectedTimelock); } function test_approveActivateEmergencyMode_HappyPath() external { diff --git a/test/unit/committees/EmergencyExecutionCommittee.t.sol b/test/unit/committees/EmergencyExecutionCommittee.t.sol index e111db80..6691b35b 100644 --- a/test/unit/committees/EmergencyExecutionCommittee.t.sol +++ b/test/unit/committees/EmergencyExecutionCommittee.t.sol @@ -24,10 +24,15 @@ contract EmergencyExecutionCommitteeUnitTest is UnitTest { new EmergencyExecutionCommittee(owner, committeeMembers, quorum, emergencyProtectedTimelock); } - function test_constructor_HappyPath() external { + function testFuzz_constructor_HappyPath( + address _owner, + uint256 _quorum, + address _emergencyProtectedTimelock + ) external { + vm.assume(_quorum > 0 && _quorum <= committeeMembers.length); EmergencyExecutionCommittee localCommittee = - new EmergencyExecutionCommittee(owner, committeeMembers, quorum, emergencyProtectedTimelock); - assertEq(localCommittee.EMERGENCY_PROTECTED_TIMELOCK(), emergencyProtectedTimelock); + new EmergencyExecutionCommittee(_owner, committeeMembers, _quorum, _emergencyProtectedTimelock); + assertEq(localCommittee.EMERGENCY_PROTECTED_TIMELOCK(), _emergencyProtectedTimelock); } function test_voteEmergencyExecute_HappyPath() external { diff --git a/test/unit/HashConsensus.t.sol b/test/unit/committees/HashConsensus.t.sol similarity index 99% rename from test/unit/HashConsensus.t.sol rename to test/unit/committees/HashConsensus.t.sol index 987c8624..8ec274a8 100644 --- a/test/unit/HashConsensus.t.sol +++ b/test/unit/committees/HashConsensus.t.sol @@ -6,9 +6,9 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Vm} from "forge-std/Test.sol"; -import {HashConsensus} from "../../contracts/committees/HashConsensus.sol"; -import {Duration, Durations} from "../../contracts/types/Duration.sol"; -import {Timestamp, Timestamps} from "../../contracts/types/Timestamp.sol"; +import {HashConsensus} from "contracts/committees/HashConsensus.sol"; +import {Duration, Durations} from "contracts/types/Duration.sol"; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; contract HashConsensusInstance is HashConsensus { constructor( From ef3852a36f0b308530e6f4033db910f99dea4f25 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 3 Sep 2024 18:02:49 +0300 Subject: [PATCH 20/86] feat: add tiebreaker sub committee tests --- .../committees/TiebreakerSubCommittee.t.sol | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 test/unit/committees/TiebreakerSubCommittee.t.sol diff --git a/test/unit/committees/TiebreakerSubCommittee.t.sol b/test/unit/committees/TiebreakerSubCommittee.t.sol new file mode 100644 index 00000000..674cb609 --- /dev/null +++ b/test/unit/committees/TiebreakerSubCommittee.t.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; +import {HashConsensus} from "contracts/committees/HashConsensus.sol"; +import {Durations} from "contracts/types/Duration.sol"; +import {Timestamp} from "contracts/types/Timestamp.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; +import {ITiebreakerCore} from "contracts/interfaces/ITiebreaker.sol"; + +import {TargetMock} from "test/utils/target-mock.sol"; + +contract TiebreakerSubCommitteeUnitTest is UnitTest { + TiebreakerSubCommittee internal tiebreakerSubCommittee; + uint256 internal quorum = 2; + address internal owner = makeAddr("owner"); + address[] internal committeeMembers = [address(0x1), address(0x2), address(0x3)]; + address internal tiebreakerCore; + uint256 internal proposalId = 1; + address internal sealable = makeAddr("sealable"); + + function setUp() external { + tiebreakerCore = address(new TargetMock()); + tiebreakerSubCommittee = new TiebreakerSubCommittee(owner, committeeMembers, quorum, tiebreakerCore); + } + + function test_constructor_HappyPath(address _owner, uint256 _quorum, address _tiebreakerCore) external { + vm.assume(_quorum > 0 && _quorum <= committeeMembers.length); + new TiebreakerSubCommittee(_owner, committeeMembers, _quorum, _tiebreakerCore); + } + + function test_scheduleProposal_HappyPath() external { + vm.prank(committeeMembers[0]); + tiebreakerSubCommittee.scheduleProposal(proposalId); + + (uint256 partialSupport,,,) = tiebreakerSubCommittee.getScheduleProposalState(proposalId); + assertEq(partialSupport, 1); + + vm.prank(committeeMembers[1]); + tiebreakerSubCommittee.scheduleProposal(proposalId); + + (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = + tiebreakerSubCommittee.getScheduleProposalState(proposalId); + assertEq(support, quorum); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); + assertFalse(isExecuted); + } + + function testFuzz_scheduleProposal_RevertOn_NotMember(address caller) external { + vm.assume(caller != committeeMembers[0] && caller != committeeMembers[1] && caller != committeeMembers[2]); + + vm.prank(caller); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.CallerIsNotMember.selector, caller)); + tiebreakerSubCommittee.scheduleProposal(proposalId); + } + + function test_executeScheduleProposal_HappyPath() external { + vm.prank(committeeMembers[0]); + tiebreakerSubCommittee.scheduleProposal(proposalId); + vm.prank(committeeMembers[1]); + tiebreakerSubCommittee.scheduleProposal(proposalId); + + vm.prank(committeeMembers[2]); + vm.expectCall(tiebreakerCore, abi.encodeWithSelector(ITiebreakerCore.scheduleProposal.selector, proposalId)); + tiebreakerSubCommittee.executeScheduleProposal(proposalId); + + (,,, bool isExecuted) = tiebreakerSubCommittee.getScheduleProposalState(proposalId); + assertTrue(isExecuted); + } + + function test_executeScheduleProposal_RevertOn_QuorumNotReached() external { + vm.prank(committeeMembers[0]); + tiebreakerSubCommittee.scheduleProposal(proposalId); + + vm.prank(committeeMembers[2]); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.QuorumIsNotReached.selector)); + tiebreakerSubCommittee.executeScheduleProposal(proposalId); + } + + function test_sealableResume_HappyPath() external { + vm.mockCall( + tiebreakerCore, + abi.encodeWithSelector(ITiebreakerCore.getSealableResumeNonce.selector, sealable), + abi.encode(0) + ); + + vm.prank(committeeMembers[0]); + tiebreakerSubCommittee.sealableResume(sealable); + + (uint256 partialSupport,,,) = tiebreakerSubCommittee.getSealableResumeState(sealable); + assertEq(partialSupport, 1); + + vm.prank(committeeMembers[1]); + tiebreakerSubCommittee.sealableResume(sealable); + + (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = + tiebreakerSubCommittee.getSealableResumeState(sealable); + assertEq(support, quorum); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); + assertFalse(isExecuted); + } + + function testFuzz_sealableResume_RevertOn_NotMember(address caller) external { + vm.assume(caller != committeeMembers[0] && caller != committeeMembers[1] && caller != committeeMembers[2]); + + vm.prank(caller); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.CallerIsNotMember.selector, caller)); + tiebreakerSubCommittee.sealableResume(sealable); + } + + function test_executeSealableResume_HappyPath() external { + vm.mockCall( + tiebreakerCore, + abi.encodeWithSelector(ITiebreakerCore.getSealableResumeNonce.selector, sealable), + abi.encode(0) + ); + + vm.prank(committeeMembers[0]); + tiebreakerSubCommittee.sealableResume(sealable); + vm.prank(committeeMembers[1]); + tiebreakerSubCommittee.sealableResume(sealable); + + vm.prank(committeeMembers[2]); + vm.expectCall(tiebreakerCore, abi.encodeWithSelector(ITiebreakerCore.sealableResume.selector, sealable, 0)); + tiebreakerSubCommittee.executeSealableResume(sealable); + + (,,, bool isExecuted) = tiebreakerSubCommittee.getSealableResumeState(sealable); + assertTrue(isExecuted); + } + + function test_executeSealableResume_RevertOn_QuorumNotReached() external { + vm.mockCall( + tiebreakerCore, + abi.encodeWithSelector(ITiebreakerCore.getSealableResumeNonce.selector, sealable), + abi.encode(0) + ); + + vm.prank(committeeMembers[0]); + tiebreakerSubCommittee.sealableResume(sealable); + + vm.prank(committeeMembers[2]); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.QuorumIsNotReached.selector)); + tiebreakerSubCommittee.executeSealableResume(sealable); + } + + function test_getScheduleProposalState_HappyPath() external { + (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = + tiebreakerSubCommittee.getScheduleProposalState(proposalId); + assertEq(support, 0); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(0)); + assertFalse(isExecuted); + + vm.prank(committeeMembers[0]); + tiebreakerSubCommittee.scheduleProposal(proposalId); + + (support, executionQuorum, quorumAt, isExecuted) = tiebreakerSubCommittee.getScheduleProposalState(proposalId); + assertEq(support, 1); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(0)); + assertFalse(isExecuted); + + vm.prank(committeeMembers[1]); + tiebreakerSubCommittee.scheduleProposal(proposalId); + + (support, executionQuorum, quorumAt, isExecuted) = tiebreakerSubCommittee.getScheduleProposalState(proposalId); + assertEq(support, quorum); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); + assertFalse(isExecuted); + + vm.prank(committeeMembers[2]); + tiebreakerSubCommittee.executeScheduleProposal(proposalId); + + (support, executionQuorum, quorumAt, isExecuted) = tiebreakerSubCommittee.getScheduleProposalState(proposalId); + assertEq(support, quorum); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); + assertTrue(isExecuted); + } + + function test_getSealableResumeState_HappyPath() external { + vm.mockCall( + tiebreakerCore, + abi.encodeWithSelector(ITiebreakerCore.getSealableResumeNonce.selector, sealable), + abi.encode(0) + ); + + (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = + tiebreakerSubCommittee.getSealableResumeState(sealable); + assertEq(support, 0); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(0)); + assertFalse(isExecuted); + + vm.prank(committeeMembers[0]); + tiebreakerSubCommittee.sealableResume(sealable); + + (support, executionQuorum, quorumAt, isExecuted) = tiebreakerSubCommittee.getSealableResumeState(sealable); + assertEq(support, 1); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(0)); + assertFalse(isExecuted); + + vm.prank(committeeMembers[1]); + tiebreakerSubCommittee.sealableResume(sealable); + + (support, executionQuorum, quorumAt, isExecuted) = tiebreakerSubCommittee.getSealableResumeState(sealable); + assertEq(support, quorum); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); + assertFalse(isExecuted); + + vm.prank(committeeMembers[2]); + tiebreakerSubCommittee.executeSealableResume(sealable); + + (support, executionQuorum, quorumAt, isExecuted) = tiebreakerSubCommittee.getSealableResumeState(sealable); + assertEq(support, quorum); + assertEq(executionQuorum, quorum); + assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); + assertTrue(isExecuted); + } +} From a2df783b6c397633c2ab37871f351b029037a679 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 3 Sep 2024 18:06:00 +0300 Subject: [PATCH 21/86] fix: reseal committee integration test fix --- test/scenario/reseal-committee.t.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/scenario/reseal-committee.t.sol b/test/scenario/reseal-committee.t.sol index 9ceb10ee..7caaf81c 100644 --- a/test/scenario/reseal-committee.t.sol +++ b/test/scenario/reseal-committee.t.sol @@ -38,16 +38,14 @@ contract ResealCommitteeTest is ScenarioTestBlueprint { for (uint256 i = 0; i < _resealCommittee.quorum() - 1; i++) { vm.prank(members[i]); _resealCommittee.voteReseal(sealable, true); - (support, quorum,, isExecuted) = _resealCommittee.getResealState(sealable); + (support, quorum,) = _resealCommittee.getResealState(sealable); assert(support < quorum); - assert(isExecuted == false); } vm.prank(members[members.length - 1]); _resealCommittee.voteReseal(sealable, true); - (support, quorum,, isExecuted) = _resealCommittee.getResealState(sealable); + (support, quorum,) = _resealCommittee.getResealState(sealable); assert(support == quorum); - assert(isExecuted == false); _assertNormalState(); From 3e2d2c77c968653d9e6b15d83ea7e63df7b990ea Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 3 Sep 2024 18:07:44 +0300 Subject: [PATCH 22/86] feat: remove arrays util --- contracts/utils/arrays.sol | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 contracts/utils/arrays.sol diff --git a/contracts/utils/arrays.sol b/contracts/utils/arrays.sol deleted file mode 100644 index c6daf147..00000000 --- a/contracts/utils/arrays.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -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) { - res[i] = value; - } - } -} From 9af81d2015e209d749068029624f0e4c8efc4991 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 4 Sep 2024 03:20:50 +0400 Subject: [PATCH 23/86] DualGovernanceConfig sanity checks. Properties renaming --- contracts/DualGovernance.sol | 80 ++++++++++++----- contracts/DualGovernanceConfigProvider.sol | 53 +++++------- contracts/libraries/DualGovernanceConfig.sol | 85 ++++++++++++------- .../libraries/DualGovernanceStateMachine.sol | 34 +++++--- contracts/types/Duration.sol | 37 ++++---- test/scenario/escrow.t.sol | 12 ++- test/scenario/gov-state-transitions.t.sol | 16 ++-- .../last-moment-malicious-proposal.t.sol | 14 +-- test/scenario/tiebreaker.t.sol | 6 +- test/scenario/veto-cooldown-mechanics.t.sol | 8 +- test/unit/DualGovernance.t.sol | 21 +++-- .../DualGovernanceStateMachine.t.sol | 15 ++-- test/utils/SetupDeployment.sol | 44 +++++++--- test/utils/scenario-test-blueprint.sol | 72 ++++++++++++---- 14 files changed, 320 insertions(+), 177 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index ffc5d822..32dd6afb 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -5,6 +5,7 @@ import {Duration} from "./types/Duration.sol"; import {Timestamp} from "./types/Timestamp.sol"; import {ITimelock} from "./interfaces/ITimelock.sol"; import {IResealManager} from "./interfaces/IResealManager.sol"; +import {IDualGovernanceConfigProvider} from "./interfaces/IDualGovernanceConfigProvider.sol"; import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; @@ -15,14 +16,16 @@ import {IResealManager} from "./interfaces/IResealManager.sol"; import {Proposers} from "./libraries/Proposers.sol"; import {Tiebreaker} from "./libraries/Tiebreaker.sol"; import {ExternalCall} from "./libraries/ExternalCalls.sol"; +import {DualGovernanceConfig} from "./libraries/DualGovernanceConfig.sol"; import {State, DualGovernanceStateMachine} from "./libraries/DualGovernanceStateMachine.sol"; -import {IDualGovernanceConfigProvider} from "./DualGovernanceConfigProvider.sol"; import {Escrow} from "./Escrow.sol"; contract DualGovernance is IDualGovernance { + using Proposers for Proposers.Context; using Tiebreaker for Tiebreaker.Context; + using DualGovernanceConfig for DualGovernanceConfig.Context; using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; // --- @@ -120,7 +123,9 @@ contract DualGovernance is IDualGovernance { // Proposals Flow // --- - function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId) { + function submitProposal( + ExternalCall[] calldata calls + ) external returns (uint256 proposalId) { _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); if (!_stateMachine.canSubmitProposal()) { revert ProposalSubmissionBlocked(); @@ -129,7 +134,9 @@ contract DualGovernance is IDualGovernance { proposalId = TIMELOCK.submit(proposer.executor, calls); } - function scheduleProposal(uint256 proposalId) external { + function scheduleProposal( + uint256 proposalId + ) external { _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = TIMELOCK.getProposalInfo(proposalId); @@ -167,7 +174,9 @@ contract DualGovernance is IDualGovernance { return _stateMachine.canSubmitProposal(); } - function canScheduleProposal(uint256 proposalId) external view returns (bool) { + function canScheduleProposal( + uint256 proposalId + ) external view returns (bool) { ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = TIMELOCK.getProposalInfo(proposalId); return _stateMachine.canScheduleProposal(submittedAt) && TIMELOCK.canSchedule(proposalId); @@ -181,7 +190,9 @@ contract DualGovernance is IDualGovernance { _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); } - function setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) external { + function setConfigProvider( + IDualGovernanceConfigProvider newConfigProvider + ) external { _checkCallerIsAdminExecutor(); _setConfigProvider(newConfigProvider); @@ -212,8 +223,8 @@ contract DualGovernance is IDualGovernance { return _stateMachine.getCurrentContext(); } - function getDynamicDelayDuration() external view returns (Duration) { - return _stateMachine.getDynamicDelayDuration(_configProvider.getDualGovernanceConfig()); + function getVetoSignallingDuration() external view returns (Duration) { + return _stateMachine.getVetoSignallingDuration(_configProvider.getDualGovernanceConfig()); } // --- @@ -225,7 +236,9 @@ contract DualGovernance is IDualGovernance { _proposers.register(proposer, executor); } - function unregisterProposer(address proposer) external { + function unregisterProposer( + address proposer + ) external { _checkCallerIsAdminExecutor(); _proposers.unregister(proposer); @@ -235,11 +248,15 @@ contract DualGovernance is IDualGovernance { } } - function isProposer(address account) external view returns (bool) { + function isProposer( + address account + ) external view returns (bool) { return _proposers.isProposer(account); } - function getProposer(address account) external view returns (Proposers.Proposer memory proposer) { + function getProposer( + address account + ) external view returns (Proposers.Proposer memory proposer) { proposer = _proposers.getProposer(account); } @@ -247,7 +264,9 @@ contract DualGovernance is IDualGovernance { proposers = _proposers.getAllProposers(); } - function isExecutor(address account) external view returns (bool) { + function isExecutor( + address account + ) external view returns (bool) { return _proposers.isExecutor(account); } @@ -255,35 +274,47 @@ contract DualGovernance is IDualGovernance { // Tiebreaker Protection // --- - function addTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { + function addTiebreakerSealableWithdrawalBlocker( + address sealableWithdrawalBlocker + ) external { _checkCallerIsAdminExecutor(); _tiebreaker.addSealableWithdrawalBlocker(sealableWithdrawalBlocker, MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT); } - function removeTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { + function removeTiebreakerSealableWithdrawalBlocker( + address sealableWithdrawalBlocker + ) external { _checkCallerIsAdminExecutor(); _tiebreaker.removeSealableWithdrawalBlocker(sealableWithdrawalBlocker); } - function setTiebreakerCommittee(address tiebreakerCommittee) external { + function setTiebreakerCommittee( + address tiebreakerCommittee + ) external { _checkCallerIsAdminExecutor(); _tiebreaker.setTiebreakerCommittee(tiebreakerCommittee); } - function setTiebreakerActivationTimeout(Duration tiebreakerActivationTimeout) external { + function setTiebreakerActivationTimeout( + Duration tiebreakerActivationTimeout + ) external { _checkCallerIsAdminExecutor(); _tiebreaker.setTiebreakerActivationTimeout( MIN_TIEBREAKER_ACTIVATION_TIMEOUT, tiebreakerActivationTimeout, MAX_TIEBREAKER_ACTIVATION_TIMEOUT ); } - function tiebreakerResumeSealable(address sealable) external { + function tiebreakerResumeSealable( + address sealable + ) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); _tiebreaker.checkTie(_stateMachine.getCurrentState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); RESEAL_MANAGER.resume(sealable); } - function tiebreakerScheduleProposal(uint256 proposalId) external { + function tiebreakerScheduleProposal( + uint256 proposalId + ) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); _tiebreaker.checkTie(_stateMachine.getCurrentState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); @@ -308,7 +339,9 @@ contract DualGovernance is IDualGovernance { // Reseal executor // --- - function resealSealable(address sealable) external { + function resealSealable( + address sealable + ) external { if (msg.sender != _resealCommittee) { revert CallerIsNotResealCommittee(msg.sender); } @@ -318,7 +351,9 @@ contract DualGovernance is IDualGovernance { RESEAL_MANAGER.reseal(sealable); } - function setResealCommittee(address resealCommittee) external { + function setResealCommittee( + address resealCommittee + ) external { _checkCallerIsAdminExecutor(); _resealCommittee = resealCommittee; } @@ -327,7 +362,9 @@ contract DualGovernance is IDualGovernance { // Private methods // --- - function _setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) internal { + function _setConfigProvider( + IDualGovernanceConfigProvider newConfigProvider + ) internal { if (address(newConfigProvider) == address(0)) { revert InvalidConfigProvider(newConfigProvider); } @@ -336,6 +373,8 @@ contract DualGovernance is IDualGovernance { return; } + IDualGovernanceConfigProvider(newConfigProvider).getDualGovernanceConfig().validate(); + _configProvider = IDualGovernanceConfigProvider(newConfigProvider); emit ConfigProviderSet(newConfigProvider); } @@ -345,4 +384,5 @@ contract DualGovernance is IDualGovernance { revert CallerIsNotAdminExecutor(msg.sender); } } + } diff --git a/contracts/DualGovernanceConfigProvider.sol b/contracts/DualGovernanceConfigProvider.sol index 806e3dd5..14f80d27 100644 --- a/contracts/DualGovernanceConfigProvider.sol +++ b/contracts/DualGovernanceConfigProvider.sol @@ -7,33 +7,37 @@ import {DualGovernanceConfig} from "./libraries/DualGovernanceConfig.sol"; import {IDualGovernanceConfigProvider} from "./interfaces/IDualGovernanceConfigProvider.sol"; contract ImmutableDualGovernanceConfigProvider is IDualGovernanceConfigProvider { + + using DualGovernanceConfig for DualGovernanceConfig.Context; + PercentD16 public immutable FIRST_SEAL_RAGE_QUIT_SUPPORT; PercentD16 public immutable SECOND_SEAL_RAGE_QUIT_SUPPORT; Duration public immutable MIN_ASSETS_LOCK_DURATION; - Duration public immutable DYNAMIC_TIMELOCK_MIN_DURATION; - Duration public immutable DYNAMIC_TIMELOCK_MAX_DURATION; + Duration public immutable VETO_SIGNALLING_MIN_DURATION; + Duration public immutable VETO_SIGNALLING_MAX_DURATION; Duration public immutable VETO_SIGNALLING_MIN_ACTIVE_DURATION; Duration public immutable VETO_SIGNALLING_DEACTIVATION_MAX_DURATION; Duration public immutable VETO_COOLDOWN_DURATION; Duration public immutable RAGE_QUIT_EXTENSION_DELAY; - Duration public immutable RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK; - uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER; + Duration public immutable RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY; + Duration public immutable RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY; + Duration public immutable RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH; - uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_A; - uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_B; - uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C; + constructor( + DualGovernanceConfig.Context memory dualGovernanceConfig + ) { + dualGovernanceConfig.validate(); - constructor(DualGovernanceConfig.Context memory dualGovernanceConfig) { FIRST_SEAL_RAGE_QUIT_SUPPORT = dualGovernanceConfig.firstSealRageQuitSupport; SECOND_SEAL_RAGE_QUIT_SUPPORT = dualGovernanceConfig.secondSealRageQuitSupport; MIN_ASSETS_LOCK_DURATION = dualGovernanceConfig.minAssetsLockDuration; - DYNAMIC_TIMELOCK_MIN_DURATION = dualGovernanceConfig.dynamicTimelockMinDuration; - DYNAMIC_TIMELOCK_MAX_DURATION = dualGovernanceConfig.dynamicTimelockMaxDuration; + VETO_SIGNALLING_MIN_DURATION = dualGovernanceConfig.vetoSignallingMinDuration; + VETO_SIGNALLING_MAX_DURATION = dualGovernanceConfig.vetoSignallingMaxDuration; VETO_SIGNALLING_MIN_ACTIVE_DURATION = dualGovernanceConfig.vetoSignallingMinActiveDuration; VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = dualGovernanceConfig.vetoSignallingDeactivationMaxDuration; @@ -41,16 +45,9 @@ contract ImmutableDualGovernanceConfigProvider is IDualGovernanceConfigProvider VETO_COOLDOWN_DURATION = dualGovernanceConfig.vetoCooldownDuration; RAGE_QUIT_EXTENSION_DELAY = dualGovernanceConfig.rageQuitExtensionDelay; - RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK = dualGovernanceConfig.rageQuitEthWithdrawalsMinTimelock; - RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER = - dualGovernanceConfig.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber; - - RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_A = - dualGovernanceConfig.rageQuitEthWithdrawalsTimelockGrowthCoeffs[0]; - RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_B = - dualGovernanceConfig.rageQuitEthWithdrawalsTimelockGrowthCoeffs[1]; - RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C = - dualGovernanceConfig.rageQuitEthWithdrawalsTimelockGrowthCoeffs[2]; + RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY = dualGovernanceConfig.rageQuitEthWithdrawalsMinDelay; + RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY = dualGovernanceConfig.rageQuitEthWithdrawalsMaxDelay; + RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH = dualGovernanceConfig.rageQuitEthWithdrawalsDelayGrowth; } function getDualGovernanceConfig() external view returns (DualGovernanceConfig.Context memory config) { @@ -58,19 +55,15 @@ contract ImmutableDualGovernanceConfigProvider is IDualGovernanceConfigProvider config.secondSealRageQuitSupport = SECOND_SEAL_RAGE_QUIT_SUPPORT; config.minAssetsLockDuration = MIN_ASSETS_LOCK_DURATION; - config.dynamicTimelockMinDuration = DYNAMIC_TIMELOCK_MIN_DURATION; - config.dynamicTimelockMaxDuration = DYNAMIC_TIMELOCK_MAX_DURATION; + config.vetoSignallingMinDuration = VETO_SIGNALLING_MIN_DURATION; + config.vetoSignallingMaxDuration = VETO_SIGNALLING_MAX_DURATION; config.vetoSignallingMinActiveDuration = VETO_SIGNALLING_MIN_ACTIVE_DURATION; config.vetoSignallingDeactivationMaxDuration = VETO_SIGNALLING_DEACTIVATION_MAX_DURATION; config.vetoCooldownDuration = VETO_COOLDOWN_DURATION; config.rageQuitExtensionDelay = RAGE_QUIT_EXTENSION_DELAY; - 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 - ]; + config.rageQuitEthWithdrawalsMinDelay = RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY; + config.rageQuitEthWithdrawalsMaxDelay = RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY; + config.rageQuitEthWithdrawalsDelayGrowth = RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH; } + } diff --git a/contracts/libraries/DualGovernanceConfig.sol b/contracts/libraries/DualGovernanceConfig.sol index c1913bd9..3e164405 100644 --- a/contracts/libraries/DualGovernanceConfig.sol +++ b/contracts/libraries/DualGovernanceConfig.sol @@ -1,24 +1,55 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + import {PercentD16} from "../types/PercentD16.sol"; import {Duration, Durations} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; library DualGovernanceConfig { + + error InvalidRageQuitSupportRange(PercentD16 firstSealRageQuitSupport, PercentD16 secondSealRageQuitSupport); + error RageQuitEthWithdrawalsDelayRange( + Duration rageQuitEthWithdrawalsMinDelay, Duration rageQuitEthWithdrawalsMaxDelay + ); + error InvalidVetoSignallingDurationRange(Duration vetoSignallingMinDuration, Duration vetoSignallingMaxDuration); + struct Context { PercentD16 firstSealRageQuitSupport; PercentD16 secondSealRageQuitSupport; + // Duration minAssetsLockDuration; - Duration dynamicTimelockMinDuration; - Duration dynamicTimelockMaxDuration; + // + Duration vetoSignallingMinDuration; + Duration vetoSignallingMaxDuration; Duration vetoSignallingMinActiveDuration; Duration vetoSignallingDeactivationMaxDuration; + // Duration vetoCooldownDuration; + // Duration rageQuitExtensionDelay; - Duration rageQuitEthWithdrawalsMinTimelock; - uint256 rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber; - uint256[3] rageQuitEthWithdrawalsTimelockGrowthCoeffs; + Duration rageQuitEthWithdrawalsMinDelay; + Duration rageQuitEthWithdrawalsMaxDelay; + Duration rageQuitEthWithdrawalsDelayGrowth; + } + + function validate( + Context memory self + ) internal pure { + if (self.firstSealRageQuitSupport >= self.secondSealRageQuitSupport) { + revert InvalidRageQuitSupportRange(self.firstSealRageQuitSupport, self.secondSealRageQuitSupport); + } + + if (self.vetoSignallingMinDuration >= self.vetoSignallingMaxDuration) { + revert InvalidVetoSignallingDurationRange(self.vetoSignallingMinDuration, self.vetoSignallingMaxDuration); + } + + if (self.rageQuitEthWithdrawalsMinDelay > self.rageQuitEthWithdrawalsMaxDelay) { + revert RageQuitEthWithdrawalsDelayRange( + self.rageQuitEthWithdrawalsMinDelay, self.rageQuitEthWithdrawalsMaxDelay + ); + } } function isFirstSealRageQuitSupportCrossed( @@ -35,19 +66,12 @@ library DualGovernanceConfig { return rageQuitSupport > self.secondSealRageQuitSupport; } - function isDynamicTimelockMaxDurationPassed( - Context memory self, - Timestamp vetoSignallingActivatedAt - ) internal view returns (bool) { - return Timestamps.now() > self.dynamicTimelockMaxDuration.addTo(vetoSignallingActivatedAt); - } - - function isDynamicTimelockDurationPassed( + function isVetoSignallingDurationPassed( Context memory self, Timestamp vetoSignallingActivatedAt, PercentD16 rageQuitSupport ) internal view returns (bool) { - return Timestamps.now() > calcDynamicDelayDuration(self, rageQuitSupport).addTo(vetoSignallingActivatedAt); + return Timestamps.now() > calcVetoSignallingDuration(self, rageQuitSupport).addTo(vetoSignallingActivatedAt); } function isVetoSignallingReactivationDurationPassed( @@ -71,46 +95,43 @@ library DualGovernanceConfig { return Timestamps.now() > self.vetoCooldownDuration.addTo(vetoCooldownEnteredAt); } - function calcDynamicDelayDuration( + function calcVetoSignallingDuration( Context memory self, PercentD16 rageQuitSupport ) internal pure returns (Duration duration_) { PercentD16 firstSealRageQuitSupport = self.firstSealRageQuitSupport; PercentD16 secondSealRageQuitSupport = self.secondSealRageQuitSupport; - Duration dynamicTimelockMinDuration = self.dynamicTimelockMinDuration; - Duration dynamicTimelockMaxDuration = self.dynamicTimelockMaxDuration; + Duration vetoSignallingMinDuration = self.vetoSignallingMinDuration; + Duration vetoSignallingMaxDuration = self.vetoSignallingMaxDuration; if (rageQuitSupport <= firstSealRageQuitSupport) { return Durations.ZERO; } if (rageQuitSupport >= secondSealRageQuitSupport) { - return dynamicTimelockMaxDuration; + return vetoSignallingMaxDuration; } - duration_ = dynamicTimelockMinDuration + duration_ = vetoSignallingMinDuration + Durations.from( PercentD16.unwrap(rageQuitSupport - firstSealRageQuitSupport) - * (dynamicTimelockMaxDuration - dynamicTimelockMinDuration).toSeconds() + * (vetoSignallingMaxDuration - vetoSignallingMinDuration).toSeconds() / PercentD16.unwrap(secondSealRageQuitSupport - firstSealRageQuitSupport) ); } - function calcRageQuitWithdrawalsTimelock( + function calcRageQuitWithdrawalsDelay( Context memory self, uint256 rageQuitRound ) internal pure returns (Duration) { - if (rageQuitRound < self.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber) { - return self.rageQuitEthWithdrawalsMinTimelock; - } - return self.rageQuitEthWithdrawalsMinTimelock - + Durations.from( - ( - self.rageQuitEthWithdrawalsTimelockGrowthCoeffs[0] * rageQuitRound * rageQuitRound - + self.rageQuitEthWithdrawalsTimelockGrowthCoeffs[1] * rageQuitRound - + self.rageQuitEthWithdrawalsTimelockGrowthCoeffs[2] - ) / 10 ** 18 - ); // TODO: rewrite in a prettier way + return Durations.from( + Math.min( + self.rageQuitEthWithdrawalsMinDelay.toSeconds() + + rageQuitRound * self.rageQuitEthWithdrawalsDelayGrowth.toSeconds(), + self.rageQuitEthWithdrawalsMaxDelay.toSeconds() + ) + ); } + } diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index d783d55f..6a5c752c 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -22,6 +22,7 @@ enum State { } library DualGovernanceStateMachine { + using DualGovernanceConfig for DualGovernanceConfig.Context; struct Context { @@ -119,7 +120,7 @@ library DualGovernanceStateMachine { self.rageQuitRound = uint8(newRageQuitRound); signallingEscrow.startRageQuit( - config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsTimelock(newRageQuitRound) + config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsDelay(newRageQuitRound) ); self.rageQuitEscrow = signallingEscrow; _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); @@ -128,33 +129,43 @@ library DualGovernanceStateMachine { emit DualGovernanceStateChanged(currentState, newState, self); } - function getCurrentContext(Context storage self) internal pure returns (Context memory) { + function getCurrentContext( + Context storage self + ) internal pure returns (Context memory) { return self; } - function getCurrentState(Context storage self) internal view returns (State) { + function getCurrentState( + Context storage self + ) internal view returns (State) { return self.state; } - function getNormalOrVetoCooldownStateExitedAt(Context storage self) internal view returns (Timestamp) { + function getNormalOrVetoCooldownStateExitedAt( + Context storage self + ) internal view returns (Timestamp) { return self.normalOrVetoCooldownExitedAt; } - function getDynamicDelayDuration( + function getVetoSignallingDuration( Context storage self, DualGovernanceConfig.Context memory config ) internal view returns (Duration) { - return config.calcDynamicDelayDuration(self.signallingEscrow.getRageQuitSupport()); + return config.calcVetoSignallingDuration(self.signallingEscrow.getRageQuitSupport()); } - function canSubmitProposal(Context storage self) internal view returns (bool) { + function canSubmitProposal( + Context storage self + ) internal view returns (bool) { State state = self.state; return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; } function canScheduleProposal(Context storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { State state = self.state; - if (state == State.Normal) return true; + if (state == State.Normal) { + return true; + } if (state == State.VetoCooldown) { return proposalSubmissionTime <= self.vetoSignallingActivatedAt; } @@ -171,9 +182,11 @@ library DualGovernanceStateMachine { self.signallingEscrow = newSignallingEscrow; emit NewSignallingEscrowDeployed(newSignallingEscrow); } + } library DualGovernanceStateTransitions { + using DualGovernanceConfig for DualGovernanceConfig.Context; function getStateTransition( @@ -211,7 +224,7 @@ library DualGovernanceStateTransitions { ) private view returns (State) { PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + if (!config.isVetoSignallingDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { return State.VetoSignalling; } @@ -230,7 +243,7 @@ library DualGovernanceStateTransitions { ) private view returns (State) { PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); - if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { + if (!config.isVetoSignallingDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { return State.VetoSignalling; } @@ -268,4 +281,5 @@ library DualGovernanceStateTransitions { ? State.VetoSignalling : State.VetoCooldown; } + } diff --git a/contracts/types/Duration.sol b/contracts/types/Duration.sol index f061273b..bc7e3227 100644 --- a/contracts/types/Duration.sol +++ b/contracts/types/Duration.sol @@ -11,21 +11,10 @@ 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; +using {lt as <, lte as <=, gt as >, gte as >=, eq as ==, notEq as !=} for Duration global; +using {plus as +, minus as -} for Duration global; + +using {addTo, plusSeconds, minusSeconds, multipliedBy, dividedBy, toSeconds} for Duration global; // --- // Comparison Ops @@ -43,6 +32,10 @@ function gt(Duration d1, Duration d2) pure returns (bool) { return Duration.unwrap(d1) > Duration.unwrap(d2); } +function gte(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); } @@ -94,28 +87,36 @@ function addTo(Duration d, Timestamp t) pure returns (Timestamp) { // Conversion Ops // --- -function toDuration(uint256 value) pure returns (Duration) { +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) { +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) { + 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/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 7bef7150..b9475012 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -15,6 +15,7 @@ import {Escrow, VetoerState, LockedAssetsTotals, WithdrawalsBatchesQueue} from " import {ScenarioTestBlueprint, LidoUtils, console} from "../utils/scenario-test-blueprint.sol"; contract EscrowHappyPath is ScenarioTestBlueprint { + using LidoUtils for LidoUtils.Context; Escrow internal escrow; @@ -578,7 +579,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { _assertVetoSignalingState(); // wait till the last second of the dynamic timelock duration - _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION()); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION()); _activateNextState(); _assertVetoSignalingState(); @@ -630,15 +631,20 @@ contract EscrowHappyPath is ScenarioTestBlueprint { _lockWstETH(vetoer, wstEthAmount); } - function externalUnlockStETH(address vetoer) external { + function externalUnlockStETH( + address vetoer + ) external { _unlockStETH(vetoer); } - function externalUnlockWstETH(address vetoer) external { + function externalUnlockWstETH( + address vetoer + ) external { _unlockWstETH(vetoer); } function externalUnlockUnstETH(address vetoer, uint256[] memory nftIds) external { _unlockUnstETH(vetoer, nftIds); } + } diff --git a/test/scenario/gov-state-transitions.t.sol b/test/scenario/gov-state-transitions.t.sol index f41f9bd3..c3d9e150 100644 --- a/test/scenario/gov-state-transitions.t.sol +++ b/test/scenario/gov-state-transitions.t.sol @@ -6,6 +6,7 @@ import {PercentsD16} from "contracts/types/PercentD16.sol"; import {ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; contract GovernanceStateTransitions is ScenarioTestBlueprint { + address internal immutable _VETOER = makeAddr("VETOER"); function setUp() external { @@ -24,12 +25,12 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MIN_DURATION().dividedBy(2)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MIN_DURATION().dividedBy(2)); _activateNextState(); _assertVetoSignalingState(); - _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MIN_DURATION().dividedBy(2).plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MIN_DURATION().dividedBy(2).plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); @@ -42,12 +43,12 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _assertVetoSignalingState(); - _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().dividedBy(2)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().dividedBy(2)); _activateNextState(); _assertVetoSignalingState(); - _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().dividedBy(2)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().dividedBy(2)); _activateNextState(); _assertVetoSignalingState(); @@ -70,7 +71,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); @@ -99,7 +100,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); @@ -121,7 +122,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); _assertVetoSignalingState(); - _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION()); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION()); _activateNextState(); _assertVetoSignalingState(); @@ -133,4 +134,5 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _activateNextState(); _assertRageQuitState(); } + } diff --git a/test/scenario/last-moment-malicious-proposal.t.sol b/test/scenario/last-moment-malicious-proposal.t.sol index 04caff99..9101301f 100644 --- a/test/scenario/last-moment-malicious-proposal.t.sol +++ b/test/scenario/last-moment-malicious-proposal.t.sol @@ -12,6 +12,7 @@ import { } from "../utils/scenario-test-blueprint.sol"; contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { + function setUp() external { _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); } @@ -121,7 +122,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _assertVetoSignalingState(); _logVetoSignallingState(); - _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); _logVetoSignallingState(); _activateNextState(); _assertRageQuitState(); @@ -225,7 +226,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("3. THE VETO SIGNALLING & DEACTIVATION PASSED BUT PROPOSAL STILL NOT EXECUTABLE"); { - _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MIN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); @@ -286,7 +287,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("3. THE VETO SIGNALLING & DEACTIVATION PASSED BUT PROPOSAL STILL NOT EXECUTABLE"); { - _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MIN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); @@ -312,7 +313,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("5. PROPOSAL EXECUTABLE IN THE NEXT VETO COOLDOWN"); { - _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MIN_DURATION().multipliedBy(2)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MIN_DURATION().multipliedBy(2)); _activateNextState(); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); @@ -329,7 +330,10 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { } } - function scheduleProposalExternal(uint256 proposalId) external { + function scheduleProposalExternal( + uint256 proposalId + ) external { _scheduleProposalViaDualGovernance(proposalId); } + } diff --git a/test/scenario/tiebreaker.t.sol b/test/scenario/tiebreaker.t.sol index a7062717..505658fa 100644 --- a/test/scenario/tiebreaker.t.sol +++ b/test/scenario/tiebreaker.t.sol @@ -7,6 +7,7 @@ import {PercentsD16} from "contracts/types/PercentD16.sol"; import {ScenarioTestBlueprint, ExternalCall, ExternalCallHelpers} from "../utils/scenario-test-blueprint.sol"; contract TiebreakerScenarioTest is ScenarioTestBlueprint { + address internal immutable _VETOER = makeAddr("VETOER"); uint256 public constant PAUSE_INFINITELY = type(uint256).max; @@ -28,7 +29,7 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { _assertNormalState(); _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); _lockStETH(_VETOER, 1 gwei); - _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertRageQuitState(); _wait(_dualGovernance.getTiebreakerState().tiebreakerActivationTimeout); @@ -103,7 +104,7 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { _assertNormalState(); _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); _lockStETH(_VETOER, 1 gwei); - _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertRageQuitState(); _wait(_dualGovernance.getTiebreakerState().tiebreakerActivationTimeout); @@ -166,4 +167,5 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { assertEq(_lido.withdrawalQueue.isPaused(), false); } + } diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index 9c19318a..711e60e5 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -10,6 +10,7 @@ import {LidoUtils} from "../utils/lido-utils.sol"; import {Escrow, ExternalCall, ExternalCallHelpers, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { + using LidoUtils for LidoUtils.Context; function setUp() external { @@ -41,7 +42,7 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { ); _assertVetoSignalingState(); - _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertRageQuitState(); } @@ -102,7 +103,10 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { } } - function scheduleProposalExternal(uint256 proposalId) external { + function scheduleProposalExternal( + uint256 proposalId + ) external { _scheduleProposal(_dualGovernance, proposalId); } + } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 032f043f..f560933a 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -10,11 +10,7 @@ import {Escrow} from "contracts/Escrow.sol"; import {Executor} from "contracts/Executor.sol"; import {DualGovernance, State} from "contracts/DualGovernance.sol"; import {IResealManager} from "contracts/interfaces/IResealManager.sol"; -import { - DualGovernanceConfig, - IDualGovernanceConfigProvider, - ImmutableDualGovernanceConfigProvider -} from "contracts/DualGovernanceConfigProvider.sol"; +import {DualGovernanceConfig, ImmutableDualGovernanceConfigProvider} from "contracts/DualGovernanceConfigProvider.sol"; import {IWstETH} from "contracts/interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; @@ -25,6 +21,7 @@ import {TimelockMock} from "test/mocks/TimelockMock.sol"; import {WithdrawalQueueMock} from "test/mocks/WithdrawalQueueMock.sol"; contract DualGovernanceUnitTests is UnitTest { + Executor private _executor = new Executor(address(this)); StETHMock private immutable _STETH_MOCK = new StETHMock(); @@ -41,17 +38,18 @@ contract DualGovernanceUnitTests is UnitTest { secondSealRageQuitSupport: PercentsD16.fromBasisPoints(15_00), // 15% // minAssetsLockDuration: Durations.from(5 hours), - dynamicTimelockMinDuration: Durations.from(3 days), - dynamicTimelockMaxDuration: Durations.from(30 days), // + vetoSignallingMinDuration: Durations.from(3 days), + vetoSignallingMaxDuration: Durations.from(30 days), vetoSignallingMinActiveDuration: Durations.from(5 hours), vetoSignallingDeactivationMaxDuration: Durations.from(5 days), + // vetoCooldownDuration: Durations.from(4 days), // rageQuitExtensionDelay: Durations.from(7 days), - rageQuitEthWithdrawalsMinTimelock: Durations.from(60 days), - rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: 2, - rageQuitEthWithdrawalsTimelockGrowthCoeffs: [uint256(0), 0, 0] + rageQuitEthWithdrawalsMinDelay: Durations.from(30 days), + rageQuitEthWithdrawalsMaxDelay: Durations.from(180 days), + rageQuitEthWithdrawalsDelayGrowth: Durations.from(15 days) }) ); @@ -156,7 +154,7 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(_dualGovernance.getCurrentState(), State.VetoSignalling); - _wait(_configProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); _dualGovernance.activateNextState(); assertEq(_dualGovernance.getCurrentState(), State.RageQuit); @@ -239,4 +237,5 @@ contract DualGovernanceUnitTests is UnitTest { // mock timelock doesn't uses proposal data _timelock.submit(address(0), new ExternalCall[](0)); } + } diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 3e5d6cdf..71b9c704 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -13,6 +13,7 @@ import {UnitTest} from "test/utils/unit-test.sol"; import {EscrowMock} from "test/mocks/EscrowMock.sol"; contract DualGovernanceStateMachineUnitTests is UnitTest { + using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; address private immutable _ESCROW_MASTER_COPY = address(new EscrowMock()); @@ -22,17 +23,18 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { secondSealRageQuitSupport: PercentsD16.fromBasisPoints(15_00), // 15% // minAssetsLockDuration: Durations.from(5 hours), - dynamicTimelockMinDuration: Durations.from(3 days), - dynamicTimelockMaxDuration: Durations.from(30 days), // + vetoSignallingMinDuration: Durations.from(3 days), + vetoSignallingMaxDuration: Durations.from(30 days), vetoSignallingMinActiveDuration: Durations.from(5 hours), vetoSignallingDeactivationMaxDuration: Durations.from(5 days), + // vetoCooldownDuration: Durations.from(4 days), // rageQuitExtensionDelay: Durations.from(7 days), - rageQuitEthWithdrawalsMinTimelock: Durations.from(60 days), - rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: 2, - rageQuitEthWithdrawalsTimelockGrowthCoeffs: [uint256(0), 0, 0] + rageQuitEthWithdrawalsMinDelay: Durations.from(30 days), + rageQuitEthWithdrawalsMaxDelay: Durations.from(180 days), + rageQuitEthWithdrawalsDelayGrowth: Durations.from(15 days) }) ); @@ -61,7 +63,7 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); assertEq(_stateMachine.state, State.VetoSignalling); - _wait(_CONFIG_PROVIDER.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); assertEq(_stateMachine.state, State.RageQuit); @@ -79,4 +81,5 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { assertEq(_stateMachine.rageQuitRound, 0); assertEq(_stateMachine.state, State.Normal); } + } diff --git a/test/utils/SetupDeployment.sol b/test/utils/SetupDeployment.sol index 34b20a15..88310c80 100644 --- a/test/utils/SetupDeployment.sol +++ b/test/utils/SetupDeployment.sol @@ -56,6 +56,7 @@ import {LidoUtils} from "./lido-utils.sol"; // --- abstract contract SetupDeployment is Test { + using Random for Random.Context; // --- // Helpers @@ -148,13 +149,17 @@ abstract contract SetupDeployment is Test { // Whole Setup Deployments // --- - function _deployTimelockedGovernanceSetup(bool isEmergencyProtectionEnabled) internal { + function _deployTimelockedGovernanceSetup( + bool isEmergencyProtectionEnabled + ) internal { _deployEmergencyProtectedTimelockContracts(isEmergencyProtectionEnabled); _timelockedGovernance = _deployTimelockedGovernance({governance: address(_lido.voting), timelock: _timelock}); _finalizeEmergencyProtectedTimelockDeploy(_timelockedGovernance); } - function _deployDualGovernanceSetup(bool isEmergencyProtectionEnabled) internal { + function _deployDualGovernanceSetup( + bool isEmergencyProtectionEnabled + ) internal { _deployEmergencyProtectedTimelockContracts(isEmergencyProtectionEnabled); _resealManager = _deployResealManager(_timelock); _dualGovernanceConfigProvider = _deployDualGovernanceConfigProvider(); @@ -236,7 +241,9 @@ abstract contract SetupDeployment is Test { // Emergency Protected Timelock Deployment // --- - function _deployEmergencyProtectedTimelockContracts(bool isEmergencyProtectionEnabled) internal { + function _deployEmergencyProtectedTimelockContracts( + bool isEmergencyProtectionEnabled + ) internal { _adminExecutor = _deployExecutor(address(this)); _timelock = _deployEmergencyProtectedTimelock(_adminExecutor); @@ -287,7 +294,9 @@ abstract contract SetupDeployment is Test { } } - function _finalizeEmergencyProtectedTimelockDeploy(IGovernance governance) internal { + function _finalizeEmergencyProtectedTimelockDeploy( + IGovernance governance + ) internal { _adminExecutor.execute( address(_timelock), 0, abi.encodeCall(_timelock.setupDelays, (_AFTER_SUBMIT_DELAY, _AFTER_SCHEDULE_DELAY)) ); @@ -295,11 +304,15 @@ abstract contract SetupDeployment is Test { _adminExecutor.transferOwnership(address(_timelock)); } - function _deployExecutor(address owner) internal returns (Executor) { + function _deployExecutor( + address owner + ) internal returns (Executor) { return new Executor(owner); } - function _deployEmergencyProtectedTimelock(Executor adminExecutor) internal returns (EmergencyProtectedTimelock) { + function _deployEmergencyProtectedTimelock( + Executor adminExecutor + ) internal returns (EmergencyProtectedTimelock) { return new EmergencyProtectedTimelock({ adminExecutor: address(adminExecutor), sanityCheckParams: EmergencyProtectedTimelock.SanityCheckParams({ @@ -360,22 +373,24 @@ abstract contract SetupDeployment is Test { secondSealRageQuitSupport: PercentsD16.fromBasisPoints(15_00), // 15% // minAssetsLockDuration: Durations.from(5 hours), - dynamicTimelockMinDuration: Durations.from(3 days), - dynamicTimelockMaxDuration: Durations.from(30 days), // + vetoSignallingMinDuration: Durations.from(3 days), + vetoSignallingMaxDuration: Durations.from(30 days), vetoSignallingMinActiveDuration: Durations.from(5 hours), vetoSignallingDeactivationMaxDuration: Durations.from(5 days), vetoCooldownDuration: Durations.from(4 days), // rageQuitExtensionDelay: Durations.from(7 days), - rageQuitEthWithdrawalsMinTimelock: Durations.from(60 days), - rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: 2, - rageQuitEthWithdrawalsTimelockGrowthCoeffs: [uint256(0), 0, 0] + rageQuitEthWithdrawalsMinDelay: Durations.from(30 days), + rageQuitEthWithdrawalsMaxDelay: Durations.from(180 days), + rageQuitEthWithdrawalsDelayGrowth: Durations.from(15 days) }) ); } - function _deployResealManager(ITimelock timelock) internal returns (ResealManager) { + function _deployResealManager( + ITimelock timelock + ) internal returns (ResealManager) { return new ResealManager(timelock); } @@ -428,10 +443,13 @@ abstract contract SetupDeployment is Test { // Helper methods // --- - function _generateRandomAddresses(uint256 count) internal returns (address[] memory addresses) { + function _generateRandomAddresses( + uint256 count + ) internal returns (address[] memory addresses) { addresses = new address[](count); for (uint256 i = 0; i < count; ++i) { addresses[i] = _random.nextAddress(); } } + } diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 4cc21da6..5e885816 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -49,6 +49,7 @@ import {TestingAssertEqExtender} from "./testing-assert-eq-extender.sol"; uint256 constant FORK_BLOCK_NUMBER = 20218312; contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { + using LidoUtils for LidoUtils.Context; constructor() SetupDeployment(LidoUtils.mainnet(), Random.create(block.timestamp)) { @@ -88,7 +89,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { { DualGovernanceStateMachine.Context memory stateContext = _dualGovernance.getCurrentStateContext(); isActive = stateContext.state == DGState.VetoSignalling; - duration = _dualGovernance.getDynamicDelayDuration().toSeconds(); + duration = _dualGovernance.getVetoSignallingDuration().toSeconds(); enteredAt = stateContext.enteredAt.toSeconds(); activatedAt = stateContext.vetoSignallingActivatedAt.toSeconds(); } @@ -124,7 +125,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { _lido.submitWstETH(account, _lido.calcSharesToDepositFromPercentageOfTVL(tvlPercentage)); } - function _getBalances(address vetoer) internal view returns (Balances memory balances) { + function _getBalances( + address vetoer + ) internal view returns (Balances memory balances) { uint256 stETHAmount = _lido.stETH.balanceOf(vetoer); uint256 wstETHShares = _lido.wstETH.balanceOf(vetoer); balances = Balances({ @@ -142,11 +145,15 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { _lido.finalizeWithdrawalQueue(); } - function _finalizeWithdrawalQueue(uint256 id) internal { + function _finalizeWithdrawalQueue( + uint256 id + ) internal { _lido.finalizeWithdrawalQueue(id); } - function _simulateRebase(PercentD16 rebaseFactor) internal { + function _simulateRebase( + PercentD16 rebaseFactor + ) internal { _lido.simulateRebase(rebaseFactor); } @@ -167,7 +174,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { vm.stopPrank(); } - function _unlockStETH(address vetoer) internal { + function _unlockStETH( + address vetoer + ) internal { vm.startPrank(vetoer); _getVetoSignallingEscrow().unlockStETH(); vm.stopPrank(); @@ -187,7 +196,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { vm.stopPrank(); } - function _unlockWstETH(address vetoer) internal { + function _unlockWstETH( + address vetoer + ) internal { Escrow escrow = _getVetoSignallingEscrow(); uint256 wstETHBalanceBefore = _lido.wstETH.balanceOf(vetoer); VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); @@ -309,11 +320,15 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(proposalId, proposalsCountBefore + 1); } - function _scheduleProposalViaDualGovernance(uint256 proposalId) internal { + function _scheduleProposalViaDualGovernance( + uint256 proposalId + ) internal { _scheduleProposal(_dualGovernance, proposalId); } - function _scheduleProposalViaTimelockedGovernance(uint256 proposalId) internal { + function _scheduleProposalViaTimelockedGovernance( + uint256 proposalId + ) internal { _scheduleProposal(_timelockedGovernance, proposalId); } @@ -321,7 +336,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { governance.scheduleProposal(proposalId); } - function _executeProposal(uint256 proposalId) internal { + function _executeProposal( + uint256 proposalId + ) internal { _timelock.execute(proposalId); } @@ -407,7 +424,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalSubmitted(uint256 proposalId) internal { + function _assertProposalSubmitted( + uint256 proposalId + ) internal { assertEq( _timelock.getProposal(proposalId).status, ProposalStatus.Submitted, @@ -415,7 +434,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalScheduled(uint256 proposalId) internal { + function _assertProposalScheduled( + uint256 proposalId + ) internal { assertEq( _timelock.getProposal(proposalId).status, ProposalStatus.Scheduled, @@ -423,7 +444,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalExecuted(uint256 proposalId) internal { + function _assertProposalExecuted( + uint256 proposalId + ) internal { assertEq( _timelock.getProposal(proposalId).status, ProposalStatus.Executed, @@ -431,7 +454,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalCancelled(uint256 proposalId) internal { + function _assertProposalCancelled( + uint256 proposalId + ) internal { assertEq(_timelock.getProposal(proposalId).status, ProposalStatus.Cancelled, "Proposal not in 'Canceled' state"); } @@ -518,12 +543,16 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { // Utils Methods // --- - function _step(string memory text) internal view { + function _step( + string memory text + ) internal view { // solhint-disable-next-line console.log(string.concat(">>> ", text, " <<<")); } - function _wait(Duration duration) internal { + function _wait( + Duration duration + ) internal { vm.warp(duration.addTo(Timestamps.now()).toSeconds()); } @@ -544,7 +573,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { _emergencyActivationCommittee.executeActivateEmergencyMode(); } - function _executeEmergencyExecute(uint256 proposalId) internal { + function _executeEmergencyExecute( + uint256 proposalId + ) internal { address[] memory members = _emergencyExecutionCommittee.getMembers(); for (uint256 i = 0; i < _emergencyExecutionCommittee.quorum(); ++i) { vm.prank(members[i]); @@ -569,14 +600,18 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { uint256 _seconds; } - function _toDuration(uint256 timestamp) internal pure returns (DurationStruct memory duration) { + function _toDuration( + uint256 timestamp + ) internal pure returns (DurationStruct memory duration) { duration._days = timestamp / 1 days; duration._hours = (timestamp - 1 days * duration._days) / 1 hours; duration._minutes = (timestamp - 1 days * duration._days - 1 hours * duration._hours) / 1 minutes; duration._seconds = timestamp % 1 minutes; } - function _formatDuration(DurationStruct memory duration) internal pure returns (string memory) { + function _formatDuration( + DurationStruct memory duration + ) internal pure returns (string memory) { // format example: 1d:22h:33m:12s return string( abi.encodePacked( @@ -591,4 +626,5 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ) ); } + } From 30492c58cf60b8f7b51a20d60ce6e3db209b3e9f Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 4 Sep 2024 03:59:41 +0400 Subject: [PATCH 24/86] RageQuitExtensionDelay -> RageQuitExtensionPeriodDuration --- contracts/DualGovernanceConfigProvider.sol | 8 +- contracts/Escrow.sol | 70 +++++--- contracts/libraries/DualGovernanceConfig.sol | 2 +- .../libraries/DualGovernanceStateMachine.sol | 2 +- contracts/libraries/EscrowState.sol | 82 +++++---- test/scenario/escrow.t.sol | 10 +- test/scenario/veto-cooldown-mechanics.t.sol | 4 +- test/unit/DualGovernance.t.sol | 2 +- .../DualGovernanceStateMachine.t.sol | 2 +- test/unit/libraries/EscrowState.t.sol | 168 ++++++++++-------- test/utils/SetupDeployment.sol | 2 +- 11 files changed, 207 insertions(+), 145 deletions(-) diff --git a/contracts/DualGovernanceConfigProvider.sol b/contracts/DualGovernanceConfigProvider.sol index 14f80d27..229f3d44 100644 --- a/contracts/DualGovernanceConfigProvider.sol +++ b/contracts/DualGovernanceConfigProvider.sol @@ -22,7 +22,7 @@ contract ImmutableDualGovernanceConfigProvider is IDualGovernanceConfigProvider Duration public immutable VETO_COOLDOWN_DURATION; - Duration public immutable RAGE_QUIT_EXTENSION_DELAY; + Duration public immutable RAGE_QUIT_EXTENSION_PERIOD_DURATION; Duration public immutable RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY; Duration public immutable RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY; Duration public immutable RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH; @@ -44,7 +44,7 @@ contract ImmutableDualGovernanceConfigProvider is IDualGovernanceConfigProvider VETO_COOLDOWN_DURATION = dualGovernanceConfig.vetoCooldownDuration; - RAGE_QUIT_EXTENSION_DELAY = dualGovernanceConfig.rageQuitExtensionDelay; + RAGE_QUIT_EXTENSION_PERIOD_DURATION = dualGovernanceConfig.rageQuitExtensionPeriodDuration; RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY = dualGovernanceConfig.rageQuitEthWithdrawalsMinDelay; RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY = dualGovernanceConfig.rageQuitEthWithdrawalsMaxDelay; RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH = dualGovernanceConfig.rageQuitEthWithdrawalsDelayGrowth; @@ -59,8 +59,10 @@ contract ImmutableDualGovernanceConfigProvider is IDualGovernanceConfigProvider config.vetoSignallingMaxDuration = VETO_SIGNALLING_MAX_DURATION; config.vetoSignallingMinActiveDuration = VETO_SIGNALLING_MIN_ACTIVE_DURATION; config.vetoSignallingDeactivationMaxDuration = VETO_SIGNALLING_DEACTIVATION_MAX_DURATION; + config.vetoCooldownDuration = VETO_COOLDOWN_DURATION; - config.rageQuitExtensionDelay = RAGE_QUIT_EXTENSION_DELAY; + + config.rageQuitExtensionPeriodDuration = RAGE_QUIT_EXTENSION_PERIOD_DURATION; config.rageQuitEthWithdrawalsMinDelay = RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY; config.rageQuitEthWithdrawalsMaxDelay = RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY; config.rageQuitEthWithdrawalsDelayGrowth = RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH; diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index f71d5f54..460151b7 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -41,6 +41,7 @@ struct VetoerState { } contract Escrow is IEscrow { + using EscrowState for EscrowState.Context; using AssetsAccounting for AssetsAccounting.Context; using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.Context; @@ -115,7 +116,9 @@ contract Escrow is IEscrow { MIN_WITHDRAWALS_BATCH_SIZE = minWithdrawalsBatchSize; } - function initialize(Duration minAssetsLockDuration) external { + function initialize( + Duration minAssetsLockDuration + ) external { if (address(this) == _SELF) { revert NonProxyCallsForbidden(); } @@ -131,7 +134,9 @@ contract Escrow is IEscrow { // Lock & unlock stETH // --- - function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { + function lockStETH( + uint256 amount + ) external returns (uint256 lockedStETHShares) { DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); @@ -157,7 +162,9 @@ contract Escrow is IEscrow { // Lock & unlock wstETH // --- - function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { + function lockWstETH( + uint256 amount + ) external returns (uint256 lockedStETHShares) { DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); @@ -183,7 +190,9 @@ contract Escrow is IEscrow { // --- // Lock & unlock unstETH // --- - function lockUnstETH(uint256[] memory unstETHIds) external { + function lockUnstETH( + uint256[] memory unstETHIds + ) external { if (unstETHIds.length == 0) { revert EmptyUnstETHIds(); } @@ -201,7 +210,9 @@ contract Escrow is IEscrow { DUAL_GOVERNANCE.activateNextState(); } - function unlockUnstETH(uint256[] memory unstETHIds) external { + function unlockUnstETH( + uint256[] memory unstETHIds + ) external { DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); @@ -226,7 +237,9 @@ contract Escrow is IEscrow { // Convert to NFT // --- - function requestWithdrawals(uint256[] calldata stETHAmounts) external returns (uint256[] memory unstETHIds) { + function requestWithdrawals( + uint256[] calldata stETHAmounts + ) external returns (uint256[] memory unstETHIds) { _escrowState.checkSignallingEscrow(); unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(stETHAmounts, address(this)); @@ -244,9 +257,9 @@ contract Escrow is IEscrow { // Start rage quit // --- - function startRageQuit(Duration rageQuitExtensionDelay, Duration rageQuitWithdrawalsTimelock) external { + function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitWithdrawalsTimelock) external { _checkCallerIsDualGovernance(); - _escrowState.startRageQuit(rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + _escrowState.startRageQuit(rageQuitExtensionPeriodDuration, rageQuitWithdrawalsTimelock); _batchesQueue.open(WITHDRAWAL_QUEUE.getLastRequestId()); } @@ -254,7 +267,9 @@ contract Escrow is IEscrow { // Request withdrawal batches // --- - function requestNextWithdrawalsBatch(uint256 batchSize) external { + function requestNextWithdrawalsBatch( + uint256 batchSize + ) external { _escrowState.checkRageQuitEscrow(); if (batchSize < MIN_WITHDRAWALS_BATCH_SIZE) { @@ -288,7 +303,9 @@ contract Escrow is IEscrow { // Claim requested withdrawal batches // --- - function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { + function claimNextWithdrawalsBatch( + uint256 maxUnstETHIdsCount + ) external { _escrowState.checkRageQuitEscrow(); _escrowState.checkBatchesClaimingInProgress(); @@ -314,13 +331,13 @@ contract Escrow is IEscrow { // Start rage quit extension delay // --- - function startRageQuitExtensionDelay() external { + function startRageQuitExtensionPeriod() external { if (!_batchesQueue.isClosed()) { revert BatchesQueueIsNotClosed(); } /// @dev This check is primarily required when only unstETH NFTs are locked in the Escrow - /// and there are no WithdrawalBatches. In this scenario, the RageQuitExtensionDelay can only begin + /// and there are no WithdrawalBatches. In this scenario, the RageQuitExtensionPeriod can only begin /// when the last locked unstETH id is finalized in the WithdrawalQueue. /// When the WithdrawalBatchesQueue is not empty, this invariant is maintained by the following: /// - Any locked unstETH during the VetoSignalling phase has an id less than any unstETH NFT created @@ -335,7 +352,7 @@ contract Escrow is IEscrow { revert UnclaimedBatches(); } - _escrowState.startRageQuitExtensionDelay(); + _escrowState.startRageQuitExtensionPeriod(); } // --- @@ -358,7 +375,9 @@ contract Escrow is IEscrow { // Escrow management // --- - function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { + function setMinAssetsLockDuration( + Duration newMinAssetsLockDuration + ) external { _checkCallerIsDualGovernance(); _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration); } @@ -374,7 +393,9 @@ contract Escrow is IEscrow { ethToWithdraw.sendTo(payable(msg.sender)); } - function withdrawETH(uint256[] calldata unstETHIds) external { + function withdrawETH( + uint256[] calldata unstETHIds + ) external { if (unstETHIds.length == 0) { revert EmptyUnstETHIds(); } @@ -398,7 +419,9 @@ contract Escrow is IEscrow { totals.unstETHFinalizedETH = unstETHTotals.finalizedETH.toUint256(); } - function getVetoerState(address vetoer) external view returns (VetoerState memory state) { + function getVetoerState( + address vetoer + ) external view returns (VetoerState memory state) { HolderAssets storage assets = _accounting.assets[vetoer]; state.unstETHIdsCount = assets.unstETHIds.length; @@ -411,7 +434,9 @@ contract Escrow is IEscrow { return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); } - function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds) { + function getNextWithdrawalBatch( + uint256 limit + ) external view returns (uint256[] memory unstETHIds) { return _batchesQueue.getNextWithdrawalsBatches(limit); } @@ -419,12 +444,12 @@ contract Escrow is IEscrow { return _batchesQueue.isClosed(); } - function isRageQuitExtensionDelayStarted() external view returns (bool) { - return _escrowState.isRageQuitExtensionDelayStarted(); + function isRageQuitExtensionPeriodStarted() external view returns (bool) { + return _escrowState.isRageQuitExtensionPeriodStarted(); } - function getRageQuitExtensionDelayStartedAt() external view returns (Timestamp) { - return _escrowState.rageQuitExtensionDelayStartedAt; + function getRageQuitExtensionPeriodStartedAt() external view returns (Timestamp) { + return _escrowState.rageQuitExtensionPeriodStartedAt; } function getRageQuitSupport() external view returns (PercentD16) { @@ -441,7 +466,7 @@ contract Escrow is IEscrow { } function isRageQuitFinalized() external view returns (bool) { - return _escrowState.isRageQuitEscrow() && _escrowState.isRageQuitExtensionDelayPassed(); + return _escrowState.isRageQuitEscrow() && _escrowState.isRageQuitExtensionPeriodPassed(); } // --- @@ -483,4 +508,5 @@ contract Escrow is IEscrow { revert CallerIsNotDualGovernance(msg.sender); } } + } diff --git a/contracts/libraries/DualGovernanceConfig.sol b/contracts/libraries/DualGovernanceConfig.sol index 3e164405..f0a0994e 100644 --- a/contracts/libraries/DualGovernanceConfig.sol +++ b/contracts/libraries/DualGovernanceConfig.sol @@ -28,7 +28,7 @@ library DualGovernanceConfig { // Duration vetoCooldownDuration; // - Duration rageQuitExtensionDelay; + Duration rageQuitExtensionPeriodDuration; Duration rageQuitEthWithdrawalsMinDelay; Duration rageQuitEthWithdrawalsMaxDelay; Duration rageQuitEthWithdrawalsDelayGrowth; diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 6a5c752c..266643e2 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -120,7 +120,7 @@ library DualGovernanceStateMachine { self.rageQuitRound = uint8(newRageQuitRound); signallingEscrow.startRageQuit( - config.rageQuitExtensionDelay, config.calcRageQuitWithdrawalsDelay(newRageQuitRound) + config.rageQuitExtensionPeriodDuration, config.calcRageQuitWithdrawalsDelay(newRageQuitRound) ); self.rageQuitEscrow = signallingEscrow; _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); diff --git a/contracts/libraries/EscrowState.sol b/contracts/libraries/EscrowState.sol index 5c5b14b0..da6c9944 100644 --- a/contracts/libraries/EscrowState.sol +++ b/contracts/libraries/EscrowState.sol @@ -21,6 +21,7 @@ enum State { /// @title EscrowState /// @notice Represents the logic to manipulate the state of the Escrow library EscrowState { + // --- // Errors // --- @@ -37,15 +38,15 @@ library EscrowState { event RageQuitTimelockStarted(Timestamp startedAt); event EscrowStateChanged(State from, State to); - event RageQuitStarted(Duration rageQuitExtensionDelay, Duration rageQuitWithdrawalsTimelock); + event RageQuitStarted(Duration rageQuitExtensionDuration, Duration rageQuitWithdrawalsTimelock); event MinAssetsLockDurationSet(Duration newAssetsLockDuration); /// @notice Stores the context of the state of the Escrow instance /// @param state The current state of the Escrow instance /// @param minAssetsLockDuration The minimum time required to pass before tokens can be unlocked from the Escrow /// contract instance - /// @param rageQuitExtensionDelay The period of time that starts after all withdrawal batches are formed, which delays - /// the exit from the RageQuit state of the DualGovernance. The main purpose of the rage quit extension delay is to provide + /// @param rageQuitExtensionPeriodDuration The period of time that starts after all withdrawal batches are formed, which delays + /// the exit from the RageQuit state of the DualGovernance. The main purpose of the rage quit extension period is to provide /// enough time for users who locked their unstETH to claim it. struct Context { /// @dev slot0: [0..7] @@ -53,9 +54,9 @@ library EscrowState { /// @dev slot0: [8..39] Duration minAssetsLockDuration; /// @dev slot0: [40..71] - Duration rageQuitExtensionDelay; + Duration rageQuitExtensionPeriodDuration; /// @dev slot0: [72..111] - Timestamp rageQuitExtensionDelayStartedAt; + Timestamp rageQuitExtensionPeriodStartedAt; /// @dev slot0: [112..143] Duration rageQuitWithdrawalsTimelock; } @@ -71,25 +72,27 @@ library EscrowState { /// @notice Starts the rage quit process /// @param self The context of the Escrow instance - /// @param rageQuitExtensionDelay The delay period for the rage quit extension + /// @param rageQuitExtensionPeriodDuration The duration of the period for the rage quit extension /// @param rageQuitWithdrawalsTimelock The timelock period for rage quit withdrawals function startRageQuit( Context storage self, - Duration rageQuitExtensionDelay, + Duration rageQuitExtensionPeriodDuration, Duration rageQuitWithdrawalsTimelock ) internal { _checkState(self, State.SignallingEscrow); _setState(self, State.RageQuitEscrow); - self.rageQuitExtensionDelay = rageQuitExtensionDelay; + self.rageQuitExtensionPeriodDuration = rageQuitExtensionPeriodDuration; self.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; - emit RageQuitStarted(rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + emit RageQuitStarted(rageQuitExtensionPeriodDuration, rageQuitWithdrawalsTimelock); } - /// @notice Starts the rage quit extension delay + /// @notice Starts the rage quit extension period /// @param self The context of the Escrow instance - function startRageQuitExtensionDelay(Context storage self) internal { - self.rageQuitExtensionDelayStartedAt = Timestamps.now(); - emit RageQuitTimelockStarted(self.rageQuitExtensionDelayStartedAt); + function startRageQuitExtensionPeriod( + Context storage self + ) internal { + self.rageQuitExtensionPeriodStartedAt = Timestamps.now(); + emit RageQuitTimelockStarted(self.rageQuitExtensionPeriodStartedAt); } /// @notice Sets the minimum assets lock duration @@ -108,32 +111,40 @@ library EscrowState { /// @notice Checks if the Escrow is in the SignallingEscrow state /// @param self The context of the Escrow instance - function checkSignallingEscrow(Context storage self) internal view { + function checkSignallingEscrow( + Context storage self + ) internal view { _checkState(self, State.SignallingEscrow); } /// @notice Checks if the Escrow is in the RageQuitEscrow state /// @param self The context of the Escrow instance - function checkRageQuitEscrow(Context storage self) internal view { + function checkRageQuitEscrow( + Context storage self + ) internal view { _checkState(self, State.RageQuitEscrow); } /// @notice Checks if batch claiming is in progress /// @param self The context of the Escrow instance - function checkBatchesClaimingInProgress(Context storage self) internal view { - if (!self.rageQuitExtensionDelayStartedAt.isZero()) { + function checkBatchesClaimingInProgress( + Context storage self + ) internal view { + if (!self.rageQuitExtensionPeriodStartedAt.isZero()) { revert ClaimingIsFinished(); } } /// @notice Checks if the withdrawals timelock has passed /// @param self The context of the Escrow instance - function checkWithdrawalsTimelockPassed(Context storage self) internal view { - if (self.rageQuitExtensionDelayStartedAt.isZero()) { + function checkWithdrawalsTimelockPassed( + Context storage self + ) internal view { + if (self.rageQuitExtensionPeriodStartedAt.isZero()) { revert RageQuitExtraTimelockNotStarted(); } - Duration withdrawalsTimelock = self.rageQuitExtensionDelay + self.rageQuitWithdrawalsTimelock; - if (Timestamps.now() <= withdrawalsTimelock.addTo(self.rageQuitExtensionDelayStartedAt)) { + Duration withdrawalsTimelock = self.rageQuitExtensionPeriodDuration + self.rageQuitWithdrawalsTimelock; + if (Timestamps.now() <= withdrawalsTimelock.addTo(self.rageQuitExtensionPeriodStartedAt)) { revert WithdrawalsTimelockNotPassed(); } } @@ -142,26 +153,32 @@ library EscrowState { // Getters // --- - /// @notice Checks if the rage quit extension delay has started + /// @notice Checks if the rage quit extension period has started /// @param self The context of the Escrow instance - /// @return True if the rage quit extension delay has started, false otherwise - function isRageQuitExtensionDelayStarted(Context storage self) internal view returns (bool) { - return self.rageQuitExtensionDelayStartedAt.isNotZero(); + /// @return True if the rage quit extension period has started, false otherwise + function isRageQuitExtensionPeriodStarted( + Context storage self + ) internal view returns (bool) { + return self.rageQuitExtensionPeriodStartedAt.isNotZero(); } - /// @notice Checks if the rage quit extension delay has passed + /// @notice Checks if the rage quit extension period has passed /// @param self The context of the Escrow instance - /// @return True if the rage quit extension delay has passed, false otherwise - function isRageQuitExtensionDelayPassed(Context storage self) internal view returns (bool) { - Timestamp rageQuitExtensionDelayStartedAt = self.rageQuitExtensionDelayStartedAt; - return rageQuitExtensionDelayStartedAt.isNotZero() - && Timestamps.now() > self.rageQuitExtensionDelay.addTo(rageQuitExtensionDelayStartedAt); + /// @return True if the rage quit extension period has passed, false otherwise + function isRageQuitExtensionPeriodPassed( + Context storage self + ) internal view returns (bool) { + Timestamp rageQuitExtensionPeriodStartedAt = self.rageQuitExtensionPeriodStartedAt; + return rageQuitExtensionPeriodStartedAt.isNotZero() + && Timestamps.now() > self.rageQuitExtensionPeriodDuration.addTo(rageQuitExtensionPeriodStartedAt); } /// @notice Checks if the Escrow is in the RageQuitEscrow state /// @param self The context of the Escrow instance /// @return True if the Escrow is in the RageQuitEscrow state, false otherwise - function isRageQuitEscrow(Context storage self) internal view returns (bool) { + function isRageQuitEscrow( + Context storage self + ) internal view returns (bool) { return self.state == State.RageQuitEscrow; } @@ -194,4 +211,5 @@ library EscrowState { self.minAssetsLockDuration = newMinAssetsLockDuration; emit MinAssetsLockDurationSet(newMinAssetsLockDuration); } + } diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index b9475012..1bc25401 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -338,7 +338,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { escrow.claimNextWithdrawalsBatch(32); } - escrow.startRageQuitExtensionDelay(); + escrow.startRageQuitExtensionPeriod(); assertEq(escrow.isRageQuitFinalized(), false); // --- @@ -393,7 +393,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { vm.expectRevert(); escrow.claimNextWithdrawalsBatch(0, new uint256[](0)); - escrow.startRageQuitExtensionDelay(); + escrow.startRageQuitExtensionPeriod(); assertEq(escrow.isRageQuitFinalized(), false); @@ -431,7 +431,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { escrow.startRageQuit(_RAGE_QUIT_EXTRA_TIMELOCK, _RAGE_QUIT_WITHDRAWALS_TIMELOCK); vm.expectRevert(Escrow.BatchesQueueIsNotClosed.selector); - escrow.startRageQuitExtensionDelay(); + escrow.startRageQuitExtensionPeriod(); escrow.requestNextWithdrawalsBatch(96); @@ -439,11 +439,11 @@ contract EscrowHappyPath is ScenarioTestBlueprint { escrow.claimNextWithdrawalsBatch(0); vm.expectRevert(Escrow.UnfinalizedUnstETHIds.selector); - escrow.startRageQuitExtensionDelay(); + escrow.startRageQuitExtensionPeriod(); _finalizeWithdrawalQueue(); - escrow.startRageQuitExtensionDelay(); + escrow.startRageQuitExtensionPeriod(); uint256[] memory hints = _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index 711e60e5..9d29c8b9 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -76,9 +76,9 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { rageQuitEscrow.claimNextWithdrawalsBatch(128); } - rageQuitEscrow.startRageQuitExtensionDelay(); + rageQuitEscrow.startRageQuitExtensionPeriod(); - _wait(_dualGovernanceConfigProvider.RAGE_QUIT_EXTENSION_DELAY().plusSeconds(1)); + _wait(_dualGovernanceConfigProvider.RAGE_QUIT_EXTENSION_PERIOD_DURATION().plusSeconds(1)); assertTrue(rageQuitEscrow.isRageQuitFinalized()); } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index f560933a..5047a875 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -46,7 +46,7 @@ contract DualGovernanceUnitTests is UnitTest { // vetoCooldownDuration: Durations.from(4 days), // - rageQuitExtensionDelay: Durations.from(7 days), + rageQuitExtensionPeriodDuration: Durations.from(7 days), rageQuitEthWithdrawalsMinDelay: Durations.from(30 days), rageQuitEthWithdrawalsMaxDelay: Durations.from(180 days), rageQuitEthWithdrawalsDelayGrowth: Durations.from(15 days) diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 71b9c704..4095a8bc 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -31,7 +31,7 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { // vetoCooldownDuration: Durations.from(4 days), // - rageQuitExtensionDelay: Durations.from(7 days), + rageQuitExtensionPeriodDuration: Durations.from(7 days), rageQuitEthWithdrawalsMinDelay: Durations.from(30 days), rageQuitEthWithdrawalsMaxDelay: Durations.from(180 days), rageQuitEthWithdrawalsDelayGrowth: Durations.from(15 days) diff --git a/test/unit/libraries/EscrowState.t.sol b/test/unit/libraries/EscrowState.t.sol index 918fbebb..9ed5d215 100644 --- a/test/unit/libraries/EscrowState.t.sol +++ b/test/unit/libraries/EscrowState.t.sol @@ -11,13 +11,16 @@ Duration constant D0 = Durations.ZERO; Timestamp constant T0 = Timestamps.ZERO; contract EscrowStateUnitTests is UnitTest { + EscrowState.Context private _context; // --- // initialize() // --- - function testFuzz_initialize_happyPath(Duration minAssetsLockDuration) external { + function testFuzz_initialize_happyPath( + Duration minAssetsLockDuration + ) external { _context.state = State.NotInitialized; vm.expectEmit(); @@ -29,13 +32,15 @@ contract EscrowStateUnitTests is UnitTest { checkContext({ state: State.SignallingEscrow, minAssetsLockDuration: minAssetsLockDuration, - rageQuitExtensionDelay: D0, + rageQuitExtensionPeriodDuration: D0, rageQuitWithdrawalsTimelock: D0, - rageQuitExtensionDelayStartedAt: T0 + rageQuitExtensionPeriodStartedAt: T0 }); } - function testFuzz_initialize_RevertOn_InvalidState(Duration minAssetsLockDuration) external { + function testFuzz_initialize_RevertOn_InvalidState( + Duration minAssetsLockDuration + ) external { _context.state = State.SignallingEscrow; // TODO: not very informative, maybe need to change to `revert UnexpectedState(self.state);`: UnexpectedState(NotInitialized)[current implementation] => UnexpectedState(SignallingEscrow)[proposed] @@ -49,53 +54,53 @@ contract EscrowStateUnitTests is UnitTest { // --- function testFuzz_startRageQuit_happyPath( - Duration rageQuitExtensionDelay, + Duration rageQuitExtensionPeriodDuration, Duration rageQuitWithdrawalsTimelock ) external { _context.state = State.SignallingEscrow; vm.expectEmit(); emit EscrowState.EscrowStateChanged(State.SignallingEscrow, State.RageQuitEscrow); - emit EscrowState.RageQuitStarted(rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + emit EscrowState.RageQuitStarted(rageQuitExtensionPeriodDuration, rageQuitWithdrawalsTimelock); - EscrowState.startRageQuit(_context, rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + EscrowState.startRageQuit(_context, rageQuitExtensionPeriodDuration, rageQuitWithdrawalsTimelock); checkContext({ state: State.RageQuitEscrow, minAssetsLockDuration: D0, - rageQuitExtensionDelay: rageQuitExtensionDelay, + rageQuitExtensionPeriodDuration: rageQuitExtensionPeriodDuration, rageQuitWithdrawalsTimelock: rageQuitWithdrawalsTimelock, - rageQuitExtensionDelayStartedAt: T0 + rageQuitExtensionPeriodStartedAt: T0 }); } function testFuzz_startRageQuit_RevertOn_InvalidState( - Duration rageQuitExtensionDelay, + Duration rageQuitExtensionPeriodDuration, Duration rageQuitWithdrawalsTimelock ) external { _context.state = State.NotInitialized; vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); - EscrowState.startRageQuit(_context, rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + EscrowState.startRageQuit(_context, rageQuitExtensionPeriodDuration, rageQuitWithdrawalsTimelock); } // --- - // startRageQuitExtensionDelay() + // startRageQuitExtensionPeriod() // --- - function test_startRageQuitExtensionDelay_happyPath() external { + function test_startRageQuitExtensionPeriod_happyPath() external { vm.expectEmit(); emit EscrowState.RageQuitTimelockStarted(Timestamps.now()); - EscrowState.startRageQuitExtensionDelay(_context); + EscrowState.startRageQuitExtensionPeriod(_context); checkContext({ state: State.NotInitialized, minAssetsLockDuration: D0, - rageQuitExtensionDelay: D0, + rageQuitExtensionPeriodDuration: D0, rageQuitWithdrawalsTimelock: D0, - rageQuitExtensionDelayStartedAt: Timestamps.now() + rageQuitExtensionPeriodStartedAt: Timestamps.now() }); } @@ -103,7 +108,9 @@ contract EscrowStateUnitTests is UnitTest { // setMinAssetsLockDuration() // --- - function test_setMinAssetsLockDuration_happyPath(Duration minAssetsLockDuration) external { + function test_setMinAssetsLockDuration_happyPath( + Duration minAssetsLockDuration + ) external { vm.assume(minAssetsLockDuration != Durations.ZERO); vm.expectEmit(); @@ -114,13 +121,15 @@ contract EscrowStateUnitTests is UnitTest { checkContext({ state: State.NotInitialized, minAssetsLockDuration: minAssetsLockDuration, - rageQuitExtensionDelay: D0, + rageQuitExtensionPeriodDuration: D0, rageQuitWithdrawalsTimelock: D0, - rageQuitExtensionDelayStartedAt: T0 + rageQuitExtensionPeriodStartedAt: T0 }); } - function test_setMinAssetsLockDuration_RevertWhen_DurationNotChanged(Duration minAssetsLockDuration) external { + function test_setMinAssetsLockDuration_RevertWhen_DurationNotChanged( + Duration minAssetsLockDuration + ) external { _context.minAssetsLockDuration = minAssetsLockDuration; vm.expectRevert( @@ -167,11 +176,11 @@ contract EscrowStateUnitTests is UnitTest { EscrowState.checkBatchesClaimingInProgress(_context); } - function testFuzz_checkBatchesClaimingInProgress_RevertOn_InvalidState(Timestamp rageQuitExtensionDelayStartedAt) - external - { - vm.assume(rageQuitExtensionDelayStartedAt > Timestamps.ZERO); - _context.rageQuitExtensionDelayStartedAt = rageQuitExtensionDelayStartedAt; + function testFuzz_checkBatchesClaimingInProgress_RevertOn_InvalidState( + Timestamp rageQuitExtensionPeriodStartedAt + ) external { + vm.assume(rageQuitExtensionPeriodStartedAt > Timestamps.ZERO); + _context.rageQuitExtensionPeriodStartedAt = rageQuitExtensionPeriodStartedAt; vm.expectRevert(EscrowState.ClaimingIsFinished.selector); EscrowState.checkBatchesClaimingInProgress(_context); @@ -182,23 +191,23 @@ contract EscrowStateUnitTests is UnitTest { // --- function testFuzz_checkWithdrawalsTimelockPassed_happyPath( - Timestamp rageQuitExtensionDelayStartedAt, - Duration rageQuitExtensionDelay, + Timestamp rageQuitExtensionPeriodStartedAt, + Duration rageQuitExtensionPeriodDuration, Duration rageQuitWithdrawalsTimelock ) external { - vm.assume(rageQuitExtensionDelayStartedAt > Timestamps.ZERO); - vm.assume(rageQuitExtensionDelayStartedAt < Timestamps.from(type(uint16).max)); - vm.assume(rageQuitExtensionDelay < Durations.from(type(uint16).max)); + vm.assume(rageQuitExtensionPeriodStartedAt > Timestamps.ZERO); + vm.assume(rageQuitExtensionPeriodStartedAt < Timestamps.from(type(uint16).max)); + vm.assume(rageQuitExtensionPeriodDuration < Durations.from(type(uint16).max)); vm.assume(rageQuitWithdrawalsTimelock < Durations.from(type(uint16).max)); - _context.rageQuitExtensionDelayStartedAt = rageQuitExtensionDelayStartedAt; - _context.rageQuitExtensionDelay = rageQuitExtensionDelay; + _context.rageQuitExtensionPeriodStartedAt = rageQuitExtensionPeriodStartedAt; + _context.rageQuitExtensionPeriodDuration = rageQuitExtensionPeriodDuration; _context.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; _wait( Durations.between( - (rageQuitExtensionDelay + rageQuitWithdrawalsTimelock).plusSeconds(1).addTo( - rageQuitExtensionDelayStartedAt + (rageQuitExtensionPeriodDuration + rageQuitWithdrawalsTimelock).plusSeconds(1).addTo( + rageQuitExtensionPeriodStartedAt ), Timestamps.now() ) @@ -213,22 +222,22 @@ contract EscrowStateUnitTests is UnitTest { } function testFuzz_checkWithdrawalsTimelockPassed_RevertWhen_WithdrawalsTimelockNotPassed( - Timestamp rageQuitExtensionDelayStartedAt, - Duration rageQuitExtensionDelay, + Timestamp rageQuitExtensionPeriodStartedAt, + Duration rageQuitExtensionPeriodDuration, Duration rageQuitWithdrawalsTimelock ) external { - vm.assume(rageQuitExtensionDelayStartedAt > Timestamps.ZERO); - vm.assume(rageQuitExtensionDelayStartedAt < Timestamps.from(type(uint16).max)); - vm.assume(rageQuitExtensionDelay < Durations.from(type(uint16).max)); + vm.assume(rageQuitExtensionPeriodStartedAt > Timestamps.ZERO); + vm.assume(rageQuitExtensionPeriodStartedAt < Timestamps.from(type(uint16).max)); + vm.assume(rageQuitExtensionPeriodDuration < Durations.from(type(uint16).max)); vm.assume(rageQuitWithdrawalsTimelock < Durations.from(type(uint16).max)); - _context.rageQuitExtensionDelayStartedAt = rageQuitExtensionDelayStartedAt; - _context.rageQuitExtensionDelay = rageQuitExtensionDelay; + _context.rageQuitExtensionPeriodStartedAt = rageQuitExtensionPeriodStartedAt; + _context.rageQuitExtensionPeriodDuration = rageQuitExtensionPeriodDuration; _context.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; _wait( Durations.between( - (rageQuitExtensionDelay + rageQuitWithdrawalsTimelock).addTo(rageQuitExtensionDelayStartedAt), + (rageQuitExtensionPeriodDuration + rageQuitWithdrawalsTimelock).addTo(rageQuitExtensionPeriodStartedAt), Timestamps.now() ) ); @@ -239,11 +248,11 @@ contract EscrowStateUnitTests is UnitTest { } function test_checkWithdrawalsTimelockPassed_RevertWhen_WithdrawalsTimelockOverflow() external { - Duration rageQuitExtensionDelay = Durations.from(DURATION_MAX_VALUE / 2); + Duration rageQuitExtensionPeriodDuration = Durations.from(DURATION_MAX_VALUE / 2); Duration rageQuitWithdrawalsTimelock = Durations.from(DURATION_MAX_VALUE / 2 + 1); - _context.rageQuitExtensionDelayStartedAt = Timestamps.from(MAX_TIMESTAMP_VALUE - 1); - _context.rageQuitExtensionDelay = rageQuitExtensionDelay; + _context.rageQuitExtensionPeriodStartedAt = Timestamps.from(MAX_TIMESTAMP_VALUE - 1); + _context.rageQuitExtensionPeriodDuration = rageQuitExtensionPeriodDuration; _context.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; vm.expectRevert(TimestampOverflow.selector); @@ -252,58 +261,62 @@ contract EscrowStateUnitTests is UnitTest { } // --- - // isRageQuitExtensionDelayStarted() + // isRageQuitExtensionPeriodStarted() // --- - function testFuzz_isRageQuitExtensionDelayStarted_happyPath(Timestamp rageQuitExtensionDelayStartedAt) external { - _context.rageQuitExtensionDelayStartedAt = rageQuitExtensionDelayStartedAt; - bool res = EscrowState.isRageQuitExtensionDelayStarted(_context); - assertEq(res, _context.rageQuitExtensionDelayStartedAt.isNotZero()); + function testFuzz_isRageQuitExtensionDelayStarted_happyPath( + Timestamp rageQuitExtensionPeriodStartedAt + ) external { + _context.rageQuitExtensionPeriodStartedAt = rageQuitExtensionPeriodStartedAt; + bool res = EscrowState.isRageQuitExtensionPeriodStarted(_context); + assertEq(res, _context.rageQuitExtensionPeriodStartedAt.isNotZero()); } // --- - // isRageQuitExtensionDelayPassed() + // isRageQuitExtensionPeriodPassed() // --- - function testFuzz_isRageQuitExtensionDelayPassed_ReturnsTrue( - Timestamp rageQuitExtensionDelayStartedAt, - Duration rageQuitExtensionDelay + function testFuzz_isRageQuitExtensionPeriodPassed_ReturnsTrue( + Timestamp rageQuitExtensionPeriodStartedAt, + Duration rageQuitExtensionPeriodDuration ) external { - vm.assume(rageQuitExtensionDelayStartedAt > Timestamps.ZERO); - vm.assume(rageQuitExtensionDelayStartedAt < Timestamps.from(type(uint16).max)); - vm.assume(rageQuitExtensionDelay < Durations.from(type(uint16).max)); + vm.assume(rageQuitExtensionPeriodStartedAt > Timestamps.ZERO); + vm.assume(rageQuitExtensionPeriodStartedAt < Timestamps.from(type(uint16).max)); + vm.assume(rageQuitExtensionPeriodDuration < Durations.from(type(uint16).max)); - _context.rageQuitExtensionDelayStartedAt = rageQuitExtensionDelayStartedAt; - _context.rageQuitExtensionDelay = rageQuitExtensionDelay; + _context.rageQuitExtensionPeriodStartedAt = rageQuitExtensionPeriodStartedAt; + _context.rageQuitExtensionPeriodDuration = rageQuitExtensionPeriodDuration; _wait( Durations.between( - rageQuitExtensionDelay.plusSeconds(1).addTo(rageQuitExtensionDelayStartedAt), Timestamps.now() + rageQuitExtensionPeriodDuration.plusSeconds(1).addTo(rageQuitExtensionPeriodStartedAt), Timestamps.now() ) ); - bool res = EscrowState.isRageQuitExtensionDelayPassed(_context); + bool res = EscrowState.isRageQuitExtensionPeriodPassed(_context); assertTrue(res); } function testFuzz_isRageQuitExtensionDelayPassed_ReturnsFalse( - Timestamp rageQuitExtensionDelayStartedAt, - Duration rageQuitExtensionDelay + Timestamp rageQuitExtensionPeriodStartedAt, + Duration rageQuitExtensionPeriodDuration ) external { - vm.assume(rageQuitExtensionDelayStartedAt > Timestamps.ZERO); - vm.assume(rageQuitExtensionDelayStartedAt < Timestamps.from(type(uint16).max)); - vm.assume(rageQuitExtensionDelay < Durations.from(type(uint16).max)); + vm.assume(rageQuitExtensionPeriodStartedAt > Timestamps.ZERO); + vm.assume(rageQuitExtensionPeriodStartedAt < Timestamps.from(type(uint16).max)); + vm.assume(rageQuitExtensionPeriodDuration < Durations.from(type(uint16).max)); - _context.rageQuitExtensionDelayStartedAt = rageQuitExtensionDelayStartedAt; - _context.rageQuitExtensionDelay = rageQuitExtensionDelay; + _context.rageQuitExtensionPeriodStartedAt = rageQuitExtensionPeriodStartedAt; + _context.rageQuitExtensionPeriodDuration = rageQuitExtensionPeriodDuration; - _wait(Durations.between(rageQuitExtensionDelay.addTo(rageQuitExtensionDelayStartedAt), Timestamps.now())); - bool res = EscrowState.isRageQuitExtensionDelayPassed(_context); + _wait( + Durations.between(rageQuitExtensionPeriodDuration.addTo(rageQuitExtensionPeriodStartedAt), Timestamps.now()) + ); + bool res = EscrowState.isRageQuitExtensionPeriodPassed(_context); assertFalse(res); } function test_isRageQuitExtensionDelayPassed_ReturnsFalseWhenRageQuitExtraTimelockNotStarted() external { _wait(Durations.from(1234)); - bool res = EscrowState.isRageQuitExtensionDelayPassed(_context); + bool res = EscrowState.isRageQuitExtensionPeriodPassed(_context); assertFalse(res); } @@ -311,7 +324,9 @@ contract EscrowStateUnitTests is UnitTest { // isRageQuitEscrow() // --- - function testFuzz_isRageQuitEscrow(bool expectedResult) external { + function testFuzz_isRageQuitEscrow( + bool expectedResult + ) external { if (expectedResult) { _context.state = State.RageQuitEscrow; } @@ -326,18 +341,19 @@ contract EscrowStateUnitTests is UnitTest { function checkContext( State state, Duration minAssetsLockDuration, - Duration rageQuitExtensionDelay, + Duration rageQuitExtensionPeriodDuration, Duration rageQuitWithdrawalsTimelock, - Timestamp rageQuitExtensionDelayStartedAt + Timestamp rageQuitExtensionPeriodStartedAt ) internal { assertEq(_context.state, state); assertEq(_context.minAssetsLockDuration, minAssetsLockDuration); - assertEq(_context.rageQuitExtensionDelay, rageQuitExtensionDelay); + assertEq(_context.rageQuitExtensionPeriodDuration, rageQuitExtensionPeriodDuration); assertEq(_context.rageQuitWithdrawalsTimelock, rageQuitWithdrawalsTimelock); - assertEq(_context.rageQuitExtensionDelayStartedAt, rageQuitExtensionDelayStartedAt); + assertEq(_context.rageQuitExtensionPeriodStartedAt, rageQuitExtensionPeriodStartedAt); } function assertEq(State a, State b) internal { assertEq(uint256(a), uint256(b)); } + } diff --git a/test/utils/SetupDeployment.sol b/test/utils/SetupDeployment.sol index 88310c80..555d85c2 100644 --- a/test/utils/SetupDeployment.sol +++ b/test/utils/SetupDeployment.sol @@ -380,7 +380,7 @@ abstract contract SetupDeployment is Test { vetoSignallingDeactivationMaxDuration: Durations.from(5 days), vetoCooldownDuration: Durations.from(4 days), // - rageQuitExtensionDelay: Durations.from(7 days), + rageQuitExtensionPeriodDuration: Durations.from(7 days), rageQuitEthWithdrawalsMinDelay: Durations.from(30 days), rageQuitEthWithdrawalsMaxDelay: Durations.from(180 days), rageQuitEthWithdrawalsDelayGrowth: Durations.from(15 days) From 07e2de488ce542d01b6c51c5a78dc097ef54e862 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 4 Sep 2024 04:20:22 +0400 Subject: [PATCH 25/86] Update Escrow & EscrowState to match new config names --- contracts/Escrow.sol | 8 +-- contracts/libraries/AssetsAccounting.sol | 16 +++--- contracts/libraries/EscrowState.sol | 32 +++++------ test/unit/libraries/EscrowState.t.sol | 68 ++++++++++++------------ 4 files changed, 63 insertions(+), 61 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 460151b7..e6e8de03 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -257,9 +257,9 @@ contract Escrow is IEscrow { // Start rage quit // --- - function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitWithdrawalsTimelock) external { + function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitEthWithdrawalsDelay) external { _checkCallerIsDualGovernance(); - _escrowState.startRageQuit(rageQuitExtensionPeriodDuration, rageQuitWithdrawalsTimelock); + _escrowState.startRageQuit(rageQuitExtensionPeriodDuration, rageQuitEthWithdrawalsDelay); _batchesQueue.open(WITHDRAWAL_QUEUE.getLastRequestId()); } @@ -388,7 +388,7 @@ contract Escrow is IEscrow { function withdrawETH() external { _escrowState.checkRageQuitEscrow(); - _escrowState.checkWithdrawalsTimelockPassed(); + _escrowState.checkEthWithdrawalsDelayPassed(); ETHValue ethToWithdraw = _accounting.accountStETHSharesWithdraw(msg.sender); ethToWithdraw.sendTo(payable(msg.sender)); } @@ -400,7 +400,7 @@ contract Escrow is IEscrow { revert EmptyUnstETHIds(); } _escrowState.checkRageQuitEscrow(); - _escrowState.checkWithdrawalsTimelockPassed(); + _escrowState.checkEthWithdrawalsDelayPassed(); ETHValue ethToWithdraw = _accounting.accountUnstETHWithdraw(msg.sender, unstETHIds); ethToWithdraw.sendTo(payable(msg.sender)); } diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index 93bb42f2..363a13ff 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -83,6 +83,7 @@ struct UnstETHRecord { /// @notice Provides functionality for accounting user stETH and unstETH tokens /// locked in the Escrow contract library AssetsAccounting { + /// @notice The context of the AssetsAccounting library /// @param stETHTotals The total number of shares and the amount of stETH locked by users /// @param unstETHTotals The total number of shares and the amount of unstETH locked by users @@ -123,7 +124,7 @@ library AssetsAccounting { error InvalidSharesValue(SharesValue value); error InvalidUnstETHStatus(uint256 unstETHId, UnstETHRecordStatus status); error InvalidUnstETHHolder(uint256 unstETHId, address actual, address expected); - error MinAssetsLockDurationNotPassed(Timestamp unlockTimelockExpiresAt); + error MinAssetsLockDurationNotPassed(Timestamp lockDurationExpiresAt); error InvalidClaimableAmount(uint256 unstETHId, ETHValue expected, ETHValue actual); // --- @@ -278,11 +279,9 @@ library AssetsAccounting { // Getters // --- - function getLockedAssetsTotals(Context storage self) - internal - view - returns (SharesValue unfinalizedShares, ETHValue finalizedETH) - { + function getLockedAssetsTotals( + Context storage self + ) internal view returns (SharesValue unfinalizedShares, ETHValue finalizedETH) { finalizedETH = self.unstETHTotals.finalizedETH; unfinalizedShares = self.stETHTotals.lockedShares + self.unstETHTotals.unfinalizedShares; } @@ -417,9 +416,12 @@ library AssetsAccounting { amountWithdrawn = unstETHRecord.claimableAmount; } - function _checkNonZeroShares(SharesValue shares) private pure { + function _checkNonZeroShares( + SharesValue shares + ) private pure { if (shares == SharesValues.ZERO) { revert InvalidSharesValue(SharesValues.ZERO); } } + } diff --git a/contracts/libraries/EscrowState.sol b/contracts/libraries/EscrowState.sol index da6c9944..66a3b8f3 100644 --- a/contracts/libraries/EscrowState.sol +++ b/contracts/libraries/EscrowState.sol @@ -28,18 +28,18 @@ library EscrowState { error ClaimingIsFinished(); error UnexpectedState(State value); - error RageQuitExtraTimelockNotStarted(); - error WithdrawalsTimelockNotPassed(); + error EthWithdrawalsDelayNotPassed(); + error RageQuitExtensionPeriodNotStarted(); error InvalidMinAssetsLockDuration(Duration newMinAssetsLockDuration); // --- // Events // --- - event RageQuitTimelockStarted(Timestamp startedAt); event EscrowStateChanged(State from, State to); - event RageQuitStarted(Duration rageQuitExtensionDuration, Duration rageQuitWithdrawalsTimelock); + event RageQuitExtensionPeriodStarted(Timestamp startedAt); event MinAssetsLockDurationSet(Duration newAssetsLockDuration); + event RageQuitStarted(Duration rageQuitExtensionDuration, Duration rageQuitEthWithdrawalsDelay); /// @notice Stores the context of the state of the Escrow instance /// @param state The current state of the Escrow instance @@ -58,7 +58,7 @@ library EscrowState { /// @dev slot0: [72..111] Timestamp rageQuitExtensionPeriodStartedAt; /// @dev slot0: [112..143] - Duration rageQuitWithdrawalsTimelock; + Duration rageQuitEthWithdrawalsDelay; } /// @notice Initializes the Escrow state to SignallingEscrow @@ -73,17 +73,17 @@ library EscrowState { /// @notice Starts the rage quit process /// @param self The context of the Escrow instance /// @param rageQuitExtensionPeriodDuration The duration of the period for the rage quit extension - /// @param rageQuitWithdrawalsTimelock The timelock period for rage quit withdrawals + /// @param rageQuitEthWithdrawalsDelay The delay for rage quit withdrawals function startRageQuit( Context storage self, Duration rageQuitExtensionPeriodDuration, - Duration rageQuitWithdrawalsTimelock + Duration rageQuitEthWithdrawalsDelay ) internal { _checkState(self, State.SignallingEscrow); _setState(self, State.RageQuitEscrow); self.rageQuitExtensionPeriodDuration = rageQuitExtensionPeriodDuration; - self.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; - emit RageQuitStarted(rageQuitExtensionPeriodDuration, rageQuitWithdrawalsTimelock); + self.rageQuitEthWithdrawalsDelay = rageQuitEthWithdrawalsDelay; + emit RageQuitStarted(rageQuitExtensionPeriodDuration, rageQuitEthWithdrawalsDelay); } /// @notice Starts the rage quit extension period @@ -92,7 +92,7 @@ library EscrowState { Context storage self ) internal { self.rageQuitExtensionPeriodStartedAt = Timestamps.now(); - emit RageQuitTimelockStarted(self.rageQuitExtensionPeriodStartedAt); + emit RageQuitExtensionPeriodStarted(self.rageQuitExtensionPeriodStartedAt); } /// @notice Sets the minimum assets lock duration @@ -135,17 +135,17 @@ library EscrowState { } } - /// @notice Checks if the withdrawals timelock has passed + /// @notice Checks if the withdrawals delay has passed /// @param self The context of the Escrow instance - function checkWithdrawalsTimelockPassed( + function checkEthWithdrawalsDelayPassed( Context storage self ) internal view { if (self.rageQuitExtensionPeriodStartedAt.isZero()) { - revert RageQuitExtraTimelockNotStarted(); + revert RageQuitExtensionPeriodNotStarted(); } - Duration withdrawalsTimelock = self.rageQuitExtensionPeriodDuration + self.rageQuitWithdrawalsTimelock; - if (Timestamps.now() <= withdrawalsTimelock.addTo(self.rageQuitExtensionPeriodStartedAt)) { - revert WithdrawalsTimelockNotPassed(); + Duration ethWithdrawalsDelay = self.rageQuitExtensionPeriodDuration + self.rageQuitEthWithdrawalsDelay; + if (Timestamps.now() <= ethWithdrawalsDelay.addTo(self.rageQuitExtensionPeriodStartedAt)) { + revert EthWithdrawalsDelayNotPassed(); } } diff --git a/test/unit/libraries/EscrowState.t.sol b/test/unit/libraries/EscrowState.t.sol index 9ed5d215..2c5d4510 100644 --- a/test/unit/libraries/EscrowState.t.sol +++ b/test/unit/libraries/EscrowState.t.sol @@ -33,7 +33,7 @@ contract EscrowStateUnitTests is UnitTest { state: State.SignallingEscrow, minAssetsLockDuration: minAssetsLockDuration, rageQuitExtensionPeriodDuration: D0, - rageQuitWithdrawalsTimelock: D0, + rageQuitEthWithdrawalsDelay: D0, rageQuitExtensionPeriodStartedAt: T0 }); } @@ -55,34 +55,34 @@ contract EscrowStateUnitTests is UnitTest { function testFuzz_startRageQuit_happyPath( Duration rageQuitExtensionPeriodDuration, - Duration rageQuitWithdrawalsTimelock + Duration rageQuitEthWithdrawalsDelay ) external { _context.state = State.SignallingEscrow; vm.expectEmit(); emit EscrowState.EscrowStateChanged(State.SignallingEscrow, State.RageQuitEscrow); - emit EscrowState.RageQuitStarted(rageQuitExtensionPeriodDuration, rageQuitWithdrawalsTimelock); + emit EscrowState.RageQuitStarted(rageQuitExtensionPeriodDuration, rageQuitEthWithdrawalsDelay); - EscrowState.startRageQuit(_context, rageQuitExtensionPeriodDuration, rageQuitWithdrawalsTimelock); + EscrowState.startRageQuit(_context, rageQuitExtensionPeriodDuration, rageQuitEthWithdrawalsDelay); checkContext({ state: State.RageQuitEscrow, minAssetsLockDuration: D0, rageQuitExtensionPeriodDuration: rageQuitExtensionPeriodDuration, - rageQuitWithdrawalsTimelock: rageQuitWithdrawalsTimelock, + rageQuitEthWithdrawalsDelay: rageQuitEthWithdrawalsDelay, rageQuitExtensionPeriodStartedAt: T0 }); } function testFuzz_startRageQuit_RevertOn_InvalidState( Duration rageQuitExtensionPeriodDuration, - Duration rageQuitWithdrawalsTimelock + Duration rageQuitEthWithdrawalsDelay ) external { _context.state = State.NotInitialized; vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); - EscrowState.startRageQuit(_context, rageQuitExtensionPeriodDuration, rageQuitWithdrawalsTimelock); + EscrowState.startRageQuit(_context, rageQuitExtensionPeriodDuration, rageQuitEthWithdrawalsDelay); } // --- @@ -91,7 +91,7 @@ contract EscrowStateUnitTests is UnitTest { function test_startRageQuitExtensionPeriod_happyPath() external { vm.expectEmit(); - emit EscrowState.RageQuitTimelockStarted(Timestamps.now()); + emit EscrowState.RageQuitExtensionPeriodStarted(Timestamps.now()); EscrowState.startRageQuitExtensionPeriod(_context); @@ -99,7 +99,7 @@ contract EscrowStateUnitTests is UnitTest { state: State.NotInitialized, minAssetsLockDuration: D0, rageQuitExtensionPeriodDuration: D0, - rageQuitWithdrawalsTimelock: D0, + rageQuitEthWithdrawalsDelay: D0, rageQuitExtensionPeriodStartedAt: Timestamps.now() }); } @@ -122,7 +122,7 @@ contract EscrowStateUnitTests is UnitTest { state: State.NotInitialized, minAssetsLockDuration: minAssetsLockDuration, rageQuitExtensionPeriodDuration: D0, - rageQuitWithdrawalsTimelock: D0, + rageQuitEthWithdrawalsDelay: D0, rageQuitExtensionPeriodStartedAt: T0 }); } @@ -187,77 +187,77 @@ contract EscrowStateUnitTests is UnitTest { } // --- - // checkWithdrawalsTimelockPassed() + // checkEthWithdrawalsDelayPassed() // --- - function testFuzz_checkWithdrawalsTimelockPassed_happyPath( + function testFuzz_checkWithdrawalsDelayPassed_happyPath( Timestamp rageQuitExtensionPeriodStartedAt, Duration rageQuitExtensionPeriodDuration, - Duration rageQuitWithdrawalsTimelock + Duration rageQuitEthWithdrawalsDelay ) external { vm.assume(rageQuitExtensionPeriodStartedAt > Timestamps.ZERO); vm.assume(rageQuitExtensionPeriodStartedAt < Timestamps.from(type(uint16).max)); vm.assume(rageQuitExtensionPeriodDuration < Durations.from(type(uint16).max)); - vm.assume(rageQuitWithdrawalsTimelock < Durations.from(type(uint16).max)); + vm.assume(rageQuitEthWithdrawalsDelay < Durations.from(type(uint16).max)); _context.rageQuitExtensionPeriodStartedAt = rageQuitExtensionPeriodStartedAt; _context.rageQuitExtensionPeriodDuration = rageQuitExtensionPeriodDuration; - _context.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; + _context.rageQuitEthWithdrawalsDelay = rageQuitEthWithdrawalsDelay; _wait( Durations.between( - (rageQuitExtensionPeriodDuration + rageQuitWithdrawalsTimelock).plusSeconds(1).addTo( + (rageQuitExtensionPeriodDuration + rageQuitEthWithdrawalsDelay).plusSeconds(1).addTo( rageQuitExtensionPeriodStartedAt ), Timestamps.now() ) ); - EscrowState.checkWithdrawalsTimelockPassed(_context); + EscrowState.checkEthWithdrawalsDelayPassed(_context); } - function test_checkWithdrawalsTimelockPassed_RevertWhen_RageQuitExtraTimelockNotStarted() external { - vm.expectRevert(EscrowState.RageQuitExtraTimelockNotStarted.selector); + function test_checkEthWithdrawalsDelayPassed_RevertWhen_RageQuitExtensionPeriodNotStarted() external { + vm.expectRevert(EscrowState.RageQuitExtensionPeriodNotStarted.selector); - EscrowState.checkWithdrawalsTimelockPassed(_context); + EscrowState.checkEthWithdrawalsDelayPassed(_context); } - function testFuzz_checkWithdrawalsTimelockPassed_RevertWhen_WithdrawalsTimelockNotPassed( + function testFuzz_checkWithdrawalsDelayPassed_RevertWhen_EthWithdrawalsDelayNotPassed( Timestamp rageQuitExtensionPeriodStartedAt, Duration rageQuitExtensionPeriodDuration, - Duration rageQuitWithdrawalsTimelock + Duration rageQuitEthWithdrawalsDelay ) external { vm.assume(rageQuitExtensionPeriodStartedAt > Timestamps.ZERO); vm.assume(rageQuitExtensionPeriodStartedAt < Timestamps.from(type(uint16).max)); vm.assume(rageQuitExtensionPeriodDuration < Durations.from(type(uint16).max)); - vm.assume(rageQuitWithdrawalsTimelock < Durations.from(type(uint16).max)); + vm.assume(rageQuitEthWithdrawalsDelay < Durations.from(type(uint16).max)); _context.rageQuitExtensionPeriodStartedAt = rageQuitExtensionPeriodStartedAt; _context.rageQuitExtensionPeriodDuration = rageQuitExtensionPeriodDuration; - _context.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; + _context.rageQuitEthWithdrawalsDelay = rageQuitEthWithdrawalsDelay; _wait( Durations.between( - (rageQuitExtensionPeriodDuration + rageQuitWithdrawalsTimelock).addTo(rageQuitExtensionPeriodStartedAt), + (rageQuitExtensionPeriodDuration + rageQuitEthWithdrawalsDelay).addTo(rageQuitExtensionPeriodStartedAt), Timestamps.now() ) ); - vm.expectRevert(EscrowState.WithdrawalsTimelockNotPassed.selector); + vm.expectRevert(EscrowState.EthWithdrawalsDelayNotPassed.selector); - EscrowState.checkWithdrawalsTimelockPassed(_context); + EscrowState.checkEthWithdrawalsDelayPassed(_context); } - function test_checkWithdrawalsTimelockPassed_RevertWhen_WithdrawalsTimelockOverflow() external { + function test_checkWithdrawalsDelayPassed_RevertWhen_EthWithdrawalsDelayOverflow() external { Duration rageQuitExtensionPeriodDuration = Durations.from(DURATION_MAX_VALUE / 2); - Duration rageQuitWithdrawalsTimelock = Durations.from(DURATION_MAX_VALUE / 2 + 1); + Duration rageQuitEthWithdrawalsDelay = Durations.from(DURATION_MAX_VALUE / 2 + 1); _context.rageQuitExtensionPeriodStartedAt = Timestamps.from(MAX_TIMESTAMP_VALUE - 1); _context.rageQuitExtensionPeriodDuration = rageQuitExtensionPeriodDuration; - _context.rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; + _context.rageQuitEthWithdrawalsDelay = rageQuitEthWithdrawalsDelay; vm.expectRevert(TimestampOverflow.selector); - EscrowState.checkWithdrawalsTimelockPassed(_context); + EscrowState.checkEthWithdrawalsDelayPassed(_context); } // --- @@ -314,7 +314,7 @@ contract EscrowStateUnitTests is UnitTest { assertFalse(res); } - function test_isRageQuitExtensionDelayPassed_ReturnsFalseWhenRageQuitExtraTimelockNotStarted() external { + function test_isRageQuitExtensionDelayPassed_ReturnsFalseWhenRageQuitExtensionPeriodNotStarted() external { _wait(Durations.from(1234)); bool res = EscrowState.isRageQuitExtensionPeriodPassed(_context); assertFalse(res); @@ -342,13 +342,13 @@ contract EscrowStateUnitTests is UnitTest { State state, Duration minAssetsLockDuration, Duration rageQuitExtensionPeriodDuration, - Duration rageQuitWithdrawalsTimelock, + Duration rageQuitEthWithdrawalsDelay, Timestamp rageQuitExtensionPeriodStartedAt ) internal { assertEq(_context.state, state); assertEq(_context.minAssetsLockDuration, minAssetsLockDuration); assertEq(_context.rageQuitExtensionPeriodDuration, rageQuitExtensionPeriodDuration); - assertEq(_context.rageQuitWithdrawalsTimelock, rageQuitWithdrawalsTimelock); + assertEq(_context.rageQuitEthWithdrawalsDelay, rageQuitEthWithdrawalsDelay); assertEq(_context.rageQuitExtensionPeriodStartedAt, rageQuitExtensionPeriodStartedAt); } From 3b7724896adfc84bd990b6d600c2988c5a10ee64 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 5 Sep 2024 14:22:38 +0300 Subject: [PATCH 26/86] fix scheduling proposal on vote --- contracts/CommitteesFactory.sol | 55 ++++++++++++++++++++++++++ contracts/committees/HashConsensus.sol | 16 +++++--- test/unit/HashConsensus.t.sol | 35 ++++++++++------ 3 files changed, 87 insertions(+), 19 deletions(-) create mode 100644 contracts/CommitteesFactory.sol diff --git a/contracts/CommitteesFactory.sol b/contracts/CommitteesFactory.sol new file mode 100644 index 00000000..64935e5e --- /dev/null +++ b/contracts/CommitteesFactory.sol @@ -0,0 +1,55 @@ +pragma solidity 0.8.26; + +import {EmergencyActivationCommittee} from "./committees/EmergencyActivationCommittee.sol"; +import {TiebreakerSubCommittee} from "./committees/TiebreakerSubCommittee.sol"; +import {EmergencyActivationCommittee} from "./committees/EmergencyActivationCommittee.sol"; +import {TiebreakerCore} from "./committees/TiebreakerCore.sol"; +import {ResealCommittee} from "./committees/ResealCommittee.sol"; +import {Duration} from "./types/Duration.sol"; + +contract CommitteesFactory { + function createEmergencyActivationCommittee( + address owner, + address[] memory committeeMembers, + uint256 executionQuorum, + address emergencyProtectedTimelock + ) external returns (EmergencyActivationCommittee) { + return new EmergencyActivationCommittee(owner, committeeMembers, executionQuorum, emergencyProtectedTimelock); + } + + function createEmergencyExecutionCommittee( + address owner, + address[] memory committeeMembers, + uint256 executionQuorum, + address emergencyProtectedTimelock + ) external returns (EmergencyActivationCommittee) { + return new EmergencyActivationCommittee(owner, committeeMembers, executionQuorum, emergencyProtectedTimelock); + } + + function createTiebreakerCore( + address owner, + address dualGovernance, + Duration timelock + ) external returns (TiebreakerCore) { + return new TiebreakerCore(owner, dualGovernance, timelock); + } + + function createTiebreakerSubCommittee( + address owner, + address[] memory committeeMembers, + uint256 executionQuorum, + address tiebreakerCore + ) external returns (TiebreakerSubCommittee) { + return new TiebreakerSubCommittee(owner, committeeMembers, executionQuorum, tiebreakerCore); + } + + function createResealCommittee( + address owner, + address[] memory committeeMembers, + uint256 executionQuorum, + address dualGovernance, + Duration timelock + ) external returns (ResealCommittee) { + return new ResealCommittee(owner, committeeMembers, executionQuorum, dualGovernance, timelock); + } +} diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index c0e8a1f0..8eabeaf3 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -51,17 +51,21 @@ abstract contract HashConsensus is Ownable { /// @param hash The hash to vote on /// @param support Indicates whether the member supports the hash function _vote(bytes32 hash, bool support) internal { - if (_hashStates[hash].usedAt > Timestamps.from(0)) { + if (_hashStates[hash].usedAt.isNotZero()) { revert HashAlreadyUsed(hash); } + if (_hashStates[hash].scheduledAt.isNotZero()) { + revert ProposalAlreadyScheduled(hash); + } + if (approves[msg.sender][hash] == support) { return; } uint256 heads = _getSupport(hash); // heads compares to quorum - 1 because the current vote is not counted yet - if (heads >= quorum - 1 && support == true && _hashStates[hash].scheduledAt == Timestamps.from(0)) { + if (heads >= quorum - 1 && support == true) { _hashStates[hash].scheduledAt = Timestamps.from(block.timestamp); } @@ -73,7 +77,7 @@ abstract contract HashConsensus is Ownable { /// @dev Internal function that handles marking a hash as used /// @param hash The hash to mark as used function _markUsed(bytes32 hash) internal { - if (_hashStates[hash].usedAt > Timestamps.from(0)) { + if (_hashStates[hash].usedAt.isNotZero()) { revert HashAlreadyUsed(hash); } @@ -106,7 +110,7 @@ abstract contract HashConsensus is Ownable { support = _getSupport(hash); executionQuorum = quorum; scheduledAt = _hashStates[hash].scheduledAt; - isUsed = _hashStates[hash].usedAt > Timestamps.from(0); + isUsed = _hashStates[hash].usedAt.isNotZero(); } /// @notice Adds new members to the contract and sets the execution quorum. @@ -183,14 +187,14 @@ abstract contract HashConsensus is Ownable { /// current support of the proposal. /// @param hash The hash of the proposal to be scheduled function schedule(bytes32 hash) public { - if (_hashStates[hash].usedAt > Timestamps.from(0)) { + if (_hashStates[hash].usedAt.isNotZero()) { revert HashAlreadyUsed(hash); } if (_getSupport(hash) < quorum) { revert QuorumIsNotReached(); } - if (_hashStates[hash].scheduledAt > Timestamps.from(0)) { + if (_hashStates[hash].scheduledAt.isNotZero()) { revert ProposalAlreadyScheduled(hash); } diff --git a/test/unit/HashConsensus.t.sol b/test/unit/HashConsensus.t.sol index 987c8624..65b330f2 100644 --- a/test/unit/HashConsensus.t.sol +++ b/test/unit/HashConsensus.t.sol @@ -409,20 +409,20 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { function test_getSupport() public { assertEq(_hashConsensusWrapper.getSupport(dataHash), 0); - for (uint256 i = 0; i < _membersCount; ++i) { + for (uint256 i = 0; i < _quorum - 1; ++i) { assertEq(_hashConsensusWrapper.getSupport(dataHash), i); vm.prank(_committeeMembers[i]); _hashConsensusWrapper.vote(dataHash, true); assertEq(_hashConsensusWrapper.getSupport(dataHash), i + 1); } - assertEq(_hashConsensusWrapper.getSupport(dataHash), _membersCount); + assertEq(_hashConsensusWrapper.getSupport(dataHash), _quorum - 1); - for (uint256 i = 0; i < _membersCount; ++i) { - assertEq(_hashConsensusWrapper.getSupport(dataHash), _membersCount - i); + for (uint256 i = 0; i < _quorum - 1; ++i) { + assertEq(_hashConsensusWrapper.getSupport(dataHash), _quorum - 1 - i); vm.prank(_committeeMembers[i]); _hashConsensusWrapper.vote(dataHash, false); - assertEq(_hashConsensusWrapper.getSupport(dataHash), _membersCount - i - 1); + assertEq(_hashConsensusWrapper.getSupport(dataHash), _quorum - 2 - i); } assertEq(_hashConsensusWrapper.getSupport(dataHash), 0); @@ -442,15 +442,11 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { Timestamp expectedQuorumAt = Timestamps.from(block.timestamp); - for (uint256 i = 0; i < _membersCount; ++i) { + for (uint256 i = 0; i < _quorum; ++i) { (support, executionQuorum, scheduledAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); assertEq(support, i); assertEq(executionQuorum, _quorum); - if (i >= executionQuorum) { - assertEq(scheduledAt, expectedQuorumAt); - } else { - assertEq(scheduledAt, Timestamps.from(0)); - } + assertEq(scheduledAt, Timestamps.from(0)); assertEq(isExecuted, false); vm.prank(_committeeMembers[i]); @@ -468,7 +464,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { } (support, executionQuorum, scheduledAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); - assertEq(support, _membersCount); + assertEq(support, _quorum); assertEq(executionQuorum, _quorum); assertEq(scheduledAt, expectedQuorumAt); assertEq(isExecuted, false); @@ -478,7 +474,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { _hashConsensusWrapper.execute(dataHash); (support, executionQuorum, scheduledAt, isExecuted) = _hashConsensusWrapper.getHashState(dataHash); - assertEq(support, _membersCount); + assertEq(support, _quorum); assertEq(executionQuorum, _quorum); assertEq(scheduledAt, expectedQuorumAt); assertEq(isExecuted, true); @@ -529,6 +525,19 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { _hashConsensusWrapper.vote(dataHash, true); } + function test_vote_RevertOn_IfProposalAlreadyScheduled() public { + bytes32 hash = keccak256("hash"); + + for (uint256 i = 0; i < _quorum; ++i) { + vm.prank(_committeeMembers[i]); + _hashConsensusWrapper.vote(hash, true); + } + + vm.expectRevert(abi.encodeWithSelector(HashConsensus.ProposalAlreadyScheduled.selector, hash)); + vm.prank(_committeeMembers[_quorum]); + _hashConsensusWrapper.vote(hash, true); + } + function test_execute_events() public { vm.prank(_stranger); vm.expectRevert(abi.encodeWithSignature("QuorumIsNotReached()")); From e1632a0094508941ad2d02508209c76ec13cae64 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 5 Sep 2024 15:35:36 +0300 Subject: [PATCH 27/86] fix: check proposal existence while committees voting --- .../EmergencyExecutionCommittee.sol | 11 ++++++ contracts/committees/TiebreakerCore.sol | 13 +++++++ .../committees/TiebreakerSubCommittee.sol | 1 + contracts/interfaces/IDualGovernance.sol | 2 ++ contracts/interfaces/ITiebreaker.sol | 1 + contracts/interfaces/ITimelock.sol | 1 + .../EmergencyExecutionCommittee.t.sol | 25 ++++++++++++- test/unit/committees/TiebreakerCore.t.sol | 35 ++++++++++++++++++- .../committees/TiebreakerSubCommittee.t.sol | 28 ++++++++++++++- 9 files changed, 114 insertions(+), 3 deletions(-) diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol index ea6b5622..3bcd8d31 100644 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -17,6 +17,8 @@ enum ProposalType { /// @notice This contract allows a committee to vote on and execute emergency proposals /// @dev Inherits from HashConsensus for voting mechanisms and ProposalsList for proposal management contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { + error ProposalDoesNotExist(uint256 proposalId); + address public immutable EMERGENCY_PROTECTED_TIMELOCK; constructor( @@ -40,6 +42,7 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { /// @param _supports Indicates whether the member supports the proposal execution function voteEmergencyExecute(uint256 proposalId, bool _supports) public { _checkCallerIsMember(); + _checkProposalExists(proposalId); (bytes memory proposalData, bytes32 key) = _encodeEmergencyExecute(proposalId); _vote(key, _supports); _pushProposal(key, uint256(ProposalType.EmergencyExecute), proposalData); @@ -70,6 +73,14 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { ); } + /// @notice Checks if a proposal exists + /// @param proposalId The ID of the proposal to check + function _checkProposalExists(uint256 proposalId) internal view { + if (proposalId > ITimelock(EMERGENCY_PROTECTED_TIMELOCK).getProposalsCount()) { + revert ProposalDoesNotExist(proposalId); + } + } + /// @dev Encodes the proposal data and generates the proposal key for an emergency execution /// @param proposalId The ID of the proposal to encode /// @return proposalData The encoded proposal data diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol index bcb75d7a..acdd0cc5 100644 --- a/contracts/committees/TiebreakerCore.sol +++ b/contracts/committees/TiebreakerCore.sol @@ -5,6 +5,7 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {ITiebreakerCore} from "../interfaces/ITiebreaker.sol"; import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; +import {ITimelock} from "../interfaces/ITimelock.sol"; import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; import {Timestamp} from "../types/Timestamp.sol"; @@ -20,6 +21,7 @@ enum ProposalType { /// @dev Inherits from HashConsensus for voting mechanisms and ProposalsList for proposal management contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { error ResumeSealableNonceMismatch(); + error ProposalDoesNotExist(uint256 proposalId); address immutable DUAL_GOVERNANCE; @@ -38,6 +40,7 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { /// @param proposalId The ID of the proposal to schedule function scheduleProposal(uint256 proposalId) public { _checkCallerIsMember(); + checkProposalExists(proposalId); (bytes memory proposalData, bytes32 key) = _encodeScheduleProposal(proposalId); _vote(key, true); _pushProposal(key, uint256(ProposalType.ScheduleProposal), proposalData); @@ -70,6 +73,16 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { ); } + /// @notice Checks if a proposal exists + /// @param proposalId The ID of the proposal to check + function checkProposalExists(uint256 proposalId) public view { + ITimelock timelock = IDualGovernance(DUAL_GOVERNANCE).TIMELOCK(); + + if (proposalId > timelock.getProposalsCount()) { + revert ProposalDoesNotExist(proposalId); + } + } + /// @notice Encodes a schedule proposal /// @dev Internal function to encode the proposal data and generate the proposal key /// @param proposalId The ID of the proposal to schedule diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol index e4113143..4d70a551 100644 --- a/contracts/committees/TiebreakerSubCommittee.sol +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -40,6 +40,7 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { /// @param proposalId The ID of the proposal to schedule function scheduleProposal(uint256 proposalId) public { _checkCallerIsMember(); + ITiebreakerCore(TIEBREAKER_CORE).checkProposalExists(proposalId); (bytes memory proposalData, bytes32 key) = _encodeApproveProposal(proposalId); _vote(key, true); _pushProposal(key, uint256(ProposalType.ScheduleProposal), proposalData); diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 1ee4e000..ccb633c1 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.26; import {IGovernance} from "./IGovernance.sol"; +import {ITimelock} from "./ITimelock.sol"; interface IDualGovernance is IGovernance { function activateNextState() external; @@ -10,4 +11,5 @@ interface IDualGovernance is IGovernance { function tiebreakerScheduleProposal(uint256 proposalId) external; function tiebreakerResumeSealable(address sealable) external; + function TIMELOCK() external view returns (ITimelock); } diff --git a/contracts/interfaces/ITiebreaker.sol b/contracts/interfaces/ITiebreaker.sol index 0993fc31..8cdfc782 100644 --- a/contracts/interfaces/ITiebreaker.sol +++ b/contracts/interfaces/ITiebreaker.sol @@ -5,4 +5,5 @@ interface ITiebreakerCore { function getSealableResumeNonce(address sealable) external view returns (uint256 nonce); function scheduleProposal(uint256 _proposalId) external; function sealableResume(address sealable, uint256 nonce) external; + function checkProposalExists(uint256 _proposalId) external view; } diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index f3c192f0..c4ebff3e 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -38,4 +38,5 @@ interface ITimelock { function activateEmergencyMode() external; function emergencyExecute(uint256 proposalId) external; function emergencyReset() external; + function getProposalsCount() external view returns (uint256 count); } diff --git a/test/unit/committees/EmergencyExecutionCommittee.t.sol b/test/unit/committees/EmergencyExecutionCommittee.t.sol index 6691b35b..bd44430a 100644 --- a/test/unit/committees/EmergencyExecutionCommittee.t.sol +++ b/test/unit/committees/EmergencyExecutionCommittee.t.sol @@ -10,6 +10,18 @@ import {ITimelock} from "contracts/interfaces/ITimelock.sol"; import {TargetMock} from "test/utils/target-mock.sol"; import {UnitTest} from "test/utils/unit-test.sol"; +contract EmergencyProtectedTimelockMock is TargetMock { + uint256 public proposalsCount; + + function getProposalsCount() external view returns (uint256 count) { + return proposalsCount; + } + + function setProposalsCount(uint256 _proposalsCount) external { + proposalsCount = _proposalsCount; + } +} + contract EmergencyExecutionCommitteeUnitTest is UnitTest { EmergencyExecutionCommittee internal emergencyExecutionCommittee; uint256 internal quorum = 2; @@ -19,7 +31,8 @@ contract EmergencyExecutionCommitteeUnitTest is UnitTest { uint256 internal proposalId = 1; function setUp() external { - emergencyProtectedTimelock = address(new TargetMock()); + emergencyProtectedTimelock = address(new EmergencyProtectedTimelockMock()); + EmergencyProtectedTimelockMock(payable(emergencyProtectedTimelock)).setProposalsCount(1); emergencyExecutionCommittee = new EmergencyExecutionCommittee(owner, committeeMembers, quorum, emergencyProtectedTimelock); } @@ -53,6 +66,16 @@ contract EmergencyExecutionCommitteeUnitTest is UnitTest { assertFalse(isExecuted); } + function test_voteEmergencyExecute_RevertOn_ProposalDoesNotExist() external { + uint256 nonExistentProposalId = proposalId + 1; + + vm.expectRevert( + abi.encodeWithSelector(EmergencyExecutionCommittee.ProposalDoesNotExist.selector, nonExistentProposalId) + ); + vm.prank(committeeMembers[0]); + emergencyExecutionCommittee.voteEmergencyExecute(nonExistentProposalId, true); + } + function testFuzz_voteEmergencyExecute_RevertOn_NotMember(address caller) external { vm.assume(caller != committeeMembers[0] && caller != committeeMembers[1] && caller != committeeMembers[2]); vm.prank(caller); diff --git a/test/unit/committees/TiebreakerCore.t.sol b/test/unit/committees/TiebreakerCore.t.sol index c4142716..2693535b 100644 --- a/test/unit/committees/TiebreakerCore.t.sol +++ b/test/unit/committees/TiebreakerCore.t.sol @@ -7,21 +7,46 @@ import {Durations, Duration} from "contracts/types/Duration.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; +import {ITimelock} from "contracts/interfaces/ITimelock.sol"; + import {TargetMock} from "test/utils/target-mock.sol"; import {UnitTest} from "test/utils/unit-test.sol"; +contract DualGovernanceMock is TargetMock { + ITimelock public TIMELOCK; + + constructor(address _timelock) { + TIMELOCK = ITimelock(_timelock); + } +} + +contract EmergencyProtectedTimelockMock is TargetMock { + uint256 public proposalsCount; + + function getProposalsCount() external view returns (uint256 count) { + return proposalsCount; + } + + function setProposalsCount(uint256 _proposalsCount) external { + proposalsCount = _proposalsCount; + } +} + contract TiebreakerCoreUnitTest is UnitTest { TiebreakerCore internal tiebreakerCore; uint256 internal quorum = 2; address internal owner = makeAddr("owner"); address[] internal committeeMembers = [address(0x1), address(0x2), address(0x3)]; address internal dualGovernance; + address internal emergencyProtectedTimelock; uint256 internal proposalId = 1; address internal sealable = makeAddr("sealable"); Duration internal timelock = Durations.from(1 days); function setUp() external { - dualGovernance = address(new TargetMock()); + emergencyProtectedTimelock = address(new EmergencyProtectedTimelockMock()); + EmergencyProtectedTimelockMock(payable(emergencyProtectedTimelock)).setProposalsCount(1); + dualGovernance = address(new DualGovernanceMock(emergencyProtectedTimelock)); tiebreakerCore = new TiebreakerCore(owner, dualGovernance, timelock); vm.prank(owner); @@ -58,6 +83,14 @@ contract TiebreakerCoreUnitTest is UnitTest { tiebreakerCore.scheduleProposal(proposalId); } + function test_scheduleProposal_RevertOn_ProposalDoesNotExist() external { + uint256 nonExistentProposalId = proposalId + 1; + + vm.expectRevert(abi.encodeWithSelector(TiebreakerCore.ProposalDoesNotExist.selector, nonExistentProposalId)); + vm.prank(committeeMembers[0]); + tiebreakerCore.scheduleProposal(nonExistentProposalId); + } + function test_executeScheduleProposal_HappyPath() external { vm.prank(committeeMembers[0]); tiebreakerCore.scheduleProposal(proposalId); diff --git a/test/unit/committees/TiebreakerSubCommittee.t.sol b/test/unit/committees/TiebreakerSubCommittee.t.sol index 674cb609..f40062ea 100644 --- a/test/unit/committees/TiebreakerSubCommittee.t.sol +++ b/test/unit/committees/TiebreakerSubCommittee.t.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.26; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; +import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; import {HashConsensus} from "contracts/committees/HashConsensus.sol"; import {Durations} from "contracts/types/Duration.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; @@ -10,6 +11,22 @@ import {ITiebreakerCore} from "contracts/interfaces/ITiebreaker.sol"; import {TargetMock} from "test/utils/target-mock.sol"; +contract TiebreakerCoreMock is TargetMock { + error ProposalDoesNotExist(uint256 proposalId); + + uint256 public proposalsCount; + + function checkProposalExists(uint256 _proposalId) external view { + if (_proposalId > proposalsCount) { + revert ProposalDoesNotExist(_proposalId); + } + } + + function setProposalsCount(uint256 _proposalsCount) external { + proposalsCount = _proposalsCount; + } +} + contract TiebreakerSubCommitteeUnitTest is UnitTest { TiebreakerSubCommittee internal tiebreakerSubCommittee; uint256 internal quorum = 2; @@ -20,7 +37,8 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest { address internal sealable = makeAddr("sealable"); function setUp() external { - tiebreakerCore = address(new TargetMock()); + tiebreakerCore = address(new TiebreakerCoreMock()); + TiebreakerCoreMock(payable(tiebreakerCore)).setProposalsCount(1); tiebreakerSubCommittee = new TiebreakerSubCommittee(owner, committeeMembers, quorum, tiebreakerCore); } @@ -78,6 +96,14 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest { tiebreakerSubCommittee.executeScheduleProposal(proposalId); } + function test_scheduleProposal_RevertOn_ProposalDoesNotExist() external { + uint256 nonExistentProposalId = proposalId + 1; + + vm.expectRevert(abi.encodeWithSelector(TiebreakerCore.ProposalDoesNotExist.selector, nonExistentProposalId)); + vm.prank(committeeMembers[0]); + tiebreakerSubCommittee.scheduleProposal(nonExistentProposalId); + } + function test_sealableResume_HappyPath() external { vm.mockCall( tiebreakerCore, From a3513c48b827e50d1f74f109705ae6b66c1b046e Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Thu, 5 Sep 2024 16:11:54 +0300 Subject: [PATCH 28/86] feat: rework getters --- contracts/DualGovernance.sol | 38 ++-- contracts/EmergencyProtectedTimelock.sol | 78 +++++--- contracts/interfaces/IDualGovernance.sol | 13 ++ .../IEmergencyProtectedTimelock.sol | 19 ++ contracts/interfaces/ITiebreaker.sol | 3 +- contracts/interfaces/ITimelock.sol | 11 +- .../libraries/DualGovernanceStateMachine.sol | 23 ++- contracts/libraries/ExecutableProposals.sol | 15 +- contracts/libraries/Tiebreaker.sol | 7 +- test/mocks/TimelockMock.sol | 8 +- test/scenario/happy-path-plan-b.t.sol | 31 +-- test/scenario/proposal-deployment-modes.t.sol | 2 +- test/scenario/tiebreaker.t.sol | 4 +- test/scenario/timelocked-governance.t.sol | 9 +- test/unit/DualGovernance.t.sol | 26 +-- test/unit/EmergencyProtectedTimelock.t.sol | 177 +++++++++--------- test/unit/libraries/ExecutableProposals.t.sol | 83 ++++---- test/unit/libraries/Tiebreaker.t.sol | 14 +- test/utils/scenario-test-blueprint.sol | 38 ++-- 19 files changed, 331 insertions(+), 268 deletions(-) create mode 100644 contracts/interfaces/IEmergencyProtectedTimelock.sol diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 6be3dad8..b6c1ff53 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -132,9 +132,8 @@ contract DualGovernance is IDualGovernance { function scheduleProposal(uint256 proposalId) external { _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); - ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = - TIMELOCK.getProposalInfo(proposalId); - if (!_stateMachine.canScheduleProposal(submittedAt)) { + ITimelock.ProposalDetails memory proposalDetails = TIMELOCK.getProposalDetails(proposalId); + if (!_stateMachine.canScheduleProposal(proposalDetails.submittedAt)) { revert ProposalSchedulingBlocked(proposalId); } TIMELOCK.schedule(proposalId); @@ -148,8 +147,8 @@ contract DualGovernance is IDualGovernance { revert NotAdminProposer(); } - State currentState = _stateMachine.getCurrentState(); - if (currentState != State.VetoSignalling && currentState != State.VetoSignallingDeactivation) { + State state = _stateMachine.getState(); + if (state != State.VetoSignalling && state != State.VetoSignallingDeactivation) { /// @dev Some proposer contracts, like Aragon Voting, may not support canceling decisions that have already /// reached consensus. This could lead to a situation where a proposer’s cancelAllPendingProposals() call /// becomes unexecutable if the Dual Governance state changes. However, it might become executable again if @@ -169,9 +168,8 @@ contract DualGovernance is IDualGovernance { } function canScheduleProposal(uint256 proposalId) external view returns (bool) { - ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = - TIMELOCK.getProposalInfo(proposalId); - return _stateMachine.canScheduleProposal(submittedAt) && TIMELOCK.canSchedule(proposalId); + ITimelock.ProposalDetails memory proposalDetails = TIMELOCK.getProposalDetails(proposalId); + return _stateMachine.canScheduleProposal(proposalDetails.submittedAt) && TIMELOCK.canSchedule(proposalId); } // --- @@ -205,16 +203,12 @@ contract DualGovernance is IDualGovernance { return address(_stateMachine.rageQuitEscrow); } - function getCurrentState() external view returns (State currentState) { - currentState = _stateMachine.getCurrentState(); + function getState() external view returns (State state) { + state = _stateMachine.getState(); } - function getCurrentStateContext() external view returns (DualGovernanceStateMachine.Context memory) { - return _stateMachine.getCurrentContext(); - } - - function getDynamicDelayDuration() external view returns (Duration) { - return _stateMachine.getDynamicDelayDuration(_configProvider.getDualGovernanceConfig()); + function getStateDetails() external view returns (IDualGovernance.StateDetails memory stateDetails) { + return _stateMachine.getStateDetails(_configProvider.getDualGovernanceConfig()); } // --- @@ -280,19 +274,21 @@ contract DualGovernance is IDualGovernance { function tiebreakerResumeSealable(address sealable) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); - _tiebreaker.checkTie(_stateMachine.getCurrentState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); + _tiebreaker.checkTie(_stateMachine.getState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); RESEAL_MANAGER.resume(sealable); } function tiebreakerScheduleProposal(uint256 proposalId) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); - _tiebreaker.checkTie(_stateMachine.getCurrentState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); + _tiebreaker.checkTie(_stateMachine.getState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); TIMELOCK.schedule(proposalId); } - function getTiebreakerContext() external view returns (ITiebreaker.Context memory tiebreakerState) { - return _tiebreaker.getTiebreakerContext(); + function getTiebreakerDetails() external view returns (ITiebreaker.TiebreakerDetails memory tiebreakerState) { + return _tiebreaker.getTiebreakerDetails( + _stateMachine.getState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt() + ); } // --- @@ -303,7 +299,7 @@ contract DualGovernance is IDualGovernance { if (msg.sender != _resealCommittee) { revert CallerIsNotResealCommittee(msg.sender); } - if (_stateMachine.getCurrentState() == State.Normal) { + if (_stateMachine.getState() == State.Normal) { revert ResealIsNotAllowedInNormalState(); } RESEAL_MANAGER.reseal(sealable); diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 7dc7bad7..12115024 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -5,7 +5,8 @@ import {Duration} from "./types/Duration.sol"; import {Timestamp} from "./types/Timestamp.sol"; import {IOwnable} from "./interfaces/IOwnable.sol"; -import {ITimelock, ProposalStatus} from "./interfaces/ITimelock.sol"; +import {ProposalStatus} from "./interfaces/ITimelock.sol"; +import {IEmergencyProtectedTimelock} from "./interfaces/IEmergencyProtectedTimelock.sol"; import {TimelockState} from "./libraries/TimelockState.sol"; import {ExternalCall} from "./libraries/ExternalCalls.sol"; @@ -17,7 +18,7 @@ import {EmergencyProtection} from "./libraries/EmergencyProtection.sol"; /// 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 { +contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { using TimelockState for TimelockState.Context; using ExecutableProposals for ExecutableProposals.Context; using EmergencyProtection for EmergencyProtection.Context; @@ -204,16 +205,42 @@ contract EmergencyProtectedTimelock is ITimelock { _proposals.cancelAll(); } - function getEmergencyProtectionContext() external view returns (EmergencyProtection.Context memory) { - return _emergencyProtection; - } - + /// @dev Returns whether the emergency protection is enabled. + /// @return A boolean indicating whether the emergency protection is enabled. function isEmergencyProtectionEnabled() public view returns (bool) { return _emergencyProtection.isEmergencyProtectionEnabled(); } - function isEmergencyModeActive() public view returns (bool isActive) { - isActive = _emergencyProtection.isEmergencyModeActive(); + /// @dev Returns whether the emergency mode is active. + /// @return A boolean indicating whether the emergency protection is enabled. + function isEmergencyModeActive() public view returns (bool) { + return _emergencyProtection.isEmergencyModeActive(); + } + + /// @dev Returns the details of the emergency protection. + /// @return details A struct containing the emergency mode duration, emergency mode ends after, and emergency protection ends after. + function getEmergencyProtectionDetails() public view returns (EmergencyProtectionDetails memory details) { + details.emergencyModeDuration = _emergencyProtection.emergencyModeDuration; + details.emergencyModeEndsAfter = _emergencyProtection.emergencyModeEndsAfter; + details.emergencyProtectionEndsAfter = _emergencyProtection.emergencyProtectionEndsAfter; + } + + /// @dev Returns the address of the emergency governance. + /// @return The address of the emergency governance. + function getEmergencyGovernance() external view returns (address) { + return _emergencyProtection.emergencyGovernance; + } + + /// @dev Returns the address of the emergency activation committee. + /// @return The address of the emergency activation committee. + function getEmergencyActivationCommittee() external view returns (address) { + return _emergencyProtection.emergencyActivationCommittee; + } + + /// @dev Returns the address of the emergency execution committee. + /// @return The address of the emergency execution committee. + function getEmergencyExecutionCommittee() external view returns (address) { + return _emergencyProtection.emergencyExecutionCommittee; } // --- @@ -238,35 +265,34 @@ contract EmergencyProtectedTimelock is ITimelock { /// @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.id = proposalId; - (proposal.status, proposal.executor, proposal.submittedAt, proposal.scheduledAt) = - _proposals.getProposalInfo(proposalId); - proposal.calls = _proposals.getProposalCalls(proposalId); + /// @return proposalDetails The Proposal struct containing the details of the proposal. + /// @return calls An array of ExternalCall structs representing the sequence of calls to be executed for the proposal. + function getProposal(uint256 proposalId) + external + view + returns (ProposalDetails memory proposalDetails, ExternalCall[] memory calls) + { + proposalDetails = _proposals.getProposalDetails(proposalId); + calls = _proposals.getProposalCalls(proposalId); } /// @notice Retrieves information about a proposal, excluding the external calls associated with it. /// @param proposalId The ID of the proposal to retrieve information for. - /// @return id The ID of the proposal. - /// @return status The current status of the proposal. Possible values are: + /// @return proposalDetails A ProposalDetails struct containing the details of the proposal. + /// id The ID of the proposal. + /// status The current status of the proposal. Possible values are: /// 0 - The proposal does not exist. /// 1 - The proposal was submitted but not scheduled. /// 2 - The proposal was submitted and scheduled but not yet executed. /// 3 - The proposal was submitted, scheduled, and executed. This is the final state of the proposal lifecycle. /// 4 - The proposal was cancelled via cancelAllNonExecutedProposals() and cannot be scheduled or executed anymore. /// This is the final state of the proposal. - /// @return executor The address of the executor responsible for executing the proposal's external calls. - /// @return submittedAt The timestamp when the proposal was submitted. - /// @return scheduledAt The timestamp when the proposal was scheduled for execution. Equals 0 if the proposal + /// executor The address of the executor responsible for executing the proposal's external calls. + /// submittedAt The timestamp when the proposal was submitted. + /// scheduledAt The timestamp when the proposal was scheduled for execution. Equals 0 if the proposal /// was submitted but not yet scheduled. - function getProposalInfo(uint256 proposalId) - external - view - returns (uint256 id, ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) - { - id = proposalId; - (status, executor, submittedAt, scheduledAt) = _proposals.getProposalInfo(proposalId); + function getProposalDetails(uint256 proposalId) external view returns (ProposalDetails memory proposalDetails) { + return _proposals.getProposalDetails(proposalId); } /// @notice Retrieves the external calls associated with the specified proposal. diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index ce156606..977d0576 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -3,8 +3,21 @@ pragma solidity 0.8.26; import {IGovernance} from "./IGovernance.sol"; import {ITiebreaker} from "./ITiebreaker.sol"; +import {Timestamp} from "../types/Timestamp.sol"; +import {Duration} from "../types/Duration.sol"; +import {State} from "../libraries/DualGovernanceStateMachine.sol"; interface IDualGovernance is IGovernance, ITiebreaker { + struct StateDetails { + State state; + Timestamp enteredAt; + Timestamp vetoSignallingActivatedAt; + Timestamp vetoSignallingReactivationTime; + Timestamp normalOrVetoCooldownExitedAt; + uint256 rageQuitRound; + Duration dynamicDelay; + } + function activateNextState() external; function resealSealable(address sealables) external; diff --git a/contracts/interfaces/IEmergencyProtectedTimelock.sol b/contracts/interfaces/IEmergencyProtectedTimelock.sol new file mode 100644 index 00000000..841ca5ed --- /dev/null +++ b/contracts/interfaces/IEmergencyProtectedTimelock.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ITimelock} from "./ITimelock.sol"; +import {Duration} from "../types/Duration.sol"; +import {Timestamp} from "../types/Timestamp.sol"; + +interface IEmergencyProtectedTimelock is ITimelock { + struct EmergencyProtectionDetails { + Duration emergencyModeDuration; + Timestamp emergencyModeEndsAfter; + Timestamp emergencyProtectionEndsAfter; + } + + function getEmergencyGovernance() external view returns (address emergencyGovernance); + function getEmergencyActivationCommittee() external view returns (address committee); + function getEmergencyExecutionCommittee() external view returns (address committee); + function getEmergencyProtectionDetails() external view returns (EmergencyProtectionDetails memory details); +} diff --git a/contracts/interfaces/ITiebreaker.sol b/contracts/interfaces/ITiebreaker.sol index 90be1e2c..5f6c535b 100644 --- a/contracts/interfaces/ITiebreaker.sol +++ b/contracts/interfaces/ITiebreaker.sol @@ -4,7 +4,8 @@ pragma solidity 0.8.26; import {Duration} from "../types/Duration.sol"; interface ITiebreaker { - struct Context { + struct TiebreakerDetails { + bool isTie; address tiebreakerCommittee; Duration tiebreakerActivationTimeout; address[] sealableWithdrawalBlockers; diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index f3c192f0..c54da3d7 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -7,13 +7,12 @@ import {ExternalCall} from "../libraries/ExternalCalls.sol"; import {Status as ProposalStatus} from "../libraries/ExecutableProposals.sol"; interface ITimelock { - struct Proposal { + struct ProposalDetails { uint256 id; - ProposalStatus status; address executor; Timestamp submittedAt; Timestamp scheduledAt; - ExternalCall[] calls; + ProposalStatus status; } function submit(address executor, ExternalCall[] calldata calls) external returns (uint256 newProposalId); @@ -26,11 +25,11 @@ interface ITimelock { function getAdminExecutor() external view returns (address); - function getProposal(uint256 proposalId) external view returns (Proposal memory proposal); - function getProposalInfo(uint256 proposalId) + function getProposal(uint256 proposalId) external view - returns (uint256 id, ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt); + returns (ProposalDetails memory proposal, ExternalCall[] memory calls); + function getProposalDetails(uint256 proposalId) external view returns (ProposalDetails memory proposalDetails); function getGovernance() external view returns (address); function setGovernance(address governance) external; diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index d783d55f..c82d5425 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -5,6 +5,7 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {IEscrow} from "../interfaces/IEscrow.sol"; +import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; import {Duration} from "../types/Duration.sol"; import {PercentD16} from "../types/PercentD16.sol"; @@ -128,11 +129,20 @@ library DualGovernanceStateMachine { emit DualGovernanceStateChanged(currentState, newState, self); } - function getCurrentContext(Context storage self) internal pure returns (Context memory) { - return self; + function getStateDetails( + Context storage self, + DualGovernanceConfig.Context memory config + ) internal view returns (IDualGovernance.StateDetails memory stateDetails) { + stateDetails.state = self.state; + stateDetails.enteredAt = self.enteredAt; + stateDetails.vetoSignallingActivatedAt = self.vetoSignallingActivatedAt; + stateDetails.vetoSignallingReactivationTime = self.vetoSignallingReactivationTime; + stateDetails.normalOrVetoCooldownExitedAt = self.normalOrVetoCooldownExitedAt; + stateDetails.rageQuitRound = self.rageQuitRound; + stateDetails.dynamicDelay = config.calcDynamicDelayDuration(self.signallingEscrow.getRageQuitSupport()); } - function getCurrentState(Context storage self) internal view returns (State) { + function getState(Context storage self) internal view returns (State) { return self.state; } @@ -140,13 +150,6 @@ library DualGovernanceStateMachine { return self.normalOrVetoCooldownExitedAt; } - function getDynamicDelayDuration( - Context storage self, - DualGovernanceConfig.Context memory config - ) internal view returns (Duration) { - return config.calcDynamicDelayDuration(self.signallingEscrow.getRageQuitSupport()); - } - function canSubmitProposal(Context storage self) internal view returns (bool) { State state = self.state; return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; diff --git a/contracts/libraries/ExecutableProposals.sol b/contracts/libraries/ExecutableProposals.sol index 5b868ca9..d943e66b 100644 --- a/contracts/libraries/ExecutableProposals.sol +++ b/contracts/libraries/ExecutableProposals.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {ITimelock} from "../interfaces/ITimelock.sol"; import {Duration} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; @@ -180,17 +181,19 @@ library ExecutableProposals { return self.proposalsCount; } - function getProposalInfo( + function getProposalDetails( Context storage self, uint256 proposalId - ) internal view returns (Status status, address executor, Timestamp submittedAt, Timestamp scheduledAt) { + ) internal view returns (ITimelock.ProposalDetails memory proposalDetails) { ProposalData memory proposalData = self.proposals[proposalId].data; _checkProposalExists(proposalId, proposalData); - status = _isProposalMarkedCancelled(self, proposalId, proposalData) ? Status.Cancelled : proposalData.status; - executor = address(proposalData.executor); - submittedAt = proposalData.submittedAt; - scheduledAt = proposalData.scheduledAt; + proposalDetails.id = proposalId; + proposalDetails.status = + _isProposalMarkedCancelled(self, proposalId, proposalData) ? Status.Cancelled : proposalData.status; + proposalDetails.executor = address(proposalData.executor); + proposalDetails.submittedAt = proposalData.submittedAt; + proposalDetails.scheduledAt = proposalData.scheduledAt; } function getProposalCalls( diff --git a/contracts/libraries/Tiebreaker.sol b/contracts/libraries/Tiebreaker.sol index b559baf2..83b8be71 100644 --- a/contracts/libraries/Tiebreaker.sol +++ b/contracts/libraries/Tiebreaker.sol @@ -186,9 +186,14 @@ library Tiebreaker { /// @dev Retrieves the tiebreaker context from the storage. /// @param self The storage context. /// @return context The tiebreaker context containing the tiebreaker committee, tiebreaker activation timeout, and sealable withdrawal blockers. - function getTiebreakerContext(Context storage self) internal view returns (ITiebreaker.Context memory context) { + function getTiebreakerDetails( + Context storage self, + DualGovernanceState state, + Timestamp normalOrVetoCooldownExitedAt + ) internal view returns (ITiebreaker.TiebreakerDetails memory context) { context.tiebreakerCommittee = self.tiebreakerCommittee; context.tiebreakerActivationTimeout = self.tiebreakerActivationTimeout; + context.isTie = isTie(self, state, normalOrVetoCooldownExitedAt); uint256 sealableWithdrawalBlockersCount = self.sealableWithdrawalBlockers.length(); context.sealableWithdrawalBlockers = new address[](sealableWithdrawalBlockersCount); diff --git a/test/mocks/TimelockMock.sol b/test/mocks/TimelockMock.sol index 276726e0..f7c498f9 100644 --- a/test/mocks/TimelockMock.sol +++ b/test/mocks/TimelockMock.sol @@ -75,7 +75,7 @@ contract TimelockMock is ITimelock { return lastCancelledProposalId; } - function getProposal(uint256 proposalId) external view returns (Proposal memory) { + function getProposal(uint256 proposalId) external view returns (ProposalDetails memory, ExternalCall[] memory) { revert("Not Implemented"); } @@ -99,11 +99,7 @@ contract TimelockMock is ITimelock { revert("Not Implemented"); } - function getProposalInfo(uint256 proposalId) - external - view - returns (uint256 id, ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) - { + function getProposalDetails(uint256 proposalId) external view returns (ProposalDetails memory) { revert("Not Implemented"); } diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index 014594f7..6681778d 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; import {IPotentiallyDangerousContract} from "../utils/interfaces/IPotentiallyDangerousContract.sol"; import { @@ -54,7 +55,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // ACT 2. 😱 DAO IS UNDER ATTACK // --- uint256 maliciousProposalId; - EmergencyProtection.Context memory emergencyState; + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory emergencyState; { // Malicious vote was proposed by the attacker with huge LDO wad (but still not the majority) ExternalCall[] memory maliciousCalls = ExternalCallHelpers.create( @@ -79,7 +80,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // emergency mode was successfully activated Timestamp expectedEmergencyModeEndTimestamp = _EMERGENCY_MODE_DURATION.addTo(Timestamps.now()); - emergencyState = _timelock.getEmergencyProtectionContext(); + emergencyState = _timelock.getEmergencyProtectionDetails(); assertTrue(_timelock.isEmergencyModeActive()); assertEq(emergencyState.emergencyModeEndsAfter, expectedEmergencyModeEndTimestamp); @@ -248,9 +249,9 @@ contract PlanBSetup is ScenarioTestBlueprint { assertFalse(_timelock.isEmergencyModeActive()); - emergencyState = _timelock.getEmergencyProtectionContext(); - assertEq(emergencyState.emergencyActivationCommittee, address(_emergencyActivationCommittee)); - assertEq(emergencyState.emergencyExecutionCommittee, address(_emergencyExecutionCommittee)); + emergencyState = _timelock.getEmergencyProtectionDetails(); + assertEq(_timelock.getEmergencyActivationCommittee(), address(_emergencyActivationCommittee)); + assertEq(_timelock.getEmergencyExecutionCommittee(), address(_emergencyExecutionCommittee)); assertEq(emergencyState.emergencyModeDuration, Durations.from(30 days)); assertEq(emergencyState.emergencyModeEndsAfter, Timestamps.ZERO); @@ -334,7 +335,7 @@ contract PlanBSetup is ScenarioTestBlueprint { _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); // emergency mode still active - assertTrue(_timelock.getEmergencyProtectionContext().emergencyModeEndsAfter > Timestamps.now()); + assertTrue(_timelock.getEmergencyProtectionDetails().emergencyModeEndsAfter > Timestamps.now()); anotherMaliciousProposalId = _submitProposalViaTimelockedGovernance("Another Rug Pool attempt", maliciousCalls); @@ -394,7 +395,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // deploy dual governance full setup { _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); - assertNotEq(_timelock.getGovernance(), _timelock.getEmergencyProtectionContext().emergencyGovernance); + assertNotEq(_timelock.getGovernance(), _timelock.getEmergencyGovernance()); } // emergency committee activates emergency mode @@ -410,17 +411,18 @@ contract PlanBSetup is ScenarioTestBlueprint { { _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); - EmergencyProtection.Context memory emergencyState = _timelock.getEmergencyProtectionContext(); + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory emergencyState = + _timelock.getEmergencyProtectionDetails(); assertTrue(emergencyState.emergencyModeEndsAfter > Timestamps.now()); _executeEmergencyReset(); - assertEq(_timelock.getGovernance(), _timelock.getEmergencyProtectionContext().emergencyGovernance); + assertEq(_timelock.getGovernance(), _timelock.getEmergencyGovernance()); - emergencyState = _timelock.getEmergencyProtectionContext(); - assertEq(emergencyState.emergencyActivationCommittee, address(0)); - assertEq(emergencyState.emergencyExecutionCommittee, address(0)); + emergencyState = _timelock.getEmergencyProtectionDetails(); + assertEq(_timelock.getEmergencyActivationCommittee(), address(0)); + assertEq(_timelock.getEmergencyExecutionCommittee(), address(0)); assertEq(emergencyState.emergencyModeDuration, Durations.ZERO); assertEq(emergencyState.emergencyModeEndsAfter, Timestamps.ZERO); assertFalse(_timelock.isEmergencyModeActive()); @@ -431,7 +433,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // deploy dual governance full setup { _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); - assertNotEq(_timelock.getGovernance(), _timelock.getEmergencyProtectionContext().emergencyGovernance); + assertNotEq(_timelock.getGovernance(), _timelock.getEmergencyGovernance()); } // wait till the protection duration passes @@ -439,7 +441,8 @@ contract PlanBSetup is ScenarioTestBlueprint { _wait(_EMERGENCY_PROTECTION_DURATION.plusSeconds(1)); } - EmergencyProtection.Context memory emergencyState = _timelock.getEmergencyProtectionContext(); + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory emergencyState = + _timelock.getEmergencyProtectionDetails(); // attempt to activate emergency protection fails { diff --git a/test/scenario/proposal-deployment-modes.t.sol b/test/scenario/proposal-deployment-modes.t.sol index 663ae551..e48712b1 100644 --- a/test/scenario/proposal-deployment-modes.t.sol +++ b/test/scenario/proposal-deployment-modes.t.sol @@ -116,7 +116,7 @@ contract ProposalDeploymentModesTest is ScenarioTestBlueprint { // emergency protection disabled after emergency mode is activated - _wait(_timelock.getEmergencyProtectionContext().emergencyModeDuration.plusSeconds(1)); + _wait(_timelock.getEmergencyProtectionDetails().emergencyModeDuration.plusSeconds(1)); assertEq(_timelock.isEmergencyModeActive(), true); assertEq(_timelock.isEmergencyProtectionEnabled(), true); diff --git a/test/scenario/tiebreaker.t.sol b/test/scenario/tiebreaker.t.sol index e56e169c..513c2cf3 100644 --- a/test/scenario/tiebreaker.t.sol +++ b/test/scenario/tiebreaker.t.sol @@ -31,7 +31,7 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertRageQuitState(); - _wait(_dualGovernance.getTiebreakerContext().tiebreakerActivationTimeout); + _wait(_dualGovernance.getTiebreakerDetails().tiebreakerActivationTimeout); _activateNextState(); ExternalCall[] memory proposalCalls = ExternalCallHelpers.create(address(0), new bytes(0)); @@ -106,7 +106,7 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertRageQuitState(); - _wait(_dualGovernance.getTiebreakerContext().tiebreakerActivationTimeout); + _wait(_dualGovernance.getTiebreakerDetails().tiebreakerActivationTimeout); _activateNextState(); // Tiebreaker subcommittee 0 diff --git a/test/scenario/timelocked-governance.t.sol b/test/scenario/timelocked-governance.t.sol index 5008c8ff..d6be5be5 100644 --- a/test/scenario/timelocked-governance.t.sol +++ b/test/scenario/timelocked-governance.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.26; import {Duration, Durations} from "contracts/types/Duration.sol"; import {IGovernance} from "contracts/interfaces/IGovernance.sol"; +import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; import {ExternalCall} from "contracts/libraries/ExecutableProposals.sol"; import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; @@ -27,7 +28,8 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { // --- // Act 2. Timeskip. Emergency protection is about to be expired. // --- - EmergencyProtection.Context memory emergencyState = _timelock.getEmergencyProtectionContext(); + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory emergencyState = + _timelock.getEmergencyProtectionDetails(); { assertEq(_timelock.isEmergencyProtectionEnabled(), true); Duration emergencyProtectionDuration = @@ -40,7 +42,7 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { // Act 3. Emergency committee has no more power to stop proposal flow. // { - vm.prank(address(emergencyState.emergencyActivationCommittee)); + vm.prank(address(_timelock.getEmergencyActivationCommittee())); vm.expectRevert( abi.encodeWithSelector( @@ -180,7 +182,8 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { // Act 4. DAO decides to not deactivate emergency mode and allow stakers to quit. // --- { - EmergencyProtection.Context memory emergencyState = _timelock.getEmergencyProtectionContext(); + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory emergencyState = + _timelock.getEmergencyProtectionDetails(); assertTrue(_timelock.isEmergencyModeActive()); _wait( diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 032f043f..f381298d 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -88,7 +88,7 @@ contract DualGovernanceUnitTests is UnitTest { _submitMockProposal(); assertEq(_timelock.getProposalsCount(), 1); assertEq(_timelock.lastCancelledProposalId(), 0); - assertEq(_dualGovernance.getCurrentState(), State.Normal); + assertEq(_dualGovernance.getState(), State.Normal); vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsSkipped(); @@ -103,7 +103,7 @@ contract DualGovernanceUnitTests is UnitTest { _submitMockProposal(); assertEq(_timelock.getProposalsCount(), 1); assertEq(_timelock.lastCancelledProposalId(), 0); - assertEq(_dualGovernance.getCurrentState(), State.Normal); + assertEq(_dualGovernance.getState(), State.Normal); Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); @@ -115,19 +115,19 @@ contract DualGovernanceUnitTests is UnitTest { signallingEscrow.lockStETH(5 ether); vm.stopPrank(); - assertEq(_dualGovernance.getCurrentState(), State.VetoSignalling); + assertEq(_dualGovernance.getState(), State.VetoSignalling); _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); vm.prank(vetoer); signallingEscrow.unlockStETH(); - assertEq(_dualGovernance.getCurrentState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getState(), State.VetoSignallingDeactivation); _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _dualGovernance.activateNextState(); - assertEq(_dualGovernance.getCurrentState(), State.VetoCooldown); + assertEq(_dualGovernance.getState(), State.VetoCooldown); vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsSkipped(); @@ -142,7 +142,7 @@ contract DualGovernanceUnitTests is UnitTest { _submitMockProposal(); assertEq(_timelock.getProposalsCount(), 1); assertEq(_timelock.lastCancelledProposalId(), 0); - assertEq(_dualGovernance.getCurrentState(), State.Normal); + assertEq(_dualGovernance.getState(), State.Normal); Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); @@ -154,12 +154,12 @@ contract DualGovernanceUnitTests is UnitTest { signallingEscrow.lockStETH(5 ether); vm.stopPrank(); - assertEq(_dualGovernance.getCurrentState(), State.VetoSignalling); + assertEq(_dualGovernance.getState(), State.VetoSignalling); _wait(_configProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); _dualGovernance.activateNextState(); - assertEq(_dualGovernance.getCurrentState(), State.RageQuit); + assertEq(_dualGovernance.getState(), State.RageQuit); vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsSkipped(); @@ -174,7 +174,7 @@ contract DualGovernanceUnitTests is UnitTest { _submitMockProposal(); assertEq(_timelock.getProposalsCount(), 1); assertEq(_timelock.lastCancelledProposalId(), 0); - assertEq(_dualGovernance.getCurrentState(), State.Normal); + assertEq(_dualGovernance.getState(), State.Normal); Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); @@ -186,7 +186,7 @@ contract DualGovernanceUnitTests is UnitTest { signallingEscrow.lockStETH(5 ether); vm.stopPrank(); - assertEq(_dualGovernance.getCurrentState(), State.VetoSignalling); + assertEq(_dualGovernance.getState(), State.VetoSignalling); vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsExecuted(); @@ -201,7 +201,7 @@ contract DualGovernanceUnitTests is UnitTest { _submitMockProposal(); assertEq(_timelock.getProposalsCount(), 1); assertEq(_timelock.lastCancelledProposalId(), 0); - assertEq(_dualGovernance.getCurrentState(), State.Normal); + assertEq(_dualGovernance.getState(), State.Normal); Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); @@ -213,14 +213,14 @@ contract DualGovernanceUnitTests is UnitTest { signallingEscrow.lockStETH(5 ether); vm.stopPrank(); - assertEq(_dualGovernance.getCurrentState(), State.VetoSignalling); + assertEq(_dualGovernance.getState(), State.VetoSignalling); _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); vm.prank(vetoer); signallingEscrow.unlockStETH(); - assertEq(_dualGovernance.getCurrentState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getState(), State.VetoSignallingDeactivation); vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsExecuted(); diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index dc208951..6dce1d50 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -7,6 +7,7 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; +import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; import {ITimelock, ProposalStatus} from "contracts/interfaces/ITimelock.sol"; import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; @@ -70,7 +71,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getProposalsCount(), 1); - ITimelock.Proposal memory proposal = _timelock.getProposal(1); + ITimelock.ProposalDetails memory proposal = _timelock.getProposalDetails(1); assertEq(proposal.status, ProposalStatus.Submitted); } @@ -85,7 +86,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _scheduleProposal(1); - ITimelock.Proposal memory proposal = _timelock.getProposal(1); + ITimelock.ProposalDetails memory proposal = _timelock.getProposalDetails(1); assertEq(proposal.status, ProposalStatus.Scheduled); } @@ -100,7 +101,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.schedule(1); - ITimelock.Proposal memory proposal = _timelock.getProposal(1); + ITimelock.ProposalDetails memory proposal = _timelock.getProposalDetails(1); assertEq(proposal.status, ProposalStatus.Submitted); } @@ -122,7 +123,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(stranger); _timelock.execute(1); - ITimelock.Proposal memory proposal = _timelock.getProposal(1); + ITimelock.ProposalDetails memory proposal = _timelock.getProposalDetails(1); assertEq(proposal.status, ProposalStatus.Executed); } @@ -141,7 +142,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [false])); _timelock.execute(1); - ITimelock.Proposal memory proposal = _timelock.getProposal(1); + ITimelock.ProposalDetails memory proposal = _timelock.getProposalDetails(1); assertEq(proposal.status, ProposalStatus.Scheduled); } @@ -157,8 +158,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _scheduleProposal(1); - ITimelock.Proposal memory proposal1 = _timelock.getProposal(1); - ITimelock.Proposal memory proposal2 = _timelock.getProposal(2); + ITimelock.ProposalDetails memory proposal1 = _timelock.getProposalDetails(1); + ITimelock.ProposalDetails memory proposal2 = _timelock.getProposalDetails(2); assertEq(proposal1.status, ProposalStatus.Scheduled); assertEq(proposal2.status, ProposalStatus.Submitted); @@ -166,8 +167,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(_dualGovernance); _timelock.cancelAllNonExecutedProposals(); - proposal1 = _timelock.getProposal(1); - proposal2 = _timelock.getProposal(2); + proposal1 = _timelock.getProposalDetails(1); + proposal2 = _timelock.getProposalDetails(2); assertEq(_timelock.getProposalsCount(), 2); assertEq(proposal1.status, ProposalStatus.Cancelled); @@ -334,7 +335,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(_emergencyEnactor); _timelock.emergencyExecute(1); - ITimelock.Proposal memory proposal = _timelock.getProposal(1); + ITimelock.ProposalDetails memory proposal = _timelock.getProposalDetails(1); assertEq(proposal.status, ProposalStatus.Executed); } @@ -399,14 +400,14 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getProposalsCount(), 1); - ITimelock.Proposal memory proposal = _timelock.getProposal(1); + ITimelock.ProposalDetails memory proposal = _timelock.getProposalDetails(1); assertEq(proposal.status, ProposalStatus.Submitted); _activateEmergencyMode(); _deactivateEmergencyMode(); - proposal = _timelock.getProposal(1); + proposal = _timelock.getProposalDetails(1); assertEq(proposal.status, ProposalStatus.Cancelled); } @@ -415,7 +416,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _activateEmergencyMode(); - EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory state = _timelock.getEmergencyProtectionDetails(); assertEq(_isEmergencyStateActivated(), true); _wait(state.emergencyModeDuration.plusSeconds(1)); @@ -459,14 +460,15 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(_emergencyEnactor); _timelock.emergencyReset(); - EmergencyProtection.Context memory newState = _timelock.getEmergencyProtectionContext(); + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory newState = + _timelock.getEmergencyProtectionDetails(); assertEq(_isEmergencyStateActivated(), false); assertEq(_timelock.getGovernance(), _emergencyGovernance); assertEq(_timelock.isEmergencyProtectionEnabled(), false); - assertEq(newState.emergencyActivationCommittee, address(0)); - assertEq(newState.emergencyExecutionCommittee, address(0)); + assertEq(_timelock.getEmergencyActivationCommittee(), address(0)); + assertEq(_timelock.getEmergencyExecutionCommittee(), address(0)); assertEq(newState.emergencyProtectionEndsAfter, Timestamps.ZERO); assertEq(newState.emergencyModeDuration, Durations.ZERO); assertEq(newState.emergencyModeEndsAfter, Timestamps.ZERO); @@ -476,13 +478,13 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _submitProposal(); _activateEmergencyMode(); - ITimelock.Proposal memory proposal = _timelock.getProposal(1); + ITimelock.ProposalDetails memory proposal = _timelock.getProposalDetails(1); assertEq(proposal.status, ProposalStatus.Submitted); vm.prank(_emergencyEnactor); _timelock.emergencyReset(); - proposal = _timelock.getProposal(1); + proposal = _timelock.getProposalDetails(1); assertEq(proposal.status, ProposalStatus.Cancelled); } @@ -506,16 +508,19 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_emergencyReset_RevertOn_ModeNotActivated() external { assertEq(_isEmergencyStateActivated(), false); - EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory state = _timelock.getEmergencyProtectionDetails(); + address emergencyActivationCommitteeBefore = _timelock.getEmergencyActivationCommittee(); + address emergencyExecutionCommitteeBefore = _timelock.getEmergencyExecutionCommittee(); vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); vm.prank(_emergencyEnactor); _timelock.emergencyReset(); - EmergencyProtection.Context memory newState = _timelock.getEmergencyProtectionContext(); + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory newState = + _timelock.getEmergencyProtectionDetails(); - assertEq(newState.emergencyExecutionCommittee, state.emergencyExecutionCommittee); - assertEq(newState.emergencyActivationCommittee, state.emergencyActivationCommittee); + assertEq(_timelock.getEmergencyActivationCommittee(), emergencyActivationCommitteeBefore); + assertEq(_timelock.getEmergencyExecutionCommittee(), emergencyExecutionCommitteeBefore); assertEq(newState.emergencyProtectionEndsAfter, state.emergencyProtectionEndsAfter); assertEq(newState.emergencyModeEndsAfter, state.emergencyModeEndsAfter); assertEq(newState.emergencyModeDuration, state.emergencyModeDuration); @@ -531,9 +536,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _localTimelock.setEmergencyProtectionActivationCommittee(_emergencyActivator); vm.stopPrank(); - EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); - - assertEq(state.emergencyActivationCommittee, _emergencyActivator); + assertEq(_timelock.getEmergencyActivationCommittee(), _emergencyActivator); assertFalse(_timelock.isEmergencyModeActive()); } @@ -545,9 +548,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(stranger); _localTimelock.setEmergencyProtectionActivationCommittee(_emergencyActivator); - EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); - - assertEq(state.emergencyActivationCommittee, address(0)); + assertEq(_localTimelock.getEmergencyActivationCommittee(), address(0)); assertFalse(_localTimelock.isEmergencyModeActive()); } @@ -560,23 +561,19 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _localTimelock.setEmergencyProtectionExecutionCommittee(_emergencyEnactor); vm.stopPrank(); - EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); - - assertEq(state.emergencyExecutionCommittee, _emergencyEnactor); + assertEq(_timelock.getEmergencyExecutionCommittee(), _emergencyEnactor); assertFalse(_localTimelock.isEmergencyModeActive()); } function testFuzz_setExecutionCommittee_RevertOn_ByStranger(address stranger) external { - vm.assume(stranger != _adminExecutor); + vm.assume(stranger != _adminExecutor && stranger != _emergencyEnactor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); vm.prank(stranger); _localTimelock.setEmergencyProtectionExecutionCommittee(_emergencyEnactor); - EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); - - assertEq(state.emergencyExecutionCommittee, address(0)); + assertEq(_localTimelock.getEmergencyExecutionCommittee(), address(0)); assertFalse(_localTimelock.isEmergencyModeActive()); } @@ -589,7 +586,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _localTimelock.setEmergencyProtectionEndDate(_emergencyProtectionDuration.addTo(Timestamps.now())); vm.stopPrank(); - EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory state = + _localTimelock.getEmergencyProtectionDetails(); assertEq(state.emergencyProtectionEndsAfter, _emergencyProtectionDuration.addTo(Timestamps.now())); assertFalse(_localTimelock.isEmergencyModeActive()); @@ -603,7 +601,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(stranger); _localTimelock.setEmergencyProtectionEndDate(_emergencyProtectionDuration.addTo(Timestamps.now())); - EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory state = + _localTimelock.getEmergencyProtectionDetails(); assertEq(state.emergencyProtectionEndsAfter, Timestamps.ZERO); assertFalse(_localTimelock.isEmergencyModeActive()); @@ -618,7 +617,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _localTimelock.setEmergencyModeDuration(_emergencyModeDuration); vm.stopPrank(); - EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory state = + _localTimelock.getEmergencyProtectionDetails(); assertEq(state.emergencyModeDuration, _emergencyModeDuration); assertFalse(_localTimelock.isEmergencyModeActive()); @@ -632,7 +632,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(stranger); _localTimelock.setEmergencyModeDuration(_emergencyModeDuration); - EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory state = + _localTimelock.getEmergencyProtectionDetails(); assertEq(state.emergencyModeDuration, Durations.ZERO); assertFalse(_localTimelock.isEmergencyModeActive()); @@ -691,16 +692,17 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); } - // EmergencyProtectedTimelock.getEmergencyProtectionContext() + // EmergencyProtectedTimelock.getEmergencyProtectionDetails() function test_get_emergency_state_deactivate() external { EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); - EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory state = + _localTimelock.getEmergencyProtectionDetails(); assertFalse(_localTimelock.isEmergencyModeActive()); - assertEq(state.emergencyActivationCommittee, address(0)); - assertEq(state.emergencyExecutionCommittee, address(0)); + assertEq(_localTimelock.getEmergencyActivationCommittee(), address(0)); + assertEq(_localTimelock.getEmergencyExecutionCommittee(), address(0)); assertEq(state.emergencyProtectionEndsAfter, Timestamps.ZERO); assertEq(state.emergencyModeDuration, Durations.ZERO); assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); @@ -712,11 +714,11 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _localTimelock.setEmergencyModeDuration(_emergencyModeDuration); vm.stopPrank(); - state = _localTimelock.getEmergencyProtectionContext(); + state = _localTimelock.getEmergencyProtectionDetails(); assertEq(_localTimelock.isEmergencyModeActive(), false); - assertEq(state.emergencyActivationCommittee, _emergencyActivator); - assertEq(state.emergencyExecutionCommittee, _emergencyEnactor); + assertEq(_localTimelock.getEmergencyActivationCommittee(), _emergencyActivator); + assertEq(_localTimelock.getEmergencyExecutionCommittee(), _emergencyEnactor); assertEq(state.emergencyProtectionEndsAfter, _emergencyProtectionDuration.addTo(Timestamps.now())); assertEq(state.emergencyModeDuration, _emergencyModeDuration); assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); @@ -724,11 +726,11 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(_emergencyActivator); _localTimelock.activateEmergencyMode(); - state = _localTimelock.getEmergencyProtectionContext(); + state = _localTimelock.getEmergencyProtectionDetails(); assertEq(_localTimelock.isEmergencyModeActive(), true); - assertEq(state.emergencyExecutionCommittee, _emergencyEnactor); - assertEq(state.emergencyActivationCommittee, _emergencyActivator); + assertEq(_localTimelock.getEmergencyExecutionCommittee(), _emergencyEnactor); + assertEq(_localTimelock.getEmergencyActivationCommittee(), _emergencyActivator); assertEq(state.emergencyModeDuration, _emergencyModeDuration); assertEq(state.emergencyProtectionEndsAfter, _emergencyProtectionDuration.addTo(Timestamps.now())); assertEq(state.emergencyModeEndsAfter, _emergencyModeDuration.addTo(Timestamps.now())); @@ -736,11 +738,11 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(_adminExecutor); _localTimelock.deactivateEmergencyMode(); - state = _localTimelock.getEmergencyProtectionContext(); + state = _localTimelock.getEmergencyProtectionDetails(); assertFalse(_timelock.isEmergencyModeActive()); - assertEq(state.emergencyActivationCommittee, address(0)); - assertEq(state.emergencyExecutionCommittee, address(0)); + assertEq(_localTimelock.getEmergencyActivationCommittee(), address(0)); + assertEq(_localTimelock.getEmergencyExecutionCommittee(), address(0)); assertEq(state.emergencyProtectionEndsAfter, Timestamps.ZERO); assertEq(state.emergencyModeDuration, Durations.ZERO); assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); @@ -763,11 +765,12 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(_emergencyEnactor); _localTimelock.emergencyReset(); - EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory state = + _localTimelock.getEmergencyProtectionDetails(); - assertFalse(_timelock.isEmergencyModeActive()); - assertEq(state.emergencyActivationCommittee, address(0)); - assertEq(state.emergencyExecutionCommittee, address(0)); + assertFalse(_localTimelock.isEmergencyModeActive()); + assertEq(_localTimelock.getEmergencyActivationCommittee(), address(0)); + assertEq(_localTimelock.getEmergencyExecutionCommittee(), address(0)); assertEq(state.emergencyProtectionEndsAfter, Timestamps.ZERO); assertEq(state.emergencyModeDuration, Durations.ZERO); assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); @@ -782,7 +785,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getGovernance(), governance); } - // EmergencyProtectedTimelock.getProposal() + // EmergencyProtectedTimelock.getProposalDetails() function test_get_proposal() external { assertEq(_timelock.getProposalsCount(), 0); @@ -792,7 +795,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.submit(_adminExecutor, executorCalls); _timelock.submit(_adminExecutor, executorCalls); - ITimelock.Proposal memory submittedProposal = _timelock.getProposal(1); + (ITimelock.ProposalDetails memory submittedProposal, ExternalCall[] memory calls) = _timelock.getProposal(1); Timestamp submitTimestamp = Timestamps.now(); @@ -801,33 +804,35 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(submittedProposal.submittedAt, submitTimestamp); assertEq(submittedProposal.scheduledAt, Timestamps.ZERO); assertEq(submittedProposal.status, ProposalStatus.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); + assertEq(calls.length, 1); + assertEq(calls[0].value, executorCalls[0].value); + assertEq(calls[0].target, executorCalls[0].target); + assertEq(calls[0].payload, executorCalls[0].payload); _wait(_timelock.getAfterSubmitDelay()); _timelock.schedule(1); Timestamp scheduleTimestamp = Timestamps.now(); - ITimelock.Proposal memory scheduledProposal = _timelock.getProposal(1); + (ITimelock.ProposalDetails memory scheduledProposal, ExternalCall[] memory scheduledCalls) = + _timelock.getProposal(1); assertEq(scheduledProposal.id, 1); assertEq(scheduledProposal.executor, _adminExecutor); assertEq(scheduledProposal.submittedAt, submitTimestamp); assertEq(scheduledProposal.scheduledAt, scheduleTimestamp); assertEq(scheduledProposal.status, ProposalStatus.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); + assertEq(scheduledCalls.length, 1); + assertEq(scheduledCalls[0].value, executorCalls[0].value); + assertEq(scheduledCalls[0].target, executorCalls[0].target); + assertEq(scheduledCalls[0].payload, executorCalls[0].payload); _wait(_timelock.getAfterScheduleDelay()); _timelock.execute(1); - ITimelock.Proposal memory executedProposal = _timelock.getProposal(1); + (ITimelock.ProposalDetails memory executedProposal, ExternalCall[] memory executedCalls) = + _timelock.getProposal(1); Timestamp executeTimestamp = Timestamps.now(); assertEq(executedProposal.id, 1); @@ -837,14 +842,15 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(executedProposal.scheduledAt, scheduleTimestamp); // assertEq(executedProposal.executedAt, executeTimestamp); // assertEq doesn't support comparing enumerables so far - 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); + assertEq(executedCalls.length, 1); + assertEq(executedCalls[0].value, executorCalls[0].value); + assertEq(executedCalls[0].target, executorCalls[0].target); + assertEq(executedCalls[0].payload, executorCalls[0].payload); _timelock.cancelAllNonExecutedProposals(); - ITimelock.Proposal memory cancelledProposal = _timelock.getProposal(2); + (ITimelock.ProposalDetails memory cancelledProposal, ExternalCall[] memory cancelledCalls) = + _timelock.getProposal(2); assertEq(cancelledProposal.id, 2); assertEq(cancelledProposal.status, ProposalStatus.Cancelled); @@ -853,17 +859,17 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(cancelledProposal.scheduledAt, Timestamps.ZERO); // assertEq(cancelledProposal.executedAt, Timestamps.ZERO); // assertEq doesn't support comparing enumerables so far - 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); + assertEq(cancelledCalls.length, 1); + assertEq(cancelledCalls[0].value, executorCalls[0].value); + assertEq(cancelledCalls[0].target, executorCalls[0].target); + assertEq(cancelledCalls[0].payload, executorCalls[0].payload); } function test_get_not_existing_proposal() external { assertEq(_timelock.getProposalsCount(), 0); vm.expectRevert(); - _timelock.getProposal(1); + _timelock.getProposalDetails(1); } // EmergencyProtectedTimelock.getProposalsCount() @@ -926,20 +932,19 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_get_proposal_submission_time() external { _submitProposal(); - assertEq(_timelock.getProposal(1).submittedAt, Timestamps.now()); + assertEq(_timelock.getProposalDetails(1).submittedAt, Timestamps.now()); } function test_getProposalInfo() external { _submitProposal(); - (uint256 id, ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) = - _timelock.getProposalInfo(1); + ITimelock.ProposalDetails memory proposalDetails = _timelock.getProposalDetails(1); - assertEq(id, 1); - assert(status == ProposalStatus.Submitted); - assertEq(executor, _adminExecutor); - assertEq(submittedAt, Timestamps.from(block.timestamp)); - assertEq(scheduledAt, Timestamps.from(0)); + assertEq(proposalDetails.id, 1); + assert(proposalDetails.status == ProposalStatus.Submitted); + assertEq(proposalDetails.executor, _adminExecutor); + assertEq(proposalDetails.submittedAt, Timestamps.from(block.timestamp)); + assertEq(proposalDetails.scheduledAt, Timestamps.from(0)); } function test_getProposalCalls() external { diff --git a/test/unit/libraries/ExecutableProposals.t.sol b/test/unit/libraries/ExecutableProposals.t.sol index 6f205777..987bf5f9 100644 --- a/test/unit/libraries/ExecutableProposals.t.sol +++ b/test/unit/libraries/ExecutableProposals.t.sol @@ -6,6 +6,7 @@ import {Vm} from "forge-std/Test.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; +import {ITimelock} from "contracts/interfaces/ITimelock.sol"; import {Executor} from "contracts/Executor.sol"; import { ExecutableProposals, ExternalCall, Status as ProposalStatus @@ -233,15 +234,14 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.submit(address(_executor), expectedCalls); uint256 proposalId = _proposals.getProposalsCount(); - (ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) = - _proposals.getProposalInfo(proposalId); + ITimelock.ProposalDetails memory proposalDetails = _proposals.getProposalDetails(proposalId); Timestamp expectedSubmittedAt = Timestamps.now(); - assertEq(status, ProposalStatus.Submitted); - assertEq(executor, address(_executor)); - assertEq(submittedAt, expectedSubmittedAt); - assertEq(scheduledAt, Timestamps.ZERO); + assertEq(proposalDetails.status, ProposalStatus.Submitted); + assertEq(proposalDetails.executor, address(_executor)); + assertEq(proposalDetails.submittedAt, expectedSubmittedAt); + assertEq(proposalDetails.scheduledAt, Timestamps.ZERO); ExternalCall[] memory calls = _proposals.getProposalCalls(proposalId); @@ -256,12 +256,12 @@ contract ExecutableProposalsUnitTests is UnitTest { Timestamp expectedScheduledAt = Timestamps.now(); - (status, executor, submittedAt, scheduledAt) = _proposals.getProposalInfo(proposalId); + proposalDetails = _proposals.getProposalDetails(proposalId); - assertEq(status, ProposalStatus.Scheduled); - assertEq(executor, address(_executor)); - assertEq(submittedAt, expectedSubmittedAt); - assertEq(scheduledAt, expectedScheduledAt); + assertEq(proposalDetails.status, ProposalStatus.Scheduled); + assertEq(proposalDetails.executor, address(_executor)); + assertEq(proposalDetails.submittedAt, expectedSubmittedAt); + assertEq(proposalDetails.scheduledAt, expectedScheduledAt); calls = _proposals.getProposalCalls(proposalId); @@ -274,12 +274,12 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.execute(proposalId, Durations.ZERO); - (status, executor, submittedAt, scheduledAt) = _proposals.getProposalInfo(proposalId); + proposalDetails = _proposals.getProposalDetails(proposalId); - assertEq(status, ProposalStatus.Executed); - assertEq(executor, address(_executor)); - assertEq(submittedAt, expectedSubmittedAt); - assertEq(scheduledAt, expectedScheduledAt); + assertEq(proposalDetails.status, ProposalStatus.Executed); + assertEq(proposalDetails.executor, address(_executor)); + assertEq(proposalDetails.submittedAt, expectedSubmittedAt); + assertEq(proposalDetails.scheduledAt, expectedScheduledAt); calls = _proposals.getProposalCalls(proposalId); @@ -296,15 +296,14 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.submit(address(_executor), expectedCalls); uint256 proposalId = _proposals.getProposalsCount(); - (ProposalStatus status, address executor, Timestamp submittedAt, Timestamp scheduledAt) = - _proposals.getProposalInfo(proposalId); + ITimelock.ProposalDetails memory proposalDetails = _proposals.getProposalDetails(proposalId); Timestamp expectedSubmittedAt = Timestamps.now(); - assertEq(status, ProposalStatus.Submitted); - assertEq(executor, address(_executor)); - assertEq(submittedAt, expectedSubmittedAt); - assertEq(scheduledAt, Timestamps.ZERO); + assertEq(proposalDetails.status, ProposalStatus.Submitted); + assertEq(proposalDetails.executor, address(_executor)); + assertEq(proposalDetails.submittedAt, expectedSubmittedAt); + assertEq(proposalDetails.scheduledAt, Timestamps.ZERO); ExternalCall[] memory calls = _proposals.getProposalCalls(proposalId); @@ -317,12 +316,12 @@ contract ExecutableProposalsUnitTests is UnitTest { ExecutableProposals.cancelAll(_proposals); - (status, executor, submittedAt, scheduledAt) = _proposals.getProposalInfo(proposalId); + proposalDetails = _proposals.getProposalDetails(proposalId); - assertEq(status, ProposalStatus.Cancelled); - assertEq(executor, address(_executor)); - assertEq(submittedAt, expectedSubmittedAt); - assertEq(scheduledAt, Timestamps.ZERO); + assertEq(proposalDetails.status, ProposalStatus.Cancelled); + assertEq(proposalDetails.executor, address(_executor)); + assertEq(proposalDetails.submittedAt, expectedSubmittedAt); + assertEq(proposalDetails.scheduledAt, Timestamps.ZERO); calls = _proposals.getProposalCalls(proposalId); @@ -336,7 +335,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function testFuzz_get_not_existing_proposal(uint256 proposalId) external { vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotFound.selector, proposalId)); - _proposals.getProposalInfo(proposalId); + _proposals.getProposalDetails(proposalId); vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotFound.selector, proposalId)); _proposals.getProposalCalls(proposalId); @@ -419,34 +418,18 @@ contract ExecutableProposalsUnitTests is UnitTest { // Validate the state of the proposals is correct before proceeding with cancellation. - (ProposalStatus executedProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = - _proposals.getProposalInfo(executedProposalId); - assertEq(executedProposalStatus, ProposalStatus.Executed); - - (ProposalStatus scheduledProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = - _proposals.getProposalInfo(scheduledProposalId); - assertEq(scheduledProposalStatus, ProposalStatus.Scheduled); - - (ProposalStatus submittedProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = - _proposals.getProposalInfo(submittedProposalId); - assertEq(submittedProposalStatus, ProposalStatus.Submitted); + assertEq(_proposals.getProposalDetails(executedProposalId).status, ProposalStatus.Executed); + assertEq(_proposals.getProposalDetails(scheduledProposalId).status, ProposalStatus.Scheduled); + assertEq(_proposals.getProposalDetails(submittedProposalId).status, ProposalStatus.Submitted); // After canceling the proposals, both submitted and scheduled proposals should transition to the Cancelled state. // However, executed proposals should remain in the Executed state. _proposals.cancelAll(); - (executedProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = - _proposals.getProposalInfo(executedProposalId); - assertEq(executedProposalStatus, ProposalStatus.Executed); - - (scheduledProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = - _proposals.getProposalInfo(scheduledProposalId); - assertEq(scheduledProposalStatus, ProposalStatus.Cancelled); - - (submittedProposalStatus, /* executor */, /* submittedAt */, /* scheduledAt */ ) = - _proposals.getProposalInfo(submittedProposalId); - assertEq(submittedProposalStatus, ProposalStatus.Cancelled); + assertEq(_proposals.getProposalDetails(executedProposalId).status, ProposalStatus.Executed); + assertEq(_proposals.getProposalDetails(scheduledProposalId).status, ProposalStatus.Cancelled); + assertEq(_proposals.getProposalDetails(submittedProposalId).status, ProposalStatus.Cancelled); } function test_can_schedule_proposal() external { diff --git a/test/unit/libraries/Tiebreaker.t.sol b/test/unit/libraries/Tiebreaker.t.sol index c3bccddb..08fc7697 100644 --- a/test/unit/libraries/Tiebreaker.t.sol +++ b/test/unit/libraries/Tiebreaker.t.sol @@ -7,6 +7,7 @@ import {State as DualGovernanceState} from "contracts/libraries/DualGovernanceSt import {Tiebreaker} from "contracts/libraries/Tiebreaker.sol"; import {Duration, Durations, Timestamp, Timestamps} from "contracts/types/Duration.sol"; import {ISealable} from "contracts/interfaces/ISealable.sol"; +import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; import {UnitTest} from "test/utils/unit-test.sol"; import {SealableMock} from "../../mocks/SealableMock.sol"; @@ -173,13 +174,14 @@ contract TiebreakerTest is UnitTest { context.tiebreakerActivationTimeout = timeout; context.tiebreakerCommittee = address(0x123); - (address committee, Duration activationTimeout, address[] memory blockers) = - Tiebreaker.getTiebreakerInfo(context); + ITiebreaker.TiebreakerDetails memory details = + Tiebreaker.getTiebreakerDetails(context, DualGovernanceState.Normal, Timestamps.from(block.timestamp)); - assertEq(committee, context.tiebreakerCommittee); - assertEq(activationTimeout, context.tiebreakerActivationTimeout); - assertEq(blockers[0], address(mockSealable1)); - assertEq(blockers.length, 1); + assertEq(details.tiebreakerCommittee, context.tiebreakerCommittee); + assertEq(details.tiebreakerActivationTimeout, context.tiebreakerActivationTimeout); + assertEq(details.sealableWithdrawalBlockers[0], address(mockSealable1)); + assertEq(details.sealableWithdrawalBlockers.length, 1); + assertEq(details.isTie, false); } function external__checkCallerIsTiebreakerCommittee() external view { diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 4cc21da6..f8b72288 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -18,6 +18,8 @@ import {Escrow, VetoerState, LockedAssetsTotals} from "contracts/Escrow.sol"; // Interfaces // --- +import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; +import {ITimelock} from "contracts/interfaces/ITimelock.sol"; import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; import {IPotentiallyDangerousContract} from "./interfaces/IPotentiallyDangerousContract.sol"; @@ -86,9 +88,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { view returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) { - DualGovernanceStateMachine.Context memory stateContext = _dualGovernance.getCurrentStateContext(); + IDualGovernance.StateDetails memory stateContext = _dualGovernance.getStateDetails(); isActive = stateContext.state == DGState.VetoSignalling; - duration = _dualGovernance.getDynamicDelayDuration().toSeconds(); + duration = _dualGovernance.getStateDetails().dynamicDelay.toSeconds(); enteredAt = stateContext.enteredAt.toSeconds(); activatedAt = stateContext.vetoSignallingActivatedAt.toSeconds(); } @@ -98,7 +100,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { view returns (bool isActive, uint256 duration, uint256 enteredAt) { - DualGovernanceStateMachine.Context memory stateContext = _dualGovernance.getCurrentStateContext(); + IDualGovernance.StateDetails memory stateContext = _dualGovernance.getStateDetails(); isActive = stateContext.state == DGState.VetoSignallingDeactivation; duration = _dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().toSeconds(); enteredAt = stateContext.enteredAt.toSeconds(); @@ -339,16 +341,16 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { } function _assertSubmittedProposalData(uint256 proposalId, address executor, ExternalCall[] memory calls) internal { - EmergencyProtectedTimelock.Proposal memory proposal = _timelock.getProposal(proposalId); + (ITimelock.ProposalDetails memory proposal, ExternalCall[] memory calls) = _timelock.getProposal(proposalId); assertEq(proposal.id, proposalId, "unexpected proposal id"); assertEq(proposal.status, ProposalStatus.Submitted, "unexpected status value"); assertEq(proposal.executor, executor, "unexpected executor"); assertEq(Timestamp.unwrap(proposal.submittedAt), block.timestamp, "unexpected scheduledAt"); - assertEq(proposal.calls.length, calls.length, "unexpected calls length"); + assertEq(calls.length, calls.length, "unexpected calls length"); - for (uint256 i = 0; i < proposal.calls.length; ++i) { + for (uint256 i = 0; i < calls.length; ++i) { ExternalCall memory expected = calls[i]; - ExternalCall memory actual = proposal.calls[i]; + ExternalCall memory actual = calls[i]; assertEq(actual.value, expected.value); assertEq(actual.target, expected.target); @@ -409,7 +411,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { function _assertProposalSubmitted(uint256 proposalId) internal { assertEq( - _timelock.getProposal(proposalId).status, + _timelock.getProposalDetails(proposalId).status, ProposalStatus.Submitted, "TimelockProposal not in 'Submitted' state" ); @@ -417,7 +419,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { function _assertProposalScheduled(uint256 proposalId) internal { assertEq( - _timelock.getProposal(proposalId).status, + _timelock.getProposalDetails(proposalId).status, ProposalStatus.Scheduled, "TimelockProposal not in 'Scheduled' state" ); @@ -425,34 +427,38 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { function _assertProposalExecuted(uint256 proposalId) internal { assertEq( - _timelock.getProposal(proposalId).status, + _timelock.getProposalDetails(proposalId).status, ProposalStatus.Executed, "TimelockProposal not in 'Executed' state" ); } function _assertProposalCancelled(uint256 proposalId) internal { - assertEq(_timelock.getProposal(proposalId).status, ProposalStatus.Cancelled, "Proposal not in 'Canceled' state"); + assertEq( + _timelock.getProposalDetails(proposalId).status, + ProposalStatus.Cancelled, + "Proposal not in 'Canceled' state" + ); } function _assertNormalState() internal { - assertEq(_dualGovernance.getCurrentState(), DGState.Normal); + assertEq(_dualGovernance.getState(), DGState.Normal); } function _assertVetoSignalingState() internal { - assertEq(_dualGovernance.getCurrentState(), DGState.VetoSignalling); + assertEq(_dualGovernance.getState(), DGState.VetoSignalling); } function _assertVetoSignalingDeactivationState() internal { - assertEq(_dualGovernance.getCurrentState(), DGState.VetoSignallingDeactivation); + assertEq(_dualGovernance.getState(), DGState.VetoSignallingDeactivation); } function _assertRageQuitState() internal { - assertEq(_dualGovernance.getCurrentState(), DGState.RageQuit); + assertEq(_dualGovernance.getState(), DGState.RageQuit); } function _assertVetoCooldownState() internal { - assertEq(_dualGovernance.getCurrentState(), DGState.VetoCooldown); + assertEq(_dualGovernance.getState(), DGState.VetoCooldown); } function _assertNoTargetMockCalls() internal { From 49513c4b2bf176bebacbb60dfaa8bd6b370f9408 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 9 Sep 2024 02:57:24 +0400 Subject: [PATCH 29/86] Update foundry.toml linter settings --- foundry.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index b40e7e68..4a8ea771 100644 --- a/foundry.toml +++ b/foundry.toml @@ -13,4 +13,6 @@ out = 'kout' test = 'test/kontrol' [fmt] -multiline_func_header = 'params_first' +multiline_func_header = 'params_first_multi' +single_line_statement_blocks = 'multi' +line_length = 120 \ No newline at end of file From 58d8104bc7603eed7fcb6c2e56fb16aed69b08c4 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 9 Sep 2024 02:59:49 +0400 Subject: [PATCH 30/86] Add unit tests for DualGovernanceConfig --- contracts/libraries/DualGovernanceConfig.sol | 38 +- contracts/types/Duration.sol | 17 +- .../unit/libraries/DualGovernanceConfig.t.sol | 341 ++++++++++++++++++ 3 files changed, 367 insertions(+), 29 deletions(-) create mode 100644 test/unit/libraries/DualGovernanceConfig.t.sol diff --git a/contracts/libraries/DualGovernanceConfig.sol b/contracts/libraries/DualGovernanceConfig.sol index f0a0994e..4c723896 100644 --- a/contracts/libraries/DualGovernanceConfig.sol +++ b/contracts/libraries/DualGovernanceConfig.sol @@ -1,20 +1,26 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; - import {PercentD16} from "../types/PercentD16.sol"; import {Duration, Durations} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; library DualGovernanceConfig { + // --- + // Errors + // --- + error InvalidSecondSealRageSupport(PercentD16 secondSealRageQuitSupport); error InvalidRageQuitSupportRange(PercentD16 firstSealRageQuitSupport, PercentD16 secondSealRageQuitSupport); - error RageQuitEthWithdrawalsDelayRange( + error InvalidRageQuitEthWithdrawalsDelayRange( Duration rageQuitEthWithdrawalsMinDelay, Duration rageQuitEthWithdrawalsMaxDelay ); error InvalidVetoSignallingDurationRange(Duration vetoSignallingMinDuration, Duration vetoSignallingMaxDuration); + // --- + // Data Types + // --- + struct Context { PercentD16 firstSealRageQuitSupport; PercentD16 secondSealRageQuitSupport; @@ -34,9 +40,7 @@ library DualGovernanceConfig { Duration rageQuitEthWithdrawalsDelayGrowth; } - function validate( - Context memory self - ) internal pure { + function validate(Context memory self) internal pure { if (self.firstSealRageQuitSupport >= self.secondSealRageQuitSupport) { revert InvalidRageQuitSupportRange(self.firstSealRageQuitSupport, self.secondSealRageQuitSupport); } @@ -46,7 +50,7 @@ library DualGovernanceConfig { } if (self.rageQuitEthWithdrawalsMinDelay > self.rageQuitEthWithdrawalsMaxDelay) { - revert RageQuitEthWithdrawalsDelayRange( + revert InvalidRageQuitEthWithdrawalsDelayRange( self.rageQuitEthWithdrawalsMinDelay, self.rageQuitEthWithdrawalsMaxDelay ); } @@ -76,9 +80,9 @@ library DualGovernanceConfig { function isVetoSignallingReactivationDurationPassed( Context memory self, - Timestamp vetoSignallingReactivationTime + Timestamp vetoSignallingReactivatedAt ) internal view returns (bool) { - return Timestamps.now() > self.vetoSignallingMinActiveDuration.addTo(vetoSignallingReactivationTime); + return Timestamps.now() > self.vetoSignallingMinActiveDuration.addTo(vetoSignallingReactivatedAt); } function isVetoSignallingDeactivationMaxDurationPassed( @@ -98,7 +102,7 @@ library DualGovernanceConfig { function calcVetoSignallingDuration( Context memory self, PercentD16 rageQuitSupport - ) internal pure returns (Duration duration_) { + ) internal pure returns (Duration) { PercentD16 firstSealRageQuitSupport = self.firstSealRageQuitSupport; PercentD16 secondSealRageQuitSupport = self.secondSealRageQuitSupport; @@ -113,7 +117,7 @@ library DualGovernanceConfig { return vetoSignallingMaxDuration; } - duration_ = vetoSignallingMinDuration + return vetoSignallingMinDuration + Durations.from( PercentD16.unwrap(rageQuitSupport - firstSealRageQuitSupport) * (vetoSignallingMaxDuration - vetoSignallingMinDuration).toSeconds() @@ -125,13 +129,11 @@ library DualGovernanceConfig { Context memory self, uint256 rageQuitRound ) internal pure returns (Duration) { - return Durations.from( - Math.min( - self.rageQuitEthWithdrawalsMinDelay.toSeconds() - + rageQuitRound * self.rageQuitEthWithdrawalsDelayGrowth.toSeconds(), - self.rageQuitEthWithdrawalsMaxDelay.toSeconds() - ) + return Durations.min( + self.rageQuitEthWithdrawalsMinDelay.plusSeconds( + rageQuitRound * self.rageQuitEthWithdrawalsDelayGrowth.toSeconds() + ), + self.rageQuitEthWithdrawalsMaxDelay ); } - } diff --git a/contracts/types/Duration.sol b/contracts/types/Duration.sol index bc7e3227..f095cd7d 100644 --- a/contracts/types/Duration.sol +++ b/contracts/types/Duration.sol @@ -13,7 +13,6 @@ uint256 constant MAX_VALUE = type(uint32).max; using {lt as <, lte as <=, gt as >, gte as >=, eq as ==, notEq as !=} for Duration global; using {plus as +, minus as -} for Duration global; - using {addTo, plusSeconds, minusSeconds, multipliedBy, dividedBy, toSeconds} for Duration global; // --- @@ -87,31 +86,24 @@ function addTo(Duration d, Timestamp t) pure returns (Timestamp) { // Conversion Ops // --- -function toDuration( - uint256 value -) pure returns (Duration) { +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) { +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) { + function from(uint256 seconds_) internal pure returns (Duration res) { res = toDuration(seconds_); } @@ -119,4 +111,7 @@ library Durations { res = toDuration(t1.toSeconds() - t2.toSeconds()); } + function min(Duration d1, Duration d2) internal pure returns (Duration res) { + res = d1 < d2 ? d1 : d2; + } } diff --git a/test/unit/libraries/DualGovernanceConfig.t.sol b/test/unit/libraries/DualGovernanceConfig.t.sol new file mode 100644 index 00000000..5a172192 --- /dev/null +++ b/test/unit/libraries/DualGovernanceConfig.t.sol @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; +import {Duration, Durations} from "contracts/types/Duration.sol"; +import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; + +import {DualGovernanceConfig, PercentD16} from "contracts/libraries/DualGovernanceConfig.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; + +contract DualGovernanceConfigTest is UnitTest { + using DualGovernanceConfig for DualGovernanceConfig.Context; + + // Unrealistically huge value for the percents value, to limit max fuzz value to avoid overflow + // due to very high values + PercentD16 internal immutable _SECOND_SEAL_RAGE_QUIT_SUPPORT_LIMIT = PercentsD16.fromBasisPoints(1_000_000_00); + Timestamp internal immutable _MAX_TIMESTAMP = Timestamps.from(block.timestamp + 100 * 365 days); + + // The actual max value will not exceed 255, but for testing is used higher upper bound + uint256 internal immutable _MAX_RAGE_QUIT_ROUND = 512; + + // --- + // validate() + // --- + + function testFuzz_validate_HappyPath(DualGovernanceConfig.Context memory config) external { + _assumeConfigParams(config); + config.validate(); + } + + function testFuzz_validate_RevertOn_InvalidSealRageQuitSupportRange(DualGovernanceConfig.Context memory config) + external + { + vm.assume(config.firstSealRageQuitSupport >= config.secondSealRageQuitSupport); + vm.assume(config.vetoSignallingMinDuration < config.vetoSignallingMaxDuration); + vm.assume(config.rageQuitEthWithdrawalsMinDelay <= config.rageQuitEthWithdrawalsMaxDelay); + + vm.expectRevert( + abi.encodeWithSelector( + DualGovernanceConfig.InvalidRageQuitSupportRange.selector, + config.firstSealRageQuitSupport, + config.secondSealRageQuitSupport + ) + ); + config.validate(); + } + + function testFuzz_validate_RevertOn_InvalidVetoSignallingDurationRange(DualGovernanceConfig.Context memory config) + external + { + vm.assume(config.firstSealRageQuitSupport < config.secondSealRageQuitSupport); + vm.assume(config.secondSealRageQuitSupport < _SECOND_SEAL_RAGE_QUIT_SUPPORT_LIMIT); + vm.assume(config.vetoSignallingMinDuration >= config.vetoSignallingMaxDuration); + vm.assume(config.rageQuitEthWithdrawalsMinDelay <= config.rageQuitEthWithdrawalsMaxDelay); + + vm.expectRevert( + abi.encodeWithSelector( + DualGovernanceConfig.InvalidVetoSignallingDurationRange.selector, + config.vetoSignallingMinDuration, + config.vetoSignallingMaxDuration + ) + ); + config.validate(); + } + + function testFuzz_validate_RevertOn_InvalidRageQuitEthWithdrawalsDelayRange( + DualGovernanceConfig.Context memory config + ) external { + vm.assume(config.firstSealRageQuitSupport < config.secondSealRageQuitSupport); + vm.assume(config.secondSealRageQuitSupport < _SECOND_SEAL_RAGE_QUIT_SUPPORT_LIMIT); + vm.assume(config.vetoSignallingMinDuration < config.vetoSignallingMaxDuration); + vm.assume(config.rageQuitEthWithdrawalsMinDelay > config.rageQuitEthWithdrawalsMaxDelay); + + vm.expectRevert( + abi.encodeWithSelector( + DualGovernanceConfig.InvalidRageQuitEthWithdrawalsDelayRange.selector, + config.rageQuitEthWithdrawalsMinDelay, + config.rageQuitEthWithdrawalsMaxDelay + ) + ); + config.validate(); + } + + // --- + // isFirstSealRageQuitSupportCrossed() + // --- + + function testFuzz_isFirstSealRageQuitSupportCrossed_HappyPath( + DualGovernanceConfig.Context memory config, + PercentD16 rageQuitSupport + ) external { + _assumeConfigParams(config); + assertEq( + config.isFirstSealRageQuitSupportCrossed(rageQuitSupport), rageQuitSupport > config.firstSealRageQuitSupport + ); + } + + // --- + // isSecondSealRageQuitSupportCrossed() + // --- + + function testFuzz_isSecondSealRageQuitSupportCrossed_HappyPath( + DualGovernanceConfig.Context memory config, + PercentD16 rageQuitSupport + ) external { + _assumeConfigParams(config); + assertEq( + config.isSecondSealRageQuitSupportCrossed(rageQuitSupport), + rageQuitSupport > config.secondSealRageQuitSupport + ); + } + + // --- + // isVetoSignallingDurationPassed() + // --- + + function testFuzz_isVetoSignallingDurationPassed_HappyPath( + DualGovernanceConfig.Context memory config, + Timestamp vetoSignallingActivatedAt, + PercentD16 rageQuitSupport + ) external { + _assumeConfigParams(config); + vm.assume(vetoSignallingActivatedAt <= _MAX_TIMESTAMP); + + Duration vetoSignallingDuration = config.calcVetoSignallingDuration(rageQuitSupport); + Timestamp vetoSignallingDurationEndTime = vetoSignallingDuration.addTo(vetoSignallingActivatedAt); + + assertEq( + config.isVetoSignallingDurationPassed(vetoSignallingActivatedAt, rageQuitSupport), + Timestamps.now() > vetoSignallingDurationEndTime + ); + } + + // --- + // isVetoSignallingReactivationDurationPassed() + // --- + function testFuzz_isVetoSignallingReactivationDurationPassed_HappyPath( + DualGovernanceConfig.Context memory config, + Timestamp vetoSignallingReactivationTime + ) external { + _assumeConfigParams(config); + vm.assume(vetoSignallingReactivationTime <= _MAX_TIMESTAMP); + + Timestamp reactivationDurationPassedAfter = + config.vetoSignallingMinActiveDuration.addTo(vetoSignallingReactivationTime); + + assertEq( + config.isVetoSignallingReactivationDurationPassed(vetoSignallingReactivationTime), + Timestamps.now() > reactivationDurationPassedAfter + ); + } + + function test_isVetoSignallingReactivationDurationPassed_HappyPath_EdgeCase() external { + DualGovernanceConfig.Context memory config; + + Timestamp vetoSignallingReactivatedAt = Timestamps.now(); + config.vetoSignallingMinActiveDuration = Durations.from(30 days); + Timestamp vetoSignallingReactivationTimestamp = + config.vetoSignallingMinActiveDuration.addTo(vetoSignallingReactivatedAt); + + _wait(config.vetoSignallingMinActiveDuration); + assertEq(Timestamps.now(), vetoSignallingReactivationTimestamp); + + assertFalse(config.isVetoSignallingReactivationDurationPassed(vetoSignallingReactivatedAt)); + + _wait(Durations.from(1 seconds)); + assertTrue(config.isVetoSignallingReactivationDurationPassed(vetoSignallingReactivatedAt)); + } + + // --- + // isVetoSignallingDeactivationMaxDurationPassed() + // --- + + function testFuzz_isVetoSignallingDeactivationMaxDurationPassed_HappyPath( + DualGovernanceConfig.Context memory config, + Timestamp vetoSignallingDeactivationEnteredAt + ) external { + _assumeConfigParams(config); + vm.assume(vetoSignallingDeactivationEnteredAt <= _MAX_TIMESTAMP); + + Timestamp vetoSignallingDeactivationTimestamp = + config.vetoSignallingDeactivationMaxDuration.addTo(vetoSignallingDeactivationEnteredAt); + + assertEq( + config.isVetoSignallingDeactivationMaxDurationPassed(vetoSignallingDeactivationEnteredAt), + Timestamps.now() > vetoSignallingDeactivationTimestamp + ); + } + + function test_isVetoSignallingDeactivationMaxDurationPassed_HappyPath_EdgeCase() external { + DualGovernanceConfig.Context memory config; + + Timestamp vetoSignallingDeactivationEnteredAt = Timestamps.now(); + config.vetoSignallingDeactivationMaxDuration = Durations.from(3 days); + Timestamp vetoSignallingDeactivationEndsAfter = + config.vetoSignallingDeactivationMaxDuration.addTo(vetoSignallingDeactivationEnteredAt); + + _wait(config.vetoSignallingDeactivationMaxDuration); + + assertEq(Timestamps.now(), vetoSignallingDeactivationEndsAfter); + assertFalse(config.isVetoSignallingDeactivationMaxDurationPassed(vetoSignallingDeactivationEnteredAt)); + + _wait(Durations.from(1 seconds)); + assertTrue(config.isVetoSignallingDeactivationMaxDurationPassed(vetoSignallingDeactivationEnteredAt)); + } + + // --- + // isVetoCooldownDurationPassed() + // --- + + function testFuzz_isVetoCooldownDurationPassed_HappyPath( + DualGovernanceConfig.Context memory config, + Timestamp vetoCooldownEnteredAt + ) external { + _assumeConfigParams(config); + vm.assume(vetoCooldownEnteredAt <= _MAX_TIMESTAMP); + + Timestamp vetoCooldownEndsAfter = config.vetoCooldownDuration.addTo(vetoCooldownEnteredAt); + + assertEq(config.isVetoCooldownDurationPassed(vetoCooldownEnteredAt), Timestamps.now() > vetoCooldownEndsAfter); + } + + function test_isVetoCooldownDurationPassed_HappyPath_EdgeCases() external { + DualGovernanceConfig.Context memory config; + + Timestamp vetoCooldownEnteredAt = Timestamps.now(); + config.vetoCooldownDuration = Durations.from(5 hours); + Timestamp vetoCooldownEndsAfter = config.vetoCooldownDuration.addTo(vetoCooldownEnteredAt); + + _wait(config.vetoCooldownDuration); + + assertEq(Timestamps.now(), vetoCooldownEndsAfter); + assertFalse(config.isVetoCooldownDurationPassed(vetoCooldownEnteredAt)); + + _wait(Durations.from(1 seconds)); + assertTrue(config.isVetoCooldownDurationPassed(vetoCooldownEnteredAt)); + } + + // --- + // calcVetoSignallingDuration() + // --- + + function testFuzz_calcVetoSignallingDuration_HappyPath_RageQuitSupportLessOrEqualThanFirstSeal( + DualGovernanceConfig.Context memory config, + PercentD16 rageQuitSupport + ) external { + _assumeConfigParams(config); + vm.assume(rageQuitSupport <= config.firstSealRageQuitSupport); + assertEq(config.calcVetoSignallingDuration(rageQuitSupport), Durations.ZERO); + } + + function testFuzz_calcVetoSignallingDuration_HappyPath_RageQuitSupportGreaterOrEqualThanSecondSeal( + DualGovernanceConfig.Context memory config, + PercentD16 rageQuitSupport + ) external { + _assumeConfigParams(config); + vm.assume(rageQuitSupport >= config.secondSealRageQuitSupport); + assertEq(config.calcVetoSignallingDuration(rageQuitSupport), config.vetoSignallingMaxDuration); + } + + function testFuzz_calcVetoSignallingDuration_HappyPath_RageQuitSupportInsideSealsRange( + DualGovernanceConfig.Context memory config, + PercentD16 rageQuitSupport + ) external { + _assumeConfigParams(config); + vm.assume(rageQuitSupport > config.firstSealRageQuitSupport); + vm.assume(rageQuitSupport < config.secondSealRageQuitSupport); + + PercentD16 rageQuitSupportFirstSealDelta = rageQuitSupport - config.firstSealRageQuitSupport; + PercentD16 secondFirstSealRangeDelta = config.secondSealRageQuitSupport - config.firstSealRageQuitSupport; + Duration vetoSignallingMaxMinDurationDelta = config.vetoSignallingMaxDuration - config.vetoSignallingMinDuration; + + Duration expectedDuration = config.vetoSignallingMinDuration + + Durations.from( + PercentD16.unwrap(rageQuitSupportFirstSealDelta) * vetoSignallingMaxMinDurationDelta.toSeconds() + / PercentD16.unwrap(secondFirstSealRangeDelta) + ); + + assertEq(config.calcVetoSignallingDuration(rageQuitSupport), expectedDuration); + } + + // --- + // calcRageQuitWithdrawalsDelay() + // --- + + function test_calcRageQuitWithdrawalsDelay_HappyPath_MinDelayWhenRageQuitRoundIsZero() external { + DualGovernanceConfig.Context memory config; + + config.rageQuitEthWithdrawalsMinDelay = Durations.from(15 days); + config.rageQuitEthWithdrawalsMaxDelay = Durations.from(90 days); + config.rageQuitEthWithdrawalsDelayGrowth = Durations.from(30 days); + + assertEq(config.calcRageQuitWithdrawalsDelay({rageQuitRound: 0}), config.rageQuitEthWithdrawalsMinDelay); + } + + function test_calcRageQuitWithdrawalsDelay_HappyPath_MaxDelayWhenRageQuitRoundIsZero() external { + DualGovernanceConfig.Context memory config; + + config.rageQuitEthWithdrawalsMinDelay = Durations.from(15 days); + config.rageQuitEthWithdrawalsMaxDelay = Durations.from(90 days); + config.rageQuitEthWithdrawalsDelayGrowth = Durations.from(30 days); + + assertEq( + config.calcRageQuitWithdrawalsDelay({rageQuitRound: _MAX_RAGE_QUIT_ROUND}), + config.rageQuitEthWithdrawalsMaxDelay + ); + } + + function testFuzz_calcRageQuitWithdrawalsDelay_HappyPath( + DualGovernanceConfig.Context memory config, + uint16 rageQuitRound + ) external { + vm.assume(rageQuitRound > 0); + vm.assume(rageQuitRound <= _MAX_RAGE_QUIT_ROUND); + + vm.assume( + config.rageQuitEthWithdrawalsMinDelay.toSeconds() + + config.rageQuitEthWithdrawalsDelayGrowth.toSeconds() * (rageQuitRound + 1) <= Durations.MAX.toSeconds() + ); + vm.assume(config.rageQuitEthWithdrawalsMinDelay <= config.rageQuitEthWithdrawalsMaxDelay); + + Duration computedRageQuitEthWithdrawalsMinDelay = config.rageQuitEthWithdrawalsMinDelay.plusSeconds( + rageQuitRound * config.rageQuitEthWithdrawalsDelayGrowth.toSeconds() + ); + if (computedRageQuitEthWithdrawalsMinDelay > config.rageQuitEthWithdrawalsMaxDelay) { + computedRageQuitEthWithdrawalsMinDelay = config.rageQuitEthWithdrawalsMaxDelay; + } + assertEq(config.calcRageQuitWithdrawalsDelay(rageQuitRound), computedRageQuitEthWithdrawalsMinDelay); + } + + // --- + // Helper Methods + // --- + + function _assumeConfigParams(DualGovernanceConfig.Context memory config) internal view { + vm.assume(config.firstSealRageQuitSupport < config.secondSealRageQuitSupport); + vm.assume(config.vetoSignallingMinDuration < config.vetoSignallingMaxDuration); + vm.assume(config.secondSealRageQuitSupport < _SECOND_SEAL_RAGE_QUIT_SUPPORT_LIMIT); + vm.assume(config.rageQuitEthWithdrawalsMinDelay <= config.rageQuitEthWithdrawalsMaxDelay); + } +} From 8aaf64f6793ad8b797bfc71c1dd57e854fa54ac4 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Mon, 9 Sep 2024 12:45:14 +0300 Subject: [PATCH 31/86] feat: move getEmergencyProtectionDetails to emergency protection lib --- contracts/EmergencyProtectedTimelock.sol | 4 +--- contracts/libraries/EmergencyProtection.sol | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 12115024..b7faf4cd 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -220,9 +220,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @dev Returns the details of the emergency protection. /// @return details A struct containing the emergency mode duration, emergency mode ends after, and emergency protection ends after. function getEmergencyProtectionDetails() public view returns (EmergencyProtectionDetails memory details) { - details.emergencyModeDuration = _emergencyProtection.emergencyModeDuration; - details.emergencyModeEndsAfter = _emergencyProtection.emergencyModeEndsAfter; - details.emergencyProtectionEndsAfter = _emergencyProtection.emergencyProtectionEndsAfter; + return _emergencyProtection.getEmergencyProtectionDetails(); } /// @dev Returns the address of the emergency governance. diff --git a/contracts/libraries/EmergencyProtection.sol b/contracts/libraries/EmergencyProtection.sol index 582d3fb7..26cdad63 100644 --- a/contracts/libraries/EmergencyProtection.sol +++ b/contracts/libraries/EmergencyProtection.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.26; import {Duration, Durations} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; +import {IEmergencyProtectedTimelock} from "../interfaces/IEmergencyProtectedTimelock.sol"; /// @title EmergencyProtection /// @dev This library manages emergency protection functionality, allowing for @@ -178,6 +179,19 @@ library EmergencyProtection { // Getters // --- + /// @dev Retrieves the details of the emergency protection. + /// @param self The storage reference to the Context struct. + /// @return details The struct containing the emergency protection details. + function getEmergencyProtectionDetails(Context storage self) + internal + view + returns (IEmergencyProtectedTimelock.EmergencyProtectionDetails memory details) + { + details.emergencyModeDuration = self.emergencyModeDuration; + details.emergencyModeEndsAfter = self.emergencyModeEndsAfter; + details.emergencyProtectionEndsAfter = self.emergencyProtectionEndsAfter; + } + /// @dev Checks if the emergency mode is activated /// @param self The storage reference to the Context struct. /// @return Whether the emergency mode is activated or not. From d14a5e7ef35f97116e5f99cd46909ee69e6c80c0 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Fri, 30 Aug 2024 15:31:30 +0300 Subject: [PATCH 32/86] fix: tiebreakerResumeSealable activates next state --- contracts/DualGovernance.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index b6c1ff53..dc872e5b 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -274,6 +274,7 @@ contract DualGovernance is IDualGovernance { function tiebreakerResumeSealable(address sealable) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); + _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); _tiebreaker.checkTie(_stateMachine.getState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); RESEAL_MANAGER.resume(sealable); } From 59dac3770d96f82a5c2d7f1c3856211db2033613 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Mon, 9 Sep 2024 14:30:13 +0300 Subject: [PATCH 33/86] fix: set quorum now doesn't revert tx on same quorum --- contracts/committees/HashConsensus.sol | 8 +++-- test/unit/HashConsensus.t.sol | 43 ++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index c0e8a1f0..cf7eb4c0 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -139,7 +139,9 @@ abstract contract HashConsensus is Ownable { emit MemberRemoved(membersToRemove[i]); } - _setQuorum(newQuorum); + if (newQuorum != quorum) { + _setQuorum(newQuorum); + } } /// @notice Gets the list of committee members @@ -222,7 +224,9 @@ abstract contract HashConsensus is Ownable { emit MemberAdded(newMembers[i]); } - _setQuorum(executionQuorum); + if (executionQuorum != quorum) { + _setQuorum(executionQuorum); + } } /// @notice Gets the number of votes in support of a given hash diff --git a/test/unit/HashConsensus.t.sol b/test/unit/HashConsensus.t.sol index 987c8624..605a14a5 100644 --- a/test/unit/HashConsensus.t.sol +++ b/test/unit/HashConsensus.t.sol @@ -58,7 +58,6 @@ abstract contract HashConsensusUnitTest is UnitTest { function test_constructor_RevertOn_WithZeroQuorum() public { uint256 invalidQuorum = 0; - vm.expectRevert(abi.encodeWithSelector(HashConsensus.InvalidQuorum.selector)); new HashConsensusInstance(_owner, _committeeMembers, invalidQuorum, Durations.from(1)); } @@ -132,7 +131,26 @@ abstract contract HashConsensusUnitTest is UnitTest { _hashConsensus.addMembers(membersToAdd, _membersCount + 2); } - function test_addMember() public { + function test_addMember_SetSameQuorum() public { + address[] memory membersToAdd = new address[](1); + membersToAdd[0] = makeAddr("NEW_MEMBER"); + assertEq(_hashConsensus.isMember(membersToAdd[0]), false); + + vm.startPrank(_owner); + _hashConsensus.addMembers(membersToAdd, _quorum); + + assertEq(_hashConsensus.quorum(), _quorum); + assertEq(_hashConsensus.getMembers().length, _membersCount + 1); + + membersToAdd[0] = makeAddr("NEW_MEMBER_2"); + + _hashConsensus.addMembers(membersToAdd, _quorum); + + assertEq(_hashConsensus.quorum(), _quorum); + assertEq(_hashConsensus.getMembers().length, _membersCount + 2); + } + + function test_addMember_HappyPath() public { address[] memory membersToAdd = new address[](2); membersToAdd[0] = makeAddr("NEW_MEMBER_1"); membersToAdd[1] = makeAddr("NEW_MEMBER_2"); @@ -212,7 +230,26 @@ abstract contract HashConsensusUnitTest is UnitTest { _hashConsensus.removeMembers(membersToRemove, _membersCount); } - function test_removeMembers() public { + function test_removeMembers_SetSameQuorum() public { + address[] memory membersToRemove = new address[](1); + membersToRemove[0] = _committeeMembers[0]; + assertEq(_hashConsensus.isMember(membersToRemove[0]), true); + + vm.startPrank(_owner); + _hashConsensus.removeMembers(membersToRemove, _quorum); + + assertEq(_hashConsensus.quorum(), _quorum); + assertEq(_hashConsensus.getMembers().length, _membersCount - 1); + + membersToRemove[0] = _committeeMembers[1]; + + _hashConsensus.removeMembers(membersToRemove, _quorum); + + assertEq(_hashConsensus.quorum(), _quorum); + assertEq(_hashConsensus.getMembers().length, _membersCount - 2); + } + + function test_removeMembers_HappyPath() public { address[] memory membersToRemove = new address[](2); membersToRemove[0] = _committeeMembers[0]; membersToRemove[1] = _committeeMembers[1]; From 68da938dddf9f6333dcf36235a3810e694ed77c1 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Mon, 9 Sep 2024 15:28:57 +0300 Subject: [PATCH 34/86] feat: create metadata parameter for submitting proposal --- contracts/DualGovernance.sol | 71 +++++++++--- contracts/EmergencyProtectedTimelock.sol | 68 +++++++---- contracts/TimelockedGovernance.sol | 19 +++- contracts/interfaces/IGovernance.sol | 13 ++- contracts/interfaces/ITimelock.sol | 41 +++++-- contracts/libraries/ExecutableProposals.sol | 15 ++- test/mocks/TimelockMock.sol | 42 +++++-- test/unit/DualGovernance.t.sol | 2 +- test/unit/EmergencyProtectedTimelock.t.sol | 106 +++++++++++++----- test/unit/TimelockedGovernance.t.sol | 22 ++-- test/unit/libraries/ExecutableProposals.t.sol | 83 ++++++++------ test/utils/scenario-test-blueprint.sol | 73 ++++++++---- 12 files changed, 388 insertions(+), 167 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index dc872e5b..dc28b5f7 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -121,16 +121,21 @@ contract DualGovernance is IDualGovernance { // Proposals Flow // --- - function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId) { + function submitProposal( + ExternalCall[] calldata calls, + string calldata metadata + ) external returns (uint256 proposalId) { _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); if (!_stateMachine.canSubmitProposal()) { revert ProposalSubmissionBlocked(); } Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); - proposalId = TIMELOCK.submit(proposer.executor, calls); + proposalId = TIMELOCK.submit(proposer.executor, calls, metadata); } - function scheduleProposal(uint256 proposalId) external { + function scheduleProposal( + uint256 proposalId + ) external { _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); ITimelock.ProposalDetails memory proposalDetails = TIMELOCK.getProposalDetails(proposalId); if (!_stateMachine.canScheduleProposal(proposalDetails.submittedAt)) { @@ -167,7 +172,9 @@ contract DualGovernance is IDualGovernance { return _stateMachine.canSubmitProposal(); } - function canScheduleProposal(uint256 proposalId) external view returns (bool) { + function canScheduleProposal( + uint256 proposalId + ) external view returns (bool) { ITimelock.ProposalDetails memory proposalDetails = TIMELOCK.getProposalDetails(proposalId); return _stateMachine.canScheduleProposal(proposalDetails.submittedAt) && TIMELOCK.canSchedule(proposalId); } @@ -180,7 +187,9 @@ contract DualGovernance is IDualGovernance { _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); } - function setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) external { + function setConfigProvider( + IDualGovernanceConfigProvider newConfigProvider + ) external { _checkCallerIsAdminExecutor(); _setConfigProvider(newConfigProvider); @@ -220,7 +229,9 @@ contract DualGovernance is IDualGovernance { _proposers.register(proposer, executor); } - function unregisterProposer(address proposer) external { + function unregisterProposer( + address proposer + ) external { _checkCallerIsAdminExecutor(); _proposers.unregister(proposer); @@ -230,11 +241,15 @@ contract DualGovernance is IDualGovernance { } } - function isProposer(address account) external view returns (bool) { + function isProposer( + address account + ) external view returns (bool) { return _proposers.isProposer(account); } - function getProposer(address account) external view returns (Proposers.Proposer memory proposer) { + function getProposer( + address account + ) external view returns (Proposers.Proposer memory proposer) { proposer = _proposers.getProposer(account); } @@ -242,7 +257,9 @@ contract DualGovernance is IDualGovernance { proposers = _proposers.getAllProposers(); } - function isExecutor(address account) external view returns (bool) { + function isExecutor( + address account + ) external view returns (bool) { return _proposers.isExecutor(account); } @@ -250,36 +267,48 @@ contract DualGovernance is IDualGovernance { // Tiebreaker Protection // --- - function addTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { + function addTiebreakerSealableWithdrawalBlocker( + address sealableWithdrawalBlocker + ) external { _checkCallerIsAdminExecutor(); _tiebreaker.addSealableWithdrawalBlocker(sealableWithdrawalBlocker, MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT); } - function removeTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { + function removeTiebreakerSealableWithdrawalBlocker( + address sealableWithdrawalBlocker + ) external { _checkCallerIsAdminExecutor(); _tiebreaker.removeSealableWithdrawalBlocker(sealableWithdrawalBlocker); } - function setTiebreakerCommittee(address tiebreakerCommittee) external { + function setTiebreakerCommittee( + address tiebreakerCommittee + ) external { _checkCallerIsAdminExecutor(); _tiebreaker.setTiebreakerCommittee(tiebreakerCommittee); } - function setTiebreakerActivationTimeout(Duration tiebreakerActivationTimeout) external { + function setTiebreakerActivationTimeout( + Duration tiebreakerActivationTimeout + ) external { _checkCallerIsAdminExecutor(); _tiebreaker.setTiebreakerActivationTimeout( MIN_TIEBREAKER_ACTIVATION_TIMEOUT, tiebreakerActivationTimeout, MAX_TIEBREAKER_ACTIVATION_TIMEOUT ); } - function tiebreakerResumeSealable(address sealable) external { + function tiebreakerResumeSealable( + address sealable + ) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); _tiebreaker.checkTie(_stateMachine.getState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); RESEAL_MANAGER.resume(sealable); } - function tiebreakerScheduleProposal(uint256 proposalId) external { + function tiebreakerScheduleProposal( + uint256 proposalId + ) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); _tiebreaker.checkTie(_stateMachine.getState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); @@ -296,7 +325,9 @@ contract DualGovernance is IDualGovernance { // Reseal executor // --- - function resealSealable(address sealable) external { + function resealSealable( + address sealable + ) external { if (msg.sender != _resealCommittee) { revert CallerIsNotResealCommittee(msg.sender); } @@ -306,7 +337,9 @@ contract DualGovernance is IDualGovernance { RESEAL_MANAGER.reseal(sealable); } - function setResealCommittee(address resealCommittee) external { + function setResealCommittee( + address resealCommittee + ) external { _checkCallerIsAdminExecutor(); _resealCommittee = resealCommittee; } @@ -315,7 +348,9 @@ contract DualGovernance is IDualGovernance { // Private methods // --- - function _setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) internal { + function _setConfigProvider( + IDualGovernanceConfigProvider newConfigProvider + ) internal { if (address(newConfigProvider) == address(0)) { revert InvalidConfigProvider(newConfigProvider); } diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index b7faf4cd..f35f7a0a 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -73,15 +73,21 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @param executor The address of the executor contract that will execute the calls. /// @param calls An array of `ExternalCall` structs representing the calls to be executed. /// @return newProposalId The ID of the newly created proposal. - function submit(address executor, ExternalCall[] calldata calls) external returns (uint256 newProposalId) { + function submit( + address executor, + ExternalCall[] calldata calls, + string calldata metadata + ) external returns (uint256 newProposalId) { _timelockState.checkCallerIsGovernance(); - newProposalId = _proposals.submit(executor, calls); + newProposalId = _proposals.submit(executor, calls, metadata); } /// @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 { + function schedule( + uint256 proposalId + ) external { _timelockState.checkCallerIsGovernance(); _proposals.schedule(proposalId, _timelockState.getAfterSubmitDelay()); } @@ -89,7 +95,9 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @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 { + function execute( + uint256 proposalId + ) external { _emergencyProtection.checkEmergencyMode({isActive: false}); _proposals.execute(proposalId, _timelockState.getAfterScheduleDelay()); } @@ -105,7 +113,9 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { // Timelock Management // --- - function setGovernance(address newGovernance) external { + function setGovernance( + address newGovernance + ) external { _checkCallerIsAdminExecutor(); _timelockState.setGovernance(newGovernance); } @@ -131,21 +141,27 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @dev Sets the emergency activation committee address. /// @param emergencyActivationCommittee The address of the emergency activation committee. - function setEmergencyProtectionActivationCommittee(address emergencyActivationCommittee) external { + function setEmergencyProtectionActivationCommittee( + address emergencyActivationCommittee + ) external { _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyActivationCommittee(emergencyActivationCommittee); } /// @dev Sets the emergency execution committee address. /// @param emergencyExecutionCommittee The address of the emergency execution committee. - function setEmergencyProtectionExecutionCommittee(address emergencyExecutionCommittee) external { + function setEmergencyProtectionExecutionCommittee( + address emergencyExecutionCommittee + ) external { _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyExecutionCommittee(emergencyExecutionCommittee); } /// @dev Sets the emergency protection end date. /// @param emergencyProtectionEndDate The timestamp of the emergency protection end date. - function setEmergencyProtectionEndDate(Timestamp emergencyProtectionEndDate) external { + function setEmergencyProtectionEndDate( + Timestamp emergencyProtectionEndDate + ) external { _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyProtectionEndDate( emergencyProtectionEndDate, MAX_EMERGENCY_PROTECTION_DURATION @@ -154,14 +170,18 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @dev Sets the emergency mode duration. /// @param emergencyModeDuration The duration of the emergency mode. - function setEmergencyModeDuration(Duration emergencyModeDuration) external { + function setEmergencyModeDuration( + Duration emergencyModeDuration + ) external { _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyModeDuration(emergencyModeDuration, MAX_EMERGENCY_MODE_DURATION); } /// @dev Sets the emergency governance address. /// @param emergencyGovernance The address of the emergency governance. - function setEmergencyGovernance(address emergencyGovernance) external { + function setEmergencyGovernance( + address emergencyGovernance + ) external { _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyGovernance(emergencyGovernance); } @@ -177,7 +197,9 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @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 { + function emergencyExecute( + uint256 proposalId + ) external { _emergencyProtection.checkEmergencyMode({isActive: true}); _emergencyProtection.checkCallerIsEmergencyExecutionCommittee(); _proposals.execute({proposalId: proposalId, afterScheduleDelay: Duration.wrap(0)}); @@ -265,11 +287,9 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @param proposalId The ID of the proposal. /// @return proposalDetails The Proposal struct containing the details of the proposal. /// @return calls An array of ExternalCall structs representing the sequence of calls to be executed for the proposal. - function getProposal(uint256 proposalId) - external - view - returns (ProposalDetails memory proposalDetails, ExternalCall[] memory calls) - { + function getProposal( + uint256 proposalId + ) external view returns (ProposalDetails memory proposalDetails, ExternalCall[] memory calls) { proposalDetails = _proposals.getProposalDetails(proposalId); calls = _proposals.getProposalCalls(proposalId); } @@ -289,14 +309,18 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// submittedAt The timestamp when the proposal was submitted. /// scheduledAt The timestamp when the proposal was scheduled for execution. Equals 0 if the proposal /// was submitted but not yet scheduled. - function getProposalDetails(uint256 proposalId) external view returns (ProposalDetails memory proposalDetails) { + function getProposalDetails( + uint256 proposalId + ) external view returns (ProposalDetails memory proposalDetails) { return _proposals.getProposalDetails(proposalId); } /// @notice Retrieves the external calls associated with the specified proposal. /// @param proposalId The ID of the proposal to retrieve external calls for. /// @return calls An array of ExternalCall structs representing the sequence of calls to be executed for the proposal. - function getProposalCalls(uint256 proposalId) external view returns (ExternalCall[] memory calls) { + function getProposalCalls( + uint256 proposalId + ) external view returns (ExternalCall[] memory calls) { calls = _proposals.getProposalCalls(proposalId); } @@ -309,7 +333,9 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @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) { + function canExecute( + uint256 proposalId + ) external view returns (bool) { return !_emergencyProtection.isEmergencyModeActive() && _proposals.canExecute(proposalId, _timelockState.getAfterScheduleDelay()); } @@ -317,7 +343,9 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @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) { + function canSchedule( + uint256 proposalId + ) external view returns (bool) { return _proposals.canSchedule(proposalId, _timelockState.getAfterSubmitDelay()); } diff --git a/contracts/TimelockedGovernance.sol b/contracts/TimelockedGovernance.sol index 4dc05c93..851075e5 100644 --- a/contracts/TimelockedGovernance.sol +++ b/contracts/TimelockedGovernance.sol @@ -25,27 +25,36 @@ contract TimelockedGovernance is IGovernance { /// @dev Submits a proposal to the timelock. /// @param calls An array of ExternalCall structs representing the calls to be executed in the proposal. /// @return proposalId The ID of the submitted proposal. - function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId) { + function submitProposal( + ExternalCall[] calldata calls, + string calldata metadata + ) external returns (uint256 proposalId) { _checkCallerIsGovernance(); - return TIMELOCK.submit(TIMELOCK.getAdminExecutor(), calls); + return TIMELOCK.submit(TIMELOCK.getAdminExecutor(), calls, metadata); } /// @dev Schedules a submitted proposal. /// @param proposalId The ID of the proposal to be scheduled. - function scheduleProposal(uint256 proposalId) external { + function scheduleProposal( + uint256 proposalId + ) external { TIMELOCK.schedule(proposalId); } /// @dev Executes a scheduled proposal. /// @param proposalId The ID of the proposal to be executed. - function executeProposal(uint256 proposalId) external { + function executeProposal( + uint256 proposalId + ) external { TIMELOCK.execute(proposalId); } /// @dev Checks if a proposal can be scheduled. /// @param proposalId The ID of the proposal to check. /// @return A boolean indicating whether the proposal can be scheduled. - function canScheduleProposal(uint256 proposalId) external view returns (bool) { + function canScheduleProposal( + uint256 proposalId + ) external view returns (bool) { return TIMELOCK.canSchedule(proposalId); } diff --git a/contracts/interfaces/IGovernance.sol b/contracts/interfaces/IGovernance.sol index b29c19f8..7556d3ab 100644 --- a/contracts/interfaces/IGovernance.sol +++ b/contracts/interfaces/IGovernance.sol @@ -4,9 +4,16 @@ pragma solidity 0.8.26; import {ExternalCall} from "../libraries/ExternalCalls.sol"; interface IGovernance { - function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId); - function scheduleProposal(uint256 proposalId) external; + function submitProposal( + ExternalCall[] calldata calls, + string calldata metadata + ) external returns (uint256 proposalId); + function scheduleProposal( + uint256 proposalId + ) external; function cancelAllPendingProposals() external; - function canScheduleProposal(uint256 proposalId) external view returns (bool); + function canScheduleProposal( + uint256 proposalId + ) external view returns (bool); } diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index c54da3d7..74b8d0a1 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -15,26 +15,43 @@ interface ITimelock { ProposalStatus status; } - function submit(address executor, ExternalCall[] calldata calls) external returns (uint256 newProposalId); - function schedule(uint256 proposalId) external; - function execute(uint256 proposalId) external; + function submit( + address executor, + ExternalCall[] calldata calls, + string calldata metadata + ) external returns (uint256 newProposalId); + 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 canSchedule( + uint256 proposalId + ) external view returns (bool); + function canExecute( + uint256 proposalId + ) external view returns (bool); function getAdminExecutor() external view returns (address); - function getProposal(uint256 proposalId) - external - view - returns (ProposalDetails memory proposal, ExternalCall[] memory calls); - function getProposalDetails(uint256 proposalId) external view returns (ProposalDetails memory proposalDetails); + function getProposal( + uint256 proposalId + ) external view returns (ProposalDetails memory proposal, ExternalCall[] memory calls); + function getProposalDetails( + uint256 proposalId + ) external view returns (ProposalDetails memory proposalDetails); function getGovernance() external view returns (address); - function setGovernance(address governance) external; + function setGovernance( + address governance + ) external; function activateEmergencyMode() external; - function emergencyExecute(uint256 proposalId) external; + function emergencyExecute( + uint256 proposalId + ) external; function emergencyReset() external; } diff --git a/contracts/libraries/ExecutableProposals.sol b/contracts/libraries/ExecutableProposals.sol index d943e66b..a4b333dd 100644 --- a/contracts/libraries/ExecutableProposals.sol +++ b/contracts/libraries/ExecutableProposals.sol @@ -66,7 +66,7 @@ library ExecutableProposals { error AfterSubmitDelayNotPassed(uint256 proposalId); error AfterScheduleDelayNotPassed(uint256 proposalId); - event ProposalSubmitted(uint256 indexed id, address indexed executor, ExternalCall[] calls); + event ProposalSubmitted(uint256 indexed id, address indexed executor, ExternalCall[] calls, string metadata); event ProposalScheduled(uint256 indexed id); event ProposalExecuted(uint256 indexed id, bytes[] callResults); event ProposalsCancelledTill(uint256 proposalId); @@ -84,7 +84,8 @@ library ExecutableProposals { function submit( Context storage self, address executor, - ExternalCall[] memory calls + ExternalCall[] memory calls, + string memory metadata ) internal returns (uint256 newProposalId) { if (calls.length == 0) { revert EmptyCalls(); @@ -103,7 +104,7 @@ library ExecutableProposals { newProposal.calls.push(calls[i]); } - emit ProposalSubmitted(newProposalId, executor, calls); + emit ProposalSubmitted(newProposalId, executor, calls, metadata); } function schedule(Context storage self, uint256 proposalId, Duration afterSubmitDelay) internal { @@ -145,7 +146,9 @@ library ExecutableProposals { emit ProposalExecuted(proposalId, results); } - function cancelAll(Context storage self) internal { + function cancelAll( + Context storage self + ) internal { uint64 lastCancelledProposalId = self.proposalsCount; self.lastCancelledProposalId = lastCancelledProposalId; emit ProposalsCancelledTill(lastCancelledProposalId); @@ -177,7 +180,9 @@ library ExecutableProposals { && Timestamps.now() >= afterSubmitDelay.addTo(proposalState.submittedAt); } - function getProposalsCount(Context storage self) internal view returns (uint256) { + function getProposalsCount( + Context storage self + ) internal view returns (uint256) { return self.proposalsCount; } diff --git a/test/mocks/TimelockMock.sol b/test/mocks/TimelockMock.sol index f7c498f9..91a51f89 100644 --- a/test/mocks/TimelockMock.sol +++ b/test/mocks/TimelockMock.sol @@ -10,7 +10,9 @@ contract TimelockMock is ITimelock { address internal immutable _ADMIN_EXECUTOR; - constructor(address adminExecutor) { + constructor( + address adminExecutor + ) { _ADMIN_EXECUTOR = adminExecutor; } @@ -24,14 +26,16 @@ contract TimelockMock is ITimelock { address internal governance; - function submit(address, ExternalCall[] calldata) external returns (uint256 newProposalId) { + function submit(address, ExternalCall[] calldata, string calldata) external returns (uint256 newProposalId) { newProposalId = submittedProposals.length + OFFSET; submittedProposals.push(newProposalId); canScheduleProposal[newProposalId] = false; return newProposalId; } - function schedule(uint256 proposalId) external { + function schedule( + uint256 proposalId + ) external { if (canScheduleProposal[proposalId] == false) { revert(); } @@ -39,15 +43,21 @@ contract TimelockMock is ITimelock { scheduledProposals.push(proposalId); } - function execute(uint256 proposalId) external { + function execute( + uint256 proposalId + ) external { executedProposals.push(proposalId); } - function canExecute(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) { + function canSchedule( + uint256 proposalId + ) external view returns (bool) { return canScheduleProposal[proposalId]; } @@ -55,7 +65,9 @@ contract TimelockMock is ITimelock { lastCancelledProposalId = submittedProposals[submittedProposals.length - 1]; } - function setSchedule(uint256 proposalId) external { + function setSchedule( + uint256 proposalId + ) external { canScheduleProposal[proposalId] = true; } @@ -75,11 +87,15 @@ contract TimelockMock is ITimelock { return lastCancelledProposalId; } - function getProposal(uint256 proposalId) external view returns (ProposalDetails memory, ExternalCall[] memory) { + function getProposal( + uint256 proposalId + ) external view returns (ProposalDetails memory, ExternalCall[] memory) { revert("Not Implemented"); } - function setGovernance(address newGovernance) external { + function setGovernance( + address newGovernance + ) external { governance = newGovernance; } @@ -91,7 +107,9 @@ contract TimelockMock is ITimelock { revert("Not Implemented"); } - function emergencyExecute(uint256 proposalId) external { + function emergencyExecute( + uint256 proposalId + ) external { revert("Not Implemented"); } @@ -99,7 +117,9 @@ contract TimelockMock is ITimelock { revert("Not Implemented"); } - function getProposalDetails(uint256 proposalId) external view returns (ProposalDetails memory) { + function getProposalDetails( + uint256 proposalId + ) external view returns (ProposalDetails memory) { revert("Not Implemented"); } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index f381298d..3d4fad4e 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -237,6 +237,6 @@ contract DualGovernanceUnitTests is UnitTest { function _submitMockProposal() internal { // mock timelock doesn't uses proposal data - _timelock.submit(address(0), new ExternalCall[](0)); + _timelock.submit(address(0), new ExternalCall[](0), ""); } } diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 6dce1d50..a8ac09da 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -56,18 +56,20 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.submit() - function testFuzz_submit_RevertOn_ByStranger(address stranger) external { + function testFuzz_submit_RevertOn_ByStranger( + address stranger + ) external { vm.assume(stranger != _dualGovernance); vm.prank(stranger); vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotGovernance.selector, [stranger])); - _timelock.submit(_adminExecutor, new ExternalCall[](0)); + _timelock.submit(_adminExecutor, new ExternalCall[](0), ""); assertEq(_timelock.getProposalsCount(), 0); } function test_submit_HappyPath() external { vm.prank(_dualGovernance); - _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); + _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock)), ""); assertEq(_timelock.getProposalsCount(), 1); @@ -90,7 +92,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Scheduled); } - function testFuzz_schedule_RevertOn_ByStranger(address stranger) external { + function testFuzz_schedule_RevertOn_ByStranger( + address stranger + ) external { vm.assume(stranger != _dualGovernance); vm.assume(stranger != address(0)); @@ -107,7 +111,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.execute() - function testFuzz_execute_HappyPath(address stranger) external { + function testFuzz_execute_HappyPath( + address stranger + ) external { vm.assume(stranger != _dualGovernance); vm.assume(stranger != address(0)); @@ -175,7 +181,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal2.status, ProposalStatus.Cancelled); } - function testFuzz_cancelAllNonExecutedProposals_RevertOn_ByStranger(address stranger) external { + function testFuzz_cancelAllNonExecutedProposals_RevertOn_ByStranger( + address stranger + ) external { vm.assume(stranger != _dualGovernance); vm.assume(stranger != address(0)); @@ -201,7 +209,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getAfterScheduleDelay(), afterScheduleDelay); } - function test_setupDelays_RevertOn_ByStranger(address stranger) external { + function test_setupDelays_RevertOn_ByStranger( + address stranger + ) external { vm.assume(stranger != _adminExecutor); vm.assume(stranger != address(0)); @@ -212,7 +222,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.transferExecutorOwnership() - function testFuzz_transferExecutorOwnership_HappyPath(address newOwner) external { + function testFuzz_transferExecutorOwnership_HappyPath( + address newOwner + ) external { vm.assume(newOwner != _adminExecutor); vm.assume(newOwner != address(0)); @@ -230,7 +242,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(executor.owner(), newOwner); } - function test_transferExecutorOwnership_RevertOn_ByStranger(address stranger) external { + function test_transferExecutorOwnership_RevertOn_ByStranger( + address stranger + ) external { vm.assume(stranger != _adminExecutor); vm.prank(stranger); @@ -240,7 +254,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.setGovernance() - function testFuzz_setGovernance_HappyPath(address newGovernance) external { + function testFuzz_setGovernance_HappyPath( + address newGovernance + ) external { vm.assume(newGovernance != _dualGovernance); vm.assume(newGovernance != address(0)); @@ -273,7 +289,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getGovernance(), currentGovernance); } - function testFuzz_setGovernance_RevertOn_ByStranger(address stranger) external { + function testFuzz_setGovernance_RevertOn_ByStranger( + address stranger + ) external { vm.assume(stranger != _adminExecutor); vm.prank(stranger); @@ -290,7 +308,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), true); } - function testFuzz_activateEmergencyMode_RevertOn_ByStranger(address stranger) external { + function testFuzz_activateEmergencyMode_RevertOn_ByStranger( + address stranger + ) external { vm.assume(stranger != _emergencyActivator); vm.assume(stranger != address(0)); @@ -341,7 +361,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_emergencyExecute_RevertOn_ModeNotActive() external { vm.startPrank(_dualGovernance); - _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); + _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock)), ""); assertEq(_timelock.getProposalsCount(), 1); @@ -358,7 +378,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.emergencyExecute(1); } - function testFuzz_emergencyExecute_RevertOn_ByStranger(address stranger) external { + function testFuzz_emergencyExecute_RevertOn_ByStranger( + address stranger + ) external { vm.assume(stranger != _emergencyEnactor); vm.assume(stranger != address(0)); @@ -411,7 +433,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Cancelled); } - function testFuzz_deactivateEmergencyMode_HappyPath_ByStranger(address stranger) external { + function testFuzz_deactivateEmergencyMode_HappyPath_ByStranger( + address stranger + ) external { vm.assume(stranger != _adminExecutor); _activateEmergencyMode(); @@ -427,7 +451,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), false); } - function testFuzz_deactivateEmergencyMode_RevertOn_ModeNotActivated(address stranger) external { + function testFuzz_deactivateEmergencyMode_RevertOn_ModeNotActivated( + address stranger + ) external { vm.assume(stranger != _adminExecutor); vm.prank(stranger); @@ -439,7 +465,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.deactivateEmergencyMode(); } - function testFuzz_deactivateEmergencyMode_RevertOn_ByStranger_ModeNotExpired(address stranger) external { + function testFuzz_deactivateEmergencyMode_RevertOn_ByStranger_ModeNotExpired( + address stranger + ) external { vm.assume(stranger != _adminExecutor); _activateEmergencyMode(); @@ -488,7 +516,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Cancelled); } - function testFuzz_emergencyReset_RevertOn_ByStranger(address stranger) external { + function testFuzz_emergencyReset_RevertOn_ByStranger( + address stranger + ) external { vm.assume(stranger != _emergencyEnactor); vm.assume(stranger != address(0)); @@ -540,7 +570,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertFalse(_timelock.isEmergencyModeActive()); } - function testFuzz_setActivationCommittee_RevertOn_ByStranger(address stranger) external { + function testFuzz_setActivationCommittee_RevertOn_ByStranger( + address stranger + ) external { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); @@ -565,7 +597,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertFalse(_localTimelock.isEmergencyModeActive()); } - function testFuzz_setExecutionCommittee_RevertOn_ByStranger(address stranger) external { + function testFuzz_setExecutionCommittee_RevertOn_ByStranger( + address stranger + ) external { vm.assume(stranger != _adminExecutor && stranger != _emergencyEnactor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); @@ -593,7 +627,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertFalse(_localTimelock.isEmergencyModeActive()); } - function testFuzz_setProtectionEndDate_RevertOn_ByStranger(address stranger) external { + function testFuzz_setProtectionEndDate_RevertOn_ByStranger( + address stranger + ) external { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); @@ -624,7 +660,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertFalse(_localTimelock.isEmergencyModeActive()); } - function testFuzz_setModeDuration_RevertOn_ByStranger(address stranger) external { + function testFuzz_setModeDuration_RevertOn_ByStranger( + address stranger + ) external { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); @@ -778,7 +816,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.getGovernance() - function testFuzz_get_governance(address governance) external { + function testFuzz_get_governance( + address governance + ) external { vm.assume(governance != address(0) && governance != _timelock.getGovernance()); vm.prank(_adminExecutor); _timelock.setGovernance(governance); @@ -792,8 +832,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.startPrank(_dualGovernance); ExternalCall[] memory executorCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); - _timelock.submit(_adminExecutor, executorCalls); - _timelock.submit(_adminExecutor, executorCalls); + _timelock.submit(_adminExecutor, executorCalls, ""); + _timelock.submit(_adminExecutor, executorCalls, ""); (ITimelock.ProposalDetails memory submittedProposal, ExternalCall[] memory calls) = _timelock.getProposal(1); @@ -874,7 +914,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.getProposalsCount() - function testFuzz_get_proposals_count(uint256 count) external { + function testFuzz_get_proposals_count( + uint256 count + ) external { vm.assume(count > 0); vm.assume(count <= type(uint8).max); assertEq(_timelock.getProposalsCount(), 0); @@ -950,7 +992,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_getProposalCalls() external { ExternalCall[] memory executorCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); vm.prank(_dualGovernance); - _timelock.submit(_adminExecutor, executorCalls); + _timelock.submit(_adminExecutor, executorCalls, ""); ExternalCall[] memory calls = _timelock.getProposalCalls(1); @@ -960,7 +1002,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(calls[0].payload, executorCalls[0].payload); } - function testFuzz_getAdminExecutor(address executor) external { + function testFuzz_getAdminExecutor( + address executor + ) external { EmergencyProtectedTimelock timelock = new EmergencyProtectedTimelock( EmergencyProtectedTimelock.SanityCheckParams({ maxAfterSubmitDelay: Durations.from(45 days), @@ -978,10 +1022,12 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function _submitProposal() internal { vm.prank(_dualGovernance); - _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); + _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock)), ""); } - function _scheduleProposal(uint256 proposalId) internal { + function _scheduleProposal( + uint256 proposalId + ) internal { vm.prank(_dualGovernance); _timelock.schedule(proposalId); } diff --git a/test/unit/TimelockedGovernance.t.sol b/test/unit/TimelockedGovernance.t.sol index 52cab0fc..eea3157e 100644 --- a/test/unit/TimelockedGovernance.t.sol +++ b/test/unit/TimelockedGovernance.t.sol @@ -32,19 +32,21 @@ contract TimelockedGovernanceUnitTests is UnitTest { assertEq(_timelock.getSubmittedProposals().length, 0); vm.prank(_governance); - _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1)), ""); assertEq(_timelock.getSubmittedProposals().length, 1); } - function testFuzz_stranger_cannot_submit_proposal(address stranger) external { + 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(TimelockedGovernance.CallerIsNotGovernance.selector, [stranger])); - _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1)), ""); assertEq(_timelock.getSubmittedProposals().length, 0); } @@ -53,7 +55,7 @@ contract TimelockedGovernanceUnitTests is UnitTest { assertEq(_timelock.getScheduledProposals().length, 0); vm.prank(_governance); - _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1)), ""); _timelock.setSchedule(1); _timelockedGovernance.scheduleProposal(1); @@ -65,7 +67,7 @@ contract TimelockedGovernanceUnitTests is UnitTest { assertEq(_timelock.getExecutedProposals().length, 0); vm.prank(_governance); - _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1)), ""); _timelock.setSchedule(1); _timelockedGovernance.scheduleProposal(1); @@ -79,8 +81,8 @@ contract TimelockedGovernanceUnitTests is UnitTest { assertEq(_timelock.getLastCancelledProposalId(), 0); vm.startPrank(_governance); - _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); - _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1)), ""); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1)), ""); _timelock.setSchedule(1); _timelockedGovernance.scheduleProposal(1); @@ -90,7 +92,9 @@ contract TimelockedGovernanceUnitTests is UnitTest { assertEq(_timelock.getLastCancelledProposalId(), 2); } - function testFuzz_stranger_cannot_cancel_all_pending_proposals(address stranger) external { + function testFuzz_stranger_cannot_cancel_all_pending_proposals( + address stranger + ) external { vm.assume(stranger != address(0) && stranger != _governance); assertEq(_timelock.getLastCancelledProposalId(), 0); @@ -104,7 +108,7 @@ contract TimelockedGovernanceUnitTests is UnitTest { function test_can_schedule() external { vm.prank(_governance); - _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1)), ""); assertFalse(_timelockedGovernance.canScheduleProposal(1)); diff --git a/test/unit/libraries/ExecutableProposals.t.sol b/test/unit/libraries/ExecutableProposals.t.sol index 987bf5f9..157c079f 100644 --- a/test/unit/libraries/ExecutableProposals.t.sol +++ b/test/unit/libraries/ExecutableProposals.t.sol @@ -31,7 +31,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_submit_reverts_if_empty_proposals() external { vm.expectRevert(ExecutableProposals.EmptyCalls.selector); - _proposals.submit(address(0), new ExternalCall[](0)); + _proposals.submit(address(0), new ExternalCall[](0), "Empty calls"); } function test_submit_proposal() external { @@ -40,13 +40,14 @@ contract ExecutableProposalsUnitTests is UnitTest { ExternalCall[] memory calls = _getMockTargetRegularStaffCalls(address(_targetMock)); uint256 expectedProposalId = proposalsCount + PROPOSAL_ID_OFFSET; + string memory description = "Regular staff calls"; vm.expectEmit(); - emit ExecutableProposals.ProposalSubmitted(expectedProposalId, address(_executor), calls); + emit ExecutableProposals.ProposalSubmitted(expectedProposalId, address(_executor), calls, description); vm.recordLogs(); - _proposals.submit(address(_executor), calls); + _proposals.submit(address(_executor), calls, description); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 1); @@ -69,10 +70,12 @@ contract ExecutableProposalsUnitTests is UnitTest { } } - function testFuzz_schedule_proposal(Duration delay) external { + function testFuzz_schedule_proposal( + Duration delay + ) external { vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 expectedProposalId = 1; ExecutableProposals.Proposal memory proposal = _proposals.proposals[expectedProposalId]; @@ -96,7 +99,9 @@ contract ExecutableProposalsUnitTests is UnitTest { assertEq(proposal.data.scheduledAt, Timestamps.now()); } - function testFuzz_cannot_schedule_unsubmitted_proposal(uint256 proposalId) external { + function testFuzz_cannot_schedule_unsubmitted_proposal( + uint256 proposalId + ) external { vm.assume(proposalId > 0); vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotSubmitted.selector, proposalId)); @@ -104,7 +109,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cannot_schedule_proposal_twice() external { - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 proposalId = 1; _proposals.schedule(proposalId, Durations.ZERO); @@ -112,10 +117,12 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.schedule(proposalId, Durations.ZERO); } - function testFuzz_cannot_schedule_proposal_before_delay_passed(Duration delay) external { + function testFuzz_cannot_schedule_proposal_before_delay_passed( + Duration delay + ) external { vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); _wait(delay.minusSeconds(1 seconds)); @@ -126,7 +133,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cannot_schedule_cancelled_proposal() external { - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); _proposals.cancelAll(); uint256 proposalId = _proposals.getProposalsCount(); @@ -135,10 +142,12 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.schedule(proposalId, Durations.ZERO); } - function testFuzz_execute_proposal(Duration delay) external { + function testFuzz_execute_proposal( + Duration delay + ) external { vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); @@ -167,14 +176,16 @@ contract ExecutableProposalsUnitTests is UnitTest { assertEq(proposal.data.scheduledAt, submittedAndScheduledAt); } - function testFuzz_cannot_execute_unsubmitted_proposal(uint256 proposalId) external { + function testFuzz_cannot_execute_unsubmitted_proposal( + uint256 proposalId + ) external { vm.assume(proposalId > 0); vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); _proposals.execute(proposalId, Durations.ZERO); } function test_cannot_execute_unscheduled_proposal() external { - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 proposalId = _proposals.getProposalsCount(); vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); @@ -182,7 +193,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cannot_execute_twice() external { - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); _proposals.execute(proposalId, Durations.ZERO); @@ -192,7 +203,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cannot_execute_cancelled_proposal() external { - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); _proposals.cancelAll(); @@ -201,9 +212,11 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.execute(proposalId, Durations.ZERO); } - function testFuzz_cannot_execute_before_delay_passed(Duration delay) external { + function testFuzz_cannot_execute_before_delay_passed( + Duration delay + ) external { vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); @@ -214,8 +227,8 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cancel_all_proposals() external { - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 proposalsCount = _proposals.getProposalsCount(); @@ -231,7 +244,7 @@ contract ExecutableProposalsUnitTests is UnitTest { // TODO: change this test completely to use getters function test_get_proposal_info_and_external_calls() external { ExternalCall[] memory expectedCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); - _proposals.submit(address(_executor), expectedCalls); + _proposals.submit(address(_executor), expectedCalls, ""); uint256 proposalId = _proposals.getProposalsCount(); ITimelock.ProposalDetails memory proposalDetails = _proposals.getProposalDetails(proposalId); @@ -293,7 +306,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_get_cancelled_proposal() external { ExternalCall[] memory expectedCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); - _proposals.submit(address(_executor), expectedCalls); + _proposals.submit(address(_executor), expectedCalls, ""); uint256 proposalId = _proposals.getProposalsCount(); ITimelock.ProposalDetails memory proposalDetails = _proposals.getProposalDetails(proposalId); @@ -333,7 +346,9 @@ contract ExecutableProposalsUnitTests is UnitTest { } } - function testFuzz_get_not_existing_proposal(uint256 proposalId) external { + function testFuzz_get_not_existing_proposal( + uint256 proposalId + ) external { vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotFound.selector, proposalId)); _proposals.getProposalDetails(proposalId); @@ -344,16 +359,16 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_count_proposals() external { assertEq(_proposals.getProposalsCount(), 0); - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); assertEq(_proposals.getProposalsCount(), 1); - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); assertEq(_proposals.getProposalsCount(), 2); _proposals.schedule(1, Durations.ZERO); assertEq(_proposals.getProposalsCount(), 2); - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); assertEq(_proposals.getProposalsCount(), 3); _proposals.schedule(2, Durations.ZERO); @@ -362,7 +377,7 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.execute(1, Durations.ZERO); assertEq(_proposals.getProposalsCount(), 3); - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); assertEq(_proposals.getProposalsCount(), 4); _proposals.cancelAll(); @@ -371,7 +386,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_can_execute_proposal() external { Duration delay = Durations.from(100 seconds); - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 proposalId = _proposals.getProposalsCount(); assert(!_proposals.canExecute(proposalId, Durations.ZERO)); @@ -390,7 +405,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_can_not_execute_cancelled_proposal() external { - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); @@ -401,18 +416,18 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cancelAll_DoesNotModifyStateOfExecutedProposals() external { - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); assertEq(_proposals.getProposalsCount(), 1); uint256 executedProposalId = 1; _proposals.schedule(executedProposalId, Durations.ZERO); _proposals.execute(executedProposalId, Durations.ZERO); - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); assertEq(_proposals.getProposalsCount(), 2); uint256 scheduledProposalId = 2; _proposals.schedule(scheduledProposalId, Durations.ZERO); - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); assertEq(_proposals.getProposalsCount(), 3); uint256 submittedProposalId = 3; @@ -434,7 +449,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_can_schedule_proposal() external { Duration delay = Durations.from(100 seconds); - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 proposalId = _proposals.getProposalsCount(); assert(!_proposals.canSchedule(proposalId, delay)); @@ -449,7 +464,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_can_not_schedule_cancelled_proposal() external { - _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 proposalId = _proposals.getProposalsCount(); assert(_proposals.canSchedule(proposalId, Durations.ZERO)); diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index f8b72288..dd90eb26 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -126,7 +126,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { _lido.submitWstETH(account, _lido.calcSharesToDepositFromPercentageOfTVL(tvlPercentage)); } - function _getBalances(address vetoer) internal view returns (Balances memory balances) { + function _getBalances( + address vetoer + ) internal view returns (Balances memory balances) { uint256 stETHAmount = _lido.stETH.balanceOf(vetoer); uint256 wstETHShares = _lido.wstETH.balanceOf(vetoer); balances = Balances({ @@ -144,11 +146,15 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { _lido.finalizeWithdrawalQueue(); } - function _finalizeWithdrawalQueue(uint256 id) internal { + function _finalizeWithdrawalQueue( + uint256 id + ) internal { _lido.finalizeWithdrawalQueue(id); } - function _simulateRebase(PercentD16 rebaseFactor) internal { + function _simulateRebase( + PercentD16 rebaseFactor + ) internal { _lido.simulateRebase(rebaseFactor); } @@ -169,7 +175,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { vm.stopPrank(); } - function _unlockStETH(address vetoer) internal { + function _unlockStETH( + address vetoer + ) internal { vm.startPrank(vetoer); _getVetoSignallingEscrow().unlockStETH(); vm.stopPrank(); @@ -189,7 +197,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { vm.stopPrank(); } - function _unlockWstETH(address vetoer) internal { + function _unlockWstETH( + address vetoer + ) internal { Escrow escrow = _getVetoSignallingEscrow(); uint256 wstETHBalanceBefore = _lido.wstETH.balanceOf(vetoer); VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); @@ -296,8 +306,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ) internal returns (uint256 proposalId) { uint256 proposalsCountBefore = _timelock.getProposalsCount(); - bytes memory script = - EvmScriptUtils.encodeEvmCallScript(address(governance), abi.encodeCall(IGovernance.submitProposal, (calls))); + bytes memory script = EvmScriptUtils.encodeEvmCallScript( + address(governance), abi.encodeCall(IGovernance.submitProposal, (calls, string(""))) + ); uint256 voteId = _lido.adoptVote(description, script); // The scheduled calls count is the same until the vote is enacted @@ -311,11 +322,15 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(proposalId, proposalsCountBefore + 1); } - function _scheduleProposalViaDualGovernance(uint256 proposalId) internal { + function _scheduleProposalViaDualGovernance( + uint256 proposalId + ) internal { _scheduleProposal(_dualGovernance, proposalId); } - function _scheduleProposalViaTimelockedGovernance(uint256 proposalId) internal { + function _scheduleProposalViaTimelockedGovernance( + uint256 proposalId + ) internal { _scheduleProposal(_timelockedGovernance, proposalId); } @@ -323,7 +338,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { governance.scheduleProposal(proposalId); } - function _executeProposal(uint256 proposalId) internal { + function _executeProposal( + uint256 proposalId + ) internal { _timelock.execute(proposalId); } @@ -409,7 +426,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalSubmitted(uint256 proposalId) internal { + function _assertProposalSubmitted( + uint256 proposalId + ) internal { assertEq( _timelock.getProposalDetails(proposalId).status, ProposalStatus.Submitted, @@ -417,7 +436,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalScheduled(uint256 proposalId) internal { + function _assertProposalScheduled( + uint256 proposalId + ) internal { assertEq( _timelock.getProposalDetails(proposalId).status, ProposalStatus.Scheduled, @@ -425,7 +446,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalExecuted(uint256 proposalId) internal { + function _assertProposalExecuted( + uint256 proposalId + ) internal { assertEq( _timelock.getProposalDetails(proposalId).status, ProposalStatus.Executed, @@ -433,7 +456,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalCancelled(uint256 proposalId) internal { + function _assertProposalCancelled( + uint256 proposalId + ) internal { assertEq( _timelock.getProposalDetails(proposalId).status, ProposalStatus.Cancelled, @@ -524,12 +549,16 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { // Utils Methods // --- - function _step(string memory text) internal view { + function _step( + string memory text + ) internal view { // solhint-disable-next-line console.log(string.concat(">>> ", text, " <<<")); } - function _wait(Duration duration) internal { + function _wait( + Duration duration + ) internal { vm.warp(duration.addTo(Timestamps.now()).toSeconds()); } @@ -550,7 +579,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { _emergencyActivationCommittee.executeActivateEmergencyMode(); } - function _executeEmergencyExecute(uint256 proposalId) internal { + function _executeEmergencyExecute( + uint256 proposalId + ) internal { address[] memory members = _emergencyExecutionCommittee.getMembers(); for (uint256 i = 0; i < _emergencyExecutionCommittee.quorum(); ++i) { vm.prank(members[i]); @@ -575,14 +606,18 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { uint256 _seconds; } - function _toDuration(uint256 timestamp) internal pure returns (DurationStruct memory duration) { + function _toDuration( + uint256 timestamp + ) internal pure returns (DurationStruct memory duration) { duration._days = timestamp / 1 days; duration._hours = (timestamp - 1 days * duration._days) / 1 hours; duration._minutes = (timestamp - 1 days * duration._days - 1 hours * duration._hours) / 1 minutes; duration._seconds = timestamp % 1 minutes; } - function _formatDuration(DurationStruct memory duration) internal pure returns (string memory) { + function _formatDuration( + DurationStruct memory duration + ) internal pure returns (string memory) { // format example: 1d:22h:33m:12s return string( abi.encodePacked( From 24ecc5928cae114df52c81173ae92f32ed2eff2c Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 9 Sep 2024 17:00:20 +0400 Subject: [PATCH 35/86] Fix foundry fmt settings --- foundry.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/foundry.toml b/foundry.toml index b40e7e68..a41bd76c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ src = 'contracts' out = 'out' libs = ['node_modules', 'lib'] test = 'test' -cache_path = 'cache_forge' +cache_path = 'cache_forge' # solc-version = "0.8.26" no-match-path = 'test/kontrol/*' @@ -13,4 +13,5 @@ out = 'kout' test = 'test/kontrol' [fmt] -multiline_func_header = 'params_first' +line_length = 120 +multiline_func_header = 'params_first_multi' From 009cf03f37db78182166373343c3379af0efda9a Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Mon, 9 Sep 2024 17:36:36 +0300 Subject: [PATCH 36/86] fix: formatter --- contracts/DualGovernance.sol | 64 ++++--------- contracts/EmergencyProtectedTimelock.sol | 60 ++++-------- contracts/interfaces/IGovernance.sol | 8 +- contracts/interfaces/ITimelock.sol | 35 +++---- contracts/libraries/ExecutableProposals.sol | 8 +- test/mocks/TimelockMock.sol | 40 ++------ test/unit/EmergencyProtectedTimelock.t.sol | 92 +++++-------------- test/unit/TimelockedGovernance.t.sol | 8 +- test/unit/libraries/ExecutableProposals.t.sol | 28 ++---- test/utils/scenario-test-blueprint.sol | 68 ++++---------- 10 files changed, 108 insertions(+), 303 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index dc28b5f7..b5ca36ca 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -133,9 +133,7 @@ contract DualGovernance is IDualGovernance { proposalId = TIMELOCK.submit(proposer.executor, calls, metadata); } - function scheduleProposal( - uint256 proposalId - ) external { + function scheduleProposal(uint256 proposalId) external { _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); ITimelock.ProposalDetails memory proposalDetails = TIMELOCK.getProposalDetails(proposalId); if (!_stateMachine.canScheduleProposal(proposalDetails.submittedAt)) { @@ -172,9 +170,7 @@ contract DualGovernance is IDualGovernance { return _stateMachine.canSubmitProposal(); } - function canScheduleProposal( - uint256 proposalId - ) external view returns (bool) { + function canScheduleProposal(uint256 proposalId) external view returns (bool) { ITimelock.ProposalDetails memory proposalDetails = TIMELOCK.getProposalDetails(proposalId); return _stateMachine.canScheduleProposal(proposalDetails.submittedAt) && TIMELOCK.canSchedule(proposalId); } @@ -187,9 +183,7 @@ contract DualGovernance is IDualGovernance { _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); } - function setConfigProvider( - IDualGovernanceConfigProvider newConfigProvider - ) external { + function setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) external { _checkCallerIsAdminExecutor(); _setConfigProvider(newConfigProvider); @@ -229,9 +223,7 @@ contract DualGovernance is IDualGovernance { _proposers.register(proposer, executor); } - function unregisterProposer( - address proposer - ) external { + function unregisterProposer(address proposer) external { _checkCallerIsAdminExecutor(); _proposers.unregister(proposer); @@ -241,15 +233,11 @@ contract DualGovernance is IDualGovernance { } } - function isProposer( - address account - ) external view returns (bool) { + function isProposer(address account) external view returns (bool) { return _proposers.isProposer(account); } - function getProposer( - address account - ) external view returns (Proposers.Proposer memory proposer) { + function getProposer(address account) external view returns (Proposers.Proposer memory proposer) { proposer = _proposers.getProposer(account); } @@ -257,9 +245,7 @@ contract DualGovernance is IDualGovernance { proposers = _proposers.getAllProposers(); } - function isExecutor( - address account - ) external view returns (bool) { + function isExecutor(address account) external view returns (bool) { return _proposers.isExecutor(account); } @@ -267,48 +253,36 @@ contract DualGovernance is IDualGovernance { // Tiebreaker Protection // --- - function addTiebreakerSealableWithdrawalBlocker( - address sealableWithdrawalBlocker - ) external { + function addTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { _checkCallerIsAdminExecutor(); _tiebreaker.addSealableWithdrawalBlocker(sealableWithdrawalBlocker, MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT); } - function removeTiebreakerSealableWithdrawalBlocker( - address sealableWithdrawalBlocker - ) external { + function removeTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { _checkCallerIsAdminExecutor(); _tiebreaker.removeSealableWithdrawalBlocker(sealableWithdrawalBlocker); } - function setTiebreakerCommittee( - address tiebreakerCommittee - ) external { + function setTiebreakerCommittee(address tiebreakerCommittee) external { _checkCallerIsAdminExecutor(); _tiebreaker.setTiebreakerCommittee(tiebreakerCommittee); } - function setTiebreakerActivationTimeout( - Duration tiebreakerActivationTimeout - ) external { + function setTiebreakerActivationTimeout(Duration tiebreakerActivationTimeout) external { _checkCallerIsAdminExecutor(); _tiebreaker.setTiebreakerActivationTimeout( MIN_TIEBREAKER_ACTIVATION_TIMEOUT, tiebreakerActivationTimeout, MAX_TIEBREAKER_ACTIVATION_TIMEOUT ); } - function tiebreakerResumeSealable( - address sealable - ) external { + function tiebreakerResumeSealable(address sealable) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); _tiebreaker.checkTie(_stateMachine.getState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); RESEAL_MANAGER.resume(sealable); } - function tiebreakerScheduleProposal( - uint256 proposalId - ) external { + function tiebreakerScheduleProposal(uint256 proposalId) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); _tiebreaker.checkTie(_stateMachine.getState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); @@ -325,9 +299,7 @@ contract DualGovernance is IDualGovernance { // Reseal executor // --- - function resealSealable( - address sealable - ) external { + function resealSealable(address sealable) external { if (msg.sender != _resealCommittee) { revert CallerIsNotResealCommittee(msg.sender); } @@ -337,9 +309,7 @@ contract DualGovernance is IDualGovernance { RESEAL_MANAGER.reseal(sealable); } - function setResealCommittee( - address resealCommittee - ) external { + function setResealCommittee(address resealCommittee) external { _checkCallerIsAdminExecutor(); _resealCommittee = resealCommittee; } @@ -348,9 +318,7 @@ contract DualGovernance is IDualGovernance { // Private methods // --- - function _setConfigProvider( - IDualGovernanceConfigProvider newConfigProvider - ) internal { + function _setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) internal { if (address(newConfigProvider) == address(0)) { revert InvalidConfigProvider(newConfigProvider); } diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index f35f7a0a..c66547d7 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -85,9 +85,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @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 { + function schedule(uint256 proposalId) external { _timelockState.checkCallerIsGovernance(); _proposals.schedule(proposalId, _timelockState.getAfterSubmitDelay()); } @@ -95,9 +93,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @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 { + function execute(uint256 proposalId) external { _emergencyProtection.checkEmergencyMode({isActive: false}); _proposals.execute(proposalId, _timelockState.getAfterScheduleDelay()); } @@ -113,9 +109,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { // Timelock Management // --- - function setGovernance( - address newGovernance - ) external { + function setGovernance(address newGovernance) external { _checkCallerIsAdminExecutor(); _timelockState.setGovernance(newGovernance); } @@ -141,27 +135,21 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @dev Sets the emergency activation committee address. /// @param emergencyActivationCommittee The address of the emergency activation committee. - function setEmergencyProtectionActivationCommittee( - address emergencyActivationCommittee - ) external { + function setEmergencyProtectionActivationCommittee(address emergencyActivationCommittee) external { _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyActivationCommittee(emergencyActivationCommittee); } /// @dev Sets the emergency execution committee address. /// @param emergencyExecutionCommittee The address of the emergency execution committee. - function setEmergencyProtectionExecutionCommittee( - address emergencyExecutionCommittee - ) external { + function setEmergencyProtectionExecutionCommittee(address emergencyExecutionCommittee) external { _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyExecutionCommittee(emergencyExecutionCommittee); } /// @dev Sets the emergency protection end date. /// @param emergencyProtectionEndDate The timestamp of the emergency protection end date. - function setEmergencyProtectionEndDate( - Timestamp emergencyProtectionEndDate - ) external { + function setEmergencyProtectionEndDate(Timestamp emergencyProtectionEndDate) external { _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyProtectionEndDate( emergencyProtectionEndDate, MAX_EMERGENCY_PROTECTION_DURATION @@ -170,18 +158,14 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @dev Sets the emergency mode duration. /// @param emergencyModeDuration The duration of the emergency mode. - function setEmergencyModeDuration( - Duration emergencyModeDuration - ) external { + function setEmergencyModeDuration(Duration emergencyModeDuration) external { _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyModeDuration(emergencyModeDuration, MAX_EMERGENCY_MODE_DURATION); } /// @dev Sets the emergency governance address. /// @param emergencyGovernance The address of the emergency governance. - function setEmergencyGovernance( - address emergencyGovernance - ) external { + function setEmergencyGovernance(address emergencyGovernance) external { _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyGovernance(emergencyGovernance); } @@ -197,9 +181,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @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 { + function emergencyExecute(uint256 proposalId) external { _emergencyProtection.checkEmergencyMode({isActive: true}); _emergencyProtection.checkCallerIsEmergencyExecutionCommittee(); _proposals.execute({proposalId: proposalId, afterScheduleDelay: Duration.wrap(0)}); @@ -287,9 +269,11 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @param proposalId The ID of the proposal. /// @return proposalDetails The Proposal struct containing the details of the proposal. /// @return calls An array of ExternalCall structs representing the sequence of calls to be executed for the proposal. - function getProposal( - uint256 proposalId - ) external view returns (ProposalDetails memory proposalDetails, ExternalCall[] memory calls) { + function getProposal(uint256 proposalId) + external + view + returns (ProposalDetails memory proposalDetails, ExternalCall[] memory calls) + { proposalDetails = _proposals.getProposalDetails(proposalId); calls = _proposals.getProposalCalls(proposalId); } @@ -309,18 +293,14 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// submittedAt The timestamp when the proposal was submitted. /// scheduledAt The timestamp when the proposal was scheduled for execution. Equals 0 if the proposal /// was submitted but not yet scheduled. - function getProposalDetails( - uint256 proposalId - ) external view returns (ProposalDetails memory proposalDetails) { + function getProposalDetails(uint256 proposalId) external view returns (ProposalDetails memory proposalDetails) { return _proposals.getProposalDetails(proposalId); } /// @notice Retrieves the external calls associated with the specified proposal. /// @param proposalId The ID of the proposal to retrieve external calls for. /// @return calls An array of ExternalCall structs representing the sequence of calls to be executed for the proposal. - function getProposalCalls( - uint256 proposalId - ) external view returns (ExternalCall[] memory calls) { + function getProposalCalls(uint256 proposalId) external view returns (ExternalCall[] memory calls) { calls = _proposals.getProposalCalls(proposalId); } @@ -333,9 +313,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @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) { + function canExecute(uint256 proposalId) external view returns (bool) { return !_emergencyProtection.isEmergencyModeActive() && _proposals.canExecute(proposalId, _timelockState.getAfterScheduleDelay()); } @@ -343,9 +321,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @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) { + function canSchedule(uint256 proposalId) external view returns (bool) { return _proposals.canSchedule(proposalId, _timelockState.getAfterSubmitDelay()); } diff --git a/contracts/interfaces/IGovernance.sol b/contracts/interfaces/IGovernance.sol index 7556d3ab..df14df66 100644 --- a/contracts/interfaces/IGovernance.sol +++ b/contracts/interfaces/IGovernance.sol @@ -8,12 +8,8 @@ interface IGovernance { ExternalCall[] calldata calls, string calldata metadata ) external returns (uint256 proposalId); - function scheduleProposal( - uint256 proposalId - ) external; + function scheduleProposal(uint256 proposalId) external; function cancelAllPendingProposals() external; - function canScheduleProposal( - uint256 proposalId - ) external view returns (bool); + function canScheduleProposal(uint256 proposalId) external view returns (bool); } diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index 74b8d0a1..eef1f120 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -20,38 +20,25 @@ interface ITimelock { ExternalCall[] calldata calls, string calldata metadata ) external returns (uint256 newProposalId); - function schedule( - uint256 proposalId - ) external; - function execute( - uint256 proposalId - ) external; + 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 canSchedule(uint256 proposalId) external view returns (bool); + function canExecute(uint256 proposalId) external view returns (bool); function getAdminExecutor() external view returns (address); - function getProposal( - uint256 proposalId - ) external view returns (ProposalDetails memory proposal, ExternalCall[] memory calls); - function getProposalDetails( - uint256 proposalId - ) external view returns (ProposalDetails memory proposalDetails); + function getProposal(uint256 proposalId) + external + view + returns (ProposalDetails memory proposal, ExternalCall[] memory calls); + function getProposalDetails(uint256 proposalId) external view returns (ProposalDetails memory proposalDetails); function getGovernance() external view returns (address); - function setGovernance( - address governance - ) external; + function setGovernance(address governance) external; function activateEmergencyMode() external; - function emergencyExecute( - uint256 proposalId - ) external; + function emergencyExecute(uint256 proposalId) external; function emergencyReset() external; } diff --git a/contracts/libraries/ExecutableProposals.sol b/contracts/libraries/ExecutableProposals.sol index a4b333dd..cd1a9130 100644 --- a/contracts/libraries/ExecutableProposals.sol +++ b/contracts/libraries/ExecutableProposals.sol @@ -146,9 +146,7 @@ library ExecutableProposals { emit ProposalExecuted(proposalId, results); } - function cancelAll( - Context storage self - ) internal { + function cancelAll(Context storage self) internal { uint64 lastCancelledProposalId = self.proposalsCount; self.lastCancelledProposalId = lastCancelledProposalId; emit ProposalsCancelledTill(lastCancelledProposalId); @@ -180,9 +178,7 @@ library ExecutableProposals { && Timestamps.now() >= afterSubmitDelay.addTo(proposalState.submittedAt); } - function getProposalsCount( - Context storage self - ) internal view returns (uint256) { + function getProposalsCount(Context storage self) internal view returns (uint256) { return self.proposalsCount; } diff --git a/test/mocks/TimelockMock.sol b/test/mocks/TimelockMock.sol index 91a51f89..543fd6e8 100644 --- a/test/mocks/TimelockMock.sol +++ b/test/mocks/TimelockMock.sol @@ -10,9 +10,7 @@ contract TimelockMock is ITimelock { address internal immutable _ADMIN_EXECUTOR; - constructor( - address adminExecutor - ) { + constructor(address adminExecutor) { _ADMIN_EXECUTOR = adminExecutor; } @@ -33,9 +31,7 @@ contract TimelockMock is ITimelock { return newProposalId; } - function schedule( - uint256 proposalId - ) external { + function schedule(uint256 proposalId) external { if (canScheduleProposal[proposalId] == false) { revert(); } @@ -43,21 +39,15 @@ contract TimelockMock is ITimelock { scheduledProposals.push(proposalId); } - function execute( - uint256 proposalId - ) external { + function execute(uint256 proposalId) external { executedProposals.push(proposalId); } - function canExecute( - 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) { + function canSchedule(uint256 proposalId) external view returns (bool) { return canScheduleProposal[proposalId]; } @@ -65,9 +55,7 @@ contract TimelockMock is ITimelock { lastCancelledProposalId = submittedProposals[submittedProposals.length - 1]; } - function setSchedule( - uint256 proposalId - ) external { + function setSchedule(uint256 proposalId) external { canScheduleProposal[proposalId] = true; } @@ -87,15 +75,11 @@ contract TimelockMock is ITimelock { return lastCancelledProposalId; } - function getProposal( - uint256 proposalId - ) external view returns (ProposalDetails memory, ExternalCall[] memory) { + function getProposal(uint256 proposalId) external view returns (ProposalDetails memory, ExternalCall[] memory) { revert("Not Implemented"); } - function setGovernance( - address newGovernance - ) external { + function setGovernance(address newGovernance) external { governance = newGovernance; } @@ -107,9 +91,7 @@ contract TimelockMock is ITimelock { revert("Not Implemented"); } - function emergencyExecute( - uint256 proposalId - ) external { + function emergencyExecute(uint256 proposalId) external { revert("Not Implemented"); } @@ -117,9 +99,7 @@ contract TimelockMock is ITimelock { revert("Not Implemented"); } - function getProposalDetails( - uint256 proposalId - ) external view returns (ProposalDetails memory) { + function getProposalDetails(uint256 proposalId) external view returns (ProposalDetails memory) { revert("Not Implemented"); } diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index a8ac09da..4feeb489 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -56,9 +56,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.submit() - function testFuzz_submit_RevertOn_ByStranger( - address stranger - ) external { + function testFuzz_submit_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _dualGovernance); vm.prank(stranger); @@ -92,9 +90,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Scheduled); } - function testFuzz_schedule_RevertOn_ByStranger( - address stranger - ) external { + function testFuzz_schedule_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _dualGovernance); vm.assume(stranger != address(0)); @@ -111,9 +107,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.execute() - function testFuzz_execute_HappyPath( - address stranger - ) external { + function testFuzz_execute_HappyPath(address stranger) external { vm.assume(stranger != _dualGovernance); vm.assume(stranger != address(0)); @@ -181,9 +175,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal2.status, ProposalStatus.Cancelled); } - function testFuzz_cancelAllNonExecutedProposals_RevertOn_ByStranger( - address stranger - ) external { + function testFuzz_cancelAllNonExecutedProposals_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _dualGovernance); vm.assume(stranger != address(0)); @@ -209,9 +201,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getAfterScheduleDelay(), afterScheduleDelay); } - function test_setupDelays_RevertOn_ByStranger( - address stranger - ) external { + function test_setupDelays_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); vm.assume(stranger != address(0)); @@ -222,9 +212,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.transferExecutorOwnership() - function testFuzz_transferExecutorOwnership_HappyPath( - address newOwner - ) external { + function testFuzz_transferExecutorOwnership_HappyPath(address newOwner) external { vm.assume(newOwner != _adminExecutor); vm.assume(newOwner != address(0)); @@ -242,9 +230,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(executor.owner(), newOwner); } - function test_transferExecutorOwnership_RevertOn_ByStranger( - address stranger - ) external { + function test_transferExecutorOwnership_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); vm.prank(stranger); @@ -254,9 +240,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.setGovernance() - function testFuzz_setGovernance_HappyPath( - address newGovernance - ) external { + function testFuzz_setGovernance_HappyPath(address newGovernance) external { vm.assume(newGovernance != _dualGovernance); vm.assume(newGovernance != address(0)); @@ -289,9 +273,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getGovernance(), currentGovernance); } - function testFuzz_setGovernance_RevertOn_ByStranger( - address stranger - ) external { + function testFuzz_setGovernance_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); vm.prank(stranger); @@ -308,9 +290,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), true); } - function testFuzz_activateEmergencyMode_RevertOn_ByStranger( - address stranger - ) external { + function testFuzz_activateEmergencyMode_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _emergencyActivator); vm.assume(stranger != address(0)); @@ -378,9 +358,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.emergencyExecute(1); } - function testFuzz_emergencyExecute_RevertOn_ByStranger( - address stranger - ) external { + function testFuzz_emergencyExecute_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _emergencyEnactor); vm.assume(stranger != address(0)); @@ -433,9 +411,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Cancelled); } - function testFuzz_deactivateEmergencyMode_HappyPath_ByStranger( - address stranger - ) external { + function testFuzz_deactivateEmergencyMode_HappyPath_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); _activateEmergencyMode(); @@ -451,9 +427,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), false); } - function testFuzz_deactivateEmergencyMode_RevertOn_ModeNotActivated( - address stranger - ) external { + function testFuzz_deactivateEmergencyMode_RevertOn_ModeNotActivated(address stranger) external { vm.assume(stranger != _adminExecutor); vm.prank(stranger); @@ -465,9 +439,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.deactivateEmergencyMode(); } - function testFuzz_deactivateEmergencyMode_RevertOn_ByStranger_ModeNotExpired( - address stranger - ) external { + function testFuzz_deactivateEmergencyMode_RevertOn_ByStranger_ModeNotExpired(address stranger) external { vm.assume(stranger != _adminExecutor); _activateEmergencyMode(); @@ -516,9 +488,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(proposal.status, ProposalStatus.Cancelled); } - function testFuzz_emergencyReset_RevertOn_ByStranger( - address stranger - ) external { + function testFuzz_emergencyReset_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _emergencyEnactor); vm.assume(stranger != address(0)); @@ -570,9 +540,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertFalse(_timelock.isEmergencyModeActive()); } - function testFuzz_setActivationCommittee_RevertOn_ByStranger( - address stranger - ) external { + function testFuzz_setActivationCommittee_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); @@ -597,9 +565,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertFalse(_localTimelock.isEmergencyModeActive()); } - function testFuzz_setExecutionCommittee_RevertOn_ByStranger( - address stranger - ) external { + function testFuzz_setExecutionCommittee_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor && stranger != _emergencyEnactor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); @@ -627,9 +593,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertFalse(_localTimelock.isEmergencyModeActive()); } - function testFuzz_setProtectionEndDate_RevertOn_ByStranger( - address stranger - ) external { + function testFuzz_setProtectionEndDate_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); @@ -660,9 +624,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertFalse(_localTimelock.isEmergencyModeActive()); } - function testFuzz_setModeDuration_RevertOn_ByStranger( - address stranger - ) external { + function testFuzz_setModeDuration_RevertOn_ByStranger(address stranger) external { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); @@ -816,9 +778,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.getGovernance() - function testFuzz_get_governance( - address governance - ) external { + function testFuzz_get_governance(address governance) external { vm.assume(governance != address(0) && governance != _timelock.getGovernance()); vm.prank(_adminExecutor); _timelock.setGovernance(governance); @@ -914,9 +874,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { // EmergencyProtectedTimelock.getProposalsCount() - function testFuzz_get_proposals_count( - uint256 count - ) external { + function testFuzz_get_proposals_count(uint256 count) external { vm.assume(count > 0); vm.assume(count <= type(uint8).max); assertEq(_timelock.getProposalsCount(), 0); @@ -1002,9 +960,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(calls[0].payload, executorCalls[0].payload); } - function testFuzz_getAdminExecutor( - address executor - ) external { + function testFuzz_getAdminExecutor(address executor) external { EmergencyProtectedTimelock timelock = new EmergencyProtectedTimelock( EmergencyProtectedTimelock.SanityCheckParams({ maxAfterSubmitDelay: Durations.from(45 days), @@ -1025,9 +981,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock)), ""); } - function _scheduleProposal( - uint256 proposalId - ) internal { + function _scheduleProposal(uint256 proposalId) internal { vm.prank(_dualGovernance); _timelock.schedule(proposalId); } diff --git a/test/unit/TimelockedGovernance.t.sol b/test/unit/TimelockedGovernance.t.sol index eea3157e..8cfa8a23 100644 --- a/test/unit/TimelockedGovernance.t.sol +++ b/test/unit/TimelockedGovernance.t.sol @@ -37,9 +37,7 @@ contract TimelockedGovernanceUnitTests is UnitTest { assertEq(_timelock.getSubmittedProposals().length, 1); } - function testFuzz_stranger_cannot_submit_proposal( - address stranger - ) external { + function testFuzz_stranger_cannot_submit_proposal(address stranger) external { vm.assume(stranger != address(0) && stranger != _governance); assertEq(_timelock.getSubmittedProposals().length, 0); @@ -92,9 +90,7 @@ contract TimelockedGovernanceUnitTests is UnitTest { assertEq(_timelock.getLastCancelledProposalId(), 2); } - function testFuzz_stranger_cannot_cancel_all_pending_proposals( - address stranger - ) external { + function testFuzz_stranger_cannot_cancel_all_pending_proposals(address stranger) external { vm.assume(stranger != address(0) && stranger != _governance); assertEq(_timelock.getLastCancelledProposalId(), 0); diff --git a/test/unit/libraries/ExecutableProposals.t.sol b/test/unit/libraries/ExecutableProposals.t.sol index 157c079f..30dc25fd 100644 --- a/test/unit/libraries/ExecutableProposals.t.sol +++ b/test/unit/libraries/ExecutableProposals.t.sol @@ -70,9 +70,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } } - function testFuzz_schedule_proposal( - Duration delay - ) external { + function testFuzz_schedule_proposal(Duration delay) external { vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); @@ -99,9 +97,7 @@ contract ExecutableProposalsUnitTests is UnitTest { assertEq(proposal.data.scheduledAt, Timestamps.now()); } - function testFuzz_cannot_schedule_unsubmitted_proposal( - uint256 proposalId - ) external { + function testFuzz_cannot_schedule_unsubmitted_proposal(uint256 proposalId) external { vm.assume(proposalId > 0); vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotSubmitted.selector, proposalId)); @@ -117,9 +113,7 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.schedule(proposalId, Durations.ZERO); } - function testFuzz_cannot_schedule_proposal_before_delay_passed( - Duration delay - ) external { + function testFuzz_cannot_schedule_proposal_before_delay_passed(Duration delay) external { vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); @@ -142,9 +136,7 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.schedule(proposalId, Durations.ZERO); } - function testFuzz_execute_proposal( - Duration delay - ) external { + function testFuzz_execute_proposal(Duration delay) external { vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); @@ -176,9 +168,7 @@ contract ExecutableProposalsUnitTests is UnitTest { assertEq(proposal.data.scheduledAt, submittedAndScheduledAt); } - function testFuzz_cannot_execute_unsubmitted_proposal( - uint256 proposalId - ) external { + function testFuzz_cannot_execute_unsubmitted_proposal(uint256 proposalId) external { vm.assume(proposalId > 0); vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); _proposals.execute(proposalId, Durations.ZERO); @@ -212,9 +202,7 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.execute(proposalId, Durations.ZERO); } - function testFuzz_cannot_execute_before_delay_passed( - Duration delay - ) external { + function testFuzz_cannot_execute_before_delay_passed(Duration delay) external { vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 proposalId = _proposals.getProposalsCount(); @@ -346,9 +334,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } } - function testFuzz_get_not_existing_proposal( - uint256 proposalId - ) external { + function testFuzz_get_not_existing_proposal(uint256 proposalId) external { vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotFound.selector, proposalId)); _proposals.getProposalDetails(proposalId); diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index dd90eb26..f0479515 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -126,9 +126,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { _lido.submitWstETH(account, _lido.calcSharesToDepositFromPercentageOfTVL(tvlPercentage)); } - function _getBalances( - address vetoer - ) internal view returns (Balances memory balances) { + function _getBalances(address vetoer) internal view returns (Balances memory balances) { uint256 stETHAmount = _lido.stETH.balanceOf(vetoer); uint256 wstETHShares = _lido.wstETH.balanceOf(vetoer); balances = Balances({ @@ -146,15 +144,11 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { _lido.finalizeWithdrawalQueue(); } - function _finalizeWithdrawalQueue( - uint256 id - ) internal { + function _finalizeWithdrawalQueue(uint256 id) internal { _lido.finalizeWithdrawalQueue(id); } - function _simulateRebase( - PercentD16 rebaseFactor - ) internal { + function _simulateRebase(PercentD16 rebaseFactor) internal { _lido.simulateRebase(rebaseFactor); } @@ -175,9 +169,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { vm.stopPrank(); } - function _unlockStETH( - address vetoer - ) internal { + function _unlockStETH(address vetoer) internal { vm.startPrank(vetoer); _getVetoSignallingEscrow().unlockStETH(); vm.stopPrank(); @@ -197,9 +189,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { vm.stopPrank(); } - function _unlockWstETH( - address vetoer - ) internal { + function _unlockWstETH(address vetoer) internal { Escrow escrow = _getVetoSignallingEscrow(); uint256 wstETHBalanceBefore = _lido.wstETH.balanceOf(vetoer); VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); @@ -322,15 +312,11 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(proposalId, proposalsCountBefore + 1); } - function _scheduleProposalViaDualGovernance( - uint256 proposalId - ) internal { + function _scheduleProposalViaDualGovernance(uint256 proposalId) internal { _scheduleProposal(_dualGovernance, proposalId); } - function _scheduleProposalViaTimelockedGovernance( - uint256 proposalId - ) internal { + function _scheduleProposalViaTimelockedGovernance(uint256 proposalId) internal { _scheduleProposal(_timelockedGovernance, proposalId); } @@ -338,9 +324,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { governance.scheduleProposal(proposalId); } - function _executeProposal( - uint256 proposalId - ) internal { + function _executeProposal(uint256 proposalId) internal { _timelock.execute(proposalId); } @@ -426,9 +410,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalSubmitted( - uint256 proposalId - ) internal { + function _assertProposalSubmitted(uint256 proposalId) internal { assertEq( _timelock.getProposalDetails(proposalId).status, ProposalStatus.Submitted, @@ -436,9 +418,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalScheduled( - uint256 proposalId - ) internal { + function _assertProposalScheduled(uint256 proposalId) internal { assertEq( _timelock.getProposalDetails(proposalId).status, ProposalStatus.Scheduled, @@ -446,9 +426,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalExecuted( - uint256 proposalId - ) internal { + function _assertProposalExecuted(uint256 proposalId) internal { assertEq( _timelock.getProposalDetails(proposalId).status, ProposalStatus.Executed, @@ -456,9 +434,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalCancelled( - uint256 proposalId - ) internal { + function _assertProposalCancelled(uint256 proposalId) internal { assertEq( _timelock.getProposalDetails(proposalId).status, ProposalStatus.Cancelled, @@ -549,16 +525,12 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { // Utils Methods // --- - function _step( - string memory text - ) internal view { + function _step(string memory text) internal view { // solhint-disable-next-line console.log(string.concat(">>> ", text, " <<<")); } - function _wait( - Duration duration - ) internal { + function _wait(Duration duration) internal { vm.warp(duration.addTo(Timestamps.now()).toSeconds()); } @@ -579,9 +551,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { _emergencyActivationCommittee.executeActivateEmergencyMode(); } - function _executeEmergencyExecute( - uint256 proposalId - ) internal { + function _executeEmergencyExecute(uint256 proposalId) internal { address[] memory members = _emergencyExecutionCommittee.getMembers(); for (uint256 i = 0; i < _emergencyExecutionCommittee.quorum(); ++i) { vm.prank(members[i]); @@ -606,18 +576,14 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { uint256 _seconds; } - function _toDuration( - uint256 timestamp - ) internal pure returns (DurationStruct memory duration) { + function _toDuration(uint256 timestamp) internal pure returns (DurationStruct memory duration) { duration._days = timestamp / 1 days; duration._hours = (timestamp - 1 days * duration._days) / 1 hours; duration._minutes = (timestamp - 1 days * duration._days - 1 hours * duration._hours) / 1 minutes; duration._seconds = timestamp % 1 minutes; } - function _formatDuration( - DurationStruct memory duration - ) internal pure returns (string memory) { + function _formatDuration(DurationStruct memory duration) internal pure returns (string memory) { // format example: 1d:22h:33m:12s return string( abi.encodePacked( From e70b026a9aaa7c3f23045955599d40491256c64a Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Mon, 9 Sep 2024 17:47:44 +0300 Subject: [PATCH 37/86] fix: missing natspec --- contracts/EmergencyProtectedTimelock.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index c66547d7..d861de80 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -72,6 +72,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// 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 `ExternalCall` structs representing the calls to be executed. + /// @param metadata A string containing additional information about the proposal. /// @return newProposalId The ID of the newly created proposal. function submit( address executor, From 884988275295fcc038bb0550ab0b530a35dc7e8e Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Mon, 9 Sep 2024 17:48:27 +0300 Subject: [PATCH 38/86] fix: formatting --- contracts/TimelockedGovernance.sol | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/contracts/TimelockedGovernance.sol b/contracts/TimelockedGovernance.sol index 851075e5..1003b7d5 100644 --- a/contracts/TimelockedGovernance.sol +++ b/contracts/TimelockedGovernance.sol @@ -35,26 +35,20 @@ contract TimelockedGovernance is IGovernance { /// @dev Schedules a submitted proposal. /// @param proposalId The ID of the proposal to be scheduled. - function scheduleProposal( - uint256 proposalId - ) external { + function scheduleProposal(uint256 proposalId) external { TIMELOCK.schedule(proposalId); } /// @dev Executes a scheduled proposal. /// @param proposalId The ID of the proposal to be executed. - function executeProposal( - uint256 proposalId - ) external { + function executeProposal(uint256 proposalId) external { TIMELOCK.execute(proposalId); } /// @dev Checks if a proposal can be scheduled. /// @param proposalId The ID of the proposal to check. /// @return A boolean indicating whether the proposal can be scheduled. - function canScheduleProposal( - uint256 proposalId - ) external view returns (bool) { + function canScheduleProposal(uint256 proposalId) external view returns (bool) { return TIMELOCK.canSchedule(proposalId); } From 3e47748167c541c38aeb7065a62e9702721b58bf Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 10 Sep 2024 02:21:13 +0400 Subject: [PATCH 39/86] Handle edge case proposalId equal to zero --- contracts/committees/EmergencyExecutionCommittee.sol | 2 +- contracts/committees/TiebreakerCore.sol | 4 ++-- .../committees/EmergencyExecutionCommittee.t.sol | 12 +++++++++++- test/unit/committees/TiebreakerCore.t.sol | 8 ++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol index 3bcd8d31..47a26334 100644 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -76,7 +76,7 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { /// @notice Checks if a proposal exists /// @param proposalId The ID of the proposal to check function _checkProposalExists(uint256 proposalId) internal view { - if (proposalId > ITimelock(EMERGENCY_PROTECTED_TIMELOCK).getProposalsCount()) { + if (proposalId == 0 || proposalId > ITimelock(EMERGENCY_PROTECTED_TIMELOCK).getProposalsCount()) { revert ProposalDoesNotExist(proposalId); } } diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol index 1118d212..ac0cce57 100644 --- a/contracts/committees/TiebreakerCore.sol +++ b/contracts/committees/TiebreakerCore.sol @@ -26,7 +26,7 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { error ResumeSealableNonceMismatch(); error ProposalDoesNotExist(uint256 proposalId); - address immutable DUAL_GOVERNANCE; + address public immutable DUAL_GOVERNANCE; mapping(address => uint256) private _sealableResumeNonces; @@ -81,7 +81,7 @@ contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { function checkProposalExists(uint256 proposalId) public view { ITimelock timelock = IDualGovernance(DUAL_GOVERNANCE).TIMELOCK(); - if (proposalId > timelock.getProposalsCount()) { + if (proposalId == 0 || proposalId > timelock.getProposalsCount()) { revert ProposalDoesNotExist(proposalId); } } diff --git a/test/unit/committees/EmergencyExecutionCommittee.t.sol b/test/unit/committees/EmergencyExecutionCommittee.t.sol index bd44430a..ff6a1fb0 100644 --- a/test/unit/committees/EmergencyExecutionCommittee.t.sol +++ b/test/unit/committees/EmergencyExecutionCommittee.t.sol @@ -66,7 +66,7 @@ contract EmergencyExecutionCommitteeUnitTest is UnitTest { assertFalse(isExecuted); } - function test_voteEmergencyExecute_RevertOn_ProposalDoesNotExist() external { + function test_voteEmergencyExecute_RevertOn_ProposalIdExceedsProposalsCount() external { uint256 nonExistentProposalId = proposalId + 1; vm.expectRevert( @@ -76,6 +76,16 @@ contract EmergencyExecutionCommitteeUnitTest is UnitTest { emergencyExecutionCommittee.voteEmergencyExecute(nonExistentProposalId, true); } + function test_voteEmergencyExecute_RevertOn_ProposalIdIsZero() external { + uint256 nonExistentProposalId = 0; + + vm.expectRevert( + abi.encodeWithSelector(EmergencyExecutionCommittee.ProposalDoesNotExist.selector, nonExistentProposalId) + ); + vm.prank(committeeMembers[0]); + emergencyExecutionCommittee.voteEmergencyExecute(nonExistentProposalId, true); + } + function testFuzz_voteEmergencyExecute_RevertOn_NotMember(address caller) external { vm.assume(caller != committeeMembers[0] && caller != committeeMembers[1] && caller != committeeMembers[2]); vm.prank(caller); diff --git a/test/unit/committees/TiebreakerCore.t.sol b/test/unit/committees/TiebreakerCore.t.sol index 340f6004..99b67cb5 100644 --- a/test/unit/committees/TiebreakerCore.t.sol +++ b/test/unit/committees/TiebreakerCore.t.sol @@ -91,6 +91,14 @@ contract TiebreakerCoreUnitTest is UnitTest { tiebreakerCore.scheduleProposal(nonExistentProposalId); } + function test_scheduleProposal_RevertOn_ProposalIdIsZero() external { + uint256 nonExistentProposalId = 0; + + vm.expectRevert(abi.encodeWithSelector(TiebreakerCore.ProposalDoesNotExist.selector, nonExistentProposalId)); + vm.prank(committeeMembers[0]); + tiebreakerCore.scheduleProposal(nonExistentProposalId); + } + function test_executeScheduleProposal_HappyPath() external { vm.prank(committeeMembers[0]); tiebreakerCore.scheduleProposal(proposalId); From 4cfa0da4ac02c48b731dcdc4975ff661fb6d324b Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 10 Sep 2024 02:44:32 +0400 Subject: [PATCH 40/86] Remove unused contract --- contracts/CommitteesFactory.sol | 55 --------------------------------- 1 file changed, 55 deletions(-) delete mode 100644 contracts/CommitteesFactory.sol diff --git a/contracts/CommitteesFactory.sol b/contracts/CommitteesFactory.sol deleted file mode 100644 index 64935e5e..00000000 --- a/contracts/CommitteesFactory.sol +++ /dev/null @@ -1,55 +0,0 @@ -pragma solidity 0.8.26; - -import {EmergencyActivationCommittee} from "./committees/EmergencyActivationCommittee.sol"; -import {TiebreakerSubCommittee} from "./committees/TiebreakerSubCommittee.sol"; -import {EmergencyActivationCommittee} from "./committees/EmergencyActivationCommittee.sol"; -import {TiebreakerCore} from "./committees/TiebreakerCore.sol"; -import {ResealCommittee} from "./committees/ResealCommittee.sol"; -import {Duration} from "./types/Duration.sol"; - -contract CommitteesFactory { - function createEmergencyActivationCommittee( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address emergencyProtectedTimelock - ) external returns (EmergencyActivationCommittee) { - return new EmergencyActivationCommittee(owner, committeeMembers, executionQuorum, emergencyProtectedTimelock); - } - - function createEmergencyExecutionCommittee( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address emergencyProtectedTimelock - ) external returns (EmergencyActivationCommittee) { - return new EmergencyActivationCommittee(owner, committeeMembers, executionQuorum, emergencyProtectedTimelock); - } - - function createTiebreakerCore( - address owner, - address dualGovernance, - Duration timelock - ) external returns (TiebreakerCore) { - return new TiebreakerCore(owner, dualGovernance, timelock); - } - - function createTiebreakerSubCommittee( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address tiebreakerCore - ) external returns (TiebreakerSubCommittee) { - return new TiebreakerSubCommittee(owner, committeeMembers, executionQuorum, tiebreakerCore); - } - - function createResealCommittee( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address dualGovernance, - Duration timelock - ) external returns (ResealCommittee) { - return new ResealCommittee(owner, committeeMembers, executionQuorum, dualGovernance, timelock); - } -} From be3d6c72b3524315b971a3e40c616ee61f72fdd0 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 10 Sep 2024 02:46:04 +0400 Subject: [PATCH 41/86] ProposalAlreadyScheduled -> HashAlreadyScheduled. Fix failed tests --- contracts/committees/HashConsensus.sol | 6 +-- test/unit/committees/HashConsensus.t.sol | 4 +- test/unit/committees/ResealCommittee.t.sol | 38 +++++++++++++------ test/unit/committees/TiebreakerCore.t.sol | 8 ++-- .../committees/TiebreakerSubCommittee.t.sol | 2 +- 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index 8eabeaf3..be006b30 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -23,11 +23,11 @@ abstract contract HashConsensus is Ownable { error AccountIsNotMember(address account); error CallerIsNotMember(address caller); error HashAlreadyUsed(bytes32 hash); + error HashAlreadyScheduled(bytes32 hash); error QuorumIsNotReached(); error InvalidQuorum(); error InvalidTimelockDuration(Duration timelock); error TimelockNotPassed(); - error ProposalAlreadyScheduled(bytes32 hash); struct HashState { Timestamp scheduledAt; @@ -56,7 +56,7 @@ abstract contract HashConsensus is Ownable { } if (_hashStates[hash].scheduledAt.isNotZero()) { - revert ProposalAlreadyScheduled(hash); + revert HashAlreadyScheduled(hash); } if (approves[msg.sender][hash] == support) { @@ -195,7 +195,7 @@ abstract contract HashConsensus is Ownable { revert QuorumIsNotReached(); } if (_hashStates[hash].scheduledAt.isNotZero()) { - revert ProposalAlreadyScheduled(hash); + revert HashAlreadyScheduled(hash); } _hashStates[hash].scheduledAt = Timestamps.from(block.timestamp); diff --git a/test/unit/committees/HashConsensus.t.sol b/test/unit/committees/HashConsensus.t.sol index 91ac2451..8b20944b 100644 --- a/test/unit/committees/HashConsensus.t.sol +++ b/test/unit/committees/HashConsensus.t.sol @@ -533,7 +533,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { _hashConsensusWrapper.vote(hash, true); } - vm.expectRevert(abi.encodeWithSelector(HashConsensus.ProposalAlreadyScheduled.selector, hash)); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.HashAlreadyScheduled.selector, hash)); vm.prank(_committeeMembers[_quorum]); _hashConsensusWrapper.vote(hash, true); } @@ -611,7 +611,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { (,, Timestamp scheduledAtBefore,) = _hashConsensusWrapper.getHashState(hash); _wait(_timelock); - vm.expectRevert(abi.encodeWithSignature("ProposalAlreadyScheduled(bytes32)", hash)); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.HashAlreadyScheduled.selector, hash)); _hashConsensusWrapper.schedule(hash); (,, Timestamp scheduledAtAfter,) = _hashConsensusWrapper.getHashState(hash); diff --git a/test/unit/committees/ResealCommittee.t.sol b/test/unit/committees/ResealCommittee.t.sol index b990eb54..83bc3d63 100644 --- a/test/unit/committees/ResealCommittee.t.sol +++ b/test/unit/committees/ResealCommittee.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.26; import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; import {Durations} from "contracts/types/Duration.sol"; -import {Timestamp} from "contracts/types/Timestamp.sol"; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; import {HashConsensus} from "contracts/committees/HashConsensus.sol"; @@ -80,27 +80,29 @@ contract ResealCommitteeUnitTest is UnitTest { } function test_getResealState_HappyPath() external { + vm.prank(owner); + resealCommittee.setQuorum(3); + (uint256 support, uint256 executionQuorum, Timestamp quorumAt) = resealCommittee.getResealState(sealable); assertEq(support, 0); - assertEq(executionQuorum, 2); - assertEq(quorumAt, Timestamp.wrap(0)); + assertEq(executionQuorum, 3); + assertEq(quorumAt, Timestamps.ZERO); vm.prank(committeeMembers[0]); resealCommittee.voteReseal(sealable, true); (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); assertEq(support, 1); - assertEq(executionQuorum, 2); - assertEq(quorumAt, Timestamp.wrap(0)); + assertEq(executionQuorum, 3); + assertEq(quorumAt, Timestamps.ZERO); vm.prank(committeeMembers[1]); resealCommittee.voteReseal(sealable, true); (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); - Timestamp quorumAtExpected = Timestamp.wrap(uint40(block.timestamp)); assertEq(support, 2); - assertEq(executionQuorum, 2); - assertEq(quorumAt, quorumAtExpected); + assertEq(executionQuorum, 3); + assertEq(quorumAt, Timestamps.ZERO); _wait(Durations.from(1)); @@ -109,19 +111,33 @@ contract ResealCommitteeUnitTest is UnitTest { (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); assertEq(support, 1); - assertEq(executionQuorum, 2); - assertEq(quorumAt, quorumAtExpected); + assertEq(executionQuorum, 3); + assertEq(quorumAt, Timestamps.ZERO); vm.prank(committeeMembers[1]); resealCommittee.voteReseal(sealable, true); + (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); + assertEq(support, 2); + assertEq(executionQuorum, 3); + assertEq(quorumAt, Timestamps.ZERO); + + vm.prank(committeeMembers[2]); + resealCommittee.voteReseal(sealable, true); + + Timestamp quorumAtExpected = Timestamps.now(); + (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); + assertEq(support, 3); + assertEq(executionQuorum, 3); + assertEq(quorumAt, quorumAtExpected); + vm.prank(committeeMembers[2]); vm.expectCall(dualGovernance, abi.encodeWithSelector(IDualGovernance.resealSealable.selector, sealable)); resealCommittee.executeReseal(sealable); (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); assertEq(support, 0); - assertEq(executionQuorum, 2); + assertEq(executionQuorum, 3); assertEq(quorumAt, Timestamp.wrap(0)); } } diff --git a/test/unit/committees/TiebreakerCore.t.sol b/test/unit/committees/TiebreakerCore.t.sol index c4142716..5ce8c285 100644 --- a/test/unit/committees/TiebreakerCore.t.sol +++ b/test/unit/committees/TiebreakerCore.t.sol @@ -5,7 +5,7 @@ import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; import {HashConsensus} from "contracts/committees/HashConsensus.sol"; import {Durations, Duration} from "contracts/types/Duration.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; -import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; +import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; import {TargetMock} from "test/utils/target-mock.sol"; import {UnitTest} from "test/utils/unit-test.sol"; @@ -68,7 +68,7 @@ contract TiebreakerCoreUnitTest is UnitTest { vm.prank(committeeMembers[2]); vm.expectCall( - dualGovernance, abi.encodeWithSelector(IDualGovernance.tiebreakerScheduleProposal.selector, proposalId) + dualGovernance, abi.encodeWithSelector(ITiebreaker.tiebreakerScheduleProposal.selector, proposalId) ); tiebreakerCore.executeScheduleProposal(proposalId); @@ -115,9 +115,7 @@ contract TiebreakerCoreUnitTest is UnitTest { _wait(timelock); vm.prank(committeeMembers[2]); - vm.expectCall( - dualGovernance, abi.encodeWithSelector(IDualGovernance.tiebreakerResumeSealable.selector, sealable) - ); + vm.expectCall(dualGovernance, abi.encodeWithSelector(ITiebreaker.tiebreakerResumeSealable.selector, sealable)); tiebreakerCore.executeSealableResume(sealable); (,,, bool isExecuted) = tiebreakerCore.getSealableResumeState(sealable, nonce); diff --git a/test/unit/committees/TiebreakerSubCommittee.t.sol b/test/unit/committees/TiebreakerSubCommittee.t.sol index 674cb609..790ab759 100644 --- a/test/unit/committees/TiebreakerSubCommittee.t.sol +++ b/test/unit/committees/TiebreakerSubCommittee.t.sol @@ -6,7 +6,7 @@ import {HashConsensus} from "contracts/committees/HashConsensus.sol"; import {Durations} from "contracts/types/Duration.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; import {UnitTest} from "test/utils/unit-test.sol"; -import {ITiebreakerCore} from "contracts/interfaces/ITiebreaker.sol"; +import {ITiebreakerCore} from "contracts/interfaces/ITiebreakerCore.sol"; import {TargetMock} from "test/utils/target-mock.sol"; From 3e4efbcf7c7a1c6e295b879e35feb277f1572120 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 10 Sep 2024 03:34:30 +0400 Subject: [PATCH 42/86] Stricter hashes scheduling and usage rules --- contracts/committees/HashConsensus.sol | 38 ++++++++----------- .../EmergencyActivationCommittee.t.sol | 4 +- .../EmergencyExecutionCommittee.t.sol | 16 ++++++-- test/unit/committees/HashConsensus.t.sol | 6 +-- test/unit/committees/ResealCommittee.t.sol | 6 ++- .../committees/TiebreakerSubCommittee.t.sol | 16 ++++++-- 6 files changed, 52 insertions(+), 34 deletions(-) diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index be006b30..a6415777 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -16,6 +16,7 @@ abstract contract HashConsensus is Ownable { event MemberRemoved(address indexed member); event QuorumSet(uint256 quorum); event HashUsed(bytes32 hash); + event HashScheduled(bytes32 hash); event Voted(address indexed signer, bytes32 hash, bool support); event TimelockDurationSet(Duration timelockDuration); @@ -23,6 +24,7 @@ abstract contract HashConsensus is Ownable { error AccountIsNotMember(address account); error CallerIsNotMember(address caller); error HashAlreadyUsed(bytes32 hash); + error HashIsNotScheduled(bytes32 hash); error HashAlreadyScheduled(bytes32 hash); error QuorumIsNotReached(); error InvalidQuorum(); @@ -51,10 +53,6 @@ abstract contract HashConsensus is Ownable { /// @param hash The hash to vote on /// @param support Indicates whether the member supports the hash function _vote(bytes32 hash, bool support) internal { - if (_hashStates[hash].usedAt.isNotZero()) { - revert HashAlreadyUsed(hash); - } - if (_hashStates[hash].scheduledAt.isNotZero()) { revert HashAlreadyScheduled(hash); } @@ -63,34 +61,32 @@ abstract contract HashConsensus is Ownable { return; } - uint256 heads = _getSupport(hash); - // heads compares to quorum - 1 because the current vote is not counted yet - if (heads >= quorum - 1 && support == true) { - _hashStates[hash].scheduledAt = Timestamps.from(block.timestamp); - } - approves[msg.sender][hash] = support; emit Voted(msg.sender, hash, support); + + if (_getSupport(hash) == quorum) { + _hashStates[hash].scheduledAt = Timestamps.now(); + emit HashScheduled(hash); + } } /// @notice Marks a hash as used if quorum is reached and timelock has passed /// @dev Internal function that handles marking a hash as used /// @param hash The hash to mark as used function _markUsed(bytes32 hash) internal { + if (_hashStates[hash].scheduledAt.isZero()) { + revert HashIsNotScheduled(hash); + } + if (_hashStates[hash].usedAt.isNotZero()) { revert HashAlreadyUsed(hash); } - uint256 support = _getSupport(hash); - - if (support == 0 || support < quorum) { - revert QuorumIsNotReached(); - } - if (timelockDuration.addTo(_hashStates[hash].scheduledAt) > Timestamps.from(block.timestamp)) { + if (timelockDuration.addTo(_hashStates[hash].scheduledAt) > Timestamps.now()) { revert TimelockNotPassed(); } - _hashStates[hash].usedAt = Timestamps.from(block.timestamp); + _hashStates[hash].usedAt = Timestamps.now(); emit HashUsed(hash); } @@ -187,18 +183,16 @@ abstract contract HashConsensus is Ownable { /// current support of the proposal. /// @param hash The hash of the proposal to be scheduled function schedule(bytes32 hash) public { - if (_hashStates[hash].usedAt.isNotZero()) { - revert HashAlreadyUsed(hash); + if (_hashStates[hash].scheduledAt.isNotZero()) { + revert HashAlreadyScheduled(hash); } if (_getSupport(hash) < quorum) { revert QuorumIsNotReached(); } - if (_hashStates[hash].scheduledAt.isNotZero()) { - revert HashAlreadyScheduled(hash); - } _hashStates[hash].scheduledAt = Timestamps.from(block.timestamp); + emit HashScheduled(hash); } /// @notice Sets the execution quorum required for certain operations. diff --git a/test/unit/committees/EmergencyActivationCommittee.t.sol b/test/unit/committees/EmergencyActivationCommittee.t.sol index e9b5247c..1cd2159c 100644 --- a/test/unit/committees/EmergencyActivationCommittee.t.sol +++ b/test/unit/committees/EmergencyActivationCommittee.t.sol @@ -11,6 +11,8 @@ import {TargetMock} from "test/utils/target-mock.sol"; import {UnitTest} from "test/utils/unit-test.sol"; contract EmergencyActivationCommitteeUnitTest is UnitTest { + bytes32 private constant _EMERGENCY_ACTIVATION_HASH = keccak256("EMERGENCY_ACTIVATE"); + EmergencyActivationCommittee internal emergencyActivationCommittee; uint256 internal quorum = 2; address internal owner = makeAddr("owner"); @@ -82,7 +84,7 @@ contract EmergencyActivationCommitteeUnitTest is UnitTest { emergencyActivationCommittee.approveActivateEmergencyMode(); vm.prank(committeeMembers[2]); - vm.expectRevert(abi.encodeWithSelector(HashConsensus.QuorumIsNotReached.selector)); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.HashIsNotScheduled.selector, _EMERGENCY_ACTIVATION_HASH)); emergencyActivationCommittee.executeActivateEmergencyMode(); } diff --git a/test/unit/committees/EmergencyExecutionCommittee.t.sol b/test/unit/committees/EmergencyExecutionCommittee.t.sol index 6691b35b..9f66155a 100644 --- a/test/unit/committees/EmergencyExecutionCommittee.t.sol +++ b/test/unit/committees/EmergencyExecutionCommittee.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; +import {EmergencyExecutionCommittee, ProposalType} from "contracts/committees/EmergencyExecutionCommittee.sol"; import {HashConsensus} from "contracts/committees/HashConsensus.sol"; import {Durations} from "contracts/types/Duration.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; @@ -81,7 +81,12 @@ contract EmergencyExecutionCommitteeUnitTest is UnitTest { emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); vm.prank(committeeMembers[2]); - vm.expectRevert(abi.encodeWithSelector(HashConsensus.QuorumIsNotReached.selector)); + vm.expectRevert( + abi.encodeWithSelector( + HashConsensus.HashIsNotScheduled.selector, + keccak256(abi.encode(ProposalType.EmergencyExecute, proposalId)) + ) + ); emergencyExecutionCommittee.executeEmergencyExecute(proposalId); } @@ -156,7 +161,12 @@ contract EmergencyExecutionCommitteeUnitTest is UnitTest { emergencyExecutionCommittee.approveEmergencyReset(); vm.prank(committeeMembers[2]); - vm.expectRevert(abi.encodeWithSelector(HashConsensus.QuorumIsNotReached.selector)); + vm.expectRevert( + abi.encodeWithSelector( + HashConsensus.HashIsNotScheduled.selector, + keccak256(abi.encode(ProposalType.EmergencyReset, bytes32(0))) + ) + ); emergencyExecutionCommittee.executeEmergencyReset(); } diff --git a/test/unit/committees/HashConsensus.t.sol b/test/unit/committees/HashConsensus.t.sol index 8b20944b..09747ef7 100644 --- a/test/unit/committees/HashConsensus.t.sol +++ b/test/unit/committees/HashConsensus.t.sol @@ -521,7 +521,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { _hashConsensusWrapper.execute(dataHash); vm.prank(_committeeMembers[0]); - vm.expectRevert(abi.encodeWithSelector(HashConsensus.HashAlreadyUsed.selector, dataHash)); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.HashAlreadyScheduled.selector, dataHash)); _hashConsensusWrapper.vote(dataHash, true); } @@ -540,7 +540,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { function test_execute_events() public { vm.prank(_stranger); - vm.expectRevert(abi.encodeWithSignature("QuorumIsNotReached()")); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.HashIsNotScheduled.selector, dataHash)); _hashConsensusWrapper.execute(dataHash); for (uint256 i = 0; i < _quorum; ++i) { @@ -594,7 +594,7 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { _hashConsensusWrapper.execute(hash); - vm.expectRevert(abi.encodeWithSelector(HashConsensus.HashAlreadyUsed.selector, hash)); + vm.expectRevert(abi.encodeWithSelector(HashConsensus.HashAlreadyScheduled.selector, hash)); _hashConsensusWrapper.schedule(hash); } diff --git a/test/unit/committees/ResealCommittee.t.sol b/test/unit/committees/ResealCommittee.t.sol index 83bc3d63..effc10c0 100644 --- a/test/unit/committees/ResealCommittee.t.sol +++ b/test/unit/committees/ResealCommittee.t.sol @@ -75,7 +75,11 @@ contract ResealCommitteeUnitTest is UnitTest { resealCommittee.voteReseal(sealable, true); vm.prank(committeeMembers[2]); - vm.expectRevert(abi.encodeWithSelector(HashConsensus.QuorumIsNotReached.selector)); + vm.expectRevert( + abi.encodeWithSelector( + HashConsensus.HashIsNotScheduled.selector, keccak256(abi.encode(sealable, /* resealNonce */ 0)) + ) + ); resealCommittee.executeReseal(sealable); } diff --git a/test/unit/committees/TiebreakerSubCommittee.t.sol b/test/unit/committees/TiebreakerSubCommittee.t.sol index 790ab759..edcaa332 100644 --- a/test/unit/committees/TiebreakerSubCommittee.t.sol +++ b/test/unit/committees/TiebreakerSubCommittee.t.sol @@ -1,9 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; +import {TiebreakerSubCommittee, ProposalType} from "contracts/committees/TiebreakerSubCommittee.sol"; import {HashConsensus} from "contracts/committees/HashConsensus.sol"; -import {Durations} from "contracts/types/Duration.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; import {UnitTest} from "test/utils/unit-test.sol"; import {ITiebreakerCore} from "contracts/interfaces/ITiebreakerCore.sol"; @@ -74,7 +73,12 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest { tiebreakerSubCommittee.scheduleProposal(proposalId); vm.prank(committeeMembers[2]); - vm.expectRevert(abi.encodeWithSelector(HashConsensus.QuorumIsNotReached.selector)); + vm.expectRevert( + abi.encodeWithSelector( + HashConsensus.HashIsNotScheduled.selector, + keccak256(abi.encode(ProposalType.ScheduleProposal, proposalId)) + ) + ); tiebreakerSubCommittee.executeScheduleProposal(proposalId); } @@ -141,7 +145,11 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest { tiebreakerSubCommittee.sealableResume(sealable); vm.prank(committeeMembers[2]); - vm.expectRevert(abi.encodeWithSelector(HashConsensus.QuorumIsNotReached.selector)); + vm.expectRevert( + abi.encodeWithSelector( + HashConsensus.HashIsNotScheduled.selector, keccak256(abi.encode(sealable, /*nonce */ 0)) + ) + ); tiebreakerSubCommittee.executeSealableResume(sealable); } From faee4cec260597ac8cac3bbc09ff2ea5f40f24a5 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 10 Sep 2024 12:57:34 +0400 Subject: [PATCH 43/86] Fix contracts formatting --- contracts/DualGovernance.sol | 70 +++++-------------- contracts/DualGovernanceConfigProvider.sol | 6 +- contracts/Escrow.sol | 50 ++++--------- contracts/libraries/AssetsAccounting.sol | 14 ++-- .../libraries/DualGovernanceStateMachine.sol | 20 ++---- contracts/libraries/EscrowState.sol | 34 +++------ foundry.toml | 5 +- test/scenario/escrow.t.sol | 10 +-- test/scenario/gov-state-transitions.t.sol | 2 - .../last-moment-malicious-proposal.t.sol | 6 +- test/scenario/tiebreaker.t.sol | 2 - test/scenario/veto-cooldown-mechanics.t.sol | 6 +- test/unit/DualGovernance.t.sol | 2 - .../DualGovernanceStateMachine.t.sol | 2 - test/unit/libraries/EscrowState.t.sol | 32 +++------ test/utils/SetupDeployment.sol | 34 +++------ test/utils/scenario-test-blueprint.sol | 70 +++++-------------- 17 files changed, 88 insertions(+), 277 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 32dd6afb..bd9ee051 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -22,7 +22,6 @@ import {State, DualGovernanceStateMachine} from "./libraries/DualGovernanceState import {Escrow} from "./Escrow.sol"; contract DualGovernance is IDualGovernance { - using Proposers for Proposers.Context; using Tiebreaker for Tiebreaker.Context; using DualGovernanceConfig for DualGovernanceConfig.Context; @@ -123,9 +122,7 @@ contract DualGovernance is IDualGovernance { // Proposals Flow // --- - function submitProposal( - ExternalCall[] calldata calls - ) external returns (uint256 proposalId) { + function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId) { _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); if (!_stateMachine.canSubmitProposal()) { revert ProposalSubmissionBlocked(); @@ -134,9 +131,7 @@ contract DualGovernance is IDualGovernance { proposalId = TIMELOCK.submit(proposer.executor, calls); } - function scheduleProposal( - uint256 proposalId - ) external { + function scheduleProposal(uint256 proposalId) external { _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = TIMELOCK.getProposalInfo(proposalId); @@ -174,9 +169,7 @@ contract DualGovernance is IDualGovernance { return _stateMachine.canSubmitProposal(); } - function canScheduleProposal( - uint256 proposalId - ) external view returns (bool) { + function canScheduleProposal(uint256 proposalId) external view returns (bool) { ( /* id */ , /* status */, /* executor */, Timestamp submittedAt, /* scheduledAt */ ) = TIMELOCK.getProposalInfo(proposalId); return _stateMachine.canScheduleProposal(submittedAt) && TIMELOCK.canSchedule(proposalId); @@ -190,9 +183,7 @@ contract DualGovernance is IDualGovernance { _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); } - function setConfigProvider( - IDualGovernanceConfigProvider newConfigProvider - ) external { + function setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) external { _checkCallerIsAdminExecutor(); _setConfigProvider(newConfigProvider); @@ -236,9 +227,7 @@ contract DualGovernance is IDualGovernance { _proposers.register(proposer, executor); } - function unregisterProposer( - address proposer - ) external { + function unregisterProposer(address proposer) external { _checkCallerIsAdminExecutor(); _proposers.unregister(proposer); @@ -248,15 +237,11 @@ contract DualGovernance is IDualGovernance { } } - function isProposer( - address account - ) external view returns (bool) { + function isProposer(address account) external view returns (bool) { return _proposers.isProposer(account); } - function getProposer( - address account - ) external view returns (Proposers.Proposer memory proposer) { + function getProposer(address account) external view returns (Proposers.Proposer memory proposer) { proposer = _proposers.getProposer(account); } @@ -264,9 +249,7 @@ contract DualGovernance is IDualGovernance { proposers = _proposers.getAllProposers(); } - function isExecutor( - address account - ) external view returns (bool) { + function isExecutor(address account) external view returns (bool) { return _proposers.isExecutor(account); } @@ -274,47 +257,35 @@ contract DualGovernance is IDualGovernance { // Tiebreaker Protection // --- - function addTiebreakerSealableWithdrawalBlocker( - address sealableWithdrawalBlocker - ) external { + function addTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { _checkCallerIsAdminExecutor(); _tiebreaker.addSealableWithdrawalBlocker(sealableWithdrawalBlocker, MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT); } - function removeTiebreakerSealableWithdrawalBlocker( - address sealableWithdrawalBlocker - ) external { + function removeTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { _checkCallerIsAdminExecutor(); _tiebreaker.removeSealableWithdrawalBlocker(sealableWithdrawalBlocker); } - function setTiebreakerCommittee( - address tiebreakerCommittee - ) external { + function setTiebreakerCommittee(address tiebreakerCommittee) external { _checkCallerIsAdminExecutor(); _tiebreaker.setTiebreakerCommittee(tiebreakerCommittee); } - function setTiebreakerActivationTimeout( - Duration tiebreakerActivationTimeout - ) external { + function setTiebreakerActivationTimeout(Duration tiebreakerActivationTimeout) external { _checkCallerIsAdminExecutor(); _tiebreaker.setTiebreakerActivationTimeout( MIN_TIEBREAKER_ACTIVATION_TIMEOUT, tiebreakerActivationTimeout, MAX_TIEBREAKER_ACTIVATION_TIMEOUT ); } - function tiebreakerResumeSealable( - address sealable - ) external { + function tiebreakerResumeSealable(address sealable) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); _tiebreaker.checkTie(_stateMachine.getCurrentState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); RESEAL_MANAGER.resume(sealable); } - function tiebreakerScheduleProposal( - uint256 proposalId - ) external { + function tiebreakerScheduleProposal(uint256 proposalId) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); _tiebreaker.checkTie(_stateMachine.getCurrentState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); @@ -339,9 +310,7 @@ contract DualGovernance is IDualGovernance { // Reseal executor // --- - function resealSealable( - address sealable - ) external { + function resealSealable(address sealable) external { if (msg.sender != _resealCommittee) { revert CallerIsNotResealCommittee(msg.sender); } @@ -351,9 +320,7 @@ contract DualGovernance is IDualGovernance { RESEAL_MANAGER.reseal(sealable); } - function setResealCommittee( - address resealCommittee - ) external { + function setResealCommittee(address resealCommittee) external { _checkCallerIsAdminExecutor(); _resealCommittee = resealCommittee; } @@ -362,9 +329,7 @@ contract DualGovernance is IDualGovernance { // Private methods // --- - function _setConfigProvider( - IDualGovernanceConfigProvider newConfigProvider - ) internal { + function _setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) internal { if (address(newConfigProvider) == address(0)) { revert InvalidConfigProvider(newConfigProvider); } @@ -384,5 +349,4 @@ contract DualGovernance is IDualGovernance { revert CallerIsNotAdminExecutor(msg.sender); } } - } diff --git a/contracts/DualGovernanceConfigProvider.sol b/contracts/DualGovernanceConfigProvider.sol index 229f3d44..94c722ac 100644 --- a/contracts/DualGovernanceConfigProvider.sol +++ b/contracts/DualGovernanceConfigProvider.sol @@ -7,7 +7,6 @@ import {DualGovernanceConfig} from "./libraries/DualGovernanceConfig.sol"; import {IDualGovernanceConfigProvider} from "./interfaces/IDualGovernanceConfigProvider.sol"; contract ImmutableDualGovernanceConfigProvider is IDualGovernanceConfigProvider { - using DualGovernanceConfig for DualGovernanceConfig.Context; PercentD16 public immutable FIRST_SEAL_RAGE_QUIT_SUPPORT; @@ -27,9 +26,7 @@ contract ImmutableDualGovernanceConfigProvider is IDualGovernanceConfigProvider Duration public immutable RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY; Duration public immutable RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH; - constructor( - DualGovernanceConfig.Context memory dualGovernanceConfig - ) { + constructor(DualGovernanceConfig.Context memory dualGovernanceConfig) { dualGovernanceConfig.validate(); FIRST_SEAL_RAGE_QUIT_SUPPORT = dualGovernanceConfig.firstSealRageQuitSupport; @@ -67,5 +64,4 @@ contract ImmutableDualGovernanceConfigProvider is IDualGovernanceConfigProvider config.rageQuitEthWithdrawalsMaxDelay = RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY; config.rageQuitEthWithdrawalsDelayGrowth = RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH; } - } diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index e6e8de03..4524c8ac 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -41,7 +41,6 @@ struct VetoerState { } contract Escrow is IEscrow { - using EscrowState for EscrowState.Context; using AssetsAccounting for AssetsAccounting.Context; using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.Context; @@ -116,9 +115,7 @@ contract Escrow is IEscrow { MIN_WITHDRAWALS_BATCH_SIZE = minWithdrawalsBatchSize; } - function initialize( - Duration minAssetsLockDuration - ) external { + function initialize(Duration minAssetsLockDuration) external { if (address(this) == _SELF) { revert NonProxyCallsForbidden(); } @@ -134,9 +131,7 @@ contract Escrow is IEscrow { // Lock & unlock stETH // --- - function lockStETH( - uint256 amount - ) external returns (uint256 lockedStETHShares) { + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); @@ -162,9 +157,7 @@ contract Escrow is IEscrow { // Lock & unlock wstETH // --- - function lockWstETH( - uint256 amount - ) external returns (uint256 lockedStETHShares) { + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); @@ -190,9 +183,7 @@ contract Escrow is IEscrow { // --- // Lock & unlock unstETH // --- - function lockUnstETH( - uint256[] memory unstETHIds - ) external { + function lockUnstETH(uint256[] memory unstETHIds) external { if (unstETHIds.length == 0) { revert EmptyUnstETHIds(); } @@ -210,9 +201,7 @@ contract Escrow is IEscrow { DUAL_GOVERNANCE.activateNextState(); } - function unlockUnstETH( - uint256[] memory unstETHIds - ) external { + function unlockUnstETH(uint256[] memory unstETHIds) external { DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); @@ -237,9 +226,7 @@ contract Escrow is IEscrow { // Convert to NFT // --- - function requestWithdrawals( - uint256[] calldata stETHAmounts - ) external returns (uint256[] memory unstETHIds) { + function requestWithdrawals(uint256[] calldata stETHAmounts) external returns (uint256[] memory unstETHIds) { _escrowState.checkSignallingEscrow(); unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(stETHAmounts, address(this)); @@ -267,9 +254,7 @@ contract Escrow is IEscrow { // Request withdrawal batches // --- - function requestNextWithdrawalsBatch( - uint256 batchSize - ) external { + function requestNextWithdrawalsBatch(uint256 batchSize) external { _escrowState.checkRageQuitEscrow(); if (batchSize < MIN_WITHDRAWALS_BATCH_SIZE) { @@ -303,9 +288,7 @@ contract Escrow is IEscrow { // Claim requested withdrawal batches // --- - function claimNextWithdrawalsBatch( - uint256 maxUnstETHIdsCount - ) external { + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { _escrowState.checkRageQuitEscrow(); _escrowState.checkBatchesClaimingInProgress(); @@ -375,9 +358,7 @@ contract Escrow is IEscrow { // Escrow management // --- - function setMinAssetsLockDuration( - Duration newMinAssetsLockDuration - ) external { + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { _checkCallerIsDualGovernance(); _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration); } @@ -393,9 +374,7 @@ contract Escrow is IEscrow { ethToWithdraw.sendTo(payable(msg.sender)); } - function withdrawETH( - uint256[] calldata unstETHIds - ) external { + function withdrawETH(uint256[] calldata unstETHIds) external { if (unstETHIds.length == 0) { revert EmptyUnstETHIds(); } @@ -419,9 +398,7 @@ contract Escrow is IEscrow { totals.unstETHFinalizedETH = unstETHTotals.finalizedETH.toUint256(); } - function getVetoerState( - address vetoer - ) external view returns (VetoerState memory state) { + function getVetoerState(address vetoer) external view returns (VetoerState memory state) { HolderAssets storage assets = _accounting.assets[vetoer]; state.unstETHIdsCount = assets.unstETHIds.length; @@ -434,9 +411,7 @@ contract Escrow is IEscrow { return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); } - function getNextWithdrawalBatch( - uint256 limit - ) external view returns (uint256[] memory unstETHIds) { + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds) { return _batchesQueue.getNextWithdrawalsBatches(limit); } @@ -508,5 +483,4 @@ contract Escrow is IEscrow { revert CallerIsNotDualGovernance(msg.sender); } } - } diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index 363a13ff..05afd75b 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -83,7 +83,6 @@ struct UnstETHRecord { /// @notice Provides functionality for accounting user stETH and unstETH tokens /// locked in the Escrow contract library AssetsAccounting { - /// @notice The context of the AssetsAccounting library /// @param stETHTotals The total number of shares and the amount of stETH locked by users /// @param unstETHTotals The total number of shares and the amount of unstETH locked by users @@ -279,9 +278,11 @@ library AssetsAccounting { // Getters // --- - function getLockedAssetsTotals( - Context storage self - ) internal view returns (SharesValue unfinalizedShares, ETHValue finalizedETH) { + function getLockedAssetsTotals(Context storage self) + internal + view + returns (SharesValue unfinalizedShares, ETHValue finalizedETH) + { finalizedETH = self.unstETHTotals.finalizedETH; unfinalizedShares = self.stETHTotals.lockedShares + self.unstETHTotals.unfinalizedShares; } @@ -416,12 +417,9 @@ library AssetsAccounting { amountWithdrawn = unstETHRecord.claimableAmount; } - function _checkNonZeroShares( - SharesValue shares - ) private pure { + function _checkNonZeroShares(SharesValue shares) private pure { if (shares == SharesValues.ZERO) { revert InvalidSharesValue(SharesValues.ZERO); } } - } diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 266643e2..b6a75af6 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -22,7 +22,6 @@ enum State { } library DualGovernanceStateMachine { - using DualGovernanceConfig for DualGovernanceConfig.Context; struct Context { @@ -129,21 +128,15 @@ library DualGovernanceStateMachine { emit DualGovernanceStateChanged(currentState, newState, self); } - function getCurrentContext( - Context storage self - ) internal pure returns (Context memory) { + function getCurrentContext(Context storage self) internal pure returns (Context memory) { return self; } - function getCurrentState( - Context storage self - ) internal view returns (State) { + function getCurrentState(Context storage self) internal view returns (State) { return self.state; } - function getNormalOrVetoCooldownStateExitedAt( - Context storage self - ) internal view returns (Timestamp) { + function getNormalOrVetoCooldownStateExitedAt(Context storage self) internal view returns (Timestamp) { return self.normalOrVetoCooldownExitedAt; } @@ -154,9 +147,7 @@ library DualGovernanceStateMachine { return config.calcVetoSignallingDuration(self.signallingEscrow.getRageQuitSupport()); } - function canSubmitProposal( - Context storage self - ) internal view returns (bool) { + function canSubmitProposal(Context storage self) internal view returns (bool) { State state = self.state; return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; } @@ -182,11 +173,9 @@ library DualGovernanceStateMachine { self.signallingEscrow = newSignallingEscrow; emit NewSignallingEscrowDeployed(newSignallingEscrow); } - } library DualGovernanceStateTransitions { - using DualGovernanceConfig for DualGovernanceConfig.Context; function getStateTransition( @@ -281,5 +270,4 @@ library DualGovernanceStateTransitions { ? State.VetoSignalling : State.VetoCooldown; } - } diff --git a/contracts/libraries/EscrowState.sol b/contracts/libraries/EscrowState.sol index 66a3b8f3..84d8ec14 100644 --- a/contracts/libraries/EscrowState.sol +++ b/contracts/libraries/EscrowState.sol @@ -21,7 +21,6 @@ enum State { /// @title EscrowState /// @notice Represents the logic to manipulate the state of the Escrow library EscrowState { - // --- // Errors // --- @@ -88,9 +87,7 @@ library EscrowState { /// @notice Starts the rage quit extension period /// @param self The context of the Escrow instance - function startRageQuitExtensionPeriod( - Context storage self - ) internal { + function startRageQuitExtensionPeriod(Context storage self) internal { self.rageQuitExtensionPeriodStartedAt = Timestamps.now(); emit RageQuitExtensionPeriodStarted(self.rageQuitExtensionPeriodStartedAt); } @@ -111,25 +108,19 @@ library EscrowState { /// @notice Checks if the Escrow is in the SignallingEscrow state /// @param self The context of the Escrow instance - function checkSignallingEscrow( - Context storage self - ) internal view { + function checkSignallingEscrow(Context storage self) internal view { _checkState(self, State.SignallingEscrow); } /// @notice Checks if the Escrow is in the RageQuitEscrow state /// @param self The context of the Escrow instance - function checkRageQuitEscrow( - Context storage self - ) internal view { + function checkRageQuitEscrow(Context storage self) internal view { _checkState(self, State.RageQuitEscrow); } /// @notice Checks if batch claiming is in progress /// @param self The context of the Escrow instance - function checkBatchesClaimingInProgress( - Context storage self - ) internal view { + function checkBatchesClaimingInProgress(Context storage self) internal view { if (!self.rageQuitExtensionPeriodStartedAt.isZero()) { revert ClaimingIsFinished(); } @@ -137,9 +128,7 @@ library EscrowState { /// @notice Checks if the withdrawals delay has passed /// @param self The context of the Escrow instance - function checkEthWithdrawalsDelayPassed( - Context storage self - ) internal view { + function checkEthWithdrawalsDelayPassed(Context storage self) internal view { if (self.rageQuitExtensionPeriodStartedAt.isZero()) { revert RageQuitExtensionPeriodNotStarted(); } @@ -156,18 +145,14 @@ library EscrowState { /// @notice Checks if the rage quit extension period has started /// @param self The context of the Escrow instance /// @return True if the rage quit extension period has started, false otherwise - function isRageQuitExtensionPeriodStarted( - Context storage self - ) internal view returns (bool) { + function isRageQuitExtensionPeriodStarted(Context storage self) internal view returns (bool) { return self.rageQuitExtensionPeriodStartedAt.isNotZero(); } /// @notice Checks if the rage quit extension period has passed /// @param self The context of the Escrow instance /// @return True if the rage quit extension period has passed, false otherwise - function isRageQuitExtensionPeriodPassed( - Context storage self - ) internal view returns (bool) { + function isRageQuitExtensionPeriodPassed(Context storage self) internal view returns (bool) { Timestamp rageQuitExtensionPeriodStartedAt = self.rageQuitExtensionPeriodStartedAt; return rageQuitExtensionPeriodStartedAt.isNotZero() && Timestamps.now() > self.rageQuitExtensionPeriodDuration.addTo(rageQuitExtensionPeriodStartedAt); @@ -176,9 +161,7 @@ library EscrowState { /// @notice Checks if the Escrow is in the RageQuitEscrow state /// @param self The context of the Escrow instance /// @return True if the Escrow is in the RageQuitEscrow state, false otherwise - function isRageQuitEscrow( - Context storage self - ) internal view returns (bool) { + function isRageQuitEscrow(Context storage self) internal view returns (bool) { return self.state == State.RageQuitEscrow; } @@ -211,5 +194,4 @@ library EscrowState { self.minAssetsLockDuration = newMinAssetsLockDuration; emit MinAssetsLockDurationSet(newMinAssetsLockDuration); } - } diff --git a/foundry.toml b/foundry.toml index 4a8ea771..ef629cd3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ src = 'contracts' out = 'out' libs = ['node_modules', 'lib'] test = 'test' -cache_path = 'cache_forge' +cache_path = 'cache_forge' # solc-version = "0.8.26" no-match-path = 'test/kontrol/*' @@ -14,5 +14,4 @@ test = 'test/kontrol' [fmt] multiline_func_header = 'params_first_multi' -single_line_statement_blocks = 'multi' -line_length = 120 \ No newline at end of file +line_length = 120 diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 1bc25401..425bb571 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -15,7 +15,6 @@ import {Escrow, VetoerState, LockedAssetsTotals, WithdrawalsBatchesQueue} from " import {ScenarioTestBlueprint, LidoUtils, console} from "../utils/scenario-test-blueprint.sol"; contract EscrowHappyPath is ScenarioTestBlueprint { - using LidoUtils for LidoUtils.Context; Escrow internal escrow; @@ -631,20 +630,15 @@ contract EscrowHappyPath is ScenarioTestBlueprint { _lockWstETH(vetoer, wstEthAmount); } - function externalUnlockStETH( - address vetoer - ) external { + function externalUnlockStETH(address vetoer) external { _unlockStETH(vetoer); } - function externalUnlockWstETH( - address vetoer - ) external { + function externalUnlockWstETH(address vetoer) external { _unlockWstETH(vetoer); } function externalUnlockUnstETH(address vetoer, uint256[] memory nftIds) external { _unlockUnstETH(vetoer, nftIds); } - } diff --git a/test/scenario/gov-state-transitions.t.sol b/test/scenario/gov-state-transitions.t.sol index c3d9e150..7946257a 100644 --- a/test/scenario/gov-state-transitions.t.sol +++ b/test/scenario/gov-state-transitions.t.sol @@ -6,7 +6,6 @@ import {PercentsD16} from "contracts/types/PercentD16.sol"; import {ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; contract GovernanceStateTransitions is ScenarioTestBlueprint { - address internal immutable _VETOER = makeAddr("VETOER"); function setUp() external { @@ -134,5 +133,4 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _activateNextState(); _assertRageQuitState(); } - } diff --git a/test/scenario/last-moment-malicious-proposal.t.sol b/test/scenario/last-moment-malicious-proposal.t.sol index 9101301f..7d4f6503 100644 --- a/test/scenario/last-moment-malicious-proposal.t.sol +++ b/test/scenario/last-moment-malicious-proposal.t.sol @@ -12,7 +12,6 @@ import { } from "../utils/scenario-test-blueprint.sol"; contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { - function setUp() external { _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); } @@ -330,10 +329,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { } } - function scheduleProposalExternal( - uint256 proposalId - ) external { + function scheduleProposalExternal(uint256 proposalId) external { _scheduleProposalViaDualGovernance(proposalId); } - } diff --git a/test/scenario/tiebreaker.t.sol b/test/scenario/tiebreaker.t.sol index 505658fa..3a95ddcc 100644 --- a/test/scenario/tiebreaker.t.sol +++ b/test/scenario/tiebreaker.t.sol @@ -7,7 +7,6 @@ import {PercentsD16} from "contracts/types/PercentD16.sol"; import {ScenarioTestBlueprint, ExternalCall, ExternalCallHelpers} from "../utils/scenario-test-blueprint.sol"; contract TiebreakerScenarioTest is ScenarioTestBlueprint { - address internal immutable _VETOER = makeAddr("VETOER"); uint256 public constant PAUSE_INFINITELY = type(uint256).max; @@ -167,5 +166,4 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { assertEq(_lido.withdrawalQueue.isPaused(), false); } - } diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index 9d29c8b9..8a297d37 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -10,7 +10,6 @@ import {LidoUtils} from "../utils/lido-utils.sol"; import {Escrow, ExternalCall, ExternalCallHelpers, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { - using LidoUtils for LidoUtils.Context; function setUp() external { @@ -103,10 +102,7 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { } } - function scheduleProposalExternal( - uint256 proposalId - ) external { + function scheduleProposalExternal(uint256 proposalId) external { _scheduleProposal(_dualGovernance, proposalId); } - } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 5047a875..de5221af 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -21,7 +21,6 @@ import {TimelockMock} from "test/mocks/TimelockMock.sol"; import {WithdrawalQueueMock} from "test/mocks/WithdrawalQueueMock.sol"; contract DualGovernanceUnitTests is UnitTest { - Executor private _executor = new Executor(address(this)); StETHMock private immutable _STETH_MOCK = new StETHMock(); @@ -237,5 +236,4 @@ contract DualGovernanceUnitTests is UnitTest { // mock timelock doesn't uses proposal data _timelock.submit(address(0), new ExternalCall[](0)); } - } diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 4095a8bc..be1bbeef 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -13,7 +13,6 @@ import {UnitTest} from "test/utils/unit-test.sol"; import {EscrowMock} from "test/mocks/EscrowMock.sol"; contract DualGovernanceStateMachineUnitTests is UnitTest { - using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; address private immutable _ESCROW_MASTER_COPY = address(new EscrowMock()); @@ -81,5 +80,4 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { assertEq(_stateMachine.rageQuitRound, 0); assertEq(_stateMachine.state, State.Normal); } - } diff --git a/test/unit/libraries/EscrowState.t.sol b/test/unit/libraries/EscrowState.t.sol index 2c5d4510..9018073f 100644 --- a/test/unit/libraries/EscrowState.t.sol +++ b/test/unit/libraries/EscrowState.t.sol @@ -11,16 +11,13 @@ Duration constant D0 = Durations.ZERO; Timestamp constant T0 = Timestamps.ZERO; contract EscrowStateUnitTests is UnitTest { - EscrowState.Context private _context; // --- // initialize() // --- - function testFuzz_initialize_happyPath( - Duration minAssetsLockDuration - ) external { + function testFuzz_initialize_happyPath(Duration minAssetsLockDuration) external { _context.state = State.NotInitialized; vm.expectEmit(); @@ -38,9 +35,7 @@ contract EscrowStateUnitTests is UnitTest { }); } - function testFuzz_initialize_RevertOn_InvalidState( - Duration minAssetsLockDuration - ) external { + function testFuzz_initialize_RevertOn_InvalidState(Duration minAssetsLockDuration) external { _context.state = State.SignallingEscrow; // TODO: not very informative, maybe need to change to `revert UnexpectedState(self.state);`: UnexpectedState(NotInitialized)[current implementation] => UnexpectedState(SignallingEscrow)[proposed] @@ -108,9 +103,7 @@ contract EscrowStateUnitTests is UnitTest { // setMinAssetsLockDuration() // --- - function test_setMinAssetsLockDuration_happyPath( - Duration minAssetsLockDuration - ) external { + function test_setMinAssetsLockDuration_happyPath(Duration minAssetsLockDuration) external { vm.assume(minAssetsLockDuration != Durations.ZERO); vm.expectEmit(); @@ -127,9 +120,7 @@ contract EscrowStateUnitTests is UnitTest { }); } - function test_setMinAssetsLockDuration_RevertWhen_DurationNotChanged( - Duration minAssetsLockDuration - ) external { + function test_setMinAssetsLockDuration_RevertWhen_DurationNotChanged(Duration minAssetsLockDuration) external { _context.minAssetsLockDuration = minAssetsLockDuration; vm.expectRevert( @@ -176,9 +167,9 @@ contract EscrowStateUnitTests is UnitTest { EscrowState.checkBatchesClaimingInProgress(_context); } - function testFuzz_checkBatchesClaimingInProgress_RevertOn_InvalidState( - Timestamp rageQuitExtensionPeriodStartedAt - ) external { + function testFuzz_checkBatchesClaimingInProgress_RevertOn_InvalidState(Timestamp rageQuitExtensionPeriodStartedAt) + external + { vm.assume(rageQuitExtensionPeriodStartedAt > Timestamps.ZERO); _context.rageQuitExtensionPeriodStartedAt = rageQuitExtensionPeriodStartedAt; vm.expectRevert(EscrowState.ClaimingIsFinished.selector); @@ -264,9 +255,7 @@ contract EscrowStateUnitTests is UnitTest { // isRageQuitExtensionPeriodStarted() // --- - function testFuzz_isRageQuitExtensionDelayStarted_happyPath( - Timestamp rageQuitExtensionPeriodStartedAt - ) external { + function testFuzz_isRageQuitExtensionDelayStarted_happyPath(Timestamp rageQuitExtensionPeriodStartedAt) external { _context.rageQuitExtensionPeriodStartedAt = rageQuitExtensionPeriodStartedAt; bool res = EscrowState.isRageQuitExtensionPeriodStarted(_context); assertEq(res, _context.rageQuitExtensionPeriodStartedAt.isNotZero()); @@ -324,9 +313,7 @@ contract EscrowStateUnitTests is UnitTest { // isRageQuitEscrow() // --- - function testFuzz_isRageQuitEscrow( - bool expectedResult - ) external { + function testFuzz_isRageQuitEscrow(bool expectedResult) external { if (expectedResult) { _context.state = State.RageQuitEscrow; } @@ -355,5 +342,4 @@ contract EscrowStateUnitTests is UnitTest { function assertEq(State a, State b) internal { assertEq(uint256(a), uint256(b)); } - } diff --git a/test/utils/SetupDeployment.sol b/test/utils/SetupDeployment.sol index 555d85c2..c181fcd1 100644 --- a/test/utils/SetupDeployment.sol +++ b/test/utils/SetupDeployment.sol @@ -56,7 +56,6 @@ import {LidoUtils} from "./lido-utils.sol"; // --- abstract contract SetupDeployment is Test { - using Random for Random.Context; // --- // Helpers @@ -149,17 +148,13 @@ abstract contract SetupDeployment is Test { // Whole Setup Deployments // --- - function _deployTimelockedGovernanceSetup( - bool isEmergencyProtectionEnabled - ) internal { + function _deployTimelockedGovernanceSetup(bool isEmergencyProtectionEnabled) internal { _deployEmergencyProtectedTimelockContracts(isEmergencyProtectionEnabled); _timelockedGovernance = _deployTimelockedGovernance({governance: address(_lido.voting), timelock: _timelock}); _finalizeEmergencyProtectedTimelockDeploy(_timelockedGovernance); } - function _deployDualGovernanceSetup( - bool isEmergencyProtectionEnabled - ) internal { + function _deployDualGovernanceSetup(bool isEmergencyProtectionEnabled) internal { _deployEmergencyProtectedTimelockContracts(isEmergencyProtectionEnabled); _resealManager = _deployResealManager(_timelock); _dualGovernanceConfigProvider = _deployDualGovernanceConfigProvider(); @@ -241,9 +236,7 @@ abstract contract SetupDeployment is Test { // Emergency Protected Timelock Deployment // --- - function _deployEmergencyProtectedTimelockContracts( - bool isEmergencyProtectionEnabled - ) internal { + function _deployEmergencyProtectedTimelockContracts(bool isEmergencyProtectionEnabled) internal { _adminExecutor = _deployExecutor(address(this)); _timelock = _deployEmergencyProtectedTimelock(_adminExecutor); @@ -294,9 +287,7 @@ abstract contract SetupDeployment is Test { } } - function _finalizeEmergencyProtectedTimelockDeploy( - IGovernance governance - ) internal { + function _finalizeEmergencyProtectedTimelockDeploy(IGovernance governance) internal { _adminExecutor.execute( address(_timelock), 0, abi.encodeCall(_timelock.setupDelays, (_AFTER_SUBMIT_DELAY, _AFTER_SCHEDULE_DELAY)) ); @@ -304,15 +295,11 @@ abstract contract SetupDeployment is Test { _adminExecutor.transferOwnership(address(_timelock)); } - function _deployExecutor( - address owner - ) internal returns (Executor) { + function _deployExecutor(address owner) internal returns (Executor) { return new Executor(owner); } - function _deployEmergencyProtectedTimelock( - Executor adminExecutor - ) internal returns (EmergencyProtectedTimelock) { + function _deployEmergencyProtectedTimelock(Executor adminExecutor) internal returns (EmergencyProtectedTimelock) { return new EmergencyProtectedTimelock({ adminExecutor: address(adminExecutor), sanityCheckParams: EmergencyProtectedTimelock.SanityCheckParams({ @@ -388,9 +375,7 @@ abstract contract SetupDeployment is Test { ); } - function _deployResealManager( - ITimelock timelock - ) internal returns (ResealManager) { + function _deployResealManager(ITimelock timelock) internal returns (ResealManager) { return new ResealManager(timelock); } @@ -443,13 +428,10 @@ abstract contract SetupDeployment is Test { // Helper methods // --- - function _generateRandomAddresses( - uint256 count - ) internal returns (address[] memory addresses) { + function _generateRandomAddresses(uint256 count) internal returns (address[] memory addresses) { addresses = new address[](count); for (uint256 i = 0; i < count; ++i) { addresses[i] = _random.nextAddress(); } } - } diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 5e885816..a7e8539f 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -49,7 +49,6 @@ import {TestingAssertEqExtender} from "./testing-assert-eq-extender.sol"; uint256 constant FORK_BLOCK_NUMBER = 20218312; contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { - using LidoUtils for LidoUtils.Context; constructor() SetupDeployment(LidoUtils.mainnet(), Random.create(block.timestamp)) { @@ -125,9 +124,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { _lido.submitWstETH(account, _lido.calcSharesToDepositFromPercentageOfTVL(tvlPercentage)); } - function _getBalances( - address vetoer - ) internal view returns (Balances memory balances) { + function _getBalances(address vetoer) internal view returns (Balances memory balances) { uint256 stETHAmount = _lido.stETH.balanceOf(vetoer); uint256 wstETHShares = _lido.wstETH.balanceOf(vetoer); balances = Balances({ @@ -145,15 +142,11 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { _lido.finalizeWithdrawalQueue(); } - function _finalizeWithdrawalQueue( - uint256 id - ) internal { + function _finalizeWithdrawalQueue(uint256 id) internal { _lido.finalizeWithdrawalQueue(id); } - function _simulateRebase( - PercentD16 rebaseFactor - ) internal { + function _simulateRebase(PercentD16 rebaseFactor) internal { _lido.simulateRebase(rebaseFactor); } @@ -174,9 +167,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { vm.stopPrank(); } - function _unlockStETH( - address vetoer - ) internal { + function _unlockStETH(address vetoer) internal { vm.startPrank(vetoer); _getVetoSignallingEscrow().unlockStETH(); vm.stopPrank(); @@ -196,9 +187,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { vm.stopPrank(); } - function _unlockWstETH( - address vetoer - ) internal { + function _unlockWstETH(address vetoer) internal { Escrow escrow = _getVetoSignallingEscrow(); uint256 wstETHBalanceBefore = _lido.wstETH.balanceOf(vetoer); VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); @@ -320,15 +309,11 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(proposalId, proposalsCountBefore + 1); } - function _scheduleProposalViaDualGovernance( - uint256 proposalId - ) internal { + function _scheduleProposalViaDualGovernance(uint256 proposalId) internal { _scheduleProposal(_dualGovernance, proposalId); } - function _scheduleProposalViaTimelockedGovernance( - uint256 proposalId - ) internal { + function _scheduleProposalViaTimelockedGovernance(uint256 proposalId) internal { _scheduleProposal(_timelockedGovernance, proposalId); } @@ -336,9 +321,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { governance.scheduleProposal(proposalId); } - function _executeProposal( - uint256 proposalId - ) internal { + function _executeProposal(uint256 proposalId) internal { _timelock.execute(proposalId); } @@ -424,9 +407,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalSubmitted( - uint256 proposalId - ) internal { + function _assertProposalSubmitted(uint256 proposalId) internal { assertEq( _timelock.getProposal(proposalId).status, ProposalStatus.Submitted, @@ -434,9 +415,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalScheduled( - uint256 proposalId - ) internal { + function _assertProposalScheduled(uint256 proposalId) internal { assertEq( _timelock.getProposal(proposalId).status, ProposalStatus.Scheduled, @@ -444,9 +423,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalExecuted( - uint256 proposalId - ) internal { + function _assertProposalExecuted(uint256 proposalId) internal { assertEq( _timelock.getProposal(proposalId).status, ProposalStatus.Executed, @@ -454,9 +431,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ); } - function _assertProposalCancelled( - uint256 proposalId - ) internal { + function _assertProposalCancelled(uint256 proposalId) internal { assertEq(_timelock.getProposal(proposalId).status, ProposalStatus.Cancelled, "Proposal not in 'Canceled' state"); } @@ -543,16 +518,12 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { // Utils Methods // --- - function _step( - string memory text - ) internal view { + function _step(string memory text) internal view { // solhint-disable-next-line console.log(string.concat(">>> ", text, " <<<")); } - function _wait( - Duration duration - ) internal { + function _wait(Duration duration) internal { vm.warp(duration.addTo(Timestamps.now()).toSeconds()); } @@ -573,9 +544,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { _emergencyActivationCommittee.executeActivateEmergencyMode(); } - function _executeEmergencyExecute( - uint256 proposalId - ) internal { + function _executeEmergencyExecute(uint256 proposalId) internal { address[] memory members = _emergencyExecutionCommittee.getMembers(); for (uint256 i = 0; i < _emergencyExecutionCommittee.quorum(); ++i) { vm.prank(members[i]); @@ -600,18 +569,14 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { uint256 _seconds; } - function _toDuration( - uint256 timestamp - ) internal pure returns (DurationStruct memory duration) { + function _toDuration(uint256 timestamp) internal pure returns (DurationStruct memory duration) { duration._days = timestamp / 1 days; duration._hours = (timestamp - 1 days * duration._days) / 1 hours; duration._minutes = (timestamp - 1 days * duration._days - 1 hours * duration._hours) / 1 minutes; duration._seconds = timestamp % 1 minutes; } - function _formatDuration( - DurationStruct memory duration - ) internal pure returns (string memory) { + function _formatDuration(DurationStruct memory duration) internal pure returns (string memory) { // format example: 1d:22h:33m:12s return string( abi.encodePacked( @@ -626,5 +591,4 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { ) ); } - } From 04d63b2bb695b937c1784e050c6ec5df52d87eda Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 10 Sep 2024 12:05:26 +0300 Subject: [PATCH 44/86] fix: remove skipping set quorum on adding/removing members --- contracts/committees/HashConsensus.sol | 49 ++++++++++++++++---------- test/unit/HashConsensus.t.sol | 17 +++++---- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index cf7eb4c0..66776116 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -72,7 +72,9 @@ abstract contract HashConsensus is Ownable { /// @notice Marks a hash as used if quorum is reached and timelock has passed /// @dev Internal function that handles marking a hash as used /// @param hash The hash to mark as used - function _markUsed(bytes32 hash) internal { + function _markUsed( + bytes32 hash + ) internal { if (_hashStates[hash].usedAt > Timestamps.from(0)) { revert HashAlreadyUsed(hash); } @@ -98,11 +100,9 @@ abstract contract HashConsensus is Ownable { /// @return executionQuorum The required number of votes for execution /// @return scheduledAt The timestamp when the quorum was reached or scheduleProposal was called /// @return isUsed Whether the hash has been used - function _getHashState(bytes32 hash) - internal - view - returns (uint256 support, uint256 executionQuorum, Timestamp scheduledAt, bool isUsed) - { + function _getHashState( + bytes32 hash + ) internal view returns (uint256 support, uint256 executionQuorum, Timestamp scheduledAt, bool isUsed) { support = _getSupport(hash); executionQuorum = quorum; scheduledAt = _hashStates[hash].scheduledAt; @@ -139,9 +139,7 @@ abstract contract HashConsensus is Ownable { emit MemberRemoved(membersToRemove[i]); } - if (newQuorum != quorum) { - _setQuorum(newQuorum); - } + _setQuorum(newQuorum); } /// @notice Gets the list of committee members @@ -155,14 +153,18 @@ abstract contract HashConsensus is Ownable { /// @dev Public function to check membership status /// @param member The address to check /// @return A boolean indicating whether the address is a member - function isMember(address member) public view returns (bool) { + function isMember( + address member + ) public view returns (bool) { return _members.contains(member); } /// @notice Sets the timelock duration /// @dev Only callable by the owner /// @param timelock The new timelock duration in seconds - function setTimelockDuration(Duration timelock) public { + function setTimelockDuration( + Duration timelock + ) public { _checkOwner(); if (timelock == timelockDuration) { revert InvalidTimelockDuration(timelock); @@ -174,8 +176,13 @@ abstract contract HashConsensus is Ownable { /// @notice Sets the quorum value /// @dev Only callable by the owner /// @param newQuorum The new quorum value - function setQuorum(uint256 newQuorum) public { + function setQuorum( + uint256 newQuorum + ) public { _checkOwner(); + if (newQuorum == quorum) { + revert InvalidQuorum(); + } _setQuorum(newQuorum); } @@ -184,7 +191,9 @@ abstract contract HashConsensus is Ownable { /// the proposal has not been scheduled yet. Could happen when execution quorum was set to the same value as /// current support of the proposal. /// @param hash The hash of the proposal to be scheduled - function schedule(bytes32 hash) public { + function schedule( + bytes32 hash + ) public { if (_hashStates[hash].usedAt > Timestamps.from(0)) { revert HashAlreadyUsed(hash); } @@ -202,8 +211,10 @@ abstract contract HashConsensus is Ownable { /// @notice Sets the execution quorum required for certain operations. /// @dev The quorum value must be greater than zero and not exceed the current number of members. /// @param executionQuorum The new quorum value to be set. - function _setQuorum(uint256 executionQuorum) internal { - if (executionQuorum == 0 || executionQuorum > _members.length() || executionQuorum == quorum) { + function _setQuorum( + uint256 executionQuorum + ) internal { + if (executionQuorum == 0 || executionQuorum > _members.length()) { revert InvalidQuorum(); } quorum = executionQuorum; @@ -224,16 +235,16 @@ abstract contract HashConsensus is Ownable { emit MemberAdded(newMembers[i]); } - if (executionQuorum != quorum) { - _setQuorum(executionQuorum); - } + _setQuorum(executionQuorum); } /// @notice Gets the number of votes in support of a given hash /// @dev Internal function to count the votes in support of a hash /// @param hash The hash to check /// @return support The number of votes in support of the hash - function _getSupport(bytes32 hash) internal view returns (uint256 support) { + function _getSupport( + bytes32 hash + ) internal view returns (uint256 support) { for (uint256 i = 0; i < _members.length(); ++i) { if (approves[_members.at(i)][hash]) { support++; diff --git a/test/unit/HashConsensus.t.sol b/test/unit/HashConsensus.t.sol index 605a14a5..ead04151 100644 --- a/test/unit/HashConsensus.t.sol +++ b/test/unit/HashConsensus.t.sol @@ -58,6 +58,7 @@ abstract contract HashConsensusUnitTest is UnitTest { function test_constructor_RevertOn_WithZeroQuorum() public { uint256 invalidQuorum = 0; + vm.expectRevert(abi.encodeWithSignature("InvalidQuorum()")); new HashConsensusInstance(_owner, _committeeMembers, invalidQuorum, Durations.from(1)); } @@ -404,20 +405,22 @@ contract HashConsensusWrapper is HashConsensus { _vote(hash, support); } - function execute(bytes32 hash) public { + function execute( + bytes32 hash + ) public { _markUsed(hash); _target.trigger(); } - function getHashState(bytes32 hash) - public - view - returns (uint256 support, uint256 executionQuorum, Timestamp scheduledAt, bool isExecuted) - { + function getHashState( + bytes32 hash + ) public view returns (uint256 support, uint256 executionQuorum, Timestamp scheduledAt, bool isExecuted) { return _getHashState(hash); } - function getSupport(bytes32 hash) public view returns (uint256 support) { + function getSupport( + bytes32 hash + ) public view returns (uint256 support) { return _getSupport(hash); } From fdca05998c40ae00b799dea1b338f2e20f2900c1 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 10 Sep 2024 12:11:56 +0300 Subject: [PATCH 45/86] fix: formatting --- contracts/committees/HashConsensus.sol | 36 ++++++++---------------- test/unit/committees/HashConsensus.t.sol | 16 +++++------ 2 files changed, 19 insertions(+), 33 deletions(-) diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index 66776116..6a32c3a7 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -72,9 +72,7 @@ abstract contract HashConsensus is Ownable { /// @notice Marks a hash as used if quorum is reached and timelock has passed /// @dev Internal function that handles marking a hash as used /// @param hash The hash to mark as used - function _markUsed( - bytes32 hash - ) internal { + function _markUsed(bytes32 hash) internal { if (_hashStates[hash].usedAt > Timestamps.from(0)) { revert HashAlreadyUsed(hash); } @@ -100,9 +98,11 @@ abstract contract HashConsensus is Ownable { /// @return executionQuorum The required number of votes for execution /// @return scheduledAt The timestamp when the quorum was reached or scheduleProposal was called /// @return isUsed Whether the hash has been used - function _getHashState( - bytes32 hash - ) internal view returns (uint256 support, uint256 executionQuorum, Timestamp scheduledAt, bool isUsed) { + function _getHashState(bytes32 hash) + internal + view + returns (uint256 support, uint256 executionQuorum, Timestamp scheduledAt, bool isUsed) + { support = _getSupport(hash); executionQuorum = quorum; scheduledAt = _hashStates[hash].scheduledAt; @@ -153,18 +153,14 @@ abstract contract HashConsensus is Ownable { /// @dev Public function to check membership status /// @param member The address to check /// @return A boolean indicating whether the address is a member - function isMember( - address member - ) public view returns (bool) { + function isMember(address member) public view returns (bool) { return _members.contains(member); } /// @notice Sets the timelock duration /// @dev Only callable by the owner /// @param timelock The new timelock duration in seconds - function setTimelockDuration( - Duration timelock - ) public { + function setTimelockDuration(Duration timelock) public { _checkOwner(); if (timelock == timelockDuration) { revert InvalidTimelockDuration(timelock); @@ -176,9 +172,7 @@ abstract contract HashConsensus is Ownable { /// @notice Sets the quorum value /// @dev Only callable by the owner /// @param newQuorum The new quorum value - function setQuorum( - uint256 newQuorum - ) public { + function setQuorum(uint256 newQuorum) public { _checkOwner(); if (newQuorum == quorum) { revert InvalidQuorum(); @@ -191,9 +185,7 @@ abstract contract HashConsensus is Ownable { /// the proposal has not been scheduled yet. Could happen when execution quorum was set to the same value as /// current support of the proposal. /// @param hash The hash of the proposal to be scheduled - function schedule( - bytes32 hash - ) public { + function schedule(bytes32 hash) public { if (_hashStates[hash].usedAt > Timestamps.from(0)) { revert HashAlreadyUsed(hash); } @@ -211,9 +203,7 @@ abstract contract HashConsensus is Ownable { /// @notice Sets the execution quorum required for certain operations. /// @dev The quorum value must be greater than zero and not exceed the current number of members. /// @param executionQuorum The new quorum value to be set. - function _setQuorum( - uint256 executionQuorum - ) internal { + function _setQuorum(uint256 executionQuorum) internal { if (executionQuorum == 0 || executionQuorum > _members.length()) { revert InvalidQuorum(); } @@ -242,9 +232,7 @@ abstract contract HashConsensus is Ownable { /// @dev Internal function to count the votes in support of a hash /// @param hash The hash to check /// @return support The number of votes in support of the hash - function _getSupport( - bytes32 hash - ) internal view returns (uint256 support) { + function _getSupport(bytes32 hash) internal view returns (uint256 support) { for (uint256 i = 0; i < _members.length(); ++i) { if (approves[_members.at(i)][hash]) { support++; diff --git a/test/unit/committees/HashConsensus.t.sol b/test/unit/committees/HashConsensus.t.sol index 8083fa98..e01729e2 100644 --- a/test/unit/committees/HashConsensus.t.sol +++ b/test/unit/committees/HashConsensus.t.sol @@ -405,22 +405,20 @@ contract HashConsensusWrapper is HashConsensus { _vote(hash, support); } - function execute( - bytes32 hash - ) public { + function execute(bytes32 hash) public { _markUsed(hash); _target.trigger(); } - function getHashState( - bytes32 hash - ) public view returns (uint256 support, uint256 executionQuorum, Timestamp scheduledAt, bool isExecuted) { + function getHashState(bytes32 hash) + public + view + returns (uint256 support, uint256 executionQuorum, Timestamp scheduledAt, bool isExecuted) + { return _getHashState(hash); } - function getSupport( - bytes32 hash - ) public view returns (uint256 support) { + function getSupport(bytes32 hash) public view returns (uint256 support) { return _getSupport(hash); } From 39ad1ff105e4c06cd9985e1b7fa530459a87f6df Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 10 Sep 2024 17:21:42 +0400 Subject: [PATCH 46/86] Add lower bound for MIN_STETH_WITHDRAWAL_AMOUNT --- contracts/Escrow.sol | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index f71d5f54..b72b1b99 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -66,6 +66,15 @@ contract Escrow is IEscrow { event ConfigProviderSet(address newConfigProvider); + // --- + // Constants + // --- + + /// @dev The lower limit for stETH transfers when requesting a withdrawal batch + /// during the Rage Quit phase. For more details, see https://github.com/lidofinance/lido-dao/issues/442. + /// The current value is chosen to ensure functionality over an extended period, spanning several decades. + uint256 private constant _MIN_TRANSFERRABLE_ST_ETH_AMOUNT = 8 wei; + // --- // Sanity check params immutables // --- @@ -265,7 +274,9 @@ contract Escrow is IEscrow { uint256 minStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT(); uint256 maxStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); - if (stETHRemaining < minStETHWithdrawalRequestAmount) { + /// @dev This check ensures that even if MIN_STETH_WITHDRAWAL_AMOUNT is set too low, + /// the withdrawal batch request process can still be completed successfully + if (stETHRemaining < Math.max(_MIN_TRANSFERRABLE_ST_ETH_AMOUNT, minStETHWithdrawalRequestAmount)) { return _batchesQueue.close(); } From 1601e12a8d62fdab5fa9aca88047ff35625c489c Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 10 Sep 2024 19:33:28 +0400 Subject: [PATCH 47/86] EmergencyExecutionCommittee _supports arg renaming --- contracts/committees/EmergencyExecutionCommittee.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol index 47a26334..d9bac860 100644 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -39,12 +39,12 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { /// @notice Votes on an emergency execution proposal /// @dev Only callable by committee members /// @param proposalId The ID of the proposal to vote on - /// @param _supports Indicates whether the member supports the proposal execution - function voteEmergencyExecute(uint256 proposalId, bool _supports) public { + /// @param _support Indicates whether the member supports the proposal execution + function voteEmergencyExecute(uint256 proposalId, bool _support) public { _checkCallerIsMember(); _checkProposalExists(proposalId); (bytes memory proposalData, bytes32 key) = _encodeEmergencyExecute(proposalId); - _vote(key, _supports); + _vote(key, _support); _pushProposal(key, uint256(ProposalType.EmergencyExecute), proposalData); } From 2698d79b6e8c26f97c216a9a23690a426d3e84f0 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 10 Sep 2024 19:52:48 +0400 Subject: [PATCH 48/86] Remove unused errors, events and imports --- contracts/DualGovernance.sol | 6 ++---- contracts/DualGovernanceConfigProvider.sol | 4 +++- contracts/EmergencyProtectedTimelock.sol | 3 +-- contracts/Escrow.sol | 6 ------ contracts/committees/EmergencyActivationCommittee.sol | 8 +++++--- contracts/committees/EmergencyExecutionCommittee.sol | 9 ++++++--- contracts/committees/HashConsensus.sol | 1 + contracts/committees/ResealCommittee.sol | 6 ++++-- contracts/committees/TiebreakerSubCommittee.sol | 6 ++++-- contracts/libraries/AssetsAccounting.sol | 2 +- contracts/libraries/DualGovernanceStateMachine.sol | 6 +++--- contracts/libraries/EmergencyProtection.sol | 1 + contracts/libraries/ExecutableProposals.sol | 3 ++- contracts/libraries/Proposers.sol | 1 - contracts/libraries/Tiebreaker.sol | 3 +++ contracts/libraries/WithdrawalBatchesQueue.sol | 2 -- 16 files changed, 36 insertions(+), 31 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index b5ca36ca..4c81d3ec 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -2,15 +2,13 @@ pragma solidity 0.8.26; import {Duration} from "./types/Duration.sol"; -import {Timestamp} from "./types/Timestamp.sol"; -import {ITimelock} from "./interfaces/ITimelock.sol"; -import {IResealManager} from "./interfaces/IResealManager.sol"; import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; +import {ITimelock} from "./interfaces/ITimelock.sol"; +import {ITiebreaker} from "./interfaces/ITiebreaker.sol"; import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; -import {ITiebreaker} from "./interfaces/ITiebreaker.sol"; import {IResealManager} from "./interfaces/IResealManager.sol"; import {Proposers} from "./libraries/Proposers.sol"; diff --git a/contracts/DualGovernanceConfigProvider.sol b/contracts/DualGovernanceConfigProvider.sol index 806e3dd5..e50bec1c 100644 --- a/contracts/DualGovernanceConfigProvider.sol +++ b/contracts/DualGovernanceConfigProvider.sol @@ -3,9 +3,11 @@ pragma solidity 0.8.26; import {Duration} from "./types/Duration.sol"; import {PercentD16} from "./types/PercentD16.sol"; -import {DualGovernanceConfig} from "./libraries/DualGovernanceConfig.sol"; + import {IDualGovernanceConfigProvider} from "./interfaces/IDualGovernanceConfigProvider.sol"; +import {DualGovernanceConfig} from "./libraries/DualGovernanceConfig.sol"; + contract ImmutableDualGovernanceConfigProvider is IDualGovernanceConfigProvider { PercentD16 public immutable FIRST_SEAL_RAGE_QUIT_SUPPORT; PercentD16 public immutable SECOND_SEAL_RAGE_QUIT_SUPPORT; diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index d861de80..7cf1a99b 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -5,11 +5,10 @@ import {Duration} from "./types/Duration.sol"; import {Timestamp} from "./types/Timestamp.sol"; import {IOwnable} from "./interfaces/IOwnable.sol"; -import {ProposalStatus} from "./interfaces/ITimelock.sol"; import {IEmergencyProtectedTimelock} from "./interfaces/IEmergencyProtectedTimelock.sol"; -import {TimelockState} from "./libraries/TimelockState.sol"; import {ExternalCall} from "./libraries/ExternalCalls.sol"; +import {TimelockState} from "./libraries/TimelockState.sol"; import {ExecutableProposals} from "./libraries/ExecutableProposals.sol"; import {EmergencyProtection} from "./libraries/EmergencyProtection.sol"; diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index f71d5f54..227cfb52 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -60,12 +60,6 @@ contract Escrow is IEscrow { error InvalidHintsLength(uint256 actual, uint256 expected); error InvalidETHSender(address actual, address expected); - // --- - // Events - // --- - - event ConfigProviderSet(address newConfigProvider); - // --- // Sanity check params immutables // --- diff --git a/contracts/committees/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol index 7e01fd48..34d10e98 100644 --- a/contracts/committees/EmergencyActivationCommittee.sol +++ b/contracts/committees/EmergencyActivationCommittee.sol @@ -3,11 +3,13 @@ pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; -import {HashConsensus} from "./HashConsensus.sol"; -import {ITimelock} from "../interfaces/ITimelock.sol"; -import {Duration, Durations} from "../types/Duration.sol"; +import {Durations} from "../types/Duration.sol"; import {Timestamp} from "../types/Timestamp.sol"; +import {ITimelock} from "../interfaces/ITimelock.sol"; + +import {HashConsensus} from "./HashConsensus.sol"; + /// @title Emergency Activation Committee Contract /// @notice This contract allows a committee to approve and execute an emergency activation /// @dev Inherits from HashConsensus to utilize voting and consensus mechanisms diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol index d9bac860..719ca409 100644 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -2,11 +2,14 @@ pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {Durations} from "../types/Duration.sol"; +import {Timestamp} from "../types/Timestamp.sol"; + +import {ITimelock} from "../interfaces/ITimelock.sol"; + import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; -import {ITimelock} from "../interfaces/ITimelock.sol"; -import {Timestamp} from "../types/Timestamp.sol"; -import {Durations} from "../types/Duration.sol"; enum ProposalType { EmergencyExecute, diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index 89990b46..035ac25a 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.26; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + import {Duration} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; diff --git a/contracts/committees/ResealCommittee.sol b/contracts/committees/ResealCommittee.sol index 0c7b3a0e..6f2770ae 100644 --- a/contracts/committees/ResealCommittee.sol +++ b/contracts/committees/ResealCommittee.sol @@ -3,11 +3,13 @@ pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Duration} from "../types/Duration.sol"; +import {Timestamp} from "../types/Timestamp.sol"; + import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; + import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; -import {Timestamp} from "../types/Timestamp.sol"; -import {Duration} from "../types/Duration.sol"; /// @title Reseal Committee Contract /// @notice This contract allows a committee to vote on and execute resealing proposals diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol index a4fbe080..01dabf8f 100644 --- a/contracts/committees/TiebreakerSubCommittee.sol +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -3,11 +3,13 @@ pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {Durations} from "../types/Duration.sol"; +import {Timestamp} from "../types/Timestamp.sol"; + import {ITiebreakerCore} from "../interfaces/ITiebreakerCore.sol"; + import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; -import {Timestamp} from "../types/Timestamp.sol"; -import {Durations} from "../types/Duration.sol"; enum ProposalType { ScheduleProposal, diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index 93bb42f2..cc72849d 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -2,8 +2,8 @@ pragma solidity 0.8.26; import {Duration} from "../types/Duration.sol"; -import {Timestamps, Timestamp} from "../types/Timestamp.sol"; import {ETHValue, ETHValues} from "../types/ETHValue.sol"; +import {Timestamps, Timestamp} from "../types/Timestamp.sol"; import {SharesValue, SharesValues} from "../types/SharesValue.sol"; import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index c82d5425..7a414cb9 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -4,13 +4,13 @@ pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; -import {IEscrow} from "../interfaces/IEscrow.sol"; -import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; - import {Duration} from "../types/Duration.sol"; import {PercentD16} from "../types/PercentD16.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; +import {IEscrow} from "../interfaces/IEscrow.sol"; +import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; + import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; enum State { diff --git a/contracts/libraries/EmergencyProtection.sol b/contracts/libraries/EmergencyProtection.sol index 26cdad63..94470046 100644 --- a/contracts/libraries/EmergencyProtection.sol +++ b/contracts/libraries/EmergencyProtection.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.26; import {Duration, Durations} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; + import {IEmergencyProtectedTimelock} from "../interfaces/IEmergencyProtectedTimelock.sol"; /// @title EmergencyProtection diff --git a/contracts/libraries/ExecutableProposals.sol b/contracts/libraries/ExecutableProposals.sol index cd1a9130..a1ad977d 100644 --- a/contracts/libraries/ExecutableProposals.sol +++ b/contracts/libraries/ExecutableProposals.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {ITimelock} from "../interfaces/ITimelock.sol"; import {Duration} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; +import {ITimelock} from "../interfaces/ITimelock.sol"; + import {ExternalCall, ExternalCalls, IExternalExecutor} from "./ExternalCalls.sol"; /// @dev Describes the lifecycle state of a proposal diff --git a/contracts/libraries/Proposers.sol b/contracts/libraries/Proposers.sol index 33024d51..b4f01198 100644 --- a/contracts/libraries/Proposers.sol +++ b/contracts/libraries/Proposers.sol @@ -19,7 +19,6 @@ library Proposers { // Events // --- - event AdminExecutorSet(address indexed adminExecutor); event ProposerRegistered(address indexed proposer, address indexed executor); event ProposerUnregistered(address indexed proposer, address indexed executor); diff --git a/contracts/libraries/Tiebreaker.sol b/contracts/libraries/Tiebreaker.sol index 68baf864..872bde88 100644 --- a/contracts/libraries/Tiebreaker.sol +++ b/contracts/libraries/Tiebreaker.sol @@ -2,10 +2,13 @@ pragma solidity 0.8.26; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + import {Duration} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Duration.sol"; + import {ISealable} from "../interfaces/ISealable.sol"; import {ITiebreaker} from "../interfaces/ITiebreaker.sol"; + import {SealableCalls} from "./SealableCalls.sol"; import {State as DualGovernanceState} from "./DualGovernanceStateMachine.sol"; diff --git a/contracts/libraries/WithdrawalBatchesQueue.sol b/contracts/libraries/WithdrawalBatchesQueue.sol index 59c2c6ab..de7be009 100644 --- a/contracts/libraries/WithdrawalBatchesQueue.sol +++ b/contracts/libraries/WithdrawalBatchesQueue.sol @@ -23,8 +23,6 @@ library WithdrawalsBatchesQueue { error EmptyBatch(); error InvalidUnstETHIdsSequence(); - error NotAllBatchesClaimed(uint256 total, uint256 claimed); - error InvalidWithdrawalsBatchesQueueState(State actual); error WithdrawalBatchesQueueIsInAbsentState(); error WithdrawalBatchesQueueIsNotInOpenedState(); error WithdrawalBatchesQueueIsNotInAbsentState(); From 6efbee84900c474434cac59dc91c8148929e9b36 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 10 Sep 2024 14:50:46 +0300 Subject: [PATCH 49/86] feat: unit tests for dual governance --- contracts/DualGovernance.sol | 10 +- test/unit/DualGovernance.t.sol | 1289 +++++++++++++++++++++++++++++++- 2 files changed, 1261 insertions(+), 38 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index b5ca36ca..ac2f410b 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -11,7 +11,6 @@ import {IWstETH} from "./interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; import {ITiebreaker} from "./interfaces/ITiebreaker.sol"; -import {IResealManager} from "./interfaces/IResealManager.sol"; import {Proposers} from "./libraries/Proposers.sol"; import {Tiebreaker} from "./libraries/Tiebreaker.sol"; @@ -47,6 +46,7 @@ contract DualGovernance is IDualGovernance { event CancelAllPendingProposalsExecuted(); event EscrowMasterCopyDeployed(address escrowMasterCopy); event ConfigProviderSet(IDualGovernanceConfigProvider newConfigProvider); + event ResealCommitteeSet(address resealCommittee); // --- // Tiebreaker Sanity Check Param Immutables @@ -312,6 +312,8 @@ contract DualGovernance is IDualGovernance { function setResealCommittee(address resealCommittee) external { _checkCallerIsAdminExecutor(); _resealCommittee = resealCommittee; + + emit ResealCommitteeSet(resealCommittee); } // --- @@ -319,14 +321,10 @@ contract DualGovernance is IDualGovernance { // --- function _setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) internal { - if (address(newConfigProvider) == address(0)) { + if (address(newConfigProvider) == address(0) || newConfigProvider == _configProvider) { revert InvalidConfigProvider(newConfigProvider); } - if (newConfigProvider == _configProvider) { - return; - } - _configProvider = IDualGovernanceConfigProvider(newConfigProvider); emit ConfigProviderSet(newConfigProvider); } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 3d4fad4e..dbecf5fa 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -1,14 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {Durations} from "contracts/types/Duration.sol"; -import {PercentsD16} from "contracts/types/PercentD16.sol"; +import {Duration, Durations, lte} from "contracts/types/Duration.sol"; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; +import {PercentsD16, PercentD16} from "contracts/types/PercentD16.sol"; import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; import {Escrow} from "contracts/Escrow.sol"; import {Executor} from "contracts/Executor.sol"; import {DualGovernance, State} from "contracts/DualGovernance.sol"; +import {Tiebreaker} from "contracts/libraries/Tiebreaker.sol"; +import {Status as ProposalStatus} from "contracts/libraries/ExecutableProposals.sol"; +import {Proposers} from "contracts/libraries/Proposers.sol"; import {IResealManager} from "contracts/interfaces/IResealManager.sol"; import { DualGovernanceConfig, @@ -16,17 +20,24 @@ import { ImmutableDualGovernanceConfigProvider } from "contracts/DualGovernanceConfigProvider.sol"; +import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; import {IWstETH} from "contracts/interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {ITimelock} from "contracts/interfaces/ITimelock.sol"; +import {ISealable} from "contracts/interfaces/ISealable.sol"; +import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; import {UnitTest} from "test/utils/unit-test.sol"; import {StETHMock} from "test/mocks/StETHMock.sol"; import {TimelockMock} from "test/mocks/TimelockMock.sol"; import {WithdrawalQueueMock} from "test/mocks/WithdrawalQueueMock.sol"; +import {SealableMock} from "test/mocks/SealableMock.sol"; contract DualGovernanceUnitTests is UnitTest { Executor private _executor = new Executor(address(this)); + address private vetoer = makeAddr("vetoer"); + StETHMock private immutable _STETH_MOCK = new StETHMock(); IWithdrawalQueue private immutable _WITHDRAWAL_QUEUE_MOCK = new WithdrawalQueueMock(); @@ -72,12 +83,158 @@ contract DualGovernanceUnitTests is UnitTest { }) }); + Escrow internal _escrow; + function setUp() external { _executor.execute( address(_dualGovernance), 0, abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(this), address(_executor)) ); + + _escrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); + _STETH_MOCK.mint(vetoer, 10 ether); + vm.prank(vetoer); + _STETH_MOCK.approve(address(_escrow), 10 ether); + } + + // --- + // submitProposal() + // --- + + function test_submitProposal_HappyPath() external { + ExternalCall[] memory calls = _generateExternalCalls(); + Proposers.Proposer memory proposer = _dualGovernance.getProposer(address(this)); + vm.expectCall( + address(_timelock), 0, abi.encodeWithSelector(TimelockMock.submit.selector, proposer.executor, calls, "") + ); + + uint256 proposalId = _dualGovernance.submitProposal(calls, ""); + uint256[] memory submittedProposals = _timelock.getSubmittedProposals(); + + assertEq(submittedProposals.length, 1); + assertEq(submittedProposals[0], proposalId); + assertEq(_timelock.getProposalsCount(), 1); + } + + function test_submitProposal_ActivatesNextStateOnSubmit() external { + vm.prank(vetoer); + _escrow.lockStETH(5 ether); + + State currentStateBefore = _dualGovernance.getState(); + + assertEq(currentStateBefore, State.VetoSignalling); + _wait(_configProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + assertEq(currentStateBefore, _dualGovernance.getState()); + + _dualGovernance.submitProposal(_generateExternalCalls(), ""); + + State currentStateAfter = _dualGovernance.getState(); + assertEq(currentStateAfter, State.RageQuit); + assert(currentStateBefore != currentStateAfter); + } + + function test_submitProposal_RevertOn_NotInNormalState() external { + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + + assertEq(_dualGovernance.getState(), State.VetoSignalling); + _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); + _escrow.unlockStETH(); + + assertEq(_dualGovernance.getState(), State.VetoSignallingDeactivation); + + vm.expectRevert(abi.encodeWithSelector(DualGovernance.ProposalSubmissionBlocked.selector)); + _dualGovernance.submitProposal(_generateExternalCalls(), ""); + } + + // --- + // scheduleProposal() + // --- + + function test_scheduleProposal_HappyPath() external { + uint256 proposalId = _dualGovernance.submitProposal(_generateExternalCalls(), ""); + Timestamp submittedAt = Timestamps.now(); + + _wait(_configProvider.DYNAMIC_TIMELOCK_MIN_DURATION()); + _timelock.setSchedule(proposalId); + + vm.mockCall( + address(_timelock), + abi.encodeWithSelector(TimelockMock.getProposalDetails.selector, proposalId), + abi.encode( + ITimelock.ProposalDetails({ + id: proposalId, + status: ProposalStatus.Submitted, + executor: address(0), + submittedAt: submittedAt, + scheduledAt: Timestamps.from(0) + }) + ) + ); + vm.expectCall(address(_timelock), 0, abi.encodeWithSelector(TimelockMock.schedule.selector, proposalId)); + _dualGovernance.scheduleProposal(proposalId); + + uint256[] memory scheduledProposals = _timelock.getScheduledProposals(); + assertEq(scheduledProposals.length, 1); + assertEq(scheduledProposals[0], proposalId); + } + + function test_scheduleProposal_ActivatesNextState() external { + uint256 proposalId = _dualGovernance.submitProposal(_generateExternalCalls(), ""); + Timestamp submittedAt = Timestamps.now(); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + _wait(_configProvider.DYNAMIC_TIMELOCK_MIN_DURATION()); + _escrow.unlockStETH(); + vm.stopPrank(); + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + _timelock.setSchedule(proposalId); + + vm.mockCall( + address(_timelock), + abi.encodeWithSelector(TimelockMock.getProposalDetails.selector, proposalId), + abi.encode( + ITimelock.ProposalDetails({ + id: proposalId, + status: ProposalStatus.Submitted, + executor: address(_executor), + submittedAt: submittedAt, + scheduledAt: Timestamps.from(0) + }) + ) + ); + + assertEq(_dualGovernance.getState(), State.VetoSignallingDeactivation); + _dualGovernance.scheduleProposal(proposalId); + assertEq(_dualGovernance.getState(), State.VetoCooldown); + } + + function test_scheduleProposal_RevertOn_CannotSchedule() external { + uint256 proposalId = _dualGovernance.submitProposal(_generateExternalCalls(), ""); + Timestamp submittedAt = Timestamps.now(); + + vm.prank(vetoer); + _escrow.lockStETH(5 ether); + + vm.mockCall( + address(_timelock), + abi.encodeWithSelector(TimelockMock.getProposalDetails.selector, proposalId), + abi.encode( + ITimelock.ProposalDetails({ + id: proposalId, + status: ProposalStatus.Submitted, + executor: address(_executor), + submittedAt: submittedAt, + scheduledAt: Timestamps.from(0) + }) + ) + ); + + vm.expectRevert(abi.encodeWithSelector(DualGovernance.ProposalSchedulingBlocked.selector, proposalId)); + _dualGovernance.scheduleProposal(proposalId); } // --- @@ -107,11 +264,7 @@ contract DualGovernanceUnitTests is UnitTest { Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); - address vetoer = makeAddr("VETOER"); - _STETH_MOCK.mint(vetoer, 10 ether); - vm.startPrank(vetoer); - _STETH_MOCK.approve(address(signallingEscrow), 10 ether); signallingEscrow.lockStETH(5 ether); vm.stopPrank(); @@ -144,14 +297,8 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(_timelock.lastCancelledProposalId(), 0); assertEq(_dualGovernance.getState(), State.Normal); - Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); - - address vetoer = makeAddr("VETOER"); - _STETH_MOCK.mint(vetoer, 10 ether); - vm.startPrank(vetoer); - _STETH_MOCK.approve(address(signallingEscrow), 10 ether); - signallingEscrow.lockStETH(5 ether); + _escrow.lockStETH(5 ether); vm.stopPrank(); assertEq(_dualGovernance.getState(), State.VetoSignalling); @@ -176,14 +323,8 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(_timelock.lastCancelledProposalId(), 0); assertEq(_dualGovernance.getState(), State.Normal); - Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); - - address vetoer = makeAddr("VETOER"); - _STETH_MOCK.mint(vetoer, 10 ether); - vm.startPrank(vetoer); - _STETH_MOCK.approve(address(signallingEscrow), 10 ether); - signallingEscrow.lockStETH(5 ether); + _escrow.lockStETH(5 ether); vm.stopPrank(); assertEq(_dualGovernance.getState(), State.VetoSignalling); @@ -203,14 +344,8 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(_timelock.lastCancelledProposalId(), 0); assertEq(_dualGovernance.getState(), State.Normal); - Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); - - address vetoer = makeAddr("VETOER"); - _STETH_MOCK.mint(vetoer, 10 ether); - vm.startPrank(vetoer); - _STETH_MOCK.approve(address(signallingEscrow), 10 ether); - signallingEscrow.lockStETH(5 ether); + _escrow.lockStETH(5 ether); vm.stopPrank(); assertEq(_dualGovernance.getState(), State.VetoSignalling); @@ -218,7 +353,7 @@ contract DualGovernanceUnitTests is UnitTest { _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); vm.prank(vetoer); - signallingEscrow.unlockStETH(); + _escrow.unlockStETH(); assertEq(_dualGovernance.getState(), State.VetoSignallingDeactivation); @@ -231,12 +366,1102 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(_timelock.lastCancelledProposalId(), 1); } + function test_cancelAllPendingProposals_RevertOn_NotAdminProposer() external { + address nonAdminProposer = makeAddr("NON_ADMIN_PROPOSER"); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.registerProposer.selector, nonAdminProposer, address(0x123)) + ); + _submitMockProposal(); + + assertEq(_timelock.getProposalsCount(), 1); + + vm.prank(nonAdminProposer); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.NotAdminProposer.selector)); + _dualGovernance.cancelAllPendingProposals(); + + assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.lastCancelledProposalId(), 0); + } + // --- - // Helper methods + // canSubmitProposal() // --- - function _submitMockProposal() internal { - // mock timelock doesn't uses proposal data - _timelock.submit(address(0), new ExternalCall[](0), ""); + function test_canSubmitProposal_HappyPath() external { + assertTrue(_dualGovernance.canSubmitProposal()); + assertEq(_dualGovernance.getState(), State.Normal); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + + _dualGovernance.activateNextState(); + assertTrue(_dualGovernance.canSubmitProposal()); + assertEq(_dualGovernance.getState(), State.VetoSignalling); + + _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); + + _escrow.unlockStETH(); + _dualGovernance.activateNextState(); + + assertFalse(_dualGovernance.canSubmitProposal()); + assertEq(_dualGovernance.getState(), State.VetoSignallingDeactivation); + + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + _dualGovernance.activateNextState(); + + assertFalse(_dualGovernance.canSubmitProposal()); + assertEq(_dualGovernance.getState(), State.VetoCooldown); + + _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); + + _dualGovernance.activateNextState(); + + assertTrue(_dualGovernance.canSubmitProposal()); + assertEq(_dualGovernance.getState(), State.Normal); + + _escrow.lockStETH(5 ether); + _wait(_configProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _dualGovernance.activateNextState(); + + assertTrue(_dualGovernance.canSubmitProposal()); + assertEq(_dualGovernance.getState(), State.RageQuit); + } + + // --- + // canScheduleProposal() + // --- + + function test_canScheduleProposal_HappyPath() external { + uint256 proposalId = 1; + Timestamp submittedAt = Timestamps.now(); + + vm.mockCall( + address(_timelock), + abi.encodeWithSelector(TimelockMock.getProposalDetails.selector, proposalId), + abi.encode( + ITimelock.ProposalDetails({ + id: proposalId, + status: ProposalStatus.Submitted, + executor: address(_executor), + submittedAt: submittedAt, + scheduledAt: Timestamps.from(0) + }) + ) + ); + + vm.mockCall( + address(_timelock), abi.encodeWithSelector(TimelockMock.canSchedule.selector, proposalId), abi.encode(true) + ); + + _wait(_configProvider.DYNAMIC_TIMELOCK_MIN_DURATION()); + + assertTrue(_dualGovernance.canScheduleProposal(proposalId)); + } + + function test_canScheduleProposal_WhenTimelockCannotSchedule() external { + uint256 proposalId = 1; + Timestamp submittedAt = Timestamps.now(); + + vm.mockCall( + address(_timelock), + abi.encodeWithSelector(TimelockMock.getProposalDetails.selector, proposalId), + abi.encode( + ITimelock.ProposalDetails({ + id: proposalId, + status: ProposalStatus.Submitted, + executor: address(_executor), + submittedAt: submittedAt, + scheduledAt: Timestamps.from(0) + }) + ) + ); + + vm.mockCall( + address(_timelock), abi.encodeWithSelector(TimelockMock.canSchedule.selector, proposalId), abi.encode(false) + ); + + _wait(_configProvider.DYNAMIC_TIMELOCK_MIN_DURATION()); + + bool canSchedule = _dualGovernance.canScheduleProposal(proposalId); + assertFalse(canSchedule); + } + + function test_canScheduleProposal_WhenNotEnoughTimeElapsed() external { + uint256 proposalId = 1; + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); + _escrow.unlockStETH(); + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getState(), State.VetoCooldown); + + vm.mockCall( + address(_timelock), + abi.encodeWithSelector(TimelockMock.getProposalDetails.selector, proposalId), + abi.encode( + ITimelock.ProposalDetails({ + id: proposalId, + status: ProposalStatus.Submitted, + executor: address(_executor), + submittedAt: Timestamps.now(), + scheduledAt: Timestamps.from(0) + }) + ) + ); + + vm.mockCall( + address(_timelock), abi.encodeWithSelector(TimelockMock.canSchedule.selector, proposalId), abi.encode(true) + ); + assertFalse(_dualGovernance.canScheduleProposal(proposalId)); + } + + // --- + // activateNextState() & getState() + // --- + + function test_activateNextState_getState_HappyPath() external { + assertEq(_dualGovernance.getState(), State.Normal); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getState(), State.VetoSignalling); + + _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); + _escrow.unlockStETH(); + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getState(), State.VetoSignallingDeactivation); + + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getState(), State.VetoCooldown); + + _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getState(), State.Normal); + + _escrow.lockStETH(5 ether); + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getState(), State.VetoSignalling); + + _wait(_configProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getState(), State.RageQuit); + } + + // --- + // setConfigProvider() + // --- + + function test_setConfigProvider() external { + ImmutableDualGovernanceConfigProvider newConfigProvider = new ImmutableDualGovernanceConfigProvider( + DualGovernanceConfig.Context({ + firstSealRageQuitSupport: PercentsD16.fromBasisPoints(5_00), // 5% + secondSealRageQuitSupport: PercentsD16.fromBasisPoints(20_00), // 20% + minAssetsLockDuration: Durations.from(6 hours), + dynamicTimelockMinDuration: Durations.from(4 days), + dynamicTimelockMaxDuration: Durations.from(35 days), + vetoSignallingMinActiveDuration: Durations.from(6 hours), + vetoSignallingDeactivationMaxDuration: Durations.from(6 days), + vetoCooldownDuration: Durations.from(5 days), + rageQuitExtensionDelay: Durations.from(8 days), + rageQuitEthWithdrawalsMinTimelock: Durations.from(65 days), + rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: 3, + rageQuitEthWithdrawalsTimelockGrowthCoeffs: [uint256(0), 0, 0] + }) + ); + + IDualGovernanceConfigProvider oldConfigProvider = _dualGovernance.getConfigProvider(); + + vm.expectEmit(); + emit DualGovernance.ConfigProviderSet(IDualGovernanceConfigProvider(address(newConfigProvider))); + + vm.expectCall( + address(_escrow), + 0, + abi.encodeWithSelector(Escrow.setMinAssetsLockDuration.selector, Durations.from(6 hours)) + ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setConfigProvider.selector, address(newConfigProvider)) + ); + + assertEq(address(_dualGovernance.getConfigProvider()), address(newConfigProvider)); + assertTrue(address(_dualGovernance.getConfigProvider()) != address(oldConfigProvider)); + } + + function testFuzz_setConfigProvider_RevertOn_NotAdminExecutor(address stranger) external { + vm.assume(stranger != address(_executor)); + ImmutableDualGovernanceConfigProvider newConfigProvider = new ImmutableDualGovernanceConfigProvider( + DualGovernanceConfig.Context({ + firstSealRageQuitSupport: PercentsD16.fromBasisPoints(5_00), // 5% + secondSealRageQuitSupport: PercentsD16.fromBasisPoints(20_00), // 20% + minAssetsLockDuration: Durations.from(6 hours), + dynamicTimelockMinDuration: Durations.from(4 days), + dynamicTimelockMaxDuration: Durations.from(35 days), + vetoSignallingMinActiveDuration: Durations.from(6 hours), + vetoSignallingDeactivationMaxDuration: Durations.from(6 days), + vetoCooldownDuration: Durations.from(5 days), + rageQuitExtensionDelay: Durations.from(8 days), + rageQuitEthWithdrawalsMinTimelock: Durations.from(65 days), + rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: 3, + rageQuitEthWithdrawalsTimelockGrowthCoeffs: [uint256(0), 0, 0] + }) + ); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, stranger)); + _dualGovernance.setConfigProvider(newConfigProvider); + + assertEq(address(_dualGovernance.getConfigProvider()), address(_configProvider)); + } + + function test_setConfigProvider_RevertOn_ConfigZeroAddress() external { + vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidConfigProvider.selector, address(0))); + _executor.execute( + address(_dualGovernance), 0, abi.encodeWithSelector(DualGovernance.setConfigProvider.selector, address(0)) + ); + } + + function test_setConfigProvider_RevertOn_SameAddress() external { + vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidConfigProvider.selector, address(_configProvider))); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setConfigProvider.selector, address(_configProvider)) + ); + } + + // --- + // getConfigProvider() + // --- + + function test_getConfigProvider_HappyPath() external { + assertEq(address(_dualGovernance.getConfigProvider()), address(_configProvider)); + + ImmutableDualGovernanceConfigProvider newConfigProvider = new ImmutableDualGovernanceConfigProvider( + DualGovernanceConfig.Context({ + firstSealRageQuitSupport: PercentsD16.fromBasisPoints(5_00), // 5% + secondSealRageQuitSupport: PercentsD16.fromBasisPoints(20_00), // 20% + minAssetsLockDuration: Durations.from(6 hours), + dynamicTimelockMinDuration: Durations.from(4 days), + dynamicTimelockMaxDuration: Durations.from(35 days), + vetoSignallingMinActiveDuration: Durations.from(6 hours), + vetoSignallingDeactivationMaxDuration: Durations.from(6 days), + vetoCooldownDuration: Durations.from(5 days), + rageQuitExtensionDelay: Durations.from(8 days), + rageQuitEthWithdrawalsMinTimelock: Durations.from(65 days), + rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: 3, + rageQuitEthWithdrawalsTimelockGrowthCoeffs: [uint256(0), 0, 0] + }) + ); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setConfigProvider.selector, address(newConfigProvider)) + ); + + assertEq(address(_dualGovernance.getConfigProvider()), address(newConfigProvider)); + assertTrue(address(_dualGovernance.getConfigProvider()) != address(_configProvider)); + } + + // --- + // getVetoSignallingEscrow() + // --- + + function test_getVetoSignallingEscrow_HappyPath() external { + assertEq(_dualGovernance.getVetoSignallingEscrow(), address(_escrow)); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + + _wait(_configProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getState(), State.RageQuit); + + assertTrue(_dualGovernance.getVetoSignallingEscrow() != address(_escrow)); + } + + // --- + // getRageQuitEscrow() + // --- + + function test_getRageQuitEscrow_HappyPath() external { + assertEq(_dualGovernance.getRageQuitEscrow(), address(0)); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + + _wait(_configProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getState(), State.RageQuit); + + assertEq(_dualGovernance.getRageQuitEscrow(), address(_escrow)); + } + + // --- + // getStateDetails() + // --- + + function test_getStateDetails_HappyPath() external { + Timestamp startTime = Timestamps.now(); + + IDualGovernance.StateDetails memory details = _dualGovernance.getStateDetails(); + assertEq(details.state, State.Normal); + assertEq(details.enteredAt, startTime); + assertEq(details.vetoSignallingActivatedAt, Timestamps.from(0)); + assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); + assertEq(details.normalOrVetoCooldownExitedAt, Timestamps.from(0)); + assertEq(details.rageQuitRound, 0); + assertEq(details.dynamicDelay, Durations.from(0)); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + vm.stopPrank(); + Timestamp vetoSignallingTime = Timestamps.now(); + _dualGovernance.activateNextState(); + + details = _dualGovernance.getStateDetails(); + assertEq(details.state, State.VetoSignalling); + assertEq(details.enteredAt, vetoSignallingTime); + assertEq(details.vetoSignallingActivatedAt, vetoSignallingTime); + assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); + assertEq(details.normalOrVetoCooldownExitedAt, vetoSignallingTime); + assertEq(details.rageQuitRound, 0); + assertTrue(details.dynamicDelay > _configProvider.DYNAMIC_TIMELOCK_MIN_DURATION()); + + _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); + vm.prank(vetoer); + _escrow.unlockStETH(); + Timestamp deactivationTime = Timestamps.now(); + _dualGovernance.activateNextState(); + + details = _dualGovernance.getStateDetails(); + assertEq(details.state, State.VetoSignallingDeactivation); + assertEq(details.enteredAt, deactivationTime); + assertEq(details.vetoSignallingActivatedAt, vetoSignallingTime); + assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); + assertEq(details.normalOrVetoCooldownExitedAt, vetoSignallingTime); + assertEq(details.rageQuitRound, 0); + assertEq(details.dynamicDelay, Durations.from(0)); + + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + Timestamp vetoCooldownTime = Timestamps.now(); + _dualGovernance.activateNextState(); + + details = _dualGovernance.getStateDetails(); + assertEq(details.state, State.VetoCooldown); + assertEq(details.enteredAt, vetoCooldownTime); + assertEq(details.vetoSignallingActivatedAt, vetoSignallingTime); + assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); + assertEq(details.normalOrVetoCooldownExitedAt, vetoSignallingTime); + assertEq(details.rageQuitRound, 0); + assertEq(details.dynamicDelay, Durations.from(0)); + + _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); + Timestamp backToNormalTime = Timestamps.now(); + _dualGovernance.activateNextState(); + + details = _dualGovernance.getStateDetails(); + assertEq(details.state, State.Normal); + assertEq(details.enteredAt, backToNormalTime); + assertEq(details.vetoSignallingActivatedAt, vetoSignallingTime); + assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); + assertEq(details.normalOrVetoCooldownExitedAt, backToNormalTime); + assertEq(details.rageQuitRound, 0); + assertEq(details.dynamicDelay, Durations.from(0)); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + Timestamp secondVetoSignallingTime = Timestamps.now(); + _dualGovernance.activateNextState(); + _wait(_configProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + Timestamp rageQuitTime = Timestamps.now(); + _dualGovernance.activateNextState(); + vm.stopPrank(); + + details = _dualGovernance.getStateDetails(); + assertEq(details.state, State.RageQuit); + assertEq(details.enteredAt, rageQuitTime); + assertEq(details.vetoSignallingActivatedAt, secondVetoSignallingTime); + assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); + assertEq(details.normalOrVetoCooldownExitedAt, backToNormalTime); + assertEq(details.rageQuitRound, 1); + assertEq(details.dynamicDelay, Durations.from(0)); + } + + // --- + // registerProposer() + // --- + + function test_registerProposer_HappyPath() external { + address newProposer = makeAddr("NEW_PROPOSER"); + address newExecutor = makeAddr("NEW_EXECUTOR"); + + assertFalse(_dualGovernance.isProposer(newProposer)); + assertFalse(_dualGovernance.isExecutor(newExecutor)); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.registerProposer.selector, newProposer, newExecutor) + ); + + assertTrue(_dualGovernance.isProposer(newProposer)); + assertTrue(_dualGovernance.isExecutor(newExecutor)); + + Proposers.Proposer memory proposer = _dualGovernance.getProposer(newProposer); + assertEq(proposer.account, newProposer); + assertEq(proposer.executor, newExecutor); + } + + function testFuzz_registerProposer_RevertOn_NotAdminExecutor(address stranger) external { + vm.assume(stranger != address(_executor)); + address newProposer = makeAddr("NEW_PROPOSER"); + address newExecutor = makeAddr("NEW_EXECUTOR"); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, stranger)); + _dualGovernance.registerProposer(newProposer, newExecutor); + } + + // --- + // unregisterProposer() + // --- + + function test_unregisterProposer_HappyPath() external { + address proposer = makeAddr("PROPOSER"); + address proposerExecutor = makeAddr("PROPOSER_EXECUTOR"); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) + ); + + assertTrue(_dualGovernance.isProposer(proposer)); + assertTrue(_dualGovernance.isExecutor(proposerExecutor)); + + _executor.execute( + address(_dualGovernance), 0, abi.encodeWithSelector(DualGovernance.unregisterProposer.selector, proposer) + ); + + assertFalse(_dualGovernance.isProposer(proposer)); + assertFalse(_dualGovernance.isExecutor(proposerExecutor)); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, proposer)); + _dualGovernance.getProposer(proposer); + } + + function testFuzz_unregisterProposer_RevertOn_NotAdminExecutor(address stranger) external { + vm.assume(stranger != address(_executor)); + address proposer = makeAddr("PROPOSER"); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, stranger)); + _dualGovernance.unregisterProposer(proposer); + } + + function test_unregisterProposer_RevertOn_UnownedAdminExecutor() external { + address proposer = makeAddr("PROPOSER"); + address proposerExecutor = makeAddr("PROPOSER_EXECUTOR"); + address adminExecutor = _timelock.getAdminExecutor(); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) + ); + + vm.expectRevert(abi.encodeWithSelector(DualGovernance.UnownedAdminExecutor.selector)); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.unregisterProposer.selector, address(this)) + ); + + assertTrue(_dualGovernance.isProposer(address(this))); + assertTrue(_dualGovernance.isExecutor(adminExecutor)); + } + + // --- + // isProposer() + // --- + + function test_isProposer_HappyPath() external { + address proposer = makeAddr("PROPOSER"); + address proposerExecutor = makeAddr("PROPOSER_EXECUTOR"); + + assertFalse(_dualGovernance.isProposer(proposer)); + assertFalse(_dualGovernance.isExecutor(proposerExecutor)); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) + ); + + assertTrue(_dualGovernance.isProposer(proposer)); + assertTrue(_dualGovernance.isExecutor(proposerExecutor)); + } + + function testFuzz_isProposer_UnregisteredProposer(address proposer) external { + vm.assume(proposer != address(this)); + + assertFalse(_dualGovernance.isProposer(proposer)); + } + + // --- + // getProposer() + // --- + + function test_getProposer_HappyPath() external { + address proposer = makeAddr("PROPOSER"); + address proposerExecutor = makeAddr("PROPOSER_EXECUTOR"); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) + ); + + Proposers.Proposer memory proposerData = _dualGovernance.getProposer(proposer); + assertEq(proposerData.account, proposer); + assertEq(proposerData.executor, proposerExecutor); + } + + function testFuzz_getProposer_RevertOn_UnregisteredProposer(address proposer) external { + vm.assume(proposer != address(this)); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, proposer)); + _dualGovernance.getProposer(proposer); + } + + // --- + // getProposers() + // --- + + function test_getProposers_HappyPath() external { + address proposer1 = makeAddr("PROPOSER1"); + address proposer2 = makeAddr("PROPOSER2"); + address proposer3 = makeAddr("PROPOSER3"); + address proposerExecutor1 = makeAddr("PROPOSER_EXECUTOR1"); + address proposerExecutor2 = makeAddr("PROPOSER_EXECUTOR2"); + address proposerExecutor3 = makeAddr("PROPOSER_EXECUTOR3"); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer1, proposerExecutor1) + ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer2, proposerExecutor2) + ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer3, proposerExecutor3) + ); + + Proposers.Proposer[] memory proposers = _dualGovernance.getProposers(); + assertEq(proposers.length, 4); + assertEq(proposers[0].account, address(this)); + assertEq(proposers[0].executor, address(_executor)); + assertEq(proposers[1].executor, proposerExecutor1); + assertEq(proposers[1].account, proposer1); + assertEq(proposers[2].executor, proposerExecutor2); + assertEq(proposers[2].account, proposer2); + assertEq(proposers[3].executor, proposerExecutor3); + assertEq(proposers[3].account, proposer3); + } + + // --- + // isExecutor() + // --- + + function test_isExecutor_HappyPath() external { + address executor = makeAddr("EXECUTOR1"); + + assertFalse(_dualGovernance.isExecutor(executor)); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(0x123), executor) + ); + + assertTrue(_dualGovernance.isExecutor(executor)); + assertTrue(_dualGovernance.isExecutor(address(_executor))); + } + + function testFuzz_isExecutor_UnregisteredExecutor(address executor) external { + vm.assume(executor != address(_executor)); + + assertFalse(_dualGovernance.isExecutor(executor)); + } + + // --- + // addTiebreakerSealableWithdrawalBlocker() + // --- + + function test_addTiebreakerSealableWithdrawalBlocker_HappyPath() external { + address blocker = address(new SealableMock()); + + vm.expectEmit(); + emit Tiebreaker.SealableWithdrawalBlockerAdded(blocker); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.addTiebreakerSealableWithdrawalBlocker.selector, blocker) + ); + } + + function testFuzz_addTiebreakerSealableWithdrawalBlocker_RevertOn_NotAdminExecutor(address stranger) external { + vm.assume(stranger != address(_executor)); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, stranger)); + _dualGovernance.addTiebreakerSealableWithdrawalBlocker(address(0x123)); + } + + // --- + // removeTiebreakerSealableWithdrawalBlocker() + // --- + + function test_removeTiebreakerSealableWithdrawalBlocker_HappyPath() external { + address blocker = address(new SealableMock()); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.addTiebreakerSealableWithdrawalBlocker.selector, blocker) + ); + + vm.expectEmit(); + emit Tiebreaker.SealableWithdrawalBlockerRemoved(blocker); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.removeTiebreakerSealableWithdrawalBlocker.selector, blocker) + ); + } + + function test_removeTiebreakerSealableWithdrawalBlocker_RevertOn_NotAdminExecutor(address stranger) external { + vm.assume(stranger != address(_executor)); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, stranger)); + _dualGovernance.removeTiebreakerSealableWithdrawalBlocker(address(0x123)); + } + + // --- + // setTiebreakerCommittee() + // --- + + function testFuzz_setTiebreakerCommittee_HappyPath(address committee) external { + vm.assume(committee != address(0)); + + vm.expectEmit(); + emit Tiebreaker.TiebreakerCommitteeSet(committee); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerCommittee.selector, committee) + ); + } + + function testFuzz_setTiebreakerCommittee_RevertOn_NotAdminExecutor(address stranger) external { + vm.assume(stranger != address(_executor)); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, stranger)); + _dualGovernance.setTiebreakerCommittee(address(0x123)); + } + + // --- + // setTiebreakerActivationTimeout() + // --- + + function testFuzz_setTiebreakerActivationTimeout_HappyPath(Duration timeout) external { + vm.assume( + lte(_dualGovernance.MIN_TIEBREAKER_ACTIVATION_TIMEOUT(), timeout) + && lte(timeout, _dualGovernance.MAX_TIEBREAKER_ACTIVATION_TIMEOUT()) + ); + vm.expectEmit(); + emit Tiebreaker.TiebreakerActivationTimeoutSet(timeout); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerActivationTimeout.selector, timeout) + ); + } + + function testFuzz_setTiebreakerActivationTimeout_RevertOn_NotAdminExecutor(address stranger) external { + vm.assume(stranger != address(_executor)); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, stranger)); + _dualGovernance.setTiebreakerActivationTimeout(Durations.from(1 days)); + } + + // --- + // tiebreakerResumeSealable() + // --- + + function test_tiebreakerResumeSealable_HappyPath() external { + address sealable = address(new SealableMock()); + address tiebreakerCommittee = makeAddr("TIEBREAKER_COMMITTEE"); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerCommittee.selector, tiebreakerCommittee) + ); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + vm.stopPrank(); + _dualGovernance.activateNextState(); + _wait(_configProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + + vm.mockCall( + address(_RESEAL_MANAGER_STUB), + abi.encodeWithSelector(IResealManager.resume.selector, sealable), + abi.encode() + ); + vm.expectCall(address(_RESEAL_MANAGER_STUB), abi.encodeWithSelector(IResealManager.resume.selector, sealable)); + vm.prank(tiebreakerCommittee); + _dualGovernance.tiebreakerResumeSealable(sealable); + } + + function testFuzz_tiebreakerResumeSealable_RevertOn_NotTiebreakerCommittee(address stranger) external { + vm.assume(stranger != makeAddr("TIEBREAKER_COMMITTEE")); + address sealable = address(new SealableMock()); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerCommittee.selector, makeAddr("TIEBREAKER_COMMITTEE")) + ); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + vm.stopPrank(); + _dualGovernance.activateNextState(); + _wait(_configProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(Tiebreaker.CallerIsNotTiebreakerCommittee.selector, stranger)); + _dualGovernance.tiebreakerResumeSealable(sealable); + } + + function test_tiebreakerResumeSealable_ActivatesNextState() external { + address sealable = address(new SealableMock()); + address tiebreakerCommittee = makeAddr("TIEBREAKER_COMMITTEE"); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerCommittee.selector, tiebreakerCommittee) + ); + + vm.startPrank(vetoer); + _escrow.lockStETH(1 ether); + vm.stopPrank(); + _dualGovernance.activateNextState(); + _wait(_configProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getState(), State.VetoSignalling); + + vm.mockCall( + address(_RESEAL_MANAGER_STUB), + abi.encodeWithSelector(IResealManager.resume.selector, sealable), + abi.encode() + ); + + vm.prank(tiebreakerCommittee); + _dualGovernance.tiebreakerResumeSealable(sealable); + + assertEq(_dualGovernance.getState(), State.VetoSignallingDeactivation); + IDualGovernance.StateDetails memory stateDetails = _dualGovernance.getStateDetails(); + assertEq(stateDetails.enteredAt, Timestamps.now()); + } + + // --- + // tiebreakerScheduleProposal() + // --- + + function test_tiebreakerScheduleProposal_HappyPath() external { + address tiebreakerCommittee = makeAddr("TIEBREAKER_COMMITTEE"); + + _submitMockProposal(); + uint256 proposalId = 1; + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerCommittee.selector, tiebreakerCommittee) + ); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + vm.stopPrank(); + _dualGovernance.activateNextState(); + _wait(_configProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + + vm.mockCall(address(_timelock), abi.encodeWithSelector(ITimelock.schedule.selector, proposalId), abi.encode()); + vm.expectCall(address(_timelock), abi.encodeWithSelector(ITimelock.schedule.selector, proposalId)); + + vm.prank(tiebreakerCommittee); + _dualGovernance.tiebreakerScheduleProposal(proposalId); + } + + function testFuzz_tiebreakerScheduleProposal_RevertOn_NotTiebreakerCommittee(address stranger) external { + vm.assume(stranger != makeAddr("TIEBREAKER_COMMITTEE")); + uint256 proposalId = 1; + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerCommittee.selector, makeAddr("TIEBREAKER_COMMITTEE")) + ); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(Tiebreaker.CallerIsNotTiebreakerCommittee.selector, stranger)); + _dualGovernance.tiebreakerScheduleProposal(proposalId); + } + + function test_tiebreakerScheduleProposal_ActivatesNextState() external { + address tiebreakerCommittee = makeAddr("TIEBREAKER_COMMITTEE"); + _submitMockProposal(); + uint256 proposalId = 1; + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerCommittee.selector, tiebreakerCommittee) + ); + + vm.startPrank(vetoer); + _escrow.lockStETH(1 ether); + vm.stopPrank(); + _dualGovernance.activateNextState(); + assertEq(uint256(_dualGovernance.getState()), uint256(State.VetoSignalling)); + + _wait(_configProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + + vm.expectCall(address(_timelock), abi.encodeWithSelector(ITimelock.schedule.selector, proposalId)); + vm.mockCall(address(_timelock), abi.encodeWithSelector(ITimelock.schedule.selector, proposalId), abi.encode()); + vm.prank(tiebreakerCommittee); + _dualGovernance.tiebreakerScheduleProposal(proposalId); + + assertEq(uint256(_dualGovernance.getState()), uint256(State.VetoSignallingDeactivation)); + + IDualGovernance.StateDetails memory stateDetails = _dualGovernance.getStateDetails(); + assertEq(stateDetails.enteredAt, Timestamps.now()); + } + + // --- + // getTiebreakerDetails() + // --- + + function test_getTiebreakerDetails_HappyPath() external { + ITiebreaker.TiebreakerDetails memory details = _dualGovernance.getTiebreakerDetails(); + + assertEq(details.tiebreakerCommittee, address(0)); + assertEq(details.tiebreakerActivationTimeout, Durations.from(0)); + assertFalse(details.isTie); + assertEq(details.sealableWithdrawalBlockers.length, 0); + + address tiebreakerCommittee = makeAddr("TIEBREAKER_COMMITTEE"); + address sealable1 = address(new SealableMock()); + address sealable2 = address(new SealableMock()); + Duration newTimeout = _dualGovernance.MIN_TIEBREAKER_ACTIVATION_TIMEOUT().plusSeconds(1); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerCommittee.selector, tiebreakerCommittee) + ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerActivationTimeout.selector, newTimeout) + ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.addTiebreakerSealableWithdrawalBlocker.selector, sealable1) + ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.addTiebreakerSealableWithdrawalBlocker.selector, sealable2) + ); + + details = _dualGovernance.getTiebreakerDetails(); + + assertEq(details.tiebreakerCommittee, tiebreakerCommittee); + assertEq(details.tiebreakerActivationTimeout, newTimeout); + assertFalse(details.isTie); + assertEq(details.sealableWithdrawalBlockers.length, 2); + assertTrue( + details.sealableWithdrawalBlockers[0] == sealable1 || details.sealableWithdrawalBlockers[1] == sealable1 + ); + assertTrue( + details.sealableWithdrawalBlockers[0] == sealable2 || details.sealableWithdrawalBlockers[1] == sealable2 + ); + } + + function test_getTiebreakerDetails_TieCondition() external { + address tiebreakerCommittee = makeAddr("TIEBREAKER_COMMITTEE"); + address sealable = address(new SealableMock()); + Duration newTimeout = _dualGovernance.MIN_TIEBREAKER_ACTIVATION_TIMEOUT().plusSeconds(1); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerCommittee.selector, tiebreakerCommittee) + ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerActivationTimeout.selector, newTimeout) + ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.addTiebreakerSealableWithdrawalBlocker.selector, sealable) + ); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + vm.stopPrank(); + _dualGovernance.activateNextState(); + assertEq(uint256(_dualGovernance.getState()), uint256(State.VetoSignalling)); + + _wait(newTimeout.plusSeconds(1)); + + ITiebreaker.TiebreakerDetails memory details = _dualGovernance.getTiebreakerDetails(); + + assertEq(details.tiebreakerCommittee, tiebreakerCommittee); + assertEq(details.tiebreakerActivationTimeout, newTimeout); + assertTrue(details.isTie); + assertEq(details.sealableWithdrawalBlockers.length, 1); + assertEq(details.sealableWithdrawalBlockers[0], sealable); + } + + // --- + // resealSealable() + // --- + + function test_resealSealable_HappyPath() external { + address resealCommittee = makeAddr("RESEAL_COMMITTEE"); + address sealable = address(new SealableMock()); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, resealCommittee) + ); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + vm.stopPrank(); + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getState(), State.VetoSignalling); + + vm.mockCall( + address(_RESEAL_MANAGER_STUB), + abi.encodeWithSelector(IResealManager.reseal.selector, sealable), + abi.encode() + ); + vm.expectCall(address(_RESEAL_MANAGER_STUB), abi.encodeWithSelector(IResealManager.reseal.selector, sealable)); + vm.prank(resealCommittee); + _dualGovernance.resealSealable(sealable); + } + + function test_resealSealable_RevertOn_NotResealCommittee() external { + address notResealCommittee = makeAddr("NOT_RESEAL_COMMITTEE"); + address sealable = address(new SealableMock()); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, makeAddr("RESEAL_COMMITTEE")) + ); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + vm.stopPrank(); + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getState(), State.VetoSignalling); + + vm.prank(notResealCommittee); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotResealCommittee.selector, notResealCommittee)); + _dualGovernance.resealSealable(sealable); + } + + function test_resealSealable_RevertOn_NormalState() external { + address resealCommittee = makeAddr("RESEAL_COMMITTEE"); + address sealable = address(new SealableMock()); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, resealCommittee) + ); + + assertEq(_dualGovernance.getState(), State.Normal); + + vm.prank(resealCommittee); + vm.expectRevert(DualGovernance.ResealIsNotAllowedInNormalState.selector); + _dualGovernance.resealSealable(sealable); + } + + // --- + // setResealCommittee() + // --- + + function testFuzz_setResealCommittee_HappyPath(address newResealCommittee) external { + vm.expectEmit(); + emit DualGovernance.ResealCommitteeSet(newResealCommittee); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, newResealCommittee) + ); + } + + function testFuzz_setResealCommittee_RevertOn_NotAdminExecutor(address stranger) external { + vm.assume(stranger != address(_executor)); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, stranger)); + _dualGovernance.setResealCommittee(makeAddr("NEW_RESEAL_COMMITTEE")); + } + + // --- + // Helper methods + // --- + + function _submitMockProposal() internal { + // mock timelock doesn't uses proposal data + _timelock.submit(address(0), new ExternalCall[](0), ""); + } + + function _generateExternalCalls() internal pure returns (ExternalCall[] memory calls) { + calls = new ExternalCall[](1); + calls[0] = ExternalCall({target: address(0x123), value: 0, payload: abi.encodeWithSignature("someFunction()")}); } } From f93138755c0be6c90634981718003b29a0b932b6 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 10 Sep 2024 18:54:07 +0300 Subject: [PATCH 50/86] fix: tiebreaker core add missing assume --- test/unit/committees/TiebreakerCore.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/committees/TiebreakerCore.t.sol b/test/unit/committees/TiebreakerCore.t.sol index 5ce8c285..d6b5c61f 100644 --- a/test/unit/committees/TiebreakerCore.t.sol +++ b/test/unit/committees/TiebreakerCore.t.sol @@ -29,6 +29,7 @@ contract TiebreakerCoreUnitTest is UnitTest { } function testFuzz_constructor_HappyPath(address _owner, address _dualGovernance, Duration _timelock) external { + vm.assume(_owner != address(0)); new TiebreakerCore(_owner, _dualGovernance, _timelock); } From 2b72f38a425e8acee1f349d75ceda76ff2866f7d Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 10 Sep 2024 23:40:32 +0400 Subject: [PATCH 51/86] Optimize gas on work with enumerable sets --- contracts/committees/HashConsensus.sol | 34 +++++++++++++-------- contracts/libraries/EnumerableProposals.sol | 3 +- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index 89990b46..46a72a45 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -127,19 +127,11 @@ abstract contract HashConsensus is Ownable { /// function will revert. The quorum is also updated and must not be zero or greater than /// the new total number of members. /// @param membersToRemove The array of addresses to be removed from the members list. - /// @param newQuorum The updated minimum number of members required for executing certain operations. - function removeMembers(address[] memory membersToRemove, uint256 newQuorum) public { + /// @param executionQuorum The updated minimum number of members required for executing certain operations. + function removeMembers(address[] memory membersToRemove, uint256 executionQuorum) public { _checkOwner(); - for (uint256 i = 0; i < membersToRemove.length; ++i) { - if (!_members.contains(membersToRemove[i])) { - revert AccountIsNotMember(membersToRemove[i]); - } - _members.remove(membersToRemove[i]); - emit MemberRemoved(membersToRemove[i]); - } - - _setQuorum(newQuorum); + _removeMembers(membersToRemove, executionQuorum); } /// @notice Gets the list of committee members @@ -216,16 +208,32 @@ abstract contract HashConsensus is Ownable { /// @param executionQuorum The minimum number of members required for executing certain operations. function _addMembers(address[] memory newMembers, uint256 executionQuorum) internal { for (uint256 i = 0; i < newMembers.length; ++i) { - if (_members.contains(newMembers[i])) { + if (!_members.add(newMembers[i])) { revert DuplicatedMember(newMembers[i]); } - _members.add(newMembers[i]); emit MemberAdded(newMembers[i]); } _setQuorum(executionQuorum); } + /// @notice Removes specified members from the contract and updates the execution quorum. + /// @dev This internal function removes multiple members from the contract. If any of the specified members are not + /// found in the members list, the function will revert. The quorum is also updated and must not be zero or + /// greater than the new total number of members. + /// @param membersToRemove The array of addresses to be removed from the members list. + /// @param executionQuorum The updated minimum number of members required for executing certain operations. + function _removeMembers(address[] memory membersToRemove, uint256 executionQuorum) internal { + for (uint256 i = 0; i < membersToRemove.length; ++i) { + if (!_members.remove(membersToRemove[i])) { + revert AccountIsNotMember(membersToRemove[i]); + } + emit MemberRemoved(membersToRemove[i]); + } + + _setQuorum(executionQuorum); + } + /// @notice Gets the number of votes in support of a given hash /// @dev Internal function to count the votes in support of a hash /// @param hash The hash to check diff --git a/contracts/libraries/EnumerableProposals.sol b/contracts/libraries/EnumerableProposals.sol index 031b2033..6ddb3aab 100644 --- a/contracts/libraries/EnumerableProposals.sol +++ b/contracts/libraries/EnumerableProposals.sol @@ -37,11 +37,10 @@ library EnumerableProposals { uint256 proposalType, bytes memory data ) internal returns (bool) { - if (!contains(map, key)) { + if (map._keys.add(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; From 0856207b70408376aa96983a5f4ef1cf401809d7 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 10 Sep 2024 23:41:16 +0400 Subject: [PATCH 52/86] Remove redundant storage writings in AssetsAccounting --- contracts/libraries/AssetsAccounting.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index 93bb42f2..ad416999 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -378,8 +378,6 @@ library AssetsAccounting { unstETHRecord.status = UnstETHRecordStatus.Finalized; unstETHRecord.claimableAmount = amountFinalized; - - self.unstETHRecords[unstETHId] = unstETHRecord; } function _claimUnstETHRecord(Context storage self, uint256 unstETHId, ETHValue claimableAmount) private { @@ -397,7 +395,6 @@ library AssetsAccounting { unstETHRecord.claimableAmount = claimableAmount; } unstETHRecord.status = UnstETHRecordStatus.Claimed; - self.unstETHRecords[unstETHId] = unstETHRecord; } function _withdrawUnstETHRecord( From 8d9cf9f34ced72383f1a1ef5fcb39e0472ebf0cc Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 10 Sep 2024 23:42:29 +0400 Subject: [PATCH 53/86] Storage loads optimization in the Proposers.unregister() --- contracts/libraries/Proposers.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/libraries/Proposers.sol b/contracts/libraries/Proposers.sol index 33024d51..22d4e656 100644 --- a/contracts/libraries/Proposers.sol +++ b/contracts/libraries/Proposers.sol @@ -98,9 +98,10 @@ library Proposers { IndexOneBased lastProposerIndex = IndicesOneBased.fromOneBasedValue(self.proposers.length); IndexOneBased proposerIndex = executorData.proposerIndex; - if (executorData.proposerIndex != lastProposerIndex) { - self.proposers[proposerIndex.toZeroBasedValue()] = self.proposers[lastProposerIndex.toZeroBasedValue()]; - self.executors[self.proposers[proposerIndex.toZeroBasedValue()]].proposerIndex = proposerIndex; + if (proposerIndex != lastProposerIndex) { + address lastProposer = self.proposers[lastProposerIndex.toZeroBasedValue()]; + self.proposers[proposerIndex.toZeroBasedValue()] = lastProposer; + self.executors[lastProposer].proposerIndex = proposerIndex; } self.proposers.pop(); From 8e9c36c6424fd573b6807b65f8861cdafa70d31a Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 10 Sep 2024 23:42:51 +0400 Subject: [PATCH 54/86] Fix TiebreakerCore constructor test --- test/unit/committees/TiebreakerCore.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/committees/TiebreakerCore.t.sol b/test/unit/committees/TiebreakerCore.t.sol index 99b67cb5..d950b835 100644 --- a/test/unit/committees/TiebreakerCore.t.sol +++ b/test/unit/committees/TiebreakerCore.t.sol @@ -54,6 +54,7 @@ contract TiebreakerCoreUnitTest is UnitTest { } function testFuzz_constructor_HappyPath(address _owner, address _dualGovernance, Duration _timelock) external { + vm.assume(_owner != address(0)); new TiebreakerCore(_owner, _dualGovernance, _timelock); } From 59654ae3d9176910e67899e93469ec9d1e04e6e5 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 11 Sep 2024 01:20:02 +0400 Subject: [PATCH 55/86] Emit Voted event when the vote is the same --- contracts/committees/HashConsensus.sol | 4 ---- test/unit/committees/HashConsensus.t.sol | 16 ++++++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index 89990b46..fb0a4e1e 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -57,10 +57,6 @@ abstract contract HashConsensus is Ownable { revert HashAlreadyScheduled(hash); } - if (approves[msg.sender][hash] == support) { - return; - } - approves[msg.sender][hash] = support; emit Voted(msg.sender, hash, support); diff --git a/test/unit/committees/HashConsensus.t.sol b/test/unit/committees/HashConsensus.t.sol index e3f3b0aa..7e2eeb91 100644 --- a/test/unit/committees/HashConsensus.t.sol +++ b/test/unit/committees/HashConsensus.t.sol @@ -521,6 +521,12 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { function test_vote() public { assertEq(_hashConsensusWrapper.approves(_committeeMembers[0], dataHash), false); + vm.prank(_committeeMembers[0]); + vm.expectEmit(address(_hashConsensusWrapper)); + emit HashConsensus.Voted(_committeeMembers[0], dataHash, false); + _hashConsensusWrapper.vote(dataHash, false); + assertEq(_hashConsensusWrapper.approves(_committeeMembers[0], dataHash), false); + vm.prank(_committeeMembers[0]); vm.expectEmit(address(_hashConsensusWrapper)); emit HashConsensus.Voted(_committeeMembers[0], dataHash, true); @@ -528,10 +534,9 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { assertEq(_hashConsensusWrapper.approves(_committeeMembers[0], dataHash), true); vm.prank(_committeeMembers[0]); - vm.recordLogs(); + vm.expectEmit(address(_hashConsensusWrapper)); + emit HashConsensus.Voted(_committeeMembers[0], dataHash, true); _hashConsensusWrapper.vote(dataHash, true); - Vm.Log[] memory logs = vm.getRecordedLogs(); - assertEq(logs.length, 0); assertEq(_hashConsensusWrapper.approves(_committeeMembers[0], dataHash), true); vm.prank(_committeeMembers[0]); @@ -541,10 +546,9 @@ contract HashConsensusInternalUnitTest is HashConsensusUnitTest { assertEq(_hashConsensusWrapper.approves(_committeeMembers[0], dataHash), false); vm.prank(_committeeMembers[0]); - vm.recordLogs(); + vm.expectEmit(address(_hashConsensusWrapper)); + emit HashConsensus.Voted(_committeeMembers[0], dataHash, false); _hashConsensusWrapper.vote(dataHash, false); - logs = vm.getRecordedLogs(); - assertEq(logs.length, 0); assertEq(_hashConsensusWrapper.approves(_committeeMembers[0], dataHash), false); } From a587ee48a87753819868ece3126c40f7323f44f0 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 11 Sep 2024 02:02:28 +0400 Subject: [PATCH 56/86] Add Deactivation -> RageQuit transition description --- docs/mechanism.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index af81e266..0667e071 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -220,11 +220,17 @@ 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 - t^S_{act} \leq \, T_{lock}(R) \big) \,\lor\, \big( R > R_2 \big) +\big( t - t^S_{act} \leq \, T_{lock}(R) \big) ``` then the Deactivation sub-state is exited so only the parent Veto Signalling state remains active. +**Transition to Rage Quit**. If, while the sub-state is active, the following condition becomes true: +```math +\big( t - t^S_{act} > L_{max} \big) \, \land \, \big( R > R_2 \big) +``` +then the Deactivation sub-state is exited along with its parent Veto Signalling state and the Rage Quit state is entered. + **Transition to Veto Cooldown**. If, while the sub-state is active, the following condition becomes true: ```math From 21733dd86893018fe8f6408cbfbf6275f52d4d4b Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 11 Sep 2024 02:33:10 +0400 Subject: [PATCH 57/86] Remove brackets from the formula --- docs/mechanism.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index 0667e071..31b1c917 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -220,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 - t^S_{act} \leq \, T_{lock}(R) \big) +t - t^S_{act} \leq \, T_{lock}(R) ``` then the Deactivation sub-state is exited so only the parent Veto Signalling state remains active. From e7311d770b48c1986248e6a193984f8d25635dea Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 11 Sep 2024 14:15:12 +0400 Subject: [PATCH 58/86] Use pending state getter in Escrow lock/unlock methods --- contracts/DualGovernance.sol | 13 ++++++++++++- contracts/Escrow.sol | 21 +++++++++++++-------- contracts/interfaces/IDualGovernance.sol | 1 + test/scenario/escrow.t.sol | 2 +- test/utils/scenario-test-blueprint.sol | 2 +- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index b5ca36ca..75d8b9de 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -16,7 +16,11 @@ import {IResealManager} from "./interfaces/IResealManager.sol"; import {Proposers} from "./libraries/Proposers.sol"; import {Tiebreaker} from "./libraries/Tiebreaker.sol"; import {ExternalCall} from "./libraries/ExternalCalls.sol"; -import {State, DualGovernanceStateMachine} from "./libraries/DualGovernanceStateMachine.sol"; +import { + State, + DualGovernanceStateMachine, + DualGovernanceStateTransitions +} from "./libraries/DualGovernanceStateMachine.sol"; import {IDualGovernanceConfigProvider} from "./DualGovernanceConfigProvider.sol"; import {Escrow} from "./Escrow.sol"; @@ -25,6 +29,7 @@ contract DualGovernance is IDualGovernance { using Proposers for Proposers.Context; using Tiebreaker for Tiebreaker.Context; using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; + using DualGovernanceStateTransitions for DualGovernanceStateMachine.Context; // --- // Errors @@ -214,6 +219,12 @@ contract DualGovernance is IDualGovernance { return _stateMachine.getStateDetails(_configProvider.getDualGovernanceConfig()); } + function hasPendingRageQuitTransition() external view returns (bool) { + (State currentState, State newState) = + _stateMachine.getStateTransition(_configProvider.getDualGovernanceConfig()); + return currentState != State.RageQuit && newState == State.RageQuit; + } + // --- // Proposers & Executors Management // --- diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index f71d5f54..fc71f8fa 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -48,12 +48,12 @@ contract Escrow is IEscrow { // --- // Errors // --- - error UnclaimedBatches(); error UnexpectedUnstETHId(); error UnfinalizedUnstETHIds(); error NonProxyCallsForbidden(); error BatchesQueueIsNotClosed(); + error PendingRageQuitTransition(); error EmptyUnstETHIds(); error InvalidBatchSize(uint256 size); error CallerIsNotDualGovernance(address caller); @@ -132,7 +132,7 @@ contract Escrow is IEscrow { // --- function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { - DUAL_GOVERNANCE.activateNextState(); + _checkNoPendingRageQuitTransition(); _escrowState.checkSignallingEscrow(); lockedStETHShares = ST_ETH.getSharesByPooledEth(amount); @@ -143,7 +143,7 @@ contract Escrow is IEscrow { } function unlockStETH() external returns (uint256 unlockedStETHShares) { - DUAL_GOVERNANCE.activateNextState(); + _checkNoPendingRageQuitTransition(); _escrowState.checkSignallingEscrow(); _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); @@ -158,7 +158,7 @@ contract Escrow is IEscrow { // --- function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { - DUAL_GOVERNANCE.activateNextState(); + _checkNoPendingRageQuitTransition(); _escrowState.checkSignallingEscrow(); WST_ETH.transferFrom(msg.sender, address(this), amount); @@ -169,7 +169,7 @@ contract Escrow is IEscrow { } function unlockWstETH() external returns (uint256 unlockedStETHShares) { - DUAL_GOVERNANCE.activateNextState(); + _checkNoPendingRageQuitTransition(); _escrowState.checkSignallingEscrow(); _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); @@ -187,8 +187,7 @@ contract Escrow is IEscrow { if (unstETHIds.length == 0) { revert EmptyUnstETHIds(); } - - DUAL_GOVERNANCE.activateNextState(); + _checkNoPendingRageQuitTransition(); _escrowState.checkSignallingEscrow(); WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); @@ -202,7 +201,7 @@ contract Escrow is IEscrow { } function unlockUnstETH(uint256[] memory unstETHIds) external { - DUAL_GOVERNANCE.activateNextState(); + _checkNoPendingRageQuitTransition(); _escrowState.checkSignallingEscrow(); _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); @@ -483,4 +482,10 @@ contract Escrow is IEscrow { revert CallerIsNotDualGovernance(msg.sender); } } + + function _checkNoPendingRageQuitTransition() internal view { + if (DUAL_GOVERNANCE.hasPendingRageQuitTransition()) { + revert PendingRageQuitTransition(); + } + } } diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 977d0576..50f9194f 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -21,4 +21,5 @@ interface IDualGovernance is IGovernance, ITiebreaker { function activateNextState() external; function resealSealable(address sealables) external; + function hasPendingRageQuitTransition() external view returns (bool); } diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 7bef7150..40b0c927 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -610,7 +610,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { vm.revertTo(snapshotId); // The attempt to unlock funds from Escrow will fail - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(Escrow.PendingRageQuitTransition.selector)); this.externalUnlockStETH(_VETOER_1); } diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index f0479515..eef91cda 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -176,7 +176,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { } function _lockWstETH(address vetoer, PercentD16 tvlPercentage) internal { - _lockStETH(vetoer, _lido.calcSharesFromPercentageOfTVL(tvlPercentage)); + _lockWstETH(vetoer, _lido.calcSharesFromPercentageOfTVL(tvlPercentage)); } function _lockWstETH(address vetoer, uint256 amount) internal { From 6d16f43499307bfb60b7d964ab71cdefbfe4ca96 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 11 Sep 2024 19:01:59 +0400 Subject: [PATCH 59/86] StateDetails.dynamicDelay -> StateDetails.vetoSignallingDuration --- contracts/interfaces/IDualGovernance.sol | 2 +- contracts/libraries/DualGovernanceStateMachine.sol | 3 ++- test/unit/DualGovernance.t.sol | 12 ++++++------ test/utils/scenario-test-blueprint.sol | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 977d0576..7e5b80dc 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -15,7 +15,7 @@ interface IDualGovernance is IGovernance, ITiebreaker { Timestamp vetoSignallingReactivationTime; Timestamp normalOrVetoCooldownExitedAt; uint256 rageQuitRound; - Duration dynamicDelay; + Duration vetoSignallingDuration; } function activateNextState() external; diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 7e2cbe97..a6d47b72 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -139,7 +139,8 @@ library DualGovernanceStateMachine { stateDetails.vetoSignallingReactivationTime = self.vetoSignallingReactivationTime; stateDetails.normalOrVetoCooldownExitedAt = self.normalOrVetoCooldownExitedAt; stateDetails.rageQuitRound = self.rageQuitRound; - stateDetails.dynamicDelay = config.calcVetoSignallingDuration(self.signallingEscrow.getRageQuitSupport()); + stateDetails.vetoSignallingDuration = + config.calcVetoSignallingDuration(self.signallingEscrow.getRageQuitSupport()); } function getState(Context storage self) internal view returns (State) { diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 6a39e6ad..bf3bf14d 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -730,7 +730,7 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); assertEq(details.normalOrVetoCooldownExitedAt, Timestamps.from(0)); assertEq(details.rageQuitRound, 0); - assertEq(details.dynamicDelay, Durations.from(0)); + assertEq(details.vetoSignallingDuration, Durations.from(0)); vm.startPrank(vetoer); _escrow.lockStETH(5 ether); @@ -745,7 +745,7 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); assertEq(details.normalOrVetoCooldownExitedAt, vetoSignallingTime); assertEq(details.rageQuitRound, 0); - assertTrue(details.dynamicDelay > _configProvider.VETO_SIGNALLING_MIN_DURATION()); + assertTrue(details.vetoSignallingDuration > _configProvider.VETO_SIGNALLING_MIN_DURATION()); _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); vm.prank(vetoer); @@ -760,7 +760,7 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); assertEq(details.normalOrVetoCooldownExitedAt, vetoSignallingTime); assertEq(details.rageQuitRound, 0); - assertEq(details.dynamicDelay, Durations.from(0)); + assertEq(details.vetoSignallingDuration, Durations.from(0)); _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); Timestamp vetoCooldownTime = Timestamps.now(); @@ -773,7 +773,7 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); assertEq(details.normalOrVetoCooldownExitedAt, vetoSignallingTime); assertEq(details.rageQuitRound, 0); - assertEq(details.dynamicDelay, Durations.from(0)); + assertEq(details.vetoSignallingDuration, Durations.from(0)); _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); Timestamp backToNormalTime = Timestamps.now(); @@ -786,7 +786,7 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); assertEq(details.normalOrVetoCooldownExitedAt, backToNormalTime); assertEq(details.rageQuitRound, 0); - assertEq(details.dynamicDelay, Durations.from(0)); + assertEq(details.vetoSignallingDuration, Durations.from(0)); vm.startPrank(vetoer); _escrow.lockStETH(5 ether); @@ -804,7 +804,7 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); assertEq(details.normalOrVetoCooldownExitedAt, backToNormalTime); assertEq(details.rageQuitRound, 1); - assertEq(details.dynamicDelay, Durations.from(0)); + assertEq(details.vetoSignallingDuration, Durations.from(0)); } // --- diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index e7655088..6f353e26 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -91,7 +91,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { { IDualGovernance.StateDetails memory stateContext = _dualGovernance.getStateDetails(); isActive = stateContext.state == DGState.VetoSignalling; - duration = _dualGovernance.getStateDetails().dynamicDelay.toSeconds(); + duration = _dualGovernance.getStateDetails().vetoSignallingDuration.toSeconds(); enteredAt = stateContext.enteredAt.toSeconds(); activatedAt = stateContext.vetoSignallingActivatedAt.toSeconds(); } From 59f12b5d6f22bd4dc17ad3765248548ebb8ed306 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 11 Sep 2024 23:01:17 +0400 Subject: [PATCH 60/86] Fix broken imports in the tests --- test/utils/scenario-test-blueprint.sol | 8 +++++++- test/utils/testing-assert-eq-extender.sol | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index f0479515..915663c8 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -23,12 +23,18 @@ import {ITimelock} from "contracts/interfaces/ITimelock.sol"; import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; import {IPotentiallyDangerousContract} from "./interfaces/IPotentiallyDangerousContract.sol"; +// --- +// Libraries +// --- + +import {Status as ProposalStatus} from "contracts/libraries/ExecutableProposals.sol"; + // --- // Main Contracts // --- import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; -import {ProposalStatus, EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; +import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; import {IGovernance} from "contracts/TimelockedGovernance.sol"; import {State as DGState, DualGovernanceStateMachine} from "contracts/DualGovernance.sol"; diff --git a/test/utils/testing-assert-eq-extender.sol b/test/utils/testing-assert-eq-extender.sol index c8f78d7d..181a7d3c 100644 --- a/test/utils/testing-assert-eq-extender.sol +++ b/test/utils/testing-assert-eq-extender.sol @@ -7,7 +7,8 @@ import {Duration} from "contracts/types/Duration.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; import {PercentD16} from "contracts/types/PercentD16.sol"; -import {ProposalStatus} from "contracts/EmergencyProtectedTimelock.sol"; +import {Status as ProposalStatus} from "contracts/libraries/ExecutableProposals.sol"; + import {State as DualGovernanceState} from "contracts/DualGovernance.sol"; contract TestingAssertEqExtender is Test { From b52e64fa94af5395a0371bca6627ffe3ca6d0ae6 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 12 Sep 2024 00:31:11 +0400 Subject: [PATCH 61/86] Remove RageQuit state check from the Escrow's lock/unlock methods --- contracts/Escrow.sol | 12 ------------ test/scenario/escrow.t.sol | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index fc71f8fa..10458f0b 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -132,7 +132,6 @@ contract Escrow is IEscrow { // --- function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { - _checkNoPendingRageQuitTransition(); _escrowState.checkSignallingEscrow(); lockedStETHShares = ST_ETH.getSharesByPooledEth(amount); @@ -143,7 +142,6 @@ contract Escrow is IEscrow { } function unlockStETH() external returns (uint256 unlockedStETHShares) { - _checkNoPendingRageQuitTransition(); _escrowState.checkSignallingEscrow(); _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); @@ -158,7 +156,6 @@ contract Escrow is IEscrow { // --- function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { - _checkNoPendingRageQuitTransition(); _escrowState.checkSignallingEscrow(); WST_ETH.transferFrom(msg.sender, address(this), amount); @@ -169,7 +166,6 @@ contract Escrow is IEscrow { } function unlockWstETH() external returns (uint256 unlockedStETHShares) { - _checkNoPendingRageQuitTransition(); _escrowState.checkSignallingEscrow(); _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); @@ -187,7 +183,6 @@ contract Escrow is IEscrow { if (unstETHIds.length == 0) { revert EmptyUnstETHIds(); } - _checkNoPendingRageQuitTransition(); _escrowState.checkSignallingEscrow(); WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); @@ -201,7 +196,6 @@ contract Escrow is IEscrow { } function unlockUnstETH(uint256[] memory unstETHIds) external { - _checkNoPendingRageQuitTransition(); _escrowState.checkSignallingEscrow(); _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); @@ -482,10 +476,4 @@ contract Escrow is IEscrow { revert CallerIsNotDualGovernance(msg.sender); } } - - function _checkNoPendingRageQuitTransition() internal view { - if (DUAL_GOVERNANCE.hasPendingRageQuitTransition()) { - revert PendingRageQuitTransition(); - } - } } diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 40b0c927..c6b6d414 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -609,9 +609,18 @@ contract EscrowHappyPath is ScenarioTestBlueprint { // Rollback the state of the node as it was before RageQuit activation vm.revertTo(snapshotId); - // The attempt to unlock funds from Escrow will fail - vm.expectRevert(abi.encodeWithSelector(Escrow.PendingRageQuitTransition.selector)); - this.externalUnlockStETH(_VETOER_1); + // Vetoer may unlock funds while the activateNextState wasn't called and the DG will + // transition into the VetoSignallingDeactivationState + _unlockStETH(_VETOER_1); + _assertVetoSignalingDeactivationState(); + + // Rollback the state of the node as it was before RageQuit activation + vm.revertTo(snapshotId); + + // While the RageQuit not started, anyone can lock stETH/wstETH/unstETH after which + // DG system will transition into RageQuit state + _lockStETH(_VETOER_2, PercentsD16.fromBasisPoints(1_00)); + _assertRageQuitState(); } // --- From e3dfb3d8ce0d0a77abd4657fe97b24e4108c1df2 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 12 Sep 2024 00:43:25 +0400 Subject: [PATCH 62/86] Add nextState property to StateDetails --- contracts/DualGovernance.sol | 14 +------------- contracts/interfaces/IDualGovernance.sol | 2 +- contracts/libraries/DualGovernanceStateMachine.sol | 8 ++++++-- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 75d8b9de..98c25c19 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.26; import {Duration} from "./types/Duration.sol"; -import {Timestamp} from "./types/Timestamp.sol"; import {ITimelock} from "./interfaces/ITimelock.sol"; import {IResealManager} from "./interfaces/IResealManager.sol"; @@ -16,11 +15,7 @@ import {IResealManager} from "./interfaces/IResealManager.sol"; import {Proposers} from "./libraries/Proposers.sol"; import {Tiebreaker} from "./libraries/Tiebreaker.sol"; import {ExternalCall} from "./libraries/ExternalCalls.sol"; -import { - State, - DualGovernanceStateMachine, - DualGovernanceStateTransitions -} from "./libraries/DualGovernanceStateMachine.sol"; +import {State, DualGovernanceStateMachine} from "./libraries/DualGovernanceStateMachine.sol"; import {IDualGovernanceConfigProvider} from "./DualGovernanceConfigProvider.sol"; import {Escrow} from "./Escrow.sol"; @@ -29,7 +24,6 @@ contract DualGovernance is IDualGovernance { using Proposers for Proposers.Context; using Tiebreaker for Tiebreaker.Context; using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; - using DualGovernanceStateTransitions for DualGovernanceStateMachine.Context; // --- // Errors @@ -219,12 +213,6 @@ contract DualGovernance is IDualGovernance { return _stateMachine.getStateDetails(_configProvider.getDualGovernanceConfig()); } - function hasPendingRageQuitTransition() external view returns (bool) { - (State currentState, State newState) = - _stateMachine.getStateTransition(_configProvider.getDualGovernanceConfig()); - return currentState != State.RageQuit && newState == State.RageQuit; - } - // --- // Proposers & Executors Management // --- diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 50f9194f..d75b3033 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -11,6 +11,7 @@ interface IDualGovernance is IGovernance, ITiebreaker { struct StateDetails { State state; Timestamp enteredAt; + State nextState; Timestamp vetoSignallingActivatedAt; Timestamp vetoSignallingReactivationTime; Timestamp normalOrVetoCooldownExitedAt; @@ -21,5 +22,4 @@ interface IDualGovernance is IGovernance, ITiebreaker { function activateNextState() external; function resealSealable(address sealables) external; - function hasPendingRageQuitTransition() external view returns (bool); } diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index c82d5425..48c8faf0 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -23,6 +23,7 @@ enum State { } library DualGovernanceStateMachine { + using DualGovernanceStateTransitions for Context; using DualGovernanceConfig for DualGovernanceConfig.Context; struct Context { @@ -88,7 +89,7 @@ library DualGovernanceStateMachine { DualGovernanceConfig.Context memory config, address escrowMasterCopy ) internal { - (State currentState, State newState) = DualGovernanceStateTransitions.getStateTransition(self, config); + (State currentState, State newState) = self.getStateTransition(config); if (currentState == newState) { return; @@ -133,8 +134,11 @@ library DualGovernanceStateMachine { Context storage self, DualGovernanceConfig.Context memory config ) internal view returns (IDualGovernance.StateDetails memory stateDetails) { - stateDetails.state = self.state; + (State currentState, State nextState) = self.getStateTransition(config); + + stateDetails.state = currentState; stateDetails.enteredAt = self.enteredAt; + stateDetails.nextState = nextState; stateDetails.vetoSignallingActivatedAt = self.vetoSignallingActivatedAt; stateDetails.vetoSignallingReactivationTime = self.vetoSignallingReactivationTime; stateDetails.normalOrVetoCooldownExitedAt = self.normalOrVetoCooldownExitedAt; From 20e162cdbb9a94ec500a0a957baf05a7859d90c2 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 12 Sep 2024 05:21:41 +0400 Subject: [PATCH 63/86] Update specification with new contracts interfaces --- docs/specification.md | 396 +++++++++++++++++++----------------------- 1 file changed, 174 insertions(+), 222 deletions(-) diff --git a/docs/specification.md b/docs/specification.md index 6eb412a1..302c4aff 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -24,18 +24,21 @@ This document provides the system description on the code architecture level. A * [Tiebreaker committee](#tiebreaker-committee) * [Administrative actions](#administrative-actions) * [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) +* Core Contracts: + * [Contract: DualGovernance.sol](#contract-dualgovernancesol) + * [Contract: EmergencyProtectedTimelock.sol](#contract-emergencyprotectedtimelocksol) + * [Contract: Executor.sol](#contract-executorsol) + * [Contract: Escrow.sol](#contract-escrowsol) + * [Contract: ImmutableDualGovernanceConfigProvider.sol](#contract-immutabledualgovernanceconfigprovidersol) + * [Contract: ResealManager.sol](#contract-resealmanagersol) +* Committees + * [Contract: ProposalsList.sol](#contract-proposalslistsol) + * [Contract: HashConsensus.sol](#contract-hashconsensussol) + * [Contract: ResealCommittee.sol](#contract-resealcommitteesol) + * [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) @@ -46,15 +49,20 @@ This document provides the system description on the code architecture level. A 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). -* [`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. -* [`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. +* [`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). +* [`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). +* [`ImmutableDualGovernanceConfigProvider.sol`](#contract-immutabledualgovernanceconfigprovidersol) is a singleton contract that stores the configurable parameters of the DualGovernance system in an immutable manner. +* [`ResealManager.sol`](#contract-resealmanagersol) is a singleton contract responsible for extending or resuming sealable contracts paused by the [GateSeal emergency protection mechanism](https://github.com/lidofinance/gate-seals). This contract is essential due to the dynamic timelock of Dual Governance, which may prevent the DAO from extending the pause in time. It holds the authority to manage the pausing and resuming of specific protocol components protected by GateSeal. + +Additionally, the system uses several committee contracts that allow members to execute, acquiring quorum, a narrow set of actions while protecting management of the committees by the Dual Governance mechanism: + +* [`ResealCommittee.sol`](#contract-resealcommitteesol) is a committee contract that allows members to obtain a quorum and reseal contracts temporarily paused by the [GateSeal emergency protection mechanism](https://github.com/lidofinance/gate-seals). +* [`TiebreakerCore.sol`](#contract-tiebreakercoresol) is a committee contract designed to approve proposals for execution in extreme situations where the Dual Governance system is deadlocked. This includes scenarios such as the inability to finalize user withdrawal requests during ongoing `RageQuit` or when the system is held in a locked state for an extended period. The `TiebreakerCore` consists of multiple `TiebreakerSubCommittee` contracts appointed by the DAO. +* [`TiebreakerSubCommittee.sol`](#contract-tiebreakersubcommitteesol) is a committee contracts that provides ability to participate in `TiebreakerCore` for external actors. +* [`EmergencyActivationCommittee`](#contract-emergencyactivationcommitteesol) is a committee contract responsible for activating Emergency Mode by acquiring quorum. Only the EmergencyExecutionCommittee can execute proposals. This committee is expected to be active for a limited period following the initial deployment or update of the DualGovernance system. +* [`EmergencyExecutionCommittee`](#contract-emergencyexecutioncommitteesol) is a committee contract that enables quorum-based execution of proposals during Emergency Mode or disabling the DualGovernance mechanism by assigning the EmergencyProtectedTimelock to Aragon Voting. Like the EmergencyActivationCommittee, this committee is also intended for short-term use after the system’s deployment or update. ## Proposal flow @@ -65,7 +73,7 @@ The system supports multiple DAO voting systems, represented in the dual governa The general proposal flow is the following: -1. A proposer submits a proposal, i.e. a set of EVM calls (represented by an array of [`ExecutorCall`](#Struct-ExecutorCall) structs) to be issued by the proposer's associated [executor contract](#Contract-Executorsol), by calling the [`DualGovernance.submitProposal`](#Function-DualGovernancesubmitProposal) function. +1. A proposer submits a proposal, i.e. a set of EVM calls (represented by an array of [`ExternalCall`](#Struct-ExternalCall) structs) to be issued by the proposer's associated [executor contract](#Contract-Executorsol), by calling the [`DualGovernance.submitProposal`](#Function-DualGovernancesubmitProposal) function. 2. This starts a [dynamic timelock period](#Dynamic-timelock) that allows stakers to oppose the DAO, potentially leaving the protocol before the timelock elapses. 3. By the end of the dynamic timelock period, the proposal is either canceled by the DAO or executable. * If it's canceled, it cannot be scheduled for execution. However, any proposer is free to submit a new proposal with the same set of calls. @@ -113,7 +121,7 @@ The emergency mode lasts up to the **emergency mode max duration** counting from 1) Only the **emergency execution committee** has the right to execute scheduled proposals 2) The same committee has the one-off right to **disable the DG subsystem**. After this action, the system should start behaving according to [this specification](plan-b.md)). This involves disconnecting the `EmergencyProtectedTimelock` contract and its associated executor contracts from the DG contracts and reconnect them to the `TimelockedGovernance` contract instance. -Disabling the DG subsystem also disables 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. +Disabling the DG subsystem 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. @@ -139,6 +147,7 @@ Possible state transitions: * `Normal` → `VetoSignalling` * `VetoSignalling` → `RageQuit` * `VetoSignallingDeactivation` sub-state entry and exit (while the parent `VetoSignalling` state is active) +* `VetoSignallingDeactivation` → `RageQuit` * `VetoSignallingDeactivation` → `VetoCooldown` * `VetoCooldown` → `Normal` * `VetoCooldown` → `VetoSignalling` @@ -192,7 +201,7 @@ The dual governance system supports a set of administrative actions, including: * Managing the [deployment mode](#Proposal-execution-and-deployment-modes): configuring or disabling the emergency protection delay, setting the emergency committee addresses and lifetime. * Setting the [Tiebreaker committee](#Tiebreaker-committee) address. -Each of these actions can only be performed by a designated **admin executor** contract (set by a configuration option), meaning that: +Each of these actions can only be performed by a designated **admin executor** contract (declared in the `EmergencyProtectedTimelock` instance), meaning that: 1. It has to be proposed by one of the proposers associated with this executor. Such proposers are called **admin proposers**. 2. It has to go through the dual governance execution flow with stakers having the power to object. @@ -200,10 +209,10 @@ Each of these actions can only be performed by a designated **admin executor** c ## Common types -### Struct: ExecutorCall +### Struct: ExternalCall ```solidity -struct ExecutorCall { +struct ExternalCall { address target; uint96 value; bytes payload; @@ -225,10 +234,11 @@ The main entry point to the dual governance system. This contract is a singleton, meaning that any DG deployment includes exactly one instance of this contract. -### Enum: DualGovernance.State +### Enum: DualGovernanceStateMachine.State ```solidity enum State { + Unset, // Indicates an uninitialized state during the contract creation Normal, VetoSignalling, VetoSignallingDeactivation, @@ -243,11 +253,11 @@ Encodes the current global [governance state](#Governance-state), affecting the ### Function: DualGovernance.submitProposal ```solidity -function submitProposal(ExecutorCall[] calls) +function submitProposal(ExecutorCall[] calls, string calldata metadata) returns (uint256 proposalId) ``` -Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance to register a new governance proposal composed of one or more EVM `calls` to be made by an executor contract currently associated with the proposer address calling this function. Starts a dynamic timelock on [scheduling the proposal](#Function-DualGovernancescheduleProposal) for execution. +Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance to register a new governance proposal composed of one or more EVM `calls`, along with the attached `metadata` text. The proposal will be executed by an executor contract currently associated with the proposer address calling this function. Starts a dynamic timelock on [scheduling the proposal](#Function-DualGovernancescheduleProposal) for execution. See: [`EmergencyProtectedTimelock.submit`](#Function-EmergencyProtectedTimelocksubmit). @@ -270,15 +280,14 @@ Triggers a transition of the current governance state (if one is possible) befor function tiebreakerScheduleProposal(uint256 proposalId) ``` -Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance to schedule the proposal with the id `proposalId` for execution, bypassing the proposal dynamic timelock and given that the proposal was previously approved by the [Tiebreaker committee](#Tiebreaker-committee) and that the tiebreaker execution timelock has elapsed. +Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance to schedule the proposal with the id `proposalId` for execution, bypassing the proposal dynamic timelock and given that the proposal was previously approved by the [Tiebreaker committee](#Tiebreaker-committee). #### Preconditions -* MUST be called by the [Tiebreaker committee address] +* MUST be called by the [Tiebreaker committee](#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 with the given id MUST be already submitted using the `DualGovernance.submitProposal` call. * The proposal MUST NOT be cancelled. -* 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. @@ -294,7 +303,7 @@ Calls the `ResealManager.resumeSealable(address sealable)` if all preconditions #### Preconditions -* MUST be called by the [Tiebreaker committee address] +* MUST be called by the [Tiebreaker committee](#Tiebreaker-committee) address * Either the Tiebreaker Condition A or the Tiebreaker Condition B MUST be met (see the [mechanism design document][mech design - tiebreaker]). @@ -324,7 +333,7 @@ Registers the `proposer` address in the system as a valid proposer and associate #### Preconditions -* MUST be called by the admin executor contract (see `Configuration.sol`). +* MUST be called by the admin executor contract. * The `proposer` address MUST NOT be already registered in the system. * The `executor` instance SHOULD be owned by the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance. @@ -341,12 +350,13 @@ Removes the registered `proposer` address from the list of valid proposers and d * MUST be called by the admin executor contract. * The `proposer` address MUST be registered in the system as proposer. +* The `proposer` address MUST NOT be the only one assigned to the admin executor. ### Function: DualGovernance.setTiebreakerCommittee ```solidity -function setTiebreakerCommittee(address newTiebreaker, address resealManager) +function setTiebreakerCommittee(address newTiebreaker) ``` Updates the address of the [Tiebreaker committee](#Tiebreaker-committee). @@ -354,6 +364,8 @@ Updates the address of the [Tiebreaker committee](#Tiebreaker-committee). #### Preconditions * MUST be called by the admin executor contract. +* The `newTiebreaker` address MUST NOT be the zero address. +* The `newTiebreaker` address MUST be different from the current tiebreaker address. ### Function: DualGovernance.activateNextState @@ -398,21 +410,11 @@ 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` -- Only governance address obtained from `EmergencyProtectedTimelock` can trigger these actions. - -```solidity -constructor(address emergencyProtectedTimelock) -``` +The **ResealManager** contract addresses this issue by enabling the extension of temporarily paused contracts into a permanent pause or resuming them if the following conditions are met: +- The contracts are paused for a limited duration, not indefinitely. +- The **DualGovernance** system is not in the `Normal` state. -Initializes the contract with the address of the EmergencyProtectedTimelock contract. - -#### Preconditions - -* emergencyProtectedTimelock MUST be a valid address. +To function properly, the **ResealManager** must be granted the `PAUSE_ROLE` and `RESUME_ROLE` for the target contracts. ### Function ResealManager.reseal @@ -420,25 +422,21 @@ Initializes the contract with the address of the EmergencyProtectedTimelock cont function reseal(address sealable) public ``` -Extends the pause of the specified `sealable` contract. This function can be called by the governance address defined in the `EmergencyProtectedTimelock`. +Extends the pause of the specified `sealable` contract indefinitely. #### Preconditions - The `ResealManager` MUST have `PAUSE_ROLE` and `RESUME_ROLE` for the target contract. -- The target contract MUST be paused until a future timestamp and not indefinitely. +- The target contract MUST be paused for a limited duration, with a future timestamp, and not indefinitely. - The function MUST be called by the governance address defined in `EmergencyProtectedTimelock`. -#### 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) external ``` -Resumes the specified `sealable` contract if it is scheduled to resume in the future. +Resumes the specified sealable contract if it is scheduled to resume at a future timestamp. #### Preconditions @@ -446,22 +444,14 @@ Resumes the specified `sealable` contract if it is scheduled to resume in the fu - The target contract MUST be paused. - The function MUST be called by the governance address defined in `EmergencyProtectedTimelock`. -#### Errors -- `SealableWrongPauseState`: Thrown if the sealable contract is in the wrong pause state. -- `SenderIsNotGovernance`: Thrown if the sender is not the governance address. - -#### Preconditions - -- The sender MUST be the governance address obtained from the `EmergencyProtectedTimelock` contract. - ## Contract: Escrow.sol 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 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 `MinAssetsLockDuration` has passed since their last funds locking operation) stETH, wstETH, and withdrawal NFTs, potentially changing the global governance state. The `MinAssetsLockDuration` 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.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. +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: @@ -472,13 +462,13 @@ At any point in time, there can be only one instance of the contract in the `Sig After the `Escrow` instance transitions into the `RageQuitEscrow` state, all locked stETH and wstETH tokens are meant to be converted into withdrawal NFTs using the permissionless `Escrow.requestNextWithdrawalsBatch()` function. -Once all funds locked in the `Escrow` instance are converted into withdrawal NFTs, finalized, and claimed, the main rage quit phase concludes, and the `Escrow.startRageQuitExtensionDelay()` method may be used to start the `RageQuitExtensionDelay` period. +Once all funds locked in the `Escrow` instance are converted into withdrawal NFTs, finalized, and claimed, the main rage quit phase concludes, and the `Escrow.startRageQuitExtensionPeriod()` method may be used to start the `RageQuitExtensionPeriod`. -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. +The purpose of the `startRageQuitExtensionPeriod` 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 `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. +When the `startRageQuitExtensionPeriod` period elapses, the `DualGovernance.activateNextState()` function exits the `RageQuit` state and initiates the `RageQuitEthWithdrawalsDelay`. 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 `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. +The duration of the `RageQuitEthWithdrawalsDelay` 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 @@ -510,6 +500,7 @@ The amount of stETH shares locked by the caller during the current method call. - The `Escrow` instance MUST be in the `SignallingEscrow` state. - The caller MUST have an allowance set on the stETH token for the `Escrow` instance equal to or greater than the locked `amount`. - The locked `amount` MUST NOT exceed the caller's stETH balance. +- The `DualGovernance` contract MUST NOT have a pending state transition to the `RageQuit` state. ### Function: Escrow.unlockStETH @@ -537,6 +528,7 @@ The amount of stETH shares unlocked by the caller. - 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. - 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 `DualGovernance` contract MUST NOT have a pending state transition to the `RageQuit` state. ### Function: Escrow.lockWstETH @@ -568,6 +560,7 @@ The amount of stETH shares locked by the caller during the current method call. - The `Escrow` instance MUST be in the `SignallingEscrow` state. - The caller MUST have an allowance set on the wstETH token for the `Escrow` instance equal to or greater than the locked `amount`. - The locked `amount` MUST NOT exceed the caller's wstETH balance. +- The `DualGovernance` contract MUST NOT have a pending state transition to the `RageQuit` state. ### Function: Escrow.unlockWstETH @@ -595,6 +588,7 @@ The amount of stETH shares unlocked by the caller. - The `Escrow` instance MUST be in the `SignallingEscrow` state. - The caller MUST have a non-zero amount of previously locked wstETH in the `Escrow` instance using the `Escrow.lockWstETH` 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 `DualGovernance` contract MUST NOT have a pending state transition to the `RageQuit` state. ### Function: Escrow.lockUnstETH @@ -624,6 +618,7 @@ The method calls the `DualGovernance.activateNextState()` function at the beginn - 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 duplicates. +- The `DualGovernance` contract MUST NOT have a pending state transition to the `RageQuit` state. ### Function: Escrow.unlockUnstETH @@ -719,34 +714,30 @@ An array of ids for the generated unstETH NFTs. ### Function Escrow.getRageQuitSupport() ```solidity -function getRageQuitSupport() view returns (uint256) +function getRageQuitSupport() view returns (PercentD16) ``` 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: +The returned value represents the total rage quit support expressed as a percentage with a precision of 16 decimals, calculated using the following formula: -```solidity -uint256 finalizedETH = unstETHTotals.finalizedETH; -uint256 unfinalizedShares = stETHTotals.lockedShares + unstETHTotals.unfinalizedShares; - -return 10 ** 18 * ( - ST_ETH.getPooledEtherByShares(unfinalizedShares) + finalizedETH -) / ( - stETH.totalSupply() + finalizedETH -); +```math +\frac{ \text{stETH.getPooledEtherByShares} (\text{lockedShares} + \text{unfinalizedShares}) + \text{finalizedETH} }{ \text{stETH.totalSupply()} + \text{finalizedETH} } ``` +where : +- `finalizedETH` refers to `unstETHTotals.finalizedETH` +- `unfinalizedShares` refers to `stETHTotals.lockedShares + unstETHTotals.unfinalizedShares` ### Function Escrow.startRageQuit ```solidity function startRageQuit( - Duration rageQuitExtensionDelay, - Duration rageQuitWithdrawalsTimelock + Duration rageQuitExtensionPeriodDuration, + Duration rageQuitEthWithdrawalsDelay ) ``` -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. +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 `RageQuitExtensionPeriod` and `RageQuitEthWithdrawalsDelay` stages. #### Preconditions @@ -756,17 +747,17 @@ Transits the `Escrow` instance from the `SignallingEscrow` state to the `RageQui ### Function Escrow.requestNextWithdrawalsBatch ```solidity -function requestNextWithdrawalsBatch(uint256 maxBatchSize) +function requestNextWithdrawalsBatch(uint256 batchSize) ``` -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. +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 `batchSize` withdrawal requests (except the final one, which may contain fewer items), 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 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. +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 `max(_MIN_TRANSFERRABLE_ST_ETH_AMOUNT, 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 `maxBatchSize` MUST be greater than or equal to `Escrow.MIN_WITHDRAWALS_BATCH_SIZE()`. +- The `batchSize` MUST be greater than or equal to `Escrow.MIN_WITHDRAWALS_BATCH_SIZE()`. - The generation of withdrawal request batches MUST not be concluded ### Function Escrow.claimNextWithdrawalsBatch(uint256, uint256[]) @@ -800,20 +791,20 @@ 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 affected 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 `RageQuitExtensionPeriod` 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 - The `Escrow` instance MUST be in the `RageQuitEscrow` state. - The provided `unstETHIds` MUST only contain finalized but unclaimed withdrawal requests with the owner set to `msg.sender`. -### Function Escrow.startRageQuitExtensionDelay +### Function Escrow.startRageQuitExtensionPeriod ```solidity -function startRageQuitExtensionDelay() +function startRageQuitExtensionPeriod() ``` -Initiates the `RageQuitExtensionDelay` once all withdrawal batches have been claimed. In cases where the `Escrow` instance only has locked unstETH NFTs, it verifies that the last unstETH NFT registered in the `WithdrawalQueue` at the time of the `Escrow.startRageQuit()` call is finalized. This ensures that every unstETH NFT locked in the Escrow can be claimed by the user during the `RageQuitExtensionDelay`. +Initiates the `RageQuitExtensionPeriod` once all withdrawal batches have been claimed. In cases where the `Escrow` instance only has locked unstETH NFTs, it verifies that the last unstETH NFT registered in the `WithdrawalQueue` at the time of the `Escrow.startRageQuit()` call is finalized. This ensures that every unstETH NFT locked in the Escrow can be claimed by the user during the `RageQuitExtensionPeriod`. #### Preconditions - All withdrawal batches MUST be formed using the `Escrow.requestNextWithdrawalsBatch()`. @@ -829,7 +820,7 @@ function isRageQuitFinalized() view returns (bool) Returns whether the rage quit process has been finalized. The rage quit process is considered finalized when all the following conditions are met: - The `Escrow` instance is in the `RageQuitEscrow` state. - All withdrawal request batches have been claimed. -- The duration of the `RageQuitExtensionDelay` has elapsed. +- The duration of the `RageQuitExtensionPeriod` has elapsed. ### Function Escrow.withdrawETH @@ -837,7 +828,7 @@ Returns whether the rage quit process has been finalized. The rage quit process function withdrawETH() ``` -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. +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 `RageQuitEthWithdrawalsDelay` 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 and wstETH shares compared to the total amount of locked stETH and wstETH shares in the Escrow instance, calculated as follows: @@ -849,8 +840,8 @@ return stETHTotals.claimedETH * assets[msg.sender].stETHLockedShares #### 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 rage quit process MUST be completed, including the expiration of the `RageQuitExtensionPeriod` duration. +- The `RageQuitEthWithdrawalsDelay` period MUST be elapsed after the expiration of the `RageQuitExtensionPeriod` duration. - The caller MUST have a non-zero amount of stETH shares to withdraw. ### Function Escrow.withdrawETH() @@ -864,8 +855,8 @@ Allows the caller (i.e. `msg.sender`) to withdraw the claimed ETH from the withd #### 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 rage quit process MUST be completed, including the expiration of the `RageQuitExtensionPeriod` duration. +- The `RageQuitEthWithdrawalsDelay` period MUST be elapsed after the expiration of the `RageQuitExtensionPeriod` 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. @@ -878,9 +869,9 @@ 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 (`Configuration.AFTER_SUBMIT_DELAY()`) must elapse. +2. The configured post-submit timelock (`EmergencyProtectedTimelock.getAfterSubmitDelay()`) must elapse. 3. The proposal must be scheduled using the `EmergencyProtectedTimelock.schedule` function. -4. The configured emergency protection delay (`Configuration.AFTER_SCHEDULE_DELAY()`) must elapse (can be zero, see below). +4. The configured emergency protection delay (`EmergencyProtectedTimelock.getAfterScheduleDelay()`) 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. @@ -898,7 +889,7 @@ The governance reset entails the following steps: ### Function: EmergencyProtectedTimelock.submit ```solidity -function submit(address executor, ExecutorCall[] calls) +function submit(address executor, ExecutorCall[] calls, string calldata metadata) returns (uint256 proposalId) ``` @@ -1007,12 +998,19 @@ Resets the `governance` address to the `EMERGENCY_GOVERNANCE` value defined in t ### 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. +The contract has the interface for managing the configuration related to emergency protection (`setEmergencyProtectionActivationCommittee`, `setEmergencyProtectionExecutionCommittee`, `setEmergencyProtectionEndDate`, `setEmergencyModeDuration`, `setEmergencyGovernance`) and general system wiring (`transferExecutorOwnership`, `setGovernance`, `setupDelays`). These functions MUST be called by the [Admin Executor](#Administrative-actions) address, basically routing any such changes through the Dual Governance mechanics. +## Contract: ImmutableDualGovernanceConfigProvider.sol -## Contract: Configuration.sol +`ImmutableDualGovernanceConfigProvider.sol` is a smart contract that stores all the constants used in the Dual Governance system and provides an interface for accessing them. It implements the `IDualGovernanceConfigProvider` interface. -`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". +### Function: ImmutableDualGovernanceConfigProvider.getDualGovernanceConfig + +```solidity +function getDualGovernanceConfig() view returns (DualGovernanceConfig.Context memory config) +``` + +This function provides the configuration settings required for the proper functioning of the DualGovernance contract, ensuring that the system can access the necessary context and parameters for managing state transitions. ## Contract: ProposalsList.sol @@ -1021,42 +1019,42 @@ The contract has the interface for managing the configuration related to emergen ### Function: ProposalsList.getProposals ```solidity -function getProposals(uint256 offset, uint256 limit) public view returns (Proposal[] memory proposals) +function getProposals(uint256 offset, uint256 limit) view returns (Proposal[] memory proposals) ``` -Returns the limited list of `Proposal`s with offset. +Returns a list of `Proposal` objects starting from the specified `offset`, with the number of proposals limited by the `limit` parameter. ### Function: ProposalsList.getProposalAt ```solidity -function getProposalAt(uint256 index) public view returns (Proposal memory) +function getProposalAt(uint256 index) view returns (Proposal memory) ``` -Returns `Proposal` at position `index` +Returns the `Proposal` located at the specified `index` in the proposals list. ### Function: ProposalsList.getProposal ```solidity -function getProposal(bytes32 key) public view returns (Proposal memory) +function getProposal(bytes32 key) view returns (Proposal memory) ``` -Returns `Proposal` by it's `key` +Returns the `Proposal` identified by its unique `key`. ### Function: ProposalsList.getProposalsLength ```solidity -function getProposalsLength() public view returns (uint256) +function getProposalsLength() view returns (uint256) ``` -Returns total number of created `Proposal`s. +Returns the total number of `Proposal` objects created. ### Function: ProposalsList.getOrderedKeys ```solidity -function getOrderedKeys(uint256 offset, uint256 limit) public view returns (bytes32[] memory) +function getOrderedKeys(uint256 offset, uint256 limit) view returns (bytes32[] memory) ``` -Returns total list of `Proposal`s keys with offset and limit. +Returns an ordered list of `Proposal` keys with the given `offset` and `limit` for pagination. ## Contract: HashConsensus.sol @@ -1065,7 +1063,7 @@ Returns total list of `Proposal`s keys with offset and limit. ### Function: HashConsensus.addMembers ```solidity -function addMembers(address[] memory newMembers, uint256 newQuorum) public onlyOwner +function addMembers(address[] memory newMembers, uint256 executionQuorum) ``` Adds new members and updates the quorum. @@ -1076,10 +1074,10 @@ Adds new members and updates the quorum. * Members MUST NOT be part of the set. * `newQuorum` MUST be greater than 0 and less than or equal to the number of members. -### Function: HashConsensus.removeMember +### Function: HashConsensus.removeMembers ```solidity -function removeMembers(address[] memory membersToRemove, uint256 newQuorum) public onlyOwner +function removeMembers(address[] memory membersToRemove, uint256 executionQuorum) ``` Removes members and updates the quorum. @@ -1093,7 +1091,7 @@ Removes members and updates the quorum. ### Function: HashConsensus.getMembers ```solidity -function getMembers() public view returns (address[] memory) +function getMembers() view returns (address[] memory) ``` Returns the list of current members. @@ -1101,15 +1099,15 @@ Returns the list of current members. ### Function: HashConsensus.isMember ```solidity -function isMember(address member) public view returns (bool) +function isMember(address member) view returns (bool) ``` -Checks if an address is a member. +Returns if an address is a member. ### Function: HashConsensus.setTimelockDuration ```solidity -function setTimelockDuration(uint256 timelock) public onlyOwner +function setTimelockDuration(uint256 timelock) ``` Sets the timelock duration. @@ -1117,11 +1115,12 @@ Sets the timelock duration. #### Preconditions * Only the owner can call this function. +* The new `timelock` value MUST not be equal to the current one ### Function: HashConsensus.setQuorum ```solidity -function setQuorum(uint256 newQuorum) public onlyOwner +function setQuorum(uint256 newQuorum) ``` Sets the quorum required for decision execution. @@ -1129,42 +1128,53 @@ 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. +* `newQuorum` MUST be greater than 0, less than or equal to the number of members, and not equal to the current `quorum` value. +## Contract: ResealCommittee.sol -### Admin functions +`ResealCommittee` is a smart contract that extends the `HashConsensus` and `PropsoalsList` contracts and allows members to obtain a quorum and reseal contracts temporarily paused by the [GateSeal emergency protection mechanism](https://github.com/lidofinance/gate-seals). It interacts with a DualGovernance contract to execute decisions once consensus is reached. -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. +### Function: ResealCommittee.voteReseal +```solidity +function voteReseal(address sealable, bool support) +``` -## Contract: TiebreakerCore.sol +Reseals sealable by voting on it and adding it to the proposal list. -`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. +#### Preconditions +* MUST be called by a member. -### Constructor +### Function: ResealCommittee.getResealState ```solidity -constructor( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address dualGovernance, - uint256 timelock -) +function getResealState(address sealable) + view + returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt) ``` -Initializes the contract with an owner, committee members, a quorum, the address of the DualGovernance contract, and a timelock duration. +Returns the state of the sealable resume proposal including support count, quorum, and execution status. + +### Function: ResealCommittee.executeReseal + +```solidity +function executeReseal(address sealable) +``` + +Executes a reseal of the sealable contract by calling the `resealSealable` method on the `DualGovernance` contract #### Preconditions +* Proposal MUST be scheduled for execution and passed the timelock duration. + -* `executionQuorum` MUST be greater than 0. -* `dualGovernance` MUST be a valid address. +## 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. ### Function: TiebreakerCore.scheduleProposal ```solidity -function scheduleProposal(uint256 proposalId) public +function scheduleProposal(uint256 proposalId) ``` Schedules a proposal for execution by voting on it and adding it to the proposal list. @@ -1172,12 +1182,12 @@ Schedules a proposal for execution by voting on it and adding it to the proposal #### Preconditions * MUST be called by a member. +* Proposal with the given id MUST be submitted into `EmergencyProtectedTimelock` ### Function: TiebreakerCore.getScheduleProposalState ```solidity function getScheduleProposalState(uint256 proposalId) - public view returns (uint256 support, uint256 executionQuorum, bool isExecuted) ``` @@ -1187,19 +1197,19 @@ Returns the state of a scheduled proposal including support count, quorum, and e ### Function: TiebreakerCore.executeScheduleProposal ```solidity -function executeScheduleProposal(uint256 proposalId) public +function executeScheduleProposal(uint256 proposalId) ``` -Executes a scheduled proposal by calling the tiebreakerScheduleProposal function on the DualGovernance contract. +Executes a scheduled proposal by calling the `tiebreakerScheduleProposal` function on the `DualGovernance` contract. #### Preconditions -* Proposal MUST have reached quorum and passed the timelock duration. +* Proposal MUST be scheduled for execution and passed the timelock duration. ### Function: TiebreakerCore.getSealableResumeNonce ```solidity -function getSealableResumeNonce(address sealable) public view returns (uint256) +function getSealableResumeNonce(address sealable) view returns (uint256) ``` Returns the current nonce for resuming operations of a sealable contract. @@ -1207,7 +1217,7 @@ Returns the current nonce for resuming operations of a sealable contract. ### Function: TiebreakerCore.sealableResume ```solidity -function sealableResume(address sealable, uint256 nonce) public +function sealableResume(address sealable, uint256 nonce) ``` Submits a request to resume operations of a sealable contract by voting on it and adding it to the proposal list. @@ -1221,7 +1231,6 @@ Submits a request to resume operations of a sealable contract by voting on it an ```solidity function getSealableResumeState(address sealable, uint256 nonce) - public view returns (uint256 support, uint256 executionQuorum, bool isExecuted) ``` @@ -1231,10 +1240,10 @@ Returns the state of a sealable resume request including support count, quorum, ### Function: TiebreakerCore.executeSealableResume ```solidity -function executeSealableResume(address sealable) external +function executeSealableResume(address sealable) ``` -Executes a sealable resume request by calling the tiebreakerResumeSealable function on the DualGovernance contract and increments the nonce. +Executes a sealable resume request by calling the `tiebreakerResumeSealable` function on the `DualGovernance` contract and increments the nonce. #### Preconditions @@ -1244,39 +1253,23 @@ 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, - 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 +function scheduleProposal(uint256 proposalId) ``` Schedules a proposal for execution by voting on it and adding it to the proposal list. #### Preconditions * MUST be called by a member. +* Proposal with the given id MUST be submitted into `EmergencyProtectedTimelock` + ### Function: TiebreakerSubCommittee.getScheduleProposalState ```solidity function getScheduleProposalState(uint256 proposalId) - public view returns (uint256 support, uint256 executionQuorum, bool isExecuted) ``` @@ -1286,7 +1279,7 @@ Returns the state of a scheduled proposal including support count, quorum, and e ### Function: TiebreakerSubCommittee.executeScheduleProposal ```solidity -function executeScheduleProposal(uint256 proposalId) public +function executeScheduleProposal(uint256 proposalId) ``` Executes a scheduled proposal by calling the scheduleProposal function on the TiebreakerCore contract. @@ -1298,7 +1291,7 @@ Executes a scheduled proposal by calling the scheduleProposal function on the Ti ### Function: TiebreakerSubCommittee.sealableResume ```solidity -function sealableResume(address sealable) public +function sealableResume(address sealable) ``` Submits a request to resume operations of a sealable contract by voting on it and adding it to the proposal list. @@ -1306,11 +1299,9 @@ Submits a request to resume operations of a sealable contract by voting on it an #### Preconditions * MUST be called by a member. -* getSealableResumeState ```solidity function getSealableResumeState(address sealable) - public view returns (uint256 support, uint256 executionQuorum, bool isExecuted) ``` @@ -1333,31 +1324,13 @@ 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, - 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.approveActivateEmergencyMode ```solidity -function approveActivateEmergencyMode() public +function approveActivateEmergencyMode() ``` -Approves the emergency activation by voting on the EMERGENCY_ACTIVATION_HASH. +Approves the emergency activation by voting on the `EMERGENCY_ACTIVATION_HASH`. #### Preconditions @@ -1367,7 +1340,6 @@ Approves the emergency activation by voting on the EMERGENCY_ACTIVATION_HASH. ```solidity function getActivateEmergencyModeState() - public view returns (uint256 support, uint256 executionQuorum, bool isExecuted) ``` @@ -1380,7 +1352,7 @@ Returns the state of the emergency activation proposal including support count, function executeActivateEmergencyMode() external ``` -Executes the emergency activation by calling the emergencyActivate function on the EmergencyProtectedTimelock contract. +Executes the emergency activation by calling the `activateEmergencyMode` function on the `EmergencyProtectedTimelock` contract. #### Preconditions @@ -1391,28 +1363,10 @@ 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, - 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 +function voteEmergencyExecute(uint256 proposalId, bool _support) ``` Allows committee members to vote on an emergency execution proposal. @@ -1425,29 +1379,28 @@ Allows committee members to vote on an emergency execution proposal. ```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 +### Function: EmergencyExecutionCommittee.executeEmergencyExecute ```solidity -function executeEmergencyExecute(uint256 proposalId) public +function executeEmergencyExecute(uint256 proposalId) ``` -Executes an emergency execution proposal by calling the emergencyExecute function on the EmergencyProtectedTimelock contract. +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. +* Emergency execution proposal MUST have reached quorum and passed the timelock duration. ### Function: EmergencyExecutionCommittee.approveEmergencyReset ```solidity -function approveEmergencyReset() public +function approveEmergencyReset() ``` Approves the governance reset by voting on the reset proposal. @@ -1460,7 +1413,6 @@ Approves the governance reset by voting on the reset proposal. ```solidity function getEmergencyResetState() - public view returns (uint256 support, uint256 executionQuorum, bool isExecuted) ``` @@ -1473,7 +1425,7 @@ Returns the state of the governance reset proposal including support count, quor function executeEmergencyReset() external ``` -Executes the governance reset by calling the emergencyReset function on the EmergencyProtectedTimelock contract. +Executes the governance reset by calling the `emergencyReset` function on the `EmergencyProtectedTimelock` contract. #### Preconditions @@ -1490,7 +1442,7 @@ During the deployment of a new dual governance version, the Lido DAO will likely A typical proposal to update the dual governance system to a new version will likely contain the following steps: 1. Set the `governance` variable in the `EmergencyProtectedTimelock` instance to the new version of the `DualGovernance` contract. -2. Update the implementation of the `Configuration` proxy contract if necessary. +2. Deploy a new instance of the `ImmutableDualGovernanceConfigProvider` contract if necessary. 3. Configure emergency protection settings in the `EmergencyProtectedTimelock` contract, including the address of the committee, the duration of emergency protection, and the duration of the emergency mode. For more significant updates involving changes to the `EmergencyProtectedTimelock` or `Proposals` mechanics, new versions of both the `DualGovernance` and `EmergencyProtectedTimelock` contracts are deployed. While this adds more steps to maintain the proposal history, such as tracking old and new versions of the Timelocks, it also eliminates the need to migrate permissions or rights from executors. The `transferExecutorOwnership()` function of the `EmergencyProtectedTimelock` facilitates the assignment of executors to the newly deployed contract. From 2158d2342aebe7309b911592274ea7d9bcc607e2 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 12 Sep 2024 12:42:46 +0400 Subject: [PATCH 64/86] Update mechanism.md doc --- docs/mechanism.md | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index 31b1c917..07b9f427 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -12,7 +12,9 @@ Additionally, there is a Gate Seal emergency committee that allows pausing certa 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. -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. +Another way of looking at dual governance is that it implements: +1) a dynamic user-extensible timelock on DAO decisions +2) a rage quit mechanism for stakers taking into account the specifics of how Ethereum withdrawals work. ## Navigation @@ -282,13 +284,13 @@ Upon entry into the Rage Quit state, three things happen: 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. +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 `RageQuitExtensionPeriodDuration` days. 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 period, this guarantees that any staker having a withdrawal NFT locked in the rage quit escrow has at least `RageQuitExtensionPeriodDuration` 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: +When the withdrawal is complete and the extension period 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 Rage Quit state is exited. @@ -297,26 +299,22 @@ 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 withdraw 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 linear function that depends on the rage quit sequence number $i$ (see below): ```math -W(i) = W_{min} + -\left\{ \begin{array}{lc} - 0, & \text{if } i \lt i_{min} \\ - g_W(i - i_{min}), & \text{otherwise} -\end{array} \right. +W(i) = \min \left\{ W_{min} + i * W_{growth} \,,\, W_{max} \right\} ``` -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). +where $W_{min}$ is `RageQuitEthWithdrawalsMinDelay`, $W_{max}$ is `RageQuitEthWithdrawalsMaxDelay`, $W_{growth}$ is `rageQuitEthWithdrawalsDelayGrowth`. 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 -RageQuitEthWithdrawalsMinTimelock = 60 days -RageQuitEthWithdrawalsTimelockGrowthStartSeqNumber = 2 -RageQuitEthWithdrawalsTimelockGrowthCoeffs = (0, TODO, TODO) +RageQuitExtensionPeriodDuration = 7 days +RageQuitEthWithdrawalsMinDelay = 60 days +RageQuitEthWithdrawalsMaxDelay = 180 days +rageQuitEthWithdrawalsDelayGrowth = 15 days ``` @@ -362,10 +360,10 @@ To resolve the potential deadlock, the mechanism contains a third-party arbiter * 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: +The Tiebreaker committee can perform the above actions, subject to a timelock of `TiebreakerExecutionTimelock` days, if any of the following two conditions is true: * **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). +* **Tiebreaker Condition B**: the 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 Reseal committee. @@ -401,6 +399,15 @@ Dual governance should not cover: ## Changelog +### 2024-09-12 +- Explicitly described the `VetoSignallingDeactivation` -> `RageQuit` state transition. +- Renamed `RageQuitExtensionDelay` to `RageQuitExtensionPeriodDuration`. +- Replaced the quadratic function for the ETH withdrawal timelock $W(i)$ with a linear function. +- Renamed `RageQuitEthWithdrawalsMinTimelock` to `RageQuitEthWithdrawalsMinDelay`. +- Removed the `RageQuitEthWithdrawalsTimelockGrowthStartSeqNumber` and `RageQuitEthWithdrawalsTimelockGrowthCoeffs` parameters. +- Introduced the `RageQuitEthWithdrawalsMaxDelay` and `RageQuitEthWithdrawalsDelayGrowth` parameters to calculate the $W(i)$ duration. +- Removed the requirement **"governance state is Rage Quit"** from **Tiebreaker Condition B**. + ### 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`. From 3ac55ef2d876bfd6c8d034337c13ad0fa8e954bf Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 12 Sep 2024 13:00:29 +0400 Subject: [PATCH 65/86] Update plan-b.md doc --- docs/plan-b.md | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/docs/plan-b.md b/docs/plan-b.md index 1cd93be9..ceb01bae 100644 --- a/docs/plan-b.md +++ b/docs/plan-b.md @@ -9,26 +9,29 @@ Timelocked Governance (TG) is a governance subsystem positioned between the Lido * [Proposal flow](#proposal-flow) * [Proposal execution](#proposal-execution) * [Common types](#common-types) -* [Contract: `TimelockedGovernance`](#contract-timelockedgovernance) -* [Contract: `EmergencyProtectedTimelock`](#contract-emergencyprotectedtimelock) -* [Contract: `Executor`](#contract-executor) -* [Contract: `Configuration`](#contract-configuration) -* [Contract: `ProposalsList`](#contract-proposalslist) -* [Contract: `HashConsensus`](#contract-hashconsensus) -* [Contract: `EmergencyActivationCommittee`](#contract-emergencyactivationcommittee) -* [Contract: `EmergencyExecutionCommittee`](#contract-emergencyexecutioncommittee) +* Core Contracts: + * [Contract: `TimelockedGovernance`](#contract-timelockedgovernance) + * [Contract: `EmergencyProtectedTimelock`](#contract-emergencyprotectedtimelock) + * [Contract: `Executor`](#contract-executor) +* Committees: + * [Contract: `ProposalsList`](#contract-proposalslist) + * [Contract: `HashConsensus`](#contract-hashconsensus) + * [Contract: `EmergencyActivationCommittee`](#contract-emergencyactivationcommittee) + * [Contract: `EmergencyExecutionCommittee`](#contract-emergencyexecutioncommittee) ## System Overview image The system comprises the following primary contracts: -- **`TimelockedGovernance.sol`**: A singleton contract that serves as the interface for submitting and scheduling the execution of governance proposals. -- **[`EmergencyProtectedTimelock.sol`]**: A singleton contract responsible for storing submitted proposals and providing an interface for their execution. It offers protection against malicious proposals submitted by the DAO, implemented as a timelock on proposal execution. This protection is enforced through the cooperation of two emergency committees that can suspend proposal execution. -- [`EmergencyProtectedTimelock.sol`]() A singleton contract that stores submitted proposals and provides an execution interface. In addition, it implements an optional protection from a malicious proposals submitted by the DAO. The protection is implemented as a timelock on proposal execution combined with two emergency committees that have the right to cooperate and suspend the execution of the proposals. -- **[`Executor.sol`]**: A contract instance responsible for executing calls resulting from governance proposals. All protocol permissions or roles protected by TG, as well as the authority to manage these roles/permissions, should be assigned exclusively to instance of this contract, rather than being assigned directly to the DAO voting system. -- **[`EmergencyActivationCommittee`]**: A contract with the authority to activate Emergency Mode. Activation requires a quorum from committee members. -- **[`EmergencyExecutionCommittee`]**: A contract that enables the execution of proposals during Emergency Mode by obtaining a quorum of committee members. +- [**`TimelockedGovernance.sol`**](#contract-timelockedgovernance): A singleton contract that serves as the interface for submitting and scheduling the execution of governance proposals. +- [**`EmergencyProtectedTimelock.sol`**](#contract-emergencyprotectedtimelock): A singleton contract that stores submitted proposals and provides an execution interface. In addition, it implements an optional protection from a malicious proposals submitted by the DAO. The protection is implemented as a timelock on proposal execution combined with two emergency committees that have the right to cooperate and suspend the execution of the proposals. +- [**`Executor.sol`**](#contract-executor): A contract instance responsible for executing calls resulting from governance proposals. All protocol permissions or roles protected by TG, as well as the authority to manage these roles/permissions, should be assigned exclusively to instance of this contract, rather than being assigned directly to the DAO voting system. + +Additionally, the system uses several committee contracts that allow members to execute, acquiring quorum, a narrow set of actions: + +- [**`EmergencyActivationCommittee`**](#contract-emergencyactivationcommittee): A contract with the authority to activate Emergency Mode. Activation requires a quorum from committee members. +- [**`EmergencyExecutionCommittee`**](#contract-emergencyexecutioncommittee): A contract that enables the execution of proposals during Emergency Mode by obtaining a quorum of committee members. ## Proposal flow image @@ -52,10 +55,10 @@ If emergency protection is enabled on the `EmergencyProtectedTimelock` instance, ## Common types -### Struct: ExecutorCall +### Struct: ExternalCall ```solidity -struct ExecutorCall { +struct ExternalCall { address target; uint96 value; bytes payload; @@ -70,11 +73,11 @@ The main entry point to the timelocked governance system, which provides an inte ### Function: `TimelockedGovernance.submitProposal` ```solidity -function submitProposal(ExecutorCall[] calls) +function submitProposal(ExecutorCall[] calls, string metadata) returns (uint256 proposalId) ``` -Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance to register a new governance proposal composed of one or more external `calls` to be made by an admin executor contract. Initiates a timelock on scheduling the proposal for execution. +Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance to register a new governance proposal composed of one or more external `calls`, along with the attached metadata text, to be made by an admin executor contract. Initiates a timelock on scheduling the proposal for execution. See: [`EmergencyProtectedTimelock.submit`](#) #### Returns @@ -118,9 +121,9 @@ See: [`EmergencyProtectedTimelock.cancelAllNonExecutedProposals`](#) 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 (`Configuration.AFTER_SUBMIT_DELAY()`) must elapse. +2. The configured post-submit timelock (`EmergencyProtectedTimelock.getAfterSubmitDelay()`) must elapse. 3. The proposal must be scheduled using the `EmergencyProtectedTimelock.schedule()` function. -4. The configured emergency protection delay (`Configuration.AFTER_SCHEDULE_DELAY()`) must elapse (can be zero, see below). +4. The configured emergency protection delay (`Configuration.getAfterScheduleDelay()`) 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 [`TimelockedGovernance`](#Contract-TimelockedGovernancesol) singleton instance. Proposal execution is permissionless, unless Emergency Mode is activated. @@ -132,7 +135,7 @@ If the Emergency Committees are set up and active, the governance proposal under While active, the Emergency Activation Committee can enable Emergency Mode. This mode prohibits anyone but the Emergency Execution Committee from executing proposals. Once the **Emergency Duration** has ended, the Emergency Execution Committee or anyone else may disable the emergency mode, canceling all pending proposals. After the emergency mode is deactivated or the Emergency Period has elapsed, the Emergency Committees lose their power. ### Function: `EmergencyProtectedTimelock.submit` ```solidity -function submit(address executor, ExecutorCall[] calls) +function submit(address executor, ExecutorCall[] calls, string metadata) returns (uint256 proposalId) ``` Registers a new governance proposal composed of one or more external `calls` to be made by the `executor` contract. Initiates the `AfterSubmitDelay`. @@ -204,7 +207,7 @@ Resets the `governance` address to the `EMERGENCY_GOVERNANCE` value defined in t * MUST be called by the Emergency Execution Committee address. ### Admin functions -The contract includes functions for managing emergency protection configuration (`setEmergencyProtection`) and general system wiring (`transferExecutorOwnership`, `setGovernance`). These functions MUST be called by the [Admin Executor](#) address. +The contract has the interface for managing the configuration related to emergency protection (`setEmergencyProtectionActivationCommittee`, `setEmergencyProtectionExecutionCommittee`, `setEmergencyProtectionEndDate`, `setEmergencyModeDuration`, `setEmergencyGovernance`) and general system wiring (`transferExecutorOwnership`, `setGovernance`, `setupDelays`). These functions MUST be called by the [Admin Executor](#) address. ## Contract: `Executor` Executes calls resulting from governance proposals' execution. Every protocol permission or role protected by the TG, as well as the permission to manage these roles/permissions, should be assigned exclusively to instances of this contract. @@ -431,4 +434,3 @@ Executes the governance reset by calling the `emergencyReset` function on the `E #### Preconditions - The governance reset proposal MUST have reached quorum and passed the timelock duration. - From 2f4fa3b79751047648c74f1ac7b686260370c247 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 12 Sep 2024 13:01:12 +0400 Subject: [PATCH 66/86] Fix section formatting in specification.md doc --- docs/specification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specification.md b/docs/specification.md index 302c4aff..3549cd5c 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -31,7 +31,7 @@ This document provides the system description on the code architecture level. A * [Contract: Escrow.sol](#contract-escrowsol) * [Contract: ImmutableDualGovernanceConfigProvider.sol](#contract-immutabledualgovernanceconfigprovidersol) * [Contract: ResealManager.sol](#contract-resealmanagersol) -* Committees +* Committees: * [Contract: ProposalsList.sol](#contract-proposalslistsol) * [Contract: HashConsensus.sol](#contract-hashconsensussol) * [Contract: ResealCommittee.sol](#contract-resealcommitteesol) From 629bc07e661ab6d91a3b2d41ec70d50d299bbded Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 12 Sep 2024 13:39:35 +0400 Subject: [PATCH 67/86] DualGovernanceConfigProvider.sol -> ImmutableDualGovernanceConfigProvider.sol --- ...rovider.sol => ImmutableDualGovernanceConfigProvider.sol} | 0 test/unit/DualGovernance.t.sol | 2 +- test/unit/libraries/DualGovernanceStateMachine.t.sol | 5 ++++- test/utils/SetupDeployment.sol | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) rename contracts/{DualGovernanceConfigProvider.sol => ImmutableDualGovernanceConfigProvider.sol} (100%) diff --git a/contracts/DualGovernanceConfigProvider.sol b/contracts/ImmutableDualGovernanceConfigProvider.sol similarity index 100% rename from contracts/DualGovernanceConfigProvider.sol rename to contracts/ImmutableDualGovernanceConfigProvider.sol diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index bf3bf14d..1b5c9075 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -18,7 +18,7 @@ import { DualGovernanceConfig, IDualGovernanceConfigProvider, ImmutableDualGovernanceConfigProvider -} from "contracts/DualGovernanceConfigProvider.sol"; +} from "contracts/ImmutableDualGovernanceConfigProvider.sol"; import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; import {IWstETH} from "contracts/interfaces/IWstETH.sol"; diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index be1bbeef..7e447cae 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -7,7 +7,10 @@ import {Durations} from "contracts/types/Duration.sol"; import {PercentsD16} from "contracts/types/PercentD16.sol"; import {DualGovernanceStateMachine, State} from "contracts/libraries/DualGovernanceStateMachine.sol"; -import {DualGovernanceConfig, ImmutableDualGovernanceConfigProvider} from "contracts/DualGovernanceConfigProvider.sol"; +import { + DualGovernanceConfig, + ImmutableDualGovernanceConfigProvider +} from "contracts/ImmutableDualGovernanceConfigProvider.sol"; import {UnitTest} from "test/utils/unit-test.sol"; import {EscrowMock} from "test/mocks/EscrowMock.sol"; diff --git a/test/utils/SetupDeployment.sol b/test/utils/SetupDeployment.sol index c181fcd1..e5a1eda3 100644 --- a/test/utils/SetupDeployment.sol +++ b/test/utils/SetupDeployment.sol @@ -39,7 +39,7 @@ import { DualGovernanceConfig, IDualGovernanceConfigProvider, ImmutableDualGovernanceConfigProvider -} from "contracts/DualGovernanceConfigProvider.sol"; +} from "contracts/ImmutableDualGovernanceConfigProvider.sol"; import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; From 49fa62a2a5268858fe6ce8bec6bf7c4b88f970e9 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 12 Sep 2024 13:42:04 +0400 Subject: [PATCH 68/86] AssetsAccounting.accountClaimedStETH() -> AssetssAccounting.accountClaimedETH() --- contracts/Escrow.sol | 2 +- contracts/libraries/AssetsAccounting.sol | 2 +- test/unit/libraries/AssetsAccounting.t.sol | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index adec929b..673eae62 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -480,7 +480,7 @@ contract Escrow is IEscrow { WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); ETHValue ethBalanceAfter = ETHValues.fromAddressBalance(address(this)); - _accounting.accountClaimedStETH(ethBalanceAfter - ethBalanceBefore); + _accounting.accountClaimedETH(ethBalanceAfter - ethBalanceBefore); } function _checkCallerIsDualGovernance() internal view { diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index 32872bd7..c47af26c 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -173,7 +173,7 @@ library AssetsAccounting { emit ETHWithdrawn(holder, stETHSharesToWithdraw, ethWithdrawn); } - function accountClaimedStETH(Context storage self, ETHValue amount) internal { + function accountClaimedETH(Context storage self, ETHValue amount) internal { self.stETHTotals.claimedETH = self.stETHTotals.claimedETH + amount; emit ETHClaimed(amount); } diff --git a/test/unit/libraries/AssetsAccounting.t.sol b/test/unit/libraries/AssetsAccounting.t.sol index b624ed33..27c3642d 100644 --- a/test/unit/libraries/AssetsAccounting.t.sol +++ b/test/unit/libraries/AssetsAccounting.t.sol @@ -306,10 +306,10 @@ contract AssetsAccountingUnitTests is UnitTest { } // --- - // accountClaimedStETH + // accountClaimedETH // --- - function testFuzz_accountClaimedStETH_happyPath(ETHValue amount, ETHValue totalClaimedETH) external { + function testFuzz_accountClaimedETH_happyPath(ETHValue amount, ETHValue totalClaimedETH) external { vm.assume(amount.toUint256() < type(uint128).max / 2); vm.assume(totalClaimedETH.toUint256() < type(uint128).max / 2); @@ -318,7 +318,7 @@ contract AssetsAccountingUnitTests is UnitTest { vm.expectEmit(); emit AssetsAccounting.ETHClaimed(amount); - AssetsAccounting.accountClaimedStETH(_accountingContext, amount); + AssetsAccounting.accountClaimedETH(_accountingContext, amount); checkAccountingContextTotalCounters( SharesValues.ZERO, totalClaimedETH + amount, SharesValues.ZERO, ETHValues.ZERO From 92039e1611d591b2fbf3a8c4dfa68b644486eb73 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 12 Sep 2024 13:52:42 +0400 Subject: [PATCH 69/86] TiebreakerCore -> TiebreakerCoreCommittee --- ...erCore.sol => TiebreakerCoreCommittee.sol} | 6 ++-- .../committees/TiebreakerSubCommittee.sol | 18 +++++----- ...rCore.sol => ITiebreakerCoreCommittee.sol} | 2 +- docs/specification.md | 36 +++++++++---------- test/unit/committees/TiebreakerCore.t.sol | 18 ++++++---- .../committees/TiebreakerSubCommittee.t.sol | 25 +++++++------ test/utils/SetupDeployment.sol | 12 +++---- 7 files changed, 64 insertions(+), 53 deletions(-) rename contracts/committees/{TiebreakerCore.sol => TiebreakerCoreCommittee.sol} (96%) rename contracts/interfaces/{ITiebreakerCore.sol => ITiebreakerCoreCommittee.sol} (90%) diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCoreCommittee.sol similarity index 96% rename from contracts/committees/TiebreakerCore.sol rename to contracts/committees/TiebreakerCoreCommittee.sol index ac0cce57..11a97009 100644 --- a/contracts/committees/TiebreakerCore.sol +++ b/contracts/committees/TiebreakerCoreCommittee.sol @@ -8,8 +8,8 @@ import {Timestamp} from "../types/Timestamp.sol"; import {ITimelock} from "../interfaces/ITimelock.sol"; import {ITiebreaker} from "../interfaces/ITiebreaker.sol"; -import {ITiebreakerCore} from "../interfaces/ITiebreakerCore.sol"; import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; +import {ITiebreakerCoreCommittee} from "../interfaces/ITiebreakerCoreCommittee.sol"; import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; @@ -19,10 +19,10 @@ enum ProposalType { ResumeSealable } -/// @title Tiebreaker Core Contract +/// @title Tiebreaker Core Committee Contract /// @notice This contract allows a committee to vote on and execute proposals for scheduling and resuming sealable addresses /// @dev Inherits from HashConsensus for voting mechanisms and ProposalsList for proposal management -contract TiebreakerCore is ITiebreakerCore, HashConsensus, ProposalsList { +contract TiebreakerCoreCommittee is ITiebreakerCoreCommittee, HashConsensus, ProposalsList { error ResumeSealableNonceMismatch(); error ProposalDoesNotExist(uint256 proposalId); diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol index 01dabf8f..4e6e0dc6 100644 --- a/contracts/committees/TiebreakerSubCommittee.sol +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -6,7 +6,7 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Durations} from "../types/Duration.sol"; import {Timestamp} from "../types/Timestamp.sol"; -import {ITiebreakerCore} from "../interfaces/ITiebreakerCore.sol"; +import {ITiebreakerCoreCommittee} from "../interfaces/ITiebreakerCoreCommittee.sol"; import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; @@ -20,15 +20,15 @@ enum ProposalType { /// @notice This contract allows a subcommittee to vote on and execute proposals for scheduling and resuming sealable addresses /// @dev Inherits from HashConsensus for voting mechanisms and ProposalsList for proposal management contract TiebreakerSubCommittee is HashConsensus, ProposalsList { - address immutable TIEBREAKER_CORE; + address public immutable TIEBREAKER_CORE_COMMITTEE; constructor( address owner, address[] memory committeeMembers, uint256 executionQuorum, - address tiebreakerCore + address tiebreakerCoreCommittee ) HashConsensus(owner, Durations.from(0)) { - TIEBREAKER_CORE = tiebreakerCore; + TIEBREAKER_CORE_COMMITTEE = tiebreakerCoreCommittee; _addMembers(committeeMembers, executionQuorum); } @@ -42,7 +42,7 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { /// @param proposalId The ID of the proposal to schedule function scheduleProposal(uint256 proposalId) public { _checkCallerIsMember(); - ITiebreakerCore(TIEBREAKER_CORE).checkProposalExists(proposalId); + ITiebreakerCoreCommittee(TIEBREAKER_CORE_COMMITTEE).checkProposalExists(proposalId); (bytes memory proposalData, bytes32 key) = _encodeApproveProposal(proposalId); _vote(key, true); _pushProposal(key, uint256(ProposalType.ScheduleProposal), proposalData); @@ -71,7 +71,8 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { (, bytes32 key) = _encodeApproveProposal(proposalId); _markUsed(key); Address.functionCall( - TIEBREAKER_CORE, abi.encodeWithSelector(ITiebreakerCore.scheduleProposal.selector, proposalId) + TIEBREAKER_CORE_COMMITTEE, + abi.encodeWithSelector(ITiebreakerCoreCommittee.scheduleProposal.selector, proposalId) ); } @@ -122,7 +123,8 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { (, bytes32 key, uint256 nonce) = _encodeSealableResume(sealable); _markUsed(key); Address.functionCall( - TIEBREAKER_CORE, abi.encodeWithSelector(ITiebreakerCore.sealableResume.selector, sealable, nonce) + TIEBREAKER_CORE_COMMITTEE, + abi.encodeWithSelector(ITiebreakerCoreCommittee.sealableResume.selector, sealable, nonce) ); } @@ -137,7 +139,7 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { view returns (bytes memory data, bytes32 key, uint256 nonce) { - nonce = ITiebreakerCore(TIEBREAKER_CORE).getSealableResumeNonce(sealable); + nonce = ITiebreakerCoreCommittee(TIEBREAKER_CORE_COMMITTEE).getSealableResumeNonce(sealable); data = abi.encode(sealable, nonce); key = keccak256(data); } diff --git a/contracts/interfaces/ITiebreakerCore.sol b/contracts/interfaces/ITiebreakerCoreCommittee.sol similarity index 90% rename from contracts/interfaces/ITiebreakerCore.sol rename to contracts/interfaces/ITiebreakerCoreCommittee.sol index 44af6301..9bfb2a09 100644 --- a/contracts/interfaces/ITiebreakerCore.sol +++ b/contracts/interfaces/ITiebreakerCoreCommittee.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -interface ITiebreakerCore { +interface ITiebreakerCoreCommittee { function getSealableResumeNonce(address sealable) external view returns (uint256 nonce); function scheduleProposal(uint256 _proposalId) external; function sealableResume(address sealable, uint256 nonce) external; diff --git a/docs/specification.md b/docs/specification.md index 3549cd5c..53628a42 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 * [Contract: ProposalsList.sol](#contract-proposalslistsol) * [Contract: HashConsensus.sol](#contract-hashconsensussol) * [Contract: ResealCommittee.sol](#contract-resealcommitteesol) - * [Contract: TiebreakerCore.sol](#contract-tiebreakercoresol) + * [Contract: TiebreakerCoreCommittee.sol](#contract-tiebreakercorecommitteesol) * [Contract: TiebreakerSubCommittee.sol](#contract-tiebreakersubcommitteesol) * [Contract: EmergencyActivationCommittee.sol](#contract-emergencyactivationcommitteesol) * [Contract: EmergencyExecutionCommittee.sol](#contract-emergencyexecutioncommitteesol) @@ -56,12 +56,12 @@ The system is composed of the following main contracts: * [`ImmutableDualGovernanceConfigProvider.sol`](#contract-immutabledualgovernanceconfigprovidersol) is a singleton contract that stores the configurable parameters of the DualGovernance system in an immutable manner. * [`ResealManager.sol`](#contract-resealmanagersol) is a singleton contract responsible for extending or resuming sealable contracts paused by the [GateSeal emergency protection mechanism](https://github.com/lidofinance/gate-seals). This contract is essential due to the dynamic timelock of Dual Governance, which may prevent the DAO from extending the pause in time. It holds the authority to manage the pausing and resuming of specific protocol components protected by GateSeal. -Additionally, the system uses several committee contracts that allow members to execute, acquiring quorum, a narrow set of actions while protecting management of the committees by the Dual Governance mechanism: - +Additionally, the system uses several committee contracts that allow members to execute, acquiring quorum, a narrow set of actions while protecting management of the committees by the Dual Governance mechanism: + * [`ResealCommittee.sol`](#contract-resealcommitteesol) is a committee contract that allows members to obtain a quorum and reseal contracts temporarily paused by the [GateSeal emergency protection mechanism](https://github.com/lidofinance/gate-seals). -* [`TiebreakerCore.sol`](#contract-tiebreakercoresol) is a committee contract designed to approve proposals for execution in extreme situations where the Dual Governance system is deadlocked. This includes scenarios such as the inability to finalize user withdrawal requests during ongoing `RageQuit` or when the system is held in a locked state for an extended period. The `TiebreakerCore` consists of multiple `TiebreakerSubCommittee` contracts appointed by the DAO. -* [`TiebreakerSubCommittee.sol`](#contract-tiebreakersubcommitteesol) is a committee contracts that provides ability to participate in `TiebreakerCore` for external actors. -* [`EmergencyActivationCommittee`](#contract-emergencyactivationcommitteesol) is a committee contract responsible for activating Emergency Mode by acquiring quorum. Only the EmergencyExecutionCommittee can execute proposals. This committee is expected to be active for a limited period following the initial deployment or update of the DualGovernance system. +* [`TiebreakerCoreCommittee.sol`](#contract-tiebreakercorecommitteesol) is a committee contract designed to approve proposals for execution in extreme situations where the Dual Governance system is deadlocked. This includes scenarios such as the inability to finalize user withdrawal requests during ongoing `RageQuit` or when the system is held in a locked state for an extended period. The `TiebreakerCoreCommittee` consists of multiple `TiebreakerSubCommittee` contracts appointed by the DAO. +* [`TiebreakerSubCommittee.sol`](#contract-tiebreakersubcommitteesol) is a committee contracts that provides ability to participate in `TiebreakerCoreCommittee` for external actors. +* [`EmergencyActivationCommittee`](#contract-emergencyactivationcommitteesol) is a committee contract responsible for activating Emergency Mode by acquiring quorum. Only the EmergencyExecutionCommittee can execute proposals. This committee is expected to be active for a limited period following the initial deployment or update of the DualGovernance system. * [`EmergencyExecutionCommittee`](#contract-emergencyexecutioncommitteesol) is a committee contract that enables quorum-based execution of proposals during Emergency Mode or disabling the DualGovernance mechanism by assigning the EmergencyProtectedTimelock to Aragon Voting. Like the EmergencyActivationCommittee, this committee is also intended for short-term use after the system’s deployment or update. @@ -1167,11 +1167,11 @@ Executes a reseal of the sealable contract by calling the `resealSealable` metho * Proposal MUST be scheduled for execution and passed the timelock duration. -## Contract: TiebreakerCore.sol +## Contract: TiebreakerCoreCommittee.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. +`TiebreakerCoreCommittee` 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. -### Function: TiebreakerCore.scheduleProposal +### Function: TiebreakerCoreCommittee.scheduleProposal ```solidity function scheduleProposal(uint256 proposalId) @@ -1184,7 +1184,7 @@ Schedules a proposal for execution by voting on it and adding it to the proposal * MUST be called by a member. * Proposal with the given id MUST be submitted into `EmergencyProtectedTimelock` -### Function: TiebreakerCore.getScheduleProposalState +### Function: TiebreakerCoreCommittee.getScheduleProposalState ```solidity function getScheduleProposalState(uint256 proposalId) @@ -1194,7 +1194,7 @@ function getScheduleProposalState(uint256 proposalId) Returns the state of a scheduled proposal including support count, quorum, and execution status. -### Function: TiebreakerCore.executeScheduleProposal +### Function: TiebreakerCoreCommittee.executeScheduleProposal ```solidity function executeScheduleProposal(uint256 proposalId) @@ -1206,7 +1206,7 @@ Executes a scheduled proposal by calling the `tiebreakerScheduleProposal` functi * Proposal MUST be scheduled for execution and passed the timelock duration. -### Function: TiebreakerCore.getSealableResumeNonce +### Function: TiebreakerCoreCommittee.getSealableResumeNonce ```solidity function getSealableResumeNonce(address sealable) view returns (uint256) @@ -1214,7 +1214,7 @@ function getSealableResumeNonce(address sealable) view returns (uint256) Returns the current nonce for resuming operations of a sealable contract. -### Function: TiebreakerCore.sealableResume +### Function: TiebreakerCoreCommittee.sealableResume ```solidity function sealableResume(address sealable, uint256 nonce) @@ -1227,7 +1227,7 @@ Submits a request to resume operations of a sealable contract by voting on it an * MUST be called by a member. * The provided nonce MUST match the current nonce of the sealable contract. -### Function: TiebreakerCore.getSealableResumeState +### Function: TiebreakerCoreCommittee.getSealableResumeState ```solidity function getSealableResumeState(address sealable, uint256 nonce) @@ -1237,7 +1237,7 @@ function getSealableResumeState(address sealable, uint256 nonce) Returns the state of a sealable resume request including support count, quorum, and execution status. -### Function: TiebreakerCore.executeSealableResume +### Function: TiebreakerCoreCommittee.executeSealableResume ```solidity function executeSealableResume(address sealable) @@ -1251,7 +1251,7 @@ Executes a sealable resume request by calling the `tiebreakerResumeSealable` fun ## 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. +`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 `TiebreakerCoreCommittee` contract to execute decisions once consensus is reached. ### Function: TiebreakerSubCommittee.scheduleProposal @@ -1282,7 +1282,7 @@ Returns the state of a scheduled proposal including support count, quorum, and e function executeScheduleProposal(uint256 proposalId) ``` -Executes a scheduled proposal by calling the scheduleProposal function on the TiebreakerCore contract. +Executes a scheduled proposal by calling the scheduleProposal function on the `TiebreakerCoreCommittee` contract. #### Preconditions @@ -1314,7 +1314,7 @@ Returns the state of a sealable resume request including support count, quorum, function executeSealableResume(address sealable) public ``` -Executes a sealable resume request by calling the sealableResume function on the TiebreakerCore contract and increments the nonce. +Executes a sealable resume request by calling the sealableResume function on the `TiebreakerCoreCommittee` contract and increments the nonce. #### Preconditions diff --git a/test/unit/committees/TiebreakerCore.t.sol b/test/unit/committees/TiebreakerCore.t.sol index d950b835..e956957c 100644 --- a/test/unit/committees/TiebreakerCore.t.sol +++ b/test/unit/committees/TiebreakerCore.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; +import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; import {HashConsensus} from "contracts/committees/HashConsensus.sol"; import {Durations, Duration} from "contracts/types/Duration.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; @@ -33,7 +33,7 @@ contract EmergencyProtectedTimelockMock is TargetMock { } contract TiebreakerCoreUnitTest is UnitTest { - TiebreakerCore internal tiebreakerCore; + TiebreakerCoreCommittee internal tiebreakerCore; uint256 internal quorum = 2; address internal owner = makeAddr("owner"); address[] internal committeeMembers = [address(0x1), address(0x2), address(0x3)]; @@ -47,7 +47,7 @@ contract TiebreakerCoreUnitTest is UnitTest { emergencyProtectedTimelock = address(new EmergencyProtectedTimelockMock()); EmergencyProtectedTimelockMock(payable(emergencyProtectedTimelock)).setProposalsCount(1); dualGovernance = address(new DualGovernanceMock(emergencyProtectedTimelock)); - tiebreakerCore = new TiebreakerCore(owner, dualGovernance, timelock); + tiebreakerCore = new TiebreakerCoreCommittee(owner, dualGovernance, timelock); vm.prank(owner); tiebreakerCore.addMembers(committeeMembers, quorum); @@ -55,7 +55,7 @@ contract TiebreakerCoreUnitTest is UnitTest { function testFuzz_constructor_HappyPath(address _owner, address _dualGovernance, Duration _timelock) external { vm.assume(_owner != address(0)); - new TiebreakerCore(_owner, _dualGovernance, _timelock); + new TiebreakerCoreCommittee(_owner, _dualGovernance, _timelock); } function test_scheduleProposal_HappyPath() external { @@ -87,7 +87,9 @@ contract TiebreakerCoreUnitTest is UnitTest { function test_scheduleProposal_RevertOn_ProposalDoesNotExist() external { uint256 nonExistentProposalId = proposalId + 1; - vm.expectRevert(abi.encodeWithSelector(TiebreakerCore.ProposalDoesNotExist.selector, nonExistentProposalId)); + vm.expectRevert( + abi.encodeWithSelector(TiebreakerCoreCommittee.ProposalDoesNotExist.selector, nonExistentProposalId) + ); vm.prank(committeeMembers[0]); tiebreakerCore.scheduleProposal(nonExistentProposalId); } @@ -95,7 +97,9 @@ contract TiebreakerCoreUnitTest is UnitTest { function test_scheduleProposal_RevertOn_ProposalIdIsZero() external { uint256 nonExistentProposalId = 0; - vm.expectRevert(abi.encodeWithSelector(TiebreakerCore.ProposalDoesNotExist.selector, nonExistentProposalId)); + vm.expectRevert( + abi.encodeWithSelector(TiebreakerCoreCommittee.ProposalDoesNotExist.selector, nonExistentProposalId) + ); vm.prank(committeeMembers[0]); tiebreakerCore.scheduleProposal(nonExistentProposalId); } @@ -142,7 +146,7 @@ contract TiebreakerCoreUnitTest is UnitTest { uint256 wrongNonce = 999; vm.prank(committeeMembers[0]); - vm.expectRevert(abi.encodeWithSelector(TiebreakerCore.ResumeSealableNonceMismatch.selector)); + vm.expectRevert(abi.encodeWithSelector(TiebreakerCoreCommittee.ResumeSealableNonceMismatch.selector)); tiebreakerCore.sealableResume(sealable, wrongNonce); } diff --git a/test/unit/committees/TiebreakerSubCommittee.t.sol b/test/unit/committees/TiebreakerSubCommittee.t.sol index 81c3d871..774ae27c 100644 --- a/test/unit/committees/TiebreakerSubCommittee.t.sol +++ b/test/unit/committees/TiebreakerSubCommittee.t.sol @@ -1,14 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {ITiebreakerCore} from "contracts/interfaces/ITiebreakerCore.sol"; +import {ITiebreakerCoreCommittee} from "contracts/interfaces/ITiebreakerCoreCommittee.sol"; -import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; +import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; import {TiebreakerSubCommittee, ProposalType} from "contracts/committees/TiebreakerSubCommittee.sol"; import {HashConsensus} from "contracts/committees/HashConsensus.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; import {UnitTest} from "test/utils/unit-test.sol"; -import {ITiebreakerCore} from "contracts/interfaces/ITiebreakerCore.sol"; import {TargetMock} from "test/utils/target-mock.sol"; @@ -81,7 +80,9 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest { tiebreakerSubCommittee.scheduleProposal(proposalId); vm.prank(committeeMembers[2]); - vm.expectCall(tiebreakerCore, abi.encodeWithSelector(ITiebreakerCore.scheduleProposal.selector, proposalId)); + vm.expectCall( + tiebreakerCore, abi.encodeWithSelector(ITiebreakerCoreCommittee.scheduleProposal.selector, proposalId) + ); tiebreakerSubCommittee.executeScheduleProposal(proposalId); (,,, bool isExecuted) = tiebreakerSubCommittee.getScheduleProposalState(proposalId); @@ -105,7 +106,9 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest { function test_scheduleProposal_RevertOn_ProposalDoesNotExist() external { uint256 nonExistentProposalId = proposalId + 1; - vm.expectRevert(abi.encodeWithSelector(TiebreakerCore.ProposalDoesNotExist.selector, nonExistentProposalId)); + vm.expectRevert( + abi.encodeWithSelector(TiebreakerCoreCommittee.ProposalDoesNotExist.selector, nonExistentProposalId) + ); vm.prank(committeeMembers[0]); tiebreakerSubCommittee.scheduleProposal(nonExistentProposalId); } @@ -113,7 +116,7 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest { function test_sealableResume_HappyPath() external { vm.mockCall( tiebreakerCore, - abi.encodeWithSelector(ITiebreakerCore.getSealableResumeNonce.selector, sealable), + abi.encodeWithSelector(ITiebreakerCoreCommittee.getSealableResumeNonce.selector, sealable), abi.encode(0) ); @@ -145,7 +148,7 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest { function test_executeSealableResume_HappyPath() external { vm.mockCall( tiebreakerCore, - abi.encodeWithSelector(ITiebreakerCore.getSealableResumeNonce.selector, sealable), + abi.encodeWithSelector(ITiebreakerCoreCommittee.getSealableResumeNonce.selector, sealable), abi.encode(0) ); @@ -155,7 +158,9 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest { tiebreakerSubCommittee.sealableResume(sealable); vm.prank(committeeMembers[2]); - vm.expectCall(tiebreakerCore, abi.encodeWithSelector(ITiebreakerCore.sealableResume.selector, sealable, 0)); + vm.expectCall( + tiebreakerCore, abi.encodeWithSelector(ITiebreakerCoreCommittee.sealableResume.selector, sealable, 0) + ); tiebreakerSubCommittee.executeSealableResume(sealable); (,,, bool isExecuted) = tiebreakerSubCommittee.getSealableResumeState(sealable); @@ -165,7 +170,7 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest { function test_executeSealableResume_RevertOn_QuorumNotReached() external { vm.mockCall( tiebreakerCore, - abi.encodeWithSelector(ITiebreakerCore.getSealableResumeNonce.selector, sealable), + abi.encodeWithSelector(ITiebreakerCoreCommittee.getSealableResumeNonce.selector, sealable), abi.encode(0) ); @@ -220,7 +225,7 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest { function test_getSealableResumeState_HappyPath() external { vm.mockCall( tiebreakerCore, - abi.encodeWithSelector(ITiebreakerCore.getSealableResumeNonce.selector, sealable), + abi.encodeWithSelector(ITiebreakerCoreCommittee.getSealableResumeNonce.selector, sealable), abi.encode(0) ); diff --git a/test/utils/SetupDeployment.sol b/test/utils/SetupDeployment.sol index e5a1eda3..28e39256 100644 --- a/test/utils/SetupDeployment.sol +++ b/test/utils/SetupDeployment.sol @@ -42,7 +42,7 @@ import { } from "contracts/ImmutableDualGovernanceConfigProvider.sol"; import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; -import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; +import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; // --- // Util Libraries @@ -120,7 +120,7 @@ abstract contract SetupDeployment is Test { ImmutableDualGovernanceConfigProvider internal _dualGovernanceConfigProvider; ResealCommittee internal _resealCommittee; - TiebreakerCore internal _tiebreakerCoreCommittee; + TiebreakerCoreCommittee internal _tiebreakerCoreCommittee; TiebreakerSubCommittee[] internal _tiebreakerSubCommittees; // --- @@ -406,21 +406,21 @@ abstract contract SetupDeployment is Test { address owner, IDualGovernance dualGovernance, Duration timelock - ) internal returns (TiebreakerCore) { - return new TiebreakerCore({owner: owner, dualGovernance: address(dualGovernance), timelock: timelock}); + ) internal returns (TiebreakerCoreCommittee) { + return new TiebreakerCoreCommittee({owner: owner, dualGovernance: address(dualGovernance), timelock: timelock}); } function _deployTiebreakerSubCommittee( address owner, uint256 quorum, address[] memory members, - TiebreakerCore tiebreakerCore + TiebreakerCoreCommittee tiebreakerCore ) internal returns (TiebreakerSubCommittee) { return new TiebreakerSubCommittee({ owner: owner, executionQuorum: quorum, committeeMembers: members, - tiebreakerCore: address(tiebreakerCore) + tiebreakerCoreCommittee: address(tiebreakerCore) }); } From 31102bcff79583eebbc69800b901cb76cd67c908 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 12 Sep 2024 13:55:07 +0400 Subject: [PATCH 70/86] Use Durations.ZERO constant inside committees inside Durations.from(0) --- contracts/committees/EmergencyActivationCommittee.sol | 2 +- contracts/committees/EmergencyExecutionCommittee.sol | 2 +- contracts/committees/TiebreakerSubCommittee.sol | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/committees/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol index 34d10e98..dce5ba81 100644 --- a/contracts/committees/EmergencyActivationCommittee.sol +++ b/contracts/committees/EmergencyActivationCommittee.sol @@ -23,7 +23,7 @@ contract EmergencyActivationCommittee is HashConsensus { address[] memory committeeMembers, uint256 executionQuorum, address emergencyProtectedTimelock - ) HashConsensus(owner, Durations.from(0)) { + ) HashConsensus(owner, Durations.ZERO) { EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; _addMembers(committeeMembers, executionQuorum); diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol index 719ca409..72d03dc2 100644 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -29,7 +29,7 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { address[] memory committeeMembers, uint256 executionQuorum, address emergencyProtectedTimelock - ) HashConsensus(owner, Durations.from(0)) { + ) HashConsensus(owner, Durations.ZERO) { EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; _addMembers(committeeMembers, executionQuorum); diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol index 4e6e0dc6..f69be1b7 100644 --- a/contracts/committees/TiebreakerSubCommittee.sol +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -27,7 +27,7 @@ contract TiebreakerSubCommittee is HashConsensus, ProposalsList { address[] memory committeeMembers, uint256 executionQuorum, address tiebreakerCoreCommittee - ) HashConsensus(owner, Durations.from(0)) { + ) HashConsensus(owner, Durations.ZERO) { TIEBREAKER_CORE_COMMITTEE = tiebreakerCoreCommittee; _addMembers(committeeMembers, executionQuorum); From fb061c00d4862ebda1a5fcd6d1d9ddd1a7bcf40d Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Sun, 15 Sep 2024 21:50:02 +0400 Subject: [PATCH 71/86] Call activateNextState twice on all signalling escrow operations --- contracts/Escrow.sol | 14 +++++++++++++- test/scenario/escrow.t.sol | 15 +++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 25cc518a..9c24f542 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -53,7 +53,6 @@ contract Escrow is IEscrow { error UnfinalizedUnstETHIds(); error NonProxyCallsForbidden(); error BatchesQueueIsNotClosed(); - error PendingRageQuitTransition(); error EmptyUnstETHIds(); error InvalidBatchSize(uint256 size); error CallerIsNotDualGovernance(address caller); @@ -135,6 +134,7 @@ contract Escrow is IEscrow { // --- function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { + DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); lockedStETHShares = ST_ETH.getSharesByPooledEth(amount); @@ -145,6 +145,7 @@ contract Escrow is IEscrow { } function unlockStETH() external returns (uint256 unlockedStETHShares) { + DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); @@ -159,6 +160,7 @@ contract Escrow is IEscrow { // --- function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { + DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); WST_ETH.transferFrom(msg.sender, address(this), amount); @@ -169,6 +171,7 @@ contract Escrow is IEscrow { } function unlockWstETH() external returns (uint256 unlockedStETHShares) { + DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); @@ -182,10 +185,12 @@ contract Escrow is IEscrow { // --- // Lock & unlock unstETH // --- + function lockUnstETH(uint256[] memory unstETHIds) external { if (unstETHIds.length == 0) { revert EmptyUnstETHIds(); } + DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); @@ -199,6 +204,7 @@ contract Escrow is IEscrow { } function unlockUnstETH(uint256[] memory unstETHIds) external { + DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); _accounting.checkMinAssetsLockDurationPassed(msg.sender, _escrowState.minAssetsLockDuration); @@ -212,10 +218,12 @@ contract Escrow is IEscrow { } function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external { + DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints); _accounting.accountUnstETHFinalized(unstETHIds, claimableAmounts); + DUAL_GOVERNANCE.activateNextState(); } // --- @@ -223,6 +231,7 @@ contract Escrow is IEscrow { // --- function requestWithdrawals(uint256[] calldata stETHAmounts) external returns (uint256[] memory unstETHIds) { + DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(stETHAmounts, address(this)); @@ -234,6 +243,9 @@ contract Escrow is IEscrow { } _accounting.accountStETHSharesUnlock(msg.sender, SharesValues.from(sharesTotal)); _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); + + /// @dev Skip calling activateNextState here to save gas, as converting stETH to unstETH NFTs + /// does not affect the RageQuit support. } // --- diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 461c3fa0..425bb571 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -609,18 +609,9 @@ contract EscrowHappyPath is ScenarioTestBlueprint { // Rollback the state of the node as it was before RageQuit activation vm.revertTo(snapshotId); - // Vetoer may unlock funds while the activateNextState wasn't called and the DG will - // transition into the VetoSignallingDeactivationState - _unlockStETH(_VETOER_1); - _assertVetoSignalingDeactivationState(); - - // Rollback the state of the node as it was before RageQuit activation - vm.revertTo(snapshotId); - - // While the RageQuit not started, anyone can lock stETH/wstETH/unstETH after which - // DG system will transition into RageQuit state - _lockStETH(_VETOER_2, PercentsD16.fromBasisPoints(1_00)); - _assertRageQuitState(); + // The attempt to unlock funds from Escrow will fail + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + this.externalUnlockStETH(_VETOER_1); } // --- From f3a8e9739730cad2e9786c1c251e52cc7ecac566 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Sun, 15 Sep 2024 22:30:37 +0400 Subject: [PATCH 72/86] Move configProvider in the DG state machine lib. Introduce persisted and effective DG states --- contracts/DualGovernance.sol | 101 +++++++------- .../libraries/DualGovernanceStateMachine.sol | 127 ++++++++++++++---- test/unit/DualGovernance.t.sol | 98 +++++++------- .../DualGovernanceStateMachine.t.sol | 14 +- test/utils/scenario-test-blueprint.sol | 10 +- 5 files changed, 208 insertions(+), 142 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index e3bcdb9a..5cf495a9 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -2,12 +2,14 @@ pragma solidity 0.8.26; import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; +import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; +import {IEscrow} from "./interfaces/IEscrow.sol"; import {ITimelock} from "./interfaces/ITimelock.sol"; import {ITiebreaker} from "./interfaces/ITiebreaker.sol"; -import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; import {IResealManager} from "./interfaces/IResealManager.sol"; import {IDualGovernanceConfigProvider} from "./interfaces/IDualGovernanceConfigProvider.sol"; @@ -34,7 +36,6 @@ contract DualGovernance is IDualGovernance { error UnownedAdminExecutor(); error CallerIsNotResealCommittee(address caller); error CallerIsNotAdminExecutor(address caller); - error InvalidConfigProvider(IDualGovernanceConfigProvider configProvider); error ProposalSubmissionBlocked(); error ProposalSchedulingBlocked(uint256 proposalId); error ResealIsNotAllowedInNormalState(); @@ -45,8 +46,7 @@ contract DualGovernance is IDualGovernance { event CancelAllPendingProposalsSkipped(); event CancelAllPendingProposalsExecuted(); - event EscrowMasterCopyDeployed(address escrowMasterCopy); - event ConfigProviderSet(IDualGovernanceConfigProvider newConfigProvider); + event EscrowMasterCopyDeployed(IEscrow escrowMasterCopy); event ResealCommitteeSet(address resealCommittee); // --- @@ -78,7 +78,7 @@ contract DualGovernance is IDualGovernance { ITimelock public immutable TIMELOCK; IResealManager public immutable RESEAL_MANAGER; - address public immutable ESCROW_MASTER_COPY; + IEscrow public immutable ESCROW_MASTER_COPY; // --- // Aspects @@ -91,7 +91,6 @@ contract DualGovernance is IDualGovernance { // --- // Standalone State Variables // --- - IDualGovernanceConfigProvider internal _configProvider; address internal _resealCommittee; constructor(ExternalDependencies memory dependencies, SanityCheckParams memory sanityCheckParams) { @@ -102,20 +101,17 @@ contract DualGovernance is IDualGovernance { MAX_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.maxTiebreakerActivationTimeout; MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = sanityCheckParams.maxSealableWithdrawalBlockersCount; - _setConfigProvider(dependencies.configProvider); + ESCROW_MASTER_COPY = new Escrow({ + dualGovernance: this, + stETH: dependencies.stETH, + wstETH: dependencies.wstETH, + withdrawalQueue: dependencies.withdrawalQueue, + minWithdrawalsBatchSize: sanityCheckParams.minWithdrawalsBatchSize + }); - ESCROW_MASTER_COPY = address( - new Escrow({ - dualGovernance: this, - stETH: dependencies.stETH, - wstETH: dependencies.wstETH, - withdrawalQueue: dependencies.withdrawalQueue, - minWithdrawalsBatchSize: sanityCheckParams.minWithdrawalsBatchSize - }) - ); emit EscrowMasterCopyDeployed(ESCROW_MASTER_COPY); - _stateMachine.initialize(dependencies.configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + _stateMachine.initialize(dependencies.configProvider, ESCROW_MASTER_COPY); } // --- @@ -126,8 +122,8 @@ contract DualGovernance is IDualGovernance { ExternalCall[] calldata calls, string calldata metadata ) external returns (uint256 proposalId) { - _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); - if (!_stateMachine.canSubmitProposal()) { + _stateMachine.activateNextState(ESCROW_MASTER_COPY); + if (!_stateMachine.canSubmitProposal({useEffectiveState: false})) { revert ProposalSubmissionBlocked(); } Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); @@ -135,24 +131,24 @@ contract DualGovernance is IDualGovernance { } function scheduleProposal(uint256 proposalId) external { - _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); - ITimelock.ProposalDetails memory proposalDetails = TIMELOCK.getProposalDetails(proposalId); - if (!_stateMachine.canScheduleProposal(proposalDetails.submittedAt)) { + _stateMachine.activateNextState(ESCROW_MASTER_COPY); + Timestamp proposalSubmittedAt = TIMELOCK.getProposalDetails(proposalId).submittedAt; + if (!_stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt})) { revert ProposalSchedulingBlocked(proposalId); } TIMELOCK.schedule(proposalId); } function cancelAllPendingProposals() external { - _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + _stateMachine.activateNextState(ESCROW_MASTER_COPY); Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); if (proposer.executor != TIMELOCK.getAdminExecutor()) { revert NotAdminProposer(); } - State state = _stateMachine.getState(); - if (state != State.VetoSignalling && state != State.VetoSignallingDeactivation) { + State persistedState = _stateMachine.getPersistedState(); + if (persistedState != State.VetoSignalling && persistedState != State.VetoSignallingDeactivation) { /// @dev Some proposer contracts, like Aragon Voting, may not support canceling decisions that have already /// reached consensus. This could lead to a situation where a proposer’s cancelAllPendingProposals() call /// becomes unexecutable if the Dual Governance state changes. However, it might become executable again if @@ -168,12 +164,13 @@ contract DualGovernance is IDualGovernance { } function canSubmitProposal() public view returns (bool) { - return _stateMachine.canSubmitProposal(); + return _stateMachine.canSubmitProposal({useEffectiveState: true}); } function canScheduleProposal(uint256 proposalId) external view returns (bool) { - ITimelock.ProposalDetails memory proposalDetails = TIMELOCK.getProposalDetails(proposalId); - return _stateMachine.canScheduleProposal(proposalDetails.submittedAt) && TIMELOCK.canSchedule(proposalId); + Timestamp proposalSubmittedAt = TIMELOCK.getProposalDetails(proposalId).submittedAt; + return _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) + && TIMELOCK.canSchedule(proposalId); } // --- @@ -181,22 +178,16 @@ contract DualGovernance is IDualGovernance { // --- function activateNextState() external { - _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); + _stateMachine.activateNextState(ESCROW_MASTER_COPY); } function setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) external { _checkCallerIsAdminExecutor(); - _setConfigProvider(newConfigProvider); - - /// @dev the minAssetsLockDuration is kept as a storage variable in the signalling Escrow instance - /// to sync the new value with current signalling escrow, it's value must be manually updated - _stateMachine.signallingEscrow.setMinAssetsLockDuration( - newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration - ); + _stateMachine.setConfigProvider(newConfigProvider); } function getConfigProvider() external view returns (IDualGovernanceConfigProvider) { - return _configProvider; + return _stateMachine.configProvider; } function getVetoSignallingEscrow() external view returns (address) { @@ -207,12 +198,16 @@ contract DualGovernance is IDualGovernance { return address(_stateMachine.rageQuitEscrow); } - function getState() external view returns (State state) { - state = _stateMachine.getState(); + function getPersistedState() external view returns (State state) { + state = _stateMachine.getPersistedState(); + } + + function getEffectiveState() external view returns (State state) { + state = _stateMachine.getEffectiveState(); } function getStateDetails() external view returns (IDualGovernance.StateDetails memory stateDetails) { - return _stateMachine.getStateDetails(_configProvider.getDualGovernanceConfig()); + return _stateMachine.getStateDetails(); } // --- @@ -278,21 +273,24 @@ contract DualGovernance is IDualGovernance { function tiebreakerResumeSealable(address sealable) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); - _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); - _tiebreaker.checkTie(_stateMachine.getState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); + _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); RESEAL_MANAGER.resume(sealable); } function tiebreakerScheduleProposal(uint256 proposalId) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); - _stateMachine.activateNextState(_configProvider.getDualGovernanceConfig(), ESCROW_MASTER_COPY); - _tiebreaker.checkTie(_stateMachine.getState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); + _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); TIMELOCK.schedule(proposalId); } function getTiebreakerDetails() external view returns (ITiebreaker.TiebreakerDetails memory tiebreakerState) { return _tiebreaker.getTiebreakerDetails( - _stateMachine.getState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt() + /// @dev Calling getEffectiveState() doesn't update the normalOrVetoCooldownStateExitedAt value, + /// but this does not distort the result of getTiebreakerDetails() + _stateMachine.getEffectiveState(), + _stateMachine.getNormalOrVetoCooldownStateExitedAt() ); } @@ -301,10 +299,11 @@ contract DualGovernance is IDualGovernance { // --- function resealSealable(address sealable) external { + _stateMachine.activateNextState(ESCROW_MASTER_COPY); if (msg.sender != _resealCommittee) { revert CallerIsNotResealCommittee(msg.sender); } - if (_stateMachine.getState() == State.Normal) { + if (_stateMachine.getPersistedState() == State.Normal) { revert ResealIsNotAllowedInNormalState(); } RESEAL_MANAGER.reseal(sealable); @@ -321,16 +320,6 @@ contract DualGovernance is IDualGovernance { // Private methods // --- - function _setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) internal { - if (address(newConfigProvider) == address(0) || newConfigProvider == _configProvider) { - revert InvalidConfigProvider(newConfigProvider); - } - - newConfigProvider.getDualGovernanceConfig().validate(); - _configProvider = newConfigProvider; - emit ConfigProviderSet(newConfigProvider); - } - function _checkCallerIsAdminExecutor() internal view { if (TIMELOCK.getAdminExecutor() != msg.sender) { revert CallerIsNotAdminExecutor(msg.sender); diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 12d6eb00..13f5f3df 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -10,6 +10,7 @@ import {Timestamp, Timestamps} from "../types/Timestamp.sol"; import {IEscrow} from "../interfaces/IEscrow.sol"; import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; +import {IDualGovernanceConfigProvider} from "../interfaces/IDualGovernanceConfigProvider.sol"; import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; @@ -59,19 +60,41 @@ library DualGovernanceStateMachine { /// @dev slot 1: [80..239] /// The address of the Escrow used during the last (may be ongoing) Rage Quit process IEscrow rageQuitEscrow; + /// + /// @dev slot 2: [0..159] + /// The address of the Dual Governance config provider + IDualGovernanceConfigProvider configProvider; } + // --- + // Errors + // --- + error AlreadyInitialized(); + error InvalidConfigProvider(IDualGovernanceConfigProvider configProvider); + + // --- + // Events + // --- event NewSignallingEscrowDeployed(IEscrow indexed escrow); event DualGovernanceStateChanged(State from, State to, Context state); + event ConfigProviderSet(IDualGovernanceConfigProvider newConfigProvider); + + // --- + // Constants + // --- uint256 internal constant MAX_RAGE_QUIT_ROUND = type(uint8).max; + // --- + // Main functionality + // --- + function initialize( Context storage self, - DualGovernanceConfig.Context memory config, - address escrowMasterCopy + IDualGovernanceConfigProvider configProvider, + IEscrow escrowMasterCopy ) internal { if (self.state != State.Unset) { revert AlreadyInitialized(); @@ -79,16 +102,17 @@ library DualGovernanceStateMachine { self.state = State.Normal; self.enteredAt = Timestamps.now(); + + _setConfigProvider(self, configProvider); + + DualGovernanceConfig.Context memory config = configProvider.getDualGovernanceConfig(); _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); emit DualGovernanceStateChanged(State.Unset, State.Normal, self); } - function activateNextState( - Context storage self, - DualGovernanceConfig.Context memory config, - address escrowMasterCopy - ) internal { + function activateNextState(Context storage self, IEscrow escrowMasterCopy) internal { + DualGovernanceConfig.Context memory config = getDualGovernanceConfig(self); (State currentState, State newState) = self.getStateTransition(config); if (currentState == newState) { @@ -130,10 +154,26 @@ library DualGovernanceStateMachine { emit DualGovernanceStateChanged(currentState, newState, self); } - function getStateDetails( - Context storage self, - DualGovernanceConfig.Context memory config - ) internal view returns (IDualGovernance.StateDetails memory stateDetails) { + function setConfigProvider(Context storage self, IDualGovernanceConfigProvider newConfigProvider) internal { + _setConfigProvider(self, newConfigProvider); + + /// @dev minAssetsLockDuration is stored as a storage variable in the Signalling Escrow instance. + /// To synchronize the new value with the current Signalling Escrow, it must be manually updated. + self.signallingEscrow.setMinAssetsLockDuration( + newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration + ); + } + + // --- + // Getters + // --- + + function getStateDetails(Context storage self) + internal + view + returns (IDualGovernance.StateDetails memory stateDetails) + { + DualGovernanceConfig.Context memory config = getDualGovernanceConfig(self); (State currentState, State nextState) = self.getStateTransition(config); stateDetails.state = currentState; @@ -147,36 +187,71 @@ library DualGovernanceStateMachine { config.calcVetoSignallingDuration(self.signallingEscrow.getRageQuitSupport()); } - function getState(Context storage self) internal view returns (State) { - return self.state; + function getPersistedState(Context storage self) internal view returns (State persistedState) { + persistedState = self.state; + } + + function getEffectiveState(Context storage self) internal view returns (State effectiveState) { + ( /* persistedState */ , effectiveState) = self.getStateTransition(getDualGovernanceConfig(self)); } function getNormalOrVetoCooldownStateExitedAt(Context storage self) internal view returns (Timestamp) { return self.normalOrVetoCooldownExitedAt; } - function canSubmitProposal(Context storage self) internal view returns (bool) { - State state = self.state; - return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; + function canSubmitProposal(Context storage self, bool useEffectiveState) internal view returns (bool) { + State effectiveState = useEffectiveState ? getEffectiveState(self) : getPersistedState(self); + return effectiveState != State.VetoSignallingDeactivation && effectiveState != State.VetoCooldown; } - function canScheduleProposal(Context storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { - State state = self.state; - if (state == State.Normal) { - return true; - } - if (state == State.VetoCooldown) { - return proposalSubmissionTime <= self.vetoSignallingActivatedAt; - } + function canScheduleProposal( + Context storage self, + bool useEffectiveState, + Timestamp proposalSubmittedAt + ) internal view returns (bool) { + State effectiveState = useEffectiveState ? getEffectiveState(self) : getPersistedState(self); + if (effectiveState == State.Normal) return true; + if (effectiveState == State.VetoCooldown) return proposalSubmittedAt <= self.vetoSignallingActivatedAt; return false; } + function getDualGovernanceConfigProvider(Context storage self) + internal + view + returns (IDualGovernanceConfigProvider) + { + return self.configProvider; + } + + function getDualGovernanceConfig(Context storage self) + internal + view + returns (DualGovernanceConfig.Context memory) + { + return self.configProvider.getDualGovernanceConfig(); + } + + // --- + // Private Methods + // --- + + function _setConfigProvider(Context storage self, IDualGovernanceConfigProvider newConfigProvider) private { + if (address(newConfigProvider) == address(0) || newConfigProvider == self.configProvider) { + revert InvalidConfigProvider(newConfigProvider); + } + + newConfigProvider.getDualGovernanceConfig().validate(); + + self.configProvider = newConfigProvider; + emit ConfigProviderSet(newConfigProvider); + } + function _deployNewSignallingEscrow( Context storage self, - address escrowMasterCopy, + IEscrow escrowMasterCopy, Duration minAssetsLockDuration ) private { - IEscrow newSignallingEscrow = IEscrow(Clones.clone(escrowMasterCopy)); + IEscrow newSignallingEscrow = IEscrow(Clones.clone(address(escrowMasterCopy))); newSignallingEscrow.initialize(minAssetsLockDuration); self.signallingEscrow = newSignallingEscrow; emit NewSignallingEscrowDeployed(newSignallingEscrow); diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 1b5c9075..15661823 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -9,7 +9,7 @@ import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; import {Escrow} from "contracts/Escrow.sol"; import {Executor} from "contracts/Executor.sol"; -import {DualGovernance, State} from "contracts/DualGovernance.sol"; +import {DualGovernance, State, DualGovernanceStateMachine} from "contracts/DualGovernance.sol"; import {Tiebreaker} from "contracts/libraries/Tiebreaker.sol"; import {Status as ProposalStatus} from "contracts/libraries/ExecutableProposals.sol"; import {Proposers} from "contracts/libraries/Proposers.sol"; @@ -122,15 +122,15 @@ contract DualGovernanceUnitTests is UnitTest { vm.prank(vetoer); _escrow.lockStETH(5 ether); - State currentStateBefore = _dualGovernance.getState(); + State currentStateBefore = _dualGovernance.getPersistedState(); assertEq(currentStateBefore, State.VetoSignalling); _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); - assertEq(currentStateBefore, _dualGovernance.getState()); + assertEq(currentStateBefore, _dualGovernance.getPersistedState()); _dualGovernance.submitProposal(_generateExternalCalls(), ""); - State currentStateAfter = _dualGovernance.getState(); + State currentStateAfter = _dualGovernance.getPersistedState(); assertEq(currentStateAfter, State.RageQuit); assert(currentStateBefore != currentStateAfter); } @@ -139,11 +139,11 @@ contract DualGovernanceUnitTests is UnitTest { vm.startPrank(vetoer); _escrow.lockStETH(5 ether); - assertEq(_dualGovernance.getState(), State.VetoSignalling); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); _escrow.unlockStETH(); - assertEq(_dualGovernance.getState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); vm.expectRevert(abi.encodeWithSelector(DualGovernance.ProposalSubmissionBlocked.selector)); _dualGovernance.submitProposal(_generateExternalCalls(), ""); @@ -208,9 +208,9 @@ contract DualGovernanceUnitTests is UnitTest { ) ); - assertEq(_dualGovernance.getState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); _dualGovernance.scheduleProposal(proposalId); - assertEq(_dualGovernance.getState(), State.VetoCooldown); + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); } function test_scheduleProposal_RevertOn_CannotSchedule() external { @@ -246,7 +246,7 @@ contract DualGovernanceUnitTests is UnitTest { _submitMockProposal(); assertEq(_timelock.getProposalsCount(), 1); assertEq(_timelock.lastCancelledProposalId(), 0); - assertEq(_dualGovernance.getState(), State.Normal); + assertEq(_dualGovernance.getPersistedState(), State.Normal); vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsSkipped(); @@ -261,7 +261,7 @@ contract DualGovernanceUnitTests is UnitTest { _submitMockProposal(); assertEq(_timelock.getProposalsCount(), 1); assertEq(_timelock.lastCancelledProposalId(), 0); - assertEq(_dualGovernance.getState(), State.Normal); + assertEq(_dualGovernance.getPersistedState(), State.Normal); Escrow signallingEscrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); @@ -269,19 +269,19 @@ contract DualGovernanceUnitTests is UnitTest { signallingEscrow.lockStETH(5 ether); vm.stopPrank(); - assertEq(_dualGovernance.getState(), State.VetoSignalling); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); vm.prank(vetoer); signallingEscrow.unlockStETH(); - assertEq(_dualGovernance.getState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _dualGovernance.activateNextState(); - assertEq(_dualGovernance.getState(), State.VetoCooldown); + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsSkipped(); @@ -296,18 +296,18 @@ contract DualGovernanceUnitTests is UnitTest { _submitMockProposal(); assertEq(_timelock.getProposalsCount(), 1); assertEq(_timelock.lastCancelledProposalId(), 0); - assertEq(_dualGovernance.getState(), State.Normal); + assertEq(_dualGovernance.getPersistedState(), State.Normal); vm.startPrank(vetoer); _escrow.lockStETH(5 ether); vm.stopPrank(); - assertEq(_dualGovernance.getState(), State.VetoSignalling); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); _dualGovernance.activateNextState(); - assertEq(_dualGovernance.getState(), State.RageQuit); + assertEq(_dualGovernance.getPersistedState(), State.RageQuit); vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsSkipped(); @@ -322,13 +322,13 @@ contract DualGovernanceUnitTests is UnitTest { _submitMockProposal(); assertEq(_timelock.getProposalsCount(), 1); assertEq(_timelock.lastCancelledProposalId(), 0); - assertEq(_dualGovernance.getState(), State.Normal); + assertEq(_dualGovernance.getPersistedState(), State.Normal); vm.startPrank(vetoer); _escrow.lockStETH(5 ether); vm.stopPrank(); - assertEq(_dualGovernance.getState(), State.VetoSignalling); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsExecuted(); @@ -343,20 +343,20 @@ contract DualGovernanceUnitTests is UnitTest { _submitMockProposal(); assertEq(_timelock.getProposalsCount(), 1); assertEq(_timelock.lastCancelledProposalId(), 0); - assertEq(_dualGovernance.getState(), State.Normal); + assertEq(_dualGovernance.getPersistedState(), State.Normal); vm.startPrank(vetoer); _escrow.lockStETH(5 ether); vm.stopPrank(); - assertEq(_dualGovernance.getState(), State.VetoSignalling); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); vm.prank(vetoer); _escrow.unlockStETH(); - assertEq(_dualGovernance.getState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsExecuted(); @@ -392,14 +392,14 @@ contract DualGovernanceUnitTests is UnitTest { function test_canSubmitProposal_HappyPath() external { assertTrue(_dualGovernance.canSubmitProposal()); - assertEq(_dualGovernance.getState(), State.Normal); + assertEq(_dualGovernance.getPersistedState(), State.Normal); vm.startPrank(vetoer); _escrow.lockStETH(5 ether); _dualGovernance.activateNextState(); assertTrue(_dualGovernance.canSubmitProposal()); - assertEq(_dualGovernance.getState(), State.VetoSignalling); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); @@ -407,27 +407,27 @@ contract DualGovernanceUnitTests is UnitTest { _dualGovernance.activateNextState(); assertFalse(_dualGovernance.canSubmitProposal()); - assertEq(_dualGovernance.getState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _dualGovernance.activateNextState(); assertFalse(_dualGovernance.canSubmitProposal()); - assertEq(_dualGovernance.getState(), State.VetoCooldown); + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); _dualGovernance.activateNextState(); assertTrue(_dualGovernance.canSubmitProposal()); - assertEq(_dualGovernance.getState(), State.Normal); + assertEq(_dualGovernance.getPersistedState(), State.Normal); _escrow.lockStETH(5 ether); _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); _dualGovernance.activateNextState(); assertTrue(_dualGovernance.canSubmitProposal()); - assertEq(_dualGovernance.getState(), State.RageQuit); + assertEq(_dualGovernance.getPersistedState(), State.RageQuit); } // --- @@ -499,7 +499,7 @@ contract DualGovernanceUnitTests is UnitTest { _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _dualGovernance.activateNextState(); - assertEq(_dualGovernance.getState(), State.VetoCooldown); + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); vm.mockCall( address(_timelock), @@ -526,33 +526,33 @@ contract DualGovernanceUnitTests is UnitTest { // --- function test_activateNextState_getState_HappyPath() external { - assertEq(_dualGovernance.getState(), State.Normal); + assertEq(_dualGovernance.getPersistedState(), State.Normal); vm.startPrank(vetoer); _escrow.lockStETH(5 ether); _dualGovernance.activateNextState(); - assertEq(_dualGovernance.getState(), State.VetoSignalling); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); _escrow.unlockStETH(); _dualGovernance.activateNextState(); - assertEq(_dualGovernance.getState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _dualGovernance.activateNextState(); - assertEq(_dualGovernance.getState(), State.VetoCooldown); + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); _dualGovernance.activateNextState(); - assertEq(_dualGovernance.getState(), State.Normal); + assertEq(_dualGovernance.getPersistedState(), State.Normal); _escrow.lockStETH(5 ether); _dualGovernance.activateNextState(); - assertEq(_dualGovernance.getState(), State.VetoSignalling); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); _dualGovernance.activateNextState(); - assertEq(_dualGovernance.getState(), State.RageQuit); + assertEq(_dualGovernance.getPersistedState(), State.RageQuit); } // --- @@ -583,7 +583,7 @@ contract DualGovernanceUnitTests is UnitTest { IDualGovernanceConfigProvider oldConfigProvider = _dualGovernance.getConfigProvider(); vm.expectEmit(); - emit DualGovernance.ConfigProviderSet(IDualGovernanceConfigProvider(address(newConfigProvider))); + emit DualGovernanceStateMachine.ConfigProviderSet(IDualGovernanceConfigProvider(address(newConfigProvider))); vm.expectCall( address(_escrow), @@ -630,14 +630,16 @@ contract DualGovernanceUnitTests is UnitTest { } function test_setConfigProvider_RevertOn_ConfigZeroAddress() external { - vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidConfigProvider.selector, address(0))); + vm.expectRevert(abi.encodeWithSelector(DualGovernanceStateMachine.InvalidConfigProvider.selector, address(0))); _executor.execute( address(_dualGovernance), 0, abi.encodeWithSelector(DualGovernance.setConfigProvider.selector, address(0)) ); } function test_setConfigProvider_RevertOn_SameAddress() external { - vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidConfigProvider.selector, address(_configProvider))); + vm.expectRevert( + abi.encodeWithSelector(DualGovernanceStateMachine.InvalidConfigProvider.selector, address(_configProvider)) + ); _executor.execute( address(_dualGovernance), 0, @@ -694,7 +696,7 @@ contract DualGovernanceUnitTests is UnitTest { _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); _dualGovernance.activateNextState(); - assertEq(_dualGovernance.getState(), State.RageQuit); + assertEq(_dualGovernance.getPersistedState(), State.RageQuit); assertTrue(_dualGovernance.getVetoSignallingEscrow() != address(_escrow)); } @@ -711,7 +713,7 @@ contract DualGovernanceUnitTests is UnitTest { _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); _dualGovernance.activateNextState(); - assertEq(_dualGovernance.getState(), State.RageQuit); + assertEq(_dualGovernance.getPersistedState(), State.RageQuit); assertEq(_dualGovernance.getRageQuitEscrow(), address(_escrow)); } @@ -1190,7 +1192,7 @@ contract DualGovernanceUnitTests is UnitTest { _dualGovernance.activateNextState(); _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); - assertEq(_dualGovernance.getState(), State.VetoSignalling); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); vm.mockCall( address(_RESEAL_MANAGER_STUB), @@ -1201,7 +1203,7 @@ contract DualGovernanceUnitTests is UnitTest { vm.prank(tiebreakerCommittee); _dualGovernance.tiebreakerResumeSealable(sealable); - assertEq(_dualGovernance.getState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); IDualGovernance.StateDetails memory stateDetails = _dualGovernance.getStateDetails(); assertEq(stateDetails.enteredAt, Timestamps.now()); } @@ -1265,7 +1267,7 @@ contract DualGovernanceUnitTests is UnitTest { _escrow.lockStETH(1 ether); vm.stopPrank(); _dualGovernance.activateNextState(); - assertEq(uint256(_dualGovernance.getState()), uint256(State.VetoSignalling)); + assertEq(uint256(_dualGovernance.getPersistedState()), uint256(State.VetoSignalling)); _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); @@ -1274,7 +1276,7 @@ contract DualGovernanceUnitTests is UnitTest { vm.prank(tiebreakerCommittee); _dualGovernance.tiebreakerScheduleProposal(proposalId); - assertEq(uint256(_dualGovernance.getState()), uint256(State.VetoSignallingDeactivation)); + assertEq(uint256(_dualGovernance.getPersistedState()), uint256(State.VetoSignallingDeactivation)); IDualGovernance.StateDetails memory stateDetails = _dualGovernance.getStateDetails(); assertEq(stateDetails.enteredAt, Timestamps.now()); @@ -1357,7 +1359,7 @@ contract DualGovernanceUnitTests is UnitTest { _escrow.lockStETH(5 ether); vm.stopPrank(); _dualGovernance.activateNextState(); - assertEq(uint256(_dualGovernance.getState()), uint256(State.VetoSignalling)); + assertEq(uint256(_dualGovernance.getPersistedState()), uint256(State.VetoSignalling)); _wait(newTimeout.plusSeconds(1)); @@ -1388,7 +1390,7 @@ contract DualGovernanceUnitTests is UnitTest { _escrow.lockStETH(5 ether); vm.stopPrank(); _dualGovernance.activateNextState(); - assertEq(_dualGovernance.getState(), State.VetoSignalling); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); vm.mockCall( address(_RESEAL_MANAGER_STUB), @@ -1414,7 +1416,7 @@ contract DualGovernanceUnitTests is UnitTest { _escrow.lockStETH(5 ether); vm.stopPrank(); _dualGovernance.activateNextState(); - assertEq(_dualGovernance.getState(), State.VetoSignalling); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); vm.prank(notResealCommittee); vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotResealCommittee.selector, notResealCommittee)); @@ -1431,7 +1433,7 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, resealCommittee) ); - assertEq(_dualGovernance.getState(), State.Normal); + assertEq(_dualGovernance.getPersistedState(), State.Normal); vm.prank(resealCommittee); vm.expectRevert(DualGovernance.ResealIsNotAllowedInNormalState.selector); diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 7e447cae..7f0737ec 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -13,12 +13,12 @@ import { } from "contracts/ImmutableDualGovernanceConfigProvider.sol"; import {UnitTest} from "test/utils/unit-test.sol"; -import {EscrowMock} from "test/mocks/EscrowMock.sol"; +import {IEscrow, EscrowMock} from "test/mocks/EscrowMock.sol"; contract DualGovernanceStateMachineUnitTests is UnitTest { using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; - address private immutable _ESCROW_MASTER_COPY = address(new EscrowMock()); + IEscrow private immutable _ESCROW_MASTER_COPY = new EscrowMock(); ImmutableDualGovernanceConfigProvider internal immutable _CONFIG_PROVIDER = new ImmutableDualGovernanceConfigProvider( DualGovernanceConfig.Context({ firstSealRageQuitSupport: PercentsD16.fromBasisPoints(3_00), // 3% @@ -43,7 +43,7 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { DualGovernanceStateMachine.Context private _stateMachine; function setUp() external { - _stateMachine.initialize(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + _stateMachine.initialize(_CONFIG_PROVIDER, _ESCROW_MASTER_COPY); } function test_activateNextState_HappyPath_MaxRageQuitsRound() external { @@ -62,23 +62,23 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { // wait here the full duration of the veto cooldown to make sure it's over from the previous iteration _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); - _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); assertEq(_stateMachine.state, State.VetoSignalling); _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); - _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); assertEq(_stateMachine.state, State.RageQuit); assertEq(_stateMachine.rageQuitRound, Math.min(i + 1, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND)); EscrowMock(signallingEscrow).__setIsRageQuitFinalized(true); - _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); assertEq(_stateMachine.state, State.VetoCooldown); } // after the sequential rage quits chain is broken, the rage quit resets to 0 _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); - _stateMachine.activateNextState(_CONFIG_PROVIDER.getDualGovernanceConfig(), _ESCROW_MASTER_COPY); + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); assertEq(_stateMachine.rageQuitRound, 0); assertEq(_stateMachine.state, State.Normal); diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index caeeaf25..afb3f0e8 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -449,23 +449,23 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { } function _assertNormalState() internal { - assertEq(_dualGovernance.getState(), DGState.Normal); + assertEq(_dualGovernance.getPersistedState(), DGState.Normal); } function _assertVetoSignalingState() internal { - assertEq(_dualGovernance.getState(), DGState.VetoSignalling); + assertEq(_dualGovernance.getPersistedState(), DGState.VetoSignalling); } function _assertVetoSignalingDeactivationState() internal { - assertEq(_dualGovernance.getState(), DGState.VetoSignallingDeactivation); + assertEq(_dualGovernance.getPersistedState(), DGState.VetoSignallingDeactivation); } function _assertRageQuitState() internal { - assertEq(_dualGovernance.getState(), DGState.RageQuit); + assertEq(_dualGovernance.getPersistedState(), DGState.RageQuit); } function _assertVetoCooldownState() internal { - assertEq(_dualGovernance.getState(), DGState.VetoCooldown); + assertEq(_dualGovernance.getPersistedState(), DGState.VetoCooldown); } function _assertNoTargetMockCalls() internal { From 6188cbe25cc05a1719702d667bf373eecadafda8 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 16 Sep 2024 02:59:20 +0400 Subject: [PATCH 73/86] Add NatSpec for DualGovernanceStateMachine lib --- .../libraries/DualGovernanceStateMachine.sol | 137 ++++++++++++++---- 1 file changed, 111 insertions(+), 26 deletions(-) diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 13f5f3df..03ddd716 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -14,6 +14,21 @@ import {IDualGovernanceConfigProvider} from "../interfaces/IDualGovernanceConfig import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; +/// @notice Enum describing the state of the Dual Governance State Machine +/// @param Unset The initial (uninitialized) state of the Dual Governance State Machine. The state machine cannot +/// operate in this state and must be initialized before use. +/// @param Normal The default state where the system is expected to remain most of the time. In this state, proposals +/// can be both submitted and scheduled for execution. +/// @param VetoSignalling Represents active opposition to DAO decisions. In this state, the scheduling of proposals +/// is blocked, but the submission of new proposals is still allowed. +/// @param VetoSignallingDeactivation A sub-state of VetoSignalling, allowing users to observe the deactivation process +/// and react before non-cancelled proposals are scheduled for execution. Both proposal submission and scheduling +/// are prohibited in this state. +/// @param VetoCooldown A state where the DAO can execute non-cancelled proposals but is prohibited from submitting +/// new proposals. +/// @param RageQuit Represents the process where users opting to leave the protocol can withdraw their funds. This state +/// is triggered when the Second Seal Threshold is crossed. During this state, the scheduling of proposals for +/// execution is forbidden, but new proposals can still be submitted. enum State { Unset, Normal, @@ -23,46 +38,44 @@ enum State { RageQuit } +/// @title Dual Governance State Machine +/// @notice Library containing the core logic for managing the states of the Dual Governance system library DualGovernanceStateMachine { using DualGovernanceStateTransitions for Context; using DualGovernanceConfig for DualGovernanceConfig.Context; + // --- + // Data types + // --- + + /// @notice Represents the context of the Dual Governance State Machine. + /// @param state The last recorded state of the Dual Governance State Machine. + /// @param enteredAt The timestamp when the current `state` was entered. + /// @param vetoSignallingActivatedAt The timestamp when the Veto Signalling state was last activated. + /// @param signallingEscrow The address of the Escrow contract used for Veto Signalling. + /// @param rageQuitRound The number of continuous Rage Quit rounds, starting at 0 and capped at MAX_RAGE_QUIT_ROUND. + /// @param vetoSignallingReactivationTime The timestamp of the last transition from VetoSignallingDeactivation to VetoSignalling. + /// @param normalOrVetoCooldownExitedAt The timestamp of the last exit from either the Normal or VetoCooldown state. + /// @param rageQuitEscrow The address of the Escrow contract used during the most recent (or ongoing) Rage Quit process. + /// @param configProvider The address of the contract providing the current configuration for the Dual Governance State Machine. struct Context { - /// /// @dev slot 0: [0..7] - /// The current state of the Dual Governance FSM State state; - /// /// @dev slot 0: [8..47] - /// The timestamp when the Dual Governance FSM entered the current state Timestamp enteredAt; - /// /// @dev slot 0: [48..87] - /// The time the VetoSignalling FSM state was entered the last time Timestamp vetoSignallingActivatedAt; - /// /// @dev slot 0: [88..247] - /// The address of the currently used Veto Signalling Escrow IEscrow signallingEscrow; - /// /// @dev slot 0: [248..255] - /// The number of the Rage Quit round. Initial value is 0. uint8 rageQuitRound; - /// /// @dev slot 1: [0..39] - /// The last time VetoSignallingDeactivation -> VetoSignalling transition happened Timestamp vetoSignallingReactivationTime; - /// /// @dev slot 1: [40..79] - /// The last time when the Dual Governance FSM exited Normal or VetoCooldown state Timestamp normalOrVetoCooldownExitedAt; - /// /// @dev slot 1: [80..239] - /// The address of the Escrow used during the last (may be ongoing) Rage Quit process IEscrow rageQuitEscrow; - /// /// @dev slot 2: [0..159] - /// The address of the Dual Governance config provider IDualGovernanceConfigProvider configProvider; } @@ -85,12 +98,19 @@ library DualGovernanceStateMachine { // Constants // --- + /// @dev The upper limit for the maximum possible continuous RageQuit rounds. Once this limit is reached, + /// the `rageQuitRound` value is capped at 255 until the system returns to the Normal or VetoCooldown state. uint256 internal constant MAX_RAGE_QUIT_ROUND = type(uint8).max; // --- // Main functionality // --- + /// @notice Initializes the Dual Governance State Machine context. + /// @param self The context of the Dual Governance State Machine to be initialized. + /// @param configProvider The address of the Dual Governance State Machine configuration provider. + /// @param escrowMasterCopy The address of the master copy used as the implementation for the minimal proxy deployment + /// of a Signalling Escrow instance. function initialize( Context storage self, IDualGovernanceConfigProvider configProvider, @@ -111,6 +131,14 @@ library DualGovernanceStateMachine { emit DualGovernanceStateChanged(State.Unset, State.Normal, self); } + /// @notice Executes a state transition for the Dual Governance State Machine, if applicable. + /// If no transition is possible from the current state, no changes are applied to the context. + /// @dev If the state transitions to RageQuit, a new instance of the Signalling Escrow is deployed using + /// `escrowMasterCopy` as the implementation for the minimal proxy, while the previous Signalling Escrow + /// instance is converted into the RageQuit escrow. + /// @param self The context of the Dual Governance State Machine. + /// @param escrowMasterCopy The address of the master copy used as the implementation for the minimal proxy + /// to deploy a new instance of the Signalling Escrow. function activateNextState(Context storage self, IEscrow escrowMasterCopy) internal { DualGovernanceConfig.Context memory config = getDualGovernanceConfig(self); (State currentState, State newState) = self.getStateTransition(config); @@ -140,7 +168,7 @@ library DualGovernanceStateMachine { uint256 currentRageQuitRound = self.rageQuitRound; /// @dev Limits the maximum value of the rage quit round to prevent failures due to arithmetic overflow - /// if the number of consecutive rage quits reaches MAX_RAGE_QUIT_ROUND. + /// if the number of continuous rage quits reaches MAX_RAGE_QUIT_ROUND. uint256 newRageQuitRound = Math.min(currentRageQuitRound + 1, MAX_RAGE_QUIT_ROUND); self.rageQuitRound = uint8(newRageQuitRound); @@ -154,11 +182,14 @@ library DualGovernanceStateMachine { emit DualGovernanceStateChanged(currentState, newState, self); } + /// @notice Updates the address of the configuration provider for the Dual Governance State Machine. + /// @param self The context of the Dual Governance State Machine. + /// @param newConfigProvider The address of the new configuration provider. function setConfigProvider(Context storage self, IDualGovernanceConfigProvider newConfigProvider) internal { _setConfigProvider(self, newConfigProvider); /// @dev minAssetsLockDuration is stored as a storage variable in the Signalling Escrow instance. - /// To synchronize the new value with the current Signalling Escrow, it must be manually updated. + /// To synchronize the new value with the current Signalling Escrow, it must be manually updated. self.signallingEscrow.setMinAssetsLockDuration( newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration ); @@ -168,6 +199,10 @@ library DualGovernanceStateMachine { // Getters // --- + /// @notice Returns detailed information about the current state of the Dual Governance State Machine. + /// @param self The context of the Dual Governance State Machine. + /// @return stateDetails A struct containing detailed information about the current state of + /// the Dual Governance State Machine. function getStateDetails(Context storage self) internal view @@ -187,23 +222,52 @@ library DualGovernanceStateMachine { config.calcVetoSignallingDuration(self.signallingEscrow.getRageQuitSupport()); } + /// @notice Returns the most recently persisted state of the Dual Governance State Machine. + /// @param self The context of the Dual Governance State Machine. + /// @return persistedState The state of the Dual Governance State Machine as last stored. function getPersistedState(Context storage self) internal view returns (State persistedState) { persistedState = self.state; } + /// @notice Returns the effective state of the Dual Governance State Machine. + /// @dev The effective state refers to the state the Dual Governance State Machine would transition to + /// upon calling `activateNextState()`. + /// @param self The context of the Dual Governance State Machine. + /// @return effectiveState The state that will become active after the next state transition. + /// If the `activateNextState` call does not trigger a state transition, `effectiveState` + /// will be the same as `persistedState`. function getEffectiveState(Context storage self) internal view returns (State effectiveState) { ( /* persistedState */ , effectiveState) = self.getStateTransition(getDualGovernanceConfig(self)); } + /// @notice Returns the timestamp when the system last exited the Normal or VetoCooldown state. + /// @param self The context of the Dual Governance State Machine. + /// @return The timestamp indicating when the Normal or VetoCooldown state was last exited. function getNormalOrVetoCooldownStateExitedAt(Context storage self) internal view returns (Timestamp) { return self.normalOrVetoCooldownExitedAt; } + /// @notice Returns whether the submission of proposals is allowed based on the `persisted` or `effective` state, + /// depending on the `useEffectiveState` value. + /// @param self The context of the Dual Governance State Machine. + /// @param useEffectiveState If `true`, the check is performed against the effective state (the state + /// the Dual Governance State Machine will enter after the next `activateNextState` call). If `false`, + /// the check is performed using the persisted state (the current state of the Dual Governance State Machine). + /// @return A boolean indicating whether the submission of proposals is allowed in the selected state. function canSubmitProposal(Context storage self, bool useEffectiveState) internal view returns (bool) { State effectiveState = useEffectiveState ? getEffectiveState(self) : getPersistedState(self); return effectiveState != State.VetoSignallingDeactivation && effectiveState != State.VetoCooldown; } + /// @notice Determines whether scheduling a proposal for execution is allowed, based on either the `persisted` + /// or `effective` state, depending on the `useEffectiveState` flag. + /// @param self The context of the Dual Governance State Machine. + /// @param useEffectiveState If `true`, the check is performed against the effective state (the state + /// the Dual Governance State Machine will transition to after the next `activateNextState` call). + /// If `false`, the check is performed using the persisted state (the current state of the Dual Governance + /// State Machine). + /// @param proposalSubmittedAt The timestamp indicating when the proposal to be scheduled was originally submitted. + /// @return A boolean indicating whether scheduling the proposal is allowed in the chosen state. function canScheduleProposal( Context storage self, bool useEffectiveState, @@ -215,6 +279,9 @@ library DualGovernanceStateMachine { return false; } + /// @notice Returns the address of the Dual Governance Config Provider. + /// @param self The context of the Dual Governance State Machine. + /// @return The address of the current Dual Governance Config Provider. function getDualGovernanceConfigProvider(Context storage self) internal view @@ -223,6 +290,10 @@ library DualGovernanceStateMachine { return self.configProvider; } + /// @notice Returns the configuration of the Dual Governance State Machine as provided by + /// the Dual Governance Config Provider. + /// @param self The context of the Dual Governance State Machine. + /// @return The current configuration of the Dual Governance State function getDualGovernanceConfig(Context storage self) internal view @@ -258,29 +329,43 @@ library DualGovernanceStateMachine { } } +/// @title Dual Governance State Transitions +/// @notice Library containing the transitions logic for the Dual Governance system library DualGovernanceStateTransitions { using DualGovernanceConfig for DualGovernanceConfig.Context; + /// @notice Returns the allowed state transition for the Dual Governance State Machine. + /// If no state transition is possible, `currentState` will be equal to `nextState`. + /// @param self The context of the Dual Governance State Machine. + /// @param config The configuration of the Dual Governance State Machine to use for determining + /// state transitions. + /// @return currentState The current state of the Dual Governance State Machine. + /// @return nextState The next state of the Dual Governance State Machine if a transition + /// is possible, otherwise it will be the same as `currentState`. function getStateTransition( DualGovernanceStateMachine.Context storage self, DualGovernanceConfig.Context memory config - ) internal view returns (State currentState, State nextStatus) { + ) internal view returns (State currentState, State nextState) { currentState = self.state; if (currentState == State.Normal) { - nextStatus = _fromNormalState(self, config); + nextState = _fromNormalState(self, config); } else if (currentState == State.VetoSignalling) { - nextStatus = _fromVetoSignallingState(self, config); + nextState = _fromVetoSignallingState(self, config); } else if (currentState == State.VetoSignallingDeactivation) { - nextStatus = _fromVetoSignallingDeactivationState(self, config); + nextState = _fromVetoSignallingDeactivationState(self, config); } else if (currentState == State.VetoCooldown) { - nextStatus = _fromVetoCooldownState(self, config); + nextState = _fromVetoCooldownState(self, config); } else if (currentState == State.RageQuit) { - nextStatus = _fromRageQuitState(self, config); + nextState = _fromRageQuitState(self, config); } else { assert(false); } } + // --- + // Private Methods + // --- + function _fromNormalState( DualGovernanceStateMachine.Context storage self, DualGovernanceConfig.Context memory config From 39aa76756dacf81a9cbd5debc9d2903833f6cff7 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 16 Sep 2024 03:45:17 +0400 Subject: [PATCH 74/86] Add persisted/effective state to the StateDetails struct --- contracts/DualGovernance.sol | 2 -- contracts/interfaces/IDualGovernance.sol | 6 ++-- .../libraries/DualGovernanceStateMachine.sol | 6 ++-- test/unit/DualGovernance.t.sol | 28 +++++++++---------- test/utils/scenario-test-blueprint.sol | 8 +++--- 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 5cf495a9..09756c7d 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -17,7 +17,6 @@ import {IDualGovernanceConfigProvider} from "./interfaces/IDualGovernanceConfigP import {Proposers} from "./libraries/Proposers.sol"; import {Tiebreaker} from "./libraries/Tiebreaker.sol"; import {ExternalCall} from "./libraries/ExternalCalls.sol"; -import {DualGovernanceConfig} from "./libraries/DualGovernanceConfig.sol"; import {State, DualGovernanceStateMachine} from "./libraries/DualGovernanceStateMachine.sol"; import {Escrow} from "./Escrow.sol"; @@ -25,7 +24,6 @@ import {Escrow} from "./Escrow.sol"; contract DualGovernance is IDualGovernance { using Proposers for Proposers.Context; using Tiebreaker for Tiebreaker.Context; - using DualGovernanceConfig for DualGovernanceConfig.Context; using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; // --- diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 966e8c25..d8f0f55e 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -9,9 +9,9 @@ import {State} from "../libraries/DualGovernanceStateMachine.sol"; interface IDualGovernance is IGovernance, ITiebreaker { struct StateDetails { - State state; - Timestamp enteredAt; - State nextState; + State effectiveState; + State persistedState; + Timestamp persistedStateEnteredAt; Timestamp vetoSignallingActivatedAt; Timestamp vetoSignallingReactivationTime; Timestamp normalOrVetoCooldownExitedAt; diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 03ddd716..54da14be 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -209,11 +209,9 @@ library DualGovernanceStateMachine { returns (IDualGovernance.StateDetails memory stateDetails) { DualGovernanceConfig.Context memory config = getDualGovernanceConfig(self); - (State currentState, State nextState) = self.getStateTransition(config); + (stateDetails.persistedState, stateDetails.effectiveState) = self.getStateTransition(config); - stateDetails.state = currentState; - stateDetails.enteredAt = self.enteredAt; - stateDetails.nextState = nextState; + stateDetails.persistedStateEnteredAt = self.enteredAt; stateDetails.vetoSignallingActivatedAt = self.vetoSignallingActivatedAt; stateDetails.vetoSignallingReactivationTime = self.vetoSignallingReactivationTime; stateDetails.normalOrVetoCooldownExitedAt = self.normalOrVetoCooldownExitedAt; diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 15661823..8a66e39f 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -726,8 +726,8 @@ contract DualGovernanceUnitTests is UnitTest { Timestamp startTime = Timestamps.now(); IDualGovernance.StateDetails memory details = _dualGovernance.getStateDetails(); - assertEq(details.state, State.Normal); - assertEq(details.enteredAt, startTime); + assertEq(details.persistedState, State.Normal); + assertEq(details.persistedStateEnteredAt, startTime); assertEq(details.vetoSignallingActivatedAt, Timestamps.from(0)); assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); assertEq(details.normalOrVetoCooldownExitedAt, Timestamps.from(0)); @@ -741,8 +741,8 @@ contract DualGovernanceUnitTests is UnitTest { _dualGovernance.activateNextState(); details = _dualGovernance.getStateDetails(); - assertEq(details.state, State.VetoSignalling); - assertEq(details.enteredAt, vetoSignallingTime); + assertEq(details.persistedState, State.VetoSignalling); + assertEq(details.persistedStateEnteredAt, vetoSignallingTime); assertEq(details.vetoSignallingActivatedAt, vetoSignallingTime); assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); assertEq(details.normalOrVetoCooldownExitedAt, vetoSignallingTime); @@ -756,8 +756,8 @@ contract DualGovernanceUnitTests is UnitTest { _dualGovernance.activateNextState(); details = _dualGovernance.getStateDetails(); - assertEq(details.state, State.VetoSignallingDeactivation); - assertEq(details.enteredAt, deactivationTime); + assertEq(details.persistedState, State.VetoSignallingDeactivation); + assertEq(details.persistedStateEnteredAt, deactivationTime); assertEq(details.vetoSignallingActivatedAt, vetoSignallingTime); assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); assertEq(details.normalOrVetoCooldownExitedAt, vetoSignallingTime); @@ -769,8 +769,8 @@ contract DualGovernanceUnitTests is UnitTest { _dualGovernance.activateNextState(); details = _dualGovernance.getStateDetails(); - assertEq(details.state, State.VetoCooldown); - assertEq(details.enteredAt, vetoCooldownTime); + assertEq(details.persistedState, State.VetoCooldown); + assertEq(details.persistedStateEnteredAt, vetoCooldownTime); assertEq(details.vetoSignallingActivatedAt, vetoSignallingTime); assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); assertEq(details.normalOrVetoCooldownExitedAt, vetoSignallingTime); @@ -782,8 +782,8 @@ contract DualGovernanceUnitTests is UnitTest { _dualGovernance.activateNextState(); details = _dualGovernance.getStateDetails(); - assertEq(details.state, State.Normal); - assertEq(details.enteredAt, backToNormalTime); + assertEq(details.persistedState, State.Normal); + assertEq(details.persistedStateEnteredAt, backToNormalTime); assertEq(details.vetoSignallingActivatedAt, vetoSignallingTime); assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); assertEq(details.normalOrVetoCooldownExitedAt, backToNormalTime); @@ -800,8 +800,8 @@ contract DualGovernanceUnitTests is UnitTest { vm.stopPrank(); details = _dualGovernance.getStateDetails(); - assertEq(details.state, State.RageQuit); - assertEq(details.enteredAt, rageQuitTime); + assertEq(details.persistedState, State.RageQuit); + assertEq(details.persistedStateEnteredAt, rageQuitTime); assertEq(details.vetoSignallingActivatedAt, secondVetoSignallingTime); assertEq(details.vetoSignallingReactivationTime, Timestamps.from(0)); assertEq(details.normalOrVetoCooldownExitedAt, backToNormalTime); @@ -1205,7 +1205,7 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); IDualGovernance.StateDetails memory stateDetails = _dualGovernance.getStateDetails(); - assertEq(stateDetails.enteredAt, Timestamps.now()); + assertEq(stateDetails.persistedStateEnteredAt, Timestamps.now()); } // --- @@ -1279,7 +1279,7 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(uint256(_dualGovernance.getPersistedState()), uint256(State.VetoSignallingDeactivation)); IDualGovernance.StateDetails memory stateDetails = _dualGovernance.getStateDetails(); - assertEq(stateDetails.enteredAt, Timestamps.now()); + assertEq(stateDetails.persistedStateEnteredAt, Timestamps.now()); } // --- diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index afb3f0e8..66fb2fac 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -95,9 +95,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) { IDualGovernance.StateDetails memory stateContext = _dualGovernance.getStateDetails(); - isActive = stateContext.state == DGState.VetoSignalling; + isActive = stateContext.persistedState == DGState.VetoSignalling; duration = _dualGovernance.getStateDetails().vetoSignallingDuration.toSeconds(); - enteredAt = stateContext.enteredAt.toSeconds(); + enteredAt = stateContext.persistedStateEnteredAt.toSeconds(); activatedAt = stateContext.vetoSignallingActivatedAt.toSeconds(); } @@ -107,9 +107,9 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { returns (bool isActive, uint256 duration, uint256 enteredAt) { IDualGovernance.StateDetails memory stateContext = _dualGovernance.getStateDetails(); - isActive = stateContext.state == DGState.VetoSignallingDeactivation; + isActive = stateContext.persistedState == DGState.VetoSignallingDeactivation; duration = _dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().toSeconds(); - enteredAt = stateContext.enteredAt.toSeconds(); + enteredAt = stateContext.persistedStateEnteredAt.toSeconds(); } // --- From fbd77fa8c3699d5a86b7da12a3c7a2c8b2edfe64 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 16 Sep 2024 04:18:40 +0400 Subject: [PATCH 75/86] Spec update and minor comments & naming tweaks --- contracts/DualGovernance.sol | 10 +-- .../libraries/DualGovernanceStateMachine.sol | 4 +- docs/specification.md | 61 +++++++++++++++++++ 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 09756c7d..53ca190b 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -196,15 +196,15 @@ contract DualGovernance is IDualGovernance { return address(_stateMachine.rageQuitEscrow); } - function getPersistedState() external view returns (State state) { - state = _stateMachine.getPersistedState(); + function getPersistedState() external view returns (State persistedState) { + persistedState = _stateMachine.getPersistedState(); } - function getEffectiveState() external view returns (State state) { - state = _stateMachine.getEffectiveState(); + function getEffectiveState() external view returns (State effectiveState) { + effectiveState = _stateMachine.getEffectiveState(); } - function getStateDetails() external view returns (IDualGovernance.StateDetails memory stateDetails) { + function getStateDetails() external view returns (StateDetails memory stateDetails) { return _stateMachine.getStateDetails(); } diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 54da14be..1faeaf64 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -51,8 +51,8 @@ library DualGovernanceStateMachine { /// @notice Represents the context of the Dual Governance State Machine. /// @param state The last recorded state of the Dual Governance State Machine. /// @param enteredAt The timestamp when the current `state` was entered. - /// @param vetoSignallingActivatedAt The timestamp when the Veto Signalling state was last activated. - /// @param signallingEscrow The address of the Escrow contract used for Veto Signalling. + /// @param vetoSignallingActivatedAt The timestamp when the VetoSignalling state was last activated. + /// @param signallingEscrow The address of the Escrow contract used for VetoSignalling. /// @param rageQuitRound The number of continuous Rage Quit rounds, starting at 0 and capped at MAX_RAGE_QUIT_ROUND. /// @param vetoSignallingReactivationTime The timestamp of the last transition from VetoSignallingDeactivation to VetoSignalling. /// @param normalOrVetoCooldownExitedAt The timestamp of the last exit from either the Normal or VetoCooldown state. diff --git a/docs/specification.md b/docs/specification.md index 53628a42..b6c74482 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -376,6 +376,38 @@ function activateNextState() Triggers a transition of the [global governance state](#Governance-state), if one is possible; does nothing otherwise. +### Function: DualGovernance.getPersistedState + +```solidity +function getPersistedState() view returns (State persistedState) +``` + +Returns the most recently persisted state of the DualGovernance. + +### Function: DualGovernance.getEffectiveState + +```solidity +function getEffectiveState() view returns (State persistedState) +``` + +Returns the effective state of the DualGovernance. The effective state refers to the state the DualGovernance would transition to upon calling `DualGovernance.activateNextState()`. + +### Function DualGovernance.getStateDetails + +```solidity +function getStateDetails() view returns (StateDetails) +``` + +This function returns detailed information about the current state of the `DualGovernance`, comprising the following data: + +- **`State effectiveState`**: The state that the `DualGovernance` would transition to upon calling `DualGovernance.activateNextState()`. +- **`State persistedState`**: The current stored state of the `DualGovernance`. +- **`Timestamp persistedStateEnteredAt`**: The timestamp when the `persistedState` was entered. +- **`Timestamp vetoSignallingActivatedAt`**: The timestamp when the `VetoSignalling` state was last activated. +- **`Timestamp vetoSignallingReactivationTime`**: The timestamp when the `VetoSignalling` state was last re-activated. +- **`Timestamp normalOrVetoCooldownExitedAt`**: The timestamp when the `Normal` or `VetoCooldown` state was last exited. +- **`uint256 rageQuitRound`**: The number of continuous RageQuit rounds. +- **`Duration vetoSignallingDuration`**: The duration of the `VetoSignalling` state, calculated based on the RageQuit support in the Veto Signalling `Escrow`. ## Contract: Executor.sol @@ -491,6 +523,9 @@ The rage quit support will be dynamically updated to reflect changes in the stET The method calls the `DualGovernance.activateNextState()` function at the beginning and end of the execution, which may transition the `Escrow` instance from the `SignallingEscrow` state to the `RageQuitEscrow` state. +> [!IMPORTANT] +> To mitigate possible failures when calling the `Escrow.lockStETH()` method, it SHOULD be used alongside the `DualGovernance.getPersistedState()`/`DualGovernance.getEffectiveState()` methods or the `DualGovernance.getStateDetails()` method. These methods help identify scenarios where `persistedState != RageQuit` but `effectiveState == RageQuit`. When this state is detected, locking funds in the `SignallingEscrow` is no longer possible and will revert. In such cases, `DualGovernance.activateNextState()` MUST be called to initiate the pending `RageQuit`. + #### Returns The amount of stETH shares locked by the caller during the current method call. @@ -519,6 +554,9 @@ assets[msg.sender].stETHLockedShares = 0; Additionally, the function triggers the `DualGovernance.activateNextState()` function at the beginning and end of the execution. +> [!IMPORTANT] +> To mitigate possible failures when calling the `Escrow.unlockStETH()` method, it SHOULD be used alongside the `DualGovernance.getPersistedState()`/`DualGovernance.getEffectiveState()` methods or the `DualGovernance.getStateDetails()` method. These methods help identify scenarios where `persistedState != RageQuit` but `effectiveState == RageQuit`. When this state is detected, unlocking funds in the `SignallingEscrow` is no longer possible and will revert. In such cases, `DualGovernance.activateNextState()` MUST be called to initiate the pending `RageQuit`. + #### Returns The amount of stETH shares unlocked by the caller. @@ -551,6 +589,9 @@ stETHTotals.lockedShares += stETHShares; The method calls the `DualGovernance.activateNextState()` function at the beginning and end of the execution, which may transition the `Escrow` instance from the `SignallingEscrow` state to the `RageQuitEscrow` state. +> [!IMPORTANT] +> To mitigate possible failures when calling the `Escrow.lockWstETH()` method, it SHOULD be used alongside the `DualGovernance.getPersistedState()`/`DualGovernance.getEffectiveState()` methods or the `DualGovernance.getStateDetails()` method. These methods help identify scenarios where `persistedState != RageQuit` but `effectiveState == RageQuit`. When this state is detected, locking funds in the `SignallingEscrow` is no longer possible and will revert. In such cases, `DualGovernance.activateNextState()` MUST be called to initiate the pending `RageQuit`. + #### Returns The amount of stETH shares locked by the caller during the current method call. @@ -579,6 +620,10 @@ assets[msg.sender].stETHLockedShares = 0; Additionally, the function triggers the `DualGovernance.activateNextState()` function at the beginning and end of the execution. +> [!IMPORTANT] +> To mitigate possible failures when calling the `Escrow.unlockWstETH()` method, it SHOULD be used alongside the `DualGovernance.getPersistedState()`/`DualGovernance.getEffectiveState()` methods or the `DualGovernance.getStateDetails()` method. These methods help identify scenarios where `persistedState != RageQuit` but `effectiveState == RageQuit`. When this state is detected, unlocking funds in the `SignallingEscrow` is no longer possible and will revert. In such cases, `DualGovernance.activateNextState()` MUST be called to initiate the pending `RageQuit`. + + #### Returns The amount of stETH shares unlocked by the caller. @@ -611,6 +656,9 @@ unstETHTotals.unfinalizedShares += amountOfShares; The method calls the `DualGovernance.activateNextState()` function at the beginning and end of the execution, which may transition the `Escrow` instance from the `SignallingEscrow` state to the `RageQuitEscrow` state. +> [!IMPORTANT] +> To mitigate possible failures when calling the `Escrow.lockUnstETH()` method, it SHOULD be used alongside the `DualGovernance.getPersistedState()`/`DualGovernance.getEffectiveState()` methods or the `DualGovernance.getStateDetails()` method. These methods help identify scenarios where `persistedState != RageQuit` but `effectiveState == RageQuit`. When this state is detected, locking funds in the `SignallingEscrow` is no longer possible and will revert. In such cases, `DualGovernance.activateNextState()` MUST be called to initiate the pending `RageQuit`. + #### Preconditions - The `Escrow` instance MUST be in the `SignallingEscrow` state. @@ -652,6 +700,9 @@ unstETHTotals.unfinalizedShares -= amountOfShares; Additionally, the function triggers the `DualGovernance.activateNextState()` function at the beginning and end of the execution. +> [!IMPORTANT] +> To mitigate possible failures when calling the `Escrow.unlockUnstETH()` method, it SHOULD be used alongside the `DualGovernance.getPersistedState()`/`DualGovernance.getEffectiveState()` methods or the `DualGovernance.getStateDetails()` method. These methods help identify scenarios where `persistedState != RageQuit` but `effectiveState == RageQuit`. When this state is detected, unlocking funds in the `SignallingEscrow` is no longer possible and will revert. In such cases, `DualGovernance.activateNextState()` MUST be called to initiate the pending `RageQuit`. + #### Preconditions - The `Escrow` instance MUST be in the `SignallingEscrow` state. @@ -689,6 +740,11 @@ Withdrawal NFTs belonging to any of the following categories are excluded from t - Withdrawal NFTs already marked as finalized - Withdrawal NFTs not locked in the `Escrow` instance +The method calls the `DualGovernance.activateNextState()` function at the beginning and end of the execution, which may transition the `Escrow` instance from the `SignallingEscrow` state to the `RageQuitEscrow` state. + +> [!IMPORTANT] +> To mitigate possible failures when calling the `Escrow.markUnstETHFinalized()` method, it SHOULD be used alongside the `DualGovernance.getPersistedState()`/`DualGovernance.getEffectiveState()` methods or the `DualGovernance.getStateDetails()` method. These methods help identify scenarios where `persistedState != RageQuit` but `effectiveState == RageQuit`. When this state is detected, calling methods that change Rage Quit support in the `SignallingEscrow` will no longer be possible and will result in a revert. In such cases, `DualGovernance.activateNextState()` MUST be called to initiate the pending `RageQuit`. + #### Preconditions - The `Escrow` instance MUST be in the `SignallingEscrow` state. @@ -703,6 +759,11 @@ Allows users who have locked their stETH and wstETH to convert it into unstETH N Internally, this function marks the total amount specified in `stETHAmounts` as unlocked from the `Escrow` and accounts for it in the form of a list of unstETH NFTs, with amounts corresponding to `stETHAmounts`. +The method calls the `DualGovernance.activateNextState()` function at the beginning of the execution. + +> [!IMPORTANT] +> To mitigate possible failures when calling the `Escrow.requestWithdrawals()` method, it SHOULD be used in conjunction with the `DualGovernance.getPersistedState()`, `DualGovernance.getEffectiveState()`, or `DualGovernance.getStateDetails()` methods. These methods help identify scenarios where `persistedState != RageQuit` but `effectiveState == RageQuit`. When this state is detected, further token manipulation within the `SignallingEscrow` is no longer possible and will result in a revert. In such cases, `DualGovernance.activateNextState()` MUST be called to initiate the pending `RageQuit`. + #### Preconditions - The total amount specified in `stETHAmounts` MUST NOT exceed the user's currently locked stETH and wstETH. - The `stETHAmounts` values MUST be in range [`WithdrawalQueue.MIN_STETH_WITHDRAWAL_AMOUNT()`, `WithdrawalQueue.MAX_STETH_WITHDRAWAL_AMOUNT()`]. From ae677c19c7181f64c1c68432434195dbd9c88cc5 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 16 Sep 2024 04:39:07 +0400 Subject: [PATCH 76/86] Add canCancelAllProposals() method to DualGovernance --- contracts/DualGovernance.sol | 9 +++++--- .../libraries/DualGovernanceStateMachine.sol | 22 ++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 53ca190b..5c3416e1 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -145,8 +145,7 @@ contract DualGovernance is IDualGovernance { revert NotAdminProposer(); } - State persistedState = _stateMachine.getPersistedState(); - if (persistedState != State.VetoSignalling && persistedState != State.VetoSignallingDeactivation) { + if (!_stateMachine.canCancelAllProposals({useEffectiveState: false})) { /// @dev Some proposer contracts, like Aragon Voting, may not support canceling decisions that have already /// reached consensus. This could lead to a situation where a proposer’s cancelAllPendingProposals() call /// becomes unexecutable if the Dual Governance state changes. However, it might become executable again if @@ -161,7 +160,7 @@ contract DualGovernance is IDualGovernance { emit CancelAllPendingProposalsExecuted(); } - function canSubmitProposal() public view returns (bool) { + function canSubmitProposal() external view returns (bool) { return _stateMachine.canSubmitProposal({useEffectiveState: true}); } @@ -171,6 +170,10 @@ contract DualGovernance is IDualGovernance { && TIMELOCK.canSchedule(proposalId); } + function canCancelAllProposals() external view returns (bool) { + return _stateMachine.canCancelAllProposals({useEffectiveState: true}); + } + // --- // Dual Governance State // --- diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 1faeaf64..5b818c7d 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -253,8 +253,8 @@ library DualGovernanceStateMachine { /// the check is performed using the persisted state (the current state of the Dual Governance State Machine). /// @return A boolean indicating whether the submission of proposals is allowed in the selected state. function canSubmitProposal(Context storage self, bool useEffectiveState) internal view returns (bool) { - State effectiveState = useEffectiveState ? getEffectiveState(self) : getPersistedState(self); - return effectiveState != State.VetoSignallingDeactivation && effectiveState != State.VetoCooldown; + State state = useEffectiveState ? getEffectiveState(self) : getPersistedState(self); + return state != State.VetoSignallingDeactivation && state != State.VetoCooldown; } /// @notice Determines whether scheduling a proposal for execution is allowed, based on either the `persisted` @@ -271,12 +271,24 @@ library DualGovernanceStateMachine { bool useEffectiveState, Timestamp proposalSubmittedAt ) internal view returns (bool) { - State effectiveState = useEffectiveState ? getEffectiveState(self) : getPersistedState(self); - if (effectiveState == State.Normal) return true; - if (effectiveState == State.VetoCooldown) return proposalSubmittedAt <= self.vetoSignallingActivatedAt; + State state = useEffectiveState ? getEffectiveState(self) : getPersistedState(self); + if (state == State.Normal) return true; + if (state == State.VetoCooldown) return proposalSubmittedAt <= self.vetoSignallingActivatedAt; return false; } + /// @notice Returns whether the cancelling of the proposals is allowed based on the `persisted` or `effective` + /// state, depending on the `useEffectiveState` value. + /// @param self The context of the Dual Governance State Machine. + /// @param useEffectiveState If `true`, the check is performed against the effective state (the state + /// the Dual Governance State Machine will enter after the next `activateNextState` call). If `false`, + /// the check is performed using the persisted state (the current state of the Dual Governance State Machine). + /// @return A boolean indicating whether the cancelling of proposals is allowed in the selected state. + function canCancelAllProposals(Context storage self, bool useEffectiveState) internal view returns (bool) { + State state = useEffectiveState ? getEffectiveState(self) : getPersistedState(self); + return state == State.VetoSignalling || state == State.VetoSignallingDeactivation; + } + /// @notice Returns the address of the Dual Governance Config Provider. /// @param self The context of the Dual Governance State Machine. /// @return The address of the current Dual Governance Config Provider. From acab4a61b6506412685bac77611ddf7575c9027f Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 18 Sep 2024 10:41:15 +0500 Subject: [PATCH 77/86] Add effective state tests for canSubmitProposal, canScheduleProposal --- test/unit/DualGovernance.t.sol | 285 ++++++++++++++++++++++++++++++++- 1 file changed, 280 insertions(+), 5 deletions(-) diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 8a66e39f..3756f483 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -430,6 +430,87 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(_dualGovernance.getPersistedState(), State.RageQuit); } + function test_canSubmitProposal_PersistedStateIsNotEqualToEffectiveState() external { + assertEq(_dualGovernance.getPersistedState(), State.Normal); + assertTrue(_dualGovernance.canSubmitProposal()); + + vm.startPrank(vetoer); + _escrow.lockStETH(0.5 ether); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignalling); + assertTrue(_dualGovernance.canSubmitProposal()); + + _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().dividedBy(2)); + + // The RageQuit second seal threshold wasn't crossed, the system should enter Deactivation state + // where the proposals submission is not allowed + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignallingDeactivation); + assertFalse(_dualGovernance.canSubmitProposal()); + + // activate VetoSignallingState again + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignalling); + assertTrue(_dualGovernance.canSubmitProposal()); + + // make the EVM snapshot to return back after the RageQuit scenario is tested + uint256 snapshotId = vm.snapshot(); + + // RageQuit scenario + _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().dividedBy(2).plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.RageQuit); + assertTrue(_dualGovernance.canSubmitProposal()); + + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getPersistedState(), State.RageQuit); + assertEq(_dualGovernance.getEffectiveState(), State.RageQuit); + assertTrue(_dualGovernance.canSubmitProposal()); + + vm.revertTo(snapshotId); + + // VetoCooldown scenario + + vm.startPrank(vetoer); + + _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); + + _escrow.unlockStETH(); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignallingDeactivation); + assertFalse(_dualGovernance.canSubmitProposal()); + + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getEffectiveState(), State.VetoCooldown); + assertFalse(_dualGovernance.canSubmitProposal()); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); + assertEq(_dualGovernance.getEffectiveState(), State.VetoCooldown); + assertFalse(_dualGovernance.canSubmitProposal()); + + _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); + assertTrue(_dualGovernance.canSubmitProposal()); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.Normal); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); + assertTrue(_dualGovernance.canSubmitProposal()); + } + // --- // canScheduleProposal() // --- @@ -521,38 +602,232 @@ contract DualGovernanceUnitTests is UnitTest { assertFalse(_dualGovernance.canScheduleProposal(proposalId)); } + function test_canScheduleProposal_PersistedStateIsNotEqualToEffectiveState() external { + uint256 proposalIdSubmittedBeforeVetoSignalling = 1; + uint256 proposalIdSubmittedAfterVetoSignalling = 2; + + // The proposal is submitted before the VetoSignalling is active + vm.mockCall( + address(_timelock), + abi.encodeWithSelector(TimelockMock.getProposalDetails.selector, proposalIdSubmittedBeforeVetoSignalling), + abi.encode( + ITimelock.ProposalDetails({ + id: proposalIdSubmittedBeforeVetoSignalling, + status: ProposalStatus.Submitted, + executor: address(_executor), + submittedAt: Timestamps.now(), + scheduledAt: Timestamps.from(0) + }) + ) + ); + + vm.mockCall( + address(_timelock), + abi.encodeWithSelector(TimelockMock.canSchedule.selector, proposalIdSubmittedBeforeVetoSignalling), + abi.encode(true) + ); + + assertEq(_dualGovernance.getPersistedState(), State.Normal); + assertTrue(_dualGovernance.canScheduleProposal(proposalIdSubmittedBeforeVetoSignalling)); + + vm.startPrank(vetoer); + _escrow.lockStETH(0.5 ether); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignalling); + assertFalse(_dualGovernance.canScheduleProposal(proposalIdSubmittedBeforeVetoSignalling)); + + _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().dividedBy(2)); + + // The proposal is submitted after the VetoSignalling is active + vm.mockCall( + address(_timelock), + abi.encodeWithSelector(TimelockMock.getProposalDetails.selector, proposalIdSubmittedAfterVetoSignalling), + abi.encode( + ITimelock.ProposalDetails({ + id: proposalIdSubmittedAfterVetoSignalling, + status: ProposalStatus.Submitted, + executor: address(_executor), + submittedAt: Timestamps.now(), + scheduledAt: Timestamps.from(0) + }) + ) + ); + + vm.mockCall( + address(_timelock), + abi.encodeWithSelector(TimelockMock.canSchedule.selector, proposalIdSubmittedAfterVetoSignalling), + abi.encode(true) + ); + + // The RageQuit second seal threshold wasn't crossed, the system should enter Deactivation state + // where the proposals submission is not allowed + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignallingDeactivation); + assertFalse(_dualGovernance.canScheduleProposal(proposalIdSubmittedBeforeVetoSignalling)); + assertFalse(_dualGovernance.canScheduleProposal(proposalIdSubmittedAfterVetoSignalling)); + + // activate VetoSignallingState again + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignalling); + assertFalse(_dualGovernance.canScheduleProposal(proposalIdSubmittedBeforeVetoSignalling)); + assertFalse(_dualGovernance.canScheduleProposal(proposalIdSubmittedAfterVetoSignalling)); + + // make the EVM snapshot to return back after the RageQuit scenario is tested + uint256 snapshotId = vm.snapshot(); + + // RageQuit scenario + _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().dividedBy(2).plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.RageQuit); + assertFalse(_dualGovernance.canScheduleProposal(proposalIdSubmittedBeforeVetoSignalling)); + assertFalse(_dualGovernance.canScheduleProposal(proposalIdSubmittedAfterVetoSignalling)); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.RageQuit); + assertEq(_dualGovernance.getEffectiveState(), State.RageQuit); + assertFalse(_dualGovernance.canScheduleProposal(proposalIdSubmittedBeforeVetoSignalling)); + assertFalse(_dualGovernance.canScheduleProposal(proposalIdSubmittedAfterVetoSignalling)); + + // mock the calls to the escrow to simulate the RageQuit was over + vm.mockCall(address(_escrow), abi.encodeWithSelector(Escrow.isRageQuitFinalized.selector), abi.encode(true)); + vm.mockCall(address(_escrow), abi.encodeWithSelector(Escrow.getRageQuitSupport.selector), abi.encode(0)); + + assertEq(_dualGovernance.getPersistedState(), State.RageQuit); + assertEq(_dualGovernance.getEffectiveState(), State.VetoCooldown); + + // As the veto signalling started after the proposal was submitted, proposal becomes schedulable + // when the RageQuit is finished + assertTrue(_dualGovernance.canScheduleProposal(proposalIdSubmittedBeforeVetoSignalling)); + // But the proposal submitted after the VetoSignalling is started is not schedulable + assertFalse(_dualGovernance.canScheduleProposal(proposalIdSubmittedAfterVetoSignalling)); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); + assertEq(_dualGovernance.getEffectiveState(), State.VetoCooldown); + assertTrue(_dualGovernance.canScheduleProposal(proposalIdSubmittedBeforeVetoSignalling)); + assertFalse(_dualGovernance.canScheduleProposal(proposalIdSubmittedAfterVetoSignalling)); + + _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); + assertTrue(_dualGovernance.canScheduleProposal(proposalIdSubmittedBeforeVetoSignalling)); + // In the Normal state the proposal submitted after the veto signalling state was activated + // becomes executable + assertTrue(_dualGovernance.canScheduleProposal(proposalIdSubmittedAfterVetoSignalling)); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.Normal); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); + assertTrue(_dualGovernance.canScheduleProposal(proposalIdSubmittedBeforeVetoSignalling)); + assertTrue(_dualGovernance.canScheduleProposal(proposalIdSubmittedAfterVetoSignalling)); + + vm.revertTo(snapshotId); + + // VetoCooldown scenario + + vm.startPrank(vetoer); + + _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); + + _escrow.unlockStETH(); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignallingDeactivation); + assertFalse(_dualGovernance.canScheduleProposal(proposalIdSubmittedBeforeVetoSignalling)); + assertFalse(_dualGovernance.canScheduleProposal(proposalIdSubmittedAfterVetoSignalling)); + + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getEffectiveState(), State.VetoCooldown); + assertTrue(_dualGovernance.canScheduleProposal(proposalIdSubmittedBeforeVetoSignalling)); + assertFalse(_dualGovernance.canScheduleProposal(proposalIdSubmittedAfterVetoSignalling)); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); + assertEq(_dualGovernance.getEffectiveState(), State.VetoCooldown); + assertTrue(_dualGovernance.canScheduleProposal(proposalIdSubmittedBeforeVetoSignalling)); + assertFalse(_dualGovernance.canScheduleProposal(proposalIdSubmittedAfterVetoSignalling)); + + _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); + assertTrue(_dualGovernance.canScheduleProposal(proposalIdSubmittedBeforeVetoSignalling)); + assertTrue(_dualGovernance.canScheduleProposal(proposalIdSubmittedAfterVetoSignalling)); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.Normal); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); + assertTrue(_dualGovernance.canScheduleProposal(proposalIdSubmittedBeforeVetoSignalling)); + assertTrue(_dualGovernance.canScheduleProposal(proposalIdSubmittedAfterVetoSignalling)); + } + // --- - // activateNextState() & getState() + // activateNextState() & getPersistedState() & getEffectiveState() // --- - function test_activateNextState_getState_HappyPath() external { + function test_activateNextState_getPersistedAndEffectiveState_HappyPath() external { assertEq(_dualGovernance.getPersistedState(), State.Normal); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); vm.startPrank(vetoer); _escrow.lockStETH(5 ether); - _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignalling); _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); _escrow.unlockStETH(); - _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignallingDeactivation); _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getEffectiveState(), State.VetoCooldown); + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); + assertEq(_dualGovernance.getEffectiveState(), State.VetoCooldown); _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getPersistedState(), State.Normal); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); _escrow.lockStETH(5 ether); - _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignalling); _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.RageQuit); + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getPersistedState(), State.RageQuit); + assertEq(_dualGovernance.getEffectiveState(), State.RageQuit); } // --- From 4ce0948022e3df7363adaa57cb6da74e6e33dffc Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 25 Sep 2024 03:12:52 +0400 Subject: [PATCH 78/86] DualGovernance natspec. Rename canCancelAllPendingProposals(). Return value for cancelAllPendingProposals(). --- contracts/DualGovernance.sol | 212 ++++++++++++++++-- contracts/TimelockedGovernance.sol | 3 +- contracts/interfaces/IGovernance.sol | 2 +- .../libraries/DualGovernanceStateMachine.sol | 99 ++++---- 4 files changed, 239 insertions(+), 77 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 5c3416e1..7f943e19 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -21,6 +21,12 @@ import {State, DualGovernanceStateMachine} from "./libraries/DualGovernanceState import {Escrow} from "./Escrow.sol"; +/// @title Dual Governance +/// @notice Main contract for the Dual Governance system, serving as the central interface for proposal submission +/// and scheduling. The contract is organized as a state machine, managing transitions between governance states +/// and coordinating interactions between the Signalling Escrow and Rage Quit Escrow. It enables stETH, wstETH, +/// and unstETH holders to participate in the governance process and influence dynamic timelock mechanisms based +/// on their locked assets. contract DualGovernance is IDualGovernance { using Proposers for Proposers.Context; using Tiebreaker for Tiebreaker.Context; @@ -48,9 +54,19 @@ contract DualGovernance is IDualGovernance { event ResealCommitteeSet(address resealCommittee); // --- - // Tiebreaker Sanity Check Param Immutables + // Sanity Check Parameters // --- + /// @notice The parameters for the sanity checks. + /// @param minWithdrawalsBatchSize The minimum number of withdrawal requests allowed to create during a single call of + /// the `Escrow.requestNextWithdrawalsBatch(batchSize)` method. + /// @param minTiebreakerActivationTimeout The lower bound for the time the Dual Governance must spend in the "locked" state + /// before the tiebreaker committee is allowed to schedule proposals. + /// @param maxTiebreakerActivationTimeout The upper bound for the time the Dual Governance must spend in the "locked" state + /// before the tiebreaker committee is allowed to schedule proposals. + /// @param maxSealableWithdrawalBlockersCount The upper bound for the number of sealable withdrawal blockers allowed to be + /// registered in the Dual Governance. This parameter prevents filling the sealable withdrawal blockers + /// with so many items that tiebreaker calls would revert due to out-of-gas errors. struct SanityCheckParams { uint256 minWithdrawalsBatchSize; Duration minTiebreakerActivationTimeout; @@ -58,13 +74,30 @@ contract DualGovernance is IDualGovernance { uint256 maxSealableWithdrawalBlockersCount; } + /// @notice The lower bound for the time the Dual Governance must spend in the "locked" state + /// before the tiebreaker committee is allowed to schedule proposals. Duration public immutable MIN_TIEBREAKER_ACTIVATION_TIMEOUT; + + /// @notice The upper bound for the time the Dual Governance must spend in the "locked" state + /// before the tiebreaker committee is allowed to schedule proposals. Duration public immutable MAX_TIEBREAKER_ACTIVATION_TIMEOUT; + + /// @notice The upper bound for the number of sealable withdrawal blockers allowed to be + /// registered in the Dual Governance. This parameter prevents filling the sealable withdrawal blockers + /// with so many items that tiebreaker calls would revert due to out-of-gas errors. uint256 public immutable MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT; // --- - // External Parts Immutables + // External Dependencies + // --- + /// @notice The external dependencies of the Dual Governance system. + /// @param stETH The address of the stETH token. + /// @param wstETH The address of the wstETH token. + /// @param withdrawalQueue The address of Lido's Withdrawal Queue and the unstETH token. + /// @param timelock The address of the Timelock contract. + /// @param resealManager The address of the Reseal Manager. + /// @param configProvider The address of the Dual Governance Config Provider. struct ExternalDependencies { IStETH stETH; IWstETH wstETH; @@ -74,21 +107,35 @@ contract DualGovernance is IDualGovernance { IDualGovernanceConfigProvider configProvider; } + /// @notice The address of the Timelock contract. ITimelock public immutable TIMELOCK; + + /// @notice The address of the Reseal Manager. IResealManager public immutable RESEAL_MANAGER; + + /// @notice The address of the Escrow contract used as the implementation for the Signalling and Rage Quit + /// instances of the Escrows managed by the DualGovernance contract. IEscrow public immutable ESCROW_MASTER_COPY; // --- // Aspects // --- + /// @dev The functionality to manage the proposer -> executor pairs. Proposers.Context internal _proposers; + + /// @dev The functionality of the tiebreaker to handle "deadlocks" of the Dual Governance. Tiebreaker.Context internal _tiebreaker; + + /// @dev The state machine implementation controlling the state of the Dual Governance. DualGovernanceStateMachine.Context internal _stateMachine; // --- // Standalone State Variables // --- + + /// @dev The address of the Reseal Committee which is allowed to "reseal" sealables paused for a limited + /// period of time when the Dual Governance proposal adoption is blocked. address internal _resealCommittee; constructor(ExternalDependencies memory dependencies, SanityCheckParams memory sanityCheckParams) { @@ -116,6 +163,17 @@ contract DualGovernance is IDualGovernance { // Proposals Flow // --- + /// @notice Allows a registered proposer to submit a proposal to the Dual Governance system. Proposals can only + /// be submitted if the system is not in the `VetoSignallingDeactivation` or `VetoCooldown` state. + /// Each proposal contains a list of external calls to be executed sequentially, and will revert if + /// any call fails during execution. + /// @param calls An array of `ExternalCall` structs representing the actions to be executed sequentially when + /// the proposal is executed. Each call in the array will be executed one-by-one in the specified order. + /// If any call reverts, the entire proposal execution will revert. + /// @param metadata A string containing additional context or information about the proposal. This can be used + /// to provide a description, rationale, or other details relevant to the proposal. + /// @return proposalId The unique identifier of the submitted proposal. This ID can be used to reference the proposal + /// in future operations (scheduling and execution) with the proposal. function submitProposal( ExternalCall[] calldata calls, string calldata metadata @@ -128,6 +186,12 @@ contract DualGovernance is IDualGovernance { proposalId = TIMELOCK.submit(proposer.executor, calls, metadata); } + /// @notice Schedules a previously submitted proposal for execution in the Dual Governance system. + /// The proposal can only be scheduled if the current state allows scheduling of the given proposal based on + /// the submission time, when the `Escrow.getAfterScheduleDelay()` has passed and proposal wasn't cancelled + /// or scheduled earlier. + /// @param proposalId The unique identifier of the proposal to be scheduled. This ID is obtained when the proposal + /// is initially submitted to the timelock contract. function scheduleProposal(uint256 proposalId) external { _stateMachine.activateNextState(ESCROW_MASTER_COPY); Timestamp proposalSubmittedAt = TIMELOCK.getProposalDetails(proposalId).submittedAt; @@ -137,7 +201,14 @@ contract DualGovernance is IDualGovernance { TIMELOCK.schedule(proposalId); } - function cancelAllPendingProposals() external { + /// @notice Allows a proposer associated with the admin executor to cancel all previously submitted or scheduled + /// but not yet executed proposals when the Dual Governance system is in the `VetoSignalling` + /// or `VetoSignallingDeactivation` state. + /// @dev If the Dual Governance state is not `VetoSignalling` or `VetoSignallingDeactivation`, the function will + /// exit early, emitting the `CancelAllPendingProposalsSkipped` event without canceling any proposals. + /// @return isProposalsCancelled A boolean indicating whether the proposals were successfully canceled (`true`) + /// or the cancellation was skipped due to an inappropriate state (`false`). + function cancelAllPendingProposals() external returns (bool) { _stateMachine.activateNextState(ESCROW_MASTER_COPY); Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); @@ -145,68 +216,122 @@ contract DualGovernance is IDualGovernance { revert NotAdminProposer(); } - if (!_stateMachine.canCancelAllProposals({useEffectiveState: false})) { + if (!_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})) { /// @dev Some proposer contracts, like Aragon Voting, may not support canceling decisions that have already - /// reached consensus. This could lead to a situation where a proposer’s cancelAllPendingProposals() call - /// becomes unexecutable if the Dual Governance state changes. However, it might become executable again if - /// the system state shifts back to VetoSignalling or VetoSignallingDeactivation. - /// To avoid such a scenario, an early return is used instead of a revert when proposals cannot be canceled - /// due to an unsuitable Dual Governance state. + /// reached consensus. This could lead to a situation where a proposer’s cancelAllPendingProposals() call + /// becomes unexecutable if the Dual Governance state changes. However, it might become executable again if + /// the system state shifts back to VetoSignalling or VetoSignallingDeactivation. + /// To avoid such a scenario, an early return is used instead of a revert when proposals cannot be canceled + /// due to an unsuitable Dual Governance state. emit CancelAllPendingProposalsSkipped(); - return; + return false; } TIMELOCK.cancelAllNonExecutedProposals(); emit CancelAllPendingProposalsExecuted(); + return true; } + /// @notice Returns whether proposal submission is allowed based on the current `effective` state of the Dual Governance system. + /// @dev Proposal submission is forbidden in the `VetoSignalling` and `VetoSignallingDeactivation` states. + /// @return canSubmitProposal A boolean value indicating whether proposal submission is allowed (`true`) or not (`false`) + /// based on the current `effective` state of the Dual Governance system. function canSubmitProposal() external view returns (bool) { return _stateMachine.canSubmitProposal({useEffectiveState: true}); } + /// @notice Returns whether a previously submitted proposal can be scheduled for execution based on the `effective` + /// state of the Dual Governance system, the proposal's submission time, and its current status. + /// @dev Proposal scheduling is allowed only if all the following conditions are met: + /// - The Dual Governance system is in the `Normal` or `VetoCooldown` state. + /// - If the system is in the `VetoCooldown` state, the proposal must have been submitted before the system + /// last entered the `VetoSignalling` state. + /// - The proposal has not already been scheduled, canceled, or executed. + /// - The required delay period, as defined by `Escrow.getAfterSubmitDelay()`, has elapsed since the proposal + /// was submitted. + /// @param proposalId The unique identifier of the proposal to check. + /// @return canScheduleProposal A boolean value indicating whether the proposal can be scheduled (`true`) or + /// not (`false`) based on the current `effective` state of the Dual Governance system and the proposal's status. function canScheduleProposal(uint256 proposalId) external view returns (bool) { Timestamp proposalSubmittedAt = TIMELOCK.getProposalDetails(proposalId).submittedAt; return _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) && TIMELOCK.canSchedule(proposalId); } - function canCancelAllProposals() external view returns (bool) { - return _stateMachine.canCancelAllProposals({useEffectiveState: true}); + /// @notice Indicates whether the cancellation of all pending proposals is allowed based on the `effective` state + /// of the Dual Governance system, ensuring that the cancellation will not be skipped when calling the + /// `DualGovernance.cancelAllPendingProposals()` method. + /// @dev Proposal cancellation is only allowed when the Dual Governance system is in the `VetoSignalling` or + /// `VetoSignallingDeactivation` states. In any other state, the cancellation will be skipped and no proposals + /// will be canceled. + /// @return canCancelAllPendingProposals A boolean value indicating whether the pending proposals can be + /// canceled (`true`) or not (`false`) based on the current `effective` state of the Dual Governance system. + function canCancelAllPendingProposals() external view returns (bool) { + return _stateMachine.canCancelAllPendingProposals({useEffectiveState: true}); } // --- // Dual Governance State // --- + /// @notice Updates the state of the Dual Governance State Machine if a state transition is possible. + /// @dev This function should be called when the `persisted` and `effective` states of the system are not equal. + /// If the states are already synchronized, the function will complete without making any changes to the system state. function activateNextState() external { _stateMachine.activateNextState(ESCROW_MASTER_COPY); } + /// @notice Updates the address of the configuration provider for the Dual Governance system. + /// @param newConfigProvider The address of the new configuration provider contract. function setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) external { _checkCallerIsAdminExecutor(); _stateMachine.setConfigProvider(newConfigProvider); } + /// @notice Returns the current configuration provider address for the Dual Governance system. + /// @return configProvider The address of the current configuration provider contract. function getConfigProvider() external view returns (IDualGovernanceConfigProvider) { return _stateMachine.configProvider; } + /// @notice Returns the address of the veto signaling escrow contract. + /// @return vetoSignallingEscrow The address of the veto signaling escrow contract. function getVetoSignallingEscrow() external view returns (address) { return address(_stateMachine.signallingEscrow); } + /// @notice Returns the address of the rage quit escrow contract used in the most recent or ongoing rage quit. + /// @dev The returned address will be the zero address if no rage quits have occurred in the system. + /// @return rageQuitEscrow The address of the rage quit escrow contract. function getRageQuitEscrow() external view returns (address) { return address(_stateMachine.rageQuitEscrow); } + /// @notice Returns the most recently stored (`persisted`) state of the Dual Governance State Machine. + /// @return persistedState The current persisted state of the system. function getPersistedState() external view returns (State persistedState) { persistedState = _stateMachine.getPersistedState(); } + /// @notice Returns the current `effective` state of the Dual Governance State Machine. + /// @dev The effective state represents the state the system would transition to upon calling `activateNextState()`. + /// @return effectiveState The current effective state of the system. function getEffectiveState() external view returns (State effectiveState) { effectiveState = _stateMachine.getEffectiveState(); } + /// @notice Returns detailed information about the current state of the Dual Governance State Machine. + /// @return stateDetails A struct containing comprehensive details about the current state of the system, including: + /// - `effectiveState`: The `effective` state of the Dual Governance system. + /// - `persistedState`: The `persisted` state of the Dual Governance system. + /// - `persistedStateEnteredAt`: The timestamp when the system entered the current `persisted` state. + /// - `vetoSignallingActivatedAt`: The timestamp when `VetoSignalling` was last activated. + /// - `vetoSignallingReactivationTime`: The timestamp of the last transition from `VetoSignallingDeactivation` + /// to `VetoSignalling`. + /// - `normalOrVetoCooldownExitedAt`: The timestamp of the last exit from either the `Normal` or `VetoCooldown` state. + /// - `rageQuitRound`: The current count of consecutive Rage Quit rounds, starting from 0. + /// - `vetoSignallingDuration`: The expected duration of the `VetoSignalling` state, based on the support for rage quit + /// in the veto signalling escrow contract. function getStateDetails() external view returns (StateDetails memory stateDetails) { return _stateMachine.getStateDetails(); } @@ -215,11 +340,19 @@ contract DualGovernance is IDualGovernance { // Proposers & Executors Management // --- + /// @notice Registers a new proposer with the associated executor in the system. + /// @dev Multiple proposers can share the same executor contract, but each proposer must be unique. + /// @param proposer The address of the proposer to register. + /// @param executor The address of the executor contract associated with the proposer. function registerProposer(address proposer, address executor) external { _checkCallerIsAdminExecutor(); _proposers.register(proposer, executor); } + /// @notice Unregisters a proposer from the system. + /// @dev There must always be at least one proposer associated with the admin executor. If an attempt is made to + /// remove the last proposer assigned to the admin executor, the function will revert. + /// @param proposer The address of the proposer to unregister. function unregisterProposer(address proposer) external { _checkCallerIsAdminExecutor(); _proposers.unregister(proposer); @@ -230,18 +363,33 @@ contract DualGovernance is IDualGovernance { } } + /// @notice Checks whether the given `account` is a registered proposer. + /// @param account The address to check. + /// @return isProposer A boolean value indicating whether the `account` is a registered + /// proposer (`true`) or not (`false`). function isProposer(address account) external view returns (bool) { return _proposers.isProposer(account); } + /// @notice Returns the proposer data if the given `account` is a registered proposer. + /// @param account The address of the proposer to retrieve information for. + /// @return proposer A Proposer struct containing the data of the registered proposer, including: + /// - `account`: The address of the registered proposer. + /// - `executor`: The address of the executor associated with the proposer. function getProposer(address account) external view returns (Proposers.Proposer memory proposer) { proposer = _proposers.getProposer(account); } + /// @notice Returns the information about all registered proposers. + /// @return proposers An array of `Proposer` structs containing the data of all registered proposers. function getProposers() external view returns (Proposers.Proposer[] memory proposers) { proposers = _proposers.getAllProposers(); } + /// @notice Checks whether the given `account` is associated with an executor contract in the system. + /// @param account The address to check. + /// @return isExecutor A boolean value indicating whether the `account` is a registered + /// executor (`true`) or not (`false`). function isExecutor(address account) external view returns (bool) { return _proposers.isExecutor(account); } @@ -250,21 +398,35 @@ contract DualGovernance is IDualGovernance { // Tiebreaker Protection // --- + /// @notice Adds a unique address of a sealable contract that can be paused and may cause a Dual Governance tie (deadlock). + /// @dev A tie may occur when user withdrawal requests cannot be processed due to the paused state of a registered sealable + /// withdrawal blocker while the Dual Governance system is in the RageQuit state. + /// The contract being added must implement the `ISealable` interface. + /// @param sealableWithdrawalBlocker The address of the sealable contract to be added as a tiebreaker withdrawal blocker. function addTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { _checkCallerIsAdminExecutor(); _tiebreaker.addSealableWithdrawalBlocker(sealableWithdrawalBlocker, MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT); } + /// @notice Removes a previously registered sealable contract from the system. + /// @param sealableWithdrawalBlocker The address of the sealable contract to be removed. function removeTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external { _checkCallerIsAdminExecutor(); _tiebreaker.removeSealableWithdrawalBlocker(sealableWithdrawalBlocker); } + /// @notice Sets the new address of the tiebreaker committee in the system. + /// @param tiebreakerCommittee The address of the new tiebreaker committee. function setTiebreakerCommittee(address tiebreakerCommittee) external { _checkCallerIsAdminExecutor(); _tiebreaker.setTiebreakerCommittee(tiebreakerCommittee); } + /// @notice Sets the new value for the tiebreaker activation timeout. + /// @dev If the Dual Governance system remains out of the `Normal` or `VetoCooldown` state for longer than + /// the `tiebreakerActivationTimeout` duration, the tiebreaker committee is allowed to schedule + /// submitted proposals. + /// @param tiebreakerActivationTimeout The new duration for the tiebreaker activation timeout. function setTiebreakerActivationTimeout(Duration tiebreakerActivationTimeout) external { _checkCallerIsAdminExecutor(); _tiebreaker.setTiebreakerActivationTimeout( @@ -272,26 +434,39 @@ contract DualGovernance is IDualGovernance { ); } + /// @notice Allows the tiebreaker committee to resume a paused sealable contract when the system is in a tie state. + /// @param sealable The address of the sealable contract to be resumed. function tiebreakerResumeSealable(address sealable) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); _stateMachine.activateNextState(ESCROW_MASTER_COPY); - _tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); + _tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.normalOrVetoCooldownExitedAt); RESEAL_MANAGER.resume(sealable); } + /// @notice Allows the tiebreaker committee to schedule for execution a submitted proposal when + /// the system is in a tie state. + /// @param proposalId The unique identifier of the proposal to be scheduled. function tiebreakerScheduleProposal(uint256 proposalId) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); _stateMachine.activateNextState(ESCROW_MASTER_COPY); - _tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.getNormalOrVetoCooldownStateExitedAt()); + _tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.normalOrVetoCooldownExitedAt); TIMELOCK.schedule(proposalId); } + /// @notice Returns detailed information about the current tiebreaker state based on the `effective` state of the system. + /// @return tiebreakerState A struct containing detailed information about the current state of the tiebreaker system, including: + /// - `isTie`: Indicates whether the system is in a tie state, allowing the tiebreaker committee to schedule proposals + /// or resume sealable contracts. + /// - `tiebreakerCommittee`: The address of the current tiebreaker committee. + /// - `tiebreakerActivationTimeout`: The required duration the system must remain in a "locked" state + /// (not in `Normal` or `VetoCooldown` state) before the tiebreaker committee is permitted to take actions. + /// - `sealableWithdrawalBlockers`: An array of sealable contracts registered in the system as withdrawal blockers. function getTiebreakerDetails() external view returns (ITiebreaker.TiebreakerDetails memory tiebreakerState) { return _tiebreaker.getTiebreakerDetails( /// @dev Calling getEffectiveState() doesn't update the normalOrVetoCooldownStateExitedAt value, - /// but this does not distort the result of getTiebreakerDetails() + /// but this does not distort the result of getTiebreakerDetails() _stateMachine.getEffectiveState(), - _stateMachine.getNormalOrVetoCooldownStateExitedAt() + _stateMachine.normalOrVetoCooldownExitedAt ); } @@ -299,6 +474,9 @@ contract DualGovernance is IDualGovernance { // Reseal executor // --- + /// @notice Allows the reseal committee to "reseal" (pause indefinitely) an instance of a sealable contract through + /// the ResealManager contract. + /// @param sealable The address of the sealable contract to be resealed. function resealSealable(address sealable) external { _stateMachine.activateNextState(ESCROW_MASTER_COPY); if (msg.sender != _resealCommittee) { @@ -310,6 +488,8 @@ contract DualGovernance is IDualGovernance { RESEAL_MANAGER.reseal(sealable); } + /// @notice Sets the address of the reseal committee. + /// @param resealCommittee The address of the new reseal committee. function setResealCommittee(address resealCommittee) external { _checkCallerIsAdminExecutor(); _resealCommittee = resealCommittee; diff --git a/contracts/TimelockedGovernance.sol b/contracts/TimelockedGovernance.sol index 1003b7d5..6cd4ff68 100644 --- a/contracts/TimelockedGovernance.sol +++ b/contracts/TimelockedGovernance.sol @@ -53,9 +53,10 @@ contract TimelockedGovernance is IGovernance { } /// @dev Cancels all pending proposals that have not been executed. - function cancelAllPendingProposals() external { + function cancelAllPendingProposals() external returns (bool) { _checkCallerIsGovernance(); TIMELOCK.cancelAllNonExecutedProposals(); + return true; } /// @dev Checks if the msg.sender is the governance address. diff --git a/contracts/interfaces/IGovernance.sol b/contracts/interfaces/IGovernance.sol index 0b567eea..51652b40 100644 --- a/contracts/interfaces/IGovernance.sol +++ b/contracts/interfaces/IGovernance.sol @@ -12,7 +12,7 @@ interface IGovernance { string calldata metadata ) external returns (uint256 proposalId); function scheduleProposal(uint256 proposalId) external; - function cancelAllPendingProposals() external; + function cancelAllPendingProposals() external returns (bool); function canScheduleProposal(uint256 proposalId) external view returns (bool); } diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 5b818c7d..5a58879c 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -16,19 +16,19 @@ import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; /// @notice Enum describing the state of the Dual Governance State Machine /// @param Unset The initial (uninitialized) state of the Dual Governance State Machine. The state machine cannot -/// operate in this state and must be initialized before use. +/// operate in this state and must be initialized before use. /// @param Normal The default state where the system is expected to remain most of the time. In this state, proposals -/// can be both submitted and scheduled for execution. +/// can be both submitted and scheduled for execution. /// @param VetoSignalling Represents active opposition to DAO decisions. In this state, the scheduling of proposals -/// is blocked, but the submission of new proposals is still allowed. +/// is blocked, but the submission of new proposals is still allowed. /// @param VetoSignallingDeactivation A sub-state of VetoSignalling, allowing users to observe the deactivation process -/// and react before non-cancelled proposals are scheduled for execution. Both proposal submission and scheduling -/// are prohibited in this state. +/// and react before non-cancelled proposals are scheduled for execution. Both proposal submission and scheduling +/// are prohibited in this state. /// @param VetoCooldown A state where the DAO can execute non-cancelled proposals but is prohibited from submitting -/// new proposals. +/// new proposals. /// @param RageQuit Represents the process where users opting to leave the protocol can withdraw their funds. This state -/// is triggered when the Second Seal Threshold is crossed. During this state, the scheduling of proposals for -/// execution is forbidden, but new proposals can still be submitted. +/// is triggered when the Second Seal Threshold is crossed. During this state, the scheduling of proposals for +/// execution is forbidden, but new proposals can still be submitted. enum State { Unset, Normal, @@ -38,7 +38,7 @@ enum State { RageQuit } -/// @title Dual Governance State Machine +/// @title Dual Governance State Machine Library /// @notice Library containing the core logic for managing the states of the Dual Governance system library DualGovernanceStateMachine { using DualGovernanceStateTransitions for Context; @@ -50,7 +50,7 @@ library DualGovernanceStateMachine { /// @notice Represents the context of the Dual Governance State Machine. /// @param state The last recorded state of the Dual Governance State Machine. - /// @param enteredAt The timestamp when the current `state` was entered. + /// @param enteredAt The timestamp when the current `persisted` `state` was entered. /// @param vetoSignallingActivatedAt The timestamp when the VetoSignalling state was last activated. /// @param signallingEscrow The address of the Escrow contract used for VetoSignalling. /// @param rageQuitRound The number of continuous Rage Quit rounds, starting at 0 and capped at MAX_RAGE_QUIT_ROUND. @@ -110,7 +110,7 @@ library DualGovernanceStateMachine { /// @param self The context of the Dual Governance State Machine to be initialized. /// @param configProvider The address of the Dual Governance State Machine configuration provider. /// @param escrowMasterCopy The address of the master copy used as the implementation for the minimal proxy deployment - /// of a Signalling Escrow instance. + /// of a Signalling Escrow instance. function initialize( Context storage self, IDualGovernanceConfigProvider configProvider, @@ -132,13 +132,13 @@ library DualGovernanceStateMachine { } /// @notice Executes a state transition for the Dual Governance State Machine, if applicable. - /// If no transition is possible from the current state, no changes are applied to the context. + /// If no transition is possible from the current `persisted` state, no changes are applied to the context. /// @dev If the state transitions to RageQuit, a new instance of the Signalling Escrow is deployed using - /// `escrowMasterCopy` as the implementation for the minimal proxy, while the previous Signalling Escrow - /// instance is converted into the RageQuit escrow. + /// `escrowMasterCopy` as the implementation for the minimal proxy, while the previous Signalling Escrow + /// instance is converted into the RageQuit escrow. /// @param self The context of the Dual Governance State Machine. /// @param escrowMasterCopy The address of the master copy used as the implementation for the minimal proxy - /// to deploy a new instance of the Signalling Escrow. + /// to deploy a new instance of the Signalling Escrow. function activateNextState(Context storage self, IEscrow escrowMasterCopy) internal { DualGovernanceConfig.Context memory config = getDualGovernanceConfig(self); (State currentState, State newState) = self.getStateTransition(config); @@ -168,7 +168,7 @@ library DualGovernanceStateMachine { uint256 currentRageQuitRound = self.rageQuitRound; /// @dev Limits the maximum value of the rage quit round to prevent failures due to arithmetic overflow - /// if the number of continuous rage quits reaches MAX_RAGE_QUIT_ROUND. + /// if the number of continuous rage quits reaches MAX_RAGE_QUIT_ROUND. uint256 newRageQuitRound = Math.min(currentRageQuitRound + 1, MAX_RAGE_QUIT_ROUND); self.rageQuitRound = uint8(newRageQuitRound); @@ -199,10 +199,10 @@ library DualGovernanceStateMachine { // Getters // --- - /// @notice Returns detailed information about the current state of the Dual Governance State Machine. + /// @notice Returns detailed information about the state of the Dual Governance State Machine. /// @param self The context of the Dual Governance State Machine. - /// @return stateDetails A struct containing detailed information about the current state of - /// the Dual Governance State Machine. + /// @return stateDetails A struct containing detailed information about the state of + /// the Dual Governance State Machine. function getStateDetails(Context storage self) internal view @@ -229,28 +229,21 @@ library DualGovernanceStateMachine { /// @notice Returns the effective state of the Dual Governance State Machine. /// @dev The effective state refers to the state the Dual Governance State Machine would transition to - /// upon calling `activateNextState()`. + /// upon calling `activateNextState()`. /// @param self The context of the Dual Governance State Machine. /// @return effectiveState The state that will become active after the next state transition. - /// If the `activateNextState` call does not trigger a state transition, `effectiveState` - /// will be the same as `persistedState`. + /// If the `activateNextState` call does not trigger a state transition, `effectiveState` + /// will be the same as `persistedState`. function getEffectiveState(Context storage self) internal view returns (State effectiveState) { ( /* persistedState */ , effectiveState) = self.getStateTransition(getDualGovernanceConfig(self)); } - /// @notice Returns the timestamp when the system last exited the Normal or VetoCooldown state. - /// @param self The context of the Dual Governance State Machine. - /// @return The timestamp indicating when the Normal or VetoCooldown state was last exited. - function getNormalOrVetoCooldownStateExitedAt(Context storage self) internal view returns (Timestamp) { - return self.normalOrVetoCooldownExitedAt; - } - /// @notice Returns whether the submission of proposals is allowed based on the `persisted` or `effective` state, - /// depending on the `useEffectiveState` value. + /// depending on the `useEffectiveState` value. /// @param self The context of the Dual Governance State Machine. - /// @param useEffectiveState If `true`, the check is performed against the effective state (the state - /// the Dual Governance State Machine will enter after the next `activateNextState` call). If `false`, - /// the check is performed using the persisted state (the current state of the Dual Governance State Machine). + /// @param useEffectiveState If `true`, the check is performed against the `effective` state, which represents the state + /// the Dual Governance State Machine will enter after the next `activateNextState` call. If `false`, the check is + /// performed against the `persisted` state, which is the currently stored state of the system. /// @return A boolean indicating whether the submission of proposals is allowed in the selected state. function canSubmitProposal(Context storage self, bool useEffectiveState) internal view returns (bool) { State state = useEffectiveState ? getEffectiveState(self) : getPersistedState(self); @@ -258,12 +251,11 @@ library DualGovernanceStateMachine { } /// @notice Determines whether scheduling a proposal for execution is allowed, based on either the `persisted` - /// or `effective` state, depending on the `useEffectiveState` flag. + /// or `effective` state, depending on the `useEffectiveState` flag. /// @param self The context of the Dual Governance State Machine. - /// @param useEffectiveState If `true`, the check is performed against the effective state (the state - /// the Dual Governance State Machine will transition to after the next `activateNextState` call). - /// If `false`, the check is performed using the persisted state (the current state of the Dual Governance - /// State Machine). + /// @param useEffectiveState If `true`, the check is performed against the `effective` state, which represents the state + /// the Dual Governance State Machine will enter after the next `activateNextState` call. If `false`, the check is + /// performed against the `persisted` state, which is the currently stored state of the system. /// @param proposalSubmittedAt The timestamp indicating when the proposal to be scheduled was originally submitted. /// @return A boolean indicating whether scheduling the proposal is allowed in the chosen state. function canScheduleProposal( @@ -278,30 +270,19 @@ library DualGovernanceStateMachine { } /// @notice Returns whether the cancelling of the proposals is allowed based on the `persisted` or `effective` - /// state, depending on the `useEffectiveState` value. + /// state, depending on the `useEffectiveState` value. /// @param self The context of the Dual Governance State Machine. - /// @param useEffectiveState If `true`, the check is performed against the effective state (the state - /// the Dual Governance State Machine will enter after the next `activateNextState` call). If `false`, - /// the check is performed using the persisted state (the current state of the Dual Governance State Machine). + /// @param useEffectiveState If `true`, the check is performed against the `effective` state, which represents the state + /// the Dual Governance State Machine will enter after the next `activateNextState` call. If `false`, the check is + /// performed against the `persisted` state, which is the currently stored state of the system. /// @return A boolean indicating whether the cancelling of proposals is allowed in the selected state. - function canCancelAllProposals(Context storage self, bool useEffectiveState) internal view returns (bool) { + function canCancelAllPendingProposals(Context storage self, bool useEffectiveState) internal view returns (bool) { State state = useEffectiveState ? getEffectiveState(self) : getPersistedState(self); return state == State.VetoSignalling || state == State.VetoSignallingDeactivation; } - /// @notice Returns the address of the Dual Governance Config Provider. - /// @param self The context of the Dual Governance State Machine. - /// @return The address of the current Dual Governance Config Provider. - function getDualGovernanceConfigProvider(Context storage self) - internal - view - returns (IDualGovernanceConfigProvider) - { - return self.configProvider; - } - /// @notice Returns the configuration of the Dual Governance State Machine as provided by - /// the Dual Governance Config Provider. + /// the Dual Governance Config Provider. /// @param self The context of the Dual Governance State Machine. /// @return The current configuration of the Dual Governance State function getDualGovernanceConfig(Context storage self) @@ -339,19 +320,19 @@ library DualGovernanceStateMachine { } } -/// @title Dual Governance State Transitions +/// @title Dual Governance State Transitions Library /// @notice Library containing the transitions logic for the Dual Governance system library DualGovernanceStateTransitions { using DualGovernanceConfig for DualGovernanceConfig.Context; /// @notice Returns the allowed state transition for the Dual Governance State Machine. - /// If no state transition is possible, `currentState` will be equal to `nextState`. + /// If no state transition is possible, `currentState` will be equal to `nextState`. /// @param self The context of the Dual Governance State Machine. /// @param config The configuration of the Dual Governance State Machine to use for determining - /// state transitions. + /// state transitions. /// @return currentState The current state of the Dual Governance State Machine. /// @return nextState The next state of the Dual Governance State Machine if a transition - /// is possible, otherwise it will be the same as `currentState`. + /// is possible, otherwise it will be the same as `currentState`. function getStateTransition( DualGovernanceStateMachine.Context storage self, DualGovernanceConfig.Context memory config From 7b164ad916a4a9f831542425964a16a770074da1 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 25 Sep 2024 03:14:35 +0400 Subject: [PATCH 79/86] Additional tests DualGovernance & DualGovernanceStateMachine --- test/unit/DualGovernance.t.sol | 227 +++++++++ .../DualGovernanceStateMachine.t.sol | 460 +++++++++++++++++- .../DualGovernanceStateTransitions.t.sol | 391 +++++++++++++++ 3 files changed, 1077 insertions(+), 1 deletion(-) create mode 100644 test/unit/libraries/DualGovernanceStateTransitions.t.sol diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 3756f483..9a30abe1 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -774,6 +774,83 @@ contract DualGovernanceUnitTests is UnitTest { assertTrue(_dualGovernance.canScheduleProposal(proposalIdSubmittedAfterVetoSignalling)); } + // --- + // canCancelAllPendingProposals() + // --- + + function test_canCancelAllPendingProposals_HappyPath() external { + assertEq(_dualGovernance.getPersistedState(), State.Normal); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); + + assertFalse(_dualGovernance.canCancelAllPendingProposals()); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignalling); + + assertTrue(_dualGovernance.canCancelAllPendingProposals()); + + _wait(_configProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); + _escrow.unlockStETH(); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignallingDeactivation); + + assertTrue(_dualGovernance.canCancelAllPendingProposals()); + + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getEffectiveState(), State.VetoCooldown); + + assertFalse(_dualGovernance.canCancelAllPendingProposals()); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); + assertEq(_dualGovernance.getEffectiveState(), State.VetoCooldown); + + assertFalse(_dualGovernance.canCancelAllPendingProposals()); + + _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); + + assertFalse(_dualGovernance.canCancelAllPendingProposals()); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.Normal); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); + + assertFalse(_dualGovernance.canCancelAllPendingProposals()); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignalling); + + assertTrue(_dualGovernance.canCancelAllPendingProposals()); + + _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.RageQuit); + + assertFalse(_dualGovernance.canCancelAllPendingProposals()); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.RageQuit); + assertEq(_dualGovernance.getEffectiveState(), State.RageQuit); + + assertFalse(_dualGovernance.canCancelAllPendingProposals()); + } + // --- // activateNextState() & getPersistedState() & getEffectiveState() // --- @@ -1647,6 +1724,156 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(details.sealableWithdrawalBlockers[0], sealable); } + function test_getTiebreakerDetails_IsTieInDifferentEffectivePersistedStates() external { + address tiebreakerCommittee = makeAddr("TIEBREAKER_COMMITTEE"); + address sealable = address(new SealableMock()); + Duration tiebreakerActivationTimeout = Durations.from(180 days); + + // for the correctness of the test, the following assumption must be true + assertTrue(tiebreakerActivationTimeout >= _configProvider.VETO_SIGNALLING_MAX_DURATION()); + + // setup tiebreaker + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerCommittee.selector, tiebreakerCommittee) + ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerActivationTimeout.selector, tiebreakerActivationTimeout) + ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.addTiebreakerSealableWithdrawalBlocker.selector, sealable) + ); + + assertEq(_dualGovernance.getPersistedState(), State.Normal); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + vm.prank(vetoer); + _escrow.lockStETH(5 ether); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignalling); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + _wait(_configProvider.VETO_SIGNALLING_MIN_DURATION().dividedBy(2)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignalling); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + vm.prank(vetoer); + _escrow.unlockStETH(); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignallingDeactivation); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION()); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignallingDeactivation); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + vm.prank(vetoer); + _escrow.lockStETH(5 ether); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignalling); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION()); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.RageQuit); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + // Simulate the sealable withdrawal blocker was paused + vm.mockCall(address(sealable), abi.encodeWithSelector(SealableMock.isPaused.selector), abi.encode(true)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.RageQuit); + // TiebreakerDetails.isTie correctly returns true even if persisted state is outdated + assertTrue(_dualGovernance.getTiebreakerDetails().isTie); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.RageQuit); + assertEq(_dualGovernance.getEffectiveState(), State.RageQuit); + assertTrue(_dualGovernance.getTiebreakerDetails().isTie); + + // Return sealable to unpaused state for further testing + vm.mockCall(address(sealable), abi.encodeWithSelector(SealableMock.isPaused.selector), abi.encode(false)); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + _wait(tiebreakerActivationTimeout); + + assertEq(_dualGovernance.getPersistedState(), State.RageQuit); + assertEq(_dualGovernance.getEffectiveState(), State.RageQuit); + assertTrue(_dualGovernance.getTiebreakerDetails().isTie); + + // simulate the rage quit was finalized but new veto signalling in progress + vm.mockCall( + _dualGovernance.getRageQuitEscrow(), + abi.encodeWithSelector(Escrow.isRageQuitFinalized.selector), + abi.encode(true) + ); + vm.mockCall( + _dualGovernance.getVetoSignallingEscrow(), + abi.encodeWithSelector(Escrow.getRageQuitSupport.selector), + abi.encode(_configProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1)) + ); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignalling); + assertTrue(_dualGovernance.getTiebreakerDetails().isTie); + + _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().dividedBy(2)); + + vm.mockCall( + _dualGovernance.getVetoSignallingEscrow(), + abi.encodeWithSelector(Escrow.getRageQuitSupport.selector), + abi.encode(PercentsD16.fromBasisPoints(1_00)) + ); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignallingDeactivation); + assertTrue(_dualGovernance.getTiebreakerDetails().isTie); + + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_dualGovernance.getEffectiveState(), State.VetoCooldown); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); + assertEq(_dualGovernance.getEffectiveState(), State.VetoCooldown); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.Normal); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + } + // --- // resealSealable() // --- diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 7f0737ec..829d5b87 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -4,7 +4,8 @@ pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Durations} from "contracts/types/Duration.sol"; -import {PercentsD16} from "contracts/types/PercentD16.sol"; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; +import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; import {DualGovernanceStateMachine, State} from "contracts/libraries/DualGovernanceStateMachine.sol"; import { @@ -46,6 +47,15 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { _stateMachine.initialize(_CONFIG_PROVIDER, _ESCROW_MASTER_COPY); } + function test_initialize_RevertOn_ReInitialization() external { + vm.expectRevert(DualGovernanceStateMachine.AlreadyInitialized.selector); + this.external__initialize(); + } + + // --- + // activateNextState() + // --- + function test_activateNextState_HappyPath_MaxRageQuitsRound() external { assertEq(_stateMachine.state, State.Normal); @@ -83,4 +93,452 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { assertEq(_stateMachine.rageQuitRound, 0); assertEq(_stateMachine.state, State.Normal); } + + // --- + // canSubmitProposal() + // --- + + function test_canSubmitProposal_HappyPath() external { + address signallingEscrow = address(_stateMachine.signallingEscrow); + + assertEq(_stateMachine.getPersistedState(), State.Normal); + assertEq(_stateMachine.getEffectiveState(), State.Normal); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); + + // simulate the first threshold of veto signalling was reached + EscrowMock(signallingEscrow).__setRageQuitSupport( + _CONFIG_PROVIDER.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentD16.wrap(1) + ); + + assertEq(_stateMachine.getPersistedState(), State.Normal); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); + + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MIN_DURATION().plusSeconds(1 minutes)); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); + assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: true})); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); + assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: false})); + assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: true})); + + // simulate the second threshold of veto signalling was reached + EscrowMock(signallingEscrow).__setRageQuitSupport( + _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); + assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: false})); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); + assertEq(_stateMachine.getEffectiveState(), State.RageQuit); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.RageQuit); + assertEq(_stateMachine.getEffectiveState(), State.RageQuit); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + + EscrowMock(address(_stateMachine.rageQuitEscrow)).__setIsRageQuitFinalized(true); + + assertEq(_stateMachine.getPersistedState(), State.RageQuit); + assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); + assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: true})); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); + assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); + assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: false})); + assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: true})); + + _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); + + assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); + assertEq(_stateMachine.getEffectiveState(), State.Normal); + assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: false})); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.Normal); + assertEq(_stateMachine.getEffectiveState(), State.Normal); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); + } + + // --- + // canScheduleProposal() + // --- + + function test_canScheduleProposal_HappyPath() external { + address signallingEscrow = address(_stateMachine.signallingEscrow); + Timestamp proposalSubmittedAt = Timestamps.now(); + + assertEq(_stateMachine.getPersistedState(), State.Normal); + assertEq(_stateMachine.getEffectiveState(), State.Normal); + assertTrue( + _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) + ); + assertTrue( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) + ); + + // simulate the first threshold of veto signalling was reached + EscrowMock(signallingEscrow).__setRageQuitSupport( + _CONFIG_PROVIDER.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentD16.wrap(1) + ); + + assertEq(_stateMachine.getPersistedState(), State.Normal); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); + assertTrue( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) + ); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) + ); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) + ); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) + ); + + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MIN_DURATION().plusSeconds(1 minutes)); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) + ); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) + ); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) + ); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) + ); + + // simulate the second threshold of veto signalling was reached + EscrowMock(signallingEscrow).__setRageQuitSupport( + _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) + ); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) + ); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) + ); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) + ); + + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); + assertEq(_stateMachine.getEffectiveState(), State.RageQuit); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) + ); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) + ); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.RageQuit); + assertEq(_stateMachine.getEffectiveState(), State.RageQuit); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) + ); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) + ); + + EscrowMock(address(_stateMachine.rageQuitEscrow)).__setIsRageQuitFinalized(true); + + assertEq(_stateMachine.getPersistedState(), State.RageQuit); + assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) + ); + assertTrue( + _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) + ); + // for proposals submitted at the same block the VetoSignalling started scheduling is allowed + assertTrue( + _stateMachine.canScheduleProposal({ + useEffectiveState: true, + proposalSubmittedAt: _stateMachine.vetoSignallingActivatedAt + }) + ); + // for proposals submitted after the VetoSignalling started scheduling is forbidden + assertFalse( + _stateMachine.canScheduleProposal({ + useEffectiveState: true, + proposalSubmittedAt: Durations.from(1 seconds).addTo(_stateMachine.vetoSignallingActivatedAt) + }) + ); + assertFalse(_stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: Timestamps.now()})); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); + assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); + + // persisted + assertTrue( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) + ); + assertTrue( + _stateMachine.canScheduleProposal({ + useEffectiveState: false, + proposalSubmittedAt: _stateMachine.vetoSignallingActivatedAt + }) + ); + // for proposals submitted after the VetoSignalling started scheduling is forbidden + assertFalse( + _stateMachine.canScheduleProposal({ + useEffectiveState: false, + proposalSubmittedAt: Durations.from(1 seconds).addTo(_stateMachine.vetoSignallingActivatedAt) + }) + ); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: Timestamps.now()}) + ); + + // effective + assertTrue( + _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) + ); + // for proposals submitted at the same block the VetoSignalling started scheduling is allowed + assertTrue( + _stateMachine.canScheduleProposal({ + useEffectiveState: true, + proposalSubmittedAt: _stateMachine.vetoSignallingActivatedAt + }) + ); + // for proposals submitted after the VetoSignalling started scheduling is forbidden + assertFalse( + _stateMachine.canScheduleProposal({ + useEffectiveState: true, + proposalSubmittedAt: Durations.from(1 seconds).addTo(_stateMachine.vetoSignallingActivatedAt) + }) + ); + assertFalse(_stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: Timestamps.now()})); + + _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); + + assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); + assertEq(_stateMachine.getEffectiveState(), State.Normal); + + // persisted + assertTrue( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) + ); + assertTrue( + _stateMachine.canScheduleProposal({ + useEffectiveState: false, + proposalSubmittedAt: _stateMachine.vetoSignallingActivatedAt + }) + ); + // for proposals submitted after the VetoSignalling started scheduling is forbidden + assertFalse( + _stateMachine.canScheduleProposal({ + useEffectiveState: false, + proposalSubmittedAt: Durations.from(1 seconds).addTo(_stateMachine.vetoSignallingActivatedAt) + }) + ); + assertFalse( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: Timestamps.now()}) + ); + + // effective + assertTrue( + _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) + ); + assertTrue(_stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: Timestamps.now()})); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.Normal); + assertEq(_stateMachine.getEffectiveState(), State.Normal); + + // persisted + assertTrue( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) + ); + assertTrue(_stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: Timestamps.now()})); + + // effective + assertTrue( + _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) + ); + assertTrue(_stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: Timestamps.now()})); + } + + // --- + // canCancelAllPendingProposals() + // --- + + function test_canCancelAllPendingProposals_HappyPath() external { + address signallingEscrow = address(_stateMachine.signallingEscrow); + Timestamp proposalSubmittedAt = Timestamps.now(); + + assertEq(_stateMachine.getPersistedState(), State.Normal); + assertEq(_stateMachine.getEffectiveState(), State.Normal); + assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); + + // simulate the first threshold of veto signalling was reached + EscrowMock(signallingEscrow).__setRageQuitSupport( + _CONFIG_PROVIDER.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentD16.wrap(1) + ); + + assertEq(_stateMachine.getPersistedState(), State.Normal); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); + assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); + assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); + assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); + assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MIN_DURATION().plusSeconds(1 minutes)); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); + assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); + assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); + assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); + assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + + // simulate the second threshold of veto signalling was reached + EscrowMock(signallingEscrow).__setRageQuitSupport( + _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); + assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); + assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); + assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); + assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); + assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); + + assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); + assertEq(_stateMachine.getEffectiveState(), State.RageQuit); + assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); + assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.RageQuit); + assertEq(_stateMachine.getEffectiveState(), State.RageQuit); + assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); + assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + + EscrowMock(address(_stateMachine.rageQuitEscrow)).__setIsRageQuitFinalized(true); + + assertEq(_stateMachine.getPersistedState(), State.RageQuit); + assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); + assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); + assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); + assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); + assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); + assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + + _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); + + assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); + assertEq(_stateMachine.getEffectiveState(), State.Normal); + assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); + assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + + _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + + assertEq(_stateMachine.getPersistedState(), State.Normal); + assertEq(_stateMachine.getEffectiveState(), State.Normal); + assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); + assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + } + + // --- + // Test helper methods + // --- + + function external__initialize() external { + _stateMachine.initialize(_CONFIG_PROVIDER, _ESCROW_MASTER_COPY); + } } diff --git a/test/unit/libraries/DualGovernanceStateTransitions.t.sol b/test/unit/libraries/DualGovernanceStateTransitions.t.sol new file mode 100644 index 00000000..a374c919 --- /dev/null +++ b/test/unit/libraries/DualGovernanceStateTransitions.t.sol @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration, Durations} from "contracts/types/Duration.sol"; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; +import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; + +import {IEscrow} from "contracts/interfaces/IEscrow.sol"; + +import { + State, + DualGovernanceStateMachine, + DualGovernanceStateTransitions +} from "contracts/libraries/DualGovernanceStateMachine.sol"; +import { + DualGovernanceConfig, + ImmutableDualGovernanceConfigProvider +} from "contracts/ImmutableDualGovernanceConfigProvider.sol"; + +import {stdError} from "forge-std/StdError.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; + +contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { + using DualGovernanceConfig for DualGovernanceConfig.Context; + using DualGovernanceStateTransitions for DualGovernanceStateMachine.Context; + + DualGovernanceStateMachine.Context internal _stateMachine; + ImmutableDualGovernanceConfigProvider internal _configProvider; + address internal _escrowMasterCopyMock = makeAddr("ESCROW_MOCK"); + + function setUp() external { + _configProvider = new ImmutableDualGovernanceConfigProvider( + DualGovernanceConfig.Context({ + firstSealRageQuitSupport: PercentsD16.fromBasisPoints(3_00), // 3% + secondSealRageQuitSupport: PercentsD16.fromBasisPoints(15_00), // 15% + // + minAssetsLockDuration: Durations.from(5 hours), + // + vetoSignallingMinDuration: Durations.from(3 days), + vetoSignallingMaxDuration: Durations.from(30 days), + vetoSignallingMinActiveDuration: Durations.from(5 hours), + vetoSignallingDeactivationMaxDuration: Durations.from(5 days), + // + vetoCooldownDuration: Durations.from(4 days), + // + rageQuitExtensionPeriodDuration: Durations.from(7 days), + rageQuitEthWithdrawalsMinDelay: Durations.from(30 days), + rageQuitEthWithdrawalsMaxDelay: Durations.from(180 days), + rageQuitEthWithdrawalsDelayGrowth: Durations.from(15 days) + }) + ); + DualGovernanceStateMachine.initialize(_stateMachine, _configProvider, IEscrow(_escrowMasterCopyMock)); + _setMockRageQuitSupportInBP(0); + } + + // --- + // getStateTransition() + // --- + + // --- + // Normal -> Normal + // --- + + function test_getStateTransition_FromNormalToNormal() external { + assertEq(_stateMachine.state, State.Normal); + + _setMockRageQuitSupportInBP(3_00); + + (State current, State next) = _stateMachine.getStateTransition(_configProvider.getDualGovernanceConfig()); + + assertEq(current, State.Normal); + assertEq(next, State.Normal); + } + + // --- + // Normal -> VetoSignalling + // --- + + function test_getStateTransition_FromNormalToVetoSignalling() external { + assertEq(_stateMachine.state, State.Normal); + + _setMockRageQuitSupportInBP(3_01); + + (State current, State next) = _stateMachine.getStateTransition(_configProvider.getDualGovernanceConfig()); + + assertEq(current, State.Normal); + assertEq(next, State.VetoSignalling); + } + + // --- + // VetoSignalling -> VetoSignalling (veto signalling still in progress) + // --- + + function test_getStateTransition_FromVetoSignallingToVetoSignalling_VetoSignallingDurationNotPassed() external { + _setupVetoSignallingState(); + _setMockRageQuitSupportInBP(3_01); + + (State current, State next) = _stateMachine.getStateTransition(_configProvider.getDualGovernanceConfig()); + + assertEq(current, State.VetoSignalling); + assertEq(next, State.VetoSignalling); + } + + // --- + // VetoSignalling -> VetoSignalling (min veto signalling duration not passed) + // --- + + function test_getStateTransition_FromVetoSignallingToVetoSignalling_VetoSignallingReactivationNotPassed() + external + { + _setMockRageQuitSupportInBP(3_01); + + // the veto signalling state was entered + _setupVetoSignallingState(); + + // wait until the duration of the veto signalling is over + _wait(_calcVetoSignallingDuration().plusSeconds(1)); + + // when the duration is over the VetoSignallingDeactivation state must be entered + _assertStateMachineTransition({from: State.VetoSignalling, to: State.VetoSignallingDeactivation}); + + // simulate the reactivation of the VetoSignallingState + _stateMachine.vetoSignallingReactivationTime = Timestamps.now(); + + // while the min veto signalling active duration hasn't passed the VetoSignalling can't be exited + _wait(_configProvider.VETO_SIGNALLING_MIN_ACTIVE_DURATION().dividedBy(2)); + + _assertStateMachineTransition({from: State.VetoSignalling, to: State.VetoSignalling}); + + // but when the duration has passed, the next state should be VetoSignallingDeactivation + _wait(_configProvider.VETO_SIGNALLING_MIN_ACTIVE_DURATION().dividedBy(2).plusSeconds(1)); + + _assertStateMachineTransition({from: State.VetoSignalling, to: State.VetoSignallingDeactivation}); + } + + // --- + // VetoSignalling -> RageQuit + // --- + + function test_getStateTransition_FromVetoSignallingToRageQuit() external { + _setMockRageQuitSupportInBP(15_01); + + // the veto signalling state was entered + _setupVetoSignallingState(); + + // while the full duration of the veto signalling hasn't passed the state machine stays in the VetoSignalling state + _wait(_calcVetoSignallingDuration().dividedBy(2)); + + _assertStateMachineTransition({from: State.VetoSignalling, to: State.VetoSignalling}); + + // when the full duration has passed the state machine should transition to the Rage Quit + _wait(_calcVetoSignallingDuration().dividedBy(2).plusSeconds(1)); + + _assertStateMachineTransition({from: State.VetoSignalling, to: State.RageQuit}); + } + + // --- + // VetoSignallingDeactivation -> VetoSignalling + // --- + + function test_getStateTransition_FromVetoSignallingDeactivationToVetoSignalling() external { + _setMockRageQuitSupportInBP(3_01); + _setupVetoSignallingDeactivationState(); + + _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignallingDeactivation}); + + _setMockRageQuitSupportInBP(15_01); + + _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignalling}); + } + + // --- + // VetoSignallingDeactivation -> RageQuit + // --- + + function test_getStateTransition_FromVetoSignallingDeactivationToRageQuit() external { + _setMockRageQuitSupportInBP(3_01); + _setupVetoSignallingDeactivationState(); + + _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignallingDeactivation}); + + _setMockRageQuitSupportInBP(15_01); + + _wait(_calcVetoSignallingDuration().plusSeconds(1 seconds)); + + _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.RageQuit}); + } + + // --- + // VetoSignallingDeactivation -> VetoCooldown + // --- + + function test_getStateTransition_FromVetoSignallingDeactivationToVetoCooldown() external { + _setMockRageQuitSupportInBP(3_01); + _setupVetoSignallingDeactivationState(); + + _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignallingDeactivation}); + + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoCooldown}); + } + + // --- + // VetoSignallingDeactivation -> VetoSignallingDeactivation + // --- + + function test_getStateTransition_FromVetoSignallingDeactivationToVetoSignallingDeactivation() external { + _setMockRageQuitSupportInBP(3_01); + _setupVetoSignallingDeactivationState(); + + _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignallingDeactivation}); + + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION()); + + _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignallingDeactivation}); + + _wait(Durations.from(1 seconds)); + + _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoCooldown}); + } + + // --- + // VetoCooldown -> VetoCooldown + // --- + + function test_getStateTransition_FromVetoCooldownToVetoCooldown() external { + _setupVetoCooldownState(); + + _assertStateMachineTransition({from: State.VetoCooldown, to: State.VetoCooldown}); + + _wait(_configProvider.VETO_COOLDOWN_DURATION().dividedBy(2)); + + _assertStateMachineTransition({from: State.VetoCooldown, to: State.VetoCooldown}); + + _wait(_configProvider.VETO_COOLDOWN_DURATION().dividedBy(2).plusSeconds(1)); + + _assertStateMachineTransition({from: State.VetoCooldown, to: State.Normal}); + } + + // --- + // VetoCooldown -> VetoSignalling + // --- + + function test_getStateTransition_FromVetoCooldownToVetoSignalling() external { + _setupVetoCooldownState(); + + _assertStateMachineTransition({from: State.VetoCooldown, to: State.VetoCooldown}); + + _setMockRageQuitSupportInBP(3_01); + + _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); + + _assertStateMachineTransition({from: State.VetoCooldown, to: State.VetoSignalling}); + } + + // --- + // VetoCooldown -> Normal + // --- + + function test_getStateTransition_FromVetoCooldownToNormal() external { + _setupVetoCooldownState(); + + _assertStateMachineTransition({from: State.VetoCooldown, to: State.VetoCooldown}); + + _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); + + _assertStateMachineTransition({from: State.VetoCooldown, to: State.Normal}); + } + + // --- + // RageQuit -> RageQuit + // --- + + function test_getStateTransition_FromRageQuitToRageQuit() external { + _setupRageQuitState(); + _setMockIsRageQuitFinalized(false); + + _assertStateMachineTransition({from: State.RageQuit, to: State.RageQuit}); + } + + // --- + // RageQuit -> VetoSignalling + // --- + + function test_getStateTransition_FromRageQuitToVetoSignalling() external { + _setupRageQuitState(); + _setMockIsRageQuitFinalized(false); + + _assertStateMachineTransition({from: State.RageQuit, to: State.RageQuit}); + + _setMockIsRageQuitFinalized(true); + _setMockRageQuitSupportInBP(3_01); + + _assertStateMachineTransition({from: State.RageQuit, to: State.VetoSignalling}); + } + + // --- + // RageQuit -> VetoCooldown + // --- + + function test_getStateTransition_FromRageQuitToVetoCooldown() external { + _setupRageQuitState(); + _setMockIsRageQuitFinalized(false); + + _assertStateMachineTransition({from: State.RageQuit, to: State.RageQuit}); + + _setMockIsRageQuitFinalized(true); + _setMockRageQuitSupportInBP(1_01); + + _assertStateMachineTransition({from: State.RageQuit, to: State.VetoCooldown}); + } + + // --- + // Unset -> assert(false) + // --- + + function test_getStateTransition_RevertOn_UnsetState() external { + _stateMachine.state = State.Unset; + + vm.expectRevert(stdError.assertionError); + this.external__getStateTransition(); + } + + // --- + // Helper test methods + // --- + + function _setupVetoSignallingState() internal { + _stateMachine.state = State.VetoSignalling; + _stateMachine.enteredAt = Timestamps.now(); + _stateMachine.vetoSignallingActivatedAt = Timestamps.now(); + } + + function _setupVetoSignallingDeactivationState() internal { + _setupVetoSignallingState(); + + _wait(_configProvider.VETO_SIGNALLING_MIN_DURATION().plusSeconds(1 hours)); + + _stateMachine.state = State.VetoSignallingDeactivation; + _stateMachine.enteredAt = Timestamps.now(); + } + + function _setupVetoCooldownState() internal { + _setupVetoSignallingDeactivationState(); + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + _stateMachine.state = State.VetoCooldown; + _stateMachine.enteredAt = Timestamps.now(); + } + + function _setupRageQuitState() internal { + _stateMachine.state = State.RageQuit; + _stateMachine.enteredAt = Timestamps.now(); + _stateMachine.rageQuitEscrow = IEscrow(_escrowMasterCopyMock); + } + + function _setMockRageQuitSupportInBP(uint256 bpValue) internal { + vm.mockCall( + address(_stateMachine.signallingEscrow), + abi.encodeWithSelector(IEscrow.getRageQuitSupport.selector), + abi.encode(PercentsD16.fromBasisPoints(bpValue)) + ); + } + + function _setMockIsRageQuitFinalized(bool isRageQuitFinalized) internal { + vm.mockCall( + address(_stateMachine.rageQuitEscrow), + abi.encodeWithSelector(IEscrow.isRageQuitFinalized.selector), + abi.encode(isRageQuitFinalized) + ); + } + + function _calcVetoSignallingDuration() internal returns (Duration) { + return _configProvider.getDualGovernanceConfig().calcVetoSignallingDuration( + _stateMachine.signallingEscrow.getRageQuitSupport() + ); + } + + function _assertStateMachineTransition(State from, State to) internal { + (State current, State next) = _stateMachine.getStateTransition(_configProvider.getDualGovernanceConfig()); + + assertEq(current, from); + assertEq(next, to); + } + + function external__getStateTransition() external returns (State current, State next) { + (current, next) = _stateMachine.getStateTransition(_configProvider.getDualGovernanceConfig()); + } +} From 6bac085c242556848e4f8eb4ae1a1711f375a916 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 25 Sep 2024 03:26:35 +0400 Subject: [PATCH 80/86] Move related methods in the IEmergencyProtectedTimelock --- .../committees/EmergencyActivationCommittee.sol | 5 +++-- .../committees/EmergencyExecutionCommittee.sol | 14 ++++++++++---- .../interfaces/IEmergencyProtectedTimelock.sol | 3 +++ contracts/interfaces/ITimelock.sol | 9 ++------- .../committees/EmergencyActivationCommittee.t.sol | 12 +++++++++--- .../committees/EmergencyExecutionCommittee.t.sol | 9 ++++++--- 6 files changed, 33 insertions(+), 19 deletions(-) diff --git a/contracts/committees/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol index dce5ba81..f15ba95a 100644 --- a/contracts/committees/EmergencyActivationCommittee.sol +++ b/contracts/committees/EmergencyActivationCommittee.sol @@ -6,7 +6,7 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Durations} from "../types/Duration.sol"; import {Timestamp} from "../types/Timestamp.sol"; -import {ITimelock} from "../interfaces/ITimelock.sol"; +import {IEmergencyProtectedTimelock} from "../interfaces/IEmergencyProtectedTimelock.sol"; import {HashConsensus} from "./HashConsensus.sol"; @@ -54,7 +54,8 @@ contract EmergencyActivationCommittee is HashConsensus { function executeActivateEmergencyMode() external { _markUsed(EMERGENCY_ACTIVATION_HASH); Address.functionCall( - EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(ITimelock.activateEmergencyMode.selector) + EMERGENCY_PROTECTED_TIMELOCK, + abi.encodeWithSelector(IEmergencyProtectedTimelock.activateEmergencyMode.selector) ); } } diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol index 72d03dc2..8f4288fa 100644 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -6,7 +6,7 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {Durations} from "../types/Duration.sol"; import {Timestamp} from "../types/Timestamp.sol"; -import {ITimelock} from "../interfaces/ITimelock.sol"; +import {IEmergencyProtectedTimelock} from "../interfaces/IEmergencyProtectedTimelock.sol"; import {HashConsensus} from "./HashConsensus.sol"; import {ProposalsList} from "./ProposalsList.sol"; @@ -72,14 +72,18 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { (, bytes32 key) = _encodeEmergencyExecute(proposalId); _markUsed(key); Address.functionCall( - EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(ITimelock.emergencyExecute.selector, proposalId) + EMERGENCY_PROTECTED_TIMELOCK, + abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyExecute.selector, proposalId) ); } /// @notice Checks if a proposal exists /// @param proposalId The ID of the proposal to check function _checkProposalExists(uint256 proposalId) internal view { - if (proposalId == 0 || proposalId > ITimelock(EMERGENCY_PROTECTED_TIMELOCK).getProposalsCount()) { + if ( + proposalId == 0 + || proposalId > IEmergencyProtectedTimelock(EMERGENCY_PROTECTED_TIMELOCK).getProposalsCount() + ) { revert ProposalDoesNotExist(proposalId); } } @@ -128,7 +132,9 @@ contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { function executeEmergencyReset() external { bytes32 proposalKey = _encodeEmergencyResetProposalKey(); _markUsed(proposalKey); - Address.functionCall(EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(ITimelock.emergencyReset.selector)); + Address.functionCall( + EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyReset.selector) + ); } /// @notice Encodes the proposal key for an emergency reset diff --git a/contracts/interfaces/IEmergencyProtectedTimelock.sol b/contracts/interfaces/IEmergencyProtectedTimelock.sol index 841ca5ed..8916d298 100644 --- a/contracts/interfaces/IEmergencyProtectedTimelock.sol +++ b/contracts/interfaces/IEmergencyProtectedTimelock.sol @@ -12,6 +12,9 @@ interface IEmergencyProtectedTimelock is ITimelock { Timestamp emergencyProtectionEndsAfter; } + function activateEmergencyMode() external; + function emergencyExecute(uint256 proposalId) external; + function emergencyReset() external; function getEmergencyGovernance() external view returns (address emergencyGovernance); function getEmergencyActivationCommittee() external view returns (address committee); function getEmergencyExecutionCommittee() external view returns (address committee); diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index df474e15..017c2236 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -28,18 +28,13 @@ interface ITimelock { function canExecute(uint256 proposalId) external view returns (bool); function getAdminExecutor() external view returns (address); + function getGovernance() external view returns (address); + function setGovernance(address governance) external; function getProposal(uint256 proposalId) external view returns (ProposalDetails memory proposal, ExternalCall[] memory calls); function getProposalDetails(uint256 proposalId) external view returns (ProposalDetails memory proposalDetails); - - function getGovernance() external view returns (address); - function setGovernance(address governance) external; - - function activateEmergencyMode() external; - function emergencyExecute(uint256 proposalId) external; - function emergencyReset() external; function getProposalsCount() external view returns (uint256 count); } diff --git a/test/unit/committees/EmergencyActivationCommittee.t.sol b/test/unit/committees/EmergencyActivationCommittee.t.sol index 1cd2159c..de423ce2 100644 --- a/test/unit/committees/EmergencyActivationCommittee.t.sol +++ b/test/unit/committees/EmergencyActivationCommittee.t.sol @@ -5,7 +5,7 @@ import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActiva import {HashConsensus} from "contracts/committees/HashConsensus.sol"; import {Durations} from "contracts/types/Duration.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; -import {ITimelock} from "contracts/interfaces/ITimelock.sol"; +import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; import {TargetMock} from "test/utils/target-mock.sol"; import {UnitTest} from "test/utils/unit-test.sol"; @@ -68,7 +68,10 @@ contract EmergencyActivationCommitteeUnitTest is UnitTest { emergencyActivationCommittee.approveActivateEmergencyMode(); vm.prank(committeeMembers[2]); - vm.expectCall(emergencyProtectedTimelock, abi.encodeWithSelector(ITimelock.activateEmergencyMode.selector)); + vm.expectCall( + emergencyProtectedTimelock, + abi.encodeWithSelector(IEmergencyProtectedTimelock.activateEmergencyMode.selector) + ); emergencyActivationCommittee.executeActivateEmergencyMode(); (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = @@ -115,7 +118,10 @@ contract EmergencyActivationCommitteeUnitTest is UnitTest { assertEq(isExecuted, false); vm.prank(committeeMembers[2]); - vm.expectCall(emergencyProtectedTimelock, abi.encodeWithSelector(ITimelock.activateEmergencyMode.selector)); + vm.expectCall( + emergencyProtectedTimelock, + abi.encodeWithSelector(IEmergencyProtectedTimelock.activateEmergencyMode.selector) + ); emergencyActivationCommittee.executeActivateEmergencyMode(); (support, executionQuorum, quorumAt, isExecuted) = emergencyActivationCommittee.getActivateEmergencyModeState(); diff --git a/test/unit/committees/EmergencyExecutionCommittee.t.sol b/test/unit/committees/EmergencyExecutionCommittee.t.sol index cc047b30..2f05da8e 100644 --- a/test/unit/committees/EmergencyExecutionCommittee.t.sol +++ b/test/unit/committees/EmergencyExecutionCommittee.t.sol @@ -5,7 +5,7 @@ import {EmergencyExecutionCommittee, ProposalType} from "contracts/committees/Em import {HashConsensus} from "contracts/committees/HashConsensus.sol"; import {Durations} from "contracts/types/Duration.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; -import {ITimelock} from "contracts/interfaces/ITimelock.sol"; +import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; import {TargetMock} from "test/utils/target-mock.sol"; import {UnitTest} from "test/utils/unit-test.sol"; @@ -101,7 +101,8 @@ contract EmergencyExecutionCommitteeUnitTest is UnitTest { vm.prank(committeeMembers[2]); vm.expectCall( - emergencyProtectedTimelock, abi.encodeWithSelector(ITimelock.emergencyExecute.selector, proposalId) + emergencyProtectedTimelock, + abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyExecute.selector, proposalId) ); emergencyExecutionCommittee.executeEmergencyExecute(proposalId); @@ -182,7 +183,9 @@ contract EmergencyExecutionCommitteeUnitTest is UnitTest { emergencyExecutionCommittee.approveEmergencyReset(); vm.prank(committeeMembers[2]); - vm.expectCall(emergencyProtectedTimelock, abi.encodeWithSelector(ITimelock.emergencyReset.selector)); + vm.expectCall( + emergencyProtectedTimelock, abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyReset.selector) + ); emergencyExecutionCommittee.executeEmergencyReset(); (,,, bool isExecuted) = emergencyExecutionCommittee.getEmergencyResetState(); From 03c76a725fe1c4331c38448030ab7e29841b3704 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 25 Sep 2024 03:49:45 +0400 Subject: [PATCH 81/86] EmergencyProtectedTimelock natspec fixes --- contracts/EmergencyProtectedTimelock.sol | 163 ++++++++++++++--------- 1 file changed, 98 insertions(+), 65 deletions(-) diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 7cf1a99b..ff7c93b1 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -13,20 +13,29 @@ import {ExecutableProposals} from "./libraries/ExecutableProposals.sol"; import {EmergencyProtection} from "./libraries/EmergencyProtection.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. +/// @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 IEmergencyProtectedTimelock { using TimelockState for TimelockState.Context; using ExecutableProposals for ExecutableProposals.Context; using EmergencyProtection for EmergencyProtection.Context; + // --- + // Errors + // --- + error CallerIsNotAdminExecutor(address value); // --- - // Sanity Check Params Immutables + // Sanity Check Parameters & Immutables // --- + + /// @notice The parameters for the sanity checks. + /// @param maxAfterSubmitDelay The maximum allowable delay before a submitted proposal can be scheduled for execution. + /// @param maxAfterScheduleDelay The maximum allowable delay before a scheduled proposal can be executed. + /// @param maxEmergencyModeDuration The maximum time the timelock can remain in emergency mode. + /// @param maxEmergencyProtectionDuration The maximum time the emergency protection mechanism can be activated. struct SanityCheckParams { Duration maxAfterSubmitDelay; Duration maxAfterScheduleDelay; @@ -34,26 +43,42 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { Duration maxEmergencyProtectionDuration; } + /// @notice The upper bound for the delay required before a submitted proposal can be scheduled for execution. Duration public immutable MAX_AFTER_SUBMIT_DELAY; + + /// @notice The upper bound for the delay required before a scheduled proposal can be executed. Duration public immutable MAX_AFTER_SCHEDULE_DELAY; + /// @notice The upper bound for the time the timelock can remain in emergency mode. Duration public immutable MAX_EMERGENCY_MODE_DURATION; + + /// @notice The upper bound for the time the emergency protection mechanism can be activated. Duration public immutable MAX_EMERGENCY_PROTECTION_DURATION; // --- // Admin Executor Immutables // --- + /// @dev The address of the admin executor, authorized to manage the EmergencyProtectedTimelock instance. address private immutable _ADMIN_EXECUTOR; // --- // Aspects // --- + /// @dev The functionality for managing the state of the timelock. TimelockState.Context internal _timelockState; + + /// @dev The functionality for managing the lifecycle of proposals. ExecutableProposals.Context internal _proposals; + + /// @dev The functionality for managing the emergency protection mechanism. EmergencyProtection.Context internal _emergencyProtection; + // --- + // Constructor + // --- + constructor(SanityCheckParams memory sanityCheckParams, address adminExecutor) { _ADMIN_EXECUTOR = adminExecutor; @@ -67,12 +92,11 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { // 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. + /// @notice Submits a new proposal to execute a series of calls through an executor. /// @param executor The address of the executor contract that will execute the calls. /// @param calls An array of `ExternalCall` structs representing the calls to be executed. /// @param metadata A string containing additional information about the proposal. - /// @return newProposalId The ID of the newly created proposal. + /// @return newProposalId The id of the newly created proposal. function submit( address executor, ExternalCall[] calldata calls, @@ -82,24 +106,21 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { newProposalId = _proposals.submit(executor, calls, metadata); } - /// @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. + /// @notice Schedules a proposal for execution after a specified delay. + /// @param proposalId The id of the proposal to be scheduled. function schedule(uint256 proposalId) external { _timelockState.checkCallerIsGovernance(); _proposals.schedule(proposalId, _timelockState.getAfterSubmitDelay()); } - /// @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. + /// @notice Executes a scheduled proposal. + /// @param proposalId The id of the proposal to be executed. function execute(uint256 proposalId) external { _emergencyProtection.checkEmergencyMode({isActive: false}); _proposals.execute(proposalId, _timelockState.getAfterScheduleDelay()); } - /// @dev Cancels all non-executed proposals. - /// Only the governance contract can call this function. + /// @notice Cancels all non-executed proposals, preventing them from being executed in the future. function cancelAllNonExecutedProposals() external { _timelockState.checkCallerIsGovernance(); _proposals.cancelAll(); @@ -109,19 +130,23 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { // Timelock Management // --- + /// @notice Updates the address of the governance contract. + /// @param newGovernance The address of the new governance contract to be set. function setGovernance(address newGovernance) external { _checkCallerIsAdminExecutor(); _timelockState.setGovernance(newGovernance); } + /// @notice Configures the delays for submitting and scheduling proposals, within defined upper bounds. + /// @param afterSubmitDelay The delay required before a submitted proposal can be scheduled. + /// @param afterScheduleDelay The delay required before a scheduled proposal can be executed. function setupDelays(Duration afterSubmitDelay, Duration afterScheduleDelay) external { _checkCallerIsAdminExecutor(); _timelockState.setAfterSubmitDelay(afterSubmitDelay, MAX_AFTER_SUBMIT_DELAY); _timelockState.setAfterScheduleDelay(afterScheduleDelay, MAX_AFTER_SCHEDULE_DELAY); } - /// @dev Transfers ownership of the executor contract to a new owner. - /// Only the admin executor can call this function. + /// @notice Transfers ownership of the executor contract to a new owner. /// @param executor The address of the executor contract. /// @param owner The address of the new owner. function transferExecutorOwnership(address executor, address owner) external { @@ -133,21 +158,21 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { // Emergency Protection Functionality // --- - /// @dev Sets the emergency activation committee address. + /// @notice Sets the emergency activation committee address. /// @param emergencyActivationCommittee The address of the emergency activation committee. function setEmergencyProtectionActivationCommittee(address emergencyActivationCommittee) external { _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyActivationCommittee(emergencyActivationCommittee); } - /// @dev Sets the emergency execution committee address. + /// @notice Sets the emergency execution committee address. /// @param emergencyExecutionCommittee The address of the emergency execution committee. function setEmergencyProtectionExecutionCommittee(address emergencyExecutionCommittee) external { _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyExecutionCommittee(emergencyExecutionCommittee); } - /// @dev Sets the emergency protection end date. + /// @notice Sets the emergency protection end date. /// @param emergencyProtectionEndDate The timestamp of the emergency protection end date. function setEmergencyProtectionEndDate(Timestamp emergencyProtectionEndDate) external { _checkCallerIsAdminExecutor(); @@ -156,39 +181,36 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { ); } - /// @dev Sets the emergency mode duration. + /// @notice Sets the emergency mode duration. /// @param emergencyModeDuration The duration of the emergency mode. function setEmergencyModeDuration(Duration emergencyModeDuration) external { _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyModeDuration(emergencyModeDuration, MAX_EMERGENCY_MODE_DURATION); } - /// @dev Sets the emergency governance address. + /// @notice Sets the emergency governance address. /// @param emergencyGovernance The address of the emergency governance. function setEmergencyGovernance(address emergencyGovernance) external { _checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyGovernance(emergencyGovernance); } - /// @dev Activates the emergency mode. - /// Only the activation committee can call this function. + /// @notice Activates the emergency mode. function activateEmergencyMode() external { _emergencyProtection.checkCallerIsEmergencyActivationCommittee(); _emergencyProtection.checkEmergencyMode({isActive: false}); _emergencyProtection.activateEmergencyMode(); } - /// @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. + /// @notice Executes a proposal during emergency mode. + /// @param proposalId The id of the proposal to be executed. function emergencyExecute(uint256 proposalId) external { _emergencyProtection.checkEmergencyMode({isActive: true}); _emergencyProtection.checkCallerIsEmergencyExecutionCommittee(); _proposals.execute({proposalId: 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. + /// @notice Deactivates the emergency mode. function deactivateEmergencyMode() external { _emergencyProtection.checkEmergencyMode({isActive: true}); if (!_emergencyProtection.isEmergencyModeDurationPassed()) { @@ -198,8 +220,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { _proposals.cancelAll(); } - /// @dev Resets the system after entering the emergency mode. - /// Only the execution committee can call this function. + /// @notice Resets the system after entering the emergency mode. function emergencyReset() external { _emergencyProtection.checkCallerIsEmergencyExecutionCommittee(); _emergencyProtection.checkEmergencyMode({isActive: true}); @@ -209,38 +230,38 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { _proposals.cancelAll(); } - /// @dev Returns whether the emergency protection is enabled. - /// @return A boolean indicating whether the emergency protection is enabled. + /// @notice Returns whether the emergency protection is enabled. + /// @return isEmergencyProtectionEnabled A boolean indicating whether the emergency protection is enabled. function isEmergencyProtectionEnabled() public view returns (bool) { return _emergencyProtection.isEmergencyProtectionEnabled(); } - /// @dev Returns whether the emergency mode is active. - /// @return A boolean indicating whether the emergency protection is enabled. + /// @notice Returns whether the emergency mode is active. + /// @return isEmergencyModeActive A boolean indicating whether the emergency protection is enabled. function isEmergencyModeActive() public view returns (bool) { return _emergencyProtection.isEmergencyModeActive(); } - /// @dev Returns the details of the emergency protection. + /// @notice Returns the details of the emergency protection. /// @return details A struct containing the emergency mode duration, emergency mode ends after, and emergency protection ends after. function getEmergencyProtectionDetails() public view returns (EmergencyProtectionDetails memory details) { return _emergencyProtection.getEmergencyProtectionDetails(); } - /// @dev Returns the address of the emergency governance. - /// @return The address of the emergency governance. + /// @notice Returns the address of the emergency governance. + /// @return emergencyGovernance The address of the emergency governance. function getEmergencyGovernance() external view returns (address) { return _emergencyProtection.emergencyGovernance; } - /// @dev Returns the address of the emergency activation committee. - /// @return The address of the emergency activation committee. + /// @notice Returns the address of the emergency activation committee. + /// @return emergencyActivationCommittee The address of the emergency activation committee. function getEmergencyActivationCommittee() external view returns (address) { return _emergencyProtection.emergencyActivationCommittee; } - /// @dev Returns the address of the emergency execution committee. - /// @return The address of the emergency execution committee. + /// @notice Returns the address of the emergency execution committee. + /// @return emergencyExecutionCommittee The address of the emergency execution committee. function getEmergencyExecutionCommittee() external view returns (address) { return _emergencyProtection.emergencyExecutionCommittee; } @@ -249,24 +270,32 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { // Timelock View Methods // --- + /// @notice Returns the address of the current governance contract. + /// @return governance The address of the governance contract. function getGovernance() external view returns (address) { return _timelockState.governance; } + /// @notice Returns the address of the admin executor. + /// @return adminExecutor The address of the admin executor. function getAdminExecutor() external view returns (address) { return _ADMIN_EXECUTOR; } + /// @notice Returns the configured delay duration required before a submitted proposal can be scheduled. + /// @return afterSubmitDelay The duration of the after-submit delay. function getAfterSubmitDelay() external view returns (Duration) { return _timelockState.getAfterSubmitDelay(); } + /// @notice Returns the configured delay duration required before a scheduled proposal can be executed. + /// @return afterScheduleDelay The duration of the after-schedule delay. function getAfterScheduleDelay() external view returns (Duration) { return _timelockState.getAfterScheduleDelay(); } - /// @dev Retrieves the details of a proposal. - /// @param proposalId The ID of the proposal. + /// @notice Retrieves the details of a proposal. + /// @param proposalId The id of the proposal. /// @return proposalDetails The Proposal struct containing the details of the proposal. /// @return calls An array of ExternalCall structs representing the sequence of calls to be executed for the proposal. function getProposal(uint256 proposalId) @@ -279,52 +308,56 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { } /// @notice Retrieves information about a proposal, excluding the external calls associated with it. - /// @param proposalId The ID of the proposal to retrieve information for. - /// @return proposalDetails A ProposalDetails struct containing the details of the proposal. - /// id The ID of the proposal. - /// status The current status of the proposal. Possible values are: - /// 0 - The proposal does not exist. - /// 1 - The proposal was submitted but not scheduled. - /// 2 - The proposal was submitted and scheduled but not yet executed. - /// 3 - The proposal was submitted, scheduled, and executed. This is the final state of the proposal lifecycle. - /// 4 - The proposal was cancelled via cancelAllNonExecutedProposals() and cannot be scheduled or executed anymore. - /// This is the final state of the proposal. - /// executor The address of the executor responsible for executing the proposal's external calls. - /// submittedAt The timestamp when the proposal was submitted. - /// scheduledAt The timestamp when the proposal was scheduled for execution. Equals 0 if the proposal - /// was submitted but not yet scheduled. + /// @param proposalId The id of the proposal to retrieve information for. + /// @return proposalDetails A ProposalDetails struct containing the details of the proposal, with the following data: + /// - `id`: The id of the proposal. + /// - `status`: The current status of the proposal. Possible values are: + /// 0 - The proposal does not exist. + /// 1 - The proposal was submitted but not scheduled. + /// 2 - The proposal was submitted and scheduled but not yet executed. + /// 3 - The proposal was submitted, scheduled, and executed. This is the final state of the proposal lifecycle. + /// 4 - The proposal was cancelled via cancelAllNonExecutedProposals() and cannot be scheduled or executed anymore. + /// This is the final state of the proposal. + /// - `executor`: The address of the executor responsible for executing the proposal's external calls. + /// - `submittedAt`: The timestamp when the proposal was submitted. + /// - `scheduledAt`: The timestamp when the proposal was scheduled for execution. Equals 0 if the proposal + /// was submitted but not yet scheduled. function getProposalDetails(uint256 proposalId) external view returns (ProposalDetails memory proposalDetails) { return _proposals.getProposalDetails(proposalId); } /// @notice Retrieves the external calls associated with the specified proposal. - /// @param proposalId The ID of the proposal to retrieve external calls for. + /// @param proposalId The id of the proposal to retrieve external calls for. /// @return calls An array of ExternalCall structs representing the sequence of calls to be executed for the proposal. function getProposalCalls(uint256 proposalId) external view returns (ExternalCall[] memory calls) { calls = _proposals.getProposalCalls(proposalId); } - /// @dev Retrieves the total number of proposals. + /// @notice Retrieves the total number of proposals. /// @return count The total number of proposals. function getProposalsCount() external view returns (uint256 count) { count = _proposals.getProposalsCount(); } - /// @dev Checks if a proposal can be executed. - /// @param proposalId The ID of the proposal. + /// @notice 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.isEmergencyModeActive() && _proposals.canExecute(proposalId, _timelockState.getAfterScheduleDelay()); } - /// @dev Checks if a proposal can be scheduled. - /// @param proposalId The ID of the proposal. + /// @notice 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, _timelockState.getAfterSubmitDelay()); } + // --- + // Private Methods + // --- + function _checkCallerIsAdminExecutor() internal view { if (msg.sender != _ADMIN_EXECUTOR) { revert CallerIsNotAdminExecutor(msg.sender); From c41e5465ca5918d058ba54bd9a7d724dcd3c3828 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 25 Sep 2024 13:42:42 +0400 Subject: [PATCH 82/86] Add natspec fo Escrow contract --- contracts/Escrow.sol | 225 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 181 insertions(+), 44 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 9c24f542..24fd9130 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -19,13 +19,11 @@ import {EscrowState} from "./libraries/EscrowState.sol"; import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalBatchesQueue.sol"; import {HolderAssets, StETHAccounting, UnstETHAccounting, AssetsAccounting} from "./libraries/AssetsAccounting.sol"; -/// @notice Summary of the total locked assets in the Escrow -/// @param stETHLockedShares Total number of stETH shares locked in the Escrow -/// @param stETHClaimedETH Total amount of ETH claimed from the stETH locked in the Escrow -/// @param unstETHUnfinalizedShares Total number of shares from unstETH NFTs that have not yet been -/// marked as finalized -/// @param unstETHFinalizedETH Total claimable amount of ETH from unstETH NFTs that have been marked -/// as finalized +/// @notice Summary of the total locked assets in the Escrow. +/// @param stETHLockedShares The total number of stETH shares currently locked in the Escrow. +/// @param stETHClaimedETH The total amount of ETH claimed from the stETH shares locked in the Escrow. +/// @param unstETHUnfinalizedShares The total number of shares from unstETH NFTs that have not yet been marked as finalized. +/// @param unstETHFinalizedETH The total amount of ETH claimable from unstETH NFTs that have been marked as finalized. struct LockedAssetsTotals { uint256 stETHLockedShares; uint256 stETHClaimedETH; @@ -33,6 +31,11 @@ struct LockedAssetsTotals { uint256 unstETHFinalizedETH; } +/// @notice Summary of the assets locked in the Escrow by a specific vetoer. +/// @param stETHLockedShares The total number of stETH shares currently locked in the Escrow by the vetoer. +/// @param unstETHLockedShares The total number of unstETH shares currently locked in the Escrow by the vetoer. +/// @param unstETHIdsCount The total number of unstETH NFTs locked in the Escrow by the vetoer. +/// @param lastAssetsLockTimestamp The timestamp of the last time the vetoer locked stETH, wstETH, or unstETH in the Escrow. struct VetoerState { uint256 stETHLockedShares; uint256 unstETHLockedShares; @@ -40,6 +43,9 @@ struct VetoerState { uint256 lastAssetsLockTimestamp; } +/// @notice This contract is used to accumulate stETH, wstETH, unstETH, and withdrawn ETH from vetoers during the +/// veto signalling and rage quit processes. +/// @dev This contract is intended to be used behind a minimal proxy deployed by the DualGovernance contract. contract Escrow is IEscrow { using EscrowState for EscrowState.Context; using AssetsAccounting for AssetsAccounting.Context; @@ -48,12 +54,12 @@ contract Escrow is IEscrow { // --- // Errors // --- + error EmptyUnstETHIds(); error UnclaimedBatches(); error UnexpectedUnstETHId(); error UnfinalizedUnstETHIds(); error NonProxyCallsForbidden(); error BatchesQueueIsNotClosed(); - error EmptyUnstETHIds(); error InvalidBatchSize(uint256 size); error CallerIsNotDualGovernance(address caller); error InvalidHintsLength(uint256 actual, uint256 expected); @@ -64,40 +70,57 @@ contract Escrow is IEscrow { // --- /// @dev The lower limit for stETH transfers when requesting a withdrawal batch - /// during the Rage Quit phase. For more details, see https://github.com/lidofinance/lido-dao/issues/442. - /// The current value is chosen to ensure functionality over an extended period, spanning several decades. + /// during the Rage Quit phase. For more details, see https://github.com/lidofinance/lido-dao/issues/442. + /// The current value is chosen to ensure functionality over an extended period, spanning several decades. uint256 private constant _MIN_TRANSFERRABLE_ST_ETH_AMOUNT = 8 wei; // --- - // Sanity check params immutables + // Sanity Check Parameters & Immutables // --- + /// @notice The minimum number of withdrawal requests allowed to create during a single call of + /// the `Escrow.requestNextWithdrawalsBatch(batchSize)` method. uint256 public immutable MIN_WITHDRAWALS_BATCH_SIZE; // --- - // Dependencies immutables + // Dependencies Immutables // --- + /// @notice The address of the stETH token. IStETH public immutable ST_ETH; + + /// @notice The address of the wstETH token. IWstETH public immutable WST_ETH; + + /// @notice The address of Lido's Withdrawal Queue and the unstETH token. IWithdrawalQueue public immutable WITHDRAWAL_QUEUE; // --- - // Implementation immutables + // Implementation Immutables + // --- + /// @dev Reference to the address of the implementation contract, used to distinguish whether the call + /// is made to the proxy or directly to the implementation. address private immutable _SELF; + + /// @dev The address of the Dual Governance contract. IDualGovernance public immutable DUAL_GOVERNANCE; // --- // Aspects // --- + /// @dev Provides the functionality to manage the state of the Escrow. EscrowState.Context internal _escrowState; + + /// @dev Handles the accounting of assets locked in the Escrow. AssetsAccounting.Context private _accounting; + + /// @dev Manages the queue of withdrawal request batches generated from the locked stETH and wstETH tokens. WithdrawalsBatchesQueue.Context private _batchesQueue; // --- - // Construction & initializing + // Construction & Initializing // --- constructor( @@ -117,6 +140,9 @@ contract Escrow is IEscrow { MIN_WITHDRAWALS_BATCH_SIZE = minWithdrawalsBatchSize; } + /// @notice Initializes the proxy instance with the specified minimum assets lock duration. + /// @param minAssetsLockDuration The minimum duration that must pass from the last stETH, wstETH, or unstETH lock + /// by the vetoer before they are allowed to unlock assets from the Escrow. function initialize(Duration minAssetsLockDuration) external { if (address(this) == _SELF) { revert NonProxyCallsForbidden(); @@ -130,9 +156,13 @@ contract Escrow is IEscrow { } // --- - // Lock & unlock stETH + // Lock & Unlock stETH // --- + /// @notice Locks the vetoer's specified `amount` of stETH in the Veto Signalling Escrow, thereby increasing + /// the rage quit support proportionally to the number of stETH shares locked. + /// @param amount The amount of stETH to be locked. + /// @return lockedStETHShares The number of stETH shares locked in the Escrow during the current invocation. function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); @@ -144,6 +174,9 @@ contract Escrow is IEscrow { DUAL_GOVERNANCE.activateNextState(); } + /// @notice Unlocks all previously locked stETH and wstETH tokens, returning them in the form of stETH tokens. + /// This action decreases the rage quit support proportionally to the number of unlocked stETH shares. + /// @return unlockedStETHShares The total number of stETH shares unlocked from the Escrow. function unlockStETH() external returns (uint256 unlockedStETHShares) { DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); @@ -156,9 +189,13 @@ contract Escrow is IEscrow { } // --- - // Lock & unlock wstETH + // Lock & Unlock wstETH // --- + /// @notice Locks the vetoer's specified `amount` of wstETH in the Veto Signalling Escrow, thereby increasing + /// the rage quit support proportionally to the number of locked wstETH shares. + /// @param amount The amount of wstETH to be locked. + /// @return lockedStETHShares The number of wstETH shares locked in the Escrow during the current invocation. function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); @@ -170,6 +207,9 @@ contract Escrow is IEscrow { DUAL_GOVERNANCE.activateNextState(); } + /// @notice Unlocks all previously locked stETH and wstETH tokens, returning them in the form of wstETH tokens. + /// This action decreases the rage quit support proportionally to the number of unlocked wstETH shares. + /// @return unlockedStETHShares The total number of wstETH shares unlocked from the Escrow. function unlockWstETH() external returns (uint256 unlockedStETHShares) { DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); @@ -183,9 +223,13 @@ contract Escrow is IEscrow { } // --- - // Lock & unlock unstETH + // Lock & Unlock unstETH // --- + /// @notice Locks the specified unstETH NFTs, identified by their ids, in the Veto Signalling Escrow, thereby increasing + /// the rage quit support proportionally to the total number of stETH shares contained in the locked unstETH NFTs. + /// @dev Locking finalized or already claimed unstETH NFTs is prohibited. + /// @param unstETHIds An array of ids representing the unstETH NFTs to be locked. function lockUnstETH(uint256[] memory unstETHIds) external { if (unstETHIds.length == 0) { revert EmptyUnstETHIds(); @@ -203,6 +247,9 @@ contract Escrow is IEscrow { DUAL_GOVERNANCE.activateNextState(); } + /// @notice Unlocks the specified unstETH NFTs, identified by their ids, from the Veto Signalling Escrow + /// that were previously locked by the vetoer. + /// @param unstETHIds An array of ids representing the unstETH NFTs to be unlocked. function unlockUnstETH(uint256[] memory unstETHIds) external { DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); @@ -217,6 +264,16 @@ contract Escrow is IEscrow { DUAL_GOVERNANCE.activateNextState(); } + /// @notice Marks the specified locked unstETH NFTs as finalized to update the rage quit support value + /// in the Veto Signalling Escrow. + /// @dev Finalizing a withdrawal NFT results in the following state changes: + /// - The value of the finalized unstETH NFT is no longer influenced by stETH token rebases. + /// - The total supply of stETH is adjusted according to the value of the finalized unstETH NFT. + /// These changes impact the rage quit support value. This function updates the status of the specified + /// unstETH NFTs to ensure accurate rage quit support accounting in the Veto Signalling Escrow. + /// @param unstETHIds An array of ids representing the unstETH NFTs to be marked as finalized. + /// @param hints An array of hints required by the WithdrawalQueue to efficiently retrieve + /// the claimable amounts for the unstETH NFTs. function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external { DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); @@ -227,9 +284,14 @@ contract Escrow is IEscrow { } // --- - // Convert to NFT + // Convert To NFT // --- + /// @notice Allows vetoers to convert their locked stETH or wstETH tokens into unstETH NFTs on behalf of the + /// Veto Signalling Escrow contract. + /// @param stETHAmounts An array representing the amounts of stETH to be converted into unstETH NFTs. + /// @return unstETHIds An array of ids representing the newly created unstETH NFTs corresponding to + /// the converted stETH amounts. function requestWithdrawals(uint256[] calldata stETHAmounts) external returns (uint256[] memory unstETHIds) { DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); @@ -245,13 +307,21 @@ contract Escrow is IEscrow { _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); /// @dev Skip calling activateNextState here to save gas, as converting stETH to unstETH NFTs - /// does not affect the RageQuit support. + /// does not affect the RageQuit support. } // --- - // Start rage quit + // Start Rage Quit // --- + /// @notice Irreversibly converts the Signalling Escrow into the Rage Quit Escrow, allowing vetoers who have locked + /// their funds in the Signalling Escrow to withdraw them in the form of ETH after the Rage Quit process + /// is completed and the specified withdrawal delay has passed. + /// @param rageQuitExtensionPeriodDuration The duration that starts after all withdrawal batches are formed, extending + /// the Rage Quit state in Dual Governance. This extension period ensures that users who have locked their unstETH + /// have sufficient time to claim it. + /// @param rageQuitEthWithdrawalsDelay The waiting period that vetoers must observe after the Rage Quit process + /// is finalized before they can withdraw ETH from the Escrow. function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitEthWithdrawalsDelay) external { _checkCallerIsDualGovernance(); _escrowState.startRageQuit(rageQuitExtensionPeriodDuration, rageQuitEthWithdrawalsDelay); @@ -259,9 +329,13 @@ contract Escrow is IEscrow { } // --- - // Request withdrawal batches + // Request Withdrawal Batches // --- + /// @notice Creates unstETH NFTs from the stETH held in the Rage Quit Escrow via the WithdrawalQueue contract. + /// This function can be called multiple times until the Rage Quit Escrow no longer holds enough stETH + /// to create a withdrawal request. + /// @param batchSize The number of withdrawal requests to process in this batch. function requestNextWithdrawalsBatch(uint256 batchSize) external { _escrowState.checkRageQuitEscrow(); @@ -274,7 +348,7 @@ contract Escrow is IEscrow { uint256 maxStETHWithdrawalRequestAmount = WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); /// @dev This check ensures that even if MIN_STETH_WITHDRAWAL_AMOUNT is set too low, - /// the withdrawal batch request process can still be completed successfully + /// the withdrawal batch request process can still be completed successfully if (stETHRemaining < Math.max(_MIN_TRANSFERRABLE_ST_ETH_AMOUNT, minStETHWithdrawalRequestAmount)) { return _batchesQueue.close(); } @@ -295,9 +369,28 @@ contract Escrow is IEscrow { } // --- - // Claim requested withdrawal batches + // Claim Requested Withdrawal Batches // --- + /// @notice Allows the claim of finalized withdrawal NFTs generated via the `Escrow.requestNextWithdrawalsBatch()` method. + /// The unstETH NFTs must be claimed sequentially, starting from the provided `fromUnstETHId`, which must be + /// the first unclaimed unstETH NFT. + /// @param fromUnstETHId The id of the first unclaimed unstETH NFT in the batch to be claimed. + /// @param hints An array of hints required by the `WithdrawalQueue` contract to efficiently process + /// the claiming of unstETH NFTs. + function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external { + _escrowState.checkRageQuitEscrow(); + _escrowState.checkBatchesClaimingInProgress(); + + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(hints.length); + + _claimNextWithdrawalsBatch(fromUnstETHId, unstETHIds, hints); + } + + /// @notice An overloaded version of `Escrow.claimNextWithdrawalsBatch(uint256, uint256[] calldata)` that calculates + /// hints for the WithdrawalQueue on-chain. This method provides a more convenient claiming process but is + /// less gas efficient compared to `Escrow.claimNextWithdrawalsBatch(uint256, uint256[] calldata)`. + /// @param maxUnstETHIdsCount The maximum number of unstETH NFTs to claim in this batch. function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { _escrowState.checkRageQuitEscrow(); _escrowState.checkBatchesClaimingInProgress(); @@ -311,32 +404,28 @@ contract Escrow is IEscrow { ); } - function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external { - _escrowState.checkRageQuitEscrow(); - _escrowState.checkBatchesClaimingInProgress(); - - uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(hints.length); - - _claimNextWithdrawalsBatch(fromUnstETHId, unstETHIds, hints); - } - // --- - // Start rage quit extension delay + // Start Rage Quit Extension Delay // --- + /// @notice Initiates the Rage Quit Extension Period once all withdrawal batches have been claimed. + /// For cases where the `Escrow` instance holds only locked unstETH NFTs, this function ensures that the last + /// unstETH NFT registered in the `WithdrawalQueue` at the time of the `Escrow.startRageQuit()` call is finalized. + /// The Rage Quit Extension Period provides additional time for vetoers who locked their unstETH NFTs in the + /// Escrow to claim them. function startRageQuitExtensionPeriod() external { if (!_batchesQueue.isClosed()) { revert BatchesQueueIsNotClosed(); } /// @dev This check is primarily required when only unstETH NFTs are locked in the Escrow - /// and there are no WithdrawalBatches. In this scenario, the RageQuitExtensionPeriod can only begin - /// when the last locked unstETH id is finalized in the WithdrawalQueue. - /// When the WithdrawalBatchesQueue is not empty, this invariant is maintained by the following: - /// - Any locked unstETH during the VetoSignalling phase has an id less than any unstETH NFT created - /// during the request for withdrawal batches. - /// - Claiming the withdrawal batches requires the finalization of the unstETH with the given id. - /// - The finalization of unstETH NFTs occurs in FIFO order. + /// and there are no WithdrawalBatches. In this scenario, the RageQuitExtensionPeriod can only begin + /// when the last locked unstETH id is finalized in the WithdrawalQueue. + /// When the WithdrawalBatchesQueue is not empty, this invariant is maintained by the following: + /// - Any locked unstETH during the VetoSignalling phase has an id less than any unstETH NFT created + /// during the request for withdrawal batches. + /// - Claiming the withdrawal batches requires the finalization of the unstETH with the given id. + /// - The finalization of unstETH NFTs occurs in FIFO order. if (_batchesQueue.getLastClaimedOrBoundaryUnstETHId() > WITHDRAWAL_QUEUE.getLastFinalizedRequestId()) { revert UnfinalizedUnstETHIds(); } @@ -349,9 +438,17 @@ contract Escrow is IEscrow { } // --- - // Claim locked unstETH NFTs + // Claim Locked unstETH NFTs // --- + /// @notice Allows users to claim finalized unstETH NFTs locked in the Rage Quit Escrow contract. + /// To safeguard the ETH associated with withdrawal NFTs, this function should be invoked while the `Escrow` + /// is in the `RageQuitEscrow` state and before the `RageQuitExtensionPeriod` ends. Any ETH corresponding to + /// unclaimed withdrawal NFTs after this period will remain controlled by code potentially influenced by pending + /// and future DAO decisions. + /// @param unstETHIds An array of ids representing the unstETH NFTs to be claimed. + /// @param hints An array of hints required by the `WithdrawalQueue` contract to efficiently process + /// the claiming of unstETH NFTs. function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external { _escrowState.checkRageQuitEscrow(); uint256[] memory claimableAmounts = WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints); @@ -365,18 +462,24 @@ contract Escrow is IEscrow { } // --- - // Escrow management + // Escrow Management // --- + /// @notice Sets the minimum duration that must elapse after the last stETH, wstETH, or unstETH lock + /// by a vetoer before they are permitted to unlock their assets from the Escrow. + /// @param newMinAssetsLockDuration The new minimum lock duration to be set. function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { _checkCallerIsDualGovernance(); _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration); } // --- - // Withdraw logic + // Withdraw Logic // --- + /// @notice Allows the caller (i.e., `msg.sender`) to withdraw all stETH and wstETH they have previously locked + /// into the contract (while it was in the Signalling state) as plain ETH, provided that + /// the Rage Quit process is completed and the Rage Quit Eth Withdrawals Delay has elapsed. function withdrawETH() external { _escrowState.checkRageQuitEscrow(); _escrowState.checkEthWithdrawalsDelayPassed(); @@ -384,6 +487,9 @@ contract Escrow is IEscrow { ethToWithdraw.sendTo(payable(msg.sender)); } + /// @notice Allows the caller (i.e., `msg.sender`) to withdraw the claimed ETH from the specified unstETH NFTs + /// that were locked by the caller in the contract while it was in the Signalling state. + /// @param unstETHIds An array of ids representing the unstETH NFTs from which the caller wants to withdraw ETH. function withdrawETH(uint256[] calldata unstETHIds) external { if (unstETHIds.length == 0) { revert EmptyUnstETHIds(); @@ -398,6 +504,12 @@ contract Escrow is IEscrow { // Getters // --- + /// @notice Returns the total amounts of locked and claimed assets in the Escrow. + /// @return totals A struct containing the total amounts of locked and claimed assets, including: + /// - `stETHClaimedETH`: The total amount of ETH claimed from locked stETH. + /// - `stETHLockedShares`: The total number of stETH shares currently locked in the Escrow. + /// - `unstETHUnfinalizedShares`: The total number of shares from unstETH NFTs that have not yet been finalized. + /// - `unstETHFinalizedETH`: The total amount of ETH from finalized unstETH NFTs. function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory totals) { StETHAccounting memory stETHTotals = _accounting.stETHTotals; totals.stETHClaimedETH = stETHTotals.claimedETH.toUint256(); @@ -408,6 +520,13 @@ contract Escrow is IEscrow { totals.unstETHFinalizedETH = unstETHTotals.finalizedETH.toUint256(); } + /// @notice Returns the state of locked assets for a specific vetoer. + /// @param vetoer The address of the vetoer whose locked asset state is being queried. + /// @return state A struct containing information about the vetoer's locked assets, including: + /// - `stETHLockedShares`: The total number of stETH shares locked by the vetoer. + /// - `unstETHLockedShares`: The total number of unstETH shares locked by the vetoer. + /// - `unstETHIdsCount`: The total number of unstETH NFTs locked by the vetoer. + /// - `lastAssetsLockTimestamp`: The timestamp of the last assets lock by the vetoer. function getVetoerState(address vetoer) external view returns (VetoerState memory state) { HolderAssets storage assets = _accounting.assets[vetoer]; @@ -417,26 +536,41 @@ contract Escrow is IEscrow { state.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp.toSeconds(); } + /// @notice Returns the total count of unstETH NFTs that have not been claimed yet. + /// @return unclaimedUnstETHIdsCount The total number of unclaimed unstETH NFTs. function getUnclaimedUnstETHIdsCount() external view returns (uint256) { return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); } + /// @notice Retrieves the unstETH NFT ids of the next batch available for claiming. + /// @param limit The maximum number of unstETH NFTs to return in the batch. + /// @return unstETHIds An array of unstETH NFT IDs available for the next withdrawal batch. function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds) { return _batchesQueue.getNextWithdrawalsBatches(limit); } + /// @notice Returns whether all withdrawal batches have been finalized. + /// @return isWithdrawalsBatchesFinalized A boolean value indicating whether all withdrawal batches have been + /// finalized (`true`) or not (`false`). function isWithdrawalsBatchesFinalized() external view returns (bool) { return _batchesQueue.isClosed(); } + /// @notice Returns whether the Rage Quit Extension Period has started. + /// @return isRageQuitExtensionPeriodStarted A boolean value indicating whether the Rage Quit Extension Period + /// has started (`true`) or not (`false`). function isRageQuitExtensionPeriodStarted() external view returns (bool) { return _escrowState.isRageQuitExtensionPeriodStarted(); } + /// @notice Returns the timestamp when the Rage Quit Extension Period started. + /// @return rageQuitExtensionPeriodStartedAt The timestamp when the Rage Quit Extension Period began. function getRageQuitExtensionPeriodStartedAt() external view returns (Timestamp) { return _escrowState.rageQuitExtensionPeriodStartedAt; } + /// @notice Returns the current Rage Quit support value as a percentage. + /// @return rageQuitSupport The current Rage Quit support as a `PercentD16` value. function getRageQuitSupport() external view returns (PercentD16) { StETHAccounting memory stETHTotals = _accounting.stETHTotals; UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; @@ -450,6 +584,8 @@ contract Escrow is IEscrow { }); } + /// @notice Returns whether the Rage Quit process has been finalized. + /// @return A boolean value indicating whether the Rage Quit process has been finalized (`true`) or not (`false`). function isRageQuitFinalized() external view returns (bool) { return _escrowState.isRageQuitEscrow() && _escrowState.isRageQuitExtensionPeriodPassed(); } @@ -458,6 +594,7 @@ contract Escrow is IEscrow { // Receive ETH // --- + /// @notice Accepts ETH payments only from the `WithdrawalQueue` contract. receive() external payable { if (msg.sender != address(WITHDRAWAL_QUEUE)) { revert InvalidETHSender(msg.sender, address(WITHDRAWAL_QUEUE)); @@ -465,7 +602,7 @@ contract Escrow is IEscrow { } // --- - // Internal methods + // Internal Methods // --- function _claimNextWithdrawalsBatch( From 0c8f50e8e199e8a30b690fc26afd5966711168dc Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 25 Sep 2024 13:43:11 +0400 Subject: [PATCH 83/86] Add tests on cancelAllPendingProposals return value --- test/unit/DualGovernance.t.sol | 15 ++++++++++----- test/unit/TimelockedGovernance.t.sol | 3 ++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 9a30abe1..5434c22f 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -251,8 +251,9 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsSkipped(); - _dualGovernance.cancelAllPendingProposals(); + bool isProposalsCancelled = _dualGovernance.cancelAllPendingProposals(); + assertFalse(isProposalsCancelled); assertEq(_timelock.getProposalsCount(), 1); assertEq(_timelock.lastCancelledProposalId(), 0); } @@ -286,8 +287,9 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsSkipped(); - _dualGovernance.cancelAllPendingProposals(); + bool isProposalsCancelled = _dualGovernance.cancelAllPendingProposals(); + assertFalse(isProposalsCancelled); assertEq(_timelock.getProposalsCount(), 1); assertEq(_timelock.lastCancelledProposalId(), 0); } @@ -312,8 +314,9 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsSkipped(); - _dualGovernance.cancelAllPendingProposals(); + bool isProposalsCancelled = _dualGovernance.cancelAllPendingProposals(); + assertFalse(isProposalsCancelled); assertEq(_timelock.getProposalsCount(), 1); assertEq(_timelock.lastCancelledProposalId(), 0); } @@ -333,8 +336,9 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsExecuted(); - _dualGovernance.cancelAllPendingProposals(); + bool isProposalsCancelled = _dualGovernance.cancelAllPendingProposals(); + assertTrue(isProposalsCancelled); assertEq(_timelock.getProposalsCount(), 1); assertEq(_timelock.lastCancelledProposalId(), 1); } @@ -361,8 +365,9 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsExecuted(); - _dualGovernance.cancelAllPendingProposals(); + bool isProposalsCancelled = _dualGovernance.cancelAllPendingProposals(); + assertTrue(isProposalsCancelled); assertEq(_timelock.getProposalsCount(), 1); assertEq(_timelock.lastCancelledProposalId(), 1); } diff --git a/test/unit/TimelockedGovernance.t.sol b/test/unit/TimelockedGovernance.t.sol index 8cfa8a23..a7ad7c7b 100644 --- a/test/unit/TimelockedGovernance.t.sol +++ b/test/unit/TimelockedGovernance.t.sol @@ -85,8 +85,9 @@ contract TimelockedGovernanceUnitTests is UnitTest { _timelock.setSchedule(1); _timelockedGovernance.scheduleProposal(1); - _timelockedGovernance.cancelAllPendingProposals(); + bool isProposalsCancelled = _timelockedGovernance.cancelAllPendingProposals(); + assertTrue(isProposalsCancelled); assertEq(_timelock.getLastCancelledProposalId(), 2); } From 2b9b021807d56254a71125b3bd32caf936965a76 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 25 Sep 2024 13:44:56 +0400 Subject: [PATCH 84/86] DualGovernance and Timelock section comments --- contracts/DualGovernance.sol | 12 ++++++++---- contracts/EmergencyProtectedTimelock.sol | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 7f943e19..f38f73c0 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -54,7 +54,7 @@ contract DualGovernance is IDualGovernance { event ResealCommitteeSet(address resealCommittee); // --- - // Sanity Check Parameters + // Sanity Check Parameters & Immutables // --- /// @notice The parameters for the sanity checks. @@ -138,6 +138,10 @@ contract DualGovernance is IDualGovernance { /// period of time when the Dual Governance proposal adoption is blocked. address internal _resealCommittee; + // --- + // Constructor + // --- + constructor(ExternalDependencies memory dependencies, SanityCheckParams memory sanityCheckParams) { TIMELOCK = dependencies.timelock; RESEAL_MANAGER = dependencies.resealManager; @@ -471,11 +475,11 @@ contract DualGovernance is IDualGovernance { } // --- - // Reseal executor + // Sealables Resealing // --- /// @notice Allows the reseal committee to "reseal" (pause indefinitely) an instance of a sealable contract through - /// the ResealManager contract. + /// the ResealManager contract. /// @param sealable The address of the sealable contract to be resealed. function resealSealable(address sealable) external { _stateMachine.activateNextState(ESCROW_MASTER_COPY); @@ -498,7 +502,7 @@ contract DualGovernance is IDualGovernance { } // --- - // Private methods + // Internal methods // --- function _checkCallerIsAdminExecutor() internal view { diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index ff7c93b1..12237025 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -355,7 +355,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { } // --- - // Private Methods + // Internal Methods // --- function _checkCallerIsAdminExecutor() internal view { From 3a481f921221f29f6337ecd8f503bfd7b2e1505d Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 25 Sep 2024 14:05:18 +0400 Subject: [PATCH 85/86] Update cancelAllPendingProposals spec --- docs/plan-b.md | 9 ++++++--- docs/specification.md | 8 ++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/plan-b.md b/docs/plan-b.md index ceb01bae..94834a5a 100644 --- a/docs/plan-b.md +++ b/docs/plan-b.md @@ -2,7 +2,7 @@ Timelocked Governance (TG) is a governance subsystem positioned between the Lido DAO, represented by the admin voting system (defaulting to Aragon's Voting), and the protocol contracts it manages. The TG subsystem helps protect users from malicious DAO proposals by allowing the **Emergency Activation Committee** to activate a long-lasting timelock on these proposals. -> Motivation: the upcoming Ethereum upgrade *Pectra* will introduce a new [withdrawal mechanism](https://eips.ethereum.org/EIPS/eip-7002) (EIP-7002), significantly affecting the operation of the Lido protocol. This enhancement will allow withdrawal queue contract to trigger withdrawals, introducing a new attack vector for the whole protocol. This poses a threat to stETH users, as governance capture (or malicious actions) could enable an upgrade to the withdrawal queue contract, resulting in the theft of user funds. Timelocked Governance in its turn provides security assurances through the implementation of guardians (emergency committees) that can halt malicious proposals and the implementation of the timelock to ensure users and committees have sufficient time to react to potential threats. +> Motivation: the upcoming Ethereum upgrade *Pectra* will introduce a new [withdrawal mechanism](https://eips.ethereum.org/EIPS/eip-7002) (EIP-7002), significantly affecting the operation of the Lido protocol. This enhancement will allow withdrawal queue contract to trigger withdrawals, introducing a new attack vector for the whole protocol. This poses a threat to stETH users, as governance capture (or malicious actions) could enable an upgrade to the withdrawal queue contract, resulting in the theft of user funds. Timelocked Governance in its turn provides security assurances through the implementation of guardians (emergency committees) that can halt malicious proposals and the implementation of the timelock to ensure users and committees have sufficient time to react to potential threats. ## Navigation * [System overview](#system-overview) @@ -29,7 +29,7 @@ The system comprises the following primary contracts: - [**`Executor.sol`**](#contract-executor): A contract instance responsible for executing calls resulting from governance proposals. All protocol permissions or roles protected by TG, as well as the authority to manage these roles/permissions, should be assigned exclusively to instance of this contract, rather than being assigned directly to the DAO voting system. Additionally, the system uses several committee contracts that allow members to execute, acquiring quorum, a narrow set of actions: - + - [**`EmergencyActivationCommittee`**](#contract-emergencyactivationcommittee): A contract with the authority to activate Emergency Mode. Activation requires a quorum from committee members. - [**`EmergencyExecutionCommittee`**](#contract-emergencyexecutioncommittee): A contract that enables the execution of proposals during Emergency Mode by obtaining a quorum of committee members. @@ -104,14 +104,17 @@ Instructs the [`EmergencyProtectedTimelock`](#) singleton instance to execute See: [`EmergencyProtectedTimelock.execute`](#) #### Preconditions - The proposal with the given id MUST be in the `Scheduled` state. + ### Function: `TimelockedGovernance.cancelAllPendingProposals` ```solidity -function cancelAllPendingProposals() +function cancelAllPendingProposals() returns (bool) ``` Cancels all currently submitted and non-executed proposals. If a proposal was submitted but not scheduled, it becomes unschedulable. If a proposal was scheduled, it becomes unexecutable. +The function will return `true` if all proposals are successfully canceled. If the subsequent call to the `EmergencyProtectedTimelock.cancelAllNonExecutedProposals()` method fails, the function will revert with an error. + See: [`EmergencyProtectedTimelock.cancelAllNonExecutedProposals`](#) #### Preconditions * MUST be called by an [admin voting system](#) diff --git a/docs/specification.md b/docs/specification.md index b6c74482..47a55d05 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -310,18 +310,18 @@ Calls the `ResealManager.resumeSealable(address sealable)` if all preconditions ### Function: DualGovernance.cancelAllPendingProposals ```solidity -function cancelAllPendingProposals() +function cancelAllPendingProposals() returns (bool) ``` Cancels all currently submitted and non-executed proposals. If a proposal was submitted but not scheduled, it becomes unschedulable. If a proposal was scheduled, it becomes unexecutable. +If the current governance state is neither `VetoSignalling` nor `VetoSignallingDeactivation`, the function will exit early without canceling any proposals, emitting the `CancelAllPendingProposalsSkipped` event and returning `false`. If proposals are successfully canceled, the `CancelAllPendingProposalsExecuted` event will be emitted, and the function will return `true`. + 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`. - +- MUST be called by an [admin proposer](#Administrative-actions). ### Function: DualGovernance.registerProposer From a3e6a8eeffff6247051508e6d9823bdf8fbbc4e4 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 27 Sep 2024 12:50:29 +0400 Subject: [PATCH 86/86] getTiebreakerDetails correct isTie value in edge case --- contracts/DualGovernance.sol | 7 +- contracts/libraries/Tiebreaker.sol | 21 +++++- test/unit/DualGovernance.t.sol | 92 +++++++++++++++++++++++ test/unit/libraries/Tiebreaker.t.sol | 108 ++++++++++++++++++++++++++- 4 files changed, 217 insertions(+), 11 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index f38f73c0..66175aaf 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -466,12 +466,7 @@ contract DualGovernance is IDualGovernance { /// (not in `Normal` or `VetoCooldown` state) before the tiebreaker committee is permitted to take actions. /// - `sealableWithdrawalBlockers`: An array of sealable contracts registered in the system as withdrawal blockers. function getTiebreakerDetails() external view returns (ITiebreaker.TiebreakerDetails memory tiebreakerState) { - return _tiebreaker.getTiebreakerDetails( - /// @dev Calling getEffectiveState() doesn't update the normalOrVetoCooldownStateExitedAt value, - /// but this does not distort the result of getTiebreakerDetails() - _stateMachine.getEffectiveState(), - _stateMachine.normalOrVetoCooldownExitedAt - ); + return _tiebreaker.getTiebreakerDetails(_stateMachine.getStateDetails()); } // --- diff --git a/contracts/libraries/Tiebreaker.sol b/contracts/libraries/Tiebreaker.sol index 872bde88..bf775705 100644 --- a/contracts/libraries/Tiebreaker.sol +++ b/contracts/libraries/Tiebreaker.sol @@ -8,6 +8,7 @@ import {Timestamp, Timestamps} from "../types/Duration.sol"; import {ISealable} from "../interfaces/ISealable.sol"; import {ITiebreaker} from "../interfaces/ITiebreaker.sol"; +import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; import {SealableCalls} from "./SealableCalls.sol"; import {State as DualGovernanceState} from "./DualGovernanceStateMachine.sol"; @@ -190,15 +191,29 @@ library Tiebreaker { /// @dev Retrieves the tiebreaker context from the storage. /// @param self The storage context. + /// @param stateDetails A struct containing detailed information about the current state of the Dual Governance system /// @return context The tiebreaker context containing the tiebreaker committee, tiebreaker activation timeout, and sealable withdrawal blockers. function getTiebreakerDetails( Context storage self, - DualGovernanceState state, - Timestamp normalOrVetoCooldownExitedAt + IDualGovernance.StateDetails memory stateDetails ) internal view returns (ITiebreaker.TiebreakerDetails memory context) { context.tiebreakerCommittee = self.tiebreakerCommittee; context.tiebreakerActivationTimeout = self.tiebreakerActivationTimeout; - context.isTie = isTie(self, state, normalOrVetoCooldownExitedAt); + + DualGovernanceState persistedState = stateDetails.persistedState; + DualGovernanceState effectiveState = stateDetails.effectiveState; + Timestamp normalOrVetoCooldownExitedAt = stateDetails.normalOrVetoCooldownExitedAt; + + if (effectiveState != persistedState) { + if (persistedState == DualGovernanceState.Normal || persistedState == DualGovernanceState.VetoCooldown) { + /// @dev When a pending state change is expected from the `Normal` or `VetoCooldown` state, + /// the `normalOrVetoCooldownExitedAt` timestamp should be set to the current timestamp to reflect + /// the behavior of the `DualGovernanceStateMachine.activateNextState()` method. + normalOrVetoCooldownExitedAt = Timestamps.now(); + } + } + + context.isTie = isTie(self, effectiveState, normalOrVetoCooldownExitedAt); uint256 sealableWithdrawalBlockersCount = self.sealableWithdrawalBlockers.length(); context.sealableWithdrawalBlockers = new address[](sealableWithdrawalBlockersCount); diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 5434c22f..36a6601c 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -1879,6 +1879,98 @@ contract DualGovernanceUnitTests is UnitTest { assertFalse(_dualGovernance.getTiebreakerDetails().isTie); } + function test_getTiebreakerDetails_NormalOrVetoCooldownExitedAtValueShouldBeUpdatedToCorrectlyCalculateIsTieValue() + external + { + address tiebreakerCommittee = makeAddr("TIEBREAKER_COMMITTEE"); + address sealable = address(new SealableMock()); + Duration tiebreakerActivationTimeout = Durations.from(180 days); + + // for the correctness of the test, the following assumption must be true + assertTrue(tiebreakerActivationTimeout >= _configProvider.VETO_SIGNALLING_MAX_DURATION()); + + // setup tiebreaker + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerCommittee.selector, tiebreakerCommittee) + ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setTiebreakerActivationTimeout.selector, tiebreakerActivationTimeout) + ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.addTiebreakerSealableWithdrawalBlocker.selector, sealable) + ); + + assertEq(_dualGovernance.getPersistedState(), State.Normal); + assertEq(_dualGovernance.getEffectiveState(), State.Normal); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + vm.prank(vetoer); + _escrow.lockStETH(5 ether); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignalling); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.RageQuit); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.RageQuit); + assertEq(_dualGovernance.getEffectiveState(), State.RageQuit); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + _wait(tiebreakerActivationTimeout); + + vm.mockCall( + _dualGovernance.getRageQuitEscrow(), + abi.encodeWithSelector(Escrow.isRageQuitFinalized.selector), + abi.encode(true) + ); + + assertEq(_dualGovernance.getPersistedState(), State.RageQuit); + assertEq(_dualGovernance.getEffectiveState(), State.VetoCooldown); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + _dualGovernance.activateNextState(); + + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); + assertEq(_dualGovernance.getEffectiveState(), State.VetoCooldown); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + // signalling accumulated rage quit support + vm.mockCall( + _dualGovernance.getVetoSignallingEscrow(), + abi.encodeWithSelector(Escrow.getRageQuitSupport.selector), + abi.encode(PercentsD16.fromBasisPoints(5_00)) + ); + + _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); + + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignalling); + + // The extra case, when the transition from the VetoCooldown should happened. + // In such case, `normalOrVetoCooldownExitedAt` will be updated and isTie value + // still will be equal to `false` + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + + _dualGovernance.activateNextState(); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); + assertEq(_dualGovernance.getEffectiveState(), State.VetoSignalling); + assertFalse(_dualGovernance.getTiebreakerDetails().isTie); + } + // --- // resealSealable() // --- diff --git a/test/unit/libraries/Tiebreaker.t.sol b/test/unit/libraries/Tiebreaker.t.sol index 0a306841..f51fe839 100644 --- a/test/unit/libraries/Tiebreaker.t.sol +++ b/test/unit/libraries/Tiebreaker.t.sol @@ -8,6 +8,7 @@ import {Tiebreaker} from "contracts/libraries/Tiebreaker.sol"; import {Duration, Durations, Timestamp, Timestamps} from "contracts/types/Duration.sol"; import {ISealable} from "contracts/interfaces/ISealable.sol"; import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; +import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; import {UnitTest} from "test/utils/unit-test.sol"; import {SealableMock} from "../../mocks/SealableMock.sol"; @@ -190,8 +191,45 @@ contract TiebreakerTest is UnitTest { context.tiebreakerActivationTimeout = timeout; context.tiebreakerCommittee = address(0x123); - ITiebreaker.TiebreakerDetails memory details = - Tiebreaker.getTiebreakerDetails(context, DualGovernanceState.Normal, Timestamps.from(block.timestamp)); + IDualGovernance.StateDetails memory stateDetails; + stateDetails.persistedState = DualGovernanceState.Normal; + stateDetails.effectiveState = DualGovernanceState.VetoSignalling; + stateDetails.normalOrVetoCooldownExitedAt = Timestamps.now(); + + ITiebreaker.TiebreakerDetails memory details = Tiebreaker.getTiebreakerDetails(context, stateDetails); + + assertEq(details.tiebreakerCommittee, context.tiebreakerCommittee); + assertEq(details.tiebreakerActivationTimeout, context.tiebreakerActivationTimeout); + assertEq(details.sealableWithdrawalBlockers[0], address(mockSealable1)); + assertEq(details.sealableWithdrawalBlockers.length, 1); + assertEq(details.isTie, false); + } + + function test_getTiebreakerDetails_HappyPath_PendingTransitionFromVetoCooldown_ExpectIsTieFalse() external { + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); + + Duration timeout = Duration.wrap(5 days); + + context.tiebreakerActivationTimeout = timeout; + context.tiebreakerCommittee = address(0x123); + + IDualGovernance.StateDetails memory stateDetails; + + stateDetails.persistedState = DualGovernanceState.VetoCooldown; + stateDetails.effectiveState = DualGovernanceState.VetoSignalling; + stateDetails.normalOrVetoCooldownExitedAt = Timestamps.now(); + + ITiebreaker.TiebreakerDetails memory details = Tiebreaker.getTiebreakerDetails(context, stateDetails); + + assertEq(details.tiebreakerCommittee, context.tiebreakerCommittee); + assertEq(details.tiebreakerActivationTimeout, context.tiebreakerActivationTimeout); + assertEq(details.sealableWithdrawalBlockers[0], address(mockSealable1)); + assertEq(details.sealableWithdrawalBlockers.length, 1); + assertEq(details.isTie, false); + + _wait(timeout); + + details = Tiebreaker.getTiebreakerDetails(context, stateDetails); assertEq(details.tiebreakerCommittee, context.tiebreakerCommittee); assertEq(details.tiebreakerActivationTimeout, context.tiebreakerActivationTimeout); @@ -200,6 +238,72 @@ contract TiebreakerTest is UnitTest { assertEq(details.isTie, false); } + function test_getTiebreakerDetails_HappyPath_PendingTransitionFromNormal_ExpectIsTieFalse() external { + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); + + Duration timeout = Duration.wrap(5 days); + + context.tiebreakerActivationTimeout = timeout; + context.tiebreakerCommittee = address(0x123); + + IDualGovernance.StateDetails memory stateDetails; + + stateDetails.persistedState = DualGovernanceState.Normal; + stateDetails.effectiveState = DualGovernanceState.VetoSignalling; + stateDetails.normalOrVetoCooldownExitedAt = Timestamps.now(); + + ITiebreaker.TiebreakerDetails memory details = Tiebreaker.getTiebreakerDetails(context, stateDetails); + + assertEq(details.tiebreakerCommittee, context.tiebreakerCommittee); + assertEq(details.tiebreakerActivationTimeout, context.tiebreakerActivationTimeout); + assertEq(details.sealableWithdrawalBlockers[0], address(mockSealable1)); + assertEq(details.sealableWithdrawalBlockers.length, 1); + assertEq(details.isTie, false); + + _wait(timeout); + + details = Tiebreaker.getTiebreakerDetails(context, stateDetails); + + assertEq(details.tiebreakerCommittee, context.tiebreakerCommittee); + assertEq(details.tiebreakerActivationTimeout, context.tiebreakerActivationTimeout); + assertEq(details.sealableWithdrawalBlockers[0], address(mockSealable1)); + assertEq(details.sealableWithdrawalBlockers.length, 1); + assertEq(details.isTie, false); + } + + function test_getTiebreakerDetails_HappyPath_PendingTransitionFromVetoSignalling_ExpectIsTieTrue() external { + Tiebreaker.addSealableWithdrawalBlocker(context, address(mockSealable1), 1); + + Duration timeout = Duration.wrap(5 days); + + context.tiebreakerActivationTimeout = timeout; + context.tiebreakerCommittee = address(0x123); + + IDualGovernance.StateDetails memory stateDetails; + + stateDetails.persistedState = DualGovernanceState.VetoSignalling; + stateDetails.effectiveState = DualGovernanceState.RageQuit; + stateDetails.normalOrVetoCooldownExitedAt = Timestamps.now(); + + ITiebreaker.TiebreakerDetails memory details = Tiebreaker.getTiebreakerDetails(context, stateDetails); + + assertEq(details.tiebreakerCommittee, context.tiebreakerCommittee); + assertEq(details.tiebreakerActivationTimeout, context.tiebreakerActivationTimeout); + assertEq(details.sealableWithdrawalBlockers[0], address(mockSealable1)); + assertEq(details.sealableWithdrawalBlockers.length, 1); + assertEq(details.isTie, false); + + _wait(timeout); + + details = Tiebreaker.getTiebreakerDetails(context, stateDetails); + + assertEq(details.tiebreakerCommittee, context.tiebreakerCommittee); + assertEq(details.tiebreakerActivationTimeout, context.tiebreakerActivationTimeout); + assertEq(details.sealableWithdrawalBlockers[0], address(mockSealable1)); + assertEq(details.sealableWithdrawalBlockers.length, 1); + assertEq(details.isTie, true); + } + function external__checkCallerIsTiebreakerCommittee() external view { Tiebreaker.checkCallerIsTiebreakerCommittee(context); }