diff --git a/.env.example b/.env.example index baa5a015..38add99d 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,7 @@ MAINNET_RPC_URL= + +# Deploy script env vars +CHAIN=<"mainnet" OR "holesky" OR "holesky-mocks"> +ETHERSCAN_MAINNET_KEY=... +DEPLOY_CONFIG_FILE_PATH=deploy-config/deploy-config.json +DEPLOYED_ADDRESSES_FILE_PATH=deploy-config/deployed-addrs.json diff --git a/.gitignore b/.gitignore index 8c09d4d1..cec3ecb0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,8 @@ cache_forge/ out/ # Foundry: development broadcast logs -!/broadcast +/broadcast /broadcast/*/31337/ /broadcast/**/dry-run/ .vscode/ +deploy-config diff --git a/.gitmodules b/.gitmodules index 5f8d4d17..fb0de12d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,7 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std + branch = tags/v1.9.3 [submodule "lib/kontrol-cheatcodes"] path = lib/kontrol-cheatcodes url = https://github.com/runtimeverification/kontrol-cheatcodes diff --git a/addresses/holesky-addresses.sol b/addresses/holesky-addresses.sol new file mode 100644 index 00000000..6f052252 --- /dev/null +++ b/addresses/holesky-addresses.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +address constant DAO_VOTING = 0xdA7d2573Df555002503F29aA4003e398d28cc00f; +address constant ST_ETH = 0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034; +address constant WST_ETH = 0x8d09a4502Cc8Cf1547aD300E066060D043f6982D; +address constant WITHDRAWAL_QUEUE = 0xc7cc160b58F8Bb0baC94b80847E2CF2800565C50; diff --git a/test/utils/mainnet-addresses.sol b/addresses/mainnet-addresses.sol similarity index 100% rename from test/utils/mainnet-addresses.sol rename to addresses/mainnet-addresses.sol diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index de4c69ab..12a73b7b 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -7,7 +7,7 @@ import {Timestamp} from "./types/Timestamp.sol"; import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; -import {IEscrow} from "./interfaces/IEscrow.sol"; +import {IEscrowBase} from "./interfaces/IEscrowBase.sol"; import {ITimelock} from "./interfaces/ITimelock.sol"; import {ITiebreaker} from "./interfaces/ITiebreaker.sol"; import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; @@ -17,6 +17,7 @@ import {IDualGovernanceConfigProvider} from "./interfaces/IDualGovernanceConfigP import {Proposers} from "./libraries/Proposers.sol"; import {Tiebreaker} from "./libraries/Tiebreaker.sol"; import {ExternalCall} from "./libraries/ExternalCalls.sol"; +import {Resealer} from "./libraries/Resealer.sol"; import {State, DualGovernanceStateMachine} from "./libraries/DualGovernanceStateMachine.sol"; import {Escrow} from "./Escrow.sol"; @@ -31,20 +32,21 @@ contract DualGovernance is IDualGovernance { using Proposers for Proposers.Context; using Tiebreaker for Tiebreaker.Context; using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; + using Resealer for Resealer.Context; // --- // Errors // --- - error NotAdminProposer(); - error UnownedAdminExecutor(); - error CallerIsNotResealCommittee(address caller); error CallerIsNotAdminExecutor(address caller); + error CallerIsNotProposalsCanceller(address caller); + error InvalidProposalsCanceller(address canceller); error ProposalSubmissionBlocked(); error ProposalSchedulingBlocked(uint256 proposalId); error ResealIsNotAllowedInNormalState(); - error InvalidResealCommittee(address resealCommittee); - error InvalidTiebreakerActivationTimeoutBounds(); + error InvalidTiebreakerActivationTimeoutBounds( + Duration minTiebreakerActivationTimeout, Duration maxTiebreakerActivationTimeout + ); // --- // Events @@ -52,8 +54,8 @@ contract DualGovernance is IDualGovernance { event CancelAllPendingProposalsSkipped(); event CancelAllPendingProposalsExecuted(); - event EscrowMasterCopyDeployed(IEscrow escrowMasterCopy); - event ResealCommitteeSet(address resealCommittee); + event ProposalsCancellerSet(address proposalsCanceller); + event EscrowMasterCopyDeployed(IEscrowBase escrowMasterCopy); // --- // Sanity Check Parameters & Immutables @@ -69,11 +71,13 @@ contract DualGovernance is IDualGovernance { /// @param maxSealableWithdrawalBlockersCount The upper bound for the number of sealable withdrawal blockers allowed to be /// registered in the Dual Governance. This parameter prevents filling the sealable withdrawal blockers /// with so many items that tiebreaker calls would revert due to out-of-gas errors. + /// @param maxMinAssetsLockDuration The upper bound for the minimum duration of assets lock in the Escrow. struct SanityCheckParams { uint256 minWithdrawalsBatchSize; Duration minTiebreakerActivationTimeout; Duration maxTiebreakerActivationTimeout; uint256 maxSealableWithdrawalBlockersCount; + Duration maxMinAssetsLockDuration; } /// @notice The lower bound for the time the Dual Governance must spend in the "locked" state @@ -93,17 +97,21 @@ contract DualGovernance is IDualGovernance { // External Dependencies // --- - /// @notice The external dependencies of the Dual Governance system. + /// @notice Token addresses that used in the Dual Governance as signalling tokens. /// @param stETH The address of the stETH token. /// @param wstETH The address of the wstETH token. /// @param withdrawalQueue The address of Lido's Withdrawal Queue and the unstETH token. - /// @param timelock The address of the Timelock contract. - /// @param resealManager The address of the Reseal Manager. - /// @param configProvider The address of the Dual Governance Config Provider. - struct ExternalDependencies { + struct SignallingTokens { IStETH stETH; IWstETH wstETH; IWithdrawalQueue withdrawalQueue; + } + + /// @notice Dependencies required by the Dual Governance contract. + /// @param timelock The address of the Timelock contract. + /// @param resealManager The address of the Reseal Manager. + /// @param configProvider The address of the Dual Governance Config Provider. + struct DualGovernanceComponents { ITimelock timelock; IResealManager resealManager; IDualGovernanceConfigProvider configProvider; @@ -112,13 +120,6 @@ contract DualGovernance is IDualGovernance { /// @notice The address of the Timelock contract. ITimelock public immutable TIMELOCK; - /// @notice The address of the Reseal Manager. - IResealManager public immutable RESEAL_MANAGER; - - /// @notice The address of the Escrow contract used as the implementation for the Signalling and Rage Quit - /// instances of the Escrows managed by the DualGovernance contract. - IEscrow public immutable ESCROW_MASTER_COPY; - // --- // Aspects // --- @@ -132,41 +133,47 @@ contract DualGovernance is IDualGovernance { /// @dev The state machine implementation controlling the state of the Dual Governance. DualGovernanceStateMachine.Context internal _stateMachine; - // --- - // Standalone State Variables - // --- + /// @dev The functionality for sealing/resuming critical components of Lido protocol. + Resealer.Context internal _resealer; - /// @dev The address of the Reseal Committee which is allowed to "reseal" sealables paused for a limited - /// period of time when the Dual Governance proposal adoption is blocked. - address internal _resealCommittee; + /// @dev The address authorized to call `cancelAllPendingProposals()`, allowing it to cancel all proposals that are + /// submitted or scheduled but not yet executed. + address internal _proposalsCanceller; // --- // Constructor // --- - constructor(ExternalDependencies memory dependencies, SanityCheckParams memory sanityCheckParams) { + constructor( + DualGovernanceComponents memory components, + SignallingTokens memory signallingTokens, + SanityCheckParams memory sanityCheckParams + ) { if (sanityCheckParams.minTiebreakerActivationTimeout > sanityCheckParams.maxTiebreakerActivationTimeout) { - revert InvalidTiebreakerActivationTimeoutBounds(); + revert InvalidTiebreakerActivationTimeoutBounds( + sanityCheckParams.minTiebreakerActivationTimeout, sanityCheckParams.maxTiebreakerActivationTimeout + ); } - TIMELOCK = dependencies.timelock; - RESEAL_MANAGER = dependencies.resealManager; + TIMELOCK = components.timelock; MIN_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.minTiebreakerActivationTimeout; MAX_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.maxTiebreakerActivationTimeout; MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = sanityCheckParams.maxSealableWithdrawalBlockersCount; - ESCROW_MASTER_COPY = new Escrow({ + IEscrowBase escrowMasterCopy = new Escrow({ dualGovernance: this, - stETH: dependencies.stETH, - wstETH: dependencies.wstETH, - withdrawalQueue: dependencies.withdrawalQueue, - minWithdrawalsBatchSize: sanityCheckParams.minWithdrawalsBatchSize + stETH: signallingTokens.stETH, + wstETH: signallingTokens.wstETH, + withdrawalQueue: signallingTokens.withdrawalQueue, + minWithdrawalsBatchSize: sanityCheckParams.minWithdrawalsBatchSize, + maxMinAssetsLockDuration: sanityCheckParams.maxMinAssetsLockDuration }); - emit EscrowMasterCopyDeployed(ESCROW_MASTER_COPY); + emit EscrowMasterCopyDeployed(escrowMasterCopy); - _stateMachine.initialize(dependencies.configProvider, ESCROW_MASTER_COPY); + _stateMachine.initialize(components.configProvider, escrowMasterCopy); + _resealer.setResealManager(components.resealManager); } // --- @@ -188,22 +195,24 @@ contract DualGovernance is IDualGovernance { ExternalCall[] calldata calls, string calldata metadata ) external returns (uint256 proposalId) { - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); if (!_stateMachine.canSubmitProposal({useEffectiveState: false})) { revert ProposalSubmissionBlocked(); } Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); - proposalId = TIMELOCK.submit(proposer.account, proposer.executor, calls, metadata); + proposalId = TIMELOCK.submit(proposer.executor, calls); + + emit ProposalSubmitted(proposer.account, proposalId, metadata); } /// @notice Schedules a previously submitted proposal for execution in the Dual Governance system. /// The proposal can only be scheduled if the current state allows scheduling of the given proposal based on - /// the submission time, when the `Escrow.getAfterScheduleDelay()` has passed and proposal wasn't cancelled + /// the submission time, when the `ITimelock.getAfterSubmitDelay()` has passed and proposal wasn't cancelled /// or scheduled earlier. /// @param proposalId The unique identifier of the proposal to be scheduled. This ID is obtained when the proposal - /// is initially submitted to the timelock contract. + /// is initially submitted to the Dual Governance system. function scheduleProposal(uint256 proposalId) external { - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); Timestamp proposalSubmittedAt = TIMELOCK.getProposalDetails(proposalId).submittedAt; if (!_stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt})) { revert ProposalSchedulingBlocked(proposalId); @@ -211,7 +220,7 @@ contract DualGovernance is IDualGovernance { TIMELOCK.schedule(proposalId); } - /// @notice Allows a proposer associated with the admin executor to cancel all previously submitted or scheduled + /// @notice Allows authorized proposer to cancel all previously submitted or scheduled /// but not yet executed proposals when the Dual Governance system is in the `VetoSignalling` /// or `VetoSignallingDeactivation` state. /// @dev If the Dual Governance state is not `VetoSignalling` or `VetoSignallingDeactivation`, the function will @@ -219,11 +228,10 @@ contract DualGovernance is IDualGovernance { /// @return isProposalsCancelled A boolean indicating whether the proposals were successfully canceled (`true`) /// or the cancellation was skipped due to an inappropriate state (`false`). function cancelAllPendingProposals() external returns (bool) { - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); - Proposers.Proposer memory proposer = _proposers.getProposer(msg.sender); - if (proposer.executor != TIMELOCK.getAdminExecutor()) { - revert NotAdminProposer(); + if (msg.sender != _proposalsCanceller) { + revert CallerIsNotProposalsCanceller(msg.sender); } if (!_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})) { @@ -257,7 +265,7 @@ contract DualGovernance is IDualGovernance { /// - If the system is in the `VetoCooldown` state, the proposal must have been submitted before the system /// last entered the `VetoSignalling` state. /// - The proposal has not already been scheduled, canceled, or executed. - /// - The required delay period, as defined by `Escrow.getAfterSubmitDelay()`, has elapsed since the proposal + /// - The required delay period, as defined by `ITimelock.getAfterSubmitDelay()`, has elapsed since the proposal /// was submitted. /// @param proposalId The unique identifier of the proposal to check. /// @return canScheduleProposal A boolean value indicating whether the proposal can be scheduled (`true`) or @@ -288,7 +296,7 @@ contract DualGovernance is IDualGovernance { /// @dev This function should be called when the `persisted` and `effective` states of the system are not equal. /// If the states are already synchronized, the function will complete without making any changes to the system state. function activateNextState() external { - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); } /// @notice Updates the address of the configuration provider for the Dual Governance system. @@ -298,6 +306,25 @@ contract DualGovernance is IDualGovernance { _stateMachine.setConfigProvider(newConfigProvider); } + /// @notice Sets the address of the proposals canceller authorized to cancel pending proposals. + /// @param newProposalsCanceller The address of the new proposals canceller. + function setProposalsCanceller(address newProposalsCanceller) external { + _checkCallerIsAdminExecutor(); + + if (newProposalsCanceller == address(0) || newProposalsCanceller == _proposalsCanceller) { + revert InvalidProposalsCanceller(newProposalsCanceller); + } + + _proposalsCanceller = newProposalsCanceller; + emit ProposalsCancellerSet(newProposalsCanceller); + } + + /// @notice Retrieves the current proposals canceller address. + /// @return address The address of the current proposals canceller. + function getProposalsCanceller() external view returns (address) { + return _proposalsCanceller; + } + /// @notice Returns the current configuration provider address for the Dual Governance system. /// @return configProvider The address of the current configuration provider contract. function getConfigProvider() external view returns (IDualGovernanceConfigProvider) { @@ -352,42 +379,45 @@ contract DualGovernance is IDualGovernance { /// @notice Registers a new proposer with the associated executor in the system. /// @dev Multiple proposers can share the same executor contract, but each proposer must be unique. - /// @param proposer The address of the proposer to register. + /// @param proposerAccount The address of the proposer to register. /// @param executor The address of the executor contract associated with the proposer. - function registerProposer(address proposer, address executor) external { + function registerProposer(address proposerAccount, address executor) external { _checkCallerIsAdminExecutor(); - _proposers.register(proposer, executor); + _proposers.register(proposerAccount, executor); } - /// @notice Unregisters a proposer from the system. - /// @dev There must always be at least one proposer associated with the admin executor. If an attempt is made to - /// remove the last proposer assigned to the admin executor, the function will revert. - /// @param proposer The address of the proposer to unregister. - function unregisterProposer(address proposer) external { + /// @notice Updates the executor associated with a specified proposer. + /// @dev Ensures that at least one proposer remains assigned to the `adminExecutor` following the update. + /// Reverts if updating the proposer’s executor would leave the `adminExecutor` without any associated proposer. + /// @param proposerAccount The address of the proposer whose executor is being updated. + /// @param newExecutor The new executor address to assign to the proposer. + function setProposerExecutor(address proposerAccount, address newExecutor) external { _checkCallerIsAdminExecutor(); - _proposers.unregister(proposer); + _proposers.setProposerExecutor(proposerAccount, newExecutor); - /// @dev after the removal of the proposer, check that admin executor still belongs to some proposer - if (!_proposers.isExecutor(msg.sender)) { - revert UnownedAdminExecutor(); - } + /// @dev after update of the proposer, check that admin executor still belongs to some proposer + _proposers.checkRegisteredExecutor(TIMELOCK.getAdminExecutor()); } - /// @notice Checks whether the given `account` is a registered proposer. - /// @param account The address to check. - /// @return isProposer A boolean value indicating whether the `account` is a registered - /// proposer (`true`) or not (`false`). - function isProposer(address account) external view returns (bool) { - return _proposers.isProposer(account); + /// @notice Unregisters a proposer from the system. + /// @dev Ensures that at least one proposer remains associated with the `adminExecutor`. If an attempt is made to + /// remove the last proposer assigned to the `adminExecutor`, the function will revert. + /// @param proposerAccount The address of the proposer to unregister. + function unregisterProposer(address proposerAccount) external { + _checkCallerIsAdminExecutor(); + _proposers.unregister(proposerAccount); + + /// @dev after the removal of the proposer, check that admin executor still belongs to some proposer + _proposers.checkRegisteredExecutor(msg.sender); } - /// @notice Returns the proposer data if the given `account` is a registered proposer. - /// @param account The address of the proposer to retrieve information for. + /// @notice Returns the proposer data if the given `proposerAccount` is a registered proposer. + /// @param proposerAccount The address of the proposer to retrieve information for. /// @return proposer A Proposer struct containing the data of the registered proposer, including: /// - `account`: The address of the registered proposer. /// - `executor`: The address of the executor associated with the proposer. - function getProposer(address account) external view returns (Proposers.Proposer memory proposer) { - proposer = _proposers.getProposer(account); + function getProposer(address proposerAccount) external view returns (Proposers.Proposer memory proposer) { + proposer = _proposers.getProposer(proposerAccount); } /// @notice Returns the information about all registered proposers. @@ -396,12 +426,20 @@ contract DualGovernance is IDualGovernance { proposers = _proposers.getAllProposers(); } - /// @notice Checks whether the given `account` is associated with an executor contract in the system. - /// @param account The address to check. - /// @return isExecutor A boolean value indicating whether the `account` is a registered + /// @notice Checks whether the given `proposerAccount` is a registered proposer. + /// @param proposerAccount The address to check. + /// @return isProposer A boolean value indicating whether the `proposerAccount` is a registered + /// proposer (`true`) or not (`false`). + function isProposer(address proposerAccount) external view returns (bool) { + return _proposers.isRegisteredProposer(proposerAccount); + } + + /// @notice Checks whether the given `executor` address is associated with an executor contract in the system. + /// @param executor The address to check. + /// @return isExecutor A boolean value indicating whether the `executor` is a registered /// executor (`true`) or not (`false`). - function isExecutor(address account) external view returns (bool) { - return _proposers.isExecutor(account); + function isExecutor(address executor) external view returns (bool) { + return _proposers.isRegisteredExecutor(executor); } // --- @@ -426,21 +464,21 @@ contract DualGovernance is IDualGovernance { } /// @notice Sets the new address of the tiebreaker committee in the system. - /// @param tiebreakerCommittee The address of the new tiebreaker committee. - function setTiebreakerCommittee(address tiebreakerCommittee) external { + /// @param newTiebreakerCommittee The address of the new tiebreaker committee. + function setTiebreakerCommittee(address newTiebreakerCommittee) external { _checkCallerIsAdminExecutor(); - _tiebreaker.setTiebreakerCommittee(tiebreakerCommittee); + _tiebreaker.setTiebreakerCommittee(newTiebreakerCommittee); } /// @notice Sets the new value for the tiebreaker activation timeout. /// @dev If the Dual Governance system remains out of the `Normal` or `VetoCooldown` state for longer than /// the `tiebreakerActivationTimeout` duration, the tiebreaker committee is allowed to schedule /// submitted proposals. - /// @param tiebreakerActivationTimeout The new duration for the tiebreaker activation timeout. - function setTiebreakerActivationTimeout(Duration tiebreakerActivationTimeout) external { + /// @param newTiebreakerActivationTimeout The new duration for the tiebreaker activation timeout. + function setTiebreakerActivationTimeout(Duration newTiebreakerActivationTimeout) external { _checkCallerIsAdminExecutor(); _tiebreaker.setTiebreakerActivationTimeout( - MIN_TIEBREAKER_ACTIVATION_TIMEOUT, tiebreakerActivationTimeout, MAX_TIEBREAKER_ACTIVATION_TIMEOUT + MIN_TIEBREAKER_ACTIVATION_TIMEOUT, newTiebreakerActivationTimeout, MAX_TIEBREAKER_ACTIVATION_TIMEOUT ); } @@ -448,9 +486,9 @@ contract DualGovernance is IDualGovernance { /// @param sealable The address of the sealable contract to be resumed. function tiebreakerResumeSealable(address sealable) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); _tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.normalOrVetoCooldownExitedAt); - RESEAL_MANAGER.resume(sealable); + _resealer.resealManager.resume(sealable); } /// @notice Allows the tiebreaker committee to schedule for execution a submitted proposal when @@ -458,7 +496,7 @@ contract DualGovernance is IDualGovernance { /// @param proposalId The unique identifier of the proposal to be scheduled. function tiebreakerScheduleProposal(uint256 proposalId) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); _tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.normalOrVetoCooldownExitedAt); TIMELOCK.schedule(proposalId); } @@ -483,27 +521,38 @@ contract DualGovernance is IDualGovernance { /// the ResealManager contract. /// @param sealable The address of the sealable contract to be resealed. function resealSealable(address sealable) external { - _stateMachine.activateNextState(ESCROW_MASTER_COPY); - if (msg.sender != _resealCommittee) { - revert CallerIsNotResealCommittee(msg.sender); - } + _stateMachine.activateNextState(); if (_stateMachine.getPersistedState() == State.Normal) { revert ResealIsNotAllowedInNormalState(); } - RESEAL_MANAGER.reseal(sealable); + _resealer.checkCallerIsResealCommittee(); + _resealer.resealManager.reseal(sealable); } /// @notice Sets the address of the reseal committee. - /// @param resealCommittee The address of the new reseal committee. - function setResealCommittee(address resealCommittee) external { + /// @param newResealCommittee The address of the new reseal committee. + function setResealCommittee(address newResealCommittee) external { + _checkCallerIsAdminExecutor(); + _resealer.setResealCommittee(newResealCommittee); + } + + /// @notice Sets the address of the Reseal Manager. + /// @param newResealManager The address of the new Reseal Manager. + function setResealManager(IResealManager newResealManager) external { _checkCallerIsAdminExecutor(); + _resealer.setResealManager(newResealManager); + } - if (resealCommittee == _resealCommittee) { - revert InvalidResealCommittee(resealCommittee); - } - _resealCommittee = resealCommittee; + /// @notice Gets the address of the Reseal Manager. + /// @return resealManager The address of the Reseal Manager. + function getResealManager() external view returns (IResealManager) { + return _resealer.resealManager; + } - emit ResealCommitteeSet(resealCommittee); + /// @notice Gets the address of the reseal committee. + /// @return resealCommittee The address of the reseal committee. + function getResealCommittee() external view returns (address) { + return _resealer.resealCommittee; } // --- diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index e09c4e5b..2861c658 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -21,12 +21,6 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { using ExecutableProposals for ExecutableProposals.Context; using EmergencyProtection for EmergencyProtection.Context; - // --- - // Errors - // --- - - error CallerIsNotAdminExecutor(address value); - // --- // Sanity Check Parameters & Immutables // --- @@ -61,13 +55,6 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @notice The upper bound for the time the emergency protection mechanism can be activated. Duration public immutable MAX_EMERGENCY_PROTECTION_DURATION; - // --- - // Admin Executor Immutables - // --- - - /// @dev The address of the admin executor, authorized to manage the EmergencyProtectedTimelock instance. - address private immutable _ADMIN_EXECUTOR; - // --- // Aspects // --- @@ -91,14 +78,14 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { Duration afterSubmitDelay, Duration afterScheduleDelay ) { - _ADMIN_EXECUTOR = adminExecutor; - MIN_EXECUTION_DELAY = sanityCheckParams.minExecutionDelay; MAX_AFTER_SUBMIT_DELAY = sanityCheckParams.maxAfterSubmitDelay; MAX_AFTER_SCHEDULE_DELAY = sanityCheckParams.maxAfterScheduleDelay; MAX_EMERGENCY_MODE_DURATION = sanityCheckParams.maxEmergencyModeDuration; MAX_EMERGENCY_PROTECTION_DURATION = sanityCheckParams.maxEmergencyProtectionDuration; + _timelockState.setAdminExecutor(adminExecutor); + if (afterSubmitDelay > Durations.ZERO) { _timelockState.setAfterSubmitDelay(afterSubmitDelay, MAX_AFTER_SUBMIT_DELAY); } @@ -115,19 +102,12 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { // --- /// @notice Submits a new proposal to execute a series of calls through an executor. - /// @param proposer The address of the proposer submitting the proposal. /// @param executor The address of the executor contract that will execute the calls. /// @param calls An array of `ExternalCall` structs representing the calls to be executed. - /// @param metadata A string containing additional information about the proposal. /// @return newProposalId The id of the newly created proposal. - function submit( - address proposer, - address executor, - ExternalCall[] calldata calls, - string calldata metadata - ) external returns (uint256 newProposalId) { + function submit(address executor, ExternalCall[] calldata calls) external returns (uint256 newProposalId) { _timelockState.checkCallerIsGovernance(); - newProposalId = _proposals.submit(proposer, executor, calls, metadata); + newProposalId = _proposals.submit(executor, calls); } /// @notice Schedules a proposal for execution after a specified delay. @@ -157,15 +137,16 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @notice Updates the address of the governance contract. /// @param newGovernance The address of the new governance contract to be set. function setGovernance(address newGovernance) external { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); _timelockState.setGovernance(newGovernance); + _proposals.cancelAll(); } /// @notice Sets the delay required to pass from the submission of a proposal before it can be scheduled for execution. /// Ensures that the new delay value complies with the defined sanity check bounds. /// @param newAfterSubmitDelay The delay required before a submitted proposal can be scheduled. function setAfterSubmitDelay(Duration newAfterSubmitDelay) external { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); _timelockState.setAfterSubmitDelay(newAfterSubmitDelay, MAX_AFTER_SUBMIT_DELAY); _timelockState.checkExecutionDelay(MIN_EXECUTION_DELAY); } @@ -174,7 +155,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// Ensures that the new delay value complies with the defined sanity check bounds. /// @param newAfterScheduleDelay The delay required before a scheduled proposal can be executed. function setAfterScheduleDelay(Duration newAfterScheduleDelay) external { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); _timelockState.setAfterScheduleDelay(newAfterScheduleDelay, MAX_AFTER_SCHEDULE_DELAY); _timelockState.checkExecutionDelay(MIN_EXECUTION_DELAY); } @@ -183,7 +164,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @param executor The address of the executor contract. /// @param owner The address of the new owner. function transferExecutorOwnership(address executor, address owner) external { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); IOwnable(executor).transferOwnership(owner); } @@ -192,40 +173,40 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { // --- /// @notice Sets the emergency activation committee address. - /// @param emergencyActivationCommittee The address of the emergency activation committee. - function setEmergencyProtectionActivationCommittee(address emergencyActivationCommittee) external { - _checkCallerIsAdminExecutor(); - _emergencyProtection.setEmergencyActivationCommittee(emergencyActivationCommittee); + /// @param newEmergencyActivationCommittee The address of the emergency activation committee. + function setEmergencyProtectionActivationCommittee(address newEmergencyActivationCommittee) external { + _timelockState.checkCallerIsAdminExecutor(); + _emergencyProtection.setEmergencyActivationCommittee(newEmergencyActivationCommittee); } /// @notice Sets the emergency execution committee address. - /// @param emergencyExecutionCommittee The address of the emergency execution committee. - function setEmergencyProtectionExecutionCommittee(address emergencyExecutionCommittee) external { - _checkCallerIsAdminExecutor(); - _emergencyProtection.setEmergencyExecutionCommittee(emergencyExecutionCommittee); + /// @param newEmergencyExecutionCommittee The address of the emergency execution committee. + function setEmergencyProtectionExecutionCommittee(address newEmergencyExecutionCommittee) external { + _timelockState.checkCallerIsAdminExecutor(); + _emergencyProtection.setEmergencyExecutionCommittee(newEmergencyExecutionCommittee); } /// @notice Sets the emergency protection end date. - /// @param emergencyProtectionEndDate The timestamp of the emergency protection end date. - function setEmergencyProtectionEndDate(Timestamp emergencyProtectionEndDate) external { - _checkCallerIsAdminExecutor(); + /// @param newEmergencyProtectionEndDate The timestamp of the emergency protection end date. + function setEmergencyProtectionEndDate(Timestamp newEmergencyProtectionEndDate) external { + _timelockState.checkCallerIsAdminExecutor(); _emergencyProtection.setEmergencyProtectionEndDate( - emergencyProtectionEndDate, MAX_EMERGENCY_PROTECTION_DURATION + newEmergencyProtectionEndDate, MAX_EMERGENCY_PROTECTION_DURATION ); } /// @notice Sets the emergency mode duration. - /// @param emergencyModeDuration The duration of the emergency mode. - function setEmergencyModeDuration(Duration emergencyModeDuration) external { - _checkCallerIsAdminExecutor(); - _emergencyProtection.setEmergencyModeDuration(emergencyModeDuration, MAX_EMERGENCY_MODE_DURATION); + /// @param newEmergencyModeDuration The duration of the emergency mode. + function setEmergencyModeDuration(Duration newEmergencyModeDuration) external { + _timelockState.checkCallerIsAdminExecutor(); + _emergencyProtection.setEmergencyModeDuration(newEmergencyModeDuration, MAX_EMERGENCY_MODE_DURATION); } /// @notice Sets the emergency governance address. - /// @param emergencyGovernance The address of the emergency governance. - function setEmergencyGovernance(address emergencyGovernance) external { - _checkCallerIsAdminExecutor(); - _emergencyProtection.setEmergencyGovernance(emergencyGovernance); + /// @param newEmergencyGovernance The address of the emergency governance. + function setEmergencyGovernance(address newEmergencyGovernance) external { + _timelockState.checkCallerIsAdminExecutor(); + _emergencyProtection.setEmergencyGovernance(newEmergencyGovernance); } /// @notice Activates the emergency mode. @@ -247,7 +228,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { function deactivateEmergencyMode() external { _emergencyProtection.checkEmergencyMode({isActive: true}); if (!_emergencyProtection.isEmergencyModeDurationPassed()) { - _checkCallerIsAdminExecutor(); + _timelockState.checkCallerIsAdminExecutor(); } _emergencyProtection.deactivateEmergencyMode(); _proposals.cancelAll(); @@ -312,7 +293,7 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { /// @notice Returns the address of the admin executor. /// @return adminExecutor The address of the admin executor. function getAdminExecutor() external view returns (address) { - return _ADMIN_EXECUTOR; + return _timelockState.adminExecutor; } /// @notice Returns the configured delay duration required before a submitted proposal can be scheduled. @@ -388,12 +369,13 @@ contract EmergencyProtectedTimelock is IEmergencyProtectedTimelock { } // --- - // Internal Methods + // Admin Executor Methods // --- - function _checkCallerIsAdminExecutor() internal view { - if (msg.sender != _ADMIN_EXECUTOR) { - revert CallerIsNotAdminExecutor(msg.sender); - } + /// @notice Sets the address of the admin executor. + /// @param newAdminExecutor The address of the new admin executor. + function setAdminExecutor(address newAdminExecutor) external { + _timelockState.checkCallerIsAdminExecutor(); + _timelockState.setAdminExecutor(newAdminExecutor); } } diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index aa54f934..12d044e7 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -4,49 +4,27 @@ pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Duration} from "./types/Duration.sol"; -import {Timestamp} from "./types/Timestamp.sol"; import {ETHValue, ETHValues} from "./types/ETHValue.sol"; import {SharesValue, SharesValues} from "./types/SharesValue.sol"; import {PercentD16, PercentsD16} from "./types/PercentD16.sol"; -import {IEscrow} from "./interfaces/IEscrow.sol"; +import {IEscrowBase} from "./interfaces/IEscrowBase.sol"; +import {ISignallingEscrow} from "./interfaces/ISignallingEscrow.sol"; +import {IRageQuitEscrow} from "./interfaces/IRageQuitEscrow.sol"; + import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; -import {IWithdrawalQueue, WithdrawalRequestStatus} from "./interfaces/IWithdrawalQueue.sol"; +import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; -import {EscrowState} from "./libraries/EscrowState.sol"; +import {EscrowState, State} from "./libraries/EscrowState.sol"; import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalsBatchesQueue.sol"; import {HolderAssets, StETHAccounting, UnstETHAccounting, AssetsAccounting} from "./libraries/AssetsAccounting.sol"; -/// @notice Summary of the total locked assets in the Escrow. -/// @param stETHLockedShares The total number of stETH shares currently locked in the Escrow. -/// @param stETHClaimedETH The total amount of ETH claimed from the stETH shares locked in the Escrow. -/// @param unstETHUnfinalizedShares The total number of shares from unstETH NFTs that have not yet been marked as finalized. -/// @param unstETHFinalizedETH The total amount of ETH claimable from unstETH NFTs that have been marked as finalized. -struct LockedAssetsTotals { - uint256 stETHLockedShares; - uint256 stETHClaimedETH; - uint256 unstETHUnfinalizedShares; - uint256 unstETHFinalizedETH; -} - -/// @notice Summary of the assets locked in the Escrow by a specific vetoer. -/// @param stETHLockedShares The total number of stETH shares currently locked in the Escrow by the vetoer. -/// @param unstETHLockedShares The total number of unstETH shares currently locked in the Escrow by the vetoer. -/// @param unstETHIdsCount The total number of unstETH NFTs locked in the Escrow by the vetoer. -/// @param lastAssetsLockTimestamp The timestamp of the last time the vetoer locked stETH, wstETH, or unstETH in the Escrow. -struct VetoerState { - uint256 stETHLockedShares; - uint256 unstETHLockedShares; - uint256 unstETHIdsCount; - uint256 lastAssetsLockTimestamp; -} - /// @notice This contract is used to accumulate stETH, wstETH, unstETH, and withdrawn ETH from vetoers during the /// veto signalling and rage quit processes. /// @dev This contract is intended to be used behind a minimal proxy deployed by the DualGovernance contract. -contract Escrow is IEscrow { +contract Escrow is ISignallingEscrow, IRageQuitEscrow { using EscrowState for EscrowState.Context; using AssetsAccounting for AssetsAccounting.Context; using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.Context; @@ -54,13 +32,14 @@ contract Escrow is IEscrow { // --- // Errors // --- + error EmptyUnstETHIds(); error UnclaimedBatches(); - error UnexpectedUnstETHId(); error UnfinalizedUnstETHIds(); error NonProxyCallsForbidden(); error BatchesQueueIsNotClosed(); error InvalidBatchSize(uint256 size); + error InvalidFromUnstETHId(uint256 unstETHId); error CallerIsNotDualGovernance(address caller); error InvalidHintsLength(uint256 actual, uint256 expected); error InvalidETHSender(address actual, address expected); @@ -72,7 +51,7 @@ contract Escrow is IEscrow { /// @dev The lower limit for stETH transfers when requesting a withdrawal batch /// during the Rage Quit phase. For more details, see https://github.com/lidofinance/lido-dao/issues/442. /// The current value is chosen to ensure functionality over an extended period, spanning several decades. - uint256 private constant _MIN_TRANSFERRABLE_ST_ETH_AMOUNT = 8 wei; + uint256 public constant MIN_TRANSFERRABLE_ST_ETH_AMOUNT = 100 wei; // --- // Sanity Check Parameters & Immutables @@ -82,6 +61,9 @@ contract Escrow is IEscrow { /// the `Escrow.requestNextWithdrawalsBatch(batchSize)` method. uint256 public immutable MIN_WITHDRAWALS_BATCH_SIZE; + /// @notice The maximum duration that can be set as the minimum assets lock duration. + Duration public immutable MAX_MIN_ASSETS_LOCK_DURATION; + // --- // Dependencies Immutables // --- @@ -99,9 +81,11 @@ contract Escrow is IEscrow { // Implementation Immutables // --- - /// @dev Reference to the address of the implementation contract, used to distinguish whether the call - /// is made to the proxy or directly to the implementation. - address private immutable _SELF; + /// @notice The address of the implementation used for Signalling and Rage Quit escrows deployed + /// by the DualGovernance contract. + /// @dev This address is also used to distinguish whether the call is made to the proxy or directly + /// to the implementation. + IEscrowBase public immutable ESCROW_MASTER_COPY; /// @dev The address of the Dual Governance contract. IDualGovernance public immutable DUAL_GOVERNANCE; @@ -128,9 +112,10 @@ contract Escrow is IEscrow { IWstETH wstETH, IWithdrawalQueue withdrawalQueue, IDualGovernance dualGovernance, - uint256 minWithdrawalsBatchSize + uint256 minWithdrawalsBatchSize, + Duration maxMinAssetsLockDuration ) { - _SELF = address(this); + ESCROW_MASTER_COPY = this; DUAL_GOVERNANCE = dualGovernance; ST_ETH = stETH; @@ -138,13 +123,18 @@ contract Escrow is IEscrow { WITHDRAWAL_QUEUE = withdrawalQueue; MIN_WITHDRAWALS_BATCH_SIZE = minWithdrawalsBatchSize; + MAX_MIN_ASSETS_LOCK_DURATION = maxMinAssetsLockDuration; } + // --- + // Escrow Base + // --- + /// @notice Initializes the proxy instance with the specified minimum assets lock duration. /// @param minAssetsLockDuration The minimum duration that must pass from the last stETH, wstETH, or unstETH lock /// by the vetoer before they are allowed to unlock assets from the Escrow. function initialize(Duration minAssetsLockDuration) external { - if (address(this) == _SELF) { + if (this == ESCROW_MASTER_COPY) { revert NonProxyCallsForbidden(); } _checkCallerIsDualGovernance(); @@ -155,8 +145,14 @@ contract Escrow is IEscrow { ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); } + /// @notice Retrieves the current state of the Escrow. + /// @return State The current state of the Escrow. + function getEscrowState() external view returns (State) { + return _escrowState.state; + } + // --- - // Lock & Unlock stETH + // Signalling Escrow: Lock & Unlock stETH // --- /// @notice Locks the vetoer's specified `amount` of stETH in the Veto Signalling Escrow, thereby increasing @@ -189,7 +185,7 @@ contract Escrow is IEscrow { } // --- - // Lock & Unlock wstETH + // Signalling Escrow: Lock & Unlock wstETH // --- /// @notice Locks the vetoer's specified `amount` of wstETH in the Veto Signalling Escrow, thereby increasing @@ -223,7 +219,7 @@ contract Escrow is IEscrow { } // --- - // Lock & Unlock unstETH + // Signalling Escrow: Lock & Unlock unstETH // --- /// @notice Locks the specified unstETH NFTs, identified by their ids, in the Veto Signalling Escrow, thereby increasing @@ -237,7 +233,7 @@ contract Escrow is IEscrow { DUAL_GOVERNANCE.activateNextState(); _escrowState.checkSignallingEscrow(); - WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { @@ -287,7 +283,7 @@ contract Escrow is IEscrow { } // --- - // Start Rage Quit + // Signalling Escrow: Start Rage Quit // --- /// @notice Irreversibly converts the Signalling Escrow into the Rage Quit Escrow, allowing vetoers who have locked @@ -305,7 +301,107 @@ contract Escrow is IEscrow { } // --- - // Request Withdrawal Batches + // Signalling Escrow: Management + // --- + + /// @notice Sets the minimum duration that must elapse after the last stETH, wstETH, or unstETH lock + /// by a vetoer before they are permitted to unlock their assets from the Escrow. + /// @param newMinAssetsLockDuration The new minimum lock duration to be set. + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { + _checkCallerIsDualGovernance(); + _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration, MAX_MIN_ASSETS_LOCK_DURATION); + } + + // --- + // Signalling Escrow: Getters + // --- + + /// @notice Returns the current Rage Quit support value as a percentage. + /// @return rageQuitSupport The current Rage Quit support as a `PercentD16` value. + function getRageQuitSupport() external view returns (PercentD16) { + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + + uint256 finalizedETH = unstETHTotals.finalizedETH.toUint256(); + uint256 unfinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); + + return PercentsD16.fromFraction({ + numerator: ST_ETH.getPooledEthByShares(unfinalizedShares) + finalizedETH, + denominator: ST_ETH.totalSupply() + finalizedETH + }); + } + + /// @notice Returns the minimum duration that must elapse after the last stETH, wstETH, or unstETH lock + /// by a vetoer before they are permitted to unlock their assets from the Escrow. + function getMinAssetsLockDuration() external view returns (Duration minAssetsLockDuration) { + minAssetsLockDuration = _escrowState.minAssetsLockDuration; + } + + /// @notice Returns the state of locked assets for a specific vetoer. + /// @param vetoer The address of the vetoer whose locked asset state is being queried. + /// @return details A struct containing information about the vetoer's locked assets, including: + /// - `unstETHIdsCount`: The total number of unstETH NFTs locked by the vetoer. + /// - `stETHLockedShares`: The total number of stETH shares locked by the vetoer. + /// - `unstETHLockedShares`: The total number of unstETH shares locked by the vetoer. + /// - `lastAssetsLockTimestamp`: The timestamp of the last assets lock by the vetoer. + function getVetoerDetails(address vetoer) external view returns (VetoerDetails memory details) { + HolderAssets storage assets = _accounting.assets[vetoer]; + + details.unstETHIdsCount = assets.unstETHIds.length; + details.stETHLockedShares = assets.stETHLockedShares; + details.unstETHLockedShares = assets.unstETHLockedShares; + details.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp; + } + + // @notice Retrieves the unstETH NFT ids of the specified vetoer. + /// @param vetoer The address of the vetoer whose unstETH NFTs are being queried. + /// @return unstETHIds An array of unstETH NFT ids locked by the vetoer. + function getVetoerUnstETHIds(address vetoer) external view returns (uint256[] memory unstETHIds) { + unstETHIds = _accounting.assets[vetoer].unstETHIds; + } + + /// @notice Returns the total amounts of locked and claimed assets in the Escrow. + /// @return details A struct containing the total amounts of locked and claimed assets, including: + /// - `totalStETHClaimedETH`: The total amount of ETH claimed from locked stETH. + /// - `totalStETHLockedShares`: The total number of stETH shares currently locked in the Escrow. + /// - `totalUnstETHUnfinalizedShares`: The total number of shares from unstETH NFTs that have not yet been finalized. + /// - `totalUnstETHFinalizedETH`: The total amount of ETH from finalized unstETH NFTs. + function getSignallingEscrowDetails() external view returns (SignallingEscrowDetails memory details) { + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + details.totalStETHClaimedETH = stETHTotals.claimedETH; + details.totalStETHLockedShares = stETHTotals.lockedShares; + + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + details.totalUnstETHUnfinalizedShares = unstETHTotals.unfinalizedShares; + details.totalUnstETHFinalizedETH = unstETHTotals.finalizedETH; + } + + /// @notice Retrieves details of locked unstETH records for the given ids. + /// @param unstETHIds The array of ids for the unstETH records to retrieve. + /// @return unstETHDetails An array of `LockedUnstETHDetails` containing the details for each provided unstETH id. + /// + /// The details include: + /// - `id`: The id of the locked unstETH NFT. + /// - `status`: The current status of the unstETH record. + /// - `lockedBy`: The address that locked the unstETH record. + /// - `shares`: The number of shares associated with the locked unstETH. + /// - `claimableAmount`: The amount of claimable ETH contained in the unstETH. This value is 0 + /// until the unstETH is finalized or claimed. + function getLockedUnstETHDetails(uint256[] calldata unstETHIds) + external + view + returns (LockedUnstETHDetails[] memory unstETHDetails) + { + uint256 unstETHIdsCount = unstETHIds.length; + unstETHDetails = new LockedUnstETHDetails[](unstETHIdsCount); + + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + unstETHDetails[i] = _accounting.getLockedUnstETHDetails(unstETHIds[i]); + } + } + + // --- + // Rage Quit Escrow: Request Withdrawal Batches // --- /// @notice Creates unstETH NFTs from the stETH held in the Rage Quit Escrow via the WithdrawalQueue contract. @@ -327,7 +423,7 @@ contract Escrow is IEscrow { /// Using only `minStETHWithdrawalRequestAmount` is insufficient because it is an external variable /// that could be decreased independently. Introducing `minWithdrawableStETHAmount` provides /// an internal safeguard, enforcing a minimum threshold within the contract. - uint256 minWithdrawableStETHAmount = Math.max(_MIN_TRANSFERRABLE_ST_ETH_AMOUNT, minStETHWithdrawalRequestAmount); + uint256 minWithdrawableStETHAmount = Math.max(MIN_TRANSFERRABLE_ST_ETH_AMOUNT, minStETHWithdrawalRequestAmount); if (stETHRemaining < minWithdrawableStETHAmount) { return _batchesQueue.close(); @@ -349,7 +445,7 @@ contract Escrow is IEscrow { } // --- - // Claim Requested Withdrawal Batches + // Rage Quit Escrow: Claim Requested Withdrawal Batches // --- /// @notice Allows the claim of finalized withdrawal NFTs generated via the `Escrow.requestNextWithdrawalsBatch()` method. @@ -385,7 +481,7 @@ contract Escrow is IEscrow { } // --- - // Start Rage Quit Extension Delay + // Rage Quit Escrow: Start Rage Quit Extension Delay // --- /// @notice Initiates the Rage Quit Extension Period once all withdrawal batches have been claimed. @@ -398,15 +494,15 @@ contract Escrow is IEscrow { revert BatchesQueueIsNotClosed(); } - /// @dev This check is primarily required when only unstETH NFTs are locked in the Escrow - /// and there are no WithdrawalsBatches. In this scenario, the RageQuitExtensionPeriod can only begin - /// when the last locked unstETH id is finalized in the WithdrawalQueue. - /// When the WithdrawalsBatchesQueue is not empty, this invariant is maintained by the following: + /// @dev This check is required when only unstETH NFTs are locked in the Escrow and there are no WithdrawalsBatches. + /// In this scenario, the RageQuitExtensionPeriod can only begin when the last locked unstETH id is finalized + /// in the WithdrawalQueue. When the WithdrawalsBatchesQueue is not empty, this invariant is maintained by + /// the following: /// - Any locked unstETH during the VetoSignalling phase has an id less than any unstETH NFT created /// during the request for withdrawal batches. /// - Claiming the withdrawal batches requires the finalization of the unstETH with the given id. /// - The finalization of unstETH NFTs occurs in FIFO order. - if (_batchesQueue.getLastClaimedOrBoundaryUnstETHId() > WITHDRAWAL_QUEUE.getLastFinalizedRequestId()) { + if (_batchesQueue.getBoundaryUnstETHId() > WITHDRAWAL_QUEUE.getLastFinalizedRequestId()) { revert UnfinalizedUnstETHIds(); } @@ -418,7 +514,7 @@ contract Escrow is IEscrow { } // --- - // Claim Locked unstETH NFTs + // Rage Quit Escrow: Claim Locked unstETH NFTs // --- /// @notice Allows users to claim finalized unstETH NFTs locked in the Rage Quit Escrow contract. @@ -442,19 +538,7 @@ contract Escrow is IEscrow { } // --- - // Escrow Management - // --- - - /// @notice Sets the minimum duration that must elapse after the last stETH, wstETH, or unstETH lock - /// by a vetoer before they are permitted to unlock their assets from the Escrow. - /// @param newMinAssetsLockDuration The new minimum lock duration to be set. - function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { - _checkCallerIsDualGovernance(); - _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration); - } - - // --- - // Withdraw Logic + // Rage Quit Escrow: Withdraw Logic // --- /// @notice Allows the caller (i.e., `msg.sender`) to withdraw all stETH and wstETH they have previously locked @@ -481,55 +565,9 @@ contract Escrow is IEscrow { } // --- - // Getters + // Rage Quit Escrow: Getters // --- - /// @notice Returns the total amounts of locked and claimed assets in the Escrow. - /// @return totals A struct containing the total amounts of locked and claimed assets, including: - /// - `stETHClaimedETH`: The total amount of ETH claimed from locked stETH. - /// - `stETHLockedShares`: The total number of stETH shares currently locked in the Escrow. - /// - `unstETHUnfinalizedShares`: The total number of shares from unstETH NFTs that have not yet been finalized. - /// - `unstETHFinalizedETH`: The total amount of ETH from finalized unstETH NFTs. - function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory totals) { - StETHAccounting memory stETHTotals = _accounting.stETHTotals; - totals.stETHClaimedETH = stETHTotals.claimedETH.toUint256(); - totals.stETHLockedShares = stETHTotals.lockedShares.toUint256(); - - UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; - totals.unstETHUnfinalizedShares = unstETHTotals.unfinalizedShares.toUint256(); - totals.unstETHFinalizedETH = unstETHTotals.finalizedETH.toUint256(); - } - - /// @notice Returns the state of locked assets for a specific vetoer. - /// @param vetoer The address of the vetoer whose locked asset state is being queried. - /// @return state A struct containing information about the vetoer's locked assets, including: - /// - `stETHLockedShares`: The total number of stETH shares locked by the vetoer. - /// - `unstETHLockedShares`: The total number of unstETH shares locked by the vetoer. - /// - `unstETHIdsCount`: The total number of unstETH NFTs locked by the vetoer. - /// - `lastAssetsLockTimestamp`: The timestamp of the last assets lock by the vetoer. - function getVetoerState(address vetoer) external view returns (VetoerState memory state) { - HolderAssets storage assets = _accounting.assets[vetoer]; - - state.unstETHIdsCount = assets.unstETHIds.length; - state.stETHLockedShares = assets.stETHLockedShares.toUint256(); - state.unstETHLockedShares = assets.unstETHLockedShares.toUint256(); - state.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp.toSeconds(); - } - - // @notice Retrieves the unstETH NFT ids of the specified vetoer. - /// @param vetoer The address of the vetoer whose unstETH NFTs are being queried. - /// @return unstETHIds An array of unstETH NFT ids locked by the vetoer. - function getVetoerUnstETHIds(address vetoer) external view returns (uint256[] memory unstETHIds) { - unstETHIds = _accounting.assets[vetoer].unstETHIds; - } - - /// @notice Returns the total count of unstETH NFTs that have not been claimed yet. - /// @return unclaimedUnstETHIdsCount The total number of unclaimed unstETH NFTs. - function getUnclaimedUnstETHIdsCount() external view returns (uint256) { - _escrowState.checkRageQuitEscrow(); - return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); - } - /// @notice Retrieves the unstETH NFT ids of the next batch available for claiming. /// @param limit The maximum number of unstETH NFTs to return in the batch. /// @return unstETHIds An array of unstETH NFT ids available for the next withdrawal batch. @@ -546,38 +584,33 @@ contract Escrow is IEscrow { return _batchesQueue.isClosed(); } - /// @notice Returns whether the Rage Quit Extension Period has started. - /// @return isRageQuitExtensionPeriodStarted A boolean value indicating whether the Rage Quit Extension Period - /// has started (`true`) or not (`false`). - function isRageQuitExtensionPeriodStarted() external view returns (bool) { - return _escrowState.isRageQuitExtensionPeriodStarted(); - } - - /// @notice Returns the timestamp when the Rage Quit Extension Period started. - /// @return rageQuitExtensionPeriodStartedAt The timestamp when the Rage Quit Extension Period began. - function getRageQuitExtensionPeriodStartedAt() external view returns (Timestamp) { - return _escrowState.rageQuitExtensionPeriodStartedAt; - } - - /// @notice Returns the current Rage Quit support value as a percentage. - /// @return rageQuitSupport The current Rage Quit support as a `PercentD16` value. - function getRageQuitSupport() external view returns (PercentD16) { - StETHAccounting memory stETHTotals = _accounting.stETHTotals; - UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; - - uint256 finalizedETH = unstETHTotals.finalizedETH.toUint256(); - uint256 unfinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); - - return PercentsD16.fromFraction({ - numerator: ST_ETH.getPooledEthByShares(unfinalizedShares) + finalizedETH, - denominator: ST_ETH.totalSupply() + finalizedETH - }); + /// @notice Returns the total count of unstETH NFTs that have not been claimed yet. + /// @return unclaimedUnstETHIdsCount The total number of unclaimed unstETH NFTs. + function getUnclaimedUnstETHIdsCount() external view returns (uint256) { + _escrowState.checkRageQuitEscrow(); + return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); } /// @notice Returns whether the Rage Quit process has been finalized. /// @return A boolean value indicating whether the Rage Quit process has been finalized (`true`) or not (`false`). function isRageQuitFinalized() external view returns (bool) { - return _escrowState.isRageQuitEscrow() && _escrowState.isRageQuitExtensionPeriodPassed(); + _escrowState.checkRageQuitEscrow(); + return _escrowState.isRageQuitExtensionPeriodPassed(); + } + + /// @notice Retrieves details about the current state of the rage quit escrow. + /// @return details A `RageQuitEscrowDetails` struct containing the following fields: + /// - `isRageQuitExtensionPeriodStarted`: Indicates whether the rage quit extension period has started. + /// - `rageQuitEthWithdrawalsDelay`: The delay period for ETH withdrawals during rage quit. + /// - `rageQuitExtensionPeriodDuration`: The duration of the rage quit extension period. + /// - `rageQuitExtensionPeriodStartedAt`: The timestamp when the rage quit extension period started. + function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory details) { + _escrowState.checkRageQuitEscrow(); + + details.rageQuitEthWithdrawalsDelay = _escrowState.rageQuitEthWithdrawalsDelay; + details.rageQuitExtensionPeriodDuration = _escrowState.rageQuitExtensionPeriodDuration; + details.rageQuitExtensionPeriodStartedAt = _escrowState.rageQuitExtensionPeriodStartedAt; + details.isRageQuitExtensionPeriodStarted = _escrowState.isRageQuitExtensionPeriodStarted(); } // --- @@ -601,7 +634,7 @@ contract Escrow is IEscrow { uint256[] memory hints ) internal { if (fromUnstETHId != unstETHIds[0]) { - revert UnexpectedUnstETHId(); + revert InvalidFromUnstETHId(fromUnstETHId); } if (hints.length != unstETHIds.length) { diff --git a/contracts/Executor.sol b/contracts/Executor.sol index 895703e8..dd649fc5 100644 --- a/contracts/Executor.sol +++ b/contracts/Executor.sol @@ -10,22 +10,35 @@ import {IExternalExecutor} from "./interfaces/IExternalExecutor.sol"; /// @notice Allows the contract owner to execute external function calls on specified target contracts with /// possible value transfers. contract Executor is IExternalExecutor, Ownable { + // --- + // Events + // --- + + event ETHReceived(address sender, uint256 value); + event Executed(address indexed target, uint256 ethValue, bytes data, bytes returndata); + + // --- + // Constructor + // --- + constructor(address owner) Ownable(owner) {} + // --- + // Main Functionality + // --- + /// @notice Allows the contract owner to execute external function calls on target contracts, optionally transferring ether. /// @param target The address of the target contract on which to execute the function call. /// @param value The amount of ether (in wei) to send with the function call. /// @param payload The calldata for the function call. - /// @return result The data returned from the function call. - function execute( - address target, - uint256 value, - bytes calldata payload - ) external payable returns (bytes memory result) { + function execute(address target, uint256 value, bytes calldata payload) external payable { _checkOwner(); - result = Address.functionCallWithValue(target, payload, value); + bytes memory returndata = Address.functionCallWithValue(target, payload, value); + emit Executed(target, value, payload, returndata); } /// @notice Allows the contract to receive ether. - receive() external payable {} + receive() external payable { + emit ETHReceived(msg.sender, msg.value); + } } diff --git a/contracts/TimelockedGovernance.sol b/contracts/TimelockedGovernance.sol index 25f08fd3..348d927e 100644 --- a/contracts/TimelockedGovernance.sol +++ b/contracts/TimelockedGovernance.sol @@ -51,7 +51,8 @@ contract TimelockedGovernance is IGovernance { string calldata metadata ) external returns (uint256 proposalId) { _checkCallerIsGovernance(); - return TIMELOCK.submit(msg.sender, TIMELOCK.getAdminExecutor(), calls, metadata); + proposalId = TIMELOCK.submit(TIMELOCK.getAdminExecutor(), calls); + emit ProposalSubmitted(msg.sender, proposalId, metadata); } /// @notice Schedules a submitted proposal. diff --git a/contracts/committees/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol deleted file mode 100644 index f15ba95a..00000000 --- a/contracts/committees/EmergencyActivationCommittee.sol +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; - -import {Durations} from "../types/Duration.sol"; -import {Timestamp} from "../types/Timestamp.sol"; - -import {IEmergencyProtectedTimelock} from "../interfaces/IEmergencyProtectedTimelock.sol"; - -import {HashConsensus} from "./HashConsensus.sol"; - -/// @title Emergency Activation Committee Contract -/// @notice This contract allows a committee to approve and execute an emergency activation -/// @dev Inherits from HashConsensus to utilize voting and consensus mechanisms -contract EmergencyActivationCommittee is HashConsensus { - address public immutable EMERGENCY_PROTECTED_TIMELOCK; - - bytes32 private constant EMERGENCY_ACTIVATION_HASH = keccak256("EMERGENCY_ACTIVATE"); - - constructor( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address emergencyProtectedTimelock - ) HashConsensus(owner, Durations.ZERO) { - EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; - - _addMembers(committeeMembers, executionQuorum); - } - - /// @notice Approves the emergency activation by casting a vote - /// @dev Only callable by committee members - function approveActivateEmergencyMode() public { - _checkCallerIsMember(); - _vote(EMERGENCY_ACTIVATION_HASH, true); - } - - /// @notice Gets the current state of the emergency activation vote - /// @return support The number of votes in support of the activation - /// @return executionQuorum The required number of votes for execution - /// @return quorumAt The timestamp when the quorum was reached - /// @return isExecuted Whether the activation has been executed - function getActivateEmergencyModeState() - public - view - returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) - { - return _getHashState(EMERGENCY_ACTIVATION_HASH); - } - - /// @notice Executes the emergency activation if the quorum is reached - /// @dev Calls the emergencyActivate function on the Emergency Protected Timelock contract - function executeActivateEmergencyMode() external { - _markUsed(EMERGENCY_ACTIVATION_HASH); - Address.functionCall( - EMERGENCY_PROTECTED_TIMELOCK, - abi.encodeWithSelector(IEmergencyProtectedTimelock.activateEmergencyMode.selector) - ); - } -} diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol deleted file mode 100644 index 8f4288fa..00000000 --- a/contracts/committees/EmergencyExecutionCommittee.sol +++ /dev/null @@ -1,145 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; - -import {Durations} from "../types/Duration.sol"; -import {Timestamp} from "../types/Timestamp.sol"; - -import {IEmergencyProtectedTimelock} from "../interfaces/IEmergencyProtectedTimelock.sol"; - -import {HashConsensus} from "./HashConsensus.sol"; -import {ProposalsList} from "./ProposalsList.sol"; - -enum ProposalType { - EmergencyExecute, - EmergencyReset -} - -/// @title Emergency Execution Committee Contract -/// @notice This contract allows a committee to vote on and execute emergency proposals -/// @dev Inherits from HashConsensus for voting mechanisms and ProposalsList for proposal management -contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { - error ProposalDoesNotExist(uint256 proposalId); - - address public immutable EMERGENCY_PROTECTED_TIMELOCK; - - constructor( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address emergencyProtectedTimelock - ) HashConsensus(owner, Durations.ZERO) { - EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; - - _addMembers(committeeMembers, executionQuorum); - } - - // --- - // Emergency Execution - // --- - - /// @notice Votes on an emergency execution proposal - /// @dev Only callable by committee members - /// @param proposalId The ID of the proposal to vote on - /// @param _support Indicates whether the member supports the proposal execution - function voteEmergencyExecute(uint256 proposalId, bool _support) public { - _checkCallerIsMember(); - _checkProposalExists(proposalId); - (bytes memory proposalData, bytes32 key) = _encodeEmergencyExecute(proposalId); - _vote(key, _support); - _pushProposal(key, uint256(ProposalType.EmergencyExecute), proposalData); - } - - /// @notice Gets the current state of an emergency execution proposal - /// @param proposalId The ID of the proposal - /// @return support The number of votes in support of the proposal - /// @return executionQuorum The required number of votes for execution - /// @return quorumAt The timestamp when the quorum was reached - /// @return isExecuted Whether the proposal has been executed - function getEmergencyExecuteState(uint256 proposalId) - public - view - returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) - { - (, bytes32 key) = _encodeEmergencyExecute(proposalId); - return _getHashState(key); - } - - /// @notice Executes an approved emergency execution proposal - /// @param proposalId The ID of the proposal to execute - function executeEmergencyExecute(uint256 proposalId) public { - (, bytes32 key) = _encodeEmergencyExecute(proposalId); - _markUsed(key); - Address.functionCall( - EMERGENCY_PROTECTED_TIMELOCK, - abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyExecute.selector, proposalId) - ); - } - - /// @notice Checks if a proposal exists - /// @param proposalId The ID of the proposal to check - function _checkProposalExists(uint256 proposalId) internal view { - if ( - proposalId == 0 - || proposalId > IEmergencyProtectedTimelock(EMERGENCY_PROTECTED_TIMELOCK).getProposalsCount() - ) { - revert ProposalDoesNotExist(proposalId); - } - } - - /// @dev Encodes the proposal data and generates the proposal key for an emergency execution - /// @param proposalId The ID of the proposal to encode - /// @return proposalData The encoded proposal data - /// @return key The generated proposal key - function _encodeEmergencyExecute(uint256 proposalId) - private - pure - returns (bytes memory proposalData, bytes32 key) - { - proposalData = abi.encode(ProposalType.EmergencyExecute, proposalId); - key = keccak256(proposalData); - } - - // --- - // Governance reset - // --- - - /// @notice Approves an emergency reset proposal - /// @dev Only callable by committee members - function approveEmergencyReset() public { - _checkCallerIsMember(); - bytes32 proposalKey = _encodeEmergencyResetProposalKey(); - _vote(proposalKey, true); - _pushProposal(proposalKey, uint256(ProposalType.EmergencyReset), bytes("")); - } - - /// @notice Gets the current state of an emergency reset proposal - /// @return support The number of votes in support of the proposal - /// @return executionQuorum The required number of votes for execution - /// @return quorumAt The timestamp when the quorum was reached - /// @return isExecuted Whether the proposal has been executed - function getEmergencyResetState() - public - view - returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) - { - bytes32 proposalKey = _encodeEmergencyResetProposalKey(); - return _getHashState(proposalKey); - } - - /// @notice Executes an approved emergency reset proposal - function executeEmergencyReset() external { - bytes32 proposalKey = _encodeEmergencyResetProposalKey(); - _markUsed(proposalKey); - Address.functionCall( - EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyReset.selector) - ); - } - - /// @notice Encodes the proposal key for an emergency reset - /// @return The generated proposal key - function _encodeEmergencyResetProposalKey() internal pure returns (bytes32) { - return keccak256(abi.encode(ProposalType.EmergencyReset, bytes32(0))); - } -} diff --git a/contracts/committees/HashConsensus.sol b/contracts/committees/HashConsensus.sol index 0e825f7d..387616d7 100644 --- a/contracts/committees/HashConsensus.sol +++ b/contracts/committees/HashConsensus.sol @@ -104,14 +104,14 @@ abstract contract HashConsensus is Ownable { /// @notice Sets the timelock duration /// @dev Only callable by the owner - /// @param timelock The new timelock duration in seconds - function setTimelockDuration(Duration timelock) external { + /// @param newTimelock The new timelock duration in seconds + function setTimelockDuration(Duration newTimelock) external { _checkOwner(); - if (timelock == _timelockDuration) { - revert InvalidTimelockDuration(timelock); + if (newTimelock == _timelockDuration) { + revert InvalidTimelockDuration(newTimelock); } - _timelockDuration = timelock; - emit TimelockDurationSet(timelock); + _timelockDuration = newTimelock; + emit TimelockDurationSet(newTimelock); } /// @notice Gets the quorum value @@ -250,7 +250,9 @@ abstract contract HashConsensus is Ownable { /// @param newMembers The array of addresses to be added as new members. /// @param executionQuorum The minimum number of members required for executing certain operations. function _addMembers(address[] memory newMembers, uint256 executionQuorum) internal { - for (uint256 i = 0; i < newMembers.length; ++i) { + uint256 membersCount = newMembers.length; + + for (uint256 i = 0; i < membersCount; ++i) { if (newMembers[i] == address(0)) { revert InvalidMemberAccount(newMembers[i]); } @@ -270,7 +272,9 @@ abstract contract HashConsensus is Ownable { /// @param membersToRemove The array of addresses to be removed from the members list. /// @param executionQuorum The updated minimum number of members required for executing certain operations. function _removeMembers(address[] memory membersToRemove, uint256 executionQuorum) internal { - for (uint256 i = 0; i < membersToRemove.length; ++i) { + uint256 membersCount = membersToRemove.length; + + for (uint256 i = 0; i < membersCount; ++i) { if (!_members.remove(membersToRemove[i])) { revert AccountIsNotMember(membersToRemove[i]); } @@ -285,7 +289,9 @@ abstract contract HashConsensus is Ownable { /// @param hash The hash to check /// @return support The number of votes in support of the hash function _getSupport(bytes32 hash) internal view returns (uint256 support) { - for (uint256 i = 0; i < _members.length(); ++i) { + uint256 membersCount = _members.length(); + + for (uint256 i = 0; i < membersCount; ++i) { if (approves[_members.at(i)][hash]) { support++; } diff --git a/contracts/committees/ResealCommittee.sol b/contracts/committees/ResealCommittee.sol deleted file mode 100644 index f71008ce..00000000 --- a/contracts/committees/ResealCommittee.sol +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; - -import {Duration} from "../types/Duration.sol"; -import {Timestamp} from "../types/Timestamp.sol"; - -import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; - -import {HashConsensus} from "./HashConsensus.sol"; -import {ProposalsList} from "./ProposalsList.sol"; - -/// @title Reseal Committee Contract -/// @notice This contract allows a committee to vote on and execute resealing proposals -/// @dev Inherits from HashConsensus for voting mechanisms and ProposalsList for proposal management -contract ResealCommittee is HashConsensus, ProposalsList { - error InvalidSealable(address sealable); - - address public immutable DUAL_GOVERNANCE; - - mapping(bytes32 hash => uint256 nonce) private _resealNonces; - - constructor( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address dualGovernance, - Duration timelock - ) HashConsensus(owner, timelock) { - DUAL_GOVERNANCE = dualGovernance; - - _addMembers(committeeMembers, executionQuorum); - } - - /// @notice Votes on a reseal proposal - /// @dev Allows committee members to vote on resealing a sealed address - /// @param sealable The address to reseal - /// @param support Indicates whether the member supports the proposal - function voteReseal(address sealable, bool support) external { - _checkCallerIsMember(); - - if (sealable == address(0)) { - revert InvalidSealable(sealable); - } - - (bytes memory proposalData, bytes32 key) = _encodeResealProposal(sealable); - _vote(key, support); - _pushProposal(key, 0, proposalData); - } - - /// @notice Gets the current state of a reseal proposal - /// @dev Retrieves the state of the reseal proposal for a sealed address - /// @param sealable The addresses for the reseal proposal - /// @return support The number of votes in support of the proposal - /// @return executionQuorum The required number of votes for execution - /// @return quorumAt The timestamp when the quorum was reached - function getResealState(address sealable) - external - view - returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt) - { - (, bytes32 key) = _encodeResealProposal(sealable); - (support, executionQuorum, quorumAt,) = _getHashState(key); - } - - /// @notice Executes an approved reseal proposal - /// @dev Executes the reseal proposal by calling the reseal function on the Dual Governance contract - /// @param sealable The address to reseal - function executeReseal(address sealable) external { - (, bytes32 key) = _encodeResealProposal(sealable); - _markUsed(key); - - Address.functionCall(DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.resealSealable.selector, sealable)); - - bytes32 resealNonceHash = keccak256(abi.encode(sealable)); - _resealNonces[resealNonceHash]++; - } - - /// @notice Encodes a reseal proposal - /// @dev Internal function to encode the proposal data and generate the proposal key - /// @param sealable The address to reseal - /// @return data The encoded proposal data - /// @return key The generated proposal key - function _encodeResealProposal(address sealable) internal view returns (bytes memory data, bytes32 key) { - bytes32 resealNonceHash = keccak256(abi.encode(sealable)); - data = abi.encode(sealable, _resealNonces[resealNonceHash]); - key = keccak256(data); - } -} diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index 8333345a..336b1d82 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -1,11 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {IDualGovernanceConfigProvider} from "./IDualGovernanceConfigProvider.sol"; import {IGovernance} from "./IGovernance.sol"; +import {IResealManager} from "./IResealManager.sol"; import {ITiebreaker} from "./ITiebreaker.sol"; import {Timestamp} from "../types/Timestamp.sol"; import {Duration} from "../types/Duration.sol"; import {State} from "../libraries/DualGovernanceStateMachine.sol"; +import {Proposers} from "../libraries/Proposers.sol"; interface IDualGovernance is IGovernance, ITiebreaker { struct StateDetails { @@ -19,7 +22,32 @@ interface IDualGovernance is IGovernance, ITiebreaker { Duration vetoSignallingDuration; } + function MIN_TIEBREAKER_ACTIVATION_TIMEOUT() external view returns (Duration); + function MAX_TIEBREAKER_ACTIVATION_TIMEOUT() external view returns (Duration); + function MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT() external view returns (uint256); + + function canSubmitProposal() external view returns (bool); + function canCancelAllPendingProposals() external view returns (bool); function activateNextState() external; + function setConfigProvider(IDualGovernanceConfigProvider newConfigProvider) external; + function getConfigProvider() external view returns (IDualGovernanceConfigProvider); + function getVetoSignallingEscrow() external view returns (address); + function getRageQuitEscrow() external view returns (address); + function getPersistedState() external view returns (State persistedState); + function getEffectiveState() external view returns (State effectiveState); + function getStateDetails() external view returns (StateDetails memory stateDetails); + + function registerProposer(address proposer, address executor) external; + function setProposerExecutor(address proposerAccount, address newExecutor) external; + function unregisterProposer(address proposer) external; + function isProposer(address account) external view returns (bool); + function getProposer(address account) external view returns (Proposers.Proposer memory proposer); + function getProposers() external view returns (Proposers.Proposer[] memory proposers); + function isExecutor(address account) external view returns (bool); function resealSealable(address sealable) external; + function setResealCommittee(address newResealCommittee) external; + function setResealManager(IResealManager newResealManager) external; + function getResealManager() external view returns (IResealManager); + function getResealCommittee() external view returns (address); } diff --git a/contracts/interfaces/IEmergencyProtectedTimelock.sol b/contracts/interfaces/IEmergencyProtectedTimelock.sol index 8916d298..6b0b47fa 100644 --- a/contracts/interfaces/IEmergencyProtectedTimelock.sol +++ b/contracts/interfaces/IEmergencyProtectedTimelock.sol @@ -12,11 +12,25 @@ interface IEmergencyProtectedTimelock is ITimelock { Timestamp emergencyProtectionEndsAfter; } + function MAX_AFTER_SUBMIT_DELAY() external view returns (Duration); + function MAX_AFTER_SCHEDULE_DELAY() external view returns (Duration); + function MAX_EMERGENCY_MODE_DURATION() external view returns (Duration); + function MAX_EMERGENCY_PROTECTION_DURATION() external view returns (Duration); + + function setEmergencyProtectionActivationCommittee(address newEmergencyActivationCommittee) external; + function setEmergencyProtectionExecutionCommittee(address newEmergencyExecutionCommittee) external; + function setEmergencyProtectionEndDate(Timestamp newEmergencyProtectionEndDate) external; + function setEmergencyModeDuration(Duration newEmergencyModeDuration) external; + function setEmergencyGovernance(address newEmergencyGovernance) external; + function activateEmergencyMode() external; function emergencyExecute(uint256 proposalId) external; + function deactivateEmergencyMode() external; function emergencyReset() external; + function isEmergencyProtectionEnabled() external view returns (bool); + function isEmergencyModeActive() external view returns (bool); + function getEmergencyProtectionDetails() external view returns (EmergencyProtectionDetails memory details); function getEmergencyGovernance() external view returns (address emergencyGovernance); function getEmergencyActivationCommittee() external view returns (address committee); function getEmergencyExecutionCommittee() external view returns (address committee); - function getEmergencyProtectionDetails() external view returns (EmergencyProtectionDetails memory details); } diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol deleted file mode 100644 index 0542e47c..00000000 --- a/contracts/interfaces/IEscrow.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Duration} from "../types/Duration.sol"; -import {PercentD16} from "../types/PercentD16.sol"; - -interface IEscrow { - function initialize(Duration minAssetsLockDuration) external; - - function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitEthWithdrawalsDelay) external; - - function isRageQuitFinalized() external view returns (bool); - function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport); - function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; -} diff --git a/contracts/interfaces/IEscrowBase.sol b/contracts/interfaces/IEscrowBase.sol new file mode 100644 index 00000000..c1e8f22e --- /dev/null +++ b/contracts/interfaces/IEscrowBase.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; + +import {State} from "../libraries/EscrowState.sol"; + +interface IEscrowBase { + function ESCROW_MASTER_COPY() external view returns (IEscrowBase); + + function initialize(Duration minAssetsLockDuration) external; + + function getEscrowState() external view returns (State); +} diff --git a/contracts/interfaces/IExternalExecutor.sol b/contracts/interfaces/IExternalExecutor.sol index 6999aa53..466c927c 100644 --- a/contracts/interfaces/IExternalExecutor.sol +++ b/contracts/interfaces/IExternalExecutor.sol @@ -2,9 +2,5 @@ pragma solidity 0.8.26; interface IExternalExecutor { - function execute( - address target, - uint256 value, - bytes calldata payload - ) external payable returns (bytes memory result); + function execute(address target, uint256 value, bytes calldata payload) external payable; } diff --git a/contracts/interfaces/IGateSeal.sol b/contracts/interfaces/IGateSeal.sol deleted file mode 100644 index 44b0b2a0..00000000 --- a/contracts/interfaces/IGateSeal.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -interface IGateSeal { - function seal(address[] calldata sealables) external; -} diff --git a/contracts/interfaces/IGovernance.sol b/contracts/interfaces/IGovernance.sol index 51652b40..ebf31d6d 100644 --- a/contracts/interfaces/IGovernance.sol +++ b/contracts/interfaces/IGovernance.sol @@ -6,6 +6,8 @@ import {ITimelock} from "./ITimelock.sol"; import {ExternalCall} from "../libraries/ExternalCalls.sol"; interface IGovernance { + event ProposalSubmitted(address indexed proposerAccount, uint256 indexed proposalId, string metadata); + function TIMELOCK() external view returns (ITimelock); function submitProposal( ExternalCall[] calldata calls, diff --git a/contracts/interfaces/IRageQuitEscrow.sol b/contracts/interfaces/IRageQuitEscrow.sol new file mode 100644 index 00000000..6649db93 --- /dev/null +++ b/contracts/interfaces/IRageQuitEscrow.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamp} from "../types/Timestamp.sol"; + +import {IEscrowBase} from "./IEscrowBase.sol"; + +interface IRageQuitEscrow is IEscrowBase { + struct RageQuitEscrowDetails { + Duration rageQuitEthWithdrawalsDelay; + Duration rageQuitExtensionPeriodDuration; + Timestamp rageQuitExtensionPeriodStartedAt; + bool isRageQuitExtensionPeriodStarted; + } + + function requestNextWithdrawalsBatch(uint256 batchSize) external; + + function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external; + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external; + function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external; + + function startRageQuitExtensionPeriod() external; + + function withdrawETH() external; + function withdrawETH(uint256[] calldata unstETHIds) external; + + function isRageQuitFinalized() external view returns (bool); + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds); + function isWithdrawalsBatchesClosed() external view returns (bool); + function getUnclaimedUnstETHIdsCount() external view returns (uint256); + function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory); +} diff --git a/contracts/interfaces/ISignallingEscrow.sol b/contracts/interfaces/ISignallingEscrow.sol new file mode 100644 index 00000000..fcdfcc43 --- /dev/null +++ b/contracts/interfaces/ISignallingEscrow.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ETHValue} from "../types/ETHValue.sol"; +import {Duration} from "../types/Duration.sol"; +import {Timestamp} from "../types/Timestamp.sol"; +import {PercentD16} from "../types/PercentD16.sol"; +import {SharesValue} from "../types/SharesValue.sol"; +import {UnstETHRecordStatus} from "../libraries/AssetsAccounting.sol"; + +import {IEscrowBase} from "./IEscrowBase.sol"; + +interface ISignallingEscrow is IEscrowBase { + struct VetoerDetails { + uint256 unstETHIdsCount; + SharesValue stETHLockedShares; + SharesValue unstETHLockedShares; + Timestamp lastAssetsLockTimestamp; + } + + struct LockedUnstETHDetails { + uint256 id; + UnstETHRecordStatus status; + address lockedBy; + SharesValue shares; + ETHValue claimableAmount; + } + + struct SignallingEscrowDetails { + SharesValue totalStETHLockedShares; + ETHValue totalStETHClaimedETH; + SharesValue totalUnstETHUnfinalizedShares; + ETHValue totalUnstETHFinalizedETH; + } + + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares); + function unlockStETH() external returns (uint256 unlockedStETHShares); + + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares); + function unlockWstETH() external returns (uint256 wstETHUnlocked); + + function lockUnstETH(uint256[] memory unstETHIds) external; + function unlockUnstETH(uint256[] memory unstETHIds) external; + + function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external; + + function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitEthWithdrawalsDelay) external; + + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; + + function getRageQuitSupport() external view returns (PercentD16); + function getMinAssetsLockDuration() external view returns (Duration); + + function getVetoerDetails(address vetoer) external view returns (VetoerDetails memory); + function getVetoerUnstETHIds(address vetoer) external view returns (uint256[] memory); + function getSignallingEscrowDetails() external view returns (SignallingEscrowDetails memory); + + function getLockedUnstETHDetails(uint256[] calldata unstETHIds) + external + view + returns (LockedUnstETHDetails[] memory); +} diff --git a/contracts/interfaces/ITiebreaker.sol b/contracts/interfaces/ITiebreaker.sol index 5f6c535b..9d4e89fa 100644 --- a/contracts/interfaces/ITiebreaker.sol +++ b/contracts/interfaces/ITiebreaker.sol @@ -13,8 +13,9 @@ interface ITiebreaker { function addTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external; function removeTiebreakerSealableWithdrawalBlocker(address sealableWithdrawalBlocker) external; - function setTiebreakerCommittee(address tiebreakerCommittee) external; - function setTiebreakerActivationTimeout(Duration tiebreakerActivationTimeout) external; + function setTiebreakerCommittee(address newTiebreakerCommittee) external; + function setTiebreakerActivationTimeout(Duration newTiebreakerActivationTimeout) external; function tiebreakerScheduleProposal(uint256 proposalId) external; + function getTiebreakerDetails() external view returns (TiebreakerDetails memory tiebreakerState); function tiebreakerResumeSealable(address sealable) external; } diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index ed4ba61d..234a000d 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {Duration} from "../types/Duration.sol"; import {Timestamp} from "../types/Timestamp.sol"; import {ExternalCall} from "../libraries/ExternalCalls.sol"; @@ -15,12 +16,7 @@ interface ITimelock { ProposalStatus status; } - function submit( - address proposer, - address executor, - ExternalCall[] calldata calls, - string calldata metadata - ) external returns (uint256 newProposalId); + function submit(address executor, ExternalCall[] calldata calls) external returns (uint256 newProposalId); function schedule(uint256 proposalId) external; function execute(uint256 proposalId) external; function cancelAllNonExecutedProposals() external; @@ -29,6 +25,7 @@ interface ITimelock { function canExecute(uint256 proposalId) external view returns (bool); function getAdminExecutor() external view returns (address); + function setAdminExecutor(address newAdminExecutor) external; function getGovernance() external view returns (address); function setGovernance(address newGovernance) external; @@ -37,5 +34,12 @@ interface ITimelock { view returns (ProposalDetails memory proposalDetails, ExternalCall[] memory calls); function getProposalDetails(uint256 proposalId) external view returns (ProposalDetails memory proposalDetails); + function getProposalCalls(uint256 proposalId) external view returns (ExternalCall[] memory calls); function getProposalsCount() external view returns (uint256 count); + + function getAfterSubmitDelay() external view returns (Duration); + function getAfterScheduleDelay() external view returns (Duration); + function setAfterSubmitDelay(Duration newAfterSubmitDelay) external; + function setAfterScheduleDelay(Duration newAfterScheduleDelay) external; + function transferExecutorOwnership(address executor, address owner) external; } diff --git a/contracts/interfaces/IWithdrawalQueue.sol b/contracts/interfaces/IWithdrawalQueue.sol index 3ec50762..8d54ba2d 100644 --- a/contracts/interfaces/IWithdrawalQueue.sol +++ b/contracts/interfaces/IWithdrawalQueue.sol @@ -3,16 +3,16 @@ pragma solidity 0.8.26; import {IERC721} from "@openzeppelin/contracts/interfaces/IERC721.sol"; -struct WithdrawalRequestStatus { - uint256 amountOfStETH; - uint256 amountOfShares; - address owner; - uint256 timestamp; - bool isFinalized; - bool isClaimed; -} - interface IWithdrawalQueue is IERC721 { + struct WithdrawalRequestStatus { + uint256 amountOfStETH; + uint256 amountOfShares; + address owner; + uint256 timestamp; + bool isFinalized; + bool isClaimed; + } + function MIN_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256); function MAX_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256); diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index 04472694..214430a4 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -7,7 +7,8 @@ import {Timestamps, Timestamp} from "../types/Timestamp.sol"; import {SharesValue, SharesValues} from "../types/SharesValue.sol"; import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; -import {WithdrawalRequestStatus} from "../interfaces/IWithdrawalQueue.sol"; +import {IWithdrawalQueue} from "../interfaces/IWithdrawalQueue.sol"; +import {ISignallingEscrow} from "../interfaces/ISignallingEscrow.sol"; /// @notice Tracks the stETH and unstETH tokens associated with users. /// @param stETHLockedShares Total number of stETH shares held by the user. @@ -223,7 +224,7 @@ library AssetsAccounting { Context storage self, address holder, uint256[] memory unstETHIds, - WithdrawalRequestStatus[] memory statuses + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses ) internal { assert(unstETHIds.length == statuses.length); @@ -345,13 +346,24 @@ library AssetsAccounting { // Getters // --- - function getLockedAssetsTotals(Context storage self) - internal - view - returns (SharesValue unfinalizedShares, ETHValue finalizedETH) - { - finalizedETH = self.unstETHTotals.finalizedETH; - unfinalizedShares = self.stETHTotals.lockedShares + self.unstETHTotals.unfinalizedShares; + /// @notice Retrieves details of locked unstETH record for the given id. + /// @param unstETHId The id for the locked unstETH record to retrieve. + /// @return unstETHDetails A `LockedUnstETHDetails` struct containing the details for provided unstETH id. + function getLockedUnstETHDetails( + Context storage self, + uint256 unstETHId + ) internal view returns (ISignallingEscrow.LockedUnstETHDetails memory unstETHDetails) { + UnstETHRecord memory unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.status == UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.NotLocked); + } + + unstETHDetails.id = unstETHId; + unstETHDetails.status = unstETHRecord.status; + unstETHDetails.lockedBy = unstETHRecord.lockedBy; + unstETHDetails.shares = unstETHRecord.shares; + unstETHDetails.claimableAmount = unstETHRecord.claimableAmount; } // --- @@ -384,7 +396,7 @@ library AssetsAccounting { Context storage self, address holder, uint256 unstETHId, - WithdrawalRequestStatus memory status + IWithdrawalQueue.WithdrawalRequestStatus memory status ) private returns (SharesValue shares) { if (status.isFinalized) { revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.Finalized); diff --git a/contracts/libraries/DualGovernanceConfig.sol b/contracts/libraries/DualGovernanceConfig.sol index 3a6eff80..f4f95f5c 100644 --- a/contracts/libraries/DualGovernanceConfig.sol +++ b/contracts/libraries/DualGovernanceConfig.sol @@ -90,26 +90,26 @@ library DualGovernanceConfig { } } - /// @notice Determines whether the first seal Rage Quit support threshold has been exceeded. + /// @notice Determines whether the first seal Rage Quit support threshold has been reached. /// @param self The configuration context. /// @param rageQuitSupport The current Rage Quit support level. - /// @return bool A boolean indicating whether the Rage Quit support level exceeds the first seal threshold. - function isFirstSealRageQuitSupportCrossed( + /// @return bool A boolean indicating whether the Rage Quit support level reaches the first seal threshold. + function isFirstSealRageQuitSupportReached( Context memory self, PercentD16 rageQuitSupport ) internal pure returns (bool) { - return rageQuitSupport > self.firstSealRageQuitSupport; + return rageQuitSupport >= self.firstSealRageQuitSupport; } - /// @notice Determines whether the second seal Rage Quit support threshold has been exceeded. + /// @notice Determines whether the second seal Rage Quit support threshold has been reached. /// @param self The configuration context. /// @param rageQuitSupport The current Rage Quit support level. - /// @return bool A boolean indicating whether the Rage Quit support level exceeds the second seal threshold. - function isSecondSealRageQuitSupportCrossed( + /// @return bool A boolean indicating whether the Rage Quit support level reaches the second seal threshold. + function isSecondSealRageQuitSupportReached( Context memory self, PercentD16 rageQuitSupport ) internal pure returns (bool) { - return rageQuitSupport > self.secondSealRageQuitSupport; + return rageQuitSupport >= self.secondSealRageQuitSupport; } /// @notice Determines whether the VetoSignalling duration has passed based on the current time. @@ -176,7 +176,7 @@ library DualGovernanceConfig { Duration vetoSignallingMinDuration = self.vetoSignallingMinDuration; Duration vetoSignallingMaxDuration = self.vetoSignallingMaxDuration; - if (rageQuitSupport <= firstSealRageQuitSupport) { + if (rageQuitSupport < firstSealRageQuitSupport) { return Durations.ZERO; } diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 3233d17a..d384fc22 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -7,7 +7,9 @@ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Duration} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; -import {IEscrow} from "../interfaces/IEscrow.sol"; +import {IEscrowBase} from "../interfaces/IEscrowBase.sol"; +import {ISignallingEscrow} from "../interfaces/ISignallingEscrow.sol"; +import {IRageQuitEscrow} from "../interfaces/IRageQuitEscrow.sol"; import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; import {IDualGovernanceConfigProvider} from "../interfaces/IDualGovernanceConfigProvider.sol"; @@ -15,7 +17,7 @@ import {DualGovernanceConfig} from "./DualGovernanceConfig.sol"; import {DualGovernanceStateTransitions} from "./DualGovernanceStateTransitions.sol"; /// @notice Enum describing the state of the Dual Governance State Machine -/// @param Unset The initial (uninitialized) state of the Dual Governance State Machine. The state machine cannot +/// @param NotInitialized The initial (uninitialized) state of the Dual Governance State Machine. The state machine cannot /// operate in this state and must be initialized before use. /// @param Normal The default state where the system is expected to remain most of the time. In this state, proposals /// can be both submitted and scheduled for execution. @@ -27,10 +29,10 @@ import {DualGovernanceStateTransitions} from "./DualGovernanceStateTransitions.s /// @param VetoCooldown A state where the DAO can execute non-cancelled proposals but is prohibited from submitting /// new proposals. /// @param RageQuit Represents the process where users opting to leave the protocol can withdraw their funds. This state -/// is triggered when the Second Seal Threshold is crossed. During this state, the scheduling of proposals for +/// is triggered when the Second Seal Threshold is reached. During this state, the scheduling of proposals for /// execution is forbidden, but new proposals can still be submitted. enum State { - Unset, + NotInitialized, Normal, VetoSignalling, VetoSignallingDeactivation, @@ -66,7 +68,7 @@ library DualGovernanceStateMachine { /// @dev slot 0: [48..87] Timestamp vetoSignallingActivatedAt; /// @dev slot 0: [88..247] - IEscrow signallingEscrow; + ISignallingEscrow signallingEscrow; /// @dev slot 0: [248..255] uint8 rageQuitRound; /// @dev slot 1: [0..39] @@ -74,7 +76,7 @@ library DualGovernanceStateMachine { /// @dev slot 1: [40..79] Timestamp normalOrVetoCooldownExitedAt; /// @dev slot 1: [80..239] - IEscrow rageQuitEscrow; + IRageQuitEscrow rageQuitEscrow; /// @dev slot 2: [0..159] IDualGovernanceConfigProvider configProvider; } @@ -90,7 +92,7 @@ library DualGovernanceStateMachine { // Events // --- - event NewSignallingEscrowDeployed(IEscrow indexed escrow); + event NewSignallingEscrowDeployed(ISignallingEscrow indexed escrow); event DualGovernanceStateChanged(State indexed from, State indexed to, Context state); event ConfigProviderSet(IDualGovernanceConfigProvider newConfigProvider); @@ -114,9 +116,9 @@ library DualGovernanceStateMachine { function initialize( Context storage self, IDualGovernanceConfigProvider configProvider, - IEscrow escrowMasterCopy + IEscrowBase escrowMasterCopy ) internal { - if (self.state != State.Unset) { + if (self.state != State.NotInitialized) { revert AlreadyInitialized(); } @@ -128,7 +130,7 @@ library DualGovernanceStateMachine { DualGovernanceConfig.Context memory config = configProvider.getDualGovernanceConfig(); _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); - emit DualGovernanceStateChanged(State.Unset, State.Normal, self); + emit DualGovernanceStateChanged(State.NotInitialized, State.Normal, self); } /// @notice Executes a state transition for the Dual Governance State Machine, if applicable. @@ -137,9 +139,7 @@ library DualGovernanceStateMachine { /// `escrowMasterCopy` as the implementation for the minimal proxy, while the previous Signalling Escrow /// instance is converted into the RageQuit escrow. /// @param self The context of the Dual Governance State Machine. - /// @param escrowMasterCopy The address of the master copy used as the implementation for the minimal proxy - /// to deploy a new instance of the Signalling Escrow. - function activateNextState(Context storage self, IEscrow escrowMasterCopy) internal { + function activateNextState(Context storage self) internal { DualGovernanceConfig.Context memory config = getDualGovernanceConfig(self); (State currentState, State newState) = self.getStateTransition(config); @@ -156,7 +156,7 @@ library DualGovernanceStateMachine { self.normalOrVetoCooldownExitedAt = newStateEnteredAt; } - if (newState == State.Normal && self.rageQuitRound != 0) { + if (newState == State.VetoCooldown && self.rageQuitRound != 0) { self.rageQuitRound = 0; } else if (newState == State.VetoSignalling) { if (currentState == State.VetoSignallingDeactivation) { @@ -165,7 +165,7 @@ library DualGovernanceStateMachine { self.vetoSignallingActivatedAt = newStateEnteredAt; } } else if (newState == State.RageQuit) { - IEscrow signallingEscrow = self.signallingEscrow; + ISignallingEscrow signallingEscrow = self.signallingEscrow; uint256 currentRageQuitRound = self.rageQuitRound; @@ -177,8 +177,8 @@ library DualGovernanceStateMachine { signallingEscrow.startRageQuit( config.rageQuitExtensionPeriodDuration, config.calcRageQuitWithdrawalsDelay(newRageQuitRound) ); - self.rageQuitEscrow = signallingEscrow; - _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + self.rageQuitEscrow = IRageQuitEscrow(address(signallingEscrow)); + _deployNewSignallingEscrow(self, signallingEscrow.ESCROW_MASTER_COPY(), config.minAssetsLockDuration); } emit DualGovernanceStateChanged(currentState, newState, self); @@ -190,11 +190,14 @@ library DualGovernanceStateMachine { function setConfigProvider(Context storage self, IDualGovernanceConfigProvider newConfigProvider) internal { _setConfigProvider(self, newConfigProvider); + ISignallingEscrow signallingEscrow = self.signallingEscrow; + Duration newMinAssetsLockDuration = newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration; + /// @dev minAssetsLockDuration is stored as a storage variable in the Signalling Escrow instance. /// To synchronize the new value with the current Signalling Escrow, it must be manually updated. - self.signallingEscrow.setMinAssetsLockDuration( - newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration - ); + if (signallingEscrow.getMinAssetsLockDuration() != newMinAssetsLockDuration) { + signallingEscrow.setMinAssetsLockDuration(newMinAssetsLockDuration); + } } // --- @@ -317,10 +320,10 @@ library DualGovernanceStateMachine { function _deployNewSignallingEscrow( Context storage self, - IEscrow escrowMasterCopy, + IEscrowBase escrowMasterCopy, Duration minAssetsLockDuration ) private { - IEscrow newSignallingEscrow = IEscrow(Clones.clone(address(escrowMasterCopy))); + ISignallingEscrow newSignallingEscrow = ISignallingEscrow(Clones.clone(address(escrowMasterCopy))); newSignallingEscrow.initialize(minAssetsLockDuration); self.signallingEscrow = newSignallingEscrow; emit NewSignallingEscrowDeployed(newSignallingEscrow); diff --git a/contracts/libraries/DualGovernanceStateTransitions.sol b/contracts/libraries/DualGovernanceStateTransitions.sol index f8a58184..b7b78dc5 100644 --- a/contracts/libraries/DualGovernanceStateTransitions.sol +++ b/contracts/libraries/DualGovernanceStateTransitions.sol @@ -48,7 +48,7 @@ library DualGovernanceStateTransitions { DualGovernanceStateMachine.Context storage self, DualGovernanceConfig.Context memory config ) private view returns (State) { - return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + return config.isFirstSealRageQuitSupportReached(self.signallingEscrow.getRageQuitSupport()) ? State.VetoSignalling : State.Normal; } @@ -63,7 +63,7 @@ library DualGovernanceStateTransitions { return State.VetoSignalling; } - if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + if (config.isSecondSealRageQuitSupportReached(rageQuitSupport)) { return State.RageQuit; } @@ -82,7 +82,7 @@ library DualGovernanceStateTransitions { return State.VetoSignalling; } - if (config.isSecondSealRageQuitSupportCrossed(rageQuitSupport)) { + if (config.isSecondSealRageQuitSupportReached(rageQuitSupport)) { return State.RageQuit; } @@ -100,7 +100,7 @@ library DualGovernanceStateTransitions { if (!config.isVetoCooldownDurationPassed(self.enteredAt)) { return State.VetoCooldown; } - return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + return config.isFirstSealRageQuitSupportReached(self.signallingEscrow.getRageQuitSupport()) ? State.VetoSignalling : State.Normal; } @@ -112,7 +112,7 @@ library DualGovernanceStateTransitions { if (!self.rageQuitEscrow.isRageQuitFinalized()) { return State.RageQuit; } - return config.isFirstSealRageQuitSupportCrossed(self.signallingEscrow.getRageQuitSupport()) + return config.isFirstSealRageQuitSupportReached(self.signallingEscrow.getRageQuitSupport()) ? State.VetoSignalling : State.VetoCooldown; } diff --git a/contracts/libraries/EmergencyProtection.sol b/contracts/libraries/EmergencyProtection.sol index b3df9833..4543a79a 100644 --- a/contracts/libraries/EmergencyProtection.sol +++ b/contracts/libraries/EmergencyProtection.sol @@ -22,7 +22,7 @@ library EmergencyProtection { error InvalidEmergencyExecutionCommittee(address committee); error InvalidEmergencyModeDuration(Duration value); error InvalidEmergencyProtectionEndDate(Timestamp value); - error UnexpectedEmergencyModeState(bool value); + error UnexpectedEmergencyModeState(bool state); // --- // Events @@ -197,7 +197,7 @@ library EmergencyProtection { /// @param isActive The expected value of the emergency mode. function checkEmergencyMode(Context storage self, bool isActive) internal view { if (isEmergencyModeActive(self) != isActive) { - revert UnexpectedEmergencyModeState(isActive); + revert UnexpectedEmergencyModeState(!isActive); } } diff --git a/contracts/libraries/EscrowState.sol b/contracts/libraries/EscrowState.sol index 9fb30ff3..45fa5be4 100644 --- a/contracts/libraries/EscrowState.sol +++ b/contracts/libraries/EscrowState.sol @@ -26,7 +26,7 @@ library EscrowState { // --- error ClaimingIsFinished(); - error UnexpectedState(State value); + error UnexpectedEscrowState(State state); error EthWithdrawalsDelayNotPassed(); error RageQuitExtensionPeriodNotStarted(); error InvalidMinAssetsLockDuration(Duration newMinAssetsLockDuration); @@ -103,8 +103,16 @@ library EscrowState { /// @notice Sets the minimum assets lock duration. /// @param self The context of the Escrow State library. /// @param newMinAssetsLockDuration The new minimum assets lock duration. - function setMinAssetsLockDuration(Context storage self, Duration newMinAssetsLockDuration) internal { - if (self.minAssetsLockDuration == newMinAssetsLockDuration) { + /// @param maxMinAssetsLockDuration Sanity check for max assets lock duration. + function setMinAssetsLockDuration( + Context storage self, + Duration newMinAssetsLockDuration, + Duration maxMinAssetsLockDuration + ) internal { + if ( + self.minAssetsLockDuration == newMinAssetsLockDuration + || newMinAssetsLockDuration > maxMinAssetsLockDuration + ) { revert InvalidMinAssetsLockDuration(newMinAssetsLockDuration); } _setMinAssetsLockDuration(self, newMinAssetsLockDuration); @@ -184,7 +192,7 @@ library EscrowState { /// @param state The expected state. function _checkState(Context storage self, State state) private view { if (self.state != state) { - revert UnexpectedState(state); + revert UnexpectedEscrowState(self.state); } } diff --git a/contracts/libraries/ExecutableProposals.sol b/contracts/libraries/ExecutableProposals.sol index 49c38e12..e385f667 100644 --- a/contracts/libraries/ExecutableProposals.sol +++ b/contracts/libraries/ExecutableProposals.sol @@ -35,8 +35,6 @@ enum Status { /// @notice Manages a collection of proposals with associated external calls stored as Proposal struct. /// Proposals are uniquely identified by sequential ids, starting from one. library ExecutableProposals { - using ExternalCalls for ExternalCall[]; - // --- // Data Types // --- @@ -81,9 +79,7 @@ library ExecutableProposals { // --- error EmptyCalls(); - error ProposalNotFound(uint256 proposalId); - error ProposalNotScheduled(uint256 proposalId); - error ProposalNotSubmitted(uint256 proposalId); + error UnexpectedProposalStatus(uint256 proposalId, Status status); error AfterSubmitDelayNotPassed(uint256 proposalId); error AfterScheduleDelayNotPassed(uint256 proposalId); @@ -91,11 +87,9 @@ library ExecutableProposals { // Events // --- - event ProposalSubmitted( - uint256 indexed id, address indexed proposer, address indexed executor, ExternalCall[] calls, string metadata - ); + event ProposalSubmitted(uint256 indexed id, address indexed executor, ExternalCall[] calls); event ProposalScheduled(uint256 indexed id); - event ProposalExecuted(uint256 indexed id, bytes[] callResults); + event ProposalExecuted(uint256 indexed id); event ProposalsCancelledTill(uint256 proposalId); // --- @@ -104,17 +98,13 @@ library ExecutableProposals { /// @notice Submits a new proposal with the specified executor and external calls. /// @param self The context of the Executable Proposal library. - /// @param proposer The address of the proposer submitting the proposal. /// @param executor The address authorized to execute the proposal. /// @param calls The list of external calls to include in the proposal. - /// @param metadata Metadata describing the proposal. /// @return newProposalId The id of the newly submitted proposal. function submit( Context storage self, - address proposer, address executor, - ExternalCall[] memory calls, - string memory metadata + ExternalCall[] memory calls ) internal returns (uint256 newProposalId) { if (calls.length == 0) { revert EmptyCalls(); @@ -133,7 +123,7 @@ library ExecutableProposals { newProposal.calls.push(calls[i]); } - emit ProposalSubmitted(newProposalId, proposer, executor, calls, metadata); + emit ProposalSubmitted(newProposalId, executor, calls); } /// @notice Marks a previously submitted proposal as scheduled for execution if the required delay period @@ -143,19 +133,21 @@ library ExecutableProposals { /// @param afterSubmitDelay The required delay duration after submission before the proposal can be scheduled. /// function schedule(Context storage self, uint256 proposalId, Duration afterSubmitDelay) internal { - ProposalData memory proposalState = self.proposals[proposalId].data; + ProposalData memory proposalData = self.proposals[proposalId].data; + + _checkProposalNotCancelled(self, proposalId, proposalData); - if (!_isProposalSubmitted(self, proposalId, proposalState)) { - revert ProposalNotSubmitted(proposalId); + if (proposalData.status != Status.Submitted) { + revert UnexpectedProposalStatus(proposalId, proposalData.status); } - if (afterSubmitDelay.addTo(proposalState.submittedAt) > Timestamps.now()) { + if (afterSubmitDelay.addTo(proposalData.submittedAt) > Timestamps.now()) { revert AfterSubmitDelayNotPassed(proposalId); } - proposalState.status = Status.Scheduled; - proposalState.scheduledAt = Timestamps.now(); - self.proposals[proposalId].data = proposalState; + proposalData.status = Status.Scheduled; + proposalData.scheduledAt = Timestamps.now(); + self.proposals[proposalId].data = proposalData; emit ProposalScheduled(proposalId); } @@ -168,8 +160,10 @@ library ExecutableProposals { function execute(Context storage self, uint256 proposalId, Duration afterScheduleDelay) internal { Proposal memory proposal = self.proposals[proposalId]; - if (!_isProposalScheduled(self, proposalId, proposal.data)) { - revert ProposalNotScheduled(proposalId); + _checkProposalNotCancelled(self, proposalId, proposal.data); + + if (proposal.data.status != Status.Scheduled) { + revert UnexpectedProposalStatus(proposalId, proposal.data.status); } if (afterScheduleDelay.addTo(proposal.data.scheduledAt) > Timestamps.now()) { @@ -178,12 +172,8 @@ library ExecutableProposals { self.proposals[proposalId].data.status = Status.Executed; - address executor = proposal.data.executor; - ExternalCall[] memory calls = proposal.calls; - - bytes[] memory results = calls.execute(IExternalExecutor(executor)); - - emit ProposalExecuted(proposalId, results); + ExternalCalls.execute(IExternalExecutor(proposal.data.executor), proposal.calls); + emit ProposalExecuted(proposalId); } /// @notice Marks all non-executed proposals up to the most recently submitted as canceled, preventing their execution. @@ -198,21 +188,6 @@ library ExecutableProposals { // Getters // --- - /// @notice Determines whether a proposal is eligible for execution based on its status and delay requirements. - /// @param self The context of the Executable Proposal library. - /// @param proposalId The id of the proposal to check for execution eligibility. - /// @param afterScheduleDelay The required delay duration after scheduling before the proposal can be executed. - /// @return bool `true` if the proposal is eligible for execution, otherwise `false`. - function canExecute( - Context storage self, - uint256 proposalId, - Duration afterScheduleDelay - ) internal view returns (bool) { - ProposalData memory proposalState = self.proposals[proposalId].data; - return _isProposalScheduled(self, proposalId, proposalState) - && Timestamps.now() >= afterScheduleDelay.addTo(proposalState.scheduledAt); - } - /// @notice Determines whether a proposal is eligible to be scheduled based on its status and required delay. /// @param self The context of the Executable Proposal library. /// @param proposalId The id of the proposal to check for scheduling eligibility. @@ -223,9 +198,24 @@ library ExecutableProposals { uint256 proposalId, Duration afterSubmitDelay ) internal view returns (bool) { - ProposalData memory proposalState = self.proposals[proposalId].data; - return _isProposalSubmitted(self, proposalId, proposalState) - && Timestamps.now() >= afterSubmitDelay.addTo(proposalState.submittedAt); + ProposalData memory proposalData = self.proposals[proposalId].data; + return proposalId > self.lastCancelledProposalId && proposalData.status == Status.Submitted + && Timestamps.now() >= afterSubmitDelay.addTo(proposalData.submittedAt); + } + + /// @notice Determines whether a proposal is eligible for execution based on its status and delay requirements. + /// @param self The context of the Executable Proposal library. + /// @param proposalId The id of the proposal to check for execution eligibility. + /// @param afterScheduleDelay The required delay duration after scheduling before the proposal can be executed. + /// @return bool `true` if the proposal is eligible for execution, otherwise `false`. + function canExecute( + Context storage self, + uint256 proposalId, + Duration afterScheduleDelay + ) internal view returns (bool) { + ProposalData memory proposalData = self.proposals[proposalId].data; + return proposalId > self.lastCancelledProposalId && proposalData.status == Status.Scheduled + && Timestamps.now() >= afterScheduleDelay.addTo(proposalData.scheduledAt); } /// @notice Returns the total count of submitted proposals. @@ -274,24 +264,18 @@ library ExecutableProposals { function _checkProposalExists(uint256 proposalId, ProposalData memory proposalData) private pure { if (proposalData.status == Status.NotExist) { - revert ProposalNotFound(proposalId); + revert UnexpectedProposalStatus(proposalId, Status.NotExist); } } - function _isProposalSubmitted( - Context storage self, - uint256 proposalId, - ProposalData memory proposalData - ) private view returns (bool) { - return proposalId > self.lastCancelledProposalId && proposalData.status == Status.Submitted; - } - - function _isProposalScheduled( + function _checkProposalNotCancelled( Context storage self, uint256 proposalId, ProposalData memory proposalData - ) private view returns (bool) { - return proposalId > self.lastCancelledProposalId && proposalData.status == Status.Scheduled; + ) private view { + if (_isProposalCancelled(self, proposalId, proposalData)) { + revert UnexpectedProposalStatus(proposalId, Status.Cancelled); + } } function _isProposalCancelled( diff --git a/contracts/libraries/ExternalCalls.sol b/contracts/libraries/ExternalCalls.sol index 32ee488e..ace806b4 100644 --- a/contracts/libraries/ExternalCalls.sol +++ b/contracts/libraries/ExternalCalls.sol @@ -20,15 +20,10 @@ library ExternalCalls { /// `IExternalExecutor` interface. /// @param calls An array of `ExternalCall` structs, each specifying a call to be executed. /// @param executor The contract responsible for executing each call, conforming to the `IExternalExecutor` interface. - /// @return results An array containing the return data from each call. - function execute( - ExternalCall[] memory calls, - IExternalExecutor executor - ) internal returns (bytes[] memory results) { + function execute(IExternalExecutor executor, ExternalCall[] memory calls) internal { uint256 callsCount = calls.length; - results = new bytes[](callsCount); for (uint256 i = 0; i < callsCount; ++i) { - results[i] = executor.execute(calls[i].target, calls[i].value, calls[i].payload); + executor.execute(calls[i].target, calls[i].value, calls[i].payload); } } } diff --git a/contracts/libraries/Proposers.sol b/contracts/libraries/Proposers.sol index a8d45ba1..a8fb4b17 100644 --- a/contracts/libraries/Proposers.sol +++ b/contracts/libraries/Proposers.sol @@ -10,16 +10,18 @@ library Proposers { // Errors // --- error InvalidExecutor(address executor); - error InvalidProposerAccount(address account); - error ProposerNotRegistered(address proposer); - error ProposerAlreadyRegistered(address proposer); + error ExecutorNotRegistered(address executor); + error InvalidProposerAccount(address proposerAccount); + error ProposerNotRegistered(address proposerAccount); + error ProposerAlreadyRegistered(address proposerAccount); // --- // Events // --- - event ProposerRegistered(address indexed proposer, address indexed executor); - event ProposerUnregistered(address indexed proposer, address indexed executor); + event ProposerRegistered(address indexed proposerAccount, address indexed executor); + event ProposerExecutorSet(address indexed proposerAccount, address indexed executor); + event ProposerUnregistered(address indexed proposerAccount, address indexed executor); // --- // Data Types @@ -84,6 +86,26 @@ library Proposers { emit ProposerRegistered(proposerAccount, executor); } + /// @notice Updates the executor for a registered proposer. + /// @param self The context storage of the Proposers library. + /// @param proposerAccount The address of the proposer to update. + /// @param newExecutor The new executor address to assign to the proposer. + function setProposerExecutor(Context storage self, address proposerAccount, address newExecutor) internal { + ExecutorData memory executorData = self.executors[proposerAccount]; + _checkRegisteredProposer(proposerAccount, executorData); + + if (newExecutor == address(0) || executorData.executor == newExecutor) { + revert InvalidExecutor(newExecutor); + } + + self.executors[proposerAccount].executor = newExecutor; + + self.executorRefsCounts[newExecutor] += 1; + self.executorRefsCounts[executorData.executor] -= 1; + + emit ProposerExecutorSet(proposerAccount, newExecutor); + } + /// @notice Unregisters a proposer, removing its association with an executor. /// @param self The context of the Proposers library. /// @param proposerAccount The address of the proposer to unregister. @@ -132,28 +154,47 @@ library Proposers { /// @param self The context of the Proposers library. /// @return proposers An array of `Proposer` structs representing all registered proposers. function getAllProposers(Context storage self) internal view returns (Proposer[] memory proposers) { - proposers = new Proposer[](self.proposers.length); - for (uint256 i = 0; i < proposers.length; ++i) { + uint256 proposersCount = self.proposers.length; + proposers = new Proposer[](proposersCount); + + for (uint256 i = 0; i < proposersCount; ++i) { proposers[i] = getProposer(self, self.proposers[i]); } } - /// @notice Checks if an account is a registered proposer. + /// @notice Checks if an `proposerAccount` is a registered proposer. /// @param self The context of the Proposers library. - /// @param account The address to check. - /// @return bool `true` if the account is a registered proposer, otherwise `false`. - function isProposer(Context storage self, address account) internal view returns (bool) { - return _isRegisteredProposer(self.executors[account]); + /// @param proposerAccount The address to check. + /// @return bool `true` if the `proposerAccount` is a registered proposer, otherwise `false`. + function isRegisteredProposer(Context storage self, address proposerAccount) internal view returns (bool) { + return _isRegisteredProposer(self.executors[proposerAccount]); } - /// @notice Checks if an account is an executor associated with any proposer. + /// @notice Checks if an `executor` address is an executor associated with any proposer. /// @param self The context of the Proposers library. - /// @param account The address to check. - /// @return bool `true` if the account is an executor, otherwise `false`. - function isExecutor(Context storage self, address account) internal view returns (bool) { - return self.executorRefsCounts[account] > 0; + /// @param executor The address to check. + /// @return bool `true` if the `executor` address is an registered executor, otherwise `false`. + function isRegisteredExecutor(Context storage self, address executor) internal view returns (bool) { + return self.executorRefsCounts[executor] > 0; + } + + // --- + // Checks + // --- + + /// @notice Checks that a given `executor` address is a registered executor. + /// @param self The storage context of the Proposers library. + /// @param executor The address to verify as a registered executor. + function checkRegisteredExecutor(Context storage self, address executor) internal view { + if (!isRegisteredExecutor(self, executor)) { + revert ExecutorNotRegistered(executor); + } } + // --- + // Private Methods + // --- + /// @notice Checks that the given proposer is registered. function _checkRegisteredProposer(address proposerAccount, ExecutorData memory executorData) internal pure { if (!_isRegisteredProposer(executorData)) { diff --git a/contracts/libraries/Resealer.sol b/contracts/libraries/Resealer.sol new file mode 100644 index 00000000..4b176512 --- /dev/null +++ b/contracts/libraries/Resealer.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IResealManager} from "../interfaces/IResealManager.sol"; + +/// @title Resealer Library +/// @dev Library for managing sealing operations for critical components of Lido protocol. +library Resealer { + // --- + // Errors + // --- + error InvalidResealManager(IResealManager resealManager); + error InvalidResealCommittee(address resealCommittee); + error CallerIsNotResealCommittee(address caller); + + // --- + // Events + // --- + event ResealCommitteeSet(address resealCommittee); + event ResealManagerSet(IResealManager resealManager); + + // --- + // Data Types + // --- + + /// @dev Struct to hold the context of the reseal operations. + /// @param resealManager The address of the Reseal Manager. + /// @param resealCommittee The address of the Reseal Committee which is allowed to "reseal" sealables paused for a limited + /// period of time when the Dual Governance proposal adoption is blocked. + struct Context { + IResealManager resealManager; + address resealCommittee; + } + + /// @dev Sets a new Reseal Manager contract address. + /// @param self The context struct containing the current state. + /// @param newResealManager The address of the new Reseal Manager. + function setResealManager(Context storage self, IResealManager newResealManager) internal { + if (newResealManager == self.resealManager || address(newResealManager) == address(0)) { + revert InvalidResealManager(newResealManager); + } + self.resealManager = newResealManager; + emit ResealManagerSet(newResealManager); + } + + /// @dev Sets a new reseal committee address. + /// @param self The context struct containing the current state. + /// @param newResealCommittee The address of the new reseal committee. + function setResealCommittee(Context storage self, address newResealCommittee) internal { + if (newResealCommittee == self.resealCommittee) { + revert InvalidResealCommittee(newResealCommittee); + } + self.resealCommittee = newResealCommittee; + emit ResealCommitteeSet(newResealCommittee); + } + + /// @dev Checks if the caller is the reseal committee. + /// @param self The context struct containing the current state. + function checkCallerIsResealCommittee(Context storage self) internal view { + if (msg.sender != self.resealCommittee) { + revert CallerIsNotResealCommittee(msg.sender); + } + } +} diff --git a/contracts/libraries/TimelockState.sol b/contracts/libraries/TimelockState.sol index 7542cacb..8abbbf35 100644 --- a/contracts/libraries/TimelockState.sol +++ b/contracts/libraries/TimelockState.sol @@ -12,6 +12,8 @@ library TimelockState { error CallerIsNotGovernance(address caller); error InvalidGovernance(address governance); + error CallerIsNotAdminExecutor(address caller); + error InvalidAdminExecutor(address adminExecutor); error InvalidExecutionDelay(Duration executionDelay); error InvalidAfterSubmitDelay(Duration afterSubmitDelay); error InvalidAfterScheduleDelay(Duration afterScheduleDelay); @@ -23,6 +25,7 @@ library TimelockState { event GovernanceSet(address newGovernance); event AfterSubmitDelaySet(Duration newAfterSubmitDelay); event AfterScheduleDelaySet(Duration newAfterScheduleDelay); + event AdminExecutorSet(address newAdminExecutor); // --- // Data Types @@ -35,10 +38,12 @@ library TimelockState { Duration afterSubmitDelay; /// @dev slot0 [192..224] Duration afterScheduleDelay; + /// @dev slot1 [0..159] + address adminExecutor; } // --- - // Main Functionality + // State Management // --- /// @notice Sets the governance address. @@ -85,6 +90,22 @@ library TimelockState { self.afterScheduleDelay = newAfterScheduleDelay; emit AfterScheduleDelaySet(newAfterScheduleDelay); } + /// @notice Sets the admin executor address. + /// @dev Reverts if the new admin executor address is zero or the same as the current one. + /// @param self The context of the timelock state. + /// @param newAdminExecutor The new admin executor address. + + function setAdminExecutor(Context storage self, address newAdminExecutor) internal { + if (newAdminExecutor == address(0) || newAdminExecutor == self.adminExecutor) { + revert InvalidAdminExecutor(newAdminExecutor); + } + self.adminExecutor = newAdminExecutor; + emit AdminExecutorSet(newAdminExecutor); + } + + // --- + // Getters + // --- /// @notice Retrieves the delay period required after a proposal is submitted before it can be scheduled. /// @param self The context of the Timelock State library. @@ -100,6 +121,10 @@ library TimelockState { return self.afterScheduleDelay; } + // --- + // Checks + // --- + /// @notice Checks if the caller is the governance address, reverting if not. /// @param self The context of the Timelock State library. function checkCallerIsGovernance(Context storage self) internal view { @@ -117,4 +142,12 @@ library TimelockState { revert InvalidExecutionDelay(executionDelay); } } + + /// @notice Ensures that the caller is the designated admin executor. + /// @param self The context of the calling contract. + function checkCallerIsAdminExecutor(Context storage self) internal view { + if (self.adminExecutor != msg.sender) { + revert CallerIsNotAdminExecutor(msg.sender); + } + } } diff --git a/contracts/libraries/WithdrawalsBatchesQueue.sol b/contracts/libraries/WithdrawalsBatchesQueue.sol index 6adf5b5b..a84339d1 100644 --- a/contracts/libraries/WithdrawalsBatchesQueue.sol +++ b/contracts/libraries/WithdrawalsBatchesQueue.sol @@ -5,11 +5,11 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; /// @notice The state of the WithdrawalBatchesQueue. -/// @param Absent The initial (uninitialized) state of the WithdrawalBatchesQueue. +/// @param NotInitialized The initial (uninitialized) state of the WithdrawalBatchesQueue. /// @param Opened In this state, the WithdrawalBatchesQueue allows the addition of new batches of unstETH ids. /// @param Closed The terminal state of the queue where adding new batches is no longer permitted. enum State { - Absent, + NotInitialized, Opened, Closed } @@ -23,9 +23,7 @@ library WithdrawalsBatchesQueue { error EmptyBatch(); error InvalidUnstETHIdsSequence(); - error WithdrawalsBatchesQueueIsInAbsentState(); - error WithdrawalsBatchesQueueIsNotInOpenedState(); - error WithdrawalsBatchesQueueIsNotInAbsentState(); + error UnexpectedWithdrawalsBatchesQueueState(State state); // --- // Events @@ -91,9 +89,7 @@ library WithdrawalsBatchesQueue { /// @param boundaryUnstETHId The id of the unstETH NFT which is used as the boundary value for the withdrawal queue. /// `boundaryUnstETHId` value is used as a lower bound for the adding unstETH ids. function open(Context storage self, uint256 boundaryUnstETHId) internal { - if (self.info.state != State.Absent) { - revert WithdrawalsBatchesQueueIsNotInAbsentState(); - } + _checkState(self, State.NotInitialized); self.info.state = State.Opened; @@ -108,7 +104,7 @@ library WithdrawalsBatchesQueue { /// @param self The context of the Withdrawals Batches Queue library. /// @param unstETHIds An array of sequential unstETH ids to be added to the queue. function addUnstETHIds(Context storage self, uint256[] memory unstETHIds) internal { - _checkInOpenedState(self); + _checkState(self, State.Opened); uint256 unstETHIdsCount = unstETHIds.length; @@ -163,7 +159,7 @@ library WithdrawalsBatchesQueue { /// @notice Closes the WithdrawalsBatchesQueue, preventing further batch additions. /// @param self The context of the Withdrawals Batches Queue library. function close(Context storage self) internal { - _checkInOpenedState(self); + _checkState(self, State.Opened); self.info.state = State.Closed; emit WithdrawalsBatchesQueueClosed(); } @@ -220,13 +216,13 @@ library WithdrawalsBatchesQueue { return self.info.totalUnstETHIdsCount - self.info.totalUnstETHIdsClaimed; } - /// @notice Returns the id of the last claimed unstETH. When the queue is empty, returns 0. + /// @notice Returns the id of the boundary unstETH. + /// @dev Reverts with an index OOB error if called when the `WithdrawalsBatchesQueue` is in the + /// `NotInitialized` state. /// @param self The context of the Withdrawals Batches Queue library. - /// @return lastClaimedUnstETHId The id of the lastClaimedUnstETHId or 0 when the queue is empty - function getLastClaimedOrBoundaryUnstETHId(Context storage self) internal view returns (uint256) { - _checkNotInAbsentState(self); - QueueInfo memory info = self.info; - return self.batches[info.lastClaimedBatchIndex].firstUnstETHId + info.lastClaimedUnstETHIdIndex; + /// @return boundaryUnstETHId The id of the boundary unstETH. + function getBoundaryUnstETHId(Context storage self) internal view returns (uint256) { + return self.batches[0].firstUnstETHId; } /// @notice Returns if all unstETH ids in the queue have been claimed. @@ -277,15 +273,9 @@ library WithdrawalsBatchesQueue { info.totalUnstETHIdsClaimed += SafeCast.toUint64(unstETHIdsCount); } - function _checkNotInAbsentState(Context storage self) private view { - if (self.info.state == State.Absent) { - revert WithdrawalsBatchesQueueIsInAbsentState(); - } - } - - function _checkInOpenedState(Context storage self) private view { - if (self.info.state != State.Opened) { - revert WithdrawalsBatchesQueueIsNotInOpenedState(); + function _checkState(Context storage self, State expectedState) private view { + if (self.info.state != expectedState) { + revert UnexpectedWithdrawalsBatchesQueueState(self.info.state); } } } diff --git a/docs/mechanism.md b/docs/mechanism.md index 07b9f427..2765edb5 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -12,7 +12,7 @@ Additionally, there is a Gate Seal emergency committee that allows pausing certa The Dual governance mechanism (DG) is an iteration on the protocol governance that gives stakers a say by allowing them to block DAO decisions and providing a negotiation device between stakers and the DAO. -Another way of looking at dual governance is that it implements: +Another way of looking at dual governance is that it implements: 1) a dynamic user-extensible timelock on DAO decisions 2) a rage quit mechanism for stakers taking into account the specifics of how Ethereum withdrawals work. @@ -137,7 +137,7 @@ The Normal state is the state the mechanism is designed to spend the most time w **Transition to Veto Signalling**. If, while the state is active, the following expression becomes true: ```math -R > R_1 +R \geq R_1 ``` where $R_1$ is `FirstSealRageQuitSupport`, the Normal state is exited and the Veto Signalling state is entered. @@ -171,8 +171,8 @@ The **dynamic timelock duration** $T_{lock}(R)$ depends on the current rage quit ```math T_{lock}(R) = \left\{ \begin{array}{lr} - 0, & \text{if } R \leq R_1 \\ - L(R), & \text{if } R_1 < R < R_2 \\ + 0, & \text{if } R < R_1 \\ + L(R), & \text{if } R_1 \leq R < R_2 \\ L_{max}, & \text{if } R \geq R_2 \end{array} \right. ``` @@ -183,7 +183,8 @@ L(R) = L_{min} + \frac{(R - R_1)} {R_2 - R_1} (L_{max} - L_{min}) where $R_1$ is `FirstSealRageQuitSupport`, $R_2$ is `SecondSealRageQuitSupport`, $L_{min}$ is `DynamicTimelockMinDuration`, $L_{max}$ is `DynamicTimelockMaxDuration`. The dependence of the dynamic timelock on the rage quit support $R$ can be illustrated by the following graph: -![image](https://github.com/lidofinance/dual-governance/assets/1699593/b98dd9f1-1e55-4b5d-8ce1-56539f4cc3f8) +![image](https://github.com/user-attachments/assets/15cb6cdb-68a6-41ce-8d47-c34da19b84f1) + When the current rage quit support changes due to stakers locking or unlocking tokens into/out of the signalling escrow or the total stETH supply changing, the dynamic timelock duration is re-evaluated. @@ -192,7 +193,7 @@ Let's now define the outgoing transitions. **Transition to Rage Quit**. If, while Veto Signalling is active and the Deactivation sub-state is not active, the following expression becomes true: ```math -\big( t - t^S_{act} > L_{max} \big) \, \land \, \big( R > R_2 \big) +\big( t - t^S_{act} > L_{max} \big) \, \land \, \big( R \geq R_2 \big) ``` the Veto Signalling state is exited and the Rage Quit state is entered. @@ -229,7 +230,7 @@ then the Deactivation sub-state is exited so only the parent Veto Signalling sta **Transition to Rage Quit**. If, while the sub-state is active, the following condition becomes true: ```math -\big( t - t^S_{act} > L_{max} \big) \, \land \, \big( R > R_2 \big) +\big( t - t^S_{act} > L_{max} \big) \, \land \, \big( R \geq R_2 \big) ``` then the Deactivation sub-state is exited along with its parent Veto Signalling state and the Rage Quit state is entered. @@ -253,7 +254,7 @@ In the Veto Cooldown state, the DAO cannot submit proposals to the DG but can ex **Transition to Veto Signalling**. If, while the state is active, the following condition becomes true: ```math -\big( t - t^C_{act} > T^C \big) \,\land\, \big( R(t) > R_1 \big) +\big( t - t^C_{act} > T^C \big) \,\land\, \big( R(t) \geq R_1 \big) ``` where $t^{C}_{act}$ is the time the Veto Cooldown state was entered and $T^{C}$ is `VetoCooldownDuration`, then the Veto Cooldown state is exited and the Veto Signalling state is entered. @@ -261,7 +262,7 @@ where $t^{C}_{act}$ is the time the Veto Cooldown state was entered and $T^{C}$ **Transition to Normal**. If, while the state is active, the following condition becomes true: ```math -\big( t - t^C_{act} > T^C \big) \,\land\, \big( R(t) \leq R_1 \big) +\big( t - t^C_{act} > T^C \big) \,\land\, \big( R(t) < R_1 \big) ``` then the Veto Cooldown state is exited and the Normal state is entered. @@ -295,9 +296,9 @@ When the withdrawal is complete and the extension period elapses, two things hap 1. A timelock lasting $W(i)$ days is started, during which the withdrawn ETH remains locked in the rage quit escrow. After the timelock elapses, stakers who participated in the rage quit can obtain their ETH from the rage quit escrow. 2. The Rage Quit state is exited. -**Transition to Veto Signalling**. If, at the moment of the Rage Quit state exit, $R(t) > R_1$, the Veto Signalling state is entered. +**Transition to Veto Signalling**. If, at the moment of the Rage Quit state exit, $R(t) \geq R_1$, the Veto Signalling state is entered. -**Transition to Veto Cooldown**. If, at the moment of the Rage Quit state exit, $R(t) \leq R_1$, the Veto Cooldown state is entered. +**Transition to Veto Cooldown**. If, at the moment of the Rage Quit state exit, $R(t) < R_1$, the Veto Cooldown state is entered. The duration of the ETH withdraw timelock $W(i)$ is a linear function that depends on the rage quit sequence number $i$ (see below): @@ -307,7 +308,7 @@ W(i) = \min \left\{ W_{min} + i * W_{growth} \,,\, W_{max} \right\} where $W_{min}$ is `RageQuitEthWithdrawalsMinDelay`, $W_{max}$ is `RageQuitEthWithdrawalsMaxDelay`, $W_{growth}$ is `rageQuitEthWithdrawalsDelayGrowth`. -The rage quit sequence number is calculated as follows: each time the Normal state is entered, the sequence number is set to 0; each time the Rage Quit state is entered, the number is incremented by 1. +The rage quit sequence number is calculated as follows: each time the VetoCooldown state is entered, the sequence number is set to 0; each time the Rage Quit state is entered, the number is incremented by 1. ```env # Proposed values, to be modeled and refined @@ -398,6 +399,11 @@ Dual governance should not cover: ## Changelog +### 2024-12-04 +- Updated calculations of Rage Quit support first/second threshold's reaching. The transition to VetoSignalling state now starts when the amount of locked funds reaches the first seal threshold (including the exact threshold value), and the transition to RageQuit state appropriately starts when the amount of locked funds reaches the second seal threshold (including the exact threshold value; the appropriate duration of time in VetoSignalling state still should pass). + +### 2024-11-15 +- The rage quit sequence number is now reset in the `VetoCooldown` state instead of the `Normal` state. This adjustment ensures that the ETH withdrawal timelock does not increase unnecessarily in cases where, after a Rage Quit, Dual Governance cycles through `VetoSignalling` → `VetoSignallingDeactivation` → `VetoCooldown` without entering the `Normal` state, as the DAO remains operational and can continue submitting and executing proposals in this scenario. ### 2024-09-12 - Explicitly described the `VetoSignallingDeactivation` -> `RageQuit` state transition. diff --git a/docs/plan-b.md b/docs/plan-b.md index b4f770c6..c9268b7b 100644 --- a/docs/plan-b.md +++ b/docs/plan-b.md @@ -9,15 +9,10 @@ Timelocked Governance (TG) is a governance subsystem positioned between the Lido * [Proposal flow](#proposal-flow) * [Proposal execution](#proposal-execution) * [Common types](#common-types) -* Core Contracts: +* Contracts: * [Contract: `TimelockedGovernance`](#contract-timelockedgovernance) * [Contract: `EmergencyProtectedTimelock`](#contract-emergencyprotectedtimelock) * [Contract: `Executor`](#contract-executor) -* Committees: - * [Contract: `ProposalsList`](#contract-proposalslist) - * [Contract: `HashConsensus`](#contract-hashconsensus) - * [Contract: `EmergencyActivationCommittee`](#contract-emergencyactivationcommittee) - * [Contract: `EmergencyExecutionCommittee`](#contract-emergencyexecutioncommittee) ## System Overview @@ -28,10 +23,6 @@ The system comprises the following primary contracts: - [**`EmergencyProtectedTimelock.sol`**](#contract-emergencyprotectedtimelock): A singleton contract that stores submitted proposals and provides an execution interface. In addition, it implements an optional protection from a malicious proposals submitted by the DAO. The protection is implemented as a timelock on proposal execution combined with two emergency committees that have the right to cooperate and suspend the execution of the proposals. - [**`Executor.sol`**](#contract-executor): A contract instance responsible for executing calls resulting from governance proposals. All protocol permissions or roles protected by TG, as well as the authority to manage these roles/permissions, should be assigned exclusively to instance of this contract, rather than being assigned directly to the DAO voting system. -Additionally, the system uses several committee contracts that allow members to execute, acquiring quorum, a narrow set of actions: - -- [**`EmergencyActivationCommittee`**](#contract-emergencyactivationcommittee): A contract with the authority to activate Emergency Mode. Activation requires a quorum from committee members. -- [**`EmergencyExecutionCommittee`**](#contract-emergencyexecutioncommittee): A contract that enables the execution of proposals during Emergency Mode by obtaining a quorum of committee members. ## Proposal flow image @@ -95,6 +86,17 @@ See: [`EmergencyProtectedTimelock.schedule`](#) #### Preconditions - The proposal with the given id MUST be in the `Submitted` state. + +### Function: `TimelockedGovernance.executeProposal` +```solidity +function executeProposal(uint256 proposalId) external +``` +Instructs the [`EmergencyProtectedTimelock`](#) singleton instance to execute the proposal with id `proposalId`. + +See: [`EmergencyProtectedTimelock.execute`](#) +#### Preconditions +- The proposal with the given id MUST be in the `Scheduled` state. + ### Function: `TimelockedGovernance.cancelAllPendingProposals` ```solidity @@ -109,7 +111,9 @@ See: [`EmergencyProtectedTimelock.cancelAllNonExecutedProposals`](#) #### Preconditions * MUST be called by an [admin voting system](#) + ## Contract: `EmergencyProtectedTimelock` + `EmergencyProtectedTimelock` is a singleton instance that stores and manages the lifecycle of proposals submitted by the DAO via the `TimelockedGovernance` contract. It can be configured with time-bound **Emergency Activation Committee** and **Emergency Execution Committee**, which act as safeguards against the execution of malicious proposals. For a proposal to be executed, the following steps have to be performed in order: @@ -123,9 +127,10 @@ The contract only allows proposal submission and scheduling by the `governance` If the Emergency Committees are set up and active, the governance proposal undergoes a separate emergency protection delay between submission and scheduling. This additional timelock is implemented to protect against the execution of malicious proposals submitted by the DAO. If the Emergency Committees aren't set, the proposal flow remains the same, but the timelock duration is zero. -If the Emergency Committees are set up and active, the governance proposal undergoes a separate emergency protection delay between submission and scheduling. This additional timelock is implemented to safeguard against the execution of malicious proposals submitted by the DAO. If the Emergency Committees aren't set, the proposal flow remains the same, but the timelock duration is zero. While active, the Emergency Activation Committee can enable Emergency Mode. This mode prohibits anyone but the Emergency Execution Committee from executing proposals. Once the **Emergency Duration** has ended, the Emergency Execution Committee or anyone else may disable the emergency mode, canceling all pending proposals. After the emergency mode is deactivated or the Emergency Period has elapsed, the Emergency Committees lose their power. + + ### Function: `EmergencyProtectedTimelock.submit` ```solidity function submit(address proposer, address executor, ExecutorCall[] calls, string metadata) @@ -219,211 +224,3 @@ Reverts if the call was unsuccessful. The result of the call. #### Preconditions * MUST be called by the contract owner (which SHOULD be the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance). - -## Contract: `Configuration` -`Configuration` is the smart contract encompassing all the constants in the Timelocked Governance design & providing the interfaces for getting access to them. It implements interfaces `IAdminExecutorConfiguration`, `ITimelockConfiguration` covering for relevant "parameters domains". - -## Contract: `ProposalsList` -`ProposalsList` implements storage for list of `Proposal`s with public interface to access. - -### Function: `ProposalsList.getProposals` -```solidity -function getProposals(uint256 offset, uint256 limit) external view returns (Proposal[] memory proposals) -``` -Returns a list of `Proposal` structs, starting from the specified `offset` and bounded to the specified `limit`. - -### Function: `ProposalsList.getProposalAt` -```solidity -function getProposalAt(uint256 index) external view returns (Proposal memory) -``` -Returns the `Proposal` at the specified index. - -### Function: `ProposalsList.getProposal` -```solidity -function getProposal(bytes32 key) external view returns (Proposal memory) -``` -Returns the `Proposal` with the given key. - -### Function: `ProposalsList.getProposalsLength` -```solidity -function getProposalsLength() external view returns (uint256) -``` -Returns the total number of created `Proposal`s. - -### Function: `ProposalsList.getOrderedKeys` -```solidity -function getOrderedKeys(uint256 offset, uint256 limit) external view returns (bytes32[] memory) -``` -Returns a list of `Proposal` keys, starting from the specified `offset` and bounded by the specified `limit`. - - -## Contract: `HashConsensus` -`HashConsensus` is an abstract contract that facilitates consensus-based decision-making among a set of members. Consensus is achieved through members voting on a specific hash, with decisions executed only if a quorum is reached and a timelock period has elapsed. - -### Function: `HashConsensus.addMember` -```solidity -function addMember(address newMember, uint256 newQuorum) external onlyOwner -``` -Adds a new member and updates the quorum. - -#### Preconditions -- Only the `owner` can call this function. -- `newQuorum` MUST be greater than 0 and less than or equal to the number of members. - -### Function: `HashConsensus.removeMember` -```solidity -function removeMember(address memberToRemove, uint256 newQuorum) external onlyOwner -``` -Removes a member and updates the quorum. - -#### Preconditions -- Only the `owner` can call this function. -- `memberToRemove` MUST be an added member. -- `newQuorum` MUST be greater than 0 and less than or equal to the number of remaining members. - -### Function: `HashConsensus.getMembers` -```solidity -function getMembers() external view returns (address[] memory) -``` -Returns the list of current members. - -### Function: `HashConsensus.isMember` -```solidity -function isMember(address member) external view returns (bool) -``` -Returns whether an account is listed as a member. - -### Function: `HashConsensus.setTimelockDuration` -```solidity -function setTimelockDuration(uint256 timelock) external onlyOwner -``` -Sets the duration of the timelock. - -#### Preconditions -- Only the `owner` can call this function. - -### Function: `HashConsensus.setQuorum` -```solidity -function setQuorum(uint256 newQuorum) external onlyOwner -``` -Sets the quorum required for decision execution. - -#### Preconditions -- Only the `owner` can call this function. -- `newQuorum` MUST be greater than 0 and less than or equal to the number of members. - -## Contract: `EmergencyActivationCommittee` -`EmergencyActivationCommittee` is a smart contract that extends the functionality of the `HashConsensus` contract to manage the emergency activation process. It allows committee members to vote on and execute the activation of emergency protocols in the specified contract. - -### Constructor -```solidity -constructor( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address emergencyProtectedTimelock -) -``` -Initializes the contract with an owner, committee members, a quorum, and the address of the `EmergencyProtectedTimelock` contract. - -#### Preconditions -- `executionQuorum` MUST be greater than 0. - -### Function: `EmergencyActivationCommittee.approveActivateEmergencyMode` -```solidity -function approveActivateEmergencyMode() public onlyMember -``` -Approves the emergency activation by voting on the `EMERGENCY_ACTIVATION_HASH`. - -#### Preconditions -- MUST be called by a committee member. - -### Function: `EmergencyActivationCommittee.getActivateEmergencyModeState` -```solidity -function getActivateEmergencyModeState() - public - view - returns (uint256 support, uint256 executionQuorum, bool isExecuted) -``` -Returns the state of the emergency activation proposal, including the support count, quorum, and execution status. - -### Function: `EmergencyActivationCommittee.executeActivateEmergencyMode` -```solidity -function executeActivateEmergencyMode() external -``` -Executes the emergency activation by calling the `emergencyActivate` function on the `EmergencyProtectedTimelock` contract. - -#### Preconditions -- The emergency activation proposal MUST have reached quorum and passed the timelock duration. - -## Contract: `EmergencyExecutionCommittee` -`EmergencyExecutionCommittee` is a smart contract that extends the functionalities of `HashConsensus` and `ProposalsList` to manage emergency execution and governance reset proposals through a consensus mechanism. It interacts with the `EmergencyProtectedTimelock` contract to execute critical emergency proposals. - -### Constructor -```solidity -constructor( - address owner, - address[] memory committeeMembers, - uint256 executionQuorum, - address emergencyProtectedTimelock -) -``` -Initializes the contract with an owner, committee members, a quorum, and the address of the `EmergencyProtectedTimelock` contract. - -#### Preconditions -- `executionQuorum` MUST be greater than 0. - -### Function: `EmergencyExecutionCommittee.voteEmergencyExecute` -```solidity -function voteEmergencyExecute(uint256 proposalId, bool _supports) public -``` -Allows committee members to vote on an emergency execution proposal. - -#### Preconditions -- MUST be called by a committee member. - -### Function: `EmergencyExecutionCommittee.getEmergencyExecuteState` -```solidity -function getEmergencyExecuteState(uint256 proposalId) - public - view - returns (uint256 support, uint256 executionQuorum, bool isExecuted) -``` -Returns the state of an emergency execution proposal, including the support count, quorum, and execution status. - -### Function: `EmergencyExecutionCommittee.executeEmergencyExecute` -```solidity -function executeEmergencyExecute(uint256 proposalId) public -``` -Executes an emergency execution proposal by calling the `emergencyExecute` function on the `EmergencyProtectedTimelock` contract. - -#### Preconditions -- The emergency execution proposal MUST have reached quorum and passed the timelock duration. - -### Function: `EmergencyExecutionCommittee.approveEmergencyReset` -```solidity -function approveEmergencyReset() public -``` -Approves the governance reset by voting on the reset proposal. - -#### Preconditions -- MUST be called by a committee member. - -### Function: `EmergencyExecutionCommittee.getEmergencyResetState` -```solidity -function getEmergencyResetState() - public - view - returns (uint256 support, uint256 executionQuorum, bool isExecuted) -``` -Returns the state of the governance reset proposal, including the support count, quorum, and execution status. - -### Function: `EmergencyExecutionCommittee.executeEmergencyReset` -```solidity -function executeEmergencyReset() external -``` -Executes the governance reset by calling the `emergencyReset` function on the `EmergencyProtectedTimelock` contract. - -#### Preconditions -- The governance reset proposal MUST have reached quorum and passed the timelock duration. - diff --git a/docs/specification.md b/docs/specification.md index e4b3fbb0..cac5f864 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -34,11 +34,8 @@ This document provides the system description on the code architecture level. A * Committees: * [Contract: ProposalsList.sol](#contract-proposalslistsol) * [Contract: HashConsensus.sol](#contract-hashconsensussol) - * [Contract: ResealCommittee.sol](#contract-resealcommitteesol) * [Contract: TiebreakerCoreCommittee.sol](#contract-tiebreakercorecommitteesol) * [Contract: TiebreakerSubCommittee.sol](#contract-tiebreakersubcommitteesol) - * [Contract: EmergencyActivationCommittee.sol](#contract-emergencyactivationcommitteesol) - * [Contract: EmergencyExecutionCommittee.sol](#contract-emergencyexecutioncommitteesol) * [Upgrade flow description](#upgrade-flow-description) @@ -58,11 +55,8 @@ The system is composed of the following main contracts: Additionally, the system uses several committee contracts that allow members to execute, acquiring quorum, a narrow set of actions while protecting management of the committees by the Dual Governance mechanism: -* [`ResealCommittee.sol`](#contract-resealcommitteesol) is a committee contract that allows members to obtain a quorum and reseal contracts temporarily paused by the [GateSeal emergency protection mechanism](https://github.com/lidofinance/gate-seals). * [`TiebreakerCoreCommittee.sol`](#contract-tiebreakercorecommitteesol) is a committee contract designed to approve proposals for execution in extreme situations where the Dual Governance system is deadlocked. This includes scenarios such as the inability to finalize user withdrawal requests during ongoing `RageQuit` or when the system is held in a locked state for an extended period. The `TiebreakerCoreCommittee` consists of multiple `TiebreakerSubCommittee` contracts appointed by the DAO. * [`TiebreakerSubCommittee.sol`](#contract-tiebreakersubcommitteesol) is a committee contracts that provides ability to participate in `TiebreakerCoreCommittee` for external actors. -* [`EmergencyActivationCommittee`](#contract-emergencyactivationcommitteesol) is a committee contract responsible for activating Emergency Mode by acquiring quorum. Only the EmergencyExecutionCommittee can execute proposals. This committee is expected to be active for a limited period following the initial deployment or update of the DualGovernance system. -* [`EmergencyExecutionCommittee`](#contract-emergencyexecutioncommitteesol) is a committee contract that enables quorum-based execution of proposals during Emergency Mode or disabling the DualGovernance mechanism by assigning the EmergencyProtectedTimelock to Aragon Voting. Like the EmergencyActivationCommittee, this committee is also intended for short-term use after the system’s deployment or update. ## Proposal flow @@ -238,7 +232,7 @@ This contract is a singleton, meaning that any DG deployment includes exactly on ```solidity enum State { - Unset, // Indicates an uninitialized state during the contract creation + NotInitialized, // Indicates an uninitialized state during the contract creation Normal, VetoSignalling, VetoSignallingDeactivation, @@ -272,56 +266,85 @@ The id of the successfully registered proposal. Triggers a transition of the current governance state (if one is possible) before checking the preconditions. -### Function: DualGovernance.tiebreakerScheduleProposal +### Function: DualGovernance.scheduleProposal -[`DualGovernance.tiebreakerScheduleProposal`]: #Function-DualGovernancetiebreakerScheduleProposal +[`DualGovernance.scheduleProposal`]: #Function-DualGovernancescheduleProposal ```solidity -function tiebreakerScheduleProposal(uint256 proposalId) +function scheduleProposal(uint256 proposalId) external ``` -Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance to schedule the proposal with the id `proposalId` for execution, bypassing the proposal dynamic timelock and given that the proposal was previously approved by the [Tiebreaker committee](#Tiebreaker-committee). +Schedules a previously submitted proposal for execution in the Dual Governance system. The function ensures that the proposal meets specific conditions before it can be scheduled. If the conditions are met, the proposal is registered for execution in the `EmergencyProtectedTimelock` singleton instance. -#### Preconditions +Preconditions -* MUST be called by the [Tiebreaker committee](#Tiebreaker-committee) address -* Either the Tiebreaker Condition A or the Tiebreaker Condition B MUST be met (see the [mechanism design document][mech design - tiebreaker]). -* The proposal with the given id MUST be already submitted using the `DualGovernance.submitProposal` call. -* The proposal MUST NOT be cancelled. +- The proposal with the specified proposalId MUST exist in the system. +- The required delay since submission (`EmergencyProtectedTimelock.getAfterSubmitDelay()`) MUST have elapsed. +- The Dual Governance system MUST BE in the `Normal` or `VetoCooldown` state +- If the system is in the `VetoCooldown` state, the proposal MUST have been submitted not later than the `VetoSignalling` state was entered. +- The proposal MUST NOT have been cancelled. +- The proposal MUST NOT already be scheduled. Triggers a transition of the current governance state (if one is possible) before checking the preconditions. -### Function: DualGovernance.tiebreakerResumeSealable - -[`DualGovernance.tiebreakerResumeSealable`]: #Function-DualGovernancetiebreakerResumeSealable +### Function: DualGovernance.cancelAllPendingProposals ```solidity -function tiebreakerResumeSealable(address sealable) +function cancelAllPendingProposals() returns (bool) ``` -Calls the `ResealManager.resumeSealable(address sealable)` if all preconditions met. +Cancels all currently submitted and non-executed proposals. If a proposal was submitted but not scheduled, it becomes unschedulable. If a proposal was scheduled, it becomes unexecutable. + +If the current governance state is neither `VetoSignalling` nor `VetoSignallingDeactivation`, the function will exit early without canceling any proposals, emitting the `CancelAllPendingProposalsSkipped` event and returning `false`. If proposals are successfully canceled, the `CancelAllPendingProposalsExecuted` event will be emitted, and the function will return `true`. #### Preconditions -* MUST be called by the [Tiebreaker committee](#Tiebreaker-committee) address -* Either the Tiebreaker Condition A or the Tiebreaker Condition B MUST be met (see the [mechanism design document][mech design - tiebreaker]). +- MUST be called by an authorized `proposalsCanceller` +Triggers a transition of the current governance state, if one is possible. -### Function: DualGovernance.cancelAllPendingProposals +### Function: DualGovernance.activateNextState ```solidity -function cancelAllPendingProposals() returns (bool) +function activateNextState() ``` -Cancels all currently submitted and non-executed proposals. If a proposal was submitted but not scheduled, it becomes unschedulable. If a proposal was scheduled, it becomes unexecutable. +Triggers a transition of the [global governance state](#Governance-state), if one is possible; does nothing otherwise. -If the current governance state is neither `VetoSignalling` nor `VetoSignallingDeactivation`, the function will exit early without canceling any proposals, emitting the `CancelAllPendingProposalsSkipped` event and returning `false`. If proposals are successfully canceled, the `CancelAllPendingProposalsExecuted` event will be emitted, and the function will return `true`. -Triggers a transition of the current governance state, if one is possible. +### Function: DualGovernance.getPersistedState -#### Preconditions +```solidity +function getPersistedState() view returns (State persistedState) +``` + +Returns the most recently persisted state of the DualGovernance. + +### Function: DualGovernance.getEffectiveState + +```solidity +function getEffectiveState() view returns (State persistedState) +``` + +Returns the effective state of the DualGovernance. The effective state refers to the state the DualGovernance would transition to upon calling `DualGovernance.activateNextState()`. + +### Function DualGovernance.getStateDetails + +```solidity +function getStateDetails() view returns (StateDetails) +``` + +This function returns detailed information about the current state of the `DualGovernance`, comprising the following data: + +- **`State effectiveState`**: The state that the `DualGovernance` would transition to upon calling `DualGovernance.activateNextState()`. +- **`State persistedState`**: The current stored state of the `DualGovernance`. +- **`Timestamp persistedStateEnteredAt`**: The timestamp when the `persistedState` was entered. +- **`Timestamp vetoSignallingActivatedAt`**: The timestamp when the `VetoSignalling` state was last activated. +- **`Timestamp vetoSignallingReactivationTime`**: The timestamp when the `VetoSignalling` state was last re-activated. +- **`Timestamp normalOrVetoCooldownExitedAt`**: The timestamp when the `Normal` or `VetoCooldown` state was last exited. +- **`uint256 rageQuitRound`**: The number of continuous RageQuit rounds. +- **`Duration vetoSignallingDuration`**: The duration of the `VetoSignalling` state, calculated based on the RageQuit support in the Veto Signalling `Escrow`. -- MUST be called by an [admin proposer](#Administrative-actions). ### Function: DualGovernance.registerProposer @@ -353,61 +376,55 @@ Removes the registered `proposer` address from the list of valid proposers and d * The `proposer` address MUST NOT be the only one assigned to the admin executor. -### Function: DualGovernance.setTiebreakerCommittee +### Function: DualGovernance.tiebreakerScheduleProposal + +[`DualGovernance.tiebreakerScheduleProposal`]: #Function-DualGovernancetiebreakerScheduleProposal ```solidity -function setTiebreakerCommittee(address newTiebreaker) +function tiebreakerScheduleProposal(uint256 proposalId) ``` -Updates the address of the [Tiebreaker committee](#Tiebreaker-committee). +Instructs the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance to schedule the proposal with the id `proposalId` for execution, bypassing the proposal dynamic timelock and given that the proposal was previously approved by the [Tiebreaker committee](#Tiebreaker-committee). #### Preconditions -* MUST be called by the admin executor contract. -* The `newTiebreaker` address MUST NOT be the zero address. -* The `newTiebreaker` address MUST be different from the current tiebreaker address. +* MUST be called by the [Tiebreaker committee](#Tiebreaker-committee) address +* Either the Tiebreaker Condition A or the Tiebreaker Condition B MUST be met (see the [mechanism design document][mech design - tiebreaker]). +* The proposal with the given id MUST be already submitted using the `DualGovernance.submitProposal` call. +* The proposal MUST NOT be cancelled. +Triggers a transition of the current governance state (if one is possible) before checking the preconditions. -### Function: DualGovernance.activateNextState +### Function: DualGovernance.tiebreakerResumeSealable + +[`DualGovernance.tiebreakerResumeSealable`]: #Function-DualGovernancetiebreakerResumeSealable ```solidity -function activateNextState() +function tiebreakerResumeSealable(address sealable) ``` -Triggers a transition of the [global governance state](#Governance-state), if one is possible; does nothing otherwise. +Calls the `ResealManager.resumeSealable(address sealable)` if all preconditions met. -### Function: DualGovernance.getPersistedState +#### Preconditions -```solidity -function getPersistedState() view returns (State persistedState) -``` +* MUST be called by the [Tiebreaker committee](#Tiebreaker-committee) address +* Either the Tiebreaker Condition A or the Tiebreaker Condition B MUST be met (see the [mechanism design document][mech design - tiebreaker]). -Returns the most recently persisted state of the DualGovernance. -### Function: DualGovernance.getEffectiveState +### Function: DualGovernance.setTiebreakerCommittee ```solidity -function getEffectiveState() view returns (State persistedState) +function setTiebreakerCommittee(address newTiebreaker) ``` -Returns the effective state of the DualGovernance. The effective state refers to the state the DualGovernance would transition to upon calling `DualGovernance.activateNextState()`. +Updates the address of the [Tiebreaker committee](#Tiebreaker-committee). -### Function DualGovernance.getStateDetails +#### Preconditions -```solidity -function getStateDetails() view returns (StateDetails) -``` +* MUST be called by the admin executor contract. +* The `newTiebreaker` address MUST NOT be the zero address. +* The `newTiebreaker` address MUST be different from the current tiebreaker address. -This function returns detailed information about the current state of the `DualGovernance`, comprising the following data: - -- **`State effectiveState`**: The state that the `DualGovernance` would transition to upon calling `DualGovernance.activateNextState()`. -- **`State persistedState`**: The current stored state of the `DualGovernance`. -- **`Timestamp persistedStateEnteredAt`**: The timestamp when the `persistedState` was entered. -- **`Timestamp vetoSignallingActivatedAt`**: The timestamp when the `VetoSignalling` state was last activated. -- **`Timestamp vetoSignallingReactivationTime`**: The timestamp when the `VetoSignalling` state was last re-activated. -- **`Timestamp normalOrVetoCooldownExitedAt`**: The timestamp when the `Normal` or `VetoCooldown` state was last exited. -- **`uint256 rageQuitRound`**: The number of continuous RageQuit rounds. -- **`Duration vetoSignallingDuration`**: The duration of the `VetoSignalling` state, calculated based on the RageQuit support in the Veto Signalling `Escrow`. ## Contract: Executor.sol @@ -415,20 +432,17 @@ Issues calls resulting from governance proposals' execution. Every protocol perm The system supports multiple instances of this contract, but all instances SHOULD be owned by the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance. + ### Function: execute ```solidity -function execute(address target, uint256 value, bytes payload) - payable returns (bytes result) +function execute(address target, uint256 value, bytes payload) payable ``` Issues a EVM call to the `target` address with the `payload` calldata, optionally sending `value` wei ETH. Reverts if the call was unsuccessful. -#### Returns - -The result of the call. #### Preconditions @@ -927,7 +941,7 @@ The governance reset entails the following steps: ### Function: EmergencyProtectedTimelock.submit ```solidity -function submit(address proposer, address executor, ExecutorCall[] calls, string calldata metadata) +function submit(address executor, ExecutorCall[] calls) returns (uint256 proposalId) ``` @@ -1145,7 +1159,7 @@ Returns if an address is a member. ### Function: HashConsensus.setTimelockDuration ```solidity -function setTimelockDuration(uint256 timelock) +function setTimelockDuration(uint256 newTimelock) ``` Sets the timelock duration. @@ -1168,42 +1182,6 @@ Sets the quorum required for decision execution. * Only the owner can call this function. * `newQuorum` MUST be greater than 0, less than or equal to the number of members, and not equal to the current `quorum` value. -## Contract: ResealCommittee.sol - -`ResealCommittee` is a smart contract that extends the `HashConsensus` and `PropsoalsList` contracts and allows members to obtain a quorum and reseal contracts temporarily paused by the [GateSeal emergency protection mechanism](https://github.com/lidofinance/gate-seals). It interacts with a DualGovernance contract to execute decisions once consensus is reached. - -### Function: ResealCommittee.voteReseal - -```solidity -function voteReseal(address sealable, bool support) -``` - -Reseals sealable by voting on it and adding it to the proposal list. - -#### Preconditions -* MUST be called by a member. - -### Function: ResealCommittee.getResealState - -```solidity -function getResealState(address sealable) - view - returns (uint256 support, uint256 executionQuorum, Timestamp quorumAt) -``` - -Returns the state of the sealable resume proposal including support count, quorum, and execution status. - -### Function: ResealCommittee.executeReseal - -```solidity -function executeReseal(address sealable) -``` - -Executes a reseal of the sealable contract by calling the `resealSealable` method on the `DualGovernance` contract - -#### Preconditions -* Proposal MUST be scheduled for execution and passed the timelock duration. - ## Contract: TiebreakerCoreCommittee.sol @@ -1358,116 +1336,6 @@ Executes a sealable resume request by calling the sealableResume function on the * Resume request MUST have reached quorum and passed the timelock duration. -## Contract: EmergencyActivationCommittee.sol - -`EmergencyActivationCommittee` is a smart contract that extends the functionalities of `HashConsensus` to manage the emergency activation process. It allows committee members to vote on and execute the activation of emergency protocols in the `HashConsensus` contract. - -### Function: EmergencyActivationCommittee.approveActivateEmergencyMode - -```solidity -function approveActivateEmergencyMode() -``` - -Approves the emergency activation by voting on the `EMERGENCY_ACTIVATION_HASH`. - -#### Preconditions - -* MUST be called by a member. - -### Function: EmergencyActivationCommittee.getActivateEmergencyModeState - -```solidity -function getActivateEmergencyModeState() - view - returns (uint256 support, uint256 executionQuorum, bool isExecuted) -``` - -Returns the state of the emergency activation proposal including support count, quorum, and execution status. - -### Function: EmergencyActivationCommittee.executeActivateEmergencyMode - -```solidity -function executeActivateEmergencyMode() external -``` - -Executes the emergency activation by calling the `activateEmergencyMode` function on the `EmergencyProtectedTimelock` contract. - -#### Preconditions - -* Emergency activation proposal MUST have reached quorum and passed the timelock duration. - - -## Contract: EmergencyExecutionCommittee.sol - -`EmergencyExecutionCommittee` is a smart contract that extends the functionalities of `HashConsensus` and `ProposalsList` to manage emergency execution and governance reset proposals through a consensus mechanism. It interacts with the `EmergencyProtectedTimelock` contract to execute critical emergency proposals. - -### Function: EmergencyExecutionCommittee.voteEmergencyExecute - -```solidity -function voteEmergencyExecute(uint256 proposalId, bool _support) -``` - -Allows committee members to vote on an emergency execution proposal. - -#### Preconditions - -* MUST be called by a member. - -### Function: EmergencyExecutionCommittee.getEmergencyExecuteState - -```solidity -function getEmergencyExecuteState(uint256 proposalId) - view - returns (uint256 support, uint256 executionQuorum, bool isExecuted) -``` - -Returns the state of an emergency execution proposal including support count, quorum, and execution status. - -### Function: EmergencyExecutionCommittee.executeEmergencyExecute - -```solidity -function executeEmergencyExecute(uint256 proposalId) -``` - -Executes an emergency execution proposal by calling the `emergencyExecute` function on the `EmergencyProtectedTimelock` contract. - -#### Preconditions -* Emergency execution proposal MUST have reached quorum and passed the timelock duration. - - -### Function: EmergencyExecutionCommittee.approveEmergencyReset - -```solidity -function approveEmergencyReset() -``` - -Approves the governance reset by voting on the reset proposal. - -#### Preconditions - -* MUST be called by a member. - -### Function: EmergencyExecutionCommittee.getEmergencyResetState - -```solidity -function getEmergencyResetState() - view - returns (uint256 support, uint256 executionQuorum, bool isExecuted) -``` - -Returns the state of the governance reset proposal including support count, quorum, and execution status. - -### Function: EmergencyExecutionCommittee.executeEmergencyReset - -```solidity -function executeEmergencyReset() external -``` - -Executes the governance reset by calling the `emergencyReset` function on the `EmergencyProtectedTimelock` contract. - -#### Preconditions - -* Governance reset proposal MUST have reached quorum and passed the timelock duration. ## Upgrade flow description diff --git a/foundry.toml b/foundry.toml index a41bd76c..cd8fec52 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,11 +1,13 @@ [profile.default] src = 'contracts' out = 'out' +script = 'scripts' libs = ['node_modules', 'lib'] test = 'test' cache_path = 'cache_forge' # solc-version = "0.8.26" no-match-path = 'test/kontrol/*' +fs_permissions = [{ access = "read", path = "./"}] [profile.kprove] src = 'test/kontrol' @@ -15,3 +17,7 @@ test = 'test/kontrol' [fmt] line_length = 120 multiline_func_header = 'params_first_multi' + +[etherscan] +mainnet = { key = "${ETHERSCAN_MAINNET_KEY}" } +holesky = { key = "${ETHERSCAN_MAINNET_KEY}", chain = "17000" } diff --git a/lib/forge-std b/lib/forge-std index 155d547c..8f24d6b0 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 155d547c449afa8715f538d69454b83944117811 +Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa diff --git a/package.json b/package.json index 7e2b49f4..f8eb831c 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,10 @@ "scripts": { "test": "forge test", "prepare": "husky", - "lint": "solhint \"contracts/**/*.sol\" \"test/**/*.sol\" --ignore-path .solhintignore", + "lint": "solhint \"addresses/**/*.sol\" \"contracts/**/*.sol\" \"scripts/**/*.sol\" \"test/**/*.sol\" --ignore-path .solhintignore", "coverage": "forge coverage", "precov-report": "mkdir -p ./coverage-report && forge coverage --report lcov --report-file ./coverage-report/lcov.info", - "cov-report": "genhtml ./coverage-report/lcov.info --rc derive_function_end_line=0 --rc branch_coverage=1 -o coverage-report --exclude test --ignore-errors inconsistent --ignore-errors category" + "cov-report": "genhtml ./coverage-report/lcov.info --rc derive_function_end_line=0 --rc branch_coverage=1 -o coverage-report --include contracts --ignore-errors inconsistent --ignore-errors category" }, "lint-staged": { "*.sol": [ diff --git a/scripts/deploy/Config.sol b/scripts/deploy/Config.sol new file mode 100644 index 00000000..a685ba14 --- /dev/null +++ b/scripts/deploy/Config.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/* solhint-disable var-name-mixedcase */ + +import {IStETH} from "contracts/interfaces/IStETH.sol"; +import {IWstETH} from "contracts/interfaces/IWstETH.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {Duration} from "contracts/types/Duration.sol"; +import {PercentD16} from "contracts/types/PercentD16.sol"; + +// TODO: choose better value for MIN_EXECUTION_DELAY +uint256 constant DEFAULT_MIN_EXECUTION_DELAY = 0 seconds; +uint256 constant DEFAULT_AFTER_SUBMIT_DELAY = 3 days; +uint256 constant DEFAULT_MAX_AFTER_SUBMIT_DELAY = 45 days; +uint256 constant DEFAULT_AFTER_SCHEDULE_DELAY = 3 days; +uint256 constant DEFAULT_MAX_AFTER_SCHEDULE_DELAY = 45 days; +uint256 constant DEFAULT_EMERGENCY_MODE_DURATION = 180 days; +uint256 constant DEFAULT_MAX_EMERGENCY_MODE_DURATION = 365 days; +uint256 constant DEFAULT_EMERGENCY_PROTECTION_DURATION = 90 days; +uint256 constant DEFAULT_MAX_EMERGENCY_PROTECTION_DURATION = 365 days; +uint256 constant DEFAULT_TIEBREAKER_CORE_QUORUM = 1; +uint256 constant DEFAULT_TIEBREAKER_EXECUTION_DELAY = 30 days; +uint256 constant DEFAULT_TIEBREAKER_SUB_COMMITTEES_COUNT = 2; +uint256 constant DEFAULT_MIN_WITHDRAWALS_BATCH_SIZE = 4; +uint256 constant DEFAULT_MIN_TIEBREAKER_ACTIVATION_TIMEOUT = 90 days; +uint256 constant DEFAULT_TIEBREAKER_ACTIVATION_TIMEOUT = 365 days; +uint256 constant DEFAULT_MAX_TIEBREAKER_ACTIVATION_TIMEOUT = 730 days; +uint256 constant DEFAULT_MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = 255; + +uint256 constant DEFAULT_FIRST_SEAL_RAGE_QUIT_SUPPORT = 3_00; // 3% +uint256 constant DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT = 15_00; // 15% +uint256 constant DEFAULT_MIN_ASSETS_LOCK_DURATION = 5 hours; +uint256 constant DEFAULT_MAX_MIN_ASSETS_LOCK_DURATION = 365 days; +uint256 constant DEFAULT_VETO_SIGNALLING_MIN_DURATION = 3 days; +uint256 constant DEFAULT_VETO_SIGNALLING_MAX_DURATION = 30 days; +uint256 constant DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION = 5 hours; +uint256 constant DEFAULT_VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = 5 days; +uint256 constant DEFAULT_VETO_COOLDOWN_DURATION = 4 days; +uint256 constant DEFAULT_RAGE_QUIT_EXTENSION_PERIOD_DURATION = 7 days; +uint256 constant DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY = 30 days; +uint256 constant DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY = 180 days; +uint256 constant DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH = 15 days; + +bytes32 constant CHAIN_NAME_MAINNET_HASH = keccak256(bytes("mainnet")); +bytes32 constant CHAIN_NAME_HOLESKY_HASH = keccak256(bytes("holesky")); +bytes32 constant CHAIN_NAME_HOLESKY_MOCKS_HASH = keccak256(bytes("holesky-mocks")); + +struct DeployConfig { + Duration MIN_EXECUTION_DELAY; + Duration AFTER_SUBMIT_DELAY; + Duration MAX_AFTER_SUBMIT_DELAY; + Duration AFTER_SCHEDULE_DELAY; + Duration MAX_AFTER_SCHEDULE_DELAY; + Duration EMERGENCY_MODE_DURATION; + Duration MAX_EMERGENCY_MODE_DURATION; + Duration EMERGENCY_PROTECTION_DURATION; + Duration MAX_EMERGENCY_PROTECTION_DURATION; + address EMERGENCY_ACTIVATION_COMMITTEE; + address EMERGENCY_EXECUTION_COMMITTEE; + uint256 TIEBREAKER_CORE_QUORUM; + Duration TIEBREAKER_EXECUTION_DELAY; + uint256 TIEBREAKER_SUB_COMMITTEES_COUNT; + address[] TIEBREAKER_SUB_COMMITTEE_1_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_2_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_3_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_4_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_5_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_6_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_7_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_8_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_9_MEMBERS; + address[] TIEBREAKER_SUB_COMMITTEE_10_MEMBERS; + uint256[] TIEBREAKER_SUB_COMMITTEES_QUORUMS; + address RESEAL_COMMITTEE; + uint256 MIN_WITHDRAWALS_BATCH_SIZE; + Duration MIN_TIEBREAKER_ACTIVATION_TIMEOUT; + Duration TIEBREAKER_ACTIVATION_TIMEOUT; + Duration MAX_TIEBREAKER_ACTIVATION_TIMEOUT; + uint256 MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT; + PercentD16 FIRST_SEAL_RAGE_QUIT_SUPPORT; + PercentD16 SECOND_SEAL_RAGE_QUIT_SUPPORT; + Duration MIN_ASSETS_LOCK_DURATION; + Duration MAX_MIN_ASSETS_LOCK_DURATION; + Duration VETO_SIGNALLING_MIN_DURATION; + Duration VETO_SIGNALLING_MAX_DURATION; + Duration VETO_SIGNALLING_MIN_ACTIVE_DURATION; + Duration VETO_SIGNALLING_DEACTIVATION_MAX_DURATION; + Duration VETO_COOLDOWN_DURATION; + Duration RAGE_QUIT_EXTENSION_PERIOD_DURATION; + Duration RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY; + Duration RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY; + Duration RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH; +} + +struct LidoContracts { + uint256 chainId; + IStETH stETH; + IWstETH wstETH; + IWithdrawalQueue withdrawalQueue; + address voting; +} + +function getSubCommitteeData( + uint256 index, + DeployConfig memory dgDeployConfig +) pure returns (uint256 quorum, address[] memory members) { + assert(index <= 10); + + if (index == 0) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[0]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS; + } + + if (index == 1) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[1]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS; + } + + if (index == 2) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[2]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_3_MEMBERS; + } + + if (index == 3) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[3]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_4_MEMBERS; + } + if (index == 4) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[4]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_5_MEMBERS; + } + if (index == 5) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[5]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_6_MEMBERS; + } + if (index == 6) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[6]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_7_MEMBERS; + } + if (index == 7) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[7]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_8_MEMBERS; + } + if (index == 8) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[8]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_9_MEMBERS; + } + if (index == 9) { + quorum = dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS[9]; + members = dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_10_MEMBERS; + } +} diff --git a/scripts/deploy/ContractsDeployment.sol b/scripts/deploy/ContractsDeployment.sol new file mode 100644 index 00000000..0ac0a25d --- /dev/null +++ b/scripts/deploy/ContractsDeployment.sol @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +// --- +// Contracts +// --- +import {Timestamps} from "contracts/types/Timestamp.sol"; +import {Duration} from "contracts/types/Duration.sol"; + +import {Executor} from "contracts/Executor.sol"; +import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; + +import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; + +import {ResealManager} from "contracts/ResealManager.sol"; +import {DualGovernance} from "contracts/DualGovernance.sol"; +import { + DualGovernanceConfig, + IDualGovernanceConfigProvider, + ImmutableDualGovernanceConfigProvider +} from "contracts/ImmutableDualGovernanceConfigProvider.sol"; + +import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; +import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; +import {ITimelock} from "contracts/interfaces/ITimelock.sol"; +import {IResealManager} from "contracts/interfaces/IResealManager.sol"; + +import {DeployConfig, LidoContracts, getSubCommitteeData} from "./Config.sol"; + +struct DeployedContracts { + Executor adminExecutor; + EmergencyProtectedTimelock timelock; + TimelockedGovernance emergencyGovernance; + ResealManager resealManager; + DualGovernance dualGovernance; + TiebreakerCoreCommittee tiebreakerCoreCommittee; + TiebreakerSubCommittee[] tiebreakerSubCommittees; +} + +library DGContractsDeployment { + function deployDualGovernanceSetup( + DeployConfig memory dgDeployConfig, + LidoContracts memory lidoAddresses, + address deployer + ) internal returns (DeployedContracts memory contracts) { + contracts = deployAdminExecutorAndTimelock(dgDeployConfig, deployer); + deployEmergencyProtectedTimelockContracts(lidoAddresses, dgDeployConfig, contracts); + contracts.resealManager = deployResealManager(contracts.timelock); + ImmutableDualGovernanceConfigProvider dualGovernanceConfigProvider = + deployDualGovernanceConfigProvider(dgDeployConfig); + DualGovernance dualGovernance = deployDualGovernance({ + configProvider: dualGovernanceConfigProvider, + timelock: contracts.timelock, + resealManager: contracts.resealManager, + dgDeployConfig: dgDeployConfig, + lidoAddresses: lidoAddresses + }); + contracts.dualGovernance = dualGovernance; + + contracts.tiebreakerCoreCommittee = deployEmptyTiebreakerCoreCommittee({ + owner: deployer, // temporary set owner to deployer, to add sub committees manually + dualGovernance: address(dualGovernance), + executionDelay: dgDeployConfig.TIEBREAKER_EXECUTION_DELAY + }); + + contracts.tiebreakerSubCommittees = deployTiebreakerSubCommittees( + address(contracts.adminExecutor), contracts.tiebreakerCoreCommittee, dgDeployConfig + ); + + contracts.tiebreakerCoreCommittee.transferOwnership(address(contracts.adminExecutor)); + + // --- + // Finalize Setup + // --- + configureDualGovernance(dgDeployConfig, lidoAddresses, contracts); + + finalizeEmergencyProtectedTimelockDeploy( + contracts.adminExecutor, contracts.timelock, address(dualGovernance), dgDeployConfig + ); + + // --- + // TODO: Use this in voting script + // Grant Reseal Manager Roles + // --- + /* vm.startPrank(address(_lido.agent)); + _lido.withdrawalQueue.grantRole( + 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(resealManager) + ); + _lido.withdrawalQueue.grantRole( + 0x2fc10cc8ae19568712f7a176fb4978616a610650813c9d05326c34abb62749c7, address(resealManager) + ); + vm.stopPrank(); */ + } + + function deployAdminExecutorAndTimelock( + DeployConfig memory dgDeployConfig, + address deployer + ) internal returns (DeployedContracts memory contracts) { + Executor adminExecutor = deployExecutor({owner: deployer}); + EmergencyProtectedTimelock timelock = deployEmergencyProtectedTimelock(address(adminExecutor), dgDeployConfig); + + contracts.adminExecutor = adminExecutor; + contracts.timelock = timelock; + } + + function deployEmergencyProtectedTimelockContracts( + LidoContracts memory lidoAddresses, + DeployConfig memory dgDeployConfig, + DeployedContracts memory contracts + ) internal { + Executor adminExecutor = contracts.adminExecutor; + EmergencyProtectedTimelock timelock = contracts.timelock; + + contracts.emergencyGovernance = + deployTimelockedGovernance({governance: lidoAddresses.voting, timelock: timelock}); + + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall( + timelock.setEmergencyProtectionActivationCommittee, (dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE) + ) + ); + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall( + timelock.setEmergencyProtectionExecutionCommittee, (dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE) + ) + ); + + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall( + timelock.setEmergencyProtectionEndDate, + (dgDeployConfig.EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now())) + ) + ); + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall(timelock.setEmergencyModeDuration, (dgDeployConfig.EMERGENCY_MODE_DURATION)) + ); + + adminExecutor.execute( + address(timelock), + 0, + abi.encodeCall(timelock.setEmergencyGovernance, (address(contracts.emergencyGovernance))) + ); + } + + function deployExecutor(address owner) internal returns (Executor) { + return new Executor(owner); + } + + function deployEmergencyProtectedTimelock( + address adminExecutor, + DeployConfig memory dgDeployConfig + ) internal returns (EmergencyProtectedTimelock) { + return new EmergencyProtectedTimelock({ + adminExecutor: address(adminExecutor), + sanityCheckParams: EmergencyProtectedTimelock.SanityCheckParams({ + minExecutionDelay: dgDeployConfig.MIN_EXECUTION_DELAY, + maxAfterSubmitDelay: dgDeployConfig.MAX_AFTER_SUBMIT_DELAY, + maxAfterScheduleDelay: dgDeployConfig.MAX_AFTER_SCHEDULE_DELAY, + maxEmergencyModeDuration: dgDeployConfig.MAX_EMERGENCY_MODE_DURATION, + maxEmergencyProtectionDuration: dgDeployConfig.MAX_EMERGENCY_PROTECTION_DURATION + }), + afterSubmitDelay: dgDeployConfig.AFTER_SUBMIT_DELAY, + afterScheduleDelay: dgDeployConfig.AFTER_SCHEDULE_DELAY + }); + } + + function deployTimelockedGovernance( + address governance, + ITimelock timelock + ) internal returns (TimelockedGovernance) { + return new TimelockedGovernance(governance, timelock); + } + + function deployResealManager(ITimelock timelock) internal returns (ResealManager) { + return new ResealManager(timelock); + } + + function deployDualGovernanceConfigProvider(DeployConfig memory dgDeployConfig) + internal + returns (ImmutableDualGovernanceConfigProvider) + { + return new ImmutableDualGovernanceConfigProvider( + DualGovernanceConfig.Context({ + firstSealRageQuitSupport: dgDeployConfig.FIRST_SEAL_RAGE_QUIT_SUPPORT, + secondSealRageQuitSupport: dgDeployConfig.SECOND_SEAL_RAGE_QUIT_SUPPORT, + // + minAssetsLockDuration: dgDeployConfig.MIN_ASSETS_LOCK_DURATION, + vetoSignallingMinDuration: dgDeployConfig.VETO_SIGNALLING_MIN_DURATION, + vetoSignallingMaxDuration: dgDeployConfig.VETO_SIGNALLING_MAX_DURATION, + // + vetoSignallingMinActiveDuration: dgDeployConfig.VETO_SIGNALLING_MIN_ACTIVE_DURATION, + vetoSignallingDeactivationMaxDuration: dgDeployConfig.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION, + vetoCooldownDuration: dgDeployConfig.VETO_COOLDOWN_DURATION, + // + rageQuitExtensionPeriodDuration: dgDeployConfig.RAGE_QUIT_EXTENSION_PERIOD_DURATION, + rageQuitEthWithdrawalsMinDelay: dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY, + rageQuitEthWithdrawalsMaxDelay: dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY, + rageQuitEthWithdrawalsDelayGrowth: dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH + }) + ); + } + + function deployDualGovernance( + IDualGovernanceConfigProvider configProvider, + ITimelock timelock, + IResealManager resealManager, + DeployConfig memory dgDeployConfig, + LidoContracts memory lidoAddresses + ) internal returns (DualGovernance) { + return new DualGovernance({ + components: DualGovernance.DualGovernanceComponents({ + timelock: timelock, + resealManager: resealManager, + configProvider: configProvider + }), + signallingTokens: DualGovernance.SignallingTokens({ + stETH: lidoAddresses.stETH, + wstETH: lidoAddresses.wstETH, + withdrawalQueue: lidoAddresses.withdrawalQueue + }), + sanityCheckParams: DualGovernance.SanityCheckParams({ + minWithdrawalsBatchSize: dgDeployConfig.MIN_WITHDRAWALS_BATCH_SIZE, + minTiebreakerActivationTimeout: dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT, + maxTiebreakerActivationTimeout: dgDeployConfig.MAX_TIEBREAKER_ACTIVATION_TIMEOUT, + maxSealableWithdrawalBlockersCount: dgDeployConfig.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT, + maxMinAssetsLockDuration: dgDeployConfig.MAX_MIN_ASSETS_LOCK_DURATION + }) + }); + } + + function deployEmptyTiebreakerCoreCommittee( + address owner, + address dualGovernance, + Duration executionDelay + ) internal returns (TiebreakerCoreCommittee) { + return new TiebreakerCoreCommittee({owner: owner, dualGovernance: dualGovernance, timelock: executionDelay}); + } + + function deployTiebreakerSubCommittees( + address owner, + TiebreakerCoreCommittee tiebreakerCoreCommittee, + DeployConfig memory dgDeployConfig + ) internal returns (TiebreakerSubCommittee[] memory tiebreakerSubCommittees) { + tiebreakerSubCommittees = new TiebreakerSubCommittee[](dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT); + address[] memory coreCommitteeMembers = new address[](dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT); + + for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { + (uint256 quorum, address[] memory members) = getSubCommitteeData(i, dgDeployConfig); + + tiebreakerSubCommittees[i] = deployTiebreakerSubCommittee({ + owner: owner, + quorum: quorum, + members: members, + tiebreakerCoreCommittee: address(tiebreakerCoreCommittee) + }); + coreCommitteeMembers[i] = address(tiebreakerSubCommittees[i]); + } + + tiebreakerCoreCommittee.addMembers(coreCommitteeMembers, dgDeployConfig.TIEBREAKER_CORE_QUORUM); + + return tiebreakerSubCommittees; + } + + function deployTiebreakerSubCommittee( + address owner, + uint256 quorum, + address[] memory members, + address tiebreakerCoreCommittee + ) internal returns (TiebreakerSubCommittee) { + return new TiebreakerSubCommittee({ + owner: owner, + executionQuorum: quorum, + committeeMembers: members, + tiebreakerCoreCommittee: tiebreakerCoreCommittee + }); + } + + function configureDualGovernance( + DeployConfig memory dgDeployConfig, + LidoContracts memory lidoAddresses, + DeployedContracts memory contracts + ) internal { + contracts.adminExecutor.execute( + address(contracts.dualGovernance), + 0, + abi.encodeCall( + contracts.dualGovernance.registerProposer, (lidoAddresses.voting, address(contracts.adminExecutor)) + ) + ); + contracts.adminExecutor.execute( + address(contracts.dualGovernance), + 0, + abi.encodeCall(contracts.dualGovernance.setProposalsCanceller, address(lidoAddresses.voting)) + ); + contracts.adminExecutor.execute( + address(contracts.dualGovernance), + 0, + abi.encodeCall( + contracts.dualGovernance.setTiebreakerActivationTimeout, dgDeployConfig.TIEBREAKER_ACTIVATION_TIMEOUT + ) + ); + contracts.adminExecutor.execute( + address(contracts.dualGovernance), + 0, + abi.encodeCall(contracts.dualGovernance.setTiebreakerCommittee, address(contracts.tiebreakerCoreCommittee)) + ); + contracts.adminExecutor.execute( + address(contracts.dualGovernance), + 0, + abi.encodeCall( + contracts.dualGovernance.addTiebreakerSealableWithdrawalBlocker, address(lidoAddresses.withdrawalQueue) + ) + ); + contracts.adminExecutor.execute( + address(contracts.dualGovernance), + 0, + abi.encodeCall(contracts.dualGovernance.setResealCommittee, dgDeployConfig.RESEAL_COMMITTEE) + ); + } + + function finalizeEmergencyProtectedTimelockDeploy( + Executor adminExecutor, + EmergencyProtectedTimelock timelock, + address dualGovernance, + DeployConfig memory dgDeployConfig + ) internal { + adminExecutor.execute(address(timelock), 0, abi.encodeCall(timelock.setGovernance, (dualGovernance))); + adminExecutor.transferOwnership(address(timelock)); + } +} diff --git a/scripts/deploy/DeployBase.s.sol b/scripts/deploy/DeployBase.s.sol new file mode 100644 index 00000000..62efd5f9 --- /dev/null +++ b/scripts/deploy/DeployBase.s.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/* solhint-disable no-console */ + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; + +import {DeployConfig, LidoContracts} from "./Config.sol"; +import {DGContractsDeployment, DeployedContracts} from "./ContractsDeployment.sol"; +import {DeployVerification} from "./DeployVerification.sol"; + +abstract contract DeployBase is Script { + using DeployVerification for DeployVerification.DeployedAddresses; + + error ChainIdMismatch(uint256 actual, uint256 expected); + + DeployConfig internal config; + LidoContracts internal lidoAddresses; + address private deployer; + + function run() external { + if (lidoAddresses.chainId != block.chainid) { + revert ChainIdMismatch({actual: block.chainid, expected: lidoAddresses.chainId}); + } + + deployer = msg.sender; + vm.label(deployer, "DEPLOYER"); + + vm.startBroadcast(); + + DeployedContracts memory contracts = + DGContractsDeployment.deployDualGovernanceSetup(config, lidoAddresses, deployer); + + vm.stopBroadcast(); + + DeployVerification.DeployedAddresses memory res = getDeployedAddresses(contracts); + + printAddresses(res); + + console.log("Verifying deploy"); + + res.verify(config, lidoAddresses); + + console.log(unicode"Verified ✅"); + } + + function getDeployedAddresses(DeployedContracts memory contracts) + internal + pure + returns (DeployVerification.DeployedAddresses memory) + { + address[] memory tiebreakerSubCommittees = new address[](contracts.tiebreakerSubCommittees.length); + for (uint256 i = 0; i < contracts.tiebreakerSubCommittees.length; ++i) { + tiebreakerSubCommittees[i] = address(contracts.tiebreakerSubCommittees[i]); + } + + return DeployVerification.DeployedAddresses({ + adminExecutor: payable(address(contracts.adminExecutor)), + timelock: address(contracts.timelock), + emergencyGovernance: address(contracts.emergencyGovernance), + resealManager: address(contracts.resealManager), + dualGovernance: address(contracts.dualGovernance), + tiebreakerCoreCommittee: address(contracts.tiebreakerCoreCommittee), + tiebreakerSubCommittees: tiebreakerSubCommittees + }); + } + + function printAddresses(DeployVerification.DeployedAddresses memory res) internal pure { + console.log("DG deployed successfully"); + console.log("DualGovernance address", res.dualGovernance); + console.log("ResealManager address", res.resealManager); + console.log("TiebreakerCoreCommittee address", res.tiebreakerCoreCommittee); + + for (uint256 i = 0; i < res.tiebreakerSubCommittees.length; ++i) { + console.log("TiebreakerSubCommittee #", i, "address", res.tiebreakerSubCommittees[i]); + } + + console.log("AdminExecutor address", res.adminExecutor); + console.log("EmergencyProtectedTimelock address", res.timelock); + console.log("EmergencyGovernance address", res.emergencyGovernance); + } +} diff --git a/scripts/deploy/DeployConfigurable.s.sol b/scripts/deploy/DeployConfigurable.s.sol new file mode 100644 index 00000000..f2876dd6 --- /dev/null +++ b/scripts/deploy/DeployConfigurable.s.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {DGDeployJSONConfigProvider} from "./JsonConfig.s.sol"; +import {DeployBase} from "./DeployBase.s.sol"; + +contract DeployConfigurable is DeployBase { + constructor() { + string memory chainName = vm.envString("CHAIN"); + string memory configFilePath = vm.envString("DEPLOY_CONFIG_FILE_PATH"); + + DGDeployJSONConfigProvider configProvider = new DGDeployJSONConfigProvider(configFilePath); + config = configProvider.loadAndValidate(); + lidoAddresses = configProvider.getLidoAddresses(chainName); + } +} diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol new file mode 100644 index 00000000..dfbc4d7f --- /dev/null +++ b/scripts/deploy/DeployVerification.sol @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Timestamps} from "contracts/types/Timestamp.sol"; +import {Durations} from "contracts/types/Duration.sol"; +import {Executor} from "contracts/Executor.sol"; +import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; +import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; +import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; +import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; +import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; +import {ResealManager} from "contracts/ResealManager.sol"; +import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; +import {DualGovernance} from "contracts/DualGovernance.sol"; +import {Escrow} from "contracts/Escrow.sol"; +import {DualGovernanceConfig} from "contracts/libraries/DualGovernanceConfig.sol"; +import {State} from "contracts/libraries/DualGovernanceStateMachine.sol"; +import {DeployConfig, LidoContracts, getSubCommitteeData} from "./Config.sol"; + +library DeployVerification { + struct DeployedAddresses { + address payable adminExecutor; + address timelock; + address emergencyGovernance; + address resealManager; + address dualGovernance; + address tiebreakerCoreCommittee; + address[] tiebreakerSubCommittees; + } + + function verify( + DeployedAddresses memory res, + DeployConfig memory dgDeployConfig, + LidoContracts memory lidoAddresses + ) internal view { + checkAdminExecutor(res.adminExecutor, res.timelock); + checkTimelock(res, dgDeployConfig); + checkEmergencyActivationCommittee(dgDeployConfig); + checkEmergencyExecutionCommittee(dgDeployConfig); + checkTimelockedGovernance(res, lidoAddresses); + checkResealManager(res); + checkDualGovernance(res, dgDeployConfig, lidoAddresses); + checkTiebreakerCoreCommittee(res, dgDeployConfig); + + for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { + checkTiebreakerSubCommittee(res, dgDeployConfig, i); + } + + checkResealCommittee(dgDeployConfig); + } + + function checkAdminExecutor(address payable executor, address timelock) internal view { + require(Executor(executor).owner() == timelock, "AdminExecutor owner != EmergencyProtectedTimelock"); + } + + function checkTimelock(DeployedAddresses memory res, DeployConfig memory dgDeployConfig) internal view { + IEmergencyProtectedTimelock timelockInstance = IEmergencyProtectedTimelock(res.timelock); + require( + timelockInstance.getAdminExecutor() == res.adminExecutor, + "Incorrect adminExecutor address in EmergencyProtectedTimelock" + ); + require( + timelockInstance.MAX_AFTER_SUBMIT_DELAY() == dgDeployConfig.MAX_AFTER_SUBMIT_DELAY, + "Incorrect parameter MAX_AFTER_SUBMIT_DELAY" + ); + require( + timelockInstance.MAX_AFTER_SCHEDULE_DELAY() == dgDeployConfig.MAX_AFTER_SCHEDULE_DELAY, + "Incorrect parameter MAX_AFTER_SCHEDULE_DELAY" + ); + require( + timelockInstance.MAX_EMERGENCY_MODE_DURATION() == dgDeployConfig.MAX_EMERGENCY_MODE_DURATION, + "Incorrect parameter MAX_EMERGENCY_MODE_DURATION" + ); + require( + timelockInstance.MAX_EMERGENCY_PROTECTION_DURATION() == dgDeployConfig.MAX_EMERGENCY_PROTECTION_DURATION, + "Incorrect parameter MAX_EMERGENCY_PROTECTION_DURATION" + ); + + require( + timelockInstance.getEmergencyActivationCommittee() == dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE, + "Incorrect emergencyActivationCommittee address in EmergencyProtectedTimelock" + ); + require( + timelockInstance.getEmergencyExecutionCommittee() == dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE, + "Incorrect emergencyExecutionCommittee address in EmergencyProtectedTimelock" + ); + + IEmergencyProtectedTimelock.EmergencyProtectionDetails memory details = + timelockInstance.getEmergencyProtectionDetails(); + require( + details.emergencyProtectionEndsAfter <= dgDeployConfig.EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now()), + "Incorrect value for emergencyProtectionEndsAfter" + ); + require( + details.emergencyModeDuration == dgDeployConfig.EMERGENCY_MODE_DURATION, + "Incorrect value for emergencyModeDuration" + ); + + require( + timelockInstance.getEmergencyGovernance() == res.emergencyGovernance, + "Incorrect emergencyGovernance address in EmergencyProtectedTimelock" + ); + require( + timelockInstance.getAfterSubmitDelay() == dgDeployConfig.AFTER_SUBMIT_DELAY, + "Incorrect parameter AFTER_SUBMIT_DELAY" + ); + require( + timelockInstance.getAfterScheduleDelay() == dgDeployConfig.AFTER_SCHEDULE_DELAY, + "Incorrect parameter AFTER_SCHEDULE_DELAY" + ); + require( + timelockInstance.getGovernance() == res.dualGovernance, + "Incorrect governance address in EmergencyProtectedTimelock" + ); + require( + timelockInstance.isEmergencyProtectionEnabled() + == (details.emergencyProtectionEndsAfter >= Timestamps.now()), + "EmergencyProtection is Disabled in EmergencyProtectedTimelock" + ); + require( + timelockInstance.isEmergencyModeActive() == false, "EmergencyMode is Active in EmergencyProtectedTimelock" + ); + require(timelockInstance.getProposalsCount() == 0, "ProposalsCount > 0 in EmergencyProtectedTimelock"); + } + + function checkEmergencyActivationCommittee(DeployConfig memory dgDeployConfig) internal pure { + // TODO: implement! + require(dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE != address(0), "Incorrect emergencyActivationCommittee"); + } + + function checkEmergencyExecutionCommittee(DeployConfig memory dgDeployConfig) internal pure { + // TODO: implement! + require(dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE != address(0), "Incorrect emergencyExecutionCommittee"); + } + + function checkTimelockedGovernance( + DeployedAddresses memory res, + LidoContracts memory lidoAddresses + ) internal view { + TimelockedGovernance emergencyTimelockedGovernance = TimelockedGovernance(res.emergencyGovernance); + require( + emergencyTimelockedGovernance.GOVERNANCE() == lidoAddresses.voting, + "TimelockedGovernance governance != Lido voting" + ); + require( + address(emergencyTimelockedGovernance.TIMELOCK()) == res.timelock, + "Incorrect address for timelock in TimelockedGovernance" + ); + } + + function checkResealManager(DeployedAddresses memory res) internal view { + require( + address(ResealManager(res.resealManager).EMERGENCY_PROTECTED_TIMELOCK()) == res.timelock, + "Incorrect address for EMERGENCY_PROTECTED_TIMELOCK in ResealManager" + ); + } + + function checkDualGovernance( + DeployedAddresses memory res, + DeployConfig memory dgDeployConfig, + LidoContracts memory lidoAddresses + ) internal view { + DualGovernance dg = DualGovernance(res.dualGovernance); + require(address(dg.TIMELOCK()) == res.timelock, "Incorrect address for timelock in DualGovernance"); + require( + address(dg.getResealManager()) == res.resealManager, "Incorrect address for resealManager in DualGovernance" + ); + require( + dg.MIN_TIEBREAKER_ACTIVATION_TIMEOUT() == dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT, + "Incorrect parameter MIN_TIEBREAKER_ACTIVATION_TIMEOUT" + ); + require( + dg.MAX_TIEBREAKER_ACTIVATION_TIMEOUT() == dgDeployConfig.MAX_TIEBREAKER_ACTIVATION_TIMEOUT, + "Incorrect parameter MAX_TIEBREAKER_ACTIVATION_TIMEOUT" + ); + require( + dg.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT() == dgDeployConfig.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT, + "Incorrect parameter MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT" + ); + + Escrow escrowTemplate = Escrow(payable(address(IEscrowBase(dg.getVetoSignallingEscrow()).ESCROW_MASTER_COPY()))); + require(escrowTemplate.DUAL_GOVERNANCE() == dg, "Escrow has incorrect DualGovernance address"); + require(escrowTemplate.ST_ETH() == lidoAddresses.stETH, "Escrow has incorrect StETH address"); + require(escrowTemplate.WST_ETH() == lidoAddresses.wstETH, "Escrow has incorrect WstETH address"); + require( + escrowTemplate.WITHDRAWAL_QUEUE() == lidoAddresses.withdrawalQueue, + "Escrow has incorrect WithdrawalQueue address" + ); + require( + escrowTemplate.MIN_WITHDRAWALS_BATCH_SIZE() == dgDeployConfig.MIN_WITHDRAWALS_BATCH_SIZE, + "Incorrect parameter MIN_WITHDRAWALS_BATCH_SIZE" + ); + + DualGovernanceConfig.Context memory dgConfig = dg.getConfigProvider().getDualGovernanceConfig(); + require( + dgConfig.firstSealRageQuitSupport == dgDeployConfig.FIRST_SEAL_RAGE_QUIT_SUPPORT, + "Incorrect parameter FIRST_SEAL_RAGE_QUIT_SUPPORT" + ); + require( + dgConfig.secondSealRageQuitSupport == dgDeployConfig.SECOND_SEAL_RAGE_QUIT_SUPPORT, + "Incorrect parameter SECOND_SEAL_RAGE_QUIT_SUPPORT" + ); + require( + dgConfig.minAssetsLockDuration == dgDeployConfig.MIN_ASSETS_LOCK_DURATION, + "Incorrect parameter MIN_ASSETS_LOCK_DURATION" + ); + require( + dgConfig.vetoSignallingMinDuration == dgDeployConfig.VETO_SIGNALLING_MIN_DURATION, + "Incorrect parameter VETO_SIGNALLING_MIN_DURATION" + ); + require( + dgConfig.vetoSignallingMaxDuration == dgDeployConfig.VETO_SIGNALLING_MAX_DURATION, + "Incorrect parameter VETO_SIGNALLING_MAX_DURATION" + ); + require( + dgConfig.vetoSignallingMinActiveDuration == dgDeployConfig.VETO_SIGNALLING_MIN_ACTIVE_DURATION, + "Incorrect parameter VETO_SIGNALLING_MIN_ACTIVE_DURATION" + ); + require( + dgConfig.vetoSignallingDeactivationMaxDuration == dgDeployConfig.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION, + "Incorrect parameter VETO_SIGNALLING_DEACTIVATION_MAX_DURATION" + ); + require( + dgConfig.vetoCooldownDuration == dgDeployConfig.VETO_COOLDOWN_DURATION, + "Incorrect parameter VETO_COOLDOWN_DURATION" + ); + require( + dgConfig.rageQuitExtensionPeriodDuration == dgDeployConfig.RAGE_QUIT_EXTENSION_PERIOD_DURATION, + "Incorrect parameter RAGE_QUIT_EXTENSION_PERIOD_DURATION" + ); + require( + dgConfig.rageQuitEthWithdrawalsMinDelay == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY, + "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY" + ); + require( + dgConfig.rageQuitEthWithdrawalsMaxDelay == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY, + "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY" + ); + require( + dgConfig.rageQuitEthWithdrawalsDelayGrowth == dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH, + "Incorrect parameter RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH" + ); + + require(dg.getPersistedState() == State.Normal, "Incorrect DualGovernance persisted state"); + require(dg.getEffectiveState() == State.Normal, "Incorrect DualGovernance effective state"); + require(dg.getProposers().length == 1, "Incorrect amount of proposers"); + require(dg.isProposer(address(lidoAddresses.voting)) == true, "Lido voting is not set as a proposers[0]"); + require(dg.isExecutor(res.adminExecutor) == true, "adminExecutor is not set as a proposers[0].executor"); + + IDualGovernance.StateDetails memory stateDetails = dg.getStateDetails(); + require(stateDetails.effectiveState == State.Normal, "Incorrect DualGovernance effectiveState"); + require(stateDetails.persistedState == State.Normal, "Incorrect DualGovernance persistedState"); + require( + stateDetails.persistedStateEnteredAt <= Timestamps.now(), "Incorrect DualGovernance persistedStateEnteredAt" + ); + require( + stateDetails.vetoSignallingActivatedAt == Timestamps.ZERO, + "Incorrect DualGovernance state vetoSignallingActivatedAt" + ); + require( + stateDetails.vetoSignallingReactivationTime == Timestamps.ZERO, + "Incorrect DualGovernance state vetoSignallingReactivationTime" + ); + require( + stateDetails.normalOrVetoCooldownExitedAt == Timestamps.ZERO, + "Incorrect DualGovernance state normalOrVetoCooldownExitedAt" + ); + require(stateDetails.rageQuitRound == 0, "Incorrect DualGovernance state rageQuitRound"); + require( + stateDetails.vetoSignallingDuration == Durations.ZERO, + "Incorrect DualGovernance state vetoSignallingDuration" + ); + + ITiebreaker.TiebreakerDetails memory ts = dg.getTiebreakerDetails(); + require( + ts.tiebreakerActivationTimeout == dgDeployConfig.TIEBREAKER_ACTIVATION_TIMEOUT, + "Incorrect parameter TIEBREAKER_ACTIVATION_TIMEOUT" + ); + require(ts.tiebreakerCommittee == res.tiebreakerCoreCommittee, "Incorrect tiebreakerCoreCommittee"); + require(ts.sealableWithdrawalBlockers.length == 1, "Incorrect amount of sealableWithdrawalBlockers"); + require( + ts.sealableWithdrawalBlockers[0] == address(lidoAddresses.withdrawalQueue), + "Lido withdrawalQueue is not set as a sealableWithdrawalBlockers[0]" + ); + } + + function checkTiebreakerCoreCommittee( + DeployedAddresses memory res, + DeployConfig memory dgDeployConfig + ) internal view { + TiebreakerCoreCommittee tcc = TiebreakerCoreCommittee(res.tiebreakerCoreCommittee); + require(tcc.owner() == res.adminExecutor, "TiebreakerCoreCommittee owner != adminExecutor"); + require( + tcc.getTimelockDuration() == dgDeployConfig.TIEBREAKER_EXECUTION_DELAY, + "Incorrect parameter TIEBREAKER_EXECUTION_DELAY" + ); + + for (uint256 i = 0; i < dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { + require(tcc.isMember(res.tiebreakerSubCommittees[i]) == true, "Incorrect member of TiebreakerCoreCommittee"); + } + require(tcc.getQuorum() == dgDeployConfig.TIEBREAKER_CORE_QUORUM, "Incorrect quorum in TiebreakerCoreCommittee"); + } + + function checkTiebreakerSubCommittee( + DeployedAddresses memory res, + DeployConfig memory dgDeployConfig, + uint256 index + ) internal view { + TiebreakerSubCommittee tsc = TiebreakerSubCommittee(res.tiebreakerSubCommittees[index]); + require(tsc.owner() == res.adminExecutor, "TiebreakerSubCommittee owner != adminExecutor"); + require(tsc.getTimelockDuration() == Durations.from(0), "TiebreakerSubCommittee timelock should be 0"); + + (uint256 quorum, address[] memory members) = getSubCommitteeData(index, dgDeployConfig); + + for (uint256 i = 0; i < members.length; ++i) { + require(tsc.isMember(members[i]) == true, "Incorrect member of TiebreakerSubCommittee"); + } + require(tsc.getQuorum() == quorum, "Incorrect quorum in TiebreakerSubCommittee"); + } + + function checkResealCommittee(DeployConfig memory dgDeployConfig) internal pure { + // TODO: implement! + require(dgDeployConfig.RESEAL_COMMITTEE != address(0), "Incorrect resealCommittee"); + } +} diff --git a/scripts/deploy/JsonConfig.s.sol b/scripts/deploy/JsonConfig.s.sol new file mode 100644 index 00000000..6ff4e783 --- /dev/null +++ b/scripts/deploy/JsonConfig.s.sol @@ -0,0 +1,492 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/* solhint-disable no-console, var-name-mixedcase */ + +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {console} from "forge-std/console.sol"; +import {IStETH} from "contracts/interfaces/IStETH.sol"; +import {IWstETH} from "contracts/interfaces/IWstETH.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import { + ST_ETH as MAINNET_ST_ETH, + WST_ETH as MAINNET_WST_ETH, + WITHDRAWAL_QUEUE as MAINNET_WITHDRAWAL_QUEUE, + DAO_VOTING as MAINNET_DAO_VOTING +} from "addresses/mainnet-addresses.sol"; +import { + ST_ETH as HOLESKY_ST_ETH, + WST_ETH as HOLESKY_WST_ETH, + WITHDRAWAL_QUEUE as HOLESKY_WITHDRAWAL_QUEUE, + DAO_VOTING as HOLESKY_DAO_VOTING +} from "addresses/holesky-addresses.sol"; +import {Durations} from "contracts/types/Duration.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; +import { + DeployConfig, + LidoContracts, + DEFAULT_MIN_EXECUTION_DELAY, + DEFAULT_AFTER_SUBMIT_DELAY, + DEFAULT_MAX_AFTER_SUBMIT_DELAY, + DEFAULT_AFTER_SCHEDULE_DELAY, + DEFAULT_MAX_AFTER_SCHEDULE_DELAY, + DEFAULT_EMERGENCY_MODE_DURATION, + DEFAULT_MAX_EMERGENCY_MODE_DURATION, + DEFAULT_EMERGENCY_PROTECTION_DURATION, + DEFAULT_MAX_EMERGENCY_PROTECTION_DURATION, + DEFAULT_TIEBREAKER_CORE_QUORUM, + DEFAULT_TIEBREAKER_EXECUTION_DELAY, + DEFAULT_TIEBREAKER_SUB_COMMITTEES_COUNT, + DEFAULT_MIN_WITHDRAWALS_BATCH_SIZE, + DEFAULT_MIN_TIEBREAKER_ACTIVATION_TIMEOUT, + DEFAULT_TIEBREAKER_ACTIVATION_TIMEOUT, + DEFAULT_MAX_TIEBREAKER_ACTIVATION_TIMEOUT, + DEFAULT_MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT, + DEFAULT_FIRST_SEAL_RAGE_QUIT_SUPPORT, + DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT, + DEFAULT_MIN_ASSETS_LOCK_DURATION, + DEFAULT_MAX_MIN_ASSETS_LOCK_DURATION, + DEFAULT_VETO_SIGNALLING_MIN_DURATION, + DEFAULT_VETO_SIGNALLING_MAX_DURATION, + DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION, + DEFAULT_VETO_SIGNALLING_DEACTIVATION_MAX_DURATION, + DEFAULT_VETO_COOLDOWN_DURATION, + DEFAULT_RAGE_QUIT_EXTENSION_PERIOD_DURATION, + DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY, + DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY, + DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH, + CHAIN_NAME_MAINNET_HASH, + CHAIN_NAME_HOLESKY_HASH, + CHAIN_NAME_HOLESKY_MOCKS_HASH +} from "./Config.sol"; + +contract DGDeployJSONConfigProvider is Script { + error InvalidQuorum(string committee, uint256 quorum); + error InvalidParameter(string parameter); + error InvalidChain(string chainName); + + string private configFilePath; + + constructor(string memory _configFilePath) { + configFilePath = _configFilePath; + } + + function loadAndValidate() external view returns (DeployConfig memory config) { + string memory jsonConfig = loadConfigFile(); + + config = DeployConfig({ + MIN_EXECUTION_DELAY: Durations.from( + stdJson.readUintOr(jsonConfig, ".MIN_EXECUTION_DELAY", DEFAULT_MIN_EXECUTION_DELAY) + ), + AFTER_SUBMIT_DELAY: Durations.from( + stdJson.readUintOr(jsonConfig, ".AFTER_SUBMIT_DELAY", DEFAULT_AFTER_SUBMIT_DELAY) + ), + MAX_AFTER_SUBMIT_DELAY: Durations.from( + stdJson.readUintOr(jsonConfig, ".MAX_AFTER_SUBMIT_DELAY", DEFAULT_MAX_AFTER_SUBMIT_DELAY) + ), + AFTER_SCHEDULE_DELAY: Durations.from( + stdJson.readUintOr(jsonConfig, ".AFTER_SCHEDULE_DELAY", DEFAULT_AFTER_SCHEDULE_DELAY) + ), + MAX_AFTER_SCHEDULE_DELAY: Durations.from( + stdJson.readUintOr(jsonConfig, ".MAX_AFTER_SCHEDULE_DELAY", DEFAULT_MAX_AFTER_SCHEDULE_DELAY) + ), + EMERGENCY_MODE_DURATION: Durations.from( + stdJson.readUintOr(jsonConfig, ".EMERGENCY_MODE_DURATION", DEFAULT_EMERGENCY_MODE_DURATION) + ), + MAX_EMERGENCY_MODE_DURATION: Durations.from( + stdJson.readUintOr(jsonConfig, ".MAX_EMERGENCY_MODE_DURATION", DEFAULT_MAX_EMERGENCY_MODE_DURATION) + ), + EMERGENCY_PROTECTION_DURATION: Durations.from( + stdJson.readUintOr(jsonConfig, ".EMERGENCY_PROTECTION_DURATION", DEFAULT_EMERGENCY_PROTECTION_DURATION) + ), + MAX_EMERGENCY_PROTECTION_DURATION: Durations.from( + stdJson.readUintOr( + jsonConfig, ".MAX_EMERGENCY_PROTECTION_DURATION", DEFAULT_MAX_EMERGENCY_PROTECTION_DURATION + ) + ), + EMERGENCY_ACTIVATION_COMMITTEE: stdJson.readAddress(jsonConfig, ".EMERGENCY_ACTIVATION_COMMITTEE"), + EMERGENCY_EXECUTION_COMMITTEE: stdJson.readAddress(jsonConfig, ".EMERGENCY_EXECUTION_COMMITTEE"), + TIEBREAKER_CORE_QUORUM: stdJson.readUintOr( + jsonConfig, ".TIEBREAKER_CORE_QUORUM", DEFAULT_TIEBREAKER_CORE_QUORUM + ), + TIEBREAKER_EXECUTION_DELAY: Durations.from( + stdJson.readUintOr(jsonConfig, ".TIEBREAKER_EXECUTION_DELAY", DEFAULT_TIEBREAKER_EXECUTION_DELAY) + ), + TIEBREAKER_SUB_COMMITTEES_COUNT: stdJson.readUintOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEES_COUNT", DEFAULT_TIEBREAKER_SUB_COMMITTEES_COUNT + ), + TIEBREAKER_SUB_COMMITTEE_1_MEMBERS: stdJson.readAddressArray(jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_1_MEMBERS"), + TIEBREAKER_SUB_COMMITTEE_2_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_2_MEMBERS", new address[](0) + ), + TIEBREAKER_SUB_COMMITTEE_3_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_3_MEMBERS", new address[](0) + ), + TIEBREAKER_SUB_COMMITTEE_4_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_4_MEMBERS", new address[](0) + ), + TIEBREAKER_SUB_COMMITTEE_5_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_5_MEMBERS", new address[](0) + ), + TIEBREAKER_SUB_COMMITTEE_6_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_6_MEMBERS", new address[](0) + ), + TIEBREAKER_SUB_COMMITTEE_7_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_7_MEMBERS", new address[](0) + ), + TIEBREAKER_SUB_COMMITTEE_8_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_8_MEMBERS", new address[](0) + ), + TIEBREAKER_SUB_COMMITTEE_9_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_9_MEMBERS", new address[](0) + ), + TIEBREAKER_SUB_COMMITTEE_10_MEMBERS: stdJson.readAddressArrayOr( + jsonConfig, ".TIEBREAKER_SUB_COMMITTEE_10_MEMBERS", new address[](0) + ), + TIEBREAKER_SUB_COMMITTEES_QUORUMS: stdJson.readUintArray(jsonConfig, ".TIEBREAKER_SUB_COMMITTEES_QUORUMS"), + RESEAL_COMMITTEE: stdJson.readAddress(jsonConfig, ".RESEAL_COMMITTEE"), + MIN_WITHDRAWALS_BATCH_SIZE: stdJson.readUintOr( + jsonConfig, ".MIN_WITHDRAWALS_BATCH_SIZE", DEFAULT_MIN_WITHDRAWALS_BATCH_SIZE + ), + MIN_TIEBREAKER_ACTIVATION_TIMEOUT: Durations.from( + stdJson.readUintOr( + jsonConfig, ".MIN_TIEBREAKER_ACTIVATION_TIMEOUT", DEFAULT_MIN_TIEBREAKER_ACTIVATION_TIMEOUT + ) + ), + TIEBREAKER_ACTIVATION_TIMEOUT: Durations.from( + stdJson.readUintOr(jsonConfig, ".TIEBREAKER_ACTIVATION_TIMEOUT", DEFAULT_TIEBREAKER_ACTIVATION_TIMEOUT) + ), + MAX_TIEBREAKER_ACTIVATION_TIMEOUT: Durations.from( + stdJson.readUintOr( + jsonConfig, ".MAX_TIEBREAKER_ACTIVATION_TIMEOUT", DEFAULT_MAX_TIEBREAKER_ACTIVATION_TIMEOUT + ) + ), + MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT: stdJson.readUintOr( + jsonConfig, ".MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT", DEFAULT_MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT + ), + FIRST_SEAL_RAGE_QUIT_SUPPORT: PercentsD16.fromBasisPoints( + stdJson.readUintOr(jsonConfig, ".FIRST_SEAL_RAGE_QUIT_SUPPORT", DEFAULT_FIRST_SEAL_RAGE_QUIT_SUPPORT) + ), + SECOND_SEAL_RAGE_QUIT_SUPPORT: PercentsD16.fromBasisPoints( + stdJson.readUintOr(jsonConfig, ".SECOND_SEAL_RAGE_QUIT_SUPPORT", DEFAULT_SECOND_SEAL_RAGE_QUIT_SUPPORT) + ), + MIN_ASSETS_LOCK_DURATION: Durations.from( + stdJson.readUintOr(jsonConfig, ".MIN_ASSETS_LOCK_DURATION", DEFAULT_MIN_ASSETS_LOCK_DURATION) + ), + MAX_MIN_ASSETS_LOCK_DURATION: Durations.from( + stdJson.readUintOr(jsonConfig, ".MAX_MIN_ASSETS_LOCK_DURATION", DEFAULT_MAX_MIN_ASSETS_LOCK_DURATION) + ), + VETO_SIGNALLING_MIN_DURATION: Durations.from( + stdJson.readUintOr(jsonConfig, ".VETO_SIGNALLING_MIN_DURATION", DEFAULT_VETO_SIGNALLING_MIN_DURATION) + ), + VETO_SIGNALLING_MAX_DURATION: Durations.from( + stdJson.readUintOr(jsonConfig, ".VETO_SIGNALLING_MAX_DURATION", DEFAULT_VETO_SIGNALLING_MAX_DURATION) + ), + VETO_SIGNALLING_MIN_ACTIVE_DURATION: Durations.from( + stdJson.readUintOr( + jsonConfig, ".VETO_SIGNALLING_MIN_ACTIVE_DURATION", DEFAULT_VETO_SIGNALLING_MIN_ACTIVE_DURATION + ) + ), + VETO_SIGNALLING_DEACTIVATION_MAX_DURATION: Durations.from( + stdJson.readUintOr( + jsonConfig, + ".VETO_SIGNALLING_DEACTIVATION_MAX_DURATION", + DEFAULT_VETO_SIGNALLING_DEACTIVATION_MAX_DURATION + ) + ), + VETO_COOLDOWN_DURATION: Durations.from( + stdJson.readUintOr(jsonConfig, ".VETO_COOLDOWN_DURATION", DEFAULT_VETO_COOLDOWN_DURATION) + ), + RAGE_QUIT_EXTENSION_PERIOD_DURATION: Durations.from( + stdJson.readUintOr( + jsonConfig, ".RAGE_QUIT_EXTENSION_PERIOD_DURATION", DEFAULT_RAGE_QUIT_EXTENSION_PERIOD_DURATION + ) + ), + RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY: Durations.from( + stdJson.readUintOr( + jsonConfig, ".RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY", DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY + ) + ), + RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY: Durations.from( + stdJson.readUintOr( + jsonConfig, ".RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY", DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY + ) + ), + RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH: Durations.from( + stdJson.readUintOr( + jsonConfig, ".RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH", DEFAULT_RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH + ) + ) + }); + + validateConfig(config); + printCommittees(config); + } + + function getLidoAddresses(string memory chainName) external view returns (LidoContracts memory) { + bytes32 chainNameHash = keccak256(bytes(chainName)); + if ( + chainNameHash != CHAIN_NAME_MAINNET_HASH && chainNameHash != CHAIN_NAME_HOLESKY_HASH + && chainNameHash != CHAIN_NAME_HOLESKY_MOCKS_HASH + ) { + revert InvalidChain(chainName); + } + + if (keccak256(bytes(chainName)) == CHAIN_NAME_MAINNET_HASH) { + return LidoContracts({ + chainId: 1, + stETH: IStETH(MAINNET_ST_ETH), + wstETH: IWstETH(MAINNET_WST_ETH), + withdrawalQueue: IWithdrawalQueue(MAINNET_WITHDRAWAL_QUEUE), + voting: MAINNET_DAO_VOTING + }); + } + + if (keccak256(bytes(chainName)) == CHAIN_NAME_HOLESKY_MOCKS_HASH) { + string memory jsonConfig = loadConfigFile(); + + return LidoContracts({ + chainId: 17000, + stETH: IStETH(stdJson.readAddress(jsonConfig, ".HOLESKY_MOCK_ST_ETH")), + wstETH: IWstETH(stdJson.readAddress(jsonConfig, ".HOLESKY_MOCK_WST_ETH")), + withdrawalQueue: IWithdrawalQueue(stdJson.readAddress(jsonConfig, ".HOLESKY_MOCK_WITHDRAWAL_QUEUE")), + voting: stdJson.readAddress(jsonConfig, ".HOLESKY_MOCK_DAO_VOTING") + }); + } + + return LidoContracts({ + chainId: 17000, + stETH: IStETH(HOLESKY_ST_ETH), + wstETH: IWstETH(HOLESKY_WST_ETH), + withdrawalQueue: IWithdrawalQueue(HOLESKY_WITHDRAWAL_QUEUE), + voting: HOLESKY_DAO_VOTING + }); + } + + function validateConfig(DeployConfig memory config) internal pure { + if ( + config.TIEBREAKER_CORE_QUORUM == 0 || config.TIEBREAKER_CORE_QUORUM > config.TIEBREAKER_SUB_COMMITTEES_COUNT + ) { + revert InvalidQuorum("TIEBREAKER_CORE", config.TIEBREAKER_CORE_QUORUM); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT == 0 || config.TIEBREAKER_SUB_COMMITTEES_COUNT > 10) { + revert InvalidParameter("TIEBREAKER_SUB_COMMITTEES_COUNT"); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_QUORUMS.length != config.TIEBREAKER_SUB_COMMITTEES_COUNT) { + revert InvalidParameter("TIEBREAKER_SUB_COMMITTEES_QUORUMS"); + } + + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[0], + "TIEBREAKER_SUB_COMMITTEE_1" + ); + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 2) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[1], + "TIEBREAKER_SUB_COMMITTEE_2" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 3) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_3_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[2], + "TIEBREAKER_SUB_COMMITTEE_3" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 4) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_4_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[3], + "TIEBREAKER_SUB_COMMITTEE_4" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 5) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_5_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[4], + "TIEBREAKER_SUB_COMMITTEE_5" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 6) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_6_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[5], + "TIEBREAKER_SUB_COMMITTEE_6" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 7) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_7_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[6], + "TIEBREAKER_SUB_COMMITTEE_7" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 8) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_8_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[7], + "TIEBREAKER_SUB_COMMITTEE_8" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 9) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_9_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[8], + "TIEBREAKER_SUB_COMMITTEE_9" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT == 10) { + checkCommitteeQuorum( + config.TIEBREAKER_SUB_COMMITTEE_10_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[9], + "TIEBREAKER_SUB_COMMITTEE_10" + ); + } + + if (config.AFTER_SUBMIT_DELAY > config.MAX_AFTER_SUBMIT_DELAY) { + revert InvalidParameter("AFTER_SUBMIT_DELAY"); + } + + if (config.AFTER_SCHEDULE_DELAY > config.MAX_AFTER_SCHEDULE_DELAY) { + revert InvalidParameter("AFTER_SCHEDULE_DELAY"); + } + + if (config.EMERGENCY_MODE_DURATION > config.MAX_EMERGENCY_MODE_DURATION) { + revert InvalidParameter("EMERGENCY_MODE_DURATION"); + } + + if (config.MIN_TIEBREAKER_ACTIVATION_TIMEOUT > config.TIEBREAKER_ACTIVATION_TIMEOUT) { + revert InvalidParameter("MIN_TIEBREAKER_ACTIVATION_TIMEOUT"); + } + + if (config.TIEBREAKER_ACTIVATION_TIMEOUT > config.MAX_TIEBREAKER_ACTIVATION_TIMEOUT) { + revert InvalidParameter("TIEBREAKER_ACTIVATION_TIMEOUT"); + } + + if (config.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT == 0) { + revert InvalidParameter("MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT"); + } + + if (config.VETO_SIGNALLING_MIN_DURATION > config.VETO_SIGNALLING_MAX_DURATION) { + revert InvalidParameter("VETO_SIGNALLING_MIN_DURATION"); + } + } + + function checkCommitteeQuorum(address[] memory committee, uint256 quorum, string memory message) internal pure { + if (quorum == 0 || quorum > committee.length) { + revert InvalidQuorum(message, quorum); + } + } + + function printCommittees(DeployConfig memory config) internal pure { + console.log("================================================="); + console.log("Loaded valid config with the following committees:"); + + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[0], + "TiebreakerSubCommittee #1 members, quorum" + ); + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 2) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[1], + "TiebreakerSubCommittee #2 members, quorum" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 3) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_3_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[2], + "TiebreakerSubCommittee #3 members, quorum" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 4) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_4_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[3], + "TiebreakerSubCommittee #4 members, quorum" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 5) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_5_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[4], + "TiebreakerSubCommittee #5 members, quorum" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 6) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_6_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[5], + "TiebreakerSubCommittee #6 members, quorum" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 7) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_7_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[6], + "TiebreakerSubCommittee #7 members, quorum" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 8) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_8_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[7], + "TiebreakerSubCommittee #8 members, quorum" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT >= 9) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_9_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[8], + "TiebreakerSubCommittee #9 members, quorum" + ); + } + + if (config.TIEBREAKER_SUB_COMMITTEES_COUNT == 10) { + printCommittee( + config.TIEBREAKER_SUB_COMMITTEE_10_MEMBERS, + config.TIEBREAKER_SUB_COMMITTEES_QUORUMS[9], + "TiebreakerSubCommittee #10 members, quorum" + ); + } + + console.log("================================================="); + } + + function printCommittee(address[] memory committee, uint256 quorum, string memory message) internal pure { + console.log(message, quorum, "of", committee.length); + for (uint256 k = 0; k < committee.length; ++k) { + console.log(">> #", k, address(committee[k])); + } + } + + function loadConfigFile() internal view returns (string memory jsonConfig) { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/", configFilePath); + jsonConfig = vm.readFile(path); + } +} diff --git a/scripts/deploy/Readme.md b/scripts/deploy/Readme.md new file mode 100644 index 00000000..5ae27630 --- /dev/null +++ b/scripts/deploy/Readme.md @@ -0,0 +1,103 @@ +# Dual Governance deploy scripts + +### Running locally with Anvil + +Start Anvil, provide RPC URL (Infura as an example) +``` +anvil --fork-url https://.infura.io/v3/ --block-time 300 +``` + +### Running the deploy script + +1. Import your private key to Cast wallet ([see the docs](https://book.getfoundry.sh/reference/cast/cast-wallet-import)), for example (we will use the account name `Deployer1` here and further for the simplicity): + + ``` + cast wallet import Deployer1 --interactive + ``` + +2. Set up the required env variables in the .env file + + ``` + CHAIN=<"mainnet" OR "holesky" OR "holesky-mocks"> + ETHERSCAN_MAINNET_KEY=... + DEPLOY_CONFIG_FILE_PATH=... (for example: "deploy-config/deploy-config.json") + ``` + +3. Create a deploy config JSON file with all the required values (at the location specified in DEPLOY_CONFIG_FILE_PATH): + ``` + { + "EMERGENCY_ACTIVATION_COMMITTEE":
, + "EMERGENCY_EXECUTION_COMMITTEE":
, + "TIEBREAKER_SUB_COMMITTEE_1_MEMBERS": [addr1,addr2,addr3], + "TIEBREAKER_SUB_COMMITTEE_2_MEMBERS": [addr1,addr2,addr3], + "TIEBREAKER_SUB_COMMITTEES_QUORUMS": [3,2], + "TIEBREAKER_SUB_COMMITTEES_COUNT": 2, + "RESEAL_COMMITTEE":
+ } + ``` + + When using `CHAIN="holesky-mocks"` you will need to provide in addition already deployed mock contracts addresses in the same JSON config file (at DEPLOY_CONFIG_FILE_PATH): + + ``` + { + ... + "HOLESKY_MOCK_ST_ETH": ..., + "HOLESKY_MOCK_WST_ETH": ..., + "HOLESKY_MOCK_WITHDRAWAL_QUEUE": ..., + "HOLESKY_MOCK_DAO_VOTING": ..., + ... + } + ``` + +4. Run the deployment script + + With the local fork (Anvil): + ``` + forge script scripts/deploy/DeployConfigurable.s.sol:DeployConfigurable --fork-url http://localhost:8545 --broadcast --account Deployer1 --sender + ``` + + On a testnet (with Etherscan verification): + ``` + forge script scripts/deploy/DeployConfigurable.s.sol:DeployConfigurable --fork-url https://holesky.infura.io/v3/ --broadcast --account Deployer1 --sender --verify + ``` + +5. [Testnet and mainnet deployment only] Run Etherscan verification for Escrow contract + + The Escrow contract is deployed internally by DualGovernance contract, so it can't be verified automatically during the initial deployment and requires manual verification afterward. To run Etherscan verification: + + a. Query the deployed DualGovernance contract instance for ESCROW_MASTER_COPY address. + + b. Run Etherscan verification (for example on a Holesky testnet) + + ``` + forge verify-contract --chain holesky --verifier-url https://api-holesky.etherscan.io/api --watch --constructor-args $(cast abi-encode "Escrow(address,address,address,address,uint256)" ) contracts/Escrow.sol:Escrow + ``` + +### Running the verification script + +1. Set up the required env variables in the .env file + + ``` + CHAIN=<"mainnet" OR "holesky" OR "holesky-mocks"> + DEPLOYED_ADDRESSES_FILE_PATH=... (for example: "deploy-config/deployed-addrs.json") + ``` + +2. Create a deployed addresses list JSON file with all the required values (at the location specified in DEPLOYED_ADDRESSES_FILE_PATH): + + ``` + { + "ADMIN_EXECUTOR": "...", + "TIMELOCK": "...", + "EMERGENCY_GOVERNANCE": "...", + "RESEAL_MANAGER": "...", + "DUAL_GOVERNANCE": "...", + "TIEBREAKER_CORE_COMMITTEE": "...", + "TIEBREAKER_SUB_COMMITTEES": ["...", "..."] + } + ``` + +3. Run the script (with the local Anvil as an example) + + ``` + forge script scripts/deploy/Verify.s.sol:Verify --fork-url http://localhost:8545 --broadcast + ``` diff --git a/scripts/deploy/Verify.s.sol b/scripts/deploy/Verify.s.sol new file mode 100644 index 00000000..2b95fa6f --- /dev/null +++ b/scripts/deploy/Verify.s.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/* solhint-disable no-console */ + +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {console} from "forge-std/console.sol"; + +import {DeployConfig, LidoContracts} from "./Config.sol"; +import {DGDeployJSONConfigProvider} from "./JsonConfig.s.sol"; +import {DeployVerification} from "./DeployVerification.sol"; + +contract Verify is Script { + using DeployVerification for DeployVerification.DeployedAddresses; + + DeployConfig internal config; + LidoContracts internal lidoAddresses; + + function run() external { + string memory chainName = vm.envString("CHAIN"); + string memory configFilePath = vm.envString("DEPLOY_CONFIG_FILE_PATH"); + string memory deployedAddressesFilePath = vm.envString("DEPLOYED_ADDRESSES_FILE_PATH"); + + DGDeployJSONConfigProvider configProvider = new DGDeployJSONConfigProvider(configFilePath); + config = configProvider.loadAndValidate(); + lidoAddresses = configProvider.getLidoAddresses(chainName); + + DeployVerification.DeployedAddresses memory res = loadDeployedAddresses(deployedAddressesFilePath); + + printAddresses(res); + + console.log("Verifying deploy"); + + res.verify(config, lidoAddresses); + + console.log(unicode"Verified ✅"); + } + + function loadDeployedAddresses(string memory deployedAddressesFilePath) + internal + view + returns (DeployVerification.DeployedAddresses memory) + { + string memory deployedAddressesJson = loadDeployedAddressesFile(deployedAddressesFilePath); + + return DeployVerification.DeployedAddresses({ + adminExecutor: payable(stdJson.readAddress(deployedAddressesJson, ".ADMIN_EXECUTOR")), + timelock: stdJson.readAddress(deployedAddressesJson, ".TIMELOCK"), + emergencyGovernance: stdJson.readAddress(deployedAddressesJson, ".EMERGENCY_GOVERNANCE"), + resealManager: stdJson.readAddress(deployedAddressesJson, ".RESEAL_MANAGER"), + dualGovernance: stdJson.readAddress(deployedAddressesJson, ".DUAL_GOVERNANCE"), + tiebreakerCoreCommittee: stdJson.readAddress(deployedAddressesJson, ".TIEBREAKER_CORE_COMMITTEE"), + tiebreakerSubCommittees: stdJson.readAddressArray(deployedAddressesJson, ".TIEBREAKER_SUB_COMMITTEES") + }); + } + + function printAddresses(DeployVerification.DeployedAddresses memory res) internal pure { + console.log("Using the following DG contracts addresses"); + console.log("DualGovernance address", res.dualGovernance); + console.log("ResealManager address", res.resealManager); + console.log("TiebreakerCoreCommittee address", res.tiebreakerCoreCommittee); + + for (uint256 i = 0; i < res.tiebreakerSubCommittees.length; ++i) { + console.log("TiebreakerSubCommittee #", i, "address", res.tiebreakerSubCommittees[i]); + } + + console.log("AdminExecutor address", res.adminExecutor); + console.log("EmergencyProtectedTimelock address", res.timelock); + console.log("EmergencyGovernance address", res.emergencyGovernance); + } + + function loadDeployedAddressesFile(string memory deployedAddressesFilePath) + internal + view + returns (string memory deployedAddressesJson) + { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/", deployedAddressesFilePath); + deployedAddressesJson = vm.readFile(path); + } +} diff --git a/scripts/lido-mocks/DeployHoleskyLidoMocks.s.sol b/scripts/lido-mocks/DeployHoleskyLidoMocks.s.sol new file mode 100644 index 00000000..2532dc1c --- /dev/null +++ b/scripts/lido-mocks/DeployHoleskyLidoMocks.s.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/* solhint-disable no-console */ + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {StETHMock} from "./StETHMock.sol"; +import {WstETHMock} from "./WstETHMock.sol"; +import {UnsafeWithdrawalQueueMock} from "./UnsafeWithdrawalQueueMock.sol"; + +struct DeployedContracts { + address stETH; + address wstETH; + address withdrawalQueue; +} + +contract DeployHoleskyLidoMocks is Script { + error DoNotRunThisOnMainnet(uint256 currentChainId); + + address private deployer; + + function run() external { + if (1 == block.chainid) { + revert DoNotRunThisOnMainnet(block.chainid); + } + + deployer = msg.sender; + vm.label(deployer, "DEPLOYER"); + + vm.startBroadcast(); + + DeployedContracts memory res = deployLidoMockContracts(); + + vm.stopBroadcast(); + + printAddresses(res); + } + + function deployLidoMockContracts() internal returns (DeployedContracts memory res) { + StETHMock stETH = new StETHMock(); + WstETHMock wstETH = new WstETHMock(stETH); + UnsafeWithdrawalQueueMock withdrawalQueue = new UnsafeWithdrawalQueueMock(address(stETH), payable(deployer)); + + stETH.mint(deployer, 100 gwei); + + res.stETH = address(stETH); + res.wstETH = address(wstETH); + res.withdrawalQueue = address(withdrawalQueue); + } + + function printAddresses(DeployedContracts memory res) internal pure { + console.log("Lido mocks deployed successfully"); + console.log("StETH address", res.stETH); + console.log("WstETH address", res.wstETH); + console.log("WithdrawalQueue address", res.withdrawalQueue); + } +} diff --git a/scripts/lido-mocks/Readme.md b/scripts/lido-mocks/Readme.md new file mode 100644 index 00000000..0c6a4ad2 --- /dev/null +++ b/scripts/lido-mocks/Readme.md @@ -0,0 +1,13 @@ +# Lido contracts mocks for Dual Governance testing on Holesky + +### Deploying + + With the local fork (Anvil): + ``` + forge script scripts/lido-mocks/DeployHoleskyLidoMocks.s.sol:DeployHoleskyLidoMocks --fork-url http://localhost:8545 --broadcast --account Deployer1 --sender + ``` + + On a testnet (with Etherscan verification): + ``` + forge script scripts/lido-mocks/DeployHoleskyLidoMocks.s.sol:DeployHoleskyLidoMocks --fork-url https://holesky.infura.io/v3/ --broadcast --account Deployer1 --sender --verify + ``` \ No newline at end of file diff --git a/scripts/lido-mocks/StETHBase.sol b/scripts/lido-mocks/StETHBase.sol new file mode 100644 index 00000000..b888851a --- /dev/null +++ b/scripts/lido-mocks/StETHBase.sol @@ -0,0 +1,193 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.26; + +import {IStETH} from "contracts/interfaces/IStETH.sol"; + +/* solhint-disable custom-errors */ + +abstract contract StETHBase is IStETH { + address internal constant INITIAL_TOKEN_HOLDER = address(0xdead); + uint256 internal constant INFINITE_ALLOWANCE = ~uint256(0); + + mapping(address => uint256) private shares; + mapping(address => mapping(address => uint256)) private allowances; + + event TransferShares(address indexed from, address indexed to, uint256 sharesValue); + + event SharesBurnt( + address indexed account, uint256 preRebaseTokenAmount, uint256 postRebaseTokenAmount, uint256 sharesAmount + ); + + uint256 internal _totalPooledEther; + uint256 internal _totalShares; + + function name() external pure virtual returns (string memory); + + function symbol() external pure virtual returns (string memory); + + function decimals() external pure returns (uint8) { + return 18; + } + + function totalSupply() external view returns (uint256) { + return _totalPooledEther; + } + + function getTotalPooledEther() external view returns (uint256) { + return _totalPooledEther; + } + + function balanceOf(address _account) external view returns (uint256) { + return getPooledEthByShares(_sharesOf(_account)); + } + + function transfer(address _recipient, uint256 _amount) external returns (bool) { + _transfer(msg.sender, _recipient, _amount); + return true; + } + + function allowance(address _owner, address _spender) external view returns (uint256) { + return allowances[_owner][_spender]; + } + + function approve(address _spender, uint256 _amount) external returns (bool) { + _approve(msg.sender, _spender, _amount); + return true; + } + + function transferFrom(address _sender, address _recipient, uint256 _amount) external returns (bool) { + _spendAllowance(_sender, msg.sender, _amount); + _transfer(_sender, _recipient, _amount); + return true; + } + + function increaseAllowance(address _spender, uint256 _addedValue) external returns (bool) { + _approve(msg.sender, _spender, allowances[msg.sender][_spender] + _addedValue); + return true; + } + + function decreaseAllowance(address _spender, uint256 _subtractedValue) external returns (bool) { + uint256 currentAllowance = allowances[msg.sender][_spender]; + require(currentAllowance >= _subtractedValue, "ALLOWANCE_BELOW_ZERO"); + _approve(msg.sender, _spender, currentAllowance - _subtractedValue); + return true; + } + + function getTotalShares() external view returns (uint256) { + return _totalShares; + } + + function sharesOf(address _account) external view returns (uint256) { + return _sharesOf(_account); + } + + function getSharesByPooledEth(uint256 _ethAmount) public view returns (uint256) { + return (_ethAmount * _totalShares) / _totalPooledEther; + } + + function getPooledEthByShares(uint256 _sharesAmount) public view returns (uint256) { + return (_sharesAmount * _totalPooledEther) / _totalShares; + } + + function transferShares(address _recipient, uint256 _sharesAmount) external returns (uint256) { + _transferShares(msg.sender, _recipient, _sharesAmount); + uint256 tokensAmount = getPooledEthByShares(_sharesAmount); + _emitTransferEvents(msg.sender, _recipient, tokensAmount, _sharesAmount); + return tokensAmount; + } + + function transferSharesFrom( + address _sender, + address _recipient, + uint256 _sharesAmount + ) external returns (uint256) { + uint256 tokensAmount = getPooledEthByShares(_sharesAmount); + _spendAllowance(_sender, msg.sender, tokensAmount); + _transferShares(_sender, _recipient, _sharesAmount); + _emitTransferEvents(_sender, _recipient, tokensAmount, _sharesAmount); + return tokensAmount; + } + + function _transfer(address _sender, address _recipient, uint256 _amount) internal { + uint256 _sharesToTransfer = getSharesByPooledEth(_amount); + _transferShares(_sender, _recipient, _sharesToTransfer); + _emitTransferEvents(_sender, _recipient, _amount, _sharesToTransfer); + } + + function _approve(address _owner, address _spender, uint256 _amount) internal { + require(_owner != address(0), "APPROVE_FROM_ZERO_ADDR"); + require(_spender != address(0), "APPROVE_TO_ZERO_ADDR"); + + allowances[_owner][_spender] = _amount; + emit Approval(_owner, _spender, _amount); + } + + function _spendAllowance(address _owner, address _spender, uint256 _amount) internal { + uint256 currentAllowance = allowances[_owner][_spender]; + if (currentAllowance != INFINITE_ALLOWANCE) { + require(currentAllowance >= _amount, "ALLOWANCE_EXCEEDED"); + _approve(_owner, _spender, currentAllowance - _amount); + } + } + + function _sharesOf(address _account) internal view returns (uint256) { + return shares[_account]; + } + + function _transferShares(address _sender, address _recipient, uint256 _sharesAmount) internal { + require(_sender != address(0), "TRANSFER_FROM_ZERO_ADDR"); + require(_recipient != address(0), "TRANSFER_TO_ZERO_ADDR"); + require(_recipient != address(this), "TRANSFER_TO_STETH_CONTRACT"); + + // Pausable functionality not needed for this implementation // _whenNotStopped(); + + uint256 currentSenderShares = shares[_sender]; + require(_sharesAmount <= currentSenderShares, "BALANCE_EXCEEDED"); + + shares[_sender] = currentSenderShares - _sharesAmount; + shares[_recipient] = shares[_recipient] + _sharesAmount; + } + + function _mintShares(address _recipient, uint256 _sharesAmount) internal returns (uint256 newTotalShares) { + require(_recipient != address(0), "MINT_TO_ZERO_ADDR"); + + newTotalShares = _totalShares + _sharesAmount; + _totalShares = newTotalShares; + + shares[_recipient] += _sharesAmount; + } + + function _burnShares(address _account, uint256 _sharesAmount) internal returns (uint256 newTotalShares) { + require(_account != address(0), "BURN_FROM_ZERO_ADDR"); + + uint256 accountShares = shares[_account]; + require(_sharesAmount <= accountShares, "BALANCE_EXCEEDED"); + + uint256 preRebaseTokenAmount = getPooledEthByShares(_sharesAmount); + + newTotalShares = _totalShares - _sharesAmount; + _totalShares = newTotalShares; + + shares[_account] = accountShares - _sharesAmount; + + uint256 postRebaseTokenAmount = getPooledEthByShares(_sharesAmount); + + emit SharesBurnt(_account, preRebaseTokenAmount, postRebaseTokenAmount, _sharesAmount); + } + + function _emitTransferEvents(address _from, address _to, uint256 _tokenAmount, uint256 _sharesAmount) internal { + emit Transfer(_from, _to, _tokenAmount); + emit TransferShares(_from, _to, _sharesAmount); + } + + function _emitTransferAfterMintingShares(address _to, uint256 _sharesAmount) internal { + _emitTransferEvents(address(0), _to, getPooledEthByShares(_sharesAmount), _sharesAmount); + } + + function _mintInitialShares(uint256 _sharesAmount) internal { + _mintShares(INITIAL_TOKEN_HOLDER, _sharesAmount); + _emitTransferAfterMintingShares(INITIAL_TOKEN_HOLDER, _sharesAmount); + } +} diff --git a/scripts/lido-mocks/StETHMock.sol b/scripts/lido-mocks/StETHMock.sol new file mode 100644 index 00000000..fca91ae9 --- /dev/null +++ b/scripts/lido-mocks/StETHMock.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {PercentD16, HUNDRED_PERCENT_D16} from "contracts/types/PercentD16.sol"; +import {StETHBase} from "./StETHBase.sol"; + +contract StETHMock is StETHBase { + constructor() { + _totalPooledEther = 100 wei; + _mintInitialShares(100 wei); + } + + function name() external pure override returns (string memory) { + return "StETHMock"; + } + + function symbol() external pure override returns (string memory) { + return "MStETH"; + } + + function rebaseTotalPooledEther(PercentD16 rebaseFactor) public { + _totalPooledEther = rebaseFactor.toUint256() * _totalPooledEther / HUNDRED_PERCENT_D16; + } + + function setTotalPooledEther(uint256 ethAmount) public { + _totalPooledEther = ethAmount; + } + + function mint(address account, uint256 ethAmount) external { + uint256 sharesAmount = getSharesByPooledEth(ethAmount); + + _mintShares(account, sharesAmount); + _totalPooledEther += ethAmount; + + _emitTransferAfterMintingShares(account, sharesAmount); + } + + function burn(address account, uint256 ethAmount) external { + uint256 sharesToBurn = this.getSharesByPooledEth(ethAmount); + _burnShares(account, sharesToBurn); + _totalPooledEther -= ethAmount; + _emitTransferEvents(account, address(0), ethAmount, sharesToBurn); + } +} diff --git a/scripts/lido-mocks/UnsafeWithdrawalQueueMock.sol b/scripts/lido-mocks/UnsafeWithdrawalQueueMock.sol new file mode 100644 index 00000000..7fe7a7c4 --- /dev/null +++ b/scripts/lido-mocks/UnsafeWithdrawalQueueMock.sol @@ -0,0 +1,485 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {IStETH} from "contracts/interfaces/IStETH.sol"; +import {StETHMock} from "./StETHMock.sol"; + +contract UnsafeWithdrawalQueueMock is IWithdrawalQueue, IERC721Metadata { + using EnumerableSet for EnumerableSet.UintSet; + + struct WithdrawalRequest { + /// @notice sum of the all ST_ETH submitted for withdrawals including this request + uint128 cumulativeStETH; + /// @notice sum of the all shares locked for withdrawal including this request + uint128 cumulativeShares; + /// @notice address that can claim or transfer the request + address owner; + /// @notice block.timestamp when the request was created + uint40 timestamp; + /// @notice flag if the request was claimed + bool claimed; + /// @notice timestamp of last oracle report for this request + uint40 reportTimestamp; + } + + struct Checkpoint { + uint256 fromRequestId; + uint256 maxShareRate; + } + + /// + + error ApprovalToOwner(); + error ApproveToCaller(); + error ArraysLengthMismatch(uint256 _firstArrayLength, uint256 _secondArrayLength); + error InvalidHint(uint256 _hint); + error InvalidRequestId(uint256 _requestId); + error InvalidRequestIdRange(uint256 startId, uint256 endId); + error NotOwner(address _sender, address _owner); + error NotOwnerOrApproved(address sender); + error NotOwnerOrApprovedForAll(address sender); + error RequestAlreadyClaimed(uint256 _requestId); + error RequestIdsNotSorted(); + error RequestNotFoundOrNotFinalized(uint256 _requestId); + error TransferToThemselves(); + error TransferToZeroAddress(); + error PausedExpected(); + error ResumedExpected(); + + /// + + uint256 public constant MIN_STETH_WITHDRAWAL_AMOUNT = 100; + uint256 public constant MAX_STETH_WITHDRAWAL_AMOUNT = 1000 * 1e18; + uint256 public constant PAUSE_INFINITELY = type(uint256).max; + uint256 internal constant NOT_FOUND = 0; + uint256 internal constant E27_PRECISION_BASE = 1e27; + + /// + + // solhint-disable-next-line var-name-mixedcase + IStETH public ST_ETH; + address payable private _refundAddress; + uint256 private _lastRequestId = 0; + uint256 private _lastFinalizedRequestId = 0; + uint256 private _lastCheckpointIndex = 0; + uint256 private _lockedEtherAmount = 0; + uint256 private _resumeSinceTimestamp = 0; + mapping(uint256 => WithdrawalRequest) private _queue; + mapping(address => EnumerableSet.UintSet) private _requestsByOwner; + mapping(uint256 => Checkpoint) private _checkpoints; + mapping(uint256 => address) private _tokenApprovals; + mapping(address => mapping(address => bool)) private _operatorApprovals; + + /// + + constructor(address stEth, address payable refundAddress) { + ST_ETH = IStETH(stEth); + _refundAddress = refundAddress; + + _queue[0] = WithdrawalRequest(0, 0, address(0), uint40(block.timestamp), true, 0); + _checkpoints[_lastCheckpointIndex] = Checkpoint(0, 0); + } + + function name() external pure override returns (string memory) { + return "WithdrawalQueueMock"; + } + + function symbol() external pure override returns (string memory) { + return "MockUnstETH"; + } + + function tokenURI(uint256 /* _requestId */ ) public view virtual override returns (string memory) { + return ""; + } + + function balanceOf(address _owner) external view override returns (uint256) { + return _requestsByOwner[_owner].length(); + } + + function ownerOf(uint256 _requestId) public view override returns (address) { + if (_requestId == 0 || _requestId > _lastRequestId) revert InvalidRequestId(_requestId); + + WithdrawalRequest storage request = _queue[_requestId]; + if (request.claimed) revert RequestAlreadyClaimed(_requestId); + + return request.owner; + } + + function getLastRequestId() public view returns (uint256) { + return _lastRequestId; + } + + function getLastFinalizedRequestId() public view returns (uint256) { + return _lastFinalizedRequestId; + } + + function getLastCheckpointIndex() public view returns (uint256) { + return _lastCheckpointIndex; + } + + function requestWithdrawals( + uint256[] calldata _amounts, + address _owner + ) public returns (uint256[] memory requestIds) { + _checkResumed(); + if (_owner == address(0)) _owner = msg.sender; + requestIds = new uint256[](_amounts.length); + for (uint256 i = 0; i < _amounts.length; ++i) { + requestIds[i] = _requestWithdrawal(_amounts[i], _owner); + } + } + + function findCheckpointHints( + uint256[] calldata _requestIds, + uint256 _firstIndex, + uint256 _lastIndex + ) external view returns (uint256[] memory hintIds) { + hintIds = new uint256[](_requestIds.length); + uint256 prevRequestId = 0; + for (uint256 i = 0; i < _requestIds.length; ++i) { + if (_requestIds[i] < prevRequestId) revert RequestIdsNotSorted(); + hintIds[i] = _findCheckpointHint(_requestIds[i], _firstIndex, _lastIndex); + _firstIndex = hintIds[i]; + prevRequestId = _requestIds[i]; + } + } + + function getWithdrawalStatus(uint256[] calldata _requestIds) + external + view + returns (WithdrawalRequestStatus[] memory statuses) + { + statuses = new WithdrawalRequestStatus[](_requestIds.length); + for (uint256 i = 0; i < _requestIds.length; ++i) { + statuses[i] = _getStatus(_requestIds[i]); + } + } + + function getClaimableEther( + uint256[] calldata _requestIds, + uint256[] calldata _hints + ) external view returns (uint256[] memory claimableEthValues) { + claimableEthValues = new uint256[](_requestIds.length); + for (uint256 i = 0; i < _requestIds.length; ++i) { + claimableEthValues[i] = _getClaimableEther(_requestIds[i], _hints[i]); + } + } + + function claimWithdrawals(uint256[] calldata _requestIds, uint256[] calldata _hints) external { + if (_requestIds.length != _hints.length) { + revert ArraysLengthMismatch(_requestIds.length, _hints.length); + } + + for (uint256 i = 0; i < _requestIds.length; ++i) { + _claim(_requestIds[i], _hints[i], payable(msg.sender)); + } + } + + function safeTransferFrom(address _from, address _to, uint256 _requestId) external pure override { + safeTransferFrom(_from, _to, _requestId, ""); + } + + function safeTransferFrom( + address, /* _from */ + address, /* _to */ + uint256, /* _requestId */ + bytes memory /* _data */ + ) public pure override { + // solhint-disable-next-line custom-errors + revert("Not implemented"); + } + + function transferFrom( + address _from, + address _to, + uint256 _requestId + ) external override(IWithdrawalQueue, IERC721) { + _transfer(_from, _to, _requestId); + } + + function approve(address _to, uint256 _requestId) external override { + address _owner = ownerOf(_requestId); + if (_to == _owner) revert ApprovalToOwner(); + if (msg.sender != _owner && !isApprovedForAll(_owner, msg.sender)) revert NotOwnerOrApprovedForAll(msg.sender); + + _tokenApprovals[_requestId] = _to; + } + + function setApprovalForAll(address _operator, bool _approved) external override { + if (msg.sender == _operator) revert ApproveToCaller(); + _operatorApprovals[msg.sender][_operator] = _approved; + } + + function getApproved(uint256 _requestId) external view override returns (address) { + if (!_existsAndNotClaimed(_requestId)) revert InvalidRequestId(_requestId); + + return _tokenApprovals[_requestId]; + } + + function isApprovedForAll(address _owner, address _operator) public view override returns (bool) { + return _operatorApprovals[_owner][_operator]; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC721).interfaceId || interfaceId == type(IERC721Metadata).interfaceId + // 0x49064906 is magic number ERC4906 interfaceId as defined in the standard https://eips.ethereum.org/EIPS/eip-4906 + || interfaceId == bytes4(0x49064906); + } + + function finalize(uint256 _lastRequestIdToBeFinalized, uint256 _maxShareRate) external payable { + _checkResumed(); + _finalize(_lastRequestIdToBeFinalized, msg.value, _maxShareRate); + } + + function refundEth(uint256 amountOfETH) public { + Address.sendValue(_refundAddress, amountOfETH); + } + + function markClaimed(uint256[] calldata _requestIds) external { + for (uint256 i = 0; i < _requestIds.length; ++i) { + _markClaimed(_requestIds[i]); + } + } + + function resume() external { + _resumeSinceTimestamp = block.timestamp; + } + + function pauseFor(uint256 _duration) external { + uint256 resumeSince; + if (_duration == PAUSE_INFINITELY) { + resumeSince = PAUSE_INFINITELY; + } else { + resumeSince = block.timestamp + _duration; + } + _resumeSinceTimestamp = resumeSince; + } + + function isPaused() external view returns (bool) { + return _isPaused(); + } + + function getResumeSinceTimestamp() external view returns (uint256) { + return _resumeSinceTimestamp; + } + + /////////////////////////////// + + function _requestWithdrawal(uint256 _amountOfStETH, address _owner) internal returns (uint256 requestId) { + ST_ETH.transferFrom(msg.sender, address(this), _amountOfStETH); + + uint256 amountOfShares = ST_ETH.getSharesByPooledEth(_amountOfStETH); + + requestId = _enqueue(uint128(_amountOfStETH), uint128(amountOfShares), _owner); + } + + function _enqueue( + uint128 _amountOfStETH, + uint128 _amountOfShares, + address _owner + ) internal returns (uint256 requestId) { + WithdrawalRequest memory lastRequest = _queue[_lastRequestId]; + + uint128 cumulativeShares = lastRequest.cumulativeShares + _amountOfShares; + uint128 cumulativeStETH = lastRequest.cumulativeStETH + _amountOfStETH; + + requestId = _lastRequestId + 1; + + _lastRequestId = requestId; + + WithdrawalRequest memory newRequest = WithdrawalRequest( + cumulativeStETH, cumulativeShares, _owner, uint40(block.timestamp), false, uint40(block.timestamp) + ); + _queue[requestId] = newRequest; + assert(_requestsByOwner[_owner].add(requestId)); + } + + function _findCheckpointHint(uint256 _requestId, uint256 _start, uint256 _end) internal view returns (uint256) { + if (_requestId == 0 || _requestId > _lastRequestId) revert InvalidRequestId(_requestId); + + if (_start == 0 || _end > _lastCheckpointIndex) revert InvalidRequestIdRange(_start, _end); + + if (_lastCheckpointIndex == 0 || _requestId > _lastFinalizedRequestId || _start > _end) return NOT_FOUND; + + if (_requestId >= _checkpoints[_end].fromRequestId) { + if (_end == _lastCheckpointIndex) return _end; + if (_requestId < _checkpoints[_end + 1].fromRequestId) return _end; + + return NOT_FOUND; + } + + if (_requestId < _checkpoints[_start].fromRequestId) { + return NOT_FOUND; + } + + uint256 min = _start; + uint256 max = _end - 1; + + while (max > min) { + uint256 mid = (max + min + 1) / 2; + if (_checkpoints[mid].fromRequestId <= _requestId) { + min = mid; + } else { + max = mid - 1; + } + } + return min; + } + + function _getStatus(uint256 _requestId) internal view returns (WithdrawalRequestStatus memory status) { + if (_requestId == 0 || _requestId > _lastRequestId) revert InvalidRequestId(_requestId); + + WithdrawalRequest memory request = _queue[_requestId]; + WithdrawalRequest memory previousRequest = _queue[_requestId - 1]; + + status = WithdrawalRequestStatus( + request.cumulativeStETH - previousRequest.cumulativeStETH, + request.cumulativeShares - previousRequest.cumulativeShares, + request.owner, + request.timestamp, + _requestId <= _lastFinalizedRequestId, + request.claimed + ); + } + + function _getClaimableEther(uint256 _requestId, uint256 _hint) internal view returns (uint256) { + if (_requestId == 0 || _requestId > _lastRequestId) revert InvalidRequestId(_requestId); + + if (_requestId > _lastFinalizedRequestId) return 0; + + WithdrawalRequest storage request = _queue[_requestId]; + if (request.claimed) return 0; + + return _calculateClaimableEther(request, _requestId, _hint); + } + + function _calculateClaimableEther( + WithdrawalRequest storage _request, + uint256 _requestId, + uint256 _hint + ) internal view returns (uint256 claimableEther) { + if (_hint == 0) revert InvalidHint(_hint); + + if (_hint > _lastCheckpointIndex) revert InvalidHint(_hint); + + Checkpoint memory checkpoint = _checkpoints[_hint]; + + if (_requestId < checkpoint.fromRequestId) revert InvalidHint(_hint); + if (_hint < _lastCheckpointIndex) { + Checkpoint memory nextCheckpoint = _checkpoints[_hint + 1]; + if (nextCheckpoint.fromRequestId <= _requestId) revert InvalidHint(_hint); + } + + WithdrawalRequest memory prevRequest = _queue[_requestId - 1]; + (uint256 batchShareRate, uint256 eth, uint256 shares) = _calcBatch(prevRequest, _request); + + if (batchShareRate > checkpoint.maxShareRate) { + eth = shares * checkpoint.maxShareRate / E27_PRECISION_BASE; + } + + return eth; + } + + function _calcBatch( + WithdrawalRequest memory _preStartRequest, + WithdrawalRequest memory _endRequest + ) internal pure returns (uint256 shareRate, uint256 _stETH, uint256 shares) { + _stETH = _endRequest.cumulativeStETH - _preStartRequest.cumulativeStETH; + shares = _endRequest.cumulativeShares - _preStartRequest.cumulativeShares; + + shareRate = _stETH * E27_PRECISION_BASE / shares; + } + + function _claim(uint256 _requestId, uint256 _hint, address payable _recipient) internal { + if (_requestId == 0) revert InvalidRequestId(_requestId); + if (_requestId > _lastFinalizedRequestId) revert RequestNotFoundOrNotFinalized(_requestId); + + WithdrawalRequest storage request = _queue[_requestId]; + + if (request.claimed) revert RequestAlreadyClaimed(_requestId); + + if (request.owner != msg.sender) revert NotOwner(msg.sender, request.owner); + + request.claimed = true; + assert(_requestsByOwner[request.owner].remove(_requestId)); + + uint256 ethWithDiscount = _calculateClaimableEther(request, _requestId, _hint); + _lockedEtherAmount -= ethWithDiscount; + Address.sendValue(_recipient, ethWithDiscount); + } + + function _transfer(address _from, address _to, uint256 _requestId) internal { + if (_to == address(0)) revert TransferToZeroAddress(); + if (_to == _from) revert TransferToThemselves(); + if (_requestId == 0 || _requestId > _lastRequestId) revert InvalidRequestId(_requestId); + + WithdrawalRequest storage request = _queue[_requestId]; + if (request.claimed) revert RequestAlreadyClaimed(_requestId); + + if (_from != request.owner) revert NotOwner(_from, request.owner); + + address msgSender = msg.sender; + if (!(_from == msgSender || isApprovedForAll(_from, msgSender) || _tokenApprovals[_requestId] == msgSender)) { + revert NotOwnerOrApproved(msgSender); + } + + delete _tokenApprovals[_requestId]; + request.owner = _to; + + assert(_requestsByOwner[_from].remove(_requestId)); + assert(_requestsByOwner[_to].add(_requestId)); + } + + function _existsAndNotClaimed(uint256 _requestId) internal view returns (bool) { + return _requestId > 0 && _requestId <= _lastRequestId && !_queue[_requestId].claimed; + } + + function _finalize(uint256 _lastRequestIdToBeFinalized, uint256 _amountOfETH, uint256 _maxShareRate) internal { + if (_lastRequestIdToBeFinalized > _lastRequestId) revert InvalidRequestId(_lastRequestIdToBeFinalized); + if (_lastRequestIdToBeFinalized <= _lastFinalizedRequestId) { + revert InvalidRequestId(_lastRequestIdToBeFinalized); + } + + WithdrawalRequest memory lastFinalizedRequest = _queue[_lastFinalizedRequestId]; + WithdrawalRequest memory requestToFinalize = _queue[_lastRequestIdToBeFinalized]; + + uint128 stETHToFinalize = requestToFinalize.cumulativeStETH - lastFinalizedRequest.cumulativeStETH; + + uint256 firstRequestIdToFinalize = _lastFinalizedRequestId + 1; + + _checkpoints[_lastCheckpointIndex + 1] = Checkpoint(firstRequestIdToFinalize, _maxShareRate); + _lastCheckpointIndex = _lastCheckpointIndex + 1; + + _lockedEtherAmount = _lockedEtherAmount + _amountOfETH; + _lastFinalizedRequestId = _lastRequestIdToBeFinalized; + + StETHMock(address(ST_ETH)).burn(address(this), stETHToFinalize); + } + + function _markClaimed(uint256 _requestId) internal { + if (_requestId == 0) revert InvalidRequestId(_requestId); + if (_requestId > _lastFinalizedRequestId) revert RequestNotFoundOrNotFinalized(_requestId); + + WithdrawalRequest storage request = _queue[_requestId]; + + if (request.claimed) revert RequestAlreadyClaimed(_requestId); + + request.claimed = true; + assert(_requestsByOwner[request.owner].remove(_requestId)); + } + + function _isPaused() internal view returns (bool) { + return block.timestamp < _resumeSinceTimestamp; + } + + function _checkResumed() internal view { + if (_isPaused()) { + revert ResumedExpected(); + } + } +} diff --git a/scripts/lido-mocks/WstETHMock.sol b/scripts/lido-mocks/WstETHMock.sol new file mode 100644 index 00000000..5bdb20d4 --- /dev/null +++ b/scripts/lido-mocks/WstETHMock.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IStETH} from "contracts/interfaces/IStETH.sol"; +import {IWstETH} from "contracts/interfaces/IWstETH.sol"; + +/* solhint-disable custom-errors, reason-string */ +contract WstETHMock is ERC20, IWstETH { + IStETH public stETH; + + constructor(IStETH _stETH) ERC20("WstETHMock", "MWst") { + stETH = _stETH; + } + + function wrap(uint256 stETHAmount) external returns (uint256) { + require(stETHAmount > 0, "wstETH: can't wrap zero stETH"); + uint256 wstETHAmount = stETH.getSharesByPooledEth(stETHAmount); + _mint(msg.sender, wstETHAmount); + stETH.transferFrom(msg.sender, address(this), stETHAmount); + return wstETHAmount; + } + + function unwrap(uint256 wstETHAmount) external returns (uint256) { + require(wstETHAmount > 0, "wstETH: zero amount unwrap not allowed"); + uint256 stETHAmount = stETH.getPooledEthByShares(wstETHAmount); + _burn(msg.sender, wstETHAmount); + stETH.transfer(msg.sender, stETHAmount); + return stETHAmount; + } + + function getStETHByWstETH(uint256 wstethAmount) external view returns (uint256) { + return stETH.getPooledEthByShares(wstethAmount); + } +} diff --git a/test/mocks/EscrowMock.sol b/test/mocks/EscrowMock.sol deleted file mode 100644 index fe3e0798..00000000 --- a/test/mocks/EscrowMock.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Duration} from "contracts/types/Duration.sol"; -import {PercentD16} from "contracts/types/PercentD16.sol"; - -import {IEscrow} from "contracts/interfaces/IEscrow.sol"; - -contract EscrowMock is IEscrow { - event __RageQuitStarted(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock); - - Duration public __minAssetsLockDuration; - PercentD16 public __rageQuitSupport; - bool public __isRageQuitFinalized; - - function __setRageQuitSupport(PercentD16 newRageQuitSupport) external { - __rageQuitSupport = newRageQuitSupport; - } - - function __setIsRageQuitFinalized(bool newIsRageQuitFinalized) external { - __isRageQuitFinalized = newIsRageQuitFinalized; - } - - function initialize(Duration minAssetsLockDuration) external { - __minAssetsLockDuration = minAssetsLockDuration; - } - - function startRageQuit(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock) external { - emit __RageQuitStarted(rageQuitExtraTimelock, rageQuitWithdrawalsTimelock); - } - - function isRageQuitFinalized() external view returns (bool) { - return __isRageQuitFinalized; - } - - function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport) { - return __rageQuitSupport; - } - - function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { - __minAssetsLockDuration = newMinAssetsLockDuration; - } -} diff --git a/test/mocks/TimelockMock.sol b/test/mocks/TimelockMock.sol index 32dd2c30..a818ab26 100644 --- a/test/mocks/TimelockMock.sol +++ b/test/mocks/TimelockMock.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {Timestamp} from "contracts/types/Timestamp.sol"; -import {ITimelock, ProposalStatus} from "contracts/interfaces/ITimelock.sol"; +import {Duration} from "contracts/types/Duration.sol"; +import {ITimelock} from "contracts/interfaces/ITimelock.sol"; import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; +/* solhint-disable custom-errors */ contract TimelockMock is ITimelock { uint8 public constant OFFSET = 1; @@ -15,6 +16,7 @@ contract TimelockMock is ITimelock { } mapping(uint256 => bool) public canScheduleProposal; + mapping(uint256 => bool) public canExecuteProposal; uint256[] public submittedProposals; uint256[] public scheduledProposals; @@ -24,12 +26,7 @@ contract TimelockMock is ITimelock { address internal governance; - function submit( - address, - address, - ExternalCall[] calldata, - string calldata - ) external returns (uint256 newProposalId) { + function submit(address, ExternalCall[] calldata) external returns (uint256 newProposalId) { newProposalId = submittedProposals.length + OFFSET; submittedProposals.push(newProposalId); canScheduleProposal[newProposalId] = false; @@ -38,18 +35,22 @@ contract TimelockMock is ITimelock { function schedule(uint256 proposalId) external { if (canScheduleProposal[proposalId] == false) { - revert(); + revert("Can't schedule"); } scheduledProposals.push(proposalId); } function execute(uint256 proposalId) external { + if (canExecuteProposal[proposalId] == false) { + revert("Can't execute"); + } + executedProposals.push(proposalId); } function canExecute(uint256 proposalId) external view returns (bool) { - revert("Not Implemented"); + return canExecuteProposal[proposalId]; } function canSchedule(uint256 proposalId) external view returns (bool) { @@ -64,6 +65,10 @@ contract TimelockMock is ITimelock { canScheduleProposal[proposalId] = true; } + function setExecutable(uint256 proposalId) external { + canExecuteProposal[proposalId] = true; + } + function getSubmittedProposals() external view returns (uint256[] memory) { return submittedProposals; } @@ -80,7 +85,11 @@ contract TimelockMock is ITimelock { return lastCancelledProposalId; } - function getProposal(uint256 proposalId) external view returns (ProposalDetails memory, ExternalCall[] memory) { + function getProposal(uint256 /* proposalId */ ) + external + view + returns (ProposalDetails memory, ExternalCall[] memory) + { revert("Not Implemented"); } @@ -96,7 +105,7 @@ contract TimelockMock is ITimelock { revert("Not Implemented"); } - function emergencyExecute(uint256 proposalId) external { + function emergencyExecute(uint256 /* proposalId */ ) external { revert("Not Implemented"); } @@ -104,7 +113,11 @@ contract TimelockMock is ITimelock { revert("Not Implemented"); } - function getProposalDetails(uint256 proposalId) external view returns (ProposalDetails memory) { + function getProposalDetails(uint256 /* proposalId */ ) external view returns (ProposalDetails memory) { + revert("Not Implemented"); + } + + function getProposalCalls(uint256 /* proposalId */ ) external view returns (ExternalCall[] memory calls) { revert("Not Implemented"); } @@ -115,4 +128,28 @@ contract TimelockMock is ITimelock { function getAdminExecutor() external view returns (address) { return _ADMIN_EXECUTOR; } + + function setAdminExecutor(address /* newAdminExecutor */ ) external { + revert("Not Implemented"); + } + + function getAfterSubmitDelay() external view returns (Duration) { + revert("Not Implemented"); + } + + function getAfterScheduleDelay() external view returns (Duration) { + revert("Not Implemented"); + } + + function setAfterSubmitDelay(Duration /* newAfterSubmitDelay */ ) external { + revert("Not Implemented"); + } + + function setAfterScheduleDelay(Duration /* newAfterScheduleDelay */ ) external { + revert("Not Implemented"); + } + + function transferExecutorOwnership(address, /* executor */ address /* owner */ ) external { + revert("Not Implemented"); + } } diff --git a/test/mocks/WithdrawalQueueMock.sol b/test/mocks/WithdrawalQueueMock.sol index 57290346..01d4053e 100644 --- a/test/mocks/WithdrawalQueueMock.sol +++ b/test/mocks/WithdrawalQueueMock.sol @@ -1,18 +1,33 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -// import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; /*, ERC721("test", "test")*/ -import {IWithdrawalQueue, WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {ETHValues, sendTo} from "contracts/types/ETHValue.sol"; /* solhint-disable no-unused-vars,custom-errors */ contract WithdrawalQueueMock is IWithdrawalQueue { + error InvalidRequestId(); + error ArraysLengthMismatch(); + + uint256 public immutable REVERT_ON_ID = type(uint256).max; + uint256 private _lastRequestId; uint256 private _lastFinalizedRequestId; uint256 private _minStETHWithdrawalAmount; uint256 private _maxStETHWithdrawalAmount; + uint256 private _claimableAmount; + uint256 private _requestWithdrawalsTransferAmount; + uint256 private _lastCheckpointIndex; + uint256[] private _getClaimableEtherResult; uint256[] private _requestWithdrawalsResult; + IERC20 private _stETH; + uint256[] private _checkpointHints; + WithdrawalRequestStatus[] private _withdrawalRequestsStatuses; - constructor() {} + constructor(IERC20 stETH) { + _stETH = stETH; + } function MIN_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256) { return _minStETHWithdrawalAmount; @@ -23,7 +38,17 @@ contract WithdrawalQueueMock is IWithdrawalQueue { } function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external { - revert("Not Implemented"); + if (requestIds.length != hints.length) { + revert ArraysLengthMismatch(); + } + + if (_claimableAmount == 0) { + return; + } + + ETHValues.from(_claimableAmount).sendTo(payable(msg.sender)); + + setClaimableAmount(0); } function getLastRequestId() external view returns (uint256) { @@ -34,82 +59,104 @@ contract WithdrawalQueueMock is IWithdrawalQueue { return _lastFinalizedRequestId; } - function getWithdrawalStatus(uint256[] calldata _requestIds) + function getWithdrawalStatus(uint256[] calldata /* _requestIds */ ) external view - returns (WithdrawalRequestStatus[] memory statuses) + returns (WithdrawalRequestStatus[] memory) { - revert("Not Implemented"); + return _withdrawalRequestsStatuses; } - /// @notice Returns amount of ether available for claim for each provided request id - /// @param _requestIds array of request ids - /// @param _hints checkpoint hints. can be found with `findCheckpointHints(_requestIds, 1, getLastCheckpointIndex())` - /// @return claimableEthValues amount of claimable ether for each request, amount is equal to 0 if request - /// is not finalized or already claimed function getClaimableEther( uint256[] calldata _requestIds, - uint256[] calldata _hints - ) external view returns (uint256[] memory claimableEthValues) { - revert("Not Implemented"); + uint256[] calldata /* _hints */ + ) external view returns (uint256[] memory) { + if (_requestIds.length > 0 && REVERT_ON_ID == _requestIds[0]) { + revert InvalidRequestId(); + } + + return _getClaimableEtherResult; } function findCheckpointHints( - uint256[] calldata _requestIds, - uint256 _firstIndex, - uint256 _lastIndex - ) external view returns (uint256[] memory hintIds) { - revert("Not Implemented"); + uint256[] calldata, /* _requestIds */ + uint256, /* _firstIndex */ + uint256 /* _lastIndex */ + ) external view returns (uint256[] memory) { + return _checkpointHints; } function getLastCheckpointIndex() external view returns (uint256) { - revert("Not Implemented"); + return _lastCheckpointIndex; } function requestWithdrawals( uint256[] calldata _amounts, address _owner ) external returns (uint256[] memory requestIds) { + uint256 totalAmount = 0; + for (uint256 i = 0; i < _amounts.length; i++) { + if (_amounts[i] < _minStETHWithdrawalAmount) { + revert("Amount is less than MIN_STETH_WITHDRAWAL_AMOUNT"); + } + if (_amounts[i] > _maxStETHWithdrawalAmount) { + revert("Amount is more than MAX_STETH_WITHDRAWAL_AMOUNT"); + } + totalAmount += _amounts[i]; + } + + if (_requestWithdrawalsTransferAmount > 0) { + _stETH.transferFrom(_owner, address(this), _requestWithdrawalsTransferAmount); + setRequestWithdrawalsTransferAmount(0); + } else { + if (totalAmount > 0) { + _stETH.transferFrom(_owner, address(this), totalAmount); + } + } + return _requestWithdrawalsResult; } - function balanceOf(address owner) external view returns (uint256 balance) { + function balanceOf(address /* owner */ ) external pure returns (uint256 /* balance */ ) { revert("Not Implemented"); } - function ownerOf(uint256 tokenId) external view returns (address owner) { + function ownerOf(uint256 /* tokenId */ ) external pure returns (address /* owner */ ) { revert("Not Implemented"); } - function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external { + function safeTransferFrom( + address, /* from */ + address, /* to */ + uint256, /* tokenId */ + bytes calldata /* data */ + ) external pure { revert("Not Implemented"); } - function safeTransferFrom(address from, address to, uint256 tokenId) external { + function safeTransferFrom(address, /* from */ address, /* to */ uint256 /* tokenId */ ) external pure { revert("Not Implemented"); } - function transferFrom(address from, address to, uint256 tokenId) external { - revert("Not Implemented"); - } + function transferFrom(address, /* from */ address, /* to */ uint256 /* tokenId */ ) external pure {} - function approve(address to, uint256 tokenId) external { + function approve(address, /* to */ uint256 /* tokenId */ ) external pure { revert("Not Implemented"); } - function setApprovalForAll(address operator, bool approved) external { + function setApprovalForAll(address, /* operator */ bool /* approved */ ) external pure { revert("Not Implemented"); } - function getApproved(uint256 tokenId) external view returns (address operator) { + function getApproved(uint256 /* tokenId */ ) external pure returns (address /* operator */ ) { revert("Not Implemented"); } - function isApprovedForAll(address owner, address operator) external view returns (bool) { + function isApprovedForAll(address, /* owner */ address /* operator */ ) external pure returns (bool) { revert("Not Implemented"); } - function supportsInterface(bytes4 interfaceId) external view returns (bool) { + function supportsInterface(bytes4 /* interfaceId */ ) external pure returns (bool) { revert("Not Implemented"); } @@ -132,4 +179,32 @@ contract WithdrawalQueueMock is IWithdrawalQueue { function setRequestWithdrawalsResult(uint256[] memory requestIds) public { _requestWithdrawalsResult = requestIds; } + + function setClaimableAmount(uint256 claimableAmount) public { + _claimableAmount = claimableAmount; + } + + function setRequestWithdrawalsTransferAmount(uint256 requestWithdrawalsTransferAmount) public { + _requestWithdrawalsTransferAmount = requestWithdrawalsTransferAmount; + } + + function setWithdrawalRequestsStatuses(WithdrawalRequestStatus[] memory statuses) public { + delete _withdrawalRequestsStatuses; + + for (uint256 i = 0; i < statuses.length; ++i) { + _withdrawalRequestsStatuses.push(statuses[i]); + } + } + + function setClaimableEtherResult(uint256[] memory claimableEther) public { + _getClaimableEtherResult = claimableEther; + } + + function setLastCheckpointIndex(uint256 index) public { + _lastCheckpointIndex = index; + } + + function setCheckpointHints(uint256[] memory hints) public { + _checkpointHints = hints; + } } diff --git a/test/mocks/WstETHMock.sol b/test/mocks/WstETHMock.sol new file mode 100644 index 00000000..569dca54 --- /dev/null +++ b/test/mocks/WstETHMock.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; +import {IStETH} from "contracts/interfaces/IStETH.sol"; +import {IWstETH} from "contracts/interfaces/IWstETH.sol"; + +/* solhint-disable no-unused-vars,custom-errors */ +contract WstETHMock is ERC20Mock, IWstETH { + IStETH public stETH; + + constructor(IStETH _stETH) { + stETH = _stETH; + } + + function wrap(uint256 stETHAmount) external returns (uint256) { + require(stETHAmount > 0, "wstETH: can't wrap zero stETH"); + uint256 wstETHAmount = stETH.getSharesByPooledEth(stETHAmount); + _mint(msg.sender, wstETHAmount); + stETH.transferFrom(msg.sender, address(this), stETHAmount); + return wstETHAmount; + } + + function unwrap(uint256 wstETHAmount) external returns (uint256) { + require(wstETHAmount > 0, "wstETH: zero amount unwrap not allowed"); + uint256 stETHAmount = stETH.getPooledEthByShares(wstETHAmount); + _burn(msg.sender, wstETHAmount); + stETH.transfer(msg.sender, stETHAmount); + return stETHAmount; + } + + function getStETHByWstETH(uint256 wstethAmount) external view returns (uint256) { + return stETH.getPooledEthByShares(wstethAmount); + } +} diff --git a/test/scenario/agent-timelock.t.sol b/test/scenario/agent-timelock.t.sol index 5cdebbee..e74756cb 100644 --- a/test/scenario/agent-timelock.t.sol +++ b/test/scenario/agent-timelock.t.sol @@ -1,13 +1,159 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {ExternalCall, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; +import {Proposers} from "contracts/libraries/Proposers.sol"; + +import {ExternalCall, ExternalCallHelpers, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; +import {LidoUtils} from "../utils/lido-utils.sol"; + +interface IRegularContract { + function regularMethod() external; +} contract AgentTimelockTest is ScenarioTestBlueprint { + using LidoUtils for LidoUtils.Context; + function setUp() external { _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); } + function testFork_AragonAgentAsExecutor_HappyPath() external { + _step("0. Grant EXECUTE_ROLE permission to the timelock on the Agent contract"); + { + _grantAgentExecutorRoleToTimelock(); + } + + address agentProposer = makeAddr("AGENT_PROPOSER"); + _step("1. Submit proposal to register Aragon as the executor"); + { + _addAragonAgentProposer(agentProposer); + } + + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); + + uint256 agentActionsProposalId; + _step("2. Submit proposal via the Agent proposer"); + { + vm.prank(agentProposer); + agentActionsProposalId = + _dualGovernance.submitProposal(regularStaffCalls, "Make regular staff using Agent as executor"); + + _assertSubmittedProposalData(agentActionsProposalId, address(_lido.agent), regularStaffCalls); + } + + _step("3. Execute the proposal"); + { + _assertProposalSubmitted(agentActionsProposalId); + _waitAfterSubmitDelayPassed(); + + _scheduleProposalViaDualGovernance(agentActionsProposalId); + _assertProposalScheduled(agentActionsProposalId); + + _waitAfterScheduleDelayPassed(); + _executeProposal(agentActionsProposalId); + _assertProposalExecuted(agentActionsProposalId); + + _assertTargetMockCalls(address(_lido.agent), regularStaffCalls); + } + } + + function testFork_AragonAgentAsExecutor_RevertOn_FailedCall() external { + _step("0. Grant EXECUTE_ROLE permission to the timelock on the Agent contract"); + { + _grantAgentExecutorRoleToTimelock(); + } + + address agentProposer = makeAddr("AGENT_PROPOSER"); + _step("1. Submit proposal to register Aragon as the executor"); + { + _addAragonAgentProposer(agentProposer); + } + + ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); + vm.mockCallRevert(regularStaffCalls[0].target, regularStaffCalls[0].payload, "INVALID TARGET"); + + uint256 agentActionsProposalId; + _step("2. Submit proposal which should revert via the Agent proposer"); + { + vm.prank(agentProposer); + agentActionsProposalId = + _dualGovernance.submitProposal(regularStaffCalls, "Make regular staff using Agent as executor"); + + _assertSubmittedProposalData(agentActionsProposalId, address(_lido.agent), regularStaffCalls); + } + + _step("3. The execution of the proposal fails"); + { + _assertProposalSubmitted(agentActionsProposalId); + _waitAfterSubmitDelayPassed(); + + _scheduleProposalViaDualGovernance(agentActionsProposalId); + _assertProposalScheduled(agentActionsProposalId); + + _waitAfterScheduleDelayPassed(); + + vm.expectRevert("INVALID TARGET"); + _executeProposal(agentActionsProposalId); + + _assertNoTargetMockCalls(); + } + } + + function testFork_AgentAsExecutor_SucceedOnEmptyAccountCalls() external { + _step("0. Grant EXECUTE_ROLE permission to the timelock on the Agent contract"); + { + _grantAgentExecutorRoleToTimelock(); + } + + address agentProposer = makeAddr("AGENT_PROPOSER"); + _step("1. Submit proposal to register Aragon as the executor"); + { + _addAragonAgentProposer(agentProposer); + } + + uint256 callValue = 1 ether; + address nonContractAccount = makeAddr("NOT_CONTRACT"); + ExternalCall[] memory callsToEmptyAccount = ExternalCallHelpers.create( + [ + ExternalCall({value: 0, target: nonContractAccount, payload: new bytes(0)}), + ExternalCall({ + value: 0, + target: nonContractAccount, + payload: abi.encodeCall(IRegularContract.regularMethod, ()) + }), + ExternalCall({value: uint96(callValue), target: nonContractAccount, payload: new bytes(0)}) + ] + ); + uint256 agentBalanceBefore = address(_lido.agent).balance; + vm.deal(address(_lido.agent), agentBalanceBefore + callValue); + + uint256 agentActionsProposalId; + _step("2. Submit proposal via the Agent proposer"); + { + vm.prank(agentProposer); + agentActionsProposalId = + _dualGovernance.submitProposal(callsToEmptyAccount, "Make different calls to EOA account"); + + _assertSubmittedProposalData(agentActionsProposalId, address(_lido.agent), callsToEmptyAccount); + } + + _step("3. Execute the proposal"); + { + _assertProposalSubmitted(agentActionsProposalId); + _waitAfterSubmitDelayPassed(); + + _scheduleProposalViaDualGovernance(agentActionsProposalId); + _assertProposalScheduled(agentActionsProposalId); + + _waitAfterScheduleDelayPassed(); + _executeProposal(agentActionsProposalId); + _assertProposalExecuted(agentActionsProposalId); + + assertEq(nonContractAccount.balance, 1 ether); + assertEq(address(_lido.agent).balance, agentBalanceBefore); + } + } + function testFork_AgentTimelockHappyPath() external { ExternalCall[] memory regularStaffCalls = _getMockTargetRegularStaffCalls(); @@ -110,4 +256,43 @@ contract AgentTimelockTest is ScenarioTestBlueprint { _assertProposalCancelled(proposalId); } } + + // --- + // Helper Methods + // --- + + function _grantAgentExecutorRoleToTimelock() internal { + _lido.grantPermission(address(_lido.agent), _lido.agent.EXECUTE_ROLE(), address(_timelock)); + assertTrue(_lido.acl.hasPermission(address(_timelock), address(_lido.agent), _lido.agent.EXECUTE_ROLE())); + } + + function _addAragonAgentProposer(address agentProposer) internal { + ExternalCall[] memory externalCalls = ExternalCallHelpers.create( + [ + ExternalCall({ + value: 0, + target: address(_dualGovernance), + payload: abi.encodeCall(_dualGovernance.registerProposer, (agentProposer, address(_lido.agent))) + }) + ] + ); + uint256 addAgentProposerProposalId = + _submitProposalViaDualGovernance("Add Aragon Agent as proposer to the Dual Governance", externalCalls); + + _assertProposalSubmitted(addAgentProposerProposalId); + _waitAfterSubmitDelayPassed(); + + _scheduleProposalViaDualGovernance(addAgentProposerProposalId); + _assertProposalScheduled(addAgentProposerProposalId); + + _waitAfterScheduleDelayPassed(); + _executeProposal(addAgentProposerProposalId); + _assertProposalExecuted(addAgentProposerProposalId); + + Proposers.Proposer[] memory proposers = _dualGovernance.getProposers(); + + assertEq(proposers.length, 2); + assertEq(proposers[1].account, agentProposer); + assertEq(proposers[1].executor, address(_lido.agent)); + } } diff --git a/test/scenario/dg-update-tokens-rotation.t.sol b/test/scenario/dg-update-tokens-rotation.t.sol new file mode 100644 index 00000000..7a59727b --- /dev/null +++ b/test/scenario/dg-update-tokens-rotation.t.sol @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Durations} from "contracts/types/Duration.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; + +import {TimelockState} from "contracts/libraries/TimelockState.sol"; +import {ExecutableProposals, Status as ProposalStatus} from "contracts/libraries/ExecutableProposals.sol"; + +import {Escrow} from "contracts/Escrow.sol"; +import {DualGovernance} from "contracts/DualGovernance.sol"; + +import {ScenarioTestBlueprint, ExternalCall, ExternalCallHelpers} from "../utils/scenario-test-blueprint.sol"; + +contract DualGovernanceUpdateTokensRotation is ScenarioTestBlueprint { + address internal immutable _VETOER = makeAddr("VETOER"); + + function setUp() external { + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); + _setupStETHBalance(_VETOER, PercentsD16.fromBasisPoints(30_00)); + } + + function testFork_DualGovernanceUpdate_OldEscrowInstanceAllowsUnlockTokens() external { + DualGovernance newDualGovernanceInstance; + _step("1. Deploy new Dual Governance implementation"); + { + newDualGovernanceInstance = _deployDualGovernance({ + timelock: _timelock, + resealManager: _resealManager, + configProvider: _dualGovernanceConfigProvider + }); + } + + uint256 updateDualGovernanceProposalId; + _step("2. Submit proposal to update the Dual Governance implementation"); + { + updateDualGovernanceProposalId = _submitProposalViaDualGovernance( + "Update the Dual Governance implementation", + _getActionsToUpdateDualGovernanceImplementation(address(newDualGovernanceInstance)) + ); + } + + _step("3. Users accumulate some stETH in the Signalling Escrow"); + { + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT() - PercentsD16.from(1)); + _assertVetoSignalingState(); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); + + _activateNextState(); + _assertVetoSignalingDeactivationState(); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + _activateNextState(); + _assertVetoCooldownState(); + } + + _step("4. When the VetoCooldown is entered proposal to update becomes executable"); + { + _scheduleProposalViaDualGovernance(updateDualGovernanceProposalId); + _assertProposalScheduled(updateDualGovernanceProposalId); + + _waitAfterScheduleDelayPassed(); + _executeProposal(updateDualGovernanceProposalId); + + assertEq(_timelock.getGovernance(), address(newDualGovernanceInstance)); + } + + _step("5. The old instance of the Dual Governance can't submit proposals anymore"); + { + // wait until the VetoCooldown ends in the old dual governance instance + _wait(_dualGovernanceConfigProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); + _activateNextState(); + _assertVetoSignalingState(); + + // old instance of the Dual Governance can't submit proposals anymore + vm.expectRevert( + abi.encodeWithSelector(TimelockState.CallerIsNotGovernance.selector, address(_dualGovernance)) + ); + vm.prank(address(_lido.voting)); + _dualGovernance.submitProposal(_getMockTargetRegularStaffCalls(), "empty metadata"); + } + + _step("6. Users can unlock stETH from the old Signalling Escrow"); + { + _unlockStETH(_VETOER); + } + + _step("7. Users can withdraw funds even if the Rage Quit is started in the old instance of the Dual Governance"); + { + // the Rage Quit started on the old DualGovernance instance + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); + _activateNextState(); + _assertRageQuitState(); + + // The Rage Quit may be finished in the previous DG instance so vetoers will not lose their funds by mistake + Escrow rageQuitEscrow = Escrow(payable(_dualGovernance.getRageQuitEscrow())); + + while (!rageQuitEscrow.isWithdrawalsBatchesClosed()) { + rageQuitEscrow.requestNextWithdrawalsBatch(96); + } + + _finalizeWithdrawalQueue(); + + while (rageQuitEscrow.getUnclaimedUnstETHIdsCount() > 0) { + rageQuitEscrow.claimNextWithdrawalsBatch(32); + } + + rageQuitEscrow.startRageQuitExtensionPeriod(); + + _wait(_dualGovernanceConfigProvider.RAGE_QUIT_EXTENSION_PERIOD_DURATION().plusSeconds(1)); + assertEq(rageQuitEscrow.isRageQuitFinalized(), true); + + // TODO: Add method Escrow.getRageQuitExtensionPeriodDuration() + _wait(_dualGovernanceConfigProvider.RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY()); + + uint256 vetoerETHBalanceBefore = _VETOER.balance; + + vm.prank(_VETOER); + rageQuitEscrow.withdrawETH(); + + assertTrue(_VETOER.balance > vetoerETHBalanceBefore); + } + } + + function testFork_DualGovernanceUpdate_LastMomentProposalAttack() external { + // DAO initiates the update of the Dual Governance + // Malicious actor locks funds in the Signalling Escrow to waste the full duration of VetoSignalling + // At the end of the VetoSignalling, malicious actor unlocks all funds from VetoSignalling and + // submits proposal to steal the control over governance + // + DualGovernance newDualGovernanceInstance; + _step("1. Deploy new Dual Governance implementation"); + { + newDualGovernanceInstance = _deployDualGovernance({ + timelock: _timelock, + resealManager: _resealManager, + configProvider: _dualGovernanceConfigProvider + }); + } + + uint256 updateDualGovernanceProposalId; + _step("2. DAO submits proposal to update the Dual Governance implementation"); + { + updateDualGovernanceProposalId = _submitProposalViaDualGovernance( + "Update the Dual Governance implementation", + _getActionsToUpdateDualGovernanceImplementation(address(newDualGovernanceInstance)) + ); + } + + _step("3. Malicious actor accumulate second seal in the Signalling Escrow"); + { + _lockStETH(_VETOER, _dualGovernanceConfigProvider.SECOND_SEAL_RAGE_QUIT_SUPPORT()); + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_MAX_DURATION().minusSeconds(_lido.voting.voteTime())); + _assertVetoSignalingState(); + } + + uint256 maliciousProposalId; + _step("4. Malicious actor unlock funds from Signalling Escrow"); + { + maliciousProposalId = _submitProposalViaDualGovernance( + "Steal control over timelock contract", + ExternalCallHelpers.create({ + target: address(_timelock), + payload: abi.encodeCall(_timelock.setGovernance, (_VETOER)) + }) + ); + _unlockStETH(_VETOER); + _assertVetoSignalingDeactivationState(); + } + + _step("5. Regular can't collect second seal in VETO_SIGNALLING_DEACTIVATION_MAX_DURATION"); + { + _wait(_dualGovernanceConfigProvider.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + _activateNextState(); + _assertVetoCooldownState(); + } + + _step("6. The Dual Governance implementation is updated on the new version"); + { + // Malicious proposal can't be executed directly on the old DualGovernance instance, as it was submitted + // during the VetoSignalling phase + vm.expectRevert( + abi.encodeWithSelector(DualGovernance.ProposalSchedulingBlocked.selector, maliciousProposalId) + ); + _scheduleProposalViaDualGovernance(maliciousProposalId); + + _scheduleProposalViaDualGovernance(updateDualGovernanceProposalId); + _assertProposalScheduled(updateDualGovernanceProposalId); + + _waitAfterScheduleDelayPassed(); + _executeProposal(updateDualGovernanceProposalId); + + assertEq(_timelock.getGovernance(), address(newDualGovernanceInstance)); + } + + _step("7. After the update malicious proposal is cancelled and can't be executed via new DualGovernance"); + { + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, maliciousProposalId, ProposalStatus.Cancelled + ) + ); + newDualGovernanceInstance.scheduleProposal(maliciousProposalId); + + assertEq(_timelock.getProposalDetails(maliciousProposalId).status, ProposalStatus.Cancelled); + } + } + + // --- + // Helper methods + // --- + function _getActionsToUpdateDualGovernanceImplementation(address newDualGovernanceInstance) + internal + returns (ExternalCall[] memory) + { + return ExternalCallHelpers.create( + [ + // register Aragon Voting as proposer + ExternalCall({ + value: 0, + target: address(newDualGovernanceInstance), + payload: abi.encodeCall( + DualGovernance.registerProposer, (address(_lido.voting), _timelock.getAdminExecutor()) + ) + }), + ExternalCall({ + value: 0, + target: address(_timelock), + payload: abi.encodeCall(_timelock.setGovernance, (address(newDualGovernanceInstance))) + }) + // NOTE: There should be additional calls with the proper setting up of the new DG implementation + ] + ); + } +} diff --git a/test/scenario/emergency-committee.t.sol b/test/scenario/emergency-committee.t.sol deleted file mode 100644 index 7a4ff016..00000000 --- a/test/scenario/emergency-committee.t.sol +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; -import {PercentsD16} from "contracts/types/PercentD16.sol"; - -import {IPotentiallyDangerousContract} from "../utils/interfaces/IPotentiallyDangerousContract.sol"; -import {ScenarioTestBlueprint, ExternalCall, ExternalCallHelpers} from "../utils/scenario-test-blueprint.sol"; -import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; - -contract EmergencyCommitteeTest is ScenarioTestBlueprint { - address internal immutable _VETOER = makeAddr("VETOER"); - uint256 public constant PAUSE_INFINITELY = type(uint256).max; - - function setUp() external { - _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); - _setupStETHBalance(_VETOER, PercentsD16.fromBasisPoints(10_00)); - _lockStETH(_VETOER, 1 ether); - } - - function test_emergency_committees_happy_path() external { - uint256 quorum; - uint256 support; - bool isExecuted; - - address[] memory members; - - ExternalCall[] memory proposalCalls = ExternalCallHelpers.create( - address(_targetMock), abi.encodeCall(IPotentiallyDangerousContract.doRegularStaff, (0)) - ); - uint256 proposalIdToExecute = _submitProposal(_dualGovernance, "Proposal for execution", proposalCalls); - - _wait(_timelock.getAfterSubmitDelay().plusSeconds(1)); - _assertCanSchedule(_dualGovernance, proposalIdToExecute, true); - _scheduleProposal(_dualGovernance, proposalIdToExecute); - - // Emergency Activation - members = _emergencyActivationCommittee.getMembers(); - for (uint256 i = 0; i < _emergencyActivationCommittee.getQuorum() - 1; i++) { - vm.prank(members[i]); - _emergencyActivationCommittee.approveActivateEmergencyMode(); - (support, quorum,, isExecuted) = _emergencyActivationCommittee.getActivateEmergencyModeState(); - assert(support < quorum); - assert(isExecuted == false); - } - - vm.prank(members[members.length - 1]); - _emergencyActivationCommittee.approveActivateEmergencyMode(); - (support, quorum,, isExecuted) = _emergencyActivationCommittee.getActivateEmergencyModeState(); - assert(support == quorum); - assert(isExecuted == false); - - _emergencyActivationCommittee.executeActivateEmergencyMode(); - (support, quorum,, isExecuted) = _emergencyActivationCommittee.getActivateEmergencyModeState(); - assert(isExecuted == true); - - // Emergency Execute - members = _emergencyExecutionCommittee.getMembers(); - for (uint256 i = 0; i < _emergencyExecutionCommittee.getQuorum() - 1; i++) { - vm.prank(members[i]); - _emergencyExecutionCommittee.voteEmergencyExecute(proposalIdToExecute, true); - (support, quorum,, isExecuted) = _emergencyExecutionCommittee.getEmergencyExecuteState(proposalIdToExecute); - assert(support < quorum); - assert(isExecuted == false); - } - - vm.prank(members[members.length - 1]); - _emergencyExecutionCommittee.voteEmergencyExecute(proposalIdToExecute, true); - (support, quorum,, isExecuted) = _emergencyExecutionCommittee.getEmergencyExecuteState(proposalIdToExecute); - assert(support == quorum); - assert(isExecuted == false); - - _emergencyExecutionCommittee.executeEmergencyExecute(proposalIdToExecute); - (support, quorum,, isExecuted) = _emergencyExecutionCommittee.getEmergencyExecuteState(proposalIdToExecute); - assert(isExecuted == true); - } -} diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index eec9589c..8470f52c 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -6,11 +6,14 @@ import {console} from "forge-std/Test.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {PercentsD16} from "contracts/types/PercentD16.sol"; -import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; +import {WithdrawalsBatchesQueue} from "contracts/libraries/WithdrawalsBatchesQueue.sol"; +import {AssetsAccounting, UnstETHRecordStatus} from "contracts/libraries/AssetsAccounting.sol"; -import {Escrow, VetoerState, LockedAssetsTotals, WithdrawalsBatchesQueue} from "contracts/Escrow.sol"; +import {Escrow} from "contracts/Escrow.sol"; import {ScenarioTestBlueprint, LidoUtils, console} from "../utils/scenario-test-blueprint.sol"; @@ -210,29 +213,30 @@ contract EscrowHappyPath is ScenarioTestBlueprint { uint256[] memory unstETHIds = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); uint256 totalSharesLocked; - WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = + _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); for (uint256 i = 0; i < unstETHIds.length; ++i) { totalSharesLocked += statuses[i].amountOfShares; } _lockUnstETH(_VETOER_1, unstETHIds); - VetoerState memory vetoerState = escrow.getVetoerState(_VETOER_1); - assertEq(vetoerState.unstETHIdsCount, 2); + Escrow.VetoerDetails memory vetoerDetails = escrow.getVetoerDetails(_VETOER_1); + assertEq(vetoerDetails.unstETHIdsCount, 2); - LockedAssetsTotals memory totals = escrow.getLockedAssetsTotals(); - assertEq(totals.unstETHFinalizedETH, 0); - assertEq(totals.unstETHUnfinalizedShares, totalSharesLocked); + ISignallingEscrow.SignallingEscrowDetails memory escrowDetails = escrow.getSignallingEscrowDetails(); + assertEq(escrowDetails.totalUnstETHFinalizedETH.toUint256(), 0); + assertEq(escrowDetails.totalUnstETHUnfinalizedShares.toUint256(), totalSharesLocked); _finalizeWithdrawalQueue(unstETHIds[0]); uint256[] memory hints = _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); escrow.markUnstETHFinalized(unstETHIds, hints); - totals = escrow.getLockedAssetsTotals(); - assertEq(totals.unstETHUnfinalizedShares, statuses[0].amountOfShares); + escrowDetails = escrow.getSignallingEscrowDetails(); + assertEq(escrowDetails.totalUnstETHUnfinalizedShares.toUint256(), statuses[0].amountOfShares); uint256 ethAmountFinalized = _lido.withdrawalQueue.getClaimableEther(unstETHIds, hints)[0]; - assertApproxEqAbs(totals.unstETHFinalizedETH, ethAmountFinalized, 1); + assertApproxEqAbs(escrowDetails.totalUnstETHFinalizedETH.toUint256(), ethAmountFinalized, 1); } function test_get_rage_quit_support() public { @@ -254,8 +258,8 @@ contract EscrowHappyPath is ScenarioTestBlueprint { uint256 totalSupply = _lido.stETH.totalSupply(); // epsilon is 2 here, because the wstETH unwrap may produce 1 wei error and stETH transfer 1 wei - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).stETHLockedShares, 2 * sharesToLock, 2); - assertEq(escrow.getVetoerState(_VETOER_1).unstETHIdsCount, 2); + assertApproxEqAbs(escrow.getVetoerDetails(_VETOER_1).stETHLockedShares.toUint256(), 2 * sharesToLock, 2); + assertEq(escrow.getVetoerDetails(_VETOER_1).unstETHIdsCount, 2); assertEq(escrow.getRageQuitSupport(), PercentsD16.fromFraction({numerator: 4 ether, denominator: totalSupply})); @@ -264,10 +268,12 @@ contract EscrowHappyPath is ScenarioTestBlueprint { _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); escrow.markUnstETHFinalized(unstETHIds, hints); - assertEq(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, sharesToLock); + assertEq(escrow.getSignallingEscrowDetails().totalUnstETHUnfinalizedShares.toUint256(), sharesToLock); uint256 ethAmountFinalized = _lido.withdrawalQueue.getClaimableEther(unstETHIds, hints)[0]; - assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHFinalizedETH, ethAmountFinalized, 1); + assertApproxEqAbs( + escrow.getSignallingEscrowDetails().totalUnstETHFinalizedETH.toUint256(), ethAmountFinalized, 1 + ); assertEq( escrow.getRageQuitSupport(), @@ -322,7 +328,8 @@ contract EscrowHappyPath is ScenarioTestBlueprint { uint256[] memory unstETHIdsToClaim = escrow.getNextWithdrawalBatch(expectedWithdrawalsBatchesCount); // assertEq(total, expectedWithdrawalsBatchesCount); - WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIdsToClaim); + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = + _lido.withdrawalQueue.getWithdrawalStatus(unstETHIdsToClaim); for (uint256 i = 0; i < statuses.length; ++i) { assertTrue(statuses[i].isFinalized); @@ -389,7 +396,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { escrow.requestNextWithdrawalsBatch(96); - vm.expectRevert(); + vm.expectRevert(WithdrawalsBatchesQueue.EmptyBatch.selector); escrow.claimNextWithdrawalsBatch(0, new uint256[](0)); escrow.startRageQuitExtensionPeriod(); @@ -481,22 +488,22 @@ contract EscrowHappyPath is ScenarioTestBlueprint { // After the Escrow enters RageQuitEscrow state, lock/unlock of tokens is forbidden // --- - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); this.externalLockStETH(_VETOER_1, 1 ether); - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); this.externalLockWstETH(_VETOER_1, 1 ether); - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); this.externalLockUnstETH(_VETOER_1, notLockedWithdrawalNfts); - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); this.externalUnlockStETH(_VETOER_1); - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); this.externalUnlockWstETH(_VETOER_1); - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); this.externalUnlockUnstETH(_VETOER_1, lockedWithdrawalNfts); } @@ -540,10 +547,84 @@ contract EscrowHappyPath is ScenarioTestBlueprint { vm.revertTo(snapshotId); // The attempt to unlock funds from Escrow will fail - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); this.externalUnlockStETH(_VETOER_1); } + function testFork_EdgeCase_frontRunningClaimUnStethFromBatchIsForbidden() external { + // Prepare vetoer1 unstETH nft to lock in Escrow + uint256 requestAmount = 10 * 1e18; + uint256[] memory amounts = new uint256[](1); + amounts[0] = requestAmount; + vm.prank(_VETOER_1); + uint256[] memory unstETHIdsVetoer1 = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); + + // Should be the same as vetoer1 unstETH nft + uint256 lastRequestIdBeforeBatch = _lido.withdrawalQueue.getLastRequestId(); + + // Lock unstETH nfts + _lockUnstETH(_VETOER_1, unstETHIdsVetoer1); + // Lock stETH to generate batch + _lockStETH(_VETOER_1, 20 * requestAmount); + + vm.prank(address(_dualGovernance)); + escrow.startRageQuit(_RAGE_QUIT_EXTRA_TIMELOCK, _RAGE_QUIT_WITHDRAWALS_TIMELOCK); + + uint256 batchSizeLimit = 16; + // Generate batch with stETH locked in Escrow + escrow.requestNextWithdrawalsBatch(batchSizeLimit); + + uint256[] memory nextWithdrawalBatch = escrow.getNextWithdrawalBatch(batchSizeLimit); + assertEq(nextWithdrawalBatch.length, 1); + assertEq(nextWithdrawalBatch[0], _lido.withdrawalQueue.getLastRequestId()); + + // Should be the id of unstETH nft in the batch + uint256 requestIdFromBatch = nextWithdrawalBatch[0]; + + // validate that the new unstEth nft is created + assertEq(requestIdFromBatch, lastRequestIdBeforeBatch + 1); + + _finalizeWithdrawalQueue(); + + // Check that unstETH nft of vetoer1 could be claimed + uint256[] memory unstETHIdsToClaim = new uint256[](1); + unstETHIdsToClaim[0] = lastRequestIdBeforeBatch; + uint256[] memory hints = _lido.withdrawalQueue.findCheckpointHints( + unstETHIdsToClaim, 1, _lido.withdrawalQueue.getLastCheckpointIndex() + ); + escrow.claimUnstETH(unstETHIdsToClaim, hints); + + // The attempt to claim funds of untEth from Escrow generated batch will fail + unstETHIdsToClaim[0] = requestIdFromBatch; + hints = _lido.withdrawalQueue.findCheckpointHints( + unstETHIdsToClaim, 1, _lido.withdrawalQueue.getLastCheckpointIndex() + ); + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, requestIdFromBatch, UnstETHRecordStatus.NotLocked + ) + ); + escrow.claimUnstETH(unstETHIdsToClaim, hints); + + // The rage quit process can be successfully finished + while (escrow.getUnclaimedUnstETHIdsCount() > 0) { + escrow.claimNextWithdrawalsBatch(batchSizeLimit); + } + + escrow.startRageQuitExtensionPeriod(); + assertEq(escrow.isRageQuitFinalized(), false); + + _wait(_RAGE_QUIT_EXTRA_TIMELOCK.plusSeconds(1)); + assertEq(escrow.isRageQuitFinalized(), true); + + _wait(_RAGE_QUIT_WITHDRAWALS_TIMELOCK.plusSeconds(1)); + + vm.startPrank(_VETOER_1); + escrow.withdrawETH(); + escrow.withdrawETH(unstETHIdsVetoer1); + vm.stopPrank(); + } + // --- // Helper external methods to test reverts // --- diff --git a/test/scenario/executor-ownership-transfer.t.sol b/test/scenario/executor-ownership-transfer.t.sol new file mode 100644 index 00000000..48e68c7c --- /dev/null +++ b/test/scenario/executor-ownership-transfer.t.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Durations} from "contracts/types/Duration.sol"; + +import {Ownable, Executor} from "contracts/Executor.sol"; +import {Proposers} from "contracts/libraries/Proposers.sol"; + +import {ExternalCall, ScenarioTestBlueprint, ExternalCallHelpers} from "../utils/scenario-test-blueprint.sol"; + +interface ISomeContract { + function someMethod(uint256 someParameter) external; +} + +contract ExecutorOwnershipTransfer is ScenarioTestBlueprint { + address private immutable _NEW_REGULAR_PROPOSER = makeAddr("NEW_REGULAR_PROPOSER"); + + Executor private _oldAdminExecutor; + Executor private _newAdminExecutor; + + function setUp() external { + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); + _newAdminExecutor = new Executor({owner: address(this)}); + + _oldAdminExecutor = Executor(payable(_timelock.getAdminExecutor())); + _newAdminExecutor.transferOwnership(address(_timelock)); + } + + function testFork_ExecutorOwnershipTransfer_HappyPath() external { + _step("1. DAO creates proposal to add new proposer and change the admin executor"); + uint256 shuffleExecutorsProposalId; + { + ExternalCall[] memory executorsShuffleCalls = ExternalCallHelpers.create( + [ + // 1. Register new proposer and assign it to the old admin executor + ExternalCall({ + value: 0, + target: address(_dualGovernance), + payload: abi.encodeCall( + _dualGovernance.registerProposer, (_NEW_REGULAR_PROPOSER, address(_oldAdminExecutor)) + ) + }), + // 2. Assign previous proposer (Aragon Voting) to the new executor + ExternalCall({ + value: 0, + target: address(_dualGovernance), + payload: abi.encodeCall( + _dualGovernance.setProposerExecutor, (address(_lido.voting), address(_newAdminExecutor)) + ) + }), + // 3. Replace the admin executor of the Timelock contract + ExternalCall({ + value: 0, + target: address(_timelock), + payload: abi.encodeCall(_timelock.setAdminExecutor, (address(_newAdminExecutor))) + }) + ] + ); + shuffleExecutorsProposalId = + _submitProposalViaDualGovernance("Register new proposer and swap executors", executorsShuffleCalls); + } + + _step("2. Proposal is scheduled and executed"); + { + _assertProposalSubmitted(shuffleExecutorsProposalId); + _waitAfterSubmitDelayPassed(); + + _scheduleProposalViaDualGovernance(shuffleExecutorsProposalId); + _assertProposalScheduled(shuffleExecutorsProposalId); + _waitAfterScheduleDelayPassed(); + + _executeProposal(shuffleExecutorsProposalId); + } + _step("3. The proposers and executors were set up correctly"); + { + assertEq(_timelock.getAdminExecutor(), address(_newAdminExecutor)); + + Proposers.Proposer[] memory proposers = _dualGovernance.getProposers(); + + assertEq(proposers.length, 2); + + assertEq(proposers[0].account, address(_lido.voting)); + assertEq(proposers[0].executor, address(_newAdminExecutor)); + + assertEq(proposers[1].account, _NEW_REGULAR_PROPOSER); + assertEq(proposers[1].executor, address(_oldAdminExecutor)); + } + + _step("4. New admin proposer can manage Dual Governance contracts"); + { + ExternalCall[] memory dgManageOperations = ExternalCallHelpers.create( + [ + ExternalCall({ + value: 0, + target: address(_dualGovernance), + payload: abi.encodeCall(_dualGovernance.unregisterProposer, (_NEW_REGULAR_PROPOSER)) + }), + ExternalCall({ + value: 0, + target: address(_timelock), + payload: abi.encodeCall( + _timelock.transferExecutorOwnership, (address(_oldAdminExecutor), address(_newAdminExecutor)) + ) + }), + ExternalCall({ + value: 0, + target: address(_oldAdminExecutor), + payload: abi.encodeCall( + _oldAdminExecutor.execute, (address(_targetMock), 0, abi.encodeCall(ISomeContract.someMethod, (42))) + ) + }), + ExternalCall({ + value: 0, + target: address(_oldAdminExecutor), + payload: abi.encodeCall(_oldAdminExecutor.transferOwnership, (address(_timelock))) + }), + ExternalCall({ + value: 0, + target: address(_timelock), + payload: abi.encodeCall(_timelock.setAfterSubmitDelay, (Durations.from(5 days))) + }), + ExternalCall({ + value: 0, + target: address(_timelock), + payload: abi.encodeCall(_timelock.setAfterScheduleDelay, (Durations.ZERO)) + }) + ] + ); + + uint256 proposalId = + _submitProposalViaDualGovernance("Manage Dual Governance parameters", dgManageOperations); + + _assertProposalSubmitted(proposalId); + _waitAfterSubmitDelayPassed(); + + _scheduleProposalViaDualGovernance(proposalId); + _assertProposalScheduled(proposalId); + _waitAfterScheduleDelayPassed(); + + _executeProposal(proposalId); + + Proposers.Proposer[] memory proposers = _dualGovernance.getProposers(); + + assertEq(proposers.length, 1); + assertEq(proposers[0].account, address(_lido.voting)); + assertEq(proposers[0].executor, address(_newAdminExecutor)); + + assertEq(_timelock.getAfterScheduleDelay(), Durations.ZERO); + + assertEq(_oldAdminExecutor.owner(), address(_timelock)); + } + } +} diff --git a/test/scenario/gov-state-transitions.t.sol b/test/scenario/gov-state-transitions.t.sol index 7946257a..a5eafee8 100644 --- a/test/scenario/gov-state-transitions.t.sol +++ b/test/scenario/gov-state-transitions.t.sol @@ -18,7 +18,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_state_min_duration() public { _assertNormalState(); - _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT()); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() - PercentsD16.from(1)); _assertNormalState(); _lockStETH(_VETOER, 1 gwei); @@ -63,7 +63,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_to_normal() public { _assertNormalState(); - _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT()); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() - PercentsD16.from(1)); _assertNormalState(); @@ -93,7 +93,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { function test_signalling_non_stop() public { _assertNormalState(); - _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT()); + _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT() - PercentsD16.from(1)); _assertNormalState(); _lockStETH(_VETOER, 1 gwei); diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index 6681778d..b387c9c6 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -14,8 +14,8 @@ import { } from "../utils/scenario-test-blueprint.sol"; import {DualGovernance} from "contracts/DualGovernance.sol"; -import {ExecutableProposals} from "contracts/libraries/ExecutableProposals.sol"; import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; +import {ExecutableProposals, Status as ProposalStatus} from "contracts/libraries/ExecutableProposals.sol"; contract PlanBSetup is ScenarioTestBlueprint { function setUp() external { @@ -97,7 +97,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // but the call still not executable _assertCanExecute(maliciousProposalId, false); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _executeProposal(maliciousProposalId); } @@ -325,7 +325,7 @@ contract PlanBSetup is ScenarioTestBlueprint { _wait(_timelock.getAfterScheduleDelay().plusSeconds(1)); _assertCanExecute(maliciousProposalId, false); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _executeProposal(maliciousProposalId); } @@ -350,7 +350,7 @@ contract PlanBSetup is ScenarioTestBlueprint { _wait(_timelock.getAfterScheduleDelay().plusSeconds(1)); _assertCanExecute(anotherMaliciousProposalId, false); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _executeProposal(anotherMaliciousProposalId); } @@ -359,10 +359,10 @@ contract PlanBSetup is ScenarioTestBlueprint { _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); assertTrue(emergencyState.emergencyModeEndsAfter < Timestamps.now()); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _executeProposal(maliciousProposalId); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _executeProposal(anotherMaliciousProposalId); } @@ -380,12 +380,18 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertProposalCancelled(anotherMaliciousProposalId); vm.expectRevert( - abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, maliciousProposalId) + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, maliciousProposalId, ProposalStatus.Cancelled + ) ); _executeProposal(maliciousProposalId); vm.expectRevert( - abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, anotherMaliciousProposalId) + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, + anotherMaliciousProposalId, + ProposalStatus.Cancelled + ) ); _executeProposal(anotherMaliciousProposalId); } diff --git a/test/scenario/reseal-committee.t.sol b/test/scenario/reseal-committee.t.sol deleted file mode 100644 index 30d419cd..00000000 --- a/test/scenario/reseal-committee.t.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {DualGovernance} from "contracts/DualGovernance.sol"; -import {ResealManager} from "contracts/ResealManager.sol"; -import {PercentsD16} from "contracts/types/PercentD16.sol"; -import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; - -import {ScenarioTestBlueprint, ExternalCall} from "../utils/scenario-test-blueprint.sol"; -import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; - -contract ResealCommitteeTest is ScenarioTestBlueprint { - address internal immutable _VETOER = makeAddr("VETOER"); - uint256 public constant PAUSE_INFINITELY = type(uint256).max; - - function setUp() external { - _deployDualGovernanceSetup({isEmergencyProtectionEnabled: true}); - _setupStETHBalance(_VETOER, PercentsD16.fromBasisPoints(10_00)); - _lockStETH(_VETOER, 1 ether); - } - - function test_reseal_committees_happy_path() external { - uint256 quorum; - uint256 support; - bool isExecuted; - - address[] memory members; - - address sealable = address(_lido.withdrawalQueue); - - vm.prank(DAO_AGENT); - _lido.withdrawalQueue.grantRole( - 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(this) - ); - - // Reseal - members = _resealCommittee.getMembers(); - for (uint256 i = 0; i < _resealCommittee.getQuorum() - 1; i++) { - vm.prank(members[i]); - _resealCommittee.voteReseal(sealable, true); - (support, quorum,) = _resealCommittee.getResealState(sealable); - assert(support < quorum); - } - - vm.prank(members[members.length - 1]); - _resealCommittee.voteReseal(sealable, true); - (support, quorum,) = _resealCommittee.getResealState(sealable); - assert(support == quorum); - - _assertNormalState(); - - vm.expectRevert(abi.encodeWithSelector(DualGovernance.ResealIsNotAllowedInNormalState.selector)); - _resealCommittee.executeReseal(sealable); - - _lockStETH(_VETOER, _dualGovernanceConfigProvider.FIRST_SEAL_RAGE_QUIT_SUPPORT()); - _lockStETH(_VETOER, 1 gwei); - _assertVetoSignalingState(); - - assertEq(_lido.withdrawalQueue.isPaused(), false); - vm.expectRevert(abi.encodeWithSelector(ResealManager.SealableWrongPauseState.selector)); - _resealCommittee.executeReseal(sealable); - - _lido.withdrawalQueue.pauseFor(3600 * 24 * 6); - assertEq(_lido.withdrawalQueue.isPaused(), true); - - _resealCommittee.executeReseal(sealable); - } -} diff --git a/test/scenario/time-sensitive-proposal-execution.t.sol b/test/scenario/time-sensitive-proposal-execution.t.sol new file mode 100644 index 00000000..e5e5e64b --- /dev/null +++ b/test/scenario/time-sensitive-proposal-execution.t.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Durations, Duration} from "contracts/types/Duration.sol"; +import {Timestamps, Timestamp} from "contracts/types/Timestamp.sol"; + +import {TimeConstraints} from "../utils/time-constraints.sol"; +import {ExternalCall, ExternalCallHelpers} from "../utils/executor-calls.sol"; +import {ScenarioTestBlueprint, LidoUtils, console} from "../utils/scenario-test-blueprint.sol"; + +interface ITimeSensitiveContract { + function timeSensitiveMethod() external; +} + +contract ScheduledProposalExecution is ScenarioTestBlueprint { + TimeConstraints private immutable _TIME_CONSTRAINTS = new TimeConstraints(); + + Duration private immutable _EXECUTION_DELAY = Durations.from(30 days); // Proposal may be executed not earlier than the 30 days from launch + Duration private immutable _EXECUTION_START_DAY_TIME = Durations.from(4 hours); // And at time frame starting from the 4:00 UTC + Duration private immutable _EXECUTION_END_DAY_TIME = Durations.from(12 hours); // till the 12:00 UTC + + function setUp() external { + _deployDualGovernanceSetup({isEmergencyProtectionEnabled: false}); + } + + function testFork_TimeFrameProposalExecution() external { + Timestamp executableAfter = _EXECUTION_DELAY.addTo(Timestamps.now()); + // Prepare the call to be launched not earlier than the minExecutionDelay seconds from the creation of the + // Aragon Voting to submit proposal and only in the day time range [executionStartDayTime, executionEndDayTime] in UTC + ExternalCall[] memory scheduledProposalCalls = ExternalCallHelpers.create( + [ + ExternalCall({ + target: address(_TIME_CONSTRAINTS), + value: 0 wei, + payload: abi.encodeCall(_TIME_CONSTRAINTS.checkExecuteAfterTimestamp, (executableAfter)) + }), + ExternalCall({ + target: address(_TIME_CONSTRAINTS), + value: 0 wei, + payload: abi.encodeCall( + _TIME_CONSTRAINTS.checkExecuteWithinDayTime, (_EXECUTION_START_DAY_TIME, _EXECUTION_END_DAY_TIME) + ) + }), + ExternalCall({ + target: address(_targetMock), + value: 0 wei, + payload: abi.encodeCall(ITimeSensitiveContract.timeSensitiveMethod, ()) + }) + ] + ); + + uint256 proposalId; + _step("1. Submit time sensitive proposal"); + { + proposalId = + _submitProposal(_dualGovernance, "DAO performs some time sensitive action", scheduledProposalCalls); + + _assertProposalSubmitted(proposalId); + _assertSubmittedProposalData(proposalId, scheduledProposalCalls); + } + + _step("2. Wait while the DG timelock has passed & schedule proposal"); + { + _wait(_timelock.getAfterSubmitDelay().plusSeconds(1)); + _assertCanScheduleViaDualGovernance(proposalId, true); + _scheduleProposalViaDualGovernance(proposalId); + _assertProposalScheduled(proposalId); + } + + _step("3. Proposal can't be executed earlier than specified date"); + { + _waitAfterScheduleDelayPassed(); + _assertCanExecute(proposalId, true); + assertTrue(Timestamps.now() < executableAfter); + + vm.expectRevert(abi.encodeWithSelector(TimeConstraints.TimestampNotReached.selector, (executableAfter))); + _executeProposal(proposalId); + } + + _step("4. Wait until the proposal become executable"); + { + _wait(_EXECUTION_DELAY); + assertTrue(Timestamps.now() >= executableAfter); + } + + uint256 midnightSnapshotId; + _step("5. Adjust current day time of the node to 00:00 UTC"); + { + // adjust current time to 00:00 UTC + _wait(_TIME_CONSTRAINTS.DAY_DURATION() - _TIME_CONSTRAINTS.getCurrentDayTime()); + assertEq(_TIME_CONSTRAINTS.getCurrentDayTime(), Durations.ZERO); + + midnightSnapshotId = vm.snapshot(); + } + + _step("6.a. Execution reverts when current time is less than allowed range"); + { + assertTrue(_TIME_CONSTRAINTS.getCurrentDayTime() < _EXECUTION_START_DAY_TIME); + + vm.expectRevert( + abi.encodeWithSelector( + TimeConstraints.DayTimeOutOfRange.selector, + _TIME_CONSTRAINTS.getCurrentDayTime(), + _EXECUTION_START_DAY_TIME, + _EXECUTION_END_DAY_TIME + ) + ); + _executeProposal(proposalId); + } + vm.revertTo(midnightSnapshotId); + + _step("6.b. Execution reverts when current time is greater than allowed range"); + { + _wait(_EXECUTION_END_DAY_TIME.plusSeconds(1)); + assertTrue(_TIME_CONSTRAINTS.getCurrentDayTime() > _EXECUTION_END_DAY_TIME); + + vm.expectRevert( + abi.encodeWithSelector( + TimeConstraints.DayTimeOutOfRange.selector, + _TIME_CONSTRAINTS.getCurrentDayTime(), + _EXECUTION_START_DAY_TIME, + _EXECUTION_END_DAY_TIME + ) + ); + _executeProposal(proposalId); + } + vm.revertTo(midnightSnapshotId); + + ExternalCall[] memory expectedProposalCalls = ExternalCallHelpers.create([scheduledProposalCalls[2]]); + + _step("6.c. Proposal executes successfully at the first second of the allowed range"); + { + _wait(_EXECUTION_START_DAY_TIME); + assertTrue( + _TIME_CONSTRAINTS.getCurrentDayTime() >= _EXECUTION_START_DAY_TIME + && _TIME_CONSTRAINTS.getCurrentDayTime() <= _EXECUTION_END_DAY_TIME + ); + + _executeProposal(proposalId); + _assertTargetMockCalls(_timelock.getAdminExecutor(), expectedProposalCalls); + } + vm.revertTo(midnightSnapshotId); + + _step("6.d. Proposal executes successfully at the last second of the allowed range"); + { + _wait(_EXECUTION_END_DAY_TIME); + assertTrue( + _TIME_CONSTRAINTS.getCurrentDayTime() >= _EXECUTION_START_DAY_TIME + && _TIME_CONSTRAINTS.getCurrentDayTime() <= _EXECUTION_END_DAY_TIME + ); + + _executeProposal(proposalId); + _assertTargetMockCalls(_timelock.getAdminExecutor(), expectedProposalCalls); + } + vm.revertTo(midnightSnapshotId); + + _step("6.e. Proposal executes successfully at the middle of the allowed range"); + { + _wait((_EXECUTION_END_DAY_TIME - _EXECUTION_START_DAY_TIME).dividedBy(2)); + assertTrue( + _TIME_CONSTRAINTS.getCurrentDayTime() >= _EXECUTION_START_DAY_TIME + && _TIME_CONSTRAINTS.getCurrentDayTime() <= _EXECUTION_END_DAY_TIME + ); + + _executeProposal(proposalId); + _assertTargetMockCalls(_timelock.getAdminExecutor(), expectedProposalCalls); + } + vm.revertTo(midnightSnapshotId); + } +} diff --git a/test/scenario/timelocked-governance.t.sol b/test/scenario/timelocked-governance.t.sol index d6be5be5..c822e75f 100644 --- a/test/scenario/timelocked-governance.t.sol +++ b/test/scenario/timelocked-governance.t.sol @@ -99,7 +99,7 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { _assertCanExecute(maliciousProposalId, false); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _executeProposal(maliciousProposalId); } @@ -174,7 +174,7 @@ contract TimelockedGovernanceScenario is ScenarioTestBlueprint { _assertCanExecute(maliciousProposalId, false); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _executeProposal(maliciousProposalId); } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 7dd6a664..31c1293b 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.26; import {Duration, Durations, lte} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {PercentsD16, PercentD16} from "contracts/types/PercentD16.sol"; +import {PercentsD16} from "contracts/types/PercentD16.sol"; import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; @@ -11,9 +11,13 @@ import {Escrow} from "contracts/Escrow.sol"; import {Executor} from "contracts/Executor.sol"; import {DualGovernance, State, DualGovernanceStateMachine} from "contracts/DualGovernance.sol"; import {Tiebreaker} from "contracts/libraries/Tiebreaker.sol"; +import {Resealer} from "contracts/libraries/Resealer.sol"; import {Status as ProposalStatus} from "contracts/libraries/ExecutableProposals.sol"; import {Proposers} from "contracts/libraries/Proposers.sol"; + +import {IGovernance} from "contracts/interfaces/IGovernance.sol"; import {IResealManager} from "contracts/interfaces/IResealManager.sol"; + import { DualGovernanceConfig, IDualGovernanceConfigProvider, @@ -24,22 +28,25 @@ import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; import {IWstETH} from "contracts/interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; import {ITimelock} from "contracts/interfaces/ITimelock.sol"; -import {ISealable} from "contracts/interfaces/ISealable.sol"; import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; import {UnitTest} from "test/utils/unit-test.sol"; import {StETHMock} from "test/mocks/StETHMock.sol"; import {TimelockMock} from "test/mocks/TimelockMock.sol"; import {WithdrawalQueueMock} from "test/mocks/WithdrawalQueueMock.sol"; import {SealableMock} from "test/mocks/SealableMock.sol"; +import {computeAddress} from "test/utils/addresses.sol"; contract DualGovernanceUnitTests is UnitTest { Executor private _executor = new Executor(address(this)); address private vetoer = makeAddr("vetoer"); + address private resealCommittee = makeAddr("resealCommittee"); + address private proposalsCanceller = makeAddr("proposalsCanceller"); StETHMock private immutable _STETH_MOCK = new StETHMock(); - IWithdrawalQueue private immutable _WITHDRAWAL_QUEUE_MOCK = new WithdrawalQueueMock(); + IWithdrawalQueue private immutable _WITHDRAWAL_QUEUE_MOCK = new WithdrawalQueueMock(_STETH_MOCK); // TODO: Replace with mocks IWstETH private immutable _WSTETH_STUB = IWstETH(makeAddr("WSTETH_STUB")); @@ -67,10 +74,13 @@ contract DualGovernanceUnitTests is UnitTest { }) ); - DualGovernance.ExternalDependencies internal _externalDependencies = DualGovernance.ExternalDependencies({ + DualGovernance.SignallingTokens internal _signallingTokens = DualGovernance.SignallingTokens({ stETH: _STETH_MOCK, wstETH: _WSTETH_STUB, - withdrawalQueue: _WITHDRAWAL_QUEUE_MOCK, + withdrawalQueue: _WITHDRAWAL_QUEUE_MOCK + }); + + DualGovernance.DualGovernanceComponents internal _dgComponents = DualGovernance.DualGovernanceComponents({ timelock: _timelock, resealManager: _RESEAL_MANAGER_STUB, configProvider: _configProvider @@ -80,11 +90,15 @@ contract DualGovernanceUnitTests is UnitTest { minWithdrawalsBatchSize: 4, minTiebreakerActivationTimeout: Durations.from(30 days), maxTiebreakerActivationTimeout: Durations.from(180 days), - maxSealableWithdrawalBlockersCount: 128 + maxSealableWithdrawalBlockersCount: 128, + maxMinAssetsLockDuration: Durations.from(365 days) }); - DualGovernance internal _dualGovernance = - new DualGovernance({dependencies: _externalDependencies, sanityCheckParams: _sanityCheckParams}); + DualGovernance internal _dualGovernance = new DualGovernance({ + components: _dgComponents, + signallingTokens: _signallingTokens, + sanityCheckParams: _sanityCheckParams + }); Escrow internal _escrow; @@ -95,6 +109,18 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.registerProposer.selector, address(this), address(_executor)) ); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setProposalsCanceller.selector, proposalsCanceller) + ); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, resealCommittee) + ); + _escrow = Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); _STETH_MOCK.mint(vetoer, 10 ether); vm.prank(vetoer); @@ -105,23 +131,94 @@ contract DualGovernanceUnitTests is UnitTest { _sanityCheckParams.minTiebreakerActivationTimeout = Durations.from(999); _sanityCheckParams.maxTiebreakerActivationTimeout = Durations.from(1000); - new DualGovernance({dependencies: _externalDependencies, sanityCheckParams: _sanityCheckParams}); + new DualGovernance({ + components: _dgComponents, + signallingTokens: _signallingTokens, + sanityCheckParams: _sanityCheckParams + }); } function test_constructor_min_max_timeout_same() external { _sanityCheckParams.minTiebreakerActivationTimeout = Durations.from(1000); _sanityCheckParams.maxTiebreakerActivationTimeout = Durations.from(1000); - new DualGovernance({dependencies: _externalDependencies, sanityCheckParams: _sanityCheckParams}); + new DualGovernance({ + components: _dgComponents, + signallingTokens: _signallingTokens, + sanityCheckParams: _sanityCheckParams + }); } function test_constructor_RevertsOn_InvalidTiebreakerActivationTimeoutBounds() external { _sanityCheckParams.minTiebreakerActivationTimeout = Durations.from(1000); _sanityCheckParams.maxTiebreakerActivationTimeout = Durations.from(999); - vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidTiebreakerActivationTimeoutBounds.selector)); + vm.expectRevert( + abi.encodeWithSelector( + DualGovernance.InvalidTiebreakerActivationTimeoutBounds.selector, + _sanityCheckParams.minTiebreakerActivationTimeout, + _sanityCheckParams.maxTiebreakerActivationTimeout + ) + ); - new DualGovernance({dependencies: _externalDependencies, sanityCheckParams: _sanityCheckParams}); + new DualGovernance({ + components: _dgComponents, + signallingTokens: _signallingTokens, + sanityCheckParams: _sanityCheckParams + }); + } + + // --- + // constructor() + // --- + + function test_constructor_HappyPath() external { + address testDeployerAddress = address(this); + uint256 testDeployerNonce = vm.getNonce(testDeployerAddress); + address predictedDualGovernanceAddress = computeAddress(testDeployerAddress, testDeployerNonce); + + address predictedEscrowCopyAddress = computeAddress(predictedDualGovernanceAddress, 1); + + vm.expectEmit(); + emit DualGovernance.EscrowMasterCopyDeployed(IEscrowBase(predictedEscrowCopyAddress)); + vm.expectEmit(); + emit Resealer.ResealManagerSet(_RESEAL_MANAGER_STUB); + + Duration minTiebreakerActivationTimeout = Durations.from(30 days); + Duration maxTiebreakerActivationTimeout = Durations.from(180 days); + uint256 maxSealableWithdrawalBlockersCount = 128; + Duration maxMinAssetsLockDuration = Durations.from(365 days); + + DualGovernance dualGovernanceLocal = new DualGovernance({ + components: DualGovernance.DualGovernanceComponents({ + timelock: _timelock, + resealManager: _RESEAL_MANAGER_STUB, + configProvider: _configProvider + }), + signallingTokens: DualGovernance.SignallingTokens({ + stETH: _STETH_MOCK, + wstETH: _WSTETH_STUB, + withdrawalQueue: _WITHDRAWAL_QUEUE_MOCK + }), + sanityCheckParams: DualGovernance.SanityCheckParams({ + minWithdrawalsBatchSize: 4, + minTiebreakerActivationTimeout: minTiebreakerActivationTimeout, + maxTiebreakerActivationTimeout: maxTiebreakerActivationTimeout, + maxSealableWithdrawalBlockersCount: maxSealableWithdrawalBlockersCount, + maxMinAssetsLockDuration: maxMinAssetsLockDuration + }) + }); + + address payable escrowMasterCopyAddress = + payable(address(IEscrowBase(dualGovernanceLocal.getVetoSignallingEscrow()).ESCROW_MASTER_COPY())); + + assertEq(address(dualGovernanceLocal.TIMELOCK()), address(_timelock)); + assertEq(dualGovernanceLocal.MIN_TIEBREAKER_ACTIVATION_TIMEOUT(), minTiebreakerActivationTimeout); + assertEq(dualGovernanceLocal.MAX_TIEBREAKER_ACTIVATION_TIMEOUT(), maxTiebreakerActivationTimeout); + assertEq(dualGovernanceLocal.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT(), maxSealableWithdrawalBlockersCount); + assertEq(escrowMasterCopyAddress, predictedEscrowCopyAddress); + + assertEq(Escrow(escrowMasterCopyAddress).MAX_MIN_ASSETS_LOCK_DURATION(), maxMinAssetsLockDuration); } // --- @@ -131,13 +228,15 @@ contract DualGovernanceUnitTests is UnitTest { function test_submitProposal_HappyPath() external { ExternalCall[] memory calls = _generateExternalCalls(); Proposers.Proposer memory proposer = _dualGovernance.getProposer(address(this)); - vm.expectCall( - address(_timelock), - 0, - abi.encodeWithSelector(TimelockMock.submit.selector, address(this), proposer.executor, calls, "") - ); + vm.expectCall(address(_timelock), 0, abi.encodeCall(TimelockMock.submit, (proposer.executor, calls))); - uint256 proposalId = _dualGovernance.submitProposal(calls, ""); + uint256 expectedProposalId = 1; + string memory metadata = "New proposal description"; + + vm.expectEmit(); + emit IGovernance.ProposalSubmitted(proposer.account, expectedProposalId, metadata); + + uint256 proposalId = _dualGovernance.submitProposal(calls, metadata); uint256[] memory submittedProposals = _timelock.getSubmittedProposals(); assertEq(submittedProposals.length, 1); @@ -278,6 +377,7 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsSkipped(); + vm.prank(proposalsCanceller); bool isProposalsCancelled = _dualGovernance.cancelAllPendingProposals(); assertFalse(isProposalsCancelled); @@ -314,6 +414,7 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsSkipped(); + vm.prank(proposalsCanceller); bool isProposalsCancelled = _dualGovernance.cancelAllPendingProposals(); assertFalse(isProposalsCancelled); @@ -341,6 +442,7 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsSkipped(); + vm.prank(proposalsCanceller); bool isProposalsCancelled = _dualGovernance.cancelAllPendingProposals(); assertFalse(isProposalsCancelled); @@ -363,6 +465,7 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsExecuted(); + vm.prank(proposalsCanceller); bool isProposalsCancelled = _dualGovernance.cancelAllPendingProposals(); assertTrue(isProposalsCancelled); @@ -392,6 +495,7 @@ contract DualGovernanceUnitTests is UnitTest { vm.expectEmit(); emit DualGovernance.CancelAllPendingProposalsExecuted(); + vm.prank(proposalsCanceller); bool isProposalsCancelled = _dualGovernance.cancelAllPendingProposals(); assertTrue(isProposalsCancelled); @@ -399,19 +503,16 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(_timelock.lastCancelledProposalId(), 1); } - function test_cancelAllPendingProposals_RevertOn_NotAdminProposer() external { - address nonAdminProposer = makeAddr("NON_ADMIN_PROPOSER"); - _executor.execute( - address(_dualGovernance), - 0, - abi.encodeWithSelector(DualGovernance.registerProposer.selector, nonAdminProposer, address(0x123)) - ); + function test_cancelAllPendingProposals_RevertOn_CallerNotProposalsCanceller() external { + address notProposalsCanceller = makeAddr("NON_PROPOSALS_CANCELLER"); _submitMockProposal(); assertEq(_timelock.getProposalsCount(), 1); - vm.prank(nonAdminProposer); - vm.expectRevert(abi.encodeWithSelector(DualGovernance.NotAdminProposer.selector)); + vm.prank(notProposalsCanceller); + vm.expectRevert( + abi.encodeWithSelector(DualGovernance.CallerIsNotProposalsCanceller.selector, notProposalsCanceller) + ); _dualGovernance.cancelAllPendingProposals(); assertEq(_timelock.getProposalsCount(), 1); @@ -475,7 +576,7 @@ contract DualGovernanceUnitTests is UnitTest { _wait(_configProvider.VETO_SIGNALLING_MAX_DURATION().dividedBy(2)); - // The RageQuit second seal threshold wasn't crossed, the system should enter Deactivation state + // The RageQuit second seal threshold wasn't reached, the system should enter Deactivation state // where the proposals submission is not allowed assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); assertEq(_dualGovernance.getEffectiveState(), State.VetoSignallingDeactivation); @@ -692,7 +793,7 @@ contract DualGovernanceUnitTests is UnitTest { abi.encode(true) ); - // The RageQuit second seal threshold wasn't crossed, the system should enter Deactivation state + // The RageQuit second seal threshold wasn't reached, the system should enter Deactivation state // where the proposals submission is not allowed assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); assertEq(_dualGovernance.getEffectiveState(), State.VetoSignallingDeactivation); @@ -984,6 +1085,58 @@ contract DualGovernanceUnitTests is UnitTest { assertTrue(address(_dualGovernance.getConfigProvider()) != address(oldConfigProvider)); } + function test_setConfigProvider_same_minAssetsLockDuration() external { + Duration newMinAssetsLockDuration = Durations.from(5 hours); + ImmutableDualGovernanceConfigProvider newConfigProvider = new ImmutableDualGovernanceConfigProvider( + DualGovernanceConfig.Context({ + firstSealRageQuitSupport: PercentsD16.fromBasisPoints(5_00), // 5% + secondSealRageQuitSupport: PercentsD16.fromBasisPoints(20_00), // 20% + // + minAssetsLockDuration: newMinAssetsLockDuration, + // + vetoSignallingMinDuration: Durations.from(4 days), + vetoSignallingMaxDuration: Durations.from(35 days), + vetoSignallingMinActiveDuration: Durations.from(6 hours), + vetoSignallingDeactivationMaxDuration: Durations.from(6 days), + vetoCooldownDuration: Durations.from(5 days), + // + rageQuitExtensionPeriodDuration: Durations.from(8 days), + rageQuitEthWithdrawalsMinDelay: Durations.from(30 days), + rageQuitEthWithdrawalsMaxDelay: Durations.from(180 days), + rageQuitEthWithdrawalsDelayGrowth: Durations.from(15 days) + }) + ); + + IDualGovernanceConfigProvider oldConfigProvider = _dualGovernance.getConfigProvider(); + + vm.expectEmit(); + emit DualGovernanceStateMachine.ConfigProviderSet(IDualGovernanceConfigProvider(address(newConfigProvider))); + vm.expectEmit(); + emit Executor.Executed( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setConfigProvider.selector, address(newConfigProvider)), + new bytes(0) + ); + + vm.expectCall( + address(_escrow), + 0, + abi.encodeWithSelector(Escrow.setMinAssetsLockDuration.selector, newMinAssetsLockDuration), + 0 + ); + vm.recordLogs(); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setConfigProvider.selector, address(newConfigProvider)) + ); + assertEq(vm.getRecordedLogs().length, 2); + + assertEq(address(_dualGovernance.getConfigProvider()), address(newConfigProvider)); + assertTrue(address(_dualGovernance.getConfigProvider()) != address(oldConfigProvider)); + } + function testFuzz_setConfigProvider_RevertOn_NotAdminExecutor(address stranger) external { vm.assume(stranger != address(_executor)); ImmutableDualGovernanceConfigProvider newConfigProvider = new ImmutableDualGovernanceConfigProvider( @@ -1031,6 +1184,66 @@ contract DualGovernanceUnitTests is UnitTest { ); } + // --- + // setProposalsCanceller() + // --- + + function test_setProposalsCanceller_HappyPath() external { + address newProposalsCanceller = makeAddr("newProposalsCanceller"); + + assertNotEq(newProposalsCanceller, _dualGovernance.getProposalsCanceller()); + + vm.expectEmit(); + emit DualGovernance.ProposalsCancellerSet(newProposalsCanceller); + + vm.prank(address(_executor)); + _dualGovernance.setProposalsCanceller(newProposalsCanceller); + + assertEq(newProposalsCanceller, _dualGovernance.getProposalsCanceller()); + } + + function testFuzz_setProposalsCanceller_HappyPath(address newProposalsCanceller) external { + vm.assume( + newProposalsCanceller != address(0) && newProposalsCanceller != _dualGovernance.getProposalsCanceller() + ); + + vm.expectEmit(); + emit DualGovernance.ProposalsCancellerSet(newProposalsCanceller); + + vm.prank(address(_executor)); + _dualGovernance.setProposalsCanceller(newProposalsCanceller); + + assertEq(newProposalsCanceller, _dualGovernance.getProposalsCanceller()); + } + + function test_setProposalsCanceller_RevertOn_CancellerIsZeroAddress() external { + vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidProposalsCanceller.selector, address(0))); + + vm.prank(address(_executor)); + _dualGovernance.setProposalsCanceller(address(0)); + } + + function test_setProposalsCanceller_RevertOn_NewCancellerAddressIsTheSame() external { + address prevProposalsCanceller = _dualGovernance.getProposalsCanceller(); + assertNotEq(prevProposalsCanceller, address(0)); + + vm.expectRevert( + abi.encodeWithSelector(DualGovernance.InvalidProposalsCanceller.selector, prevProposalsCanceller) + ); + + vm.prank(address(_executor)); + _dualGovernance.setProposalsCanceller(prevProposalsCanceller); + } + + function testFuzz_setProposalsCanceller_RevertOn_CalledNotByAdminExecutor(address notAllowedCaller) external { + vm.assume(notAllowedCaller != address(_executor)); + + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, notAllowedCaller)); + + vm.prank(notAllowedCaller); + _dualGovernance.setProposalsCanceller(notAllowedCaller); + } + // --- // getConfigProvider() // --- @@ -1228,6 +1441,63 @@ contract DualGovernanceUnitTests is UnitTest { _dualGovernance.registerProposer(newProposer, newExecutor); } + // --- + // setProposerExecutor() + // --- + + function test_setProposerExecutor_HappyPath() external { + address newProposer = makeAddr("NEW_PROPOSER"); + address newExecutor = makeAddr("NEW_EXECUTOR"); + + assertEq(_dualGovernance.getProposers().length, 1); + assertFalse(_dualGovernance.isProposer(newProposer)); + + vm.prank(address(_executor)); + _dualGovernance.registerProposer(newProposer, newExecutor); + + assertEq(_dualGovernance.getProposers().length, 2); + assertTrue(_dualGovernance.isProposer(newProposer)); + + vm.prank(address(_executor)); + _dualGovernance.setProposerExecutor(newProposer, address(_executor)); + + assertEq(_dualGovernance.getProposers().length, 2); + assertTrue(_dualGovernance.isProposer(newProposer)); + assertFalse(_dualGovernance.isExecutor(newExecutor)); + } + + function testFuzz_setProposerExecutor_RevertOn_CalledNotByAdminExecutor(address notAllowedCaller) external { + vm.assume(notAllowedCaller != address(_executor)); + + address newProposer = makeAddr("NEW_PROPOSER"); + address newExecutor = makeAddr("NEW_EXECUTOR"); + + vm.prank(address(_executor)); + _dualGovernance.registerProposer(newProposer, newExecutor); + + assertEq(_dualGovernance.getProposers().length, 2); + assertTrue(_dualGovernance.isProposer(newProposer)); + + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, notAllowedCaller)); + + vm.prank(notAllowedCaller); + _dualGovernance.setProposerExecutor(newProposer, address(_executor)); + } + + function test_setProposerExecutor_RevertOn_AttemptToRemoveLastAdminProposer() external { + address newExecutor = makeAddr("NEW_EXECUTOR"); + + assertEq(_dualGovernance.getProposers().length, 1); + + assertTrue(_dualGovernance.isProposer(address(this))); + assertTrue(_dualGovernance.isExecutor(address(_executor))); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, address(_executor))); + + vm.prank(address(_executor)); + _dualGovernance.setProposerExecutor(address(this), address(newExecutor)); + } + // --- // unregisterProposer() // --- @@ -1276,7 +1546,7 @@ contract DualGovernanceUnitTests is UnitTest { abi.encodeWithSelector(DualGovernance.registerProposer.selector, proposer, proposerExecutor) ); - vm.expectRevert(abi.encodeWithSelector(DualGovernance.UnownedAdminExecutor.selector)); + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, (adminExecutor))); _executor.execute( address(_dualGovernance), 0, @@ -2009,15 +2279,8 @@ contract DualGovernanceUnitTests is UnitTest { // --- function test_resealSealable_HappyPath() external { - address resealCommittee = makeAddr("RESEAL_COMMITTEE"); address sealable = address(new SealableMock()); - _executor.execute( - address(_dualGovernance), - 0, - abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, resealCommittee) - ); - vm.startPrank(vetoer); _escrow.lockStETH(5 ether); vm.stopPrank(); @@ -2051,20 +2314,13 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(_dualGovernance.getPersistedState(), State.VetoSignalling); vm.prank(notResealCommittee); - vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotResealCommittee.selector, notResealCommittee)); + vm.expectRevert(abi.encodeWithSelector(Resealer.CallerIsNotResealCommittee.selector, notResealCommittee)); _dualGovernance.resealSealable(sealable); } function test_resealSealable_RevertOn_NormalState() external { - address resealCommittee = makeAddr("RESEAL_COMMITTEE"); address sealable = address(new SealableMock()); - _executor.execute( - address(_dualGovernance), - 0, - abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, resealCommittee) - ); - assertEq(_dualGovernance.getPersistedState(), State.Normal); vm.prank(resealCommittee); @@ -2077,18 +2333,32 @@ contract DualGovernanceUnitTests is UnitTest { // --- function testFuzz_setResealCommittee_HappyPath(address newResealCommittee) external { - address resealCommittee = makeAddr("RESEAL_COMMITTEE"); vm.assume(newResealCommittee != resealCommittee); - _executor.execute( address(_dualGovernance), 0, - abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, resealCommittee) + abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, newResealCommittee) ); + assertEq(newResealCommittee, address(_dualGovernance.getResealCommittee())); + } - vm.expectEmit(); - emit DualGovernance.ResealCommitteeSet(newResealCommittee); + function testFuzz_setResealCommittee_RevertOn_NotAdminExecutor(address stranger) external { + vm.assume(stranger != address(_executor)); + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, stranger)); + _dualGovernance.setResealCommittee(makeAddr("NEW_RESEAL_COMMITTEE")); + } + + function testFuzz_setResealCommittee_RevertOn_InvalidResealCommittee(address newResealCommittee) external { + vm.assume(_dualGovernance.getResealCommittee() != newResealCommittee); + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, newResealCommittee) + ); + + vm.expectRevert(abi.encodeWithSelector(Resealer.InvalidResealCommittee.selector, newResealCommittee)); _executor.execute( address(_dualGovernance), 0, @@ -2096,19 +2366,58 @@ contract DualGovernanceUnitTests is UnitTest { ); } - function testFuzz_setResealCommittee_RevertOn_NotAdminExecutor(address stranger) external { + // --- + // setResealManager() + // --- + + function testFuzz_setResealManager_HappyPath(address newResealManager) external { + vm.assume(newResealManager != address(0) && newResealManager != address(_RESEAL_MANAGER_STUB)); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealManager.selector, newResealManager) + ); + } + + function test_setResealManger_RevertOn_CallerIsNotAdminExecutor(address stranger) external { vm.assume(stranger != address(_executor)); + address newResealManager = makeAddr("NEW_RESEAL_MANAGER"); vm.prank(stranger); vm.expectRevert(abi.encodeWithSelector(DualGovernance.CallerIsNotAdminExecutor.selector, stranger)); - _dualGovernance.setResealCommittee(makeAddr("NEW_RESEAL_COMMITTEE")); + _dualGovernance.setResealManager(IResealManager(newResealManager)); + } + + // --- + // getResealManager() + // --- + + function testFuzz_getResealManager_HappyPath(address newResealManager) external { + vm.assume(newResealManager != address(_RESEAL_MANAGER_STUB)); + vm.assume(newResealManager != address(0)); + + _executor.execute( + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealManager.selector, newResealManager) + ); + + assertEq(newResealManager, address(_dualGovernance.getResealManager())); } - function test_setResealCommittee_RevertOn_SameAddress() external { - vm.expectRevert(abi.encodeWithSelector(DualGovernance.InvalidResealCommittee.selector, address(0))); + // --- + // getResealCommittee() + // --- + + function testFuzz_getResealCommittee_HappyPath(address newResealCommittee) external { + vm.assume(newResealCommittee != resealCommittee); _executor.execute( - address(_dualGovernance), 0, abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, address(0)) + address(_dualGovernance), + 0, + abi.encodeWithSelector(DualGovernance.setResealCommittee.selector, newResealCommittee) ); + assertEq(newResealCommittee, address(_dualGovernance.getResealCommittee())); } // --- @@ -2116,8 +2425,27 @@ contract DualGovernanceUnitTests is UnitTest { // --- function _submitMockProposal() internal { - // mock timelock doesn't uses proposal data - _timelock.submit(msg.sender, address(0), new ExternalCall[](0), ""); + _timelock.submit(address(0), new ExternalCall[](0)); + } + + function _scheduleProposal(uint256 proposalId, Timestamp submittedAt) internal { + _timelock.setSchedule(proposalId); + + vm.mockCall( + address(_timelock), + abi.encodeWithSelector(TimelockMock.getProposalDetails.selector, proposalId), + abi.encode( + ITimelock.ProposalDetails({ + id: proposalId, + status: ProposalStatus.Submitted, + executor: address(_executor), + submittedAt: submittedAt, + scheduledAt: Timestamps.from(0) + }) + ) + ); + vm.expectCall(address(_timelock), 0, abi.encodeWithSelector(TimelockMock.schedule.selector, proposalId)); + _dualGovernance.scheduleProposal(proposalId); } function _generateExternalCalls() internal pure returns (ExternalCall[] memory calls) { diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index a44c885b..f308fa23 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -11,6 +11,7 @@ import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtec import {ITimelock, ProposalStatus} from "contracts/interfaces/ITimelock.sol"; import {EmergencyProtection} from "contracts/libraries/EmergencyProtection.sol"; +import {ExecutableProposals} from "contracts/libraries/ExecutableProposals.sol"; import {Executor} from "contracts/Executor.sol"; import {EmergencyProtectedTimelock, TimelockState} from "contracts/EmergencyProtectedTimelock.sol"; @@ -76,6 +77,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { Duration afterSubmitDelay = _defaultAfterSubmitDelay; Duration afterScheduleDelay = _defaultAfterScheduleDelay; + vm.expectEmit(); + emit TimelockState.AdminExecutorSet(adminExecutor); + vm.expectEmit(); emit TimelockState.AfterSubmitDelaySet(afterSubmitDelay); @@ -98,12 +102,15 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { Duration afterSubmitDelay = Durations.ZERO; Duration afterScheduleDelay = Durations.ZERO; + vm.expectEmit(); + emit TimelockState.AdminExecutorSet(adminExecutor); + vm.recordLogs(); EmergencyProtectedTimelock timelock = new EmergencyProtectedTimelock(sanityCheckParams, adminExecutor, afterSubmitDelay, afterScheduleDelay); - assertEq(vm.getRecordedLogs().length, 0); + assertEq(vm.getRecordedLogs().length, 1); _assertEmergencyProtectedTimelockConstructorParams( timelock, sanityCheckParams, adminExecutor, afterSubmitDelay, afterScheduleDelay @@ -158,6 +165,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { Duration afterSubmitDelay, Duration afterScheduleDelay ) external { + vm.assume(adminExecutor != address(0)); vm.assume(afterSubmitDelay <= sanityCheckParams.maxAfterSubmitDelay); vm.assume(afterScheduleDelay <= sanityCheckParams.maxAfterScheduleDelay); vm.assume(afterSubmitDelay.toSeconds() + afterScheduleDelay.toSeconds() <= MAX_DURATION_VALUE); @@ -178,21 +186,19 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.prank(stranger); vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotGovernance.selector, [stranger])); - _timelock.submit(stranger, _adminExecutor, new ExternalCall[](0), ""); + _timelock.submit(_adminExecutor, new ExternalCall[](0)); assertEq(_timelock.getProposalsCount(), 0); } function test_submit_HappyPath() external { string memory testMetadata = "testMetadata"; - vm.expectEmit(true, true, true, true); + vm.expectEmit(); emit ExecutableProposals.ProposalSubmitted( - 1, _dualGovernance, _adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock)), testMetadata + 1, _adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock)) ); vm.prank(_dualGovernance); - _timelock.submit( - _dualGovernance, _adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock)), testMetadata - ); + _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_timelock.getProposalsCount(), 1); @@ -264,7 +270,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _activateEmergencyMode(); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [false])); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _timelock.execute(1); ITimelock.ProposalDetails memory proposal = _timelock.getProposalDetails(1); @@ -350,9 +356,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_setAfterSubmitDelay_RevertOn_CalledNotByAdminExecutor() external { Duration newAfterSubmitDelay = _defaultSanityCheckParams.maxAfterSubmitDelay + Durations.from(1 seconds); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, address(this)) - ); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, address(this))); _timelock.setAfterSubmitDelay(newAfterSubmitDelay); } @@ -422,9 +426,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_setAfterScheduleDelay_RevertOn_CalledNotByAdminExecutor() external { Duration newAfterScheduleDelay = _defaultSanityCheckParams.maxAfterScheduleDelay + Durations.from(1 seconds); - vm.expectRevert( - abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, address(this)) - ); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, address(this))); _timelock.setAfterScheduleDelay(newAfterScheduleDelay); } @@ -479,7 +481,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); _timelock.transferExecutorOwnership(_adminExecutor, makeAddr("newOwner")); } @@ -492,6 +494,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.expectEmit(address(_timelock)); emit TimelockState.GovernanceSet(newGovernance); + vm.expectEmit(address(_timelock)); + emit ExecutableProposals.ProposalsCancelledTill(0); + vm.recordLogs(); vm.prank(_adminExecutor); _timelock.setGovernance(newGovernance); @@ -500,7 +505,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 1); + assertEq(entries.length, 2); } function test_setGovernance_RevertOn_ZeroAddress() external { @@ -522,7 +527,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); _timelock.setGovernance(makeAddr("newGovernance")); } @@ -554,7 +559,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), true); vm.prank(_emergencyActivator); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [false])); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); _timelock.activateEmergencyMode(); assertEq(_isEmergencyStateActivated(), true); @@ -586,7 +591,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_emergencyExecute_RevertOn_ModeNotActive() external { vm.startPrank(_dualGovernance); - _timelock.submit(_dualGovernance, _adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_timelock.getProposalsCount(), 1); @@ -599,7 +604,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.isEmergencyModeActive(), false); vm.prank(_emergencyActivator); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); _timelock.emergencyExecute(1); } @@ -676,11 +681,11 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); _timelock.deactivateEmergencyMode(); vm.prank(_adminExecutor); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); _timelock.deactivateEmergencyMode(); } @@ -691,7 +696,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_isEmergencyStateActivated(), true); vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); _timelock.deactivateEmergencyMode(); } @@ -757,7 +762,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { address emergencyActivationCommitteeBefore = _timelock.getEmergencyActivationCommittee(); address emergencyExecutionCommitteeBefore = _timelock.getEmergencyExecutionCommittee(); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, [true])); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); vm.prank(_emergencyEnactor); _timelock.emergencyReset(); @@ -789,7 +794,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); vm.prank(stranger); _localTimelock.setEmergencyProtectionActivationCommittee(_emergencyActivator); @@ -814,7 +819,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor && stranger != _emergencyEnactor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); vm.prank(stranger); _localTimelock.setEmergencyProtectionExecutionCommittee(_emergencyEnactor); @@ -842,7 +847,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); vm.prank(stranger); _localTimelock.setEmergencyProtectionEndDate(_emergencyProtectionDuration.addTo(Timestamps.now())); @@ -873,7 +878,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.assume(stranger != _adminExecutor); EmergencyProtectedTimelock _localTimelock = _deployEmergencyProtectedTimelock(); - vm.expectRevert(abi.encodeWithSelector(EmergencyProtectedTimelock.CallerIsNotAdminExecutor.selector, stranger)); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); vm.prank(stranger); _localTimelock.setEmergencyModeDuration(_emergencyModeDuration); @@ -1038,8 +1043,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { vm.startPrank(_dualGovernance); ExternalCall[] memory executorCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); ExternalCall[] memory anotherExecutorCalls = _getMockTargetRegularStaffCalls(address(_anotherTargetMock)); - _timelock.submit(_dualGovernance, _adminExecutor, executorCalls, ""); - _timelock.submit(_dualGovernance, _adminExecutor, anotherExecutorCalls, ""); + _timelock.submit(_adminExecutor, executorCalls); + _timelock.submit(_adminExecutor, anotherExecutorCalls); (ITimelock.ProposalDetails memory submittedProposal, ExternalCall[] memory calls) = _timelock.getProposal(1); @@ -1196,7 +1201,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { function test_getProposalCalls() external { ExternalCall[] memory executorCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); vm.prank(_dualGovernance); - _timelock.submit(_dualGovernance, _adminExecutor, executorCalls, ""); + _timelock.submit(_adminExecutor, executorCalls); ExternalCall[] memory calls = _timelock.getProposalCalls(1); @@ -1207,6 +1212,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { } function testFuzz_getAdminExecutor(address executor) external { + vm.assume(executor != address(0)); Duration afterSubmitDelay = Durations.from(3 days); Duration afterScheduleDelay = Durations.from(1 days); @@ -1226,11 +1232,27 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(timelock.getAdminExecutor(), executor); } + function testFuzz_setAdminExecutor_HappyPath(address adminExecutor) external { + vm.assume(adminExecutor != _adminExecutor && adminExecutor != address(0)); + vm.prank(_adminExecutor); + _timelock.setAdminExecutor(adminExecutor); + + assertEq(_timelock.getAdminExecutor(), adminExecutor); + } + + function test_setAdminExecutor_RevertOn_NotAdminExecutor(address stranger) external { + vm.assume(stranger != _adminExecutor); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(TimelockState.CallerIsNotAdminExecutor.selector, stranger)); + _timelock.setAdminExecutor(address(0x123)); + } + // Utils function _submitProposal() internal { vm.prank(_dualGovernance); - _timelock.submit(_dualGovernance, _adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _timelock.submit(_adminExecutor, _getMockTargetRegularStaffCalls(address(_targetMock))); } function _scheduleProposal(uint256 proposalId) internal { diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index 2707003d..b292d95b 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -1,65 +1,1300 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {stdError} from "forge-std/StdError.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; -import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {ETHValues, sendTo} from "contracts/types/ETHValue.sol"; +import {ETHValues} from "contracts/types/ETHValue.sol"; import {SharesValues} from "contracts/types/SharesValue.sol"; import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; import {Escrow} from "contracts/Escrow.sol"; -import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; +import {EscrowState as EscrowStateLib, State as EscrowState} from "contracts/libraries/EscrowState.sol"; +import {WithdrawalsBatchesQueue} from "contracts/libraries/WithdrawalsBatchesQueue.sol"; +import {AssetsAccounting, UnstETHRecordStatus} from "contracts/libraries/AssetsAccounting.sol"; + +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; +import {IRageQuitEscrow} from "contracts/interfaces/IRageQuitEscrow.sol"; import {IStETH} from "contracts/interfaces/IStETH.sol"; import {IWstETH} from "contracts/interfaces/IWstETH.sol"; import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; -import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; -import {StETHMock} from "test/mocks/StETHMock.sol"; +import {StETHMock} from "scripts/lido-mocks/StETHMock.sol"; +import {WstETHMock} from "test/mocks/WstETHMock.sol"; import {WithdrawalQueueMock} from "test/mocks/WithdrawalQueueMock.sol"; import {UnitTest} from "test/utils/unit-test.sol"; +import {Random} from "test/utils/random.sol"; + +interface IEscrow is ISignallingEscrow, IRageQuitEscrow {} + +contract EscrowUnitTests is UnitTest { + Random.Context private _random; + address private _dualGovernance = makeAddr("dualGovernance"); + address private _vetoer = makeAddr("vetoer"); + + Escrow private _masterCopy; + Escrow private _escrow; + + StETHMock private _stETH; + WstETHMock private _wstETH; + + WithdrawalQueueMock private _withdrawalQueue; + + Duration private _minLockAssetDuration = Durations.from(1 days); + Duration private _maxMinAssetsLockDuration = Durations.from(100 days); + uint256 private stethAmount = 100 ether; + + function setUp() external { + _random = Random.create(block.timestamp); + _stETH = new StETHMock(); + _wstETH = new WstETHMock(_stETH); + _withdrawalQueue = new WithdrawalQueueMock(_stETH); + _withdrawalQueue.setMaxStETHWithdrawalAmount(1_000 ether); + _masterCopy = _createEscrow(100, _maxMinAssetsLockDuration); + _escrow = + _createInitializedEscrowProxy({minWithdrawalsBatchSize: 100, minAssetsLockDuration: _minLockAssetDuration}); + + vm.prank(address(_escrow)); + _stETH.approve(address(_withdrawalQueue), type(uint256).max); + + vm.startPrank(_vetoer); + _stETH.approve(address(_escrow), type(uint256).max); + _stETH.approve(address(_wstETH), type(uint256).max); + _wstETH.approve(address(_escrow), type(uint256).max); + vm.stopPrank(); + + _stETH.mint(_vetoer, stethAmount); + _wstETH.mint(_vetoer, stethAmount); + + vm.mockCall( + _dualGovernance, abi.encodeWithSelector(IDualGovernance.activateNextState.selector), abi.encode(true) + ); + _withdrawalQueue.setMinStETHWithdrawalAmount(100); + _withdrawalQueue.setMaxStETHWithdrawalAmount(1000 * 1e18); + + vm.label(address(_escrow), "Escrow"); + vm.label(address(_stETH), "StETHMock"); + vm.label(address(_wstETH), "WstETHMock"); + vm.label(address(_withdrawalQueue), "WithdrawalQueueMock"); + } + + /* */ + + // --- + // constructor() + // --- + + function testFuzz_constructor( + address steth, + address wsteth, + address withdrawalQueue, + address dualGovernance, + uint256 size, + Duration maxMinAssetsLockDuration + ) external { + Escrow instance = new Escrow( + IStETH(steth), + IWstETH(wsteth), + IWithdrawalQueue(withdrawalQueue), + IDualGovernance(dualGovernance), + size, + maxMinAssetsLockDuration + ); + + assertEq(address(instance.ST_ETH()), address(steth)); + assertEq(address(instance.WST_ETH()), address(wsteth)); + assertEq(address(instance.WITHDRAWAL_QUEUE()), address(withdrawalQueue)); + assertEq(address(instance.DUAL_GOVERNANCE()), address(dualGovernance)); + assertEq(instance.MIN_WITHDRAWALS_BATCH_SIZE(), size); + assertEq(instance.MAX_MIN_ASSETS_LOCK_DURATION(), maxMinAssetsLockDuration); + } + + // --- + // initialize() + // --- + + function test_initialize_HappyPath() external { + vm.expectEmit(); + emit EscrowStateLib.EscrowStateChanged(EscrowState.NotInitialized, EscrowState.SignallingEscrow); + vm.expectEmit(); + emit EscrowStateLib.MinAssetsLockDurationSet(Durations.ZERO); + + vm.expectCall(address(_stETH), abi.encodeCall(IERC20.approve, (address(_wstETH), type(uint256).max))); + vm.expectCall(address(_stETH), abi.encodeCall(IERC20.approve, (address(_withdrawalQueue), type(uint256).max))); + + _createInitializedEscrowProxy({minWithdrawalsBatchSize: 100, minAssetsLockDuration: Durations.ZERO}); + + assertEq(_escrow.MIN_WITHDRAWALS_BATCH_SIZE(), 100); + } + + function test_initialize_RevertOn_CalledNotViaProxy() external { + Escrow instance = _createEscrow(100, _maxMinAssetsLockDuration); + + vm.expectRevert(Escrow.NonProxyCallsForbidden.selector); + instance.initialize(Durations.ZERO); + } + + function testFuzz_initialize_RevertOn_CalledNotFromDualGovernance(address stranger) external { + vm.assume(stranger != _dualGovernance); + IEscrow instance = IEscrow(address(_createEscrowProxy(100))); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(Escrow.CallerIsNotDualGovernance.selector, stranger)); + instance.initialize(Durations.ZERO); + } + + // --- + // getEscrowState() + // --- + + function test_getEscrowState_HappyPath() external { + assertTrue(_masterCopy.getEscrowState() == EscrowState.NotInitialized); + assertTrue(_escrow.getEscrowState() == EscrowState.SignallingEscrow); + + _transitToRageQuit(); + assertTrue(_escrow.getEscrowState() == EscrowState.RageQuitEscrow); + } + + // --- + // lockStETH() + // --- + + function test_lockStETH_HappyPath() external { + uint256 amount = 1 ether; + + uint256 sharesAmount = _stETH.getSharesByPooledEth(amount); + uint256 vetoerBalanceBefore = _stETH.balanceOf(_vetoer); + uint256 escrowBalanceBefore = _stETH.balanceOf(address(_escrow)); + + vm.expectCall( + address(_stETH), + abi.encodeCall(IStETH.transferSharesFrom, (address(_vetoer), address(_escrow), sharesAmount)) + ); + vm.expectCall(address(_dualGovernance), abi.encodeCall(IDualGovernance.activateNextState, ()), 2); + + vm.prank(_vetoer); + uint256 lockedStETHShares = _escrow.lockStETH(amount); + + assertEq(lockedStETHShares, sharesAmount); + + uint256 vetoerBalanceAfter = _stETH.balanceOf(_vetoer); + uint256 escrowBalanceAfter = _stETH.balanceOf(address(_escrow)); + + assertEq(vetoerBalanceAfter, vetoerBalanceBefore - amount); + assertEq(escrowBalanceAfter, escrowBalanceBefore + amount); + + IEscrow.SignallingEscrowDetails memory signallingEscrowDetails = _escrow.getSignallingEscrowDetails(); + + assertEq(signallingEscrowDetails.totalStETHLockedShares.toUint256(), lockedStETHShares); + assertEq(signallingEscrowDetails.totalStETHClaimedETH.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHUnfinalizedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHFinalizedETH.toUint256(), 0); + + IEscrow.VetoerDetails memory state = _escrow.getVetoerDetails(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares.toUint256(), lockedStETHShares); + assertEq(state.unstETHLockedShares.toUint256(), 0); + assertEq(state.lastAssetsLockTimestamp, Timestamps.now()); + } + + function test_lockStETH_RevertOn_UnexpectedEscrowState() external { + _transitToRageQuit(); + + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow) + ); + vm.prank(_vetoer); + _escrow.lockStETH(1 ether); + + assertEq(_stETH.balanceOf(address(_escrow)), 0); + } + + // --- + // unlockStETH() + // --- + + function test_unlockStETH_HappyPath() external { + uint256 amount = 1 ether; + uint256 sharesAmount = _stETH.getSharesByPooledEth(amount); + + vm.startPrank(_vetoer); + _escrow.lockStETH(amount); + + uint256 vetoerBalanceBefore = _stETH.balanceOf(_vetoer); + uint256 escrowBalanceBefore = _stETH.balanceOf(address(_escrow)); + + _wait(_minLockAssetDuration.plusSeconds(1)); + + vm.expectCall(address(_stETH), abi.encodeCall(IStETH.transferShares, (address(_vetoer), sharesAmount))); + vm.expectCall(address(_dualGovernance), abi.encodeCall(IDualGovernance.activateNextState, ()), 2); + uint256 unlockedStETHShares = _escrow.unlockStETH(); + assertEq(unlockedStETHShares, sharesAmount); + + uint256 vetoerBalanceAfter = _stETH.balanceOf(_vetoer); + uint256 escrowBalanceAfter = _stETH.balanceOf(address(_escrow)); + + assertEq(vetoerBalanceAfter, vetoerBalanceBefore + amount); + assertEq(escrowBalanceAfter, escrowBalanceBefore - amount); + + IEscrow.SignallingEscrowDetails memory signallingEscrowDetails = _escrow.getSignallingEscrowDetails(); + + assertEq(signallingEscrowDetails.totalStETHLockedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalStETHClaimedETH.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHUnfinalizedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHFinalizedETH.toUint256(), 0); + + IEscrow.VetoerDetails memory state = _escrow.getVetoerDetails(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares.toUint256(), 0); + assertEq(state.unstETHLockedShares.toUint256(), 0); + } + + function test_unlockStETH_RevertOn_UnexpectedEscrowState() external { + vm.prank(_vetoer); + _escrow.lockStETH(1 ether); + + _transitToRageQuit(); + + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow) + ); + vm.prank(_vetoer); + _escrow.unlockStETH(); + } + + function test_unlockStETH_RevertOn_MinAssetsLockDurationNotPassed() external { + vm.startPrank(_vetoer); + _escrow.lockStETH(1 ether); + + uint256 lastLockTimestamp = block.timestamp; + + _wait(_minLockAssetDuration.minusSeconds(1)); + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.MinAssetsLockDurationNotPassed.selector, + Durations.from(lastLockTimestamp) + _minLockAssetDuration + ) + ); + _escrow.unlockStETH(); + } + + // --- + // lockWstETH() + // --- + + function test_lockWstETH_HappyPath() external { + uint256 amount = 1 ether; + + vm.startPrank(_vetoer); + _wstETH.wrap(amount); + + uint256 vetoerWStBalanceBefore = _wstETH.balanceOf(_vetoer); + uint256 escrowBalanceBefore = _stETH.balanceOf(address(_escrow)); + + vm.expectCall(address(_wstETH), abi.encodeCall(IERC20.transferFrom, (_vetoer, address(_escrow), amount))); + vm.expectCall(address(_dualGovernance), abi.encodeCall(IDualGovernance.activateNextState, ()), 2); + + uint256 lockedStETHShares = _escrow.lockWstETH(amount); + assertEq(lockedStETHShares, _stETH.getSharesByPooledEth(amount)); + + uint256 vetoerWStBalanceAfter = _wstETH.balanceOf(_vetoer); + uint256 escrowBalanceAfter = _stETH.balanceOf(address(_escrow)); + + assertEq(vetoerWStBalanceAfter, vetoerWStBalanceBefore - lockedStETHShares); + assertEq(escrowBalanceAfter, escrowBalanceBefore + lockedStETHShares); + + IEscrow.SignallingEscrowDetails memory signallingEscrowDetails = _escrow.getSignallingEscrowDetails(); + + assertEq(signallingEscrowDetails.totalStETHLockedShares.toUint256(), lockedStETHShares); + assertEq(signallingEscrowDetails.totalStETHClaimedETH.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHUnfinalizedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHFinalizedETH.toUint256(), 0); + + IEscrow.VetoerDetails memory state = _escrow.getVetoerDetails(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares.toUint256(), lockedStETHShares); + assertEq(state.unstETHLockedShares.toUint256(), 0); + assertEq(state.lastAssetsLockTimestamp, Timestamps.now()); + } + + function test_lockWstETH_RevertOn_UnexpectedEscrowState() external { + _transitToRageQuit(); + + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow) + ); + vm.prank(_vetoer); + _escrow.lockWstETH(1 ether); + } + + // --- + // unlockWstETH() + // --- + + function test_unlockWstETH_HappyPath() external { + uint256 amount = 1 ether; + + vm.startPrank(_vetoer); + _wstETH.wrap(amount); + + _escrow.lockWstETH(amount); + + _wait(_minLockAssetDuration.plusSeconds(1)); + + uint256 vetoerWStBalanceBefore = _wstETH.balanceOf(_vetoer); + uint256 escrowBalanceBefore = _stETH.balanceOf(address(_escrow)); + + vm.expectCall(address(_wstETH), abi.encodeCall(IWstETH.wrap, (amount))); + vm.expectCall(address(_wstETH), abi.encodeCall(IERC20.transfer, (_vetoer, amount))); + vm.expectCall(address(_dualGovernance), abi.encodeCall(IDualGovernance.activateNextState, ()), 2); + uint256 unlockedStETHShares = _escrow.unlockWstETH(); + + assertEq(unlockedStETHShares, _stETH.getPooledEthByShares(amount)); + + uint256 vetoerWStBalanceAfter = _wstETH.balanceOf(_vetoer); + uint256 escrowBalanceAfter = _stETH.balanceOf(address(_escrow)); + + assertEq(vetoerWStBalanceAfter, vetoerWStBalanceBefore + unlockedStETHShares); + assertEq(escrowBalanceAfter, escrowBalanceBefore - unlockedStETHShares); + + IEscrow.SignallingEscrowDetails memory signallingEscrowDetails = _escrow.getSignallingEscrowDetails(); + + assertEq(signallingEscrowDetails.totalStETHLockedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalStETHClaimedETH.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHUnfinalizedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHFinalizedETH.toUint256(), 0); + + IEscrow.VetoerDetails memory state = _escrow.getVetoerDetails(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares.toUint256(), 0); + assertEq(state.unstETHLockedShares.toUint256(), 0); + } + + function test_unlockWstETH_RevertOn_UnexpectedEscrowState() external { + uint256 amount = 1 ether; + + vm.startPrank(_vetoer); + _wstETH.wrap(amount); + _escrow.lockWstETH(amount); + vm.stopPrank(); + + _transitToRageQuit(); + + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow) + ); + vm.prank(_vetoer); + _escrow.unlockWstETH(); + } + + function test_unlockWstETH_RevertOn_MinAssetsLockDurationNotPassed() external { + uint256 amount = 1 ether; + + vm.startPrank(_vetoer); + _wstETH.wrap(amount); + _escrow.lockWstETH(amount); + + uint256 lastLockTimestamp = block.timestamp; + + _wait(_minLockAssetDuration.minusSeconds(1)); + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.MinAssetsLockDurationNotPassed.selector, + Durations.from(lastLockTimestamp) + _minLockAssetDuration + ) + ); + _escrow.unlockWstETH(); + } + + // --- + // lockUnstETH() + // --- + + function test_lockUnstETH_HappyPath() external { + uint256[] memory unstethIds = new uint256[](2); + unstethIds[0] = 1; + unstethIds[1] = 2; + + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = new IWithdrawalQueue.WithdrawalRequestStatus[](2); + statuses[0] = IWithdrawalQueue.WithdrawalRequestStatus(1 ether, 1 ether, _vetoer, block.timestamp, false, false); + statuses[1] = IWithdrawalQueue.WithdrawalRequestStatus(2 ether, 2 ether, _vetoer, block.timestamp, false, false); + + _withdrawalQueue.setWithdrawalRequestsStatuses(statuses); + + vm.expectCall( + address(_withdrawalQueue), + abi.encodeCall(IWithdrawalQueue.transferFrom, (_vetoer, address(_escrow), unstethIds[0])) + ); + vm.expectCall( + address(_withdrawalQueue), + abi.encodeCall(IWithdrawalQueue.transferFrom, (_vetoer, address(_escrow), unstethIds[1])) + ); + vm.expectCall(address(_dualGovernance), abi.encodeCall(IDualGovernance.activateNextState, ()), 2); + + vm.prank(_vetoer); + _escrow.lockUnstETH(unstethIds); + + IEscrow.SignallingEscrowDetails memory signallingEscrowDetails = _escrow.getSignallingEscrowDetails(); + + assertEq(signallingEscrowDetails.totalStETHLockedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalStETHClaimedETH.toUint256(), 0); + assertEq( + signallingEscrowDetails.totalUnstETHUnfinalizedShares.toUint256(), + statuses[0].amountOfShares + statuses[1].amountOfShares + ); + assertEq(signallingEscrowDetails.totalUnstETHFinalizedETH.toUint256(), 0); + + IEscrow.VetoerDetails memory state = _escrow.getVetoerDetails(_vetoer); + + assertEq(state.unstETHIdsCount, 2); + assertEq(state.stETHLockedShares.toUint256(), 0); + assertEq(state.unstETHLockedShares.toUint256(), statuses[0].amountOfShares + statuses[1].amountOfShares); + } + + function test_lockUnstETH_RevertOn_EmptyUnstETHIds() external { + uint256[] memory unstethIds = new uint256[](0); + + vm.expectRevert(abi.encodeWithSelector(Escrow.EmptyUnstETHIds.selector)); + _escrow.lockUnstETH(unstethIds); + } + + function test_lockUnstETH_RevertOn_UnexpectedEscrowState() external { + uint256[] memory unstethIds = new uint256[](1); + unstethIds[0] = 1; + + _transitToRageQuit(); + + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow) + ); + _escrow.lockUnstETH(unstethIds); + } + + // --- + // unlockUnstETH() + // --- + + function test_unlockUnstETH_HappyPath() external { + uint256[] memory unstETHAmounts = new uint256[](2); + unstETHAmounts[0] = 1 ether; + unstETHAmounts[1] = 2 ether; + + uint256[] memory unstethIds = _vetoerLockedUnstEth(unstETHAmounts); + + _wait(_minLockAssetDuration.plusSeconds(1)); + + vm.expectCall( + address(_withdrawalQueue), + abi.encodeCall(IWithdrawalQueue.transferFrom, (address(_escrow), _vetoer, unstethIds[0])) + ); + vm.expectCall( + address(_withdrawalQueue), + abi.encodeCall(IWithdrawalQueue.transferFrom, (address(_escrow), _vetoer, unstethIds[1])) + ); + vm.expectCall(address(_dualGovernance), abi.encodeCall(IDualGovernance.activateNextState, ()), 2); + + vm.prank(_vetoer); + _escrow.unlockUnstETH(unstethIds); + + IEscrow.SignallingEscrowDetails memory signallingEscrowDetails = _escrow.getSignallingEscrowDetails(); + + assertEq(signallingEscrowDetails.totalStETHLockedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalStETHClaimedETH.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHUnfinalizedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHFinalizedETH.toUint256(), 0); + + IEscrow.VetoerDetails memory state = _escrow.getVetoerDetails(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares.toUint256(), 0); + assertEq(state.unstETHLockedShares.toUint256(), 0); + } + + function test_unlockUnstETH_EmptyUnstETHIds() external { + uint256[] memory unstethIds = new uint256[](0); + + _wait(_minLockAssetDuration.plusSeconds(1)); + + vm.expectCall(address(_dualGovernance), abi.encodeCall(IDualGovernance.activateNextState, ()), 0); + + vm.expectRevert(Escrow.EmptyUnstETHIds.selector); + vm.prank(_vetoer); + _escrow.unlockUnstETH(unstethIds); + } + + function test_unlockUnstETH_RevertOn_MinAssetsLockDurationNotPassed() external { + uint256[] memory unstethIds = new uint256[](1); + + _wait(_minLockAssetDuration.minusSeconds(1)); + + // Exception. Due to no assets of holder registered in Escrow. + vm.expectRevert( + abi.encodeWithSelector(AssetsAccounting.MinAssetsLockDurationNotPassed.selector, _minLockAssetDuration) + ); + vm.prank(_vetoer); + _escrow.unlockUnstETH(unstethIds); + } + + function test_unlockUnstETH_RevertOn_UnexpectedEscrowState() external { + _transitToRageQuit(); + + uint256[] memory unstethIds = new uint256[](1); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow) + ); + vm.prank(_vetoer); + _escrow.unlockUnstETH(unstethIds); + } + + // --- + // markUnstETHFinalized() + // --- + + function test_markUnstETHFinalized_HappyPath() external { + uint256[] memory unstethIds = new uint256[](2); + uint256[] memory hints = new uint256[](2); + uint256[] memory responses = new uint256[](2); + + unstethIds[0] = 1; + unstethIds[1] = 1; + + hints[0] = 1; + hints[1] = 1; + + responses[0] = 1 ether; + responses[1] = 1 ether; + + _withdrawalQueue.setClaimableEtherResult(responses); + vm.expectCall( + address(_withdrawalQueue), abi.encodeCall(IWithdrawalQueue.getClaimableEther, (unstethIds, hints)) + ); + + _escrow.markUnstETHFinalized(unstethIds, hints); + } + + function test_markUnstETHFinalized_RevertOn_UnexpectedEscrowState() external { + _transitToRageQuit(); + + uint256[] memory unstethIds = new uint256[](0); + uint256[] memory hints = new uint256[](0); + + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.RageQuitEscrow) + ); + _escrow.markUnstETHFinalized(unstethIds, hints); + } + + // --- + // startRageQuit() + // --- + + function test_startRageQuit_HappyPath() external { + uint256 lri = Random.nextUint256(_random, 100500); + _withdrawalQueue.setLastRequestId(lri); + + vm.expectEmit(); + emit EscrowStateLib.RageQuitStarted(Durations.ZERO, Durations.ZERO); + vm.expectEmit(); + emit WithdrawalsBatchesQueue.WithdrawalsBatchesQueueOpened(lri); + + _transitToRageQuit(); + } + + function testFuzz_startRageQuit_RevertOn_CalledNotByDualGovernance(address stranger) external { + vm.assume(stranger != _dualGovernance); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(Escrow.CallerIsNotDualGovernance.selector, stranger)); + _escrow.startRageQuit(Durations.ZERO, Durations.ZERO); + } + + // --- + // requestNextWithdrawalsBatch() + // --- + + function test_requestNextWithdrawalsBatch_HappyPath() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.WithdrawalsBatchesQueueOpened(unstEthIds[0] - 1); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _stETH.mint(address(_escrow), stethAmount); + _withdrawalQueue.setMinStETHWithdrawalAmount(1); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.UnstETHIdsAdded(unstEthIds); + vm.expectEmit(); + emit WithdrawalsBatchesQueue.WithdrawalsBatchesQueueClosed(); + _escrow.requestNextWithdrawalsBatch(100); + } + + function test_requestNextWithdrawalsBatch_ReturnsEarlyAndClosesWithdrawalsBatchesQueue_When_EscrowHasZeroAmountOfStETH( + ) external { + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(new uint256[](0)); + + _ensureWithdrawalsBatchesQueueClosed(); + } + + function test_requestNextWithdrawalsBatch_RevertOn_UnexpectedEscrowState() external { + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); + _escrow.requestNextWithdrawalsBatch(1); + } + + function test_requestNextWithdrawalsBatch_RevertOn_InvalidBatchSize() external { + _transitToRageQuit(); + + uint256 batchSize = 1; + + vm.expectRevert(abi.encodeWithSelector(Escrow.InvalidBatchSize.selector, batchSize)); + _escrow.requestNextWithdrawalsBatch(batchSize); + } + + function test_requestNextWithdrawalsBatch_RevertOn_InvalidUnstETHIdsSequence() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.WithdrawalsBatchesQueueOpened(unstEthIds[0] - 1); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _stETH.mint(address(_escrow), stethAmount); + _withdrawalQueue.setRequestWithdrawalsTransferAmount( + stethAmount - 2 * _escrow.MIN_TRANSFERRABLE_ST_ETH_AMOUNT() + ); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.UnstETHIdsAdded(unstEthIds); + _escrow.requestNextWithdrawalsBatch(100); + + vm.expectRevert(WithdrawalsBatchesQueue.InvalidUnstETHIdsSequence.selector); + _escrow.requestNextWithdrawalsBatch(100); + } + + // --- + // claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) + // --- + + function test_claimNextWithdrawalsBatch_2_HappyPath() external { + IEscrow.SignallingEscrowDetails memory signallingEscrowDetails = _escrow.getSignallingEscrowDetails(); + + assertEq(signallingEscrowDetails.totalStETHLockedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalStETHClaimedETH.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHUnfinalizedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHFinalizedETH.toUint256(), 0); + + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + _withdrawalQueue.setLastCheckpointIndex(1); + _withdrawalQueue.setCheckpointHints(new uint256[](unstEthIds.length)); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + + _withdrawalQueue.setClaimableAmount(stethAmount); + vm.deal(address(_withdrawalQueue), stethAmount); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.UnstETHIdsClaimed(unstEthIds); + vm.expectEmit(); + emit AssetsAccounting.ETHClaimed(ETHValues.from(stethAmount)); + _escrow.claimNextWithdrawalsBatch(unstEthIds[0], new uint256[](unstEthIds.length)); + + signallingEscrowDetails = _escrow.getSignallingEscrowDetails(); + + assertEq(signallingEscrowDetails.totalStETHLockedShares.toUint256(), stethAmount); + assertEq(signallingEscrowDetails.totalStETHClaimedETH.toUint256(), stethAmount); + assertEq(signallingEscrowDetails.totalUnstETHUnfinalizedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHFinalizedETH.toUint256(), 0); + + IEscrow.VetoerDetails memory state = _escrow.getVetoerDetails(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares.toUint256(), stethAmount); + assertEq(state.unstETHLockedShares.toUint256(), 0); + } + + function test_claimNextWithdrawalsBatch_2_RevertOn_UnexpectedEscrowState() external { + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); + _escrow.claimNextWithdrawalsBatch(1, new uint256[](1)); + } + + function test_claimNextWithdrawalsBatch_2_RevertOn_ClaimingIsFinished() external { + _transitToRageQuit(); + + _escrow.requestNextWithdrawalsBatch(100); + _escrow.startRageQuitExtensionPeriod(); + + vm.expectRevert(EscrowStateLib.ClaimingIsFinished.selector); + _escrow.claimNextWithdrawalsBatch(1, new uint256[](1)); + } + + function test_claimNextWithdrawalsBatch_2_RevertOn_EmptyBatch() external { + _transitToRageQuit(); + + vm.expectRevert(WithdrawalsBatchesQueue.EmptyBatch.selector); + _escrow.claimNextWithdrawalsBatch(1, new uint256[](1)); + } + + function test_claimNextWithdrawalsBatch_2_RevertOn_InvalidFromUnstETHId() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + + _withdrawalQueue.setClaimableAmount(stethAmount); + vm.deal(address(_withdrawalQueue), stethAmount); + + vm.expectRevert(abi.encodeWithSelector(Escrow.InvalidFromUnstETHId.selector, unstEthIds[0] + 10)); + _escrow.claimNextWithdrawalsBatch(unstEthIds[0] + 10, new uint256[](1)); + } + + function test_claimNextWithdrawalsBatch_2_RevertOn_InvalidHintsLength() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + + _withdrawalQueue.setClaimableAmount(stethAmount); + vm.deal(address(_withdrawalQueue), stethAmount); + + vm.expectRevert(abi.encodeWithSelector(Escrow.InvalidHintsLength.selector, 10, 1)); + _escrow.claimNextWithdrawalsBatch(unstEthIds[0], new uint256[](10)); + } + + // --- + // claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) + // --- + + function test_claimNextWithdrawalsBatch_1_HappyPath() external { + IEscrow.SignallingEscrowDetails memory signallingEscrowDetails = _escrow.getSignallingEscrowDetails(); + + assertEq(signallingEscrowDetails.totalStETHLockedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalStETHClaimedETH.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHUnfinalizedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHFinalizedETH.toUint256(), 0); + + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + + IEscrow.VetoerDetails memory vetoerState = _escrow.getVetoerDetails(_vetoer); + + assertEq(vetoerState.unstETHIdsCount, 0); + assertEq(vetoerState.stETHLockedShares.toUint256(), stethAmount); + assertEq(vetoerState.unstETHLockedShares.toUint256(), 0); + + _claimStEthViaWQ(unstEthIds, stethAmount); + + signallingEscrowDetails = _escrow.getSignallingEscrowDetails(); + + assertEq(signallingEscrowDetails.totalStETHLockedShares.toUint256(), stethAmount); + assertEq(signallingEscrowDetails.totalStETHClaimedETH.toUint256(), stethAmount); + assertEq(signallingEscrowDetails.totalUnstETHUnfinalizedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHFinalizedETH.toUint256(), 0); + + vetoerState = _escrow.getVetoerDetails(_vetoer); + + assertEq(vetoerState.unstETHIdsCount, 0); + assertEq(vetoerState.stETHLockedShares.toUint256(), stethAmount); + assertEq(vetoerState.unstETHLockedShares.toUint256(), 0); + } + + function test_claimNextWithdrawalsBatch_1_RevertOn_UnexpectedEscrowState() external { + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); + _escrow.claimNextWithdrawalsBatch(1); + } + + function test_claimNextWithdrawalsBatch_1_RevertOn_ClaimingIsFinished() external { + _transitToRageQuit(); + + _escrow.requestNextWithdrawalsBatch(100); + _escrow.startRageQuitExtensionPeriod(); + + vm.expectRevert(EscrowStateLib.ClaimingIsFinished.selector); + _escrow.claimNextWithdrawalsBatch(1); + } + + function test_claimNextWithdrawalsBatch_1_RevertOn_EmptyBatch() external { + _transitToRageQuit(); + + vm.expectRevert(WithdrawalsBatchesQueue.EmptyBatch.selector); + _escrow.claimNextWithdrawalsBatch(1); + } + + function test_claimNextWithdrawalsBatch_1_RevertOn_InvalidHintsLength() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + _withdrawalQueue.setLastCheckpointIndex(1); + _withdrawalQueue.setCheckpointHints(new uint256[](unstEthIds.length + 10)); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + + vm.expectRevert(abi.encodeWithSelector(Escrow.InvalidHintsLength.selector, 11, 1)); + _escrow.claimNextWithdrawalsBatch(unstEthIds.length); + } + + // --- + // startRageQuitExtensionPeriod() + // --- + + function test_startRageQuitExtensionPeriod_HappyPath() external { + _transitToRageQuit(); + + _ensureWithdrawalsBatchesQueueClosed(); + + _ensureRageQuitExtensionPeriodStartedNow(); + } + + function test_startRageQuitExtensionPeriod_RevertOn_BatchesQueueIsNotClosed() external { + vm.expectRevert(Escrow.BatchesQueueIsNotClosed.selector); + _escrow.startRageQuitExtensionPeriod(); + } + + function test_startRageQuitExtensionPeriod_RevertOn_UnclaimedBatches() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + + vm.expectRevert(Escrow.UnclaimedBatches.selector); + _escrow.startRageQuitExtensionPeriod(); + } + + function test_startRageQuitExtensionPeriod_RevertOn_UnfinalizedUnstETHIds() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + _claimStEthViaWQ(unstEthIds, stethAmount); + + _withdrawalQueue.setLastFinalizedRequestId(0); + + vm.expectRevert(Escrow.UnfinalizedUnstETHIds.selector); + _escrow.startRageQuitExtensionPeriod(); + } + + // --- + // claimUnstETH() + // --- + + function test_claimUnstETH_HappyPath() external { + uint256[] memory unstEthAmounts = new uint256[](1); + unstEthAmounts[0] = 1 ether; + + uint256[] memory unstEthIds = _vetoerLockedUnstEth(unstEthAmounts); + uint256[] memory hints = _finalizeUnstEth(unstEthAmounts, unstEthIds); + + _transitToRageQuit(); + + _claimUnstEthFromEscrow(unstEthAmounts, unstEthIds, hints); + + IEscrow.SignallingEscrowDetails memory signallingEscrowDetails = _escrow.getSignallingEscrowDetails(); + + assertEq(signallingEscrowDetails.totalStETHLockedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalStETHClaimedETH.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHUnfinalizedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHFinalizedETH.toUint256(), unstEthAmounts[0]); + + IEscrow.VetoerDetails memory state = _escrow.getVetoerDetails(_vetoer); + + assertEq(state.unstETHIdsCount, 1); + assertEq(state.stETHLockedShares.toUint256(), 0); + assertEq(state.unstETHLockedShares.toUint256(), unstEthAmounts[0]); + } + + function test_claimUnstETH_RevertOn_UnexpectedEscrowState() external { + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); + _escrow.claimUnstETH(new uint256[](1), new uint256[](1)); + } + + function test_claimUnstETH_RevertOn_InvalidRequestId() external { + uint256[] memory unstETHIds = new uint256[](1); + unstETHIds[0] = _withdrawalQueue.REVERT_ON_ID(); + uint256[] memory hints = new uint256[](1); + + _transitToRageQuit(); + + vm.expectRevert(WithdrawalQueueMock.InvalidRequestId.selector); + _escrow.claimUnstETH(unstETHIds, hints); + } + + function test_claimUnstETH_RevertOn_ArraysLengthMismatch() external { + uint256[] memory unstETHIds = new uint256[](2); + uint256[] memory hints = new uint256[](1); + uint256[] memory responses = new uint256[](1); + responses[0] = 1 ether; + + _transitToRageQuit(); + + _withdrawalQueue.setClaimableEtherResult(responses); + + vm.expectRevert(WithdrawalQueueMock.ArraysLengthMismatch.selector); + _escrow.claimUnstETH(unstETHIds, hints); + } + + function test_claimUnstETH_RevertOn_InvalidUnstETHStatus() external { + uint256[] memory unstEthAmounts = new uint256[](1); + unstEthAmounts[0] = 1 ether; + + uint256[] memory unstEthIds = new uint256[](1); + unstEthIds[0] = Random.nextUint256(_random, 100500); + + uint256[] memory hints = _finalizeUnstEth(unstEthAmounts, unstEthIds); + + _transitToRageQuit(); + + _withdrawalQueue.setClaimableAmount(unstEthAmounts[0]); + _withdrawalQueue.setClaimableEtherResult(unstEthAmounts); + vm.deal(address(_withdrawalQueue), unstEthAmounts[0]); + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, unstEthIds[0], UnstETHRecordStatus.NotLocked + ) + ); + _escrow.claimUnstETH(unstEthIds, hints); + } + + // --- + // setMinAssetsLockDuration() + // --- + + function test_setMinAssetsLockDuration_HappyPath() external { + Duration newMinAssetsLockDuration = Durations.from(200); + vm.expectEmit(); + emit EscrowStateLib.MinAssetsLockDurationSet(newMinAssetsLockDuration); + vm.prank(_dualGovernance); + _escrow.setMinAssetsLockDuration(newMinAssetsLockDuration); + } + + function testFuzz_setMinAssetsLockDuration_RevertOn_CalledNotFromDualGovernance(address stranger) external { + vm.assume(stranger != _dualGovernance); + + Duration newMinAssetsLockDuration = Durations.from(200); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(Escrow.CallerIsNotDualGovernance.selector, stranger)); + _escrow.setMinAssetsLockDuration(newMinAssetsLockDuration); + } + + // --- + // withdrawETH() + // --- + + function test_withdrawETH_HappyPath() external { + uint256 balanceBefore = _vetoer.balance; + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + _claimStEthViaWQ(unstEthIds, stethAmount); + _ensureRageQuitExtensionPeriodStartedNow(); + assertTrue(_escrow.getRageQuitEscrowDetails().isRageQuitExtensionPeriodStarted); + + _wait(Durations.from(1)); + + vm.startPrank(_vetoer); + vm.expectEmit(); + emit AssetsAccounting.ETHWithdrawn(_vetoer, SharesValues.from(stethAmount), ETHValues.from(stethAmount)); + _escrow.withdrawETH(); + vm.stopPrank(); + + assertEq(_vetoer.balance, balanceBefore + stethAmount); + + IEscrow.SignallingEscrowDetails memory signallingEscrowDetails = _escrow.getSignallingEscrowDetails(); + + assertEq(signallingEscrowDetails.totalStETHLockedShares.toUint256(), stethAmount); + assertEq(signallingEscrowDetails.totalStETHClaimedETH.toUint256(), stethAmount); + assertEq(signallingEscrowDetails.totalUnstETHUnfinalizedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHFinalizedETH.toUint256(), 0); + + IEscrow.VetoerDetails memory state = _escrow.getVetoerDetails(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares.toUint256(), 0); + assertEq(state.unstETHLockedShares.toUint256(), 0); + } + + function test_withdrawETH_RevertOn_UnexpectedEscrowState() external { + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); + _escrow.withdrawETH(); + } + + function test_withdrawETH_RevertOn_RageQuitExtensionPeriodNotStarted() external { + _transitToRageQuit(); + + vm.expectRevert(EscrowStateLib.RageQuitExtensionPeriodNotStarted.selector); + _escrow.withdrawETH(); + } + + function test_withdrawETH_RevertOn_EthWithdrawalsDelayNotPassed() external { + uint256 balanceBefore = _vetoer.balance; + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + + _transitToRageQuit(Durations.from(1), Durations.from(2)); + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + _claimStEthViaWQ(unstEthIds, stethAmount); + _ensureRageQuitExtensionPeriodStartedNow(); + assertTrue(_escrow.getRageQuitEscrowDetails().isRageQuitExtensionPeriodStarted); + + vm.startPrank(_vetoer); + vm.expectRevert(EscrowStateLib.EthWithdrawalsDelayNotPassed.selector); + _escrow.withdrawETH(); + vm.stopPrank(); + + assertEq(_vetoer.balance, balanceBefore); + } + + function test_withdrawETH_RevertOn_InvalidSharesValue() external { + uint256 balanceBefore = _vetoer.balance; + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + address _vetoer2 = makeAddr("vetoer2"); + _stETH.mint(_vetoer2, 100 ether); + + vm.startPrank(_vetoer2); + _stETH.approve(address(_escrow), type(uint256).max); + _escrow.lockStETH(100 ether); + vm.stopPrank(); + + _vetoerLockedStEth(stethAmount); + + _wait(_minLockAssetDuration.plusSeconds(1)); + + _vetoerUnlockedStEth(stethAmount); + + _transitToRageQuit(); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + _claimStEthViaWQ(unstEthIds, stethAmount); + _ensureRageQuitExtensionPeriodStartedNow(); + assertTrue(_escrow.getRageQuitEscrowDetails().isRageQuitExtensionPeriodStarted); + + _wait(_minLockAssetDuration); + + vm.startPrank(_vetoer); + vm.expectRevert(abi.encodeWithSelector(AssetsAccounting.InvalidSharesValue.selector, SharesValues.ZERO)); + _escrow.withdrawETH(); + vm.stopPrank(); + + assertEq(_vetoer.balance, balanceBefore); + } + + // --- + // withdrawETH(uint256[] calldata unstETHIds) + // --- + + function test_withdrawETH_2_HappyPath() external { + uint256 balanceBefore = _vetoer.balance; + uint256[] memory unstEthAmounts = new uint256[](2); + unstEthAmounts[0] = 1 ether; + unstEthAmounts[1] = 10 ether; + + uint256[] memory unstEthIds = _vetoerLockedUnstEth(unstEthAmounts); + uint256[] memory hints = _finalizeUnstEth(unstEthAmounts, unstEthIds); + + _transitToRageQuit(); + + uint256 sum = _claimUnstEthFromEscrow(unstEthAmounts, unstEthIds, hints); + + _ensureWithdrawalsBatchesQueueClosed(); + + _ensureRageQuitExtensionPeriodStartedNow(); + assertTrue(_escrow.getRageQuitEscrowDetails().isRageQuitExtensionPeriodStarted); + + _wait(Durations.from(1)); + + vm.startPrank(_vetoer); + vm.expectEmit(); + emit AssetsAccounting.UnstETHWithdrawn(unstEthIds, ETHValues.from(sum)); + _escrow.withdrawETH(unstEthIds); + vm.stopPrank(); + + assertEq(_vetoer.balance, balanceBefore + sum); + + IEscrow.SignallingEscrowDetails memory signallingEscrowDetails = _escrow.getSignallingEscrowDetails(); + + assertEq(signallingEscrowDetails.totalStETHLockedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalStETHClaimedETH.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHUnfinalizedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHFinalizedETH.toUint256(), unstEthAmounts[0] + unstEthAmounts[1]); + + IEscrow.VetoerDetails memory state = _escrow.getVetoerDetails(_vetoer); + + assertEq(state.unstETHIdsCount, 2); + assertEq(state.stETHLockedShares.toUint256(), 0); + assertEq(state.unstETHLockedShares.toUint256(), unstEthAmounts[0] + unstEthAmounts[1]); + } + + function test_withdrawETH_2_RevertOn_EmptyUnstETHIds() external { + vm.expectRevert(Escrow.EmptyUnstETHIds.selector); + _escrow.withdrawETH(new uint256[](0)); + } + + function test_withdrawETH_2_RevertOn_UnexpectedEscrowState() external { + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); + _escrow.withdrawETH(new uint256[](1)); + } + + function test_withdrawETH_2_RevertOn_RageQuitExtensionPeriodNotStarted() external { + _transitToRageQuit(); + + vm.expectRevert(EscrowStateLib.RageQuitExtensionPeriodNotStarted.selector); + _escrow.withdrawETH(new uint256[](1)); + } + + function test_withdrawETH_2_RevertOn_EthWithdrawalsDelayNotPassed() external { + uint256 balanceBefore = _vetoer.balance; + uint256[] memory unstEthAmounts = new uint256[](2); + unstEthAmounts[0] = 1 ether; + unstEthAmounts[1] = 10 ether; + + uint256[] memory unstEthIds = _vetoerLockedUnstEth(unstEthAmounts); + uint256[] memory hints = _finalizeUnstEth(unstEthAmounts, unstEthIds); + + _transitToRageQuit(Durations.from(10), Durations.from(10)); + + _claimUnstEthFromEscrow(unstEthAmounts, unstEthIds, hints); + + _ensureWithdrawalsBatchesQueueClosed(); + + _ensureRageQuitExtensionPeriodStartedNow(); + assertTrue(_escrow.getRageQuitEscrowDetails().isRageQuitExtensionPeriodStarted); + + _wait(Durations.from(1)); -contract EscrowUnitTests is UnitTest { - address private _dualGovernance = makeAddr("dualGovernance"); - address private _vetoer = makeAddr("vetoer"); + vm.startPrank(_vetoer); + vm.expectRevert(EscrowStateLib.EthWithdrawalsDelayNotPassed.selector); + _escrow.withdrawETH(unstEthIds); + vm.stopPrank(); - Escrow private _masterCopy; - Escrow private _escrow; + assertEq(_vetoer.balance, balanceBefore); + } - StETHMock private _stETH; - IWstETH private _wstETH; + function test_withdrawETH_2_RevertOn_InvalidUnstETHStatus() external { + uint256 balanceBefore = _vetoer.balance; + uint256[] memory unstEthAmounts = new uint256[](2); + unstEthAmounts[0] = 1 ether; + unstEthAmounts[1] = 10 ether; - address private _withdrawalQueue; + uint256[] memory unstEthIds = _vetoerLockedUnstEth(unstEthAmounts); + _finalizeUnstEth(unstEthAmounts, unstEthIds); - Duration private _minLockAssetDuration = Durations.from(1 days); - uint256 private stethAmount = 100 ether; + _transitToRageQuit(); - function setUp() external { - _stETH = new StETHMock(); - _stETH.__setShareRate(1); - _wstETH = IWstETH(address(new ERC20Mock())); - _withdrawalQueue = address(new WithdrawalQueueMock()); - _masterCopy = - new Escrow(_stETH, _wstETH, WithdrawalQueueMock(_withdrawalQueue), IDualGovernance(_dualGovernance), 100); - _escrow = Escrow(payable(Clones.clone(address(_masterCopy)))); + _ensureWithdrawalsBatchesQueueClosed(); - vm.prank(_dualGovernance); - _escrow.initialize(_minLockAssetDuration); + _ensureRageQuitExtensionPeriodStartedNow(); + assertTrue(_escrow.getRageQuitEscrowDetails().isRageQuitExtensionPeriodStarted); + + _wait(Durations.from(1)); vm.startPrank(_vetoer); - ERC20Mock(address(_stETH)).approve(address(_escrow), type(uint256).max); - ERC20Mock(address(_wstETH)).approve(address(_escrow), type(uint256).max); + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, unstEthIds[0], UnstETHRecordStatus.Finalized + ) + ); + _escrow.withdrawETH(unstEthIds); vm.stopPrank(); - vm.mockCall( - _dualGovernance, abi.encodeWithSelector(IDualGovernance.activateNextState.selector), abi.encode(true) - ); + assertEq(_vetoer.balance, balanceBefore); + } + + // --- + // getLockedAssetsTotals() + // --- + + function test_getLockedAssetsTotals() external view { + IEscrow.SignallingEscrowDetails memory signallingEscrowDetails = _escrow.getSignallingEscrowDetails(); + + assertEq(signallingEscrowDetails.totalStETHLockedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalStETHClaimedETH.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHUnfinalizedShares.toUint256(), 0); + assertEq(signallingEscrowDetails.totalUnstETHFinalizedETH.toUint256(), 0); + } + + // --- + // getVetoerState() + // --- + + function test_getVetoerState() external { + _vetoerLockedStEth(stethAmount); + + IEscrow.VetoerDetails memory state = _escrow.getVetoerDetails(_vetoer); + + assertEq(state.unstETHIdsCount, 0); + assertEq(state.stETHLockedShares.toUint256(), _stETH.getSharesByPooledEth(stethAmount)); + assertEq(state.unstETHLockedShares.toUint256(), 0); + assertEq(state.lastAssetsLockTimestamp, Timestamps.now()); } + // --- // getVetoerUnstETHIds() // --- @@ -71,7 +1306,7 @@ contract EscrowUnitTests is UnitTest { assertEq(_escrow.getVetoerUnstETHIds(_vetoer).length, 0); - uint256[] memory unstEthIds = vetoerLockedUnstEth(unstEthAmounts); + uint256[] memory unstEthIds = _vetoerLockedUnstEth(unstEthAmounts); uint256[] memory vetoerUnstEthIds = _escrow.getVetoerUnstETHIds(_vetoer); @@ -99,32 +1334,105 @@ contract EscrowUnitTests is UnitTest { } // --- - // getUnclaimedUnstETHIdsCount() + // getLockedUnstETHDetails() // --- - function test_getUnclaimedUnstETHIdsCount_RevertOn_UnexpectedState_Signaling() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); - _escrow.getUnclaimedUnstETHIdsCount(); + function test_getLockedUnstETHDetails_HappyPath() external { + uint256[] memory unstEthAmounts = new uint256[](2); + unstEthAmounts[0] = 1 ether; + unstEthAmounts[1] = 10 ether; + + assertEq(_escrow.getVetoerUnstETHIds(_vetoer).length, 0); + + uint256[] memory unstEthIds = _vetoerLockedUnstEth(unstEthAmounts); + + IEscrow.LockedUnstETHDetails[] memory unstETHDetails = _escrow.getLockedUnstETHDetails(unstEthIds); + + assertEq(unstETHDetails.length, unstEthIds.length); + + assertEq(unstETHDetails[0].id, unstEthIds[0]); + assertEq(unstETHDetails[0].lockedBy, _vetoer); + assertTrue(unstETHDetails[0].status == UnstETHRecordStatus.Locked); + assertEq(unstETHDetails[0].shares.toUint256(), unstEthAmounts[0]); + assertEq(unstETHDetails[0].claimableAmount.toUint256(), 0); + + assertEq(unstETHDetails[1].id, unstEthIds[1]); + assertEq(unstETHDetails[1].lockedBy, _vetoer); + assertTrue(unstETHDetails[1].status == UnstETHRecordStatus.Locked); + assertEq(unstETHDetails[1].shares.toUint256(), unstEthAmounts[1]); + assertEq(unstETHDetails[1].claimableAmount.toUint256(), 0); } - function test_getUnclaimedUnstETHIdsCount_RevertOn_UnexpectedState_NotInitialized() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); - _masterCopy.getUnclaimedUnstETHIdsCount(); + function test_getLockedUnstETHDetails_RevertOn_unstETHNotLocked() external { + uint256 notLockedUnstETHId = 42; + + uint256[] memory notLockedUnstETHIds = new uint256[](1); + notLockedUnstETHIds[0] = notLockedUnstETHId; + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, notLockedUnstETHId, UnstETHRecordStatus.NotLocked + ) + ); + _escrow.getLockedUnstETHDetails(notLockedUnstETHIds); } // --- // getNextWithdrawalBatch() // --- - function test_getNextWithdrawalBatch_RevertOn_UnexpectedState_Signaling() external { + function test_getNextWithdrawalBatch() external { + uint256[] memory unstEthIds = _getUnstEthIdsFromWQ(); + + _vetoerLockedStEth(stethAmount); + + _transitToRageQuit(); + + uint256[] memory claimableUnstEthIds = _escrow.getNextWithdrawalBatch(100); + assertEq(claimableUnstEthIds.length, 0); + + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + _withdrawalQueue.setLastCheckpointIndex(1); + _withdrawalQueue.setCheckpointHints(new uint256[](unstEthIds.length)); + _ensureUnstEthAddedToWithdrawalsBatchesQueue(unstEthIds, stethAmount); + + claimableUnstEthIds = _escrow.getNextWithdrawalBatch(100); + assertEq(claimableUnstEthIds.length, unstEthIds.length); + assertEq(claimableUnstEthIds[0], unstEthIds[0]); + + _withdrawalQueue.setClaimableAmount(stethAmount); + vm.deal(address(_withdrawalQueue), stethAmount); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.UnstETHIdsClaimed(unstEthIds); + vm.expectEmit(); + emit AssetsAccounting.ETHClaimed(ETHValues.from(stethAmount)); + _escrow.claimNextWithdrawalsBatch(unstEthIds[0], new uint256[](unstEthIds.length)); + + claimableUnstEthIds = _escrow.getNextWithdrawalBatch(100); + assertEq(claimableUnstEthIds.length, 0); + } + + function test_getNextWithdrawalBatch_RevertOn_RageQuit_IsNotStarted() external { + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); + _escrow.getNextWithdrawalBatch(100); + } + + function test_getNextWithdrawalBatch_RevertOn_UnexpectedEscrowState_Signaling() external { uint256 batchLimit = 10; - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); _escrow.getNextWithdrawalBatch(batchLimit); } - function test_getNextWithdrawalBatch_RevertOn_UnexpectedState_NotInitialized() external { + function test_getNextWithdrawalBatch_RevertOn_UnexpectedEscrowState_NotInitialized() external { uint256 batchLimit = 10; - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized) + ); _masterCopy.getNextWithdrawalBatch(batchLimit); } @@ -132,56 +1440,464 @@ contract EscrowUnitTests is UnitTest { // isWithdrawalsBatchesClosed() // --- - function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedState_Signaling() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); + function test_isWithdrawalsBatchesClosed() external { + _transitToRageQuit(); + assertFalse(_escrow.isWithdrawalsBatchesClosed()); + + _withdrawalQueue.setRequestWithdrawalsResult(new uint256[](0)); + + _ensureWithdrawalsBatchesQueueClosed(); + + assertTrue(_escrow.isWithdrawalsBatchesClosed()); + } + + function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedEscrowState_Signaling() external { + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); _escrow.isWithdrawalsBatchesClosed(); } - function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedState_NotInitialized() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); + function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedEscrowState_NotInitialized() external { + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized) + ); _masterCopy.isWithdrawalsBatchesClosed(); } - function vetoerLockedUnstEth(uint256[] memory amounts) internal returns (uint256[] memory unstethIds) { - unstethIds = new uint256[](amounts.length); - WithdrawalRequestStatus[] memory statuses = new WithdrawalRequestStatus[](amounts.length); + // --- + // getUnclaimedUnstETHIdsCount() + // --- - for (uint256 i = 0; i < amounts.length; ++i) { - unstethIds[i] = i; - statuses[i] = WithdrawalRequestStatus(amounts[i], amounts[i], _vetoer, block.timestamp, false, false); - } + function test_getUnclaimedUnstETHIdsCount_RevertOn_UnexpectedEscrowState_Signaling() external { + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); + _escrow.getUnclaimedUnstETHIdsCount(); + } - vm.mockCall( - _withdrawalQueue, - abi.encodeWithSelector(IWithdrawalQueue.getWithdrawalStatus.selector, unstethIds), - abi.encode(statuses) + function test_getUnclaimedUnstETHIdsCount_RevertOn_UnexpectedEscrowState_NotInitialized() external { + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized) ); - vm.mockCall(_withdrawalQueue, abi.encodeWithSelector(IWithdrawalQueue.transferFrom.selector), abi.encode(true)); + _masterCopy.getUnclaimedUnstETHIdsCount(); + } - vm.startPrank(_vetoer); - _escrow.lockUnstETH(unstethIds); + // --- + // isRageQuitExtensionPeriodStarted() + // --- + + function test_isRageQuitExtensionPeriodStarted() external { + _transitToRageQuit(); + + assertFalse(_escrow.getRageQuitEscrowDetails().isRageQuitExtensionPeriodStarted); + + _ensureWithdrawalsBatchesQueueClosed(); + + _ensureRageQuitExtensionPeriodStartedNow(); + + assertTrue(_escrow.getRageQuitEscrowDetails().isRageQuitExtensionPeriodStarted); + + assertEq(_escrow.getRageQuitEscrowDetails().rageQuitExtensionPeriodStartedAt, Timestamps.now()); + } + + // --- + // getRageQuitExtensionPeriodStartedAt() + // --- + + function test_getRageQuitExtensionPeriodStartedAt_RevertOn_NotInitializedState() external { + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized) + ); + _masterCopy.getRageQuitEscrowDetails(); + } + + function test_getRageQuitExtensionPeriodStartedAt_RevertOn_SignallingState() external { + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); + _escrow.getRageQuitEscrowDetails().rageQuitExtensionPeriodStartedAt; + } + + function test_getRageQuitExtensionPeriodStartedAt() external { + _transitToRageQuit(); + Timestamp res = _escrow.getRageQuitEscrowDetails().rageQuitExtensionPeriodStartedAt; + assertEq(res.toSeconds(), Timestamps.ZERO.toSeconds()); + } + + // --- + // getRageQuitSupport() + // --- + + function test_getRageQuitSupport() external { + uint256 stEthLockedAmount = 80 ether + 100 wei; + uint256[] memory unstEthAmounts = new uint256[](2); + unstEthAmounts[0] = 1 ether; + unstEthAmounts[1] = 10 ether; + uint256[] memory finalizedUnstEthAmounts = new uint256[](1); + uint256[] memory finalizedUnstEthIds = new uint256[](1); + + PercentD16 actualSupport = + PercentsD16.fromFraction({numerator: stEthLockedAmount, denominator: _stETH.totalSupply()}); + + _vetoerLockedStEth(stEthLockedAmount); + + PercentD16 support = _escrow.getRageQuitSupport(); + assertEq(support, actualSupport); + assertEq(support, PercentsD16.fromBasisPoints(80_00)); + + // When some unstEth are locked in escrow => rage quit support changed + + uint256[] memory unstEthIds = _vetoerLockedUnstEth(unstEthAmounts); + + finalizedUnstEthAmounts[0] = unstEthAmounts[0]; + finalizedUnstEthIds[0] = unstEthIds[0]; + + _finalizeUnstEth(finalizedUnstEthAmounts, finalizedUnstEthIds); + + actualSupport = PercentsD16.fromFraction({ + numerator: stEthLockedAmount + unstEthAmounts[1] + unstEthAmounts[0], + denominator: _stETH.totalSupply() + unstEthAmounts[0] + }); + + support = _escrow.getRageQuitSupport(); + assertEq(support, actualSupport); + assertEq(support, PercentsD16.fromBasisPoints(91_00)); + } + + // --- + // isRageQuitFinalized() + // --- + + function test_isRageQuitFinalized() external { + _transitToRageQuit(); + + _ensureWithdrawalsBatchesQueueClosed(); + + _ensureRageQuitExtensionPeriodStartedNow(); + + _wait(Durations.from(1)); + + assertTrue(_escrow.isRageQuitFinalized()); + } + + // --- + // getRageQuitEscrowDetails() + // --- + + function test_getRageQuitEscrowDetails_RevertOn_UnexpectedEscrowState_Signaling() external { + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.SignallingEscrow) + ); + _escrow.getRageQuitEscrowDetails(); + } + + function test_getRageQuitEscrowDetails_RevertOn_UnexpectedEscrowState_NotInitialized() external { + vm.expectRevert( + abi.encodeWithSelector(EscrowStateLib.UnexpectedEscrowState.selector, EscrowState.NotInitialized) + ); + _masterCopy.getRageQuitEscrowDetails(); + } + + // --- + // receive() + // --- + + function test_receive() external { + vm.deal(address(_withdrawalQueue), 1 ether); + vm.deal(address(this), 1 ether); + + assertEq(address(_escrow).balance, 0); + + vm.startPrank(address(_withdrawalQueue)); + ETHValues.from(1 ether).sendTo(payable(address(_escrow))); vm.stopPrank(); + + assertEq(address(_escrow).balance, 1 ether); + + vm.expectRevert( + abi.encodeWithSelector(Escrow.InvalidETHSender.selector, address(this), address(_withdrawalQueue)) + ); + ETHValues.from(1 ether).sendTo(payable(address(_escrow))); + + assertEq(address(_escrow).balance, 1 ether); + assertEq(address(this).balance, 1 ether); + assertEq(address(_withdrawalQueue).balance, 0); + } + + // --- + // MIN_TRANSFERRABLE_ST_ETH_AMOUNT + // --- + + function test_MIN_TRANSFERRABLE_ST_ETH_AMOUNT() external { + assertEq(_escrow.MIN_TRANSFERRABLE_ST_ETH_AMOUNT(), 100); + } + + function test_MIN_TRANSFERRABLE_ST_ETH_AMOUNT_gt_minWithdrawableStETHAmountWei_HappyPath() external { + uint256 amountToLock = 100; + + uint256 minWithdrawableStETHAmountWei = 99; + assertEq(_escrow.MIN_TRANSFERRABLE_ST_ETH_AMOUNT(), 100); + _withdrawalQueue.setMinStETHWithdrawalAmount(minWithdrawableStETHAmountWei); + + // Lock stETH + _stETH.mint(_vetoer, amountToLock); + vm.prank(_vetoer); + _escrow.lockStETH(amountToLock); + assertEq(_stETH.balanceOf(address(_escrow)), amountToLock); + + // Request withdrawal + vm.prank(_dualGovernance); + _escrow.startRageQuit(Durations.ZERO, Durations.ZERO); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 0); + assertEq(_escrow.isWithdrawalsBatchesClosed(), false); + + uint256[] memory unstEthIds = new uint256[](1); + unstEthIds[0] = 1; + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + _escrow.requestNextWithdrawalsBatch(100); + + assertEq(_stETH.balanceOf(address(_escrow)), 0); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 1); + assertEq(_escrow.isWithdrawalsBatchesClosed(), true); + } + + function test_MIN_TRANSFERRABLE_ST_ETH_AMOUNT_gt_minWithdrawableStETHAmountWei_HappyPath_closes_queue() external { + uint256 amountToLock = 99; + + uint256 minWithdrawableStETHAmountWei = 99; + assertEq(_escrow.MIN_TRANSFERRABLE_ST_ETH_AMOUNT(), 100); + _withdrawalQueue.setMinStETHWithdrawalAmount(minWithdrawableStETHAmountWei); + + // Lock stETH + _stETH.mint(_vetoer, amountToLock); + vm.prank(_vetoer); + _escrow.lockStETH(amountToLock); + assertEq(_stETH.balanceOf(address(_escrow)), amountToLock); + + // Request withdrawal + vm.prank(_dualGovernance); + _escrow.startRageQuit(Durations.ZERO, Durations.ZERO); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 0); + assertEq(_escrow.isWithdrawalsBatchesClosed(), false); + + uint256[] memory unstEthIds = new uint256[](0); + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + _escrow.requestNextWithdrawalsBatch(100); + + assertEq(_stETH.balanceOf(address(_escrow)), 99); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 0); + assertEq(_escrow.isWithdrawalsBatchesClosed(), true); + } + + function test_MIN_TRANSFERRABLE_ST_ETH_AMOUNT_lt_minWithdrawableStETHAmountWei_HappyPath() external { + uint256 amountToLock = 101; + + uint256 minWithdrawableStETHAmountWei = 101; + assertEq(_escrow.MIN_TRANSFERRABLE_ST_ETH_AMOUNT(), 100); + _withdrawalQueue.setMinStETHWithdrawalAmount(minWithdrawableStETHAmountWei); + + // Lock stETH + _stETH.mint(_vetoer, amountToLock); + vm.prank(_vetoer); + _escrow.lockStETH(amountToLock); + assertEq(_stETH.balanceOf(address(_escrow)), amountToLock); + + // Request withdrawal + vm.prank(_dualGovernance); + _escrow.startRageQuit(Durations.ZERO, Durations.ZERO); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 0); + assertEq(_escrow.isWithdrawalsBatchesClosed(), false); + + uint256[] memory unstEthIds = new uint256[](1); + unstEthIds[0] = 1; + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + _escrow.requestNextWithdrawalsBatch(100); + + assertEq(_stETH.balanceOf(address(_escrow)), 0); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 1); + assertEq(_escrow.isWithdrawalsBatchesClosed(), true); + } + + function test_MIN_TRANSFERRABLE_ST_ETH_AMOUNT_lt_minWithdrawableStETHAmountWei_HappyPath_closes_queue() external { + uint256 amountToLock = 100; + + uint256 minWithdrawableStETHAmountWei = 101; + assertEq(_escrow.MIN_TRANSFERRABLE_ST_ETH_AMOUNT(), 100); + _withdrawalQueue.setMinStETHWithdrawalAmount(minWithdrawableStETHAmountWei); + + // Lock stETH + _stETH.mint(_vetoer, amountToLock); + vm.prank(_vetoer); + _escrow.lockStETH(amountToLock); + assertEq(_stETH.balanceOf(address(_escrow)), amountToLock); + + // Request withdrawal + vm.prank(_dualGovernance); + _escrow.startRageQuit(Durations.ZERO, Durations.ZERO); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 0); + assertEq(_escrow.isWithdrawalsBatchesClosed(), false); + + uint256[] memory unstEthIds = new uint256[](1); + unstEthIds[0] = 1; + _withdrawalQueue.setRequestWithdrawalsResult(unstEthIds); + _escrow.requestNextWithdrawalsBatch(100); + + assertEq(_stETH.balanceOf(address(_escrow)), 100); + assertEq(_escrow.getNextWithdrawalBatch(100).length, 0); + assertEq(_escrow.isWithdrawalsBatchesClosed(), true); } - function createEscrow(uint256 size) internal returns (Escrow) { - return - new Escrow(_stETH, _wstETH, WithdrawalQueueMock(_withdrawalQueue), IDualGovernance(_dualGovernance), size); + // --- + // helper methods + // --- + + function _createEscrow(uint256 size, Duration maxMinAssetsLockDuration) internal returns (Escrow) { + return new Escrow( + _stETH, _wstETH, _withdrawalQueue, IDualGovernance(_dualGovernance), size, maxMinAssetsLockDuration + ); } - function createEscrowProxy(uint256 minWithdrawalsBatchSize) internal returns (Escrow) { - Escrow masterCopy = createEscrow(minWithdrawalsBatchSize); + function _createEscrowProxy(uint256 minWithdrawalsBatchSize) internal returns (Escrow) { + Escrow masterCopy = _createEscrow(minWithdrawalsBatchSize, _maxMinAssetsLockDuration); return Escrow(payable(Clones.clone(address(masterCopy)))); } - function createInitializedEscrowProxy( + function _createInitializedEscrowProxy( uint256 minWithdrawalsBatchSize, Duration minAssetsLockDuration ) internal returns (Escrow) { - Escrow instance = createEscrowProxy(minWithdrawalsBatchSize); + Escrow instance = _createEscrowProxy(minWithdrawalsBatchSize); - vm.startPrank(_dualGovernance); + vm.prank(_dualGovernance); instance.initialize(minAssetsLockDuration); - vm.stopPrank(); return instance; } + + function _transitToRageQuit() internal { + vm.prank(_dualGovernance); + _escrow.startRageQuit(Durations.ZERO, Durations.ZERO); + } + + function _transitToRageQuit(Duration rqExtensionPeriod, Duration rqEthWithdrawalsDelay) internal { + vm.prank(_dualGovernance); + _escrow.startRageQuit(rqExtensionPeriod, rqEthWithdrawalsDelay); + } + + function _vetoerLockedStEth(uint256 amount) internal { + vm.prank(_vetoer); + _escrow.lockStETH(amount); + } + + function _vetoerLockedUnstEth(uint256[] memory amounts) internal returns (uint256[] memory unstethIds) { + unstethIds = new uint256[](amounts.length); + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](amounts.length); + + for (uint256 i = 0; i < amounts.length; ++i) { + unstethIds[i] = i; + statuses[i] = + IWithdrawalQueue.WithdrawalRequestStatus(amounts[i], amounts[i], _vetoer, block.timestamp, false, false); + } + + _withdrawalQueue.setWithdrawalRequestsStatuses(statuses); + + vm.prank(_vetoer); + _escrow.lockUnstETH(unstethIds); + } + + function _finalizeUnstEth( + uint256[] memory amounts, + uint256[] memory finalizedUnstEthIds + ) internal returns (uint256[] memory hints) { + assertEq(amounts.length, finalizedUnstEthIds.length); + + hints = new uint256[](amounts.length); + uint256[] memory responses = new uint256[](amounts.length); + + for (uint256 i = 0; i < amounts.length; ++i) { + hints[i] = i; + responses[i] = amounts[i]; + } + + _withdrawalQueue.setClaimableEtherResult(responses); + + _escrow.markUnstETHFinalized(finalizedUnstEthIds, hints); + + for (uint256 i = 0; i < amounts.length; ++i) { + _stETH.burn(_vetoer, amounts[i]); + } + } + + function _claimUnstEthFromEscrow( + uint256[] memory amounts, + uint256[] memory unstEthIds, + uint256[] memory hints + ) internal returns (uint256 sum) { + assertEq(amounts.length, unstEthIds.length); + assertEq(amounts.length, hints.length); + + sum = 0; + for (uint256 i = 0; i < amounts.length; ++i) { + sum += amounts[i]; + } + + _withdrawalQueue.setClaimableAmount(sum); + _withdrawalQueue.setClaimableEtherResult(amounts); + vm.deal(address(_withdrawalQueue), sum); + + vm.expectEmit(); + emit AssetsAccounting.UnstETHClaimed(unstEthIds, ETHValues.from(sum)); + _escrow.claimUnstETH(unstEthIds, hints); + } + + function _claimStEthViaWQ(uint256[] memory unstEthIds, uint256 amount) internal { + _withdrawalQueue.setClaimableAmount(amount); + _withdrawalQueue.setLastCheckpointIndex(1); + _withdrawalQueue.setCheckpointHints(new uint256[](unstEthIds.length)); + vm.deal(address(_withdrawalQueue), amount); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.UnstETHIdsClaimed(unstEthIds); + vm.expectEmit(); + emit AssetsAccounting.ETHClaimed(ETHValues.from(amount)); + _escrow.claimNextWithdrawalsBatch(unstEthIds.length); + } + + function _vetoerUnlockedStEth(uint256 amount) internal { + vm.startPrank(_vetoer); + vm.expectEmit(); + emit AssetsAccounting.StETHSharesUnlocked(_vetoer, SharesValues.from(_stETH.getSharesByPooledEth(amount))); + _escrow.unlockStETH(); + vm.stopPrank(); + } + + function _ensureUnstEthAddedToWithdrawalsBatchesQueue(uint256[] memory unstEthIds, uint256 ethAmount) internal { + vm.expectEmit(); + emit WithdrawalsBatchesQueue.UnstETHIdsAdded(unstEthIds); + _escrow.requestNextWithdrawalsBatch(100); + + assertEq(_stETH.balanceOf(address(_escrow)), 0); + } + + function _ensureWithdrawalsBatchesQueueClosed() internal { + vm.expectEmit(); + emit WithdrawalsBatchesQueue.WithdrawalsBatchesQueueClosed(); + _escrow.requestNextWithdrawalsBatch(100); + } + + function _ensureRageQuitExtensionPeriodStartedNow() internal { + vm.expectEmit(); + emit EscrowStateLib.RageQuitExtensionPeriodStarted(Timestamps.now()); + _escrow.startRageQuitExtensionPeriod(); + } + + function _getUnstEthIdsFromWQ() internal returns (uint256[] memory unstEthIds) { + uint256 lri = Random.nextUint256(_random, 100500); + _withdrawalQueue.setLastRequestId(lri); + _withdrawalQueue.setLastFinalizedRequestId(lri + 1); + + unstEthIds = new uint256[](1); + unstEthIds[0] = lri + 1; + } } diff --git a/test/unit/Executor.t.sol b/test/unit/Executor.t.sol new file mode 100644 index 00000000..086f7515 --- /dev/null +++ b/test/unit/Executor.t.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {Executor} from "contracts/Executor.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; + +contract ExecutorTarget { + event NonPayableMethodCalled(address caller); + event PayableMethodCalled(address caller, uint256 value); + + function nonPayableMethod() external { + emit NonPayableMethodCalled(msg.sender); + } + + function payableMethod() external payable { + emit PayableMethodCalled(msg.sender, msg.value); + } +} + +contract ExecutorUnitTests is UnitTest { + address internal _owner = makeAddr("OWNER"); + + Executor internal _executor; + ExecutorTarget internal _target; + + function setUp() external { + _target = new ExecutorTarget(); + _executor = new Executor(_owner); + } + + function test_constructor_HappyPath() external { + assertEq(_executor.owner(), _owner); + } + + function test_constructor_RevertOn_ZeroOwnerAddress() external { + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableInvalidOwner.selector, address(0))); + new Executor(address(0)); + } + + // --- + // execute() + // --- + + function test_execute_HappyPath_NonPayableMethod_ZeroValue() external { + vm.expectEmit(); + emit ExecutorTarget.NonPayableMethodCalled(address(_executor)); + + vm.expectEmit(); + emit Executor.Executed(address(_target), 0, abi.encodeCall(_target.nonPayableMethod, ()), new bytes(0)); + + vm.prank(_owner); + _executor.execute({target: address(_target), value: 0, payload: abi.encodeCall(_target.nonPayableMethod, ())}); + } + + function test_execute_HappyPath_PayableMethod_ZeroValue() external { + vm.expectEmit(); + emit ExecutorTarget.PayableMethodCalled(address(_executor), 0); + + vm.expectEmit(); + emit Executor.Executed(address(_target), 0, abi.encodeCall(_target.payableMethod, ()), new bytes(0)); + + vm.prank(_owner); + _executor.execute({target: address(_target), value: 0, payload: abi.encodeCall(_target.payableMethod, ())}); + } + + function test_execute_HappyPath_PayableMethod_NonZeroValue() external { + uint256 valueAmount = 1 ether; + + vm.deal(address(_executor), valueAmount); + + assertEq(address(_target).balance, 0); + assertEq(address(_executor).balance, 1 ether); + + vm.expectEmit(); + emit ExecutorTarget.PayableMethodCalled(address(_executor), valueAmount); + + vm.expectEmit(); + emit Executor.Executed(address(_target), valueAmount, abi.encodeCall(_target.payableMethod, ()), new bytes(0)); + + vm.prank(_owner); + _executor.execute({ + target: address(_target), + value: valueAmount, + payload: abi.encodeCall(_target.payableMethod, ()) + }); + + assertEq(address(_target).balance, valueAmount); + assertEq(address(_executor).balance, 0); + } + + function test_execute_RevertOn_NonPayableMethod_NonZeroValue() external { + uint256 callValue = 1 ether; + + vm.deal(address(_executor), callValue); + assertEq(address(_executor).balance, callValue); + + vm.prank(_owner); + vm.expectRevert(Address.FailedInnerCall.selector); + _executor.execute({ + target: address(_target), + value: callValue, + payload: abi.encodeCall(_target.nonPayableMethod, ()) + }); + } + // --- + // receive() + // --- + + function test_receive_HappyPath() external { + uint256 sendValue = 1 ether; + + vm.deal(address(this), sendValue); + + assertEq(address(this).balance, sendValue); + assertEq(address(_executor).balance, 0); + + Address.sendValue(payable(address(_executor)), sendValue); + + assertEq(address(this).balance, 0); + assertEq(address(_executor).balance, sendValue); + } + + function test_receive_HappyPath_UsingSend() external { + uint256 sendValue = 1 ether; + + vm.deal(address(this), sendValue); + + assertEq(address(this).balance, sendValue); + assertEq(address(_executor).balance, 0); + + bool success = payable(address(_executor)).send(sendValue); + + assertTrue(success); + assertEq(address(this).balance, 0); + assertEq(address(_executor).balance, sendValue); + } + + // --- + // Custom call + // --- + + function test_RevertOnInvalidMethodCall() external { + vm.prank(_owner); + (bool success, bytes memory returndata) = + address(_executor).call{value: 1 ether}(abi.encodeWithSelector(bytes4(0xdeadbeaf), 42)); + + assertFalse(success); + assertEq(returndata, new bytes(0)); + } +} diff --git a/test/unit/TimelockedGovernance.t.sol b/test/unit/TimelockedGovernance.t.sol index e961f05d..10a6f35e 100644 --- a/test/unit/TimelockedGovernance.t.sol +++ b/test/unit/TimelockedGovernance.t.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.26; import {ITimelock} from "contracts/interfaces/ITimelock.sol"; +import {IGovernance} from "contracts/interfaces/IGovernance.sol"; import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; import {UnitTest} from "test/utils/unit-test.sol"; @@ -43,8 +44,14 @@ contract TimelockedGovernanceUnitTests is UnitTest { function test_submit_proposal() external { assertEq(_timelock.getSubmittedProposals().length, 0); + uint256 expectedProposalId = 1; + string memory metadata = "proposal description"; + + vm.expectEmit(); + emit IGovernance.ProposalSubmitted(_governance, expectedProposalId, metadata); + vm.prank(_governance); - _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1)), ""); + _timelockedGovernance.submitProposal(_getMockTargetRegularStaffCalls(address(0x1)), metadata); assertEq(_timelock.getSubmittedProposals().length, 1); } @@ -82,6 +89,7 @@ contract TimelockedGovernanceUnitTests is UnitTest { _timelock.setSchedule(1); _timelockedGovernance.scheduleProposal(1); + _timelock.setExecutable(1); _timelock.execute(1); assertEq(_timelock.getExecutedProposals().length, 1); diff --git a/test/unit/committees/EmergencyActivationCommittee.t.sol b/test/unit/committees/EmergencyActivationCommittee.t.sol deleted file mode 100644 index 31c9053a..00000000 --- a/test/unit/committees/EmergencyActivationCommittee.t.sol +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; -import {HashConsensus} from "contracts/committees/HashConsensus.sol"; -import {Durations} from "contracts/types/Duration.sol"; -import {Timestamp} from "contracts/types/Timestamp.sol"; -import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; - -import {TargetMock} from "test/utils/target-mock.sol"; -import {UnitTest} from "test/utils/unit-test.sol"; - -contract EmergencyActivationCommitteeUnitTest is UnitTest { - bytes32 private constant _EMERGENCY_ACTIVATION_HASH = keccak256("EMERGENCY_ACTIVATE"); - - EmergencyActivationCommittee internal emergencyActivationCommittee; - uint256 internal quorum = 2; - address internal owner = makeAddr("owner"); - address[] internal committeeMembers = [address(0x1), address(0x2), address(0x3)]; - address internal emergencyProtectedTimelock; - - function setUp() external { - emergencyProtectedTimelock = address(new TargetMock()); - emergencyActivationCommittee = - new EmergencyActivationCommittee(owner, committeeMembers, quorum, emergencyProtectedTimelock); - } - - function testFuzz_constructor_HappyPath( - address _owner, - uint256 _quorum, - address _emergencyProtectedTimelock - ) external { - vm.assume(_owner != address(0)); - vm.assume(_quorum > 0 && _quorum <= committeeMembers.length); - EmergencyActivationCommittee localCommittee = - new EmergencyActivationCommittee(_owner, committeeMembers, _quorum, _emergencyProtectedTimelock); - assertEq(localCommittee.EMERGENCY_PROTECTED_TIMELOCK(), _emergencyProtectedTimelock); - } - - function test_approveActivateEmergencyMode_HappyPath() external { - vm.prank(committeeMembers[0]); - emergencyActivationCommittee.approveActivateEmergencyMode(); - - (uint256 partialSupport,,,) = emergencyActivationCommittee.getActivateEmergencyModeState(); - assertEq(partialSupport, 1); - - vm.prank(committeeMembers[1]); - emergencyActivationCommittee.approveActivateEmergencyMode(); - - (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = - emergencyActivationCommittee.getActivateEmergencyModeState(); - assertEq(support, quorum); - assertEq(executionQuorum, quorum); - assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); - assertEq(isExecuted, false); - } - - function testFuzz_approveActivateEmergencyMode_RevertOn_NotMember(address caller) external { - vm.assume(caller != committeeMembers[0] && caller != committeeMembers[1] && caller != committeeMembers[2]); - vm.prank(caller); - vm.expectRevert(abi.encodeWithSelector(HashConsensus.CallerIsNotMember.selector, caller)); - emergencyActivationCommittee.approveActivateEmergencyMode(); - } - - function test_executeActivateEmergencyMode_HappyPath() external { - vm.prank(committeeMembers[0]); - emergencyActivationCommittee.approveActivateEmergencyMode(); - vm.prank(committeeMembers[1]); - emergencyActivationCommittee.approveActivateEmergencyMode(); - - vm.prank(committeeMembers[2]); - vm.expectCall( - emergencyProtectedTimelock, - abi.encodeWithSelector(IEmergencyProtectedTimelock.activateEmergencyMode.selector) - ); - emergencyActivationCommittee.executeActivateEmergencyMode(); - - (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = - emergencyActivationCommittee.getActivateEmergencyModeState(); - assertEq(support, 2); - assertEq(executionQuorum, 2); - assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); - assertEq(isExecuted, true); - } - - function test_executeActivateEmergencyMode_RevertOn_QuorumNotReached() external { - vm.prank(committeeMembers[0]); - emergencyActivationCommittee.approveActivateEmergencyMode(); - - vm.prank(committeeMembers[2]); - vm.expectRevert(abi.encodeWithSelector(HashConsensus.HashIsNotScheduled.selector, _EMERGENCY_ACTIVATION_HASH)); - emergencyActivationCommittee.executeActivateEmergencyMode(); - } - - function test_getActivateEmergencyModeState_HappyPath() external { - (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = - emergencyActivationCommittee.getActivateEmergencyModeState(); - assertEq(support, 0); - assertEq(executionQuorum, 2); - assertEq(quorumAt, Timestamp.wrap(0)); - - vm.prank(committeeMembers[0]); - emergencyActivationCommittee.approveActivateEmergencyMode(); - - (support, executionQuorum, quorumAt, isExecuted) = emergencyActivationCommittee.getActivateEmergencyModeState(); - assertEq(support, 1); - assertEq(executionQuorum, 2); - assertEq(quorumAt, Timestamp.wrap(0)); - assertEq(isExecuted, false); - - vm.prank(committeeMembers[1]); - emergencyActivationCommittee.approveActivateEmergencyMode(); - - (support, executionQuorum, quorumAt, isExecuted) = emergencyActivationCommittee.getActivateEmergencyModeState(); - Timestamp quorumAtExpected = Timestamp.wrap(uint40(block.timestamp)); - assertEq(support, 2); - assertEq(executionQuorum, 2); - assertEq(quorumAt, quorumAtExpected); - assertEq(isExecuted, false); - - vm.prank(committeeMembers[2]); - vm.expectCall( - emergencyProtectedTimelock, - abi.encodeWithSelector(IEmergencyProtectedTimelock.activateEmergencyMode.selector) - ); - emergencyActivationCommittee.executeActivateEmergencyMode(); - - (support, executionQuorum, quorumAt, isExecuted) = emergencyActivationCommittee.getActivateEmergencyModeState(); - assertEq(support, 2); - assertEq(executionQuorum, 2); - assertEq(quorumAt, quorumAtExpected); - assertEq(isExecuted, true); - } -} diff --git a/test/unit/committees/EmergencyExecutionCommittee.t.sol b/test/unit/committees/EmergencyExecutionCommittee.t.sol deleted file mode 100644 index 4b0325c0..00000000 --- a/test/unit/committees/EmergencyExecutionCommittee.t.sol +++ /dev/null @@ -1,240 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {EmergencyExecutionCommittee, ProposalType} from "contracts/committees/EmergencyExecutionCommittee.sol"; -import {HashConsensus} from "contracts/committees/HashConsensus.sol"; -import {Durations} from "contracts/types/Duration.sol"; -import {Timestamp} from "contracts/types/Timestamp.sol"; -import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; - -import {TargetMock} from "test/utils/target-mock.sol"; -import {UnitTest} from "test/utils/unit-test.sol"; - -contract EmergencyProtectedTimelockMock is TargetMock { - uint256 public proposalsCount; - - function getProposalsCount() external view returns (uint256 count) { - return proposalsCount; - } - - function setProposalsCount(uint256 _proposalsCount) external { - proposalsCount = _proposalsCount; - } -} - -contract EmergencyExecutionCommitteeUnitTest is UnitTest { - EmergencyExecutionCommittee internal emergencyExecutionCommittee; - uint256 internal quorum = 2; - address internal owner = makeAddr("owner"); - address[] internal committeeMembers = [address(0x1), address(0x2), address(0x3)]; - address internal emergencyProtectedTimelock; - uint256 internal proposalId = 1; - - function setUp() external { - emergencyProtectedTimelock = address(new EmergencyProtectedTimelockMock()); - EmergencyProtectedTimelockMock(payable(emergencyProtectedTimelock)).setProposalsCount(1); - emergencyExecutionCommittee = - new EmergencyExecutionCommittee(owner, committeeMembers, quorum, emergencyProtectedTimelock); - } - - function testFuzz_constructor_HappyPath( - address _owner, - uint256 _quorum, - address _emergencyProtectedTimelock - ) external { - vm.assume(_owner != address(0)); - vm.assume(_quorum > 0 && _quorum <= committeeMembers.length); - EmergencyExecutionCommittee localCommittee = - new EmergencyExecutionCommittee(_owner, committeeMembers, _quorum, _emergencyProtectedTimelock); - assertEq(localCommittee.EMERGENCY_PROTECTED_TIMELOCK(), _emergencyProtectedTimelock); - } - - function test_voteEmergencyExecute_HappyPath() external { - vm.prank(committeeMembers[0]); - emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); - - (uint256 partialSupport,,,) = emergencyExecutionCommittee.getEmergencyExecuteState(proposalId); - assertEq(partialSupport, 1); - - vm.prank(committeeMembers[1]); - emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); - - (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = - emergencyExecutionCommittee.getEmergencyExecuteState(proposalId); - assertEq(support, quorum); - assertEq(executionQuorum, quorum); - assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); - assertFalse(isExecuted); - } - - function test_voteEmergencyExecute_RevertOn_ProposalIdExceedsProposalsCount() external { - uint256 nonExistentProposalId = proposalId + 1; - - vm.expectRevert( - abi.encodeWithSelector(EmergencyExecutionCommittee.ProposalDoesNotExist.selector, nonExistentProposalId) - ); - vm.prank(committeeMembers[0]); - emergencyExecutionCommittee.voteEmergencyExecute(nonExistentProposalId, true); - } - - function test_voteEmergencyExecute_RevertOn_ProposalIdIsZero() external { - uint256 nonExistentProposalId = 0; - - vm.expectRevert( - abi.encodeWithSelector(EmergencyExecutionCommittee.ProposalDoesNotExist.selector, nonExistentProposalId) - ); - vm.prank(committeeMembers[0]); - emergencyExecutionCommittee.voteEmergencyExecute(nonExistentProposalId, true); - } - - function testFuzz_voteEmergencyExecute_RevertOn_NotMember(address caller) external { - vm.assume(caller != committeeMembers[0] && caller != committeeMembers[1] && caller != committeeMembers[2]); - vm.prank(caller); - vm.expectRevert(abi.encodeWithSelector(HashConsensus.CallerIsNotMember.selector, caller)); - emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); - } - - function test_executeEmergencyExecute_HappyPath() external { - vm.prank(committeeMembers[0]); - emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); - vm.prank(committeeMembers[1]); - emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); - - vm.prank(committeeMembers[2]); - vm.expectCall( - emergencyProtectedTimelock, - abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyExecute.selector, proposalId) - ); - emergencyExecutionCommittee.executeEmergencyExecute(proposalId); - - (,,, bool isExecuted) = emergencyExecutionCommittee.getEmergencyExecuteState(proposalId); - assertTrue(isExecuted); - } - - function test_executeEmergencyExecute_RevertOn_QuorumNotReached() external { - vm.prank(committeeMembers[0]); - emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); - - vm.prank(committeeMembers[2]); - vm.expectRevert( - abi.encodeWithSelector( - HashConsensus.HashIsNotScheduled.selector, - keccak256(abi.encode(ProposalType.EmergencyExecute, proposalId)) - ) - ); - emergencyExecutionCommittee.executeEmergencyExecute(proposalId); - } - - function test_getEmergencyExecuteState_HappyPath() external { - (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = - emergencyExecutionCommittee.getEmergencyExecuteState(proposalId); - assertEq(support, 0); - assertEq(executionQuorum, quorum); - assertEq(quorumAt, Timestamp.wrap(0)); - assertFalse(isExecuted); - - vm.prank(committeeMembers[0]); - emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); - (support, executionQuorum, quorumAt, isExecuted) = - emergencyExecutionCommittee.getEmergencyExecuteState(proposalId); - assertEq(support, 1); - assertEq(executionQuorum, quorum); - assertEq(quorumAt, Timestamp.wrap(0)); - assertFalse(isExecuted); - - vm.prank(committeeMembers[1]); - emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); - (support, executionQuorum, quorumAt, isExecuted) = - emergencyExecutionCommittee.getEmergencyExecuteState(proposalId); - assertEq(support, 2); - assertEq(executionQuorum, quorum); - assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); - assertFalse(isExecuted); - - vm.prank(committeeMembers[2]); - emergencyExecutionCommittee.executeEmergencyExecute(proposalId); - (support, executionQuorum, quorumAt, isExecuted) = - emergencyExecutionCommittee.getEmergencyExecuteState(proposalId); - assertEq(support, 2); - assertTrue(isExecuted); - } - - function test_approveEmergencyReset_HappyPath() external { - vm.prank(committeeMembers[0]); - emergencyExecutionCommittee.approveEmergencyReset(); - - (uint256 partialSupport,,,) = emergencyExecutionCommittee.getEmergencyResetState(); - assertEq(partialSupport, 1); - - vm.prank(committeeMembers[1]); - emergencyExecutionCommittee.approveEmergencyReset(); - - (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = - emergencyExecutionCommittee.getEmergencyResetState(); - assertEq(support, quorum); - assertEq(executionQuorum, quorum); - assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); - assertFalse(isExecuted); - } - - function test_executeEmergencyReset_HappyPath() external { - vm.prank(committeeMembers[0]); - emergencyExecutionCommittee.approveEmergencyReset(); - vm.prank(committeeMembers[1]); - emergencyExecutionCommittee.approveEmergencyReset(); - - vm.prank(committeeMembers[2]); - vm.expectCall( - emergencyProtectedTimelock, abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyReset.selector) - ); - emergencyExecutionCommittee.executeEmergencyReset(); - - (,,, bool isExecuted) = emergencyExecutionCommittee.getEmergencyResetState(); - assertTrue(isExecuted); - } - - function test_executeEmergencyReset_RevertOn_QuorumNotReached() external { - vm.prank(committeeMembers[0]); - emergencyExecutionCommittee.approveEmergencyReset(); - - vm.prank(committeeMembers[2]); - vm.expectRevert( - abi.encodeWithSelector( - HashConsensus.HashIsNotScheduled.selector, - keccak256(abi.encode(ProposalType.EmergencyReset, bytes32(0))) - ) - ); - emergencyExecutionCommittee.executeEmergencyReset(); - } - - function test_getEmergencyResetState_HappyPath() external { - (uint256 support, uint256 executionQuorum, Timestamp quorumAt, bool isExecuted) = - emergencyExecutionCommittee.getEmergencyResetState(); - assertEq(support, 0); - assertEq(executionQuorum, quorum); - assertEq(quorumAt, Timestamp.wrap(0)); - assertFalse(isExecuted); - - vm.prank(committeeMembers[0]); - emergencyExecutionCommittee.approveEmergencyReset(); - (support, executionQuorum, quorumAt, isExecuted) = emergencyExecutionCommittee.getEmergencyResetState(); - assertEq(support, 1); - assertEq(executionQuorum, quorum); - assertEq(quorumAt, Timestamp.wrap(0)); - assertFalse(isExecuted); - - vm.prank(committeeMembers[1]); - emergencyExecutionCommittee.approveEmergencyReset(); - (support, executionQuorum, quorumAt, isExecuted) = emergencyExecutionCommittee.getEmergencyResetState(); - assertEq(support, 2); - assertEq(executionQuorum, quorum); - assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); - assertFalse(isExecuted); - - vm.prank(committeeMembers[2]); - emergencyExecutionCommittee.executeEmergencyReset(); - (support, executionQuorum, quorumAt, isExecuted) = emergencyExecutionCommittee.getEmergencyResetState(); - assertEq(support, 2); - assertTrue(isExecuted); - } -} diff --git a/test/unit/committees/ResealCommittee.t.sol b/test/unit/committees/ResealCommittee.t.sol deleted file mode 100644 index fc82a968..00000000 --- a/test/unit/committees/ResealCommittee.t.sol +++ /dev/null @@ -1,153 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; -import {Durations} from "contracts/types/Duration.sol"; -import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; -import {HashConsensus} from "contracts/committees/HashConsensus.sol"; - -import {TargetMock} from "test/utils/target-mock.sol"; -import {UnitTest} from "test/utils/unit-test.sol"; - -contract ResealCommitteeUnitTest is UnitTest { - ResealCommittee internal resealCommittee; - - uint256 internal quorum = 2; - address internal owner = makeAddr("owner"); - address[] internal committeeMembers = [address(0x1), address(0x2), address(0x3)]; - address internal sealable = makeAddr("sealable"); - address internal dualGovernance; - - function setUp() external { - dualGovernance = address(new TargetMock()); - resealCommittee = new ResealCommittee(owner, committeeMembers, quorum, dualGovernance, Durations.from(0)); - } - - function test_constructor_HappyPath() external { - ResealCommittee resealCommitteeLocal = - new ResealCommittee(owner, committeeMembers, quorum, dualGovernance, Durations.from(0)); - assertEq(resealCommitteeLocal.DUAL_GOVERNANCE(), dualGovernance); - } - - function test_voteReseal_HappyPath() external { - vm.prank(committeeMembers[0]); - resealCommittee.voteReseal(sealable, true); - - (uint256 partialSupport,,) = resealCommittee.getResealState(sealable); - assertEq(partialSupport, 1); - - vm.prank(committeeMembers[1]); - resealCommittee.voteReseal(sealable, true); - - (uint256 support, uint256 executionQuorum, Timestamp quorumAt) = resealCommittee.getResealState(sealable); - - assertEq(support, quorum); - assertEq(executionQuorum, quorum); - assertEq(quorumAt, Timestamp.wrap(uint40(block.timestamp))); - } - - function test_voteReseal_RevertOn_ZeroAddress() external { - vm.prank(committeeMembers[0]); - vm.expectRevert(abi.encodeWithSelector(ResealCommittee.InvalidSealable.selector, address(0))); - resealCommittee.voteReseal(address(0), true); - } - - function testFuzz_voteReseal_RevertOn_NotMember(address caller) external { - vm.assume(caller != committeeMembers[0] && caller != committeeMembers[1] && caller != committeeMembers[2]); - vm.prank(caller); - vm.expectRevert(abi.encodeWithSelector(HashConsensus.CallerIsNotMember.selector, caller)); - resealCommittee.voteReseal(sealable, true); - } - - function test_executeReseal_HappyPath() external { - vm.prank(committeeMembers[0]); - resealCommittee.voteReseal(sealable, true); - vm.prank(committeeMembers[1]); - resealCommittee.voteReseal(sealable, true); - - vm.prank(committeeMembers[2]); - vm.expectCall(dualGovernance, abi.encodeWithSelector(IDualGovernance.resealSealable.selector, sealable)); - resealCommittee.executeReseal(sealable); - - (uint256 support, uint256 executionQuorum, Timestamp quorumAt) = resealCommittee.getResealState(sealable); - assertEq(support, 0); - assertEq(executionQuorum, quorum); - assertEq(quorumAt, Timestamp.wrap(0)); - } - - function test_executeReseal_RevertOn_QuorumNotReached() external { - vm.prank(committeeMembers[0]); - resealCommittee.voteReseal(sealable, true); - - vm.prank(committeeMembers[2]); - vm.expectRevert( - abi.encodeWithSelector( - HashConsensus.HashIsNotScheduled.selector, keccak256(abi.encode(sealable, /* resealNonce */ 0)) - ) - ); - resealCommittee.executeReseal(sealable); - } - - function test_getResealState_HappyPath() external { - vm.prank(owner); - resealCommittee.setQuorum(3); - - (uint256 support, uint256 executionQuorum, Timestamp quorumAt) = resealCommittee.getResealState(sealable); - assertEq(support, 0); - assertEq(executionQuorum, 3); - assertEq(quorumAt, Timestamps.ZERO); - - vm.prank(committeeMembers[0]); - resealCommittee.voteReseal(sealable, true); - - (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); - assertEq(support, 1); - assertEq(executionQuorum, 3); - assertEq(quorumAt, Timestamps.ZERO); - - vm.prank(committeeMembers[1]); - resealCommittee.voteReseal(sealable, true); - - (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); - assertEq(support, 2); - assertEq(executionQuorum, 3); - assertEq(quorumAt, Timestamps.ZERO); - - _wait(Durations.from(1)); - - vm.prank(committeeMembers[1]); - resealCommittee.voteReseal(sealable, false); - - (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); - assertEq(support, 1); - assertEq(executionQuorum, 3); - assertEq(quorumAt, Timestamps.ZERO); - - vm.prank(committeeMembers[1]); - resealCommittee.voteReseal(sealable, true); - - (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); - assertEq(support, 2); - assertEq(executionQuorum, 3); - assertEq(quorumAt, Timestamps.ZERO); - - vm.prank(committeeMembers[2]); - resealCommittee.voteReseal(sealable, true); - - Timestamp quorumAtExpected = Timestamps.now(); - (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); - assertEq(support, 3); - assertEq(executionQuorum, 3); - assertEq(quorumAt, quorumAtExpected); - - vm.prank(committeeMembers[2]); - vm.expectCall(dualGovernance, abi.encodeWithSelector(IDualGovernance.resealSealable.selector, sealable)); - resealCommittee.executeReseal(sealable); - - (support, executionQuorum, quorumAt) = resealCommittee.getResealState(sealable); - assertEq(support, 0); - assertEq(executionQuorum, 3); - assertEq(quorumAt, Timestamp.wrap(0)); - } -} diff --git a/test/unit/libraries/AssetsAccounting.t.sol b/test/unit/libraries/AssetsAccounting.t.sol index cf928b64..8e2e6734 100644 --- a/test/unit/libraries/AssetsAccounting.t.sol +++ b/test/unit/libraries/AssetsAccounting.t.sol @@ -10,9 +10,9 @@ import {SharesValue, SharesValues, SharesValueOverflow, SharesValueUnderflow} fr import {IndicesOneBased} from "contracts/types/IndexOneBased.sol"; import {Durations} from "contracts/types/Duration.sol"; import {Timestamps} from "contracts/types/Timestamp.sol"; -import { - AssetsAccounting, WithdrawalRequestStatus, UnstETHRecordStatus -} from "contracts/libraries/AssetsAccounting.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; +import {AssetsAccounting, UnstETHRecordStatus} from "contracts/libraries/AssetsAccounting.sol"; import {UnitTest, Duration} from "test/utils/unit-test.sol"; @@ -347,8 +347,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHIds.push(genRandomUnstEthId(1024)); _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = - new WithdrawalRequestStatus[](amountsOfShares.length); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](amountsOfShares.length); uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); for (uint256 i = 0; i < amountsOfShares.length; ++i) { @@ -389,10 +389,11 @@ contract AssetsAccountingUnitTests is UnitTest { function testFuzz_accountUnstETHLock_RevertOn_UnstETHIdsLengthNotEqualToWithdrawalRequestStatusesLength( address holder ) external { - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](1); uint256[] memory unstETHIds = new uint256[](0); - vm.expectRevert(); + vm.expectRevert(stdError.assertionError); AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); } @@ -411,8 +412,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = - new WithdrawalRequestStatus[](amountsOfShares.length); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](amountsOfShares.length); uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); for (uint256 i = 0; i < amountsOfShares.length; ++i) { @@ -450,8 +451,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = - new WithdrawalRequestStatus[](amountsOfShares.length); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](amountsOfShares.length); uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); for (uint256 i = 0; i < amountsOfShares.length; ++i) { @@ -464,7 +465,7 @@ contract AssetsAccountingUnitTests is UnitTest { withdrawalRequestStatuses[withdrawalRequestStatuses.length - 1].isClaimed = true; - vm.expectRevert(); + vm.expectRevert(stdError.assertionError); AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); } @@ -483,8 +484,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = - new WithdrawalRequestStatus[](amountsOfShares.length); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](amountsOfShares.length); uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); for (uint256 i = 0; i < amountsOfShares.length; ++i) { @@ -522,8 +523,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = - new WithdrawalRequestStatus[](amountsOfShares.length); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](amountsOfShares.length); uint256[] memory unstETHIds = new uint256[](amountsOfShares.length); for (uint256 i = 0; i < amountsOfShares.length; ++i) { @@ -559,7 +560,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](0); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](0); uint256[] memory unstETHIds = new uint256[](0); vm.expectEmit(); @@ -586,7 +588,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](1); uint256[] memory unstETHIds = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(0); @@ -611,7 +614,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](1); uint256[] memory unstETHIds = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(0); @@ -635,7 +639,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.assets[holder].unstETHLockedShares = holderUnstETHLockedShares; _accountingContext.unstETHTotals.unfinalizedShares = initialTotalUnfinalizedShares; - WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); + IWithdrawalQueue.WithdrawalRequestStatus[] memory withdrawalRequestStatuses = + new IWithdrawalQueue.WithdrawalRequestStatus[](1); uint256[] memory unstETHIds = new uint256[](1); unstETHIds[0] = genRandomUnstEthId(0); @@ -1416,39 +1421,59 @@ contract AssetsAccountingUnitTests is UnitTest { } // --- - // getLockedAssetsTotals + // getLockedUnstETHDetails // --- - function testFuzz_getLockedAssetsTotals_happyPath( - ETHValue totalFinalizedETH, - SharesValue totalLockedShares, - SharesValue totalUnfinalizedShares - ) external { - vm.assume(totalFinalizedETH.toUint256() < type(uint96).max); - vm.assume(totalLockedShares.toUint256() < type(uint96).max); - vm.assume(totalUnfinalizedShares.toUint256() < type(uint96).max); - _accountingContext.unstETHTotals.finalizedETH = totalFinalizedETH; - _accountingContext.unstETHTotals.unfinalizedShares = totalUnfinalizedShares; - _accountingContext.stETHTotals.lockedShares = totalLockedShares; + function test_getLockedUnstETHDetails_HappyPath() external { + uint256 unstETHIdsCount = 4; + uint256[] memory unstETHIds = new uint256[](unstETHIdsCount); + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + address holder = address(uint160(uint256(keccak256(abi.encode(i))))); - (SharesValue unfinalizedShares, ETHValue finalizedETH) = - AssetsAccounting.getLockedAssetsTotals(_accountingContext); + _accountingContext.unstETHRecords[unstETHIds[i]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus(i + 1); + _accountingContext.unstETHRecords[unstETHIds[i]].shares = SharesValues.from((i + 1) * 1 ether); + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from((i + 1) * 10 ether); + + _accountingContext.assets[holder].unstETHIds.push(unstETHIds[i]); + } + + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + ISignallingEscrow.LockedUnstETHDetails memory unstETHDetails = + AssetsAccounting.getLockedUnstETHDetails(_accountingContext, unstETHIds[i]); - assertEq(unfinalizedShares, totalLockedShares + totalUnfinalizedShares); - assertEq(finalizedETH, totalFinalizedETH); + assertEq(unstETHDetails.id, unstETHIds[i]); + assertEq(unstETHDetails.status, UnstETHRecordStatus(i + 1)); + assertEq(unstETHDetails.lockedBy, address(uint160(uint256(keccak256(abi.encode(i)))))); + assertEq(unstETHDetails.shares, SharesValues.from((i + 1) * 1 ether)); + assertEq(unstETHDetails.claimableAmount, ETHValues.from((i + 1) * 10 ether)); + } } - function test_getLockedAssetsTotals_RevertOn_UnfinalizedSharesOverflow() external { - ETHValue totalFinalizedETH = ETHValues.from(1); - SharesValue totalUnfinalizedShares = SharesValues.from(type(uint128).max - 1); - SharesValue totalLockedShares = SharesValues.from(type(uint128).max - 1); + function test_getLockedUnstETHDetails_RevertOn_UnstETHNotLocked() external { + uint256 unstETHIdsCount = 4; + uint256[] memory unstETHIds = new uint256[](unstETHIdsCount); + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + address holder = address(uint160(uint256(keccak256(abi.encode(i))))); - _accountingContext.unstETHTotals.finalizedETH = totalFinalizedETH; - _accountingContext.unstETHTotals.unfinalizedShares = totalUnfinalizedShares; - _accountingContext.stETHTotals.lockedShares = totalLockedShares; + _accountingContext.unstETHRecords[unstETHIds[i]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus(i + 1); + _accountingContext.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(i * 1 ether); + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(i * 10 ether); - vm.expectRevert(SharesValueOverflow.selector); - AssetsAccounting.getLockedAssetsTotals(_accountingContext); + _accountingContext.assets[holder].unstETHIds.push(unstETHIds[i]); + } + + uint256 notLockedUnstETHId = genRandomUnstEthId(5); + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, notLockedUnstETHId, UnstETHRecordStatus.NotLocked + ) + ); + AssetsAccounting.getLockedUnstETHDetails(_accountingContext, notLockedUnstETHId); } // --- diff --git a/test/unit/libraries/DualGovernanceConfig.t.sol b/test/unit/libraries/DualGovernanceConfig.t.sol index 6b37503d..ec9a6c68 100644 --- a/test/unit/libraries/DualGovernanceConfig.t.sol +++ b/test/unit/libraries/DualGovernanceConfig.t.sol @@ -106,31 +106,32 @@ contract DualGovernanceConfigTest is UnitTest { } // --- - // isFirstSealRageQuitSupportCrossed() + // isFirstSealRageQuitSupportReached() // --- - function testFuzz_isFirstSealRageQuitSupportCrossed_HappyPath( + function testFuzz_isFirstSealRageQuitSupportReached_HappyPath( DualGovernanceConfig.Context memory config, PercentD16 rageQuitSupport ) external { _assumeConfigParams(config); assertEq( - config.isFirstSealRageQuitSupportCrossed(rageQuitSupport), rageQuitSupport > config.firstSealRageQuitSupport + config.isFirstSealRageQuitSupportReached(rageQuitSupport), + rageQuitSupport >= config.firstSealRageQuitSupport ); } // --- - // isSecondSealRageQuitSupportCrossed() + // isSecondSealRageQuitSupportReached() // --- - function testFuzz_isSecondSealRageQuitSupportCrossed_HappyPath( + function testFuzz_isSecondSealRageQuitSupportReached_HappyPath( DualGovernanceConfig.Context memory config, PercentD16 rageQuitSupport ) external { _assumeConfigParams(config); assertEq( - config.isSecondSealRageQuitSupportCrossed(rageQuitSupport), - rageQuitSupport > config.secondSealRageQuitSupport + config.isSecondSealRageQuitSupportReached(rageQuitSupport), + rageQuitSupport >= config.secondSealRageQuitSupport ); } @@ -264,12 +265,12 @@ contract DualGovernanceConfigTest is UnitTest { // calcVetoSignallingDuration() // --- - function testFuzz_calcVetoSignallingDuration_HappyPath_RageQuitSupportLessOrEqualThanFirstSeal( + function testFuzz_calcVetoSignallingDuration_HappyPath_RageQuitSupportLessThanFirstSeal( DualGovernanceConfig.Context memory config, PercentD16 rageQuitSupport ) external { _assumeConfigParams(config); - vm.assume(rageQuitSupport <= config.firstSealRageQuitSupport); + vm.assume(rageQuitSupport < config.firstSealRageQuitSupport); assertEq(config.calcVetoSignallingDuration(rageQuitSupport), Durations.ZERO); } @@ -287,7 +288,7 @@ contract DualGovernanceConfigTest is UnitTest { PercentD16 rageQuitSupport ) external { _assumeConfigParams(config); - vm.assume(rageQuitSupport > config.firstSealRageQuitSupport); + vm.assume(rageQuitSupport >= config.firstSealRageQuitSupport); vm.assume(rageQuitSupport < config.secondSealRageQuitSupport); PercentD16 rageQuitSupportFirstSealDelta = rageQuitSupport - config.firstSealRageQuitSupport; diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 829d5b87..7ae20adc 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -3,6 +3,10 @@ pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; +import {IRageQuitEscrow} from "contracts/interfaces/IRageQuitEscrow.sol"; + import {Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; @@ -14,12 +18,12 @@ import { } from "contracts/ImmutableDualGovernanceConfigProvider.sol"; import {UnitTest} from "test/utils/unit-test.sol"; -import {IEscrow, EscrowMock} from "test/mocks/EscrowMock.sol"; contract DualGovernanceStateMachineUnitTests is UnitTest { using DualGovernanceStateMachine for DualGovernanceStateMachine.Context; - IEscrow private immutable _ESCROW_MASTER_COPY = new EscrowMock(); + address private immutable _ESCROW_MASTER_COPY_MOCK = makeAddr("ESCROW_MASTER_COPY_MOCK"); + ImmutableDualGovernanceConfigProvider internal immutable _CONFIG_PROVIDER = new ImmutableDualGovernanceConfigProvider( DualGovernanceConfig.Context({ firstSealRageQuitSupport: PercentsD16.fromBasisPoints(3_00), // 3% @@ -44,7 +48,10 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { DualGovernanceStateMachine.Context private _stateMachine; function setUp() external { - _stateMachine.initialize(_CONFIG_PROVIDER, _ESCROW_MASTER_COPY); + _stateMachine.initialize(_CONFIG_PROVIDER, IEscrowBase(_ESCROW_MASTER_COPY_MOCK)); + _mockRageQuitFinalized(false); + _mockRageQuitSupport(PercentsD16.from(0)); + _mockEscrowMasterCopy(); } function test_initialize_RevertOn_ReInitialization() external { @@ -56,42 +63,126 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { // activateNextState() // --- + function test_activateNextState_SideEffects_RageQuitRoundResetInVetoCooldownAfterRageQuit() external { + // Transition state machine into the VetoSignalling state + _mockRageQuitSupport(_CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1)); + _activateNextState(); + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); + + // Simulate the Rage Quit process has completed and in the SignallingEscrow the first seal is not reached + _mockRageQuitFinalized(true); + _activateNextState(); + _mockRageQuitSupport(PercentsD16.fromBasisPoints(0)); + + // Rage Quit Round should reset after system entered the VetoCooldown + + _assertState({persisted: State.RageQuit, effective: State.VetoCooldown}); + assertEq(_stateMachine.rageQuitRound, 1); + + _activateNextState(); + + _assertState({persisted: State.VetoCooldown, effective: State.VetoCooldown}); + assertEq(_stateMachine.rageQuitRound, 0); + } + + function test_activateNextState_SideEffects_RageQuitRoundResetInVetoCooldownAfterVetoSignallingDeactivation() + external + { + // Transition state machine into the RageQuit state + _mockRageQuitSupport(_CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1)); + _activateNextState(); + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); + + // Simulate the Rage Quit process has completed and in the SignallingEscrow the first seal is reached + _mockRageQuitFinalized(true); + _activateNextState(); + + _assertState({persisted: State.RageQuit, effective: State.VetoSignalling}); + assertEq(_stateMachine.rageQuitRound, 1); + + _activateNextState(); + + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + assertEq(_stateMachine.rageQuitRound, 1); + + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().dividedBy(2)); + + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + assertEq(_stateMachine.rageQuitRound, 1); + + // Simulate the Rage Quit support decreased + _mockRageQuitSupport(PercentsD16.from(3_00)); + + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignallingDeactivation}); + assertEq(_stateMachine.rageQuitRound, 1); + + _activateNextState(); + + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignallingDeactivation}); + assertEq(_stateMachine.rageQuitRound, 1); + + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoCooldown}); + assertEq(_stateMachine.rageQuitRound, 1); + + _activateNextState(); + + _assertState({persisted: State.VetoCooldown, effective: State.VetoCooldown}); + assertEq(_stateMachine.rageQuitRound, 0); + } + function test_activateNextState_HappyPath_MaxRageQuitsRound() external { - assertEq(_stateMachine.state, State.Normal); + _assertState({persisted: State.Normal, effective: State.Normal}); - for (uint256 i = 0; i < 2 * DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND; ++i) { - address signallingEscrow = address(_stateMachine.signallingEscrow); - EscrowMock(signallingEscrow).__setRageQuitSupport( - _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) - ); - assertTrue( - _stateMachine.signallingEscrow.getRageQuitSupport() > _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() - ); - assertEq(_stateMachine.rageQuitRound, Math.min(i, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND)); + // For the simplicity, simulate that Signalling Escrow always has rage quit support greater than the second seal + _mockRageQuitSupport(_CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00)); + // And that the Rage Quit finalized + _mockRageQuitFinalized(true); - // wait here the full duration of the veto cooldown to make sure it's over from the previous iteration - _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); + assertTrue( + _stateMachine.signallingEscrow.getRageQuitSupport() > _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + ); + _assertState({persisted: State.Normal, effective: State.VetoSignalling}); + _activateNextState(); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); - assertEq(_stateMachine.state, State.VetoSignalling); + // Simulate sequential Rage Quits + for (uint256 i = 0; i < 2 * DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND; ++i) { + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + assertEq(_stateMachine.rageQuitRound, Math.min(i, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND)); _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.state, State.RageQuit); + // Effective state is VetoSignalling, as the rage quit is considered finalized + _assertState({persisted: State.RageQuit, effective: State.VetoSignalling}); assertEq(_stateMachine.rageQuitRound, Math.min(i + 1, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND)); - EscrowMock(signallingEscrow).__setIsRageQuitFinalized(true); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); - assertEq(_stateMachine.state, State.VetoCooldown); + _activateNextState(); } + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().dividedBy(2)); + // after the sequential rage quits chain is broken, the rage quit resets to 0 - _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _mockRageQuitSupport(PercentsD16.from(0)); + + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignallingDeactivation}); + assertEq(_stateMachine.rageQuitRound, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND); + + _activateNextState(); + + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignallingDeactivation}); + assertEq(_stateMachine.rageQuitRound, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND); + + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoCooldown}); + assertEq(_stateMachine.rageQuitRound, DualGovernanceStateMachine.MAX_RAGE_QUIT_ROUND); + + _activateNextState(); + + _assertState({persisted: State.VetoCooldown, effective: State.VetoCooldown}); assertEq(_stateMachine.rageQuitRound, 0); - assertEq(_stateMachine.state, State.Normal); } // --- @@ -99,102 +190,71 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { // --- function test_canSubmitProposal_HappyPath() external { - address signallingEscrow = address(_stateMachine.signallingEscrow); - - assertEq(_stateMachine.getPersistedState(), State.Normal); - assertEq(_stateMachine.getEffectiveState(), State.Normal); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); + _assertState({persisted: State.Normal, effective: State.Normal}); + _assertCanSubmitProposal({persisted: true, effective: true}); // simulate the first threshold of veto signalling was reached - EscrowMock(signallingEscrow).__setRageQuitSupport( - _CONFIG_PROVIDER.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentD16.wrap(1) - ); + _mockRageQuitSupport(_CONFIG_PROVIDER.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.from(1)); - assertEq(_stateMachine.getPersistedState(), State.Normal); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.Normal, effective: State.VetoSignalling}); + _assertCanSubmitProposal({persisted: true, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + _assertCanSubmitProposal({persisted: true, effective: true}); - _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MIN_DURATION().plusSeconds(1 minutes)); + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MIN_DURATION().plusSeconds(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignallingDeactivation}); + _assertCanSubmitProposal({persisted: true, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); - assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignallingDeactivation}); + _assertCanSubmitProposal({persisted: false, effective: false}); // simulate the second threshold of veto signalling was reached - EscrowMock(signallingEscrow).__setRageQuitSupport( - _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) - ); + _mockRageQuitSupport(_CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.from(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignalling}); + _assertCanSubmitProposal({persisted: false, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + _assertCanSubmitProposal({persisted: true, effective: true}); _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.RageQuit); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.VetoSignalling, effective: State.RageQuit}); + _assertCanSubmitProposal({persisted: true, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.RageQuit); - assertEq(_stateMachine.getEffectiveState(), State.RageQuit); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.RageQuit, effective: State.RageQuit}); + _assertCanSubmitProposal({persisted: true, effective: true}); - EscrowMock(address(_stateMachine.rageQuitEscrow)).__setIsRageQuitFinalized(true); + _mockRageQuitFinalized(true); + _mockRageQuitSupport(PercentsD16.from(0)); - assertEq(_stateMachine.getPersistedState(), State.RageQuit); - assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.RageQuit, effective: State.VetoCooldown}); + _assertCanSubmitProposal({persisted: true, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); - assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); - assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.VetoCooldown, effective: State.VetoCooldown}); + _assertCanSubmitProposal({persisted: false, effective: false}); _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); - assertEq(_stateMachine.getEffectiveState(), State.Normal); - assertFalse(_stateMachine.canSubmitProposal({useEffectiveState: false})); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); + _assertState({persisted: State.VetoCooldown, effective: State.Normal}); + _assertCanSubmitProposal({persisted: false, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.Normal); - assertEq(_stateMachine.getEffectiveState(), State.Normal); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: true})); - assertTrue(_stateMachine.canSubmitProposal({useEffectiveState: false})); + _assertState({persisted: State.Normal, effective: State.Normal}); + _assertCanSubmitProposal({persisted: true, effective: true}); } // --- @@ -202,232 +262,117 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { // --- function test_canScheduleProposal_HappyPath() external { - address signallingEscrow = address(_stateMachine.signallingEscrow); Timestamp proposalSubmittedAt = Timestamps.now(); - assertEq(_stateMachine.getPersistedState(), State.Normal); - assertEq(_stateMachine.getEffectiveState(), State.Normal); - assertTrue( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertTrue( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); + _assertState({persisted: State.Normal, effective: State.Normal}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: true, effective: true}); // simulate the first threshold of veto signalling was reached - EscrowMock(signallingEscrow).__setRageQuitSupport( - _CONFIG_PROVIDER.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentD16.wrap(1) - ); + _mockRageQuitSupport(_CONFIG_PROVIDER.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.from(1)); - assertEq(_stateMachine.getPersistedState(), State.Normal); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertTrue( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); + _assertState({persisted: State.Normal, effective: State.VetoSignalling}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: true, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: false}); - _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MIN_DURATION().plusSeconds(1 minutes)); + _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MIN_DURATION().plusSeconds(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignallingDeactivation}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignallingDeactivation}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: false}); // simulate the second threshold of veto signalling was reached - EscrowMock(signallingEscrow).__setRageQuitSupport( - _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) - ); + _mockRageQuitSupport(_CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.from(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignalling}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: false}); _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.RageQuit); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); + _assertState({persisted: State.VetoSignalling, effective: State.RageQuit}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.RageQuit); - assertEq(_stateMachine.getEffectiveState(), State.RageQuit); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); + _assertState({persisted: State.RageQuit, effective: State.RageQuit}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: false}); - EscrowMock(address(_stateMachine.rageQuitEscrow)).__setIsRageQuitFinalized(true); + _mockRageQuitFinalized(true); + _mockRageQuitSupport(PercentsD16.from(0)); - assertEq(_stateMachine.getPersistedState(), State.RageQuit); - assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertTrue( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); - // for proposals submitted at the same block the VetoSignalling started scheduling is allowed - assertTrue( - _stateMachine.canScheduleProposal({ - useEffectiveState: true, - proposalSubmittedAt: _stateMachine.vetoSignallingActivatedAt - }) - ); - // for proposals submitted after the VetoSignalling started scheduling is forbidden - assertFalse( - _stateMachine.canScheduleProposal({ - useEffectiveState: true, - proposalSubmittedAt: Durations.from(1 seconds).addTo(_stateMachine.vetoSignallingActivatedAt) - }) - ); - assertFalse(_stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: Timestamps.now()})); - - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _assertState({persisted: State.RageQuit, effective: State.VetoCooldown}); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: false, effective: true}); - assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); - assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); - - // persisted - assertTrue( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertTrue( - _stateMachine.canScheduleProposal({ - useEffectiveState: false, - proposalSubmittedAt: _stateMachine.vetoSignallingActivatedAt - }) - ); - // for proposals submitted after the VetoSignalling started scheduling is forbidden - assertFalse( - _stateMachine.canScheduleProposal({ - useEffectiveState: false, - proposalSubmittedAt: Durations.from(1 seconds).addTo(_stateMachine.vetoSignallingActivatedAt) - }) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: Timestamps.now()}) - ); - - // effective - assertTrue( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); // for proposals submitted at the same block the VetoSignalling started scheduling is allowed - assertTrue( - _stateMachine.canScheduleProposal({ - useEffectiveState: true, - proposalSubmittedAt: _stateMachine.vetoSignallingActivatedAt - }) - ); + _assertCanScheduleProposal({ + proposalSubmittedAt: _stateMachine.vetoSignallingActivatedAt, + persisted: false, + effective: true + }); // for proposals submitted after the VetoSignalling started scheduling is forbidden - assertFalse( - _stateMachine.canScheduleProposal({ - useEffectiveState: true, - proposalSubmittedAt: Durations.from(1 seconds).addTo(_stateMachine.vetoSignallingActivatedAt) - }) - ); - assertFalse(_stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: Timestamps.now()})); + _assertCanScheduleProposal({ + proposalSubmittedAt: Durations.from(1 seconds).addTo(_stateMachine.vetoSignallingActivatedAt), + persisted: false, + effective: false + }); + _assertCanScheduleProposal({proposalSubmittedAt: Timestamps.now(), persisted: false, effective: false}); + + _activateNextState(); + + _assertState({persisted: State.VetoCooldown, effective: State.VetoCooldown}); + + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: true, effective: true}); + _assertCanScheduleProposal({ + proposalSubmittedAt: _stateMachine.vetoSignallingActivatedAt, + persisted: true, + effective: true + }); + // for proposals submitted after the VetoSignalling started scheduling is forbidden + _assertCanScheduleProposal({ + proposalSubmittedAt: Durations.from(1 seconds).addTo(_stateMachine.vetoSignallingActivatedAt), + persisted: false, + effective: false + }); + _assertCanScheduleProposal({proposalSubmittedAt: Timestamps.now(), persisted: false, effective: false}); _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); - assertEq(_stateMachine.getEffectiveState(), State.Normal); + _assertState({persisted: State.VetoCooldown, effective: State.Normal}); - // persisted - assertTrue( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertTrue( - _stateMachine.canScheduleProposal({ - useEffectiveState: false, - proposalSubmittedAt: _stateMachine.vetoSignallingActivatedAt - }) - ); - // for proposals submitted after the VetoSignalling started scheduling is forbidden - assertFalse( - _stateMachine.canScheduleProposal({ - useEffectiveState: false, - proposalSubmittedAt: Durations.from(1 seconds).addTo(_stateMachine.vetoSignallingActivatedAt) - }) - ); - assertFalse( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: Timestamps.now()}) - ); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: true, effective: true}); + _assertCanScheduleProposal({ + proposalSubmittedAt: _stateMachine.vetoSignallingActivatedAt, + persisted: true, + effective: true + }); - // effective - assertTrue( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertTrue(_stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: Timestamps.now()})); + // for proposals submitted after the VetoSignalling started scheduling is forbidden + _assertCanScheduleProposal({ + proposalSubmittedAt: Durations.from(1 seconds).addTo(_stateMachine.vetoSignallingActivatedAt), + persisted: false, + effective: true + }); + _assertCanScheduleProposal({proposalSubmittedAt: Timestamps.now(), persisted: false, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.Normal); - assertEq(_stateMachine.getEffectiveState(), State.Normal); + _assertState({persisted: State.Normal, effective: State.Normal}); // persisted - assertTrue( - _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertTrue(_stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: Timestamps.now()})); - - // effective - assertTrue( - _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}) - ); - assertTrue(_stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: Timestamps.now()})); + _assertCanScheduleProposal({proposalSubmittedAt: proposalSubmittedAt, persisted: true, effective: true}); + _assertCanScheduleProposal({proposalSubmittedAt: Timestamps.now(), persisted: true, effective: true}); } // --- @@ -435,110 +380,150 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { // --- function test_canCancelAllPendingProposals_HappyPath() external { - address signallingEscrow = address(_stateMachine.signallingEscrow); - Timestamp proposalSubmittedAt = Timestamps.now(); - - assertEq(_stateMachine.getPersistedState(), State.Normal); - assertEq(_stateMachine.getEffectiveState(), State.Normal); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); + _assertState({persisted: State.Normal, effective: State.Normal}); + _assertCanCancelAllPendingProposals({persisted: false, effective: false}); // simulate the first threshold of veto signalling was reached - EscrowMock(signallingEscrow).__setRageQuitSupport( - _CONFIG_PROVIDER.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentD16.wrap(1) - ); + _mockRageQuitSupport(_CONFIG_PROVIDER.FIRST_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.from(1)); - assertEq(_stateMachine.getPersistedState(), State.Normal); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.Normal, effective: State.VetoSignalling}); + _assertCanCancelAllPendingProposals({persisted: false, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + _assertCanCancelAllPendingProposals({persisted: true, effective: true}); _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MIN_DURATION().plusSeconds(1 minutes)); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignallingDeactivation}); + _assertCanCancelAllPendingProposals({persisted: true, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignallingDeactivation); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignallingDeactivation}); + _assertCanCancelAllPendingProposals({persisted: true, effective: true}); // simulate the second threshold of veto signalling was reached - EscrowMock(signallingEscrow).__setRageQuitSupport( - _CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.fromBasisPoints(1_00) - ); + _mockRageQuitSupport(_CONFIG_PROVIDER.SECOND_SEAL_RAGE_QUIT_SUPPORT() + PercentsD16.from(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoSignallingDeactivation); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.VetoSignallingDeactivation, effective: State.VetoSignalling}); + _assertCanCancelAllPendingProposals({persisted: true, effective: true}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.VetoSignalling); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.VetoSignalling, effective: State.VetoSignalling}); + _assertCanCancelAllPendingProposals({persisted: true, effective: true}); _wait(_CONFIG_PROVIDER.VETO_SIGNALLING_MAX_DURATION().plusSeconds(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoSignalling); - assertEq(_stateMachine.getEffectiveState(), State.RageQuit); - assertTrue(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.VetoSignalling, effective: State.RageQuit}); + _assertCanCancelAllPendingProposals({persisted: true, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.RageQuit); - assertEq(_stateMachine.getEffectiveState(), State.RageQuit); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.RageQuit, effective: State.RageQuit}); + _assertCanCancelAllPendingProposals({persisted: false, effective: false}); - EscrowMock(address(_stateMachine.rageQuitEscrow)).__setIsRageQuitFinalized(true); + _mockRageQuitFinalized(true); + _mockRageQuitSupport(PercentsD16.from(0)); - assertEq(_stateMachine.getPersistedState(), State.RageQuit); - assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.RageQuit, effective: State.VetoCooldown}); + _assertCanCancelAllPendingProposals({persisted: false, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); - assertEq(_stateMachine.getEffectiveState(), State.VetoCooldown); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.VetoCooldown, effective: State.VetoCooldown}); + _assertCanCancelAllPendingProposals({persisted: false, effective: false}); _wait(_CONFIG_PROVIDER.VETO_COOLDOWN_DURATION().plusSeconds(1)); - assertEq(_stateMachine.getPersistedState(), State.VetoCooldown); - assertEq(_stateMachine.getEffectiveState(), State.Normal); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.VetoCooldown, effective: State.Normal}); + _assertCanCancelAllPendingProposals({persisted: false, effective: false}); - _stateMachine.activateNextState(_ESCROW_MASTER_COPY); + _activateNextState(); - assertEq(_stateMachine.getPersistedState(), State.Normal); - assertEq(_stateMachine.getEffectiveState(), State.Normal); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: false})); - assertFalse(_stateMachine.canCancelAllPendingProposals({useEffectiveState: true})); + _assertState({persisted: State.Normal, effective: State.Normal}); + _assertCanCancelAllPendingProposals({persisted: false, effective: false}); } // --- // Test helper methods // --- + function _mockEscrowMasterCopy() internal { + vm.mockCall( + _ESCROW_MASTER_COPY_MOCK, + abi.encodeWithSelector(IEscrowBase.ESCROW_MASTER_COPY.selector), + abi.encode(_ESCROW_MASTER_COPY_MOCK) + ); + } + + function _mockRageQuitSupport(PercentD16 rageQuitSupport) internal { + vm.mockCall( + _ESCROW_MASTER_COPY_MOCK, + abi.encodeCall(ISignallingEscrow.getRageQuitSupport, ()), + abi.encode(rageQuitSupport) + ); + } + + function _mockRageQuitFinalized(bool isRageQuitFinalized) internal { + vm.mockCall( + _ESCROW_MASTER_COPY_MOCK, + abi.encodeCall(IRageQuitEscrow.isRageQuitFinalized, ()), + abi.encode(isRageQuitFinalized) + ); + } + + function _activateNextState() internal { + _stateMachine.activateNextState(); + } + + function _assertState(State persisted, State effective) internal { + assertEq(_stateMachine.getPersistedState(), persisted, "Unexpected Persisted State"); + assertEq(_stateMachine.getEffectiveState(), effective, "Unexpected Effective State"); + } + + function _assertCanCancelAllPendingProposals(bool persisted, bool effective) internal { + assertEq( + _stateMachine.canCancelAllPendingProposals({useEffectiveState: false}), + persisted, + "Unexpected persisted canCancelAllPendingProposals() value" + ); + assertEq( + _stateMachine.canCancelAllPendingProposals({useEffectiveState: true}), + effective, + "Unexpected effective canCancelAllPendingProposals() value" + ); + } + + function _assertCanScheduleProposal(Timestamp proposalSubmittedAt, bool persisted, bool effective) internal { + assertEq( + _stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt}), + persisted, + "Unexpected persisted canScheduleProposal() value" + ); + assertEq( + _stateMachine.canScheduleProposal({useEffectiveState: true, proposalSubmittedAt: proposalSubmittedAt}), + effective, + "Unexpected persisted canScheduleProposal() value" + ); + } + + function _assertCanSubmitProposal(bool persisted, bool effective) internal { + assertEq( + _stateMachine.canSubmitProposal({useEffectiveState: false}), + persisted, + "Unexpected persisted canSubmitProposal() value" + ); + assertEq( + _stateMachine.canSubmitProposal({useEffectiveState: true}), + effective, + "Unexpected effective canSubmitProposal() value" + ); + } + function external__initialize() external { - _stateMachine.initialize(_CONFIG_PROVIDER, _ESCROW_MASTER_COPY); + _stateMachine.initialize(_CONFIG_PROVIDER, IEscrowBase(_ESCROW_MASTER_COPY_MOCK)); } } diff --git a/test/unit/libraries/DualGovernanceStateTransitions.t.sol b/test/unit/libraries/DualGovernanceStateTransitions.t.sol index a374c919..d012b7ad 100644 --- a/test/unit/libraries/DualGovernanceStateTransitions.t.sol +++ b/test/unit/libraries/DualGovernanceStateTransitions.t.sol @@ -5,7 +5,9 @@ import {Duration, Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; -import {IEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; +import {IRageQuitEscrow} from "contracts/interfaces/IRageQuitEscrow.sol"; import { State, @@ -50,7 +52,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { rageQuitEthWithdrawalsDelayGrowth: Durations.from(15 days) }) ); - DualGovernanceStateMachine.initialize(_stateMachine, _configProvider, IEscrow(_escrowMasterCopyMock)); + DualGovernanceStateMachine.initialize(_stateMachine, _configProvider, IEscrowBase(_escrowMasterCopyMock)); _setMockRageQuitSupportInBP(0); } @@ -65,7 +67,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { function test_getStateTransition_FromNormalToNormal() external { assertEq(_stateMachine.state, State.Normal); - _setMockRageQuitSupportInBP(3_00); + _setMockRageQuitSupportInBP(2_99); (State current, State next) = _stateMachine.getStateTransition(_configProvider.getDualGovernanceConfig()); @@ -80,7 +82,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { function test_getStateTransition_FromNormalToVetoSignalling() external { assertEq(_stateMachine.state, State.Normal); - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); (State current, State next) = _stateMachine.getStateTransition(_configProvider.getDualGovernanceConfig()); @@ -94,7 +96,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { function test_getStateTransition_FromVetoSignallingToVetoSignalling_VetoSignallingDurationNotPassed() external { _setupVetoSignallingState(); - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); (State current, State next) = _stateMachine.getStateTransition(_configProvider.getDualGovernanceConfig()); @@ -109,7 +111,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { function test_getStateTransition_FromVetoSignallingToVetoSignalling_VetoSignallingReactivationNotPassed() external { - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); // the veto signalling state was entered _setupVetoSignallingState(); @@ -139,7 +141,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { // --- function test_getStateTransition_FromVetoSignallingToRageQuit() external { - _setMockRageQuitSupportInBP(15_01); + _setMockRageQuitSupportInBP(15_00); // the veto signalling state was entered _setupVetoSignallingState(); @@ -160,12 +162,12 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { // --- function test_getStateTransition_FromVetoSignallingDeactivationToVetoSignalling() external { - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); _setupVetoSignallingDeactivationState(); _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignallingDeactivation}); - _setMockRageQuitSupportInBP(15_01); + _setMockRageQuitSupportInBP(15_00); _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignalling}); } @@ -175,12 +177,12 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { // --- function test_getStateTransition_FromVetoSignallingDeactivationToRageQuit() external { - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); _setupVetoSignallingDeactivationState(); _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignallingDeactivation}); - _setMockRageQuitSupportInBP(15_01); + _setMockRageQuitSupportInBP(15_00); _wait(_calcVetoSignallingDuration().plusSeconds(1 seconds)); @@ -192,7 +194,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { // --- function test_getStateTransition_FromVetoSignallingDeactivationToVetoCooldown() external { - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); _setupVetoSignallingDeactivationState(); _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignallingDeactivation}); @@ -207,7 +209,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { // --- function test_getStateTransition_FromVetoSignallingDeactivationToVetoSignallingDeactivation() external { - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); _setupVetoSignallingDeactivationState(); _assertStateMachineTransition({from: State.VetoSignallingDeactivation, to: State.VetoSignallingDeactivation}); @@ -248,7 +250,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { _assertStateMachineTransition({from: State.VetoCooldown, to: State.VetoCooldown}); - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); _wait(_configProvider.VETO_COOLDOWN_DURATION().plusSeconds(1)); @@ -291,7 +293,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { _assertStateMachineTransition({from: State.RageQuit, to: State.RageQuit}); _setMockIsRageQuitFinalized(true); - _setMockRageQuitSupportInBP(3_01); + _setMockRageQuitSupportInBP(3_00); _assertStateMachineTransition({from: State.RageQuit, to: State.VetoSignalling}); } @@ -313,11 +315,11 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { } // --- - // Unset -> assert(false) + // NotInitialized -> assert(false) // --- - function test_getStateTransition_RevertOn_UnsetState() external { - _stateMachine.state = State.Unset; + function test_getStateTransition_RevertOn_NotInitializedState() external { + _stateMachine.state = State.NotInitialized; vm.expectRevert(stdError.assertionError); this.external__getStateTransition(); @@ -353,13 +355,13 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { function _setupRageQuitState() internal { _stateMachine.state = State.RageQuit; _stateMachine.enteredAt = Timestamps.now(); - _stateMachine.rageQuitEscrow = IEscrow(_escrowMasterCopyMock); + _stateMachine.rageQuitEscrow = IRageQuitEscrow(_escrowMasterCopyMock); } function _setMockRageQuitSupportInBP(uint256 bpValue) internal { vm.mockCall( address(_stateMachine.signallingEscrow), - abi.encodeWithSelector(IEscrow.getRageQuitSupport.selector), + abi.encodeWithSelector(ISignallingEscrow.getRageQuitSupport.selector), abi.encode(PercentsD16.fromBasisPoints(bpValue)) ); } @@ -367,7 +369,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { function _setMockIsRageQuitFinalized(bool isRageQuitFinalized) internal { vm.mockCall( address(_stateMachine.rageQuitEscrow), - abi.encodeWithSelector(IEscrow.isRageQuitFinalized.selector), + abi.encodeWithSelector(IRageQuitEscrow.isRageQuitFinalized.selector), abi.encode(isRageQuitFinalized) ); } diff --git a/test/unit/libraries/EmergencyProtection.t.sol b/test/unit/libraries/EmergencyProtection.t.sol index 4fbb01ab..28c238c9 100644 --- a/test/unit/libraries/EmergencyProtection.t.sol +++ b/test/unit/libraries/EmergencyProtection.t.sol @@ -194,7 +194,7 @@ contract EmergencyProtectionTest is UnitTest { } function test_checkEmergencyMode_RevertOn_NotInEmergencyMode() external { - vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, true)); + vm.expectRevert(abi.encodeWithSelector(EmergencyProtection.UnexpectedEmergencyModeState.selector, false)); EmergencyProtection.checkEmergencyMode(ctx, true); } diff --git a/test/unit/libraries/EscrowState.t.sol b/test/unit/libraries/EscrowState.t.sol index c7685468..be0bb1f4 100644 --- a/test/unit/libraries/EscrowState.t.sol +++ b/test/unit/libraries/EscrowState.t.sol @@ -38,8 +38,7 @@ contract EscrowStateUnitTests is UnitTest { function testFuzz_initialize_RevertOn_InvalidState(Duration minAssetsLockDuration) external { _context.state = State.SignallingEscrow; - // TODO: not very informative, maybe need to change to `revert UnexpectedState(self.state);`: UnexpectedState(NotInitialized)[current implementation] => UnexpectedState(SignallingEscrow)[proposed] - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.NotInitialized)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.SignallingEscrow)); EscrowState.initialize(_context, minAssetsLockDuration); } @@ -75,7 +74,7 @@ contract EscrowStateUnitTests is UnitTest { ) external { _context.state = State.NotInitialized; - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.NotInitialized)); EscrowState.startRageQuit(_context, rageQuitExtensionPeriodDuration, rageQuitEthWithdrawalsDelay); } @@ -103,13 +102,17 @@ contract EscrowStateUnitTests is UnitTest { // setMinAssetsLockDuration() // --- - function test_setMinAssetsLockDuration_happyPath(Duration minAssetsLockDuration) external { + function testFuzz_setMinAssetsLockDuration_happyPath( + Duration minAssetsLockDuration, + Duration maxMinAssetsLockDuration + ) external { vm.assume(minAssetsLockDuration != Durations.ZERO); + vm.assume(minAssetsLockDuration <= maxMinAssetsLockDuration); vm.expectEmit(); emit EscrowState.MinAssetsLockDurationSet(minAssetsLockDuration); - EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration); + EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration, maxMinAssetsLockDuration); checkContext({ state: State.NotInitialized, @@ -120,13 +123,25 @@ contract EscrowStateUnitTests is UnitTest { }); } - function test_setMinAssetsLockDuration_RevertWhen_DurationNotChanged(Duration minAssetsLockDuration) external { + function testFuzz_setMinAssetsLockDuration_RevertWhen_DurationNotChanged(Duration minAssetsLockDuration) external { _context.minAssetsLockDuration = minAssetsLockDuration; vm.expectRevert( abi.encodeWithSelector(EscrowState.InvalidMinAssetsLockDuration.selector, minAssetsLockDuration) ); - EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration); + EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration, Durations.from(MAX_DURATION_VALUE)); + } + + function testFuzz_setMinAssetsLockDuration_RevertWhen_DurationGreaterThenMaxMinAssetsLockDuration( + Duration minAssetsLockDuration, + Duration maxMinAssetsLockDuration + ) external { + vm.assume(minAssetsLockDuration > maxMinAssetsLockDuration); + + vm.expectRevert( + abi.encodeWithSelector(EscrowState.InvalidMinAssetsLockDuration.selector, minAssetsLockDuration) + ); + EscrowState.setMinAssetsLockDuration(_context, minAssetsLockDuration, maxMinAssetsLockDuration); } // --- @@ -139,8 +154,11 @@ contract EscrowStateUnitTests is UnitTest { } function test_checkSignallingEscrow_RevertOn_InvalidState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.SignallingEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.NotInitialized)); + EscrowState.checkSignallingEscrow(_context); + _context.state = State.RageQuitEscrow; + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.RageQuitEscrow)); EscrowState.checkSignallingEscrow(_context); } @@ -154,8 +172,11 @@ contract EscrowStateUnitTests is UnitTest { } function test_checkRageQuitEscrow_RevertOn_InvalidState() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.NotInitialized)); + EscrowState.checkRageQuitEscrow(_context); + _context.state = State.SignallingEscrow; + vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedEscrowState.selector, State.SignallingEscrow)); EscrowState.checkRageQuitEscrow(_context); } diff --git a/test/unit/libraries/ExecutableProposals.t.sol b/test/unit/libraries/ExecutableProposals.t.sol index d95c328a..4613d4ac 100644 --- a/test/unit/libraries/ExecutableProposals.t.sol +++ b/test/unit/libraries/ExecutableProposals.t.sol @@ -32,7 +32,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_submit_reverts_if_empty_proposals() external { vm.expectRevert(ExecutableProposals.EmptyCalls.selector); - _proposals.submit(proposer, address(0), new ExternalCall[](0), "Empty calls"); + _proposals.submit(address(0), new ExternalCall[](0)); } function test_submit_proposal() external { @@ -41,14 +41,13 @@ contract ExecutableProposalsUnitTests is UnitTest { ExternalCall[] memory calls = _getMockTargetRegularStaffCalls(address(_targetMock)); uint256 expectedProposalId = proposalsCount + PROPOSAL_ID_OFFSET; - string memory description = "Regular staff calls"; vm.expectEmit(); - emit ExecutableProposals.ProposalSubmitted(expectedProposalId, proposer, address(_executor), calls, description); + emit ExecutableProposals.ProposalSubmitted(expectedProposalId, address(_executor), calls); vm.recordLogs(); - _proposals.submit(proposer, address(_executor), calls, description); + _proposals.submit(address(_executor), calls); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 1); @@ -74,7 +73,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function testFuzz_schedule_proposal(Duration delay) external { vm.assume(delay > Durations.ZERO && delay.toSeconds() <= MAX_DURATION_VALUE); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 expectedProposalId = 1; ExecutableProposals.Proposal memory proposal = _proposals.proposals[expectedProposalId]; @@ -101,23 +100,31 @@ contract ExecutableProposalsUnitTests is UnitTest { function testFuzz_cannot_schedule_unsubmitted_proposal(uint256 proposalId) external { vm.assume(proposalId > 0); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotSubmitted.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.NotExist + ) + ); _proposals.schedule(proposalId, Durations.ZERO); } function test_cannot_schedule_proposal_twice() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = 1; _proposals.schedule(proposalId, Durations.ZERO); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotSubmitted.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.Scheduled + ) + ); _proposals.schedule(proposalId, Durations.ZERO); } function testFuzz_cannot_schedule_proposal_before_delay_passed(Duration delay) external { vm.assume(delay > Durations.ZERO && delay.toSeconds() <= MAX_DURATION_VALUE); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); _wait(delay.minusSeconds(1 seconds)); @@ -128,19 +135,23 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cannot_schedule_cancelled_proposal() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); _proposals.cancelAll(); uint256 proposalId = _proposals.getProposalsCount(); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotSubmitted.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.Cancelled + ) + ); _proposals.schedule(proposalId, Durations.ZERO); } function testFuzz_execute_proposal(Duration delay) external { vm.assume(delay > Durations.ZERO && delay.toSeconds() <= MAX_DURATION_VALUE); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); @@ -171,41 +182,57 @@ contract ExecutableProposalsUnitTests is UnitTest { function testFuzz_cannot_execute_unsubmitted_proposal(uint256 proposalId) external { vm.assume(proposalId > 0); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.NotExist + ) + ); _proposals.execute(proposalId, Durations.ZERO); } function test_cannot_execute_unscheduled_proposal() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.Submitted + ) + ); _proposals.execute(proposalId, Durations.ZERO); } function test_cannot_execute_twice() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); _proposals.execute(proposalId, Durations.ZERO); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.Executed + ) + ); _proposals.execute(proposalId, Durations.ZERO); } function test_cannot_execute_cancelled_proposal() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); _proposals.cancelAll(); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotScheduled.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.Cancelled + ) + ); _proposals.execute(proposalId, Durations.ZERO); } function testFuzz_cannot_execute_before_delay_passed(Duration delay) external { vm.assume(delay > Durations.ZERO && delay.toSeconds() <= MAX_DURATION_VALUE); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); @@ -216,8 +243,8 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cancel_all_proposals() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalsCount = _proposals.getProposalsCount(); @@ -233,7 +260,7 @@ contract ExecutableProposalsUnitTests is UnitTest { // TODO: change this test completely to use getters function test_get_proposal_info_and_external_calls() external { ExternalCall[] memory expectedCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); - _proposals.submit(proposer, address(_executor), expectedCalls, ""); + _proposals.submit(address(_executor), expectedCalls); uint256 proposalId = _proposals.getProposalsCount(); ITimelock.ProposalDetails memory proposalDetails = _proposals.getProposalDetails(proposalId); @@ -295,7 +322,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_get_cancelled_proposal() external { ExternalCall[] memory expectedCalls = _getMockTargetRegularStaffCalls(address(_targetMock)); - _proposals.submit(proposer, address(_executor), expectedCalls, ""); + _proposals.submit(address(_executor), expectedCalls); uint256 proposalId = _proposals.getProposalsCount(); ITimelock.ProposalDetails memory proposalDetails = _proposals.getProposalDetails(proposalId); @@ -336,26 +363,34 @@ contract ExecutableProposalsUnitTests is UnitTest { } function testFuzz_get_not_existing_proposal(uint256 proposalId) external { - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotFound.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.NotExist + ) + ); _proposals.getProposalDetails(proposalId); - vm.expectRevert(abi.encodeWithSelector(ExecutableProposals.ProposalNotFound.selector, proposalId)); + vm.expectRevert( + abi.encodeWithSelector( + ExecutableProposals.UnexpectedProposalStatus.selector, proposalId, ProposalStatus.NotExist + ) + ); _proposals.getProposalCalls(proposalId); } function test_count_proposals() external { assertEq(_proposals.getProposalsCount(), 0); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.getProposalsCount(), 1); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.getProposalsCount(), 2); _proposals.schedule(1, Durations.ZERO); assertEq(_proposals.getProposalsCount(), 2); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.getProposalsCount(), 3); _proposals.schedule(2, Durations.ZERO); @@ -364,7 +399,7 @@ contract ExecutableProposalsUnitTests is UnitTest { _proposals.execute(1, Durations.ZERO); assertEq(_proposals.getProposalsCount(), 3); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.getProposalsCount(), 4); _proposals.cancelAll(); @@ -373,7 +408,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_can_execute_proposal() external { Duration delay = Durations.from(100 seconds); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); assert(!_proposals.canExecute(proposalId, Durations.ZERO)); @@ -392,7 +427,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_can_not_execute_cancelled_proposal() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); @@ -403,18 +438,18 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_cancelAll_DoesNotModifyStateOfExecutedProposals() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.getProposalsCount(), 1); uint256 executedProposalId = 1; _proposals.schedule(executedProposalId, Durations.ZERO); _proposals.execute(executedProposalId, Durations.ZERO); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.getProposalsCount(), 2); uint256 scheduledProposalId = 2; _proposals.schedule(scheduledProposalId, Durations.ZERO); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.getProposalsCount(), 3); uint256 submittedProposalId = 3; @@ -436,7 +471,7 @@ contract ExecutableProposalsUnitTests is UnitTest { function test_can_schedule_proposal() external { Duration delay = Durations.from(100 seconds); - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); assert(!_proposals.canSchedule(proposalId, delay)); @@ -451,7 +486,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function test_can_not_schedule_cancelled_proposal() external { - _proposals.submit(proposer, address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); + _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.getProposalsCount(); assert(_proposals.canSchedule(proposalId, Durations.ZERO)); diff --git a/test/unit/libraries/Proposers.t.sol b/test/unit/libraries/Proposers.t.sol index 7eb2ea42..c64ce860 100644 --- a/test/unit/libraries/Proposers.t.sol +++ b/test/unit/libraries/Proposers.t.sol @@ -60,6 +60,51 @@ contract ProposersLibraryUnitTests is UnitTest { _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); } + // --- + // setProposerExecutor() + // --- + + function test_setProposerExecutor_HappyPath() external { + Proposers.Proposer memory defaultProposer = _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + assertNotEq(defaultProposer.executor, _ADMIN_EXECUTOR); + + uint256 adminExecutorRefsCountBefore = _proposers.executorRefsCounts[_ADMIN_EXECUTOR]; + uint256 defaultExecutorRefsCountBefore = _proposers.executorRefsCounts[_DEFAULT_EXECUTOR]; + + vm.expectEmit(true, true, false, false); + emit Proposers.ProposerExecutorSet(defaultProposer.account, _ADMIN_EXECUTOR); + + _proposers.setProposerExecutor(defaultProposer.account, _ADMIN_EXECUTOR); + + defaultProposer = _proposers.getProposer(_DEFAULT_PROPOSER); + assertEq(defaultProposer.executor, _ADMIN_EXECUTOR); + + // check executor references updated properly + assertEq(_proposers.executorRefsCounts[_DEFAULT_EXECUTOR], defaultExecutorRefsCountBefore - 1); + assertEq(_proposers.executorRefsCounts[_ADMIN_EXECUTOR], adminExecutorRefsCountBefore + 1); + } + + function test_setProposerExecutor_RevertOn_ZeroAddressExecutor() external { + Proposers.Proposer memory defaultProposer = _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.InvalidExecutor.selector, address(0))); + this.external__setProposerExecutor(defaultProposer.account, address(0)); + } + + function test_setProposerExecutor_RevertOn_SameExecutorAddress() external { + Proposers.Proposer memory defaultProposer = _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.InvalidExecutor.selector, _DEFAULT_EXECUTOR)); + this.external__setProposerExecutor(defaultProposer.account, _DEFAULT_EXECUTOR); + } + + function test_setProposerExecutor_RevertOn_NonRegisteredPropsoer() external { + assertEq(_proposers.proposers.length, 0); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); + this.external__setProposerExecutor(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + } + // --- // unregister() // --- @@ -68,35 +113,35 @@ contract ProposersLibraryUnitTests is UnitTest { _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); assertEq(_proposers.proposers.length, 1); - assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); - assertTrue(_proposers.isExecutor(_ADMIN_EXECUTOR)); + assertTrue(_proposers.isRegisteredProposer(_ADMIN_PROPOSER)); + assertTrue(_proposers.isRegisteredExecutor(_ADMIN_EXECUTOR)); _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); assertEq(_proposers.proposers.length, 2); - assertTrue(_proposers.isProposer(_DEFAULT_PROPOSER)); - assertTrue(_proposers.isExecutor(_DEFAULT_EXECUTOR)); + assertTrue(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); + assertTrue(_proposers.isRegisteredExecutor(_DEFAULT_EXECUTOR)); _proposers.unregister(_DEFAULT_PROPOSER); assertEq(_proposers.proposers.length, 1); - assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); - assertFalse(_proposers.isExecutor(_DEFAULT_EXECUTOR)); + assertFalse(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); + assertFalse(_proposers.isRegisteredExecutor(_DEFAULT_EXECUTOR)); _proposers.unregister(_ADMIN_PROPOSER); assertEq(_proposers.proposers.length, 0); - assertFalse(_proposers.isProposer(_ADMIN_PROPOSER)); - assertFalse(_proposers.isExecutor(_ADMIN_EXECUTOR)); + assertFalse(_proposers.isRegisteredProposer(_ADMIN_PROPOSER)); + assertFalse(_proposers.isRegisteredExecutor(_ADMIN_EXECUTOR)); } function test_unregister_RevertOn_ProposerIsNotRegistered() external { - assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); + assertFalse(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); _proposers.unregister(_DEFAULT_PROPOSER); _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); - assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); - assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); + assertFalse(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); + assertTrue(_proposers.isRegisteredProposer(_ADMIN_PROPOSER)); vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); _proposers.unregister(_DEFAULT_PROPOSER); @@ -104,7 +149,7 @@ contract ProposersLibraryUnitTests is UnitTest { function test_uregister_Emit_ProposerUnregistered() external { _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); - assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); + assertTrue(_proposers.isRegisteredProposer(_ADMIN_PROPOSER)); vm.expectEmit(true, true, true, false); emit Proposers.ProposerUnregistered(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); @@ -118,14 +163,14 @@ contract ProposersLibraryUnitTests is UnitTest { function test_getProposer_HappyPath() external { _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); - assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); + assertTrue(_proposers.isRegisteredProposer(_ADMIN_PROPOSER)); Proposers.Proposer memory adminProposer = _proposers.getProposer(_ADMIN_PROPOSER); assertEq(adminProposer.account, _ADMIN_PROPOSER); assertEq(adminProposer.executor, _ADMIN_EXECUTOR); _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); - assertTrue(_proposers.isProposer(_DEFAULT_PROPOSER)); + assertTrue(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); Proposers.Proposer memory defaultProposer = _proposers.getProposer(_DEFAULT_PROPOSER); assertEq(defaultProposer.account, _DEFAULT_PROPOSER); @@ -133,14 +178,14 @@ contract ProposersLibraryUnitTests is UnitTest { } function test_getProposer_RevertOn_RetrievingUnregisteredProposer() external { - assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); + assertFalse(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); _proposers.getProposer(_DEFAULT_PROPOSER); _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); - assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); - assertFalse(_proposers.isProposer(_DEFAULT_PROPOSER)); + assertTrue(_proposers.isRegisteredProposer(_ADMIN_PROPOSER)); + assertFalse(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); vm.expectRevert(abi.encodeWithSelector(Proposers.ProposerNotRegistered.selector, _DEFAULT_PROPOSER)); _proposers.getProposer(_DEFAULT_PROPOSER); @@ -155,7 +200,7 @@ contract ProposersLibraryUnitTests is UnitTest { assertEq(emptyProposers.length, 0); _proposers.register(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); - assertTrue(_proposers.isProposer(_ADMIN_PROPOSER)); + assertTrue(_proposers.isRegisteredProposer(_ADMIN_PROPOSER)); Proposers.Proposer[] memory allProposers = _proposers.getAllProposers(); assertEq(allProposers.length, 1); @@ -164,7 +209,7 @@ contract ProposersLibraryUnitTests is UnitTest { assertEq(allProposers[0].executor, _ADMIN_EXECUTOR); _proposers.register(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); - assertTrue(_proposers.isProposer(_DEFAULT_PROPOSER)); + assertTrue(_proposers.isRegisteredProposer(_DEFAULT_PROPOSER)); allProposers = _proposers.getAllProposers(); assertEq(allProposers.length, 2); @@ -253,4 +298,97 @@ contract ProposersLibraryUnitTests is UnitTest { assertEq(allProposers[1].account, celine); assertEq(allProposers[1].executor, celineExecutor); } + + // --- + // checkRegisteredExecutor() + // --- + + function test_checkRegisteredExecutor_HappyPath() external { + address notExecutor = makeAddr("notExecutor"); + + assertEq(_proposers.proposers.length, 0); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _ADMIN_EXECUTOR)); + this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _DEFAULT_EXECUTOR)); + this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); + this.external__checkRegisteredExecutor(notExecutor); + + // --- + // register admin proposer + // --- + + _registerProposer(_ADMIN_PROPOSER, _ADMIN_EXECUTOR); + + this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _DEFAULT_EXECUTOR)); + this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); + this.external__checkRegisteredExecutor(notExecutor); + + // --- + // register default proposer + // --- + + _registerProposer(_DEFAULT_PROPOSER, _DEFAULT_EXECUTOR); + + this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); + + this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); + this.external__checkRegisteredExecutor(notExecutor); + + // --- + // change default proposer's executor on admin + // --- + + _proposers.setProposerExecutor(_DEFAULT_PROPOSER, _ADMIN_EXECUTOR); + + this.external__checkRegisteredExecutor(_ADMIN_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, _DEFAULT_EXECUTOR)); + this.external__checkRegisteredExecutor(_DEFAULT_EXECUTOR); + + vm.expectRevert(abi.encodeWithSelector(Proposers.ExecutorNotRegistered.selector, notExecutor)); + this.external__checkRegisteredExecutor(notExecutor); + } + + // --- + // Helper Methods + // --- + + function _registerProposer( + address account, + address executor + ) private returns (Proposers.Proposer memory proposer) { + uint256 proposersCountBefore = _proposers.proposers.length; + uint256 executorRefsCountBefore = _proposers.executorRefsCounts[executor]; + + _proposers.register(account, executor); + + uint256 proposersCountAfter = _proposers.proposers.length; + uint256 executorRefsCountAfter = _proposers.executorRefsCounts[executor]; + + assertEq(proposersCountAfter, proposersCountBefore + 1); + assertEq(executorRefsCountAfter, executorRefsCountBefore + 1); + + proposer = _proposers.getProposer(account); + + assertEq(proposer.account, account, "Invalid proposer account"); + assertEq(proposer.executor, executor, "Invalid proposer executor"); + } + + function external__setProposerExecutor(address proposer, address executor) external { + _proposers.setProposerExecutor(proposer, executor); + } + + function external__checkRegisteredExecutor(address executor) external { + _proposers.checkRegisteredExecutor(executor); + } } diff --git a/test/unit/libraries/Resealer.t.sol b/test/unit/libraries/Resealer.t.sol new file mode 100644 index 00000000..f06c9903 --- /dev/null +++ b/test/unit/libraries/Resealer.t.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Resealer} from "contracts/libraries/Resealer.sol"; +import {IResealManager} from "contracts/interfaces/IResealManager.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; + +contract ResealerTest is UnitTest { + using Resealer for Resealer.Context; + + Resealer.Context ctx; + + address resealManager = makeAddr("resealManager"); + address resealCommittee = makeAddr("resealCommittee"); + + function setUp() external { + ctx.resealManager = IResealManager(resealManager); + ctx.resealCommittee = resealCommittee; + } + + function testFuzz_setResealManager_HappyPath(IResealManager newResealManager) external { + vm.assume(newResealManager != ctx.resealManager && address(newResealManager) != address(0)); + + vm.expectEmit(); + emit Resealer.ResealManagerSet(newResealManager); + this.external__setResealManager(newResealManager); + + assertEq(address(ctx.resealManager), address(newResealManager)); + } + + function test_setResealManager_RevertOn_InvalidResealManager() external { + vm.expectRevert(abi.encodeWithSelector(Resealer.InvalidResealManager.selector, address(ctx.resealManager))); + this.external__setResealManager(ctx.resealManager); + + vm.expectRevert(abi.encodeWithSelector(Resealer.InvalidResealManager.selector, address(0))); + this.external__setResealManager(IResealManager(address(0))); + } + + function testFuzz_setResealCommittee_HappyPath(address newResealCommittee) external { + vm.assume(newResealCommittee != ctx.resealCommittee); + + vm.expectEmit(); + emit Resealer.ResealCommitteeSet(newResealCommittee); + this.external__setResealCommittee(newResealCommittee); + + assertEq(ctx.resealCommittee, newResealCommittee); + } + + function test_setResealCommittee_RevertOn_InvalidResealCommittee() external { + vm.expectRevert(abi.encodeWithSelector(Resealer.InvalidResealCommittee.selector, ctx.resealCommittee)); + this.external__setResealCommittee(ctx.resealCommittee); + } + + function test_checkCallerIsResealCommittee_HappyPath() external { + vm.prank(resealCommittee); + this.external__checkCallerIsResealCommittee(); + } + + function testFuzz_checkCallerIsResealCommittee_RevertOn_Stranger(address stranger) external { + vm.assume(stranger != resealCommittee); + + vm.prank(stranger); + vm.expectRevert(abi.encodeWithSelector(Resealer.CallerIsNotResealCommittee.selector, stranger)); + this.external__checkCallerIsResealCommittee(); + } + + function external__checkCallerIsResealCommittee() external view { + ctx.checkCallerIsResealCommittee(); + } + + function external__setResealCommittee(address newResealCommittee) external { + ctx.setResealCommittee(newResealCommittee); + } + + function external__setResealManager(IResealManager newResealManager) external { + ctx.setResealManager(newResealManager); + } +} diff --git a/test/unit/libraries/TimelockState.t.sol b/test/unit/libraries/TimelockState.t.sol index 9931bbdc..d200e542 100644 --- a/test/unit/libraries/TimelockState.t.sol +++ b/test/unit/libraries/TimelockState.t.sol @@ -12,6 +12,7 @@ contract TimelockStateUnitTests is UnitTest { TimelockState.Context internal _timelockState; address internal governance = makeAddr("governance"); + address internal adminExecutor = makeAddr("adminExecutor"); Duration internal afterSubmitDelay = Durations.from(1 days); Duration internal afterScheduleDelay = Durations.from(2 days); @@ -20,6 +21,7 @@ contract TimelockStateUnitTests is UnitTest { function setUp() external { TimelockState.setGovernance(_timelockState, governance); + TimelockState.setAdminExecutor(_timelockState, adminExecutor); TimelockState.setAfterSubmitDelay(_timelockState, afterSubmitDelay, maxAfterSubmitDelay); TimelockState.setAfterScheduleDelay(_timelockState, afterScheduleDelay, maxAfterScheduleDelay); } @@ -87,6 +89,25 @@ contract TimelockStateUnitTests is UnitTest { TimelockState.setAfterScheduleDelay(_timelockState, afterScheduleDelay, maxAfterScheduleDelay); } + function testFuzz_setAdminExecutor_HappyPath(address newAdminExecutor) external { + vm.assume(newAdminExecutor != address(0) && newAdminExecutor != adminExecutor); + + vm.expectEmit(); + emit TimelockState.AdminExecutorSet(newAdminExecutor); + + TimelockState.setAdminExecutor(_timelockState, newAdminExecutor); + } + + function test_setAdminExecutor_RevertOn_ZeroAddress() external { + vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidAdminExecutor.selector, address(0))); + TimelockState.setAdminExecutor(_timelockState, address(0)); + } + + function test_setAdminExecutor_RevertOn_SameAddress() external { + vm.expectRevert(abi.encodeWithSelector(TimelockState.InvalidAdminExecutor.selector, adminExecutor)); + TimelockState.setAdminExecutor(_timelockState, adminExecutor); + } + function testFuzz_getAfterSubmitDelay_HappyPath(Duration newAfterSubmitDelay) external { TimelockState.setAfterSubmitDelay(_timelockState, newAfterSubmitDelay, newAfterSubmitDelay); assertEq(TimelockState.getAfterSubmitDelay(_timelockState), newAfterSubmitDelay); diff --git a/test/unit/libraries/WithdrawalBatchesQueue.t.sol b/test/unit/libraries/WithdrawalBatchesQueue.t.sol index bd8f11fc..b524a80c 100644 --- a/test/unit/libraries/WithdrawalBatchesQueue.t.sol +++ b/test/unit/libraries/WithdrawalBatchesQueue.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {stdError} from "forge-std/StdError.sol"; import {UnitTest} from "test/utils/unit-test.sol"; import {WithdrawalsBatchesQueue, State} from "contracts/libraries/WithdrawalsBatchesQueue.sol"; @@ -15,7 +16,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { // --- function test_open_HappyPath() external { - assertEq(_batchesQueue.info.state, State.Absent); + assertEq(_batchesQueue.info.state, State.NotInitialized); assertEq(_batchesQueue.batches.length, 0); _batchesQueue.open(_DEFAULT_BOUNDARY_UNST_ETH_ID); @@ -30,12 +31,16 @@ contract WithdrawalsBatchesQueueTest is UnitTest { _batchesQueue.info.state = State.Opened; assertEq(_batchesQueue.info.state, State.Opened); - vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalsBatchesQueueIsNotInAbsentState.selector); + vm.expectRevert( + abi.encodeWithSelector( + WithdrawalsBatchesQueue.UnexpectedWithdrawalsBatchesQueueState.selector, State.Opened + ) + ); _batchesQueue.open(_DEFAULT_BOUNDARY_UNST_ETH_ID); } function test_open_Emit_WithdrawalsBatchesQueueOpened() external { - assertEq(_batchesQueue.info.state, State.Absent); + assertEq(_batchesQueue.info.state, State.NotInitialized); vm.expectEmit(true, false, false, false); emit WithdrawalsBatchesQueue.WithdrawalsBatchesQueueOpened(_DEFAULT_BOUNDARY_UNST_ETH_ID); @@ -125,8 +130,22 @@ contract WithdrawalsBatchesQueueTest is UnitTest { ); } - function test_addUnstETHIds_RevertOn_QueueNotInOpenedState() external { - vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalsBatchesQueueIsNotInOpenedState.selector); + function test_addUnstETHIds_RevertOn_QueueInNotInitializedState() external { + vm.expectRevert( + abi.encodeWithSelector( + WithdrawalsBatchesQueue.UnexpectedWithdrawalsBatchesQueueState.selector, State.NotInitialized + ) + ); + _batchesQueue.addUnstETHIds(new uint256[](0)); + } + + function test_addUnstETHIds_RevertOn_QueueInClosedState() external { + _batchesQueue.info.state = State.Closed; + vm.expectRevert( + abi.encodeWithSelector( + WithdrawalsBatchesQueue.UnexpectedWithdrawalsBatchesQueueState.selector, State.Closed + ) + ); _batchesQueue.addUnstETHIds(new uint256[](0)); } @@ -147,7 +166,8 @@ contract WithdrawalsBatchesQueueTest is UnitTest { // violate the order of the NFT ids unstETHIds[2] = unstETHIds[3]; - vm.expectRevert(); + vm.expectRevert(stdError.assertionError); + _batchesQueue.addUnstETHIds(unstETHIds); } @@ -292,6 +312,48 @@ contract WithdrawalsBatchesQueueTest is UnitTest { _batchesQueue.claimNextBatch(1); } + function test_claimNextBatch_RevertOn_NothingToClaim() external { + _openBatchesQueue(); + + uint256 unstETHIdsCount = 5; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); + + _batchesQueue.addUnstETHIds(unstETHIds); + assertEq(_batchesQueue.info.totalUnstETHIdsCount, unstETHIdsCount); + assertEq(_batchesQueue.batches.length, 2); + + uint256 maxUnstETHIdsCount = 5; + uint256[] memory claimedIds = _batchesQueue.claimNextBatch(maxUnstETHIdsCount); + + assertEq(claimedIds.length, maxUnstETHIdsCount); + assertEq(_batchesQueue.info.totalUnstETHIdsClaimed, maxUnstETHIdsCount); + + vm.expectRevert(WithdrawalsBatchesQueue.EmptyBatch.selector); + _batchesQueue.claimNextBatch(100); + } + + function test_claimNextBatch_RevertOn_AccountingError_TotalUnstETHClaimed_GT_TotalUnstETHCount() external { + _openBatchesQueue(); + + _batchesQueue.info.totalUnstETHIdsClaimed = 1; + vm.expectRevert(stdError.arithmeticError); + _batchesQueue.claimNextBatch(100); + } + + function test_claimNextBatch_RevertOn_AccountingError_LastClaimedBatchIndexOutOfArrayBounds() external { + _openBatchesQueue(); + + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: 1, firstUnstETHId: firstUnstETHId}); + + _batchesQueue.addUnstETHIds(unstETHIds); + + _batchesQueue.info.lastClaimedBatchIndex = 2; + vm.expectRevert(stdError.indexOOBError); + _batchesQueue.claimNextBatch(100); + } + // --- // close() // --- @@ -305,13 +367,21 @@ contract WithdrawalsBatchesQueueTest is UnitTest { } function test_close_RevertOn_QueueNotInOpenedState() external { - vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalsBatchesQueueIsNotInOpenedState.selector); + vm.expectRevert( + abi.encodeWithSelector( + WithdrawalsBatchesQueue.UnexpectedWithdrawalsBatchesQueueState.selector, State.NotInitialized + ) + ); _batchesQueue.close(); _batchesQueue.open({boundaryUnstETHId: 1}); _batchesQueue.close(); - vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalsBatchesQueueIsNotInOpenedState.selector); + vm.expectRevert( + abi.encodeWithSelector( + WithdrawalsBatchesQueue.UnexpectedWithdrawalsBatchesQueueState.selector, State.Closed + ) + ); _batchesQueue.close(); } @@ -329,7 +399,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { // calcRequestAmounts() // --- - function test_calcRequestAmounts_HappyPath_WithoutReminder() external { + function test_calcRequestAmounts_HappyPath_WithoutRemainder() external { _openBatchesQueue(); assertEq(_batchesQueue.info.state, State.Opened); @@ -354,7 +424,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { } } - function test_calcRequestAmounts_HappyPath_WithReminder() external { + function test_calcRequestAmounts_HappyPath_WithRemainder() external { _openBatchesQueue(); assertEq(_batchesQueue.info.state, State.Opened); @@ -380,6 +450,13 @@ contract WithdrawalsBatchesQueueTest is UnitTest { } } + function test_calcRequestAmounts_RevertOn_MaxRequestAmountIsZero() external { + _openBatchesQueue(); + + vm.expectRevert(stdError.divisionError); + WithdrawalsBatchesQueue.calcRequestAmounts({minRequestAmount: 1, maxRequestAmount: 0, remainingAmount: 100}); + } + // --- // getNextWithdrawalsBatches() // --- @@ -481,36 +558,75 @@ contract WithdrawalsBatchesQueueTest is UnitTest { assertEq(_batchesQueue.info.totalUnstETHIdsClaimed, 0); } + function test_getNextWithdrawalsBatches_RevertOn_AccountingError_TotalUnstETHClaimed_GT_TotalUnstETHCount() + external + { + _openBatchesQueue(); + + _batchesQueue.info.totalUnstETHIdsClaimed = 1; + vm.expectRevert(stdError.arithmeticError); + _batchesQueue.getNextWithdrawalsBatches(10); + } + + function test_getNextWithdrawalsBatches_RevertOn_AccountingError_LastClaimedBatchIndexOutOfArrayBounds() external { + _openBatchesQueue(); + + _batchesQueue.info.lastClaimedBatchIndex = 2; + vm.expectRevert(stdError.indexOOBError); + _batchesQueue.getNextWithdrawalsBatches(10); + } + // --- - // getLastClaimedOrBoundaryUnstETHId() + // getBoundaryUnstETHId() // --- - function test_getLastClaimedOrBoundaryUnstETHId_HappyPath_EmptyQueueReturnsBoundaryUnstETHId() external { + function test_getBoundaryUnstETHId_HappyPath_EmptyQueue() external { _openBatchesQueue(); - assertEq(_batchesQueue.getLastClaimedOrBoundaryUnstETHId(), _DEFAULT_BOUNDARY_UNST_ETH_ID); + _batchesQueue.close(); + assertEq(_batchesQueue.getBoundaryUnstETHId(), _DEFAULT_BOUNDARY_UNST_ETH_ID); } - function test_getLastClaimedOrBoundaryUnstETHId_HappyPath_NotEmptyQueueReturnsLastClaimedUnstETHId() external { + function test_getBoundaryUnstETHId_HappyPath_NotEmptyQueue() external { _openBatchesQueue(); + uint256 unstETHIdsCount = 5; uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); _batchesQueue.addUnstETHIds(unstETHIds); assertEq(_batchesQueue.info.totalUnstETHIdsCount, 5); - assertEq(_batchesQueue.getLastClaimedOrBoundaryUnstETHId(), _DEFAULT_BOUNDARY_UNST_ETH_ID); + _batchesQueue.close(); - uint256 maxUnstETHIdsCount = 3; - _batchesQueue.claimNextBatch(maxUnstETHIdsCount); - assertEq(_batchesQueue.getLastClaimedOrBoundaryUnstETHId(), unstETHIds[2]); + assertEq(_batchesQueue.getBoundaryUnstETHId(), _DEFAULT_BOUNDARY_UNST_ETH_ID); + } - _batchesQueue.claimNextBatch(maxUnstETHIdsCount); - assertEq(_batchesQueue.getLastClaimedOrBoundaryUnstETHId(), unstETHIds[unstETHIds.length - 1]); + function test_getBoundaryUnstETHId_RevertOn_NotInitializedQueueState() external { + vm.expectRevert(stdError.indexOOBError); + _batchesQueue.getBoundaryUnstETHId(); } - function test_getLastClaimedOrBoundaryUnstETHId_RevertOn_AbsentQueueState() external { - vm.expectRevert(WithdrawalsBatchesQueue.WithdrawalsBatchesQueueIsInAbsentState.selector); - _batchesQueue.getLastClaimedOrBoundaryUnstETHId(); + // --- + // isAllBatchesClaimed() + // --- + + function testFuzz_isAllBatchesClaimed_HappyPath_ReturnsTrue(uint64 count) external { + _batchesQueue.info.totalUnstETHIdsClaimed = count; + _batchesQueue.info.totalUnstETHIdsCount = count; + + bool res = _batchesQueue.isAllBatchesClaimed(); + assertTrue(res); + } + + function testFuzz_isAllBatchesClaimed_HappyPath_ReturnsFalse( + uint64 totalUnstETHClaimed, + uint64 totalUnstETHCount + ) external { + vm.assume(totalUnstETHClaimed != totalUnstETHCount); + _batchesQueue.info.totalUnstETHIdsClaimed = totalUnstETHClaimed; + _batchesQueue.info.totalUnstETHIdsCount = totalUnstETHCount; + + bool res = _batchesQueue.isAllBatchesClaimed(); + assertFalse(res); } // --- @@ -548,6 +664,13 @@ contract WithdrawalsBatchesQueueTest is UnitTest { assertEq(totalUnclaimed, 0); } + function testFuzz_getTotalUnclaimedUnstETHIdsCount_RevertOn_AccountingError_IncorrectTotals() external { + _batchesQueue.info.totalUnstETHIdsClaimed = 1; + + vm.expectRevert(stdError.arithmeticError); + _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + } + // --- // isAllBatchesClaimed() // --- diff --git a/test/utils/SetupDeployment.sol b/test/utils/SetupDeployment.sol index 4e08face..9e1d1201 100644 --- a/test/utils/SetupDeployment.sol +++ b/test/utils/SetupDeployment.sol @@ -8,16 +8,13 @@ import {Test} from "forge-std/Test.sol"; // --- import {PercentsD16} from "contracts/types/PercentD16.sol"; -import {Timestamps} from "contracts/types/Timestamp.sol"; import {Durations, Duration} from "contracts/types/Duration.sol"; // --- // Interfaces // --- import {ITimelock} from "contracts/interfaces/ITimelock.sol"; -import {IGovernance} from "contracts/interfaces/IGovernance.sol"; import {IResealManager} from "contracts/interfaces/IResealManager.sol"; -import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; // --- // Contracts @@ -27,21 +24,15 @@ 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/ImmutableDualGovernanceConfigProvider.sol"; -import {ResealCommittee} from "contracts/committees/ResealCommittee.sol"; import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; // --- @@ -50,6 +41,8 @@ import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommitte import {Random} from "./random.sol"; import {LidoUtils} from "./lido-utils.sol"; +import {DeployConfig, LidoContracts} from "../../scripts/deploy/Config.sol"; +import {DeployedContracts, DGContractsDeployment} from "../../scripts/deploy/ContractsDeployment.sol"; // --- // Lido Addresses @@ -61,6 +54,10 @@ abstract contract SetupDeployment is Test { // Helpers // --- + DeployConfig internal dgDeployConfig; + LidoContracts internal lidoAddresses; + DeployedContracts internal contracts; + Random.Context internal _random; LidoUtils.Context internal _lido; @@ -68,7 +65,8 @@ abstract contract SetupDeployment is Test { // Emergency Protected Timelock Deployment Parameters // --- - Duration internal immutable _MIN_EXECUTION_DELAY = Durations.from(0 seconds); + // TODO: consider to use non zero value for the more realistic setup in the tests + Duration internal immutable _MIN_EXECUTION_DELAY = Durations.ZERO; Duration internal immutable _AFTER_SUBMIT_DELAY = Durations.from(3 days); Duration internal immutable _MAX_AFTER_SUBMIT_DELAY = Durations.from(45 days); @@ -110,8 +108,8 @@ abstract contract SetupDeployment is Test { Executor internal _adminExecutor; EmergencyProtectedTimelock internal _timelock; TimelockedGovernance internal _emergencyGovernance; - EmergencyActivationCommittee internal _emergencyActivationCommittee; - EmergencyExecutionCommittee internal _emergencyExecutionCommittee; + address internal _emergencyActivationCommittee; + address internal _emergencyExecutionCommittee; // --- // Dual Governance Contracts @@ -120,7 +118,7 @@ abstract contract SetupDeployment is Test { DualGovernance internal _dualGovernance; ImmutableDualGovernanceConfigProvider internal _dualGovernanceConfigProvider; - ResealCommittee internal _resealCommittee; + address internal _resealCommittee; TiebreakerCoreCommittee internal _tiebreakerCoreCommittee; TiebreakerSubCommittee[] internal _tiebreakerSubCommittees; @@ -143,6 +141,57 @@ abstract contract SetupDeployment is Test { _lido = lido; _random = random; _targetMock = new TargetMock(); + + _emergencyActivationCommittee = makeAddr("EMERGENCY_ACTIVATION_COMMITTEE"); + _emergencyExecutionCommittee = makeAddr("EMERGENCY_EXECUTION_COMMITTEE"); + _resealCommittee = makeAddr("RESEAL_COMMITTEE"); + + dgDeployConfig.MIN_EXECUTION_DELAY = dgDeployConfig.AFTER_SUBMIT_DELAY = _AFTER_SUBMIT_DELAY; + dgDeployConfig.MAX_AFTER_SUBMIT_DELAY = _MAX_AFTER_SUBMIT_DELAY; + dgDeployConfig.AFTER_SCHEDULE_DELAY = _AFTER_SCHEDULE_DELAY; + dgDeployConfig.MAX_AFTER_SCHEDULE_DELAY = _MAX_AFTER_SCHEDULE_DELAY; + dgDeployConfig.EMERGENCY_MODE_DURATION = _EMERGENCY_MODE_DURATION; + dgDeployConfig.MAX_EMERGENCY_MODE_DURATION = _MAX_EMERGENCY_MODE_DURATION; + dgDeployConfig.EMERGENCY_PROTECTION_DURATION = _EMERGENCY_PROTECTION_DURATION; + dgDeployConfig.MAX_EMERGENCY_PROTECTION_DURATION = _MAX_EMERGENCY_PROTECTION_DURATION; + + dgDeployConfig.EMERGENCY_ACTIVATION_COMMITTEE = _emergencyActivationCommittee; + dgDeployConfig.EMERGENCY_EXECUTION_COMMITTEE = _emergencyExecutionCommittee; + + dgDeployConfig.TIEBREAKER_CORE_QUORUM = TIEBREAKER_SUB_COMMITTEES_COUNT; + dgDeployConfig.TIEBREAKER_EXECUTION_DELAY = TIEBREAKER_EXECUTION_DELAY; + dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_COUNT = TIEBREAKER_SUB_COMMITTEES_COUNT; + dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_1_MEMBERS = + _generateRandomAddresses(TIEBREAKER_SUB_COMMITTEE_MEMBERS_COUNT); + dgDeployConfig.TIEBREAKER_SUB_COMMITTEE_2_MEMBERS = + _generateRandomAddresses(TIEBREAKER_SUB_COMMITTEE_MEMBERS_COUNT); + dgDeployConfig.TIEBREAKER_SUB_COMMITTEES_QUORUMS = + [TIEBREAKER_SUB_COMMITTEE_QUORUM, TIEBREAKER_SUB_COMMITTEE_QUORUM]; + + dgDeployConfig.RESEAL_COMMITTEE = _resealCommittee; + + dgDeployConfig.MIN_WITHDRAWALS_BATCH_SIZE = 4; + dgDeployConfig.MIN_TIEBREAKER_ACTIVATION_TIMEOUT = MIN_TIEBREAKER_ACTIVATION_TIMEOUT; + dgDeployConfig.TIEBREAKER_ACTIVATION_TIMEOUT = TIEBREAKER_ACTIVATION_TIMEOUT; + dgDeployConfig.MAX_TIEBREAKER_ACTIVATION_TIMEOUT = MAX_TIEBREAKER_ACTIVATION_TIMEOUT; + dgDeployConfig.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT; + dgDeployConfig.FIRST_SEAL_RAGE_QUIT_SUPPORT = PercentsD16.fromBasisPoints(3_00); // 3% + dgDeployConfig.SECOND_SEAL_RAGE_QUIT_SUPPORT = PercentsD16.fromBasisPoints(15_00); // 15% + dgDeployConfig.MIN_ASSETS_LOCK_DURATION = Durations.from(5 hours); + dgDeployConfig.VETO_SIGNALLING_MIN_DURATION = Durations.from(3 days); + dgDeployConfig.VETO_SIGNALLING_MAX_DURATION = Durations.from(30 days); + dgDeployConfig.VETO_SIGNALLING_MIN_ACTIVE_DURATION = Durations.from(5 hours); + dgDeployConfig.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = Durations.from(5 days); + dgDeployConfig.VETO_COOLDOWN_DURATION = Durations.from(4 days); + dgDeployConfig.RAGE_QUIT_EXTENSION_PERIOD_DURATION = Durations.from(7 days); + dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MIN_DELAY = Durations.from(30 days); + dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_MAX_DELAY = Durations.from(180 days); + dgDeployConfig.RAGE_QUIT_ETH_WITHDRAWALS_DELAY_GROWTH = Durations.from(15 days); + + lidoAddresses.stETH = _lido.stETH; + lidoAddresses.wstETH = _lido.wstETH; + lidoAddresses.withdrawalQueue = _lido.withdrawalQueue; + lidoAddresses.voting = address(_lido.voting); } // --- @@ -151,8 +200,11 @@ abstract contract SetupDeployment is Test { function _deployTimelockedGovernanceSetup(bool isEmergencyProtectionEnabled) internal { _deployEmergencyProtectedTimelockContracts(isEmergencyProtectionEnabled); - _timelockedGovernance = _deployTimelockedGovernance({governance: address(_lido.voting), timelock: _timelock}); - _finalizeEmergencyProtectedTimelockDeploy(_timelockedGovernance); + _timelockedGovernance = + DGContractsDeployment.deployTimelockedGovernance({governance: address(_lido.voting), timelock: _timelock}); + DGContractsDeployment.finalizeEmergencyProtectedTimelockDeploy( + _adminExecutor, _timelock, address(_timelockedGovernance), dgDeployConfig + ); } function _deployDualGovernanceSetup(bool isEmergencyProtectionEnabled) internal { @@ -164,61 +216,29 @@ abstract contract SetupDeployment is Test { resealManager: _resealManager, configProvider: _dualGovernanceConfigProvider }); + contracts.dualGovernance = _dualGovernance; - _tiebreakerCoreCommittee = _deployEmptyTiebreakerCoreCommittee({ + _tiebreakerCoreCommittee = DGContractsDeployment.deployEmptyTiebreakerCoreCommittee({ owner: address(this), // temporary set owner to deployer, to add sub committees manually - dualGovernance: _dualGovernance, - timelock: TIEBREAKER_EXECUTION_DELAY + dualGovernance: address(_dualGovernance), + executionDelay: TIEBREAKER_EXECUTION_DELAY }); - address[] memory coreCommitteeMembers = new address[](TIEBREAKER_SUB_COMMITTEES_COUNT); - - for (uint256 i = 0; i < TIEBREAKER_SUB_COMMITTEES_COUNT; ++i) { - address[] memory members = _generateRandomAddresses(TIEBREAKER_SUB_COMMITTEE_MEMBERS_COUNT); - _tiebreakerSubCommittees.push( - _deployTiebreakerSubCommittee({ - owner: address(_adminExecutor), - quorum: TIEBREAKER_SUB_COMMITTEE_QUORUM, - members: members, - tiebreakerCore: _tiebreakerCoreCommittee - }) - ); - coreCommitteeMembers[i] = address(_tiebreakerSubCommittees[i]); - } + contracts.tiebreakerCoreCommittee = _tiebreakerCoreCommittee; - _tiebreakerCoreCommittee.addMembers(coreCommitteeMembers, coreCommitteeMembers.length); + _tiebreakerSubCommittees = DGContractsDeployment.deployTiebreakerSubCommittees( + address(_adminExecutor), _tiebreakerCoreCommittee, dgDeployConfig + ); _tiebreakerCoreCommittee.transferOwnership(address(_adminExecutor)); - _resealCommittee = _deployResealCommittee(); - // --- // Finalize Setup // --- - _adminExecutor.execute( - address(_dualGovernance), - 0, - abi.encodeCall(_dualGovernance.registerProposer, (address(_lido.voting), address(_adminExecutor))) - ); - _adminExecutor.execute( - address(_dualGovernance), - 0, - abi.encodeCall(_dualGovernance.setTiebreakerActivationTimeout, TIEBREAKER_ACTIVATION_TIMEOUT) - ); - _adminExecutor.execute( - address(_dualGovernance), - 0, - abi.encodeCall(_dualGovernance.setTiebreakerCommittee, address(_tiebreakerCoreCommittee)) - ); - _adminExecutor.execute( - address(_dualGovernance), - 0, - abi.encodeCall(_dualGovernance.addTiebreakerSealableWithdrawalBlocker, address(_lido.withdrawalQueue)) - ); - _adminExecutor.execute( - address(_dualGovernance), 0, abi.encodeCall(_dualGovernance.setResealCommittee, address(_resealCommittee)) - ); - _finalizeEmergencyProtectedTimelockDeploy(_dualGovernance); + DGContractsDeployment.configureDualGovernance(dgDeployConfig, lidoAddresses, contracts); + DGContractsDeployment.finalizeEmergencyProtectedTimelockDeploy( + _adminExecutor, _timelock, address(_dualGovernance), dgDeployConfig + ); // --- // Grant Reseal Manager Roles @@ -238,109 +258,29 @@ abstract contract SetupDeployment is Test { // --- function _deployEmergencyProtectedTimelockContracts(bool isEmergencyProtectionEnabled) internal { - _adminExecutor = _deployExecutor(address(this)); - _timelock = _deployEmergencyProtectedTimelock(_adminExecutor); + contracts = DGContractsDeployment.deployAdminExecutorAndTimelock(dgDeployConfig, address(this)); + _adminExecutor = contracts.adminExecutor; + _timelock = contracts.timelock; if (isEmergencyProtectionEnabled) { - _emergencyActivationCommittee = _deployEmergencyActivationCommittee({ - quorum: _EMERGENCY_ACTIVATION_COMMITTEE_QUORUM, - members: _generateRandomAddresses(_EMERGENCY_ACTIVATION_COMMITTEE_MEMBERS_COUNT), - owner: address(_adminExecutor), - timelock: _timelock - }); - - _emergencyExecutionCommittee = _deployEmergencyExecutionCommittee({ - quorum: _EMERGENCY_EXECUTION_COMMITTEE_QUORUM, - members: _generateRandomAddresses(_EMERGENCY_EXECUTION_COMMITTEE_MEMBERS_COUNT), - owner: address(_adminExecutor), - timelock: _timelock - }); _emergencyGovernance = _deployTimelockedGovernance({governance: address(_lido.voting), timelock: _timelock}); - _adminExecutor.execute( - address(_timelock), - 0, - abi.encodeCall( - _timelock.setEmergencyProtectionActivationCommittee, (address(_emergencyActivationCommittee)) - ) - ); - _adminExecutor.execute( - address(_timelock), - 0, - abi.encodeCall( - _timelock.setEmergencyProtectionExecutionCommittee, (address(_emergencyExecutionCommittee)) - ) - ); - _adminExecutor.execute( - address(_timelock), - 0, - abi.encodeCall( - _timelock.setEmergencyProtectionEndDate, (_EMERGENCY_PROTECTION_DURATION.addTo(Timestamps.now())) - ) - ); - _adminExecutor.execute( - address(_timelock), 0, abi.encodeCall(_timelock.setEmergencyModeDuration, (_EMERGENCY_MODE_DURATION)) - ); - - _adminExecutor.execute( - address(_timelock), 0, abi.encodeCall(_timelock.setEmergencyGovernance, (address(_emergencyGovernance))) - ); - } - } - - function _finalizeEmergencyProtectedTimelockDeploy(IGovernance governance) internal { - _adminExecutor.execute(address(_timelock), 0, abi.encodeCall(_timelock.setGovernance, (address(governance)))); - _adminExecutor.transferOwnership(address(_timelock)); - } + DGContractsDeployment.deployEmergencyProtectedTimelockContracts(lidoAddresses, dgDeployConfig, contracts); - function _deployExecutor(address owner) internal returns (Executor) { - return new Executor(owner); + _emergencyGovernance = contracts.emergencyGovernance; + } } function _deployEmergencyProtectedTimelock(Executor adminExecutor) internal returns (EmergencyProtectedTimelock) { - return new EmergencyProtectedTimelock({ - adminExecutor: address(adminExecutor), - sanityCheckParams: EmergencyProtectedTimelock.SanityCheckParams({ - minExecutionDelay: _MIN_EXECUTION_DELAY, - maxAfterSubmitDelay: _MAX_AFTER_SUBMIT_DELAY, - maxAfterScheduleDelay: _MAX_AFTER_SCHEDULE_DELAY, - maxEmergencyModeDuration: _MAX_EMERGENCY_MODE_DURATION, - maxEmergencyProtectionDuration: _MAX_EMERGENCY_PROTECTION_DURATION - }), - afterSubmitDelay: _AFTER_SUBMIT_DELAY, - afterScheduleDelay: _AFTER_SCHEDULE_DELAY - }); - } - - 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)); + return DGContractsDeployment.deployEmergencyProtectedTimelock(address(adminExecutor), dgDeployConfig); } - function _deployResealCommittee() internal returns (ResealCommittee) { - uint256 quorum = 3; - uint256 membersCount = 5; - address[] memory committeeMembers = new address[](membersCount); - for (uint256 i = 0; i < membersCount; ++i) { - committeeMembers[i] = makeAddr(string(abi.encode(0xFA + i * membersCount + 65))); - } + // --- + // Dual Governance Deployment + // --- - return new ResealCommittee( - address(_adminExecutor), committeeMembers, quorum, address(_dualGovernance), Durations.from(0) - ); + function _deployDualGovernanceConfigProvider() internal returns (ImmutableDualGovernanceConfigProvider) { + return DGContractsDeployment.deployDualGovernanceConfigProvider(dgDeployConfig); } function _deployTimelockedGovernance( @@ -354,30 +294,8 @@ abstract contract SetupDeployment is Test { // 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), - // - vetoSignallingMinDuration: Durations.from(3 days), - vetoSignallingMaxDuration: Durations.from(30 days), - vetoSignallingMinActiveDuration: Durations.from(5 hours), - vetoSignallingDeactivationMaxDuration: Durations.from(5 days), - vetoCooldownDuration: Durations.from(4 days), - // - rageQuitExtensionPeriodDuration: Durations.from(7 days), - rageQuitEthWithdrawalsMinDelay: Durations.from(30 days), - rageQuitEthWithdrawalsMaxDelay: Durations.from(180 days), - rageQuitEthWithdrawalsDelayGrowth: Durations.from(15 days) - }) - ); - } - function _deployResealManager(ITimelock timelock) internal returns (ResealManager) { - return new ResealManager(timelock); + return DGContractsDeployment.deployResealManager(timelock); } function _deployDualGovernance( @@ -385,44 +303,9 @@ abstract contract SetupDeployment is Test { IResealManager resealManager, IDualGovernanceConfigProvider configProvider ) internal returns (DualGovernance) { - return new DualGovernance({ - dependencies: DualGovernance.ExternalDependencies({ - stETH: _lido.stETH, - wstETH: _lido.wstETH, - withdrawalQueue: _lido.withdrawalQueue, - timelock: timelock, - resealManager: resealManager, - configProvider: configProvider - }), - sanityCheckParams: DualGovernance.SanityCheckParams({ - minWithdrawalsBatchSize: 4, - minTiebreakerActivationTimeout: MIN_TIEBREAKER_ACTIVATION_TIMEOUT, - maxTiebreakerActivationTimeout: MAX_TIEBREAKER_ACTIVATION_TIMEOUT, - maxSealableWithdrawalBlockersCount: MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT - }) - }); - } - - function _deployEmptyTiebreakerCoreCommittee( - address owner, - IDualGovernance dualGovernance, - Duration timelock - ) internal returns (TiebreakerCoreCommittee) { - return new TiebreakerCoreCommittee({owner: owner, dualGovernance: address(dualGovernance), timelock: timelock}); - } - - function _deployTiebreakerSubCommittee( - address owner, - uint256 quorum, - address[] memory members, - TiebreakerCoreCommittee tiebreakerCore - ) internal returns (TiebreakerSubCommittee) { - return new TiebreakerSubCommittee({ - owner: owner, - executionQuorum: quorum, - committeeMembers: members, - tiebreakerCoreCommittee: address(tiebreakerCore) - }); + return DGContractsDeployment.deployDualGovernance( + configProvider, timelock, resealManager, dgDeployConfig, lidoAddresses + ); } // --- diff --git a/test/utils/addresses.sol b/test/utils/addresses.sol new file mode 100644 index 00000000..7a3c4ba1 --- /dev/null +++ b/test/utils/addresses.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +function computeAddress(address deployer, uint256 nonce) returns (address) { + bytes memory data; + + if (nonce == 0x00) { + data = abi.encodePacked(hex"94", deployer, hex"80"); + } else if (nonce <= 0x7f) { + data = abi.encodePacked(hex"d6", hex"94", deployer, uint8(nonce)); + } else if (nonce <= 0xff) { + data = abi.encodePacked(hex"d7", hex"94", deployer, hex"81", uint8(nonce)); + } else if (nonce <= 0xffff) { + data = abi.encodePacked(hex"d8", hex"94", deployer, hex"82", uint16(nonce)); + } else if (nonce <= 0xffffff) { + data = abi.encodePacked(hex"d9", hex"94", deployer, hex"83", uint24(nonce)); + } else { + data = abi.encodePacked(hex"da", hex"94", deployer, hex"84", uint32(nonce)); + } + + return address(uint160(uint256(keccak256(data)))); +} diff --git a/test/utils/interfaces/IAragonAgent.sol b/test/utils/interfaces/IAragonAgent.sol index fbbf1f13..11d8642a 100644 --- a/test/utils/interfaces/IAragonAgent.sol +++ b/test/utils/interfaces/IAragonAgent.sol @@ -4,5 +4,6 @@ pragma solidity 0.8.26; import {IAragonForwarder} from "./IAragonForwarder.sol"; interface IAragonAgent is IAragonForwarder { + function EXECUTE_ROLE() external pure returns (bytes32); function RUN_SCRIPT_ROLE() external pure returns (bytes32); } diff --git a/test/utils/lido-utils.sol b/test/utils/lido-utils.sol index 38ac97c0..9f3f42c0 100644 --- a/test/utils/lido-utils.sol +++ b/test/utils/lido-utils.sol @@ -27,7 +27,7 @@ import { DAO_AGENT, DAO_VOTING, DAO_TOKEN_MANAGER -} from "./mainnet-addresses.sol"; +} from "addresses/mainnet-addresses.sol"; uint256 constant ST_ETH_TRANSFERS_SHARE_LOSS_COMPENSATION = 8; // TODO: evaluate min enough value diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 59c713a9..f6385c4c 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -12,7 +12,8 @@ import {PercentD16} from "contracts/types/PercentD16.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {Escrow, VetoerState, LockedAssetsTotals} from "contracts/Escrow.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; +import {Escrow} from "contracts/Escrow.sol"; // --- // Interfaces @@ -20,7 +21,7 @@ import {Escrow, VetoerState, LockedAssetsTotals} from "contracts/Escrow.sol"; import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; import {ITimelock} from "contracts/interfaces/ITimelock.sol"; -import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; import {IPotentiallyDangerousContract} from "./interfaces/IPotentiallyDangerousContract.sol"; // --- @@ -198,7 +199,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { function _unlockWstETH(address vetoer) internal { Escrow escrow = _getVetoSignallingEscrow(); uint256 wstETHBalanceBefore = _lido.wstETH.balanceOf(vetoer); - VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + ISignallingEscrow.VetoerDetails memory vetoerDetailsBefore = escrow.getVetoerDetails(vetoer); vm.startPrank(vetoer); uint256 wstETHUnlocked = escrow.unlockWstETH(); @@ -206,17 +207,21 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { // 1 wei rounding issue may arise because of the wrapping stETH into wstETH before // sending funds to the user - assertApproxEqAbs(wstETHUnlocked, vetoerStateBefore.stETHLockedShares, 1); - assertApproxEqAbs(_lido.wstETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerStateBefore.stETHLockedShares, 1); + assertApproxEqAbs(wstETHUnlocked, vetoerDetailsBefore.stETHLockedShares.toUint256(), 1); + assertApproxEqAbs( + _lido.wstETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerDetailsBefore.stETHLockedShares.toUint256(), 1 + ); } function _lockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); - LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); + ISignallingEscrow.VetoerDetails memory vetoerDetailsBefore = escrow.getVetoerDetails(vetoer); + ISignallingEscrow.SignallingEscrowDetails memory signallingEscrowDetailsBefore = + escrow.getSignallingEscrowDetails(); uint256 unstETHTotalSharesLocked = 0; - WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = + _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); for (uint256 i = 0; i < unstETHIds.length; ++i) { unstETHTotalSharesLocked += statuses[i].amountOfShares; } @@ -231,23 +236,26 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), address(escrow)); } - VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); - assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount + unstETHIds.length); + ISignallingEscrow.VetoerDetails memory vetoerDetailsAfter = escrow.getVetoerDetails(vetoer); + assertEq(vetoerDetailsAfter.unstETHIdsCount, vetoerDetailsBefore.unstETHIdsCount + unstETHIds.length); - LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + ISignallingEscrow.SignallingEscrowDetails memory signallingEscrowDetailsAfter = + escrow.getSignallingEscrowDetails(); assertEq( - lockedAssetsTotalsAfter.unstETHUnfinalizedShares, - lockedAssetsTotalsBefore.unstETHUnfinalizedShares + unstETHTotalSharesLocked + signallingEscrowDetailsAfter.totalUnstETHUnfinalizedShares.toUint256(), + signallingEscrowDetailsBefore.totalUnstETHUnfinalizedShares.toUint256() + unstETHTotalSharesLocked ); } function _unlockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); - LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); + ISignallingEscrow.VetoerDetails memory vetoerDetailsBefore = escrow.getVetoerDetails(vetoer); + ISignallingEscrow.SignallingEscrowDetails memory signallingEscrowDetailsBefore = + escrow.getSignallingEscrowDetails(); uint256 unstETHTotalSharesUnlocked = 0; - WithdrawalRequestStatus[] memory statuses = _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = + _lido.withdrawalQueue.getWithdrawalStatus(unstETHIds); for (uint256 i = 0; i < unstETHIds.length; ++i) { unstETHTotalSharesUnlocked += statuses[i].amountOfShares; } @@ -260,14 +268,15 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), vetoer); } - VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); - assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount - unstETHIds.length); + ISignallingEscrow.VetoerDetails memory vetoerDetailsAfter = escrow.getVetoerDetails(vetoer); + assertEq(vetoerDetailsAfter.unstETHIdsCount, vetoerDetailsBefore.unstETHIdsCount - unstETHIds.length); // TODO: implement correct assert. It must consider was unstETH finalized or not - LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + ISignallingEscrow.SignallingEscrowDetails memory signallingEscrowDetailsAfter = + escrow.getSignallingEscrowDetails(); assertEq( - lockedAssetsTotalsAfter.unstETHUnfinalizedShares, - lockedAssetsTotalsBefore.unstETHUnfinalizedShares - unstETHTotalSharesUnlocked + signallingEscrowDetailsAfter.totalUnstETHUnfinalizedShares.toUint256(), + signallingEscrowDetailsBefore.totalUnstETHUnfinalizedShares.toUint256() - unstETHTotalSharesUnlocked ); } @@ -549,30 +558,18 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { } function _executeActivateEmergencyMode() internal { - address[] memory members = _emergencyActivationCommittee.getMembers(); - for (uint256 i = 0; i < _emergencyActivationCommittee.getQuorum(); ++i) { - vm.prank(members[i]); - _emergencyActivationCommittee.approveActivateEmergencyMode(); - } - _emergencyActivationCommittee.executeActivateEmergencyMode(); + vm.prank(_emergencyActivationCommittee); + _timelock.activateEmergencyMode(); } function _executeEmergencyExecute(uint256 proposalId) internal { - address[] memory members = _emergencyExecutionCommittee.getMembers(); - for (uint256 i = 0; i < _emergencyExecutionCommittee.getQuorum(); ++i) { - vm.prank(members[i]); - _emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); - } - _emergencyExecutionCommittee.executeEmergencyExecute(proposalId); + vm.prank(_emergencyExecutionCommittee); + _timelock.emergencyExecute(proposalId); } function _executeEmergencyReset() internal { - address[] memory members = _emergencyExecutionCommittee.getMembers(); - for (uint256 i = 0; i < _emergencyExecutionCommittee.getQuorum(); ++i) { - vm.prank(members[i]); - _emergencyExecutionCommittee.approveEmergencyReset(); - } - _emergencyExecutionCommittee.executeEmergencyReset(); + vm.prank(_emergencyExecutionCommittee); + _timelock.emergencyReset(); } struct DurationStruct { diff --git a/test/utils/testing-assert-eq-extender.sol b/test/utils/testing-assert-eq-extender.sol index cb507c2d..fc9d6571 100644 --- a/test/utils/testing-assert-eq-extender.sol +++ b/test/utils/testing-assert-eq-extender.sol @@ -45,6 +45,10 @@ contract TestingAssertEqExtender is Test { assertEq(uint256(a), uint256(b)); } + function assertEq(DualGovernanceState a, DualGovernanceState b, string memory message) internal { + assertEq(uint256(a), uint256(b), message); + } + function assertEq(Balances memory b1, Balances memory b2, uint256 sharesEpsilon) internal { assertEq(b1.wstETHShares, b2.wstETHShares); assertEq(b1.wstETHAmount, b2.wstETHAmount); diff --git a/test/utils/time-constraints.sol b/test/utils/time-constraints.sol new file mode 100644 index 00000000..c8cd6203 --- /dev/null +++ b/test/utils/time-constraints.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Durations, Duration} from "contracts/types/Duration.sol"; +import {Timestamps, Timestamp} from "contracts/types/Timestamp.sol"; + +/// @title Time Constraints Contract +/// @notice Provides functionality to restrict execution of functions based on time constraints. +contract TimeConstraints { + // --- + // Errors + // --- + + error DayTimeOverflow(); + error InvalidDayTimeRange(Duration startDayTime, Duration endDayTime); + error DayTimeOutOfRange(Duration currentDayTime, Duration startDayTime, Duration endDayTime); + error TimestampNotReached(Timestamp requiredTimestamp); + + // --- + // Constants + // --- + + /// @notice Total number of seconds in a day (24 hours). + Duration public immutable DAY_DURATION = Durations.from(24 hours); + + // --- + // Time Constraints Checks + // --- + + /// @notice Checks that the transaction can only be executed within a specific time range during the day. + /// @param startDayTime The start time of the allowed range in seconds since midnight (UTC). + /// @param endDayTime The end time of the allowed range in seconds since midnight (UTC). + function checkExecuteWithinDayTime(Duration startDayTime, Duration endDayTime) external view { + _validateDayTime(startDayTime); + _validateDayTime(endDayTime); + + if (startDayTime > endDayTime) { + revert InvalidDayTimeRange(startDayTime, endDayTime); + } + + Duration currentDayTime = getCurrentDayTime(); + if (currentDayTime < startDayTime || currentDayTime > endDayTime) { + revert DayTimeOutOfRange(currentDayTime, startDayTime, endDayTime); + } + } + + /// @notice Checks that the transaction can only be executed after a specific timestamp. + /// @param timestamp The Unix timestamp after which the function can be executed. + function checkExecuteAfterTimestamp(Timestamp timestamp) external view { + if (Timestamps.now() < timestamp) { + revert TimestampNotReached(timestamp); + } + } + + // --- + // Getters + // --- + + /// @notice Gets the current time in seconds since midnight (UTC). + /// @return The current time in seconds since midnight. + function getCurrentDayTime() public view returns (Duration) { + return Durations.from(block.timestamp % DAY_DURATION.toSeconds()); + } + + // --- + // Internal Methods + // --- + + /// @notice Validates that a provided day time value is within the [0:00:00, 23:59:59] range. + /// @param dayTime The day time value in seconds to validate. + /// @dev Reverts with `DayTimeOverflow` if the value exceeds the number of seconds in a day. + function _validateDayTime(Duration dayTime) internal view { + if (dayTime >= DAY_DURATION) { + revert DayTimeOverflow(); + } + } +}