From b0a51b49f20c9998115c8c44bc0d163b2b941a3c Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 14 Nov 2024 00:37:42 +0400 Subject: [PATCH 001/107] Add scenario test of time-sensitive proposal launch --- .../time-sensitive-proposal-execution.t.sol | 170 ++++++++++++++++++ test/utils/time-constraints.sol | 77 ++++++++ 2 files changed, 247 insertions(+) create mode 100644 test/scenario/time-sensitive-proposal-execution.t.sol create mode 100644 test/utils/time-constraints.sol diff --git a/test/scenario/time-sensitive-proposal-execution.t.sol b/test/scenario/time-sensitive-proposal-execution.t.sol new file mode 100644 index 00000000..a3caa751 --- /dev/null +++ b/test/scenario/time-sensitive-proposal-execution.t.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Durations, Duration} from "contracts/types/Duration.sol"; +import {Timestamps, Timestamp} from "contracts/types/Timestamp.sol"; + +import {TimeConstraints} from "../utils/time-constraints.sol"; +import {ExternalCall, ExternalCallHelpers} from "../utils/executor-calls.sol"; +import {ScenarioTestBlueprint, LidoUtils, console} from "../utils/scenario-test-blueprint.sol"; + +interface ITimeSensitiveContract { + function timeSensitiveMethod() external; +} + +contract ScheduledProposalExecution is ScenarioTestBlueprint { + TimeConstraints private immutable _TIME_CONSTRAINTS = new TimeConstraints(); + + Duration private immutable _MIN_EXECUTION_DELAY = Durations.from(30 days); // Proposal may be executed not earlier than the 30 days from launch + Duration private immutable _EXECUTION_START_DAY_TIME = Durations.from(4 hours); // And at time frame starting from the 4:00 UTC + Duration private immutable _EXECUTION_END_DAY_TIME = Durations.from(12 hours); // till the 12:00 UTC + + function setUp() external { + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); + } + + function testFork_TimeFrameProposalExecution() external { + Timestamp executableAfter = _MIN_EXECUTION_DELAY.addTo(Timestamps.now()); + // Prepare the call to be launched not earlier than the minExecutionDelay seconds from the creation of the + // Aragon Voting to submit proposal and only in the day time range [executionStartDayTime, executionEndDayTime] in UTC + ExternalCall[] memory scheduledProposalCalls = ExternalCallHelpers.create( + [ + ExternalCall({ + target: address(_TIME_CONSTRAINTS), + value: 0 wei, + payload: abi.encodeCall(_TIME_CONSTRAINTS.checkExecuteAfterTimestamp, (executableAfter)) + }), + ExternalCall({ + target: address(_TIME_CONSTRAINTS), + value: 0 wei, + payload: abi.encodeCall( + _TIME_CONSTRAINTS.checkExecuteWithinDayTime, (_EXECUTION_START_DAY_TIME, _EXECUTION_END_DAY_TIME) + ) + }), + ExternalCall({ + target: address(_targetMock), + value: 0 wei, + payload: abi.encodeCall(ITimeSensitiveContract.timeSensitiveMethod, ()) + }) + ] + ); + + uint256 proposalId; + _step("1. Submit time sensitive proposal"); + { + proposalId = + _submitProposal(_dualGovernance, "DAO performs some time sensitive action", scheduledProposalCalls); + + _assertProposalSubmitted(proposalId); + _assertSubmittedProposalData(proposalId, scheduledProposalCalls); + } + + _step("2. Wait while the DG timelock has passed & schedule proposal"); + { + _wait(_timelock.getAfterSubmitDelay().plusSeconds(1)); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); + _assertProposalScheduled(proposalId); + } + + _step("3. Proposal can't be executed earlier than specified date"); + { + _waitAfterScheduleDelayPassed(); + _assertCanExecute(proposalId, true); + assertTrue(Timestamps.now() < executableAfter); + + vm.expectRevert(abi.encodeWithSelector(TimeConstraints.TimestampNotReached.selector, (executableAfter))); + _executeProposal(proposalId); + } + + _step("4. Wait until the proposal become executable"); + { + _wait(_MIN_EXECUTION_DELAY); + assertTrue(Timestamps.now() >= executableAfter); + } + + uint256 midnightSnapshotId; + _step("5. Adjust current day time of the node to 00:00 UTC"); + { + // adjust current time to 00:00 UTC + _wait(_TIME_CONSTRAINTS.DAY_DURATION() - _TIME_CONSTRAINTS.getCurrentDayTime()); + assertEq(_TIME_CONSTRAINTS.getCurrentDayTime(), Durations.ZERO); + + midnightSnapshotId = vm.snapshot(); + } + + _step("6.a. Execution reverts when current time is less than allowed range"); + { + assertTrue(_TIME_CONSTRAINTS.getCurrentDayTime() < _EXECUTION_START_DAY_TIME); + + vm.expectRevert( + abi.encodeWithSelector( + TimeConstraints.DayTimeOutOfRange.selector, + _TIME_CONSTRAINTS.getCurrentDayTime(), + _EXECUTION_START_DAY_TIME, + _EXECUTION_END_DAY_TIME + ) + ); + _executeProposal(proposalId); + } + vm.revertTo(midnightSnapshotId); + + _step("6.b. Execution reverts when current time is greater than allowed range"); + { + _wait(_EXECUTION_END_DAY_TIME.plusSeconds(1)); + assertTrue(_TIME_CONSTRAINTS.getCurrentDayTime() > _EXECUTION_END_DAY_TIME); + + vm.expectRevert( + abi.encodeWithSelector( + TimeConstraints.DayTimeOutOfRange.selector, + _TIME_CONSTRAINTS.getCurrentDayTime(), + _EXECUTION_START_DAY_TIME, + _EXECUTION_END_DAY_TIME + ) + ); + _executeProposal(proposalId); + } + vm.revertTo(midnightSnapshotId); + + ExternalCall[] memory expectedProposalCalls = ExternalCallHelpers.create([scheduledProposalCalls[2]]); + + _step("6.c. Proposal executes successfully at the first second of the allowed range"); + { + _wait(_EXECUTION_START_DAY_TIME); + assertTrue( + _TIME_CONSTRAINTS.getCurrentDayTime() >= _EXECUTION_START_DAY_TIME + && _TIME_CONSTRAINTS.getCurrentDayTime() <= _EXECUTION_END_DAY_TIME + ); + + _executeProposal(proposalId); + _assertTargetMockCalls(_timelock.getAdminExecutor(), expectedProposalCalls); + } + vm.revertTo(midnightSnapshotId); + + _step("6.d. Proposal executes successfully at the last second of the allowed range"); + { + _wait(_EXECUTION_END_DAY_TIME); + assertTrue( + _TIME_CONSTRAINTS.getCurrentDayTime() >= _EXECUTION_START_DAY_TIME + && _TIME_CONSTRAINTS.getCurrentDayTime() <= _EXECUTION_END_DAY_TIME + ); + + _executeProposal(proposalId); + _assertTargetMockCalls(_timelock.getAdminExecutor(), expectedProposalCalls); + } + vm.revertTo(midnightSnapshotId); + + _step("6.e. Proposal executes successfully at the middle of the allowed range"); + { + _wait((_EXECUTION_END_DAY_TIME - _EXECUTION_START_DAY_TIME).dividedBy(2)); + assertTrue( + _TIME_CONSTRAINTS.getCurrentDayTime() >= _EXECUTION_START_DAY_TIME + && _TIME_CONSTRAINTS.getCurrentDayTime() <= _EXECUTION_END_DAY_TIME + ); + + _executeProposal(proposalId); + _assertTargetMockCalls(_timelock.getAdminExecutor(), expectedProposalCalls); + } + vm.revertTo(midnightSnapshotId); + } +} diff --git a/test/utils/time-constraints.sol b/test/utils/time-constraints.sol new file mode 100644 index 00000000..c8cd6203 --- /dev/null +++ b/test/utils/time-constraints.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Durations, Duration} from "contracts/types/Duration.sol"; +import {Timestamps, Timestamp} from "contracts/types/Timestamp.sol"; + +/// @title Time Constraints Contract +/// @notice Provides functionality to restrict execution of functions based on time constraints. +contract TimeConstraints { + // --- + // Errors + // --- + + error DayTimeOverflow(); + error InvalidDayTimeRange(Duration startDayTime, Duration endDayTime); + error DayTimeOutOfRange(Duration currentDayTime, Duration startDayTime, Duration endDayTime); + error TimestampNotReached(Timestamp requiredTimestamp); + + // --- + // Constants + // --- + + /// @notice Total number of seconds in a day (24 hours). + Duration public immutable DAY_DURATION = Durations.from(24 hours); + + // --- + // Time Constraints Checks + // --- + + /// @notice Checks that the transaction can only be executed within a specific time range during the day. + /// @param startDayTime The start time of the allowed range in seconds since midnight (UTC). + /// @param endDayTime The end time of the allowed range in seconds since midnight (UTC). + function checkExecuteWithinDayTime(Duration startDayTime, Duration endDayTime) external view { + _validateDayTime(startDayTime); + _validateDayTime(endDayTime); + + if (startDayTime > endDayTime) { + revert InvalidDayTimeRange(startDayTime, endDayTime); + } + + Duration currentDayTime = getCurrentDayTime(); + if (currentDayTime < startDayTime || currentDayTime > endDayTime) { + revert DayTimeOutOfRange(currentDayTime, startDayTime, endDayTime); + } + } + + /// @notice Checks that the transaction can only be executed after a specific timestamp. + /// @param timestamp The Unix timestamp after which the function can be executed. + function checkExecuteAfterTimestamp(Timestamp timestamp) external view { + if (Timestamps.now() < timestamp) { + revert TimestampNotReached(timestamp); + } + } + + // --- + // Getters + // --- + + /// @notice Gets the current time in seconds since midnight (UTC). + /// @return The current time in seconds since midnight. + function getCurrentDayTime() public view returns (Duration) { + return Durations.from(block.timestamp % DAY_DURATION.toSeconds()); + } + + // --- + // Internal Methods + // --- + + /// @notice Validates that a provided day time value is within the [0:00:00, 23:59:59] range. + /// @param dayTime The day time value in seconds to validate. + /// @dev Reverts with `DayTimeOverflow` if the value exceeds the number of seconds in a day. + function _validateDayTime(Duration dayTime) internal view { + if (dayTime >= DAY_DURATION) { + revert DayTimeOverflow(); + } + } +} From f1c9482910612d823767408a097bfb4c358128bc Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 15 Nov 2024 03:11:21 +0400 Subject: [PATCH 002/107] Reset RageQuitRound in the VetoCooldown state --- .../libraries/DualGovernanceStateMachine.sol | 2 +- docs/mechanism.md | 7 +- .../DualGovernanceStateMachine.t.sol | 676 +++++++++--------- test/utils/testing-assert-eq-extender.sol | 4 + 4 files changed, 333 insertions(+), 356 deletions(-) diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index cfb174a7..670283d4 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -154,7 +154,7 @@ library DualGovernanceStateMachine { self.normalOrVetoCooldownExitedAt = Timestamps.now(); } - if (newState == State.Normal && self.rageQuitRound != 0) { + if (newState == State.VetoCooldown && self.rageQuitRound != 0) { self.rageQuitRound = 0; } else if (newState == State.VetoSignalling) { if (currentState == State.VetoSignallingDeactivation) { diff --git a/docs/mechanism.md b/docs/mechanism.md index 07b9f427..314564a5 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -12,7 +12,7 @@ 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: +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. @@ -307,7 +307,7 @@ W(i) = \min \left\{ W_{min} + i * W_{growth} \,,\, W_{max} \right\} 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. +The rage quit sequence number is calculated as follows: each time the VetoCooldown 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 @@ -399,6 +399,9 @@ Dual governance should not cover: ## Changelog +### 2024-11-15 +- The rage quit sequence number is now reset in the `VetoCooldown` state instead of the `Normal` state. This adjustment ensures that the ETH withdrawal timelock does not increase unnecessarily in cases where, after a Rage Quit, Dual Governance cycles through `VetoSignalling` → `VetoSignallingDeactivation` → `VetoCooldown` without entering the `Normal` state, as the DAO remains operational and can continue submitting and executing proposals in this scenario. + ### 2024-09-12 - Explicitly described the `VetoSignallingDeactivation` -> `RageQuit` state transition. - Renamed `RageQuitExtensionDelay` to `RageQuitExtensionPeriodDuration`. diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 829d5b87..3377885f 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {IEscrow} from "contracts/interfaces/IEscrow.sol"; + import {Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; @@ -14,12 +16,12 @@ import { } from "contracts/ImmutableDualGovernanceConfigProvider.sol"; import {UnitTest} from "test/utils/unit-test.sol"; -import {IEscrow, EscrowMock} from "test/mocks/EscrowMock.sol"; contract DualGovernanceStateMachineUnitTests is UnitTest { using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; - IEscrow private immutable _ESCROW_MASTER_COPY = new EscrowMock(); + address private immutable _ESCROW_MASTER_COPY_MOCK = makeAddr("ESCROW_MASTER_COPY_MOCK"); + ImmutableDualGovernanceConfigProvider internal immutable _CONFIG_PROVIDER = new ImmutableDualGovernanceConfigProvider( DualGovernanceConfig.Context({ firstSealRageQuitSupport: PercentsD16.fromBasisPoints(3_00), // 3% @@ -44,7 +46,9 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { DualGovernanceStateMachine.Context private _stateMachine; function setUp() external { - _stateMachine.initialize(_CONFIG_PROVIDER, _ESCROW_MASTER_COPY); + _stateMachine.initialize(_CONFIG_PROVIDER, IEscrow(_ESCROW_MASTER_COPY_MOCK)); + _mockRageQuitFinalized(false); + _mockRageQuitSupport(PercentsD16.from(0)); } function test_initialize_RevertOn_ReInitialization() external { @@ -56,42 +60,126 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { // activateNextState() // --- + function test_activateNextState_SideEffects_RageQuitRoundResetInVetoCooldownAfterRageQuit() external { + // Transition state machine into the VetoSignalling state + _mockRageQuitSupport(_CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1)); + _activateNextState(); + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); + + // Simulate the Rage Quit process has completed and in the SignallingEscrow the first seal is not crossed + _mockRageQuitFinalized(true); + _activateNextState(); + _mockRageQuitSupport(PercentsD16.fromBasisPoints(0)); + + // Rage Quit Round should reset after system entered the VetoCooldown + + _assertState({persisted: State.RageQuit, effective: State.VetoCooldown}); + assertEq(_stateMachine.rageQuitRound, 1); + + _activateNextState(); + + _assertState({persisted: State.VetoCooldown, effective: State.VetoCooldown}); + assertEq(_stateMachine.rageQuitRound, 0); + } + + function test_activateNextState_SideEffects_RageQuitRoundResetInVetoCooldownAfterVetoSignallingDeactivation() + external + { + // Transition state machine into the RageQuit state + _mockRageQuitSupport(_CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1)); + _activateNextState(); + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); + + // Simulate the Rage Quit process has completed and in the SignallingEscrow the first seal is crossed + _mockRageQuitFinalized(true); + _activateNextState(); + + _assertState({persisted: State.RageQuit, effective: State.VetoSignalling}); + assertEq(_stateMachine.rageQuitRound, 1); + + _activateNextState(); + + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + assertEq(_stateMachine.rageQuitRound, 1); + + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().dividedBy(2)); + + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + assertEq(_stateMachine.rageQuitRound, 1); + + // Simulate the Rage Quit support decreased + _mockRageQuitSupport(PercentsD16.from(3_00)); + + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignallingDeactivation}); + assertEq(_stateMachine.rageQuitRound, 1); + + _activateNextState(); + + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignallingDeactivation}); + assertEq(_stateMachine.rageQuitRound, 1); + + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoCooldown}); + assertEq(_stateMachine.rageQuitRound, 1); + + _activateNextState(); + + _assertState({persisted: State.VetoCooldown, effective: State.VetoCooldown}); + assertEq(_stateMachine.rageQuitRound, 0); + } + function test_activateNextState_HappyPath_MaxRageQuitsRound() external { - assertEq(_stateMachine.state, State.Normal); + _assertState({persisted: State.Normal, effective: 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)); + // For the simplicity, simulate that Signalling Escrow always has rage quit support greater than the second seal + _mockRageQuitSupport(_CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00)); + // And that the Rage Quit finalized + _mockRageQuitFinalized(true); - // 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)); + assertTrue( + _stateMachine.signallingEscrow.getRageQuitSupport() > _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + ); + _assertState({persisted: State.Normal, effective: State.VetoSignalling}); + _activateNextState(); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); - assertEq(_stateMachine.state, State.VetoSignalling); + // Simulate sequential Rage Quits + for (uint256 i = 0; i < 2 * DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND; ++i) { + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + assertEq(_stateMachine.rageQuitRound, Math.min(i, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND)); _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.state, State.RageQuit); + // Effective state is VetoSignalling, as the rage quit is considered finalized + _assertState({persisted: State.RageQuit, effective: State.VetoSignalling}); assertEq(_stateMachine.rageQuitRound, Math.min(i + 1, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND)); - EscrowMock(signallingEscrow).__setIsRageQuitFinalized(true); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); - assertEq(_stateMachine.state, State.VetoCooldown); + _activateNextState(); } + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().dividedBy(2)); + // after the sequential rage quits chain is broken, the rage quit resets to 0 - _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _mockRageQuitSupport(PercentsD16.from(0)); + + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignallingDeactivation}); + assertEq(_stateMachine.rageQuitRound, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND); + + _activateNextState(); + + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignallingDeactivation}); + assertEq(_stateMachine.rageQuitRound, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND); + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoCooldown}); + assertEq(_stateMachine.rageQuitRound, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND); + + _activateNextState(); + + _assertState({persisted: State.VetoCooldown, effective: State.VetoCooldown}); assertEq(_stateMachine.rageQuitRound, 0); - assertEq(_stateMachine.state, State.Normal); } // --- @@ -99,102 +187,71 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { // --- 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})); + _assertState({persisted: State.Normal, effective: State.Normal}); + _assertCanSubmitProposal({persisted: true, effective: true}); // simulate the first threshold of veto signalling was reached - EscrowMock(signallingEscrow).__setRageQuitSupport( - _CONFIG_PROVIDER.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentD16.wrap(1) - ); + _mockRageQuitSupport(_CONFIG_PROVIDER.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.from(1)); - assertEq(_stateMachine.getPersistedState(), State.Normal); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.Normal, effective: State.VetoSignalling}); + _assertCanSubmitProposal({persisted: true, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + _assertCanSubmitProposal({persisted: true, effective: true}); - _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MIN_DURATION().plusSeconds(1 minutes)); + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MIN_DURATION().plusSeconds(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignallingDeactivation}); + _assertCanSubmitProposal({persisted: true, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); - assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignallingDeactivation}); + _assertCanSubmitProposal({persisted: false, effective: false}); // simulate the second threshold of veto signalling was reached - EscrowMock(signallingEscrow).__setRageQuitSupport( - _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) - ); + _mockRageQuitSupport(_CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.from(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignalling}); + _assertCanSubmitProposal({persisted: false, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + _assertCanSubmitProposal({persisted: true, effective: 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})); + _assertState({persisted: State.VetoSignalling, effective: State.RageQuit}); + _assertCanSubmitProposal({persisted: true, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.RageQuit); - assertEq(_stateMachine.getEffectiveState(), State.RageQuit); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.RageQuit, effective: State.RageQuit}); + _assertCanSubmitProposal({persisted: true, effective: true}); - EscrowMock(address(_stateMachine.rageQuitEscrow)).__setIsRageQuitFinalized(true); + _mockRageQuitFinalized(true); + _mockRageQuitSupport(PercentsD16.from(0)); - assertEq(_stateMachine.getPersistedState(), State.RageQuit); - assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.RageQuit, effective: State.VetoCooldown}); + _assertCanSubmitProposal({persisted: true, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); - assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); - assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.VetoCooldown, effective: State.VetoCooldown}); + _assertCanSubmitProposal({persisted: false, effective: false}); _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})); + _assertState({persisted: State.VetoCooldown, effective: State.Normal}); + _assertCanSubmitProposal({persisted: false, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.Normal); - assertEq(_stateMachine.getEffectiveState(), State.Normal); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); + _assertState({persisted: State.Normal, effective: State.Normal}); + _assertCanSubmitProposal({persisted: true, effective: true}); } // --- @@ -202,232 +259,117 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { // --- 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}) - ); + _assertState({persisted: State.Normal, effective: State.Normal}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: true, effective: true}); // simulate the first threshold of veto signalling was reached - EscrowMock(signallingEscrow).__setRageQuitSupport( - _CONFIG_PROVIDER.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentD16.wrap(1) - ); + _mockRageQuitSupport(_CONFIG_PROVIDER.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.from(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}) - ); + _assertState({persisted: State.Normal, effective: State.VetoSignalling}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: true, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: false}); - _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MIN_DURATION().plusSeconds(1 minutes)); + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MIN_DURATION().plusSeconds(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignallingDeactivation}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignallingDeactivation}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: false}); // simulate the second threshold of veto signalling was reached - EscrowMock(signallingEscrow).__setRageQuitSupport( - _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) - ); + _mockRageQuitSupport(_CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.from(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignalling}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: false}); _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); + _assertState({persisted: State.VetoSignalling, effective: State.RageQuit}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: false}); - assertEq(_stateMachine.getPersistedState(), State.RageQuit); - assertEq(_stateMachine.getEffectiveState(), State.RageQuit); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); + _activateNextState(); - 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()})); + _assertState({persisted: State.RageQuit, effective: State.RageQuit}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _mockRageQuitFinalized(true); + _mockRageQuitSupport(PercentsD16.from(0)); - assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); - assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); + _assertState({persisted: State.RageQuit, effective: State.VetoCooldown}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: true}); - // 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 - }) - ); + _assertCanScheduleProposal({ + proposalSubmittedAt: _stateMachine.vetoSignallingActivatedAt, + persisted: false, + effective: true + }); // 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()})); + _assertCanScheduleProposal({ + proposalSubmittedAt: Durations.from(1 seconds).addTo(_stateMachine.vetoSignallingActivatedAt), + persisted: false, + effective: false + }); + _assertCanScheduleProposal({proposalSubmittedAt: Timestamps.now(), persisted: false, effective: false}); + + _activateNextState(); + + _assertState({persisted: State.VetoCooldown, effective: State.VetoCooldown}); + + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: true, effective: true}); + _assertCanScheduleProposal({ + proposalSubmittedAt: _stateMachine.vetoSignallingActivatedAt, + persisted: true, + effective: true + }); + // for proposals submitted after the VetoSignalling started scheduling is forbidden + _assertCanScheduleProposal({ + proposalSubmittedAt: Durations.from(1 seconds).addTo(_stateMachine.vetoSignallingActivatedAt), + persisted: false, + effective: false + }); + _assertCanScheduleProposal({proposalSubmittedAt: Timestamps.now(), persisted: false, effective: false}); _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); - assertEq(_stateMachine.getEffectiveState(), State.Normal); + _assertState({persisted: State.VetoCooldown, effective: 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()}) - ); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: true, effective: true}); + _assertCanScheduleProposal({ + proposalSubmittedAt: _stateMachine.vetoSignallingActivatedAt, + persisted: true, + effective: true + }); - // effective - assertTrue( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertTrue(_stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: Timestamps.now()})); + // for proposals submitted after the VetoSignalling started scheduling is forbidden + _assertCanScheduleProposal({ + proposalSubmittedAt: Durations.from(1 seconds).addTo(_stateMachine.vetoSignallingActivatedAt), + persisted: false, + effective: true + }); + _assertCanScheduleProposal({proposalSubmittedAt: Timestamps.now(), persisted: false, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.Normal); - assertEq(_stateMachine.getEffectiveState(), State.Normal); + _assertState({persisted: State.Normal, effective: 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()})); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: true, effective: true}); + _assertCanScheduleProposal({proposalSubmittedAt: Timestamps.now(), persisted: true, effective: true}); } // --- @@ -435,110 +377,138 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { // --- 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})); + _assertState({persisted: State.Normal, effective: State.Normal}); + _assertCanCancelAllPendingProposals({persisted: false, effective: false}); // simulate the first threshold of veto signalling was reached - EscrowMock(signallingEscrow).__setRageQuitSupport( - _CONFIG_PROVIDER.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentD16.wrap(1) - ); + _mockRageQuitSupport(_CONFIG_PROVIDER.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.from(1)); - assertEq(_stateMachine.getPersistedState(), State.Normal); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.Normal, effective: State.VetoSignalling}); + _assertCanCancelAllPendingProposals({persisted: false, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + _assertCanCancelAllPendingProposals({persisted: true, effective: 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})); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignallingDeactivation}); + _assertCanCancelAllPendingProposals({persisted: true, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignallingDeactivation}); + _assertCanCancelAllPendingProposals({persisted: true, effective: true}); // simulate the second threshold of veto signalling was reached - EscrowMock(signallingEscrow).__setRageQuitSupport( - _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) - ); + _mockRageQuitSupport(_CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.from(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignalling}); + _assertCanCancelAllPendingProposals({persisted: true, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + _assertCanCancelAllPendingProposals({persisted: true, effective: 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})); + _assertState({persisted: State.VetoSignalling, effective: State.RageQuit}); + _assertCanCancelAllPendingProposals({persisted: true, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.RageQuit); - assertEq(_stateMachine.getEffectiveState(), State.RageQuit); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.RageQuit, effective: State.RageQuit}); + _assertCanCancelAllPendingProposals({persisted: false, effective: false}); - EscrowMock(address(_stateMachine.rageQuitEscrow)).__setIsRageQuitFinalized(true); + _mockRageQuitFinalized(true); + _mockRageQuitSupport(PercentsD16.from(0)); - assertEq(_stateMachine.getPersistedState(), State.RageQuit); - assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.RageQuit, effective: State.VetoCooldown}); + _assertCanCancelAllPendingProposals({persisted: false, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); - assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.VetoCooldown, effective: State.VetoCooldown}); + _assertCanCancelAllPendingProposals({persisted: false, effective: false}); _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})); + _assertState({persisted: State.VetoCooldown, effective: State.Normal}); + _assertCanCancelAllPendingProposals({persisted: false, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.Normal); - assertEq(_stateMachine.getEffectiveState(), State.Normal); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.Normal, effective: State.Normal}); + _assertCanCancelAllPendingProposals({persisted: false, effective: false}); } // --- // Test helper methods // --- + function _mockRageQuitSupport(PercentD16 rageQuitSupport) internal { + vm.mockCall( + _ESCROW_MASTER_COPY_MOCK, abi.encodeCall(IEscrow.getRageQuitSupport, ()), abi.encode(rageQuitSupport) + ); + } + + function _mockRageQuitFinalized(bool isRageQuitFinalized) internal { + vm.mockCall( + _ESCROW_MASTER_COPY_MOCK, abi.encodeCall(IEscrow.isRageQuitFinalized, ()), abi.encode(isRageQuitFinalized) + ); + } + + function _activateNextState() internal { + _stateMachine.activateNextState(IEscrow(_ESCROW_MASTER_COPY_MOCK)); + } + + function _assertState(State persisted, State effective) internal { + assertEq(_stateMachine.getPersistedState(), persisted, "Unexpected Persisted State"); + assertEq(_stateMachine.getEffectiveState(), effective, "Unexpected Effective State"); + } + + function _assertCanCancelAllPendingProposals(bool persisted, bool effective) internal { + assertEq( + _stateMachine.canCancelAllPendingProposals({useEffectiveState: false}), + persisted, + "Unexpected persisted canCancelAllPendingProposals() value" + ); + assertEq( + _stateMachine.canCancelAllPendingProposals({useEffectiveState: true}), + effective, + "Unexpected effective canCancelAllPendingProposals() value" + ); + } + + function _assertCanScheduleProposal(Timestamp proposalSubmittedAt, bool persisted, bool effective) internal { + assertEq( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}), + persisted, + "Unexpected persisted canScheduleProposal() value" + ); + assertEq( + _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}), + effective, + "Unexpected persisted canScheduleProposal() value" + ); + } + + function _assertCanSubmitProposal(bool persisted, bool effective) internal { + assertEq( + _stateMachine.canSubmitProposal({useEffectiveState: false}), + persisted, + "Unexpected persisted canSubmitProposal() value" + ); + assertEq( + _stateMachine.canSubmitProposal({useEffectiveState: true}), + effective, + "Unexpected effective canSubmitProposal() value" + ); + } + function external__initialize() external { - _stateMachine.initialize(_CONFIG_PROVIDER, _ESCROW_MASTER_COPY); + _stateMachine.initialize(_CONFIG_PROVIDER, IEscrow(_ESCROW_MASTER_COPY_MOCK)); } } diff --git a/test/utils/testing-assert-eq-extender.sol b/test/utils/testing-assert-eq-extender.sol index fcac37da..d4dc06cd 100644 --- a/test/utils/testing-assert-eq-extender.sol +++ b/test/utils/testing-assert-eq-extender.sol @@ -41,6 +41,10 @@ contract TestingAssertEqExtender is Test { assertEq(uint256(a), uint256(b)); } + function assertEq(DualGovernanceState a, DualGovernanceState b, string memory message) internal { + assertEq(uint256(a), uint256(b), message); + } + function assertEq(Balances memory b1, Balances memory b2, uint256 sharesEpsilon) internal { assertEq(b1.wstETHShares, b2.wstETHShares); assertEq(b1.wstETHAmount, b2.wstETHAmount); From 83adb3b99593d63f410d35c4479abfa0d10cade7 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Sat, 16 Nov 2024 00:56:18 +0400 Subject: [PATCH 003/107] Cancel pending proposals on governance change --- contracts/EmergencyProtectedTimelock.sol | 1 + test/scenario/dg-update-tokens-rotation.t.sol | 234 ++++++++++++++++++ test/unit/EmergencyProtectedTimelock.t.sol | 6 +- 3 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 test/scenario/dg-update-tokens-rotation.t.sol diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 4530ece5..28b707bd 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -135,6 +135,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { function setGovernance(address newGovernance) external { _checkCallerIsAdminExecutor(); _timelockState.setGovernance(newGovernance); + _proposals.cancelAll(); } /// @notice Configures the delays for submitting and scheduling proposals, within defined upper bounds. diff --git a/test/scenario/dg-update-tokens-rotation.t.sol b/test/scenario/dg-update-tokens-rotation.t.sol new file mode 100644 index 00000000..b99db258 --- /dev/null +++ b/test/scenario/dg-update-tokens-rotation.t.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Durations} from "contracts/types/Duration.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; + +import {TimelockState} from "contracts/libraries/TimelockState.sol"; +import {ExecutableProposals, Status as ProposalStatus} from "contracts/libraries/ExecutableProposals.sol"; + +import {Escrow} from "contracts/Escrow.sol"; +import {DualGovernance} from "contracts/DualGovernance.sol"; + +import {ScenarioTestBlueprint, ExternalCall, ExternalCallHelpers} from "../utils/scenario-test-blueprint.sol"; + +contract DualGovernanceUpdateTokensRotation is ScenarioTestBlueprint { + address internal immutable _VETOER = makeAddr("VETOER"); + + function setUp() external { + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); + _setupStETHBalance(_VETOER, PercentsD16.fromBasisPoints(30_00)); + } + + function testFork_DualGovernanceUpdate_OldEscrowInstanceAllowsUnlockTokens() external { + DualGovernance newDualGovernanceInstance; + _step("1. Deploy new Dual Governance implementation"); + { + newDualGovernanceInstance = _deployDualGovernance({ + timelock: _timelock, + resealManager: _resealManager, + configProvider: _dualGovernanceConfigProvider + }); + } + + uint256 updateDualGovernanceProposalId; + _step("2. Submit proposal to update the Dual Governance implementation"); + { + updateDualGovernanceProposalId = _submitProposalViaDualGovernance( + "Update the Dual Governance implementation", + _getActionsToUpdateDualGovernanceImplementation(address(newDualGovernanceInstance)) + ); + } + + _step("3. Users accumulate some stETH in the Signalling Escrow"); + { + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); + _assertVetoSignalingState(); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); + + _activateNextState(); + _assertVetoSignalingDeactivationState(); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + _activateNextState(); + _assertVetoCooldownState(); + } + + _step("4. When the VetoCooldown is entered proposal to update becomes executable"); + { + _scheduleProposalViaDualGovernance(updateDualGovernanceProposalId); + _assertProposalScheduled(updateDualGovernanceProposalId); + + _waitAfterScheduleDelayPassed(); + _executeProposal(updateDualGovernanceProposalId); + + assertEq(_timelock.getGovernance(), address(newDualGovernanceInstance)); + } + + _step("5. The old instance of the Dual Governance can't submit proposals anymore"); + { + // wait until the VetoCooldown ends in the old dual governance instance + _wait(_dualGovernanceConfigProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); + _activateNextState(); + _assertVetoSignalingState(); + + // old instance of the Dual Governance can't submit proposals anymore + vm.expectRevert( + abi.encodeWithSelector(TimelockState.CallerIsNotGovernance.selector, address(_dualGovernance)) + ); + vm.prank(address(_lido.voting)); + _dualGovernance.submitProposal(_getMockTargetRegularStaffCalls(), "empty metadata"); + } + + _step("6. Users can unlock stETH from the old Signalling Escrow"); + { + _unlockStETH(_VETOER); + } + + _step("7. Users can withdraw funds even if the Rage Quit is started in the old instance of the Dual Governance"); + { + // the Rage Quit started on the old DualGovernance instance + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.from(1)); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); + _activateNextState(); + _assertRageQuitState(); + + // The Rage Quit may be finished in the previous DG instance so vetoers will not lose their funds by mistake + Escrow rageQuitEscrow = Escrow(payable(_dualGovernance.getRageQuitEscrow())); + + while (!rageQuitEscrow.isWithdrawalsBatchesFinalized()) { + rageQuitEscrow.requestNextWithdrawalsBatch(96); + } + + _finalizeWithdrawalQueue(); + + while (rageQuitEscrow.getUnclaimedUnstETHIdsCount() > 0) { + rageQuitEscrow.claimNextWithdrawalsBatch(32); + } + + rageQuitEscrow.startRageQuitExtensionPeriod(); + + _wait(_dualGovernanceConfigProvider.RAGE_QUIT_EXTENSION_PERIOD_DURATION().plusSeconds(1)); + assertEq(rageQuitEscrow.isRageQuitFinalized(), true); + + // TODO: Add method Escrow.getRageQuitExtensionPeriodDuration() + _wait(_dualGovernanceConfigProvider.RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY()); + + uint256 vetoerETHBalanceBefore = _VETOER.balance; + + vm.prank(_VETOER); + rageQuitEscrow.withdrawETH(); + + assertTrue(_VETOER.balance > vetoerETHBalanceBefore); + } + } + + function testFork_DualGovernanceUpdate_LastMomentProposalAttack() external { + // DAO initiates the update of the Dual Governance + // Malicious actor locks funds in the Signalling Escrow to waste the full duration of VetoSignalling + // At the end of the VetoSignalling, malicious actor unlocks all funds from VetoSignalling and + // submits proposal to steal the control over governance + // + DualGovernance newDualGovernanceInstance; + _step("1. Deploy new Dual Governance implementation"); + { + newDualGovernanceInstance = _deployDualGovernance({ + timelock: _timelock, + resealManager: _resealManager, + configProvider: _dualGovernanceConfigProvider + }); + } + + uint256 updateDualGovernanceProposalId; + _step("2. DAO submits proposal to update the Dual Governance implementation"); + { + updateDualGovernanceProposalId = _submitProposalViaDualGovernance( + "Update the Dual Governance implementation", + _getActionsToUpdateDualGovernanceImplementation(address(newDualGovernanceInstance)) + ); + } + + _step("3. Malicious actor accumulate second seal in the Signalling Escrow"); + { + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().minusSeconds(_lido.voting.voteTime())); + _assertVetoSignalingState(); + } + + uint256 maliciousProposalId; + _step("4. Malicious actor unlock funds from Signalling Escrow"); + { + maliciousProposalId = _submitProposalViaDualGovernance( + "Steal control over timelock contract", + ExternalCallHelpers.create({ + target: address(_timelock), + payload: abi.encodeCall(_timelock.setGovernance, (_VETOER)) + }) + ); + _unlockStETH(_VETOER); + _assertVetoSignalingDeactivationState(); + } + + _step("5. Regular can't collect second seal in VETO_SIGNALLING_DEACTIVATION_MAX_DURATION"); + { + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + _activateNextState(); + _assertVetoCooldownState(); + } + + _step("6. The Dual Governance implementation is updated on the new version"); + { + // Malicious proposal can't be executed directly on the old DualGovernance instance, as it was submitted + // during the VetoSignalling phase + vm.expectRevert( + abi.encodeWithSelector(DualGovernance.ProposalSchedulingBlocked.selector, maliciousProposalId) + ); + _scheduleProposalViaDualGovernance(maliciousProposalId); + + _scheduleProposalViaDualGovernance(updateDualGovernanceProposalId); + _assertProposalScheduled(updateDualGovernanceProposalId); + + _waitAfterScheduleDelayPassed(); + _executeProposal(updateDualGovernanceProposalId); + + assertEq(_timelock.getGovernance(), address(newDualGovernanceInstance)); + } + + _step("7. After the update malicious proposal is cancelled and can't be executed via new DualGovernance"); + { + vm.expectRevert( + abi.encodeWithSelector(ExecutableProposals.ProposalNotSubmitted.selector, maliciousProposalId) + ); + newDualGovernanceInstance.scheduleProposal(maliciousProposalId); + + assertEq(_timelock.getProposalDetails(maliciousProposalId).status, ProposalStatus.Cancelled); + } + } + + // --- + // Helper methods + // --- + function _getActionsToUpdateDualGovernanceImplementation(address newDualGovernanceInstance) + internal + returns (ExternalCall[] memory) + { + return ExternalCallHelpers.create( + [ + // register Aragon Voting as proposer + ExternalCall({ + value: 0, + target: address(newDualGovernanceInstance), + payload: abi.encodeCall( + DualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor()) + ) + }), + ExternalCall({ + value: 0, + target: address(_timelock), + payload: abi.encodeCall(_timelock.setGovernance, (address(newDualGovernanceInstance))) + }) + // NOTE: There should be additional calls with the proper setting up of the new DG implementation + ] + ); + } +} diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index d5827462..08e63fd3 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -11,6 +11,7 @@ import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtec import {ITimelock, ProposalStatus} from "contracts/interfaces/ITimelock.sol"; import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; +import {ExecutableProposals} from "contracts/libraries/ExecutableProposals.sol"; import {Executor} from "contracts/Executor.sol"; import {EmergencyProtectedTimelock, TimelockState} from "contracts/EmergencyProtectedTimelock.sol"; @@ -263,6 +264,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.expectEmit(address(_timelock)); emit TimelockState.GovernanceSet(newGovernance); + vm.expectEmit(address(_timelock)); + emit ExecutableProposals.ProposalsCancelledTill(0); + vm.recordLogs(); vm.prank(_adminExecutor); _timelock.setGovernance(newGovernance); @@ -271,7 +275,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 1); + assertEq(entries.length, 2); } function test_setGovernance_RevertOn_ZeroAddress() external { From b6561808be32459c037799b77aaf69ce6234c00b Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 22 Nov 2024 19:16:02 +0300 Subject: [PATCH 004/107] check if minAssetsLockDuration changed on set --- contracts/Escrow.sol | 4 +++ contracts/interfaces/IEscrow.sol | 1 + .../libraries/DualGovernanceStateMachine.sol | 11 ++++-- test/mocks/EscrowMock.sol | 4 +++ test/unit/DualGovernance.t.sol | 35 +++++++++++++++++++ 5 files changed, 52 insertions(+), 3 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 0116a2af..2ca6629b 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -442,6 +442,10 @@ contract Escrow is IEscrow { // Escrow Management // --- + function getMinAssetsLockDuration() external view returns (Duration) { + return _escrowState.minAssetsLockDuration; + } + /// @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. diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol index 0542e47c..1b3332d4 100644 --- a/contracts/interfaces/IEscrow.sol +++ b/contracts/interfaces/IEscrow.sol @@ -11,5 +11,6 @@ interface IEscrow { function isRageQuitFinalized() external view returns (bool); function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport); + function getMinAssetsLockDuration() external view returns (Duration minAssetsLockDuration); function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; } diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index cfb174a7..62aafec3 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -190,9 +190,14 @@ library DualGovernanceStateMachine { /// @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 - ); + if ( + self.signallingEscrow.getMinAssetsLockDuration() + != newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration + ) { + self.signallingEscrow.setMinAssetsLockDuration( + newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration + ); + } } // --- diff --git a/test/mocks/EscrowMock.sol b/test/mocks/EscrowMock.sol index fe3e0798..86a1f570 100644 --- a/test/mocks/EscrowMock.sol +++ b/test/mocks/EscrowMock.sol @@ -40,4 +40,8 @@ contract EscrowMock is IEscrow { function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { __minAssetsLockDuration = newMinAssetsLockDuration; } + + function getMinAssetsLockDuration() external view returns (Duration minAssetsLockDuration) { + return __minAssetsLockDuration; + } } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index debc767d..86d4d55f 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -957,6 +957,41 @@ contract DualGovernanceUnitTests is UnitTest { assertTrue(address(_dualGovernance.getConfigProvider()) != address(oldConfigProvider)); } + function test_setConfigProvider_same_minAssetsLockDuration() external { + ImmutableDualGovernanceConfigProvider newConfigProvider = new ImmutableDualGovernanceConfigProvider( + DualGovernanceConfig.Context({ + firstSealRageQuitSupport: PercentsD16.fromBasisPoints(5_00), // 5% + secondSealRageQuitSupport: PercentsD16.fromBasisPoints(20_00), // 20% + // + minAssetsLockDuration: Durations.from(5 hours), + // + vetoSignallingMinDuration: Durations.from(4 days), + vetoSignallingMaxDuration: Durations.from(35 days), + vetoSignallingMinActiveDuration: Durations.from(6 hours), + vetoSignallingDeactivationMaxDuration: Durations.from(6 days), + vetoCooldownDuration: Durations.from(5 days), + // + rageQuitExtensionPeriodDuration: Durations.from(8 days), + rageQuitEthWithdrawalsMinDelay: Durations.from(30 days), + rageQuitEthWithdrawalsMaxDelay: Durations.from(180 days), + rageQuitEthWithdrawalsDelayGrowth: Durations.from(15 days) + }) + ); + + IDualGovernanceConfigProvider oldConfigProvider = _dualGovernance.getConfigProvider(); + + vm.expectEmit(); + emit DualGovernanceStateMachine.ConfigProviderSet(IDualGovernanceConfigProvider(address(newConfigProvider))); + _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( From 76d22fc4637065f7cf9a41e405733f5e5aae0c75 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Wed, 27 Nov 2024 12:30:08 +0300 Subject: [PATCH 005/107] gas optimization --- contracts/libraries/DualGovernanceStateMachine.sol | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 62aafec3..d3bb7ed6 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -188,15 +188,13 @@ library DualGovernanceStateMachine { function setConfigProvider(Context storage self, IDualGovernanceConfigProvider newConfigProvider) internal { _setConfigProvider(self, newConfigProvider); + IEscrow signallingEscrow = self.signallingEscrow; + Duration newMinAssetsLockDuration = newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration; + /// @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. - if ( - self.signallingEscrow.getMinAssetsLockDuration() - != newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration - ) { - self.signallingEscrow.setMinAssetsLockDuration( - newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration - ); + if (signallingEscrow.getMinAssetsLockDuration() != newMinAssetsLockDuration) { + signallingEscrow.setMinAssetsLockDuration(newMinAssetsLockDuration); } } From d5ed8cd4df7bc877785a7b4ce448d90007ecd486 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:56:59 +0400 Subject: [PATCH 006/107] Add deploy script draft --- foundry.toml | 1 + package.json | 2 +- scripts/deploy/Config.s.sol | 300 ++++++++++++++++++++++++++++++++ scripts/deploy/Deploy.s.sol | 333 ++++++++++++++++++++++++++++++++++++ 4 files changed, 635 insertions(+), 1 deletion(-) create mode 100644 scripts/deploy/Config.s.sol create mode 100644 scripts/deploy/Deploy.s.sol diff --git a/foundry.toml b/foundry.toml index a41bd76c..e2331b32 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,7 @@ [profile.default] src = 'contracts' out = 'out' +script = 'scripts' libs = ['node_modules', 'lib'] test = 'test' cache_path = 'cache_forge' diff --git a/package.json b/package.json index 7e2b49f4..00359545 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "scripts": { "test": "forge test", "prepare": "husky", - "lint": "solhint \"contracts/**/*.sol\" \"test/**/*.sol\" --ignore-path .solhintignore", + "lint": "solhint \"contracts/**/*.sol\" \"scripts/**/*.sol\" \"test/**/*.sol\" --ignore-path .solhintignore", "coverage": "forge coverage", "precov-report": "mkdir -p ./coverage-report && forge coverage --report lcov --report-file ./coverage-report/lcov.info", "cov-report": "genhtml ./coverage-report/lcov.info --rc derive_function_end_line=0 --rc branch_coverage=1 -o coverage-report --exclude test --ignore-errors inconsistent --ignore-errors category" diff --git a/scripts/deploy/Config.s.sol b/scripts/deploy/Config.s.sol new file mode 100644 index 00000000..44d75a72 --- /dev/null +++ b/scripts/deploy/Config.s.sol @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/* solhint-disable no-console, var-name-mixedcase */ + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {Durations, Duration} from "contracts/types/Duration.sol"; +import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; + +string constant ARRAY_SEPARATOR = ","; + +struct ConfigValues { + Duration AFTER_SUBMIT_DELAY; + Duration MAX_AFTER_SUBMIT_DELAY; + Duration AFTER_SCHEDULE_DELAY; + Duration MAX_AFTER_SCHEDULE_DELAY; + Duration EMERGENCY_MODE_DURATION; + Duration MAX_EMERGENCY_MODE_DURATION; + Duration EMERGENCY_PROTECTION_DURATION; + Duration MAX_EMERGENCY_PROTECTION_DURATION; + uint256 EMERGENCY_ACTIVATION_COMMITTEE_QUORUM; + address[] EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS; + uint256 EMERGENCY_EXECUTION_COMMITTEE_QUORUM; + address[] EMERGENCY_EXECUTION_COMMITTEE_MEMBERS; + uint256 TIEBREAKER_CORE_QUORUM; + Duration TIEBREAKER_EXECUTION_DELAY; + uint256 TIEBREAKER_SUB_COMMITTEES_COUNT; + address[] TIEBREAKER_SUB_COMMITTEE_1_MEMBERS; + uint256 TIEBREAKER_SUB_COMMITTEE_1_QUORUM; + address[] TIEBREAKER_SUB_COMMITTEE_2_MEMBERS; + uint256 TIEBREAKER_SUB_COMMITTEE_2_QUORUM; + address[] RESEAL_COMMITTEE_MEMBERS; + uint256 RESEAL_COMMITTEE_QUORUM; + uint256 MIN_WITHDRAWALS_BATCH_SIZE; + Duration MIN_TIEBREAKER_ACTIVATION_TIMEOUT; + Duration TIEBREAKER_ACTIVATION_TIMEOUT; + Duration MAX_TIEBREAKER_ACTIVATION_TIMEOUT; + uint256 MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT; + PercentD16 FIRST_SEAL_RAGE_QUIT_SUPPORT; + PercentD16 SECOND_SEAL_RAGE_QUIT_SUPPORT; + Duration MIN_ASSETS_LOCK_DURATION; + Duration DYNAMIC_TIMELOCK_MIN_DURATION; + Duration DYNAMIC_TIMELOCK_MAX_DURATION; + Duration VETO_SIGNALLING_MIN_ACTIVE_DURATION; + Duration VETO_SIGNALLING_DEACTIVATION_MAX_DURATION; + Duration VETO_COOLDOWN_DURATION; + Duration RAGE_QUIT_EXTENSION_DELAY; + Duration RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK; + uint256 RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER; + uint256[3] RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS; +} + +contract DGDeployConfig is Script { + error InvalidRageQuitETHWithdrawalsTimelockGrowthCoeffs(uint256[] coeffs); + error InvalidQuorum(string committee, uint256 quorum); + + uint256 internal immutable DEFAULT_AFTER_SUBMIT_DELAY = 3 days; + uint256 internal immutable DEFAULT_MAX_AFTER_SUBMIT_DELAY = 45 days; + uint256 internal immutable DEFAULT_AFTER_SCHEDULE_DELAY = 3 days; + uint256 internal immutable DEFAULT_MAX_AFTER_SCHEDULE_DELAY = 45 days; + uint256 internal immutable DEFAULT_EMERGENCY_MODE_DURATION = 180 days; + uint256 internal immutable DEFAULT_MAX_EMERGENCY_MODE_DURATION = 365 days; + uint256 internal immutable DEFAULT_EMERGENCY_PROTECTION_DURATION = 90 days; + uint256 internal immutable DEFAULT_MAX_EMERGENCY_PROTECTION_DURATION = 365 days; + uint256 internal immutable DEFAULT_EMERGENCY_ACTIVATION_COMMITTEE_QUORUM = 3; + uint256 internal immutable DEFAULT_EMERGENCY_EXECUTION_COMMITTEE_QUORUM = 5; + uint256 internal immutable DEFAULT_TIEBREAKER_CORE_QUORUM = 1; + uint256 internal immutable DEFAULT_TIEBREAKER_EXECUTION_DELAY = 30 days; + uint256 internal immutable DEFAULT_TIEBREAKER_SUB_COMMITTEES_COUNT = 2; + uint256 internal immutable DEFAULT_TIEBREAKER_SUB_COMMITTEE_1_QUORUM = 5; + uint256 internal immutable DEFAULT_TIEBREAKER_SUB_COMMITTEE_2_QUORUM = 5; + uint256 internal immutable DEFAULT_RESEAL_COMMITTEE_QUORUM = 3; + uint256 internal immutable DEFAULT_MIN_WITHDRAWALS_BATCH_SIZE = 4; + uint256 internal immutable DEFAULT_MIN_TIEBREAKER_ACTIVATION_TIMEOUT = 90 days; + uint256 internal immutable DEFAULT_TIEBREAKER_ACTIVATION_TIMEOUT = 365 days; + uint256 internal immutable DEFAULT_MAX_TIEBREAKER_ACTIVATION_TIMEOUT = 730 days; + uint256 internal immutable DEFAULT_MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = 255; + + uint256 internal immutable DEFAULT_FIRST_SEAL_RAGE_QUIT_SUPPORT = 3_00; // 3% + uint256 internal immutable DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT = 15_00; // 15% + uint256 internal immutable DEFAULT_MIN_ASSETS_LOCK_DURATION = 5 hours; + uint256 internal immutable DEFAULT_DYNAMIC_TIMELOCK_MIN_DURATION = 3 days; + uint256 internal immutable DEFAULT_DYNAMIC_TIMELOCK_MAX_DURATION = 30 days; + uint256 internal immutable DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION = 5 hours; + uint256 internal immutable DEFAULT_VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = 5 days; + uint256 internal immutable DEFAULT_VETO_COOLDOWN_DURATION = 4 days; + uint256 internal immutable DEFAULT_RAGE_QUIT_EXTENSION_DELAY = 7 days; + uint256 internal immutable DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK = 60 days; + uint256 internal immutable DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER = 2; + uint256[] internal DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS = new uint256[](3); + + constructor() { + // TODO: are these values correct as a default? + DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[0] = 0; + DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[1] = 0; + DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[2] = 0; + } + + function loadAndValidate() external returns (ConfigValues memory config) { + config = ConfigValues({ + AFTER_SUBMIT_DELAY: Durations.from(vm.envOr("AFTER_SUBMIT_DELAY", DEFAULT_AFTER_SUBMIT_DELAY)), + MAX_AFTER_SUBMIT_DELAY: Durations.from(vm.envOr("MAX_AFTER_SUBMIT_DELAY", DEFAULT_MAX_AFTER_SUBMIT_DELAY)), + AFTER_SCHEDULE_DELAY: Durations.from(vm.envOr("AFTER_SCHEDULE_DELAY", DEFAULT_AFTER_SCHEDULE_DELAY)), + MAX_AFTER_SCHEDULE_DELAY: Durations.from(vm.envOr("MAX_AFTER_SCHEDULE_DELAY", DEFAULT_MAX_AFTER_SCHEDULE_DELAY)), + EMERGENCY_MODE_DURATION: Durations.from(vm.envOr("EMERGENCY_MODE_DURATION", DEFAULT_EMERGENCY_MODE_DURATION)), + MAX_EMERGENCY_MODE_DURATION: Durations.from( + vm.envOr("MAX_EMERGENCY_MODE_DURATION", DEFAULT_MAX_EMERGENCY_MODE_DURATION) + ), + EMERGENCY_PROTECTION_DURATION: Durations.from( + vm.envOr("EMERGENCY_PROTECTION_DURATION", DEFAULT_EMERGENCY_PROTECTION_DURATION) + ), + MAX_EMERGENCY_PROTECTION_DURATION: Durations.from( + vm.envOr("MAX_EMERGENCY_PROTECTION_DURATION", DEFAULT_MAX_EMERGENCY_PROTECTION_DURATION) + ), + EMERGENCY_ACTIVATION_COMMITTEE_QUORUM: vm.envOr( + "EMERGENCY_ACTIVATION_COMMITTEE_QUORUM", DEFAULT_EMERGENCY_ACTIVATION_COMMITTEE_QUORUM + ), + EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS: vm.envAddress("EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS", ARRAY_SEPARATOR), + EMERGENCY_EXECUTION_COMMITTEE_QUORUM: vm.envOr( + "EMERGENCY_EXECUTION_COMMITTEE_QUORUM", DEFAULT_EMERGENCY_EXECUTION_COMMITTEE_QUORUM + ), + EMERGENCY_EXECUTION_COMMITTEE_MEMBERS: vm.envAddress("EMERGENCY_EXECUTION_COMMITTEE_MEMBERS", ARRAY_SEPARATOR), + // TODO: Do we need to configure this? + TIEBREAKER_CORE_QUORUM: DEFAULT_TIEBREAKER_CORE_QUORUM, + TIEBREAKER_EXECUTION_DELAY: Durations.from( + vm.envOr("TIEBREAKER_EXECUTION_DELAY", DEFAULT_TIEBREAKER_EXECUTION_DELAY) + ), + // TODO: Do we need to configure this? + TIEBREAKER_SUB_COMMITTEES_COUNT: DEFAULT_TIEBREAKER_SUB_COMMITTEES_COUNT, + TIEBREAKER_SUB_COMMITTEE_1_MEMBERS: vm.envAddress("TIEBREAKER_SUB_COMMITTEE_1_MEMBERS", ARRAY_SEPARATOR), + TIEBREAKER_SUB_COMMITTEE_1_QUORUM: vm.envOr( + "TIEBREAKER_SUB_COMMITTEE_1_QUORUM", DEFAULT_TIEBREAKER_SUB_COMMITTEE_1_QUORUM + ), + TIEBREAKER_SUB_COMMITTEE_2_MEMBERS: vm.envAddress("TIEBREAKER_SUB_COMMITTEE_2_MEMBERS", ARRAY_SEPARATOR), + TIEBREAKER_SUB_COMMITTEE_2_QUORUM: vm.envOr( + "TIEBREAKER_SUB_COMMITTEE_2_QUORUM", DEFAULT_TIEBREAKER_SUB_COMMITTEE_2_QUORUM + ), + RESEAL_COMMITTEE_MEMBERS: vm.envAddress("RESEAL_COMMITTEE_MEMBERS", ARRAY_SEPARATOR), + RESEAL_COMMITTEE_QUORUM: vm.envOr("RESEAL_COMMITTEE_QUORUM", DEFAULT_RESEAL_COMMITTEE_QUORUM), + MIN_WITHDRAWALS_BATCH_SIZE: vm.envOr("MIN_WITHDRAWALS_BATCH_SIZE", DEFAULT_MIN_WITHDRAWALS_BATCH_SIZE), + MIN_TIEBREAKER_ACTIVATION_TIMEOUT: Durations.from( + vm.envOr("MIN_TIEBREAKER_ACTIVATION_TIMEOUT", DEFAULT_MIN_TIEBREAKER_ACTIVATION_TIMEOUT) + ), + TIEBREAKER_ACTIVATION_TIMEOUT: Durations.from( + vm.envOr("TIEBREAKER_ACTIVATION_TIMEOUT", DEFAULT_TIEBREAKER_ACTIVATION_TIMEOUT) + ), + MAX_TIEBREAKER_ACTIVATION_TIMEOUT: Durations.from( + vm.envOr("MAX_TIEBREAKER_ACTIVATION_TIMEOUT", DEFAULT_MAX_TIEBREAKER_ACTIVATION_TIMEOUT) + ), + MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT: vm.envOr( + "MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT", DEFAULT_MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT + ), + FIRST_SEAL_RAGE_QUIT_SUPPORT: PercentsD16.fromBasisPoints( + vm.envOr("FIRST_SEAL_RAGE_QUIT_SUPPORT", DEFAULT_FIRST_SEAL_RAGE_QUIT_SUPPORT) + ), + SECOND_SEAL_RAGE_QUIT_SUPPORT: PercentsD16.fromBasisPoints( + vm.envOr("SECOND_SEAL_RAGE_QUIT_SUPPORT", DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT) + ), + MIN_ASSETS_LOCK_DURATION: Durations.from(vm.envOr("MIN_ASSETS_LOCK_DURATION", DEFAULT_MIN_ASSETS_LOCK_DURATION)), + DYNAMIC_TIMELOCK_MIN_DURATION: Durations.from( + vm.envOr("DYNAMIC_TIMELOCK_MIN_DURATION", DEFAULT_DYNAMIC_TIMELOCK_MIN_DURATION) + ), + DYNAMIC_TIMELOCK_MAX_DURATION: Durations.from( + vm.envOr("DYNAMIC_TIMELOCK_MAX_DURATION", DEFAULT_DYNAMIC_TIMELOCK_MAX_DURATION) + ), + VETO_SIGNALLING_MIN_ACTIVE_DURATION: Durations.from( + vm.envOr("VETO_SIGNALLING_MIN_ACTIVE_DURATION", DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION) + ), + VETO_SIGNALLING_DEACTIVATION_MAX_DURATION: Durations.from( + vm.envOr("VETO_SIGNALLING_DEACTIVATION_MAX_DURATION", DEFAULT_VETO_SIGNALLING_DEACTIVATION_MAX_DURATION) + ), + VETO_COOLDOWN_DURATION: Durations.from(vm.envOr("VETO_COOLDOWN_DURATION", DEFAULT_VETO_COOLDOWN_DURATION)), + RAGE_QUIT_EXTENSION_DELAY: Durations.from( + vm.envOr("RAGE_QUIT_EXTENSION_DELAY", DEFAULT_RAGE_QUIT_EXTENSION_DELAY) + ), + RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK: Durations.from( + vm.envOr("RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK", DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK) + ), + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER: vm.envOr( + "RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER", + DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER + ), + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS: getValidCoeffs() + }); + + validateConfig(config); + printCommittees(config); + } + + function getValidCoeffs() internal returns (uint256[3] memory coeffs) { + uint256[] memory coeffsRaw = vm.envOr( + "RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS", + ARRAY_SEPARATOR, + DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS + ); + + if (coeffsRaw.length != 3) { + revert InvalidRageQuitETHWithdrawalsTimelockGrowthCoeffs(coeffsRaw); + } + + // TODO: validate each coeff value? + coeffs[0] = coeffsRaw[0]; + coeffs[1] = coeffsRaw[1]; + coeffs[2] = coeffsRaw[2]; + } + + function validateConfig(ConfigValues memory config) internal pure { + if ( + config.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM == 0 + || config.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM > config.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS.length + ) { + revert InvalidQuorum("EMERGENCY_ACTIVATION_COMMITTEE", config.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM); + } + + if ( + config.EMERGENCY_EXECUTION_COMMITTEE_QUORUM == 0 + || config.EMERGENCY_EXECUTION_COMMITTEE_QUORUM > config.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS.length + ) { + revert InvalidQuorum("EMERGENCY_EXECUTION_COMMITTEE", config.EMERGENCY_EXECUTION_COMMITTEE_QUORUM); + } + + if ( + config.TIEBREAKER_SUB_COMMITTEE_1_QUORUM == 0 + || config.TIEBREAKER_SUB_COMMITTEE_1_QUORUM > config.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS.length + ) { + revert InvalidQuorum("TIEBREAKER_SUB_COMMITTEE_1", config.TIEBREAKER_SUB_COMMITTEE_1_QUORUM); + } + + if ( + config.TIEBREAKER_SUB_COMMITTEE_2_QUORUM == 0 + || config.TIEBREAKER_SUB_COMMITTEE_2_QUORUM > config.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS.length + ) { + revert InvalidQuorum("TIEBREAKER_SUB_COMMITTEE_2", config.TIEBREAKER_SUB_COMMITTEE_2_QUORUM); + } + + if ( + config.RESEAL_COMMITTEE_QUORUM == 0 + || config.RESEAL_COMMITTEE_QUORUM > config.RESEAL_COMMITTEE_MEMBERS.length + ) { + revert InvalidQuorum("RESEAL_COMMITTEE", config.RESEAL_COMMITTEE_QUORUM); + } + } + + function printCommittees(ConfigValues memory config) internal view { + console.log("================================================="); + console.log("Loaded valid config with the following committees:"); + + console.log( + "EmergencyActivationCommittee members, quorum", + config.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, + "of", + config.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS.length + ); + for (uint256 k = 0; k < config.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS.length; ++k) { + console.log(">> #", k, address(config.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS[k])); + } + + console.log( + "EmergencyExecutionCommittee members, quorum", + config.EMERGENCY_EXECUTION_COMMITTEE_QUORUM, + "of", + config.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS.length + ); + for (uint256 k = 0; k < config.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS.length; ++k) { + console.log(">> #", k, address(config.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS[k])); + } + + console.log( + "TiebreakerSubCommittee #1 members, quorum", + config.TIEBREAKER_SUB_COMMITTEE_1_QUORUM, + "of", + config.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS.length + ); + for (uint256 k = 0; k < config.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS.length; ++k) { + console.log(">> #", k, address(config.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS[k])); + } + + console.log( + "TiebreakerSubCommittee #2 members, quorum", + config.TIEBREAKER_SUB_COMMITTEE_2_QUORUM, + "of", + config.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS.length + ); + for (uint256 k = 0; k < config.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS.length; ++k) { + console.log(">> #", k, address(config.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS[k])); + } + + console.log( + "ResealCommittee members, quorum", + config.RESEAL_COMMITTEE_QUORUM, + "of", + config.RESEAL_COMMITTEE_MEMBERS.length + ); + for (uint256 k = 0; k < config.RESEAL_COMMITTEE_MEMBERS.length; ++k) { + console.log(">> #", k, address(config.RESEAL_COMMITTEE_MEMBERS[k])); + } + console.log("================================================="); + } +} diff --git a/scripts/deploy/Deploy.s.sol b/scripts/deploy/Deploy.s.sol new file mode 100644 index 00000000..b94208d2 --- /dev/null +++ b/scripts/deploy/Deploy.s.sol @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/* solhint-disable no-console */ + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; + +// --- +// Contracts +// --- +import {Timestamps} from "contracts/types/Timestamp.sol"; + +import {Executor} from "contracts/Executor.sol"; +import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; + +import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; +import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; + +import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; + +import {ResealManager} from "contracts/ResealManager.sol"; +import {DualGovernance} from "contracts/DualGovernance.sol"; +import { + DualGovernanceConfig, + IDualGovernanceConfigProvider, + ImmutableDualGovernanceConfigProvider +} from "contracts/DualGovernanceConfigProvider.sol"; + +import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; +import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; +import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; + +import {LidoUtils} from "test/utils/lido-utils.sol"; // TODO: del! +import {DGDeployConfig, ConfigValues} from "./Config.s.sol"; + +contract DeployDG is Script { + LidoUtils.Context internal _lido = LidoUtils.mainnet(); // TODO: del! + ConfigValues private dgDeployConfig; + + // Emergency Protected Timelock Contracts + // --- + Executor internal adminExecutor; + EmergencyProtectedTimelock internal timelock; + TimelockedGovernance internal emergencyGovernance; + EmergencyActivationCommittee internal emergencyActivationCommittee; + EmergencyExecutionCommittee internal emergencyExecutionCommittee; + + // --- + // Dual Governance Contracts + // --- + ResealManager internal resealManager; + DualGovernance internal dualGovernance; + ImmutableDualGovernanceConfigProvider internal dualGovernanceConfigProvider; + + ResealCommittee internal resealCommittee; + TiebreakerCore internal tiebreakerCoreCommittee; + TiebreakerSubCommittee[] internal tiebreakerSubCommittees; + + function run() external { + DGDeployConfig config = new DGDeployConfig(); + dgDeployConfig = config.loadAndValidate(); + + deployDualGovernanceSetup(); + + console.log("DG deployed successfully"); + console.log("DualGovernance address", address(dualGovernance)); + console.log("ResealManager address", address(resealManager)); + console.log("TiebreakerCoreCommittee address", address(tiebreakerCoreCommittee)); + for (uint256 i = 0; i < tiebreakerSubCommittees.length; ++i) { + console.log("TiebreakerSubCommittee #", i, "address", address(tiebreakerSubCommittees[i])); + } + console.log("AdminExecutor address", address(adminExecutor)); + console.log("EmergencyProtectedTimelock address", address(timelock)); + console.log("EmergencyGovernance address", address(emergencyGovernance)); + console.log("EmergencyActivationCommittee address", address(emergencyActivationCommittee)); + console.log("EmergencyExecutionCommittee address", address(emergencyExecutionCommittee)); + console.log("ResealCommittee address", address(resealCommittee)); + } + + function deployDualGovernanceSetup() internal { + deployEmergencyProtectedTimelockContracts(); + resealManager = deployResealManager(); + dualGovernanceConfigProvider = deployDualGovernanceConfigProvider(); + dualGovernance = deployDualGovernance({configProvider: dualGovernanceConfigProvider}); + + tiebreakerCoreCommittee = deployEmptyTiebreakerCoreCommittee({ + owner: address(this), // temporary set owner to deployer, to add sub committees manually TODO: check + timelockSeconds: dgDeployConfig.TIEBREAKER_EXECUTION_DELAY.toSeconds() + }); + + deployTiebreakerSubCommittees(); + + tiebreakerCoreCommittee.transferOwnership(address(adminExecutor)); + + resealCommittee = deployResealCommittee(); + + // --- + // Finalize Setup + // --- + adminExecutor.execute( + address(dualGovernance), + 0, + abi.encodeCall(dualGovernance.registerProposer, (address(_lido.voting), address(adminExecutor))) // TODO: check + ); + adminExecutor.execute( + address(dualGovernance), + 0, + abi.encodeCall(dualGovernance.setTiebreakerActivationTimeout, dgDeployConfig.TIEBREAKER_ACTIVATION_TIMEOUT) + ); + adminExecutor.execute( + address(dualGovernance), + 0, + abi.encodeCall(dualGovernance.setTiebreakerCommittee, address(tiebreakerCoreCommittee)) + ); + adminExecutor.execute( + address(dualGovernance), + 0, + abi.encodeCall(dualGovernance.addTiebreakerSealableWithdrawalBlocker, address(_lido.withdrawalQueue)) // TODO: check + ); + adminExecutor.execute( + address(dualGovernance), 0, abi.encodeCall(dualGovernance.setResealCommittee, address(resealCommittee)) + ); + + finalizeEmergencyProtectedTimelockDeploy(); + + // --- + // TODO: Grant Reseal Manager Roles + // --- + /* vm.startPrank(address(_lido.agent)); + _lido.withdrawalQueue.grantRole( + 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(resealManager) + ); + _lido.withdrawalQueue.grantRole( + 0x2fc10cc8ae19568712f7a176fb4978616a610650813c9d05326c34abb62749c7, address(resealManager) + ); + vm.stopPrank(); */ + } + + function deployEmergencyProtectedTimelockContracts() internal { + adminExecutor = deployExecutor(address(this)); + timelock = deployEmergencyProtectedTimelock(); + + emergencyActivationCommittee = deployEmergencyActivationCommittee({ + quorum: dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, + members: dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS, + owner: address(adminExecutor) // TODO: check + }); + + emergencyExecutionCommittee = deployEmergencyExecutionCommittee({ + quorum: dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_QUORUM, + members: dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS, + owner: address(adminExecutor) // TODO: check + }); + emergencyGovernance = deployTimelockedGovernance({governance: address(_lido.voting)}); + + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall(timelock.setEmergencyProtectionActivationCommittee, (address(emergencyActivationCommittee))) + ); + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall(timelock.setEmergencyProtectionExecutionCommittee, (address(emergencyExecutionCommittee))) + ); + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall( + timelock.setEmergencyProtectionEndDate, + (dgDeployConfig.EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now())) + ) + ); + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall(timelock.setEmergencyModeDuration, (dgDeployConfig.EMERGENCY_MODE_DURATION)) + ); + + adminExecutor.execute( + address(timelock), 0, abi.encodeCall(timelock.setEmergencyGovernance, (address(emergencyGovernance))) + ); + + // TODO: timelock. transferExecutorOwnership ??? + } + + function deployExecutor(address owner) internal returns (Executor) { + return new Executor(owner); + } + + function deployEmergencyProtectedTimelock() internal returns (EmergencyProtectedTimelock) { + return new EmergencyProtectedTimelock({ + adminExecutor: address(adminExecutor), + sanityCheckParams: EmergencyProtectedTimelock.SanityCheckParams({ + maxAfterSubmitDelay: dgDeployConfig.MAX_AFTER_SUBMIT_DELAY, + maxAfterScheduleDelay: dgDeployConfig.MAX_AFTER_SCHEDULE_DELAY, + maxEmergencyModeDuration: dgDeployConfig.MAX_EMERGENCY_MODE_DURATION, + maxEmergencyProtectionDuration: dgDeployConfig.MAX_EMERGENCY_PROTECTION_DURATION + }) + }); + } + + function deployEmergencyActivationCommittee( + address owner, + uint256 quorum, + address[] memory members + ) internal returns (EmergencyActivationCommittee) { + return new EmergencyActivationCommittee(owner, members, quorum, address(timelock)); + } + + function deployEmergencyExecutionCommittee( + address owner, + uint256 quorum, + address[] memory members + ) internal returns (EmergencyExecutionCommittee) { + return new EmergencyExecutionCommittee(owner, members, quorum, address(timelock)); + } + + function deployTimelockedGovernance(address governance) internal returns (TimelockedGovernance) { + return new TimelockedGovernance(governance, timelock); + } + + function deployResealManager() internal returns (ResealManager) { + return new ResealManager(timelock); + } + + function deployDualGovernanceConfigProvider() internal returns (ImmutableDualGovernanceConfigProvider) { + return new ImmutableDualGovernanceConfigProvider( + DualGovernanceConfig.Context({ + firstSealRageQuitSupport: dgDeployConfig.FIRST_SEAL_RAGE_QUIT_SUPPORT, + secondSealRageQuitSupport: dgDeployConfig.SECOND_SEAL_RAGE_QUIT_SUPPORT, + // + minAssetsLockDuration: dgDeployConfig.MIN_ASSETS_LOCK_DURATION, + dynamicTimelockMinDuration: dgDeployConfig.DYNAMIC_TIMELOCK_MIN_DURATION, + dynamicTimelockMaxDuration: dgDeployConfig.DYNAMIC_TIMELOCK_MAX_DURATION, + // + vetoSignallingMinActiveDuration: dgDeployConfig.VETO_SIGNALLING_MIN_ACTIVE_DURATION, + vetoSignallingDeactivationMaxDuration: dgDeployConfig.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION, + vetoCooldownDuration: dgDeployConfig.VETO_COOLDOWN_DURATION, + // + rageQuitExtensionDelay: dgDeployConfig.RAGE_QUIT_EXTENSION_DELAY, + rageQuitEthWithdrawalsMinTimelock: dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK, + rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: dgDeployConfig + .RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER, + rageQuitEthWithdrawalsTimelockGrowthCoeffs: dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS + }) + ); + } + + function deployDualGovernance(IDualGovernanceConfigProvider configProvider) internal returns (DualGovernance) { + return new DualGovernance({ + dependencies: DualGovernance.ExternalDependencies({ + stETH: _lido.stETH, // TODO: mainnet addr? + wstETH: _lido.wstETH, + withdrawalQueue: _lido.withdrawalQueue, + timelock: timelock, + resealManager: resealManager, + configProvider: configProvider + }), + sanityCheckParams: DualGovernance.SanityCheckParams({ + minWithdrawalsBatchSize: dgDeployConfig.MIN_WITHDRAWALS_BATCH_SIZE, + minTiebreakerActivationTimeout: dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT, + maxTiebreakerActivationTimeout: dgDeployConfig.MAX_TIEBREAKER_ACTIVATION_TIMEOUT, + maxSealableWithdrawalBlockersCount: dgDeployConfig.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT + }) + }); + } + + function deployEmptyTiebreakerCoreCommittee( + address owner, + uint256 timelockSeconds + ) internal returns (TiebreakerCore) { + return new TiebreakerCore({owner: owner, dualGovernance: address(dualGovernance), timelock: timelockSeconds}); + } + + function deployTiebreakerSubCommittees() internal { + address[] memory coreCommitteeMembers = new address[](dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT); + + for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { + address[] memory members; + uint256 quorum; + + if (i == 0) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_QUORUM; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS; + } else { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_QUORUM; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS; + } + + tiebreakerSubCommittees.push( + deployTiebreakerSubCommittee({owner: address(adminExecutor), quorum: quorum, members: members}) + ); + coreCommitteeMembers[i] = address(tiebreakerSubCommittees[i]); + } + + tiebreakerCoreCommittee.addMembers(coreCommitteeMembers, coreCommitteeMembers.length); + } + + function deployTiebreakerSubCommittee( + address owner, + uint256 quorum, + address[] memory members + ) internal returns (TiebreakerSubCommittee) { + return new TiebreakerSubCommittee({ + owner: owner, + executionQuorum: quorum, + committeeMembers: members, + tiebreakerCore: address(tiebreakerCoreCommittee) + }); + } + + function deployResealCommittee() internal returns (ResealCommittee) { + uint256 quorum = dgDeployConfig.RESEAL_COMMITTEE_QUORUM; + address[] memory committeeMembers = dgDeployConfig.RESEAL_COMMITTEE_MEMBERS; + + // TODO: Do we need to use timelock here? + return new ResealCommittee(address(adminExecutor), committeeMembers, quorum, address(dualGovernance), 0); + } + + function finalizeEmergencyProtectedTimelockDeploy() internal { + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall( + timelock.setupDelays, (dgDeployConfig.AFTER_SUBMIT_DELAY, dgDeployConfig.AFTER_SCHEDULE_DELAY) + ) + ); + adminExecutor.execute(address(timelock), 0, abi.encodeCall(timelock.setGovernance, (address(dualGovernance)))); + adminExecutor.transferOwnership(address(timelock)); + } +} From 8b486bbda66c05d14766dda01eaaecd159217c41 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:37:51 +0400 Subject: [PATCH 007/107] Move Lido addresses file --- {test/utils => addresses}/mainnet-addresses.sol | 0 package.json | 2 +- test/scenario/emergency-committee.t.sol | 2 +- test/scenario/reseal-committee.t.sol | 2 +- test/utils/lido-utils.sol | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename {test/utils => addresses}/mainnet-addresses.sol (100%) diff --git a/test/utils/mainnet-addresses.sol b/addresses/mainnet-addresses.sol similarity index 100% rename from test/utils/mainnet-addresses.sol rename to addresses/mainnet-addresses.sol diff --git a/package.json b/package.json index 00359545..4bc6a503 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "scripts": { "test": "forge test", "prepare": "husky", - "lint": "solhint \"contracts/**/*.sol\" \"scripts/**/*.sol\" \"test/**/*.sol\" --ignore-path .solhintignore", + "lint": "solhint \"addresses/**/*.sol\" \"contracts/**/*.sol\" \"scripts/**/*.sol\" \"test/**/*.sol\" --ignore-path .solhintignore", "coverage": "forge coverage", "precov-report": "mkdir -p ./coverage-report && forge coverage --report lcov --report-file ./coverage-report/lcov.info", "cov-report": "genhtml ./coverage-report/lcov.info --rc derive_function_end_line=0 --rc branch_coverage=1 -o coverage-report --exclude test --ignore-errors inconsistent --ignore-errors category" diff --git a/test/scenario/emergency-committee.t.sol b/test/scenario/emergency-committee.t.sol index 7a4ff016..32eed792 100644 --- a/test/scenario/emergency-committee.t.sol +++ b/test/scenario/emergency-committee.t.sol @@ -6,7 +6,7 @@ import {PercentsD16} from "contracts/types/PercentD16.sol"; import {IPotentiallyDangerousContract} from "../utils/interfaces/IPotentiallyDangerousContract.sol"; import {ScenarioTestBlueprint, ExternalCall, ExternalCallHelpers} from "../utils/scenario-test-blueprint.sol"; -import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; +import {DAO_AGENT} from "addresses/mainnet-addresses.sol"; contract EmergencyCommitteeTest is ScenarioTestBlueprint { address internal immutable _VETOER = makeAddr("VETOER"); diff --git a/test/scenario/reseal-committee.t.sol b/test/scenario/reseal-committee.t.sol index 30d419cd..619c2042 100644 --- a/test/scenario/reseal-committee.t.sol +++ b/test/scenario/reseal-committee.t.sol @@ -7,7 +7,7 @@ import {PercentsD16} from "contracts/types/PercentD16.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; import {ScenarioTestBlueprint, ExternalCall} from "../utils/scenario-test-blueprint.sol"; -import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; +import {DAO_AGENT} from "addresses/mainnet-addresses.sol"; contract ResealCommitteeTest is ScenarioTestBlueprint { address internal immutable _VETOER = makeAddr("VETOER"); diff --git a/test/utils/lido-utils.sol b/test/utils/lido-utils.sol index 38ac97c0..9f3f42c0 100644 --- a/test/utils/lido-utils.sol +++ b/test/utils/lido-utils.sol @@ -27,7 +27,7 @@ import { DAO_AGENT, DAO_VOTING, DAO_TOKEN_MANAGER -} from "./mainnet-addresses.sol"; +} from "addresses/mainnet-addresses.sol"; uint256 constant ST_ETH_TRANSFERS_SHARE_LOSS_COMPENSATION = 8; // TODO: evaluate min enough value From 14a90803b45818c998bbe57e15789a6e1a07f948 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 23 Aug 2024 19:24:52 +0400 Subject: [PATCH 008/107] Use deployer address --- .env.example | 8 ++++++++ .gitignore | 2 +- scripts/deploy/Config.s.sol | 2 ++ scripts/deploy/Deploy.s.sol | 15 +++++++++++---- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index baa5a015..73269529 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,9 @@ MAINNET_RPC_URL= + +# Deploy script env vars +DEPLOYER_PRIVATE_KEY=... +EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS=addr1,addr2,addr3 +EMERGENCY_EXECUTION_COMMITTEE_MEMBERS=addr1,addr2,addr3 +TIEBREAKER_SUB_COMMITTEE_1_MEMBERS=addr1,addr2,addr3 +TIEBREAKER_SUB_COMMITTEE_2_MEMBERS=addr1,addr2,addr3 +RESEAL_COMMITTEE_MEMBERS=addr1,addr2,addr3 diff --git a/.gitignore b/.gitignore index 8c09d4d1..37222e47 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ cache_forge/ out/ # Foundry: development broadcast logs -!/broadcast +/broadcast /broadcast/*/31337/ /broadcast/**/dry-run/ .vscode/ diff --git a/scripts/deploy/Config.s.sol b/scripts/deploy/Config.s.sol index 44d75a72..46ec6bb2 100644 --- a/scripts/deploy/Config.s.sol +++ b/scripts/deploy/Config.s.sol @@ -11,6 +11,7 @@ import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; string constant ARRAY_SEPARATOR = ","; struct ConfigValues { + uint256 DEPLOYER_PRIVATE_KEY; Duration AFTER_SUBMIT_DELAY; Duration MAX_AFTER_SUBMIT_DELAY; Duration AFTER_SCHEDULE_DELAY; @@ -99,6 +100,7 @@ contract DGDeployConfig is Script { function loadAndValidate() external returns (ConfigValues memory config) { config = ConfigValues({ + DEPLOYER_PRIVATE_KEY: vm.envUint("DEPLOYER_PRIVATE_KEY"), AFTER_SUBMIT_DELAY: Durations.from(vm.envOr("AFTER_SUBMIT_DELAY", DEFAULT_AFTER_SUBMIT_DELAY)), MAX_AFTER_SUBMIT_DELAY: Durations.from(vm.envOr("MAX_AFTER_SUBMIT_DELAY", DEFAULT_MAX_AFTER_SUBMIT_DELAY)), AFTER_SCHEDULE_DELAY: Durations.from(vm.envOr("AFTER_SCHEDULE_DELAY", DEFAULT_AFTER_SCHEDULE_DELAY)), diff --git a/scripts/deploy/Deploy.s.sol b/scripts/deploy/Deploy.s.sol index b94208d2..3268fccc 100644 --- a/scripts/deploy/Deploy.s.sol +++ b/scripts/deploy/Deploy.s.sol @@ -57,12 +57,19 @@ contract DeployDG is Script { TiebreakerCore internal tiebreakerCoreCommittee; TiebreakerSubCommittee[] internal tiebreakerSubCommittees; + address internal deployer; + function run() external { - DGDeployConfig config = new DGDeployConfig(); - dgDeployConfig = config.loadAndValidate(); + DGDeployConfig configProvider = new DGDeployConfig(); + dgDeployConfig = configProvider.loadAndValidate(); + + deployer = vm.addr(dgDeployConfig.DEPLOYER_PRIVATE_KEY); + vm.startBroadcast(dgDeployConfig.DEPLOYER_PRIVATE_KEY); deployDualGovernanceSetup(); + vm.stopBroadcast(); + console.log("DG deployed successfully"); console.log("DualGovernance address", address(dualGovernance)); console.log("ResealManager address", address(resealManager)); @@ -85,7 +92,7 @@ contract DeployDG is Script { dualGovernance = deployDualGovernance({configProvider: dualGovernanceConfigProvider}); tiebreakerCoreCommittee = deployEmptyTiebreakerCoreCommittee({ - owner: address(this), // temporary set owner to deployer, to add sub committees manually TODO: check + owner: deployer, // temporary set owner to deployer, to add sub committees manually TODO: check timelockSeconds: dgDeployConfig.TIEBREAKER_EXECUTION_DELAY.toSeconds() }); @@ -138,7 +145,7 @@ contract DeployDG is Script { } function deployEmergencyProtectedTimelockContracts() internal { - adminExecutor = deployExecutor(address(this)); + adminExecutor = deployExecutor(deployer); timelock = deployEmergencyProtectedTimelock(); emergencyActivationCommittee = deployEmergencyActivationCommittee({ From 427d7442d84d3dad0e6fcf651a238ea956a7b0ee Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 23 Aug 2024 22:04:23 +0400 Subject: [PATCH 009/107] Implement switching between networks --- .env.example | 1 + addresses/holesky-addresses.sol | 7 +++++ scripts/deploy/Config.s.sol | 51 +++++++++++++++++++++++++++++++++ scripts/deploy/Deploy.s.sol | 30 ++++++++++--------- 4 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 addresses/holesky-addresses.sol diff --git a/.env.example b/.env.example index 73269529..193bdbc7 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ MAINNET_RPC_URL= # Deploy script env vars +CHAIN=<"mainnet" OR "holesky"> DEPLOYER_PRIVATE_KEY=... EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS=addr1,addr2,addr3 EMERGENCY_EXECUTION_COMMITTEE_MEMBERS=addr1,addr2,addr3 diff --git a/addresses/holesky-addresses.sol b/addresses/holesky-addresses.sol new file mode 100644 index 00000000..6f052252 --- /dev/null +++ b/addresses/holesky-addresses.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +address constant DAO_VOTING = 0xdA7d2573Df555002503F29aA4003e398d28cc00f; +address constant ST_ETH = 0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034; +address constant WST_ETH = 0x8d09a4502Cc8Cf1547aD300E066060D043f6982D; +address constant WITHDRAWAL_QUEUE = 0xc7cc160b58F8Bb0baC94b80847E2CF2800565C50; diff --git a/scripts/deploy/Config.s.sol b/scripts/deploy/Config.s.sol index 46ec6bb2..6e12ab5b 100644 --- a/scripts/deploy/Config.s.sol +++ b/scripts/deploy/Config.s.sol @@ -5,12 +5,31 @@ pragma solidity 0.8.26; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/console.sol"; +import {IStETH} from "contracts/interfaces/IStETH.sol"; +import {IWstETH} from "contracts/interfaces/IWstETH.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {IAragonVoting} from "test/utils/interfaces/IAragonVoting.sol"; // TODO: move to a proper location +import { + ST_ETH as MAINNET_ST_ETH, + WST_ETH as MAINNET_WST_ETH, + WITHDRAWAL_QUEUE as MAINNET_WITHDRAWAL_QUEUE, + DAO_VOTING as MAINNET_DAO_VOTING +} from "addresses/mainnet-addresses.sol"; +import { + ST_ETH as HOLESKY_ST_ETH, + WST_ETH as HOLESKY_WST_ETH, + WITHDRAWAL_QUEUE as HOLESKY_WITHDRAWAL_QUEUE, + DAO_VOTING as HOLESKY_DAO_VOTING +} from "addresses/holesky-addresses.sol"; import {Durations, Duration} from "contracts/types/Duration.sol"; import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; string constant ARRAY_SEPARATOR = ","; +bytes32 constant CHAIN_NAME_MAINNET_HASH = keccak256(bytes("mainnet")); +bytes32 constant CHAIN_NAME_HOLESKY_HASH = keccak256(bytes("holesky")); struct ConfigValues { + string CHAIN; uint256 DEPLOYER_PRIVATE_KEY; Duration AFTER_SUBMIT_DELAY; Duration MAX_AFTER_SUBMIT_DELAY; @@ -52,9 +71,17 @@ struct ConfigValues { uint256[3] RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS; } +struct LidoAddresses { + IStETH stETH; + IWstETH wstETH; + IWithdrawalQueue withdrawalQueue; + IAragonVoting voting; +} + contract DGDeployConfig is Script { error InvalidRageQuitETHWithdrawalsTimelockGrowthCoeffs(uint256[] coeffs); error InvalidQuorum(string committee, uint256 quorum); + error InvalidChain(string chainName); uint256 internal immutable DEFAULT_AFTER_SUBMIT_DELAY = 3 days; uint256 internal immutable DEFAULT_MAX_AFTER_SUBMIT_DELAY = 45 days; @@ -100,6 +127,7 @@ contract DGDeployConfig is Script { function loadAndValidate() external returns (ConfigValues memory config) { config = ConfigValues({ + CHAIN: vm.envString("CHAIN"), DEPLOYER_PRIVATE_KEY: vm.envUint("DEPLOYER_PRIVATE_KEY"), AFTER_SUBMIT_DELAY: Durations.from(vm.envOr("AFTER_SUBMIT_DELAY", DEFAULT_AFTER_SUBMIT_DELAY)), MAX_AFTER_SUBMIT_DELAY: Durations.from(vm.envOr("MAX_AFTER_SUBMIT_DELAY", DEFAULT_MAX_AFTER_SUBMIT_DELAY)), @@ -208,6 +236,11 @@ contract DGDeployConfig is Script { } function validateConfig(ConfigValues memory config) internal pure { + bytes32 chainNameHash = keccak256(bytes(config.CHAIN)); + if (chainNameHash != CHAIN_NAME_MAINNET_HASH && chainNameHash != CHAIN_NAME_HOLESKY_HASH) { + revert InvalidChain(config.CHAIN); + } + if ( config.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM == 0 || config.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM > config.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS.length @@ -299,4 +332,22 @@ contract DGDeployConfig is Script { } console.log("================================================="); } + + function lidoAddresses(ConfigValues memory config) external pure returns (LidoAddresses memory) { + if (keccak256(bytes(config.CHAIN)) == CHAIN_NAME_MAINNET_HASH) { + return LidoAddresses({ + stETH: IStETH(MAINNET_ST_ETH), + wstETH: IWstETH(MAINNET_WST_ETH), + withdrawalQueue: IWithdrawalQueue(MAINNET_WITHDRAWAL_QUEUE), + voting: IAragonVoting(MAINNET_DAO_VOTING) + }); + } + + return LidoAddresses({ + stETH: IStETH(HOLESKY_ST_ETH), + wstETH: IWstETH(HOLESKY_WST_ETH), + withdrawalQueue: IWithdrawalQueue(HOLESKY_WITHDRAWAL_QUEUE), + voting: IAragonVoting(HOLESKY_DAO_VOTING) + }); + } } diff --git a/scripts/deploy/Deploy.s.sol b/scripts/deploy/Deploy.s.sol index 3268fccc..a74820f0 100644 --- a/scripts/deploy/Deploy.s.sol +++ b/scripts/deploy/Deploy.s.sol @@ -31,11 +31,10 @@ import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; -import {LidoUtils} from "test/utils/lido-utils.sol"; // TODO: del! -import {DGDeployConfig, ConfigValues} from "./Config.s.sol"; +import {DGDeployConfig, ConfigValues, LidoAddresses} from "./Config.s.sol"; contract DeployDG is Script { - LidoUtils.Context internal _lido = LidoUtils.mainnet(); // TODO: del! + LidoAddresses internal lidoAddresses; ConfigValues private dgDeployConfig; // Emergency Protected Timelock Contracts @@ -63,6 +62,9 @@ contract DeployDG is Script { DGDeployConfig configProvider = new DGDeployConfig(); dgDeployConfig = configProvider.loadAndValidate(); + // TODO: check chain id? + + lidoAddresses = configProvider.lidoAddresses(dgDeployConfig); deployer = vm.addr(dgDeployConfig.DEPLOYER_PRIVATE_KEY); vm.startBroadcast(dgDeployConfig.DEPLOYER_PRIVATE_KEY); @@ -92,7 +94,7 @@ contract DeployDG is Script { dualGovernance = deployDualGovernance({configProvider: dualGovernanceConfigProvider}); tiebreakerCoreCommittee = deployEmptyTiebreakerCoreCommittee({ - owner: deployer, // temporary set owner to deployer, to add sub committees manually TODO: check + owner: deployer, // temporary set owner to deployer, to add sub committees manually timelockSeconds: dgDeployConfig.TIEBREAKER_EXECUTION_DELAY.toSeconds() }); @@ -108,7 +110,7 @@ contract DeployDG is Script { adminExecutor.execute( address(dualGovernance), 0, - abi.encodeCall(dualGovernance.registerProposer, (address(_lido.voting), address(adminExecutor))) // TODO: check + abi.encodeCall(dualGovernance.registerProposer, (address(lidoAddresses.voting), address(adminExecutor))) ); adminExecutor.execute( address(dualGovernance), @@ -123,7 +125,9 @@ contract DeployDG is Script { adminExecutor.execute( address(dualGovernance), 0, - abi.encodeCall(dualGovernance.addTiebreakerSealableWithdrawalBlocker, address(_lido.withdrawalQueue)) // TODO: check + abi.encodeCall( + dualGovernance.addTiebreakerSealableWithdrawalBlocker, address(lidoAddresses.withdrawalQueue) + ) ); adminExecutor.execute( address(dualGovernance), 0, abi.encodeCall(dualGovernance.setResealCommittee, address(resealCommittee)) @@ -151,15 +155,15 @@ contract DeployDG is Script { emergencyActivationCommittee = deployEmergencyActivationCommittee({ quorum: dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, members: dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS, - owner: address(adminExecutor) // TODO: check + owner: address(adminExecutor) }); emergencyExecutionCommittee = deployEmergencyExecutionCommittee({ quorum: dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_QUORUM, members: dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS, - owner: address(adminExecutor) // TODO: check + owner: address(adminExecutor) }); - emergencyGovernance = deployTimelockedGovernance({governance: address(_lido.voting)}); + emergencyGovernance = deployTimelockedGovernance({governance: address(lidoAddresses.voting)}); adminExecutor.execute( address(timelock), @@ -188,8 +192,6 @@ contract DeployDG is Script { adminExecutor.execute( address(timelock), 0, abi.encodeCall(timelock.setEmergencyGovernance, (address(emergencyGovernance))) ); - - // TODO: timelock. transferExecutorOwnership ??? } function deployExecutor(address owner) internal returns (Executor) { @@ -258,9 +260,9 @@ contract DeployDG is Script { function deployDualGovernance(IDualGovernanceConfigProvider configProvider) internal returns (DualGovernance) { return new DualGovernance({ dependencies: DualGovernance.ExternalDependencies({ - stETH: _lido.stETH, // TODO: mainnet addr? - wstETH: _lido.wstETH, - withdrawalQueue: _lido.withdrawalQueue, + stETH: lidoAddresses.stETH, + wstETH: lidoAddresses.wstETH, + withdrawalQueue: lidoAddresses.withdrawalQueue, timelock: timelock, resealManager: resealManager, configProvider: configProvider From ca6638b7f9d26c8a0a2b0f1a0b64683bb7bced7f Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:21:03 +0400 Subject: [PATCH 010/107] Merge changes from the develop branch --- scripts/deploy/Deploy.s.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/deploy/Deploy.s.sol b/scripts/deploy/Deploy.s.sol index a74820f0..eb77863c 100644 --- a/scripts/deploy/Deploy.s.sol +++ b/scripts/deploy/Deploy.s.sol @@ -10,6 +10,7 @@ import {console} from "forge-std/console.sol"; // Contracts // --- import {Timestamps} from "contracts/types/Timestamp.sol"; +import {Duration, Durations} from "contracts/types/Duration.sol"; import {Executor} from "contracts/Executor.sol"; import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; @@ -95,7 +96,7 @@ contract DeployDG is Script { tiebreakerCoreCommittee = deployEmptyTiebreakerCoreCommittee({ owner: deployer, // temporary set owner to deployer, to add sub committees manually - timelockSeconds: dgDeployConfig.TIEBREAKER_EXECUTION_DELAY.toSeconds() + _timelock: dgDeployConfig.TIEBREAKER_EXECUTION_DELAY }); deployTiebreakerSubCommittees(); @@ -276,11 +277,8 @@ contract DeployDG is Script { }); } - function deployEmptyTiebreakerCoreCommittee( - address owner, - uint256 timelockSeconds - ) internal returns (TiebreakerCore) { - return new TiebreakerCore({owner: owner, dualGovernance: address(dualGovernance), timelock: timelockSeconds}); + function deployEmptyTiebreakerCoreCommittee(address owner, Duration _timelock) internal returns (TiebreakerCore) { + return new TiebreakerCore({owner: owner, dualGovernance: address(dualGovernance), timelock: _timelock}); } function deployTiebreakerSubCommittees() internal { @@ -325,7 +323,9 @@ contract DeployDG is Script { address[] memory committeeMembers = dgDeployConfig.RESEAL_COMMITTEE_MEMBERS; // TODO: Do we need to use timelock here? - return new ResealCommittee(address(adminExecutor), committeeMembers, quorum, address(dualGovernance), 0); + return new ResealCommittee( + address(adminExecutor), committeeMembers, quorum, address(dualGovernance), Durations.from(0) + ); } function finalizeEmergencyProtectedTimelockDeploy() internal { From b1fe19bf072392f1d706a479a544ae69f66adc4e Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:56:59 +0400 Subject: [PATCH 011/107] Validate deployed contracts as a last step of the script --- scripts/deploy/Deploy.s.sol | 50 ++++++++--- scripts/deploy/DeployValidation.sol | 125 ++++++++++++++++++++++++++++ scripts/deploy/Readme.md | 27 ++++++ 3 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 scripts/deploy/DeployValidation.sol create mode 100644 scripts/deploy/Readme.md diff --git a/scripts/deploy/Deploy.s.sol b/scripts/deploy/Deploy.s.sol index eb77863c..9c578130 100644 --- a/scripts/deploy/Deploy.s.sol +++ b/scripts/deploy/Deploy.s.sol @@ -33,8 +33,11 @@ import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; import {DGDeployConfig, ConfigValues, LidoAddresses} from "./Config.s.sol"; +import {DeployValidation} from "./DeployValidation.sol"; contract DeployDG is Script { + using DeployValidation for DeployValidation.DeployResult; + LidoAddresses internal lidoAddresses; ConfigValues private dgDeployConfig; @@ -73,19 +76,27 @@ contract DeployDG is Script { vm.stopBroadcast(); + DeployValidation.DeployResult memory res = getDeployedAddresses(); + console.log("DG deployed successfully"); - console.log("DualGovernance address", address(dualGovernance)); - console.log("ResealManager address", address(resealManager)); - console.log("TiebreakerCoreCommittee address", address(tiebreakerCoreCommittee)); + console.log("DualGovernance address", res.dualGovernance); + console.log("ResealManager address", res.resealManager); + console.log("TiebreakerCoreCommittee address", res.tiebreakerCoreCommittee); for (uint256 i = 0; i < tiebreakerSubCommittees.length; ++i) { console.log("TiebreakerSubCommittee #", i, "address", address(tiebreakerSubCommittees[i])); } - console.log("AdminExecutor address", address(adminExecutor)); - console.log("EmergencyProtectedTimelock address", address(timelock)); - console.log("EmergencyGovernance address", address(emergencyGovernance)); - console.log("EmergencyActivationCommittee address", address(emergencyActivationCommittee)); - console.log("EmergencyExecutionCommittee address", address(emergencyExecutionCommittee)); - console.log("ResealCommittee address", address(resealCommittee)); + console.log("AdminExecutor address", res.adminExecutor); + console.log("EmergencyProtectedTimelock address", res.timelock); + console.log("EmergencyGovernance address", res.emergencyGovernance); + console.log("EmergencyActivationCommittee address", res.emergencyActivationCommittee); + console.log("EmergencyExecutionCommittee address", res.emergencyExecutionCommittee); + console.log("ResealCommittee address", res.resealCommittee); + + console.log("Verifying deploy"); + + res.check(); + + console.log(unicode"Verified ✅"); } function deployDualGovernanceSetup() internal { @@ -176,6 +187,8 @@ contract DeployDG is Script { 0, abi.encodeCall(timelock.setEmergencyProtectionExecutionCommittee, (address(emergencyExecutionCommittee))) ); + + // TODO: Do we really need to set it? adminExecutor.execute( address(timelock), 0, @@ -322,7 +335,7 @@ contract DeployDG is Script { uint256 quorum = dgDeployConfig.RESEAL_COMMITTEE_QUORUM; address[] memory committeeMembers = dgDeployConfig.RESEAL_COMMITTEE_MEMBERS; - // TODO: Do we need to use timelock here? + // TODO: Don't we need to use timelock here? return new ResealCommittee( address(adminExecutor), committeeMembers, quorum, address(dualGovernance), Durations.from(0) ); @@ -339,4 +352,21 @@ contract DeployDG is Script { adminExecutor.execute(address(timelock), 0, abi.encodeCall(timelock.setGovernance, (address(dualGovernance)))); adminExecutor.transferOwnership(address(timelock)); } + + function getDeployedAddresses() internal view returns (DeployValidation.DeployResult memory) { + return DeployValidation.DeployResult({ + deployer: address(deployer), + adminExecutor: payable(address(adminExecutor)), + timelock: address(timelock), + emergencyGovernance: address(emergencyGovernance), + emergencyActivationCommittee: address(emergencyActivationCommittee), + emergencyExecutionCommittee: address(emergencyExecutionCommittee), + resealManager: address(resealManager), + dualGovernance: address(dualGovernance), + resealCommittee: address(resealCommittee), + tiebreakerCoreCommittee: address(tiebreakerCoreCommittee), + tiebreakerSubCommittee1: address(tiebreakerSubCommittees[0]), + tiebreakerSubCommittee2: address(tiebreakerSubCommittees[1]) + }); + } } diff --git a/scripts/deploy/DeployValidation.sol b/scripts/deploy/DeployValidation.sol new file mode 100644 index 00000000..cf5ec41b --- /dev/null +++ b/scripts/deploy/DeployValidation.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Timestamps} from "contracts/types/Timestamp.sol"; +import {Executor} from "contracts/Executor.sol"; +import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; +import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; +import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; +import {DGDeployConfig, ConfigValues} from "./Config.s.sol"; + +library DeployValidation { + struct DeployResult { + address deployer; // TODO: not used, del? + address payable adminExecutor; + address timelock; + address emergencyGovernance; + address emergencyActivationCommittee; + address emergencyExecutionCommittee; + address resealManager; + address dualGovernance; + address resealCommittee; + address tiebreakerCoreCommittee; + address tiebreakerSubCommittee1; + address tiebreakerSubCommittee2; + } + + function check(DeployResult memory res) internal { + DGDeployConfig configProvider = new DGDeployConfig(); + ConfigValues memory dgDeployConfig = configProvider.loadAndValidate(); + + checkAdminExecutor(res.adminExecutor, res.timelock); + checkTimelock(res, dgDeployConfig); + checkEmergencyActivationCommittee(res.emergencyActivationCommittee, res.adminExecutor); + checkEmergencyExecutionCommittee(res.emergencyExecutionCommittee, res.adminExecutor); + checkTimelockedGovernance(); + checkResealManager(); + // TODO: check dualGovernanceConfigProvider? + checkDualGovernance(); + checkTiebreakerCoreCommittee(); + checkTiebreakerSubCommittee1(); + checkTiebreakerSubCommittee2(); + checkResealCommittee(); + } + + function checkAdminExecutor(address payable executor, address timelock) internal view { + require(Executor(executor).owner() == timelock); + } + + function checkTimelock(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { + EmergencyProtectedTimelock timelockInstance = EmergencyProtectedTimelock(res.timelock); + require(timelockInstance.getAdminExecutor() == res.adminExecutor); + require(timelockInstance.MAX_AFTER_SUBMIT_DELAY() == dgDeployConfig.MAX_AFTER_SUBMIT_DELAY); + require(timelockInstance.MAX_AFTER_SCHEDULE_DELAY() == dgDeployConfig.MAX_AFTER_SCHEDULE_DELAY); + require(timelockInstance.MAX_EMERGENCY_MODE_DURATION() == dgDeployConfig.MAX_EMERGENCY_MODE_DURATION); + require( + timelockInstance.MAX_EMERGENCY_PROTECTION_DURATION() == dgDeployConfig.MAX_EMERGENCY_PROTECTION_DURATION + ); + // committees + require( + timelockInstance.getEmergencyProtectionContext().emergencyActivationCommittee + == res.emergencyActivationCommittee + ); + require( + timelockInstance.getEmergencyProtectionContext().emergencyExecutionCommittee + == res.emergencyExecutionCommittee + ); + require( + timelockInstance.getEmergencyProtectionContext().emergencyProtectionEndsAfter + <= dgDeployConfig.EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()) + ); + require( + timelockInstance.getEmergencyProtectionContext().emergencyModeDuration + == dgDeployConfig.EMERGENCY_MODE_DURATION + ); + require(timelockInstance.getEmergencyProtectionContext().emergencyGovernance == res.emergencyGovernance); + require(timelockInstance.getAfterSubmitDelay() == dgDeployConfig.AFTER_SUBMIT_DELAY); + require(timelockInstance.getGovernance() == res.dualGovernance); + } + + function checkEmergencyActivationCommittee( + address emergencyActivationCommittee, + address adminExecutor + ) internal view { + require(EmergencyActivationCommittee(emergencyActivationCommittee).owner() == adminExecutor); + // TODO: check members? + // TODO: check quorum? + } + + function checkEmergencyExecutionCommittee( + address emergencyExecutionCommittee, + address adminExecutor + ) internal view { + require(EmergencyExecutionCommittee(emergencyExecutionCommittee).owner() == adminExecutor); + // TODO: check members? + // TODO: check quorum? + } + + function checkTimelockedGovernance() internal view { + // TODO: implement + } + + function checkResealManager() internal view { + // TODO: implement + } + + function checkDualGovernance() internal view { + // TODO: implement + } + + function checkTiebreakerCoreCommittee() internal view { + // TODO: implement + } + + function checkTiebreakerSubCommittee1() internal view { + // TODO: implement + } + + function checkTiebreakerSubCommittee2() internal view { + // TODO: implement + } + + function checkResealCommittee() internal view { + // TODO: implement + } +} diff --git a/scripts/deploy/Readme.md b/scripts/deploy/Readme.md new file mode 100644 index 00000000..7fea346b --- /dev/null +++ b/scripts/deploy/Readme.md @@ -0,0 +1,27 @@ +# Dual Governance deploy script + +### Running locally with Anvil + +Start Anvil, provide RPC url (Infura as an example) +``` +anvil --fork-url https://.infura.io/v3/ --block-time 300 +``` + +### Running the script + +1. Set up required env variables in .env file + + ``` + CHAIN=<"mainnet" OR "holesky"> + DEPLOYER_PRIVATE_KEY=... + EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS=addr1,addr2,addr3 + EMERGENCY_EXECUTION_COMMITTEE_MEMBERS=addr1,addr2,addr3 + TIEBREAKER_SUB_COMMITTEE_1_MEMBERS=addr1,addr2,addr3 + TIEBREAKER_SUB_COMMITTEE_2_MEMBERS=addr1,addr2,addr3 + RESEAL_COMMITTEE_MEMBERS=addr1,addr2,addr3 + ``` +2. Run the script (with the local Anvil as an example) + + ``` + forge script scripts/deploy/Deploy.s.sol:DeployDG --fork-url http://localhost:8545 --broadcast + ``` From e2fc06c25e175a3ac20b7db0be19dfff54561ecc Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Mon, 2 Sep 2024 13:53:11 +0400 Subject: [PATCH 012/107] Implement missing methods in DeployValidation --- scripts/deploy/Config.s.sol | 2 +- scripts/deploy/Deploy.s.sol | 3 +- scripts/deploy/DeployValidation.sol | 174 ++++++++++++++++++++++------ 3 files changed, 141 insertions(+), 38 deletions(-) diff --git a/scripts/deploy/Config.s.sol b/scripts/deploy/Config.s.sol index 6e12ab5b..d5f52ac1 100644 --- a/scripts/deploy/Config.s.sol +++ b/scripts/deploy/Config.s.sol @@ -120,7 +120,7 @@ contract DGDeployConfig is Script { constructor() { // TODO: are these values correct as a default? - DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[0] = 0; + DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[0] = 0; // TODO: set to 1 ? DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[1] = 0; DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[2] = 0; } diff --git a/scripts/deploy/Deploy.s.sol b/scripts/deploy/Deploy.s.sol index 9c578130..39838abb 100644 --- a/scripts/deploy/Deploy.s.sol +++ b/scripts/deploy/Deploy.s.sol @@ -335,7 +335,7 @@ contract DeployDG is Script { uint256 quorum = dgDeployConfig.RESEAL_COMMITTEE_QUORUM; address[] memory committeeMembers = dgDeployConfig.RESEAL_COMMITTEE_MEMBERS; - // TODO: Don't we need to use timelock here? + // TODO: Don't we need to use non-zero timelock here? return new ResealCommittee( address(adminExecutor), committeeMembers, quorum, address(dualGovernance), Durations.from(0) ); @@ -355,7 +355,6 @@ contract DeployDG is Script { function getDeployedAddresses() internal view returns (DeployValidation.DeployResult memory) { return DeployValidation.DeployResult({ - deployer: address(deployer), adminExecutor: payable(address(adminExecutor)), timelock: address(timelock), emergencyGovernance: address(emergencyGovernance), diff --git a/scripts/deploy/DeployValidation.sol b/scripts/deploy/DeployValidation.sol index cf5ec41b..2fdf34cb 100644 --- a/scripts/deploy/DeployValidation.sol +++ b/scripts/deploy/DeployValidation.sol @@ -2,15 +2,25 @@ pragma solidity 0.8.26; import {Timestamps} from "contracts/types/Timestamp.sol"; +import {Durations} from "contracts/types/Duration.sol"; +import {PercentD16} from "contracts/types/PercentD16.sol"; import {Executor} from "contracts/Executor.sol"; import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; +import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; +import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; +import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; -import {DGDeployConfig, ConfigValues} from "./Config.s.sol"; +import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; +import {ResealManager} from "contracts/ResealManager.sol"; +import {DualGovernance} from "contracts/DualGovernance.sol"; +import {Escrow} from "contracts/Escrow.sol"; +import {DualGovernanceConfig} from "contracts/libraries/DualGovernanceConfig.sol"; +import {State} from "contracts/libraries/DualGovernanceStateMachine.sol"; +import {DGDeployConfig, LidoAddresses, ConfigValues} from "./Config.s.sol"; library DeployValidation { struct DeployResult { - address deployer; // TODO: not used, del? address payable adminExecutor; address timelock; address emergencyGovernance; @@ -27,19 +37,19 @@ library DeployValidation { function check(DeployResult memory res) internal { DGDeployConfig configProvider = new DGDeployConfig(); ConfigValues memory dgDeployConfig = configProvider.loadAndValidate(); + LidoAddresses memory lidoAddresses = configProvider.lidoAddresses(dgDeployConfig); checkAdminExecutor(res.adminExecutor, res.timelock); checkTimelock(res, dgDeployConfig); - checkEmergencyActivationCommittee(res.emergencyActivationCommittee, res.adminExecutor); - checkEmergencyExecutionCommittee(res.emergencyExecutionCommittee, res.adminExecutor); - checkTimelockedGovernance(); - checkResealManager(); - // TODO: check dualGovernanceConfigProvider? - checkDualGovernance(); - checkTiebreakerCoreCommittee(); - checkTiebreakerSubCommittee1(); - checkTiebreakerSubCommittee2(); - checkResealCommittee(); + checkEmergencyActivationCommittee(res.emergencyActivationCommittee, res.adminExecutor, dgDeployConfig); + checkEmergencyExecutionCommittee(res.emergencyExecutionCommittee, res.adminExecutor, dgDeployConfig); + checkTimelockedGovernance(res, lidoAddresses); + checkResealManager(res); + checkDualGovernance(res, dgDeployConfig, lidoAddresses); + checkTiebreakerCoreCommittee(res, dgDeployConfig); + checkTiebreakerSubCommittee1(res, dgDeployConfig); + checkTiebreakerSubCommittee2(res, dgDeployConfig); + checkResealCommittee(res, dgDeployConfig); } function checkAdminExecutor(address payable executor, address timelock) internal view { @@ -55,7 +65,7 @@ library DeployValidation { require( timelockInstance.MAX_EMERGENCY_PROTECTION_DURATION() == dgDeployConfig.MAX_EMERGENCY_PROTECTION_DURATION ); - // committees + require( timelockInstance.getEmergencyProtectionContext().emergencyActivationCommittee == res.emergencyActivationCommittee @@ -79,47 +89,141 @@ library DeployValidation { function checkEmergencyActivationCommittee( address emergencyActivationCommittee, - address adminExecutor + address adminExecutor, + ConfigValues memory dgDeployConfig ) internal view { - require(EmergencyActivationCommittee(emergencyActivationCommittee).owner() == adminExecutor); - // TODO: check members? - // TODO: check quorum? + EmergencyActivationCommittee committee = EmergencyActivationCommittee(emergencyActivationCommittee); + require(committee.owner() == adminExecutor); + + for (uint256 i = 0; i < dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS.length; ++i) { + require(committee.isMember(dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS[i]) == true); + } + require(committee.quorum() == dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM); } function checkEmergencyExecutionCommittee( address emergencyExecutionCommittee, - address adminExecutor + address adminExecutor, + ConfigValues memory dgDeployConfig ) internal view { - require(EmergencyExecutionCommittee(emergencyExecutionCommittee).owner() == adminExecutor); - // TODO: check members? - // TODO: check quorum? + EmergencyExecutionCommittee committee = EmergencyExecutionCommittee(emergencyExecutionCommittee); + require(committee.owner() == adminExecutor); + + for (uint256 i = 0; i < dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS.length; ++i) { + require(committee.isMember(dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS[i]) == true); + } + require(committee.quorum() == dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_QUORUM); } - function checkTimelockedGovernance() internal view { - // TODO: implement + function checkTimelockedGovernance(DeployResult memory res, LidoAddresses memory lidoAddresses) internal view { + TimelockedGovernance emergencyTimelockedGovernance = TimelockedGovernance(res.emergencyGovernance); + require(emergencyTimelockedGovernance.GOVERNANCE() == address(lidoAddresses.voting)); + require(address(emergencyTimelockedGovernance.TIMELOCK()) == res.timelock); } - function checkResealManager() internal view { - // TODO: implement + function checkResealManager(DeployResult memory res) internal view { + require(address(ResealManager(res.resealManager).EMERGENCY_PROTECTED_TIMELOCK()) == res.timelock); } - function checkDualGovernance() internal view { - // TODO: implement + function checkDualGovernance( + DeployResult memory res, + ConfigValues memory dgDeployConfig, + LidoAddresses memory lidoAddresses + ) internal view { + DualGovernance dg = DualGovernance(res.dualGovernance); + require(address(dg.TIMELOCK()) == res.timelock); + require(address(dg.RESEAL_MANAGER()) == res.resealManager); + require(dg.MIN_TIEBREAKER_ACTIVATION_TIMEOUT() == dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT); + require(dg.MAX_TIEBREAKER_ACTIVATION_TIMEOUT() == dgDeployConfig.MAX_TIEBREAKER_ACTIVATION_TIMEOUT); + require(dg.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT() == dgDeployConfig.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT); + + Escrow escrowTemplate = Escrow(payable(dg.ESCROW_MASTER_COPY())); + require(escrowTemplate.DUAL_GOVERNANCE() == dg); + require(escrowTemplate.ST_ETH() == lidoAddresses.stETH); + require(escrowTemplate.WST_ETH() == lidoAddresses.wstETH); + require(escrowTemplate.WITHDRAWAL_QUEUE() == lidoAddresses.withdrawalQueue); + require(escrowTemplate.MIN_WITHDRAWALS_BATCH_SIZE() == dgDeployConfig.MIN_WITHDRAWALS_BATCH_SIZE); + + DualGovernanceConfig.Context memory dgConfig = dg.getConfigProvider().getDualGovernanceConfig(); + require( + PercentD16.unwrap(dgConfig.firstSealRageQuitSupport) + == PercentD16.unwrap(dgDeployConfig.FIRST_SEAL_RAGE_QUIT_SUPPORT) + ); + require( + PercentD16.unwrap(dgConfig.secondSealRageQuitSupport) + == PercentD16.unwrap(dgDeployConfig.SECOND_SEAL_RAGE_QUIT_SUPPORT) + ); + require(dgConfig.minAssetsLockDuration == dgDeployConfig.MIN_ASSETS_LOCK_DURATION); + require(dgConfig.dynamicTimelockMinDuration == dgDeployConfig.DYNAMIC_TIMELOCK_MIN_DURATION); + require(dgConfig.dynamicTimelockMaxDuration == dgDeployConfig.DYNAMIC_TIMELOCK_MAX_DURATION); + require(dgConfig.vetoSignallingMinActiveDuration == dgDeployConfig.VETO_SIGNALLING_MIN_ACTIVE_DURATION); + require( + dgConfig.vetoSignallingDeactivationMaxDuration == dgDeployConfig.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION + ); + require(dgConfig.vetoCooldownDuration == dgDeployConfig.VETO_COOLDOWN_DURATION); + require(dgConfig.rageQuitExtensionDelay == dgDeployConfig.RAGE_QUIT_EXTENSION_DELAY); + require(dgConfig.rageQuitEthWithdrawalsMinTimelock == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK); + require( + dgConfig.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber + == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER + ); + require( + dgConfig.rageQuitEthWithdrawalsTimelockGrowthCoeffs[0] + == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[0] + ); + require( + dgConfig.rageQuitEthWithdrawalsTimelockGrowthCoeffs[1] + == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[1] + ); + require( + dgConfig.rageQuitEthWithdrawalsTimelockGrowthCoeffs[2] + == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[2] + ); + + require(dg.getCurrentState() == State.Normal); } - function checkTiebreakerCoreCommittee() internal view { - // TODO: implement + function checkTiebreakerCoreCommittee(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { + TiebreakerCore tcc = TiebreakerCore(res.tiebreakerCoreCommittee); + require(tcc.owner() == res.adminExecutor); + require(tcc.timelockDuration() == dgDeployConfig.TIEBREAKER_EXECUTION_DELAY); + + // TODO: N sub committees + require(tcc.isMember(res.tiebreakerSubCommittee1) == true); + require(tcc.isMember(res.tiebreakerSubCommittee2) == true); + require(tcc.quorum() == 2); } - function checkTiebreakerSubCommittee1() internal view { - // TODO: implement + function checkTiebreakerSubCommittee1(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { + TiebreakerSubCommittee tsc = TiebreakerSubCommittee(res.tiebreakerSubCommittee1); + require(tsc.owner() == res.adminExecutor); + require(tsc.timelockDuration() == Durations.from(0)); // TODO: is it correct? + + for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS.length; ++i) { + require(tsc.isMember(dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS[i]) == true); + } + require(tsc.quorum() == dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_QUORUM); } - function checkTiebreakerSubCommittee2() internal view { - // TODO: implement + function checkTiebreakerSubCommittee2(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { + TiebreakerSubCommittee tsc = TiebreakerSubCommittee(res.tiebreakerSubCommittee2); + require(tsc.owner() == res.adminExecutor); + require(tsc.timelockDuration() == Durations.from(0), "TiebreakerSubCommittee2 timelock should be 0"); // TODO: is it correct? + + for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS.length; ++i) { + require(tsc.isMember(dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS[i]) == true); + } + require(tsc.quorum() == dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_QUORUM); } - function checkResealCommittee() internal view { - // TODO: implement + function checkResealCommittee(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { + ResealCommittee rc = ResealCommittee(res.resealCommittee); + require(rc.owner() == res.adminExecutor); + require(rc.timelockDuration() == Durations.from(0), "ResealCommittee timelock should be 0"); // TODO: is it correct? + + for (uint256 i = 0; i < dgDeployConfig.RESEAL_COMMITTEE_MEMBERS.length; ++i) { + require(rc.isMember(dgDeployConfig.RESEAL_COMMITTEE_MEMBERS[i]) == true); + } + require(rc.quorum() == dgDeployConfig.RESEAL_COMMITTEE_QUORUM); } } From cc5c69e75f0b2823f3504d323fd9bc3cda372878 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Mon, 2 Sep 2024 21:17:05 +0400 Subject: [PATCH 013/107] Make deploy validation errors more verbose --- scripts/deploy/DeployValidation.sol | 233 ++++++++++++++++++++-------- 1 file changed, 171 insertions(+), 62 deletions(-) diff --git a/scripts/deploy/DeployValidation.sol b/scripts/deploy/DeployValidation.sol index 2fdf34cb..da8de732 100644 --- a/scripts/deploy/DeployValidation.sol +++ b/scripts/deploy/DeployValidation.sol @@ -53,38 +53,64 @@ library DeployValidation { } function checkAdminExecutor(address payable executor, address timelock) internal view { - require(Executor(executor).owner() == timelock); + require(Executor(executor).owner() == timelock, "AdminExecutor owner != EmergencyProtectedTimelock"); } function checkTimelock(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { EmergencyProtectedTimelock timelockInstance = EmergencyProtectedTimelock(res.timelock); - require(timelockInstance.getAdminExecutor() == res.adminExecutor); - require(timelockInstance.MAX_AFTER_SUBMIT_DELAY() == dgDeployConfig.MAX_AFTER_SUBMIT_DELAY); - require(timelockInstance.MAX_AFTER_SCHEDULE_DELAY() == dgDeployConfig.MAX_AFTER_SCHEDULE_DELAY); - require(timelockInstance.MAX_EMERGENCY_MODE_DURATION() == dgDeployConfig.MAX_EMERGENCY_MODE_DURATION); require( - timelockInstance.MAX_EMERGENCY_PROTECTION_DURATION() == dgDeployConfig.MAX_EMERGENCY_PROTECTION_DURATION + timelockInstance.getAdminExecutor() == res.adminExecutor, + "Incorrect adminExecutor address in EmergencyProtectedTimelock" + ); + require( + timelockInstance.MAX_AFTER_SUBMIT_DELAY() == dgDeployConfig.MAX_AFTER_SUBMIT_DELAY, + "Incorrect parameter MAX_AFTER_SUBMIT_DELAY" + ); + require( + timelockInstance.MAX_AFTER_SCHEDULE_DELAY() == dgDeployConfig.MAX_AFTER_SCHEDULE_DELAY, + "Incorrect parameter MAX_AFTER_SCHEDULE_DELAY" + ); + require( + timelockInstance.MAX_EMERGENCY_MODE_DURATION() == dgDeployConfig.MAX_EMERGENCY_MODE_DURATION, + "Incorrect parameter MAX_EMERGENCY_MODE_DURATION" + ); + require( + timelockInstance.MAX_EMERGENCY_PROTECTION_DURATION() == dgDeployConfig.MAX_EMERGENCY_PROTECTION_DURATION, + "Incorrect parameter MAX_EMERGENCY_PROTECTION_DURATION" ); require( timelockInstance.getEmergencyProtectionContext().emergencyActivationCommittee - == res.emergencyActivationCommittee + == res.emergencyActivationCommittee, + "Incorrect emergencyActivationCommittee address in EmergencyProtectedTimelock" ); require( timelockInstance.getEmergencyProtectionContext().emergencyExecutionCommittee - == res.emergencyExecutionCommittee + == res.emergencyExecutionCommittee, + "Incorrect emergencyExecutionCommittee address in EmergencyProtectedTimelock" ); require( timelockInstance.getEmergencyProtectionContext().emergencyProtectionEndsAfter - <= dgDeployConfig.EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()) + <= dgDeployConfig.EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()), + "Incorrect value for emergencyProtectionEndsAfter" ); require( timelockInstance.getEmergencyProtectionContext().emergencyModeDuration - == dgDeployConfig.EMERGENCY_MODE_DURATION + == dgDeployConfig.EMERGENCY_MODE_DURATION, + "Incorrect value for emergencyModeDuration" + ); + require( + timelockInstance.getEmergencyProtectionContext().emergencyGovernance == res.emergencyGovernance, + "Incorrect emergencyGovernance address in EmergencyProtectedTimelock" + ); + require( + timelockInstance.getAfterSubmitDelay() == dgDeployConfig.AFTER_SUBMIT_DELAY, + "Incorrect parameter AFTER_SUBMIT_DELAY" + ); + require( + timelockInstance.getGovernance() == res.dualGovernance, + "Incorrect governance address in EmergencyProtectedTimelock" ); - require(timelockInstance.getEmergencyProtectionContext().emergencyGovernance == res.emergencyGovernance); - require(timelockInstance.getAfterSubmitDelay() == dgDeployConfig.AFTER_SUBMIT_DELAY); - require(timelockInstance.getGovernance() == res.dualGovernance); } function checkEmergencyActivationCommittee( @@ -93,12 +119,18 @@ library DeployValidation { ConfigValues memory dgDeployConfig ) internal view { EmergencyActivationCommittee committee = EmergencyActivationCommittee(emergencyActivationCommittee); - require(committee.owner() == adminExecutor); + require(committee.owner() == adminExecutor, "EmergencyActivationCommittee owner != adminExecutor"); for (uint256 i = 0; i < dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS.length; ++i) { - require(committee.isMember(dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS[i]) == true); + require( + committee.isMember(dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS[i]) == true, + "Incorrect member of EmergencyActivationCommittee" + ); } - require(committee.quorum() == dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM); + require( + committee.quorum() == dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, + "EmergencyActivationCommittee has incorrect quorum set" + ); } function checkEmergencyExecutionCommittee( @@ -107,22 +139,37 @@ library DeployValidation { ConfigValues memory dgDeployConfig ) internal view { EmergencyExecutionCommittee committee = EmergencyExecutionCommittee(emergencyExecutionCommittee); - require(committee.owner() == adminExecutor); + require(committee.owner() == adminExecutor, "EmergencyExecutionCommittee owner != adminExecutor"); for (uint256 i = 0; i < dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS.length; ++i) { - require(committee.isMember(dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS[i]) == true); + require( + committee.isMember(dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS[i]) == true, + "Incorrect member of EmergencyExecutionCommittee" + ); } - require(committee.quorum() == dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_QUORUM); + require( + committee.quorum() == dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_QUORUM, + "EmergencyExecutionCommittee has incorrect quorum set" + ); } function checkTimelockedGovernance(DeployResult memory res, LidoAddresses memory lidoAddresses) internal view { TimelockedGovernance emergencyTimelockedGovernance = TimelockedGovernance(res.emergencyGovernance); - require(emergencyTimelockedGovernance.GOVERNANCE() == address(lidoAddresses.voting)); - require(address(emergencyTimelockedGovernance.TIMELOCK()) == res.timelock); + require( + emergencyTimelockedGovernance.GOVERNANCE() == address(lidoAddresses.voting), + "TimelockedGovernance governance != Lido voting" + ); + require( + address(emergencyTimelockedGovernance.TIMELOCK()) == res.timelock, + "Incorrect address for timelock in TimelockedGovernance" + ); } function checkResealManager(DeployResult memory res) internal view { - require(address(ResealManager(res.resealManager).EMERGENCY_PROTECTED_TIMELOCK()) == res.timelock); + require( + address(ResealManager(res.resealManager).EMERGENCY_PROTECTED_TIMELOCK()) == res.timelock, + "Incorrect address for EMERGENCY_PROTECTED_TIMELOCK in ResealManager" + ); } function checkDualGovernance( @@ -131,99 +178,161 @@ library DeployValidation { LidoAddresses memory lidoAddresses ) internal view { DualGovernance dg = DualGovernance(res.dualGovernance); - require(address(dg.TIMELOCK()) == res.timelock); - require(address(dg.RESEAL_MANAGER()) == res.resealManager); - require(dg.MIN_TIEBREAKER_ACTIVATION_TIMEOUT() == dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT); - require(dg.MAX_TIEBREAKER_ACTIVATION_TIMEOUT() == dgDeployConfig.MAX_TIEBREAKER_ACTIVATION_TIMEOUT); - require(dg.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT() == dgDeployConfig.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT); + require(address(dg.TIMELOCK()) == res.timelock, "Incorrect address for timelock in DualGovernance"); + require( + address(dg.RESEAL_MANAGER()) == res.resealManager, "Incorrect address for resealManager in DualGovernance" + ); + require( + dg.MIN_TIEBREAKER_ACTIVATION_TIMEOUT() == dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT, + "Incorrect parameter MIN_TIEBREAKER_ACTIVATION_TIMEOUT" + ); + require( + dg.MAX_TIEBREAKER_ACTIVATION_TIMEOUT() == dgDeployConfig.MAX_TIEBREAKER_ACTIVATION_TIMEOUT, + "Incorrect parameter MAX_TIEBREAKER_ACTIVATION_TIMEOUT" + ); + require( + dg.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT() == dgDeployConfig.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT, + "Incorrect parameter MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT" + ); Escrow escrowTemplate = Escrow(payable(dg.ESCROW_MASTER_COPY())); - require(escrowTemplate.DUAL_GOVERNANCE() == dg); - require(escrowTemplate.ST_ETH() == lidoAddresses.stETH); - require(escrowTemplate.WST_ETH() == lidoAddresses.wstETH); - require(escrowTemplate.WITHDRAWAL_QUEUE() == lidoAddresses.withdrawalQueue); - require(escrowTemplate.MIN_WITHDRAWALS_BATCH_SIZE() == dgDeployConfig.MIN_WITHDRAWALS_BATCH_SIZE); + require(escrowTemplate.DUAL_GOVERNANCE() == dg, "Escrow has incorrect DualGovernance address"); + require(escrowTemplate.ST_ETH() == lidoAddresses.stETH, "Escrow has incorrect StETH address"); + require(escrowTemplate.WST_ETH() == lidoAddresses.wstETH, "Escrow has incorrect WstETH address"); + require( + escrowTemplate.WITHDRAWAL_QUEUE() == lidoAddresses.withdrawalQueue, + "Escrow has incorrect WithdrawalQueue address" + ); + require( + escrowTemplate.MIN_WITHDRAWALS_BATCH_SIZE() == dgDeployConfig.MIN_WITHDRAWALS_BATCH_SIZE, + "Incorrect parameter MIN_WITHDRAWALS_BATCH_SIZE" + ); DualGovernanceConfig.Context memory dgConfig = dg.getConfigProvider().getDualGovernanceConfig(); require( PercentD16.unwrap(dgConfig.firstSealRageQuitSupport) - == PercentD16.unwrap(dgDeployConfig.FIRST_SEAL_RAGE_QUIT_SUPPORT) + == PercentD16.unwrap(dgDeployConfig.FIRST_SEAL_RAGE_QUIT_SUPPORT), + "Incorrect parameter FIRST_SEAL_RAGE_QUIT_SUPPORT" ); require( PercentD16.unwrap(dgConfig.secondSealRageQuitSupport) - == PercentD16.unwrap(dgDeployConfig.SECOND_SEAL_RAGE_QUIT_SUPPORT) + == PercentD16.unwrap(dgDeployConfig.SECOND_SEAL_RAGE_QUIT_SUPPORT), + "Incorrect parameter SECOND_SEAL_RAGE_QUIT_SUPPORT" + ); + require( + dgConfig.minAssetsLockDuration == dgDeployConfig.MIN_ASSETS_LOCK_DURATION, + "Incorrect parameter MIN_ASSETS_LOCK_DURATION" + ); + require( + dgConfig.dynamicTimelockMinDuration == dgDeployConfig.DYNAMIC_TIMELOCK_MIN_DURATION, + "Incorrect parameter DYNAMIC_TIMELOCK_MIN_DURATION" + ); + require( + dgConfig.dynamicTimelockMaxDuration == dgDeployConfig.DYNAMIC_TIMELOCK_MAX_DURATION, + "Incorrect parameter DYNAMIC_TIMELOCK_MAX_DURATION" ); - require(dgConfig.minAssetsLockDuration == dgDeployConfig.MIN_ASSETS_LOCK_DURATION); - require(dgConfig.dynamicTimelockMinDuration == dgDeployConfig.DYNAMIC_TIMELOCK_MIN_DURATION); - require(dgConfig.dynamicTimelockMaxDuration == dgDeployConfig.DYNAMIC_TIMELOCK_MAX_DURATION); - require(dgConfig.vetoSignallingMinActiveDuration == dgDeployConfig.VETO_SIGNALLING_MIN_ACTIVE_DURATION); require( - dgConfig.vetoSignallingDeactivationMaxDuration == dgDeployConfig.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION + dgConfig.vetoSignallingMinActiveDuration == dgDeployConfig.VETO_SIGNALLING_MIN_ACTIVE_DURATION, + "Incorrect parameter VETO_SIGNALLING_MIN_ACTIVE_DURATION" + ); + require( + dgConfig.vetoSignallingDeactivationMaxDuration == dgDeployConfig.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION, + "Incorrect parameter VETO_SIGNALLING_DEACTIVATION_MAX_DURATION" + ); + require( + dgConfig.vetoCooldownDuration == dgDeployConfig.VETO_COOLDOWN_DURATION, + "Incorrect parameter VETO_COOLDOWN_DURATION" + ); + require( + dgConfig.rageQuitExtensionDelay == dgDeployConfig.RAGE_QUIT_EXTENSION_DELAY, + "Incorrect parameter RAGE_QUIT_EXTENSION_DELAY" + ); + require( + dgConfig.rageQuitEthWithdrawalsMinTimelock == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK, + "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK" ); - require(dgConfig.vetoCooldownDuration == dgDeployConfig.VETO_COOLDOWN_DURATION); - require(dgConfig.rageQuitExtensionDelay == dgDeployConfig.RAGE_QUIT_EXTENSION_DELAY); - require(dgConfig.rageQuitEthWithdrawalsMinTimelock == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK); require( dgConfig.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber - == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER + == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER, + "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER" ); require( dgConfig.rageQuitEthWithdrawalsTimelockGrowthCoeffs[0] - == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[0] + == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[0], + "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[0]" ); require( dgConfig.rageQuitEthWithdrawalsTimelockGrowthCoeffs[1] - == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[1] + == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[1], + "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[1]" ); require( dgConfig.rageQuitEthWithdrawalsTimelockGrowthCoeffs[2] - == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[2] + == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[2], + "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[2]" ); - require(dg.getCurrentState() == State.Normal); + require(dg.getCurrentState() == State.Normal, "Incorrect DualGovernance state"); } function checkTiebreakerCoreCommittee(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { TiebreakerCore tcc = TiebreakerCore(res.tiebreakerCoreCommittee); - require(tcc.owner() == res.adminExecutor); - require(tcc.timelockDuration() == dgDeployConfig.TIEBREAKER_EXECUTION_DELAY); + require(tcc.owner() == res.adminExecutor, "TiebreakerCoreCommittee owner != adminExecutor"); + require( + tcc.timelockDuration() == dgDeployConfig.TIEBREAKER_EXECUTION_DELAY, + "Incorrect parameter TIEBREAKER_EXECUTION_DELAY" + ); // TODO: N sub committees - require(tcc.isMember(res.tiebreakerSubCommittee1) == true); - require(tcc.isMember(res.tiebreakerSubCommittee2) == true); - require(tcc.quorum() == 2); + require(tcc.isMember(res.tiebreakerSubCommittee1) == true, "Incorrect member of TiebreakerCoreCommittee"); + require(tcc.isMember(res.tiebreakerSubCommittee2) == true, "Incorrect member of TiebreakerCoreCommittee"); + require(tcc.quorum() == 2, "Incorrect quorum in TiebreakerCoreCommittee"); } function checkTiebreakerSubCommittee1(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { TiebreakerSubCommittee tsc = TiebreakerSubCommittee(res.tiebreakerSubCommittee1); - require(tsc.owner() == res.adminExecutor); - require(tsc.timelockDuration() == Durations.from(0)); // TODO: is it correct? + require(tsc.owner() == res.adminExecutor, "TiebreakerSubCommittee1 owner != adminExecutor"); + require(tsc.timelockDuration() == Durations.from(0), "TiebreakerSubCommittee1 timelock should be 0"); // TODO: is it correct? for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS.length; ++i) { - require(tsc.isMember(dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS[i]) == true); + require( + tsc.isMember(dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS[i]) == true, + "Incorrect member of TiebreakerSubCommittee1" + ); } - require(tsc.quorum() == dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_QUORUM); + require( + tsc.quorum() == dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_QUORUM, + "Incorrect quorum in TiebreakerSubCommittee1" + ); } function checkTiebreakerSubCommittee2(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { TiebreakerSubCommittee tsc = TiebreakerSubCommittee(res.tiebreakerSubCommittee2); - require(tsc.owner() == res.adminExecutor); + require(tsc.owner() == res.adminExecutor, "TiebreakerSubCommittee1 owner != adminExecutor"); require(tsc.timelockDuration() == Durations.from(0), "TiebreakerSubCommittee2 timelock should be 0"); // TODO: is it correct? for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS.length; ++i) { - require(tsc.isMember(dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS[i]) == true); + require( + tsc.isMember(dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS[i]) == true, + "Incorrect member of TiebreakerSubCommittee2" + ); } - require(tsc.quorum() == dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_QUORUM); + require( + tsc.quorum() == dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_QUORUM, + "Incorrect quorum in TiebreakerSubCommittee2" + ); } function checkResealCommittee(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { ResealCommittee rc = ResealCommittee(res.resealCommittee); - require(rc.owner() == res.adminExecutor); + require(rc.owner() == res.adminExecutor, "ResealCommittee owner != adminExecutor"); require(rc.timelockDuration() == Durations.from(0), "ResealCommittee timelock should be 0"); // TODO: is it correct? for (uint256 i = 0; i < dgDeployConfig.RESEAL_COMMITTEE_MEMBERS.length; ++i) { - require(rc.isMember(dgDeployConfig.RESEAL_COMMITTEE_MEMBERS[i]) == true); + require( + rc.isMember(dgDeployConfig.RESEAL_COMMITTEE_MEMBERS[i]) == true, "Incorrect member of ResealCommittee" + ); } - require(rc.quorum() == dgDeployConfig.RESEAL_COMMITTEE_QUORUM); + require(rc.quorum() == dgDeployConfig.RESEAL_COMMITTEE_QUORUM, "Incorrect quorum in ResealCommittee"); } } From ae569d559588742fc5f8037a7ccd8423db19a9f5 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Tue, 3 Sep 2024 19:09:00 +0400 Subject: [PATCH 014/107] Various improvements --- scripts/deploy/Config.s.sol | 42 +++-- scripts/deploy/Deploy.s.sol | 265 ++++++++++++++++++---------- scripts/deploy/DeployValidation.sol | 49 +++-- 3 files changed, 230 insertions(+), 126 deletions(-) diff --git a/scripts/deploy/Config.s.sol b/scripts/deploy/Config.s.sol index d5f52ac1..fec93c9c 100644 --- a/scripts/deploy/Config.s.sol +++ b/scripts/deploy/Config.s.sol @@ -78,6 +78,24 @@ struct LidoAddresses { IAragonVoting voting; } +function getLidoAddresses(ConfigValues memory config) pure returns (LidoAddresses memory) { + if (keccak256(bytes(config.CHAIN)) == CHAIN_NAME_MAINNET_HASH) { + return LidoAddresses({ + stETH: IStETH(MAINNET_ST_ETH), + wstETH: IWstETH(MAINNET_WST_ETH), + withdrawalQueue: IWithdrawalQueue(MAINNET_WITHDRAWAL_QUEUE), + voting: IAragonVoting(MAINNET_DAO_VOTING) + }); + } + + return LidoAddresses({ + stETH: IStETH(HOLESKY_ST_ETH), + wstETH: IWstETH(HOLESKY_WST_ETH), + withdrawalQueue: IWithdrawalQueue(HOLESKY_WITHDRAWAL_QUEUE), + voting: IAragonVoting(HOLESKY_DAO_VOTING) + }); +} + contract DGDeployConfig is Script { error InvalidRageQuitETHWithdrawalsTimelockGrowthCoeffs(uint256[] coeffs); error InvalidQuorum(string committee, uint256 quorum); @@ -275,6 +293,12 @@ contract DGDeployConfig is Script { ) { revert InvalidQuorum("RESEAL_COMMITTEE", config.RESEAL_COMMITTEE_QUORUM); } + + // TODO: AFTER_SUBMIT_DELAY <= MAX_AFTER_SUBMIT_DELAY + // TODO: AFTER_SCHEDULE_DELAY <= MAX_AFTER_SCHEDULE_DELAY + // TODO: EMERGENCY_MODE_DURATION <= MAX_EMERGENCY_MODE_DURATION + // TODO: MIN_TIEBREAKER_ACTIVATION_TIMEOUT <= TIEBREAKER_ACTIVATION_TIMEOUT <= MAX_TIEBREAKER_ACTIVATION_TIMEOUT + // TODO: DYNAMIC_TIMELOCK_MIN_DURATION <= DYNAMIC_TIMELOCK_MAX_DURATION } function printCommittees(ConfigValues memory config) internal view { @@ -332,22 +356,4 @@ contract DGDeployConfig is Script { } console.log("================================================="); } - - function lidoAddresses(ConfigValues memory config) external pure returns (LidoAddresses memory) { - if (keccak256(bytes(config.CHAIN)) == CHAIN_NAME_MAINNET_HASH) { - return LidoAddresses({ - stETH: IStETH(MAINNET_ST_ETH), - wstETH: IWstETH(MAINNET_WST_ETH), - withdrawalQueue: IWithdrawalQueue(MAINNET_WITHDRAWAL_QUEUE), - voting: IAragonVoting(MAINNET_DAO_VOTING) - }); - } - - return LidoAddresses({ - stETH: IStETH(HOLESKY_ST_ETH), - wstETH: IWstETH(HOLESKY_WST_ETH), - withdrawalQueue: IWithdrawalQueue(HOLESKY_WITHDRAWAL_QUEUE), - voting: IAragonVoting(HOLESKY_DAO_VOTING) - }); - } } diff --git a/scripts/deploy/Deploy.s.sol b/scripts/deploy/Deploy.s.sol index 39838abb..9c35cdf6 100644 --- a/scripts/deploy/Deploy.s.sol +++ b/scripts/deploy/Deploy.s.sol @@ -32,59 +32,51 @@ import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; -import {DGDeployConfig, ConfigValues, LidoAddresses} from "./Config.s.sol"; +import {DGDeployConfig, ConfigValues, LidoAddresses, getLidoAddresses} from "./Config.s.sol"; import {DeployValidation} from "./DeployValidation.sol"; +struct DeployedContracts { + Executor adminExecutor; + EmergencyProtectedTimelock timelock; + TimelockedGovernance emergencyGovernance; + EmergencyActivationCommittee emergencyActivationCommittee; + EmergencyExecutionCommittee emergencyExecutionCommittee; + ResealManager resealManager; + DualGovernance dualGovernance; + ResealCommittee resealCommittee; + TiebreakerCore tiebreakerCoreCommittee; + address tiebreakerSubCommittee1; + address tiebreakerSubCommittee2; +} + contract DeployDG is Script { using DeployValidation for DeployValidation.DeployResult; - LidoAddresses internal lidoAddresses; - ConfigValues private dgDeployConfig; - - // Emergency Protected Timelock Contracts - // --- - Executor internal adminExecutor; - EmergencyProtectedTimelock internal timelock; - TimelockedGovernance internal emergencyGovernance; - EmergencyActivationCommittee internal emergencyActivationCommittee; - EmergencyExecutionCommittee internal emergencyExecutionCommittee; - - // --- - // Dual Governance Contracts - // --- - ResealManager internal resealManager; - DualGovernance internal dualGovernance; - ImmutableDualGovernanceConfigProvider internal dualGovernanceConfigProvider; - - ResealCommittee internal resealCommittee; - TiebreakerCore internal tiebreakerCoreCommittee; - TiebreakerSubCommittee[] internal tiebreakerSubCommittees; + DeployedContracts private _contracts; address internal deployer; function run() external { DGDeployConfig configProvider = new DGDeployConfig(); - dgDeployConfig = configProvider.loadAndValidate(); + ConfigValues memory _dgDeployConfig = configProvider.loadAndValidate(); // TODO: check chain id? - lidoAddresses = configProvider.lidoAddresses(dgDeployConfig); - deployer = vm.addr(dgDeployConfig.DEPLOYER_PRIVATE_KEY); - vm.startBroadcast(dgDeployConfig.DEPLOYER_PRIVATE_KEY); + deployer = vm.addr(_dgDeployConfig.DEPLOYER_PRIVATE_KEY); + vm.startBroadcast(_dgDeployConfig.DEPLOYER_PRIVATE_KEY); - deployDualGovernanceSetup(); + deployDualGovernanceSetup(_dgDeployConfig); vm.stopBroadcast(); - DeployValidation.DeployResult memory res = getDeployedAddresses(); + DeployValidation.DeployResult memory res = getDeployedAddresses(_contracts); console.log("DG deployed successfully"); console.log("DualGovernance address", res.dualGovernance); console.log("ResealManager address", res.resealManager); console.log("TiebreakerCoreCommittee address", res.tiebreakerCoreCommittee); - for (uint256 i = 0; i < tiebreakerSubCommittees.length; ++i) { - console.log("TiebreakerSubCommittee #", i, "address", address(tiebreakerSubCommittees[i])); - } + console.log("TiebreakerSubCommittee #1 address", _contracts.tiebreakerSubCommittee1); + console.log("TiebreakerSubCommittee #2 address", _contracts.tiebreakerSubCommittee2); console.log("AdminExecutor address", res.adminExecutor); console.log("EmergencyProtectedTimelock address", res.timelock); console.log("EmergencyGovernance address", res.emergencyGovernance); @@ -99,53 +91,74 @@ contract DeployDG is Script { console.log(unicode"Verified ✅"); } - function deployDualGovernanceSetup() internal { - deployEmergencyProtectedTimelockContracts(); - resealManager = deployResealManager(); - dualGovernanceConfigProvider = deployDualGovernanceConfigProvider(); - dualGovernance = deployDualGovernance({configProvider: dualGovernanceConfigProvider}); + function deployDualGovernanceSetup(ConfigValues memory dgDeployConfig) internal { + LidoAddresses memory lidoAddresses = getLidoAddresses(dgDeployConfig); + _contracts = deployEmergencyProtectedTimelockContracts(lidoAddresses, dgDeployConfig, _contracts); + _contracts.resealManager = deployResealManager(_contracts.timelock); + ImmutableDualGovernanceConfigProvider dualGovernanceConfigProvider = + deployDualGovernanceConfigProvider(dgDeployConfig); + DualGovernance dualGovernance = deployDualGovernance({ + configProvider: dualGovernanceConfigProvider, + timelock: _contracts.timelock, + resealManager: _contracts.resealManager, + dgDeployConfig: dgDeployConfig, + lidoAddresses: lidoAddresses + }); + _contracts.dualGovernance = dualGovernance; - tiebreakerCoreCommittee = deployEmptyTiebreakerCoreCommittee({ + _contracts.tiebreakerCoreCommittee = deployEmptyTiebreakerCoreCommittee({ owner: deployer, // temporary set owner to deployer, to add sub committees manually - _timelock: dgDeployConfig.TIEBREAKER_EXECUTION_DELAY + dualGovernance: address(dualGovernance), + executionDelay: dgDeployConfig.TIEBREAKER_EXECUTION_DELAY }); - deployTiebreakerSubCommittees(); + address[] memory tiebreakerSubCommittees = deployTiebreakerSubCommittees( + address(_contracts.adminExecutor), _contracts.tiebreakerCoreCommittee, dgDeployConfig + ); + _contracts.tiebreakerSubCommittee1 = tiebreakerSubCommittees[0]; + _contracts.tiebreakerSubCommittee2 = tiebreakerSubCommittees[1]; - tiebreakerCoreCommittee.transferOwnership(address(adminExecutor)); + _contracts.tiebreakerCoreCommittee.transferOwnership(address(_contracts.adminExecutor)); - resealCommittee = deployResealCommittee(); + _contracts.resealCommittee = + deployResealCommittee(address(_contracts.adminExecutor), address(dualGovernance), dgDeployConfig); // --- // Finalize Setup // --- - adminExecutor.execute( + _contracts.adminExecutor.execute( address(dualGovernance), 0, - abi.encodeCall(dualGovernance.registerProposer, (address(lidoAddresses.voting), address(adminExecutor))) + abi.encodeCall( + dualGovernance.registerProposer, (address(lidoAddresses.voting), address(_contracts.adminExecutor)) + ) ); - adminExecutor.execute( + _contracts.adminExecutor.execute( address(dualGovernance), 0, abi.encodeCall(dualGovernance.setTiebreakerActivationTimeout, dgDeployConfig.TIEBREAKER_ACTIVATION_TIMEOUT) ); - adminExecutor.execute( + _contracts.adminExecutor.execute( address(dualGovernance), 0, - abi.encodeCall(dualGovernance.setTiebreakerCommittee, address(tiebreakerCoreCommittee)) + abi.encodeCall(dualGovernance.setTiebreakerCommittee, address(_contracts.tiebreakerCoreCommittee)) ); - adminExecutor.execute( + _contracts.adminExecutor.execute( address(dualGovernance), 0, abi.encodeCall( dualGovernance.addTiebreakerSealableWithdrawalBlocker, address(lidoAddresses.withdrawalQueue) ) ); - adminExecutor.execute( - address(dualGovernance), 0, abi.encodeCall(dualGovernance.setResealCommittee, address(resealCommittee)) + _contracts.adminExecutor.execute( + address(dualGovernance), + 0, + abi.encodeCall(dualGovernance.setResealCommittee, address(_contracts.resealCommittee)) ); - finalizeEmergencyProtectedTimelockDeploy(); + finalizeEmergencyProtectedTimelockDeploy( + _contracts.adminExecutor, _contracts.timelock, address(dualGovernance), dgDeployConfig + ); // --- // TODO: Grant Reseal Manager Roles @@ -160,32 +173,45 @@ contract DeployDG is Script { vm.stopPrank(); */ } - function deployEmergencyProtectedTimelockContracts() internal { - adminExecutor = deployExecutor(deployer); - timelock = deployEmergencyProtectedTimelock(); - - emergencyActivationCommittee = deployEmergencyActivationCommittee({ + function deployEmergencyProtectedTimelockContracts( + LidoAddresses memory lidoAddresses, + ConfigValues memory dgDeployConfig, + DeployedContracts memory contracts + ) internal returns (DeployedContracts memory) { + Executor adminExecutor = deployExecutor(deployer); + EmergencyProtectedTimelock timelock = deployEmergencyProtectedTimelock(address(adminExecutor), dgDeployConfig); + + contracts.adminExecutor = adminExecutor; + contracts.timelock = timelock; + contracts.emergencyActivationCommittee = deployEmergencyActivationCommittee({ quorum: dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, members: dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS, - owner: address(adminExecutor) + owner: address(adminExecutor), + timelock: address(timelock) }); - emergencyExecutionCommittee = deployEmergencyExecutionCommittee({ + contracts.emergencyExecutionCommittee = deployEmergencyExecutionCommittee({ quorum: dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_QUORUM, members: dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS, - owner: address(adminExecutor) + owner: address(adminExecutor), + timelock: address(timelock) }); - emergencyGovernance = deployTimelockedGovernance({governance: address(lidoAddresses.voting)}); + contracts.emergencyGovernance = + deployTimelockedGovernance({governance: address(lidoAddresses.voting), timelock: timelock}); adminExecutor.execute( address(timelock), 0, - abi.encodeCall(timelock.setEmergencyProtectionActivationCommittee, (address(emergencyActivationCommittee))) + abi.encodeCall( + timelock.setEmergencyProtectionActivationCommittee, (address(contracts.emergencyActivationCommittee)) + ) ); adminExecutor.execute( address(timelock), 0, - abi.encodeCall(timelock.setEmergencyProtectionExecutionCommittee, (address(emergencyExecutionCommittee))) + abi.encodeCall( + timelock.setEmergencyProtectionExecutionCommittee, (address(contracts.emergencyExecutionCommittee)) + ) ); // TODO: Do we really need to set it? @@ -204,15 +230,22 @@ contract DeployDG is Script { ); adminExecutor.execute( - address(timelock), 0, abi.encodeCall(timelock.setEmergencyGovernance, (address(emergencyGovernance))) + address(timelock), + 0, + abi.encodeCall(timelock.setEmergencyGovernance, (address(contracts.emergencyGovernance))) ); + + return contracts; } function deployExecutor(address owner) internal returns (Executor) { return new Executor(owner); } - function deployEmergencyProtectedTimelock() internal returns (EmergencyProtectedTimelock) { + function deployEmergencyProtectedTimelock( + address adminExecutor, + ConfigValues memory dgDeployConfig + ) internal returns (EmergencyProtectedTimelock) { return new EmergencyProtectedTimelock({ adminExecutor: address(adminExecutor), sanityCheckParams: EmergencyProtectedTimelock.SanityCheckParams({ @@ -227,7 +260,8 @@ contract DeployDG is Script { function deployEmergencyActivationCommittee( address owner, uint256 quorum, - address[] memory members + address[] memory members, + address timelock ) internal returns (EmergencyActivationCommittee) { return new EmergencyActivationCommittee(owner, members, quorum, address(timelock)); } @@ -235,20 +269,27 @@ contract DeployDG is Script { function deployEmergencyExecutionCommittee( address owner, uint256 quorum, - address[] memory members + address[] memory members, + address timelock ) internal returns (EmergencyExecutionCommittee) { return new EmergencyExecutionCommittee(owner, members, quorum, address(timelock)); } - function deployTimelockedGovernance(address governance) internal returns (TimelockedGovernance) { + function deployTimelockedGovernance( + address governance, + EmergencyProtectedTimelock timelock + ) internal returns (TimelockedGovernance) { return new TimelockedGovernance(governance, timelock); } - function deployResealManager() internal returns (ResealManager) { + function deployResealManager(EmergencyProtectedTimelock timelock) internal returns (ResealManager) { return new ResealManager(timelock); } - function deployDualGovernanceConfigProvider() internal returns (ImmutableDualGovernanceConfigProvider) { + function deployDualGovernanceConfigProvider(ConfigValues memory dgDeployConfig) + internal + returns (ImmutableDualGovernanceConfigProvider) + { return new ImmutableDualGovernanceConfigProvider( DualGovernanceConfig.Context({ firstSealRageQuitSupport: dgDeployConfig.FIRST_SEAL_RAGE_QUIT_SUPPORT, @@ -271,7 +312,13 @@ contract DeployDG is Script { ); } - function deployDualGovernance(IDualGovernanceConfigProvider configProvider) internal returns (DualGovernance) { + function deployDualGovernance( + IDualGovernanceConfigProvider configProvider, + EmergencyProtectedTimelock timelock, + ResealManager resealManager, + ConfigValues memory dgDeployConfig, + LidoAddresses memory lidoAddresses + ) internal returns (DualGovernance) { return new DualGovernance({ dependencies: DualGovernance.ExternalDependencies({ stETH: lidoAddresses.stETH, @@ -290,11 +337,19 @@ contract DeployDG is Script { }); } - function deployEmptyTiebreakerCoreCommittee(address owner, Duration _timelock) internal returns (TiebreakerCore) { - return new TiebreakerCore({owner: owner, dualGovernance: address(dualGovernance), timelock: _timelock}); + function deployEmptyTiebreakerCoreCommittee( + address owner, + address dualGovernance, + Duration executionDelay + ) internal returns (TiebreakerCore) { + return new TiebreakerCore({owner: owner, dualGovernance: dualGovernance, timelock: executionDelay}); } - function deployTiebreakerSubCommittees() internal { + function deployTiebreakerSubCommittees( + address owner, + TiebreakerCore tiebreakerCoreCommittee, + ConfigValues memory dgDeployConfig + ) internal returns (address[] memory) { address[] memory coreCommitteeMembers = new address[](dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT); for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { @@ -308,40 +363,54 @@ contract DeployDG is Script { quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_QUORUM; members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS; } - - tiebreakerSubCommittees.push( - deployTiebreakerSubCommittee({owner: address(adminExecutor), quorum: quorum, members: members}) + coreCommitteeMembers[i] = address( + deployTiebreakerSubCommittee({ + owner: owner, + quorum: quorum, + members: members, + tiebreakerCoreCommittee: address(tiebreakerCoreCommittee) + }) ); - coreCommitteeMembers[i] = address(tiebreakerSubCommittees[i]); } + // TODO: configurable quorum? tiebreakerCoreCommittee.addMembers(coreCommitteeMembers, coreCommitteeMembers.length); + + return coreCommitteeMembers; } function deployTiebreakerSubCommittee( address owner, uint256 quorum, - address[] memory members + address[] memory members, + address tiebreakerCoreCommittee ) internal returns (TiebreakerSubCommittee) { return new TiebreakerSubCommittee({ owner: owner, executionQuorum: quorum, committeeMembers: members, - tiebreakerCore: address(tiebreakerCoreCommittee) + tiebreakerCore: tiebreakerCoreCommittee }); } - function deployResealCommittee() internal returns (ResealCommittee) { + function deployResealCommittee( + address adminExecutor, + address dualGovernance, + ConfigValues memory dgDeployConfig + ) internal returns (ResealCommittee) { uint256 quorum = dgDeployConfig.RESEAL_COMMITTEE_QUORUM; address[] memory committeeMembers = dgDeployConfig.RESEAL_COMMITTEE_MEMBERS; // TODO: Don't we need to use non-zero timelock here? - return new ResealCommittee( - address(adminExecutor), committeeMembers, quorum, address(dualGovernance), Durations.from(0) - ); + return new ResealCommittee(adminExecutor, committeeMembers, quorum, dualGovernance, Durations.from(0)); } - function finalizeEmergencyProtectedTimelockDeploy() internal { + function finalizeEmergencyProtectedTimelockDeploy( + Executor adminExecutor, + EmergencyProtectedTimelock timelock, + address dualGovernance, + ConfigValues memory dgDeployConfig + ) internal { adminExecutor.execute( address(timelock), 0, @@ -349,23 +418,27 @@ contract DeployDG is Script { timelock.setupDelays, (dgDeployConfig.AFTER_SUBMIT_DELAY, dgDeployConfig.AFTER_SCHEDULE_DELAY) ) ); - adminExecutor.execute(address(timelock), 0, abi.encodeCall(timelock.setGovernance, (address(dualGovernance)))); + adminExecutor.execute(address(timelock), 0, abi.encodeCall(timelock.setGovernance, (dualGovernance))); adminExecutor.transferOwnership(address(timelock)); } - function getDeployedAddresses() internal view returns (DeployValidation.DeployResult memory) { + function getDeployedAddresses(DeployedContracts memory contracts) + internal + pure + returns (DeployValidation.DeployResult memory) + { return DeployValidation.DeployResult({ - adminExecutor: payable(address(adminExecutor)), - timelock: address(timelock), - emergencyGovernance: address(emergencyGovernance), - emergencyActivationCommittee: address(emergencyActivationCommittee), - emergencyExecutionCommittee: address(emergencyExecutionCommittee), - resealManager: address(resealManager), - dualGovernance: address(dualGovernance), - resealCommittee: address(resealCommittee), - tiebreakerCoreCommittee: address(tiebreakerCoreCommittee), - tiebreakerSubCommittee1: address(tiebreakerSubCommittees[0]), - tiebreakerSubCommittee2: address(tiebreakerSubCommittees[1]) + adminExecutor: payable(address(contracts.adminExecutor)), + timelock: address(contracts.timelock), + emergencyGovernance: address(contracts.emergencyGovernance), + emergencyActivationCommittee: address(contracts.emergencyActivationCommittee), + emergencyExecutionCommittee: address(contracts.emergencyExecutionCommittee), + resealManager: address(contracts.resealManager), + dualGovernance: address(contracts.dualGovernance), + resealCommittee: address(contracts.resealCommittee), + tiebreakerCoreCommittee: address(contracts.tiebreakerCoreCommittee), + tiebreakerSubCommittee1: contracts.tiebreakerSubCommittee1, + tiebreakerSubCommittee2: contracts.tiebreakerSubCommittee2 }); } } diff --git a/scripts/deploy/DeployValidation.sol b/scripts/deploy/DeployValidation.sol index da8de732..54087495 100644 --- a/scripts/deploy/DeployValidation.sol +++ b/scripts/deploy/DeployValidation.sol @@ -17,7 +17,7 @@ import {DualGovernance} from "contracts/DualGovernance.sol"; import {Escrow} from "contracts/Escrow.sol"; import {DualGovernanceConfig} from "contracts/libraries/DualGovernanceConfig.sol"; import {State} from "contracts/libraries/DualGovernanceStateMachine.sol"; -import {DGDeployConfig, LidoAddresses, ConfigValues} from "./Config.s.sol"; +import {DGDeployConfig, ConfigValues, LidoAddresses, getLidoAddresses} from "./Config.s.sol"; library DeployValidation { struct DeployResult { @@ -37,12 +37,12 @@ library DeployValidation { function check(DeployResult memory res) internal { DGDeployConfig configProvider = new DGDeployConfig(); ConfigValues memory dgDeployConfig = configProvider.loadAndValidate(); - LidoAddresses memory lidoAddresses = configProvider.lidoAddresses(dgDeployConfig); + LidoAddresses memory lidoAddresses = getLidoAddresses(dgDeployConfig); checkAdminExecutor(res.adminExecutor, res.timelock); checkTimelock(res, dgDeployConfig); - checkEmergencyActivationCommittee(res.emergencyActivationCommittee, res.adminExecutor, dgDeployConfig); - checkEmergencyExecutionCommittee(res.emergencyExecutionCommittee, res.adminExecutor, dgDeployConfig); + checkEmergencyActivationCommittee(res, dgDeployConfig); + checkEmergencyExecutionCommittee(res, dgDeployConfig); checkTimelockedGovernance(res, lidoAddresses); checkResealManager(res); checkDualGovernance(res, dgDeployConfig, lidoAddresses); @@ -107,6 +107,10 @@ library DeployValidation { timelockInstance.getAfterSubmitDelay() == dgDeployConfig.AFTER_SUBMIT_DELAY, "Incorrect parameter AFTER_SUBMIT_DELAY" ); + require( + timelockInstance.getAfterScheduleDelay() == dgDeployConfig.AFTER_SCHEDULE_DELAY, + "Incorrect parameter AFTER_SCHEDULE_DELAY" + ); require( timelockInstance.getGovernance() == res.dualGovernance, "Incorrect governance address in EmergencyProtectedTimelock" @@ -114,12 +118,15 @@ library DeployValidation { } function checkEmergencyActivationCommittee( - address emergencyActivationCommittee, - address adminExecutor, + DeployResult memory res, ConfigValues memory dgDeployConfig ) internal view { - EmergencyActivationCommittee committee = EmergencyActivationCommittee(emergencyActivationCommittee); - require(committee.owner() == adminExecutor, "EmergencyActivationCommittee owner != adminExecutor"); + EmergencyActivationCommittee committee = EmergencyActivationCommittee(res.emergencyActivationCommittee); + require(committee.owner() == res.adminExecutor, "EmergencyActivationCommittee owner != adminExecutor"); + require( + committee.EMERGENCY_PROTECTED_TIMELOCK() == res.timelock, + "EmergencyActivationCommittee EMERGENCY_PROTECTED_TIMELOCK != timelock" + ); for (uint256 i = 0; i < dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS.length; ++i) { require( @@ -134,12 +141,15 @@ library DeployValidation { } function checkEmergencyExecutionCommittee( - address emergencyExecutionCommittee, - address adminExecutor, + DeployResult memory res, ConfigValues memory dgDeployConfig ) internal view { - EmergencyExecutionCommittee committee = EmergencyExecutionCommittee(emergencyExecutionCommittee); - require(committee.owner() == adminExecutor, "EmergencyExecutionCommittee owner != adminExecutor"); + EmergencyExecutionCommittee committee = EmergencyExecutionCommittee(res.emergencyExecutionCommittee); + require(committee.owner() == res.adminExecutor, "EmergencyExecutionCommittee owner != adminExecutor"); + require( + committee.EMERGENCY_PROTECTED_TIMELOCK() == res.timelock, + "EmergencyExecutionCommittee EMERGENCY_PROTECTED_TIMELOCK != timelock" + ); for (uint256 i = 0; i < dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS.length; ++i) { require( @@ -273,6 +283,20 @@ library DeployValidation { ); require(dg.getCurrentState() == State.Normal, "Incorrect DualGovernance state"); + require(dg.getProposers().length == 1, "Incorrect amount of proposers"); + require(dg.isProposer(address(lidoAddresses.voting)) == true, "Lido voting is not set as a proposers[0]"); + + DualGovernance.TiebreakerState memory ts = dg.getTiebreakerState(); + require( + ts.tiebreakerActivationTimeout == dgDeployConfig.TIEBREAKER_ACTIVATION_TIMEOUT, + "Incorrect parameter TIEBREAKER_ACTIVATION_TIMEOUT" + ); + require(ts.tiebreakerCommittee == res.tiebreakerCoreCommittee, "Incorrect tiebreakerCoreCommittee"); + require(ts.sealableWithdrawalBlockers.length == 1, "Incorrect amount of sealableWithdrawalBlockers"); + require( + ts.sealableWithdrawalBlockers[0] == address(lidoAddresses.withdrawalQueue), + "Lido withdrawalQueue is not set as a sealableWithdrawalBlockers[0]" + ); } function checkTiebreakerCoreCommittee(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { @@ -327,6 +351,7 @@ library DeployValidation { ResealCommittee rc = ResealCommittee(res.resealCommittee); require(rc.owner() == res.adminExecutor, "ResealCommittee owner != adminExecutor"); require(rc.timelockDuration() == Durations.from(0), "ResealCommittee timelock should be 0"); // TODO: is it correct? + require(rc.DUAL_GOVERNANCE() == res.dualGovernance, "Incorrect dualGovernance in ResealCommittee"); for (uint256 i = 0; i < dgDeployConfig.RESEAL_COMMITTEE_MEMBERS.length; ++i) { require( From b43b8f47bcdbe10fdc5b1cc3feed707f12dde206 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 11 Sep 2024 00:01:29 +0400 Subject: [PATCH 015/107] Various improvements --- .../interfaces/IAragonVoting.sol | 0 scripts/deploy/Config.s.sol | 371 +++++++++------ scripts/deploy/Deploy.s.sol | 441 ++---------------- scripts/deploy/DeployConfig.sol | 119 +++++ scripts/deploy/DeployContracts.sol | 371 +++++++++++++++ scripts/deploy/DeployValidation.sol | 88 ++-- test/utils/lido-utils.sol | 2 +- 7 files changed, 812 insertions(+), 580 deletions(-) rename {test/utils => contracts}/interfaces/IAragonVoting.sol (100%) create mode 100644 scripts/deploy/DeployConfig.sol create mode 100644 scripts/deploy/DeployContracts.sol diff --git a/test/utils/interfaces/IAragonVoting.sol b/contracts/interfaces/IAragonVoting.sol similarity index 100% rename from test/utils/interfaces/IAragonVoting.sol rename to contracts/interfaces/IAragonVoting.sol diff --git a/scripts/deploy/Config.s.sol b/scripts/deploy/Config.s.sol index fec93c9c..25392b1a 100644 --- a/scripts/deploy/Config.s.sol +++ b/scripts/deploy/Config.s.sol @@ -8,7 +8,7 @@ import {console} from "forge-std/console.sol"; import {IStETH} from "contracts/interfaces/IStETH.sol"; import {IWstETH} from "contracts/interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; -import {IAragonVoting} from "test/utils/interfaces/IAragonVoting.sol"; // TODO: move to a proper location +import {IAragonVoting} from "contracts/interfaces/IAragonVoting.sol"; import { ST_ETH as MAINNET_ST_ETH, WST_ETH as MAINNET_WST_ETH, @@ -21,66 +21,19 @@ import { WITHDRAWAL_QUEUE as HOLESKY_WITHDRAWAL_QUEUE, DAO_VOTING as HOLESKY_DAO_VOTING } from "addresses/holesky-addresses.sol"; -import {Durations, Duration} from "contracts/types/Duration.sol"; -import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; +import {Durations} from "contracts/types/Duration.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; +import {DeployConfig, LidoContracts} from "./DeployConfig.sol"; string constant ARRAY_SEPARATOR = ","; bytes32 constant CHAIN_NAME_MAINNET_HASH = keccak256(bytes("mainnet")); bytes32 constant CHAIN_NAME_HOLESKY_HASH = keccak256(bytes("holesky")); +// TODO: implement "holesky-mocks" -struct ConfigValues { - string CHAIN; - uint256 DEPLOYER_PRIVATE_KEY; - Duration AFTER_SUBMIT_DELAY; - Duration MAX_AFTER_SUBMIT_DELAY; - Duration AFTER_SCHEDULE_DELAY; - Duration MAX_AFTER_SCHEDULE_DELAY; - Duration EMERGENCY_MODE_DURATION; - Duration MAX_EMERGENCY_MODE_DURATION; - Duration EMERGENCY_PROTECTION_DURATION; - Duration MAX_EMERGENCY_PROTECTION_DURATION; - uint256 EMERGENCY_ACTIVATION_COMMITTEE_QUORUM; - address[] EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS; - uint256 EMERGENCY_EXECUTION_COMMITTEE_QUORUM; - address[] EMERGENCY_EXECUTION_COMMITTEE_MEMBERS; - uint256 TIEBREAKER_CORE_QUORUM; - Duration TIEBREAKER_EXECUTION_DELAY; - uint256 TIEBREAKER_SUB_COMMITTEES_COUNT; - address[] TIEBREAKER_SUB_COMMITTEE_1_MEMBERS; - uint256 TIEBREAKER_SUB_COMMITTEE_1_QUORUM; - address[] TIEBREAKER_SUB_COMMITTEE_2_MEMBERS; - uint256 TIEBREAKER_SUB_COMMITTEE_2_QUORUM; - address[] RESEAL_COMMITTEE_MEMBERS; - uint256 RESEAL_COMMITTEE_QUORUM; - uint256 MIN_WITHDRAWALS_BATCH_SIZE; - Duration MIN_TIEBREAKER_ACTIVATION_TIMEOUT; - Duration TIEBREAKER_ACTIVATION_TIMEOUT; - Duration MAX_TIEBREAKER_ACTIVATION_TIMEOUT; - uint256 MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT; - PercentD16 FIRST_SEAL_RAGE_QUIT_SUPPORT; - PercentD16 SECOND_SEAL_RAGE_QUIT_SUPPORT; - Duration MIN_ASSETS_LOCK_DURATION; - Duration DYNAMIC_TIMELOCK_MIN_DURATION; - Duration DYNAMIC_TIMELOCK_MAX_DURATION; - Duration VETO_SIGNALLING_MIN_ACTIVE_DURATION; - Duration VETO_SIGNALLING_DEACTIVATION_MAX_DURATION; - Duration VETO_COOLDOWN_DURATION; - Duration RAGE_QUIT_EXTENSION_DELAY; - Duration RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK; - uint256 RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER; - uint256[3] RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS; -} - -struct LidoAddresses { - IStETH stETH; - IWstETH wstETH; - IWithdrawalQueue withdrawalQueue; - IAragonVoting voting; -} - -function getLidoAddresses(ConfigValues memory config) pure returns (LidoAddresses memory) { +function getLidoAddresses(DeployConfig memory config) pure returns (LidoContracts memory) { if (keccak256(bytes(config.CHAIN)) == CHAIN_NAME_MAINNET_HASH) { - return LidoAddresses({ + return LidoContracts({ + chainId: 1, stETH: IStETH(MAINNET_ST_ETH), wstETH: IWstETH(MAINNET_WST_ETH), withdrawalQueue: IWithdrawalQueue(MAINNET_WITHDRAWAL_QUEUE), @@ -88,7 +41,8 @@ function getLidoAddresses(ConfigValues memory config) pure returns (LidoAddresse }); } - return LidoAddresses({ + return LidoContracts({ + chainId: 17000, stETH: IStETH(HOLESKY_ST_ETH), wstETH: IWstETH(HOLESKY_WST_ETH), withdrawalQueue: IWithdrawalQueue(HOLESKY_WITHDRAWAL_QUEUE), @@ -96,9 +50,12 @@ function getLidoAddresses(ConfigValues memory config) pure returns (LidoAddresse }); } -contract DGDeployConfig is Script { +// TODO: rename to EnvConfig + +contract DGDeployConfigProvider is Script { error InvalidRageQuitETHWithdrawalsTimelockGrowthCoeffs(uint256[] coeffs); error InvalidQuorum(string committee, uint256 quorum); + error InvalidParameter(string parameter); error InvalidChain(string chainName); uint256 internal immutable DEFAULT_AFTER_SUBMIT_DELAY = 3 days; @@ -114,8 +71,6 @@ contract DGDeployConfig is Script { uint256 internal immutable DEFAULT_TIEBREAKER_CORE_QUORUM = 1; uint256 internal immutable DEFAULT_TIEBREAKER_EXECUTION_DELAY = 30 days; uint256 internal immutable DEFAULT_TIEBREAKER_SUB_COMMITTEES_COUNT = 2; - uint256 internal immutable DEFAULT_TIEBREAKER_SUB_COMMITTEE_1_QUORUM = 5; - uint256 internal immutable DEFAULT_TIEBREAKER_SUB_COMMITTEE_2_QUORUM = 5; uint256 internal immutable DEFAULT_RESEAL_COMMITTEE_QUORUM = 3; uint256 internal immutable DEFAULT_MIN_WITHDRAWALS_BATCH_SIZE = 4; uint256 internal immutable DEFAULT_MIN_TIEBREAKER_ACTIVATION_TIMEOUT = 90 days; @@ -138,15 +93,14 @@ contract DGDeployConfig is Script { constructor() { // TODO: are these values correct as a default? - DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[0] = 0; // TODO: set to 1 ? + DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[0] = 1; DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[1] = 0; DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[2] = 0; } - function loadAndValidate() external returns (ConfigValues memory config) { - config = ConfigValues({ + function loadAndValidate() external returns (DeployConfig memory config) { + config = DeployConfig({ CHAIN: vm.envString("CHAIN"), - DEPLOYER_PRIVATE_KEY: vm.envUint("DEPLOYER_PRIVATE_KEY"), AFTER_SUBMIT_DELAY: Durations.from(vm.envOr("AFTER_SUBMIT_DELAY", DEFAULT_AFTER_SUBMIT_DELAY)), MAX_AFTER_SUBMIT_DELAY: Durations.from(vm.envOr("MAX_AFTER_SUBMIT_DELAY", DEFAULT_MAX_AFTER_SUBMIT_DELAY)), AFTER_SCHEDULE_DELAY: Durations.from(vm.envOr("AFTER_SCHEDULE_DELAY", DEFAULT_AFTER_SCHEDULE_DELAY)), @@ -169,21 +123,42 @@ contract DGDeployConfig is Script { "EMERGENCY_EXECUTION_COMMITTEE_QUORUM", DEFAULT_EMERGENCY_EXECUTION_COMMITTEE_QUORUM ), EMERGENCY_EXECUTION_COMMITTEE_MEMBERS: vm.envAddress("EMERGENCY_EXECUTION_COMMITTEE_MEMBERS", ARRAY_SEPARATOR), - // TODO: Do we need to configure this? - TIEBREAKER_CORE_QUORUM: DEFAULT_TIEBREAKER_CORE_QUORUM, + TIEBREAKER_CORE_QUORUM: vm.envOr("TIEBREAKER_CORE_QUORUM", DEFAULT_TIEBREAKER_CORE_QUORUM), TIEBREAKER_EXECUTION_DELAY: Durations.from( vm.envOr("TIEBREAKER_EXECUTION_DELAY", DEFAULT_TIEBREAKER_EXECUTION_DELAY) ), - // TODO: Do we need to configure this? - TIEBREAKER_SUB_COMMITTEES_COUNT: DEFAULT_TIEBREAKER_SUB_COMMITTEES_COUNT, + TIEBREAKER_SUB_COMMITTEES_COUNT: vm.envOr( + "TIEBREAKER_SUB_COMMITTEES_COUNT", DEFAULT_TIEBREAKER_SUB_COMMITTEES_COUNT + ), TIEBREAKER_SUB_COMMITTEE_1_MEMBERS: vm.envAddress("TIEBREAKER_SUB_COMMITTEE_1_MEMBERS", ARRAY_SEPARATOR), - TIEBREAKER_SUB_COMMITTEE_1_QUORUM: vm.envOr( - "TIEBREAKER_SUB_COMMITTEE_1_QUORUM", DEFAULT_TIEBREAKER_SUB_COMMITTEE_1_QUORUM + TIEBREAKER_SUB_COMMITTEE_2_MEMBERS: vm.envOr( + "TIEBREAKER_SUB_COMMITTEE_2_MEMBERS", ARRAY_SEPARATOR, new address[](0) + ), + TIEBREAKER_SUB_COMMITTEE_3_MEMBERS: vm.envOr( + "TIEBREAKER_SUB_COMMITTEE_3_MEMBERS", ARRAY_SEPARATOR, new address[](0) + ), + TIEBREAKER_SUB_COMMITTEE_4_MEMBERS: vm.envOr( + "TIEBREAKER_SUB_COMMITTEE_4_MEMBERS", ARRAY_SEPARATOR, new address[](0) + ), + TIEBREAKER_SUB_COMMITTEE_5_MEMBERS: vm.envOr( + "TIEBREAKER_SUB_COMMITTEE_5_MEMBERS", ARRAY_SEPARATOR, new address[](0) ), - TIEBREAKER_SUB_COMMITTEE_2_MEMBERS: vm.envAddress("TIEBREAKER_SUB_COMMITTEE_2_MEMBERS", ARRAY_SEPARATOR), - TIEBREAKER_SUB_COMMITTEE_2_QUORUM: vm.envOr( - "TIEBREAKER_SUB_COMMITTEE_2_QUORUM", DEFAULT_TIEBREAKER_SUB_COMMITTEE_2_QUORUM + TIEBREAKER_SUB_COMMITTEE_6_MEMBERS: vm.envOr( + "TIEBREAKER_SUB_COMMITTEE_6_MEMBERS", ARRAY_SEPARATOR, new address[](0) ), + TIEBREAKER_SUB_COMMITTEE_7_MEMBERS: vm.envOr( + "TIEBREAKER_SUB_COMMITTEE_7_MEMBERS", ARRAY_SEPARATOR, new address[](0) + ), + TIEBREAKER_SUB_COMMITTEE_8_MEMBERS: vm.envOr( + "TIEBREAKER_SUB_COMMITTEE_8_MEMBERS", ARRAY_SEPARATOR, new address[](0) + ), + TIEBREAKER_SUB_COMMITTEE_9_MEMBERS: vm.envOr( + "TIEBREAKER_SUB_COMMITTEE_9_MEMBERS", ARRAY_SEPARATOR, new address[](0) + ), + TIEBREAKER_SUB_COMMITTEE_10_MEMBERS: vm.envOr( + "TIEBREAKER_SUB_COMMITTEE_10_MEMBERS", ARRAY_SEPARATOR, new address[](0) + ), + TIEBREAKER_SUB_COMMITTEES_QUORUMS: vm.envUint("TIEBREAKER_SUB_COMMITTEES_QUORUMS", ARRAY_SEPARATOR), RESEAL_COMMITTEE_MEMBERS: vm.envAddress("RESEAL_COMMITTEE_MEMBERS", ARRAY_SEPARATOR), RESEAL_COMMITTEE_QUORUM: vm.envOr("RESEAL_COMMITTEE_QUORUM", DEFAULT_RESEAL_COMMITTEE_QUORUM), MIN_WITHDRAWALS_BATCH_SIZE: vm.envOr("MIN_WITHDRAWALS_BATCH_SIZE", DEFAULT_MIN_WITHDRAWALS_BATCH_SIZE), @@ -253,45 +228,115 @@ contract DGDeployConfig is Script { coeffs[2] = coeffsRaw[2]; } - function validateConfig(ConfigValues memory config) internal pure { + function validateConfig(DeployConfig memory config) internal pure { bytes32 chainNameHash = keccak256(bytes(config.CHAIN)); if (chainNameHash != CHAIN_NAME_MAINNET_HASH && chainNameHash != CHAIN_NAME_HOLESKY_HASH) { revert InvalidChain(config.CHAIN); } + checkCommitteeQuorum( + config.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS, + config.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, + "EMERGENCY_ACTIVATION_COMMITTEE" + ); + checkCommitteeQuorum( + config.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS, + config.EMERGENCY_EXECUTION_COMMITTEE_QUORUM, + "EMERGENCY_EXECUTION_COMMITTEE" + ); + + checkCommitteeQuorum(config.RESEAL_COMMITTEE_MEMBERS, config.RESEAL_COMMITTEE_QUORUM, "RESEAL_COMMITTEE"); + if ( - config.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM == 0 - || config.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM > config.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS.length + config.TIEBREAKER_CORE_QUORUM == 0 || config.TIEBREAKER_CORE_QUORUM > config.TIEBREAKER_SUB_COMMITTEES_COUNT ) { - revert InvalidQuorum("EMERGENCY_ACTIVATION_COMMITTEE", config.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM); + revert InvalidQuorum("TIEBREAKER_CORE", config.TIEBREAKER_CORE_QUORUM); } - if ( - config.EMERGENCY_EXECUTION_COMMITTEE_QUORUM == 0 - || config.EMERGENCY_EXECUTION_COMMITTEE_QUORUM > config.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS.length - ) { - revert InvalidQuorum("EMERGENCY_EXECUTION_COMMITTEE", config.EMERGENCY_EXECUTION_COMMITTEE_QUORUM); + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT == 0 || config.TIEBREAKER_SUB_COMMITTEES_COUNT > 10) { + revert InvalidParameter("TIEBREAKER_SUB_COMMITTEES_COUNT"); } - if ( - config.TIEBREAKER_SUB_COMMITTEE_1_QUORUM == 0 - || config.TIEBREAKER_SUB_COMMITTEE_1_QUORUM > config.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS.length - ) { - revert InvalidQuorum("TIEBREAKER_SUB_COMMITTEE_1", config.TIEBREAKER_SUB_COMMITTEE_1_QUORUM); + if (config.TIEBREAKER_SUB_COMMITTEES_QUORUMS.length != config.TIEBREAKER_SUB_COMMITTEES_COUNT) { + revert InvalidParameter("TIEBREAKER_SUB_COMMITTEES_QUORUMS"); } - if ( - config.TIEBREAKER_SUB_COMMITTEE_2_QUORUM == 0 - || config.TIEBREAKER_SUB_COMMITTEE_2_QUORUM > config.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS.length - ) { - revert InvalidQuorum("TIEBREAKER_SUB_COMMITTEE_2", config.TIEBREAKER_SUB_COMMITTEE_2_QUORUM); + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[0], + "TIEBREAKER_SUB_COMMITTEE_1" + ); + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 2) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[1], + "TIEBREAKER_SUB_COMMITTEE_2" + ); } - if ( - config.RESEAL_COMMITTEE_QUORUM == 0 - || config.RESEAL_COMMITTEE_QUORUM > config.RESEAL_COMMITTEE_MEMBERS.length - ) { - revert InvalidQuorum("RESEAL_COMMITTEE", config.RESEAL_COMMITTEE_QUORUM); + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 3) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_3_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[2], + "TIEBREAKER_SUB_COMMITTEE_3" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 4) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_4_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[3], + "TIEBREAKER_SUB_COMMITTEE_4" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 5) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_5_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[4], + "TIEBREAKER_SUB_COMMITTEE_5" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 6) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_6_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[5], + "TIEBREAKER_SUB_COMMITTEE_6" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 7) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_7_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[6], + "TIEBREAKER_SUB_COMMITTEE_7" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 8) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_8_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[7], + "TIEBREAKER_SUB_COMMITTEE_8" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 9) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_9_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[8], + "TIEBREAKER_SUB_COMMITTEE_9" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT == 10) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_10_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[9], + "TIEBREAKER_SUB_COMMITTEE_10" + ); } // TODO: AFTER_SUBMIT_DELAY <= MAX_AFTER_SUBMIT_DELAY @@ -301,59 +346,117 @@ contract DGDeployConfig is Script { // TODO: DYNAMIC_TIMELOCK_MIN_DURATION <= DYNAMIC_TIMELOCK_MAX_DURATION } - function printCommittees(ConfigValues memory config) internal view { + function checkCommitteeQuorum(address[] memory committee, uint256 quorum, string memory message) internal pure { + if (quorum == 0 || quorum > committee.length) { + revert InvalidQuorum(message, quorum); + } + } + + function printCommittees(DeployConfig memory config) internal view { console.log("================================================="); console.log("Loaded valid config with the following committees:"); - console.log( - "EmergencyActivationCommittee members, quorum", + printCommittee( + config.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS, config.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, - "of", - config.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS.length + "EmergencyActivationCommittee members, quorum" ); - for (uint256 k = 0; k < config.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS.length; ++k) { - console.log(">> #", k, address(config.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS[k])); - } - console.log( - "EmergencyExecutionCommittee members, quorum", + printCommittee( + config.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS, config.EMERGENCY_EXECUTION_COMMITTEE_QUORUM, - "of", - config.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS.length + "EmergencyExecutionCommittee members, quorum" ); - for (uint256 k = 0; k < config.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS.length; ++k) { - console.log(">> #", k, address(config.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS[k])); - } - console.log( - "TiebreakerSubCommittee #1 members, quorum", - config.TIEBREAKER_SUB_COMMITTEE_1_QUORUM, - "of", - config.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS.length + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[0], + "TiebreakerSubCommittee #1 members, quorum" ); - for (uint256 k = 0; k < config.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS.length; ++k) { - console.log(">> #", k, address(config.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS[k])); + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 2) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[1], + "TiebreakerSubCommittee #2 members, quorum" + ); } - console.log( - "TiebreakerSubCommittee #2 members, quorum", - config.TIEBREAKER_SUB_COMMITTEE_2_QUORUM, - "of", - config.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS.length - ); - for (uint256 k = 0; k < config.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS.length; ++k) { - console.log(">> #", k, address(config.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS[k])); + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 3) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_3_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[2], + "TiebreakerSubCommittee #3 members, quorum" + ); } - console.log( - "ResealCommittee members, quorum", - config.RESEAL_COMMITTEE_QUORUM, - "of", - config.RESEAL_COMMITTEE_MEMBERS.length - ); - for (uint256 k = 0; k < config.RESEAL_COMMITTEE_MEMBERS.length; ++k) { - console.log(">> #", k, address(config.RESEAL_COMMITTEE_MEMBERS[k])); + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 4) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_4_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[3], + "TiebreakerSubCommittee #4 members, quorum" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 5) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_5_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[4], + "TiebreakerSubCommittee #5 members, quorum" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 6) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_6_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[5], + "TiebreakerSubCommittee #6 members, quorum" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 7) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_7_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[6], + "TiebreakerSubCommittee #7 members, quorum" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 8) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_8_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[7], + "TiebreakerSubCommittee #8 members, quorum" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 9) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_9_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[8], + "TiebreakerSubCommittee #9 members, quorum" + ); } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT == 10) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_10_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[9], + "TiebreakerSubCommittee #10 members, quorum" + ); + } + + printCommittee( + config.RESEAL_COMMITTEE_MEMBERS, config.RESEAL_COMMITTEE_QUORUM, "ResealCommittee members, quorum" + ); + console.log("================================================="); } + + function printCommittee(address[] memory committee, uint256 quorum, string memory message) internal view { + console.log(message, quorum, "of", committee.length); + for (uint256 k = 0; k < committee.length; ++k) { + console.log(">> #", k, address(committee[k])); + } + } } diff --git a/scripts/deploy/Deploy.s.sol b/scripts/deploy/Deploy.s.sol index 9c35cdf6..30d1ee25 100644 --- a/scripts/deploy/Deploy.s.sol +++ b/scripts/deploy/Deploy.s.sol @@ -6,83 +6,48 @@ pragma solidity 0.8.26; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/console.sol"; -// --- -// Contracts -// --- -import {Timestamps} from "contracts/types/Timestamp.sol"; -import {Duration, Durations} from "contracts/types/Duration.sol"; - -import {Executor} from "contracts/Executor.sol"; -import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; - -import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; -import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; - -import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; - -import {ResealManager} from "contracts/ResealManager.sol"; -import {DualGovernance} from "contracts/DualGovernance.sol"; -import { - DualGovernanceConfig, - IDualGovernanceConfigProvider, - ImmutableDualGovernanceConfigProvider -} from "contracts/DualGovernanceConfigProvider.sol"; - -import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; -import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; -import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; - -import {DGDeployConfig, ConfigValues, LidoAddresses, getLidoAddresses} from "./Config.s.sol"; +import {DeployConfig} from "./DeployConfig.sol"; +import {DGDeployConfigProvider} from "./Config.s.sol"; +import {DeployDGContracts, DeployedContracts} from "./DeployContracts.sol"; import {DeployValidation} from "./DeployValidation.sol"; -struct DeployedContracts { - Executor adminExecutor; - EmergencyProtectedTimelock timelock; - TimelockedGovernance emergencyGovernance; - EmergencyActivationCommittee emergencyActivationCommittee; - EmergencyExecutionCommittee emergencyExecutionCommittee; - ResealManager resealManager; - DualGovernance dualGovernance; - ResealCommittee resealCommittee; - TiebreakerCore tiebreakerCoreCommittee; - address tiebreakerSubCommittee1; - address tiebreakerSubCommittee2; -} - contract DeployDG is Script { using DeployValidation for DeployValidation.DeployResult; - DeployedContracts private _contracts; + error ChainIdMismatch(uint256 actual, uint256 expected); + + DeployConfig internal config; + address private deployer; + uint256 private pk; + string private chainName; + uint256 private chainId; - address internal deployer; + /* TODO: constructor(string memory _chainName, uint256 _chainId) { + chainName = _chainName; + chainId = _chainId; + } */ function run() external { - DGDeployConfig configProvider = new DGDeployConfig(); - ConfigValues memory _dgDeployConfig = configProvider.loadAndValidate(); + DGDeployConfigProvider configProvider = new DGDeployConfigProvider(); + config = configProvider.loadAndValidate(); - // TODO: check chain id? + if (config.chainId != block.chainid) { + revert ChainIdMismatch({actual: block.chainid, expected: chainId}); + } + + pk = vm.envUint("DEPLOYER_PRIVATE_KEY"); + deployer = vm.addr(pk); + vm.label(deployer, "DEPLOYER"); - deployer = vm.addr(_dgDeployConfig.DEPLOYER_PRIVATE_KEY); - vm.startBroadcast(_dgDeployConfig.DEPLOYER_PRIVATE_KEY); + vm.startBroadcast(pk); - deployDualGovernanceSetup(_dgDeployConfig); + DeployedContracts memory contracts = DeployDGContracts.deployDualGovernanceSetup(config, deployer); vm.stopBroadcast(); - DeployValidation.DeployResult memory res = getDeployedAddresses(_contracts); + DeployValidation.DeployResult memory res = getDeployedAddresses(contracts); - console.log("DG deployed successfully"); - console.log("DualGovernance address", res.dualGovernance); - console.log("ResealManager address", res.resealManager); - console.log("TiebreakerCoreCommittee address", res.tiebreakerCoreCommittee); - console.log("TiebreakerSubCommittee #1 address", _contracts.tiebreakerSubCommittee1); - console.log("TiebreakerSubCommittee #2 address", _contracts.tiebreakerSubCommittee2); - console.log("AdminExecutor address", res.adminExecutor); - console.log("EmergencyProtectedTimelock address", res.timelock); - console.log("EmergencyGovernance address", res.emergencyGovernance); - console.log("EmergencyActivationCommittee address", res.emergencyActivationCommittee); - console.log("EmergencyExecutionCommittee address", res.emergencyExecutionCommittee); - console.log("ResealCommittee address", res.resealCommittee); + printAddresses(res); console.log("Verifying deploy"); @@ -91,337 +56,6 @@ contract DeployDG is Script { console.log(unicode"Verified ✅"); } - function deployDualGovernanceSetup(ConfigValues memory dgDeployConfig) internal { - LidoAddresses memory lidoAddresses = getLidoAddresses(dgDeployConfig); - _contracts = deployEmergencyProtectedTimelockContracts(lidoAddresses, dgDeployConfig, _contracts); - _contracts.resealManager = deployResealManager(_contracts.timelock); - ImmutableDualGovernanceConfigProvider dualGovernanceConfigProvider = - deployDualGovernanceConfigProvider(dgDeployConfig); - DualGovernance dualGovernance = deployDualGovernance({ - configProvider: dualGovernanceConfigProvider, - timelock: _contracts.timelock, - resealManager: _contracts.resealManager, - dgDeployConfig: dgDeployConfig, - lidoAddresses: lidoAddresses - }); - _contracts.dualGovernance = dualGovernance; - - _contracts.tiebreakerCoreCommittee = deployEmptyTiebreakerCoreCommittee({ - owner: deployer, // temporary set owner to deployer, to add sub committees manually - dualGovernance: address(dualGovernance), - executionDelay: dgDeployConfig.TIEBREAKER_EXECUTION_DELAY - }); - - address[] memory tiebreakerSubCommittees = deployTiebreakerSubCommittees( - address(_contracts.adminExecutor), _contracts.tiebreakerCoreCommittee, dgDeployConfig - ); - _contracts.tiebreakerSubCommittee1 = tiebreakerSubCommittees[0]; - _contracts.tiebreakerSubCommittee2 = tiebreakerSubCommittees[1]; - - _contracts.tiebreakerCoreCommittee.transferOwnership(address(_contracts.adminExecutor)); - - _contracts.resealCommittee = - deployResealCommittee(address(_contracts.adminExecutor), address(dualGovernance), dgDeployConfig); - - // --- - // Finalize Setup - // --- - _contracts.adminExecutor.execute( - address(dualGovernance), - 0, - abi.encodeCall( - dualGovernance.registerProposer, (address(lidoAddresses.voting), address(_contracts.adminExecutor)) - ) - ); - _contracts.adminExecutor.execute( - address(dualGovernance), - 0, - abi.encodeCall(dualGovernance.setTiebreakerActivationTimeout, dgDeployConfig.TIEBREAKER_ACTIVATION_TIMEOUT) - ); - _contracts.adminExecutor.execute( - address(dualGovernance), - 0, - abi.encodeCall(dualGovernance.setTiebreakerCommittee, address(_contracts.tiebreakerCoreCommittee)) - ); - _contracts.adminExecutor.execute( - address(dualGovernance), - 0, - abi.encodeCall( - dualGovernance.addTiebreakerSealableWithdrawalBlocker, address(lidoAddresses.withdrawalQueue) - ) - ); - _contracts.adminExecutor.execute( - address(dualGovernance), - 0, - abi.encodeCall(dualGovernance.setResealCommittee, address(_contracts.resealCommittee)) - ); - - finalizeEmergencyProtectedTimelockDeploy( - _contracts.adminExecutor, _contracts.timelock, address(dualGovernance), dgDeployConfig - ); - - // --- - // TODO: Grant Reseal Manager Roles - // --- - /* vm.startPrank(address(_lido.agent)); - _lido.withdrawalQueue.grantRole( - 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(resealManager) - ); - _lido.withdrawalQueue.grantRole( - 0x2fc10cc8ae19568712f7a176fb4978616a610650813c9d05326c34abb62749c7, address(resealManager) - ); - vm.stopPrank(); */ - } - - function deployEmergencyProtectedTimelockContracts( - LidoAddresses memory lidoAddresses, - ConfigValues memory dgDeployConfig, - DeployedContracts memory contracts - ) internal returns (DeployedContracts memory) { - Executor adminExecutor = deployExecutor(deployer); - EmergencyProtectedTimelock timelock = deployEmergencyProtectedTimelock(address(adminExecutor), dgDeployConfig); - - contracts.adminExecutor = adminExecutor; - contracts.timelock = timelock; - contracts.emergencyActivationCommittee = deployEmergencyActivationCommittee({ - quorum: dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, - members: dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS, - owner: address(adminExecutor), - timelock: address(timelock) - }); - - contracts.emergencyExecutionCommittee = deployEmergencyExecutionCommittee({ - quorum: dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_QUORUM, - members: dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS, - owner: address(adminExecutor), - timelock: address(timelock) - }); - contracts.emergencyGovernance = - deployTimelockedGovernance({governance: address(lidoAddresses.voting), timelock: timelock}); - - adminExecutor.execute( - address(timelock), - 0, - abi.encodeCall( - timelock.setEmergencyProtectionActivationCommittee, (address(contracts.emergencyActivationCommittee)) - ) - ); - adminExecutor.execute( - address(timelock), - 0, - abi.encodeCall( - timelock.setEmergencyProtectionExecutionCommittee, (address(contracts.emergencyExecutionCommittee)) - ) - ); - - // TODO: Do we really need to set it? - adminExecutor.execute( - address(timelock), - 0, - abi.encodeCall( - timelock.setEmergencyProtectionEndDate, - (dgDeployConfig.EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now())) - ) - ); - adminExecutor.execute( - address(timelock), - 0, - abi.encodeCall(timelock.setEmergencyModeDuration, (dgDeployConfig.EMERGENCY_MODE_DURATION)) - ); - - adminExecutor.execute( - address(timelock), - 0, - abi.encodeCall(timelock.setEmergencyGovernance, (address(contracts.emergencyGovernance))) - ); - - return contracts; - } - - function deployExecutor(address owner) internal returns (Executor) { - return new Executor(owner); - } - - function deployEmergencyProtectedTimelock( - address adminExecutor, - ConfigValues memory dgDeployConfig - ) internal returns (EmergencyProtectedTimelock) { - return new EmergencyProtectedTimelock({ - adminExecutor: address(adminExecutor), - sanityCheckParams: EmergencyProtectedTimelock.SanityCheckParams({ - maxAfterSubmitDelay: dgDeployConfig.MAX_AFTER_SUBMIT_DELAY, - maxAfterScheduleDelay: dgDeployConfig.MAX_AFTER_SCHEDULE_DELAY, - maxEmergencyModeDuration: dgDeployConfig.MAX_EMERGENCY_MODE_DURATION, - maxEmergencyProtectionDuration: dgDeployConfig.MAX_EMERGENCY_PROTECTION_DURATION - }) - }); - } - - function deployEmergencyActivationCommittee( - address owner, - uint256 quorum, - address[] memory members, - address timelock - ) internal returns (EmergencyActivationCommittee) { - return new EmergencyActivationCommittee(owner, members, quorum, address(timelock)); - } - - function deployEmergencyExecutionCommittee( - address owner, - uint256 quorum, - address[] memory members, - address timelock - ) internal returns (EmergencyExecutionCommittee) { - return new EmergencyExecutionCommittee(owner, members, quorum, address(timelock)); - } - - function deployTimelockedGovernance( - address governance, - EmergencyProtectedTimelock timelock - ) internal returns (TimelockedGovernance) { - return new TimelockedGovernance(governance, timelock); - } - - function deployResealManager(EmergencyProtectedTimelock timelock) internal returns (ResealManager) { - return new ResealManager(timelock); - } - - function deployDualGovernanceConfigProvider(ConfigValues memory dgDeployConfig) - internal - returns (ImmutableDualGovernanceConfigProvider) - { - return new ImmutableDualGovernanceConfigProvider( - DualGovernanceConfig.Context({ - firstSealRageQuitSupport: dgDeployConfig.FIRST_SEAL_RAGE_QUIT_SUPPORT, - secondSealRageQuitSupport: dgDeployConfig.SECOND_SEAL_RAGE_QUIT_SUPPORT, - // - minAssetsLockDuration: dgDeployConfig.MIN_ASSETS_LOCK_DURATION, - dynamicTimelockMinDuration: dgDeployConfig.DYNAMIC_TIMELOCK_MIN_DURATION, - dynamicTimelockMaxDuration: dgDeployConfig.DYNAMIC_TIMELOCK_MAX_DURATION, - // - vetoSignallingMinActiveDuration: dgDeployConfig.VETO_SIGNALLING_MIN_ACTIVE_DURATION, - vetoSignallingDeactivationMaxDuration: dgDeployConfig.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION, - vetoCooldownDuration: dgDeployConfig.VETO_COOLDOWN_DURATION, - // - rageQuitExtensionDelay: dgDeployConfig.RAGE_QUIT_EXTENSION_DELAY, - rageQuitEthWithdrawalsMinTimelock: dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK, - rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: dgDeployConfig - .RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER, - rageQuitEthWithdrawalsTimelockGrowthCoeffs: dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS - }) - ); - } - - function deployDualGovernance( - IDualGovernanceConfigProvider configProvider, - EmergencyProtectedTimelock timelock, - ResealManager resealManager, - ConfigValues memory dgDeployConfig, - LidoAddresses memory lidoAddresses - ) internal returns (DualGovernance) { - return new DualGovernance({ - dependencies: DualGovernance.ExternalDependencies({ - stETH: lidoAddresses.stETH, - wstETH: lidoAddresses.wstETH, - withdrawalQueue: lidoAddresses.withdrawalQueue, - timelock: timelock, - resealManager: resealManager, - configProvider: configProvider - }), - sanityCheckParams: DualGovernance.SanityCheckParams({ - minWithdrawalsBatchSize: dgDeployConfig.MIN_WITHDRAWALS_BATCH_SIZE, - minTiebreakerActivationTimeout: dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT, - maxTiebreakerActivationTimeout: dgDeployConfig.MAX_TIEBREAKER_ACTIVATION_TIMEOUT, - maxSealableWithdrawalBlockersCount: dgDeployConfig.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT - }) - }); - } - - function deployEmptyTiebreakerCoreCommittee( - address owner, - address dualGovernance, - Duration executionDelay - ) internal returns (TiebreakerCore) { - return new TiebreakerCore({owner: owner, dualGovernance: dualGovernance, timelock: executionDelay}); - } - - function deployTiebreakerSubCommittees( - address owner, - TiebreakerCore tiebreakerCoreCommittee, - ConfigValues memory dgDeployConfig - ) internal returns (address[] memory) { - address[] memory coreCommitteeMembers = new address[](dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT); - - for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { - address[] memory members; - uint256 quorum; - - if (i == 0) { - quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_QUORUM; - members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS; - } else { - quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_QUORUM; - members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS; - } - coreCommitteeMembers[i] = address( - deployTiebreakerSubCommittee({ - owner: owner, - quorum: quorum, - members: members, - tiebreakerCoreCommittee: address(tiebreakerCoreCommittee) - }) - ); - } - - // TODO: configurable quorum? - tiebreakerCoreCommittee.addMembers(coreCommitteeMembers, coreCommitteeMembers.length); - - return coreCommitteeMembers; - } - - function deployTiebreakerSubCommittee( - address owner, - uint256 quorum, - address[] memory members, - address tiebreakerCoreCommittee - ) internal returns (TiebreakerSubCommittee) { - return new TiebreakerSubCommittee({ - owner: owner, - executionQuorum: quorum, - committeeMembers: members, - tiebreakerCore: tiebreakerCoreCommittee - }); - } - - function deployResealCommittee( - address adminExecutor, - address dualGovernance, - ConfigValues memory dgDeployConfig - ) internal returns (ResealCommittee) { - uint256 quorum = dgDeployConfig.RESEAL_COMMITTEE_QUORUM; - address[] memory committeeMembers = dgDeployConfig.RESEAL_COMMITTEE_MEMBERS; - - // TODO: Don't we need to use non-zero timelock here? - return new ResealCommittee(adminExecutor, committeeMembers, quorum, dualGovernance, Durations.from(0)); - } - - function finalizeEmergencyProtectedTimelockDeploy( - Executor adminExecutor, - EmergencyProtectedTimelock timelock, - address dualGovernance, - ConfigValues memory dgDeployConfig - ) internal { - adminExecutor.execute( - address(timelock), - 0, - abi.encodeCall( - timelock.setupDelays, (dgDeployConfig.AFTER_SUBMIT_DELAY, dgDeployConfig.AFTER_SCHEDULE_DELAY) - ) - ); - adminExecutor.execute(address(timelock), 0, abi.encodeCall(timelock.setGovernance, (dualGovernance))); - adminExecutor.transferOwnership(address(timelock)); - } - function getDeployedAddresses(DeployedContracts memory contracts) internal pure @@ -437,8 +71,25 @@ contract DeployDG is Script { dualGovernance: address(contracts.dualGovernance), resealCommittee: address(contracts.resealCommittee), tiebreakerCoreCommittee: address(contracts.tiebreakerCoreCommittee), - tiebreakerSubCommittee1: contracts.tiebreakerSubCommittee1, - tiebreakerSubCommittee2: contracts.tiebreakerSubCommittee2 + tiebreakerSubCommittees: contracts.tiebreakerSubCommittees }); } + + function printAddresses(DeployValidation.DeployResult memory res) internal pure { + console.log("DG deployed successfully"); + console.log("DualGovernance address", res.dualGovernance); + console.log("ResealManager address", res.resealManager); + console.log("TiebreakerCoreCommittee address", res.tiebreakerCoreCommittee); + + for (uint256 i = 0; i < config.TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { + console.log("TiebreakerSubCommittee #", i, "address", contracts.tiebreakerSubCommittees[i]); + } + + console.log("AdminExecutor address", res.adminExecutor); + console.log("EmergencyProtectedTimelock address", res.timelock); + console.log("EmergencyGovernance address", res.emergencyGovernance); + console.log("EmergencyActivationCommittee address", res.emergencyActivationCommittee); + console.log("EmergencyExecutionCommittee address", res.emergencyExecutionCommittee); + console.log("ResealCommittee address", res.resealCommittee); + } } diff --git a/scripts/deploy/DeployConfig.sol b/scripts/deploy/DeployConfig.sol new file mode 100644 index 00000000..9d61a2c8 --- /dev/null +++ b/scripts/deploy/DeployConfig.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/* solhint-disable var-name-mixedcase */ + +import {IStETH} from "contracts/interfaces/IStETH.sol"; +import {IWstETH} from "contracts/interfaces/IWstETH.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {IAragonVoting} from "contracts/interfaces/IAragonVoting.sol"; +import {Duration} from "contracts/types/Duration.sol"; +import {PercentD16} from "contracts/types/PercentD16.sol"; + +struct DeployConfig { + string CHAIN; + Duration AFTER_SUBMIT_DELAY; + Duration MAX_AFTER_SUBMIT_DELAY; + Duration AFTER_SCHEDULE_DELAY; + Duration MAX_AFTER_SCHEDULE_DELAY; + Duration EMERGENCY_MODE_DURATION; + Duration MAX_EMERGENCY_MODE_DURATION; + Duration EMERGENCY_PROTECTION_DURATION; + Duration MAX_EMERGENCY_PROTECTION_DURATION; + uint256 EMERGENCY_ACTIVATION_COMMITTEE_QUORUM; + address[] EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS; + uint256 EMERGENCY_EXECUTION_COMMITTEE_QUORUM; + address[] EMERGENCY_EXECUTION_COMMITTEE_MEMBERS; + uint256 TIEBREAKER_CORE_QUORUM; + Duration TIEBREAKER_EXECUTION_DELAY; + uint256 TIEBREAKER_SUB_COMMITTEES_COUNT; + address[] TIEBREAKER_SUB_COMMITTEE_1_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_2_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_3_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_4_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_5_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_6_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_7_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_8_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_9_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_10_MEMBERS; + uint256[] TIEBREAKER_SUB_COMMITTEES_QUORUMS; + address[] RESEAL_COMMITTEE_MEMBERS; + uint256 RESEAL_COMMITTEE_QUORUM; + uint256 MIN_WITHDRAWALS_BATCH_SIZE; + Duration MIN_TIEBREAKER_ACTIVATION_TIMEOUT; + Duration TIEBREAKER_ACTIVATION_TIMEOUT; + Duration MAX_TIEBREAKER_ACTIVATION_TIMEOUT; + uint256 MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT; + PercentD16 FIRST_SEAL_RAGE_QUIT_SUPPORT; + PercentD16 SECOND_SEAL_RAGE_QUIT_SUPPORT; + Duration MIN_ASSETS_LOCK_DURATION; + Duration DYNAMIC_TIMELOCK_MIN_DURATION; + Duration DYNAMIC_TIMELOCK_MAX_DURATION; + Duration VETO_SIGNALLING_MIN_ACTIVE_DURATION; + Duration VETO_SIGNALLING_DEACTIVATION_MAX_DURATION; + Duration VETO_COOLDOWN_DURATION; + Duration RAGE_QUIT_EXTENSION_DELAY; + Duration RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK; + uint256 RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER; + uint256[3] RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS; +} + +struct LidoContracts { + uint256 chainId; + IStETH stETH; + IWstETH wstETH; + IWithdrawalQueue withdrawalQueue; + IAragonVoting voting; +} + +function getSubCommitteeData( + uint256 index, + DeployConfig memory dgDeployConfig +) pure returns (uint256 quorum, address[] memory members) { + assert(index <= 10); + + if (index == 0) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[0]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS; + } + + if (index == 1) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[1]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS; + } + + if (index == 2) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[2]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_3_MEMBERS; + } + + if (index == 3) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[3]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_4_MEMBERS; + } + if (index == 4) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[4]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_5_MEMBERS; + } + if (index == 5) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[5]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_6_MEMBERS; + } + if (index == 6) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[6]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_7_MEMBERS; + } + if (index == 7) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[7]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_8_MEMBERS; + } + if (index == 8) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[8]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_9_MEMBERS; + } + if (index == 9) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[9]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_10_MEMBERS; + } +} diff --git a/scripts/deploy/DeployContracts.sol b/scripts/deploy/DeployContracts.sol new file mode 100644 index 00000000..9d89cf2f --- /dev/null +++ b/scripts/deploy/DeployContracts.sol @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +// --- +// Contracts +// --- +import {Timestamps} from "contracts/types/Timestamp.sol"; +import {Duration, Durations} from "contracts/types/Duration.sol"; + +import {Executor} from "contracts/Executor.sol"; +import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; + +import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; +import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; + +import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; + +import {ResealManager} from "contracts/ResealManager.sol"; +import {DualGovernance} from "contracts/DualGovernance.sol"; +import { + DualGovernanceConfig, + IDualGovernanceConfigProvider, + ImmutableDualGovernanceConfigProvider +} from "contracts/DualGovernanceConfigProvider.sol"; + +import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; +import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; +import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; + +import {DeployConfig, LidoContracts, getSubCommitteeData} from "./DeployConfig.sol"; +import {getLidoAddresses} from "./Config.s.sol"; + +struct DeployedContracts { + Executor adminExecutor; + EmergencyProtectedTimelock timelock; + TimelockedGovernance emergencyGovernance; + EmergencyActivationCommittee emergencyActivationCommittee; + EmergencyExecutionCommittee emergencyExecutionCommittee; + ResealManager resealManager; + DualGovernance dualGovernance; + ResealCommittee resealCommittee; + TiebreakerCore tiebreakerCoreCommittee; + address[] tiebreakerSubCommittees; +} + +library DeployDGContracts { + function deployDualGovernanceSetup( + DeployConfig memory dgDeployConfig, + address deployer + ) internal returns (DeployedContracts memory contracts) { + LidoContracts memory lidoAddresses = getLidoAddresses(dgDeployConfig); + contracts = deployEmergencyProtectedTimelockContracts(lidoAddresses, dgDeployConfig, contracts, deployer); + contracts.resealManager = deployResealManager(contracts.timelock); + ImmutableDualGovernanceConfigProvider dualGovernanceConfigProvider = + deployDualGovernanceConfigProvider(dgDeployConfig); + DualGovernance dualGovernance = deployDualGovernance({ + configProvider: dualGovernanceConfigProvider, + timelock: contracts.timelock, + resealManager: contracts.resealManager, + dgDeployConfig: dgDeployConfig, + lidoAddresses: lidoAddresses + }); + contracts.dualGovernance = dualGovernance; + + contracts.tiebreakerCoreCommittee = deployEmptyTiebreakerCoreCommittee({ + owner: deployer, // temporary set owner to deployer, to add sub committees manually + dualGovernance: address(dualGovernance), + executionDelay: dgDeployConfig.TIEBREAKER_EXECUTION_DELAY + }); + + contracts.tiebreakerSubCommittees = deployTiebreakerSubCommittees( + address(contracts.adminExecutor), contracts.tiebreakerCoreCommittee, dgDeployConfig + ); + + contracts.tiebreakerCoreCommittee.transferOwnership(address(contracts.adminExecutor)); + + contracts.resealCommittee = + deployResealCommittee(address(contracts.adminExecutor), address(dualGovernance), dgDeployConfig); + + // --- + // Finalize Setup + // --- + contracts.adminExecutor.execute( + address(dualGovernance), + 0, + abi.encodeCall( + dualGovernance.registerProposer, (address(lidoAddresses.voting), address(contracts.adminExecutor)) + ) + ); + contracts.adminExecutor.execute( + address(dualGovernance), + 0, + abi.encodeCall(dualGovernance.setTiebreakerActivationTimeout, dgDeployConfig.TIEBREAKER_ACTIVATION_TIMEOUT) + ); + contracts.adminExecutor.execute( + address(dualGovernance), + 0, + abi.encodeCall(dualGovernance.setTiebreakerCommittee, address(contracts.tiebreakerCoreCommittee)) + ); + contracts.adminExecutor.execute( + address(dualGovernance), + 0, + abi.encodeCall( + dualGovernance.addTiebreakerSealableWithdrawalBlocker, address(lidoAddresses.withdrawalQueue) + ) + ); + contracts.adminExecutor.execute( + address(dualGovernance), + 0, + abi.encodeCall(dualGovernance.setResealCommittee, address(contracts.resealCommittee)) + ); + + finalizeEmergencyProtectedTimelockDeploy( + contracts.adminExecutor, contracts.timelock, address(dualGovernance), dgDeployConfig + ); + + // --- + // TODO: Use this in voting script + // Grant Reseal Manager Roles + // --- + /* vm.startPrank(address(_lido.agent)); + _lido.withdrawalQueue.grantRole( + 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(resealManager) + ); + _lido.withdrawalQueue.grantRole( + 0x2fc10cc8ae19568712f7a176fb4978616a610650813c9d05326c34abb62749c7, address(resealManager) + ); + vm.stopPrank(); */ + } + + function deployEmergencyProtectedTimelockContracts( + LidoContracts memory lidoAddresses, + DeployConfig memory dgDeployConfig, + DeployedContracts memory contracts, + address deployer + ) internal returns (DeployedContracts memory) { + Executor adminExecutor = deployExecutor(deployer); + EmergencyProtectedTimelock timelock = deployEmergencyProtectedTimelock(address(adminExecutor), dgDeployConfig); + + contracts.adminExecutor = adminExecutor; + contracts.timelock = timelock; + contracts.emergencyActivationCommittee = deployEmergencyActivationCommittee({ + quorum: dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, + members: dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS, + owner: address(adminExecutor), + timelock: address(timelock) + }); + + contracts.emergencyExecutionCommittee = deployEmergencyExecutionCommittee({ + quorum: dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_QUORUM, + members: dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS, + owner: address(adminExecutor), + timelock: address(timelock) + }); + contracts.emergencyGovernance = + deployTimelockedGovernance({governance: address(lidoAddresses.voting), timelock: timelock}); + + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall( + timelock.setEmergencyProtectionActivationCommittee, (address(contracts.emergencyActivationCommittee)) + ) + ); + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall( + timelock.setEmergencyProtectionExecutionCommittee, (address(contracts.emergencyExecutionCommittee)) + ) + ); + + // TODO: Do we really need to set it? + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall( + timelock.setEmergencyProtectionEndDate, + (dgDeployConfig.EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now())) + ) + ); + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall(timelock.setEmergencyModeDuration, (dgDeployConfig.EMERGENCY_MODE_DURATION)) + ); + + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall(timelock.setEmergencyGovernance, (address(contracts.emergencyGovernance))) + ); + + return contracts; + } + + function deployExecutor(address owner) internal returns (Executor) { + return new Executor(owner); + } + + function deployEmergencyProtectedTimelock( + address adminExecutor, + DeployConfig memory dgDeployConfig + ) internal returns (EmergencyProtectedTimelock) { + return new EmergencyProtectedTimelock({ + adminExecutor: address(adminExecutor), + sanityCheckParams: EmergencyProtectedTimelock.SanityCheckParams({ + maxAfterSubmitDelay: dgDeployConfig.MAX_AFTER_SUBMIT_DELAY, + maxAfterScheduleDelay: dgDeployConfig.MAX_AFTER_SCHEDULE_DELAY, + maxEmergencyModeDuration: dgDeployConfig.MAX_EMERGENCY_MODE_DURATION, + maxEmergencyProtectionDuration: dgDeployConfig.MAX_EMERGENCY_PROTECTION_DURATION + }) + }); + } + + function deployEmergencyActivationCommittee( + address owner, + uint256 quorum, + address[] memory members, + address timelock + ) internal returns (EmergencyActivationCommittee) { + return new EmergencyActivationCommittee(owner, members, quorum, address(timelock)); + } + + function deployEmergencyExecutionCommittee( + address owner, + uint256 quorum, + address[] memory members, + address timelock + ) internal returns (EmergencyExecutionCommittee) { + return new EmergencyExecutionCommittee(owner, members, quorum, address(timelock)); + } + + function deployTimelockedGovernance( + address governance, + EmergencyProtectedTimelock timelock + ) internal returns (TimelockedGovernance) { + return new TimelockedGovernance(governance, timelock); + } + + function deployResealManager(EmergencyProtectedTimelock timelock) internal returns (ResealManager) { + return new ResealManager(timelock); + } + + function deployDualGovernanceConfigProvider(DeployConfig memory dgDeployConfig) + internal + returns (ImmutableDualGovernanceConfigProvider) + { + return new ImmutableDualGovernanceConfigProvider( + DualGovernanceConfig.Context({ + firstSealRageQuitSupport: dgDeployConfig.FIRST_SEAL_RAGE_QUIT_SUPPORT, + secondSealRageQuitSupport: dgDeployConfig.SECOND_SEAL_RAGE_QUIT_SUPPORT, + // + minAssetsLockDuration: dgDeployConfig.MIN_ASSETS_LOCK_DURATION, + dynamicTimelockMinDuration: dgDeployConfig.DYNAMIC_TIMELOCK_MIN_DURATION, + dynamicTimelockMaxDuration: dgDeployConfig.DYNAMIC_TIMELOCK_MAX_DURATION, + // + vetoSignallingMinActiveDuration: dgDeployConfig.VETO_SIGNALLING_MIN_ACTIVE_DURATION, + vetoSignallingDeactivationMaxDuration: dgDeployConfig.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION, + vetoCooldownDuration: dgDeployConfig.VETO_COOLDOWN_DURATION, + // + rageQuitExtensionDelay: dgDeployConfig.RAGE_QUIT_EXTENSION_DELAY, + rageQuitEthWithdrawalsMinTimelock: dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK, + rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: dgDeployConfig + .RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER, + rageQuitEthWithdrawalsTimelockGrowthCoeffs: dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS + }) + ); + } + + function deployDualGovernance( + IDualGovernanceConfigProvider configProvider, + EmergencyProtectedTimelock timelock, + ResealManager resealManager, + DeployConfig memory dgDeployConfig, + LidoContracts memory lidoAddresses + ) internal returns (DualGovernance) { + return new DualGovernance({ + dependencies: DualGovernance.ExternalDependencies({ + stETH: lidoAddresses.stETH, + wstETH: lidoAddresses.wstETH, + withdrawalQueue: lidoAddresses.withdrawalQueue, + timelock: timelock, + resealManager: resealManager, + configProvider: configProvider + }), + sanityCheckParams: DualGovernance.SanityCheckParams({ + minWithdrawalsBatchSize: dgDeployConfig.MIN_WITHDRAWALS_BATCH_SIZE, + minTiebreakerActivationTimeout: dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT, + maxTiebreakerActivationTimeout: dgDeployConfig.MAX_TIEBREAKER_ACTIVATION_TIMEOUT, + maxSealableWithdrawalBlockersCount: dgDeployConfig.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT + }) + }); + } + + function deployEmptyTiebreakerCoreCommittee( + address owner, + address dualGovernance, + Duration executionDelay + ) internal returns (TiebreakerCore) { + return new TiebreakerCore({owner: owner, dualGovernance: dualGovernance, timelock: executionDelay}); + } + + function deployTiebreakerSubCommittees( + address owner, + TiebreakerCore tiebreakerCoreCommittee, + DeployConfig memory dgDeployConfig + ) internal returns (address[] memory) { + address[] memory coreCommitteeMembers = new address[](dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT); + + for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { + (uint256 quorum, address[] memory members) = getSubCommitteeData(i, dgDeployConfig); + + coreCommitteeMembers[i] = address( + deployTiebreakerSubCommittee({ + owner: owner, + quorum: quorum, + members: members, + tiebreakerCoreCommittee: address(tiebreakerCoreCommittee) + }) + ); + } + + tiebreakerCoreCommittee.addMembers(coreCommitteeMembers, dgDeployConfig.TIEBREAKER_CORE_QUORUM); + + return coreCommitteeMembers; + } + + function deployTiebreakerSubCommittee( + address owner, + uint256 quorum, + address[] memory members, + address tiebreakerCoreCommittee + ) internal returns (TiebreakerSubCommittee) { + return new TiebreakerSubCommittee({ + owner: owner, + executionQuorum: quorum, + committeeMembers: members, + tiebreakerCore: tiebreakerCoreCommittee + }); + } + + function deployResealCommittee( + address adminExecutor, + address dualGovernance, + DeployConfig memory dgDeployConfig + ) internal returns (ResealCommittee) { + uint256 quorum = dgDeployConfig.RESEAL_COMMITTEE_QUORUM; + address[] memory committeeMembers = dgDeployConfig.RESEAL_COMMITTEE_MEMBERS; + + // TODO: Don't we need to use non-zero timelock here? + return new ResealCommittee(adminExecutor, committeeMembers, quorum, dualGovernance, Durations.from(0)); + } + + function finalizeEmergencyProtectedTimelockDeploy( + Executor adminExecutor, + EmergencyProtectedTimelock timelock, + address dualGovernance, + DeployConfig memory dgDeployConfig + ) internal { + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall( + timelock.setupDelays, (dgDeployConfig.AFTER_SUBMIT_DELAY, dgDeployConfig.AFTER_SCHEDULE_DELAY) + ) + ); + adminExecutor.execute(address(timelock), 0, abi.encodeCall(timelock.setGovernance, (dualGovernance))); + adminExecutor.transferOwnership(address(timelock)); + } +} diff --git a/scripts/deploy/DeployValidation.sol b/scripts/deploy/DeployValidation.sol index 54087495..bd00946a 100644 --- a/scripts/deploy/DeployValidation.sol +++ b/scripts/deploy/DeployValidation.sol @@ -17,7 +17,10 @@ import {DualGovernance} from "contracts/DualGovernance.sol"; import {Escrow} from "contracts/Escrow.sol"; import {DualGovernanceConfig} from "contracts/libraries/DualGovernanceConfig.sol"; import {State} from "contracts/libraries/DualGovernanceStateMachine.sol"; -import {DGDeployConfig, ConfigValues, LidoAddresses, getLidoAddresses} from "./Config.s.sol"; +import {DeployConfig, LidoContracts, getSubCommitteeData} from "./DeployConfig.sol"; +import {DGDeployConfigProvider, getLidoAddresses} from "./Config.s.sol"; + +// TODO: long error texts in require() library DeployValidation { struct DeployResult { @@ -30,14 +33,13 @@ library DeployValidation { address dualGovernance; address resealCommittee; address tiebreakerCoreCommittee; - address tiebreakerSubCommittee1; - address tiebreakerSubCommittee2; + address[] tiebreakerSubCommittees; } function check(DeployResult memory res) internal { - DGDeployConfig configProvider = new DGDeployConfig(); - ConfigValues memory dgDeployConfig = configProvider.loadAndValidate(); - LidoAddresses memory lidoAddresses = getLidoAddresses(dgDeployConfig); + DGDeployConfigProvider configProvider = new DGDeployConfigProvider(); + DeployConfig memory dgDeployConfig = configProvider.loadAndValidate(); + LidoContracts memory lidoAddresses = getLidoAddresses(dgDeployConfig); checkAdminExecutor(res.adminExecutor, res.timelock); checkTimelock(res, dgDeployConfig); @@ -47,8 +49,11 @@ library DeployValidation { checkResealManager(res); checkDualGovernance(res, dgDeployConfig, lidoAddresses); checkTiebreakerCoreCommittee(res, dgDeployConfig); - checkTiebreakerSubCommittee1(res, dgDeployConfig); - checkTiebreakerSubCommittee2(res, dgDeployConfig); + + for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { + checkTiebreakerSubCommittee(res, dgDeployConfig, i); + } + checkResealCommittee(res, dgDeployConfig); } @@ -56,7 +61,7 @@ library DeployValidation { require(Executor(executor).owner() == timelock, "AdminExecutor owner != EmergencyProtectedTimelock"); } - function checkTimelock(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { + function checkTimelock(DeployResult memory res, DeployConfig memory dgDeployConfig) internal view { EmergencyProtectedTimelock timelockInstance = EmergencyProtectedTimelock(res.timelock); require( timelockInstance.getAdminExecutor() == res.adminExecutor, @@ -119,7 +124,7 @@ library DeployValidation { function checkEmergencyActivationCommittee( DeployResult memory res, - ConfigValues memory dgDeployConfig + DeployConfig memory dgDeployConfig ) internal view { EmergencyActivationCommittee committee = EmergencyActivationCommittee(res.emergencyActivationCommittee); require(committee.owner() == res.adminExecutor, "EmergencyActivationCommittee owner != adminExecutor"); @@ -142,7 +147,7 @@ library DeployValidation { function checkEmergencyExecutionCommittee( DeployResult memory res, - ConfigValues memory dgDeployConfig + DeployConfig memory dgDeployConfig ) internal view { EmergencyExecutionCommittee committee = EmergencyExecutionCommittee(res.emergencyExecutionCommittee); require(committee.owner() == res.adminExecutor, "EmergencyExecutionCommittee owner != adminExecutor"); @@ -163,7 +168,7 @@ library DeployValidation { ); } - function checkTimelockedGovernance(DeployResult memory res, LidoAddresses memory lidoAddresses) internal view { + function checkTimelockedGovernance(DeployResult memory res, LidoContracts memory lidoAddresses) internal view { TimelockedGovernance emergencyTimelockedGovernance = TimelockedGovernance(res.emergencyGovernance); require( emergencyTimelockedGovernance.GOVERNANCE() == address(lidoAddresses.voting), @@ -184,8 +189,8 @@ library DeployValidation { function checkDualGovernance( DeployResult memory res, - ConfigValues memory dgDeployConfig, - LidoAddresses memory lidoAddresses + DeployConfig memory dgDeployConfig, + LidoContracts memory lidoAddresses ) internal view { DualGovernance dg = DualGovernance(res.dualGovernance); require(address(dg.TIMELOCK()) == res.timelock, "Incorrect address for timelock in DualGovernance"); @@ -299,7 +304,7 @@ library DeployValidation { ); } - function checkTiebreakerCoreCommittee(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { + function checkTiebreakerCoreCommittee(DeployResult memory res, DeployConfig memory dgDeployConfig) internal view { TiebreakerCore tcc = TiebreakerCore(res.tiebreakerCoreCommittee); require(tcc.owner() == res.adminExecutor, "TiebreakerCoreCommittee owner != adminExecutor"); require( @@ -307,50 +312,33 @@ library DeployValidation { "Incorrect parameter TIEBREAKER_EXECUTION_DELAY" ); - // TODO: N sub committees - require(tcc.isMember(res.tiebreakerSubCommittee1) == true, "Incorrect member of TiebreakerCoreCommittee"); - require(tcc.isMember(res.tiebreakerSubCommittee2) == true, "Incorrect member of TiebreakerCoreCommittee"); - require(tcc.quorum() == 2, "Incorrect quorum in TiebreakerCoreCommittee"); - } - - function checkTiebreakerSubCommittee1(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { - TiebreakerSubCommittee tsc = TiebreakerSubCommittee(res.tiebreakerSubCommittee1); - require(tsc.owner() == res.adminExecutor, "TiebreakerSubCommittee1 owner != adminExecutor"); - require(tsc.timelockDuration() == Durations.from(0), "TiebreakerSubCommittee1 timelock should be 0"); // TODO: is it correct? - - for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS.length; ++i) { - require( - tsc.isMember(dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS[i]) == true, - "Incorrect member of TiebreakerSubCommittee1" - ); + for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { + require(tcc.isMember(res.tiebreakerSubCommittees[i]) == true, "Incorrect member of TiebreakerCoreCommittee"); } - require( - tsc.quorum() == dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_QUORUM, - "Incorrect quorum in TiebreakerSubCommittee1" - ); + require(tcc.quorum() == dgDeployConfig.TIEBREAKER_CORE_QUORUM, "Incorrect quorum in TiebreakerCoreCommittee"); } - function checkTiebreakerSubCommittee2(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { - TiebreakerSubCommittee tsc = TiebreakerSubCommittee(res.tiebreakerSubCommittee2); - require(tsc.owner() == res.adminExecutor, "TiebreakerSubCommittee1 owner != adminExecutor"); - require(tsc.timelockDuration() == Durations.from(0), "TiebreakerSubCommittee2 timelock should be 0"); // TODO: is it correct? + function checkTiebreakerSubCommittee( + DeployResult memory res, + DeployConfig memory dgDeployConfig, + uint256 index + ) internal view { + TiebreakerSubCommittee tsc = TiebreakerSubCommittee(res.tiebreakerSubCommittees[index]); + require(tsc.owner() == res.adminExecutor, "TiebreakerSubCommittee owner != adminExecutor"); + require(tsc.timelockDuration() == Durations.from(0), "TiebreakerSubCommittee timelock should be 0"); - for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS.length; ++i) { - require( - tsc.isMember(dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS[i]) == true, - "Incorrect member of TiebreakerSubCommittee2" - ); + (uint256 quorum, address[] memory members) = getSubCommitteeData(index, dgDeployConfig); + + for (uint256 i = 0; i < members.length; ++i) { + require(tsc.isMember(members[i]) == true, "Incorrect member of TiebreakerSubCommittee"); } - require( - tsc.quorum() == dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_QUORUM, - "Incorrect quorum in TiebreakerSubCommittee2" - ); + require(tsc.quorum() == quorum, "Incorrect quorum in TiebreakerSubCommittee"); } - function checkResealCommittee(DeployResult memory res, ConfigValues memory dgDeployConfig) internal view { + function checkResealCommittee(DeployResult memory res, DeployConfig memory dgDeployConfig) internal view { ResealCommittee rc = ResealCommittee(res.resealCommittee); require(rc.owner() == res.adminExecutor, "ResealCommittee owner != adminExecutor"); - require(rc.timelockDuration() == Durations.from(0), "ResealCommittee timelock should be 0"); // TODO: is it correct? + require(rc.timelockDuration() == Durations.from(0), "ResealCommittee timelock should be 0"); require(rc.DUAL_GOVERNANCE() == res.dualGovernance, "Incorrect dualGovernance in ResealCommittee"); for (uint256 i = 0; i < dgDeployConfig.RESEAL_COMMITTEE_MEMBERS.length; ++i) { diff --git a/test/utils/lido-utils.sol b/test/utils/lido-utils.sol index 9f3f42c0..13ef9952 100644 --- a/test/utils/lido-utils.sol +++ b/test/utils/lido-utils.sol @@ -13,7 +13,7 @@ import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; import {IAragonACL} from "./interfaces/IAragonACL.sol"; import {IAragonAgent} from "./interfaces/IAragonAgent.sol"; -import {IAragonVoting} from "./interfaces/IAragonVoting.sol"; +import {IAragonVoting} from "contracts/interfaces/IAragonVoting.sol"; import {IAragonForwarder} from "./interfaces/IAragonForwarder.sol"; import {EvmScriptUtils} from "./evm-script-utils.sol"; From 9e7e0a292c284f9f3af4868f5f88953cb6557935 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 11 Sep 2024 00:08:40 +0400 Subject: [PATCH 016/107] Rename some files --- scripts/deploy/Deploy.s.sol | 16 +++++----- scripts/deploy/DeployContracts.sol | 2 +- ...yValidation.sol => DeployVerification.sol} | 32 +++++++++++-------- .../deploy/{Config.s.sol => EnvConfig.s.sol} | 2 -- 4 files changed, 28 insertions(+), 24 deletions(-) rename scripts/deploy/{DeployValidation.sol => DeployVerification.sol} (94%) rename scripts/deploy/{Config.s.sol => EnvConfig.s.sol} (99%) diff --git a/scripts/deploy/Deploy.s.sol b/scripts/deploy/Deploy.s.sol index 30d1ee25..19b673dc 100644 --- a/scripts/deploy/Deploy.s.sol +++ b/scripts/deploy/Deploy.s.sol @@ -7,12 +7,12 @@ import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/console.sol"; import {DeployConfig} from "./DeployConfig.sol"; -import {DGDeployConfigProvider} from "./Config.s.sol"; +import {DGDeployConfigProvider} from "./EnvConfig.s.sol"; import {DeployDGContracts, DeployedContracts} from "./DeployContracts.sol"; -import {DeployValidation} from "./DeployValidation.sol"; +import {DeployVerification} from "./DeployVerification.sol"; contract DeployDG is Script { - using DeployValidation for DeployValidation.DeployResult; + using DeployVerification for DeployVerification.DeployedAddresses; error ChainIdMismatch(uint256 actual, uint256 expected); @@ -45,13 +45,13 @@ contract DeployDG is Script { vm.stopBroadcast(); - DeployValidation.DeployResult memory res = getDeployedAddresses(contracts); + DeployVerification.DeployedAddresses memory res = getDeployedAddresses(contracts); printAddresses(res); console.log("Verifying deploy"); - res.check(); + res.verify(); console.log(unicode"Verified ✅"); } @@ -59,9 +59,9 @@ contract DeployDG is Script { function getDeployedAddresses(DeployedContracts memory contracts) internal pure - returns (DeployValidation.DeployResult memory) + returns (DeployVerification.DeployedAddresses memory) { - return DeployValidation.DeployResult({ + return DeployVerification.DeployedAddresses({ adminExecutor: payable(address(contracts.adminExecutor)), timelock: address(contracts.timelock), emergencyGovernance: address(contracts.emergencyGovernance), @@ -75,7 +75,7 @@ contract DeployDG is Script { }); } - function printAddresses(DeployValidation.DeployResult memory res) internal pure { + function printAddresses(DeployVerification.DeployedAddresses memory res) internal pure { console.log("DG deployed successfully"); console.log("DualGovernance address", res.dualGovernance); console.log("ResealManager address", res.resealManager); diff --git a/scripts/deploy/DeployContracts.sol b/scripts/deploy/DeployContracts.sol index 9d89cf2f..19c7d3b7 100644 --- a/scripts/deploy/DeployContracts.sol +++ b/scripts/deploy/DeployContracts.sol @@ -28,7 +28,7 @@ import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; import {DeployConfig, LidoContracts, getSubCommitteeData} from "./DeployConfig.sol"; -import {getLidoAddresses} from "./Config.s.sol"; +import {getLidoAddresses} from "./EnvConfig.s.sol"; // TODO: make a param struct DeployedContracts { Executor adminExecutor; diff --git a/scripts/deploy/DeployValidation.sol b/scripts/deploy/DeployVerification.sol similarity index 94% rename from scripts/deploy/DeployValidation.sol rename to scripts/deploy/DeployVerification.sol index bd00946a..585ade7b 100644 --- a/scripts/deploy/DeployValidation.sol +++ b/scripts/deploy/DeployVerification.sol @@ -18,12 +18,12 @@ import {Escrow} from "contracts/Escrow.sol"; import {DualGovernanceConfig} from "contracts/libraries/DualGovernanceConfig.sol"; import {State} from "contracts/libraries/DualGovernanceStateMachine.sol"; import {DeployConfig, LidoContracts, getSubCommitteeData} from "./DeployConfig.sol"; -import {DGDeployConfigProvider, getLidoAddresses} from "./Config.s.sol"; +import {DGDeployConfigProvider, getLidoAddresses} from "./EnvConfig.s.sol"; // TODO: make a param // TODO: long error texts in require() -library DeployValidation { - struct DeployResult { +library DeployVerification { + struct DeployedAddresses { address payable adminExecutor; address timelock; address emergencyGovernance; @@ -36,7 +36,7 @@ library DeployValidation { address[] tiebreakerSubCommittees; } - function check(DeployResult memory res) internal { + function verify(DeployedAddresses memory res) internal { DGDeployConfigProvider configProvider = new DGDeployConfigProvider(); DeployConfig memory dgDeployConfig = configProvider.loadAndValidate(); LidoContracts memory lidoAddresses = getLidoAddresses(dgDeployConfig); @@ -61,7 +61,7 @@ library DeployValidation { require(Executor(executor).owner() == timelock, "AdminExecutor owner != EmergencyProtectedTimelock"); } - function checkTimelock(DeployResult memory res, DeployConfig memory dgDeployConfig) internal view { + function checkTimelock(DeployedAddresses memory res, DeployConfig memory dgDeployConfig) internal view { EmergencyProtectedTimelock timelockInstance = EmergencyProtectedTimelock(res.timelock); require( timelockInstance.getAdminExecutor() == res.adminExecutor, @@ -123,7 +123,7 @@ library DeployValidation { } function checkEmergencyActivationCommittee( - DeployResult memory res, + DeployedAddresses memory res, DeployConfig memory dgDeployConfig ) internal view { EmergencyActivationCommittee committee = EmergencyActivationCommittee(res.emergencyActivationCommittee); @@ -146,7 +146,7 @@ library DeployValidation { } function checkEmergencyExecutionCommittee( - DeployResult memory res, + DeployedAddresses memory res, DeployConfig memory dgDeployConfig ) internal view { EmergencyExecutionCommittee committee = EmergencyExecutionCommittee(res.emergencyExecutionCommittee); @@ -168,7 +168,10 @@ library DeployValidation { ); } - function checkTimelockedGovernance(DeployResult memory res, LidoContracts memory lidoAddresses) internal view { + function checkTimelockedGovernance( + DeployedAddresses memory res, + LidoContracts memory lidoAddresses + ) internal view { TimelockedGovernance emergencyTimelockedGovernance = TimelockedGovernance(res.emergencyGovernance); require( emergencyTimelockedGovernance.GOVERNANCE() == address(lidoAddresses.voting), @@ -180,7 +183,7 @@ library DeployValidation { ); } - function checkResealManager(DeployResult memory res) internal view { + function checkResealManager(DeployedAddresses memory res) internal view { require( address(ResealManager(res.resealManager).EMERGENCY_PROTECTED_TIMELOCK()) == res.timelock, "Incorrect address for EMERGENCY_PROTECTED_TIMELOCK in ResealManager" @@ -188,7 +191,7 @@ library DeployValidation { } function checkDualGovernance( - DeployResult memory res, + DeployedAddresses memory res, DeployConfig memory dgDeployConfig, LidoContracts memory lidoAddresses ) internal view { @@ -304,7 +307,10 @@ library DeployValidation { ); } - function checkTiebreakerCoreCommittee(DeployResult memory res, DeployConfig memory dgDeployConfig) internal view { + function checkTiebreakerCoreCommittee( + DeployedAddresses memory res, + DeployConfig memory dgDeployConfig + ) internal view { TiebreakerCore tcc = TiebreakerCore(res.tiebreakerCoreCommittee); require(tcc.owner() == res.adminExecutor, "TiebreakerCoreCommittee owner != adminExecutor"); require( @@ -319,7 +325,7 @@ library DeployValidation { } function checkTiebreakerSubCommittee( - DeployResult memory res, + DeployedAddresses memory res, DeployConfig memory dgDeployConfig, uint256 index ) internal view { @@ -335,7 +341,7 @@ library DeployValidation { require(tsc.quorum() == quorum, "Incorrect quorum in TiebreakerSubCommittee"); } - function checkResealCommittee(DeployResult memory res, DeployConfig memory dgDeployConfig) internal view { + function checkResealCommittee(DeployedAddresses memory res, DeployConfig memory dgDeployConfig) internal view { ResealCommittee rc = ResealCommittee(res.resealCommittee); require(rc.owner() == res.adminExecutor, "ResealCommittee owner != adminExecutor"); require(rc.timelockDuration() == Durations.from(0), "ResealCommittee timelock should be 0"); diff --git a/scripts/deploy/Config.s.sol b/scripts/deploy/EnvConfig.s.sol similarity index 99% rename from scripts/deploy/Config.s.sol rename to scripts/deploy/EnvConfig.s.sol index 25392b1a..186322b9 100644 --- a/scripts/deploy/Config.s.sol +++ b/scripts/deploy/EnvConfig.s.sol @@ -50,8 +50,6 @@ function getLidoAddresses(DeployConfig memory config) pure returns (LidoContract }); } -// TODO: rename to EnvConfig - contract DGDeployConfigProvider is Script { error InvalidRageQuitETHWithdrawalsTimelockGrowthCoeffs(uint256[] coeffs); error InvalidQuorum(string committee, uint256 quorum); From fd67602571dec3e5d345c11e0847d90c8347b331 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 11 Sep 2024 00:11:05 +0400 Subject: [PATCH 017/107] Rename Deploy.s.sol to DeployBase.s.sol --- scripts/deploy/{Deploy.s.sol => DeployBase.s.sol} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts/deploy/{Deploy.s.sol => DeployBase.s.sol} (100%) diff --git a/scripts/deploy/Deploy.s.sol b/scripts/deploy/DeployBase.s.sol similarity index 100% rename from scripts/deploy/Deploy.s.sol rename to scripts/deploy/DeployBase.s.sol From d77ec35f86ece334936ad1eee1a67a8e0e8ab583 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 11 Sep 2024 02:17:29 +0400 Subject: [PATCH 018/107] Extract deploy main functionality to a separate abstract contract. Add separate deploy verification script. --- .../deploy/{DeployConfig.sol => Config.sol} | 1 - ...yContracts.sol => ContractsDeployment.sol} | 7 +- scripts/deploy/DeployBase.s.sol | 33 ++++----- scripts/deploy/DeployConfigurable.s.sol | 14 ++++ scripts/deploy/DeployVerification.sol | 13 ++-- scripts/deploy/EnvConfig.s.sol | 53 +++++++------- scripts/deploy/Verify.s.sol | 70 +++++++++++++++++++ 7 files changed, 131 insertions(+), 60 deletions(-) rename scripts/deploy/{DeployConfig.sol => Config.sol} (99%) rename scripts/deploy/{DeployContracts.sol => ContractsDeployment.sol} (98%) create mode 100644 scripts/deploy/DeployConfigurable.s.sol create mode 100644 scripts/deploy/Verify.s.sol diff --git a/scripts/deploy/DeployConfig.sol b/scripts/deploy/Config.sol similarity index 99% rename from scripts/deploy/DeployConfig.sol rename to scripts/deploy/Config.sol index 9d61a2c8..e08a70f8 100644 --- a/scripts/deploy/DeployConfig.sol +++ b/scripts/deploy/Config.sol @@ -11,7 +11,6 @@ import {Duration} from "contracts/types/Duration.sol"; import {PercentD16} from "contracts/types/PercentD16.sol"; struct DeployConfig { - string CHAIN; Duration AFTER_SUBMIT_DELAY; Duration MAX_AFTER_SUBMIT_DELAY; Duration AFTER_SCHEDULE_DELAY; diff --git a/scripts/deploy/DeployContracts.sol b/scripts/deploy/ContractsDeployment.sol similarity index 98% rename from scripts/deploy/DeployContracts.sol rename to scripts/deploy/ContractsDeployment.sol index 19c7d3b7..6a48b4e1 100644 --- a/scripts/deploy/DeployContracts.sol +++ b/scripts/deploy/ContractsDeployment.sol @@ -27,8 +27,7 @@ import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; -import {DeployConfig, LidoContracts, getSubCommitteeData} from "./DeployConfig.sol"; -import {getLidoAddresses} from "./EnvConfig.s.sol"; // TODO: make a param +import {DeployConfig, LidoContracts, getSubCommitteeData} from "./Config.sol"; struct DeployedContracts { Executor adminExecutor; @@ -43,12 +42,12 @@ struct DeployedContracts { address[] tiebreakerSubCommittees; } -library DeployDGContracts { +library DGContractsDeployment { function deployDualGovernanceSetup( DeployConfig memory dgDeployConfig, + LidoContracts memory lidoAddresses, address deployer ) internal returns (DeployedContracts memory contracts) { - LidoContracts memory lidoAddresses = getLidoAddresses(dgDeployConfig); contracts = deployEmergencyProtectedTimelockContracts(lidoAddresses, dgDeployConfig, contracts, deployer); contracts.resealManager = deployResealManager(contracts.timelock); ImmutableDualGovernanceConfigProvider dualGovernanceConfigProvider = diff --git a/scripts/deploy/DeployBase.s.sol b/scripts/deploy/DeployBase.s.sol index 19b673dc..b192fea0 100644 --- a/scripts/deploy/DeployBase.s.sol +++ b/scripts/deploy/DeployBase.s.sol @@ -6,33 +6,23 @@ pragma solidity 0.8.26; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/console.sol"; -import {DeployConfig} from "./DeployConfig.sol"; -import {DGDeployConfigProvider} from "./EnvConfig.s.sol"; -import {DeployDGContracts, DeployedContracts} from "./DeployContracts.sol"; +import {DeployConfig, LidoContracts} from "./Config.sol"; +import {DGContractsDeployment, DeployedContracts} from "./ContractsDeployment.sol"; import {DeployVerification} from "./DeployVerification.sol"; -contract DeployDG is Script { +abstract contract DeployBase is Script { using DeployVerification for DeployVerification.DeployedAddresses; error ChainIdMismatch(uint256 actual, uint256 expected); DeployConfig internal config; + LidoContracts internal lidoAddresses; address private deployer; uint256 private pk; - string private chainName; - uint256 private chainId; - - /* TODO: constructor(string memory _chainName, uint256 _chainId) { - chainName = _chainName; - chainId = _chainId; - } */ function run() external { - DGDeployConfigProvider configProvider = new DGDeployConfigProvider(); - config = configProvider.loadAndValidate(); - - if (config.chainId != block.chainid) { - revert ChainIdMismatch({actual: block.chainid, expected: chainId}); + if (lidoAddresses.chainId != block.chainid) { + revert ChainIdMismatch({actual: block.chainid, expected: lidoAddresses.chainId}); } pk = vm.envUint("DEPLOYER_PRIVATE_KEY"); @@ -41,7 +31,8 @@ contract DeployDG is Script { vm.startBroadcast(pk); - DeployedContracts memory contracts = DeployDGContracts.deployDualGovernanceSetup(config, deployer); + DeployedContracts memory contracts = + DGContractsDeployment.deployDualGovernanceSetup(config, lidoAddresses, deployer); vm.stopBroadcast(); @@ -51,7 +42,7 @@ contract DeployDG is Script { console.log("Verifying deploy"); - res.verify(); + res.verify(config, lidoAddresses); console.log(unicode"Verified ✅"); } @@ -75,14 +66,14 @@ contract DeployDG is Script { }); } - function printAddresses(DeployVerification.DeployedAddresses memory res) internal pure { + function printAddresses(DeployVerification.DeployedAddresses memory res) internal view { console.log("DG deployed successfully"); console.log("DualGovernance address", res.dualGovernance); console.log("ResealManager address", res.resealManager); console.log("TiebreakerCoreCommittee address", res.tiebreakerCoreCommittee); - for (uint256 i = 0; i < config.TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { - console.log("TiebreakerSubCommittee #", i, "address", contracts.tiebreakerSubCommittees[i]); + for (uint256 i = 0; i < res.tiebreakerSubCommittees.length; ++i) { + console.log("TiebreakerSubCommittee #", i, "address", res.tiebreakerSubCommittees[i]); } console.log("AdminExecutor address", res.adminExecutor); diff --git a/scripts/deploy/DeployConfigurable.s.sol b/scripts/deploy/DeployConfigurable.s.sol new file mode 100644 index 00000000..078acbc4 --- /dev/null +++ b/scripts/deploy/DeployConfigurable.s.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {DGDeployConfigProvider} from "./EnvConfig.s.sol"; +import {DeployBase} from "./DeployBase.s.sol"; + +contract DeployConfigurable is DeployBase { + constructor() { + string memory chainName = vm.envString("CHAIN"); + DGDeployConfigProvider configProvider = new DGDeployConfigProvider(); + config = configProvider.loadAndValidate(); + lidoAddresses = configProvider.getLidoAddresses(chainName); + } +} diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index 585ade7b..69a5d1e7 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -17,8 +17,7 @@ import {DualGovernance} from "contracts/DualGovernance.sol"; import {Escrow} from "contracts/Escrow.sol"; import {DualGovernanceConfig} from "contracts/libraries/DualGovernanceConfig.sol"; import {State} from "contracts/libraries/DualGovernanceStateMachine.sol"; -import {DeployConfig, LidoContracts, getSubCommitteeData} from "./DeployConfig.sol"; -import {DGDeployConfigProvider, getLidoAddresses} from "./EnvConfig.s.sol"; // TODO: make a param +import {DeployConfig, LidoContracts, getSubCommitteeData} from "./Config.sol"; // TODO: long error texts in require() @@ -36,11 +35,11 @@ library DeployVerification { address[] tiebreakerSubCommittees; } - function verify(DeployedAddresses memory res) internal { - DGDeployConfigProvider configProvider = new DGDeployConfigProvider(); - DeployConfig memory dgDeployConfig = configProvider.loadAndValidate(); - LidoContracts memory lidoAddresses = getLidoAddresses(dgDeployConfig); - + function verify( + DeployedAddresses memory res, + DeployConfig memory dgDeployConfig, + LidoContracts memory lidoAddresses + ) internal view { checkAdminExecutor(res.adminExecutor, res.timelock); checkTimelock(res, dgDeployConfig); checkEmergencyActivationCommittee(res, dgDeployConfig); diff --git a/scripts/deploy/EnvConfig.s.sol b/scripts/deploy/EnvConfig.s.sol index 186322b9..7c613ded 100644 --- a/scripts/deploy/EnvConfig.s.sol +++ b/scripts/deploy/EnvConfig.s.sol @@ -23,33 +23,13 @@ import { } from "addresses/holesky-addresses.sol"; import {Durations} from "contracts/types/Duration.sol"; import {PercentsD16} from "contracts/types/PercentD16.sol"; -import {DeployConfig, LidoContracts} from "./DeployConfig.sol"; +import {DeployConfig, LidoContracts} from "./Config.sol"; string constant ARRAY_SEPARATOR = ","; bytes32 constant CHAIN_NAME_MAINNET_HASH = keccak256(bytes("mainnet")); bytes32 constant CHAIN_NAME_HOLESKY_HASH = keccak256(bytes("holesky")); // TODO: implement "holesky-mocks" -function getLidoAddresses(DeployConfig memory config) pure returns (LidoContracts memory) { - if (keccak256(bytes(config.CHAIN)) == CHAIN_NAME_MAINNET_HASH) { - return LidoContracts({ - chainId: 1, - stETH: IStETH(MAINNET_ST_ETH), - wstETH: IWstETH(MAINNET_WST_ETH), - withdrawalQueue: IWithdrawalQueue(MAINNET_WITHDRAWAL_QUEUE), - voting: IAragonVoting(MAINNET_DAO_VOTING) - }); - } - - return LidoContracts({ - chainId: 17000, - stETH: IStETH(HOLESKY_ST_ETH), - wstETH: IWstETH(HOLESKY_WST_ETH), - withdrawalQueue: IWithdrawalQueue(HOLESKY_WITHDRAWAL_QUEUE), - voting: IAragonVoting(HOLESKY_DAO_VOTING) - }); -} - contract DGDeployConfigProvider is Script { error InvalidRageQuitETHWithdrawalsTimelockGrowthCoeffs(uint256[] coeffs); error InvalidQuorum(string committee, uint256 quorum); @@ -98,7 +78,6 @@ contract DGDeployConfigProvider is Script { function loadAndValidate() external returns (DeployConfig memory config) { config = DeployConfig({ - CHAIN: vm.envString("CHAIN"), AFTER_SUBMIT_DELAY: Durations.from(vm.envOr("AFTER_SUBMIT_DELAY", DEFAULT_AFTER_SUBMIT_DELAY)), MAX_AFTER_SUBMIT_DELAY: Durations.from(vm.envOr("MAX_AFTER_SUBMIT_DELAY", DEFAULT_MAX_AFTER_SUBMIT_DELAY)), AFTER_SCHEDULE_DELAY: Durations.from(vm.envOr("AFTER_SCHEDULE_DELAY", DEFAULT_AFTER_SCHEDULE_DELAY)), @@ -209,6 +188,31 @@ contract DGDeployConfigProvider is Script { printCommittees(config); } + function getLidoAddresses(string memory chainName) external pure returns (LidoContracts memory) { + bytes32 chainNameHash = keccak256(bytes(chainName)); + if (chainNameHash != CHAIN_NAME_MAINNET_HASH && chainNameHash != CHAIN_NAME_HOLESKY_HASH) { + revert InvalidChain(chainName); + } + + if (keccak256(bytes(chainName)) == CHAIN_NAME_MAINNET_HASH) { + return LidoContracts({ + chainId: 1, + stETH: IStETH(MAINNET_ST_ETH), + wstETH: IWstETH(MAINNET_WST_ETH), + withdrawalQueue: IWithdrawalQueue(MAINNET_WITHDRAWAL_QUEUE), + voting: IAragonVoting(MAINNET_DAO_VOTING) + }); + } + + return LidoContracts({ + chainId: 17000, + stETH: IStETH(HOLESKY_ST_ETH), + wstETH: IWstETH(HOLESKY_WST_ETH), + withdrawalQueue: IWithdrawalQueue(HOLESKY_WITHDRAWAL_QUEUE), + voting: IAragonVoting(HOLESKY_DAO_VOTING) + }); + } + function getValidCoeffs() internal returns (uint256[3] memory coeffs) { uint256[] memory coeffsRaw = vm.envOr( "RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS", @@ -227,11 +231,6 @@ contract DGDeployConfigProvider is Script { } function validateConfig(DeployConfig memory config) internal pure { - bytes32 chainNameHash = keccak256(bytes(config.CHAIN)); - if (chainNameHash != CHAIN_NAME_MAINNET_HASH && chainNameHash != CHAIN_NAME_HOLESKY_HASH) { - revert InvalidChain(config.CHAIN); - } - checkCommitteeQuorum( config.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS, config.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, diff --git a/scripts/deploy/Verify.s.sol b/scripts/deploy/Verify.s.sol new file mode 100644 index 00000000..6cb49c6a --- /dev/null +++ b/scripts/deploy/Verify.s.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/* solhint-disable no-console */ + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; + +import {DeployConfig, LidoContracts} from "./Config.sol"; +import {DGDeployConfigProvider} from "./EnvConfig.s.sol"; +import {DeployVerification} from "./DeployVerification.sol"; + +contract Verify is Script { + using DeployVerification for DeployVerification.DeployedAddresses; + + DeployConfig internal config; + LidoContracts internal lidoAddresses; + address private deployer; + uint256 private pk; + + function run() external { + string memory chainName = vm.envString("CHAIN"); + DGDeployConfigProvider configProvider = new DGDeployConfigProvider(); + config = configProvider.loadAndValidate(); + lidoAddresses = configProvider.getLidoAddresses(chainName); + + DeployVerification.DeployedAddresses memory res = loadDeployedAddresses(); + + printAddresses(res); + + console.log("Verifying deploy"); + + res.verify(config, lidoAddresses); + + console.log(unicode"Verified ✅"); + } + + function loadDeployedAddresses() internal view returns (DeployVerification.DeployedAddresses memory) { + return DeployVerification.DeployedAddresses({ + adminExecutor: payable(vm.envAddress("ADMIN_EXECUTOR")), + timelock: vm.envAddress("TIMELOCK"), + emergencyGovernance: vm.envAddress("EMERGENCY_GOVERNANCE"), + emergencyActivationCommittee: vm.envAddress("EMERGENCY_ACTIVATION_COMMITTEE"), + emergencyExecutionCommittee: vm.envAddress("EMERGENCY_EXECUTION_COMMITTEE"), + resealManager: vm.envAddress("RESEAL_MANAGER"), + dualGovernance: vm.envAddress("DUAL_GOVERNANCE"), + resealCommittee: vm.envAddress("RESEAL_COMMITTEE"), + tiebreakerCoreCommittee: vm.envAddress("TIEBREAKER_CORE_COMMITTEE"), + tiebreakerSubCommittees: vm.envAddress("TIEBREAKER_SUB_COMMITTEES", ",") + }); + } + + function printAddresses(DeployVerification.DeployedAddresses memory res) internal view { + console.log("Using the following DG contracts addresses"); + console.log("DualGovernance address", res.dualGovernance); + console.log("ResealManager address", res.resealManager); + console.log("TiebreakerCoreCommittee address", res.tiebreakerCoreCommittee); + + for (uint256 i = 0; i < res.tiebreakerSubCommittees.length; ++i) { + console.log("TiebreakerSubCommittee #", i, "address", res.tiebreakerSubCommittees[i]); + } + + console.log("AdminExecutor address", res.adminExecutor); + console.log("EmergencyProtectedTimelock address", res.timelock); + console.log("EmergencyGovernance address", res.emergencyGovernance); + console.log("EmergencyActivationCommittee address", res.emergencyActivationCommittee); + console.log("EmergencyExecutionCommittee address", res.emergencyExecutionCommittee); + console.log("ResealCommittee address", res.resealCommittee); + } +} From 2102f8d5feacd1b8d2201665a3ded0a6662fe018 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 11 Sep 2024 09:40:02 +0400 Subject: [PATCH 019/107] Add support for mocked Lido contracts on Holesky --- scripts/deploy/DeployVerification.sol | 24 ++++++++------- scripts/deploy/EnvConfig.s.sol | 19 ++++++++++-- scripts/deploy/Readme.md | 44 ++++++++++++++++++++++++--- 3 files changed, 69 insertions(+), 18 deletions(-) diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index 69a5d1e7..78c0d24f 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -5,8 +5,10 @@ import {Timestamps} from "contracts/types/Timestamp.sol"; import {Durations} from "contracts/types/Duration.sol"; import {PercentD16} from "contracts/types/PercentD16.sol"; import {Executor} from "contracts/Executor.sol"; +import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; +import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; @@ -84,27 +86,27 @@ library DeployVerification { ); require( - timelockInstance.getEmergencyProtectionContext().emergencyActivationCommittee - == res.emergencyActivationCommittee, + timelockInstance.getEmergencyActivationCommittee() == res.emergencyActivationCommittee, "Incorrect emergencyActivationCommittee address in EmergencyProtectedTimelock" ); require( - timelockInstance.getEmergencyProtectionContext().emergencyExecutionCommittee - == res.emergencyExecutionCommittee, + timelockInstance.getEmergencyExecutionCommittee() == res.emergencyExecutionCommittee, "Incorrect emergencyExecutionCommittee address in EmergencyProtectedTimelock" ); + + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory details = + timelockInstance.getEmergencyProtectionDetails(); require( - timelockInstance.getEmergencyProtectionContext().emergencyProtectionEndsAfter - <= dgDeployConfig.EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()), + details.emergencyProtectionEndsAfter <= dgDeployConfig.EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()), "Incorrect value for emergencyProtectionEndsAfter" ); require( - timelockInstance.getEmergencyProtectionContext().emergencyModeDuration - == dgDeployConfig.EMERGENCY_MODE_DURATION, + details.emergencyModeDuration == dgDeployConfig.EMERGENCY_MODE_DURATION, "Incorrect value for emergencyModeDuration" ); + require( - timelockInstance.getEmergencyProtectionContext().emergencyGovernance == res.emergencyGovernance, + timelockInstance.getEmergencyGovernance() == res.emergencyGovernance, "Incorrect emergencyGovernance address in EmergencyProtectedTimelock" ); require( @@ -289,11 +291,11 @@ library DeployVerification { "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[2]" ); - require(dg.getCurrentState() == State.Normal, "Incorrect DualGovernance state"); + require(dg.getState() == State.Normal, "Incorrect DualGovernance state"); require(dg.getProposers().length == 1, "Incorrect amount of proposers"); require(dg.isProposer(address(lidoAddresses.voting)) == true, "Lido voting is not set as a proposers[0]"); - DualGovernance.TiebreakerState memory ts = dg.getTiebreakerState(); + ITiebreaker.TiebreakerDetails memory ts = dg.getTiebreakerDetails(); require( ts.tiebreakerActivationTimeout == dgDeployConfig.TIEBREAKER_ACTIVATION_TIMEOUT, "Incorrect parameter TIEBREAKER_ACTIVATION_TIMEOUT" diff --git a/scripts/deploy/EnvConfig.s.sol b/scripts/deploy/EnvConfig.s.sol index 7c613ded..74652be3 100644 --- a/scripts/deploy/EnvConfig.s.sol +++ b/scripts/deploy/EnvConfig.s.sol @@ -28,7 +28,7 @@ import {DeployConfig, LidoContracts} from "./Config.sol"; string constant ARRAY_SEPARATOR = ","; bytes32 constant CHAIN_NAME_MAINNET_HASH = keccak256(bytes("mainnet")); bytes32 constant CHAIN_NAME_HOLESKY_HASH = keccak256(bytes("holesky")); -// TODO: implement "holesky-mocks" +bytes32 constant CHAIN_NAME_HOLESKY_MOCKS_HASH = keccak256(bytes("holesky-mocks")); contract DGDeployConfigProvider is Script { error InvalidRageQuitETHWithdrawalsTimelockGrowthCoeffs(uint256[] coeffs); @@ -188,9 +188,12 @@ contract DGDeployConfigProvider is Script { printCommittees(config); } - function getLidoAddresses(string memory chainName) external pure returns (LidoContracts memory) { + function getLidoAddresses(string memory chainName) external view returns (LidoContracts memory) { bytes32 chainNameHash = keccak256(bytes(chainName)); - if (chainNameHash != CHAIN_NAME_MAINNET_HASH && chainNameHash != CHAIN_NAME_HOLESKY_HASH) { + if ( + chainNameHash != CHAIN_NAME_MAINNET_HASH && chainNameHash != CHAIN_NAME_HOLESKY_HASH + && chainNameHash != CHAIN_NAME_HOLESKY_MOCKS_HASH + ) { revert InvalidChain(chainName); } @@ -204,6 +207,16 @@ contract DGDeployConfigProvider is Script { }); } + if (keccak256(bytes(chainName)) == CHAIN_NAME_HOLESKY_MOCKS_HASH) { + return LidoContracts({ + chainId: 17000, + stETH: IStETH(vm.envAddress("HOLESKY_MOCK_ST_ETH")), + wstETH: IWstETH(vm.envAddress("HOLESKY_MOCK_WST_ETH")), + withdrawalQueue: IWithdrawalQueue(vm.envAddress("HOLESKY_MOCK_WITHDRAWAL_QUEUE")), + voting: IAragonVoting(vm.envAddress("HOLESKY_MOCK_DAO_VOTING")) + }); + } + return LidoContracts({ chainId: 17000, stETH: IStETH(HOLESKY_ST_ETH), diff --git a/scripts/deploy/Readme.md b/scripts/deploy/Readme.md index 7fea346b..9fc348cb 100644 --- a/scripts/deploy/Readme.md +++ b/scripts/deploy/Readme.md @@ -1,4 +1,4 @@ -# Dual Governance deploy script +# Dual Governance deploy scripts ### Running locally with Anvil @@ -7,21 +7,57 @@ Start Anvil, provide RPC url (Infura as an example) anvil --fork-url https://.infura.io/v3/ --block-time 300 ``` -### Running the script +### Running the deploy script 1. Set up required env variables in .env file ``` - CHAIN=<"mainnet" OR "holesky"> + CHAIN=<"mainnet" OR "holesky" OR "holesky-mocks"> DEPLOYER_PRIVATE_KEY=... EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS=addr1,addr2,addr3 EMERGENCY_EXECUTION_COMMITTEE_MEMBERS=addr1,addr2,addr3 TIEBREAKER_SUB_COMMITTEE_1_MEMBERS=addr1,addr2,addr3 TIEBREAKER_SUB_COMMITTEE_2_MEMBERS=addr1,addr2,addr3 + TIEBREAKER_SUB_COMMITTEES_QUORUMS=3,2 + TIEBREAKER_SUB_COMMITTEES_COUNT=2 RESEAL_COMMITTEE_MEMBERS=addr1,addr2,addr3 ``` + + When using `CHAIN="holesky-mocks"` you will need to provide in addition already deployed mock contracts addresses: + + ``` + HOLESKY_MOCK_ST_ETH=... + HOLESKY_MOCK_WST_ETH=... + HOLESKY_MOCK_WITHDRAWAL_QUEUE=... + HOLESKY_MOCK_DAO_VOTING=... + ``` + +2. Run the script (with the local Anvil as an example) + + ``` + forge script scripts/deploy/DeployConfigurable.s.sol:DeployConfigurable --fork-url http://localhost:8545 --broadcast + ``` + +### Running the verification script + +1. Set up required env variables in .env file + + ``` + CHAIN=<"mainnet" OR "holesky" OR "holesky-mocks"> + ADMIN_EXECUTOR=... + TIMELOCK=... + EMERGENCY_GOVERNANCE=... + EMERGENCY_ACTIVATION_COMMITTEE=... + EMERGENCY_EXECUTION_COMMITTEE=... + RESEAL_MANAGER=... + DUAL_GOVERNANCE=... + RESEAL_COMMITTEE=... + TIEBREAKER_CORE_COMMITTEE=... + TIEBREAKER_SUB_COMMITTEES=... + ``` + 2. Run the script (with the local Anvil as an example) ``` - forge script scripts/deploy/Deploy.s.sol:DeployDG --fork-url http://localhost:8545 --broadcast + forge script scripts/deploy/Verify.s.sol:Verify --fork-url http://localhost:8545 --broadcast ``` From 67fffa7d91d137876550a1f1b06cb9ed9ff48405 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 11 Sep 2024 22:24:15 +0400 Subject: [PATCH 020/107] Add more checks to DeployVerification and config validation --- .env.example | 2 ++ scripts/deploy/DeployVerification.sol | 27 ++++++++++++++++++++++++++ scripts/deploy/EnvConfig.s.sol | 28 ++++++++++++++++++++++----- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 193bdbc7..a94ee95c 100644 --- a/.env.example +++ b/.env.example @@ -7,4 +7,6 @@ EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS=addr1,addr2,addr3 EMERGENCY_EXECUTION_COMMITTEE_MEMBERS=addr1,addr2,addr3 TIEBREAKER_SUB_COMMITTEE_1_MEMBERS=addr1,addr2,addr3 TIEBREAKER_SUB_COMMITTEE_2_MEMBERS=addr1,addr2,addr3 +TIEBREAKER_SUB_COMMITTEES_QUORUMS=2,2 +TIEBREAKER_SUB_COMMITTEES_COUNT=2 RESEAL_COMMITTEE_MEMBERS=addr1,addr2,addr3 diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index 78c0d24f..f3f570e5 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -15,6 +15,7 @@ import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecuti import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; import {ResealManager} from "contracts/ResealManager.sol"; +import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; import {DualGovernance} from "contracts/DualGovernance.sol"; import {Escrow} from "contracts/Escrow.sol"; import {DualGovernanceConfig} from "contracts/libraries/DualGovernanceConfig.sol"; @@ -121,6 +122,14 @@ library DeployVerification { timelockInstance.getGovernance() == res.dualGovernance, "Incorrect governance address in EmergencyProtectedTimelock" ); + require( + timelockInstance.isEmergencyProtectionEnabled() == true, + "EmergencyProtection is Disabled in EmergencyProtectedTimelock" + ); + require( + timelockInstance.isEmergencyModeActive() == false, "EmergencyMode is Active in EmergencyProtectedTimelock" + ); + require(timelockInstance.getProposalsCount() == 0, "ProposalsCount > 0 in EmergencyProtectedTimelock"); } function checkEmergencyActivationCommittee( @@ -295,6 +304,24 @@ library DeployVerification { require(dg.getProposers().length == 1, "Incorrect amount of proposers"); require(dg.isProposer(address(lidoAddresses.voting)) == true, "Lido voting is not set as a proposers[0]"); + IDualGovernance.StateDetails memory stateDetails = dg.getStateDetails(); + require(stateDetails.state == State.Normal, "Incorrect DualGovernance state"); + require(stateDetails.enteredAt <= Timestamps.now(), "Incorrect DualGovernance state enteredAt"); + require( + stateDetails.vetoSignallingActivatedAt == Timestamps.ZERO, + "Incorrect DualGovernance state vetoSignallingActivatedAt" + ); + require( + stateDetails.vetoSignallingReactivationTime == Timestamps.ZERO, + "Incorrect DualGovernance state vetoSignallingReactivationTime" + ); + require( + stateDetails.normalOrVetoCooldownExitedAt == Timestamps.ZERO, + "Incorrect DualGovernance state normalOrVetoCooldownExitedAt" + ); + require(stateDetails.rageQuitRound == 0, "Incorrect DualGovernance state rageQuitRound"); + require(stateDetails.dynamicDelay == Durations.ZERO, "Incorrect DualGovernance state dynamicDelay"); + ITiebreaker.TiebreakerDetails memory ts = dg.getTiebreakerDetails(); require( ts.tiebreakerActivationTimeout == dgDeployConfig.TIEBREAKER_ACTIVATION_TIMEOUT, diff --git a/scripts/deploy/EnvConfig.s.sol b/scripts/deploy/EnvConfig.s.sol index 74652be3..937789b0 100644 --- a/scripts/deploy/EnvConfig.s.sol +++ b/scripts/deploy/EnvConfig.s.sol @@ -349,11 +349,29 @@ contract DGDeployConfigProvider is Script { ); } - // TODO: AFTER_SUBMIT_DELAY <= MAX_AFTER_SUBMIT_DELAY - // TODO: AFTER_SCHEDULE_DELAY <= MAX_AFTER_SCHEDULE_DELAY - // TODO: EMERGENCY_MODE_DURATION <= MAX_EMERGENCY_MODE_DURATION - // TODO: MIN_TIEBREAKER_ACTIVATION_TIMEOUT <= TIEBREAKER_ACTIVATION_TIMEOUT <= MAX_TIEBREAKER_ACTIVATION_TIMEOUT - // TODO: DYNAMIC_TIMELOCK_MIN_DURATION <= DYNAMIC_TIMELOCK_MAX_DURATION + if (config.AFTER_SUBMIT_DELAY > config.MAX_AFTER_SUBMIT_DELAY) { + revert InvalidParameter("AFTER_SUBMIT_DELAY"); + } + + if (config.AFTER_SCHEDULE_DELAY > config.MAX_AFTER_SCHEDULE_DELAY) { + revert InvalidParameter("AFTER_SCHEDULE_DELAY"); + } + + if (config.EMERGENCY_MODE_DURATION > config.MAX_EMERGENCY_MODE_DURATION) { + revert InvalidParameter("EMERGENCY_MODE_DURATION"); + } + + if (config.MIN_TIEBREAKER_ACTIVATION_TIMEOUT > config.TIEBREAKER_ACTIVATION_TIMEOUT) { + revert InvalidParameter("MIN_TIEBREAKER_ACTIVATION_TIMEOUT"); + } + + if (config.TIEBREAKER_ACTIVATION_TIMEOUT > config.MAX_TIEBREAKER_ACTIVATION_TIMEOUT) { + revert InvalidParameter("TIEBREAKER_ACTIVATION_TIMEOUT"); + } + + if (config.DYNAMIC_TIMELOCK_MIN_DURATION > config.DYNAMIC_TIMELOCK_MAX_DURATION) { + revert InvalidParameter("DYNAMIC_TIMELOCK_MIN_DURATION"); + } } function checkCommitteeQuorum(address[] memory committee, uint256 quorum, string memory message) internal pure { From 3490786b10bcfb40694c403bf3686b236dc9c999 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Thu, 12 Sep 2024 23:51:22 +0400 Subject: [PATCH 021/107] Fix DeployConfig fields according to changed DGConfig fields --- scripts/deploy/Config.sol | 12 ++--- scripts/deploy/ContractsDeployment.sol | 27 +++++------ scripts/deploy/DeployVerification.sol | 45 +++++++----------- scripts/deploy/EnvConfig.s.sol | 66 ++++++++------------------ 4 files changed, 58 insertions(+), 92 deletions(-) diff --git a/scripts/deploy/Config.sol b/scripts/deploy/Config.sol index e08a70f8..08d4fcb6 100644 --- a/scripts/deploy/Config.sol +++ b/scripts/deploy/Config.sol @@ -47,15 +47,15 @@ struct DeployConfig { PercentD16 FIRST_SEAL_RAGE_QUIT_SUPPORT; PercentD16 SECOND_SEAL_RAGE_QUIT_SUPPORT; Duration MIN_ASSETS_LOCK_DURATION; - Duration DYNAMIC_TIMELOCK_MIN_DURATION; - Duration DYNAMIC_TIMELOCK_MAX_DURATION; + Duration VETO_SIGNALLING_MIN_DURATION; + Duration VETO_SIGNALLING_MAX_DURATION; Duration VETO_SIGNALLING_MIN_ACTIVE_DURATION; Duration VETO_SIGNALLING_DEACTIVATION_MAX_DURATION; Duration VETO_COOLDOWN_DURATION; - Duration RAGE_QUIT_EXTENSION_DELAY; - Duration RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK; - uint256 RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER; - uint256[3] RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS; + Duration RAGE_QUIT_EXTENSION_PERIOD_DURATION; + Duration RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY; + Duration RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY; + Duration RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH; } struct LidoContracts { diff --git a/scripts/deploy/ContractsDeployment.sol b/scripts/deploy/ContractsDeployment.sol index 6a48b4e1..27bb905f 100644 --- a/scripts/deploy/ContractsDeployment.sol +++ b/scripts/deploy/ContractsDeployment.sol @@ -21,10 +21,10 @@ 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"; +import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; import {DeployConfig, LidoContracts, getSubCommitteeData} from "./Config.sol"; @@ -38,7 +38,7 @@ struct DeployedContracts { ResealManager resealManager; DualGovernance dualGovernance; ResealCommittee resealCommittee; - TiebreakerCore tiebreakerCoreCommittee; + TiebreakerCoreCommittee tiebreakerCoreCommittee; address[] tiebreakerSubCommittees; } @@ -251,18 +251,17 @@ library DGContractsDeployment { secondSealRageQuitSupport: dgDeployConfig.SECOND_SEAL_RAGE_QUIT_SUPPORT, // minAssetsLockDuration: dgDeployConfig.MIN_ASSETS_LOCK_DURATION, - dynamicTimelockMinDuration: dgDeployConfig.DYNAMIC_TIMELOCK_MIN_DURATION, - dynamicTimelockMaxDuration: dgDeployConfig.DYNAMIC_TIMELOCK_MAX_DURATION, + vetoSignallingMinDuration: dgDeployConfig.VETO_SIGNALLING_MIN_DURATION, + vetoSignallingMaxDuration: dgDeployConfig.VETO_SIGNALLING_MAX_DURATION, // vetoSignallingMinActiveDuration: dgDeployConfig.VETO_SIGNALLING_MIN_ACTIVE_DURATION, vetoSignallingDeactivationMaxDuration: dgDeployConfig.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION, vetoCooldownDuration: dgDeployConfig.VETO_COOLDOWN_DURATION, // - rageQuitExtensionDelay: dgDeployConfig.RAGE_QUIT_EXTENSION_DELAY, - rageQuitEthWithdrawalsMinTimelock: dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK, - rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: dgDeployConfig - .RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER, - rageQuitEthWithdrawalsTimelockGrowthCoeffs: dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS + rageQuitExtensionPeriodDuration: dgDeployConfig.RAGE_QUIT_EXTENSION_PERIOD_DURATION, + rageQuitEthWithdrawalsMinDelay: dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY, + rageQuitEthWithdrawalsMaxDelay: dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY, + rageQuitEthWithdrawalsDelayGrowth: dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH }) ); } @@ -296,13 +295,13 @@ library DGContractsDeployment { address owner, address dualGovernance, Duration executionDelay - ) internal returns (TiebreakerCore) { - return new TiebreakerCore({owner: owner, dualGovernance: dualGovernance, timelock: executionDelay}); + ) internal returns (TiebreakerCoreCommittee) { + return new TiebreakerCoreCommittee({owner: owner, dualGovernance: dualGovernance, timelock: executionDelay}); } function deployTiebreakerSubCommittees( address owner, - TiebreakerCore tiebreakerCoreCommittee, + TiebreakerCoreCommittee tiebreakerCoreCommittee, DeployConfig memory dgDeployConfig ) internal returns (address[] memory) { address[] memory coreCommitteeMembers = new address[](dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT); @@ -335,7 +334,7 @@ library DGContractsDeployment { owner: owner, executionQuorum: quorum, committeeMembers: members, - tiebreakerCore: tiebreakerCoreCommittee + tiebreakerCoreCommittee: tiebreakerCoreCommittee }); } diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index f3f570e5..ef7ae700 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -9,7 +9,7 @@ import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtec import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; -import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; +import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; @@ -252,12 +252,12 @@ library DeployVerification { "Incorrect parameter MIN_ASSETS_LOCK_DURATION" ); require( - dgConfig.dynamicTimelockMinDuration == dgDeployConfig.DYNAMIC_TIMELOCK_MIN_DURATION, - "Incorrect parameter DYNAMIC_TIMELOCK_MIN_DURATION" + dgConfig.vetoSignallingMinDuration == dgDeployConfig.VETO_SIGNALLING_MIN_DURATION, + "Incorrect parameter VETO_SIGNALLING_MIN_DURATION" ); require( - dgConfig.dynamicTimelockMaxDuration == dgDeployConfig.DYNAMIC_TIMELOCK_MAX_DURATION, - "Incorrect parameter DYNAMIC_TIMELOCK_MAX_DURATION" + dgConfig.vetoSignallingMaxDuration == dgDeployConfig.VETO_SIGNALLING_MAX_DURATION, + "Incorrect parameter VETO_SIGNALLING_MAX_DURATION" ); require( dgConfig.vetoSignallingMinActiveDuration == dgDeployConfig.VETO_SIGNALLING_MIN_ACTIVE_DURATION, @@ -272,32 +272,20 @@ library DeployVerification { "Incorrect parameter VETO_COOLDOWN_DURATION" ); require( - dgConfig.rageQuitExtensionDelay == dgDeployConfig.RAGE_QUIT_EXTENSION_DELAY, - "Incorrect parameter RAGE_QUIT_EXTENSION_DELAY" + dgConfig.rageQuitExtensionPeriodDuration == dgDeployConfig.RAGE_QUIT_EXTENSION_PERIOD_DURATION, + "Incorrect parameter RAGE_QUIT_EXTENSION_PERIOD_DURATION" ); require( - dgConfig.rageQuitEthWithdrawalsMinTimelock == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK, - "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK" + dgConfig.rageQuitEthWithdrawalsMinDelay == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY, + "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY" ); require( - dgConfig.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber - == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER, - "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER" + dgConfig.rageQuitEthWithdrawalsMaxDelay == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY, + "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY" ); require( - dgConfig.rageQuitEthWithdrawalsTimelockGrowthCoeffs[0] - == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[0], - "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[0]" - ); - require( - dgConfig.rageQuitEthWithdrawalsTimelockGrowthCoeffs[1] - == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[1], - "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[1]" - ); - require( - dgConfig.rageQuitEthWithdrawalsTimelockGrowthCoeffs[2] - == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[2], - "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[2]" + dgConfig.rageQuitEthWithdrawalsDelayGrowth == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH, + "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH" ); require(dg.getState() == State.Normal, "Incorrect DualGovernance state"); @@ -320,7 +308,10 @@ library DeployVerification { "Incorrect DualGovernance state normalOrVetoCooldownExitedAt" ); require(stateDetails.rageQuitRound == 0, "Incorrect DualGovernance state rageQuitRound"); - require(stateDetails.dynamicDelay == Durations.ZERO, "Incorrect DualGovernance state dynamicDelay"); + require( + stateDetails.vetoSignallingDuration == Durations.ZERO, + "Incorrect DualGovernance state vetoSignallingDuration" + ); ITiebreaker.TiebreakerDetails memory ts = dg.getTiebreakerDetails(); require( @@ -339,7 +330,7 @@ library DeployVerification { DeployedAddresses memory res, DeployConfig memory dgDeployConfig ) internal view { - TiebreakerCore tcc = TiebreakerCore(res.tiebreakerCoreCommittee); + TiebreakerCoreCommittee tcc = TiebreakerCoreCommittee(res.tiebreakerCoreCommittee); require(tcc.owner() == res.adminExecutor, "TiebreakerCoreCommittee owner != adminExecutor"); require( tcc.timelockDuration() == dgDeployConfig.TIEBREAKER_EXECUTION_DELAY, diff --git a/scripts/deploy/EnvConfig.s.sol b/scripts/deploy/EnvConfig.s.sol index 937789b0..85182c38 100644 --- a/scripts/deploy/EnvConfig.s.sol +++ b/scripts/deploy/EnvConfig.s.sol @@ -31,7 +31,6 @@ bytes32 constant CHAIN_NAME_HOLESKY_HASH = keccak256(bytes("holesky")); bytes32 constant CHAIN_NAME_HOLESKY_MOCKS_HASH = keccak256(bytes("holesky-mocks")); contract DGDeployConfigProvider is Script { - error InvalidRageQuitETHWithdrawalsTimelockGrowthCoeffs(uint256[] coeffs); error InvalidQuorum(string committee, uint256 quorum); error InvalidParameter(string parameter); error InvalidChain(string chainName); @@ -59,22 +58,15 @@ contract DGDeployConfigProvider is Script { uint256 internal immutable DEFAULT_FIRST_SEAL_RAGE_QUIT_SUPPORT = 3_00; // 3% uint256 internal immutable DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT = 15_00; // 15% uint256 internal immutable DEFAULT_MIN_ASSETS_LOCK_DURATION = 5 hours; - uint256 internal immutable DEFAULT_DYNAMIC_TIMELOCK_MIN_DURATION = 3 days; - uint256 internal immutable DEFAULT_DYNAMIC_TIMELOCK_MAX_DURATION = 30 days; + uint256 internal immutable DEFAULT_VETO_SIGNALLING_MIN_DURATION = 3 days; + uint256 internal immutable DEFAULT_VETO_SIGNALLING_MAX_DURATION = 30 days; uint256 internal immutable DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION = 5 hours; uint256 internal immutable DEFAULT_VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = 5 days; uint256 internal immutable DEFAULT_VETO_COOLDOWN_DURATION = 4 days; - uint256 internal immutable DEFAULT_RAGE_QUIT_EXTENSION_DELAY = 7 days; - uint256 internal immutable DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK = 60 days; - uint256 internal immutable DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER = 2; - uint256[] internal DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS = new uint256[](3); - - constructor() { - // TODO: are these values correct as a default? - DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[0] = 1; - DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[1] = 0; - DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS[2] = 0; - } + uint256 internal immutable DEFAULT_RAGE_QUIT_EXTENSION_PERIOD_DURATION = 7 days; + uint256 internal immutable DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY = 30 days; + uint256 internal immutable DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY = 180 days; + uint256 internal immutable DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH = 15 days; function loadAndValidate() external returns (DeployConfig memory config) { config = DeployConfig({ @@ -158,11 +150,11 @@ contract DGDeployConfigProvider is Script { vm.envOr("SECOND_SEAL_RAGE_QUIT_SUPPORT", DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT) ), MIN_ASSETS_LOCK_DURATION: Durations.from(vm.envOr("MIN_ASSETS_LOCK_DURATION", DEFAULT_MIN_ASSETS_LOCK_DURATION)), - DYNAMIC_TIMELOCK_MIN_DURATION: Durations.from( - vm.envOr("DYNAMIC_TIMELOCK_MIN_DURATION", DEFAULT_DYNAMIC_TIMELOCK_MIN_DURATION) + VETO_SIGNALLING_MIN_DURATION: Durations.from( + vm.envOr("VETO_SIGNALLING_MIN_DURATION", DEFAULT_VETO_SIGNALLING_MIN_DURATION) ), - DYNAMIC_TIMELOCK_MAX_DURATION: Durations.from( - vm.envOr("DYNAMIC_TIMELOCK_MAX_DURATION", DEFAULT_DYNAMIC_TIMELOCK_MAX_DURATION) + VETO_SIGNALLING_MAX_DURATION: Durations.from( + vm.envOr("VETO_SIGNALLING_MAX_DURATION", DEFAULT_VETO_SIGNALLING_MAX_DURATION) ), VETO_SIGNALLING_MIN_ACTIVE_DURATION: Durations.from( vm.envOr("VETO_SIGNALLING_MIN_ACTIVE_DURATION", DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION) @@ -171,17 +163,18 @@ contract DGDeployConfigProvider is Script { vm.envOr("VETO_SIGNALLING_DEACTIVATION_MAX_DURATION", DEFAULT_VETO_SIGNALLING_DEACTIVATION_MAX_DURATION) ), VETO_COOLDOWN_DURATION: Durations.from(vm.envOr("VETO_COOLDOWN_DURATION", DEFAULT_VETO_COOLDOWN_DURATION)), - RAGE_QUIT_EXTENSION_DELAY: Durations.from( - vm.envOr("RAGE_QUIT_EXTENSION_DELAY", DEFAULT_RAGE_QUIT_EXTENSION_DELAY) + RAGE_QUIT_EXTENSION_PERIOD_DURATION: Durations.from( + vm.envOr("RAGE_QUIT_EXTENSION_PERIOD_DURATION", DEFAULT_RAGE_QUIT_EXTENSION_PERIOD_DURATION) ), - RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK: Durations.from( - vm.envOr("RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK", DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK) + RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY: Durations.from( + vm.envOr("RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY", DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY) ), - RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER: vm.envOr( - "RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER", - DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER + RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY: Durations.from( + vm.envOr("RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY", DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY) ), - RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS: getValidCoeffs() + RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH: Durations.from( + vm.envOr("RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH", DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH) + ) }); validateConfig(config); @@ -226,23 +219,6 @@ contract DGDeployConfigProvider is Script { }); } - function getValidCoeffs() internal returns (uint256[3] memory coeffs) { - uint256[] memory coeffsRaw = vm.envOr( - "RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS", - ARRAY_SEPARATOR, - DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFFS - ); - - if (coeffsRaw.length != 3) { - revert InvalidRageQuitETHWithdrawalsTimelockGrowthCoeffs(coeffsRaw); - } - - // TODO: validate each coeff value? - coeffs[0] = coeffsRaw[0]; - coeffs[1] = coeffsRaw[1]; - coeffs[2] = coeffsRaw[2]; - } - function validateConfig(DeployConfig memory config) internal pure { checkCommitteeQuorum( config.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS, @@ -369,8 +345,8 @@ contract DGDeployConfigProvider is Script { revert InvalidParameter("TIEBREAKER_ACTIVATION_TIMEOUT"); } - if (config.DYNAMIC_TIMELOCK_MIN_DURATION > config.DYNAMIC_TIMELOCK_MAX_DURATION) { - revert InvalidParameter("DYNAMIC_TIMELOCK_MIN_DURATION"); + if (config.VETO_SIGNALLING_MIN_DURATION > config.VETO_SIGNALLING_MAX_DURATION) { + revert InvalidParameter("VETO_SIGNALLING_MIN_DURATION"); } } From 67a1f62edc6c888bb9b3cadb7d4d268c724acb9b Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:58:18 +0400 Subject: [PATCH 022/107] Update forge-std library --- .gitignore | 1 + .gitmodules | 1 + lib/forge-std | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 37222e47..cec3ecb0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ out/ /broadcast/*/31337/ /broadcast/**/dry-run/ .vscode/ +deploy-config diff --git a/.gitmodules b/.gitmodules index 5f8d4d17..fb0de12d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,7 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std + branch = tags/v1.9.3 [submodule "lib/kontrol-cheatcodes"] path = lib/kontrol-cheatcodes url = https://github.com/runtimeverification/kontrol-cheatcodes diff --git a/lib/forge-std b/lib/forge-std index 155d547c..8f24d6b0 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 155d547c449afa8715f538d69454b83944117811 +Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa From 4779f2ef3826a58595edf5a6c52e42c9cd45f43a Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:00:55 +0400 Subject: [PATCH 023/107] Allow to read files from project dir. Update .env.example. --- .env.example | 9 ++------- foundry.toml | 1 + 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index a94ee95c..b39cb898 100644 --- a/.env.example +++ b/.env.example @@ -3,10 +3,5 @@ MAINNET_RPC_URL= # Deploy script env vars CHAIN=<"mainnet" OR "holesky"> DEPLOYER_PRIVATE_KEY=... -EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS=addr1,addr2,addr3 -EMERGENCY_EXECUTION_COMMITTEE_MEMBERS=addr1,addr2,addr3 -TIEBREAKER_SUB_COMMITTEE_1_MEMBERS=addr1,addr2,addr3 -TIEBREAKER_SUB_COMMITTEE_2_MEMBERS=addr1,addr2,addr3 -TIEBREAKER_SUB_COMMITTEES_QUORUMS=2,2 -TIEBREAKER_SUB_COMMITTEES_COUNT=2 -RESEAL_COMMITTEE_MEMBERS=addr1,addr2,addr3 +DEPLOY_CONFIG_FILE_PATH=deploy-config/deploy-config.json +DEPLOYED_ADDRESSES_FILE_PATH=deploy-config/deployed-addrs.json diff --git a/foundry.toml b/foundry.toml index e2331b32..fc060006 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,6 +7,7 @@ test = 'test' cache_path = 'cache_forge' # solc-version = "0.8.26" no-match-path = 'test/kontrol/*' +fs_permissions = [{ access = "read", path = "./"}] [profile.kprove] src = 'test/kontrol' From df422331411134400c2f9036a74478ab13e7b62f Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:01:35 +0400 Subject: [PATCH 024/107] Move deploy config defaults to Config.sol --- scripts/deploy/Config.sol | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/scripts/deploy/Config.sol b/scripts/deploy/Config.sol index 08d4fcb6..9d5fbf23 100644 --- a/scripts/deploy/Config.sol +++ b/scripts/deploy/Config.sol @@ -10,6 +10,43 @@ import {IAragonVoting} from "contracts/interfaces/IAragonVoting.sol"; import {Duration} from "contracts/types/Duration.sol"; import {PercentD16} from "contracts/types/PercentD16.sol"; +uint256 constant DEFAULT_AFTER_SUBMIT_DELAY = 3 days; +uint256 constant DEFAULT_MAX_AFTER_SUBMIT_DELAY = 45 days; +uint256 constant DEFAULT_AFTER_SCHEDULE_DELAY = 3 days; +uint256 constant DEFAULT_MAX_AFTER_SCHEDULE_DELAY = 45 days; +uint256 constant DEFAULT_EMERGENCY_MODE_DURATION = 180 days; +uint256 constant DEFAULT_MAX_EMERGENCY_MODE_DURATION = 365 days; +uint256 constant DEFAULT_EMERGENCY_PROTECTION_DURATION = 90 days; +uint256 constant DEFAULT_MAX_EMERGENCY_PROTECTION_DURATION = 365 days; +uint256 constant DEFAULT_EMERGENCY_ACTIVATION_COMMITTEE_QUORUM = 3; +uint256 constant DEFAULT_EMERGENCY_EXECUTION_COMMITTEE_QUORUM = 5; +uint256 constant DEFAULT_TIEBREAKER_CORE_QUORUM = 1; +uint256 constant DEFAULT_TIEBREAKER_EXECUTION_DELAY = 30 days; +uint256 constant DEFAULT_TIEBREAKER_SUB_COMMITTEES_COUNT = 2; +uint256 constant DEFAULT_RESEAL_COMMITTEE_QUORUM = 3; +uint256 constant DEFAULT_MIN_WITHDRAWALS_BATCH_SIZE = 4; +uint256 constant DEFAULT_MIN_TIEBREAKER_ACTIVATION_TIMEOUT = 90 days; +uint256 constant DEFAULT_TIEBREAKER_ACTIVATION_TIMEOUT = 365 days; +uint256 constant DEFAULT_MAX_TIEBREAKER_ACTIVATION_TIMEOUT = 730 days; +uint256 constant DEFAULT_MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = 255; + +uint256 constant DEFAULT_FIRST_SEAL_RAGE_QUIT_SUPPORT = 3_00; // 3% +uint256 constant DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT = 15_00; // 15% +uint256 constant DEFAULT_MIN_ASSETS_LOCK_DURATION = 5 hours; +uint256 constant DEFAULT_VETO_SIGNALLING_MIN_DURATION = 3 days; +uint256 constant DEFAULT_VETO_SIGNALLING_MAX_DURATION = 30 days; +uint256 constant DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION = 5 hours; +uint256 constant DEFAULT_VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = 5 days; +uint256 constant DEFAULT_VETO_COOLDOWN_DURATION = 4 days; +uint256 constant DEFAULT_RAGE_QUIT_EXTENSION_PERIOD_DURATION = 7 days; +uint256 constant DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY = 30 days; +uint256 constant DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY = 180 days; +uint256 constant DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH = 15 days; + +bytes32 constant CHAIN_NAME_MAINNET_HASH = keccak256(bytes("mainnet")); +bytes32 constant CHAIN_NAME_HOLESKY_HASH = keccak256(bytes("holesky")); +bytes32 constant CHAIN_NAME_HOLESKY_MOCKS_HASH = keccak256(bytes("holesky-mocks")); + struct DeployConfig { Duration AFTER_SUBMIT_DELAY; Duration MAX_AFTER_SUBMIT_DELAY; From 1a606b04504572d17d7f1f05d47cb56fb7cc9656 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:02:46 +0400 Subject: [PATCH 025/107] Read deploy parameters from JSON file --- scripts/deploy/ContractsDeployment.sol | 101 ++++--- scripts/deploy/DeployBase.s.sol | 9 +- scripts/deploy/DeployConfigurable.s.sol | 6 +- .../{EnvConfig.s.sol => JsonConfig.s.sol} | 277 +++++++++++------- scripts/deploy/Verify.s.sol | 50 +++- 5 files changed, 274 insertions(+), 169 deletions(-) rename scripts/deploy/{EnvConfig.s.sol => JsonConfig.s.sol} (57%) diff --git a/scripts/deploy/ContractsDeployment.sol b/scripts/deploy/ContractsDeployment.sol index 27bb905f..69d92055 100644 --- a/scripts/deploy/ContractsDeployment.sol +++ b/scripts/deploy/ContractsDeployment.sol @@ -26,6 +26,8 @@ import { import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; +import {ITimelock} from "contracts/interfaces/ITimelock.sol"; +import {IResealManager} from "contracts/interfaces/IResealManager.sol"; import {DeployConfig, LidoContracts, getSubCommitteeData} from "./Config.sol"; @@ -39,7 +41,7 @@ struct DeployedContracts { DualGovernance dualGovernance; ResealCommittee resealCommittee; TiebreakerCoreCommittee tiebreakerCoreCommittee; - address[] tiebreakerSubCommittees; + TiebreakerSubCommittee[] tiebreakerSubCommittees; } library DGContractsDeployment { @@ -79,35 +81,7 @@ library DGContractsDeployment { // --- // Finalize Setup // --- - contracts.adminExecutor.execute( - address(dualGovernance), - 0, - abi.encodeCall( - dualGovernance.registerProposer, (address(lidoAddresses.voting), address(contracts.adminExecutor)) - ) - ); - contracts.adminExecutor.execute( - address(dualGovernance), - 0, - abi.encodeCall(dualGovernance.setTiebreakerActivationTimeout, dgDeployConfig.TIEBREAKER_ACTIVATION_TIMEOUT) - ); - contracts.adminExecutor.execute( - address(dualGovernance), - 0, - abi.encodeCall(dualGovernance.setTiebreakerCommittee, address(contracts.tiebreakerCoreCommittee)) - ); - contracts.adminExecutor.execute( - address(dualGovernance), - 0, - abi.encodeCall( - dualGovernance.addTiebreakerSealableWithdrawalBlocker, address(lidoAddresses.withdrawalQueue) - ) - ); - contracts.adminExecutor.execute( - address(dualGovernance), - 0, - abi.encodeCall(dualGovernance.setResealCommittee, address(contracts.resealCommittee)) - ); + configureDualGovernance(dgDeployConfig, lidoAddresses, contracts); finalizeEmergencyProtectedTimelockDeploy( contracts.adminExecutor, contracts.timelock, address(dualGovernance), dgDeployConfig @@ -232,12 +206,12 @@ library DGContractsDeployment { function deployTimelockedGovernance( address governance, - EmergencyProtectedTimelock timelock + ITimelock timelock ) internal returns (TimelockedGovernance) { return new TimelockedGovernance(governance, timelock); } - function deployResealManager(EmergencyProtectedTimelock timelock) internal returns (ResealManager) { + function deployResealManager(ITimelock timelock) internal returns (ResealManager) { return new ResealManager(timelock); } @@ -268,8 +242,8 @@ library DGContractsDeployment { function deployDualGovernance( IDualGovernanceConfigProvider configProvider, - EmergencyProtectedTimelock timelock, - ResealManager resealManager, + ITimelock timelock, + IResealManager resealManager, DeployConfig memory dgDeployConfig, LidoContracts memory lidoAddresses ) internal returns (DualGovernance) { @@ -303,25 +277,25 @@ library DGContractsDeployment { address owner, TiebreakerCoreCommittee tiebreakerCoreCommittee, DeployConfig memory dgDeployConfig - ) internal returns (address[] memory) { + ) internal returns (TiebreakerSubCommittee[] memory tiebreakerSubCommittees) { + tiebreakerSubCommittees = new TiebreakerSubCommittee[](dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT); address[] memory coreCommitteeMembers = new address[](dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT); for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { (uint256 quorum, address[] memory members) = getSubCommitteeData(i, dgDeployConfig); - coreCommitteeMembers[i] = address( - deployTiebreakerSubCommittee({ - owner: owner, - quorum: quorum, - members: members, - tiebreakerCoreCommittee: address(tiebreakerCoreCommittee) - }) - ); + tiebreakerSubCommittees[i] = deployTiebreakerSubCommittee({ + owner: owner, + quorum: quorum, + members: members, + tiebreakerCoreCommittee: address(tiebreakerCoreCommittee) + }); + coreCommitteeMembers[i] = address(tiebreakerSubCommittees[i]); } tiebreakerCoreCommittee.addMembers(coreCommitteeMembers, dgDeployConfig.TIEBREAKER_CORE_QUORUM); - return coreCommitteeMembers; + return tiebreakerSubCommittees; } function deployTiebreakerSubCommittee( @@ -350,6 +324,45 @@ library DGContractsDeployment { return new ResealCommittee(adminExecutor, committeeMembers, quorum, dualGovernance, Durations.from(0)); } + function configureDualGovernance( + DeployConfig memory dgDeployConfig, + LidoContracts memory lidoAddresses, + DeployedContracts memory contracts + ) internal { + contracts.adminExecutor.execute( + address(contracts.dualGovernance), + 0, + abi.encodeCall( + contracts.dualGovernance.registerProposer, + (address(lidoAddresses.voting), address(contracts.adminExecutor)) + ) + ); + contracts.adminExecutor.execute( + address(contracts.dualGovernance), + 0, + abi.encodeCall( + contracts.dualGovernance.setTiebreakerActivationTimeout, dgDeployConfig.TIEBREAKER_ACTIVATION_TIMEOUT + ) + ); + contracts.adminExecutor.execute( + address(contracts.dualGovernance), + 0, + abi.encodeCall(contracts.dualGovernance.setTiebreakerCommittee, address(contracts.tiebreakerCoreCommittee)) + ); + contracts.adminExecutor.execute( + address(contracts.dualGovernance), + 0, + abi.encodeCall( + contracts.dualGovernance.addTiebreakerSealableWithdrawalBlocker, address(lidoAddresses.withdrawalQueue) + ) + ); + contracts.adminExecutor.execute( + address(contracts.dualGovernance), + 0, + abi.encodeCall(contracts.dualGovernance.setResealCommittee, address(contracts.resealCommittee)) + ); + } + function finalizeEmergencyProtectedTimelockDeploy( Executor adminExecutor, EmergencyProtectedTimelock timelock, diff --git a/scripts/deploy/DeployBase.s.sol b/scripts/deploy/DeployBase.s.sol index b192fea0..e9f1f169 100644 --- a/scripts/deploy/DeployBase.s.sol +++ b/scripts/deploy/DeployBase.s.sol @@ -52,6 +52,11 @@ abstract contract DeployBase is Script { pure returns (DeployVerification.DeployedAddresses memory) { + address[] memory tiebreakerSubCommittees = new address[](contracts.tiebreakerSubCommittees.length); + for (uint256 i = 0; i < contracts.tiebreakerSubCommittees.length; ++i) { + tiebreakerSubCommittees[i] = address(contracts.tiebreakerSubCommittees[i]); + } + return DeployVerification.DeployedAddresses({ adminExecutor: payable(address(contracts.adminExecutor)), timelock: address(contracts.timelock), @@ -62,11 +67,11 @@ abstract contract DeployBase is Script { dualGovernance: address(contracts.dualGovernance), resealCommittee: address(contracts.resealCommittee), tiebreakerCoreCommittee: address(contracts.tiebreakerCoreCommittee), - tiebreakerSubCommittees: contracts.tiebreakerSubCommittees + tiebreakerSubCommittees: tiebreakerSubCommittees }); } - function printAddresses(DeployVerification.DeployedAddresses memory res) internal view { + function printAddresses(DeployVerification.DeployedAddresses memory res) internal pure { console.log("DG deployed successfully"); console.log("DualGovernance address", res.dualGovernance); console.log("ResealManager address", res.resealManager); diff --git a/scripts/deploy/DeployConfigurable.s.sol b/scripts/deploy/DeployConfigurable.s.sol index 078acbc4..f2876dd6 100644 --- a/scripts/deploy/DeployConfigurable.s.sol +++ b/scripts/deploy/DeployConfigurable.s.sol @@ -1,13 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {DGDeployConfigProvider} from "./EnvConfig.s.sol"; +import {DGDeployJSONConfigProvider} from "./JsonConfig.s.sol"; import {DeployBase} from "./DeployBase.s.sol"; contract DeployConfigurable is DeployBase { constructor() { string memory chainName = vm.envString("CHAIN"); - DGDeployConfigProvider configProvider = new DGDeployConfigProvider(); + string memory configFilePath = vm.envString("DEPLOY_CONFIG_FILE_PATH"); + + DGDeployJSONConfigProvider configProvider = new DGDeployJSONConfigProvider(configFilePath); config = configProvider.loadAndValidate(); lidoAddresses = configProvider.getLidoAddresses(chainName); } diff --git a/scripts/deploy/EnvConfig.s.sol b/scripts/deploy/JsonConfig.s.sol similarity index 57% rename from scripts/deploy/EnvConfig.s.sol rename to scripts/deploy/JsonConfig.s.sol index 85182c38..f128c772 100644 --- a/scripts/deploy/EnvConfig.s.sol +++ b/scripts/deploy/JsonConfig.s.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.26; /* solhint-disable no-console, var-name-mixedcase */ import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; import {console} from "forge-std/console.sol"; import {IStETH} from "contracts/interfaces/IStETH.sol"; import {IWstETH} from "contracts/interfaces/IWstETH.sol"; @@ -23,157 +24,208 @@ import { } from "addresses/holesky-addresses.sol"; import {Durations} from "contracts/types/Duration.sol"; import {PercentsD16} from "contracts/types/PercentD16.sol"; -import {DeployConfig, LidoContracts} from "./Config.sol"; - -string constant ARRAY_SEPARATOR = ","; -bytes32 constant CHAIN_NAME_MAINNET_HASH = keccak256(bytes("mainnet")); -bytes32 constant CHAIN_NAME_HOLESKY_HASH = keccak256(bytes("holesky")); -bytes32 constant CHAIN_NAME_HOLESKY_MOCKS_HASH = keccak256(bytes("holesky-mocks")); - -contract DGDeployConfigProvider is Script { +import { + DeployConfig, + LidoContracts, + DEFAULT_AFTER_SUBMIT_DELAY, + DEFAULT_MAX_AFTER_SUBMIT_DELAY, + DEFAULT_AFTER_SCHEDULE_DELAY, + DEFAULT_MAX_AFTER_SCHEDULE_DELAY, + DEFAULT_EMERGENCY_MODE_DURATION, + DEFAULT_MAX_EMERGENCY_MODE_DURATION, + DEFAULT_EMERGENCY_PROTECTION_DURATION, + DEFAULT_MAX_EMERGENCY_PROTECTION_DURATION, + DEFAULT_EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, + DEFAULT_EMERGENCY_EXECUTION_COMMITTEE_QUORUM, + DEFAULT_TIEBREAKER_CORE_QUORUM, + DEFAULT_TIEBREAKER_EXECUTION_DELAY, + DEFAULT_TIEBREAKER_SUB_COMMITTEES_COUNT, + DEFAULT_RESEAL_COMMITTEE_QUORUM, + DEFAULT_MIN_WITHDRAWALS_BATCH_SIZE, + DEFAULT_MIN_TIEBREAKER_ACTIVATION_TIMEOUT, + DEFAULT_TIEBREAKER_ACTIVATION_TIMEOUT, + DEFAULT_MAX_TIEBREAKER_ACTIVATION_TIMEOUT, + DEFAULT_MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT, + DEFAULT_FIRST_SEAL_RAGE_QUIT_SUPPORT, + DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT, + DEFAULT_MIN_ASSETS_LOCK_DURATION, + DEFAULT_VETO_SIGNALLING_MIN_DURATION, + DEFAULT_VETO_SIGNALLING_MAX_DURATION, + DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION, + DEFAULT_VETO_SIGNALLING_DEACTIVATION_MAX_DURATION, + DEFAULT_VETO_COOLDOWN_DURATION, + DEFAULT_RAGE_QUIT_EXTENSION_PERIOD_DURATION, + DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY, + DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY, + DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH, + CHAIN_NAME_MAINNET_HASH, + CHAIN_NAME_HOLESKY_HASH, + CHAIN_NAME_HOLESKY_MOCKS_HASH +} from "./Config.sol"; + +contract DGDeployJSONConfigProvider is Script { error InvalidQuorum(string committee, uint256 quorum); error InvalidParameter(string parameter); error InvalidChain(string chainName); - uint256 internal immutable DEFAULT_AFTER_SUBMIT_DELAY = 3 days; - uint256 internal immutable DEFAULT_MAX_AFTER_SUBMIT_DELAY = 45 days; - uint256 internal immutable DEFAULT_AFTER_SCHEDULE_DELAY = 3 days; - uint256 internal immutable DEFAULT_MAX_AFTER_SCHEDULE_DELAY = 45 days; - uint256 internal immutable DEFAULT_EMERGENCY_MODE_DURATION = 180 days; - uint256 internal immutable DEFAULT_MAX_EMERGENCY_MODE_DURATION = 365 days; - uint256 internal immutable DEFAULT_EMERGENCY_PROTECTION_DURATION = 90 days; - uint256 internal immutable DEFAULT_MAX_EMERGENCY_PROTECTION_DURATION = 365 days; - uint256 internal immutable DEFAULT_EMERGENCY_ACTIVATION_COMMITTEE_QUORUM = 3; - uint256 internal immutable DEFAULT_EMERGENCY_EXECUTION_COMMITTEE_QUORUM = 5; - uint256 internal immutable DEFAULT_TIEBREAKER_CORE_QUORUM = 1; - uint256 internal immutable DEFAULT_TIEBREAKER_EXECUTION_DELAY = 30 days; - uint256 internal immutable DEFAULT_TIEBREAKER_SUB_COMMITTEES_COUNT = 2; - uint256 internal immutable DEFAULT_RESEAL_COMMITTEE_QUORUM = 3; - uint256 internal immutable DEFAULT_MIN_WITHDRAWALS_BATCH_SIZE = 4; - uint256 internal immutable DEFAULT_MIN_TIEBREAKER_ACTIVATION_TIMEOUT = 90 days; - uint256 internal immutable DEFAULT_TIEBREAKER_ACTIVATION_TIMEOUT = 365 days; - uint256 internal immutable DEFAULT_MAX_TIEBREAKER_ACTIVATION_TIMEOUT = 730 days; - uint256 internal immutable DEFAULT_MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = 255; - - uint256 internal immutable DEFAULT_FIRST_SEAL_RAGE_QUIT_SUPPORT = 3_00; // 3% - uint256 internal immutable DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT = 15_00; // 15% - uint256 internal immutable DEFAULT_MIN_ASSETS_LOCK_DURATION = 5 hours; - uint256 internal immutable DEFAULT_VETO_SIGNALLING_MIN_DURATION = 3 days; - uint256 internal immutable DEFAULT_VETO_SIGNALLING_MAX_DURATION = 30 days; - uint256 internal immutable DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION = 5 hours; - uint256 internal immutable DEFAULT_VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = 5 days; - uint256 internal immutable DEFAULT_VETO_COOLDOWN_DURATION = 4 days; - uint256 internal immutable DEFAULT_RAGE_QUIT_EXTENSION_PERIOD_DURATION = 7 days; - uint256 internal immutable DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY = 30 days; - uint256 internal immutable DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY = 180 days; - uint256 internal immutable DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH = 15 days; - - function loadAndValidate() external returns (DeployConfig memory config) { + string private configFilePath; + + constructor(string memory _configFilePath) { + configFilePath = _configFilePath; + } + + function loadAndValidate() external view returns (DeployConfig memory config) { + string memory jsonConfig = loadConfigFile(); + config = DeployConfig({ - AFTER_SUBMIT_DELAY: Durations.from(vm.envOr("AFTER_SUBMIT_DELAY", DEFAULT_AFTER_SUBMIT_DELAY)), - MAX_AFTER_SUBMIT_DELAY: Durations.from(vm.envOr("MAX_AFTER_SUBMIT_DELAY", DEFAULT_MAX_AFTER_SUBMIT_DELAY)), - AFTER_SCHEDULE_DELAY: Durations.from(vm.envOr("AFTER_SCHEDULE_DELAY", DEFAULT_AFTER_SCHEDULE_DELAY)), - MAX_AFTER_SCHEDULE_DELAY: Durations.from(vm.envOr("MAX_AFTER_SCHEDULE_DELAY", DEFAULT_MAX_AFTER_SCHEDULE_DELAY)), - EMERGENCY_MODE_DURATION: Durations.from(vm.envOr("EMERGENCY_MODE_DURATION", DEFAULT_EMERGENCY_MODE_DURATION)), + AFTER_SUBMIT_DELAY: Durations.from( + stdJson.readUintOr(jsonConfig, ".AFTER_SUBMIT_DELAY", DEFAULT_AFTER_SUBMIT_DELAY) + ), + MAX_AFTER_SUBMIT_DELAY: Durations.from( + stdJson.readUintOr(jsonConfig, ".MAX_AFTER_SUBMIT_DELAY", DEFAULT_MAX_AFTER_SUBMIT_DELAY) + ), + AFTER_SCHEDULE_DELAY: Durations.from( + stdJson.readUintOr(jsonConfig, ".AFTER_SCHEDULE_DELAY", DEFAULT_AFTER_SCHEDULE_DELAY) + ), + MAX_AFTER_SCHEDULE_DELAY: Durations.from( + stdJson.readUintOr(jsonConfig, ".MAX_AFTER_SCHEDULE_DELAY", DEFAULT_MAX_AFTER_SCHEDULE_DELAY) + ), + EMERGENCY_MODE_DURATION: Durations.from( + stdJson.readUintOr(jsonConfig, ".EMERGENCY_MODE_DURATION", DEFAULT_EMERGENCY_MODE_DURATION) + ), MAX_EMERGENCY_MODE_DURATION: Durations.from( - vm.envOr("MAX_EMERGENCY_MODE_DURATION", DEFAULT_MAX_EMERGENCY_MODE_DURATION) + stdJson.readUintOr(jsonConfig, ".MAX_EMERGENCY_MODE_DURATION", DEFAULT_MAX_EMERGENCY_MODE_DURATION) ), EMERGENCY_PROTECTION_DURATION: Durations.from( - vm.envOr("EMERGENCY_PROTECTION_DURATION", DEFAULT_EMERGENCY_PROTECTION_DURATION) + stdJson.readUintOr(jsonConfig, ".EMERGENCY_PROTECTION_DURATION", DEFAULT_EMERGENCY_PROTECTION_DURATION) ), MAX_EMERGENCY_PROTECTION_DURATION: Durations.from( - vm.envOr("MAX_EMERGENCY_PROTECTION_DURATION", DEFAULT_MAX_EMERGENCY_PROTECTION_DURATION) + stdJson.readUintOr( + jsonConfig, ".MAX_EMERGENCY_PROTECTION_DURATION", DEFAULT_MAX_EMERGENCY_PROTECTION_DURATION + ) ), - EMERGENCY_ACTIVATION_COMMITTEE_QUORUM: vm.envOr( - "EMERGENCY_ACTIVATION_COMMITTEE_QUORUM", DEFAULT_EMERGENCY_ACTIVATION_COMMITTEE_QUORUM + EMERGENCY_ACTIVATION_COMMITTEE_QUORUM: stdJson.readUintOr( + jsonConfig, ".EMERGENCY_ACTIVATION_COMMITTEE_QUORUM", DEFAULT_EMERGENCY_ACTIVATION_COMMITTEE_QUORUM ), - EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS: vm.envAddress("EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS", ARRAY_SEPARATOR), - EMERGENCY_EXECUTION_COMMITTEE_QUORUM: vm.envOr( - "EMERGENCY_EXECUTION_COMMITTEE_QUORUM", DEFAULT_EMERGENCY_EXECUTION_COMMITTEE_QUORUM + EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS: stdJson.readAddressArray( + jsonConfig, ".EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS" + ), + EMERGENCY_EXECUTION_COMMITTEE_QUORUM: stdJson.readUintOr( + jsonConfig, ".EMERGENCY_EXECUTION_COMMITTEE_QUORUM", DEFAULT_EMERGENCY_EXECUTION_COMMITTEE_QUORUM + ), + EMERGENCY_EXECUTION_COMMITTEE_MEMBERS: stdJson.readAddressArray( + jsonConfig, ".EMERGENCY_EXECUTION_COMMITTEE_MEMBERS" + ), + TIEBREAKER_CORE_QUORUM: stdJson.readUintOr( + jsonConfig, ".TIEBREAKER_CORE_QUORUM", DEFAULT_TIEBREAKER_CORE_QUORUM ), - EMERGENCY_EXECUTION_COMMITTEE_MEMBERS: vm.envAddress("EMERGENCY_EXECUTION_COMMITTEE_MEMBERS", ARRAY_SEPARATOR), - TIEBREAKER_CORE_QUORUM: vm.envOr("TIEBREAKER_CORE_QUORUM", DEFAULT_TIEBREAKER_CORE_QUORUM), TIEBREAKER_EXECUTION_DELAY: Durations.from( - vm.envOr("TIEBREAKER_EXECUTION_DELAY", DEFAULT_TIEBREAKER_EXECUTION_DELAY) + stdJson.readUintOr(jsonConfig, ".TIEBREAKER_EXECUTION_DELAY", DEFAULT_TIEBREAKER_EXECUTION_DELAY) ), - TIEBREAKER_SUB_COMMITTEES_COUNT: vm.envOr( - "TIEBREAKER_SUB_COMMITTEES_COUNT", DEFAULT_TIEBREAKER_SUB_COMMITTEES_COUNT + TIEBREAKER_SUB_COMMITTEES_COUNT: stdJson.readUintOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEES_COUNT", DEFAULT_TIEBREAKER_SUB_COMMITTEES_COUNT ), - TIEBREAKER_SUB_COMMITTEE_1_MEMBERS: vm.envAddress("TIEBREAKER_SUB_COMMITTEE_1_MEMBERS", ARRAY_SEPARATOR), - TIEBREAKER_SUB_COMMITTEE_2_MEMBERS: vm.envOr( - "TIEBREAKER_SUB_COMMITTEE_2_MEMBERS", ARRAY_SEPARATOR, new address[](0) + TIEBREAKER_SUB_COMMITTEE_1_MEMBERS: stdJson.readAddressArray(jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_1_MEMBERS"), + TIEBREAKER_SUB_COMMITTEE_2_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_2_MEMBERS", new address[](0) ), - TIEBREAKER_SUB_COMMITTEE_3_MEMBERS: vm.envOr( - "TIEBREAKER_SUB_COMMITTEE_3_MEMBERS", ARRAY_SEPARATOR, new address[](0) + TIEBREAKER_SUB_COMMITTEE_3_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_3_MEMBERS", new address[](0) ), - TIEBREAKER_SUB_COMMITTEE_4_MEMBERS: vm.envOr( - "TIEBREAKER_SUB_COMMITTEE_4_MEMBERS", ARRAY_SEPARATOR, new address[](0) + TIEBREAKER_SUB_COMMITTEE_4_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_4_MEMBERS", new address[](0) ), - TIEBREAKER_SUB_COMMITTEE_5_MEMBERS: vm.envOr( - "TIEBREAKER_SUB_COMMITTEE_5_MEMBERS", ARRAY_SEPARATOR, new address[](0) + TIEBREAKER_SUB_COMMITTEE_5_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_5_MEMBERS", new address[](0) ), - TIEBREAKER_SUB_COMMITTEE_6_MEMBERS: vm.envOr( - "TIEBREAKER_SUB_COMMITTEE_6_MEMBERS", ARRAY_SEPARATOR, new address[](0) + TIEBREAKER_SUB_COMMITTEE_6_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_6_MEMBERS", new address[](0) ), - TIEBREAKER_SUB_COMMITTEE_7_MEMBERS: vm.envOr( - "TIEBREAKER_SUB_COMMITTEE_7_MEMBERS", ARRAY_SEPARATOR, new address[](0) + TIEBREAKER_SUB_COMMITTEE_7_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_7_MEMBERS", new address[](0) ), - TIEBREAKER_SUB_COMMITTEE_8_MEMBERS: vm.envOr( - "TIEBREAKER_SUB_COMMITTEE_8_MEMBERS", ARRAY_SEPARATOR, new address[](0) + TIEBREAKER_SUB_COMMITTEE_8_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_8_MEMBERS", new address[](0) ), - TIEBREAKER_SUB_COMMITTEE_9_MEMBERS: vm.envOr( - "TIEBREAKER_SUB_COMMITTEE_9_MEMBERS", ARRAY_SEPARATOR, new address[](0) + TIEBREAKER_SUB_COMMITTEE_9_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_9_MEMBERS", new address[](0) ), - TIEBREAKER_SUB_COMMITTEE_10_MEMBERS: vm.envOr( - "TIEBREAKER_SUB_COMMITTEE_10_MEMBERS", ARRAY_SEPARATOR, new address[](0) + TIEBREAKER_SUB_COMMITTEE_10_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_10_MEMBERS", new address[](0) + ), + TIEBREAKER_SUB_COMMITTEES_QUORUMS: stdJson.readUintArray(jsonConfig, ".TIEBREAKER_SUB_COMMITTEES_QUORUMS"), + RESEAL_COMMITTEE_MEMBERS: stdJson.readAddressArray(jsonConfig, ".RESEAL_COMMITTEE_MEMBERS"), + RESEAL_COMMITTEE_QUORUM: stdJson.readUintOr( + jsonConfig, ".RESEAL_COMMITTEE_QUORUM", DEFAULT_RESEAL_COMMITTEE_QUORUM + ), + MIN_WITHDRAWALS_BATCH_SIZE: stdJson.readUintOr( + jsonConfig, ".MIN_WITHDRAWALS_BATCH_SIZE", DEFAULT_MIN_WITHDRAWALS_BATCH_SIZE ), - TIEBREAKER_SUB_COMMITTEES_QUORUMS: vm.envUint("TIEBREAKER_SUB_COMMITTEES_QUORUMS", ARRAY_SEPARATOR), - RESEAL_COMMITTEE_MEMBERS: vm.envAddress("RESEAL_COMMITTEE_MEMBERS", ARRAY_SEPARATOR), - RESEAL_COMMITTEE_QUORUM: vm.envOr("RESEAL_COMMITTEE_QUORUM", DEFAULT_RESEAL_COMMITTEE_QUORUM), - MIN_WITHDRAWALS_BATCH_SIZE: vm.envOr("MIN_WITHDRAWALS_BATCH_SIZE", DEFAULT_MIN_WITHDRAWALS_BATCH_SIZE), MIN_TIEBREAKER_ACTIVATION_TIMEOUT: Durations.from( - vm.envOr("MIN_TIEBREAKER_ACTIVATION_TIMEOUT", DEFAULT_MIN_TIEBREAKER_ACTIVATION_TIMEOUT) + stdJson.readUintOr( + jsonConfig, ".MIN_TIEBREAKER_ACTIVATION_TIMEOUT", DEFAULT_MIN_TIEBREAKER_ACTIVATION_TIMEOUT + ) ), TIEBREAKER_ACTIVATION_TIMEOUT: Durations.from( - vm.envOr("TIEBREAKER_ACTIVATION_TIMEOUT", DEFAULT_TIEBREAKER_ACTIVATION_TIMEOUT) + stdJson.readUintOr(jsonConfig, ".TIEBREAKER_ACTIVATION_TIMEOUT", DEFAULT_TIEBREAKER_ACTIVATION_TIMEOUT) ), MAX_TIEBREAKER_ACTIVATION_TIMEOUT: Durations.from( - vm.envOr("MAX_TIEBREAKER_ACTIVATION_TIMEOUT", DEFAULT_MAX_TIEBREAKER_ACTIVATION_TIMEOUT) + stdJson.readUintOr( + jsonConfig, ".MAX_TIEBREAKER_ACTIVATION_TIMEOUT", DEFAULT_MAX_TIEBREAKER_ACTIVATION_TIMEOUT + ) ), - MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT: vm.envOr( - "MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT", DEFAULT_MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT + MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT: stdJson.readUintOr( + jsonConfig, ".MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT", DEFAULT_MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT ), FIRST_SEAL_RAGE_QUIT_SUPPORT: PercentsD16.fromBasisPoints( - vm.envOr("FIRST_SEAL_RAGE_QUIT_SUPPORT", DEFAULT_FIRST_SEAL_RAGE_QUIT_SUPPORT) + stdJson.readUintOr(jsonConfig, ".FIRST_SEAL_RAGE_QUIT_SUPPORT", DEFAULT_FIRST_SEAL_RAGE_QUIT_SUPPORT) ), SECOND_SEAL_RAGE_QUIT_SUPPORT: PercentsD16.fromBasisPoints( - vm.envOr("SECOND_SEAL_RAGE_QUIT_SUPPORT", DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT) + stdJson.readUintOr(jsonConfig, ".SECOND_SEAL_RAGE_QUIT_SUPPORT", DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT) + ), + MIN_ASSETS_LOCK_DURATION: Durations.from( + stdJson.readUintOr(jsonConfig, ".MIN_ASSETS_LOCK_DURATION", DEFAULT_MIN_ASSETS_LOCK_DURATION) ), - MIN_ASSETS_LOCK_DURATION: Durations.from(vm.envOr("MIN_ASSETS_LOCK_DURATION", DEFAULT_MIN_ASSETS_LOCK_DURATION)), VETO_SIGNALLING_MIN_DURATION: Durations.from( - vm.envOr("VETO_SIGNALLING_MIN_DURATION", DEFAULT_VETO_SIGNALLING_MIN_DURATION) + stdJson.readUintOr(jsonConfig, ".VETO_SIGNALLING_MIN_DURATION", DEFAULT_VETO_SIGNALLING_MIN_DURATION) ), VETO_SIGNALLING_MAX_DURATION: Durations.from( - vm.envOr("VETO_SIGNALLING_MAX_DURATION", DEFAULT_VETO_SIGNALLING_MAX_DURATION) + stdJson.readUintOr(jsonConfig, ".VETO_SIGNALLING_MAX_DURATION", DEFAULT_VETO_SIGNALLING_MAX_DURATION) ), VETO_SIGNALLING_MIN_ACTIVE_DURATION: Durations.from( - vm.envOr("VETO_SIGNALLING_MIN_ACTIVE_DURATION", DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION) + stdJson.readUintOr( + jsonConfig, ".VETO_SIGNALLING_MIN_ACTIVE_DURATION", DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION + ) ), VETO_SIGNALLING_DEACTIVATION_MAX_DURATION: Durations.from( - vm.envOr("VETO_SIGNALLING_DEACTIVATION_MAX_DURATION", DEFAULT_VETO_SIGNALLING_DEACTIVATION_MAX_DURATION) + stdJson.readUintOr( + jsonConfig, + ".VETO_SIGNALLING_DEACTIVATION_MAX_DURATION", + DEFAULT_VETO_SIGNALLING_DEACTIVATION_MAX_DURATION + ) + ), + VETO_COOLDOWN_DURATION: Durations.from( + stdJson.readUintOr(jsonConfig, ".VETO_COOLDOWN_DURATION", DEFAULT_VETO_COOLDOWN_DURATION) ), - VETO_COOLDOWN_DURATION: Durations.from(vm.envOr("VETO_COOLDOWN_DURATION", DEFAULT_VETO_COOLDOWN_DURATION)), RAGE_QUIT_EXTENSION_PERIOD_DURATION: Durations.from( - vm.envOr("RAGE_QUIT_EXTENSION_PERIOD_DURATION", DEFAULT_RAGE_QUIT_EXTENSION_PERIOD_DURATION) + stdJson.readUintOr( + jsonConfig, ".RAGE_QUIT_EXTENSION_PERIOD_DURATION", DEFAULT_RAGE_QUIT_EXTENSION_PERIOD_DURATION + ) ), RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY: Durations.from( - vm.envOr("RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY", DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY) + stdJson.readUintOr( + jsonConfig, ".RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY", DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY + ) ), RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY: Durations.from( - vm.envOr("RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY", DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY) + stdJson.readUintOr( + jsonConfig, ".RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY", DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY + ) ), RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH: Durations.from( - vm.envOr("RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH", DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH) + stdJson.readUintOr( + jsonConfig, ".RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH", DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH + ) ) }); @@ -201,12 +253,15 @@ contract DGDeployConfigProvider is Script { } if (keccak256(bytes(chainName)) == CHAIN_NAME_HOLESKY_MOCKS_HASH) { + // TODO: is it ok to use the same file? + string memory jsonConfig = loadConfigFile(); + return LidoContracts({ chainId: 17000, - stETH: IStETH(vm.envAddress("HOLESKY_MOCK_ST_ETH")), - wstETH: IWstETH(vm.envAddress("HOLESKY_MOCK_WST_ETH")), - withdrawalQueue: IWithdrawalQueue(vm.envAddress("HOLESKY_MOCK_WITHDRAWAL_QUEUE")), - voting: IAragonVoting(vm.envAddress("HOLESKY_MOCK_DAO_VOTING")) + stETH: IStETH(stdJson.readAddress(jsonConfig, ".HOLESKY_MOCK_ST_ETH")), + wstETH: IWstETH(stdJson.readAddress(jsonConfig, ".HOLESKY_MOCK_WST_ETH")), + withdrawalQueue: IWithdrawalQueue(stdJson.readAddress(jsonConfig, ".HOLESKY_MOCK_WITHDRAWAL_QUEUE")), + voting: IAragonVoting(stdJson.readAddress(jsonConfig, ".HOLESKY_MOCK_DAO_VOTING")) }); } @@ -345,6 +400,10 @@ contract DGDeployConfigProvider is Script { revert InvalidParameter("TIEBREAKER_ACTIVATION_TIMEOUT"); } + if (config.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT == 0) { + revert InvalidParameter("MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT"); + } + if (config.VETO_SIGNALLING_MIN_DURATION > config.VETO_SIGNALLING_MAX_DURATION) { revert InvalidParameter("VETO_SIGNALLING_MIN_DURATION"); } @@ -356,7 +415,7 @@ contract DGDeployConfigProvider is Script { } } - function printCommittees(DeployConfig memory config) internal view { + function printCommittees(DeployConfig memory config) internal pure { console.log("================================================="); console.log("Loaded valid config with the following committees:"); @@ -457,10 +516,16 @@ contract DGDeployConfigProvider is Script { console.log("================================================="); } - function printCommittee(address[] memory committee, uint256 quorum, string memory message) internal view { + function printCommittee(address[] memory committee, uint256 quorum, string memory message) internal pure { console.log(message, quorum, "of", committee.length); for (uint256 k = 0; k < committee.length; ++k) { console.log(">> #", k, address(committee[k])); } } + + function loadConfigFile() internal view returns (string memory jsonConfig) { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/", configFilePath); + jsonConfig = vm.readFile(path); + } } diff --git a/scripts/deploy/Verify.s.sol b/scripts/deploy/Verify.s.sol index 6cb49c6a..a6037826 100644 --- a/scripts/deploy/Verify.s.sol +++ b/scripts/deploy/Verify.s.sol @@ -4,10 +4,11 @@ pragma solidity 0.8.26; /* solhint-disable no-console */ import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; import {console} from "forge-std/console.sol"; import {DeployConfig, LidoContracts} from "./Config.sol"; -import {DGDeployConfigProvider} from "./EnvConfig.s.sol"; +import {DGDeployJSONConfigProvider} from "./JsonConfig.s.sol"; import {DeployVerification} from "./DeployVerification.sol"; contract Verify is Script { @@ -20,11 +21,14 @@ contract Verify is Script { function run() external { string memory chainName = vm.envString("CHAIN"); - DGDeployConfigProvider configProvider = new DGDeployConfigProvider(); + string memory configFilePath = vm.envString("DEPLOY_CONFIG_FILE_PATH"); + string memory deployedAddressesFilePath = vm.envString("DEPLOYED_ADDRESSES_FILE_PATH"); + + DGDeployJSONConfigProvider configProvider = new DGDeployJSONConfigProvider(configFilePath); config = configProvider.loadAndValidate(); lidoAddresses = configProvider.getLidoAddresses(chainName); - DeployVerification.DeployedAddresses memory res = loadDeployedAddresses(); + DeployVerification.DeployedAddresses memory res = loadDeployedAddresses(deployedAddressesFilePath); printAddresses(res); @@ -35,22 +39,28 @@ contract Verify is Script { console.log(unicode"Verified ✅"); } - function loadDeployedAddresses() internal view returns (DeployVerification.DeployedAddresses memory) { + function loadDeployedAddresses(string memory deployedAddressesFilePath) + internal + view + returns (DeployVerification.DeployedAddresses memory) + { + string memory deployedAddressesJson = loadDeployedAddressesFile(deployedAddressesFilePath); + return DeployVerification.DeployedAddresses({ - adminExecutor: payable(vm.envAddress("ADMIN_EXECUTOR")), - timelock: vm.envAddress("TIMELOCK"), - emergencyGovernance: vm.envAddress("EMERGENCY_GOVERNANCE"), - emergencyActivationCommittee: vm.envAddress("EMERGENCY_ACTIVATION_COMMITTEE"), - emergencyExecutionCommittee: vm.envAddress("EMERGENCY_EXECUTION_COMMITTEE"), - resealManager: vm.envAddress("RESEAL_MANAGER"), - dualGovernance: vm.envAddress("DUAL_GOVERNANCE"), - resealCommittee: vm.envAddress("RESEAL_COMMITTEE"), - tiebreakerCoreCommittee: vm.envAddress("TIEBREAKER_CORE_COMMITTEE"), - tiebreakerSubCommittees: vm.envAddress("TIEBREAKER_SUB_COMMITTEES", ",") + adminExecutor: payable(stdJson.readAddress(deployedAddressesJson, ".ADMIN_EXECUTOR")), + timelock: stdJson.readAddress(deployedAddressesJson, ".TIMELOCK"), + emergencyGovernance: stdJson.readAddress(deployedAddressesJson, ".EMERGENCY_GOVERNANCE"), + emergencyActivationCommittee: stdJson.readAddress(deployedAddressesJson, ".EMERGENCY_ACTIVATION_COMMITTEE"), + emergencyExecutionCommittee: stdJson.readAddress(deployedAddressesJson, ".EMERGENCY_EXECUTION_COMMITTEE"), + resealManager: stdJson.readAddress(deployedAddressesJson, ".RESEAL_MANAGER"), + dualGovernance: stdJson.readAddress(deployedAddressesJson, ".DUAL_GOVERNANCE"), + resealCommittee: stdJson.readAddress(deployedAddressesJson, ".RESEAL_COMMITTEE"), + tiebreakerCoreCommittee: stdJson.readAddress(deployedAddressesJson, ".TIEBREAKER_CORE_COMMITTEE"), + tiebreakerSubCommittees: stdJson.readAddressArray(deployedAddressesJson, ".TIEBREAKER_SUB_COMMITTEES") }); } - function printAddresses(DeployVerification.DeployedAddresses memory res) internal view { + function printAddresses(DeployVerification.DeployedAddresses memory res) internal pure { console.log("Using the following DG contracts addresses"); console.log("DualGovernance address", res.dualGovernance); console.log("ResealManager address", res.resealManager); @@ -67,4 +77,14 @@ contract Verify is Script { console.log("EmergencyExecutionCommittee address", res.emergencyExecutionCommittee); console.log("ResealCommittee address", res.resealCommittee); } + + function loadDeployedAddressesFile(string memory deployedAddressesFilePath) + internal + view + returns (string memory deployedAddressesJson) + { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/", deployedAddressesFilePath); + deployedAddressesJson = vm.readFile(path); + } } From 998b38cb8a7dc816fe31dbce2cb3ae00925ffd10 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:03:21 +0400 Subject: [PATCH 026/107] Use deploy script for scenario tests configuration --- test/utils/SetupDeployment.sol | 312 +++++++++++---------------------- 1 file changed, 100 insertions(+), 212 deletions(-) diff --git a/test/utils/SetupDeployment.sol b/test/utils/SetupDeployment.sol index 28e39256..b4aaed42 100644 --- a/test/utils/SetupDeployment.sol +++ b/test/utils/SetupDeployment.sol @@ -8,16 +8,13 @@ import {Test} from "forge-std/Test.sol"; // --- import {PercentsD16} from "contracts/types/PercentD16.sol"; -import {Timestamps} from "contracts/types/Timestamp.sol"; import {Durations, Duration} from "contracts/types/Duration.sol"; // --- // Interfaces // --- import {ITimelock} from "contracts/interfaces/ITimelock.sol"; -import {IGovernance} from "contracts/interfaces/IGovernance.sol"; import {IResealManager} from "contracts/interfaces/IResealManager.sol"; -import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; // --- // Contracts @@ -32,11 +29,9 @@ import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActiva import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; -import {Escrow} from "contracts/Escrow.sol"; import {ResealManager} from "contracts/ResealManager.sol"; import {DualGovernance} from "contracts/DualGovernance.sol"; import { - DualGovernanceConfig, IDualGovernanceConfigProvider, ImmutableDualGovernanceConfigProvider } from "contracts/ImmutableDualGovernanceConfigProvider.sol"; @@ -50,6 +45,8 @@ import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommitte import {Random} from "./random.sol"; import {LidoUtils} from "./lido-utils.sol"; +import {DeployConfig, LidoContracts} from "../../scripts/deploy/Config.sol"; +import {DeployedContracts, DGContractsDeployment} from "../../scripts/deploy/ContractsDeployment.sol"; // --- // Lido Addresses @@ -61,6 +58,10 @@ abstract contract SetupDeployment is Test { // Helpers // --- + DeployConfig internal dgDeployConfig; + LidoContracts internal lidoAddresses; + DeployedContracts internal contracts; + Random.Context internal _random; LidoUtils.Context internal _lido; @@ -142,6 +143,61 @@ abstract contract SetupDeployment is Test { _lido = lido; _random = random; _targetMock = new TargetMock(); + + dgDeployConfig.AFTER_SUBMIT_DELAY = _AFTER_SUBMIT_DELAY; + dgDeployConfig.MAX_AFTER_SUBMIT_DELAY = _MAX_AFTER_SUBMIT_DELAY; + dgDeployConfig.AFTER_SCHEDULE_DELAY = _AFTER_SCHEDULE_DELAY; + dgDeployConfig.MAX_AFTER_SCHEDULE_DELAY = _MAX_AFTER_SCHEDULE_DELAY; + dgDeployConfig.EMERGENCY_MODE_DURATION = _EMERGENCY_MODE_DURATION; + dgDeployConfig.MAX_EMERGENCY_MODE_DURATION = _MAX_EMERGENCY_MODE_DURATION; + dgDeployConfig.EMERGENCY_PROTECTION_DURATION = _EMERGENCY_PROTECTION_DURATION; + dgDeployConfig.MAX_EMERGENCY_PROTECTION_DURATION = _MAX_EMERGENCY_PROTECTION_DURATION; + dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM = _EMERGENCY_ACTIVATION_COMMITTEE_QUORUM; + dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS = + _generateRandomAddresses(_EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS_COUNT); + dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_QUORUM = _EMERGENCY_EXECUTION_COMMITTEE_QUORUM; + dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS = + _generateRandomAddresses(_EMERGENCY_EXECUTION_COMMITTEE_MEMBERS_COUNT); + + dgDeployConfig.TIEBREAKER_CORE_QUORUM = TIEBREAKER_SUB_COMMITTEES_COUNT; + dgDeployConfig.TIEBREAKER_EXECUTION_DELAY = TIEBREAKER_EXECUTION_DELAY; + dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT = TIEBREAKER_SUB_COMMITTEES_COUNT; + dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS = + _generateRandomAddresses(TIEBREAKER_SUB_COMMITTEE_MEMBERS_COUNT); + dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS = + _generateRandomAddresses(TIEBREAKER_SUB_COMMITTEE_MEMBERS_COUNT); + dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS = + [TIEBREAKER_SUB_COMMITTEE_QUORUM, TIEBREAKER_SUB_COMMITTEE_QUORUM]; + + uint256 resealCommitteeMembersCount = 5; + dgDeployConfig.RESEAL_COMMITTEE_MEMBERS = new address[](resealCommitteeMembersCount); + for (uint256 i = 0; i < resealCommitteeMembersCount; ++i) { + dgDeployConfig.RESEAL_COMMITTEE_MEMBERS[i] = + makeAddr(string(abi.encode(0xFA + i * resealCommitteeMembersCount + 65))); + } + dgDeployConfig.RESEAL_COMMITTEE_QUORUM = 3; + dgDeployConfig.MIN_WITHDRAWALS_BATCH_SIZE = 4; + dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT = MIN_TIEBREAKER_ACTIVATION_TIMEOUT; + dgDeployConfig.TIEBREAKER_ACTIVATION_TIMEOUT = TIEBREAKER_ACTIVATION_TIMEOUT; + dgDeployConfig.MAX_TIEBREAKER_ACTIVATION_TIMEOUT = MAX_TIEBREAKER_ACTIVATION_TIMEOUT; + dgDeployConfig.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT; + dgDeployConfig.FIRST_SEAL_RAGE_QUIT_SUPPORT = PercentsD16.fromBasisPoints(3_00); // 3% + dgDeployConfig.SECOND_SEAL_RAGE_QUIT_SUPPORT = PercentsD16.fromBasisPoints(15_00); // 15% + dgDeployConfig.MIN_ASSETS_LOCK_DURATION = Durations.from(5 hours); + dgDeployConfig.VETO_SIGNALLING_MIN_DURATION = Durations.from(3 days); + dgDeployConfig.VETO_SIGNALLING_MAX_DURATION = Durations.from(30 days); + dgDeployConfig.VETO_SIGNALLING_MIN_ACTIVE_DURATION = Durations.from(5 hours); + dgDeployConfig.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = Durations.from(5 days); + dgDeployConfig.VETO_COOLDOWN_DURATION = Durations.from(4 days); + dgDeployConfig.RAGE_QUIT_EXTENSION_PERIOD_DURATION = Durations.from(7 days); + dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY = Durations.from(30 days); + dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY = Durations.from(180 days); + dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH = Durations.from(15 days); + + lidoAddresses.stETH = _lido.stETH; + lidoAddresses.wstETH = _lido.wstETH; + lidoAddresses.withdrawalQueue = _lido.withdrawalQueue; + lidoAddresses.voting = _lido.voting; } // --- @@ -150,8 +206,11 @@ abstract contract SetupDeployment is Test { function _deployTimelockedGovernanceSetup(bool isEmergencyProtectionEnabled) internal { _deployEmergencyProtectedTimelockContracts(isEmergencyProtectionEnabled); - _timelockedGovernance = _deployTimelockedGovernance({governance: address(_lido.voting), timelock: _timelock}); - _finalizeEmergencyProtectedTimelockDeploy(_timelockedGovernance); + _timelockedGovernance = + DGContractsDeployment.deployTimelockedGovernance({governance: address(_lido.voting), timelock: _timelock}); + DGContractsDeployment.finalizeEmergencyProtectedTimelockDeploy( + _adminExecutor, _timelock, address(_timelockedGovernance), dgDeployConfig + ); } function _deployDualGovernanceSetup(bool isEmergencyProtectionEnabled) internal { @@ -163,61 +222,34 @@ abstract contract SetupDeployment is Test { resealManager: _resealManager, configProvider: _dualGovernanceConfigProvider }); + contracts.dualGovernance = _dualGovernance; - _tiebreakerCoreCommittee = _deployEmptyTiebreakerCoreCommittee({ + _tiebreakerCoreCommittee = DGContractsDeployment.deployEmptyTiebreakerCoreCommittee({ owner: address(this), // temporary set owner to deployer, to add sub committees manually - dualGovernance: _dualGovernance, - timelock: TIEBREAKER_EXECUTION_DELAY + dualGovernance: address(_dualGovernance), + executionDelay: TIEBREAKER_EXECUTION_DELAY }); - address[] memory coreCommitteeMembers = new address[](TIEBREAKER_SUB_COMMITTEES_COUNT); - - for (uint256 i = 0; i < TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { - address[] memory members = _generateRandomAddresses(TIEBREAKER_SUB_COMMITTEE_MEMBERS_COUNT); - _tiebreakerSubCommittees.push( - _deployTiebreakerSubCommittee({ - owner: address(_adminExecutor), - quorum: TIEBREAKER_SUB_COMMITTEE_QUORUM, - members: members, - tiebreakerCore: _tiebreakerCoreCommittee - }) - ); - coreCommitteeMembers[i] = address(_tiebreakerSubCommittees[i]); - } + contracts.tiebreakerCoreCommittee = _tiebreakerCoreCommittee; - _tiebreakerCoreCommittee.addMembers(coreCommitteeMembers, coreCommitteeMembers.length); + _tiebreakerSubCommittees = DGContractsDeployment.deployTiebreakerSubCommittees( + address(_adminExecutor), _tiebreakerCoreCommittee, dgDeployConfig + ); _tiebreakerCoreCommittee.transferOwnership(address(_adminExecutor)); - _resealCommittee = _deployResealCommittee(); + _resealCommittee = DGContractsDeployment.deployResealCommittee( + address(_adminExecutor), address(_dualGovernance), dgDeployConfig + ); + contracts.resealCommittee = _resealCommittee; // --- // Finalize Setup // --- - _adminExecutor.execute( - address(_dualGovernance), - 0, - abi.encodeCall(_dualGovernance.registerProposer, (address(_lido.voting), address(_adminExecutor))) - ); - _adminExecutor.execute( - address(_dualGovernance), - 0, - abi.encodeCall(_dualGovernance.setTiebreakerActivationTimeout, TIEBREAKER_ACTIVATION_TIMEOUT) - ); - _adminExecutor.execute( - address(_dualGovernance), - 0, - abi.encodeCall(_dualGovernance.setTiebreakerCommittee, address(_tiebreakerCoreCommittee)) - ); - _adminExecutor.execute( - address(_dualGovernance), - 0, - abi.encodeCall(_dualGovernance.addTiebreakerSealableWithdrawalBlocker, address(_lido.withdrawalQueue)) - ); - _adminExecutor.execute( - address(_dualGovernance), 0, abi.encodeCall(_dualGovernance.setResealCommittee, address(_resealCommittee)) - ); - _finalizeEmergencyProtectedTimelockDeploy(_dualGovernance); + DGContractsDeployment.configureDualGovernance(dgDeployConfig, lidoAddresses, contracts); + DGContractsDeployment.finalizeEmergencyProtectedTimelockDeploy( + _adminExecutor, _timelock, address(_dualGovernance), dgDeployConfig + ); // --- // Grant Reseal Manager Roles @@ -237,116 +269,25 @@ abstract contract SetupDeployment is Test { // --- function _deployEmergencyProtectedTimelockContracts(bool isEmergencyProtectionEnabled) internal { - _adminExecutor = _deployExecutor(address(this)); - _timelock = _deployEmergencyProtectedTimelock(_adminExecutor); - if (isEmergencyProtectionEnabled) { - _emergencyActivationCommittee = _deployEmergencyActivationCommittee({ - quorum: _EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, - members: _generateRandomAddresses(_EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS_COUNT), - owner: address(_adminExecutor), - timelock: _timelock - }); - - _emergencyExecutionCommittee = _deployEmergencyExecutionCommittee({ - quorum: _EMERGENCY_EXECUTION_COMMITTEE_QUORUM, - members: _generateRandomAddresses(_EMERGENCY_EXECUTION_COMMITTEE_MEMBERS_COUNT), - owner: address(_adminExecutor), - timelock: _timelock - }); - _emergencyGovernance = _deployTimelockedGovernance({governance: address(_lido.voting), timelock: _timelock}); - - _adminExecutor.execute( - address(_timelock), - 0, - abi.encodeCall( - _timelock.setEmergencyProtectionActivationCommittee, (address(_emergencyActivationCommittee)) - ) - ); - _adminExecutor.execute( - address(_timelock), - 0, - abi.encodeCall( - _timelock.setEmergencyProtectionExecutionCommittee, (address(_emergencyExecutionCommittee)) - ) - ); - _adminExecutor.execute( - address(_timelock), - 0, - abi.encodeCall( - _timelock.setEmergencyProtectionEndDate, (_EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now())) - ) - ); - _adminExecutor.execute( - address(_timelock), 0, abi.encodeCall(_timelock.setEmergencyModeDuration, (_EMERGENCY_MODE_DURATION)) - ); - - _adminExecutor.execute( - address(_timelock), 0, abi.encodeCall(_timelock.setEmergencyGovernance, (address(_emergencyGovernance))) + contracts = DGContractsDeployment.deployEmergencyProtectedTimelockContracts( + lidoAddresses, dgDeployConfig, contracts, address(this) ); + _adminExecutor = contracts.adminExecutor; + _timelock = contracts.timelock; + _emergencyActivationCommittee = contracts.emergencyActivationCommittee; + _emergencyExecutionCommittee = contracts.emergencyExecutionCommittee; + _emergencyGovernance = contracts.emergencyGovernance; + } else { + _adminExecutor = DGContractsDeployment.deployExecutor(address(this)); + _timelock = _deployEmergencyProtectedTimelock(_adminExecutor); + contracts.adminExecutor = _adminExecutor; + contracts.timelock = _timelock; } } - function _finalizeEmergencyProtectedTimelockDeploy(IGovernance governance) internal { - _adminExecutor.execute( - address(_timelock), 0, abi.encodeCall(_timelock.setupDelays, (_AFTER_SUBMIT_DELAY, _AFTER_SCHEDULE_DELAY)) - ); - _adminExecutor.execute(address(_timelock), 0, abi.encodeCall(_timelock.setGovernance, (address(governance)))); - _adminExecutor.transferOwnership(address(_timelock)); - } - - function _deployExecutor(address owner) internal returns (Executor) { - return new Executor(owner); - } - function _deployEmergencyProtectedTimelock(Executor adminExecutor) internal returns (EmergencyProtectedTimelock) { - return new EmergencyProtectedTimelock({ - adminExecutor: address(adminExecutor), - sanityCheckParams: EmergencyProtectedTimelock.SanityCheckParams({ - maxAfterSubmitDelay: _MAX_AFTER_SUBMIT_DELAY, - maxAfterScheduleDelay: _MAX_AFTER_SCHEDULE_DELAY, - maxEmergencyModeDuration: _MAX_EMERGENCY_MODE_DURATION, - maxEmergencyProtectionDuration: _MAX_EMERGENCY_PROTECTION_DURATION - }) - }); - } - - function _deployEmergencyActivationCommittee( - EmergencyProtectedTimelock timelock, - address owner, - uint256 quorum, - address[] memory members - ) internal returns (EmergencyActivationCommittee) { - return new EmergencyActivationCommittee(owner, members, quorum, address(timelock)); - } - - function _deployEmergencyExecutionCommittee( - EmergencyProtectedTimelock timelock, - address owner, - uint256 quorum, - address[] memory members - ) internal returns (EmergencyExecutionCommittee) { - return new EmergencyExecutionCommittee(owner, members, quorum, address(timelock)); - } - - function _deployResealCommittee() internal returns (ResealCommittee) { - uint256 quorum = 3; - uint256 membersCount = 5; - address[] memory committeeMembers = new address[](membersCount); - for (uint256 i = 0; i < membersCount; ++i) { - committeeMembers[i] = makeAddr(string(abi.encode(0xFA + i * membersCount + 65))); - } - - return new ResealCommittee( - address(_adminExecutor), committeeMembers, quorum, address(_dualGovernance), Durations.from(0) - ); - } - - function _deployTimelockedGovernance( - address governance, - ITimelock timelock - ) internal returns (TimelockedGovernance) { - return new TimelockedGovernance(governance, timelock); + return DGContractsDeployment.deployEmergencyProtectedTimelock(address(adminExecutor), dgDeployConfig); } // --- @@ -354,29 +295,11 @@ abstract contract SetupDeployment is Test { // --- function _deployDualGovernanceConfigProvider() internal returns (ImmutableDualGovernanceConfigProvider) { - return 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) - }) - ); + return DGContractsDeployment.deployDualGovernanceConfigProvider(dgDeployConfig); } function _deployResealManager(ITimelock timelock) internal returns (ResealManager) { - return new ResealManager(timelock); + return DGContractsDeployment.deployResealManager(timelock); } function _deployDualGovernance( @@ -384,44 +307,9 @@ abstract contract SetupDeployment is Test { IResealManager resealManager, IDualGovernanceConfigProvider configProvider ) internal returns (DualGovernance) { - return new DualGovernance({ - dependencies: DualGovernance.ExternalDependencies({ - stETH: _lido.stETH, - wstETH: _lido.wstETH, - withdrawalQueue: _lido.withdrawalQueue, - timelock: timelock, - resealManager: resealManager, - configProvider: configProvider - }), - sanityCheckParams: DualGovernance.SanityCheckParams({ - minWithdrawalsBatchSize: 4, - minTiebreakerActivationTimeout: MIN_TIEBREAKER_ACTIVATION_TIMEOUT, - maxTiebreakerActivationTimeout: MAX_TIEBREAKER_ACTIVATION_TIMEOUT, - maxSealableWithdrawalBlockersCount: MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT - }) - }); - } - - function _deployEmptyTiebreakerCoreCommittee( - address owner, - IDualGovernance dualGovernance, - Duration timelock - ) internal returns (TiebreakerCoreCommittee) { - return new TiebreakerCoreCommittee({owner: owner, dualGovernance: address(dualGovernance), timelock: timelock}); - } - - function _deployTiebreakerSubCommittee( - address owner, - uint256 quorum, - address[] memory members, - TiebreakerCoreCommittee tiebreakerCore - ) internal returns (TiebreakerSubCommittee) { - return new TiebreakerSubCommittee({ - owner: owner, - executionQuorum: quorum, - committeeMembers: members, - tiebreakerCoreCommittee: address(tiebreakerCore) - }); + return DGContractsDeployment.deployDualGovernance( + configProvider, timelock, resealManager, dgDeployConfig, lidoAddresses + ); } // --- From e93de5645a0dfd5e0f9e44e9273fe7b428c73c9d Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:03:48 +0400 Subject: [PATCH 027/107] Update Readme file --- scripts/deploy/Readme.md | 74 +++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/scripts/deploy/Readme.md b/scripts/deploy/Readme.md index 9fc348cb..5339d3c3 100644 --- a/scripts/deploy/Readme.md +++ b/scripts/deploy/Readme.md @@ -9,30 +9,40 @@ anvil --fork-url https://.infura.io/v3/ --bloc ### Running the deploy script -1. Set up required env variables in .env file +1. Set up the required env variables in the .env file ``` CHAIN=<"mainnet" OR "holesky" OR "holesky-mocks"> DEPLOYER_PRIVATE_KEY=... - EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS=addr1,addr2,addr3 - EMERGENCY_EXECUTION_COMMITTEE_MEMBERS=addr1,addr2,addr3 - TIEBREAKER_SUB_COMMITTEE_1_MEMBERS=addr1,addr2,addr3 - TIEBREAKER_SUB_COMMITTEE_2_MEMBERS=addr1,addr2,addr3 - TIEBREAKER_SUB_COMMITTEES_QUORUMS=3,2 - TIEBREAKER_SUB_COMMITTEES_COUNT=2 - RESEAL_COMMITTEE_MEMBERS=addr1,addr2,addr3 + DEPLOY_CONFIG_FILE_PATH=... (for example: "deploy-config/deploy-config.json") + ``` +2. Create a deploy config JSON file with all the required values (at the location specified in DEPLOY_CONFIG_FILE_PATH): + ``` + { + "EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS": [addr1,addr2,addr3], + "EMERGENCY_EXECUTION_COMMITTEE_MEMBERS": [addr1,addr2,addr3], + "TIEBREAKER_SUB_COMMITTEE_1_MEMBERS": [addr1,addr2,addr3], + "TIEBREAKER_SUB_COMMITTEE_2_MEMBERS": [addr1,addr2,addr3], + "TIEBREAKER_SUB_COMMITTEES_QUORUMS": [3,2], + "TIEBREAKER_SUB_COMMITTEES_COUNT": 2, + "RESEAL_COMMITTEE_MEMBERS": [addr1,addr2,addr3] + } ``` - When using `CHAIN="holesky-mocks"` you will need to provide in addition already deployed mock contracts addresses: + When using `CHAIN="holesky-mocks"` you will need to provide in addition already deployed mock contracts addresses in the same JSON config file (at DEPLOY_CONFIG_FILE_PATH): ``` - HOLESKY_MOCK_ST_ETH=... - HOLESKY_MOCK_WST_ETH=... - HOLESKY_MOCK_WITHDRAWAL_QUEUE=... - HOLESKY_MOCK_DAO_VOTING=... + { + ... + "HOLESKY_MOCK_ST_ETH": ..., + "HOLESKY_MOCK_WST_ETH": ..., + "HOLESKY_MOCK_WITHDRAWAL_QUEUE": ..., + "HOLESKY_MOCK_DAO_VOTING": ..., + ... + } ``` -2. Run the script (with the local Anvil as an example) +3. Run the script (with the local Anvil as an example) ``` forge script scripts/deploy/DeployConfigurable.s.sol:DeployConfigurable --fork-url http://localhost:8545 --broadcast @@ -40,23 +50,31 @@ anvil --fork-url https://.infura.io/v3/ --bloc ### Running the verification script -1. Set up required env variables in .env file +1. Set up the required env variables in the .env file ``` CHAIN=<"mainnet" OR "holesky" OR "holesky-mocks"> - ADMIN_EXECUTOR=... - TIMELOCK=... - EMERGENCY_GOVERNANCE=... - EMERGENCY_ACTIVATION_COMMITTEE=... - EMERGENCY_EXECUTION_COMMITTEE=... - RESEAL_MANAGER=... - DUAL_GOVERNANCE=... - RESEAL_COMMITTEE=... - TIEBREAKER_CORE_COMMITTEE=... - TIEBREAKER_SUB_COMMITTEES=... - ``` - -2. Run the script (with the local Anvil as an example) + DEPLOYED_ADDRESSES_FILE_PATH=... (for example: "deploy-config/deployed-addrs.json") + ``` + +2. Create a deployed addresses list JSON file with all the required values (at the location specified in DEPLOYED_ADDRESSES_FILE_PATH): + + ``` + { + "ADMIN_EXECUTOR": "...", + "TIMELOCK": "...", + "EMERGENCY_GOVERNANCE": "...", + "EMERGENCY_ACTIVATION_COMMITTEE": "...", + "EMERGENCY_EXECUTION_COMMITTEE": "...", + "RESEAL_MANAGER": "...", + "DUAL_GOVERNANCE": "...", + "RESEAL_COMMITTEE": "...", + "TIEBREAKER_CORE_COMMITTEE": "...", + "TIEBREAKER_SUB_COMMITTEES": ["...", "..."] + } + ``` + +3. Run the script (with the local Anvil as an example) ``` forge script scripts/deploy/Verify.s.sol:Verify --fork-url http://localhost:8545 --broadcast From c100709db4a641243596d2efffa0e18877f05cb2 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 4 Oct 2024 19:42:05 +0400 Subject: [PATCH 028/107] Fix deploy verification --- scripts/deploy/DeployVerification.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index ef7ae700..0ea4609d 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -223,7 +223,7 @@ library DeployVerification { "Incorrect parameter MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT" ); - Escrow escrowTemplate = Escrow(payable(dg.ESCROW_MASTER_COPY())); + Escrow escrowTemplate = Escrow(payable(address(dg.ESCROW_MASTER_COPY()))); require(escrowTemplate.DUAL_GOVERNANCE() == dg, "Escrow has incorrect DualGovernance address"); require(escrowTemplate.ST_ETH() == lidoAddresses.stETH, "Escrow has incorrect StETH address"); require(escrowTemplate.WST_ETH() == lidoAddresses.wstETH, "Escrow has incorrect WstETH address"); @@ -288,13 +288,17 @@ library DeployVerification { "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH" ); - require(dg.getState() == State.Normal, "Incorrect DualGovernance state"); + require(dg.getPersistedState() == State.Normal, "Incorrect DualGovernance persisted state"); + require(dg.getEffectiveState() == State.Normal, "Incorrect DualGovernance effective state"); require(dg.getProposers().length == 1, "Incorrect amount of proposers"); require(dg.isProposer(address(lidoAddresses.voting)) == true, "Lido voting is not set as a proposers[0]"); IDualGovernance.StateDetails memory stateDetails = dg.getStateDetails(); - require(stateDetails.state == State.Normal, "Incorrect DualGovernance state"); - require(stateDetails.enteredAt <= Timestamps.now(), "Incorrect DualGovernance state enteredAt"); + require(stateDetails.effectiveState == State.Normal, "Incorrect DualGovernance effectiveState"); + require(stateDetails.persistedState == State.Normal, "Incorrect DualGovernance persistedState"); + require( + stateDetails.persistedStateEnteredAt <= Timestamps.now(), "Incorrect DualGovernance persistedStateEnteredAt" + ); require( stateDetails.vetoSignallingActivatedAt == Timestamps.ZERO, "Incorrect DualGovernance state vetoSignallingActivatedAt" From 2f5cd8d55150f54c744beb2ca4a0646a1ddc7430 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 4 Oct 2024 20:32:56 +0400 Subject: [PATCH 029/107] Use the credentials stored in cast wallet --- scripts/deploy/DeployBase.s.sol | 5 ++--- scripts/deploy/Readme.md | 16 +++++++++++----- scripts/deploy/Verify.s.sol | 2 -- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/scripts/deploy/DeployBase.s.sol b/scripts/deploy/DeployBase.s.sol index e9f1f169..59f72e70 100644 --- a/scripts/deploy/DeployBase.s.sol +++ b/scripts/deploy/DeployBase.s.sol @@ -25,11 +25,10 @@ abstract contract DeployBase is Script { revert ChainIdMismatch({actual: block.chainid, expected: lidoAddresses.chainId}); } - pk = vm.envUint("DEPLOYER_PRIVATE_KEY"); - deployer = vm.addr(pk); + deployer = msg.sender; vm.label(deployer, "DEPLOYER"); - vm.startBroadcast(pk); + vm.startBroadcast(); DeployedContracts memory contracts = DGContractsDeployment.deployDualGovernanceSetup(config, lidoAddresses, deployer); diff --git a/scripts/deploy/Readme.md b/scripts/deploy/Readme.md index 5339d3c3..4ac36477 100644 --- a/scripts/deploy/Readme.md +++ b/scripts/deploy/Readme.md @@ -9,14 +9,20 @@ anvil --fork-url https://.infura.io/v3/ --bloc ### Running the deploy script -1. Set up the required env variables in the .env file +1. Import your private key to Cast wallet ([see the docs](https://book.getfoundry.sh/reference/cast/cast-wallet-import)), for example (we will use the account name `Deployer1` here and further for the simplicity): + + ``` + cast wallet import Deployer1 --interactive + ``` + +2. Set up the required env variables in the .env file ``` CHAIN=<"mainnet" OR "holesky" OR "holesky-mocks"> - DEPLOYER_PRIVATE_KEY=... DEPLOY_CONFIG_FILE_PATH=... (for example: "deploy-config/deploy-config.json") ``` -2. Create a deploy config JSON file with all the required values (at the location specified in DEPLOY_CONFIG_FILE_PATH): + +3. Create a deploy config JSON file with all the required values (at the location specified in DEPLOY_CONFIG_FILE_PATH): ``` { "EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS": [addr1,addr2,addr3], @@ -42,10 +48,10 @@ anvil --fork-url https://.infura.io/v3/ --bloc } ``` -3. Run the script (with the local Anvil as an example) +4. Run the script (with the local Anvil as an example) ``` - forge script scripts/deploy/DeployConfigurable.s.sol:DeployConfigurable --fork-url http://localhost:8545 --broadcast + forge script scripts/deploy/DeployConfigurable.s.sol:DeployConfigurable --fork-url http://localhost:8545 --broadcast --account Deployer1 --sender ``` ### Running the verification script diff --git a/scripts/deploy/Verify.s.sol b/scripts/deploy/Verify.s.sol index a6037826..56deaf3a 100644 --- a/scripts/deploy/Verify.s.sol +++ b/scripts/deploy/Verify.s.sol @@ -16,8 +16,6 @@ contract Verify is Script { DeployConfig internal config; LidoContracts internal lidoAddresses; - address private deployer; - uint256 private pk; function run() external { string memory chainName = vm.envString("CHAIN"); From 5cef79683c01453fa6d3625fbf3726c5690a2484 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:33:25 +0400 Subject: [PATCH 030/107] Remove deployment of EmergencyActivationCommittee, EmergencyExecutionCommittee, ResealCommittee from deployment script --- scripts/deploy/Config.sol | 12 +--- scripts/deploy/ContractsDeployment.sol | 80 +++++------------------ scripts/deploy/DeployBase.s.sol | 6 -- scripts/deploy/DeployVerification.sol | 79 ++++------------------- scripts/deploy/JsonConfig.s.sol | 51 +-------------- scripts/deploy/Readme.md | 9 +-- scripts/deploy/Verify.s.sol | 6 -- test/utils/SetupDeployment.sol | 89 +++++++++++++++++--------- 8 files changed, 100 insertions(+), 232 deletions(-) diff --git a/scripts/deploy/Config.sol b/scripts/deploy/Config.sol index 9d5fbf23..5c925fc2 100644 --- a/scripts/deploy/Config.sol +++ b/scripts/deploy/Config.sol @@ -18,12 +18,9 @@ uint256 constant DEFAULT_EMERGENCY_MODE_DURATION = 180 days; uint256 constant DEFAULT_MAX_EMERGENCY_MODE_DURATION = 365 days; uint256 constant DEFAULT_EMERGENCY_PROTECTION_DURATION = 90 days; uint256 constant DEFAULT_MAX_EMERGENCY_PROTECTION_DURATION = 365 days; -uint256 constant DEFAULT_EMERGENCY_ACTIVATION_COMMITTEE_QUORUM = 3; -uint256 constant DEFAULT_EMERGENCY_EXECUTION_COMMITTEE_QUORUM = 5; uint256 constant DEFAULT_TIEBREAKER_CORE_QUORUM = 1; uint256 constant DEFAULT_TIEBREAKER_EXECUTION_DELAY = 30 days; uint256 constant DEFAULT_TIEBREAKER_SUB_COMMITTEES_COUNT = 2; -uint256 constant DEFAULT_RESEAL_COMMITTEE_QUORUM = 3; uint256 constant DEFAULT_MIN_WITHDRAWALS_BATCH_SIZE = 4; uint256 constant DEFAULT_MIN_TIEBREAKER_ACTIVATION_TIMEOUT = 90 days; uint256 constant DEFAULT_TIEBREAKER_ACTIVATION_TIMEOUT = 365 days; @@ -56,10 +53,8 @@ struct DeployConfig { Duration MAX_EMERGENCY_MODE_DURATION; Duration EMERGENCY_PROTECTION_DURATION; Duration MAX_EMERGENCY_PROTECTION_DURATION; - uint256 EMERGENCY_ACTIVATION_COMMITTEE_QUORUM; - address[] EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS; - uint256 EMERGENCY_EXECUTION_COMMITTEE_QUORUM; - address[] EMERGENCY_EXECUTION_COMMITTEE_MEMBERS; + address EMERGENCY_ACTIVATION_COMMITTEE; + address EMERGENCY_EXECUTION_COMMITTEE; uint256 TIEBREAKER_CORE_QUORUM; Duration TIEBREAKER_EXECUTION_DELAY; uint256 TIEBREAKER_SUB_COMMITTEES_COUNT; @@ -74,8 +69,7 @@ struct DeployConfig { address[] TIEBREAKER_SUB_COMMITTEE_9_MEMBERS; address[] TIEBREAKER_SUB_COMMITTEE_10_MEMBERS; uint256[] TIEBREAKER_SUB_COMMITTEES_QUORUMS; - address[] RESEAL_COMMITTEE_MEMBERS; - uint256 RESEAL_COMMITTEE_QUORUM; + address RESEAL_COMMITTEE; uint256 MIN_WITHDRAWALS_BATCH_SIZE; Duration MIN_TIEBREAKER_ACTIVATION_TIMEOUT; Duration TIEBREAKER_ACTIVATION_TIMEOUT; diff --git a/scripts/deploy/ContractsDeployment.sol b/scripts/deploy/ContractsDeployment.sol index 69d92055..8ff3144d 100644 --- a/scripts/deploy/ContractsDeployment.sol +++ b/scripts/deploy/ContractsDeployment.sol @@ -5,14 +5,11 @@ pragma solidity 0.8.26; // Contracts // --- import {Timestamps} from "contracts/types/Timestamp.sol"; -import {Duration, Durations} from "contracts/types/Duration.sol"; +import {Duration} from "contracts/types/Duration.sol"; import {Executor} from "contracts/Executor.sol"; import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; -import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; -import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; - import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; import {ResealManager} from "contracts/ResealManager.sol"; @@ -23,7 +20,6 @@ import { ImmutableDualGovernanceConfigProvider } from "contracts/ImmutableDualGovernanceConfigProvider.sol"; -import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; import {ITimelock} from "contracts/interfaces/ITimelock.sol"; @@ -35,11 +31,8 @@ struct DeployedContracts { Executor adminExecutor; EmergencyProtectedTimelock timelock; TimelockedGovernance emergencyGovernance; - EmergencyActivationCommittee emergencyActivationCommittee; - EmergencyExecutionCommittee emergencyExecutionCommittee; ResealManager resealManager; DualGovernance dualGovernance; - ResealCommittee resealCommittee; TiebreakerCoreCommittee tiebreakerCoreCommittee; TiebreakerSubCommittee[] tiebreakerSubCommittees; } @@ -50,7 +43,8 @@ library DGContractsDeployment { LidoContracts memory lidoAddresses, address deployer ) internal returns (DeployedContracts memory contracts) { - contracts = deployEmergencyProtectedTimelockContracts(lidoAddresses, dgDeployConfig, contracts, deployer); + contracts = deployAdminExecutorAndTimelock(dgDeployConfig, deployer); + deployEmergencyProtectedTimelockContracts(lidoAddresses, dgDeployConfig, contracts); contracts.resealManager = deployResealManager(contracts.timelock); ImmutableDualGovernanceConfigProvider dualGovernanceConfigProvider = deployDualGovernanceConfigProvider(dgDeployConfig); @@ -75,9 +69,6 @@ library DGContractsDeployment { contracts.tiebreakerCoreCommittee.transferOwnership(address(contracts.adminExecutor)); - contracts.resealCommittee = - deployResealCommittee(address(contracts.adminExecutor), address(dualGovernance), dgDeployConfig); - // --- // Finalize Setup // --- @@ -101,30 +92,25 @@ library DGContractsDeployment { vm.stopPrank(); */ } - function deployEmergencyProtectedTimelockContracts( - LidoContracts memory lidoAddresses, + function deployAdminExecutorAndTimelock( DeployConfig memory dgDeployConfig, - DeployedContracts memory contracts, address deployer - ) internal returns (DeployedContracts memory) { + ) internal returns (DeployedContracts memory contracts) { Executor adminExecutor = deployExecutor(deployer); EmergencyProtectedTimelock timelock = deployEmergencyProtectedTimelock(address(adminExecutor), dgDeployConfig); contracts.adminExecutor = adminExecutor; contracts.timelock = timelock; - contracts.emergencyActivationCommittee = deployEmergencyActivationCommittee({ - quorum: dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, - members: dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS, - owner: address(adminExecutor), - timelock: address(timelock) - }); + } + + function deployEmergencyProtectedTimelockContracts( + LidoContracts memory lidoAddresses, + DeployConfig memory dgDeployConfig, + DeployedContracts memory contracts + ) internal { + Executor adminExecutor = contracts.adminExecutor; + EmergencyProtectedTimelock timelock = contracts.timelock; - contracts.emergencyExecutionCommittee = deployEmergencyExecutionCommittee({ - quorum: dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_QUORUM, - members: dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS, - owner: address(adminExecutor), - timelock: address(timelock) - }); contracts.emergencyGovernance = deployTimelockedGovernance({governance: address(lidoAddresses.voting), timelock: timelock}); @@ -132,14 +118,14 @@ library DGContractsDeployment { address(timelock), 0, abi.encodeCall( - timelock.setEmergencyProtectionActivationCommittee, (address(contracts.emergencyActivationCommittee)) + timelock.setEmergencyProtectionActivationCommittee, (dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE) ) ); adminExecutor.execute( address(timelock), 0, abi.encodeCall( - timelock.setEmergencyProtectionExecutionCommittee, (address(contracts.emergencyExecutionCommittee)) + timelock.setEmergencyProtectionExecutionCommittee, (dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE) ) ); @@ -163,8 +149,6 @@ library DGContractsDeployment { 0, abi.encodeCall(timelock.setEmergencyGovernance, (address(contracts.emergencyGovernance))) ); - - return contracts; } function deployExecutor(address owner) internal returns (Executor) { @@ -186,24 +170,6 @@ library DGContractsDeployment { }); } - function deployEmergencyActivationCommittee( - address owner, - uint256 quorum, - address[] memory members, - address timelock - ) internal returns (EmergencyActivationCommittee) { - return new EmergencyActivationCommittee(owner, members, quorum, address(timelock)); - } - - function deployEmergencyExecutionCommittee( - address owner, - uint256 quorum, - address[] memory members, - address timelock - ) internal returns (EmergencyExecutionCommittee) { - return new EmergencyExecutionCommittee(owner, members, quorum, address(timelock)); - } - function deployTimelockedGovernance( address governance, ITimelock timelock @@ -312,18 +278,6 @@ library DGContractsDeployment { }); } - function deployResealCommittee( - address adminExecutor, - address dualGovernance, - DeployConfig memory dgDeployConfig - ) internal returns (ResealCommittee) { - uint256 quorum = dgDeployConfig.RESEAL_COMMITTEE_QUORUM; - address[] memory committeeMembers = dgDeployConfig.RESEAL_COMMITTEE_MEMBERS; - - // TODO: Don't we need to use non-zero timelock here? - return new ResealCommittee(adminExecutor, committeeMembers, quorum, dualGovernance, Durations.from(0)); - } - function configureDualGovernance( DeployConfig memory dgDeployConfig, LidoContracts memory lidoAddresses, @@ -359,7 +313,7 @@ library DGContractsDeployment { contracts.adminExecutor.execute( address(contracts.dualGovernance), 0, - abi.encodeCall(contracts.dualGovernance.setResealCommittee, address(contracts.resealCommittee)) + abi.encodeCall(contracts.dualGovernance.setResealCommittee, dgDeployConfig.RESEAL_COMMITTEE) ); } diff --git a/scripts/deploy/DeployBase.s.sol b/scripts/deploy/DeployBase.s.sol index 59f72e70..29aaa5ed 100644 --- a/scripts/deploy/DeployBase.s.sol +++ b/scripts/deploy/DeployBase.s.sol @@ -60,11 +60,8 @@ abstract contract DeployBase is Script { adminExecutor: payable(address(contracts.adminExecutor)), timelock: address(contracts.timelock), emergencyGovernance: address(contracts.emergencyGovernance), - emergencyActivationCommittee: address(contracts.emergencyActivationCommittee), - emergencyExecutionCommittee: address(contracts.emergencyExecutionCommittee), resealManager: address(contracts.resealManager), dualGovernance: address(contracts.dualGovernance), - resealCommittee: address(contracts.resealCommittee), tiebreakerCoreCommittee: address(contracts.tiebreakerCoreCommittee), tiebreakerSubCommittees: tiebreakerSubCommittees }); @@ -83,8 +80,5 @@ abstract contract DeployBase is Script { console.log("AdminExecutor address", res.adminExecutor); console.log("EmergencyProtectedTimelock address", res.timelock); console.log("EmergencyGovernance address", res.emergencyGovernance); - console.log("EmergencyActivationCommittee address", res.emergencyActivationCommittee); - console.log("EmergencyExecutionCommittee address", res.emergencyExecutionCommittee); - console.log("ResealCommittee address", res.resealCommittee); } } diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index 0ea4609d..72faf6ac 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -7,12 +7,9 @@ import {PercentD16} from "contracts/types/PercentD16.sol"; import {Executor} from "contracts/Executor.sol"; import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; -import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; -import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; -import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; import {ResealManager} from "contracts/ResealManager.sol"; import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; @@ -29,11 +26,8 @@ library DeployVerification { address payable adminExecutor; address timelock; address emergencyGovernance; - address emergencyActivationCommittee; - address emergencyExecutionCommittee; address resealManager; address dualGovernance; - address resealCommittee; address tiebreakerCoreCommittee; address[] tiebreakerSubCommittees; } @@ -45,8 +39,8 @@ library DeployVerification { ) internal view { checkAdminExecutor(res.adminExecutor, res.timelock); checkTimelock(res, dgDeployConfig); - checkEmergencyActivationCommittee(res, dgDeployConfig); - checkEmergencyExecutionCommittee(res, dgDeployConfig); + checkEmergencyActivationCommittee(dgDeployConfig); + checkEmergencyExecutionCommittee(dgDeployConfig); checkTimelockedGovernance(res, lidoAddresses); checkResealManager(res); checkDualGovernance(res, dgDeployConfig, lidoAddresses); @@ -56,7 +50,7 @@ library DeployVerification { checkTiebreakerSubCommittee(res, dgDeployConfig, i); } - checkResealCommittee(res, dgDeployConfig); + checkResealCommittee(dgDeployConfig); } function checkAdminExecutor(address payable executor, address timelock) internal view { @@ -87,11 +81,11 @@ library DeployVerification { ); require( - timelockInstance.getEmergencyActivationCommittee() == res.emergencyActivationCommittee, + timelockInstance.getEmergencyActivationCommittee() == dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE, "Incorrect emergencyActivationCommittee address in EmergencyProtectedTimelock" ); require( - timelockInstance.getEmergencyExecutionCommittee() == res.emergencyExecutionCommittee, + timelockInstance.getEmergencyExecutionCommittee() == dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE, "Incorrect emergencyExecutionCommittee address in EmergencyProtectedTimelock" ); @@ -132,50 +126,14 @@ library DeployVerification { require(timelockInstance.getProposalsCount() == 0, "ProposalsCount > 0 in EmergencyProtectedTimelock"); } - function checkEmergencyActivationCommittee( - DeployedAddresses memory res, - DeployConfig memory dgDeployConfig - ) internal view { - EmergencyActivationCommittee committee = EmergencyActivationCommittee(res.emergencyActivationCommittee); - require(committee.owner() == res.adminExecutor, "EmergencyActivationCommittee owner != adminExecutor"); - require( - committee.EMERGENCY_PROTECTED_TIMELOCK() == res.timelock, - "EmergencyActivationCommittee EMERGENCY_PROTECTED_TIMELOCK != timelock" - ); - - for (uint256 i = 0; i < dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS.length; ++i) { - require( - committee.isMember(dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS[i]) == true, - "Incorrect member of EmergencyActivationCommittee" - ); - } - require( - committee.quorum() == dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, - "EmergencyActivationCommittee has incorrect quorum set" - ); + function checkEmergencyActivationCommittee(DeployConfig memory dgDeployConfig) internal pure { + // TODO: implement! + require(dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE != address(0), "Incorrect emergencyActivationCommittee"); } - function checkEmergencyExecutionCommittee( - DeployedAddresses memory res, - DeployConfig memory dgDeployConfig - ) internal view { - EmergencyExecutionCommittee committee = EmergencyExecutionCommittee(res.emergencyExecutionCommittee); - require(committee.owner() == res.adminExecutor, "EmergencyExecutionCommittee owner != adminExecutor"); - require( - committee.EMERGENCY_PROTECTED_TIMELOCK() == res.timelock, - "EmergencyExecutionCommittee EMERGENCY_PROTECTED_TIMELOCK != timelock" - ); - - for (uint256 i = 0; i < dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS.length; ++i) { - require( - committee.isMember(dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS[i]) == true, - "Incorrect member of EmergencyExecutionCommittee" - ); - } - require( - committee.quorum() == dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_QUORUM, - "EmergencyExecutionCommittee has incorrect quorum set" - ); + function checkEmergencyExecutionCommittee(DeployConfig memory dgDeployConfig) internal pure { + // TODO: implement! + require(dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE != address(0), "Incorrect emergencyExecutionCommittee"); } function checkTimelockedGovernance( @@ -364,17 +322,8 @@ library DeployVerification { require(tsc.quorum() == quorum, "Incorrect quorum in TiebreakerSubCommittee"); } - function checkResealCommittee(DeployedAddresses memory res, DeployConfig memory dgDeployConfig) internal view { - ResealCommittee rc = ResealCommittee(res.resealCommittee); - require(rc.owner() == res.adminExecutor, "ResealCommittee owner != adminExecutor"); - require(rc.timelockDuration() == Durations.from(0), "ResealCommittee timelock should be 0"); - require(rc.DUAL_GOVERNANCE() == res.dualGovernance, "Incorrect dualGovernance in ResealCommittee"); - - for (uint256 i = 0; i < dgDeployConfig.RESEAL_COMMITTEE_MEMBERS.length; ++i) { - require( - rc.isMember(dgDeployConfig.RESEAL_COMMITTEE_MEMBERS[i]) == true, "Incorrect member of ResealCommittee" - ); - } - require(rc.quorum() == dgDeployConfig.RESEAL_COMMITTEE_QUORUM, "Incorrect quorum in ResealCommittee"); + function checkResealCommittee(DeployConfig memory dgDeployConfig) internal pure { + // TODO: implement! + require(dgDeployConfig.RESEAL_COMMITTEE != address(0), "Incorrect resealCommittee"); } } diff --git a/scripts/deploy/JsonConfig.s.sol b/scripts/deploy/JsonConfig.s.sol index f128c772..1c9df3f2 100644 --- a/scripts/deploy/JsonConfig.s.sol +++ b/scripts/deploy/JsonConfig.s.sol @@ -35,12 +35,9 @@ import { DEFAULT_MAX_EMERGENCY_MODE_DURATION, DEFAULT_EMERGENCY_PROTECTION_DURATION, DEFAULT_MAX_EMERGENCY_PROTECTION_DURATION, - DEFAULT_EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, - DEFAULT_EMERGENCY_EXECUTION_COMMITTEE_QUORUM, DEFAULT_TIEBREAKER_CORE_QUORUM, DEFAULT_TIEBREAKER_EXECUTION_DELAY, DEFAULT_TIEBREAKER_SUB_COMMITTEES_COUNT, - DEFAULT_RESEAL_COMMITTEE_QUORUM, DEFAULT_MIN_WITHDRAWALS_BATCH_SIZE, DEFAULT_MIN_TIEBREAKER_ACTIVATION_TIMEOUT, DEFAULT_TIEBREAKER_ACTIVATION_TIMEOUT, @@ -104,18 +101,8 @@ contract DGDeployJSONConfigProvider is Script { jsonConfig, ".MAX_EMERGENCY_PROTECTION_DURATION", DEFAULT_MAX_EMERGENCY_PROTECTION_DURATION ) ), - EMERGENCY_ACTIVATION_COMMITTEE_QUORUM: stdJson.readUintOr( - jsonConfig, ".EMERGENCY_ACTIVATION_COMMITTEE_QUORUM", DEFAULT_EMERGENCY_ACTIVATION_COMMITTEE_QUORUM - ), - EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS: stdJson.readAddressArray( - jsonConfig, ".EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS" - ), - EMERGENCY_EXECUTION_COMMITTEE_QUORUM: stdJson.readUintOr( - jsonConfig, ".EMERGENCY_EXECUTION_COMMITTEE_QUORUM", DEFAULT_EMERGENCY_EXECUTION_COMMITTEE_QUORUM - ), - EMERGENCY_EXECUTION_COMMITTEE_MEMBERS: stdJson.readAddressArray( - jsonConfig, ".EMERGENCY_EXECUTION_COMMITTEE_MEMBERS" - ), + EMERGENCY_ACTIVATION_COMMITTEE: stdJson.readAddress(jsonConfig, ".EMERGENCY_ACTIVATION_COMMITTEE"), + EMERGENCY_EXECUTION_COMMITTEE: stdJson.readAddress(jsonConfig, ".EMERGENCY_EXECUTION_COMMITTEE"), TIEBREAKER_CORE_QUORUM: stdJson.readUintOr( jsonConfig, ".TIEBREAKER_CORE_QUORUM", DEFAULT_TIEBREAKER_CORE_QUORUM ), @@ -154,10 +141,7 @@ contract DGDeployJSONConfigProvider is Script { jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_10_MEMBERS", new address[](0) ), TIEBREAKER_SUB_COMMITTEES_QUORUMS: stdJson.readUintArray(jsonConfig, ".TIEBREAKER_SUB_COMMITTEES_QUORUMS"), - RESEAL_COMMITTEE_MEMBERS: stdJson.readAddressArray(jsonConfig, ".RESEAL_COMMITTEE_MEMBERS"), - RESEAL_COMMITTEE_QUORUM: stdJson.readUintOr( - jsonConfig, ".RESEAL_COMMITTEE_QUORUM", DEFAULT_RESEAL_COMMITTEE_QUORUM - ), + RESEAL_COMMITTEE: stdJson.readAddress(jsonConfig, ".RESEAL_COMMITTEE"), MIN_WITHDRAWALS_BATCH_SIZE: stdJson.readUintOr( jsonConfig, ".MIN_WITHDRAWALS_BATCH_SIZE", DEFAULT_MIN_WITHDRAWALS_BATCH_SIZE ), @@ -275,19 +259,6 @@ contract DGDeployJSONConfigProvider is Script { } function validateConfig(DeployConfig memory config) internal pure { - checkCommitteeQuorum( - config.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS, - config.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, - "EMERGENCY_ACTIVATION_COMMITTEE" - ); - checkCommitteeQuorum( - config.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS, - config.EMERGENCY_EXECUTION_COMMITTEE_QUORUM, - "EMERGENCY_EXECUTION_COMMITTEE" - ); - - checkCommitteeQuorum(config.RESEAL_COMMITTEE_MEMBERS, config.RESEAL_COMMITTEE_QUORUM, "RESEAL_COMMITTEE"); - if ( config.TIEBREAKER_CORE_QUORUM == 0 || config.TIEBREAKER_CORE_QUORUM > config.TIEBREAKER_SUB_COMMITTEES_COUNT ) { @@ -419,18 +390,6 @@ contract DGDeployJSONConfigProvider is Script { console.log("================================================="); console.log("Loaded valid config with the following committees:"); - printCommittee( - config.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS, - config.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, - "EmergencyActivationCommittee members, quorum" - ); - - printCommittee( - config.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS, - config.EMERGENCY_EXECUTION_COMMITTEE_QUORUM, - "EmergencyExecutionCommittee members, quorum" - ); - printCommittee( config.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS, config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[0], @@ -509,10 +468,6 @@ contract DGDeployJSONConfigProvider is Script { ); } - printCommittee( - config.RESEAL_COMMITTEE_MEMBERS, config.RESEAL_COMMITTEE_QUORUM, "ResealCommittee members, quorum" - ); - console.log("================================================="); } diff --git a/scripts/deploy/Readme.md b/scripts/deploy/Readme.md index 4ac36477..8e256b9d 100644 --- a/scripts/deploy/Readme.md +++ b/scripts/deploy/Readme.md @@ -25,13 +25,13 @@ anvil --fork-url https://.infura.io/v3/ --bloc 3. Create a deploy config JSON file with all the required values (at the location specified in DEPLOY_CONFIG_FILE_PATH): ``` { - "EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS": [addr1,addr2,addr3], - "EMERGENCY_EXECUTION_COMMITTEE_MEMBERS": [addr1,addr2,addr3], + "EMERGENCY_ACTIVATION_COMMITTEE":
, + "EMERGENCY_EXECUTION_COMMITTEE":
, "TIEBREAKER_SUB_COMMITTEE_1_MEMBERS": [addr1,addr2,addr3], "TIEBREAKER_SUB_COMMITTEE_2_MEMBERS": [addr1,addr2,addr3], "TIEBREAKER_SUB_COMMITTEES_QUORUMS": [3,2], "TIEBREAKER_SUB_COMMITTEES_COUNT": 2, - "RESEAL_COMMITTEE_MEMBERS": [addr1,addr2,addr3] + "RESEAL_COMMITTEE":
} ``` @@ -70,11 +70,8 @@ anvil --fork-url https://.infura.io/v3/ --bloc "ADMIN_EXECUTOR": "...", "TIMELOCK": "...", "EMERGENCY_GOVERNANCE": "...", - "EMERGENCY_ACTIVATION_COMMITTEE": "...", - "EMERGENCY_EXECUTION_COMMITTEE": "...", "RESEAL_MANAGER": "...", "DUAL_GOVERNANCE": "...", - "RESEAL_COMMITTEE": "...", "TIEBREAKER_CORE_COMMITTEE": "...", "TIEBREAKER_SUB_COMMITTEES": ["...", "..."] } diff --git a/scripts/deploy/Verify.s.sol b/scripts/deploy/Verify.s.sol index 56deaf3a..2b95fa6f 100644 --- a/scripts/deploy/Verify.s.sol +++ b/scripts/deploy/Verify.s.sol @@ -48,11 +48,8 @@ contract Verify is Script { adminExecutor: payable(stdJson.readAddress(deployedAddressesJson, ".ADMIN_EXECUTOR")), timelock: stdJson.readAddress(deployedAddressesJson, ".TIMELOCK"), emergencyGovernance: stdJson.readAddress(deployedAddressesJson, ".EMERGENCY_GOVERNANCE"), - emergencyActivationCommittee: stdJson.readAddress(deployedAddressesJson, ".EMERGENCY_ACTIVATION_COMMITTEE"), - emergencyExecutionCommittee: stdJson.readAddress(deployedAddressesJson, ".EMERGENCY_EXECUTION_COMMITTEE"), resealManager: stdJson.readAddress(deployedAddressesJson, ".RESEAL_MANAGER"), dualGovernance: stdJson.readAddress(deployedAddressesJson, ".DUAL_GOVERNANCE"), - resealCommittee: stdJson.readAddress(deployedAddressesJson, ".RESEAL_COMMITTEE"), tiebreakerCoreCommittee: stdJson.readAddress(deployedAddressesJson, ".TIEBREAKER_CORE_COMMITTEE"), tiebreakerSubCommittees: stdJson.readAddressArray(deployedAddressesJson, ".TIEBREAKER_SUB_COMMITTEES") }); @@ -71,9 +68,6 @@ contract Verify is Script { console.log("AdminExecutor address", res.adminExecutor); console.log("EmergencyProtectedTimelock address", res.timelock); console.log("EmergencyGovernance address", res.emergencyGovernance); - console.log("EmergencyActivationCommittee address", res.emergencyActivationCommittee); - console.log("EmergencyExecutionCommittee address", res.emergencyExecutionCommittee); - console.log("ResealCommittee address", res.resealCommittee); } function loadDeployedAddressesFile(string memory deployedAddressesFilePath) diff --git a/test/utils/SetupDeployment.sol b/test/utils/SetupDeployment.sol index b4aaed42..14ccac34 100644 --- a/test/utils/SetupDeployment.sol +++ b/test/utils/SetupDeployment.sol @@ -152,12 +152,9 @@ abstract contract SetupDeployment is Test { dgDeployConfig.MAX_EMERGENCY_MODE_DURATION = _MAX_EMERGENCY_MODE_DURATION; dgDeployConfig.EMERGENCY_PROTECTION_DURATION = _EMERGENCY_PROTECTION_DURATION; dgDeployConfig.MAX_EMERGENCY_PROTECTION_DURATION = _MAX_EMERGENCY_PROTECTION_DURATION; - dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_QUORUM = _EMERGENCY_ACTIVATION_COMMITTEE_QUORUM; - dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS = - _generateRandomAddresses(_EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS_COUNT); - dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_QUORUM = _EMERGENCY_EXECUTION_COMMITTEE_QUORUM; - dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE_MEMBERS = - _generateRandomAddresses(_EMERGENCY_EXECUTION_COMMITTEE_MEMBERS_COUNT); + + dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE = address(0); + dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE = address(0); dgDeployConfig.TIEBREAKER_CORE_QUORUM = TIEBREAKER_SUB_COMMITTEES_COUNT; dgDeployConfig.TIEBREAKER_EXECUTION_DELAY = TIEBREAKER_EXECUTION_DELAY; @@ -169,13 +166,8 @@ abstract contract SetupDeployment is Test { dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS = [TIEBREAKER_SUB_COMMITTEE_QUORUM, TIEBREAKER_SUB_COMMITTEE_QUORUM]; - uint256 resealCommitteeMembersCount = 5; - dgDeployConfig.RESEAL_COMMITTEE_MEMBERS = new address[](resealCommitteeMembersCount); - for (uint256 i = 0; i < resealCommitteeMembersCount; ++i) { - dgDeployConfig.RESEAL_COMMITTEE_MEMBERS[i] = - makeAddr(string(abi.encode(0xFA + i * resealCommitteeMembersCount + 65))); - } - dgDeployConfig.RESEAL_COMMITTEE_QUORUM = 3; + dgDeployConfig.RESEAL_COMMITTEE = address(0); + dgDeployConfig.MIN_WITHDRAWALS_BATCH_SIZE = 4; dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT = MIN_TIEBREAKER_ACTIVATION_TIMEOUT; dgDeployConfig.TIEBREAKER_ACTIVATION_TIMEOUT = TIEBREAKER_ACTIVATION_TIMEOUT; @@ -237,10 +229,8 @@ abstract contract SetupDeployment is Test { _tiebreakerCoreCommittee.transferOwnership(address(_adminExecutor)); - _resealCommittee = DGContractsDeployment.deployResealCommittee( - address(_adminExecutor), address(_dualGovernance), dgDeployConfig - ); - contracts.resealCommittee = _resealCommittee; + _resealCommittee = _deployResealCommittee(); + dgDeployConfig.RESEAL_COMMITTEE = address(_resealCommittee); // --- // Finalize Setup @@ -269,20 +259,30 @@ abstract contract SetupDeployment is Test { // --- function _deployEmergencyProtectedTimelockContracts(bool isEmergencyProtectionEnabled) internal { + contracts = DGContractsDeployment.deployAdminExecutorAndTimelock(dgDeployConfig, address(this)); + _adminExecutor = contracts.adminExecutor; + _timelock = contracts.timelock; + if (isEmergencyProtectionEnabled) { - contracts = DGContractsDeployment.deployEmergencyProtectedTimelockContracts( - lidoAddresses, dgDeployConfig, contracts, address(this) - ); - _adminExecutor = contracts.adminExecutor; - _timelock = contracts.timelock; - _emergencyActivationCommittee = contracts.emergencyActivationCommittee; - _emergencyExecutionCommittee = contracts.emergencyExecutionCommittee; + _emergencyActivationCommittee = _deployEmergencyActivationCommittee({ + quorum: _EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, + members: _generateRandomAddresses(_EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS_COUNT), + owner: address(_adminExecutor), + timelock: _timelock + }); + + _emergencyExecutionCommittee = _deployEmergencyExecutionCommittee({ + quorum: _EMERGENCY_EXECUTION_COMMITTEE_QUORUM, + members: _generateRandomAddresses(_EMERGENCY_EXECUTION_COMMITTEE_MEMBERS_COUNT), + owner: address(_adminExecutor), + timelock: _timelock + }); + dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE = address(_emergencyActivationCommittee); + dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE = address(_emergencyExecutionCommittee); + + DGContractsDeployment.deployEmergencyProtectedTimelockContracts(lidoAddresses, dgDeployConfig, contracts); + _emergencyGovernance = contracts.emergencyGovernance; - } else { - _adminExecutor = DGContractsDeployment.deployExecutor(address(this)); - _timelock = _deployEmergencyProtectedTimelock(_adminExecutor); - contracts.adminExecutor = _adminExecutor; - contracts.timelock = _timelock; } } @@ -298,6 +298,37 @@ abstract contract SetupDeployment is Test { return DGContractsDeployment.deployDualGovernanceConfigProvider(dgDeployConfig); } + function _deployEmergencyActivationCommittee( + EmergencyProtectedTimelock timelock, + address owner, + uint256 quorum, + address[] memory members + ) internal returns (EmergencyActivationCommittee) { + return new EmergencyActivationCommittee(owner, members, quorum, address(timelock)); + } + + function _deployEmergencyExecutionCommittee( + EmergencyProtectedTimelock timelock, + address owner, + uint256 quorum, + address[] memory members + ) internal returns (EmergencyExecutionCommittee) { + return new EmergencyExecutionCommittee(owner, members, quorum, address(timelock)); + } + + function _deployResealCommittee() internal returns (ResealCommittee) { + uint256 quorum = 3; + uint256 membersCount = 5; + address[] memory committeeMembers = new address[](membersCount); + for (uint256 i = 0; i < membersCount; ++i) { + committeeMembers[i] = makeAddr(string(abi.encode(0xFA + i * membersCount + 65))); + } + + return new ResealCommittee( + address(_adminExecutor), committeeMembers, quorum, address(_dualGovernance), Durations.from(0) + ); + } + function _deployResealManager(ITimelock timelock) internal returns (ResealManager) { return DGContractsDeployment.deployResealManager(timelock); } From 9f4579ef596e8202f6885a1fc34f93d2d138fb8a Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:37:24 +0400 Subject: [PATCH 031/107] Update .env.example --- .env.example | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.env.example b/.env.example index b39cb898..a92bdad3 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,6 @@ MAINNET_RPC_URL= # Deploy script env vars -CHAIN=<"mainnet" OR "holesky"> -DEPLOYER_PRIVATE_KEY=... +CHAIN=<"mainnet" OR "holesky" OR "holesky-mocks"> DEPLOY_CONFIG_FILE_PATH=deploy-config/deploy-config.json DEPLOYED_ADDRESSES_FILE_PATH=deploy-config/deployed-addrs.json From 20a4d37de1e639e35bd5c3aef470916ccde98589 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Sun, 13 Oct 2024 22:19:22 +0400 Subject: [PATCH 032/107] Support etherscan verification --- .env.example | 1 + foundry.toml | 4 ++++ scripts/deploy/Readme.md | 11 +++++++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index a92bdad3..38add99d 100644 --- a/.env.example +++ b/.env.example @@ -2,5 +2,6 @@ MAINNET_RPC_URL= # Deploy script env vars CHAIN=<"mainnet" OR "holesky" OR "holesky-mocks"> +ETHERSCAN_MAINNET_KEY=... DEPLOY_CONFIG_FILE_PATH=deploy-config/deploy-config.json DEPLOYED_ADDRESSES_FILE_PATH=deploy-config/deployed-addrs.json diff --git a/foundry.toml b/foundry.toml index fc060006..cd8fec52 100644 --- a/foundry.toml +++ b/foundry.toml @@ -17,3 +17,7 @@ test = 'test/kontrol' [fmt] line_length = 120 multiline_func_header = 'params_first_multi' + +[etherscan] +mainnet = { key = "${ETHERSCAN_MAINNET_KEY}" } +holesky = { key = "${ETHERSCAN_MAINNET_KEY}", chain = "17000" } diff --git a/scripts/deploy/Readme.md b/scripts/deploy/Readme.md index 8e256b9d..62dab88e 100644 --- a/scripts/deploy/Readme.md +++ b/scripts/deploy/Readme.md @@ -2,7 +2,7 @@ ### Running locally with Anvil -Start Anvil, provide RPC url (Infura as an example) +Start Anvil, provide RPC URL (Infura as an example) ``` anvil --fork-url https://.infura.io/v3/ --block-time 300 ``` @@ -19,6 +19,7 @@ anvil --fork-url https://.infura.io/v3/ --bloc ``` CHAIN=<"mainnet" OR "holesky" OR "holesky-mocks"> + ETHERSCAN_MAINNET_KEY=... DEPLOY_CONFIG_FILE_PATH=... (for example: "deploy-config/deploy-config.json") ``` @@ -48,12 +49,18 @@ anvil --fork-url https://.infura.io/v3/ --bloc } ``` -4. Run the script (with the local Anvil as an example) +4. Run the deployment script + With the local fork (Anvil): ``` forge script scripts/deploy/DeployConfigurable.s.sol:DeployConfigurable --fork-url http://localhost:8545 --broadcast --account Deployer1 --sender ``` + On a testnet (with Etherscan verification): + ``` + forge script scripts/deploy/DeployConfigurable.s.sol:DeployConfigurable --fork-url https://holesky.infura.io/v3/ --broadcast --account Deployer1 --sender --verify + ``` + ### Running the verification script 1. Set up the required env variables in the .env file From 4f17d3b756ab44c55c8bc6f3a724bcbca0dc3773 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 16 Oct 2024 20:54:40 +0400 Subject: [PATCH 033/107] Fix review comments --- scripts/deploy/DeployBase.s.sol | 1 - scripts/deploy/DeployVerification.sol | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/scripts/deploy/DeployBase.s.sol b/scripts/deploy/DeployBase.s.sol index 29aaa5ed..62efd5f9 100644 --- a/scripts/deploy/DeployBase.s.sol +++ b/scripts/deploy/DeployBase.s.sol @@ -18,7 +18,6 @@ abstract contract DeployBase is Script { DeployConfig internal config; LidoContracts internal lidoAddresses; address private deployer; - uint256 private pk; function run() external { if (lidoAddresses.chainId != block.chainid) { diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index 72faf6ac..a127123f 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.26; import {Timestamps} from "contracts/types/Timestamp.sol"; import {Durations} from "contracts/types/Duration.sol"; -import {PercentD16} from "contracts/types/PercentD16.sol"; import {Executor} from "contracts/Executor.sol"; import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; @@ -196,13 +195,11 @@ library DeployVerification { DualGovernanceConfig.Context memory dgConfig = dg.getConfigProvider().getDualGovernanceConfig(); require( - PercentD16.unwrap(dgConfig.firstSealRageQuitSupport) - == PercentD16.unwrap(dgDeployConfig.FIRST_SEAL_RAGE_QUIT_SUPPORT), + dgConfig.firstSealRageQuitSupport == dgDeployConfig.FIRST_SEAL_RAGE_QUIT_SUPPORT, "Incorrect parameter FIRST_SEAL_RAGE_QUIT_SUPPORT" ); require( - PercentD16.unwrap(dgConfig.secondSealRageQuitSupport) - == PercentD16.unwrap(dgDeployConfig.SECOND_SEAL_RAGE_QUIT_SUPPORT), + dgConfig.secondSealRageQuitSupport == dgDeployConfig.SECOND_SEAL_RAGE_QUIT_SUPPORT, "Incorrect parameter SECOND_SEAL_RAGE_QUIT_SUPPORT" ); require( @@ -250,6 +247,7 @@ library DeployVerification { require(dg.getEffectiveState() == State.Normal, "Incorrect DualGovernance effective state"); require(dg.getProposers().length == 1, "Incorrect amount of proposers"); require(dg.isProposer(address(lidoAddresses.voting)) == true, "Lido voting is not set as a proposers[0]"); + require(dg.isExecutor(res.adminExecutor) == true, "adminExecutor is not set as a proposers[0].executor"); IDualGovernance.StateDetails memory stateDetails = dg.getStateDetails(); require(stateDetails.effectiveState == State.Normal, "Incorrect DualGovernance effectiveState"); From 55e18801ae92bdee86917dfbcec1a84cdbe5142d Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:19:20 +0400 Subject: [PATCH 034/107] Remove unnecessary comments --- scripts/deploy/ContractsDeployment.sol | 1 - scripts/deploy/DeployVerification.sol | 2 -- scripts/deploy/JsonConfig.s.sol | 1 - 3 files changed, 4 deletions(-) diff --git a/scripts/deploy/ContractsDeployment.sol b/scripts/deploy/ContractsDeployment.sol index 8ff3144d..80879c80 100644 --- a/scripts/deploy/ContractsDeployment.sol +++ b/scripts/deploy/ContractsDeployment.sol @@ -129,7 +129,6 @@ library DGContractsDeployment { ) ); - // TODO: Do we really need to set it? adminExecutor.execute( address(timelock), 0, diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index a127123f..c6a8879f 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -18,8 +18,6 @@ import {DualGovernanceConfig} from "contracts/libraries/DualGovernanceConfig.sol import {State} from "contracts/libraries/DualGovernanceStateMachine.sol"; import {DeployConfig, LidoContracts, getSubCommitteeData} from "./Config.sol"; -// TODO: long error texts in require() - library DeployVerification { struct DeployedAddresses { address payable adminExecutor; diff --git a/scripts/deploy/JsonConfig.s.sol b/scripts/deploy/JsonConfig.s.sol index 1c9df3f2..7d48e7ef 100644 --- a/scripts/deploy/JsonConfig.s.sol +++ b/scripts/deploy/JsonConfig.s.sol @@ -237,7 +237,6 @@ contract DGDeployJSONConfigProvider is Script { } if (keccak256(bytes(chainName)) == CHAIN_NAME_HOLESKY_MOCKS_HASH) { - // TODO: is it ok to use the same file? string memory jsonConfig = loadConfigFile(); return LidoContracts({ From a0be879bef9204bc6e242959ee0874b600239faf Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 15 Oct 2024 17:59:29 +0400 Subject: [PATCH 035/107] Remove reseal, emergency activation and execution committees --- .../EmergencyActivationCommittee.sol | 61 ----- .../EmergencyExecutionCommittee.sol | 145 ----------- contracts/committees/ResealCommittee.sol | 90 ------- docs/plan-b.md | 224 +--------------- docs/specification.md | 152 ----------- scripts/deploy/DeployVerification.sol | 8 +- test/scenario/emergency-committee.t.sol | 77 ------ test/scenario/reseal-committee.t.sol | 68 ----- .../EmergencyActivationCommittee.t.sol | 134 ---------- .../EmergencyExecutionCommittee.t.sol | 240 ------------------ test/unit/committees/ResealCommittee.t.sol | 153 ----------- test/utils/SetupDeployment.sol | 75 ++---- test/utils/scenario-test-blueprint.sol | 24 +- 13 files changed, 34 insertions(+), 1417 deletions(-) delete mode 100644 contracts/committees/EmergencyActivationCommittee.sol delete mode 100644 contracts/committees/EmergencyExecutionCommittee.sol delete mode 100644 contracts/committees/ResealCommittee.sol delete mode 100644 test/scenario/emergency-committee.t.sol delete mode 100644 test/scenario/reseal-committee.t.sol delete mode 100644 test/unit/committees/EmergencyActivationCommittee.t.sol delete mode 100644 test/unit/committees/EmergencyExecutionCommittee.t.sol delete mode 100644 test/unit/committees/ResealCommittee.t.sol diff --git a/contracts/committees/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol deleted file mode 100644 index f15ba95a..00000000 --- a/contracts/committees/EmergencyActivationCommittee.sol +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: MIT -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 {IEmergencyProtectedTimelock} from "../interfaces/IEmergencyProtectedTimelock.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 -contract EmergencyActivationCommittee is HashConsensus { - address public immutable EMERGENCY_PROTECTED_TIMELOCK; - - bytes32 private constant EMERGENCY_ACTIVATION_HASH = keccak256("EMERGENCY_ACTIVATE"); - - constructor( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address emergencyProtectedTimelock - ) HashConsensus(owner, Durations.ZERO) { - EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; - - _addMembers(committeeMembers, executionQuorum); - } - - /// @notice Approves the emergency activation by casting a vote - /// @dev Only callable by committee members - function approveActivateEmergencyMode() public { - _checkCallerIsMember(); - _vote(EMERGENCY_ACTIVATION_HASH, true); - } - - /// @notice Gets the current state of the emergency activation vote - /// @return support The number of votes in support of the activation - /// @return executionQuorum The required number of votes for execution - /// @return quorumAt The timestamp when the quorum was reached - /// @return isExecuted Whether the activation has been executed - function getActivateEmergencyModeState() - public - view - returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) - { - return _getHashState(EMERGENCY_ACTIVATION_HASH); - } - - /// @notice Executes the emergency activation if the quorum is reached - /// @dev Calls the emergencyActivate function on the Emergency Protected Timelock contract - function executeActivateEmergencyMode() external { - _markUsed(EMERGENCY_ACTIVATION_HASH); - Address.functionCall( - EMERGENCY_PROTECTED_TIMELOCK, - abi.encodeWithSelector(IEmergencyProtectedTimelock.activateEmergencyMode.selector) - ); - } -} diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol deleted file mode 100644 index 8f4288fa..00000000 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ /dev/null @@ -1,145 +0,0 @@ -// SPDX-License-Identifier: MIT -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 {IEmergencyProtectedTimelock} from "../interfaces/IEmergencyProtectedTimelock.sol"; - -import {HashConsensus} from "./HashConsensus.sol"; -import {ProposalsList} from "./ProposalsList.sol"; - -enum ProposalType { - EmergencyExecute, - EmergencyReset -} - -/// @title Emergency Execution Committee Contract -/// @notice This contract allows a committee to vote on and execute emergency proposals -/// @dev Inherits from HashConsensus for voting mechanisms and ProposalsList for proposal management -contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { - error ProposalDoesNotExist(uint256 proposalId); - - address public immutable EMERGENCY_PROTECTED_TIMELOCK; - - constructor( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address emergencyProtectedTimelock - ) HashConsensus(owner, Durations.ZERO) { - EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; - - _addMembers(committeeMembers, executionQuorum); - } - - // --- - // Emergency Execution - // --- - - /// @notice Votes on an emergency execution proposal - /// @dev Only callable by committee members - /// @param proposalId The ID of the proposal to vote on - /// @param _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, _support); - _pushProposal(key, uint256(ProposalType.EmergencyExecute), proposalData); - } - - /// @notice Gets the current state of an emergency execution proposal - /// @param proposalId The ID of the proposal - /// @return support The number of votes in support of the proposal - /// @return executionQuorum The required number of votes for execution - /// @return quorumAt The timestamp when the quorum was reached - /// @return isExecuted Whether the proposal has been executed - function getEmergencyExecuteState(uint256 proposalId) - public - view - returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) - { - (, bytes32 key) = _encodeEmergencyExecute(proposalId); - return _getHashState(key); - } - - /// @notice Executes an approved emergency execution proposal - /// @param proposalId The ID of the proposal to execute - function executeEmergencyExecute(uint256 proposalId) public { - (, bytes32 key) = _encodeEmergencyExecute(proposalId); - _markUsed(key); - Address.functionCall( - 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 > IEmergencyProtectedTimelock(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 - /// @return key The generated proposal key - function _encodeEmergencyExecute(uint256 proposalId) - private - pure - returns (bytes memory proposalData, bytes32 key) - { - proposalData = abi.encode(ProposalType.EmergencyExecute, proposalId); - key = keccak256(proposalData); - } - - // --- - // Governance reset - // --- - - /// @notice Approves an emergency reset proposal - /// @dev Only callable by committee members - function approveEmergencyReset() public { - _checkCallerIsMember(); - bytes32 proposalKey = _encodeEmergencyResetProposalKey(); - _vote(proposalKey, true); - _pushProposal(proposalKey, uint256(ProposalType.EmergencyReset), bytes("")); - } - - /// @notice Gets the current state of an emergency reset proposal - /// @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 getEmergencyResetState() - public - view - returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) - { - bytes32 proposalKey = _encodeEmergencyResetProposalKey(); - return _getHashState(proposalKey); - } - - /// @notice Executes an approved emergency reset proposal - function executeEmergencyReset() external { - bytes32 proposalKey = _encodeEmergencyResetProposalKey(); - _markUsed(proposalKey); - Address.functionCall( - EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyReset.selector) - ); - } - - /// @notice Encodes the proposal key for an emergency reset - /// @return The generated proposal key - function _encodeEmergencyResetProposalKey() internal pure returns (bytes32) { - return keccak256(abi.encode(ProposalType.EmergencyReset, bytes32(0))); - } -} diff --git a/contracts/committees/ResealCommittee.sol b/contracts/committees/ResealCommittee.sol deleted file mode 100644 index f71008ce..00000000 --- a/contracts/committees/ResealCommittee.sol +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: MIT -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"; - -/// @title Reseal Committee Contract -/// @notice This contract allows a committee to vote on and execute resealing proposals -/// @dev Inherits from HashConsensus for voting mechanisms and ProposalsList for proposal management -contract ResealCommittee is HashConsensus, ProposalsList { - error InvalidSealable(address sealable); - - address public immutable DUAL_GOVERNANCE; - - mapping(bytes32 hash => uint256 nonce) private _resealNonces; - - constructor( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address dualGovernance, - Duration timelock - ) HashConsensus(owner, timelock) { - DUAL_GOVERNANCE = dualGovernance; - - _addMembers(committeeMembers, executionQuorum); - } - - /// @notice Votes on a reseal proposal - /// @dev Allows committee members to vote on resealing a sealed address - /// @param sealable The address to reseal - /// @param support Indicates whether the member supports the proposal - function voteReseal(address sealable, bool support) external { - _checkCallerIsMember(); - - if (sealable == address(0)) { - revert InvalidSealable(sealable); - } - - (bytes memory proposalData, bytes32 key) = _encodeResealProposal(sealable); - _vote(key, support); - _pushProposal(key, 0, proposalData); - } - - /// @notice Gets the current state of a reseal proposal - /// @dev Retrieves the state of the reseal proposal for a sealed address - /// @param sealable The addresses for the reseal proposal - /// @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 - function getResealState(address sealable) - external - view - returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt) - { - (, bytes32 key) = _encodeResealProposal(sealable); - (support, executionQuorum, quorumAt,) = _getHashState(key); - } - - /// @notice Executes an approved reseal proposal - /// @dev Executes the reseal proposal by calling the reseal function on the Dual Governance contract - /// @param sealable The address to reseal - function executeReseal(address sealable) external { - (, bytes32 key) = _encodeResealProposal(sealable); - _markUsed(key); - - Address.functionCall(DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.resealSealable.selector, sealable)); - - bytes32 resealNonceHash = keccak256(abi.encode(sealable)); - _resealNonces[resealNonceHash]++; - } - - /// @notice Encodes a reseal proposal - /// @dev Internal function to encode the proposal data and generate the proposal key - /// @param sealable The address to reseal - /// @return data The encoded proposal data - /// @return key The generated proposal key - function _encodeResealProposal(address sealable) internal view returns (bytes memory data, bytes32 key) { - bytes32 resealNonceHash = keccak256(abi.encode(sealable)); - data = abi.encode(sealable, _resealNonces[resealNonceHash]); - key = keccak256(data); - } -} diff --git a/docs/plan-b.md b/docs/plan-b.md index 8afadff5..10bce15d 100644 --- a/docs/plan-b.md +++ b/docs/plan-b.md @@ -9,15 +9,10 @@ Timelocked Governance (TG) is a governance subsystem positioned between the Lido * [Proposal flow](#proposal-flow) * [Proposal execution](#proposal-execution) * [Common types](#common-types) -* Core Contracts: +* 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 @@ -28,10 +23,6 @@ The system comprises the following primary contracts: - [**`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 @@ -109,7 +100,9 @@ See: [`EmergencyProtectedTimelock.cancelAllNonExecutedProposals`](#) #### Preconditions * MUST be called by an [admin voting system](#) + ## Contract: `EmergencyProtectedTimelock` + `EmergencyProtectedTimelock` is a singleton instance that stores and manages the lifecycle of proposals submitted by the DAO via the `TimelockedGovernance` contract. It can be configured with time-bound **Emergency Activation Committee** and **Emergency Execution Committee**, which act as safeguards against the execution of malicious proposals. For a proposal to be executed, the following steps have to be performed in order: @@ -123,9 +116,10 @@ The contract only allows proposal submission and scheduling by the `governance` If the Emergency Committees are set up and active, the governance proposal undergoes a separate emergency protection delay between submission and scheduling. This additional timelock is implemented to protect against the execution of malicious proposals submitted by the DAO. If the Emergency Committees aren't set, the proposal flow remains the same, but the timelock duration is zero. -If the Emergency Committees are set up and active, the governance proposal undergoes a separate emergency protection delay between submission and scheduling. This additional timelock is implemented to safeguard against the execution of malicious proposals submitted by the DAO. If the Emergency Committees aren't set, the proposal flow remains the same, but the timelock duration is zero. 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 proposer, address executor, ExecutorCall[] calls, string metadata) @@ -219,211 +213,3 @@ Reverts if the call was unsuccessful. The result of the call. #### Preconditions * MUST be called by the contract owner (which SHOULD be the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance). - -## Contract: `Configuration` -`Configuration` is the smart contract encompassing all the constants in the Timelocked Governance design & providing the interfaces for getting access to them. It implements interfaces `IAdminExecutorConfiguration`, `ITimelockConfiguration` covering for relevant "parameters domains". - -## Contract: `ProposalsList` -`ProposalsList` implements storage for list of `Proposal`s with public interface to access. - -### Function: `ProposalsList.getProposals` -```solidity -function getProposals(uint256 offset, uint256 limit) external view returns (Proposal[] memory proposals) -``` -Returns a list of `Proposal` structs, starting from the specified `offset` and bounded to the specified `limit`. - -### Function: `ProposalsList.getProposalAt` -```solidity -function getProposalAt(uint256 index) external view returns (Proposal memory) -``` -Returns the `Proposal` at the specified index. - -### Function: `ProposalsList.getProposal` -```solidity -function getProposal(bytes32 key) external view returns (Proposal memory) -``` -Returns the `Proposal` with the given key. - -### Function: `ProposalsList.getProposalsLength` -```solidity -function getProposalsLength() external view returns (uint256) -``` -Returns the total number of created `Proposal`s. - -### Function: `ProposalsList.getOrderedKeys` -```solidity -function getOrderedKeys(uint256 offset, uint256 limit) external view returns (bytes32[] memory) -``` -Returns a list of `Proposal` keys, starting from the specified `offset` and bounded by the specified `limit`. - - -## Contract: `HashConsensus` -`HashConsensus` is an abstract contract that facilitates consensus-based decision-making among a set of members. Consensus is achieved through members voting on a specific hash, with decisions executed only if a quorum is reached and a timelock period has elapsed. - -### Function: `HashConsensus.addMember` -```solidity -function addMember(address newMember, uint256 newQuorum) external onlyOwner -``` -Adds a new member and updates the quorum. - -#### Preconditions -- Only the `owner` can call this function. -- `newQuorum` MUST be greater than 0 and less than or equal to the number of members. - -### Function: `HashConsensus.removeMember` -```solidity -function removeMember(address memberToRemove, uint256 newQuorum) external onlyOwner -``` -Removes a member and updates the quorum. - -#### Preconditions -- Only the `owner` can call this function. -- `memberToRemove` MUST be an added member. -- `newQuorum` MUST be greater than 0 and less than or equal to the number of remaining members. - -### Function: `HashConsensus.getMembers` -```solidity -function getMembers() external view returns (address[] memory) -``` -Returns the list of current members. - -### Function: `HashConsensus.isMember` -```solidity -function isMember(address member) external view returns (bool) -``` -Returns whether an account is listed as a member. - -### Function: `HashConsensus.setTimelockDuration` -```solidity -function setTimelockDuration(uint256 timelock) external onlyOwner -``` -Sets the duration of the timelock. - -#### Preconditions -- Only the `owner` can call this function. - -### Function: `HashConsensus.setQuorum` -```solidity -function setQuorum(uint256 newQuorum) external onlyOwner -``` -Sets the quorum required for decision execution. - -#### Preconditions -- Only the `owner` can call this function. -- `newQuorum` MUST be greater than 0 and less than or equal to the number of members. - -## Contract: `EmergencyActivationCommittee` -`EmergencyActivationCommittee` is a smart contract that extends the functionality of the `HashConsensus` contract to manage the emergency activation process. It allows committee members to vote on and execute the activation of emergency protocols in the specified 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. - -### Function: `EmergencyActivationCommittee.approveActivateEmergencyMode` -```solidity -function approveActivateEmergencyMode() public onlyMember -``` -Approves the emergency activation by voting on the `EMERGENCY_ACTIVATION_HASH`. - -#### Preconditions -- MUST be called by a committee member. - -### Function: `EmergencyActivationCommittee.getActivateEmergencyModeState` -```solidity -function getActivateEmergencyModeState() - public - view - returns (uint256 support, uint256 executionQuorum, bool isExecuted) -``` -Returns the state of the emergency activation proposal, including the support count, quorum, and execution status. - -### Function: `EmergencyActivationCommittee.executeActivateEmergencyMode` -```solidity -function executeActivateEmergencyMode() external -``` -Executes the emergency activation by calling the `emergencyActivate` function on the `EmergencyProtectedTimelock` contract. - -#### Preconditions -- The emergency activation proposal MUST have reached quorum and passed the timelock duration. - -## Contract: `EmergencyExecutionCommittee` -`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. - -### Function: `EmergencyExecutionCommittee.voteEmergencyExecute` -```solidity -function voteEmergencyExecute(uint256 proposalId, bool _supports) public -``` -Allows committee members to vote on an emergency execution proposal. - -#### Preconditions -- MUST be called by a committee member. - -### Function: `EmergencyExecutionCommittee.getEmergencyExecuteState` -```solidity -function getEmergencyExecuteState(uint256 proposalId) - public - view - returns (uint256 support, uint256 executionQuorum, bool isExecuted) -``` -Returns the state of an emergency execution proposal, including the support count, quorum, and execution status. - -### Function: `EmergencyExecutionCommittee.executeEmergencyExecute` -```solidity -function executeEmergencyExecute(uint256 proposalId) public -``` -Executes an emergency execution proposal by calling the `emergencyExecute` function on the `EmergencyProtectedTimelock` contract. - -#### Preconditions -- The emergency execution proposal MUST have reached quorum and passed the timelock duration. - -### Function: `EmergencyExecutionCommittee.approveEmergencyReset` -```solidity -function approveEmergencyReset() public -``` -Approves the governance reset by voting on the reset proposal. - -#### Preconditions -- MUST be called by a committee member. - -### Function: `EmergencyExecutionCommittee.getEmergencyResetState` -```solidity -function getEmergencyResetState() - public - view - returns (uint256 support, uint256 executionQuorum, bool isExecuted) -``` -Returns the state of the governance reset proposal, including the support count, quorum, and execution status. - -### Function: `EmergencyExecutionCommittee.executeEmergencyReset` -```solidity -function executeEmergencyReset() external -``` -Executes the governance reset by calling the `emergencyReset` function on the `EmergencyProtectedTimelock` contract. - -#### Preconditions -- The governance reset proposal MUST have reached quorum and passed the timelock duration. - diff --git a/docs/specification.md b/docs/specification.md index d11fce03..0f4ddf64 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -34,11 +34,8 @@ This document provides the system description on the code architecture level. A * Committees: * [Contract: ProposalsList.sol](#contract-proposalslistsol) * [Contract: HashConsensus.sol](#contract-hashconsensussol) - * [Contract: ResealCommittee.sol](#contract-resealcommitteesol) * [Contract: TiebreakerCoreCommittee.sol](#contract-tiebreakercorecommitteesol) * [Contract: TiebreakerSubCommittee.sol](#contract-tiebreakersubcommitteesol) - * [Contract: EmergencyActivationCommittee.sol](#contract-emergencyactivationcommitteesol) - * [Contract: EmergencyExecutionCommittee.sol](#contract-emergencyexecutioncommitteesol) * [Upgrade flow description](#upgrade-flow-description) @@ -58,11 +55,8 @@ The system is composed of the following main contracts: 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). * [`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. ## Proposal flow @@ -1168,42 +1162,6 @@ Sets the quorum required for decision execution. * Only the owner can call this function. * `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 - -`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. - -### Function: ResealCommittee.voteReseal - -```solidity -function voteReseal(address sealable, bool support) -``` - -Reseals sealable by voting on it and adding it to the proposal list. - -#### Preconditions -* MUST be called by a member. - -### Function: ResealCommittee.getResealState - -```solidity -function getResealState(address sealable) - view - returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt) -``` - -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. - ## Contract: TiebreakerCoreCommittee.sol @@ -1358,116 +1316,6 @@ Executes a sealable resume request by calling the sealableResume function on the * Resume request MUST have reached quorum and passed the timelock duration. -## Contract: EmergencyActivationCommittee.sol - -`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. - -### Function: EmergencyActivationCommittee.approveActivateEmergencyMode - -```solidity -function approveActivateEmergencyMode() -``` - -Approves the emergency activation by voting on the `EMERGENCY_ACTIVATION_HASH`. - -#### Preconditions - -* MUST be called by a member. - -### Function: EmergencyActivationCommittee.getActivateEmergencyModeState - -```solidity -function getActivateEmergencyModeState() - view - returns (uint256 support, uint256 executionQuorum, bool isExecuted) -``` - -Returns the state of the emergency activation proposal including support count, quorum, and execution status. - -### Function: EmergencyActivationCommittee.executeActivateEmergencyMode - -```solidity -function executeActivateEmergencyMode() external -``` - -Executes the emergency activation by calling the `activateEmergencyMode` function on the `EmergencyProtectedTimelock` contract. - -#### Preconditions - -* Emergency activation proposal MUST have reached quorum and passed the timelock duration. - - -## Contract: EmergencyExecutionCommittee.sol - -`EmergencyExecutionCommittee` is a smart contract that extends the functionalities of `HashConsensus` and `ProposalsList` to manage emergency execution and governance reset proposals through a consensus mechanism. It interacts with the `EmergencyProtectedTimelock` contract to execute critical emergency proposals. - -### Function: EmergencyExecutionCommittee.voteEmergencyExecute - -```solidity -function voteEmergencyExecute(uint256 proposalId, bool _support) -``` - -Allows committee members to vote on an emergency execution proposal. - -#### Preconditions - -* MUST be called by a member. - -### Function: EmergencyExecutionCommittee.getEmergencyExecuteState - -```solidity -function getEmergencyExecuteState(uint256 proposalId) - view - returns (uint256 support, uint256 executionQuorum, bool isExecuted) -``` - -Returns the state of an emergency execution proposal including support count, quorum, and execution status. - -### Function: EmergencyExecutionCommittee.executeEmergencyExecute - -```solidity -function executeEmergencyExecute(uint256 proposalId) -``` - -Executes an emergency execution proposal by calling the `emergencyExecute` function on the `EmergencyProtectedTimelock` contract. - -#### Preconditions -* Emergency execution proposal MUST have reached quorum and passed the timelock duration. - - -### Function: EmergencyExecutionCommittee.approveEmergencyReset - -```solidity -function approveEmergencyReset() -``` - -Approves the governance reset by voting on the reset proposal. - -#### Preconditions - -* MUST be called by a member. - -### Function: EmergencyExecutionCommittee.getEmergencyResetState - -```solidity -function getEmergencyResetState() - view - returns (uint256 support, uint256 executionQuorum, bool isExecuted) -``` - -Returns the state of the governance reset proposal including support count, quorum, and execution status. - -### Function: EmergencyExecutionCommittee.executeEmergencyReset - -```solidity -function executeEmergencyReset() external -``` - -Executes the governance reset by calling the `emergencyReset` function on the `EmergencyProtectedTimelock` contract. - -#### Preconditions - -* Governance reset proposal MUST have reached quorum and passed the timelock duration. ## Upgrade flow description diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index c6a8879f..06f82495 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -291,14 +291,14 @@ library DeployVerification { TiebreakerCoreCommittee tcc = TiebreakerCoreCommittee(res.tiebreakerCoreCommittee); require(tcc.owner() == res.adminExecutor, "TiebreakerCoreCommittee owner != adminExecutor"); require( - tcc.timelockDuration() == dgDeployConfig.TIEBREAKER_EXECUTION_DELAY, + tcc.getTimelockDuration() == dgDeployConfig.TIEBREAKER_EXECUTION_DELAY, "Incorrect parameter TIEBREAKER_EXECUTION_DELAY" ); for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { require(tcc.isMember(res.tiebreakerSubCommittees[i]) == true, "Incorrect member of TiebreakerCoreCommittee"); } - require(tcc.quorum() == dgDeployConfig.TIEBREAKER_CORE_QUORUM, "Incorrect quorum in TiebreakerCoreCommittee"); + require(tcc.getQuorum() == dgDeployConfig.TIEBREAKER_CORE_QUORUM, "Incorrect quorum in TiebreakerCoreCommittee"); } function checkTiebreakerSubCommittee( @@ -308,14 +308,14 @@ library DeployVerification { ) internal view { TiebreakerSubCommittee tsc = TiebreakerSubCommittee(res.tiebreakerSubCommittees[index]); require(tsc.owner() == res.adminExecutor, "TiebreakerSubCommittee owner != adminExecutor"); - require(tsc.timelockDuration() == Durations.from(0), "TiebreakerSubCommittee timelock should be 0"); + require(tsc.getTimelockDuration() == Durations.from(0), "TiebreakerSubCommittee timelock should be 0"); (uint256 quorum, address[] memory members) = getSubCommitteeData(index, dgDeployConfig); for (uint256 i = 0; i < members.length; ++i) { require(tsc.isMember(members[i]) == true, "Incorrect member of TiebreakerSubCommittee"); } - require(tsc.quorum() == quorum, "Incorrect quorum in TiebreakerSubCommittee"); + require(tsc.getQuorum() == quorum, "Incorrect quorum in TiebreakerSubCommittee"); } function checkResealCommittee(DeployConfig memory dgDeployConfig) internal pure { diff --git a/test/scenario/emergency-committee.t.sol b/test/scenario/emergency-committee.t.sol deleted file mode 100644 index 32eed792..00000000 --- a/test/scenario/emergency-committee.t.sol +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; -import {PercentsD16} from "contracts/types/PercentD16.sol"; - -import {IPotentiallyDangerousContract} from "../utils/interfaces/IPotentiallyDangerousContract.sol"; -import {ScenarioTestBlueprint, ExternalCall, ExternalCallHelpers} from "../utils/scenario-test-blueprint.sol"; -import {DAO_AGENT} from "addresses/mainnet-addresses.sol"; - -contract EmergencyCommitteeTest is ScenarioTestBlueprint { - address internal immutable _VETOER = makeAddr("VETOER"); - uint256 public constant PAUSE_INFINITELY = type(uint256).max; - - function setUp() external { - _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); - _setupStETHBalance(_VETOER, PercentsD16.fromBasisPoints(10_00)); - _lockStETH(_VETOER, 1 ether); - } - - function test_emergency_committees_happy_path() external { - uint256 quorum; - uint256 support; - bool isExecuted; - - address[] memory members; - - ExternalCall[] memory proposalCalls = ExternalCallHelpers.create( - address(_targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRegularStaff, (0)) - ); - uint256 proposalIdToExecute = _submitProposal(_dualGovernance, "Proposal for execution", proposalCalls); - - _wait(_timelock.getAfterSubmitDelay().plusSeconds(1)); - _assertCanSchedule(_dualGovernance, proposalIdToExecute, true); - _scheduleProposal(_dualGovernance, proposalIdToExecute); - - // Emergency Activation - members = _emergencyActivationCommittee.getMembers(); - for (uint256 i = 0; i < _emergencyActivationCommittee.getQuorum() - 1; i++) { - vm.prank(members[i]); - _emergencyActivationCommittee.approveActivateEmergencyMode(); - (support, quorum,, isExecuted) = _emergencyActivationCommittee.getActivateEmergencyModeState(); - assert(support < quorum); - assert(isExecuted == false); - } - - vm.prank(members[members.length - 1]); - _emergencyActivationCommittee.approveActivateEmergencyMode(); - (support, quorum,, isExecuted) = _emergencyActivationCommittee.getActivateEmergencyModeState(); - assert(support == quorum); - assert(isExecuted == false); - - _emergencyActivationCommittee.executeActivateEmergencyMode(); - (support, quorum,, isExecuted) = _emergencyActivationCommittee.getActivateEmergencyModeState(); - assert(isExecuted == true); - - // Emergency Execute - members = _emergencyExecutionCommittee.getMembers(); - for (uint256 i = 0; i < _emergencyExecutionCommittee.getQuorum() - 1; i++) { - vm.prank(members[i]); - _emergencyExecutionCommittee.voteEmergencyExecute(proposalIdToExecute, true); - (support, quorum,, isExecuted) = _emergencyExecutionCommittee.getEmergencyExecuteState(proposalIdToExecute); - assert(support < quorum); - assert(isExecuted == false); - } - - vm.prank(members[members.length - 1]); - _emergencyExecutionCommittee.voteEmergencyExecute(proposalIdToExecute, true); - (support, quorum,, isExecuted) = _emergencyExecutionCommittee.getEmergencyExecuteState(proposalIdToExecute); - assert(support == quorum); - assert(isExecuted == false); - - _emergencyExecutionCommittee.executeEmergencyExecute(proposalIdToExecute); - (support, quorum,, isExecuted) = _emergencyExecutionCommittee.getEmergencyExecuteState(proposalIdToExecute); - assert(isExecuted == true); - } -} diff --git a/test/scenario/reseal-committee.t.sol b/test/scenario/reseal-committee.t.sol deleted file mode 100644 index 619c2042..00000000 --- a/test/scenario/reseal-committee.t.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {DualGovernance} from "contracts/DualGovernance.sol"; -import {ResealManager} from "contracts/ResealManager.sol"; -import {PercentsD16} from "contracts/types/PercentD16.sol"; -import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; - -import {ScenarioTestBlueprint, ExternalCall} from "../utils/scenario-test-blueprint.sol"; -import {DAO_AGENT} from "addresses/mainnet-addresses.sol"; - -contract ResealCommitteeTest is ScenarioTestBlueprint { - address internal immutable _VETOER = makeAddr("VETOER"); - uint256 public constant PAUSE_INFINITELY = type(uint256).max; - - function setUp() external { - _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); - _setupStETHBalance(_VETOER, PercentsD16.fromBasisPoints(10_00)); - _lockStETH(_VETOER, 1 ether); - } - - function test_reseal_committees_happy_path() external { - uint256 quorum; - uint256 support; - bool isExecuted; - - address[] memory members; - - address sealable = address(_lido.withdrawalQueue); - - vm.prank(DAO_AGENT); - _lido.withdrawalQueue.grantRole( - 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(this) - ); - - // Reseal - members = _resealCommittee.getMembers(); - for (uint256 i = 0; i < _resealCommittee.getQuorum() - 1; i++) { - vm.prank(members[i]); - _resealCommittee.voteReseal(sealable, true); - (support, quorum,) = _resealCommittee.getResealState(sealable); - assert(support < quorum); - } - - vm.prank(members[members.length - 1]); - _resealCommittee.voteReseal(sealable, true); - (support, quorum,) = _resealCommittee.getResealState(sealable); - assert(support == quorum); - - _assertNormalState(); - - vm.expectRevert(abi.encodeWithSelector(DualGovernance.ResealIsNotAllowedInNormalState.selector)); - _resealCommittee.executeReseal(sealable); - - _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT()); - _lockStETH(_VETOER, 1 gwei); - _assertVetoSignalingState(); - - assertEq(_lido.withdrawalQueue.isPaused(), false); - vm.expectRevert(abi.encodeWithSelector(ResealManager.SealableWrongPauseState.selector)); - _resealCommittee.executeReseal(sealable); - - _lido.withdrawalQueue.pauseFor(3600 * 24 * 6); - assertEq(_lido.withdrawalQueue.isPaused(), true); - - _resealCommittee.executeReseal(sealable); - } -} diff --git a/test/unit/committees/EmergencyActivationCommittee.t.sol b/test/unit/committees/EmergencyActivationCommittee.t.sol deleted file mode 100644 index 31c9053a..00000000 --- a/test/unit/committees/EmergencyActivationCommittee.t.sol +++ /dev/null @@ -1,134 +0,0 @@ -// 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 {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; - -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"); - 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 testFuzz_constructor_HappyPath( - address _owner, - uint256 _quorum, - address _emergencyProtectedTimelock - ) external { - vm.assume(_owner != address(0)); - vm.assume(_quorum > 0 && _quorum <= committeeMembers.length); - 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(IEmergencyProtectedTimelock.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.HashIsNotScheduled.selector, _EMERGENCY_ACTIVATION_HASH)); - 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(IEmergencyProtectedTimelock.activateEmergencyMode.selector) - ); - emergencyActivationCommittee.executeActivateEmergencyMode(); - - (support, executionQuorum, quorumAt, isExecuted) = emergencyActivationCommittee.getActivateEmergencyModeState(); - assertEq(support, 2); - assertEq(executionQuorum, 2); - assertEq(quorumAt, quorumAtExpected); - assertEq(isExecuted, true); - } -} diff --git a/test/unit/committees/EmergencyExecutionCommittee.t.sol b/test/unit/committees/EmergencyExecutionCommittee.t.sol deleted file mode 100644 index 4b0325c0..00000000 --- a/test/unit/committees/EmergencyExecutionCommittee.t.sol +++ /dev/null @@ -1,240 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -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"; -import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.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; - 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 EmergencyProtectedTimelockMock()); - EmergencyProtectedTimelockMock(payable(emergencyProtectedTimelock)).setProposalsCount(1); - emergencyExecutionCommittee = - new EmergencyExecutionCommittee(owner, committeeMembers, quorum, emergencyProtectedTimelock); - } - - function testFuzz_constructor_HappyPath( - address _owner, - uint256 _quorum, - address _emergencyProtectedTimelock - ) external { - vm.assume(_owner != address(0)); - vm.assume(_quorum > 0 && _quorum <= committeeMembers.length); - 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 test_voteEmergencyExecute_RevertOn_ProposalIdExceedsProposalsCount() external { - uint256 nonExistentProposalId = proposalId + 1; - - vm.expectRevert( - abi.encodeWithSelector(EmergencyExecutionCommittee.ProposalDoesNotExist.selector, nonExistentProposalId) - ); - vm.prank(committeeMembers[0]); - 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); - 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(IEmergencyProtectedTimelock.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.HashIsNotScheduled.selector, - keccak256(abi.encode(ProposalType.EmergencyExecute, proposalId)) - ) - ); - 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(IEmergencyProtectedTimelock.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.HashIsNotScheduled.selector, - keccak256(abi.encode(ProposalType.EmergencyReset, bytes32(0))) - ) - ); - 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); - } -} diff --git a/test/unit/committees/ResealCommittee.t.sol b/test/unit/committees/ResealCommittee.t.sol deleted file mode 100644 index fc82a968..00000000 --- a/test/unit/committees/ResealCommittee.t.sol +++ /dev/null @@ -1,153 +0,0 @@ -// 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, Timestamps} 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 test_voteReseal_RevertOn_ZeroAddress() external { - vm.prank(committeeMembers[0]); - vm.expectRevert(abi.encodeWithSelector(ResealCommittee.InvalidSealable.selector, address(0))); - resealCommittee.voteReseal(address(0), true); - } - - 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.HashIsNotScheduled.selector, keccak256(abi.encode(sealable, /* resealNonce */ 0)) - ) - ); - resealCommittee.executeReseal(sealable); - } - - 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, 3); - assertEq(quorumAt, Timestamps.ZERO); - - vm.prank(committeeMembers[0]); - resealCommittee.voteReseal(sealable, true); - - (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); - assertEq(support, 1); - 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); - - _wait(Durations.from(1)); - - vm.prank(committeeMembers[1]); - resealCommittee.voteReseal(sealable, false); - - (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); - assertEq(support, 1); - 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, 3); - assertEq(quorumAt, Timestamp.wrap(0)); - } -} diff --git a/test/utils/SetupDeployment.sol b/test/utils/SetupDeployment.sol index 14ccac34..8dee3b28 100644 --- a/test/utils/SetupDeployment.sol +++ b/test/utils/SetupDeployment.sol @@ -24,9 +24,6 @@ import {TargetMock} from "./target-mock.sol"; import {Executor} from "contracts/Executor.sol"; import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; -import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; -import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; - import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; import {ResealManager} from "contracts/ResealManager.sol"; @@ -36,7 +33,6 @@ import { ImmutableDualGovernanceConfigProvider } from "contracts/ImmutableDualGovernanceConfigProvider.sol"; -import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; // --- @@ -110,8 +106,8 @@ abstract contract SetupDeployment is Test { Executor internal _adminExecutor; EmergencyProtectedTimelock internal _timelock; TimelockedGovernance internal _emergencyGovernance; - EmergencyActivationCommittee internal _emergencyActivationCommittee; - EmergencyExecutionCommittee internal _emergencyExecutionCommittee; + address internal _emergencyActivationCommittee; + address internal _emergencyExecutionCommittee; // --- // Dual Governance Contracts @@ -120,7 +116,7 @@ abstract contract SetupDeployment is Test { DualGovernance internal _dualGovernance; ImmutableDualGovernanceConfigProvider internal _dualGovernanceConfigProvider; - ResealCommittee internal _resealCommittee; + address internal _resealCommittee; TiebreakerCoreCommittee internal _tiebreakerCoreCommittee; TiebreakerSubCommittee[] internal _tiebreakerSubCommittees; @@ -144,6 +140,10 @@ abstract contract SetupDeployment is Test { _random = random; _targetMock = new TargetMock(); + _emergencyActivationCommittee = makeAddr("EMERGENCY_ACTIVATION_COMMITTEE"); + _emergencyExecutionCommittee = makeAddr("EMERGENCY_EXECUTION_COMMITTEE"); + _resealCommittee = makeAddr("RESEAL_COMMITTEE"); + dgDeployConfig.AFTER_SUBMIT_DELAY = _AFTER_SUBMIT_DELAY; dgDeployConfig.MAX_AFTER_SUBMIT_DELAY = _MAX_AFTER_SUBMIT_DELAY; dgDeployConfig.AFTER_SCHEDULE_DELAY = _AFTER_SCHEDULE_DELAY; @@ -153,8 +153,8 @@ abstract contract SetupDeployment is Test { dgDeployConfig.EMERGENCY_PROTECTION_DURATION = _EMERGENCY_PROTECTION_DURATION; dgDeployConfig.MAX_EMERGENCY_PROTECTION_DURATION = _MAX_EMERGENCY_PROTECTION_DURATION; - dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE = address(0); - dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE = address(0); + dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE = _emergencyActivationCommittee; + dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE = _emergencyExecutionCommittee; dgDeployConfig.TIEBREAKER_CORE_QUORUM = TIEBREAKER_SUB_COMMITTEES_COUNT; dgDeployConfig.TIEBREAKER_EXECUTION_DELAY = TIEBREAKER_EXECUTION_DELAY; @@ -166,7 +166,7 @@ abstract contract SetupDeployment is Test { dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS = [TIEBREAKER_SUB_COMMITTEE_QUORUM, TIEBREAKER_SUB_COMMITTEE_QUORUM]; - dgDeployConfig.RESEAL_COMMITTEE = address(0); + dgDeployConfig.RESEAL_COMMITTEE = _resealCommittee; dgDeployConfig.MIN_WITHDRAWALS_BATCH_SIZE = 4; dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT = MIN_TIEBREAKER_ACTIVATION_TIMEOUT; @@ -229,9 +229,6 @@ abstract contract SetupDeployment is Test { _tiebreakerCoreCommittee.transferOwnership(address(_adminExecutor)); - _resealCommittee = _deployResealCommittee(); - dgDeployConfig.RESEAL_COMMITTEE = address(_resealCommittee); - // --- // Finalize Setup // --- @@ -264,21 +261,7 @@ abstract contract SetupDeployment is Test { _timelock = contracts.timelock; if (isEmergencyProtectionEnabled) { - _emergencyActivationCommittee = _deployEmergencyActivationCommittee({ - quorum: _EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, - members: _generateRandomAddresses(_EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS_COUNT), - owner: address(_adminExecutor), - timelock: _timelock - }); - - _emergencyExecutionCommittee = _deployEmergencyExecutionCommittee({ - quorum: _EMERGENCY_EXECUTION_COMMITTEE_QUORUM, - members: _generateRandomAddresses(_EMERGENCY_EXECUTION_COMMITTEE_MEMBERS_COUNT), - owner: address(_adminExecutor), - timelock: _timelock - }); - dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE = address(_emergencyActivationCommittee); - dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE = address(_emergencyExecutionCommittee); + _emergencyGovernance = _deployTimelockedGovernance({governance: address(_lido.voting), timelock: _timelock}); DGContractsDeployment.deployEmergencyProtectedTimelockContracts(lidoAddresses, dgDeployConfig, contracts); @@ -298,36 +281,16 @@ abstract contract SetupDeployment is Test { return DGContractsDeployment.deployDualGovernanceConfigProvider(dgDeployConfig); } - function _deployEmergencyActivationCommittee( - EmergencyProtectedTimelock timelock, - address owner, - uint256 quorum, - address[] memory members - ) internal returns (EmergencyActivationCommittee) { - return new EmergencyActivationCommittee(owner, members, quorum, address(timelock)); + function _deployTimelockedGovernance( + address governance, + ITimelock timelock + ) internal returns (TimelockedGovernance) { + return new TimelockedGovernance(governance, timelock); } - function _deployEmergencyExecutionCommittee( - EmergencyProtectedTimelock timelock, - address owner, - uint256 quorum, - address[] memory members - ) internal returns (EmergencyExecutionCommittee) { - return new EmergencyExecutionCommittee(owner, members, quorum, address(timelock)); - } - - function _deployResealCommittee() internal returns (ResealCommittee) { - uint256 quorum = 3; - uint256 membersCount = 5; - address[] memory committeeMembers = new address[](membersCount); - for (uint256 i = 0; i < membersCount; ++i) { - committeeMembers[i] = makeAddr(string(abi.encode(0xFA + i * membersCount + 65))); - } - - return new ResealCommittee( - address(_adminExecutor), committeeMembers, quorum, address(_dualGovernance), Durations.from(0) - ); - } + // --- + // Dual Governance Deployment + // --- function _deployResealManager(ITimelock timelock) internal returns (ResealManager) { return DGContractsDeployment.deployResealManager(timelock); diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 59c713a9..305572ae 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -549,30 +549,18 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { } function _executeActivateEmergencyMode() internal { - address[] memory members = _emergencyActivationCommittee.getMembers(); - for (uint256 i = 0; i < _emergencyActivationCommittee.getQuorum(); ++i) { - vm.prank(members[i]); - _emergencyActivationCommittee.approveActivateEmergencyMode(); - } - _emergencyActivationCommittee.executeActivateEmergencyMode(); + vm.prank(_emergencyActivationCommittee); + _timelock.activateEmergencyMode(); } function _executeEmergencyExecute(uint256 proposalId) internal { - address[] memory members = _emergencyExecutionCommittee.getMembers(); - for (uint256 i = 0; i < _emergencyExecutionCommittee.getQuorum(); ++i) { - vm.prank(members[i]); - _emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); - } - _emergencyExecutionCommittee.executeEmergencyExecute(proposalId); + vm.prank(_emergencyExecutionCommittee); + _timelock.emergencyExecute(proposalId); } function _executeEmergencyReset() internal { - address[] memory members = _emergencyExecutionCommittee.getMembers(); - for (uint256 i = 0; i < _emergencyExecutionCommittee.getQuorum(); ++i) { - vm.prank(members[i]); - _emergencyExecutionCommittee.approveEmergencyReset(); - } - _emergencyExecutionCommittee.executeEmergencyReset(); + vm.prank(_emergencyExecutionCommittee); + _timelock.emergencyReset(); } struct DurationStruct { From fada19fe8d2860392aa338bb1fcc36cbab9f6358 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Mon, 14 Oct 2024 17:17:27 +0300 Subject: [PATCH 036/107] change reseal manager variable in dual governance --- contracts/DualGovernance.sol | 28 ++++++-- scripts/deploy/DeployVerification.sol | 7 +- test/unit/DualGovernance.t.sol | 98 +++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 10 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index de4c69ab..dc6c169f 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -45,6 +45,7 @@ contract DualGovernance is IDualGovernance { error ResealIsNotAllowedInNormalState(); error InvalidResealCommittee(address resealCommittee); error InvalidTiebreakerActivationTimeoutBounds(); + error InvalidResealManager(address resealManager); // --- // Events @@ -54,6 +55,7 @@ contract DualGovernance is IDualGovernance { event CancelAllPendingProposalsExecuted(); event EscrowMasterCopyDeployed(IEscrow escrowMasterCopy); event ResealCommitteeSet(address resealCommittee); + event ResealManagerSet(address resealManager); // --- // Sanity Check Parameters & Immutables @@ -112,9 +114,6 @@ contract DualGovernance is IDualGovernance { /// @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; @@ -140,6 +139,9 @@ contract DualGovernance is IDualGovernance { /// period of time when the Dual Governance proposal adoption is blocked. address internal _resealCommittee; + /// @dev The address of the Reseal Manager. + IResealManager internal _resealManager; + // --- // Constructor // --- @@ -150,7 +152,6 @@ contract DualGovernance is IDualGovernance { } TIMELOCK = dependencies.timelock; - RESEAL_MANAGER = dependencies.resealManager; MIN_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.minTiebreakerActivationTimeout; MAX_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.maxTiebreakerActivationTimeout; @@ -163,10 +164,12 @@ contract DualGovernance is IDualGovernance { withdrawalQueue: dependencies.withdrawalQueue, minWithdrawalsBatchSize: sanityCheckParams.minWithdrawalsBatchSize }); - emit EscrowMasterCopyDeployed(ESCROW_MASTER_COPY); _stateMachine.initialize(dependencies.configProvider, ESCROW_MASTER_COPY); + + _resealManager = dependencies.resealManager; + emit ResealManagerSet(address(_resealManager)); } // --- @@ -450,7 +453,7 @@ contract DualGovernance is IDualGovernance { _tiebreaker.checkCallerIsTiebreakerCommittee(); _stateMachine.activateNextState(ESCROW_MASTER_COPY); _tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.normalOrVetoCooldownExitedAt); - RESEAL_MANAGER.resume(sealable); + _resealManager.resume(sealable); } /// @notice Allows the tiebreaker committee to schedule for execution a submitted proposal when @@ -490,7 +493,7 @@ contract DualGovernance is IDualGovernance { if (_stateMachine.getPersistedState() == State.Normal) { revert ResealIsNotAllowedInNormalState(); } - RESEAL_MANAGER.reseal(sealable); + _resealManager.reseal(sealable); } /// @notice Sets the address of the reseal committee. @@ -506,6 +509,17 @@ contract DualGovernance is IDualGovernance { emit ResealCommitteeSet(resealCommittee); } + /// @notice Sets the address of the Reseal Manager. + /// @param resealManager The address of the new Reseal Manager. + function setResealManager(address resealManager) external { + _checkCallerIsAdminExecutor(); + if (resealManager == address(0) || resealManager == address(_resealManager)) { + revert InvalidResealManager(resealManager); + } + _resealManager = IResealManager(resealManager); + emit ResealManagerSet(resealManager); + } + // --- // Internal methods // --- diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index 06f82495..6408821c 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -162,9 +162,10 @@ library DeployVerification { ) internal view { DualGovernance dg = DualGovernance(res.dualGovernance); require(address(dg.TIMELOCK()) == res.timelock, "Incorrect address for timelock in DualGovernance"); - require( - address(dg.RESEAL_MANAGER()) == res.resealManager, "Incorrect address for resealManager in DualGovernance" - ); + // TODO: uncomment this check when getter is added + // require( + // address(dg.getResealManager()) == res.resealManager, "Incorrect address for resealManager in DualGovernance" + // ); require( dg.MIN_TIEBREAKER_ACTIVATION_TIMEOUT() == dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT, "Incorrect parameter MIN_TIEBREAKER_ACTIVATION_TIMEOUT" diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 7dd6a664..105cbf0c 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -26,6 +26,7 @@ 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 {IEscrow} from "contracts/interfaces/IEscrow.sol"; import {UnitTest} from "test/utils/unit-test.sol"; import {StETHMock} from "test/mocks/StETHMock.sol"; @@ -124,6 +125,50 @@ contract DualGovernanceUnitTests is UnitTest { new DualGovernance({dependencies: _externalDependencies, sanityCheckParams: _sanityCheckParams}); } + // --- + // constructor() + // --- + + function test_constructor_HappyPath() external { + address testDeployerAddress = address(this); + uint256 testDeployerNonce = vm.getNonce(testDeployerAddress); + address predictedDualGovernanceAddress = computeAddress(testDeployerAddress, testDeployerNonce); + + address predictedEscrowCopyAddress = computeAddress(predictedDualGovernanceAddress, 1); + + vm.expectEmit(); + emit DualGovernance.EscrowMasterCopyDeployed(IEscrow(predictedEscrowCopyAddress)); + vm.expectEmit(); + emit DualGovernance.ResealManagerSet(address(_RESEAL_MANAGER_STUB)); + + Duration minTiebreakerActivationTimeout = Durations.from(30 days); + Duration maxTiebreakerActivationTimeout = Durations.from(180 days); + uint256 maxSealableWithdrawalBlockersCount = 128; + + DualGovernance dualGovernanceLocal = 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: minTiebreakerActivationTimeout, + maxTiebreakerActivationTimeout: maxTiebreakerActivationTimeout, + maxSealableWithdrawalBlockersCount: maxSealableWithdrawalBlockersCount + }) + }); + + assertEq(address(dualGovernanceLocal.TIMELOCK()), address(_timelock)); + assertEq(dualGovernanceLocal.MIN_TIEBREAKER_ACTIVATION_TIMEOUT(), minTiebreakerActivationTimeout); + assertEq(dualGovernanceLocal.MAX_TIEBREAKER_ACTIVATION_TIMEOUT(), maxTiebreakerActivationTimeout); + assertEq(dualGovernanceLocal.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT(), maxSealableWithdrawalBlockersCount); + assertEq(address(dualGovernanceLocal.ESCROW_MASTER_COPY()), predictedEscrowCopyAddress); + } + // --- // submitProposal() // --- @@ -2111,6 +2156,39 @@ contract DualGovernanceUnitTests is UnitTest { ); } + // --- + // setResealManager() + // --- + + function testFuzz_setResealManager_HappyPath(address newResealManager) external { + vm.assume(newResealManager != address(0) && newResealManager != address(_RESEAL_MANAGER_STUB)); + + vm.expectEmit(); + emit DualGovernance.ResealManagerSet(newResealManager); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealManager.selector, newResealManager) + ); + } + + function test_setResealManager_RevertOn_InvalidAddress() external { + vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidResealManager.selector, address(0))); + _executor.execute( + address(_dualGovernance), 0, abi.encodeWithSelector(DualGovernance.setResealManager.selector, address(0)) + ); + + vm.expectRevert( + abi.encodeWithSelector(DualGovernance.InvalidResealManager.selector, address(_RESEAL_MANAGER_STUB)) + ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealManager.selector, address(_RESEAL_MANAGER_STUB)) + ); + } + // --- // Helper methods // --- @@ -2124,4 +2202,24 @@ contract DualGovernanceUnitTests is UnitTest { calls = new ExternalCall[](1); calls[0] = ExternalCall({target: address(0x123), value: 0, payload: abi.encodeWithSignature("someFunction()")}); } + + function computeAddress(address deployer, uint256 nonce) public pure returns (address) { + bytes memory data; + + if (nonce == 0x00) { + data = abi.encodePacked(hex"94", deployer, hex"80"); + } else if (nonce <= 0x7f) { + data = abi.encodePacked(hex"d6", hex"94", deployer, uint8(nonce)); + } else if (nonce <= 0xff) { + data = abi.encodePacked(hex"d7", hex"94", deployer, hex"81", uint8(nonce)); + } else if (nonce <= 0xffff) { + data = abi.encodePacked(hex"d8", hex"94", deployer, hex"82", uint16(nonce)); + } else if (nonce <= 0xffffff) { + data = abi.encodePacked(hex"d9", hex"94", deployer, hex"83", uint24(nonce)); + } else { + data = abi.encodePacked(hex"da", hex"94", deployer, hex"84", uint32(nonce)); + } + + return address(uint160(uint256(keccak256(data)))); + } } From 5f40d3a6cf92d73665aa10568b70dbffda344f98 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Mon, 14 Oct 2024 17:17:42 +0300 Subject: [PATCH 037/107] change admin executor variable in timelock --- contracts/EmergencyProtectedTimelock.sol | 45 ++++++++++++++++------ test/unit/EmergencyProtectedTimelock.t.sol | 25 ++++++++++++ 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 29671dc0..c4c8e229 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -21,11 +21,18 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { using ExecutableProposals for ExecutableProposals.Context; using EmergencyProtection for EmergencyProtection.Context; + // --- + // Events + // --- + + event AdminExecutorSet(address newAdminExecutor); + // --- // Errors // --- error CallerIsNotAdminExecutor(address value); + error InvalidAdminExecutor(address adminExecutor); // --- // Sanity Check Parameters & Immutables @@ -55,13 +62,6 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @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 // --- @@ -75,17 +75,25 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @dev The functionality for managing the emergency protection mechanism. EmergencyProtection.Context internal _emergencyProtection; + // --- + // Admin Executor + // --- + + /// @dev The address of the admin executor, authorized to manage the EmergencyProtectedTimelock instance. + address private _adminExecutor; + // --- // Constructor // --- constructor(SanityCheckParams memory sanityCheckParams, address adminExecutor) { - _ADMIN_EXECUTOR = adminExecutor; - MAX_AFTER_SUBMIT_DELAY = sanityCheckParams.maxAfterSubmitDelay; MAX_AFTER_SCHEDULE_DELAY = sanityCheckParams.maxAfterScheduleDelay; MAX_EMERGENCY_MODE_DURATION = sanityCheckParams.maxEmergencyModeDuration; MAX_EMERGENCY_PROTECTION_DURATION = sanityCheckParams.maxEmergencyProtectionDuration; + + _adminExecutor = adminExecutor; + emit AdminExecutorSet(adminExecutor); } // --- @@ -281,7 +289,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @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; + return _adminExecutor; } /// @notice Returns the configured delay duration required before a submitted proposal can be scheduled. @@ -356,12 +364,27 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { return _proposals.canSchedule(proposalId, _timelockState.getAfterSubmitDelay()); } + // --- + // Admin Executor Methods + // --- + + /// @notice Sets the address of the admin executor. + /// @param newAdminExecutor The address of the new admin executor. + function setAdminExecutor(address newAdminExecutor) external { + _checkCallerIsAdminExecutor(); + if (newAdminExecutor == address(0) || newAdminExecutor == _adminExecutor) { + revert InvalidAdminExecutor(newAdminExecutor); + } + _adminExecutor = newAdminExecutor; + emit AdminExecutorSet(newAdminExecutor); + } + // --- // Internal Methods // --- function _checkCallerIsAdminExecutor() internal view { - if (msg.sender != _ADMIN_EXECUTOR) { + if (msg.sender != _adminExecutor) { revert CallerIsNotAdminExecutor(msg.sender); } } diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 6340fe42..97baf507 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -63,6 +63,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { EmergencyProtectedTimelock.SanityCheckParams memory sanityCheckParams, address adminExecutor ) external { + vm.expectEmit(); + emit EmergencyProtectedTimelock.AdminExecutorSet(adminExecutor); EmergencyProtectedTimelock timelock = new EmergencyProtectedTimelock(sanityCheckParams, adminExecutor); assertEq(timelock.getAdminExecutor(), adminExecutor); @@ -1002,6 +1004,29 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(timelock.getAdminExecutor(), executor); } + function testFuzz_setAdminExecutor_HappyPath(address adminExecutor) external { + vm.assume(adminExecutor != _adminExecutor && adminExecutor != address(0)); + + vm.expectEmit(); + emit EmergencyProtectedTimelock.AdminExecutorSet(adminExecutor); + vm.prank(_adminExecutor); + _timelock.setAdminExecutor(adminExecutor); + + assertEq(_timelock.getAdminExecutor(), adminExecutor); + } + + function test_setAdminExecutor_RevertOn_InvalidAddress() external { + vm.startPrank(_adminExecutor); + + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidAdminExecutor.selector, address(0))); + _timelock.setAdminExecutor(address(0)); + + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidAdminExecutor.selector, _adminExecutor) + ); + _timelock.setAdminExecutor(_adminExecutor); + } + // Utils function _submitProposal() internal { From adb4b2eb1e99b2bd354fd792ea6cf0687e48a3da Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 15 Oct 2024 16:17:16 +0300 Subject: [PATCH 038/107] review fixes --- contracts/DualGovernance.sol | 26 +++++---- contracts/EmergencyProtectedTimelock.sol | 27 ++------- contracts/libraries/TimelockState.sol | 16 +++++ test/unit/DualGovernance.t.sol | 68 ++++++++++++++-------- test/unit/EmergencyProtectedTimelock.t.sol | 21 ++----- test/unit/libraries/TimelockState.t.sol | 21 +++++++ test/utils/addresses.sol | 22 +++++++ 7 files changed, 129 insertions(+), 72 deletions(-) create mode 100644 test/utils/addresses.sol diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index dc6c169f..761145a1 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -167,9 +167,7 @@ contract DualGovernance is IDualGovernance { emit EscrowMasterCopyDeployed(ESCROW_MASTER_COPY); _stateMachine.initialize(dependencies.configProvider, ESCROW_MASTER_COPY); - - _resealManager = dependencies.resealManager; - emit ResealManagerSet(address(_resealManager)); + _setResealManager(address(dependencies.resealManager)); } // --- @@ -500,12 +498,10 @@ contract DualGovernance is IDualGovernance { /// @param resealCommittee The address of the new reseal committee. function setResealCommittee(address resealCommittee) external { _checkCallerIsAdminExecutor(); - if (resealCommittee == _resealCommittee) { revert InvalidResealCommittee(resealCommittee); } _resealCommittee = resealCommittee; - emit ResealCommitteeSet(resealCommittee); } @@ -513,17 +509,27 @@ contract DualGovernance is IDualGovernance { /// @param resealManager The address of the new Reseal Manager. function setResealManager(address resealManager) external { _checkCallerIsAdminExecutor(); - if (resealManager == address(0) || resealManager == address(_resealManager)) { - revert InvalidResealManager(resealManager); - } - _resealManager = IResealManager(resealManager); - emit ResealManagerSet(resealManager); + _setResealManager(resealManager); + } + + /// @notice Gets the address of the Reseal Manager. + /// @return resealManager The address of the Reseal Manager. + function getResealManager() external view returns (IResealManager) { + return _resealManager; } // --- // Internal methods // --- + function _setResealManager(address resealManager) internal { + if (resealManager == address(_resealManager) || resealManager == address(0)) { + revert InvalidResealManager(resealManager); + } + _resealManager = IResealManager(resealManager); + emit ResealManagerSet(resealManager); + } + function _checkCallerIsAdminExecutor() internal view { if (TIMELOCK.getAdminExecutor() != msg.sender) { revert CallerIsNotAdminExecutor(msg.sender); diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index c4c8e229..0eb07302 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -21,18 +21,11 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { using ExecutableProposals for ExecutableProposals.Context; using EmergencyProtection for EmergencyProtection.Context; - // --- - // Events - // --- - - event AdminExecutorSet(address newAdminExecutor); - // --- // Errors // --- error CallerIsNotAdminExecutor(address value); - error InvalidAdminExecutor(address adminExecutor); // --- // Sanity Check Parameters & Immutables @@ -75,13 +68,6 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @dev The functionality for managing the emergency protection mechanism. EmergencyProtection.Context internal _emergencyProtection; - // --- - // Admin Executor - // --- - - /// @dev The address of the admin executor, authorized to manage the EmergencyProtectedTimelock instance. - address private _adminExecutor; - // --- // Constructor // --- @@ -92,8 +78,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { MAX_EMERGENCY_MODE_DURATION = sanityCheckParams.maxEmergencyModeDuration; MAX_EMERGENCY_PROTECTION_DURATION = sanityCheckParams.maxEmergencyProtectionDuration; - _adminExecutor = adminExecutor; - emit AdminExecutorSet(adminExecutor); + _timelockState.setAdminExecutor(adminExecutor); } // --- @@ -289,7 +274,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @notice Returns the address of the admin executor. /// @return adminExecutor The address of the admin executor. function getAdminExecutor() external view returns (address) { - return _adminExecutor; + return _timelockState.adminExecutor; } /// @notice Returns the configured delay duration required before a submitted proposal can be scheduled. @@ -372,11 +357,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @param newAdminExecutor The address of the new admin executor. function setAdminExecutor(address newAdminExecutor) external { _checkCallerIsAdminExecutor(); - if (newAdminExecutor == address(0) || newAdminExecutor == _adminExecutor) { - revert InvalidAdminExecutor(newAdminExecutor); - } - _adminExecutor = newAdminExecutor; - emit AdminExecutorSet(newAdminExecutor); + _timelockState.setAdminExecutor(newAdminExecutor); } // --- @@ -384,7 +365,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { // --- function _checkCallerIsAdminExecutor() internal view { - if (msg.sender != _adminExecutor) { + if (msg.sender != _timelockState.adminExecutor) { revert CallerIsNotAdminExecutor(msg.sender); } } diff --git a/contracts/libraries/TimelockState.sol b/contracts/libraries/TimelockState.sol index 2ac74d7c..e5f802e9 100644 --- a/contracts/libraries/TimelockState.sol +++ b/contracts/libraries/TimelockState.sol @@ -14,6 +14,7 @@ library TimelockState { error InvalidGovernance(address value); error InvalidAfterSubmitDelay(Duration value); error InvalidAfterScheduleDelay(Duration value); + error InvalidAdminExecutor(address adminExecutor); // --- // Events @@ -22,6 +23,7 @@ library TimelockState { event GovernanceSet(address newGovernance); event AfterSubmitDelaySet(Duration newAfterSubmitDelay); event AfterScheduleDelaySet(Duration newAfterScheduleDelay); + event AdminExecutorSet(address newAdminExecutor); // --- // Data Types @@ -34,6 +36,8 @@ library TimelockState { Duration afterSubmitDelay; /// @dev slot0 [192..224] Duration afterScheduleDelay; + /// @dev slot1 [0..159] + address adminExecutor; } // --- @@ -84,6 +88,18 @@ library TimelockState { self.afterScheduleDelay = newAfterScheduleDelay; emit AfterScheduleDelaySet(newAfterScheduleDelay); } + /// @notice Sets the admin executor address. + /// @dev Reverts if the new admin executor address is zero or the same as the current one. + /// @param self The context of the timelock state. + /// @param newAdminExecutor The new admin executor address. + + function setAdminExecutor(Context storage self, address newAdminExecutor) internal { + if (newAdminExecutor == address(0) || newAdminExecutor == self.adminExecutor) { + revert InvalidAdminExecutor(newAdminExecutor); + } + self.adminExecutor = newAdminExecutor; + emit AdminExecutorSet(newAdminExecutor); + } /// @notice Retrieves the delay period required after a proposal is submitted before it can be scheduled. /// @param self The context of the Timelock State library. diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 105cbf0c..6bf355b1 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -33,6 +33,7 @@ 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"; +import {computeAddress} from "test/utils/addresses.sol"; contract DualGovernanceUnitTests is UnitTest { Executor private _executor = new Executor(address(this)); @@ -2156,6 +2157,21 @@ contract DualGovernanceUnitTests is UnitTest { ); } + function testFuzz_setResealCommittee_RevertOn_InvalidResealCommittee(address newResealCommittee) external { + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, newResealCommittee) + ); + + vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidResealCommittee.selector, newResealCommittee)); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, newResealCommittee) + ); + } + // --- // setResealManager() // --- @@ -2174,19 +2190,43 @@ contract DualGovernanceUnitTests is UnitTest { } function test_setResealManager_RevertOn_InvalidAddress() external { + vm.expectRevert( + abi.encodeWithSelector(DualGovernance.InvalidResealManager.selector, address(_RESEAL_MANAGER_STUB)) + ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealManager.selector, address(_RESEAL_MANAGER_STUB)) + ); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidResealManager.selector, address(0))); _executor.execute( address(_dualGovernance), 0, abi.encodeWithSelector(DualGovernance.setResealManager.selector, address(0)) ); + } + + function test_setResealManger_RevertOn_CallerIsNotAdminExecutor(address stranger) external { + vm.assume(stranger != address(_executor)); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, stranger)); + _dualGovernance.setResealManager(address(0x123)); + } + + // --- + // getResealManager() + // --- + + function testFuzz_getResealManager_HappyPath(address newResealManager) external { + vm.assume(newResealManager != address(_RESEAL_MANAGER_STUB)); - vm.expectRevert( - abi.encodeWithSelector(DualGovernance.InvalidResealManager.selector, address(_RESEAL_MANAGER_STUB)) - ); _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.setResealManager.selector, address(_RESEAL_MANAGER_STUB)) + abi.encodeWithSelector(DualGovernance.setResealManager.selector, newResealManager) ); + + assertEq(newResealManager, address(_dualGovernance.getResealManager())); } // --- @@ -2202,24 +2242,4 @@ contract DualGovernanceUnitTests is UnitTest { calls = new ExternalCall[](1); calls[0] = ExternalCall({target: address(0x123), value: 0, payload: abi.encodeWithSignature("someFunction()")}); } - - function computeAddress(address deployer, uint256 nonce) public pure returns (address) { - bytes memory data; - - if (nonce == 0x00) { - data = abi.encodePacked(hex"94", deployer, hex"80"); - } else if (nonce <= 0x7f) { - data = abi.encodePacked(hex"d6", hex"94", deployer, uint8(nonce)); - } else if (nonce <= 0xff) { - data = abi.encodePacked(hex"d7", hex"94", deployer, hex"81", uint8(nonce)); - } else if (nonce <= 0xffff) { - data = abi.encodePacked(hex"d8", hex"94", deployer, hex"82", uint16(nonce)); - } else if (nonce <= 0xffffff) { - data = abi.encodePacked(hex"d9", hex"94", deployer, hex"83", uint24(nonce)); - } else { - data = abi.encodePacked(hex"da", hex"94", deployer, hex"84", uint32(nonce)); - } - - return address(uint160(uint256(keccak256(data)))); - } } diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 97baf507..59783c74 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -63,12 +63,10 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { EmergencyProtectedTimelock.SanityCheckParams memory sanityCheckParams, address adminExecutor ) external { - vm.expectEmit(); - emit EmergencyProtectedTimelock.AdminExecutorSet(adminExecutor); + vm.assume(adminExecutor != address(0)); EmergencyProtectedTimelock timelock = new EmergencyProtectedTimelock(sanityCheckParams, adminExecutor); assertEq(timelock.getAdminExecutor(), adminExecutor); - assertEq(timelock.MAX_AFTER_SUBMIT_DELAY(), sanityCheckParams.maxAfterSubmitDelay); assertEq(timelock.MAX_AFTER_SCHEDULE_DELAY(), sanityCheckParams.maxAfterScheduleDelay); assertEq(timelock.MAX_EMERGENCY_MODE_DURATION(), sanityCheckParams.maxEmergencyModeDuration); @@ -1006,25 +1004,18 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function testFuzz_setAdminExecutor_HappyPath(address adminExecutor) external { vm.assume(adminExecutor != _adminExecutor && adminExecutor != address(0)); - - vm.expectEmit(); - emit EmergencyProtectedTimelock.AdminExecutorSet(adminExecutor); vm.prank(_adminExecutor); _timelock.setAdminExecutor(adminExecutor); assertEq(_timelock.getAdminExecutor(), adminExecutor); } - function test_setAdminExecutor_RevertOn_InvalidAddress() external { - vm.startPrank(_adminExecutor); - - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidAdminExecutor.selector, address(0))); - _timelock.setAdminExecutor(address(0)); + function test_setAdminExecutor_RevertOn_NotAdminExecutor(address stranger) external { + vm.assume(stranger != _adminExecutor); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidAdminExecutor.selector, _adminExecutor) - ); - _timelock.setAdminExecutor(_adminExecutor); + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + _timelock.setAdminExecutor(address(0x123)); } // Utils diff --git a/test/unit/libraries/TimelockState.t.sol b/test/unit/libraries/TimelockState.t.sol index 68758e86..aa78fc36 100644 --- a/test/unit/libraries/TimelockState.t.sol +++ b/test/unit/libraries/TimelockState.t.sol @@ -12,6 +12,7 @@ contract TimelockStateUnitTests is UnitTest { TimelockState.Context internal _timelockState; address internal governance = makeAddr("governance"); + address internal adminExecutor = makeAddr("adminExecutor"); Duration internal afterSubmitDelay = Durations.from(1 days); Duration internal afterScheduleDelay = Durations.from(2 days); @@ -20,6 +21,7 @@ contract TimelockStateUnitTests is UnitTest { function setUp() external { TimelockState.setGovernance(_timelockState, governance); + TimelockState.setAdminExecutor(_timelockState, adminExecutor); TimelockState.setAfterSubmitDelay(_timelockState, afterSubmitDelay, maxAfterSubmitDelay); TimelockState.setAfterScheduleDelay(_timelockState, afterScheduleDelay, maxAfterScheduleDelay); } @@ -87,6 +89,25 @@ contract TimelockStateUnitTests is UnitTest { TimelockState.setAfterScheduleDelay(_timelockState, afterScheduleDelay, maxAfterScheduleDelay); } + function testFuzz_setAdminExecutor_HappyPath(address newAdminExecutor) external { + vm.assume(newAdminExecutor != address(0) && newAdminExecutor != adminExecutor); + + vm.expectEmit(); + emit TimelockState.AdminExecutorSet(newAdminExecutor); + + TimelockState.setAdminExecutor(_timelockState, newAdminExecutor); + } + + function test_setAdminExecutor_RevertOn_ZeroAddress() external { + vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidAdminExecutor.selector, address(0))); + TimelockState.setAdminExecutor(_timelockState, address(0)); + } + + function test_setAdminExecutor_RevertOn_SameAddress() external { + vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidAdminExecutor.selector, adminExecutor)); + TimelockState.setAdminExecutor(_timelockState, adminExecutor); + } + function testFuzz_getAfterSubmitDelay_HappyPath(Duration newAfterSubmitDelay) external { TimelockState.setAfterSubmitDelay(_timelockState, newAfterSubmitDelay, newAfterSubmitDelay); assertEq(TimelockState.getAfterSubmitDelay(_timelockState), newAfterSubmitDelay); diff --git a/test/utils/addresses.sol b/test/utils/addresses.sol new file mode 100644 index 00000000..7a3c4ba1 --- /dev/null +++ b/test/utils/addresses.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +function computeAddress(address deployer, uint256 nonce) returns (address) { + bytes memory data; + + if (nonce == 0x00) { + data = abi.encodePacked(hex"94", deployer, hex"80"); + } else if (nonce <= 0x7f) { + data = abi.encodePacked(hex"d6", hex"94", deployer, uint8(nonce)); + } else if (nonce <= 0xff) { + data = abi.encodePacked(hex"d7", hex"94", deployer, hex"81", uint8(nonce)); + } else if (nonce <= 0xffff) { + data = abi.encodePacked(hex"d8", hex"94", deployer, hex"82", uint16(nonce)); + } else if (nonce <= 0xffffff) { + data = abi.encodePacked(hex"d9", hex"94", deployer, hex"83", uint24(nonce)); + } else { + data = abi.encodePacked(hex"da", hex"94", deployer, hex"84", uint32(nonce)); + } + + return address(uint160(uint256(keccak256(data)))); +} From c3f2e97e959d8b9795ef4a38bcf284cf878ccefa Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:19:35 +0400 Subject: [PATCH 039/107] Fix build after merge of PR#136 --- scripts/deploy/DeployVerification.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index 6408821c..30abcee6 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -162,10 +162,9 @@ library DeployVerification { ) internal view { DualGovernance dg = DualGovernance(res.dualGovernance); require(address(dg.TIMELOCK()) == res.timelock, "Incorrect address for timelock in DualGovernance"); - // TODO: uncomment this check when getter is added - // require( - // address(dg.getResealManager()) == res.resealManager, "Incorrect address for resealManager in DualGovernance" - // ); + require( + address(dg.getResealManager()) == res.resealManager, "Incorrect address for resealManager in DualGovernance" + ); require( dg.MIN_TIEBREAKER_ACTIVATION_TIMEOUT() == dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT, "Incorrect parameter MIN_TIEBREAKER_ACTIVATION_TIMEOUT" From ec7eaf17e5024efce72d6765607160a097da3575 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Fri, 18 Oct 2024 13:56:57 +0300 Subject: [PATCH 040/107] add draft for resealer library --- contracts/DualGovernance.sol | 40 ++++++-------------------- contracts/libraries/Resealer.sol | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 31 deletions(-) create mode 100644 contracts/libraries/Resealer.sol diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 761145a1..94332515 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -17,6 +17,7 @@ import {IDualGovernanceConfigProvider} from "./interfaces/IDualGovernanceConfigP import {Proposers} from "./libraries/Proposers.sol"; import {Tiebreaker} from "./libraries/Tiebreaker.sol"; import {ExternalCall} from "./libraries/ExternalCalls.sol"; +import {Resealer} from "./libraries/Resealer.sol"; import {State, DualGovernanceStateMachine} from "./libraries/DualGovernanceStateMachine.sol"; import {Escrow} from "./Escrow.sol"; @@ -31,6 +32,7 @@ contract DualGovernance is IDualGovernance { using Proposers for Proposers.Context; using Tiebreaker for Tiebreaker.Context; using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; + using Resealer for Resealer.Context; // --- // Errors @@ -131,16 +133,7 @@ contract DualGovernance is IDualGovernance { /// @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; - - /// @dev The address of the Reseal Manager. - IResealManager internal _resealManager; + Resealer.Context internal _resealer; // --- // Constructor @@ -167,7 +160,7 @@ contract DualGovernance is IDualGovernance { emit EscrowMasterCopyDeployed(ESCROW_MASTER_COPY); _stateMachine.initialize(dependencies.configProvider, ESCROW_MASTER_COPY); - _setResealManager(address(dependencies.resealManager)); + _resealer.setResealManager(address(dependencies.resealManager)); } // --- @@ -451,7 +444,7 @@ contract DualGovernance is IDualGovernance { _tiebreaker.checkCallerIsTiebreakerCommittee(); _stateMachine.activateNextState(ESCROW_MASTER_COPY); _tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.normalOrVetoCooldownExitedAt); - _resealManager.resume(sealable); + _resealer.resume(sealable); } /// @notice Allows the tiebreaker committee to schedule for execution a submitted proposal when @@ -485,51 +478,36 @@ contract DualGovernance is IDualGovernance { /// @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) { - revert CallerIsNotResealCommittee(msg.sender); - } if (_stateMachine.getPersistedState() == State.Normal) { revert ResealIsNotAllowedInNormalState(); } - _resealManager.reseal(sealable); + _resealer.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(); - if (resealCommittee == _resealCommittee) { - revert InvalidResealCommittee(resealCommittee); - } - _resealCommittee = resealCommittee; - emit ResealCommitteeSet(resealCommittee); + _resealer.setResealCommittee(resealCommittee); } /// @notice Sets the address of the Reseal Manager. /// @param resealManager The address of the new Reseal Manager. function setResealManager(address resealManager) external { _checkCallerIsAdminExecutor(); - _setResealManager(resealManager); + _resealer.setResealManager(resealManager); } /// @notice Gets the address of the Reseal Manager. /// @return resealManager The address of the Reseal Manager. function getResealManager() external view returns (IResealManager) { - return _resealManager; + return _resealer.resealManager; } // --- // Internal methods // --- - function _setResealManager(address resealManager) internal { - if (resealManager == address(_resealManager) || resealManager == address(0)) { - revert InvalidResealManager(resealManager); - } - _resealManager = IResealManager(resealManager); - emit ResealManagerSet(resealManager); - } - function _checkCallerIsAdminExecutor() internal view { if (TIMELOCK.getAdminExecutor() != msg.sender) { revert CallerIsNotAdminExecutor(msg.sender); diff --git a/contracts/libraries/Resealer.sol b/contracts/libraries/Resealer.sol new file mode 100644 index 00000000..ec4f1c59 --- /dev/null +++ b/contracts/libraries/Resealer.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IResealManager} from "../interfaces/IResealManager.sol"; + +library Resealer { + error InvalidResealManager(address resealManager); + error InvalidResealCommittee(address resealCommittee); + error CallerIsNotResealCommittee(address caller); + + event ResealCommitteeSet(address resealCommittee); + event ResealManagerSet(address resealManager); + + struct Context { + /// @dev The address of the Reseal Manager. + IResealManager resealManager; + /// @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 resealCommittee; + } + + function setResealManager(Context storage self, address newResealManager) internal { + if (newResealManager == address(self.resealManager) || newResealManager == address(0)) { + revert InvalidResealManager(newResealManager); + } + self.resealManager = IResealManager(newResealManager); + emit ResealManagerSet(newResealManager); + } + + function setResealCommittee(Context storage self, address newResealCommittee) internal { + if (newResealCommittee == self.resealCommittee) { + revert InvalidResealCommittee(newResealCommittee); + } + self.resealCommittee = newResealCommittee; + emit ResealCommitteeSet(newResealCommittee); + } + + function reseal(Context storage self, address sealable) internal { + if (msg.sender != self.resealCommittee) { + revert CallerIsNotResealCommittee(msg.sender); + } + self.resealManager.reseal(sealable); + } + + function resume(Context storage self, address sealable) internal { + self.resealManager.resume(sealable); + } +} From c1a6c67c6544d6b1f6f51b303d21f314dbcc5822 Mon Sep 17 00:00:00 2001 From: Roman Kolpakov Date: Tue, 22 Oct 2024 14:06:57 +0300 Subject: [PATCH 041/107] review fixes --- contracts/DualGovernance.sol | 16 +++-- contracts/libraries/Resealer.sol | 34 +++++++--- test/unit/DualGovernance.t.sol | 79 +++++++--------------- test/unit/EmergencyProtectedTimelock.t.sol | 1 + test/unit/libraries/Resealer.t.sol | 78 +++++++++++++++++++++ 5 files changed, 140 insertions(+), 68 deletions(-) create mode 100644 test/unit/libraries/Resealer.t.sol diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 94332515..daccd6c0 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -45,9 +45,7 @@ contract DualGovernance is IDualGovernance { error ProposalSubmissionBlocked(); error ProposalSchedulingBlocked(uint256 proposalId); error ResealIsNotAllowedInNormalState(); - error InvalidResealCommittee(address resealCommittee); error InvalidTiebreakerActivationTimeoutBounds(); - error InvalidResealManager(address resealManager); // --- // Events @@ -56,8 +54,6 @@ contract DualGovernance is IDualGovernance { event CancelAllPendingProposalsSkipped(); event CancelAllPendingProposalsExecuted(); event EscrowMasterCopyDeployed(IEscrow escrowMasterCopy); - event ResealCommitteeSet(address resealCommittee); - event ResealManagerSet(address resealManager); // --- // Sanity Check Parameters & Immutables @@ -133,6 +129,7 @@ contract DualGovernance is IDualGovernance { /// @dev The state machine implementation controlling the state of the Dual Governance. DualGovernanceStateMachine.Context internal _stateMachine; + /// @dev The functionality for sealing/resuming critical components of Lido protocol. Resealer.Context internal _resealer; // --- @@ -444,7 +441,7 @@ contract DualGovernance is IDualGovernance { _tiebreaker.checkCallerIsTiebreakerCommittee(); _stateMachine.activateNextState(ESCROW_MASTER_COPY); _tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.normalOrVetoCooldownExitedAt); - _resealer.resume(sealable); + _resealer.resealManager.resume(sealable); } /// @notice Allows the tiebreaker committee to schedule for execution a submitted proposal when @@ -481,7 +478,8 @@ contract DualGovernance is IDualGovernance { if (_stateMachine.getPersistedState() == State.Normal) { revert ResealIsNotAllowedInNormalState(); } - _resealer.reseal(sealable); + _resealer.checkCallerIsResealCommittee(); + _resealer.resealManager.reseal(sealable); } /// @notice Sets the address of the reseal committee. @@ -504,6 +502,12 @@ contract DualGovernance is IDualGovernance { return _resealer.resealManager; } + /// @notice Gets the address of the reseal committee. + /// @return resealCommittee The address of the reseal committee. + function getResealCommittee() external view returns (address) { + return _resealer.resealCommittee; + } + // --- // Internal methods // --- diff --git a/contracts/libraries/Resealer.sol b/contracts/libraries/Resealer.sol index ec4f1c59..176a6fb1 100644 --- a/contracts/libraries/Resealer.sol +++ b/contracts/libraries/Resealer.sol @@ -3,22 +3,38 @@ pragma solidity 0.8.26; import {IResealManager} from "../interfaces/IResealManager.sol"; +/// @title Resealer Library +/// @dev Library for managing sealing operations for critical components of Lido protocol. library Resealer { + // --- + // Errors + // --- error InvalidResealManager(address resealManager); error InvalidResealCommittee(address resealCommittee); error CallerIsNotResealCommittee(address caller); + // --- + // Events + // --- event ResealCommitteeSet(address resealCommittee); event ResealManagerSet(address resealManager); + // --- + // Data Types + // --- + + /// @dev Struct to hold the context of the reseal operations. + /// @param resealManager The address of the Reseal Manager. + /// @param resealCommittee 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. struct Context { - /// @dev The address of the Reseal Manager. IResealManager resealManager; - /// @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 resealCommittee; } + /// @dev Sets a new Reseal Manager contract address. + /// @param self The context struct containing the current state. + /// @param newResealManager The address of the new Reseal Manager. function setResealManager(Context storage self, address newResealManager) internal { if (newResealManager == address(self.resealManager) || newResealManager == address(0)) { revert InvalidResealManager(newResealManager); @@ -27,6 +43,9 @@ library Resealer { emit ResealManagerSet(newResealManager); } + /// @dev Sets a new reseal committee address. + /// @param self The context struct containing the current state. + /// @param newResealCommittee The address of the new reseal committee. function setResealCommittee(Context storage self, address newResealCommittee) internal { if (newResealCommittee == self.resealCommittee) { revert InvalidResealCommittee(newResealCommittee); @@ -35,14 +54,11 @@ library Resealer { emit ResealCommitteeSet(newResealCommittee); } - function reseal(Context storage self, address sealable) internal { + /// @dev Checks if the caller is the reseal committee. + /// @param self The context struct containing the current state. + function checkCallerIsResealCommittee(Context storage self) internal view { if (msg.sender != self.resealCommittee) { revert CallerIsNotResealCommittee(msg.sender); } - self.resealManager.reseal(sealable); - } - - function resume(Context storage self, address sealable) internal { - self.resealManager.resume(sealable); } } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 6bf355b1..129539fc 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -11,6 +11,7 @@ import {Escrow} from "contracts/Escrow.sol"; import {Executor} from "contracts/Executor.sol"; import {DualGovernance, State, DualGovernanceStateMachine} from "contracts/DualGovernance.sol"; import {Tiebreaker} from "contracts/libraries/Tiebreaker.sol"; +import {Resealer} from "contracts/libraries/Resealer.sol"; import {Status as ProposalStatus} from "contracts/libraries/ExecutableProposals.sol"; import {Proposers} from "contracts/libraries/Proposers.sol"; import {IResealManager} from "contracts/interfaces/IResealManager.sol"; @@ -39,6 +40,7 @@ contract DualGovernanceUnitTests is UnitTest { Executor private _executor = new Executor(address(this)); address private vetoer = makeAddr("vetoer"); + address private resealCommittee = makeAddr("resealCommittee"); StETHMock private immutable _STETH_MOCK = new StETHMock(); IWithdrawalQueue private immutable _WITHDRAWAL_QUEUE_MOCK = new WithdrawalQueueMock(); @@ -97,6 +99,12 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(this), address(_executor)) ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, resealCommittee) + ); + _escrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); _STETH_MOCK.mint(vetoer, 10 ether); vm.prank(vetoer); @@ -140,7 +148,7 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.EscrowMasterCopyDeployed(IEscrow(predictedEscrowCopyAddress)); vm.expectEmit(); - emit DualGovernance.ResealManagerSet(address(_RESEAL_MANAGER_STUB)); + emit Resealer.ResealManagerSet(address(_RESEAL_MANAGER_STUB)); Duration minTiebreakerActivationTimeout = Durations.from(30 days); Duration maxTiebreakerActivationTimeout = Durations.from(180 days); @@ -2055,15 +2063,8 @@ contract DualGovernanceUnitTests is UnitTest { // --- 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(); @@ -2102,15 +2103,8 @@ contract DualGovernanceUnitTests is UnitTest { } 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.getPersistedState(), State.Normal); vm.prank(resealCommittee); @@ -2123,23 +2117,13 @@ contract DualGovernanceUnitTests is UnitTest { // --- function testFuzz_setResealCommittee_HappyPath(address newResealCommittee) external { - address resealCommittee = makeAddr("RESEAL_COMMITTEE"); vm.assume(newResealCommittee != resealCommittee); - - _executor.execute( - address(_dualGovernance), - 0, - abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, resealCommittee) - ); - - vm.expectEmit(); - emit DualGovernance.ResealCommitteeSet(newResealCommittee); - _executor.execute( address(_dualGovernance), 0, abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, newResealCommittee) ); + assertEq(newResealCommittee, address(_dualGovernance.getResealCommittee())); } function testFuzz_setResealCommittee_RevertOn_NotAdminExecutor(address stranger) external { @@ -2150,13 +2134,6 @@ contract DualGovernanceUnitTests is UnitTest { _dualGovernance.setResealCommittee(makeAddr("NEW_RESEAL_COMMITTEE")); } - function test_setResealCommittee_RevertOn_SameAddress() external { - vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidResealCommittee.selector, address(0))); - _executor.execute( - address(_dualGovernance), 0, abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, address(0)) - ); - } - function testFuzz_setResealCommittee_RevertOn_InvalidResealCommittee(address newResealCommittee) external { _executor.execute( address(_dualGovernance), @@ -2164,7 +2141,7 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, newResealCommittee) ); - vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidResealCommittee.selector, newResealCommittee)); + vm.expectRevert(abi.encodeWithSelector(Resealer.InvalidResealCommittee.selector, newResealCommittee)); _executor.execute( address(_dualGovernance), 0, @@ -2179,9 +2156,6 @@ contract DualGovernanceUnitTests is UnitTest { function testFuzz_setResealManager_HappyPath(address newResealManager) external { vm.assume(newResealManager != address(0) && newResealManager != address(_RESEAL_MANAGER_STUB)); - vm.expectEmit(); - emit DualGovernance.ResealManagerSet(newResealManager); - _executor.execute( address(_dualGovernance), 0, @@ -2189,22 +2163,6 @@ contract DualGovernanceUnitTests is UnitTest { ); } - function test_setResealManager_RevertOn_InvalidAddress() external { - vm.expectRevert( - abi.encodeWithSelector(DualGovernance.InvalidResealManager.selector, address(_RESEAL_MANAGER_STUB)) - ); - _executor.execute( - address(_dualGovernance), - 0, - abi.encodeWithSelector(DualGovernance.setResealManager.selector, address(_RESEAL_MANAGER_STUB)) - ); - - vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidResealManager.selector, address(0))); - _executor.execute( - address(_dualGovernance), 0, abi.encodeWithSelector(DualGovernance.setResealManager.selector, address(0)) - ); - } - function test_setResealManger_RevertOn_CallerIsNotAdminExecutor(address stranger) external { vm.assume(stranger != address(_executor)); @@ -2219,6 +2177,7 @@ contract DualGovernanceUnitTests is UnitTest { function testFuzz_getResealManager_HappyPath(address newResealManager) external { vm.assume(newResealManager != address(_RESEAL_MANAGER_STUB)); + vm.assume(newResealManager != address(0)); _executor.execute( address(_dualGovernance), @@ -2229,6 +2188,20 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(newResealManager, address(_dualGovernance.getResealManager())); } + // --- + // getResealCommittee() + // --- + + function testFuzz_getResealCommittee_HappyPath(address newResealCommittee) external { + vm.assume(newResealCommittee != resealCommittee); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, newResealCommittee) + ); + assertEq(newResealCommittee, address(_dualGovernance.getResealCommittee())); + } + // --- // Helper methods // --- diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 59783c74..789e9b74 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -989,6 +989,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { } function testFuzz_getAdminExecutor(address executor) external { + vm.assume(executor != address(0)); EmergencyProtectedTimelock timelock = new EmergencyProtectedTimelock( EmergencyProtectedTimelock.SanityCheckParams({ maxAfterSubmitDelay: Durations.from(45 days), diff --git a/test/unit/libraries/Resealer.t.sol b/test/unit/libraries/Resealer.t.sol new file mode 100644 index 00000000..dddd84da --- /dev/null +++ b/test/unit/libraries/Resealer.t.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Resealer} from "contracts/libraries/Resealer.sol"; +import {IResealManager} from "contracts/interfaces/IResealManager.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; + +contract ResealerTest is UnitTest { + using Resealer for Resealer.Context; + + Resealer.Context ctx; + + address resealManager = makeAddr("resealManager"); + address resealCommittee = makeAddr("resealCommittee"); + + function setUp() external { + ctx.resealManager = IResealManager(resealManager); + ctx.resealCommittee = resealCommittee; + } + + function test_setResealManager_HappyPath(address newResealManager) external { + vm.assume(newResealManager != address(ctx.resealManager) && newResealManager != address(0)); + + vm.expectEmit(); + emit Resealer.ResealManagerSet(newResealManager); + this.external__setResealManager(newResealManager); + + assertEq(address(ctx.resealManager), newResealManager); + } + + function test_setResealManager_RevertOn_InvalidResealManager() external { + vm.expectRevert(abi.encodeWithSelector(Resealer.InvalidResealManager.selector, address(ctx.resealManager))); + this.external__setResealManager(address(ctx.resealManager)); + + vm.expectRevert(abi.encodeWithSelector(Resealer.InvalidResealManager.selector, address(0))); + this.external__setResealManager(address(0)); + } + + function testFuzz_setResealCommittee_HappyPath(address newResealCommittee) external { + vm.assume(newResealCommittee != ctx.resealCommittee); + + vm.expectEmit(); + emit Resealer.ResealCommitteeSet(newResealCommittee); + this.external__setResealCommittee(newResealCommittee); + + assertEq(ctx.resealCommittee, newResealCommittee); + } + + function test_setResealCommittee_RevertOn_InvalidResealCommittee() external { + vm.expectRevert(abi.encodeWithSelector(Resealer.InvalidResealCommittee.selector, ctx.resealCommittee)); + this.external__setResealCommittee(ctx.resealCommittee); + } + + function test_checkCallerIsResealCommittee_HappyPath() external { + vm.prank(resealCommittee); + this.external__checkCallerIsResealCommittee(); + } + + function testFuzz_checkCallerIsResealCommittee_RevertOn_Stranger(address stranger) external { + vm.assume(stranger != resealCommittee); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(Resealer.CallerIsNotResealCommittee.selector, stranger)); + this.external__checkCallerIsResealCommittee(); + } + + function external__checkCallerIsResealCommittee() external view { + ctx.checkCallerIsResealCommittee(); + } + + function external__setResealCommittee(address newResealCommittee) external { + ctx.setResealCommittee(newResealCommittee); + } + + function external__setResealManager(address newResealManager) external { + ctx.setResealManager(newResealManager); + } +} From 2e5e723bfdc9af1aaefb090b2cbc132c7d3e6979 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 25 Oct 2024 14:03:28 +0300 Subject: [PATCH 042/107] tests: add test case to forbid front-running claim of unStETH from batch --- test/scenario/escrow.t.sol | 52 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index eec9589c..e27dcd90 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -11,6 +11,7 @@ import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; import {Escrow, VetoerState, LockedAssetsTotals, WithdrawalsBatchesQueue} from "contracts/Escrow.sol"; +import {AssetsAccounting, UnstETHRecordStatus} from "contracts/libraries/AssetsAccounting.sol"; import {ScenarioTestBlueprint, LidoUtils, console} from "../utils/scenario-test-blueprint.sol"; @@ -544,6 +545,57 @@ contract EscrowHappyPath is ScenarioTestBlueprint { this.externalUnlockStETH(_VETOER_1); } + function testFork_EdgeCase_frontRunningClaimUnStethFromBatchIsForbidden() external { + // Prepare vetoer1 unstETH nft to lock in Escrow + uint256 requestAmount = 10 * 1e18; + uint256[] memory amounts = new uint256[](1); + amounts[i] = requestAmount; + vm.prank(_VETOER_1); + uint256[] memory unstETHIdsVetoer1 = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); + + // Should be the same as vetoer1 unstETH nft + uint256 lastRequestIdBeforeBatch = _lido.withdrawalQueue.getLastRequestId(); + + // Lock ustETH nfts + _lockUnstETH(_VETOER_1, unstETHIdsVetoer1); + // Lock stETH to generate batch + _lockStETH(_VETOER_1, 20 * requestAmount); + + vm.prank(address(_dualGovernance)); + escrow.startRageQuit(_RAGE_QUIT_EXTRA_TIMELOCK, _RAGE_QUIT_WITHDRAWALS_TIMELOCK); + + // Generate batch with stETH locked in Escrow + escrow.requestNextWithdrawalsBatch(1); + + // Should be the id of ustETH nft in the batch + uint256 requestIdFromBatch = _lido.withdrawalQueue.getLastRequestId(); + + // validate that the new ustEth nft is created + require(requestIdFromBatch == lastRequestIdBeforeBatch + 1); + + _finalizeWithdrawalQueue(); + + // Check that ustETH nft of vetoer1 could be claimed + uint256[] memory unstETHIdsToClaim = new uint256[](1); + unstETHIdsToClaim[0] = lastRequestIdBeforeBatch; + uint256[] memory hints = _lido.withdrawalQueue.findCheckpointHints( + unstETHIdsToClaim, 1, _lido.withdrawalQueue.getLastCheckpointIndex() + ); + escrow.claimUnstETH(unstETHIdsToClaim, hints); + + // The attempt to claim funds of unSteth from Escrow generated batch will fail + unstETHIdsToClaim[0] = requestIdFromBatch; + hints = _lido.withdrawalQueue.findCheckpointHints( + unstETHIdsToClaim, 1, _lido.withdrawalQueue.getLastCheckpointIndex() + ); + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, requestIdFromBatch, UnstETHRecordStatus.NotLocked + ) + ); + escrow.claimUnstETH(unstETHIdsToClaim, hints); + } + // --- // Helper external methods to test reverts // --- From 318ea70b63b3ad4c4e531318c1a51a4ae279d527 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Sun, 27 Oct 2024 19:31:53 +0400 Subject: [PATCH 043/107] Fix not working test --- test/scenario/escrow.t.sol | 41 +++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index e27dcd90..90fe99b4 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -549,14 +549,14 @@ contract EscrowHappyPath is ScenarioTestBlueprint { // Prepare vetoer1 unstETH nft to lock in Escrow uint256 requestAmount = 10 * 1e18; uint256[] memory amounts = new uint256[](1); - amounts[i] = requestAmount; + amounts[0] = requestAmount; vm.prank(_VETOER_1); uint256[] memory unstETHIdsVetoer1 = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); // Should be the same as vetoer1 unstETH nft uint256 lastRequestIdBeforeBatch = _lido.withdrawalQueue.getLastRequestId(); - // Lock ustETH nfts + // Lock unstETH nfts _lockUnstETH(_VETOER_1, unstETHIdsVetoer1); // Lock stETH to generate batch _lockStETH(_VETOER_1, 20 * requestAmount); @@ -564,18 +564,23 @@ contract EscrowHappyPath is ScenarioTestBlueprint { vm.prank(address(_dualGovernance)); escrow.startRageQuit(_RAGE_QUIT_EXTRA_TIMELOCK, _RAGE_QUIT_WITHDRAWALS_TIMELOCK); + uint256 batchSizeLimit = 16; // Generate batch with stETH locked in Escrow - escrow.requestNextWithdrawalsBatch(1); + escrow.requestNextWithdrawalsBatch(batchSizeLimit); - // Should be the id of ustETH nft in the batch - uint256 requestIdFromBatch = _lido.withdrawalQueue.getLastRequestId(); + uint256[] memory nextWithdrawalBatch = escrow.getNextWithdrawalBatch(batchSizeLimit); + assertEq(nextWithdrawalBatch.length, 1); + assertEq(nextWithdrawalBatch[0], _lido.withdrawalQueue.getLastRequestId()); - // validate that the new ustEth nft is created - require(requestIdFromBatch == lastRequestIdBeforeBatch + 1); + // Should be the id of unstETH nft in the batch + uint256 requestIdFromBatch = nextWithdrawalBatch[0]; + + // validate that the new unstEth nft is created + assertEq(requestIdFromBatch, lastRequestIdBeforeBatch + 1); _finalizeWithdrawalQueue(); - // Check that ustETH nft of vetoer1 could be claimed + // Check that unstETH nft of vetoer1 could be claimed uint256[] memory unstETHIdsToClaim = new uint256[](1); unstETHIdsToClaim[0] = lastRequestIdBeforeBatch; uint256[] memory hints = _lido.withdrawalQueue.findCheckpointHints( @@ -583,7 +588,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { ); escrow.claimUnstETH(unstETHIdsToClaim, hints); - // The attempt to claim funds of unSteth from Escrow generated batch will fail + // The attempt to claim funds of untEth from Escrow generated batch will fail unstETHIdsToClaim[0] = requestIdFromBatch; hints = _lido.withdrawalQueue.findCheckpointHints( unstETHIdsToClaim, 1, _lido.withdrawalQueue.getLastCheckpointIndex() @@ -594,6 +599,24 @@ contract EscrowHappyPath is ScenarioTestBlueprint { ) ); escrow.claimUnstETH(unstETHIdsToClaim, hints); + + // The rage quit process can be successfully finished + while (escrow.getUnclaimedUnstETHIdsCount() > 0) { + escrow.claimNextWithdrawalsBatch(batchSizeLimit); + } + + escrow.startRageQuitExtensionPeriod(); + assertEq(escrow.isRageQuitFinalized(), false); + + _wait(_RAGE_QUIT_EXTRA_TIMELOCK.plusSeconds(1)); + assertEq(escrow.isRageQuitFinalized(), true); + + _wait(_RAGE_QUIT_WITHDRAWALS_TIMELOCK.plusSeconds(1)); + + vm.startPrank(_VETOER_1); + escrow.withdrawETH(); + escrow.withdrawETH(unstETHIdsVetoer1); + vm.stopPrank(); } // --- From f7b34effacd8842e5423d440c33c2df088800a92 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 28 Nov 2024 21:51:04 +0400 Subject: [PATCH 044/107] Add missing executeProposal to plan-b doc --- docs/plan-b.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/plan-b.md b/docs/plan-b.md index 10bce15d..7f87a956 100644 --- a/docs/plan-b.md +++ b/docs/plan-b.md @@ -86,6 +86,17 @@ See: [`EmergencyProtectedTimelock.schedule`](#) #### Preconditions - The proposal with the given id MUST be in the `Submitted` state. + +### Function: `TimelockedGovernance.executeProposal` +```solidity +function executeProposal(uint256 proposalId) external +``` +Instructs the [`EmergencyProtectedTimelock`](#) singleton instance to execute the proposal with id `proposalId`. + +See: [`EmergencyProtectedTimelock.execute`](#) +#### Preconditions +- The proposal with the given id MUST be in the `Scheduled` state. + ### Function: `TimelockedGovernance.cancelAllPendingProposals` ```solidity From 407dd5de4b9e621e568a10ab5d96ac9af700a25a Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 13 Nov 2024 12:08:26 +0400 Subject: [PATCH 045/107] Remove unused interfaces and tests. --- contracts/interfaces/IGateSeal.sol | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 contracts/interfaces/IGateSeal.sol diff --git a/contracts/interfaces/IGateSeal.sol b/contracts/interfaces/IGateSeal.sol deleted file mode 100644 index 44b0b2a0..00000000 --- a/contracts/interfaces/IGateSeal.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -interface IGateSeal { - function seal(address[] calldata sealables) external; -} From d1188d2ebca04403a89ea699f12db1a011f479b5 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 13 Nov 2024 12:11:08 +0400 Subject: [PATCH 046/107] Update interfaces --- contracts/Escrow.sol | 26 +----- contracts/interfaces/IDualGovernance.sol | 29 ++++++ .../IEmergencyProtectedTimelock.sol | 22 ++++- contracts/interfaces/IEscrow.sol | 52 +++++++++++ contracts/interfaces/ITiebreaker.sol | 1 + contracts/interfaces/ITimelock.sol | 1 + scripts/deploy/DeployVerification.sol | 3 +- test/mocks/EscrowMock.sol | 92 ++++++++++++++++++- test/mocks/TimelockMock.sol | 4 + test/scenario/escrow.t.sol | 3 +- test/utils/scenario-test-blueprint.sol | 3 +- 11 files changed, 203 insertions(+), 33 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index aa54f934..9c8139a6 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -9,7 +9,7 @@ import {ETHValue, ETHValues} from "./types/ETHValue.sol"; import {SharesValue, SharesValues} from "./types/SharesValue.sol"; import {PercentD16, PercentsD16} from "./types/PercentD16.sol"; -import {IEscrow} from "./interfaces/IEscrow.sol"; +import {IEscrow, LockedAssetsTotals, VetoerState} from "./interfaces/IEscrow.sol"; import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; import {IWithdrawalQueue, WithdrawalRequestStatus} from "./interfaces/IWithdrawalQueue.sol"; @@ -19,30 +19,6 @@ import {EscrowState} from "./libraries/EscrowState.sol"; import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalsBatchesQueue.sol"; import {HolderAssets, StETHAccounting, UnstETHAccounting, AssetsAccounting} from "./libraries/AssetsAccounting.sol"; -/// @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; - uint256 unstETHUnfinalizedShares; - 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; - uint256 unstETHIdsCount; - 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. diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 8333345a..24833d07 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -1,11 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {IDualGovernanceConfigProvider} from "./IDualGovernanceConfigProvider.sol"; import {IGovernance} from "./IGovernance.sol"; +import {IEscrow} from "./IEscrow.sol"; +import {IResealManager} from "./IResealManager.sol"; import {ITiebreaker} from "./ITiebreaker.sol"; import {Timestamp} from "../types/Timestamp.sol"; import {Duration} from "../types/Duration.sol"; import {State} from "../libraries/DualGovernanceStateMachine.sol"; +import {Proposers} from "../libraries/Proposers.sol"; interface IDualGovernance is IGovernance, ITiebreaker { struct StateDetails { @@ -19,7 +23,32 @@ interface IDualGovernance is IGovernance, ITiebreaker { Duration vetoSignallingDuration; } + function MIN_TIEBREAKER_ACTIVATION_TIMEOUT() external view returns (Duration); + function MAX_TIEBREAKER_ACTIVATION_TIMEOUT() external view returns (Duration); + function MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT() external view returns (uint256); + function ESCROW_MASTER_COPY() external view returns (IEscrow); + + function canSubmitProposal() external view returns (bool); + function canCancelAllPendingProposals() external view returns (bool); function activateNextState() external; + function setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) external; + function getConfigProvider() external view returns (IDualGovernanceConfigProvider); + function getVetoSignallingEscrow() external view returns (address); + function getRageQuitEscrow() external view returns (address); + function getPersistedState() external view returns (State persistedState); + function getEffectiveState() external view returns (State effectiveState); + function getStateDetails() external view returns (StateDetails memory stateDetails); + + function registerProposer(address proposer, address executor) external; + function unregisterProposer(address proposer) external; + function isProposer(address account) external view returns (bool); + function getProposer(address account) external view returns (Proposers.Proposer memory proposer); + function getProposers() external view returns (Proposers.Proposer[] memory proposers); + function isExecutor(address account) external view returns (bool); function resealSealable(address sealable) external; + function setResealCommittee(address resealCommittee) external; + function setResealManager(address resealManager) external; + function getResealManager() external view returns (IResealManager); + function getResealCommittee() external view returns (address); } diff --git a/contracts/interfaces/IEmergencyProtectedTimelock.sol b/contracts/interfaces/IEmergencyProtectedTimelock.sol index 8916d298..e71dac97 100644 --- a/contracts/interfaces/IEmergencyProtectedTimelock.sol +++ b/contracts/interfaces/IEmergencyProtectedTimelock.sol @@ -12,11 +12,31 @@ interface IEmergencyProtectedTimelock is ITimelock { Timestamp emergencyProtectionEndsAfter; } + function MAX_AFTER_SUBMIT_DELAY() external view returns (Duration); + function MAX_AFTER_SCHEDULE_DELAY() external view returns (Duration); + function MAX_EMERGENCY_MODE_DURATION() external view returns (Duration); + function MAX_EMERGENCY_PROTECTION_DURATION() external view returns (Duration); + + function setupDelays(Duration afterSubmitDelay, Duration afterScheduleDelay) external; + function transferExecutorOwnership(address executor, address owner) external; + function setEmergencyProtectionActivationCommittee(address emergencyActivationCommittee) external; + function setEmergencyProtectionExecutionCommittee(address emergencyExecutionCommittee) external; + function setEmergencyProtectionEndDate(Timestamp emergencyProtectionEndDate) external; + function setEmergencyModeDuration(Duration emergencyModeDuration) external; + function setEmergencyGovernance(address emergencyGovernance) external; + function activateEmergencyMode() external; function emergencyExecute(uint256 proposalId) external; + function deactivateEmergencyMode() external; function emergencyReset() external; + function isEmergencyProtectionEnabled() external view returns (bool); + function isEmergencyModeActive() external view returns (bool); + function getEmergencyProtectionDetails() external view returns (EmergencyProtectionDetails memory details); 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); + function getAfterSubmitDelay() external view returns (Duration); + function getAfterScheduleDelay() external view returns (Duration); + + function setAdminExecutor(address newAdminExecutor) external; } diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol index 0542e47c..32aa42e3 100644 --- a/contracts/interfaces/IEscrow.sol +++ b/contracts/interfaces/IEscrow.sol @@ -3,12 +3,64 @@ pragma solidity 0.8.26; import {Duration} from "../types/Duration.sol"; import {PercentD16} from "../types/PercentD16.sol"; +import {Timestamp} from "../types/Timestamp.sol"; + +/// @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; + uint256 unstETHUnfinalizedShares; + 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; + uint256 unstETHIdsCount; + uint256 lastAssetsLockTimestamp; +} interface IEscrow { function initialize(Duration minAssetsLockDuration) external; + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares); + function unlockStETH() external returns (uint256 unlockedStETHShares); + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares); + function unlockWstETH() external returns (uint256 unlockedStETHShares); + function lockUnstETH(uint256[] memory unstETHIds) external; + function unlockUnstETH(uint256[] memory unstETHIds) external; + function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external; + function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitEthWithdrawalsDelay) external; + function requestNextWithdrawalsBatch(uint256 batchSize) external; + + function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external; + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external; + + function startRageQuitExtensionPeriod() external; + function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external; + + function withdrawETH() external; + function withdrawETH(uint256[] calldata unstETHIds) external; + + function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory totals); + function getVetoerState(address vetoer) external view returns (VetoerState memory state); + function getUnclaimedUnstETHIdsCount() external view returns (uint256); + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds); + function isWithdrawalsBatchesFinalized() external view returns (bool); + function isRageQuitExtensionPeriodStarted() external view returns (bool); + function getRageQuitExtensionPeriodStartedAt() external view returns (Timestamp); + function isRageQuitFinalized() external view returns (bool); function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport); function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; diff --git a/contracts/interfaces/ITiebreaker.sol b/contracts/interfaces/ITiebreaker.sol index 5f6c535b..6375fdf7 100644 --- a/contracts/interfaces/ITiebreaker.sol +++ b/contracts/interfaces/ITiebreaker.sol @@ -16,5 +16,6 @@ interface ITiebreaker { function setTiebreakerCommittee(address tiebreakerCommittee) external; function setTiebreakerActivationTimeout(Duration tiebreakerActivationTimeout) external; function tiebreakerScheduleProposal(uint256 proposalId) external; + function getTiebreakerDetails() external view returns (TiebreakerDetails memory tiebreakerState); function tiebreakerResumeSealable(address sealable) external; } diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index ed4ba61d..ba1b70dd 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -37,5 +37,6 @@ interface ITimelock { view returns (ProposalDetails memory proposalDetails, ExternalCall[] memory calls); function getProposalDetails(uint256 proposalId) external view returns (ProposalDetails memory proposalDetails); + function getProposalCalls(uint256 proposalId) external view returns (ExternalCall[] memory calls); function getProposalsCount() external view returns (uint256 count); } diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index 30abcee6..ada81e9a 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -5,7 +5,6 @@ import {Timestamps} from "contracts/types/Timestamp.sol"; import {Durations} from "contracts/types/Duration.sol"; import {Executor} from "contracts/Executor.sol"; import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; -import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; @@ -55,7 +54,7 @@ library DeployVerification { } function checkTimelock(DeployedAddresses memory res, DeployConfig memory dgDeployConfig) internal view { - EmergencyProtectedTimelock timelockInstance = EmergencyProtectedTimelock(res.timelock); + IEmergencyProtectedTimelock timelockInstance = IEmergencyProtectedTimelock(res.timelock); require( timelockInstance.getAdminExecutor() == res.adminExecutor, "Incorrect adminExecutor address in EmergencyProtectedTimelock" diff --git a/test/mocks/EscrowMock.sol b/test/mocks/EscrowMock.sol index fe3e0798..dc6e56e6 100644 --- a/test/mocks/EscrowMock.sol +++ b/test/mocks/EscrowMock.sol @@ -3,9 +3,11 @@ pragma solidity 0.8.26; import {Duration} from "contracts/types/Duration.sol"; import {PercentD16} from "contracts/types/PercentD16.sol"; +import {Timestamp} from "contracts/types/Timestamp.sol"; -import {IEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrow, LockedAssetsTotals, VetoerState} from "contracts/interfaces/IEscrow.sol"; +/* solhint-disable custom-errors */ contract EscrowMock is IEscrow { event __RageQuitStarted(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock); @@ -25,8 +27,92 @@ contract EscrowMock is IEscrow { __minAssetsLockDuration = minAssetsLockDuration; } - function startRageQuit(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock) external { - emit __RageQuitStarted(rageQuitExtraTimelock, rageQuitWithdrawalsTimelock); + function lockStETH(uint256 /* amount */ ) external returns (uint256 /* lockedStETHShares */ ) { + revert("Not implemented"); + } + + function unlockStETH() external returns (uint256 /* unlockedStETHShares */ ) { + revert("Not implemented"); + } + + function lockWstETH(uint256 /* amount */ ) external returns (uint256 /* lockedStETHShares */ ) { + revert("Not implemented"); + } + + function unlockWstETH() external returns (uint256 /* unlockedStETHShares */ ) { + revert("Not implemented"); + } + + function lockUnstETH(uint256[] memory /* unstETHIds */ ) external { + revert("Not implemented"); + } + + function unlockUnstETH(uint256[] memory /* unstETHIds */ ) external { + revert("Not implemented"); + } + + function markUnstETHFinalized(uint256[] memory, /* unstETHIds */ uint256[] calldata /* hints */ ) external { + revert("Not implemented"); + } + + function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitEthWithdrawalsDelay) external { + emit __RageQuitStarted(rageQuitExtensionPeriodDuration, rageQuitEthWithdrawalsDelay); + } + + function requestNextWithdrawalsBatch(uint256 /* batchSize */ ) external { + revert("Not implemented"); + } + + function claimNextWithdrawalsBatch(uint256, /* fromUnstETHId */ uint256[] calldata /* hints */ ) external { + revert("Not implemented"); + } + + function claimNextWithdrawalsBatch(uint256 /* maxUnstETHIdsCount */ ) external { + revert("Not implemented"); + } + + function startRageQuitExtensionPeriod() external { + revert("Not implemented"); + } + + function claimUnstETH(uint256[] calldata, /* unstETHIds */ uint256[] calldata /* hints */ ) external { + revert("Not implemented"); + } + + function withdrawETH() external { + revert("Not implemented"); + } + + function withdrawETH(uint256[] calldata /* unstETHIds */ ) external { + revert("Not implemented"); + } + + function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory /* totals */ ) { + revert("Not implemented"); + } + + function getVetoerState(address /* vetoer */ ) external view returns (VetoerState memory /* state */ ) { + revert("Not implemented"); + } + + function getUnclaimedUnstETHIdsCount() external view returns (uint256) { + revert("Not implemented"); + } + + function getNextWithdrawalBatch(uint256 /* limit */ ) external view returns (uint256[] memory /* unstETHIds */ ) { + revert("Not implemented"); + } + + function isWithdrawalsBatchesFinalized() external view returns (bool) { + revert("Not implemented"); + } + + function isRageQuitExtensionPeriodStarted() external view returns (bool) { + revert("Not implemented"); + } + + function getRageQuitExtensionPeriodStartedAt() external view returns (Timestamp) { + revert("Not implemented"); } function isRageQuitFinalized() external view returns (bool) { diff --git a/test/mocks/TimelockMock.sol b/test/mocks/TimelockMock.sol index 32dd2c30..3d18c676 100644 --- a/test/mocks/TimelockMock.sol +++ b/test/mocks/TimelockMock.sol @@ -108,6 +108,10 @@ contract TimelockMock is ITimelock { revert("Not Implemented"); } + function getProposalCalls(uint256 proposalId) external view returns (ExternalCall[] memory calls) { + revert("Not Implemented"); + } + function getProposalsCount() external view returns (uint256 count) { return submittedProposals.length; } diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 90fe99b4..3a88dfd3 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -10,7 +10,8 @@ import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; -import {Escrow, VetoerState, LockedAssetsTotals, WithdrawalsBatchesQueue} from "contracts/Escrow.sol"; +import {IEscrow, LockedAssetsTotals, VetoerState} from "contracts/interfaces/IEscrow.sol"; +import {Escrow, WithdrawalsBatchesQueue} from "contracts/Escrow.sol"; import {AssetsAccounting, UnstETHRecordStatus} from "contracts/libraries/AssetsAccounting.sol"; import {ScenarioTestBlueprint, LidoUtils, console} from "../utils/scenario-test-blueprint.sol"; diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 305572ae..430f34fa 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -12,7 +12,8 @@ import {PercentD16} from "contracts/types/PercentD16.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {Escrow, VetoerState, LockedAssetsTotals} from "contracts/Escrow.sol"; +import {IEscrow, LockedAssetsTotals, VetoerState} from "contracts/interfaces/IEscrow.sol"; +import {Escrow} from "contracts/Escrow.sol"; // --- // Interfaces From 490bb0eb2d4db6d32d203835bb8fd7fc0852285f Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:13:11 +0400 Subject: [PATCH 047/107] Move IAragonVoting interface to tests folder --- scripts/deploy/Config.sol | 3 +-- scripts/deploy/ContractsDeployment.sol | 5 ++--- scripts/deploy/DeployVerification.sol | 4 ++-- scripts/deploy/JsonConfig.s.sol | 7 +++---- test/utils/SetupDeployment.sol | 2 +- {contracts => test/utils}/interfaces/IAragonVoting.sol | 0 test/utils/lido-utils.sol | 2 +- 7 files changed, 10 insertions(+), 13 deletions(-) rename {contracts => test/utils}/interfaces/IAragonVoting.sol (100%) diff --git a/scripts/deploy/Config.sol b/scripts/deploy/Config.sol index eba20e94..4b46f375 100644 --- a/scripts/deploy/Config.sol +++ b/scripts/deploy/Config.sol @@ -6,7 +6,6 @@ pragma solidity 0.8.26; import {IStETH} from "contracts/interfaces/IStETH.sol"; import {IWstETH} from "contracts/interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; -import {IAragonVoting} from "contracts/interfaces/IAragonVoting.sol"; import {Duration} from "contracts/types/Duration.sol"; import {PercentD16} from "contracts/types/PercentD16.sol"; @@ -97,7 +96,7 @@ struct LidoContracts { IStETH stETH; IWstETH wstETH; IWithdrawalQueue withdrawalQueue; - IAragonVoting voting; + address voting; } function getSubCommitteeData( diff --git a/scripts/deploy/ContractsDeployment.sol b/scripts/deploy/ContractsDeployment.sol index eab35747..25f1ea4e 100644 --- a/scripts/deploy/ContractsDeployment.sol +++ b/scripts/deploy/ContractsDeployment.sol @@ -112,7 +112,7 @@ library DGContractsDeployment { EmergencyProtectedTimelock timelock = contracts.timelock; contracts.emergencyGovernance = - deployTimelockedGovernance({governance: address(lidoAddresses.voting), timelock: timelock}); + deployTimelockedGovernance({governance: lidoAddresses.voting, timelock: timelock}); adminExecutor.execute( address(timelock), @@ -289,8 +289,7 @@ library DGContractsDeployment { address(contracts.dualGovernance), 0, abi.encodeCall( - contracts.dualGovernance.registerProposer, - (address(lidoAddresses.voting), address(contracts.adminExecutor)) + contracts.dualGovernance.registerProposer, (lidoAddresses.voting, address(contracts.adminExecutor)) ) ); contracts.adminExecutor.execute( diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index ada81e9a..c17a68a4 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -138,7 +138,7 @@ library DeployVerification { ) internal view { TimelockedGovernance emergencyTimelockedGovernance = TimelockedGovernance(res.emergencyGovernance); require( - emergencyTimelockedGovernance.GOVERNANCE() == address(lidoAddresses.voting), + emergencyTimelockedGovernance.GOVERNANCE() == lidoAddresses.voting, "TimelockedGovernance governance != Lido voting" ); require( @@ -243,7 +243,7 @@ library DeployVerification { require(dg.getPersistedState() == State.Normal, "Incorrect DualGovernance persisted state"); require(dg.getEffectiveState() == State.Normal, "Incorrect DualGovernance effective state"); require(dg.getProposers().length == 1, "Incorrect amount of proposers"); - require(dg.isProposer(address(lidoAddresses.voting)) == true, "Lido voting is not set as a proposers[0]"); + require(dg.isProposer(lidoAddresses.voting) == true, "Lido voting is not set as a proposers[0]"); require(dg.isExecutor(res.adminExecutor) == true, "adminExecutor is not set as a proposers[0].executor"); IDualGovernance.StateDetails memory stateDetails = dg.getStateDetails(); diff --git a/scripts/deploy/JsonConfig.s.sol b/scripts/deploy/JsonConfig.s.sol index e9c5aa5e..5922de0d 100644 --- a/scripts/deploy/JsonConfig.s.sol +++ b/scripts/deploy/JsonConfig.s.sol @@ -9,7 +9,6 @@ import {console} from "forge-std/console.sol"; import {IStETH} from "contracts/interfaces/IStETH.sol"; import {IWstETH} from "contracts/interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; -import {IAragonVoting} from "contracts/interfaces/IAragonVoting.sol"; import { ST_ETH as MAINNET_ST_ETH, WST_ETH as MAINNET_WST_ETH, @@ -236,7 +235,7 @@ contract DGDeployJSONConfigProvider is Script { stETH: IStETH(MAINNET_ST_ETH), wstETH: IWstETH(MAINNET_WST_ETH), withdrawalQueue: IWithdrawalQueue(MAINNET_WITHDRAWAL_QUEUE), - voting: IAragonVoting(MAINNET_DAO_VOTING) + voting: MAINNET_DAO_VOTING }); } @@ -248,7 +247,7 @@ contract DGDeployJSONConfigProvider is Script { stETH: IStETH(stdJson.readAddress(jsonConfig, ".HOLESKY_MOCK_ST_ETH")), wstETH: IWstETH(stdJson.readAddress(jsonConfig, ".HOLESKY_MOCK_WST_ETH")), withdrawalQueue: IWithdrawalQueue(stdJson.readAddress(jsonConfig, ".HOLESKY_MOCK_WITHDRAWAL_QUEUE")), - voting: IAragonVoting(stdJson.readAddress(jsonConfig, ".HOLESKY_MOCK_DAO_VOTING")) + voting: stdJson.readAddress(jsonConfig, ".HOLESKY_MOCK_DAO_VOTING") }); } @@ -257,7 +256,7 @@ contract DGDeployJSONConfigProvider is Script { stETH: IStETH(HOLESKY_ST_ETH), wstETH: IWstETH(HOLESKY_WST_ETH), withdrawalQueue: IWithdrawalQueue(HOLESKY_WITHDRAWAL_QUEUE), - voting: IAragonVoting(HOLESKY_DAO_VOTING) + voting: HOLESKY_DAO_VOTING }); } diff --git a/test/utils/SetupDeployment.sol b/test/utils/SetupDeployment.sol index d8256db8..9e1d1201 100644 --- a/test/utils/SetupDeployment.sol +++ b/test/utils/SetupDeployment.sol @@ -191,7 +191,7 @@ abstract contract SetupDeployment is Test { lidoAddresses.stETH = _lido.stETH; lidoAddresses.wstETH = _lido.wstETH; lidoAddresses.withdrawalQueue = _lido.withdrawalQueue; - lidoAddresses.voting = _lido.voting; + lidoAddresses.voting = address(_lido.voting); } // --- diff --git a/contracts/interfaces/IAragonVoting.sol b/test/utils/interfaces/IAragonVoting.sol similarity index 100% rename from contracts/interfaces/IAragonVoting.sol rename to test/utils/interfaces/IAragonVoting.sol diff --git a/test/utils/lido-utils.sol b/test/utils/lido-utils.sol index 13ef9952..9f3f42c0 100644 --- a/test/utils/lido-utils.sol +++ b/test/utils/lido-utils.sol @@ -13,7 +13,7 @@ import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; import {IAragonACL} from "./interfaces/IAragonACL.sol"; import {IAragonAgent} from "./interfaces/IAragonAgent.sol"; -import {IAragonVoting} from "contracts/interfaces/IAragonVoting.sol"; +import {IAragonVoting} from "./interfaces/IAragonVoting.sol"; import {IAragonForwarder} from "./interfaces/IAragonForwarder.sol"; import {EvmScriptUtils} from "./evm-script-utils.sol"; From d6251ac46a8190dc7e13a495c59c0dbeeff85f48 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:28:33 +0400 Subject: [PATCH 048/107] Move some methods to ITimelock interface --- .../IEmergencyProtectedTimelock.sol | 6 ------ contracts/interfaces/ITimelock.sol | 7 +++++++ test/mocks/TimelockMock.sol | 21 +++++++++++++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/contracts/interfaces/IEmergencyProtectedTimelock.sol b/contracts/interfaces/IEmergencyProtectedTimelock.sol index e71dac97..fa8ab86e 100644 --- a/contracts/interfaces/IEmergencyProtectedTimelock.sol +++ b/contracts/interfaces/IEmergencyProtectedTimelock.sol @@ -17,8 +17,6 @@ interface IEmergencyProtectedTimelock is ITimelock { function MAX_EMERGENCY_MODE_DURATION() external view returns (Duration); function MAX_EMERGENCY_PROTECTION_DURATION() external view returns (Duration); - function setupDelays(Duration afterSubmitDelay, Duration afterScheduleDelay) external; - function transferExecutorOwnership(address executor, address owner) external; function setEmergencyProtectionActivationCommittee(address emergencyActivationCommittee) external; function setEmergencyProtectionExecutionCommittee(address emergencyExecutionCommittee) external; function setEmergencyProtectionEndDate(Timestamp emergencyProtectionEndDate) external; @@ -35,8 +33,4 @@ interface IEmergencyProtectedTimelock is ITimelock { function getEmergencyGovernance() external view returns (address emergencyGovernance); function getEmergencyActivationCommittee() external view returns (address committee); function getEmergencyExecutionCommittee() external view returns (address committee); - function getAfterSubmitDelay() external view returns (Duration); - function getAfterScheduleDelay() external view returns (Duration); - - function setAdminExecutor(address newAdminExecutor) external; } diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index ba1b70dd..da94fda1 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {Duration} from "../types/Duration.sol"; import {Timestamp} from "../types/Timestamp.sol"; import {ExternalCall} from "../libraries/ExternalCalls.sol"; @@ -29,6 +30,7 @@ interface ITimelock { function canExecute(uint256 proposalId) external view returns (bool); function getAdminExecutor() external view returns (address); + function setAdminExecutor(address newAdminExecutor) external; function getGovernance() external view returns (address); function setGovernance(address newGovernance) external; @@ -39,4 +41,9 @@ interface ITimelock { function getProposalDetails(uint256 proposalId) external view returns (ProposalDetails memory proposalDetails); function getProposalCalls(uint256 proposalId) external view returns (ExternalCall[] memory calls); function getProposalsCount() external view returns (uint256 count); + + function getAfterSubmitDelay() external view returns (Duration); + function getAfterScheduleDelay() external view returns (Duration); + function setupDelays(Duration afterSubmitDelay, Duration afterScheduleDelay) external; + function transferExecutorOwnership(address executor, address owner) external; } diff --git a/test/mocks/TimelockMock.sol b/test/mocks/TimelockMock.sol index 3d18c676..0af130e6 100644 --- a/test/mocks/TimelockMock.sol +++ b/test/mocks/TimelockMock.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {Duration} from "contracts/types/Duration.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; import {ITimelock, ProposalStatus} from "contracts/interfaces/ITimelock.sol"; import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; @@ -119,4 +120,24 @@ contract TimelockMock is ITimelock { function getAdminExecutor() external view returns (address) { return _ADMIN_EXECUTOR; } + + function setAdminExecutor(address newAdminExecutor) external { + revert("Not Implemented"); + } + + function getAfterSubmitDelay() external view returns (Duration) { + revert("Not Implemented"); + } + + function getAfterScheduleDelay() external view returns (Duration) { + revert("Not Implemented"); + } + + function setupDelays(Duration afterSubmitDelay, Duration afterScheduleDelay) external { + revert("Not Implemented"); + } + + function transferExecutorOwnership(address executor, address owner) external { + revert("Not Implemented"); + } } From fabdd300ccb5aebbdbbe2c93af2df32e8a88bc8f Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 20 Nov 2024 19:04:43 +0400 Subject: [PATCH 049/107] Move struct declarations --- contracts/Escrow.sol | 6 +-- contracts/interfaces/IEscrow.sol | 46 +++++++++++----------- contracts/interfaces/IWithdrawalQueue.sol | 18 ++++----- contracts/libraries/AssetsAccounting.sol | 6 +-- test/mocks/EscrowMock.sol | 2 +- test/mocks/WithdrawalQueueMock.sol | 4 +- test/scenario/escrow.t.sol | 14 ++++--- test/unit/libraries/AssetsAccounting.t.sol | 40 ++++++++++--------- test/utils/scenario-test-blueprint.sol | 28 +++++++------ 9 files changed, 86 insertions(+), 78 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 9c8139a6..ec9c4bb4 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -9,10 +9,10 @@ import {ETHValue, ETHValues} from "./types/ETHValue.sol"; import {SharesValue, SharesValues} from "./types/SharesValue.sol"; import {PercentD16, PercentsD16} from "./types/PercentD16.sol"; -import {IEscrow, LockedAssetsTotals, VetoerState} from "./interfaces/IEscrow.sol"; +import {IEscrow} from "./interfaces/IEscrow.sol"; import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; -import {IWithdrawalQueue, WithdrawalRequestStatus} from "./interfaces/IWithdrawalQueue.sol"; +import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; import {EscrowState} from "./libraries/EscrowState.sol"; @@ -213,7 +213,7 @@ contract Escrow is IEscrow { DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); - WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol index 32aa42e3..e0e80d4c 100644 --- a/contracts/interfaces/IEscrow.sol +++ b/contracts/interfaces/IEscrow.sol @@ -5,31 +5,31 @@ import {Duration} from "../types/Duration.sol"; import {PercentD16} from "../types/PercentD16.sol"; import {Timestamp} from "../types/Timestamp.sol"; -/// @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; - uint256 unstETHUnfinalizedShares; - uint256 unstETHFinalizedETH; -} +interface IEscrow { + /// @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; + uint256 unstETHUnfinalizedShares; + 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; - uint256 unstETHIdsCount; - uint256 lastAssetsLockTimestamp; -} + /// @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; + uint256 unstETHIdsCount; + uint256 lastAssetsLockTimestamp; + } -interface IEscrow { function initialize(Duration minAssetsLockDuration) external; function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares); diff --git a/contracts/interfaces/IWithdrawalQueue.sol b/contracts/interfaces/IWithdrawalQueue.sol index 3ec50762..8d54ba2d 100644 --- a/contracts/interfaces/IWithdrawalQueue.sol +++ b/contracts/interfaces/IWithdrawalQueue.sol @@ -3,16 +3,16 @@ pragma solidity 0.8.26; import {IERC721} from "@openzeppelin/contracts/interfaces/IERC721.sol"; -struct WithdrawalRequestStatus { - uint256 amountOfStETH; - uint256 amountOfShares; - address owner; - uint256 timestamp; - bool isFinalized; - bool isClaimed; -} - interface IWithdrawalQueue is IERC721 { + struct WithdrawalRequestStatus { + uint256 amountOfStETH; + uint256 amountOfShares; + address owner; + uint256 timestamp; + bool isFinalized; + bool isClaimed; + } + function MIN_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256); function MAX_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256); diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index 04472694..f706a6ea 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -7,7 +7,7 @@ import {Timestamps, Timestamp} from "../types/Timestamp.sol"; import {SharesValue, SharesValues} from "../types/SharesValue.sol"; import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; -import {WithdrawalRequestStatus} from "../interfaces/IWithdrawalQueue.sol"; +import {IWithdrawalQueue} from "../interfaces/IWithdrawalQueue.sol"; /// @notice Tracks the stETH and unstETH tokens associated with users. /// @param stETHLockedShares Total number of stETH shares held by the user. @@ -223,7 +223,7 @@ library AssetsAccounting { Context storage self, address holder, uint256[] memory unstETHIds, - WithdrawalRequestStatus[] memory statuses + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses ) internal { assert(unstETHIds.length == statuses.length); @@ -384,7 +384,7 @@ library AssetsAccounting { Context storage self, address holder, uint256 unstETHId, - WithdrawalRequestStatus memory status + IWithdrawalQueue.WithdrawalRequestStatus memory status ) private returns (SharesValue shares) { if (status.isFinalized) { revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.Finalized); diff --git a/test/mocks/EscrowMock.sol b/test/mocks/EscrowMock.sol index dc6e56e6..c56d4535 100644 --- a/test/mocks/EscrowMock.sol +++ b/test/mocks/EscrowMock.sol @@ -5,7 +5,7 @@ import {Duration} from "contracts/types/Duration.sol"; import {PercentD16} from "contracts/types/PercentD16.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; -import {IEscrow, LockedAssetsTotals, VetoerState} from "contracts/interfaces/IEscrow.sol"; +import {IEscrow} from "contracts/interfaces/IEscrow.sol"; /* solhint-disable custom-errors */ contract EscrowMock is IEscrow { diff --git a/test/mocks/WithdrawalQueueMock.sol b/test/mocks/WithdrawalQueueMock.sol index 57290346..4fde8a18 100644 --- a/test/mocks/WithdrawalQueueMock.sol +++ b/test/mocks/WithdrawalQueueMock.sol @@ -2,7 +2,7 @@ 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"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; /* solhint-disable no-unused-vars,custom-errors */ contract WithdrawalQueueMock is IWithdrawalQueue { @@ -37,7 +37,7 @@ contract WithdrawalQueueMock is IWithdrawalQueue { function getWithdrawalStatus(uint256[] calldata _requestIds) external view - returns (WithdrawalRequestStatus[] memory statuses) + returns (IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses) { revert("Not Implemented"); } diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 3a88dfd3..093197de 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -6,11 +6,11 @@ import {console} from "forge-std/Test.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {PercentsD16} from "contracts/types/PercentD16.sol"; -import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; -import {IEscrow, LockedAssetsTotals, VetoerState} from "contracts/interfaces/IEscrow.sol"; +import {IEscrow} from "contracts/interfaces/IEscrow.sol"; import {Escrow, WithdrawalsBatchesQueue} from "contracts/Escrow.sol"; import {AssetsAccounting, UnstETHRecordStatus} from "contracts/libraries/AssetsAccounting.sol"; @@ -212,17 +212,18 @@ contract EscrowHappyPath is ScenarioTestBlueprint { uint256[] memory unstETHIds = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); uint256 totalSharesLocked; - WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = + _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); for (uint256 i = 0; i < unstETHIds.length; ++i) { totalSharesLocked += statuses[i].amountOfShares; } _lockUnstETH(_VETOER_1, unstETHIds); - VetoerState memory vetoerState = escrow.getVetoerState(_VETOER_1); + IEscrow.VetoerState memory vetoerState = escrow.getVetoerState(_VETOER_1); assertEq(vetoerState.unstETHIdsCount, 2); - LockedAssetsTotals memory totals = escrow.getLockedAssetsTotals(); + IEscrow.LockedAssetsTotals memory totals = escrow.getLockedAssetsTotals(); assertEq(totals.unstETHFinalizedETH, 0); assertEq(totals.unstETHUnfinalizedShares, totalSharesLocked); @@ -324,7 +325,8 @@ contract EscrowHappyPath is ScenarioTestBlueprint { uint256[] memory unstETHIdsToClaim = escrow.getNextWithdrawalBatch(expectedWithdrawalsBatchesCount); // assertEq(total, expectedWithdrawalsBatchesCount); - WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIdsToClaim); + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = + _lido.withdrawalQueue.getWithdrawalStatus(unstETHIdsToClaim); for (uint256 i = 0; i < statuses.length; ++i) { assertTrue(statuses[i].isFinalized); diff --git a/test/unit/libraries/AssetsAccounting.t.sol b/test/unit/libraries/AssetsAccounting.t.sol index cf928b64..88dd8f52 100644 --- a/test/unit/libraries/AssetsAccounting.t.sol +++ b/test/unit/libraries/AssetsAccounting.t.sol @@ -10,9 +10,8 @@ import {SharesValue, SharesValues, SharesValueOverflow, SharesValueUnderflow} fr import {IndicesOneBased} from "contracts/types/IndexOneBased.sol"; import {Durations} from "contracts/types/Duration.sol"; import {Timestamps} from "contracts/types/Timestamp.sol"; -import { - AssetsAccounting, WithdrawalRequestStatus, UnstETHRecordStatus -} from "contracts/libraries/AssetsAccounting.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {AssetsAccounting, UnstETHRecordStatus} from "contracts/libraries/AssetsAccounting.sol"; import {UnitTest, Duration} from "test/utils/unit-test.sol"; @@ -347,8 +346,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHIds.push(genRandomUnstEthId(1024)); _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = - new WithdrawalRequestStatus[](amountsOfShares.length); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](amountsOfShares.length); uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); for (uint256 i = 0; i < amountsOfShares.length; ++i) { @@ -389,7 +388,8 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountUnstETHLock_RevertOn_UnstETHIdsLengthNotEqualToWithdrawalRequestStatusesLength( address holder ) external { - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](1); uint256[] memory unstETHIds = new uint256[](0); vm.expectRevert(); @@ -411,8 +411,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = - new WithdrawalRequestStatus[](amountsOfShares.length); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](amountsOfShares.length); uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); for (uint256 i = 0; i < amountsOfShares.length; ++i) { @@ -450,8 +450,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = - new WithdrawalRequestStatus[](amountsOfShares.length); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](amountsOfShares.length); uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); for (uint256 i = 0; i < amountsOfShares.length; ++i) { @@ -483,8 +483,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = - new WithdrawalRequestStatus[](amountsOfShares.length); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](amountsOfShares.length); uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); for (uint256 i = 0; i < amountsOfShares.length; ++i) { @@ -522,8 +522,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = - new WithdrawalRequestStatus[](amountsOfShares.length); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](amountsOfShares.length); uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); for (uint256 i = 0; i < amountsOfShares.length; ++i) { @@ -559,7 +559,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](0); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](0); uint256[] memory unstETHIds = new uint256[](0); vm.expectEmit(); @@ -586,7 +587,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](1); uint256[] memory unstETHIds = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(0); @@ -611,7 +613,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](1); uint256[] memory unstETHIds = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(0); @@ -635,7 +638,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](1); uint256[] memory unstETHIds = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(0); diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 430f34fa..864cf88c 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -12,7 +12,7 @@ import {PercentD16} from "contracts/types/PercentD16.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {IEscrow, LockedAssetsTotals, VetoerState} from "contracts/interfaces/IEscrow.sol"; +import {IEscrow} from "contracts/interfaces/IEscrow.sol"; import {Escrow} from "contracts/Escrow.sol"; // --- @@ -21,7 +21,7 @@ import {Escrow} from "contracts/Escrow.sol"; import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; import {ITimelock} from "contracts/interfaces/ITimelock.sol"; -import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; import {IPotentiallyDangerousContract} from "./interfaces/IPotentiallyDangerousContract.sol"; // --- @@ -199,7 +199,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { function _unlockWstETH(address vetoer) internal { Escrow escrow = _getVetoSignallingEscrow(); uint256 wstETHBalanceBefore = _lido.wstETH.balanceOf(vetoer); - VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + IEscrow.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); vm.startPrank(vetoer); uint256 wstETHUnlocked = escrow.unlockWstETH(); @@ -213,11 +213,12 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { function _lockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); - LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); + IEscrow.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + IEscrow.LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); uint256 unstETHTotalSharesLocked = 0; - WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = + _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); for (uint256 i = 0; i < unstETHIds.length; ++i) { unstETHTotalSharesLocked += statuses[i].amountOfShares; } @@ -232,10 +233,10 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), address(escrow)); } - VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); + IEscrow.VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount + unstETHIds.length); - LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + IEscrow.LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); assertEq( lockedAssetsTotalsAfter.unstETHUnfinalizedShares, lockedAssetsTotalsBefore.unstETHUnfinalizedShares + unstETHTotalSharesLocked @@ -244,11 +245,12 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { function _unlockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); - LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); + IEscrow.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + IEscrow.LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); uint256 unstETHTotalSharesUnlocked = 0; - WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = + _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); for (uint256 i = 0; i < unstETHIds.length; ++i) { unstETHTotalSharesUnlocked += statuses[i].amountOfShares; } @@ -261,11 +263,11 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), vetoer); } - VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); + IEscrow.VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount - unstETHIds.length); // TODO: implement correct assert. It must consider was unstETH finalized or not - LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + IEscrow.LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); assertEq( lockedAssetsTotalsAfter.unstETHUnfinalizedShares, lockedAssetsTotalsBefore.unstETHUnfinalizedShares - unstETHTotalSharesUnlocked From 3b2201a1ccc47c960a58beec46a7c60a6b2d58e9 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:21:23 +0400 Subject: [PATCH 050/107] Add methods executeProposal(), canExecuteProposal() to DualGovernance contracts and appropriate interfaces. Fix obsolete comments. --- contracts/DualGovernance.sol | 35 ++++++++- contracts/interfaces/IDualGovernance.sol | 1 + contracts/interfaces/IGovernance.sol | 1 + test/mocks/TimelockMock.sol | 35 ++++++--- test/unit/DualGovernance.t.sol | 94 +++++++++++++++++++++++- 5 files changed, 150 insertions(+), 16 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index daccd6c0..99f6fd32 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -44,6 +44,7 @@ contract DualGovernance is IDualGovernance { error CallerIsNotAdminExecutor(address caller); error ProposalSubmissionBlocked(); error ProposalSchedulingBlocked(uint256 proposalId); + error ProposalExecutionBlocked(uint256 proposalId); error ResealIsNotAllowedInNormalState(); error InvalidTiebreakerActivationTimeoutBounds(); @@ -189,10 +190,10 @@ contract DualGovernance is IDualGovernance { /// @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 + /// the submission time, when the `ITimelock.getAfterSubmitDelay()` 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. + /// is initially submitted to the Dual Governance system. function scheduleProposal(uint256 proposalId) external { _stateMachine.activateNextState(ESCROW_MASTER_COPY); Timestamp proposalSubmittedAt = TIMELOCK.getProposalDetails(proposalId).submittedAt; @@ -202,6 +203,21 @@ contract DualGovernance is IDualGovernance { TIMELOCK.schedule(proposalId); } + /// @notice Executes a proposal previously scheduled for execution in the Dual Governance system. + /// The proposal can only be executed when the `ITimelock.getAfterScheduleDelay()` has passed and proposal + /// wasn't cancelled earlier. + /// @param proposalId The unique identifier of the proposal to be executed. This ID is obtained when the proposal + /// is initially submitted to the Dual Governance system. + function executeProposal(uint256 proposalId) external { + _stateMachine.activateNextState(ESCROW_MASTER_COPY); + + if (!TIMELOCK.canExecute(proposalId)) { + revert ProposalExecutionBlocked(proposalId); + } + + TIMELOCK.execute(proposalId); + } + /// @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. @@ -248,7 +264,7 @@ contract DualGovernance is IDualGovernance { /// - 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 + /// - The required delay period, as defined by `ITimelock.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 @@ -259,6 +275,19 @@ contract DualGovernance is IDualGovernance { && TIMELOCK.canSchedule(proposalId); } + /// @notice Returns whether a previously scheduled proposal can be executed based on the proposal's submission time, + /// and its current status. + /// @dev Proposal execution is allowed only if all the following conditions are met: + /// - The proposal has already been scheduled. + /// - The required delay period, as defined by `ITimelock.getAfterScheduleDelay()`, has elapsed since the proposal + /// was scheduled. + /// @param proposalId The unique identifier of the proposal to check. + /// @return canExecuteProposal A boolean value indicating whether the proposal can be executed (`true`) or + /// not (`false`) based on the proposal's status. + function canExecuteProposal(uint256 proposalId) external view returns (bool) { + return TIMELOCK.canExecute(proposalId); + } + /// @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. diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 24833d07..526f77e6 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -29,6 +29,7 @@ interface IDualGovernance is IGovernance, ITiebreaker { function ESCROW_MASTER_COPY() external view returns (IEscrow); function canSubmitProposal() external view returns (bool); + function canExecuteProposal(uint256 proposalId) external view returns (bool); function canCancelAllPendingProposals() external view returns (bool); function activateNextState() external; function setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) external; diff --git a/contracts/interfaces/IGovernance.sol b/contracts/interfaces/IGovernance.sol index 51652b40..75b4a62a 100644 --- a/contracts/interfaces/IGovernance.sol +++ b/contracts/interfaces/IGovernance.sol @@ -12,6 +12,7 @@ interface IGovernance { string calldata metadata ) external returns (uint256 proposalId); function scheduleProposal(uint256 proposalId) external; + function executeProposal(uint256 proposalId) external; function cancelAllPendingProposals() external returns (bool); function canScheduleProposal(uint256 proposalId) external view returns (bool); diff --git a/test/mocks/TimelockMock.sol b/test/mocks/TimelockMock.sol index 0af130e6..e09efbf0 100644 --- a/test/mocks/TimelockMock.sol +++ b/test/mocks/TimelockMock.sol @@ -2,10 +2,10 @@ pragma solidity 0.8.26; import {Duration} from "contracts/types/Duration.sol"; -import {Timestamp} from "contracts/types/Timestamp.sol"; -import {ITimelock, ProposalStatus} from "contracts/interfaces/ITimelock.sol"; +import {ITimelock} from "contracts/interfaces/ITimelock.sol"; import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; +/* solhint-disable custom-errors */ contract TimelockMock is ITimelock { uint8 public constant OFFSET = 1; @@ -16,6 +16,7 @@ contract TimelockMock is ITimelock { } mapping(uint256 => bool) public canScheduleProposal; + mapping(uint256 => bool) public canExecuteProposal; uint256[] public submittedProposals; uint256[] public scheduledProposals; @@ -39,18 +40,22 @@ contract TimelockMock is ITimelock { function schedule(uint256 proposalId) external { if (canScheduleProposal[proposalId] == false) { - revert(); + revert("Can't schedule"); } scheduledProposals.push(proposalId); } function execute(uint256 proposalId) external { + if (canExecuteProposal[proposalId] == false) { + revert("Can't execute"); + } + executedProposals.push(proposalId); } function canExecute(uint256 proposalId) external view returns (bool) { - revert("Not Implemented"); + return canExecuteProposal[proposalId]; } function canSchedule(uint256 proposalId) external view returns (bool) { @@ -65,6 +70,10 @@ contract TimelockMock is ITimelock { canScheduleProposal[proposalId] = true; } + function setExecutable(uint256 proposalId) external { + canExecuteProposal[proposalId] = true; + } + function getSubmittedProposals() external view returns (uint256[] memory) { return submittedProposals; } @@ -81,7 +90,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"); } @@ -97,7 +110,7 @@ contract TimelockMock is ITimelock { revert("Not Implemented"); } - function emergencyExecute(uint256 proposalId) external { + function emergencyExecute(uint256 /* proposalId */ ) external { revert("Not Implemented"); } @@ -105,11 +118,11 @@ 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"); } - function getProposalCalls(uint256 proposalId) external view returns (ExternalCall[] memory calls) { + function getProposalCalls(uint256 /* proposalId */ ) external view returns (ExternalCall[] memory calls) { revert("Not Implemented"); } @@ -121,7 +134,7 @@ contract TimelockMock is ITimelock { return _ADMIN_EXECUTOR; } - function setAdminExecutor(address newAdminExecutor) external { + function setAdminExecutor(address /* newAdminExecutor */ ) external { revert("Not Implemented"); } @@ -133,11 +146,11 @@ contract TimelockMock is ITimelock { revert("Not Implemented"); } - function setupDelays(Duration afterSubmitDelay, Duration afterScheduleDelay) external { + function setupDelays(Duration, /* afterSubmitDelay */ Duration /* afterScheduleDelay */ ) external { revert("Not Implemented"); } - function transferExecutorOwnership(address executor, address owner) external { + function transferExecutorOwnership(address, /* executor */ address /* owner */ ) external { revert("Not Implemented"); } } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 129539fc..d635bc11 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.26; 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 {PercentsD16} from "contracts/types/PercentD16.sol"; import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; @@ -25,7 +25,6 @@ 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 {IEscrow} from "contracts/interfaces/IEscrow.sol"; @@ -319,6 +318,61 @@ contract DualGovernanceUnitTests is UnitTest { _dualGovernance.scheduleProposal(proposalId); } + // --- + // executeProposal() + // --- + + function testFuzz_executeProposal_HappyPath(address stranger) external { + uint256 proposalId = _dualGovernance.submitProposal(_generateExternalCalls(), ""); + + _scheduleProposal(proposalId, Timestamps.now()); + + uint256[] memory scheduledProposals = _timelock.getScheduledProposals(); + assertEq(scheduledProposals.length, 1); + assertEq(scheduledProposals[0], proposalId); + + _timelock.setExecutable(proposalId); + + vm.prank(stranger); + _dualGovernance.executeProposal(proposalId); + + assertEq(_timelock.getExecutedProposals().length, 1); + assertEq(_timelock.getExecutedProposals()[0], proposalId); + } + + function test_executeProposal_ActivatesNextState() external { + uint256 proposalId = _dualGovernance.submitProposal(_generateExternalCalls(), ""); + + _scheduleProposal(proposalId, Timestamps.now()); + assertEq(_timelock.getScheduledProposals().length, 1); + + vm.startPrank(vetoer); + _escrow.lockStETH(5 ether); + _wait(_configProvider.VETO_SIGNALLING_MIN_DURATION()); + _escrow.unlockStETH(); + vm.stopPrank(); + _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + _timelock.setExecutable(proposalId); + assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); + + _dualGovernance.executeProposal(proposalId); + + assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); + } + + function test_executeProposal_RevertOn_CannotExecute() external { + uint256 proposalId = _dualGovernance.submitProposal(_generateExternalCalls(), ""); + _scheduleProposal(proposalId, Timestamps.now()); + + uint256[] memory scheduledProposals = _timelock.getScheduledProposals(); + assertEq(scheduledProposals.length, 1); + assertEq(scheduledProposals[0], proposalId); + + vm.expectRevert(abi.encodeWithSelector(DualGovernance.ProposalExecutionBlocked.selector, proposalId)); + _dualGovernance.executeProposal(proposalId); + } + // --- // cancelAllPendingProposals() // --- @@ -597,6 +651,22 @@ contract DualGovernanceUnitTests is UnitTest { assertTrue(_dualGovernance.canSubmitProposal()); } + // --- + // canExecuteProposal() + // --- + + function test_canExecuteProposal() external { + uint256 proposalId = _dualGovernance.submitProposal(_generateExternalCalls(), ""); + + _scheduleProposal(proposalId, Timestamps.now()); + assertEq(_timelock.getScheduledProposals().length, 1); + + assertFalse(_dualGovernance.canExecuteProposal(proposalId)); + + _timelock.setExecutable(proposalId); + assertTrue(_dualGovernance.canExecuteProposal(proposalId)); + } + // --- // canScheduleProposal() // --- @@ -2211,6 +2281,26 @@ contract DualGovernanceUnitTests is UnitTest { _timelock.submit(msg.sender, address(0), new ExternalCall[](0), ""); } + function _scheduleProposal(uint256 proposalId, Timestamp submittedAt) internal { + _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) + }) + ) + ); + vm.expectCall(address(_timelock), 0, abi.encodeWithSelector(TimelockMock.schedule.selector, proposalId)); + _dualGovernance.scheduleProposal(proposalId); + } + 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 942dde0b43f91c1502b5e45054790f2f7b58c387 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 29 Nov 2024 16:50:12 +0300 Subject: [PATCH 051/107] Merge develop-archive-sorted-through into fix/interfaces-update --- contracts/DualGovernance.sol | 30 ---------- contracts/interfaces/IDualGovernance.sol | 1 - contracts/interfaces/IEscrow.sol | 2 +- contracts/interfaces/IGovernance.sol | 1 - contracts/interfaces/ITimelock.sol | 3 +- test/mocks/EscrowMock.sol | 2 +- test/mocks/TimelockMock.sol | 6 +- test/unit/DualGovernance.t.sol | 71 ------------------------ test/unit/Escrow.t.sol | 7 ++- test/unit/TimelockedGovernance.t.sol | 1 + 10 files changed, 14 insertions(+), 110 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 99f6fd32..c5811848 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -40,11 +40,9 @@ contract DualGovernance is IDualGovernance { error NotAdminProposer(); error UnownedAdminExecutor(); - error CallerIsNotResealCommittee(address caller); error CallerIsNotAdminExecutor(address caller); error ProposalSubmissionBlocked(); error ProposalSchedulingBlocked(uint256 proposalId); - error ProposalExecutionBlocked(uint256 proposalId); error ResealIsNotAllowedInNormalState(); error InvalidTiebreakerActivationTimeoutBounds(); @@ -203,21 +201,6 @@ contract DualGovernance is IDualGovernance { TIMELOCK.schedule(proposalId); } - /// @notice Executes a proposal previously scheduled for execution in the Dual Governance system. - /// The proposal can only be executed when the `ITimelock.getAfterScheduleDelay()` has passed and proposal - /// wasn't cancelled earlier. - /// @param proposalId The unique identifier of the proposal to be executed. This ID is obtained when the proposal - /// is initially submitted to the Dual Governance system. - function executeProposal(uint256 proposalId) external { - _stateMachine.activateNextState(ESCROW_MASTER_COPY); - - if (!TIMELOCK.canExecute(proposalId)) { - revert ProposalExecutionBlocked(proposalId); - } - - TIMELOCK.execute(proposalId); - } - /// @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. @@ -275,19 +258,6 @@ contract DualGovernance is IDualGovernance { && TIMELOCK.canSchedule(proposalId); } - /// @notice Returns whether a previously scheduled proposal can be executed based on the proposal's submission time, - /// and its current status. - /// @dev Proposal execution is allowed only if all the following conditions are met: - /// - The proposal has already been scheduled. - /// - The required delay period, as defined by `ITimelock.getAfterScheduleDelay()`, has elapsed since the proposal - /// was scheduled. - /// @param proposalId The unique identifier of the proposal to check. - /// @return canExecuteProposal A boolean value indicating whether the proposal can be executed (`true`) or - /// not (`false`) based on the proposal's status. - function canExecuteProposal(uint256 proposalId) external view returns (bool) { - return TIMELOCK.canExecute(proposalId); - } - /// @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. diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 526f77e6..24833d07 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -29,7 +29,6 @@ interface IDualGovernance is IGovernance, ITiebreaker { function ESCROW_MASTER_COPY() external view returns (IEscrow); function canSubmitProposal() external view returns (bool); - function canExecuteProposal(uint256 proposalId) external view returns (bool); function canCancelAllPendingProposals() external view returns (bool); function activateNextState() external; function setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) external; diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol index e0e80d4c..6ddbb476 100644 --- a/contracts/interfaces/IEscrow.sol +++ b/contracts/interfaces/IEscrow.sol @@ -57,7 +57,7 @@ interface IEscrow { function getVetoerState(address vetoer) external view returns (VetoerState memory state); function getUnclaimedUnstETHIdsCount() external view returns (uint256); function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds); - function isWithdrawalsBatchesFinalized() external view returns (bool); + function isWithdrawalsBatchesClosed() external view returns (bool); function isRageQuitExtensionPeriodStarted() external view returns (bool); function getRageQuitExtensionPeriodStartedAt() external view returns (Timestamp); diff --git a/contracts/interfaces/IGovernance.sol b/contracts/interfaces/IGovernance.sol index 75b4a62a..51652b40 100644 --- a/contracts/interfaces/IGovernance.sol +++ b/contracts/interfaces/IGovernance.sol @@ -12,7 +12,6 @@ interface IGovernance { string calldata metadata ) external returns (uint256 proposalId); function scheduleProposal(uint256 proposalId) external; - function executeProposal(uint256 proposalId) external; function cancelAllPendingProposals() external returns (bool); function canScheduleProposal(uint256 proposalId) external view returns (bool); diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index da94fda1..f94fe3e6 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -44,6 +44,7 @@ interface ITimelock { function getAfterSubmitDelay() external view returns (Duration); function getAfterScheduleDelay() external view returns (Duration); - function setupDelays(Duration afterSubmitDelay, Duration afterScheduleDelay) external; + function setAfterSubmitDelay(Duration newAfterSubmitDelay) external; + function setAfterScheduleDelay(Duration newAfterScheduleDelay) external; function transferExecutorOwnership(address executor, address owner) external; } diff --git a/test/mocks/EscrowMock.sol b/test/mocks/EscrowMock.sol index c56d4535..d01d69a8 100644 --- a/test/mocks/EscrowMock.sol +++ b/test/mocks/EscrowMock.sol @@ -103,7 +103,7 @@ contract EscrowMock is IEscrow { revert("Not implemented"); } - function isWithdrawalsBatchesFinalized() external view returns (bool) { + function isWithdrawalsBatchesClosed() external view returns (bool) { revert("Not implemented"); } diff --git a/test/mocks/TimelockMock.sol b/test/mocks/TimelockMock.sol index e09efbf0..7ae66eab 100644 --- a/test/mocks/TimelockMock.sol +++ b/test/mocks/TimelockMock.sol @@ -146,7 +146,11 @@ contract TimelockMock is ITimelock { revert("Not Implemented"); } - function setupDelays(Duration, /* afterSubmitDelay */ Duration /* afterScheduleDelay */ ) external { + function setAfterSubmitDelay(Duration /* newAfterSubmitDelay */ ) external { + revert("Not Implemented"); + } + + function setAfterScheduleDelay(Duration /* newAfterScheduleDelay */ ) external { revert("Not Implemented"); } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index d635bc11..78db21b3 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -318,61 +318,6 @@ contract DualGovernanceUnitTests is UnitTest { _dualGovernance.scheduleProposal(proposalId); } - // --- - // executeProposal() - // --- - - function testFuzz_executeProposal_HappyPath(address stranger) external { - uint256 proposalId = _dualGovernance.submitProposal(_generateExternalCalls(), ""); - - _scheduleProposal(proposalId, Timestamps.now()); - - uint256[] memory scheduledProposals = _timelock.getScheduledProposals(); - assertEq(scheduledProposals.length, 1); - assertEq(scheduledProposals[0], proposalId); - - _timelock.setExecutable(proposalId); - - vm.prank(stranger); - _dualGovernance.executeProposal(proposalId); - - assertEq(_timelock.getExecutedProposals().length, 1); - assertEq(_timelock.getExecutedProposals()[0], proposalId); - } - - function test_executeProposal_ActivatesNextState() external { - uint256 proposalId = _dualGovernance.submitProposal(_generateExternalCalls(), ""); - - _scheduleProposal(proposalId, Timestamps.now()); - assertEq(_timelock.getScheduledProposals().length, 1); - - vm.startPrank(vetoer); - _escrow.lockStETH(5 ether); - _wait(_configProvider.VETO_SIGNALLING_MIN_DURATION()); - _escrow.unlockStETH(); - vm.stopPrank(); - _wait(_configProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); - - _timelock.setExecutable(proposalId); - assertEq(_dualGovernance.getPersistedState(), State.VetoSignallingDeactivation); - - _dualGovernance.executeProposal(proposalId); - - assertEq(_dualGovernance.getPersistedState(), State.VetoCooldown); - } - - function test_executeProposal_RevertOn_CannotExecute() external { - uint256 proposalId = _dualGovernance.submitProposal(_generateExternalCalls(), ""); - _scheduleProposal(proposalId, Timestamps.now()); - - uint256[] memory scheduledProposals = _timelock.getScheduledProposals(); - assertEq(scheduledProposals.length, 1); - assertEq(scheduledProposals[0], proposalId); - - vm.expectRevert(abi.encodeWithSelector(DualGovernance.ProposalExecutionBlocked.selector, proposalId)); - _dualGovernance.executeProposal(proposalId); - } - // --- // cancelAllPendingProposals() // --- @@ -651,22 +596,6 @@ contract DualGovernanceUnitTests is UnitTest { assertTrue(_dualGovernance.canSubmitProposal()); } - // --- - // canExecuteProposal() - // --- - - function test_canExecuteProposal() external { - uint256 proposalId = _dualGovernance.submitProposal(_generateExternalCalls(), ""); - - _scheduleProposal(proposalId, Timestamps.now()); - assertEq(_timelock.getScheduledProposals().length, 1); - - assertFalse(_dualGovernance.canExecuteProposal(proposalId)); - - _timelock.setExecutable(proposalId); - assertTrue(_dualGovernance.canExecuteProposal(proposalId)); - } - // --- // canScheduleProposal() // --- diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index 2707003d..1e49c5d7 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -18,7 +18,6 @@ import {IStETH} from "contracts/interfaces/IStETH.sol"; import {IWstETH} from "contracts/interfaces/IWstETH.sol"; import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; -import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; import {StETHMock} from "test/mocks/StETHMock.sol"; import {WithdrawalQueueMock} from "test/mocks/WithdrawalQueueMock.sol"; @@ -144,11 +143,13 @@ contract EscrowUnitTests is UnitTest { function vetoerLockedUnstEth(uint256[] memory amounts) internal returns (uint256[] memory unstethIds) { unstethIds = new uint256[](amounts.length); - WithdrawalRequestStatus[] memory statuses = new WithdrawalRequestStatus[](amounts.length); + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](amounts.length); for (uint256 i = 0; i < amounts.length; ++i) { unstethIds[i] = i; - statuses[i] = WithdrawalRequestStatus(amounts[i], amounts[i], _vetoer, block.timestamp, false, false); + statuses[i] = + IWithdrawalQueue.WithdrawalRequestStatus(amounts[i], amounts[i], _vetoer, block.timestamp, false, false); } vm.mockCall( diff --git a/test/unit/TimelockedGovernance.t.sol b/test/unit/TimelockedGovernance.t.sol index e961f05d..8437bb55 100644 --- a/test/unit/TimelockedGovernance.t.sol +++ b/test/unit/TimelockedGovernance.t.sol @@ -82,6 +82,7 @@ contract TimelockedGovernanceUnitTests is UnitTest { _timelock.setSchedule(1); _timelockedGovernance.scheduleProposal(1); + _timelock.setExecutable(1); _timelock.execute(1); assertEq(_timelock.getExecutedProposals().length, 1); From eeede1a107595bd9e5badcdf72e56084e03fa6c6 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 29 Nov 2024 18:15:43 +0300 Subject: [PATCH 052/107] fix test and natspec --- contracts/Escrow.sol | 2 ++ test/unit/DualGovernance.t.sol | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 2ca6629b..d9f2adc9 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -442,6 +442,8 @@ contract Escrow is IEscrow { // Escrow Management // --- + /// @notice Returns 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. function getMinAssetsLockDuration() external view returns (Duration) { return _escrowState.minAssetsLockDuration; } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 86d4d55f..0f2466dd 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -958,12 +958,13 @@ contract DualGovernanceUnitTests is UnitTest { } function test_setConfigProvider_same_minAssetsLockDuration() external { + Duration newMinAssetsLockDuration = Durations.from(5 hours); ImmutableDualGovernanceConfigProvider newConfigProvider = new ImmutableDualGovernanceConfigProvider( DualGovernanceConfig.Context({ firstSealRageQuitSupport: PercentsD16.fromBasisPoints(5_00), // 5% secondSealRageQuitSupport: PercentsD16.fromBasisPoints(20_00), // 20% // - minAssetsLockDuration: Durations.from(5 hours), + minAssetsLockDuration: newMinAssetsLockDuration, // vetoSignallingMinDuration: Durations.from(4 days), vetoSignallingMaxDuration: Durations.from(35 days), @@ -982,11 +983,19 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernanceStateMachine.ConfigProviderSet(IDualGovernanceConfigProvider(address(newConfigProvider))); + vm.expectCall( + address(_escrow), + 0, + abi.encodeWithSelector(Escrow.setMinAssetsLockDuration.selector, newMinAssetsLockDuration), + 0 + ); + vm.recordLogs(); _executor.execute( address(_dualGovernance), 0, abi.encodeWithSelector(DualGovernance.setConfigProvider.selector, address(newConfigProvider)) ); + assertEq(vm.getRecordedLogs().length, 1); assertEq(address(_dualGovernance.getConfigProvider()), address(newConfigProvider)); assertTrue(address(_dualGovernance.getConfigProvider()) != address(oldConfigProvider)); From eed31d5ef3e347e1a1da9670139d46766b3ac509 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Sun, 27 Oct 2024 22:06:15 +0400 Subject: [PATCH 053/107] Remove unused AssetsAccounting.getLockedAssetsTotal getter --- contracts/libraries/AssetsAccounting.sol | 13 -------- test/unit/libraries/AssetsAccounting.t.sol | 36 ---------------------- 2 files changed, 49 deletions(-) diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index f706a6ea..84c1ba53 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -341,19 +341,6 @@ library AssetsAccounting { emit UnstETHWithdrawn(unstETHIds, amountWithdrawn); } - // --- - // Getters - // --- - - function getLockedAssetsTotals(Context storage self) - internal - view - returns (SharesValue unfinalizedShares, ETHValue finalizedETH) - { - finalizedETH = self.unstETHTotals.finalizedETH; - unfinalizedShares = self.stETHTotals.lockedShares + self.unstETHTotals.unfinalizedShares; - } - // --- // Checks // --- diff --git a/test/unit/libraries/AssetsAccounting.t.sol b/test/unit/libraries/AssetsAccounting.t.sol index 88dd8f52..7fce5dae 100644 --- a/test/unit/libraries/AssetsAccounting.t.sol +++ b/test/unit/libraries/AssetsAccounting.t.sol @@ -1419,42 +1419,6 @@ contract AssetsAccountingUnitTests is UnitTest { AssetsAccounting.accountUnstETHWithdraw(_accountingContext, holder, unstETHIds); } - // --- - // getLockedAssetsTotals - // --- - - function testFuzz_getLockedAssetsTotals_happyPath( - ETHValue totalFinalizedETH, - SharesValue totalLockedShares, - SharesValue totalUnfinalizedShares - ) external { - vm.assume(totalFinalizedETH.toUint256() < type(uint96).max); - vm.assume(totalLockedShares.toUint256() < type(uint96).max); - vm.assume(totalUnfinalizedShares.toUint256() < type(uint96).max); - _accountingContext.unstETHTotals.finalizedETH = totalFinalizedETH; - _accountingContext.unstETHTotals.unfinalizedShares = totalUnfinalizedShares; - _accountingContext.stETHTotals.lockedShares = totalLockedShares; - - (SharesValue unfinalizedShares, ETHValue finalizedETH) = - AssetsAccounting.getLockedAssetsTotals(_accountingContext); - - assertEq(unfinalizedShares, totalLockedShares + totalUnfinalizedShares); - assertEq(finalizedETH, totalFinalizedETH); - } - - function test_getLockedAssetsTotals_RevertOn_UnfinalizedSharesOverflow() external { - ETHValue totalFinalizedETH = ETHValues.from(1); - SharesValue totalUnfinalizedShares = SharesValues.from(type(uint128).max - 1); - SharesValue totalLockedShares = SharesValues.from(type(uint128).max - 1); - - _accountingContext.unstETHTotals.finalizedETH = totalFinalizedETH; - _accountingContext.unstETHTotals.unfinalizedShares = totalUnfinalizedShares; - _accountingContext.stETHTotals.lockedShares = totalLockedShares; - - vm.expectRevert(SharesValueOverflow.selector); - AssetsAccounting.getLockedAssetsTotals(_accountingContext); - } - // --- // checkMinAssetsLockDurationPassed // --- From f41b8709dbb542d23f21bc1bd6e72c6b26463b98 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 29 Nov 2024 20:01:35 +0400 Subject: [PATCH 054/107] Fix merge errors --- test/scenario/dg-update-tokens-rotation.t.sol | 2 +- test/scenario/time-sensitive-proposal-execution.t.sol | 6 +++--- test/unit/DualGovernance.t.sol | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/scenario/dg-update-tokens-rotation.t.sol b/test/scenario/dg-update-tokens-rotation.t.sol index b99db258..97f98191 100644 --- a/test/scenario/dg-update-tokens-rotation.t.sol +++ b/test/scenario/dg-update-tokens-rotation.t.sol @@ -96,7 +96,7 @@ contract DualGovernanceUpdateTokensRotation is ScenarioTestBlueprint { // The Rage Quit may be finished in the previous DG instance so vetoers will not lose their funds by mistake Escrow rageQuitEscrow = Escrow(payable(_dualGovernance.getRageQuitEscrow())); - while (!rageQuitEscrow.isWithdrawalsBatchesFinalized()) { + while (!rageQuitEscrow.isWithdrawalsBatchesClosed()) { rageQuitEscrow.requestNextWithdrawalsBatch(96); } diff --git a/test/scenario/time-sensitive-proposal-execution.t.sol b/test/scenario/time-sensitive-proposal-execution.t.sol index a3caa751..e5e5e64b 100644 --- a/test/scenario/time-sensitive-proposal-execution.t.sol +++ b/test/scenario/time-sensitive-proposal-execution.t.sol @@ -15,7 +15,7 @@ interface ITimeSensitiveContract { contract ScheduledProposalExecution is ScenarioTestBlueprint { TimeConstraints private immutable _TIME_CONSTRAINTS = new TimeConstraints(); - Duration private immutable _MIN_EXECUTION_DELAY = Durations.from(30 days); // Proposal may be executed not earlier than the 30 days from launch + Duration private immutable _EXECUTION_DELAY = Durations.from(30 days); // Proposal may be executed not earlier than the 30 days from launch Duration private immutable _EXECUTION_START_DAY_TIME = Durations.from(4 hours); // And at time frame starting from the 4:00 UTC Duration private immutable _EXECUTION_END_DAY_TIME = Durations.from(12 hours); // till the 12:00 UTC @@ -24,7 +24,7 @@ contract ScheduledProposalExecution is ScenarioTestBlueprint { } function testFork_TimeFrameProposalExecution() external { - Timestamp executableAfter = _MIN_EXECUTION_DELAY.addTo(Timestamps.now()); + Timestamp executableAfter = _EXECUTION_DELAY.addTo(Timestamps.now()); // Prepare the call to be launched not earlier than the minExecutionDelay seconds from the creation of the // Aragon Voting to submit proposal and only in the day time range [executionStartDayTime, executionEndDayTime] in UTC ExternalCall[] memory scheduledProposalCalls = ExternalCallHelpers.create( @@ -79,7 +79,7 @@ contract ScheduledProposalExecution is ScenarioTestBlueprint { _step("4. Wait until the proposal become executable"); { - _wait(_MIN_EXECUTION_DELAY); + _wait(_EXECUTION_DELAY); assertTrue(Timestamps.now() >= executableAfter); } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 958027b9..caad1759 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -2141,7 +2141,7 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); vm.prank(notResealCommittee); - vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotResealCommittee.selector, notResealCommittee)); + vm.expectRevert(abi.encodeWithSelector(Resealer.CallerIsNotResealCommittee.selector, notResealCommittee)); _dualGovernance.resealSealable(sealable); } From 10ec606aa59d1e3095709cd2d35f116af5762d54 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 28 Oct 2024 02:26:22 +0400 Subject: [PATCH 055/107] Move checkCallerIsAdminExecutor into TimelockState --- contracts/EmergencyProtectedTimelock.sol | 38 +++++++--------------- contracts/libraries/TimelockState.sol | 21 +++++++++++- test/unit/EmergencyProtectedTimelock.t.sol | 20 ++++++------ 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index e97a79e8..55e4b69b 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -21,12 +21,6 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { using ExecutableProposals for ExecutableProposals.Context; using EmergencyProtection for EmergencyProtection.Context; - // --- - // Errors - // --- - - error CallerIsNotAdminExecutor(address value); - // --- // Sanity Check Parameters & Immutables // --- @@ -150,7 +144,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @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.checkCallerIsAdminExecutor(); _timelockState.setGovernance(newGovernance); _proposals.cancelAll(); } @@ -159,7 +153,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// Ensures that the new delay value complies with the defined sanity check bounds. /// @param newAfterSubmitDelay The delay required before a submitted proposal can be scheduled. function setAfterSubmitDelay(Duration newAfterSubmitDelay) external { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); _timelockState.setAfterSubmitDelay(newAfterSubmitDelay, MAX_AFTER_SUBMIT_DELAY); _timelockState.checkExecutionDelay(MIN_EXECUTION_DELAY); } @@ -168,7 +162,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// Ensures that the new delay value complies with the defined sanity check bounds. /// @param newAfterScheduleDelay The delay required before a scheduled proposal can be executed. function setAfterScheduleDelay(Duration newAfterScheduleDelay) external { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); _timelockState.setAfterScheduleDelay(newAfterScheduleDelay, MAX_AFTER_SCHEDULE_DELAY); _timelockState.checkExecutionDelay(MIN_EXECUTION_DELAY); } @@ -177,7 +171,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @param executor The address of the executor contract. /// @param owner The address of the new owner. function transferExecutorOwnership(address executor, address owner) external { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); IOwnable(executor).transferOwnership(owner); } @@ -188,21 +182,21 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @notice Sets the emergency activation committee address. /// @param emergencyActivationCommittee The address of the emergency activation committee. function setEmergencyProtectionActivationCommittee(address emergencyActivationCommittee) external { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyActivationCommittee(emergencyActivationCommittee); } /// @notice Sets the emergency execution committee address. /// @param emergencyExecutionCommittee The address of the emergency execution committee. function setEmergencyProtectionExecutionCommittee(address emergencyExecutionCommittee) external { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyExecutionCommittee(emergencyExecutionCommittee); } /// @notice Sets the emergency protection end date. /// @param emergencyProtectionEndDate The timestamp of the emergency protection end date. function setEmergencyProtectionEndDate(Timestamp emergencyProtectionEndDate) external { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyProtectionEndDate( emergencyProtectionEndDate, MAX_EMERGENCY_PROTECTION_DURATION ); @@ -211,14 +205,14 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @notice Sets the emergency mode duration. /// @param emergencyModeDuration The duration of the emergency mode. function setEmergencyModeDuration(Duration emergencyModeDuration) external { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyModeDuration(emergencyModeDuration, MAX_EMERGENCY_MODE_DURATION); } /// @notice Sets the emergency governance address. /// @param emergencyGovernance The address of the emergency governance. function setEmergencyGovernance(address emergencyGovernance) external { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyGovernance(emergencyGovernance); } @@ -241,7 +235,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { function deactivateEmergencyMode() external { _emergencyProtection.checkEmergencyMode({isActive: true}); if (!_emergencyProtection.isEmergencyModeDurationPassed()) { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); } _emergencyProtection.deactivateEmergencyMode(); _proposals.cancelAll(); @@ -388,17 +382,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @notice Sets the address of the admin executor. /// @param newAdminExecutor The address of the new admin executor. function setAdminExecutor(address newAdminExecutor) external { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); _timelockState.setAdminExecutor(newAdminExecutor); } - - // --- - // Internal Methods - // --- - - function _checkCallerIsAdminExecutor() internal view { - if (msg.sender != _timelockState.adminExecutor) { - revert CallerIsNotAdminExecutor(msg.sender); - } - } } diff --git a/contracts/libraries/TimelockState.sol b/contracts/libraries/TimelockState.sol index 19258615..098edc69 100644 --- a/contracts/libraries/TimelockState.sol +++ b/contracts/libraries/TimelockState.sol @@ -12,6 +12,7 @@ library TimelockState { error CallerIsNotGovernance(address caller); error InvalidGovernance(address governance); + error CallerIsNotAdminExecutor(address caller); error InvalidAdminExecutor(address adminExecutor); error InvalidExecutionDelay(Duration executionDelay); error InvalidAfterSubmitDelay(Duration afterSubmitDelay); @@ -42,7 +43,7 @@ library TimelockState { } // --- - // Main Functionality + // State Management // --- /// @notice Sets the governance address. @@ -102,6 +103,10 @@ library TimelockState { emit AdminExecutorSet(newAdminExecutor); } + // --- + // Getters + // --- + /// @notice Retrieves the delay period required after a proposal is submitted before it can be scheduled. /// @param self The context of the Timelock State library. /// @return Duration The current after submit delay. @@ -116,6 +121,10 @@ library TimelockState { return self.afterScheduleDelay; } + // --- + // Checks + // --- + /// @notice Checks if the caller is the governance address, reverting if not. /// @param self The context of the Timelock State library. function checkCallerIsGovernance(Context storage self) internal view { @@ -133,4 +142,14 @@ library TimelockState { revert InvalidExecutionDelay(executionDelay); } } + + /// @notice Ensures that the caller is the designated admin executor. + /// @param self The context of the calling contract. + function checkCallerIsAdminExecutor(Context storage self) internal view { + if (self.adminExecutor != msg.sender) { + revert CallerIsNotAdminExecutor(msg.sender); + } + } } + + diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index fd41500f..3f7823ef 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -359,7 +359,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { Duration newAfterSubmitDelay = _defaultSanityCheckParams.maxAfterSubmitDelay + Durations.from(1 seconds); vm.expectRevert( - abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, address(this)) + abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, address(this)) ); _timelock.setAfterSubmitDelay(newAfterSubmitDelay); } @@ -431,7 +431,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { Duration newAfterScheduleDelay = _defaultSanityCheckParams.maxAfterScheduleDelay + Durations.from(1 seconds); vm.expectRevert( - abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, address(this)) + abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, address(this)) ); _timelock.setAfterScheduleDelay(newAfterScheduleDelay); } @@ -487,7 +487,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); _timelock.transferExecutorOwnership(_adminExecutor, makeAddr("newOwner")); } @@ -533,7 +533,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); _timelock.setGovernance(makeAddr("newGovernance")); } @@ -702,7 +702,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), true); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); _timelock.deactivateEmergencyMode(); } @@ -800,7 +800,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); vm.prank(stranger); _localTimelock.setEmergencyProtectionActivationCommittee(_emergencyActivator); @@ -825,7 +825,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor && stranger != _emergencyEnactor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); vm.prank(stranger); _localTimelock.setEmergencyProtectionExecutionCommittee(_emergencyEnactor); @@ -853,7 +853,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); vm.prank(stranger); _localTimelock.setEmergencyProtectionEndDate(_emergencyProtectionDuration.addTo(Timestamps.now())); @@ -884,7 +884,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); vm.prank(stranger); _localTimelock.setEmergencyModeDuration(_emergencyModeDuration); @@ -1250,7 +1250,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); _timelock.setAdminExecutor(address(0x123)); } From 8d12fa4ab44ac1acbc0eb7050509ad11dced1635 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 28 Oct 2024 04:05:35 +0400 Subject: [PATCH 056/107] Include only contracts dir to coverage report --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4bc6a503..f8eb831c 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "lint": "solhint \"addresses/**/*.sol\" \"contracts/**/*.sol\" \"scripts/**/*.sol\" \"test/**/*.sol\" --ignore-path .solhintignore", "coverage": "forge coverage", "precov-report": "mkdir -p ./coverage-report && forge coverage --report lcov --report-file ./coverage-report/lcov.info", - "cov-report": "genhtml ./coverage-report/lcov.info --rc derive_function_end_line=0 --rc branch_coverage=1 -o coverage-report --exclude test --ignore-errors inconsistent --ignore-errors category" + "cov-report": "genhtml ./coverage-report/lcov.info --rc derive_function_end_line=0 --rc branch_coverage=1 -o coverage-report --include contracts --ignore-errors inconsistent --ignore-errors category" }, "lint-staged": { "*.sol": [ From 36546682e86d8c9c3f91dcb644c52232953b43c1 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 28 Oct 2024 02:10:58 +0400 Subject: [PATCH 057/107] Add separate flag to manage proposals cancelling permission --- contracts/DualGovernance.sol | 58 +++++++-- contracts/interfaces/IDualGovernance.sol | 2 +- contracts/libraries/Proposers.sol | 123 ++++++++++++++---- scripts/deploy/ContractsDeployment.sol | 3 +- test/scenario/dg-update-tokens-rotation.t.sol | 2 +- test/scenario/happy-path-plan-b.t.sol | 8 +- test/scenario/timelocked-governance.t.sol | 4 +- test/unit/DualGovernance.t.sol | 32 ++--- test/unit/libraries/Proposers.t.sol | 52 ++++---- 9 files changed, 198 insertions(+), 86 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index c5811848..6edf0e7e 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -38,13 +38,13 @@ contract DualGovernance is IDualGovernance { // Errors // --- - error NotAdminProposer(); error UnownedAdminExecutor(); error CallerIsNotAdminExecutor(address caller); error ProposalSubmissionBlocked(); error ProposalSchedulingBlocked(uint256 proposalId); error ResealIsNotAllowedInNormalState(); error InvalidTiebreakerActivationTimeoutBounds(); + error ProposerNotPermittedToCancelProposals(address proposer); // --- // Events @@ -201,7 +201,7 @@ contract DualGovernance is IDualGovernance { TIMELOCK.schedule(proposalId); } - /// @notice Allows a proposer associated with the admin executor to cancel all previously submitted or scheduled + /// @notice Allows authorized proposer 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 @@ -212,8 +212,8 @@ contract DualGovernance is IDualGovernance { _stateMachine.activateNextState(ESCROW_MASTER_COPY); Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); - if (proposer.executor != TIMELOCK.getAdminExecutor()) { - revert NotAdminProposer(); + if (!proposer.canCancelProposals) { + revert ProposerNotPermittedToCancelProposals(msg.sender); } if (!_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})) { @@ -342,25 +342,59 @@ contract DualGovernance is IDualGovernance { /// @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 proposerAccount 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 { + /// @param canCancelProposals Indicates if the proposer has authority to cancel all proposals that are + /// submitted or scheduled but not yet executed. + function registerProposer(address proposerAccount, address executor, bool canCancelProposals) external { _checkCallerIsAdminExecutor(); - _proposers.register(proposer, executor); + _proposers.register(proposerAccount, executor, canCancelProposals); + } + + /// @notice Grants the specified proposer the permission to cancel all proposals that have been submitted + /// or scheduled but are not yet executed. + /// @param proposerAccount The address of the proposer to grant proposal-canceling permission. + function grantProposalsCancellingPermission(address proposerAccount) external { + _checkCallerIsAdminExecutor(); + _proposers.setCanCancelProposals(proposerAccount, true); + } + + /// @notice Revokes the permission to cancel proposals from the specified proposer. + /// @param proposerAccount The address of the proposer from whom to revoke proposal-canceling permission. + function revokeProposalsCancellingPermission(address proposerAccount) external { + _checkCallerIsAdminExecutor(); + _proposers.setCanCancelProposals(proposerAccount, false); + } + + /// @notice Allows the caller to renounce their proposal-canceling permission. + function renounceProposalsCancellingPermission() external { + _proposers.setCanCancelProposals(msg.sender, false); + } + + /// @notice Updates the executor associated with a specified proposer. + /// @dev Ensures that at least one proposer remains assigned to the `adminExecutor` following the update. + /// Reverts if updating the proposer’s executor would leave the `adminExecutor` without any associated proposer. + /// @param proposerAccount The address of the proposer whose executor is being updated. + /// @param executor The new executor address to assign to the proposer. + function setProposerExecutor(address proposerAccount, address executor) external { + _checkCallerIsAdminExecutor(); + _proposers.setProposerExecutor(proposerAccount, executor); + + /// @dev after update of the proposer, check that admin executor still belongs to some proposer + _proposers.checkRegisteredExecutor(TIMELOCK.getAdminExecutor()); } /// @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. + /// @dev Ensures that at least one proposer remains associated with the `adminExecutor`. If an attempt is made to + /// remove the last proposer assigned to the `adminExecutor`, the function will revert. /// @param proposer The address of the proposer to unregister. function unregisterProposer(address proposer) external { _checkCallerIsAdminExecutor(); + _proposers.unregister(proposer); /// @dev after the removal of the proposer, check that admin executor still belongs to some proposer - if (!_proposers.isExecutor(msg.sender)) { - revert UnownedAdminExecutor(); - } + _proposers.checkRegisteredExecutor(TIMELOCK.getAdminExecutor()); } /// @notice Checks whether the given `account` is a registered proposer. diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 24833d07..87db8a67 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -39,7 +39,7 @@ interface IDualGovernance is IGovernance, ITiebreaker { function getEffectiveState() external view returns (State effectiveState); function getStateDetails() external view returns (StateDetails memory stateDetails); - function registerProposer(address proposer, address executor) external; + function registerProposer(address proposer, address executor, bool canCancelProposals) external; function unregisterProposer(address proposer) external; function isProposer(address account) external view returns (bool); function getProposer(address account) external view returns (Proposers.Proposer memory proposer); diff --git a/contracts/libraries/Proposers.sol b/contracts/libraries/Proposers.sol index a8d45ba1..87371cf5 100644 --- a/contracts/libraries/Proposers.sol +++ b/contracts/libraries/Proposers.sol @@ -11,6 +11,7 @@ library Proposers { // --- error InvalidExecutor(address executor); error InvalidProposerAccount(address account); + error ExecutorNotRegistered(address account); error ProposerNotRegistered(address proposer); error ProposerAlreadyRegistered(address proposer); @@ -19,6 +20,8 @@ library Proposers { // --- event ProposerRegistered(address indexed proposer, address indexed executor); + event ProposerExecutorSet(address indexed proposer, address indexed executor); + event ProposerCanCancelProposalsSet(address indexed proposer, bool canCancelProposals); event ProposerUnregistered(address indexed proposer, address indexed executor); // --- @@ -28,30 +31,36 @@ library Proposers { /// @notice The info about the registered proposer and associated executor. /// @param account Address of the proposer. /// @param executor The address of the executor assigned to execute proposals submitted by the proposer. + /// @param canCancelProposals Indicates whether the proposer has the authority to cancel all proposals that are + /// submitted or scheduled but not yet executed. struct Proposer { address account; address executor; + bool canCancelProposals; } - /// @notice Internal information about a proposer’s executor. + /// @notice Internal information about a proposer’s executor and proposal cancellation permissions. /// @param proposerIndex The one-based index of the proposer associated with the `executor` from /// the `Context.proposers` array. /// @param executor The address of the executor associated with the proposer. - struct ExecutorData { + /// @param canCancelProposals Indicates if the proposer has authority to cancel all proposals that are + /// submitted or scheduled but not yet executed. + struct ProposerData { /// @dev slot0: [0..31] IndexOneBased proposerIndex; /// @dev slot0: [32..191] address executor; + /// @dev slot0: [192..192] + bool canCancelProposals; } /// @notice The context of the Proposers library. /// @param proposers List of all registered proposers. - /// @param executors Mapping of proposers to their executor data. - /// @param executorRefsCounts Mapping of executors to the count of proposers associated - /// with each executor. + /// @param proposersData A mapping that associates each proposer’s address with their respective proposer data. + /// @param executorRefsCounts Mapping of executors to the count of proposers associated with each executor. struct Context { address[] proposers; - mapping(address proposer => ExecutorData) executors; + mapping(address proposer => ProposerData) proposersData; mapping(address executor => uint256 usagesCount) executorRefsCounts; } @@ -63,7 +72,14 @@ library Proposers { /// @param self The context of the Proposers library. /// @param proposerAccount The address of the proposer to register. /// @param executor The address of the executor to assign to the proposer. - function register(Context storage self, address proposerAccount, address executor) internal { + /// @param canCancelProposals Indicates if the proposer has authority to cancel all proposals that are + /// submitted or scheduled but not yet executed. + function register( + Context storage self, + address proposerAccount, + address executor, + bool canCancelProposals + ) internal { if (proposerAccount == address(0)) { revert InvalidProposerAccount(proposerAccount); } @@ -72,41 +88,78 @@ library Proposers { revert InvalidExecutor(executor); } - if (_isRegisteredProposer(self.executors[proposerAccount])) { + if (_isRegisteredProposer(self.proposersData[proposerAccount])) { revert ProposerAlreadyRegistered(proposerAccount); } self.proposers.push(proposerAccount); - self.executors[proposerAccount] = - ExecutorData({proposerIndex: IndicesOneBased.fromOneBasedValue(self.proposers.length), executor: executor}); + self.proposersData[proposerAccount] = ProposerData({ + executor: executor, + canCancelProposals: canCancelProposals, + proposerIndex: IndicesOneBased.fromOneBasedValue(self.proposers.length) + }); self.executorRefsCounts[executor] += 1; emit ProposerRegistered(proposerAccount, executor); } + /// @notice Updates the cancellation permissions for a registered proposer. + /// @param self The context storage of the Proposers library. + /// @param proposerAccount The address of the proposer to update permission to cancel proposals. + /// @param canCancelProposals A boolean indicating whether the proposer has permission to cancel proposals. + function setCanCancelProposals(Context storage self, address proposerAccount, bool canCancelProposals) internal { + ProposerData memory proposerData = self.proposersData[proposerAccount]; + _checkRegisteredProposer(proposerAccount, proposerData); + + if (proposerData.canCancelProposals != canCancelProposals) { + self.proposersData[proposerAccount].canCancelProposals = canCancelProposals; + emit ProposerCanCancelProposalsSet(proposerAccount, canCancelProposals); + } + } + + /// @notice Updates the executor for a registered proposer. + /// @param self The context storage of the Proposers library. + /// @param proposerAccount The address of the proposer to update. + /// @param executor The new executor address to assign to the proposer. + function setProposerExecutor(Context storage self, address proposerAccount, address executor) internal { + ProposerData memory proposerData = self.proposersData[proposerAccount]; + _checkRegisteredProposer(proposerAccount, proposerData); + + if (executor == address(0) || proposerData.executor == executor) { + revert InvalidExecutor(executor); + } + + self.proposersData[proposerAccount].executor = executor; + + self.executorRefsCounts[executor] += 1; + self.executorRefsCounts[proposerData.executor] -= 1; + + emit ProposerExecutorSet(proposerAccount, executor); + } + /// @notice Unregisters a proposer, removing its association with an executor. /// @param self The context of the Proposers library. /// @param proposerAccount The address of the proposer to unregister. function unregister(Context storage self, address proposerAccount) internal { - ExecutorData memory executorData = self.executors[proposerAccount]; + ProposerData memory proposerData = self.proposersData[proposerAccount]; - _checkRegisteredProposer(proposerAccount, executorData); + _checkRegisteredProposer(proposerAccount, proposerData); IndexOneBased lastProposerIndex = IndicesOneBased.fromOneBasedValue(self.proposers.length); - IndexOneBased proposerIndex = executorData.proposerIndex; + IndexOneBased proposerIndex = proposerData.proposerIndex; if (proposerIndex != lastProposerIndex) { address lastProposer = self.proposers[lastProposerIndex.toZeroBasedValue()]; self.proposers[proposerIndex.toZeroBasedValue()] = lastProposer; - self.executors[lastProposer].proposerIndex = proposerIndex; + self.proposersData[lastProposer].proposerIndex = proposerIndex; } self.proposers.pop(); - delete self.executors[proposerAccount]; + delete self.proposersData[proposerAccount]; - self.executorRefsCounts[executorData.executor] -= 1; + self.executorRefsCounts[proposerData.executor] -= 1; - emit ProposerUnregistered(proposerAccount, executorData.executor); + emit ProposerUnregistered(proposerAccount, proposerData.executor); } // --- @@ -121,11 +174,12 @@ library Proposers { Context storage self, address proposerAccount ) internal view returns (Proposer memory proposer) { - ExecutorData memory executorData = self.executors[proposerAccount]; - _checkRegisteredProposer(proposerAccount, executorData); + ProposerData memory proposerData = self.proposersData[proposerAccount]; + _checkRegisteredProposer(proposerAccount, proposerData); proposer.account = proposerAccount; - proposer.executor = executorData.executor; + proposer.executor = proposerData.executor; + proposer.canCancelProposals = proposerData.canCancelProposals; } /// @notice Retrieves all registered proposers. @@ -143,7 +197,7 @@ library Proposers { /// @param account The address to check. /// @return bool `true` if the account is a registered proposer, otherwise `false`. function isProposer(Context storage self, address account) internal view returns (bool) { - return _isRegisteredProposer(self.executors[account]); + return _isRegisteredProposer(self.proposersData[account]); } /// @notice Checks if an account is an executor associated with any proposer. @@ -154,15 +208,30 @@ library Proposers { return self.executorRefsCounts[account] > 0; } - /// @notice Checks that the given proposer is registered. - function _checkRegisteredProposer(address proposerAccount, ExecutorData memory executorData) internal pure { - if (!_isRegisteredProposer(executorData)) { + // --- + // Checks + // --- + + /// @notice Checks that a given account is a registered executor. + /// @param self The storage context of the Proposers library. + /// @param account The address to verify as a registered executor. + function checkRegisteredExecutor(Context storage self, address account) internal view { + if (!isExecutor(self, account)) { + revert ExecutorNotRegistered(account); + } + } + + // --- + // Private Methods + // --- + + function _checkRegisteredProposer(address proposerAccount, ProposerData memory proposerData) internal pure { + if (!_isRegisteredProposer(proposerData)) { revert ProposerNotRegistered(proposerAccount); } } - /// @notice Checks if the given executor data belongs to a registered proposer. - function _isRegisteredProposer(ExecutorData memory executorData) internal pure returns (bool) { - return executorData.proposerIndex.isNotEmpty(); + function _isRegisteredProposer(ProposerData memory proposerData) internal pure returns (bool) { + return proposerData.proposerIndex.isNotEmpty(); } } diff --git a/scripts/deploy/ContractsDeployment.sol b/scripts/deploy/ContractsDeployment.sol index 25f1ea4e..f8de105d 100644 --- a/scripts/deploy/ContractsDeployment.sol +++ b/scripts/deploy/ContractsDeployment.sol @@ -289,7 +289,8 @@ library DGContractsDeployment { address(contracts.dualGovernance), 0, abi.encodeCall( - contracts.dualGovernance.registerProposer, (lidoAddresses.voting, address(contracts.adminExecutor)) + contracts.dualGovernance.registerProposer, + (address(lidoAddresses.voting), address(contracts.adminExecutor), true) ) ); contracts.adminExecutor.execute( diff --git a/test/scenario/dg-update-tokens-rotation.t.sol b/test/scenario/dg-update-tokens-rotation.t.sol index 97f98191..974a4c86 100644 --- a/test/scenario/dg-update-tokens-rotation.t.sol +++ b/test/scenario/dg-update-tokens-rotation.t.sol @@ -219,7 +219,7 @@ contract DualGovernanceUpdateTokensRotation is ScenarioTestBlueprint { value: 0, target: address(newDualGovernanceInstance), payload: abi.encodeCall( - DualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor()) + DualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor(), true) ) }), ExternalCall({ diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index 6681778d..7c537148 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -132,7 +132,9 @@ contract PlanBSetup is ScenarioTestBlueprint { address(_timelock) ], [ - abi.encodeCall(_dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor())), + abi.encodeCall( + _dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor(), true) + ), // Only Dual Governance contract can call the Timelock contract abi.encodeCall(_timelock.setGovernance, (address(_dualGovernance))), // Now the emergency mode may be deactivated (all scheduled calls will be canceled) @@ -216,7 +218,9 @@ contract PlanBSetup is ScenarioTestBlueprint { ExternalCall[] memory dualGovernanceUpdateCalls = ExternalCallHelpers.create( [address(dualGovernanceV2), address(_timelock), address(_timelock), address(_timelock)], [ - abi.encodeCall(_dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor())), + abi.encodeCall( + _dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor(), true) + ), // Update the controller for timelock abi.encodeCall(_timelock.setGovernance, address(dualGovernanceV2)), // Assembly the emergency committee again, until the new version of Dual Governance is battle tested diff --git a/test/scenario/timelocked-governance.t.sol b/test/scenario/timelocked-governance.t.sol index d6be5be5..793fb99c 100644 --- a/test/scenario/timelocked-governance.t.sol +++ b/test/scenario/timelocked-governance.t.sol @@ -226,7 +226,9 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { ExternalCall[] memory dualGovernanceLaunchCalls = ExternalCallHelpers.create( [address(_dualGovernance), address(_timelock)], [ - abi.encodeCall(_dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor())), + abi.encodeCall( + _dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor(), true) + ), abi.encodeCall(_timelock.setGovernance, (address(_dualGovernance))) ] ); diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index caad1759..240b18a6 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -95,7 +95,7 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(this), address(_executor)) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(this), address(_executor), true) ); _executor.execute( @@ -452,19 +452,21 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(_timelock.lastCancelledProposalId(), 1); } - function test_cancelAllPendingProposals_RevertOn_NotAdminProposer() external { + function test_cancelAllPendingProposals_RevertOn_ProposerCanNotCancelProposers() external { address nonAdminProposer = makeAddr("NON_ADMIN_PROPOSER"); _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, nonAdminProposer, address(0x123)) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, nonAdminProposer, address(0x123), false) ); _submitMockProposal(); assertEq(_timelock.getProposalsCount(), 1); vm.prank(nonAdminProposer); - vm.expectRevert(abi.encodeWithSelector(DualGovernance.NotAdminProposer.selector)); + vm.expectRevert( + abi.encodeWithSelector(DualGovernance.ProposerNotPermittedToCancelProposals.selector, nonAdminProposer) + ); _dualGovernance.cancelAllPendingProposals(); assertEq(_timelock.getProposalsCount(), 1); @@ -1304,7 +1306,7 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, newProposer, newExecutor) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, newProposer, newExecutor, true) ); assertTrue(_dualGovernance.isProposer(newProposer)); @@ -1322,7 +1324,7 @@ contract DualGovernanceUnitTests is UnitTest { vm.prank(stranger); vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, stranger)); - _dualGovernance.registerProposer(newProposer, newExecutor); + _dualGovernance.registerProposer(newProposer, newExecutor, true); } // --- @@ -1336,7 +1338,7 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor, true) ); assertTrue(_dualGovernance.isProposer(proposer)); @@ -1370,10 +1372,10 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor, true) ); - vm.expectRevert(abi.encodeWithSelector(DualGovernance.UnownedAdminExecutor.selector)); + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, adminExecutor)); _executor.execute( address(_dualGovernance), 0, @@ -1398,7 +1400,7 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor, true) ); assertTrue(_dualGovernance.isProposer(proposer)); @@ -1422,7 +1424,7 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor, true) ); Proposers.Proposer memory proposerData = _dualGovernance.getProposer(proposer); @@ -1452,17 +1454,17 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer1, proposerExecutor1) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer1, proposerExecutor1, true) ); _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer2, proposerExecutor2) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer2, proposerExecutor2, true) ); _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer3, proposerExecutor3) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer3, proposerExecutor3, true) ); Proposers.Proposer[] memory proposers = _dualGovernance.getProposers(); @@ -1489,7 +1491,7 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(0x123), executor) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(0x123), executor, true) ); assertTrue(_dualGovernance.isExecutor(executor)); diff --git a/test/unit/libraries/Proposers.t.sol b/test/unit/libraries/Proposers.t.sol index 7eb2ea42..c3079e4f 100644 --- a/test/unit/libraries/Proposers.t.sol +++ b/test/unit/libraries/Proposers.t.sol @@ -20,7 +20,7 @@ contract ProposersLibraryUnitTests is UnitTest { // --- function test_register_HappyPath() external { // adding admin proposer - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); Proposers.Proposer[] memory allProposers = _proposers.getAllProposers(); assertEq(allProposers.length, 1); @@ -28,7 +28,7 @@ contract ProposersLibraryUnitTests is UnitTest { assertEq(allProposers[0].executor, _ADMIN_EXECUTOR); // adding non admin proposer - _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR, true); allProposers = _proposers.getAllProposers(); assertEq(allProposers.length, 2); @@ -38,26 +38,26 @@ contract ProposersLibraryUnitTests is UnitTest { function test_register_RevertOn_InvalidProposerAccount() external { vm.expectRevert(abi.encodeWithSelector(Proposers.InvalidProposerAccount.selector, address(0))); - _proposers.register(address(0), _ADMIN_EXECUTOR); + _proposers.register(address(0), _ADMIN_EXECUTOR, true); } function test_register_RevertOn_InvalidExecutor() external { vm.expectRevert(abi.encodeWithSelector(Proposers.InvalidExecutor.selector, address(0))); - _proposers.register(_ADMIN_PROPOSER, address(0)); + _proposers.register(_ADMIN_PROPOSER, address(0), true); } function test_register_RevertOn_ProposerAlreadyRegistered() external { - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerAlreadyRegistered.selector, _ADMIN_PROPOSER)); - _proposers.register(_ADMIN_PROPOSER, _DEFAULT_EXECUTOR); + _proposers.register(_ADMIN_PROPOSER, _DEFAULT_EXECUTOR, true); } function test_register_Emit_ProposerRegistered() external { vm.expectEmit(true, true, true, false); emit Proposers.ProposerRegistered(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); } // --- @@ -65,13 +65,13 @@ contract ProposersLibraryUnitTests is UnitTest { // --- function test_unregister_HappyPath() external { - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); assertEq(_proposers.proposers.length, 1); assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); assertTrue(_proposers.isExecutor(_ADMIN_EXECUTOR)); - _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR, true); assertEq(_proposers.proposers.length, 2); assertTrue(_proposers.isProposer(_DEFAULT_PROPOSER)); assertTrue(_proposers.isExecutor(_DEFAULT_EXECUTOR)); @@ -93,7 +93,7 @@ contract ProposersLibraryUnitTests is UnitTest { vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); _proposers.unregister(_DEFAULT_PROPOSER); - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); @@ -103,7 +103,7 @@ contract ProposersLibraryUnitTests is UnitTest { } function test_uregister_Emit_ProposerUnregistered() external { - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); vm.expectEmit(true, true, true, false); @@ -117,14 +117,14 @@ contract ProposersLibraryUnitTests is UnitTest { // --- function test_getProposer_HappyPath() external { - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); Proposers.Proposer memory adminProposer = _proposers.getProposer(_ADMIN_PROPOSER); assertEq(adminProposer.account, _ADMIN_PROPOSER); assertEq(adminProposer.executor, _ADMIN_EXECUTOR); - _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR, true); assertTrue(_proposers.isProposer(_DEFAULT_PROPOSER)); Proposers.Proposer memory defaultProposer = _proposers.getProposer(_DEFAULT_PROPOSER); @@ -138,7 +138,7 @@ contract ProposersLibraryUnitTests is UnitTest { vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); _proposers.getProposer(_DEFAULT_PROPOSER); - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); @@ -154,7 +154,7 @@ contract ProposersLibraryUnitTests is UnitTest { Proposers.Proposer[] memory emptyProposers = _proposers.getAllProposers(); assertEq(emptyProposers.length, 0); - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); Proposers.Proposer[] memory allProposers = _proposers.getAllProposers(); @@ -163,7 +163,7 @@ contract ProposersLibraryUnitTests is UnitTest { assertEq(allProposers[0].account, _ADMIN_PROPOSER); assertEq(allProposers[0].executor, _ADMIN_EXECUTOR); - _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR, true); assertTrue(_proposers.isProposer(_DEFAULT_PROPOSER)); allProposers = _proposers.getAllProposers(); @@ -181,7 +181,7 @@ contract ProposersLibraryUnitTests is UnitTest { // --- function test_unregister_Spam() external { - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); address dravee = makeAddr("Dravee"); address draveeExecutor = makeAddr("draveeExecutor"); @@ -192,10 +192,10 @@ contract ProposersLibraryUnitTests is UnitTest { address bob = makeAddr("Bob"); address bobExecutor = makeAddr("bobExecutor"); - _proposers.register(alice, aliceExecutor); - _proposers.register(bob, bobExecutor); - _proposers.register(celine, celineExecutor); - _proposers.register(dravee, draveeExecutor); + _proposers.register(alice, aliceExecutor, false); + _proposers.register(bob, bobExecutor, true); + _proposers.register(celine, celineExecutor, false); + _proposers.register(dravee, draveeExecutor, true); _proposers.unregister(bob); _proposers.unregister(dravee); @@ -204,7 +204,7 @@ contract ProposersLibraryUnitTests is UnitTest { } function test_unregister_CorrectPosition() external { - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); address dravee = makeAddr("Dravee"); address draveeExecutor = makeAddr("draveeExecutor"); @@ -215,10 +215,10 @@ contract ProposersLibraryUnitTests is UnitTest { address bob = makeAddr("Bob"); address bobExecutor = makeAddr("bobExecutor"); - _proposers.register(alice, aliceExecutor); - _proposers.register(bob, bobExecutor); - _proposers.register(celine, celineExecutor); - _proposers.register(dravee, draveeExecutor); + _proposers.register(alice, aliceExecutor, false); + _proposers.register(bob, bobExecutor, true); + _proposers.register(celine, celineExecutor, false); + _proposers.register(dravee, draveeExecutor, true); _proposers.unregister(bob); From fa587f2ea585a73c0e58752b7541672a4588d089 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 29 Oct 2024 02:35:04 +0400 Subject: [PATCH 058/107] Single canceller address. Add tests --- contracts/DualGovernance.sol | 55 +++-- contracts/interfaces/IDualGovernance.sol | 2 +- contracts/libraries/Proposers.sol | 96 +++------ scripts/deploy/ContractsDeployment.sol | 7 +- test/scenario/dg-update-tokens-rotation.t.sol | 2 +- test/scenario/happy-path-plan-b.t.sol | 8 +- test/scenario/timelocked-governance.t.sol | 4 +- test/unit/DualGovernance.t.sol | 144 ++++++++++++- test/unit/libraries/Proposers.t.sol | 190 +++++++++++++++--- 9 files changed, 368 insertions(+), 140 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 6edf0e7e..af75a725 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -38,13 +38,13 @@ contract DualGovernance is IDualGovernance { // Errors // --- - error UnownedAdminExecutor(); error CallerIsNotAdminExecutor(address caller); + error CallerIsNotProposalsCanceller(address caller); + error InvalidProposalsCanceller(address canceller); error ProposalSubmissionBlocked(); error ProposalSchedulingBlocked(uint256 proposalId); error ResealIsNotAllowedInNormalState(); error InvalidTiebreakerActivationTimeoutBounds(); - error ProposerNotPermittedToCancelProposals(address proposer); // --- // Events @@ -53,6 +53,7 @@ contract DualGovernance is IDualGovernance { event CancelAllPendingProposalsSkipped(); event CancelAllPendingProposalsExecuted(); event EscrowMasterCopyDeployed(IEscrow escrowMasterCopy); + event ProposalsCancellerSet(address proposalsCanceller); // --- // Sanity Check Parameters & Immutables @@ -131,6 +132,10 @@ contract DualGovernance is IDualGovernance { /// @dev The functionality for sealing/resuming critical components of Lido protocol. Resealer.Context internal _resealer; + /// @dev The address authorized to call `cancelAllPendingProposals()`, allowing it to cancel all proposals that are + /// submitted or scheduled but not yet executed. + address internal _proposalsCanceller; + // --- // Constructor // --- @@ -211,9 +216,8 @@ contract DualGovernance is IDualGovernance { function cancelAllPendingProposals() external returns (bool) { _stateMachine.activateNextState(ESCROW_MASTER_COPY); - Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); - if (!proposer.canCancelProposals) { - revert ProposerNotPermittedToCancelProposals(msg.sender); + if (msg.sender != _proposalsCanceller) { + revert CallerIsNotProposalsCanceller(msg.sender); } if (!_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})) { @@ -288,6 +292,21 @@ contract DualGovernance is IDualGovernance { _stateMachine.setConfigProvider(newConfigProvider); } + function setProposalsCanceller(address newProposalsCanceller) external { + _checkCallerIsAdminExecutor(); + + if (newProposalsCanceller == address(0) || newProposalsCanceller == _proposalsCanceller) { + revert InvalidProposalsCanceller(newProposalsCanceller); + } + + _proposalsCanceller = newProposalsCanceller; + emit ProposalsCancellerSet(newProposalsCanceller); + } + + function getProposalsCanceller() external view returns (address) { + return _proposalsCanceller; + } + /// @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) { @@ -344,31 +363,9 @@ contract DualGovernance is IDualGovernance { /// @dev Multiple proposers can share the same executor contract, but each proposer must be unique. /// @param proposerAccount The address of the proposer to register. /// @param executor The address of the executor contract associated with the proposer. - /// @param canCancelProposals Indicates if the proposer has authority to cancel all proposals that are - /// submitted or scheduled but not yet executed. - function registerProposer(address proposerAccount, address executor, bool canCancelProposals) external { - _checkCallerIsAdminExecutor(); - _proposers.register(proposerAccount, executor, canCancelProposals); - } - - /// @notice Grants the specified proposer the permission to cancel all proposals that have been submitted - /// or scheduled but are not yet executed. - /// @param proposerAccount The address of the proposer to grant proposal-canceling permission. - function grantProposalsCancellingPermission(address proposerAccount) external { + function registerProposer(address proposerAccount, address executor) external { _checkCallerIsAdminExecutor(); - _proposers.setCanCancelProposals(proposerAccount, true); - } - - /// @notice Revokes the permission to cancel proposals from the specified proposer. - /// @param proposerAccount The address of the proposer from whom to revoke proposal-canceling permission. - function revokeProposalsCancellingPermission(address proposerAccount) external { - _checkCallerIsAdminExecutor(); - _proposers.setCanCancelProposals(proposerAccount, false); - } - - /// @notice Allows the caller to renounce their proposal-canceling permission. - function renounceProposalsCancellingPermission() external { - _proposers.setCanCancelProposals(msg.sender, false); + _proposers.register(proposerAccount, executor); } /// @notice Updates the executor associated with a specified proposer. diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 87db8a67..24833d07 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -39,7 +39,7 @@ interface IDualGovernance is IGovernance, ITiebreaker { function getEffectiveState() external view returns (State effectiveState); function getStateDetails() external view returns (StateDetails memory stateDetails); - function registerProposer(address proposer, address executor, bool canCancelProposals) external; + function registerProposer(address proposer, address executor) external; function unregisterProposer(address proposer) external; function isProposer(address account) external view returns (bool); function getProposer(address account) external view returns (Proposers.Proposer memory proposer); diff --git a/contracts/libraries/Proposers.sol b/contracts/libraries/Proposers.sol index 87371cf5..f8bcfeda 100644 --- a/contracts/libraries/Proposers.sol +++ b/contracts/libraries/Proposers.sol @@ -10,8 +10,8 @@ library Proposers { // Errors // --- error InvalidExecutor(address executor); - error InvalidProposerAccount(address account); error ExecutorNotRegistered(address account); + error InvalidProposerAccount(address account); error ProposerNotRegistered(address proposer); error ProposerAlreadyRegistered(address proposer); @@ -21,7 +21,6 @@ library Proposers { event ProposerRegistered(address indexed proposer, address indexed executor); event ProposerExecutorSet(address indexed proposer, address indexed executor); - event ProposerCanCancelProposalsSet(address indexed proposer, bool canCancelProposals); event ProposerUnregistered(address indexed proposer, address indexed executor); // --- @@ -31,36 +30,30 @@ library Proposers { /// @notice The info about the registered proposer and associated executor. /// @param account Address of the proposer. /// @param executor The address of the executor assigned to execute proposals submitted by the proposer. - /// @param canCancelProposals Indicates whether the proposer has the authority to cancel all proposals that are - /// submitted or scheduled but not yet executed. struct Proposer { address account; address executor; - bool canCancelProposals; } - /// @notice Internal information about a proposer’s executor and proposal cancellation permissions. + /// @notice Internal information about a proposer’s executor. /// @param proposerIndex The one-based index of the proposer associated with the `executor` from /// the `Context.proposers` array. /// @param executor The address of the executor associated with the proposer. - /// @param canCancelProposals Indicates if the proposer has authority to cancel all proposals that are - /// submitted or scheduled but not yet executed. - struct ProposerData { + struct ExecutorData { /// @dev slot0: [0..31] IndexOneBased proposerIndex; /// @dev slot0: [32..191] address executor; - /// @dev slot0: [192..192] - bool canCancelProposals; } /// @notice The context of the Proposers library. /// @param proposers List of all registered proposers. - /// @param proposersData A mapping that associates each proposer’s address with their respective proposer data. - /// @param executorRefsCounts Mapping of executors to the count of proposers associated with each executor. + /// @param executors Mapping of proposers to their executor data. + /// @param executorRefsCounts Mapping of executors to the count of proposers associated + /// with each executor. struct Context { address[] proposers; - mapping(address proposer => ProposerData) proposersData; + mapping(address proposer => ExecutorData) executors; mapping(address executor => uint256 usagesCount) executorRefsCounts; } @@ -72,14 +65,7 @@ library Proposers { /// @param self The context of the Proposers library. /// @param proposerAccount The address of the proposer to register. /// @param executor The address of the executor to assign to the proposer. - /// @param canCancelProposals Indicates if the proposer has authority to cancel all proposals that are - /// submitted or scheduled but not yet executed. - function register( - Context storage self, - address proposerAccount, - address executor, - bool canCancelProposals - ) internal { + function register(Context storage self, address proposerAccount, address executor) internal { if (proposerAccount == address(0)) { revert InvalidProposerAccount(proposerAccount); } @@ -88,51 +74,34 @@ library Proposers { revert InvalidExecutor(executor); } - if (_isRegisteredProposer(self.proposersData[proposerAccount])) { + if (_isRegisteredProposer(self.executors[proposerAccount])) { revert ProposerAlreadyRegistered(proposerAccount); } self.proposers.push(proposerAccount); - self.proposersData[proposerAccount] = ProposerData({ - executor: executor, - canCancelProposals: canCancelProposals, - proposerIndex: IndicesOneBased.fromOneBasedValue(self.proposers.length) - }); + self.executors[proposerAccount] = + ExecutorData({proposerIndex: IndicesOneBased.fromOneBasedValue(self.proposers.length), executor: executor}); self.executorRefsCounts[executor] += 1; emit ProposerRegistered(proposerAccount, executor); } - /// @notice Updates the cancellation permissions for a registered proposer. - /// @param self The context storage of the Proposers library. - /// @param proposerAccount The address of the proposer to update permission to cancel proposals. - /// @param canCancelProposals A boolean indicating whether the proposer has permission to cancel proposals. - function setCanCancelProposals(Context storage self, address proposerAccount, bool canCancelProposals) internal { - ProposerData memory proposerData = self.proposersData[proposerAccount]; - _checkRegisteredProposer(proposerAccount, proposerData); - - if (proposerData.canCancelProposals != canCancelProposals) { - self.proposersData[proposerAccount].canCancelProposals = canCancelProposals; - emit ProposerCanCancelProposalsSet(proposerAccount, canCancelProposals); - } - } - /// @notice Updates the executor for a registered proposer. /// @param self The context storage of the Proposers library. /// @param proposerAccount The address of the proposer to update. /// @param executor The new executor address to assign to the proposer. function setProposerExecutor(Context storage self, address proposerAccount, address executor) internal { - ProposerData memory proposerData = self.proposersData[proposerAccount]; - _checkRegisteredProposer(proposerAccount, proposerData); + ExecutorData memory executorData = self.executors[proposerAccount]; + _checkRegisteredProposer(proposerAccount, executorData); - if (executor == address(0) || proposerData.executor == executor) { + if (executor == address(0) || executorData.executor == executor) { revert InvalidExecutor(executor); } - self.proposersData[proposerAccount].executor = executor; + self.executors[proposerAccount].executor = executor; self.executorRefsCounts[executor] += 1; - self.executorRefsCounts[proposerData.executor] -= 1; + self.executorRefsCounts[executorData.executor] -= 1; emit ProposerExecutorSet(proposerAccount, executor); } @@ -141,25 +110,25 @@ library Proposers { /// @param self The context of the Proposers library. /// @param proposerAccount The address of the proposer to unregister. function unregister(Context storage self, address proposerAccount) internal { - ProposerData memory proposerData = self.proposersData[proposerAccount]; + ExecutorData memory executorData = self.executors[proposerAccount]; - _checkRegisteredProposer(proposerAccount, proposerData); + _checkRegisteredProposer(proposerAccount, executorData); IndexOneBased lastProposerIndex = IndicesOneBased.fromOneBasedValue(self.proposers.length); - IndexOneBased proposerIndex = proposerData.proposerIndex; + IndexOneBased proposerIndex = executorData.proposerIndex; if (proposerIndex != lastProposerIndex) { address lastProposer = self.proposers[lastProposerIndex.toZeroBasedValue()]; self.proposers[proposerIndex.toZeroBasedValue()] = lastProposer; - self.proposersData[lastProposer].proposerIndex = proposerIndex; + self.executors[lastProposer].proposerIndex = proposerIndex; } self.proposers.pop(); - delete self.proposersData[proposerAccount]; + delete self.executors[proposerAccount]; - self.executorRefsCounts[proposerData.executor] -= 1; + self.executorRefsCounts[executorData.executor] -= 1; - emit ProposerUnregistered(proposerAccount, proposerData.executor); + emit ProposerUnregistered(proposerAccount, executorData.executor); } // --- @@ -174,12 +143,11 @@ library Proposers { Context storage self, address proposerAccount ) internal view returns (Proposer memory proposer) { - ProposerData memory proposerData = self.proposersData[proposerAccount]; - _checkRegisteredProposer(proposerAccount, proposerData); + ExecutorData memory executorData = self.executors[proposerAccount]; + _checkRegisteredProposer(proposerAccount, executorData); proposer.account = proposerAccount; - proposer.executor = proposerData.executor; - proposer.canCancelProposals = proposerData.canCancelProposals; + proposer.executor = executorData.executor; } /// @notice Retrieves all registered proposers. @@ -197,7 +165,7 @@ library Proposers { /// @param account The address to check. /// @return bool `true` if the account is a registered proposer, otherwise `false`. function isProposer(Context storage self, address account) internal view returns (bool) { - return _isRegisteredProposer(self.proposersData[account]); + return _isRegisteredProposer(self.executors[account]); } /// @notice Checks if an account is an executor associated with any proposer. @@ -225,13 +193,15 @@ library Proposers { // Private Methods // --- - function _checkRegisteredProposer(address proposerAccount, ProposerData memory proposerData) internal pure { - if (!_isRegisteredProposer(proposerData)) { + /// @notice Checks that the given proposer is registered. + function _checkRegisteredProposer(address proposerAccount, ExecutorData memory executorData) internal pure { + if (!_isRegisteredProposer(executorData)) { revert ProposerNotRegistered(proposerAccount); } } - function _isRegisteredProposer(ProposerData memory proposerData) internal pure returns (bool) { - return proposerData.proposerIndex.isNotEmpty(); + /// @notice Checks if the given executor data belongs to a registered proposer. + function _isRegisteredProposer(ExecutorData memory executorData) internal pure returns (bool) { + return executorData.proposerIndex.isNotEmpty(); } } diff --git a/scripts/deploy/ContractsDeployment.sol b/scripts/deploy/ContractsDeployment.sol index f8de105d..03b4d67e 100644 --- a/scripts/deploy/ContractsDeployment.sol +++ b/scripts/deploy/ContractsDeployment.sol @@ -290,9 +290,14 @@ library DGContractsDeployment { 0, abi.encodeCall( contracts.dualGovernance.registerProposer, - (address(lidoAddresses.voting), address(contracts.adminExecutor), true) + (address(lidoAddresses.voting), address(contracts.adminExecutor)) ) ); + contracts.adminExecutor.execute( + address(contracts.dualGovernance), + 0, + abi.encodeCall(contracts.dualGovernance.setProposalsCanceller, address(lidoAddresses.voting)) + ); contracts.adminExecutor.execute( address(contracts.dualGovernance), 0, diff --git a/test/scenario/dg-update-tokens-rotation.t.sol b/test/scenario/dg-update-tokens-rotation.t.sol index 974a4c86..97f98191 100644 --- a/test/scenario/dg-update-tokens-rotation.t.sol +++ b/test/scenario/dg-update-tokens-rotation.t.sol @@ -219,7 +219,7 @@ contract DualGovernanceUpdateTokensRotation is ScenarioTestBlueprint { value: 0, target: address(newDualGovernanceInstance), payload: abi.encodeCall( - DualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor(), true) + DualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor()) ) }), ExternalCall({ diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index 7c537148..6681778d 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -132,9 +132,7 @@ contract PlanBSetup is ScenarioTestBlueprint { address(_timelock) ], [ - abi.encodeCall( - _dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor(), true) - ), + abi.encodeCall(_dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor())), // Only Dual Governance contract can call the Timelock contract abi.encodeCall(_timelock.setGovernance, (address(_dualGovernance))), // Now the emergency mode may be deactivated (all scheduled calls will be canceled) @@ -218,9 +216,7 @@ contract PlanBSetup is ScenarioTestBlueprint { ExternalCall[] memory dualGovernanceUpdateCalls = ExternalCallHelpers.create( [address(dualGovernanceV2), address(_timelock), address(_timelock), address(_timelock)], [ - abi.encodeCall( - _dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor(), true) - ), + abi.encodeCall(_dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor())), // Update the controller for timelock abi.encodeCall(_timelock.setGovernance, address(dualGovernanceV2)), // Assembly the emergency committee again, until the new version of Dual Governance is battle tested diff --git a/test/scenario/timelocked-governance.t.sol b/test/scenario/timelocked-governance.t.sol index 793fb99c..d6be5be5 100644 --- a/test/scenario/timelocked-governance.t.sol +++ b/test/scenario/timelocked-governance.t.sol @@ -226,9 +226,7 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { ExternalCall[] memory dualGovernanceLaunchCalls = ExternalCallHelpers.create( [address(_dualGovernance), address(_timelock)], [ - abi.encodeCall( - _dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor(), true) - ), + abi.encodeCall(_dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor())), abi.encodeCall(_timelock.setGovernance, (address(_dualGovernance))) ] ); diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 240b18a6..1714f80b 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -40,6 +40,7 @@ contract DualGovernanceUnitTests is UnitTest { address private vetoer = makeAddr("vetoer"); address private resealCommittee = makeAddr("resealCommittee"); + address private proposalsCanceller = makeAddr("proposalsCanceller"); StETHMock private immutable _STETH_MOCK = new StETHMock(); IWithdrawalQueue private immutable _WITHDRAWAL_QUEUE_MOCK = new WithdrawalQueueMock(); @@ -95,7 +96,13 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(this), address(_executor), true) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(this), address(_executor)) + ); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setProposalsCanceller.selector, proposalsCanceller) ); _executor.execute( @@ -331,6 +338,7 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsSkipped(); + vm.prank(proposalsCanceller); bool isProposalsCancelled = _dualGovernance.cancelAllPendingProposals(); assertFalse(isProposalsCancelled); @@ -367,6 +375,7 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsSkipped(); + vm.prank(proposalsCanceller); bool isProposalsCancelled = _dualGovernance.cancelAllPendingProposals(); assertFalse(isProposalsCancelled); @@ -394,6 +403,7 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsSkipped(); + vm.prank(proposalsCanceller); bool isProposalsCancelled = _dualGovernance.cancelAllPendingProposals(); assertFalse(isProposalsCancelled); @@ -416,6 +426,7 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsExecuted(); + vm.prank(proposalsCanceller); bool isProposalsCancelled = _dualGovernance.cancelAllPendingProposals(); assertTrue(isProposalsCancelled); @@ -445,6 +456,7 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsExecuted(); + vm.prank(proposalsCanceller); bool isProposalsCancelled = _dualGovernance.cancelAllPendingProposals(); assertTrue(isProposalsCancelled); @@ -453,19 +465,14 @@ contract DualGovernanceUnitTests is UnitTest { } function test_cancelAllPendingProposals_RevertOn_ProposerCanNotCancelProposers() external { - address nonAdminProposer = makeAddr("NON_ADMIN_PROPOSER"); - _executor.execute( - address(_dualGovernance), - 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, nonAdminProposer, address(0x123), false) - ); + address notProposalsCanceller = makeAddr("NON_ADMIN_PROPOSER"); _submitMockProposal(); assertEq(_timelock.getProposalsCount(), 1); - vm.prank(nonAdminProposer); + vm.prank(notProposalsCanceller); vm.expectRevert( - abi.encodeWithSelector(DualGovernance.ProposerNotPermittedToCancelProposals.selector, nonAdminProposer) + abi.encodeWithSelector(DualGovernance.CallerIsNotProposalsCanceller.selector, notProposalsCanceller) ); _dualGovernance.cancelAllPendingProposals(); @@ -1130,6 +1137,66 @@ contract DualGovernanceUnitTests is UnitTest { ); } + // --- + // setProposalsCanceller() + // --- + + function test_setProposalsCanceller_HappyPath() external { + address newProposalsCanceller = makeAddr("newProposalsCanceller"); + + assertNotEq(newProposalsCanceller, _dualGovernance.getProposalsCanceller()); + + vm.expectEmit(true, true, false, false); + emit DualGovernance.ProposalsCancellerSet(newProposalsCanceller); + + vm.prank(address(_executor)); + _dualGovernance.setProposalsCanceller(newProposalsCanceller); + + assertEq(newProposalsCanceller, _dualGovernance.getProposalsCanceller()); + } + + function testFuzz_setProposalsCanceller_HappyPath(address newProposalsCanceller) external { + vm.assume( + newProposalsCanceller != address(0) && newProposalsCanceller != _dualGovernance.getProposalsCanceller() + ); + + vm.expectEmit(true, true, false, false); + emit DualGovernance.ProposalsCancellerSet(newProposalsCanceller); + + vm.prank(address(_executor)); + _dualGovernance.setProposalsCanceller(newProposalsCanceller); + + assertEq(newProposalsCanceller, _dualGovernance.getProposalsCanceller()); + } + + function test_setProposalsCanceller_RevertOn_CancellerIsZeroAddress() external { + vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidProposalsCanceller.selector, address(0))); + + vm.prank(address(_executor)); + _dualGovernance.setProposalsCanceller(address(0)); + } + + function test_setProposalsCanceller_RevertOn_NewCancellerAddressIsTheSame() external { + address prevProposalsCanceller = _dualGovernance.getProposalsCanceller(); + assertNotEq(prevProposalsCanceller, address(0)); + + vm.expectRevert( + abi.encodeWithSelector(DualGovernance.InvalidProposalsCanceller.selector, prevProposalsCanceller) + ); + + vm.prank(address(_executor)); + _dualGovernance.setProposalsCanceller(prevProposalsCanceller); + } + + function testFuzz_setProposalsCanceller_RevertOn_CalledNotByAdminExecutor(address notAllowedCaller) external { + vm.assume(notAllowedCaller != address(_executor)); + + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, notAllowedCaller)); + + vm.prank(notAllowedCaller); + _dualGovernance.setProposalsCanceller(notAllowedCaller); + } + // --- // getConfigProvider() // --- @@ -1324,7 +1391,64 @@ contract DualGovernanceUnitTests is UnitTest { vm.prank(stranger); vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, stranger)); - _dualGovernance.registerProposer(newProposer, newExecutor, true); + _dualGovernance.registerProposer(newProposer, newExecutor); + } + + // --- + // setProposerExecutor() + // --- + + function test_setProposerExecutor_HappyPath() external { + address newProposer = makeAddr("NEW_PROPOSER"); + address newExecutor = makeAddr("NEW_EXECUTOR"); + + assertEq(_dualGovernance.getProposers().length, 1); + assertFalse(_dualGovernance.isProposer(newProposer)); + + vm.prank(address(_executor)); + _dualGovernance.registerProposer(newProposer, newExecutor); + + assertEq(_dualGovernance.getProposers().length, 2); + assertTrue(_dualGovernance.isProposer(newProposer)); + + vm.prank(address(_executor)); + _dualGovernance.setProposerExecutor(newProposer, address(_executor)); + + assertEq(_dualGovernance.getProposers().length, 2); + assertTrue(_dualGovernance.isProposer(newProposer)); + assertFalse(_dualGovernance.isExecutor(newExecutor)); + } + + function testFuzz_setProposerExecutor_RevertOn_CalledNotByAdminExecutor(address notAllowedCaller) external { + vm.assume(notAllowedCaller != address(_executor)); + + address newProposer = makeAddr("NEW_PROPOSER"); + address newExecutor = makeAddr("NEW_EXECUTOR"); + + vm.prank(address(_executor)); + _dualGovernance.registerProposer(newProposer, newExecutor); + + assertEq(_dualGovernance.getProposers().length, 2); + assertTrue(_dualGovernance.isProposer(newProposer)); + + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, notAllowedCaller)); + + vm.prank(notAllowedCaller); + _dualGovernance.setProposerExecutor(newProposer, address(_executor)); + } + + function test_setProposerExecutor_RevertOn_AttemptToRemoveLastAdminProposer() external { + address newExecutor = makeAddr("NEW_EXECUTOR"); + + assertEq(_dualGovernance.getProposers().length, 1); + + assertTrue(_dualGovernance.isProposer(address(this))); + assertTrue(_dualGovernance.isExecutor(address(_executor))); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, address(_executor))); + + vm.prank(address(_executor)); + _dualGovernance.setProposerExecutor(address(this), address(newExecutor)); } // --- diff --git a/test/unit/libraries/Proposers.t.sol b/test/unit/libraries/Proposers.t.sol index c3079e4f..54d96758 100644 --- a/test/unit/libraries/Proposers.t.sol +++ b/test/unit/libraries/Proposers.t.sol @@ -20,7 +20,7 @@ contract ProposersLibraryUnitTests is UnitTest { // --- function test_register_HappyPath() external { // adding admin proposer - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); Proposers.Proposer[] memory allProposers = _proposers.getAllProposers(); assertEq(allProposers.length, 1); @@ -28,7 +28,7 @@ contract ProposersLibraryUnitTests is UnitTest { assertEq(allProposers[0].executor, _ADMIN_EXECUTOR); // adding non admin proposer - _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR, true); + _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); allProposers = _proposers.getAllProposers(); assertEq(allProposers.length, 2); @@ -38,26 +38,71 @@ contract ProposersLibraryUnitTests is UnitTest { function test_register_RevertOn_InvalidProposerAccount() external { vm.expectRevert(abi.encodeWithSelector(Proposers.InvalidProposerAccount.selector, address(0))); - _proposers.register(address(0), _ADMIN_EXECUTOR, true); + _proposers.register(address(0), _ADMIN_EXECUTOR); } function test_register_RevertOn_InvalidExecutor() external { vm.expectRevert(abi.encodeWithSelector(Proposers.InvalidExecutor.selector, address(0))); - _proposers.register(_ADMIN_PROPOSER, address(0), true); + _proposers.register(_ADMIN_PROPOSER, address(0)); } function test_register_RevertOn_ProposerAlreadyRegistered() external { - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerAlreadyRegistered.selector, _ADMIN_PROPOSER)); - _proposers.register(_ADMIN_PROPOSER, _DEFAULT_EXECUTOR, true); + _proposers.register(_ADMIN_PROPOSER, _DEFAULT_EXECUTOR); } function test_register_Emit_ProposerRegistered() external { vm.expectEmit(true, true, true, false); emit Proposers.ProposerRegistered(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + } + + // --- + // setProposerExecutor() + // --- + + function test_setProposerExecutor_HappyPath() external { + Proposers.Proposer memory defaultProposer = _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + assertNotEq(defaultProposer.executor, _ADMIN_EXECUTOR); + + uint256 adminExecutorRefsCountBefore = _proposers.executorRefsCounts[_ADMIN_EXECUTOR]; + uint256 defaultExecutorRefsCountBefore = _proposers.executorRefsCounts[_DEFAULT_EXECUTOR]; + + vm.expectEmit(true, true, false, false); + emit Proposers.ProposerExecutorSet(defaultProposer.account, _ADMIN_EXECUTOR); + + _proposers.setProposerExecutor(defaultProposer.account, _ADMIN_EXECUTOR); + + defaultProposer = _proposers.getProposer(_DEFAULT_PROPOSER); + assertEq(defaultProposer.executor, _ADMIN_EXECUTOR); + + // check executor references updated properly + assertEq(_proposers.executorRefsCounts[_DEFAULT_EXECUTOR], defaultExecutorRefsCountBefore - 1); + assertEq(_proposers.executorRefsCounts[_ADMIN_EXECUTOR], adminExecutorRefsCountBefore + 1); + } + + function test_setProposerExecutor_RevertOn_ZeroAddressExecutor() external { + Proposers.Proposer memory defaultProposer = _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.InvalidExecutor.selector, address(0))); + this.external__setProposerExecutor(defaultProposer.account, address(0)); + } + + function test_setProposerExecutor_RevertOn_SameExecutorAddress() external { + Proposers.Proposer memory defaultProposer = _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.InvalidExecutor.selector, _DEFAULT_EXECUTOR)); + this.external__setProposerExecutor(defaultProposer.account, _DEFAULT_EXECUTOR); + } + + function test_setProposerExecutor_RevertOn_NonRegisteredPropsoer() external { + assertEq(_proposers.proposers.length, 0); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); + this.external__setProposerExecutor(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); } // --- @@ -65,13 +110,13 @@ contract ProposersLibraryUnitTests is UnitTest { // --- function test_unregister_HappyPath() external { - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); assertEq(_proposers.proposers.length, 1); assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); assertTrue(_proposers.isExecutor(_ADMIN_EXECUTOR)); - _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR, true); + _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); assertEq(_proposers.proposers.length, 2); assertTrue(_proposers.isProposer(_DEFAULT_PROPOSER)); assertTrue(_proposers.isExecutor(_DEFAULT_EXECUTOR)); @@ -93,7 +138,7 @@ contract ProposersLibraryUnitTests is UnitTest { vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); _proposers.unregister(_DEFAULT_PROPOSER); - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); @@ -103,7 +148,7 @@ contract ProposersLibraryUnitTests is UnitTest { } function test_uregister_Emit_ProposerUnregistered() external { - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); vm.expectEmit(true, true, true, false); @@ -117,14 +162,14 @@ contract ProposersLibraryUnitTests is UnitTest { // --- function test_getProposer_HappyPath() external { - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); Proposers.Proposer memory adminProposer = _proposers.getProposer(_ADMIN_PROPOSER); assertEq(adminProposer.account, _ADMIN_PROPOSER); assertEq(adminProposer.executor, _ADMIN_EXECUTOR); - _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR, true); + _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); assertTrue(_proposers.isProposer(_DEFAULT_PROPOSER)); Proposers.Proposer memory defaultProposer = _proposers.getProposer(_DEFAULT_PROPOSER); @@ -138,7 +183,7 @@ contract ProposersLibraryUnitTests is UnitTest { vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); _proposers.getProposer(_DEFAULT_PROPOSER); - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); @@ -154,7 +199,7 @@ contract ProposersLibraryUnitTests is UnitTest { Proposers.Proposer[] memory emptyProposers = _proposers.getAllProposers(); assertEq(emptyProposers.length, 0); - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); Proposers.Proposer[] memory allProposers = _proposers.getAllProposers(); @@ -163,7 +208,7 @@ contract ProposersLibraryUnitTests is UnitTest { assertEq(allProposers[0].account, _ADMIN_PROPOSER); assertEq(allProposers[0].executor, _ADMIN_EXECUTOR); - _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR, true); + _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); assertTrue(_proposers.isProposer(_DEFAULT_PROPOSER)); allProposers = _proposers.getAllProposers(); @@ -181,7 +226,7 @@ contract ProposersLibraryUnitTests is UnitTest { // --- function test_unregister_Spam() external { - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); address dravee = makeAddr("Dravee"); address draveeExecutor = makeAddr("draveeExecutor"); @@ -192,10 +237,10 @@ contract ProposersLibraryUnitTests is UnitTest { address bob = makeAddr("Bob"); address bobExecutor = makeAddr("bobExecutor"); - _proposers.register(alice, aliceExecutor, false); - _proposers.register(bob, bobExecutor, true); - _proposers.register(celine, celineExecutor, false); - _proposers.register(dravee, draveeExecutor, true); + _proposers.register(alice, aliceExecutor); + _proposers.register(bob, bobExecutor); + _proposers.register(celine, celineExecutor); + _proposers.register(dravee, draveeExecutor); _proposers.unregister(bob); _proposers.unregister(dravee); @@ -204,7 +249,7 @@ contract ProposersLibraryUnitTests is UnitTest { } function test_unregister_CorrectPosition() external { - _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR, true); + _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); address dravee = makeAddr("Dravee"); address draveeExecutor = makeAddr("draveeExecutor"); @@ -215,10 +260,10 @@ contract ProposersLibraryUnitTests is UnitTest { address bob = makeAddr("Bob"); address bobExecutor = makeAddr("bobExecutor"); - _proposers.register(alice, aliceExecutor, false); - _proposers.register(bob, bobExecutor, true); - _proposers.register(celine, celineExecutor, false); - _proposers.register(dravee, draveeExecutor, true); + _proposers.register(alice, aliceExecutor); + _proposers.register(bob, bobExecutor); + _proposers.register(celine, celineExecutor); + _proposers.register(dravee, draveeExecutor); _proposers.unregister(bob); @@ -253,4 +298,97 @@ contract ProposersLibraryUnitTests is UnitTest { assertEq(allProposers[1].account, celine); assertEq(allProposers[1].executor, celineExecutor); } + + // --- + // checkRegisteredExecutor() + // --- + + function test_checkRegisteredExecutor_HappyPath() external { + address notExecutor = makeAddr("notExecutor"); + + assertEq(_proposers.proposers.length, 0); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _ADMIN_EXECUTOR)); + this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _DEFAULT_EXECUTOR)); + this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); + this.external__checkRegisteredExecutor(notExecutor); + + // --- + // register admin proposer + // --- + + _registerProposer(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + + this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _DEFAULT_EXECUTOR)); + this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); + this.external__checkRegisteredExecutor(notExecutor); + + // --- + // register default proposer + // --- + + _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + + this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); + + this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); + this.external__checkRegisteredExecutor(notExecutor); + + // --- + // change default proposer's executor on admin + // --- + + _proposers.setProposerExecutor(_DEFAULT_PROPOSER, _ADMIN_EXECUTOR); + + this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _DEFAULT_EXECUTOR)); + this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); + this.external__checkRegisteredExecutor(notExecutor); + } + + // --- + // Helper Methods + // --- + + function _registerProposer( + address account, + address executor + ) private returns (Proposers.Proposer memory proposer) { + uint256 proposersCountBefore = _proposers.proposers.length; + uint256 executorRefsCountBefore = _proposers.executorRefsCounts[executor]; + + _proposers.register(account, executor); + + uint256 proposersCountAfter = _proposers.proposers.length; + uint256 executorRefsCountAfter = _proposers.executorRefsCounts[executor]; + + assertEq(proposersCountAfter, proposersCountBefore + 1); + assertEq(executorRefsCountAfter, executorRefsCountBefore + 1); + + proposer = _proposers.getProposer(account); + + assertEq(proposer.account, account, "Invalid proposer account"); + assertEq(proposer.executor, executor, "Invalid proposer executor"); + } + + function external__setProposerExecutor(address proposer, address executor) external { + _proposers.setProposerExecutor(proposer, executor); + } + + function external__checkRegisteredExecutor(address executor) external { + _proposers.checkRegisteredExecutor(executor); + } } From 542979823bcf9729d80adb4c19bf55ef560ac69b Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 20 Nov 2024 14:07:13 +0400 Subject: [PATCH 059/107] Exclude changes for setting proposer's executor --- contracts/DualGovernance.sol | 29 ++---- contracts/libraries/Proposers.sol | 39 -------- test/unit/DualGovernance.t.sol | 77 ++-------------- test/unit/libraries/Proposers.t.sol | 138 ---------------------------- 4 files changed, 19 insertions(+), 264 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index af75a725..e67aecc8 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -38,6 +38,7 @@ contract DualGovernance is IDualGovernance { // Errors // --- + error UnownedAdminExecutor(); error CallerIsNotAdminExecutor(address caller); error CallerIsNotProposalsCanceller(address caller); error InvalidProposalsCanceller(address canceller); @@ -361,37 +362,25 @@ contract DualGovernance is IDualGovernance { /// @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 proposerAccount The address of the proposer to register. + /// @param proposer The address of the proposer to register. /// @param executor The address of the executor contract associated with the proposer. - function registerProposer(address proposerAccount, address executor) external { + function registerProposer(address proposer, address executor) external { _checkCallerIsAdminExecutor(); - _proposers.register(proposerAccount, executor); - } - - /// @notice Updates the executor associated with a specified proposer. - /// @dev Ensures that at least one proposer remains assigned to the `adminExecutor` following the update. - /// Reverts if updating the proposer’s executor would leave the `adminExecutor` without any associated proposer. - /// @param proposerAccount The address of the proposer whose executor is being updated. - /// @param executor The new executor address to assign to the proposer. - function setProposerExecutor(address proposerAccount, address executor) external { - _checkCallerIsAdminExecutor(); - _proposers.setProposerExecutor(proposerAccount, executor); - - /// @dev after update of the proposer, check that admin executor still belongs to some proposer - _proposers.checkRegisteredExecutor(TIMELOCK.getAdminExecutor()); + _proposers.register(proposer, executor); } /// @notice Unregisters a proposer from the system. - /// @dev Ensures that at least one proposer remains associated with the `adminExecutor`. If an attempt is made to - /// remove the last proposer assigned to the `adminExecutor`, the function will revert. + /// @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); /// @dev after the removal of the proposer, check that admin executor still belongs to some proposer - _proposers.checkRegisteredExecutor(TIMELOCK.getAdminExecutor()); + if (!_proposers.isExecutor(TIMELOCK.getAdminExecutor())) { + revert UnownedAdminExecutor(); + } } /// @notice Checks whether the given `account` is a registered proposer. diff --git a/contracts/libraries/Proposers.sol b/contracts/libraries/Proposers.sol index f8bcfeda..a8d45ba1 100644 --- a/contracts/libraries/Proposers.sol +++ b/contracts/libraries/Proposers.sol @@ -10,7 +10,6 @@ library Proposers { // Errors // --- error InvalidExecutor(address executor); - error ExecutorNotRegistered(address account); error InvalidProposerAccount(address account); error ProposerNotRegistered(address proposer); error ProposerAlreadyRegistered(address proposer); @@ -20,7 +19,6 @@ library Proposers { // --- event ProposerRegistered(address indexed proposer, address indexed executor); - event ProposerExecutorSet(address indexed proposer, address indexed executor); event ProposerUnregistered(address indexed proposer, address indexed executor); // --- @@ -86,26 +84,6 @@ library Proposers { emit ProposerRegistered(proposerAccount, executor); } - /// @notice Updates the executor for a registered proposer. - /// @param self The context storage of the Proposers library. - /// @param proposerAccount The address of the proposer to update. - /// @param executor The new executor address to assign to the proposer. - function setProposerExecutor(Context storage self, address proposerAccount, address executor) internal { - ExecutorData memory executorData = self.executors[proposerAccount]; - _checkRegisteredProposer(proposerAccount, executorData); - - if (executor == address(0) || executorData.executor == executor) { - revert InvalidExecutor(executor); - } - - self.executors[proposerAccount].executor = executor; - - self.executorRefsCounts[executor] += 1; - self.executorRefsCounts[executorData.executor] -= 1; - - emit ProposerExecutorSet(proposerAccount, executor); - } - /// @notice Unregisters a proposer, removing its association with an executor. /// @param self The context of the Proposers library. /// @param proposerAccount The address of the proposer to unregister. @@ -176,23 +154,6 @@ library Proposers { return self.executorRefsCounts[account] > 0; } - // --- - // Checks - // --- - - /// @notice Checks that a given account is a registered executor. - /// @param self The storage context of the Proposers library. - /// @param account The address to verify as a registered executor. - function checkRegisteredExecutor(Context storage self, address account) internal view { - if (!isExecutor(self, account)) { - revert ExecutorNotRegistered(account); - } - } - - // --- - // Private Methods - // --- - /// @notice Checks that the given proposer is registered. function _checkRegisteredProposer(address proposerAccount, ExecutorData memory executorData) internal pure { if (!_isRegisteredProposer(executorData)) { diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 1714f80b..7cf9203a 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -1373,7 +1373,7 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, newProposer, newExecutor, true) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, newProposer, newExecutor) ); assertTrue(_dualGovernance.isProposer(newProposer)); @@ -1394,63 +1394,6 @@ contract DualGovernanceUnitTests is UnitTest { _dualGovernance.registerProposer(newProposer, newExecutor); } - // --- - // setProposerExecutor() - // --- - - function test_setProposerExecutor_HappyPath() external { - address newProposer = makeAddr("NEW_PROPOSER"); - address newExecutor = makeAddr("NEW_EXECUTOR"); - - assertEq(_dualGovernance.getProposers().length, 1); - assertFalse(_dualGovernance.isProposer(newProposer)); - - vm.prank(address(_executor)); - _dualGovernance.registerProposer(newProposer, newExecutor); - - assertEq(_dualGovernance.getProposers().length, 2); - assertTrue(_dualGovernance.isProposer(newProposer)); - - vm.prank(address(_executor)); - _dualGovernance.setProposerExecutor(newProposer, address(_executor)); - - assertEq(_dualGovernance.getProposers().length, 2); - assertTrue(_dualGovernance.isProposer(newProposer)); - assertFalse(_dualGovernance.isExecutor(newExecutor)); - } - - function testFuzz_setProposerExecutor_RevertOn_CalledNotByAdminExecutor(address notAllowedCaller) external { - vm.assume(notAllowedCaller != address(_executor)); - - address newProposer = makeAddr("NEW_PROPOSER"); - address newExecutor = makeAddr("NEW_EXECUTOR"); - - vm.prank(address(_executor)); - _dualGovernance.registerProposer(newProposer, newExecutor); - - assertEq(_dualGovernance.getProposers().length, 2); - assertTrue(_dualGovernance.isProposer(newProposer)); - - vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, notAllowedCaller)); - - vm.prank(notAllowedCaller); - _dualGovernance.setProposerExecutor(newProposer, address(_executor)); - } - - function test_setProposerExecutor_RevertOn_AttemptToRemoveLastAdminProposer() external { - address newExecutor = makeAddr("NEW_EXECUTOR"); - - assertEq(_dualGovernance.getProposers().length, 1); - - assertTrue(_dualGovernance.isProposer(address(this))); - assertTrue(_dualGovernance.isExecutor(address(_executor))); - - vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, address(_executor))); - - vm.prank(address(_executor)); - _dualGovernance.setProposerExecutor(address(this), address(newExecutor)); - } - // --- // unregisterProposer() // --- @@ -1462,7 +1405,7 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor, true) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) ); assertTrue(_dualGovernance.isProposer(proposer)); @@ -1496,10 +1439,10 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor, true) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) ); - vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, adminExecutor)); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.UnownedAdminExecutor.selector)); _executor.execute( address(_dualGovernance), 0, @@ -1524,7 +1467,7 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor, true) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) ); assertTrue(_dualGovernance.isProposer(proposer)); @@ -1548,7 +1491,7 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor, true) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) ); Proposers.Proposer memory proposerData = _dualGovernance.getProposer(proposer); @@ -1578,17 +1521,17 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer1, proposerExecutor1, true) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer1, proposerExecutor1) ); _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer2, proposerExecutor2, true) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer2, proposerExecutor2) ); _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer3, proposerExecutor3, true) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer3, proposerExecutor3) ); Proposers.Proposer[] memory proposers = _dualGovernance.getProposers(); @@ -1615,7 +1558,7 @@ contract DualGovernanceUnitTests is UnitTest { _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(0x123), executor, true) + abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(0x123), executor) ); assertTrue(_dualGovernance.isExecutor(executor)); diff --git a/test/unit/libraries/Proposers.t.sol b/test/unit/libraries/Proposers.t.sol index 54d96758..7eb2ea42 100644 --- a/test/unit/libraries/Proposers.t.sol +++ b/test/unit/libraries/Proposers.t.sol @@ -60,51 +60,6 @@ contract ProposersLibraryUnitTests is UnitTest { _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); } - // --- - // setProposerExecutor() - // --- - - function test_setProposerExecutor_HappyPath() external { - Proposers.Proposer memory defaultProposer = _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); - assertNotEq(defaultProposer.executor, _ADMIN_EXECUTOR); - - uint256 adminExecutorRefsCountBefore = _proposers.executorRefsCounts[_ADMIN_EXECUTOR]; - uint256 defaultExecutorRefsCountBefore = _proposers.executorRefsCounts[_DEFAULT_EXECUTOR]; - - vm.expectEmit(true, true, false, false); - emit Proposers.ProposerExecutorSet(defaultProposer.account, _ADMIN_EXECUTOR); - - _proposers.setProposerExecutor(defaultProposer.account, _ADMIN_EXECUTOR); - - defaultProposer = _proposers.getProposer(_DEFAULT_PROPOSER); - assertEq(defaultProposer.executor, _ADMIN_EXECUTOR); - - // check executor references updated properly - assertEq(_proposers.executorRefsCounts[_DEFAULT_EXECUTOR], defaultExecutorRefsCountBefore - 1); - assertEq(_proposers.executorRefsCounts[_ADMIN_EXECUTOR], adminExecutorRefsCountBefore + 1); - } - - function test_setProposerExecutor_RevertOn_ZeroAddressExecutor() external { - Proposers.Proposer memory defaultProposer = _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); - - vm.expectRevert(abi.encodeWithSelector(Proposers.InvalidExecutor.selector, address(0))); - this.external__setProposerExecutor(defaultProposer.account, address(0)); - } - - function test_setProposerExecutor_RevertOn_SameExecutorAddress() external { - Proposers.Proposer memory defaultProposer = _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); - - vm.expectRevert(abi.encodeWithSelector(Proposers.InvalidExecutor.selector, _DEFAULT_EXECUTOR)); - this.external__setProposerExecutor(defaultProposer.account, _DEFAULT_EXECUTOR); - } - - function test_setProposerExecutor_RevertOn_NonRegisteredPropsoer() external { - assertEq(_proposers.proposers.length, 0); - - vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); - this.external__setProposerExecutor(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); - } - // --- // unregister() // --- @@ -298,97 +253,4 @@ contract ProposersLibraryUnitTests is UnitTest { assertEq(allProposers[1].account, celine); assertEq(allProposers[1].executor, celineExecutor); } - - // --- - // checkRegisteredExecutor() - // --- - - function test_checkRegisteredExecutor_HappyPath() external { - address notExecutor = makeAddr("notExecutor"); - - assertEq(_proposers.proposers.length, 0); - - vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _ADMIN_EXECUTOR)); - this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); - - vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _DEFAULT_EXECUTOR)); - this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); - - vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); - this.external__checkRegisteredExecutor(notExecutor); - - // --- - // register admin proposer - // --- - - _registerProposer(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); - - this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); - - vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _DEFAULT_EXECUTOR)); - this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); - - vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); - this.external__checkRegisteredExecutor(notExecutor); - - // --- - // register default proposer - // --- - - _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); - - this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); - - this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); - - vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); - this.external__checkRegisteredExecutor(notExecutor); - - // --- - // change default proposer's executor on admin - // --- - - _proposers.setProposerExecutor(_DEFAULT_PROPOSER, _ADMIN_EXECUTOR); - - this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); - - vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _DEFAULT_EXECUTOR)); - this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); - - vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); - this.external__checkRegisteredExecutor(notExecutor); - } - - // --- - // Helper Methods - // --- - - function _registerProposer( - address account, - address executor - ) private returns (Proposers.Proposer memory proposer) { - uint256 proposersCountBefore = _proposers.proposers.length; - uint256 executorRefsCountBefore = _proposers.executorRefsCounts[executor]; - - _proposers.register(account, executor); - - uint256 proposersCountAfter = _proposers.proposers.length; - uint256 executorRefsCountAfter = _proposers.executorRefsCounts[executor]; - - assertEq(proposersCountAfter, proposersCountBefore + 1); - assertEq(executorRefsCountAfter, executorRefsCountBefore + 1); - - proposer = _proposers.getProposer(account); - - assertEq(proposer.account, account, "Invalid proposer account"); - assertEq(proposer.executor, executor, "Invalid proposer executor"); - } - - function external__setProposerExecutor(address proposer, address executor) external { - _proposers.setProposerExecutor(proposer, executor); - } - - function external__checkRegisteredExecutor(address executor) external { - _proposers.checkRegisteredExecutor(executor); - } } From 215e22e1abda907dd62e8af8ee7c35b03fa34602 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 28 Nov 2024 04:54:16 +0400 Subject: [PATCH 060/107] Add natspec to set/getProposalsCanceller --- contracts/DualGovernance.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index e67aecc8..17b048c8 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -293,6 +293,8 @@ contract DualGovernance is IDualGovernance { _stateMachine.setConfigProvider(newConfigProvider); } + /// @notice Sets the address of the proposals canceller authorized to cancel pending proposals. + /// @param newProposalsCanceller The address of the new proposals canceller. function setProposalsCanceller(address newProposalsCanceller) external { _checkCallerIsAdminExecutor(); @@ -304,6 +306,8 @@ contract DualGovernance is IDualGovernance { emit ProposalsCancellerSet(newProposalsCanceller); } + /// @notice Retrieves the current proposals canceller address. + /// @return address The address of the current proposals canceller. function getProposalsCanceller() external view returns (address) { return _proposalsCanceller; } From 07ffeeae2b1cc5c46b70a63581d477fd7b6d5b0e Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 28 Nov 2024 04:54:42 +0400 Subject: [PATCH 061/107] Use expectEmit() without params in tests --- test/unit/DualGovernance.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 7cf9203a..820b730c 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -1146,7 +1146,7 @@ contract DualGovernanceUnitTests is UnitTest { assertNotEq(newProposalsCanceller, _dualGovernance.getProposalsCanceller()); - vm.expectEmit(true, true, false, false); + vm.expectEmit(); emit DualGovernance.ProposalsCancellerSet(newProposalsCanceller); vm.prank(address(_executor)); @@ -1160,7 +1160,7 @@ contract DualGovernanceUnitTests is UnitTest { newProposalsCanceller != address(0) && newProposalsCanceller != _dualGovernance.getProposalsCanceller() ); - vm.expectEmit(true, true, false, false); + vm.expectEmit(); emit DualGovernance.ProposalsCancellerSet(newProposalsCanceller); vm.prank(address(_executor)); From 2efe1b28cf16f17259818eebe8fdab5c55ef8486 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 29 Nov 2024 21:09:45 +0400 Subject: [PATCH 062/107] Fix merging changes --- contracts/DualGovernance.sol | 2 +- scripts/deploy/ContractsDeployment.sol | 3 +-- test/unit/DualGovernance.t.sol | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 17b048c8..9e89eb40 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -382,7 +382,7 @@ contract DualGovernance is IDualGovernance { _proposers.unregister(proposer); /// @dev after the removal of the proposer, check that admin executor still belongs to some proposer - if (!_proposers.isExecutor(TIMELOCK.getAdminExecutor())) { + if (!_proposers.isExecutor(msg.sender)) { revert UnownedAdminExecutor(); } } diff --git a/scripts/deploy/ContractsDeployment.sol b/scripts/deploy/ContractsDeployment.sol index 03b4d67e..adbe878a 100644 --- a/scripts/deploy/ContractsDeployment.sol +++ b/scripts/deploy/ContractsDeployment.sol @@ -289,8 +289,7 @@ library DGContractsDeployment { address(contracts.dualGovernance), 0, abi.encodeCall( - contracts.dualGovernance.registerProposer, - (address(lidoAddresses.voting), address(contracts.adminExecutor)) + contracts.dualGovernance.registerProposer, (lidoAddresses.voting, address(contracts.adminExecutor)) ) ); contracts.adminExecutor.execute( diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 820b730c..21ed1c23 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -464,8 +464,8 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(_timelock.lastCancelledProposalId(), 1); } - function test_cancelAllPendingProposals_RevertOn_ProposerCanNotCancelProposers() external { - address notProposalsCanceller = makeAddr("NON_ADMIN_PROPOSER"); + function test_cancelAllPendingProposals_RevertOn_CallerNotProposalsCanceller() external { + address notProposalsCanceller = makeAddr("NON_PROPOSALS_CANCELLER"); _submitMockProposal(); assertEq(_timelock.getProposalsCount(), 1); From 6ab5448f9f127bd219870d19c5437859d0006a45 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 28 Oct 2024 04:00:32 +0400 Subject: [PATCH 063/107] Split owner and operator roles in Executor --- contracts/EmergencyProtectedTimelock.sol | 8 --- contracts/Executor.sol | 71 +++++++++++++++++-- contracts/interfaces/ITimelock.sol | 2 +- scripts/deploy/ContractsDeployment.sol | 9 +-- test/unit/DualGovernance.t.sol | 11 ++- test/unit/EmergencyProtectedTimelock.t.sol | 33 +-------- test/unit/libraries/ExecutableProposals.t.sol | 2 +- 7 files changed, 85 insertions(+), 51 deletions(-) diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 55e4b69b..33e2c43d 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -167,14 +167,6 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { _timelockState.checkExecutionDelay(MIN_EXECUTION_DELAY); } - /// @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 { - _timelockState.checkCallerIsAdminExecutor(); - IOwnable(executor).transferOwnership(owner); - } - // --- // Emergency Protection Functionality // --- diff --git a/contracts/Executor.sol b/contracts/Executor.sol index 895703e8..0d67d79b 100644 --- a/contracts/Executor.sol +++ b/contracts/Executor.sol @@ -7,12 +7,42 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IExternalExecutor} from "./interfaces/IExternalExecutor.sol"; /// @title Executor -/// @notice Allows the contract owner to execute external function calls on specified target contracts with -/// possible value transfers. +/// @notice Allows the designated operator to execute external function calls on specified target contracts with +/// possible value transfers. The owner can set the operator. contract Executor is IExternalExecutor, Ownable { - constructor(address owner) Ownable(owner) {} + // --- + // Events + // --- - /// @notice Allows the contract owner to execute external function calls on target contracts, optionally transferring ether. + event OperatorSet(address indexed newOperator); + event Executed(address indexed target, uint256 value, bytes data); + + // --- + // Errors + // --- + + error CallerIsNotOperator(address caller); + error InvalidOperator(address account); + + // --- + // Storage variables + // --- + + address private _operator; + + // --- + // Constructor + // --- + + constructor(address owner, address operator) Ownable(owner) { + _setOperator(operator); + } + + // --- + // Main Functionality + // --- + + /// @notice Allows the operator to execute external function calls on target contracts, optionally transferring ether. /// @param target The address of the target contract on which to execute the function call. /// @param value The amount of ether (in wei) to send with the function call. /// @param payload The calldata for the function call. @@ -22,10 +52,41 @@ contract Executor is IExternalExecutor, Ownable { uint256 value, bytes calldata payload ) external payable returns (bytes memory result) { - _checkOwner(); + if (msg.sender != _operator) { + revert CallerIsNotOperator(msg.sender); + } result = Address.functionCallWithValue(target, payload, value); + emit Executed(target, value, payload); + } + + // --- + // Management Operations + // --- + + /// @notice Allows the owner to set a new operator. + /// @param newOperator The address of the new operator. + function setOperator(address newOperator) external onlyOwner { + _setOperator(newOperator); + } + + /// @notice Returns the current operator. + /// @return operator The address of the current operator. + function getOperator() external view returns (address operator) { + return _operator; } /// @notice Allows the contract to receive ether. receive() external payable {} + + // --- + // Internal Methods + // --- + + function _setOperator(address newOperator) internal { + if (newOperator == address(0) || newOperator == _operator) { + revert InvalidOperator(newOperator); + } + _operator = newOperator; + emit OperatorSet(newOperator); + } } diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index f94fe3e6..618420d4 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -46,5 +46,5 @@ interface ITimelock { function getAfterScheduleDelay() external view returns (Duration); function setAfterSubmitDelay(Duration newAfterSubmitDelay) external; function setAfterScheduleDelay(Duration newAfterScheduleDelay) external; - function transferExecutorOwnership(address executor, address owner) external; + // function transferExecutorOwnership(address executor, address owner) external; } diff --git a/scripts/deploy/ContractsDeployment.sol b/scripts/deploy/ContractsDeployment.sol index adbe878a..89483957 100644 --- a/scripts/deploy/ContractsDeployment.sol +++ b/scripts/deploy/ContractsDeployment.sol @@ -96,7 +96,7 @@ library DGContractsDeployment { DeployConfig memory dgDeployConfig, address deployer ) internal returns (DeployedContracts memory contracts) { - Executor adminExecutor = deployExecutor(deployer); + Executor adminExecutor = deployExecutor({owner: deployer, operator: deployer}); EmergencyProtectedTimelock timelock = deployEmergencyProtectedTimelock(address(adminExecutor), dgDeployConfig); contracts.adminExecutor = adminExecutor; @@ -150,8 +150,8 @@ library DGContractsDeployment { ); } - function deployExecutor(address owner) internal returns (Executor) { - return new Executor(owner); + function deployExecutor(address owner, address operator) internal returns (Executor) { + return new Executor(owner, operator); } function deployEmergencyProtectedTimelock( @@ -330,6 +330,7 @@ library DGContractsDeployment { DeployConfig memory dgDeployConfig ) internal { adminExecutor.execute(address(timelock), 0, abi.encodeCall(timelock.setGovernance, (dualGovernance))); - adminExecutor.transferOwnership(address(timelock)); + adminExecutor.setOperator(address(timelock)); + adminExecutor.transferOwnership(address(adminExecutor)); } } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 21ed1c23..48a49614 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -36,7 +36,7 @@ import {SealableMock} from "test/mocks/SealableMock.sol"; import {computeAddress} from "test/utils/addresses.sol"; contract DualGovernanceUnitTests is UnitTest { - Executor private _executor = new Executor(address(this)); + Executor private _executor = new Executor(address(this), address(this)); address private vetoer = makeAddr("vetoer"); address private resealCommittee = makeAddr("resealCommittee"); @@ -1072,6 +1072,13 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernanceStateMachine.ConfigProviderSet(IDualGovernanceConfigProvider(address(newConfigProvider))); + vm.expectEmit(); + emit Executor.Executed( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setConfigProvider.selector, address(newConfigProvider)) + ); + vm.expectCall( address(_escrow), 0, @@ -1084,7 +1091,7 @@ contract DualGovernanceUnitTests is UnitTest { 0, abi.encodeWithSelector(DualGovernance.setConfigProvider.selector, address(newConfigProvider)) ); - assertEq(vm.getRecordedLogs().length, 1); + assertEq(vm.getRecordedLogs().length, 2); assertEq(address(_dualGovernance.getConfigProvider()), address(newConfigProvider)); assertTrue(address(_dualGovernance.getConfigProvider()) != address(oldConfigProvider)); diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 3f7823ef..b45510b7 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -48,7 +48,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { Duration private _defaultAfterScheduleDelay = Durations.from(2 days); function setUp() external { - _executor = new Executor(address(this)); + _executor = new Executor(address(this), address(this)); _adminExecutor = address(_executor); _timelock = _deployEmergencyProtectedTimelock(); @@ -56,7 +56,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _targetMock = new TargetMock(); _anotherTargetMock = new TargetMock(); - _executor.transferOwnership(address(_timelock)); + _executor.setOperator(address(_timelock)); + _executor.transferOwnership(address(_executor)); vm.startPrank(_adminExecutor); _timelock.setGovernance(_dualGovernance); @@ -463,34 +464,6 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getAfterScheduleDelay(), newAfterScheduleDelay); } - // EmergencyProtectedTimelock.transferExecutorOwnership() - - function testFuzz_transferExecutorOwnership_HappyPath(address newOwner) external { - vm.assume(newOwner != _adminExecutor); - vm.assume(newOwner != address(0)); - - Executor executor = new Executor(address(_timelock)); - - assertEq(executor.owner(), address(_timelock)); - - vm.prank(_adminExecutor); - - vm.expectEmit(address(executor)); - emit Ownable.OwnershipTransferred(address(_timelock), newOwner); - - _timelock.transferExecutorOwnership(address(executor), newOwner); - - assertEq(executor.owner(), newOwner); - } - - function test_transferExecutorOwnership_RevertOn_ByStranger(address stranger) external { - vm.assume(stranger != _adminExecutor); - - vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); - _timelock.transferExecutorOwnership(_adminExecutor, makeAddr("newOwner")); - } - // EmergencyProtectedTimelock.setGovernance() function testFuzz_setGovernance_HappyPath(address newGovernance) external { diff --git a/test/unit/libraries/ExecutableProposals.t.sol b/test/unit/libraries/ExecutableProposals.t.sol index d95c328a..4919fef2 100644 --- a/test/unit/libraries/ExecutableProposals.t.sol +++ b/test/unit/libraries/ExecutableProposals.t.sol @@ -27,7 +27,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function setUp() external { _targetMock = new TargetMock(); - _executor = new Executor(address(this)); + _executor = new Executor(address(this), address(this)); } function test_submit_reverts_if_empty_proposals() external { From fec66675df20f75b44071ce013dcceaa06b95963 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Tue, 29 Oct 2024 00:01:24 +0400 Subject: [PATCH 064/107] Make Executor.execute() compatible with Agent.execute() --- contracts/Executor.sol | 13 +- contracts/interfaces/IExternalExecutor.sol | 6 +- contracts/libraries/ExecutableProposals.sol | 12 +- contracts/libraries/ExternalCalls.sol | 9 +- test/scenario/agent-timelock.t.sol | 140 ++++++++++++++++++++ test/utils/interfaces/IAragonAgent.sol | 1 + 6 files changed, 151 insertions(+), 30 deletions(-) diff --git a/contracts/Executor.sol b/contracts/Executor.sol index 0d67d79b..443ca275 100644 --- a/contracts/Executor.sol +++ b/contracts/Executor.sol @@ -15,7 +15,7 @@ contract Executor is IExternalExecutor, Ownable { // --- event OperatorSet(address indexed newOperator); - event Executed(address indexed target, uint256 value, bytes data); + event Execute(address indexed sender, address indexed target, uint256 ethValue, bytes data); // --- // Errors @@ -46,17 +46,12 @@ contract Executor is IExternalExecutor, Ownable { /// @param target The address of the target contract on which to execute the function call. /// @param value The amount of ether (in wei) to send with the function call. /// @param payload The calldata for the function call. - /// @return result The data returned from the function call. - function execute( - address target, - uint256 value, - bytes calldata payload - ) external payable returns (bytes memory result) { + function execute(address target, uint256 value, bytes calldata payload) external payable { if (msg.sender != _operator) { revert CallerIsNotOperator(msg.sender); } - result = Address.functionCallWithValue(target, payload, value); - emit Executed(target, value, payload); + Address.functionCallWithValue(target, payload, value); + emit Execute(msg.sender, target, value, payload); } // --- diff --git a/contracts/interfaces/IExternalExecutor.sol b/contracts/interfaces/IExternalExecutor.sol index 6999aa53..466c927c 100644 --- a/contracts/interfaces/IExternalExecutor.sol +++ b/contracts/interfaces/IExternalExecutor.sol @@ -2,9 +2,5 @@ pragma solidity 0.8.26; interface IExternalExecutor { - function execute( - address target, - uint256 value, - bytes calldata payload - ) external payable returns (bytes memory result); + function execute(address target, uint256 value, bytes calldata payload) external payable; } diff --git a/contracts/libraries/ExecutableProposals.sol b/contracts/libraries/ExecutableProposals.sol index 49c38e12..070af315 100644 --- a/contracts/libraries/ExecutableProposals.sol +++ b/contracts/libraries/ExecutableProposals.sol @@ -35,8 +35,6 @@ enum Status { /// @notice Manages a collection of proposals with associated external calls stored as Proposal struct. /// Proposals are uniquely identified by sequential ids, starting from one. library ExecutableProposals { - using ExternalCalls for ExternalCall[]; - // --- // Data Types // --- @@ -95,7 +93,7 @@ library ExecutableProposals { uint256 indexed id, address indexed proposer, address indexed executor, ExternalCall[] calls, string metadata ); event ProposalScheduled(uint256 indexed id); - event ProposalExecuted(uint256 indexed id, bytes[] callResults); + event ProposalExecuted(uint256 indexed id); event ProposalsCancelledTill(uint256 proposalId); // --- @@ -178,12 +176,8 @@ library ExecutableProposals { self.proposals[proposalId].data.status = Status.Executed; - address executor = proposal.data.executor; - ExternalCall[] memory calls = proposal.calls; - - bytes[] memory results = calls.execute(IExternalExecutor(executor)); - - emit ProposalExecuted(proposalId, results); + ExternalCalls.execute(IExternalExecutor(proposal.data.executor), proposal.calls); + emit ProposalExecuted(proposalId); } /// @notice Marks all non-executed proposals up to the most recently submitted as canceled, preventing their execution. diff --git a/contracts/libraries/ExternalCalls.sol b/contracts/libraries/ExternalCalls.sol index 32ee488e..ace806b4 100644 --- a/contracts/libraries/ExternalCalls.sol +++ b/contracts/libraries/ExternalCalls.sol @@ -20,15 +20,10 @@ library ExternalCalls { /// `IExternalExecutor` interface. /// @param calls An array of `ExternalCall` structs, each specifying a call to be executed. /// @param executor The contract responsible for executing each call, conforming to the `IExternalExecutor` interface. - /// @return results An array containing the return data from each call. - function execute( - ExternalCall[] memory calls, - IExternalExecutor executor - ) internal returns (bytes[] memory results) { + function execute(IExternalExecutor executor, ExternalCall[] memory calls) internal { uint256 callsCount = calls.length; - results = new bytes[](callsCount); for (uint256 i = 0; i < callsCount; ++i) { - results[i] = executor.execute(calls[i].target, calls[i].value, calls[i].payload); + executor.execute(calls[i].target, calls[i].value, calls[i].payload); } } } diff --git a/test/scenario/agent-timelock.t.sol b/test/scenario/agent-timelock.t.sol index 5cdebbee..2546c6da 100644 --- a/test/scenario/agent-timelock.t.sol +++ b/test/scenario/agent-timelock.t.sol @@ -2,12 +2,152 @@ pragma solidity 0.8.26; import {ExternalCall, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; +import {LidoUtils} from "../utils/lido-utils.sol"; contract AgentTimelockTest is ScenarioTestBlueprint { + using LidoUtils for LidoUtils.Context; + function setUp() external { _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); } + function testFork_AragonAgentAsExecutor_HappyPath() external { + _step("0. Tweak the default setup to make Agent admin executor"); + + // set the Agent as admin executor in the timelock contract + vm.prank(address(_adminExecutor)); + _timelock.setAdminExecutor(address(_lido.agent)); + + assertEq(_timelock.getAdminExecutor(), address(_lido.agent)); + + // grant EXECUTE_ROLE permission to the timelock contract to allow call Agent.execute() method + _lido.grantPermission({app: address(_lido.agent), role: _lido.agent.EXECUTE_ROLE(), grantee: address(_timelock)}); + + // update proposers for the Voting + vm.startPrank(address(_lido.agent)); + + address tmpProposer = makeAddr("tmpProposer"); + + _dualGovernance.registerProposer(tmpProposer, address(_lido.agent)); + _dualGovernance.unregisterProposer(address(_lido.voting)); + + _dualGovernance.registerProposer(address(_lido.voting), address(_lido.agent)); + _dualGovernance.unregisterProposer(tmpProposer); + vm.stopPrank(); + + _step("Test setup preparations have done!"); + + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); + + uint256 proposalId; + _step("1. THE PROPOSAL IS SUBMITTED"); + { + proposalId = _submitProposalViaDualGovernance( + "Propose to doSmth on target passing dual governance", regularStaffCalls + ); + + _assertSubmittedProposalData(proposalId, _getAdminExecutor(), regularStaffCalls); + _assertCanScheduleViaDualGovernance(proposalId, false); + } + + _step("2. THE PROPOSAL IS SCHEDULED"); + { + _waitAfterSubmitDelayPassed(); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); + + _assertProposalScheduled(proposalId); + _assertCanExecute(proposalId, false); + } + + _step("3. THE PROPOSAL CAN BE EXECUTED"); + { + // wait until the second delay has passed + _waitAfterScheduleDelayPassed(); + + // Now proposal can be executed + _assertCanExecute(proposalId, true); + + _assertNoTargetMockCalls(); + + _executeProposal(proposalId); + _assertProposalExecuted(proposalId); + + _assertCanExecute(proposalId, false); + _assertCanScheduleViaDualGovernance(proposalId, false); + + _assertTargetMockCalls(address(_lido.agent), regularStaffCalls); + } + } + + function testFork_AragonAgentAsExecutor_RevertOn_FailedCall() external { + _step("0. Tweak the default setup to make Agent admin executor"); + + // set the Agent as admin executor in the timelock contract + vm.prank(address(_adminExecutor)); + _timelock.setAdminExecutor(address(_lido.agent)); + + assertEq(_timelock.getAdminExecutor(), address(_lido.agent)); + + // grant EXECUTE_ROLE permission to the timelock contract to allow call Agent.execute() method + _lido.grantPermission({app: address(_lido.agent), role: _lido.agent.EXECUTE_ROLE(), grantee: address(_timelock)}); + + // update proposers for the Voting + vm.startPrank(address(_lido.agent)); + + address tmpProposer = makeAddr("tmpProposer"); + + _dualGovernance.registerProposer(tmpProposer, address(_lido.agent)); + _dualGovernance.unregisterProposer(address(_lido.voting)); + + _dualGovernance.registerProposer(address(_lido.voting), address(_lido.agent)); + _dualGovernance.unregisterProposer(tmpProposer); + vm.stopPrank(); + + _step("Test setup preparations have done!"); + + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); + + // code to invalid contract + regularStaffCalls[0].target = makeAddr("invalidTarget"); + vm.mockCallRevert(regularStaffCalls[0].target, regularStaffCalls[0].payload, "INVALID TARGET"); + + uint256 proposalId; + _step("1. THE PROPOSAL IS SUBMITTED"); + { + proposalId = _submitProposalViaDualGovernance( + "Propose to doSmth on target passing dual governance", regularStaffCalls + ); + + _assertSubmittedProposalData(proposalId, _getAdminExecutor(), regularStaffCalls); + _assertCanScheduleViaDualGovernance(proposalId, false); + } + + _step("2. THE PROPOSAL IS SCHEDULED"); + { + _waitAfterSubmitDelayPassed(); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); + + _assertProposalScheduled(proposalId); + _assertCanExecute(proposalId, false); + } + + _step("3. THE PROPOSAL CAN BE EXECUTED"); + { + // wait until the second delay has passed + _waitAfterScheduleDelayPassed(); + + // Now proposal can be executed + _assertCanExecute(proposalId, true); + + _assertNoTargetMockCalls(); + + vm.expectRevert("INVALID TARGET"); + _executeProposal(proposalId); + } + } + function testFork_AgentTimelockHappyPath() external { ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); diff --git a/test/utils/interfaces/IAragonAgent.sol b/test/utils/interfaces/IAragonAgent.sol index fbbf1f13..11d8642a 100644 --- a/test/utils/interfaces/IAragonAgent.sol +++ b/test/utils/interfaces/IAragonAgent.sol @@ -4,5 +4,6 @@ pragma solidity 0.8.26; import {IAragonForwarder} from "./IAragonForwarder.sol"; interface IAragonAgent is IAragonForwarder { + function EXECUTE_ROLE() external pure returns (bytes32); function RUN_SCRIPT_ROLE() external pure returns (bytes32); } From f0077d8a07a4860fb6a83f52b69b9ca8e90ab497 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 20 Nov 2024 13:53:07 +0400 Subject: [PATCH 065/107] Add method to set executor for registered proposer --- contracts/DualGovernance.sol | 34 ++++--- contracts/libraries/Proposers.sol | 39 ++++++++ test/unit/DualGovernance.t.sol | 62 ++++++++++++- test/unit/libraries/Proposers.t.sol | 138 ++++++++++++++++++++++++++++ 4 files changed, 259 insertions(+), 14 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 9e89eb40..6b856ead 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -38,7 +38,6 @@ contract DualGovernance is IDualGovernance { // Errors // --- - error UnownedAdminExecutor(); error CallerIsNotAdminExecutor(address caller); error CallerIsNotProposalsCanceller(address caller); error InvalidProposalsCanceller(address canceller); @@ -366,25 +365,36 @@ contract DualGovernance is IDualGovernance { /// @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 proposerAccount 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 { + function registerProposer(address proposerAccount, address executor) external { _checkCallerIsAdminExecutor(); - _proposers.register(proposer, executor); + _proposers.register(proposerAccount, executor); + } + + /// @notice Updates the executor associated with a specified proposer. + /// @dev Ensures that at least one proposer remains assigned to the `adminExecutor` following the update. + /// Reverts if updating the proposer’s executor would leave the `adminExecutor` without any associated proposer. + /// @param proposerAccount The address of the proposer whose executor is being updated. + /// @param executor The new executor address to assign to the proposer. + function setProposerExecutor(address proposerAccount, address executor) external { + _checkCallerIsAdminExecutor(); + _proposers.setProposerExecutor(proposerAccount, executor); + + /// @dev after update of the proposer, check that admin executor still belongs to some proposer + _proposers.checkRegisteredExecutor(TIMELOCK.getAdminExecutor()); } /// @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 { + /// @dev Ensures that at least one proposer remains associated with the `adminExecutor`. If an attempt is made to + /// remove the last proposer assigned to the `adminExecutor`, the function will revert. + /// @param proposerAccount The address of the proposer to unregister. + function unregisterProposer(address proposerAccount) external { _checkCallerIsAdminExecutor(); - _proposers.unregister(proposer); + _proposers.unregister(proposerAccount); /// @dev after the removal of the proposer, check that admin executor still belongs to some proposer - if (!_proposers.isExecutor(msg.sender)) { - revert UnownedAdminExecutor(); - } + _proposers.checkRegisteredExecutor(msg.sender); } /// @notice Checks whether the given `account` is a registered proposer. diff --git a/contracts/libraries/Proposers.sol b/contracts/libraries/Proposers.sol index a8d45ba1..f8bcfeda 100644 --- a/contracts/libraries/Proposers.sol +++ b/contracts/libraries/Proposers.sol @@ -10,6 +10,7 @@ library Proposers { // Errors // --- error InvalidExecutor(address executor); + error ExecutorNotRegistered(address account); error InvalidProposerAccount(address account); error ProposerNotRegistered(address proposer); error ProposerAlreadyRegistered(address proposer); @@ -19,6 +20,7 @@ library Proposers { // --- event ProposerRegistered(address indexed proposer, address indexed executor); + event ProposerExecutorSet(address indexed proposer, address indexed executor); event ProposerUnregistered(address indexed proposer, address indexed executor); // --- @@ -84,6 +86,26 @@ library Proposers { emit ProposerRegistered(proposerAccount, executor); } + /// @notice Updates the executor for a registered proposer. + /// @param self The context storage of the Proposers library. + /// @param proposerAccount The address of the proposer to update. + /// @param executor The new executor address to assign to the proposer. + function setProposerExecutor(Context storage self, address proposerAccount, address executor) internal { + ExecutorData memory executorData = self.executors[proposerAccount]; + _checkRegisteredProposer(proposerAccount, executorData); + + if (executor == address(0) || executorData.executor == executor) { + revert InvalidExecutor(executor); + } + + self.executors[proposerAccount].executor = executor; + + self.executorRefsCounts[executor] += 1; + self.executorRefsCounts[executorData.executor] -= 1; + + emit ProposerExecutorSet(proposerAccount, executor); + } + /// @notice Unregisters a proposer, removing its association with an executor. /// @param self The context of the Proposers library. /// @param proposerAccount The address of the proposer to unregister. @@ -154,6 +176,23 @@ library Proposers { return self.executorRefsCounts[account] > 0; } + // --- + // Checks + // --- + + /// @notice Checks that a given account is a registered executor. + /// @param self The storage context of the Proposers library. + /// @param account The address to verify as a registered executor. + function checkRegisteredExecutor(Context storage self, address account) internal view { + if (!isExecutor(self, account)) { + revert ExecutorNotRegistered(account); + } + } + + // --- + // Private Methods + // --- + /// @notice Checks that the given proposer is registered. function _checkRegisteredProposer(address proposerAccount, ExecutorData memory executorData) internal pure { if (!_isRegisteredProposer(executorData)) { diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 48a49614..b33c26f8 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -1073,7 +1073,8 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernanceStateMachine.ConfigProviderSet(IDualGovernanceConfigProvider(address(newConfigProvider))); vm.expectEmit(); - emit Executor.Executed( + emit Executor.Execute( + address(this), address(_dualGovernance), 0, abi.encodeWithSelector(DualGovernance.setConfigProvider.selector, address(newConfigProvider)) @@ -1401,6 +1402,63 @@ contract DualGovernanceUnitTests is UnitTest { _dualGovernance.registerProposer(newProposer, newExecutor); } + // --- + // setProposerExecutor() + // --- + + function test_setProposerExecutor_HappyPath() external { + address newProposer = makeAddr("NEW_PROPOSER"); + address newExecutor = makeAddr("NEW_EXECUTOR"); + + assertEq(_dualGovernance.getProposers().length, 1); + assertFalse(_dualGovernance.isProposer(newProposer)); + + vm.prank(address(_executor)); + _dualGovernance.registerProposer(newProposer, newExecutor); + + assertEq(_dualGovernance.getProposers().length, 2); + assertTrue(_dualGovernance.isProposer(newProposer)); + + vm.prank(address(_executor)); + _dualGovernance.setProposerExecutor(newProposer, address(_executor)); + + assertEq(_dualGovernance.getProposers().length, 2); + assertTrue(_dualGovernance.isProposer(newProposer)); + assertFalse(_dualGovernance.isExecutor(newExecutor)); + } + + function testFuzz_setProposerExecutor_RevertOn_CalledNotByAdminExecutor(address notAllowedCaller) external { + vm.assume(notAllowedCaller != address(_executor)); + + address newProposer = makeAddr("NEW_PROPOSER"); + address newExecutor = makeAddr("NEW_EXECUTOR"); + + vm.prank(address(_executor)); + _dualGovernance.registerProposer(newProposer, newExecutor); + + assertEq(_dualGovernance.getProposers().length, 2); + assertTrue(_dualGovernance.isProposer(newProposer)); + + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, notAllowedCaller)); + + vm.prank(notAllowedCaller); + _dualGovernance.setProposerExecutor(newProposer, address(_executor)); + } + + function test_setProposerExecutor_RevertOn_AttemptToRemoveLastAdminProposer() external { + address newExecutor = makeAddr("NEW_EXECUTOR"); + + assertEq(_dualGovernance.getProposers().length, 1); + + assertTrue(_dualGovernance.isProposer(address(this))); + assertTrue(_dualGovernance.isExecutor(address(_executor))); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, address(_executor))); + + vm.prank(address(_executor)); + _dualGovernance.setProposerExecutor(address(this), address(newExecutor)); + } + // --- // unregisterProposer() // --- @@ -1449,7 +1507,7 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) ); - vm.expectRevert(abi.encodeWithSelector(DualGovernance.UnownedAdminExecutor.selector)); + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, (adminExecutor))); _executor.execute( address(_dualGovernance), 0, diff --git a/test/unit/libraries/Proposers.t.sol b/test/unit/libraries/Proposers.t.sol index 7eb2ea42..54d96758 100644 --- a/test/unit/libraries/Proposers.t.sol +++ b/test/unit/libraries/Proposers.t.sol @@ -60,6 +60,51 @@ contract ProposersLibraryUnitTests is UnitTest { _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); } + // --- + // setProposerExecutor() + // --- + + function test_setProposerExecutor_HappyPath() external { + Proposers.Proposer memory defaultProposer = _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + assertNotEq(defaultProposer.executor, _ADMIN_EXECUTOR); + + uint256 adminExecutorRefsCountBefore = _proposers.executorRefsCounts[_ADMIN_EXECUTOR]; + uint256 defaultExecutorRefsCountBefore = _proposers.executorRefsCounts[_DEFAULT_EXECUTOR]; + + vm.expectEmit(true, true, false, false); + emit Proposers.ProposerExecutorSet(defaultProposer.account, _ADMIN_EXECUTOR); + + _proposers.setProposerExecutor(defaultProposer.account, _ADMIN_EXECUTOR); + + defaultProposer = _proposers.getProposer(_DEFAULT_PROPOSER); + assertEq(defaultProposer.executor, _ADMIN_EXECUTOR); + + // check executor references updated properly + assertEq(_proposers.executorRefsCounts[_DEFAULT_EXECUTOR], defaultExecutorRefsCountBefore - 1); + assertEq(_proposers.executorRefsCounts[_ADMIN_EXECUTOR], adminExecutorRefsCountBefore + 1); + } + + function test_setProposerExecutor_RevertOn_ZeroAddressExecutor() external { + Proposers.Proposer memory defaultProposer = _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.InvalidExecutor.selector, address(0))); + this.external__setProposerExecutor(defaultProposer.account, address(0)); + } + + function test_setProposerExecutor_RevertOn_SameExecutorAddress() external { + Proposers.Proposer memory defaultProposer = _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.InvalidExecutor.selector, _DEFAULT_EXECUTOR)); + this.external__setProposerExecutor(defaultProposer.account, _DEFAULT_EXECUTOR); + } + + function test_setProposerExecutor_RevertOn_NonRegisteredPropsoer() external { + assertEq(_proposers.proposers.length, 0); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); + this.external__setProposerExecutor(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + } + // --- // unregister() // --- @@ -253,4 +298,97 @@ contract ProposersLibraryUnitTests is UnitTest { assertEq(allProposers[1].account, celine); assertEq(allProposers[1].executor, celineExecutor); } + + // --- + // checkRegisteredExecutor() + // --- + + function test_checkRegisteredExecutor_HappyPath() external { + address notExecutor = makeAddr("notExecutor"); + + assertEq(_proposers.proposers.length, 0); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _ADMIN_EXECUTOR)); + this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _DEFAULT_EXECUTOR)); + this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); + this.external__checkRegisteredExecutor(notExecutor); + + // --- + // register admin proposer + // --- + + _registerProposer(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + + this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _DEFAULT_EXECUTOR)); + this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); + this.external__checkRegisteredExecutor(notExecutor); + + // --- + // register default proposer + // --- + + _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + + this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); + + this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); + this.external__checkRegisteredExecutor(notExecutor); + + // --- + // change default proposer's executor on admin + // --- + + _proposers.setProposerExecutor(_DEFAULT_PROPOSER, _ADMIN_EXECUTOR); + + this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _DEFAULT_EXECUTOR)); + this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); + this.external__checkRegisteredExecutor(notExecutor); + } + + // --- + // Helper Methods + // --- + + function _registerProposer( + address account, + address executor + ) private returns (Proposers.Proposer memory proposer) { + uint256 proposersCountBefore = _proposers.proposers.length; + uint256 executorRefsCountBefore = _proposers.executorRefsCounts[executor]; + + _proposers.register(account, executor); + + uint256 proposersCountAfter = _proposers.proposers.length; + uint256 executorRefsCountAfter = _proposers.executorRefsCounts[executor]; + + assertEq(proposersCountAfter, proposersCountBefore + 1); + assertEq(executorRefsCountAfter, executorRefsCountBefore + 1); + + proposer = _proposers.getProposer(account); + + assertEq(proposer.account, account, "Invalid proposer account"); + assertEq(proposer.executor, executor, "Invalid proposer executor"); + } + + function external__setProposerExecutor(address proposer, address executor) external { + _proposers.setProposerExecutor(proposer, executor); + } + + function external__checkRegisteredExecutor(address executor) external { + _proposers.checkRegisteredExecutor(executor); + } } From 58ad456464f40c6a5ab88e1c93c21bc3ee5f8274 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 20 Nov 2024 13:57:06 +0400 Subject: [PATCH 066/107] Add scenario test on executors management --- .../exeuctor-ownership-transfer.t.sol | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 test/scenario/exeuctor-ownership-transfer.t.sol diff --git a/test/scenario/exeuctor-ownership-transfer.t.sol b/test/scenario/exeuctor-ownership-transfer.t.sol new file mode 100644 index 00000000..69bf7b5c --- /dev/null +++ b/test/scenario/exeuctor-ownership-transfer.t.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Durations} from "contracts/types/Duration.sol"; + +import {Ownable, Executor} from "contracts/Executor.sol"; +import {Proposers} from "contracts/libraries/Proposers.sol"; + +import {ExternalCall, ScenarioTestBlueprint, ExternalCallHelpers} from "../utils/scenario-test-blueprint.sol"; + +contract ExecutorOwnershipTransfer is ScenarioTestBlueprint { + address private immutable _NEW_REGULAR_PROPOSER = makeAddr("NEW_REGULAR_PROPOSER"); + + Executor private _oldAdminExecutor; + Executor private _newAdminExecutor; + + function setUp() external { + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); + _newAdminExecutor = new Executor({owner: address(this), operator: address(_timelock)}); + + _oldAdminExecutor = Executor(payable(_timelock.getAdminExecutor())); + _newAdminExecutor.transferOwnership(address(_newAdminExecutor)); + } + + function testFork_ExecutorOwnershipTransfer_HappyPath() external { + _step("1. DAO creates proposal to add new proposer and change the admin executor"); + uint256 shuffleExecutorsProposalId; + { + ExternalCall[] memory executorsShuffleCalls = ExternalCallHelpers.create( + [ + // 1. Register new proposer and assign it to the old admin executor + ExternalCall({ + value: 0, + target: address(_dualGovernance), + payload: abi.encodeCall( + _dualGovernance.registerProposer, (_NEW_REGULAR_PROPOSER, address(_oldAdminExecutor)) + ) + }), + // 2. Assign previous proposer (Aragon Voting) to the new executor + ExternalCall({ + value: 0, + target: address(_dualGovernance), + payload: abi.encodeCall( + _dualGovernance.setProposerExecutor, (address(_lido.voting), address(_newAdminExecutor)) + ) + }), + // 3. Make new admin executor owner of the previous admin executor + ExternalCall({ + value: 0, + target: address(_oldAdminExecutor), + payload: abi.encodeCall(Ownable.transferOwnership, (address(_newAdminExecutor))) + }), + // 4. Replace the admin executor of the Timelock contract + ExternalCall({ + value: 0, + target: address(_timelock), + payload: abi.encodeCall(_timelock.setAdminExecutor, (address(_newAdminExecutor))) + }) + ] + ); + shuffleExecutorsProposalId = + _submitProposalViaDualGovernance("Register new proposer and swap executors", executorsShuffleCalls); + } + + _step("2. Proposal is scheduled and executed"); + { + _assertProposalSubmitted(shuffleExecutorsProposalId); + _waitAfterSubmitDelayPassed(); + + _scheduleProposalViaDualGovernance(shuffleExecutorsProposalId); + _assertProposalScheduled(shuffleExecutorsProposalId); + _waitAfterScheduleDelayPassed(); + + _executeProposal(shuffleExecutorsProposalId); + } + _step("3. The proposers and executors were set up correctly"); + { + assertEq(_oldAdminExecutor.owner(), address(_newAdminExecutor)); + assertEq(_timelock.getAdminExecutor(), address(_newAdminExecutor)); + + Proposers.Proposer[] memory proposers = _dualGovernance.getProposers(); + + assertEq(proposers.length, 2); + + assertEq(proposers[0].account, address(_lido.voting)); + assertEq(proposers[0].executor, address(_newAdminExecutor)); + + assertEq(proposers[1].account, _NEW_REGULAR_PROPOSER); + assertEq(proposers[1].executor, address(_oldAdminExecutor)); + } + + _step("4. New admin proposer can manage Dual Governance contracts"); + { + ExternalCall[] memory dgManageOperations = ExternalCallHelpers.create( + [ + ExternalCall({ + value: 0, + target: address(_dualGovernance), + payload: abi.encodeCall(_dualGovernance.unregisterProposer, (_NEW_REGULAR_PROPOSER)) + }), + ExternalCall({ + value: 0, + target: address(_timelock), + payload: abi.encodeCall(_timelock.setupDelays, (Durations.from(5 days), Durations.ZERO)) + }), + ExternalCall({ + value: 0, + target: address(_oldAdminExecutor), + payload: abi.encodeCall(_oldAdminExecutor.setOperator, (_NEW_REGULAR_PROPOSER)) + }) + ] + ); + + uint256 proposalId = + _submitProposalViaDualGovernance("Manage Dual Governance parameters", dgManageOperations); + + _assertProposalSubmitted(proposalId); + _waitAfterSubmitDelayPassed(); + + _scheduleProposalViaDualGovernance(proposalId); + _assertProposalScheduled(proposalId); + _waitAfterScheduleDelayPassed(); + + _executeProposal(proposalId); + + Proposers.Proposer[] memory proposers = _dualGovernance.getProposers(); + + assertEq(proposers.length, 1); + assertEq(proposers[0].account, address(_lido.voting)); + assertEq(proposers[0].executor, address(_newAdminExecutor)); + + assertEq(_timelock.getAfterScheduleDelay(), Durations.ZERO); + + assertEq(_oldAdminExecutor.getOperator(), _NEW_REGULAR_PROPOSER); + } + } +} From 0f583d8e60bbc6fa8279d6befe5e20e13ca848da Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 27 Nov 2024 14:42:58 +0400 Subject: [PATCH 067/107] =?UTF-8?q?Revert=20the=20changes=20in=20the=20Exe?= =?UTF-8?q?cutor=E2=80=99s=20ownership=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/EmergencyProtectedTimelock.sol | 8 +++ contracts/Executor.sol | 50 +------------------ scripts/deploy/ContractsDeployment.sol | 9 ++-- ....sol => executor-ownership-transfer.t.sol} | 37 +++++++++----- test/unit/DualGovernance.t.sol | 2 +- test/unit/EmergencyProtectedTimelock.t.sol | 5 +- test/unit/libraries/ExecutableProposals.t.sol | 2 +- 7 files changed, 42 insertions(+), 71 deletions(-) rename test/scenario/{exeuctor-ownership-transfer.t.sol => executor-ownership-transfer.t.sol} (85%) diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 33e2c43d..0fdc1daf 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -167,6 +167,14 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { _timelockState.checkExecutionDelay(MIN_EXECUTION_DELAY); } + /// @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 { + _checkCallerIsAdminExecutor(); + IOwnable(executor).transferOwnership(owner); + } + // --- // Emergency Protection Functionality // --- diff --git a/contracts/Executor.sol b/contracts/Executor.sol index 443ca275..0ef5139a 100644 --- a/contracts/Executor.sol +++ b/contracts/Executor.sol @@ -14,29 +14,13 @@ contract Executor is IExternalExecutor, Ownable { // Events // --- - event OperatorSet(address indexed newOperator); event Execute(address indexed sender, address indexed target, uint256 ethValue, bytes data); - // --- - // Errors - // --- - - error CallerIsNotOperator(address caller); - error InvalidOperator(address account); - - // --- - // Storage variables - // --- - - address private _operator; - // --- // Constructor // --- - constructor(address owner, address operator) Ownable(owner) { - _setOperator(operator); - } + constructor(address owner) Ownable(owner) {} // --- // Main Functionality @@ -47,41 +31,11 @@ contract Executor is IExternalExecutor, Ownable { /// @param value The amount of ether (in wei) to send with the function call. /// @param payload The calldata for the function call. function execute(address target, uint256 value, bytes calldata payload) external payable { - if (msg.sender != _operator) { - revert CallerIsNotOperator(msg.sender); - } + _checkOwner(); Address.functionCallWithValue(target, payload, value); emit Execute(msg.sender, target, value, payload); } - // --- - // Management Operations - // --- - - /// @notice Allows the owner to set a new operator. - /// @param newOperator The address of the new operator. - function setOperator(address newOperator) external onlyOwner { - _setOperator(newOperator); - } - - /// @notice Returns the current operator. - /// @return operator The address of the current operator. - function getOperator() external view returns (address operator) { - return _operator; - } - /// @notice Allows the contract to receive ether. receive() external payable {} - - // --- - // Internal Methods - // --- - - function _setOperator(address newOperator) internal { - if (newOperator == address(0) || newOperator == _operator) { - revert InvalidOperator(newOperator); - } - _operator = newOperator; - emit OperatorSet(newOperator); - } } diff --git a/scripts/deploy/ContractsDeployment.sol b/scripts/deploy/ContractsDeployment.sol index 89483957..dcd84c3e 100644 --- a/scripts/deploy/ContractsDeployment.sol +++ b/scripts/deploy/ContractsDeployment.sol @@ -96,7 +96,7 @@ library DGContractsDeployment { DeployConfig memory dgDeployConfig, address deployer ) internal returns (DeployedContracts memory contracts) { - Executor adminExecutor = deployExecutor({owner: deployer, operator: deployer}); + Executor adminExecutor = deployExecutor({owner: deployer}); EmergencyProtectedTimelock timelock = deployEmergencyProtectedTimelock(address(adminExecutor), dgDeployConfig); contracts.adminExecutor = adminExecutor; @@ -150,8 +150,8 @@ library DGContractsDeployment { ); } - function deployExecutor(address owner, address operator) internal returns (Executor) { - return new Executor(owner, operator); + function deployExecutor(address owner) internal returns (Executor) { + return new Executor(owner); } function deployEmergencyProtectedTimelock( @@ -330,7 +330,6 @@ library DGContractsDeployment { DeployConfig memory dgDeployConfig ) internal { adminExecutor.execute(address(timelock), 0, abi.encodeCall(timelock.setGovernance, (dualGovernance))); - adminExecutor.setOperator(address(timelock)); - adminExecutor.transferOwnership(address(adminExecutor)); + adminExecutor.transferOwnership(address(timelock)); } } diff --git a/test/scenario/exeuctor-ownership-transfer.t.sol b/test/scenario/executor-ownership-transfer.t.sol similarity index 85% rename from test/scenario/exeuctor-ownership-transfer.t.sol rename to test/scenario/executor-ownership-transfer.t.sol index 69bf7b5c..6eaf3841 100644 --- a/test/scenario/exeuctor-ownership-transfer.t.sol +++ b/test/scenario/executor-ownership-transfer.t.sol @@ -8,6 +8,10 @@ import {Proposers} from "contracts/libraries/Proposers.sol"; import {ExternalCall, ScenarioTestBlueprint, ExternalCallHelpers} from "../utils/scenario-test-blueprint.sol"; +interface ISomeContract { + function someMethod(uint256 someParameter) external; +} + contract ExecutorOwnershipTransfer is ScenarioTestBlueprint { address private immutable _NEW_REGULAR_PROPOSER = makeAddr("NEW_REGULAR_PROPOSER"); @@ -16,10 +20,10 @@ contract ExecutorOwnershipTransfer is ScenarioTestBlueprint { function setUp() external { _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); - _newAdminExecutor = new Executor({owner: address(this), operator: address(_timelock)}); + _newAdminExecutor = new Executor({owner: address(this)}); _oldAdminExecutor = Executor(payable(_timelock.getAdminExecutor())); - _newAdminExecutor.transferOwnership(address(_newAdminExecutor)); + _newAdminExecutor.transferOwnership(address(_timelock)); } function testFork_ExecutorOwnershipTransfer_HappyPath() external { @@ -44,13 +48,7 @@ contract ExecutorOwnershipTransfer is ScenarioTestBlueprint { _dualGovernance.setProposerExecutor, (address(_lido.voting), address(_newAdminExecutor)) ) }), - // 3. Make new admin executor owner of the previous admin executor - ExternalCall({ - value: 0, - target: address(_oldAdminExecutor), - payload: abi.encodeCall(Ownable.transferOwnership, (address(_newAdminExecutor))) - }), - // 4. Replace the admin executor of the Timelock contract + // 3. Replace the admin executor of the Timelock contract ExternalCall({ value: 0, target: address(_timelock), @@ -75,7 +73,6 @@ contract ExecutorOwnershipTransfer is ScenarioTestBlueprint { } _step("3. The proposers and executors were set up correctly"); { - assertEq(_oldAdminExecutor.owner(), address(_newAdminExecutor)); assertEq(_timelock.getAdminExecutor(), address(_newAdminExecutor)); Proposers.Proposer[] memory proposers = _dualGovernance.getProposers(); @@ -101,12 +98,26 @@ contract ExecutorOwnershipTransfer is ScenarioTestBlueprint { ExternalCall({ value: 0, target: address(_timelock), - payload: abi.encodeCall(_timelock.setupDelays, (Durations.from(5 days), Durations.ZERO)) + payload: abi.encodeCall( + _timelock.transferExecutorOwnership, (address(_oldAdminExecutor), address(_newAdminExecutor)) + ) + }), + ExternalCall({ + value: 0, + target: address(_oldAdminExecutor), + payload: abi.encodeCall( + _oldAdminExecutor.execute, (address(_targetMock), 0, abi.encodeCall(ISomeContract.someMethod, (42))) + ) }), ExternalCall({ value: 0, target: address(_oldAdminExecutor), - payload: abi.encodeCall(_oldAdminExecutor.setOperator, (_NEW_REGULAR_PROPOSER)) + payload: abi.encodeCall(_oldAdminExecutor.transferOwnership, (address(_timelock))) + }), + ExternalCall({ + value: 0, + target: address(_timelock), + payload: abi.encodeCall(_timelock.setupDelays, (Durations.from(5 days), Durations.ZERO)) }) ] ); @@ -131,7 +142,7 @@ contract ExecutorOwnershipTransfer is ScenarioTestBlueprint { assertEq(_timelock.getAfterScheduleDelay(), Durations.ZERO); - assertEq(_oldAdminExecutor.getOperator(), _NEW_REGULAR_PROPOSER); + assertEq(_oldAdminExecutor.owner(), address(_timelock)); } } } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index b33c26f8..80890e2d 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -36,7 +36,7 @@ import {SealableMock} from "test/mocks/SealableMock.sol"; import {computeAddress} from "test/utils/addresses.sol"; contract DualGovernanceUnitTests is UnitTest { - Executor private _executor = new Executor(address(this), address(this)); + Executor private _executor = new Executor(address(this)); address private vetoer = makeAddr("vetoer"); address private resealCommittee = makeAddr("resealCommittee"); diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index b45510b7..711aefa6 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -48,7 +48,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { Duration private _defaultAfterScheduleDelay = Durations.from(2 days); function setUp() external { - _executor = new Executor(address(this), address(this)); + _executor = new Executor(address(this)); _adminExecutor = address(_executor); _timelock = _deployEmergencyProtectedTimelock(); @@ -56,8 +56,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _targetMock = new TargetMock(); _anotherTargetMock = new TargetMock(); - _executor.setOperator(address(_timelock)); - _executor.transferOwnership(address(_executor)); + _executor.transferOwnership(address(_timelock)); vm.startPrank(_adminExecutor); _timelock.setGovernance(_dualGovernance); diff --git a/test/unit/libraries/ExecutableProposals.t.sol b/test/unit/libraries/ExecutableProposals.t.sol index 4919fef2..d95c328a 100644 --- a/test/unit/libraries/ExecutableProposals.t.sol +++ b/test/unit/libraries/ExecutableProposals.t.sol @@ -27,7 +27,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function setUp() external { _targetMock = new TargetMock(); - _executor = new Executor(address(this), address(this)); + _executor = new Executor(address(this)); } function test_submit_reverts_if_empty_proposals() external { From 5af27ca4b9c16d428cdd788b15087fa8b542f847 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 28 Nov 2024 03:52:15 +0400 Subject: [PATCH 068/107] Update agent as executor tests --- test/scenario/agent-timelock.t.sol | 177 +++++++++++++++-------------- 1 file changed, 89 insertions(+), 88 deletions(-) diff --git a/test/scenario/agent-timelock.t.sol b/test/scenario/agent-timelock.t.sol index 2546c6da..80756884 100644 --- a/test/scenario/agent-timelock.t.sol +++ b/test/scenario/agent-timelock.t.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {ExternalCall, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; +import {Proposers} from "contracts/libraries/Proposers.sol"; + +import {ExternalCall, ExternalCallHelpers, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; import {LidoUtils} from "../utils/lido-utils.sol"; contract AgentTimelockTest is ScenarioTestBlueprint { @@ -12,139 +14,138 @@ contract AgentTimelockTest is ScenarioTestBlueprint { } function testFork_AragonAgentAsExecutor_HappyPath() external { - _step("0. Tweak the default setup to make Agent admin executor"); - - // set the Agent as admin executor in the timelock contract - vm.prank(address(_adminExecutor)); - _timelock.setAdminExecutor(address(_lido.agent)); - - assertEq(_timelock.getAdminExecutor(), address(_lido.agent)); + _step("0. Grant EXECUTE_ROLE permission to the timelock on the Agent contract"); + { + _lido.grantPermission(address(_lido.agent), _lido.agent.EXECUTE_ROLE(), address(_timelock)); + assertTrue(_lido.acl.hasPermission(address(_timelock), address(_lido.agent), _lido.agent.EXECUTE_ROLE())); + } - // grant EXECUTE_ROLE permission to the timelock contract to allow call Agent.execute() method - _lido.grantPermission({app: address(_lido.agent), role: _lido.agent.EXECUTE_ROLE(), grantee: address(_timelock)}); + address agentProposer = makeAddr("AGENT_PROPOSER"); + _step("1. Submit proposal to register Aragon as the executor"); + { + ExternalCall[] memory externalCalls = ExternalCallHelpers.create( + [ + ExternalCall({ + value: 0, + target: address(_dualGovernance), + payload: abi.encodeCall(_dualGovernance.registerProposer, (agentProposer, address(_lido.agent))) + }) + ] + ); + uint256 addAgentProposerProposalId = + _submitProposalViaDualGovernance("Add Aragon Agent as proposer to the Dual Governance", externalCalls); - // update proposers for the Voting - vm.startPrank(address(_lido.agent)); + _assertProposalSubmitted(addAgentProposerProposalId); + _waitAfterSubmitDelayPassed(); - address tmpProposer = makeAddr("tmpProposer"); + _scheduleProposalViaDualGovernance(addAgentProposerProposalId); + _assertProposalScheduled(addAgentProposerProposalId); - _dualGovernance.registerProposer(tmpProposer, address(_lido.agent)); - _dualGovernance.unregisterProposer(address(_lido.voting)); + _waitAfterScheduleDelayPassed(); + _executeProposal(addAgentProposerProposalId); + _assertProposalExecuted(addAgentProposerProposalId); - _dualGovernance.registerProposer(address(_lido.voting), address(_lido.agent)); - _dualGovernance.unregisterProposer(tmpProposer); - vm.stopPrank(); + Proposers.Proposer[] memory proposers = _dualGovernance.getProposers(); - _step("Test setup preparations have done!"); + assertEq(proposers.length, 2); + assertEq(proposers[1].account, agentProposer); + assertEq(proposers[1].executor, address(_lido.agent)); + } ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); - uint256 proposalId; - _step("1. THE PROPOSAL IS SUBMITTED"); + uint256 agentActionsProposalId; + _step("2. Submit proposal via the Agent proposer"); { - proposalId = _submitProposalViaDualGovernance( - "Propose to doSmth on target passing dual governance", regularStaffCalls - ); + vm.prank(agentProposer); + agentActionsProposalId = + _dualGovernance.submitProposal(regularStaffCalls, "Make regular staff using Agent as executor"); - _assertSubmittedProposalData(proposalId, _getAdminExecutor(), regularStaffCalls); - _assertCanScheduleViaDualGovernance(proposalId, false); + _assertSubmittedProposalData(agentActionsProposalId, address(_lido.agent), regularStaffCalls); } - _step("2. THE PROPOSAL IS SCHEDULED"); + _step("3. Execute the proposal"); { + _assertProposalSubmitted(agentActionsProposalId); _waitAfterSubmitDelayPassed(); - _assertCanScheduleViaDualGovernance(proposalId, true); - _scheduleProposalViaDualGovernance(proposalId); - _assertProposalScheduled(proposalId); - _assertCanExecute(proposalId, false); - } + _scheduleProposalViaDualGovernance(agentActionsProposalId); + _assertProposalScheduled(agentActionsProposalId); - _step("3. THE PROPOSAL CAN BE EXECUTED"); - { - // wait until the second delay has passed _waitAfterScheduleDelayPassed(); - - // Now proposal can be executed - _assertCanExecute(proposalId, true); - - _assertNoTargetMockCalls(); - - _executeProposal(proposalId); - _assertProposalExecuted(proposalId); - - _assertCanExecute(proposalId, false); - _assertCanScheduleViaDualGovernance(proposalId, false); + _executeProposal(agentActionsProposalId); + _assertProposalExecuted(agentActionsProposalId); _assertTargetMockCalls(address(_lido.agent), regularStaffCalls); } } function testFork_AragonAgentAsExecutor_RevertOn_FailedCall() external { - _step("0. Tweak the default setup to make Agent admin executor"); - - // set the Agent as admin executor in the timelock contract - vm.prank(address(_adminExecutor)); - _timelock.setAdminExecutor(address(_lido.agent)); - - assertEq(_timelock.getAdminExecutor(), address(_lido.agent)); + _step("0. Grant EXECUTE_ROLE permission to the timelock on the Agent contract"); + { + _lido.grantPermission(address(_lido.agent), _lido.agent.EXECUTE_ROLE(), address(_timelock)); + assertTrue(_lido.acl.hasPermission(address(_timelock), address(_lido.agent), _lido.agent.EXECUTE_ROLE())); + } - // grant EXECUTE_ROLE permission to the timelock contract to allow call Agent.execute() method - _lido.grantPermission({app: address(_lido.agent), role: _lido.agent.EXECUTE_ROLE(), grantee: address(_timelock)}); + address agentProposer = makeAddr("AGENT_PROPOSER"); + _step("1. Submit proposal to register Aragon as the executor"); + { + ExternalCall[] memory externalCalls = ExternalCallHelpers.create( + [ + ExternalCall({ + value: 0, + target: address(_dualGovernance), + payload: abi.encodeCall(_dualGovernance.registerProposer, (agentProposer, address(_lido.agent))) + }) + ] + ); + uint256 addAgentProposerProposalId = + _submitProposalViaDualGovernance("Add Aragon Agent as proposer to the Dual Governance", externalCalls); - // update proposers for the Voting - vm.startPrank(address(_lido.agent)); + _assertProposalSubmitted(addAgentProposerProposalId); + _waitAfterSubmitDelayPassed(); - address tmpProposer = makeAddr("tmpProposer"); + _scheduleProposalViaDualGovernance(addAgentProposerProposalId); + _assertProposalScheduled(addAgentProposerProposalId); - _dualGovernance.registerProposer(tmpProposer, address(_lido.agent)); - _dualGovernance.unregisterProposer(address(_lido.voting)); + _waitAfterScheduleDelayPassed(); + _executeProposal(addAgentProposerProposalId); + _assertProposalExecuted(addAgentProposerProposalId); - _dualGovernance.registerProposer(address(_lido.voting), address(_lido.agent)); - _dualGovernance.unregisterProposer(tmpProposer); - vm.stopPrank(); + Proposers.Proposer[] memory proposers = _dualGovernance.getProposers(); - _step("Test setup preparations have done!"); + assertEq(proposers.length, 2); + assertEq(proposers[1].account, agentProposer); + assertEq(proposers[1].executor, address(_lido.agent)); + } ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); - - // code to invalid contract - regularStaffCalls[0].target = makeAddr("invalidTarget"); vm.mockCallRevert(regularStaffCalls[0].target, regularStaffCalls[0].payload, "INVALID TARGET"); - uint256 proposalId; - _step("1. THE PROPOSAL IS SUBMITTED"); + uint256 agentActionsProposalId; + _step("2. Submit proposal which should revert via the Agent proposer"); { - proposalId = _submitProposalViaDualGovernance( - "Propose to doSmth on target passing dual governance", regularStaffCalls - ); + vm.prank(agentProposer); + agentActionsProposalId = + _dualGovernance.submitProposal(regularStaffCalls, "Make regular staff using Agent as executor"); - _assertSubmittedProposalData(proposalId, _getAdminExecutor(), regularStaffCalls); - _assertCanScheduleViaDualGovernance(proposalId, false); + _assertSubmittedProposalData(agentActionsProposalId, address(_lido.agent), regularStaffCalls); } - _step("2. THE PROPOSAL IS SCHEDULED"); + _step("3. The execution of the proposal fails"); { + _assertProposalSubmitted(agentActionsProposalId); _waitAfterSubmitDelayPassed(); - _assertCanScheduleViaDualGovernance(proposalId, true); - _scheduleProposalViaDualGovernance(proposalId); - _assertProposalScheduled(proposalId); - _assertCanExecute(proposalId, false); - } + _scheduleProposalViaDualGovernance(agentActionsProposalId); + _assertProposalScheduled(agentActionsProposalId); - _step("3. THE PROPOSAL CAN BE EXECUTED"); - { - // wait until the second delay has passed _waitAfterScheduleDelayPassed(); - // Now proposal can be executed - _assertCanExecute(proposalId, true); + vm.expectRevert("INVALID TARGET"); + _executeProposal(agentActionsProposalId); _assertNoTargetMockCalls(); - - vm.expectRevert("INVALID TARGET"); - _executeProposal(proposalId); } } From db161b9b12a5105211c7f16a36cf981845d4ef2a Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 28 Nov 2024 04:19:07 +0400 Subject: [PATCH 069/107] Add scenario test with calls to EOA from agent --- test/scenario/agent-timelock.t.sol | 160 ++++++++++++++++++----------- 1 file changed, 102 insertions(+), 58 deletions(-) diff --git a/test/scenario/agent-timelock.t.sol b/test/scenario/agent-timelock.t.sol index 80756884..e74756cb 100644 --- a/test/scenario/agent-timelock.t.sol +++ b/test/scenario/agent-timelock.t.sol @@ -6,6 +6,10 @@ import {Proposers} from "contracts/libraries/Proposers.sol"; import {ExternalCall, ExternalCallHelpers, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; import {LidoUtils} from "../utils/lido-utils.sol"; +interface IRegularContract { + function regularMethod() external; +} + contract AgentTimelockTest is ScenarioTestBlueprint { using LidoUtils for LidoUtils.Context; @@ -16,40 +20,13 @@ contract AgentTimelockTest is ScenarioTestBlueprint { function testFork_AragonAgentAsExecutor_HappyPath() external { _step("0. Grant EXECUTE_ROLE permission to the timelock on the Agent contract"); { - _lido.grantPermission(address(_lido.agent), _lido.agent.EXECUTE_ROLE(), address(_timelock)); - assertTrue(_lido.acl.hasPermission(address(_timelock), address(_lido.agent), _lido.agent.EXECUTE_ROLE())); + _grantAgentExecutorRoleToTimelock(); } address agentProposer = makeAddr("AGENT_PROPOSER"); _step("1. Submit proposal to register Aragon as the executor"); { - ExternalCall[] memory externalCalls = ExternalCallHelpers.create( - [ - ExternalCall({ - value: 0, - target: address(_dualGovernance), - payload: abi.encodeCall(_dualGovernance.registerProposer, (agentProposer, address(_lido.agent))) - }) - ] - ); - uint256 addAgentProposerProposalId = - _submitProposalViaDualGovernance("Add Aragon Agent as proposer to the Dual Governance", externalCalls); - - _assertProposalSubmitted(addAgentProposerProposalId); - _waitAfterSubmitDelayPassed(); - - _scheduleProposalViaDualGovernance(addAgentProposerProposalId); - _assertProposalScheduled(addAgentProposerProposalId); - - _waitAfterScheduleDelayPassed(); - _executeProposal(addAgentProposerProposalId); - _assertProposalExecuted(addAgentProposerProposalId); - - Proposers.Proposer[] memory proposers = _dualGovernance.getProposers(); - - assertEq(proposers.length, 2); - assertEq(proposers[1].account, agentProposer); - assertEq(proposers[1].executor, address(_lido.agent)); + _addAragonAgentProposer(agentProposer); } ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); @@ -83,40 +60,13 @@ contract AgentTimelockTest is ScenarioTestBlueprint { function testFork_AragonAgentAsExecutor_RevertOn_FailedCall() external { _step("0. Grant EXECUTE_ROLE permission to the timelock on the Agent contract"); { - _lido.grantPermission(address(_lido.agent), _lido.agent.EXECUTE_ROLE(), address(_timelock)); - assertTrue(_lido.acl.hasPermission(address(_timelock), address(_lido.agent), _lido.agent.EXECUTE_ROLE())); + _grantAgentExecutorRoleToTimelock(); } address agentProposer = makeAddr("AGENT_PROPOSER"); _step("1. Submit proposal to register Aragon as the executor"); { - ExternalCall[] memory externalCalls = ExternalCallHelpers.create( - [ - ExternalCall({ - value: 0, - target: address(_dualGovernance), - payload: abi.encodeCall(_dualGovernance.registerProposer, (agentProposer, address(_lido.agent))) - }) - ] - ); - uint256 addAgentProposerProposalId = - _submitProposalViaDualGovernance("Add Aragon Agent as proposer to the Dual Governance", externalCalls); - - _assertProposalSubmitted(addAgentProposerProposalId); - _waitAfterSubmitDelayPassed(); - - _scheduleProposalViaDualGovernance(addAgentProposerProposalId); - _assertProposalScheduled(addAgentProposerProposalId); - - _waitAfterScheduleDelayPassed(); - _executeProposal(addAgentProposerProposalId); - _assertProposalExecuted(addAgentProposerProposalId); - - Proposers.Proposer[] memory proposers = _dualGovernance.getProposers(); - - assertEq(proposers.length, 2); - assertEq(proposers[1].account, agentProposer); - assertEq(proposers[1].executor, address(_lido.agent)); + _addAragonAgentProposer(agentProposer); } ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); @@ -149,6 +99,61 @@ contract AgentTimelockTest is ScenarioTestBlueprint { } } + function testFork_AgentAsExecutor_SucceedOnEmptyAccountCalls() external { + _step("0. Grant EXECUTE_ROLE permission to the timelock on the Agent contract"); + { + _grantAgentExecutorRoleToTimelock(); + } + + address agentProposer = makeAddr("AGENT_PROPOSER"); + _step("1. Submit proposal to register Aragon as the executor"); + { + _addAragonAgentProposer(agentProposer); + } + + uint256 callValue = 1 ether; + address nonContractAccount = makeAddr("NOT_CONTRACT"); + ExternalCall[] memory callsToEmptyAccount = ExternalCallHelpers.create( + [ + ExternalCall({value: 0, target: nonContractAccount, payload: new bytes(0)}), + ExternalCall({ + value: 0, + target: nonContractAccount, + payload: abi.encodeCall(IRegularContract.regularMethod, ()) + }), + ExternalCall({value: uint96(callValue), target: nonContractAccount, payload: new bytes(0)}) + ] + ); + uint256 agentBalanceBefore = address(_lido.agent).balance; + vm.deal(address(_lido.agent), agentBalanceBefore + callValue); + + uint256 agentActionsProposalId; + _step("2. Submit proposal via the Agent proposer"); + { + vm.prank(agentProposer); + agentActionsProposalId = + _dualGovernance.submitProposal(callsToEmptyAccount, "Make different calls to EOA account"); + + _assertSubmittedProposalData(agentActionsProposalId, address(_lido.agent), callsToEmptyAccount); + } + + _step("3. Execute the proposal"); + { + _assertProposalSubmitted(agentActionsProposalId); + _waitAfterSubmitDelayPassed(); + + _scheduleProposalViaDualGovernance(agentActionsProposalId); + _assertProposalScheduled(agentActionsProposalId); + + _waitAfterScheduleDelayPassed(); + _executeProposal(agentActionsProposalId); + _assertProposalExecuted(agentActionsProposalId); + + assertEq(nonContractAccount.balance, 1 ether); + assertEq(address(_lido.agent).balance, agentBalanceBefore); + } + } + function testFork_AgentTimelockHappyPath() external { ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); @@ -251,4 +256,43 @@ contract AgentTimelockTest is ScenarioTestBlueprint { _assertProposalCancelled(proposalId); } } + + // --- + // Helper Methods + // --- + + function _grantAgentExecutorRoleToTimelock() internal { + _lido.grantPermission(address(_lido.agent), _lido.agent.EXECUTE_ROLE(), address(_timelock)); + assertTrue(_lido.acl.hasPermission(address(_timelock), address(_lido.agent), _lido.agent.EXECUTE_ROLE())); + } + + function _addAragonAgentProposer(address agentProposer) internal { + ExternalCall[] memory externalCalls = ExternalCallHelpers.create( + [ + ExternalCall({ + value: 0, + target: address(_dualGovernance), + payload: abi.encodeCall(_dualGovernance.registerProposer, (agentProposer, address(_lido.agent))) + }) + ] + ); + uint256 addAgentProposerProposalId = + _submitProposalViaDualGovernance("Add Aragon Agent as proposer to the Dual Governance", externalCalls); + + _assertProposalSubmitted(addAgentProposerProposalId); + _waitAfterSubmitDelayPassed(); + + _scheduleProposalViaDualGovernance(addAgentProposerProposalId); + _assertProposalScheduled(addAgentProposerProposalId); + + _waitAfterScheduleDelayPassed(); + _executeProposal(addAgentProposerProposalId); + _assertProposalExecuted(addAgentProposerProposalId); + + Proposers.Proposer[] memory proposers = _dualGovernance.getProposers(); + + assertEq(proposers.length, 2); + assertEq(proposers[1].account, agentProposer); + assertEq(proposers[1].executor, address(_lido.agent)); + } } From 4b9a8e5e4cb51cb46760e9e9d852fe4a0500d5cb Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 28 Nov 2024 04:19:24 +0400 Subject: [PATCH 070/107] Update Executor natspec --- contracts/Executor.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/Executor.sol b/contracts/Executor.sol index 0ef5139a..53e64014 100644 --- a/contracts/Executor.sol +++ b/contracts/Executor.sol @@ -7,8 +7,8 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IExternalExecutor} from "./interfaces/IExternalExecutor.sol"; /// @title Executor -/// @notice Allows the designated operator to execute external function calls on specified target contracts with -/// possible value transfers. The owner can set the operator. +/// @notice Allows the contract owner to execute external function calls on specified target contracts with +/// possible value transfers. contract Executor is IExternalExecutor, Ownable { // --- // Events @@ -26,7 +26,7 @@ contract Executor is IExternalExecutor, Ownable { // Main Functionality // --- - /// @notice Allows the operator to execute external function calls on target contracts, optionally transferring ether. + /// @notice Allows the contract owner to execute external function calls on target contracts, optionally transferring ether. /// @param target The address of the target contract on which to execute the function call. /// @param value The amount of ether (in wei) to send with the function call. /// @param payload The calldata for the function call. From 64a212c59178de39eb7f5388acea710bf94a915c Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 28 Nov 2024 04:40:20 +0400 Subject: [PATCH 071/107] Naming convention for the proposers and executors --- contracts/DualGovernance.sol | 34 ++++++------ contracts/EmergencyProtectedTimelock.sol | 2 +- contracts/interfaces/IDualGovernance.sol | 4 +- contracts/interfaces/ITimelock.sol | 2 +- contracts/libraries/Proposers.sol | 44 ++++++++-------- scripts/deploy/DeployVerification.sol | 8 ++- .../executor-ownership-transfer.t.sol | 7 ++- test/unit/DualGovernance.t.sol | 52 +++++++++---------- test/unit/libraries/Proposers.t.sol | 38 +++++++------- 9 files changed, 100 insertions(+), 91 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 6b856ead..19d4426d 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -397,21 +397,13 @@ contract DualGovernance is IDualGovernance { _proposers.checkRegisteredExecutor(msg.sender); } - /// @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. + /// @notice Returns the proposer data if the given `proposerAccount` is a registered proposer. + /// @param proposerAccount 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); + function getProposer(address proposerAccount) external view returns (Proposers.Proposer memory proposer) { + proposer = _proposers.getProposer(proposerAccount); } /// @notice Returns the information about all registered proposers. @@ -420,12 +412,20 @@ contract DualGovernance is IDualGovernance { 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 + /// @notice Checks whether the given `proposerAccount` is a registered proposer. + /// @param proposerAccount The address to check. + /// @return isProposer A boolean value indicating whether the `proposerAccount` is a registered + /// proposer (`true`) or not (`false`). + function isRegisteredProposer(address proposerAccount) external view returns (bool) { + return _proposers.isRegisteredProposer(proposerAccount); + } + + /// @notice Checks whether the given `executor` address is associated with an executor contract in the system. + /// @param executor The address to check. + /// @return isExecutor A boolean value indicating whether the `executor` is a registered /// executor (`true`) or not (`false`). - function isExecutor(address account) external view returns (bool) { - return _proposers.isExecutor(account); + function isRegisteredExecutor(address executor) external view returns (bool) { + return _proposers.isRegisteredExecutor(executor); } // --- diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 0fdc1daf..55e4b69b 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -171,7 +171,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @param executor The address of the executor contract. /// @param owner The address of the new owner. function transferExecutorOwnership(address executor, address owner) external { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); IOwnable(executor).transferOwnership(owner); } diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 24833d07..db7596b8 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -41,10 +41,10 @@ interface IDualGovernance is IGovernance, ITiebreaker { function registerProposer(address proposer, address executor) external; function unregisterProposer(address proposer) external; - function isProposer(address account) external view returns (bool); + function isRegisteredProposer(address account) external view returns (bool); function getProposer(address account) external view returns (Proposers.Proposer memory proposer); function getProposers() external view returns (Proposers.Proposer[] memory proposers); - function isExecutor(address account) external view returns (bool); + function isRegisteredExecutor(address account) external view returns (bool); function resealSealable(address sealable) external; function setResealCommittee(address resealCommittee) external; diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index 618420d4..f94fe3e6 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -46,5 +46,5 @@ interface ITimelock { function getAfterScheduleDelay() external view returns (Duration); function setAfterSubmitDelay(Duration newAfterSubmitDelay) external; function setAfterScheduleDelay(Duration newAfterScheduleDelay) external; - // function transferExecutorOwnership(address executor, address owner) external; + function transferExecutorOwnership(address executor, address owner) external; } diff --git a/contracts/libraries/Proposers.sol b/contracts/libraries/Proposers.sol index f8bcfeda..5bbb471c 100644 --- a/contracts/libraries/Proposers.sol +++ b/contracts/libraries/Proposers.sol @@ -10,18 +10,18 @@ library Proposers { // Errors // --- error InvalidExecutor(address executor); - error ExecutorNotRegistered(address account); - error InvalidProposerAccount(address account); - error ProposerNotRegistered(address proposer); - error ProposerAlreadyRegistered(address proposer); + error ExecutorNotRegistered(address executor); + error InvalidProposerAccount(address proposerAccount); + error ProposerNotRegistered(address proposerAccount); + error ProposerAlreadyRegistered(address proposerAccount); // --- // Events // --- - event ProposerRegistered(address indexed proposer, address indexed executor); - event ProposerExecutorSet(address indexed proposer, address indexed executor); - event ProposerUnregistered(address indexed proposer, address indexed executor); + event ProposerRegistered(address indexed proposerAccount, address indexed executor); + event ProposerExecutorSet(address indexed proposerAccount, address indexed executor); + event ProposerUnregistered(address indexed proposerAccount, address indexed executor); // --- // Data Types @@ -160,32 +160,32 @@ library Proposers { } } - /// @notice Checks if an account is a registered proposer. + /// @notice Checks if an `proposerAccount` is a registered proposer. /// @param self The context of the Proposers library. - /// @param account The address to check. - /// @return bool `true` if the account is a registered proposer, otherwise `false`. - function isProposer(Context storage self, address account) internal view returns (bool) { - return _isRegisteredProposer(self.executors[account]); + /// @param proposerAccount The address to check. + /// @return bool `true` if the `proposerAccount` is a registered proposer, otherwise `false`. + function isRegisteredProposer(Context storage self, address proposerAccount) internal view returns (bool) { + return _isRegisteredProposer(self.executors[proposerAccount]); } - /// @notice Checks if an account is an executor associated with any proposer. + /// @notice Checks if an `executor` address is an executor associated with any proposer. /// @param self The context of the Proposers library. - /// @param account The address to check. - /// @return bool `true` if the account is an executor, otherwise `false`. - function isExecutor(Context storage self, address account) internal view returns (bool) { - return self.executorRefsCounts[account] > 0; + /// @param executor The address to check. + /// @return bool `true` if the `executor` address is an registered executor, otherwise `false`. + function isRegisteredExecutor(Context storage self, address executor) internal view returns (bool) { + return self.executorRefsCounts[executor] > 0; } // --- // Checks // --- - /// @notice Checks that a given account is a registered executor. + /// @notice Checks that a given `executor` address is a registered executor. /// @param self The storage context of the Proposers library. - /// @param account The address to verify as a registered executor. - function checkRegisteredExecutor(Context storage self, address account) internal view { - if (!isExecutor(self, account)) { - revert ExecutorNotRegistered(account); + /// @param executor The address to verify as a registered executor. + function checkRegisteredExecutor(Context storage self, address executor) internal view { + if (!isRegisteredExecutor(self, executor)) { + revert ExecutorNotRegistered(executor); } } diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index c17a68a4..be9205a8 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -243,8 +243,12 @@ library DeployVerification { require(dg.getPersistedState() == State.Normal, "Incorrect DualGovernance persisted state"); require(dg.getEffectiveState() == State.Normal, "Incorrect DualGovernance effective state"); require(dg.getProposers().length == 1, "Incorrect amount of proposers"); - require(dg.isProposer(lidoAddresses.voting) == true, "Lido voting is not set as a proposers[0]"); - require(dg.isExecutor(res.adminExecutor) == true, "adminExecutor is not set as a proposers[0].executor"); + require( + dg.isRegisteredProposer(address(lidoAddresses.voting)) == true, "Lido voting is not set as a proposers[0]" + ); + require( + dg.isRegisteredExecutor(res.adminExecutor) == true, "adminExecutor is not set as a proposers[0].executor" + ); IDualGovernance.StateDetails memory stateDetails = dg.getStateDetails(); require(stateDetails.effectiveState == State.Normal, "Incorrect DualGovernance effectiveState"); diff --git a/test/scenario/executor-ownership-transfer.t.sol b/test/scenario/executor-ownership-transfer.t.sol index 6eaf3841..48e68c7c 100644 --- a/test/scenario/executor-ownership-transfer.t.sol +++ b/test/scenario/executor-ownership-transfer.t.sol @@ -117,7 +117,12 @@ contract ExecutorOwnershipTransfer is ScenarioTestBlueprint { ExternalCall({ value: 0, target: address(_timelock), - payload: abi.encodeCall(_timelock.setupDelays, (Durations.from(5 days), Durations.ZERO)) + payload: abi.encodeCall(_timelock.setAfterSubmitDelay, (Durations.from(5 days))) + }), + ExternalCall({ + value: 0, + target: address(_timelock), + payload: abi.encodeCall(_timelock.setAfterScheduleDelay, (Durations.ZERO)) }) ] ); diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 80890e2d..8de47811 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -1375,8 +1375,8 @@ contract DualGovernanceUnitTests is UnitTest { address newProposer = makeAddr("NEW_PROPOSER"); address newExecutor = makeAddr("NEW_EXECUTOR"); - assertFalse(_dualGovernance.isProposer(newProposer)); - assertFalse(_dualGovernance.isExecutor(newExecutor)); + assertFalse(_dualGovernance.isRegisteredProposer(newProposer)); + assertFalse(_dualGovernance.isRegisteredExecutor(newExecutor)); _executor.execute( address(_dualGovernance), @@ -1384,8 +1384,8 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.registerProposer.selector, newProposer, newExecutor) ); - assertTrue(_dualGovernance.isProposer(newProposer)); - assertTrue(_dualGovernance.isExecutor(newExecutor)); + assertTrue(_dualGovernance.isRegisteredProposer(newProposer)); + assertTrue(_dualGovernance.isRegisteredExecutor(newExecutor)); Proposers.Proposer memory proposer = _dualGovernance.getProposer(newProposer); assertEq(proposer.account, newProposer); @@ -1411,20 +1411,20 @@ contract DualGovernanceUnitTests is UnitTest { address newExecutor = makeAddr("NEW_EXECUTOR"); assertEq(_dualGovernance.getProposers().length, 1); - assertFalse(_dualGovernance.isProposer(newProposer)); + assertFalse(_dualGovernance.isRegisteredProposer(newProposer)); vm.prank(address(_executor)); _dualGovernance.registerProposer(newProposer, newExecutor); assertEq(_dualGovernance.getProposers().length, 2); - assertTrue(_dualGovernance.isProposer(newProposer)); + assertTrue(_dualGovernance.isRegisteredProposer(newProposer)); vm.prank(address(_executor)); _dualGovernance.setProposerExecutor(newProposer, address(_executor)); assertEq(_dualGovernance.getProposers().length, 2); - assertTrue(_dualGovernance.isProposer(newProposer)); - assertFalse(_dualGovernance.isExecutor(newExecutor)); + assertTrue(_dualGovernance.isRegisteredProposer(newProposer)); + assertFalse(_dualGovernance.isRegisteredExecutor(newExecutor)); } function testFuzz_setProposerExecutor_RevertOn_CalledNotByAdminExecutor(address notAllowedCaller) external { @@ -1437,7 +1437,7 @@ contract DualGovernanceUnitTests is UnitTest { _dualGovernance.registerProposer(newProposer, newExecutor); assertEq(_dualGovernance.getProposers().length, 2); - assertTrue(_dualGovernance.isProposer(newProposer)); + assertTrue(_dualGovernance.isRegisteredProposer(newProposer)); vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, notAllowedCaller)); @@ -1450,8 +1450,8 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(_dualGovernance.getProposers().length, 1); - assertTrue(_dualGovernance.isProposer(address(this))); - assertTrue(_dualGovernance.isExecutor(address(_executor))); + assertTrue(_dualGovernance.isRegisteredProposer(address(this))); + assertTrue(_dualGovernance.isRegisteredExecutor(address(_executor))); vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, address(_executor))); @@ -1473,15 +1473,15 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) ); - assertTrue(_dualGovernance.isProposer(proposer)); - assertTrue(_dualGovernance.isExecutor(proposerExecutor)); + assertTrue(_dualGovernance.isRegisteredProposer(proposer)); + assertTrue(_dualGovernance.isRegisteredExecutor(proposerExecutor)); _executor.execute( address(_dualGovernance), 0, abi.encodeWithSelector(DualGovernance.unregisterProposer.selector, proposer) ); - assertFalse(_dualGovernance.isProposer(proposer)); - assertFalse(_dualGovernance.isExecutor(proposerExecutor)); + assertFalse(_dualGovernance.isRegisteredProposer(proposer)); + assertFalse(_dualGovernance.isRegisteredExecutor(proposerExecutor)); vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, proposer)); _dualGovernance.getProposer(proposer); @@ -1514,8 +1514,8 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.unregisterProposer.selector, address(this)) ); - assertTrue(_dualGovernance.isProposer(address(this))); - assertTrue(_dualGovernance.isExecutor(adminExecutor)); + assertTrue(_dualGovernance.isRegisteredProposer(address(this))); + assertTrue(_dualGovernance.isRegisteredExecutor(adminExecutor)); } // --- @@ -1526,8 +1526,8 @@ contract DualGovernanceUnitTests is UnitTest { address proposer = makeAddr("PROPOSER"); address proposerExecutor = makeAddr("PROPOSER_EXECUTOR"); - assertFalse(_dualGovernance.isProposer(proposer)); - assertFalse(_dualGovernance.isExecutor(proposerExecutor)); + assertFalse(_dualGovernance.isRegisteredProposer(proposer)); + assertFalse(_dualGovernance.isRegisteredExecutor(proposerExecutor)); _executor.execute( address(_dualGovernance), @@ -1535,14 +1535,14 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) ); - assertTrue(_dualGovernance.isProposer(proposer)); - assertTrue(_dualGovernance.isExecutor(proposerExecutor)); + assertTrue(_dualGovernance.isRegisteredProposer(proposer)); + assertTrue(_dualGovernance.isRegisteredExecutor(proposerExecutor)); } function testFuzz_isProposer_UnregisteredProposer(address proposer) external { vm.assume(proposer != address(this)); - assertFalse(_dualGovernance.isProposer(proposer)); + assertFalse(_dualGovernance.isRegisteredProposer(proposer)); } // --- @@ -1618,7 +1618,7 @@ contract DualGovernanceUnitTests is UnitTest { function test_isExecutor_HappyPath() external { address executor = makeAddr("EXECUTOR1"); - assertFalse(_dualGovernance.isExecutor(executor)); + assertFalse(_dualGovernance.isRegisteredExecutor(executor)); _executor.execute( address(_dualGovernance), @@ -1626,14 +1626,14 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(0x123), executor) ); - assertTrue(_dualGovernance.isExecutor(executor)); - assertTrue(_dualGovernance.isExecutor(address(_executor))); + assertTrue(_dualGovernance.isRegisteredExecutor(executor)); + assertTrue(_dualGovernance.isRegisteredExecutor(address(_executor))); } function testFuzz_isExecutor_UnregisteredExecutor(address executor) external { vm.assume(executor != address(_executor)); - assertFalse(_dualGovernance.isExecutor(executor)); + assertFalse(_dualGovernance.isRegisteredExecutor(executor)); } // --- diff --git a/test/unit/libraries/Proposers.t.sol b/test/unit/libraries/Proposers.t.sol index 54d96758..c64ce860 100644 --- a/test/unit/libraries/Proposers.t.sol +++ b/test/unit/libraries/Proposers.t.sol @@ -113,35 +113,35 @@ contract ProposersLibraryUnitTests is UnitTest { _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); assertEq(_proposers.proposers.length, 1); - assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); - assertTrue(_proposers.isExecutor(_ADMIN_EXECUTOR)); + assertTrue(_proposers.isRegisteredProposer(_ADMIN_PROPOSER)); + assertTrue(_proposers.isRegisteredExecutor(_ADMIN_EXECUTOR)); _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); assertEq(_proposers.proposers.length, 2); - assertTrue(_proposers.isProposer(_DEFAULT_PROPOSER)); - assertTrue(_proposers.isExecutor(_DEFAULT_EXECUTOR)); + assertTrue(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); + assertTrue(_proposers.isRegisteredExecutor(_DEFAULT_EXECUTOR)); _proposers.unregister(_DEFAULT_PROPOSER); assertEq(_proposers.proposers.length, 1); - assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); - assertFalse(_proposers.isExecutor(_DEFAULT_EXECUTOR)); + assertFalse(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); + assertFalse(_proposers.isRegisteredExecutor(_DEFAULT_EXECUTOR)); _proposers.unregister(_ADMIN_PROPOSER); assertEq(_proposers.proposers.length, 0); - assertFalse(_proposers.isProposer(_ADMIN_PROPOSER)); - assertFalse(_proposers.isExecutor(_ADMIN_EXECUTOR)); + assertFalse(_proposers.isRegisteredProposer(_ADMIN_PROPOSER)); + assertFalse(_proposers.isRegisteredExecutor(_ADMIN_EXECUTOR)); } function test_unregister_RevertOn_ProposerIsNotRegistered() external { - assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); + assertFalse(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); _proposers.unregister(_DEFAULT_PROPOSER); _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); - assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); - assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); + assertFalse(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); + assertTrue(_proposers.isRegisteredProposer(_ADMIN_PROPOSER)); vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); _proposers.unregister(_DEFAULT_PROPOSER); @@ -149,7 +149,7 @@ contract ProposersLibraryUnitTests is UnitTest { function test_uregister_Emit_ProposerUnregistered() external { _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); - assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); + assertTrue(_proposers.isRegisteredProposer(_ADMIN_PROPOSER)); vm.expectEmit(true, true, true, false); emit Proposers.ProposerUnregistered(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); @@ -163,14 +163,14 @@ contract ProposersLibraryUnitTests is UnitTest { function test_getProposer_HappyPath() external { _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); - assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); + assertTrue(_proposers.isRegisteredProposer(_ADMIN_PROPOSER)); Proposers.Proposer memory adminProposer = _proposers.getProposer(_ADMIN_PROPOSER); assertEq(adminProposer.account, _ADMIN_PROPOSER); assertEq(adminProposer.executor, _ADMIN_EXECUTOR); _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); - assertTrue(_proposers.isProposer(_DEFAULT_PROPOSER)); + assertTrue(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); Proposers.Proposer memory defaultProposer = _proposers.getProposer(_DEFAULT_PROPOSER); assertEq(defaultProposer.account, _DEFAULT_PROPOSER); @@ -178,14 +178,14 @@ contract ProposersLibraryUnitTests is UnitTest { } function test_getProposer_RevertOn_RetrievingUnregisteredProposer() external { - assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); + assertFalse(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); _proposers.getProposer(_DEFAULT_PROPOSER); _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); - assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); - assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); + assertTrue(_proposers.isRegisteredProposer(_ADMIN_PROPOSER)); + assertFalse(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); _proposers.getProposer(_DEFAULT_PROPOSER); @@ -200,7 +200,7 @@ contract ProposersLibraryUnitTests is UnitTest { assertEq(emptyProposers.length, 0); _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); - assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); + assertTrue(_proposers.isRegisteredProposer(_ADMIN_PROPOSER)); Proposers.Proposer[] memory allProposers = _proposers.getAllProposers(); assertEq(allProposers.length, 1); @@ -209,7 +209,7 @@ contract ProposersLibraryUnitTests is UnitTest { assertEq(allProposers[0].executor, _ADMIN_EXECUTOR); _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); - assertTrue(_proposers.isProposer(_DEFAULT_PROPOSER)); + assertTrue(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); allProposers = _proposers.getAllProposers(); assertEq(allProposers.length, 2); From 27ec2b3498d8bcaf9ffac36443bab3923db632aa Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 29 Nov 2024 22:28:08 +0400 Subject: [PATCH 072/107] Restore transferExecutorOwnership unit tests --- test/unit/EmergencyProtectedTimelock.t.sol | 36 ++++++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 711aefa6..b0881965 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -358,9 +358,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_setAfterSubmitDelay_RevertOn_CalledNotByAdminExecutor() external { Duration newAfterSubmitDelay = _defaultSanityCheckParams.maxAfterSubmitDelay + Durations.from(1 seconds); - vm.expectRevert( - abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, address(this)) - ); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, address(this))); _timelock.setAfterSubmitDelay(newAfterSubmitDelay); } @@ -430,9 +428,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_setAfterScheduleDelay_RevertOn_CalledNotByAdminExecutor() external { Duration newAfterScheduleDelay = _defaultSanityCheckParams.maxAfterScheduleDelay + Durations.from(1 seconds); - vm.expectRevert( - abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, address(this)) - ); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, address(this))); _timelock.setAfterScheduleDelay(newAfterScheduleDelay); } @@ -463,6 +459,34 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.getAfterScheduleDelay(), newAfterScheduleDelay); } + // EmergencyProtectedTimelock.transferExecutorOwnership() + + function testFuzz_transferExecutorOwnership_HappyPath(address newOwner) external { + vm.assume(newOwner != _adminExecutor); + vm.assume(newOwner != address(0)); + + Executor executor = new Executor(address(_timelock)); + + assertEq(executor.owner(), address(_timelock)); + + vm.prank(_adminExecutor); + + vm.expectEmit(address(executor)); + emit Ownable.OwnershipTransferred(address(_timelock), newOwner); + + _timelock.transferExecutorOwnership(address(executor), newOwner); + + assertEq(executor.owner(), newOwner); + } + + function test_transferExecutorOwnership_RevertOn_ByStranger(address stranger) external { + vm.assume(stranger != _adminExecutor); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); + _timelock.transferExecutorOwnership(_adminExecutor, makeAddr("newOwner")); + } + // EmergencyProtectedTimelock.setGovernance() function testFuzz_setGovernance_HappyPath(address newGovernance) external { From 4a9350f5125d48a4981e06c8ff6ea59394bf729d Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 28 Nov 2024 05:34:27 +0400 Subject: [PATCH 073/107] Escrow interface refactor --- contracts/DualGovernance.sol | 29 +++-- contracts/Escrow.sol | 82 +++++++++----- contracts/interfaces/IDualGovernance.sol | 2 - contracts/interfaces/IEscrow.sol | 96 ++++++++++++----- .../libraries/DualGovernanceStateMachine.sol | 27 +++-- scripts/deploy/DeployVerification.sol | 3 +- test/mocks/EscrowMock.sol | 100 ++++++++++-------- test/scenario/escrow.t.sol | 11 +- test/unit/DualGovernance.t.sol | 9 +- .../DualGovernanceStateMachine.t.sol | 34 ++++-- .../DualGovernanceStateTransitions.t.sol | 10 +- test/utils/scenario-test-blueprint.sol | 20 ++-- 12 files changed, 262 insertions(+), 161 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 19d4426d..f87982f5 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -7,7 +7,7 @@ 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 {IEscrowBase} from "./interfaces/IEscrow.sol"; import {ITimelock} from "./interfaces/ITimelock.sol"; import {ITiebreaker} from "./interfaces/ITiebreaker.sol"; import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; @@ -52,8 +52,8 @@ contract DualGovernance is IDualGovernance { event CancelAllPendingProposalsSkipped(); event CancelAllPendingProposalsExecuted(); - event EscrowMasterCopyDeployed(IEscrow escrowMasterCopy); event ProposalsCancellerSet(address proposalsCanceller); + event EscrowMasterCopyDeployed(IEscrowBase escrowMasterCopy); // --- // Sanity Check Parameters & Immutables @@ -112,10 +112,6 @@ contract DualGovernance is IDualGovernance { /// @notice The address of the Timelock contract. ITimelock public immutable TIMELOCK; - /// @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 // --- @@ -151,16 +147,17 @@ contract DualGovernance is IDualGovernance { MAX_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.maxTiebreakerActivationTimeout; MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = sanityCheckParams.maxSealableWithdrawalBlockersCount; - ESCROW_MASTER_COPY = new Escrow({ + IEscrowBase escrowMasterCopy = 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, ESCROW_MASTER_COPY); + emit EscrowMasterCopyDeployed(escrowMasterCopy); + + _stateMachine.initialize(dependencies.configProvider, escrowMasterCopy); _resealer.setResealManager(address(dependencies.resealManager)); } @@ -183,7 +180,7 @@ contract DualGovernance is IDualGovernance { ExternalCall[] calldata calls, string calldata metadata ) external returns (uint256 proposalId) { - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); if (!_stateMachine.canSubmitProposal({useEffectiveState: false})) { revert ProposalSubmissionBlocked(); } @@ -198,7 +195,7 @@ contract DualGovernance is IDualGovernance { /// @param proposalId The unique identifier of the proposal to be scheduled. This ID is obtained when the proposal /// is initially submitted to the Dual Governance system. function scheduleProposal(uint256 proposalId) external { - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); Timestamp proposalSubmittedAt = TIMELOCK.getProposalDetails(proposalId).submittedAt; if (!_stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt})) { revert ProposalSchedulingBlocked(proposalId); @@ -214,7 +211,7 @@ contract DualGovernance is IDualGovernance { /// @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); + _stateMachine.activateNextState(); if (msg.sender != _proposalsCanceller) { revert CallerIsNotProposalsCanceller(msg.sender); @@ -282,7 +279,7 @@ contract DualGovernance is IDualGovernance { /// @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); + _stateMachine.activateNextState(); } /// @notice Updates the address of the configuration provider for the Dual Governance system. @@ -472,7 +469,7 @@ contract DualGovernance is IDualGovernance { /// @param sealable The address of the sealable contract to be resumed. function tiebreakerResumeSealable(address sealable) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); _tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.normalOrVetoCooldownExitedAt); _resealer.resealManager.resume(sealable); } @@ -482,7 +479,7 @@ contract DualGovernance is IDualGovernance { /// @param proposalId The unique identifier of the proposal to be scheduled. function tiebreakerScheduleProposal(uint256 proposalId) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); _tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.normalOrVetoCooldownExitedAt); TIMELOCK.schedule(proposalId); } @@ -507,7 +504,7 @@ contract DualGovernance is IDualGovernance { /// the ResealManager contract. /// @param sealable The address of the sealable contract to be resealed. function resealSealable(address sealable) external { - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); if (_stateMachine.getPersistedState() == State.Normal) { revert ResealIsNotAllowedInNormalState(); } diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 3ba3a99f..9f493de8 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -9,20 +9,20 @@ import {ETHValue, ETHValues} from "./types/ETHValue.sol"; import {SharesValue, SharesValues} from "./types/SharesValue.sol"; import {PercentD16, PercentsD16} from "./types/PercentD16.sol"; -import {IEscrow} from "./interfaces/IEscrow.sol"; +import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "./interfaces/IEscrow.sol"; 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 {EscrowState} from "./libraries/EscrowState.sol"; +import {EscrowState, State} from "./libraries/EscrowState.sol"; import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalsBatchesQueue.sol"; import {HolderAssets, StETHAccounting, UnstETHAccounting, AssetsAccounting} from "./libraries/AssetsAccounting.sol"; /// @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 { +contract Escrow is ISignallingEscrow, IRageQuitEscrow { using EscrowState for EscrowState.Context; using AssetsAccounting for AssetsAccounting.Context; using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.Context; @@ -77,7 +77,7 @@ contract Escrow is IEscrow { /// @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; + IEscrowBase public immutable ESCROW_MASTER_COPY; /// @dev The address of the Dual Governance contract. IDualGovernance public immutable DUAL_GOVERNANCE; @@ -106,7 +106,7 @@ contract Escrow is IEscrow { IDualGovernance dualGovernance, uint256 minWithdrawalsBatchSize ) { - _SELF = address(this); + ESCROW_MASTER_COPY = this; DUAL_GOVERNANCE = dualGovernance; ST_ETH = stETH; @@ -120,7 +120,7 @@ contract Escrow is IEscrow { /// @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) { + if (this == ESCROW_MASTER_COPY) { revert NonProxyCallsForbidden(); } _checkCallerIsDualGovernance(); @@ -131,6 +131,13 @@ contract Escrow is IEscrow { ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); } + // --- + // Base Escrow Getters + // --- + function getEscrowState() external view returns (State) { + return _escrowState.state; + } + // --- // Lock & Unlock stETH // --- @@ -274,12 +281,47 @@ contract Escrow is IEscrow { /// 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 { + function startRageQuit( + Duration rageQuitExtensionPeriodDuration, + Duration rageQuitEthWithdrawalsDelay + ) external returns (IRageQuitEscrow rageQuitEscrow) { _checkCallerIsDualGovernance(); _escrowState.startRageQuit(rageQuitExtensionPeriodDuration, rageQuitEthWithdrawalsDelay); _batchesQueue.open(WITHDRAWAL_QUEUE.getLastRequestId()); + rageQuitEscrow = IRageQuitEscrow(this); + } + + // --- + // Signalling Escrow Getters + // --- + + /// @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; + + uint256 finalizedETH = unstETHTotals.finalizedETH.toUint256(); + uint256 unfinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); + + return PercentsD16.fromFraction({ + numerator: ST_ETH.getPooledEthByShares(unfinalizedShares) + finalizedETH, + denominator: ST_ETH.totalSupply() + finalizedETH + }); + } + + /// @notice Returns 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. + function getMinAssetsLockDuration() external view returns (Duration minAssetsLockDuration) { + minAssetsLockDuration = _escrowState.minAssetsLockDuration; } + // TODO: implement + function getLockedUnstETHState(uint256 unstETHId) external view returns (LockedUnstETHState memory) {} + + // TODO: implement + function getSignallingEscrowState() external view returns (SignallingEscrowState memory) {} + // --- // Request Withdrawal Batches // --- @@ -421,12 +463,6 @@ contract Escrow is IEscrow { // Escrow Management // --- - /// @notice Returns 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. - function getMinAssetsLockDuration() external view returns (Duration) { - return _escrowState.minAssetsLockDuration; - } - /// @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. @@ -463,9 +499,12 @@ contract Escrow is IEscrow { } // --- - // Getters + // Rage Quit Escrow Getters // --- + // TODO: implement + function getRageQuitEscrowState() external view returns (RageQuitEscrowState memory) {} + /// @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. @@ -541,21 +580,6 @@ contract Escrow is IEscrow { 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; - - uint256 finalizedETH = unstETHTotals.finalizedETH.toUint256(); - uint256 unfinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); - - return PercentsD16.fromFraction({ - numerator: ST_ETH.getPooledEthByShares(unfinalizedShares) + finalizedETH, - denominator: ST_ETH.totalSupply() + finalizedETH - }); - } - /// @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) { diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index db7596b8..70d2ce02 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.26; import {IDualGovernanceConfigProvider} from "./IDualGovernanceConfigProvider.sol"; import {IGovernance} from "./IGovernance.sol"; -import {IEscrow} from "./IEscrow.sol"; import {IResealManager} from "./IResealManager.sol"; import {ITiebreaker} from "./ITiebreaker.sol"; import {Timestamp} from "../types/Timestamp.sol"; @@ -26,7 +25,6 @@ interface IDualGovernance is IGovernance, ITiebreaker { function MIN_TIEBREAKER_ACTIVATION_TIMEOUT() external view returns (Duration); function MAX_TIEBREAKER_ACTIVATION_TIMEOUT() external view returns (Duration); function MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT() external view returns (uint256); - function ESCROW_MASTER_COPY() external view returns (IEscrow); function canSubmitProposal() external view returns (bool); function canCancelAllPendingProposals() external view returns (bool); diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol index a51a5030..7c72b64e 100644 --- a/contracts/interfaces/IEscrow.sol +++ b/contracts/interfaces/IEscrow.sol @@ -2,15 +2,30 @@ pragma solidity 0.8.26; import {Duration} from "../types/Duration.sol"; +import {Timestamp} from "../types/Timestamp.sol"; import {PercentD16} from "../types/PercentD16.sol"; import {Timestamp} from "../types/Timestamp.sol"; -interface IEscrow { +import {ETHValue} from "../types/ETHValue.sol"; +import {SharesValue} from "../types/SharesValue.sol"; + +import {State as EscrowState} from "../libraries/EscrowState.sol"; +import {UnstETHRecordStatus} from "../libraries/AssetsAccounting.sol"; + +interface IEscrowBase { + struct VetoerState { + uint256 stETHLockedShares; + uint256 unstETHLockedShares; + uint256 unstETHIdsCount; + uint256 lastAssetsLockTimestamp; + } + /// @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. + /// TODO: Remove and use LockedUnstETHState instead struct LockedAssetsTotals { uint256 stETHLockedShares; uint256 stETHClaimedETH; @@ -18,51 +33,80 @@ interface IEscrow { 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; - uint256 unstETHIdsCount; - uint256 lastAssetsLockTimestamp; - } + // TODO: add standalone getter + function ESCROW_MASTER_COPY() external view returns (IEscrowBase); function initialize(Duration minAssetsLockDuration) external; + function getEscrowState() external view returns (EscrowState); + function getVetoerState(address vetoer) external view returns (VetoerState memory); +} + +interface ISignallingEscrow is IEscrowBase { + struct LockedUnstETHState { + UnstETHRecordStatus status; + address lockedBy; + SharesValue shares; + ETHValue claimableAmount; + } + + struct SignallingEscrowState { + PercentD16 rageQuitSupport; + // + ETHValue totalStETHClaimedETH; + SharesValue totalStETHLockedShares; + // + ETHValue totalUnstETHFinalizedETH; + SharesValue totalUnstETHUnfinalizedShares; + } + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares); function unlockStETH() external returns (uint256 unlockedStETHShares); + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares); - function unlockWstETH() external returns (uint256 unlockedStETHShares); + function unlockWstETH() external returns (uint256 wstETHUnlocked); + function lockUnstETH(uint256[] memory unstETHIds) external; function unlockUnstETH(uint256[] memory unstETHIds) external; + function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external; - function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitEthWithdrawalsDelay) external; + function startRageQuit( + Duration rageQuitExtensionPeriodDuration, + Duration rageQuitEthWithdrawalsDelay + ) external returns (IRageQuitEscrow); + + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; + + function getRageQuitSupport() external view returns (PercentD16); + function getMinAssetsLockDuration() external view returns (Duration); + function getLockedUnstETHState(uint256 unstETHId) external view returns (LockedUnstETHState memory); + function getSignallingEscrowState() external view returns (SignallingEscrowState memory); +} + +interface IRageQuitEscrow is IEscrowBase { + struct RageQuitEscrowState { + bool isRageQuitFinalized; + bool isWithdrawalsBatchesClosed; + bool isRageQuitExtensionPeriodStarted; + uint256 unclaimedUnstETHIdsCount; + Duration rageQuitWithdrawalsDelay; + Duration rageQuitExtensionPeriodDuration; + Timestamp rageQuitExtensionPeriodStartedAt; + } function requestNextWithdrawalsBatch(uint256 batchSize) external; function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external; function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external; + function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external; function startRageQuitExtensionPeriod() external; - function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external; function withdrawETH() external; function withdrawETH(uint256[] calldata unstETHIds) external; - function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory totals); - function getVetoerState(address vetoer) external view returns (VetoerState memory state); - function getUnclaimedUnstETHIdsCount() external view returns (uint256); - function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds); - function isWithdrawalsBatchesClosed() external view returns (bool); - function isRageQuitExtensionPeriodStarted() external view returns (bool); - function getRageQuitExtensionPeriodStartedAt() external view returns (Timestamp); - function isRageQuitFinalized() external view returns (bool); - function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport); - function getMinAssetsLockDuration() external view returns (Duration minAssetsLockDuration); - function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; + function getRageQuitEscrowState() external view returns (RageQuitEscrowState memory); + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds); } diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index c67b53ee..11bb6457 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -7,7 +7,7 @@ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Duration} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; -import {IEscrow} from "../interfaces/IEscrow.sol"; +import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "../interfaces/IEscrow.sol"; import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; import {IDualGovernanceConfigProvider} from "../interfaces/IDualGovernanceConfigProvider.sol"; @@ -66,7 +66,7 @@ library DualGovernanceStateMachine { /// @dev slot 0: [48..87] Timestamp vetoSignallingActivatedAt; /// @dev slot 0: [88..247] - IEscrow signallingEscrow; + ISignallingEscrow signallingEscrow; /// @dev slot 0: [248..255] uint8 rageQuitRound; /// @dev slot 1: [0..39] @@ -74,7 +74,7 @@ library DualGovernanceStateMachine { /// @dev slot 1: [40..79] Timestamp normalOrVetoCooldownExitedAt; /// @dev slot 1: [80..239] - IEscrow rageQuitEscrow; + IRageQuitEscrow rageQuitEscrow; /// @dev slot 2: [0..159] IDualGovernanceConfigProvider configProvider; } @@ -90,7 +90,7 @@ library DualGovernanceStateMachine { // Events // --- - event NewSignallingEscrowDeployed(IEscrow indexed escrow); + event NewSignallingEscrowDeployed(ISignallingEscrow indexed escrow); event DualGovernanceStateChanged(State indexed from, State indexed to, Context state); event ConfigProviderSet(IDualGovernanceConfigProvider newConfigProvider); @@ -114,7 +114,7 @@ library DualGovernanceStateMachine { function initialize( Context storage self, IDualGovernanceConfigProvider configProvider, - IEscrow escrowMasterCopy + IEscrowBase escrowMasterCopy ) internal { if (self.state != State.Unset) { revert AlreadyInitialized(); @@ -137,9 +137,7 @@ library DualGovernanceStateMachine { /// `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 { + function activateNextState(Context storage self) internal { DualGovernanceConfig.Context memory config = getDualGovernanceConfig(self); (State currentState, State newState) = self.getStateTransition(config); @@ -165,7 +163,7 @@ library DualGovernanceStateMachine { self.vetoSignallingActivatedAt = newStateEnteredAt; } } else if (newState == State.RageQuit) { - IEscrow signallingEscrow = self.signallingEscrow; + ISignallingEscrow signallingEscrow = self.signallingEscrow; uint256 currentRageQuitRound = self.rageQuitRound; @@ -174,11 +172,10 @@ library DualGovernanceStateMachine { uint256 newRageQuitRound = Math.min(currentRageQuitRound + 1, MAX_RAGE_QUIT_ROUND); self.rageQuitRound = uint8(newRageQuitRound); - signallingEscrow.startRageQuit( + self.rageQuitEscrow = signallingEscrow.startRageQuit( config.rageQuitExtensionPeriodDuration, config.calcRageQuitWithdrawalsDelay(newRageQuitRound) ); - self.rageQuitEscrow = signallingEscrow; - _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + _deployNewSignallingEscrow(self, signallingEscrow.ESCROW_MASTER_COPY(), config.minAssetsLockDuration); } emit DualGovernanceStateChanged(currentState, newState, self); @@ -190,7 +187,7 @@ library DualGovernanceStateMachine { function setConfigProvider(Context storage self, IDualGovernanceConfigProvider newConfigProvider) internal { _setConfigProvider(self, newConfigProvider); - IEscrow signallingEscrow = self.signallingEscrow; + ISignallingEscrow signallingEscrow = self.signallingEscrow; Duration newMinAssetsLockDuration = newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration; /// @dev minAssetsLockDuration is stored as a storage variable in the Signalling Escrow instance. @@ -320,10 +317,10 @@ library DualGovernanceStateMachine { function _deployNewSignallingEscrow( Context storage self, - IEscrow escrowMasterCopy, + IEscrowBase escrowMasterCopy, Duration minAssetsLockDuration ) private { - IEscrow newSignallingEscrow = IEscrow(Clones.clone(address(escrowMasterCopy))); + ISignallingEscrow newSignallingEscrow = ISignallingEscrow(Clones.clone(address(escrowMasterCopy))); newSignallingEscrow.initialize(minAssetsLockDuration); self.signallingEscrow = newSignallingEscrow; emit NewSignallingEscrowDeployed(newSignallingEscrow); diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index be9205a8..edc465d1 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -6,6 +6,7 @@ import {Durations} from "contracts/types/Duration.sol"; import {Executor} from "contracts/Executor.sol"; import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; @@ -177,7 +178,7 @@ library DeployVerification { "Incorrect parameter MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT" ); - Escrow escrowTemplate = Escrow(payable(address(dg.ESCROW_MASTER_COPY()))); + Escrow escrowTemplate = Escrow(payable(address(IEscrowBase(dg.getVetoSignallingEscrow()).ESCROW_MASTER_COPY()))); require(escrowTemplate.DUAL_GOVERNANCE() == dg, "Escrow has incorrect DualGovernance address"); require(escrowTemplate.ST_ETH() == lidoAddresses.stETH, "Escrow has incorrect StETH address"); require(escrowTemplate.WST_ETH() == lidoAddresses.wstETH, "Escrow has incorrect WstETH address"); diff --git a/test/mocks/EscrowMock.sol b/test/mocks/EscrowMock.sol index d87c5965..2c9c38e1 100644 --- a/test/mocks/EscrowMock.sol +++ b/test/mocks/EscrowMock.sol @@ -5,113 +5,119 @@ import {Duration} from "contracts/types/Duration.sol"; import {PercentD16} from "contracts/types/PercentD16.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; -import {IEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "contracts/interfaces/IEscrow.sol"; +import {State as EscrowState} from "contracts/libraries/EscrowState.sol"; + +contract EscrowMock is ISignallingEscrow, IRageQuitEscrow { + IEscrowBase public immutable ESCROW_MASTER_COPY = this; -/* solhint-disable custom-errors */ -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 lockStETH(uint256 /* amount */ ) external returns (uint256 /* lockedStETHShares */ ) { + function getEscrowState() external view returns (EscrowState) { revert("Not implemented"); } - function unlockStETH() external returns (uint256 /* unlockedStETHShares */ ) { + function getVetoerState(address vetoer) external view returns (VetoerState memory) { revert("Not implemented"); } - function lockWstETH(uint256 /* amount */ ) external returns (uint256 /* lockedStETHShares */ ) { + // --- + // Signalling Escrow Methods + // --- + + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { revert("Not implemented"); } - function unlockWstETH() external returns (uint256 /* unlockedStETHShares */ ) { + function unlockStETH() external returns (uint256 unlockedStETHShares) { revert("Not implemented"); } - function lockUnstETH(uint256[] memory /* unstETHIds */ ) external { + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { revert("Not implemented"); } - function unlockUnstETH(uint256[] memory /* unstETHIds */ ) external { + function unlockWstETH() external returns (uint256 wstETHUnlocked) { revert("Not implemented"); } - function markUnstETHFinalized(uint256[] memory, /* unstETHIds */ uint256[] calldata /* hints */ ) external { + function lockUnstETH(uint256[] memory unstETHIds) external { revert("Not implemented"); } - function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitEthWithdrawalsDelay) external { - emit __RageQuitStarted(rageQuitExtensionPeriodDuration, rageQuitEthWithdrawalsDelay); + function unlockUnstETH(uint256[] memory unstETHIds) external { + revert("Not implemented"); } - function requestNextWithdrawalsBatch(uint256 /* batchSize */ ) external { + function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external { revert("Not implemented"); } - function claimNextWithdrawalsBatch(uint256, /* fromUnstETHId */ uint256[] calldata /* hints */ ) external { - revert("Not implemented"); + function startRageQuit( + Duration rageQuitExtraTimelock, + Duration rageQuitWithdrawalsTimelock + ) external returns (IRageQuitEscrow) { + emit __RageQuitStarted(rageQuitExtraTimelock, rageQuitWithdrawalsTimelock); + return this; } - function claimNextWithdrawalsBatch(uint256 /* maxUnstETHIdsCount */ ) external { - revert("Not implemented"); + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { + __minAssetsLockDuration = newMinAssetsLockDuration; } - function startRageQuitExtensionPeriod() external { - revert("Not implemented"); + function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport) { + return __rageQuitSupport; } - function claimUnstETH(uint256[] calldata, /* unstETHIds */ uint256[] calldata /* hints */ ) external { + function getMinAssetsLockDuration() external view returns (Duration) { revert("Not implemented"); } - function withdrawETH() external { + function getLockedUnstETHState(uint256 unstETHId) external view returns (LockedUnstETHState memory) { revert("Not implemented"); } - function withdrawETH(uint256[] calldata /* unstETHIds */ ) external { + function getSignallingEscrowState() external view returns (SignallingEscrowState memory) { revert("Not implemented"); } - function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory /* totals */ ) { + // --- + // Rage Quit Escrow + // --- + + function requestNextWithdrawalsBatch(uint256 batchSize) external { revert("Not implemented"); } - function getVetoerState(address /* vetoer */ ) external view returns (VetoerState memory /* state */ ) { + function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external { revert("Not implemented"); } - function getUnclaimedUnstETHIdsCount() external view returns (uint256) { + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { revert("Not implemented"); } - function getNextWithdrawalBatch(uint256 /* limit */ ) external view returns (uint256[] memory /* unstETHIds */ ) { + function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external { revert("Not implemented"); } - function isWithdrawalsBatchesClosed() external view returns (bool) { + function startRageQuitExtensionPeriod() external { revert("Not implemented"); } - function isRageQuitExtensionPeriodStarted() external view returns (bool) { + function withdrawETH() external { revert("Not implemented"); } - function getRageQuitExtensionPeriodStartedAt() external view returns (Timestamp) { + function withdrawETH(uint256[] calldata unstETHIds) external { revert("Not implemented"); } @@ -119,15 +125,23 @@ contract EscrowMock is IEscrow { return __isRageQuitFinalized; } - function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport) { - return __rageQuitSupport; + function getRageQuitEscrowState() external view returns (RageQuitEscrowState memory) { + revert("Not implemented"); } - function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { - __minAssetsLockDuration = newMinAssetsLockDuration; + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds) { + revert("Not implemented"); } - function getMinAssetsLockDuration() external view returns (Duration minAssetsLockDuration) { - return __minAssetsLockDuration; + // --- + // Mock methods + // --- + + function __setRageQuitSupport(PercentD16 newRageQuitSupport) external { + __rageQuitSupport = newRageQuitSupport; + } + + function __setIsRageQuitFinalized(bool newIsRageQuitFinalized) external { + __isRageQuitFinalized = newIsRageQuitFinalized; } } diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 093197de..737d4042 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -6,14 +6,15 @@ import {console} from "forge-std/Test.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {PercentsD16} from "contracts/types/PercentD16.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; - -import {IEscrow} from "contracts/interfaces/IEscrow.sol"; -import {Escrow, WithdrawalsBatchesQueue} from "contracts/Escrow.sol"; +import {WithdrawalsBatchesQueue} from "contracts/libraries/WithdrawalsBatchesQueue.sol"; import {AssetsAccounting, UnstETHRecordStatus} from "contracts/libraries/AssetsAccounting.sol"; +import {Escrow} from "contracts/Escrow.sol"; + import {ScenarioTestBlueprint, LidoUtils, console} from "../utils/scenario-test-blueprint.sol"; contract EscrowHappyPath is ScenarioTestBlueprint { @@ -220,10 +221,10 @@ contract EscrowHappyPath is ScenarioTestBlueprint { _lockUnstETH(_VETOER_1, unstETHIds); - IEscrow.VetoerState memory vetoerState = escrow.getVetoerState(_VETOER_1); + Escrow.VetoerState memory vetoerState = escrow.getVetoerState(_VETOER_1); assertEq(vetoerState.unstETHIdsCount, 2); - IEscrow.LockedAssetsTotals memory totals = escrow.getLockedAssetsTotals(); + IEscrowBase.LockedAssetsTotals memory totals = escrow.getLockedAssetsTotals(); assertEq(totals.unstETHFinalizedETH, 0); assertEq(totals.unstETHUnfinalizedShares, totalSharesLocked); diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 8de47811..07ee894c 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -26,7 +26,7 @@ import {IWstETH} from "contracts/interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; import {ITimelock} from "contracts/interfaces/ITimelock.sol"; import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; -import {IEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; import {UnitTest} from "test/utils/unit-test.sol"; import {StETHMock} from "test/mocks/StETHMock.sol"; @@ -152,7 +152,7 @@ contract DualGovernanceUnitTests is UnitTest { address predictedEscrowCopyAddress = computeAddress(predictedDualGovernanceAddress, 1); vm.expectEmit(); - emit DualGovernance.EscrowMasterCopyDeployed(IEscrow(predictedEscrowCopyAddress)); + emit DualGovernance.EscrowMasterCopyDeployed(IEscrowBase(predictedEscrowCopyAddress)); vm.expectEmit(); emit Resealer.ResealManagerSet(address(_RESEAL_MANAGER_STUB)); @@ -181,7 +181,10 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(dualGovernanceLocal.MIN_TIEBREAKER_ACTIVATION_TIMEOUT(), minTiebreakerActivationTimeout); assertEq(dualGovernanceLocal.MAX_TIEBREAKER_ACTIVATION_TIMEOUT(), maxTiebreakerActivationTimeout); assertEq(dualGovernanceLocal.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT(), maxSealableWithdrawalBlockersCount); - assertEq(address(dualGovernanceLocal.ESCROW_MASTER_COPY()), predictedEscrowCopyAddress); + assertEq( + address(IEscrowBase(dualGovernanceLocal.getVetoSignallingEscrow()).ESCROW_MASTER_COPY()), + predictedEscrowCopyAddress + ); } // --- diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 3377885f..2fed86f2 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {IEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "contracts/interfaces/IEscrow.sol"; import {Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; @@ -46,9 +46,11 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { DualGovernanceStateMachine.Context private _stateMachine; function setUp() external { - _stateMachine.initialize(_CONFIG_PROVIDER, IEscrow(_ESCROW_MASTER_COPY_MOCK)); + _stateMachine.initialize(_CONFIG_PROVIDER, IEscrowBase(_ESCROW_MASTER_COPY_MOCK)); _mockRageQuitFinalized(false); _mockRageQuitSupport(PercentsD16.from(0)); + _mockEscrowMasterCopy(); + _mockStartRageQuit(); } function test_initialize_RevertOn_ReInitialization() external { @@ -448,20 +450,40 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { // Test helper methods // --- + function _mockEscrowMasterCopy() internal { + vm.mockCall( + _ESCROW_MASTER_COPY_MOCK, + abi.encodeWithSelector(IEscrowBase.ESCROW_MASTER_COPY.selector), + abi.encode(_ESCROW_MASTER_COPY_MOCK) + ); + } + + function _mockStartRageQuit() internal { + vm.mockCall( + _ESCROW_MASTER_COPY_MOCK, + abi.encodeWithSelector(ISignallingEscrow.startRageQuit.selector), + abi.encode(_stateMachine.signallingEscrow) + ); + } + function _mockRageQuitSupport(PercentD16 rageQuitSupport) internal { vm.mockCall( - _ESCROW_MASTER_COPY_MOCK, abi.encodeCall(IEscrow.getRageQuitSupport, ()), abi.encode(rageQuitSupport) + _ESCROW_MASTER_COPY_MOCK, + abi.encodeCall(ISignallingEscrow.getRageQuitSupport, ()), + abi.encode(rageQuitSupport) ); } function _mockRageQuitFinalized(bool isRageQuitFinalized) internal { vm.mockCall( - _ESCROW_MASTER_COPY_MOCK, abi.encodeCall(IEscrow.isRageQuitFinalized, ()), abi.encode(isRageQuitFinalized) + _ESCROW_MASTER_COPY_MOCK, + abi.encodeCall(IRageQuitEscrow.isRageQuitFinalized, ()), + abi.encode(isRageQuitFinalized) ); } function _activateNextState() internal { - _stateMachine.activateNextState(IEscrow(_ESCROW_MASTER_COPY_MOCK)); + _stateMachine.activateNextState(); } function _assertState(State persisted, State effective) internal { @@ -509,6 +531,6 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { } function external__initialize() external { - _stateMachine.initialize(_CONFIG_PROVIDER, IEscrow(_ESCROW_MASTER_COPY_MOCK)); + _stateMachine.initialize(_CONFIG_PROVIDER, IEscrowBase(_ESCROW_MASTER_COPY_MOCK)); } } diff --git a/test/unit/libraries/DualGovernanceStateTransitions.t.sol b/test/unit/libraries/DualGovernanceStateTransitions.t.sol index a374c919..f04dc41e 100644 --- a/test/unit/libraries/DualGovernanceStateTransitions.t.sol +++ b/test/unit/libraries/DualGovernanceStateTransitions.t.sol @@ -5,7 +5,7 @@ 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 {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "contracts/interfaces/IEscrow.sol"; import { State, @@ -50,7 +50,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { rageQuitEthWithdrawalsDelayGrowth: Durations.from(15 days) }) ); - DualGovernanceStateMachine.initialize(_stateMachine, _configProvider, IEscrow(_escrowMasterCopyMock)); + DualGovernanceStateMachine.initialize(_stateMachine, _configProvider, IEscrowBase(_escrowMasterCopyMock)); _setMockRageQuitSupportInBP(0); } @@ -353,13 +353,13 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { function _setupRageQuitState() internal { _stateMachine.state = State.RageQuit; _stateMachine.enteredAt = Timestamps.now(); - _stateMachine.rageQuitEscrow = IEscrow(_escrowMasterCopyMock); + _stateMachine.rageQuitEscrow = IRageQuitEscrow(_escrowMasterCopyMock); } function _setMockRageQuitSupportInBP(uint256 bpValue) internal { vm.mockCall( address(_stateMachine.signallingEscrow), - abi.encodeWithSelector(IEscrow.getRageQuitSupport.selector), + abi.encodeWithSelector(ISignallingEscrow.getRageQuitSupport.selector), abi.encode(PercentsD16.fromBasisPoints(bpValue)) ); } @@ -367,7 +367,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { function _setMockIsRageQuitFinalized(bool isRageQuitFinalized) internal { vm.mockCall( address(_stateMachine.rageQuitEscrow), - abi.encodeWithSelector(IEscrow.isRageQuitFinalized.selector), + abi.encodeWithSelector(IRageQuitEscrow.isRageQuitFinalized.selector), abi.encode(isRageQuitFinalized) ); } diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 864cf88c..bddce0cc 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -12,7 +12,7 @@ import {PercentD16} from "contracts/types/PercentD16.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {IEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; import {Escrow} from "contracts/Escrow.sol"; // --- @@ -199,7 +199,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { function _unlockWstETH(address vetoer) internal { Escrow escrow = _getVetoSignallingEscrow(); uint256 wstETHBalanceBefore = _lido.wstETH.balanceOf(vetoer); - IEscrow.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + IEscrowBase.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); vm.startPrank(vetoer); uint256 wstETHUnlocked = escrow.unlockWstETH(); @@ -213,8 +213,8 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { function _lockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - IEscrow.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); - IEscrow.LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); + IEscrowBase.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); uint256 unstETHTotalSharesLocked = 0; IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = @@ -233,10 +233,10 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), address(escrow)); } - IEscrow.VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); + IEscrowBase.VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount + unstETHIds.length); - IEscrow.LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); assertEq( lockedAssetsTotalsAfter.unstETHUnfinalizedShares, lockedAssetsTotalsBefore.unstETHUnfinalizedShares + unstETHTotalSharesLocked @@ -245,8 +245,8 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { function _unlockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - IEscrow.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); - IEscrow.LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); + IEscrowBase.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); uint256 unstETHTotalSharesUnlocked = 0; IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = @@ -263,11 +263,11 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), vetoer); } - IEscrow.VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); + IEscrowBase.VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount - unstETHIds.length); // TODO: implement correct assert. It must consider was unstETH finalized or not - IEscrow.LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); assertEq( lockedAssetsTotalsAfter.unstETHUnfinalizedShares, lockedAssetsTotalsBefore.unstETHUnfinalizedShares - unstETHTotalSharesUnlocked From 61b03c03091ca63a7580bde7df095cd6cf13348f Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:27:03 +0400 Subject: [PATCH 074/107] Add mock contracts for StEth, WstEth, WithdrawalQueue for testing the setup on Holesky --- scripts/deploy/DeployVerification.sol | 3 +- scripts/deploy/Readme.md | 12 + .../lido-mocks/DeployHoleskyLidoMocks.s.sol | 58 +++ scripts/lido-mocks/Readme.md | 13 + scripts/lido-mocks/StETHBase.sol | 193 +++++++ scripts/lido-mocks/StETHMock.sol | 44 ++ .../lido-mocks/UnsafeWithdrawalQueueMock.sol | 485 ++++++++++++++++++ scripts/lido-mocks/WstETHMock.sol | 35 ++ 8 files changed, 842 insertions(+), 1 deletion(-) create mode 100644 scripts/lido-mocks/DeployHoleskyLidoMocks.s.sol create mode 100644 scripts/lido-mocks/Readme.md create mode 100644 scripts/lido-mocks/StETHBase.sol create mode 100644 scripts/lido-mocks/StETHMock.sol create mode 100644 scripts/lido-mocks/UnsafeWithdrawalQueueMock.sol create mode 100644 scripts/lido-mocks/WstETHMock.sol diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index be9205a8..e464c578 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -113,7 +113,8 @@ library DeployVerification { "Incorrect governance address in EmergencyProtectedTimelock" ); require( - timelockInstance.isEmergencyProtectionEnabled() == true, + timelockInstance.isEmergencyProtectionEnabled() + == (details.emergencyProtectionEndsAfter >= Timestamps.now()), "EmergencyProtection is Disabled in EmergencyProtectedTimelock" ); require( diff --git a/scripts/deploy/Readme.md b/scripts/deploy/Readme.md index 62dab88e..5ae27630 100644 --- a/scripts/deploy/Readme.md +++ b/scripts/deploy/Readme.md @@ -61,6 +61,18 @@ anvil --fork-url https://.infura.io/v3/ --bloc forge script scripts/deploy/DeployConfigurable.s.sol:DeployConfigurable --fork-url https://holesky.infura.io/v3/ --broadcast --account Deployer1 --sender --verify ``` +5. [Testnet and mainnet deployment only] Run Etherscan verification for Escrow contract + + The Escrow contract is deployed internally by DualGovernance contract, so it can't be verified automatically during the initial deployment and requires manual verification afterward. To run Etherscan verification: + + a. Query the deployed DualGovernance contract instance for ESCROW_MASTER_COPY address. + + b. Run Etherscan verification (for example on a Holesky testnet) + + ``` + forge verify-contract --chain holesky --verifier-url https://api-holesky.etherscan.io/api --watch --constructor-args $(cast abi-encode "Escrow(address,address,address,address,uint256)" ) contracts/Escrow.sol:Escrow + ``` + ### Running the verification script 1. Set up the required env variables in the .env file diff --git a/scripts/lido-mocks/DeployHoleskyLidoMocks.s.sol b/scripts/lido-mocks/DeployHoleskyLidoMocks.s.sol new file mode 100644 index 00000000..2532dc1c --- /dev/null +++ b/scripts/lido-mocks/DeployHoleskyLidoMocks.s.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/* solhint-disable no-console */ + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {StETHMock} from "./StETHMock.sol"; +import {WstETHMock} from "./WstETHMock.sol"; +import {UnsafeWithdrawalQueueMock} from "./UnsafeWithdrawalQueueMock.sol"; + +struct DeployedContracts { + address stETH; + address wstETH; + address withdrawalQueue; +} + +contract DeployHoleskyLidoMocks is Script { + error DoNotRunThisOnMainnet(uint256 currentChainId); + + address private deployer; + + function run() external { + if (1 == block.chainid) { + revert DoNotRunThisOnMainnet(block.chainid); + } + + deployer = msg.sender; + vm.label(deployer, "DEPLOYER"); + + vm.startBroadcast(); + + DeployedContracts memory res = deployLidoMockContracts(); + + vm.stopBroadcast(); + + printAddresses(res); + } + + function deployLidoMockContracts() internal returns (DeployedContracts memory res) { + StETHMock stETH = new StETHMock(); + WstETHMock wstETH = new WstETHMock(stETH); + UnsafeWithdrawalQueueMock withdrawalQueue = new UnsafeWithdrawalQueueMock(address(stETH), payable(deployer)); + + stETH.mint(deployer, 100 gwei); + + res.stETH = address(stETH); + res.wstETH = address(wstETH); + res.withdrawalQueue = address(withdrawalQueue); + } + + function printAddresses(DeployedContracts memory res) internal pure { + console.log("Lido mocks deployed successfully"); + console.log("StETH address", res.stETH); + console.log("WstETH address", res.wstETH); + console.log("WithdrawalQueue address", res.withdrawalQueue); + } +} diff --git a/scripts/lido-mocks/Readme.md b/scripts/lido-mocks/Readme.md new file mode 100644 index 00000000..0c6a4ad2 --- /dev/null +++ b/scripts/lido-mocks/Readme.md @@ -0,0 +1,13 @@ +# Lido contracts mocks for Dual Governance testing on Holesky + +### Deploying + + With the local fork (Anvil): + ``` + forge script scripts/lido-mocks/DeployHoleskyLidoMocks.s.sol:DeployHoleskyLidoMocks --fork-url http://localhost:8545 --broadcast --account Deployer1 --sender + ``` + + On a testnet (with Etherscan verification): + ``` + forge script scripts/lido-mocks/DeployHoleskyLidoMocks.s.sol:DeployHoleskyLidoMocks --fork-url https://holesky.infura.io/v3/ --broadcast --account Deployer1 --sender --verify + ``` \ No newline at end of file diff --git a/scripts/lido-mocks/StETHBase.sol b/scripts/lido-mocks/StETHBase.sol new file mode 100644 index 00000000..b888851a --- /dev/null +++ b/scripts/lido-mocks/StETHBase.sol @@ -0,0 +1,193 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.26; + +import {IStETH} from "contracts/interfaces/IStETH.sol"; + +/* solhint-disable custom-errors */ + +abstract contract StETHBase is IStETH { + address internal constant INITIAL_TOKEN_HOLDER = address(0xdead); + uint256 internal constant INFINITE_ALLOWANCE = ~uint256(0); + + mapping(address => uint256) private shares; + mapping(address => mapping(address => uint256)) private allowances; + + event TransferShares(address indexed from, address indexed to, uint256 sharesValue); + + event SharesBurnt( + address indexed account, uint256 preRebaseTokenAmount, uint256 postRebaseTokenAmount, uint256 sharesAmount + ); + + uint256 internal _totalPooledEther; + uint256 internal _totalShares; + + function name() external pure virtual returns (string memory); + + function symbol() external pure virtual returns (string memory); + + function decimals() external pure returns (uint8) { + return 18; + } + + function totalSupply() external view returns (uint256) { + return _totalPooledEther; + } + + function getTotalPooledEther() external view returns (uint256) { + return _totalPooledEther; + } + + function balanceOf(address _account) external view returns (uint256) { + return getPooledEthByShares(_sharesOf(_account)); + } + + function transfer(address _recipient, uint256 _amount) external returns (bool) { + _transfer(msg.sender, _recipient, _amount); + return true; + } + + function allowance(address _owner, address _spender) external view returns (uint256) { + return allowances[_owner][_spender]; + } + + function approve(address _spender, uint256 _amount) external returns (bool) { + _approve(msg.sender, _spender, _amount); + return true; + } + + function transferFrom(address _sender, address _recipient, uint256 _amount) external returns (bool) { + _spendAllowance(_sender, msg.sender, _amount); + _transfer(_sender, _recipient, _amount); + return true; + } + + function increaseAllowance(address _spender, uint256 _addedValue) external returns (bool) { + _approve(msg.sender, _spender, allowances[msg.sender][_spender] + _addedValue); + return true; + } + + function decreaseAllowance(address _spender, uint256 _subtractedValue) external returns (bool) { + uint256 currentAllowance = allowances[msg.sender][_spender]; + require(currentAllowance >= _subtractedValue, "ALLOWANCE_BELOW_ZERO"); + _approve(msg.sender, _spender, currentAllowance - _subtractedValue); + return true; + } + + function getTotalShares() external view returns (uint256) { + return _totalShares; + } + + function sharesOf(address _account) external view returns (uint256) { + return _sharesOf(_account); + } + + function getSharesByPooledEth(uint256 _ethAmount) public view returns (uint256) { + return (_ethAmount * _totalShares) / _totalPooledEther; + } + + function getPooledEthByShares(uint256 _sharesAmount) public view returns (uint256) { + return (_sharesAmount * _totalPooledEther) / _totalShares; + } + + function transferShares(address _recipient, uint256 _sharesAmount) external returns (uint256) { + _transferShares(msg.sender, _recipient, _sharesAmount); + uint256 tokensAmount = getPooledEthByShares(_sharesAmount); + _emitTransferEvents(msg.sender, _recipient, tokensAmount, _sharesAmount); + return tokensAmount; + } + + function transferSharesFrom( + address _sender, + address _recipient, + uint256 _sharesAmount + ) external returns (uint256) { + uint256 tokensAmount = getPooledEthByShares(_sharesAmount); + _spendAllowance(_sender, msg.sender, tokensAmount); + _transferShares(_sender, _recipient, _sharesAmount); + _emitTransferEvents(_sender, _recipient, tokensAmount, _sharesAmount); + return tokensAmount; + } + + function _transfer(address _sender, address _recipient, uint256 _amount) internal { + uint256 _sharesToTransfer = getSharesByPooledEth(_amount); + _transferShares(_sender, _recipient, _sharesToTransfer); + _emitTransferEvents(_sender, _recipient, _amount, _sharesToTransfer); + } + + function _approve(address _owner, address _spender, uint256 _amount) internal { + require(_owner != address(0), "APPROVE_FROM_ZERO_ADDR"); + require(_spender != address(0), "APPROVE_TO_ZERO_ADDR"); + + allowances[_owner][_spender] = _amount; + emit Approval(_owner, _spender, _amount); + } + + function _spendAllowance(address _owner, address _spender, uint256 _amount) internal { + uint256 currentAllowance = allowances[_owner][_spender]; + if (currentAllowance != INFINITE_ALLOWANCE) { + require(currentAllowance >= _amount, "ALLOWANCE_EXCEEDED"); + _approve(_owner, _spender, currentAllowance - _amount); + } + } + + function _sharesOf(address _account) internal view returns (uint256) { + return shares[_account]; + } + + function _transferShares(address _sender, address _recipient, uint256 _sharesAmount) internal { + require(_sender != address(0), "TRANSFER_FROM_ZERO_ADDR"); + require(_recipient != address(0), "TRANSFER_TO_ZERO_ADDR"); + require(_recipient != address(this), "TRANSFER_TO_STETH_CONTRACT"); + + // Pausable functionality not needed for this implementation // _whenNotStopped(); + + uint256 currentSenderShares = shares[_sender]; + require(_sharesAmount <= currentSenderShares, "BALANCE_EXCEEDED"); + + shares[_sender] = currentSenderShares - _sharesAmount; + shares[_recipient] = shares[_recipient] + _sharesAmount; + } + + function _mintShares(address _recipient, uint256 _sharesAmount) internal returns (uint256 newTotalShares) { + require(_recipient != address(0), "MINT_TO_ZERO_ADDR"); + + newTotalShares = _totalShares + _sharesAmount; + _totalShares = newTotalShares; + + shares[_recipient] += _sharesAmount; + } + + function _burnShares(address _account, uint256 _sharesAmount) internal returns (uint256 newTotalShares) { + require(_account != address(0), "BURN_FROM_ZERO_ADDR"); + + uint256 accountShares = shares[_account]; + require(_sharesAmount <= accountShares, "BALANCE_EXCEEDED"); + + uint256 preRebaseTokenAmount = getPooledEthByShares(_sharesAmount); + + newTotalShares = _totalShares - _sharesAmount; + _totalShares = newTotalShares; + + shares[_account] = accountShares - _sharesAmount; + + uint256 postRebaseTokenAmount = getPooledEthByShares(_sharesAmount); + + emit SharesBurnt(_account, preRebaseTokenAmount, postRebaseTokenAmount, _sharesAmount); + } + + function _emitTransferEvents(address _from, address _to, uint256 _tokenAmount, uint256 _sharesAmount) internal { + emit Transfer(_from, _to, _tokenAmount); + emit TransferShares(_from, _to, _sharesAmount); + } + + function _emitTransferAfterMintingShares(address _to, uint256 _sharesAmount) internal { + _emitTransferEvents(address(0), _to, getPooledEthByShares(_sharesAmount), _sharesAmount); + } + + function _mintInitialShares(uint256 _sharesAmount) internal { + _mintShares(INITIAL_TOKEN_HOLDER, _sharesAmount); + _emitTransferAfterMintingShares(INITIAL_TOKEN_HOLDER, _sharesAmount); + } +} diff --git a/scripts/lido-mocks/StETHMock.sol b/scripts/lido-mocks/StETHMock.sol new file mode 100644 index 00000000..fca91ae9 --- /dev/null +++ b/scripts/lido-mocks/StETHMock.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {PercentD16, HUNDRED_PERCENT_D16} from "contracts/types/PercentD16.sol"; +import {StETHBase} from "./StETHBase.sol"; + +contract StETHMock is StETHBase { + constructor() { + _totalPooledEther = 100 wei; + _mintInitialShares(100 wei); + } + + function name() external pure override returns (string memory) { + return "StETHMock"; + } + + function symbol() external pure override returns (string memory) { + return "MStETH"; + } + + function rebaseTotalPooledEther(PercentD16 rebaseFactor) public { + _totalPooledEther = rebaseFactor.toUint256() * _totalPooledEther / HUNDRED_PERCENT_D16; + } + + function setTotalPooledEther(uint256 ethAmount) public { + _totalPooledEther = ethAmount; + } + + function mint(address account, uint256 ethAmount) external { + uint256 sharesAmount = getSharesByPooledEth(ethAmount); + + _mintShares(account, sharesAmount); + _totalPooledEther += ethAmount; + + _emitTransferAfterMintingShares(account, sharesAmount); + } + + function burn(address account, uint256 ethAmount) external { + uint256 sharesToBurn = this.getSharesByPooledEth(ethAmount); + _burnShares(account, sharesToBurn); + _totalPooledEther -= ethAmount; + _emitTransferEvents(account, address(0), ethAmount, sharesToBurn); + } +} diff --git a/scripts/lido-mocks/UnsafeWithdrawalQueueMock.sol b/scripts/lido-mocks/UnsafeWithdrawalQueueMock.sol new file mode 100644 index 00000000..7fe7a7c4 --- /dev/null +++ b/scripts/lido-mocks/UnsafeWithdrawalQueueMock.sol @@ -0,0 +1,485 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {IStETH} from "contracts/interfaces/IStETH.sol"; +import {StETHMock} from "./StETHMock.sol"; + +contract UnsafeWithdrawalQueueMock is IWithdrawalQueue, IERC721Metadata { + using EnumerableSet for EnumerableSet.UintSet; + + struct WithdrawalRequest { + /// @notice sum of the all ST_ETH submitted for withdrawals including this request + uint128 cumulativeStETH; + /// @notice sum of the all shares locked for withdrawal including this request + uint128 cumulativeShares; + /// @notice address that can claim or transfer the request + address owner; + /// @notice block.timestamp when the request was created + uint40 timestamp; + /// @notice flag if the request was claimed + bool claimed; + /// @notice timestamp of last oracle report for this request + uint40 reportTimestamp; + } + + struct Checkpoint { + uint256 fromRequestId; + uint256 maxShareRate; + } + + /// + + error ApprovalToOwner(); + error ApproveToCaller(); + error ArraysLengthMismatch(uint256 _firstArrayLength, uint256 _secondArrayLength); + error InvalidHint(uint256 _hint); + error InvalidRequestId(uint256 _requestId); + error InvalidRequestIdRange(uint256 startId, uint256 endId); + error NotOwner(address _sender, address _owner); + error NotOwnerOrApproved(address sender); + error NotOwnerOrApprovedForAll(address sender); + error RequestAlreadyClaimed(uint256 _requestId); + error RequestIdsNotSorted(); + error RequestNotFoundOrNotFinalized(uint256 _requestId); + error TransferToThemselves(); + error TransferToZeroAddress(); + error PausedExpected(); + error ResumedExpected(); + + /// + + uint256 public constant MIN_STETH_WITHDRAWAL_AMOUNT = 100; + uint256 public constant MAX_STETH_WITHDRAWAL_AMOUNT = 1000 * 1e18; + uint256 public constant PAUSE_INFINITELY = type(uint256).max; + uint256 internal constant NOT_FOUND = 0; + uint256 internal constant E27_PRECISION_BASE = 1e27; + + /// + + // solhint-disable-next-line var-name-mixedcase + IStETH public ST_ETH; + address payable private _refundAddress; + uint256 private _lastRequestId = 0; + uint256 private _lastFinalizedRequestId = 0; + uint256 private _lastCheckpointIndex = 0; + uint256 private _lockedEtherAmount = 0; + uint256 private _resumeSinceTimestamp = 0; + mapping(uint256 => WithdrawalRequest) private _queue; + mapping(address => EnumerableSet.UintSet) private _requestsByOwner; + mapping(uint256 => Checkpoint) private _checkpoints; + mapping(uint256 => address) private _tokenApprovals; + mapping(address => mapping(address => bool)) private _operatorApprovals; + + /// + + constructor(address stEth, address payable refundAddress) { + ST_ETH = IStETH(stEth); + _refundAddress = refundAddress; + + _queue[0] = WithdrawalRequest(0, 0, address(0), uint40(block.timestamp), true, 0); + _checkpoints[_lastCheckpointIndex] = Checkpoint(0, 0); + } + + function name() external pure override returns (string memory) { + return "WithdrawalQueueMock"; + } + + function symbol() external pure override returns (string memory) { + return "MockUnstETH"; + } + + function tokenURI(uint256 /* _requestId */ ) public view virtual override returns (string memory) { + return ""; + } + + function balanceOf(address _owner) external view override returns (uint256) { + return _requestsByOwner[_owner].length(); + } + + function ownerOf(uint256 _requestId) public view override returns (address) { + if (_requestId == 0 || _requestId > _lastRequestId) revert InvalidRequestId(_requestId); + + WithdrawalRequest storage request = _queue[_requestId]; + if (request.claimed) revert RequestAlreadyClaimed(_requestId); + + return request.owner; + } + + function getLastRequestId() public view returns (uint256) { + return _lastRequestId; + } + + function getLastFinalizedRequestId() public view returns (uint256) { + return _lastFinalizedRequestId; + } + + function getLastCheckpointIndex() public view returns (uint256) { + return _lastCheckpointIndex; + } + + function requestWithdrawals( + uint256[] calldata _amounts, + address _owner + ) public returns (uint256[] memory requestIds) { + _checkResumed(); + if (_owner == address(0)) _owner = msg.sender; + requestIds = new uint256[](_amounts.length); + for (uint256 i = 0; i < _amounts.length; ++i) { + requestIds[i] = _requestWithdrawal(_amounts[i], _owner); + } + } + + function findCheckpointHints( + uint256[] calldata _requestIds, + uint256 _firstIndex, + uint256 _lastIndex + ) external view returns (uint256[] memory hintIds) { + hintIds = new uint256[](_requestIds.length); + uint256 prevRequestId = 0; + for (uint256 i = 0; i < _requestIds.length; ++i) { + if (_requestIds[i] < prevRequestId) revert RequestIdsNotSorted(); + hintIds[i] = _findCheckpointHint(_requestIds[i], _firstIndex, _lastIndex); + _firstIndex = hintIds[i]; + prevRequestId = _requestIds[i]; + } + } + + function getWithdrawalStatus(uint256[] calldata _requestIds) + external + view + returns (WithdrawalRequestStatus[] memory statuses) + { + statuses = new WithdrawalRequestStatus[](_requestIds.length); + for (uint256 i = 0; i < _requestIds.length; ++i) { + statuses[i] = _getStatus(_requestIds[i]); + } + } + + function getClaimableEther( + uint256[] calldata _requestIds, + uint256[] calldata _hints + ) external view returns (uint256[] memory claimableEthValues) { + claimableEthValues = new uint256[](_requestIds.length); + for (uint256 i = 0; i < _requestIds.length; ++i) { + claimableEthValues[i] = _getClaimableEther(_requestIds[i], _hints[i]); + } + } + + function claimWithdrawals(uint256[] calldata _requestIds, uint256[] calldata _hints) external { + if (_requestIds.length != _hints.length) { + revert ArraysLengthMismatch(_requestIds.length, _hints.length); + } + + for (uint256 i = 0; i < _requestIds.length; ++i) { + _claim(_requestIds[i], _hints[i], payable(msg.sender)); + } + } + + function safeTransferFrom(address _from, address _to, uint256 _requestId) external pure override { + safeTransferFrom(_from, _to, _requestId, ""); + } + + function safeTransferFrom( + address, /* _from */ + address, /* _to */ + uint256, /* _requestId */ + bytes memory /* _data */ + ) public pure override { + // solhint-disable-next-line custom-errors + revert("Not implemented"); + } + + function transferFrom( + address _from, + address _to, + uint256 _requestId + ) external override(IWithdrawalQueue, IERC721) { + _transfer(_from, _to, _requestId); + } + + function approve(address _to, uint256 _requestId) external override { + address _owner = ownerOf(_requestId); + if (_to == _owner) revert ApprovalToOwner(); + if (msg.sender != _owner && !isApprovedForAll(_owner, msg.sender)) revert NotOwnerOrApprovedForAll(msg.sender); + + _tokenApprovals[_requestId] = _to; + } + + function setApprovalForAll(address _operator, bool _approved) external override { + if (msg.sender == _operator) revert ApproveToCaller(); + _operatorApprovals[msg.sender][_operator] = _approved; + } + + function getApproved(uint256 _requestId) external view override returns (address) { + if (!_existsAndNotClaimed(_requestId)) revert InvalidRequestId(_requestId); + + return _tokenApprovals[_requestId]; + } + + function isApprovedForAll(address _owner, address _operator) public view override returns (bool) { + return _operatorApprovals[_owner][_operator]; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC721).interfaceId || interfaceId == type(IERC721Metadata).interfaceId + // 0x49064906 is magic number ERC4906 interfaceId as defined in the standard https://eips.ethereum.org/EIPS/eip-4906 + || interfaceId == bytes4(0x49064906); + } + + function finalize(uint256 _lastRequestIdToBeFinalized, uint256 _maxShareRate) external payable { + _checkResumed(); + _finalize(_lastRequestIdToBeFinalized, msg.value, _maxShareRate); + } + + function refundEth(uint256 amountOfETH) public { + Address.sendValue(_refundAddress, amountOfETH); + } + + function markClaimed(uint256[] calldata _requestIds) external { + for (uint256 i = 0; i < _requestIds.length; ++i) { + _markClaimed(_requestIds[i]); + } + } + + function resume() external { + _resumeSinceTimestamp = block.timestamp; + } + + function pauseFor(uint256 _duration) external { + uint256 resumeSince; + if (_duration == PAUSE_INFINITELY) { + resumeSince = PAUSE_INFINITELY; + } else { + resumeSince = block.timestamp + _duration; + } + _resumeSinceTimestamp = resumeSince; + } + + function isPaused() external view returns (bool) { + return _isPaused(); + } + + function getResumeSinceTimestamp() external view returns (uint256) { + return _resumeSinceTimestamp; + } + + /////////////////////////////// + + function _requestWithdrawal(uint256 _amountOfStETH, address _owner) internal returns (uint256 requestId) { + ST_ETH.transferFrom(msg.sender, address(this), _amountOfStETH); + + uint256 amountOfShares = ST_ETH.getSharesByPooledEth(_amountOfStETH); + + requestId = _enqueue(uint128(_amountOfStETH), uint128(amountOfShares), _owner); + } + + function _enqueue( + uint128 _amountOfStETH, + uint128 _amountOfShares, + address _owner + ) internal returns (uint256 requestId) { + WithdrawalRequest memory lastRequest = _queue[_lastRequestId]; + + uint128 cumulativeShares = lastRequest.cumulativeShares + _amountOfShares; + uint128 cumulativeStETH = lastRequest.cumulativeStETH + _amountOfStETH; + + requestId = _lastRequestId + 1; + + _lastRequestId = requestId; + + WithdrawalRequest memory newRequest = WithdrawalRequest( + cumulativeStETH, cumulativeShares, _owner, uint40(block.timestamp), false, uint40(block.timestamp) + ); + _queue[requestId] = newRequest; + assert(_requestsByOwner[_owner].add(requestId)); + } + + function _findCheckpointHint(uint256 _requestId, uint256 _start, uint256 _end) internal view returns (uint256) { + if (_requestId == 0 || _requestId > _lastRequestId) revert InvalidRequestId(_requestId); + + if (_start == 0 || _end > _lastCheckpointIndex) revert InvalidRequestIdRange(_start, _end); + + if (_lastCheckpointIndex == 0 || _requestId > _lastFinalizedRequestId || _start > _end) return NOT_FOUND; + + if (_requestId >= _checkpoints[_end].fromRequestId) { + if (_end == _lastCheckpointIndex) return _end; + if (_requestId < _checkpoints[_end + 1].fromRequestId) return _end; + + return NOT_FOUND; + } + + if (_requestId < _checkpoints[_start].fromRequestId) { + return NOT_FOUND; + } + + uint256 min = _start; + uint256 max = _end - 1; + + while (max > min) { + uint256 mid = (max + min + 1) / 2; + if (_checkpoints[mid].fromRequestId <= _requestId) { + min = mid; + } else { + max = mid - 1; + } + } + return min; + } + + function _getStatus(uint256 _requestId) internal view returns (WithdrawalRequestStatus memory status) { + if (_requestId == 0 || _requestId > _lastRequestId) revert InvalidRequestId(_requestId); + + WithdrawalRequest memory request = _queue[_requestId]; + WithdrawalRequest memory previousRequest = _queue[_requestId - 1]; + + status = WithdrawalRequestStatus( + request.cumulativeStETH - previousRequest.cumulativeStETH, + request.cumulativeShares - previousRequest.cumulativeShares, + request.owner, + request.timestamp, + _requestId <= _lastFinalizedRequestId, + request.claimed + ); + } + + function _getClaimableEther(uint256 _requestId, uint256 _hint) internal view returns (uint256) { + if (_requestId == 0 || _requestId > _lastRequestId) revert InvalidRequestId(_requestId); + + if (_requestId > _lastFinalizedRequestId) return 0; + + WithdrawalRequest storage request = _queue[_requestId]; + if (request.claimed) return 0; + + return _calculateClaimableEther(request, _requestId, _hint); + } + + function _calculateClaimableEther( + WithdrawalRequest storage _request, + uint256 _requestId, + uint256 _hint + ) internal view returns (uint256 claimableEther) { + if (_hint == 0) revert InvalidHint(_hint); + + if (_hint > _lastCheckpointIndex) revert InvalidHint(_hint); + + Checkpoint memory checkpoint = _checkpoints[_hint]; + + if (_requestId < checkpoint.fromRequestId) revert InvalidHint(_hint); + if (_hint < _lastCheckpointIndex) { + Checkpoint memory nextCheckpoint = _checkpoints[_hint + 1]; + if (nextCheckpoint.fromRequestId <= _requestId) revert InvalidHint(_hint); + } + + WithdrawalRequest memory prevRequest = _queue[_requestId - 1]; + (uint256 batchShareRate, uint256 eth, uint256 shares) = _calcBatch(prevRequest, _request); + + if (batchShareRate > checkpoint.maxShareRate) { + eth = shares * checkpoint.maxShareRate / E27_PRECISION_BASE; + } + + return eth; + } + + function _calcBatch( + WithdrawalRequest memory _preStartRequest, + WithdrawalRequest memory _endRequest + ) internal pure returns (uint256 shareRate, uint256 _stETH, uint256 shares) { + _stETH = _endRequest.cumulativeStETH - _preStartRequest.cumulativeStETH; + shares = _endRequest.cumulativeShares - _preStartRequest.cumulativeShares; + + shareRate = _stETH * E27_PRECISION_BASE / shares; + } + + function _claim(uint256 _requestId, uint256 _hint, address payable _recipient) internal { + if (_requestId == 0) revert InvalidRequestId(_requestId); + if (_requestId > _lastFinalizedRequestId) revert RequestNotFoundOrNotFinalized(_requestId); + + WithdrawalRequest storage request = _queue[_requestId]; + + if (request.claimed) revert RequestAlreadyClaimed(_requestId); + + if (request.owner != msg.sender) revert NotOwner(msg.sender, request.owner); + + request.claimed = true; + assert(_requestsByOwner[request.owner].remove(_requestId)); + + uint256 ethWithDiscount = _calculateClaimableEther(request, _requestId, _hint); + _lockedEtherAmount -= ethWithDiscount; + Address.sendValue(_recipient, ethWithDiscount); + } + + function _transfer(address _from, address _to, uint256 _requestId) internal { + if (_to == address(0)) revert TransferToZeroAddress(); + if (_to == _from) revert TransferToThemselves(); + if (_requestId == 0 || _requestId > _lastRequestId) revert InvalidRequestId(_requestId); + + WithdrawalRequest storage request = _queue[_requestId]; + if (request.claimed) revert RequestAlreadyClaimed(_requestId); + + if (_from != request.owner) revert NotOwner(_from, request.owner); + + address msgSender = msg.sender; + if (!(_from == msgSender || isApprovedForAll(_from, msgSender) || _tokenApprovals[_requestId] == msgSender)) { + revert NotOwnerOrApproved(msgSender); + } + + delete _tokenApprovals[_requestId]; + request.owner = _to; + + assert(_requestsByOwner[_from].remove(_requestId)); + assert(_requestsByOwner[_to].add(_requestId)); + } + + function _existsAndNotClaimed(uint256 _requestId) internal view returns (bool) { + return _requestId > 0 && _requestId <= _lastRequestId && !_queue[_requestId].claimed; + } + + function _finalize(uint256 _lastRequestIdToBeFinalized, uint256 _amountOfETH, uint256 _maxShareRate) internal { + if (_lastRequestIdToBeFinalized > _lastRequestId) revert InvalidRequestId(_lastRequestIdToBeFinalized); + if (_lastRequestIdToBeFinalized <= _lastFinalizedRequestId) { + revert InvalidRequestId(_lastRequestIdToBeFinalized); + } + + WithdrawalRequest memory lastFinalizedRequest = _queue[_lastFinalizedRequestId]; + WithdrawalRequest memory requestToFinalize = _queue[_lastRequestIdToBeFinalized]; + + uint128 stETHToFinalize = requestToFinalize.cumulativeStETH - lastFinalizedRequest.cumulativeStETH; + + uint256 firstRequestIdToFinalize = _lastFinalizedRequestId + 1; + + _checkpoints[_lastCheckpointIndex + 1] = Checkpoint(firstRequestIdToFinalize, _maxShareRate); + _lastCheckpointIndex = _lastCheckpointIndex + 1; + + _lockedEtherAmount = _lockedEtherAmount + _amountOfETH; + _lastFinalizedRequestId = _lastRequestIdToBeFinalized; + + StETHMock(address(ST_ETH)).burn(address(this), stETHToFinalize); + } + + function _markClaimed(uint256 _requestId) internal { + if (_requestId == 0) revert InvalidRequestId(_requestId); + if (_requestId > _lastFinalizedRequestId) revert RequestNotFoundOrNotFinalized(_requestId); + + WithdrawalRequest storage request = _queue[_requestId]; + + if (request.claimed) revert RequestAlreadyClaimed(_requestId); + + request.claimed = true; + assert(_requestsByOwner[request.owner].remove(_requestId)); + } + + function _isPaused() internal view returns (bool) { + return block.timestamp < _resumeSinceTimestamp; + } + + function _checkResumed() internal view { + if (_isPaused()) { + revert ResumedExpected(); + } + } +} diff --git a/scripts/lido-mocks/WstETHMock.sol b/scripts/lido-mocks/WstETHMock.sol new file mode 100644 index 00000000..5bdb20d4 --- /dev/null +++ b/scripts/lido-mocks/WstETHMock.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IStETH} from "contracts/interfaces/IStETH.sol"; +import {IWstETH} from "contracts/interfaces/IWstETH.sol"; + +/* solhint-disable custom-errors, reason-string */ +contract WstETHMock is ERC20, IWstETH { + IStETH public stETH; + + constructor(IStETH _stETH) ERC20("WstETHMock", "MWst") { + stETH = _stETH; + } + + function wrap(uint256 stETHAmount) external returns (uint256) { + require(stETHAmount > 0, "wstETH: can't wrap zero stETH"); + uint256 wstETHAmount = stETH.getSharesByPooledEth(stETHAmount); + _mint(msg.sender, wstETHAmount); + stETH.transferFrom(msg.sender, address(this), stETHAmount); + return wstETHAmount; + } + + function unwrap(uint256 wstETHAmount) external returns (uint256) { + require(wstETHAmount > 0, "wstETH: zero amount unwrap not allowed"); + uint256 stETHAmount = stETH.getPooledEthByShares(wstETHAmount); + _burn(msg.sender, wstETHAmount); + stETH.transfer(msg.sender, stETHAmount); + return stETHAmount; + } + + function getStETHByWstETH(uint256 wstethAmount) external view returns (uint256) { + return stETH.getPooledEthByShares(wstethAmount); + } +} From 38c92249a8f777beadf1f772030a64f71e65f93d Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 29 Nov 2024 23:43:24 +0400 Subject: [PATCH 075/107] Change rage quit support checks --- contracts/libraries/DualGovernanceConfig.sol | 18 ++++++------- .../libraries/DualGovernanceStateMachine.sol | 2 +- .../DualGovernanceStateTransitions.sol | 10 +++---- contracts/libraries/TimelockState.sol | 2 -- docs/mechanism.md | 20 +++++++------- test/scenario/dg-update-tokens-rotation.t.sol | 4 +-- test/scenario/gov-state-transitions.t.sol | 6 ++--- test/unit/DualGovernance.t.sol | 4 +-- .../unit/libraries/DualGovernanceConfig.t.sol | 17 ++++++------ .../DualGovernanceStateMachine.t.sol | 4 +-- .../DualGovernanceStateTransitions.t.sol | 26 +++++++++---------- 11 files changed, 57 insertions(+), 56 deletions(-) diff --git a/contracts/libraries/DualGovernanceConfig.sol b/contracts/libraries/DualGovernanceConfig.sol index 3a6eff80..f4f95f5c 100644 --- a/contracts/libraries/DualGovernanceConfig.sol +++ b/contracts/libraries/DualGovernanceConfig.sol @@ -90,26 +90,26 @@ library DualGovernanceConfig { } } - /// @notice Determines whether the first seal Rage Quit support threshold has been exceeded. + /// @notice Determines whether the first seal Rage Quit support threshold has been reached. /// @param self The configuration context. /// @param rageQuitSupport The current Rage Quit support level. - /// @return bool A boolean indicating whether the Rage Quit support level exceeds the first seal threshold. - function isFirstSealRageQuitSupportCrossed( + /// @return bool A boolean indicating whether the Rage Quit support level reaches the first seal threshold. + function isFirstSealRageQuitSupportReached( Context memory self, PercentD16 rageQuitSupport ) internal pure returns (bool) { - return rageQuitSupport > self.firstSealRageQuitSupport; + return rageQuitSupport >= self.firstSealRageQuitSupport; } - /// @notice Determines whether the second seal Rage Quit support threshold has been exceeded. + /// @notice Determines whether the second seal Rage Quit support threshold has been reached. /// @param self The configuration context. /// @param rageQuitSupport The current Rage Quit support level. - /// @return bool A boolean indicating whether the Rage Quit support level exceeds the second seal threshold. - function isSecondSealRageQuitSupportCrossed( + /// @return bool A boolean indicating whether the Rage Quit support level reaches the second seal threshold. + function isSecondSealRageQuitSupportReached( Context memory self, PercentD16 rageQuitSupport ) internal pure returns (bool) { - return rageQuitSupport > self.secondSealRageQuitSupport; + return rageQuitSupport >= self.secondSealRageQuitSupport; } /// @notice Determines whether the VetoSignalling duration has passed based on the current time. @@ -176,7 +176,7 @@ library DualGovernanceConfig { Duration vetoSignallingMinDuration = self.vetoSignallingMinDuration; Duration vetoSignallingMaxDuration = self.vetoSignallingMaxDuration; - if (rageQuitSupport <= firstSealRageQuitSupport) { + if (rageQuitSupport < firstSealRageQuitSupport) { return Durations.ZERO; } diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index c67b53ee..85aba270 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -27,7 +27,7 @@ import {DualGovernanceStateTransitions} from "./DualGovernanceStateTransitions.s /// @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 +/// is triggered when the Second Seal Threshold is reached. During this state, the scheduling of proposals for /// execution is forbidden, but new proposals can still be submitted. enum State { Unset, diff --git a/contracts/libraries/DualGovernanceStateTransitions.sol b/contracts/libraries/DualGovernanceStateTransitions.sol index f8a58184..b7b78dc5 100644 --- a/contracts/libraries/DualGovernanceStateTransitions.sol +++ b/contracts/libraries/DualGovernanceStateTransitions.sol @@ -48,7 +48,7 @@ library DualGovernanceStateTransitions { DualGovernanceStateMachine.Context storage self, DualGovernanceConfig.Context memory config ) private view returns (State) { - return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + return config.isFirstSealRageQuitSupportReached(self.signallingEscrow.getRageQuitSupport()) ? State.VetoSignalling : State.Normal; } @@ -63,7 +63,7 @@ library DualGovernanceStateTransitions { return State.VetoSignalling; } - if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + if (config.isSecondSealRageQuitSupportReached(rageQuitSupport)) { return State.RageQuit; } @@ -82,7 +82,7 @@ library DualGovernanceStateTransitions { return State.VetoSignalling; } - if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + if (config.isSecondSealRageQuitSupportReached(rageQuitSupport)) { return State.RageQuit; } @@ -100,7 +100,7 @@ library DualGovernanceStateTransitions { if (!config.isVetoCooldownDurationPassed(self.enteredAt)) { return State.VetoCooldown; } - return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + return config.isFirstSealRageQuitSupportReached(self.signallingEscrow.getRageQuitSupport()) ? State.VetoSignalling : State.Normal; } @@ -112,7 +112,7 @@ library DualGovernanceStateTransitions { if (!self.rageQuitEscrow.isRageQuitFinalized()) { return State.RageQuit; } - return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + return config.isFirstSealRageQuitSupportReached(self.signallingEscrow.getRageQuitSupport()) ? State.VetoSignalling : State.VetoCooldown; } diff --git a/contracts/libraries/TimelockState.sol b/contracts/libraries/TimelockState.sol index 098edc69..8abbbf35 100644 --- a/contracts/libraries/TimelockState.sol +++ b/contracts/libraries/TimelockState.sol @@ -151,5 +151,3 @@ library TimelockState { } } } - - diff --git a/docs/mechanism.md b/docs/mechanism.md index 314564a5..fa3b1400 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -137,7 +137,7 @@ The Normal state is the state the mechanism is designed to spend the most time w **Transition to Veto Signalling**. If, while the state is active, the following expression becomes true: ```math -R > R_1 +R \geq R_1 ``` where $R_1$ is `FirstSealRageQuitSupport`, the Normal state is exited and the Veto Signalling state is entered. @@ -171,8 +171,8 @@ The **dynamic timelock duration** $T_{lock}(R)$ depends on the current rage quit ```math T_{lock}(R) = \left\{ \begin{array}{lr} - 0, & \text{if } R \leq R_1 \\ - L(R), & \text{if } R_1 < R < R_2 \\ + 0, & \text{if } R < R_1 \\ + L(R), & \text{if } R_1 \leq R < R_2 \\ L_{max}, & \text{if } R \geq R_2 \end{array} \right. ``` @@ -192,7 +192,7 @@ Let's now define the outgoing transitions. **Transition to Rage Quit**. If, while Veto Signalling is active and the Deactivation sub-state is not active, the following expression becomes true: ```math -\big( t - t^S_{act} > L_{max} \big) \, \land \, \big( R > R_2 \big) +\big( t - t^S_{act} > L_{max} \big) \, \land \, \big( R \geq R_2 \big) ``` the Veto Signalling state is exited and the Rage Quit state is entered. @@ -229,7 +229,7 @@ then the Deactivation sub-state is exited so only the parent Veto Signalling sta **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) +\big( t - t^S_{act} > L_{max} \big) \, \land \, \big( R \geq R_2 \big) ``` then the Deactivation sub-state is exited along with its parent Veto Signalling state and the Rage Quit state is entered. @@ -253,7 +253,7 @@ In the Veto Cooldown state, the DAO cannot submit proposals to the DG but can ex **Transition to Veto Signalling**. If, while the state is active, the following condition becomes true: ```math -\big( t - t^C_{act} > T^C \big) \,\land\, \big( R(t) > R_1 \big) +\big( t - t^C_{act} > T^C \big) \,\land\, \big( R(t) \geq R_1 \big) ``` where $t^{C}_{act}$ is the time the Veto Cooldown state was entered and $T^{C}$ is `VetoCooldownDuration`, then the Veto Cooldown state is exited and the Veto Signalling state is entered. @@ -261,7 +261,7 @@ where $t^{C}_{act}$ is the time the Veto Cooldown state was entered and $T^{C}$ **Transition to Normal**. If, while the state is active, the following condition becomes true: ```math -\big( t - t^C_{act} > T^C \big) \,\land\, \big( R(t) \leq R_1 \big) +\big( t - t^C_{act} > T^C \big) \,\land\, \big( R(t) < R_1 \big) ``` then the Veto Cooldown state is exited and the Normal state is entered. @@ -295,9 +295,9 @@ When the withdrawal is complete and the extension period elapses, two things hap 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. -**Transition to Veto Signalling**. If, at the moment of the Rage Quit state exit, $R(t) > R_1$, the Veto Signalling state is entered. +**Transition to Veto Signalling**. If, at the moment of the Rage Quit state exit, $R(t) \geq R_1$, the Veto Signalling state is entered. -**Transition to Veto Cooldown**. If, at the moment of the Rage Quit state exit, $R(t) \leq R_1$, the Veto Cooldown state is entered. +**Transition to Veto Cooldown**. If, at the moment of the Rage Quit state exit, $R(t) < R_1$, the Veto Cooldown state is entered. The duration of the ETH withdraw timelock $W(i)$ is a linear function that depends on the rage quit sequence number $i$ (see below): @@ -398,6 +398,8 @@ Dual governance should not cover: ## Changelog +### 2024-12-04 +- Updated calculations of Rage Quit support first/second seal reaching. ### 2024-11-15 - The rage quit sequence number is now reset in the `VetoCooldown` state instead of the `Normal` state. This adjustment ensures that the ETH withdrawal timelock does not increase unnecessarily in cases where, after a Rage Quit, Dual Governance cycles through `VetoSignalling` → `VetoSignallingDeactivation` → `VetoCooldown` without entering the `Normal` state, as the DAO remains operational and can continue submitting and executing proposals in this scenario. diff --git a/test/scenario/dg-update-tokens-rotation.t.sol b/test/scenario/dg-update-tokens-rotation.t.sol index 97f98191..88d22b60 100644 --- a/test/scenario/dg-update-tokens-rotation.t.sol +++ b/test/scenario/dg-update-tokens-rotation.t.sol @@ -42,7 +42,7 @@ contract DualGovernanceUpdateTokensRotation is ScenarioTestBlueprint { _step("3. Users accumulate some stETH in the Signalling Escrow"); { - _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() - PercentsD16.from(1)); _assertVetoSignalingState(); _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); @@ -88,7 +88,7 @@ contract DualGovernanceUpdateTokensRotation is ScenarioTestBlueprint { _step("7. Users can withdraw funds even if the Rage Quit is started in the old instance of the Dual Governance"); { // the Rage Quit started on the old DualGovernance instance - _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.from(1)); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertRageQuitState(); diff --git a/test/scenario/gov-state-transitions.t.sol b/test/scenario/gov-state-transitions.t.sol index 7946257a..a5eafee8 100644 --- a/test/scenario/gov-state-transitions.t.sol +++ b/test/scenario/gov-state-transitions.t.sol @@ -18,7 +18,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_state_min_duration() public { _assertNormalState(); - _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT()); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() - PercentsD16.from(1)); _assertNormalState(); _lockStETH(_VETOER, 1 gwei); @@ -63,7 +63,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_to_normal() public { _assertNormalState(); - _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT()); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() - PercentsD16.from(1)); _assertNormalState(); @@ -93,7 +93,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_non_stop() public { _assertNormalState(); - _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT()); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() - PercentsD16.from(1)); _assertNormalState(); _lockStETH(_VETOER, 1 gwei); diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 8de47811..e6b3b1c4 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -537,7 +537,7 @@ contract DualGovernanceUnitTests is UnitTest { _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().dividedBy(2)); - // The RageQuit second seal threshold wasn't crossed, the system should enter Deactivation state + // The RageQuit second seal threshold wasn't reached, the system should enter Deactivation state // where the proposals submission is not allowed assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); assertEq(_dualGovernance.getEffectiveState(), State.VetoSignallingDeactivation); @@ -754,7 +754,7 @@ contract DualGovernanceUnitTests is UnitTest { abi.encode(true) ); - // The RageQuit second seal threshold wasn't crossed, the system should enter Deactivation state + // The RageQuit second seal threshold wasn't reached, the system should enter Deactivation state // where the proposals submission is not allowed assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); assertEq(_dualGovernance.getEffectiveState(), State.VetoSignallingDeactivation); diff --git a/test/unit/libraries/DualGovernanceConfig.t.sol b/test/unit/libraries/DualGovernanceConfig.t.sol index 6b37503d..2fd17294 100644 --- a/test/unit/libraries/DualGovernanceConfig.t.sol +++ b/test/unit/libraries/DualGovernanceConfig.t.sol @@ -106,31 +106,32 @@ contract DualGovernanceConfigTest is UnitTest { } // --- - // isFirstSealRageQuitSupportCrossed() + // isFirstSealRageQuitSupportReached() // --- - function testFuzz_isFirstSealRageQuitSupportCrossed_HappyPath( + function testFuzz_isFirstSealRageQuitSupportReached_HappyPath( DualGovernanceConfig.Context memory config, PercentD16 rageQuitSupport ) external { _assumeConfigParams(config); assertEq( - config.isFirstSealRageQuitSupportCrossed(rageQuitSupport), rageQuitSupport > config.firstSealRageQuitSupport + config.isFirstSealRageQuitSupportReached(rageQuitSupport), + rageQuitSupport >= config.firstSealRageQuitSupport ); } // --- - // isSecondSealRageQuitSupportCrossed() + // isSecondSealRageQuitSupportReached() // --- - function testFuzz_isSecondSealRageQuitSupportCrossed_HappyPath( + function testFuzz_isSecondSealRageQuitSupportReached_HappyPath( DualGovernanceConfig.Context memory config, PercentD16 rageQuitSupport ) external { _assumeConfigParams(config); assertEq( - config.isSecondSealRageQuitSupportCrossed(rageQuitSupport), - rageQuitSupport > config.secondSealRageQuitSupport + config.isSecondSealRageQuitSupportReached(rageQuitSupport), + rageQuitSupport >= config.secondSealRageQuitSupport ); } @@ -269,7 +270,7 @@ contract DualGovernanceConfigTest is UnitTest { PercentD16 rageQuitSupport ) external { _assumeConfigParams(config); - vm.assume(rageQuitSupport <= config.firstSealRageQuitSupport); + vm.assume(rageQuitSupport < config.firstSealRageQuitSupport); assertEq(config.calcVetoSignallingDuration(rageQuitSupport), Durations.ZERO); } diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 3377885f..b6c5cea3 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -66,7 +66,7 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { _activateNextState(); _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); - // Simulate the Rage Quit process has completed and in the SignallingEscrow the first seal is not crossed + // Simulate the Rage Quit process has completed and in the SignallingEscrow the first seal is not reached _mockRageQuitFinalized(true); _activateNextState(); _mockRageQuitSupport(PercentsD16.fromBasisPoints(0)); @@ -90,7 +90,7 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { _activateNextState(); _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); - // Simulate the Rage Quit process has completed and in the SignallingEscrow the first seal is crossed + // Simulate the Rage Quit process has completed and in the SignallingEscrow the first seal is reached _mockRageQuitFinalized(true); _activateNextState(); diff --git a/test/unit/libraries/DualGovernanceStateTransitions.t.sol b/test/unit/libraries/DualGovernanceStateTransitions.t.sol index a374c919..f9798105 100644 --- a/test/unit/libraries/DualGovernanceStateTransitions.t.sol +++ b/test/unit/libraries/DualGovernanceStateTransitions.t.sol @@ -65,7 +65,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { function test_getStateTransition_FromNormalToNormal() external { assertEq(_stateMachine.state, State.Normal); - _setMockRageQuitSupportInBP(3_00); + _setMockRageQuitSupportInBP(2_99); (State current, State next) = _stateMachine.getStateTransition(_configProvider.getDualGovernanceConfig()); @@ -80,7 +80,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { function test_getStateTransition_FromNormalToVetoSignalling() external { assertEq(_stateMachine.state, State.Normal); - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); (State current, State next) = _stateMachine.getStateTransition(_configProvider.getDualGovernanceConfig()); @@ -94,7 +94,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { function test_getStateTransition_FromVetoSignallingToVetoSignalling_VetoSignallingDurationNotPassed() external { _setupVetoSignallingState(); - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); (State current, State next) = _stateMachine.getStateTransition(_configProvider.getDualGovernanceConfig()); @@ -109,7 +109,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { function test_getStateTransition_FromVetoSignallingToVetoSignalling_VetoSignallingReactivationNotPassed() external { - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); // the veto signalling state was entered _setupVetoSignallingState(); @@ -139,7 +139,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { // --- function test_getStateTransition_FromVetoSignallingToRageQuit() external { - _setMockRageQuitSupportInBP(15_01); + _setMockRageQuitSupportInBP(15_00); // the veto signalling state was entered _setupVetoSignallingState(); @@ -160,12 +160,12 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { // --- function test_getStateTransition_FromVetoSignallingDeactivationToVetoSignalling() external { - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); _setupVetoSignallingDeactivationState(); _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignallingDeactivation}); - _setMockRageQuitSupportInBP(15_01); + _setMockRageQuitSupportInBP(15_00); _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignalling}); } @@ -175,12 +175,12 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { // --- function test_getStateTransition_FromVetoSignallingDeactivationToRageQuit() external { - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); _setupVetoSignallingDeactivationState(); _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignallingDeactivation}); - _setMockRageQuitSupportInBP(15_01); + _setMockRageQuitSupportInBP(15_00); _wait(_calcVetoSignallingDuration().plusSeconds(1 seconds)); @@ -192,7 +192,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { // --- function test_getStateTransition_FromVetoSignallingDeactivationToVetoCooldown() external { - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); _setupVetoSignallingDeactivationState(); _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignallingDeactivation}); @@ -207,7 +207,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { // --- function test_getStateTransition_FromVetoSignallingDeactivationToVetoSignallingDeactivation() external { - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); _setupVetoSignallingDeactivationState(); _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignallingDeactivation}); @@ -248,7 +248,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { _assertStateMachineTransition({from: State.VetoCooldown, to: State.VetoCooldown}); - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); @@ -291,7 +291,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { _assertStateMachineTransition({from: State.RageQuit, to: State.RageQuit}); _setMockIsRageQuitFinalized(true); - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); _assertStateMachineTransition({from: State.RageQuit, to: State.VetoSignalling}); } From adecd91f8337b9315dae969877fd24d9055d8956 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 4 Dec 2024 21:13:24 +0400 Subject: [PATCH 076/107] Update changelog, change test name --- docs/mechanism.md | 2 +- test/unit/libraries/DualGovernanceConfig.t.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index fa3b1400..da571f19 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -399,7 +399,7 @@ Dual governance should not cover: ## Changelog ### 2024-12-04 -- Updated calculations of Rage Quit support first/second seal reaching. +- Updated calculations of Rage Quit support first/second threshold's reaching. The transition to VetoSignalling state now starts when the amount of locked funds reaches the first seal threshold (including the exact threshold value), and the transition to RageQuit state appropriately starts when the amount of locked funds reaches the second seal threshold (including the exact threshold value; the appropriate duration of time in VetoSignalling state still should pass). ### 2024-11-15 - The rage quit sequence number is now reset in the `VetoCooldown` state instead of the `Normal` state. This adjustment ensures that the ETH withdrawal timelock does not increase unnecessarily in cases where, after a Rage Quit, Dual Governance cycles through `VetoSignalling` → `VetoSignallingDeactivation` → `VetoCooldown` without entering the `Normal` state, as the DAO remains operational and can continue submitting and executing proposals in this scenario. diff --git a/test/unit/libraries/DualGovernanceConfig.t.sol b/test/unit/libraries/DualGovernanceConfig.t.sol index 2fd17294..ec9a6c68 100644 --- a/test/unit/libraries/DualGovernanceConfig.t.sol +++ b/test/unit/libraries/DualGovernanceConfig.t.sol @@ -265,7 +265,7 @@ contract DualGovernanceConfigTest is UnitTest { // calcVetoSignallingDuration() // --- - function testFuzz_calcVetoSignallingDuration_HappyPath_RageQuitSupportLessOrEqualThanFirstSeal( + function testFuzz_calcVetoSignallingDuration_HappyPath_RageQuitSupportLessThanFirstSeal( DualGovernanceConfig.Context memory config, PercentD16 rageQuitSupport ) external { @@ -288,7 +288,7 @@ contract DualGovernanceConfigTest is UnitTest { PercentD16 rageQuitSupport ) external { _assumeConfigParams(config); - vm.assume(rageQuitSupport > config.firstSealRageQuitSupport); + vm.assume(rageQuitSupport >= config.firstSealRageQuitSupport); vm.assume(rageQuitSupport < config.secondSealRageQuitSupport); PercentD16 rageQuitSupportFirstSealDelta = rageQuitSupport - config.firstSealRageQuitSupport; From 4cc7230a7f2cea62154dc95669c82542d8f4278f Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 5 Dec 2024 03:42:24 +0400 Subject: [PATCH 077/107] Implement getters logic. Split IEscrow file. --- contracts/DualGovernance.sol | 2 +- contracts/Escrow.sol | 218 ++++++++++-------- contracts/interfaces/IEscrow.sol | 112 --------- contracts/interfaces/IEscrowBase.sol | 14 ++ contracts/interfaces/IRageQuitEscrow.sol | 34 +++ contracts/interfaces/ISignallingEscrow.sol | 65 ++++++ .../libraries/DualGovernanceStateMachine.sol | 4 +- scripts/deploy/DeployVerification.sol | 2 +- test/mocks/EscrowMock.sol | 22 +- test/scenario/dg-update-tokens-rotation.t.sol | 4 +- test/scenario/escrow.t.sol | 34 +-- test/scenario/veto-cooldown-mechanics.t.sol | 4 +- test/unit/DualGovernance.t.sol | 2 +- test/unit/Escrow.t.sol | 22 +- .../DualGovernanceStateMachine.t.sol | 4 +- .../DualGovernanceStateTransitions.t.sol | 4 +- test/utils/scenario-test-blueprint.sol | 42 ++-- 17 files changed, 310 insertions(+), 279 deletions(-) delete mode 100644 contracts/interfaces/IEscrow.sol create mode 100644 contracts/interfaces/IEscrowBase.sol create mode 100644 contracts/interfaces/IRageQuitEscrow.sol create mode 100644 contracts/interfaces/ISignallingEscrow.sol diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index f87982f5..d5f1fb96 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -7,7 +7,7 @@ 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 {IEscrowBase} from "./interfaces/IEscrow.sol"; +import {IEscrowBase} from "./interfaces/IEscrowBase.sol"; import {ITimelock} from "./interfaces/ITimelock.sol"; import {ITiebreaker} from "./interfaces/ITiebreaker.sol"; import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 9f493de8..c7deb5d5 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -4,12 +4,14 @@ pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Duration} from "./types/Duration.sol"; -import {Timestamp} from "./types/Timestamp.sol"; import {ETHValue, ETHValues} from "./types/ETHValue.sol"; import {SharesValue, SharesValues} from "./types/SharesValue.sol"; import {PercentD16, PercentsD16} from "./types/PercentD16.sol"; -import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "./interfaces/IEscrow.sol"; +import {IEscrowBase} from "./interfaces/IEscrowBase.sol"; +import {ISignallingEscrow} from "./interfaces/ISignallingEscrow.sol"; +import {IRageQuitEscrow} from "./interfaces/IRageQuitEscrow.sol"; + import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; @@ -17,7 +19,13 @@ import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; import {EscrowState, State} from "./libraries/EscrowState.sol"; import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalsBatchesQueue.sol"; -import {HolderAssets, StETHAccounting, UnstETHAccounting, AssetsAccounting} from "./libraries/AssetsAccounting.sol"; +import { + HolderAssets, + UnstETHRecord, + StETHAccounting, + UnstETHAccounting, + AssetsAccounting +} from "./libraries/AssetsAccounting.sol"; /// @notice This contract is used to accumulate stETH, wstETH, unstETH, and withdrawn ETH from vetoers during the /// veto signalling and rage quit processes. @@ -116,6 +124,10 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { MIN_WITHDRAWALS_BATCH_SIZE = minWithdrawalsBatchSize; } + // --- + // Escrow Base + // --- + /// @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. @@ -131,15 +143,12 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); } - // --- - // Base Escrow Getters - // --- function getEscrowState() external view returns (State) { return _escrowState.state; } // --- - // Lock & Unlock stETH + // Signalling Escrow: Lock & Unlock stETH // --- /// @notice Locks the vetoer's specified `amount` of stETH in the Veto Signalling Escrow, thereby increasing @@ -206,7 +215,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Lock & Unlock unstETH + // Signalling Escrow: Lock & Unlock unstETH // --- /// @notice Locks the specified unstETH NFTs, identified by their ids, in the Veto Signalling Escrow, thereby increasing @@ -270,7 +279,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Start Rage Quit + // Signalling Escrow: Start Rage Quit // --- /// @notice Irreversibly converts the Signalling Escrow into the Rage Quit Escrow, allowing vetoers who have locked @@ -292,7 +301,19 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Signalling Escrow Getters + // Signalling 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); + } + + // --- + // Signalling Escrow: Getters // --- /// @notice Returns the current Rage Quit support value as a percentage. @@ -316,14 +337,74 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { minAssetsLockDuration = _escrowState.minAssetsLockDuration; } - // TODO: implement - function getLockedUnstETHState(uint256 unstETHId) external view returns (LockedUnstETHState memory) {} + /// @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 details A struct containing information about the vetoer's locked assets, including: + /// - `unstETHIdsCount`: The total number of unstETH NFTs locked by the vetoer. + /// - `stETHLockedShares`: The total number of stETH shares locked by the vetoer. + /// - `unstETHLockedShares`: The total number of unstETH shares locked by the vetoer. + /// - `lastAssetsLockTimestamp`: The timestamp of the last assets lock by the vetoer. + function getVetoerDetails(address vetoer) external view returns (VetoerDetails memory details) { + HolderAssets storage assets = _accounting.assets[vetoer]; - // TODO: implement - function getSignallingEscrowState() external view returns (SignallingEscrowState memory) {} + details.unstETHIdsCount = assets.unstETHIds.length; + details.stETHLockedShares = assets.stETHLockedShares; + details.unstETHLockedShares = assets.unstETHLockedShares; + details.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp; + } + + // @notice Retrieves the unstETH NFT ids of the specified vetoer. + /// @param vetoer The address of the vetoer whose unstETH NFTs are being queried. + /// @return unstETHIds An array of unstETH NFT ids locked by the vetoer. + function getVetoerUnstETHIds(address vetoer) external view returns (uint256[] memory unstETHIds) { + unstETHIds = _accounting.assets[vetoer].unstETHIds; + } + + /// @notice Returns the total amounts of locked and claimed assets in the Escrow. + /// @return details A struct containing the total amounts of locked and claimed assets, including: + /// - `totalStETHClaimedETH`: The total amount of ETH claimed from locked stETH. + /// - `totalStETHLockedShares`: The total number of stETH shares currently locked in the Escrow. + /// - `totalUnstETHUnfinalizedShares`: The total number of shares from unstETH NFTs that have not yet been finalized. + /// - `totalUnstETHFinalizedETH`: The total amount of ETH from finalized unstETH NFTs. + function getSignallingEscrowDetails() external view returns (SignallingEscrowDetails memory details) { + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + details.totalStETHClaimedETH = stETHTotals.claimedETH; + details.totalStETHLockedShares = stETHTotals.lockedShares; + + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + details.totalUnstETHUnfinalizedShares = unstETHTotals.unfinalizedShares; + details.totalUnstETHFinalizedETH = unstETHTotals.finalizedETH; + } + + /// @notice Retrieves details of locked unstETH records for the given ids. + /// @param unstETHIds The array of ids for the unstETH records to retrieve. + /// @return unstETHDetails An array of `LockedUnstETHDetails` containing the details for each provided unstETH id. + /// + /// The details include: + /// - `status`: The current status of the unstETH record. + /// - `lockedBy`: The address that locked the unstETH record. + /// - `shares`: The number of shares associated with the locked unstETH. + /// - `claimableAmount`: The amount of claimable ETH contained in the unstETH. This value is 0 + /// until the unstETH is finalized or claimed. + function getLockedUnstETHDetails(uint256[] calldata unstETHIds) + external + view + returns (LockedUnstETHDetails[] memory unstETHDetails) + { + unstETHDetails = new LockedUnstETHDetails[](unstETHIds.length); + + for (uint256 i = 0; i < unstETHIds.length; ++i) { + UnstETHRecord memory unstETHRecord = _accounting.unstETHRecords[unstETHIds[i]]; + + unstETHDetails[i].status = unstETHRecord.status; + unstETHDetails[i].lockedBy = unstETHRecord.lockedBy; + unstETHDetails[i].shares = unstETHRecord.shares; + unstETHDetails[i].claimableAmount = unstETHRecord.claimableAmount; + } + } // --- - // Request Withdrawal Batches + // Rage Quit Escrow: Request Withdrawal Batches // --- /// @notice Creates unstETH NFTs from the stETH held in the Rage Quit Escrow via the WithdrawalQueue contract. @@ -367,7 +448,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Claim Requested Withdrawal Batches + // Rage Quit Escrow: Claim Requested Withdrawal Batches // --- /// @notice Allows the claim of finalized withdrawal NFTs generated via the `Escrow.requestNextWithdrawalsBatch()` method. @@ -403,7 +484,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Start Rage Quit Extension Delay + // Rage Quit Escrow: Start Rage Quit Extension Delay // --- /// @notice Initiates the Rage Quit Extension Period once all withdrawal batches have been claimed. @@ -436,7 +517,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Claim Locked unstETH NFTs + // Rage Quit Escrow: Claim Locked unstETH NFTs // --- /// @notice Allows users to claim finalized unstETH NFTs locked in the Rage Quit Escrow contract. @@ -460,19 +541,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // 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 + // Rage Quit Escrow: Withdraw Logic // --- /// @notice Allows the caller (i.e., `msg.sender`) to withdraw all stETH and wstETH they have previously locked @@ -502,53 +571,10 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { // Rage Quit Escrow Getters // --- - // TODO: implement - function getRageQuitEscrowState() external view returns (RageQuitEscrowState memory) {} - - /// @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(); - totals.stETHLockedShares = stETHTotals.lockedShares.toUint256(); - - UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; - totals.unstETHUnfinalizedShares = unstETHTotals.unfinalizedShares.toUint256(); - 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]; - - state.unstETHIdsCount = assets.unstETHIds.length; - state.stETHLockedShares = assets.stETHLockedShares.toUint256(); - state.unstETHLockedShares = assets.unstETHLockedShares.toUint256(); - state.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp.toSeconds(); - } - - // @notice Retrieves the unstETH NFT ids of the specified vetoer. - /// @param vetoer The address of the vetoer whose unstETH NFTs are being queried. - /// @return unstETHIds An array of unstETH NFT ids locked by the vetoer. - function getVetoerUnstETHIds(address vetoer) external view returns (uint256[] memory unstETHIds) { - unstETHIds = _accounting.assets[vetoer].unstETHIds; - } - - /// @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) { - _escrowState.checkRageQuitEscrow(); - return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + /// @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(); } /// @notice Retrieves the unstETH NFT ids of the next batch available for claiming. @@ -559,31 +585,25 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { unstETHIds = _batchesQueue.getNextWithdrawalsBatches(limit); } - /// @notice Returns whether all withdrawal batches have been closed. - /// @return isWithdrawalsBatchesClosed A boolean value indicating whether all withdrawal batches have been - /// closed (`true`) or not (`false`). - function isWithdrawalsBatchesClosed() external view returns (bool) { + /// @notice Retrieves details about the current state of the rage quit escrow. + /// @return details A `RageQuitEscrowDetails` struct containing the following fields: + /// - `isWithdrawalsBatchesClosed`: Indicates whether the withdrawals batches are closed. + /// - `isRageQuitExtensionPeriodStarted`: Indicates whether the rage quit extension period has started. + /// - `unclaimedUnstETHIdsCount`: The total count of unstETH NFTs that have not been claimed yet. + /// - `rageQuitEthWithdrawalsDelay`: The delay period for ETH withdrawals during rage quit. + /// - `rageQuitExtensionPeriodDuration`: The duration of the rage quit extension period. + /// - `rageQuitExtensionPeriodStartedAt`: The timestamp when the rage quit extension period started. + function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory details) { _escrowState.checkRageQuitEscrow(); - 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(); - } + details.isWithdrawalsBatchesClosed = _batchesQueue.isClosed(); + details.isRageQuitExtensionPeriodStarted = _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; - } + details.unclaimedUnstETHIdsCount = _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); - /// @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(); + details.rageQuitEthWithdrawalsDelay = _escrowState.rageQuitEthWithdrawalsDelay; + details.rageQuitExtensionPeriodDuration = _escrowState.rageQuitExtensionPeriodDuration; + details.rageQuitExtensionPeriodStartedAt = _escrowState.rageQuitExtensionPeriodStartedAt; } // --- diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol deleted file mode 100644 index 7c72b64e..00000000 --- a/contracts/interfaces/IEscrow.sol +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Duration} from "../types/Duration.sol"; -import {Timestamp} from "../types/Timestamp.sol"; -import {PercentD16} from "../types/PercentD16.sol"; -import {Timestamp} from "../types/Timestamp.sol"; - -import {ETHValue} from "../types/ETHValue.sol"; -import {SharesValue} from "../types/SharesValue.sol"; - -import {State as EscrowState} from "../libraries/EscrowState.sol"; -import {UnstETHRecordStatus} from "../libraries/AssetsAccounting.sol"; - -interface IEscrowBase { - struct VetoerState { - uint256 stETHLockedShares; - uint256 unstETHLockedShares; - uint256 unstETHIdsCount; - uint256 lastAssetsLockTimestamp; - } - - /// @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. - /// TODO: Remove and use LockedUnstETHState instead - struct LockedAssetsTotals { - uint256 stETHLockedShares; - uint256 stETHClaimedETH; - uint256 unstETHUnfinalizedShares; - uint256 unstETHFinalizedETH; - } - - // TODO: add standalone getter - function ESCROW_MASTER_COPY() external view returns (IEscrowBase); - - function initialize(Duration minAssetsLockDuration) external; - - function getEscrowState() external view returns (EscrowState); - function getVetoerState(address vetoer) external view returns (VetoerState memory); -} - -interface ISignallingEscrow is IEscrowBase { - struct LockedUnstETHState { - UnstETHRecordStatus status; - address lockedBy; - SharesValue shares; - ETHValue claimableAmount; - } - - struct SignallingEscrowState { - PercentD16 rageQuitSupport; - // - ETHValue totalStETHClaimedETH; - SharesValue totalStETHLockedShares; - // - ETHValue totalUnstETHFinalizedETH; - SharesValue totalUnstETHUnfinalizedShares; - } - - function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares); - function unlockStETH() external returns (uint256 unlockedStETHShares); - - function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares); - function unlockWstETH() external returns (uint256 wstETHUnlocked); - - function lockUnstETH(uint256[] memory unstETHIds) external; - function unlockUnstETH(uint256[] memory unstETHIds) external; - - function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external; - - function startRageQuit( - Duration rageQuitExtensionPeriodDuration, - Duration rageQuitEthWithdrawalsDelay - ) external returns (IRageQuitEscrow); - - function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; - - function getRageQuitSupport() external view returns (PercentD16); - function getMinAssetsLockDuration() external view returns (Duration); - function getLockedUnstETHState(uint256 unstETHId) external view returns (LockedUnstETHState memory); - function getSignallingEscrowState() external view returns (SignallingEscrowState memory); -} - -interface IRageQuitEscrow is IEscrowBase { - struct RageQuitEscrowState { - bool isRageQuitFinalized; - bool isWithdrawalsBatchesClosed; - bool isRageQuitExtensionPeriodStarted; - uint256 unclaimedUnstETHIdsCount; - Duration rageQuitWithdrawalsDelay; - Duration rageQuitExtensionPeriodDuration; - Timestamp rageQuitExtensionPeriodStartedAt; - } - - function requestNextWithdrawalsBatch(uint256 batchSize) external; - - function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external; - function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external; - function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external; - - function startRageQuitExtensionPeriod() external; - - function withdrawETH() external; - function withdrawETH(uint256[] calldata unstETHIds) external; - - function isRageQuitFinalized() external view returns (bool); - function getRageQuitEscrowState() external view returns (RageQuitEscrowState memory); - function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds); -} diff --git a/contracts/interfaces/IEscrowBase.sol b/contracts/interfaces/IEscrowBase.sol new file mode 100644 index 00000000..c1e8f22e --- /dev/null +++ b/contracts/interfaces/IEscrowBase.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; + +import {State} from "../libraries/EscrowState.sol"; + +interface IEscrowBase { + function ESCROW_MASTER_COPY() external view returns (IEscrowBase); + + function initialize(Duration minAssetsLockDuration) external; + + function getEscrowState() external view returns (State); +} diff --git a/contracts/interfaces/IRageQuitEscrow.sol b/contracts/interfaces/IRageQuitEscrow.sol new file mode 100644 index 00000000..d31b6c2e --- /dev/null +++ b/contracts/interfaces/IRageQuitEscrow.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamp} from "../types/Timestamp.sol"; + +import {IEscrowBase} from "./IEscrowBase.sol"; + +interface IRageQuitEscrow is IEscrowBase { + struct RageQuitEscrowDetails { + bool isRageQuitFinalized; + bool isWithdrawalsBatchesClosed; + bool isRageQuitExtensionPeriodStarted; + uint256 unclaimedUnstETHIdsCount; + Duration rageQuitEthWithdrawalsDelay; + Duration rageQuitExtensionPeriodDuration; + Timestamp rageQuitExtensionPeriodStartedAt; + } + + function requestNextWithdrawalsBatch(uint256 batchSize) external; + + function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external; + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external; + function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external; + + function startRageQuitExtensionPeriod() external; + + function withdrawETH() external; + function withdrawETH(uint256[] calldata unstETHIds) external; + + function isRageQuitFinalized() external view returns (bool); + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds); + function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory); +} diff --git a/contracts/interfaces/ISignallingEscrow.sol b/contracts/interfaces/ISignallingEscrow.sol new file mode 100644 index 00000000..e1f0275b --- /dev/null +++ b/contracts/interfaces/ISignallingEscrow.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ETHValue} from "../types/ETHValue.sol"; +import {Duration} from "../types/Duration.sol"; +import {Timestamp} from "../types/Timestamp.sol"; +import {PercentD16} from "../types/PercentD16.sol"; +import {SharesValue} from "../types/SharesValue.sol"; +import {UnstETHRecordStatus} from "../libraries/AssetsAccounting.sol"; + +import {IEscrowBase} from "./IEscrowBase.sol"; +import {IRageQuitEscrow} from "./IRageQuitEscrow.sol"; + +interface ISignallingEscrow is IEscrowBase { + struct VetoerDetails { + uint256 unstETHIdsCount; + SharesValue stETHLockedShares; + SharesValue unstETHLockedShares; + Timestamp lastAssetsLockTimestamp; + } + + struct LockedUnstETHDetails { + UnstETHRecordStatus status; + address lockedBy; + SharesValue shares; + ETHValue claimableAmount; + } + + struct SignallingEscrowDetails { + SharesValue totalStETHLockedShares; + ETHValue totalStETHClaimedETH; + SharesValue totalUnstETHUnfinalizedShares; + ETHValue totalUnstETHFinalizedETH; + } + + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares); + function unlockStETH() external returns (uint256 unlockedStETHShares); + + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares); + function unlockWstETH() external returns (uint256 wstETHUnlocked); + + function lockUnstETH(uint256[] memory unstETHIds) external; + function unlockUnstETH(uint256[] memory unstETHIds) external; + + function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external; + + function startRageQuit( + Duration rageQuitExtensionPeriodDuration, + Duration rageQuitEthWithdrawalsDelay + ) external returns (IRageQuitEscrow); + + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; + + function getRageQuitSupport() external view returns (PercentD16); + function getMinAssetsLockDuration() external view returns (Duration); + + function getVetoerDetails(address vetoer) external view returns (VetoerDetails memory); + function getVetoerUnstETHIds(address vetoer) external view returns (uint256[] memory); + function getSignallingEscrowDetails() external view returns (SignallingEscrowDetails memory); + + function getLockedUnstETHDetails(uint256[] calldata unstETHIds) + external + view + returns (LockedUnstETHDetails[] memory); +} diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 11bb6457..c1fdb083 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -7,7 +7,9 @@ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Duration} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; -import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "../interfaces/IEscrow.sol"; +import {IEscrowBase} from "../interfaces/IEscrowBase.sol"; +import {ISignallingEscrow} from "../interfaces/ISignallingEscrow.sol"; +import {IRageQuitEscrow} from "../interfaces/IRageQuitEscrow.sol"; import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; import {IDualGovernanceConfigProvider} from "../interfaces/IDualGovernanceConfigProvider.sol"; diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index edc465d1..c0830070 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -6,7 +6,7 @@ import {Durations} from "contracts/types/Duration.sol"; import {Executor} from "contracts/Executor.sol"; import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; -import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; diff --git a/test/mocks/EscrowMock.sol b/test/mocks/EscrowMock.sol index 2c9c38e1..dad55de7 100644 --- a/test/mocks/EscrowMock.sol +++ b/test/mocks/EscrowMock.sol @@ -3,9 +3,11 @@ pragma solidity 0.8.26; import {Duration} from "contracts/types/Duration.sol"; import {PercentD16} from "contracts/types/PercentD16.sol"; -import {Timestamp} from "contracts/types/Timestamp.sol"; -import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; +import {IRageQuitEscrow} from "contracts/interfaces/IRageQuitEscrow.sol"; + import {State as EscrowState} from "contracts/libraries/EscrowState.sol"; contract EscrowMock is ISignallingEscrow, IRageQuitEscrow { @@ -25,7 +27,11 @@ contract EscrowMock is ISignallingEscrow, IRageQuitEscrow { revert("Not implemented"); } - function getVetoerState(address vetoer) external view returns (VetoerState memory) { + function getVetoerDetails(address vetoer) external view returns (VetoerDetails memory) { + revert("Not implemented"); + } + + function getVetoerUnstETHIds(address vetoer) external view returns (uint256[] memory) { revert("Not implemented"); } @@ -81,11 +87,15 @@ contract EscrowMock is ISignallingEscrow, IRageQuitEscrow { revert("Not implemented"); } - function getLockedUnstETHState(uint256 unstETHId) external view returns (LockedUnstETHState memory) { + function getLockedUnstETHDetails(uint256[] calldata unstETHIds) + external + view + returns (LockedUnstETHDetails[] memory) + { revert("Not implemented"); } - function getSignallingEscrowState() external view returns (SignallingEscrowState memory) { + function getSignallingEscrowDetails() external view returns (SignallingEscrowDetails memory) { revert("Not implemented"); } @@ -125,7 +135,7 @@ contract EscrowMock is ISignallingEscrow, IRageQuitEscrow { return __isRageQuitFinalized; } - function getRageQuitEscrowState() external view returns (RageQuitEscrowState memory) { + function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory) { revert("Not implemented"); } diff --git a/test/scenario/dg-update-tokens-rotation.t.sol b/test/scenario/dg-update-tokens-rotation.t.sol index 97f98191..a415e13c 100644 --- a/test/scenario/dg-update-tokens-rotation.t.sol +++ b/test/scenario/dg-update-tokens-rotation.t.sol @@ -96,13 +96,13 @@ contract DualGovernanceUpdateTokensRotation is ScenarioTestBlueprint { // The Rage Quit may be finished in the previous DG instance so vetoers will not lose their funds by mistake Escrow rageQuitEscrow = Escrow(payable(_dualGovernance.getRageQuitEscrow())); - while (!rageQuitEscrow.isWithdrawalsBatchesClosed()) { + while (!rageQuitEscrow.getRageQuitEscrowDetails().isWithdrawalsBatchesClosed) { rageQuitEscrow.requestNextWithdrawalsBatch(96); } _finalizeWithdrawalQueue(); - while (rageQuitEscrow.getUnclaimedUnstETHIdsCount() > 0) { + while (rageQuitEscrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { rageQuitEscrow.claimNextWithdrawalsBatch(32); } diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 737d4042..01582c83 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -6,8 +6,8 @@ import {console} from "forge-std/Test.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {PercentsD16} from "contracts/types/PercentD16.sol"; -import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; import {WithdrawalsBatchesQueue} from "contracts/libraries/WithdrawalsBatchesQueue.sol"; @@ -221,22 +221,22 @@ contract EscrowHappyPath is ScenarioTestBlueprint { _lockUnstETH(_VETOER_1, unstETHIds); - Escrow.VetoerState memory vetoerState = escrow.getVetoerState(_VETOER_1); - assertEq(vetoerState.unstETHIdsCount, 2); + Escrow.VetoerDetails memory vetoerDetails = escrow.getVetoerDetails(_VETOER_1); + assertEq(vetoerDetails.unstETHIdsCount, 2); - IEscrowBase.LockedAssetsTotals memory totals = escrow.getLockedAssetsTotals(); - assertEq(totals.unstETHFinalizedETH, 0); - assertEq(totals.unstETHUnfinalizedShares, totalSharesLocked); + ISignallingEscrow.SignallingEscrowDetails memory escrowDetails = escrow.getSignallingEscrowDetails(); + assertEq(escrowDetails.totalUnstETHFinalizedETH.toUint256(), 0); + assertEq(escrowDetails.totalUnstETHUnfinalizedShares.toUint256(), totalSharesLocked); _finalizeWithdrawalQueue(unstETHIds[0]); uint256[] memory hints = _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); escrow.markUnstETHFinalized(unstETHIds, hints); - totals = escrow.getLockedAssetsTotals(); - assertEq(totals.unstETHUnfinalizedShares, statuses[0].amountOfShares); + escrowDetails = escrow.getSignallingEscrowDetails(); + assertEq(escrowDetails.totalUnstETHUnfinalizedShares.toUint256(), statuses[0].amountOfShares); uint256 ethAmountFinalized = _lido.withdrawalQueue.getClaimableEther(unstETHIds, hints)[0]; - assertApproxEqAbs(totals.unstETHFinalizedETH, ethAmountFinalized, 1); + assertApproxEqAbs(escrowDetails.totalUnstETHFinalizedETH.toUint256(), ethAmountFinalized, 1); } function test_get_rage_quit_support() public { @@ -258,8 +258,8 @@ contract EscrowHappyPath is ScenarioTestBlueprint { uint256 totalSupply = _lido.stETH.totalSupply(); // epsilon is 2 here, because the wstETH unwrap may produce 1 wei error and stETH transfer 1 wei - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).stETHLockedShares, 2 * sharesToLock, 2); - assertEq(escrow.getVetoerState(_VETOER_1).unstETHIdsCount, 2); + assertApproxEqAbs(escrow.getVetoerDetails(_VETOER_1).stETHLockedShares.toUint256(), 2 * sharesToLock, 2); + assertEq(escrow.getVetoerDetails(_VETOER_1).unstETHIdsCount, 2); assertEq(escrow.getRageQuitSupport(), PercentsD16.fromFraction({numerator: 4 ether, denominator: totalSupply})); @@ -268,10 +268,12 @@ contract EscrowHappyPath is ScenarioTestBlueprint { _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); escrow.markUnstETHFinalized(unstETHIds, hints); - assertEq(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, sharesToLock); + assertEq(escrow.getSignallingEscrowDetails().totalUnstETHUnfinalizedShares.toUint256(), sharesToLock); uint256 ethAmountFinalized = _lido.withdrawalQueue.getClaimableEther(unstETHIds, hints)[0]; - assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHFinalizedETH, ethAmountFinalized, 1); + assertApproxEqAbs( + escrow.getSignallingEscrowDetails().totalUnstETHFinalizedETH.toUint256(), ethAmountFinalized, 1 + ); assertEq( escrow.getRageQuitSupport(), @@ -314,7 +316,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { assertEq(_lido.withdrawalQueue.balanceOf(address(escrow)), 20); - while (!escrow.isWithdrawalsBatchesClosed()) { + while (!escrow.getRageQuitEscrowDetails().isWithdrawalsBatchesClosed) { escrow.requestNextWithdrawalsBatch(96); } @@ -338,7 +340,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { unstETHIdsToClaim, 1, _lido.withdrawalQueue.getLastCheckpointIndex() ); - while (escrow.getUnclaimedUnstETHIdsCount() > 0) { + while (escrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { escrow.claimNextWithdrawalsBatch(32); } @@ -605,7 +607,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { escrow.claimUnstETH(unstETHIdsToClaim, hints); // The rage quit process can be successfully finished - while (escrow.getUnclaimedUnstETHIdsCount() > 0) { + while (escrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { escrow.claimNextWithdrawalsBatch(batchSizeLimit); } diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index 8964f61d..fc19ce76 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -65,13 +65,13 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { // request withdrawals batches Escrow rageQuitEscrow = _getRageQuitEscrow(); - while (!rageQuitEscrow.isWithdrawalsBatchesClosed()) { + while (!rageQuitEscrow.getRageQuitEscrowDetails().isWithdrawalsBatchesClosed) { rageQuitEscrow.requestNextWithdrawalsBatch(96); } _lido.finalizeWithdrawalQueue(); - while (rageQuitEscrow.getUnclaimedUnstETHIdsCount() > 0) { + while (rageQuitEscrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { rageQuitEscrow.claimNextWithdrawalsBatch(128); } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 07ee894c..dfda5d50 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -26,7 +26,7 @@ import {IWstETH} from "contracts/interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; import {ITimelock} from "contracts/interfaces/ITimelock.sol"; import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; -import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; import {UnitTest} from "test/utils/unit-test.sol"; import {StETHMock} from "test/mocks/StETHMock.sol"; diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index 1e49c5d7..b9a538f2 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -97,20 +97,6 @@ contract EscrowUnitTests is UnitTest { assertEq(_escrow.getVetoerUnstETHIds(_vetoer).length, 0); } - // --- - // getUnclaimedUnstETHIdsCount() - // --- - - function test_getUnclaimedUnstETHIdsCount_RevertOn_UnexpectedState_Signaling() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); - _escrow.getUnclaimedUnstETHIdsCount(); - } - - function test_getUnclaimedUnstETHIdsCount_RevertOn_UnexpectedState_NotInitialized() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); - _masterCopy.getUnclaimedUnstETHIdsCount(); - } - // --- // getNextWithdrawalBatch() // --- @@ -131,14 +117,14 @@ contract EscrowUnitTests is UnitTest { // isWithdrawalsBatchesClosed() // --- - function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedState_Signaling() external { + function test_getRageQuitEscrowDetails_RevertOn_UnexpectedState_Signaling() external { vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); - _escrow.isWithdrawalsBatchesClosed(); + _escrow.getRageQuitEscrowDetails(); } - function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedState_NotInitialized() external { + function test_getRageQuitEscrowDetails_RevertOn_UnexpectedState_NotInitialized() external { vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); - _masterCopy.isWithdrawalsBatchesClosed(); + _masterCopy.getRageQuitEscrowDetails(); } function vetoerLockedUnstEth(uint256[] memory amounts) internal returns (uint256[] memory unstethIds) { diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 2fed86f2..50c89901 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -3,7 +3,9 @@ pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; +import {IRageQuitEscrow} from "contracts/interfaces/IRageQuitEscrow.sol"; import {Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; diff --git a/test/unit/libraries/DualGovernanceStateTransitions.t.sol b/test/unit/libraries/DualGovernanceStateTransitions.t.sol index f04dc41e..ab6a5537 100644 --- a/test/unit/libraries/DualGovernanceStateTransitions.t.sol +++ b/test/unit/libraries/DualGovernanceStateTransitions.t.sol @@ -5,7 +5,9 @@ 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 {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; +import {IRageQuitEscrow} from "contracts/interfaces/IRageQuitEscrow.sol"; import { State, diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index bddce0cc..f6385c4c 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -12,7 +12,7 @@ import {PercentD16} from "contracts/types/PercentD16.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; import {Escrow} from "contracts/Escrow.sol"; // --- @@ -199,7 +199,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { function _unlockWstETH(address vetoer) internal { Escrow escrow = _getVetoSignallingEscrow(); uint256 wstETHBalanceBefore = _lido.wstETH.balanceOf(vetoer); - IEscrowBase.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + ISignallingEscrow.VetoerDetails memory vetoerDetailsBefore = escrow.getVetoerDetails(vetoer); vm.startPrank(vetoer); uint256 wstETHUnlocked = escrow.unlockWstETH(); @@ -207,14 +207,17 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { // 1 wei rounding issue may arise because of the wrapping stETH into wstETH before // sending funds to the user - assertApproxEqAbs(wstETHUnlocked, vetoerStateBefore.stETHLockedShares, 1); - assertApproxEqAbs(_lido.wstETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerStateBefore.stETHLockedShares, 1); + assertApproxEqAbs(wstETHUnlocked, vetoerDetailsBefore.stETHLockedShares.toUint256(), 1); + assertApproxEqAbs( + _lido.wstETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerDetailsBefore.stETHLockedShares.toUint256(), 1 + ); } function _lockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - IEscrowBase.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); - IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); + ISignallingEscrow.VetoerDetails memory vetoerDetailsBefore = escrow.getVetoerDetails(vetoer); + ISignallingEscrow.SignallingEscrowDetails memory signallingEscrowDetailsBefore = + escrow.getSignallingEscrowDetails(); uint256 unstETHTotalSharesLocked = 0; IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = @@ -233,20 +236,22 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), address(escrow)); } - IEscrowBase.VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); - assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount + unstETHIds.length); + ISignallingEscrow.VetoerDetails memory vetoerDetailsAfter = escrow.getVetoerDetails(vetoer); + assertEq(vetoerDetailsAfter.unstETHIdsCount, vetoerDetailsBefore.unstETHIdsCount + unstETHIds.length); - IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + ISignallingEscrow.SignallingEscrowDetails memory signallingEscrowDetailsAfter = + escrow.getSignallingEscrowDetails(); assertEq( - lockedAssetsTotalsAfter.unstETHUnfinalizedShares, - lockedAssetsTotalsBefore.unstETHUnfinalizedShares + unstETHTotalSharesLocked + signallingEscrowDetailsAfter.totalUnstETHUnfinalizedShares.toUint256(), + signallingEscrowDetailsBefore.totalUnstETHUnfinalizedShares.toUint256() + unstETHTotalSharesLocked ); } function _unlockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - IEscrowBase.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); - IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); + ISignallingEscrow.VetoerDetails memory vetoerDetailsBefore = escrow.getVetoerDetails(vetoer); + ISignallingEscrow.SignallingEscrowDetails memory signallingEscrowDetailsBefore = + escrow.getSignallingEscrowDetails(); uint256 unstETHTotalSharesUnlocked = 0; IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = @@ -263,14 +268,15 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), vetoer); } - IEscrowBase.VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); - assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount - unstETHIds.length); + ISignallingEscrow.VetoerDetails memory vetoerDetailsAfter = escrow.getVetoerDetails(vetoer); + assertEq(vetoerDetailsAfter.unstETHIdsCount, vetoerDetailsBefore.unstETHIdsCount - unstETHIds.length); // TODO: implement correct assert. It must consider was unstETH finalized or not - IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + ISignallingEscrow.SignallingEscrowDetails memory signallingEscrowDetailsAfter = + escrow.getSignallingEscrowDetails(); assertEq( - lockedAssetsTotalsAfter.unstETHUnfinalizedShares, - lockedAssetsTotalsBefore.unstETHUnfinalizedShares - unstETHTotalSharesUnlocked + signallingEscrowDetailsAfter.totalUnstETHUnfinalizedShares.toUint256(), + signallingEscrowDetailsBefore.totalUnstETHUnfinalizedShares.toUint256() - unstETHTotalSharesUnlocked ); } From 278dc696a6d9d1c16ce047b43bc6cadbbaa05ea1 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 5 Dec 2024 15:05:12 +0400 Subject: [PATCH 078/107] Add missing natspec to Escrow. Remove EscrowMock --- contracts/Escrow.sol | 8 +- test/mocks/EscrowMock.sol | 157 -------------------------------------- 2 files changed, 6 insertions(+), 159 deletions(-) delete mode 100644 test/mocks/EscrowMock.sol diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index c7deb5d5..14d4b034 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -83,8 +83,10 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { // 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. + /// @notice The address of the implementation used for Signalling and Rage Quit escrows deployed + /// by the DualGovernance contract. + /// @dev This address is also used to distinguish whether the call is made to the proxy or directly + /// to the implementation. IEscrowBase public immutable ESCROW_MASTER_COPY; /// @dev The address of the Dual Governance contract. @@ -143,6 +145,8 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); } + /// @notice Retrieves the current state of the Escrow. + /// @return State The current state of the Escrow. function getEscrowState() external view returns (State) { return _escrowState.state; } diff --git a/test/mocks/EscrowMock.sol b/test/mocks/EscrowMock.sol deleted file mode 100644 index dad55de7..00000000 --- a/test/mocks/EscrowMock.sol +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Duration} from "contracts/types/Duration.sol"; -import {PercentD16} from "contracts/types/PercentD16.sol"; - -import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; -import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; -import {IRageQuitEscrow} from "contracts/interfaces/IRageQuitEscrow.sol"; - -import {State as EscrowState} from "contracts/libraries/EscrowState.sol"; - -contract EscrowMock is ISignallingEscrow, IRageQuitEscrow { - IEscrowBase public immutable ESCROW_MASTER_COPY = this; - - event __RageQuitStarted(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock); - - Duration public __minAssetsLockDuration; - PercentD16 public __rageQuitSupport; - bool public __isRageQuitFinalized; - - function initialize(Duration minAssetsLockDuration) external { - __minAssetsLockDuration = minAssetsLockDuration; - } - - function getEscrowState() external view returns (EscrowState) { - revert("Not implemented"); - } - - function getVetoerDetails(address vetoer) external view returns (VetoerDetails memory) { - revert("Not implemented"); - } - - function getVetoerUnstETHIds(address vetoer) external view returns (uint256[] memory) { - revert("Not implemented"); - } - - // --- - // Signalling Escrow Methods - // --- - - function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { - revert("Not implemented"); - } - - function unlockStETH() external returns (uint256 unlockedStETHShares) { - revert("Not implemented"); - } - - function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { - revert("Not implemented"); - } - - function unlockWstETH() external returns (uint256 wstETHUnlocked) { - revert("Not implemented"); - } - - function lockUnstETH(uint256[] memory unstETHIds) external { - revert("Not implemented"); - } - - function unlockUnstETH(uint256[] memory unstETHIds) external { - revert("Not implemented"); - } - - function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external { - revert("Not implemented"); - } - - function startRageQuit( - Duration rageQuitExtraTimelock, - Duration rageQuitWithdrawalsTimelock - ) external returns (IRageQuitEscrow) { - emit __RageQuitStarted(rageQuitExtraTimelock, rageQuitWithdrawalsTimelock); - return this; - } - - function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { - __minAssetsLockDuration = newMinAssetsLockDuration; - } - - function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport) { - return __rageQuitSupport; - } - - function getMinAssetsLockDuration() external view returns (Duration) { - revert("Not implemented"); - } - - function getLockedUnstETHDetails(uint256[] calldata unstETHIds) - external - view - returns (LockedUnstETHDetails[] memory) - { - revert("Not implemented"); - } - - function getSignallingEscrowDetails() external view returns (SignallingEscrowDetails memory) { - revert("Not implemented"); - } - - // --- - // Rage Quit Escrow - // --- - - function requestNextWithdrawalsBatch(uint256 batchSize) external { - revert("Not implemented"); - } - - function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external { - revert("Not implemented"); - } - - function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { - revert("Not implemented"); - } - - function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external { - revert("Not implemented"); - } - - function startRageQuitExtensionPeriod() external { - revert("Not implemented"); - } - - function withdrawETH() external { - revert("Not implemented"); - } - - function withdrawETH(uint256[] calldata unstETHIds) external { - revert("Not implemented"); - } - - function isRageQuitFinalized() external view returns (bool) { - return __isRageQuitFinalized; - } - - function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory) { - revert("Not implemented"); - } - - function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds) { - revert("Not implemented"); - } - - // --- - // Mock methods - // --- - - function __setRageQuitSupport(PercentD16 newRageQuitSupport) external { - __rageQuitSupport = newRageQuitSupport; - } - - function __setIsRageQuitFinalized(bool newIsRageQuitFinalized) external { - __isRageQuitFinalized = newIsRageQuitFinalized; - } -} From f696d5a3d45c755219b7392599e82486f892ca65 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 5 Dec 2024 14:34:01 +0300 Subject: [PATCH 079/107] increase MIN_TRANSFERRABLE_ST_ETH_AMOUNT --- contracts/Escrow.sol | 4 +- test/mocks/WithdrawalQueueMock.sol | 17 +++- test/unit/DualGovernance.t.sol | 2 +- test/unit/Escrow.t.sol | 155 ++++++++++++++++++++++++----- 4 files changed, 148 insertions(+), 30 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 3ba3a99f..87c61c97 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -48,7 +48,7 @@ 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. - uint256 private constant _MIN_TRANSFERRABLE_ST_ETH_AMOUNT = 8 wei; + uint256 public constant MIN_TRANSFERRABLE_ST_ETH_AMOUNT = 100 wei; // --- // Sanity Check Parameters & Immutables @@ -303,7 +303,7 @@ contract Escrow is IEscrow { /// Using only `minStETHWithdrawalRequestAmount` is insufficient because it is an external variable /// that could be decreased independently. Introducing `minWithdrawableStETHAmount` provides /// an internal safeguard, enforcing a minimum threshold within the contract. - uint256 minWithdrawableStETHAmount = Math.max(_MIN_TRANSFERRABLE_ST_ETH_AMOUNT, minStETHWithdrawalRequestAmount); + uint256 minWithdrawableStETHAmount = Math.max(MIN_TRANSFERRABLE_ST_ETH_AMOUNT, minStETHWithdrawalRequestAmount); if (stETHRemaining < minWithdrawableStETHAmount) { return _batchesQueue.close(); diff --git a/test/mocks/WithdrawalQueueMock.sol b/test/mocks/WithdrawalQueueMock.sol index 4fde8a18..06cd5253 100644 --- a/test/mocks/WithdrawalQueueMock.sol +++ b/test/mocks/WithdrawalQueueMock.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.26; // import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; /*, ERC721("test", "test")*/ import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /* solhint-disable no-unused-vars,custom-errors */ contract WithdrawalQueueMock is IWithdrawalQueue { @@ -11,8 +12,11 @@ contract WithdrawalQueueMock is IWithdrawalQueue { uint256 private _minStETHWithdrawalAmount; uint256 private _maxStETHWithdrawalAmount; uint256[] private _requestWithdrawalsResult; + IERC20 private _stETH; - constructor() {} + constructor(IERC20 stETH) { + _stETH = stETH; + } function MIN_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256) { return _minStETHWithdrawalAmount; @@ -70,6 +74,17 @@ contract WithdrawalQueueMock is IWithdrawalQueue { uint256[] calldata _amounts, address _owner ) external returns (uint256[] memory requestIds) { + uint256 totalAmount = 0; + for (uint256 i = 0; i < _amounts.length; i++) { + if (_amounts[i] < _minStETHWithdrawalAmount) { + revert("Amount is less than MIN_STETH_WITHDRAWAL_AMOUNT"); + } + if (_amounts[i] > _maxStETHWithdrawalAmount) { + revert("Amount is more than MAX_STETH_WITHDRAWAL_AMOUNT"); + } + totalAmount += _amounts[i]; + } + IERC20(_stETH).transferFrom(_owner, address(this), totalAmount); return _requestWithdrawalsResult; } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 8de47811..7e7f3d44 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -43,7 +43,7 @@ contract DualGovernanceUnitTests is UnitTest { address private proposalsCanceller = makeAddr("proposalsCanceller"); StETHMock private immutable _STETH_MOCK = new StETHMock(); - IWithdrawalQueue private immutable _WITHDRAWAL_QUEUE_MOCK = new WithdrawalQueueMock(); + IWithdrawalQueue private immutable _WITHDRAWAL_QUEUE_MOCK = new WithdrawalQueueMock(_STETH_MOCK); // TODO: Replace with mocks IWstETH private immutable _WSTETH_STUB = IWstETH(makeAddr("WSTETH_STUB")); diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index 1e49c5d7..50ef9ee9 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -33,7 +33,7 @@ contract EscrowUnitTests is UnitTest { StETHMock private _stETH; IWstETH private _wstETH; - address private _withdrawalQueue; + WithdrawalQueueMock private _withdrawalQueue; Duration private _minLockAssetDuration = Durations.from(1 days); uint256 private stethAmount = 100 ether; @@ -42,11 +42,15 @@ contract EscrowUnitTests is UnitTest { _stETH = new StETHMock(); _stETH.__setShareRate(1); _wstETH = IWstETH(address(new ERC20Mock())); - _withdrawalQueue = address(new WithdrawalQueueMock()); + _withdrawalQueue = new WithdrawalQueueMock(_stETH); + _withdrawalQueue.setMaxStETHWithdrawalAmount(1_000 ether); _masterCopy = new Escrow(_stETH, _wstETH, WithdrawalQueueMock(_withdrawalQueue), IDualGovernance(_dualGovernance), 100); _escrow = Escrow(payable(Clones.clone(address(_masterCopy)))); + vm.prank(address(_escrow)); + _stETH.approve(address(_withdrawalQueue), type(uint256).max); + vm.prank(_dualGovernance); _escrow.initialize(_minLockAssetDuration); @@ -141,6 +145,125 @@ contract EscrowUnitTests is UnitTest { _masterCopy.isWithdrawalsBatchesClosed(); } + // --- + // MIN_TRANSFERRABLE_ST_ETH_AMOUNT + // --- + + function test_MIN_TRANSFERRABLE_ST_ETH_AMOUNT_gt_minWithdrawableStETHAmountWei_HappyPath() external { + uint256 amountToLock = 100; + + uint256 minWithdrawableStETHAmountWei = 99; + assertEq(_escrow.MIN_TRANSFERRABLE_ST_ETH_AMOUNT(), 100); + _withdrawalQueue.setMinStETHWithdrawalAmount(minWithdrawableStETHAmountWei); + + // Lock stETH + _stETH.mint(_vetoer, amountToLock); + vm.prank(_vetoer); + _escrow.lockStETH(amountToLock); + assertEq(_stETH.balanceOf(address(_escrow)), amountToLock); + + // Request withdrawal + vm.prank(_dualGovernance); + _escrow.startRageQuit(Durations.ZERO, Durations.ZERO); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 0); + assertEq(_escrow.isWithdrawalsBatchesClosed(), false); + + uint256[] memory unstEthIds = new uint256[](1); + unstEthIds[0] = 1; + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + _escrow.requestNextWithdrawalsBatch(100); + + assertEq(_stETH.balanceOf(address(_escrow)), 0); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 1); + assertEq(_escrow.isWithdrawalsBatchesClosed(), true); + } + + function test_MIN_TRANSFERRABLE_ST_ETH_AMOUNT_gt_minWithdrawableStETHAmountWei_HappyPath_closes_queue() external { + uint256 amountToLock = 99; + + uint256 minWithdrawableStETHAmountWei = 99; + assertEq(_escrow.MIN_TRANSFERRABLE_ST_ETH_AMOUNT(), 100); + _withdrawalQueue.setMinStETHWithdrawalAmount(minWithdrawableStETHAmountWei); + + // Lock stETH + _stETH.mint(_vetoer, amountToLock); + vm.prank(_vetoer); + _escrow.lockStETH(amountToLock); + assertEq(_stETH.balanceOf(address(_escrow)), amountToLock); + + // Request withdrawal + vm.prank(_dualGovernance); + _escrow.startRageQuit(Durations.ZERO, Durations.ZERO); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 0); + assertEq(_escrow.isWithdrawalsBatchesClosed(), false); + + uint256[] memory unstEthIds = new uint256[](0); + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + _escrow.requestNextWithdrawalsBatch(100); + + assertEq(_stETH.balanceOf(address(_escrow)), 99); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 0); + assertEq(_escrow.isWithdrawalsBatchesClosed(), true); + } + + function test_MIN_TRANSFERRABLE_ST_ETH_AMOUNT_lt_minWithdrawableStETHAmountWei_HappyPath() external { + uint256 amountToLock = 101; + + uint256 minWithdrawableStETHAmountWei = 101; + assertEq(_escrow.MIN_TRANSFERRABLE_ST_ETH_AMOUNT(), 100); + _withdrawalQueue.setMinStETHWithdrawalAmount(minWithdrawableStETHAmountWei); + + // Lock stETH + _stETH.mint(_vetoer, amountToLock); + vm.prank(_vetoer); + _escrow.lockStETH(amountToLock); + assertEq(_stETH.balanceOf(address(_escrow)), amountToLock); + + // Request withdrawal + vm.prank(_dualGovernance); + _escrow.startRageQuit(Durations.ZERO, Durations.ZERO); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 0); + assertEq(_escrow.isWithdrawalsBatchesClosed(), false); + + uint256[] memory unstEthIds = new uint256[](1); + unstEthIds[0] = 1; + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + _escrow.requestNextWithdrawalsBatch(100); + + assertEq(_stETH.balanceOf(address(_escrow)), 0); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 1); + assertEq(_escrow.isWithdrawalsBatchesClosed(), true); + } + + function test_MIN_TRANSFERRABLE_ST_ETH_AMOUNT_lt_minWithdrawableStETHAmountWei_HappyPath_closes_queue() external { + uint256 amountToLock = 100; + + uint256 minWithdrawableStETHAmountWei = 101; + assertEq(_escrow.MIN_TRANSFERRABLE_ST_ETH_AMOUNT(), 100); + _withdrawalQueue.setMinStETHWithdrawalAmount(minWithdrawableStETHAmountWei); + + // Lock stETH + _stETH.mint(_vetoer, amountToLock); + vm.prank(_vetoer); + _escrow.lockStETH(amountToLock); + assertEq(_stETH.balanceOf(address(_escrow)), amountToLock); + + // Request withdrawal + vm.prank(_dualGovernance); + _escrow.startRageQuit(Durations.ZERO, Durations.ZERO); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 0); + assertEq(_escrow.isWithdrawalsBatchesClosed(), false); + + uint256[] memory unstEthIds = new uint256[](1); + unstEthIds[0] = 1; + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + _escrow.requestNextWithdrawalsBatch(100); + + assertEq(_stETH.balanceOf(address(_escrow)), 100); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 0); + assertEq(_escrow.isWithdrawalsBatchesClosed(), true); + } + function vetoerLockedUnstEth(uint256[] memory amounts) internal returns (uint256[] memory unstethIds) { unstethIds = new uint256[](amounts.length); IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = @@ -153,36 +276,16 @@ contract EscrowUnitTests is UnitTest { } vm.mockCall( - _withdrawalQueue, + address(_withdrawalQueue), abi.encodeWithSelector(IWithdrawalQueue.getWithdrawalStatus.selector, unstethIds), abi.encode(statuses) ); - vm.mockCall(_withdrawalQueue, abi.encodeWithSelector(IWithdrawalQueue.transferFrom.selector), abi.encode(true)); + vm.mockCall( + address(_withdrawalQueue), abi.encodeWithSelector(IWithdrawalQueue.transferFrom.selector), abi.encode(true) + ); vm.startPrank(_vetoer); _escrow.lockUnstETH(unstethIds); vm.stopPrank(); } - - function createEscrow(uint256 size) internal returns (Escrow) { - return - new Escrow(_stETH, _wstETH, WithdrawalQueueMock(_withdrawalQueue), IDualGovernance(_dualGovernance), size); - } - - function createEscrowProxy(uint256 minWithdrawalsBatchSize) internal returns (Escrow) { - Escrow masterCopy = createEscrow(minWithdrawalsBatchSize); - return Escrow(payable(Clones.clone(address(masterCopy)))); - } - - function createInitializedEscrowProxy( - uint256 minWithdrawalsBatchSize, - Duration minAssetsLockDuration - ) internal returns (Escrow) { - Escrow instance = createEscrowProxy(minWithdrawalsBatchSize); - - vm.startPrank(_dualGovernance); - instance.initialize(minAssetsLockDuration); - vm.stopPrank(); - return instance; - } } From 88a133fac3bd776bc525453b53ca7c52c901f551 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Thu, 5 Dec 2024 14:35:33 +0300 Subject: [PATCH 080/107] test getter --- test/unit/Escrow.t.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index 50ef9ee9..0f1a17ce 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -149,6 +149,10 @@ contract EscrowUnitTests is UnitTest { // MIN_TRANSFERRABLE_ST_ETH_AMOUNT // --- + function test_MIN_TRANSFERRABLE_ST_ETH_AMOUNT() external { + assertEq(_escrow.MIN_TRANSFERRABLE_ST_ETH_AMOUNT(), 100); + } + function test_MIN_TRANSFERRABLE_ST_ETH_AMOUNT_gt_minWithdrawableStETHAmountWei_HappyPath() external { uint256 amountToLock = 100; From 72e8bdfe5833cdc5dfeaeee1a80d4dd495d9bd3c Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Thu, 5 Dec 2024 17:20:49 +0400 Subject: [PATCH 081/107] Update graph of DG timelock duration --- docs/mechanism.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/mechanism.md b/docs/mechanism.md index da571f19..2765edb5 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -183,7 +183,8 @@ L(R) = L_{min} + \frac{(R - R_1)} {R_2 - R_1} (L_{max} - L_{min}) where $R_1$ is `FirstSealRageQuitSupport`, $R_2$ is `SecondSealRageQuitSupport`, $L_{min}$ is `DynamicTimelockMinDuration`, $L_{max}$ is `DynamicTimelockMaxDuration`. The dependence of the dynamic timelock on the rage quit support $R$ can be illustrated by the following graph: -![image](https://github.com/lidofinance/dual-governance/assets/1699593/b98dd9f1-1e55-4b5d-8ce1-56539f4cc3f8) +![image](https://github.com/user-attachments/assets/15cb6cdb-68a6-41ce-8d47-c34da19b84f1) + When the current rage quit support changes due to stakers locking or unlocking tokens into/out of the signalling escrow or the total stETH supply changing, the dynamic timelock duration is re-evaluated. From dbc39d2081886b4efe578c2042090674c3e122f4 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 00:53:29 +0400 Subject: [PATCH 082/107] Escrow.startRageQuit() without returning value --- contracts/Escrow.sol | 6 +----- contracts/interfaces/ISignallingEscrow.sol | 5 +---- contracts/libraries/DualGovernanceStateMachine.sol | 3 ++- test/unit/libraries/DualGovernanceStateMachine.t.sol | 9 --------- 4 files changed, 4 insertions(+), 19 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 14d4b034..776352b7 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -294,14 +294,10 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { /// 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 returns (IRageQuitEscrow rageQuitEscrow) { + function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitEthWithdrawalsDelay) external { _checkCallerIsDualGovernance(); _escrowState.startRageQuit(rageQuitExtensionPeriodDuration, rageQuitEthWithdrawalsDelay); _batchesQueue.open(WITHDRAWAL_QUEUE.getLastRequestId()); - rageQuitEscrow = IRageQuitEscrow(this); } // --- diff --git a/contracts/interfaces/ISignallingEscrow.sol b/contracts/interfaces/ISignallingEscrow.sol index e1f0275b..5829029a 100644 --- a/contracts/interfaces/ISignallingEscrow.sol +++ b/contracts/interfaces/ISignallingEscrow.sol @@ -44,10 +44,7 @@ interface ISignallingEscrow is IEscrowBase { function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external; - function startRageQuit( - Duration rageQuitExtensionPeriodDuration, - Duration rageQuitEthWithdrawalsDelay - ) external returns (IRageQuitEscrow); + function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitEthWithdrawalsDelay) external; function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index c1fdb083..c837a128 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -174,9 +174,10 @@ library DualGovernanceStateMachine { uint256 newRageQuitRound = Math.min(currentRageQuitRound + 1, MAX_RAGE_QUIT_ROUND); self.rageQuitRound = uint8(newRageQuitRound); - self.rageQuitEscrow = signallingEscrow.startRageQuit( + signallingEscrow.startRageQuit( config.rageQuitExtensionPeriodDuration, config.calcRageQuitWithdrawalsDelay(newRageQuitRound) ); + self.rageQuitEscrow = IRageQuitEscrow(address(signallingEscrow)); _deployNewSignallingEscrow(self, signallingEscrow.ESCROW_MASTER_COPY(), config.minAssetsLockDuration); } diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 50c89901..fb3414fb 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -52,7 +52,6 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { _mockRageQuitFinalized(false); _mockRageQuitSupport(PercentsD16.from(0)); _mockEscrowMasterCopy(); - _mockStartRageQuit(); } function test_initialize_RevertOn_ReInitialization() external { @@ -460,14 +459,6 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { ); } - function _mockStartRageQuit() internal { - vm.mockCall( - _ESCROW_MASTER_COPY_MOCK, - abi.encodeWithSelector(ISignallingEscrow.startRageQuit.selector), - abi.encode(_stateMachine.signallingEscrow) - ); - } - function _mockRageQuitSupport(PercentD16 rageQuitSupport) internal { vm.mockCall( _ESCROW_MASTER_COPY_MOCK, From 0bd250f387fe4ae3ae56192ef80966b595f52def Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 01:11:48 +0400 Subject: [PATCH 083/107] Use prefix new for arguments in the setters --- contracts/DualGovernance.sol | 30 +++++++++---------- contracts/EmergencyProtectedTimelock.sol | 30 +++++++++---------- contracts/committees/HashConsensus.sol | 12 ++++---- contracts/interfaces/IDualGovernance.sol | 4 +-- .../IEmergencyProtectedTimelock.sol | 10 +++---- contracts/interfaces/ITiebreaker.sol | 4 +-- contracts/libraries/Proposers.sol | 14 ++++----- docs/specification.md | 2 +- 8 files changed, 53 insertions(+), 53 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 19d4426d..f5ace612 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -376,10 +376,10 @@ contract DualGovernance is IDualGovernance { /// @dev Ensures that at least one proposer remains assigned to the `adminExecutor` following the update. /// Reverts if updating the proposer’s executor would leave the `adminExecutor` without any associated proposer. /// @param proposerAccount The address of the proposer whose executor is being updated. - /// @param executor The new executor address to assign to the proposer. - function setProposerExecutor(address proposerAccount, address executor) external { + /// @param newExecutor The new executor address to assign to the proposer. + function setProposerExecutor(address proposerAccount, address newExecutor) external { _checkCallerIsAdminExecutor(); - _proposers.setProposerExecutor(proposerAccount, executor); + _proposers.setProposerExecutor(proposerAccount, newExecutor); /// @dev after update of the proposer, check that admin executor still belongs to some proposer _proposers.checkRegisteredExecutor(TIMELOCK.getAdminExecutor()); @@ -450,21 +450,21 @@ contract DualGovernance is IDualGovernance { } /// @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 { + /// @param newTiebreakerCommittee The address of the new tiebreaker committee. + function setTiebreakerCommittee(address newTiebreakerCommittee) external { _checkCallerIsAdminExecutor(); - _tiebreaker.setTiebreakerCommittee(tiebreakerCommittee); + _tiebreaker.setTiebreakerCommittee(newTiebreakerCommittee); } /// @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 { + /// @param newTiebreakerActivationTimeout The new duration for the tiebreaker activation timeout. + function setTiebreakerActivationTimeout(Duration newTiebreakerActivationTimeout) external { _checkCallerIsAdminExecutor(); _tiebreaker.setTiebreakerActivationTimeout( - MIN_TIEBREAKER_ACTIVATION_TIMEOUT, tiebreakerActivationTimeout, MAX_TIEBREAKER_ACTIVATION_TIMEOUT + MIN_TIEBREAKER_ACTIVATION_TIMEOUT, newTiebreakerActivationTimeout, MAX_TIEBREAKER_ACTIVATION_TIMEOUT ); } @@ -516,17 +516,17 @@ contract DualGovernance is IDualGovernance { } /// @notice Sets the address of the reseal committee. - /// @param resealCommittee The address of the new reseal committee. - function setResealCommittee(address resealCommittee) external { + /// @param newResealCommittee The address of the new reseal committee. + function setResealCommittee(address newResealCommittee) external { _checkCallerIsAdminExecutor(); - _resealer.setResealCommittee(resealCommittee); + _resealer.setResealCommittee(newResealCommittee); } /// @notice Sets the address of the Reseal Manager. - /// @param resealManager The address of the new Reseal Manager. - function setResealManager(address resealManager) external { + /// @param newResealManager The address of the new Reseal Manager. + function setResealManager(address newResealManager) external { _checkCallerIsAdminExecutor(); - _resealer.setResealManager(resealManager); + _resealer.setResealManager(newResealManager); } /// @notice Gets the address of the Reseal Manager. diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 55e4b69b..331f4502 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -180,40 +180,40 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { // --- /// @notice Sets the emergency activation committee address. - /// @param emergencyActivationCommittee The address of the emergency activation committee. - function setEmergencyProtectionActivationCommittee(address emergencyActivationCommittee) external { + /// @param newEmergencyActivationCommittee The address of the emergency activation committee. + function setEmergencyProtectionActivationCommittee(address newEmergencyActivationCommittee) external { _timelockState.checkCallerIsAdminExecutor(); - _emergencyProtection.setEmergencyActivationCommittee(emergencyActivationCommittee); + _emergencyProtection.setEmergencyActivationCommittee(newEmergencyActivationCommittee); } /// @notice Sets the emergency execution committee address. - /// @param emergencyExecutionCommittee The address of the emergency execution committee. - function setEmergencyProtectionExecutionCommittee(address emergencyExecutionCommittee) external { + /// @param newEmergencyExecutionCommittee The address of the emergency execution committee. + function setEmergencyProtectionExecutionCommittee(address newEmergencyExecutionCommittee) external { _timelockState.checkCallerIsAdminExecutor(); - _emergencyProtection.setEmergencyExecutionCommittee(emergencyExecutionCommittee); + _emergencyProtection.setEmergencyExecutionCommittee(newEmergencyExecutionCommittee); } /// @notice Sets the emergency protection end date. - /// @param emergencyProtectionEndDate The timestamp of the emergency protection end date. - function setEmergencyProtectionEndDate(Timestamp emergencyProtectionEndDate) external { + /// @param newEmergencyProtectionEndDate The timestamp of the emergency protection end date. + function setEmergencyProtectionEndDate(Timestamp newEmergencyProtectionEndDate) external { _timelockState.checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyProtectionEndDate( - emergencyProtectionEndDate, MAX_EMERGENCY_PROTECTION_DURATION + newEmergencyProtectionEndDate, MAX_EMERGENCY_PROTECTION_DURATION ); } /// @notice Sets the emergency mode duration. - /// @param emergencyModeDuration The duration of the emergency mode. - function setEmergencyModeDuration(Duration emergencyModeDuration) external { + /// @param newEmergencyModeDuration The duration of the emergency mode. + function setEmergencyModeDuration(Duration newEmergencyModeDuration) external { _timelockState.checkCallerIsAdminExecutor(); - _emergencyProtection.setEmergencyModeDuration(emergencyModeDuration, MAX_EMERGENCY_MODE_DURATION); + _emergencyProtection.setEmergencyModeDuration(newEmergencyModeDuration, MAX_EMERGENCY_MODE_DURATION); } /// @notice Sets the emergency governance address. - /// @param emergencyGovernance The address of the emergency governance. - function setEmergencyGovernance(address emergencyGovernance) external { + /// @param newEmergencyGovernance The address of the emergency governance. + function setEmergencyGovernance(address newEmergencyGovernance) external { _timelockState.checkCallerIsAdminExecutor(); - _emergencyProtection.setEmergencyGovernance(emergencyGovernance); + _emergencyProtection.setEmergencyGovernance(newEmergencyGovernance); } /// @notice Activates the emergency mode. diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index 0e825f7d..2643c4f6 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -104,14 +104,14 @@ abstract contract HashConsensus is Ownable { /// @notice Sets the timelock duration /// @dev Only callable by the owner - /// @param timelock The new timelock duration in seconds - function setTimelockDuration(Duration timelock) external { + /// @param newTimelock The new timelock duration in seconds + function setTimelockDuration(Duration newTimelock) external { _checkOwner(); - if (timelock == _timelockDuration) { - revert InvalidTimelockDuration(timelock); + if (newTimelock == _timelockDuration) { + revert InvalidTimelockDuration(newTimelock); } - _timelockDuration = timelock; - emit TimelockDurationSet(timelock); + _timelockDuration = newTimelock; + emit TimelockDurationSet(newTimelock); } /// @notice Gets the quorum value diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index db7596b8..1c0989cf 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -47,8 +47,8 @@ interface IDualGovernance is IGovernance, ITiebreaker { function isRegisteredExecutor(address account) external view returns (bool); function resealSealable(address sealable) external; - function setResealCommittee(address resealCommittee) external; - function setResealManager(address resealManager) external; + function setResealCommittee(address newResealCommittee) external; + function setResealManager(address newResealManager) external; function getResealManager() external view returns (IResealManager); function getResealCommittee() external view returns (address); } diff --git a/contracts/interfaces/IEmergencyProtectedTimelock.sol b/contracts/interfaces/IEmergencyProtectedTimelock.sol index fa8ab86e..6b0b47fa 100644 --- a/contracts/interfaces/IEmergencyProtectedTimelock.sol +++ b/contracts/interfaces/IEmergencyProtectedTimelock.sol @@ -17,11 +17,11 @@ interface IEmergencyProtectedTimelock is ITimelock { function MAX_EMERGENCY_MODE_DURATION() external view returns (Duration); function MAX_EMERGENCY_PROTECTION_DURATION() external view returns (Duration); - function setEmergencyProtectionActivationCommittee(address emergencyActivationCommittee) external; - function setEmergencyProtectionExecutionCommittee(address emergencyExecutionCommittee) external; - function setEmergencyProtectionEndDate(Timestamp emergencyProtectionEndDate) external; - function setEmergencyModeDuration(Duration emergencyModeDuration) external; - function setEmergencyGovernance(address emergencyGovernance) external; + function setEmergencyProtectionActivationCommittee(address newEmergencyActivationCommittee) external; + function setEmergencyProtectionExecutionCommittee(address newEmergencyExecutionCommittee) external; + function setEmergencyProtectionEndDate(Timestamp newEmergencyProtectionEndDate) external; + function setEmergencyModeDuration(Duration newEmergencyModeDuration) external; + function setEmergencyGovernance(address newEmergencyGovernance) external; function activateEmergencyMode() external; function emergencyExecute(uint256 proposalId) external; diff --git a/contracts/interfaces/ITiebreaker.sol b/contracts/interfaces/ITiebreaker.sol index 6375fdf7..9d4e89fa 100644 --- a/contracts/interfaces/ITiebreaker.sol +++ b/contracts/interfaces/ITiebreaker.sol @@ -13,8 +13,8 @@ interface ITiebreaker { function addTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external; function removeTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external; - function setTiebreakerCommittee(address tiebreakerCommittee) external; - function setTiebreakerActivationTimeout(Duration tiebreakerActivationTimeout) external; + function setTiebreakerCommittee(address newTiebreakerCommittee) external; + function setTiebreakerActivationTimeout(Duration newTiebreakerActivationTimeout) external; function tiebreakerScheduleProposal(uint256 proposalId) external; function getTiebreakerDetails() external view returns (TiebreakerDetails memory tiebreakerState); function tiebreakerResumeSealable(address sealable) external; diff --git a/contracts/libraries/Proposers.sol b/contracts/libraries/Proposers.sol index 5bbb471c..058d7b6f 100644 --- a/contracts/libraries/Proposers.sol +++ b/contracts/libraries/Proposers.sol @@ -89,21 +89,21 @@ library Proposers { /// @notice Updates the executor for a registered proposer. /// @param self The context storage of the Proposers library. /// @param proposerAccount The address of the proposer to update. - /// @param executor The new executor address to assign to the proposer. - function setProposerExecutor(Context storage self, address proposerAccount, address executor) internal { + /// @param newExecutor The new executor address to assign to the proposer. + function setProposerExecutor(Context storage self, address proposerAccount, address newExecutor) internal { ExecutorData memory executorData = self.executors[proposerAccount]; _checkRegisteredProposer(proposerAccount, executorData); - if (executor == address(0) || executorData.executor == executor) { - revert InvalidExecutor(executor); + if (newExecutor == address(0) || executorData.executor == newExecutor) { + revert InvalidExecutor(newExecutor); } - self.executors[proposerAccount].executor = executor; + self.executors[proposerAccount].executor = newExecutor; - self.executorRefsCounts[executor] += 1; + self.executorRefsCounts[newExecutor] += 1; self.executorRefsCounts[executorData.executor] -= 1; - emit ProposerExecutorSet(proposerAccount, executor); + emit ProposerExecutorSet(proposerAccount, newExecutor); } /// @notice Unregisters a proposer, removing its association with an executor. diff --git a/docs/specification.md b/docs/specification.md index 4bd812b9..d62816f2 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -1139,7 +1139,7 @@ Returns if an address is a member. ### Function: HashConsensus.setTimelockDuration ```solidity -function setTimelockDuration(uint256 timelock) +function setTimelockDuration(uint256 newTimelock) ``` Sets the timelock duration. From e8a2688db4eacbb6850526e5615218bf64e16369 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 04:38:44 +0400 Subject: [PATCH 084/107] Refactor Executor event --- contracts/Executor.sol | 6 +++--- test/unit/DualGovernance.t.sol | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/Executor.sol b/contracts/Executor.sol index 53e64014..f2b2ec25 100644 --- a/contracts/Executor.sol +++ b/contracts/Executor.sol @@ -14,7 +14,7 @@ contract Executor is IExternalExecutor, Ownable { // Events // --- - event Execute(address indexed sender, address indexed target, uint256 ethValue, bytes data); + event Executed(address indexed target, uint256 ethValue, bytes data, bytes result); // --- // Constructor @@ -32,8 +32,8 @@ contract Executor is IExternalExecutor, Ownable { /// @param payload The calldata for the function call. function execute(address target, uint256 value, bytes calldata payload) external payable { _checkOwner(); - Address.functionCallWithValue(target, payload, value); - emit Execute(msg.sender, target, value, payload); + bytes memory result = Address.functionCallWithValue(target, payload, value); + emit Executed(target, value, payload, result); } /// @notice Allows the contract to receive ether. diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index add00fc9..ed35a999 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -1073,11 +1073,11 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernanceStateMachine.ConfigProviderSet(IDualGovernanceConfigProvider(address(newConfigProvider))); vm.expectEmit(); - emit Executor.Execute( - address(this), + emit Executor.Executed( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.setConfigProvider.selector, address(newConfigProvider)) + abi.encodeWithSelector(DualGovernance.setConfigProvider.selector, address(newConfigProvider)), + new bytes(0) ); vm.expectCall( From 80fa52e61eb5efb7c88542d65c208d0ae75d1216 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 06:26:25 +0400 Subject: [PATCH 085/107] Add unit tests for Executor contract --- contracts/Executor.sol | 11 ++- test/unit/Executor.t.sol | 153 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 test/unit/Executor.t.sol diff --git a/contracts/Executor.sol b/contracts/Executor.sol index f2b2ec25..dd649fc5 100644 --- a/contracts/Executor.sol +++ b/contracts/Executor.sol @@ -14,7 +14,8 @@ contract Executor is IExternalExecutor, Ownable { // Events // --- - event Executed(address indexed target, uint256 ethValue, bytes data, bytes result); + event ETHReceived(address sender, uint256 value); + event Executed(address indexed target, uint256 ethValue, bytes data, bytes returndata); // --- // Constructor @@ -32,10 +33,12 @@ contract Executor is IExternalExecutor, Ownable { /// @param payload The calldata for the function call. function execute(address target, uint256 value, bytes calldata payload) external payable { _checkOwner(); - bytes memory result = Address.functionCallWithValue(target, payload, value); - emit Executed(target, value, payload, result); + bytes memory returndata = Address.functionCallWithValue(target, payload, value); + emit Executed(target, value, payload, returndata); } /// @notice Allows the contract to receive ether. - receive() external payable {} + receive() external payable { + emit ETHReceived(msg.sender, msg.value); + } } diff --git a/test/unit/Executor.t.sol b/test/unit/Executor.t.sol new file mode 100644 index 00000000..086f7515 --- /dev/null +++ b/test/unit/Executor.t.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {Executor} from "contracts/Executor.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; + +contract ExecutorTarget { + event NonPayableMethodCalled(address caller); + event PayableMethodCalled(address caller, uint256 value); + + function nonPayableMethod() external { + emit NonPayableMethodCalled(msg.sender); + } + + function payableMethod() external payable { + emit PayableMethodCalled(msg.sender, msg.value); + } +} + +contract ExecutorUnitTests is UnitTest { + address internal _owner = makeAddr("OWNER"); + + Executor internal _executor; + ExecutorTarget internal _target; + + function setUp() external { + _target = new ExecutorTarget(); + _executor = new Executor(_owner); + } + + function test_constructor_HappyPath() external { + assertEq(_executor.owner(), _owner); + } + + function test_constructor_RevertOn_ZeroOwnerAddress() external { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableInvalidOwner.selector, address(0))); + new Executor(address(0)); + } + + // --- + // execute() + // --- + + function test_execute_HappyPath_NonPayableMethod_ZeroValue() external { + vm.expectEmit(); + emit ExecutorTarget.NonPayableMethodCalled(address(_executor)); + + vm.expectEmit(); + emit Executor.Executed(address(_target), 0, abi.encodeCall(_target.nonPayableMethod, ()), new bytes(0)); + + vm.prank(_owner); + _executor.execute({target: address(_target), value: 0, payload: abi.encodeCall(_target.nonPayableMethod, ())}); + } + + function test_execute_HappyPath_PayableMethod_ZeroValue() external { + vm.expectEmit(); + emit ExecutorTarget.PayableMethodCalled(address(_executor), 0); + + vm.expectEmit(); + emit Executor.Executed(address(_target), 0, abi.encodeCall(_target.payableMethod, ()), new bytes(0)); + + vm.prank(_owner); + _executor.execute({target: address(_target), value: 0, payload: abi.encodeCall(_target.payableMethod, ())}); + } + + function test_execute_HappyPath_PayableMethod_NonZeroValue() external { + uint256 valueAmount = 1 ether; + + vm.deal(address(_executor), valueAmount); + + assertEq(address(_target).balance, 0); + assertEq(address(_executor).balance, 1 ether); + + vm.expectEmit(); + emit ExecutorTarget.PayableMethodCalled(address(_executor), valueAmount); + + vm.expectEmit(); + emit Executor.Executed(address(_target), valueAmount, abi.encodeCall(_target.payableMethod, ()), new bytes(0)); + + vm.prank(_owner); + _executor.execute({ + target: address(_target), + value: valueAmount, + payload: abi.encodeCall(_target.payableMethod, ()) + }); + + assertEq(address(_target).balance, valueAmount); + assertEq(address(_executor).balance, 0); + } + + function test_execute_RevertOn_NonPayableMethod_NonZeroValue() external { + uint256 callValue = 1 ether; + + vm.deal(address(_executor), callValue); + assertEq(address(_executor).balance, callValue); + + vm.prank(_owner); + vm.expectRevert(Address.FailedInnerCall.selector); + _executor.execute({ + target: address(_target), + value: callValue, + payload: abi.encodeCall(_target.nonPayableMethod, ()) + }); + } + // --- + // receive() + // --- + + function test_receive_HappyPath() external { + uint256 sendValue = 1 ether; + + vm.deal(address(this), sendValue); + + assertEq(address(this).balance, sendValue); + assertEq(address(_executor).balance, 0); + + Address.sendValue(payable(address(_executor)), sendValue); + + assertEq(address(this).balance, 0); + assertEq(address(_executor).balance, sendValue); + } + + function test_receive_HappyPath_UsingSend() external { + uint256 sendValue = 1 ether; + + vm.deal(address(this), sendValue); + + assertEq(address(this).balance, sendValue); + assertEq(address(_executor).balance, 0); + + bool success = payable(address(_executor)).send(sendValue); + + assertTrue(success); + assertEq(address(this).balance, 0); + assertEq(address(_executor).balance, sendValue); + } + + // --- + // Custom call + // --- + + function test_RevertOnInvalidMethodCall() external { + vm.prank(_owner); + (bool success, bytes memory returndata) = + address(_executor).call{value: 1 ether}(abi.encodeWithSelector(bytes4(0xdeadbeaf), 42)); + + assertFalse(success); + assertEq(returndata, new bytes(0)); + } +} From 784dfd51f629512f9f13a5508a0f17566bad623b Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 06:28:33 +0400 Subject: [PATCH 086/107] Add separate ProposalReported event --- contracts/DualGovernance.sol | 4 +- contracts/EmergencyProtectedTimelock.sol | 11 +--- contracts/TimelockedGovernance.sol | 3 +- contracts/interfaces/IGovernance.sol | 2 + contracts/interfaces/ITimelock.sol | 7 +-- contracts/libraries/ExecutableProposals.sol | 12 +--- test/mocks/TimelockMock.sol | 7 +-- test/unit/DualGovernance.t.sol | 20 ++++--- test/unit/EmergencyProtectedTimelock.t.sol | 20 +++---- test/unit/TimelockedGovernance.t.sol | 9 ++- test/unit/libraries/ExecutableProposals.t.sol | 55 +++++++++---------- 11 files changed, 70 insertions(+), 80 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 19d4426d..26123b27 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -188,7 +188,9 @@ contract DualGovernance is IDualGovernance { revert ProposalSubmissionBlocked(); } Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); - proposalId = TIMELOCK.submit(proposer.account, proposer.executor, calls, metadata); + proposalId = TIMELOCK.submit(proposer.executor, calls); + + emit ProposalReported(proposer.account, proposalId, metadata); } /// @notice Schedules a previously submitted proposal for execution in the Dual Governance system. diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 55e4b69b..e3ebdab2 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -102,19 +102,12 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { // --- /// @notice Submits a new proposal to execute a series of calls through an executor. - /// @param proposer The address of the proposer submitting the proposal. /// @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 proposer, - address executor, - ExternalCall[] calldata calls, - string calldata metadata - ) external returns (uint256 newProposalId) { + function submit(address executor, ExternalCall[] calldata calls) external returns (uint256 newProposalId) { _timelockState.checkCallerIsGovernance(); - newProposalId = _proposals.submit(proposer, executor, calls, metadata); + newProposalId = _proposals.submit(executor, calls); } /// @notice Schedules a proposal for execution after a specified delay. diff --git a/contracts/TimelockedGovernance.sol b/contracts/TimelockedGovernance.sol index 25f08fd3..5cc4fb44 100644 --- a/contracts/TimelockedGovernance.sol +++ b/contracts/TimelockedGovernance.sol @@ -51,7 +51,8 @@ contract TimelockedGovernance is IGovernance { string calldata metadata ) external returns (uint256 proposalId) { _checkCallerIsGovernance(); - return TIMELOCK.submit(msg.sender, TIMELOCK.getAdminExecutor(), calls, metadata); + proposalId = TIMELOCK.submit(TIMELOCK.getAdminExecutor(), calls); + emit ProposalReported(msg.sender, proposalId, metadata); } /// @notice Schedules a submitted proposal. diff --git a/contracts/interfaces/IGovernance.sol b/contracts/interfaces/IGovernance.sol index 51652b40..fd3421a1 100644 --- a/contracts/interfaces/IGovernance.sol +++ b/contracts/interfaces/IGovernance.sol @@ -6,6 +6,8 @@ import {ITimelock} from "./ITimelock.sol"; import {ExternalCall} from "../libraries/ExternalCalls.sol"; interface IGovernance { + event ProposalReported(address indexed proposerAccount, uint256 indexed proposalId, string metadata); + function TIMELOCK() external view returns (ITimelock); function submitProposal( ExternalCall[] calldata calls, diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index f94fe3e6..234a000d 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -16,12 +16,7 @@ interface ITimelock { ProposalStatus status; } - function submit( - address proposer, - address executor, - ExternalCall[] calldata calls, - string calldata metadata - ) external returns (uint256 newProposalId); + function submit(address executor, ExternalCall[] calldata calls) external returns (uint256 newProposalId); function schedule(uint256 proposalId) external; function execute(uint256 proposalId) external; function cancelAllNonExecutedProposals() external; diff --git a/contracts/libraries/ExecutableProposals.sol b/contracts/libraries/ExecutableProposals.sol index 070af315..9405bae0 100644 --- a/contracts/libraries/ExecutableProposals.sol +++ b/contracts/libraries/ExecutableProposals.sol @@ -89,9 +89,7 @@ library ExecutableProposals { // Events // --- - event ProposalSubmitted( - uint256 indexed id, address indexed proposer, address indexed executor, ExternalCall[] calls, string metadata - ); + event ProposalSubmitted(uint256 indexed id, address indexed executor, ExternalCall[] calls); event ProposalScheduled(uint256 indexed id); event ProposalExecuted(uint256 indexed id); event ProposalsCancelledTill(uint256 proposalId); @@ -102,17 +100,13 @@ library ExecutableProposals { /// @notice Submits a new proposal with the specified executor and external calls. /// @param self The context of the Executable Proposal library. - /// @param proposer The address of the proposer submitting the proposal. /// @param executor The address authorized to execute the proposal. /// @param calls The list of external calls to include in the proposal. - /// @param metadata Metadata describing the proposal. /// @return newProposalId The id of the newly submitted proposal. function submit( Context storage self, - address proposer, address executor, - ExternalCall[] memory calls, - string memory metadata + ExternalCall[] memory calls ) internal returns (uint256 newProposalId) { if (calls.length == 0) { revert EmptyCalls(); @@ -131,7 +125,7 @@ library ExecutableProposals { newProposal.calls.push(calls[i]); } - emit ProposalSubmitted(newProposalId, proposer, executor, calls, metadata); + emit ProposalSubmitted(newProposalId, executor, calls); } /// @notice Marks a previously submitted proposal as scheduled for execution if the required delay period diff --git a/test/mocks/TimelockMock.sol b/test/mocks/TimelockMock.sol index 7ae66eab..a818ab26 100644 --- a/test/mocks/TimelockMock.sol +++ b/test/mocks/TimelockMock.sol @@ -26,12 +26,7 @@ contract TimelockMock is ITimelock { address internal governance; - function submit( - address, - address, - ExternalCall[] calldata, - string calldata - ) external returns (uint256 newProposalId) { + function submit(address, ExternalCall[] calldata) external returns (uint256 newProposalId) { newProposalId = submittedProposals.length + OFFSET; submittedProposals.push(newProposalId); canScheduleProposal[newProposalId] = false; diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index ed35a999..67c7c9ea 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -14,7 +14,10 @@ import {Tiebreaker} from "contracts/libraries/Tiebreaker.sol"; import {Resealer} from "contracts/libraries/Resealer.sol"; import {Status as ProposalStatus} from "contracts/libraries/ExecutableProposals.sol"; import {Proposers} from "contracts/libraries/Proposers.sol"; + +import {IGovernance} from "contracts/interfaces/IGovernance.sol"; import {IResealManager} from "contracts/interfaces/IResealManager.sol"; + import { DualGovernanceConfig, IDualGovernanceConfigProvider, @@ -191,13 +194,15 @@ contract DualGovernanceUnitTests is UnitTest { 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, address(this), proposer.executor, calls, "") - ); + vm.expectCall(address(_timelock), 0, abi.encodeCall(TimelockMock.submit, (proposer.executor, calls))); + + uint256 expectedProposalId = 1; + string memory metadata = "New proposal description"; + + vm.expectEmit(); + emit IGovernance.ProposalReported(proposer.account, expectedProposalId, metadata); - uint256 proposalId = _dualGovernance.submitProposal(calls, ""); + uint256 proposalId = _dualGovernance.submitProposal(calls, metadata); uint256[] memory submittedProposals = _timelock.getSubmittedProposals(); assertEq(submittedProposals.length, 1); @@ -2384,8 +2389,7 @@ contract DualGovernanceUnitTests is UnitTest { // --- function _submitMockProposal() internal { - // mock timelock doesn't uses proposal data - _timelock.submit(msg.sender, address(0), new ExternalCall[](0), ""); + _timelock.submit(address(0), new ExternalCall[](0)); } function _scheduleProposal(uint256 proposalId, Timestamp submittedAt) internal { diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index b0881965..2f235c1d 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -186,21 +186,19 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(stranger); vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotGovernance.selector, [stranger])); - _timelock.submit(stranger, _adminExecutor, new ExternalCall[](0), ""); + _timelock.submit(_adminExecutor, new ExternalCall[](0)); assertEq(_timelock.getProposalsCount(), 0); } function test_submit_HappyPath() external { string memory testMetadata = "testMetadata"; - vm.expectEmit(true, true, true, true); + vm.expectEmit(); emit ExecutableProposals.ProposalSubmitted( - 1, _dualGovernance, _adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock)), testMetadata + 1, _adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock)) ); vm.prank(_dualGovernance); - _timelock.submit( - _dualGovernance, _adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock)), testMetadata - ); + _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_timelock.getProposalsCount(), 1); @@ -593,7 +591,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_emergencyExecute_RevertOn_ModeNotActive() external { vm.startPrank(_dualGovernance); - _timelock.submit(_dualGovernance, _adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_timelock.getProposalsCount(), 1); @@ -1045,8 +1043,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.startPrank(_dualGovernance); ExternalCall[] memory executorCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); ExternalCall[] memory anotherExecutorCalls = _getMockTargetRegularStaffCalls(address(_anotherTargetMock)); - _timelock.submit(_dualGovernance, _adminExecutor, executorCalls, ""); - _timelock.submit(_dualGovernance, _adminExecutor, anotherExecutorCalls, ""); + _timelock.submit(_adminExecutor, executorCalls); + _timelock.submit(_adminExecutor, anotherExecutorCalls); (ITimelock.ProposalDetails memory submittedProposal, ExternalCall[] memory calls) = _timelock.getProposal(1); @@ -1203,7 +1201,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_getProposalCalls() external { ExternalCall[] memory executorCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); vm.prank(_dualGovernance); - _timelock.submit(_dualGovernance, _adminExecutor, executorCalls, ""); + _timelock.submit(_adminExecutor, executorCalls); ExternalCall[] memory calls = _timelock.getProposalCalls(1); @@ -1254,7 +1252,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function _submitProposal() internal { vm.prank(_dualGovernance); - _timelock.submit(_dualGovernance, _adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); } function _scheduleProposal(uint256 proposalId) internal { diff --git a/test/unit/TimelockedGovernance.t.sol b/test/unit/TimelockedGovernance.t.sol index 8437bb55..80b83b45 100644 --- a/test/unit/TimelockedGovernance.t.sol +++ b/test/unit/TimelockedGovernance.t.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.26; import {ITimelock} from "contracts/interfaces/ITimelock.sol"; +import {IGovernance} from "contracts/interfaces/IGovernance.sol"; import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; import {UnitTest} from "test/utils/unit-test.sol"; @@ -43,8 +44,14 @@ contract TimelockedGovernanceUnitTests is UnitTest { function test_submit_proposal() external { assertEq(_timelock.getSubmittedProposals().length, 0); + uint256 expectedProposalId = 1; + string memory metadata = "proposal description"; + + vm.expectEmit(); + emit IGovernance.ProposalReported(_governance, expectedProposalId, metadata); + vm.prank(_governance); - _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1)), ""); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1)), metadata); assertEq(_timelock.getSubmittedProposals().length, 1); } diff --git a/test/unit/libraries/ExecutableProposals.t.sol b/test/unit/libraries/ExecutableProposals.t.sol index d95c328a..9b6f9f1f 100644 --- a/test/unit/libraries/ExecutableProposals.t.sol +++ b/test/unit/libraries/ExecutableProposals.t.sol @@ -32,7 +32,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_submit_reverts_if_empty_proposals() external { vm.expectRevert(ExecutableProposals.EmptyCalls.selector); - _proposals.submit(proposer, address(0), new ExternalCall[](0), "Empty calls"); + _proposals.submit(address(0), new ExternalCall[](0)); } function test_submit_proposal() external { @@ -41,14 +41,13 @@ 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, proposer, address(_executor), calls, description); + emit ExecutableProposals.ProposalSubmitted(expectedProposalId, address(_executor), calls); vm.recordLogs(); - _proposals.submit(proposer, address(_executor), calls, description); + _proposals.submit(address(_executor), calls); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 1); @@ -74,7 +73,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function testFuzz_schedule_proposal(Duration delay) external { vm.assume(delay > Durations.ZERO && delay.toSeconds() <= MAX_DURATION_VALUE); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 expectedProposalId = 1; ExecutableProposals.Proposal memory proposal = _proposals.proposals[expectedProposalId]; @@ -106,7 +105,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cannot_schedule_proposal_twice() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = 1; _proposals.schedule(proposalId, Durations.ZERO); @@ -117,7 +116,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function testFuzz_cannot_schedule_proposal_before_delay_passed(Duration delay) external { vm.assume(delay > Durations.ZERO && delay.toSeconds() <= MAX_DURATION_VALUE); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); _wait(delay.minusSeconds(1 seconds)); @@ -128,7 +127,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cannot_schedule_cancelled_proposal() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); _proposals.cancelAll(); uint256 proposalId = _proposals.getProposalsCount(); @@ -140,7 +139,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function testFuzz_execute_proposal(Duration delay) external { vm.assume(delay > Durations.ZERO && delay.toSeconds() <= MAX_DURATION_VALUE); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); @@ -176,7 +175,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cannot_execute_unscheduled_proposal() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); @@ -184,7 +183,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cannot_execute_twice() external { - _proposals.submit(proposer, 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); @@ -194,7 +193,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cannot_execute_cancelled_proposal() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); _proposals.cancelAll(); @@ -205,7 +204,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function testFuzz_cannot_execute_before_delay_passed(Duration delay) external { vm.assume(delay > Durations.ZERO && delay.toSeconds() <= MAX_DURATION_VALUE); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); @@ -216,8 +215,8 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cancel_all_proposals() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalsCount = _proposals.getProposalsCount(); @@ -233,7 +232,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(proposer, address(_executor), expectedCalls, ""); + _proposals.submit(address(_executor), expectedCalls); uint256 proposalId = _proposals.getProposalsCount(); ITimelock.ProposalDetails memory proposalDetails = _proposals.getProposalDetails(proposalId); @@ -295,7 +294,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_get_cancelled_proposal() external { ExternalCall[] memory expectedCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); - _proposals.submit(proposer, address(_executor), expectedCalls, ""); + _proposals.submit(address(_executor), expectedCalls); uint256 proposalId = _proposals.getProposalsCount(); ITimelock.ProposalDetails memory proposalDetails = _proposals.getProposalDetails(proposalId); @@ -346,16 +345,16 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_count_proposals() external { assertEq(_proposals.getProposalsCount(), 0); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.getProposalsCount(), 1); - _proposals.submit(proposer, 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(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.getProposalsCount(), 3); _proposals.schedule(2, Durations.ZERO); @@ -364,7 +363,7 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.execute(1, Durations.ZERO); assertEq(_proposals.getProposalsCount(), 3); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.getProposalsCount(), 4); _proposals.cancelAll(); @@ -373,7 +372,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_can_execute_proposal() external { Duration delay = Durations.from(100 seconds); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); assert(!_proposals.canExecute(proposalId, Durations.ZERO)); @@ -392,7 +391,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_can_not_execute_cancelled_proposal() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); @@ -403,18 +402,18 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cancelAll_DoesNotModifyStateOfExecutedProposals() external { - _proposals.submit(proposer, 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(proposer, 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(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.getProposalsCount(), 3); uint256 submittedProposalId = 3; @@ -436,7 +435,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_can_schedule_proposal() external { Duration delay = Durations.from(100 seconds); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); assert(!_proposals.canSchedule(proposalId, delay)); @@ -451,7 +450,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_can_not_schedule_cancelled_proposal() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); assert(_proposals.canSchedule(proposalId, Durations.ZERO)); From 1ee0b1b31abcbe8a96e2896aafa03028d9233299 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 06:41:22 +0400 Subject: [PATCH 087/107] Remove "registered" prefix from DG methods for proposers and executors --- contracts/DualGovernance.sol | 4 +- contracts/interfaces/IDualGovernance.sol | 4 +- scripts/deploy/DeployVerification.sol | 8 +--- test/unit/DualGovernance.t.sol | 52 ++++++++++++------------ 4 files changed, 32 insertions(+), 36 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 19d4426d..e67cd8a8 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -416,7 +416,7 @@ contract DualGovernance is IDualGovernance { /// @param proposerAccount The address to check. /// @return isProposer A boolean value indicating whether the `proposerAccount` is a registered /// proposer (`true`) or not (`false`). - function isRegisteredProposer(address proposerAccount) external view returns (bool) { + function isProposer(address proposerAccount) external view returns (bool) { return _proposers.isRegisteredProposer(proposerAccount); } @@ -424,7 +424,7 @@ contract DualGovernance is IDualGovernance { /// @param executor The address to check. /// @return isExecutor A boolean value indicating whether the `executor` is a registered /// executor (`true`) or not (`false`). - function isRegisteredExecutor(address executor) external view returns (bool) { + function isExecutor(address executor) external view returns (bool) { return _proposers.isRegisteredExecutor(executor); } diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index db7596b8..24833d07 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -41,10 +41,10 @@ interface IDualGovernance is IGovernance, ITiebreaker { function registerProposer(address proposer, address executor) external; function unregisterProposer(address proposer) external; - function isRegisteredProposer(address account) external view returns (bool); + function isProposer(address account) external view returns (bool); function getProposer(address account) external view returns (Proposers.Proposer memory proposer); function getProposers() external view returns (Proposers.Proposer[] memory proposers); - function isRegisteredExecutor(address account) external view returns (bool); + function isExecutor(address account) external view returns (bool); function resealSealable(address sealable) external; function setResealCommittee(address resealCommittee) external; diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index e464c578..36ba1446 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -244,12 +244,8 @@ library DeployVerification { require(dg.getPersistedState() == State.Normal, "Incorrect DualGovernance persisted state"); require(dg.getEffectiveState() == State.Normal, "Incorrect DualGovernance effective state"); require(dg.getProposers().length == 1, "Incorrect amount of proposers"); - require( - dg.isRegisteredProposer(address(lidoAddresses.voting)) == true, "Lido voting is not set as a proposers[0]" - ); - require( - dg.isRegisteredExecutor(res.adminExecutor) == true, "adminExecutor is not set as a proposers[0].executor" - ); + require(dg.isProposer(address(lidoAddresses.voting)) == true, "Lido voting is not set as a proposers[0]"); + require(dg.isExecutor(res.adminExecutor) == true, "adminExecutor is not set as a proposers[0].executor"); IDualGovernance.StateDetails memory stateDetails = dg.getStateDetails(); require(stateDetails.effectiveState == State.Normal, "Incorrect DualGovernance effectiveState"); diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index add00fc9..0e545b21 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -1375,8 +1375,8 @@ contract DualGovernanceUnitTests is UnitTest { address newProposer = makeAddr("NEW_PROPOSER"); address newExecutor = makeAddr("NEW_EXECUTOR"); - assertFalse(_dualGovernance.isRegisteredProposer(newProposer)); - assertFalse(_dualGovernance.isRegisteredExecutor(newExecutor)); + assertFalse(_dualGovernance.isProposer(newProposer)); + assertFalse(_dualGovernance.isExecutor(newExecutor)); _executor.execute( address(_dualGovernance), @@ -1384,8 +1384,8 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.registerProposer.selector, newProposer, newExecutor) ); - assertTrue(_dualGovernance.isRegisteredProposer(newProposer)); - assertTrue(_dualGovernance.isRegisteredExecutor(newExecutor)); + assertTrue(_dualGovernance.isProposer(newProposer)); + assertTrue(_dualGovernance.isExecutor(newExecutor)); Proposers.Proposer memory proposer = _dualGovernance.getProposer(newProposer); assertEq(proposer.account, newProposer); @@ -1411,20 +1411,20 @@ contract DualGovernanceUnitTests is UnitTest { address newExecutor = makeAddr("NEW_EXECUTOR"); assertEq(_dualGovernance.getProposers().length, 1); - assertFalse(_dualGovernance.isRegisteredProposer(newProposer)); + assertFalse(_dualGovernance.isProposer(newProposer)); vm.prank(address(_executor)); _dualGovernance.registerProposer(newProposer, newExecutor); assertEq(_dualGovernance.getProposers().length, 2); - assertTrue(_dualGovernance.isRegisteredProposer(newProposer)); + assertTrue(_dualGovernance.isProposer(newProposer)); vm.prank(address(_executor)); _dualGovernance.setProposerExecutor(newProposer, address(_executor)); assertEq(_dualGovernance.getProposers().length, 2); - assertTrue(_dualGovernance.isRegisteredProposer(newProposer)); - assertFalse(_dualGovernance.isRegisteredExecutor(newExecutor)); + assertTrue(_dualGovernance.isProposer(newProposer)); + assertFalse(_dualGovernance.isExecutor(newExecutor)); } function testFuzz_setProposerExecutor_RevertOn_CalledNotByAdminExecutor(address notAllowedCaller) external { @@ -1437,7 +1437,7 @@ contract DualGovernanceUnitTests is UnitTest { _dualGovernance.registerProposer(newProposer, newExecutor); assertEq(_dualGovernance.getProposers().length, 2); - assertTrue(_dualGovernance.isRegisteredProposer(newProposer)); + assertTrue(_dualGovernance.isProposer(newProposer)); vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, notAllowedCaller)); @@ -1450,8 +1450,8 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(_dualGovernance.getProposers().length, 1); - assertTrue(_dualGovernance.isRegisteredProposer(address(this))); - assertTrue(_dualGovernance.isRegisteredExecutor(address(_executor))); + assertTrue(_dualGovernance.isProposer(address(this))); + assertTrue(_dualGovernance.isExecutor(address(_executor))); vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, address(_executor))); @@ -1473,15 +1473,15 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) ); - assertTrue(_dualGovernance.isRegisteredProposer(proposer)); - assertTrue(_dualGovernance.isRegisteredExecutor(proposerExecutor)); + assertTrue(_dualGovernance.isProposer(proposer)); + assertTrue(_dualGovernance.isExecutor(proposerExecutor)); _executor.execute( address(_dualGovernance), 0, abi.encodeWithSelector(DualGovernance.unregisterProposer.selector, proposer) ); - assertFalse(_dualGovernance.isRegisteredProposer(proposer)); - assertFalse(_dualGovernance.isRegisteredExecutor(proposerExecutor)); + assertFalse(_dualGovernance.isProposer(proposer)); + assertFalse(_dualGovernance.isExecutor(proposerExecutor)); vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, proposer)); _dualGovernance.getProposer(proposer); @@ -1514,8 +1514,8 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.unregisterProposer.selector, address(this)) ); - assertTrue(_dualGovernance.isRegisteredProposer(address(this))); - assertTrue(_dualGovernance.isRegisteredExecutor(adminExecutor)); + assertTrue(_dualGovernance.isProposer(address(this))); + assertTrue(_dualGovernance.isExecutor(adminExecutor)); } // --- @@ -1526,8 +1526,8 @@ contract DualGovernanceUnitTests is UnitTest { address proposer = makeAddr("PROPOSER"); address proposerExecutor = makeAddr("PROPOSER_EXECUTOR"); - assertFalse(_dualGovernance.isRegisteredProposer(proposer)); - assertFalse(_dualGovernance.isRegisteredExecutor(proposerExecutor)); + assertFalse(_dualGovernance.isProposer(proposer)); + assertFalse(_dualGovernance.isExecutor(proposerExecutor)); _executor.execute( address(_dualGovernance), @@ -1535,14 +1535,14 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) ); - assertTrue(_dualGovernance.isRegisteredProposer(proposer)); - assertTrue(_dualGovernance.isRegisteredExecutor(proposerExecutor)); + assertTrue(_dualGovernance.isProposer(proposer)); + assertTrue(_dualGovernance.isExecutor(proposerExecutor)); } function testFuzz_isProposer_UnregisteredProposer(address proposer) external { vm.assume(proposer != address(this)); - assertFalse(_dualGovernance.isRegisteredProposer(proposer)); + assertFalse(_dualGovernance.isProposer(proposer)); } // --- @@ -1618,7 +1618,7 @@ contract DualGovernanceUnitTests is UnitTest { function test_isExecutor_HappyPath() external { address executor = makeAddr("EXECUTOR1"); - assertFalse(_dualGovernance.isRegisteredExecutor(executor)); + assertFalse(_dualGovernance.isExecutor(executor)); _executor.execute( address(_dualGovernance), @@ -1626,14 +1626,14 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(0x123), executor) ); - assertTrue(_dualGovernance.isRegisteredExecutor(executor)); - assertTrue(_dualGovernance.isRegisteredExecutor(address(_executor))); + assertTrue(_dualGovernance.isExecutor(executor)); + assertTrue(_dualGovernance.isExecutor(address(_executor))); } function testFuzz_isExecutor_UnregisteredExecutor(address executor) external { vm.assume(executor != address(_executor)); - assertFalse(_dualGovernance.isRegisteredExecutor(executor)); + assertFalse(_dualGovernance.isExecutor(executor)); } // --- From 444954129ee254c18bf65133c667fcfb8e92b584 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 6 Dec 2024 12:19:43 +0300 Subject: [PATCH 088/107] Add maximum assets lock duration to Escrow contract --- contracts/Escrow.sol | 11 ++++++++--- contracts/interfaces/IEscrow.sol | 2 ++ contracts/libraries/EscrowState.sol | 10 ++++++++-- scripts/deploy/Config.sol | 2 ++ scripts/deploy/ContractsDeployment.sol | 3 ++- scripts/deploy/JsonConfig.s.sol | 4 ++++ test/mocks/EscrowMock.sol | 9 +++++++++ test/unit/DualGovernance.t.sol | 3 ++- test/unit/Escrow.t.sol | 10 ++++++++-- test/unit/libraries/EscrowState.t.sol | 24 ++++++++++++++++++++---- 10 files changed, 65 insertions(+), 13 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 87c61c97..1c5f9081 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {Duration} from "./types/Duration.sol"; +import {Durations, Duration} from "./types/Duration.sol"; import {Timestamp} from "./types/Timestamp.sol"; import {ETHValue, ETHValues} from "./types/ETHValue.sol"; import {SharesValue, SharesValues} from "./types/SharesValue.sol"; @@ -58,6 +58,9 @@ contract Escrow is IEscrow { /// the `Escrow.requestNextWithdrawalsBatch(batchSize)` method. uint256 public immutable MIN_WITHDRAWALS_BATCH_SIZE; + /// @notice The maximum duration that can be set as the minimum assets lock duration. + Duration public immutable MAX_ASSETS_LOCK_DURATION; + // --- // Dependencies Immutables // --- @@ -104,7 +107,8 @@ contract Escrow is IEscrow { IWstETH wstETH, IWithdrawalQueue withdrawalQueue, IDualGovernance dualGovernance, - uint256 minWithdrawalsBatchSize + uint256 minWithdrawalsBatchSize, + Duration maxAssetsLockDuration ) { _SELF = address(this); DUAL_GOVERNANCE = dualGovernance; @@ -114,6 +118,7 @@ contract Escrow is IEscrow { WITHDRAWAL_QUEUE = withdrawalQueue; MIN_WITHDRAWALS_BATCH_SIZE = minWithdrawalsBatchSize; + MAX_ASSETS_LOCK_DURATION = maxAssetsLockDuration; } /// @notice Initializes the proxy instance with the specified minimum assets lock duration. @@ -432,7 +437,7 @@ contract Escrow is IEscrow { /// @param newMinAssetsLockDuration The new minimum lock duration to be set. function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { _checkCallerIsDualGovernance(); - _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration); + _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration, MAX_ASSETS_LOCK_DURATION); } // --- diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol index a51a5030..a0e8973c 100644 --- a/contracts/interfaces/IEscrow.sol +++ b/contracts/interfaces/IEscrow.sol @@ -32,6 +32,8 @@ interface IEscrow { function initialize(Duration minAssetsLockDuration) external; + function MAX_ASSETS_LOCK_DURATION() external view returns (Duration); + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares); function unlockStETH() external returns (uint256 unlockedStETHShares); function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares); diff --git a/contracts/libraries/EscrowState.sol b/contracts/libraries/EscrowState.sol index 9fb30ff3..dbd37c5b 100644 --- a/contracts/libraries/EscrowState.sol +++ b/contracts/libraries/EscrowState.sol @@ -103,8 +103,14 @@ library EscrowState { /// @notice Sets the minimum assets lock duration. /// @param self The context of the Escrow State library. /// @param newMinAssetsLockDuration The new minimum assets lock duration. - function setMinAssetsLockDuration(Context storage self, Duration newMinAssetsLockDuration) internal { - if (self.minAssetsLockDuration == newMinAssetsLockDuration) { + /// @param maxAssetsLockDuration Sanity check for max assets lock duration. + function setMinAssetsLockDuration( + Context storage self, + Duration newMinAssetsLockDuration, + Duration maxAssetsLockDuration + ) internal { + if (self.minAssetsLockDuration == newMinAssetsLockDuration || newMinAssetsLockDuration > maxAssetsLockDuration) + { revert InvalidMinAssetsLockDuration(newMinAssetsLockDuration); } _setMinAssetsLockDuration(self, newMinAssetsLockDuration); diff --git a/scripts/deploy/Config.sol b/scripts/deploy/Config.sol index 4b46f375..bb438f90 100644 --- a/scripts/deploy/Config.sol +++ b/scripts/deploy/Config.sol @@ -31,6 +31,7 @@ uint256 constant DEFAULT_MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = 255; uint256 constant DEFAULT_FIRST_SEAL_RAGE_QUIT_SUPPORT = 3_00; // 3% uint256 constant DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT = 15_00; // 15% uint256 constant DEFAULT_MIN_ASSETS_LOCK_DURATION = 5 hours; +uint256 constant DEFAULT_MAX_ASSETS_LOCK_DURATION = 365 days; uint256 constant DEFAULT_VETO_SIGNALLING_MIN_DURATION = 3 days; uint256 constant DEFAULT_VETO_SIGNALLING_MAX_DURATION = 30 days; uint256 constant DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION = 5 hours; @@ -80,6 +81,7 @@ struct DeployConfig { PercentD16 FIRST_SEAL_RAGE_QUIT_SUPPORT; PercentD16 SECOND_SEAL_RAGE_QUIT_SUPPORT; Duration MIN_ASSETS_LOCK_DURATION; + Duration MAX_ASSETS_LOCK_DURATION; Duration VETO_SIGNALLING_MIN_DURATION; Duration VETO_SIGNALLING_MAX_DURATION; Duration VETO_SIGNALLING_MIN_ACTIVE_DURATION; diff --git a/scripts/deploy/ContractsDeployment.sol b/scripts/deploy/ContractsDeployment.sol index dcd84c3e..f5a1b33d 100644 --- a/scripts/deploy/ContractsDeployment.sol +++ b/scripts/deploy/ContractsDeployment.sol @@ -228,7 +228,8 @@ library DGContractsDeployment { minWithdrawalsBatchSize: dgDeployConfig.MIN_WITHDRAWALS_BATCH_SIZE, minTiebreakerActivationTimeout: dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT, maxTiebreakerActivationTimeout: dgDeployConfig.MAX_TIEBREAKER_ACTIVATION_TIMEOUT, - maxSealableWithdrawalBlockersCount: dgDeployConfig.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT + maxSealableWithdrawalBlockersCount: dgDeployConfig.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT, + maxAssetsLockDuration: dgDeployConfig.MAX_ASSETS_LOCK_DURATION }) }); } diff --git a/scripts/deploy/JsonConfig.s.sol b/scripts/deploy/JsonConfig.s.sol index 5922de0d..f4227b21 100644 --- a/scripts/deploy/JsonConfig.s.sol +++ b/scripts/deploy/JsonConfig.s.sol @@ -46,6 +46,7 @@ import { DEFAULT_FIRST_SEAL_RAGE_QUIT_SUPPORT, DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT, DEFAULT_MIN_ASSETS_LOCK_DURATION, + DEFAULT_MAX_ASSETS_LOCK_DURATION, DEFAULT_VETO_SIGNALLING_MIN_DURATION, DEFAULT_VETO_SIGNALLING_MAX_DURATION, DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION, @@ -173,6 +174,9 @@ contract DGDeployJSONConfigProvider is Script { MIN_ASSETS_LOCK_DURATION: Durations.from( stdJson.readUintOr(jsonConfig, ".MIN_ASSETS_LOCK_DURATION", DEFAULT_MIN_ASSETS_LOCK_DURATION) ), + MAX_ASSETS_LOCK_DURATION: Durations.from( + stdJson.readUintOr(jsonConfig, ".MAX_ASSETS_LOCK_DURATION", DEFAULT_MAX_ASSETS_LOCK_DURATION) + ), VETO_SIGNALLING_MIN_DURATION: Durations.from( stdJson.readUintOr(jsonConfig, ".VETO_SIGNALLING_MIN_DURATION", DEFAULT_VETO_SIGNALLING_MIN_DURATION) ), diff --git a/test/mocks/EscrowMock.sol b/test/mocks/EscrowMock.sol index d87c5965..ed58f4a1 100644 --- a/test/mocks/EscrowMock.sol +++ b/test/mocks/EscrowMock.sol @@ -12,6 +12,7 @@ contract EscrowMock is IEscrow { event __RageQuitStarted(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock); Duration public __minAssetsLockDuration; + Duration public __maxAssetsLockDuration; PercentD16 public __rageQuitSupport; bool public __isRageQuitFinalized; @@ -130,4 +131,12 @@ contract EscrowMock is IEscrow { function getMinAssetsLockDuration() external view returns (Duration minAssetsLockDuration) { return __minAssetsLockDuration; } + + function MAX_ASSETS_LOCK_DURATION() external view returns (Duration) { + return __maxAssetsLockDuration; + } + + function setMaxAssetsLockDuration(Duration newMaxAssetsLockDuration) external { + __maxAssetsLockDuration = newMaxAssetsLockDuration; + } } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index add00fc9..3e8fe3ba 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -84,7 +84,8 @@ contract DualGovernanceUnitTests is UnitTest { minWithdrawalsBatchSize: 4, minTiebreakerActivationTimeout: Durations.from(30 days), maxTiebreakerActivationTimeout: Durations.from(180 days), - maxSealableWithdrawalBlockersCount: 128 + maxSealableWithdrawalBlockersCount: 128, + maxAssetsLockDuration: Durations.from(365 days) }); DualGovernance internal _dualGovernance = diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index 0f1a17ce..a697b1a8 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -44,8 +44,14 @@ contract EscrowUnitTests is UnitTest { _wstETH = IWstETH(address(new ERC20Mock())); _withdrawalQueue = new WithdrawalQueueMock(_stETH); _withdrawalQueue.setMaxStETHWithdrawalAmount(1_000 ether); - _masterCopy = - new Escrow(_stETH, _wstETH, WithdrawalQueueMock(_withdrawalQueue), IDualGovernance(_dualGovernance), 100); + _masterCopy = new Escrow( + _stETH, + _wstETH, + WithdrawalQueueMock(_withdrawalQueue), + IDualGovernance(_dualGovernance), + 100, + Durations.from(1000) + ); _escrow = Escrow(payable(Clones.clone(address(_masterCopy)))); vm.prank(address(_escrow)); diff --git a/test/unit/libraries/EscrowState.t.sol b/test/unit/libraries/EscrowState.t.sol index c7685468..88ff03f0 100644 --- a/test/unit/libraries/EscrowState.t.sol +++ b/test/unit/libraries/EscrowState.t.sol @@ -103,13 +103,17 @@ contract EscrowStateUnitTests is UnitTest { // setMinAssetsLockDuration() // --- - function test_setMinAssetsLockDuration_happyPath(Duration minAssetsLockDuration) external { + function testFuzz_setMinAssetsLockDuration_happyPath( + Duration minAssetsLockDuration, + Duration maxAssetsLockDuration + ) external { vm.assume(minAssetsLockDuration != Durations.ZERO); + vm.assume(minAssetsLockDuration <= maxAssetsLockDuration); vm.expectEmit(); emit EscrowState.MinAssetsLockDurationSet(minAssetsLockDuration); - EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration); + EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration, maxAssetsLockDuration); checkContext({ state: State.NotInitialized, @@ -120,13 +124,25 @@ contract EscrowStateUnitTests is UnitTest { }); } - function test_setMinAssetsLockDuration_RevertWhen_DurationNotChanged(Duration minAssetsLockDuration) external { + function testFuzz_setMinAssetsLockDuration_RevertWhen_DurationNotChanged(Duration minAssetsLockDuration) external { _context.minAssetsLockDuration = minAssetsLockDuration; vm.expectRevert( abi.encodeWithSelector(EscrowState.InvalidMinAssetsLockDuration.selector, minAssetsLockDuration) ); - EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration); + EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration, Durations.from(type(uint16).max)); + } + + function testFuzz_setMinAssetsLockDuration_RevertWhen_DurationGreaterThenMaxAssetsLockDuration( + Duration minAssetsLockDuration, + Duration maxAssetsLockDuration + ) external { + vm.assume(minAssetsLockDuration > maxAssetsLockDuration); + + vm.expectRevert( + abi.encodeWithSelector(EscrowState.InvalidMinAssetsLockDuration.selector, minAssetsLockDuration) + ); + EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration, maxAssetsLockDuration); } // --- From b41866634ba195cd6db4cfb508f4554162a438cc Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 6 Dec 2024 12:20:52 +0300 Subject: [PATCH 089/107] DualGovernance dependencies split --- contracts/DualGovernance.sol | 37 +++++++++++++------- scripts/deploy/ContractsDeployment.sol | 10 +++--- test/unit/DualGovernance.t.sol | 47 +++++++++++++++++++------- 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 19d4426d..4b78cf17 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -69,11 +69,13 @@ contract DualGovernance is IDualGovernance { /// @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. + /// @param maxAssetsLockDuration The maximum duration for which assets can be locked in the Rage Quit Escrow contract. struct SanityCheckParams { uint256 minWithdrawalsBatchSize; Duration minTiebreakerActivationTimeout; Duration maxTiebreakerActivationTimeout; uint256 maxSealableWithdrawalBlockersCount; + Duration maxAssetsLockDuration; } /// @notice The lower bound for the time the Dual Governance must spend in the "locked" state @@ -93,17 +95,21 @@ contract DualGovernance is IDualGovernance { // External Dependencies // --- - /// @notice The external dependencies of the Dual Governance system. + /// @notice Token addresses tha used in the Dual Governance as signalling tokens. /// @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 { + struct SignallingTokens { IStETH stETH; IWstETH wstETH; IWithdrawalQueue withdrawalQueue; + } + + /// @notice Dependencies required by the Dual Governance contract. + /// @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 DualGovernanceComponents { ITimelock timelock; IResealManager resealManager; IDualGovernanceConfigProvider configProvider; @@ -140,12 +146,16 @@ contract DualGovernance is IDualGovernance { // Constructor // --- - constructor(ExternalDependencies memory dependencies, SanityCheckParams memory sanityCheckParams) { + constructor( + DualGovernanceComponents memory components, + SignallingTokens memory signallingTokens, + SanityCheckParams memory sanityCheckParams + ) { if (sanityCheckParams.minTiebreakerActivationTimeout > sanityCheckParams.maxTiebreakerActivationTimeout) { revert InvalidTiebreakerActivationTimeoutBounds(); } - TIMELOCK = dependencies.timelock; + TIMELOCK = components.timelock; MIN_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.minTiebreakerActivationTimeout; MAX_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.maxTiebreakerActivationTimeout; @@ -153,15 +163,16 @@ contract DualGovernance is IDualGovernance { ESCROW_MASTER_COPY = new Escrow({ dualGovernance: this, - stETH: dependencies.stETH, - wstETH: dependencies.wstETH, - withdrawalQueue: dependencies.withdrawalQueue, - minWithdrawalsBatchSize: sanityCheckParams.minWithdrawalsBatchSize + stETH: signallingTokens.stETH, + wstETH: signallingTokens.wstETH, + withdrawalQueue: signallingTokens.withdrawalQueue, + minWithdrawalsBatchSize: sanityCheckParams.minWithdrawalsBatchSize, + maxAssetsLockDuration: sanityCheckParams.maxAssetsLockDuration }); emit EscrowMasterCopyDeployed(ESCROW_MASTER_COPY); - _stateMachine.initialize(dependencies.configProvider, ESCROW_MASTER_COPY); - _resealer.setResealManager(address(dependencies.resealManager)); + _stateMachine.initialize(components.configProvider, ESCROW_MASTER_COPY); + _resealer.setResealManager(address(components.resealManager)); } // --- diff --git a/scripts/deploy/ContractsDeployment.sol b/scripts/deploy/ContractsDeployment.sol index f5a1b33d..af87be0e 100644 --- a/scripts/deploy/ContractsDeployment.sol +++ b/scripts/deploy/ContractsDeployment.sol @@ -216,14 +216,16 @@ library DGContractsDeployment { LidoContracts memory lidoAddresses ) internal returns (DualGovernance) { return new DualGovernance({ - dependencies: DualGovernance.ExternalDependencies({ - stETH: lidoAddresses.stETH, - wstETH: lidoAddresses.wstETH, - withdrawalQueue: lidoAddresses.withdrawalQueue, + components: DualGovernance.DualGovernanceComponents({ timelock: timelock, resealManager: resealManager, configProvider: configProvider }), + signallingTokens: DualGovernance.SignallingTokens({ + stETH: lidoAddresses.stETH, + wstETH: lidoAddresses.wstETH, + withdrawalQueue: lidoAddresses.withdrawalQueue + }), sanityCheckParams: DualGovernance.SanityCheckParams({ minWithdrawalsBatchSize: dgDeployConfig.MIN_WITHDRAWALS_BATCH_SIZE, minTiebreakerActivationTimeout: dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT, diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 3e8fe3ba..e678a87a 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -71,10 +71,13 @@ contract DualGovernanceUnitTests is UnitTest { }) ); - DualGovernance.ExternalDependencies internal _externalDependencies = DualGovernance.ExternalDependencies({ + DualGovernance.SignallingTokens internal _signallingTokens = DualGovernance.SignallingTokens({ stETH: _STETH_MOCK, wstETH: _WSTETH_STUB, - withdrawalQueue: _WITHDRAWAL_QUEUE_MOCK, + withdrawalQueue: _WITHDRAWAL_QUEUE_MOCK + }); + + DualGovernance.DualGovernanceComponents internal _dgComponents = DualGovernance.DualGovernanceComponents({ timelock: _timelock, resealManager: _RESEAL_MANAGER_STUB, configProvider: _configProvider @@ -88,8 +91,11 @@ contract DualGovernanceUnitTests is UnitTest { maxAssetsLockDuration: Durations.from(365 days) }); - DualGovernance internal _dualGovernance = - new DualGovernance({dependencies: _externalDependencies, sanityCheckParams: _sanityCheckParams}); + DualGovernance internal _dualGovernance = new DualGovernance({ + components: _dgComponents, + signallingTokens: _signallingTokens, + sanityCheckParams: _sanityCheckParams + }); Escrow internal _escrow; @@ -122,14 +128,22 @@ contract DualGovernanceUnitTests is UnitTest { _sanityCheckParams.minTiebreakerActivationTimeout = Durations.from(999); _sanityCheckParams.maxTiebreakerActivationTimeout = Durations.from(1000); - new DualGovernance({dependencies: _externalDependencies, sanityCheckParams: _sanityCheckParams}); + new DualGovernance({ + components: _dgComponents, + signallingTokens: _signallingTokens, + sanityCheckParams: _sanityCheckParams + }); } function test_constructor_min_max_timeout_same() external { _sanityCheckParams.minTiebreakerActivationTimeout = Durations.from(1000); _sanityCheckParams.maxTiebreakerActivationTimeout = Durations.from(1000); - new DualGovernance({dependencies: _externalDependencies, sanityCheckParams: _sanityCheckParams}); + new DualGovernance({ + components: _dgComponents, + signallingTokens: _signallingTokens, + sanityCheckParams: _sanityCheckParams + }); } function test_constructor_RevertsOn_InvalidTiebreakerActivationTimeoutBounds() external { @@ -138,7 +152,11 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidTiebreakerActivationTimeoutBounds.selector)); - new DualGovernance({dependencies: _externalDependencies, sanityCheckParams: _sanityCheckParams}); + new DualGovernance({ + components: _dgComponents, + signallingTokens: _signallingTokens, + sanityCheckParams: _sanityCheckParams + }); } // --- @@ -160,21 +178,25 @@ contract DualGovernanceUnitTests is UnitTest { Duration minTiebreakerActivationTimeout = Durations.from(30 days); Duration maxTiebreakerActivationTimeout = Durations.from(180 days); uint256 maxSealableWithdrawalBlockersCount = 128; + Duration maxAssetsLockDuration = Durations.from(365 days); DualGovernance dualGovernanceLocal = new DualGovernance({ - dependencies: DualGovernance.ExternalDependencies({ - stETH: _STETH_MOCK, - wstETH: _WSTETH_STUB, - withdrawalQueue: _WITHDRAWAL_QUEUE_MOCK, + components: DualGovernance.DualGovernanceComponents({ timelock: _timelock, resealManager: _RESEAL_MANAGER_STUB, configProvider: _configProvider }), + signallingTokens: DualGovernance.SignallingTokens({ + stETH: _STETH_MOCK, + wstETH: _WSTETH_STUB, + withdrawalQueue: _WITHDRAWAL_QUEUE_MOCK + }), sanityCheckParams: DualGovernance.SanityCheckParams({ minWithdrawalsBatchSize: 4, minTiebreakerActivationTimeout: minTiebreakerActivationTimeout, maxTiebreakerActivationTimeout: maxTiebreakerActivationTimeout, - maxSealableWithdrawalBlockersCount: maxSealableWithdrawalBlockersCount + maxSealableWithdrawalBlockersCount: maxSealableWithdrawalBlockersCount, + maxAssetsLockDuration: maxAssetsLockDuration }) }); @@ -183,6 +205,7 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(dualGovernanceLocal.MAX_TIEBREAKER_ACTIVATION_TIMEOUT(), maxTiebreakerActivationTimeout); assertEq(dualGovernanceLocal.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT(), maxSealableWithdrawalBlockersCount); assertEq(address(dualGovernanceLocal.ESCROW_MASTER_COPY()), predictedEscrowCopyAddress); + assertEq(dualGovernanceLocal.ESCROW_MASTER_COPY().MAX_ASSETS_LOCK_DURATION(), maxAssetsLockDuration); } // --- From 27ca7696eee5c5fe1ff46a70b9dc4a9ba40aa3a6 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Tue, 29 Oct 2024 19:16:16 +0400 Subject: [PATCH 090/107] Add some unit tests for WithdrawalBatchesQueue. Minor improvements. --- test/scenario/escrow.t.sol | 2 +- test/unit/libraries/AssetsAccounting.t.sol | 4 +- .../libraries/WithdrawalBatchesQueue.t.sol | 114 +++++++++++++++++- 3 files changed, 114 insertions(+), 6 deletions(-) diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 093197de..31f9c754 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -393,7 +393,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { escrow.requestNextWithdrawalsBatch(96); - vm.expectRevert(); + vm.expectRevert(WithdrawalsBatchesQueue.EmptyBatch.selector); escrow.claimNextWithdrawalsBatch(0, new uint256[](0)); escrow.startRageQuitExtensionPeriod(); diff --git a/test/unit/libraries/AssetsAccounting.t.sol b/test/unit/libraries/AssetsAccounting.t.sol index 7fce5dae..e1c9b3df 100644 --- a/test/unit/libraries/AssetsAccounting.t.sol +++ b/test/unit/libraries/AssetsAccounting.t.sol @@ -392,7 +392,7 @@ contract AssetsAccountingUnitTests is UnitTest { new IWithdrawalQueue.WithdrawalRequestStatus[](1); uint256[] memory unstETHIds = new uint256[](0); - vm.expectRevert(); + vm.expectRevert(stdError.assertionError); AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); } @@ -464,7 +464,7 @@ contract AssetsAccountingUnitTests is UnitTest { withdrawalRequestStatuses[withdrawalRequestStatuses.length - 1].isClaimed = true; - vm.expectRevert(); + vm.expectRevert(stdError.assertionError); AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); } diff --git a/test/unit/libraries/WithdrawalBatchesQueue.t.sol b/test/unit/libraries/WithdrawalBatchesQueue.t.sol index bd8f11fc..c3e30c1b 100644 --- a/test/unit/libraries/WithdrawalBatchesQueue.t.sol +++ b/test/unit/libraries/WithdrawalBatchesQueue.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {stdError} from "forge-std/StdError.sol"; import {UnitTest} from "test/utils/unit-test.sol"; import {WithdrawalsBatchesQueue, State} from "contracts/libraries/WithdrawalsBatchesQueue.sol"; @@ -147,7 +148,8 @@ contract WithdrawalsBatchesQueueTest is UnitTest { // violate the order of the NFT ids unstETHIds[2] = unstETHIds[3]; - vm.expectRevert(); + vm.expectRevert(stdError.assertionError); + _batchesQueue.addUnstETHIds(unstETHIds); } @@ -292,6 +294,48 @@ contract WithdrawalsBatchesQueueTest is UnitTest { _batchesQueue.claimNextBatch(1); } + function test_claimNextBatch_RevertWOn_NothingToClaim() external { + _openBatchesQueue(); + + uint256 unstETHIdsCount = 5; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); + + _batchesQueue.addUnstETHIds(unstETHIds); + assertEq(_batchesQueue.info.totalUnstETHIdsCount, unstETHIdsCount); + assertEq(_batchesQueue.batches.length, 2); + + uint256 maxUnstETHIdsCount = 5; + uint256[] memory claimedIds = _batchesQueue.claimNextBatch(maxUnstETHIdsCount); + + assertEq(claimedIds.length, maxUnstETHIdsCount); + assertEq(_batchesQueue.info.totalUnstETHIdsClaimed, maxUnstETHIdsCount); + + vm.expectRevert(WithdrawalsBatchesQueue.EmptyBatch.selector); + _batchesQueue.claimNextBatch(100); + } + + function test_claimNextBatch_RevertOn_AccountingError_TotalUnstETHClaimed_GT_TotalUnstETHCount() external { + _openBatchesQueue(); + + _batchesQueue.info.totalUnstETHIdsClaimed = 1; + vm.expectRevert(stdError.arithmeticError); + _batchesQueue.claimNextBatch(100); + } + + function test_claimNextBatch_RevertOn_AccountingError_LastClaimedBatchIndexOutOfArrayBounds() external { + _openBatchesQueue(); + + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: 1, firstUnstETHId: firstUnstETHId}); + + _batchesQueue.addUnstETHIds(unstETHIds); + + _batchesQueue.info.lastClaimedBatchIndex = 2; + vm.expectRevert(stdError.indexOOBError); + _batchesQueue.claimNextBatch(100); + } + // --- // close() // --- @@ -329,7 +373,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { // calcRequestAmounts() // --- - function test_calcRequestAmounts_HappyPath_WithoutReminder() external { + function test_calcRequestAmounts_HappyPath_WithoutRemainder() external { _openBatchesQueue(); assertEq(_batchesQueue.info.state, State.Opened); @@ -354,7 +398,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { } } - function test_calcRequestAmounts_HappyPath_WithReminder() external { + function test_calcRequestAmounts_HappyPath_WithRemainder() external { _openBatchesQueue(); assertEq(_batchesQueue.info.state, State.Opened); @@ -380,6 +424,13 @@ contract WithdrawalsBatchesQueueTest is UnitTest { } } + function test_calcRequestAmounts_RevertOn_DivisionByZero() external { + _openBatchesQueue(); + + vm.expectRevert(stdError.divisionError); + WithdrawalsBatchesQueue.calcRequestAmounts({minRequestAmount: 1, maxRequestAmount: 0, remainingAmount: 100}); + } + // --- // getNextWithdrawalsBatches() // --- @@ -481,6 +532,24 @@ contract WithdrawalsBatchesQueueTest is UnitTest { assertEq(_batchesQueue.info.totalUnstETHIdsClaimed, 0); } + function test_getNextWithdrawalsBatches_RevertOn_AccountingError_TotalUnstETHClaimed_GT_TotalUnstETHCount() + external + { + _openBatchesQueue(); + + _batchesQueue.info.totalUnstETHIdsClaimed = 1; + vm.expectRevert(stdError.arithmeticError); + _batchesQueue.getNextWithdrawalsBatches(10); + } + + function test_getNextWithdrawalsBatches_RevertOn_AccountingError_LastClaimedBatchIndexOutOfArrayBounds() external { + _openBatchesQueue(); + + _batchesQueue.info.lastClaimedBatchIndex = 2; + vm.expectRevert(stdError.indexOOBError); + _batchesQueue.getNextWithdrawalsBatches(10); + } + // --- // getLastClaimedOrBoundaryUnstETHId() // --- @@ -513,6 +582,38 @@ contract WithdrawalsBatchesQueueTest is UnitTest { _batchesQueue.getLastClaimedOrBoundaryUnstETHId(); } + function test_getLastClaimedOrBoundaryUnstETHId_RevertOn_LastClaimedBatchIndexOutOfArrayBounds() external { + _openBatchesQueue(); + _batchesQueue.info.lastClaimedBatchIndex = 2; + + vm.expectRevert(stdError.indexOOBError); + _batchesQueue.getLastClaimedOrBoundaryUnstETHId(); + } + + // --- + // isAllBatchesClaimed() + // --- + + function testFuzz_isAllBatchesClaimed_HappyPath_ReturnsTrue(uint64 count) external { + _batchesQueue.info.totalUnstETHIdsClaimed = count; + _batchesQueue.info.totalUnstETHIdsCount = count; + + bool res = _batchesQueue.isAllBatchesClaimed(); + assert(res == true); + } + + function testFuzz_isAllBatchesClaimed_HappyPath_ReturnsFalse( + uint64 totalUnstETHClaimed, + uint64 totalUnstETHCount + ) external { + vm.assume(totalUnstETHClaimed != totalUnstETHCount); + _batchesQueue.info.totalUnstETHIdsClaimed = totalUnstETHClaimed; + _batchesQueue.info.totalUnstETHIdsCount = totalUnstETHCount; + + bool res = _batchesQueue.isAllBatchesClaimed(); + assert(res == false); + } + // --- // getTotalUnclaimedUnstETHIdsCount() // --- @@ -548,6 +649,13 @@ contract WithdrawalsBatchesQueueTest is UnitTest { assertEq(totalUnclaimed, 0); } + function testFuzz_getTotalUnclaimedUnstETHIdsCount_RevertOn_AccountingError_IncorrectTotals() external { + _batchesQueue.info.totalUnstETHIdsClaimed = 1; + + vm.expectRevert(stdError.arithmeticError); + _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + } + // --- // isAllBatchesClaimed() // --- From c5906b87debc538f96ac1ba60c53641518b194d5 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 6 Dec 2024 13:30:10 +0400 Subject: [PATCH 091/107] Add unit tests for Escrow contract --- test/mocks/WithdrawalQueueMock.sol | 126 +- test/mocks/WstETHMock.sol | 35 + test/unit/Escrow.t.sol | 1534 ++++++++++++++++- .../libraries/WithdrawalBatchesQueue.t.sol | 8 +- 4 files changed, 1634 insertions(+), 69 deletions(-) create mode 100644 test/mocks/WstETHMock.sol diff --git a/test/mocks/WithdrawalQueueMock.sol b/test/mocks/WithdrawalQueueMock.sol index 06cd5253..01d4053e 100644 --- a/test/mocks/WithdrawalQueueMock.sol +++ b/test/mocks/WithdrawalQueueMock.sol @@ -1,18 +1,29 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -// import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; /*, ERC721("test", "test")*/ -import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {ETHValues, sendTo} from "contracts/types/ETHValue.sol"; /* solhint-disable no-unused-vars,custom-errors */ contract WithdrawalQueueMock is IWithdrawalQueue { + error InvalidRequestId(); + error ArraysLengthMismatch(); + + uint256 public immutable REVERT_ON_ID = type(uint256).max; + uint256 private _lastRequestId; uint256 private _lastFinalizedRequestId; uint256 private _minStETHWithdrawalAmount; uint256 private _maxStETHWithdrawalAmount; + uint256 private _claimableAmount; + uint256 private _requestWithdrawalsTransferAmount; + uint256 private _lastCheckpointIndex; + uint256[] private _getClaimableEtherResult; uint256[] private _requestWithdrawalsResult; IERC20 private _stETH; + uint256[] private _checkpointHints; + WithdrawalRequestStatus[] private _withdrawalRequestsStatuses; constructor(IERC20 stETH) { _stETH = stETH; @@ -27,7 +38,17 @@ contract WithdrawalQueueMock is IWithdrawalQueue { } function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external { - revert("Not Implemented"); + if (requestIds.length != hints.length) { + revert ArraysLengthMismatch(); + } + + if (_claimableAmount == 0) { + return; + } + + ETHValues.from(_claimableAmount).sendTo(payable(msg.sender)); + + setClaimableAmount(0); } function getLastRequestId() external view returns (uint256) { @@ -38,36 +59,35 @@ contract WithdrawalQueueMock is IWithdrawalQueue { return _lastFinalizedRequestId; } - function getWithdrawalStatus(uint256[] calldata _requestIds) + function getWithdrawalStatus(uint256[] calldata /* _requestIds */ ) external view - returns (IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses) + returns (WithdrawalRequestStatus[] memory) { - revert("Not Implemented"); + return _withdrawalRequestsStatuses; } - /// @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"); + uint256[] calldata /* _hints */ + ) external view returns (uint256[] memory) { + if (_requestIds.length > 0 && REVERT_ON_ID == _requestIds[0]) { + revert InvalidRequestId(); + } + + return _getClaimableEtherResult; } function findCheckpointHints( - uint256[] calldata _requestIds, - uint256 _firstIndex, - uint256 _lastIndex - ) external view returns (uint256[] memory hintIds) { - revert("Not Implemented"); + uint256[] calldata, /* _requestIds */ + uint256, /* _firstIndex */ + uint256 /* _lastIndex */ + ) external view returns (uint256[] memory) { + return _checkpointHints; } function getLastCheckpointIndex() external view returns (uint256) { - revert("Not Implemented"); + return _lastCheckpointIndex; } function requestWithdrawals( @@ -84,47 +104,59 @@ contract WithdrawalQueueMock is IWithdrawalQueue { } totalAmount += _amounts[i]; } - IERC20(_stETH).transferFrom(_owner, address(this), totalAmount); + + if (_requestWithdrawalsTransferAmount > 0) { + _stETH.transferFrom(_owner, address(this), _requestWithdrawalsTransferAmount); + setRequestWithdrawalsTransferAmount(0); + } else { + if (totalAmount > 0) { + _stETH.transferFrom(_owner, address(this), totalAmount); + } + } + return _requestWithdrawalsResult; } - function balanceOf(address owner) external view returns (uint256 balance) { + function balanceOf(address /* owner */ ) external pure returns (uint256 /* balance */ ) { revert("Not Implemented"); } - function ownerOf(uint256 tokenId) external view returns (address owner) { + function ownerOf(uint256 /* tokenId */ ) external pure returns (address /* owner */ ) { revert("Not Implemented"); } - function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external { + function safeTransferFrom( + address, /* from */ + address, /* to */ + uint256, /* tokenId */ + bytes calldata /* data */ + ) external pure { revert("Not Implemented"); } - function safeTransferFrom(address from, address to, uint256 tokenId) external { + function safeTransferFrom(address, /* from */ address, /* to */ uint256 /* tokenId */ ) external pure { revert("Not Implemented"); } - function transferFrom(address from, address to, uint256 tokenId) external { - revert("Not Implemented"); - } + function transferFrom(address, /* from */ address, /* to */ uint256 /* tokenId */ ) external pure {} - function approve(address to, uint256 tokenId) external { + function approve(address, /* to */ uint256 /* tokenId */ ) external pure { revert("Not Implemented"); } - function setApprovalForAll(address operator, bool approved) external { + function setApprovalForAll(address, /* operator */ bool /* approved */ ) external pure { revert("Not Implemented"); } - function getApproved(uint256 tokenId) external view returns (address operator) { + function getApproved(uint256 /* tokenId */ ) external pure returns (address /* operator */ ) { revert("Not Implemented"); } - function isApprovedForAll(address owner, address operator) external view returns (bool) { + function isApprovedForAll(address, /* owner */ address /* operator */ ) external pure returns (bool) { revert("Not Implemented"); } - function supportsInterface(bytes4 interfaceId) external view returns (bool) { + function supportsInterface(bytes4 /* interfaceId */ ) external pure returns (bool) { revert("Not Implemented"); } @@ -147,4 +179,32 @@ contract WithdrawalQueueMock is IWithdrawalQueue { function setRequestWithdrawalsResult(uint256[] memory requestIds) public { _requestWithdrawalsResult = requestIds; } + + function setClaimableAmount(uint256 claimableAmount) public { + _claimableAmount = claimableAmount; + } + + function setRequestWithdrawalsTransferAmount(uint256 requestWithdrawalsTransferAmount) public { + _requestWithdrawalsTransferAmount = requestWithdrawalsTransferAmount; + } + + function setWithdrawalRequestsStatuses(WithdrawalRequestStatus[] memory statuses) public { + delete _withdrawalRequestsStatuses; + + for (uint256 i = 0; i < statuses.length; ++i) { + _withdrawalRequestsStatuses.push(statuses[i]); + } + } + + function setClaimableEtherResult(uint256[] memory claimableEther) public { + _getClaimableEtherResult = claimableEther; + } + + function setLastCheckpointIndex(uint256 index) public { + _lastCheckpointIndex = index; + } + + function setCheckpointHints(uint256[] memory hints) public { + _checkpointHints = hints; + } } diff --git a/test/mocks/WstETHMock.sol b/test/mocks/WstETHMock.sol new file mode 100644 index 00000000..569dca54 --- /dev/null +++ b/test/mocks/WstETHMock.sol @@ -0,0 +1,35 @@ +// 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"; +import {IWstETH} from "contracts/interfaces/IWstETH.sol"; + +/* solhint-disable no-unused-vars,custom-errors */ +contract WstETHMock is ERC20Mock, IWstETH { + IStETH public stETH; + + constructor(IStETH _stETH) { + stETH = _stETH; + } + + function wrap(uint256 stETHAmount) external returns (uint256) { + require(stETHAmount > 0, "wstETH: can't wrap zero stETH"); + uint256 wstETHAmount = stETH.getSharesByPooledEth(stETHAmount); + _mint(msg.sender, wstETHAmount); + stETH.transferFrom(msg.sender, address(this), stETHAmount); + return wstETHAmount; + } + + function unwrap(uint256 wstETHAmount) external returns (uint256) { + require(wstETHAmount > 0, "wstETH: zero amount unwrap not allowed"); + uint256 stETHAmount = stETH.getPooledEthByShares(wstETHAmount); + _burn(msg.sender, wstETHAmount); + stETH.transfer(msg.sender, stETHAmount); + return stETHAmount; + } + + function getStETHByWstETH(uint256 wstethAmount) external view returns (uint256) { + return stETH.getPooledEthByShares(wstethAmount); + } +} diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index 0f1a17ce..f22cea67 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -3,27 +3,33 @@ pragma solidity 0.8.26; import {stdError} from "forge-std/StdError.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; -import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {ETHValues, sendTo} from "contracts/types/ETHValue.sol"; +import {ETHValues} from "contracts/types/ETHValue.sol"; import {SharesValues} from "contracts/types/SharesValue.sol"; import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; import {Escrow} from "contracts/Escrow.sol"; -import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; +import {EscrowState as EscrowStateLib, State as EscrowState} from "contracts/libraries/EscrowState.sol"; +import {WithdrawalsBatchesQueue} from "contracts/libraries/WithdrawalsBatchesQueue.sol"; +import {AssetsAccounting, UnstETHRecordStatus} from "contracts/libraries/AssetsAccounting.sol"; +import {IEscrow} from "contracts/interfaces/IEscrow.sol"; import {IStETH} from "contracts/interfaces/IStETH.sol"; import {IWstETH} from "contracts/interfaces/IWstETH.sol"; import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; -import {StETHMock} from "test/mocks/StETHMock.sol"; +import {StETHMock} from "scripts/lido-mocks/StETHMock.sol"; +import {WstETHMock} from "test/mocks/WstETHMock.sol"; import {WithdrawalQueueMock} from "test/mocks/WithdrawalQueueMock.sol"; import {UnitTest} from "test/utils/unit-test.sol"; +import {Random} from "test/utils/random.sol"; contract EscrowUnitTests is UnitTest { + Random.Context private _random; address private _dualGovernance = makeAddr("dualGovernance"); address private _vetoer = makeAddr("vetoer"); @@ -31,7 +37,7 @@ contract EscrowUnitTests is UnitTest { Escrow private _escrow; StETHMock private _stETH; - IWstETH private _wstETH; + WstETHMock private _wstETH; WithdrawalQueueMock private _withdrawalQueue; @@ -39,30 +45,1206 @@ contract EscrowUnitTests is UnitTest { uint256 private stethAmount = 100 ether; function setUp() external { + _random = Random.create(block.timestamp); _stETH = new StETHMock(); - _stETH.__setShareRate(1); - _wstETH = IWstETH(address(new ERC20Mock())); + _wstETH = new WstETHMock(_stETH); _withdrawalQueue = new WithdrawalQueueMock(_stETH); _withdrawalQueue.setMaxStETHWithdrawalAmount(1_000 ether); - _masterCopy = - new Escrow(_stETH, _wstETH, WithdrawalQueueMock(_withdrawalQueue), IDualGovernance(_dualGovernance), 100); - _escrow = Escrow(payable(Clones.clone(address(_masterCopy)))); + _masterCopy = _createEscrow(100); + _escrow = + _createInitializedEscrowProxy({minWithdrawalsBatchSize: 100, minAssetsLockDuration: _minLockAssetDuration}); vm.prank(address(_escrow)); _stETH.approve(address(_withdrawalQueue), type(uint256).max); + vm.startPrank(_vetoer); + _stETH.approve(address(_escrow), type(uint256).max); + _stETH.approve(address(_wstETH), type(uint256).max); + _wstETH.approve(address(_escrow), type(uint256).max); + vm.stopPrank(); + + _stETH.mint(_vetoer, stethAmount); + _wstETH.mint(_vetoer, stethAmount); + + vm.mockCall( + _dualGovernance, abi.encodeWithSelector(IDualGovernance.activateNextState.selector), abi.encode(true) + ); + _withdrawalQueue.setMinStETHWithdrawalAmount(100); + _withdrawalQueue.setMaxStETHWithdrawalAmount(1000 * 1e18); + + vm.label(address(_escrow), "Escrow"); + vm.label(address(_stETH), "StETHMock"); + vm.label(address(_wstETH), "WstETHMock"); + vm.label(address(_withdrawalQueue), "WithdrawalQueueMock"); + } + + /* */ + + // --- + // constructor() + // --- + + function testFuzz_constructor( + address steth, + address wsteth, + address withdrawalQueue, + address dualGovernance, + uint256 size + ) external { + Escrow instance = new Escrow( + IStETH(steth), IWstETH(wsteth), IWithdrawalQueue(withdrawalQueue), IDualGovernance(dualGovernance), size + ); + + assertEq(address(instance.ST_ETH()), address(steth)); + assertEq(address(instance.WST_ETH()), address(wsteth)); + assertEq(address(instance.WITHDRAWAL_QUEUE()), address(withdrawalQueue)); + assertEq(address(instance.DUAL_GOVERNANCE()), address(dualGovernance)); + assertEq(instance.MIN_WITHDRAWALS_BATCH_SIZE(), size); + } + + // --- + // initialize() + // --- + + function test_initialize_HappyPath() external { + vm.expectEmit(); + emit EscrowStateLib.EscrowStateChanged(EscrowState.NotInitialized, EscrowState.SignallingEscrow); + vm.expectEmit(); + emit EscrowStateLib.MinAssetsLockDurationSet(Durations.ZERO); + + vm.expectCall(address(_stETH), abi.encodeCall(IERC20.approve, (address(_wstETH), type(uint256).max))); + vm.expectCall(address(_stETH), abi.encodeCall(IERC20.approve, (address(_withdrawalQueue), type(uint256).max))); + + _createInitializedEscrowProxy({minWithdrawalsBatchSize: 100, minAssetsLockDuration: Durations.ZERO}); + + assertEq(_escrow.MIN_WITHDRAWALS_BATCH_SIZE(), 100); + } + + function test_initialize_RevertOn_CalledNotViaProxy() external { + Escrow instance = _createEscrow(100); + + vm.expectRevert(Escrow.NonProxyCallsForbidden.selector); + instance.initialize(Durations.ZERO); + } + + function testFuzz_initialize_RevertOn_CalledNotFromDualGovernance(address stranger) external { + vm.assume(stranger != _dualGovernance); + IEscrow instance = _createEscrowProxy(100); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(Escrow.CallerIsNotDualGovernance.selector, stranger)); + instance.initialize(Durations.ZERO); + } + + // --- + // lockStETH() + // --- + + function test_lockStETH_HappyPath() external { + uint256 amount = 1 ether; + + uint256 sharesAmount = _stETH.getSharesByPooledEth(amount); + uint256 vetoerBalanceBefore = _stETH.balanceOf(_vetoer); + uint256 escrowBalanceBefore = _stETH.balanceOf(address(_escrow)); + + vm.expectCall( + address(_stETH), + abi.encodeCall(IStETH.transferSharesFrom, (address(_vetoer), address(_escrow), sharesAmount)) + ); + vm.expectCall(address(_dualGovernance), abi.encodeCall(IDualGovernance.activateNextState, ()), 2); + + vm.prank(_vetoer); + uint256 lockedStETHShares = _escrow.lockStETH(amount); + + assertEq(lockedStETHShares, sharesAmount); + + uint256 vetoerBalanceAfter = _stETH.balanceOf(_vetoer); + uint256 escrowBalanceAfter = _stETH.balanceOf(address(_escrow)); + + assertEq(vetoerBalanceAfter, vetoerBalanceBefore - amount); + assertEq(escrowBalanceAfter, escrowBalanceBefore + amount); + + IEscrow.LockedAssetsTotals memory escrowLockedAssets = _escrow.getLockedAssetsTotals(); + + assertEq(escrowLockedAssets.stETHLockedShares, lockedStETHShares); + assertEq(escrowLockedAssets.stETHClaimedETH, 0); + assertEq(escrowLockedAssets.unstETHUnfinalizedShares, 0); + assertEq(escrowLockedAssets.unstETHFinalizedETH, 0); + + IEscrow.VetoerState memory state = _escrow.getVetoerState(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares, lockedStETHShares); + assertEq(state.unstETHLockedShares, 0); + assertEq(state.lastAssetsLockTimestamp, Timestamps.now().toSeconds()); + } + + function test_lockStETH_RevertOn_UnexpectedEscrowState() external { + _transitToRageQuit(); + + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.SignallingEscrow)); + vm.prank(_vetoer); + _escrow.lockStETH(1 ether); + + assertEq(_stETH.balanceOf(address(_escrow)), 0); + } + + // --- + // unlockStETH() + // --- + + function test_unlockStETH_HappyPath() external { + uint256 amount = 1 ether; + uint256 sharesAmount = _stETH.getSharesByPooledEth(amount); + + vm.startPrank(_vetoer); + _escrow.lockStETH(amount); + + uint256 vetoerBalanceBefore = _stETH.balanceOf(_vetoer); + uint256 escrowBalanceBefore = _stETH.balanceOf(address(_escrow)); + + _wait(_minLockAssetDuration.plusSeconds(1)); + + vm.expectCall(address(_stETH), abi.encodeCall(IStETH.transferShares, (address(_vetoer), sharesAmount))); + vm.expectCall(address(_dualGovernance), abi.encodeCall(IDualGovernance.activateNextState, ()), 2); + uint256 unlockedStETHShares = _escrow.unlockStETH(); + assertEq(unlockedStETHShares, sharesAmount); + + uint256 vetoerBalanceAfter = _stETH.balanceOf(_vetoer); + uint256 escrowBalanceAfter = _stETH.balanceOf(address(_escrow)); + + assertEq(vetoerBalanceAfter, vetoerBalanceBefore + amount); + assertEq(escrowBalanceAfter, escrowBalanceBefore - amount); + + IEscrow.LockedAssetsTotals memory escrowLockedAssets = _escrow.getLockedAssetsTotals(); + + assertEq(escrowLockedAssets.stETHLockedShares, 0); + assertEq(escrowLockedAssets.stETHClaimedETH, 0); + assertEq(escrowLockedAssets.unstETHUnfinalizedShares, 0); + assertEq(escrowLockedAssets.unstETHFinalizedETH, 0); + + IEscrow.VetoerState memory state = _escrow.getVetoerState(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares, 0); + assertEq(state.unstETHLockedShares, 0); + } + + function test_unlockStETH_RevertOn_UnexpectedEscrowState() external { + vm.prank(_vetoer); + _escrow.lockStETH(1 ether); + + _transitToRageQuit(); + + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.SignallingEscrow)); + vm.prank(_vetoer); + _escrow.unlockStETH(); + } + + function test_unlockStETH_RevertOn_MinAssetsLockDurationNotPassed() external { + vm.startPrank(_vetoer); + _escrow.lockStETH(1 ether); + + uint256 lastLockTimestamp = block.timestamp; + + _wait(_minLockAssetDuration.minusSeconds(1)); + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.MinAssetsLockDurationNotPassed.selector, + Durations.from(lastLockTimestamp) + _minLockAssetDuration + ) + ); + _escrow.unlockStETH(); + } + + // --- + // lockWstETH() + // --- + + function test_lockWstETH_HappyPath() external { + uint256 amount = 1 ether; + + vm.startPrank(_vetoer); + _wstETH.wrap(amount); + + uint256 vetoerWStBalanceBefore = _wstETH.balanceOf(_vetoer); + uint256 escrowBalanceBefore = _stETH.balanceOf(address(_escrow)); + + vm.expectCall(address(_wstETH), abi.encodeCall(IERC20.transferFrom, (_vetoer, address(_escrow), amount))); + vm.expectCall(address(_dualGovernance), abi.encodeCall(IDualGovernance.activateNextState, ()), 2); + + uint256 lockedStETHShares = _escrow.lockWstETH(amount); + assertEq(lockedStETHShares, _stETH.getSharesByPooledEth(amount)); + + uint256 vetoerWStBalanceAfter = _wstETH.balanceOf(_vetoer); + uint256 escrowBalanceAfter = _stETH.balanceOf(address(_escrow)); + + assertEq(vetoerWStBalanceAfter, vetoerWStBalanceBefore - lockedStETHShares); + assertEq(escrowBalanceAfter, escrowBalanceBefore + lockedStETHShares); + + IEscrow.LockedAssetsTotals memory escrowLockedAssets = _escrow.getLockedAssetsTotals(); + + assertEq(escrowLockedAssets.stETHLockedShares, lockedStETHShares); + assertEq(escrowLockedAssets.stETHClaimedETH, 0); + assertEq(escrowLockedAssets.unstETHUnfinalizedShares, 0); + assertEq(escrowLockedAssets.unstETHFinalizedETH, 0); + + IEscrow.VetoerState memory state = _escrow.getVetoerState(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares, lockedStETHShares); + assertEq(state.unstETHLockedShares, 0); + assertEq(state.lastAssetsLockTimestamp, Timestamps.now().toSeconds()); + } + + function test_lockWstETH_RevertOn_UnexpectedEscrowState() external { + _transitToRageQuit(); + + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.SignallingEscrow)); + vm.prank(_vetoer); + _escrow.lockWstETH(1 ether); + } + + // --- + // unlockWstETH() + // --- + + function test_unlockWstETH_HappyPath() external { + uint256 amount = 1 ether; + + vm.startPrank(_vetoer); + _wstETH.wrap(amount); + + _escrow.lockWstETH(amount); + + _wait(_minLockAssetDuration.plusSeconds(1)); + + uint256 vetoerWStBalanceBefore = _wstETH.balanceOf(_vetoer); + uint256 escrowBalanceBefore = _stETH.balanceOf(address(_escrow)); + + vm.expectCall(address(_wstETH), abi.encodeCall(IWstETH.wrap, (amount))); + vm.expectCall(address(_wstETH), abi.encodeCall(IERC20.transfer, (_vetoer, amount))); + vm.expectCall(address(_dualGovernance), abi.encodeCall(IDualGovernance.activateNextState, ()), 2); + uint256 unlockedStETHShares = _escrow.unlockWstETH(); + + assertEq(unlockedStETHShares, _stETH.getPooledEthByShares(amount)); + + uint256 vetoerWStBalanceAfter = _wstETH.balanceOf(_vetoer); + uint256 escrowBalanceAfter = _stETH.balanceOf(address(_escrow)); + + assertEq(vetoerWStBalanceAfter, vetoerWStBalanceBefore + unlockedStETHShares); + assertEq(escrowBalanceAfter, escrowBalanceBefore - unlockedStETHShares); + + IEscrow.LockedAssetsTotals memory escrowLockedAssets = _escrow.getLockedAssetsTotals(); + + assertEq(escrowLockedAssets.stETHLockedShares, 0); + assertEq(escrowLockedAssets.stETHClaimedETH, 0); + assertEq(escrowLockedAssets.unstETHUnfinalizedShares, 0); + assertEq(escrowLockedAssets.unstETHFinalizedETH, 0); + + IEscrow.VetoerState memory state = _escrow.getVetoerState(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares, 0); + assertEq(state.unstETHLockedShares, 0); + } + + function test_unlockWstETH_RevertOn_UnexpectedEscrowState() external { + uint256 amount = 1 ether; + + vm.startPrank(_vetoer); + _wstETH.wrap(amount); + _escrow.lockWstETH(amount); + vm.stopPrank(); + + _transitToRageQuit(); + + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.SignallingEscrow)); + vm.prank(_vetoer); + _escrow.unlockWstETH(); + } + + function test_unlockWstETH_RevertOn_MinAssetsLockDurationNotPassed() external { + uint256 amount = 1 ether; + + vm.startPrank(_vetoer); + _wstETH.wrap(amount); + _escrow.lockWstETH(amount); + + uint256 lastLockTimestamp = block.timestamp; + + _wait(_minLockAssetDuration.minusSeconds(1)); + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.MinAssetsLockDurationNotPassed.selector, + Durations.from(lastLockTimestamp) + _minLockAssetDuration + ) + ); + _escrow.unlockWstETH(); + } + + // --- + // lockUnstETH() + // --- + + function test_lockUnstETH_HappyPath() external { + uint256[] memory unstethIds = new uint256[](2); + unstethIds[0] = 1; + unstethIds[1] = 2; + + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = new IWithdrawalQueue.WithdrawalRequestStatus[](2); + statuses[0] = IWithdrawalQueue.WithdrawalRequestStatus(1 ether, 1 ether, _vetoer, block.timestamp, false, false); + statuses[1] = IWithdrawalQueue.WithdrawalRequestStatus(2 ether, 2 ether, _vetoer, block.timestamp, false, false); + + _withdrawalQueue.setWithdrawalRequestsStatuses(statuses); + + vm.expectCall( + address(_withdrawalQueue), + abi.encodeCall(IWithdrawalQueue.transferFrom, (_vetoer, address(_escrow), unstethIds[0])) + ); + vm.expectCall( + address(_withdrawalQueue), + abi.encodeCall(IWithdrawalQueue.transferFrom, (_vetoer, address(_escrow), unstethIds[1])) + ); + vm.expectCall(address(_dualGovernance), abi.encodeCall(IDualGovernance.activateNextState, ()), 2); + + vm.prank(_vetoer); + _escrow.lockUnstETH(unstethIds); + + IEscrow.LockedAssetsTotals memory escrowLockedAssets = _escrow.getLockedAssetsTotals(); + + assertEq(escrowLockedAssets.stETHLockedShares, 0); + assertEq(escrowLockedAssets.stETHClaimedETH, 0); + assertEq(escrowLockedAssets.unstETHUnfinalizedShares, statuses[0].amountOfShares + statuses[1].amountOfShares); + assertEq(escrowLockedAssets.unstETHFinalizedETH, 0); + + IEscrow.VetoerState memory state = _escrow.getVetoerState(_vetoer); + + assertEq(state.unstETHIdsCount, 2); + assertEq(state.stETHLockedShares, 0); + assertEq(state.unstETHLockedShares, statuses[0].amountOfShares + statuses[1].amountOfShares); + } + + function test_lockUnstETH_RevertOn_EmptyUnstETHIds() external { + uint256[] memory unstethIds = new uint256[](0); + + vm.expectRevert(abi.encodeWithSelector(Escrow.EmptyUnstETHIds.selector)); + _escrow.lockUnstETH(unstethIds); + } + + function test_lockUnstETH_RevertOn_UnexpectedEscrowState() external { + uint256[] memory unstethIds = new uint256[](1); + unstethIds[0] = 1; + + _transitToRageQuit(); + + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.SignallingEscrow)); + _escrow.lockUnstETH(unstethIds); + } + + // --- + // unlockUnstETH() + // --- + + function test_unlockUnstETH_HappyPath() external { + uint256[] memory unstETHAmounts = new uint256[](2); + unstETHAmounts[0] = 1 ether; + unstETHAmounts[1] = 2 ether; + + uint256[] memory unstethIds = _vetoerLockedUnstEth(unstETHAmounts); + + _wait(_minLockAssetDuration.plusSeconds(1)); + + vm.expectCall( + address(_withdrawalQueue), + abi.encodeCall(IWithdrawalQueue.transferFrom, (address(_escrow), _vetoer, unstethIds[0])) + ); + vm.expectCall( + address(_withdrawalQueue), + abi.encodeCall(IWithdrawalQueue.transferFrom, (address(_escrow), _vetoer, unstethIds[1])) + ); + vm.expectCall(address(_dualGovernance), abi.encodeCall(IDualGovernance.activateNextState, ()), 2); + + vm.prank(_vetoer); + _escrow.unlockUnstETH(unstethIds); + + IEscrow.LockedAssetsTotals memory escrowLockedAssets = _escrow.getLockedAssetsTotals(); + + assertEq(escrowLockedAssets.stETHLockedShares, 0); + assertEq(escrowLockedAssets.stETHClaimedETH, 0); + assertEq(escrowLockedAssets.unstETHUnfinalizedShares, 0); + assertEq(escrowLockedAssets.unstETHFinalizedETH, 0); + + IEscrow.VetoerState memory state = _escrow.getVetoerState(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares, 0); + assertEq(state.unstETHLockedShares, 0); + } + + function test_unlockUnstETH_EmptyUnstETHIds() external { + uint256[] memory unstethIds = new uint256[](0); + + _wait(_minLockAssetDuration.plusSeconds(1)); + + vm.expectCall(address(_dualGovernance), abi.encodeCall(IDualGovernance.activateNextState, ()), 0); + + vm.expectRevert(Escrow.EmptyUnstETHIds.selector); + vm.prank(_vetoer); + _escrow.unlockUnstETH(unstethIds); + } + + function test_unlockUnstETH_RevertOn_MinAssetsLockDurationNotPassed() external { + uint256[] memory unstethIds = new uint256[](1); + + _wait(_minLockAssetDuration.minusSeconds(1)); + + // Exception. Due to no assets of holder registered in Escrow. + vm.expectRevert( + abi.encodeWithSelector(AssetsAccounting.MinAssetsLockDurationNotPassed.selector, _minLockAssetDuration) + ); + vm.prank(_vetoer); + _escrow.unlockUnstETH(unstethIds); + } + + function test_unlockUnstETH_RevertOn_UnexpectedEscrowState() external { + _transitToRageQuit(); + + uint256[] memory unstethIds = new uint256[](1); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.SignallingEscrow)); + vm.prank(_vetoer); + _escrow.unlockUnstETH(unstethIds); + } + + // --- + // markUnstETHFinalized() + // --- + + function test_markUnstETHFinalized_HappyPath() external { + uint256[] memory unstethIds = new uint256[](2); + uint256[] memory hints = new uint256[](2); + uint256[] memory responses = new uint256[](2); + + unstethIds[0] = 1; + unstethIds[1] = 1; + + hints[0] = 1; + hints[1] = 1; + + responses[0] = 1 ether; + responses[1] = 1 ether; + + _withdrawalQueue.setClaimableEtherResult(responses); + vm.expectCall( + address(_withdrawalQueue), abi.encodeCall(IWithdrawalQueue.getClaimableEther, (unstethIds, hints)) + ); + + _escrow.markUnstETHFinalized(unstethIds, hints); + } + + function test_markUnstETHFinalized_RevertOn_UnexpectedEscrowState() external { + _transitToRageQuit(); + + uint256[] memory unstethIds = new uint256[](0); + uint256[] memory hints = new uint256[](0); + + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.SignallingEscrow)); + _escrow.markUnstETHFinalized(unstethIds, hints); + } + + // --- + // startRageQuit() + // --- + + function test_startRageQuit_HappyPath() external { + uint256 lri = Random.nextUint256(_random, 100500); + _withdrawalQueue.setLastRequestId(lri); + + vm.expectEmit(); + emit EscrowStateLib.RageQuitStarted(Durations.ZERO, Durations.ZERO); + vm.expectEmit(); + emit WithdrawalsBatchesQueue.WithdrawalsBatchesQueueOpened(lri); + + _transitToRageQuit(); + } + + function testFuzz_startRageQuit_RevertOn_CalledNotByDualGovernance(address stranger) external { + vm.assume(stranger != _dualGovernance); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(Escrow.CallerIsNotDualGovernance.selector, stranger)); + _escrow.startRageQuit(Durations.ZERO, Durations.ZERO); + } + + // --- + // requestNextWithdrawalsBatch() + // --- + + function test_requestNextWithdrawalsBatch_HappyPath() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.WithdrawalsBatchesQueueOpened(unstEthIds[0] - 1); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _stETH.mint(address(_escrow), stethAmount); + _withdrawalQueue.setMinStETHWithdrawalAmount(1); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.UnstETHIdsAdded(unstEthIds); + vm.expectEmit(); + emit WithdrawalsBatchesQueue.WithdrawalsBatchesQueueClosed(); + _escrow.requestNextWithdrawalsBatch(100); + } + + function test_requestNextWithdrawalsBatch_ReturnsEarlyAndClosesWithdrawalsBatchesQueue_When_EscrowHasZeroAmountOfStETH( + ) external { + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(new uint256[](0)); + + _ensureWithdrawalsBatchesQueueClosed(); + } + + function test_requestNextWithdrawalsBatch_RevertOn_UnexpectedEscrowState() external { + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + _escrow.requestNextWithdrawalsBatch(1); + } + + function test_requestNextWithdrawalsBatch_RevertOn_InvalidBatchSize() external { + _transitToRageQuit(); + + uint256 batchSize = 1; + + vm.expectRevert(abi.encodeWithSelector(Escrow.InvalidBatchSize.selector, batchSize)); + _escrow.requestNextWithdrawalsBatch(batchSize); + } + + function test_requestNextWithdrawalsBatch_RevertOn_InvalidUnstETHIdsSequence() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.WithdrawalsBatchesQueueOpened(unstEthIds[0] - 1); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _stETH.mint(address(_escrow), stethAmount); + _withdrawalQueue.setRequestWithdrawalsTransferAmount( + stethAmount - 2 * _escrow.MIN_TRANSFERRABLE_ST_ETH_AMOUNT() + ); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.UnstETHIdsAdded(unstEthIds); + _escrow.requestNextWithdrawalsBatch(100); + + vm.expectRevert(WithdrawalsBatchesQueue.InvalidUnstETHIdsSequence.selector); + _escrow.requestNextWithdrawalsBatch(100); + } + + // --- + // claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) + // --- + + function test_claimNextWithdrawalsBatch_2_HappyPath() external { + IEscrow.LockedAssetsTotals memory escrowLockedAssets = _escrow.getLockedAssetsTotals(); + + assertEq(escrowLockedAssets.stETHLockedShares, 0); + assertEq(escrowLockedAssets.stETHClaimedETH, 0); + assertEq(escrowLockedAssets.unstETHUnfinalizedShares, 0); + assertEq(escrowLockedAssets.unstETHFinalizedETH, 0); + + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + _withdrawalQueue.setLastCheckpointIndex(1); + _withdrawalQueue.setCheckpointHints(new uint256[](unstEthIds.length)); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + + _withdrawalQueue.setClaimableAmount(stethAmount); + vm.deal(address(_withdrawalQueue), stethAmount); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.UnstETHIdsClaimed(unstEthIds); + vm.expectEmit(); + emit AssetsAccounting.ETHClaimed(ETHValues.from(stethAmount)); + _escrow.claimNextWithdrawalsBatch(unstEthIds[0], new uint256[](unstEthIds.length)); + + escrowLockedAssets = _escrow.getLockedAssetsTotals(); + + assertEq(escrowLockedAssets.stETHLockedShares, stethAmount); + assertEq(escrowLockedAssets.stETHClaimedETH, stethAmount); + assertEq(escrowLockedAssets.unstETHUnfinalizedShares, 0); + assertEq(escrowLockedAssets.unstETHFinalizedETH, 0); + + IEscrow.VetoerState memory state = _escrow.getVetoerState(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares, stethAmount); + assertEq(state.unstETHLockedShares, 0); + } + + function test_claimNextWithdrawalsBatch_2_RevertOn_UnexpectedState() external { + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, 2)); + _escrow.claimNextWithdrawalsBatch(1, new uint256[](1)); + } + + function test_claimNextWithdrawalsBatch_2_RevertOn_ClaimingIsFinished() external { + _transitToRageQuit(); + + _escrow.requestNextWithdrawalsBatch(100); + _escrow.startRageQuitExtensionPeriod(); + + vm.expectRevert(EscrowStateLib.ClaimingIsFinished.selector); + _escrow.claimNextWithdrawalsBatch(1, new uint256[](1)); + } + + function test_claimNextWithdrawalsBatch_2_RevertOn_EmptyBatch() external { + _transitToRageQuit(); + + vm.expectRevert(WithdrawalsBatchesQueue.EmptyBatch.selector); + _escrow.claimNextWithdrawalsBatch(1, new uint256[](1)); + } + + function test_claimNextWithdrawalsBatch_2_RevertOn_UnexpectedUnstETHId() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + + _withdrawalQueue.setClaimableAmount(stethAmount); + vm.deal(address(_withdrawalQueue), stethAmount); + + vm.expectRevert(Escrow.UnexpectedUnstETHId.selector); + _escrow.claimNextWithdrawalsBatch(unstEthIds[0] + 10, new uint256[](1)); + } + + function test_claimNextWithdrawalsBatch_2_RevertOn_InvalidHintsLength() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + + _withdrawalQueue.setClaimableAmount(stethAmount); + vm.deal(address(_withdrawalQueue), stethAmount); + + vm.expectRevert(abi.encodeWithSelector(Escrow.InvalidHintsLength.selector, 10, 1)); + _escrow.claimNextWithdrawalsBatch(unstEthIds[0], new uint256[](10)); + } + + // --- + // claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) + // --- + + function test_claimNextWithdrawalsBatch_1_HappyPath() external { + IEscrow.LockedAssetsTotals memory escrowLockedAssets = _escrow.getLockedAssetsTotals(); + + assertEq(escrowLockedAssets.stETHLockedShares, 0); + assertEq(escrowLockedAssets.stETHClaimedETH, 0); + assertEq(escrowLockedAssets.unstETHUnfinalizedShares, 0); + assertEq(escrowLockedAssets.unstETHFinalizedETH, 0); + + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + + IEscrow.VetoerState memory vetoerState = _escrow.getVetoerState(_vetoer); + + assertEq(vetoerState.unstETHIdsCount, 0); + assertEq(vetoerState.stETHLockedShares, stethAmount); + assertEq(vetoerState.unstETHLockedShares, 0); + + _claimStEthViaWQ(unstEthIds, stethAmount); + + escrowLockedAssets = _escrow.getLockedAssetsTotals(); + + assertEq(escrowLockedAssets.stETHLockedShares, stethAmount); + assertEq(escrowLockedAssets.stETHClaimedETH, stethAmount); + assertEq(escrowLockedAssets.unstETHUnfinalizedShares, 0); + assertEq(escrowLockedAssets.unstETHFinalizedETH, 0); + + vetoerState = _escrow.getVetoerState(_vetoer); + + assertEq(vetoerState.unstETHIdsCount, 0); + assertEq(vetoerState.stETHLockedShares, stethAmount); + assertEq(vetoerState.unstETHLockedShares, 0); + } + + function test_claimNextWithdrawalsBatch_1_RevertOn_UnexpectedState() external { + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, 2)); + _escrow.claimNextWithdrawalsBatch(1); + } + + function test_claimNextWithdrawalsBatch_1_RevertOn_ClaimingIsFinished() external { + _transitToRageQuit(); + + _escrow.requestNextWithdrawalsBatch(100); + _escrow.startRageQuitExtensionPeriod(); + + vm.expectRevert(EscrowStateLib.ClaimingIsFinished.selector); + _escrow.claimNextWithdrawalsBatch(1); + } + + function test_claimNextWithdrawalsBatch_1_RevertOn_EmptyBatch() external { + _transitToRageQuit(); + + vm.expectRevert(WithdrawalsBatchesQueue.EmptyBatch.selector); + _escrow.claimNextWithdrawalsBatch(1); + } + + function test_claimNextWithdrawalsBatch_1_RevertOn_InvalidHintsLength() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + _withdrawalQueue.setLastCheckpointIndex(1); + _withdrawalQueue.setCheckpointHints(new uint256[](unstEthIds.length + 10)); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + + vm.expectRevert(abi.encodeWithSelector(Escrow.InvalidHintsLength.selector, 11, 1)); + _escrow.claimNextWithdrawalsBatch(unstEthIds.length); + } + + // --- + // startRageQuitExtensionPeriod() + // --- + + function test_startRageQuitExtensionPeriod_HappyPath() external { + _transitToRageQuit(); + + _ensureWithdrawalsBatchesQueueClosed(); + + _ensureRageQuitExtensionPeriodStartedNow(); + } + + function test_startRageQuitExtensionPeriod_RevertOn_BatchesQueueIsNotClosed() external { + vm.expectRevert(Escrow.BatchesQueueIsNotClosed.selector); + _escrow.startRageQuitExtensionPeriod(); + } + + function test_startRageQuitExtensionPeriod_RevertOn_UnclaimedBatches() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + + vm.expectRevert(Escrow.UnclaimedBatches.selector); + _escrow.startRageQuitExtensionPeriod(); + } + + function test_startRageQuitExtensionPeriod_RevertOn_UnfinalizedUnstETHIds() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + _claimStEthViaWQ(unstEthIds, stethAmount); + + _withdrawalQueue.setLastFinalizedRequestId(0); + + vm.expectRevert(Escrow.UnfinalizedUnstETHIds.selector); + _escrow.startRageQuitExtensionPeriod(); + } + + // --- + // claimUnstETH() + // --- + + function test_claimUnstETH_HappyPath() external { + uint256[] memory unstEthAmounts = new uint256[](1); + unstEthAmounts[0] = 1 ether; + + uint256[] memory unstEthIds = _vetoerLockedUnstEth(unstEthAmounts); + uint256[] memory hints = _finalizeUnstEth(unstEthAmounts, unstEthIds); + + _transitToRageQuit(); + + _claimUnstEthFromEscrow(unstEthAmounts, unstEthIds, hints); + + IEscrow.LockedAssetsTotals memory escrowLockedAssets = _escrow.getLockedAssetsTotals(); + + assertEq(escrowLockedAssets.stETHLockedShares, 0); + assertEq(escrowLockedAssets.stETHClaimedETH, 0); + assertEq(escrowLockedAssets.unstETHUnfinalizedShares, 0); + assertEq(escrowLockedAssets.unstETHFinalizedETH, unstEthAmounts[0]); + + IEscrow.VetoerState memory state = _escrow.getVetoerState(_vetoer); + + assertEq(state.unstETHIdsCount, 1); + assertEq(state.stETHLockedShares, 0); + assertEq(state.unstETHLockedShares, unstEthAmounts[0]); + } + + function test_claimUnstETH_RevertOn_UnexpectedEscrowState() external { + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + _escrow.claimUnstETH(new uint256[](1), new uint256[](1)); + } + + function test_claimUnstETH_RevertOn_InvalidRequestId() external { + bytes memory wqInvalidRequestIdError = abi.encode("WithdrawalQueue.InvalidRequestId"); + uint256[] memory unstETHIds = new uint256[](1); + unstETHIds[0] = _withdrawalQueue.REVERT_ON_ID(); + uint256[] memory hints = new uint256[](1); + + _transitToRageQuit(); + + vm.expectRevert(WithdrawalQueueMock.InvalidRequestId.selector); + _escrow.claimUnstETH(unstETHIds, hints); + } + + function test_claimUnstETH_RevertOn_ArraysLengthMismatch() external { + bytes memory wqArraysLengthMismatchError = abi.encode("WithdrawalQueue.ArraysLengthMismatch"); + uint256[] memory unstETHIds = new uint256[](2); + uint256[] memory hints = new uint256[](1); + uint256[] memory responses = new uint256[](1); + responses[0] = 1 ether; + + _transitToRageQuit(); + + _withdrawalQueue.setClaimableEtherResult(responses); + + vm.expectRevert(WithdrawalQueueMock.ArraysLengthMismatch.selector); + _escrow.claimUnstETH(unstETHIds, hints); + } + + function test_claimUnstETH_RevertOn_InvalidUnstETHStatus() external { + uint256[] memory unstEthAmounts = new uint256[](1); + unstEthAmounts[0] = 1 ether; + + uint256[] memory unstEthIds = new uint256[](1); + unstEthIds[0] = Random.nextUint256(_random, 100500); + + uint256[] memory hints = _finalizeUnstEth(unstEthAmounts, unstEthIds); + + _transitToRageQuit(); + + _withdrawalQueue.setClaimableAmount(unstEthAmounts[0]); + _withdrawalQueue.setClaimableEtherResult(unstEthAmounts); + vm.deal(address(_withdrawalQueue), unstEthAmounts[0]); + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, unstEthIds[0], UnstETHRecordStatus.NotLocked + ) + ); + _escrow.claimUnstETH(unstEthIds, hints); + } + + // --- + // setMinAssetsLockDuration() + // --- + + function test_setMinAssetsLockDuration_HappyPath() external { + Duration newMinAssetsLockDuration = Durations.from(200); + vm.expectEmit(); + emit EscrowStateLib.MinAssetsLockDurationSet(newMinAssetsLockDuration); vm.prank(_dualGovernance); - _escrow.initialize(_minLockAssetDuration); + _escrow.setMinAssetsLockDuration(newMinAssetsLockDuration); + } + + function testFuzz_setMinAssetsLockDuration_RevertOn_CalledNotFromDualGovernance(address stranger) external { + vm.assume(stranger != _dualGovernance); + + Duration newMinAssetsLockDuration = Durations.from(200); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(Escrow.CallerIsNotDualGovernance.selector, stranger)); + _escrow.setMinAssetsLockDuration(newMinAssetsLockDuration); + } + + // --- + // withdrawETH() + // --- + + function test_withdrawETH_HappyPath() external { + uint256 balanceBefore = _vetoer.balance; + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + _claimStEthViaWQ(unstEthIds, stethAmount); + _ensureRageQuitExtensionPeriodStartedNow(); + assertTrue(_escrow.isRageQuitExtensionPeriodStarted()); + + _wait(Durations.from(1)); vm.startPrank(_vetoer); - ERC20Mock(address(_stETH)).approve(address(_escrow), type(uint256).max); - ERC20Mock(address(_wstETH)).approve(address(_escrow), type(uint256).max); + vm.expectEmit(); + emit AssetsAccounting.ETHWithdrawn(_vetoer, SharesValues.from(stethAmount), ETHValues.from(stethAmount)); + _escrow.withdrawETH(); vm.stopPrank(); - vm.mockCall( - _dualGovernance, abi.encodeWithSelector(IDualGovernance.activateNextState.selector), abi.encode(true) + assertEq(_vetoer.balance, balanceBefore + stethAmount); + + IEscrow.LockedAssetsTotals memory escrowLockedAssets = _escrow.getLockedAssetsTotals(); + + assertEq(escrowLockedAssets.stETHLockedShares, stethAmount); + assertEq(escrowLockedAssets.stETHClaimedETH, stethAmount); + assertEq(escrowLockedAssets.unstETHUnfinalizedShares, 0); + assertEq(escrowLockedAssets.unstETHFinalizedETH, 0); + + IEscrow.VetoerState memory state = _escrow.getVetoerState(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares, 0); + assertEq(state.unstETHLockedShares, 0); + } + + function test_withdrawETH_RevertOn_UnexpectedEscrowState() external { + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + _escrow.withdrawETH(); + } + + function test_withdrawETH_RevertOn_RageQuitExtensionPeriodNotStarted() external { + _transitToRageQuit(); + + vm.expectRevert(EscrowStateLib.RageQuitExtensionPeriodNotStarted.selector); + _escrow.withdrawETH(); + } + + function test_withdrawETH_RevertOn_EthWithdrawalsDelayNotPassed() external { + uint256 balanceBefore = _vetoer.balance; + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + + _transitToRageQuit(Durations.from(1), Durations.from(2)); + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + _claimStEthViaWQ(unstEthIds, stethAmount); + _ensureRageQuitExtensionPeriodStartedNow(); + assertTrue(_escrow.isRageQuitExtensionPeriodStarted()); + + vm.startPrank(_vetoer); + vm.expectRevert(EscrowStateLib.EthWithdrawalsDelayNotPassed.selector); + _escrow.withdrawETH(); + vm.stopPrank(); + + assertEq(_vetoer.balance, balanceBefore); + } + + function test_withdrawETH_RevertOn_InvalidSharesValue() external { + uint256 balanceBefore = _vetoer.balance; + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + address _vetoer2 = makeAddr("vetoer2"); + _stETH.mint(_vetoer2, 100 ether); + + vm.startPrank(_vetoer2); + _stETH.approve(address(_escrow), type(uint256).max); + _escrow.lockStETH(100 ether); + vm.stopPrank(); + + _vetoerLockedStEth(stethAmount); + + _wait(_minLockAssetDuration.plusSeconds(1)); + + _vetoerUnlockedStEth(stethAmount); + + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + _claimStEthViaWQ(unstEthIds, stethAmount); + _ensureRageQuitExtensionPeriodStartedNow(); + assertTrue(_escrow.isRageQuitExtensionPeriodStarted()); + + _wait(_minLockAssetDuration); + + vm.startPrank(_vetoer); + vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, SharesValues.ZERO)); + _escrow.withdrawETH(); + vm.stopPrank(); + + assertEq(_vetoer.balance, balanceBefore); + } + + // --- + // withdrawETH(uint256[] calldata unstETHIds) + // --- + + function test_withdrawETH_2_HappyPath() external { + uint256 balanceBefore = _vetoer.balance; + uint256[] memory unstEthAmounts = new uint256[](2); + unstEthAmounts[0] = 1 ether; + unstEthAmounts[1] = 10 ether; + + uint256[] memory unstEthIds = _vetoerLockedUnstEth(unstEthAmounts); + uint256[] memory hints = _finalizeUnstEth(unstEthAmounts, unstEthIds); + + _transitToRageQuit(); + + uint256 sum = _claimUnstEthFromEscrow(unstEthAmounts, unstEthIds, hints); + + _ensureWithdrawalsBatchesQueueClosed(); + + _ensureRageQuitExtensionPeriodStartedNow(); + assertTrue(_escrow.isRageQuitExtensionPeriodStarted()); + + _wait(Durations.from(1)); + + vm.startPrank(_vetoer); + vm.expectEmit(); + emit AssetsAccounting.UnstETHWithdrawn(unstEthIds, ETHValues.from(sum)); + _escrow.withdrawETH(unstEthIds); + vm.stopPrank(); + + assertEq(_vetoer.balance, balanceBefore + sum); + + IEscrow.LockedAssetsTotals memory escrowLockedAssets = _escrow.getLockedAssetsTotals(); + + assertEq(escrowLockedAssets.stETHLockedShares, 0); + assertEq(escrowLockedAssets.stETHClaimedETH, 0); + assertEq(escrowLockedAssets.unstETHUnfinalizedShares, 0); + assertEq(escrowLockedAssets.unstETHFinalizedETH, unstEthAmounts[0] + unstEthAmounts[1]); + + IEscrow.VetoerState memory state = _escrow.getVetoerState(_vetoer); + + assertEq(state.unstETHIdsCount, 2); + assertEq(state.stETHLockedShares, 0); + assertEq(state.unstETHLockedShares, unstEthAmounts[0] + unstEthAmounts[1]); + } + + function test_withdrawETH_2_RevertOn_EmptyUnstETHIds() external { + vm.expectRevert(Escrow.EmptyUnstETHIds.selector); + _escrow.withdrawETH(new uint256[](0)); + } + + function test_withdrawETH_2_RevertOn_UnexpectedEscrowState() external { + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + _escrow.withdrawETH(new uint256[](1)); + } + + function test_withdrawETH_2_RevertOn_RageQuitExtensionPeriodNotStarted() external { + _transitToRageQuit(); + + vm.expectRevert(EscrowStateLib.RageQuitExtensionPeriodNotStarted.selector); + _escrow.withdrawETH(new uint256[](1)); + } + + function test_withdrawETH_2_RevertOn_EthWithdrawalsDelayNotPassed() external { + uint256 balanceBefore = _vetoer.balance; + uint256[] memory unstEthAmounts = new uint256[](2); + unstEthAmounts[0] = 1 ether; + unstEthAmounts[1] = 10 ether; + + uint256[] memory unstEthIds = _vetoerLockedUnstEth(unstEthAmounts); + uint256[] memory hints = _finalizeUnstEth(unstEthAmounts, unstEthIds); + + _transitToRageQuit(Durations.from(10), Durations.from(10)); + + _claimUnstEthFromEscrow(unstEthAmounts, unstEthIds, hints); + + _ensureWithdrawalsBatchesQueueClosed(); + + _ensureRageQuitExtensionPeriodStartedNow(); + assertTrue(_escrow.isRageQuitExtensionPeriodStarted()); + + _wait(Durations.from(1)); + + vm.startPrank(_vetoer); + vm.expectRevert(EscrowStateLib.EthWithdrawalsDelayNotPassed.selector); + _escrow.withdrawETH(unstEthIds); + vm.stopPrank(); + + assertEq(_vetoer.balance, balanceBefore); + } + + function test_withdrawETH_2_RevertOn_InvalidUnstETHStatus() external { + uint256 balanceBefore = _vetoer.balance; + uint256[] memory unstEthAmounts = new uint256[](2); + unstEthAmounts[0] = 1 ether; + unstEthAmounts[1] = 10 ether; + + uint256[] memory unstEthIds = _vetoerLockedUnstEth(unstEthAmounts); + _finalizeUnstEth(unstEthAmounts, unstEthIds); + + _transitToRageQuit(); + + _ensureWithdrawalsBatchesQueueClosed(); + + _ensureRageQuitExtensionPeriodStartedNow(); + assertTrue(_escrow.isRageQuitExtensionPeriodStarted()); + + _wait(Durations.from(1)); + + vm.startPrank(_vetoer); + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, unstEthIds[0], UnstETHRecordStatus.Finalized + ) ); + _escrow.withdrawETH(unstEthIds); + vm.stopPrank(); + + assertEq(_vetoer.balance, balanceBefore); + } + + // --- + // getLockedAssetsTotals() + // --- + + function test_getLockedAssetsTotals() external view { + IEscrow.LockedAssetsTotals memory escrowLockedAssets = _escrow.getLockedAssetsTotals(); + + assertEq(escrowLockedAssets.stETHLockedShares, 0); + assertEq(escrowLockedAssets.stETHClaimedETH, 0); + assertEq(escrowLockedAssets.unstETHUnfinalizedShares, 0); + assertEq(escrowLockedAssets.unstETHFinalizedETH, 0); + } + + // --- + // getVetoerState() + // --- + + function test_getVetoerState() external { + _vetoerLockedStEth(stethAmount); + + IEscrow.VetoerState memory state = _escrow.getVetoerState(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares, _stETH.getSharesByPooledEth(stethAmount)); + assertEq(state.unstETHLockedShares, 0); + assertEq(state.lastAssetsLockTimestamp, Timestamps.now().toSeconds()); } + // --- // getVetoerUnstETHIds() // --- @@ -74,7 +1256,7 @@ contract EscrowUnitTests is UnitTest { assertEq(_escrow.getVetoerUnstETHIds(_vetoer).length, 0); - uint256[] memory unstEthIds = vetoerLockedUnstEth(unstEthAmounts); + uint256[] memory unstEthIds = _vetoerLockedUnstEth(unstEthAmounts); uint256[] memory vetoerUnstEthIds = _escrow.getVetoerUnstETHIds(_vetoer); @@ -105,13 +1287,18 @@ contract EscrowUnitTests is UnitTest { // getUnclaimedUnstETHIdsCount() // --- + function test_getUnclaimedUnstETHIdsCount() external { + _transitToRageQuit(); + assertEq(_escrow.getUnclaimedUnstETHIdsCount(), 0); + } + function test_getUnclaimedUnstETHIdsCount_RevertOn_UnexpectedState_Signaling() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); _escrow.getUnclaimedUnstETHIdsCount(); } function test_getUnclaimedUnstETHIdsCount_RevertOn_UnexpectedState_NotInitialized() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); _masterCopy.getUnclaimedUnstETHIdsCount(); } @@ -119,15 +1306,52 @@ contract EscrowUnitTests is UnitTest { // getNextWithdrawalBatch() // --- + function test_getNextWithdrawalBatch() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + + _transitToRageQuit(); + + uint256[] memory claimableUnstEthIds = _escrow.getNextWithdrawalBatch(100); + assertEq(claimableUnstEthIds.length, 0); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + _withdrawalQueue.setLastCheckpointIndex(1); + _withdrawalQueue.setCheckpointHints(new uint256[](unstEthIds.length)); + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + + claimableUnstEthIds = _escrow.getNextWithdrawalBatch(100); + assertEq(claimableUnstEthIds.length, unstEthIds.length); + assertEq(claimableUnstEthIds[0], unstEthIds[0]); + + _withdrawalQueue.setClaimableAmount(stethAmount); + vm.deal(address(_withdrawalQueue), stethAmount); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.UnstETHIdsClaimed(unstEthIds); + vm.expectEmit(); + emit AssetsAccounting.ETHClaimed(ETHValues.from(stethAmount)); + _escrow.claimNextWithdrawalsBatch(unstEthIds[0], new uint256[](unstEthIds.length)); + + claimableUnstEthIds = _escrow.getNextWithdrawalBatch(100); + assertEq(claimableUnstEthIds.length, 0); + } + + function test_getNextWithdrawalBatch_RevertOn_RageQuit_IsNotStarted() external { + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + _escrow.getNextWithdrawalBatch(100); + } + function test_getNextWithdrawalBatch_RevertOn_UnexpectedState_Signaling() external { uint256 batchLimit = 10; - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); _escrow.getNextWithdrawalBatch(batchLimit); } function test_getNextWithdrawalBatch_RevertOn_UnexpectedState_NotInitialized() external { uint256 batchLimit = 10; - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); _masterCopy.getNextWithdrawalBatch(batchLimit); } @@ -135,16 +1359,136 @@ contract EscrowUnitTests is UnitTest { // isWithdrawalsBatchesClosed() // --- + function test_isWithdrawalsBatchesClosed() external { + _transitToRageQuit(); + assertFalse(_escrow.isWithdrawalsBatchesClosed()); + + _withdrawalQueue.setRequestWithdrawalsResult(new uint256[](0)); + + _ensureWithdrawalsBatchesQueueClosed(); + + assertTrue(_escrow.isWithdrawalsBatchesClosed()); + } + function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedState_Signaling() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); _escrow.isWithdrawalsBatchesClosed(); } function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedState_NotInitialized() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); _masterCopy.isWithdrawalsBatchesClosed(); } + // --- + // isRageQuitExtensionPeriodStarted() + // --- + + function test_isRageQuitExtensionPeriodStarted() external { + assertFalse(_escrow.isRageQuitExtensionPeriodStarted()); + + _transitToRageQuit(); + + _ensureWithdrawalsBatchesQueueClosed(); + + _ensureRageQuitExtensionPeriodStartedNow(); + + assertTrue(_escrow.isRageQuitExtensionPeriodStarted()); + + assertEq(_escrow.getRageQuitExtensionPeriodStartedAt(), Timestamps.now()); + } + + // --- + // getRageQuitExtensionPeriodStartedAt() + // --- + + function test_getRageQuitExtensionPeriodStartedAt() external view { + Timestamp res = _escrow.getRageQuitExtensionPeriodStartedAt(); + assertEq(res.toSeconds(), Timestamps.ZERO.toSeconds()); + } + + // --- + // getRageQuitSupport() + // --- + + function test_getRageQuitSupport() external { + uint256 stEthLockedAmount = 80 ether + 100 wei; + uint256[] memory unstEthAmounts = new uint256[](2); + unstEthAmounts[0] = 1 ether; + unstEthAmounts[1] = 10 ether; + uint256[] memory finalizedUnstEthAmounts = new uint256[](1); + uint256[] memory finalizedUnstEthIds = new uint256[](1); + + PercentD16 actualSupport = + PercentsD16.fromFraction({numerator: stEthLockedAmount, denominator: _stETH.totalSupply()}); + + _vetoerLockedStEth(stEthLockedAmount); + + PercentD16 support = _escrow.getRageQuitSupport(); + assertEq(support, actualSupport); + assertEq(support, PercentsD16.fromBasisPoints(80_00)); + + // When some unstEth are locked in escrow => rage quit support changed + + uint256[] memory unstEthIds = _vetoerLockedUnstEth(unstEthAmounts); + + finalizedUnstEthAmounts[0] = unstEthAmounts[0]; + finalizedUnstEthIds[0] = unstEthIds[0]; + + _finalizeUnstEth(finalizedUnstEthAmounts, finalizedUnstEthIds); + + actualSupport = PercentsD16.fromFraction({ + numerator: stEthLockedAmount + unstEthAmounts[1] + unstEthAmounts[0], + denominator: _stETH.totalSupply() + unstEthAmounts[0] + }); + + support = _escrow.getRageQuitSupport(); + assertEq(support, actualSupport); + assertEq(support, PercentsD16.fromBasisPoints(91_00)); + } + + // --- + // isRageQuitFinalized() + // --- + + function test_isRageQuitFinalized() external { + _transitToRageQuit(); + + _ensureWithdrawalsBatchesQueueClosed(); + + _ensureRageQuitExtensionPeriodStartedNow(); + + _wait(Durations.from(1)); + + assertTrue(_escrow.isRageQuitFinalized()); + } + + // --- + // receive() + // --- + + function test_receive() external { + vm.deal(address(_withdrawalQueue), 1 ether); + vm.deal(address(this), 1 ether); + + assertEq(address(_escrow).balance, 0); + + vm.startPrank(address(_withdrawalQueue)); + ETHValues.from(1 ether).sendTo(payable(address(_escrow))); + vm.stopPrank(); + + assertEq(address(_escrow).balance, 1 ether); + + vm.expectRevert( + abi.encodeWithSelector(Escrow.InvalidETHSender.selector, address(this), address(_withdrawalQueue)) + ); + ETHValues.from(1 ether).sendTo(payable(address(_escrow))); + + assertEq(address(_escrow).balance, 1 ether); + assertEq(address(this).balance, 1 ether); + assertEq(address(_withdrawalQueue).balance, 0); + } + // --- // MIN_TRANSFERRABLE_ST_ETH_AMOUNT // --- @@ -268,7 +1612,46 @@ contract EscrowUnitTests is UnitTest { assertEq(_escrow.isWithdrawalsBatchesClosed(), true); } - function vetoerLockedUnstEth(uint256[] memory amounts) internal returns (uint256[] memory unstethIds) { + // --- + // helper methods + // --- + + function _createEscrow(uint256 size) internal returns (Escrow) { + return new Escrow(_stETH, _wstETH, _withdrawalQueue, IDualGovernance(_dualGovernance), size); + } + + function _createEscrowProxy(uint256 minWithdrawalsBatchSize) internal returns (Escrow) { + Escrow masterCopy = _createEscrow(minWithdrawalsBatchSize); + return Escrow(payable(Clones.clone(address(masterCopy)))); + } + + function _createInitializedEscrowProxy( + uint256 minWithdrawalsBatchSize, + Duration minAssetsLockDuration + ) internal returns (Escrow) { + Escrow instance = _createEscrowProxy(minWithdrawalsBatchSize); + + vm.prank(_dualGovernance); + instance.initialize(minAssetsLockDuration); + return instance; + } + + function _transitToRageQuit() internal { + vm.prank(_dualGovernance); + _escrow.startRageQuit(Durations.ZERO, Durations.ZERO); + } + + function _transitToRageQuit(Duration rqExtensionPeriod, Duration rqEthWithdrawalsDelay) internal { + vm.prank(_dualGovernance); + _escrow.startRageQuit(rqExtensionPeriod, rqEthWithdrawalsDelay); + } + + function _vetoerLockedStEth(uint256 amount) internal { + vm.prank(_vetoer); + _escrow.lockStETH(amount); + } + + function _vetoerLockedUnstEth(uint256[] memory amounts) internal returns (uint256[] memory unstethIds) { unstethIds = new uint256[](amounts.length); IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = new IWithdrawalQueue.WithdrawalRequestStatus[](amounts.length); @@ -279,17 +1662,104 @@ contract EscrowUnitTests is UnitTest { IWithdrawalQueue.WithdrawalRequestStatus(amounts[i], amounts[i], _vetoer, block.timestamp, false, false); } - vm.mockCall( - address(_withdrawalQueue), - abi.encodeWithSelector(IWithdrawalQueue.getWithdrawalStatus.selector, unstethIds), - abi.encode(statuses) - ); - vm.mockCall( - address(_withdrawalQueue), abi.encodeWithSelector(IWithdrawalQueue.transferFrom.selector), abi.encode(true) - ); + _withdrawalQueue.setWithdrawalRequestsStatuses(statuses); - vm.startPrank(_vetoer); + vm.prank(_vetoer); _escrow.lockUnstETH(unstethIds); + } + + function _finalizeUnstEth( + uint256[] memory amounts, + uint256[] memory finalizedUnstEthIds + ) internal returns (uint256[] memory hints) { + assertEq(amounts.length, finalizedUnstEthIds.length); + + hints = new uint256[](amounts.length); + uint256[] memory responses = new uint256[](amounts.length); + + for (uint256 i = 0; i < amounts.length; ++i) { + hints[i] = i; + responses[i] = amounts[i]; + } + + _withdrawalQueue.setClaimableEtherResult(responses); + + _escrow.markUnstETHFinalized(finalizedUnstEthIds, hints); + + for (uint256 i = 0; i < amounts.length; ++i) { + _stETH.burn(_vetoer, amounts[i]); + } + } + + function _claimUnstEthFromEscrow( + uint256[] memory amounts, + uint256[] memory unstEthIds, + uint256[] memory hints + ) internal returns (uint256 sum) { + assertEq(amounts.length, unstEthIds.length); + assertEq(amounts.length, hints.length); + + sum = 0; + for (uint256 i = 0; i < amounts.length; ++i) { + sum += amounts[i]; + } + + _withdrawalQueue.setClaimableAmount(sum); + _withdrawalQueue.setClaimableEtherResult(amounts); + vm.deal(address(_withdrawalQueue), sum); + + vm.expectEmit(); + emit AssetsAccounting.UnstETHClaimed(unstEthIds, ETHValues.from(sum)); + _escrow.claimUnstETH(unstEthIds, hints); + } + + function _claimStEthViaWQ(uint256[] memory unstEthIds, uint256 amount) internal { + _withdrawalQueue.setClaimableAmount(amount); + _withdrawalQueue.setLastCheckpointIndex(1); + _withdrawalQueue.setCheckpointHints(new uint256[](unstEthIds.length)); + vm.deal(address(_withdrawalQueue), amount); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.UnstETHIdsClaimed(unstEthIds); + vm.expectEmit(); + emit AssetsAccounting.ETHClaimed(ETHValues.from(amount)); + _escrow.claimNextWithdrawalsBatch(unstEthIds.length); + } + + function _vetoerUnlockedStEth(uint256 amount) internal { + vm.startPrank(_vetoer); + vm.expectEmit(); + emit AssetsAccounting.StETHSharesUnlocked(_vetoer, SharesValues.from(_stETH.getSharesByPooledEth(amount))); + _escrow.unlockStETH(); vm.stopPrank(); } + + function _ensureUnstEthAddedToWithdrawalsBatchesQueue(uint256[] memory unstEthIds, uint256 ethAmount) internal { + vm.expectEmit(); + emit WithdrawalsBatchesQueue.UnstETHIdsAdded(unstEthIds); + _escrow.requestNextWithdrawalsBatch(100); + + assertEq(_stETH.balanceOf(address(_escrow)), 0); + } + + function _ensureWithdrawalsBatchesQueueClosed() internal { + vm.expectEmit(); + emit WithdrawalsBatchesQueue.WithdrawalsBatchesQueueClosed(); + _escrow.requestNextWithdrawalsBatch(100); + } + + function _ensureRageQuitExtensionPeriodStartedNow() internal { + vm.expectEmit(); + emit EscrowStateLib.RageQuitExtensionPeriodStarted(Timestamps.now()); + _escrow.startRageQuitExtensionPeriod(); + } + + function _getUnstEthIdsFromWQ() internal returns (uint256[] memory unstEthIds) { + uint256 lri = Random.nextUint256(_random, 100500); + _withdrawalQueue.setLastRequestId(lri); + _withdrawalQueue.setLastFinalizedRequestId(lri + 1); + + unstEthIds = new uint256[](1); + unstEthIds[0] = lri + 1; + } } diff --git a/test/unit/libraries/WithdrawalBatchesQueue.t.sol b/test/unit/libraries/WithdrawalBatchesQueue.t.sol index c3e30c1b..4ea20861 100644 --- a/test/unit/libraries/WithdrawalBatchesQueue.t.sol +++ b/test/unit/libraries/WithdrawalBatchesQueue.t.sol @@ -294,7 +294,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { _batchesQueue.claimNextBatch(1); } - function test_claimNextBatch_RevertWOn_NothingToClaim() external { + function test_claimNextBatch_RevertOn_NothingToClaim() external { _openBatchesQueue(); uint256 unstETHIdsCount = 5; @@ -424,7 +424,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { } } - function test_calcRequestAmounts_RevertOn_DivisionByZero() external { + function test_calcRequestAmounts_RevertOn_MaxRequestAmountIsZero() external { _openBatchesQueue(); vm.expectRevert(stdError.divisionError); @@ -599,7 +599,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { _batchesQueue.info.totalUnstETHIdsCount = count; bool res = _batchesQueue.isAllBatchesClaimed(); - assert(res == true); + assertTrue(res); } function testFuzz_isAllBatchesClaimed_HappyPath_ReturnsFalse( @@ -611,7 +611,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { _batchesQueue.info.totalUnstETHIdsCount = totalUnstETHCount; bool res = _batchesQueue.isAllBatchesClaimed(); - assert(res == false); + assertFalse(res); } // --- From e77f36c21b62d86efbc9e60634aa97fdcd4f5f79 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 6 Dec 2024 14:34:39 +0300 Subject: [PATCH 092/107] Rename MAX_ASSETS_LOCK_DURATION to MAX_MIN_ASSETS_LOCK_DURATION for consistency --- contracts/DualGovernance.sol | 8 ++++---- contracts/Escrow.sol | 8 ++++---- contracts/interfaces/IEscrow.sol | 2 -- contracts/libraries/EscrowState.sol | 10 ++++++---- scripts/deploy/Config.sol | 4 ++-- scripts/deploy/ContractsDeployment.sol | 2 +- scripts/deploy/JsonConfig.s.sol | 6 +++--- test/mocks/EscrowMock.sol | 10 +++++----- test/unit/DualGovernance.t.sol | 10 ++++++---- test/unit/libraries/EscrowState.t.sol | 14 +++++++------- 10 files changed, 38 insertions(+), 36 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 4b78cf17..276ceec5 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -69,13 +69,13 @@ contract DualGovernance is IDualGovernance { /// @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. - /// @param maxAssetsLockDuration The maximum duration for which assets can be locked in the Rage Quit Escrow contract. + /// @param maxMinAssetsLockDuration The upper bound for the minimum duration of assets lock in the Escrow. struct SanityCheckParams { uint256 minWithdrawalsBatchSize; Duration minTiebreakerActivationTimeout; Duration maxTiebreakerActivationTimeout; uint256 maxSealableWithdrawalBlockersCount; - Duration maxAssetsLockDuration; + Duration maxMinAssetsLockDuration; } /// @notice The lower bound for the time the Dual Governance must spend in the "locked" state @@ -95,7 +95,7 @@ contract DualGovernance is IDualGovernance { // External Dependencies // --- - /// @notice Token addresses tha used in the Dual Governance as signalling tokens. + /// @notice Token addresses that used in the Dual Governance as signalling tokens. /// @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. @@ -167,7 +167,7 @@ contract DualGovernance is IDualGovernance { wstETH: signallingTokens.wstETH, withdrawalQueue: signallingTokens.withdrawalQueue, minWithdrawalsBatchSize: sanityCheckParams.minWithdrawalsBatchSize, - maxAssetsLockDuration: sanityCheckParams.maxAssetsLockDuration + maxMinAssetsLockDuration: sanityCheckParams.maxMinAssetsLockDuration }); emit EscrowMasterCopyDeployed(ESCROW_MASTER_COPY); diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 1c5f9081..38208429 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -59,7 +59,7 @@ contract Escrow is IEscrow { uint256 public immutable MIN_WITHDRAWALS_BATCH_SIZE; /// @notice The maximum duration that can be set as the minimum assets lock duration. - Duration public immutable MAX_ASSETS_LOCK_DURATION; + Duration public immutable MAX_MIN_ASSETS_LOCK_DURATION; // --- // Dependencies Immutables @@ -108,7 +108,7 @@ contract Escrow is IEscrow { IWithdrawalQueue withdrawalQueue, IDualGovernance dualGovernance, uint256 minWithdrawalsBatchSize, - Duration maxAssetsLockDuration + Duration maxMinAssetsLockDuration ) { _SELF = address(this); DUAL_GOVERNANCE = dualGovernance; @@ -118,7 +118,7 @@ contract Escrow is IEscrow { WITHDRAWAL_QUEUE = withdrawalQueue; MIN_WITHDRAWALS_BATCH_SIZE = minWithdrawalsBatchSize; - MAX_ASSETS_LOCK_DURATION = maxAssetsLockDuration; + MAX_MIN_ASSETS_LOCK_DURATION = maxMinAssetsLockDuration; } /// @notice Initializes the proxy instance with the specified minimum assets lock duration. @@ -437,7 +437,7 @@ contract Escrow is IEscrow { /// @param newMinAssetsLockDuration The new minimum lock duration to be set. function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { _checkCallerIsDualGovernance(); - _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration, MAX_ASSETS_LOCK_DURATION); + _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration, MAX_MIN_ASSETS_LOCK_DURATION); } // --- diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol index a0e8973c..a51a5030 100644 --- a/contracts/interfaces/IEscrow.sol +++ b/contracts/interfaces/IEscrow.sol @@ -32,8 +32,6 @@ interface IEscrow { function initialize(Duration minAssetsLockDuration) external; - function MAX_ASSETS_LOCK_DURATION() external view returns (Duration); - function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares); function unlockStETH() external returns (uint256 unlockedStETHShares); function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares); diff --git a/contracts/libraries/EscrowState.sol b/contracts/libraries/EscrowState.sol index dbd37c5b..844e6158 100644 --- a/contracts/libraries/EscrowState.sol +++ b/contracts/libraries/EscrowState.sol @@ -103,14 +103,16 @@ library EscrowState { /// @notice Sets the minimum assets lock duration. /// @param self The context of the Escrow State library. /// @param newMinAssetsLockDuration The new minimum assets lock duration. - /// @param maxAssetsLockDuration Sanity check for max assets lock duration. + /// @param maxMinAssetsLockDuration Sanity check for max assets lock duration. function setMinAssetsLockDuration( Context storage self, Duration newMinAssetsLockDuration, - Duration maxAssetsLockDuration + Duration maxMinAssetsLockDuration ) internal { - if (self.minAssetsLockDuration == newMinAssetsLockDuration || newMinAssetsLockDuration > maxAssetsLockDuration) - { + if ( + self.minAssetsLockDuration == newMinAssetsLockDuration + || newMinAssetsLockDuration > maxMinAssetsLockDuration + ) { revert InvalidMinAssetsLockDuration(newMinAssetsLockDuration); } _setMinAssetsLockDuration(self, newMinAssetsLockDuration); diff --git a/scripts/deploy/Config.sol b/scripts/deploy/Config.sol index bb438f90..a685ba14 100644 --- a/scripts/deploy/Config.sol +++ b/scripts/deploy/Config.sol @@ -31,7 +31,7 @@ uint256 constant DEFAULT_MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = 255; uint256 constant DEFAULT_FIRST_SEAL_RAGE_QUIT_SUPPORT = 3_00; // 3% uint256 constant DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT = 15_00; // 15% uint256 constant DEFAULT_MIN_ASSETS_LOCK_DURATION = 5 hours; -uint256 constant DEFAULT_MAX_ASSETS_LOCK_DURATION = 365 days; +uint256 constant DEFAULT_MAX_MIN_ASSETS_LOCK_DURATION = 365 days; uint256 constant DEFAULT_VETO_SIGNALLING_MIN_DURATION = 3 days; uint256 constant DEFAULT_VETO_SIGNALLING_MAX_DURATION = 30 days; uint256 constant DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION = 5 hours; @@ -81,7 +81,7 @@ struct DeployConfig { PercentD16 FIRST_SEAL_RAGE_QUIT_SUPPORT; PercentD16 SECOND_SEAL_RAGE_QUIT_SUPPORT; Duration MIN_ASSETS_LOCK_DURATION; - Duration MAX_ASSETS_LOCK_DURATION; + Duration MAX_MIN_ASSETS_LOCK_DURATION; Duration VETO_SIGNALLING_MIN_DURATION; Duration VETO_SIGNALLING_MAX_DURATION; Duration VETO_SIGNALLING_MIN_ACTIVE_DURATION; diff --git a/scripts/deploy/ContractsDeployment.sol b/scripts/deploy/ContractsDeployment.sol index af87be0e..0ac0a25d 100644 --- a/scripts/deploy/ContractsDeployment.sol +++ b/scripts/deploy/ContractsDeployment.sol @@ -231,7 +231,7 @@ library DGContractsDeployment { minTiebreakerActivationTimeout: dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT, maxTiebreakerActivationTimeout: dgDeployConfig.MAX_TIEBREAKER_ACTIVATION_TIMEOUT, maxSealableWithdrawalBlockersCount: dgDeployConfig.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT, - maxAssetsLockDuration: dgDeployConfig.MAX_ASSETS_LOCK_DURATION + maxMinAssetsLockDuration: dgDeployConfig.MAX_MIN_ASSETS_LOCK_DURATION }) }); } diff --git a/scripts/deploy/JsonConfig.s.sol b/scripts/deploy/JsonConfig.s.sol index f4227b21..6ff4e783 100644 --- a/scripts/deploy/JsonConfig.s.sol +++ b/scripts/deploy/JsonConfig.s.sol @@ -46,7 +46,7 @@ import { DEFAULT_FIRST_SEAL_RAGE_QUIT_SUPPORT, DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT, DEFAULT_MIN_ASSETS_LOCK_DURATION, - DEFAULT_MAX_ASSETS_LOCK_DURATION, + DEFAULT_MAX_MIN_ASSETS_LOCK_DURATION, DEFAULT_VETO_SIGNALLING_MIN_DURATION, DEFAULT_VETO_SIGNALLING_MAX_DURATION, DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION, @@ -174,8 +174,8 @@ contract DGDeployJSONConfigProvider is Script { MIN_ASSETS_LOCK_DURATION: Durations.from( stdJson.readUintOr(jsonConfig, ".MIN_ASSETS_LOCK_DURATION", DEFAULT_MIN_ASSETS_LOCK_DURATION) ), - MAX_ASSETS_LOCK_DURATION: Durations.from( - stdJson.readUintOr(jsonConfig, ".MAX_ASSETS_LOCK_DURATION", DEFAULT_MAX_ASSETS_LOCK_DURATION) + MAX_MIN_ASSETS_LOCK_DURATION: Durations.from( + stdJson.readUintOr(jsonConfig, ".MAX_MIN_ASSETS_LOCK_DURATION", DEFAULT_MAX_MIN_ASSETS_LOCK_DURATION) ), VETO_SIGNALLING_MIN_DURATION: Durations.from( stdJson.readUintOr(jsonConfig, ".VETO_SIGNALLING_MIN_DURATION", DEFAULT_VETO_SIGNALLING_MIN_DURATION) diff --git a/test/mocks/EscrowMock.sol b/test/mocks/EscrowMock.sol index ed58f4a1..07dd6180 100644 --- a/test/mocks/EscrowMock.sol +++ b/test/mocks/EscrowMock.sol @@ -12,7 +12,7 @@ contract EscrowMock is IEscrow { event __RageQuitStarted(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock); Duration public __minAssetsLockDuration; - Duration public __maxAssetsLockDuration; + Duration public __maxMinAssetsLockDuration; PercentD16 public __rageQuitSupport; bool public __isRageQuitFinalized; @@ -132,11 +132,11 @@ contract EscrowMock is IEscrow { return __minAssetsLockDuration; } - function MAX_ASSETS_LOCK_DURATION() external view returns (Duration) { - return __maxAssetsLockDuration; + function MAX_MIN_ASSETS_LOCK_DURATION() external view returns (Duration) { + return __maxMinAssetsLockDuration; } - function setMaxAssetsLockDuration(Duration newMaxAssetsLockDuration) external { - __maxAssetsLockDuration = newMaxAssetsLockDuration; + function setmaxMinAssetsLockDuration(Duration newmaxMinAssetsLockDuration) external { + __maxMinAssetsLockDuration = newmaxMinAssetsLockDuration; } } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index e678a87a..bfc37f4f 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -88,7 +88,7 @@ contract DualGovernanceUnitTests is UnitTest { minTiebreakerActivationTimeout: Durations.from(30 days), maxTiebreakerActivationTimeout: Durations.from(180 days), maxSealableWithdrawalBlockersCount: 128, - maxAssetsLockDuration: Durations.from(365 days) + maxMinAssetsLockDuration: Durations.from(365 days) }); DualGovernance internal _dualGovernance = new DualGovernance({ @@ -178,7 +178,7 @@ contract DualGovernanceUnitTests is UnitTest { Duration minTiebreakerActivationTimeout = Durations.from(30 days); Duration maxTiebreakerActivationTimeout = Durations.from(180 days); uint256 maxSealableWithdrawalBlockersCount = 128; - Duration maxAssetsLockDuration = Durations.from(365 days); + Duration maxMinAssetsLockDuration = Durations.from(365 days); DualGovernance dualGovernanceLocal = new DualGovernance({ components: DualGovernance.DualGovernanceComponents({ @@ -196,7 +196,7 @@ contract DualGovernanceUnitTests is UnitTest { minTiebreakerActivationTimeout: minTiebreakerActivationTimeout, maxTiebreakerActivationTimeout: maxTiebreakerActivationTimeout, maxSealableWithdrawalBlockersCount: maxSealableWithdrawalBlockersCount, - maxAssetsLockDuration: maxAssetsLockDuration + maxMinAssetsLockDuration: maxMinAssetsLockDuration }) }); @@ -205,7 +205,9 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(dualGovernanceLocal.MAX_TIEBREAKER_ACTIVATION_TIMEOUT(), maxTiebreakerActivationTimeout); assertEq(dualGovernanceLocal.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT(), maxSealableWithdrawalBlockersCount); assertEq(address(dualGovernanceLocal.ESCROW_MASTER_COPY()), predictedEscrowCopyAddress); - assertEq(dualGovernanceLocal.ESCROW_MASTER_COPY().MAX_ASSETS_LOCK_DURATION(), maxAssetsLockDuration); + + address payable escrowMasterCopyAddress = payable(address(dualGovernanceLocal.ESCROW_MASTER_COPY())); + assertEq(Escrow(escrowMasterCopyAddress).MAX_MIN_ASSETS_LOCK_DURATION(), maxMinAssetsLockDuration); } // --- diff --git a/test/unit/libraries/EscrowState.t.sol b/test/unit/libraries/EscrowState.t.sol index 88ff03f0..9beb5920 100644 --- a/test/unit/libraries/EscrowState.t.sol +++ b/test/unit/libraries/EscrowState.t.sol @@ -105,15 +105,15 @@ contract EscrowStateUnitTests is UnitTest { function testFuzz_setMinAssetsLockDuration_happyPath( Duration minAssetsLockDuration, - Duration maxAssetsLockDuration + Duration maxMinAssetsLockDuration ) external { vm.assume(minAssetsLockDuration != Durations.ZERO); - vm.assume(minAssetsLockDuration <= maxAssetsLockDuration); + vm.assume(minAssetsLockDuration <= maxMinAssetsLockDuration); vm.expectEmit(); emit EscrowState.MinAssetsLockDurationSet(minAssetsLockDuration); - EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration, maxAssetsLockDuration); + EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration, maxMinAssetsLockDuration); checkContext({ state: State.NotInitialized, @@ -133,16 +133,16 @@ contract EscrowStateUnitTests is UnitTest { EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration, Durations.from(type(uint16).max)); } - function testFuzz_setMinAssetsLockDuration_RevertWhen_DurationGreaterThenMaxAssetsLockDuration( + function testFuzz_setMinAssetsLockDuration_RevertWhen_DurationGreaterThenmaxMinAssetsLockDuration( Duration minAssetsLockDuration, - Duration maxAssetsLockDuration + Duration maxMinAssetsLockDuration ) external { - vm.assume(minAssetsLockDuration > maxAssetsLockDuration); + vm.assume(minAssetsLockDuration > maxMinAssetsLockDuration); vm.expectRevert( abi.encodeWithSelector(EscrowState.InvalidMinAssetsLockDuration.selector, minAssetsLockDuration) ); - EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration, maxAssetsLockDuration); + EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration, maxMinAssetsLockDuration); } // --- From ef218958d87a61d3e6dd404cb45243b52806cf21 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 16:47:16 +0400 Subject: [PATCH 093/107] Add setProposerExecutor to IDualGovernance. Fix test assume --- contracts/interfaces/IDualGovernance.sol | 1 + test/unit/DualGovernance.t.sol | 1 + 2 files changed, 2 insertions(+) diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 1c0989cf..ebd10686 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -40,6 +40,7 @@ interface IDualGovernance is IGovernance, ITiebreaker { function getStateDetails() external view returns (StateDetails memory stateDetails); function registerProposer(address proposer, address executor) external; + function setProposerExecutor(address proposerAccount, address newExecutor) external; function unregisterProposer(address proposer) external; function isRegisteredProposer(address account) external view returns (bool); function getProposer(address account) external view returns (Proposers.Proposer memory proposer); diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index add00fc9..1ef609c4 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -2312,6 +2312,7 @@ contract DualGovernanceUnitTests is UnitTest { } function testFuzz_setResealCommittee_RevertOn_InvalidResealCommittee(address newResealCommittee) external { + vm.assume(_dualGovernance.getResealCommittee() != newResealCommittee); _executor.execute( address(_dualGovernance), 0, From e324c684772d9f3b0389c623248924d6ffec7357 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 18:24:38 +0400 Subject: [PATCH 094/107] Added AssetsAccounting.getLockedUnstETHDetails(). Update RageQuitEscrow getters. --- contracts/Escrow.sol | 44 +++++++++---------- contracts/interfaces/IRageQuitEscrow.sol | 5 +-- contracts/libraries/AssetsAccounting.sol | 24 ++++++++++ contracts/libraries/Proposers.sol | 6 ++- test/scenario/dg-update-tokens-rotation.t.sol | 4 +- test/scenario/escrow.t.sol | 6 +-- test/scenario/veto-cooldown-mechanics.t.sol | 4 +- 7 files changed, 59 insertions(+), 34 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 776352b7..dc0aec0b 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -19,13 +19,7 @@ import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; import {EscrowState, State} from "./libraries/EscrowState.sol"; import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalsBatchesQueue.sol"; -import { - HolderAssets, - UnstETHRecord, - StETHAccounting, - UnstETHAccounting, - AssetsAccounting -} from "./libraries/AssetsAccounting.sol"; +import {HolderAssets, StETHAccounting, UnstETHAccounting, AssetsAccounting} from "./libraries/AssetsAccounting.sol"; /// @notice This contract is used to accumulate stETH, wstETH, unstETH, and withdrawn ETH from vetoers during the /// veto signalling and rage quit processes. @@ -38,6 +32,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { // --- // Errors // --- + error EmptyUnstETHIds(); error UnclaimedBatches(); error UnexpectedUnstETHId(); @@ -185,7 +180,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Lock & Unlock wstETH + // Signalling Escrow: Lock & Unlock wstETH // --- /// @notice Locks the vetoer's specified `amount` of wstETH in the Veto Signalling Escrow, thereby increasing @@ -394,12 +389,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { unstETHDetails = new LockedUnstETHDetails[](unstETHIds.length); for (uint256 i = 0; i < unstETHIds.length; ++i) { - UnstETHRecord memory unstETHRecord = _accounting.unstETHRecords[unstETHIds[i]]; - - unstETHDetails[i].status = unstETHRecord.status; - unstETHDetails[i].lockedBy = unstETHRecord.lockedBy; - unstETHDetails[i].shares = unstETHRecord.shares; - unstETHDetails[i].claimableAmount = unstETHRecord.claimableAmount; + unstETHDetails[i] = _accounting.getLockedUnstETHDetails(unstETHIds[i]); } } @@ -568,13 +558,14 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Rage Quit Escrow Getters + // Rage Quit Escrow: Getters // --- /// @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(); + _escrowState.checkRageQuitEscrow(); + return _escrowState.isRageQuitExtensionPeriodPassed(); } /// @notice Retrieves the unstETH NFT ids of the next batch available for claiming. @@ -585,22 +576,31 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { unstETHIds = _batchesQueue.getNextWithdrawalsBatches(limit); } + /// @notice Returns whether all withdrawal batches have been closed. + /// @return isWithdrawalsBatchesClosed A boolean value indicating whether all withdrawal batches have been + /// closed (`true`) or not (`false`). + function isWithdrawalsBatchesClosed() external view returns (bool) { + _escrowState.checkRageQuitEscrow(); + return _batchesQueue.isClosed(); + } + + /// @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) { + _escrowState.checkRageQuitEscrow(); + return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + } + /// @notice Retrieves details about the current state of the rage quit escrow. /// @return details A `RageQuitEscrowDetails` struct containing the following fields: - /// - `isWithdrawalsBatchesClosed`: Indicates whether the withdrawals batches are closed. /// - `isRageQuitExtensionPeriodStarted`: Indicates whether the rage quit extension period has started. - /// - `unclaimedUnstETHIdsCount`: The total count of unstETH NFTs that have not been claimed yet. /// - `rageQuitEthWithdrawalsDelay`: The delay period for ETH withdrawals during rage quit. /// - `rageQuitExtensionPeriodDuration`: The duration of the rage quit extension period. /// - `rageQuitExtensionPeriodStartedAt`: The timestamp when the rage quit extension period started. function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory details) { _escrowState.checkRageQuitEscrow(); - details.isWithdrawalsBatchesClosed = _batchesQueue.isClosed(); details.isRageQuitExtensionPeriodStarted = _escrowState.isRageQuitExtensionPeriodStarted(); - - details.unclaimedUnstETHIdsCount = _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); - details.rageQuitEthWithdrawalsDelay = _escrowState.rageQuitEthWithdrawalsDelay; details.rageQuitExtensionPeriodDuration = _escrowState.rageQuitExtensionPeriodDuration; details.rageQuitExtensionPeriodStartedAt = _escrowState.rageQuitExtensionPeriodStartedAt; diff --git a/contracts/interfaces/IRageQuitEscrow.sol b/contracts/interfaces/IRageQuitEscrow.sol index d31b6c2e..d90a3a61 100644 --- a/contracts/interfaces/IRageQuitEscrow.sol +++ b/contracts/interfaces/IRageQuitEscrow.sol @@ -8,10 +8,7 @@ import {IEscrowBase} from "./IEscrowBase.sol"; interface IRageQuitEscrow is IEscrowBase { struct RageQuitEscrowDetails { - bool isRageQuitFinalized; - bool isWithdrawalsBatchesClosed; bool isRageQuitExtensionPeriodStarted; - uint256 unclaimedUnstETHIdsCount; Duration rageQuitEthWithdrawalsDelay; Duration rageQuitExtensionPeriodDuration; Timestamp rageQuitExtensionPeriodStartedAt; @@ -30,5 +27,7 @@ interface IRageQuitEscrow is IEscrowBase { function isRageQuitFinalized() external view returns (bool); function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds); + function isWithdrawalsBatchesClosed() external view returns (bool); + function getUnclaimedUnstETHIdsCount() external view returns (uint256); function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory); } diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index 84c1ba53..1a2d8226 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -8,6 +8,7 @@ import {SharesValue, SharesValues} from "../types/SharesValue.sol"; import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; import {IWithdrawalQueue} from "../interfaces/IWithdrawalQueue.sol"; +import {ISignallingEscrow} from "../interfaces/ISignallingEscrow.sol"; /// @notice Tracks the stETH and unstETH tokens associated with users. /// @param stETHLockedShares Total number of stETH shares held by the user. @@ -341,6 +342,29 @@ library AssetsAccounting { emit UnstETHWithdrawn(unstETHIds, amountWithdrawn); } + // --- + // Getters + // --- + + /// @notice Retrieves details of locked unstETH record for the given id. + /// @param unstETHId The id for the locked unstETH record to retrieve. + /// @return unstETHDetails A `LockedUnstETHDetails` struct containing the details for provided unstETH id. + function getLockedUnstETHDetails( + Context storage self, + uint256 unstETHId + ) internal view returns (ISignallingEscrow.LockedUnstETHDetails memory unstETHDetails) { + UnstETHRecord memory unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.status == UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.NotLocked); + } + + unstETHDetails.status = unstETHRecord.status; + unstETHDetails.lockedBy = unstETHRecord.lockedBy; + unstETHDetails.shares = unstETHRecord.shares; + unstETHDetails.claimableAmount = unstETHRecord.claimableAmount; + } + // --- // Checks // --- diff --git a/contracts/libraries/Proposers.sol b/contracts/libraries/Proposers.sol index 5bbb471c..9d78c3a8 100644 --- a/contracts/libraries/Proposers.sol +++ b/contracts/libraries/Proposers.sol @@ -154,8 +154,10 @@ library Proposers { /// @param self The context of the Proposers library. /// @return proposers An array of `Proposer` structs representing all registered proposers. function getAllProposers(Context storage self) internal view returns (Proposer[] memory proposers) { - proposers = new Proposer[](self.proposers.length); - for (uint256 i = 0; i < proposers.length; ++i) { + uint256 proposersCount = self.proposers.length; + proposers = new Proposer[](proposersCount); + + for (uint256 i = 0; i < proposersCount; ++i) { proposers[i] = getProposer(self, self.proposers[i]); } } diff --git a/test/scenario/dg-update-tokens-rotation.t.sol b/test/scenario/dg-update-tokens-rotation.t.sol index a415e13c..97f98191 100644 --- a/test/scenario/dg-update-tokens-rotation.t.sol +++ b/test/scenario/dg-update-tokens-rotation.t.sol @@ -96,13 +96,13 @@ contract DualGovernanceUpdateTokensRotation is ScenarioTestBlueprint { // The Rage Quit may be finished in the previous DG instance so vetoers will not lose their funds by mistake Escrow rageQuitEscrow = Escrow(payable(_dualGovernance.getRageQuitEscrow())); - while (!rageQuitEscrow.getRageQuitEscrowDetails().isWithdrawalsBatchesClosed) { + while (!rageQuitEscrow.isWithdrawalsBatchesClosed()) { rageQuitEscrow.requestNextWithdrawalsBatch(96); } _finalizeWithdrawalQueue(); - while (rageQuitEscrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { + while (rageQuitEscrow.getUnclaimedUnstETHIdsCount() > 0) { rageQuitEscrow.claimNextWithdrawalsBatch(32); } diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 01582c83..fd9b3d4c 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -316,7 +316,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { assertEq(_lido.withdrawalQueue.balanceOf(address(escrow)), 20); - while (!escrow.getRageQuitEscrowDetails().isWithdrawalsBatchesClosed) { + while (!escrow.isWithdrawalsBatchesClosed()) { escrow.requestNextWithdrawalsBatch(96); } @@ -340,7 +340,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { unstETHIdsToClaim, 1, _lido.withdrawalQueue.getLastCheckpointIndex() ); - while (escrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { + while (escrow.getUnclaimedUnstETHIdsCount() > 0) { escrow.claimNextWithdrawalsBatch(32); } @@ -607,7 +607,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { escrow.claimUnstETH(unstETHIdsToClaim, hints); // The rage quit process can be successfully finished - while (escrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { + while (escrow.getUnclaimedUnstETHIdsCount() > 0) { escrow.claimNextWithdrawalsBatch(batchSizeLimit); } diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index fc19ce76..8964f61d 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -65,13 +65,13 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { // request withdrawals batches Escrow rageQuitEscrow = _getRageQuitEscrow(); - while (!rageQuitEscrow.getRageQuitEscrowDetails().isWithdrawalsBatchesClosed) { + while (!rageQuitEscrow.isWithdrawalsBatchesClosed()) { rageQuitEscrow.requestNextWithdrawalsBatch(96); } _lido.finalizeWithdrawalQueue(); - while (rageQuitEscrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { + while (rageQuitEscrow.getUnclaimedUnstETHIdsCount() > 0) { rageQuitEscrow.claimNextWithdrawalsBatch(128); } From c9d44426e4af5b4951138bd5c66cf14868e76fe7 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 19:40:41 +0400 Subject: [PATCH 095/107] Add tests for getLockedUnstETHDetails method --- contracts/Escrow.sol | 1 + contracts/interfaces/IRageQuitEscrow.sol | 2 +- contracts/interfaces/ISignallingEscrow.sol | 1 + contracts/libraries/AssetsAccounting.sol | 1 + test/unit/Escrow.t.sol | 44 +++++++++++++++++ test/unit/libraries/AssetsAccounting.t.sol | 57 ++++++++++++++++++++++ 6 files changed, 105 insertions(+), 1 deletion(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 8a615dc9..beaec06e 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -376,6 +376,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { /// @return unstETHDetails An array of `LockedUnstETHDetails` containing the details for each provided unstETH id. /// /// The details include: + /// - `id`: The id of the locked unstETH NFT. /// - `status`: The current status of the unstETH record. /// - `lockedBy`: The address that locked the unstETH record. /// - `shares`: The number of shares associated with the locked unstETH. diff --git a/contracts/interfaces/IRageQuitEscrow.sol b/contracts/interfaces/IRageQuitEscrow.sol index d90a3a61..6649db93 100644 --- a/contracts/interfaces/IRageQuitEscrow.sol +++ b/contracts/interfaces/IRageQuitEscrow.sol @@ -8,10 +8,10 @@ import {IEscrowBase} from "./IEscrowBase.sol"; interface IRageQuitEscrow is IEscrowBase { struct RageQuitEscrowDetails { - bool isRageQuitExtensionPeriodStarted; Duration rageQuitEthWithdrawalsDelay; Duration rageQuitExtensionPeriodDuration; Timestamp rageQuitExtensionPeriodStartedAt; + bool isRageQuitExtensionPeriodStarted; } function requestNextWithdrawalsBatch(uint256 batchSize) external; diff --git a/contracts/interfaces/ISignallingEscrow.sol b/contracts/interfaces/ISignallingEscrow.sol index 5829029a..de91a3a6 100644 --- a/contracts/interfaces/ISignallingEscrow.sol +++ b/contracts/interfaces/ISignallingEscrow.sol @@ -20,6 +20,7 @@ interface ISignallingEscrow is IEscrowBase { } struct LockedUnstETHDetails { + uint256 id; UnstETHRecordStatus status; address lockedBy; SharesValue shares; diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index 1a2d8226..214430a4 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -359,6 +359,7 @@ library AssetsAccounting { revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.NotLocked); } + unstETHDetails.id = unstETHId; unstETHDetails.status = unstETHRecord.status; unstETHDetails.lockedBy = unstETHRecord.lockedBy; unstETHDetails.shares = unstETHRecord.shares; diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index c0613835..6a689960 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -1290,6 +1290,50 @@ contract EscrowUnitTests is UnitTest { assertEq(_escrow.getVetoerUnstETHIds(_vetoer).length, 0); } + // --- + // getLockedUnstETHDetails() + // --- + + function test_getLockedUnstETHDetails_HappyPath() external { + uint256[] memory unstEthAmounts = new uint256[](2); + unstEthAmounts[0] = 1 ether; + unstEthAmounts[1] = 10 ether; + + assertEq(_escrow.getVetoerUnstETHIds(_vetoer).length, 0); + + uint256[] memory unstEthIds = _vetoerLockedUnstEth(unstEthAmounts); + + IEscrow.LockedUnstETHDetails[] memory unstETHDetails = _escrow.getLockedUnstETHDetails(unstEthIds); + + assertEq(unstETHDetails.length, unstEthIds.length); + + assertEq(unstETHDetails[0].id, unstEthIds[0]); + assertEq(unstETHDetails[0].lockedBy, _vetoer); + assertTrue(unstETHDetails[0].status == UnstETHRecordStatus.Locked); + assertEq(unstETHDetails[0].shares.toUint256(), unstEthAmounts[0]); + assertEq(unstETHDetails[0].claimableAmount.toUint256(), 0); + + assertEq(unstETHDetails[1].id, unstEthIds[1]); + assertEq(unstETHDetails[1].lockedBy, _vetoer); + assertTrue(unstETHDetails[1].status == UnstETHRecordStatus.Locked); + assertEq(unstETHDetails[1].shares.toUint256(), unstEthAmounts[1]); + assertEq(unstETHDetails[1].claimableAmount.toUint256(), 0); + } + + function test_getLockedUnstETHDetails_RevertOn_unstETHNotLocked() external { + uint256 notLockedUnstETHId = 42; + + uint256[] memory notLockedUnstETHIds = new uint256[](1); + notLockedUnstETHIds[0] = notLockedUnstETHId; + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, notLockedUnstETHId, UnstETHRecordStatus.NotLocked + ) + ); + _escrow.getLockedUnstETHDetails(notLockedUnstETHIds); + } + // --- // getNextWithdrawalBatch() // --- diff --git a/test/unit/libraries/AssetsAccounting.t.sol b/test/unit/libraries/AssetsAccounting.t.sol index e1c9b3df..2ff5a3d9 100644 --- a/test/unit/libraries/AssetsAccounting.t.sol +++ b/test/unit/libraries/AssetsAccounting.t.sol @@ -11,6 +11,7 @@ import {IndicesOneBased} from "contracts/types/IndexOneBased.sol"; import {Durations} from "contracts/types/Duration.sol"; import {Timestamps} from "contracts/types/Timestamp.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; import {AssetsAccounting, UnstETHRecordStatus} from "contracts/libraries/AssetsAccounting.sol"; import {UnitTest, Duration} from "test/utils/unit-test.sol"; @@ -1419,6 +1420,62 @@ contract AssetsAccountingUnitTests is UnitTest { AssetsAccounting.accountUnstETHWithdraw(_accountingContext, holder, unstETHIds); } + // --- + // getLockedUnstETHDetails + // --- + + function test_getLockedUnstETHDetails_HappyPath() external { + uint256 unstETHIdsCount = 4; + uint256[] memory unstETHIds = new uint256[](unstETHIdsCount); + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + address holder = address(uint160(uint256(keccak256(abi.encode(i))))); + + _accountingContext.unstETHRecords[unstETHIds[i]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus(i + 1); + _accountingContext.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(i * 1 ether); + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(i * 10 ether); + + _accountingContext.assets[holder].unstETHIds.push(unstETHIds[i]); + } + + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + ISignallingEscrow.LockedUnstETHDetails memory unstETHDetails = + AssetsAccounting.getLockedUnstETHDetails(_accountingContext, unstETHIds[i]); + + assertEq(unstETHDetails.id, unstETHIds[i]); + assertEq(unstETHDetails.status, UnstETHRecordStatus(i + 1)); + assertEq(unstETHDetails.lockedBy, address(uint160(uint256(keccak256(abi.encode(i)))))); + assertEq(unstETHDetails.shares, SharesValues.from(i * 1 ether)); + assertEq(unstETHDetails.claimableAmount, ETHValues.from(i * 10 ether)); + } + } + + function test_getLockedUnstETHDetails_RevertOn_UnstETHNotLocked() external { + uint256 unstETHIdsCount = 4; + uint256[] memory unstETHIds = new uint256[](unstETHIdsCount); + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + address holder = address(uint160(uint256(keccak256(abi.encode(i))))); + + _accountingContext.unstETHRecords[unstETHIds[i]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus(i + 1); + _accountingContext.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(i * 1 ether); + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(i * 10 ether); + + _accountingContext.assets[holder].unstETHIds.push(unstETHIds[i]); + } + + uint256 notLockedUnstETHId = genRandomUnstEthId(5); + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, notLockedUnstETHId, UnstETHRecordStatus.NotLocked + ) + ); + AssetsAccounting.getLockedUnstETHDetails(_accountingContext, notLockedUnstETHId); + } + // --- // checkMinAssetsLockDurationPassed // --- From 9d03b29ced14ce3f12c69a4a70a54a8bd5621f4a Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 19:53:08 +0400 Subject: [PATCH 096/107] Add getEscrowState() unit test --- contracts/Escrow.sol | 3 ++- test/unit/Escrow.t.sol | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index beaec06e..6d4e428c 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -389,7 +389,8 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { { unstETHDetails = new LockedUnstETHDetails[](unstETHIds.length); - for (uint256 i = 0; i < unstETHIds.length; ++i) { + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { unstETHDetails[i] = _accounting.getLockedUnstETHDetails(unstETHIds[i]); } } diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index 6a689960..1572bc39 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -140,6 +140,18 @@ contract EscrowUnitTests is UnitTest { instance.initialize(Durations.ZERO); } + // --- + // getEscrowState() + // --- + + function test_getEscrowState_HappyPath() external { + assertTrue(_masterCopy.getEscrowState() == EscrowState.NotInitialized); + assertTrue(_escrow.getEscrowState() == EscrowState.SignallingEscrow); + + _transitToRageQuit(); + assertTrue(_escrow.getEscrowState() == EscrowState.RageQuitEscrow); + } + // --- // lockStETH() // --- From 38cbf38cfc342f17d8bd9bba4070f4b02fd33d62 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 20:11:57 +0400 Subject: [PATCH 097/107] AssetsAccounting test improvement. Small gas tweaks --- contracts/Escrow.sol | 20 ++++++++++---------- test/unit/libraries/AssetsAccounting.t.sol | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 6d4e428c..1a24919e 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -387,9 +387,9 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { view returns (LockedUnstETHDetails[] memory unstETHDetails) { - unstETHDetails = new LockedUnstETHDetails[](unstETHIds.length); - uint256 unstETHIdsCount = unstETHIds.length; + unstETHDetails = new LockedUnstETHDetails[](unstETHIdsCount); + for (uint256 i = 0; i < unstETHIdsCount; ++i) { unstETHDetails[i] = _accounting.getLockedUnstETHDetails(unstETHIds[i]); } @@ -563,13 +563,6 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { // Rage Quit Escrow: Getters // --- - /// @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) { - _escrowState.checkRageQuitEscrow(); - return _escrowState.isRageQuitExtensionPeriodPassed(); - } - /// @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. @@ -593,6 +586,13 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); } + /// @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) { + _escrowState.checkRageQuitEscrow(); + return _escrowState.isRageQuitExtensionPeriodPassed(); + } + /// @notice Retrieves details about the current state of the rage quit escrow. /// @return details A `RageQuitEscrowDetails` struct containing the following fields: /// - `isRageQuitExtensionPeriodStarted`: Indicates whether the rage quit extension period has started. @@ -602,10 +602,10 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory details) { _escrowState.checkRageQuitEscrow(); - details.isRageQuitExtensionPeriodStarted = _escrowState.isRageQuitExtensionPeriodStarted(); details.rageQuitEthWithdrawalsDelay = _escrowState.rageQuitEthWithdrawalsDelay; details.rageQuitExtensionPeriodDuration = _escrowState.rageQuitExtensionPeriodDuration; details.rageQuitExtensionPeriodStartedAt = _escrowState.rageQuitExtensionPeriodStartedAt; + details.isRageQuitExtensionPeriodStarted = _escrowState.isRageQuitExtensionPeriodStarted(); } // --- diff --git a/test/unit/libraries/AssetsAccounting.t.sol b/test/unit/libraries/AssetsAccounting.t.sol index 2ff5a3d9..8e2e6734 100644 --- a/test/unit/libraries/AssetsAccounting.t.sol +++ b/test/unit/libraries/AssetsAccounting.t.sol @@ -1433,8 +1433,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.unstETHRecords[unstETHIds[i]].lockedBy = holder; _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus(i + 1); - _accountingContext.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(i * 1 ether); - _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(i * 10 ether); + _accountingContext.unstETHRecords[unstETHIds[i]].shares = SharesValues.from((i + 1) * 1 ether); + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from((i + 1) * 10 ether); _accountingContext.assets[holder].unstETHIds.push(unstETHIds[i]); } @@ -1446,8 +1446,8 @@ contract AssetsAccountingUnitTests is UnitTest { assertEq(unstETHDetails.id, unstETHIds[i]); assertEq(unstETHDetails.status, UnstETHRecordStatus(i + 1)); assertEq(unstETHDetails.lockedBy, address(uint160(uint256(keccak256(abi.encode(i)))))); - assertEq(unstETHDetails.shares, SharesValues.from(i * 1 ether)); - assertEq(unstETHDetails.claimableAmount, ETHValues.from(i * 10 ether)); + assertEq(unstETHDetails.shares, SharesValues.from((i + 1) * 1 ether)); + assertEq(unstETHDetails.claimableAmount, ETHValues.from((i + 1) * 10 ether)); } } From b9852146385e6224b6c90f0c2db7ae0ce302b50c Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 20:19:34 +0400 Subject: [PATCH 098/107] IGovernance.ProposalReported() -> IGovernance.ProposalSubmitted() --- contracts/DualGovernance.sol | 2 +- contracts/TimelockedGovernance.sol | 2 +- contracts/interfaces/IGovernance.sol | 2 +- test/unit/DualGovernance.t.sol | 2 +- test/unit/TimelockedGovernance.t.sol | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 26123b27..8ff969e9 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -190,7 +190,7 @@ contract DualGovernance is IDualGovernance { Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); proposalId = TIMELOCK.submit(proposer.executor, calls); - emit ProposalReported(proposer.account, proposalId, metadata); + emit ProposalSubmitted(proposer.account, proposalId, metadata); } /// @notice Schedules a previously submitted proposal for execution in the Dual Governance system. diff --git a/contracts/TimelockedGovernance.sol b/contracts/TimelockedGovernance.sol index 5cc4fb44..348d927e 100644 --- a/contracts/TimelockedGovernance.sol +++ b/contracts/TimelockedGovernance.sol @@ -52,7 +52,7 @@ contract TimelockedGovernance is IGovernance { ) external returns (uint256 proposalId) { _checkCallerIsGovernance(); proposalId = TIMELOCK.submit(TIMELOCK.getAdminExecutor(), calls); - emit ProposalReported(msg.sender, proposalId, metadata); + emit ProposalSubmitted(msg.sender, proposalId, metadata); } /// @notice Schedules a submitted proposal. diff --git a/contracts/interfaces/IGovernance.sol b/contracts/interfaces/IGovernance.sol index fd3421a1..ebf31d6d 100644 --- a/contracts/interfaces/IGovernance.sol +++ b/contracts/interfaces/IGovernance.sol @@ -6,7 +6,7 @@ import {ITimelock} from "./ITimelock.sol"; import {ExternalCall} from "../libraries/ExternalCalls.sol"; interface IGovernance { - event ProposalReported(address indexed proposerAccount, uint256 indexed proposalId, string metadata); + event ProposalSubmitted(address indexed proposerAccount, uint256 indexed proposalId, string metadata); function TIMELOCK() external view returns (ITimelock); function submitProposal( diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 67c7c9ea..ece1ea86 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -200,7 +200,7 @@ contract DualGovernanceUnitTests is UnitTest { string memory metadata = "New proposal description"; vm.expectEmit(); - emit IGovernance.ProposalReported(proposer.account, expectedProposalId, metadata); + emit IGovernance.ProposalSubmitted(proposer.account, expectedProposalId, metadata); uint256 proposalId = _dualGovernance.submitProposal(calls, metadata); uint256[] memory submittedProposals = _timelock.getSubmittedProposals(); diff --git a/test/unit/TimelockedGovernance.t.sol b/test/unit/TimelockedGovernance.t.sol index 80b83b45..10a6f35e 100644 --- a/test/unit/TimelockedGovernance.t.sol +++ b/test/unit/TimelockedGovernance.t.sol @@ -48,7 +48,7 @@ contract TimelockedGovernanceUnitTests is UnitTest { string memory metadata = "proposal description"; vm.expectEmit(); - emit IGovernance.ProposalReported(_governance, expectedProposalId, metadata); + emit IGovernance.ProposalSubmitted(_governance, expectedProposalId, metadata); vm.prank(_governance); _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1)), metadata); From a8f3edecc90ec0c59a2378ad72edd79a6bdd8f0c Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 6 Dec 2024 19:44:06 +0300 Subject: [PATCH 099/107] using IResealManager --- contracts/DualGovernance.sol | 4 ++-- contracts/interfaces/IDualGovernance.sol | 2 +- contracts/libraries/Resealer.sol | 10 +++++----- test/unit/DualGovernance.t.sol | 5 +++-- test/unit/libraries/Resealer.t.sol | 12 ++++++------ 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index f01eeeff..b8c4ed31 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -169,7 +169,7 @@ contract DualGovernance is IDualGovernance { emit EscrowMasterCopyDeployed(escrowMasterCopy); _stateMachine.initialize(components.configProvider, escrowMasterCopy); - _resealer.setResealManager(address(components.resealManager)); + _resealer.setResealManager(components.resealManager); } // --- @@ -532,7 +532,7 @@ contract DualGovernance is IDualGovernance { /// @notice Sets the address of the Reseal Manager. /// @param newResealManager The address of the new Reseal Manager. - function setResealManager(address newResealManager) external { + function setResealManager(IResealManager newResealManager) external { _checkCallerIsAdminExecutor(); _resealer.setResealManager(newResealManager); } diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 721b2eba..336b1d82 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -47,7 +47,7 @@ interface IDualGovernance is IGovernance, ITiebreaker { function resealSealable(address sealable) external; function setResealCommittee(address newResealCommittee) external; - function setResealManager(address newResealManager) external; + function setResealManager(IResealManager newResealManager) external; function getResealManager() external view returns (IResealManager); function getResealCommittee() external view returns (address); } diff --git a/contracts/libraries/Resealer.sol b/contracts/libraries/Resealer.sol index 176a6fb1..4b176512 100644 --- a/contracts/libraries/Resealer.sol +++ b/contracts/libraries/Resealer.sol @@ -9,7 +9,7 @@ library Resealer { // --- // Errors // --- - error InvalidResealManager(address resealManager); + error InvalidResealManager(IResealManager resealManager); error InvalidResealCommittee(address resealCommittee); error CallerIsNotResealCommittee(address caller); @@ -17,7 +17,7 @@ library Resealer { // Events // --- event ResealCommitteeSet(address resealCommittee); - event ResealManagerSet(address resealManager); + event ResealManagerSet(IResealManager resealManager); // --- // Data Types @@ -35,11 +35,11 @@ library Resealer { /// @dev Sets a new Reseal Manager contract address. /// @param self The context struct containing the current state. /// @param newResealManager The address of the new Reseal Manager. - function setResealManager(Context storage self, address newResealManager) internal { - if (newResealManager == address(self.resealManager) || newResealManager == address(0)) { + function setResealManager(Context storage self, IResealManager newResealManager) internal { + if (newResealManager == self.resealManager || address(newResealManager) == address(0)) { revert InvalidResealManager(newResealManager); } - self.resealManager = IResealManager(newResealManager); + self.resealManager = newResealManager; emit ResealManagerSet(newResealManager); } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 561bcfbb..3b9c1088 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -173,7 +173,7 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.EscrowMasterCopyDeployed(IEscrowBase(predictedEscrowCopyAddress)); vm.expectEmit(); - emit Resealer.ResealManagerSet(address(_RESEAL_MANAGER_STUB)); + emit Resealer.ResealManagerSet(_RESEAL_MANAGER_STUB); Duration minTiebreakerActivationTimeout = Durations.from(30 days); Duration maxTiebreakerActivationTimeout = Durations.from(180 days); @@ -2371,10 +2371,11 @@ contract DualGovernanceUnitTests is UnitTest { function test_setResealManger_RevertOn_CallerIsNotAdminExecutor(address stranger) external { vm.assume(stranger != address(_executor)); + address newResealManager = makeAddr("NEW_RESEAL_MANAGER"); vm.prank(stranger); vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, stranger)); - _dualGovernance.setResealManager(address(0x123)); + _dualGovernance.setResealManager(IResealManager(newResealManager)); } // --- diff --git a/test/unit/libraries/Resealer.t.sol b/test/unit/libraries/Resealer.t.sol index dddd84da..f06c9903 100644 --- a/test/unit/libraries/Resealer.t.sol +++ b/test/unit/libraries/Resealer.t.sol @@ -18,22 +18,22 @@ contract ResealerTest is UnitTest { ctx.resealCommittee = resealCommittee; } - function test_setResealManager_HappyPath(address newResealManager) external { - vm.assume(newResealManager != address(ctx.resealManager) && newResealManager != address(0)); + function testFuzz_setResealManager_HappyPath(IResealManager newResealManager) external { + vm.assume(newResealManager != ctx.resealManager && address(newResealManager) != address(0)); vm.expectEmit(); emit Resealer.ResealManagerSet(newResealManager); this.external__setResealManager(newResealManager); - assertEq(address(ctx.resealManager), newResealManager); + assertEq(address(ctx.resealManager), address(newResealManager)); } function test_setResealManager_RevertOn_InvalidResealManager() external { vm.expectRevert(abi.encodeWithSelector(Resealer.InvalidResealManager.selector, address(ctx.resealManager))); - this.external__setResealManager(address(ctx.resealManager)); + this.external__setResealManager(ctx.resealManager); vm.expectRevert(abi.encodeWithSelector(Resealer.InvalidResealManager.selector, address(0))); - this.external__setResealManager(address(0)); + this.external__setResealManager(IResealManager(address(0))); } function testFuzz_setResealCommittee_HappyPath(address newResealCommittee) external { @@ -72,7 +72,7 @@ contract ResealerTest is UnitTest { ctx.setResealCommittee(newResealCommittee); } - function external__setResealManager(address newResealManager) external { + function external__setResealManager(IResealManager newResealManager) external { ctx.setResealManager(newResealManager); } } From 77c1117a24578f676c7408ba16d89fb34254fe5d Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 03:10:09 +0400 Subject: [PATCH 100/107] Update checks on unexpected state --- contracts/Escrow.sol | 14 ++-- contracts/libraries/EmergencyProtection.sol | 4 +- contracts/libraries/EscrowState.sol | 4 +- .../libraries/WithdrawalsBatchesQueue.sol | 35 +++------ test/scenario/escrow.t.sol | 14 ++-- test/scenario/happy-path-plan-b.t.sol | 10 +-- test/scenario/timelocked-governance.t.sol | 4 +- test/unit/EmergencyProtectedTimelock.t.sol | 12 +-- test/unit/Escrow.t.sol | 78 +++++++++++-------- test/unit/libraries/EmergencyProtection.t.sol | 2 +- test/unit/libraries/EscrowState.t.sol | 15 ++-- .../libraries/WithdrawalBatchesQueue.t.sol | 65 +++++++++++----- 12 files changed, 144 insertions(+), 113 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 1a24919e..aba6720c 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -35,11 +35,11 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { error EmptyUnstETHIds(); error UnclaimedBatches(); - error UnexpectedUnstETHId(); error UnfinalizedUnstETHIds(); error NonProxyCallsForbidden(); error BatchesQueueIsNotClosed(); error InvalidBatchSize(uint256 size); + error InvalidFromUnstETHId(uint256 unstETHId); error CallerIsNotDualGovernance(address caller); error InvalidHintsLength(uint256 actual, uint256 expected); error InvalidETHSender(address actual, address expected); @@ -489,15 +489,15 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { revert BatchesQueueIsNotClosed(); } - /// @dev This check is primarily required when only unstETH NFTs are locked in the Escrow - /// and there are no WithdrawalsBatches. In this scenario, the RageQuitExtensionPeriod can only begin - /// when the last locked unstETH id is finalized in the WithdrawalQueue. - /// When the WithdrawalsBatchesQueue is not empty, this invariant is maintained by the following: + /// @dev This check is required when only unstETH NFTs are locked in the Escrow and there are no WithdrawalsBatches. + /// In this scenario, the RageQuitExtensionPeriod can only begin when the last locked unstETH id is finalized + /// in the WithdrawalQueue. When the WithdrawalsBatchesQueue 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()) { + if (_batchesQueue.getBoundaryUnstETHId() > WITHDRAWAL_QUEUE.getLastFinalizedRequestId()) { revert UnfinalizedUnstETHIds(); } @@ -629,7 +629,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { uint256[] memory hints ) internal { if (fromUnstETHId != unstETHIds[0]) { - revert UnexpectedUnstETHId(); + revert InvalidFromUnstETHId(fromUnstETHId); } if (hints.length != unstETHIds.length) { diff --git a/contracts/libraries/EmergencyProtection.sol b/contracts/libraries/EmergencyProtection.sol index b3df9833..4543a79a 100644 --- a/contracts/libraries/EmergencyProtection.sol +++ b/contracts/libraries/EmergencyProtection.sol @@ -22,7 +22,7 @@ library EmergencyProtection { error InvalidEmergencyExecutionCommittee(address committee); error InvalidEmergencyModeDuration(Duration value); error InvalidEmergencyProtectionEndDate(Timestamp value); - error UnexpectedEmergencyModeState(bool value); + error UnexpectedEmergencyModeState(bool state); // --- // Events @@ -197,7 +197,7 @@ library EmergencyProtection { /// @param isActive The expected value of the emergency mode. function checkEmergencyMode(Context storage self, bool isActive) internal view { if (isEmergencyModeActive(self) != isActive) { - revert UnexpectedEmergencyModeState(isActive); + revert UnexpectedEmergencyModeState(!isActive); } } diff --git a/contracts/libraries/EscrowState.sol b/contracts/libraries/EscrowState.sol index 9fb30ff3..dbfbee59 100644 --- a/contracts/libraries/EscrowState.sol +++ b/contracts/libraries/EscrowState.sol @@ -26,7 +26,7 @@ library EscrowState { // --- error ClaimingIsFinished(); - error UnexpectedState(State value); + error UnexpectedEscrowState(State state); error EthWithdrawalsDelayNotPassed(); error RageQuitExtensionPeriodNotStarted(); error InvalidMinAssetsLockDuration(Duration newMinAssetsLockDuration); @@ -184,7 +184,7 @@ library EscrowState { /// @param state The expected state. function _checkState(Context storage self, State state) private view { if (self.state != state) { - revert UnexpectedState(state); + revert UnexpectedEscrowState(self.state); } } diff --git a/contracts/libraries/WithdrawalsBatchesQueue.sol b/contracts/libraries/WithdrawalsBatchesQueue.sol index 6adf5b5b..b5a1f574 100644 --- a/contracts/libraries/WithdrawalsBatchesQueue.sol +++ b/contracts/libraries/WithdrawalsBatchesQueue.sol @@ -23,9 +23,7 @@ library WithdrawalsBatchesQueue { error EmptyBatch(); error InvalidUnstETHIdsSequence(); - error WithdrawalsBatchesQueueIsInAbsentState(); - error WithdrawalsBatchesQueueIsNotInOpenedState(); - error WithdrawalsBatchesQueueIsNotInAbsentState(); + error UnexpectedWithdrawalsBatchesQueueState(State state); // --- // Events @@ -91,9 +89,7 @@ library WithdrawalsBatchesQueue { /// @param boundaryUnstETHId The id of the unstETH NFT which is used as the boundary value for the withdrawal queue. /// `boundaryUnstETHId` value is used as a lower bound for the adding unstETH ids. function open(Context storage self, uint256 boundaryUnstETHId) internal { - if (self.info.state != State.Absent) { - revert WithdrawalsBatchesQueueIsNotInAbsentState(); - } + _checkState(self, State.Absent); self.info.state = State.Opened; @@ -108,7 +104,7 @@ library WithdrawalsBatchesQueue { /// @param self The context of the Withdrawals Batches Queue library. /// @param unstETHIds An array of sequential unstETH ids to be added to the queue. function addUnstETHIds(Context storage self, uint256[] memory unstETHIds) internal { - _checkInOpenedState(self); + _checkState(self, State.Opened); uint256 unstETHIdsCount = unstETHIds.length; @@ -163,7 +159,7 @@ library WithdrawalsBatchesQueue { /// @notice Closes the WithdrawalsBatchesQueue, preventing further batch additions. /// @param self The context of the Withdrawals Batches Queue library. function close(Context storage self) internal { - _checkInOpenedState(self); + _checkState(self, State.Opened); self.info.state = State.Closed; emit WithdrawalsBatchesQueueClosed(); } @@ -220,13 +216,12 @@ library WithdrawalsBatchesQueue { return self.info.totalUnstETHIdsCount - self.info.totalUnstETHIdsClaimed; } - /// @notice Returns the id of the last claimed unstETH. When the queue is empty, returns 0. + /// @notice Returns the ID of the boundary unstETH. + /// @dev Reverts with an index OOB error if called when the `WithdrawalsBatchesQueue` is in the `Absent` state. /// @param self The context of the Withdrawals Batches Queue library. - /// @return lastClaimedUnstETHId The id of the lastClaimedUnstETHId or 0 when the queue is empty - function getLastClaimedOrBoundaryUnstETHId(Context storage self) internal view returns (uint256) { - _checkNotInAbsentState(self); - QueueInfo memory info = self.info; - return self.batches[info.lastClaimedBatchIndex].firstUnstETHId + info.lastClaimedUnstETHIdIndex; + /// @return boundaryUnstETHId The id of the boundary unstETH. + function getBoundaryUnstETHId(Context storage self) internal view returns (uint256) { + return self.batches[0].firstUnstETHId; } /// @notice Returns if all unstETH ids in the queue have been claimed. @@ -277,15 +272,9 @@ library WithdrawalsBatchesQueue { info.totalUnstETHIdsClaimed += SafeCast.toUint64(unstETHIdsCount); } - function _checkNotInAbsentState(Context storage self) private view { - if (self.info.state == State.Absent) { - revert WithdrawalsBatchesQueueIsInAbsentState(); - } - } - - function _checkInOpenedState(Context storage self) private view { - if (self.info.state != State.Opened) { - revert WithdrawalsBatchesQueueIsNotInOpenedState(); + function _checkState(Context storage self, State expectedState) private view { + if (self.info.state != expectedState) { + revert UnexpectedWithdrawalsBatchesQueueState(self.info.state); } } } diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 26604958..8470f52c 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -488,22 +488,22 @@ contract EscrowHappyPath is ScenarioTestBlueprint { // After the Escrow enters RageQuitEscrow state, lock/unlock of tokens is forbidden // --- - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); this.externalLockStETH(_VETOER_1, 1 ether); - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); this.externalLockWstETH(_VETOER_1, 1 ether); - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); this.externalLockUnstETH(_VETOER_1, notLockedWithdrawalNfts); - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); this.externalUnlockStETH(_VETOER_1); - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); this.externalUnlockWstETH(_VETOER_1); - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); this.externalUnlockUnstETH(_VETOER_1, lockedWithdrawalNfts); } @@ -547,7 +547,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(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); this.externalUnlockStETH(_VETOER_1); } diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index 6681778d..aa6c12fd 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -97,7 +97,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // but the call still not executable _assertCanExecute(maliciousProposalId, false); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _executeProposal(maliciousProposalId); } @@ -325,7 +325,7 @@ contract PlanBSetup is ScenarioTestBlueprint { _wait(_timelock.getAfterScheduleDelay().plusSeconds(1)); _assertCanExecute(maliciousProposalId, false); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _executeProposal(maliciousProposalId); } @@ -350,7 +350,7 @@ contract PlanBSetup is ScenarioTestBlueprint { _wait(_timelock.getAfterScheduleDelay().plusSeconds(1)); _assertCanExecute(anotherMaliciousProposalId, false); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _executeProposal(anotherMaliciousProposalId); } @@ -359,10 +359,10 @@ contract PlanBSetup is ScenarioTestBlueprint { _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); assertTrue(emergencyState.emergencyModeEndsAfter < Timestamps.now()); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _executeProposal(maliciousProposalId); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _executeProposal(anotherMaliciousProposalId); } diff --git a/test/scenario/timelocked-governance.t.sol b/test/scenario/timelocked-governance.t.sol index d6be5be5..c822e75f 100644 --- a/test/scenario/timelocked-governance.t.sol +++ b/test/scenario/timelocked-governance.t.sol @@ -99,7 +99,7 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { _assertCanExecute(maliciousProposalId, false); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _executeProposal(maliciousProposalId); } @@ -174,7 +174,7 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { _assertCanExecute(maliciousProposalId, false); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _executeProposal(maliciousProposalId); } diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index b0881965..bc00a00c 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -272,7 +272,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _activateEmergencyMode(); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [false])); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _timelock.execute(1); ITimelock.ProposalDetails memory proposal = _timelock.getProposalDetails(1); @@ -561,7 +561,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), true); vm.prank(_emergencyActivator); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [false])); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _timelock.activateEmergencyMode(); assertEq(_isEmergencyStateActivated(), true); @@ -606,7 +606,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.isEmergencyModeActive(), false); vm.prank(_emergencyActivator); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); _timelock.emergencyExecute(1); } @@ -683,11 +683,11 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); _timelock.deactivateEmergencyMode(); vm.prank(_adminExecutor); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); _timelock.deactivateEmergencyMode(); } @@ -764,7 +764,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { address emergencyActivationCommitteeBefore = _timelock.getEmergencyActivationCommittee(); address emergencyExecutionCommitteeBefore = _timelock.getEmergencyExecutionCommittee(); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); vm.prank(_emergencyEnactor); _timelock.emergencyReset(); diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index 1572bc39..a421af46 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -198,7 +198,7 @@ contract EscrowUnitTests is UnitTest { function test_lockStETH_RevertOn_UnexpectedEscrowState() external { _transitToRageQuit(); - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); vm.prank(_vetoer); _escrow.lockStETH(1 ether); @@ -252,7 +252,7 @@ contract EscrowUnitTests is UnitTest { _transitToRageQuit(); - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); vm.prank(_vetoer); _escrow.unlockStETH(); } @@ -317,7 +317,7 @@ contract EscrowUnitTests is UnitTest { function test_lockWstETH_RevertOn_UnexpectedEscrowState() external { _transitToRageQuit(); - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); vm.prank(_vetoer); _escrow.lockWstETH(1 ether); } @@ -376,7 +376,7 @@ contract EscrowUnitTests is UnitTest { _transitToRageQuit(); - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); vm.prank(_vetoer); _escrow.unlockWstETH(); } @@ -459,7 +459,7 @@ contract EscrowUnitTests is UnitTest { _transitToRageQuit(); - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); _escrow.lockUnstETH(unstethIds); } @@ -532,7 +532,7 @@ contract EscrowUnitTests is UnitTest { _transitToRageQuit(); uint256[] memory unstethIds = new uint256[](1); - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); vm.prank(_vetoer); _escrow.unlockUnstETH(unstethIds); } @@ -569,7 +569,7 @@ contract EscrowUnitTests is UnitTest { uint256[] memory unstethIds = new uint256[](0); uint256[] memory hints = new uint256[](0); - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); _escrow.markUnstETHFinalized(unstethIds, hints); } @@ -630,7 +630,7 @@ contract EscrowUnitTests is UnitTest { } function test_requestNextWithdrawalsBatch_RevertOn_UnexpectedEscrowState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); _escrow.requestNextWithdrawalsBatch(1); } @@ -711,8 +711,8 @@ contract EscrowUnitTests is UnitTest { assertEq(state.unstETHLockedShares.toUint256(), 0); } - function test_claimNextWithdrawalsBatch_2_RevertOn_UnexpectedState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, 2)); + function test_claimNextWithdrawalsBatch_2_RevertOn_UnexpectedEscrowState() external { + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, 2)); _escrow.claimNextWithdrawalsBatch(1, new uint256[](1)); } @@ -733,7 +733,7 @@ contract EscrowUnitTests is UnitTest { _escrow.claimNextWithdrawalsBatch(1, new uint256[](1)); } - function test_claimNextWithdrawalsBatch_2_RevertOn_UnexpectedUnstETHId() external { + function test_claimNextWithdrawalsBatch_2_RevertOn_InvalidFromUnstETHId() external { uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); _vetoerLockedStEth(stethAmount); @@ -746,7 +746,7 @@ contract EscrowUnitTests is UnitTest { _withdrawalQueue.setClaimableAmount(stethAmount); vm.deal(address(_withdrawalQueue), stethAmount); - vm.expectRevert(Escrow.UnexpectedUnstETHId.selector); + vm.expectRevert(abi.encodeWithSelector(Escrow.InvalidFromUnstETHId.selector, unstEthIds)); _escrow.claimNextWithdrawalsBatch(unstEthIds[0] + 10, new uint256[](1)); } @@ -810,8 +810,8 @@ contract EscrowUnitTests is UnitTest { assertEq(vetoerState.unstETHLockedShares.toUint256(), 0); } - function test_claimNextWithdrawalsBatch_1_RevertOn_UnexpectedState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, 2)); + function test_claimNextWithdrawalsBatch_1_RevertOn_UnexpectedEscrowState() external { + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, 2)); _escrow.claimNextWithdrawalsBatch(1); } @@ -926,7 +926,7 @@ contract EscrowUnitTests is UnitTest { } function test_claimUnstETH_RevertOn_UnexpectedEscrowState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); _escrow.claimUnstETH(new uint256[](1), new uint256[](1)); } @@ -1045,7 +1045,7 @@ contract EscrowUnitTests is UnitTest { } function test_withdrawETH_RevertOn_UnexpectedEscrowState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); _escrow.withdrawETH(); } @@ -1167,7 +1167,7 @@ contract EscrowUnitTests is UnitTest { } function test_withdrawETH_2_RevertOn_UnexpectedEscrowState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); _escrow.withdrawETH(new uint256[](1)); } @@ -1383,19 +1383,19 @@ contract EscrowUnitTests is UnitTest { } function test_getNextWithdrawalBatch_RevertOn_RageQuit_IsNotStarted() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); _escrow.getNextWithdrawalBatch(100); } - function test_getNextWithdrawalBatch_RevertOn_UnexpectedState_Signaling() external { + function test_getNextWithdrawalBatch_RevertOn_UnexpectedEscrowState_Signaling() external { uint256 batchLimit = 10; - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); _escrow.getNextWithdrawalBatch(batchLimit); } - function test_getNextWithdrawalBatch_RevertOn_UnexpectedState_NotInitialized() external { + function test_getNextWithdrawalBatch_RevertOn_UnexpectedEscrowState_NotInitialized() external { uint256 batchLimit = 10; - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized)); _masterCopy.getNextWithdrawalBatch(batchLimit); } @@ -1414,16 +1414,30 @@ contract EscrowUnitTests is UnitTest { assertTrue(_escrow.isWithdrawalsBatchesClosed()); } - function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedState_Signaling() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedEscrowState_Signaling() external { + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); _escrow.isWithdrawalsBatchesClosed(); } - function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedState_NotInitialized() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedEscrowState_NotInitialized() external { + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized)); _masterCopy.isWithdrawalsBatchesClosed(); } + // --- + // getUnclaimedUnstETHIdsCount() + // --- + + function test_getUnclaimedUnstETHIdsCount_RevertOn_UnexpectedEscrowState_Signaling() external { + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); + _escrow.getUnclaimedUnstETHIdsCount(); + } + + function test_getUnclaimedUnstETHIdsCount_RevertOn_UnexpectedEscrowState_NotInitialized() external { + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized)); + _masterCopy.getUnclaimedUnstETHIdsCount(); + } + // --- // isRageQuitExtensionPeriodStarted() // --- @@ -1447,12 +1461,12 @@ contract EscrowUnitTests is UnitTest { // --- function test_getRageQuitExtensionPeriodStartedAt_RevertOn_NotInitializedState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); _masterCopy.getRageQuitEscrowDetails(); } function test_getRageQuitExtensionPeriodStartedAt_RevertOn_SignallingState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); _escrow.getRageQuitEscrowDetails().rageQuitExtensionPeriodStartedAt; } @@ -1522,13 +1536,13 @@ contract EscrowUnitTests is UnitTest { // getRageQuitEscrowDetails() // --- - function test_getRageQuitEscrowDetails_RevertOn_UnexpectedState_Signaling() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + function test_getRageQuitEscrowDetails_RevertOn_UnexpectedEscrowState_Signaling() external { + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); _escrow.getRageQuitEscrowDetails(); } - function test_getRageQuitEscrowDetails_RevertOn_UnexpectedState_NotInitialized() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedState.selector, EscrowState.RageQuitEscrow)); + function test_getRageQuitEscrowDetails_RevertOn_UnexpectedEscrowState_NotInitialized() external { + vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); _masterCopy.getRageQuitEscrowDetails(); } diff --git a/test/unit/libraries/EmergencyProtection.t.sol b/test/unit/libraries/EmergencyProtection.t.sol index 4fbb01ab..28c238c9 100644 --- a/test/unit/libraries/EmergencyProtection.t.sol +++ b/test/unit/libraries/EmergencyProtection.t.sol @@ -194,7 +194,7 @@ contract EmergencyProtectionTest is UnitTest { } function test_checkEmergencyMode_RevertOn_NotInEmergencyMode() external { - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); EmergencyProtection.checkEmergencyMode(ctx, true); } diff --git a/test/unit/libraries/EscrowState.t.sol b/test/unit/libraries/EscrowState.t.sol index c7685468..ecb91ee2 100644 --- a/test/unit/libraries/EscrowState.t.sol +++ b/test/unit/libraries/EscrowState.t.sol @@ -38,8 +38,7 @@ contract EscrowStateUnitTests is UnitTest { 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] - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.NotInitialized)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.SignallingEscrow)); EscrowState.initialize(_context, minAssetsLockDuration); } @@ -75,7 +74,7 @@ contract EscrowStateUnitTests is UnitTest { ) external { _context.state = State.NotInitialized; - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.NotInitialized)); EscrowState.startRageQuit(_context, rageQuitExtensionPeriodDuration, rageQuitEthWithdrawalsDelay); } @@ -139,8 +138,11 @@ contract EscrowStateUnitTests is UnitTest { } function test_checkSignallingEscrow_RevertOn_InvalidState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.NotInitialized)); + EscrowState.checkSignallingEscrow(_context); + _context.state = State.RageQuitEscrow; + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); EscrowState.checkSignallingEscrow(_context); } @@ -154,8 +156,11 @@ contract EscrowStateUnitTests is UnitTest { } function test_checkRageQuitEscrow_RevertOn_InvalidState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.NotInitialized)); + EscrowState.checkRageQuitEscrow(_context); + _context.state = State.SignallingEscrow; + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.SignallingEscrow)); EscrowState.checkRageQuitEscrow(_context); } diff --git a/test/unit/libraries/WithdrawalBatchesQueue.t.sol b/test/unit/libraries/WithdrawalBatchesQueue.t.sol index 4ea20861..9b465c70 100644 --- a/test/unit/libraries/WithdrawalBatchesQueue.t.sol +++ b/test/unit/libraries/WithdrawalBatchesQueue.t.sol @@ -31,7 +31,11 @@ contract WithdrawalsBatchesQueueTest is UnitTest { _batchesQueue.info.state = State.Opened; assertEq(_batchesQueue.info.state, State.Opened); - vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalsBatchesQueueIsNotInAbsentState.selector); + vm.expectRevert( + abi.encodeWithSelector( + WithdrawalsBatchesQueue.UnexpectedWithdrawalsBatchesQueueState.selector, State.Opened + ) + ); _batchesQueue.open(_DEFAULT_BOUNDARY_UNST_ETH_ID); } @@ -126,8 +130,22 @@ contract WithdrawalsBatchesQueueTest is UnitTest { ); } - function test_addUnstETHIds_RevertOn_QueueNotInOpenedState() external { - vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalsBatchesQueueIsNotInOpenedState.selector); + function test_addUnstETHIds_RevertOn_QueueInAbsentState() external { + vm.expectRevert( + abi.encodeWithSelector( + WithdrawalsBatchesQueue.UnexpectedWithdrawalsBatchesQueueState.selector, State.Absent + ) + ); + _batchesQueue.addUnstETHIds(new uint256[](0)); + } + + function test_addUnstETHIds_RevertOn_QueueInClosedState() external { + _batchesQueue.info.state = State.Closed; + vm.expectRevert( + abi.encodeWithSelector( + WithdrawalsBatchesQueue.UnexpectedWithdrawalsBatchesQueueState.selector, State.Closed + ) + ); _batchesQueue.addUnstETHIds(new uint256[](0)); } @@ -349,13 +367,21 @@ contract WithdrawalsBatchesQueueTest is UnitTest { } function test_close_RevertOn_QueueNotInOpenedState() external { - vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalsBatchesQueueIsNotInOpenedState.selector); + vm.expectRevert( + abi.encodeWithSelector( + WithdrawalsBatchesQueue.UnexpectedWithdrawalsBatchesQueueState.selector, State.Absent + ) + ); _batchesQueue.close(); _batchesQueue.open({boundaryUnstETHId: 1}); _batchesQueue.close(); - vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalsBatchesQueueIsNotInOpenedState.selector); + vm.expectRevert( + abi.encodeWithSelector( + WithdrawalsBatchesQueue.UnexpectedWithdrawalsBatchesQueueState.selector, State.Closed + ) + ); _batchesQueue.close(); } @@ -551,43 +577,40 @@ contract WithdrawalsBatchesQueueTest is UnitTest { } // --- - // getLastClaimedOrBoundaryUnstETHId() + // getBoundaryUnstETHId() // --- - function test_getLastClaimedOrBoundaryUnstETHId_HappyPath_EmptyQueueReturnsBoundaryUnstETHId() external { + function test_getBoundaryUnstETHId_HappyPath_EmptyQueue() external { _openBatchesQueue(); - assertEq(_batchesQueue.getLastClaimedOrBoundaryUnstETHId(), _DEFAULT_BOUNDARY_UNST_ETH_ID); + _batchesQueue.close(); + assertEq(_batchesQueue.getBoundaryUnstETHId(), _DEFAULT_BOUNDARY_UNST_ETH_ID); } - function test_getLastClaimedOrBoundaryUnstETHId_HappyPath_NotEmptyQueueReturnsLastClaimedUnstETHId() external { + function test_getBoundaryUnstETHId_HappyPath_NotEmptyQueue() external { _openBatchesQueue(); + uint256 unstETHIdsCount = 5; uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); _batchesQueue.addUnstETHIds(unstETHIds); assertEq(_batchesQueue.info.totalUnstETHIdsCount, 5); - assertEq(_batchesQueue.getLastClaimedOrBoundaryUnstETHId(), _DEFAULT_BOUNDARY_UNST_ETH_ID); - - uint256 maxUnstETHIdsCount = 3; - _batchesQueue.claimNextBatch(maxUnstETHIdsCount); - assertEq(_batchesQueue.getLastClaimedOrBoundaryUnstETHId(), unstETHIds[2]); + _batchesQueue.close(); - _batchesQueue.claimNextBatch(maxUnstETHIdsCount); - assertEq(_batchesQueue.getLastClaimedOrBoundaryUnstETHId(), unstETHIds[unstETHIds.length - 1]); + assertEq(_batchesQueue.getBoundaryUnstETHId(), _DEFAULT_BOUNDARY_UNST_ETH_ID); } - function test_getLastClaimedOrBoundaryUnstETHId_RevertOn_AbsentQueueState() external { - vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalsBatchesQueueIsInAbsentState.selector); - _batchesQueue.getLastClaimedOrBoundaryUnstETHId(); + function test_getBoundaryUnstETHId_RevertOn_AbsentQueueState() external { + vm.expectRevert(stdError.indexOOBError); + _batchesQueue.getBoundaryUnstETHId(); } - function test_getLastClaimedOrBoundaryUnstETHId_RevertOn_LastClaimedBatchIndexOutOfArrayBounds() external { + function test_getBoundaryUnstETHId_RevertOn_LastClaimedBatchIndexOutOfArrayBounds() external { _openBatchesQueue(); _batchesQueue.info.lastClaimedBatchIndex = 2; vm.expectRevert(stdError.indexOOBError); - _batchesQueue.getLastClaimedOrBoundaryUnstETHId(); + _batchesQueue.getBoundaryUnstETHId(); } // --- From 8e39d28e41f2b5cd67f9aea0a6c78864ddc43945 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 03:21:53 +0400 Subject: [PATCH 101/107] Use NotInitialized as default state for StateMachine and WithdrawalsBatchesQueue --- contracts/libraries/DualGovernanceStateMachine.sol | 8 ++++---- contracts/libraries/WithdrawalsBatchesQueue.sol | 9 +++++---- docs/specification.md | 2 +- .../libraries/DualGovernanceStateTransitions.t.sol | 6 +++--- test/unit/libraries/WithdrawalBatchesQueue.t.sol | 12 ++++++------ 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 83a6d73b..d384fc22 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -17,7 +17,7 @@ import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; import {DualGovernanceStateTransitions} from "./DualGovernanceStateTransitions.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 +/// @param NotInitialized 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. @@ -32,7 +32,7 @@ import {DualGovernanceStateTransitions} from "./DualGovernanceStateTransitions.s /// is triggered when the Second Seal Threshold is reached. During this state, the scheduling of proposals for /// execution is forbidden, but new proposals can still be submitted. enum State { - Unset, + NotInitialized, Normal, VetoSignalling, VetoSignallingDeactivation, @@ -118,7 +118,7 @@ library DualGovernanceStateMachine { IDualGovernanceConfigProvider configProvider, IEscrowBase escrowMasterCopy ) internal { - if (self.state != State.Unset) { + if (self.state != State.NotInitialized) { revert AlreadyInitialized(); } @@ -130,7 +130,7 @@ library DualGovernanceStateMachine { DualGovernanceConfig.Context memory config = configProvider.getDualGovernanceConfig(); _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); - emit DualGovernanceStateChanged(State.Unset, State.Normal, self); + emit DualGovernanceStateChanged(State.NotInitialized, State.Normal, self); } /// @notice Executes a state transition for the Dual Governance State Machine, if applicable. diff --git a/contracts/libraries/WithdrawalsBatchesQueue.sol b/contracts/libraries/WithdrawalsBatchesQueue.sol index b5a1f574..405fa029 100644 --- a/contracts/libraries/WithdrawalsBatchesQueue.sol +++ b/contracts/libraries/WithdrawalsBatchesQueue.sol @@ -5,11 +5,11 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; /// @notice The state of the WithdrawalBatchesQueue. -/// @param Absent The initial (uninitialized) state of the WithdrawalBatchesQueue. +/// @param NotInitialized The initial (uninitialized) state of the WithdrawalBatchesQueue. /// @param Opened In this state, the WithdrawalBatchesQueue allows the addition of new batches of unstETH ids. /// @param Closed The terminal state of the queue where adding new batches is no longer permitted. enum State { - Absent, + NotInitialized, Opened, Closed } @@ -89,7 +89,7 @@ library WithdrawalsBatchesQueue { /// @param boundaryUnstETHId The id of the unstETH NFT which is used as the boundary value for the withdrawal queue. /// `boundaryUnstETHId` value is used as a lower bound for the adding unstETH ids. function open(Context storage self, uint256 boundaryUnstETHId) internal { - _checkState(self, State.Absent); + _checkState(self, State.NotInitialized); self.info.state = State.Opened; @@ -217,7 +217,8 @@ library WithdrawalsBatchesQueue { } /// @notice Returns the ID of the boundary unstETH. - /// @dev Reverts with an index OOB error if called when the `WithdrawalsBatchesQueue` is in the `Absent` state. + /// @dev Reverts with an index OOB error if called when the `WithdrawalsBatchesQueue` is in the + /// `NotInitialized` state. /// @param self The context of the Withdrawals Batches Queue library. /// @return boundaryUnstETHId The id of the boundary unstETH. function getBoundaryUnstETHId(Context storage self) internal view returns (uint256) { diff --git a/docs/specification.md b/docs/specification.md index d62816f2..9d6ecb76 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -232,7 +232,7 @@ This contract is a singleton, meaning that any DG deployment includes exactly on ```solidity enum State { - Unset, // Indicates an uninitialized state during the contract creation + NotInitialized, // Indicates an uninitialized state during the contract creation Normal, VetoSignalling, VetoSignallingDeactivation, diff --git a/test/unit/libraries/DualGovernanceStateTransitions.t.sol b/test/unit/libraries/DualGovernanceStateTransitions.t.sol index 9c3abf47..d012b7ad 100644 --- a/test/unit/libraries/DualGovernanceStateTransitions.t.sol +++ b/test/unit/libraries/DualGovernanceStateTransitions.t.sol @@ -315,11 +315,11 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { } // --- - // Unset -> assert(false) + // NotInitialized -> assert(false) // --- - function test_getStateTransition_RevertOn_UnsetState() external { - _stateMachine.state = State.Unset; + function test_getStateTransition_RevertOn_NotInitializedState() external { + _stateMachine.state = State.NotInitialized; vm.expectRevert(stdError.assertionError); this.external__getStateTransition(); diff --git a/test/unit/libraries/WithdrawalBatchesQueue.t.sol b/test/unit/libraries/WithdrawalBatchesQueue.t.sol index 9b465c70..d22288aa 100644 --- a/test/unit/libraries/WithdrawalBatchesQueue.t.sol +++ b/test/unit/libraries/WithdrawalBatchesQueue.t.sol @@ -16,7 +16,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { // --- function test_open_HappyPath() external { - assertEq(_batchesQueue.info.state, State.Absent); + assertEq(_batchesQueue.info.state, State.NotInitialized); assertEq(_batchesQueue.batches.length, 0); _batchesQueue.open(_DEFAULT_BOUNDARY_UNST_ETH_ID); @@ -40,7 +40,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { } function test_open_Emit_WithdrawalsBatchesQueueOpened() external { - assertEq(_batchesQueue.info.state, State.Absent); + assertEq(_batchesQueue.info.state, State.NotInitialized); vm.expectEmit(true, false, false, false); emit WithdrawalsBatchesQueue.WithdrawalsBatchesQueueOpened(_DEFAULT_BOUNDARY_UNST_ETH_ID); @@ -130,10 +130,10 @@ contract WithdrawalsBatchesQueueTest is UnitTest { ); } - function test_addUnstETHIds_RevertOn_QueueInAbsentState() external { + function test_addUnstETHIds_RevertOn_QueueInNotInitializedState() external { vm.expectRevert( abi.encodeWithSelector( - WithdrawalsBatchesQueue.UnexpectedWithdrawalsBatchesQueueState.selector, State.Absent + WithdrawalsBatchesQueue.UnexpectedWithdrawalsBatchesQueueState.selector, State.NotInitialized ) ); _batchesQueue.addUnstETHIds(new uint256[](0)); @@ -369,7 +369,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { function test_close_RevertOn_QueueNotInOpenedState() external { vm.expectRevert( abi.encodeWithSelector( - WithdrawalsBatchesQueue.UnexpectedWithdrawalsBatchesQueueState.selector, State.Absent + WithdrawalsBatchesQueue.UnexpectedWithdrawalsBatchesQueueState.selector, State.NotInitialized ) ); _batchesQueue.close(); @@ -600,7 +600,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { assertEq(_batchesQueue.getBoundaryUnstETHId(), _DEFAULT_BOUNDARY_UNST_ETH_ID); } - function test_getBoundaryUnstETHId_RevertOn_AbsentQueueState() external { + function test_getBoundaryUnstETHId_RevertOn_NotInitializedQueueState() external { vm.expectRevert(stdError.indexOOBError); _batchesQueue.getBoundaryUnstETHId(); } From 835b5a02e23a4c4cec276b693b9608fdc0da6f24 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 04:18:18 +0400 Subject: [PATCH 102/107] Refactor ExecutableProposals invalid states error --- contracts/libraries/ExecutableProposals.sol | 56 +++++++++---------- test/scenario/dg-update-tokens-rotation.t.sol | 4 +- test/scenario/happy-path-plan-b.t.sol | 12 +++- test/unit/libraries/ExecutableProposals.t.sol | 54 +++++++++++++++--- 4 files changed, 83 insertions(+), 43 deletions(-) diff --git a/contracts/libraries/ExecutableProposals.sol b/contracts/libraries/ExecutableProposals.sol index 070af315..a1e4ab39 100644 --- a/contracts/libraries/ExecutableProposals.sol +++ b/contracts/libraries/ExecutableProposals.sol @@ -79,9 +79,7 @@ library ExecutableProposals { // --- error EmptyCalls(); - error ProposalNotFound(uint256 proposalId); - error ProposalNotScheduled(uint256 proposalId); - error ProposalNotSubmitted(uint256 proposalId); + error UnexpectedProposalStatus(uint256 proposalId, Status status); error AfterSubmitDelayNotPassed(uint256 proposalId); error AfterScheduleDelayNotPassed(uint256 proposalId); @@ -141,19 +139,21 @@ library ExecutableProposals { /// @param afterSubmitDelay The required delay duration after submission before the proposal can be scheduled. /// function schedule(Context storage self, uint256 proposalId, Duration afterSubmitDelay) internal { - ProposalData memory proposalState = self.proposals[proposalId].data; + ProposalData memory proposalData = self.proposals[proposalId].data; + + _checkProposalNotCancelled(self, proposalId, proposalData); - if (!_isProposalSubmitted(self, proposalId, proposalState)) { - revert ProposalNotSubmitted(proposalId); + if (proposalData.status != Status.Submitted) { + revert UnexpectedProposalStatus(proposalId, proposalData.status); } - if (afterSubmitDelay.addTo(proposalState.submittedAt) > Timestamps.now()) { + if (afterSubmitDelay.addTo(proposalData.submittedAt) > Timestamps.now()) { revert AfterSubmitDelayNotPassed(proposalId); } - proposalState.status = Status.Scheduled; - proposalState.scheduledAt = Timestamps.now(); - self.proposals[proposalId].data = proposalState; + proposalData.status = Status.Scheduled; + proposalData.scheduledAt = Timestamps.now(); + self.proposals[proposalId].data = proposalData; emit ProposalScheduled(proposalId); } @@ -166,8 +166,10 @@ library ExecutableProposals { function execute(Context storage self, uint256 proposalId, Duration afterScheduleDelay) internal { Proposal memory proposal = self.proposals[proposalId]; - if (!_isProposalScheduled(self, proposalId, proposal.data)) { - revert ProposalNotScheduled(proposalId); + _checkProposalNotCancelled(self, proposalId, proposal.data); + + if (proposal.data.status != Status.Scheduled) { + revert UnexpectedProposalStatus(proposalId, proposal.data.status); } if (afterScheduleDelay.addTo(proposal.data.scheduledAt) > Timestamps.now()) { @@ -202,9 +204,9 @@ library ExecutableProposals { uint256 proposalId, Duration afterScheduleDelay ) internal view returns (bool) { - ProposalData memory proposalState = self.proposals[proposalId].data; - return _isProposalScheduled(self, proposalId, proposalState) - && Timestamps.now() >= afterScheduleDelay.addTo(proposalState.scheduledAt); + ProposalData memory proposalData = self.proposals[proposalId].data; + return proposalId > self.lastCancelledProposalId && proposalData.status == Status.Scheduled + && Timestamps.now() >= afterScheduleDelay.addTo(proposalData.scheduledAt); } /// @notice Determines whether a proposal is eligible to be scheduled based on its status and required delay. @@ -217,9 +219,9 @@ library ExecutableProposals { uint256 proposalId, Duration afterSubmitDelay ) internal view returns (bool) { - ProposalData memory proposalState = self.proposals[proposalId].data; - return _isProposalSubmitted(self, proposalId, proposalState) - && Timestamps.now() >= afterSubmitDelay.addTo(proposalState.submittedAt); + ProposalData memory proposalData = self.proposals[proposalId].data; + return proposalId > self.lastCancelledProposalId && proposalData.status == Status.Submitted + && Timestamps.now() >= afterSubmitDelay.addTo(proposalData.submittedAt); } /// @notice Returns the total count of submitted proposals. @@ -268,24 +270,18 @@ library ExecutableProposals { function _checkProposalExists(uint256 proposalId, ProposalData memory proposalData) private pure { if (proposalData.status == Status.NotExist) { - revert ProposalNotFound(proposalId); + revert UnexpectedProposalStatus(proposalId, Status.NotExist); } } - function _isProposalSubmitted( + function _checkProposalNotCancelled( Context storage self, uint256 proposalId, ProposalData memory proposalData - ) private view returns (bool) { - return proposalId > self.lastCancelledProposalId && proposalData.status == Status.Submitted; - } - - function _isProposalScheduled( - Context storage self, - uint256 proposalId, - ProposalData memory proposalData - ) private view returns (bool) { - return proposalId > self.lastCancelledProposalId && proposalData.status == Status.Scheduled; + ) private view { + if (_isProposalCancelled(self, proposalId, proposalData)) { + revert UnexpectedProposalStatus(proposalId, Status.Cancelled); + } } function _isProposalCancelled( diff --git a/test/scenario/dg-update-tokens-rotation.t.sol b/test/scenario/dg-update-tokens-rotation.t.sol index 88d22b60..7a59727b 100644 --- a/test/scenario/dg-update-tokens-rotation.t.sol +++ b/test/scenario/dg-update-tokens-rotation.t.sol @@ -197,7 +197,9 @@ contract DualGovernanceUpdateTokensRotation is ScenarioTestBlueprint { _step("7. After the update malicious proposal is cancelled and can't be executed via new DualGovernance"); { vm.expectRevert( - abi.encodeWithSelector(ExecutableProposals.ProposalNotSubmitted.selector, maliciousProposalId) + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, maliciousProposalId, ProposalStatus.Cancelled + ) ); newDualGovernanceInstance.scheduleProposal(maliciousProposalId); diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index aa6c12fd..b387c9c6 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -14,8 +14,8 @@ import { } from "../utils/scenario-test-blueprint.sol"; import {DualGovernance} from "contracts/DualGovernance.sol"; -import {ExecutableProposals} from "contracts/libraries/ExecutableProposals.sol"; import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; +import {ExecutableProposals, Status as ProposalStatus} from "contracts/libraries/ExecutableProposals.sol"; contract PlanBSetup is ScenarioTestBlueprint { function setUp() external { @@ -380,12 +380,18 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertProposalCancelled(anotherMaliciousProposalId); vm.expectRevert( - abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, maliciousProposalId) + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, maliciousProposalId, ProposalStatus.Cancelled + ) ); _executeProposal(maliciousProposalId); vm.expectRevert( - abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, anotherMaliciousProposalId) + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, + anotherMaliciousProposalId, + ProposalStatus.Cancelled + ) ); _executeProposal(anotherMaliciousProposalId); } diff --git a/test/unit/libraries/ExecutableProposals.t.sol b/test/unit/libraries/ExecutableProposals.t.sol index d95c328a..6da61154 100644 --- a/test/unit/libraries/ExecutableProposals.t.sol +++ b/test/unit/libraries/ExecutableProposals.t.sol @@ -101,7 +101,11 @@ contract ExecutableProposalsUnitTests is UnitTest { function testFuzz_cannot_schedule_unsubmitted_proposal(uint256 proposalId) external { vm.assume(proposalId > 0); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotSubmitted.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.NotExist + ) + ); _proposals.schedule(proposalId, Durations.ZERO); } @@ -110,7 +114,11 @@ contract ExecutableProposalsUnitTests is UnitTest { uint256 proposalId = 1; _proposals.schedule(proposalId, Durations.ZERO); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotSubmitted.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.Scheduled + ) + ); _proposals.schedule(proposalId, Durations.ZERO); } @@ -133,7 +141,11 @@ contract ExecutableProposalsUnitTests is UnitTest { uint256 proposalId = _proposals.getProposalsCount(); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotSubmitted.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.Cancelled + ) + ); _proposals.schedule(proposalId, Durations.ZERO); } @@ -171,7 +183,11 @@ contract ExecutableProposalsUnitTests is UnitTest { function testFuzz_cannot_execute_unsubmitted_proposal(uint256 proposalId) external { vm.assume(proposalId > 0); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.NotExist + ) + ); _proposals.execute(proposalId, Durations.ZERO); } @@ -179,7 +195,11 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 proposalId = _proposals.getProposalsCount(); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.Submitted + ) + ); _proposals.execute(proposalId, Durations.ZERO); } @@ -189,7 +209,11 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.schedule(proposalId, Durations.ZERO); _proposals.execute(proposalId, Durations.ZERO); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.Executed + ) + ); _proposals.execute(proposalId, Durations.ZERO); } @@ -199,7 +223,11 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.schedule(proposalId, Durations.ZERO); _proposals.cancelAll(); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.Cancelled + ) + ); _proposals.execute(proposalId, Durations.ZERO); } @@ -336,10 +364,18 @@ contract ExecutableProposalsUnitTests is UnitTest { } function testFuzz_get_not_existing_proposal(uint256 proposalId) external { - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotFound.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.NotExist + ) + ); _proposals.getProposalDetails(proposalId); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotFound.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.NotExist + ) + ); _proposals.getProposalCalls(proposalId); } From 602235ff3374cb4521248de7abcfa39b4395dcf3 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 6 Dec 2024 19:59:20 +0300 Subject: [PATCH 103/107] detailed error --- contracts/DualGovernance.sol | 8 ++++++-- test/unit/DualGovernance.t.sol | 8 +++++++- test/unit/Escrow.t.sol | 6 ++++-- test/unit/libraries/EscrowState.t.sol | 4 ++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index b8c4ed31..e8c6e0b7 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -44,7 +44,9 @@ contract DualGovernance is IDualGovernance { error ProposalSubmissionBlocked(); error ProposalSchedulingBlocked(uint256 proposalId); error ResealIsNotAllowedInNormalState(); - error InvalidTiebreakerActivationTimeoutBounds(); + error InvalidTiebreakerActivationTimeoutBounds( + Duration minTiebreakerActivationTimeout, Duration maxTiebreakerActivationTimeout + ); // --- // Events @@ -148,7 +150,9 @@ contract DualGovernance is IDualGovernance { SanityCheckParams memory sanityCheckParams ) { if (sanityCheckParams.minTiebreakerActivationTimeout > sanityCheckParams.maxTiebreakerActivationTimeout) { - revert InvalidTiebreakerActivationTimeoutBounds(); + revert InvalidTiebreakerActivationTimeoutBounds( + sanityCheckParams.minTiebreakerActivationTimeout, sanityCheckParams.maxTiebreakerActivationTimeout + ); } TIMELOCK = components.timelock; diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 3b9c1088..20263fdc 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -150,7 +150,13 @@ contract DualGovernanceUnitTests is UnitTest { _sanityCheckParams.minTiebreakerActivationTimeout = Durations.from(1000); _sanityCheckParams.maxTiebreakerActivationTimeout = Durations.from(999); - vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidTiebreakerActivationTimeoutBounds.selector)); + vm.expectRevert( + abi.encodeWithSelector( + DualGovernance.InvalidTiebreakerActivationTimeoutBounds.selector, + _sanityCheckParams.minTiebreakerActivationTimeout, + _sanityCheckParams.maxTiebreakerActivationTimeout + ) + ); new DualGovernance({ components: _dgComponents, diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index 32eee4ff..05f38efa 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -94,7 +94,8 @@ contract EscrowUnitTests is UnitTest { address wsteth, address withdrawalQueue, address dualGovernance, - uint256 size + uint256 size, + Duration maxMinAssetsLockDuration ) external { Escrow instance = new Escrow( IStETH(steth), @@ -102,7 +103,7 @@ contract EscrowUnitTests is UnitTest { IWithdrawalQueue(withdrawalQueue), IDualGovernance(dualGovernance), size, - _maxMinAssetsLockDuration + maxMinAssetsLockDuration ); assertEq(address(instance.ST_ETH()), address(steth)); @@ -110,6 +111,7 @@ contract EscrowUnitTests is UnitTest { assertEq(address(instance.WITHDRAWAL_QUEUE()), address(withdrawalQueue)); assertEq(address(instance.DUAL_GOVERNANCE()), address(dualGovernance)); assertEq(instance.MIN_WITHDRAWALS_BATCH_SIZE(), size); + assertEq(instance.MAX_MIN_ASSETS_LOCK_DURATION(), maxMinAssetsLockDuration); } // --- diff --git a/test/unit/libraries/EscrowState.t.sol b/test/unit/libraries/EscrowState.t.sol index 9beb5920..b4040116 100644 --- a/test/unit/libraries/EscrowState.t.sol +++ b/test/unit/libraries/EscrowState.t.sol @@ -130,10 +130,10 @@ contract EscrowStateUnitTests is UnitTest { vm.expectRevert( abi.encodeWithSelector(EscrowState.InvalidMinAssetsLockDuration.selector, minAssetsLockDuration) ); - EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration, Durations.from(type(uint16).max)); + EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration, Durations.from(MAX_DURATION_VALUE)); } - function testFuzz_setMinAssetsLockDuration_RevertWhen_DurationGreaterThenmaxMinAssetsLockDuration( + function testFuzz_setMinAssetsLockDuration_RevertWhen_DurationGreaterThenMaxMinAssetsLockDuration( Duration minAssetsLockDuration, Duration maxMinAssetsLockDuration ) external { From e0e266a4e2bc817019d0b95a3a9fc3486f0ed5f2 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Fri, 6 Dec 2024 21:09:20 +0400 Subject: [PATCH 104/107] Fix tests --- contracts/interfaces/ISignallingEscrow.sol | 1 - .../libraries/WithdrawalsBatchesQueue.sol | 2 +- test/unit/Escrow.t.sol | 101 +++++++++++++----- .../libraries/WithdrawalBatchesQueue.t.sol | 8 -- 4 files changed, 74 insertions(+), 38 deletions(-) diff --git a/contracts/interfaces/ISignallingEscrow.sol b/contracts/interfaces/ISignallingEscrow.sol index de91a3a6..fcdfcc43 100644 --- a/contracts/interfaces/ISignallingEscrow.sol +++ b/contracts/interfaces/ISignallingEscrow.sol @@ -9,7 +9,6 @@ import {SharesValue} from "../types/SharesValue.sol"; import {UnstETHRecordStatus} from "../libraries/AssetsAccounting.sol"; import {IEscrowBase} from "./IEscrowBase.sol"; -import {IRageQuitEscrow} from "./IRageQuitEscrow.sol"; interface ISignallingEscrow is IEscrowBase { struct VetoerDetails { diff --git a/contracts/libraries/WithdrawalsBatchesQueue.sol b/contracts/libraries/WithdrawalsBatchesQueue.sol index 405fa029..a84339d1 100644 --- a/contracts/libraries/WithdrawalsBatchesQueue.sol +++ b/contracts/libraries/WithdrawalsBatchesQueue.sol @@ -216,7 +216,7 @@ library WithdrawalsBatchesQueue { return self.info.totalUnstETHIdsCount - self.info.totalUnstETHIdsClaimed; } - /// @notice Returns the ID of the boundary unstETH. + /// @notice Returns the id of the boundary unstETH. /// @dev Reverts with an index OOB error if called when the `WithdrawalsBatchesQueue` is in the /// `NotInitialized` state. /// @param self The context of the Withdrawals Batches Queue library. diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index a421af46..e4179f97 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {stdError} from "forge-std/StdError.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -198,7 +197,9 @@ contract EscrowUnitTests is UnitTest { function test_lockStETH_RevertOn_UnexpectedEscrowState() external { _transitToRageQuit(); - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow) + ); vm.prank(_vetoer); _escrow.lockStETH(1 ether); @@ -252,7 +253,9 @@ contract EscrowUnitTests is UnitTest { _transitToRageQuit(); - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow) + ); vm.prank(_vetoer); _escrow.unlockStETH(); } @@ -317,7 +320,9 @@ contract EscrowUnitTests is UnitTest { function test_lockWstETH_RevertOn_UnexpectedEscrowState() external { _transitToRageQuit(); - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow) + ); vm.prank(_vetoer); _escrow.lockWstETH(1 ether); } @@ -376,7 +381,9 @@ contract EscrowUnitTests is UnitTest { _transitToRageQuit(); - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow) + ); vm.prank(_vetoer); _escrow.unlockWstETH(); } @@ -459,7 +466,9 @@ contract EscrowUnitTests is UnitTest { _transitToRageQuit(); - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow) + ); _escrow.lockUnstETH(unstethIds); } @@ -532,7 +541,9 @@ contract EscrowUnitTests is UnitTest { _transitToRageQuit(); uint256[] memory unstethIds = new uint256[](1); - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow) + ); vm.prank(_vetoer); _escrow.unlockUnstETH(unstethIds); } @@ -569,7 +580,9 @@ contract EscrowUnitTests is UnitTest { uint256[] memory unstethIds = new uint256[](0); uint256[] memory hints = new uint256[](0); - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow) + ); _escrow.markUnstETHFinalized(unstethIds, hints); } @@ -630,7 +643,9 @@ contract EscrowUnitTests is UnitTest { } function test_requestNextWithdrawalsBatch_RevertOn_UnexpectedEscrowState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); _escrow.requestNextWithdrawalsBatch(1); } @@ -712,7 +727,9 @@ contract EscrowUnitTests is UnitTest { } function test_claimNextWithdrawalsBatch_2_RevertOn_UnexpectedEscrowState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, 2)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); _escrow.claimNextWithdrawalsBatch(1, new uint256[](1)); } @@ -746,7 +763,7 @@ contract EscrowUnitTests is UnitTest { _withdrawalQueue.setClaimableAmount(stethAmount); vm.deal(address(_withdrawalQueue), stethAmount); - vm.expectRevert(abi.encodeWithSelector(Escrow.InvalidFromUnstETHId.selector, unstEthIds)); + vm.expectRevert(abi.encodeWithSelector(Escrow.InvalidFromUnstETHId.selector, unstEthIds[0] + 10)); _escrow.claimNextWithdrawalsBatch(unstEthIds[0] + 10, new uint256[](1)); } @@ -811,7 +828,9 @@ contract EscrowUnitTests is UnitTest { } function test_claimNextWithdrawalsBatch_1_RevertOn_UnexpectedEscrowState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, 2)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); _escrow.claimNextWithdrawalsBatch(1); } @@ -926,12 +945,13 @@ contract EscrowUnitTests is UnitTest { } function test_claimUnstETH_RevertOn_UnexpectedEscrowState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); _escrow.claimUnstETH(new uint256[](1), new uint256[](1)); } function test_claimUnstETH_RevertOn_InvalidRequestId() external { - bytes memory wqInvalidRequestIdError = abi.encode("WithdrawalQueue.InvalidRequestId"); uint256[] memory unstETHIds = new uint256[](1); unstETHIds[0] = _withdrawalQueue.REVERT_ON_ID(); uint256[] memory hints = new uint256[](1); @@ -943,7 +963,6 @@ contract EscrowUnitTests is UnitTest { } function test_claimUnstETH_RevertOn_ArraysLengthMismatch() external { - bytes memory wqArraysLengthMismatchError = abi.encode("WithdrawalQueue.ArraysLengthMismatch"); uint256[] memory unstETHIds = new uint256[](2); uint256[] memory hints = new uint256[](1); uint256[] memory responses = new uint256[](1); @@ -1045,7 +1064,9 @@ contract EscrowUnitTests is UnitTest { } function test_withdrawETH_RevertOn_UnexpectedEscrowState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); _escrow.withdrawETH(); } @@ -1167,7 +1188,9 @@ contract EscrowUnitTests is UnitTest { } function test_withdrawETH_2_RevertOn_UnexpectedEscrowState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); _escrow.withdrawETH(new uint256[](1)); } @@ -1383,19 +1406,25 @@ contract EscrowUnitTests is UnitTest { } function test_getNextWithdrawalBatch_RevertOn_RageQuit_IsNotStarted() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); _escrow.getNextWithdrawalBatch(100); } function test_getNextWithdrawalBatch_RevertOn_UnexpectedEscrowState_Signaling() external { uint256 batchLimit = 10; - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); _escrow.getNextWithdrawalBatch(batchLimit); } function test_getNextWithdrawalBatch_RevertOn_UnexpectedEscrowState_NotInitialized() external { uint256 batchLimit = 10; - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized) + ); _masterCopy.getNextWithdrawalBatch(batchLimit); } @@ -1415,12 +1444,16 @@ contract EscrowUnitTests is UnitTest { } function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedEscrowState_Signaling() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); _escrow.isWithdrawalsBatchesClosed(); } function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedEscrowState_NotInitialized() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized) + ); _masterCopy.isWithdrawalsBatchesClosed(); } @@ -1429,12 +1462,16 @@ contract EscrowUnitTests is UnitTest { // --- function test_getUnclaimedUnstETHIdsCount_RevertOn_UnexpectedEscrowState_Signaling() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); _escrow.getUnclaimedUnstETHIdsCount(); } function test_getUnclaimedUnstETHIdsCount_RevertOn_UnexpectedEscrowState_NotInitialized() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized) + ); _masterCopy.getUnclaimedUnstETHIdsCount(); } @@ -1461,12 +1498,16 @@ contract EscrowUnitTests is UnitTest { // --- function test_getRageQuitExtensionPeriodStartedAt_RevertOn_NotInitializedState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized) + ); _masterCopy.getRageQuitEscrowDetails(); } function test_getRageQuitExtensionPeriodStartedAt_RevertOn_SignallingState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); _escrow.getRageQuitEscrowDetails().rageQuitExtensionPeriodStartedAt; } @@ -1537,12 +1578,16 @@ contract EscrowUnitTests is UnitTest { // --- function test_getRageQuitEscrowDetails_RevertOn_UnexpectedEscrowState_Signaling() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); _escrow.getRageQuitEscrowDetails(); } function test_getRageQuitEscrowDetails_RevertOn_UnexpectedEscrowState_NotInitialized() external { - vm.expectRevert(abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized) + ); _masterCopy.getRageQuitEscrowDetails(); } diff --git a/test/unit/libraries/WithdrawalBatchesQueue.t.sol b/test/unit/libraries/WithdrawalBatchesQueue.t.sol index d22288aa..b524a80c 100644 --- a/test/unit/libraries/WithdrawalBatchesQueue.t.sol +++ b/test/unit/libraries/WithdrawalBatchesQueue.t.sol @@ -605,14 +605,6 @@ contract WithdrawalsBatchesQueueTest is UnitTest { _batchesQueue.getBoundaryUnstETHId(); } - function test_getBoundaryUnstETHId_RevertOn_LastClaimedBatchIndexOutOfArrayBounds() external { - _openBatchesQueue(); - _batchesQueue.info.lastClaimedBatchIndex = 2; - - vm.expectRevert(stdError.indexOOBError); - _batchesQueue.getBoundaryUnstETHId(); - } - // --- // isAllBatchesClaimed() // --- From ade229094831ef93fbe6e3440e9098d38a717340 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 6 Dec 2024 20:26:10 +0300 Subject: [PATCH 105/107] getters order --- contracts/libraries/ExecutableProposals.sol | 30 ++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/contracts/libraries/ExecutableProposals.sol b/contracts/libraries/ExecutableProposals.sol index a1e4ab39..295b8e6e 100644 --- a/contracts/libraries/ExecutableProposals.sol +++ b/contracts/libraries/ExecutableProposals.sol @@ -194,21 +194,6 @@ library ExecutableProposals { // Getters // --- - /// @notice Determines whether a proposal is eligible for execution based on its status and delay requirements. - /// @param self The context of the Executable Proposal library. - /// @param proposalId The id of the proposal to check for execution eligibility. - /// @param afterScheduleDelay The required delay duration after scheduling before the proposal can be executed. - /// @return bool `true` if the proposal is eligible for execution, otherwise `false`. - function canExecute( - Context storage self, - uint256 proposalId, - Duration afterScheduleDelay - ) internal view returns (bool) { - ProposalData memory proposalData = self.proposals[proposalId].data; - return proposalId > self.lastCancelledProposalId && proposalData.status == Status.Scheduled - && Timestamps.now() >= afterScheduleDelay.addTo(proposalData.scheduledAt); - } - /// @notice Determines whether a proposal is eligible to be scheduled based on its status and required delay. /// @param self The context of the Executable Proposal library. /// @param proposalId The id of the proposal to check for scheduling eligibility. @@ -224,6 +209,21 @@ library ExecutableProposals { && Timestamps.now() >= afterSubmitDelay.addTo(proposalData.submittedAt); } + /// @notice Determines whether a proposal is eligible for execution based on its status and delay requirements. + /// @param self The context of the Executable Proposal library. + /// @param proposalId The id of the proposal to check for execution eligibility. + /// @param afterScheduleDelay The required delay duration after scheduling before the proposal can be executed. + /// @return bool `true` if the proposal is eligible for execution, otherwise `false`. + function canExecute( + Context storage self, + uint256 proposalId, + Duration afterScheduleDelay + ) internal view returns (bool) { + ProposalData memory proposalData = self.proposals[proposalId].data; + return proposalId > self.lastCancelledProposalId && proposalData.status == Status.Scheduled + && Timestamps.now() >= afterScheduleDelay.addTo(proposalData.scheduledAt); + } + /// @notice Returns the total count of submitted proposals. /// @param self The context of the Executable Proposal library. /// @return uint256 The number of submitted proposal From dafd88780bc00b62bf4e4a59c651add7ef1caecc Mon Sep 17 00:00:00 2001 From: Aleksandr Tarelkin Date: Fri, 6 Dec 2024 20:45:57 +0300 Subject: [PATCH 106/107] hashConsensus gas optimization --- contracts/committees/HashConsensus.sol | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index 2643c4f6..387616d7 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -250,7 +250,9 @@ abstract contract HashConsensus is Ownable { /// @param newMembers The array of addresses to be added as new members. /// @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) { + uint256 membersCount = newMembers.length; + + for (uint256 i = 0; i < membersCount; ++i) { if (newMembers[i] == address(0)) { revert InvalidMemberAccount(newMembers[i]); } @@ -270,7 +272,9 @@ abstract contract HashConsensus is Ownable { /// @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) { + uint256 membersCount = membersToRemove.length; + + for (uint256 i = 0; i < membersCount; ++i) { if (!_members.remove(membersToRemove[i])) { revert AccountIsNotMember(membersToRemove[i]); } @@ -285,7 +289,9 @@ abstract contract HashConsensus is Ownable { /// @param hash The hash to check /// @return support The number of votes in support of the hash function _getSupport(bytes32 hash) internal view returns (uint256 support) { - for (uint256 i = 0; i < _members.length(); ++i) { + uint256 membersCount = _members.length(); + + for (uint256 i = 0; i < membersCount; ++i) { if (approves[_members.at(i)][hash]) { support++; } From 20baa96bdf743344b0ab29f8dc62dd034b94b597 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Mon, 9 Dec 2024 12:30:50 +0400 Subject: [PATCH 107/107] Update spec with the code changes. Add scheduleProposal method --- docs/specification.md | 140 ++++++++++++++++++++++++------------------ 1 file changed, 80 insertions(+), 60 deletions(-) diff --git a/docs/specification.md b/docs/specification.md index 9d6ecb76..cac5f864 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -266,56 +266,85 @@ The id of the successfully registered proposal. Triggers a transition of the current governance state (if one is possible) before checking the preconditions. -### Function: DualGovernance.tiebreakerScheduleProposal +### Function: DualGovernance.scheduleProposal -[`DualGovernance.tiebreakerScheduleProposal`]: #Function-DualGovernancetiebreakerScheduleProposal +[`DualGovernance.scheduleProposal`]: #Function-DualGovernancescheduleProposal ```solidity -function tiebreakerScheduleProposal(uint256 proposalId) +function scheduleProposal(uint256 proposalId) external ``` -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). +Schedules a previously submitted proposal for execution in the Dual Governance system. The function ensures that the proposal meets specific conditions before it can be scheduled. If the conditions are met, the proposal is registered for execution in the `EmergencyProtectedTimelock` singleton instance. -#### Preconditions +Preconditions -* 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 cancelled. +- The proposal with the specified proposalId MUST exist in the system. +- The required delay since submission (`EmergencyProtectedTimelock.getAfterSubmitDelay()`) MUST have elapsed. +- The Dual Governance system MUST BE in the `Normal` or `VetoCooldown` state +- If the system is in the `VetoCooldown` state, the proposal MUST have been submitted not later than the `VetoSignalling` state was entered. +- The proposal MUST NOT have been cancelled. +- The proposal MUST NOT already be scheduled. Triggers a transition of the current governance state (if one is possible) before checking the preconditions. -### Function: DualGovernance.tiebreakerResumeSealable - -[`DualGovernance.tiebreakerResumeSealable`]: #Function-DualGovernancetiebreakerResumeSealable +### Function: DualGovernance.cancelAllPendingProposals ```solidity -function tiebreakerResumeSealable(address sealable) +function cancelAllPendingProposals() returns (bool) ``` -Calls the `ResealManager.resumeSealable(address sealable)` if all preconditions met. +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`. #### Preconditions -* 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]). +- MUST be called by an authorized `proposalsCanceller` +Triggers a transition of the current governance state, if one is possible. -### Function: DualGovernance.cancelAllPendingProposals +### Function: DualGovernance.activateNextState ```solidity -function cancelAllPendingProposals() returns (bool) +function activateNextState() ``` -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. +Triggers a transition of the [global governance state](#Governance-state), if one is possible; does nothing otherwise. -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. +### Function: DualGovernance.getPersistedState -#### Preconditions +```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`. -- MUST be called by an [admin proposer](#Administrative-actions). ### Function: DualGovernance.registerProposer @@ -347,61 +376,55 @@ Removes the registered `proposer` address from the list of valid proposers and d * The `proposer` address MUST NOT be the only one assigned to the admin executor. -### Function: DualGovernance.setTiebreakerCommittee +### Function: DualGovernance.tiebreakerScheduleProposal + +[`DualGovernance.tiebreakerScheduleProposal`]: #Function-DualGovernancetiebreakerScheduleProposal ```solidity -function setTiebreakerCommittee(address newTiebreaker) +function tiebreakerScheduleProposal(uint256 proposalId) ``` -Updates the address of the [Tiebreaker committee](#Tiebreaker-committee). +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 admin executor contract. -* The `newTiebreaker` address MUST NOT be the zero address. -* The `newTiebreaker` address MUST be different from the current tiebreaker 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 cancelled. +Triggers a transition of the current governance state (if one is possible) before checking the preconditions. -### Function: DualGovernance.activateNextState +### Function: DualGovernance.tiebreakerResumeSealable + +[`DualGovernance.tiebreakerResumeSealable`]: #Function-DualGovernancetiebreakerResumeSealable ```solidity -function activateNextState() +function tiebreakerResumeSealable(address sealable) ``` -Triggers a transition of the [global governance state](#Governance-state), if one is possible; does nothing otherwise. +Calls the `ResealManager.resumeSealable(address sealable)` if all preconditions met. -### Function: DualGovernance.getPersistedState +#### Preconditions -```solidity -function getPersistedState() view returns (State persistedState) -``` +* 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]). -Returns the most recently persisted state of the DualGovernance. -### Function: DualGovernance.getEffectiveState +### Function: DualGovernance.setTiebreakerCommittee ```solidity -function getEffectiveState() view returns (State persistedState) +function setTiebreakerCommittee(address newTiebreaker) ``` -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 +Updates the address of the [Tiebreaker committee](#Tiebreaker-committee). -```solidity -function getStateDetails() view returns (StateDetails) -``` +#### Preconditions -This function returns detailed information about the current state of the `DualGovernance`, comprising the following data: +* 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. -- **`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 @@ -409,20 +432,17 @@ Issues calls resulting from governance proposals' execution. Every protocol perm The system supports multiple instances of this contract, but all instances SHOULD be owned by the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance. + ### Function: execute ```solidity -function execute(address target, uint256 value, bytes payload) - payable returns (bytes result) +function execute(address target, uint256 value, bytes payload) payable ``` Issues a EVM call to the `target` address with the `payload` calldata, optionally sending `value` wei ETH. Reverts if the call was unsuccessful. -#### Returns - -The result of the call. #### Preconditions @@ -921,7 +941,7 @@ The governance reset entails the following steps: ### Function: EmergencyProtectedTimelock.submit ```solidity -function submit(address proposer, address executor, ExecutorCall[] calls, string calldata metadata) +function submit(address executor, ExecutorCall[] calls) returns (uint256 proposalId) ```