From 4a9350f5125d48a4981e06c8ff6ea59394bf729d Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 28 Nov 2024 05:34:27 +0400 Subject: [PATCH 1/8] Escrow interface refactor --- contracts/DualGovernance.sol | 29 +++-- contracts/Escrow.sol | 82 +++++++++----- contracts/interfaces/IDualGovernance.sol | 2 - contracts/interfaces/IEscrow.sol | 96 ++++++++++++----- .../libraries/DualGovernanceStateMachine.sol | 27 +++-- scripts/deploy/DeployVerification.sol | 3 +- test/mocks/EscrowMock.sol | 100 ++++++++++-------- test/scenario/escrow.t.sol | 11 +- test/unit/DualGovernance.t.sol | 9 +- .../DualGovernanceStateMachine.t.sol | 34 ++++-- .../DualGovernanceStateTransitions.t.sol | 10 +- test/utils/scenario-test-blueprint.sol | 20 ++-- 12 files changed, 262 insertions(+), 161 deletions(-) diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index 19d4426d..f87982f5 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -7,7 +7,7 @@ import {Timestamp} from "./types/Timestamp.sol"; import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; -import {IEscrow} from "./interfaces/IEscrow.sol"; +import {IEscrowBase} from "./interfaces/IEscrow.sol"; import {ITimelock} from "./interfaces/ITimelock.sol"; import {ITiebreaker} from "./interfaces/ITiebreaker.sol"; import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; @@ -52,8 +52,8 @@ contract DualGovernance is IDualGovernance { event CancelAllPendingProposalsSkipped(); event CancelAllPendingProposalsExecuted(); - event EscrowMasterCopyDeployed(IEscrow escrowMasterCopy); event ProposalsCancellerSet(address proposalsCanceller); + event EscrowMasterCopyDeployed(IEscrowBase escrowMasterCopy); // --- // Sanity Check Parameters & Immutables @@ -112,10 +112,6 @@ contract DualGovernance is IDualGovernance { /// @notice The address of the Timelock contract. ITimelock public immutable TIMELOCK; - /// @notice The address of the Escrow contract used as the implementation for the Signalling and Rage Quit - /// instances of the Escrows managed by the DualGovernance contract. - IEscrow public immutable ESCROW_MASTER_COPY; - // --- // Aspects // --- @@ -151,16 +147,17 @@ contract DualGovernance is IDualGovernance { MAX_TIEBREAKER_ACTIVATION_TIMEOUT = sanityCheckParams.maxTiebreakerActivationTimeout; MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT = sanityCheckParams.maxSealableWithdrawalBlockersCount; - ESCROW_MASTER_COPY = new Escrow({ + IEscrowBase escrowMasterCopy = new Escrow({ dualGovernance: this, stETH: dependencies.stETH, wstETH: dependencies.wstETH, withdrawalQueue: dependencies.withdrawalQueue, minWithdrawalsBatchSize: sanityCheckParams.minWithdrawalsBatchSize }); - emit EscrowMasterCopyDeployed(ESCROW_MASTER_COPY); - _stateMachine.initialize(dependencies.configProvider, ESCROW_MASTER_COPY); + emit EscrowMasterCopyDeployed(escrowMasterCopy); + + _stateMachine.initialize(dependencies.configProvider, escrowMasterCopy); _resealer.setResealManager(address(dependencies.resealManager)); } @@ -183,7 +180,7 @@ contract DualGovernance is IDualGovernance { ExternalCall[] calldata calls, string calldata metadata ) external returns (uint256 proposalId) { - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); if (!_stateMachine.canSubmitProposal({useEffectiveState: false})) { revert ProposalSubmissionBlocked(); } @@ -198,7 +195,7 @@ contract DualGovernance is IDualGovernance { /// @param proposalId The unique identifier of the proposal to be scheduled. This ID is obtained when the proposal /// is initially submitted to the Dual Governance system. function scheduleProposal(uint256 proposalId) external { - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); Timestamp proposalSubmittedAt = TIMELOCK.getProposalDetails(proposalId).submittedAt; if (!_stateMachine.canScheduleProposal({useEffectiveState: false, proposalSubmittedAt: proposalSubmittedAt})) { revert ProposalSchedulingBlocked(proposalId); @@ -214,7 +211,7 @@ contract DualGovernance is IDualGovernance { /// @return isProposalsCancelled A boolean indicating whether the proposals were successfully canceled (`true`) /// or the cancellation was skipped due to an inappropriate state (`false`). function cancelAllPendingProposals() external returns (bool) { - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); if (msg.sender != _proposalsCanceller) { revert CallerIsNotProposalsCanceller(msg.sender); @@ -282,7 +279,7 @@ contract DualGovernance is IDualGovernance { /// @dev This function should be called when the `persisted` and `effective` states of the system are not equal. /// If the states are already synchronized, the function will complete without making any changes to the system state. function activateNextState() external { - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); } /// @notice Updates the address of the configuration provider for the Dual Governance system. @@ -472,7 +469,7 @@ contract DualGovernance is IDualGovernance { /// @param sealable The address of the sealable contract to be resumed. function tiebreakerResumeSealable(address sealable) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); _tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.normalOrVetoCooldownExitedAt); _resealer.resealManager.resume(sealable); } @@ -482,7 +479,7 @@ contract DualGovernance is IDualGovernance { /// @param proposalId The unique identifier of the proposal to be scheduled. function tiebreakerScheduleProposal(uint256 proposalId) external { _tiebreaker.checkCallerIsTiebreakerCommittee(); - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); _tiebreaker.checkTie(_stateMachine.getPersistedState(), _stateMachine.normalOrVetoCooldownExitedAt); TIMELOCK.schedule(proposalId); } @@ -507,7 +504,7 @@ contract DualGovernance is IDualGovernance { /// the ResealManager contract. /// @param sealable The address of the sealable contract to be resealed. function resealSealable(address sealable) external { - _stateMachine.activateNextState(ESCROW_MASTER_COPY); + _stateMachine.activateNextState(); if (_stateMachine.getPersistedState() == State.Normal) { revert ResealIsNotAllowedInNormalState(); } diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 3ba3a99f..9f493de8 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -9,20 +9,20 @@ import {ETHValue, ETHValues} from "./types/ETHValue.sol"; import {SharesValue, SharesValues} from "./types/SharesValue.sol"; import {PercentD16, PercentsD16} from "./types/PercentD16.sol"; -import {IEscrow} from "./interfaces/IEscrow.sol"; +import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "./interfaces/IEscrow.sol"; import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; -import {EscrowState} from "./libraries/EscrowState.sol"; +import {EscrowState, State} from "./libraries/EscrowState.sol"; import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalsBatchesQueue.sol"; import {HolderAssets, StETHAccounting, UnstETHAccounting, AssetsAccounting} from "./libraries/AssetsAccounting.sol"; /// @notice This contract is used to accumulate stETH, wstETH, unstETH, and withdrawn ETH from vetoers during the /// veto signalling and rage quit processes. /// @dev This contract is intended to be used behind a minimal proxy deployed by the DualGovernance contract. -contract Escrow is IEscrow { +contract Escrow is ISignallingEscrow, IRageQuitEscrow { using EscrowState for EscrowState.Context; using AssetsAccounting for AssetsAccounting.Context; using WithdrawalsBatchesQueue for WithdrawalsBatchesQueue.Context; @@ -77,7 +77,7 @@ contract Escrow is IEscrow { /// @dev Reference to the address of the implementation contract, used to distinguish whether the call /// is made to the proxy or directly to the implementation. - address private immutable _SELF; + IEscrowBase public immutable ESCROW_MASTER_COPY; /// @dev The address of the Dual Governance contract. IDualGovernance public immutable DUAL_GOVERNANCE; @@ -106,7 +106,7 @@ contract Escrow is IEscrow { IDualGovernance dualGovernance, uint256 minWithdrawalsBatchSize ) { - _SELF = address(this); + ESCROW_MASTER_COPY = this; DUAL_GOVERNANCE = dualGovernance; ST_ETH = stETH; @@ -120,7 +120,7 @@ contract Escrow is IEscrow { /// @param minAssetsLockDuration The minimum duration that must pass from the last stETH, wstETH, or unstETH lock /// by the vetoer before they are allowed to unlock assets from the Escrow. function initialize(Duration minAssetsLockDuration) external { - if (address(this) == _SELF) { + if (this == ESCROW_MASTER_COPY) { revert NonProxyCallsForbidden(); } _checkCallerIsDualGovernance(); @@ -131,6 +131,13 @@ contract Escrow is IEscrow { ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); } + // --- + // Base Escrow Getters + // --- + function getEscrowState() external view returns (State) { + return _escrowState.state; + } + // --- // Lock & Unlock stETH // --- @@ -274,12 +281,47 @@ contract Escrow is IEscrow { /// have sufficient time to claim it. /// @param rageQuitEthWithdrawalsDelay The waiting period that vetoers must observe after the Rage Quit process /// is finalized before they can withdraw ETH from the Escrow. - function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitEthWithdrawalsDelay) external { + function startRageQuit( + Duration rageQuitExtensionPeriodDuration, + Duration rageQuitEthWithdrawalsDelay + ) external returns (IRageQuitEscrow rageQuitEscrow) { _checkCallerIsDualGovernance(); _escrowState.startRageQuit(rageQuitExtensionPeriodDuration, rageQuitEthWithdrawalsDelay); _batchesQueue.open(WITHDRAWAL_QUEUE.getLastRequestId()); + rageQuitEscrow = IRageQuitEscrow(this); + } + + // --- + // Signalling Escrow Getters + // --- + + /// @notice Returns the current Rage Quit support value as a percentage. + /// @return rageQuitSupport The current Rage Quit support as a `PercentD16` value. + function getRageQuitSupport() external view returns (PercentD16) { + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + + uint256 finalizedETH = unstETHTotals.finalizedETH.toUint256(); + uint256 unfinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); + + return PercentsD16.fromFraction({ + numerator: ST_ETH.getPooledEthByShares(unfinalizedShares) + finalizedETH, + denominator: ST_ETH.totalSupply() + finalizedETH + }); + } + + /// @notice Returns the minimum duration that must elapse after the last stETH, wstETH, or unstETH lock + /// by a vetoer before they are permitted to unlock their assets from the Escrow. + function getMinAssetsLockDuration() external view returns (Duration minAssetsLockDuration) { + minAssetsLockDuration = _escrowState.minAssetsLockDuration; } + // TODO: implement + function getLockedUnstETHState(uint256 unstETHId) external view returns (LockedUnstETHState memory) {} + + // TODO: implement + function getSignallingEscrowState() external view returns (SignallingEscrowState memory) {} + // --- // Request Withdrawal Batches // --- @@ -421,12 +463,6 @@ contract Escrow is IEscrow { // Escrow Management // --- - /// @notice Returns the minimum duration that must elapse after the last stETH, wstETH, or unstETH lock - /// by a vetoer before they are permitted to unlock their assets from the Escrow. - function getMinAssetsLockDuration() external view returns (Duration) { - return _escrowState.minAssetsLockDuration; - } - /// @notice Sets the minimum duration that must elapse after the last stETH, wstETH, or unstETH lock /// by a vetoer before they are permitted to unlock their assets from the Escrow. /// @param newMinAssetsLockDuration The new minimum lock duration to be set. @@ -463,9 +499,12 @@ contract Escrow is IEscrow { } // --- - // Getters + // Rage Quit Escrow Getters // --- + // TODO: implement + function getRageQuitEscrowState() external view returns (RageQuitEscrowState memory) {} + /// @notice Returns the total amounts of locked and claimed assets in the Escrow. /// @return totals A struct containing the total amounts of locked and claimed assets, including: /// - `stETHClaimedETH`: The total amount of ETH claimed from locked stETH. @@ -541,21 +580,6 @@ contract Escrow is IEscrow { return _escrowState.rageQuitExtensionPeriodStartedAt; } - /// @notice Returns the current Rage Quit support value as a percentage. - /// @return rageQuitSupport The current Rage Quit support as a `PercentD16` value. - function getRageQuitSupport() external view returns (PercentD16) { - StETHAccounting memory stETHTotals = _accounting.stETHTotals; - UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; - - uint256 finalizedETH = unstETHTotals.finalizedETH.toUint256(); - uint256 unfinalizedShares = (stETHTotals.lockedShares + unstETHTotals.unfinalizedShares).toUint256(); - - return PercentsD16.fromFraction({ - numerator: ST_ETH.getPooledEthByShares(unfinalizedShares) + finalizedETH, - denominator: ST_ETH.totalSupply() + finalizedETH - }); - } - /// @notice Returns whether the Rage Quit process has been finalized. /// @return A boolean value indicating whether the Rage Quit process has been finalized (`true`) or not (`false`). function isRageQuitFinalized() external view returns (bool) { diff --git a/contracts/interfaces/IDualGovernance.sol b/contracts/interfaces/IDualGovernance.sol index db7596b8..70d2ce02 100644 --- a/contracts/interfaces/IDualGovernance.sol +++ b/contracts/interfaces/IDualGovernance.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.26; import {IDualGovernanceConfigProvider} from "./IDualGovernanceConfigProvider.sol"; import {IGovernance} from "./IGovernance.sol"; -import {IEscrow} from "./IEscrow.sol"; import {IResealManager} from "./IResealManager.sol"; import {ITiebreaker} from "./ITiebreaker.sol"; import {Timestamp} from "../types/Timestamp.sol"; @@ -26,7 +25,6 @@ interface IDualGovernance is IGovernance, ITiebreaker { function MIN_TIEBREAKER_ACTIVATION_TIMEOUT() external view returns (Duration); function MAX_TIEBREAKER_ACTIVATION_TIMEOUT() external view returns (Duration); function MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT() external view returns (uint256); - function ESCROW_MASTER_COPY() external view returns (IEscrow); function canSubmitProposal() external view returns (bool); function canCancelAllPendingProposals() external view returns (bool); diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol index a51a5030..7c72b64e 100644 --- a/contracts/interfaces/IEscrow.sol +++ b/contracts/interfaces/IEscrow.sol @@ -2,15 +2,30 @@ pragma solidity 0.8.26; import {Duration} from "../types/Duration.sol"; +import {Timestamp} from "../types/Timestamp.sol"; import {PercentD16} from "../types/PercentD16.sol"; import {Timestamp} from "../types/Timestamp.sol"; -interface IEscrow { +import {ETHValue} from "../types/ETHValue.sol"; +import {SharesValue} from "../types/SharesValue.sol"; + +import {State as EscrowState} from "../libraries/EscrowState.sol"; +import {UnstETHRecordStatus} from "../libraries/AssetsAccounting.sol"; + +interface IEscrowBase { + struct VetoerState { + uint256 stETHLockedShares; + uint256 unstETHLockedShares; + uint256 unstETHIdsCount; + uint256 lastAssetsLockTimestamp; + } + /// @notice Summary of the total locked assets in the Escrow. /// @param stETHLockedShares The total number of stETH shares currently locked in the Escrow. /// @param stETHClaimedETH The total amount of ETH claimed from the stETH shares locked in the Escrow. /// @param unstETHUnfinalizedShares The total number of shares from unstETH NFTs that have not yet been marked as finalized. /// @param unstETHFinalizedETH The total amount of ETH claimable from unstETH NFTs that have been marked as finalized. + /// TODO: Remove and use LockedUnstETHState instead struct LockedAssetsTotals { uint256 stETHLockedShares; uint256 stETHClaimedETH; @@ -18,51 +33,80 @@ interface IEscrow { uint256 unstETHFinalizedETH; } - /// @notice Summary of the assets locked in the Escrow by a specific vetoer. - /// @param stETHLockedShares The total number of stETH shares currently locked in the Escrow by the vetoer. - /// @param unstETHLockedShares The total number of unstETH shares currently locked in the Escrow by the vetoer. - /// @param unstETHIdsCount The total number of unstETH NFTs locked in the Escrow by the vetoer. - /// @param lastAssetsLockTimestamp The timestamp of the last time the vetoer locked stETH, wstETH, or unstETH in the Escrow. - struct VetoerState { - uint256 stETHLockedShares; - uint256 unstETHLockedShares; - uint256 unstETHIdsCount; - uint256 lastAssetsLockTimestamp; - } + // TODO: add standalone getter + function ESCROW_MASTER_COPY() external view returns (IEscrowBase); function initialize(Duration minAssetsLockDuration) external; + function getEscrowState() external view returns (EscrowState); + function getVetoerState(address vetoer) external view returns (VetoerState memory); +} + +interface ISignallingEscrow is IEscrowBase { + struct LockedUnstETHState { + UnstETHRecordStatus status; + address lockedBy; + SharesValue shares; + ETHValue claimableAmount; + } + + struct SignallingEscrowState { + PercentD16 rageQuitSupport; + // + ETHValue totalStETHClaimedETH; + SharesValue totalStETHLockedShares; + // + ETHValue totalUnstETHFinalizedETH; + SharesValue totalUnstETHUnfinalizedShares; + } + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares); function unlockStETH() external returns (uint256 unlockedStETHShares); + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares); - function unlockWstETH() external returns (uint256 unlockedStETHShares); + function unlockWstETH() external returns (uint256 wstETHUnlocked); + function lockUnstETH(uint256[] memory unstETHIds) external; function unlockUnstETH(uint256[] memory unstETHIds) external; + function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external; - function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitEthWithdrawalsDelay) external; + function startRageQuit( + Duration rageQuitExtensionPeriodDuration, + Duration rageQuitEthWithdrawalsDelay + ) external returns (IRageQuitEscrow); + + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; + + function getRageQuitSupport() external view returns (PercentD16); + function getMinAssetsLockDuration() external view returns (Duration); + function getLockedUnstETHState(uint256 unstETHId) external view returns (LockedUnstETHState memory); + function getSignallingEscrowState() external view returns (SignallingEscrowState memory); +} + +interface IRageQuitEscrow is IEscrowBase { + struct RageQuitEscrowState { + bool isRageQuitFinalized; + bool isWithdrawalsBatchesClosed; + bool isRageQuitExtensionPeriodStarted; + uint256 unclaimedUnstETHIdsCount; + Duration rageQuitWithdrawalsDelay; + Duration rageQuitExtensionPeriodDuration; + Timestamp rageQuitExtensionPeriodStartedAt; + } function requestNextWithdrawalsBatch(uint256 batchSize) external; function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external; function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external; + function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external; function startRageQuitExtensionPeriod() external; - function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external; function withdrawETH() external; function withdrawETH(uint256[] calldata unstETHIds) external; - function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory totals); - function getVetoerState(address vetoer) external view returns (VetoerState memory state); - function getUnclaimedUnstETHIdsCount() external view returns (uint256); - function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds); - function isWithdrawalsBatchesClosed() external view returns (bool); - function isRageQuitExtensionPeriodStarted() external view returns (bool); - function getRageQuitExtensionPeriodStartedAt() external view returns (Timestamp); - function isRageQuitFinalized() external view returns (bool); - function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport); - function getMinAssetsLockDuration() external view returns (Duration minAssetsLockDuration); - function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; + function getRageQuitEscrowState() external view returns (RageQuitEscrowState memory); + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds); } diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index c67b53ee..11bb6457 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -7,7 +7,7 @@ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Duration} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; -import {IEscrow} from "../interfaces/IEscrow.sol"; +import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "../interfaces/IEscrow.sol"; import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; import {IDualGovernanceConfigProvider} from "../interfaces/IDualGovernanceConfigProvider.sol"; @@ -66,7 +66,7 @@ library DualGovernanceStateMachine { /// @dev slot 0: [48..87] Timestamp vetoSignallingActivatedAt; /// @dev slot 0: [88..247] - IEscrow signallingEscrow; + ISignallingEscrow signallingEscrow; /// @dev slot 0: [248..255] uint8 rageQuitRound; /// @dev slot 1: [0..39] @@ -74,7 +74,7 @@ library DualGovernanceStateMachine { /// @dev slot 1: [40..79] Timestamp normalOrVetoCooldownExitedAt; /// @dev slot 1: [80..239] - IEscrow rageQuitEscrow; + IRageQuitEscrow rageQuitEscrow; /// @dev slot 2: [0..159] IDualGovernanceConfigProvider configProvider; } @@ -90,7 +90,7 @@ library DualGovernanceStateMachine { // Events // --- - event NewSignallingEscrowDeployed(IEscrow indexed escrow); + event NewSignallingEscrowDeployed(ISignallingEscrow indexed escrow); event DualGovernanceStateChanged(State indexed from, State indexed to, Context state); event ConfigProviderSet(IDualGovernanceConfigProvider newConfigProvider); @@ -114,7 +114,7 @@ library DualGovernanceStateMachine { function initialize( Context storage self, IDualGovernanceConfigProvider configProvider, - IEscrow escrowMasterCopy + IEscrowBase escrowMasterCopy ) internal { if (self.state != State.Unset) { revert AlreadyInitialized(); @@ -137,9 +137,7 @@ library DualGovernanceStateMachine { /// `escrowMasterCopy` as the implementation for the minimal proxy, while the previous Signalling Escrow /// instance is converted into the RageQuit escrow. /// @param self The context of the Dual Governance State Machine. - /// @param escrowMasterCopy The address of the master copy used as the implementation for the minimal proxy - /// to deploy a new instance of the Signalling Escrow. - function activateNextState(Context storage self, IEscrow escrowMasterCopy) internal { + function activateNextState(Context storage self) internal { DualGovernanceConfig.Context memory config = getDualGovernanceConfig(self); (State currentState, State newState) = self.getStateTransition(config); @@ -165,7 +163,7 @@ library DualGovernanceStateMachine { self.vetoSignallingActivatedAt = newStateEnteredAt; } } else if (newState == State.RageQuit) { - IEscrow signallingEscrow = self.signallingEscrow; + ISignallingEscrow signallingEscrow = self.signallingEscrow; uint256 currentRageQuitRound = self.rageQuitRound; @@ -174,11 +172,10 @@ library DualGovernanceStateMachine { uint256 newRageQuitRound = Math.min(currentRageQuitRound + 1, MAX_RAGE_QUIT_ROUND); self.rageQuitRound = uint8(newRageQuitRound); - signallingEscrow.startRageQuit( + self.rageQuitEscrow = signallingEscrow.startRageQuit( config.rageQuitExtensionPeriodDuration, config.calcRageQuitWithdrawalsDelay(newRageQuitRound) ); - self.rageQuitEscrow = signallingEscrow; - _deployNewSignallingEscrow(self, escrowMasterCopy, config.minAssetsLockDuration); + _deployNewSignallingEscrow(self, signallingEscrow.ESCROW_MASTER_COPY(), config.minAssetsLockDuration); } emit DualGovernanceStateChanged(currentState, newState, self); @@ -190,7 +187,7 @@ library DualGovernanceStateMachine { function setConfigProvider(Context storage self, IDualGovernanceConfigProvider newConfigProvider) internal { _setConfigProvider(self, newConfigProvider); - IEscrow signallingEscrow = self.signallingEscrow; + ISignallingEscrow signallingEscrow = self.signallingEscrow; Duration newMinAssetsLockDuration = newConfigProvider.getDualGovernanceConfig().minAssetsLockDuration; /// @dev minAssetsLockDuration is stored as a storage variable in the Signalling Escrow instance. @@ -320,10 +317,10 @@ library DualGovernanceStateMachine { function _deployNewSignallingEscrow( Context storage self, - IEscrow escrowMasterCopy, + IEscrowBase escrowMasterCopy, Duration minAssetsLockDuration ) private { - IEscrow newSignallingEscrow = IEscrow(Clones.clone(address(escrowMasterCopy))); + ISignallingEscrow newSignallingEscrow = ISignallingEscrow(Clones.clone(address(escrowMasterCopy))); newSignallingEscrow.initialize(minAssetsLockDuration); self.signallingEscrow = newSignallingEscrow; emit NewSignallingEscrowDeployed(newSignallingEscrow); diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index be9205a8..edc465d1 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -6,6 +6,7 @@ import {Durations} from "contracts/types/Duration.sol"; import {Executor} from "contracts/Executor.sol"; import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; @@ -177,7 +178,7 @@ library DeployVerification { "Incorrect parameter MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT" ); - Escrow escrowTemplate = Escrow(payable(address(dg.ESCROW_MASTER_COPY()))); + Escrow escrowTemplate = Escrow(payable(address(IEscrowBase(dg.getVetoSignallingEscrow()).ESCROW_MASTER_COPY()))); require(escrowTemplate.DUAL_GOVERNANCE() == dg, "Escrow has incorrect DualGovernance address"); require(escrowTemplate.ST_ETH() == lidoAddresses.stETH, "Escrow has incorrect StETH address"); require(escrowTemplate.WST_ETH() == lidoAddresses.wstETH, "Escrow has incorrect WstETH address"); diff --git a/test/mocks/EscrowMock.sol b/test/mocks/EscrowMock.sol index d87c5965..2c9c38e1 100644 --- a/test/mocks/EscrowMock.sol +++ b/test/mocks/EscrowMock.sol @@ -5,113 +5,119 @@ import {Duration} from "contracts/types/Duration.sol"; import {PercentD16} from "contracts/types/PercentD16.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; -import {IEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "contracts/interfaces/IEscrow.sol"; +import {State as EscrowState} from "contracts/libraries/EscrowState.sol"; + +contract EscrowMock is ISignallingEscrow, IRageQuitEscrow { + IEscrowBase public immutable ESCROW_MASTER_COPY = this; -/* solhint-disable custom-errors */ -contract EscrowMock is IEscrow { event __RageQuitStarted(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock); Duration public __minAssetsLockDuration; PercentD16 public __rageQuitSupport; bool public __isRageQuitFinalized; - function __setRageQuitSupport(PercentD16 newRageQuitSupport) external { - __rageQuitSupport = newRageQuitSupport; - } - - function __setIsRageQuitFinalized(bool newIsRageQuitFinalized) external { - __isRageQuitFinalized = newIsRageQuitFinalized; - } - function initialize(Duration minAssetsLockDuration) external { __minAssetsLockDuration = minAssetsLockDuration; } - function lockStETH(uint256 /* amount */ ) external returns (uint256 /* lockedStETHShares */ ) { + function getEscrowState() external view returns (EscrowState) { revert("Not implemented"); } - function unlockStETH() external returns (uint256 /* unlockedStETHShares */ ) { + function getVetoerState(address vetoer) external view returns (VetoerState memory) { revert("Not implemented"); } - function lockWstETH(uint256 /* amount */ ) external returns (uint256 /* lockedStETHShares */ ) { + // --- + // Signalling Escrow Methods + // --- + + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { revert("Not implemented"); } - function unlockWstETH() external returns (uint256 /* unlockedStETHShares */ ) { + function unlockStETH() external returns (uint256 unlockedStETHShares) { revert("Not implemented"); } - function lockUnstETH(uint256[] memory /* unstETHIds */ ) external { + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { revert("Not implemented"); } - function unlockUnstETH(uint256[] memory /* unstETHIds */ ) external { + function unlockWstETH() external returns (uint256 wstETHUnlocked) { revert("Not implemented"); } - function markUnstETHFinalized(uint256[] memory, /* unstETHIds */ uint256[] calldata /* hints */ ) external { + function lockUnstETH(uint256[] memory unstETHIds) external { revert("Not implemented"); } - function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitEthWithdrawalsDelay) external { - emit __RageQuitStarted(rageQuitExtensionPeriodDuration, rageQuitEthWithdrawalsDelay); + function unlockUnstETH(uint256[] memory unstETHIds) external { + revert("Not implemented"); } - function requestNextWithdrawalsBatch(uint256 /* batchSize */ ) external { + function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external { revert("Not implemented"); } - function claimNextWithdrawalsBatch(uint256, /* fromUnstETHId */ uint256[] calldata /* hints */ ) external { - revert("Not implemented"); + function startRageQuit( + Duration rageQuitExtraTimelock, + Duration rageQuitWithdrawalsTimelock + ) external returns (IRageQuitEscrow) { + emit __RageQuitStarted(rageQuitExtraTimelock, rageQuitWithdrawalsTimelock); + return this; } - function claimNextWithdrawalsBatch(uint256 /* maxUnstETHIdsCount */ ) external { - revert("Not implemented"); + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { + __minAssetsLockDuration = newMinAssetsLockDuration; } - function startRageQuitExtensionPeriod() external { - revert("Not implemented"); + function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport) { + return __rageQuitSupport; } - function claimUnstETH(uint256[] calldata, /* unstETHIds */ uint256[] calldata /* hints */ ) external { + function getMinAssetsLockDuration() external view returns (Duration) { revert("Not implemented"); } - function withdrawETH() external { + function getLockedUnstETHState(uint256 unstETHId) external view returns (LockedUnstETHState memory) { revert("Not implemented"); } - function withdrawETH(uint256[] calldata /* unstETHIds */ ) external { + function getSignallingEscrowState() external view returns (SignallingEscrowState memory) { revert("Not implemented"); } - function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory /* totals */ ) { + // --- + // Rage Quit Escrow + // --- + + function requestNextWithdrawalsBatch(uint256 batchSize) external { revert("Not implemented"); } - function getVetoerState(address /* vetoer */ ) external view returns (VetoerState memory /* state */ ) { + function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external { revert("Not implemented"); } - function getUnclaimedUnstETHIdsCount() external view returns (uint256) { + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { revert("Not implemented"); } - function getNextWithdrawalBatch(uint256 /* limit */ ) external view returns (uint256[] memory /* unstETHIds */ ) { + function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external { revert("Not implemented"); } - function isWithdrawalsBatchesClosed() external view returns (bool) { + function startRageQuitExtensionPeriod() external { revert("Not implemented"); } - function isRageQuitExtensionPeriodStarted() external view returns (bool) { + function withdrawETH() external { revert("Not implemented"); } - function getRageQuitExtensionPeriodStartedAt() external view returns (Timestamp) { + function withdrawETH(uint256[] calldata unstETHIds) external { revert("Not implemented"); } @@ -119,15 +125,23 @@ contract EscrowMock is IEscrow { return __isRageQuitFinalized; } - function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport) { - return __rageQuitSupport; + function getRageQuitEscrowState() external view returns (RageQuitEscrowState memory) { + revert("Not implemented"); } - function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { - __minAssetsLockDuration = newMinAssetsLockDuration; + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds) { + revert("Not implemented"); } - function getMinAssetsLockDuration() external view returns (Duration minAssetsLockDuration) { - return __minAssetsLockDuration; + // --- + // Mock methods + // --- + + function __setRageQuitSupport(PercentD16 newRageQuitSupport) external { + __rageQuitSupport = newRageQuitSupport; + } + + function __setIsRageQuitFinalized(bool newIsRageQuitFinalized) external { + __isRageQuitFinalized = newIsRageQuitFinalized; } } diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 093197de..737d4042 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -6,14 +6,15 @@ import {console} from "forge-std/Test.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {PercentsD16} from "contracts/types/PercentD16.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; - -import {IEscrow} from "contracts/interfaces/IEscrow.sol"; -import {Escrow, WithdrawalsBatchesQueue} from "contracts/Escrow.sol"; +import {WithdrawalsBatchesQueue} from "contracts/libraries/WithdrawalsBatchesQueue.sol"; import {AssetsAccounting, UnstETHRecordStatus} from "contracts/libraries/AssetsAccounting.sol"; +import {Escrow} from "contracts/Escrow.sol"; + import {ScenarioTestBlueprint, LidoUtils, console} from "../utils/scenario-test-blueprint.sol"; contract EscrowHappyPath is ScenarioTestBlueprint { @@ -220,10 +221,10 @@ contract EscrowHappyPath is ScenarioTestBlueprint { _lockUnstETH(_VETOER_1, unstETHIds); - IEscrow.VetoerState memory vetoerState = escrow.getVetoerState(_VETOER_1); + Escrow.VetoerState memory vetoerState = escrow.getVetoerState(_VETOER_1); assertEq(vetoerState.unstETHIdsCount, 2); - IEscrow.LockedAssetsTotals memory totals = escrow.getLockedAssetsTotals(); + IEscrowBase.LockedAssetsTotals memory totals = escrow.getLockedAssetsTotals(); assertEq(totals.unstETHFinalizedETH, 0); assertEq(totals.unstETHUnfinalizedShares, totalSharesLocked); diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 8de47811..07ee894c 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -26,7 +26,7 @@ import {IWstETH} from "contracts/interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; import {ITimelock} from "contracts/interfaces/ITimelock.sol"; import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; -import {IEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; import {UnitTest} from "test/utils/unit-test.sol"; import {StETHMock} from "test/mocks/StETHMock.sol"; @@ -152,7 +152,7 @@ contract DualGovernanceUnitTests is UnitTest { address predictedEscrowCopyAddress = computeAddress(predictedDualGovernanceAddress, 1); vm.expectEmit(); - emit DualGovernance.EscrowMasterCopyDeployed(IEscrow(predictedEscrowCopyAddress)); + emit DualGovernance.EscrowMasterCopyDeployed(IEscrowBase(predictedEscrowCopyAddress)); vm.expectEmit(); emit Resealer.ResealManagerSet(address(_RESEAL_MANAGER_STUB)); @@ -181,7 +181,10 @@ contract DualGovernanceUnitTests is UnitTest { assertEq(dualGovernanceLocal.MIN_TIEBREAKER_ACTIVATION_TIMEOUT(), minTiebreakerActivationTimeout); assertEq(dualGovernanceLocal.MAX_TIEBREAKER_ACTIVATION_TIMEOUT(), maxTiebreakerActivationTimeout); assertEq(dualGovernanceLocal.MAX_SEALABLE_WITHDRAWAL_BLOCKERS_COUNT(), maxSealableWithdrawalBlockersCount); - assertEq(address(dualGovernanceLocal.ESCROW_MASTER_COPY()), predictedEscrowCopyAddress); + assertEq( + address(IEscrowBase(dualGovernanceLocal.getVetoSignallingEscrow()).ESCROW_MASTER_COPY()), + predictedEscrowCopyAddress + ); } // --- diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 3377885f..2fed86f2 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {IEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "contracts/interfaces/IEscrow.sol"; import {Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; @@ -46,9 +46,11 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { DualGovernanceStateMachine.Context private _stateMachine; function setUp() external { - _stateMachine.initialize(_CONFIG_PROVIDER, IEscrow(_ESCROW_MASTER_COPY_MOCK)); + _stateMachine.initialize(_CONFIG_PROVIDER, IEscrowBase(_ESCROW_MASTER_COPY_MOCK)); _mockRageQuitFinalized(false); _mockRageQuitSupport(PercentsD16.from(0)); + _mockEscrowMasterCopy(); + _mockStartRageQuit(); } function test_initialize_RevertOn_ReInitialization() external { @@ -448,20 +450,40 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { // Test helper methods // --- + function _mockEscrowMasterCopy() internal { + vm.mockCall( + _ESCROW_MASTER_COPY_MOCK, + abi.encodeWithSelector(IEscrowBase.ESCROW_MASTER_COPY.selector), + abi.encode(_ESCROW_MASTER_COPY_MOCK) + ); + } + + function _mockStartRageQuit() internal { + vm.mockCall( + _ESCROW_MASTER_COPY_MOCK, + abi.encodeWithSelector(ISignallingEscrow.startRageQuit.selector), + abi.encode(_stateMachine.signallingEscrow) + ); + } + function _mockRageQuitSupport(PercentD16 rageQuitSupport) internal { vm.mockCall( - _ESCROW_MASTER_COPY_MOCK, abi.encodeCall(IEscrow.getRageQuitSupport, ()), abi.encode(rageQuitSupport) + _ESCROW_MASTER_COPY_MOCK, + abi.encodeCall(ISignallingEscrow.getRageQuitSupport, ()), + abi.encode(rageQuitSupport) ); } function _mockRageQuitFinalized(bool isRageQuitFinalized) internal { vm.mockCall( - _ESCROW_MASTER_COPY_MOCK, abi.encodeCall(IEscrow.isRageQuitFinalized, ()), abi.encode(isRageQuitFinalized) + _ESCROW_MASTER_COPY_MOCK, + abi.encodeCall(IRageQuitEscrow.isRageQuitFinalized, ()), + abi.encode(isRageQuitFinalized) ); } function _activateNextState() internal { - _stateMachine.activateNextState(IEscrow(_ESCROW_MASTER_COPY_MOCK)); + _stateMachine.activateNextState(); } function _assertState(State persisted, State effective) internal { @@ -509,6 +531,6 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { } function external__initialize() external { - _stateMachine.initialize(_CONFIG_PROVIDER, IEscrow(_ESCROW_MASTER_COPY_MOCK)); + _stateMachine.initialize(_CONFIG_PROVIDER, IEscrowBase(_ESCROW_MASTER_COPY_MOCK)); } } diff --git a/test/unit/libraries/DualGovernanceStateTransitions.t.sol b/test/unit/libraries/DualGovernanceStateTransitions.t.sol index a374c919..f04dc41e 100644 --- a/test/unit/libraries/DualGovernanceStateTransitions.t.sol +++ b/test/unit/libraries/DualGovernanceStateTransitions.t.sol @@ -5,7 +5,7 @@ import {Duration, Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; -import {IEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "contracts/interfaces/IEscrow.sol"; import { State, @@ -50,7 +50,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { rageQuitEthWithdrawalsDelayGrowth: Durations.from(15 days) }) ); - DualGovernanceStateMachine.initialize(_stateMachine, _configProvider, IEscrow(_escrowMasterCopyMock)); + DualGovernanceStateMachine.initialize(_stateMachine, _configProvider, IEscrowBase(_escrowMasterCopyMock)); _setMockRageQuitSupportInBP(0); } @@ -353,13 +353,13 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { function _setupRageQuitState() internal { _stateMachine.state = State.RageQuit; _stateMachine.enteredAt = Timestamps.now(); - _stateMachine.rageQuitEscrow = IEscrow(_escrowMasterCopyMock); + _stateMachine.rageQuitEscrow = IRageQuitEscrow(_escrowMasterCopyMock); } function _setMockRageQuitSupportInBP(uint256 bpValue) internal { vm.mockCall( address(_stateMachine.signallingEscrow), - abi.encodeWithSelector(IEscrow.getRageQuitSupport.selector), + abi.encodeWithSelector(ISignallingEscrow.getRageQuitSupport.selector), abi.encode(PercentsD16.fromBasisPoints(bpValue)) ); } @@ -367,7 +367,7 @@ contract DualGovernanceStateTransitionsUnitTestSuite is UnitTest { function _setMockIsRageQuitFinalized(bool isRageQuitFinalized) internal { vm.mockCall( address(_stateMachine.rageQuitEscrow), - abi.encodeWithSelector(IEscrow.isRageQuitFinalized.selector), + abi.encodeWithSelector(IRageQuitEscrow.isRageQuitFinalized.selector), abi.encode(isRageQuitFinalized) ); } diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index 864cf88c..bddce0cc 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -12,7 +12,7 @@ import {PercentD16} from "contracts/types/PercentD16.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {IEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; import {Escrow} from "contracts/Escrow.sol"; // --- @@ -199,7 +199,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { function _unlockWstETH(address vetoer) internal { Escrow escrow = _getVetoSignallingEscrow(); uint256 wstETHBalanceBefore = _lido.wstETH.balanceOf(vetoer); - IEscrow.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + IEscrowBase.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); vm.startPrank(vetoer); uint256 wstETHUnlocked = escrow.unlockWstETH(); @@ -213,8 +213,8 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { function _lockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - IEscrow.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); - IEscrow.LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); + IEscrowBase.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); uint256 unstETHTotalSharesLocked = 0; IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = @@ -233,10 +233,10 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), address(escrow)); } - IEscrow.VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); + IEscrowBase.VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount + unstETHIds.length); - IEscrow.LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); assertEq( lockedAssetsTotalsAfter.unstETHUnfinalizedShares, lockedAssetsTotalsBefore.unstETHUnfinalizedShares + unstETHTotalSharesLocked @@ -245,8 +245,8 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { function _unlockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - IEscrow.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); - IEscrow.LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); + IEscrowBase.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); uint256 unstETHTotalSharesUnlocked = 0; IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = @@ -263,11 +263,11 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), vetoer); } - IEscrow.VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); + IEscrowBase.VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount - unstETHIds.length); // TODO: implement correct assert. It must consider was unstETH finalized or not - IEscrow.LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); assertEq( lockedAssetsTotalsAfter.unstETHUnfinalizedShares, lockedAssetsTotalsBefore.unstETHUnfinalizedShares - unstETHTotalSharesUnlocked From 4cc7230a7f2cea62154dc95669c82542d8f4278f Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 5 Dec 2024 03:42:24 +0400 Subject: [PATCH 2/8] Implement getters logic. Split IEscrow file. --- contracts/DualGovernance.sol | 2 +- contracts/Escrow.sol | 218 ++++++++++-------- contracts/interfaces/IEscrow.sol | 112 --------- contracts/interfaces/IEscrowBase.sol | 14 ++ contracts/interfaces/IRageQuitEscrow.sol | 34 +++ contracts/interfaces/ISignallingEscrow.sol | 65 ++++++ .../libraries/DualGovernanceStateMachine.sol | 4 +- scripts/deploy/DeployVerification.sol | 2 +- test/mocks/EscrowMock.sol | 22 +- test/scenario/dg-update-tokens-rotation.t.sol | 4 +- test/scenario/escrow.t.sol | 34 +-- test/scenario/veto-cooldown-mechanics.t.sol | 4 +- test/unit/DualGovernance.t.sol | 2 +- test/unit/Escrow.t.sol | 22 +- .../DualGovernanceStateMachine.t.sol | 4 +- .../DualGovernanceStateTransitions.t.sol | 4 +- test/utils/scenario-test-blueprint.sol | 42 ++-- 17 files changed, 310 insertions(+), 279 deletions(-) delete mode 100644 contracts/interfaces/IEscrow.sol create mode 100644 contracts/interfaces/IEscrowBase.sol create mode 100644 contracts/interfaces/IRageQuitEscrow.sol create mode 100644 contracts/interfaces/ISignallingEscrow.sol diff --git a/contracts/DualGovernance.sol b/contracts/DualGovernance.sol index f87982f5..d5f1fb96 100644 --- a/contracts/DualGovernance.sol +++ b/contracts/DualGovernance.sol @@ -7,7 +7,7 @@ import {Timestamp} from "./types/Timestamp.sol"; import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; -import {IEscrowBase} from "./interfaces/IEscrow.sol"; +import {IEscrowBase} from "./interfaces/IEscrowBase.sol"; import {ITimelock} from "./interfaces/ITimelock.sol"; import {ITiebreaker} from "./interfaces/ITiebreaker.sol"; import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 9f493de8..c7deb5d5 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -4,12 +4,14 @@ pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Duration} from "./types/Duration.sol"; -import {Timestamp} from "./types/Timestamp.sol"; import {ETHValue, ETHValues} from "./types/ETHValue.sol"; import {SharesValue, SharesValues} from "./types/SharesValue.sol"; import {PercentD16, PercentsD16} from "./types/PercentD16.sol"; -import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "./interfaces/IEscrow.sol"; +import {IEscrowBase} from "./interfaces/IEscrowBase.sol"; +import {ISignallingEscrow} from "./interfaces/ISignallingEscrow.sol"; +import {IRageQuitEscrow} from "./interfaces/IRageQuitEscrow.sol"; + import {IStETH} from "./interfaces/IStETH.sol"; import {IWstETH} from "./interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "./interfaces/IWithdrawalQueue.sol"; @@ -17,7 +19,13 @@ import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; import {EscrowState, State} from "./libraries/EscrowState.sol"; import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalsBatchesQueue.sol"; -import {HolderAssets, StETHAccounting, UnstETHAccounting, AssetsAccounting} from "./libraries/AssetsAccounting.sol"; +import { + HolderAssets, + UnstETHRecord, + StETHAccounting, + UnstETHAccounting, + AssetsAccounting +} from "./libraries/AssetsAccounting.sol"; /// @notice This contract is used to accumulate stETH, wstETH, unstETH, and withdrawn ETH from vetoers during the /// veto signalling and rage quit processes. @@ -116,6 +124,10 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { MIN_WITHDRAWALS_BATCH_SIZE = minWithdrawalsBatchSize; } + // --- + // Escrow Base + // --- + /// @notice Initializes the proxy instance with the specified minimum assets lock duration. /// @param minAssetsLockDuration The minimum duration that must pass from the last stETH, wstETH, or unstETH lock /// by the vetoer before they are allowed to unlock assets from the Escrow. @@ -131,15 +143,12 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); } - // --- - // Base Escrow Getters - // --- function getEscrowState() external view returns (State) { return _escrowState.state; } // --- - // Lock & Unlock stETH + // Signalling Escrow: Lock & Unlock stETH // --- /// @notice Locks the vetoer's specified `amount` of stETH in the Veto Signalling Escrow, thereby increasing @@ -206,7 +215,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Lock & Unlock unstETH + // Signalling Escrow: Lock & Unlock unstETH // --- /// @notice Locks the specified unstETH NFTs, identified by their ids, in the Veto Signalling Escrow, thereby increasing @@ -270,7 +279,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Start Rage Quit + // Signalling Escrow: Start Rage Quit // --- /// @notice Irreversibly converts the Signalling Escrow into the Rage Quit Escrow, allowing vetoers who have locked @@ -292,7 +301,19 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Signalling Escrow Getters + // Signalling Escrow: Management + // --- + + /// @notice Sets the minimum duration that must elapse after the last stETH, wstETH, or unstETH lock + /// by a vetoer before they are permitted to unlock their assets from the Escrow. + /// @param newMinAssetsLockDuration The new minimum lock duration to be set. + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { + _checkCallerIsDualGovernance(); + _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration); + } + + // --- + // Signalling Escrow: Getters // --- /// @notice Returns the current Rage Quit support value as a percentage. @@ -316,14 +337,74 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { minAssetsLockDuration = _escrowState.minAssetsLockDuration; } - // TODO: implement - function getLockedUnstETHState(uint256 unstETHId) external view returns (LockedUnstETHState memory) {} + /// @notice Returns the state of locked assets for a specific vetoer. + /// @param vetoer The address of the vetoer whose locked asset state is being queried. + /// @return details A struct containing information about the vetoer's locked assets, including: + /// - `unstETHIdsCount`: The total number of unstETH NFTs locked by the vetoer. + /// - `stETHLockedShares`: The total number of stETH shares locked by the vetoer. + /// - `unstETHLockedShares`: The total number of unstETH shares locked by the vetoer. + /// - `lastAssetsLockTimestamp`: The timestamp of the last assets lock by the vetoer. + function getVetoerDetails(address vetoer) external view returns (VetoerDetails memory details) { + HolderAssets storage assets = _accounting.assets[vetoer]; - // TODO: implement - function getSignallingEscrowState() external view returns (SignallingEscrowState memory) {} + details.unstETHIdsCount = assets.unstETHIds.length; + details.stETHLockedShares = assets.stETHLockedShares; + details.unstETHLockedShares = assets.unstETHLockedShares; + details.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp; + } + + // @notice Retrieves the unstETH NFT ids of the specified vetoer. + /// @param vetoer The address of the vetoer whose unstETH NFTs are being queried. + /// @return unstETHIds An array of unstETH NFT ids locked by the vetoer. + function getVetoerUnstETHIds(address vetoer) external view returns (uint256[] memory unstETHIds) { + unstETHIds = _accounting.assets[vetoer].unstETHIds; + } + + /// @notice Returns the total amounts of locked and claimed assets in the Escrow. + /// @return details A struct containing the total amounts of locked and claimed assets, including: + /// - `totalStETHClaimedETH`: The total amount of ETH claimed from locked stETH. + /// - `totalStETHLockedShares`: The total number of stETH shares currently locked in the Escrow. + /// - `totalUnstETHUnfinalizedShares`: The total number of shares from unstETH NFTs that have not yet been finalized. + /// - `totalUnstETHFinalizedETH`: The total amount of ETH from finalized unstETH NFTs. + function getSignallingEscrowDetails() external view returns (SignallingEscrowDetails memory details) { + StETHAccounting memory stETHTotals = _accounting.stETHTotals; + details.totalStETHClaimedETH = stETHTotals.claimedETH; + details.totalStETHLockedShares = stETHTotals.lockedShares; + + UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; + details.totalUnstETHUnfinalizedShares = unstETHTotals.unfinalizedShares; + details.totalUnstETHFinalizedETH = unstETHTotals.finalizedETH; + } + + /// @notice Retrieves details of locked unstETH records for the given ids. + /// @param unstETHIds The array of ids for the unstETH records to retrieve. + /// @return unstETHDetails An array of `LockedUnstETHDetails` containing the details for each provided unstETH id. + /// + /// The details include: + /// - `status`: The current status of the unstETH record. + /// - `lockedBy`: The address that locked the unstETH record. + /// - `shares`: The number of shares associated with the locked unstETH. + /// - `claimableAmount`: The amount of claimable ETH contained in the unstETH. This value is 0 + /// until the unstETH is finalized or claimed. + function getLockedUnstETHDetails(uint256[] calldata unstETHIds) + external + view + returns (LockedUnstETHDetails[] memory unstETHDetails) + { + unstETHDetails = new LockedUnstETHDetails[](unstETHIds.length); + + for (uint256 i = 0; i < unstETHIds.length; ++i) { + UnstETHRecord memory unstETHRecord = _accounting.unstETHRecords[unstETHIds[i]]; + + unstETHDetails[i].status = unstETHRecord.status; + unstETHDetails[i].lockedBy = unstETHRecord.lockedBy; + unstETHDetails[i].shares = unstETHRecord.shares; + unstETHDetails[i].claimableAmount = unstETHRecord.claimableAmount; + } + } // --- - // Request Withdrawal Batches + // Rage Quit Escrow: Request Withdrawal Batches // --- /// @notice Creates unstETH NFTs from the stETH held in the Rage Quit Escrow via the WithdrawalQueue contract. @@ -367,7 +448,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Claim Requested Withdrawal Batches + // Rage Quit Escrow: Claim Requested Withdrawal Batches // --- /// @notice Allows the claim of finalized withdrawal NFTs generated via the `Escrow.requestNextWithdrawalsBatch()` method. @@ -403,7 +484,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Start Rage Quit Extension Delay + // Rage Quit Escrow: Start Rage Quit Extension Delay // --- /// @notice Initiates the Rage Quit Extension Period once all withdrawal batches have been claimed. @@ -436,7 +517,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Claim Locked unstETH NFTs + // Rage Quit Escrow: Claim Locked unstETH NFTs // --- /// @notice Allows users to claim finalized unstETH NFTs locked in the Rage Quit Escrow contract. @@ -460,19 +541,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Escrow Management - // --- - - /// @notice Sets the minimum duration that must elapse after the last stETH, wstETH, or unstETH lock - /// by a vetoer before they are permitted to unlock their assets from the Escrow. - /// @param newMinAssetsLockDuration The new minimum lock duration to be set. - function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { - _checkCallerIsDualGovernance(); - _escrowState.setMinAssetsLockDuration(newMinAssetsLockDuration); - } - - // --- - // Withdraw Logic + // Rage Quit Escrow: Withdraw Logic // --- /// @notice Allows the caller (i.e., `msg.sender`) to withdraw all stETH and wstETH they have previously locked @@ -502,53 +571,10 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { // Rage Quit Escrow Getters // --- - // TODO: implement - function getRageQuitEscrowState() external view returns (RageQuitEscrowState memory) {} - - /// @notice Returns the total amounts of locked and claimed assets in the Escrow. - /// @return totals A struct containing the total amounts of locked and claimed assets, including: - /// - `stETHClaimedETH`: The total amount of ETH claimed from locked stETH. - /// - `stETHLockedShares`: The total number of stETH shares currently locked in the Escrow. - /// - `unstETHUnfinalizedShares`: The total number of shares from unstETH NFTs that have not yet been finalized. - /// - `unstETHFinalizedETH`: The total amount of ETH from finalized unstETH NFTs. - function getLockedAssetsTotals() external view returns (LockedAssetsTotals memory totals) { - StETHAccounting memory stETHTotals = _accounting.stETHTotals; - totals.stETHClaimedETH = stETHTotals.claimedETH.toUint256(); - totals.stETHLockedShares = stETHTotals.lockedShares.toUint256(); - - UnstETHAccounting memory unstETHTotals = _accounting.unstETHTotals; - totals.unstETHUnfinalizedShares = unstETHTotals.unfinalizedShares.toUint256(); - totals.unstETHFinalizedETH = unstETHTotals.finalizedETH.toUint256(); - } - - /// @notice Returns the state of locked assets for a specific vetoer. - /// @param vetoer The address of the vetoer whose locked asset state is being queried. - /// @return state A struct containing information about the vetoer's locked assets, including: - /// - `stETHLockedShares`: The total number of stETH shares locked by the vetoer. - /// - `unstETHLockedShares`: The total number of unstETH shares locked by the vetoer. - /// - `unstETHIdsCount`: The total number of unstETH NFTs locked by the vetoer. - /// - `lastAssetsLockTimestamp`: The timestamp of the last assets lock by the vetoer. - function getVetoerState(address vetoer) external view returns (VetoerState memory state) { - HolderAssets storage assets = _accounting.assets[vetoer]; - - state.unstETHIdsCount = assets.unstETHIds.length; - state.stETHLockedShares = assets.stETHLockedShares.toUint256(); - state.unstETHLockedShares = assets.unstETHLockedShares.toUint256(); - state.lastAssetsLockTimestamp = assets.lastAssetsLockTimestamp.toSeconds(); - } - - // @notice Retrieves the unstETH NFT ids of the specified vetoer. - /// @param vetoer The address of the vetoer whose unstETH NFTs are being queried. - /// @return unstETHIds An array of unstETH NFT ids locked by the vetoer. - function getVetoerUnstETHIds(address vetoer) external view returns (uint256[] memory unstETHIds) { - unstETHIds = _accounting.assets[vetoer].unstETHIds; - } - - /// @notice Returns the total count of unstETH NFTs that have not been claimed yet. - /// @return unclaimedUnstETHIdsCount The total number of unclaimed unstETH NFTs. - function getUnclaimedUnstETHIdsCount() external view returns (uint256) { - _escrowState.checkRageQuitEscrow(); - return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + /// @notice Returns whether the Rage Quit process has been finalized. + /// @return A boolean value indicating whether the Rage Quit process has been finalized (`true`) or not (`false`). + function isRageQuitFinalized() external view returns (bool) { + return _escrowState.isRageQuitEscrow() && _escrowState.isRageQuitExtensionPeriodPassed(); } /// @notice Retrieves the unstETH NFT ids of the next batch available for claiming. @@ -559,31 +585,25 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { unstETHIds = _batchesQueue.getNextWithdrawalsBatches(limit); } - /// @notice Returns whether all withdrawal batches have been closed. - /// @return isWithdrawalsBatchesClosed A boolean value indicating whether all withdrawal batches have been - /// closed (`true`) or not (`false`). - function isWithdrawalsBatchesClosed() external view returns (bool) { + /// @notice Retrieves details about the current state of the rage quit escrow. + /// @return details A `RageQuitEscrowDetails` struct containing the following fields: + /// - `isWithdrawalsBatchesClosed`: Indicates whether the withdrawals batches are closed. + /// - `isRageQuitExtensionPeriodStarted`: Indicates whether the rage quit extension period has started. + /// - `unclaimedUnstETHIdsCount`: The total count of unstETH NFTs that have not been claimed yet. + /// - `rageQuitEthWithdrawalsDelay`: The delay period for ETH withdrawals during rage quit. + /// - `rageQuitExtensionPeriodDuration`: The duration of the rage quit extension period. + /// - `rageQuitExtensionPeriodStartedAt`: The timestamp when the rage quit extension period started. + function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory details) { _escrowState.checkRageQuitEscrow(); - return _batchesQueue.isClosed(); - } - /// @notice Returns whether the Rage Quit Extension Period has started. - /// @return isRageQuitExtensionPeriodStarted A boolean value indicating whether the Rage Quit Extension Period - /// has started (`true`) or not (`false`). - function isRageQuitExtensionPeriodStarted() external view returns (bool) { - return _escrowState.isRageQuitExtensionPeriodStarted(); - } + details.isWithdrawalsBatchesClosed = _batchesQueue.isClosed(); + details.isRageQuitExtensionPeriodStarted = _escrowState.isRageQuitExtensionPeriodStarted(); - /// @notice Returns the timestamp when the Rage Quit Extension Period started. - /// @return rageQuitExtensionPeriodStartedAt The timestamp when the Rage Quit Extension Period began. - function getRageQuitExtensionPeriodStartedAt() external view returns (Timestamp) { - return _escrowState.rageQuitExtensionPeriodStartedAt; - } + details.unclaimedUnstETHIdsCount = _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); - /// @notice Returns whether the Rage Quit process has been finalized. - /// @return A boolean value indicating whether the Rage Quit process has been finalized (`true`) or not (`false`). - function isRageQuitFinalized() external view returns (bool) { - return _escrowState.isRageQuitEscrow() && _escrowState.isRageQuitExtensionPeriodPassed(); + details.rageQuitEthWithdrawalsDelay = _escrowState.rageQuitEthWithdrawalsDelay; + details.rageQuitExtensionPeriodDuration = _escrowState.rageQuitExtensionPeriodDuration; + details.rageQuitExtensionPeriodStartedAt = _escrowState.rageQuitExtensionPeriodStartedAt; } // --- diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol deleted file mode 100644 index 7c72b64e..00000000 --- a/contracts/interfaces/IEscrow.sol +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Duration} from "../types/Duration.sol"; -import {Timestamp} from "../types/Timestamp.sol"; -import {PercentD16} from "../types/PercentD16.sol"; -import {Timestamp} from "../types/Timestamp.sol"; - -import {ETHValue} from "../types/ETHValue.sol"; -import {SharesValue} from "../types/SharesValue.sol"; - -import {State as EscrowState} from "../libraries/EscrowState.sol"; -import {UnstETHRecordStatus} from "../libraries/AssetsAccounting.sol"; - -interface IEscrowBase { - struct VetoerState { - uint256 stETHLockedShares; - uint256 unstETHLockedShares; - uint256 unstETHIdsCount; - uint256 lastAssetsLockTimestamp; - } - - /// @notice Summary of the total locked assets in the Escrow. - /// @param stETHLockedShares The total number of stETH shares currently locked in the Escrow. - /// @param stETHClaimedETH The total amount of ETH claimed from the stETH shares locked in the Escrow. - /// @param unstETHUnfinalizedShares The total number of shares from unstETH NFTs that have not yet been marked as finalized. - /// @param unstETHFinalizedETH The total amount of ETH claimable from unstETH NFTs that have been marked as finalized. - /// TODO: Remove and use LockedUnstETHState instead - struct LockedAssetsTotals { - uint256 stETHLockedShares; - uint256 stETHClaimedETH; - uint256 unstETHUnfinalizedShares; - uint256 unstETHFinalizedETH; - } - - // TODO: add standalone getter - function ESCROW_MASTER_COPY() external view returns (IEscrowBase); - - function initialize(Duration minAssetsLockDuration) external; - - function getEscrowState() external view returns (EscrowState); - function getVetoerState(address vetoer) external view returns (VetoerState memory); -} - -interface ISignallingEscrow is IEscrowBase { - struct LockedUnstETHState { - UnstETHRecordStatus status; - address lockedBy; - SharesValue shares; - ETHValue claimableAmount; - } - - struct SignallingEscrowState { - PercentD16 rageQuitSupport; - // - ETHValue totalStETHClaimedETH; - SharesValue totalStETHLockedShares; - // - ETHValue totalUnstETHFinalizedETH; - SharesValue totalUnstETHUnfinalizedShares; - } - - function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares); - function unlockStETH() external returns (uint256 unlockedStETHShares); - - function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares); - function unlockWstETH() external returns (uint256 wstETHUnlocked); - - function lockUnstETH(uint256[] memory unstETHIds) external; - function unlockUnstETH(uint256[] memory unstETHIds) external; - - function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external; - - function startRageQuit( - Duration rageQuitExtensionPeriodDuration, - Duration rageQuitEthWithdrawalsDelay - ) external returns (IRageQuitEscrow); - - function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; - - function getRageQuitSupport() external view returns (PercentD16); - function getMinAssetsLockDuration() external view returns (Duration); - function getLockedUnstETHState(uint256 unstETHId) external view returns (LockedUnstETHState memory); - function getSignallingEscrowState() external view returns (SignallingEscrowState memory); -} - -interface IRageQuitEscrow is IEscrowBase { - struct RageQuitEscrowState { - bool isRageQuitFinalized; - bool isWithdrawalsBatchesClosed; - bool isRageQuitExtensionPeriodStarted; - uint256 unclaimedUnstETHIdsCount; - Duration rageQuitWithdrawalsDelay; - Duration rageQuitExtensionPeriodDuration; - Timestamp rageQuitExtensionPeriodStartedAt; - } - - function requestNextWithdrawalsBatch(uint256 batchSize) external; - - function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external; - function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external; - function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external; - - function startRageQuitExtensionPeriod() external; - - function withdrawETH() external; - function withdrawETH(uint256[] calldata unstETHIds) external; - - function isRageQuitFinalized() external view returns (bool); - function getRageQuitEscrowState() external view returns (RageQuitEscrowState memory); - function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds); -} diff --git a/contracts/interfaces/IEscrowBase.sol b/contracts/interfaces/IEscrowBase.sol new file mode 100644 index 00000000..c1e8f22e --- /dev/null +++ b/contracts/interfaces/IEscrowBase.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; + +import {State} from "../libraries/EscrowState.sol"; + +interface IEscrowBase { + function ESCROW_MASTER_COPY() external view returns (IEscrowBase); + + function initialize(Duration minAssetsLockDuration) external; + + function getEscrowState() external view returns (State); +} diff --git a/contracts/interfaces/IRageQuitEscrow.sol b/contracts/interfaces/IRageQuitEscrow.sol new file mode 100644 index 00000000..d31b6c2e --- /dev/null +++ b/contracts/interfaces/IRageQuitEscrow.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Duration} from "../types/Duration.sol"; +import {Timestamp} from "../types/Timestamp.sol"; + +import {IEscrowBase} from "./IEscrowBase.sol"; + +interface IRageQuitEscrow is IEscrowBase { + struct RageQuitEscrowDetails { + bool isRageQuitFinalized; + bool isWithdrawalsBatchesClosed; + bool isRageQuitExtensionPeriodStarted; + uint256 unclaimedUnstETHIdsCount; + Duration rageQuitEthWithdrawalsDelay; + Duration rageQuitExtensionPeriodDuration; + Timestamp rageQuitExtensionPeriodStartedAt; + } + + function requestNextWithdrawalsBatch(uint256 batchSize) external; + + function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external; + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external; + function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external; + + function startRageQuitExtensionPeriod() external; + + function withdrawETH() external; + function withdrawETH(uint256[] calldata unstETHIds) external; + + function isRageQuitFinalized() external view returns (bool); + function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds); + function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory); +} diff --git a/contracts/interfaces/ISignallingEscrow.sol b/contracts/interfaces/ISignallingEscrow.sol new file mode 100644 index 00000000..e1f0275b --- /dev/null +++ b/contracts/interfaces/ISignallingEscrow.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ETHValue} from "../types/ETHValue.sol"; +import {Duration} from "../types/Duration.sol"; +import {Timestamp} from "../types/Timestamp.sol"; +import {PercentD16} from "../types/PercentD16.sol"; +import {SharesValue} from "../types/SharesValue.sol"; +import {UnstETHRecordStatus} from "../libraries/AssetsAccounting.sol"; + +import {IEscrowBase} from "./IEscrowBase.sol"; +import {IRageQuitEscrow} from "./IRageQuitEscrow.sol"; + +interface ISignallingEscrow is IEscrowBase { + struct VetoerDetails { + uint256 unstETHIdsCount; + SharesValue stETHLockedShares; + SharesValue unstETHLockedShares; + Timestamp lastAssetsLockTimestamp; + } + + struct LockedUnstETHDetails { + UnstETHRecordStatus status; + address lockedBy; + SharesValue shares; + ETHValue claimableAmount; + } + + struct SignallingEscrowDetails { + SharesValue totalStETHLockedShares; + ETHValue totalStETHClaimedETH; + SharesValue totalUnstETHUnfinalizedShares; + ETHValue totalUnstETHFinalizedETH; + } + + function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares); + function unlockStETH() external returns (uint256 unlockedStETHShares); + + function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares); + function unlockWstETH() external returns (uint256 wstETHUnlocked); + + function lockUnstETH(uint256[] memory unstETHIds) external; + function unlockUnstETH(uint256[] memory unstETHIds) external; + + function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external; + + function startRageQuit( + Duration rageQuitExtensionPeriodDuration, + Duration rageQuitEthWithdrawalsDelay + ) external returns (IRageQuitEscrow); + + function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; + + function getRageQuitSupport() external view returns (PercentD16); + function getMinAssetsLockDuration() external view returns (Duration); + + function getVetoerDetails(address vetoer) external view returns (VetoerDetails memory); + function getVetoerUnstETHIds(address vetoer) external view returns (uint256[] memory); + function getSignallingEscrowDetails() external view returns (SignallingEscrowDetails memory); + + function getLockedUnstETHDetails(uint256[] calldata unstETHIds) + external + view + returns (LockedUnstETHDetails[] memory); +} diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index 11bb6457..c1fdb083 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -7,7 +7,9 @@ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Duration} from "../types/Duration.sol"; import {Timestamp, Timestamps} from "../types/Timestamp.sol"; -import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "../interfaces/IEscrow.sol"; +import {IEscrowBase} from "../interfaces/IEscrowBase.sol"; +import {ISignallingEscrow} from "../interfaces/ISignallingEscrow.sol"; +import {IRageQuitEscrow} from "../interfaces/IRageQuitEscrow.sol"; import {IDualGovernance} from "../interfaces/IDualGovernance.sol"; import {IDualGovernanceConfigProvider} from "../interfaces/IDualGovernanceConfigProvider.sol"; diff --git a/scripts/deploy/DeployVerification.sol b/scripts/deploy/DeployVerification.sol index edc465d1..c0830070 100644 --- a/scripts/deploy/DeployVerification.sol +++ b/scripts/deploy/DeployVerification.sol @@ -6,7 +6,7 @@ import {Durations} from "contracts/types/Duration.sol"; import {Executor} from "contracts/Executor.sol"; import {IEmergencyProtectedTimelock} from "contracts/interfaces/IEmergencyProtectedTimelock.sol"; import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; -import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; import {TiebreakerCoreCommittee} from "contracts/committees/TiebreakerCoreCommittee.sol"; import {TiebreakerSubCommittee} from "contracts/committees/TiebreakerSubCommittee.sol"; import {TimelockedGovernance} from "contracts/TimelockedGovernance.sol"; diff --git a/test/mocks/EscrowMock.sol b/test/mocks/EscrowMock.sol index 2c9c38e1..dad55de7 100644 --- a/test/mocks/EscrowMock.sol +++ b/test/mocks/EscrowMock.sol @@ -3,9 +3,11 @@ pragma solidity 0.8.26; import {Duration} from "contracts/types/Duration.sol"; import {PercentD16} from "contracts/types/PercentD16.sol"; -import {Timestamp} from "contracts/types/Timestamp.sol"; -import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; +import {IRageQuitEscrow} from "contracts/interfaces/IRageQuitEscrow.sol"; + import {State as EscrowState} from "contracts/libraries/EscrowState.sol"; contract EscrowMock is ISignallingEscrow, IRageQuitEscrow { @@ -25,7 +27,11 @@ contract EscrowMock is ISignallingEscrow, IRageQuitEscrow { revert("Not implemented"); } - function getVetoerState(address vetoer) external view returns (VetoerState memory) { + function getVetoerDetails(address vetoer) external view returns (VetoerDetails memory) { + revert("Not implemented"); + } + + function getVetoerUnstETHIds(address vetoer) external view returns (uint256[] memory) { revert("Not implemented"); } @@ -81,11 +87,15 @@ contract EscrowMock is ISignallingEscrow, IRageQuitEscrow { revert("Not implemented"); } - function getLockedUnstETHState(uint256 unstETHId) external view returns (LockedUnstETHState memory) { + function getLockedUnstETHDetails(uint256[] calldata unstETHIds) + external + view + returns (LockedUnstETHDetails[] memory) + { revert("Not implemented"); } - function getSignallingEscrowState() external view returns (SignallingEscrowState memory) { + function getSignallingEscrowDetails() external view returns (SignallingEscrowDetails memory) { revert("Not implemented"); } @@ -125,7 +135,7 @@ contract EscrowMock is ISignallingEscrow, IRageQuitEscrow { return __isRageQuitFinalized; } - function getRageQuitEscrowState() external view returns (RageQuitEscrowState memory) { + function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory) { revert("Not implemented"); } diff --git a/test/scenario/dg-update-tokens-rotation.t.sol b/test/scenario/dg-update-tokens-rotation.t.sol index 97f98191..a415e13c 100644 --- a/test/scenario/dg-update-tokens-rotation.t.sol +++ b/test/scenario/dg-update-tokens-rotation.t.sol @@ -96,13 +96,13 @@ contract DualGovernanceUpdateTokensRotation is ScenarioTestBlueprint { // The Rage Quit may be finished in the previous DG instance so vetoers will not lose their funds by mistake Escrow rageQuitEscrow = Escrow(payable(_dualGovernance.getRageQuitEscrow())); - while (!rageQuitEscrow.isWithdrawalsBatchesClosed()) { + while (!rageQuitEscrow.getRageQuitEscrowDetails().isWithdrawalsBatchesClosed) { rageQuitEscrow.requestNextWithdrawalsBatch(96); } _finalizeWithdrawalQueue(); - while (rageQuitEscrow.getUnclaimedUnstETHIdsCount() > 0) { + while (rageQuitEscrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { rageQuitEscrow.claimNextWithdrawalsBatch(32); } diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 737d4042..01582c83 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -6,8 +6,8 @@ import {console} from "forge-std/Test.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {PercentsD16} from "contracts/types/PercentD16.sol"; -import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; import {WithdrawalsBatchesQueue} from "contracts/libraries/WithdrawalsBatchesQueue.sol"; @@ -221,22 +221,22 @@ contract EscrowHappyPath is ScenarioTestBlueprint { _lockUnstETH(_VETOER_1, unstETHIds); - Escrow.VetoerState memory vetoerState = escrow.getVetoerState(_VETOER_1); - assertEq(vetoerState.unstETHIdsCount, 2); + Escrow.VetoerDetails memory vetoerDetails = escrow.getVetoerDetails(_VETOER_1); + assertEq(vetoerDetails.unstETHIdsCount, 2); - IEscrowBase.LockedAssetsTotals memory totals = escrow.getLockedAssetsTotals(); - assertEq(totals.unstETHFinalizedETH, 0); - assertEq(totals.unstETHUnfinalizedShares, totalSharesLocked); + ISignallingEscrow.SignallingEscrowDetails memory escrowDetails = escrow.getSignallingEscrowDetails(); + assertEq(escrowDetails.totalUnstETHFinalizedETH.toUint256(), 0); + assertEq(escrowDetails.totalUnstETHUnfinalizedShares.toUint256(), totalSharesLocked); _finalizeWithdrawalQueue(unstETHIds[0]); uint256[] memory hints = _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); escrow.markUnstETHFinalized(unstETHIds, hints); - totals = escrow.getLockedAssetsTotals(); - assertEq(totals.unstETHUnfinalizedShares, statuses[0].amountOfShares); + escrowDetails = escrow.getSignallingEscrowDetails(); + assertEq(escrowDetails.totalUnstETHUnfinalizedShares.toUint256(), statuses[0].amountOfShares); uint256 ethAmountFinalized = _lido.withdrawalQueue.getClaimableEther(unstETHIds, hints)[0]; - assertApproxEqAbs(totals.unstETHFinalizedETH, ethAmountFinalized, 1); + assertApproxEqAbs(escrowDetails.totalUnstETHFinalizedETH.toUint256(), ethAmountFinalized, 1); } function test_get_rage_quit_support() public { @@ -258,8 +258,8 @@ contract EscrowHappyPath is ScenarioTestBlueprint { uint256 totalSupply = _lido.stETH.totalSupply(); // epsilon is 2 here, because the wstETH unwrap may produce 1 wei error and stETH transfer 1 wei - assertApproxEqAbs(escrow.getVetoerState(_VETOER_1).stETHLockedShares, 2 * sharesToLock, 2); - assertEq(escrow.getVetoerState(_VETOER_1).unstETHIdsCount, 2); + assertApproxEqAbs(escrow.getVetoerDetails(_VETOER_1).stETHLockedShares.toUint256(), 2 * sharesToLock, 2); + assertEq(escrow.getVetoerDetails(_VETOER_1).unstETHIdsCount, 2); assertEq(escrow.getRageQuitSupport(), PercentsD16.fromFraction({numerator: 4 ether, denominator: totalSupply})); @@ -268,10 +268,12 @@ contract EscrowHappyPath is ScenarioTestBlueprint { _lido.withdrawalQueue.findCheckpointHints(unstETHIds, 1, _lido.withdrawalQueue.getLastCheckpointIndex()); escrow.markUnstETHFinalized(unstETHIds, hints); - assertEq(escrow.getLockedAssetsTotals().unstETHUnfinalizedShares, sharesToLock); + assertEq(escrow.getSignallingEscrowDetails().totalUnstETHUnfinalizedShares.toUint256(), sharesToLock); uint256 ethAmountFinalized = _lido.withdrawalQueue.getClaimableEther(unstETHIds, hints)[0]; - assertApproxEqAbs(escrow.getLockedAssetsTotals().unstETHFinalizedETH, ethAmountFinalized, 1); + assertApproxEqAbs( + escrow.getSignallingEscrowDetails().totalUnstETHFinalizedETH.toUint256(), ethAmountFinalized, 1 + ); assertEq( escrow.getRageQuitSupport(), @@ -314,7 +316,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { assertEq(_lido.withdrawalQueue.balanceOf(address(escrow)), 20); - while (!escrow.isWithdrawalsBatchesClosed()) { + while (!escrow.getRageQuitEscrowDetails().isWithdrawalsBatchesClosed) { escrow.requestNextWithdrawalsBatch(96); } @@ -338,7 +340,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { unstETHIdsToClaim, 1, _lido.withdrawalQueue.getLastCheckpointIndex() ); - while (escrow.getUnclaimedUnstETHIdsCount() > 0) { + while (escrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { escrow.claimNextWithdrawalsBatch(32); } @@ -605,7 +607,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { escrow.claimUnstETH(unstETHIdsToClaim, hints); // The rage quit process can be successfully finished - while (escrow.getUnclaimedUnstETHIdsCount() > 0) { + while (escrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { escrow.claimNextWithdrawalsBatch(batchSizeLimit); } diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index 8964f61d..fc19ce76 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -65,13 +65,13 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { // request withdrawals batches Escrow rageQuitEscrow = _getRageQuitEscrow(); - while (!rageQuitEscrow.isWithdrawalsBatchesClosed()) { + while (!rageQuitEscrow.getRageQuitEscrowDetails().isWithdrawalsBatchesClosed) { rageQuitEscrow.requestNextWithdrawalsBatch(96); } _lido.finalizeWithdrawalQueue(); - while (rageQuitEscrow.getUnclaimedUnstETHIdsCount() > 0) { + while (rageQuitEscrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { rageQuitEscrow.claimNextWithdrawalsBatch(128); } diff --git a/test/unit/DualGovernance.t.sol b/test/unit/DualGovernance.t.sol index 07ee894c..dfda5d50 100644 --- a/test/unit/DualGovernance.t.sol +++ b/test/unit/DualGovernance.t.sol @@ -26,7 +26,7 @@ import {IWstETH} from "contracts/interfaces/IWstETH.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; import {ITimelock} from "contracts/interfaces/ITimelock.sol"; import {ITiebreaker} from "contracts/interfaces/ITiebreaker.sol"; -import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; import {UnitTest} from "test/utils/unit-test.sol"; import {StETHMock} from "test/mocks/StETHMock.sol"; diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index 1e49c5d7..b9a538f2 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -97,20 +97,6 @@ contract EscrowUnitTests is UnitTest { assertEq(_escrow.getVetoerUnstETHIds(_vetoer).length, 0); } - // --- - // getUnclaimedUnstETHIdsCount() - // --- - - function test_getUnclaimedUnstETHIdsCount_RevertOn_UnexpectedState_Signaling() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); - _escrow.getUnclaimedUnstETHIdsCount(); - } - - function test_getUnclaimedUnstETHIdsCount_RevertOn_UnexpectedState_NotInitialized() external { - vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); - _masterCopy.getUnclaimedUnstETHIdsCount(); - } - // --- // getNextWithdrawalBatch() // --- @@ -131,14 +117,14 @@ contract EscrowUnitTests is UnitTest { // isWithdrawalsBatchesClosed() // --- - function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedState_Signaling() external { + function test_getRageQuitEscrowDetails_RevertOn_UnexpectedState_Signaling() external { vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); - _escrow.isWithdrawalsBatchesClosed(); + _escrow.getRageQuitEscrowDetails(); } - function test_isWithdrawalsBatchesClosed_RevertOn_UnexpectedState_NotInitialized() external { + function test_getRageQuitEscrowDetails_RevertOn_UnexpectedState_NotInitialized() external { vm.expectRevert(abi.encodeWithSelector(EscrowState.UnexpectedState.selector, State.RageQuitEscrow)); - _masterCopy.isWithdrawalsBatchesClosed(); + _masterCopy.getRageQuitEscrowDetails(); } function vetoerLockedUnstEth(uint256[] memory amounts) internal returns (uint256[] memory unstethIds) { diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 2fed86f2..50c89901 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -3,7 +3,9 @@ pragma solidity 0.8.26; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; +import {IRageQuitEscrow} from "contracts/interfaces/IRageQuitEscrow.sol"; import {Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; diff --git a/test/unit/libraries/DualGovernanceStateTransitions.t.sol b/test/unit/libraries/DualGovernanceStateTransitions.t.sol index f04dc41e..ab6a5537 100644 --- a/test/unit/libraries/DualGovernanceStateTransitions.t.sol +++ b/test/unit/libraries/DualGovernanceStateTransitions.t.sol @@ -5,7 +5,9 @@ import {Duration, Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; -import {IEscrowBase, ISignallingEscrow, IRageQuitEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; +import {IRageQuitEscrow} from "contracts/interfaces/IRageQuitEscrow.sol"; import { State, diff --git a/test/utils/scenario-test-blueprint.sol b/test/utils/scenario-test-blueprint.sol index bddce0cc..f6385c4c 100644 --- a/test/utils/scenario-test-blueprint.sol +++ b/test/utils/scenario-test-blueprint.sol @@ -12,7 +12,7 @@ import {PercentD16} from "contracts/types/PercentD16.sol"; import {Duration, Durations} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {IEscrowBase} from "contracts/interfaces/IEscrow.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; import {Escrow} from "contracts/Escrow.sol"; // --- @@ -199,7 +199,7 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { function _unlockWstETH(address vetoer) internal { Escrow escrow = _getVetoSignallingEscrow(); uint256 wstETHBalanceBefore = _lido.wstETH.balanceOf(vetoer); - IEscrowBase.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); + ISignallingEscrow.VetoerDetails memory vetoerDetailsBefore = escrow.getVetoerDetails(vetoer); vm.startPrank(vetoer); uint256 wstETHUnlocked = escrow.unlockWstETH(); @@ -207,14 +207,17 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { // 1 wei rounding issue may arise because of the wrapping stETH into wstETH before // sending funds to the user - assertApproxEqAbs(wstETHUnlocked, vetoerStateBefore.stETHLockedShares, 1); - assertApproxEqAbs(_lido.wstETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerStateBefore.stETHLockedShares, 1); + assertApproxEqAbs(wstETHUnlocked, vetoerDetailsBefore.stETHLockedShares.toUint256(), 1); + assertApproxEqAbs( + _lido.wstETH.balanceOf(vetoer), wstETHBalanceBefore + vetoerDetailsBefore.stETHLockedShares.toUint256(), 1 + ); } function _lockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - IEscrowBase.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); - IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); + ISignallingEscrow.VetoerDetails memory vetoerDetailsBefore = escrow.getVetoerDetails(vetoer); + ISignallingEscrow.SignallingEscrowDetails memory signallingEscrowDetailsBefore = + escrow.getSignallingEscrowDetails(); uint256 unstETHTotalSharesLocked = 0; IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = @@ -233,20 +236,22 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), address(escrow)); } - IEscrowBase.VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); - assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount + unstETHIds.length); + ISignallingEscrow.VetoerDetails memory vetoerDetailsAfter = escrow.getVetoerDetails(vetoer); + assertEq(vetoerDetailsAfter.unstETHIdsCount, vetoerDetailsBefore.unstETHIdsCount + unstETHIds.length); - IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + ISignallingEscrow.SignallingEscrowDetails memory signallingEscrowDetailsAfter = + escrow.getSignallingEscrowDetails(); assertEq( - lockedAssetsTotalsAfter.unstETHUnfinalizedShares, - lockedAssetsTotalsBefore.unstETHUnfinalizedShares + unstETHTotalSharesLocked + signallingEscrowDetailsAfter.totalUnstETHUnfinalizedShares.toUint256(), + signallingEscrowDetailsBefore.totalUnstETHUnfinalizedShares.toUint256() + unstETHTotalSharesLocked ); } function _unlockUnstETH(address vetoer, uint256[] memory unstETHIds) internal { Escrow escrow = _getVetoSignallingEscrow(); - IEscrowBase.VetoerState memory vetoerStateBefore = escrow.getVetoerState(vetoer); - IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsBefore = escrow.getLockedAssetsTotals(); + ISignallingEscrow.VetoerDetails memory vetoerDetailsBefore = escrow.getVetoerDetails(vetoer); + ISignallingEscrow.SignallingEscrowDetails memory signallingEscrowDetailsBefore = + escrow.getSignallingEscrowDetails(); uint256 unstETHTotalSharesUnlocked = 0; IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = @@ -263,14 +268,15 @@ contract ScenarioTestBlueprint is TestingAssertEqExtender, SetupDeployment { assertEq(_lido.withdrawalQueue.ownerOf(unstETHIds[i]), vetoer); } - IEscrowBase.VetoerState memory vetoerStateAfter = escrow.getVetoerState(vetoer); - assertEq(vetoerStateAfter.unstETHIdsCount, vetoerStateBefore.unstETHIdsCount - unstETHIds.length); + ISignallingEscrow.VetoerDetails memory vetoerDetailsAfter = escrow.getVetoerDetails(vetoer); + assertEq(vetoerDetailsAfter.unstETHIdsCount, vetoerDetailsBefore.unstETHIdsCount - unstETHIds.length); // TODO: implement correct assert. It must consider was unstETH finalized or not - IEscrowBase.LockedAssetsTotals memory lockedAssetsTotalsAfter = escrow.getLockedAssetsTotals(); + ISignallingEscrow.SignallingEscrowDetails memory signallingEscrowDetailsAfter = + escrow.getSignallingEscrowDetails(); assertEq( - lockedAssetsTotalsAfter.unstETHUnfinalizedShares, - lockedAssetsTotalsBefore.unstETHUnfinalizedShares - unstETHTotalSharesUnlocked + signallingEscrowDetailsAfter.totalUnstETHUnfinalizedShares.toUint256(), + signallingEscrowDetailsBefore.totalUnstETHUnfinalizedShares.toUint256() - unstETHTotalSharesUnlocked ); } From 278dc696a6d9d1c16ce047b43bc6cadbbaa05ea1 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Thu, 5 Dec 2024 15:05:12 +0400 Subject: [PATCH 3/8] Add missing natspec to Escrow. Remove EscrowMock --- contracts/Escrow.sol | 8 +- test/mocks/EscrowMock.sol | 157 -------------------------------------- 2 files changed, 6 insertions(+), 159 deletions(-) delete mode 100644 test/mocks/EscrowMock.sol diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index c7deb5d5..14d4b034 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -83,8 +83,10 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { // Implementation Immutables // --- - /// @dev Reference to the address of the implementation contract, used to distinguish whether the call - /// is made to the proxy or directly to the implementation. + /// @notice The address of the implementation used for Signalling and Rage Quit escrows deployed + /// by the DualGovernance contract. + /// @dev This address is also used to distinguish whether the call is made to the proxy or directly + /// to the implementation. IEscrowBase public immutable ESCROW_MASTER_COPY; /// @dev The address of the Dual Governance contract. @@ -143,6 +145,8 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { ST_ETH.approve(address(WITHDRAWAL_QUEUE), type(uint256).max); } + /// @notice Retrieves the current state of the Escrow. + /// @return State The current state of the Escrow. function getEscrowState() external view returns (State) { return _escrowState.state; } diff --git a/test/mocks/EscrowMock.sol b/test/mocks/EscrowMock.sol deleted file mode 100644 index dad55de7..00000000 --- a/test/mocks/EscrowMock.sol +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import {Duration} from "contracts/types/Duration.sol"; -import {PercentD16} from "contracts/types/PercentD16.sol"; - -import {IEscrowBase} from "contracts/interfaces/IEscrowBase.sol"; -import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; -import {IRageQuitEscrow} from "contracts/interfaces/IRageQuitEscrow.sol"; - -import {State as EscrowState} from "contracts/libraries/EscrowState.sol"; - -contract EscrowMock is ISignallingEscrow, IRageQuitEscrow { - IEscrowBase public immutable ESCROW_MASTER_COPY = this; - - event __RageQuitStarted(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock); - - Duration public __minAssetsLockDuration; - PercentD16 public __rageQuitSupport; - bool public __isRageQuitFinalized; - - function initialize(Duration minAssetsLockDuration) external { - __minAssetsLockDuration = minAssetsLockDuration; - } - - function getEscrowState() external view returns (EscrowState) { - revert("Not implemented"); - } - - function getVetoerDetails(address vetoer) external view returns (VetoerDetails memory) { - revert("Not implemented"); - } - - function getVetoerUnstETHIds(address vetoer) external view returns (uint256[] memory) { - revert("Not implemented"); - } - - // --- - // Signalling Escrow Methods - // --- - - function lockStETH(uint256 amount) external returns (uint256 lockedStETHShares) { - revert("Not implemented"); - } - - function unlockStETH() external returns (uint256 unlockedStETHShares) { - revert("Not implemented"); - } - - function lockWstETH(uint256 amount) external returns (uint256 lockedStETHShares) { - revert("Not implemented"); - } - - function unlockWstETH() external returns (uint256 wstETHUnlocked) { - revert("Not implemented"); - } - - function lockUnstETH(uint256[] memory unstETHIds) external { - revert("Not implemented"); - } - - function unlockUnstETH(uint256[] memory unstETHIds) external { - revert("Not implemented"); - } - - function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external { - revert("Not implemented"); - } - - function startRageQuit( - Duration rageQuitExtraTimelock, - Duration rageQuitWithdrawalsTimelock - ) external returns (IRageQuitEscrow) { - emit __RageQuitStarted(rageQuitExtraTimelock, rageQuitWithdrawalsTimelock); - return this; - } - - function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external { - __minAssetsLockDuration = newMinAssetsLockDuration; - } - - function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport) { - return __rageQuitSupport; - } - - function getMinAssetsLockDuration() external view returns (Duration) { - revert("Not implemented"); - } - - function getLockedUnstETHDetails(uint256[] calldata unstETHIds) - external - view - returns (LockedUnstETHDetails[] memory) - { - revert("Not implemented"); - } - - function getSignallingEscrowDetails() external view returns (SignallingEscrowDetails memory) { - revert("Not implemented"); - } - - // --- - // Rage Quit Escrow - // --- - - function requestNextWithdrawalsBatch(uint256 batchSize) external { - revert("Not implemented"); - } - - function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external { - revert("Not implemented"); - } - - function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external { - revert("Not implemented"); - } - - function claimUnstETH(uint256[] calldata unstETHIds, uint256[] calldata hints) external { - revert("Not implemented"); - } - - function startRageQuitExtensionPeriod() external { - revert("Not implemented"); - } - - function withdrawETH() external { - revert("Not implemented"); - } - - function withdrawETH(uint256[] calldata unstETHIds) external { - revert("Not implemented"); - } - - function isRageQuitFinalized() external view returns (bool) { - return __isRageQuitFinalized; - } - - function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory) { - revert("Not implemented"); - } - - function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds) { - revert("Not implemented"); - } - - // --- - // Mock methods - // --- - - function __setRageQuitSupport(PercentD16 newRageQuitSupport) external { - __rageQuitSupport = newRageQuitSupport; - } - - function __setIsRageQuitFinalized(bool newIsRageQuitFinalized) external { - __isRageQuitFinalized = newIsRageQuitFinalized; - } -} From dbc39d2081886b4efe578c2042090674c3e122f4 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 00:53:29 +0400 Subject: [PATCH 4/8] Escrow.startRageQuit() without returning value --- contracts/Escrow.sol | 6 +----- contracts/interfaces/ISignallingEscrow.sol | 5 +---- contracts/libraries/DualGovernanceStateMachine.sol | 3 ++- test/unit/libraries/DualGovernanceStateMachine.t.sol | 9 --------- 4 files changed, 4 insertions(+), 19 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 14d4b034..776352b7 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -294,14 +294,10 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { /// have sufficient time to claim it. /// @param rageQuitEthWithdrawalsDelay The waiting period that vetoers must observe after the Rage Quit process /// is finalized before they can withdraw ETH from the Escrow. - function startRageQuit( - Duration rageQuitExtensionPeriodDuration, - Duration rageQuitEthWithdrawalsDelay - ) external returns (IRageQuitEscrow rageQuitEscrow) { + function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitEthWithdrawalsDelay) external { _checkCallerIsDualGovernance(); _escrowState.startRageQuit(rageQuitExtensionPeriodDuration, rageQuitEthWithdrawalsDelay); _batchesQueue.open(WITHDRAWAL_QUEUE.getLastRequestId()); - rageQuitEscrow = IRageQuitEscrow(this); } // --- diff --git a/contracts/interfaces/ISignallingEscrow.sol b/contracts/interfaces/ISignallingEscrow.sol index e1f0275b..5829029a 100644 --- a/contracts/interfaces/ISignallingEscrow.sol +++ b/contracts/interfaces/ISignallingEscrow.sol @@ -44,10 +44,7 @@ interface ISignallingEscrow is IEscrowBase { function markUnstETHFinalized(uint256[] memory unstETHIds, uint256[] calldata hints) external; - function startRageQuit( - Duration rageQuitExtensionPeriodDuration, - Duration rageQuitEthWithdrawalsDelay - ) external returns (IRageQuitEscrow); + function startRageQuit(Duration rageQuitExtensionPeriodDuration, Duration rageQuitEthWithdrawalsDelay) external; function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; diff --git a/contracts/libraries/DualGovernanceStateMachine.sol b/contracts/libraries/DualGovernanceStateMachine.sol index c1fdb083..c837a128 100644 --- a/contracts/libraries/DualGovernanceStateMachine.sol +++ b/contracts/libraries/DualGovernanceStateMachine.sol @@ -174,9 +174,10 @@ library DualGovernanceStateMachine { uint256 newRageQuitRound = Math.min(currentRageQuitRound + 1, MAX_RAGE_QUIT_ROUND); self.rageQuitRound = uint8(newRageQuitRound); - self.rageQuitEscrow = signallingEscrow.startRageQuit( + signallingEscrow.startRageQuit( config.rageQuitExtensionPeriodDuration, config.calcRageQuitWithdrawalsDelay(newRageQuitRound) ); + self.rageQuitEscrow = IRageQuitEscrow(address(signallingEscrow)); _deployNewSignallingEscrow(self, signallingEscrow.ESCROW_MASTER_COPY(), config.minAssetsLockDuration); } diff --git a/test/unit/libraries/DualGovernanceStateMachine.t.sol b/test/unit/libraries/DualGovernanceStateMachine.t.sol index 50c89901..fb3414fb 100644 --- a/test/unit/libraries/DualGovernanceStateMachine.t.sol +++ b/test/unit/libraries/DualGovernanceStateMachine.t.sol @@ -52,7 +52,6 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { _mockRageQuitFinalized(false); _mockRageQuitSupport(PercentsD16.from(0)); _mockEscrowMasterCopy(); - _mockStartRageQuit(); } function test_initialize_RevertOn_ReInitialization() external { @@ -460,14 +459,6 @@ contract DualGovernanceStateMachineUnitTests is UnitTest { ); } - function _mockStartRageQuit() internal { - vm.mockCall( - _ESCROW_MASTER_COPY_MOCK, - abi.encodeWithSelector(ISignallingEscrow.startRageQuit.selector), - abi.encode(_stateMachine.signallingEscrow) - ); - } - function _mockRageQuitSupport(PercentD16 rageQuitSupport) internal { vm.mockCall( _ESCROW_MASTER_COPY_MOCK, From e324c684772d9f3b0389c623248924d6ffec7357 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 18:24:38 +0400 Subject: [PATCH 5/8] Added AssetsAccounting.getLockedUnstETHDetails(). Update RageQuitEscrow getters. --- contracts/Escrow.sol | 44 +++++++++---------- contracts/interfaces/IRageQuitEscrow.sol | 5 +-- contracts/libraries/AssetsAccounting.sol | 24 ++++++++++ contracts/libraries/Proposers.sol | 6 ++- test/scenario/dg-update-tokens-rotation.t.sol | 4 +- test/scenario/escrow.t.sol | 6 +-- test/scenario/veto-cooldown-mechanics.t.sol | 4 +- 7 files changed, 59 insertions(+), 34 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 776352b7..dc0aec0b 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -19,13 +19,7 @@ import {IDualGovernance} from "./interfaces/IDualGovernance.sol"; import {EscrowState, State} from "./libraries/EscrowState.sol"; import {WithdrawalsBatchesQueue} from "./libraries/WithdrawalsBatchesQueue.sol"; -import { - HolderAssets, - UnstETHRecord, - StETHAccounting, - UnstETHAccounting, - AssetsAccounting -} from "./libraries/AssetsAccounting.sol"; +import {HolderAssets, StETHAccounting, UnstETHAccounting, AssetsAccounting} from "./libraries/AssetsAccounting.sol"; /// @notice This contract is used to accumulate stETH, wstETH, unstETH, and withdrawn ETH from vetoers during the /// veto signalling and rage quit processes. @@ -38,6 +32,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { // --- // Errors // --- + error EmptyUnstETHIds(); error UnclaimedBatches(); error UnexpectedUnstETHId(); @@ -185,7 +180,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Lock & Unlock wstETH + // Signalling Escrow: Lock & Unlock wstETH // --- /// @notice Locks the vetoer's specified `amount` of wstETH in the Veto Signalling Escrow, thereby increasing @@ -394,12 +389,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { unstETHDetails = new LockedUnstETHDetails[](unstETHIds.length); for (uint256 i = 0; i < unstETHIds.length; ++i) { - UnstETHRecord memory unstETHRecord = _accounting.unstETHRecords[unstETHIds[i]]; - - unstETHDetails[i].status = unstETHRecord.status; - unstETHDetails[i].lockedBy = unstETHRecord.lockedBy; - unstETHDetails[i].shares = unstETHRecord.shares; - unstETHDetails[i].claimableAmount = unstETHRecord.claimableAmount; + unstETHDetails[i] = _accounting.getLockedUnstETHDetails(unstETHIds[i]); } } @@ -568,13 +558,14 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { } // --- - // Rage Quit Escrow Getters + // Rage Quit Escrow: Getters // --- /// @notice Returns whether the Rage Quit process has been finalized. /// @return A boolean value indicating whether the Rage Quit process has been finalized (`true`) or not (`false`). function isRageQuitFinalized() external view returns (bool) { - return _escrowState.isRageQuitEscrow() && _escrowState.isRageQuitExtensionPeriodPassed(); + _escrowState.checkRageQuitEscrow(); + return _escrowState.isRageQuitExtensionPeriodPassed(); } /// @notice Retrieves the unstETH NFT ids of the next batch available for claiming. @@ -585,22 +576,31 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { unstETHIds = _batchesQueue.getNextWithdrawalsBatches(limit); } + /// @notice Returns whether all withdrawal batches have been closed. + /// @return isWithdrawalsBatchesClosed A boolean value indicating whether all withdrawal batches have been + /// closed (`true`) or not (`false`). + function isWithdrawalsBatchesClosed() external view returns (bool) { + _escrowState.checkRageQuitEscrow(); + return _batchesQueue.isClosed(); + } + + /// @notice Returns the total count of unstETH NFTs that have not been claimed yet. + /// @return unclaimedUnstETHIdsCount The total number of unclaimed unstETH NFTs. + function getUnclaimedUnstETHIdsCount() external view returns (uint256) { + _escrowState.checkRageQuitEscrow(); + return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + } + /// @notice Retrieves details about the current state of the rage quit escrow. /// @return details A `RageQuitEscrowDetails` struct containing the following fields: - /// - `isWithdrawalsBatchesClosed`: Indicates whether the withdrawals batches are closed. /// - `isRageQuitExtensionPeriodStarted`: Indicates whether the rage quit extension period has started. - /// - `unclaimedUnstETHIdsCount`: The total count of unstETH NFTs that have not been claimed yet. /// - `rageQuitEthWithdrawalsDelay`: The delay period for ETH withdrawals during rage quit. /// - `rageQuitExtensionPeriodDuration`: The duration of the rage quit extension period. /// - `rageQuitExtensionPeriodStartedAt`: The timestamp when the rage quit extension period started. function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory details) { _escrowState.checkRageQuitEscrow(); - details.isWithdrawalsBatchesClosed = _batchesQueue.isClosed(); details.isRageQuitExtensionPeriodStarted = _escrowState.isRageQuitExtensionPeriodStarted(); - - details.unclaimedUnstETHIdsCount = _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); - details.rageQuitEthWithdrawalsDelay = _escrowState.rageQuitEthWithdrawalsDelay; details.rageQuitExtensionPeriodDuration = _escrowState.rageQuitExtensionPeriodDuration; details.rageQuitExtensionPeriodStartedAt = _escrowState.rageQuitExtensionPeriodStartedAt; diff --git a/contracts/interfaces/IRageQuitEscrow.sol b/contracts/interfaces/IRageQuitEscrow.sol index d31b6c2e..d90a3a61 100644 --- a/contracts/interfaces/IRageQuitEscrow.sol +++ b/contracts/interfaces/IRageQuitEscrow.sol @@ -8,10 +8,7 @@ import {IEscrowBase} from "./IEscrowBase.sol"; interface IRageQuitEscrow is IEscrowBase { struct RageQuitEscrowDetails { - bool isRageQuitFinalized; - bool isWithdrawalsBatchesClosed; bool isRageQuitExtensionPeriodStarted; - uint256 unclaimedUnstETHIdsCount; Duration rageQuitEthWithdrawalsDelay; Duration rageQuitExtensionPeriodDuration; Timestamp rageQuitExtensionPeriodStartedAt; @@ -30,5 +27,7 @@ interface IRageQuitEscrow is IEscrowBase { function isRageQuitFinalized() external view returns (bool); function getNextWithdrawalBatch(uint256 limit) external view returns (uint256[] memory unstETHIds); + function isWithdrawalsBatchesClosed() external view returns (bool); + function getUnclaimedUnstETHIdsCount() external view returns (uint256); function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory); } diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index 84c1ba53..1a2d8226 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -8,6 +8,7 @@ import {SharesValue, SharesValues} from "../types/SharesValue.sol"; import {IndexOneBased, IndicesOneBased} from "../types/IndexOneBased.sol"; import {IWithdrawalQueue} from "../interfaces/IWithdrawalQueue.sol"; +import {ISignallingEscrow} from "../interfaces/ISignallingEscrow.sol"; /// @notice Tracks the stETH and unstETH tokens associated with users. /// @param stETHLockedShares Total number of stETH shares held by the user. @@ -341,6 +342,29 @@ library AssetsAccounting { emit UnstETHWithdrawn(unstETHIds, amountWithdrawn); } + // --- + // Getters + // --- + + /// @notice Retrieves details of locked unstETH record for the given id. + /// @param unstETHId The id for the locked unstETH record to retrieve. + /// @return unstETHDetails A `LockedUnstETHDetails` struct containing the details for provided unstETH id. + function getLockedUnstETHDetails( + Context storage self, + uint256 unstETHId + ) internal view returns (ISignallingEscrow.LockedUnstETHDetails memory unstETHDetails) { + UnstETHRecord memory unstETHRecord = self.unstETHRecords[unstETHId]; + + if (unstETHRecord.status == UnstETHRecordStatus.NotLocked) { + revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.NotLocked); + } + + unstETHDetails.status = unstETHRecord.status; + unstETHDetails.lockedBy = unstETHRecord.lockedBy; + unstETHDetails.shares = unstETHRecord.shares; + unstETHDetails.claimableAmount = unstETHRecord.claimableAmount; + } + // --- // Checks // --- diff --git a/contracts/libraries/Proposers.sol b/contracts/libraries/Proposers.sol index 5bbb471c..9d78c3a8 100644 --- a/contracts/libraries/Proposers.sol +++ b/contracts/libraries/Proposers.sol @@ -154,8 +154,10 @@ library Proposers { /// @param self The context of the Proposers library. /// @return proposers An array of `Proposer` structs representing all registered proposers. function getAllProposers(Context storage self) internal view returns (Proposer[] memory proposers) { - proposers = new Proposer[](self.proposers.length); - for (uint256 i = 0; i < proposers.length; ++i) { + uint256 proposersCount = self.proposers.length; + proposers = new Proposer[](proposersCount); + + for (uint256 i = 0; i < proposersCount; ++i) { proposers[i] = getProposer(self, self.proposers[i]); } } diff --git a/test/scenario/dg-update-tokens-rotation.t.sol b/test/scenario/dg-update-tokens-rotation.t.sol index a415e13c..97f98191 100644 --- a/test/scenario/dg-update-tokens-rotation.t.sol +++ b/test/scenario/dg-update-tokens-rotation.t.sol @@ -96,13 +96,13 @@ contract DualGovernanceUpdateTokensRotation is ScenarioTestBlueprint { // The Rage Quit may be finished in the previous DG instance so vetoers will not lose their funds by mistake Escrow rageQuitEscrow = Escrow(payable(_dualGovernance.getRageQuitEscrow())); - while (!rageQuitEscrow.getRageQuitEscrowDetails().isWithdrawalsBatchesClosed) { + while (!rageQuitEscrow.isWithdrawalsBatchesClosed()) { rageQuitEscrow.requestNextWithdrawalsBatch(96); } _finalizeWithdrawalQueue(); - while (rageQuitEscrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { + while (rageQuitEscrow.getUnclaimedUnstETHIdsCount() > 0) { rageQuitEscrow.claimNextWithdrawalsBatch(32); } diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 01582c83..fd9b3d4c 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -316,7 +316,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { assertEq(_lido.withdrawalQueue.balanceOf(address(escrow)), 20); - while (!escrow.getRageQuitEscrowDetails().isWithdrawalsBatchesClosed) { + while (!escrow.isWithdrawalsBatchesClosed()) { escrow.requestNextWithdrawalsBatch(96); } @@ -340,7 +340,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { unstETHIdsToClaim, 1, _lido.withdrawalQueue.getLastCheckpointIndex() ); - while (escrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { + while (escrow.getUnclaimedUnstETHIdsCount() > 0) { escrow.claimNextWithdrawalsBatch(32); } @@ -607,7 +607,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { escrow.claimUnstETH(unstETHIdsToClaim, hints); // The rage quit process can be successfully finished - while (escrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { + while (escrow.getUnclaimedUnstETHIdsCount() > 0) { escrow.claimNextWithdrawalsBatch(batchSizeLimit); } diff --git a/test/scenario/veto-cooldown-mechanics.t.sol b/test/scenario/veto-cooldown-mechanics.t.sol index fc19ce76..8964f61d 100644 --- a/test/scenario/veto-cooldown-mechanics.t.sol +++ b/test/scenario/veto-cooldown-mechanics.t.sol @@ -65,13 +65,13 @@ contract VetoCooldownMechanicsTest is ScenarioTestBlueprint { // request withdrawals batches Escrow rageQuitEscrow = _getRageQuitEscrow(); - while (!rageQuitEscrow.getRageQuitEscrowDetails().isWithdrawalsBatchesClosed) { + while (!rageQuitEscrow.isWithdrawalsBatchesClosed()) { rageQuitEscrow.requestNextWithdrawalsBatch(96); } _lido.finalizeWithdrawalQueue(); - while (rageQuitEscrow.getRageQuitEscrowDetails().unclaimedUnstETHIdsCount > 0) { + while (rageQuitEscrow.getUnclaimedUnstETHIdsCount() > 0) { rageQuitEscrow.claimNextWithdrawalsBatch(128); } From c9d44426e4af5b4951138bd5c66cf14868e76fe7 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 19:40:41 +0400 Subject: [PATCH 6/8] Add tests for getLockedUnstETHDetails method --- contracts/Escrow.sol | 1 + contracts/interfaces/IRageQuitEscrow.sol | 2 +- contracts/interfaces/ISignallingEscrow.sol | 1 + contracts/libraries/AssetsAccounting.sol | 1 + test/unit/Escrow.t.sol | 44 +++++++++++++++++ test/unit/libraries/AssetsAccounting.t.sol | 57 ++++++++++++++++++++++ 6 files changed, 105 insertions(+), 1 deletion(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 8a615dc9..beaec06e 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -376,6 +376,7 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { /// @return unstETHDetails An array of `LockedUnstETHDetails` containing the details for each provided unstETH id. /// /// The details include: + /// - `id`: The id of the locked unstETH NFT. /// - `status`: The current status of the unstETH record. /// - `lockedBy`: The address that locked the unstETH record. /// - `shares`: The number of shares associated with the locked unstETH. diff --git a/contracts/interfaces/IRageQuitEscrow.sol b/contracts/interfaces/IRageQuitEscrow.sol index d90a3a61..6649db93 100644 --- a/contracts/interfaces/IRageQuitEscrow.sol +++ b/contracts/interfaces/IRageQuitEscrow.sol @@ -8,10 +8,10 @@ import {IEscrowBase} from "./IEscrowBase.sol"; interface IRageQuitEscrow is IEscrowBase { struct RageQuitEscrowDetails { - bool isRageQuitExtensionPeriodStarted; Duration rageQuitEthWithdrawalsDelay; Duration rageQuitExtensionPeriodDuration; Timestamp rageQuitExtensionPeriodStartedAt; + bool isRageQuitExtensionPeriodStarted; } function requestNextWithdrawalsBatch(uint256 batchSize) external; diff --git a/contracts/interfaces/ISignallingEscrow.sol b/contracts/interfaces/ISignallingEscrow.sol index 5829029a..de91a3a6 100644 --- a/contracts/interfaces/ISignallingEscrow.sol +++ b/contracts/interfaces/ISignallingEscrow.sol @@ -20,6 +20,7 @@ interface ISignallingEscrow is IEscrowBase { } struct LockedUnstETHDetails { + uint256 id; UnstETHRecordStatus status; address lockedBy; SharesValue shares; diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index 1a2d8226..214430a4 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -359,6 +359,7 @@ library AssetsAccounting { revert InvalidUnstETHStatus(unstETHId, UnstETHRecordStatus.NotLocked); } + unstETHDetails.id = unstETHId; unstETHDetails.status = unstETHRecord.status; unstETHDetails.lockedBy = unstETHRecord.lockedBy; unstETHDetails.shares = unstETHRecord.shares; diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index c0613835..6a689960 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -1290,6 +1290,50 @@ contract EscrowUnitTests is UnitTest { assertEq(_escrow.getVetoerUnstETHIds(_vetoer).length, 0); } + // --- + // getLockedUnstETHDetails() + // --- + + function test_getLockedUnstETHDetails_HappyPath() external { + uint256[] memory unstEthAmounts = new uint256[](2); + unstEthAmounts[0] = 1 ether; + unstEthAmounts[1] = 10 ether; + + assertEq(_escrow.getVetoerUnstETHIds(_vetoer).length, 0); + + uint256[] memory unstEthIds = _vetoerLockedUnstEth(unstEthAmounts); + + IEscrow.LockedUnstETHDetails[] memory unstETHDetails = _escrow.getLockedUnstETHDetails(unstEthIds); + + assertEq(unstETHDetails.length, unstEthIds.length); + + assertEq(unstETHDetails[0].id, unstEthIds[0]); + assertEq(unstETHDetails[0].lockedBy, _vetoer); + assertTrue(unstETHDetails[0].status == UnstETHRecordStatus.Locked); + assertEq(unstETHDetails[0].shares.toUint256(), unstEthAmounts[0]); + assertEq(unstETHDetails[0].claimableAmount.toUint256(), 0); + + assertEq(unstETHDetails[1].id, unstEthIds[1]); + assertEq(unstETHDetails[1].lockedBy, _vetoer); + assertTrue(unstETHDetails[1].status == UnstETHRecordStatus.Locked); + assertEq(unstETHDetails[1].shares.toUint256(), unstEthAmounts[1]); + assertEq(unstETHDetails[1].claimableAmount.toUint256(), 0); + } + + function test_getLockedUnstETHDetails_RevertOn_unstETHNotLocked() external { + uint256 notLockedUnstETHId = 42; + + uint256[] memory notLockedUnstETHIds = new uint256[](1); + notLockedUnstETHIds[0] = notLockedUnstETHId; + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, notLockedUnstETHId, UnstETHRecordStatus.NotLocked + ) + ); + _escrow.getLockedUnstETHDetails(notLockedUnstETHIds); + } + // --- // getNextWithdrawalBatch() // --- diff --git a/test/unit/libraries/AssetsAccounting.t.sol b/test/unit/libraries/AssetsAccounting.t.sol index e1c9b3df..2ff5a3d9 100644 --- a/test/unit/libraries/AssetsAccounting.t.sol +++ b/test/unit/libraries/AssetsAccounting.t.sol @@ -11,6 +11,7 @@ import {IndicesOneBased} from "contracts/types/IndexOneBased.sol"; import {Durations} from "contracts/types/Duration.sol"; import {Timestamps} from "contracts/types/Timestamp.sol"; import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {ISignallingEscrow} from "contracts/interfaces/ISignallingEscrow.sol"; import {AssetsAccounting, UnstETHRecordStatus} from "contracts/libraries/AssetsAccounting.sol"; import {UnitTest, Duration} from "test/utils/unit-test.sol"; @@ -1419,6 +1420,62 @@ contract AssetsAccountingUnitTests is UnitTest { AssetsAccounting.accountUnstETHWithdraw(_accountingContext, holder, unstETHIds); } + // --- + // getLockedUnstETHDetails + // --- + + function test_getLockedUnstETHDetails_HappyPath() external { + uint256 unstETHIdsCount = 4; + uint256[] memory unstETHIds = new uint256[](unstETHIdsCount); + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + address holder = address(uint160(uint256(keccak256(abi.encode(i))))); + + _accountingContext.unstETHRecords[unstETHIds[i]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus(i + 1); + _accountingContext.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(i * 1 ether); + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(i * 10 ether); + + _accountingContext.assets[holder].unstETHIds.push(unstETHIds[i]); + } + + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + ISignallingEscrow.LockedUnstETHDetails memory unstETHDetails = + AssetsAccounting.getLockedUnstETHDetails(_accountingContext, unstETHIds[i]); + + assertEq(unstETHDetails.id, unstETHIds[i]); + assertEq(unstETHDetails.status, UnstETHRecordStatus(i + 1)); + assertEq(unstETHDetails.lockedBy, address(uint160(uint256(keccak256(abi.encode(i)))))); + assertEq(unstETHDetails.shares, SharesValues.from(i * 1 ether)); + assertEq(unstETHDetails.claimableAmount, ETHValues.from(i * 10 ether)); + } + } + + function test_getLockedUnstETHDetails_RevertOn_UnstETHNotLocked() external { + uint256 unstETHIdsCount = 4; + uint256[] memory unstETHIds = new uint256[](unstETHIdsCount); + for (uint256 i = 0; i < unstETHIdsCount; ++i) { + unstETHIds[i] = genRandomUnstEthId(i); + address holder = address(uint160(uint256(keccak256(abi.encode(i))))); + + _accountingContext.unstETHRecords[unstETHIds[i]].lockedBy = holder; + _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus(i + 1); + _accountingContext.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(i * 1 ether); + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(i * 10 ether); + + _accountingContext.assets[holder].unstETHIds.push(unstETHIds[i]); + } + + uint256 notLockedUnstETHId = genRandomUnstEthId(5); + + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, notLockedUnstETHId, UnstETHRecordStatus.NotLocked + ) + ); + AssetsAccounting.getLockedUnstETHDetails(_accountingContext, notLockedUnstETHId); + } + // --- // checkMinAssetsLockDurationPassed // --- From 9d03b29ced14ce3f12c69a4a70a54a8bd5621f4a Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 19:53:08 +0400 Subject: [PATCH 7/8] Add getEscrowState() unit test --- contracts/Escrow.sol | 3 ++- test/unit/Escrow.t.sol | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index beaec06e..6d4e428c 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -389,7 +389,8 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { { unstETHDetails = new LockedUnstETHDetails[](unstETHIds.length); - for (uint256 i = 0; i < unstETHIds.length; ++i) { + uint256 unstETHIdsCount = unstETHIds.length; + for (uint256 i = 0; i < unstETHIdsCount; ++i) { unstETHDetails[i] = _accounting.getLockedUnstETHDetails(unstETHIds[i]); } } diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index 6a689960..1572bc39 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -140,6 +140,18 @@ contract EscrowUnitTests is UnitTest { instance.initialize(Durations.ZERO); } + // --- + // getEscrowState() + // --- + + function test_getEscrowState_HappyPath() external { + assertTrue(_masterCopy.getEscrowState() == EscrowState.NotInitialized); + assertTrue(_escrow.getEscrowState() == EscrowState.SignallingEscrow); + + _transitToRageQuit(); + assertTrue(_escrow.getEscrowState() == EscrowState.RageQuitEscrow); + } + // --- // lockStETH() // --- From 38cbf38cfc342f17d8bd9bba4070f4b02fd33d62 Mon Sep 17 00:00:00 2001 From: Bogdan Kovtun Date: Fri, 6 Dec 2024 20:11:57 +0400 Subject: [PATCH 8/8] AssetsAccounting test improvement. Small gas tweaks --- contracts/Escrow.sol | 20 ++++++++++---------- test/unit/libraries/AssetsAccounting.t.sol | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/Escrow.sol b/contracts/Escrow.sol index 6d4e428c..1a24919e 100644 --- a/contracts/Escrow.sol +++ b/contracts/Escrow.sol @@ -387,9 +387,9 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { view returns (LockedUnstETHDetails[] memory unstETHDetails) { - unstETHDetails = new LockedUnstETHDetails[](unstETHIds.length); - uint256 unstETHIdsCount = unstETHIds.length; + unstETHDetails = new LockedUnstETHDetails[](unstETHIdsCount); + for (uint256 i = 0; i < unstETHIdsCount; ++i) { unstETHDetails[i] = _accounting.getLockedUnstETHDetails(unstETHIds[i]); } @@ -563,13 +563,6 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { // Rage Quit Escrow: Getters // --- - /// @notice Returns whether the Rage Quit process has been finalized. - /// @return A boolean value indicating whether the Rage Quit process has been finalized (`true`) or not (`false`). - function isRageQuitFinalized() external view returns (bool) { - _escrowState.checkRageQuitEscrow(); - return _escrowState.isRageQuitExtensionPeriodPassed(); - } - /// @notice Retrieves the unstETH NFT ids of the next batch available for claiming. /// @param limit The maximum number of unstETH NFTs to return in the batch. /// @return unstETHIds An array of unstETH NFT ids available for the next withdrawal batch. @@ -593,6 +586,13 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { return _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); } + /// @notice Returns whether the Rage Quit process has been finalized. + /// @return A boolean value indicating whether the Rage Quit process has been finalized (`true`) or not (`false`). + function isRageQuitFinalized() external view returns (bool) { + _escrowState.checkRageQuitEscrow(); + return _escrowState.isRageQuitExtensionPeriodPassed(); + } + /// @notice Retrieves details about the current state of the rage quit escrow. /// @return details A `RageQuitEscrowDetails` struct containing the following fields: /// - `isRageQuitExtensionPeriodStarted`: Indicates whether the rage quit extension period has started. @@ -602,10 +602,10 @@ contract Escrow is ISignallingEscrow, IRageQuitEscrow { function getRageQuitEscrowDetails() external view returns (RageQuitEscrowDetails memory details) { _escrowState.checkRageQuitEscrow(); - details.isRageQuitExtensionPeriodStarted = _escrowState.isRageQuitExtensionPeriodStarted(); details.rageQuitEthWithdrawalsDelay = _escrowState.rageQuitEthWithdrawalsDelay; details.rageQuitExtensionPeriodDuration = _escrowState.rageQuitExtensionPeriodDuration; details.rageQuitExtensionPeriodStartedAt = _escrowState.rageQuitExtensionPeriodStartedAt; + details.isRageQuitExtensionPeriodStarted = _escrowState.isRageQuitExtensionPeriodStarted(); } // --- diff --git a/test/unit/libraries/AssetsAccounting.t.sol b/test/unit/libraries/AssetsAccounting.t.sol index 2ff5a3d9..8e2e6734 100644 --- a/test/unit/libraries/AssetsAccounting.t.sol +++ b/test/unit/libraries/AssetsAccounting.t.sol @@ -1433,8 +1433,8 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.unstETHRecords[unstETHIds[i]].lockedBy = holder; _accountingContext.unstETHRecords[unstETHIds[i]].status = UnstETHRecordStatus(i + 1); - _accountingContext.unstETHRecords[unstETHIds[i]].shares = SharesValues.from(i * 1 ether); - _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from(i * 10 ether); + _accountingContext.unstETHRecords[unstETHIds[i]].shares = SharesValues.from((i + 1) * 1 ether); + _accountingContext.unstETHRecords[unstETHIds[i]].claimableAmount = ETHValues.from((i + 1) * 10 ether); _accountingContext.assets[holder].unstETHIds.push(unstETHIds[i]); } @@ -1446,8 +1446,8 @@ contract AssetsAccountingUnitTests is UnitTest { assertEq(unstETHDetails.id, unstETHIds[i]); assertEq(unstETHDetails.status, UnstETHRecordStatus(i + 1)); assertEq(unstETHDetails.lockedBy, address(uint160(uint256(keccak256(abi.encode(i)))))); - assertEq(unstETHDetails.shares, SharesValues.from(i * 1 ether)); - assertEq(unstETHDetails.claimableAmount, ETHValues.from(i * 10 ether)); + assertEq(unstETHDetails.shares, SharesValues.from((i + 1) * 1 ether)); + assertEq(unstETHDetails.claimableAmount, ETHValues.from((i + 1) * 10 ether)); } }