From 22c0bde092c1b1a04fe9d48ff734e4f8f51828f7 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 14 Aug 2024 19:52:40 +0400 Subject: [PATCH 1/5] Fix param description --- contracts/libraries/WithdrawalBatchesQueue.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/libraries/WithdrawalBatchesQueue.sol b/contracts/libraries/WithdrawalBatchesQueue.sol index 59c2c6ab..d4ca6306 100644 --- a/contracts/libraries/WithdrawalBatchesQueue.sol +++ b/contracts/libraries/WithdrawalBatchesQueue.sol @@ -5,7 +5,7 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; /// @notice The state of the WithdrawalBatchesQueue -/// @param Empty The initial (uninitialized) state of the WithdrawalBatchesQueue +/// @param Absent The initial (uninitialized) state of the WithdrawalBatchesQueue /// @param Opened In this state, the WithdrawalBatchesQueue allows the addition of new batches of unstETH ids /// @param Closed The terminal state of the queue. In this state, the addition of new batches is forbidden enum State { From c5f9035d7c86d15d6f9cad5e1df4f850d10ac36a Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Wed, 14 Aug 2024 19:53:40 +0400 Subject: [PATCH 2/5] Fix empty expectRevert() statements --- test/scenario/escrow.t.sol | 2 +- test/unit/libraries/AssetsAccounting.t.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 7bef7150..178a5d90 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -389,7 +389,7 @@ contract EscrowHappyPath is ScenarioTestBlueprint { escrow.requestNextWithdrawalsBatch(96); - vm.expectRevert(); + vm.expectRevert(WithdrawalsBatchesQueue.EmptyBatch.selector); escrow.claimNextWithdrawalsBatch(0, new uint256[](0)); escrow.startRageQuitExtensionDelay(); diff --git a/test/unit/libraries/AssetsAccounting.t.sol b/test/unit/libraries/AssetsAccounting.t.sol index f41865cc..dfd8eaba 100644 --- a/test/unit/libraries/AssetsAccounting.t.sol +++ b/test/unit/libraries/AssetsAccounting.t.sol @@ -412,7 +412,7 @@ contract AssetsAccountingUnitTests is UnitTest { WithdrawalRequestStatus[] memory withdrawalRequestStatuses = new WithdrawalRequestStatus[](1); uint256[] memory unstETHIds = new uint256[](0); - vm.expectRevert(); + vm.expectRevert(stdError.assertionError); AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); } @@ -486,7 +486,7 @@ contract AssetsAccountingUnitTests is UnitTest { withdrawalRequestStatuses[withdrawalRequestStatuses.length - 1].isClaimed = true; - vm.expectRevert(); + vm.expectRevert(stdError.assertionError); AssetsAccounting.accountUnstETHLock(_accountingState, holder, unstETHIds, withdrawalRequestStatuses); } From b2db06734dd6ea0d696e969630bfa7bd797fbb7b Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:41:36 +0400 Subject: [PATCH 3/5] Add unit tests for WithdrawalBatchesQueue --- .../libraries/WithdrawalBatchesQueue.t.sol | 136 +++++++++++++++++- 1 file changed, 133 insertions(+), 3 deletions(-) diff --git a/test/unit/libraries/WithdrawalBatchesQueue.t.sol b/test/unit/libraries/WithdrawalBatchesQueue.t.sol index e823f77d..48f95d00 100644 --- a/test/unit/libraries/WithdrawalBatchesQueue.t.sol +++ b/test/unit/libraries/WithdrawalBatchesQueue.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +import {stdError} from "forge-std/StdError.sol"; import {UnitTest} from "test/utils/unit-test.sol"; import {WithdrawalsBatchesQueue, State} from "contracts/libraries/WithdrawalBatchesQueue.sol"; @@ -147,7 +148,8 @@ contract WithdrawalsBatchesQueueTest is UnitTest { // violate the order of the NFT ids unstETHIds[2] = unstETHIds[3]; - vm.expectRevert(); + vm.expectRevert(stdError.assertionError); + _batchesQueue.addUnstETHIds(unstETHIds); } @@ -285,6 +287,55 @@ contract WithdrawalsBatchesQueueTest is UnitTest { } } + function test_claimNextBatch_RevertWhen_EmptyQueue() external { + _openBatchesQueue(); + + vm.expectRevert(WithdrawalsBatchesQueue.EmptyBatch.selector); + _batchesQueue.claimNextBatch(100); + } + + function test_claimNextBatch_RevertWhen_NothingToClaim() external { + _openBatchesQueue(); + + uint256 unstETHIdsCount = 5; + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: unstETHIdsCount, firstUnstETHId: firstUnstETHId}); + + _batchesQueue.addUnstETHIds(unstETHIds); + assertEq(_batchesQueue.info.totalUnstETHIdsCount, unstETHIdsCount); + assertEq(_batchesQueue.batches.length, 2); + + uint256 maxUnstETHIdsCount = 5; + uint256[] memory claimedIds = _batchesQueue.claimNextBatch(maxUnstETHIdsCount); + + assertEq(claimedIds.length, maxUnstETHIdsCount); + assertEq(_batchesQueue.info.totalUnstETHIdsClaimed, maxUnstETHIdsCount); + + vm.expectRevert(WithdrawalsBatchesQueue.EmptyBatch.selector); + _batchesQueue.claimNextBatch(100); + } + + function test_claimNextBatch_RevertOn_AccountingError_TotalUnstETHClaimed_GT_TotalUnstETHCount() external { + _openBatchesQueue(); + + _batchesQueue.info.totalUnstETHIdsClaimed = 1; + vm.expectRevert(stdError.arithmeticError); + _batchesQueue.claimNextBatch(100); + } + + function test_claimNextBatch_RevertOn_AccountingError_LastClaimedBatchIndexOutOfArrayBounds() external { + _openBatchesQueue(); + + uint256 firstUnstETHId = _DEFAULT_BOUNDARY_UNST_ETH_ID + 1; + uint256[] memory unstETHIds = _generateFakeUnstETHIds({length: 1, firstUnstETHId: firstUnstETHId}); + + _batchesQueue.addUnstETHIds(unstETHIds); + + _batchesQueue.info.lastClaimedBatchIndex = 2; + vm.expectRevert(stdError.indexOOBError); + _batchesQueue.claimNextBatch(100); + } + // --- // close() // --- @@ -322,7 +373,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { // calcRequestAmounts() // --- - function test_calcRequestAmounts_HappyPath_WithoutReminder() external { + function test_calcRequestAmounts_HappyPath_WithoutRemainder() external { _openBatchesQueue(); assertEq(_batchesQueue.info.state, State.Opened); @@ -347,7 +398,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { } } - function test_calcRequestAmounts_HappyPath_WithReminder() external { + function test_calcRequestAmounts_HappyPath_WithRemainder() external { _openBatchesQueue(); assertEq(_batchesQueue.info.state, State.Opened); @@ -373,6 +424,13 @@ contract WithdrawalsBatchesQueueTest is UnitTest { } } + function test_calcRequestAmounts_RevertOn_DivisionByZero() external { + _openBatchesQueue(); + + vm.expectRevert(stdError.divisionError); + WithdrawalsBatchesQueue.calcRequestAmounts({minRequestAmount: 1, maxRequestAmount: 0, remainingAmount: 100}); + } + // --- // getNextWithdrawalsBatches() // --- @@ -474,6 +532,24 @@ contract WithdrawalsBatchesQueueTest is UnitTest { assertEq(_batchesQueue.info.totalUnstETHIdsClaimed, 0); } + function test_getNextWithdrawalsBatches_RevertOn_AccountingError_TotalUnstETHClaimed_GT_TotalUnstETHCount() + external + { + _openBatchesQueue(); + + _batchesQueue.info.totalUnstETHIdsClaimed = 1; + vm.expectRevert(stdError.arithmeticError); + _batchesQueue.getNextWithdrawalsBatches(10); + } + + function test_getNextWithdrawalsBatches_RevertOn_AccountingError_LastClaimedBatchIndexOutOfArrayBounds() external { + _openBatchesQueue(); + + _batchesQueue.info.lastClaimedBatchIndex = 2; + vm.expectRevert(stdError.indexOOBError); + _batchesQueue.getNextWithdrawalsBatches(10); + } + // --- // getBoundaryUnstETHId() // --- @@ -519,6 +595,28 @@ contract WithdrawalsBatchesQueueTest is UnitTest { assertEq(_batchesQueue.getTotalUnstETHIdsCount(), 5); } + // --- + // getTotalUnclaimedUnstETHIdsCount() + // --- + + function testFuzz_getTotalUnclaimedUnstETHIdsCount_HappyPath(uint64 totalCount, uint64 totalClaimed) external { + vm.assume(totalClaimed < totalCount); + + _batchesQueue.info.totalUnstETHIdsCount = totalCount; + _batchesQueue.info.totalUnstETHIdsClaimed = totalClaimed; + + uint256 res = _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + + assert(res == totalCount - totalClaimed); + } + + function testFuzz_getTotalUnclaimedUnstETHIdsCount_RevertOn_AccountingError_IncorrectTotals() external { + _batchesQueue.info.totalUnstETHIdsClaimed = 1; + + vm.expectRevert(stdError.arithmeticError); + _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); + } + // --- // getLastClaimedOrBoundaryUnstETHId() // --- @@ -551,6 +649,38 @@ contract WithdrawalsBatchesQueueTest is UnitTest { _batchesQueue.getLastClaimedOrBoundaryUnstETHId(); } + function test_getLastClaimedOrBoundaryUnstETHId_RevertOn_LastClaimedBatchIndexOutOfArrayBounds() external { + _openBatchesQueue(); + _batchesQueue.info.lastClaimedBatchIndex = 2; + + vm.expectRevert(stdError.indexOOBError); + _batchesQueue.getLastClaimedOrBoundaryUnstETHId(); + } + + // --- + // isAllBatchesClaimed() + // --- + + function testFuzz_isAllBatchesClaimed_HappyPath_ReturnsTrue(uint64 count) external { + _batchesQueue.info.totalUnstETHIdsClaimed = count; + _batchesQueue.info.totalUnstETHIdsCount = count; + + bool res = _batchesQueue.isAllBatchesClaimed(); + assert(res == true); + } + + function testFuzz_isAllBatchesClaimed_HappyPath_ReturnsFalse( + uint64 totalUnstETHClaimed, + uint64 totalUnstETHCount + ) external { + vm.assume(totalUnstETHClaimed != totalUnstETHCount); + _batchesQueue.info.totalUnstETHIdsClaimed = totalUnstETHClaimed; + _batchesQueue.info.totalUnstETHIdsCount = totalUnstETHCount; + + bool res = _batchesQueue.isAllBatchesClaimed(); + assert(res == false); + } + // --- // isClosed() // --- From 60d34bb38e0b98676bdc6b9f4e9ec69620c6c459 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Mon, 19 Aug 2024 13:58:59 +0400 Subject: [PATCH 4/5] Increase test coverage for Escrow contract --- contracts/interfaces/IEscrow.sol | 7 + test/mocks/DualGovernanceMock.sol | 57 +++++++ test/mocks/StETHMock.sol | 27 ++++ test/mocks/WithdrawalQueueMock.sol | 134 ++++++++++++++++ test/mocks/WstETHMock.sol | 19 +++ test/unit/Escrow.t.sol | 243 +++++++++++++++++++++++++++++ 6 files changed, 487 insertions(+) create mode 100644 test/mocks/DualGovernanceMock.sol create mode 100644 test/mocks/StETHMock.sol create mode 100644 test/mocks/WithdrawalQueueMock.sol create mode 100644 test/mocks/WstETHMock.sol create mode 100644 test/unit/Escrow.t.sol diff --git a/contracts/interfaces/IEscrow.sol b/contracts/interfaces/IEscrow.sol index dc0f8878..eaf8211f 100644 --- a/contracts/interfaces/IEscrow.sol +++ b/contracts/interfaces/IEscrow.sol @@ -2,14 +2,21 @@ pragma solidity 0.8.26; import {Duration} from "../types/Duration.sol"; +import {Timestamp} from "../types/Timestamp.sol"; import {PercentD16} from "../types/PercentD16.sol"; interface IEscrow { function initialize(Duration minAssetsLockDuration) external; function startRageQuit(Duration rageQuitExtraTimelock, Duration rageQuitWithdrawalsTimelock) external; + function requestNextWithdrawalsBatch(uint256 batchSize) external; + function claimNextWithdrawalsBatch(uint256 maxUnstETHIdsCount) external; + function claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) external; + function startRageQuitExtensionDelay() external; function isRageQuitFinalized() external view returns (bool); function getRageQuitSupport() external view returns (PercentD16 rageQuitSupport); function setMinAssetsLockDuration(Duration newMinAssetsLockDuration) external; + function isRageQuitExtensionDelayStarted() external view returns (bool); + function getRageQuitExtensionDelayStartedAt() external view returns (Timestamp); } diff --git a/test/mocks/DualGovernanceMock.sol b/test/mocks/DualGovernanceMock.sol new file mode 100644 index 00000000..dcd55d5e --- /dev/null +++ b/test/mocks/DualGovernanceMock.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; +import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; +import {IEscrow} from "contracts/interfaces/IEscrow.sol"; +import {Duration} from "contracts/types/Duration.sol"; + +contract DualGovernanceMock is IDualGovernance { + function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId) { + revert("Not Implemented"); + } + + function scheduleProposal(uint256 proposalId) external { + revert("Not Implemented"); + } + + function cancelAllPendingProposals() external { + revert("Not Implemented"); + } + + function canScheduleProposal(uint256 proposalId) external view returns (bool) { + revert("Not Implemented"); + } + + function activateNextState() external { + revert("Not Implemented"); + } + + function resealSealable(address sealables) external { + revert("Not Implemented"); + } + + function tiebreakerScheduleProposal(uint256 proposalId) external { + revert("Not Implemented"); + } + + function tiebreakerResumeSealable(address sealable) external { + revert("Not Implemented"); + } + + function initializeEscrow(IEscrow instance, Duration minAssetsLockDuration) external { + instance.initialize(minAssetsLockDuration); + } + + function startRageQuitForEscrow( + IEscrow instance, + Duration rageQuitExtensionDelay, + Duration rageQuitWithdrawalsTimelock + ) external { + instance.startRageQuit(rageQuitExtensionDelay, rageQuitWithdrawalsTimelock); + } + + function setMinAssetsLockDurationForEscrow(IEscrow instance, Duration newMinAssetsLockDuration) external { + instance.setMinAssetsLockDuration(newMinAssetsLockDuration); + } +} diff --git a/test/mocks/StETHMock.sol b/test/mocks/StETHMock.sol new file mode 100644 index 00000000..bcc65435 --- /dev/null +++ b/test/mocks/StETHMock.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; +import {IStETH} from "contracts/interfaces/IStETH.sol"; + +contract StETHMock is ERC20Mock, IStETH { + function getSharesByPooledEth(uint256 ethAmount) external view returns (uint256) { + revert("Not Implemented"); + } + + function getPooledEthByShares(uint256 sharesAmount) external view returns (uint256) { + revert("Not Implemented"); + } + + function transferShares(address to, uint256 amount) external { + revert("Not Implemented"); + } + + function transferSharesFrom( + address _sender, + address _recipient, + uint256 _sharesAmount + ) external returns (uint256) { + revert("Not Implemented"); + } +} diff --git a/test/mocks/WithdrawalQueueMock.sol b/test/mocks/WithdrawalQueueMock.sol new file mode 100644 index 00000000..7e4270e1 --- /dev/null +++ b/test/mocks/WithdrawalQueueMock.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +// import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; /*, ERC721("test", "test")*/ +import {IWithdrawalQueue, WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; + +contract WithdrawalQueueMock is IWithdrawalQueue { + uint256 _lastRequestId; + uint256 _lastFinalizedRequestId; + uint256 _minStETHWithdrawalAmount; + uint256 _maxStETHWithdrawalAmount; + uint256[] _requestWithdrawalsResult; + + constructor() {} + + function MIN_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256) { + return _minStETHWithdrawalAmount; + } + + function MAX_STETH_WITHDRAWAL_AMOUNT() external view returns (uint256) { + return _maxStETHWithdrawalAmount; + } + + function claimWithdrawals(uint256[] calldata requestIds, uint256[] calldata hints) external { + revert("Not Implemented"); + } + + function getLastRequestId() external view returns (uint256) { + return _lastRequestId; + } + + function getLastFinalizedRequestId() external view returns (uint256) { + return _lastFinalizedRequestId; + } + + function getWithdrawalStatus(uint256[] calldata _requestIds) + external + view + returns (WithdrawalRequestStatus[] memory statuses) + { + revert("Not Implemented"); + } + + /// @notice Returns amount of ether available for claim for each provided request id + /// @param _requestIds array of request ids + /// @param _hints checkpoint hints. can be found with `findCheckpointHints(_requestIds, 1, getLastCheckpointIndex())` + /// @return claimableEthValues amount of claimable ether for each request, amount is equal to 0 if request + /// is not finalized or already claimed + function getClaimableEther( + uint256[] calldata _requestIds, + uint256[] calldata _hints + ) external view returns (uint256[] memory claimableEthValues) { + revert("Not Implemented"); + } + + function findCheckpointHints( + uint256[] calldata _requestIds, + uint256 _firstIndex, + uint256 _lastIndex + ) external view returns (uint256[] memory hintIds) { + revert("Not Implemented"); + } + + function getLastCheckpointIndex() external view returns (uint256) { + revert("Not Implemented"); + } + + function requestWithdrawals( + uint256[] calldata _amounts, + address _owner + ) external returns (uint256[] memory requestIds) { + return _requestWithdrawalsResult; + } + + function balanceOf(address owner) external view returns (uint256 balance) { + revert("Not Implemented"); + } + + function ownerOf(uint256 tokenId) external view returns (address owner) { + revert("Not Implemented"); + } + + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external { + revert("Not Implemented"); + } + + function safeTransferFrom(address from, address to, uint256 tokenId) external { + revert("Not Implemented"); + } + + function transferFrom(address from, address to, uint256 tokenId) external { + revert("Not Implemented"); + } + + function approve(address to, uint256 tokenId) external { + revert("Not Implemented"); + } + + function setApprovalForAll(address operator, bool approved) external { + revert("Not Implemented"); + } + + function getApproved(uint256 tokenId) external view returns (address operator) { + revert("Not Implemented"); + } + + function isApprovedForAll(address owner, address operator) external view returns (bool) { + revert("Not Implemented"); + } + + function supportsInterface(bytes4 interfaceId) external view returns (bool) { + revert("Not Implemented"); + } + + function setLastRequestId(uint256 id) public { + _lastRequestId = id; + } + + function setLastFinalizedRequestId(uint256 id) public { + _lastFinalizedRequestId = id; + } + + function setMinStETHWithdrawalAmount(uint256 amount) public { + _minStETHWithdrawalAmount = amount; + } + + function setMaxStETHWithdrawalAmount(uint256 amount) public { + _maxStETHWithdrawalAmount = amount; + } + + function setRequestWithdrawalsResult(uint256[] memory requestIds) public { + _requestWithdrawalsResult = requestIds; + } +} diff --git a/test/mocks/WstETHMock.sol b/test/mocks/WstETHMock.sol new file mode 100644 index 00000000..0d89debc --- /dev/null +++ b/test/mocks/WstETHMock.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; +import {IWstETH} from "contracts/interfaces/IWstETH.sol"; + +contract WstETHMock is ERC20Mock, IWstETH { + function wrap(uint256 stETHAmount) external returns (uint256) { + revert("Not Implemented"); + } + + function unwrap(uint256 wstETHAmount) external returns (uint256) { + revert("Not Implemented"); + } + + function getStETHByWstETH(uint256 wstethAmount) external view returns (uint256) { + revert("Not Implemented"); + } +} diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol new file mode 100644 index 00000000..25617de2 --- /dev/null +++ b/test/unit/Escrow.t.sol @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; + +import {IEscrow} from "contracts/interfaces/IEscrow.sol"; +import {IStETH} from "contracts/interfaces/IStETH.sol"; +import {IWstETH} from "contracts/interfaces/IWstETH.sol"; +import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; +import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; +import {Escrow} from "contracts/Escrow.sol"; +import {EscrowState as EscrowStateLib, State as EscrowState} from "contracts/libraries/EscrowState.sol"; +import {WithdrawalsBatchesQueue} from "contracts/libraries/WithdrawalBatchesQueue.sol"; +import {Duration, Durations} from "contracts/types/Duration.sol"; +import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; +import {StETHMock} from "test/mocks/StETHMock.sol"; +import {WstETHMock} from "test/mocks/WstETHMock.sol"; +import {DualGovernanceMock} from "test/mocks/DualGovernanceMock.sol"; +import {WithdrawalQueueMock} from "test/mocks/WithdrawalQueueMock.sol"; +import {UnitTest} from "test/utils/unit-test.sol"; +import {LidoUtils} from "test/utils/lido-utils.sol"; +import {Random} from "test/utils/random.sol"; + +contract EscrowUnitTests is UnitTest { + Random.Context _random; + IStETH _stETH; + IWstETH _wstETH; + WithdrawalQueueMock _withdrawalQueue; + DualGovernanceMock _dualGovernance; + + function setUp() external { + _random = Random.create(block.timestamp); + _stETH = new StETHMock(); + _wstETH = new WstETHMock(); + _dualGovernance = new DualGovernanceMock(); + _withdrawalQueue = new WithdrawalQueueMock(); + } + + // --- + // Escrow constructor() + // --- + + function testFuzz_Escrow_constructor(uint256 size) external { + Escrow instance = new Escrow(_stETH, _wstETH, _withdrawalQueue, _dualGovernance, size); + + assertEq(address(instance.ST_ETH()), address(_stETH)); + assertEq(address(instance.WST_ETH()), address(_wstETH)); + assertEq(address(instance.WITHDRAWAL_QUEUE()), address(_withdrawalQueue)); + assertEq(address(instance.DUAL_GOVERNANCE()), address(_dualGovernance)); + assertEq(instance.MIN_WITHDRAWALS_BATCH_SIZE(), size); + } + + // --- + // initialize() + // --- + + function test_initialize_HappyPath() external { + vm.expectEmit(); + emit EscrowStateLib.EscrowStateChanged(EscrowState.NotInitialized, EscrowState.SignallingEscrow); + emit EscrowStateLib.MinAssetsLockDurationSet(Durations.ZERO); + + createInitializedEscrowProxy(100, Durations.ZERO); + } + + function test_initialize_RevertWhen_CalledNotViaProxy() external { + Escrow instance = createEscrow(100); + + vm.expectRevert(Escrow.NonProxyCallsForbidden.selector); + instance.initialize(Durations.ZERO); + } + + function test_initialize_RevertWhen_CalledNotFromDualGovernance() external { + IEscrow instance = createEscrowProxy(100); + + vm.expectRevert(abi.encodeWithSelector(Escrow.CallerIsNotDualGovernance.selector, address(this))); + + instance.initialize(Durations.ZERO); + } + + // --- + // requestNextWithdrawalsBatch() + // --- + + function test_requestNextWithdrawalsBatch_RevertOn_InvalidBatchSize() external { + IEscrow instance = createInitializedEscrowProxy(100, Durations.ZERO); + + uint256 lri = Random.nextUint256(_random, 100500); + _withdrawalQueue.setLastRequestId(lri); + + _dualGovernance.startRageQuitForEscrow(instance, Durations.from(1), Durations.from(2)); + + uint256 batchSize = 99; + vm.expectRevert(abi.encodeWithSelector(Escrow.InvalidBatchSize.selector, batchSize)); + instance.requestNextWithdrawalsBatch(99); + } + + // --- + // claimNextWithdrawalsBatch(uint256 fromUnstETHId, uint256[] calldata hints) + // --- + + function test_claimNextWithdrawalsBatch_2_RevertOn_UnexpectedUnstETHId() external { + IEscrow instance = createInitializedEscrowProxy(100, Durations.ZERO); + + uint256 lri = Random.nextUint256(_random, 100500); + _withdrawalQueue.setLastRequestId(lri); + _withdrawalQueue.setLastFinalizedRequestId(lri + 1); + + uint256[] memory withdrawalReqIds = new uint256[](1); + withdrawalReqIds[0] = lri + 1; + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.WithdrawalBatchesQueueOpened(lri); + _dualGovernance.startRageQuitForEscrow(instance, Durations.from(1), Durations.from(2)); + + _withdrawalQueue.setMinStETHWithdrawalAmount(0); + _withdrawalQueue.setMaxStETHWithdrawalAmount(20); + _withdrawalQueue.setRequestWithdrawalsResult(withdrawalReqIds); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.UnstETHIdsAdded(withdrawalReqIds); + instance.requestNextWithdrawalsBatch(100); + + vm.expectRevert(Escrow.UnexpectedUnstETHId.selector); + instance.claimNextWithdrawalsBatch(lri + 10, new uint256[](1)); + } + + function test_claimNextWithdrawalsBatch_2_RevertOn_InvalidHintsLength() external { + IEscrow instance = createInitializedEscrowProxy(100, Durations.ZERO); + + uint256 lri = Random.nextUint256(_random, 100500); + _withdrawalQueue.setLastRequestId(lri); + _withdrawalQueue.setLastFinalizedRequestId(lri + 1); + + uint256[] memory withdrawalReqIds = new uint256[](1); + withdrawalReqIds[0] = lri + 1; + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.WithdrawalBatchesQueueOpened(lri); + _dualGovernance.startRageQuitForEscrow(instance, Durations.from(1), Durations.from(2)); + + _withdrawalQueue.setMinStETHWithdrawalAmount(0); + _withdrawalQueue.setMaxStETHWithdrawalAmount(20); + _withdrawalQueue.setRequestWithdrawalsResult(withdrawalReqIds); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.UnstETHIdsAdded(withdrawalReqIds); + instance.requestNextWithdrawalsBatch(100); + + vm.expectRevert(abi.encodeWithSelector(Escrow.InvalidHintsLength.selector, 10, 1)); + instance.claimNextWithdrawalsBatch(lri + 1, new uint256[](10)); + } + + // --- + // startRageQuitExtensionDelay() + // --- + + function test_startRageQuitExtensionDelay_RevertOn_UnclaimedBatches() external { + IEscrow instance = createInitializedEscrowProxy(100, Durations.ZERO); + + uint256 lri = Random.nextUint256(_random, 100500); + _withdrawalQueue.setLastRequestId(lri); + _withdrawalQueue.setLastFinalizedRequestId(lri + 1); + + uint256[] memory withdrawalReqIds = new uint256[](1); + withdrawalReqIds[0] = lri + 1; + + _dualGovernance.startRageQuitForEscrow(instance, Durations.from(1), Durations.from(2)); + _withdrawalQueue.setMinStETHWithdrawalAmount(0); + _withdrawalQueue.setMaxStETHWithdrawalAmount(20); + _withdrawalQueue.setRequestWithdrawalsResult(withdrawalReqIds); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.UnstETHIdsAdded(withdrawalReqIds); + instance.requestNextWithdrawalsBatch(100); + + _withdrawalQueue.setMinStETHWithdrawalAmount(10); + + vm.expectEmit(); + emit WithdrawalsBatchesQueue.WithdrawalBatchesQueueClosed(); + instance.requestNextWithdrawalsBatch(100); + + vm.expectRevert(Escrow.UnclaimedBatches.selector); + instance.startRageQuitExtensionDelay(); + } + + // --- + // setMinAssetsLockDuration() + // --- + + function test_setMinAssetsLockDuration_HappyPath() external { + IEscrow instance = createInitializedEscrowProxy(100, Durations.ZERO); + + Duration newMinAssetsLockDuration = Durations.from(200); + vm.expectEmit(); + emit EscrowStateLib.MinAssetsLockDurationSet(newMinAssetsLockDuration); + _dualGovernance.setMinAssetsLockDurationForEscrow(instance, newMinAssetsLockDuration); + } + + // --- + // isRageQuitExtensionDelayStarted() + // --- + + function test_isRageQuitExtensionDelayStarted() external { + IEscrow instance = createInitializedEscrowProxy(100, Durations.ZERO); + + bool res = instance.isRageQuitExtensionDelayStarted(); + assert(res == false); + } + + // --- + // getRageQuitExtensionDelayStartedAt() + // --- + + function test_getRageQuitExtensionDelayStartedAt() external { + IEscrow instance = createInitializedEscrowProxy(100, Durations.ZERO); + + Timestamp res = instance.getRageQuitExtensionDelayStartedAt(); + assert(res.toSeconds() == Timestamps.ZERO.toSeconds()); + } + + // --- + // helper methods + // --- + + function createEscrow(uint256 size) internal returns (Escrow) { + return new Escrow(_stETH, _wstETH, _withdrawalQueue, _dualGovernance, size); + } + + function createEscrowProxy(uint256 minWithdrawalsBatchSize) internal returns (IEscrow) { + Escrow masterCopy = createEscrow(minWithdrawalsBatchSize); + return IEscrow(Clones.clone(address(masterCopy))); + } + + function createInitializedEscrowProxy( + uint256 minWithdrawalsBatchSize, + Duration minAssetsLockDuration + ) internal returns (IEscrow) { + IEscrow instance = createEscrowProxy(minWithdrawalsBatchSize); + + _dualGovernance.initializeEscrow(instance, minAssetsLockDuration); + return instance; + } +} From ff5c382b5b3781990873b6adbb1561e4e1d0c0b8 Mon Sep 17 00:00:00 2001 From: Artem G <175325367+sandstone-ag@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:03:01 +0400 Subject: [PATCH 5/5] Minor improvements in Escrow unit tests --- test/mocks/DualGovernanceMock.sol | 1 + test/mocks/StETHMock.sol | 1 + test/mocks/WithdrawalQueueMock.sol | 11 ++++++----- test/mocks/WstETHMock.sol | 1 + test/unit/Escrow.t.sol | 17 +++++++---------- .../unit/libraries/WithdrawalBatchesQueue.t.sol | 6 +++--- 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/test/mocks/DualGovernanceMock.sol b/test/mocks/DualGovernanceMock.sol index dcd55d5e..bb24c51b 100644 --- a/test/mocks/DualGovernanceMock.sol +++ b/test/mocks/DualGovernanceMock.sol @@ -6,6 +6,7 @@ import {ExternalCall} from "contracts/libraries/ExternalCalls.sol"; import {IEscrow} from "contracts/interfaces/IEscrow.sol"; import {Duration} from "contracts/types/Duration.sol"; +/* solhint-disable no-unused-vars,custom-errors */ contract DualGovernanceMock is IDualGovernance { function submitProposal(ExternalCall[] calldata calls) external returns (uint256 proposalId) { revert("Not Implemented"); diff --git a/test/mocks/StETHMock.sol b/test/mocks/StETHMock.sol index bcc65435..0b1f6727 100644 --- a/test/mocks/StETHMock.sol +++ b/test/mocks/StETHMock.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.26; import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; import {IStETH} from "contracts/interfaces/IStETH.sol"; +/* solhint-disable no-unused-vars,custom-errors */ contract StETHMock is ERC20Mock, IStETH { function getSharesByPooledEth(uint256 ethAmount) external view returns (uint256) { revert("Not Implemented"); diff --git a/test/mocks/WithdrawalQueueMock.sol b/test/mocks/WithdrawalQueueMock.sol index 7e4270e1..57290346 100644 --- a/test/mocks/WithdrawalQueueMock.sol +++ b/test/mocks/WithdrawalQueueMock.sol @@ -4,12 +4,13 @@ pragma solidity 0.8.26; // import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; /*, ERC721("test", "test")*/ import {IWithdrawalQueue, WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol"; +/* solhint-disable no-unused-vars,custom-errors */ contract WithdrawalQueueMock is IWithdrawalQueue { - uint256 _lastRequestId; - uint256 _lastFinalizedRequestId; - uint256 _minStETHWithdrawalAmount; - uint256 _maxStETHWithdrawalAmount; - uint256[] _requestWithdrawalsResult; + uint256 private _lastRequestId; + uint256 private _lastFinalizedRequestId; + uint256 private _minStETHWithdrawalAmount; + uint256 private _maxStETHWithdrawalAmount; + uint256[] private _requestWithdrawalsResult; constructor() {} diff --git a/test/mocks/WstETHMock.sol b/test/mocks/WstETHMock.sol index 0d89debc..a999635c 100644 --- a/test/mocks/WstETHMock.sol +++ b/test/mocks/WstETHMock.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.26; import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; import {IWstETH} from "contracts/interfaces/IWstETH.sol"; +/* solhint-disable no-unused-vars,custom-errors */ contract WstETHMock is ERC20Mock, IWstETH { function wrap(uint256 stETHAmount) external returns (uint256) { revert("Not Implemented"); diff --git a/test/unit/Escrow.t.sol b/test/unit/Escrow.t.sol index 25617de2..27d723ba 100644 --- a/test/unit/Escrow.t.sol +++ b/test/unit/Escrow.t.sol @@ -6,8 +6,6 @@ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {IEscrow} from "contracts/interfaces/IEscrow.sol"; import {IStETH} from "contracts/interfaces/IStETH.sol"; import {IWstETH} from "contracts/interfaces/IWstETH.sol"; -import {IDualGovernance} from "contracts/interfaces/IDualGovernance.sol"; -import {IWithdrawalQueue} from "contracts/interfaces/IWithdrawalQueue.sol"; import {Escrow} from "contracts/Escrow.sol"; import {EscrowState as EscrowStateLib, State as EscrowState} from "contracts/libraries/EscrowState.sol"; import {WithdrawalsBatchesQueue} from "contracts/libraries/WithdrawalBatchesQueue.sol"; @@ -18,15 +16,14 @@ import {WstETHMock} from "test/mocks/WstETHMock.sol"; import {DualGovernanceMock} from "test/mocks/DualGovernanceMock.sol"; import {WithdrawalQueueMock} from "test/mocks/WithdrawalQueueMock.sol"; import {UnitTest} from "test/utils/unit-test.sol"; -import {LidoUtils} from "test/utils/lido-utils.sol"; import {Random} from "test/utils/random.sol"; contract EscrowUnitTests is UnitTest { - Random.Context _random; - IStETH _stETH; - IWstETH _wstETH; - WithdrawalQueueMock _withdrawalQueue; - DualGovernanceMock _dualGovernance; + Random.Context private _random; + IStETH private _stETH; + IWstETH private _wstETH; + WithdrawalQueueMock private _withdrawalQueue; + DualGovernanceMock private _dualGovernance; function setUp() external { _random = Random.create(block.timestamp); @@ -204,7 +201,7 @@ contract EscrowUnitTests is UnitTest { IEscrow instance = createInitializedEscrowProxy(100, Durations.ZERO); bool res = instance.isRageQuitExtensionDelayStarted(); - assert(res == false); + assertFalse(res); } // --- @@ -215,7 +212,7 @@ contract EscrowUnitTests is UnitTest { IEscrow instance = createInitializedEscrowProxy(100, Durations.ZERO); Timestamp res = instance.getRageQuitExtensionDelayStartedAt(); - assert(res.toSeconds() == Timestamps.ZERO.toSeconds()); + assertEq(res.toSeconds(), Timestamps.ZERO.toSeconds()); } // --- diff --git a/test/unit/libraries/WithdrawalBatchesQueue.t.sol b/test/unit/libraries/WithdrawalBatchesQueue.t.sol index 48f95d00..b5dc5af5 100644 --- a/test/unit/libraries/WithdrawalBatchesQueue.t.sol +++ b/test/unit/libraries/WithdrawalBatchesQueue.t.sol @@ -607,7 +607,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { uint256 res = _batchesQueue.getTotalUnclaimedUnstETHIdsCount(); - assert(res == totalCount - totalClaimed); + assertEq(res, totalCount - totalClaimed); } function testFuzz_getTotalUnclaimedUnstETHIdsCount_RevertOn_AccountingError_IncorrectTotals() external { @@ -666,7 +666,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { _batchesQueue.info.totalUnstETHIdsCount = count; bool res = _batchesQueue.isAllBatchesClaimed(); - assert(res == true); + assertTrue(res); } function testFuzz_isAllBatchesClaimed_HappyPath_ReturnsFalse( @@ -678,7 +678,7 @@ contract WithdrawalsBatchesQueueTest is UnitTest { _batchesQueue.info.totalUnstETHIdsCount = totalUnstETHCount; bool res = _batchesQueue.isAllBatchesClaimed(); - assert(res == false); + assertFalse(res); } // ---