From 80a07691ad71b1f6b7b5639e18cf523106a7587c Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 7 Aug 2024 05:14:42 +0400 Subject: [PATCH 1/2] Use PercentD16 type for percents. Separate interfaces. Some libraries fixes. --- contracts/DualGovernanceConfigProvider.sol | 5 ++- contracts/Escrow.sol | 11 ++--- contracts/TimelockedGovernance.sol | 3 +- contracts/interfaces/IDualGovernance.sol | 2 +- contracts/interfaces/IEscrow.sol | 3 +- contracts/interfaces/IGovernance.sol | 12 ++++++ contracts/interfaces/IStETH.sol | 18 -------- contracts/interfaces/ITimelock.sol | 10 +---- contracts/interfaces/IWithdrawalQueue.sol | 18 -------- contracts/interfaces/IWstETH.sol | 2 +- contracts/libraries/DualGovernanceConfig.sol | 22 +++++----- .../libraries/DualGovernanceStateMachine.sol | 5 ++- contracts/libraries/EmergencyProtection.sol | 19 ++++---- contracts/types/PercentD16.sol | 43 +++++++++++++++++++ 14 files changed, 95 insertions(+), 78 deletions(-) create mode 100644 contracts/interfaces/IGovernance.sol create mode 100644 contracts/types/PercentD16.sol diff --git a/contracts/DualGovernanceConfigProvider.sol b/contracts/DualGovernanceConfigProvider.sol index cf40fa20..9b5900a2 100644 --- a/contracts/DualGovernanceConfigProvider.sol +++ b/contracts/DualGovernanceConfigProvider.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.26; import {Duration} from "./types/Duration.sol"; +import {PercentD16} from "./types/PercentD16.sol"; import {DualGovernanceConfig} from "./libraries/DualGovernanceConfig.sol"; interface IDualGovernanceConfigProvider { @@ -9,8 +10,8 @@ interface IDualGovernanceConfigProvider { } contract ImmutableDualGovernanceConfigProvider is IDualGovernanceConfigProvider { - uint256 public immutable FIRST_SEAL_RAGE_QUIT_SUPPORT; - uint256 public immutable SECOND_SEAL_RAGE_QUIT_SUPPORT; + PercentD16 public immutable FIRST_SEAL_RAGE_QUIT_SUPPORT; + PercentD16 public immutable SECOND_SEAL_RAGE_QUIT_SUPPORT; Duration public immutable MIN_ASSETS_LOCK_DURATION; Duration public immutable DYNAMIC_TIMELOCK_MIN_DURATION; diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 335519dd..8e5bece7 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -5,6 +5,7 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Duration} from "./types/Duration.sol"; import {Timestamp} from "./types/Timestamp.sol"; +import {PercentD16, PercentsD16} from "./types/PercentD16.sol"; import {IEscrow} from "./interfaces/IEscrow.sol"; @@ -376,17 +377,17 @@ contract Escrow is IEscrow { return _escrowState.rageQuitExtensionDelayStartedAt; } - function getRageQuitSupport() external view returns (uint256 rageQuitSupport) { + function getRageQuitSupport() external view returns (PercentD16) { StETHAccounting memory stETHTotals = _accounting.stETHTotals; UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; uint256 finalizedETH = unstETHTotals.finalizedETH.toUint256(); uint256 ufinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); - rageQuitSupport = ( - 10 ** 18 * (ST_ETH.getPooledEthByShares(ufinalizedShares) + finalizedETH) - / (ST_ETH.totalSupply() + finalizedETH) - ); + return PercentsD16.fromFraction({ + numerator: ST_ETH.getPooledEthByShares(ufinalizedShares) + finalizedETH, + denominator: ST_ETH.totalSupply() + finalizedETH + }); } function isRageQuitFinalized() external view returns (bool) { diff --git a/contracts/TimelockedGovernance.sol b/contracts/TimelockedGovernance.sol index 944f5ab2..3d597967 100644 --- a/contracts/TimelockedGovernance.sol +++ b/contracts/TimelockedGovernance.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {IGovernance, ITimelock} from "./interfaces/ITimelock.sol"; +import {ITimelock} from "./interfaces/ITimelock.sol"; +import {IGovernance} from "./interfaces/IGovernance.sol"; import {ExternalCall} from "./libraries/ExternalCalls.sol"; diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 383cb611..80bf5b27 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {IGovernance} from "./ITimelock.sol"; +import {IGovernance} from "./IGovernance.sol"; interface IDualGovernance is IGovernance { function activateNextState() external; diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol index 24d4efa1..dc0f8878 100644 --- a/contracts/interfaces/IEscrow.sol +++ b/contracts/interfaces/IEscrow.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.26; import {Duration} from "../types/Duration.sol"; +import {PercentD16} from "../types/PercentD16.sol"; interface IEscrow { function initialize(Duration minAssetsLockDuration) external; @@ -9,6 +10,6 @@ interface IEscrow { function startRageQuit(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock) external; function isRageQuitFinalized() external view returns (bool); - function getRageQuitSupport() external view returns (uint256 rageQuitSupport); + function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport); function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; } diff --git a/contracts/interfaces/IGovernance.sol b/contracts/interfaces/IGovernance.sol new file mode 100644 index 00000000..b29c19f8 --- /dev/null +++ b/contracts/interfaces/IGovernance.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ExternalCall} from "../libraries/ExternalCalls.sol"; + +interface IGovernance { + function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId); + function scheduleProposal(uint256 proposalId) external; + function cancelAllPendingProposals() external; + + function canScheduleProposal(uint256 proposalId) external view returns (bool); +} diff --git a/contracts/interfaces/IStETH.sol b/contracts/interfaces/IStETH.sol index 9feb6a33..207793a5 100644 --- a/contracts/interfaces/IStETH.sol +++ b/contracts/interfaces/IStETH.sol @@ -4,23 +4,6 @@ pragma solidity 0.8.26; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IStETH is IERC20 { - function STAKING_CONTROL_ROLE() external view returns (bytes32); - function removeStakingLimit() external; - function getStakeLimitFullInfo() - external - view - returns ( - bool isStakingPaused, - bool isStakingLimitSet, - uint256 currentStakeLimit, - uint256 maxStakeLimit, - uint256 maxStakeLimitGrowthBlocks, - uint256 prevStakeLimit, - uint256 prevStakeBlockNumber - ); - - function getTotalShares() external view returns (uint256); - function sharesOf(address account) external view returns (uint256); function getSharesByPooledEth(uint256 ethAmount) external view returns (uint256); function getPooledEthByShares(uint256 sharesAmount) external view returns (uint256); @@ -31,5 +14,4 @@ interface IStETH is IERC20 { address _recipient, uint256 _sharesAmount ) external returns (uint256); - function submit(address referral) external payable returns (uint256); } diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index 1df53c99..6f452a6a 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -3,16 +3,8 @@ pragma solidity 0.8.26; import {Timestamp} from "../types/Timestamp.sol"; -import {Status as ProposalStatus} from "../libraries/ExecutableProposals.sol"; import {ExternalCall} from "../libraries/ExternalCalls.sol"; - -interface IGovernance { - function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId); - function scheduleProposal(uint256 proposalId) external; - function cancelAllPendingProposals() external; - - function canScheduleProposal(uint256 proposalId) external view returns (bool); -} +import {Status as ProposalStatus} from "../libraries/ExecutableProposals.sol"; interface ITimelock { struct Proposal { diff --git a/contracts/interfaces/IWithdrawalQueue.sol b/contracts/interfaces/IWithdrawalQueue.sol index 9009206d..1a9b88c7 100644 --- a/contracts/interfaces/IWithdrawalQueue.sol +++ b/contracts/interfaces/IWithdrawalQueue.sol @@ -18,8 +18,6 @@ interface IWithdrawalQueue is IERC721 { function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external; - function getLastFinalizedRequestId() external view returns (uint256); - function transferFrom(address from, address to, uint256 requestId) external; function getWithdrawalStatus(uint256[] calldata _requestIds) @@ -27,8 +25,6 @@ interface IWithdrawalQueue is IERC721 { view returns (WithdrawalRequestStatus[] memory statuses); - function getLastRequestId() external view returns (uint256); - /// @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())` @@ -46,22 +42,8 @@ interface IWithdrawalQueue is IERC721 { ) external view returns (uint256[] memory hintIds); function getLastCheckpointIndex() external view returns (uint256); - function balanceOf(address owner) external view returns (uint256); - function requestWithdrawals( uint256[] calldata _amounts, address _owner ) external returns (uint256[] memory requestIds); - - function setApprovalForAll(address _operator, bool _approved) external; - - function requestWithdrawalsWstETH( - uint256[] calldata _amounts, - address _owner - ) external returns (uint256[] memory requestIds); - - function grantRole(bytes32 role, address account) external; - function pauseFor(uint256 duration) external; - function isPaused() external returns (bool); - function finalize(uint256 _lastRequestIdToBeFinalized, uint256 _maxShareRate) external payable; } diff --git a/contracts/interfaces/IWstETH.sol b/contracts/interfaces/IWstETH.sol index 6b6d6e17..557553ad 100644 --- a/contracts/interfaces/IWstETH.sol +++ b/contracts/interfaces/IWstETH.sol @@ -5,6 +5,6 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IWstETH is IERC20 { function wrap(uint256 stETHAmount) external returns (uint256); - function unwrap(uint256 wstETHAmount) external returns (uint256); + function getStETHByWstETH(uint256 wstethAmount) external view returns (uint256); } diff --git a/contracts/libraries/DualGovernanceConfig.sol b/contracts/libraries/DualGovernanceConfig.sol index d8638dd2..09f93cb3 100644 --- a/contracts/libraries/DualGovernanceConfig.sol +++ b/contracts/libraries/DualGovernanceConfig.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {PercentD16} from "../types/PercentD16.sol"; import {Duration, Durations} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; library DualGovernanceConfig { struct Context { - uint256 firstSealRageQuitSupport; - uint256 secondSealRageQuitSupport; + PercentD16 firstSealRageQuitSupport; + PercentD16 secondSealRageQuitSupport; Duration minAssetsLockDuration; Duration dynamicTimelockMaxDuration; Duration dynamicTimelockMinDuration; @@ -22,14 +23,14 @@ library DualGovernanceConfig { function isFirstSealRageQuitSupportCrossed( Context memory self, - uint256 rageQuitSupport + PercentD16 rageQuitSupport ) internal pure returns (bool) { return rageQuitSupport > self.firstSealRageQuitSupport; } function isSecondSealRageQuitSupportCrossed( Context memory self, - uint256 rageQuitSupport + PercentD16 rageQuitSupport ) internal pure returns (bool) { return rageQuitSupport > self.secondSealRageQuitSupport; } @@ -44,7 +45,7 @@ library DualGovernanceConfig { function isDynamicTimelockDurationPassed( Context memory self, Timestamp vetoSignallingActivatedAt, - uint256 rageQuitSupport + PercentD16 rageQuitSupport ) internal view returns (bool) { Duration dynamicTimelock = calcDynamicDelayDuration(self, rageQuitSupport); return Timestamps.now() > dynamicTimelock.addTo(vetoSignallingActivatedAt); @@ -73,10 +74,11 @@ library DualGovernanceConfig { function calcDynamicDelayDuration( Context memory self, - uint256 rageQuitSupport + PercentD16 rageQuitSupport ) internal pure returns (Duration duration_) { - uint256 firstSealRageQuitSupport = self.firstSealRageQuitSupport; - uint256 secondSealRageQuitSupport = self.secondSealRageQuitSupport; + PercentD16 firstSealRageQuitSupport = self.firstSealRageQuitSupport; + PercentD16 secondSealRageQuitSupport = self.secondSealRageQuitSupport; + Duration dynamicTimelockMinDuration = self.dynamicTimelockMinDuration; Duration dynamicTimelockMaxDuration = self.dynamicTimelockMaxDuration; @@ -90,9 +92,9 @@ library DualGovernanceConfig { duration_ = dynamicTimelockMinDuration + Durations.from( - (rageQuitSupport - firstSealRageQuitSupport) + PercentD16.unwrap(rageQuitSupport - firstSealRageQuitSupport) * (dynamicTimelockMaxDuration - dynamicTimelockMinDuration).toSeconds() - / (secondSealRageQuitSupport - firstSealRageQuitSupport) + / PercentD16.unwrap(secondSealRageQuitSupport - firstSealRageQuitSupport) ); } diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 9b801457..59a9c810 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -7,6 +7,7 @@ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {IEscrow} from "../interfaces/IEscrow.sol"; import {Duration} from "../types/Duration.sol"; +import {PercentD16} from "../types/PercentD16.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; @@ -200,7 +201,7 @@ library DualGovernanceStateTransitions { DualGovernanceStateMachine.Context storage self, DualGovernanceConfig.Context memory config ) private view returns (State) { - uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { return State.VetoSignalling; @@ -219,7 +220,7 @@ library DualGovernanceStateTransitions { DualGovernanceStateMachine.Context storage self, DualGovernanceConfig.Context memory config ) private view returns (State) { - uint256 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); + PercentD16 rageQuitSupport = self.signallingEscrow.getRageQuitSupport(); if (!config.isDynamicTimelockDurationPassed(self.vetoSignallingActivatedAt, rageQuitSupport)) { return State.VetoSignalling; diff --git a/contracts/libraries/EmergencyProtection.sol b/contracts/libraries/EmergencyProtection.sol index 0bc7d2b6..5f7db838 100644 --- a/contracts/libraries/EmergencyProtection.sol +++ b/contracts/libraries/EmergencyProtection.sol @@ -52,7 +52,6 @@ library EmergencyProtection { } self.emergencyModeEndsAfter = self.emergencyModeDuration.addTo(now_); - self.emergencyProtectionEndsAfter = now_; emit EmergencyModeActivated(); } @@ -73,7 +72,7 @@ library EmergencyProtection { // --- function setEmergencyGovernance(Context storage self, address newEmergencyGovernance) internal { - if (self.emergencyGovernance == newEmergencyGovernance) { + if (newEmergencyGovernance == self.emergencyGovernance) { return; } self.emergencyGovernance = newEmergencyGovernance; @@ -82,18 +81,18 @@ library EmergencyProtection { function setEmergencyProtectionEndDate( Context storage self, - Timestamp emergencyProtectionEndDate, + Timestamp newEmergencyProtectionEndDate, Duration maxEmergencyProtectionDuration ) internal { - if (emergencyProtectionEndDate > maxEmergencyProtectionDuration.addTo(Timestamps.now())) { - revert InvalidEmergencyProtectionEndDate(emergencyProtectionEndDate); + if (newEmergencyProtectionEndDate > maxEmergencyProtectionDuration.addTo(Timestamps.now())) { + revert InvalidEmergencyProtectionEndDate(newEmergencyProtectionEndDate); } - if (self.emergencyProtectionEndsAfter == emergencyProtectionEndDate) { + if (newEmergencyProtectionEndDate == self.emergencyProtectionEndsAfter) { return; } - self.emergencyProtectionEndsAfter = emergencyProtectionEndDate; - emit EmergencyProtectionEndDateSet(emergencyProtectionEndDate); + self.emergencyProtectionEndsAfter = newEmergencyProtectionEndDate; + emit EmergencyProtectionEndDateSet(newEmergencyProtectionEndDate); } function setEmergencyModeDuration( @@ -113,7 +112,7 @@ library EmergencyProtection { } function setEmergencyActivationCommittee(Context storage self, address newActivationCommittee) internal { - if (self.emergencyActivationCommittee == newActivationCommittee) { + if (newActivationCommittee == self.emergencyActivationCommittee) { return; } self.emergencyActivationCommittee = newActivationCommittee; @@ -121,7 +120,7 @@ library EmergencyProtection { } function setEmergencyExecutionCommittee(Context storage self, address newExecutionCommittee) internal { - if (self.emergencyActivationCommittee == newExecutionCommittee) { + if (newExecutionCommittee == self.emergencyExecutionCommittee) { return; } self.emergencyExecutionCommittee = newExecutionCommittee; diff --git a/contracts/types/PercentD16.sol b/contracts/types/PercentD16.sol new file mode 100644 index 00000000..30e354e5 --- /dev/null +++ b/contracts/types/PercentD16.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +type PercentD16 is uint256; + +uint256 constant HUNDRED_PERCENTS_UINT256 = 100 * 10 ** 18; + +error Overflow(); + +using {lt as <, gte as >=, gt as >, minus as -, plus as +} for PercentD16 global; + +function lt(PercentD16 a, PercentD16 b) pure returns (bool) { + return PercentD16.unwrap(a) < PercentD16.unwrap(b); +} + +function gt(PercentD16 a, PercentD16 b) pure returns (bool) { + return PercentD16.unwrap(a) > PercentD16.unwrap(b); +} + +function gte(PercentD16 a, PercentD16 b) pure returns (bool) { + return PercentD16.unwrap(a) >= PercentD16.unwrap(b); +} + +function minus(PercentD16 a, PercentD16 b) pure returns (PercentD16) { + if (b > a) { + revert Overflow(); + } + return PercentD16.wrap(PercentD16.unwrap(a) - PercentD16.unwrap(b)); +} + +function plus(PercentD16 a, PercentD16 b) pure returns (PercentD16) { + return PercentD16.wrap(PercentD16.unwrap(a) + PercentD16.unwrap(b)); +} + +library PercentsD16 { + function fromBasisPoints(uint256 bpValue) internal pure returns (PercentD16) { + return PercentD16.wrap(HUNDRED_PERCENTS_UINT256 * bpValue / 100_00); + } + + function fromFraction(uint256 numerator, uint256 denominator) internal pure returns (PercentD16) { + return PercentD16.wrap(HUNDRED_PERCENTS_UINT256 * numerator / denominator); + } +} From 5c6f205a66604e66a2a687cef0c4ca1db3a0aa05 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Wed, 7 Aug 2024 05:47:42 +0400 Subject: [PATCH 2/2] Fix broken tests. Refactor blueprint & utils test contracts --- contracts/types/PercentD16.sol | 2 +- test/scenario/agent-timelock.t.sol | 36 +- test/scenario/escrow.t.sol | 241 ++- test/scenario/gov-state-transitions.t.sol | 21 +- test/scenario/happy-path-plan-b.t.sol | 274 ++-- test/scenario/happy-path.t.sol | 55 +- .../last-moment-malicious-proposal.t.sol | 87 +- test/scenario/proposal-deployment-modes.t.sol | 44 +- test/scenario/tiebraker.t.sol | 123 +- test/scenario/timelocked-governance.t.sol | 58 +- test/scenario/veto-cooldown-mechanics.t.sol | 60 +- test/unit/EmergencyProtectedTimelock.t.sol | 1288 +++++++++-------- test/unit/TimelockedGovernance.t.sol | 169 +-- test/unit/libraries/EmergencyProtection.t.sol | 649 ++++----- test/unit/libraries/ExecutableProposals.t.sol | 51 +- test/unit/mocks/TimelockMock.sol | 8 +- test/utils/SetupDeployment.sol | 408 ++++++ test/utils/deployment.sol | 240 --- test/utils/evm-script-utils.sol | 27 + test/utils/interfaces/IAragonACL.sol | 8 + test/utils/interfaces/IAragonAgent.sol | 8 + test/utils/interfaces/IAragonForwarder.sol | 6 + .../IAragonVoting.sol} | 21 +- .../IPotentiallyDangerousContract.sol | 8 + test/utils/interfaces/IStETH.sol | 27 + test/utils/interfaces/IWithdrawalQueue.sol | 13 + test/utils/interfaces/IWstETH.sol | 9 + test/utils/lido-utils.sol | 259 ++++ test/utils/mainnet-addresses.sol | 1 + test/utils/percents.sol | 51 - test/utils/random.sol | 46 + test/utils/scenario-test-blueprint.sol | 477 ++---- test/utils/target-mock.sol | 34 + test/utils/testing-assert-eq-extender.sol | 52 + test/utils/unit-test.sol | 40 +- test/utils/utils.sol | 213 --- 36 files changed, 2633 insertions(+), 2481 deletions(-) create mode 100644 test/utils/SetupDeployment.sol delete mode 100644 test/utils/deployment.sol create mode 100644 test/utils/evm-script-utils.sol create mode 100644 test/utils/interfaces/IAragonACL.sol create mode 100644 test/utils/interfaces/IAragonAgent.sol create mode 100644 test/utils/interfaces/IAragonForwarder.sol rename test/utils/{interfaces.sol => interfaces/IAragonVoting.sol} (52%) create mode 100644 test/utils/interfaces/IPotentiallyDangerousContract.sol create mode 100644 test/utils/interfaces/IStETH.sol create mode 100644 test/utils/interfaces/IWithdrawalQueue.sol create mode 100644 test/utils/interfaces/IWstETH.sol create mode 100644 test/utils/lido-utils.sol delete mode 100644 test/utils/percents.sol create mode 100644 test/utils/random.sol create mode 100644 test/utils/target-mock.sol create mode 100644 test/utils/testing-assert-eq-extender.sol delete mode 100644 test/utils/utils.sol diff --git a/contracts/types/PercentD16.sol b/contracts/types/PercentD16.sol index 30e354e5..e1d276a4 100644 --- a/contracts/types/PercentD16.sol +++ b/contracts/types/PercentD16.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.26; type PercentD16 is uint256; -uint256 constant HUNDRED_PERCENTS_UINT256 = 100 * 10 ** 18; +uint256 constant HUNDRED_PERCENTS_UINT256 = 100 * 10 ** 16; error Overflow(); diff --git a/test/scenario/agent-timelock.t.sol b/test/scenario/agent-timelock.t.sol index 2ac2b056..5cdebbee 100644 --- a/test/scenario/agent-timelock.t.sol +++ b/test/scenario/agent-timelock.t.sol @@ -5,30 +5,28 @@ import {ExternalCall, ScenarioTestBlueprint} from "../utils/scenario-test-bluepr contract AgentTimelockTest is ScenarioTestBlueprint { function setUp() external { - _selectFork(); - _deployTarget(); - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ true); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); } function testFork_AgentTimelockHappyPath() external { - ExternalCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); uint256 proposalId; _step("1. THE PROPOSAL IS SUBMITTED"); { - proposalId = _submitProposal( - _dualGovernance, "Propose to doSmth on target passing dual governance", regularStaffCalls + proposalId = _submitProposalViaDualGovernance( + "Propose to doSmth on target passing dual governance", regularStaffCalls ); - _assertSubmittedProposalData(proposalId, _timelock.getAdminExecutor(), regularStaffCalls); - _assertCanSchedule(_dualGovernance, proposalId, false); + _assertSubmittedProposalData(proposalId, _getAdminExecutor(), regularStaffCalls); + _assertCanScheduleViaDualGovernance(proposalId, false); } _step("2. THE PROPOSAL IS SCHEDULED"); { _waitAfterSubmitDelayPassed(); - _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _assertCanExecute(proposalId, false); @@ -48,27 +46,27 @@ contract AgentTimelockTest is ScenarioTestBlueprint { _assertProposalExecuted(proposalId); _assertCanExecute(proposalId, false); - _assertCanSchedule(_dualGovernance, proposalId, false); + _assertCanScheduleViaDualGovernance(proposalId, false); - _assertTargetMockCalls(_timelock.getAdminExecutor(), regularStaffCalls); + _assertTargetMockCalls(_getAdminExecutor(), regularStaffCalls); } } function testFork_TimelockEmergencyReset() external { - ExternalCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); // --- // 1. THE PROPOSAL IS SUBMITTED // --- uint256 proposalId; { - proposalId = _submitProposal( - _dualGovernance, "Propose to doSmth on target passing dual governance", regularStaffCalls + proposalId = _submitProposalViaDualGovernance( + "Propose to doSmth on target passing dual governance", regularStaffCalls ); - _assertSubmittedProposalData(proposalId, _timelock.getAdminExecutor(), regularStaffCalls); + _assertSubmittedProposalData(proposalId, _getAdminExecutor(), regularStaffCalls); // proposal can't be scheduled until the AFTER_SUBMIT_DELAY has passed - _assertCanSchedule(_dualGovernance, proposalId, false); + _assertCanScheduleViaDualGovernance(proposalId, false); } // --- @@ -80,9 +78,9 @@ contract AgentTimelockTest is ScenarioTestBlueprint { // when the first delay is passed and the is no opposition from the stETH holders // the proposal can be scheduled - _assertCanSchedule(_dualGovernance, proposalId, true); + _assertCanScheduleViaDualGovernance(proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); // proposal can't be executed until the second delay has ended _assertProposalScheduled(proposalId); diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index ed7e92e3..73709c81 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -1,97 +1,59 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {Duration as DurationType} from "contracts/types/Duration.sol"; -import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; -import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; - -import { - Escrow, - Balances, - WITHDRAWAL_QUEUE, - ScenarioTestBlueprint, - VetoerState, - LockedAssetsTotals, - Durations -} from "../utils/scenario-test-blueprint.sol"; - -contract TestHelpers is ScenarioTestBlueprint { - function rebase(int256 deltaBP) public { - bytes32 CL_BALANCE_POSITION = 0xa66d35f054e68143c18f32c990ed5cb972bb68a68f500cd2dd3a16bbf3686483; // keccak256("lido.Lido.beaconBalance"); +import {Duration, Durations} from "contracts/types/Duration.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; - uint256 totalSupply = _ST_ETH.totalSupply(); - uint256 clBalance = uint256(vm.load(address(_ST_ETH), CL_BALANCE_POSITION)); - - int256 delta = (deltaBP * int256(totalSupply) / 10000); - vm.store(address(_ST_ETH), CL_BALANCE_POSITION, bytes32(uint256(int256(clBalance) + delta))); - - assertEq( - uint256(int256(totalSupply) * deltaBP / 10000 + int256(totalSupply)), _ST_ETH.totalSupply(), "total supply" - ); - } +import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; - function finalizeWQ() public { - uint256 lastRequestId = _WITHDRAWAL_QUEUE.getLastRequestId(); - finalizeWQ(lastRequestId); - } +import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; - function finalizeWQ(uint256 id) public { - uint256 finalizationShareRate = _ST_ETH.getPooledEthByShares(1e27) + 1e9; // TODO check finalization rate - address lido = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; - vm.prank(lido); - _WITHDRAWAL_QUEUE.finalize(id, finalizationShareRate); +import {Escrow, VetoerState, LockedAssetsTotals} from "contracts/Escrow.sol"; - bytes32 LOCKED_ETHER_AMOUNT_POSITION = 0x0e27eaa2e71c8572ab988fef0b54cd45bbd1740de1e22343fb6cda7536edc12f; // keccak256("lido.WithdrawalQueue.lockedEtherAmount"); +import {ScenarioTestBlueprint, LidoUtils, console} from "../utils/scenario-test-blueprint.sol"; - vm.store(WITHDRAWAL_QUEUE, LOCKED_ETHER_AMOUNT_POSITION, bytes32(address(WITHDRAWAL_QUEUE).balance)); - } -} +contract EscrowHappyPath is ScenarioTestBlueprint { + using LidoUtils for LidoUtils.Context; -contract EscrowHappyPath is TestHelpers { Escrow internal escrow; - DurationType internal immutable _RAGE_QUIT_EXTRA_TIMELOCK = Durations.from(14 days); - DurationType internal immutable _RAGE_QUIT_WITHDRAWALS_TIMELOCK = Durations.from(7 days); + Duration internal immutable _RAGE_QUIT_EXTRA_TIMELOCK = Durations.from(14 days); + Duration internal immutable _RAGE_QUIT_WITHDRAWALS_TIMELOCK = Durations.from(7 days); address internal immutable _VETOER_1 = makeAddr("VETOER_1"); address internal immutable _VETOER_2 = makeAddr("VETOER_2"); - Balances internal _firstVetoerBalances; - Balances internal _secondVetoerBalances; - function setUp() external { - _selectFork(); - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); escrow = _getVetoSignallingEscrow(); - _setupStETHWhale(_VETOER_1); + _setupStETHBalance(_VETOER_1, PercentsD16.fromBasisPoints(10_00)); + vm.startPrank(_VETOER_1); - _ST_ETH.approve(address(_WST_ETH), type(uint256).max); - _ST_ETH.approve(address(escrow), type(uint256).max); - _ST_ETH.approve(address(_WITHDRAWAL_QUEUE), type(uint256).max); - _WST_ETH.approve(address(escrow), type(uint256).max); + _lido.stETH.approve(address(_lido.wstETH), type(uint256).max); + _lido.stETH.approve(address(escrow), type(uint256).max); + _lido.stETH.approve(address(_lido.withdrawalQueue), type(uint256).max); + _lido.wstETH.approve(address(escrow), type(uint256).max); - _WST_ETH.wrap(100_000 * 10 ** 18); + _lido.wstETH.wrap(100_000 * 10 ** 18); vm.stopPrank(); - _setupStETHWhale(_VETOER_2); + _setupStETHBalance(_VETOER_2, PercentsD16.fromBasisPoints(10_00)); + vm.startPrank(_VETOER_2); - _ST_ETH.approve(address(_WST_ETH), type(uint256).max); - _ST_ETH.approve(address(escrow), type(uint256).max); - _ST_ETH.approve(address(_WITHDRAWAL_QUEUE), type(uint256).max); - _WST_ETH.approve(address(escrow), type(uint256).max); + _lido.stETH.approve(address(_lido.wstETH), type(uint256).max); + _lido.stETH.approve(address(escrow), type(uint256).max); + _lido.stETH.approve(address(_lido.withdrawalQueue), type(uint256).max); + _lido.wstETH.approve(address(escrow), type(uint256).max); - _WST_ETH.wrap(100_000 * 10 ** 18); + _lido.wstETH.wrap(100_000 * 10 ** 18); vm.stopPrank(); - - _firstVetoerBalances = _getBalances(_VETOER_1); - _secondVetoerBalances = _getBalances(_VETOER_2); } function test_lock_unlock() public { - uint256 firstVetoerStETHBalanceBefore = _ST_ETH.balanceOf(_VETOER_1); - uint256 secondVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_2); + uint256 firstVetoerStETHBalanceBefore = _lido.stETH.balanceOf(_VETOER_1); + uint256 secondVetoerWstETHBalanceBefore = _lido.wstETH.balanceOf(_VETOER_2); uint256 firstVetoerLockStETHAmount = 1 ether; uint256 firstVetoerLockWstETHAmount = 2 ether; @@ -109,30 +71,29 @@ contract EscrowHappyPath is TestHelpers { _unlockStETH(_VETOER_1); assertApproxEqAbs( - _ST_ETH.balanceOf(_VETOER_1), - firstVetoerStETHBalanceBefore + _ST_ETH.getPooledEthByShares(firstVetoerLockWstETHAmount), + _lido.stETH.balanceOf(_VETOER_1), + firstVetoerStETHBalanceBefore + _lido.stETH.getPooledEthByShares(firstVetoerLockWstETHAmount), 1 ); _unlockWstETH(_VETOER_2); assertApproxEqAbs( secondVetoerWstETHBalanceBefore, - _WST_ETH.balanceOf(_VETOER_2), - secondVetoerWstETHBalanceBefore + _ST_ETH.getSharesByPooledEth(secondVetoerLockWstETHAmount) + _lido.wstETH.balanceOf(_VETOER_2), + secondVetoerWstETHBalanceBefore + _lido.stETH.getSharesByPooledEth(secondVetoerLockWstETHAmount) ); } function test_lock_unlock_w_rebase() public { uint256 firstVetoerStETHAmount = 10 * 10 ** 18; - uint256 firstVetoerStETHShares = _ST_ETH.getSharesByPooledEth(firstVetoerStETHAmount); + uint256 firstVetoerStETHShares = _lido.stETH.getSharesByPooledEth(firstVetoerStETHAmount); uint256 firstVetoerWstETHAmount = 11 * 10 ** 18; uint256 secondVetoerStETHAmount = 13 * 10 ** 18; - uint256 secondVetoerStETHShares = _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount); uint256 secondVetoerWstETHAmount = 17 * 10 ** 18; - uint256 firstVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_1); - uint256 secondVetoerStETHSharesBefore = _ST_ETH.sharesOf(_VETOER_2); + uint256 firstVetoerWstETHBalanceBefore = _lido.wstETH.balanceOf(_VETOER_1); + uint256 secondVetoerStETHSharesBefore = _lido.stETH.sharesOf(_VETOER_2); _lockStETH(_VETOER_1, firstVetoerStETHAmount); _lockWstETH(_VETOER_1, firstVetoerWstETHAmount); @@ -140,23 +101,17 @@ contract EscrowHappyPath is TestHelpers { _lockStETH(_VETOER_2, secondVetoerStETHAmount); _lockWstETH(_VETOER_2, secondVetoerWstETHAmount); - rebase(100); - - uint256 firstVetoerStETHSharesAfterRebase = _ST_ETH.sharesOf(_VETOER_1); - uint256 firstVetoerWstETHBalanceAfterRebase = _WST_ETH.balanceOf(_VETOER_1); - - uint256 secondVetoerStETHSharesAfterRebase = _ST_ETH.sharesOf(_VETOER_2); - uint256 secondVetoerWstETHBalanceAfterRebase = _WST_ETH.balanceOf(_VETOER_2); + _simulateRebase(PercentsD16.fromBasisPoints(101_00)); // +1% _wait(_dualGovernanceConfigProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); _unlockWstETH(_VETOER_1); assertApproxEqAbs( firstVetoerWstETHBalanceBefore + firstVetoerStETHShares, - _WST_ETH.balanceOf(_VETOER_1), + _lido.wstETH.balanceOf(_VETOER_1), // Even though the wstETH itself doesn't have rounding issues, the Escrow contract wraps stETH into wstETH // so the the rounding issue may happen because of it. Another rounding may happen on the converting stETH amount - // into shares via _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount) + // into shares via _lido.stETH.getSharesByPooledEth(secondVetoerStETHAmount) 2 ); @@ -164,9 +119,10 @@ contract EscrowHappyPath is TestHelpers { assertApproxEqAbs( // all locked stETH and wstETH was withdrawn as stETH - _ST_ETH.getPooledEthByShares(secondVetoerStETHSharesBefore + secondVetoerWstETHAmount), - _ST_ETH.balanceOf(_VETOER_2), - 1 + _lido.stETH.getPooledEthByShares(secondVetoerStETHSharesBefore + secondVetoerWstETHAmount), + _lido.stETH.balanceOf(_VETOER_2), + // Considering that during the previous operation 2 wei may be lost, total rounding error may be 3 wei + 3 ); } @@ -176,10 +132,10 @@ contract EscrowHappyPath is TestHelpers { uint256 secondVetoerStETHAmount = 13 * 10 ** 18; uint256 secondVetoerWstETHAmount = 17 * 10 ** 18; - uint256 secondVetoerStETHShares = _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount); + uint256 secondVetoerStETHShares = _lido.stETH.getSharesByPooledEth(secondVetoerStETHAmount); - uint256 firstVetoerStETHSharesBefore = _ST_ETH.sharesOf(_VETOER_1); - uint256 secondVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_2); + uint256 firstVetoerStETHSharesBefore = _lido.stETH.sharesOf(_VETOER_1); + uint256 secondVetoerWstETHBalanceBefore = _lido.wstETH.balanceOf(_VETOER_2); _lockStETH(_VETOER_1, firstVetoerStETHAmount); _lockWstETH(_VETOER_1, firstVetoerWstETHAmount); @@ -187,15 +143,15 @@ contract EscrowHappyPath is TestHelpers { _lockStETH(_VETOER_2, secondVetoerStETHAmount); _lockWstETH(_VETOER_2, secondVetoerWstETHAmount); - rebase(-100); + _simulateRebase(PercentsD16.fromBasisPoints(99_00)); // -1% _wait(_dualGovernanceConfigProvider.MIN_ASSETS_LOCK_DURATION().plusSeconds(1)); _unlockStETH(_VETOER_1); assertApproxEqAbs( // all locked stETH and wstETH was withdrawn as stETH - _ST_ETH.getPooledEthByShares(firstVetoerStETHSharesBefore + firstVetoerWstETHAmount), - _ST_ETH.balanceOf(_VETOER_1), + _lido.stETH.getPooledEthByShares(firstVetoerStETHSharesBefore + firstVetoerWstETHAmount), + _lido.stETH.balanceOf(_VETOER_1), 1 ); @@ -203,10 +159,10 @@ contract EscrowHappyPath is TestHelpers { assertApproxEqAbs( secondVetoerWstETHBalanceBefore + secondVetoerStETHShares, - _WST_ETH.balanceOf(_VETOER_2), + _lido.wstETH.balanceOf(_VETOER_2), // Even though the wstETH itself doesn't have rounding issues, the Escrow contract wraps stETH into wstETH // so the the rounding issue may happen because of it. Another rounding may happen on the converting stETH amount - // into shares via _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount) + // into shares via _lido.stETH.getSharesByPooledEth(secondVetoerStETHAmount) 2 ); } @@ -218,7 +174,7 @@ contract EscrowHappyPath is TestHelpers { } vm.prank(_VETOER_1); - uint256[] memory unstETHIds = _WITHDRAWAL_QUEUE.requestWithdrawals(amounts, _VETOER_1); + uint256[] memory unstETHIds = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); _lockUnstETH(_VETOER_1, unstETHIds); @@ -234,9 +190,9 @@ contract EscrowHappyPath is TestHelpers { } vm.prank(_VETOER_1); - uint256[] memory unstETHIds = _WITHDRAWAL_QUEUE.requestWithdrawals(amounts, _VETOER_1); + uint256[] memory unstETHIds = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); - finalizeWQ(); + _finalizeWithdrawalQueue(); vm.expectRevert(); this.externalLockUnstETH(_VETOER_1, unstETHIds); @@ -250,10 +206,10 @@ contract EscrowHappyPath is TestHelpers { } vm.prank(_VETOER_1); - uint256[] memory unstETHIds = _WITHDRAWAL_QUEUE.requestWithdrawals(amounts, _VETOER_1); + uint256[] memory unstETHIds = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); uint256 totalSharesLocked; - WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); for (uint256 i = 0; i < unstETHIds.length; ++i) { totalSharesLocked += statuses[i].amountOfShares; } @@ -267,14 +223,14 @@ contract EscrowHappyPath is TestHelpers { assertEq(totals.unstETHFinalizedETH, 0); assertEq(totals.unstETHUnfinalizedShares, totalSharesLocked); - finalizeWQ(unstETHIds[0]); + _finalizeWithdrawalQueue(unstETHIds[0]); uint256[] memory hints = - _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); + _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); escrow.markUnstETHFinalized(unstETHIds, hints); totals = escrow.getLockedAssetsTotals(); assertEq(totals.unstETHUnfinalizedShares, statuses[0].amountOfShares); - uint256 ethAmountFinalized = _WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints)[0]; + uint256 ethAmountFinalized = _lido.withdrawalQueue.getClaimableEther(unstETHIds, hints)[0]; assertApproxEqAbs(totals.unstETHFinalizedETH, ethAmountFinalized, 1); } @@ -284,38 +240,40 @@ contract EscrowHappyPath is TestHelpers { amounts[i] = 1e18; } - uint256 totalSupply = _ST_ETH.totalSupply(); - vm.prank(_VETOER_1); - uint256[] memory unstETHIds = _WITHDRAWAL_QUEUE.requestWithdrawals(amounts, _VETOER_1); + uint256[] memory unstETHIds = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); uint256 amountToLock = 1e18; - uint256 sharesToLock = _ST_ETH.getSharesByPooledEth(amountToLock); + uint256 sharesToLock = _lido.stETH.getSharesByPooledEth(amountToLock); _lockStETH(_VETOER_1, amountToLock); _lockWstETH(_VETOER_1, sharesToLock); _lockUnstETH(_VETOER_1, unstETHIds); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).stETHLockedShares, 2 * sharesToLock, 1); + 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); - uint256 rageQuitSupport = escrow.getRageQuitSupport(); - assertEq(rageQuitSupport, 4 * 1e18 * 1e18 / totalSupply); + assertEq(escrow.getRageQuitSupport(), PercentsD16.fromFraction({numerator: 4 ether, denominator: totalSupply})); - finalizeWQ(unstETHIds[0]); + _finalizeWithdrawalQueue(unstETHIds[0]); uint256[] memory hints = - _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); + _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); escrow.markUnstETHFinalized(unstETHIds, hints); assertEq(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, sharesToLock); - uint256 ethAmountFinalized = _WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints)[0]; + + uint256 ethAmountFinalized = _lido.withdrawalQueue.getClaimableEther(unstETHIds, hints)[0]; assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHFinalizedETH, ethAmountFinalized, 1); - rageQuitSupport = escrow.getRageQuitSupport(); assertEq( - rageQuitSupport, - 10 ** 18 * (_ST_ETH.getPooledEthByShares(3 * sharesToLock) + ethAmountFinalized) - / (_ST_ETH.totalSupply() + ethAmountFinalized) + escrow.getRageQuitSupport(), + PercentsD16.fromFraction({ + numerator: _lido.stETH.getPooledEthByShares(3 * sharesToLock) + ethAmountFinalized, + denominator: _lido.stETH.totalSupply() + ethAmountFinalized + }) ); } @@ -327,15 +285,15 @@ contract EscrowHappyPath is TestHelpers { } vm.prank(_VETOER_1); - uint256[] memory unstETHIds = _WITHDRAWAL_QUEUE.requestWithdrawals(amounts, _VETOER_1); + uint256[] memory unstETHIds = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); - uint256 requestShares = _ST_ETH.getSharesByPooledEth(30 * requestAmount); + uint256 requestShares = _lido.stETH.getSharesByPooledEth(30 * requestAmount); _lockStETH(_VETOER_1, 20 * requestAmount); _lockWstETH(_VETOER_1, requestShares); _lockUnstETH(_VETOER_1, unstETHIds); - rebase(100); + _simulateRebase(PercentsD16.fromBasisPoints(101_00)); // +1% vm.expectRevert(); escrow.startRageQuit(_RAGE_QUIT_EXTRA_TIMELOCK, _RAGE_QUIT_WITHDRAWALS_TIMELOCK); @@ -343,39 +301,39 @@ contract EscrowHappyPath is TestHelpers { vm.prank(address(_dualGovernance)); escrow.startRageQuit(_RAGE_QUIT_EXTRA_TIMELOCK, _RAGE_QUIT_WITHDRAWALS_TIMELOCK); - uint256 escrowStETHBalance = _ST_ETH.balanceOf(address(escrow)); + uint256 escrowStETHBalance = _lido.stETH.balanceOf(address(escrow)); uint256 expectedWithdrawalBatchesCount = escrowStETHBalance / requestAmount + 1; - assertEq(_WITHDRAWAL_QUEUE.balanceOf(address(escrow)), 10); + assertEq(_lido.withdrawalQueue.balanceOf(address(escrow)), 10); escrow.requestNextWithdrawalsBatch(10); - assertEq(_WITHDRAWAL_QUEUE.balanceOf(address(escrow)), 20); + assertEq(_lido.withdrawalQueue.balanceOf(address(escrow)), 20); while (!escrow.isWithdrawalsBatchesFinalized()) { escrow.requestNextWithdrawalsBatch(96); } - assertEq(_WITHDRAWAL_QUEUE.balanceOf(address(escrow)), 10 + expectedWithdrawalBatchesCount); + assertEq(_lido.withdrawalQueue.balanceOf(address(escrow)), 10 + expectedWithdrawalBatchesCount); assertEq(escrow.isRageQuitFinalized(), false); - vm.deal(WITHDRAWAL_QUEUE, 1000 * requestAmount); - finalizeWQ(); + _finalizeWithdrawalQueue(); uint256[] memory unstETHIdsToClaim = escrow.getNextWithdrawalBatch(expectedWithdrawalBatchesCount); // assertEq(total, expectedWithdrawalBatchesCount); - WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIdsToClaim); + WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIdsToClaim); for (uint256 i = 0; i < statuses.length; ++i) { assertTrue(statuses[i].isFinalized); assertFalse(statuses[i].isClaimed); } - uint256[] memory hints = - _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIdsToClaim, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); + uint256[] memory hints = _lido.withdrawalQueue.findCheckpointHints( + unstETHIdsToClaim, 1, _lido.withdrawalQueue.getLastCheckpointIndex() + ); while (!escrow.isWithdrawalsClaimed()) { - escrow.claimNextWithdrawalsBatch(128); + escrow.claimNextWithdrawalsBatch(32); } assertEq(escrow.isRageQuitFinalized(), false); @@ -385,7 +343,7 @@ contract EscrowHappyPath is TestHelpers { // --- { uint256[] memory hints = - _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); + _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); escrow.claimUnstETH(unstETHIds, hints); // but it can't be withdrawn before withdrawal timelock has passed @@ -418,15 +376,14 @@ contract EscrowHappyPath is TestHelpers { } vm.prank(_VETOER_1); - uint256[] memory unstETHIds = _WITHDRAWAL_QUEUE.requestWithdrawals(amounts, _VETOER_1); + uint256[] memory unstETHIds = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); _lockUnstETH(_VETOER_1, unstETHIds); vm.prank(address(_dualGovernance)); escrow.startRageQuit(_RAGE_QUIT_EXTRA_TIMELOCK, _RAGE_QUIT_WITHDRAWALS_TIMELOCK); - vm.deal(WITHDRAWAL_QUEUE, 100 * requestAmount); - finalizeWQ(); + _finalizeWithdrawalQueue(); escrow.requestNextWithdrawalsBatch(96); @@ -435,7 +392,7 @@ contract EscrowHappyPath is TestHelpers { assertEq(escrow.isRageQuitFinalized(), false); uint256[] memory hints = - _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); + _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); escrow.claimUnstETH(unstETHIds, hints); @@ -455,7 +412,7 @@ contract EscrowHappyPath is TestHelpers { uint256 firstVetoerStETHAmount = 10 ether; uint256 firstVetoerWstETHAmount = 11 ether; - uint256 firstVetoerStETHShares = _ST_ETH.getSharesByPooledEth(firstVetoerStETHAmount); + uint256 firstVetoerStETHShares = _lido.stETH.getSharesByPooledEth(firstVetoerStETHAmount); uint256 totalSharesLocked = firstVetoerWstETHAmount + firstVetoerStETHShares; _lockStETH(_VETOER_1, firstVetoerStETHAmount); @@ -480,7 +437,7 @@ contract EscrowHappyPath is TestHelpers { assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, firstVetoerStETHShares, 2); uint256[] memory wstETHWithdrawalRequestAmounts = new uint256[](1); - wstETHWithdrawalRequestAmounts[0] = _ST_ETH.getPooledEthByShares(firstVetoerWstETHAmount); + wstETHWithdrawalRequestAmounts[0] = _lido.stETH.getPooledEthByShares(firstVetoerWstETHAmount); vm.prank(_VETOER_1); uint256[] memory wstETHWithdrawalRequestIds = escrow.requestWithdrawals(wstETHWithdrawalRequestAmounts); @@ -488,12 +445,12 @@ contract EscrowHappyPath is TestHelpers { assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, 0, 2); assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, totalSharesLocked, 2); - finalizeWQ(wstETHWithdrawalRequestIds[0]); + _finalizeWithdrawalQueue(wstETHWithdrawalRequestIds[0]); escrow.markUnstETHFinalized( stETHWithdrawalRequestIds, - _WITHDRAWAL_QUEUE.findCheckpointHints( - stETHWithdrawalRequestIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex() + _lido.withdrawalQueue.findCheckpointHints( + stETHWithdrawalRequestIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex() ) ); assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, 0, 2); @@ -502,8 +459,8 @@ contract EscrowHappyPath is TestHelpers { escrow.markUnstETHFinalized( wstETHWithdrawalRequestIds, - _WITHDRAWAL_QUEUE.findCheckpointHints( - wstETHWithdrawalRequestIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex() + _lido.withdrawalQueue.findCheckpointHints( + wstETHWithdrawalRequestIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex() ) ); assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, 0, 2); @@ -526,8 +483,8 @@ contract EscrowHappyPath is TestHelpers { nftAmounts[0] = 1 ether; vm.startPrank(_VETOER_1); - uint256[] memory lockedWithdrawalNfts = _WITHDRAWAL_QUEUE.requestWithdrawals(nftAmounts, _VETOER_1); - uint256[] memory notLockedWithdrawalNfts = _WITHDRAWAL_QUEUE.requestWithdrawals(nftAmounts, _VETOER_1); + uint256[] memory lockedWithdrawalNfts = _lido.withdrawalQueue.requestWithdrawals(nftAmounts, _VETOER_1); + uint256[] memory notLockedWithdrawalNfts = _lido.withdrawalQueue.requestWithdrawals(nftAmounts, _VETOER_1); vm.stopPrank(); _lockStETH(_VETOER_1, 1 ether); diff --git a/test/scenario/gov-state-transitions.t.sol b/test/scenario/gov-state-transitions.t.sol index 884aed7d..f41f9bd3 100644 --- a/test/scenario/gov-state-transitions.t.sol +++ b/test/scenario/gov-state-transitions.t.sol @@ -1,21 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {ScenarioTestBlueprint, percents, Durations} from "../utils/scenario-test-blueprint.sol"; +import {Durations} from "contracts/types/Duration.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; +import {ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; contract GovernanceStateTransitions is ScenarioTestBlueprint { address internal immutable _VETOER = makeAddr("VETOER"); function setUp() external { - _selectFork(); - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); - _depositStETH(_VETOER, 1 ether); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); + _setupStETHBalance( + _VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); } function test_signalling_state_min_duration() public { _assertNormalState(); - _lockStETH(_VETOER, percents(_dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT()); _assertNormalState(); _lockStETH(_VETOER, 1 gwei); @@ -35,7 +38,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_state_max_duration() public { _assertNormalState(); - _lockStETH(_VETOER, percents(_dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); _assertVetoSignalingState(); @@ -60,7 +63,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_to_normal() public { _assertNormalState(); - _lockStETH(_VETOER, percents(_dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT()); _assertNormalState(); @@ -90,7 +93,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_non_stop() public { _assertNormalState(); - _lockStETH(_VETOER, percents(_dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT()); _assertNormalState(); _lockStETH(_VETOER, 1 gwei); @@ -115,7 +118,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_to_rage_quit() public { _assertNormalState(); - _lockStETH(_VETOER, percents(_dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); _assertVetoSignalingState(); _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION()); diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index bc6ec84c..947b7057 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -1,29 +1,28 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {IPotentiallyDangerousContract} from "../utils/interfaces/IPotentiallyDangerousContract.sol"; + import { - IDangerousContract, ScenarioTestBlueprint, ExternalCall, ExternalCallHelpers, - DualGovernance, Timestamp, Timestamps, Durations } 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"; contract PlanBSetup is ScenarioTestBlueprint { function setUp() external { - _selectFork(); - _deployTarget(); - _deployTimelockedGovernanceSetup( /* isEmergencyProtectionEnabled */ true); + _deployTimelockedGovernanceSetup({isEmergencyProtectionEnabled: true}); } function testFork_PlanB_Scenario() external { - ExternalCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); // --- // ACT 1. 📈 DAO OPERATES AS USUALLY @@ -35,12 +34,12 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertProposalSubmitted(proposalId); _assertSubmittedProposalData(proposalId, regularStaffCalls); - _assertCanSchedule(_timelockedGovernance, proposalId, false); + _assertCanScheduleViaTimelockedGovernance(proposalId, false); _waitAfterSubmitDelayPassed(); - _assertCanSchedule(_timelockedGovernance, proposalId, true); - _scheduleProposal(_timelockedGovernance, proposalId); + _assertCanScheduleViaTimelockedGovernance(proposalId, true); + _scheduleProposalViaTimelockedGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); @@ -58,20 +57,21 @@ contract PlanBSetup is ScenarioTestBlueprint { EmergencyProtection.Context memory emergencyState; { // Malicious vote was proposed by the attacker with huge LDO wad (but still not the majority) - ExternalCall[] memory maliciousCalls = - ExternalCallHelpers.create(address(_target), abi.encodeCall(IDangerousContract.doRugPool, ())); + ExternalCall[] memory maliciousCalls = ExternalCallHelpers.create( + address(_targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRugPool, ()) + ); - maliciousProposalId = _submitProposal(_timelockedGovernance, "Rug Pool attempt", maliciousCalls); + maliciousProposalId = _submitProposalViaTimelockedGovernance("Rug Pool attempt", maliciousCalls); // the call isn't executable until the delay has passed _assertProposalSubmitted(maliciousProposalId); - _assertCanSchedule(_timelockedGovernance, maliciousProposalId, false); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, false); // some time required to assemble the emergency committee and activate emergency mode _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); // malicious call still can't be scheduled - _assertCanSchedule(_timelockedGovernance, maliciousProposalId, false); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, false); // emergency committee activates emergency mode vm.prank(address(_emergencyActivationCommittee)); @@ -88,15 +88,15 @@ contract PlanBSetup is ScenarioTestBlueprint { // only the emergency committee _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); - _assertCanSchedule(_timelockedGovernance, maliciousProposalId, true); - _scheduleProposal(_timelockedGovernance, maliciousProposalId); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, true); + _scheduleProposalViaTimelockedGovernance(maliciousProposalId); _waitAfterScheduleDelayPassed(); // but the call still not executable _assertCanExecute(maliciousProposalId, false); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, true)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, false)); _executeProposal(maliciousProposalId); } @@ -112,11 +112,18 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertCanExecute(maliciousProposalId, false); // Dual Governance is deployed into mainnet - _deployDualGovernance(); + _resealManager = _deployResealManager(_timelock); + _dualGovernanceConfigProvider = _deployDualGovernanceConfigProvider(); + _dualGovernance = _deployDualGovernance({ + timelock: _timelock, + resealManager: _resealManager, + configProvider: _dualGovernanceConfigProvider + }); ExternalCall[] memory dualGovernanceLaunchCalls = ExternalCallHelpers.create( - address(_timelock), + [address(_dualGovernance), address(_timelock), address(_timelock), address(_timelock)], [ + 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) @@ -125,11 +132,11 @@ contract PlanBSetup is ScenarioTestBlueprint { abi.encodeCall( _timelock.setupEmergencyProtection, ( - address(_ADMIN_PROPOSER), // DAO_VOTING TODO: declare variable in scenario test blueprint + address(_emergencyGovernance), address(_emergencyActivationCommittee), address(_emergencyExecutionCommittee), _EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()), - Durations.from(30 days) + _EMERGENCY_MODE_DURATION ) ) ] @@ -137,13 +144,13 @@ contract PlanBSetup is ScenarioTestBlueprint { // The vote to launch Dual Governance is launched and reached the quorum (the major part of LDO holder still have power) uint256 dualGovernanceLunchProposalId = - _submitProposal(_timelockedGovernance, "Launch the Dual Governance", dualGovernanceLaunchCalls); + _submitProposalViaTimelockedGovernance("Launch the Dual Governance", dualGovernanceLaunchCalls); // wait until the after submit delay has passed _waitAfterSubmitDelayPassed(); - _assertCanSchedule(_timelockedGovernance, dualGovernanceLunchProposalId, true); - _scheduleProposal(_timelockedGovernance, dualGovernanceLunchProposalId); + _assertCanScheduleViaTimelockedGovernance(dualGovernanceLunchProposalId, true); + _scheduleProposalViaTimelockedGovernance(dualGovernanceLunchProposalId); _assertProposalScheduled(dualGovernanceLunchProposalId); _waitAfterScheduleDelayPassed(); @@ -173,8 +180,8 @@ contract PlanBSetup is ScenarioTestBlueprint { _waitAfterSubmitDelayPassed(); - _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); _waitAfterScheduleDelayPassed(); @@ -184,105 +191,112 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertTargetMockCalls(_timelock.getAdminExecutor(), regularStaffCalls); } - // TODO: update the deployment logic below - // // --- - // // ACT 5. 🔜 NEW DUAL GOVERNANCE VERSION IS COMING - // // --- - // { - // // some time later, the major Dual Governance update release is ready to be launched - // _wait(Durations.from(365 days)); - // DualGovernance dualGovernanceV2 = - // new DualGovernance(address(_config), address(_timelock), address(_escrowMasterCopy), _ADMIN_PROPOSER); - - // ExternalCall[] memory dualGovernanceUpdateCalls = ExternalCallHelpers.create( - // address(_timelock), - // [ - // // 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 - // abi.encodeCall( - // _timelock.setEmergencyProtection, - // ( - // address(_emergencyActivationCommittee), - // address(_emergencyExecutionCommittee), - // _EMERGENCY_PROTECTION_DURATION, - // Durations.from(30 days) - // ) - // ) - // ] - // ); - - // uint256 updateDualGovernanceProposalId = - // _submitProposal(_dualGovernance, "Update Dual Governance to V2", dualGovernanceUpdateCalls); - - // _waitAfterSubmitDelayPassed(); - - // _assertCanSchedule(_dualGovernance, updateDualGovernanceProposalId, true); - // _scheduleProposal(_dualGovernance, updateDualGovernanceProposalId); - - // _waitAfterScheduleDelayPassed(); - - // // but the call still not executable - // _assertCanExecute(updateDualGovernanceProposalId, true); - // _executeProposal(updateDualGovernanceProposalId); - - // // new version of dual governance attached to timelock - // assertEq(_timelock.getGovernance(), address(dualGovernanceV2)); - - // // - emergency protection enabled - // assertTrue(_timelock.isEmergencyProtectionEnabled()); - - // emergencyState = _timelock.getEmergencyState(); - // assertEq(emergencyState.activationCommittee, address(_emergencyActivationCommittee)); - // assertEq(emergencyState.executionCommittee, address(_emergencyExecutionCommittee)); - // assertFalse(emergencyState.isEmergencyModeActivated); - // assertEq(emergencyState.emergencyModeDuration, Durations.from(30 days)); - // assertEq(emergencyState.emergencyModeEndsAfter, Timestamps.ZERO); - - // // use the new version of the dual governance in the future calls - // _dualGovernance = dualGovernanceV2; - // } - - // // --- - // // ACT 7. 📆 DAO CONTINUES THEIR REGULAR DUTIES (PROTECTED BY DUAL GOVERNANCE V2) - // // --- - // { - // uint256 proposalId = _submitProposal( - // _dualGovernance, "DAO does regular staff on potentially dangerous contract", regularStaffCalls - // ); - // _assertProposalSubmitted(proposalId); - // _assertSubmittedProposalData(proposalId, regularStaffCalls); - - // _waitAfterSubmitDelayPassed(); - - // _assertCanSchedule(_dualGovernance, proposalId, true); - // _scheduleProposal(_dualGovernance, proposalId); - // _assertProposalScheduled(proposalId); - - // // wait while the after schedule delay has passed - // _waitAfterScheduleDelayPassed(); - - // // execute the proposal - // _assertCanExecute(proposalId, true); - // _executeProposal(proposalId); - // _assertProposalExecuted(proposalId); - - // // call successfully executed - // _assertTargetMockCalls(_timelock.getAdminExecutor(), regularStaffCalls); - // } + + // --- + // ACT 5. 🔜 NEW DUAL GOVERNANCE VERSION IS COMING + // --- + { + // some time later, the major Dual Governance update release is ready to be launched + _wait(Durations.from(365 days)); + DualGovernance dualGovernanceV2 = _deployDualGovernance({ + timelock: _timelock, + resealManager: _resealManager, + configProvider: _dualGovernanceConfigProvider + }); + + ExternalCall[] memory dualGovernanceUpdateCalls = ExternalCallHelpers.create( + [address(dualGovernanceV2), address(_timelock), address(_timelock)], + [ + 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 + abi.encodeCall( + _timelock.setupEmergencyProtection, + ( + address(_emergencyGovernance), + address(_emergencyActivationCommittee), + address(_emergencyExecutionCommittee), + _EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()), + Durations.from(30 days) + ) + ) + ] + ); + + uint256 updateDualGovernanceProposalId = + _submitProposalViaDualGovernance("Update Dual Governance to V2", dualGovernanceUpdateCalls); + + _waitAfterSubmitDelayPassed(); + + _assertCanScheduleViaDualGovernance(updateDualGovernanceProposalId, true); + _scheduleProposalViaDualGovernance(updateDualGovernanceProposalId); + + _waitAfterScheduleDelayPassed(); + + // but the call still not executable + _assertCanExecute(updateDualGovernanceProposalId, true); + _executeProposal(updateDualGovernanceProposalId); + + // new version of dual governance attached to timelock + assertEq(_timelock.getGovernance(), address(dualGovernanceV2)); + + // - emergency protection enabled + assertTrue(_timelock.isEmergencyProtectionEnabled()); + + assertFalse(_timelock.isEmergencyModeActive()); + + EmergencyProtection.Context memory emergencyState = _timelock.getEmergencyProtectionContext(); + assertEq(emergencyState.emergencyActivationCommittee, address(_emergencyActivationCommittee)); + assertEq(emergencyState.emergencyExecutionCommittee, address(_emergencyExecutionCommittee)); + assertEq(emergencyState.emergencyModeDuration, Durations.from(30 days)); + assertEq(emergencyState.emergencyModeEndsAfter, Timestamps.ZERO); + + // use the new version of the dual governance in the future calls + _dualGovernance = dualGovernanceV2; + } + + // --- + // ACT 7. 📆 DAO CONTINUES THEIR REGULAR DUTIES (PROTECTED BY DUAL GOVERNANCE V2) + // --- + { + uint256 proposalId = _submitProposal( + _dualGovernance, "DAO does regular staff on potentially dangerous contract", regularStaffCalls + ); + _assertProposalSubmitted(proposalId); + _assertSubmittedProposalData(proposalId, regularStaffCalls); + + _waitAfterSubmitDelayPassed(); + + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); + _assertProposalScheduled(proposalId); + + // wait while the after schedule delay has passed + _waitAfterScheduleDelayPassed(); + + // execute the proposal + _assertCanExecute(proposalId, true); + _executeProposal(proposalId); + _assertProposalExecuted(proposalId); + + // call successfully executed + _assertTargetMockCalls(_timelock.getAdminExecutor(), regularStaffCalls); + } } function testFork_SubmittedCallsCantBeExecutedAfterEmergencyModeDeactivation() external { - ExternalCall[] memory maliciousCalls = - ExternalCallHelpers.create(address(_target), abi.encodeCall(IDangerousContract.doRugPool, ())); + ExternalCall[] memory maliciousCalls = ExternalCallHelpers.create( + address(_targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRugPool, ()) + ); // schedule some malicious call uint256 maliciousProposalId; { - maliciousProposalId = _submitProposal(_timelockedGovernance, "Rug Pool attempt", maliciousCalls); + maliciousProposalId = _submitProposalViaTimelockedGovernance("Rug Pool attempt", maliciousCalls); // malicious calls can't be executed until the delays have passed - _assertCanSchedule(_timelockedGovernance, maliciousProposalId, false); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, false); } // activate emergency mode @@ -301,14 +315,14 @@ contract PlanBSetup is ScenarioTestBlueprint { // the after submit delay has passed, and proposal can be scheduled, but not executed _wait(_timelock.getAfterScheduleDelay() + Durations.from(1 seconds)); _wait(_timelock.getAfterSubmitDelay().plusSeconds(1)); - _assertCanSchedule(_timelockedGovernance, maliciousProposalId, true); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, true); - _scheduleProposal(_timelockedGovernance, maliciousProposalId); + _scheduleProposalViaTimelockedGovernance(maliciousProposalId); _wait(_timelock.getAfterScheduleDelay().plusSeconds(1)); _assertCanExecute(maliciousProposalId, false); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, true)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, false)); _executeProposal(maliciousProposalId); } @@ -318,22 +332,22 @@ contract PlanBSetup is ScenarioTestBlueprint { _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); // emergency mode still active - assertTrue(emergencyState.emergencyModeEndsAfter > Timestamps.now()); + assertTrue(_timelock.getEmergencyProtectionContext().emergencyModeEndsAfter > Timestamps.now()); anotherMaliciousProposalId = - _submitProposal(_timelockedGovernance, "Another Rug Pool attempt", maliciousCalls); + _submitProposalViaTimelockedGovernance("Another Rug Pool attempt", maliciousCalls); // malicious calls can't be executed until the delays have passed _assertCanExecute(anotherMaliciousProposalId, false); // the after submit delay has passed, and proposal can not be executed _wait(_timelock.getAfterSubmitDelay().plusSeconds(1)); - _assertCanSchedule(_timelockedGovernance, anotherMaliciousProposalId, true); + _assertCanScheduleViaTimelockedGovernance(anotherMaliciousProposalId, true); _wait(_timelock.getAfterScheduleDelay().plusSeconds(1)); _assertCanExecute(anotherMaliciousProposalId, false); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, true)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, false)); _executeProposal(anotherMaliciousProposalId); } @@ -342,10 +356,10 @@ contract PlanBSetup is ScenarioTestBlueprint { _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); assertTrue(emergencyState.emergencyModeEndsAfter < Timestamps.now()); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, true)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, false)); _executeProposal(maliciousProposalId); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, true)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, false)); _executeProposal(anotherMaliciousProposalId); } @@ -377,23 +391,25 @@ contract PlanBSetup is ScenarioTestBlueprint { function testFork_EmergencyResetGovernance() external { // deploy dual governance full setup { - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ true); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); assertNotEq(_timelock.getGovernance(), _timelock.getEmergencyProtectionContext().emergencyGovernance); } // emergency committee activates emergency mode - EmergencyProtection.Context memory emergencyState; { vm.prank(address(_emergencyActivationCommittee)); _timelock.activateEmergencyMode(); - assertFalse(_timelock.isEmergencyModeActive()); + assertTrue(_timelock.isEmergencyModeActive()); } // before the end of the emergency mode emergency committee can reset the controller to // disable dual governance { _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); + + EmergencyProtection.Context memory emergencyState = _timelock.getEmergencyProtectionContext(); + assertTrue(emergencyState.emergencyModeEndsAfter > Timestamps.now()); _executeEmergencyReset(); @@ -412,7 +428,7 @@ contract PlanBSetup is ScenarioTestBlueprint { function testFork_ExpiredEmergencyCommitteeHasNoPower() external { // deploy dual governance full setup { - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ true); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); assertNotEq(_timelock.getGovernance(), _timelock.getEmergencyProtectionContext().emergencyGovernance); } diff --git a/test/scenario/happy-path.t.sol b/test/scenario/happy-path.t.sol index fd6deea2..0bf8ec82 100644 --- a/test/scenario/happy-path.t.sol +++ b/test/scenario/happy-path.t.sol @@ -1,28 +1,24 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import { - Utils, - ExternalCall, - IDangerousContract, - ExternalCallHelpers, - ScenarioTestBlueprint -} from "../utils/scenario-test-blueprint.sol"; +import {EvmScriptUtils} from "../utils/evm-script-utils.sol"; +import {IPotentiallyDangerousContract} from "../utils/interfaces/IPotentiallyDangerousContract.sol"; + +import {ExternalCall, ExternalCallHelpers, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; import {ExecutableProposals} from "contracts/libraries/ExecutableProposals.sol"; -import {IAragonAgent, IAragonForwarder} from "../utils/interfaces.sol"; -import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; +import {LidoUtils, EvmScriptUtils} from "../utils/lido-utils.sol"; contract HappyPathTest is ScenarioTestBlueprint { + using LidoUtils for LidoUtils.Context; + function setUp() external { - _selectFork(); - _deployTarget(); - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); } function testFork_HappyPath() external { - ExternalCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); uint256 proposalId = _submitProposal( _dualGovernance, "DAO does regular staff on potentially dangerous contract", regularStaffCalls @@ -34,13 +30,13 @@ contract HappyPathTest is ScenarioTestBlueprint { // the min execution delay hasn't elapsed yet vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.AfterSubmitDelayNotPassed.selector, (proposalId))); - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); // wait till the first phase of timelock passes _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); - _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); @@ -53,16 +49,17 @@ contract HappyPathTest is ScenarioTestBlueprint { function testFork_HappyPathWithMultipleItems() external { // additional phase required here, grant rights to call DAO Agent to the admin executor - Utils.grantPermission(DAO_AGENT, IAragonAgent(DAO_AGENT).RUN_SCRIPT_ROLE(), _timelock.getAdminExecutor()); + _lido.grantPermission(address(_lido.agent), _lido.agent.RUN_SCRIPT_ROLE(), _timelock.getAdminExecutor()); - bytes memory agentDoRegularStaffPayload = abi.encodeCall(IDangerousContract.doRegularStaff, (42)); - bytes memory targetCallEvmScript = Utils.encodeEvmCallScript(address(_target), agentDoRegularStaffPayload); + bytes memory agentDoRegularStaffPayload = abi.encodeCall(IPotentiallyDangerousContract.doRegularStaff, (42)); + bytes memory targetCallEvmScript = + EvmScriptUtils.encodeEvmCallScript(address(_targetMock), agentDoRegularStaffPayload); ExternalCall[] memory multipleCalls = ExternalCallHelpers.create( - [DAO_AGENT, address(_target)], + [address(_lido.agent), address(_targetMock)], [ - abi.encodeCall(IAragonForwarder.forward, (targetCallEvmScript)), - abi.encodeCall(IDangerousContract.doRegularStaff, (43)) + abi.encodeCall(_lido.agent.forward, (targetCallEvmScript)), + abi.encodeCall(IPotentiallyDangerousContract.doRegularStaff, (43)) ] ); @@ -71,17 +68,17 @@ contract HappyPathTest is ScenarioTestBlueprint { _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); // proposal can't be scheduled before the after submit delay has passed - _assertCanSchedule(_dualGovernance, proposalId, false); + _assertCanScheduleViaDualGovernance(proposalId, false); // the min execution delay hasn't elapsed yet vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.AfterSubmitDelayNotPassed.selector, (proposalId))); - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); // wait till the DG-enforced timelock elapses _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); - _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); @@ -90,12 +87,12 @@ contract HappyPathTest is ScenarioTestBlueprint { _executeProposal(proposalId); address[] memory senders = new address[](2); - senders[0] = DAO_AGENT; + senders[0] = address(_lido.agent); senders[1] = _timelock.getAdminExecutor(); ExternalCall[] memory expectedTargetCalls = ExternalCallHelpers.create( - [DAO_AGENT, address(_target)], - [agentDoRegularStaffPayload, abi.encodeCall(IDangerousContract.doRegularStaff, (43))] + [address(_lido.agent), address(_targetMock)], + [agentDoRegularStaffPayload, abi.encodeCall(IPotentiallyDangerousContract.doRegularStaff, (43))] ); _assertTargetMockCalls(senders, expectedTargetCalls); diff --git a/test/scenario/last-moment-malicious-proposal.t.sol b/test/scenario/last-moment-malicious-proposal.t.sol index 51e55b83..04caff99 100644 --- a/test/scenario/last-moment-malicious-proposal.t.sol +++ b/test/scenario/last-moment-malicious-proposal.t.sol @@ -1,30 +1,23 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {PercentsD16} from "contracts/types/PercentD16.sol"; + +import {IPotentiallyDangerousContract} from "../utils/interfaces/IPotentiallyDangerousContract.sol"; + +import {DualGovernance} from "contracts/DualGovernance.sol"; + import { - percents, - ScenarioTestBlueprint, - ExternalCall, - ExternalCallHelpers, - DualGovernance, - Durations + ScenarioTestBlueprint, ExternalCall, ExternalCallHelpers, Durations } from "../utils/scenario-test-blueprint.sol"; -interface IDangerousContract { - function doRegularStaff(uint256 magic) external; - function doRugPool() external; - function doControversialStaff() external; -} - contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { function setUp() external { - _selectFork(); - _deployTarget(); - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); } function testFork_LastMomentMaliciousProposal() external { - ExternalCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); uint256 proposalId; _step("1. DAO SUBMITS PROPOSAL WITH REGULAR STAFF"); @@ -38,9 +31,10 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { } address maliciousActor = makeAddr("MALICIOUS_ACTOR"); + _setupStETHBalance(maliciousActor, PercentsD16.fromBasisPoints(15_00)); _step("2. MALICIOUS ACTOR STARTS ACQUIRE VETO SIGNALLING DURATION"); { - _lockStETH(maliciousActor, percents("12.0")); + _lockStETH(maliciousActor, PercentsD16.fromBasisPoints(12_00)); _assertVetoSignalingState(); _logVetoSignallingState(); @@ -58,7 +52,9 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { maliciousProposalId = _submitProposal( _dualGovernance, "Malicious Proposal", - ExternalCallHelpers.create(address(_target), abi.encodeCall(IDangerousContract.doRugPool, ())) + ExternalCallHelpers.create( + address(_targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRugPool, ()) + ) ); // the both calls aren't executable until the delay has passed @@ -76,10 +72,17 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { } address stEthHolders = makeAddr("STETH_WHALE"); + _setupStETHBalance( + stEthHolders, + _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); _step("5. STETH HOLDERS ACQUIRING QUORUM TO VETO MALICIOUS PROPOSAL"); { _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().dividedBy(2)); - _lockStETH(stEthHolders, percents(_dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() + 1)); + _lockStETH( + stEthHolders, + _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1) + ); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); @@ -91,7 +94,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("6. MALICIOUS PROPOSAL CAN'T BE EXECUTED IN THE VETO COOLDOWN STATE"); { // the regular proposal can be executed - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); _waitAfterScheduleDelayPassed(); _executeProposal(proposalId); @@ -113,7 +116,8 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _logVetoSignallingState(); // the second seal rage quit support is reached - _lockStETH(stEthHolders, percents(_dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _setupStETHBalance(stEthHolders, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); + _lockStETH(stEthHolders, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); _assertVetoSignalingState(); _logVetoSignallingState(); @@ -130,7 +134,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { } function testFork_VetoSignallingDeactivationDefaultDuration() external { - ExternalCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); uint256 proposalId; // --- @@ -151,8 +155,11 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { address maliciousActor = makeAddr("MALICIOUS_ACTOR"); { _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); - - _lockStETH(maliciousActor, percents("12.0")); + _setupStETHBalance( + maliciousActor, + _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); + _lockStETH(maliciousActor, PercentsD16.fromBasisPoints(12_00)); _assertVetoSignalingState(); _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); @@ -175,8 +182,8 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _activateNextState(); _assertVetoCooldownState(); - _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); @@ -190,15 +197,22 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { function testFork_VetoSignallingToNormalState() external { address maliciousActor = makeAddr("MALICIOUS_ACTOR"); + _setupStETHBalance( + maliciousActor, + _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); _step("2. MALICIOUS ACTOR LOCKS FIRST SEAL THRESHOLD TO ACTIVATE VETO SIGNALLING BEFORE PROPOSAL SUBMISSION"); { - _lockStETH(maliciousActor, percents("3.50")); + _lockStETH( + maliciousActor, + _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1) + ); _assertVetoSignalingState(); _logVetoSignallingState(); } uint256 proposalId; - ExternalCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); _step("2. DAO SUBMITS PROPOSAL WITH REGULAR STAFF"); { proposalId = _submitProposal( @@ -234,7 +248,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("5. PROPOSAL EXECUTABLE IN THE NORMAL STATE"); { - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); _executeProposal(proposalId); @@ -246,13 +260,20 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { address maliciousActor = makeAddr("MALICIOUS_ACTOR"); _step("2. MALICIOUS ACTOR LOCKS FIRST SEAL THRESHOLD TO ACTIVATE VETO SIGNALLING BEFORE PROPOSAL SUBMISSION"); { - _lockStETH(maliciousActor, percents("3.50")); + _setupStETHBalance( + maliciousActor, + _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); + _lockStETH( + maliciousActor, + _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1) + ); _assertVetoSignalingState(); _logVetoSignallingState(); } uint256 proposalId; - ExternalCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); _step("2. DAO SUBMITS PROPOSAL WITH REGULAR STAFF"); { proposalId = _submitProposal( @@ -300,7 +321,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _activateNextState(); _assertVetoCooldownState(); - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); _executeProposal(proposalId); @@ -309,6 +330,6 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { } function scheduleProposalExternal(uint256 proposalId) external { - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); } } diff --git a/test/scenario/proposal-deployment-modes.t.sol b/test/scenario/proposal-deployment-modes.t.sol index 1da2b173..93d95e69 100644 --- a/test/scenario/proposal-deployment-modes.t.sol +++ b/test/scenario/proposal-deployment-modes.t.sol @@ -1,35 +1,25 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {Durations} from "contracts/types/Duration.sol"; -import {Timestamps} from "contracts/types/Timestamp.sol"; - -import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; import {ExecutableProposals} from "contracts/libraries/ExecutableProposals.sol"; -import {percents, ScenarioTestBlueprint, ExternalCall} from "../utils/scenario-test-blueprint.sol"; +import {ScenarioTestBlueprint, ExternalCall} from "../utils/scenario-test-blueprint.sol"; contract ProposalDeploymentModesTest is ScenarioTestBlueprint { - function setUp() external { - _selectFork(); - - _deployTarget(); - } - function test_regular_deployment_mode() external { - _deployDualGovernanceSetup(false); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); (uint256 proposalId, ExternalCall[] memory regularStaffCalls) = _createAndAssertProposal(); _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.AfterSubmitDelayNotPassed.selector, (proposalId))); - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); - _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); @@ -41,19 +31,19 @@ contract ProposalDeploymentModesTest is ScenarioTestBlueprint { } function test_protected_deployment_mode_execute_after_timelock() external { - _deployDualGovernanceSetup(true); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); (uint256 proposalId, ExternalCall[] memory regularStaffCalls) = _createAndAssertProposal(); _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.AfterSubmitDelayNotPassed.selector, (proposalId))); - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); - _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); @@ -65,19 +55,19 @@ contract ProposalDeploymentModesTest is ScenarioTestBlueprint { } function test_protected_deployment_mode_execute_in_emergency_mode() external { - _deployDualGovernanceSetup(true); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); (uint256 proposalId, ExternalCall[] memory regularStaffCalls) = _createAndAssertProposal(); _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.AfterSubmitDelayNotPassed.selector, (proposalId))); - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); - _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); @@ -105,12 +95,12 @@ contract ProposalDeploymentModesTest is ScenarioTestBlueprint { _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.AfterSubmitDelayNotPassed.selector, (proposalId))); - _scheduleProposal(_dualGovernance, proposalId); + _scheduleProposalViaDualGovernance(proposalId); _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); - _assertCanSchedule(_dualGovernance, proposalId, true); - _scheduleProposal(_dualGovernance, proposalId); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); _assertProposalScheduled(proposalId); _waitAfterScheduleDelayPassed(); @@ -139,7 +129,7 @@ contract ProposalDeploymentModesTest is ScenarioTestBlueprint { } function _createAndAssertProposal() internal returns (uint256, ExternalCall[] memory) { - ExternalCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); uint256 proposalId = _submitProposal( _dualGovernance, "DAO does regular staff on potentially dangerous contract", regularStaffCalls diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol index 80aa6e97..54228184 100644 --- a/test/scenario/tiebraker.t.sol +++ b/test/scenario/tiebraker.t.sol @@ -1,22 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import { - ScenarioTestBlueprint, percents, ExternalCall, ExternalCallHelpers -} from "../utils/scenario-test-blueprint.sol"; +import {Durations} from "contracts/types/Duration.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; -import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; - -import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; +import {ScenarioTestBlueprint, ExternalCall, ExternalCallHelpers} from "../utils/scenario-test-blueprint.sol"; contract TiebreakerScenarioTest is ScenarioTestBlueprint { address internal immutable _VETOER = makeAddr("VETOER"); uint256 public constant PAUSE_INFINITELY = type(uint256).max; function setUp() external { - _selectFork(); - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); - _depositStETH(_VETOER, 1 ether); + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); + _setupStETHBalance( + _VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); } function test_proposal_approval() external { @@ -28,7 +26,7 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { // Tiebreak activation _assertNormalState(); - _lockStETH(_VETOER, percents(_dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); _lockStETH(_VETOER, 1 gwei); _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); _activateNextState(); @@ -45,19 +43,19 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { vm.prank(members[i]); _tiebreakerSubCommittees[0].scheduleProposal(proposalIdToExecute); (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getScheduleProposalState(proposalIdToExecute); - assert(support < quorum); - assert(isExecuted == false); + assertTrue(support < quorum); + assertFalse(isExecuted); } vm.prank(members[members.length - 1]); _tiebreakerSubCommittees[0].scheduleProposal(proposalIdToExecute); (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getScheduleProposalState(proposalIdToExecute); - assert(support == quorum); - assert(isExecuted == false); + assertEq(support, quorum); + assertFalse(isExecuted); _tiebreakerSubCommittees[0].executeScheduleProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _tiebreakerCommittee.getScheduleProposalState(proposalIdToExecute); - assert(support < quorum); + (support, quorum, isExecuted) = _tiebreakerCoreCommittee.getScheduleProposalState(proposalIdToExecute); + assertTrue(support < quorum); // Tiebreaker subcommittee 1 members = _tiebreakerSubCommittees[1].getMembers(); @@ -65,25 +63,25 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { vm.prank(members[i]); _tiebreakerSubCommittees[1].scheduleProposal(proposalIdToExecute); (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getScheduleProposalState(proposalIdToExecute); - assert(support < quorum); - assert(isExecuted == false); + assertTrue(support < quorum); + assertEq(isExecuted, false); } vm.prank(members[members.length - 1]); _tiebreakerSubCommittees[1].scheduleProposal(proposalIdToExecute); (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getScheduleProposalState(proposalIdToExecute); - assert(support == quorum); - assert(isExecuted == false); + assertEq(support, quorum); + assertFalse(isExecuted); // Approve proposal for scheduling _tiebreakerSubCommittees[1].executeScheduleProposal(proposalIdToExecute); - (support, quorum, isExecuted) = _tiebreakerCommittee.getScheduleProposalState(proposalIdToExecute); - assert(support == quorum); + (support, quorum, isExecuted) = _tiebreakerCoreCommittee.getScheduleProposalState(proposalIdToExecute); + assertEq(support, quorum); // Waiting for submit delay pass - _wait(_timelock.getAfterSubmitDelay()); + _wait(Durations.from(_tiebreakerCoreCommittee.timelockDuration())); - _tiebreakerCommittee.executeScheduleProposal(proposalIdToExecute); + _tiebreakerCoreCommittee.executeScheduleProposal(proposalIdToExecute); } function test_resume_withdrawals() external { @@ -93,17 +91,17 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { address[] memory members; - vm.prank(DAO_AGENT); - _WITHDRAWAL_QUEUE.grantRole( - 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(DAO_AGENT) + vm.prank(address(_lido.agent)); + _lido.withdrawalQueue.grantRole( + 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(_lido.agent) ); - vm.prank(DAO_AGENT); - _WITHDRAWAL_QUEUE.pauseFor(type(uint256).max); - assertEq(_WITHDRAWAL_QUEUE.isPaused(), true); + vm.prank(address(_lido.agent)); + _lido.withdrawalQueue.pauseFor(type(uint256).max); + assertEq(_lido.withdrawalQueue.isPaused(), true); // Tiebreak activation _assertNormalState(); - _lockStETH(_VETOER, percents(_dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); _lockStETH(_VETOER, 1 gwei); _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); _activateNextState(); @@ -115,50 +113,57 @@ contract TiebreakerScenarioTest is ScenarioTestBlueprint { members = _tiebreakerSubCommittees[0].getMembers(); for (uint256 i = 0; i < _tiebreakerSubCommittees[0].quorum() - 1; i++) { vm.prank(members[i]); - _tiebreakerSubCommittees[0].sealableResume(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[0].sealableResume(address(_lido.withdrawalQueue)); (support, quorum, isExecuted) = - _tiebreakerSubCommittees[0].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); - assert(support < quorum); - assert(isExecuted == false); + _tiebreakerSubCommittees[0].getSealableResumeState(address(_lido.withdrawalQueue)); + assertTrue(support < quorum); + assertFalse(isExecuted); } vm.prank(members[members.length - 1]); - _tiebreakerSubCommittees[0].sealableResume(address(_WITHDRAWAL_QUEUE)); - (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); - assert(support == quorum); - assert(isExecuted == false); - - _tiebreakerSubCommittees[0].executeSealableResume(address(_WITHDRAWAL_QUEUE)); - (support, quorum, isExecuted) = _tiebreakerCommittee.getSealableResumeState( - address(_WITHDRAWAL_QUEUE), _tiebreakerCommittee.getSealableResumeNonce(address(_WITHDRAWAL_QUEUE)) + _tiebreakerSubCommittees[0].sealableResume(address(_lido.withdrawalQueue)); + (support, quorum, isExecuted) = + _tiebreakerSubCommittees[0].getSealableResumeState(address(_lido.withdrawalQueue)); + assertEq(support, quorum); + assertFalse(isExecuted); + + _tiebreakerSubCommittees[0].executeSealableResume(address(_lido.withdrawalQueue)); + (support, quorum, isExecuted) = _tiebreakerCoreCommittee.getSealableResumeState( + address(_lido.withdrawalQueue), + _tiebreakerCoreCommittee.getSealableResumeNonce(address(_lido.withdrawalQueue)) ); - assert(support < quorum); + assertTrue(support < quorum); // Tiebreaker subcommittee 1 members = _tiebreakerSubCommittees[1].getMembers(); for (uint256 i = 0; i < _tiebreakerSubCommittees[1].quorum() - 1; i++) { vm.prank(members[i]); - _tiebreakerSubCommittees[1].sealableResume(address(_WITHDRAWAL_QUEUE)); + _tiebreakerSubCommittees[1].sealableResume(address(_lido.withdrawalQueue)); (support, quorum, isExecuted) = - _tiebreakerSubCommittees[1].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); - assert(support < quorum); - assert(isExecuted == false); + _tiebreakerSubCommittees[1].getSealableResumeState(address(_lido.withdrawalQueue)); + assertTrue(support < quorum); + assertEq(isExecuted, false); } vm.prank(members[members.length - 1]); - _tiebreakerSubCommittees[1].sealableResume(address(_WITHDRAWAL_QUEUE)); - (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); - assert(support == quorum); - assert(isExecuted == false); - - _tiebreakerSubCommittees[1].executeSealableResume(address(_WITHDRAWAL_QUEUE)); - (support, quorum, isExecuted) = _tiebreakerCommittee.getSealableResumeState( - address(_WITHDRAWAL_QUEUE), _tiebreakerCommittee.getSealableResumeNonce(address(_WITHDRAWAL_QUEUE)) + _tiebreakerSubCommittees[1].sealableResume(address(_lido.withdrawalQueue)); + (support, quorum, isExecuted) = + _tiebreakerSubCommittees[1].getSealableResumeState(address(_lido.withdrawalQueue)); + assertEq(support, quorum); + assertFalse(isExecuted); + + _tiebreakerSubCommittees[1].executeSealableResume(address(_lido.withdrawalQueue)); + (support, quorum, isExecuted) = _tiebreakerCoreCommittee.getSealableResumeState( + address(_lido.withdrawalQueue), + _tiebreakerCoreCommittee.getSealableResumeNonce(address(_lido.withdrawalQueue)) ); - assert(support == quorum); + assertEq(support, quorum); + + // Waiting for submit delay pass + _wait(Durations.from(_tiebreakerCoreCommittee.timelockDuration())); - _tiebreakerCommittee.executeSealableResume(address(_WITHDRAWAL_QUEUE)); + _tiebreakerCoreCommittee.executeSealableResume(address(_lido.withdrawalQueue)); - assertEq(_WITHDRAWAL_QUEUE.isPaused(), false); + assertEq(_lido.withdrawalQueue.isPaused(), false); } } diff --git a/test/scenario/timelocked-governance.t.sol b/test/scenario/timelocked-governance.t.sol index cdbfb9f0..0e452a97 100644 --- a/test/scenario/timelocked-governance.t.sol +++ b/test/scenario/timelocked-governance.t.sol @@ -1,20 +1,19 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {IGovernance} from "contracts/interfaces/ITimelock.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; -import {Timestamps, Timestamp} from "contracts/types/Timestamp.sol"; -import {Durations, minus} from "contracts/types/Duration.sol"; + +import {IGovernance} from "contracts/interfaces/IGovernance.sol"; import {ExternalCall} from "contracts/libraries/ExecutableProposals.sol"; import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; -import {ScenarioTestBlueprint, ExternalCallHelpers, IDangerousContract} from "../utils/scenario-test-blueprint.sol"; +import {ScenarioTestBlueprint, ExternalCallHelpers} from "../utils/scenario-test-blueprint.sol"; + +import {IPotentiallyDangerousContract} from "../utils/interfaces/IPotentiallyDangerousContract.sol"; contract TimelockedGovernanceScenario is ScenarioTestBlueprint { function setUp() external { - _selectFork(); - _deployTarget(); - _deployTimelockedGovernanceSetup( /* isEmergencyProtectionEnabled */ true); + _deployTimelockedGovernanceSetup({isEmergencyProtectionEnabled: true}); } function test_operatesAsDefault() external { @@ -77,7 +76,7 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { (uint256 maliciousProposalId,) = _submitAndAssertMaliciousProposal(); { _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); - _assertCanSchedule(_timelockedGovernance, maliciousProposalId, false); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, false); } // --- @@ -91,8 +90,8 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); - _assertCanSchedule(_timelockedGovernance, maliciousProposalId, true); - _scheduleProposal(_timelockedGovernance, maliciousProposalId); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, true); + _scheduleProposalViaTimelockedGovernance(maliciousProposalId); _waitAfterScheduleDelayPassed(); @@ -114,8 +113,8 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { _waitAfterSubmitDelayPassed(); - _assertCanSchedule(_timelockedGovernance, deactivateEmergencyModeProposalId, true); - _scheduleProposal(_timelockedGovernance, deactivateEmergencyModeProposalId); + _assertCanScheduleViaTimelockedGovernance(deactivateEmergencyModeProposalId, true); + _scheduleProposalViaTimelockedGovernance(deactivateEmergencyModeProposalId); _assertProposalScheduled(deactivateEmergencyModeProposalId); _waitAfterScheduleDelayPassed(); @@ -152,7 +151,7 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { (uint256 maliciousProposalId,) = _submitAndAssertMaliciousProposal(); { _wait(_timelock.getAfterSubmitDelay().dividedBy(2)); - _assertCanSchedule(_timelockedGovernance, maliciousProposalId, false); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, false); } // --- @@ -166,8 +165,8 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { _wait(_timelock.getAfterSubmitDelay().dividedBy(2).plusSeconds(1)); - _assertCanSchedule(_timelockedGovernance, maliciousProposalId, true); - _scheduleProposal(_timelockedGovernance, maliciousProposalId); + _assertCanScheduleViaTimelockedGovernance(maliciousProposalId, true); + _scheduleProposalViaTimelockedGovernance(maliciousProposalId); _waitAfterScheduleDelayPassed(); @@ -213,10 +212,20 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { // Act 2. DAO decides to upgrade system to dual governance. // --- { - _deployDualGovernance(); + _resealManager = _deployResealManager(_timelock); + _dualGovernanceConfigProvider = _deployDualGovernanceConfigProvider(); + _dualGovernance = _deployDualGovernance({ + timelock: _timelock, + resealManager: _resealManager, + configProvider: _dualGovernanceConfigProvider + }); ExternalCall[] memory dualGovernanceLaunchCalls = ExternalCallHelpers.create( - [address(_timelock)], [abi.encodeCall(_timelock.setGovernance, (address(_dualGovernance)))] + [address(_dualGovernance), address(_timelock)], + [ + abi.encodeCall(_dualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor())), + abi.encodeCall(_timelock.setGovernance, (address(_dualGovernance))) + ] ); uint256 dualGovernanceLunchProposalId = @@ -224,8 +233,8 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { _waitAfterSubmitDelayPassed(); - _assertCanSchedule(_timelockedGovernance, dualGovernanceLunchProposalId, true); - _scheduleProposal(_timelockedGovernance, dualGovernanceLunchProposalId); + _assertCanScheduleViaTimelockedGovernance(dualGovernanceLunchProposalId, true); + _scheduleProposalViaTimelockedGovernance(dualGovernanceLunchProposalId); _assertProposalScheduled(dualGovernanceLunchProposalId); _waitAfterScheduleDelayPassed(); @@ -264,8 +273,8 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { _waitAfterSubmitDelayPassed(); - _assertCanSchedule(_dualGovernance, timelockedGovernanceLunchProposalId, true); - _scheduleProposal(_dualGovernance, timelockedGovernanceLunchProposalId); + _assertCanScheduleViaDualGovernance(timelockedGovernanceLunchProposalId, true); + _scheduleProposalViaDualGovernance(timelockedGovernanceLunchProposalId); _waitAfterScheduleDelayPassed(); @@ -284,7 +293,7 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { } function _submitAndAssertProposal(IGovernance governance) internal returns (uint256, ExternalCall[] memory) { - ExternalCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); uint256 proposalId = _submitProposal(governance, "DAO does regular staff on potentially dangerous contract", regularStaffCalls); @@ -295,8 +304,9 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { } function _submitAndAssertMaliciousProposal() internal returns (uint256, ExternalCall[] memory) { - ExternalCall[] memory maliciousCalls = - ExternalCallHelpers.create(address(_target), abi.encodeCall(IDangerousContract.doRugPool, ())); + ExternalCall[] memory maliciousCalls = ExternalCallHelpers.create( + address(_targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRugPool, ()) + ); uint256 proposalId = _submitProposal( _timelockedGovernance, "DAO does malicious staff on potentially dangerous contract", maliciousCalls diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index 2ad238b0..d04fac99 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -1,31 +1,23 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import { - Escrow, - percents, - ExternalCall, - ExternalCallHelpers, - ScenarioTestBlueprint, - DualGovernance, - console -} from "../utils/scenario-test-blueprint.sol"; - -interface IDangerousContract { - function doRegularStaff(uint256 magic) external; - function doRugPool() external; - function doControversialStaff() external; -} +import {PercentsD16} from "contracts/types/PercentD16.sol"; +import {DualGovernance} from "contracts/DualGovernance.sol"; + +import {IPotentiallyDangerousContract} from "../utils/interfaces/IPotentiallyDangerousContract.sol"; + +import {LidoUtils} from "../utils/lido-utils.sol"; +import {Escrow, ExternalCall, ExternalCallHelpers, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { + using LidoUtils for LidoUtils.Context; + function setUp() external { - _selectFork(); - _deployTarget(); _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); } function testFork_ProposalSubmittedInRageQuitNonExecutableInTheNextVetoCooldown() external { - ExternalCall[] memory regularStaffCalls = _getTargetRegularStaffCalls(); + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); uint256 proposalId; _step("1. THE PROPOSAL IS SUBMITTED"); @@ -38,12 +30,15 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { _assertCanSchedule(_dualGovernance, proposalId, false); } - uint256 vetoedStETHAmount; address vetoer = makeAddr("MALICIOUS_ACTOR"); + _setupStETHBalance( + vetoer, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) + ); _step("2. THE SECOND SEAL RAGE QUIT SUPPORT IS ACQUIRED"); { - vetoedStETHAmount = - _lockStETH(vetoer, percents(_dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() + 1)); + _lockStETH( + vetoer, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1) + ); _assertVetoSignalingState(); _wait(_dualGovernanceConfigProvider.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); @@ -59,7 +54,9 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { anotherProposalId = _submitProposal( _dualGovernance, "Another Proposal", - ExternalCallHelpers.create(address(_target), abi.encodeCall(IDangerousContract.doRugPool, ())) + ExternalCallHelpers.create( + address(_targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRugPool, ()) + ) ); } @@ -72,8 +69,7 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { rageQuitEscrow.requestNextWithdrawalsBatch(96); } - vm.deal(address(_WITHDRAWAL_QUEUE), 2 * vetoedStETHAmount); - _finalizeWQ(); + _lido.finalizeWithdrawalQueue(); while (!rageQuitEscrow.isWithdrawalsClaimed()) { rageQuitEscrow.claimNextWithdrawalsBatch(128); @@ -107,20 +103,4 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { function scheduleProposalExternal(uint256 proposalId) external { _scheduleProposal(_dualGovernance, proposalId); } - - function _finalizeWQ() internal { - uint256 lastRequestId = _WITHDRAWAL_QUEUE.getLastRequestId(); - _finalizeWQ(lastRequestId); - } - - function _finalizeWQ(uint256 id) internal { - uint256 finalizationShareRate = _ST_ETH.getPooledEthByShares(1e27) + 1e9; // TODO check finalization rate - address lido = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; - vm.prank(lido); - _WITHDRAWAL_QUEUE.finalize(id, finalizationShareRate); - - bytes32 LOCKED_ETHER_AMOUNT_POSITION = 0x0e27eaa2e71c8572ab988fef0b54cd45bbd1740de1e22343fb6cda7536edc12f; // keccak256("lido.WithdrawalQueue.lockedEtherAmount"); - - vm.store(address(_WITHDRAWAL_QUEUE), LOCKED_ETHER_AMOUNT_POSITION, bytes32(address(_WITHDRAWAL_QUEUE).balance)); - } } diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 2233ea23..cd6b3fd3 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -1,855 +1,889 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -// import {Vm} from "forge-std/Test.sol"; -// import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; - -// import {Executor} from "contracts/Executor.sol"; -// import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; -// import {ITimelock, ProposalStatus} from "contracts/interfaces/ITimelock.sol"; -// import {Executor} from "contracts/Executor.sol"; -// import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; -// import {ExecutableProposals} from "contracts/libraries/ExecutableProposals.sol"; - -// import {UnitTest, Duration, Timestamp, Timestamps, Durations, console} from "test/utils/unit-test.sol"; -// import {TargetMock} from "test/utils/utils.sol"; -// import {ExternalCall, ExternalCallHelpers} from "test/utils/executor-calls.sol"; -// import {IDangerousContract} from "test/utils/interfaces.sol"; -// import {Deployment} from "test/utils/deployment.sol"; - -// contract EmergencyProtectedTimelockUnitTests is UnitTest { -// EmergencyProtectedTimelock private _timelock; -// TargetMock private _targetMock; -// Executor private _executor; +import {Vm} from "forge-std/Test.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -// address private _emergencyActivator = makeAddr("EMERGENCY_ACTIVATION_COMMITTEE"); -// address private _emergencyEnactor = makeAddr("EMERGENCY_EXECUTION_COMMITTEE"); -// Duration private _emergencyModeDuration = Durations.from(180 days); -// Duration private _emergencyProtectionDuration = Durations.from(90 days); +import {Duration, Durations} from "contracts/types/Duration.sol"; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -// address private _emergencyGovernance = makeAddr("EMERGENCY_GOVERNANCE"); -// address private _dualGovernance = makeAddr("DUAL_GOVERNANCE"); -// address private _adminExecutor; +import {ITimelock, ProposalStatus} from "contracts/interfaces/ITimelock.sol"; -// function setUp() external { -// _executor = new Executor(address(this)); -// _timelock = new EmergencyProtectedTimelock( -// EmergencyProtectedTimelock.SanityCheckParams({ -// maxAfterSubmitDelay: Durations.from(45 days), -// maxAfterScheduleDelay: Durations.from(45 days), -// maxEmergencyModeDuration: Durations.from(365 days), -// maxEmergencyProtectionDuration: Durations.from(365 days) -// }), -// _executor -// ); +import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; -// _targetMock = new TargetMock(); +import {Executor} from "contracts/Executor.sol"; +import {EmergencyProtectedTimelock, TimelockState} from "contracts/EmergencyProtectedTimelock.sol"; -// _executor.transferOwnership(address(_timelock)); -// _adminExecutor = address(_executor); +import {UnitTest} from "test/utils/unit-test.sol"; +import {TargetMock} from "test/utils/target-mock.sol"; +import {ExternalCall} from "test/utils/executor-calls.sol"; -// vm.startPrank(_adminExecutor); -// _timelock.setGovernance(_dualGovernance); -// _timelock.setDelays({afterSubmitDelay: Durations.from(3 days), afterScheduleDelay: Durations.from(2 days)}); -// _timelock.setEmergencyProtection( -// _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration -// ); -// vm.stopPrank(); -// } +contract EmergencyProtectedTimelockUnitTests is UnitTest { + EmergencyProtectedTimelock private _timelock; + TargetMock private _targetMock; + Executor private _executor; -// // EmergencyProtectedTimelock.submit() + address private _emergencyActivator = makeAddr("EMERGENCY_ACTIVATION_COMMITTEE"); + address private _emergencyEnactor = makeAddr("EMERGENCY_EXECUTION_COMMITTEE"); + Duration private _emergencyModeDuration = Durations.from(180 days); + Duration private _emergencyProtectionDuration = Durations.from(90 days); -// function testFuzz_stranger_cannot_submit_proposal(address stranger) external { -// vm.assume(stranger != _dualGovernance); + address private _emergencyGovernance = makeAddr("EMERGENCY_GOVERNANCE"); + address private _dualGovernance = makeAddr("DUAL_GOVERNANCE"); + address private _adminExecutor; -// vm.prank(stranger); -// vm.expectRevert( -// abi.encodeWithSelector(EmergencyProtectedTimelock.NotGovernance.selector, [stranger, _dualGovernance]) -// ); -// _timelock.submit(_adminExecutor, new ExternalCall[](0)); -// assertEq(_timelock.getProposalsCount(), 0); -// } + function setUp() external { + _executor = new Executor(address(this)); + _adminExecutor = address(_executor); -// function test_governance_can_submit_proposal() external { -// vm.prank(_dualGovernance); -// _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls(address(_targetMock))); + _timelock = _deployEmergencyProtectedTimelock(); -// assertEq(_timelock.getProposalsCount(), 1); + _targetMock = new TargetMock(); -// ITimelock.Proposal memory proposal = _timelock.getProposal(1); -// assertEq(proposal.status, ProposalStatus.Submitted); -// } + _executor.transferOwnership(address(_timelock)); -// // EmergencyProtectedTimelock.schedule() + vm.startPrank(_adminExecutor); + _timelock.setGovernance(_dualGovernance); + _timelock.setDelays({afterSubmitDelay: Durations.from(3 days), afterScheduleDelay: Durations.from(2 days)}); + _timelock.setupEmergencyProtection( + _emergencyGovernance, + _emergencyActivator, + _emergencyEnactor, + _emergencyProtectionDuration.addTo(Timestamps.now()), + _emergencyModeDuration + ); + vm.stopPrank(); + } -// function test_governance_can_schedule_proposal() external { -// _submitProposal(); + // EmergencyProtectedTimelock.submit() -// assertEq(_timelock.getProposalsCount(), 1); + function testFuzz_stranger_cannot_submit_proposal(address stranger) external { + vm.assume(stranger != _dualGovernance); -// _wait(_timelock.getAfterSubmitDelay()); + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidGovernance.selector, [stranger])); + _timelock.submit(_adminExecutor, new ExternalCall[](0)); + assertEq(_timelock.getProposalsCount(), 0); + } -// _scheduleProposal(1); + function test_governance_can_submit_proposal() external { + vm.prank(_dualGovernance); + _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); -// ITimelock.Proposal memory proposal = _timelock.getProposal(1); -// assertEq(proposal.status, ProposalStatus.Scheduled); -// } + assertEq(_timelock.getProposalsCount(), 1); -// function testFuzz_stranger_cannot_schedule_proposal(address stranger) external { -// vm.assume(stranger != _dualGovernance); -// vm.assume(stranger != address(0)); + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Submitted); + } -// _submitProposal(); + // EmergencyProtectedTimelock.schedule() -// vm.prank(stranger); -// vm.expectRevert( -// abi.encodeWithSelector(EmergencyProtectedTimelock.NotGovernance.selector, [stranger, _dualGovernance]) -// ); -// _timelock.schedule(1); + function test_governance_can_schedule_proposal() external { + _submitProposal(); -// ITimelock.Proposal memory proposal = _timelock.getProposal(1); -// assertEq(proposal.status, ProposalStatus.Submitted); -// } + assertEq(_timelock.getProposalsCount(), 1); -// // EmergencyProtectedTimelock.execute() + _wait(_timelock.getAfterSubmitDelay()); -// function testFuzz_anyone_can_execute_proposal(address stranger) external { -// vm.assume(stranger != _dualGovernance); -// vm.assume(stranger != address(0)); + _scheduleProposal(1); -// _submitProposal(); -// assertEq(_timelock.getProposalsCount(), 1); + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Scheduled); + } -// _wait(_timelock.getAfterSubmitDelay()); + function testFuzz_stranger_cannot_schedule_proposal(address stranger) external { + vm.assume(stranger != _dualGovernance); + vm.assume(stranger != address(0)); -// _scheduleProposal(1); + _submitProposal(); -// _wait(_timelock.getAfterScheduleDelay()); + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidGovernance.selector, [stranger])); -// vm.prank(stranger); -// _timelock.execute(1); + _timelock.schedule(1); -// ITimelock.Proposal memory proposal = _timelock.getProposal(1); -// assertEq(proposal.status, ProposalStatus.Executed); -// } + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Submitted); + } -// function test_cannot_execute_proposal_if_emergency_mode_active() external { -// _submitProposal(); + // EmergencyProtectedTimelock.execute() -// assertEq(_timelock.getProposalsCount(), 1); + function testFuzz_anyone_can_execute_proposal(address stranger) external { + vm.assume(stranger != _dualGovernance); + vm.assume(stranger != address(0)); -// _wait(_timelock.getAfterSubmitDelay()); -// _scheduleProposal(1); + _submitProposal(); + assertEq(_timelock.getProposalsCount(), 1); -// _wait(_timelock.getAfterScheduleDelay()); + _wait(_timelock.getAfterSubmitDelay()); -// _activateEmergencyMode(); + _scheduleProposal(1); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeStatus.selector, [true, false])); -// _timelock.execute(1); + _wait(_timelock.getAfterScheduleDelay()); -// ITimelock.Proposal memory proposal = _timelock.getProposal(1); -// assertEq(proposal.status, ProposalStatus.Scheduled); -// } + vm.prank(stranger); + _timelock.execute(1); -// // EmergencyProtectedTimelock.cancelAllNonExecutedProposals() + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Executed); + } -// function test_governance_can_cancel_all_non_executed_proposals() external { -// _submitProposal(); -// _submitProposal(); + function test_cannot_execute_proposal_if_emergency_mode_active() external { + _submitProposal(); -// assertEq(_timelock.getProposalsCount(), 2); + assertEq(_timelock.getProposalsCount(), 1); -// _wait(_timelock.getAfterSubmitDelay()); + _wait(_timelock.getAfterSubmitDelay()); + _scheduleProposal(1); -// _scheduleProposal(1); + _wait(_timelock.getAfterScheduleDelay()); -// ITimelock.Proposal memory proposal1 = _timelock.getProposal(1); -// ITimelock.Proposal memory proposal2 = _timelock.getProposal(2); + _activateEmergencyMode(); -// assertEq(proposal1.status, ProposalStatus.Scheduled); -// assertEq(proposal2.status, ProposalStatus.Submitted); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, [false])); + _timelock.execute(1); -// vm.prank(_dualGovernance); -// _timelock.cancelAllNonExecutedProposals(); + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Scheduled); + } -// proposal1 = _timelock.getProposal(1); -// proposal2 = _timelock.getProposal(2); + // EmergencyProtectedTimelock.cancelAllNonExecutedProposals() -// assertEq(_timelock.getProposalsCount(), 2); -// assertEq(proposal1.status, ProposalStatus.Cancelled); -// assertEq(proposal2.status, ProposalStatus.Cancelled); -// } + function test_governance_can_cancel_all_non_executed_proposals() external { + _submitProposal(); + _submitProposal(); -// function testFuzz_stranger_cannot_cancel_all_non_executed_proposals(address stranger) external { -// vm.assume(stranger != _dualGovernance); -// vm.assume(stranger != address(0)); + assertEq(_timelock.getProposalsCount(), 2); -// vm.prank(stranger); -// vm.expectRevert( -// abi.encodeWithSelector(EmergencyProtectedTimelock.NotGovernance.selector, [stranger, _dualGovernance]) -// ); -// _timelock.cancelAllNonExecutedProposals(); -// } + _wait(_timelock.getAfterSubmitDelay()); -// // EmergencyProtectedTimelock.transferExecutorOwnership() + _scheduleProposal(1); -// function testFuzz_admin_executor_can_transfer_executor_ownership(address newOwner) external { -// vm.assume(newOwner != _adminExecutor); -// vm.assume(newOwner != address(0)); + ITimelock.Proposal memory proposal1 = _timelock.getProposal(1); + ITimelock.Proposal memory proposal2 = _timelock.getProposal(2); -// Executor executor = new Executor(address(_timelock)); + assertEq(proposal1.status, ProposalStatus.Scheduled); + assertEq(proposal2.status, ProposalStatus.Submitted); -// assertEq(executor.owner(), address(_timelock)); + vm.prank(_dualGovernance); + _timelock.cancelAllNonExecutedProposals(); -// vm.prank(_adminExecutor); + proposal1 = _timelock.getProposal(1); + proposal2 = _timelock.getProposal(2); -// vm.expectEmit(address(executor)); -// emit Ownable.OwnershipTransferred(address(_timelock), newOwner); + assertEq(_timelock.getProposalsCount(), 2); + assertEq(proposal1.status, ProposalStatus.Cancelled); + assertEq(proposal2.status, ProposalStatus.Cancelled); + } -// _timelock.transferExecutorOwnership(address(executor), newOwner); + function testFuzz_stranger_cannot_cancel_all_non_executed_proposals(address stranger) external { + vm.assume(stranger != _dualGovernance); + vm.assume(stranger != address(0)); -// assertEq(executor.owner(), newOwner); -// } + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidGovernance.selector, [stranger])); -// function test_stranger_cannot_transfer_executor_ownership(address stranger) external { -// vm.assume(stranger != _adminExecutor); + _timelock.cancelAllNonExecutedProposals(); + } -// vm.prank(stranger); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.NotAdminExecutor.selector, stranger)); -// _timelock.transferExecutorOwnership(_adminExecutor, makeAddr("newOwner")); -// } + // EmergencyProtectedTimelock.transferExecutorOwnership() -// // EmergencyProtectedTimelock.setGovernance() + function testFuzz_admin_executor_can_transfer_executor_ownership(address newOwner) external { + vm.assume(newOwner != _adminExecutor); + vm.assume(newOwner != address(0)); -// function testFuzz_admin_executor_can_set_governance(address newGovernance) external { -// vm.assume(newGovernance != _dualGovernance); -// vm.assume(newGovernance != address(0)); + Executor executor = new Executor(address(_timelock)); -// vm.expectEmit(address(_timelock)); -// emit EmergencyProtectedTimelock.GovernanceSet(newGovernance); + assertEq(executor.owner(), address(_timelock)); -// vm.recordLogs(); -// vm.prank(_adminExecutor); -// _timelock.setGovernance(newGovernance); + vm.prank(_adminExecutor); -// assertEq(_timelock.getGovernance(), newGovernance); + vm.expectEmit(address(executor)); + emit Ownable.OwnershipTransferred(address(_timelock), newOwner); -// Vm.Log[] memory entries = vm.getRecordedLogs(); + _timelock.transferExecutorOwnership(address(executor), newOwner); -// assertEq(entries.length, 1); -// } + assertEq(executor.owner(), newOwner); + } -// function test_cannot_set_governance_to_zero() external { -// vm.prank(_adminExecutor); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidGovernance.selector, address(0))); -// _timelock.setGovernance(address(0)); -// } + function test_stranger_cannot_transfer_executor_ownership(address stranger) external { + vm.assume(stranger != _adminExecutor); -// function test_cannot_set_governance_to_the_same_address() external { -// address currentGovernance = _timelock.getGovernance(); -// vm.prank(_adminExecutor); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidGovernance.selector, _dualGovernance)); -// _timelock.setGovernance(currentGovernance); + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidAdminExecutor.selector, stranger)); + _timelock.transferExecutorOwnership(_adminExecutor, makeAddr("newOwner")); + } -// assertEq(_timelock.getGovernance(), currentGovernance); -// } + // EmergencyProtectedTimelock.setGovernance() -// function testFuzz_stranger_cannot_set_governance(address stranger) external { -// vm.assume(stranger != _adminExecutor); + function testFuzz_admin_executor_can_set_governance(address newGovernance) external { + vm.assume(newGovernance != _dualGovernance); + vm.assume(newGovernance != address(0)); -// vm.prank(stranger); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.NotAdminExecutor.selector, stranger)); -// _timelock.setGovernance(makeAddr("newGovernance")); -// } + vm.expectEmit(address(_timelock)); + emit TimelockState.GovernanceSet(newGovernance); -// // EmergencyProtectedTimelock.activateEmergencyMode() + vm.recordLogs(); + vm.prank(_adminExecutor); + _timelock.setGovernance(newGovernance); -// function test_emergency_activator_can_activate_emergency_mode() external { -// vm.prank(_emergencyActivator); -// _timelock.activateEmergencyMode(); + assertEq(_timelock.getGovernance(), newGovernance); -// assertEq(_isEmergencyStateActivated(), true); -// } + Vm.Log[] memory entries = vm.getRecordedLogs(); -// function testFuzz_stranger_cannot_activate_emergency_mode(address stranger) external { -// vm.assume(stranger != _emergencyActivator); -// vm.assume(stranger != address(0)); + assertEq(entries.length, 1); + } -// vm.prank(stranger); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyActivator.selector, stranger)); -// _timelock.activateEmergencyMode(); + function test_cannot_set_governance_to_zero() external { + vm.prank(_adminExecutor); + vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidGovernance.selector, address(0))); + _timelock.setGovernance(address(0)); + } -// assertEq(_isEmergencyStateActivated(), false); -// } + // TODO: Update test after the convention about return/revert is resolved + // function test_cannot_set_governance_to_the_same_address() external { + // address currentGovernance = _timelock.getGovernance(); + // vm.prank(_adminExecutor); + // vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidGovernance.selector, _dualGovernance)); + // _timelock.setGovernance(currentGovernance); -// function test_cannot_activate_emergency_mode_if_already_active() external { -// _activateEmergencyMode(); + // assertEq(_timelock.getGovernance(), currentGovernance); + // } -// assertEq(_isEmergencyStateActivated(), true); + function testFuzz_stranger_cannot_set_governance(address stranger) external { + vm.assume(stranger != _adminExecutor); -// vm.prank(_emergencyActivator); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeStatus.selector, [true, false])); -// _timelock.activateEmergencyMode(); + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidAdminExecutor.selector, stranger)); + _timelock.setGovernance(makeAddr("newGovernance")); + } -// assertEq(_isEmergencyStateActivated(), true); -// } + // EmergencyProtectedTimelock.activateEmergencyMode() -// // EmergencyProtectedTimelock.emergencyExecute() + function test_emergency_activator_can_activate_emergency_mode() external { + vm.prank(_emergencyActivator); + _timelock.activateEmergencyMode(); -// function test_emergency_executior_can_execute_proposal() external { -// _submitProposal(); + assertEq(_isEmergencyStateActivated(), true); + } -// assertEq(_timelock.getProposalsCount(), 1); + function testFuzz_stranger_cannot_activate_emergency_mode(address stranger) external { + vm.assume(stranger != _emergencyActivator); + vm.assume(stranger != address(0)); -// _wait(_timelock.getAfterSubmitDelay()); + vm.prank(stranger); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyActivationCommittee.selector, stranger) + ); + _timelock.activateEmergencyMode(); -// _scheduleProposal(1); + assertEq(_isEmergencyStateActivated(), false); + } -// _wait(_timelock.getAfterScheduleDelay()); + function test_cannot_activate_emergency_mode_if_already_active() external { + _activateEmergencyMode(); -// _activateEmergencyMode(); + assertEq(_isEmergencyStateActivated(), true); -// assertEq(_isEmergencyStateActivated(), true); + vm.prank(_emergencyActivator); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, [false])); + _timelock.activateEmergencyMode(); -// vm.prank(_emergencyEnactor); -// _timelock.emergencyExecute(1); + assertEq(_isEmergencyStateActivated(), true); + } -// ITimelock.Proposal memory proposal = _timelock.getProposal(1); -// assertEq(proposal.status, ProposalStatus.Executed); -// } + // EmergencyProtectedTimelock.emergencyExecute() -// function test_cannot_emergency_execute_proposal_if_mode_not_activated() external { -// vm.startPrank(_dualGovernance); -// _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls(address(_targetMock))); + function test_emergency_executior_can_execute_proposal() external { + _submitProposal(); -// assertEq(_timelock.getProposalsCount(), 1); + assertEq(_timelock.getProposalsCount(), 1); -// _wait(_timelock.getAfterSubmitDelay()); -// _timelock.schedule(1); + _wait(_timelock.getAfterSubmitDelay()); -// _wait(_timelock.getAfterScheduleDelay()); -// vm.stopPrank(); + _scheduleProposal(1); -// assertEq(_timelock.isEmergencyModeActive(), false); + _wait(_timelock.getAfterScheduleDelay()); -// vm.prank(_emergencyActivator); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeStatus.selector, [false, true])); -// _timelock.emergencyExecute(1); -// } + _activateEmergencyMode(); -// function testFuzz_stranger_cannot_emergency_execute_proposal(address stranger) external { -// vm.assume(stranger != _emergencyEnactor); -// vm.assume(stranger != address(0)); + assertEq(_isEmergencyStateActivated(), true); -// _submitProposal(); + vm.prank(_emergencyEnactor); + _timelock.emergencyExecute(1); -// assertEq(_timelock.getProposalsCount(), 1); + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Executed); + } -// _wait(_timelock.getAfterSubmitDelay()); + function test_cannot_emergency_execute_proposal_if_mode_not_activated() external { + vm.startPrank(_dualGovernance); + _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); -// _scheduleProposal(1); + assertEq(_timelock.getProposalsCount(), 1); -// _wait(_timelock.getAfterScheduleDelay()); + _wait(_timelock.getAfterSubmitDelay()); + _timelock.schedule(1); -// _activateEmergencyMode(); + _wait(_timelock.getAfterScheduleDelay()); + vm.stopPrank(); -// assertEq(_isEmergencyStateActivated(), true); + assertEq(_timelock.isEmergencyModeActive(), false); -// vm.prank(stranger); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, stranger)); -// _timelock.emergencyExecute(1); -// } + vm.prank(_emergencyActivator); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, [true])); + _timelock.emergencyExecute(1); + } -// // EmergencyProtectedTimelock.deactivateEmergencyMode() + function testFuzz_stranger_cannot_emergency_execute_proposal(address stranger) external { + vm.assume(stranger != _emergencyEnactor); + vm.assume(stranger != address(0)); -// function test_admin_executor_can_deactivate_emergency_mode_if_delay_not_passed() external { -// _submitProposal(); -// _activateEmergencyMode(); + _submitProposal(); -// vm.prank(_adminExecutor); -// _timelock.deactivateEmergencyMode(); + assertEq(_timelock.getProposalsCount(), 1); -// assertEq(_isEmergencyStateActivated(), false); -// } + _wait(_timelock.getAfterSubmitDelay()); -// function test_after_deactivation_all_proposals_are_cancelled() external { -// _submitProposal(); + _scheduleProposal(1); -// assertEq(_timelock.getProposalsCount(), 1); + _wait(_timelock.getAfterScheduleDelay()); -// ITimelock.Proposal memory proposal = _timelock.getProposal(1); -// assertEq(proposal.status, ProposalStatus.Submitted); + _activateEmergencyMode(); -// _activateEmergencyMode(); + assertEq(_isEmergencyStateActivated(), true); -// _deactivateEmergencyMode(); + vm.prank(stranger); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyExecutionCommittee.selector, stranger) + ); + _timelock.emergencyExecute(1); + } -// proposal = _timelock.getProposal(1); -// assertEq(proposal.status, ProposalStatus.Cancelled); -// } + // EmergencyProtectedTimelock.deactivateEmergencyMode() -// function testFuzz_stranger_can_deactivate_emergency_mode_if_passed(address stranger) external { -// vm.assume(stranger != _adminExecutor); + function test_admin_executor_can_deactivate_emergency_mode_if_delay_not_passed() external { + _submitProposal(); + _activateEmergencyMode(); -// _activateEmergencyMode(); + vm.prank(_adminExecutor); + _timelock.deactivateEmergencyMode(); -// EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); -// assertEq(_isEmergencyStateActivated(), true); + assertEq(_isEmergencyStateActivated(), false); + } -// _wait(state.emergencyModeDuration.plusSeconds(1)); + function test_after_deactivation_all_proposals_are_cancelled() external { + _submitProposal(); -// vm.prank(stranger); -// _timelock.deactivateEmergencyMode(); + assertEq(_timelock.getProposalsCount(), 1); -// state = _timelock.getEmergencyState(); -// assertEq(_isEmergencyStateActivated(), false); -// } + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Submitted); -// function testFuzz_cannot_deactivate_emergency_mode_if_not_activated(address stranger) external { -// vm.assume(stranger != _adminExecutor); + _activateEmergencyMode(); -// vm.prank(stranger); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeStatus.selector, [false, true])); -// _timelock.deactivateEmergencyMode(); + _deactivateEmergencyMode(); -// vm.prank(_adminExecutor); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeStatus.selector, [false, true])); -// _timelock.deactivateEmergencyMode(); -// } + proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Cancelled); + } -// function testFuzz_stranger_cannot_deactivate_emergency_mode_if_not_passed(address stranger) external { -// vm.assume(stranger != _adminExecutor); + function testFuzz_stranger_can_deactivate_emergency_mode_if_passed(address stranger) external { + vm.assume(stranger != _adminExecutor); -// _activateEmergencyMode(); -// assertEq(_isEmergencyStateActivated(), true); + _activateEmergencyMode(); -// vm.prank(stranger); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.NotAdminExecutor.selector, stranger)); -// _timelock.deactivateEmergencyMode(); -// } + EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); + assertEq(_isEmergencyStateActivated(), true); -// // EmergencyProtectedTimelock.emergencyReset() + _wait(state.emergencyModeDuration.plusSeconds(1)); -// function test_execution_committee_can_emergency_reset() external { -// _activateEmergencyMode(); -// assertEq(_isEmergencyStateActivated(), true); -// assertEq(_timelock.isEmergencyProtectionEnabled(), true); + vm.prank(stranger); + _timelock.deactivateEmergencyMode(); -// vm.prank(_emergencyEnactor); -// _timelock.emergencyReset(); + assertEq(_isEmergencyStateActivated(), false); + } -// EmergencyProtection.Context memory newState = _timelock.getEmergencyProtectionContext(); + function testFuzz_cannot_deactivate_emergency_mode_if_not_activated(address stranger) external { + vm.assume(stranger != _adminExecutor); -// assertEq(_isEmergencyStateActivated(), false); -// assertEq(_timelock.getGovernance(), _emergencyGovernance); -// assertEq(_timelock.isEmergencyProtectionEnabled(), false); + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, [true])); + _timelock.deactivateEmergencyMode(); -// assertEq(newState.activationCommittee, address(0)); -// assertEq(newState.executionCommittee, address(0)); -// assertEq(newState.protectedTill, Timestamps.ZERO); -// assertEq(newState.emergencyModeDuration, Durations.ZERO); -// assertEq(newState.emergencyModeEndsAfter, Timestamps.ZERO); -// } + vm.prank(_adminExecutor); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, [true])); + _timelock.deactivateEmergencyMode(); + } -// function test_after_emergency_reset_all_proposals_are_cancelled() external { -// _submitProposal(); -// _activateEmergencyMode(); + function testFuzz_stranger_cannot_deactivate_emergency_mode_if_not_passed(address stranger) external { + vm.assume(stranger != _adminExecutor); -// ITimelock.Proposal memory proposal = _timelock.getProposal(1); -// assertEq(proposal.status, ProposalStatus.Submitted); + _activateEmergencyMode(); + assertEq(_isEmergencyStateActivated(), true); -// vm.prank(_emergencyEnactor); -// _timelock.emergencyReset(); + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidAdminExecutor.selector, stranger)); + _timelock.deactivateEmergencyMode(); + } -// proposal = _timelock.getProposal(1); -// assertEq(proposal.status, ProposalStatus.Cancelled); -// } + // EmergencyProtectedTimelock.emergencyReset() -// function testFuzz_stranger_cannot_emergency_reset_governance(address stranger) external { -// vm.assume(stranger != _emergencyEnactor); -// vm.assume(stranger != address(0)); + function test_execution_committee_can_emergency_reset() external { + _activateEmergencyMode(); + assertEq(_isEmergencyStateActivated(), true); + assertEq(_timelock.isEmergencyProtectionEnabled(), true); -// _activateEmergencyMode(); + vm.prank(_emergencyEnactor); + _timelock.emergencyReset(); -// assertEq(_isEmergencyStateActivated(), true); + EmergencyProtection.Context memory newState = _timelock.getEmergencyProtectionContext(); -// vm.prank(stranger); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, stranger)); -// _timelock.emergencyReset(); + assertEq(_isEmergencyStateActivated(), false); + assertEq(_timelock.getGovernance(), _emergencyGovernance); + assertEq(_timelock.isEmergencyProtectionEnabled(), false); -// assertEq(_isEmergencyStateActivated(), true); -// } + assertEq(newState.emergencyActivationCommittee, address(0)); + assertEq(newState.emergencyExecutionCommittee, address(0)); + assertEq(newState.emergencyProtectionEndsAfter, Timestamps.ZERO); + assertEq(newState.emergencyModeDuration, Durations.ZERO); + assertEq(newState.emergencyModeEndsAfter, Timestamps.ZERO); + } -// function test_cannot_emergency_reset_if_emergency_mode_not_activated() external { -// assertEq(_isEmergencyStateActivated(), false); + function test_after_emergency_reset_all_proposals_are_cancelled() external { + _submitProposal(); + _activateEmergencyMode(); -// EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); + ITimelock.Proposal memory proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Submitted); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeStatus.selector, [false, true])); -// vm.prank(_emergencyEnactor); -// _timelock.emergencyReset(); + vm.prank(_emergencyEnactor); + _timelock.emergencyReset(); -// EmergencyProtection.Context memory newState = _timelock.getEmergencyProtectionContext(); + proposal = _timelock.getProposal(1); + assertEq(proposal.status, ProposalStatus.Cancelled); + } -// assertEq(newState.executionCommittee, state.executionCommittee); -// assertEq(newState.activationCommittee, state.activationCommittee); -// assertEq(newState.protectedTill, state.protectedTill); -// assertEq(newState.emergencyModeEndsAfter, state.emergencyModeEndsAfter); -// assertEq(newState.emergencyModeDuration, state.emergencyModeDuration); -// assertEq(newState.isEmergencyModeActivated, state.isEmergencyModeActivated); -// } + function testFuzz_stranger_cannot_emergency_reset_governance(address stranger) external { + vm.assume(stranger != _emergencyEnactor); + vm.assume(stranger != address(0)); -// // EmergencyProtectedTimelock.setEmergencyProtection() + _activateEmergencyMode(); -// function test_admin_executor_can_set_emenrgency_protection() external { -// EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + assertEq(_isEmergencyStateActivated(), true); -// vm.prank(_adminExecutor); -// _localTimelock.setEmergencyProtection( -// _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration -// ); + vm.prank(stranger); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyExecutionCommittee.selector, stranger) + ); + _timelock.emergencyReset(); -// EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); + assertEq(_isEmergencyStateActivated(), true); + } -// assertEq(state.activationCommittee, _emergencyActivator); -// assertEq(state.executionCommittee, _emergencyEnactor); -// assertEq(state.protectedTill, _emergencyProtectionDuration.addTo(Timestamps.now())); -// assertEq(state.emergencyModeDuration, _emergencyModeDuration); -// assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); -// assertEq(state.isEmergencyModeActivated, false); -// } + function test_cannot_emergency_reset_if_emergency_mode_not_activated() external { + assertEq(_isEmergencyStateActivated(), false); -// function testFuzz_stranger_cannot_set_emergency_protection(address stranger) external { -// vm.assume(stranger != _adminExecutor); -// vm.assume(stranger != address(0)); + EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); -// EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, [true])); + vm.prank(_emergencyEnactor); + _timelock.emergencyReset(); -// vm.prank(stranger); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.NotAdminExecutor.selector, stranger)); -// _localTimelock.setEmergencyProtection( -// _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration -// ); + EmergencyProtection.Context memory newState = _timelock.getEmergencyProtectionContext(); -// EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); + assertEq(newState.emergencyExecutionCommittee, state.emergencyExecutionCommittee); + assertEq(newState.emergencyActivationCommittee, state.emergencyActivationCommittee); + assertEq(newState.emergencyProtectionEndsAfter, state.emergencyProtectionEndsAfter); + assertEq(newState.emergencyModeEndsAfter, state.emergencyModeEndsAfter); + assertEq(newState.emergencyModeDuration, state.emergencyModeDuration); + assertFalse(_timelock.isEmergencyModeActive()); + } -// assertEq(state.activationCommittee, address(0)); -// assertEq(state.executionCommittee, address(0)); -// assertEq(state.protectedTill, Timestamps.ZERO); -// assertEq(state.emergencyModeDuration, Durations.ZERO); -// assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); -// assertEq(state.isEmergencyModeActivated, false); -// } + // EmergencyProtectedTimelock.setupEmergencyProtection() -// // EmergencyProtectedTimelock.isEmergencyProtectionEnabled() + function test_admin_executor_can_set_emenrgency_protection() external { + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); -// function test_is_emergency_protection_enabled_deactivate() external { -// EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + vm.prank(_adminExecutor); + _localTimelock.setupEmergencyProtection( + _emergencyGovernance, + _emergencyActivator, + _emergencyEnactor, + _emergencyProtectionDuration.addTo(Timestamps.now()), + _emergencyModeDuration + ); -// assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); + EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); -// vm.prank(_adminExecutor); -// _localTimelock.setEmergencyProtection( -// _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration -// ); + assertEq(state.emergencyActivationCommittee, _emergencyActivator); + assertEq(state.emergencyExecutionCommittee, _emergencyEnactor); + assertEq(state.emergencyProtectionEndsAfter, _emergencyProtectionDuration.addTo(Timestamps.now())); + assertEq(state.emergencyModeDuration, _emergencyModeDuration); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); + assertFalse(_timelock.isEmergencyModeActive()); + } -// assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); + function testFuzz_stranger_cannot_set_emergency_protection(address stranger) external { + vm.assume(stranger != _adminExecutor); + vm.assume(stranger != address(0)); -// vm.prank(_emergencyActivator); -// _localTimelock.activateEmergencyMode(); + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); -// assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.InvalidAdminExecutor.selector, stranger)); + _localTimelock.setupEmergencyProtection( + _emergencyGovernance, + _emergencyActivator, + _emergencyEnactor, + _emergencyProtectionDuration.addTo(Timestamps.now()), + _emergencyModeDuration + ); -// vm.prank(_adminExecutor); -// _localTimelock.deactivateEmergencyMode(); + EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); -// assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); -// } + assertEq(state.emergencyActivationCommittee, address(0)); + assertEq(state.emergencyExecutionCommittee, address(0)); + assertEq(state.emergencyProtectionEndsAfter, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); + assertFalse(_localTimelock.isEmergencyModeActive()); + } -// function test_is_emergency_protection_enabled_reset() external { -// EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + // EmergencyProtectedTimelock.isEmergencyProtectionEnabled() -// assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); + function test_is_emergency_protection_enabled_deactivate() external { + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); -// vm.prank(_adminExecutor); -// _localTimelock.setEmergencyProtection( -// _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration -// ); + assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); -// assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); + vm.prank(_adminExecutor); + _localTimelock.setupEmergencyProtection( + _emergencyGovernance, + _emergencyActivator, + _emergencyEnactor, + _emergencyProtectionDuration.addTo(Timestamps.now()), + _emergencyModeDuration + ); -// vm.prank(_emergencyActivator); -// _localTimelock.activateEmergencyMode(); + assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); -// assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); + vm.prank(_emergencyActivator); + _localTimelock.activateEmergencyMode(); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); + + vm.prank(_adminExecutor); + _localTimelock.deactivateEmergencyMode(); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); + } + + function test_is_emergency_protection_enabled_reset() external { + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); -// vm.prank(_emergencyEnactor); -// _localTimelock.emergencyReset(); + vm.prank(_adminExecutor); + _localTimelock.setupEmergencyProtection( + _emergencyGovernance, + _emergencyActivator, + _emergencyEnactor, + _emergencyProtectionDuration.addTo(Timestamps.now()), + _emergencyModeDuration + ); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); + + vm.prank(_emergencyActivator); + _localTimelock.activateEmergencyMode(); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), true); + + vm.prank(_emergencyEnactor); + _localTimelock.emergencyReset(); + + assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); + } + + // EmergencyProtectedTimelock.getEmergencyProtectionContext() + + function test_get_emergency_state_deactivate() external { + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); + + EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); -// assertEq(_localTimelock.isEmergencyProtectionEnabled(), false); -// } + assertFalse(_localTimelock.isEmergencyModeActive()); + assertEq(state.emergencyActivationCommittee, address(0)); + assertEq(state.emergencyExecutionCommittee, address(0)); + assertEq(state.emergencyProtectionEndsAfter, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); + + vm.prank(_adminExecutor); + _localTimelock.setupEmergencyProtection( + _emergencyGovernance, + _emergencyActivator, + _emergencyEnactor, + _emergencyProtectionDuration.addTo(Timestamps.now()), + _emergencyModeDuration + ); -// // EmergencyProtectedTimelock.getEmergencyState() + state = _localTimelock.getEmergencyProtectionContext(); -// function test_get_emergency_state_deactivate() external { -// EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + assertEq(_localTimelock.isEmergencyModeActive(), false); + assertEq(state.emergencyActivationCommittee, _emergencyActivator); + assertEq(state.emergencyExecutionCommittee, _emergencyEnactor); + assertEq(state.emergencyProtectionEndsAfter, _emergencyProtectionDuration.addTo(Timestamps.now())); + assertEq(state.emergencyModeDuration, _emergencyModeDuration); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); -// EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); + vm.prank(_emergencyActivator); + _localTimelock.activateEmergencyMode(); -// assertEq(state.isEmergencyModeActivated, false); -// assertEq(state.activationCommittee, address(0)); -// assertEq(state.executionCommittee, address(0)); -// assertEq(state.protectedTill, Timestamps.ZERO); -// assertEq(state.emergencyModeDuration, Durations.ZERO); -// assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); + state = _localTimelock.getEmergencyProtectionContext(); -// vm.prank(_adminExecutor); -// _localTimelock.setEmergencyProtection( -// _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration -// ); + assertEq(_localTimelock.isEmergencyModeActive(), true); + assertEq(state.emergencyExecutionCommittee, _emergencyEnactor); + assertEq(state.emergencyActivationCommittee, _emergencyActivator); + assertEq(state.emergencyModeDuration, _emergencyModeDuration); + assertEq(state.emergencyProtectionEndsAfter, _emergencyProtectionDuration.addTo(Timestamps.now())); + assertEq(state.emergencyModeEndsAfter, _emergencyModeDuration.addTo(Timestamps.now())); -// state = _localTimelock.getEmergencyState(); + vm.prank(_adminExecutor); + _localTimelock.deactivateEmergencyMode(); -// assertEq(_localTimelock.getEmergencyState().isEmergencyModeActivated, false); -// assertEq(state.activationCommittee, _emergencyActivator); -// assertEq(state.executionCommittee, _emergencyEnactor); -// assertEq(state.protectedTill, _emergencyProtectionDuration.addTo(Timestamps.now())); -// assertEq(state.emergencyModeDuration, _emergencyModeDuration); -// assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); + state = _localTimelock.getEmergencyProtectionContext(); -// vm.prank(_emergencyActivator); -// _localTimelock.activateEmergencyMode(); + assertFalse(_timelock.isEmergencyModeActive()); + assertEq(state.emergencyActivationCommittee, address(0)); + assertEq(state.emergencyExecutionCommittee, address(0)); + assertEq(state.emergencyProtectionEndsAfter, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); + } -// state = _localTimelock.getEmergencyState(); + function test_get_emergency_state_reset() external { + EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); -// assertEq(_localTimelock.getEmergencyState().isEmergencyModeActivated, true); -// assertEq(state.executionCommittee, _emergencyEnactor); -// assertEq(state.activationCommittee, _emergencyActivator); -// assertEq(state.emergencyModeDuration, _emergencyModeDuration); -// assertEq(state.protectedTill, _emergencyProtectionDuration.addTo(Timestamps.now())); -// assertEq(state.emergencyModeEndsAfter, _emergencyModeDuration.addTo(Timestamps.now())); + vm.prank(_adminExecutor); + _localTimelock.setupEmergencyProtection( + _emergencyGovernance, + _emergencyActivator, + _emergencyEnactor, + _emergencyProtectionDuration.addTo(Timestamps.now()), + _emergencyModeDuration + ); + + vm.prank(_emergencyActivator); + _localTimelock.activateEmergencyMode(); + + vm.prank(_emergencyEnactor); + _localTimelock.emergencyReset(); + + EmergencyProtection.Context memory state = _localTimelock.getEmergencyProtectionContext(); + + assertFalse(_timelock.isEmergencyModeActive()); + assertEq(state.emergencyActivationCommittee, address(0)); + assertEq(state.emergencyExecutionCommittee, address(0)); + assertEq(state.emergencyProtectionEndsAfter, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); + } + + // EmergencyProtectedTimelock.getGovernance() + + function testFuzz_get_governance(address governance) external { + vm.assume(governance != address(0) && governance != _timelock.getGovernance()); + vm.prank(_adminExecutor); + _timelock.setGovernance(governance); + assertEq(_timelock.getGovernance(), governance); + } + + // EmergencyProtectedTimelock.getProposal() + + function test_get_proposal() external { + assertEq(_timelock.getProposalsCount(), 0); + + vm.startPrank(_dualGovernance); + ExternalCall[] memory executorCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); + _timelock.submit(_adminExecutor, executorCalls); + _timelock.submit(_adminExecutor, executorCalls); + + ITimelock.Proposal memory submittedProposal = _timelock.getProposal(1); + + Timestamp submitTimestamp = Timestamps.now(); + + assertEq(submittedProposal.id, 1); + assertEq(submittedProposal.executor, _adminExecutor); + assertEq(submittedProposal.submittedAt, submitTimestamp); + assertEq(submittedProposal.scheduledAt, Timestamps.ZERO); + assertEq(submittedProposal.status, ProposalStatus.Submitted); + assertEq(submittedProposal.calls.length, 1); + assertEq(submittedProposal.calls[0].value, executorCalls[0].value); + assertEq(submittedProposal.calls[0].target, executorCalls[0].target); + assertEq(submittedProposal.calls[0].payload, executorCalls[0].payload); + + _wait(_timelock.getAfterSubmitDelay()); -// vm.prank(_adminExecutor); -// _localTimelock.deactivateEmergencyMode(); + _timelock.schedule(1); + Timestamp scheduleTimestamp = Timestamps.now(); -// state = _localTimelock.getEmergencyState(); + ITimelock.Proposal memory scheduledProposal = _timelock.getProposal(1); -// assertEq(state.isEmergencyModeActivated, false); -// assertEq(state.activationCommittee, address(0)); -// assertEq(state.executionCommittee, address(0)); -// assertEq(state.protectedTill, Timestamps.ZERO); -// assertEq(state.emergencyModeDuration, Durations.ZERO); -// assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); -// } + assertEq(scheduledProposal.id, 1); + assertEq(scheduledProposal.executor, _adminExecutor); + assertEq(scheduledProposal.submittedAt, submitTimestamp); + assertEq(scheduledProposal.scheduledAt, scheduleTimestamp); + assertEq(scheduledProposal.status, ProposalStatus.Scheduled); + assertEq(scheduledProposal.calls.length, 1); + assertEq(scheduledProposal.calls[0].value, executorCalls[0].value); + assertEq(scheduledProposal.calls[0].target, executorCalls[0].target); + assertEq(scheduledProposal.calls[0].payload, executorCalls[0].payload); -// function test_get_emergency_state_reset() external { -// EmergencyProtectedTimelock _localTimelock = new EmergencyProtectedTimelock(address(_config)); + _wait(_timelock.getAfterScheduleDelay()); -// vm.prank(_adminExecutor); -// _localTimelock.setEmergencyProtection( -// _emergencyActivator, _emergencyEnactor, _emergencyProtectionDuration, _emergencyModeDuration -// ); + _timelock.execute(1); -// vm.prank(_emergencyActivator); -// _localTimelock.activateEmergencyMode(); + ITimelock.Proposal memory executedProposal = _timelock.getProposal(1); + Timestamp executeTimestamp = Timestamps.now(); -// vm.prank(_emergencyEnactor); -// _localTimelock.emergencyReset(); + assertEq(executedProposal.id, 1); + assertEq(executedProposal.status, ProposalStatus.Executed); + assertEq(executedProposal.executor, _adminExecutor); + assertEq(executedProposal.submittedAt, submitTimestamp); + assertEq(executedProposal.scheduledAt, scheduleTimestamp); + // assertEq(executedProposal.executedAt, executeTimestamp); + // assertEq doesn't support comparing enumerables so far + assertEq(executedProposal.calls.length, 1); + assertEq(executedProposal.calls[0].value, executorCalls[0].value); + assertEq(executedProposal.calls[0].target, executorCalls[0].target); + assertEq(executedProposal.calls[0].payload, executorCalls[0].payload); -// EmergencyProtection.Context memory state = _timelock.getEmergencyProtectionContext(); + _timelock.cancelAllNonExecutedProposals(); -// assertEq(state.isEmergencyModeActivated, false); -// assertEq(state.activationCommittee, address(0)); -// assertEq(state.executionCommittee, address(0)); -// assertEq(state.protectedTill, Timestamps.ZERO); -// assertEq(state.emergencyModeDuration, Durations.ZERO); -// assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); -// } + ITimelock.Proposal memory cancelledProposal = _timelock.getProposal(2); -// // EmergencyProtectedTimelock.getGovernance() + assertEq(cancelledProposal.id, 2); + assertEq(cancelledProposal.status, ProposalStatus.Cancelled); + assertEq(cancelledProposal.executor, _adminExecutor); + assertEq(cancelledProposal.submittedAt, submitTimestamp); + assertEq(cancelledProposal.scheduledAt, Timestamps.ZERO); + // assertEq(cancelledProposal.executedAt, Timestamps.ZERO); + // assertEq doesn't support comparing enumerables so far + assertEq(cancelledProposal.calls.length, 1); + assertEq(cancelledProposal.calls[0].value, executorCalls[0].value); + assertEq(cancelledProposal.calls[0].target, executorCalls[0].target); + assertEq(cancelledProposal.calls[0].payload, executorCalls[0].payload); + } -// function testFuzz_get_governance(address governance) external { -// vm.assume(governance != address(0) && governance != _timelock.getGovernance()); -// vm.prank(_adminExecutor); -// _timelock.setGovernance(governance); -// assertEq(_timelock.getGovernance(), governance); -// } + function test_get_not_existing_proposal() external { + assertEq(_timelock.getProposalsCount(), 0); -// // EmergencyProtectedTimelock.getProposal() - -// function test_get_proposal() external { -// assertEq(_timelock.getProposalsCount(), 0); - -// vm.startPrank(_dualGovernance); -// ExternalCall[] memory executorCalls = _getTargetRegularStaffCalls(address(_targetMock)); -// _timelock.submit(_adminExecutor, executorCalls); -// _timelock.submit(_adminExecutor, executorCalls); + vm.expectRevert(); + _timelock.getProposal(1); + } -// ITimelock.Proposal memory submittedProposal = _timelock.getProposal(1); + // EmergencyProtectedTimelock.getProposalsCount() -// Timestamp submitTimestamp = Timestamps.now(); + function testFuzz_get_proposals_count(uint256 count) external { + vm.assume(count > 0); + vm.assume(count <= type(uint8).max); + assertEq(_timelock.getProposalsCount(), 0); -// assertEq(submittedProposal.id, 1); -// assertEq(submittedProposal.executor, _adminExecutor); -// assertEq(submittedProposal.submittedAt, submitTimestamp); -// assertEq(submittedProposal.scheduledAt, Timestamps.ZERO); -// assertEq(submittedProposal.status, ProposalStatus.Submitted); -// assertEq(submittedProposal.calls.length, 1); -// assertEq(submittedProposal.calls[0].value, executorCalls[0].value); -// assertEq(submittedProposal.calls[0].target, executorCalls[0].target); -// assertEq(submittedProposal.calls[0].payload, executorCalls[0].payload); + for (uint256 i = 1; i <= count; i++) { + _submitProposal(); + assertEq(_timelock.getProposalsCount(), i); + } + } -// _wait(_timelock.getAfterSubmitDelay()); + // EmergencyProtectedTimelock.canExecute() -// _timelock.schedule(1); -// Timestamp scheduleTimestamp = Timestamps.now(); + function test_can_execute() external { + assertEq(_timelock.canExecute(1), false); + _submitProposal(); + assertEq(_timelock.canExecute(1), false); -// ITimelock.Proposal memory scheduledProposal = _timelock.getProposal(1); + _wait(_timelock.getAfterSubmitDelay()); -// assertEq(scheduledProposal.id, 1); -// assertEq(scheduledProposal.executor, _adminExecutor); -// assertEq(scheduledProposal.submittedAt, submitTimestamp); -// assertEq(scheduledProposal.scheduledAt, scheduleTimestamp); -// assertEq(scheduledProposal.status, ProposalStatus.Scheduled); -// assertEq(scheduledProposal.calls.length, 1); -// assertEq(scheduledProposal.calls[0].value, executorCalls[0].value); -// assertEq(scheduledProposal.calls[0].target, executorCalls[0].target); -// assertEq(scheduledProposal.calls[0].payload, executorCalls[0].payload); + _scheduleProposal(1); -// _wait(_timelock.getAfterScheduleDelay()); + assertEq(_timelock.canExecute(1), false); -// _timelock.execute(1); + _wait(_timelock.getAfterScheduleDelay()); -// ITimelock.Proposal memory executedProposal = _timelock.getProposal(1); -// Timestamp executeTimestamp = Timestamps.now(); + assertEq(_timelock.canExecute(1), true); -// assertEq(executedProposal.id, 1); -// assertEq(executedProposal.status, ProposalStatus.Executed); -// assertEq(executedProposal.executor, _adminExecutor); -// assertEq(executedProposal.submittedAt, submitTimestamp); -// assertEq(executedProposal.scheduledAt, scheduleTimestamp); -// // assertEq(executedProposal.executedAt, executeTimestamp); -// // assertEq doesn't support comparing enumerables so far -// assertEq(executedProposal.calls.length, 1); -// assertEq(executedProposal.calls[0].value, executorCalls[0].value); -// assertEq(executedProposal.calls[0].target, executorCalls[0].target); -// assertEq(executedProposal.calls[0].payload, executorCalls[0].payload); + vm.prank(_dualGovernance); + _timelock.cancelAllNonExecutedProposals(); -// _timelock.cancelAllNonExecutedProposals(); + assertEq(_timelock.canExecute(1), false); + } -// ITimelock.Proposal memory cancelledProposal = _timelock.getProposal(2); + // EmergencyProtectedTimelock.canSchedule() -// assertEq(cancelledProposal.id, 2); -// assertEq(cancelledProposal.status, ProposalStatus.Cancelled); -// assertEq(cancelledProposal.executor, _adminExecutor); -// assertEq(cancelledProposal.submittedAt, submitTimestamp); -// assertEq(cancelledProposal.scheduledAt, Timestamps.ZERO); -// // assertEq(cancelledProposal.executedAt, Timestamps.ZERO); -// // assertEq doesn't support comparing enumerables so far -// assertEq(cancelledProposal.calls.length, 1); -// assertEq(cancelledProposal.calls[0].value, executorCalls[0].value); -// assertEq(cancelledProposal.calls[0].target, executorCalls[0].target); -// assertEq(cancelledProposal.calls[0].payload, executorCalls[0].payload); -// } + function test_can_schedule() external { + assertEq(_timelock.canExecute(1), false); + _submitProposal(); -// function test_get_not_existing_proposal() external { -// assertEq(_timelock.getProposalsCount(), 0); + _wait(_timelock.getAfterSubmitDelay()); -// vm.expectRevert(); -// _timelock.getProposal(1); -// } + assertEq(_timelock.canSchedule(1), true); -// // EmergencyProtectedTimelock.getProposalsCount() + assertEq(_timelock.canSchedule(1), true); + + _scheduleProposal(1); -// function testFuzz_get_proposals_count(uint256 count) external { -// vm.assume(count > 0); -// vm.assume(count <= type(uint8).max); -// assertEq(_timelock.getProposalsCount(), 0); + assertEq(_timelock.canSchedule(1), false); -// for (uint256 i = 1; i <= count; i++) { -// _submitProposal(); -// assertEq(_timelock.getProposalsCount(), i); -// } -// } + vm.prank(_dualGovernance); + _timelock.cancelAllNonExecutedProposals(); -// // EmergencyProtectedTimelock.canExecute() + assertEq(_timelock.canSchedule(1), false); + } -// function test_can_execute() external { -// assertEq(_timelock.canExecute(1), false); -// _submitProposal(); -// assertEq(_timelock.canExecute(1), false); + function test_get_proposal_submission_time() external { + _submitProposal(); + assertEq(_timelock.getProposal(1).submittedAt, Timestamps.now()); + } -// _wait(_timelock.getAfterSubmitDelay()); + // Utils -// _scheduleProposal(1); + function _submitProposal() internal { + vm.prank(_dualGovernance); + _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); + } -// assertEq(_timelock.canExecute(1), false); + function _scheduleProposal(uint256 proposalId) internal { + vm.prank(_dualGovernance); + _timelock.schedule(proposalId); + } -// _wait(_timelock.getAfterScheduleDelay()); + function _isEmergencyStateActivated() internal view returns (bool) { + return _timelock.isEmergencyModeActive(); + } -// assertEq(_timelock.canExecute(1), true); + function _activateEmergencyMode() internal { + vm.prank(_emergencyActivator); + _timelock.activateEmergencyMode(); + assertEq(_isEmergencyStateActivated(), true); + } -// vm.prank(_dualGovernance); -// _timelock.cancelAllNonExecutedProposals(); + function _deactivateEmergencyMode() internal { + vm.prank(_adminExecutor); + _timelock.deactivateEmergencyMode(); + assertEq(_isEmergencyStateActivated(), false); + } -// assertEq(_timelock.canExecute(1), false); -// } - -// // EmergencyProtectedTimelock.canSchedule() - -// function test_can_schedule() external { -// assertEq(_timelock.canExecute(1), false); -// _submitProposal(); - -// // the scheduling is possible just after submit, because the AFTER_SUBMIT_DELAY is equal to zero -// assertEq(_timelock.canSchedule(1), true); - -// _wait(_timelock.getAfterSubmitDelay()); - -// assertEq(_timelock.canSchedule(1), true); - -// _scheduleProposal(1); - -// assertEq(_timelock.canSchedule(1), false); - -// vm.prank(_dualGovernance); -// _timelock.cancelAllNonExecutedProposals(); - -// assertEq(_timelock.canSchedule(1), false); -// } - -// function test_get_proposal_submission_time() external { -// _submitProposal(); -// assertEq(_timelock.getProposal(1).submittedAt, Timestamps.now()); -// } - -// // Utils - -// function _submitProposal() internal { -// vm.prank(_dualGovernance); -// _timelock.submit(_adminExecutor, _getTargetRegularStaffCalls(address(_targetMock))); -// } - -// function _scheduleProposal(uint256 proposalId) internal { -// vm.prank(_dualGovernance); -// _timelock.schedule(proposalId); -// } - -// function _isEmergencyStateActivated() internal view returns (bool) { -// EmergencyState memory state = _timelock.getEmergencyState(); -// return state.isEmergencyModeActivated; -// } - -// function _activateEmergencyMode() internal { -// vm.prank(_emergencyActivator); -// _timelock.activateEmergencyMode(); -// assertEq(_isEmergencyStateActivated(), true); -// } - -// function _deactivateEmergencyMode() internal { -// vm.prank(_adminExecutor); -// _timelock.deactivateEmergencyMode(); -// assertEq(_isEmergencyStateActivated(), false); -// } -// } + function _deployEmergencyProtectedTimelock() internal returns (EmergencyProtectedTimelock) { + return new EmergencyProtectedTimelock( + EmergencyProtectedTimelock.SanityCheckParams({ + maxAfterSubmitDelay: Durations.from(45 days), + maxAfterScheduleDelay: Durations.from(45 days), + maxEmergencyModeDuration: Durations.from(365 days), + maxEmergencyProtectionDuration: Durations.from(365 days) + }), + _adminExecutor + ); + } +} diff --git a/test/unit/TimelockedGovernance.t.sol b/test/unit/TimelockedGovernance.t.sol index d507fd1c..adf0fe17 100644 --- a/test/unit/TimelockedGovernance.t.sol +++ b/test/unit/TimelockedGovernance.t.sol @@ -1,132 +1,115 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -// import {Vm} from "forge-std/Test.sol"; +import {ITimelock} from "contracts/interfaces/ITimelock.sol"; +import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; -// import {Executor} from "contracts/Executor.sol"; -// import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; -// import {IConfigurableTimelock, IEmergencyProtectedTimelockConfig} from "contracts/interfaces/ITimelock.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; -// import {UnitTest} from "test/utils/unit-test.sol"; -// import {TargetMock} from "test/utils/utils.sol"; +import {TimelockMock} from "./mocks/TimelockMock.sol"; -// import {TimelockMock} from "./mocks/TimelockMock.sol"; -// import {IEmergencyProtectedTimelockConfigProvider} from -// "contracts/configuration/EmergencyProtectedTimelockConfigProvider.sol"; -// import {Deployment} from "test/utils/deployment.sol"; +contract TimelockedGovernanceUnitTests is UnitTest { + TimelockMock private _timelock; + TimelockedGovernance private _timelockedGovernance; -// contract ConfigurableTimelockMock is TimelockMock, IConfigurableTimelock { -// IEmergencyProtectedTimelockConfig public immutable CONFIG; + address private _emergencyGovernance = makeAddr("EMERGENCY_GOVERNANCE"); + address private _governance = makeAddr("GOVERNANCE"); + address private _adminExecutor = makeAddr("ADMIN_EXECUTOR"); -// constructor(IEmergencyProtectedTimelockConfig config) { -// CONFIG = config; -// } -// } + function setUp() external { + _timelock = new TimelockMock(_adminExecutor); + _timelockedGovernance = new TimelockedGovernance(_governance, _timelock); + } -// contract SingleGovernanceUnitTests is UnitTest { -// TimelockMock private _timelock; -// IEmergencyProtectedTimelockConfigProvider private _config; -// TimelockedGovernance private _timelockedGovernance; + function testFuzz_constructor(address governance, ITimelock timelock) external { + TimelockedGovernance instance = new TimelockedGovernance(governance, timelock); -// address private _emergencyGovernance = makeAddr("EMERGENCY_GOVERNANCE"); -// address private _governance = makeAddr("GOVERNANCE"); + assertEq(instance.GOVERNANCE(), governance); + assertEq(address(instance.TIMELOCK()), address(timelock)); + } -// function setUp() external { -// Executor _executor = new Executor(address(this)); -// _config = Deployment.deployEmergencyProtectedTimelockConfigProvider(); -// _timelock = new ConfigurableTimelockMock(_config); -// _timelockedGovernance = new TimelockedGovernance(_governance, address(_timelock)); -// } + function test_submit_proposal() external { + assertEq(_timelock.getSubmittedProposals().length, 0); -// function testFuzz_constructor(address governance, address timelock) external { -// SingleGovernance instance = new SingleGovernance(governance, timelock); + vm.prank(_governance); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); -// assertEq(instance.GOVERNANCE(), governance); -// assertEq(address(instance.TIMELOCK()), address(timelock)); -// } + assertEq(_timelock.getSubmittedProposals().length, 1); + } -// function test_submit_proposal() external { -// assertEq(_timelock.getSubmittedProposals().length, 0); + function testFuzz_stranger_cannot_submit_proposal(address stranger) external { + vm.assume(stranger != address(0) && stranger != _governance); -// vm.prank(_governance); -// _timelockedGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + assertEq(_timelock.getSubmittedProposals().length, 0); -// assertEq(_timelock.getSubmittedProposals().length, 1); -// } + vm.startPrank(stranger); + vm.expectRevert(abi.encodeWithSelector(TimelockedGovernance.NotGovernance.selector, [stranger])); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); -// function testFuzz_stranger_cannot_submit_proposal(address stranger) external { -// vm.assume(stranger != address(0) && stranger != _governance); + assertEq(_timelock.getSubmittedProposals().length, 0); + } -// assertEq(_timelock.getSubmittedProposals().length, 0); + function test_schedule_proposal() external { + assertEq(_timelock.getScheduledProposals().length, 0); -// vm.startPrank(stranger); -// vm.expectRevert(abi.encodeWithSelector(TimelockedGovernance.NotGovernance.selector, [stranger])); -// _timelockedGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + vm.prank(_governance); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); -// assertEq(_timelock.getSubmittedProposals().length, 0); -// } + _timelock.setSchedule(1); + _timelockedGovernance.scheduleProposal(1); -// function test_schedule_proposal() external { -// assertEq(_timelock.getScheduledProposals().length, 0); + assertEq(_timelock.getScheduledProposals().length, 1); + } -// vm.prank(_governance); -// _timelockedGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + function test_execute_proposal() external { + assertEq(_timelock.getExecutedProposals().length, 0); -// _timelock.setSchedule(1); -// _timelockedGovernance.scheduleProposal(1); + vm.prank(_governance); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); -// assertEq(_timelock.getScheduledProposals().length, 1); -// } + _timelock.setSchedule(1); + _timelockedGovernance.scheduleProposal(1); -// function test_execute_proposal() external { -// assertEq(_timelock.getExecutedProposals().length, 0); + _timelockedGovernance.executeProposal(1); -// vm.prank(_governance); -// _timelockedGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + assertEq(_timelock.getExecutedProposals().length, 1); + } -// _timelock.setSchedule(1); -// _timelockedGovernance.scheduleProposal(1); + function test_cancel_all_pending_proposals() external { + assertEq(_timelock.getLastCancelledProposalId(), 0); -// _timelockedGovernance.executeProposal(1); + vm.startPrank(_governance); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); -// assertEq(_timelock.getExecutedProposals().length, 1); -// } + _timelock.setSchedule(1); + _timelockedGovernance.scheduleProposal(1); -// function test_cancel_all_pending_proposals() external { -// assertEq(_timelock.getLastCancelledProposalId(), 0); + _timelockedGovernance.cancelAllPendingProposals(); -// vm.startPrank(_governance); -// _timelockedGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); -// _timelockedGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); + assertEq(_timelock.getLastCancelledProposalId(), 2); + } -// _timelock.setSchedule(1); -// _timelockedGovernance.scheduleProposal(1); + function testFuzz_stranger_cannot_cancel_all_pending_proposals(address stranger) external { + vm.assume(stranger != address(0) && stranger != _governance); -// _timelockedGovernance.cancelAllPendingProposals(); + assertEq(_timelock.getLastCancelledProposalId(), 0); -// assertEq(_timelock.getLastCancelledProposalId(), 2); -// } + vm.startPrank(stranger); + vm.expectRevert(abi.encodeWithSelector(TimelockedGovernance.NotGovernance.selector, [stranger])); + _timelockedGovernance.cancelAllPendingProposals(); -// function testFuzz_stranger_cannot_cancel_all_pending_proposals(address stranger) external { -// vm.assume(stranger != address(0) && stranger != _governance); + assertEq(_timelock.getLastCancelledProposalId(), 0); + } -// assertEq(_timelock.getLastCancelledProposalId(), 0); + function test_can_schedule() external { + vm.prank(_governance); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1))); -// vm.startPrank(stranger); -// vm.expectRevert(abi.encodeWithSelector(TimelockedGovernance.NotGovernance.selector, [stranger])); -// _timelockedGovernance.cancelAllPendingProposals(); + assertFalse(_timelockedGovernance.canScheduleProposal(1)); -// assertEq(_timelock.getLastCancelledProposalId(), 0); -// } + _timelock.setSchedule(1); -// function test_can_schedule() external { -// vm.prank(_governance); -// _timelockedGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); - -// assertFalse(_timelockedGovernance.canScheduleProposal(1)); - -// _timelock.setSchedule(1); - -// assertTrue(_timelockedGovernance.canScheduleProposal(1)); -// } -// } + assertTrue(_timelockedGovernance.canScheduleProposal(1)); + } +} diff --git a/test/unit/libraries/EmergencyProtection.t.sol b/test/unit/libraries/EmergencyProtection.t.sol index d3e19c69..c286ed6e 100644 --- a/test/unit/libraries/EmergencyProtection.t.sol +++ b/test/unit/libraries/EmergencyProtection.t.sol @@ -1,411 +1,398 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -// import {Test, Vm} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Test.sol"; + +import {Timestamps} from "contracts/types/Timestamp.sol"; +import {Duration, Durations} from "contracts/types/Duration.sol"; + +import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; + +contract EmergencyProtectionUnitTests is UnitTest { + using EmergencyProtection for EmergencyProtection.Context; + + address internal _emergencyGovernance = makeAddr("EMERGENCY_GOVERNANCE"); + + EmergencyProtection.Context internal _emergencyProtection; + + function testFuzz_setup_emergency_protection( + address activationCommittee, + address executionCommittee, + address emergencyGovernance, + Duration protectionDuration, + Duration duration + ) external { + vm.assume(protectionDuration > Durations.ZERO); + vm.assume(duration > Durations.ZERO); + // vm.assume(activationCommittee != address(0)); + // vm.assume(executionCommittee != address(0)); + uint256 expectedLogEntiresCount = 2; + if (emergencyGovernance != address(0)) { + vm.expectEmit(); + emit EmergencyProtection.EmergencyGovernanceSet(emergencyGovernance); + expectedLogEntiresCount += 1; + } + + if (activationCommittee != address(0)) { + vm.expectEmit(); + emit EmergencyProtection.EmergencyActivationCommitteeSet(activationCommittee); + expectedLogEntiresCount += 1; + } + if (executionCommittee != address(0)) { + vm.expectEmit(); + emit EmergencyProtection.EmergencyExecutionCommitteeSet(executionCommittee); + expectedLogEntiresCount += 1; + } + vm.expectEmit(); + emit EmergencyProtection.EmergencyProtectionEndDateSet(protectionDuration.addTo(Timestamps.now())); + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeDurationSet(duration); + + vm.recordLogs(); + + _setup(emergencyGovernance, activationCommittee, executionCommittee, protectionDuration, duration); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, expectedLogEntiresCount); + + assertEq(_emergencyProtection.emergencyGovernance, emergencyGovernance); + assertEq(_emergencyProtection.emergencyActivationCommittee, activationCommittee); + assertEq(_emergencyProtection.emergencyExecutionCommittee, executionCommittee); + assertEq(_emergencyProtection.emergencyProtectionEndsAfter, protectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyModeDuration, duration); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); + } + + function test_setup_same_activation_committee() external { + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + address activationCommittee = makeAddr("activationCommittee"); + + _setup(_emergencyGovernance, activationCommittee, address(0x2), protectionDuration, emergencyModeDuration); + + Duration newProtectionDuration = Durations.from(200 seconds); + Duration newEmergencyModeDuration = Durations.from(300 seconds); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x3)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyProtectionEndDateSet(newProtectionDuration.addTo(Timestamps.now())); + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); + + vm.recordLogs(); + _setup(_emergencyGovernance, activationCommittee, address(0x3), newProtectionDuration, newEmergencyModeDuration); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 3); + + assertEq(_emergencyProtection.emergencyActivationCommittee, activationCommittee); + assertEq(_emergencyProtection.emergencyExecutionCommittee, address(0x3)); + assertEq(_emergencyProtection.emergencyProtectionEndsAfter, newProtectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); + } + + function test_setup_same_execution_committee() external { + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + address executionCommittee = makeAddr("executionCommittee"); + + _setup(_emergencyGovernance, address(0x1), executionCommittee, protectionDuration, emergencyModeDuration); + + Duration newProtectionDuration = Durations.from(200 seconds); + Duration newEmergencyModeDuration = Durations.from(300 seconds); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x2)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyProtectionEndDateSet(newProtectionDuration.addTo(Timestamps.now())); + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); + + vm.recordLogs(); + _setup(_emergencyGovernance, address(0x2), executionCommittee, newProtectionDuration, newEmergencyModeDuration); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 3); + + assertEq(_emergencyProtection.emergencyActivationCommittee, address(0x2)); + assertEq(_emergencyProtection.emergencyExecutionCommittee, executionCommittee); + assertEq(_emergencyProtection.emergencyProtectionEndsAfter, newProtectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); + } + + function test_setup_same_protected_till() external { + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + + Duration newProtectionDuration = protectionDuration; // the new value is the same as previous one + Duration newEmergencyModeDuration = Durations.from(200 seconds); + + vm.expectEmit(); + emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x4)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); -// import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; - -// import {UnitTest, Duration, Durations, Timestamp, Timestamps} from "test/utils/unit-test.sol"; - -// contract EmergencyProtectionUnitTests is UnitTest { -// using EmergencyProtection for EmergencyProtection.Context; - -// EmergencyProtection.Context internal _emergencyProtection; - -// function testFuzz_setup_emergency_protection( -// address activationCommittee, -// address executionCommittee, -// address emergencyGovernance, -// Duration protectionDuration, -// Duration duration -// ) external { -// vm.assume(protectionDuration > Durations.ZERO); -// vm.assume(duration > Durations.ZERO); -// // vm.assume(activationCommittee != address(0)); -// // vm.assume(executionCommittee != address(0)); - -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyActivationCommitteeSet(activationCommittee); -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyExecutionCommitteeSet(executionCommittee); -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyProtectionEndDateSet(protectionDuration.addTo(Timestamps.now())); -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyModeDurationSet(duration); - -// vm.recordLogs(); - -// _emergencyProtection.setEmergencyGovernance(emergencyGovernance); -// _emergencyProtection.setEmergencyProtectionEndDate(emergencyProtectionEndDate, maxEmergencyProtectionDuration); - -// _emergencyProtection.setup(activationCommittee, executionCommittee, protectionDuration, duration); - -// Vm.Log[] memory entries = vm.getRecordedLogs(); -// assertEq(entries.length, 4); - -// assertEq(_emergencyProtection.activationCommittee, activationCommittee); -// assertEq(_emergencyProtection.executionCommittee, executionCommittee); -// assertEq(_emergencyProtection.protectedTill, protectionDuration.addTo(Timestamps.now())); -// assertEq(_emergencyProtection.emergencyModeDuration, duration); -// assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); -// } - -// function test_setup_same_activation_committee() external { -// Duration protectionDuration = Durations.from(100 seconds); -// Duration emergencyModeDuration = Durations.from(100 seconds); -// address activationCommittee = makeAddr("activationCommittee"); - -// _emergencyProtection.setup(activationCommittee, address(0x2), protectionDuration, emergencyModeDuration); - -// Duration newProtectionDuration = Durations.from(200 seconds); -// Duration newEmergencyModeDuration = Durations.from(300 seconds); - -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x3)); -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(newProtectionDuration.addTo(Timestamps.now())); -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); - -// vm.recordLogs(); -// _emergencyProtection.setup(activationCommittee, address(0x3), newProtectionDuration, newEmergencyModeDuration); - -// Vm.Log[] memory entries = vm.getRecordedLogs(); -// assertEq(entries.length, 3); - -// assertEq(_emergencyProtection.activationCommittee, activationCommittee); -// assertEq(_emergencyProtection.executionCommittee, address(0x3)); -// assertEq(_emergencyProtection.protectedTill, newProtectionDuration.addTo(Timestamps.now())); -// assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); -// assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); -// } + vm.recordLogs(); + _setup(_emergencyGovernance, address(0x3), address(0x4), newProtectionDuration, newEmergencyModeDuration); -// function test_setup_same_execution_committee() external { -// Duration protectionDuration = Durations.from(100 seconds); -// Duration emergencyModeDuration = Durations.from(100 seconds); -// address executionCommittee = makeAddr("executionCommittee"); + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 3); -// _emergencyProtection.setup(address(0x1), executionCommittee, protectionDuration, emergencyModeDuration); + assertEq(_emergencyProtection.emergencyActivationCommittee, address(0x3)); + assertEq(_emergencyProtection.emergencyExecutionCommittee, address(0x4)); + assertEq(_emergencyProtection.emergencyProtectionEndsAfter, protectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); + } -// Duration newProtectionDuration = Durations.from(200 seconds); -// Duration newEmergencyModeDuration = Durations.from(300 seconds); + function test_setup_same_emergency_mode_duration() external { + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x2)); -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(newProtectionDuration.addTo(Timestamps.now())); -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); -// vm.recordLogs(); -// _emergencyProtection.setup(address(0x2), executionCommittee, newProtectionDuration, newEmergencyModeDuration); + Duration newProtectionDuration = Durations.from(200 seconds); + Duration newEmergencyModeDuration = emergencyModeDuration; // the new value is the same as previous one -// Vm.Log[] memory entries = vm.getRecordedLogs(); -// assertEq(entries.length, 3); + vm.expectEmit(); + emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x4)); + vm.expectEmit(); + emit EmergencyProtection.EmergencyProtectionEndDateSet(newProtectionDuration.addTo(Timestamps.now())); -// assertEq(_emergencyProtection.activationCommittee, address(0x2)); -// assertEq(_emergencyProtection.executionCommittee, executionCommittee); -// assertEq(_emergencyProtection.protectedTill, newProtectionDuration.addTo(Timestamps.now())); -// assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); -// assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); -// } - -// function test_setup_same_protected_till() external { -// Duration protectionDuration = Durations.from(100 seconds); -// Duration emergencyModeDuration = Durations.from(100 seconds); - -// _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); - -// Duration newProtectionDuration = protectionDuration; // the new value is the same as previous one -// Duration newEmergencyModeDuration = Durations.from(200 seconds); - -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x4)); -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); + vm.recordLogs(); + _setup(_emergencyGovernance, address(0x3), address(0x4), newProtectionDuration, newEmergencyModeDuration); -// vm.recordLogs(); -// _emergencyProtection.setup(address(0x3), address(0x4), newProtectionDuration, newEmergencyModeDuration); + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 3); -// Vm.Log[] memory entries = vm.getRecordedLogs(); -// assertEq(entries.length, 3); + assertEq(_emergencyProtection.emergencyActivationCommittee, address(0x3)); + assertEq(_emergencyProtection.emergencyExecutionCommittee, address(0x4)); + assertEq(_emergencyProtection.emergencyProtectionEndsAfter, newProtectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); + } -// assertEq(_emergencyProtection.activationCommittee, address(0x3)); -// assertEq(_emergencyProtection.executionCommittee, address(0x4)); -// assertEq(_emergencyProtection.protectedTill, protectionDuration.addTo(Timestamps.now())); -// assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); -// assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); -// } + function test_activate_emergency_mode() external { + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); -// function test_setup_same_emergency_mode_duration() external { -// Duration protectionDuration = Durations.from(100 seconds); -// Duration emergencyModeDuration = Durations.from(100 seconds); - -// _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); - -// Duration newProtectionDuration = Durations.from(200 seconds); -// Duration newEmergencyModeDuration = emergencyModeDuration; // the new value is the same as previous one + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x4)); -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(newProtectionDuration.addTo(Timestamps.now())); + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeActivated(); -// vm.recordLogs(); -// _emergencyProtection.setup(address(0x3), address(0x4), newProtectionDuration, newEmergencyModeDuration); + vm.recordLogs(); -// Vm.Log[] memory entries = vm.getRecordedLogs(); -// assertEq(entries.length, 3); + _emergencyProtection.activateEmergencyMode(); -// assertEq(_emergencyProtection.activationCommittee, address(0x3)); -// assertEq(_emergencyProtection.executionCommittee, address(0x4)); -// assertEq(_emergencyProtection.protectedTill, newProtectionDuration.addTo(Timestamps.now())); -// assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); -// assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); -// } + Vm.Log[] memory entries = vm.getRecordedLogs(); -// function test_activate_emergency_mode() external { -// Duration protectionDuration = Durations.from(100 seconds); -// Duration emergencyModeDuration = Durations.from(100 seconds); + assertEq(entries.length, 1); + assertEq(_emergencyProtection.emergencyModeEndsAfter, emergencyModeDuration.addTo(Timestamps.now())); + } -// _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + function test_cannot_activate_emergency_mode_if_protected_till_expired() external { + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyModeActivated(Timestamps.now()); + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); -// vm.recordLogs(); + _wait(protectionDuration.plusSeconds(1)); -// _emergencyProtection.activate(); + vm.expectRevert( + abi.encodeWithSelector( + EmergencyProtection.EmergencyProtectionExpired.selector, + _emergencyProtection.emergencyProtectionEndsAfter + ) + ); + _emergencyProtection.activateEmergencyMode(); + } -// Vm.Log[] memory entries = vm.getRecordedLogs(); + function testFuzz_deactivate_emergency_mode( + address activationCommittee, + address executionCommittee, + Duration protectionDuration, + Duration emergencyModeDuration + ) external { + vm.assume(activationCommittee != address(0)); + vm.assume(executionCommittee != address(0)); -// assertEq(entries.length, 1); -// assertEq(_emergencyProtection.emergencyModeEndsAfter, emergencyModeDuration.addTo(Timestamps.now())); -// } + _setup(_emergencyGovernance, activationCommittee, executionCommittee, protectionDuration, emergencyModeDuration); + _emergencyProtection.activateEmergencyMode(); -// function test_cannot_activate_emergency_mode_if_protected_till_expired() external { -// Duration protectionDuration = Durations.from(100 seconds); -// Duration emergencyModeDuration = Durations.from(100 seconds); + vm.expectEmit(); + emit EmergencyProtection.EmergencyModeDeactivated(); -// _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + vm.recordLogs(); -// _wait(protectionDuration.plusSeconds(1)); + _emergencyProtection.deactivateEmergencyMode(); -// vm.expectRevert( -// abi.encodeWithSelector( -// EmergencyProtection.EmergencyCommitteeExpired.selector, -// Timestamps.now(), -// _emergencyProtection.protectedTill -// ) -// ); -// _emergencyProtection.activate(); -// } + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 1); -// function testFuzz_deactivate_emergency_mode( -// address activationCommittee, -// address executionCommittee, -// Duration protectionDuration, -// Duration emergencyModeDuration -// ) external { -// vm.assume(activationCommittee != address(0)); -// vm.assume(executionCommittee != address(0)); + assertEq(_emergencyProtection.emergencyActivationCommittee, address(0)); + assertEq(_emergencyProtection.emergencyExecutionCommittee, address(0)); + assertEq(_emergencyProtection.emergencyProtectionEndsAfter, Timestamps.ZERO); + assertEq(_emergencyProtection.emergencyModeDuration, Durations.ZERO); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); + } -// _emergencyProtection.setup(activationCommittee, executionCommittee, protectionDuration, emergencyModeDuration); -// _emergencyProtection.activate(); + function test_is_emergency_mode_activated() external { + assertEq(_emergencyProtection.isEmergencyModeActive(), false); -// vm.expectEmit(); -// emit EmergencyProtection.EmergencyModeDeactivated(Timestamps.now()); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); -// vm.recordLogs(); + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); -// _emergencyProtection.deactivate(); + assertEq(_emergencyProtection.isEmergencyModeActive(), false); -// Vm.Log[] memory entries = vm.getRecordedLogs(); -// assertEq(entries.length, 1); + _emergencyProtection.activateEmergencyMode(); -// assertEq(_emergencyProtection.activationCommittee, address(0)); -// assertEq(_emergencyProtection.executionCommittee, address(0)); -// assertEq(_emergencyProtection.protectedTill, Timestamps.ZERO); -// assertEq(_emergencyProtection.emergencyModeDuration, Durations.ZERO); -// assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); -// } + assertEq(_emergencyProtection.isEmergencyModeActive(), true); -// // function test_get_emergency_state() external { -// // EmergencyState memory state = _emergencyProtection.getEmergencyState(); + _emergencyProtection.deactivateEmergencyMode(); -// // assertEq(state.activationCommittee, address(0)); -// // assertEq(state.executionCommittee, address(0)); -// // assertEq(state.protectedTill, Timestamps.ZERO); -// // assertEq(state.emergencyModeDuration, Durations.ZERO); -// // assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); -// // assertEq(state.isEmergencyModeActivated, false); + assertEq(_emergencyProtection.isEmergencyModeActive(), false); + } -// // Duration protectionDuration = Durations.from(100 seconds); -// // Duration emergencyModeDuration = Durations.from(200 seconds); + function test_is_emergency_mode_passed() external { + assertEq(_emergencyProtection.isEmergencyModeDurationPassed(), false); -// // _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(200 seconds); -// // state = _emergencyProtection.getEmergencyState(); + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); -// // assertEq(state.activationCommittee, address(0x1)); -// // assertEq(state.executionCommittee, address(0x2)); -// // assertEq(state.protectedTill, protectionDuration.addTo(Timestamps.now())); -// // assertEq(state.emergencyModeDuration, emergencyModeDuration); -// // assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); -// // assertEq(state.isEmergencyModeActivated, false); + assertEq(_emergencyProtection.isEmergencyModeDurationPassed(), false); -// // _emergencyProtection.activate(); + _emergencyProtection.activateEmergencyMode(); -// // state = _emergencyProtection.getEmergencyState(); + assertEq(_emergencyProtection.isEmergencyModeDurationPassed(), false); -// // assertEq(state.activationCommittee, address(0x1)); -// // assertEq(state.executionCommittee, address(0x2)); -// // assertEq(state.protectedTill, protectionDuration.addTo(Timestamps.now())); -// // assertEq(state.emergencyModeDuration, emergencyModeDuration); -// // assertEq(state.emergencyModeEndsAfter, emergencyModeDuration.addTo(Timestamps.now())); -// // assertEq(state.isEmergencyModeActivated, true); + _wait(emergencyModeDuration.plusSeconds(1)); -// // _emergencyProtection.deactivate(); + assertEq(_emergencyProtection.isEmergencyModeDurationPassed(), true); -// // state = _emergencyProtection.getEmergencyState(); + _emergencyProtection.deactivateEmergencyMode(); -// // assertEq(state.activationCommittee, address(0)); -// // assertEq(state.executionCommittee, address(0)); -// // assertEq(state.protectedTill, Timestamps.ZERO); -// // assertEq(state.emergencyModeDuration, Durations.ZERO); -// // assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); -// // assertEq(state.isEmergencyModeActivated, false); -// // } + assertEq(_emergencyProtection.isEmergencyModeDurationPassed(), false); + } -// function test_is_emergency_mode_activated() external { -// assertEq(_emergencyProtection.isEmergencyModeActivated(), false); + function test_is_emergency_protection_enabled() external { + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(200 seconds); -// Duration protectionDuration = Durations.from(100 seconds); -// Duration emergencyModeDuration = Durations.from(100 seconds); + assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), false); -// _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); -// assertEq(_emergencyProtection.isEmergencyModeActivated(), false); + assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); -// _emergencyProtection.activate(); + EmergencyProtection.Context memory emergencyState = _emergencyProtection; -// assertEq(_emergencyProtection.isEmergencyModeActivated(), true); + _wait(Durations.between(emergencyState.emergencyProtectionEndsAfter, Timestamps.now())); -// _emergencyProtection.deactivate(); + // _wait(emergencyState.emergencyProtectionEndsAfter.absDiff(Timestamps.now())); -// assertEq(_emergencyProtection.isEmergencyModeActivated(), false); -// } + EmergencyProtection.activateEmergencyMode(_emergencyProtection); -// function test_is_emergency_mode_passed() external { -// assertEq(_emergencyProtection.isEmergencyModePassed(), false); + _wait(emergencyModeDuration); -// Duration protectionDuration = Durations.from(100 seconds); -// Duration emergencyModeDuration = Durations.from(200 seconds); + assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); -// _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + _wait(protectionDuration); -// assertEq(_emergencyProtection.isEmergencyModePassed(), false); + assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); -// _emergencyProtection.activate(); + EmergencyProtection.deactivateEmergencyMode(_emergencyProtection); -// assertEq(_emergencyProtection.isEmergencyModePassed(), false); + assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), false); + } -// _wait(emergencyModeDuration.plusSeconds(1)); + function testFuzz_check_activation_committee(address committee, address stranger) external { + vm.assume(committee != address(0)); + vm.assume(stranger != address(0) && stranger != committee); -// assertEq(_emergencyProtection.isEmergencyModePassed(), true); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyActivationCommittee.selector, [stranger]) + ); + _emergencyProtection.checkEmergencyActivationCommittee(stranger); + _emergencyProtection.checkEmergencyActivationCommittee(address(0)); -// _emergencyProtection.deactivate(); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); -// assertEq(_emergencyProtection.isEmergencyModePassed(), false); -// } + _setup(_emergencyGovernance, committee, address(0x2), protectionDuration, emergencyModeDuration); -// function test_is_emergency_protection_enabled() external { -// Duration protectionDuration = Durations.from(100 seconds); -// Duration emergencyModeDuration = Durations.from(200 seconds); + _emergencyProtection.checkEmergencyActivationCommittee(committee); -// assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), false); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyActivationCommittee.selector, [stranger]) + ); + _emergencyProtection.checkEmergencyActivationCommittee(stranger); + } -// _emergencyProtection.setup( -// address(0x1), address(0x2), protectionDuration.addTo(Timestamps.now()), emergencyModeDuration -// ); + function testFuzz_check_execution_committee(address committee, address stranger) external { + vm.assume(committee != address(0)); + vm.assume(stranger != address(0) && stranger != committee); -// assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyExecutionCommittee.selector, [stranger]) + ); + _emergencyProtection.checkEmergencyExecutionCommittee(stranger); + _emergencyProtection.checkEmergencyExecutionCommittee(address(0)); -// EmergencyProtection.Context memory emergencyState = _emergencyProtection.getEmergencyState(); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); -// _wait(Durations.between(emergencyState.emergencyProtectionEndsAfter, Timestamps.now())); + _setup(_emergencyGovernance, address(0x1), committee, protectionDuration, emergencyModeDuration); -// // _wait(emergencyState.protectedTill.absDiff(Timestamps.now())); + _emergencyProtection.checkEmergencyExecutionCommittee(committee); -// EmergencyProtection.activate(_emergencyProtection); + vm.expectRevert( + abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyExecutionCommittee.selector, [stranger]) + ); + _emergencyProtection.checkEmergencyExecutionCommittee(stranger); + } -// _wait(emergencyModeDuration); + function test_check_emergency_mode_active() external { + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, [true])); + _emergencyProtection.checkEmergencyMode(true); + _emergencyProtection.checkEmergencyMode(false); -// assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); -// _wait(protectionDuration); + _setup(_emergencyGovernance, address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + _emergencyProtection.activateEmergencyMode(); -// assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); + _emergencyProtection.checkEmergencyMode(true); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeState.selector, [true])); + } -// EmergencyProtection.deactivate(_emergencyProtection); - -// assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), false); -// } - -// function testFuzz_check_activation_committee(address committee, address stranger) external { -// vm.assume(committee != address(0)); -// vm.assume(stranger != address(0) && stranger != committee); - -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyActivator.selector, [stranger])); -// _emergencyProtection.checkActivationCommittee(stranger); -// _emergencyProtection.checkActivationCommittee(address(0)); - -// Duration protectionDuration = Durations.from(100 seconds); -// Duration emergencyModeDuration = Durations.from(100 seconds); - -// _emergencyProtection.setup(committee, address(0x2), protectionDuration, emergencyModeDuration); - -// _emergencyProtection.checkActivationCommittee(committee); - -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyActivator.selector, [stranger])); -// _emergencyProtection.checkActivationCommittee(stranger); -// } - -// function testFuzz_check_execution_committee(address committee, address stranger) external { -// vm.assume(committee != address(0)); -// vm.assume(stranger != address(0) && stranger != committee); - -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, [stranger])); -// _emergencyProtection.checkExecutionCommittee(stranger); -// _emergencyProtection.checkExecutionCommittee(address(0)); - -// Duration protectionDuration = Durations.from(100 seconds); -// Duration emergencyModeDuration = Durations.from(100 seconds); - -// _emergencyProtection.setup(address(0x1), committee, protectionDuration, emergencyModeDuration); - -// _emergencyProtection.checkExecutionCommittee(committee); - -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.NotEmergencyEnactor.selector, [stranger])); -// _emergencyProtection.checkExecutionCommittee(stranger); -// } - -// function test_check_emergency_mode_active() external { -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeStatus.selector, [false, true])); -// _emergencyProtection.checkEmergencyModeStatus(true); -// _emergencyProtection.checkEmergencyModeStatus(false); - -// Duration protectionDuration = Durations.from(100 seconds); -// Duration emergencyModeDuration = Durations.from(100 seconds); - -// _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); -// _emergencyProtection.activate(); - -// _emergencyProtection.checkEmergencyModeStatus(true); -// vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeStatus.selector, [true, false])); -// } -// } + function _setup( + address newEmergencyGovernance, + address newEmergencyActivationCommittee, + address newEmergencyExecutionCommittee, + Duration protectionDuration, + Duration emergencyModeDuration + ) internal { + _emergencyProtection.setEmergencyGovernance(newEmergencyGovernance); + _emergencyProtection.setEmergencyActivationCommittee(newEmergencyActivationCommittee); + _emergencyProtection.setEmergencyExecutionCommittee(newEmergencyExecutionCommittee); + _emergencyProtection.setEmergencyProtectionEndDate(protectionDuration.addTo(Timestamps.now()), Durations.MAX); + _emergencyProtection.setEmergencyModeDuration(emergencyModeDuration, Durations.MAX); + } +} diff --git a/test/unit/libraries/ExecutableProposals.t.sol b/test/unit/libraries/ExecutableProposals.t.sol index c61e4053..ba4b2832 100644 --- a/test/unit/libraries/ExecutableProposals.t.sol +++ b/test/unit/libraries/ExecutableProposals.t.sol @@ -3,13 +3,16 @@ pragma solidity 0.8.26; import {Vm} from "forge-std/Test.sol"; +import {Duration, Durations} from "contracts/types/Duration.sol"; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; + import {Executor} from "contracts/Executor.sol"; import { ExecutableProposals, ExternalCall, Status as ProposalStatus } from "contracts/libraries/ExecutableProposals.sol"; -import {TargetMock} from "test/utils/utils.sol"; -import {UnitTest, Timestamps, Timestamp, Durations, Duration} from "test/utils/unit-test.sol"; +import {TargetMock} from "test/utils/target-mock.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; contract ExecutableProposalsUnitTests is UnitTest { using ExecutableProposals for ExecutableProposals.State; @@ -33,7 +36,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_submit_proposal() external { uint256 proposalsCount = _proposals.getProposalsCount(); - ExternalCall[] memory calls = _getTargetRegularStaffCalls(address(_targetMock)); + ExternalCall[] memory calls = _getMockTargetRegularStaffCalls(address(_targetMock)); uint256 expectedProposalId = proposalsCount + PROPOSAL_ID_OFFSET; @@ -68,7 +71,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function testFuzz_schedule_proposal(Duration delay) external { vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 expectedProposalId = 1; ExecutableProposals.Proposal memory proposal = _proposals.proposals[expectedProposalId]; @@ -100,7 +103,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cannot_schedule_proposal_twice() external { - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = 1; _proposals.schedule(proposalId, Durations.ZERO); @@ -111,7 +114,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function testFuzz_cannot_schedule_proposal_before_delay_passed(Duration delay) external { vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); _wait(delay.minusSeconds(1 seconds)); @@ -122,7 +125,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cannot_schedule_cancelled_proposal() external { - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); _proposals.cancelAll(); uint256 proposalId = _proposals.getProposalsCount(); @@ -134,7 +137,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function testFuzz_execute_proposal(Duration delay) external { vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); @@ -170,7 +173,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cannot_execute_unscheduled_proposal() external { - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); @@ -178,7 +181,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cannot_execute_twice() external { - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); _proposals.execute(proposalId, Durations.ZERO); @@ -188,7 +191,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cannot_execute_cancelled_proposal() external { - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); _proposals.cancelAll(); @@ -199,7 +202,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function testFuzz_cannot_execute_before_delay_passed(Duration delay) external { vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); @@ -210,8 +213,8 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cancel_all_proposals() external { - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalsCount = _proposals.getProposalsCount(); @@ -226,7 +229,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 = _getTargetRegularStaffCalls(address(_targetMock)); + ExternalCall[] memory expectedCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); _proposals.submit(address(_executor), expectedCalls); uint256 proposalId = _proposals.getProposalsCount(); @@ -289,7 +292,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_get_cancelled_proposal() external { - ExternalCall[] memory expectedCalls = _getTargetRegularStaffCalls(address(_targetMock)); + ExternalCall[] memory expectedCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); _proposals.submit(address(_executor), expectedCalls); uint256 proposalId = _proposals.getProposalsCount(); @@ -342,16 +345,16 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_count_proposals() external { assertEq(_proposals.getProposalsCount(), 0); - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.getProposalsCount(), 1); - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.getProposalsCount(), 2); _proposals.schedule(1, Durations.ZERO); assertEq(_proposals.getProposalsCount(), 2); - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.getProposalsCount(), 3); _proposals.schedule(2, Durations.ZERO); @@ -360,7 +363,7 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.execute(1, Durations.ZERO); assertEq(_proposals.getProposalsCount(), 3); - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.getProposalsCount(), 4); _proposals.cancelAll(); @@ -369,7 +372,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_can_execute_proposal() external { Duration delay = Durations.from(100 seconds); - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); assert(!_proposals.canExecute(proposalId, Durations.ZERO)); @@ -388,7 +391,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_can_not_execute_cancelled_proposal() external { - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); @@ -400,7 +403,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_can_schedule_proposal() external { Duration delay = Durations.from(100 seconds); - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); assert(!_proposals.canSchedule(proposalId, delay)); @@ -415,7 +418,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_can_not_schedule_cancelled_proposal() external { - _proposals.submit(address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); assert(_proposals.canSchedule(proposalId, Durations.ZERO)); diff --git a/test/unit/mocks/TimelockMock.sol b/test/unit/mocks/TimelockMock.sol index 68e85296..9ed3c8da 100644 --- a/test/unit/mocks/TimelockMock.sol +++ b/test/unit/mocks/TimelockMock.sol @@ -8,6 +8,12 @@ import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; contract TimelockMock is ITimelock { uint8 public constant OFFSET = 1; + address internal immutable _ADMIN_EXECUTOR; + + constructor(address adminExecutor) { + _ADMIN_EXECUTOR = adminExecutor; + } + mapping(uint256 => bool) public canScheduleProposal; uint256[] public submittedProposals; @@ -84,6 +90,6 @@ contract TimelockMock is ITimelock { } function getAdminExecutor() external view returns (address) { - revert("Not Implemented"); + return _ADMIN_EXECUTOR; } } diff --git a/test/utils/SetupDeployment.sol b/test/utils/SetupDeployment.sol new file mode 100644 index 00000000..3377acb5 --- /dev/null +++ b/test/utils/SetupDeployment.sol @@ -0,0 +1,408 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Test} from "forge-std/Test.sol"; + +// --- +// Types +// --- + +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 +// --- +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 {Escrow} from "contracts/Escrow.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"; +// --- +// Util Libraries +// --- + +import {Random} from "./random.sol"; +import {LidoUtils} from "./lido-utils.sol"; + +// --- +// Lido Addresses +// --- + +abstract contract SetupDeployment is Test { + using Random for Random.Context; + // --- + // Helpers + // --- + + Random.Context internal _random; + LidoUtils.Context internal _lido; + + // --- + // Emergency Protected Timelock Deployment Parameters + // --- + + Duration internal immutable _AFTER_SUBMIT_DELAY = Durations.from(3 days); + Duration internal immutable _MAX_AFTER_SUBMIT_DELAY = Durations.from(45 days); + + Duration internal immutable _AFTER_SCHEDULE_DELAY = Durations.from(3 days); + Duration internal immutable _MAX_AFTER_SCHEDULE_DELAY = Durations.from(45 days); + + Duration internal immutable _EMERGENCY_MODE_DURATION = Durations.from(180 days); + Duration internal immutable _MAX_EMERGENCY_MODE_DURATION = Durations.from(365 days); + + Duration internal immutable _EMERGENCY_PROTECTION_DURATION = Durations.from(90 days); + Duration internal immutable _MAX_EMERGENCY_PROTECTION_DURATION = Durations.from(365 days); + + uint256 internal immutable _EMERGENCY_ACTIVATION_COMMITTEE_QUORUM = 3; + uint256 internal immutable _EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS_COUNT = 5; + + uint256 internal immutable _EMERGENCY_EXECUTION_COMMITTEE_QUORUM = 5; + uint256 internal immutable _EMERGENCY_EXECUTION_COMMITTEE_MEMBERS_COUNT = 8; + + // --- + // Dual Governance Deployment Parameters + // --- + uint256 internal immutable TIEBREAKER_CORE_QUORUM = 1; + Duration internal immutable TIEBREAKER_EXECUTION_DELAY = Durations.from(30 days); + + uint256 internal immutable TIEBREAKER_SUB_COMMITTEES_COUNT = 2; + uint256 internal immutable TIEBREAKER_SUB_COMMITTEE_MEMBERS_COUNT = 5; + uint256 internal immutable TIEBREAKER_SUB_COMMITTEE_QUORUM = 5; + + Duration internal immutable MIN_TIEBREAKER_ACTIVATION_TIMEOUT = Durations.from(90 days); + Duration internal immutable TIEBREAKER_ACTIVATION_TIMEOUT = Durations.from(365 days); + Duration internal immutable MAX_TIEBREAKER_ACTIVATION_TIMEOUT = Durations.from(730 days); + uint256 internal immutable MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = 255; + + // + + // --- + // 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; + + // --- + // Timelocked Governance Contracts + // --- + TimelockedGovernance internal _timelockedGovernance; + + // --- + // Target Mock Helper Contract + // --- + + TargetMock internal _targetMock; + + // --- + // Constructor + // --- + + constructor(LidoUtils.Context memory lido, Random.Context memory random) { + _lido = lido; + _random = random; + _targetMock = new TargetMock(); + } + + // --- + // Whole Setup Deployments + // --- + + function _deployTimelockedGovernanceSetup(bool isEmergencyProtectionEnabled) internal { + _deployEmergencyProtectedTimelockContracts(isEmergencyProtectionEnabled); + _timelockedGovernance = _deployTimelockedGovernance({governance: address(_lido.voting), timelock: _timelock}); + _finalizeEmergencyProtectedTimelockDeploy(_timelockedGovernance); + } + + function _deployDualGovernanceSetup(bool isEmergencyProtectionEnabled) internal { + _deployEmergencyProtectedTimelockContracts(isEmergencyProtectionEnabled); + _resealManager = _deployResealManager(_timelock); + _dualGovernanceConfigProvider = _deployDualGovernanceConfigProvider(); + _dualGovernance = _deployDualGovernance({ + timelock: _timelock, + resealManager: _resealManager, + configProvider: _dualGovernanceConfigProvider + }); + + _tiebreakerCoreCommittee = _deployEmptyTiebreakerCoreCommittee({ + owner: address(this), // temporary set owner to deployer, to add sub committees manually + dualGovernance: _dualGovernance, + timelock: TIEBREAKER_EXECUTION_DELAY.toSeconds() + }); + + 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 + }) + ); + _tiebreakerCoreCommittee.addMember(address(_tiebreakerSubCommittees[i]), i + 1); + } + + _tiebreakerCoreCommittee.transferOwnership(address(_adminExecutor)); + + // --- + // 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)) + ); + + _finalizeEmergencyProtectedTimelockDeploy(_dualGovernance); + + // --- + // Grant Reseal Manager Roles + // --- + vm.startPrank(address(_lido.agent)); + _lido.withdrawalQueue.grantRole( + 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(_resealManager) + ); + _lido.withdrawalQueue.grantRole( + 0x2fc10cc8ae19568712f7a176fb4978616a610650813c9d05326c34abb62749c7, address(_resealManager) + ); + vm.stopPrank(); + } + + // --- + // Emergency Protected Timelock Deployment + // --- + + 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.setupEmergencyProtection, + ( + address(_emergencyGovernance), + address(_emergencyActivationCommittee), + address(_emergencyExecutionCommittee), + _EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()), + _EMERGENCY_MODE_DURATION + ) + ) + ); + } + } + + function _finalizeEmergencyProtectedTimelockDeploy(IGovernance governance) internal { + _adminExecutor.execute( + address(_timelock), 0, abi.encodeCall(_timelock.setDelays, (_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 _deployTimelockedGovernance( + address governance, + ITimelock timelock + ) internal returns (TimelockedGovernance) { + return new TimelockedGovernance(governance, timelock); + } + + // --- + // Dual Governance Deployment + // --- + + 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), + dynamicTimelockMinDuration: Durations.from(3 days), + dynamicTimelockMaxDuration: Durations.from(30 days), + // + vetoSignallingMinActiveDuration: Durations.from(5 hours), + vetoSignallingDeactivationMaxDuration: Durations.from(5 days), + vetoCooldownDuration: Durations.from(4 days), + // + rageQuitExtensionDelay: Durations.from(7 days), + rageQuitEthWithdrawalsMinTimelock: Durations.from(60 days), + rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: 2, + rageQuitEthWithdrawalsTimelockGrowthCoeffs: [uint256(0), 0, 0] + }) + ); + } + + function _deployResealManager(ITimelock timelock) internal returns (ResealManager) { + return new ResealManager(timelock); + } + + function _deployDualGovernance( + ITimelock timelock, + IResealManager resealManager, + IDualGovernanceConfigProvider configProvider + ) internal returns (DualGovernance) { + return new DualGovernance({ + timelock: timelock, + resealManager: resealManager, + configProvider: configProvider, + dualGovernanceSanityCheckParams: DualGovernance.SanityCheckParams({ + minTiebreakerActivationTimeout: MIN_TIEBREAKER_ACTIVATION_TIMEOUT, + maxTiebreakerActivationTimeout: MAX_TIEBREAKER_ACTIVATION_TIMEOUT, + maxSealableWithdrawalBlockersCount: MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT + }), + escrowSanityCheckParams: Escrow.SanityCheckParams({minWithdrawalsBatchSize: 4}), + escrowProtocolDependencies: Escrow.ProtocolDependencies({ + stETH: _lido.stETH, + wstETH: _lido.wstETH, + withdrawalQueue: _lido.withdrawalQueue + }) + }); + } + + function _deployEmptyTiebreakerCoreCommittee( + address owner, + IDualGovernance dualGovernance, + uint256 timelock + ) internal returns (TiebreakerCore) { + return new TiebreakerCore({ + owner: owner, + executionQuorum: TIEBREAKER_CORE_QUORUM, + committeeMembers: new address[](0), + dualGovernance: address(dualGovernance), + timelock: timelock + }); + } + + function _deployTiebreakerSubCommittee( + address owner, + uint256 quorum, + address[] memory members, + TiebreakerCore tiebreakerCore + ) internal returns (TiebreakerSubCommittee) { + return new TiebreakerSubCommittee({ + owner: owner, + executionQuorum: quorum, + committeeMembers: members, + tiebreakerCore: address(tiebreakerCore) + }); + } + + // --- + // Helper methods + // --- + + function _generateRandomAddresses(uint256 count) internal returns (address[] memory addresses) { + addresses = new address[](count); + for (uint256 i = 0; i < count; ++i) { + addresses[i] = _random.nextAddress(); + } + } +} diff --git a/test/utils/deployment.sol b/test/utils/deployment.sol deleted file mode 100644 index b951d8ce..00000000 --- a/test/utils/deployment.sol +++ /dev/null @@ -1,240 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -uint256 constant PERCENT = 10 ** 16; - -// --- -// Types -// --- -import {Durations} from "contracts/types/Duration.sol"; - -// --- -// Interfaces -// --- -import {IStETH} from "contracts/interfaces/IStETH.sol"; -import {IWstETH} from "contracts/interfaces/IWstETH.sol"; -import {IResealManager} from "contracts/interfaces/IResealManager.sol"; -import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; - -import {ITimelock} from "contracts/interfaces/ITimelock.sol"; - -// --- -// Core Contracts -// --- - -import {Escrow} from "contracts/Escrow.sol"; -import {Executor} from "contracts/Executor.sol"; -import {ResealManager} from "contracts/ResealManager.sol"; -import {DualGovernance} from "contracts/DualGovernance.sol"; -import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; -import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; - -// --- -// Committees -// --- - -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"; - -// --- -// Configuration -// --- - -import { - DualGovernanceConfig, - IDualGovernanceConfigProvider, - ImmutableDualGovernanceConfigProvider -} from "contracts/DualGovernanceConfigProvider.sol"; - -library Deployment { - // --- - // Executor - // --- - function deployExecutor(address owner) internal returns (Executor executor) { - executor = new Executor(owner); - } - - // --- - // Emergency Protected Timelock - // --- - - function deployEmergencyProtectedTimelock( - EmergencyProtectedTimelock.SanityCheckParams memory sanityCheckParams, - Executor adminExecutor - ) internal returns (EmergencyProtectedTimelock timelock) { - timelock = new EmergencyProtectedTimelock(sanityCheckParams, address(adminExecutor)); - } - - // --- - // Dual Governance Configuration - // --- - - function deployDualGovernanceConfigProvider() - internal - returns (ImmutableDualGovernanceConfigProvider dualGovernanceConfigProvider) - { - dualGovernanceConfigProvider = new ImmutableDualGovernanceConfigProvider( - DualGovernanceConfig.Context({ - firstSealRageQuitSupport: 3 * PERCENT, - secondSealRageQuitSupport: 15 * PERCENT, - // - minAssetsLockDuration: Durations.from(5 hours), - dynamicTimelockMinDuration: Durations.from(3 days), - dynamicTimelockMaxDuration: Durations.from(30 days), - // - vetoSignallingMinActiveDuration: Durations.from(5 hours), - vetoSignallingDeactivationMaxDuration: Durations.from(5 days), - vetoCooldownDuration: Durations.from(4 days), - // - rageQuitExtensionDelay: Durations.from(7 days), - rageQuitEthWithdrawalsMinTimelock: Durations.from(60 days), - rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber: 2, - rageQuitEthWithdrawalsTimelockGrowthCoeffs: [uint256(0), 0, 0] - }) - ); - } - - // --- - // Dual Governance - // --- - - function deployDualGovernance( - ITimelock timelock, - IResealManager resealManager, - IDualGovernanceConfigProvider configProvider, - DualGovernance.SanityCheckParams memory dualGovernanceSanityCheckParams, - Escrow.SanityCheckParams memory escrowSanityCheckParams, - Escrow.ProtocolDependencies memory escrowProtocolDependencies - ) internal returns (DualGovernance dualGovernance) { - dualGovernance = new DualGovernance( - timelock, - resealManager, - configProvider, - dualGovernanceSanityCheckParams, - escrowSanityCheckParams, - escrowProtocolDependencies - ); - } - - // --- - // Reseal Manager - // --- - function deployResealManager(ITimelock timelock) internal returns (ResealManager resealManager) { - resealManager = new ResealManager(timelock); - } - - // --- - // Timelocked Governance - // --- - function deployTimelockedGovernance( - address governance, - ITimelock timelock - ) internal returns (TimelockedGovernance) { - return new TimelockedGovernance(governance, timelock); - } - - // --- - // Committees - // --- - - function deployEmergencyActivationCommittee( - address adminExecutor, - address emergencyProtectedTimelock, - address[] memory committeeMembers, - uint256 executionQuorum - ) internal returns (EmergencyActivationCommittee) { - return new EmergencyActivationCommittee({ - owner: adminExecutor, - committeeMembers: committeeMembers, - executionQuorum: executionQuorum, - emergencyProtectedTimelock: emergencyProtectedTimelock - }); - } - - function deployEmergencyExecutionCommittee( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address emergencyProtectedTimelock - ) internal returns (EmergencyExecutionCommittee) { - return new EmergencyExecutionCommittee(owner, committeeMembers, executionQuorum, emergencyProtectedTimelock); - } - - function deployTiebreakerCoreCommittee( - address adminExecutor, - address dualGovernance - ) internal returns (TiebreakerCore tiebreakerCore) { - tiebreakerCore = new TiebreakerCore({ - owner: adminExecutor, - dualGovernance: dualGovernance, - committeeMembers: new address[](0), - executionQuorum: 1, - timelock: 0 - }); - } - - function deployTiebreakerSubCommittee( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address tiebreakerCore - ) internal returns (TiebreakerSubCommittee) { - return new TiebreakerSubCommittee(owner, committeeMembers, executionQuorum, tiebreakerCore); - } - - // --- - // Dual Governance Setup Deployment - // --- - - struct DualGovernanceSetup { - Executor adminExecutor; - ResealManager resealManager; - EmergencyProtectedTimelock timelock; - TimelockedGovernance emergencyGovernance; - ImmutableDualGovernanceConfigProvider dualGovernanceConfigProvider; - DualGovernance dualGovernance; - } - - function deployDualGovernanceContracts( - IStETH stETH, - IWstETH wstETH, - IWithdrawalQueue withdrawalQueue, - address emergencyGovernance - ) internal returns (DualGovernanceSetup memory setup) { - address tmpExecutorOwner = address(this); - setup.adminExecutor = deployExecutor({owner: tmpExecutorOwner}); - - setup.timelock = deployEmergencyProtectedTimelock({ - adminExecutor: setup.adminExecutor, - sanityCheckParams: EmergencyProtectedTimelock.SanityCheckParams({ - maxAfterSubmitDelay: Durations.from(45 days), - maxAfterScheduleDelay: Durations.from(45 days), - maxEmergencyModeDuration: Durations.from(365 days), - maxEmergencyProtectionDuration: Durations.from(180 days) - }) - }); - - setup.resealManager = deployResealManager(setup.timelock); - setup.dualGovernanceConfigProvider = deployDualGovernanceConfigProvider(); - setup.emergencyGovernance = deployTimelockedGovernance(emergencyGovernance, setup.timelock); - - setup.dualGovernance = deployDualGovernance({ - timelock: setup.timelock, - resealManager: setup.resealManager, - configProvider: setup.dualGovernanceConfigProvider, - dualGovernanceSanityCheckParams: DualGovernance.SanityCheckParams({ - minTiebreakerActivationTimeout: Durations.from(180 days), - maxTiebreakerActivationTimeout: Durations.from(365 days), - maxSealableWithdrawalBlockersCount: 255 - }), - escrowSanityCheckParams: Escrow.SanityCheckParams({minWithdrawalsBatchSize: 4}), - escrowProtocolDependencies: Escrow.ProtocolDependencies({ - stETH: stETH, - wstETH: wstETH, - withdrawalQueue: withdrawalQueue - }) - }); - } -} diff --git a/test/utils/evm-script-utils.sol b/test/utils/evm-script-utils.sol new file mode 100644 index 00000000..6cd91ba2 --- /dev/null +++ b/test/utils/evm-script-utils.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +library EvmScriptUtils { + struct EvmScriptCall { + address target; + bytes data; + } + + function encodeEvmCallScript(address target, bytes memory data) internal pure returns (bytes memory) { + EvmScriptCall[] memory calls = new EvmScriptCall[](1); + calls[0] = EvmScriptCall(target, data); + return encodeEvmCallScript(calls); + } + + function encodeEvmCallScript(EvmScriptCall[] memory calls) internal pure returns (bytes memory) { + bytes memory script = new bytes(4); + script[3] = 0x01; + + for (uint256 i = 0; i < calls.length; ++i) { + EvmScriptCall memory call = calls[i]; + script = bytes.concat(script, bytes20(call.target), bytes4(uint32(call.data.length)), call.data); + } + + return script; + } +} diff --git a/test/utils/interfaces/IAragonACL.sol b/test/utils/interfaces/IAragonACL.sol new file mode 100644 index 00000000..d22e8f0d --- /dev/null +++ b/test/utils/interfaces/IAragonACL.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +interface IAragonACL { + function getPermissionManager(address app, bytes32 role) external view returns (address); + function grantPermission(address grantee, address app, bytes32 role) external; + function hasPermission(address who, address app, bytes32 role) external view returns (bool); +} diff --git a/test/utils/interfaces/IAragonAgent.sol b/test/utils/interfaces/IAragonAgent.sol new file mode 100644 index 00000000..fbbf1f13 --- /dev/null +++ b/test/utils/interfaces/IAragonAgent.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IAragonForwarder} from "./IAragonForwarder.sol"; + +interface IAragonAgent is IAragonForwarder { + function RUN_SCRIPT_ROLE() external pure returns (bytes32); +} diff --git a/test/utils/interfaces/IAragonForwarder.sol b/test/utils/interfaces/IAragonForwarder.sol new file mode 100644 index 00000000..909840de --- /dev/null +++ b/test/utils/interfaces/IAragonForwarder.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +interface IAragonForwarder { + function forward(bytes memory evmScript) external; +} diff --git a/test/utils/interfaces.sol b/test/utils/interfaces/IAragonVoting.sol similarity index 52% rename from test/utils/interfaces.sol rename to test/utils/interfaces/IAragonVoting.sol index 40afc0a5..cafc3295 100644 --- a/test/utils/interfaces.sol +++ b/test/utils/interfaces/IAragonVoting.sol @@ -1,9 +1,6 @@ +// SPDX-License-Identifier: MIT pragma solidity 0.8.26; -interface IAragonAgent { - function RUN_SCRIPT_ROLE() external pure returns (bytes32); -} - interface IAragonVoting { function newVote( bytes calldata script, @@ -20,19 +17,3 @@ interface IAragonVoting { function voteTime() external view returns (uint64); function minAcceptQuorumPct() external view returns (uint64); } - -interface IAragonACL { - function getPermissionManager(address app, bytes32 role) external view returns (address); - function grantPermission(address grantee, address app, bytes32 role) external; - function hasPermission(address who, address app, bytes32 role) external view returns (bool); -} - -interface IAragonForwarder { - function forward(bytes memory evmScript) external; -} - -interface IDangerousContract { - function doRegularStaff(uint256 magic) external; - function doRugPool() external; - function doControversialStaff() external; -} diff --git a/test/utils/interfaces/IPotentiallyDangerousContract.sol b/test/utils/interfaces/IPotentiallyDangerousContract.sol new file mode 100644 index 00000000..62e5b2d5 --- /dev/null +++ b/test/utils/interfaces/IPotentiallyDangerousContract.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +interface IPotentiallyDangerousContract { + function doRegularStaff(uint256 magic) external; + function doRugPool() external; + function doControversialStaff() external; +} diff --git a/test/utils/interfaces/IStETH.sol b/test/utils/interfaces/IStETH.sol new file mode 100644 index 00000000..713854cd --- /dev/null +++ b/test/utils/interfaces/IStETH.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IStETH as IStETHBase} from "contracts/interfaces/IStETH.sol"; + +interface IStETH is IStETHBase { + function getTotalShares() external view returns (uint256); + function sharesOf(address account) external view returns (uint256); + + function removeStakingLimit() external; + function getCurrentStakeLimit() external view returns (uint256); + function getStakeLimitFullInfo() + external + view + returns ( + bool isStakingPaused, + bool isStakingLimitSet, + uint256 currentStakeLimit, + uint256 maxStakeLimit, + uint256 maxStakeLimitGrowthBlocks, + uint256 prevStakeLimit, + uint256 prevStakeBlockNumber + ); + function STAKING_CONTROL_ROLE() external view returns (bytes32); + + function submit(address referral) external payable returns (uint256); +} diff --git a/test/utils/interfaces/IWithdrawalQueue.sol b/test/utils/interfaces/IWithdrawalQueue.sol new file mode 100644 index 00000000..685951a7 --- /dev/null +++ b/test/utils/interfaces/IWithdrawalQueue.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IWithdrawalQueue as IWithdrawalQueueBase} from "contracts/interfaces/IWithdrawalQueue.sol"; + +interface IWithdrawalQueue is IWithdrawalQueueBase { + function getLastRequestId() external view returns (uint256); + function setApprovalForAll(address _operator, bool _approved) external; + function grantRole(bytes32 role, address account) external; + function pauseFor(uint256 duration) external; + function isPaused() external returns (bool); + function finalize(uint256 _lastRequestIdToBeFinalized, uint256 _maxShareRate) external payable; +} diff --git a/test/utils/interfaces/IWstETH.sol b/test/utils/interfaces/IWstETH.sol new file mode 100644 index 00000000..3684907f --- /dev/null +++ b/test/utils/interfaces/IWstETH.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IWstETH as IWstETHBase} from "contracts/interfaces/IWstETH.sol"; + +interface IWstETH is IWstETHBase { +/// @dev event though in the tests there is no need in additional methods of the WstETH token, +/// it's kept for consistency +} diff --git a/test/utils/lido-utils.sol b/test/utils/lido-utils.sol new file mode 100644 index 00000000..38ac97c0 --- /dev/null +++ b/test/utils/lido-utils.sol @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Vm} from "forge-std/Test.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; + +import {IStETH} from "./interfaces/IStETH.sol"; +import {IWstETH} from "./interfaces/IWstETH.sol"; +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 {IAragonForwarder} from "./interfaces/IAragonForwarder.sol"; + +import {EvmScriptUtils} from "./evm-script-utils.sol"; + +import { + ST_ETH, + WST_ETH, + WITHDRAWAL_QUEUE, + DAO_ACL, + LDO_TOKEN, + DAO_AGENT, + DAO_VOTING, + DAO_TOKEN_MANAGER +} from "./mainnet-addresses.sol"; + +uint256 constant ST_ETH_TRANSFERS_SHARE_LOSS_COMPENSATION = 8; // TODO: evaluate min enough value + +library LidoUtils { + struct Context { + // core + IStETH stETH; + IWstETH wstETH; + IWithdrawalQueue withdrawalQueue; + // aragon governance + IAragonACL acl; + IERC20 ldoToken; + IAragonAgent agent; + IAragonVoting voting; + IAragonForwarder tokenManager; + } + + Vm internal constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + address internal constant DEFAULT_LDO_WHALE = address(0x1D0_1D0_1D0_1D0_1d0_1D0_1D0_1D0_1D0_1d0_1d0_1d0_1D0_1); + + function mainnet() internal pure returns (Context memory ctx) { + ctx.stETH = IStETH(ST_ETH); + ctx.wstETH = IWstETH(WST_ETH); + ctx.withdrawalQueue = IWithdrawalQueue(WITHDRAWAL_QUEUE); + + ctx.acl = IAragonACL(DAO_ACL); + ctx.agent = IAragonAgent(DAO_AGENT); + ctx.voting = IAragonVoting(DAO_VOTING); + ctx.ldoToken = IERC20(LDO_TOKEN); + ctx.tokenManager = IAragonForwarder(DAO_TOKEN_MANAGER); + } + + function calcAmountFromPercentageOfTVL( + Context memory self, + PercentD16 percentage + ) internal view returns (uint256) { + uint256 totalSupply = self.stETH.totalSupply(); + uint256 amount = + totalSupply * PercentD16.unwrap(percentage) / PercentD16.unwrap(PercentsD16.fromBasisPoints(100_00)); + + /// @dev Below transformation helps to fix the rounding issue + PercentD16 resulting = PercentsD16.fromFraction({numerator: amount, denominator: totalSupply}); + return amount * PercentD16.unwrap(percentage) / PercentD16.unwrap(resulting); + } + + function calcSharesFromPercentageOfTVL( + Context memory self, + PercentD16 percentage + ) internal view returns (uint256) { + uint256 totalShares = self.stETH.getTotalShares(); + uint256 shares = + totalShares * PercentD16.unwrap(percentage) / PercentD16.unwrap(PercentsD16.fromBasisPoints(100_00)); + + /// @dev Below transformation helps to fix the rounding issue + PercentD16 resulting = PercentsD16.fromFraction({numerator: shares, denominator: totalShares}); + return shares * PercentD16.unwrap(percentage) / PercentD16.unwrap(resulting); + } + + function calcAmountToDepositFromPercentageOfTVL( + Context memory self, + PercentD16 percentage + ) internal view returns (uint256) { + uint256 totalSupply = self.stETH.totalSupply(); + /// @dev Calculate amount and shares using the following rule: + /// bal / (totalSupply + bal) = percentage => bal = totalSupply * percentage / (1 - percentage) + uint256 amount = totalSupply * PercentD16.unwrap(percentage) + / PercentD16.unwrap(PercentsD16.fromBasisPoints(100_00) - percentage); + + /// @dev Below transformation helps to fix the rounding issue + PercentD16 resulting = PercentsD16.fromFraction({numerator: amount, denominator: totalSupply + amount}); + return amount * PercentD16.unwrap(percentage) / PercentD16.unwrap(resulting); + } + + function calcSharesToDepositFromPercentageOfTVL( + Context memory self, + PercentD16 percentage + ) internal view returns (uint256) { + uint256 totalShares = self.stETH.getTotalShares(); + /// @dev Calculate amount and shares using the following rule: + /// bal / (totalShares + bal) = percentage => bal = totalShares * percentage / (1 - percentage) + uint256 shares = totalShares * PercentD16.unwrap(percentage) + / PercentD16.unwrap(PercentsD16.fromBasisPoints(100_00) - percentage); + + /// @dev Below transformation helps to fix the rounding issue + PercentD16 resulting = PercentsD16.fromFraction({numerator: shares, denominator: totalShares + shares}); + return shares * PercentD16.unwrap(percentage) / PercentD16.unwrap(resulting); + } + + function submitStETH( + Context memory self, + address account, + uint256 balance + ) internal returns (uint256 sharesMinted) { + vm.deal(account, balance + 0.1 ether); + + vm.prank(account); + sharesMinted = self.stETH.submit{value: balance}(address(0)); + } + + function submitWstETH( + Context memory self, + address account, + uint256 balance + ) internal returns (uint256 wstEthMinted) { + uint256 stEthAmount = self.wstETH.getStETHByWstETH(balance); + submitStETH(self, account, stEthAmount); + + vm.startPrank(account); + self.stETH.approve(address(self.wstETH), stEthAmount); + wstEthMinted = self.wstETH.wrap(stEthAmount); + vm.stopPrank(); + } + + function finalizeWithdrawalQueue(Context memory self) internal { + finalizeWithdrawalQueue(self, self.withdrawalQueue.getLastRequestId()); + } + + function finalizeWithdrawalQueue(Context memory self, uint256 id) internal { + vm.deal(address(self.withdrawalQueue), 10_000_000 ether); + uint256 finalizationShareRate = self.stETH.getPooledEthByShares(1e27) + 1e9; // TODO check finalization rate + vm.prank(address(self.stETH)); + self.withdrawalQueue.finalize(id, finalizationShareRate); + + bytes32 lockedEtherAmountSlot = 0x0e27eaa2e71c8572ab988fef0b54cd45bbd1740de1e22343fb6cda7536edc12f; // keccak256("lido.WithdrawalQueue.lockedEtherAmount"); + + vm.store(address(self.withdrawalQueue), lockedEtherAmountSlot, bytes32(address(self.withdrawalQueue).balance)); + } + + /// @param rebaseFactor - rebase factor with 10 ** 16 precision + /// 10 ** 18 => equal to no rebase + /// 10 ** 18 - 1 => equal to decrease equal to 10 ** -18 % + /// 10 ** 18 + 1 => equal to increase equal to 10 ** -18 % + function simulateRebase(Context memory self, PercentD16 rebaseFactor) internal { + bytes32 clBeaconBalanceSlot = keccak256("lido.Lido.beaconBalance"); + uint256 totalSupply = self.stETH.totalSupply(); + + uint256 oldClBalance = uint256(vm.load(address(self.stETH), clBeaconBalanceSlot)); + uint256 newClBalance = PercentD16.unwrap(rebaseFactor) * oldClBalance / 10 ** 18; + + vm.store(address(self.stETH), clBeaconBalanceSlot, bytes32(newClBalance)); + + // validate that total supply of the token updated expectedly + if (rebaseFactor > PercentsD16.fromBasisPoints(100_00)) { + uint256 clBalanceDelta = newClBalance - oldClBalance; + assert(self.stETH.totalSupply() == totalSupply + clBalanceDelta); + } else { + uint256 clBalanceDelta = oldClBalance - newClBalance; + assert(self.stETH.totalSupply() == totalSupply - clBalanceDelta); + } + } + + function removeStakingLimit(Context memory self) external { + bytes32 stakingLimitSlot = keccak256("lido.Lido.stakeLimit"); + uint256 stakingLimitEncodedData = uint256(vm.load(address(self.stETH), stakingLimitSlot)); + // See the self encoding here: https://github.com/lidofinance/lido-dao/blob/5fcedc6e9a9f3ec154e69cff47c2b9e25503a78a/contracts/0.4.24/lib/StakeLimitUtils.sol#L10 + // To remove staking limit, most significant 96 bits must be set to zero + stakingLimitEncodedData &= 2 ** 160 - 1; + vm.store(address(self.stETH), stakingLimitSlot, bytes32(stakingLimitEncodedData)); + assert(self.stETH.getCurrentStakeLimit() == type(uint256).max); + } + + // --- + // ACL + // --- + + function grantPermission(Context memory self, address app, bytes32 role, address grantee) internal { + if (!self.acl.hasPermission(grantee, app, role)) { + address manager = self.acl.getPermissionManager(app, role); + vm.prank(manager); + self.acl.grantPermission(grantee, app, role); + assert(self.acl.hasPermission(grantee, app, role)); + } + } + + // --- + // Aragon Governance + // --- + + function setupLDOWhale(Context memory self, address account) internal { + vm.startPrank(address(self.agent)); + self.ldoToken.transfer(account, self.ldoToken.balanceOf(address(self.agent))); + vm.stopPrank(); + + assert(self.ldoToken.balanceOf(account) >= self.voting.minAcceptQuorumPct()); + + // need to increase block number since MiniMe snapshotting relies on it + vm.roll(block.number + 1); + vm.warp(block.timestamp + 15); + } + + function supportVoteAndWaitTillDecided(Context memory self, uint256 voteId, address voter) internal { + supportVote(self, voteId, voter); + vm.warp(block.timestamp + self.voting.voteTime()); + } + + function supportVote(Context memory self, uint256 voteId, address voter) internal { + vote(self, voteId, voter, true); + } + + function vote(Context memory self, uint256 voteId, address voter, bool support) internal { + vm.prank(voter); + self.voting.vote(voteId, support, false); + } + + // Creates vote with given description and script, votes for it, and waits until it can be executed + function adoptVote( + Context memory self, + string memory description, + bytes memory script + ) internal returns (uint256 voteId) { + if (self.ldoToken.balanceOf(DEFAULT_LDO_WHALE) < self.voting.minAcceptQuorumPct()) { + setupLDOWhale(self, DEFAULT_LDO_WHALE); + } + bytes memory voteScript = EvmScriptUtils.encodeEvmCallScript( + address(self.voting), abi.encodeCall(self.voting.newVote, (script, description, false, false)) + ); + + voteId = self.voting.votesLength(); + + vm.prank(DEFAULT_LDO_WHALE); + self.tokenManager.forward(voteScript); + supportVoteAndWaitTillDecided(self, voteId, DEFAULT_LDO_WHALE); + } + + function executeVote(Context memory self, uint256 voteId) internal { + self.voting.executeVote(voteId); + } +} diff --git a/test/utils/mainnet-addresses.sol b/test/utils/mainnet-addresses.sol index b5db4b7f..8d9643f9 100644 --- a/test/utils/mainnet-addresses.sol +++ b/test/utils/mainnet-addresses.sol @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT pragma solidity 0.8.26; address constant DAO_ACL = 0x9895F0F17cc1d1891b6f18ee0b483B6f221b37Bb; diff --git a/test/utils/percents.sol b/test/utils/percents.sol deleted file mode 100644 index 43eff5cd..00000000 --- a/test/utils/percents.sol +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -error InvalidPercentsString(string value); - -struct Percents { - uint256 value; - uint256 precision; -} - -uint256 constant PRECISION = 16; - -function percents(uint256 value) pure returns (Percents memory result) { - result.value = value; - result.precision = PRECISION; -} - -function percents(string memory value) pure returns (Percents memory result) { - result = percents(value, PRECISION); -} - -function percents(string memory value, uint256 precision) pure returns (Percents memory result) { - uint256 integerPart; - uint256 fractionalPart; - uint256 fractionalPartLength; - - bytes memory bvalue = bytes(value); - uint256 length = bytes(value).length; - bytes1 dot = bytes1("."); - - bool isFractionalPart = false; - for (uint256 i = 0; i < length; ++i) { - if (bytes1(bvalue[i]) == dot) { - if (isFractionalPart) { - revert InvalidPercentsString(value); - } - isFractionalPart = true; - } else if (uint8(bvalue[i]) >= 48 && uint8(bvalue[i]) <= 57) { - if (isFractionalPart) { - fractionalPartLength += 1; - fractionalPart = 10 * fractionalPart + (uint8(bvalue[i]) - 48); - } else { - integerPart = 10 * integerPart + (uint8(bvalue[i]) - 48); - } - } else { - revert InvalidPercentsString(value); - } - } - result.precision = precision; - result.value = 10 ** precision * integerPart + 10 ** (precision - fractionalPartLength) * fractionalPart; -} diff --git a/test/utils/random.sol b/test/utils/random.sol new file mode 100644 index 00000000..a84e0bec --- /dev/null +++ b/test/utils/random.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: BSL-1.1 +pragma solidity 0.8.26; + +library Random { + struct Context { + bytes32 seed; + bytes32 value; + } + + function create(uint256 seed) internal pure returns (Context memory self) { + self.seed = bytes32(seed); + self.value = self.seed; + } + + function nextUint256(Context storage self) internal returns (uint256) { + return uint256(_nextValue(self)); + } + + /// @param maxValue - exclusive upper bound of the random + /// @return random uint256 in range [0, maxValue). When maxValue is 0, returns 0 + function nextUint256(Context storage self, uint256 maxValue) internal returns (uint256) { + if (maxValue == 0) { + return 0; + } + return nextUint256(self) % maxValue; + } + + /// @param minValue - inclusive lower bound + /// @param maxValue - exclusive upper bound of the random + function nextUint256(Context storage self, uint256 minValue, uint256 maxValue) internal returns (uint256) { + return minValue + nextUint256(self, maxValue - minValue); + } + + function nextBool(Context storage self) internal returns (bool) { + return nextUint256(self) % 2 == 0; + } + + function nextAddress(Context storage self) internal returns (address) { + return address(uint160(nextUint256(self))); + } + + function _nextValue(Context storage self) private returns (bytes32) { + self.value = keccak256(abi.encode(self.value)); + return self.value; + } +} diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index b1b61dd8..aaf74fdb 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -1,121 +1,71 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {Test} from "forge-std/Test.sol"; -import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {Durations, Duration} from "contracts/types/Duration.sol"; - +import {console} from "forge-std/Test.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import { - TransparentUpgradeableProxy, - ProxyAdmin -} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; - -import {EscrowState, Escrow, VetoerState, LockedAssetsTotals} from "contracts/Escrow.sol"; -import {Executor} from "contracts/Executor.sol"; - -import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; -import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; -import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; -import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; - -import {ResealManager} from "contracts/ResealManager.sol"; - -import { - ProposalStatus, EmergencyProtection, EmergencyProtectedTimelock -} from "contracts/EmergencyProtectedTimelock.sol"; - -import {DualGovernance, State as DGState, DualGovernanceStateMachine} from "contracts/DualGovernance.sol"; -import {TimelockedGovernance, IGovernance} from "contracts/TimelockedGovernance.sol"; - -import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; -import {Percents, percents} from "../utils/percents.sol"; -import {IStETH} from "contracts/interfaces/IStETH.sol"; -import {IWstETH} from "contracts/interfaces/IWstETH.sol"; +// --- +// Types +// --- -import {IWithdrawalQueue, WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; -import {IDangerousContract} from "../utils/interfaces.sol"; -import {ExternalCallHelpers} from "../utils/executor-calls.sol"; -import {Utils, TargetMock, console} from "../utils/utils.sol"; -import {ImmutableDualGovernanceConfigProvider} from "contracts/DualGovernanceConfigProvider.sol"; - -import {DAO_VOTING, ST_ETH, WST_ETH, WITHDRAWAL_QUEUE, DAO_AGENT} from "../utils/mainnet-addresses.sol"; - -import {Deployment} from "../utils/deployment.sol"; - -struct Balances { - uint256 stETHAmount; - uint256 stETHShares; - uint256 wstETHAmount; - uint256 wstETHShares; -} - -uint256 constant PERCENTS_PRECISION = 16; - -function countDigits(uint256 number) pure returns (uint256 digitsCount) { - do { - digitsCount++; - } while (number / 10 != 0); -} +import {PercentD16} from "contracts/types/PercentD16.sol"; +import {Duration, Durations} from "contracts/types/Duration.sol"; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -Duration constant ONE_SECOND = Duration.wrap(1); +import {Escrow, VetoerState, LockedAssetsTotals} from "contracts/Escrow.sol"; -contract ScenarioTestBlueprint is Test { - address internal immutable _ADMIN_PROPOSER = DAO_VOTING; - Duration internal immutable _EMERGENCY_MODE_DURATION = Durations.from(180 days); - Duration internal immutable _EMERGENCY_PROTECTION_DURATION = Durations.from(90 days); - address internal immutable _EMERGENCY_ACTIVATION_COMMITTEE = makeAddr("EMERGENCY_ACTIVATION_COMMITTEE"); - address internal immutable _EMERGENCY_EXECUTION_COMMITTEE = makeAddr("EMERGENCY_EXECUTION_COMMITTEE"); +// --- +// Interfaces +// --- - Duration internal immutable _SEALING_DURATION = Durations.from(14 days); - Duration internal immutable _SEALING_COMMITTEE_LIFETIME = Durations.from(365 days); - address internal immutable _SEALING_COMMITTEE = makeAddr("SEALING_COMMITTEE"); +import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {IPotentiallyDangerousContract} from "./interfaces/IPotentiallyDangerousContract.sol"; - Duration internal immutable _AFTER_SUBMIT_DELAY = Durations.from(3 days); - Duration internal immutable _AFTER_SCHEDULE_DELAY = Durations.from(2 days); +// --- +// Main Contracts +// --- - // --- - // Protocol Dependencies - // --- +import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; +import {ProposalStatus, EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; +import {IGovernance} from "contracts/TimelockedGovernance.sol"; +import {State as DGState, DualGovernanceStateMachine} from "contracts/DualGovernance.sol"; - IStETH public immutable _ST_ETH = IStETH(ST_ETH); - IWstETH public immutable _WST_ETH = IWstETH(WST_ETH); - IWithdrawalQueue public immutable _WITHDRAWAL_QUEUE = IWithdrawalQueue(WITHDRAWAL_QUEUE); +// --- +// Test Utils +// --- - // --- - // Core Components - // --- +import {TargetMock} from "../utils/target-mock.sol"; - Executor internal _adminExecutor; - EmergencyProtectedTimelock internal _timelock; +import {Random} from "../utils/random.sol"; +import {ExternalCallHelpers} from "../utils/executor-calls.sol"; - ResealManager internal _resealManager; - DualGovernance internal _dualGovernance; - ImmutableDualGovernanceConfigProvider internal _dualGovernanceConfigProvider; +import {LidoUtils, EvmScriptUtils} from "./lido-utils.sol"; - TimelockedGovernance internal _timelockedGovernance; +import {EvmScriptUtils} from "../utils/evm-script-utils.sol"; - // --- - // Committees - // --- +import {SetupDeployment} from "./SetupDeployment.sol"; +import {TestingAssertEqExtender} from "./testing-assert-eq-extender.sol"; - EmergencyActivationCommittee internal _emergencyActivationCommittee; - EmergencyExecutionCommittee internal _emergencyExecutionCommittee; - TiebreakerCore internal _tiebreakerCommittee; - TiebreakerSubCommittee[] internal _tiebreakerSubCommittees; +uint256 constant FORK_BLOCK_NUMBER = 20218312; - address[] internal _sealableWithdrawalBlockers = [WITHDRAWAL_QUEUE]; +contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { + using LidoUtils for LidoUtils.Context; - // --- - // Helper Contracts - // --- - TargetMock internal _target; + constructor() SetupDeployment(LidoUtils.mainnet(), Random.create(block.timestamp)) { + /// Maybe not the best idea to do it in the constructor, consider move it into setUp method + vm.createSelectFork(vm.envString("MAINNET_RPC_URL")); + vm.rollFork(FORK_BLOCK_NUMBER); + _lido.removeStakingLimit(); + } // --- // Helper Getters // --- + function _getAdminExecutor() internal view returns (address) { + return _timelock.getAdminExecutor(); + } + function _getVetoSignallingEscrow() internal view returns (Escrow) { return Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); } @@ -125,8 +75,10 @@ contract ScenarioTestBlueprint is Test { return Escrow(payable(rageQuitEscrow)); } - function _getTargetRegularStaffCalls() internal view returns (ExternalCall[] memory) { - return ExternalCallHelpers.create(address(_target), abi.encodeCall(IDangerousContract.doRegularStaff, (42))); + function _getMockTargetRegularStaffCalls() internal view returns (ExternalCall[] memory) { + return ExternalCallHelpers.create( + address(_targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRegularStaff, (42)) + ); } function _getVetoSignallingState() @@ -153,60 +105,70 @@ contract ScenarioTestBlueprint is Test { } // --- - // Network Configuration + // Balances Manipulation // --- - function _selectFork() internal { - Utils.selectFork(); + + function _setupStETHBalance(address account, uint256 amount) internal { + _lido.submitStETH(account, amount); } - // --- - // Balances Manipulation - // --- + function _setupStETHBalance(address account, PercentD16 tvlPercentage) internal { + _lido.submitStETH(account, _lido.calcAmountToDepositFromPercentageOfTVL(tvlPercentage)); + } - function _depositStETH( - address account, - uint256 amountToMint - ) internal returns (uint256 sharesMinted, uint256 amountMinted) { - return Utils.depositStETH(account, amountToMint); + function _setupWstETHBalance(address account, uint256 amount) internal { + _lido.submitWstETH(account, amount); } - function _setupStETHWhale(address vetoer) internal returns (uint256 shares, uint256 amount) { - Utils.removeLidoStakingLimit(); - return Utils.setupStETHWhale(vetoer, percents("10.0")); + function _setupWstETHBalance(address account, PercentD16 tvlPercentage) internal { + _lido.submitWstETH(account, _lido.calcSharesToDepositFromPercentageOfTVL(tvlPercentage)); } - function _setupStETHWhale( - address vetoer, - Percents memory vetoPowerInPercents - ) internal returns (uint256 shares, uint256 amount) { - Utils.removeLidoStakingLimit(); - return Utils.setupStETHWhale(vetoer, vetoPowerInPercents); + function _submitStETH( + address account, + uint256 amountToMint + ) internal returns (uint256 sharesMinted, uint256 amountMinted) { + _lido.submitStETH(account, amountToMint); } function _getBalances(address vetoer) internal view returns (Balances memory balances) { - uint256 stETHAmount = _ST_ETH.balanceOf(vetoer); - uint256 wstETHShares = _WST_ETH.balanceOf(vetoer); + uint256 stETHAmount = _lido.stETH.balanceOf(vetoer); + uint256 wstETHShares = _lido.wstETH.balanceOf(vetoer); balances = Balances({ stETHAmount: stETHAmount, - stETHShares: _ST_ETH.getSharesByPooledEth(stETHAmount), - wstETHAmount: _ST_ETH.getPooledEthByShares(wstETHShares), + stETHShares: _lido.stETH.getSharesByPooledEth(stETHAmount), + wstETHAmount: _lido.stETH.getPooledEthByShares(wstETHShares), wstETHShares: wstETHShares }); } + // --- + // Withdrawal Queue Operations + // --- + function _finalizeWithdrawalQueue() internal { + _lido.finalizeWithdrawalQueue(); + } + + function _finalizeWithdrawalQueue(uint256 id) internal { + _lido.finalizeWithdrawalQueue(id); + } + + function _simulateRebase(PercentD16 rebaseFactor) internal { + _lido.simulateRebase(rebaseFactor); + } + // --- // Escrow Manipulation // --- - function _lockStETH(address vetoer, Percents memory vetoPowerInPercents) internal returns (uint256 amount) { - (, amount) = _setupStETHWhale(vetoer, vetoPowerInPercents); - _lockStETH(vetoer, amount); + function _lockStETH(address vetoer, PercentD16 tvlPercentage) internal { + _lockStETH(vetoer, _lido.calcAmountFromPercentageOfTVL(tvlPercentage)); } function _lockStETH(address vetoer, uint256 amount) internal { Escrow escrow = _getVetoSignallingEscrow(); vm.startPrank(vetoer); - if (_ST_ETH.allowance(vetoer, address(escrow)) < amount) { - _ST_ETH.approve(address(escrow), amount); + if (_lido.stETH.allowance(vetoer, address(escrow)) < amount) { + _lido.stETH.approve(address(escrow), amount); } escrow.lockStETH(amount); vm.stopPrank(); @@ -218,11 +180,15 @@ contract ScenarioTestBlueprint is Test { vm.stopPrank(); } + function _lockWstETH(address vetoer, PercentD16 tvlPercentage) internal { + _lockStETH(vetoer, _lido.calcSharesFromPercentageOfTVL(tvlPercentage)); + } + function _lockWstETH(address vetoer, uint256 amount) internal { Escrow escrow = _getVetoSignallingEscrow(); vm.startPrank(vetoer); - if (_WST_ETH.allowance(vetoer, address(escrow)) < amount) { - _WST_ETH.approve(address(escrow), amount); + if (_lido.wstETH.allowance(vetoer, address(escrow)) < amount) { + _lido.wstETH.approve(address(escrow), amount); } escrow.lockWstETH(amount); vm.stopPrank(); @@ -230,7 +196,7 @@ contract ScenarioTestBlueprint is Test { function _unlockWstETH(address vetoer) internal { Escrow escrow = _getVetoSignallingEscrow(); - uint256 wstETHBalanceBefore = _WST_ETH.balanceOf(vetoer); + uint256 wstETHBalanceBefore = _lido.wstETH.balanceOf(vetoer); VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); vm.startPrank(vetoer); @@ -240,7 +206,7 @@ contract ScenarioTestBlueprint is Test { // 1 wei rounding issue may arise because of the wrapping stETH into wstETH before // sending funds to the user assertApproxEqAbs(wstETHUnlocked, vetoerStateBefore.stETHLockedShares, 1); - assertApproxEqAbs(_WST_ETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerStateBefore.stETHLockedShares, 1); + assertApproxEqAbs(_lido.wstETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerStateBefore.stETHLockedShares, 1); } function _lockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { @@ -249,19 +215,19 @@ contract ScenarioTestBlueprint is Test { LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); uint256 unstETHTotalSharesLocked = 0; - WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); for (uint256 i = 0; i < unstETHIds.length; ++i) { unstETHTotalSharesLocked += statuses[i].amountOfShares; } vm.startPrank(vetoer); - _WITHDRAWAL_QUEUE.setApprovalForAll(address(escrow), true); + _lido.withdrawalQueue.setApprovalForAll(address(escrow), true); escrow.lockUnstETH(unstETHIds); - _WITHDRAWAL_QUEUE.setApprovalForAll(address(escrow), false); + _lido.withdrawalQueue.setApprovalForAll(address(escrow), false); vm.stopPrank(); for (uint256 i = 0; i < unstETHIds.length; ++i) { - assertEq(_WITHDRAWAL_QUEUE.ownerOf(unstETHIds[i]), address(escrow)); + assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), address(escrow)); } VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); @@ -280,7 +246,7 @@ contract ScenarioTestBlueprint is Test { LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); uint256 unstETHTotalSharesUnlocked = 0; - WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); for (uint256 i = 0; i < unstETHIds.length; ++i) { unstETHTotalSharesUnlocked += statuses[i].amountOfShares; } @@ -290,7 +256,7 @@ contract ScenarioTestBlueprint is Test { vm.stopPrank(); for (uint256 i = 0; i < unstETHIds.length; ++i) { - assertEq(_WITHDRAWAL_QUEUE.ownerOf(unstETHIds[i]), vetoer); + assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), vetoer); } VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); @@ -314,6 +280,20 @@ contract ScenarioTestBlueprint is Test { // --- // Proposals Submission // --- + function _submitProposalViaDualGovernance( + string memory description, + ExternalCall[] memory calls + ) internal returns (uint256 proposalId) { + proposalId = _submitProposal(_dualGovernance, description, calls); + } + + function _submitProposalViaTimelockedGovernance( + string memory description, + ExternalCall[] memory calls + ) internal returns (uint256 proposalId) { + proposalId = _submitProposal(_timelockedGovernance, description, calls); + } + function _submitProposal( IGovernance governance, string memory description, @@ -322,20 +302,28 @@ contract ScenarioTestBlueprint is Test { uint256 proposalsCountBefore = _timelock.getProposalsCount(); bytes memory script = - Utils.encodeEvmCallScript(address(governance), abi.encodeCall(IGovernance.submitProposal, (calls))); - uint256 voteId = Utils.adoptVote(DAO_VOTING, description, script); + EvmScriptUtils.encodeEvmCallScript(address(governance), abi.encodeCall(IGovernance.submitProposal, (calls))); + uint256 voteId = _lido.adoptVote(description, script); // The scheduled calls count is the same until the vote is enacted assertEq(_timelock.getProposalsCount(), proposalsCountBefore); // executing the vote - Utils.executeVote(DAO_VOTING, voteId); + _lido.executeVote(voteId); proposalId = _timelock.getProposalsCount(); // new call is scheduled but has not executable yet assertEq(proposalId, proposalsCountBefore + 1); } + function _scheduleProposalViaDualGovernance(uint256 proposalId) internal { + _scheduleProposal(_dualGovernance, proposalId); + } + + function _scheduleProposalViaTimelockedGovernance(uint256 proposalId) internal { + _scheduleProposal(_timelockedGovernance, proposalId); + } + function _scheduleProposal(IGovernance governance, uint256 proposalId) internal { governance.scheduleProposal(proposalId); } @@ -376,7 +364,7 @@ contract ScenarioTestBlueprint is Test { } function _assertTargetMockCalls(address sender, ExternalCall[] memory calls) internal { - TargetMock.Call[] memory called = _target.getCalls(); + TargetMock.Call[] memory called = _targetMock.getCalls(); assertEq(called.length, calls.length); for (uint256 i = 0; i < calls.length; ++i) { @@ -385,11 +373,11 @@ contract ScenarioTestBlueprint is Test { assertEq(called[i].data, calls[i].payload); assertEq(called[i].blockNumber, block.number); } - _target.reset(); + _targetMock.reset(); } function _assertTargetMockCalls(address[] memory senders, ExternalCall[] memory calls) internal { - TargetMock.Call[] memory called = _target.getCalls(); + TargetMock.Call[] memory called = _targetMock.getCalls(); assertEq(called.length, calls.length); assertEq(called.length, senders.length); @@ -399,13 +387,21 @@ contract ScenarioTestBlueprint is Test { assertEq(called[i].data, calls[i].payload, "Unexpected payload"); assertEq(called[i].blockNumber, block.number); } - _target.reset(); + _targetMock.reset(); } function _assertCanExecute(uint256 proposalId, bool canExecute) internal { assertEq(_timelock.canExecute(proposalId), canExecute, "unexpected canExecute() value"); } + function _assertCanScheduleViaDualGovernance(uint256 proposalId, bool canSchedule) internal { + _assertCanSchedule(_dualGovernance, proposalId, canSchedule); + } + + function _assertCanScheduleViaTimelockedGovernance(uint256 proposalId, bool canSchedule) internal { + _assertCanSchedule(_timelockedGovernance, proposalId, canSchedule); + } + function _assertCanSchedule(IGovernance governance, uint256 proposalId, bool canSchedule) internal { assertEq(governance.canScheduleProposal(proposalId), canSchedule, "unexpected canSchedule() value"); } @@ -467,7 +463,7 @@ contract ScenarioTestBlueprint is Test { } function _assertNoTargetMockCalls() internal { - assertEq(_target.getCalls().length, 0, "Unexpected target calls count"); + assertEq(_targetMock.getCalls().length, 0, "Unexpected target calls count"); } // --- @@ -525,166 +521,6 @@ contract ScenarioTestBlueprint is Test { /* solhint-enable no-console */ } - // --- - // Test Setup Deployment - // --- - - function _deployDualGovernanceSetup(bool isEmergencyProtectionEnabled) internal { - Deployment.DualGovernanceSetup memory dgSetup = Deployment.deployDualGovernanceContracts({ - stETH: _ST_ETH, - wstETH: _WST_ETH, - withdrawalQueue: _WITHDRAWAL_QUEUE, - emergencyGovernance: DAO_VOTING - }); - _adminExecutor = dgSetup.adminExecutor; - _timelock = dgSetup.timelock; - _resealManager = dgSetup.resealManager; - _dualGovernanceConfigProvider = dgSetup.dualGovernanceConfigProvider; - _dualGovernance = dgSetup.dualGovernance; - - _deployTiebreaker(); - _deployEmergencyActivationCommittee(); - _deployEmergencyExecutionCommittee(); - _finishTimelockSetup( - address(_dualGovernance), address(dgSetup.emergencyGovernance), isEmergencyProtectionEnabled - ); - } - - function _deployTimelockedGovernanceSetup(bool isEmergencyProtectionEnabled) internal { - Deployment.DualGovernanceSetup memory dgSetup = Deployment.deployDualGovernanceContracts({ - stETH: _ST_ETH, - wstETH: _WST_ETH, - withdrawalQueue: _WITHDRAWAL_QUEUE, - emergencyGovernance: DAO_VOTING - }); - _timelockedGovernance = dgSetup.emergencyGovernance; - _adminExecutor = dgSetup.adminExecutor; - _timelock = dgSetup.timelock; - - _deployEmergencyActivationCommittee(); - _deployEmergencyExecutionCommittee(); - _finishTimelockSetup( - address(_timelockedGovernance), address(dgSetup.emergencyGovernance), isEmergencyProtectionEnabled - ); - } - - function _deployTarget() internal { - _target = new TargetMock(); - } - - function _deployDualGovernance() internal { - revert("not implemented"); - } - - function _deployTiebreaker() internal { - uint256 subCommitteeMembersCount = 5; - uint256 subCommitteeQuorum = 5; - uint256 subCommitteesCount = 2; - - _tiebreakerCommittee = - new TiebreakerCore(address(_adminExecutor), new address[](0), 1, address(_dualGovernance), 0); - - for (uint256 i = 0; i < subCommitteesCount; ++i) { - address[] memory committeeMembers = new address[](subCommitteeMembersCount); - for (uint256 j = 0; j < subCommitteeMembersCount; j++) { - committeeMembers[j] = makeAddr(string(abi.encode(i + j * subCommitteeMembersCount + 65))); - } - _tiebreakerSubCommittees.push( - new TiebreakerSubCommittee( - address(_adminExecutor), committeeMembers, subCommitteeQuorum, address(_tiebreakerCommittee) - ) - ); - - vm.prank(address(_adminExecutor)); - _tiebreakerCommittee.addMember(address(_tiebreakerSubCommittees[i]), i + 1); - } - } - - function _deployEmergencyActivationCommittee() internal { - uint256 quorum = 3; - uint256 membersCount = 5; - address[] memory committeeMembers = new address[](membersCount); - for (uint256 i = 0; i < membersCount; ++i) { - committeeMembers[i] = makeAddr(string(abi.encode(0xFE + i * membersCount + 65))); - } - _emergencyActivationCommittee = - new EmergencyActivationCommittee(address(_adminExecutor), committeeMembers, quorum, address(_timelock)); - } - - function _deployEmergencyExecutionCommittee() internal { - uint256 quorum = 3; - uint256 membersCount = 5; - address[] memory committeeMembers = new address[](membersCount); - for (uint256 i = 0; i < membersCount; ++i) { - committeeMembers[i] = makeAddr(string(abi.encode(0xFD + i * membersCount + 65))); - } - _emergencyExecutionCommittee = - new EmergencyExecutionCommittee(address(_adminExecutor), committeeMembers, quorum, address(_timelock)); - } - - function _finishTimelockSetup( - address governance, - address emergencyGovernance, - bool isEmergencyProtectionEnabled - ) internal { - _adminExecutor.execute( - address(_timelock), 0, abi.encodeCall(_timelock.setDelays, (_AFTER_SUBMIT_DELAY, _AFTER_SCHEDULE_DELAY)) - ); - - if (isEmergencyProtectionEnabled) { - _adminExecutor.execute( - address(_timelock), - 0, - abi.encodeCall( - _timelock.setupEmergencyProtection, - ( - emergencyGovernance, - address(_emergencyActivationCommittee), - address(_emergencyExecutionCommittee), - _EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()), - _EMERGENCY_MODE_DURATION - ) - ) - ); - - assertEq(_timelock.isEmergencyProtectionEnabled(), true); - } - - vm.prank(DAO_AGENT); - _WITHDRAWAL_QUEUE.grantRole( - 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(_resealManager) - ); - vm.prank(DAO_AGENT); - _WITHDRAWAL_QUEUE.grantRole( - 0x2fc10cc8ae19568712f7a176fb4978616a610650813c9d05326c34abb62749c7, address(_resealManager) - ); - - if (governance == address(_dualGovernance)) { - _adminExecutor.execute( - address(_dualGovernance), - 0, - abi.encodeCall(_dualGovernance.setTiebreakerCommittee, address(_tiebreakerCommittee)) - ); - _adminExecutor.execute( - address(_dualGovernance), - 0, - abi.encodeCall(_dualGovernance.setTiebreakerActivationTimeout, Durations.from(365 days)) - ); - _adminExecutor.execute( - address(_dualGovernance), - 0, - abi.encodeCall(_dualGovernance.addTiebreakerSealableWithdrawalBlocker, WITHDRAWAL_QUEUE) - ); - _adminExecutor.execute( - address(_dualGovernance), - 0, - abi.encodeCall(_dualGovernance.registerProposer, (_ADMIN_PROPOSER, address(_adminExecutor))) - ); - } - _adminExecutor.execute(address(_timelock), 0, abi.encodeCall(_timelock.setGovernance, (governance))); - _adminExecutor.transferOwnership(address(_timelock)); - } - // --- // Utils Methods // --- @@ -699,11 +535,11 @@ contract ScenarioTestBlueprint is Test { } function _waitAfterSubmitDelayPassed() internal { - _wait(_timelock.getAfterSubmitDelay() + ONE_SECOND); + _wait(_timelock.getAfterSubmitDelay() + Durations.from(1 seconds)); } function _waitAfterScheduleDelayPassed() internal { - _wait(_timelock.getAfterScheduleDelay() + ONE_SECOND); + _wait(_timelock.getAfterScheduleDelay() + Durations.from(1 seconds)); } function _executeEmergencyActivate() internal { @@ -762,37 +598,4 @@ contract ScenarioTestBlueprint is Test { ) ); } - - function assertEq(uint40 a, uint40 b) internal { - assertEq(uint256(a), uint256(b)); - } - - function assertEq(Timestamp a, Timestamp b) internal { - assertEq(uint256(Timestamp.unwrap(a)), uint256(Timestamp.unwrap(b))); - } - - function assertEq(Duration a, Duration b) internal { - assertEq(uint256(Duration.unwrap(a)), uint256(Duration.unwrap(b))); - } - - function assertEq(ProposalStatus a, ProposalStatus b) internal { - assertEq(uint256(a), uint256(b)); - } - - function assertEq(ProposalStatus a, ProposalStatus b, string memory message) internal { - assertEq(uint256(a), uint256(b), message); - } - - function assertEq(DGState a, DGState b) internal { - assertEq(uint256(a), uint256(b)); - } - - function assertEq(Balances memory b1, Balances memory b2, uint256 stETHSharesEpsilon) internal { - assertEq(b1.wstETHShares, b2.wstETHShares); - assertEq(b1.wstETHAmount, b2.wstETHAmount); - - uint256 stETHAmountEpsilon = _ST_ETH.getPooledEthByShares(stETHSharesEpsilon); - assertApproxEqAbs(b1.stETHShares, b2.stETHShares, stETHSharesEpsilon); - assertApproxEqAbs(b1.stETHAmount, b2.stETHAmount, stETHAmountEpsilon); - } } diff --git a/test/utils/target-mock.sol b/test/utils/target-mock.sol new file mode 100644 index 00000000..8e2f19c3 --- /dev/null +++ b/test/utils/target-mock.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +// May be used as a mock contract to collect method calls +contract TargetMock { + struct Call { + uint256 value; + address sender; + uint256 blockNumber; + bytes data; + } + + Call[] public calls; + + function getCallsLength() external view returns (uint256) { + return calls.length; + } + + function getCalls() external view returns (Call[] memory calls_) { + calls_ = calls; + } + + function reset() external { + for (uint256 i = 0; i < calls.length; ++i) { + calls.pop(); + } + } + + fallback() external payable { + calls.push(Call({value: msg.value, sender: msg.sender, blockNumber: block.number, data: msg.data})); + } + + receive() external payable {} +} diff --git a/test/utils/testing-assert-eq-extender.sol b/test/utils/testing-assert-eq-extender.sol new file mode 100644 index 00000000..c8f78d7d --- /dev/null +++ b/test/utils/testing-assert-eq-extender.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Test} from "forge-std/Test.sol"; + +import {Duration} from "contracts/types/Duration.sol"; +import {Timestamp} from "contracts/types/Timestamp.sol"; +import {PercentD16} from "contracts/types/PercentD16.sol"; + +import {ProposalStatus} from "contracts/EmergencyProtectedTimelock.sol"; +import {State as DualGovernanceState} from "contracts/DualGovernance.sol"; + +contract TestingAssertEqExtender is Test { + struct Balances { + uint256 stETHAmount; + uint256 stETHShares; + uint256 wstETHAmount; + uint256 wstETHShares; + } + + function assertEq(Duration a, Duration b) internal { + assertEq(uint256(Duration.unwrap(a)), uint256(Duration.unwrap(b))); + } + + function assertEq(Timestamp a, Timestamp b) internal { + assertEq(uint256(Timestamp.unwrap(a)), uint256(Timestamp.unwrap(b))); + } + + function assertEq(ProposalStatus a, ProposalStatus b) internal { + assertEq(uint256(a), uint256(b)); + } + + function assertEq(ProposalStatus a, ProposalStatus b, string memory message) internal { + assertEq(uint256(a), uint256(b), message); + } + + function assertEq(DualGovernanceState a, DualGovernanceState b) internal { + assertEq(uint256(a), uint256(b)); + } + + function assertEq(Balances memory b1, Balances memory b2, uint256 sharesEpsilon) internal { + assertEq(b1.wstETHShares, b2.wstETHShares); + assertEq(b1.wstETHAmount, b2.wstETHAmount); + + assertApproxEqAbs(b1.stETHShares, b2.stETHShares, sharesEpsilon); + assertApproxEqAbs(b1.stETHAmount, b2.stETHAmount, sharesEpsilon); + } + + function assertEq(PercentD16 a, PercentD16 b) internal { + assertEq(PercentD16.unwrap(a), PercentD16.unwrap(b)); + } +} diff --git a/test/utils/unit-test.sol b/test/utils/unit-test.sol index 3789da5b..2e41ec31 100644 --- a/test/utils/unit-test.sol +++ b/test/utils/unit-test.sol @@ -1,46 +1,22 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {Duration, Durations} from "contracts/types/Duration.sol"; +import {Duration} from "contracts/types/Duration.sol"; // solhint-disable-next-line -import {Test, console} from "forge-std/Test.sol"; import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; import {ExternalCallHelpers} from "test/utils/executor-calls.sol"; -import {IDangerousContract} from "test/utils/interfaces.sol"; -import {Status as ProposalStatus} from "contracts/libraries/ExecutableProposals.sol"; +import {IPotentiallyDangerousContract} from "./interfaces/IPotentiallyDangerousContract.sol"; +import {TestingAssertEqExtender} from "./testing-assert-eq-extender.sol"; -contract UnitTest is Test { +contract UnitTest is TestingAssertEqExtender { function _wait(Duration duration) internal { vm.warp(block.timestamp + Duration.unwrap(duration)); } - function _getTargetRegularStaffCalls(address targetMock) internal pure returns (ExternalCall[] memory) { - return ExternalCallHelpers.create(address(targetMock), abi.encodeCall(IDangerousContract.doRegularStaff, (42))); - } - - function assertEq(Timestamp a, Timestamp b) internal { - assertEq(uint256(Timestamp.unwrap(a)), uint256(Timestamp.unwrap(b))); - } - - function assertEq(Timestamp a, Timestamp b, string memory message) internal { - assertEq(uint256(Timestamp.unwrap(a)), uint256(Timestamp.unwrap(b)), message); - } - - function assertEq(Duration a, Duration b) internal { - assertEq(uint256(Duration.unwrap(a)), uint256(Duration.unwrap(b))); - } - - function assertEq(Duration a, Duration b, string memory message) internal { - assertEq(uint256(Duration.unwrap(a)), uint256(Duration.unwrap(b)), message); - } - - function assertEq(ProposalStatus a, ProposalStatus b) internal { - assertEq(uint256(a), uint256(b)); - } - - function assertEq(ProposalStatus a, ProposalStatus b, string memory message) internal { - assertEq(uint256(a), uint256(b), message); + function _getMockTargetRegularStaffCalls(address targetMock) internal pure returns (ExternalCall[] memory) { + return ExternalCallHelpers.create( + address(targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRegularStaff, (42)) + ); } } diff --git a/test/utils/utils.sol b/test/utils/utils.sol deleted file mode 100644 index d2c4dcfc..00000000 --- a/test/utils/utils.sol +++ /dev/null @@ -1,213 +0,0 @@ -pragma solidity 0.8.26; - -// solhint-disable-next-line -import "forge-std/console2.sol"; -import "forge-std/Vm.sol"; -import "forge-std/Test.sol"; - -import {stdStorage, StdStorage} from "forge-std/Test.sol"; - -import {Percents, percents} from "../utils/percents.sol"; - -import "./mainnet-addresses.sol"; -import "./interfaces.sol"; - -import {IStETH, IERC20} from "contracts/interfaces/IStETH.sol"; - -// May be used as a mock contract to collect method calls -contract TargetMock { - struct Call { - uint256 value; - address sender; - uint256 blockNumber; - bytes data; - } - - Call[] public calls; - - function getCallsLength() external view returns (uint256) { - return calls.length; - } - - function getCalls() external view returns (Call[] memory calls_) { - calls_ = calls; - } - - function reset() external { - for (uint256 i = 0; i < calls.length; ++i) { - calls.pop(); - } - } - - fallback() external payable { - calls.push(Call({value: msg.value, sender: msg.sender, blockNumber: block.number, data: msg.data})); - } -} - -library Utils { - using stdStorage for StdStorage; - - Vm constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - - struct EvmScriptCall { - address target; - bytes data; - } - - function selectFork() internal { - vm.createSelectFork(vm.envString("MAINNET_RPC_URL")); - vm.rollFork(20218312); - } - - function encodeEvmCallScript(address target, bytes memory data) internal pure returns (bytes memory) { - EvmScriptCall[] memory calls = new EvmScriptCall[](1); - calls[0] = EvmScriptCall(target, data); - return encodeEvmCallScript(calls); - } - - function encodeEvmCallScript(EvmScriptCall[] memory calls) internal pure returns (bytes memory) { - bytes memory script = new bytes(4); - script[3] = 0x01; - - for (uint256 i = 0; i < calls.length; ++i) { - EvmScriptCall memory call = calls[i]; - script = bytes.concat(script, bytes20(call.target), bytes4(uint32(call.data.length)), call.data); - } - - return script; - } - - function setupLdoWhale(address addr) internal { - vm.startPrank(DAO_AGENT); - IERC20(LDO_TOKEN).transfer(addr, IERC20(LDO_TOKEN).balanceOf(DAO_AGENT)); - vm.stopPrank(); - // solhint-disable-next-line - console.log( - "LDO whale %x balance: %d LDO at block %d", addr, IERC20(LDO_TOKEN).balanceOf(addr) / 10 ** 18, block.number - ); - assert(IERC20(LDO_TOKEN).balanceOf(addr) >= IAragonVoting(DAO_VOTING).minAcceptQuorumPct()); - // need to increase block number since MiniMe snapshotting relies on it - vm.roll(block.number + 1); - vm.warp(block.timestamp + 15); - } - - function setupStETHWhale(address addr) internal returns (uint256 shares, uint256 balance) { - // 15% of total stETH supply - return setupStETHWhale(addr, percents("30.00")); - } - - function setupStETHWhale( - address addr, - Percents memory totalSupplyPercentage - ) internal returns (uint256 shares, uint256 balance) { - uint256 ST_ETH_TRANSFERS_SHARE_LOSS_COMPENSATION = 8; // TODO: evaluate min enough value - // bal / (totalSupply + bal) = percentage => bal = totalSupply * percentage / (1 - percentage) - shares = ST_ETH_TRANSFERS_SHARE_LOSS_COMPENSATION - + IStETH(ST_ETH).getTotalShares() * totalSupplyPercentage.value - / (100 * 10 ** totalSupplyPercentage.precision - totalSupplyPercentage.value); - // to compensate StETH wei lost on submit/transfers, generate slightly larger eth amount - return depositStETH(addr, IStETH(ST_ETH).getPooledEthByShares(shares)); - } - - function depositStETH( - address addr, - uint256 amountToMint - ) internal returns (uint256 sharesMinted, uint256 amountMinted) { - uint256 sharesBalanceBefore = IStETH(ST_ETH).sharesOf(addr); - uint256 amountBalanceBefore = IStETH(ST_ETH).balanceOf(addr); - - // solhint-disable-next-line - console.log("setting ETH balance of address %x to %d ETH", addr, amountToMint / 10 ** 18); - vm.deal(addr, amountToMint); - vm.prank(addr); - IStETH(ST_ETH).submit{value: amountToMint}(address(0)); - - sharesMinted = IStETH(ST_ETH).sharesOf(addr) - sharesBalanceBefore; - amountMinted = IStETH(ST_ETH).balanceOf(addr) - amountBalanceBefore; - - // solhint-disable-next-line - console.log("stETH balance of address %x: %d stETH", addr, (amountMinted) / 10 ** 18); - } - - function removeLidoStakingLimit() external { - grantPermission(ST_ETH, IStETH(ST_ETH).STAKING_CONTROL_ROLE(), address(this)); - (, bool isStakingLimitSet,,,,,) = IStETH(ST_ETH).getStakeLimitFullInfo(); - if (isStakingLimitSet) { - IStETH(ST_ETH).removeStakingLimit(); - } - // solhint-disable-next-line - console.log("Lido staking limit removed"); - } - - function grantPermission(address app, bytes32 role, address grantee) internal { - IAragonACL acl = IAragonACL(DAO_ACL); - if (!acl.hasPermission(grantee, app, role)) { - // solhint-disable-next-line - console.log("granting permission %x on %x to %x", uint256(role), app, grantee); - address manager = acl.getPermissionManager(app, role); - vm.prank(manager); - acl.grantPermission(grantee, app, role); - assert(acl.hasPermission(grantee, app, role)); - } - } - - function supportVoteAndWaitTillDecided(uint256 voteId, address voter) internal { - supportVote(voteId, voter); - vm.warp(block.timestamp + IAragonVoting(DAO_VOTING).voteTime()); - } - - function supportVote(uint256 voteId, address voter) internal { - vote(voteId, voter, true); - } - - function vote(uint256 voteId, address voter, bool support) internal { - // solhint-disable-next-line - console.log("voting from %x at block %d", voter, block.number); - vm.prank(voter); - IAragonVoting(DAO_VOTING).vote(voteId, support, false); - } - - // Creates vote with given description and script, votes for it, and waits until it can be executed - function adoptVote( - address voting, - string memory description, - bytes memory script - ) internal returns (uint256 voteId) { - uint256 ldoWhalePrivateKey = uint256(keccak256(abi.encodePacked("LDO_WHALE"))); - address ldoWhale = vm.addr(ldoWhalePrivateKey); - if (IERC20(LDO_TOKEN).balanceOf(ldoWhale) < IAragonVoting(DAO_VOTING).minAcceptQuorumPct()) { - setupLdoWhale(ldoWhale); - } - bytes memory voteScript = Utils.encodeEvmCallScript( - voting, abi.encodeCall(IAragonVoting.newVote, (script, description, false, false)) - ); - - voteId = IAragonVoting(voting).votesLength(); - - vm.prank(ldoWhale); - IAragonForwarder(DAO_TOKEN_MANAGER).forward(voteScript); - supportVoteAndWaitTillDecided(voteId, ldoWhale); - } - - function executeVote(address voting, uint256 voteId) internal { - IAragonVoting(voting).executeVote(voteId); - } - - function predictDeployedAddress(address _origin, uint256 _nonce) public pure returns (address) { - bytes memory data; - if (_nonce == 0x00) { - data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), _origin, bytes1(0x80)); - } else if (_nonce <= 0x7f) { - data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), _origin, bytes1(uint8(_nonce))); - } else if (_nonce <= 0xff) { - data = abi.encodePacked(bytes1(0xd7), bytes1(0x94), _origin, bytes1(0x81), uint8(_nonce)); - } else if (_nonce <= 0xffff) { - data = abi.encodePacked(bytes1(0xd8), bytes1(0x94), _origin, bytes1(0x82), uint16(_nonce)); - } else if (_nonce <= 0xffffff) { - data = abi.encodePacked(bytes1(0xd9), bytes1(0x94), _origin, bytes1(0x83), uint24(_nonce)); - } else { - data = abi.encodePacked(bytes1(0xda), bytes1(0x94), _origin, bytes1(0x84), uint32(_nonce)); - } - return address(uint160(uint256(keccak256(data)))); - } -}