diff --git a/test/scenario/escrow.t.sol b/test/scenario/escrow.t.sol index 746e923e..252dc7da 100644 --- a/test/scenario/escrow.t.sol +++ b/test/scenario/escrow.t.sol @@ -11,6 +11,7 @@ import {WithdrawalRequestStatus} from "contracts/interfaces/IWithdrawalQueue.sol import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; import {Escrow, VetoerState, LockedAssetsTotals, WithdrawalsBatchesQueue} from "contracts/Escrow.sol"; +import {AssetsAccounting, UnstETHRecordStatus} from "contracts/libraries/AssetsAccounting.sol"; import {ScenarioTestBlueprint, LidoUtils, console} from "../utils/scenario-test-blueprint.sol"; @@ -544,6 +545,80 @@ contract EscrowHappyPath is ScenarioTestBlueprint { this.externalUnlockStETH(_VETOER_1); } + function testFork_EdgeCase_frontRunningClaimUnStethFromBatchIsForbidden() external { + // Prepare vetoer1 unstETH nft to lock in Escrow + uint256 requestAmount = 10 * 1e18; + uint256[] memory amounts = new uint256[](1); + amounts[0] = requestAmount; + vm.prank(_VETOER_1); + uint256[] memory unstETHIdsVetoer1 = _lido.withdrawalQueue.requestWithdrawals(amounts, _VETOER_1); + + // Should be the same as vetoer1 unstETH nft + uint256 lastRequestIdBeforeBatch = _lido.withdrawalQueue.getLastRequestId(); + + // Lock unstETH nfts + _lockUnstETH(_VETOER_1, unstETHIdsVetoer1); + // Lock stETH to generate batch + _lockStETH(_VETOER_1, 20 * requestAmount); + + vm.prank(address(_dualGovernance)); + escrow.startRageQuit(_RAGE_QUIT_EXTRA_TIMELOCK, _RAGE_QUIT_WITHDRAWALS_TIMELOCK); + + uint256 batchSizeLimit = 16; + // Generate batch with stETH locked in Escrow + escrow.requestNextWithdrawalsBatch(batchSizeLimit); + + uint256[] memory nextWithdrawalBatch = escrow.getNextWithdrawalBatch(batchSizeLimit); + assertEq(nextWithdrawalBatch.length, 1); + assertEq(nextWithdrawalBatch[0], _lido.withdrawalQueue.getLastRequestId()); + + // Should be the id of unstETH nft in the batch + uint256 requestIdFromBatch = nextWithdrawalBatch[0]; + + // validate that the new unstEth nft is created + assertEq(requestIdFromBatch, lastRequestIdBeforeBatch + 1); + + _finalizeWithdrawalQueue(); + + // Check that unstETH nft of vetoer1 could be claimed + uint256[] memory unstETHIdsToClaim = new uint256[](1); + unstETHIdsToClaim[0] = lastRequestIdBeforeBatch; + uint256[] memory hints = _lido.withdrawalQueue.findCheckpointHints( + unstETHIdsToClaim, 1, _lido.withdrawalQueue.getLastCheckpointIndex() + ); + escrow.claimUnstETH(unstETHIdsToClaim, hints); + + // The attempt to claim funds of untEth from Escrow generated batch will fail + unstETHIdsToClaim[0] = requestIdFromBatch; + hints = _lido.withdrawalQueue.findCheckpointHints( + unstETHIdsToClaim, 1, _lido.withdrawalQueue.getLastCheckpointIndex() + ); + vm.expectRevert( + abi.encodeWithSelector( + AssetsAccounting.InvalidUnstETHStatus.selector, requestIdFromBatch, UnstETHRecordStatus.NotLocked + ) + ); + escrow.claimUnstETH(unstETHIdsToClaim, hints); + + // The rage quit process can be successfully finished + while (escrow.getUnclaimedUnstETHIdsCount() > 0) { + escrow.claimNextWithdrawalsBatch(batchSizeLimit); + } + + escrow.startRageQuitExtensionPeriod(); + assertEq(escrow.isRageQuitFinalized(), false); + + _wait(_RAGE_QUIT_EXTRA_TIMELOCK.plusSeconds(1)); + assertEq(escrow.isRageQuitFinalized(), true); + + _wait(_RAGE_QUIT_WITHDRAWALS_TIMELOCK.plusSeconds(1)); + + vm.startPrank(_VETOER_1); + escrow.withdrawETH(); + escrow.withdrawETH(unstETHIdsVetoer1); + vm.stopPrank(); + } + // --- // Helper external methods to test reverts // ---