diff --git a/contracts/Configuration.sol b/contracts/Configuration.sol index 44cfc9bd..17f3b6dc 100644 --- a/contracts/Configuration.sol +++ b/contracts/Configuration.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Durations, Duration} from "./types/Duration.sol"; import {IConfiguration, DualGovernanceConfig} from "./interfaces/IConfiguration.sol"; uint256 constant PERCENT = 10 ** 16; @@ -8,39 +9,42 @@ uint256 constant PERCENT = 10 ** 16; contract Configuration is IConfiguration { error MaxSealablesLimitOverflow(uint256 count, uint256 limit); + uint256 public immutable MIN_WITHDRAWALS_BATCH_SIZE = 8; + uint256 public immutable MAX_WITHDRAWALS_BATCH_SIZE = 128; + // --- // Dual Governance State Properties // --- uint256 public immutable FIRST_SEAL_RAGE_QUIT_SUPPORT = 3 * PERCENT; uint256 public immutable SECOND_SEAL_RAGE_QUIT_SUPPORT = 15 * PERCENT; - uint256 public immutable DYNAMIC_TIMELOCK_MIN_DURATION = 3 days; - uint256 public immutable DYNAMIC_TIMELOCK_MAX_DURATION = 30 days; + Duration public immutable DYNAMIC_TIMELOCK_MIN_DURATION = Durations.from(3 days); + Duration public immutable DYNAMIC_TIMELOCK_MAX_DURATION = Durations.from(30 days); - uint256 public immutable VETO_SIGNALLING_MIN_ACTIVE_DURATION = 5 hours; - uint256 public immutable VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = 5 days; - uint256 public immutable RAGE_QUIT_ACCUMULATION_MAX_DURATION = 3 days; + Duration public immutable VETO_SIGNALLING_MIN_ACTIVE_DURATION = Durations.from(5 hours); + Duration public immutable VETO_SIGNALLING_DEACTIVATION_MAX_DURATION = Durations.from(5 days); + Duration public immutable RAGE_QUIT_ACCUMULATION_MAX_DURATION = Durations.from(3 days); - uint256 public immutable VETO_COOLDOWN_DURATION = 4 days; + Duration public immutable VETO_COOLDOWN_DURATION = Durations.from(4 days); - uint256 public immutable RAGE_QUIT_EXTENSION_DELAY = 7 days; - uint256 public immutable RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK = 60 days; - uint256 public immutable RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_START_SEQ_NUMBER = 2; + Duration public immutable RAGE_QUIT_EXTENSION_DELAY = Durations.from(7 days); + Duration public immutable RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK = Durations.from(60 days); + uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER = 2; - uint256 public immutable RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_A = 0; - uint256 public immutable RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_B = 0; - uint256 public immutable RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_C = 0; + uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_A = 0; + uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_B = 0; + uint256 public immutable RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C = 0; // --- address public immutable ADMIN_EXECUTOR; address public immutable EMERGENCY_GOVERNANCE; - uint256 public immutable AFTER_SUBMIT_DELAY = 3 days; - uint256 public immutable AFTER_SCHEDULE_DELAY = 2 days; + Duration public immutable AFTER_SUBMIT_DELAY = Durations.from(3 days); + Duration public immutable AFTER_SCHEDULE_DELAY = Durations.from(2 days); - uint256 public immutable SIGNALLING_ESCROW_MIN_LOCK_TIME = 5 hours; + Duration public immutable SIGNALLING_ESCROW_MIN_LOCK_TIME = Durations.from(5 hours); - uint256 public immutable TIE_BREAK_ACTIVATION_TIMEOUT = 365 days; + Duration public immutable TIE_BREAK_ACTIVATION_TIMEOUT = Durations.from(365 days); // Sealables Array Representation uint256 private immutable MAX_SELABLES_COUNT = 5; @@ -84,8 +88,8 @@ contract Configuration is IConfiguration { returns ( uint256 firstSealRageQuitSupport, uint256 secondSealRageQuitSupport, - uint256 dynamicTimelockMinDuration, - uint256 dynamicTimelockMaxDuration + Duration dynamicTimelockMinDuration, + Duration dynamicTimelockMaxDuration ) { firstSealRageQuitSupport = FIRST_SEAL_RAGE_QUIT_SUPPORT; @@ -103,12 +107,13 @@ contract Configuration is IConfiguration { config.vetoSignallingDeactivationMaxDuration = VETO_SIGNALLING_DEACTIVATION_MAX_DURATION; config.vetoCooldownDuration = VETO_COOLDOWN_DURATION; config.rageQuitExtensionDelay = RAGE_QUIT_EXTENSION_DELAY; - config.rageQuitEthClaimMinTimelock = RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK; - config.rageQuitEthClaimTimelockGrowthStartSeqNumber = RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_START_SEQ_NUMBER; - config.rageQuitEthClaimTimelockGrowthCoeffs = [ - RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_A, - RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_B, - RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_C + config.rageQuitEthWithdrawalsMinTimelock = RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK; + config.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber = + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER; + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs = [ + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_A, + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_B, + RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C ]; } } diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 3a6c7a5a..dfc8a733 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -1,31 +1,36 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; import {ITimelock, IGovernance} from "./interfaces/ITimelock.sol"; +import {ISealable} from "./interfaces/ISealable.sol"; +import {IResealManager} from "./interfaces/IResealManager.sol"; import {ConfigurationProvider} from "./ConfigurationProvider.sol"; import {Proposers, Proposer} from "./libraries/Proposers.sol"; import {ExecutorCall} from "./libraries/Proposals.sol"; import {EmergencyProtection} from "./libraries/EmergencyProtection.sol"; import {State, DualGovernanceState} from "./libraries/DualGovernanceState.sol"; +import {TiebreakerProtection} from "./libraries/TiebreakerProtection.sol"; contract DualGovernance is IGovernance, ConfigurationProvider { using Proposers for Proposers.State; using DualGovernanceState for DualGovernanceState.Store; + using TiebreakerProtection for TiebreakerProtection.Tiebreaker; - event TiebreakerSet(address tiebreakCommittee); event ProposalScheduled(uint256 proposalId); - error ProposalNotExecutable(uint256 proposalId); - error NotTiebreaker(address account, address tiebreakCommittee); + error NotResealCommitttee(address account); ITimelock public immutable TIMELOCK; - address internal _tiebreaker; - + TiebreakerProtection.Tiebreaker internal _tiebreaker; Proposers.State internal _proposers; DualGovernanceState.Store internal _dgState; EmergencyProtection.State internal _emergencyProtection; + address internal _resealCommittee; + IResealManager internal _resealManager; constructor( address config, @@ -49,8 +54,12 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function scheduleProposal(uint256 proposalId) external { _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); - uint256 proposalSubmissionTime = TIMELOCK.schedule(proposalId); + + Timestamp proposalSubmissionTime = TIMELOCK.getProposalSubmissionTime(proposalId); _dgState.checkCanScheduleProposal(proposalSubmissionTime); + + TIMELOCK.schedule(proposalId); + emit ProposalScheduled(proposalId); } @@ -59,11 +68,11 @@ contract DualGovernance is IGovernance, ConfigurationProvider { TIMELOCK.cancelAllNonExecutedProposals(); } - function vetoSignallingEscrow() external view returns (address) { + function getVetoSignallingEscrow() external view returns (address) { return address(_dgState.signallingEscrow); } - function rageQuitEscrow() external view returns (address) { + function getRageQuitEscrow() external view returns (address) { return address(_dgState.rageQuitEscrow); } @@ -79,14 +88,14 @@ contract DualGovernance is IGovernance, ConfigurationProvider { _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); } - function currentState() external view returns (State) { + function getCurrentState() external view returns (State) { return _dgState.currentState(); } function getVetoSignallingState() external view - returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) + returns (bool isActive, Duration duration, Timestamp activatedAt, Timestamp enteredAt) { (isActive, duration, activatedAt, enteredAt) = _dgState.getVetoSignallingState(CONFIG.getDualGovernanceConfig()); } @@ -94,12 +103,12 @@ contract DualGovernance is IGovernance, ConfigurationProvider { function getVetoSignallingDeactivationState() external view - returns (bool isActive, uint256 duration, uint256 enteredAt) + returns (bool isActive, Duration duration, Timestamp enteredAt) { (isActive, duration, enteredAt) = _dgState.getVetoSignallingDeactivationState(CONFIG.getDualGovernanceConfig()); } - function getVetoSignallingDuration() external view returns (uint256) { + function getVetoSignallingDuration() external view returns (Duration) { return _dgState.getVetoSignallingDuration(CONFIG.getDualGovernanceConfig()); } @@ -141,29 +150,38 @@ contract DualGovernance is IGovernance, ConfigurationProvider { // Tiebreaker Protection // --- + function tiebreakerResumeSealable(address sealable) external { + _tiebreaker.checkTiebreakerCommittee(msg.sender); + _dgState.checkTiebreak(CONFIG); + _tiebreaker.resumeSealable(sealable); + } + function tiebreakerScheduleProposal(uint256 proposalId) external { - _checkTiebreakerCommittee(msg.sender); - _dgState.activateNextState(CONFIG.getDualGovernanceConfig()); + _tiebreaker.checkTiebreakerCommittee(msg.sender); _dgState.checkTiebreak(CONFIG); TIMELOCK.schedule(proposalId); } - function setTiebreakerCommittee(address newTiebreaker) external { + function setTiebreakerProtection(address newTiebreaker, address resealManager) external { _checkAdminExecutor(msg.sender); - address oldTiebreaker = _tiebreaker; - if (newTiebreaker != oldTiebreaker) { - _tiebreaker = newTiebreaker; - emit TiebreakerSet(newTiebreaker); - } + _tiebreaker.setTiebreaker(newTiebreaker, resealManager); } // --- - // Internal Helper Methods + // Reseal executor // --- - function _checkTiebreakerCommittee(address account) internal view { - if (account != _tiebreaker) { - revert NotTiebreaker(account, _tiebreaker); + function resealSealables(address[] memory sealables) external { + if (msg.sender != _resealCommittee) { + revert NotResealCommitttee(msg.sender); } + _dgState.checkResealState(); + _resealManager.reseal(sealables); + } + + function setReseal(address resealManager, address resealCommittee) external { + _checkAdminExecutor(msg.sender); + _resealCommittee = resealCommittee; + _resealManager = IResealManager(resealManager); } } diff --git a/contracts/EmergencyProtectedTimelock.sol b/contracts/EmergencyProtectedTimelock.sol index 019feb72..e8a84dc3 100644 --- a/contracts/EmergencyProtectedTimelock.sol +++ b/contracts/EmergencyProtectedTimelock.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Duration} from "./types/Duration.sol"; +import {Timestamp} from "./types/Timestamp.sol"; + import {IOwnable} from "./interfaces/IOwnable.sol"; import {ITimelock} from "./interfaces/ITimelock.sol"; @@ -9,6 +12,11 @@ import {EmergencyProtection, EmergencyState} from "./libraries/EmergencyProtecti import {ConfigurationProvider} from "./ConfigurationProvider.sol"; +/// @title EmergencyProtectedTimelock +/// @dev A timelock contract with emergency protection functionality. +/// The contract allows for submitting, scheduling, and executing proposals, +/// while providing emergency protection features to prevent unauthorized +/// execution during emergency situations. contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { using Proposals for Proposals.State; using EmergencyProtection for EmergencyProtection.State; @@ -25,31 +33,55 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { constructor(address config) ConfigurationProvider(config) {} + // --- + // Main Timelock Functionality + // --- + + /// @dev Submits a new proposal to execute a series of calls through an executor. + /// Only the governance contract can call this function. + /// @param executor The address of the executor contract that will execute the calls. + /// @param calls An array of `ExecutorCall` structs representing the calls to be executed. + /// @return newProposalId The ID of the newly created proposal. function submit(address executor, ExecutorCall[] calldata calls) external returns (uint256 newProposalId) { _checkGovernance(msg.sender); newProposalId = _proposals.submit(executor, calls); } - function schedule(uint256 proposalId) external returns (uint256 submittedAt) { + /// @dev Schedules a proposal for execution after a specified delay. + /// Only the governance contract can call this function. + /// @param proposalId The ID of the proposal to be scheduled. + function schedule(uint256 proposalId) external { _checkGovernance(msg.sender); - submittedAt = _proposals.schedule(proposalId, CONFIG.AFTER_SUBMIT_DELAY()); + _proposals.schedule(proposalId, CONFIG.AFTER_SUBMIT_DELAY()); } + /// @dev Executes a scheduled proposal. + /// Checks if emergency mode is active and prevents execution if it is. + /// @param proposalId The ID of the proposal to be executed. function execute(uint256 proposalId) external { _emergencyProtection.checkEmergencyModeActive(false); _proposals.execute(proposalId, CONFIG.AFTER_SCHEDULE_DELAY()); } + /// @dev Cancels all non-executed proposals. + /// Only the governance contract can call this function. function cancelAllNonExecutedProposals() external { _checkGovernance(msg.sender); _proposals.cancelAll(); } + /// @dev Transfers ownership of the executor contract to a new owner. + /// Only the admin executor can call this function. + /// @param executor The address of the executor contract. + /// @param owner The address of the new owner. function transferExecutorOwnership(address executor, address owner) external { _checkAdminExecutor(msg.sender); IOwnable(executor).transferOwnership(owner); } + /// @dev Sets a new governance contract address. + /// Only the admin executor can call this function. + /// @param newGovernance The address of the new governance contract. function setGovernance(address newGovernance) external { _checkAdminExecutor(msg.sender); _setGovernance(newGovernance); @@ -59,18 +91,25 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { // Emergency Protection Functionality // --- + /// @dev Activates the emergency mode. + /// Only the activation committee can call this function. function activateEmergencyMode() external { _emergencyProtection.checkActivationCommittee(msg.sender); _emergencyProtection.checkEmergencyModeActive(false); _emergencyProtection.activate(); } + /// @dev Executes a proposal during emergency mode. + /// Checks if emergency mode is active and if the caller is part of the execution committee. + /// @param proposalId The ID of the proposal to be executed. function emergencyExecute(uint256 proposalId) external { _emergencyProtection.checkEmergencyModeActive(true); _emergencyProtection.checkExecutionCommittee(msg.sender); - _proposals.execute(proposalId, /* afterScheduleDelay */ 0); + _proposals.execute(proposalId, /* afterScheduleDelay */ Duration.wrap(0)); } + /// @dev Deactivates the emergency mode. + /// If the emergency mode has not passed, only the admin executor can call this function. function deactivateEmergencyMode() external { _emergencyProtection.checkEmergencyModeActive(true); if (!_emergencyProtection.isEmergencyModePassed()) { @@ -80,6 +119,8 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { _proposals.cancelAll(); } + /// @dev Resets the system after entering the emergency mode. + /// Only the execution committee can call this function. function emergencyReset() external { _emergencyProtection.checkEmergencyModeActive(true); _emergencyProtection.checkExecutionCommittee(msg.sender); @@ -88,20 +129,30 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { _proposals.cancelAll(); } + /// @dev Sets the parameters for the emergency protection functionality. + /// Only the admin executor can call this function. + /// @param activator The address of the activation committee. + /// @param enactor The address of the execution committee. + /// @param protectionDuration The duration of the protection period. + /// @param emergencyModeDuration The duration of the emergency mode. function setEmergencyProtection( address activator, address enactor, - uint256 protectionDuration, - uint256 emergencyModeDuration + Duration protectionDuration, + Duration emergencyModeDuration ) external { _checkAdminExecutor(msg.sender); _emergencyProtection.setup(activator, enactor, protectionDuration, emergencyModeDuration); } + /// @dev Checks if the emergency protection functionality is enabled. + /// @return A boolean indicating if the emergency protection is enabled. function isEmergencyProtectionEnabled() external view returns (bool) { return _emergencyProtection.isEmergencyProtectionEnabled(); } + /// @dev Retrieves the current emergency state. + /// @return res The EmergencyState struct containing the current emergency state. function getEmergencyState() external view returns (EmergencyState memory res) { res = _emergencyProtection.getEmergencyState(); } @@ -110,27 +161,43 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { // Timelock View Methods // --- + /// @dev Retrieves the address of the current governance contract. + /// @return The address of the current governance contract. function getGovernance() external view returns (address) { return _governance; } + /// @dev Retrieves the details of a proposal. + /// @param proposalId The ID of the proposal. + /// @return proposal The Proposal struct containing the details of the proposal. function getProposal(uint256 proposalId) external view returns (Proposal memory proposal) { proposal = _proposals.get(proposalId); } + /// @dev Retrieves the total number of proposals. + /// @return count The total number of proposals. function getProposalsCount() external view returns (uint256 count) { count = _proposals.count(); } - // --- - // Proposals Lifecycle View Methods - // --- + /// @dev Retrieves the submission time of a proposal. + /// @param proposalId The ID of the proposal. + /// @return submittedAt The submission time of the proposal. + function getProposalSubmissionTime(uint256 proposalId) external view returns (Timestamp submittedAt) { + submittedAt = _proposals.getProposalSubmissionTime(proposalId); + } + /// @dev Checks if a proposal can be executed. + /// @param proposalId The ID of the proposal. + /// @return A boolean indicating if the proposal can be executed. function canExecute(uint256 proposalId) external view returns (bool) { return !_emergencyProtection.isEmergencyModeActivated() && _proposals.canExecute(proposalId, CONFIG.AFTER_SCHEDULE_DELAY()); } + /// @dev Checks if a proposal can be scheduled. + /// @param proposalId The ID of the proposal. + /// @return A boolean indicating if the proposal can be scheduled. function canSchedule(uint256 proposalId) external view returns (bool) { return _proposals.canSchedule(proposalId, CONFIG.AFTER_SUBMIT_DELAY()); } @@ -139,6 +206,8 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { // Internal Methods // --- + /// @dev Internal function to set the governance contract address. + /// @param newGovernance The address of the new governance contract. function _setGovernance(address newGovernance) internal { address prevGovernance = _governance; if (newGovernance == prevGovernance || newGovernance == address(0)) { @@ -148,6 +217,8 @@ contract EmergencyProtectedTimelock is ITimelock, ConfigurationProvider { emit GovernanceSet(newGovernance); } + /// @dev Internal function to check if the caller is the governance contract. + /// @param account The address to check. function _checkGovernance(address account) internal view { if (_governance != account) { revert NotGovernance(account, _governance); diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 2488acb5..a0d274b1 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -2,18 +2,28 @@ pragma solidity 0.8.23; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {Duration} from "./types/Duration.sol"; +import {Timestamp, Timestamps} from "./types/Timestamp.sol"; import {IEscrow} from "./interfaces/IEscrow.sol"; -import {IConfiguration} from "./interfaces/IConfiguration.sol"; +import {IEscrowConfigration} from "./interfaces/IConfiguration.sol"; import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; import {IWithdrawalQueue, WithdrawalRequestStatus} from "./interfaces/IWithdrawalQueue.sol"; -import {AssetsAccounting, LockedAssetsStats, LockedAssetsTotals} from "./libraries/AssetsAccounting.sol"; - -import {ArrayUtils} from "./utils/arrays.sol"; +import { + ETHValue, + ETHValues, + SharesValue, + SharesValues, + HolderAssets, + StETHAccounting, + UnstETHAccounting, + AssetsAccounting +} from "./libraries/AssetsAccounting.sol"; +import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalBatchesQueue.sol"; interface IDualGovernance { function activateNextState() external; @@ -25,50 +35,63 @@ enum EscrowState { RageQuitEscrow } +struct LockedAssetsTotals { + uint256 stETHLockedShares; + uint256 stETHClaimedETH; + uint256 unstETHUnfinalizedShares; + uint256 unstETHFinalizedETH; +} + struct VetoerState { - uint256 stETHShares; - uint256 wstETHShares; - uint256 unstETHShares; + uint256 stETHLockedShares; + uint256 unstETHLockedShares; + uint256 unstETHIdsCount; + uint256 lastAssetsLockTimestamp; } contract Escrow is IEscrow { using AssetsAccounting for AssetsAccounting.State; + using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.State; - error EmptyBatch(); - error ZeroWithdraw(); + error UnexpectedUnstETHId(); + error InvalidHintsLength(uint256 actual, uint256 expected); + error ClaimingIsFinished(); + error InvalidBatchSize(uint256 size); error WithdrawalsTimelockNotPassed(); error InvalidETHSender(address actual, address expected); error NotDualGovernance(address actual, address expected); - error InvalidNextBatch(uint256 actualRequestId, uint256 expectedRequestId); error MasterCopyCallForbidden(); error InvalidState(EscrowState actual, EscrowState expected); error RageQuitExtraTimelockNotStarted(); - uint256 public immutable RAGE_QUIT_TIMELOCK = 30 days; address public immutable MASTER_COPY; + uint256 public immutable MIN_WITHDRAWAL_REQUEST_AMOUNT; + uint256 public immutable MAX_WITHDRAWAL_REQUEST_AMOUNT; + IStETH public immutable ST_ETH; IWstETH public immutable WST_ETH; IWithdrawalQueue public immutable WITHDRAWAL_QUEUE; - IConfiguration public immutable CONFIG; + IEscrowConfigration public immutable CONFIG; EscrowState internal _escrowState; IDualGovernance private _dualGovernance; AssetsAccounting.State private _accounting; + WithdrawalsBatchesQueue.State private _batchesQueue; - uint256[] internal _withdrawalUnstETHIds; - - uint256 internal _rageQuitExtraTimelock; - uint256 internal _rageQuitWithdrawalsTimelock; - uint256 internal _rageQuitTimelockStartedAt; + Duration internal _rageQuitExtensionDelay; + Duration internal _rageQuitWithdrawalsTimelock; + Timestamp internal _rageQuitTimelockStartedAt; constructor(address stETH, address wstETH, address withdrawalQueue, address config) { ST_ETH = IStETH(stETH); WST_ETH = IWstETH(wstETH); - WITHDRAWAL_QUEUE = IWithdrawalQueue(withdrawalQueue); MASTER_COPY = address(this); - CONFIG = IConfiguration(config); + CONFIG = IEscrowConfigration(config); + WITHDRAWAL_QUEUE = IWithdrawalQueue(withdrawalQueue); + MIN_WITHDRAWAL_REQUEST_AMOUNT = WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT(); + MAX_WITHDRAWAL_REQUEST_AMOUNT = WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); } function initialize(address dualGovernance) external { @@ -80,64 +103,49 @@ contract Escrow is IEscrow { _escrowState = EscrowState.SignallingEscrow; _dualGovernance = IDualGovernance(dualGovernance); + ST_ETH.approve(address(WST_ETH), type(uint256).max); ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); - WST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); } // --- // Lock & Unlock stETH // --- - function lockStETH(uint256 amount) external { - uint256 shares = ST_ETH.getSharesByPooledEth(amount); - _accounting.accountStETHLock(msg.sender, shares); - ST_ETH.transferSharesFrom(msg.sender, address(this), shares); + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { + lockedStETHShares = ST_ETH.getSharesByPooledEth(amount); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); + ST_ETH.transferSharesFrom(msg.sender, address(this), lockedStETHShares); _activateNextGovernanceState(); } - function unlockStETH() external { + function unlockStETH() external returns (uint256 unlockedStETHShares) { + _activateNextGovernanceState(); _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); - uint256 sharesUnlocked = _accounting.accountStETHUnlock(msg.sender); - ST_ETH.transferShares(msg.sender, sharesUnlocked); + unlockedStETHShares = _accounting.accountStETHSharesUnlock(msg.sender).toUint256(); + ST_ETH.transferShares(msg.sender, unlockedStETHShares); _activateNextGovernanceState(); } - function requestWithdrawalsStETH(uint256[] calldata amounts) external returns (uint256[] memory unstETHIds) { - unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(amounts, address(this)); - WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); - - uint256 sharesTotal = 0; - for (uint256 i = 0; i < statuses.length; ++i) { - sharesTotal += statuses[i].amountOfShares; - } - _accounting.accountStETHUnlock(msg.sender, sharesTotal); - _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); - } - // --- // Lock / Unlock wstETH // --- - function lockWstETH(uint256 amount) external { - _accounting.accountWstETHLock(msg.sender, amount); + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { WST_ETH.transferFrom(msg.sender, address(this), amount); + lockedStETHShares = ST_ETH.getSharesByPooledEth(WST_ETH.unwrap(amount)); + _accounting.accountStETHSharesLock(msg.sender, SharesValues.from(lockedStETHShares)); _activateNextGovernanceState(); } - function unlockWstETH() external returns (uint256 wstETHUnlocked) { + function unlockWstETH() external returns (uint256 unlockedStETHShares) { + _activateNextGovernanceState(); _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); - wstETHUnlocked = _accounting.accountWstETHUnlock(msg.sender); - WST_ETH.transfer(msg.sender, wstETHUnlocked); + SharesValue wstETHUnlocked = _accounting.accountStETHSharesUnlock(msg.sender); + unlockedStETHShares = WST_ETH.wrap(ST_ETH.getPooledEthByShares(wstETHUnlocked.toUint256())); + WST_ETH.transfer(msg.sender, unlockedStETHShares); _activateNextGovernanceState(); } - function requestWithdrawalsWstETH(uint256[] calldata amounts) external returns (uint256[] memory unstETHIds) { - uint256 totalAmount = ArrayUtils.sum(amounts); - _accounting.accountWstETHUnlock(msg.sender, totalAmount); - unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawalsWstETH(amounts, address(this)); - _accounting.accountUnstETHLock(msg.sender, unstETHIds, WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds)); - } - // --- // Lock / Unlock unstETH // --- @@ -149,14 +157,18 @@ contract Escrow is IEscrow { for (uint256 i = 0; i < unstETHIdsCount; ++i) { WITHDRAWAL_QUEUE.transferFrom(msg.sender, address(this), unstETHIds[i]); } + _activateNextGovernanceState(); } function unlockUnstETH(uint256[] memory unstETHIds) external { - _accounting.accountUnstETHUnlock(CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME(), msg.sender, unstETHIds); + _activateNextGovernanceState(); + _accounting.checkAssetsUnlockDelayPassed(msg.sender, CONFIG.SIGNALLING_ESCROW_MIN_LOCK_TIME()); + _accounting.accountUnstETHUnlock(msg.sender, unstETHIds); uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { WITHDRAWAL_QUEUE.transferFrom(address(this), msg.sender, unstETHIds[i]); } + _activateNextGovernanceState(); } function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external { @@ -166,52 +178,87 @@ contract Escrow is IEscrow { _accounting.accountUnstETHFinalized(unstETHIds, claimableAmounts); } + // --- + // Convert to NFT + // --- + + function requestWithdrawals(uint256[] calldata stEthAmounts) external returns (uint256[] memory unstETHIds) { + unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(stEthAmounts, address(this)); + WithdrawalRequestStatus[] memory statuses = WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + + uint256 sharesTotal = 0; + for (uint256 i = 0; i < statuses.length; ++i) { + sharesTotal += statuses[i].amountOfShares; + } + _accounting.accountStETHSharesUnlock(msg.sender, SharesValues.from(sharesTotal)); + _accounting.accountUnstETHLock(msg.sender, unstETHIds, statuses); + } + // --- // State Updates // --- - function startRageQuit(uint256 rageQuitExtraTimelock, uint256 rageQuitWithdrawalsTimelock) external { + function startRageQuit(Duration rageQuitExtensionDelay, Duration rageQuitWithdrawalsTimelock) external { _checkDualGovernance(msg.sender); _checkEscrowState(EscrowState.SignallingEscrow); + _batchesQueue.open(); _escrowState = EscrowState.RageQuitEscrow; - _rageQuitExtraTimelock = rageQuitExtraTimelock; + _rageQuitExtensionDelay = rageQuitExtensionDelay; _rageQuitWithdrawalsTimelock = rageQuitWithdrawalsTimelock; + } - uint256 wstETHBalance = WST_ETH.balanceOf(address(this)); - if (wstETHBalance > 0) { - WST_ETH.unwrap(wstETHBalance); + function requestNextWithdrawalsBatch(uint256 maxBatchSize) external { + _checkEscrowState(EscrowState.RageQuitEscrow); + _batchesQueue.checkOpened(); + + if (maxBatchSize < CONFIG.MIN_WITHDRAWALS_BATCH_SIZE() || maxBatchSize > CONFIG.MAX_WITHDRAWALS_BATCH_SIZE()) { + revert InvalidBatchSize(maxBatchSize); } - ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); + + uint256 stETHRemaining = ST_ETH.balanceOf(address(this)); + if (stETHRemaining < MIN_WITHDRAWAL_REQUEST_AMOUNT) { + return _batchesQueue.close(); + } + + uint256[] memory requestAmounts = WithdrawalsBatchesQueue.calcRequestAmounts({ + minRequestAmount: MIN_WITHDRAWAL_REQUEST_AMOUNT, + requestAmount: MAX_WITHDRAWAL_REQUEST_AMOUNT, + amount: Math.min(stETHRemaining, MAX_WITHDRAWAL_REQUEST_AMOUNT * maxBatchSize) + }); + + _batchesQueue.add(WITHDRAWAL_QUEUE.requestWithdrawals(requestAmounts, address(this))); } - function requestNextWithdrawalsBatch(uint256 maxWithdrawalRequestsCount) external { + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { _checkEscrowState(EscrowState.RageQuitEscrow); + if (!_rageQuitTimelockStartedAt.isZero()) { + revert ClaimingIsFinished(); + } + + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(maxUnstETHIdsCount); - uint256[] memory requestAmounts = _accounting.formWithdrawalBatch( - WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT(), - WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(), - ST_ETH.balanceOf(address(this)), - maxWithdrawalRequestsCount + _claimNextWithdrawalsBatch( + unstETHIds, WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, WITHDRAWAL_QUEUE.getLastCheckpointIndex()) ); - uint256[] memory unstETHIds = WITHDRAWAL_QUEUE.requestWithdrawals(requestAmounts, address(this)); - _accounting.accountWithdrawalBatch(unstETHIds); } - function claimNextWithdrawalsBatch(uint256 offset, uint256[] calldata hints) external { + function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external { _checkEscrowState(EscrowState.RageQuitEscrow); - uint256[] memory unstETHIds = _accounting.accountWithdrawalBatchClaimed(offset, hints.length); + if (!_rageQuitTimelockStartedAt.isZero()) { + revert ClaimingIsFinished(); + } - if (unstETHIds.length > 0) { - uint256 ethBalanceBefore = address(this).balance; - WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); - uint256 ethAmountClaimed = address(this).balance - ethBalanceBefore; + uint256[] memory unstETHIds = _batchesQueue.claimNextBatch(hints.length); - _accounting.accountClaimedETH(ethAmountClaimed); + if (unstETHIds.length > 0 && fromUnstETHId != unstETHIds[0]) { + revert UnexpectedUnstETHId(); } - if (_accounting.getIsWithdrawalsClaimed()) { - _rageQuitTimelockStartedAt = block.timestamp; + if (hints.length != unstETHIds.length) { + revert InvalidHintsLength(hints.length, unstETHIds.length); } + + _claimNextWithdrawalsBatch(unstETHIds, hints); } function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external { @@ -222,30 +269,26 @@ contract Escrow is IEscrow { WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); uint256 ethBalanceAfter = address(this).balance; - uint256 totalAmountClaimed = _accounting.accountUnstETHClaimed(unstETHIds, claimableAmounts); - assert(totalAmountClaimed == ethBalanceAfter - ethBalanceBefore); + ETHValue totalAmountClaimed = _accounting.accountUnstETHClaimed(unstETHIds, claimableAmounts); + assert(totalAmountClaimed == ETHValues.from(ethBalanceAfter - ethBalanceBefore)); } // --- // Withdraw Logic // --- - function withdrawStETHAsETH() external { + function withdrawETH() external { _checkEscrowState(EscrowState.RageQuitEscrow); _checkWithdrawalsTimelockPassed(); - Address.sendValue(payable(msg.sender), _accounting.accountStETHWithdraw(msg.sender)); + ETHValue ethToWithdraw = _accounting.accountStETHSharesWithdraw(msg.sender); + ethToWithdraw.sendTo(payable(msg.sender)); } - function withdrawWstETHAsETH() external { + function withdrawETH(uint256[] calldata unstETHIds) external { _checkEscrowState(EscrowState.RageQuitEscrow); _checkWithdrawalsTimelockPassed(); - Address.sendValue(payable(msg.sender), _accounting.accountWstETHWithdraw(msg.sender)); - } - - function withdrawUnstETHAsETH(uint256[] calldata unstETHIds) external { - _checkEscrowState(EscrowState.RageQuitEscrow); - _checkWithdrawalsTimelockPassed(); - Address.sendValue(payable(msg.sender), _accounting.accountUnstETHWithdraw(msg.sender, unstETHIds)); + ETHValue ethToWithdraw = _accounting.accountUnstETHWithdraw(msg.sender, unstETHIds); + ethToWithdraw.sendTo(payable(msg.sender)); } // --- @@ -253,54 +296,63 @@ contract Escrow is IEscrow { // --- function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory totals) { - totals = _accounting.totals; + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + totals.stETHClaimedETH = stETHTotals.claimedETH.toUint256(); + totals.stETHLockedShares = stETHTotals.lockedShares.toUint256(); + + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + totals.unstETHUnfinalizedShares = unstETHTotals.unfinalizedShares.toUint256(); + totals.unstETHFinalizedETH = unstETHTotals.finalizedETH.toUint256(); } - function getVetoerState(address vetoer) external view returns (VetoerState memory vetoerState) { - LockedAssetsStats memory stats = _accounting.assets[vetoer]; - vetoerState.stETHShares = stats.stETHShares; - vetoerState.wstETHShares = stats.wstETHShares; - vetoerState.unstETHShares = stats.unstETHShares; + function getVetoerState(address vetoer) external view returns (VetoerState memory state) { + HolderAssets storage assets = _accounting.assets[vetoer]; + + state.unstETHIdsCount = assets.unstETHIds.length; + state.stETHLockedShares = assets.stETHLockedShares.toUint256(); + state.unstETHLockedShares = assets.stETHLockedShares.toUint256(); + state.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp.toSeconds(); } - function getNextWithdrawalBatches(uint256 limit) - external - view - returns (uint256 offset, uint256 total, uint256[] memory unstETHIds) - { - offset = _accounting.claimedBatchesCount; - total = _accounting.withdrawalBatchIds.length; - if (total == offset) { - return (offset, total, unstETHIds); - } - uint256 count = Math.min(limit, total - offset); - unstETHIds = new uint256[](count); - for (uint256 i = 0; i < count; ++i) { - unstETHIds[i] = _accounting.withdrawalBatchIds[offset + i]; - } + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds) { + return _batchesQueue.getNextWithdrawalsBatches(limit); + } + + function isWithdrawalsBatchesFinalized() external view returns (bool) { + return _batchesQueue.isClosed(); } - function getIsWithdrawalsClaimed() external view returns (bool) { - return _accounting.getIsWithdrawalsClaimed(); + function isWithdrawalsClaimed() external view returns (bool) { + return !_rageQuitTimelockStartedAt.isZero(); } - function getRageQuitTimelockStartedAt() external view returns (uint256) { + function getRageQuitTimelockStartedAt() external view returns (Timestamp) { return _rageQuitTimelockStartedAt; } function getRageQuitSupport() external view returns (uint256 rageQuitSupport) { - (uint256 rebaseableShares, uint256 finalizedAmount) = _accounting.getLocked(); - uint256 rebaseableAmount = ST_ETH.getPooledEthByShares(rebaseableShares); - rageQuitSupport = (10 ** 18 * (rebaseableAmount + finalizedAmount)) / (ST_ETH.totalSupply() + finalizedAmount); + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + + uint256 finalizedETH = unstETHTotals.finalizedETH.toUint256(); + uint256 ufinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); + + rageQuitSupport = ( + 10 ** 18 * (ST_ETH.getPooledEthByShares(ufinalizedShares) + finalizedETH) + / (ST_ETH.totalSupply() + finalizedETH) + ); } function isRageQuitFinalized() external view returns (bool) { - return _escrowState == EscrowState.RageQuitEscrow && _accounting.getIsWithdrawalsClaimed() - && _rageQuitTimelockStartedAt != 0 && block.timestamp > _rageQuitTimelockStartedAt + _rageQuitExtraTimelock; + return ( + _escrowState == EscrowState.RageQuitEscrow && _batchesQueue.isClosed() + && !_rageQuitTimelockStartedAt.isZero() + && Timestamps.now() > _rageQuitExtensionDelay.addTo(_rageQuitTimelockStartedAt) + ); } // --- - // RECEIVE + // Receive ETH // --- receive() external payable { @@ -313,6 +365,20 @@ contract Escrow is IEscrow { // Internal Methods // --- + function _claimNextWithdrawalsBatch(uint256[] memory unstETHIds, uint256[] memory hints) internal { + uint256 ethBalanceBefore = address(this).balance; + WITHDRAWAL_QUEUE.claimWithdrawals(unstETHIds, hints); + uint256 ethAmountClaimed = address(this).balance - ethBalanceBefore; + + if (ethAmountClaimed > 0) { + _accounting.accountClaimedStETH(ETHValues.from(ethAmountClaimed)); + } + + if (_batchesQueue.isClosed() && _batchesQueue.isAllUnstETHClaimed()) { + _rageQuitTimelockStartedAt = Timestamps.now(); + } + } + function _activateNextGovernanceState() internal { _dualGovernance.activateNextState(); } @@ -330,10 +396,11 @@ contract Escrow is IEscrow { } function _checkWithdrawalsTimelockPassed() internal view { - if (_rageQuitTimelockStartedAt == 0) { + if (_rageQuitTimelockStartedAt.isZero()) { revert RageQuitExtraTimelockNotStarted(); } - if (block.timestamp <= _rageQuitTimelockStartedAt + _rageQuitExtraTimelock + _rageQuitWithdrawalsTimelock) { + Duration withdrawalsTimelock = _rageQuitExtensionDelay + _rageQuitWithdrawalsTimelock; + if (Timestamps.now() <= withdrawalsTimelock.addTo(_rageQuitTimelockStartedAt)) { revert WithdrawalsTimelockNotPassed(); } } diff --git a/contracts/GateSealBreaker.sol b/contracts/GateSealBreaker.sol deleted file mode 100644 index 58c75587..00000000 --- a/contracts/GateSealBreaker.sol +++ /dev/null @@ -1,128 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -import {IGateSeal} from "./interfaces/IGateSeal.sol"; -import {ISealable} from "./interfaces/ISealable.sol"; -import {SealableCalls} from "./libraries/SealableCalls.sol"; - -interface IDualGovernance { - function isSchedulingEnabled() external view returns (bool); -} - -contract GateSealBreaker is Ownable { - using SafeCast for uint256; - using SealableCalls for ISealable; - - struct GateSealState { - uint40 registeredAt; - uint40 releaseStartedAt; - uint40 releaseEnactedAt; - } - - error GovernanceLocked(); - error ReleaseNotStarted(); - error GateSealNotActivated(); - error ReleaseDelayNotPassed(); - error DualGovernanceIsLocked(); - error GateSealAlreadyReleased(); - error MinSealDurationNotPassed(); - error GateSealIsNotRegistered(IGateSeal gateSeal); - error GateSealAlreadyRegistered(IGateSeal gateSeal, uint256 registeredAt); - - event ReleaseIsPausedConditionNotMet(ISealable sealable); - event ReleaseResumeCallFailed(ISealable sealable, bytes lowLevelError); - event ReleaseIsPausedCheckFailed(ISealable sealable, bytes lowLevelError); - - uint256 public immutable RELEASE_DELAY; - IDualGovernance public immutable DUAL_GOVERNANCE; - - constructor(uint256 releaseDelay, address owner, address dualGovernance) Ownable(owner) { - RELEASE_DELAY = releaseDelay; - DUAL_GOVERNANCE = IDualGovernance(dualGovernance); - } - - mapping(IGateSeal gateSeal => GateSealState) internal _gateSeals; - - function registerGateSeal(IGateSeal gateSeal) external { - _checkOwner(); - if (_gateSeals[gateSeal].registeredAt != 0) { - revert GateSealAlreadyRegistered(gateSeal, _gateSeals[gateSeal].registeredAt); - } - _gateSeals[gateSeal].registeredAt = block.timestamp.toUint40(); - } - - function startRelease(IGateSeal gateSeal) external { - _checkGateSealRegistered(gateSeal); - _checkGateSealActivated(gateSeal); - _checkMinSealDurationPassed(gateSeal); - _checkGateSealNotReleased(gateSeal); - _checkGovernanceNotLocked(); - - _gateSeals[gateSeal].releaseStartedAt = block.timestamp.toUint40(); - } - - function enactRelease(IGateSeal gateSeal) external { - _checkGateSealRegistered(gateSeal); - GateSealState memory gateSealState = _gateSeals[gateSeal]; - if (gateSealState.releaseStartedAt == 0) { - revert ReleaseNotStarted(); - } - if (block.timestamp <= gateSealState.releaseStartedAt + RELEASE_DELAY) { - revert ReleaseDelayNotPassed(); - } - - _gateSeals[gateSeal].releaseEnactedAt = block.timestamp.toUint40(); - - address[] memory sealed_ = gateSeal.sealed_sealables(); - - for (uint256 i = 0; i < sealed_.length; ++i) { - ISealable sealable = ISealable(sealed_[i]); - (bool isPausedCallSuccess, bytes memory isPausedLowLevelError, bool isPaused) = sealable.callIsPaused(); - if (!isPausedCallSuccess) { - emit ReleaseIsPausedCheckFailed(sealable, isPausedLowLevelError); - } - if (!isPaused) { - emit ReleaseIsPausedConditionNotMet(sealable); - continue; - } - (bool resumeCallSuccess, bytes memory lowLevelError) = sealable.callResume(); - if (!resumeCallSuccess) { - emit ReleaseResumeCallFailed(sealable, lowLevelError); - } - } - } - - function _checkGateSealRegistered(IGateSeal gateSeal) internal view { - if (_gateSeals[gateSeal].registeredAt == 0) { - revert GateSealIsNotRegistered(gateSeal); - } - } - - function _checkGateSealActivated(IGateSeal gateSeal) internal view { - address[] memory sealed_ = gateSeal.sealed_sealables(); - if (sealed_.length == 0) { - revert GateSealNotActivated(); - } - } - - function _checkMinSealDurationPassed(IGateSeal gateSeal) internal view { - if (block.timestamp < gateSeal.get_expiry_timestamp() + gateSeal.get_min_seal_duration()) { - revert MinSealDurationNotPassed(); - } - } - - function _checkGateSealNotReleased(IGateSeal gateSeal) internal view { - if (_gateSeals[gateSeal].releaseStartedAt != 0) { - revert GateSealAlreadyReleased(); - } - } - - function _checkGovernanceNotLocked() internal view { - if (!DUAL_GOVERNANCE.isSchedulingEnabled()) { - revert GovernanceLocked(); - } - } -} diff --git a/contracts/ResealManager.sol b/contracts/ResealManager.sol new file mode 100644 index 00000000..5fdb2137 --- /dev/null +++ b/contracts/ResealManager.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {ISealable} from "./interfaces/ISealable.sol"; + +interface IEmergencyProtectedTimelock { + function getGovernance() external view returns (address); +} + +contract ResealManager { + error SealableWrongPauseState(); + error SenderIsNotGovernance(); + error NotAllowed(); + + uint256 public constant PAUSE_INFINITELY = type(uint256).max; + address public immutable EMERGENCY_PROTECTED_TIMELOCK; + + constructor(address emergencyProtectedTimelock) { + EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; + } + + function reseal(address[] memory sealables) public onlyGovernance { + for (uint256 i = 0; i < sealables.length; ++i) { + uint256 sealableResumeSinceTimestamp = ISealable(sealables[i]).getResumeSinceTimestamp(); + if (sealableResumeSinceTimestamp < block.timestamp || sealableResumeSinceTimestamp == PAUSE_INFINITELY) { + revert SealableWrongPauseState(); + } + Address.functionCall(sealables[i], abi.encodeWithSelector(ISealable.resume.selector)); + Address.functionCall(sealables[i], abi.encodeWithSelector(ISealable.pauseFor.selector, PAUSE_INFINITELY)); + } + } + + function resume(address sealable) public onlyGovernance { + uint256 sealableResumeSinceTimestamp = ISealable(sealable).getResumeSinceTimestamp(); + if (sealableResumeSinceTimestamp < block.timestamp) { + revert SealableWrongPauseState(); + } + Address.functionCall(sealable, abi.encodeWithSelector(ISealable.resume.selector)); + } + + modifier onlyGovernance() { + address governance = IEmergencyProtectedTimelock(EMERGENCY_PROTECTED_TIMELOCK).getGovernance(); + if (msg.sender != governance) { + revert SenderIsNotGovernance(); + } + _; + } +} diff --git a/contracts/committees/EmergencyActivationCommittee.sol b/contracts/committees/EmergencyActivationCommittee.sol new file mode 100644 index 00000000..038dd46b --- /dev/null +++ b/contracts/committees/EmergencyActivationCommittee.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {HashConsensus} from "./HashConsensus.sol"; + +interface IEmergencyProtectedTimelock { + function emergencyActivate() external; +} + +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, committeeMembers, executionQuorum, 0) { + EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; + } + + function approveEmergencyActivate() public onlyMember { + _vote(EMERGENCY_ACTIVATION_HASH, true); + } + + function getEmergencyActivateState() + public + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + { + return _getHashState(EMERGENCY_ACTIVATION_HASH); + } + + function executeEmergencyActivate() external { + _markUsed(EMERGENCY_ACTIVATION_HASH); + Address.functionCall( + EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyActivate.selector) + ); + } +} diff --git a/contracts/committees/EmergencyExecutionCommittee.sol b/contracts/committees/EmergencyExecutionCommittee.sol new file mode 100644 index 00000000..1c670227 --- /dev/null +++ b/contracts/committees/EmergencyExecutionCommittee.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {HashConsensus} from "./HashConsensus.sol"; +import {ProposalsList} from "./ProposalsList.sol"; + +interface IEmergencyProtectedTimelock { + function emergencyExecute(uint256 proposalId) external; + function emergencyReset() external; +} + +enum ProposalType { + EmergencyExecute, + EmergencyReset +} + +contract EmergencyExecutionCommittee is HashConsensus, ProposalsList { + address public immutable EMERGENCY_PROTECTED_TIMELOCK; + + constructor( + address owner, + address[] memory committeeMembers, + uint256 executionQuorum, + address emergencyProtectedTimelock + ) HashConsensus(owner, committeeMembers, executionQuorum, 0) { + EMERGENCY_PROTECTED_TIMELOCK = emergencyProtectedTimelock; + } + + // Emergency Execution + + function voteEmergencyExecute(uint256 proposalId, bool _supports) public onlyMember { + (bytes memory proposalData, bytes32 key) = _encodeEmergencyExecute(proposalId); + _vote(key, _supports); + _pushProposal(key, uint256(ProposalType.EmergencyExecute), proposalData); + } + + function getEmergencyExecuteState(uint256 proposalId) + public + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + { + (, bytes32 key) = _encodeEmergencyExecute(proposalId); + return _getHashState(key); + } + + function executeEmergencyExecute(uint256 proposalId) public { + (, bytes32 key) = _encodeEmergencyExecute(proposalId); + _markUsed(key); + Address.functionCall( + EMERGENCY_PROTECTED_TIMELOCK, + abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyExecute.selector, proposalId) + ); + } + + function _encodeEmergencyExecute(uint256 proposalId) + private + pure + returns (bytes memory proposalData, bytes32 key) + { + proposalData = abi.encode(ProposalType.EmergencyExecute, proposalId); + key = keccak256(proposalData); + } + + // Governance reset + + function approveEmergencyReset() public onlyMember { + bytes32 proposalKey = _encodeEmergencyResetProposalKey(); + _vote(proposalKey, true); + _pushProposal(proposalKey, uint256(ProposalType.EmergencyReset), bytes("")); + } + + function getEmergencyResetState() + public + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + { + bytes32 proposalKey = _encodeEmergencyResetProposalKey(); + return _getHashState(proposalKey); + } + + function executeEmergencyReset() external { + bytes32 proposalKey = _encodeEmergencyResetProposalKey(); + _markUsed(proposalKey); + Address.functionCall( + EMERGENCY_PROTECTED_TIMELOCK, abi.encodeWithSelector(IEmergencyProtectedTimelock.emergencyReset.selector) + ); + } + + 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 new file mode 100644 index 00000000..fb4a2d65 --- /dev/null +++ b/contracts/committees/HashConsensus.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +abstract contract HashConsensus is Ownable { + using EnumerableSet for EnumerableSet.AddressSet; + + event MemberAdded(address indexed member); + event MemberRemoved(address indexed member); + event QuorumSet(uint256 quorum); + event HashUsed(bytes32 hash); + event Voted(address indexed signer, bytes32 hash, bool support); + event TimelockDurationSet(uint256 timelockDuration); + + error IsNotMember(); + error SenderIsNotMember(); + error HashAlreadyUsed(); + error QuorumIsNotReached(); + error InvalidQuorum(); + error DuplicatedMember(address member); + error TimelockNotPassed(); + + struct HashState { + uint40 quorumAt; + uint40 usedAt; + } + + uint256 public quorum; + uint256 public timelockDuration; + + mapping(bytes32 => HashState) private _hashStates; + EnumerableSet.AddressSet private _members; + mapping(address signer => mapping(bytes32 => bool)) public approves; + + constructor(address owner, address[] memory newMembers, uint256 executionQuorum, uint256 timelock) Ownable(owner) { + if (executionQuorum == 0) { + revert InvalidQuorum(); + } + quorum = executionQuorum; + emit QuorumSet(executionQuorum); + + timelockDuration = timelock; + emit TimelockDurationSet(timelock); + + for (uint256 i = 0; i < newMembers.length; ++i) { + _addMember(newMembers[i]); + } + } + + function _vote(bytes32 hash, bool support) internal { + if (_hashStates[hash].usedAt > 0) { + revert HashAlreadyUsed(); + } + + if (approves[msg.sender][hash] == support) { + return; + } + + uint256 heads = _getSupport(hash); + if (heads == quorum - 1 && support == true) { + _hashStates[hash].quorumAt = uint40(block.timestamp); + } + + approves[msg.sender][hash] = support; + emit Voted(msg.sender, hash, support); + } + + function _markUsed(bytes32 hash) internal { + if (_hashStates[hash].usedAt > 0) { + revert HashAlreadyUsed(); + } + if (_getSupport(hash) < quorum) { + revert QuorumIsNotReached(); + } + if (block.timestamp < _hashStates[hash].quorumAt + timelockDuration) { + revert TimelockNotPassed(); + } + + _hashStates[hash].usedAt = uint40(block.timestamp); + + emit HashUsed(hash); + } + + function _getHashState(bytes32 hash) + internal + view + returns (uint256 support, uint256 execuitionQuorum, bool isUsed) + { + support = _getSupport(hash); + execuitionQuorum = quorum; + isUsed = _hashStates[hash].usedAt > 0; + } + + function addMember(address newMember, uint256 newQuorum) public onlyOwner { + _addMember(newMember); + + if (newQuorum == 0 || newQuorum > _members.length()) { + revert InvalidQuorum(); + } + quorum = newQuorum; + emit QuorumSet(newQuorum); + } + + function removeMember(address memberToRemove, uint256 newQuorum) public onlyOwner { + if (!_members.contains(memberToRemove)) { + revert IsNotMember(); + } + _members.remove(memberToRemove); + emit MemberRemoved(memberToRemove); + + if (newQuorum == 0 || newQuorum > _members.length()) { + revert InvalidQuorum(); + } + quorum = newQuorum; + emit QuorumSet(newQuorum); + } + + function getMembers() public view returns (address[] memory) { + return _members.values(); + } + + function isMember(address member) public view returns (bool) { + return _members.contains(member); + } + + function setTimelockDuration(uint256 timelock) public onlyOwner { + timelockDuration = timelock; + emit TimelockDurationSet(timelock); + } + + function setQuorum(uint256 newQuorum) public onlyOwner { + if (newQuorum == 0 || newQuorum > _members.length()) { + revert InvalidQuorum(); + } + + quorum = newQuorum; + emit QuorumSet(newQuorum); + } + + function _addMember(address newMember) internal { + if (_members.contains(newMember)) { + revert DuplicatedMember(newMember); + } + _members.add(newMember); + emit MemberAdded(newMember); + } + + function _getSupport(bytes32 hash) internal view returns (uint256 support) { + for (uint256 i = 0; i < _members.length(); ++i) { + if (approves[_members.at(i)][hash]) { + support++; + } + } + } + + modifier onlyMember() { + if (!_members.contains(msg.sender)) { + revert SenderIsNotMember(); + } + _; + } +} diff --git a/contracts/committees/ProposalsList.sol b/contracts/committees/ProposalsList.sol new file mode 100644 index 00000000..625a5841 --- /dev/null +++ b/contracts/committees/ProposalsList.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {EnumerableProposals, Proposal} from "../libraries/EnumerableProposals.sol"; + +contract ProposalsList { + using EnumerableProposals for EnumerableProposals.Bytes32ToProposalMap; + + EnumerableProposals.Bytes32ToProposalMap internal _proposals; + + function getProposals(uint256 offset, uint256 limit) public view returns (Proposal[] memory proposals) { + bytes32[] memory keys = _proposals.orederedKeys(offset, limit); + + uint256 length = keys.length; + proposals = new Proposal[](length); + + for (uint256 i = 0; i < length; ++i) { + proposals[i] = _proposals.get(keys[i]); + } + } + + function getProposalAt(uint256 index) public view returns (Proposal memory) { + return _proposals.at(index); + } + + function getProposal(bytes32 key) public view returns (Proposal memory) { + return _proposals.get(key); + } + + function proposalsLength() public view returns (uint256) { + return _proposals.length(); + } + + function orederedKeys(uint256 offset, uint256 limit) public view returns (bytes32[] memory) { + return _proposals.orederedKeys(offset, limit); + } + + function _pushProposal(bytes32 key, uint256 proposalType, bytes memory data) internal { + _proposals.push(key, proposalType, data); + } +} diff --git a/contracts/committees/ResealCommittee.sol b/contracts/committees/ResealCommittee.sol new file mode 100644 index 00000000..1b40a5d6 --- /dev/null +++ b/contracts/committees/ResealCommittee.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {HashConsensus} from "./HashConsensus.sol"; +import {ProposalsList} from "./ProposalsList.sol"; + +interface IDualGovernance { + function reseal(address[] memory sealables) external; +} + +contract ResealCommittee is HashConsensus, ProposalsList { + address public immutable DUAL_GOVERNANCE; + + mapping(bytes32 => uint256) private _resealNonces; + + constructor( + address owner, + address[] memory committeeMembers, + uint256 executionQuorum, + address dualGovernance, + uint256 timelock + ) HashConsensus(owner, committeeMembers, executionQuorum, timelock) { + DUAL_GOVERNANCE = dualGovernance; + } + + function voteReseal(address[] memory sealables, bool support) public onlyMember { + (bytes memory proposalData, bytes32 key) = _encodeResealProposal(sealables); + _vote(key, support); + _pushProposal(key, 0, proposalData); + } + + function getResealState(address[] memory sealables) + public + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + { + (, bytes32 key) = _encodeResealProposal(sealables); + return _getHashState(key); + } + + function executeReseal(address[] memory sealables) external { + (, bytes32 key) = _encodeResealProposal(sealables); + _markUsed(key); + + Address.functionCall(DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.reseal.selector, sealables)); + + bytes32 resealNonceHash = keccak256(abi.encode(sealables)); + _resealNonces[resealNonceHash]++; + } + + function _encodeResealProposal(address[] memory sealables) internal view returns (bytes memory data, bytes32 key) { + bytes32 resealNonceHash = keccak256(abi.encode(sealables)); + data = abi.encode(sealables, _resealNonces[resealNonceHash]); + key = keccak256(data); + } +} diff --git a/contracts/committees/TiebreakerCore.sol b/contracts/committees/TiebreakerCore.sol new file mode 100644 index 00000000..a380036a --- /dev/null +++ b/contracts/committees/TiebreakerCore.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {HashConsensus} from "./HashConsensus.sol"; +import {ProposalsList} from "./ProposalsList.sol"; + +interface IDualGovernance { + function tiebreakerScheduleProposal(uint256 proposalId) external; + function tiebreakerResumeSealable(address sealable) external; +} + +enum ProposalType { + ScheduleProposal, + ResumeSelable +} + +contract TiebreakerCore is HashConsensus, ProposalsList { + error ResumeSealableNonceMismatch(); + + address immutable DUAL_GOVERNANCE; + + mapping(address => uint256) private _sealableResumeNonces; + + constructor( + address owner, + address[] memory committeeMembers, + uint256 executionQuorum, + address dualGovernance, + uint256 timelock + ) HashConsensus(owner, committeeMembers, executionQuorum, timelock) { + DUAL_GOVERNANCE = dualGovernance; + } + + // Schedule proposal + + function scheduleProposal(uint256 proposalId) public onlyMember { + (bytes memory proposalData, bytes32 key) = _encodeScheduleProposal(proposalId); + _vote(key, true); + _pushProposal(key, uint256(ProposalType.ScheduleProposal), proposalData); + } + + function getScheduleProposalState(uint256 proposalId) + public + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + { + (, bytes32 key) = _encodeScheduleProposal(proposalId); + return _getHashState(key); + } + + function executeScheduleProposal(uint256 proposalId) public { + (, bytes32 key) = _encodeScheduleProposal(proposalId); + _markUsed(key); + Address.functionCall( + DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.tiebreakerScheduleProposal.selector, proposalId) + ); + } + + function _encodeScheduleProposal(uint256 proposalId) internal pure returns (bytes memory data, bytes32 key) { + data = abi.encode(ProposalType.ScheduleProposal, proposalId); + key = keccak256(data); + } + + // Resume sealable + + function getSealableResumeNonce(address sealable) public view returns (uint256) { + return _sealableResumeNonces[sealable]; + } + + function sealableResume(address sealable, uint256 nonce) public onlyMember { + if (nonce != _sealableResumeNonces[sealable]) { + revert ResumeSealableNonceMismatch(); + } + (bytes memory proposalData, bytes32 key) = _encodeSealableResume(sealable, nonce); + _vote(key, true); + _pushProposal(key, uint256(ProposalType.ResumeSelable), proposalData); + } + + function getSealableResumeState( + address sealable, + uint256 nonce + ) public view returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) { + (, bytes32 key) = _encodeSealableResume(sealable, nonce); + return _getHashState(key); + } + + function executeSealableResume(address sealable) external { + (, bytes32 key) = _encodeSealableResume(sealable, _sealableResumeNonces[sealable]); + _markUsed(key); + _sealableResumeNonces[sealable]++; + Address.functionCall( + DUAL_GOVERNANCE, abi.encodeWithSelector(IDualGovernance.tiebreakerResumeSealable.selector, sealable) + ); + } + + function _encodeSealableResume( + address sealable, + uint256 nonce + ) private pure returns (bytes memory data, bytes32 key) { + data = abi.encode(ProposalType.ResumeSelable, sealable, nonce); + key = keccak256(data); + } +} diff --git a/contracts/committees/TiebreakerSubCommittee.sol b/contracts/committees/TiebreakerSubCommittee.sol new file mode 100644 index 00000000..1ceb40fc --- /dev/null +++ b/contracts/committees/TiebreakerSubCommittee.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {HashConsensus} from "./HashConsensus.sol"; +import {ProposalsList} from "./ProposalsList.sol"; + +interface ITiebreakerCore { + function getSealableResumeNonce(address sealable) external view returns (uint256 nonce); + function scheduleProposal(uint256 _proposalId) external; + function sealableResume(address sealable, uint256 nonce) external; +} + +enum ProposalType { + ScheduleProposal, + ResumeSelable +} + +contract TiebreakerSubCommittee is HashConsensus, ProposalsList { + address immutable TIEBREAKER_CORE; + + constructor( + address owner, + address[] memory committeeMembers, + uint256 executionQuorum, + address tiebreakerCore + ) HashConsensus(owner, committeeMembers, executionQuorum, 0) { + TIEBREAKER_CORE = tiebreakerCore; + } + + // Schedule proposal + + function scheduleProposal(uint256 proposalId) public onlyMember { + (bytes memory proposalData, bytes32 key) = _encodeAproveProposal(proposalId); + _vote(key, true); + _pushProposal(key, uint256(ProposalType.ScheduleProposal), proposalData); + } + + function getScheduleProposalState(uint256 proposalId) + public + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + { + (, bytes32 key) = _encodeAproveProposal(proposalId); + return _getHashState(key); + } + + function executeScheduleProposal(uint256 proposalId) public { + (, bytes32 key) = _encodeAproveProposal(proposalId); + _markUsed(key); + Address.functionCall( + TIEBREAKER_CORE, abi.encodeWithSelector(ITiebreakerCore.scheduleProposal.selector, proposalId) + ); + } + + function _encodeAproveProposal(uint256 proposalId) internal pure returns (bytes memory data, bytes32 key) { + data = abi.encode(ProposalType.ScheduleProposal, data); + key = keccak256(data); + } + + // Sealable resume + + function sealableResume(address sealable) public { + (bytes memory proposalData, bytes32 key,) = _encodeSealableResume(sealable); + _vote(key, true); + _pushProposal(key, uint256(ProposalType.ResumeSelable), proposalData); + } + + function getSealableResumeState(address sealable) + public + view + returns (uint256 support, uint256 execuitionQuorum, bool isExecuted) + { + (, bytes32 key,) = _encodeSealableResume(sealable); + return _getHashState(key); + } + + function executeSealableResume(address sealable) public { + (, bytes32 key, uint256 nonce) = _encodeSealableResume(sealable); + _markUsed(key); + Address.functionCall( + TIEBREAKER_CORE, abi.encodeWithSelector(ITiebreakerCore.sealableResume.selector, sealable, nonce) + ); + } + + function _encodeSealableResume(address sealable) + internal + view + returns (bytes memory data, bytes32 key, uint256 nonce) + { + nonce = ITiebreakerCore(TIEBREAKER_CORE).getSealableResumeNonce(sealable); + data = abi.encode(sealable, nonce); + key = keccak256(data); + } +} diff --git a/contracts/interfaces/IConfiguration.sol b/contracts/interfaces/IConfiguration.sol index 04aa9f14..1b572e87 100644 --- a/contracts/interfaces/IConfiguration.sol +++ b/contracts/interfaces/IConfiguration.sol @@ -1,20 +1,27 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Duration} from "../types/Duration.sol"; + struct DualGovernanceConfig { uint256 firstSealRageQuitSupport; uint256 secondSealRageQuitSupport; - // TODO: consider dynamicDelayMaxDuration - uint256 dynamicTimelockMaxDuration; - uint256 dynamicTimelockMinDuration; - uint256 vetoSignallingMinActiveDuration; - uint256 vetoSignallingDeactivationMaxDuration; - uint256 vetoCooldownDuration; - uint256 rageQuitExtraTimelock; - uint256 rageQuitExtensionDelay; - uint256 rageQuitEthClaimMinTimelock; - uint256 rageQuitEthClaimTimelockGrowthStartSeqNumber; - uint256[3] rageQuitEthClaimTimelockGrowthCoeffs; + Duration dynamicTimelockMaxDuration; + Duration dynamicTimelockMinDuration; + Duration vetoSignallingMinActiveDuration; + Duration vetoSignallingDeactivationMaxDuration; + Duration vetoCooldownDuration; + Duration rageQuitExtraTimelock; + Duration rageQuitExtensionDelay; + Duration rageQuitEthWithdrawalsMinTimelock; + uint256 rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber; + uint256[3] rageQuitEthWithdrawalsTimelockGrowthCoeffs; +} + +interface IEscrowConfigration { + function MIN_WITHDRAWALS_BATCH_SIZE() external view returns (uint256); + function MAX_WITHDRAWALS_BATCH_SIZE() external view returns (uint256); + function SIGNALLING_ESCROW_MIN_LOCK_TIME() external view returns (Duration); } interface IAdminExecutorConfiguration { @@ -22,35 +29,33 @@ interface IAdminExecutorConfiguration { } interface ITimelockConfiguration { - function AFTER_SUBMIT_DELAY() external view returns (uint256); - function AFTER_SCHEDULE_DELAY() external view returns (uint256); + function AFTER_SUBMIT_DELAY() external view returns (Duration); + function AFTER_SCHEDULE_DELAY() external view returns (Duration); function EMERGENCY_GOVERNANCE() external view returns (address); } interface IDualGovernanceConfiguration { - function TIE_BREAK_ACTIVATION_TIMEOUT() external view returns (uint256); + function TIE_BREAK_ACTIVATION_TIMEOUT() external view returns (Duration); - function VETO_COOLDOWN_DURATION() external view returns (uint256); - function VETO_SIGNALLING_MIN_ACTIVE_DURATION() external view returns (uint256); + function VETO_COOLDOWN_DURATION() external view returns (Duration); + function VETO_SIGNALLING_MIN_ACTIVE_DURATION() external view returns (Duration); - function VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() external view returns (uint256); + function VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() external view returns (Duration); - function DYNAMIC_TIMELOCK_MIN_DURATION() external view returns (uint256); - function DYNAMIC_TIMELOCK_MAX_DURATION() external view returns (uint256); + function DYNAMIC_TIMELOCK_MIN_DURATION() external view returns (Duration); + function DYNAMIC_TIMELOCK_MAX_DURATION() external view returns (Duration); function FIRST_SEAL_RAGE_QUIT_SUPPORT() external view returns (uint256); function SECOND_SEAL_RAGE_QUIT_SUPPORT() external view returns (uint256); - function RAGE_QUIT_EXTENSION_DELAY() external view returns (uint256); - function RAGE_QUIT_ETH_CLAIM_MIN_TIMELOCK() external view returns (uint256); - function RAGE_QUIT_ACCUMULATION_MAX_DURATION() external view returns (uint256); - function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_START_SEQ_NUMBER() external view returns (uint256); - - function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_A() external view returns (uint256); - function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_B() external view returns (uint256); - function RAGE_QUIT_ETH_CLAIM_TIMELOCK_GROWTH_COEFF_C() external view returns (uint256); + function RAGE_QUIT_EXTENSION_DELAY() external view returns (Duration); + function RAGE_QUIT_ETH_WITHDRAWALS_MIN_TIMELOCK() external view returns (Duration); + function RAGE_QUIT_ACCUMULATION_MAX_DURATION() external view returns (Duration); + function RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_START_SEQ_NUMBER() external view returns (uint256); - function SIGNALLING_ESCROW_MIN_LOCK_TIME() external view returns (uint256); + function RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_A() external view returns (uint256); + function RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_B() external view returns (uint256); + function RAGE_QUIT_ETH_WITHDRAWALS_TIMELOCK_GROWTH_COEFF_C() external view returns (uint256); function sealableWithdrawalBlockers() external view returns (address[] memory); @@ -60,11 +65,16 @@ interface IDualGovernanceConfiguration { returns ( uint256 firstSealThreshold, uint256 secondSealThreshold, - uint256 signallingMinDuration, - uint256 signallingMaxDuration + Duration signallingMinDuration, + Duration signallingMaxDuration ); function getDualGovernanceConfig() external view returns (DualGovernanceConfig memory config); } -interface IConfiguration is IAdminExecutorConfiguration, ITimelockConfiguration, IDualGovernanceConfiguration {} +interface IConfiguration is + IEscrowConfigration, + ITimelockConfiguration, + IAdminExecutorConfiguration, + IDualGovernanceConfiguration +{} diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol index d8c44087..3586d311 100644 --- a/contracts/interfaces/IEscrow.sol +++ b/contracts/interfaces/IEscrow.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Duration} from "../types/Duration.sol"; + interface IEscrow { function initialize(address dualGovernance) external; - function startRageQuit(uint256 rageQuitExtraTimelock, uint256 rageQuitWithdrawalsTimelock) external; + function startRageQuit(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock) external; function MASTER_COPY() external view returns (address); function isRageQuitFinalized() external view returns (bool); diff --git a/contracts/interfaces/IGateSeal.sol b/contracts/interfaces/IGateSeal.sol index 2b2a121d..9dd6d07e 100644 --- a/contracts/interfaces/IGateSeal.sol +++ b/contracts/interfaces/IGateSeal.sol @@ -2,8 +2,5 @@ pragma solidity 0.8.23; interface IGateSeal { - function get_min_seal_duration() external view returns (uint256); - function get_expiry_timestamp() external view returns (uint256); - function sealed_sealables() external view returns (address[] memory); function seal(address[] calldata sealables) external; } diff --git a/contracts/interfaces/IResealManager.sol b/contracts/interfaces/IResealManager.sol new file mode 100644 index 00000000..ca1f2ee4 --- /dev/null +++ b/contracts/interfaces/IResealManager.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +interface IResealManager { + function resume(address sealable) external; + function reseal(address[] memory sealables) external; +} diff --git a/contracts/interfaces/ISealable.sol b/contracts/interfaces/ISealable.sol index 12239b92..df924ec5 100644 --- a/contracts/interfaces/ISealable.sol +++ b/contracts/interfaces/ISealable.sol @@ -5,4 +5,5 @@ interface ISealable { function resume() external; function pauseFor(uint256 duration) external; function isPaused() external view returns (bool); + function getResumeSinceTimestamp() external view returns (uint256); } diff --git a/contracts/interfaces/ITimelock.sol b/contracts/interfaces/ITimelock.sol index 0f080e8d..1b2118ae 100644 --- a/contracts/interfaces/ITimelock.sol +++ b/contracts/interfaces/ITimelock.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Timestamp} from "../types/Timestamp.sol"; import {ExecutorCall} from "./IExecutor.sol"; interface IGovernance { @@ -13,10 +14,12 @@ interface IGovernance { interface ITimelock { function submit(address executor, ExecutorCall[] calldata calls) external returns (uint256 newProposalId); - function schedule(uint256 proposalId) external returns (uint256 submittedAt); + function schedule(uint256 proposalId) external; function execute(uint256 proposalId) external; function cancelAllNonExecutedProposals() external; function canSchedule(uint256 proposalId) external view returns (bool); function canExecute(uint256 proposalId) external view returns (bool); + + function getProposalSubmissionTime(uint256 proposalId) external view returns (Timestamp submittedAt); } diff --git a/contracts/interfaces/IWithdrawalQueue.sol b/contracts/interfaces/IWithdrawalQueue.sol index e3beea17..0d2ac472 100644 --- a/contracts/interfaces/IWithdrawalQueue.sol +++ b/contracts/interfaces/IWithdrawalQueue.sol @@ -37,6 +37,13 @@ interface IWithdrawalQueue { uint256[] calldata _hints ) external view returns (uint256[] memory claimableEthValues); + function findCheckpointHints( + uint256[] calldata _requestIds, + uint256 _firstIndex, + uint256 _lastIndex + ) external view returns (uint256[] memory hintIds); + function getLastCheckpointIndex() external view returns (uint256); + function balanceOf(address owner) external view returns (uint256); function requestWithdrawals( @@ -48,4 +55,8 @@ interface IWithdrawalQueue { uint256[] calldata _amounts, address _owner ) external returns (uint256[] memory requestIds); + + function grantRole(bytes32 role, address account) external; + function pauseFor(uint256 duration) external; + function isPaused() external returns (bool); } diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index afee69ec..2de7288d 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -1,14 +1,41 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {ETHValue, ETHValues} from "../types/ETHValue.sol"; +import {SharesValue, SharesValues} from "../types/SharesValue.sol"; +import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; import {WithdrawalRequestStatus} from "../interfaces/IWithdrawalQueue.sol"; -import {TimeUtils} from "../utils/time.sol"; -import {ArrayUtils} from "../utils/arrays.sol"; +import {Duration} from "../types/Duration.sol"; +import {Timestamps, Timestamp} from "../types/Timestamp.sol"; + +struct HolderAssets { + // The total shares amount of stETH/wstETH accounted to the holder + SharesValue stETHLockedShares; + // The total shares amount of unstETH NFTs accounted to the holder + SharesValue unstETHLockedShares; + // The timestamp when the last time was accounted lock of shares or unstETHs + Timestamp lastAssetsLockTimestamp; + // The ids of the unstETH NFTs accounted to the holder + uint256[] unstETHIds; +} + +struct UnstETHAccounting { + // The cumulative amount of unfinalized unstETH shares locked in the Escrow + SharesValue unfinalizedShares; + // The total amount of ETH claimable from the finalized unstETH locked in the Escrow + ETHValue finalizedETH; +} + +struct StETHAccounting { + // The total amount of shares of locked stETH and wstETH tokens + SharesValue lockedShares; + // The total amount of ETH received during the claiming of the locked stETH + ETHValue claimedETH; +} -enum WithdrawalRequestState { +enum UnstETHRecordStatus { NotLocked, Locked, Finalized, @@ -16,224 +43,148 @@ enum WithdrawalRequestState { Withdrawn } -struct WithdrawalRequest { - address owner; - uint96 claimableAmount; - uint128 shares; - uint64 vetoerUnstETHIndexOneBased; - WithdrawalRequestState state; -} - -struct LockedAssetsStats { - uint128 stETHShares; - uint128 wstETHShares; - uint128 unstETHShares; - uint128 sharesFinalized; - uint128 amountFinalized; - uint40 lastAssetsLockTimestamp; -} - -struct LockedAssetsTotals { - uint128 shares; - uint128 sharesFinalized; - uint128 amountFinalized; - uint128 amountClaimed; +struct UnstETHRecord { + // The one based index of the unstETH record in the UnstETHAccounting.unstETHIds list + IndexOneBased index; + // The address of the holder who locked unstETH + address lockedBy; + // The current status of the unstETH + UnstETHRecordStatus status; + // The amount of shares contained in the unstETH + SharesValue shares; + // The amount of ETH contained in the unstETH (this value equals to 0 until NFT is mark as finalized or claimed) + ETHValue claimableAmount; } library AssetsAccounting { - using SafeCast for uint256; - - event StETHLocked(address indexed vetoer, uint256 shares); - event StETHUnlocked(address indexed vetoer, uint256 shares); - event StETHWithdrawn(address indexed vetoer, uint256 stETHShares, uint256 ethAmount); + struct State { + StETHAccounting stETHTotals; + UnstETHAccounting unstETHTotals; + mapping(address account => HolderAssets) assets; + mapping(uint256 unstETHId => UnstETHRecord) unstETHRecords; + } - event WstETHLocked(address indexed vetoer, uint256 shares); - event WstETHUnlocked(address indexed vetoer, uint256 shares); - event WstETHWithdrawn(address indexed vetoer, uint256 wstETHShares, uint256 ethAmount); + // --- + // Events + // --- - event UnstETHLocked(address indexed vetoer, uint256[] ids, uint256 shares); + event ETHWithdrawn(address indexed holder, SharesValue shares, ETHValue value); + event StETHSharesLocked(address indexed holder, SharesValue shares); + event StETHSharesUnlocked(address indexed holder, SharesValue shares); + event UnstETHFinalized(uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement); event UnstETHUnlocked( - address indexed vetoer, - uint256[] ids, - uint256 sharesDecrement, - uint256 finalizedSharesDecrement, - uint256 finalizedAmountDecrement + address indexed holder, uint256[] ids, SharesValue finalizedSharesIncrement, ETHValue finalizedAmountIncrement ); - event UnstETHFinalized(uint256[] ids, uint256 finalizedSharesIncrement, uint256 finalizedAmountIncrement); - event UnstETHClaimed(uint256[] ids, uint256 ethAmount); - event UnstETHWithdrawn(uint256[] ids, uint256 ethAmount); - - event WithdrawalBatchCreated(uint256[] ids); - event WithdrawalBatchesClaimed(uint256 offset, uint256 count); - - error NoBatchesToClaim(); - error EmptyWithdrawalBatch(); - error WithdrawalBatchesFormed(); - error NotWithdrawalRequestOwner(uint256 id, address actual, address expected); - error InvalidSharesLock(address vetoer, uint256 shares); - error InvalidSharesUnlock(address vetoer, uint256 shares); - error InvalidSharesWithdraw(address vetoer, uint256 shares); - error WithdrawalRequestFinalized(uint256 id); - error ClaimableAmountChanged(uint256 id, uint256 actual, uint256 expected); - error WithdrawalRequestNotClaimable(uint256 id, WithdrawalRequestState state); - error WithdrawalRequestWasNotLocked(uint256 id); - error WithdrawalRequestAlreadyLocked(uint256 id); - error InvalidUnstETHOwner(address actual, address expected); - error InvalidWithdrawlRequestState(uint256 id, WithdrawalRequestState actual, WithdrawalRequestState expected); - error InvalidWithdrawalBatchesOffset(uint256 actual, uint256 expected); - error InvalidWithdrawalBatchesCount(uint256 actual, uint256 expected); - error AssetsUnlockDelayNotPassed(uint256 unlockTimelockExpiresAt); - error NotEnoughStETHToUnlock(uint256 requested, uint256 sharesBalance); + event UnstETHLocked(address indexed holder, uint256[] ids, SharesValue shares); + event UnstETHClaimed(uint256[] unstETHIds, ETHValue totalAmountClaimed); + event UnstETHWithdrawn(uint256[] unstETHIds, ETHValue amountWithdrawn); - struct State { - LockedAssetsTotals totals; - mapping(address vetoer => LockedAssetsStats) assets; - mapping(uint256 unstETHId => WithdrawalRequest) requests; - mapping(address vetoer => uint256[] unstETHIds) vetoersUnstETHIds; - uint256[] withdrawalBatchIds; - uint256 claimedBatchesCount; - bool isAllWithdrawalBatchesFormed; - } + event ETHClaimed(ETHValue amount); // --- - // stETH Operations Accounting + // Errors // --- - function accountStETHLock(State storage self, address vetoer, uint256 shares) internal { - _checkNonZeroSharesLock(vetoer, shares); - uint128 sharesUint128 = shares.toUint128(); - self.assets[vetoer].stETHShares += sharesUint128; - self.assets[vetoer].lastAssetsLockTimestamp = TimeUtils.timestamp(); - self.totals.shares += sharesUint128; - emit StETHLocked(vetoer, shares); - } + error InvalidSharesValue(SharesValue value); + error InvalidUnstETHStatus(uint256 unstETHId, UnstETHRecordStatus status); + error InvalidUnstETHHolder(uint256 unstETHId, address actual, address expected); + error AssetsUnlockDelayNotPassed(Timestamp unlockTimelockExpiresAt); + error InvalidClaimableAmount(uint256 unstETHId, ETHValue expected, ETHValue actual); - function accountStETHUnlock(State storage self, address vetoer) internal returns (uint128 sharesUnlocked) { - sharesUnlocked = accountStETHUnlock(self, vetoer, self.assets[vetoer].stETHShares); - } + // --- + // stETH shares operations accounting + // --- - function accountStETHUnlock( - State storage self, - address vetoer, - uint256 shares - ) internal returns (uint128 sharesUnlocked) { - _checkStETHSharesUnlock(self, vetoer, shares); - sharesUnlocked = shares.toUint128(); - self.totals.shares -= sharesUnlocked; - self.assets[vetoer].stETHShares -= sharesUnlocked; - emit StETHUnlocked(vetoer, sharesUnlocked); + function accountStETHSharesLock(State storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares + shares; + HolderAssets storage assets = self.assets[holder]; + assets.stETHLockedShares = assets.stETHLockedShares + shares; + assets.lastAssetsLockTimestamp = Timestamps.now(); + emit StETHSharesLocked(holder, shares); } - function accountStETHWithdraw(State storage self, address vetoer) internal returns (uint256 ethAmount) { - uint256 stETHShares = self.assets[vetoer].stETHShares; - _checkNonZeroSharesWithdraw(vetoer, stETHShares); - self.assets[vetoer].stETHShares = 0; - ethAmount = self.totals.amountClaimed * stETHShares / self.totals.shares; - emit StETHWithdrawn(vetoer, stETHShares, ethAmount); + function accountStETHSharesUnlock(State storage self, address holder) internal returns (SharesValue shares) { + shares = self.assets[holder].stETHLockedShares; + accountStETHSharesUnlock(self, holder, shares); } - // --- - // wstETH Operations Accounting - // --- + function accountStETHSharesUnlock(State storage self, address holder, SharesValue shares) internal { + _checkNonZeroShares(shares); - function checkAssetsUnlockDelayPassed(State storage self, address vetoer, uint256 delay) internal view { - _checkAssetsUnlockDelayPassed(self, delay, vetoer); - } + HolderAssets storage assets = self.assets[holder]; + if (assets.stETHLockedShares < shares) { + revert InvalidSharesValue(shares); + } - function accountWstETHLock(State storage self, address vetoer, uint256 shares) internal { - _checkNonZeroSharesLock(vetoer, shares); - uint128 sharesUint128 = shares.toUint128(); - self.assets[vetoer].wstETHShares += sharesUint128; - self.assets[vetoer].lastAssetsLockTimestamp = TimeUtils.timestamp(); - self.totals.shares += sharesUint128; - emit WstETHLocked(vetoer, shares); + self.stETHTotals.lockedShares = self.stETHTotals.lockedShares - shares; + assets.stETHLockedShares = assets.stETHLockedShares - shares; + emit StETHSharesUnlocked(holder, shares); } - function accountWstETHUnlock(State storage self, address vetoer) internal returns (uint128 sharesUnlocked) { - sharesUnlocked = accountWstETHUnlock(self, vetoer, self.assets[vetoer].wstETHShares); - } + function accountStETHSharesWithdraw(State storage self, address holder) internal returns (ETHValue ethWithdrawn) { + HolderAssets storage assets = self.assets[holder]; + SharesValue stETHSharesToWithdraw = assets.stETHLockedShares; - function accountWstETHUnlock( - State storage self, - address vetoer, - uint256 shares - ) internal returns (uint128 sharesUnlocked) { - _checkNonZeroSharesUnlock(vetoer, shares); - sharesUnlocked = shares.toUint128(); - self.totals.shares -= sharesUnlocked; - self.assets[vetoer].wstETHShares -= sharesUnlocked; - emit WstETHUnlocked(vetoer, sharesUnlocked); + _checkNonZeroShares(stETHSharesToWithdraw); + + assets.stETHLockedShares = SharesValues.ZERO; + ethWithdrawn = + SharesValues.calcETHValue(self.stETHTotals.claimedETH, stETHSharesToWithdraw, self.stETHTotals.lockedShares); + + emit ETHWithdrawn(holder, stETHSharesToWithdraw, ethWithdrawn); } - function accountWstETHWithdraw(State storage self, address vetoer) internal returns (uint256 ethAmount) { - uint256 wstETHShares = self.assets[vetoer].wstETHShares; - _checkNonZeroSharesWithdraw(vetoer, wstETHShares); - self.assets[vetoer].wstETHShares = 0; - ethAmount = self.totals.amountClaimed * wstETHShares / self.totals.shares; - emit WstETHWithdrawn(vetoer, wstETHShares, ethAmount); + function accountClaimedStETH(State storage self, ETHValue amount) internal { + self.stETHTotals.claimedETH = self.stETHTotals.claimedETH + amount; + emit ETHClaimed(amount); } // --- - // unstETH Operations Accounting + // unstETH operations accounting // --- function accountUnstETHLock( State storage self, - address vetoer, + address holder, uint256[] memory unstETHIds, WithdrawalRequestStatus[] memory statuses ) internal { assert(unstETHIds.length == statuses.length); - uint256 totalUnstETHSharesLocked; + SharesValue totalUnstETHLocked; uint256 unstETHcount = unstETHIds.length; for (uint256 i = 0; i < unstETHcount; ++i) { - totalUnstETHSharesLocked += _addWithdrawalRequest(self, vetoer, unstETHIds[i], statuses[i]); + totalUnstETHLocked = totalUnstETHLocked + _addUnstETHRecord(self, holder, unstETHIds[i], statuses[i]); } - uint128 totalUnstETHSharesLockedUint128 = totalUnstETHSharesLocked.toUint128(); - self.assets[vetoer].unstETHShares += totalUnstETHSharesLockedUint128; - self.assets[vetoer].lastAssetsLockTimestamp = TimeUtils.timestamp(); - self.totals.shares += totalUnstETHSharesLockedUint128; - emit UnstETHLocked(vetoer, unstETHIds, totalUnstETHSharesLocked); - } + self.assets[holder].lastAssetsLockTimestamp = Timestamps.now(); + self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares + totalUnstETHLocked; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares + totalUnstETHLocked; - function accountUnstETHUnlock( - State storage self, - uint256 assetsUnlockDelay, - address vetoer, - uint256[] memory unstETHIds - ) internal { - _checkAssetsUnlockDelayPassed(self, assetsUnlockDelay, vetoer); + emit UnstETHLocked(holder, unstETHIds, totalUnstETHLocked); + } - uint256 totalUnstETHSharesUnlocked; - uint256 totalFinalizedSharesUnlocked; - uint256 totalFinalizedAmountUnlocked; + function accountUnstETHUnlock(State storage self, address holder, uint256[] memory unstETHIds) internal { + SharesValue totalSharesUnlocked; + SharesValue totalFinalizedSharesUnlocked; + ETHValue totalFinalizedAmountUnlocked; uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { - (uint256 sharesUnlocked, uint256 finalizedSharesUnlocked, uint256 finalizedAmountUnlocked) = - _removeWithdrawalRequest(self, vetoer, unstETHIds[i]); - - totalUnstETHSharesUnlocked += sharesUnlocked; - totalFinalizedSharesUnlocked += finalizedSharesUnlocked; - totalFinalizedAmountUnlocked += finalizedAmountUnlocked; + (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) = + _removeUnstETHRecord(self, holder, unstETHIds[i]); + if (finalizedAmountUnlocked > ETHValues.ZERO) { + totalFinalizedAmountUnlocked = totalFinalizedAmountUnlocked + finalizedAmountUnlocked; + totalFinalizedSharesUnlocked = totalFinalizedSharesUnlocked + sharesUnlocked; + } + totalSharesUnlocked = totalSharesUnlocked + sharesUnlocked; } + self.assets[holder].unstETHLockedShares = self.assets[holder].unstETHLockedShares - totalSharesUnlocked; + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH - totalFinalizedAmountUnlocked; + self.unstETHTotals.unfinalizedShares = + self.unstETHTotals.unfinalizedShares - (totalSharesUnlocked - totalFinalizedSharesUnlocked); - uint128 totalUnstETHSharesUnlockedUint128 = totalUnstETHSharesUnlocked.toUint128(); - uint128 totalFinalizedSharesUnlockedUint128 = totalFinalizedSharesUnlocked.toUint128(); - uint128 totalFinalizedAmountUnlockedUint128 = totalFinalizedAmountUnlocked.toUint128(); - - self.assets[vetoer].unstETHShares -= totalUnstETHSharesUnlockedUint128; - self.assets[vetoer].sharesFinalized -= totalFinalizedSharesUnlockedUint128; - self.assets[vetoer].amountFinalized -= totalFinalizedAmountUnlockedUint128; - - self.totals.shares -= totalUnstETHSharesUnlockedUint128; - self.totals.sharesFinalized -= totalFinalizedSharesUnlockedUint128; - self.totals.amountFinalized -= totalFinalizedAmountUnlockedUint128; - - emit UnstETHUnlocked( - vetoer, unstETHIds, totalUnstETHSharesUnlocked, totalFinalizedSharesUnlocked, totalFinalizedAmountUnlocked - ); + emit UnstETHUnlocked(holder, unstETHIds, totalSharesUnlocked, totalFinalizedAmountUnlocked); } function accountUnstETHFinalized( @@ -243,26 +194,19 @@ library AssetsAccounting { ) internal { assert(claimableAmounts.length == unstETHIds.length); - uint256 totalSharesFinalized; - uint256 totalAmountFinalized; + ETHValue totalAmountFinalized; + SharesValue totalSharesFinalized; uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { - (address owner, uint256 sharesFinalized, uint256 amountFinalized) = - _finalizeWithdrawalRequest(self, unstETHIds[i], claimableAmounts[i]); - - self.assets[owner].sharesFinalized += sharesFinalized.toUint128(); - self.assets[owner].amountFinalized += amountFinalized.toUint128(); - - totalSharesFinalized += sharesFinalized; - totalAmountFinalized += amountFinalized; + (SharesValue sharesFinalized, ETHValue amountFinalized) = + _finalizeUnstETHRecord(self, unstETHIds[i], claimableAmounts[i]); + totalSharesFinalized = totalSharesFinalized + sharesFinalized; + totalAmountFinalized = totalAmountFinalized + amountFinalized; } - uint128 totalSharesFinalizedUint128 = totalSharesFinalized.toUint128(); - uint128 totalAmountFinalizedUint128 = totalAmountFinalized.toUint128(); - - self.totals.sharesFinalized += totalSharesFinalizedUint128; - self.totals.amountFinalized += totalAmountFinalizedUint128; + self.unstETHTotals.finalizedETH = self.unstETHTotals.finalizedETH + totalAmountFinalized; + self.unstETHTotals.unfinalizedShares = self.unstETHTotals.unfinalizedShares - totalSharesFinalized; emit UnstETHFinalized(unstETHIds, totalSharesFinalized, totalAmountFinalized); } @@ -270,292 +214,174 @@ library AssetsAccounting { State storage self, uint256[] memory unstETHIds, uint256[] memory claimableAmounts - ) internal returns (uint256 totalAmountClaimed) { + ) internal returns (ETHValue totalAmountClaimed) { uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { - totalAmountClaimed += _claimWithdrawalRequest(self, unstETHIds[i], claimableAmounts[i]); + ETHValue claimableAmount = ETHValues.from(claimableAmounts[i]); + totalAmountClaimed = totalAmountClaimed + claimableAmount; + _claimUnstETHRecord(self, unstETHIds[i], claimableAmount); } - self.totals.amountClaimed += totalAmountClaimed.toUint128(); emit UnstETHClaimed(unstETHIds, totalAmountClaimed); } function accountUnstETHWithdraw( State storage self, - address vetoer, + address holder, uint256[] calldata unstETHIds - ) internal returns (uint256 amountWithdrawn) { + ) internal returns (ETHValue amountWithdrawn) { uint256 unstETHIdsCount = unstETHIds.length; for (uint256 i = 0; i < unstETHIdsCount; ++i) { - amountWithdrawn += _withdrawWithdrawalRequest(self, vetoer, unstETHIds[i]); + amountWithdrawn = amountWithdrawn + _withdrawUnstETHRecord(self, holder, unstETHIds[i]); } emit UnstETHWithdrawn(unstETHIds, amountWithdrawn); } - // --- - // Withdraw Batches - // --- - - function formWithdrawalBatch( - State storage self, - uint256 minRequestAmount, - uint256 maxRequestAmount, - uint256 stETHBalance, - uint256 requestAmountsCountLimit - ) internal returns (uint256[] memory requestAmounts) { - if (self.isAllWithdrawalBatchesFormed) { - revert WithdrawalBatchesFormed(); - } - if (requestAmountsCountLimit == 0) { - revert EmptyWithdrawalBatch(); - } - - uint256 maxAmount = maxRequestAmount * requestAmountsCountLimit; - if (stETHBalance >= maxAmount) { - return ArrayUtils.seed(requestAmountsCountLimit, maxRequestAmount); - } - - self.isAllWithdrawalBatchesFormed = true; - - uint256 requestsCount = stETHBalance / maxRequestAmount; - uint256 lastRequestAmount = stETHBalance % maxRequestAmount; - - if (lastRequestAmount < minRequestAmount) { - return ArrayUtils.seed(requestsCount, maxRequestAmount); - } - - requestAmounts = ArrayUtils.seed(requestsCount + 1, maxRequestAmount); - requestAmounts[requestsCount] = lastRequestAmount; - } - - function accountWithdrawalBatch(State storage self, uint256[] memory unstETHIds) internal { - uint256 unstETHIdsCount = unstETHIds.length; - for (uint256 i = 0; i < unstETHIdsCount; ++i) { - self.withdrawalBatchIds.push(unstETHIds[i]); - } - emit WithdrawalBatchCreated(unstETHIds); - } - - function accountWithdrawalBatchClaimed( - State storage self, - uint256 offset, - uint256 count - ) internal returns (uint256[] memory unstETHIds) { - if (count == 0) { - return unstETHIds; - } - uint256 batchesCount = self.withdrawalBatchIds.length; - uint256 claimedBatchesCount = self.claimedBatchesCount; - if (claimedBatchesCount == batchesCount) { - revert NoBatchesToClaim(); - } - if (claimedBatchesCount != offset) { - revert InvalidWithdrawalBatchesOffset(offset, claimedBatchesCount); - } - if (count > batchesCount - claimedBatchesCount) { - revert InvalidWithdrawalBatchesCount(count, batchesCount - claimedBatchesCount); - } - - unstETHIds = new uint256[](count); - for (uint256 i = 0; i < count; ++i) { - unstETHIds[i] = self.withdrawalBatchIds[claimedBatchesCount + i]; - } - self.claimedBatchesCount += count; - emit WithdrawalBatchesClaimed(offset, count); - } - - function accountClaimedETH(State storage self, uint256 amount) internal { - self.totals.amountClaimed += amount.toUint128(); - } - // --- // Getters // --- - function getLocked(State storage self) internal view returns (uint256 rebaseableShares, uint256 finalizedAmount) { - rebaseableShares = self.totals.shares - self.totals.sharesFinalized; - finalizedAmount = self.totals.amountFinalized; - } - - function getIsWithdrawalsClaimed(State storage self) internal view returns (bool) { - return self.claimedBatchesCount == self.withdrawalBatchIds.length; + function getLockedAssetsTotals(State storage self) + internal + view + returns (SharesValue ufinalizedShares, ETHValue finalizedETH) + { + finalizedETH = self.unstETHTotals.finalizedETH; + ufinalizedShares = self.stETHTotals.lockedShares + self.unstETHTotals.unfinalizedShares; } - function _checkWithdrawalRequestStatusOwner(WithdrawalRequestStatus memory status, address account) private pure { - if (status.owner != account) { - revert InvalidUnstETHOwner(account, status.owner); + function checkAssetsUnlockDelayPassed( + State storage self, + address holder, + Duration assetsUnlockDelay + ) internal view { + Timestamp assetsUnlockAllowedAfter = assetsUnlockDelay.addTo(self.assets[holder].lastAssetsLockTimestamp); + if (Timestamps.now() <= assetsUnlockAllowedAfter) { + revert AssetsUnlockDelayNotPassed(assetsUnlockAllowedAfter); } } // --- - // Private Methods + // Helper methods // --- - function _addWithdrawalRequest( + function _addUnstETHRecord( State storage self, - address vetoer, + address holder, uint256 unstETHId, WithdrawalRequestStatus memory status - ) private returns (uint256 amountOfShares) { - amountOfShares = status.amountOfShares; - WithdrawalRequest storage request = self.requests[unstETHId]; + ) private returns (SharesValue shares) { + if (status.isFinalized) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.Finalized); + } + // must never be true, for unfinalized requests + assert(!status.isClaimed); - _checkWithdrawalRequestNotLocked(request, unstETHId); - _checkWithdrawalRequestStatusNotFinalized(status, unstETHId); + if (self.unstETHRecords[unstETHId].status != UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, self.unstETHRecords[unstETHId].status); + } - self.vetoersUnstETHIds[vetoer].push(unstETHId); + HolderAssets storage assets = self.assets[holder]; + assets.unstETHIds.push(unstETHId); - request.owner = vetoer; - request.state = WithdrawalRequestState.Locked; - request.vetoerUnstETHIndexOneBased = self.vetoersUnstETHIds[vetoer].length.toUint64(); - request.shares = amountOfShares.toUint128(); - assert(request.claimableAmount == 0); + shares = SharesValues.from(status.amountOfShares); + self.unstETHRecords[unstETHId] = UnstETHRecord({ + lockedBy: holder, + status: UnstETHRecordStatus.Locked, + index: IndicesOneBased.from(assets.unstETHIds.length), + shares: shares, + claimableAmount: ETHValues.ZERO + }); } - function _removeWithdrawalRequest( + function _removeUnstETHRecord( State storage self, - address vetoer, + address holder, uint256 unstETHId - ) private returns (uint256 sharesUnlocked, uint256 finalizedSharesUnlocked, uint256 finalizedAmountUnlocked) { - WithdrawalRequest storage request = self.requests[unstETHId]; + ) private returns (SharesValue sharesUnlocked, ETHValue finalizedAmountUnlocked) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); + } - _checkWithdrawalRequestOwner(request, vetoer); - _checkWithdrawalRequestWasLocked(request, unstETHId); + if (unstETHRecord.status == UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.NotLocked); + } - sharesUnlocked = request.shares; - if (request.state == WithdrawalRequestState.Finalized) { - finalizedSharesUnlocked = sharesUnlocked; - finalizedAmountUnlocked = request.claimableAmount; + sharesUnlocked = unstETHRecord.shares; + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + finalizedAmountUnlocked = unstETHRecord.claimableAmount; } - uint256[] storage vetoerUnstETHIds = self.vetoersUnstETHIds[vetoer]; - uint256 unstETHIdIndex = request.vetoerUnstETHIndexOneBased - 1; - uint256 lastUnstETHIdIndex = vetoerUnstETHIds.length - 1; + HolderAssets storage assets = self.assets[holder]; + IndexOneBased unstETHIdIndex = unstETHRecord.index; + IndexOneBased lastUnstETHIdIndex = IndicesOneBased.from(assets.unstETHIds.length); + if (lastUnstETHIdIndex != unstETHIdIndex) { - uint256 lastUnstETHId = vetoerUnstETHIds[lastUnstETHIdIndex]; - vetoerUnstETHIds[unstETHIdIndex] = lastUnstETHId; - self.requests[lastUnstETHId].vetoerUnstETHIndexOneBased = (unstETHIdIndex + 1).toUint64(); + uint256 lastUnstETHId = assets.unstETHIds[lastUnstETHIdIndex.value()]; + assets.unstETHIds[unstETHIdIndex.value()] = lastUnstETHId; + self.unstETHRecords[lastUnstETHId].index = unstETHIdIndex; } - vetoerUnstETHIds.pop(); - delete self.requests[unstETHId]; + assets.unstETHIds.pop(); + delete self.unstETHRecords[unstETHId]; } - function _finalizeWithdrawalRequest( + function _finalizeUnstETHRecord( State storage self, uint256 unstETHId, uint256 claimableAmount - ) private returns (address owner, uint256 sharesFinalized, uint256 amountFinalized) { - WithdrawalRequest storage request = self.requests[unstETHId]; - if (claimableAmount == 0 || request.state != WithdrawalRequestState.Locked) { - return (request.owner, 0, 0); + ) private returns (SharesValue sharesFinalized, ETHValue amountFinalized) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (claimableAmount == 0 || unstETHRecord.status != UnstETHRecordStatus.Locked) { + return (sharesFinalized, amountFinalized); } - owner = request.owner; - request.state = WithdrawalRequestState.Finalized; - request.claimableAmount = claimableAmount.toUint96(); + sharesFinalized = unstETHRecord.shares; + amountFinalized = ETHValues.from(claimableAmount); - sharesFinalized = request.shares; - amountFinalized = claimableAmount; - } + unstETHRecord.status = UnstETHRecordStatus.Finalized; + unstETHRecord.claimableAmount = amountFinalized; - function _claimWithdrawalRequest( - State storage self, - uint256 unstETHId, - uint256 claimableAmount - ) private returns (uint256 amountClaimed) { - WithdrawalRequest storage request = self.requests[unstETHId]; + self.unstETHRecords[unstETHId] = unstETHRecord; + } - if (request.state != WithdrawalRequestState.Locked && request.state != WithdrawalRequestState.Finalized) { - revert WithdrawalRequestNotClaimable(unstETHId, request.state); + function _claimUnstETHRecord(State storage self, uint256 unstETHId, ETHValue claimableAmount) private { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; + if (unstETHRecord.status != UnstETHRecordStatus.Locked && unstETHRecord.status != UnstETHRecordStatus.Finalized) + { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); } - if (request.state == WithdrawalRequestState.Finalized && request.claimableAmount != claimableAmount) { - revert ClaimableAmountChanged(unstETHId, claimableAmount, request.claimableAmount); + if (unstETHRecord.status == UnstETHRecordStatus.Finalized) { + // if the unstETH was marked finalized earlier, it's claimable amount must stay the same + if (unstETHRecord.claimableAmount != claimableAmount) { + revert InvalidClaimableAmount(unstETHId, claimableAmount, unstETHRecord.claimableAmount); + } } else { - request.claimableAmount = claimableAmount.toUint96(); + unstETHRecord.claimableAmount = claimableAmount; } - request.state = WithdrawalRequestState.Claimed; - amountClaimed = claimableAmount; + unstETHRecord.status = UnstETHRecordStatus.Claimed; + self.unstETHRecords[unstETHId] = unstETHRecord; } - function _withdrawWithdrawalRequest( + function _withdrawUnstETHRecord( State storage self, - address vetoer, + address holder, uint256 unstETHId - ) private returns (uint256 amountWithdrawn) { - WithdrawalRequest storage request = self.requests[unstETHId]; + ) private returns (ETHValue amountWithdrawn) { + UnstETHRecord storage unstETHRecord = self.unstETHRecords[unstETHId]; - if (request.owner != vetoer) { - revert NotWithdrawalRequestOwner(unstETHId, vetoer, request.owner); + if (unstETHRecord.status != UnstETHRecordStatus.Claimed) { + revert InvalidUnstETHStatus(unstETHId, unstETHRecord.status); } - if (request.state != WithdrawalRequestState.Claimed) { - revert InvalidWithdrawlRequestState(unstETHId, request.state, WithdrawalRequestState.Claimed); + if (unstETHRecord.lockedBy != holder) { + revert InvalidUnstETHHolder(unstETHId, holder, unstETHRecord.lockedBy); } - request.state = WithdrawalRequestState.Withdrawn; - amountWithdrawn = request.claimableAmount; + unstETHRecord.status = UnstETHRecordStatus.Withdrawn; + amountWithdrawn = unstETHRecord.claimableAmount; } - function _checkWithdrawalRequestOwner(WithdrawalRequest storage request, address account) private view { - if (request.owner != account) { - revert InvalidUnstETHOwner(account, request.owner); - } - } - - function _checkWithdrawalRequestStatusNotFinalized( - WithdrawalRequestStatus memory status, - uint256 id - ) private pure { - if (status.isFinalized) { - revert WithdrawalRequestFinalized(id); - } - // it can't be claimed without finalization - assert(!status.isClaimed); - } - - function _checkWithdrawalRequestNotLocked(WithdrawalRequest storage request, uint256 unstETHId) private view { - if (request.vetoerUnstETHIndexOneBased != 0) { - revert WithdrawalRequestAlreadyLocked(unstETHId); - } - } - - function _checkWithdrawalRequestWasLocked(WithdrawalRequest storage request, uint256 id) private view { - if (request.vetoerUnstETHIndexOneBased == 0) { - revert WithdrawalRequestWasNotLocked(id); - } - } - - function _checkNonZeroSharesLock(address vetoer, uint256 shares) private pure { - if (shares == 0) { - revert InvalidSharesLock(vetoer, 0); - } - } - - function _checkNonZeroSharesUnlock(address vetoer, uint256 shares) private pure { - if (shares == 0) { - revert InvalidSharesUnlock(vetoer, 0); - } - } - - function _checkStETHSharesUnlock(State storage self, address vetoer, uint256 shares) private view { - if (shares == 0) { - revert InvalidSharesUnlock(vetoer, 0); - } - - if (self.assets[vetoer].stETHShares < shares) { - revert NotEnoughStETHToUnlock(shares, self.assets[vetoer].stETHShares); - } - } - - function _checkNonZeroSharesWithdraw(address vetoer, uint256 shares) private pure { - if (shares == 0) { - revert InvalidSharesWithdraw(vetoer, 0); - } - } - - function _checkAssetsUnlockDelayPassed( - State storage self, - uint256 assetsUnlockDelay, - address vetoer - ) private view { - if (block.timestamp <= self.assets[vetoer].lastAssetsLockTimestamp + assetsUnlockDelay) { - revert AssetsUnlockDelayNotPassed(self.assets[vetoer].lastAssetsLockTimestamp + assetsUnlockDelay); + function _checkNonZeroShares(SharesValue shares) private pure { + if (shares == SharesValues.ZERO) { + revert InvalidSharesValue(SharesValues.ZERO); } } } diff --git a/contracts/libraries/DualGovernanceState.sol b/contracts/libraries/DualGovernanceState.sol index b55499f4..5b030899 100644 --- a/contracts/libraries/DualGovernanceState.sol +++ b/contracts/libraries/DualGovernanceState.sol @@ -7,7 +7,8 @@ import {IEscrow} from "../interfaces/IEscrow.sol"; import {ISealable} from "../interfaces/ISealable.sol"; import {IDualGovernanceConfiguration as IConfiguration, DualGovernanceConfig} from "../interfaces/IConfiguration.sol"; -import {TimeUtils} from "../utils/time.sol"; +import {Duration, Durations} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; enum State { Normal, @@ -21,14 +22,14 @@ library DualGovernanceState { // TODO: Optimize storage layout efficiency struct Store { State state; - uint40 enteredAt; + Timestamp enteredAt; // the time the veto signalling state was entered - uint40 vetoSignallingActivationTime; - IEscrow signallingEscrow; + Timestamp vetoSignallingActivationTime; + IEscrow signallingEscrow; // 248 // the time the Deactivation sub-state was last exited without exiting the parent Veto Signalling state - uint40 vetoSignallingReactivationTime; + Timestamp vetoSignallingReactivationTime; // the last time a proposal was submitted to the DG subsystem - uint40 lastAdoptableStateExitedAt; + Timestamp lastAdoptableStateExitedAt; IEscrow rageQuitEscrow; uint8 rageQuitRound; } @@ -37,6 +38,7 @@ library DualGovernanceState { error AlreadyInitialized(); error ProposalsCreationSuspended(); error ProposalsAdoptionSuspended(); + error ResealIsNotAllowedInNormalState(); event NewSignallingEscrowDeployed(address indexed escrow); event DualGovernanceStateChanged(State oldState, State newState); @@ -90,7 +92,7 @@ library DualGovernanceState { } } - function checkCanScheduleProposal(Store storage self, uint256 proposalSubmittedAt) internal view { + function checkCanScheduleProposal(Store storage self, Timestamp proposalSubmittedAt) internal view { if (!canScheduleProposal(self, proposalSubmittedAt)) { revert ProposalsAdoptionSuspended(); } @@ -102,11 +104,17 @@ library DualGovernanceState { } } + function checkResealState(Store storage self) internal view { + if (self.state == State.Normal) { + revert ResealIsNotAllowedInNormalState(); + } + } + function currentState(Store storage self) internal view returns (State) { return self.state; } - function canScheduleProposal(Store storage self, uint256 proposalSubmissionTime) internal view returns (bool) { + function canScheduleProposal(Store storage self, Timestamp proposalSubmissionTime) internal view returns (bool) { State state = self.state; if (state == State.Normal) return true; if (state == State.VetoCooldown) { @@ -129,7 +137,9 @@ library DualGovernanceState { if (isProposalsAdoptionAllowed(self)) return false; // for the governance is locked for long period of time - if (block.timestamp - self.lastAdoptableStateExitedAt >= config.TIE_BREAK_ACTIVATION_TIMEOUT()) return true; + if (Timestamps.now() >= config.TIE_BREAK_ACTIVATION_TIMEOUT().addTo(self.lastAdoptableStateExitedAt)) { + return true; + } if (self.state != State.RageQuit) return false; @@ -143,17 +153,17 @@ library DualGovernanceState { function getVetoSignallingState( Store storage self, DualGovernanceConfig memory config - ) internal view returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) { + ) internal view returns (bool isActive, Duration duration, Timestamp activatedAt, Timestamp enteredAt) { isActive = self.state == State.VetoSignalling; - duration = isActive ? getVetoSignallingDuration(self, config) : 0; - enteredAt = isActive ? self.enteredAt : 0; - activatedAt = isActive ? self.vetoSignallingActivationTime : 0; + duration = isActive ? getVetoSignallingDuration(self, config) : Duration.wrap(0); + enteredAt = isActive ? self.enteredAt : Timestamps.ZERO; + activatedAt = isActive ? self.vetoSignallingActivationTime : Timestamps.ZERO; } function getVetoSignallingDuration( Store storage self, DualGovernanceConfig memory config - ) internal view returns (uint256) { + ) internal view returns (Duration) { uint256 totalSupport = self.signallingEscrow.getRageQuitSupport(); return _calcDynamicTimelockDuration(config, totalSupport); } @@ -166,10 +176,10 @@ library DualGovernanceState { function getVetoSignallingDeactivationState( Store storage self, DualGovernanceConfig memory config - ) internal view returns (bool isActive, uint256 duration, uint256 enteredAt) { + ) internal view returns (bool isActive, Duration duration, Timestamp enteredAt) { isActive = self.state == State.VetoSignallingDeactivation; duration = config.vetoSignallingDeactivationMaxDuration; - enteredAt = isActive ? self.enteredAt : 0; + enteredAt = isActive ? self.enteredAt : Timestamps.ZERO; } // --- @@ -253,7 +263,7 @@ library DualGovernanceState { State oldState, State newState ) private { - uint40 timestamp = TimeUtils.timestamp(); + Timestamp timestamp = Timestamps.now(); self.enteredAt = timestamp; // track the time when the governance state allowed execution if (oldState == State.Normal || oldState == State.VetoCooldown) { @@ -301,7 +311,7 @@ library DualGovernanceState { Store storage self, DualGovernanceConfig memory config ) private view returns (bool) { - return block.timestamp - self.vetoSignallingActivationTime > config.dynamicTimelockMaxDuration; + return Timestamps.now() > config.dynamicTimelockMaxDuration.addTo(self.vetoSignallingActivationTime); } function _isDynamicTimelockDurationPassed( @@ -309,29 +319,29 @@ library DualGovernanceState { DualGovernanceConfig memory config, uint256 rageQuitSupport ) private view returns (bool) { - uint256 vetoSignallingDurationPassed = block.timestamp - self.vetoSignallingActivationTime; - return vetoSignallingDurationPassed > _calcDynamicTimelockDuration(config, rageQuitSupport); + Duration dynamicTimelock = _calcDynamicTimelockDuration(config, rageQuitSupport); + return Timestamps.now() > dynamicTimelock.addTo(self.vetoSignallingActivationTime); } function _isVetoSignallingReactivationDurationPassed( Store storage self, DualGovernanceConfig memory config ) private view returns (bool) { - return block.timestamp - self.vetoSignallingReactivationTime > config.vetoSignallingMinActiveDuration; + return Timestamps.now() > config.vetoSignallingMinActiveDuration.addTo(self.vetoSignallingReactivationTime); } function _isVetoSignallingDeactivationMaxDurationPassed( Store storage self, DualGovernanceConfig memory config ) private view returns (bool) { - return block.timestamp - self.enteredAt > config.vetoSignallingDeactivationMaxDuration; + return Timestamps.now() > config.vetoSignallingDeactivationMaxDuration.addTo(self.enteredAt); } function _isVetoCooldownDurationPassed( Store storage self, DualGovernanceConfig memory config ) private view returns (bool) { - return block.timestamp - self.enteredAt > config.vetoCooldownDuration; + return Timestamps.now() > config.vetoCooldownDuration.addTo(self.enteredAt); } function _deployNewSignallingEscrow(Store storage self, address escrowMasterCopy) private { @@ -344,29 +354,31 @@ library DualGovernanceState { function _calcRageQuitWithdrawalsTimelock( DualGovernanceConfig memory config, uint256 rageQuitRound - ) private pure returns (uint256) { - if (rageQuitRound < config.rageQuitEthClaimTimelockGrowthStartSeqNumber) { - return config.rageQuitEthClaimMinTimelock; + ) private pure returns (Duration) { + if (rageQuitRound < config.rageQuitEthWithdrawalsTimelockGrowthStartSeqNumber) { + return config.rageQuitEthWithdrawalsMinTimelock; } - return config.rageQuitEthClaimMinTimelock - + ( - config.rageQuitEthClaimTimelockGrowthCoeffs[0] * rageQuitRound * rageQuitRound - + config.rageQuitEthClaimTimelockGrowthCoeffs[1] * rageQuitRound - + config.rageQuitEthClaimTimelockGrowthCoeffs[2] - ) / 10 ** 18; // TODO: rewrite in a prettier way + return config.rageQuitEthWithdrawalsMinTimelock + + Durations.from( + ( + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[0] * rageQuitRound * rageQuitRound + + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[1] * rageQuitRound + + config.rageQuitEthWithdrawalsTimelockGrowthCoeffs[2] + ) / 10 ** 18 + ); // TODO: rewrite in a prettier way } function _calcDynamicTimelockDuration( DualGovernanceConfig memory config, uint256 rageQuitSupport - ) internal pure returns (uint256 duration_) { + ) internal pure returns (Duration duration_) { uint256 firstSealRageQuitSupport = config.firstSealRageQuitSupport; uint256 secondSealRageQuitSupport = config.secondSealRageQuitSupport; - uint256 dynamicTimelockMinDuration = config.dynamicTimelockMinDuration; - uint256 dynamicTimelockMaxDuration = config.dynamicTimelockMaxDuration; + Duration dynamicTimelockMinDuration = config.dynamicTimelockMinDuration; + Duration dynamicTimelockMaxDuration = config.dynamicTimelockMaxDuration; if (rageQuitSupport < firstSealRageQuitSupport) { - return 0; + return Durations.ZERO; } if (rageQuitSupport >= secondSealRageQuitSupport) { @@ -374,7 +386,10 @@ library DualGovernanceState { } duration_ = dynamicTimelockMinDuration - + (rageQuitSupport - firstSealRageQuitSupport) * (dynamicTimelockMaxDuration - dynamicTimelockMinDuration) - / (secondSealRageQuitSupport - firstSealRageQuitSupport); + + Durations.from( + (rageQuitSupport - firstSealRageQuitSupport) + * (dynamicTimelockMaxDuration - dynamicTimelockMinDuration).toSeconds() + / (secondSealRageQuitSupport - firstSealRageQuitSupport) + ); } } diff --git a/contracts/libraries/EmergencyProtection.sol b/contracts/libraries/EmergencyProtection.sol index bbbee390..f52bb4f3 100644 --- a/contracts/libraries/EmergencyProtection.sol +++ b/contracts/libraries/EmergencyProtection.sol @@ -1,37 +1,38 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {Duration, Durations} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; struct EmergencyState { address executionCommittee; address activationCommittee; - uint256 protectedTill; + Timestamp protectedTill; bool isEmergencyModeActivated; - uint256 emergencyModeDuration; - uint256 emergencyModeEndsAfter; + Duration emergencyModeDuration; + Timestamp emergencyModeEndsAfter; } library EmergencyProtection { error NotEmergencyActivator(address account); error NotEmergencyEnactor(address account); - error EmergencyCommitteeExpired(uint256 timestamp, uint256 protectedTill); + error EmergencyCommitteeExpired(Timestamp timestamp, Timestamp protectedTill); error InvalidEmergencyModeActiveValue(bool actual, bool expected); - event EmergencyModeActivated(uint256 timestamp); - event EmergencyModeDeactivated(uint256 timestamp); + event EmergencyModeActivated(Timestamp timestamp); + event EmergencyModeDeactivated(Timestamp timestamp); event EmergencyActivationCommitteeSet(address indexed activationCommittee); event EmergencyExecutionCommitteeSet(address indexed executionCommittee); - event EmergencyModeDurationSet(uint256 emergencyModeDuration); - event EmergencyCommitteeProtectedTillSet(uint256 protectedTill); + event EmergencyModeDurationSet(Duration emergencyModeDuration); + event EmergencyCommitteeProtectedTillSet(Timestamp newProtectedTill); struct State { // has rights to activate emergency mode address activationCommittee; - uint40 protectedTill; + Timestamp protectedTill; // till this time, the committee may activate the emergency mode - uint40 emergencyModeEndsAfter; - uint32 emergencyModeDuration; + Timestamp emergencyModeEndsAfter; + Duration emergencyModeDuration; // has rights to execute proposals in emergency mode address executionCommittee; } @@ -40,8 +41,8 @@ library EmergencyProtection { State storage self, address activationCommittee, address executionCommittee, - uint256 protectionDuration, - uint256 emergencyModeDuration + Duration protectionDuration, + Duration emergencyModeDuration ) internal { address prevActivationCommittee = self.activationCommittee; if (activationCommittee != prevActivationCommittee) { @@ -55,36 +56,37 @@ library EmergencyProtection { emit EmergencyExecutionCommitteeSet(executionCommittee); } - uint256 prevProtectedTill = self.protectedTill; - uint256 protectedTill = block.timestamp + protectionDuration; + Timestamp prevProtectedTill = self.protectedTill; + Timestamp newProtectedTill = protectionDuration.addTo(Timestamps.now()); - if (protectedTill != prevProtectedTill) { - self.protectedTill = SafeCast.toUint40(protectedTill); - emit EmergencyCommitteeProtectedTillSet(protectedTill); + if (newProtectedTill != prevProtectedTill) { + self.protectedTill = newProtectedTill; + emit EmergencyCommitteeProtectedTillSet(newProtectedTill); } - uint256 prevEmergencyModeDuration = self.emergencyModeDuration; + Duration prevEmergencyModeDuration = self.emergencyModeDuration; if (emergencyModeDuration != prevEmergencyModeDuration) { - self.emergencyModeDuration = SafeCast.toUint32(emergencyModeDuration); + self.emergencyModeDuration = emergencyModeDuration; emit EmergencyModeDurationSet(emergencyModeDuration); } } function activate(State storage self) internal { - if (block.timestamp > self.protectedTill) { - revert EmergencyCommitteeExpired(block.timestamp, self.protectedTill); + Timestamp timestamp = Timestamps.now(); + if (timestamp > self.protectedTill) { + revert EmergencyCommitteeExpired(timestamp, self.protectedTill); } - self.emergencyModeEndsAfter = SafeCast.toUint40(block.timestamp + self.emergencyModeDuration); - emit EmergencyModeActivated(block.timestamp); + self.emergencyModeEndsAfter = self.emergencyModeDuration.addTo(timestamp); + emit EmergencyModeActivated(timestamp); } function deactivate(State storage self) internal { self.activationCommittee = address(0); self.executionCommittee = address(0); - self.protectedTill = 0; - self.emergencyModeDuration = 0; - self.emergencyModeEndsAfter = 0; - emit EmergencyModeDeactivated(block.timestamp); + self.protectedTill = Timestamps.ZERO; + self.emergencyModeEndsAfter = Timestamps.ZERO; + self.emergencyModeDuration = Durations.ZERO; + emit EmergencyModeDeactivated(Timestamps.now()); } function getEmergencyState(State storage self) internal view returns (EmergencyState memory res) { @@ -97,16 +99,16 @@ library EmergencyProtection { } function isEmergencyModeActivated(State storage self) internal view returns (bool) { - return self.emergencyModeEndsAfter != 0; + return self.emergencyModeEndsAfter.isNotZero(); } function isEmergencyModePassed(State storage self) internal view returns (bool) { - uint256 endsAfter = self.emergencyModeEndsAfter; - return endsAfter != 0 && block.timestamp > endsAfter; + Timestamp endsAfter = self.emergencyModeEndsAfter; + return endsAfter.isNotZero() && Timestamps.now() > endsAfter; } function isEmergencyProtectionEnabled(State storage self) internal view returns (bool) { - return block.timestamp <= self.protectedTill || self.emergencyModeEndsAfter != 0; + return Timestamps.now() <= self.protectedTill || self.emergencyModeEndsAfter.isNotZero(); } function checkActivationCommittee(State storage self, address account) internal view { diff --git a/contracts/libraries/EnumerableProposals.sol b/contracts/libraries/EnumerableProposals.sol new file mode 100644 index 00000000..d5954f9e --- /dev/null +++ b/contracts/libraries/EnumerableProposals.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +struct Proposal { + uint40 submittedAt; + uint256 proposalType; + bytes data; +} + +library EnumerableProposals { + using EnumerableSet for EnumerableSet.Bytes32Set; + + error ProposalDoesNotExist(bytes32 key); + error OffsetOutOfBounds(); + + struct Bytes32ToProposalMap { + bytes32[] _orderedKeys; + EnumerableSet.Bytes32Set _keys; + mapping(bytes32 key => Proposal) _proposals; + } + + function push( + Bytes32ToProposalMap storage map, + bytes32 key, + uint256 proposalType, + bytes memory data + ) internal returns (bool) { + if (!contains(map, key)) { + Proposal memory proposal = Proposal(uint40(block.timestamp), proposalType, data); + map._proposals[key] = proposal; + map._orderedKeys.push(key); + map._keys.add(key); + return true; + } + return false; + } + + function contains(Bytes32ToProposalMap storage map, bytes32 key) internal view returns (bool) { + return map._keys.contains(key); + } + + function length(Bytes32ToProposalMap storage map) internal view returns (uint256) { + return map._orderedKeys.length; + } + + function at(Bytes32ToProposalMap storage map, uint256 index) internal view returns (Proposal memory) { + bytes32 key = map._orderedKeys[index]; + return map._proposals[key]; + } + + function get(Bytes32ToProposalMap storage map, bytes32 key) internal view returns (Proposal memory value) { + if (!contains(map, key)) { + revert ProposalDoesNotExist(key); + } + value = map._proposals[key]; + } + + function orederedKeys(Bytes32ToProposalMap storage map) internal view returns (bytes32[] memory) { + return map._orderedKeys; + } + + function orederedKeys( + Bytes32ToProposalMap storage map, + uint256 offset, + uint256 limit + ) internal view returns (bytes32[] memory keys) { + if (offset >= map._orderedKeys.length) { + revert OffsetOutOfBounds(); + } + + uint256 keysLength = limit; + if (keysLength > map._orderedKeys.length - offset) { + keysLength = map._orderedKeys.length - offset; + } + + keys = new bytes32[](keysLength); + for (uint256 i = 0; i < keysLength; ++i) { + keys[i] = map._orderedKeys[offset + i]; + } + } +} diff --git a/contracts/libraries/Proposals.sol b/contracts/libraries/Proposals.sol index e404a93f..6d1ae5d1 100644 --- a/contracts/libraries/Proposals.sol +++ b/contracts/libraries/Proposals.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {IExecutor, ExecutorCall} from "../interfaces/IExecutor.sol"; +import {Duration} from "../types/Duration.sol"; +import {Timestamp, Timestamps} from "../types/Timestamp.sol"; -import {TimeUtils} from "../utils/time.sol"; +import {IExecutor, ExecutorCall} from "../interfaces/IExecutor.sol"; enum Status { NotExist, @@ -17,18 +18,18 @@ struct Proposal { uint256 id; Status status; address executor; - uint256 submittedAt; - uint256 scheduledAt; - uint256 executedAt; + Timestamp submittedAt; + Timestamp scheduledAt; + Timestamp executedAt; ExecutorCall[] calls; } library Proposals { struct ProposalPacked { address executor; - uint40 submittedAt; - uint40 scheduledAt; - uint40 executedAt; + Timestamp submittedAt; + Timestamp scheduledAt; + Timestamp executedAt; ExecutorCall[] calls; } @@ -69,7 +70,7 @@ library Proposals { ProposalPacked storage newProposal = self.proposals[newProposalIndex]; newProposal.executor = executor; - newProposal.submittedAt = TimeUtils.timestamp(); + newProposal.submittedAt = Timestamps.now(); // copying of arrays of custom types from calldata to storage has not been supported by the // Solidity compiler yet, so insert item by item @@ -81,22 +82,17 @@ library Proposals { emit ProposalSubmitted(newProposalId, executor, calls); } - function schedule( - State storage self, - uint256 proposalId, - uint256 afterSubmitDelay - ) internal returns (uint256 submittedAt) { + function schedule(State storage self, uint256 proposalId, Duration afterSubmitDelay) internal { _checkProposalSubmitted(self, proposalId); _checkAfterSubmitDelayPassed(self, proposalId, afterSubmitDelay); - ProposalPacked storage proposal = _packed(self, proposalId); - submittedAt = proposal.submittedAt; - proposal.scheduledAt = TimeUtils.timestamp(); + ProposalPacked storage proposal = _packed(self, proposalId); + proposal.scheduledAt = Timestamps.now(); emit ProposalScheduled(proposalId); } - function execute(State storage self, uint256 proposalId, uint256 afterScheduleDelay) internal { + function execute(State storage self, uint256 proposalId, Duration afterScheduleDelay) internal { _checkProposalScheduled(self, proposalId); _checkAfterScheduleDelayPassed(self, proposalId, afterScheduleDelay); _executeProposal(self, proposalId); @@ -121,6 +117,14 @@ library Proposals { proposal.calls = packed.calls; } + function getProposalSubmissionTime( + State storage self, + uint256 proposalId + ) internal view returns (Timestamp submittedAt) { + _checkProposalExists(self, proposalId); + submittedAt = _packed(self, proposalId).submittedAt; + } + function count(State storage self) internal view returns (uint256 count_) { count_ = self.proposals.length; } @@ -128,24 +132,24 @@ library Proposals { function canExecute( State storage self, uint256 proposalId, - uint256 afterScheduleDelay + Duration afterScheduleDelay ) internal view returns (bool) { return _getProposalStatus(self, proposalId) == Status.Scheduled - && block.timestamp >= _packed(self, proposalId).scheduledAt + afterScheduleDelay; + && Timestamps.now() >= afterScheduleDelay.addTo(_packed(self, proposalId).scheduledAt); } function canSchedule( State storage self, uint256 proposalId, - uint256 afterSubmitDelay + Duration afterSubmitDelay ) internal view returns (bool) { return _getProposalStatus(self, proposalId) == Status.Submitted - && block.timestamp >= _packed(self, proposalId).submittedAt + afterSubmitDelay; + && Timestamps.now() >= afterSubmitDelay.addTo(_packed(self, proposalId).submittedAt); } function _executeProposal(State storage self, uint256 proposalId) private { ProposalPacked storage packed = _packed(self, proposalId); - packed.executedAt = TimeUtils.timestamp(); + packed.executedAt = Timestamps.now(); ExecutorCall[] memory calls = packed.calls; uint256 callsCount = calls.length; @@ -187,9 +191,9 @@ library Proposals { function _checkAfterSubmitDelayPassed( State storage self, uint256 proposalId, - uint256 afterSubmitDelay + Duration afterSubmitDelay ) private view { - if (block.timestamp < _packed(self, proposalId).submittedAt + afterSubmitDelay) { + if (Timestamps.now() < afterSubmitDelay.addTo(_packed(self, proposalId).submittedAt)) { revert AfterSubmitDelayNotPassed(proposalId); } } @@ -197,9 +201,9 @@ library Proposals { function _checkAfterScheduleDelayPassed( State storage self, uint256 proposalId, - uint256 afterScheduleDelay + Duration afterScheduleDelay ) private view { - if (block.timestamp < _packed(self, proposalId).scheduledAt + afterScheduleDelay) { + if (Timestamps.now() < afterScheduleDelay.addTo(_packed(self, proposalId).scheduledAt)) { revert AfterScheduleDelayNotPassed(proposalId); } } @@ -209,10 +213,10 @@ library Proposals { ProposalPacked storage packed = _packed(self, proposalId); - if (packed.executedAt != 0) return Status.Executed; + if (packed.executedAt.isNotZero()) return Status.Executed; if (proposalId <= self.lastCancelledProposalId) return Status.Cancelled; - if (packed.scheduledAt != 0) return Status.Scheduled; - if (packed.submittedAt != 0) return Status.Submitted; + if (packed.scheduledAt.isNotZero()) return Status.Scheduled; + if (packed.submittedAt.isNotZero()) return Status.Submitted; assert(false); } } diff --git a/contracts/libraries/TiebreakerProtection.sol b/contracts/libraries/TiebreakerProtection.sol new file mode 100644 index 00000000..fb30649a --- /dev/null +++ b/contracts/libraries/TiebreakerProtection.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +interface IResealManger { + function resume(address sealable) external; +} + +library TiebreakerProtection { + struct Tiebreaker { + address tiebreaker; + IResealManger resealManager; + } + + event TiebreakerSet(address tiebreakCommittee, address resealManager); + event SealableResumed(address sealable); + + error ProposalNotExecutable(uint256 proposalId); + error NotTiebreaker(address account, address tiebreakCommittee); + error TieBreakerAddressIsSame(); + + function resumeSealable(Tiebreaker storage self, address sealable) internal { + self.resealManager.resume(sealable); + emit SealableResumed(sealable); + } + + function setTiebreaker(Tiebreaker storage self, address tiebreaker, address resealManager) internal { + if (self.tiebreaker == tiebreaker) { + revert TieBreakerAddressIsSame(); + } + + self.tiebreaker = tiebreaker; + self.resealManager = IResealManger(resealManager); + emit TiebreakerSet(tiebreaker, resealManager); + } + + function checkTiebreakerCommittee(Tiebreaker storage self, address account) internal view { + if (account != self.tiebreaker) { + revert NotTiebreaker(account, self.tiebreaker); + } + } +} diff --git a/contracts/libraries/WithdrawalBatchesQueue.sol b/contracts/libraries/WithdrawalBatchesQueue.sol new file mode 100644 index 00000000..6c096be8 --- /dev/null +++ b/contracts/libraries/WithdrawalBatchesQueue.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import {ArrayUtils} from "../utils/arrays.sol"; +import {SequentialBatch, SequentialBatches} from "../types/SequentialBatches.sol"; + +enum Status { + // The default status of the WithdrawalsBatchesQueue. In the closed state the only action allowed + // to be called is open(), which transfers it into Opened state. + Empty, + // In the Opened state WithdrawalsBatchesQueue allows to add batches into the queue + Opened, + // When the WithdrawalsBatchesQueue enters Filled queue - it's not allowed to add batches and + // only allowed to mark batches claimed + Closed +} + +struct QueueIndex { + uint32 batchIndex; + uint16 valueIndex; +} + +library WithdrawalsBatchesQueue { + using SafeCast for uint256; + + struct State { + Status status; + QueueIndex lastClaimedUnstETHIdIndex; + uint48 totalUnstETHCount; + uint48 totalUnstETHClaimed; + SequentialBatch[] batches; + } + + event UnstETHIdsAdded(uint256[] unstETHIds); + event UnstETHIdsClaimed(uint256[] unstETHIds); + + error InvalidWithdrawalsBatchesQueueStatus(Status status); + + function calcRequestAmounts( + uint256 minRequestAmount, + uint256 requestAmount, + uint256 amount + ) internal pure returns (uint256[] memory requestAmounts) { + uint256 requestsCount = amount / requestAmount; + // last request amount will be equal to zero when it's multiple requestAmount + // when it's in the range [0, minRequestAmount) - it will not be included in the result + uint256 lastRequestAmount = amount - requestsCount * requestAmount; + if (lastRequestAmount >= minRequestAmount) { + requestsCount += 1; + } + requestAmounts = ArrayUtils.seed(requestsCount, requestAmount); + if (lastRequestAmount >= minRequestAmount) { + requestAmounts[requestsCount - 1] = lastRequestAmount; + } + } + + function open(State storage self) internal { + _checkStatus(self, Status.Empty); + // insert empty batch as a stub for first item + self.batches.push(SequentialBatches.create({seed: 0, count: 1})); + self.status = Status.Opened; + } + + function close(State storage self) internal { + _checkStatus(self, Status.Opened); + self.status = Status.Closed; + } + + function isClosed(State storage self) internal view returns (bool) { + return self.status == Status.Closed; + } + + function isAllUnstETHClaimed(State storage self) internal view returns (bool) { + return self.totalUnstETHClaimed == self.totalUnstETHCount; + } + + function checkOpened(State storage self) internal view { + _checkStatus(self, Status.Opened); + } + + function add(State storage self, uint256[] memory unstETHIds) internal { + uint256 unstETHIdsCount = unstETHIds.length; + if (unstETHIdsCount == 0) { + return; + } + + // before creating the batch, assert that the unstETHIds is sequential + for (uint256 i = 0; i < unstETHIdsCount - 1; ++i) { + assert(unstETHIds[i + 1] == unstETHIds[i] + 1); + } + + uint256 lastBatchIndex = self.batches.length - 1; + SequentialBatch lastWithdrawalsBatch = self.batches[lastBatchIndex]; + SequentialBatch newWithdrawalsBatch = SequentialBatches.create({seed: unstETHIds[0], count: unstETHIdsCount}); + + if (SequentialBatches.canMerge(lastWithdrawalsBatch, newWithdrawalsBatch)) { + self.batches[lastBatchIndex] = SequentialBatches.merge(lastWithdrawalsBatch, newWithdrawalsBatch); + } else { + self.batches.push(newWithdrawalsBatch); + } + + self.totalUnstETHCount += newWithdrawalsBatch.size().toUint48(); + emit UnstETHIdsAdded(unstETHIds); + } + + function claimNextBatch( + State storage self, + uint256 maxUnstETHIdsCount + ) internal returns (uint256[] memory unstETHIds) { + (unstETHIds, self.lastClaimedUnstETHIdIndex) = _getNextClaimableUnstETHIds(self, maxUnstETHIdsCount); + self.totalUnstETHClaimed += unstETHIds.length.toUint48(); + emit UnstETHIdsClaimed(unstETHIds); + } + + function getNextWithdrawalsBatches( + State storage self, + uint256 limit + ) internal view returns (uint256[] memory unstETHIds) { + (unstETHIds,) = _getNextClaimableUnstETHIds(self, limit); + } + + function _getNextClaimableUnstETHIds( + State storage self, + uint256 maxUnstETHIdsCount + ) private view returns (uint256[] memory unstETHIds, QueueIndex memory lastClaimedUnstETHIdIndex) { + uint256 unstETHIdsCount = Math.min(self.totalUnstETHCount - self.totalUnstETHClaimed, maxUnstETHIdsCount); + + unstETHIds = new uint256[](unstETHIdsCount); + lastClaimedUnstETHIdIndex = self.lastClaimedUnstETHIdIndex; + SequentialBatch currentBatch = self.batches[lastClaimedUnstETHIdIndex.batchIndex]; + + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + lastClaimedUnstETHIdIndex.valueIndex += 1; + if (currentBatch.size() == lastClaimedUnstETHIdIndex.valueIndex) { + lastClaimedUnstETHIdIndex.batchIndex += 1; + lastClaimedUnstETHIdIndex.valueIndex = 0; + currentBatch = self.batches[lastClaimedUnstETHIdIndex.batchIndex]; + } + unstETHIds[i] = currentBatch.valueAt(lastClaimedUnstETHIdIndex.valueIndex); + } + } + + function _checkStatus(State storage self, Status expectedStatus) private view { + if (self.status != expectedStatus) { + revert InvalidWithdrawalsBatchesQueueStatus(self.status); + } + } +} diff --git a/contracts/model/DualGovernanceModel.sol b/contracts/model/DualGovernanceModel.sol index fd41a127..273a975f 100644 --- a/contracts/model/DualGovernanceModel.sol +++ b/contracts/model/DualGovernanceModel.sol @@ -87,7 +87,7 @@ contract DualGovernanceModel { "Proposals can only be scheduled in Normal or Veto Cooldown states." ); if (currentState == State.VetoCooldown) { - (,, uint256 submissionTime,,) = emergencyProtectedTimelock.proposals(proposalId); + (,,, uint256 submissionTime,) = emergencyProtectedTimelock.proposals(proposalId); require( submissionTime < lastVetoSignallingTime, "Proposal submitted after the last time Veto Signalling state was entered." diff --git a/contracts/types/Duration.sol b/contracts/types/Duration.sol new file mode 100644 index 00000000..0a599061 --- /dev/null +++ b/contracts/types/Duration.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Timestamp, Timestamps} from "./Timestamp.sol"; + +type Duration is uint32; + +error DurationOverflow(); +error DurationUnderflow(); + +// the max possible duration is ~ 106 years +uint256 constant MAX_VALUE = type(uint32).max; + +using {lt as <} for Duration global; +using {lte as <=} for Duration global; +using {gt as >} for Duration global; +using {eq as ==} for Duration global; +using {notEq as !=} for Duration global; + +using {plus as +} for Duration global; +using {minus as -} for Duration global; + +using {addTo} for Duration global; +using {plusSeconds} for Duration global; +using {minusSeconds} for Duration global; +using {multipliedBy} for Duration global; +using {dividedBy} for Duration global; +using {toSeconds} for Duration global; + +// --- +// Comparison Ops +// --- + +function lt(Duration d1, Duration d2) pure returns (bool) { + return Duration.unwrap(d1) < Duration.unwrap(d2); +} + +function lte(Duration d1, Duration d2) pure returns (bool) { + return Duration.unwrap(d1) <= Duration.unwrap(d2); +} + +function gt(Duration d1, Duration d2) pure returns (bool) { + return Duration.unwrap(d1) > Duration.unwrap(d2); +} + +function eq(Duration d1, Duration d2) pure returns (bool) { + return Duration.unwrap(d1) == Duration.unwrap(d2); +} + +function notEq(Duration d1, Duration d2) pure returns (bool) { + return !(d1 == d2); +} + +// --- +// Arithmetic Operations +// --- + +function plus(Duration d1, Duration d2) pure returns (Duration) { + return toDuration(Duration.unwrap(d1) + Duration.unwrap(d2)); +} + +function minus(Duration d1, Duration d2) pure returns (Duration) { + if (d1 < d2) { + revert DurationUnderflow(); + } + return Duration.wrap(Duration.unwrap(d1) - Duration.unwrap(d2)); +} + +function plusSeconds(Duration d, uint256 seconds_) pure returns (Duration) { + return toDuration(Duration.unwrap(d) + seconds_); +} + +function minusSeconds(Duration d, uint256 seconds_) pure returns (Duration) { + uint256 durationValue = Duration.unwrap(d); + if (durationValue < seconds_) { + revert DurationUnderflow(); + } + return Duration.wrap(uint32(durationValue - seconds_)); +} + +function dividedBy(Duration d, uint256 divisor) pure returns (Duration) { + return Duration.wrap(uint32(Duration.unwrap(d) / divisor)); +} + +function multipliedBy(Duration d, uint256 multiplicand) pure returns (Duration) { + return toDuration(Duration.unwrap(d) * multiplicand); +} + +function addTo(Duration d, Timestamp t) pure returns (Timestamp) { + return Timestamps.from(t.toSeconds() + d.toSeconds()); +} + +// --- +// Conversion Ops +// --- + +function toDuration(uint256 value) pure returns (Duration) { + if (value > MAX_VALUE) { + revert DurationOverflow(); + } + return Duration.wrap(uint32(value)); +} + +function toSeconds(Duration d) pure returns (uint256) { + return Duration.unwrap(d); +} + +library Durations { + Duration internal constant ZERO = Duration.wrap(0); + + Duration internal constant MIN = ZERO; + Duration internal constant MAX = Duration.wrap(uint32(MAX_VALUE)); + + function from(uint256 seconds_) internal pure returns (Duration res) { + res = toDuration(seconds_); + } + + function between(Timestamp t1, Timestamp t2) internal pure returns (Duration res) { + res = toDuration(t1.toSeconds() - t2.toSeconds()); + } +} diff --git a/contracts/types/ETHValue.sol b/contracts/types/ETHValue.sol new file mode 100644 index 00000000..3fb11785 --- /dev/null +++ b/contracts/types/ETHValue.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +type ETHValue is uint128; + +error ETHValueOverflow(); +error ETHValueUnderflow(); + +using {plus as +, minus as -, lt as <, gt as >, eq as ==, neq as !=} for ETHValue global; +using {toUint256} for ETHValue global; +using {sendTo} for ETHValue global; + +function sendTo(ETHValue value, address payable recipient) { + Address.sendValue(recipient, value.toUint256()); +} + +function plus(ETHValue v1, ETHValue v2) pure returns (ETHValue) { + return ETHValues.from(ETHValue.unwrap(v1) + ETHValue.unwrap(v2)); +} + +function minus(ETHValue v1, ETHValue v2) pure returns (ETHValue) { + if (v1 < v2) { + revert ETHValueUnderflow(); + } + return ETHValues.from(ETHValue.unwrap(v1) + ETHValue.unwrap(v2)); +} + +function lt(ETHValue v1, ETHValue v2) pure returns (bool) { + return ETHValue.unwrap(v1) < ETHValue.unwrap(v2); +} + +function gt(ETHValue v1, ETHValue v2) pure returns (bool) { + return ETHValue.unwrap(v1) > ETHValue.unwrap(v2); +} + +function eq(ETHValue v1, ETHValue v2) pure returns (bool) { + return ETHValue.unwrap(v1) == ETHValue.unwrap(v2); +} + +function neq(ETHValue v1, ETHValue v2) pure returns (bool) { + return ETHValue.unwrap(v1) != ETHValue.unwrap(v2); +} + +function toUint256(ETHValue value) pure returns (uint256) { + return ETHValue.unwrap(value); +} + +library ETHValues { + ETHValue internal constant ZERO = ETHValue.wrap(0); + + function from(uint256 value) internal pure returns (ETHValue) { + if (value > type(uint128).max) { + revert ETHValueOverflow(); + } + return ETHValue.wrap(uint128(value)); + } +} diff --git a/contracts/types/IndexOneBased.sol b/contracts/types/IndexOneBased.sol new file mode 100644 index 00000000..201ce080 --- /dev/null +++ b/contracts/types/IndexOneBased.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +type IndexOneBased is uint32; + +error IndexOneBasedOverflow(); +error IndexOneBasedUnderflow(); + +using {neq as !=} for IndexOneBased global; +using {value} for IndexOneBased global; + +function neq(IndexOneBased i1, IndexOneBased i2) pure returns (bool) { + return IndexOneBased.unwrap(i1) != IndexOneBased.unwrap(i2); +} + +function value(IndexOneBased index) pure returns (uint256) { + if (IndexOneBased.unwrap(index) == 0) { + revert IndexOneBasedUnderflow(); + } + unchecked { + return IndexOneBased.unwrap(index) - 1; + } +} + +library IndicesOneBased { + function from(uint256 value) internal pure returns (IndexOneBased) { + if (value > type(uint32).max) { + revert IndexOneBasedOverflow(); + } + return IndexOneBased.wrap(uint32(value)); + } +} diff --git a/contracts/types/SequentialBatches.sol b/contracts/types/SequentialBatches.sol new file mode 100644 index 00000000..6d944497 --- /dev/null +++ b/contracts/types/SequentialBatches.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +uint256 constant BATCH_SIZE_LENGTH = 16; +uint256 constant BATCH_SIZE_MASK = 2 ** BATCH_SIZE_LENGTH - 1; + +uint256 constant MAX_BATCH_SIZE = BATCH_SIZE_MASK; +uint256 constant MAX_BATCH_VALUE = 2 ** (256 - BATCH_SIZE_LENGTH) - 1; + +// Stores the info about the withdrawals batch encoded as single uint256 +// The 230 MST bits stores the id of the UnstETH id +// the 16 LST bits stores the size of the batch (max size is 2 ^ 16 - 1= 65535) +type SequentialBatch is uint256; + +error BatchValueOverflow(); +error InvalidBatchSize(uint256 size); +error IndexOutOfBounds(uint256 index); + +using {size} for SequentialBatch global; +using {last} for SequentialBatch global; +using {first} for SequentialBatch global; +using {valueAt} for SequentialBatch global; +using {capacity} for SequentialBatch global; + +function capacity(SequentialBatch) pure returns (uint256) { + return MAX_BATCH_SIZE; +} + +function size(SequentialBatch batch) pure returns (uint256) { + unchecked { + return SequentialBatch.unwrap(batch) & BATCH_SIZE_MASK; + } +} + +function first(SequentialBatch batch) pure returns (uint256) { + unchecked { + return SequentialBatch.unwrap(batch) >> BATCH_SIZE_LENGTH; + } +} + +function last(SequentialBatch batch) pure returns (uint256) { + unchecked { + return batch.first() + batch.size() - 1; + } +} + +function valueAt(SequentialBatch batch, uint256 index) pure returns (uint256) { + if (index >= batch.size()) { + revert IndexOutOfBounds(index); + } + unchecked { + return batch.first() + index; + } +} + +library SequentialBatches { + function create(uint256 seed, uint256 count) internal pure returns (SequentialBatch) { + if (seed > MAX_BATCH_VALUE) { + revert BatchValueOverflow(); + } + if (count == 0 || count > MAX_BATCH_SIZE) { + revert InvalidBatchSize(count); + } + unchecked { + return SequentialBatch.wrap(seed << BATCH_SIZE_LENGTH | count); + } + } + + function canMerge(SequentialBatch b1, SequentialBatch b2) internal pure returns (bool) { + unchecked { + return b1.last() == b2.first() && b1.capacity() - b1.size() > 0; + } + } + + function merge(SequentialBatch b1, SequentialBatch b2) internal pure returns (SequentialBatch b3) { + return create(b1.first(), b1.size() + b2.size()); + } +} diff --git a/contracts/types/SharesValue.sol b/contracts/types/SharesValue.sol new file mode 100644 index 00000000..f5048477 --- /dev/null +++ b/contracts/types/SharesValue.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {ETHValue, ETHValues} from "./ETHValue.sol"; + +type SharesValue is uint128; + +error SharesValueOverflow(); + +using {plus as +, minus as -, eq as ==, lt as <} for SharesValue global; +using {toUint256} for SharesValue global; + +function plus(SharesValue v1, SharesValue v2) pure returns (SharesValue) { + return SharesValue.wrap(SharesValue.unwrap(v1) + SharesValue.unwrap(v2)); +} + +function minus(SharesValue v1, SharesValue v2) pure returns (SharesValue) { + return SharesValue.wrap(SharesValue.unwrap(v1) - SharesValue.unwrap(v2)); +} + +function lt(SharesValue v1, SharesValue v2) pure returns (bool) { + return SharesValue.unwrap(v1) < SharesValue.unwrap(v2); +} + +function eq(SharesValue v1, SharesValue v2) pure returns (bool) { + return SharesValue.unwrap(v1) == SharesValue.unwrap(v2); +} + +function toUint256(SharesValue v) pure returns (uint256) { + return SharesValue.unwrap(v); +} + +library SharesValues { + SharesValue internal constant ZERO = SharesValue.wrap(0); + + function from(uint256 value) internal pure returns (SharesValue) { + if (value > type(uint128).max) { + revert SharesValueOverflow(); + } + return SharesValue.wrap(uint128(value)); + } + + function calcETHValue( + ETHValue totalPooled, + SharesValue share, + SharesValue total + ) internal pure returns (ETHValue) { + return ETHValues.from(totalPooled.toUint256() * share.toUint256() / total.toUint256()); + } +} diff --git a/contracts/types/Timestamp.sol b/contracts/types/Timestamp.sol new file mode 100644 index 00000000..ab06379f --- /dev/null +++ b/contracts/types/Timestamp.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +type Timestamp is uint40; + +error TimestampOverflow(); +error TimestampUnderflow(); + +uint256 constant MAX_TIMESTAMP_VALUE = type(uint40).max; + +using {lt as <} for Timestamp global; +using {gt as >} for Timestamp global; +using {gte as >=} for Timestamp global; +using {lte as <=} for Timestamp global; +using {eq as ==} for Timestamp global; +using {notEq as !=} for Timestamp global; + +using {isZero} for Timestamp global; +using {isNotZero} for Timestamp global; +using {toSeconds} for Timestamp global; + +// --- +// Comparison Ops +// --- + +function lt(Timestamp t1, Timestamp t2) pure returns (bool) { + return Timestamp.unwrap(t1) < Timestamp.unwrap(t2); +} + +function gt(Timestamp t1, Timestamp t2) pure returns (bool) { + return Timestamp.unwrap(t1) > Timestamp.unwrap(t2); +} + +function gte(Timestamp t1, Timestamp t2) pure returns (bool) { + return Timestamp.unwrap(t1) >= Timestamp.unwrap(t2); +} + +function lte(Timestamp t1, Timestamp t2) pure returns (bool) { + return Timestamp.unwrap(t1) <= Timestamp.unwrap(t2); +} + +function eq(Timestamp t1, Timestamp t2) pure returns (bool) { + return Timestamp.unwrap(t1) == Timestamp.unwrap(t2); +} + +function notEq(Timestamp t1, Timestamp t2) pure returns (bool) { + return !(t1 == t2); +} + +function isZero(Timestamp t) pure returns (bool) { + return Timestamp.unwrap(t) == 0; +} + +function isNotZero(Timestamp t) pure returns (bool) { + return Timestamp.unwrap(t) != 0; +} + +// --- +// Conversion Ops +// --- + +function toSeconds(Timestamp t) pure returns (uint256) { + return Timestamp.unwrap(t); +} + +uint256 constant MAX_VALUE = type(uint40).max; + +library Timestamps { + Timestamp internal constant ZERO = Timestamp.wrap(0); + + Timestamp internal constant MIN = ZERO; + Timestamp internal constant MAX = Timestamp.wrap(uint40(MAX_TIMESTAMP_VALUE)); + + function now() internal view returns (Timestamp res) { + res = Timestamp.wrap(uint40(block.timestamp)); + } + + function from(uint256 value) internal pure returns (Timestamp res) { + if (value > MAX_TIMESTAMP_VALUE) { + revert TimestampOverflow(); + } + return Timestamp.wrap(uint40(value)); + } +} diff --git a/contracts/utils/time.sol b/contracts/utils/time.sol deleted file mode 100644 index 05bc93b3..00000000 --- a/contracts/utils/time.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; - -library TimeUtils { - function timestamp() internal view returns (uint40) { - return timestamp(block.timestamp); - } - - function timestamp(uint256 value) internal pure returns (uint40) { - return SafeCast.toUint40(value); - } - - function duration(uint256 value) internal pure returns (uint32) { - return SafeCast.toUint32(value); - } -} diff --git a/docs/mechanism.md b/docs/mechanism.md index f6e23f4a..af81e266 100644 --- a/docs/mechanism.md +++ b/docs/mechanism.md @@ -270,8 +270,8 @@ The Rage Quit state allows all stakers who elected to leave the protocol via rag Upon entry into the Rage Quit state, three things happen: -1. The veto signalling escrow is irreversibly transformed into the **rage quit escrow**, an immutable smart contract that holds all tokens that are part of the rage quit withdrawal process, i.e. stETH, wstETH, withdrawal NFTs, and the withdrawn ETH, and allows stakers to claim the withdrawn ETH after a certain timelock following the completion of the withdrawal process (with the timelock being determined at the moment of the Rage Quit state entry). -2. All stETH and wstETH held by the rage quit escrow are sent for withdrawal via the regular Lido Withdrawal Queue mechanism, generating a set of batch withdrawal NFTs held by the rage quit escrow. +1. The veto signalling escrow is irreversibly transformed into the **rage quit escrow**, an immutable smart contract that holds all tokens that are part of the rage quit withdrawal process, i.e. stETH, wstETH, withdrawal NFTs, and the withdrawn ETH, and allows stakers to retrieve the withdrawn ETH after a certain timelock following the completion of the withdrawal process (with the timelock being determined at the moment of the Rage Quit state entry). +2. All stETH and wstETH held by the rage quit escrow will be processed for withdrawals through the regular Lido Withdrawal Queue mechanism, generating a set of batch withdrawal NFTs held by the rage quit escrow. 3. A new instance of the veto signalling escrow smart contract is deployed. This way, at any point in time, there is only one veto signalling escrow but there may be multiple rage quit escrows from previous rage quits. In this state, the DAO is allowed to submit proposals to the DG but cannot execute any pending proposals. Stakers are not allowed to lock (w)stETH or withdrawal NFTs into the rage quit escrow so joining the ongoing rage quit is not possible. However, they can lock their tokens that are not part of the ongoing rage quit process to the newly-deployed veto signalling escrow to potentially trigger a new rage quit later. @@ -291,7 +291,7 @@ When the withdrawal is complete and the extension delay elapses, two things happ **Transition to Veto Cooldown**. If, at the moment of the Rage Quit state exit, $R(t) \leq R_1$, the Veto Cooldown state is entered. -The duration of the ETH claim timelock $W(i)$ is a non-linear function that depends on the rage quit sequence number $i$ (see below): +The duration of the ETH withdraw timelock $W(i)$ is a non-linear function that depends on the rage quit sequence number $i$ (see below): ```math W(i) = W_{min} + @@ -301,16 +301,16 @@ W(i) = W_{min} + \end{array} \right. ``` -where $W_{min}$ is `RageQuitEthClaimMinTimelock`, $i_{min}$ is `RageQuitEthClaimTimelockGrowthStartSeqNumber`, and $g_W(x)$ is a quadratic polynomial function with coefficients `RageQuitEthClaimTimelockGrowthCoeffs` (a list of length 3). +where $W_{min}$ is `RageQuitEthWithdrawalsMinTimelock`, $i_{min}$ is `RageQuitEthWithdrawalsTimelockGrowthStartSeqNumber`, and $g_W(x)$ is a quadratic polynomial function with coefficients `RageQuitEthWithdrawalsTimelockGrowthCoeffs` (a list of length 3). The rage quit sequence number is calculated as follows: each time the Normal state is entered, the sequence number is set to 0; each time the Rage Quit state is entered, the number is incremented by 1. ```env # Proposed values, to be modeled and refined RageQuitExtensionDelay = 7 days -RageQuitEthClaimMinTimelock = 60 days -RageQuitEthClaimTimelockGrowthStartSeqNumber = 2 -RageQuitEthClaimTimelockGrowthCoeffs = (0, TODO, TODO) +RageQuitEthWithdrawalsMinTimelock = 60 days +RageQuitEthWithdrawalsTimelockGrowthStartSeqNumber = 2 +RageQuitEthWithdrawalsTimelockGrowthCoeffs = (0, TODO, TODO) ``` @@ -395,6 +395,10 @@ Dual governance should not cover: ## Changelog +### 2024-06-25 +- Instead of using the "wNFT" shortcut for the "Lido: stETH Withdrawal NFT" token, the official symbol "unstETH" is now used. +- For the consistency with the codebase, the `RageQuitEthClaimMinTimelock`, `RageQuitEthClaimTimelockGrowthStartSeqNumber`, `RageQuitEthClaimTimelockGrowthCoeffs` parameters were renamed into `RageQuitEthWithdrawalsMinTimelock`, `RageQuitEthWithdrawalsTimelockGrowthStartSeqNumber`, `RageQuitEthWithdrawalsTimelockGrowthCoeffs`. + ### 2024-04-24 * Removed the logic with the extension of the Veto Signalling duration upon new proposal submission. diff --git a/docs/specification.md b/docs/specification.md index dc43cc78..9a99fe73 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -41,7 +41,13 @@ The system is composed of the following main contracts: * [`DualGovernance.sol`](#Contract-DualGovernancesol) is a singleton that provides an interface for submitting governance proposals and scheduling their execution, as well as managing the list of supported proposers (DAO voting systems). Implements a state machine tracking the current global governance state which, in turn, determines whether proposal submission and execution is currently allowed. * [`EmergencyProtectedTimelock.sol`](#Contract-EmergencyProtectedTimelocksol) is a singleton that stores submitted proposals and provides an interface for their execution. In addition, it implements an optional temporary protection from a zero-day vulnerability in the dual governance contracts following the initial deployment or upgrade of the system. The protection is implemented as a timelock on proposal execution combined with two emergency committees that have the right to cooperate and disable the dual governance. * [`Executor.sol`](#Contract-Executorsol) contract instances make calls resulting from governance proposals' execution. Every protocol permission or role protected by the DG, as well as the permission to manage this role/permission, should be assigned exclusively to one of the instances of this contract (in contrast with being assigned directly to a DAO voting system). +* [`ResealExecutor.sol`](#Contract-ResealExecutorsol) contract instances make calls to extend protocol withdrawals pause in case of contracts were put into an emergency pause by the [GateSeal emergency protection mechanism](https://github.com/lidofinance/gate-seals) and the DAO governance is currently blocked by the DG system. Has pause and resume roles for all protocols withdrawals contracts. * [`Escrow.sol`](#Contract-Escrowsol) is a contract that can hold stETH, wstETH, withdrawal NFTs, and plain ETH. It can exist in two states, each serving a different purpose: either an oracle for users' opposition to DAO proposals or an immutable and ungoverned accumulator for the ETH withdrawn as a result of the [rage quit](#Rage-quit). +* [`TiebreakerCore.sol`](#contract-tiebreakercoresol) allows to approve proposals for execution and release protocol withdrawals in case of DAO execution ability is locked by `DualGovernance`. Consists of set of `TiebreakerSubCommittee` appointed by the DAO. +* [`TiebreakerSubCommittee.sol`](#contract-tiebreakersubcommitteesol) provides ability to participate in `TiebreakerCore` for external actors. +* [`EmergencyActivationCommittee`](#contract-emergencyactivationcommitteesol) contract that can activate the Emergency Mode, while only `EmergencyExecutionCommittee` can perform proposal execution. Requires to get quorum from committee members. +* [`EmergencyExecutionCommittee`](#contract-emergencyexecutioncommitteesol) contract provides ability to execute proposals in case of the Emergency Mode or renounce renounce further execution rights, by getting quorum of committee members. +* [`ResealExecutor`] ## Proposal flow @@ -64,7 +70,7 @@ The general proposal flow is the following: Each submitted proposal requires a minimum timelock before it can be scheduled for execution. -At any time, including while a proposal's timelock is lasting, stakers can signal their opposition to the DAO by locking their (w)stETH or withdrawal NFTs (wNFTs) into the [signalling escrow contract](#Contract-Escrowsol). If the opposition exceeds some minimum threshold, the [global governance state](#Governance-state) gets changed, blocking any DAO execution and thus effectively extending the timelock of all pending (i.e. submitted but not scheduled for execution) proposals. +At any time, including while a proposal's timelock is lasting, stakers can signal their opposition to the DAO by locking their (w)stETH or stETH withdrawal NFTs (unstETH) into the [signalling escrow contract](#Contract-Escrowsol). If the opposition exceeds some minimum threshold, the [global governance state](#Governance-state) gets changed, blocking any DAO execution and thus effectively extending the timelock of all pending (i.e. submitted but not scheduled for execution) proposals. ![image](https://github.com/lidofinance/dual-governance/assets/1699593/98273df0-f3fd-4149-929d-3315a8e81aa8) @@ -86,17 +92,17 @@ The proposal execution flow comes after the dynamic timelock elapses and the pro #### Regular deployment mode -In the regular deployment mode, the emergency protection delay is set to zero and all calls from scheduled proposals are immediately executable by anyone via calling the [`EmergencyProtectedTimelock.execute`](#Function-EmergencyProtectedTimelockexecute) function. +In the regular deployment mode, the **emergency protection delay** is set to zero and all calls from scheduled proposals are immediately executable by anyone via calling the [`EmergencyProtectedTimelock.execute`](#Function-EmergencyProtectedTimelockexecute) function. #### Protected deployment mode -The protected deployment mode is a temporary mode designed to be active during an initial period after the deployment or upgrade of the DG contracts. In this mode, scheduled proposals cannot be executed immediately; instead, before calling [`EmergencyProtectedTimelock.execute`](#Funtion-EmergencyProtectedTimelockexecute), one has to wait until an **emergency protection timelock** elapses since the proposal scheduling time. +The protected deployment mode is a temporary mode designed to be active during an initial period after the deployment or upgrade of the DG contracts. In this mode, scheduled proposals cannot be executed immediately; instead, before calling [`EmergencyProtectedTimelock.execute`](#Funtion-EmergencyProtectedTimelockexecute), one has to wait until an emergency protection delay elapses since the proposal scheduling time. ![image](https://github.com/lidofinance/dual-governance/assets/1699593/38cb2371-bdb0-4681-9dfd-356fa1ed7959) In this mode, an **emergency activation committee** has the one-off and time-limited right to activate an adversarial **emergency mode** if they see a scheduled proposal that was created or altered due to a vulnerability in the DG contracts or if governance execution is prevented by such a vulnerability. Once the emergency mode is activated, the emergency activation committee is disabled, i.e. loses the ability to activate the emergency mode again. If the emergency activation committee doesn't activate the emergency mode within the duration of the **emergency protection duration** since the committee was configured by the DAO, it gets automatically disabled as well. -The emergency mode lasts up to the **emergency mode max duration** counting from the moment of its activation. While it's active, 1) only the **emergency execution committee** has the right to execute scheduled proposals, and 2) the same committee has the one-off right to **disable the DG subsystem**, i.e. disconnect executor contracts from the DG contracts and reconnect them to the Lido DAO Voting/Agent contract. The latter also disables the emergency mode and the emergency execution committee, so any proposal can be executed by the DAO without cooperation from any other actors. +The emergency mode lasts up to the **emergency mode max duration** counting from the moment of its activation. While it's active, 1) only the **emergency execution committee** has the right to execute scheduled proposals, and 2) the same committee has the one-off right to **disable the DG subsystem**, i.e. disconnect the `EmergencyProtectedTimelock` contract and its associated executor contracts from the DG contracts and reconnect it to the Lido DAO Voting/Agent contract. The latter also disables the emergency mode and the emergency execution committee, so any proposal can be executed by the DAO without cooperation from any other actors. If the emergency execution committee doesn't disable the DG until the emergency mode max duration elapses, anyone gets the right to deactivate the emergency mode, switching the system back to the protected mode and disabling the emergency committee. @@ -205,7 +211,7 @@ The main entry point to the dual governance system. * Implements a state machine tracking the current [global governance state](#Governance-state) which, in turn, determines whether proposal submission and execution is currently allowed. * Deploys and tracks the [`Escrow`](#Contract-Escrowsol) contract instances. Tracks the current signalling escrow. -This contract is a singleton, meaning that any DG deployment includes exectly one instance of this contract. +This contract is a singleton, meaning that any DG deployment includes exactly one instance of this contract. ### Enum: DualGovernance.State @@ -373,7 +379,7 @@ Registers the `proposer` address in the system as a valid proposer and associate #### Preconditions -* MUST be called by the admin executor contract (see `Config.sol`). +* MUST be called by the admin executor contract (see `Configuration.sol`). * The `proposer` address MUST NOT be already registered in the system. * The `executor` instance SHOULD be owned by the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance. @@ -439,20 +445,60 @@ The result of the call. * MUST be called by the contract owner (which SHOULD be the [`EmergencyProtectedTimelock`](#Contract-EmergencyProtectedTimelocksol) singleton instance). +## Contract: ResealExecutor.sol + +In the Lido protocol, specific critical components (`WithdrawalQueue` and `ValidatorsExitBus`) are safeguarded by the `GateSeal` contract instance. According to the gate seals [documentation](https://github.com/lidofinance/gate-seals?tab=readme-ov-file#what-is-a-gateseal): + +>*"A GateSeal is a contract that allows the designated account to instantly put a set of contracts on pause (i.e. seal) for a limited duration. This will give the Lido DAO the time to come up with a solution, hold a vote, implement changes, etc.".* + +However, the effectiveness of this approach is contingent upon the predictability of the DAO's solution adoption timeframe. With the dual governance system, proposal execution may experience significant delays based on the current state of the `DualGovernance` contract. There's a risk that `GateSeal`'s pause period may expire before the Lido DAO can implement the necessary fixes. + +To address this compatibility challenge between gate seals and dual governance, the `ResealExecutor` contract is introduced. The `ResealExecutor` allows to extend pause of temporarily paused contracts to permanent pause, if conditions are met: +- `ResealExecutor` has `PAUSE_ROLE` and `RESUME_ROLE` for target contracts. +- Contracts are paused until timestamp after current timestamp and not for infinite time. +- The DAO governance is blocked by `DualGovernance`. + +It inherits `OwnableExecutor` and provides ability to extend contracts pause for committee set by DAO. + +### Function ResealExecutor.reseal + +```solidity +function reseal(address[] memory sealables) +``` + +This function extends pause of `sealables`. Can be called by committee address. + +#### Preconditions + +- `ResealExecutor` has `PAUSE_ROLE` and `RESUME_ROLE` for target contracts. +- Contracts are paused until timestamp after current timestamp and not for infinite time. +- The DAO governance is blocked by `DualGovernance`. + +### Function ResealExecutor.setResealCommittee + +```solidity +function setResealCommittee(address newResealCommittee) +``` + +This function set `resealCommittee` address to `newResealCommittee`. Can be called by owner. + +#### Preconditions + +- Can be called by `OWNER`. ## Contract: Escrow.sol -The `Escrow` contract serves as an accumulator of users' (w)stETH, withdrawal NFTs, and ETH. It has two internal states and serves a different purpose depending on its state: +The `Escrow` contract serves as an accumulator of users' (w)stETH, withdrawal NFTs (unstETH), and ETH. It has two internal states and serves a different purpose depending on its state: * The initial state is the `SignallingEscrow` state. In this state, the contract serves as an oracle for users' opposition to DAO proposals. It allows users to lock and unlock (unlocking is permitted only for the caller after the `SignallingEscrowMinLockTime` duration has passed since their last funds locking operation) stETH, wstETH, and withdrawal NFTs, potentially changing the global governance state. The `SignallingEscrowMinLockTime` duration, measured in hours, safeguards against manipulating the dual governance state through instant lock/unlock actions within the `Escrow` contract instance. * The final state is the `RageQuitEscrow` state. In this state, the contract serves as an immutable and ungoverned accumulator for the ETH withdrawn as a result of the [rage quit](#Rage-quit) and enforces a timelock on reclaiming this ETH by users. -The `DualGovernance` contract tracks the current signalling escrow contract using the `DualGovernance.signallingEscrow` pointer. Upon the initial deployment of the system, an instance of `Escrow` is deployed in the `SignallingEscrow` state by the `DualGovernance` contract and the `DualGovernance.signallingEscrow` pointer is set to this contract. +The `DualGovernance` contract tracks the current signalling escrow contract using the `DualGovernance.getVetoSignallingEscrow()` pointer. Upon the initial deployment of the system, an instance of `Escrow` is deployed in the `SignallingEscrow` state by the `DualGovernance` contract and the `DualGovernance.getVetoSignallingEscrow()` pointer is set to this contract. Each time the governance enters the global `RageQuit` state, two things happen simultaneously: -1. The `Escrow` instance currently stored in the `DualGovernance.signallingEscrow` pointer changes its state from `SignallingEscrow` to `RageQuitEscrow`. This is the only possible (and thus irreversible) state transition. -2. The `DualGovernance` contract deploys a new instance of `Escrow` in the `SignallingEscrow` state and resets the `DualGovernance.signallingEscrow` pointer to this newly-deployed contract. +1. The `Escrow` instance currently stored in the `DualGovernance.getVetoSignallingEscrow()` pointer changes its state from `SignallingEscrow` to `RageQuitEscrow`. This is the only possible (and thus irreversible) state transition. +2. The `DualGovernance` contract deploys a new instance of `Escrow` in the `SignallingEscrow` state and resets the `DualGovernance.getVetoSignallingEscrow()` pointer to this newly-deployed contract. At any point in time, there can be only one instance of the contract in the `SignallingEscrow` state (so the contract in this state is a singleton) but multiple instances of the contract in the `RageQuitEscrow` state. @@ -462,14 +508,14 @@ Once all funds locked in the `Escrow` instance are converted into withdrawal NFT The purpose of the `RageQuitExtensionDelay` phase is to provide sufficient time to participants who locked withdrawal NFTs to claim them before Lido DAO's proposal execution is unblocked. As soon as a withdrawal NFT is claimed, the user's ETH is no longer affected by any code controlled by the DAO. -When the `RageQuitExtensionDelay` period elapses, the `DualGovernance.activateNextState()` function exits the `RageQuit` state and initiates the `RageQuitEthClaimTimelock`. Throughout this timelock, tokens remain locked within the `Escrow` instance and are inaccessible for withdrawal. Once the timelock expires, participants in the rage quit process can retrieve their ETH by withdrawing it from the `Escrow` instance. +When the `RageQuitExtensionDelay` period elapses, the `DualGovernance.activateNextState()` function exits the `RageQuit` state and initiates the `RageQuitEthWithdrawalsTimelock`. Throughout this timelock, tokens remain locked within the `Escrow` instance and are inaccessible for withdrawal. Once the timelock expires, participants in the rage quit process can retrieve their ETH by withdrawing it from the `Escrow` instance. -The duration of the `RageQuitEthClaimTimelock` is dynamic and varies based on the number of "continuous" rage quits. A pair of rage quits is considered continuous when `DualGovernance` has not transitioned to the `Normal` or `VetoCooldown` state between them. +The duration of the `RageQuitEthWithdrawalsTimelock` is dynamic and varies based on the number of "continuous" rage quits. A pair of rage quits is considered continuous when `DualGovernance` has not transitioned to the `Normal` or `VetoCooldown` state between them. ### Function: Escrow.lockStETH ```solidity! -function lockStETH(uint256 amount) +function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) ``` Transfers the specified `amount` of stETH from the caller's (i.e., `msg.sender`) account into the `SignallingEscrow` instance of the `Escrow` contract. @@ -477,15 +523,19 @@ Transfers the specified `amount` of stETH from the caller's (i.e., `msg.sender`) The total rage quit support is updated proportionally to the number of shares corresponding to the locked stETH (see the `Escrow.getRageQuitSupport()` function for the details). For the correct rage quit support calculation, the function updates the number of locked stETH shares in the protocol as follows: ```solidity -uint256 amountInShares = stETH.getSharesByPooledEther(amount); +amountInShares = stETH.getSharesByPooledEther(amount); -_vetoersLockedAssets[msg.sender].stETHShares += amountInShares; -_totalStEthSharesLocked += amountInShares; +assets[msg.sender].stETHLockedShares += amountInShares; +stETHTotals.lockedShares += amountInShares; ``` The rage quit support will be dynamically updated to reflect changes in the stETH balance due to protocol rewards or validators slashing. -Finally, calls the `DualGovernance.activateNextState()` function. This action may transit the `Escrow` instance from the `SignallingEscrow` state into the `RageQuitEscrow` state. +Finally, the function calls `DualGovernance.activateNextState()`, which may transition the `Escrow` instance from the `SignallingEscrow` state to the `RageQuitEscrow` state. + +#### Returns + +The amount of stETH shares locked by the caller during the current method call. #### Preconditions @@ -496,42 +546,54 @@ Finally, calls the `DualGovernance.activateNextState()` function. This action ma ### Function: Escrow.unlockStETH ```solidity -function unlockStETH() +function unlockStETH() external returns (uint256 unlockedStETHShares) ``` -Allows the caller (i.e. `msg.sender`) to unlock the previously locked stETH in the `SignallingEscrow` instance of the `Escrow` contract. The locked stETH balance may change due to protocol rewards or validators slashing, potentially altering the original locked amount. The total unlocked stETH equals the sum of all previously locked stETH by the caller, accounting for any changes during the locking period. +Allows the caller (i.e., `msg.sender`) to unlock all previously locked stETH and wstETH in the `SignallingEscrow` instance of the `Escrow` contract as stETH. The locked balance may change due to protocol rewards or validator slashing, potentially altering the original locked amount. The total unlocked stETH amount equals the sum of all previously locked stETH and wstETH by the caller, accounting for any changes during the locking period. -For the correct rage quit support calculation, the function updates the number of locked stETH shares in the protocol as follows: +For accurate rage quit support calculation, the function updates the number of locked stETH shares in the protocol as follows: ```solidity -_totalStEthSharesLocked -= _vetoersLockedAssets[msg.sender].stETHShares; -_vetoersLockedAssets[msg.sender].stETHShares = 0; +stETHTotals.lockedShares -= _assets[msg.sender].stETHLockedShares; +assets[msg.sender].stETHLockedShares = 0; ``` Additionally, the function triggers the `DualGovernance.activateNextState()` function at the beginning and end of the execution. +#### Returns + +The amount of stETH shares unlocked by the caller. + #### Preconditions - The `Escrow` instance MUST be in the `SignallingEscrow` state. - The caller MUST have a non-zero amount of previously locked stETH in the `Escrow` instance using the `Escrow.lockStETH` function. -- At least the duration of the `SignallingEscrowMinLockTime` MUST have passed since the caller last invoked any of the methods `Escrow.lockStETH`, `Escrow.lockWstETH`, or `Escrow.lockUnstETH`. +- The duration of the `SignallingEscrowMinLockTime` MUST have passed since the caller last invoked any of the methods `Escrow.lockStETH`, `Escrow.lockWstETH`, or `Escrow.lockUnstETH`. ### Function: Escrow.lockWstETH ```solidity -function lockWstETH(uint256 amount) +function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) ``` -Transfers the specified `amount` of wstETH from the caller's (i.e., `msg.sender`) account into the `SignallingEscrow` instance of the `Escrow` contract. +Transfers the specified `amount` of wstETH from the caller's (i.e., `msg.sender`) account into the `SignallingEscrow` instance of the `Escrow` contract and unwraps it into the stETH. -The total rage quit support is updated proportionally to the `amount` of locked wstETH (see the `Escrow.getRageQuitSupport()` function for the details). For the correct rage quit support calculation, the function updates the number of locked wstETH in the protocol as follows: +The total rage quit support is updated proportionally to the `amount` of locked wstETH (see the `Escrow.getRageQuitSupport()` function for details). For accurate rage quit support calculation, the function updates the number of locked stETH shares in the protocol as follows: ```solidity -_vetoersLockedAssets[msg.sender].wstETHShares += amount; -_totalStEthSharesLocked += amount; +stETHAmount = WST_ETH.unwrap(amount); +// Use getSharesByPooledEther(), because unwrap() method may transfer 1 wei less amount of stETH +stETHShares = ST_ETH.getSharesByPooledEth(stETHAmount); + +assets[msg.sender].stETHLockedShares += stETHShares; +stETHTotals.lockedShares += stETHShares; ``` -Finally, calls the `DualGovernance.activateNextState()` function. This action may transit the `Escrow` instance from the `SignallingEscrow` state into the `RageQuitEscrow` state. +Finally, the function calls the `DualGovernance.activateNextState()`. This action may transition the `Escrow` instance from the `SignallingEscrow` state into the `RageQuitEscrow` state. + +#### Returns + +The amount of stETH shares locked by the caller during the current method call. #### Preconditions @@ -542,20 +604,24 @@ Finally, calls the `DualGovernance.activateNextState()` function. This action ma ### Function: Escrow.unlockWstETH ```solidity -function unlockWstETH() +function unlockWstETH() external returns (uint256 unlockedStETHShares) ``` -Allows the caller (i.e. `msg.sender`) to unlock previously locked wstETH from the `SignallingEscrow` instance of the `Escrow` contract. The total unlocked wstETH equals the sum of all previously locked wstETH by the caller. +Allows the caller (i.e. `msg.sender`) to unlock previously locked wstETH and stETH from the `SignallingEscrow` instance of the `Escrow` contract as wstETH. The locked balance may change due to protocol rewards or validator slashing, potentially altering the original locked amount. The total unlocked wstETH equals the sum of all previously locked wstETH and stETH by the caller. -For the correct rage quit support calculation, the function updates the number of locked wstETH shares in the protocol as follows: +For the correct rage quit support calculation, the function updates the number of locked stETH shares in the protocol as follows: ```solidity -_totalStEthSharesLocked -= _vetoersLockedAssets[msg.sender].wstETHShares; -_vetoersLockedAssets[msg.sender].wstETHShares = 0; +stETHTotals.lockedShares -= _assets[msg.sender].stETHLockedShares; +assets[msg.sender].stETHLockedShares = 0; ``` Additionally, the function triggers the `DualGovernance.activateNextState()` function at the beginning and end of the execution. +#### Returns + +The amount of stETH shares unlocked by the caller. + #### Preconditions - The `Escrow` instance MUST be in the `SignallingEscrow` state. @@ -569,15 +635,16 @@ Additionally, the function triggers the `DualGovernance.activateNextState()` fun function lockUnstETH(uint256[] unstETHIds) ``` -Transfers the WIthdrawal NFTs with ids contained in the `unstETHIds` from the caller's (i.e. `msg.sender`) account into the `SignallingEscrow` instance of the `Escrow` contract. +Transfers the withdrawal NFTs with ids contained in the `unstETHIds` from the caller's (i.e. `msg.sender`) account into the `SignallingEscrow` instance of the `Escrow` contract. + -To correctly calculate the rage quit support (see the `Escrow.getRageQuitSupport()` function for the details), updates the number of locked Withdrawal NFT shares in the protocol for each withdrawal NFT in the `unstETHIds`, as follows: +To correctly calculate the rage quit support (see the `Escrow.getRageQuitSupport()` function for the details), updates the number of locked withdrawal NFT shares in the protocol for each withdrawal NFT in the `unstETHIds`, as follows: ```solidity -uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; +uint256 amountOfShares = withdrawalRequests[id].amountOfShares; -_vetoersLockedAssets[msg.sender].withdrawalNFTShares += amountOfShares; -_totalWithdrawlNFTSharesLocked += amountOfShares; +assets[msg.sender].unstETHLockedShares += amountOfShares; +unstETHTotals.unfinalizedShares += amountOfShares; ``` Finally, calls the `DualGovernance.activateNextState()` function. This action may transition the `Escrow` instance from the `SignallingEscrow` state into the `RageQuitEscrow` state. @@ -587,7 +654,7 @@ Finally, calls the `DualGovernance.activateNextState()` function. This action ma - The `Escrow` instance MUST be in the `SignallingEscrow` state. - The caller MUST be the owner of all withdrawal NFTs with the given ids. - The caller MUST grant permission to the `SignallingEscrow` instance to transfer tokens with the given ids (`approve()` or `setApprovalForAll()`). -- The passed ids MUST NOT contain the finalized or claimed Withdrawal NFTs. +- The passed ids MUST NOT contain the finalized or claimed withdrawal NFTs. - The passed ids MUST NOT contain duplicates. ### Function: Escrow.unlockUnstETH @@ -596,32 +663,28 @@ Finally, calls the `DualGovernance.activateNextState()` function. This action ma function unlockUnstETH(uint256[] unstETHIds) ``` -Allows the caller (i.e. `msg.sender`) to unlock a set of previously locked Withdrawal NFTs with ids `unstETHIds` from the `SignallingEscrow` instance of the `Escrow` contract. +Allows the caller (i.e. `msg.sender`) to unlock a set of previously locked withdrawal NFTs with ids `unstETHIds` from the `SignallingEscrow` instance of the `Escrow` contract. -To correctly calculate the rage quit support (see the `Escrow.getRageQuitSupport()` function for details), updates the number of locked Withdrawal NFT shares in the protocol for each withdrawal NFT in the `unstETHIds`, as follows: +To correctly calculate the rage quit support (see the `Escrow.getRageQuitSupport()` function for details), updates the number of locked withdrawal NFT shares in the protocol for each withdrawal NFT in the `unstETHIds`, as follows: -- If the Withdrawal NFT was marked as finalized (see the `Escrow.markUnstETHFinalized()` function for details): +- If the withdrawal NFT was marked as finalized (see the `Escrow.markUnstETHFinalized()` function for details): ```solidity -uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; +uint256 amountOfShares = withdrawalRequests[id].amountOfShares; uint256 claimableAmount = _getClaimableEther(id); -_totalWithdrawlNFTSharesLocked -= amountOfShares; -_totalFinalizedWithdrawlNFTSharesLocked -= amountOfShares; -_totalFinalizedWithdrawlNFTAmountLocked -= claimableAmount; - -_vetoersLockedAssets[msg.sender].withdrawalNFTShares -= amountOfShares; -_vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTShares -= amountOfShares; -_vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTAmount -= claimableAmount; +assets[msg.sender].unstETHLockedShares -= amountOfShares; +unstETHTotals.finalizedETH -= claimableAmount; +unstETHTotals.unfinalizedShares -= amountOfShares; ``` - if the Withdrawal NFT wasn't marked as finalized: ```solidity -uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; +uint256 amountOfShares = withdrawalRequests[id].amountOfShares; -_totalWithdrawlNFTSharesLocked -= amountOfShares; -_vetoersLockedAssets[msg.sender].withdrawalNFTShares -= amountOfShares; +assets[msg.sender].unstETHLockedShares -= amountOfShares; +unstETHTotals.unfinalizedShares -= amountOfShares; ``` Additionally, the function triggers the `DualGovernance.activateNextState()` function at the beginning and end of the execution. @@ -629,7 +692,7 @@ Additionally, the function triggers the `DualGovernance.activateNextState()` fun #### Preconditions - The `Escrow` instance MUST be in the `SignallingEscrow` state. -- Each provided Withdrawal NFT MUST have been previously locked by the caller. +- Each provided withdrawal NFT MUST have been previously locked by the caller. - At least the duration of the `SignallingEscrowMinLockTime` MUST have passed since the caller last invoked any of the methods `Escrow.lockStETH`, `Escrow.lockWstETH`, or `Escrow.lockUnstETH`. ### Function Escrow.markUnstETHFinalized @@ -638,31 +701,28 @@ Additionally, the function triggers the `DualGovernance.activateNextState()` fun function markUnstETHFinalized(uint256[] unstETHIds, uint256[] hints) ``` -Marks the provided Withdrawal NFTs with ids `unstETHIds` as finalized to accurately calculate their rage quit support. +Marks the provided withdrawal NFTs with ids `unstETHIds` as finalized to accurately calculate their rage quit support. -The finalization of the Withdrawal NFT leads to the following events: +The finalization of the withdrawal NFT leads to the following events: -- The value of the Withdrawal NFT is no longer affected by stETH token rebases. -- The total supply of stETH is adjusted based on the value of the finalized Withdrawal NFT. +- The value of the withdrawal NFT is no longer affected by stETH token rebases. +- The total supply of stETH is adjusted based on the value of the finalized withdrawal NFT. -As both of these events affect the rage quit support value, this function updates the number of finalized Withdrawal NFTs for the correct rage quit support accounting. +As both of these events affect the rage quit support value, this function updates the number of finalized withdrawal NFTs for the correct rage quit support accounting. -For each Withdrawal NFT in the `unstETHIds`: +For each withdrawal NFT in the `unstETHIds`: ```solidity uint256 claimableAmount = _getClaimableEther(id); -uint256 amountOfShares = WithdrawalRequest[id].amountOfShares; - -_totalFinalizedWithdrawlNFTSharesLocked += amountOfShares; -_totalFinalizedWithdrawlNFTAmountLocked += claimableAmount; +uint256 amountOfShares = withdrawalRequests[id].amountOfShares; -_vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTShares += amountOfShares; -_vetoersLockedAssets[msg.sender].finalizedWithdrawalNFTAmount += claimableAmount; +unstETHTotals.finalizedETH += claimableAmount; +unstETHTotals.unfinalizedShares -= amountOfShares; ``` Withdrawal NFTs belonging to any of the following categories are excluded from the rage quit support update: -- Claimed or unfinalized Withdrawal NFTs +- Claimed or unfinalized withdrawal NFTs - Withdrawal NFTs already marked as finalized - Withdrawal NFTs not locked in the `Escrow` instance @@ -676,34 +736,31 @@ Withdrawal NFTs belonging to any of the following categories are excluded from t function getRageQuitSupport() view returns (uint256) ``` -Calculates and returns the total rage quit support as a percentage of the stETH total supply locked in the instance of the `Escrow` contract. It considers contributions from stETH, wstETH, and non-finalized Withdrawal NFTs while adjusting for the impact of locked finalized Withdrawal NFTs. +Calculates and returns the total rage quit support as a percentage of the stETH total supply locked in the instance of the `Escrow` contract. It considers contributions from stETH, wstETH, and non-finalized withdrawal NFTs while adjusting for the impact of locked finalized withdrawal NFTs. The returned value represents the total rage quit support expressed as a percentage with a precision of 16 decimals. It is computed using the following formula: ```solidity -uint256 rebaseableAmount = stETH.getPooledEthByShares( - _totalStEthSharesLocked + - _totalWstEthSharesLocked + - _totalWithdrawalNFTSharesLocked - - _totalFinalizedWithdrawalNFTSharesLocked -); +uint256 finalizedETH = unstETHTotals.finalizedETH; +uint256 unfinalizedShares = stETHTotals.lockedShares + unstETHTotals.unfinalizedShares; return 10 ** 18 * ( - rebaseableAmount + _totalFinalizedWithdrawalNFTAmountLocked + ST_ETH.getPooledEtherByShares(unfinalizedShares) + finalizedETH ) / ( - stETH.totalSupply() + _totalFinalizedWithdrawalNFTAmountLocked + stETH.totalSupply() + finalizedETH ); ``` ### Function Escrow.startRageQuit ```solidity -function startRageQuit() +function startRageQuit( + Duration rageQuitExtensionDelay, + Duration rageQuitWithdrawalsTimelock +) ``` -Transits the `Escrow` instance from the `SignallingEscrow` state to the `RageQuitEscrow` state. Following this transition, locked funds become unwithdrawable and are accessible to users only as plain ETH after the completion of the full `RageQuit` process, including the `RageQuitExtensionDelay` and `RageQuitEthClaimTimelock` stages. - -As the initial step of transitioning to the `RageQuitEscrow` state, all locked wstETH is converted into stETH, and the maximum stETH allowance is granted to the `WithdrawalQueue` contract for the upcoming creation of Withdrawal NFTs. +Transits the `Escrow` instance from the `SignallingEscrow` state to the `RageQuitEscrow` state. Following this transition, locked funds become unwithdrawable and are accessible to users only as plain ETH after the completion of the full `RageQuit` process, including the `RageQuitExtensionDelay` and `RageQuitEthWithdrawalsTimelock` stages. #### Preconditions @@ -713,32 +770,40 @@ As the initial step of transitioning to the `RageQuitEscrow` state, all locked w ### Function Escrow.requestNextWithdrawalsBatch ```solidity -function requestNextWithdrawalsBatch(uint256 maxWithdrawalRequestsCount) +function requestNextWithdrawalsBatch(uint256 maxBatchSize) ``` -Transfers stETH held in the `RageQuitEscrow` instance into the `WithdrawalQueue`. The function may be invoked multiple times until all stETH is converted into Withdrawal NFTs. For each Withdrawal NFT, the owner is set to `Escrow` contract instance. Each call creates up to `maxWithdrawalRequestsCount` withdrawal requests, where each withdrawal request size equals `WithdrawalQueue.MAX_STETH_WITHDRAWAL_AMOUNT()`, except for potentially the last batch, which may have a smaller size. +Transfers stETH held in the `RageQuitEscrow` instance into the `WithdrawalQueue`. The function may be invoked multiple times until all stETH is converted into withdrawal NFTs. For each withdrawal NFT, the owner is set to `Escrow` contract instance. Each call creates up to `maxBatchSize` withdrawal requests, where each withdrawal request size equals `WithdrawalQueue.MAX_STETH_WITHDRAWAL_AMOUNT()`, except for potentially the last batch, which may have a smaller size. -Upon execution, the function updates the count of withdrawal requests generated by all invocations. When the remaining stETH balance on the contract falls below `WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT()`, the generation of withdrawal batches is concluded, and subsequent function calls will revert. +Upon execution, the function tracks the ids of the withdrawal requests generated by all invocations. When the remaining stETH balance on the contract falls below `WITHDRAWAL_QUEUE.MIN_STETH_WITHDRAWAL_AMOUNT()`, the generation of withdrawal batches is concluded, and subsequent function calls will revert. #### Preconditions - The `Escrow` instance MUST be in the `RageQuitEscrow` state. -- The `maxWithdrawalRequestsCount` MUST be greater than 0 -- The generation of WithdrawalRequest batches MUST not be concluded +- The `maxBatchSize` MUST be greater than or equal to `CONFIG.MIN_WITHDRAWALS_BATCH_SIZE()` and less than or equal to `CONFIG.MAX_WITHDRAWALS_BATCH_SIZE()`. +- The generation of withdrawal request batches MUST not be concluded -### Function Escrow.claimNextWithdrawalsBatch +### Function Escrow.claimNextWithdrawalsBatch(uint256, uint256[]) ```solidity -function claimNextWithdrawalsBatch(uint256[] withdrawalRequestIds, uint256[] hints) +function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] hints) ``` -Allows users to claim finalized Withdrawal NFTs generated by the `Escrow.requestNextWithdrawalsBatch()` function. -Tracks the total amount of claimed ETH updating the `_totalClaimedEthAmount` variable. Upon claiming the last batch, the `RageQuitExtensionDelay` period commences. +Allows users to claim finalized withdrawal NFTs generated by the `Escrow.requestNextWithdrawalsBatch()` function. +Tracks the total amount of claimed ETH updating the `stETHTotals.claimedETH` variable. Upon claiming the last batch, the `RageQuitExtensionDelay` period commences. #### Preconditions - The `Escrow` instance MUST be in the `RageQuitEscrow` state. -- The `withdrawalRequestIds` array MUST contain only the ids of finalized but unclaimed withdrawal requests generated by the `Escrow.requestNextWithdrawalsBatch()` function. +- The `fromUnstETHId` MUST be equal to the id of the first unclaimed withdrawal NFT locked in the `Escrow`. The ids of the unclaimed withdrawal NFTs can be retrieved via the `getNextWithdrawalBatch()` method. + +### Function Escrow.claimNextWithdrawalsBatch(uint256) + +```solidity +function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) +``` + +This is an overload version of `Escrow.claimNextWithdrawalsBatch(uint256, uint256[])`. It retrieves hints for processing the withdrawal NFTs on-chain. ### Function Escrow.claimUnstETH @@ -746,9 +811,9 @@ Tracks the total amount of claimed ETH updating the `_totalClaimedEthAmount` var function claimUnstETH(uint256[] unstETHIds, uint256[] hints) ``` -Allows users to claim the ETH associated with finalized Withdrawal NFTs with ids `unstETHIds` locked in the `Escrow` contract. Upon calling this function, the claimed ETH is transferred to the `Escrow` contract instance. +Allows users to claim the ETH associated with finalized withdrawal NFTs with ids `unstETHIds` locked in the `Escrow` contract. Upon calling this function, the claimed ETH is transferred to the `Escrow` contract instance. -To safeguard the ETH associated with Withdrawal NFTs, this function should be invoked when the `Escrow` is in the `RageQuitEscrow` state and before the `RageQuitExtensionDelay` period ends. The ETH corresponding to unclaimed Withdrawal NFTs after this period ends would still be controlled by the code potentially afftected by pending and future DAO decisions. +To safeguard the ETH associated with withdrawal NFTs, this function should be invoked when the `Escrow` is in the `RageQuitEscrow` state and before the `RageQuitExtensionDelay` period ends. The ETH corresponding to unclaimed withdrawal NFTs after this period ends would still be controlled by the code potentially affected by pending and future DAO decisions. #### Preconditions @@ -766,67 +831,43 @@ Returns whether the rage quit process has been finalized. The rage quit process - All withdrawal request batches have been claimed. - The duration of the `RageQuitExtensionDelay` has elapsed. -### Function Escrow.withdrawStEthAsEth +### Function Escrow.withdrawETH ```solidity -function withdrawStEthAsEth() +function withdrawETH() ``` -Allows the caller (i.e. `msg.sender`) to withdraw all stETH they have previouusly locked into `Escrow` contract instance (while it was in the `SignallingEscrow` state) as plain ETH, given that the `RageQuit` process is completed and that the `RageQuitEthClaimTimelock` has elapsed. Upon execution, the function transfers ETH to the caller's account and marks the corresponding stETH as withdrawn for the caller. +Allows the caller (i.e. `msg.sender`) to withdraw all stETH and wstETH they have previously locked into `Escrow` contract instance (while it was in the `SignallingEscrow` state) as plain ETH, given that the `RageQuit` process is completed and that the `RageQuitEthWithdrawalsTimelock` has elapsed. Upon execution, the function transfers ETH to the caller's account and marks the corresponding stETH and wstETH as withdrawn for the caller. -The amount of ETH sent to the caller is determined by the proportion of the user's stETH shares compared to the total amount of locked stETH and wstETH shares in the Escrow instance, calculated as follows: +The amount of ETH sent to the caller is determined by the proportion of the user's stETH and wstETH shares compared to the total amount of locked stETH and wstETH shares in the Escrow instance, calculated as follows: ```solidity -return _totalClaimedEthAmount * _vetoersLockedAssets[msg.sender].stETHShares - / (_totalStEthSharesLocked + _totalWstEthSharesLocked); +return stETHTotals.claimedETH * assets[msg.sender].stETHLockedShares + / stETHTotals.lockedShares; ``` #### Preconditions - The `Escrow` instance MUST be in the `RageQuitEscrow` state. - The rage quit process MUST be completed, including the expiration of the `RageQuitExtensionDelay` duration. -- The `RageQuitEthClaimTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. -- The caller MUST have a non-zero amount of stETH to withdraw. -- The caller MUST NOT have previously withdrawn stETH. - -### Function Escrow.withdrawWstEthAsEth - -```solidity -function withdrawWstEthAsEth() external -``` - -Allows the caller (i.e. `msg.sender`) to withdraw all wstETH they have previouusly locked into `Escrow` contract instance (while it was in the `SignallingEscrow` state) as plain ETH, given that the `RageQuit` process is completed and that the `RageQuitEthClaimTimelock` has elapsed. Upon execution, the function transfers ETH to the caller's account and marks the corresponding wstETH as withdrawn for the caller. - -The amount of ETH sent to the caller is determined by the proportion of the user's wstETH funds compared to the total amount of locked stETH and wstETH shares in the Escrow instance, calculated as follows: - -```solidity -return _totalClaimedEthAmount * - _vetoersLockedAssets[msg.sender].wstETHShares / - (_totalStEthSharesLocked + _totalWstEthSharesLocked); -``` - -#### Preconditions -- The `Escrow` instance MUST be in the `RageQuitEscrow` state. -- The rage quit process MUST be completed, including the expiration of the `RageQuitExtensionDelay` duration. -- The `RageQuitEthClaimTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. -- The caller MUST have a non-zero amount of wstETH to withdraw. -- The caller MUST NOT have previously withdrawn wstETH. +- The `RageQuitEthWithdrawalsTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. +- The caller MUST have a non-zero amount of stETH shares to withdraw. -### Function Escrow.withdrawUnstETHAsEth +### Function Escrow.withdrawETH() ```solidity -function withdrawUnstETHAsEth(uint256[] unstETHIds) +function withdrawETH(uint256[] unstETHIds) ``` -Allows the caller (i.e. `msg.sender`) to withdraw the claimed ETH from the Withdrawal NFTs with ids `unstETHIds` locked by the caller in the `Escrow` contract while the latter was in the `SignallingEscrow` state. Upon execution, all ETH previously claimed from the NFTs is transferred to the caller's account, and the NFTs are marked as withdrawn. +Allows the caller (i.e. `msg.sender`) to withdraw the claimed ETH from the withdrawal NFTs with ids `unstETHIds` locked by the caller in the `Escrow` contract while the latter was in the `SignallingEscrow` state. Upon execution, all ETH previously claimed from the NFTs is transferred to the caller's account, and the NFTs are marked as withdrawn. #### Preconditions - The `Escrow` instance MUST be in the `RageQuitEscrow` state. - The rage quit process MUST be completed, including the expiration of the `RageQuitExtensionDelay` duration. -- The `RageQuitEthClaimTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. +- The `RageQuitEthWithdrawalsTimelock` period MUST be elapsed after the expiration of the `RageQuitExtensionDelay` duration. - The caller MUST be set as the owner of the provided NFTs. -- Each Withdrawal NFT MUST have been claimed using the `Escrow.claimUnstETH()` function. +- Each withdrawal NFT MUST have been claimed using the `Escrow.claimUnstETH()` function. - Withdrawal NFTs must not have been withdrawn previously. @@ -837,14 +878,14 @@ Allows the caller (i.e. `msg.sender`) to withdraw the claimed ETH from the Withd For a proposal to be executed, the following steps have to be performed in order: 1. The proposal must be submitted using the `EmergencyProtectedTimelock.submit` function. -2. The configured post-submit timelock must elapse. +2. The configured post-submit timelock (`Configuration.AFTER_SUBMIT_DELAY()`) must elapse. 3. The proposal must be scheduled using the `EmergencyProtectedTimelock.schedule` function. -4. The configured emergency protection timelock must elapse (can be zero, see below). +4. The configured emergency protection delay (`Configuration.AFTER_SCHEDULE_DELAY()`) must elapse (can be zero, see below). 5. The proposal must be executed using the `EmergencyProtectedTimelock.execute` function. The contract only allows proposal submission and scheduling by the `governance` address. Normally, this address points to the [`DualGovernance`](#Contract-DualGovernancesol) singleton instance. Proposal execution is permissionless, unless Emergency Mode is activated. -If the Emergency Committees are set up and active, the governance proposal gets a separate emergency protection timelock between submitting and scheduling. This additional timelock is implemented in the `EmergencyProtectedTimelock` contract to protect from zero-day vulnerability in the logic of `DualGovenance.sol` and other core DG contracts. If the Emergency Committees aren't set, the proposal flow is the same, but the timelock duration is zero. +If the Emergency Committees are set up and active, the governance proposal gets a separate emergency protection delay between submitting and scheduling. This additional timelock is implemented in the `EmergencyProtectedTimelock` contract to protect from zero-day vulnerability in the logic of `DualGovernance.sol` and other core DG contracts. If the Emergency Committees aren't set, the proposal flow is the same, but the timelock duration is zero. Emergency Activation Committee, while active, can enable the Emergency Mode. This mode prohibits anyone but the Emergency Execution Committee from executing proposals. It also allows the Emergency Execution Committee to reset the governance, effectively disabling the Dual Governance subsystem. @@ -973,6 +1014,11 @@ The contract has the interface for managing the configuration related to emergen `Configuration.sol` is the smart contract encompassing all the constants in the Dual Governance design & providing the interfaces for getting access to them. It implements interfaces `IAdminExecutorConfiguration`, `ITimelockConfiguration`, `IDualGovernanceConfiguration` covering for relevant "parameters domains". +## Contract: TiebreakerCore.sol +## Contract: TiebreakerSubCommittee.sol +## Contract: EmergencyActivationCommittee.sol +## Contract: EmergencyExecutionCommittee.sol +## Contract: ResealCommittee.sol ## Upgrade flow description diff --git a/test/mocks/GateSealMock.sol b/test/mocks/GateSealMock.sol index fbd8e069..632c2269 100644 --- a/test/mocks/GateSealMock.sol +++ b/test/mocks/GateSealMock.sol @@ -12,11 +12,11 @@ contract GateSealMock is IGateSeal { uint256 internal constant _INFINITE_DURATION = type(uint256).max; uint256 internal _expiryTimestamp; - uint256 internal _minSealDuration; + uint256 internal _seal_duration_seconds; address[] internal _sealedSealables; - constructor(uint256 minSealDuration, uint256 lifetime) { - _minSealDuration = minSealDuration; + constructor(uint256 sealDurationSeconds, uint256 lifetime) { + _seal_duration_seconds = sealDurationSeconds; _expiryTimestamp = block.timestamp + lifetime; } @@ -28,22 +28,10 @@ contract GateSealMock is IGateSeal { _expiryTimestamp = block.timestamp; for (uint256 i = 0; i < sealables.length; ++i) { - ISealable(sealables[i]).pauseFor(_INFINITE_DURATION); + ISealable(sealables[i]).pauseFor(_seal_duration_seconds); assert(ISealable(sealables[i]).isPaused()); } emit SealablesSealed(sealables); } - - function sealed_sealables() external view returns (address[] memory) { - return _sealedSealables; - } - - function get_min_seal_duration() external view returns (uint256) { - return _minSealDuration; - } - - function get_expiry_timestamp() external view returns (uint256) { - return _expiryTimestamp; - } } diff --git a/test/scenario/agent-timelock.t.sol b/test/scenario/agent-timelock.t.sol index f209a1e9..75e365c8 100644 --- a/test/scenario/agent-timelock.t.sol +++ b/test/scenario/agent-timelock.t.sol @@ -76,7 +76,7 @@ contract AgentTimelockTest is ScenarioTestBlueprint { // --- { // wait until the delay has passed - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() + 1); + _wait(_config.AFTER_SUBMIT_DELAY().plusSeconds(1)); // when the first delay is passed and the is no opposition from the stETH holders // the proposal can be scheduled @@ -95,17 +95,17 @@ contract AgentTimelockTest is ScenarioTestBlueprint { { // some time passes and emergency committee activates emergency mode // and resets the controller - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); // committee resets governance - vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); + vm.prank(address(_emergencyActivationCommittee)); _timelock.activateEmergencyMode(); - vm.prank(_EMERGENCY_EXECUTION_COMMITTEE); + vm.prank(address(_emergencyExecutionCommittee)); _timelock.emergencyReset(); // proposal is canceled now - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2 + 1); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2).plusSeconds(1)); // remove canceled call from the timelock _assertCanExecute(proposalId, false); diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 66a07544..77ea81e0 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -2,14 +2,15 @@ pragma solidity 0.8.23; import {WithdrawalRequestStatus} from "../utils/interfaces.sol"; - +import {Duration as DurationType} from "contracts/types/Duration.sol"; import { Escrow, Balances, + WITHDRAWAL_QUEUE, + ScenarioTestBlueprint, VetoerState, LockedAssetsTotals, - WITHDRAWAL_QUEUE, - ScenarioTestBlueprint + Durations } from "../utils/scenario-test-blueprint.sol"; contract TestHelpers is ScenarioTestBlueprint { @@ -47,8 +48,8 @@ contract TestHelpers is ScenarioTestBlueprint { contract EscrowHappyPath is TestHelpers { Escrow internal escrow; - uint256 internal immutable _RAGE_QUIT_EXTRA_TIMELOCK = 14 days; - uint256 internal immutable _RAGE_QUIT_WITHDRAWALS_TIMELOCK = 7 days; + DurationType internal immutable _RAGE_QUIT_EXTRA_TIMELOCK = Durations.from(14 days); + DurationType internal immutable _RAGE_QUIT_WITHDRAWALS_TIMELOCK = Durations.from(7 days); address internal immutable _VETOER_1 = makeAddr("VETOER_1"); address internal immutable _VETOER_2 = makeAddr("VETOER_2"); @@ -88,29 +89,35 @@ contract EscrowHappyPath is TestHelpers { function test_lock_unlock() public { uint256 firstVetoerStETHBalanceBefore = _ST_ETH.balanceOf(_VETOER_1); - uint256 firstVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_1); - - uint256 secondVetoerStETHBalanceBefore = _ST_ETH.balanceOf(_VETOER_2); uint256 secondVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_2); - _lockStETH(_VETOER_1, 10 ** 18); - _lockWstETH(_VETOER_1, 2 * 10 ** 18); + uint256 firstVetoerLockStETHAmount = 1 ether; + uint256 firstVetoerLockWstETHAmount = 2 ether; - _lockStETH(_VETOER_2, 3 * 10 ** 18); - _lockWstETH(_VETOER_2, 5 * 10 ** 18); + uint256 secondVetoerLockStETHAmount = 3 ether; + uint256 secondVetoerLockWstETHAmount = 5 ether; - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); + _lockStETH(_VETOER_1, firstVetoerLockStETHAmount); + _lockWstETH(_VETOER_1, firstVetoerLockWstETHAmount); - _unlockStETH(_VETOER_1); - _unlockWstETH(_VETOER_1); - _unlockStETH(_VETOER_2); - _unlockWstETH(_VETOER_2); + _lockStETH(_VETOER_2, secondVetoerLockStETHAmount); + _lockWstETH(_VETOER_2, secondVetoerLockWstETHAmount); + + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); - assertEq(firstVetoerWstETHBalanceBefore, _WST_ETH.balanceOf(_VETOER_1)); - assertApproxEqAbs(firstVetoerStETHBalanceBefore, _ST_ETH.balanceOf(_VETOER_1), 1); + _unlockStETH(_VETOER_1); + assertApproxEqAbs( + _ST_ETH.balanceOf(_VETOER_1), + firstVetoerStETHBalanceBefore + _ST_ETH.getPooledEthByShares(firstVetoerLockWstETHAmount), + 1 + ); - assertEq(secondVetoerWstETHBalanceBefore, _WST_ETH.balanceOf(_VETOER_2)); - assertApproxEqAbs(secondVetoerStETHBalanceBefore, _ST_ETH.balanceOf(_VETOER_2), 1); + _unlockWstETH(_VETOER_2); + assertApproxEqAbs( + secondVetoerWstETHBalanceBefore, + _WST_ETH.balanceOf(_VETOER_2), + secondVetoerWstETHBalanceBefore + _ST_ETH.getSharesByPooledEth(secondVetoerLockWstETHAmount) + ); } function test_lock_unlock_w_rebase() public { @@ -122,6 +129,9 @@ contract EscrowHappyPath is TestHelpers { uint256 secondVetoerStETHShares = _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount); uint256 secondVetoerWstETHAmount = 17 * 10 ** 18; + uint256 firstVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_1); + uint256 secondVetoerStETHSharesBefore = _ST_ETH.sharesOf(_VETOER_2); + _lockStETH(_VETOER_1, firstVetoerStETHAmount); _lockWstETH(_VETOER_1, firstVetoerWstETHAmount); @@ -136,27 +146,26 @@ contract EscrowHappyPath is TestHelpers { uint256 secondVetoerStETHSharesAfterRebase = _ST_ETH.sharesOf(_VETOER_2); uint256 secondVetoerWstETHBalanceAfterRebase = _WST_ETH.balanceOf(_VETOER_2); - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); - _unlockStETH(_VETOER_1); _unlockWstETH(_VETOER_1); - - _unlockStETH(_VETOER_2); - _unlockWstETH(_VETOER_2); - assertApproxEqAbs( - _ST_ETH.getPooledEthByShares(firstVetoerStETHSharesAfterRebase + firstVetoerStETHShares), - _ST_ETH.balanceOf(_VETOER_1), - 1 + firstVetoerWstETHBalanceBefore + firstVetoerStETHShares, + _WST_ETH.balanceOf(_VETOER_1), + // Even though the wstETH itself doesn't have rounding issues, the Escrow contract wraps stETH into wstETH + // so the the rounding issue may happen because of it. Another rounding may happen on the converting stETH amount + // into shares via _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount) + 2 ); - assertEq(firstVetoerWstETHBalanceAfterRebase + firstVetoerWstETHAmount, _WST_ETH.balanceOf(_VETOER_1)); + + _unlockStETH(_VETOER_2); assertApproxEqAbs( - _ST_ETH.getPooledEthByShares(secondVetoerStETHSharesAfterRebase + secondVetoerStETHShares), + // all locked stETH and wstETH was withdrawn as stETH + _ST_ETH.getPooledEthByShares(secondVetoerStETHSharesBefore + secondVetoerWstETHAmount), _ST_ETH.balanceOf(_VETOER_2), 1 ); - assertEq(secondVetoerWstETHBalanceAfterRebase + secondVetoerWstETHAmount, _WST_ETH.balanceOf(_VETOER_2)); } function test_lock_unlock_w_negative_rebase() public { @@ -165,11 +174,9 @@ contract EscrowHappyPath is TestHelpers { uint256 secondVetoerStETHAmount = 13 * 10 ** 18; uint256 secondVetoerWstETHAmount = 17 * 10 ** 18; + uint256 secondVetoerStETHShares = _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount); uint256 firstVetoerStETHSharesBefore = _ST_ETH.sharesOf(_VETOER_1); - uint256 firstVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_1); - - uint256 secondVetoerStETHSharesBefore = _ST_ETH.sharesOf(_VETOER_2); uint256 secondVetoerWstETHBalanceBefore = _WST_ETH.balanceOf(_VETOER_2); _lockStETH(_VETOER_1, firstVetoerStETHAmount); @@ -180,19 +187,26 @@ contract EscrowHappyPath is TestHelpers { rebase(-100); - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); _unlockStETH(_VETOER_1); - _unlockWstETH(_VETOER_1); + assertApproxEqAbs( + // all locked stETH and wstETH was withdrawn as stETH + _ST_ETH.getPooledEthByShares(firstVetoerStETHSharesBefore + firstVetoerWstETHAmount), + _ST_ETH.balanceOf(_VETOER_1), + 1 + ); - _unlockStETH(_VETOER_2); _unlockWstETH(_VETOER_2); - assertApproxEqAbs(_ST_ETH.getPooledEthByShares(firstVetoerStETHSharesBefore), _ST_ETH.balanceOf(_VETOER_1), 1); - assertEq(firstVetoerWstETHBalanceBefore, _WST_ETH.balanceOf(_VETOER_1)); - - assertApproxEqAbs(_ST_ETH.getPooledEthByShares(secondVetoerStETHSharesBefore), _ST_ETH.balanceOf(_VETOER_2), 1); - assertEq(secondVetoerWstETHBalanceBefore, _WST_ETH.balanceOf(_VETOER_2)); + assertApproxEqAbs( + secondVetoerWstETHBalanceBefore + secondVetoerStETHShares, + _WST_ETH.balanceOf(_VETOER_2), + // Even though the wstETH itself doesn't have rounding issues, the Escrow contract wraps stETH into wstETH + // so the the rounding issue may happen because of it. Another rounding may happen on the converting stETH amount + // into shares via _ST_ETH.getSharesByPooledEth(secondVetoerStETHAmount) + 2 + ); } function test_lock_unlock_withdrawal_nfts() public { @@ -206,7 +220,7 @@ contract EscrowHappyPath is TestHelpers { _lockUnstETH(_VETOER_1, unstETHIds); - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); _unlockUnstETH(_VETOER_1, unstETHIds); } @@ -227,33 +241,39 @@ contract EscrowHappyPath is TestHelpers { } function test_check_finalization() public { - uint256 totalSharesLocked = _ST_ETH.getSharesByPooledEth(2 * 1e18); - uint256 expectedSharesFinalized = _ST_ETH.getSharesByPooledEth(1 * 1e18); + uint256 totalAmountLocked = 2 ether; uint256[] memory amounts = new uint256[](2); for (uint256 i = 0; i < 2; ++i) { - amounts[i] = 1e18; + amounts[i] = 1 ether; } vm.prank(_VETOER_1); uint256[] memory unstETHIds = _WITHDRAWAL_QUEUE.requestWithdrawals(amounts, _VETOER_1); + uint256 totalSharesLocked; + WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); + for (uint256 i = 0; i < unstETHIds.length; ++i) { + totalSharesLocked += statuses[i].amountOfShares; + } + _lockUnstETH(_VETOER_1, unstETHIds); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); - assertEq(escrow.getLockedAssetsTotals().sharesFinalized, 0); + VetoerState memory vetoerState = escrow.getVetoerState(_VETOER_1); + assertEq(vetoerState.unstETHIdsCount, 2); + + LockedAssetsTotals memory totals = escrow.getLockedAssetsTotals(); + assertEq(totals.unstETHFinalizedETH, 0); + assertEq(totals.unstETHUnfinalizedShares, totalSharesLocked); finalizeWQ(unstETHIds[0]); uint256[] memory hints = _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); escrow.markUnstETHFinalized(unstETHIds, hints); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); - - assertApproxEqAbs(escrow.getLockedAssetsTotals().sharesFinalized, expectedSharesFinalized, 1); + totals = escrow.getLockedAssetsTotals(); + assertEq(totals.unstETHUnfinalizedShares, statuses[0].amountOfShares); uint256 ethAmountFinalized = _WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints)[0]; - assertApproxEqAbs(escrow.getLockedAssetsTotals().amountFinalized, ethAmountFinalized, 1); + assertApproxEqAbs(totals.unstETHFinalizedETH, ethAmountFinalized, 1); } function test_get_rage_quit_support() public { @@ -274,10 +294,8 @@ contract EscrowHappyPath is TestHelpers { _lockWstETH(_VETOER_1, sharesToLock); _lockUnstETH(_VETOER_1, unstETHIds); - VetoerState memory vetoerState = escrow.getVetoerState(_VETOER_1); - assertApproxEqAbs(vetoerState.stETHShares, sharesToLock, 1); - assertEq(vetoerState.wstETHShares, sharesToLock); - assertApproxEqAbs(vetoerState.unstETHShares, _ST_ETH.getSharesByPooledEth(2e18), 1); + assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).stETHLockedShares, 2 * sharesToLock, 1); + assertEq(escrow.getVetoerState(_VETOER_1).unstETHIdsCount, 2); uint256 rageQuitSupport = escrow.getRageQuitSupport(); assertEq(rageQuitSupport, 4 * 1e18 * 1e18 / totalSupply); @@ -287,11 +305,9 @@ contract EscrowHappyPath is TestHelpers { _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); escrow.markUnstETHFinalized(unstETHIds, hints); - LockedAssetsTotals memory totals = escrow.getLockedAssetsTotals(); - - assertApproxEqAbs(totals.sharesFinalized, sharesToLock, 1); + assertEq(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, sharesToLock); uint256 ethAmountFinalized = _WITHDRAWAL_QUEUE.getClaimableEther(unstETHIds, hints)[0]; - assertApproxEqAbs(totals.amountFinalized, ethAmountFinalized, 1); + assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHFinalizedETH, ethAmountFinalized, 1); rageQuitSupport = escrow.getRageQuitSupport(); assertEq( @@ -333,7 +349,9 @@ contract EscrowHappyPath is TestHelpers { assertEq(_WITHDRAWAL_QUEUE.balanceOf(address(escrow)), 20); - escrow.requestNextWithdrawalsBatch(200); + while (!escrow.isWithdrawalsBatchesFinalized()) { + escrow.requestNextWithdrawalsBatch(96); + } assertEq(_WITHDRAWAL_QUEUE.balanceOf(address(escrow)), 10 + expectedWithdrawalBatchesCount); assertEq(escrow.isRageQuitFinalized(), false); @@ -341,9 +359,8 @@ contract EscrowHappyPath is TestHelpers { vm.deal(WITHDRAWAL_QUEUE, 1000 * requestAmount); finalizeWQ(); - (uint256 offset, uint256 total, uint256[] memory unstETHIdsToClaim) = - escrow.getNextWithdrawalBatches(expectedWithdrawalBatchesCount); - assertEq(total, expectedWithdrawalBatchesCount); + uint256[] memory unstETHIdsToClaim = escrow.getNextWithdrawalBatch(expectedWithdrawalBatchesCount); + // assertEq(total, expectedWithdrawalBatchesCount); WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIdsToClaim); @@ -355,7 +372,9 @@ contract EscrowHappyPath is TestHelpers { uint256[] memory hints = _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIdsToClaim, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); - escrow.claimNextWithdrawalsBatch(offset, hints); + while (!escrow.isWithdrawalsClaimed()) { + escrow.claimNextWithdrawalsBatch(128); + } assertEq(escrow.isRageQuitFinalized(), false); @@ -370,21 +389,21 @@ contract EscrowHappyPath is TestHelpers { // but it can't be withdrawn before withdrawal timelock has passed vm.expectRevert(); vm.prank(_VETOER_1); - escrow.withdrawUnstETHAsETH(unstETHIds); + escrow.withdrawETH(unstETHIds); } vm.expectRevert(); vm.prank(_VETOER_1); - escrow.withdrawStETHAsETH(); + escrow.withdrawETH(); - _wait(_RAGE_QUIT_EXTRA_TIMELOCK + 1); + _wait(_RAGE_QUIT_EXTRA_TIMELOCK.plusSeconds(1)); assertEq(escrow.isRageQuitFinalized(), true); - _wait(_RAGE_QUIT_WITHDRAWALS_TIMELOCK + 1); + _wait(_RAGE_QUIT_WITHDRAWALS_TIMELOCK.plusSeconds(1)); vm.startPrank(_VETOER_1); - escrow.withdrawStETHAsETH(); - escrow.withdrawUnstETHAsETH(unstETHIds); + escrow.withdrawETH(); + escrow.withdrawETH(unstETHIds); vm.stopPrank(); } @@ -407,6 +426,8 @@ contract EscrowHappyPath is TestHelpers { vm.deal(WITHDRAWAL_QUEUE, 100 * requestAmount); finalizeWQ(); + escrow.requestNextWithdrawalsBatch(96); + escrow.claimNextWithdrawalsBatch(0, new uint256[](0)); assertEq(escrow.isRageQuitFinalized(), false); @@ -418,13 +439,13 @@ contract EscrowHappyPath is TestHelpers { assertEq(escrow.isRageQuitFinalized(), false); - _wait(_RAGE_QUIT_EXTRA_TIMELOCK + 1); + _wait(_RAGE_QUIT_EXTRA_TIMELOCK.plusSeconds(1)); assertEq(escrow.isRageQuitFinalized(), true); - _wait(_RAGE_QUIT_WITHDRAWALS_TIMELOCK + 1); + _wait(_RAGE_QUIT_WITHDRAWALS_TIMELOCK.plusSeconds(1)); vm.startPrank(_VETOER_1); - escrow.withdrawUnstETHAsETH(unstETHIds); + escrow.withdrawETH(unstETHIds); vm.stopPrank(); } @@ -436,32 +457,34 @@ contract EscrowHappyPath is TestHelpers { uint256 totalSharesLocked = firstVetoerWstETHAmount + firstVetoerStETHShares; _lockStETH(_VETOER_1, firstVetoerStETHAmount); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).stETHShares, firstVetoerStETHShares, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, firstVetoerStETHShares, 1); + assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).stETHLockedShares, firstVetoerStETHShares, 1); + assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, firstVetoerStETHShares, 1); _lockWstETH(_VETOER_1, firstVetoerWstETHAmount); - assertEq(escrow.getVetoerState(_VETOER_1).wstETHShares, firstVetoerWstETHAmount); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); + assertApproxEqAbs( + escrow.getVetoerState(_VETOER_1).stETHLockedShares, firstVetoerWstETHAmount + firstVetoerStETHShares, 2 + ); + assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, totalSharesLocked, 2); - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); uint256[] memory stETHWithdrawalRequestAmounts = new uint256[](1); stETHWithdrawalRequestAmounts[0] = firstVetoerStETHAmount; vm.prank(_VETOER_1); - uint256[] memory stETHWithdrawalRequestIds = escrow.requestWithdrawalsStETH(stETHWithdrawalRequestAmounts); + uint256[] memory stETHWithdrawalRequestIds = escrow.requestWithdrawals(stETHWithdrawalRequestAmounts); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, firstVetoerStETHShares, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); + assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, firstVetoerWstETHAmount, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, firstVetoerStETHShares, 2); uint256[] memory wstETHWithdrawalRequestAmounts = new uint256[](1); - wstETHWithdrawalRequestAmounts[0] = firstVetoerWstETHAmount; + wstETHWithdrawalRequestAmounts[0] = _ST_ETH.getPooledEthByShares(firstVetoerWstETHAmount); vm.prank(_VETOER_1); - uint256[] memory wstETHWithdrawalRequestIds = escrow.requestWithdrawalsWstETH(wstETHWithdrawalRequestAmounts); + uint256[] memory wstETHWithdrawalRequestIds = escrow.requestWithdrawals(wstETHWithdrawalRequestAmounts); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); + assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, 0, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, totalSharesLocked, 2); finalizeWQ(wstETHWithdrawalRequestIds[0]); @@ -471,8 +494,9 @@ contract EscrowHappyPath is TestHelpers { stETHWithdrawalRequestIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex() ) ); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); + assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, 0, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, firstVetoerWstETHAmount, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHFinalizedETH, firstVetoerStETHAmount, 2); escrow.markUnstETHFinalized( wstETHWithdrawalRequestIds, @@ -480,16 +504,16 @@ contract EscrowHappyPath is TestHelpers { wstETHWithdrawalRequestIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex() ) ); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, totalSharesLocked, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, totalSharesLocked, 1); + assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, 0, 2); + assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, 0, 2); - _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME() + 1); + _wait(_config.SIGNALLING_ESCROW_MIN_LOCK_TIME().plusSeconds(1)); vm.prank(_VETOER_1); escrow.unlockUnstETH(stETHWithdrawalRequestIds); - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, firstVetoerWstETHAmount, 1); - assertApproxEqAbs(escrow.getLockedAssetsTotals().shares, firstVetoerWstETHAmount, 1); + // // assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).unstETHShares, firstVetoerWstETHAmount, 1); + // assertApproxEqAbs(escrow.getLockedAssetsTotals().stETHLockedShares, firstVetoerWstETHAmount, 1); vm.prank(_VETOER_1); escrow.unlockUnstETH(wstETHWithdrawalRequestIds); diff --git a/test/scenario/gate-seal-breaker.t.sol b/test/scenario/gate-seal-breaker.t.sol deleted file mode 100644 index 4f0dea97..00000000 --- a/test/scenario/gate-seal-breaker.t.sol +++ /dev/null @@ -1,202 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import {percents, ScenarioTestBlueprint} from "../utils/scenario-test-blueprint.sol"; - -import {GateSealMock} from "../mocks/GateSealMock.sol"; -import {GateSealBreaker, IGateSeal} from "contracts/GateSealBreaker.sol"; - -import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; - -contract SealBreakerScenarioTest is ScenarioTestBlueprint { - uint256 private immutable _RELEASE_DELAY = 5 days; - uint256 private immutable _MIN_SEAL_DURATION = 14 days; - - address private immutable _VETOER = makeAddr("VETOER"); - - IGateSeal private _gateSeal; - address[] private _sealables; - GateSealBreaker private _sealBreaker; - - function setUp() external { - _selectFork(); - _deployTarget(); - _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); - - _sealables.push(address(_WITHDRAWAL_QUEUE)); - - _gateSeal = new GateSealMock(_MIN_SEAL_DURATION, _SEALING_COMMITTEE_LIFETIME); - - _sealBreaker = new GateSealBreaker(_RELEASE_DELAY, address(this), address(_dualGovernance)); - - _sealBreaker.registerGateSeal(_gateSeal); - - // grant rights to gate seal to pause/resume the withdrawal queue - vm.startPrank(DAO_AGENT); - _WITHDRAWAL_QUEUE.grantRole(_WITHDRAWAL_QUEUE.PAUSE_ROLE(), address(_gateSeal)); - _WITHDRAWAL_QUEUE.grantRole(_WITHDRAWAL_QUEUE.RESUME_ROLE(), address(_sealBreaker)); - vm.stopPrank(); - } - - function testFork_DualGovernanceLockedThenSeal() external { - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - _assertNormalState(); - - _lockStETH(_VETOER, percents("10.0")); - _assertVetoSignalingState(); - - // sealing committee seals Withdrawal Queue - vm.prank(_SEALING_COMMITTEE); - _gateSeal.seal(_sealables); - - // seal can't be released before the min sealing duration has passed - vm.expectRevert(GateSealBreaker.MinSealDurationNotPassed.selector); - _sealBreaker.startRelease(_gateSeal); - - // validate Withdrawal Queue was paused - assertTrue(_WITHDRAWAL_QUEUE.isPaused()); - - _wait(_MIN_SEAL_DURATION + 1); - - // validate the dual governance still in the veto signaling state - _assertVetoSignalingState(); - - // seal can't be released before the governance returns to Normal state - vm.expectRevert(GateSealBreaker.GovernanceLocked.selector); - _sealBreaker.startRelease(_gateSeal); - - // wait the governance returns to normal state - _wait(14 days); - _activateNextState(); - _assertVetoSignalingDeactivationState(); - - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); - _activateNextState(); - _assertVetoCooldownState(); - - // anyone may start release the seal - _sealBreaker.startRelease(_gateSeal); - - // reverts until timelock - vm.expectRevert(GateSealBreaker.ReleaseDelayNotPassed.selector); - _sealBreaker.enactRelease(_gateSeal); - - // anyone may release the seal after timelock - _wait(_RELEASE_DELAY + 1); - _sealBreaker.enactRelease(_gateSeal); - - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - } - - function testFork_SealThenDualGovernanceLocked() external { - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - _assertNormalState(); - - // sealing committee seals the Withdrawal Queue - vm.prank(_SEALING_COMMITTEE); - _gateSeal.seal(_sealables); - - // validate Withdrawal Queue was paused - assertTrue(_WITHDRAWAL_QUEUE.isPaused()); - - // wait some time, before dual governance enters veto signaling state - _wait(_MIN_SEAL_DURATION / 2); - - _lockStETH(_VETOER, percents("10.0")); - _assertVetoSignalingState(); - - // seal can't be released before the min sealing duration has passed - vm.expectRevert(GateSealBreaker.MinSealDurationNotPassed.selector); - _sealBreaker.startRelease(_gateSeal); - - _wait(_MIN_SEAL_DURATION / 2 + 1); - - // seal can't be released before the governance returns to Normal state - vm.expectRevert(GateSealBreaker.GovernanceLocked.selector); - _sealBreaker.startRelease(_gateSeal); - - // wait the governance returns to normal state - _wait(14 days); - _activateNextState(); - _assertVetoSignalingDeactivationState(); - - _wait(_dualGovernance.CONFIG().VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); - _activateNextState(); - _assertVetoCooldownState(); - - // the stETH whale takes his funds back from Escrow - _unlockStETH(_VETOER); - - _wait(_dualGovernance.CONFIG().VETO_COOLDOWN_DURATION() + 1); - _activateNextState(); - _assertNormalState(); - - // now seal may be released - _sealBreaker.startRelease(_gateSeal); - - // reverts until timelock - vm.expectRevert(GateSealBreaker.ReleaseDelayNotPassed.selector); - _sealBreaker.enactRelease(_gateSeal); - - // anyone may release the seal after timelock - _wait(_RELEASE_DELAY + 1); - _sealBreaker.enactRelease(_gateSeal); - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - } - - function testFork_SealWhenDualGovernanceNotLocked() external { - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - _assertNormalState(); - - // sealing committee seals the Withdrawal Queue - vm.prank(_SEALING_COMMITTEE); - _gateSeal.seal(_sealables); - - // validate Withdrawal Queue was paused - assertTrue(_WITHDRAWAL_QUEUE.isPaused()); - - // seal can't be released before the min sealing duration has passed - vm.expectRevert(GateSealBreaker.MinSealDurationNotPassed.selector); - _sealBreaker.startRelease(_gateSeal); - - vm.warp(block.timestamp + _MIN_SEAL_DURATION + 1); - - // now seal may be released - _sealBreaker.startRelease(_gateSeal); - - // reverts until timelock - vm.expectRevert(GateSealBreaker.ReleaseDelayNotPassed.selector); - _sealBreaker.enactRelease(_gateSeal); - - // anyone may release the seal after timelock - _wait(_RELEASE_DELAY + 1); - _sealBreaker.enactRelease(_gateSeal); - - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - } - - function testFork_GateSealMayBeReleasedOnlyOnce() external { - assertFalse(_WITHDRAWAL_QUEUE.isPaused()); - _assertNormalState(); - - // sealing committee seals the Withdrawal Queue - vm.prank(_SEALING_COMMITTEE); - _gateSeal.seal(_sealables); - - // validate Withdrawal Queue was paused - assertTrue(_WITHDRAWAL_QUEUE.isPaused()); - - // seal can't be released before the min sealing duration has passed - vm.expectRevert(GateSealBreaker.MinSealDurationNotPassed.selector); - _sealBreaker.startRelease(_gateSeal); - - _wait(_MIN_SEAL_DURATION + 1); - - // now seal may be released - _sealBreaker.startRelease(_gateSeal); - - // An attempt to release same gate seal the second time fails - vm.expectRevert(GateSealBreaker.GateSealAlreadyReleased.selector); - _sealBreaker.startRelease(_gateSeal); - } -} diff --git a/test/scenario/gov-state-transitions.t.sol b/test/scenario/gov-state-transitions.t.sol index 90b35b4e..efb12696 100644 --- a/test/scenario/gov-state-transitions.t.sol +++ b/test/scenario/gov-state-transitions.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {ScenarioTestBlueprint, percents} from "../utils/scenario-test-blueprint.sol"; +import {ScenarioTestBlueprint, percents, Durations} from "../utils/scenario-test-blueprint.sol"; contract GovernanceStateTransitions is ScenarioTestBlueprint { address internal immutable _VETOER = makeAddr("VETOER"); @@ -21,12 +21,12 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() / 2); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().dividedBy(2)); _activateNextState(); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() / 2 + 1); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().dividedBy(2).plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); @@ -39,19 +39,19 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() / 2); + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().dividedBy(2)); _activateNextState(); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() / 2); + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().dividedBy(2)); _activateNextState(); _assertVetoSignalingState(); _lockStETH(_VETOER, 1 gwei); - _wait(1 seconds); + _wait(Durations.from(1 seconds)); _activateNextState(); _assertRageQuitState(); @@ -67,12 +67,12 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() + 1); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); @@ -81,7 +81,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _getVetoSignallingEscrow().unlockStETH(); vm.stopPrank(); - _wait(_config.VETO_COOLDOWN_DURATION() + 1); + _wait(_config.VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertNormalState(); @@ -96,17 +96,17 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() + 1); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); - _wait(_config.VETO_COOLDOWN_DURATION() + 1); + _wait(_config.VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingState(); @@ -126,7 +126,7 @@ contract GovernanceStateTransitions is ScenarioTestBlueprint { _lockStETH(_VETOER, 1 gwei); _assertVetoSignalingState(); - _wait(1 seconds); + _wait(Durations.from(1 seconds)); _activateNextState(); _assertRageQuitState(); } diff --git a/test/scenario/happy-path-plan-b.t.sol b/test/scenario/happy-path-plan-b.t.sol index 33f7fef9..ac52bc20 100644 --- a/test/scenario/happy-path-plan-b.t.sol +++ b/test/scenario/happy-path-plan-b.t.sol @@ -8,7 +8,10 @@ import { ScenarioTestBlueprint, ExecutorCall, ExecutorCallHelpers, - DualGovernance + DualGovernance, + Timestamp, + Timestamps, + Durations } from "../utils/scenario-test-blueprint.sol"; import {Proposals} from "contracts/libraries/Proposals.sol"; @@ -66,24 +69,24 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertCanSchedule(_singleGovernance, maliciousProposalId, false); // some time required to assemble the emergency committee and activate emergency mode - _wait(_config.AFTER_SUBMIT_DELAY() / 2); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); // malicious call still can't be scheduled _assertCanSchedule(_singleGovernance, maliciousProposalId, false); // emergency committee activates emergency mode - vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); + vm.prank(address(_emergencyActivationCommittee)); _timelock.activateEmergencyMode(); // emergency mode was successfully activated - uint256 expectedEmergencyModeEndTimestamp = block.timestamp + _EMERGENCY_MODE_DURATION; + Timestamp expectedEmergencyModeEndTimestamp = _EMERGENCY_MODE_DURATION.addTo(Timestamps.now()); emergencyState = _timelock.getEmergencyState(); assertTrue(emergencyState.isEmergencyModeActivated); assertEq(emergencyState.emergencyModeEndsAfter, expectedEmergencyModeEndTimestamp); // after the submit delay has passed, the call still may be scheduled, but executed // only the emergency committee - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2 + 1); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2).plusSeconds(1)); _assertCanSchedule(_singleGovernance, maliciousProposalId, true); _scheduleProposal(_singleGovernance, maliciousProposalId); @@ -105,7 +108,7 @@ contract PlanBSetup is ScenarioTestBlueprint { { // Lido contributors work hard to implement and ship the Dual Governance mechanism // before the emergency mode is over - vm.warp(block.timestamp + _EMERGENCY_PROTECTION_DURATION / 2); + _wait(_EMERGENCY_PROTECTION_DURATION.dividedBy(2)); // Time passes but malicious proposal still on hold _assertCanExecute(maliciousProposalId, false); @@ -124,10 +127,10 @@ contract PlanBSetup is ScenarioTestBlueprint { abi.encodeCall( _timelock.setEmergencyProtection, ( - _EMERGENCY_ACTIVATION_COMMITTEE, - _EMERGENCY_EXECUTION_COMMITTEE, + address(_emergencyActivationCommittee), + address(_emergencyExecutionCommittee), _EMERGENCY_PROTECTION_DURATION, - 30 days + Durations.from(30 days) ) ) ] @@ -147,8 +150,7 @@ contract PlanBSetup is ScenarioTestBlueprint { _waitAfterScheduleDelayPassed(); // now emergency committee may execute the proposal - vm.prank(_EMERGENCY_EXECUTION_COMMITTEE); - _timelock.emergencyExecute(dualGovernanceLunchProposalId); + _executeEmergencyExecute(dualGovernanceLunchProposalId); assertEq(_timelock.getGovernance(), address(_dualGovernance)); // TODO: check emergency protection also was applied @@ -161,7 +163,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // ACT 4. 🫡 EMERGENCY COMMITTEE LIFETIME IS ENDED // --- { - vm.warp(block.timestamp + _EMERGENCY_PROTECTION_DURATION + 1); + _wait(_EMERGENCY_PROTECTION_DURATION.plusSeconds(1)); assertFalse(_timelock.isEmergencyProtectionEnabled()); uint256 proposalId = _submitProposal( @@ -189,7 +191,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // --- { // some time later, the major Dual Governance update release is ready to be launched - vm.warp(block.timestamp + 365 days); + _wait(Durations.from(365 days)); DualGovernance dualGovernanceV2 = new DualGovernance(address(_config), address(_timelock), address(_escrowMasterCopy), _ADMIN_PROPOSER); @@ -202,10 +204,10 @@ contract PlanBSetup is ScenarioTestBlueprint { abi.encodeCall( _timelock.setEmergencyProtection, ( - _EMERGENCY_ACTIVATION_COMMITTEE, - _EMERGENCY_EXECUTION_COMMITTEE, + address(_emergencyActivationCommittee), + address(_emergencyExecutionCommittee), _EMERGENCY_PROTECTION_DURATION, - 30 days + Durations.from(30 days) ) ) ] @@ -232,11 +234,11 @@ contract PlanBSetup is ScenarioTestBlueprint { assertTrue(_timelock.isEmergencyProtectionEnabled()); emergencyState = _timelock.getEmergencyState(); - assertEq(emergencyState.activationCommittee, _EMERGENCY_ACTIVATION_COMMITTEE); - assertEq(emergencyState.executionCommittee, _EMERGENCY_EXECUTION_COMMITTEE); + assertEq(emergencyState.activationCommittee, address(_emergencyActivationCommittee)); + assertEq(emergencyState.executionCommittee, address(_emergencyExecutionCommittee)); assertFalse(emergencyState.isEmergencyModeActivated); - assertEq(emergencyState.emergencyModeDuration, 30 days); - assertEq(emergencyState.emergencyModeEndsAfter, 0); + assertEq(emergencyState.emergencyModeDuration, Durations.from(30 days)); + assertEq(emergencyState.emergencyModeEndsAfter, Timestamps.ZERO); // use the new version of the dual governance in the future calls _dualGovernance = dualGovernanceV2; @@ -287,9 +289,9 @@ contract PlanBSetup is ScenarioTestBlueprint { // activate emergency mode EmergencyState memory emergencyState; { - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); - vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); + vm.prank(address(_emergencyActivationCommittee)); _timelock.activateEmergencyMode(); emergencyState = _timelock.getEmergencyState(); @@ -299,12 +301,13 @@ contract PlanBSetup is ScenarioTestBlueprint { // delay for malicious proposal has passed, but it can't be executed because of emergency mode was activated { // the after submit delay has passed, and proposal can be scheduled, but not executed - _wait(_config.AFTER_SUBMIT_DELAY() + 1); + _wait(_config.AFTER_SCHEDULE_DELAY() + Durations.from(1 seconds)); + _wait(_config.AFTER_SUBMIT_DELAY().plusSeconds(1)); _assertCanSchedule(_singleGovernance, maliciousProposalId, true); _scheduleProposal(_singleGovernance, maliciousProposalId); - _wait(_config.AFTER_SCHEDULE_DELAY() + 1); + _wait(_config.AFTER_SCHEDULE_DELAY().plusSeconds(1)); _assertCanExecute(maliciousProposalId, false); vm.expectRevert( @@ -316,10 +319,10 @@ contract PlanBSetup is ScenarioTestBlueprint { // another malicious call is scheduled during the emergency mode also can't be executed uint256 anotherMaliciousProposalId; { - vm.warp(block.timestamp + _EMERGENCY_MODE_DURATION / 2); + _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); // emergency mode still active - assertTrue(emergencyState.emergencyModeEndsAfter > block.timestamp); + assertTrue(emergencyState.emergencyModeEndsAfter > Timestamps.now()); anotherMaliciousProposalId = _submitProposal(_singleGovernance, "Another Rug Pool attempt", maliciousCalls); @@ -327,10 +330,10 @@ contract PlanBSetup is ScenarioTestBlueprint { _assertCanExecute(anotherMaliciousProposalId, false); // the after submit delay has passed, and proposal can not be executed - _wait(_config.AFTER_SUBMIT_DELAY() + 1); + _wait(_config.AFTER_SUBMIT_DELAY().plusSeconds(1)); _assertCanSchedule(_singleGovernance, anotherMaliciousProposalId, true); - _wait(_config.AFTER_SCHEDULE_DELAY() + 1); + _wait(_config.AFTER_SCHEDULE_DELAY().plusSeconds(1)); _assertCanExecute(anotherMaliciousProposalId, false); vm.expectRevert( @@ -341,8 +344,8 @@ contract PlanBSetup is ScenarioTestBlueprint { // emergency mode is over but proposals can't be executed until the emergency mode turned off manually { - vm.warp(block.timestamp + _EMERGENCY_MODE_DURATION / 2); - assertTrue(emergencyState.emergencyModeEndsAfter < block.timestamp); + _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); + assertTrue(emergencyState.emergencyModeEndsAfter < Timestamps.now()); vm.expectRevert( abi.encodeWithSelector(EmergencyProtection.InvalidEmergencyModeActiveValue.selector, true, false) @@ -387,7 +390,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // emergency committee activates emergency mode EmergencyState memory emergencyState; { - vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); + vm.prank(address(_emergencyActivationCommittee)); _timelock.activateEmergencyMode(); emergencyState = _timelock.getEmergencyState(); @@ -397,19 +400,18 @@ contract PlanBSetup is ScenarioTestBlueprint { // before the end of the emergency mode emergency committee can reset the controller to // disable dual governance { - vm.warp(block.timestamp + _EMERGENCY_MODE_DURATION / 2); - assertTrue(emergencyState.emergencyModeEndsAfter > block.timestamp); + _wait(_EMERGENCY_MODE_DURATION.dividedBy(2)); + assertTrue(emergencyState.emergencyModeEndsAfter > Timestamps.now()); - vm.prank(_EMERGENCY_EXECUTION_COMMITTEE); - _timelock.emergencyReset(); + _executeEmergencyReset(); assertEq(_timelock.getGovernance(), _config.EMERGENCY_GOVERNANCE()); emergencyState = _timelock.getEmergencyState(); assertEq(emergencyState.activationCommittee, address(0)); assertEq(emergencyState.executionCommittee, address(0)); - assertEq(emergencyState.emergencyModeDuration, 0); - assertEq(emergencyState.emergencyModeEndsAfter, 0); + assertEq(emergencyState.emergencyModeDuration, Durations.ZERO); + assertEq(emergencyState.emergencyModeEndsAfter, Timestamps.ZERO); assertFalse(emergencyState.isEmergencyModeActivated); } } @@ -423,7 +425,7 @@ contract PlanBSetup is ScenarioTestBlueprint { // wait till the protection duration passes { - vm.warp(block.timestamp + _EMERGENCY_PROTECTION_DURATION + 1); + _wait(_EMERGENCY_PROTECTION_DURATION.plusSeconds(1)); } EmergencyState memory emergencyState = _timelock.getEmergencyState(); @@ -437,7 +439,7 @@ contract PlanBSetup is ScenarioTestBlueprint { emergencyState.protectedTill ) ); - vm.prank(_EMERGENCY_ACTIVATION_COMMITTEE); + vm.prank(address(_emergencyActivationCommittee)); _timelock.activateEmergencyMode(); } } diff --git a/test/scenario/happy-path.t.sol b/test/scenario/happy-path.t.sol index 3cde7bd0..fd55104b 100644 --- a/test/scenario/happy-path.t.sol +++ b/test/scenario/happy-path.t.sol @@ -29,14 +29,14 @@ contract HappyPathTest is ScenarioTestBlueprint { _assertProposalSubmitted(proposalId); _assertSubmittedProposalData(proposalId, regularStaffCalls); - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); // the min execution delay hasn't elapsed yet vm.expectRevert(abi.encodeWithSelector(Proposals.AfterSubmitDelayNotPassed.selector, (proposalId))); _scheduleProposal(_dualGovernance, proposalId); // wait till the first phase of timelock passes - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2 + 1); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2).plusSeconds(1)); _assertCanSchedule(_dualGovernance, proposalId, true); _scheduleProposal(_dualGovernance, proposalId); @@ -67,7 +67,7 @@ contract HappyPathTest is ScenarioTestBlueprint { uint256 proposalId = _submitProposal(_dualGovernance, "Multiple items", multipleCalls); - _wait(_config.AFTER_SUBMIT_DELAY() / 2); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); // proposal can't be scheduled before the after submit delay has passed _assertCanSchedule(_dualGovernance, proposalId, false); @@ -77,7 +77,7 @@ contract HappyPathTest is ScenarioTestBlueprint { _scheduleProposal(_dualGovernance, proposalId); // wait till the DG-enforced timelock elapses - _wait(_config.AFTER_SUBMIT_DELAY() / 2 + 1); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2).plusSeconds(1)); _assertCanSchedule(_dualGovernance, proposalId, true); _scheduleProposal(_dualGovernance, proposalId); diff --git a/test/scenario/last-moment-malicious-proposal.t.sol b/test/scenario/last-moment-malicious-proposal.t.sol index d40e4df3..99bf4e47 100644 --- a/test/scenario/last-moment-malicious-proposal.t.sol +++ b/test/scenario/last-moment-malicious-proposal.t.sol @@ -6,7 +6,8 @@ import { ScenarioTestBlueprint, ExecutorCall, ExecutorCallHelpers, - DualGovernanceState + DualGovernanceState, + Durations } from "../utils/scenario-test-blueprint.sol"; interface IDangerousContract { @@ -44,7 +45,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _logVetoSignallingState(); // almost all veto signalling period has passed - _wait(20 days); + _wait(Durations.from(20 days)); _activateNextState(); _assertVetoSignalingState(); _logVetoSignallingState(); @@ -68,7 +69,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("4. MALICIOUS ACTOR UNLOCK FUNDS FROM ESCROW"); { - _wait(12 seconds); + _wait(Durations.from(12 seconds)); _unlockStETH(maliciousActor); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); @@ -77,12 +78,12 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { address stEthHolders = makeAddr("STETH_WHALE"); _step("5. STETH HOLDERS ACQUIRING QUORUM TO VETO MALICIOUS PROPOSAL"); { - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() / 2); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().dividedBy(2)); _lockStETH(stEthHolders, percents(_config.FIRST_SEAL_RAGE_QUIT_SUPPORT() + 1)); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() / 2 + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().dividedBy(2).plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); } @@ -104,7 +105,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("7. NEW VETO SIGNALLING ROUND FOR MALICIOUS PROPOSAL IS STARTED"); { - _wait(_config.VETO_COOLDOWN_DURATION() + 1); + _wait(_config.VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingState(); _logVetoSignallingState(); @@ -114,7 +115,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _assertVetoSignalingState(); _logVetoSignallingState(); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() + 1); + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); _logVetoSignallingState(); _activateNextState(); _assertRageQuitState(); @@ -145,17 +146,17 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { // --- address maliciousActor = makeAddr("MALICIOUS_ACTOR"); { - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2)); _lockStETH(maliciousActor, percents("12.0")); _assertVetoSignalingState(); - vm.warp(block.timestamp + _config.AFTER_SUBMIT_DELAY() / 2 + 1); + _wait(_config.AFTER_SUBMIT_DELAY().dividedBy(2).plusSeconds(1)); _assertProposalSubmitted(proposalId); (, uint256 currentVetoSignallingDuration,,) = _getVetoSignallingState(); - vm.warp(block.timestamp + currentVetoSignallingDuration + 1); + _wait(Durations.from(currentVetoSignallingDuration + 1)); _activateNextState(); _assertVetoSignalingDeactivationState(); @@ -165,7 +166,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { // ACT 3. THE VETO SIGNALLING DEACTIVATION DURATION EQUALS TO "VETO_SIGNALLING_DEACTIVATION_MAX_DURATION" DAYS // --- { - vm.warp(block.timestamp + _config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); @@ -206,12 +207,12 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("3. THE VETO SIGNALLING & DEACTIVATION PASSED BUT PROPOSAL STILL NOT EXECUTABLE"); { - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() + 1); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); @@ -222,7 +223,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("4. AFTER THE VETO COOLDOWN GOVERNANCE TRANSITIONS INTO NORMAL STATE"); { _unlockStETH(maliciousActor); - _wait(_config.VETO_COOLDOWN_DURATION() + 1); + _wait(_config.VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertNormalState(); } @@ -260,12 +261,12 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("3. THE VETO SIGNALLING & DEACTIVATION PASSED BUT PROPOSAL STILL NOT EXECUTABLE"); { - _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION() + 1); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); @@ -275,7 +276,7 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("4. AFTER THE VETO COOLDOWN NEW VETO SIGNALLING ROUND STARTED"); { - _wait(_config.VETO_COOLDOWN_DURATION() + 1); + _wait(_config.VETO_COOLDOWN_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoSignalingState(); _logVetoSignallingState(); @@ -286,12 +287,12 @@ contract LastMomentMaliciousProposalSuccessor is ScenarioTestBlueprint { _step("5. PROPOSAL EXECUTABLE IN THE NEXT VETO COOLDOWN"); { - _wait(2 * _config.DYNAMIC_TIMELOCK_MIN_DURATION()); + _wait(_config.DYNAMIC_TIMELOCK_MIN_DURATION().multipliedBy(2)); _activateNextState(); _assertVetoSignalingDeactivationState(); _logVetoSignallingDeactivationState(); - _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION() + 1); + _wait(_config.VETO_SIGNALLING_DEACTIVATION_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertVetoCooldownState(); diff --git a/test/scenario/tiebraker.t.sol b/test/scenario/tiebraker.t.sol new file mode 100644 index 00000000..4af73b90 --- /dev/null +++ b/test/scenario/tiebraker.t.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import { + ScenarioTestBlueprint, percents, ExecutorCall, ExecutorCallHelpers +} from "../utils/scenario-test-blueprint.sol"; + +import {EmergencyProtectedTimelock} from "contracts/EmergencyProtectedTimelock.sol"; + +import {DAO_AGENT} from "../utils/mainnet-addresses.sol"; + +contract TiebreakerScenarioTest is ScenarioTestBlueprint { + address internal immutable _VETOER = makeAddr("VETOER"); + uint256 public constant PAUSE_INFINITELY = type(uint256).max; + + function setUp() external { + _selectFork(); + _deployDualGovernanceSetup( /* isEmergencyProtectionEnabled */ false); + _depositStETH(_VETOER, 1 ether); + } + + function test_proposal_approval() external { + uint256 quorum; + uint256 support; + bool isExecuted; + + address[] memory members; + + // Tiebreak activation + _assertNormalState(); + _lockStETH(_VETOER, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, 1 gwei); + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _activateNextState(); + _assertRageQuitState(); + _wait(_config.TIE_BREAK_ACTIVATION_TIMEOUT()); + _activateNextState(); + + ExecutorCall[] memory proposalCalls = ExecutorCallHelpers.create(address(0), new bytes(0)); + uint256 proposalIdToExecute = _submitProposal(_dualGovernance, "Proposal for execution", proposalCalls); + + // Tiebreaker subcommittee 0 + members = _tiebreakerSubCommittees[0].getMembers(); + for (uint256 i = 0; i < _tiebreakerSubCommittees[0].quorum() - 1; i++) { + vm.prank(members[i]); + _tiebreakerSubCommittees[0].scheduleProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getScheduleProposalState(proposalIdToExecute); + assert(support < quorum); + assert(isExecuted == false); + } + + vm.prank(members[members.length - 1]); + _tiebreakerSubCommittees[0].scheduleProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getScheduleProposalState(proposalIdToExecute); + assert(support == quorum); + assert(isExecuted == false); + + _tiebreakerSubCommittees[0].executeScheduleProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _tiebreakerCommittee.getScheduleProposalState(proposalIdToExecute); + assert(support < quorum); + + // Tiebreaker subcommittee 1 + members = _tiebreakerSubCommittees[1].getMembers(); + for (uint256 i = 0; i < _tiebreakerSubCommittees[1].quorum() - 1; i++) { + vm.prank(members[i]); + _tiebreakerSubCommittees[1].scheduleProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getScheduleProposalState(proposalIdToExecute); + assert(support < quorum); + assert(isExecuted == false); + } + + vm.prank(members[members.length - 1]); + _tiebreakerSubCommittees[1].scheduleProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getScheduleProposalState(proposalIdToExecute); + assert(support == quorum); + assert(isExecuted == false); + + // Approve proposal for scheduling + _tiebreakerSubCommittees[1].executeScheduleProposal(proposalIdToExecute); + (support, quorum, isExecuted) = _tiebreakerCommittee.getScheduleProposalState(proposalIdToExecute); + assert(support == quorum); + + // Waiting for submit delay pass + _wait(_config.AFTER_SUBMIT_DELAY()); + + _tiebreakerCommittee.executeScheduleProposal(proposalIdToExecute); + } + + function test_resume_withdrawals() external { + uint256 quorum; + uint256 support; + bool isExecuted; + + address[] memory members; + + vm.prank(DAO_AGENT); + _WITHDRAWAL_QUEUE.grantRole( + 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(DAO_AGENT) + ); + vm.prank(DAO_AGENT); + _WITHDRAWAL_QUEUE.pauseFor(type(uint256).max); + assertEq(_WITHDRAWAL_QUEUE.isPaused(), true); + + // Tiebreak activation + _assertNormalState(); + _lockStETH(_VETOER, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT())); + _lockStETH(_VETOER, 1 gwei); + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); + _activateNextState(); + _assertRageQuitState(); + _wait(_config.TIE_BREAK_ACTIVATION_TIMEOUT()); + _activateNextState(); + + // Tiebreaker subcommittee 0 + members = _tiebreakerSubCommittees[0].getMembers(); + for (uint256 i = 0; i < _tiebreakerSubCommittees[0].quorum() - 1; i++) { + vm.prank(members[i]); + _tiebreakerSubCommittees[0].sealableResume(address(_WITHDRAWAL_QUEUE)); + (support, quorum, isExecuted) = + _tiebreakerSubCommittees[0].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); + assert(support < quorum); + assert(isExecuted == false); + } + + vm.prank(members[members.length - 1]); + _tiebreakerSubCommittees[0].sealableResume(address(_WITHDRAWAL_QUEUE)); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[0].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); + assert(support == quorum); + assert(isExecuted == false); + + _tiebreakerSubCommittees[0].executeSealableResume(address(_WITHDRAWAL_QUEUE)); + (support, quorum, isExecuted) = _tiebreakerCommittee.getSealableResumeState( + address(_WITHDRAWAL_QUEUE), _tiebreakerCommittee.getSealableResumeNonce(address(_WITHDRAWAL_QUEUE)) + ); + assert(support < quorum); + + // Tiebreaker subcommittee 1 + members = _tiebreakerSubCommittees[1].getMembers(); + for (uint256 i = 0; i < _tiebreakerSubCommittees[1].quorum() - 1; i++) { + vm.prank(members[i]); + _tiebreakerSubCommittees[1].sealableResume(address(_WITHDRAWAL_QUEUE)); + (support, quorum, isExecuted) = + _tiebreakerSubCommittees[1].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); + assert(support < quorum); + assert(isExecuted == false); + } + + vm.prank(members[members.length - 1]); + _tiebreakerSubCommittees[1].sealableResume(address(_WITHDRAWAL_QUEUE)); + (support, quorum, isExecuted) = _tiebreakerSubCommittees[1].getSealableResumeState(address(_WITHDRAWAL_QUEUE)); + assert(support == quorum); + assert(isExecuted == false); + + _tiebreakerSubCommittees[1].executeSealableResume(address(_WITHDRAWAL_QUEUE)); + (support, quorum, isExecuted) = _tiebreakerCommittee.getSealableResumeState( + address(_WITHDRAWAL_QUEUE), _tiebreakerCommittee.getSealableResumeNonce(address(_WITHDRAWAL_QUEUE)) + ); + assert(support == quorum); + + _tiebreakerCommittee.executeSealableResume(address(_WITHDRAWAL_QUEUE)); + + assertEq(_WITHDRAWAL_QUEUE.isPaused(), false); + } +} diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index 38e75c0e..e7ae5a69 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -44,7 +44,7 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { vetoedStETHAmount = _lockStETH(vetoer, percents(_config.SECOND_SEAL_RAGE_QUIT_SUPPORT() + 1)); _assertVetoSignalingState(); - _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION() + 1); + _wait(_config.DYNAMIC_TIMELOCK_MAX_DURATION().plusSeconds(1)); _activateNextState(); _assertRageQuitState(); } @@ -68,26 +68,18 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { uint256 requestAmount = _WITHDRAWAL_QUEUE.MAX_STETH_WITHDRAWAL_AMOUNT(); uint256 maxRequestsCount = vetoedStETHAmount / requestAmount + 1; - rageQuitEscrow.requestNextWithdrawalsBatch(maxRequestsCount); + while (!rageQuitEscrow.isWithdrawalsBatchesFinalized()) { + rageQuitEscrow.requestNextWithdrawalsBatch(96); + } - vm.deal(address(_WITHDRAWAL_QUEUE), vetoedStETHAmount); + vm.deal(address(_WITHDRAWAL_QUEUE), 2 * vetoedStETHAmount); _finalizeWQ(); - uint256 batchSizeLimit = 200; - - while (true) { - (uint256 offset, uint256 total, uint256[] memory unstETHIds) = - rageQuitEscrow.getNextWithdrawalBatches(batchSizeLimit); - if (offset == total) { - break; - } - uint256[] memory hints = - _WITHDRAWAL_QUEUE.findCheckpointHints(unstETHIds, 1, _WITHDRAWAL_QUEUE.getLastCheckpointIndex()); - - rageQuitEscrow.claimNextWithdrawalsBatch(offset, hints); + while (!rageQuitEscrow.isWithdrawalsClaimed()) { + rageQuitEscrow.claimNextWithdrawalsBatch(128); } - _wait(_config.RAGE_QUIT_EXTENSION_DELAY() + 1); + _wait(_config.RAGE_QUIT_EXTENSION_DELAY().plusSeconds(1)); assertTrue(rageQuitEscrow.isRageQuitFinalized()); } diff --git a/test/unit/EmergencyProtectedTimelock.t.sol b/test/unit/EmergencyProtectedTimelock.t.sol index 7216c248..094c2781 100644 --- a/test/unit/EmergencyProtectedTimelock.t.sol +++ b/test/unit/EmergencyProtectedTimelock.t.sol @@ -12,7 +12,7 @@ import {Executor} from "contracts/Executor.sol"; import {Proposal, Proposals, ExecutorCall, Status} from "contracts/libraries/Proposals.sol"; import {EmergencyProtection, EmergencyState} from "contracts/libraries/EmergencyProtection.sol"; -import {UnitTest} from "test/utils/unit-test.sol"; +import {UnitTest, Duration, Timestamp, Timestamps, Durations, console} from "test/utils/unit-test.sol"; import {TargetMock} from "test/utils/utils.sol"; import {ExecutorCallHelpers} from "test/utils/executor-calls.sol"; import {IDangerousContract} from "test/utils/interfaces.sol"; @@ -25,8 +25,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { address private _emergencyActivator = makeAddr("EMERGENCY_ACTIVATION_COMMITTEE"); address private _emergencyEnactor = makeAddr("EMERGENCY_EXECUTION_COMMITTEE"); - uint256 private _emergencyModeDuration = 180 days; - uint256 private _emergencyProtectionDuration = 90 days; + Duration private _emergencyModeDuration = Durations.from(180 days); + Duration private _emergencyProtectionDuration = Durations.from(90 days); address private _emergencyGovernance = makeAddr("EMERGENCY_GOVERNANCE"); address private _dualGovernance = makeAddr("DUAL_GOVERNANCE"); @@ -395,7 +395,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { EmergencyState memory state = _timelock.getEmergencyState(); assertEq(_isEmergencyStateActivated(), true); - _wait(state.emergencyModeDuration + 1); + _wait(state.emergencyModeDuration.plusSeconds(1)); vm.prank(stranger); _timelock.deactivateEmergencyMode(); @@ -449,9 +449,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(newState.activationCommittee, address(0)); assertEq(newState.executionCommittee, address(0)); - assertEq(newState.protectedTill, 0); - assertEq(newState.emergencyModeDuration, 0); - assertEq(newState.emergencyModeEndsAfter, 0); + assertEq(newState.protectedTill, Timestamps.ZERO); + assertEq(newState.emergencyModeDuration, Durations.ZERO); + assertEq(newState.emergencyModeEndsAfter, Timestamps.ZERO); } function test_after_emergency_reset_all_proposals_are_cancelled() external { @@ -518,9 +518,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(state.activationCommittee, _emergencyActivator); assertEq(state.executionCommittee, _emergencyEnactor); - assertEq(state.protectedTill, block.timestamp + _emergencyProtectionDuration); + assertEq(state.protectedTill, _emergencyProtectionDuration.addTo(Timestamps.now())); assertEq(state.emergencyModeDuration, _emergencyModeDuration); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); assertEq(state.isEmergencyModeActivated, false); } @@ -540,9 +540,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(state.activationCommittee, address(0)); assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, 0); - assertEq(state.emergencyModeDuration, 0); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.protectedTill, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); assertEq(state.isEmergencyModeActivated, false); } @@ -604,9 +604,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(state.isEmergencyModeActivated, false); assertEq(state.activationCommittee, address(0)); assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, 0); - assertEq(state.emergencyModeDuration, 0); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.protectedTill, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); vm.prank(_adminExecutor); _localTimelock.setEmergencyProtection( @@ -618,9 +618,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_localTimelock.getEmergencyState().isEmergencyModeActivated, false); assertEq(state.activationCommittee, _emergencyActivator); assertEq(state.executionCommittee, _emergencyEnactor); - assertEq(state.protectedTill, block.timestamp + _emergencyProtectionDuration); + assertEq(state.protectedTill, _emergencyProtectionDuration.addTo(Timestamps.now())); assertEq(state.emergencyModeDuration, _emergencyModeDuration); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); vm.prank(_emergencyActivator); _localTimelock.activateEmergencyMode(); @@ -628,11 +628,11 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { state = _localTimelock.getEmergencyState(); assertEq(_localTimelock.getEmergencyState().isEmergencyModeActivated, true); - assertEq(state.activationCommittee, _emergencyActivator); assertEq(state.executionCommittee, _emergencyEnactor); - assertEq(state.protectedTill, block.timestamp + _emergencyProtectionDuration); + assertEq(state.activationCommittee, _emergencyActivator); assertEq(state.emergencyModeDuration, _emergencyModeDuration); - assertEq(state.emergencyModeEndsAfter, block.timestamp + _emergencyModeDuration); + assertEq(state.protectedTill, _emergencyProtectionDuration.addTo(Timestamps.now())); + assertEq(state.emergencyModeEndsAfter, _emergencyModeDuration.addTo(Timestamps.now())); vm.prank(_adminExecutor); _localTimelock.deactivateEmergencyMode(); @@ -642,9 +642,9 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(state.isEmergencyModeActivated, false); assertEq(state.activationCommittee, address(0)); assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, 0); - assertEq(state.emergencyModeDuration, 0); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.protectedTill, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); } function test_get_emergency_state_reset() external { @@ -666,15 +666,15 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(state.isEmergencyModeActivated, false); assertEq(state.activationCommittee, address(0)); assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, 0); - assertEq(state.emergencyModeDuration, 0); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.protectedTill, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); } // EmergencyProtectedTimelock.getGovernance() function testFuzz_get_governance(address governance) external { - vm.assume(governance != address(0)); + vm.assume(governance != address(0) && governance != _timelock.getGovernance()); vm.prank(_adminExecutor); _timelock.setGovernance(governance); assertEq(_timelock.getGovernance(), governance); @@ -692,13 +692,13 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { Proposal memory submittedProposal = _timelock.getProposal(1); - uint256 submitTimestamp = block.timestamp; + Timestamp submitTimestamp = Timestamps.now(); assertEq(submittedProposal.id, 1); assertEq(submittedProposal.executor, _adminExecutor); assertEq(submittedProposal.submittedAt, submitTimestamp); - assertEq(submittedProposal.scheduledAt, 0); - assertEq(submittedProposal.executedAt, 0); + assertEq(submittedProposal.scheduledAt, Timestamps.ZERO); + assertEq(submittedProposal.executedAt, Timestamps.ZERO); // assertEq doesn't support comparing enumerables so far assert(submittedProposal.status == Status.Submitted); assertEq(submittedProposal.calls.length, 1); @@ -709,7 +709,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _wait(_config.AFTER_SUBMIT_DELAY()); _timelock.schedule(1); - uint256 scheduleTimestamp = block.timestamp; + Timestamp scheduleTimestamp = Timestamps.now(); Proposal memory scheduledProposal = _timelock.getProposal(1); @@ -717,7 +717,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(scheduledProposal.executor, _adminExecutor); assertEq(scheduledProposal.submittedAt, submitTimestamp); assertEq(scheduledProposal.scheduledAt, scheduleTimestamp); - assertEq(scheduledProposal.executedAt, 0); + assertEq(scheduledProposal.executedAt, Timestamps.ZERO); // // assertEq doesn't support comparing enumerables so far assert(scheduledProposal.status == Status.Scheduled); assertEq(scheduledProposal.calls.length, 1); @@ -730,7 +730,7 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { _timelock.execute(1); Proposal memory executedProposal = _timelock.getProposal(1); - uint256 executeTimestamp = block.timestamp; + Timestamp executeTimestamp = Timestamps.now(); assertEq(executedProposal.id, 1); assertEq(executedProposal.executor, _adminExecutor); @@ -751,8 +751,8 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(cancelledProposal.id, 2); assertEq(cancelledProposal.executor, _adminExecutor); assertEq(cancelledProposal.submittedAt, submitTimestamp); - assertEq(cancelledProposal.scheduledAt, 0); - assertEq(cancelledProposal.executedAt, 0); + assertEq(cancelledProposal.scheduledAt, Timestamps.ZERO); + assertEq(cancelledProposal.executedAt, Timestamps.ZERO); // assertEq doesn't support comparing enumerables so far assert(cancelledProposal.status == Status.Cancelled); assertEq(cancelledProposal.calls.length, 1); @@ -825,6 +825,13 @@ contract EmergencyProtectedTimelockUnitTests is UnitTest { assertEq(_timelock.canSchedule(1), false); } + // EmergencyProtectedTimelock.getProposalSubmissionTime() + + function test_get_proposal_submission_time() external { + _submitProposal(); + assertEq(_timelock.getProposalSubmissionTime(1), Timestamps.now()); + } + // Utils function _submitProposal() internal { diff --git a/test/unit/SingleGovernance.t.sol b/test/unit/SingleGovernance.t.sol index 489370bf..50619594 100644 --- a/test/unit/SingleGovernance.t.sol +++ b/test/unit/SingleGovernance.t.sol @@ -50,9 +50,7 @@ contract SingleGovernanceUnitTests is UnitTest { assertEq(_timelock.getSubmittedProposals().length, 0); vm.startPrank(stranger); - vm.expectRevert( - abi.encodeWithSelector(SingleGovernance.NotGovernance.selector, [stranger]) - ); + vm.expectRevert(abi.encodeWithSelector(SingleGovernance.NotGovernance.selector, [stranger])); _singleGovernance.submitProposal(_getTargetRegularStaffCalls(address(0x1))); assertEq(_timelock.getSubmittedProposals().length, 0); @@ -93,7 +91,7 @@ contract SingleGovernanceUnitTests is UnitTest { _timelock.setSchedule(1); _singleGovernance.scheduleProposal(1); - + _singleGovernance.cancelAllPendingProposals(); assertEq(_timelock.getLastCancelledProposalId(), 2); @@ -105,9 +103,7 @@ contract SingleGovernanceUnitTests is UnitTest { assertEq(_timelock.getLastCancelledProposalId(), 0); vm.startPrank(stranger); - vm.expectRevert( - abi.encodeWithSelector(SingleGovernance.NotGovernance.selector, [stranger]) - ); + vm.expectRevert(abi.encodeWithSelector(SingleGovernance.NotGovernance.selector, [stranger])); _singleGovernance.cancelAllPendingProposals(); assertEq(_timelock.getLastCancelledProposalId(), 0); @@ -123,4 +119,4 @@ contract SingleGovernanceUnitTests is UnitTest { assertTrue(_singleGovernance.canSchedule(1)); } -} \ No newline at end of file +} diff --git a/test/unit/libraries/EmergencyProtection.t.sol b/test/unit/libraries/EmergencyProtection.t.sol index 442c708f..d3681e08 100644 --- a/test/unit/libraries/EmergencyProtection.t.sol +++ b/test/unit/libraries/EmergencyProtection.t.sol @@ -5,7 +5,7 @@ import {Test, Vm} from "forge-std/Test.sol"; import {EmergencyProtection, EmergencyState} from "contracts/libraries/EmergencyProtection.sol"; -import {UnitTest} from "test/utils/unit-test.sol"; +import {UnitTest, Duration, Durations, Timestamp, Timestamps} from "test/utils/unit-test.sol"; contract EmergencyProtectionUnitTests is UnitTest { using EmergencyProtection for EmergencyProtection.State; @@ -15,11 +15,11 @@ contract EmergencyProtectionUnitTests is UnitTest { function testFuzz_setup_emergency_protection( address activationCommittee, address executionCommittee, - uint256 protectedTill, - uint256 duration + Duration protectionDuration, + Duration duration ) external { - vm.assume(protectedTill > 0 && protectedTill < type(uint40).max); - vm.assume(duration > 0 && duration < type(uint32).max); + vm.assume(protectionDuration > Durations.ZERO); + vm.assume(duration > Durations.ZERO); vm.assume(activationCommittee != address(0)); vm.assume(executionCommittee != address(0)); @@ -28,125 +28,150 @@ contract EmergencyProtectionUnitTests is UnitTest { vm.expectEmit(); emit EmergencyProtection.EmergencyExecutionCommitteeSet(executionCommittee); vm.expectEmit(); - emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + protectedTill); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(protectionDuration.addTo(Timestamps.now())); vm.expectEmit(); emit EmergencyProtection.EmergencyModeDurationSet(duration); vm.recordLogs(); - _emergencyProtection.setup(activationCommittee, executionCommittee, protectedTill, duration); + _emergencyProtection.setup(activationCommittee, executionCommittee, protectionDuration, duration); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 4); assertEq(_emergencyProtection.activationCommittee, activationCommittee); assertEq(_emergencyProtection.executionCommittee, executionCommittee); - assertEq(_emergencyProtection.protectedTill, block.timestamp + protectedTill); + assertEq(_emergencyProtection.protectedTill, protectionDuration.addTo(Timestamps.now())); assertEq(_emergencyProtection.emergencyModeDuration, duration); - assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } function test_setup_same_activation_committee() external { + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); address activationCommittee = makeAddr("activationCommittee"); - _emergencyProtection.setup(activationCommittee, address(0x2), 100, 100); + _emergencyProtection.setup(activationCommittee, address(0x2), protectionDuration, emergencyModeDuration); + + Duration newProtectionDuration = Durations.from(200 seconds); + Duration newEmergencyModeDuration = Durations.from(300 seconds); vm.expectEmit(); emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x3)); vm.expectEmit(); - emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + 200); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(newProtectionDuration.addTo(Timestamps.now())); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDurationSet(300); + emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); vm.recordLogs(); - _emergencyProtection.setup(activationCommittee, address(0x3), 200, 300); + _emergencyProtection.setup(activationCommittee, address(0x3), newProtectionDuration, newEmergencyModeDuration); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); assertEq(_emergencyProtection.activationCommittee, activationCommittee); assertEq(_emergencyProtection.executionCommittee, address(0x3)); - assertEq(_emergencyProtection.protectedTill, block.timestamp + 200); - assertEq(_emergencyProtection.emergencyModeDuration, 300); - assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + assertEq(_emergencyProtection.protectedTill, newProtectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } function test_setup_same_execution_committee() external { + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); address executionCommittee = makeAddr("executionCommittee"); - _emergencyProtection.setup(address(0x1), executionCommittee, 100, 100); + _emergencyProtection.setup(address(0x1), executionCommittee, protectionDuration, emergencyModeDuration); + + Duration newProtectionDuration = Durations.from(200 seconds); + Duration newEmergencyModeDuration = Durations.from(300 seconds); vm.expectEmit(); emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x2)); vm.expectEmit(); - emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + 200); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(newProtectionDuration.addTo(Timestamps.now())); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDurationSet(300); + emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); vm.recordLogs(); - _emergencyProtection.setup(address(0x2), executionCommittee, 200, 300); + _emergencyProtection.setup(address(0x2), executionCommittee, newProtectionDuration, newEmergencyModeDuration); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); assertEq(_emergencyProtection.activationCommittee, address(0x2)); assertEq(_emergencyProtection.executionCommittee, executionCommittee); - assertEq(_emergencyProtection.protectedTill, block.timestamp + 200); - assertEq(_emergencyProtection.emergencyModeDuration, 300); - assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + assertEq(_emergencyProtection.protectedTill, newProtectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } function test_setup_same_protected_till() external { - _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + + Duration newProtectionDuration = protectionDuration; // the new value is the same as previous one + Duration newEmergencyModeDuration = Durations.from(200 seconds); vm.expectEmit(); emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); vm.expectEmit(); emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x4)); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDurationSet(200); + emit EmergencyProtection.EmergencyModeDurationSet(newEmergencyModeDuration); vm.recordLogs(); - _emergencyProtection.setup(address(0x3), address(0x4), 100, 200); + _emergencyProtection.setup(address(0x3), address(0x4), newProtectionDuration, newEmergencyModeDuration); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); assertEq(_emergencyProtection.activationCommittee, address(0x3)); assertEq(_emergencyProtection.executionCommittee, address(0x4)); - assertEq(_emergencyProtection.protectedTill, block.timestamp + 100); - assertEq(_emergencyProtection.emergencyModeDuration, 200); - assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + assertEq(_emergencyProtection.protectedTill, protectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } function test_setup_same_emergency_mode_duration() external { - _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); + + Duration newProtectionDuration = Durations.from(200 seconds); + Duration newEmergencyModeDuration = emergencyModeDuration; // the new value is the same as previous one vm.expectEmit(); emit EmergencyProtection.EmergencyActivationCommitteeSet(address(0x3)); vm.expectEmit(); emit EmergencyProtection.EmergencyExecutionCommitteeSet(address(0x4)); vm.expectEmit(); - emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(block.timestamp + 200); + emit EmergencyProtection.EmergencyCommitteeProtectedTillSet(newProtectionDuration.addTo(Timestamps.now())); vm.recordLogs(); - _emergencyProtection.setup(address(0x3), address(0x4), 200, 100); + _emergencyProtection.setup(address(0x3), address(0x4), newProtectionDuration, newEmergencyModeDuration); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 3); assertEq(_emergencyProtection.activationCommittee, address(0x3)); assertEq(_emergencyProtection.executionCommittee, address(0x4)); - assertEq(_emergencyProtection.protectedTill, block.timestamp + 200); - assertEq(_emergencyProtection.emergencyModeDuration, 100); - assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + assertEq(_emergencyProtection.protectedTill, newProtectionDuration.addTo(Timestamps.now())); + assertEq(_emergencyProtection.emergencyModeDuration, newEmergencyModeDuration); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } function test_activate_emergency_mode() external { - _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeActivated(block.timestamp); + emit EmergencyProtection.EmergencyModeActivated(Timestamps.now()); vm.recordLogs(); @@ -155,19 +180,22 @@ contract EmergencyProtectionUnitTests is UnitTest { Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 1); - assertEq(_emergencyProtection.emergencyModeEndsAfter, block.timestamp + 100); + assertEq(_emergencyProtection.emergencyModeEndsAfter, emergencyModeDuration.addTo(Timestamps.now())); } function test_cannot_activate_emergency_mode_if_protected_till_expired() external { - uint256 protectedTill = 100; - _emergencyProtection.setup(address(0x1), address(0x2), protectedTill, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); - _wait(protectedTill + 1); + _wait(protectionDuration.plusSeconds(1)); vm.expectRevert( abi.encodeWithSelector( EmergencyProtection.EmergencyCommitteeExpired.selector, - [block.timestamp, _emergencyProtection.protectedTill] + Timestamps.now(), + _emergencyProtection.protectedTill ) ); _emergencyProtection.activate(); @@ -176,19 +204,17 @@ contract EmergencyProtectionUnitTests is UnitTest { function testFuzz_deactivate_emergency_mode( address activationCommittee, address executionCommittee, - uint256 protectedTill, - uint256 duration + Duration protectionDuration, + Duration emergencyModeDuration ) external { - vm.assume(protectedTill > 0 && protectedTill < type(uint40).max); - vm.assume(duration > 0 && duration < type(uint32).max); vm.assume(activationCommittee != address(0)); vm.assume(executionCommittee != address(0)); - _emergencyProtection.setup(activationCommittee, executionCommittee, protectedTill, duration); + _emergencyProtection.setup(activationCommittee, executionCommittee, protectionDuration, emergencyModeDuration); _emergencyProtection.activate(); vm.expectEmit(); - emit EmergencyProtection.EmergencyModeDeactivated(block.timestamp); + emit EmergencyProtection.EmergencyModeDeactivated(Timestamps.now()); vm.recordLogs(); @@ -199,9 +225,9 @@ contract EmergencyProtectionUnitTests is UnitTest { assertEq(_emergencyProtection.activationCommittee, address(0)); assertEq(_emergencyProtection.executionCommittee, address(0)); - assertEq(_emergencyProtection.protectedTill, 0); - assertEq(_emergencyProtection.emergencyModeDuration, 0); - assertEq(_emergencyProtection.emergencyModeEndsAfter, 0); + assertEq(_emergencyProtection.protectedTill, Timestamps.ZERO); + assertEq(_emergencyProtection.emergencyModeDuration, Durations.ZERO); + assertEq(_emergencyProtection.emergencyModeEndsAfter, Timestamps.ZERO); } function test_get_emergency_state() external { @@ -209,20 +235,23 @@ contract EmergencyProtectionUnitTests is UnitTest { assertEq(state.activationCommittee, address(0)); assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, 0); - assertEq(state.emergencyModeDuration, 0); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.protectedTill, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); assertEq(state.isEmergencyModeActivated, false); - _emergencyProtection.setup(address(0x1), address(0x2), 100, 200); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(200 seconds); + + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); state = _emergencyProtection.getEmergencyState(); assertEq(state.activationCommittee, address(0x1)); assertEq(state.executionCommittee, address(0x2)); - assertEq(state.protectedTill, block.timestamp + 100); - assertEq(state.emergencyModeDuration, 200); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.protectedTill, protectionDuration.addTo(Timestamps.now())); + assertEq(state.emergencyModeDuration, emergencyModeDuration); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); assertEq(state.isEmergencyModeActivated, false); _emergencyProtection.activate(); @@ -231,9 +260,9 @@ contract EmergencyProtectionUnitTests is UnitTest { assertEq(state.activationCommittee, address(0x1)); assertEq(state.executionCommittee, address(0x2)); - assertEq(state.protectedTill, block.timestamp + 100); - assertEq(state.emergencyModeDuration, 200); - assertEq(state.emergencyModeEndsAfter, block.timestamp + 200); + assertEq(state.protectedTill, protectionDuration.addTo(Timestamps.now())); + assertEq(state.emergencyModeDuration, emergencyModeDuration); + assertEq(state.emergencyModeEndsAfter, emergencyModeDuration.addTo(Timestamps.now())); assertEq(state.isEmergencyModeActivated, true); _emergencyProtection.deactivate(); @@ -242,16 +271,19 @@ contract EmergencyProtectionUnitTests is UnitTest { assertEq(state.activationCommittee, address(0)); assertEq(state.executionCommittee, address(0)); - assertEq(state.protectedTill, 0); - assertEq(state.emergencyModeDuration, 0); - assertEq(state.emergencyModeEndsAfter, 0); + assertEq(state.protectedTill, Timestamps.ZERO); + assertEq(state.emergencyModeDuration, Durations.ZERO); + assertEq(state.emergencyModeEndsAfter, Timestamps.ZERO); assertEq(state.isEmergencyModeActivated, false); } function test_is_emergency_mode_activated() external { assertEq(_emergencyProtection.isEmergencyModeActivated(), false); - _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); assertEq(_emergencyProtection.isEmergencyModeActivated(), false); @@ -267,9 +299,10 @@ contract EmergencyProtectionUnitTests is UnitTest { function test_is_emergency_mode_passed() external { assertEq(_emergencyProtection.isEmergencyModePassed(), false); - uint256 duration = 200; + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(200 seconds); - _emergencyProtection.setup(address(0x1), address(0x2), 100, duration); + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); assertEq(_emergencyProtection.isEmergencyModePassed(), false); @@ -277,7 +310,7 @@ contract EmergencyProtectionUnitTests is UnitTest { assertEq(_emergencyProtection.isEmergencyModePassed(), false); - _wait(duration + 1); + _wait(emergencyModeDuration.plusSeconds(1)); assertEq(_emergencyProtection.isEmergencyModePassed(), true); @@ -287,24 +320,28 @@ contract EmergencyProtectionUnitTests is UnitTest { } function test_is_emergency_protection_enabled() external { - uint256 protectedTill = 100; - uint256 duration = 200; + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(200 seconds); assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), false); - _emergencyProtection.setup(address(0x1), address(0x2), protectedTill, duration); + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); - _wait(protectedTill - block.timestamp); + EmergencyState memory emergencyState = _emergencyProtection.getEmergencyState(); + + _wait(Durations.between(emergencyState.protectedTill, Timestamps.now())); + + // _wait(emergencyState.protectedTill.absDiff(Timestamps.now())); EmergencyProtection.activate(_emergencyProtection); - _wait(duration); + _wait(emergencyModeDuration); assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); - _wait(100); + _wait(protectionDuration); assertEq(_emergencyProtection.isEmergencyProtectionEnabled(), true); @@ -321,7 +358,10 @@ contract EmergencyProtectionUnitTests is UnitTest { _emergencyProtection.checkActivationCommittee(stranger); _emergencyProtection.checkActivationCommittee(address(0)); - _emergencyProtection.setup(committee, address(0x2), 100, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(committee, address(0x2), protectionDuration, emergencyModeDuration); _emergencyProtection.checkActivationCommittee(committee); @@ -337,7 +377,10 @@ contract EmergencyProtectionUnitTests is UnitTest { _emergencyProtection.checkExecutionCommittee(stranger); _emergencyProtection.checkExecutionCommittee(address(0)); - _emergencyProtection.setup(address(0x1), committee, 100, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(address(0x1), committee, protectionDuration, emergencyModeDuration); _emergencyProtection.checkExecutionCommittee(committee); @@ -352,7 +395,10 @@ contract EmergencyProtectionUnitTests is UnitTest { _emergencyProtection.checkEmergencyModeActive(true); _emergencyProtection.checkEmergencyModeActive(false); - _emergencyProtection.setup(address(0x1), address(0x2), 100, 100); + Duration protectionDuration = Durations.from(100 seconds); + Duration emergencyModeDuration = Durations.from(100 seconds); + + _emergencyProtection.setup(address(0x1), address(0x2), protectionDuration, emergencyModeDuration); _emergencyProtection.activate(); _emergencyProtection.checkEmergencyModeActive(true); diff --git a/test/unit/libraries/Proposals.t.sol b/test/unit/libraries/Proposals.t.sol index 3b4ed280..0b70209a 100644 --- a/test/unit/libraries/Proposals.t.sol +++ b/test/unit/libraries/Proposals.t.sol @@ -7,9 +7,7 @@ import {Executor} from "contracts/Executor.sol"; import {Proposals, ExecutorCall, Proposal, Status} from "contracts/libraries/Proposals.sol"; import {TargetMock} from "test/utils/utils.sol"; -import {UnitTest} from "test/utils/unit-test.sol"; -import {IDangerousContract} from "test/utils/interfaces.sol"; -import {ExecutorCallHelpers} from "test/utils/executor-calls.sol"; +import {UnitTest, Timestamps, Timestamp, Durations, Duration} from "test/utils/unit-test.sol"; contract ProposalsUnitTests is UnitTest { using Proposals for Proposals.State; @@ -50,9 +48,9 @@ contract ProposalsUnitTests is UnitTest { Proposals.ProposalPacked memory proposal = _proposals.proposals[proposalsCount - PROPOSAL_ID_OFFSET]; assertEq(proposal.executor, address(_executor)); - assertEq(proposal.submittedAt, block.timestamp); - assertEq(proposal.executedAt, 0); - assertEq(proposal.scheduledAt, 0); + assertEq(proposal.submittedAt, Timestamps.now()); + assertEq(proposal.executedAt, Timestamps.ZERO); + assertEq(proposal.scheduledAt, Timestamps.ZERO); assertEq(proposal.calls.length, 1); for (uint256 i = 0; i < proposal.calls.length; i++) { @@ -62,17 +60,17 @@ contract ProposalsUnitTests is UnitTest { } } - function testFuzz_schedule_proposal(uint256 delay) external { - vm.assume(delay > 0 && delay < type(uint40).max); + function testFuzz_schedule_proposal(Duration delay) external { + vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); Proposals.ProposalPacked memory proposal = _proposals.proposals[0]; - uint256 submittedAt = block.timestamp; + Timestamp submittedAt = Timestamps.now(); assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, 0); - assertEq(proposal.executedAt, 0); + assertEq(proposal.scheduledAt, Timestamps.ZERO); + assertEq(proposal.executedAt, Timestamps.ZERO); uint256 proposalId = _proposals.count(); @@ -85,32 +83,32 @@ contract ProposalsUnitTests is UnitTest { proposal = _proposals.proposals[0]; assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, block.timestamp); - assertEq(proposal.executedAt, 0); + assertEq(proposal.scheduledAt, Timestamps.now()); + assertEq(proposal.executedAt, Timestamps.ZERO); } function testFuzz_cannot_schedule_unsubmitted_proposal(uint256 proposalId) external { vm.assume(proposalId > 0); vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotSubmitted.selector, proposalId)); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); } function test_cannot_schedule_proposal_twice() external { Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = 1; - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotSubmitted.selector, proposalId)); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); } - function testFuzz_cannot_schedule_proposal_before_delay_passed(uint256 delay) external { - vm.assume(delay > 0 && delay < type(uint40).max); + function testFuzz_cannot_schedule_proposal_before_delay_passed(Duration delay) external { + vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); - _wait(delay - 1); + _wait(delay.minusSeconds(1 seconds)); vm.expectRevert(abi.encodeWithSelector(Proposals.AfterSubmitDelayNotPassed.selector, PROPOSAL_ID_OFFSET)); Proposals.schedule(_proposals, PROPOSAL_ID_OFFSET, delay); @@ -123,21 +121,21 @@ contract ProposalsUnitTests is UnitTest { uint256 proposalId = _proposals.count(); vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotSubmitted.selector, proposalId)); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); } - function testFuzz_execute_proposal(uint256 delay) external { - vm.assume(delay > 0 && delay < type(uint40).max); + function testFuzz_execute_proposal(Duration delay) external { + vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); - uint256 submittedAndScheduledAt = block.timestamp; + Timestamp submittedAndScheduledAt = Timestamps.now(); assertEq(_proposals.proposals[0].submittedAt, submittedAndScheduledAt); assertEq(_proposals.proposals[0].scheduledAt, submittedAndScheduledAt); - assertEq(_proposals.proposals[0].executedAt, 0); + assertEq(_proposals.proposals[0].executedAt, Timestamps.ZERO); _wait(delay); @@ -153,13 +151,13 @@ contract ProposalsUnitTests is UnitTest { assertEq(_proposals.proposals[0].submittedAt, submittedAndScheduledAt); assertEq(_proposals.proposals[0].scheduledAt, submittedAndScheduledAt); - assertEq(proposal.executedAt, block.timestamp); + assertEq(proposal.executedAt, Timestamps.now()); } function testFuzz_cannot_execute_unsubmitted_proposal(uint256 proposalId) external { vm.assume(proposalId > 0); vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); - Proposals.execute(_proposals, proposalId, 0); + Proposals.execute(_proposals, proposalId, Durations.ZERO); } function test_cannot_execute_unscheduled_proposal() external { @@ -167,36 +165,36 @@ contract ProposalsUnitTests is UnitTest { uint256 proposalId = _proposals.count(); vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); - Proposals.execute(_proposals, proposalId, 0); + Proposals.execute(_proposals, proposalId, Durations.ZERO); } function test_cannot_execute_twice() external { Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - Proposals.schedule(_proposals, proposalId, 0); - Proposals.execute(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); + Proposals.execute(_proposals, proposalId, Durations.ZERO); vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); - Proposals.execute(_proposals, proposalId, 0); + Proposals.execute(_proposals, proposalId, Durations.ZERO); } function test_cannot_execute_cancelled_proposal() external { Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); Proposals.cancelAll(_proposals); vm.expectRevert(abi.encodeWithSelector(Proposals.ProposalNotScheduled.selector, proposalId)); - Proposals.execute(_proposals, proposalId, 0); + Proposals.execute(_proposals, proposalId, Durations.ZERO); } - function testFuzz_cannot_execute_before_delay_passed(uint256 delay) external { - vm.assume(delay > 0 && delay < type(uint40).max); + function testFuzz_cannot_execute_before_delay_passed(Duration delay) external { + vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); - _wait(delay - 1); + _wait(delay.minusSeconds(1 seconds)); vm.expectRevert(abi.encodeWithSelector(Proposals.AfterScheduleDelayNotPassed.selector, proposalId)); Proposals.execute(_proposals, proposalId, delay); @@ -208,7 +206,7 @@ contract ProposalsUnitTests is UnitTest { uint256 proposalsCount = _proposals.count(); - Proposals.schedule(_proposals, proposalsCount, 0); + Proposals.schedule(_proposals, proposalsCount, Durations.ZERO); vm.expectEmit(); emit Proposals.ProposalsCancelledTill(proposalsCount); @@ -224,13 +222,13 @@ contract ProposalsUnitTests is UnitTest { Proposal memory proposal = _proposals.get(proposalId); - uint256 submittedAt = block.timestamp; + Timestamp submittedAt = Timestamps.now(); assertEq(proposal.id, proposalId); assertEq(proposal.executor, address(_executor)); assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, 0); - assertEq(proposal.executedAt, 0); + assertEq(proposal.scheduledAt, Timestamps.ZERO); + assertEq(proposal.executedAt, Timestamps.ZERO); assertEq(proposal.calls.length, 1); assert(proposal.status == Status.Submitted); @@ -240,9 +238,9 @@ contract ProposalsUnitTests is UnitTest { assertEq(proposal.calls[i].payload, calls[i].payload); } - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); - uint256 scheduledAt = block.timestamp; + Timestamp scheduledAt = Timestamps.now(); proposal = _proposals.get(proposalId); @@ -250,7 +248,7 @@ contract ProposalsUnitTests is UnitTest { assertEq(proposal.executor, address(_executor)); assertEq(proposal.submittedAt, submittedAt); assertEq(proposal.scheduledAt, scheduledAt); - assertEq(proposal.executedAt, 0); + assertEq(proposal.executedAt, Timestamps.ZERO); assertEq(proposal.calls.length, 1); assert(proposal.status == Status.Scheduled); @@ -260,9 +258,9 @@ contract ProposalsUnitTests is UnitTest { assertEq(proposal.calls[i].payload, calls[i].payload); } - Proposals.execute(_proposals, proposalId, 0); + Proposals.execute(_proposals, proposalId, Durations.ZERO); - uint256 executedAt = block.timestamp; + Timestamp executedAt = Timestamps.now(); proposal = _proposals.get(proposalId); @@ -288,13 +286,13 @@ contract ProposalsUnitTests is UnitTest { Proposal memory proposal = _proposals.get(proposalId); - uint256 submittedAt = block.timestamp; + Timestamp submittedAt = Timestamps.now(); assertEq(proposal.id, proposalId); assertEq(proposal.executor, address(_executor)); assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, 0); - assertEq(proposal.executedAt, 0); + assertEq(proposal.scheduledAt, Timestamps.ZERO); + assertEq(proposal.executedAt, Timestamps.ZERO); assertEq(proposal.calls.length, 1); assert(proposal.status == Status.Submitted); @@ -311,8 +309,8 @@ contract ProposalsUnitTests is UnitTest { assertEq(proposal.id, proposalId); assertEq(proposal.executor, address(_executor)); assertEq(proposal.submittedAt, submittedAt); - assertEq(proposal.scheduledAt, 0); - assertEq(proposal.executedAt, 0); + assertEq(proposal.scheduledAt, Timestamps.ZERO); + assertEq(proposal.executedAt, Timestamps.ZERO); assertEq(proposal.calls.length, 1); assert(proposal.status == Status.Cancelled); @@ -343,13 +341,13 @@ contract ProposalsUnitTests is UnitTest { Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); assertEq(_proposals.count(), 4); - Proposals.schedule(_proposals, 1, 0); + Proposals.schedule(_proposals, 1, Durations.ZERO); assertEq(_proposals.count(), 4); - Proposals.schedule(_proposals, 2, 0); + Proposals.schedule(_proposals, 2, Durations.ZERO); assertEq(_proposals.count(), 4); - Proposals.execute(_proposals, 1, 0); + Proposals.execute(_proposals, 1, Durations.ZERO); assertEq(_proposals.count(), 4); Proposals.cancelAll(_proposals); @@ -357,57 +355,59 @@ contract ProposalsUnitTests is UnitTest { } function test_can_execute_proposal() external { + Duration delay = Durations.from(100 seconds); Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - assert(!_proposals.canExecute(proposalId, 0)); + assert(!_proposals.canExecute(proposalId, Durations.ZERO)); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); - assert(!_proposals.canExecute(proposalId, 100)); + assert(!_proposals.canExecute(proposalId, delay)); - _wait(100); + _wait(delay); - assert(_proposals.canExecute(proposalId, 100)); + assert(_proposals.canExecute(proposalId, delay)); - Proposals.execute(_proposals, proposalId, 0); + Proposals.execute(_proposals, proposalId, Durations.ZERO); - assert(!_proposals.canExecute(proposalId, 100)); + assert(!_proposals.canExecute(proposalId, delay)); } function test_can_not_execute_cancelled_proposal() external { Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - Proposals.schedule(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, Durations.ZERO); - assert(_proposals.canExecute(proposalId, 0)); + assert(_proposals.canExecute(proposalId, Durations.ZERO)); Proposals.cancelAll(_proposals); - assert(!_proposals.canExecute(proposalId, 0)); + assert(!_proposals.canExecute(proposalId, Durations.ZERO)); } function test_can_schedule_proposal() external { + Duration delay = Durations.from(100 seconds); Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - assert(!_proposals.canSchedule(proposalId, 100)); + assert(!_proposals.canSchedule(proposalId, delay)); - _wait(100); + _wait(delay); - assert(_proposals.canSchedule(proposalId, 100)); + assert(_proposals.canSchedule(proposalId, delay)); - Proposals.schedule(_proposals, proposalId, 100); - Proposals.execute(_proposals, proposalId, 0); + Proposals.schedule(_proposals, proposalId, delay); + Proposals.execute(_proposals, proposalId, Durations.ZERO); - assert(!_proposals.canSchedule(proposalId, 100)); + assert(!_proposals.canSchedule(proposalId, delay)); } function test_can_not_schedule_cancelled_proposal() external { Proposals.submit(_proposals, address(_executor), _getTargetRegularStaffCalls(address(_targetMock))); uint256 proposalId = _proposals.count(); - assert(_proposals.canSchedule(proposalId, 0)); + assert(_proposals.canSchedule(proposalId, Durations.ZERO)); Proposals.cancelAll(_proposals); - assert(!_proposals.canSchedule(proposalId, 0)); + assert(!_proposals.canSchedule(proposalId, Durations.ZERO)); } } diff --git a/test/unit/mocks/TimelockMock.sol b/test/unit/mocks/TimelockMock.sol index 63f58cc9..bea86f22 100644 --- a/test/unit/mocks/TimelockMock.sol +++ b/test/unit/mocks/TimelockMock.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Timestamp} from "contracts/types/Timestamp.sol"; import {ITimelock} from "contracts/interfaces/ITimelock.sol"; import {ExecutorCall} from "contracts/libraries/Proposals.sol"; @@ -22,13 +23,12 @@ contract TimelockMock is ITimelock { return newProposalId; } - function schedule(uint256 proposalId) external returns (uint256 submittedAt) { + function schedule(uint256 proposalId) external { if (canScheduleProposal[proposalId] == false) { revert(); } scheduledProposals.push(proposalId); - return 0; } function execute(uint256 proposalId) external { @@ -66,4 +66,8 @@ contract TimelockMock is ITimelock { function getLastCancelledProposalId() external view returns (uint256) { return lastCancelledProposalId; } + + function getProposalSubmissionTime(uint256 proposalId) external view returns (Timestamp submittedAt) { + revert("Not Implemented"); + } } diff --git a/test/utils/interfaces.sol b/test/utils/interfaces.sol index 40cd495f..5a2e90f3 100644 --- a/test/utils/interfaces.sol +++ b/test/utils/interfaces.sol @@ -105,10 +105,13 @@ interface IWithdrawalQueue is IERC721 { function grantRole(bytes32 role, address account) external; function hasRole(bytes32 role, address account) external view returns (bool); function isPaused() external view returns (bool); + function resume() external; + function pauseFor(uint256 duration) external; + function getResumeSinceTimestamp() external view returns (uint256); } interface IDangerousContract { function doRegularStaff(uint256 magic) external; function doRugPool() external; function doControversialStaff() external; -} \ No newline at end of file +} diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index fabbb898..6354155d 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -2,6 +2,8 @@ pragma solidity 0.8.23; import {Test} from "forge-std/Test.sol"; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; +import {Durations, Duration as DurationType} from "contracts/types/Duration.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import { @@ -13,6 +15,13 @@ import {Escrow, VetoerState, LockedAssetsTotals} from "contracts/Escrow.sol"; import {IConfiguration, Configuration} from "contracts/Configuration.sol"; import {Executor} from "contracts/Executor.sol"; +import {EmergencyActivationCommittee} from "contracts/committees/EmergencyActivationCommittee.sol"; +import {EmergencyExecutionCommittee} from "contracts/committees/EmergencyExecutionCommittee.sol"; +import {TiebreakerCore} from "contracts/committees/TiebreakerCore.sol"; +import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; + +import {ResealManager} from "contracts/ResealManager.sol"; + import { ExecutorCall, EmergencyState, @@ -26,11 +35,18 @@ import {DualGovernance, DualGovernanceState, State} from "contracts/DualGovernan import {Proposal, Status as ProposalStatus} from "contracts/libraries/Proposals.sol"; import {Percents, percents} from "../utils/percents.sol"; -import {IERC20, IStEth, IWstETH, IWithdrawalQueue, WithdrawalRequestStatus, IDangerousContract} from "../utils/interfaces.sol"; +import { + IERC20, + IStEth, + IWstETH, + IWithdrawalQueue, + WithdrawalRequestStatus, + IDangerousContract +} from "../utils/interfaces.sol"; import {ExecutorCallHelpers} from "../utils/executor-calls.sol"; import {Utils, TargetMock, console} from "../utils/utils.sol"; -import {DAO_VOTING, ST_ETH, WST_ETH, WITHDRAWAL_QUEUE} from "../utils/mainnet-addresses.sol"; +import {DAO_VOTING, ST_ETH, WST_ETH, WITHDRAWAL_QUEUE, DAO_AGENT} from "../utils/mainnet-addresses.sol"; struct Balances { uint256 stETHAmount; @@ -47,23 +63,28 @@ function countDigits(uint256 number) pure returns (uint256 digitsCount) { } while (number / 10 != 0); } +DurationType constant ONE_SECOND = DurationType.wrap(1); + contract ScenarioTestBlueprint is Test { address internal immutable _ADMIN_PROPOSER = DAO_VOTING; - uint256 internal immutable _EMERGENCY_MODE_DURATION = 180 days; - uint256 internal immutable _EMERGENCY_PROTECTION_DURATION = 90 days; + DurationType internal immutable _EMERGENCY_MODE_DURATION = Durations.from(180 days); + DurationType internal immutable _EMERGENCY_PROTECTION_DURATION = Durations.from(90 days); address internal immutable _EMERGENCY_ACTIVATION_COMMITTEE = makeAddr("EMERGENCY_ACTIVATION_COMMITTEE"); address internal immutable _EMERGENCY_EXECUTION_COMMITTEE = makeAddr("EMERGENCY_EXECUTION_COMMITTEE"); - uint256 internal immutable _SEALING_DURATION = 14 days; - uint256 internal immutable _SEALING_COMMITTEE_LIFETIME = 365 days; + DurationType internal immutable _SEALING_DURATION = Durations.from(14 days); + DurationType internal immutable _SEALING_COMMITTEE_LIFETIME = Durations.from(365 days); address internal immutable _SEALING_COMMITTEE = makeAddr("SEALING_COMMITTEE"); - address internal immutable _TIEBREAK_COMMITTEE = makeAddr("TIEBREAK_COMMITTEE"); - IStEth public immutable _ST_ETH = IStEth(ST_ETH); IWstETH public immutable _WST_ETH = IWstETH(WST_ETH); IWithdrawalQueue public immutable _WITHDRAWAL_QUEUE = IWithdrawalQueue(WITHDRAWAL_QUEUE); + EmergencyActivationCommittee internal _emergencyActivationCommittee; + EmergencyExecutionCommittee internal _emergencyExecutionCommittee; + TiebreakerCore internal _tiebreakerCommittee; + TiebreakerSubCommittee[] internal _tiebreakerSubCommittees; + TargetMock internal _target; IConfiguration internal _config; @@ -79,17 +100,19 @@ contract ScenarioTestBlueprint is Test { SingleGovernance internal _singleGovernance; DualGovernance internal _dualGovernance; + ResealManager internal _resealManager; + address[] internal _sealableWithdrawalBlockers = [WITHDRAWAL_QUEUE]; // --- // Helper Getters // --- function _getVetoSignallingEscrow() internal view returns (Escrow) { - return Escrow(payable(_dualGovernance.vetoSignallingEscrow())); + return Escrow(payable(_dualGovernance.getVetoSignallingEscrow())); } function _getRageQuitEscrow() internal view returns (Escrow) { - address rageQuitEscrow = _dualGovernance.rageQuitEscrow(); + address rageQuitEscrow = _dualGovernance.getRageQuitEscrow(); return Escrow(payable(rageQuitEscrow)); } @@ -102,7 +125,25 @@ contract ScenarioTestBlueprint is Test { view returns (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) { - return _dualGovernance.getVetoSignallingState(); + DurationType duration_; + Timestamp activatedAt_; + Timestamp enteredAt_; + (isActive, duration_, activatedAt_, enteredAt_) = _dualGovernance.getVetoSignallingState(); + duration = DurationType.unwrap(duration_); + enteredAt = Timestamp.unwrap(enteredAt_); + activatedAt = Timestamp.unwrap(activatedAt_); + } + + function _getVetoSignallingDeactivationState() + internal + view + returns (bool isActive, uint256 duration, uint256 enteredAt) + { + Timestamp enteredAt_; + DurationType duration_; + (isActive, duration_, enteredAt_) = _dualGovernance.getVetoSignallingDeactivationState(); + duration = DurationType.unwrap(duration_); + enteredAt = Timestamp.unwrap(enteredAt_); } // --- @@ -184,20 +225,22 @@ contract ScenarioTestBlueprint is Test { function _unlockWstETH(address vetoer) internal { Escrow escrow = _getVetoSignallingEscrow(); uint256 wstETHBalanceBefore = _WST_ETH.balanceOf(vetoer); - uint256 vetoerWstETHSharesBefore = escrow.getVetoerState(vetoer).wstETHShares; + VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); vm.startPrank(vetoer); uint256 wstETHUnlocked = escrow.unlockWstETH(); vm.stopPrank(); - assertEq(wstETHUnlocked, vetoerWstETHSharesBefore); - assertEq(_WST_ETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerWstETHSharesBefore); + // 1 wei rounding issue may arise because of the wrapping stETH into wstETH before + // sending funds to the user + assertApproxEqAbs(wstETHUnlocked, vetoerStateBefore.stETHLockedShares, 1); + assertApproxEqAbs(_WST_ETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerStateBefore.stETHLockedShares, 1); } function _lockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - uint256 vetoerUnstETHSharesBefore = escrow.getVetoerState(vetoer).unstETHShares; - uint256 totalSharesBefore = escrow.getLockedAssetsTotals().shares; + VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); uint256 unstETHTotalSharesLocked = 0; WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); @@ -215,19 +258,25 @@ contract ScenarioTestBlueprint is Test { assertEq(_WITHDRAWAL_QUEUE.ownerOf(unstETHIds[i]), address(escrow)); } - assertEq(escrow.getVetoerState(vetoer).unstETHShares, vetoerUnstETHSharesBefore + unstETHTotalSharesLocked); - assertEq(escrow.getLockedAssetsTotals().shares, totalSharesBefore + unstETHTotalSharesLocked); + VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); + assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount + unstETHIds.length); + + LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + assertEq( + lockedAssetsTotalsAfter.unstETHUnfinalizedShares, + lockedAssetsTotalsBefore.unstETHUnfinalizedShares + unstETHTotalSharesLocked + ); } function _unlockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - uint256 vetoerUnstETHSharesBefore = escrow.getVetoerState(vetoer).unstETHShares; - uint256 totalSharesBefore = escrow.getLockedAssetsTotals().shares; + VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); - uint256 unstETHTotalSharesLocked = 0; + uint256 unstETHTotalSharesUnlocked = 0; WithdrawalRequestStatus[] memory statuses = _WITHDRAWAL_QUEUE.getWithdrawalStatus(unstETHIds); for (uint256 i = 0; i < unstETHIds.length; ++i) { - unstETHTotalSharesLocked += statuses[i].amountOfShares; + unstETHTotalSharesUnlocked += statuses[i].amountOfShares; } vm.prank(vetoer); @@ -237,8 +286,15 @@ contract ScenarioTestBlueprint is Test { assertEq(_WITHDRAWAL_QUEUE.ownerOf(unstETHIds[i]), vetoer); } - assertEq(escrow.getVetoerState(vetoer).unstETHShares, vetoerUnstETHSharesBefore - unstETHTotalSharesLocked); - assertEq(escrow.getLockedAssetsTotals().shares, totalSharesBefore - unstETHTotalSharesLocked); + VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); + assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount - unstETHIds.length); + + // TODO: implement correct assert. It must consider was unstETH finalized or not + LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + assertEq( + lockedAssetsTotalsAfter.unstETHUnfinalizedShares, + lockedAssetsTotalsBefore.unstETHUnfinalizedShares - unstETHTotalSharesUnlocked + ); } // --- @@ -299,8 +355,8 @@ contract ScenarioTestBlueprint is Test { assertEq(proposal.id, proposalId, "unexpected proposal id"); assertEq(uint256(proposal.status), uint256(ProposalStatus.Submitted), "unexpected status value"); assertEq(proposal.executor, executor, "unexpected executor"); - assertEq(proposal.submittedAt, block.timestamp, "unexpected scheduledAt"); - assertEq(proposal.executedAt, 0, "unexpected executedAt"); + assertEq(Timestamp.unwrap(proposal.submittedAt), block.timestamp, "unexpected scheduledAt"); + assertEq(Timestamp.unwrap(proposal.executedAt), 0, "unexpected executedAt"); assertEq(proposal.calls.length, calls.length, "unexpected calls length"); for (uint256 i = 0; i < proposal.calls.length; ++i) { @@ -377,23 +433,23 @@ contract ScenarioTestBlueprint is Test { } function _assertNormalState() internal { - assertEq(uint256(_dualGovernance.currentState()), uint256(State.Normal)); + assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.Normal)); } function _assertVetoSignalingState() internal { - assertEq(uint256(_dualGovernance.currentState()), uint256(State.VetoSignalling)); + assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.VetoSignalling)); } function _assertVetoSignalingDeactivationState() internal { - assertEq(uint256(_dualGovernance.currentState()), uint256(State.VetoSignallingDeactivation)); + assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.VetoSignallingDeactivation)); } function _assertRageQuitState() internal { - assertEq(uint256(_dualGovernance.currentState()), uint256(State.RageQuit)); + assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.RageQuit)); } function _assertVetoCooldownState() internal { - assertEq(uint256(_dualGovernance.currentState()), uint256(State.VetoCooldown)); + assertEq(uint256(_dualGovernance.getCurrentState()), uint256(State.VetoCooldown)); } function _assertNoTargetMockCalls() internal { @@ -405,8 +461,7 @@ contract ScenarioTestBlueprint is Test { // --- function _logVetoSignallingState() internal { /* solhint-disable no-console */ - (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) = - _dualGovernance.getVetoSignallingState(); + (bool isActive, uint256 duration, uint256 activatedAt, uint256 enteredAt) = _getVetoSignallingState(); if (!isActive) { console.log("VetoSignalling state is not active\n"); @@ -431,7 +486,7 @@ contract ScenarioTestBlueprint is Test { function _logVetoSignallingDeactivationState() internal { /* solhint-disable no-console */ - (bool isActive, uint256 duration, uint256 enteredAt) = _dualGovernance.getVetoSignallingDeactivationState(); + (bool isActive, uint256 duration, uint256 enteredAt) = _getVetoSignallingDeactivationState(); if (!isActive) { console.log("VetoSignallingDeactivation state is not active\n"); @@ -467,6 +522,9 @@ contract ScenarioTestBlueprint is Test { _deployEscrowMasterCopy(); _deployUngovernedTimelock(); _deployDualGovernance(); + _deployEmergencyActivationCommittee(); + _deployEmergencyExecutionCommittee(); + _deployTiebreaker(); _finishTimelockSetup(address(_dualGovernance), isEmergencyProtectionEnabled); } @@ -477,6 +535,9 @@ contract ScenarioTestBlueprint is Test { _deployEscrowMasterCopy(); _deployUngovernedTimelock(); _deploySingleGovernance(); + _deployEmergencyActivationCommittee(); + _deployEmergencyExecutionCommittee(); + _deployTiebreaker(); _finishTimelockSetup(address(_singleGovernance), isEmergencyProtectionEnabled); } @@ -515,6 +576,52 @@ contract ScenarioTestBlueprint is Test { _escrowMasterCopy = new Escrow(ST_ETH, WST_ETH, WITHDRAWAL_QUEUE, address(_config)); } + function _deployTiebreaker() internal { + uint256 subCommitteeMembersCount = 5; + uint256 subCommitteeQuorum = 5; + uint256 subCommitteesCount = 2; + + _tiebreakerCommittee = + new TiebreakerCore(address(_adminExecutor), new address[](0), 1, address(_dualGovernance), 0); + + for (uint256 i = 0; i < subCommitteesCount; ++i) { + address[] memory committeeMembers = new address[](subCommitteeMembersCount); + for (uint256 j = 0; j < subCommitteeMembersCount; j++) { + committeeMembers[j] = makeAddr(string(abi.encode(i + j * subCommitteeMembersCount + 65))); + } + _tiebreakerSubCommittees.push( + new TiebreakerSubCommittee( + address(_adminExecutor), committeeMembers, subCommitteeQuorum, address(_tiebreakerCommittee) + ) + ); + + vm.prank(address(_adminExecutor)); + _tiebreakerCommittee.addMember(address(_tiebreakerSubCommittees[i]), i + 1); + } + } + + function _deployEmergencyActivationCommittee() internal { + uint256 quorum = 3; + uint256 membersCount = 5; + address[] memory committeeMembers = new address[](membersCount); + for (uint256 i = 0; i < membersCount; ++i) { + committeeMembers[i] = makeAddr(string(abi.encode(0xFE + i * membersCount + 65))); + } + _emergencyActivationCommittee = + new EmergencyActivationCommittee(address(_adminExecutor), committeeMembers, quorum, address(_timelock)); + } + + function _deployEmergencyExecutionCommittee() internal { + uint256 quorum = 3; + uint256 membersCount = 5; + address[] memory committeeMembers = new address[](membersCount); + for (uint256 i = 0; i < membersCount; ++i) { + committeeMembers[i] = makeAddr(string(abi.encode(0xFD + i * membersCount + 65))); + } + _emergencyExecutionCommittee = + new EmergencyExecutionCommittee(address(_adminExecutor), committeeMembers, quorum, address(_timelock)); + } + function _finishTimelockSetup(address governance, bool isEmergencyProtectionEnabled) internal { if (isEmergencyProtectionEnabled) { _adminExecutor.execute( @@ -523,8 +630,8 @@ contract ScenarioTestBlueprint is Test { abi.encodeCall( _timelock.setEmergencyProtection, ( - _EMERGENCY_ACTIVATION_COMMITTEE, - _EMERGENCY_EXECUTION_COMMITTEE, + address(_emergencyActivationCommittee), + address(_emergencyExecutionCommittee), _EMERGENCY_PROTECTION_DURATION, _EMERGENCY_MODE_DURATION ) @@ -532,11 +639,24 @@ contract ScenarioTestBlueprint is Test { ); } + _resealManager = new ResealManager(address(_timelock)); + + vm.prank(DAO_AGENT); + _WITHDRAWAL_QUEUE.grantRole( + 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d, address(_resealManager) + ); + vm.prank(DAO_AGENT); + _WITHDRAWAL_QUEUE.grantRole( + 0x2fc10cc8ae19568712f7a176fb4978616a610650813c9d05326c34abb62749c7, address(_resealManager) + ); + if (governance == address(_dualGovernance)) { _adminExecutor.execute( address(_dualGovernance), 0, - abi.encodeCall(_dualGovernance.setTiebreakerCommittee, (_TIEBREAK_COMMITTEE)) + abi.encodeCall( + _dualGovernance.setTiebreakerProtection, (address(_tiebreakerCommittee), address(_resealManager)) + ) ); } _adminExecutor.execute(address(_timelock), 0, abi.encodeCall(_timelock.setGovernance, (governance))); @@ -552,16 +672,43 @@ contract ScenarioTestBlueprint is Test { console.log(string.concat(">>> ", text, " <<<")); } - function _wait(uint256 duration) internal { - vm.warp(block.timestamp + duration); + function _wait(DurationType duration) internal { + vm.warp(duration.addTo(Timestamps.now()).toSeconds()); } function _waitAfterSubmitDelayPassed() internal { - _wait(_config.AFTER_SUBMIT_DELAY() + 1); + _wait(_config.AFTER_SUBMIT_DELAY() + ONE_SECOND); } function _waitAfterScheduleDelayPassed() internal { - _wait(_config.AFTER_SCHEDULE_DELAY() + 1); + _wait(_config.AFTER_SCHEDULE_DELAY() + ONE_SECOND); + } + + function _executeEmergencyActivate() internal { + address[] memory members = _emergencyActivationCommittee.getMembers(); + for (uint256 i = 0; i < _emergencyActivationCommittee.quorum(); ++i) { + vm.prank(members[i]); + _emergencyActivationCommittee.approveEmergencyActivate(); + } + _emergencyActivationCommittee.executeEmergencyActivate(); + } + + function _executeEmergencyExecute(uint256 proposalId) internal { + address[] memory members = _emergencyExecutionCommittee.getMembers(); + for (uint256 i = 0; i < _emergencyExecutionCommittee.quorum(); ++i) { + vm.prank(members[i]); + _emergencyExecutionCommittee.voteEmergencyExecute(proposalId, true); + } + _emergencyExecutionCommittee.executeEmergencyExecute(proposalId); + } + + function _executeEmergencyReset() internal { + address[] memory members = _emergencyExecutionCommittee.getMembers(); + for (uint256 i = 0; i < _emergencyExecutionCommittee.quorum(); ++i) { + vm.prank(members[i]); + _emergencyExecutionCommittee.approveEmergencyReset(); + } + _emergencyExecutionCommittee.executeEmergencyReset(); } struct Duration { @@ -594,6 +741,18 @@ contract ScenarioTestBlueprint is Test { ); } + function assertEq(uint40 a, uint40 b) internal { + assertEq(uint256(a), uint256(b)); + } + + function assertEq(Timestamp a, Timestamp b) internal { + assertEq(uint256(Timestamp.unwrap(a)), uint256(Timestamp.unwrap(b))); + } + + function assertEq(DurationType a, DurationType b) internal { + assertEq(uint256(DurationType.unwrap(a)), uint256(DurationType.unwrap(b))); + } + function assertEq(ProposalStatus a, ProposalStatus b) internal { assertEq(uint256(a), uint256(b)); } diff --git a/test/utils/unit-test.sol b/test/utils/unit-test.sol index 6ff279a4..3ec9b59c 100644 --- a/test/utils/unit-test.sol +++ b/test/utils/unit-test.sol @@ -1,18 +1,29 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; +import {Duration, Durations} from "contracts/types/Duration.sol"; + // solhint-disable-next-line -import {Test} from "forge-std/Test.sol"; +import {Test, console} from "forge-std/Test.sol"; import {ExecutorCall} from "contracts/libraries/Proposals.sol"; import {ExecutorCallHelpers} from "test/utils/executor-calls.sol"; import {IDangerousContract} from "test/utils/interfaces.sol"; contract UnitTest is Test { - function _wait(uint256 duration) internal { - vm.warp(block.timestamp + duration); + function _wait(Duration duration) internal { + vm.warp(block.timestamp + Duration.unwrap(duration)); } function _getTargetRegularStaffCalls(address targetMock) internal pure returns (ExecutorCall[] memory) { return ExecutorCallHelpers.create(address(targetMock), abi.encodeCall(IDangerousContract.doRegularStaff, (42))); } -} \ No newline at end of file + + function assertEq(Timestamp a, Timestamp b) internal { + assertEq(uint256(Timestamp.unwrap(a)), uint256(Timestamp.unwrap(b))); + } + + function assertEq(Duration a, Duration b) internal { + assertEq(uint256(Duration.unwrap(a)), uint256(Duration.unwrap(b))); + } +} diff --git a/test/utils/utils.sol b/test/utils/utils.sol index 9072e0ce..15224876 100644 --- a/test/utils/utils.sol +++ b/test/utils/utils.sol @@ -54,7 +54,7 @@ library Utils { function selectFork() internal { vm.createSelectFork(vm.envString("MAINNET_RPC_URL")); - vm.rollFork(18984396); + vm.rollFork(20218312); } function encodeEvmCallScript(address target, bytes memory data) internal pure returns (bytes memory) {