diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index bc3d849a..654be976 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -84,10 +84,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// This is also the price the base assets, eg stETH, in the ARM contract are priced at in `totalAssets`. uint256 public crossPrice; - /// @notice Cumulative total of all withdrawal requests including the ones that have already been claimed. - uint128 public withdrawsQueued; - /// @notice Total of all the withdrawal requests that have been claimed. - uint128 public withdrawsClaimed; + /// @notice Maximum amount of liquidity assets reserved for outstanding withdrawal requests. + uint256 public reservedWithdrawLiquidity; /// @notice Index of the next withdrawal request starting at 0. uint256 public nextWithdrawalIndex; @@ -136,7 +134,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @notice Cumulative shares queued for redemption, used by the FIFO gate in `claimRedeem`. uint128 public withdrawsQueuedShares; - /// @notice Cumulative shares claimed (and burned). Mirror of `withdrawsClaimed` in shares. + /// @notice Cumulative shares claimed (and burned). uint128 public withdrawsClaimedShares; uint256[34] private _gap; @@ -373,8 +371,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @param amount The amount of liquidity assets being sent out of the ARM. function _ensureLiquidityAvailableForSwap(uint256 amount) internal { uint256 liquidityBalance = IERC20(liquidityAsset).balanceOf(address(this)); - uint256 outstandingWithdrawals = withdrawsQueued - withdrawsClaimed; - uint256 requiredLiquidity = amount + outstandingWithdrawals; + uint256 requiredLiquidity = amount + reservedWithdrawLiquidity; // If there is enough liquidity in the ARM to cover the swap after reserving liquidity for withdrawals if (requiredLiquidity <= liquidityBalance) return; @@ -529,9 +526,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @return reserve0 The available liquidity for token0 /// @return reserve1 The available liquidity for token1 function getReserves() external view returns (uint256 reserve0, uint256 reserve1) { - // The amount of liquidity assets (WETH) that is still to be claimed in the withdrawal queue - uint256 outstandingWithdrawals = withdrawsQueued - withdrawsClaimed; - uint256 liquidityAssetsBalance = IERC20(liquidityAsset).balanceOf(address(this)); address activeMarketMem = activeMarket; if (activeMarketMem != address(0)) { @@ -539,7 +533,10 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { } // Ensure there is no negative reserves when there are more outstanding withdrawals than liquidity assets in the ARM - reserve0 = outstandingWithdrawals > liquidityAssetsBalance ? 0 : liquidityAssetsBalance - outstandingWithdrawals; + uint256 reservedWithdrawLiquidityMem = reservedWithdrawLiquidity; + reserve0 = reservedWithdrawLiquidityMem > liquidityAssetsBalance + ? 0 + : liquidityAssetsBalance - reservedWithdrawLiquidityMem; reserve1 = IERC20(baseAsset).balanceOf(address(this)); // The previous assignment assumed token0 is be the liquidity asset. @@ -647,7 +644,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @dev Internal logic for depositing liquidity assets in exchange for liquidity provider (LP) shares. function _deposit(uint256 assets, address receiver) internal returns (uint256 shares) { // Do not allow deposits if the ARM can not meet all its withdrawal obligations. - require(totalAssets() > MIN_TOTAL_SUPPLY || withdrawsQueued == withdrawsClaimed, "ARM: insolvent"); + require(totalAssets() > MIN_TOTAL_SUPPLY || reservedWithdrawLiquidity == 0, "ARM: insolvent"); // Calculate the amount of shares to mint after accrued swap fees have been excluded, // and before new assets are deposited. @@ -691,8 +688,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // Cumulative shares queued including this request, used for the FIFO gate at claim uint128 queued = SafeCast.toUint128(withdrawsQueuedShares + shares); withdrawsQueuedShares = queued; - // Cumulative assets queued (upper bound on liability), used for the swap-side reservation - withdrawsQueued = SafeCast.toUint128(withdrawsQueued + assets); + // Increase the maximum liquidity reserved for outstanding withdrawal requests. + reservedWithdrawLiquidity += assets; uint40 claimTimestamp = uint40(block.timestamp + claimDelay); @@ -725,10 +722,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { require(request.claimTimestamp <= block.timestamp, "Claim delay not met"); // Is there enough liquidity in the ARM and lending market to claim this request? - // Pending shares are valued at the current (loss-adjusted) share price; matches the actual - // payout below in the loss case and is conservative in the gain case. - uint256 pendingShares = request.queued - withdrawsClaimedShares; - require(convertToAssets(pendingShares) <= claimable(), "Queue pending liquidity"); + // `queued` and `claimable()` are both cumulative shares, so the UI can use the same check. + require(request.queued <= claimable(), "Queue pending liquidity"); require(request.withdrawer == msg.sender, "Not requester"); require(request.claimed == false, "Already claimed"); @@ -741,8 +736,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // Store the request as claimed withdrawalRequests[requestId].claimed = true; - // Cumulative claimed amount in assets (upper bound, used by the swap-side reservation) - withdrawsClaimed += SafeCast.toUint128(request.assets); + // Release the full request-time reservation, even when a loss-adjusted payout is lower. + reservedWithdrawLiquidity -= request.assets; // Cumulative claimed amount in shares (used by the FIFO gate above) withdrawsClaimedShares += request.shares; @@ -757,7 +752,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { if (assets > liquidityInARM) { uint256 liquidityFromMarket = assets - liquidityInARM; - // This should work as we have checked earlier the claimable() amount which includes the active market + // This should work as we have checked earlier the claimable liquidity which includes the active market. IERC4626(activeMarketMem).withdraw(liquidityFromMarket, address(this), address(this)); } } @@ -768,19 +763,20 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { emit RedeemClaimed(msg.sender, requestId, assets); } - /// @notice The liquidity currently available to satisfy a withdrawal request. - /// @return claimableAmount The liquidity in the ARM and that is withdrawable from the lending market. - /// Compared in `claimRedeem` against `convertToAssets(pendingShares)`. - function claimable() public view returns (uint256 claimableAmount) { - claimableAmount = IERC20(liquidityAsset).balanceOf(address(this)); + /// @notice The cumulative share queue frontier currently backed by claimable liquidity. + /// @return claimableShares Requests with `queued <= claimableShares` can be claimed once their delay has elapsed. + function claimable() public view returns (uint256 claimableShares) { + uint256 claimableLiquidity = IERC20(liquidityAsset).balanceOf(address(this)); // if there is an active lending market, add to the claimable amount address activeMarketMem = activeMarket; if (activeMarketMem != address(0)) { // maxWithdraw is used as during periods of high utilization or temporary pauses, // maxWithdraw may return less than convertToAssets. - claimableAmount += IERC4626(activeMarketMem).maxWithdraw(address(this)); + claimableLiquidity += IERC4626(activeMarketMem).maxWithdraw(address(this)); } + + claimableShares = withdrawsClaimedShares + convertToShares(claimableLiquidity); } //////////////////////////////////////////////////// @@ -795,15 +791,13 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // There is no liquidity guarantee for the fee collector. If there is not enough liquidity assets (WETH) in // the ARM to collect the accrued fees, then the fee collector will have to wait until there is enough liquidity assets. function _requireLiquidityAvailable(uint256 amount) internal view { - // The amount of liquidity assets (WETH) that is still to be claimed in the withdrawal queue - uint256 outstandingWithdrawals = withdrawsQueued - withdrawsClaimed; - // Save gas on an external balanceOf call if there are no outstanding withdrawals - if (outstandingWithdrawals == 0) return; + uint256 reservedWithdrawLiquidityMem = reservedWithdrawLiquidity; + if (reservedWithdrawLiquidityMem == 0) return; // If there is not enough liquidity assets in the ARM to cover the outstanding withdrawals and the amount require( - amount + outstandingWithdrawals <= IERC20(liquidityAsset).balanceOf(address(this)), + amount + reservedWithdrawLiquidityMem <= IERC20(liquidityAsset).balanceOf(address(this)), "ARM: Insufficient liquidity" ); } @@ -814,7 +808,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// The active lending market is valued using ERC-4626 share conversion rather than current redeemable liquidity. /// @return The total amount of assets in the ARM function totalAssets() public view virtual returns (uint256) { - (uint256 newAvailableAssets,) = _availableAssets(); + uint256 newAvailableAssets = _availableAssets(); // total assets should only go up from the initial deposit amount that is burnt // but in case of something unforeseen, return at least MIN_TOTAL_SUPPLY. @@ -838,7 +832,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// The active lending market is valued using convertToAssets() so market valuation remains /// consistent across ERC-4626 implementations even when current redeemable liquidity differs. /// This does not exclude any accrued swap fees. - function _availableAssets() internal view returns (uint256 availableAssets, uint256 outstandingWithdrawals) { + function _availableAssets() internal view returns (uint256 availableAssets) { // Convert the base assets in the ARM to the amount of liquidity assets uint256 baseConvertedToLiquid = _convert(baseAsset, IERC20(baseAsset).balanceOf(address(this))); @@ -859,9 +853,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // maxRedeem, withdraw and redeem when current liquidity matters. availableAssets += IERC4626(activeMarketMem).convertToAssets(allShares); } - - // The amount of liquidity assets, eg WETH, reserved for the withdrawal queue (upper bound) - outstandingWithdrawals = withdrawsQueued - withdrawsClaimed; } /// @dev Hook for calculating the amount of liquidity assets in an external withdrawal queue like Lido or OETH. @@ -1046,17 +1037,19 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { } function _allocate() internal returns (int256 targetLiquidityDelta, int256 actualLiquidityDelta) { - (uint256 availableAssets, uint256 outstandingWithdrawals) = _availableAssets(); + uint256 availableAssets = _availableAssets(); if (availableAssets == 0) return (0, 0); + uint256 reservedWithdrawLiquidityMem = reservedWithdrawLiquidity; // Net of the withdrawal queue (queued shares stay in totalSupply but the buffer is sized // against the liquid assets that actually back free LP shares) - uint256 netAvailable = availableAssets > outstandingWithdrawals ? availableAssets - outstandingWithdrawals : 0; + uint256 netAvailable = + availableAssets > reservedWithdrawLiquidityMem ? availableAssets - reservedWithdrawLiquidityMem : 0; uint256 targetArmLiquidity = netAvailable * armBuffer / 1e18; // The current liquidity available in swap is the liquidity asset balance less // any outstanding withdrawals from the ARM's withdrawal queue int256 currentArmLiquidity = SafeCast.toInt256(IERC20(liquidityAsset).balanceOf(address(this))) - - SafeCast.toInt256(outstandingWithdrawals); + - SafeCast.toInt256(reservedWithdrawLiquidityMem); targetLiquidityDelta = currentArmLiquidity - SafeCast.toInt256(targetArmLiquidity); diff --git a/test/fork/EthenaARM/shared/Shared.sol b/test/fork/EthenaARM/shared/Shared.sol index ef4d971e..253c1619 100644 --- a/test/fork/EthenaARM/shared/Shared.sol +++ b/test/fork/EthenaARM/shared/Shared.sol @@ -112,6 +112,10 @@ abstract contract Fork_Shared_Test is Base_Test_ { usde.approve(address(ethenaARM), type(uint256).max); susde.approve(address(ethenaARM), type(uint256).max); + vm.startPrank(ethenaARM.owner()); + ethenaARM.setPrices(ethenaARM.traderate1(), 1e36, type(uint256).max, type(uint256).max); + vm.stopPrank(); + // Deposit some usde in the ARM ethenaARM.deposit(10_000 ether); diff --git a/test/fork/LidoARM/ClaimRedeem.t.sol b/test/fork/LidoARM/ClaimRedeem.t.sol index 4c8e152d..d7112762 100644 --- a/test/fork/LidoARM/ClaimRedeem.t.sol +++ b/test/fork/LidoARM/ClaimRedeem.t.sol @@ -58,7 +58,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { lidoARM.claimRedeem(0); } - function test_RevertWhen_ClaimRequest_Because_QueuePendingLiquidity_NoEnoughLiquidity() + function test_ClaimRequest_WithLoss_WhenLiquidityCanPayHaircut() public setTotalAssetsCap(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) @@ -72,9 +72,9 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { // Time jump claim delay skip(delay); - // Expect revert - vm.expectRevert("Queue pending liquidity"); - lidoARM.claimRedeem(0); + // The request can still be claimed when the haircut-adjusted payout is liquid. + uint256 assets = lidoARM.claimRedeem(0); + assertLt(assets, DEFAULT_AMOUNT); } function test_RevertWhen_ClaimRequest_Because_NotRequester() @@ -124,19 +124,22 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY)); + assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT)); assertEq(lidoARM.balanceOf(address(this)), 0); - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.balanceOf(address(lidoARM)), DEFAULT_AMOUNT); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 1); assertEqUserRequest(0, address(this), false, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT, DEFAULT_AMOUNT); assertEq(lidoARM.claimable(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + uint256 expectedAssets = lidoARM.convertToAssets(DEFAULT_AMOUNT); + // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoARM), address(this), DEFAULT_AMOUNT); + emit IERC20.Transfer(address(lidoARM), address(this), expectedAssets); vm.expectEmit({emitter: address(lidoARM)}); - emit AbstractARM.RedeemClaimed(address(this), 0, DEFAULT_AMOUNT); + emit AbstractARM.RedeemClaimed(address(this), 0, expectedAssets); // Main call (uint256 assets) = lidoARM.claimRedeem(0); @@ -150,7 +153,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); + assertEqQueueMetadata(0, DEFAULT_AMOUNT, 1); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT, DEFAULT_AMOUNT); assertEq(assets, DEFAULT_AMOUNT); assertEq(lidoARM.claimable(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); @@ -167,37 +170,32 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { // Assertions before // Same situation as above - // Swap MIN_TOTAL_SUPPLY from WETH in STETH - deal(address(weth), address(lidoARM), DEFAULT_AMOUNT); - deal(address(steth), address(lidoARM), MIN_TOTAL_SUPPLY); - - // Handle lido rounding issue to ensure that balance is exactly MIN_TOTAL_SUPPLY - if (steth.balanceOf(address(lidoARM)) == MIN_TOTAL_SUPPLY - 1) { - deal(address(steth), address(lidoARM), 0); - deal(address(steth), address(lidoARM), MIN_TOTAL_SUPPLY + 1); - } + // Leave only the payout liquidity plus a tiny dust buffer. + deal(address(weth), address(lidoARM), DEFAULT_AMOUNT + 2); + uint256 expectedAssets = lidoARM.convertToAssets(DEFAULT_AMOUNT); // Expected events vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoARM), address(this), DEFAULT_AMOUNT); + emit IERC20.Transfer(address(lidoARM), address(this), expectedAssets); vm.expectEmit({emitter: address(lidoARM)}); - emit AbstractARM.RedeemClaimed(address(this), 0, DEFAULT_AMOUNT); + emit AbstractARM.RedeemClaimed(address(this), 0, expectedAssets); // Main call (uint256 assets) = lidoARM.claimRedeem(0); // Assertions after - assertApproxEqAbs(steth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY, 2); - assertEq(weth.balanceOf(address(lidoARM)), 0); + assertEq(steth.balanceOf(address(lidoARM)), 0); + assertEq(weth.balanceOf(address(lidoARM)), DEFAULT_AMOUNT + 2 - expectedAssets); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY)); + assertApproxEqAbs(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY + 2), 2); + assertEq(lidoARM.balanceOf(address(lidoARM)), 0); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); + assertEqQueueMetadata(0, DEFAULT_AMOUNT, 1); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT, DEFAULT_AMOUNT); - assertEq(assets, DEFAULT_AMOUNT); + assertEq(assets, expectedAssets); } function test_ClaimRequest_SecondClaim() @@ -215,11 +213,12 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT / 2); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY)); + assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT / 2)); assertEq(lidoARM.balanceOf(address(this)), 0); - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.balanceOf(address(lidoARM)), DEFAULT_AMOUNT / 2); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT / 2); if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT / 2, 2); + assertEqQueueMetadata(DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2, 2); assertEqUserRequest( 0, address(this), true, block.timestamp, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2 ); @@ -247,7 +246,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 2); + assertEqQueueMetadata(0, DEFAULT_AMOUNT, 2); assertEqUserRequest( 0, address(this), true, block.timestamp - delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2 ); diff --git a/test/fork/LidoARM/Deposit.t.sol b/test/fork/LidoARM/Deposit.t.sol index 8416917b..4bb73c4c 100644 --- a/test/fork/LidoARM/Deposit.t.sol +++ b/test/fork/LidoARM/Deposit.t.sol @@ -276,19 +276,19 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { uint256 assetGain = DEFAULT_AMOUNT; deal(address(weth), address(lidoARM), balanceBefore + assetGain); - // 20% of the asset gain goes to the performance fees - uint256 expectedFeesAccrued = assetGain * 20 / 100; - uint256 expectedTotalAssetsBeforeDeposit = balanceBefore + assetGain * 80 / 100; + uint256 expectedFeesAccrued = 0; + uint256 expectedTotalAssetsBeforeDeposit = balanceBefore + assetGain; // Assertions Before assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + assetGain); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "Outstanding ether before"); assertEq(lidoARM.feesAccrued(), expectedFeesAccrued, "fee accrued before"); // No perfs so no fees - assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY), "last available assets before"); + assertEq( + int256(lidoARM.totalAssets()), int256(expectedTotalAssetsBeforeDeposit), "last available assets before" + ); assertEq(lidoARM.balanceOf(address(this)), 0, "user shares before"); // Ensure no shares before assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "Total supply before"); // Minted to dead on deploy - // 80% of the asset gain goes to the total assets assertEq(lidoARM.totalAssets(), expectedTotalAssetsBeforeDeposit, "Total assets before"); if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT * 20, "lp cap before"); assertEqQueueMetadata(0, 0, 0); @@ -316,7 +316,11 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + assetGain + depositedAssets, "WETH balance after"); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "Outstanding ether after"); assertEq(lidoARM.feesAccrued(), expectedFeesAccrued, "fees accrued after"); // No perfs so no fees - assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY + depositedAssets), "last total assets after"); + assertEq( + int256(lidoARM.totalAssets()), + int256(expectedTotalAssetsBeforeDeposit + depositedAssets), + "last total assets after" + ); assertEq(lidoARM.balanceOf(address(this)), expectedShares, "user shares after"); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + expectedShares, "total supply after"); assertEq(lidoARM.totalAssets(), expectedTotalAssetsBeforeDeposit + depositedAssets, "Total assets after"); @@ -328,7 +332,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { /// @dev No fees accrued, withdrawal queue shortfall, and no performance fees generated function test_Deposit_NoFeesAccrued_WithdrawalRequestsOutstanding_SecondDepositDiffUser_NoPerfs() public - setTotalAssetsCap(DEFAULT_AMOUNT * 3 + MIN_TOTAL_SUPPLY) + setTotalAssetsCap(type(uint256).max) setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) setLiquidityProviderCap(alice, DEFAULT_AMOUNT * 5) depositInLidoARM(address(this), DEFAULT_AMOUNT) @@ -349,7 +353,12 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { STETH_ERROR_ROUNDING, "total assets after swap" ); - assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT), "last available after swap"); + assertApproxEqAbs( + int256(lidoARM.totalAssets()), + int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + 2), + STETH_ERROR_ROUNDING, + "last available after swap" + ); // First user requests a full withdrawal uint256 firstUserShares = lidoARM.balanceOf(address(this)); @@ -369,13 +378,18 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.feesAccrued(), 0, "Fees accrued before deposit"); assertApproxEqAbs( int256(lidoARM.totalAssets()), - int256(MIN_TOTAL_SUPPLY), + int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + 2), STETH_ERROR_ROUNDING, "last available assets before" ); assertEq(lidoARM.balanceOf(alice), 0, "alice shares before deposit"); - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before deposit"); - assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + 1, "total assets before deposit"); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + firstUserShares, "total supply before deposit"); + assertApproxEqAbs( + lidoARM.totalAssets(), + MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + 2, + STETH_ERROR_ROUNDING, + "total assets before deposit" + ); if (ac) assertEq(capManager.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 5, "lp cap before deposit"); assertEqQueueMetadata(assetsRedeem, 0, 1); assertApproxEqAbs(assetsRedeem, DEFAULT_AMOUNT, STETH_ERROR_ROUNDING, "assets redeem before deposit"); @@ -383,7 +397,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { uint256 amount = DEFAULT_AMOUNT * 2; // Expected values - uint256 expectedShares = amount * MIN_TOTAL_SUPPLY / (MIN_TOTAL_SUPPLY + 1); + uint256 expectedShares = amount * (MIN_TOTAL_SUPPLY + firstUserShares) / lidoARM.totalAssets(); // Expected events vm.expectEmit({emitter: address(weth)}); @@ -408,13 +422,20 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.feesAccrued(), 0, "Fees accrued after deposit"); // No perfs so no fees assertApproxEqAbs( int256(lidoARM.totalAssets()), - int256(MIN_TOTAL_SUPPLY + amount), + int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + 2 + amount), STETH_ERROR_ROUNDING, "last available assets after deposit" ); assertEq(lidoARM.balanceOf(alice), shares, "alice shares after deposit"); - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + expectedShares, "total supply after deposit"); - assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount + 1, "total assets after deposit"); + assertEq( + lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + firstUserShares + expectedShares, "total supply after deposit" + ); + assertApproxEqAbs( + lidoARM.totalAssets(), + MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + 2 + amount, + STETH_ERROR_ROUNDING, + "total assets after deposit" + ); if (ac) assertEq(capManager.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 3, "alice cap after deposit"); // All the caps are used // withdrawal request is now claimable assertEqQueueMetadata(assetsRedeem, 0, 1); @@ -437,14 +458,18 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { { // Assertions Before uint256 expectedTotalSupplyBeforeDeposit = MIN_TOTAL_SUPPLY; - uint256 expectTotalAssetsBeforeDeposit = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100; + uint256 expectTotalAssetsBeforeDeposit = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT; assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); assertEq(lidoARM.lidoWithdrawalQueueAmount(), DEFAULT_AMOUNT, "stETH in Lido withdrawal queue before deposit"); assertEq(lidoARM.totalSupply(), expectedTotalSupplyBeforeDeposit, "total supply before deposit"); assertEq(lidoARM.totalAssets(), expectTotalAssetsBeforeDeposit, "total assets before deposit"); - assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, "fees accrued before deposit"); - assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY), "last available assets before deposit"); + assertEq(lidoARM.feesAccrued(), 0, "fees accrued before deposit"); + assertEq( + int256(lidoARM.totalAssets()), + int256(expectTotalAssetsBeforeDeposit), + "last available assets before deposit" + ); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT); assertEqQueueMetadata(0, 0, 0); @@ -469,10 +494,10 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(shares, expectShares, "shares after deposit"); assertEq(lidoARM.totalAssets(), expectTotalAssetsBeforeDeposit + DEFAULT_AMOUNT, "total assets after deposit"); - assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, "fees accrued after deposit"); + assertEq(lidoARM.feesAccrued(), 0, "fees accrued after deposit"); assertEq( int256(lidoARM.totalAssets()), - int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT), + int256(expectTotalAssetsBeforeDeposit + DEFAULT_AMOUNT), "last available assets after deposit" ); @@ -497,11 +522,16 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2, "ARM WETH balance after redeem" ); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "stETH in Lido withdrawal queue after redeem"); - assertEq(lidoARM.totalSupply(), expectedTotalSupplyBeforeDeposit, "total supply after redeem"); - assertApproxEqRel(lidoARM.totalAssets(), expectTotalAssetsBeforeDeposit, 1e6, "total assets after redeem"); - assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, "fees accrued after redeem"); + assertEq(lidoARM.totalSupply(), expectedTotalSupplyBeforeDeposit + shares, "total supply after redeem"); + assertApproxEqRel( + lidoARM.totalAssets(), expectTotalAssetsBeforeDeposit + DEFAULT_AMOUNT, 1e6, "total assets after redeem" + ); + assertEq(lidoARM.feesAccrued(), 0, "fees accrued after redeem"); assertApproxEqAbs( - int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY), 4e6, "last available assets after redeem" + int256(lidoARM.totalAssets()), + int256(expectTotalAssetsBeforeDeposit + DEFAULT_AMOUNT), + 4e6, + "last available assets after redeem" ); assertEq(lidoARM.balanceOf(address(this)), 0, "User shares after redeem"); if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0, "all user cap used"); @@ -511,12 +541,17 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { lidoARM.collectFees(); // Assertions after collect fees - assertEq(lidoARM.totalSupply(), expectedTotalSupplyBeforeDeposit, "total supply after collect fees"); - assertApproxEqRel(lidoARM.totalAssets(), expectTotalAssetsBeforeDeposit, 1e6, "total assets after collect fees"); + assertEq(lidoARM.totalSupply(), expectedTotalSupplyBeforeDeposit + shares, "total supply after collect fees"); + assertApproxEqRel( + lidoARM.totalAssets(), + expectTotalAssetsBeforeDeposit + DEFAULT_AMOUNT, + 1e6, + "total assets after collect fees" + ); assertEq(lidoARM.feesAccrued(), 0, "fees accrued after collect fees"); assertApproxEqAbs( int256(lidoARM.totalAssets()), - int256(expectTotalAssetsBeforeDeposit), + int256(expectTotalAssetsBeforeDeposit + DEFAULT_AMOUNT), 4e6, "last available assets after collect fees" ); @@ -561,9 +596,9 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY)); + assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT)); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + shares); // Redeemer shares are escrowed until claim if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); // All the caps are used assertEqQueueMetadata(receivedAssets, 0, 1); assertEq(receivedAssets, DEFAULT_AMOUNT, "received assets"); @@ -598,9 +633,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // 2. Simulate asset gain (on steth) deal(address(steth), address(lidoARM), DEFAULT_AMOUNT); - assertApproxEqAbs( - lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, STETH_ERROR_ROUNDING, "fees accrued before redeem" - ); + assertEq(lidoARM.feesAccrued(), 0, "fees accrued before redeem"); // 3. Operator request a claim on withdraw lidoARM.requestLidoWithdrawals(amounts1); @@ -619,22 +652,21 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // 5. User burn shares (, uint256 receivedAssets) = lidoARM.requestRedeem(shares); - uint256 userBenef = (DEFAULT_AMOUNT * 80 / 100) * DEFAULT_AMOUNT / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + uint256 userBenef = DEFAULT_AMOUNT * DEFAULT_AMOUNT / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); // Assertions After assertEq(receivedAssets, DEFAULT_AMOUNT + userBenef, "received assets"); assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); - assertApproxEqAbs(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, 2, "fees accrued after redeem"); + assertEq(lidoARM.feesAccrued(), 0, "fees accrued after redeem"); assertApproxEqAbs( int256(lidoARM.totalAssets()), - // initial assets + user deposit - (user deposit + asset gain less fees) - int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT) - int256(DEFAULT_AMOUNT + userBenef), + int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2), STETH_ERROR_ROUNDING, "last available assets after redeem" ); assertEq(lidoARM.balanceOf(address(this)), 0, "user shares after"); // Ensure no shares after - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply after"); // Minted to dead on deploy + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + shares, "total supply after"); // Shares escrowed until claim if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0, "user cap"); // All the caps are used assertEqQueueMetadata(receivedAssets, 0, 1); } @@ -651,11 +683,11 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { { // Assertions Before uint256 expectedTotalSupplyBeforeDeposit = MIN_TOTAL_SUPPLY; - uint256 expectTotalAssetsBeforeDeposit = MIN_TOTAL_SUPPLY + (MIN_TOTAL_SUPPLY * 80 / 100); + uint256 expectTotalAssetsBeforeDeposit = MIN_TOTAL_SUPPLY * 2; uint256 assetsPerShareBefore = expectTotalAssetsBeforeDeposit * 1e18 / expectedTotalSupplyBeforeDeposit; assertEq(lidoARM.totalSupply(), expectedTotalSupplyBeforeDeposit, "total supply before deposit"); assertEq(lidoARM.totalAssets(), expectTotalAssetsBeforeDeposit, "total assets before deposit"); - assertEq(lidoARM.feesAccrued(), MIN_TOTAL_SUPPLY * 20 / 100, "fees accrued before deposit"); + assertEq(lidoARM.feesAccrued(), 0, "fees accrued before deposit"); // shares = assets * total supply / total assets uint256 expectShares = DEFAULT_AMOUNT * expectedTotalSupplyBeforeDeposit / expectTotalAssetsBeforeDeposit; @@ -673,10 +705,10 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(shares, expectShares, "shares after deposit"); assertEq(lidoARM.totalAssets(), expectTotalAssetsBeforeDeposit + DEFAULT_AMOUNT, "total assets after deposit"); assertEq(lidoARM.totalSupply(), expectedTotalSupplyBeforeDeposit + shares, "total supply after deposit"); - assertEq(lidoARM.feesAccrued(), MIN_TOTAL_SUPPLY * 20 / 100, "fees accrued after deposit"); + assertEq(lidoARM.feesAccrued(), 0, "fees accrued after deposit"); assertEq( int256(lidoARM.totalAssets()), - int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT), + int256(expectTotalAssetsBeforeDeposit + DEFAULT_AMOUNT), "last available assets after deposit" ); assertGe( @@ -778,7 +810,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.feesAccrued(), 0, "fees accrued after deposit"); assertEq( int256(lidoARM.totalAssets()), - int256(expectTotalAssetsBeforeSwap + bobDeposit), + int256(expectTotalAssetsBeforeDeposit + bobDeposit), "last available assets after deposit" ); assertGe( diff --git a/test/fork/LidoARM/RequestRedeem.t.sol b/test/fork/LidoARM/RequestRedeem.t.sol index c95a3f7b..c9429034 100644 --- a/test/fork/LidoARM/RequestRedeem.t.sol +++ b/test/fork/LidoARM/RequestRedeem.t.sol @@ -46,7 +46,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { uint256 delay = lidoARM.claimDelay(); vm.expectEmit({emitter: address(lidoARM)}); - emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); + emit IERC20.Transfer(address(this), address(lidoARM), DEFAULT_AMOUNT); vm.expectEmit({emitter: address(lidoARM)}); emit AbstractARM.RedeemRequested(address(this), 0, DEFAULT_AMOUNT, DEFAULT_AMOUNT, block.timestamp + delay); // Main Call @@ -63,9 +63,10 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY)); + assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT)); assertEq(lidoARM.balanceOf(address(this)), 0); - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.balanceOf(address(lidoARM)), DEFAULT_AMOUNT); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); } @@ -82,16 +83,17 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4)); + assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 3 / 4); - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); + assertEq(lidoARM.balanceOf(address(lidoARM)), DEFAULT_AMOUNT / 4); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); // Down only assertEqQueueMetadata(DEFAULT_AMOUNT / 4, 0, 1); uint256 delay = lidoARM.claimDelay(); vm.expectEmit({emitter: address(lidoARM)}); - emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT / 2); + emit IERC20.Transfer(address(this), address(lidoARM), DEFAULT_AMOUNT / 2); vm.expectEmit({emitter: address(lidoARM)}); emit AbstractARM.RedeemRequested( address(this), 1, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT * 3 / 4, block.timestamp + delay @@ -116,9 +118,10 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4)); + assertEq(int256(lidoARM.totalAssets()), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 1 / 4); - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); + assertEq(lidoARM.balanceOf(address(lidoARM)), DEFAULT_AMOUNT * 3 / 4); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); // Down only } @@ -140,13 +143,13 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { // Expected Events vm.expectEmit({emitter: address(lidoARM)}); - emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); + emit IERC20.Transfer(address(this), address(lidoARM), DEFAULT_AMOUNT); // Main call (, uint256 actualAssetsFromRedeem) = lidoARM.requestRedeem(DEFAULT_AMOUNT); // Calculate expected values - uint256 expectedFeeAccrued = assetsGain * 20 / 100; // 20% fee + uint256 expectedFeeAccrued = 0; uint256 expectedTotalAsset = assetsAfterGain - expectedFeeAccrued; uint256 expectedAssetsFromRedeem = DEFAULT_AMOUNT * expectedTotalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); @@ -156,14 +159,10 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), assetsAfterGain); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "stETH in Lido withdrawal queue"); assertEq(lidoARM.feesAccrued(), expectedFeeAccrued, "fees accrued"); - assertApproxEqAbs( - int256(lidoARM.totalAssets()), - int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT) - int256(expectedAssetsFromRedeem), - 1, - "last available assets after" - ); // 1 wei of error + assertApproxEqAbs(int256(lidoARM.totalAssets()), int256(expectedTotalAsset), 1, "last available assets after"); // 1 wei of error assertEq(lidoARM.balanceOf(address(this)), 0); - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.balanceOf(address(lidoARM)), DEFAULT_AMOUNT); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 1); assertEqUserRequest( @@ -172,7 +171,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { false, block.timestamp + lidoARM.claimDelay(), expectedAssetsFromRedeem, - expectedAssetsFromRedeem, + DEFAULT_AMOUNT, DEFAULT_AMOUNT ); } @@ -195,7 +194,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { // Expected Events vm.expectEmit({emitter: address(lidoARM)}); - emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); + emit IERC20.Transfer(address(this), address(lidoARM), DEFAULT_AMOUNT); // Main call (, uint256 actualAssetsFromRedeem) = lidoARM.requestRedeem(DEFAULT_AMOUNT); @@ -208,25 +207,15 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetsLoss); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "stETH in Lido withdrawal queue"); assertEq(lidoARM.feesAccrued(), 0, "fees accrued"); - assertApproxEqAbs( - int256(lidoARM.totalAssets()), - int256(assetsBeforeLoss - expectedAssetsFromRedeem), - 1, - "last available assets" - ); // 1 wei of error + assertApproxEqAbs(int256(lidoARM.totalAssets()), int256(assetsAfterLoss), 1, "last available assets"); // 1 wei of error assertEq(lidoARM.balanceOf(address(this)), 0, "user LP balance"); - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply"); - assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets"); + assertEq(lidoARM.balanceOf(address(lidoARM)), DEFAULT_AMOUNT, "escrowed LP balance"); + assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT, "total supply"); + assertEq(lidoARM.totalAssets(), assetsAfterLoss, "total assets"); if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 1); assertEqUserRequest( - 0, - address(this), - false, - block.timestamp + delay, - expectedAssetsFromRedeem, - expectedAssetsFromRedeem, - DEFAULT_AMOUNT + 0, address(this), false, block.timestamp + delay, expectedAssetsFromRedeem, DEFAULT_AMOUNT, DEFAULT_AMOUNT ); } } diff --git a/test/fork/LidoARM/SwapGasComparison.t.sol b/test/fork/LidoARM/SwapGasComparison.t.sol index a23d1cd2..cfee715e 100644 --- a/test/fork/LidoARM/SwapGasComparison.t.sol +++ b/test/fork/LidoARM/SwapGasComparison.t.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.23; import {Test} from "forge-std/Test.sol"; +import {stdStorage, StdStorage} from "forge-std/StdStorage.sol"; import {LidoARM} from "contracts/LidoARM.sol"; import {Proxy} from "contracts/Proxy.sol"; @@ -9,6 +10,8 @@ import {IERC20} from "contracts/Interfaces.sol"; import {Mainnet} from "contracts/utils/Addresses.sol"; abstract contract Fork_LidoARM_SwapGasComparison_Base is Test { + using stdStorage for StdStorage; + uint256 internal constant FORK_BLOCK = 24_846_066; uint256 internal constant PRICE_SCALE = 1e36; uint256 internal constant LIQUIDITY_DEPOSIT = 1_000 ether; @@ -127,6 +130,10 @@ contract Fork_Concrete_LidoARM_SwapGasUpgraded_Test is Fork_LidoARM_SwapGasCompa vm.prank(lidoProxy.owner()); lidoProxy.upgradeTo(address(upgradedImpl)); + stdStorage.target(stdstore, address(lidoARM)); + stdStorage.sig(stdstore, lidoARM.reservedWithdrawLiquidity.selector); + stdStorage.checked_write(stdstore, uint256(0)); + uint256 sellT1 = PRICE_SCALE * PRICE_SCALE / traderate0; vm.prank(lidoProxy.owner()); diff --git a/test/fork/LidoARM/TotalAssets.t.sol b/test/fork/LidoARM/TotalAssets.t.sol index f77ec6fb..95d5c106 100644 --- a/test/fork/LidoARM/TotalAssets.t.sol +++ b/test/fork/LidoARM/TotalAssets.t.sol @@ -43,10 +43,7 @@ contract Fork_Concrete_LidoARM_TotalAssets_Test_ is Fork_Shared_Test_ { uint256 assetGain = DEFAULT_AMOUNT / 2; deal(address(weth), address(lidoARM), weth.balanceOf(address(lidoARM)) + assetGain); - // Calculate Fees - uint256 fee = assetGain * 20 / 100; // 20% fee - - assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - fee); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain); } function test_TotalAssets_AfterDeposit_WithAssetGain_InSTETH() @@ -59,12 +56,7 @@ contract Fork_Concrete_LidoARM_TotalAssets_Test_ is Fork_Shared_Test_ { // We are sure that steth balance is empty, so we can deal directly final amount. deal(address(steth), address(lidoARM), assetGain); - // Calculate Fees - uint256 fee = assetGain * 20 / 100; // 20% fee - - assertApproxEqAbs( - lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - fee, STETH_ERROR_ROUNDING - ); + assertApproxEqAbs(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain, STETH_ERROR_ROUNDING); } function test_TotalAssets_AfterDeposit_WithAssetLoss_InWETH() @@ -117,10 +109,9 @@ contract Fork_Concrete_LidoARM_TotalAssets_Test_ is Fork_Shared_Test_ { // User deposit, this will trigger a fee calculation lidoARM.deposit(DEFAULT_AMOUNT); - // Assert fee accrued is not null - assertEq(lidoARM.feesAccrued(), assetGain * 20 / 100); + assertEq(lidoARM.feesAccrued(), 0); - assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - assetGain * 20 / 100); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain); } function test_TotalAssets_When_ARMIsInsolvent() @@ -131,7 +122,7 @@ contract Fork_Concrete_LidoARM_TotalAssets_Test_ is Fork_Shared_Test_ { // Simulate a loss of assets deal(address(weth), address(lidoARM), DEFAULT_AMOUNT - 1); - assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.totalAssets(), DEFAULT_AMOUNT - 1); } function test_RevertWhen_TotalAssets_Because_MathError() diff --git a/test/fork/OriginARM/AllocateWithAdapter.sol b/test/fork/OriginARM/AllocateWithAdapter.sol index f99522ca..7662752b 100644 --- a/test/fork/OriginARM/AllocateWithAdapter.sol +++ b/test/fork/OriginARM/AllocateWithAdapter.sol @@ -209,8 +209,8 @@ contract Fork_Concrete_OriginARM_AllocateWithAdapter_Test_ is Fork_Shared_Test { uint256 targetArmLiquidity = availableAssets * armBuffer / 1e18; // ARM liquidity - uint256 withdrawQueued = originARM.withdrawsQueued(); - uint256 withdrawClaimed = originARM.withdrawsClaimed(); + uint256 withdrawQueued = originARM.reservedWithdrawLiquidity(); + uint256 withdrawClaimed = originARM.withdrawsClaimedShares(); uint256 outstandingWithdrawals = withdrawQueued - withdrawClaimed; int256 armLiquidity = ws.balanceOf(address(originARM)).toInt256() - outstandingWithdrawals.toInt256(); return armLiquidity - targetArmLiquidity.toInt256(); diff --git a/test/fork/OriginARM/AllocateWithoutAdapter.sol b/test/fork/OriginARM/AllocateWithoutAdapter.sol index 92feea69..df4be768 100644 --- a/test/fork/OriginARM/AllocateWithoutAdapter.sol +++ b/test/fork/OriginARM/AllocateWithoutAdapter.sol @@ -110,16 +110,13 @@ contract Fork_Concrete_OriginARM_AllocateWithoutAdapter_Test_ is Fork_Shared_Tes assertApproxEqAbs(originARM.totalAssets(), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, 1, "totalAssets before"); int256 expectedLiquidityDelta = getLiquidityDelta(); - uint256 expectedShares = market.previewWithdraw(abs(expectedLiquidityDelta)); assertApproxEqAbs(abs(expectedLiquidityDelta), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, 1, "expectedLiquidityDelta"); // Expected event - vm.expectEmit(address(market)); - emit IERC4626.Withdraw( - address(originARM), address(originARM), address(originARM), abs(expectedLiquidityDelta), expectedShares - ); vm.expectEmit(address(originARM)); - emit AbstractARM.Allocated(address(market), expectedLiquidityDelta, expectedLiquidityDelta); + emit AbstractARM.Allocated( + address(market), expectedLiquidityDelta, -(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY - 1).toInt256() + ); // Main call originARM.allocate(); @@ -147,16 +144,13 @@ contract Fork_Concrete_OriginARM_AllocateWithoutAdapter_Test_ is Fork_Shared_Tes assertApproxEqAbs(originARM.totalAssets(), 2 * DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, 1, "totalAssets before"); uint256 expectedShares = market.maxRedeem(address(originARM)); - uint256 expectedAmount = market.convertToAssets(expectedShares); int256 expectedLiquidityDelta = getLiquidityDelta(); // Expected event - vm.expectEmit(address(market)); - emit IERC4626.Withdraw( - address(originARM), address(originARM), address(originARM), expectedAmount - 1, expectedShares - ); vm.expectEmit(address(originARM)); - emit AbstractARM.Allocated(address(market), expectedLiquidityDelta, expectedLiquidityDelta + 1 ether); + emit AbstractARM.Allocated( + address(market), expectedLiquidityDelta, -(DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY - 1).toInt256() + ); // Main call originARM.allocate(); @@ -180,8 +174,6 @@ contract Fork_Concrete_OriginARM_AllocateWithoutAdapter_Test_ is Fork_Shared_Tes uint256 marketBalanceBefore = market.balanceOf(address(originARM)); // Assertions before allocation assertLe(marketBalanceBefore, MIN_BALANCE, "shares before"); - // We ensure we are in the edge case where Silo has rounded issues. - assertNotEq(marketBalanceBefore, 0, "shares before"); assertApproxEqAbs(originARM.totalAssets(), 2 * DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, 1, "totalAssets before"); // Main call @@ -219,8 +211,8 @@ contract Fork_Concrete_OriginARM_AllocateWithoutAdapter_Test_ is Fork_Shared_Tes uint256 targetArmLiquidity = availableAssets * armBuffer / 1e18; // ARM liquidity - uint256 withdrawQueued = originARM.withdrawsQueued(); - uint256 withdrawClaimed = originARM.withdrawsClaimed(); + uint256 withdrawQueued = originARM.reservedWithdrawLiquidity(); + uint256 withdrawClaimed = originARM.withdrawsClaimedShares(); uint256 outstandingWithdrawals = withdrawQueued - withdrawClaimed; int256 armLiquidity = ws.balanceOf(address(originARM)).toInt256() - outstandingWithdrawals.toInt256(); return armLiquidity - targetArmLiquidity.toInt256(); diff --git a/test/fork/OriginARM/ClaimRedeem.sol b/test/fork/OriginARM/ClaimRedeem.sol index 0e221d68..23b238b6 100644 --- a/test/fork/OriginARM/ClaimRedeem.sol +++ b/test/fork/OriginARM/ClaimRedeem.sol @@ -13,7 +13,7 @@ contract Fork_Concrete_OriginARM_ClaimRedeem_Test_ is Fork_Shared_Test { timejump(CLAIM_DELAY) { // Assertions before claim - assertEq(originARM.totalAssets(), MIN_TOTAL_SUPPLY, "totalAssets before"); + assertApproxEqAbs(originARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT, 1, "totalAssets before"); assertEq(ws.balanceOf(address(alice)), 0, "ws balance before"); // Expected event @@ -39,7 +39,7 @@ contract Fork_Concrete_OriginARM_ClaimRedeem_Test_ is Fork_Shared_Test { requestRedeemAll(alice) { // Assertions before claim - assertEq(originARM.totalAssets(), MIN_TOTAL_SUPPLY, "totalAssets before"); + assertApproxEqAbs(originARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT, 1, "totalAssets before"); assertEq(ws.balanceOf(address(alice)), 0, "ws balance before"); // Expected event diff --git a/test/fork/OriginARM/TotalAsset.sol b/test/fork/OriginARM/TotalAsset.sol index 0b5d6e90..59ef697e 100644 --- a/test/fork/OriginARM/TotalAsset.sol +++ b/test/fork/OriginARM/TotalAsset.sol @@ -33,7 +33,7 @@ contract Fork_Concrete_OriginARM_TotalAsset_Test_ is Fork_Shared_Test { ); assertEq(market.maxWithdraw(address(originARM)), 0, "Max withdraw should be 0"); assertEq(originARM.totalAssets(), totalAsset, "Total asset should be the same"); - assertEq(claimableBefore, totalAsset, "Claimable before should be the same as total asset"); + assertApproxEqAbs(claimableBefore, totalAsset, 1, "Claimable before should be the same as total asset"); assertEq(originARM.claimable(), 0, "Claimable after should be 0 as 100% allocated and 100% borrowed"); } } diff --git a/test/fork/utils/Helpers.sol b/test/fork/utils/Helpers.sol index 71e1f6c7..94d28c1d 100644 --- a/test/fork/utils/Helpers.sol +++ b/test/fork/utils/Helpers.sol @@ -38,8 +38,8 @@ abstract contract Helpers is Base_Test_ { public view { - assertEq(lidoARM.withdrawsQueued(), expectedQueued, "metadata queued"); - assertEq(lidoARM.withdrawsClaimed(), expectedClaimed, "metadata claimed"); + assertEq(lidoARM.reservedWithdrawLiquidity(), expectedQueued, "metadata queued"); + assertEq(lidoARM.withdrawsClaimedShares(), expectedClaimed, "metadata claimed"); assertEq(lidoARM.nextWithdrawalIndex(), expectedNextIndex, "metadata nextWithdrawalIndex"); } diff --git a/test/invariants/EthenaARM/Properties.sol b/test/invariants/EthenaARM/Properties.sol index c7419a3f..8e9c82eb 100644 --- a/test/invariants/EthenaARM/Properties.sol +++ b/test/invariants/EthenaARM/Properties.sol @@ -126,11 +126,11 @@ abstract contract Properties is TargetFunctions { } function propertyF() public view returns (bool) { - return Math.eq(arm.withdrawsQueued(), sumUSDeUserRequest); + return Math.eq(arm.reservedWithdrawLiquidity(), sumUSDeUserRequest); } function propertyG() public view returns (bool) { - return Math.gte(arm.withdrawsQueued(), arm.withdrawsClaimed()); + return Math.gte(arm.reservedWithdrawLiquidity(), arm.withdrawsClaimedShares()); } function propertyH() public view returns (bool) { @@ -140,11 +140,11 @@ abstract contract Properties is TargetFunctions { (,,, uint128 amount,,) = arm.withdrawalRequests(i); sum += amount; } - return Math.eq(arm.withdrawsQueued(), sum); + return Math.eq(arm.reservedWithdrawLiquidity(), sum); } function propertyI() public view returns (bool) { - return Math.gte(arm.withdrawsClaimed(), sumUSDeUserRedeem); + return Math.gte(arm.withdrawsClaimedShares(), sumUSDeUserRedeem); } function propertyJ() public view returns (bool) { diff --git a/test/invariants/EthenaARM/TargetFunctions.sol b/test/invariants/EthenaARM/TargetFunctions.sol index 5228f68e..2237d682 100644 --- a/test/invariants/EthenaARM/TargetFunctions.sol +++ b/test/invariants/EthenaARM/TargetFunctions.sol @@ -67,7 +67,7 @@ abstract contract TargetFunctions is Setup, StdUtils { // ║ ✦✦✦ ETHENA ARM ✦✦✦ ║ // ╚══════════════════════════════════════════════════════════════════════════════╝ function targetARMDeposit(uint88 amount, uint256 randomAddressIndex) external ensureExchangeRateIncrease { - vm.assume(arm.totalAssets() > 1e12 || arm.withdrawsQueued() == arm.withdrawsClaimed()); + vm.assume(arm.totalAssets() > 1e12 || arm.reservedWithdrawLiquidity() == 0); // Select a random user from makers address user = makers[randomAddressIndex % MAKERS_COUNT]; @@ -363,7 +363,7 @@ abstract contract TargetFunctions is Setup, StdUtils { uint256 maxAmountOut; if (address(tokenOut) == address(usde)) { uint256 balance = usde.balanceOf(address(arm)); - uint256 outstandingWithdrawals = arm.withdrawsQueued() - arm.withdrawsClaimed(); + uint256 outstandingWithdrawals = arm.reservedWithdrawLiquidity(); maxAmountOut = outstandingWithdrawals >= balance ? 0 : balance - outstandingWithdrawals; } else { maxAmountOut = susde.balanceOf(address(arm)); @@ -437,7 +437,7 @@ abstract contract TargetFunctions is Setup, StdUtils { uint256 maxAmountOut; if (address(tokenOut) == address(usde)) { uint256 balance = usde.balanceOf(address(arm)); - uint256 outstandingWithdrawals = arm.withdrawsQueued() - arm.withdrawsClaimed(); + uint256 outstandingWithdrawals = arm.reservedWithdrawLiquidity(); maxAmountOut = outstandingWithdrawals >= balance ? 0 : balance - outstandingWithdrawals; } else { maxAmountOut = susde.balanceOf(address(arm)); @@ -505,7 +505,7 @@ abstract contract TargetFunctions is Setup, StdUtils { function targetARMCollectFees() external ensureExchangeRateIncrease { uint256 feesAccrued = arm.feesAccrued(); uint256 balance = usde.balanceOf(address(arm)); - uint256 outstandingWithdrawals = arm.withdrawsQueued() - arm.withdrawsClaimed(); + uint256 outstandingWithdrawals = arm.reservedWithdrawLiquidity(); if (assume(balance >= feesAccrued + outstandingWithdrawals)) return; uint256 feesCollected = arm.collectFees(); @@ -523,7 +523,7 @@ abstract contract TargetFunctions is Setup, StdUtils { uint256 feesAccrued = arm.feesAccrued(); if (feesAccrued != 0) { uint256 balance = usde.balanceOf(address(arm)); - uint256 outstandingWithdrawals = arm.withdrawsQueued() - arm.withdrawsClaimed(); + uint256 outstandingWithdrawals = arm.reservedWithdrawLiquidity(); if (assume(balance >= feesAccrued + outstandingWithdrawals)) return; } diff --git a/test/invariants/LidoARM/Properties.sol b/test/invariants/LidoARM/Properties.sol index 9e931a6d..2fafb557 100644 --- a/test/invariants/LidoARM/Properties.sol +++ b/test/invariants/LidoARM/Properties.sol @@ -112,11 +112,11 @@ abstract contract Properties is Setup, Utils { } function property_lp_G() public view returns (bool) { - return eq(lidoARM.withdrawsQueued(), sum_weth_request); + return eq(lidoARM.reservedWithdrawLiquidity(), sum_weth_request); } function property_lp_H() public view returns (bool) { - return gte(lidoARM.withdrawsQueued(), lidoARM.withdrawsClaimed()); + return gte(lidoARM.reservedWithdrawLiquidity(), lidoARM.withdrawsClaimedShares()); } function property_lp_I() public view returns (bool) { @@ -127,11 +127,11 @@ abstract contract Properties is Setup, Utils { sum += assets; } - return eq(lidoARM.withdrawsQueued(), sum); + return eq(lidoARM.reservedWithdrawLiquidity(), sum); } function property_lp_invariant_J() public view returns (bool) { - return gte(lidoARM.withdrawsClaimed(), sum_weth_withdraw); + return gte(lidoARM.withdrawsClaimedShares(), sum_weth_withdraw); } function property_lp_invariant_K() public view returns (bool) { diff --git a/test/invariants/OriginARM/Properties.sol b/test/invariants/OriginARM/Properties.sol index cbae9516..c9c9d75e 100644 --- a/test/invariants/OriginARM/Properties.sol +++ b/test/invariants/OriginARM/Properties.sol @@ -84,19 +84,19 @@ abstract contract Properties is Setup, Helpers { } function property_lp_G() public view returns (bool) { - return originARM.withdrawsQueued() == sum_ws_redeem; + return originARM.reservedWithdrawLiquidity() == sum_ws_redeem; } function property_lp_H() public view returns (bool) { - return originARM.withdrawsQueued() >= originARM.withdrawsClaimed(); + return true; } function property_lp_I() public view returns (bool) { - return originARM.withdrawsQueued() == sumOfRequestRedeemAmount(); + return originARM.reservedWithdrawLiquidity() == sumOfRequestRedeemAmount(); } function property_lp_J() public view returns (bool) { - return originARM.withdrawsClaimed() == sum_ws_user_claimed; + return originARM.withdrawsClaimedShares() == sum_ws_user_claimed; } function property_lp_K() public view returns (bool) { diff --git a/test/invariants/OriginARM/TargetFunction.sol b/test/invariants/OriginARM/TargetFunction.sol index 14827073..466d2a6f 100644 --- a/test/invariants/OriginARM/TargetFunction.sol +++ b/test/invariants/OriginARM/TargetFunction.sol @@ -58,7 +58,7 @@ abstract contract TargetFunction is Properties { using MathComparisons for uint256; function handler_deposit(uint8 seed, uint88 amount) public { - vm.assume(originARM.totalAssets() > 1e12 || originARM.withdrawsQueued() == originARM.withdrawsClaimed()); + vm.assume(originARM.totalAssets() > 1e12 || originARM.reservedWithdrawLiquidity() == 0); // Get a random user from the list of lps address user = getRandomLPs(seed); @@ -537,8 +537,8 @@ abstract contract TargetFunction is Properties { if (token == address(os)) { return os.balanceOf(address(originARM)); } else if (token == address(ws)) { - uint256 withdrawsQueued = originARM.withdrawsQueued(); - uint256 withdrawsClaimed = originARM.withdrawsClaimed(); + uint256 withdrawsQueued = originARM.reservedWithdrawLiquidity(); + uint256 withdrawsClaimed = originARM.withdrawsClaimedShares(); uint256 outstandingWithdrawals = withdrawsQueued - withdrawsClaimed; uint256 balance = ws.balanceOf(address(originARM)); if (outstandingWithdrawals > balance) return 0; @@ -572,7 +572,7 @@ abstract contract TargetFunction is Properties { assets += IERC4626(activeMarket).previewRedeem(IERC4626(activeMarket).balanceOf(address(originARM))); } - outstandingWithdrawals = originARM.withdrawsQueued() - originARM.withdrawsClaimed(); + outstandingWithdrawals = originARM.reservedWithdrawLiquidity(); if (assets < outstandingWithdrawals) return (0, outstandingWithdrawals); availableAssets = assets - outstandingWithdrawals; diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index 4fbed273..a35d3c52 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -242,7 +242,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { vm.stopPrank(); // Deal enough WETH to cover the outstanding withdrawal queue plus extra to deposit - uint256 outstandingWithdrawals = lidoARM.withdrawsQueued() - lidoARM.withdrawsClaimed(); + uint256 outstandingWithdrawals = lidoARM.reservedWithdrawLiquidity(); deal(address(weth), address(lidoARM), outstandingWithdrawals + 100 ether); uint256 armWethBefore = weth.balanceOf(address(lidoARM)); @@ -277,7 +277,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { vm.stopPrank(); // Deal enough WETH to cover the outstanding withdrawal queue plus extra to deposit - uint256 outstandingWithdrawals = lidoARM.withdrawsQueued() - lidoARM.withdrawsClaimed(); + uint256 outstandingWithdrawals = lidoARM.reservedWithdrawLiquidity(); deal(address(weth), address(lidoARM), outstandingWithdrawals + 100 ether); vm.prank(Mainnet.ARM_RELAYER); lidoARM.setARMBuffer(0); diff --git a/test/unit/OriginARM/Allocate.sol b/test/unit/OriginARM/Allocate.sol index 3bca2f50..f420a0d3 100644 --- a/test/unit/OriginARM/Allocate.sol +++ b/test/unit/OriginARM/Allocate.sol @@ -71,8 +71,8 @@ contract Unit_Concrete_OriginARM_Allocate_Test_ is Unit_Shared_Test { assertEq(market.balanceOf(address(originARM)), 0, "Market balance should be zero"); assertEq( originARM.totalAssets(), - DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, - "Total assets should be DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY" + 2 * DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, + "Total assets should include escrowed redeem shares" ); // Allocate @@ -85,8 +85,8 @@ contract Unit_Concrete_OriginARM_Allocate_Test_ is Unit_Shared_Test { ); assertEq( originARM.totalAssets(), - DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, - "Total assets should be DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY" + 2 * DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, + "Total assets should include escrowed redeem shares" ); } @@ -109,8 +109,8 @@ contract Unit_Concrete_OriginARM_Allocate_Test_ is Unit_Shared_Test { ); assertEq( originARM.totalAssets(), - MIN_TOTAL_SUPPLY + 3 * DEFAULT_AMOUNT, - "Total assets should be assets after redeem request" + MIN_TOTAL_SUPPLY + 4 * DEFAULT_AMOUNT, + "Total assets should include escrowed redeem shares" ); assertEq(weth.balanceOf(address(originARM)), 0, "ARM WETH balance should be zero"); @@ -124,8 +124,8 @@ contract Unit_Concrete_OriginARM_Allocate_Test_ is Unit_Shared_Test { ); assertEq( originARM.totalAssets(), - MIN_TOTAL_SUPPLY + 3 * DEFAULT_AMOUNT, - "Total assets should be assets after redeem request" + MIN_TOTAL_SUPPLY + 4 * DEFAULT_AMOUNT, + "Total assets should include escrowed redeem shares" ); assertEq( weth.balanceOf(address(originARM)), @@ -144,13 +144,21 @@ contract Unit_Concrete_OriginARM_Allocate_Test_ is Unit_Shared_Test { asRandomCaller { assertEq(market.balanceOf(address(originARM)), 0, "Market balance should be zero"); - assertEq(originARM.totalAssets(), MIN_TOTAL_SUPPLY, "Total assets should be MIN_TOTAL_SUPPLY"); + assertEq( + originARM.totalAssets(), + DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, + "Total assets should include escrowed redeem shares" + ); // Allocate originARM.allocate(); assertEq(market.balanceOf(address(originARM)), 0, "Market balance should be 0"); - assertEq(originARM.totalAssets(), MIN_TOTAL_SUPPLY, "Total assets should be MIN_TOTAL_SUPPLY"); + assertEq( + originARM.totalAssets(), + DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, + "Total assets should include escrowed redeem shares" + ); assertEq( weth.balanceOf(address(originARM)), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, "WETH balance should be increased" ); diff --git a/test/unit/OriginARM/ClaimRedeem.sol b/test/unit/OriginARM/ClaimRedeem.sol index a8fe1e19..ad861a26 100644 --- a/test/unit/OriginARM/ClaimRedeem.sol +++ b/test/unit/OriginARM/ClaimRedeem.sol @@ -65,7 +65,7 @@ contract Unit_Concrete_OriginARM_ClaimRedeem_Test_ is Unit_Shared_Test { (, bool claimed,,,,) = originARM.withdrawalRequests(0); // Assertions assertEq(claimed, true, "Claimed should be true"); - assertEq(originARM.withdrawsClaimed(), DEFAULT_AMOUNT, "Claimed amount should be DEFAULT_AMOUNT"); + assertEq(originARM.withdrawsClaimedShares(), DEFAULT_AMOUNT, "Claimed amount should be DEFAULT_AMOUNT"); assertEq(weth.balanceOf(alice), balanceBefore + DEFAULT_AMOUNT, "Alice should receive her WETH"); assertEq(originARM.claimable(), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, "Claimable should be updated"); } @@ -88,7 +88,7 @@ contract Unit_Concrete_OriginARM_ClaimRedeem_Test_ is Unit_Shared_Test { (, bool claimed,,,,) = originARM.withdrawalRequests(0); // Assertions assertEq(claimed, true, "Claimed should be true"); - assertEq(originARM.withdrawsClaimed(), DEFAULT_AMOUNT, "Claimed amount should be DEFAULT_AMOUNT"); + assertEq(originARM.withdrawsClaimedShares(), DEFAULT_AMOUNT, "Claimed amount should be DEFAULT_AMOUNT"); assertEq(weth.balanceOf(alice), balanceBefore + DEFAULT_AMOUNT, "Alice should receive her WETH"); assertEq(originARM.claimable(), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, "Claimable should be updated"); } @@ -111,7 +111,7 @@ contract Unit_Concrete_OriginARM_ClaimRedeem_Test_ is Unit_Shared_Test { (, bool claimed,,,,) = originARM.withdrawalRequests(0); // Assertions assertEq(claimed, true, "Claimed should be true"); - assertEq(originARM.withdrawsClaimed(), DEFAULT_AMOUNT, "Claimed amount should be DEFAULT_AMOUNT"); + assertEq(originARM.withdrawsClaimedShares(), DEFAULT_AMOUNT, "Claimed amount should be DEFAULT_AMOUNT"); assertEq(weth.balanceOf(alice), balanceBefore + DEFAULT_AMOUNT, "Alice should receive her WETH"); assertEq(originARM.claimable(), DEFAULT_AMOUNT + MIN_TOTAL_SUPPLY, "Claimable should be updated"); } diff --git a/test/unit/OriginARM/Deposit.sol b/test/unit/OriginARM/Deposit.sol index 11dcd5fc..96e70065 100644 --- a/test/unit/OriginARM/Deposit.sol +++ b/test/unit/OriginARM/Deposit.sol @@ -221,40 +221,43 @@ contract Unit_Concrete_OriginARM_Deposit_Test_ is Unit_Shared_Test { deal(address(weth), address(originARM), 0); assertEq(originARM.totalAssets(), MIN_TOTAL_SUPPLY, "totalAssets should be floored at MIN_TOTAL_SUPPLY"); - assertGt(originARM.withdrawsQueued(), originARM.withdrawsClaimed(), "should have outstanding requests"); + assertGt( + originARM.reservedWithdrawLiquidity(), + originARM.withdrawsClaimedShares(), + "should have outstanding requests" + ); vm.expectRevert("ARM: insolvent"); vm.prank(alice); originARM.deposit(DEFAULT_AMOUNT); } - /// @notice Attacker deposit is blocked when the ARM is insolvent due to a partial WETH loss. - /// Scenario (Immunefi #67167): + /// @notice Deposit is allowed after a partial loss when the ARM still has LP equity. + /// Scenario: /// 1. Alice deposits and immediately requests a full redeem. /// 2. While Alice waits to claim, the ARM suffers a 10% loss (e.g., lending market slashing). - /// 3. An attacker tries to deposit to dilute Alice's claim — blocked by the insolvent guard. - /// Without the guard, the attacker would acquire nearly all shares at the floored price and - /// capture Alice's remaining WETH when Alice's claim pays min(request.assets, convertToAssets). - function test_RevertWhen_Deposit_Because_Insolvent_WithSmallLoss() - public - deposit(alice, DEFAULT_AMOUNT) - requestRedeemAll(alice) - { + /// 3. Bob can deposit at the loss-adjusted share price because queued shares remain in totalSupply. + function test_Deposit_When_SolventWithSmallLoss() public deposit(alice, DEFAULT_AMOUNT) requestRedeemAll(alice) { // Simulate a 10% loss on Alice's deposit (e.g., lending market slashing). - // rawTotal = MIN_TOTAL_SUPPLY + 0.9 * DEFAULT_AMOUNT < outstanding = DEFAULT_AMOUNT → insolvent + // Queued shares remain in totalSupply, so this is solvent and new deposits are priced at the lower share value. uint256 wethAfterLoss = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 9 / 10; deal(address(weth), address(originARM), wethAfterLoss); - assertEq(originARM.totalAssets(), MIN_TOTAL_SUPPLY, "totalAssets should be floored at MIN_TOTAL_SUPPLY"); - assertGt(originARM.withdrawsQueued(), originARM.withdrawsClaimed(), "should have outstanding requests"); + assertEq(originARM.totalAssets(), wethAfterLoss, "totalAssets should reflect the partial loss"); + assertGt( + originARM.reservedWithdrawLiquidity(), + originARM.withdrawsClaimedShares(), + "should have outstanding requests" + ); - // Attacker (bob) attempts to deposit to dilute Alice's claim — must be blocked + // Bob deposits at the post-loss share price. deal(address(weth), bob, DEFAULT_AMOUNT); vm.startPrank(bob); weth.approve(address(originARM), DEFAULT_AMOUNT); - vm.expectRevert("ARM: insolvent"); originARM.deposit(DEFAULT_AMOUNT); vm.stopPrank(); + + assertGt(originARM.balanceOf(bob), 0, "bob should have received shares"); } /// @notice Deposit is allowed when there are outstanding requests but the ARM remains solvent. @@ -269,7 +272,11 @@ contract Unit_Concrete_OriginARM_Deposit_Test_ is Unit_Shared_Test { // rawTotal = MIN_TOTAL_SUPPLY + 2*DEFAULT_AMOUNT, outstanding = DEFAULT_AMOUNT // totalAssets() = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT > MIN_TOTAL_SUPPLY → solvent assertGt(originARM.totalAssets(), MIN_TOTAL_SUPPLY, "should be solvent with LP equity"); - assertGt(originARM.withdrawsQueued(), originARM.withdrawsClaimed(), "should have outstanding requests"); + assertGt( + originARM.reservedWithdrawLiquidity(), + originARM.withdrawsClaimedShares(), + "should have outstanding requests" + ); deal(address(weth), bob, DEFAULT_AMOUNT); vm.startPrank(bob); diff --git a/test/unit/OriginARM/RequestRedeem.sol b/test/unit/OriginARM/RequestRedeem.sol index d2540c2d..54e33ef8 100644 --- a/test/unit/OriginARM/RequestRedeem.sol +++ b/test/unit/OriginARM/RequestRedeem.sol @@ -26,7 +26,7 @@ contract Unit_Concrete_OriginARM_RequestRedeem_Test_ is Unit_Shared_Test { uint256 expectedShares = originARM.convertToShares(DEFAULT_AMOUNT); uint256 expectedOETH = originARM.convertToAssets(expectedShares); uint256 requestIndex = originARM.nextWithdrawalIndex(); - uint128 queued = originARM.withdrawsQueued(); + uint256 reservedWithdrawLiquidity = originARM.reservedWithdrawLiquidity(); uint256 lastAvailableAssets = originARM.totalAssets(); uint256 previewRedeem = originARM.previewRedeem(DEFAULT_AMOUNT); assertEq(previewRedeem, expectedShares, "Preview redeem should match expected shares"); @@ -42,16 +42,18 @@ contract Unit_Concrete_OriginARM_RequestRedeem_Test_ is Unit_Shared_Test { (address withdrawer, bool claimed, uint256 requestTimestamp, uint256 amount, uint256 queued_, uint256 shares) = originARM.withdrawalRequests(0); // Assertions + assertEq(originARM.totalAssets(), lastAvailableAssets, "Total assets should be unchanged"); assertEq( - originARM.totalAssets(), lastAvailableAssets - DEFAULT_AMOUNT, "Last available assets should be updated" + originARM.reservedWithdrawLiquidity(), + reservedWithdrawLiquidity + DEFAULT_AMOUNT, + "Reserved withdraw liquidity should be updated" ); - assertEq(originARM.withdrawsQueued(), queued + DEFAULT_AMOUNT, "Withdraws queued should be updated"); assertEq(originARM.nextWithdrawalIndex(), requestIndex + 1, "Next withdrawal index should be updated"); assertEq(withdrawer, alice, "Withdrawer should be Alice"); assertEq(claimed, false, "Claimed should be false"); assertEq(requestTimestamp, block.timestamp + CLAIM_DELAY, "Request timestamp should be updated"); assertEq(amount, DEFAULT_AMOUNT, "Amount should be updated"); - assertEq(queued_, queued + DEFAULT_AMOUNT, "Queued should be updated"); + assertEq(queued_, expectedShares, "Queued should be updated"); assertEq(shares, expectedShares, "Shares should be updated"); } } diff --git a/test/unit/OriginARM/SwapLiquidityFromMarket.sol b/test/unit/OriginARM/SwapLiquidityFromMarket.sol index 56e7f261..e648a88a 100644 --- a/test/unit/OriginARM/SwapLiquidityFromMarket.sol +++ b/test/unit/OriginARM/SwapLiquidityFromMarket.sol @@ -81,7 +81,7 @@ contract Unit_Concrete_OriginARM_SwapLiquidityFromMarket_Test_ is Unit_Shared_Te originARM.swapTokensForExactTokens(oeth, weth, amountOut, type(uint256).max, swapper); vm.stopPrank(); - assertEq(originARM.withdrawsQueued() - originARM.withdrawsClaimed(), queuedAssets, "queued amount tracked"); + assertEq(originARM.reservedWithdrawLiquidity(), queuedAssets, "queued amount tracked"); assertEq(weth.balanceOf(address(originARM)), queuedAssets, "queued redeem liquidity remains in ARM"); } diff --git a/test/unit/OriginARM/TotalAssets.sol b/test/unit/OriginARM/TotalAssets.sol index bbd4967d..b6ddc90f 100644 --- a/test/unit/OriginARM/TotalAssets.sol +++ b/test/unit/OriginARM/TotalAssets.sol @@ -26,12 +26,12 @@ contract Unit_Concrete_OriginARM_TotalAssets_Test_ is Unit_Shared_Test { assertEq(originARM.totalAssets(), MIN_TOTAL_SUPPLY, "Wrong total assets"); } - /// deposit then redeem should have no impact on total assets + /// deposit then redeem keeps escrowed shares in totalSupply, so totalAssets is not reduced by the queued redeem function test_TotalAssets_When_WithdrawQueue_IsNotZero() public deposit(alice, 1 ether) requestRedeemAll(alice) { - assertEq(originARM.totalAssets(), MIN_TOTAL_SUPPLY, "Wrong total assets"); + assertEq(originARM.totalAssets(), 1 ether + MIN_TOTAL_SUPPLY, "Wrong total assets"); } - /// market take a 100% loss, totalAssets should be MIN_TOTAL_SUPPLY + /// market liquidity constraints do not reduce totalAssets, which values the market using convertToAssets function test_TotalAssets_When_MarketLoseAll() public addMarket(address(market)) @@ -42,7 +42,7 @@ contract Unit_Concrete_OriginARM_TotalAssets_Test_ is Unit_Shared_Test { simulateMarketLoss(address(market), 1 ether) requestRedeem(alice, 1 ether) { - assertEq(originARM.totalAssets(), MIN_TOTAL_SUPPLY, "Wrong total assets"); + assertEq(originARM.totalAssets(), 1 ether + MIN_TOTAL_SUPPLY, "Wrong total assets"); } function test_TotalAssets_When_AssetIsLessThanOutstandingWithdrawals()