From 2c24ef0b9ca5cfbf8c7703558ac10169c32bacfb Mon Sep 17 00:00:00 2001 From: 0xtechdean <“dean@othentic.xyz”> Date: Wed, 28 Jan 2026 11:54:59 +0200 Subject: [PATCH] Simplify unallocation to sync-only flow Move syncRedeem from requestUnallocate to completeUnallocate, eliminating async flow and proportional distribution. Each user now gets exactly their pending amount via direct syncRedeem call during completion. - Remove async flow logic and proportional distribution - Add rescueTokens function for owner to recover surplus tokens - Update tests to reflect new sync-only behavior - Remove obsolete skip_test_ functions that tested old async flow - Reduce bytecode by ~700 bytes (now 23,750 bytes with 826 margin) --- script/OnboardOperator.s.sol | 5 +- src/AlephAVS.sol | 105 ++-- test/AlephAVS.t.sol | 216 ++------ test/UnallocateEdgeCases.t.sol | 964 ++------------------------------- 4 files changed, 144 insertions(+), 1146 deletions(-) diff --git a/script/OnboardOperator.s.sol b/script/OnboardOperator.s.sol index 0780788..cdbfba2 100644 --- a/script/OnboardOperator.s.sol +++ b/script/OnboardOperator.s.sol @@ -65,6 +65,7 @@ contract OnboardOperator is Script { // ALLOCATION_CONFIGURATION_DELAY is 126,000 blocks on mainnet (~17.5 days at 12s/block) // This is a constant set in AllocationManager but not exposed via IAllocationManager interface uint32 constant ALLOCATION_CONFIGURATION_DELAY = 126_000; + function run() external { // Get operator private key uint256 operatorPrivateKey = getOperatorPrivateKey(); @@ -438,7 +439,9 @@ contract OnboardOperator is Script { console.log(" [SKIP] Step 4: Stake allocation (requires Step 3)"); console.log(" [OK] Step 5: Operator AVS split set to 0"); console.log("\nNext steps:"); - console.log(" 1. Wait for ALLOCATION_CONFIGURATION_DELAY (~%s days / %s blocks)", estimatedDays, configDelay); + console.log( + " 1. Wait for ALLOCATION_CONFIGURATION_DELAY (~%s days / %s blocks)", estimatedDays, configDelay + ); console.log(" 2. Re-run this script to complete Steps 2, 3, and 4"); vm.stopBroadcast(); return; diff --git a/src/AlephAVS.sol b/src/AlephAVS.sol index 31cedc8..a1390ae 100644 --- a/src/AlephAVS.sol +++ b/src/AlephAVS.sol @@ -27,7 +27,6 @@ import {IMintableBurnableERC20} from "./interfaces/IMintableBurnableERC20.sol"; import {AlephSlashing} from "./libraries/AlephSlashing.sol"; import {AlephVaultManagement} from "./libraries/AlephVaultManagement.sol"; import {RewardsManagement} from "./libraries/RewardsManagement.sol"; -import {UnallocateManagement} from "./libraries/UnallocateManagement.sol"; import {AlephValidation} from "./libraries/AlephValidation.sol"; contract AlephAVS is IAlephAVS, AlephAVSPausable, ReentrancyGuard { @@ -161,47 +160,42 @@ contract AlephAVS is IAlephAVS, AlephAVSPausable, ReentrancyGuard { * @param _alephVault The Aleph vault address * @return userPendingAmount The user's pending unallocation amount * @return totalPendingAmount The total pending unallocation amount for the vault - * @return redeemableAmount The amount currently redeemable from the vault - * @return canComplete Whether the user can complete unallocation (has pending amount and vault has redeemable amount) + * @return vaultBalance The vault's underlying token balance (indicates if syncRedeem will work) + * @return canComplete Whether the user can complete (has pending and vault has sufficient balance) */ function getPendingUnallocateStatus(address _user, address _alephVault) external view - returns (uint256 userPendingAmount, uint256 totalPendingAmount, uint256 redeemableAmount, bool canComplete) + returns (uint256 userPendingAmount, uint256 totalPendingAmount, uint256 vaultBalance, bool canComplete) { AVSStorage storage $ = _getAVSStorage(); userPendingAmount = $.pendingUnallocate[_user][_alephVault]; totalPendingAmount = $.totalPendingUnallocate[_alephVault]; - // Get redeemable amount from vault - redeemableAmount = IAlephVault(_alephVault).redeemableAmount(address(this)); + // Check vault's underlying token balance to see if syncRedeem will work + address _underlyingToken = IAlephVault(_alephVault).underlyingToken(); + vaultBalance = IERC20(_underlyingToken).balanceOf(_alephVault); - // User can complete if they have pending amount and vault has redeemable amount - canComplete = userPendingAmount > 0 && redeemableAmount > 0; + // User can complete if they have pending amount and vault has enough balance + canComplete = userPendingAmount > 0 && vaultBalance >= userPendingAmount; } /** * @notice Calculates the expected amount that will be withdrawn in completeUnallocate - * @dev View function to get expected amount for signature generation. See interface documentation for details. + * @dev View function to get expected amount for signature generation. + * With the new sync flow, user receives their full pending amount. * * @param _user The user address to calculate for * @param _alephVault The Aleph vault address - * @return expectedAmount The expected amount that will be withdrawn and deposited to strategy + * @return expectedAmount The expected amount (equals user's pending amount) */ function calculateCompleteUnallocateAmount(address _user, address _alephVault) external view returns (uint256 expectedAmount) { - AVSStorage storage $ = _getAVSStorage(); - uint256 _userPendingAmount = $.pendingUnallocate[_user][_alephVault]; - uint256 _totalPending = $.totalPendingUnallocate[_alephVault]; - uint256 _vaultRedeemableAmount = IAlephVault(_alephVault).redeemableAmount(address(this)); - uint256 _withdrawnAmount = $.vaultWithdrawnAmount[_alephVault]; - - expectedAmount = UnallocateManagement.calculateCompleteUnallocateAmountView( - _userPendingAmount, _totalPending, _vaultRedeemableAmount, _withdrawnAmount - ); + // User receives their full pending amount via syncRedeem + expectedAmount = _getAVSStorage().pendingUnallocate[_user][_alephVault]; } function registerOperator( @@ -263,6 +257,14 @@ contract AlephAVS is IAlephAVS, AlephAVSPausable, ReentrancyGuard { ); } + /// @notice Rescues tokens stuck in the contract + /// @param _token The token address to rescue + /// @param _to The recipient address + /// @param _amount The amount to rescue + function rescueTokens(address _token, address _to, uint256 _amount) external onlyRole(OWNER) { + IERC20(_token).transfer(_to, _amount); + } + function allocate(address _alephVault, IAlephVaultDeposit.RequestDepositParams calldata _requestDepositParams) external nonReentrant @@ -322,12 +324,13 @@ contract AlephAVS is IAlephAVS, AlephAVSPausable, ReentrancyGuard { } /** - * @notice Requests to unallocate funds by redeeming slashed tokens from vault - * @dev First step of two-step unallocate flow. See interface documentation for detailed usage. + * @notice Requests to unallocate funds by burning slashed tokens + * @dev First step of two-step unallocate flow. Emits UnallocateRequested event for manager notification. + * Manager should ensure vault has liquidity before user calls completeUnallocate. * * @param _alephVault The Aleph vault address to unallocate from * @param _tokenAmount The amount of slashed strategy tokens to unallocate - * @return batchId The batch ID for the redeem request + * @return batchId Always 0 (sync flow) * @return estAmountToRedeem The estimated amount that will be redeemed from the vault */ function requestUnallocate(address _alephVault, uint256 _tokenAmount) @@ -350,15 +353,8 @@ contract AlephAVS is IAlephAVS, AlephAVSPausable, ReentrancyGuard { AlephVaultManagement.validateAndBurnSlashedTokens(msg.sender, _slashedStrategy, _tokenAmount, address(this)); - IAlephVaultRedeem.RedeemRequestParams memory _p = - IAlephVaultRedeem.RedeemRequestParams({classId: _classId, estAmountToRedeem: estAmountToRedeem}); - try IAlephVaultRedeem(_alephVault).syncRedeem(_p) { - $.vaultWithdrawnAmount[_alephVault] += estAmountToRedeem; - batchId = 0; - } catch { - batchId = IAlephVaultRedeem(_alephVault).requestRedeem(_p); - } - + // Record pending amount - syncRedeem will be called in completeUnallocate + batchId = 0; $.pendingUnallocate[msg.sender][_alephVault] += estAmountToRedeem; $.totalPendingUnallocate[_alephVault] += estAmountToRedeem; @@ -370,8 +366,8 @@ contract AlephAVS is IAlephAVS, AlephAVSPausable, ReentrancyGuard { } /** - * @notice Completes the unallocation by withdrawing redeemable amount and depositing back to strategy - * @dev Second step of two-step unallocate flow. See interface documentation for detailed usage. + * @notice Completes the unallocation by calling syncRedeem and depositing back to strategy + * @dev Second step of two-step unallocate flow. Manager must ensure vault has liquidity first. * * @param _alephVault The Aleph vault address to complete unallocation from * @param _strategyDepositExpiry The expiry timestamp for the strategy deposit signature @@ -398,44 +394,19 @@ contract AlephAVS is IAlephAVS, AlephAVSPausable, ReentrancyGuard { uint256 _userPendingAmount = $.pendingUnallocate[msg.sender][_alephVault]; if (_userPendingAmount == 0) revert NoPendingUnallocation(); - // Calculate expected amount first (before withdrawing) so signature can be generated - // Use the stored estAmountToRedeem as the expected amount - uint256 _totalPending = $.totalPendingUnallocate[_alephVault]; - if (_totalPending == 0 || _totalPending < _userPendingAmount) revert InvalidAmount(); - - uint256 _vaultRedeemableAmount = IAlephVault(_alephVault).redeemableAmount(address(this)); - uint256 _withdrawnAmount = $.vaultWithdrawnAmount[_alephVault]; - - // Calculate expected amount using view function (before state changes) - uint256 _expectedAmount = UnallocateManagement.calculateCompleteUnallocateAmountView( - _userPendingAmount, _totalPending, _vaultRedeemableAmount, _withdrawnAmount - ); - - if (_expectedAmount == 0) revert InvalidAmount(); - - // Step 1: Withdraw redeemable amount from vault and calculate total available - uint256 _totalRedeemableAmount = UnallocateManagement.withdrawAndCalculateAvailable( - _alephVault, _vaultToken, _vaultRedeemableAmount, _withdrawnAmount - ); + // Call syncRedeem to get funds from vault + IAlephVaultRedeem.RedeemRequestParams memory _p = + IAlephVaultRedeem.RedeemRequestParams({classId: _classId, estAmountToRedeem: _userPendingAmount}); + IAlephVaultRedeem(_alephVault).syncRedeem(_p); - // Step 2: Validate expected amount but don't cap it - _amount = _expectedAmount; - if (_amount == 0) revert InvalidAmount(); - if (_amount > _totalRedeemableAmount) revert InvalidAmount(); - uint256 _contractBalance = _vaultToken.balanceOf(address(this)); - if (_amount > _contractBalance) revert InvalidAmount(); - - // Step 3: Update storage - // Calculate new values using _totalRedeemableAmount directly (total available after withdrawal) - (uint256 _newVaultWithdrawnAmount, uint256 _newTotalPending) = UnallocateManagement.calculateUnallocationStorageUpdates( - _totalRedeemableAmount, _amount, _userPendingAmount, _totalPending - ); + // User receives their full pending amount + _amount = _userPendingAmount; - $.vaultWithdrawnAmount[_alephVault] = _newVaultWithdrawnAmount; + // Update storage $.pendingUnallocate[msg.sender][_alephVault] = 0; - $.totalPendingUnallocate[_alephVault] = _newTotalPending; + $.totalPendingUnallocate[_alephVault] -= _userPendingAmount; - // Step 4: Deposit to strategy (interaction) + // Deposit to strategy _shares = AlephVaultManagement.depositToOriginalStrategy( STRATEGY_MANAGER, _originalStrategy, diff --git a/test/AlephAVS.t.sol b/test/AlephAVS.t.sol index aee0bbf..c68627c 100644 --- a/test/AlephAVS.t.sol +++ b/test/AlephAVS.t.sol @@ -654,6 +654,12 @@ contract AlephAVSTest is Test { // Initialize vault _initializeVaultForTests(); + + // Enable sync redeem for the vault (required since we removed async fallback) + alephVault.enableSyncRedeem(CLASS_ID); + + // Fund the vault with tokens for sync redeem operations + underlyingToken.transfer(address(alephVault), STAKE_AMOUNT * 5); } function test_OperatorAllocatesToAlephVault() public { @@ -914,14 +920,8 @@ contract AlephAVSTest is Test { ); uint256 expectedRedeemAmount = ERC4626Math.previewRedeem(unallocateAmount, unallocateAmount, unallocateAmount); - // Mock syncRedeem to revert (async only by default) - vm.mockCallRevert( - address(alephVault), - abi.encodeCall( - IAlephVaultRedeem.syncRedeem, (IAlephVaultRedeem.RedeemRequestParams(CLASS_ID, expectedRedeemAmount)) - ), - abi.encodeWithSelector(IAlephVaultRedeem.OnlyAsyncRedeemAllowed.selector) - ); + // syncRedeem is enabled in setUp via enableSyncRedeem(CLASS_ID) + // The real MinimalAlephVault will handle the sync redeem // Mock depositIntoStrategyWithSignature vm.mockCall( @@ -930,31 +930,12 @@ contract AlephAVSTest is Test { abi.encode(expectedRedeemAmount) // Return shares ); - // Mock requestRedeem for async flow + // For sync flow: redeemableAmount returns 0 (funds already withdrawn by syncRedeem) + // The funds are in the contract and tracked via vaultWithdrawnAmount vm.mockCall( - address(alephVault), - abi.encodeCall( - IAlephVaultRedeem.requestRedeem, (IAlephVaultRedeem.RedeemRequestParams(CLASS_ID, expectedRedeemAmount)) - ), - abi.encode(uint48(0)) - ); - - // Mock redeemableAmount and withdrawRedeemableAmount for completeUnallocate - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(expectedRedeemAmount) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(expectedRedeemAmount) + address(alephVault), abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), abi.encode(0) ); + // Don't need to mock withdrawRedeemableAmount since redeemableAmount is 0 // Mock vault token approve vm.mockCall(address(underlyingToken), abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true)); @@ -1286,6 +1267,9 @@ contract AlephAVSTest is Test { alephAVS.allocate(address(alephVault), params); } + /** + * @notice Test: Unallocate with different price per share (vault appreciation) + */ function test_Unallocate_DifferentPricePerShare() public { address tokenHolder = makeAddr("tokenHolder"); uint256 unallocateAmount = ALLOCATE_AMOUNT; @@ -1313,28 +1297,9 @@ contract AlephAVSTest is Test { vm.mockCall(MOCK_SLASHED_TOKEN, abi.encodeWithSelector(IERC20.totalSupply.selector), abi.encode(1e18)); uint256 expectedRedeemAmount = ERC4626Math.previewRedeem(unallocateAmount, highPPS, 1e18); - vm.mockCall( - address(alephVault), - abi.encodeCall( - IAlephVaultRedeem.requestRedeem, (IAlephVaultRedeem.RedeemRequestParams(CLASS_ID, expectedRedeemAmount)) - ), - abi.encode(uint48(0)) - ); - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(expectedRedeemAmount) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(expectedRedeemAmount) - ); + + // Ensure vault has enough tokens for syncRedeem + underlyingToken.transfer(address(alephVault), expectedRedeemAmount); vm.mockCall( MOCK_STRATEGY_MANAGER, @@ -1353,6 +1318,9 @@ contract AlephAVSTest is Test { assertEq(shares, expectedRedeemAmount, "Shares should match"); } + /** + * @notice Test: Unallocate partial amount + */ function test_Unallocate_PartialAmount() public { address tokenHolder = makeAddr("tokenHolder"); uint256 totalAmount = ALLOCATE_AMOUNT; @@ -1379,28 +1347,9 @@ contract AlephAVSTest is Test { ); uint256 expectedRedeemAmount = ERC4626Math.previewRedeem(partialAmount, totalAmount, totalAmount); - vm.mockCall( - address(alephVault), - abi.encodeCall( - IAlephVaultRedeem.requestRedeem, (IAlephVaultRedeem.RedeemRequestParams(CLASS_ID, expectedRedeemAmount)) - ), - abi.encode(uint48(0)) - ); - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(expectedRedeemAmount) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(expectedRedeemAmount) - ); + + // Ensure vault has enough tokens for syncRedeem + underlyingToken.transfer(address(alephVault), expectedRedeemAmount); vm.mockCall( MOCK_STRATEGY_MANAGER, @@ -1419,6 +1368,9 @@ contract AlephAVSTest is Test { assertEq(shares, expectedRedeemAmount, "Shares should match"); } + /** + * @notice Test: Multiple sequential unallocations by same user + */ function test_Unallocate_MultipleUnallocations() public { address tokenHolder = makeAddr("tokenHolder"); uint256 totalAmount = ALLOCATE_AMOUNT; @@ -1448,29 +1400,10 @@ contract AlephAVSTest is Test { ); uint256 firstExpectedRedeemAmount = ERC4626Math.previewRedeem(firstUnallocate, totalAmount, totalAmount); - vm.mockCall( - address(alephVault), - abi.encodeCall( - IAlephVaultRedeem.requestRedeem, - (IAlephVaultRedeem.RedeemRequestParams(CLASS_ID, firstExpectedRedeemAmount)) - ), - abi.encode(uint48(0)) - ); - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(firstExpectedRedeemAmount) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(firstExpectedRedeemAmount) - ); + + // Fund vault for first syncRedeem + underlyingToken.transfer(address(alephVault), firstExpectedRedeemAmount); + vm.mockCall( MOCK_STRATEGY_MANAGER, abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), @@ -1496,37 +1429,16 @@ contract AlephAVSTest is Test { abi.encode() ); - // Calculate expected redeem amount for second unallocation uint256 secondExpectedRedeemAmount = ERC4626Math.previewRedeem(secondUnallocate, totalAmount, totalAmount); - vm.mockCall( - address(alephVault), - abi.encodeCall( - IAlephVaultRedeem.requestRedeem, - (IAlephVaultRedeem.RedeemRequestParams(CLASS_ID, secondExpectedRedeemAmount)) - ), - abi.encode(uint48(0)) - ); - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(secondExpectedRedeemAmount) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(secondExpectedRedeemAmount) - ); + + // Fund vault for second syncRedeem + underlyingToken.transfer(address(alephVault), secondExpectedRedeemAmount); + vm.mockCall( MOCK_STRATEGY_MANAGER, abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), abi.encode(secondExpectedRedeemAmount) ); - vm.mockCall(address(underlyingToken), abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true)); vm.prank(tokenHolder); alephAVS.requestUnallocate(address(alephVault), secondUnallocate); @@ -1785,6 +1697,9 @@ contract AlephAVSTest is Test { // (This is tested implicitly - the nonReentrant modifier prevents re-entry) } + /** + * @notice Test: Reentrancy protection on unallocate + */ function test_ReentrancyProtection_Unallocate() public { address tokenHolder = makeAddr("tokenHolder"); uint256 unallocateAmount = ALLOCATE_AMOUNT; @@ -1812,28 +1727,10 @@ contract AlephAVSTest is Test { ); uint256 expectedRedeemAmount = ERC4626Math.previewRedeem(unallocateAmount, unallocateAmount, unallocateAmount); - vm.mockCall( - address(alephVault), - abi.encodeCall( - IAlephVaultRedeem.requestRedeem, (IAlephVaultRedeem.RedeemRequestParams(CLASS_ID, expectedRedeemAmount)) - ), - abi.encode(uint48(0)) - ); - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(expectedRedeemAmount) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(expectedRedeemAmount) - ); + + // Fund vault for syncRedeem + underlyingToken.transfer(address(alephVault), expectedRedeemAmount); + vm.mockCall( MOCK_STRATEGY_MANAGER, abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), @@ -1942,6 +1839,9 @@ contract AlephAVSTest is Test { alephAVS.allocate(address(alephVault), params); } + /** + * @notice Test: Unallocate emits correct event + */ function test_Unallocate_EmitsCorrectEvent() public { address tokenHolder = makeAddr("tokenHolder"); uint256 unallocateAmount = ALLOCATE_AMOUNT; @@ -1969,28 +1869,10 @@ contract AlephAVSTest is Test { ); uint256 expectedRedeemAmount = ERC4626Math.previewRedeem(unallocateAmount, unallocateAmount, unallocateAmount); - vm.mockCall( - address(alephVault), - abi.encodeCall( - IAlephVaultRedeem.requestRedeem, (IAlephVaultRedeem.RedeemRequestParams(CLASS_ID, expectedRedeemAmount)) - ), - abi.encode(uint48(0)) - ); - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(expectedRedeemAmount) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(expectedRedeemAmount) - ); + + // Fund vault for syncRedeem + underlyingToken.transfer(address(alephVault), expectedRedeemAmount); + vm.mockCall( MOCK_STRATEGY_MANAGER, abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), diff --git a/test/UnallocateEdgeCases.t.sol b/test/UnallocateEdgeCases.t.sol index 3063872..824b212 100644 --- a/test/UnallocateEdgeCases.t.sol +++ b/test/UnallocateEdgeCases.t.sol @@ -25,140 +25,72 @@ contract UnallocateEdgeCasesTest is AlephAVSTest { address user3 = makeAddr("user3"); /** - * @notice Test: Multiple users with proportional distribution - * @dev User1 requests 100, User2 requests 200, User3 requests 100 - * Total pending: 400, redeemable: 300 - * Expected: User1 gets 75, User2 gets 150, User3 gets 75 + * @notice Test: Multiple users complete unallocations with sync flow + * @dev Each user gets exactly their pending amount via syncRedeem */ - function test_CompleteUnallocate_MultipleUsers_ProportionalDistribution() public { + function test_CompleteUnallocate_MultipleUsers_SyncFlow() public { uint256 user1Amount = 100e18; uint256 user2Amount = 200e18; uint256 user3Amount = 100e18; - uint256 totalPending = user1Amount + user2Amount + user3Amount; // 400e18 + uint256 totalPending = user1Amount + user2Amount + user3Amount; - // Setup: All users have slashed tokens + // Setup mocks vm.mockCall(MOCK_SLASHED_TOKEN, abi.encodeCall(IERC20.balanceOf, (user1)), abi.encode(user1Amount)); vm.mockCall(MOCK_SLASHED_TOKEN, abi.encodeCall(IERC20.balanceOf, (user2)), abi.encode(user2Amount)); vm.mockCall(MOCK_SLASHED_TOKEN, abi.encodeCall(IERC20.balanceOf, (user3)), abi.encode(user3Amount)); - - // Mock calculateAmountToRedeem (1:1 for simplicity) vm.mockCall(MOCK_SLASHED_TOKEN, abi.encodeWithSelector(IERC20.totalSupply.selector), abi.encode(totalPending)); vm.mockCall( address(alephVault), abi.encodeCall(IAlephVault.assetsPerClassOf, (CLASS_ID, address(alephAVS))), abi.encode(totalPending) ); + vm.mockCall(address(underlyingToken), abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true)); - // User1 requests unallocation + // All users request unallocation _mockRequestUnallocate(user1, user1Amount, user1Amount); vm.prank(user1); alephAVS.requestUnallocate(address(alephVault), user1Amount); - // User2 requests unallocation _mockRequestUnallocate(user2, user2Amount, user2Amount); vm.prank(user2); alephAVS.requestUnallocate(address(alephVault), user2Amount); - // User3 requests unallocation _mockRequestUnallocate(user3, user3Amount, user3Amount); vm.prank(user3); alephAVS.requestUnallocate(address(alephVault), user3Amount); - // Vault has 300e18 redeemable (less than total pending 400e18) - uint256 redeemableAmount = 300e18; - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(redeemableAmount) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(redeemableAmount) - ); - vm.mockCall(address(underlyingToken), abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true)); - - // Expected amounts (proportional: userPending / totalPending * redeemable) - uint256 expectedUser1 = (user1Amount * redeemableAmount) / totalPending; // 75e18 - uint256 expectedUser2 = (user2Amount * redeemableAmount) / totalPending; // 150e18 - uint256 expectedUser3 = (user3Amount * redeemableAmount) / totalPending; // 75e18 - - // User1 completes unallocation + // User1 completes - gets exact pending via syncRedeem + underlyingToken.transfer(address(alephVault), user1Amount); vm.mockCall( MOCK_STRATEGY_MANAGER, abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(expectedUser1) + abi.encode(user1Amount) ); vm.prank(user1); - (uint256 amount1, uint256 shares1) = - alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount1, expectedUser1, "User1 should get proportional share"); - assertEq(shares1, expectedUser1, "User1 shares should match amount"); - - // Update mocks for User2 - // After User1 completes, the vaultWithdrawnAmount is updated to totalRedeemableAmount - amount1 - // But since vault still has no new redeemable, we need to update the withdrawn amount - uint256 remainingAfterUser1 = redeemableAmount - expectedUser1; // 225e18 - uint256 totalPendingAfterUser1 = totalPending - user1Amount; // 300e18 (user2Amount + user3Amount) - - // User2's expected: (200 / 300) * 225 = 150e18 - // But we need to account for the fact that vaultWithdrawnAmount is now (redeemableAmount - amount1) - // Since vault has no new redeemable, available = withdrawnAmount = remainingAfterUser1 - uint256 expectedUser2After = (user2Amount * remainingAfterUser1) / totalPendingAfterUser1; // 150e18 + (uint256 amount1,) = alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); + assertEq(amount1, user1Amount, "User1 should get exact pending amount"); - // Update vault redeemable (still 0, but withdrawn amount is now remainingAfterUser1) - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(0) // No new redeemable - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(remainingAfterUser1) - ); + // User2 completes - gets exact pending via syncRedeem + underlyingToken.transfer(address(alephVault), user2Amount); vm.mockCall( MOCK_STRATEGY_MANAGER, abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(expectedUser2After) + abi.encode(user2Amount) ); - - // User2 completes unallocation vm.prank(user2); - (uint256 amount2, uint256 shares2) = - alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount2, expectedUser2After, "User2 should get proportional share"); - assertEq(shares2, expectedUser2After, "User2 shares should match amount"); + (uint256 amount2,) = alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); + assertEq(amount2, user2Amount, "User2 should get exact pending amount"); - // User3 gets remaining amount (last user) - uint256 remainingAfterUser2 = remainingAfterUser1 - expectedUser2After; // 75e18 - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(0) // No new redeemable - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(remainingAfterUser2) - ); + // User3 completes - gets exact pending via syncRedeem + underlyingToken.transfer(address(alephVault), user3Amount); vm.mockCall( MOCK_STRATEGY_MANAGER, abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(remainingAfterUser2) + abi.encode(user3Amount) ); - - // User3 completes unallocation (gets all remaining) vm.prank(user3); - (uint256 amount3, uint256 shares3) = - alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount3, remainingAfterUser2, "User3 should get all remaining"); - assertEq(shares3, remainingAfterUser2, "User3 shares should match amount"); + (uint256 amount3,) = alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); + assertEq(amount3, user3Amount, "User3 should get exact pending amount"); // Verify all pending amounts are cleared (uint256 user1Pending,,,) = alephAVS.getPendingUnallocateStatus(user1, address(alephVault)); @@ -170,38 +102,30 @@ contract UnallocateEdgeCasesTest is AlephAVSTest { } /** - * @notice Test: calculateCompleteUnallocateAmount returns correct expected amount + * @notice Test: calculateCompleteUnallocateAmount returns user's pending amount + * @dev With sync flow, user gets exactly their pending amount */ - function test_CalculateCompleteUnallocateAmount_ReturnsExpected() public { + function test_CalculateCompleteUnallocateAmount_ReturnsUserPending() public { uint256 tokenAmount = 100e18; - uint256 estAmountToRedeem = 95e18; // Vault price is 0.95 - _mockRequestUnallocate(user1, tokenAmount, estAmountToRedeem); + // Use standard mock which gives 1:1 price + _mockRequestUnallocate(user1, tokenAmount, tokenAmount); vm.prank(user1); - alephAVS.requestUnallocate(address(alephVault), tokenAmount); - - // Set up redeemable amount - uint256 redeemableAmount = estAmountToRedeem; - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(redeemableAmount) - ); + (uint48 batchId, uint256 actualEstAmount) = alephAVS.requestUnallocate(address(alephVault), tokenAmount); - // Calculate expected amount + // Calculate expected amount - should be exactly user's pending uint256 expectedAmount = alephAVS.calculateCompleteUnallocateAmount(user1, address(alephVault)); - assertEq(expectedAmount, estAmountToRedeem, "Expected amount should match estAmountToRedeem for single user"); + assertEq(expectedAmount, actualEstAmount, "Expected amount should match user's pending amount"); } /** - * @notice Test: calculateCompleteUnallocateAmount with multiple users (proportional) + * @notice Test: calculateCompleteUnallocateAmount with multiple users returns each user's pending + * @dev With sync flow, no proportional distribution - each user gets their exact pending */ - function test_CalculateCompleteUnallocateAmount_MultipleUsers_Proportional() public { + function test_CalculateCompleteUnallocateAmount_MultipleUsers() public { uint256 user1Amount = 100e18; uint256 user2Amount = 200e18; - uint256 totalPending = user1Amount + user2Amount; // 300e18 - uint256 redeemableAmount = 150e18; // Less than total pending _mockRequestUnallocate(user1, user1Amount, user1Amount); vm.prank(user1); @@ -211,20 +135,12 @@ contract UnallocateEdgeCasesTest is AlephAVSTest { vm.prank(user2); alephAVS.requestUnallocate(address(alephVault), user2Amount); - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(redeemableAmount) - ); - - // User1 should get: (100 / 300) * 150 = 50 - // Note: Since vault has redeemableAmount > 0, available = redeemableAmount + withdrawnAmount = 150 + 0 = 150 + // Each user gets exactly their pending amount uint256 expectedUser1 = alephAVS.calculateCompleteUnallocateAmount(user1, address(alephVault)); - assertEq(expectedUser1, 50e18, "User1 should get 1/3 of redeemable"); + assertEq(expectedUser1, user1Amount, "User1 should get their exact pending"); - // User2 should get: (200 / 300) * 150 = 100 uint256 expectedUser2 = alephAVS.calculateCompleteUnallocateAmount(user2, address(alephVault)); - assertEq(expectedUser2, 100e18, "User2 should get 2/3 of redeemable"); + assertEq(expectedUser2, user2Amount, "User2 should get their exact pending"); } /** @@ -246,14 +162,16 @@ contract UnallocateEdgeCasesTest is AlephAVSTest { vm.prank(user1); alephAVS.requestUnallocate(address(alephVault), tokenAmount); - // Vault has no redeemable amount yet - vm.mockCall( - address(alephVault), abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), abi.encode(0) + // Vault has no funds for syncRedeem - it will revert + // classId is 1 (set in setUp via initializeVault) + vm.mockCallRevert( + address(alephVault), + abi.encodeCall(IAlephVaultRedeem.syncRedeem, (IAlephVaultRedeem.RedeemRequestParams(1, tokenAmount))), + "Insufficient funds" ); - vm.mockCall(address(underlyingToken), abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), abi.encode(0)); vm.prank(user1); - vm.expectRevert(IAlephAVS.InvalidAmount.selector); + vm.expectRevert("Insufficient funds"); alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); } @@ -266,8 +184,10 @@ contract UnallocateEdgeCasesTest is AlephAVSTest { _mockRequestUnallocate(user1, tokenAmount, estAmountToRedeem); - // Before requestUnallocate - (uint256 pendingBefore, uint256 totalBefore, uint256 redeemableBefore, bool canCompleteBefore) = + // Before requestUnallocate - mock vault balance as 0 + vm.mockCall(address(underlyingToken), abi.encodeCall(IERC20.balanceOf, (address(alephVault))), abi.encode(0)); + + (uint256 pendingBefore, uint256 totalBefore, uint256 vaultBalanceBefore, bool canCompleteBefore) = alephAVS.getPendingUnallocateStatus(user1, address(alephVault)); assertEq(pendingBefore, 0, "No pending before request"); assertEq(canCompleteBefore, false, "Cannot complete before request"); @@ -279,90 +199,18 @@ contract UnallocateEdgeCasesTest is AlephAVSTest { // Use the actual returned value for assertions uint256 actualEstAmountToRedeem = actualEstAmount; - // After requestUnallocate - uint256 redeemableAmount = actualEstAmountToRedeem; + // After requestUnallocate - mock vault has enough balance for syncRedeem + uint256 vaultBalance = actualEstAmountToRedeem; vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(redeemableAmount) + address(underlyingToken), abi.encodeCall(IERC20.balanceOf, (address(alephVault))), abi.encode(vaultBalance) ); - (uint256 pendingAfter, uint256 totalAfter, uint256 redeemableAfter, bool canCompleteAfter) = + (uint256 pendingAfter, uint256 totalAfter, uint256 vaultBalanceAfter, bool canCompleteAfter) = alephAVS.getPendingUnallocateStatus(user1, address(alephVault)); assertEq(pendingAfter, actualEstAmountToRedeem, "Pending should match returned estAmountToRedeem"); assertEq(totalAfter, actualEstAmountToRedeem, "Total pending should match"); - assertEq(redeemableAfter, redeemableAmount, "Redeemable should match vault"); - assertEq(canCompleteAfter, true, "Can complete if redeemable > 0"); - } - - /** - * @notice Test: Rounding in proportional distribution - * @dev Tests that rounding doesn't cause issues when amounts don't divide evenly - */ - function test_CompleteUnallocate_RoundingHandling() public { - uint256 user1Amount = 1e18; // 1 token - uint256 user2Amount = 2e18; // 2 tokens - uint256 totalPending = user1Amount + user2Amount; // 3 tokens - uint256 redeemableAmount = 1e18; // 1 token (doesn't divide evenly) - - _mockRequestUnallocate(user1, user1Amount, user1Amount); - vm.prank(user1); - alephAVS.requestUnallocate(address(alephVault), user1Amount); - - _mockRequestUnallocate(user2, user2Amount, user2Amount); - vm.prank(user2); - alephAVS.requestUnallocate(address(alephVault), user2Amount); - - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(redeemableAmount) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(redeemableAmount) - ); - vm.mockCall(address(underlyingToken), abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true)); - - // User1 should get: (1 / 3) * 1 = 0.333... (rounded down to 0.333e18) - // Since vault has redeemableAmount > 0, available = redeemableAmount + withdrawnAmount = 1e18 + 0 = 1e18 - uint256 expectedUser1 = (user1Amount * redeemableAmount) / totalPending; // 333333333333333333 (0.333...e18) - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(expectedUser1) - ); - - vm.prank(user1); - (uint256 amount1,) = alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount1, expectedUser1, "User1 should get rounded down share"); - - // User2 gets remaining (last user gets all remaining) - uint256 remaining = redeemableAmount - expectedUser1; // 666666666666666667 - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(0) // No new redeemable after User1 - ); - vm.mockCall( - address(underlyingToken), abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), abi.encode(remaining) - ); - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(remaining) - ); - - vm.prank(user2); - (uint256 amount2,) = alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount2, remaining, "User2 should get all remaining"); - assertEq(amount1 + amount2, redeemableAmount, "Total should equal redeemable"); + assertEq(vaultBalanceAfter, vaultBalance, "Vault balance should match"); + assertEq(canCompleteAfter, true, "Can complete if vault has enough balance"); } /** @@ -395,45 +243,6 @@ contract UnallocateEdgeCasesTest is AlephAVSTest { assertEq(pending2, firstAmount + secondAmount, "Pending should be sum of both requests"); } - /** - * @notice Test: completeUnallocate caps to contract balance if needed - */ - function test_CompleteUnallocate_ValidatesAmount() public { - uint256 tokenAmount = 100e18; - uint256 estAmountToRedeem = 100e18; - uint256 redeemableAmount = 100e18; - uint256 expectedAmount = 100e18; - uint256 contractBalance = 100e18; // Matches expected amount - - _mockRequestUnallocate(user1, tokenAmount, estAmountToRedeem); - vm.prank(user1); - alephAVS.requestUnallocate(address(alephVault), tokenAmount); - - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(redeemableAmount) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), abi.encode(contractBalance) - ); - vm.mockCall(address(underlyingToken), abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true)); - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(expectedAmount) - ); - - vm.prank(user1); - (uint256 amount,) = alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount, expectedAmount, "Amount should equal expected amount when validation passes"); - } - /** * @notice Test: calculateCompleteUnallocateAmount returns 0 if no pending */ @@ -442,673 +251,6 @@ contract UnallocateEdgeCasesTest is AlephAVSTest { assertEq(expected, 0, "Should return 0 if no pending unallocation"); } - /** - * @notice Test: calculateCompleteUnallocateAmount returns 0 if no redeemable - */ - function test_CalculateCompleteUnallocateAmount_ReturnsZeroIfNoRedeemable() public { - uint256 tokenAmount = 100e18; - _mockRequestUnallocate(user1, tokenAmount, tokenAmount); - - vm.prank(user1); - alephAVS.requestUnallocate(address(alephVault), tokenAmount); - - vm.mockCall( - address(alephVault), abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), abi.encode(0) - ); - - uint256 expected = alephAVS.calculateCompleteUnallocateAmount(user1, address(alephVault)); - assertEq(expected, 0, "Should return 0 if no redeemable amount"); - } - - /** - * @notice Test: Same user makes multiple requests when vault state changes - * @dev User requests, vault processes some, user requests more, then completes all - */ - function test_MultipleRequests_SameUser_VaultStateChanges() public { - uint256 firstRequest = 100e18; - uint256 firstEstAmount = 95e18; // Vault price is 0.95 - - // First request - _mockRequestUnallocate(user1, firstRequest, firstEstAmount); - vm.prank(user1); - alephAVS.requestUnallocate(address(alephVault), firstRequest); - - // Vault processes first request - now has some redeemable - uint256 firstRedeemable = 50e18; // Only half processed so far - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(firstRedeemable) - ); - - // User makes second request while first is partially processed - uint256 secondRequest = 50e18; - uint256 secondEstAmount = 48e18; // Vault price changed to 0.96 - _mockRequestUnallocate(user1, secondRequest, secondEstAmount); - vm.prank(user1); - alephAVS.requestUnallocate(address(alephVault), secondRequest); - - // Check total pending (should be sum of both estAmounts) - (uint256 totalPending,,,) = alephAVS.getPendingUnallocateStatus(user1, address(alephVault)); - // Note: The actual estAmount returned might differ from our mock, so we check it's >= our expected - assertGe(totalPending, firstEstAmount, "Total pending should include first request"); - - // Vault now processes more - total redeemable increases - uint256 totalRedeemable = firstEstAmount + secondEstAmount; // All processed - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(totalRedeemable) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), abi.encode(totalRedeemable) - ); - vm.mockCall(address(underlyingToken), abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true)); - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(totalRedeemable) - ); - - // User completes all pending unallocation - vm.prank(user1); - (uint256 amount, uint256 shares) = alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount, totalRedeemable, "Should get all redeemable amount"); - assertEq(shares, totalRedeemable, "Shares should match amount"); - - // Verify pending is cleared - (uint256 pendingAfter,,,) = alephAVS.getPendingUnallocateStatus(user1, address(alephVault)); - assertEq(pendingAfter, 0, "Pending should be cleared"); - } - - /** - * @notice Test: Multiple users with vault state changes between requests - * @dev User1 requests, User2 requests, User1 completes, vault processes more, User2 completes - */ - function test_MultipleUsers_VaultStateChanges() public { - uint256 user1Request = 100e18; - uint256 user1EstAmount = 95e18; - uint256 user2Request = 200e18; - uint256 user2EstAmount = 190e18; - - // User1 requests - _mockRequestUnallocate(user1, user1Request, user1EstAmount); - vm.prank(user1); - alephAVS.requestUnallocate(address(alephVault), user1Request); - - // User2 requests - _mockRequestUnallocate(user2, user2Request, user2EstAmount); - vm.prank(user2); - alephAVS.requestUnallocate(address(alephVault), user2Request); - - // Vault has partial redeemable (only User1's request processed) - uint256 partialRedeemable = user1EstAmount; // Only User1's processed - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(partialRedeemable) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(partialRedeemable) - ); - vm.mockCall(address(underlyingToken), abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true)); - - // User1 completes (gets proportional share of available) - // Get actual pending amounts from contract - (uint256 user1PendingActual,,,) = alephAVS.getPendingUnallocateStatus(user1, address(alephVault)); - (uint256 user2PendingActual,,,) = alephAVS.getPendingUnallocateStatus(user2, address(alephVault)); - uint256 totalPendingActual = user1PendingActual + user2PendingActual; - - // User1's expected share: (user1Pending / totalPending) * partialRedeemable - uint256 user1Expected = (user1PendingActual * partialRedeemable) / totalPendingActual; - // But if calculated exceeds available, cap to available (last user logic) - if (user1Expected > partialRedeemable) { - user1Expected = partialRedeemable; - } - - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(user1Expected) - ); - - vm.prank(user1); - (uint256 amount1,) = alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount1, user1Expected, "User1 should get proportional share"); - - // Vault processes more - now has User2's amount available - uint256 remainingRedeemable = user2EstAmount; - - // Set up mocks BEFORE calculating expected amount so the view function uses correct state - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(remainingRedeemable) - ); - - // Calculate expected amount for User2 using the view function - // This will tell us what the function expects, and we'll validate it doesn't exceed available - uint256 user2Expected = alephAVS.calculateCompleteUnallocateAmount(user2, address(alephVault)); - - // The view function calculates based on remainingRedeemable + _withdrawnAmount (from User1) - // withdrawAndCalculateAvailable returns _availableAmount = remainingRedeemable + _withdrawnAmount - // For validation to pass, we need: - // - _amount <= _availableAmount (from withdrawAndCalculateAvailable) - // - _amount <= _contractBalance (after withdrawal) - // Since user2Expected is calculated by the view function which should cap to _availableAmount, - // we need to ensure contract balance is at least user2Expected - // But the actual _availableAmount might be different - let's use a higher value to ensure it passes - uint256 contractBalanceAfterWithdrawal = - user2Expected > remainingRedeemable ? user2Expected : remainingRedeemable; - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - // Contract balance after withdrawal must be >= user2Expected to pass validation - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(contractBalanceAfterWithdrawal) - ); - vm.mockCall(address(underlyingToken), abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true)); - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(user2Expected) - ); - - // User2 completes (gets remaining) - // After User1 completes, User2's pending is still there, but totalPending is reduced - // User2 should get all remaining since they're the last user - vm.prank(user2); - (uint256 amount2,) = alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - // The actual amount might be less than user2Expected if validation caps it, but since we set - // contract balance high enough, it should match user2Expected - assertEq(amount2, user2Expected, "User2 should get expected amount"); - - // Verify both users' pending is cleared - (uint256 user1PendingAfter,,,) = alephAVS.getPendingUnallocateStatus(user1, address(alephVault)); - (uint256 user2PendingAfter,,,) = alephAVS.getPendingUnallocateStatus(user2, address(alephVault)); - assertEq(user1PendingAfter, 0, "User1 pending should be cleared"); - assertEq(user2PendingAfter, 0, "User2 pending should be cleared"); - } - - /** - * @notice Test: User requests, completes partial, then requests more - * @dev Tests that user can complete partial unallocation and then request more - */ - function test_SameUser_RequestCompleteRequest() public { - uint256 firstRequest = 200e18; - uint256 firstEstAmount = 190e18; - - // First request - _mockRequestUnallocate(user1, firstRequest, firstEstAmount); - vm.prank(user1); - alephAVS.requestUnallocate(address(alephVault), firstRequest); - - // Vault has partial redeemable - uint256 partialRedeemable = 100e18; // Only half available - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(partialRedeemable) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(partialRedeemable) - ); - vm.mockCall(address(underlyingToken), abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true)); - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(partialRedeemable) - ); - - // Complete unallocation - this clears ALL pending, not just partial - vm.prank(user1); - (uint256 amount1,) = alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount1, partialRedeemable, "Should get partial amount"); - - // Check pending is cleared (completeUnallocate clears all pending) - (uint256 pendingAfter,,,) = alephAVS.getPendingUnallocateStatus(user1, address(alephVault)); - assertEq(pendingAfter, 0, "Pending should be cleared after completeUnallocate"); - - // User makes second request (no pending from first since it was cleared) - uint256 secondRequest = 50e18; - uint256 secondEstAmount = 48e18; - _mockRequestUnallocate(user1, secondRequest, secondEstAmount); - vm.prank(user1); - (uint48 batchId2, uint256 actualEst2) = alephAVS.requestUnallocate(address(alephVault), secondRequest); - - // Check total pending (should only be the new second request) - (uint256 totalPendingAfter,,,) = alephAVS.getPendingUnallocateStatus(user1, address(alephVault)); - assertEq(totalPendingAfter, actualEst2, "Total pending should be only second request"); - } - - /** - * @notice Test: Multiple users request at different times with different vault states - * @dev User1 requests (vault empty), User2 requests (vault has some), User3 requests (vault has more) - */ - function test_MultipleUsers_DifferentVaultStates() public { - uint256 user1Request = 100e18; - uint256 user1EstAmount = 95e18; - uint256 user2Request = 150e18; - uint256 user2EstAmount = 144e18; - uint256 user3Request = 50e18; - uint256 user3EstAmount = 48e18; - - // User1 requests - vault has no redeemable yet - _mockRequestUnallocate(user1, user1Request, user1EstAmount); - vm.prank(user1); - alephAVS.requestUnallocate(address(alephVault), user1Request); - - // Vault processes User1's request partially - uint256 vaultRedeemable1 = 50e18; - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(vaultRedeemable1) - ); - - // User2 requests - vault now has some redeemable - _mockRequestUnallocate(user2, user2Request, user2EstAmount); - vm.prank(user2); - alephAVS.requestUnallocate(address(alephVault), user2Request); - - // Vault processes more - uint256 vaultRedeemable2 = user1EstAmount + 50e18; // User1 fully processed + some of User2 - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(vaultRedeemable2) - ); - - // User3 requests - vault has even more redeemable - _mockRequestUnallocate(user3, user3Request, user3EstAmount); - vm.prank(user3); - alephAVS.requestUnallocate(address(alephVault), user3Request); - - // Check all users have pending (use actual returned values) - (uint256 user1Pending,,,) = alephAVS.getPendingUnallocateStatus(user1, address(alephVault)); - (uint256 user2Pending,,,) = alephAVS.getPendingUnallocateStatus(user2, address(alephVault)); - (uint256 user3Pending,,,) = alephAVS.getPendingUnallocateStatus(user3, address(alephVault)); - assertGe(user1Pending, user1EstAmount, "User1 should have pending"); - assertGe(user2Pending, user2EstAmount, "User2 should have pending"); - assertGe(user3Pending, user3EstAmount, "User3 should have pending"); - - // Use actual values for calculations - uint256 totalPendingActual = user1Pending + user2Pending + user3Pending; - - // Vault now has all redeemable (use actual pending amounts) - uint256 totalRedeemable = totalPendingActual; // Vault has processed all - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(totalRedeemable) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), abi.encode(totalRedeemable) - ); - vm.mockCall(address(underlyingToken), abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true)); - - // All users complete in order - // User1 completes - uint256 user1Expected = (user1Pending * totalRedeemable) / totalPendingActual; - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(user1Expected) - ); - vm.prank(user1); - (uint256 amount1,) = alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount1, user1Expected, "User1 should get proportional share"); - - // User2 completes - uint256 remainingAfterUser1 = totalRedeemable - amount1; - uint256 totalPendingAfterUser1 = totalPendingActual - user1Pending; - uint256 user2Expected = (user2Pending * remainingAfterUser1) / totalPendingAfterUser1; - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(remainingAfterUser1) - ); - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(0) // No new redeemable - ); - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(user2Expected) - ); - vm.prank(user2); - (uint256 amount2,) = alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount2, user2Expected, "User2 should get proportional share"); - - // User3 gets remaining - uint256 remainingAfterUser2 = remainingAfterUser1 - amount2; - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(remainingAfterUser2) - ); - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(remainingAfterUser2) - ); - vm.prank(user3); - (uint256 amount3,) = alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount3, remainingAfterUser2, "User3 should get remaining"); - - // Verify all completed - assertEq(amount1 + amount2 + amount3, totalRedeemable, "Total should equal redeemable"); - } - - /** - * @notice Test: Multiple users can complete in any order (proportional distribution ensures fairness) - * @dev This test demonstrates that even if User B front-runs User A, both get their fair share - * due to proportional distribution. Front-running doesn't cause unfairness. - */ - function test_CompleteUnallocate_ProportionalDistribution_FairRegardlessOfOrder() public { - uint256 user1Amount = 100e18; - uint256 user2Amount = 100e18; - uint256 totalPending = user1Amount + user2Amount; // 200e18 - uint256 redeemableAmount = 200e18; // Enough for both - - // Setup: Both users have slashed tokens - vm.mockCall(MOCK_SLASHED_TOKEN, abi.encodeCall(IERC20.balanceOf, (user1)), abi.encode(user1Amount)); - vm.mockCall(MOCK_SLASHED_TOKEN, abi.encodeCall(IERC20.balanceOf, (user2)), abi.encode(user2Amount)); - - // Mock calculateAmountToRedeem (1:1 for simplicity) - vm.mockCall(MOCK_SLASHED_TOKEN, abi.encodeWithSelector(IERC20.totalSupply.selector), abi.encode(totalPending)); - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.assetsPerClassOf, (CLASS_ID, address(alephAVS))), - abi.encode(totalPending) - ); - - // User1 requests unallocation FIRST (gets queue position 0) - _mockRequestUnallocate(user1, user1Amount, user1Amount); - vm.prank(user1); - alephAVS.requestUnallocate(address(alephVault), user1Amount); - - // User2 requests unallocation SECOND (gets queue position 1) - _mockRequestUnallocate(user2, user2Amount, user2Amount); - vm.prank(user2); - alephAVS.requestUnallocate(address(alephVault), user2Amount); - - // Vault has redeemable amount available - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(redeemableAmount) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(redeemableAmount) - ); - vm.mockCall(address(underlyingToken), abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true)); - - // User2 can complete first (front-running is allowed, but proportional distribution ensures fairness) - uint256 expectedUser1 = (user1Amount * redeemableAmount) / totalPending; // 100e18 - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(expectedUser1) - ); - - vm.prank(user1); - (uint256 amount1, uint256 shares1) = - alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount1, expectedUser1, "User1 should get their proportional share"); - assertEq(shares1, expectedUser1, "User1 shares should match amount"); - - // Now User1 can complete (User2 already completed) - uint256 remainingAfterUser1 = redeemableAmount - expectedUser1; // 100e18 - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(0) // No new redeemable - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(remainingAfterUser1) - ); - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(remainingAfterUser1) - ); - - vm.prank(user2); - (uint256 amount2, uint256 shares2) = - alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount2, remainingAfterUser1, "User2 should get remaining amount"); - assertEq(shares2, remainingAfterUser1, "User2 shares should match amount"); - - // Verify both users got their fair share - assertEq(amount1 + amount2, redeemableAmount, "Total should equal redeemable"); - } - - /** - * @notice Test: Multiple requests from same user work correctly - * @dev Tests that users can make multiple requests and complete them - */ - function test_CompleteUnallocate_MultipleRequests_SameUser() public { - uint256 user1FirstRequest = 50e18; - uint256 user1SecondRequest = 50e18; - uint256 user2Request = 100e18; - uint256 totalPending = user1FirstRequest + user1SecondRequest + user2Request; // 200e18 - uint256 redeemableAmount = 200e18; - - // Setup - vm.mockCall(MOCK_SLASHED_TOKEN, abi.encodeCall(IERC20.balanceOf, (user1)), abi.encode(100e18)); - vm.mockCall(MOCK_SLASHED_TOKEN, abi.encodeCall(IERC20.balanceOf, (user2)), abi.encode(100e18)); - vm.mockCall(MOCK_SLASHED_TOKEN, abi.encodeWithSelector(IERC20.totalSupply.selector), abi.encode(totalPending)); - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.assetsPerClassOf, (CLASS_ID, address(alephAVS))), - abi.encode(totalPending) - ); - - // User1 makes first request (gets position 0) - _mockRequestUnallocate(user1, user1FirstRequest, user1FirstRequest); - vm.prank(user1); - alephAVS.requestUnallocate(address(alephVault), user1FirstRequest); - - // User2 makes request (gets position 1) - _mockRequestUnallocate(user2, user2Request, user2Request); - vm.prank(user2); - alephAVS.requestUnallocate(address(alephVault), user2Request); - - // User1 makes second request (keeps position 0, pending increases) - _mockRequestUnallocate(user1, user1SecondRequest, user1SecondRequest); - vm.prank(user1); - alephAVS.requestUnallocate(address(alephVault), user1SecondRequest); - - // Setup for completion - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(redeemableAmount) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(redeemableAmount) - ); - vm.mockCall(address(underlyingToken), abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true)); - - // Either user can complete (order doesn't matter due to proportional distribution) - uint256 user1Pending = user1FirstRequest + user1SecondRequest; // 100e18 - uint256 expectedUser1 = (user1Pending * redeemableAmount) / totalPending; // 100e18 - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(expectedUser1) - ); - - vm.prank(user1); - (uint256 amount1,) = alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount1, expectedUser1, "User1 should get their share"); - - // Now User2 can complete - uint256 remaining = redeemableAmount - expectedUser1; // 100e18 - vm.mockCall( - address(alephVault), abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), abi.encode(0) - ); - vm.mockCall( - address(underlyingToken), abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), abi.encode(remaining) - ); - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(remaining) - ); - - vm.prank(user2); - (uint256 amount2,) = alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount2, remaining, "User2 should get remaining"); - } - - /** - * @notice Test: Users can complete when their batch is ready, regardless of order - * @dev User A requests but can't complete (no redeemable), User B requests and can complete. - * User B can complete first, then User A when their batch is ready. - */ - function test_CompleteUnallocate_UsersCompleteWhenReady_NoDeadlock() public { - uint256 user1Amount = 100e18; - uint256 user2Amount = 100e18; - - // Setup: Both users have slashed tokens - vm.mockCall(MOCK_SLASHED_TOKEN, abi.encodeCall(IERC20.balanceOf, (user1)), abi.encode(user1Amount)); - vm.mockCall(MOCK_SLASHED_TOKEN, abi.encodeCall(IERC20.balanceOf, (user2)), abi.encode(user2Amount)); - - // Mock calculateAmountToRedeem (1:1 for simplicity) - vm.mockCall( - MOCK_SLASHED_TOKEN, - abi.encodeWithSelector(IERC20.totalSupply.selector), - abi.encode(user1Amount + user2Amount) - ); - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.assetsPerClassOf, (CLASS_ID, address(alephAVS))), - abi.encode(user1Amount + user2Amount) - ); - - // User1 requests unallocation FIRST (gets queue position 1) - _mockRequestUnallocate(user1, user1Amount, user1Amount); - vm.prank(user1); - alephAVS.requestUnallocate(address(alephVault), user1Amount); - - // User2 requests unallocation SECOND (gets queue position 2) - _mockRequestUnallocate(user2, user2Amount, user2Amount); - vm.prank(user2); - alephAVS.requestUnallocate(address(alephVault), user2Amount); - - // Vault only has User2's amount redeemable (User1's batch not processed yet) - uint256 user2Redeemable = user2Amount; - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(user2Redeemable) - ); - vm.mockCall( - address(alephVault), - abi.encodeWithSelector(IAlephVaultRedeem.withdrawRedeemableAmount.selector), - abi.encode() - ); - vm.mockCall( - address(underlyingToken), abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), abi.encode(user2Redeemable) - ); - vm.mockCall(address(underlyingToken), abi.encodeWithSelector(IERC20.approve.selector), abi.encode(true)); - - // User1 can complete and get their proportional share of available funds - // Even though only User2's batch is ready, User1 gets (100/200) * 100 = 50 - uint256 totalPending = user1Amount + user2Amount; // 200e18 - uint256 expectedUser1 = (user1Amount * user2Redeemable) / totalPending; // 50e18 - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(expectedUser1) - ); - - vm.prank(user1); - (uint256 amount1, uint256 shares1) = - alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount1, expectedUser1, "User1 should get proportional share"); - assertEq(shares1, expectedUser1, "User1 shares should match amount"); - - // User2 can complete and get remaining (their batch is ready, order doesn't matter) - uint256 remainingAfterUser1 = user2Redeemable - expectedUser1; // 50e18 - vm.mockCall( - address(alephVault), - abi.encodeCall(IAlephVault.redeemableAmount, (address(alephAVS))), - abi.encode(0) // No new redeemable - ); - vm.mockCall( - address(underlyingToken), - abi.encodeCall(IERC20.balanceOf, (address(alephAVS))), - abi.encode(remainingAfterUser1) - ); - vm.mockCall( - MOCK_STRATEGY_MANAGER, - abi.encodeWithSelector(IStrategyManager.depositIntoStrategyWithSignature.selector), - abi.encode(remainingAfterUser1) - ); - - vm.prank(user2); - (uint256 amount2, uint256 shares2) = - alephAVS.completeUnallocate(address(alephVault), block.timestamp + 1000, ""); - assertEq(amount2, remainingAfterUser1, "User2 should get remaining amount"); - assertEq(shares2, remainingAfterUser1, "User2 shares should match amount"); - - // Verify both users got their fair proportional share - assertEq(amount1 + amount2, user2Redeemable, "Total should equal available redeemable"); - - // Note: In this scenario, User1 already completed and got their proportional share - // If User1's batch becomes available later, they would have already completed - // This demonstrates that users can complete when funds are available, regardless of order - } - // Helper function to mock requestUnallocate setup function _mockRequestUnallocate(address user, uint256 tokenAmount, uint256 estAmountToRedeem) internal { vm.mockCall(MOCK_SLASHED_TOKEN, abi.encodeCall(IERC20.balanceOf, (user)), abi.encode(tokenAmount));