From 19b55d7886f5e4bffe25f4c98a0c1a6e64088ca2 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Thu, 3 Oct 2024 21:20:29 +1000 Subject: [PATCH] Only accrued fees when they are collected (#30) * Only accrue fees when the fees are collected * packed fee and lastAvailableAssets back into a single slot --- src/contracts/AbstractARM.sol | 103 +++++------ .../ClaimRedeem.t.sol | 10 +- .../Constructor.t.sol | 2 +- .../LidoFixedPriceMultiLpARM/Deposit.t.sol | 173 ++++++++++++------ .../RequestRedeem.t.sol | 89 +++++---- 5 files changed, 216 insertions(+), 161 deletions(-) diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index 6be4afb..f7c0e4a 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -95,12 +95,10 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// 2,000 = 20% performance fee /// 500 = 5% performance fee uint16 public fee; - /// @notice The performance fees accrued but not collected. - /// This is removed from the available assets. - uint112 public feesAccrued; - /// @notice The available assets at the last time performance fees were calculated. - /// This can only go up so is a high watermark. - uint128 public lastAvailableAssets; + /// @notice The available assets the the last time performance fees were collected and adjusted + /// for liquidity assets (WETH) deposited and redeemed. + /// This can be negative if there were asset gains and then all the liquidity providers redeemed. + int128 public lastAvailableAssets; /// @notice The account that can collect the performance fee address public feeCollector; @@ -118,7 +116,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { address indexed withdrawer, uint256 indexed requestId, uint256 assets, uint256 queued, uint256 claimTimestamp ); event RedeemClaimed(address indexed withdrawer, uint256 indexed requestId, uint256 assets); - event FeeCalculated(uint256 newFeesAccrued, uint256 assetIncrease); event FeeCollected(address indexed feeCollector, uint256 fee); event FeeUpdated(uint256 fee); event FeeCollectorUpdated(address indexed newFeeCollector); @@ -168,7 +165,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // Initialize the last available assets to the current available assets // This ensures no performance fee is accrued when the performance fee is calculated when the fee is set - lastAvailableAssets = SafeCast.toUint128(_availableAssets()); + lastAvailableAssets = SafeCast.toInt128(SafeCast.toInt256(_availableAssets())); _setFee(_fee); _setFeeCollector(_feeCollector); @@ -406,10 +403,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @param assets The amount of liquidity assets to deposit /// @return shares The amount of shares that were minted function deposit(uint256 assets) external returns (uint256 shares) { - // Accrue any performance fees based on the increase in available assets before - // the liquidity asset from the deposit is transferred into the ARM - _accruePerformanceFee(); - // Calculate the amount of shares to mint after the performance fees have been accrued // which reduces the available assets, and before new assets are deposited. shares = convertToShares(assets); @@ -421,7 +414,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { _mint(msg.sender, shares); // Add the deposited assets to the last available assets - lastAvailableAssets = SafeCast.toUint128(lastAvailableAssets + assets); + lastAvailableAssets += SafeCast.toInt128(SafeCast.toInt256(assets)); // Check the liquidity provider caps after the new assets have been deposited if (liquidityProviderController != address(0)) { @@ -443,10 +436,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @return requestId The index of the withdrawal request /// @return assets The amount of liquidity assets that will be claimable by the redeemer function requestRedeem(uint256 shares) external returns (uint256 requestId, uint256 assets) { - // Accrue any performance fees based on the increase in available assets before - // the liquidity asset from the redeem is reserved for the ARM withdrawal queue - _accruePerformanceFee(); - // Calculate the amount of assets to transfer to the redeemer assets = convertToAssets(shares); @@ -471,7 +460,7 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { _burn(msg.sender, shares); // Remove the redeemed assets from the last available assets - lastAvailableAssets = SafeCast.toUint128(lastAvailableAssets - assets); + lastAvailableAssets -= SafeCast.toInt128(SafeCast.toInt256(assets)); emit RedeemRequested(msg.sender, requestId, assets, queued, claimTimestamp); } @@ -566,21 +555,17 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @notice The total amount of assets in the ARM and external withdrawal queue, /// less the liquidity assets reserved for the ARM's withdrawal queue and accrued fees. function totalAssets() public view virtual returns (uint256) { - uint256 availableAssetsBeforeFees = _availableAssets(); - - // If the available assets have decreased, then we don't charge a performance fee - if (availableAssetsBeforeFees <= lastAvailableAssets) return availableAssetsBeforeFees; + (uint256 fees, uint256 newAvailableAssets) = _feesAccrued(); - // Calculate the increase in assets since the last time fees were calculated - uint256 assetIncrease = availableAssetsBeforeFees - lastAvailableAssets; + if (fees > newAvailableAssets) return 0; - // Calculate the performance fee and remove from the available assets before new fees are removed - return availableAssetsBeforeFees - ((assetIncrease * fee) / FEE_SCALE); + // Calculate the performance fee and remove from the available assets + return newAvailableAssets - fees; } /// @dev Calculate the available assets which is the assets in the ARM, external withdrawal queue, - /// less liquidity assets reserved for the ARM's withdrawal queue and accrued fees. - /// The accrued fees are from the last time fees were calculated. + /// less liquidity assets reserved for the ARM's withdrawal queue. + /// This does not exclude any accrued performance fees. function _availableAssets() internal view returns (uint256) { // Get the assets in the ARM and external withdrawal queue uint256 assets = token0.balanceOf(address(this)) + token1.balanceOf(address(this)) + _externalWithdrawQueue(); @@ -591,13 +576,12 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { // If the ARM becomes insolvent enough that the available assets in the ARM and external withdrawal queue // is less than the outstanding withdrawals and accrued fees. - if (assets + claimedMem < queuedMem + feesAccrued) { + if (assets + claimedMem < queuedMem) { return 0; } // Need to remove the liquidity assets that have been reserved for the withdrawal queue - // and any accrued fees - return assets + claimedMem - queuedMem - feesAccrued; + return assets + claimedMem - queuedMem; } /// @dev Hook for calculating the amount of assets in an external withdrawal queue like Lido or OETH @@ -627,27 +611,6 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// Performance Fee Functions //////////////////////////////////////////////////// - /// @dev Accrues the performance fee based on the increase in available assets - /// Needs to be called before any action that changes the liquidity provider shares. eg deposit and redeem - function _accruePerformanceFee() internal { - uint256 newAvailableAssets = _availableAssets(); - - // Do not accrued a performance fee if the available assets has decreased - if (newAvailableAssets <= lastAvailableAssets) return; - - uint256 assetIncrease = newAvailableAssets - lastAvailableAssets; - uint256 newFeesAccrued = (assetIncrease * fee) / FEE_SCALE; - - // Save the new accrued fees back to storage - feesAccrued = SafeCast.toUint112(feesAccrued + newFeesAccrued); - // Save the new available assets back to storage less the new accrued fees. - // This is be updated again in the post deposit and post withdraw hooks to include - // the assets deposited or withdrawn - lastAvailableAssets = SafeCast.toUint128(newAvailableAssets - newFeesAccrued); - - emit FeeCalculated(newFeesAccrued, assetIncrease); - } - /// @notice Owner sets the performance fee on increased assets /// @param _fee The performance fee measured in basis points (1/100th of a percent) /// 10,000 = 100% performance fee @@ -664,8 +627,8 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { function _setFee(uint256 _fee) internal { require(_fee <= FEE_SCALE, "ARM: fee too high"); - // Accrued any performance fees up to this point using the old fee - _accruePerformanceFee(); + // Collect any performance fees up to this point using the old fee + collectFees(); fee = SafeCast.toUint16(_fee); @@ -682,19 +645,37 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { /// @notice Transfer accrued performance fees to the fee collector /// This requires enough liquidity assets in the ARM to cover the accrued fees. - function collectFees() external returns (uint256 fees) { + function collectFees() public returns (uint256 fees) { + uint256 newAvailableAssets; // Accrue any performance fees up to this point - _accruePerformanceFee(); + (fees, newAvailableAssets) = _feesAccrued(); - // Read the updated accrued fees from storage - fees = feesAccrued; - require(fees <= IERC20(liquidityAsset).balanceOf(address(this)), "ARM: insufficient liquidity"); + if (fee == 0) return fees; - // Reset the accrued fees in storage - feesAccrued = 0; + require(fees <= IERC20(liquidityAsset).balanceOf(address(this)), "ARM: insufficient liquidity"); IERC20(liquidityAsset).transfer(feeCollector, fees); + // Save the new available assets back to storage less the collected fees. + lastAvailableAssets = SafeCast.toInt128(SafeCast.toInt256(newAvailableAssets) - SafeCast.toInt256(fees)); + emit FeeCollected(feeCollector, fees); } + + /// @notice Calculates the performance fees accrued since the last time fees were collected + function feesAccrued() public view returns (uint256 fees) { + (fees,) = _feesAccrued(); + } + + function _feesAccrued() public view returns (uint256 fees, uint256 newAvailableAssets) { + newAvailableAssets = _availableAssets(); + + // Calculate the increase in assets since the last time fees were calculated + int256 assetIncrease = SafeCast.toInt256(newAvailableAssets) - lastAvailableAssets; + + // Do not accrued a performance fee if the available assets has decreased + if (assetIncrease <= 0) return (0, newAvailableAssets); + + fees = SafeCast.toUint256(assetIncrease) * fee / FEE_SCALE; + } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol index de65cff..3845d60 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol @@ -121,7 +121,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -142,7 +142,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -186,7 +186,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), 0); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -210,7 +210,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT / 2); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -233,7 +233,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); diff --git a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol index 7f050d0..7a3f14f 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Constructor.t.sol @@ -22,7 +22,7 @@ contract Fork_Concrete_LidoARM_Constructor_Test is Fork_Shared_Test_ { assertEq(lidoARM.operator(), operator); assertEq(lidoARM.feeCollector(), feeCollector); assertEq(lidoARM.fee(), 2000); - assertEq(lidoARM.lastAvailableAssets(), 1e12); + assertEq(lidoARM.lastAvailableAssets(), int256(1e12)); assertEq(lidoARM.feesAccrued(), 0); // the 20% performance fee is removed on initialization assertEq(lidoARM.totalAssets(), 1e12); diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 943570c..1e55915 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -108,7 +108,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before"); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets before"); @@ -131,7 +131,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount)); assertEq(lidoARM.balanceOf(address(this)), shares); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); @@ -154,7 +154,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount)); assertEq(lidoARM.balanceOf(address(this)), amount); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); @@ -177,7 +177,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount * 2)); assertEq(lidoARM.balanceOf(address(this)), shares * 2); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); @@ -201,7 +201,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount)); assertEq(lidoARM.balanceOf(alice), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); @@ -225,7 +225,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount * 2); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount * 2)); assertEq(lidoARM.balanceOf(alice), shares); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); @@ -239,32 +239,32 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { function test_Deposit_NoFeesAccrued_EmptyWithdrawQueue_FirstDeposit_WithPerfs() public setTotalAssetsCap(type(uint256).max) // No need to restrict it for this test. - setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) + setLiquidityProviderCap(address(this), DEFAULT_AMOUNT * 20) { // simulate asset gain uint256 balanceBefore = weth.balanceOf(address(lidoARM)); 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; + // Assertions Before assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + assetGain); assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether before"); - assertEq(lidoARM.feesAccrued(), 0, "fee accrued before"); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY, "last available assets before"); + assertEq(lidoARM.feesAccrued(), expectedFeesAccrued, "fee accrued before"); // No perfs so no fees + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), "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(), balanceBefore + assetGain * 80 / 100, "Total assets before"); - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT, "lp cap before"); + assertEq(lidoARM.totalAssets(), expectedTotalAssetsBeforeDeposit, "Total assets before"); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT * 20, "lp cap before"); assertEqQueueMetadata(0, 0, 0, 0); - // 20% of the asset gain goes to the performance fees - uint256 feesAccrued = assetGain * 20 / 100; - uint256 rawTotalAsset = weth.balanceOf(address(lidoARM)) - feesAccrued; // No steth and no externalWithdrawQueue - uint256 depositedAssets = DEFAULT_AMOUNT; - - uint256 expectedShares = depositedAssets * MIN_TOTAL_SUPPLY / rawTotalAsset; + uint256 depositedAssets = DEFAULT_AMOUNT * 20; + uint256 expectedShares = depositedAssets * MIN_TOTAL_SUPPLY / expectedTotalAssetsBeforeDeposit; // Expected events vm.expectEmit({emitter: address(weth)}); emit IERC20.Transfer(address(this), address(lidoARM), depositedAssets); @@ -283,14 +283,11 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(steth.balanceOf(address(lidoARM)), 0, "stETH balance after"); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + assetGain + depositedAssets, "WETH balance after"); assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether after"); - assertEq(lidoARM.feesAccrued(), feesAccrued, "fees accrued after"); // No perfs so no fees - assertEq( - lidoARM.lastAvailableAssets(), - MIN_TOTAL_SUPPLY + (assetGain * 80 / 100) + depositedAssets, - "last total assets after" - ); + assertEq(lidoARM.feesAccrued(), expectedFeesAccrued, "fees accrued after"); // No perfs so no fees + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + 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"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used assertEqQueueMetadata(0, 0, 0, 0); } @@ -324,7 +321,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore, "WETH ARM balance before"); assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether before"); assertEq(lidoARM.feesAccrued(), 0, "Fees accrued before"); - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY, "last available assets before"); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), "last available assets before"); assertEq(lidoARM.balanceOf(alice), 0, "alice shares before"); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY, "total supply before"); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY, "total assets before"); @@ -353,7 +350,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore + amount, "WETH ARM balance after"); assertEq(lidoARM.outstandingEther(), 0, "Outstanding ether after"); assertEq(lidoARM.feesAccrued(), 0, "Fees accrued after"); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + amount, "last available assets after"); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount), "last available assets after"); assertEq(lidoARM.balanceOf(alice), shares, "alice shares after"); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount, "total supply after"); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount, "total assets after"); @@ -364,11 +361,12 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { } /// @notice Test the following scenario: - /// 1. ARM gain assets + /// 1. ARM gain assets in stETH /// 2. Operator request a withdraw from Lido on steth /// 3. User deposit liquidity /// 4. Operator claim the withdrawal on Lido /// 5. User burn shares + /// 6. Operator collects the performance fees /// Checking that amount deposited can be retrieved function test_Deposit_WithOutStandingWithdrawRequest_BeforeDeposit_ClaimedLidoWithdraw_WithAssetGain() public @@ -377,19 +375,22 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { // Assertions Before + uint256 expectedTotalSupplyBeforeDeposit = MIN_TOTAL_SUPPLY; + uint256 expectTotalAssetsBeforeDeposit = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100; assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); - assertEq(lidoARM.outstandingEther(), DEFAULT_AMOUNT); - assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.outstandingEther(), 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(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), "last available assets before deposit"); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares before - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy - assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT); assertEqQueueMetadata(0, 0, 0, 0); - // Expected values - uint256 expectShares = DEFAULT_AMOUNT * MIN_TOTAL_SUPPLY / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100); + // Expected values = 1249998437501 + // shares = assets * total supply / total assets + uint256 expectShares = DEFAULT_AMOUNT * expectedTotalSupplyBeforeDeposit / expectTotalAssetsBeforeDeposit; // Expected events vm.expectEmit({emitter: address(weth)}); @@ -402,30 +403,60 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { requests[0] = requestId; // Main calls - // 1. User mint shares + // 3. User mint shares uint256 shares = lidoARM.deposit(DEFAULT_AMOUNT); - // 2. Lido finalization process is simulated + + 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.lastAvailableAssets(), + int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT), + "last available assets after deposit" + ); + + // 4. Lido finalization process is simulated lidoARM.totalAssets(); _mockFunctionClaimWithdrawOnLidoARM(DEFAULT_AMOUNT); - // 3. Operator claim withdrawal on lido + + // 4. Operator claim withdrawal on lido lidoARM.totalAssets(); lidoARM.claimStETHWithdrawalForWETH(requests); - // 4. User burn shares + + // 5. User burn shares (, uint256 receivedAssets) = lidoARM.requestRedeem(shares); - uint256 excessLeftover = DEFAULT_AMOUNT - receivedAssets; - // Assertions After - assertEq(steth.balanceOf(address(lidoARM)), 0); - assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); - assertEq(lidoARM.outstandingEther(), 0); - assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 80 / 100 + excessLeftover); - assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used + // Assertions after redeem + // This difference comes from the small value of shares, which reduces the precision of the calculation + assertApproxEqRel(receivedAssets, DEFAULT_AMOUNT, 1e6, "received assets from redeem"); // 1e6 -> 0.0000000001%, + assertEq(steth.balanceOf(address(lidoARM)), 0, "ARM stETH balance after redeem"); + assertEq( + weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2, "ARM WETH balance after redeem" + ); + assertEq(lidoARM.outstandingEther(), 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"); + assertApproxEqAbs( + lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), 4e6, "last available assets after redeem" + ); + assertEq(lidoARM.balanceOf(address(this)), 0, "User shares after redeem"); + assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "all user cap used"); assertEqQueueMetadata(receivedAssets, 0, 0, 1); - assertApproxEqRel(receivedAssets, DEFAULT_AMOUNT, 1e6, "received assets"); // 1e6 -> 0.0000000001%, - // This difference comes from the small value of shares, which reduces the precision of the calculation + + // 6. collect fees + 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.feesAccrued(), 0, "fees accrued after collect fees"); + assertApproxEqAbs( + lidoARM.lastAvailableAssets(), + int256(expectTotalAssetsBeforeDeposit), + 4e6, + "last available assets after collect fees" + ); } /// @notice Test the following scenario: @@ -465,7 +496,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used @@ -476,7 +507,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { /// @notice Test the following scenario: /// 1. User deposit liquidity /// 2. ARM asset gain (on steth) - /// 2. Operator request a withdraw from Lido on steth + /// 3. Operator request a withdraw from Lido on steth /// 4. Operator claim the withdrawal on Lido /// 5. User burn shares /// Checking that amount deposited + benefice can be retrieved @@ -489,33 +520,55 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Main calls: // 1. User mint shares + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), "last available assets before deposit"); + uint256 shares = lidoARM.deposit(DEFAULT_AMOUNT); - // Simulate a swap from WETH to stETH + + assertEq(lidoARM.feesAccrued(), 0, "fees accrued after deposit"); + assertEq( + lidoARM.lastAvailableAssets(), + int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT), + "last available assets after deposit" + ); + + // 2. Simulate asset gain (on steth) deal(address(steth), address(lidoARM), DEFAULT_AMOUNT); - // 2. Operator request a claim on withdraw + assertApproxEqAbs( + lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, STETH_ERROR_ROUNDING, "fees accrued before redeem" + ); + + // 3. Operator request a claim on withdraw lidoARM.requestStETHWithdrawalForETH(amounts1); + // 3. We simulate the finalization of the process _mockFunctionClaimWithdrawOnLidoARM(DEFAULT_AMOUNT); uint256 requestId = stETHWithdrawal.getLastRequestId(); uint256[] memory requests = new uint256[](1); requests[0] = requestId; + // 4. Operator claim the withdrawal on lido lidoARM.claimStETHWithdrawalForWETH(requests); + // 5. User burn shares (, uint256 receivedAssets) = lidoARM.requestRedeem(shares); uint256 userBenef = (DEFAULT_AMOUNT * 80 / 100) * DEFAULT_AMOUNT / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - uint256 deadAddressBenef = (DEFAULT_AMOUNT * 80 / 100) * MIN_TOTAL_SUPPLY / (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.outstandingEther(), 0); - assertEq(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100); // No perfs so no fees - assertApproxEqAbs(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + deadAddressBenef, STETH_ERROR_ROUNDING); - assertEq(lidoARM.balanceOf(address(this)), 0); // Ensure no shares after - assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); // Minted to dead on deploy - assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // All the caps are used + assertApproxEqAbs(lidoARM.feesAccrued(), DEFAULT_AMOUNT * 20 / 100, 2, "fees accrued after redeem"); + assertApproxEqAbs( + lidoARM.lastAvailableAssets(), + // initial assets + user deposit - (user deposit + asset gain less fees) + int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT) - int256(DEFAULT_AMOUNT + userBenef), + 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(liquidityProviderController.liquidityProviderCaps(address(this)), 0, "user cap"); // All the caps are used assertEqQueueMetadata(receivedAssets, 0, 0, 1); - assertEq(receivedAssets, DEFAULT_AMOUNT + userBenef, "received assets"); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index 0e26415..0d4d417 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -33,7 +33,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -57,7 +57,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); @@ -76,7 +76,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 3 / 4); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 3 / 4); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only @@ -104,7 +104,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); assertEq(lidoARM.outstandingEther(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4)); assertEq(lidoARM.balanceOf(address(this)), DEFAULT_AMOUNT * 1 / 4); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 1 / 4); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); // Down only @@ -121,34 +121,47 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { // Not needed as the same as in `test_RequestRedeem_AfterFirstDeposit_NoPerfs_EmptyWithdrawQueue` // Simulate assets gain in ARM + uint256 assetsBeforeGain = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT; uint256 assetsGain = DEFAULT_AMOUNT; - deal(address(weth), address(lidoARM), weth.balanceOf(address(lidoARM)) + assetsGain); - - // Calculate expected values - uint256 feeAccrued = assetsGain * 20 / 100; // 20% fee - uint256 totalAsset = weth.balanceOf(address(lidoARM)) - feeAccrued; - uint256 expectedAssets = DEFAULT_AMOUNT * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - uint256 expectedAssetsDead = MIN_TOTAL_SUPPLY * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + uint256 assetsAfterGain = assetsBeforeGain + assetsGain; + deal(address(weth), address(lidoARM), assetsAfterGain); - vm.expectEmit({emitter: address(lidoARM)}); - emit AbstractARM.FeeCalculated(feeAccrued, assetsGain); + // Expected Events vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); + // Main call - lidoARM.requestRedeem(DEFAULT_AMOUNT); + (, uint256 actualAssetsFromRedeem) = lidoARM.requestRedeem(DEFAULT_AMOUNT); + + // Calculate expected values + uint256 expectedFeeAccrued = assetsGain * 20 / 100; // 20% fee + uint256 expectedTotalAsset = assetsAfterGain - expectedFeeAccrued; + uint256 expectedAssetsFromRedeem = DEFAULT_AMOUNT * expectedTotalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - uint256 delay = lidoARM.CLAIM_DELAY(); // Assertions After + assertEq(actualAssetsFromRedeem, expectedAssetsFromRedeem, "Assets from redeem"); assertEq(steth.balanceOf(address(lidoARM)), 0); - assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT * 2); // +perfs - assertEq(lidoARM.outstandingEther(), 0); - assertEq(lidoARM.feesAccrued(), feeAccrued); - assertApproxEqAbs(lidoARM.lastAvailableAssets(), expectedAssetsDead, 1); // 1 wei of error + assertEq(weth.balanceOf(address(lidoARM)), assetsAfterGain); + assertEq(lidoARM.outstandingEther(), 0, "stETH in Lido withdrawal queue"); + assertEq(lidoARM.feesAccrued(), expectedFeeAccrued, "fees accrued"); + assertApproxEqAbs( + lidoARM.lastAvailableAssets(), + int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT) - int256(expectedAssetsFromRedeem), + 1, + "last available assets after" + ); // 1 wei of error assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(expectedAssets, 0, 0, 1); - assertEqUserRequest(0, address(this), false, block.timestamp + delay, expectedAssets, expectedAssets); + assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 0, 1); + assertEqUserRequest( + 0, + address(this), + false, + block.timestamp + lidoARM.CLAIM_DELAY(), + expectedAssetsFromRedeem, + expectedAssetsFromRedeem + ); } /// @notice Test the `requestRedeem` function when ARM lost a bit of money before the request. @@ -162,31 +175,39 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { // Not needed as the same as in `test_RequestRedeem_AfterFirstDeposit_NoPerfs_EmptyWithdrawQueue` // Simulate assets loss in ARM + uint256 assetsBeforeLoss = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT; uint256 assetsLoss = DEFAULT_AMOUNT / 10; // 0.1 ether of loss - deal(address(weth), address(lidoARM), weth.balanceOf(address(lidoARM)) - assetsLoss); - - // Calculate expected values - uint256 feeAccrued = 0; // No profits - uint256 totalAsset = weth.balanceOf(address(lidoARM)); - uint256 expectedAssets = DEFAULT_AMOUNT * totalAsset / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); - uint256 expectedLastAvailableAssets = lidoARM.lastAvailableAssets() - expectedAssets; + uint256 assetsAfterLoss = assetsBeforeLoss - assetsLoss; + deal(address(weth), address(lidoARM), assetsAfterLoss); + // Expected Events vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(this), address(0), DEFAULT_AMOUNT); + // Main call - lidoARM.requestRedeem(DEFAULT_AMOUNT); + (, uint256 actualAssetsFromRedeem) = lidoARM.requestRedeem(DEFAULT_AMOUNT); uint256 delay = lidoARM.CLAIM_DELAY(); // Assertions After + uint256 expectedAssetsFromRedeem = DEFAULT_AMOUNT * assetsAfterLoss / (MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); + assertEq(actualAssetsFromRedeem, expectedAssetsFromRedeem, "Assets from redeem"); assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - assetsLoss); - assertEq(lidoARM.outstandingEther(), 0, "outstanding ether"); - assertEq(lidoARM.feesAccrued(), feeAccrued, "fees accrued"); - assertApproxEqAbs(lidoARM.lastAvailableAssets(), expectedLastAvailableAssets, 1, "last available assets"); // 1 wei of error + assertEq(lidoARM.outstandingEther(), 0, "stETH in Lido withdrawal queue"); + assertEq(lidoARM.feesAccrued(), 0, "fees accrued"); + assertApproxEqAbs( + lidoARM.lastAvailableAssets(), + int256(assetsBeforeLoss - expectedAssetsFromRedeem), + 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(), assetsAfterLoss - actualAssetsFromRedeem, "total assets"); assertEq(liquidityProviderController.liquidityProviderCaps(address(this)), 0); - assertEqQueueMetadata(expectedAssets, 0, 0, 1); - assertEqUserRequest(0, address(this), false, block.timestamp + delay, expectedAssets, expectedAssets); + assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 0, 1); + assertEqUserRequest( + 0, address(this), false, block.timestamp + delay, expectedAssetsFromRedeem, expectedAssetsFromRedeem + ); } }