diff --git a/.gitignore b/.gitignore index 9b792cf..8a07b5c 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,7 @@ dependencies/ soldeer.lock # Coverage -lock.info* +lcov.info* # Defender Actions dist diff --git a/test/fork/LidoFixedPriceMultiLpARM/CollectFees.t.sol b/test/fork/LidoFixedPriceMultiLpARM/CollectFees.t.sol new file mode 100644 index 0000000..e84b8d6 --- /dev/null +++ b/test/fork/LidoFixedPriceMultiLpARM/CollectFees.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +// Contracts +import {IERC20} from "contracts/Interfaces.sol"; +import {PerformanceFee} from "contracts/PerformanceFee.sol"; + +contract Fork_Concrete_LidoFixedPriceMultiLpARM_CollectFees_Test_ is Fork_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + /// @notice This test is expected to revert as almost all the liquidity is in stETH + function test_RevertWhen_CollectFees_Because_InsufficientLiquidity() + public + simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(steth), true) + { + vm.expectRevert("ARM: insufficient liquidity"); + lidoFixedPriceMulltiLpARM.collectFees(); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_CollectFees_Once() + public + simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(weth), true) + { + address feeCollector = lidoFixedPriceMulltiLpARM.feeCollector(); + uint256 fee = DEFAULT_AMOUNT * 20 / 100; + + // Expected Events + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(lidoFixedPriceMulltiLpARM), feeCollector, fee); + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit PerformanceFee.FeeCollected(feeCollector, fee); + + // Main call + uint256 claimedFee = lidoFixedPriceMulltiLpARM.collectFees(); + + // Assertions after + assertEq(claimedFee, fee); + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), 0); + } + + function test_CollectFees_Twice() + public + simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(weth), true) + collectFeesOnLidoFixedPriceMultiLpARM + simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(weth), true) + { + // Main call + uint256 claimedFee = lidoFixedPriceMulltiLpARM.collectFees(); + + // Assertions after + assertEq(claimedFee, DEFAULT_AMOUNT * 20 / 100); // This test should pass! + } +} diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index cca60cb..34d19c6 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -8,6 +8,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; import {IERC20} from "contracts/Interfaces.sol"; import {MultiLP} from "contracts/MultiLP.sol"; import {PerformanceFee} from "contracts/PerformanceFee.sol"; +import {LiquidityProviderControllerARM} from "contracts/LiquidityProviderControllerARM.sol"; contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// @@ -150,4 +151,28 @@ contract Fork_Concrete_lidoFixedPriceMulltiLpARM_Setters_Test_ is Fork_Shared_Te vm.expectRevert("ARM: Only owner can call this function."); lidoFixedPriceMulltiLpARM.setOperator(address(0)); } + + ////////////////////////////////////////////////////// + /// --- LIQUIIDITY PROVIDER CONTROLLER - REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_LiquidityProviderController_SetLiquidityProvider_Because_NotOwner() + public + asRandomAddress + { + vm.expectRevert("ARM: Only owner can call this function."); + lidoFixedPriceMulltiLpARM.setLiquidityProviderController(address(0)); + } + + ////////////////////////////////////////////////////// + /// --- LIQUIIDITY PROVIDER CONTROLLER - PASSING TESTS + ////////////////////////////////////////////////////// + function test_LiquidityProviderController_SetLiquidityProvider() public asLidoFixedPriceMultiLpARMOwner { + address newLiquidityProviderController = vm.randomAddress(); + + vm.expectEmit({emitter: address(lidoFixedPriceMulltiLpARM)}); + emit LiquidityProviderControllerARM.LiquidityProviderControllerUpdated(newLiquidityProviderController); + lidoFixedPriceMulltiLpARM.setLiquidityProviderController(newLiquidityProviderController); + + assertEq(lidoFixedPriceMulltiLpARM.liquidityProviderController(), newLiquidityProviderController); + } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol index a92e2a4..bfeb97c 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/TotalAssets.t.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +// Foundry +import {stdError} from "forge-std/StdError.sol"; + // Test imports import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; @@ -24,6 +27,23 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared // Approve STETH for Lido lidoFixedPriceMulltiLpARM.approveStETH(); + + deal(address(weth), address(this), 1_000 ether); + weth.approve(address(lidoFixedPriceMulltiLpARM), type(uint256).max); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TEST + ////////////////////////////////////////////////////// + function test_RevertWhen_TotalAssets_Because_MathError() + public + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT, address(weth), true) + requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + simulateAssetGainInLidoFixedPriceMultiLpARM(DEFAULT_AMOUNT * 2, address(weth), false) + { + vm.expectRevert(stdError.arithmeticError); + lidoFixedPriceMulltiLpARM.totalAssets(); } ////////////////////////////////////////////////////// @@ -133,4 +153,36 @@ contract Fork_Concrete_LidoFixedPriceMultiLpARM_TotalAssets_Test_ is Fork_Shared // Check total assets after withdrawal is the same as before assertApproxEqAbs(lidoFixedPriceMulltiLpARM.totalAssets(), totalAssetsBefore, STETH_ERROR_ROUNDING); } + + function test_TotalAssets_With_FeeAccrued_NotNull() public { + uint256 assetGain = DEFAULT_AMOUNT; + // Simulate asset gain + deal( + address(weth), + address(lidoFixedPriceMulltiLpARM), + weth.balanceOf(address(lidoFixedPriceMulltiLpARM)) + assetGain + ); + + // User deposit, this will trigger a fee calculation + lidoFixedPriceMulltiLpARM.deposit(DEFAULT_AMOUNT); + + // Assert fee accrued is not null + assertEq(lidoFixedPriceMulltiLpARM.feesAccrued(), assetGain * 20 / 100); + + assertEq( + lidoFixedPriceMulltiLpARM.totalAssets(), + MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + assetGain - assetGain * 20 / 100 + ); + } + + function test_TotalAssets_When_ARMIsInsolvent() + public + depositInLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + requestRedeemFromLidoFixedPriceMultiLpARM(address(this), DEFAULT_AMOUNT) + { + // Simulate a loss of assets + deal(address(weth), address(lidoFixedPriceMulltiLpARM), DEFAULT_AMOUNT - 1); + + assertEq(lidoFixedPriceMulltiLpARM.totalAssets(), 0); + } } diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index ae2570c..c5d7464 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -8,6 +8,9 @@ import {VmSafe} from "forge-std/Vm.sol"; import {Helpers} from "test/fork/utils/Helpers.sol"; import {MockCall} from "test/fork/utils/MockCall.sol"; +// Contracts +import {IERC20} from "contracts/Interfaces.sol"; + abstract contract Modifiers is Helpers { /// @notice Impersonate Alice. modifier asAlice() { @@ -72,6 +75,7 @@ abstract contract Modifiers is Helpers { _; } + /// @notice Deposit WETH into the LidoFixedPriceMultiLpARM contract. modifier depositInLidoFixedPriceMultiLpARM(address user, uint256 amount) { // Todo: extend this logic to other modifier if needed (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); @@ -113,6 +117,7 @@ abstract contract Modifiers is Helpers { _; } + /// @notice Claim redeem from LidoFixedPriceMultiLpARM contract. modifier claimRequestOnLidoFixedPriceMultiLpARM(address user, uint256 requestId) { // Todo: extend this logic to other modifier if needed (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); @@ -130,6 +135,41 @@ abstract contract Modifiers is Helpers { _; } + /// @notice Simulate asset gain or loss in LidoFixedPriceMultiLpARM contract. + modifier simulateAssetGainInLidoFixedPriceMultiLpARM(uint256 assetGain, address token, bool gain) { + // Todo: extend this logic to other modifier if needed + (VmSafe.CallerMode mode, address _address, address _origin) = vm.readCallers(); + vm.stopPrank(); + + if (gain) { + deal( + token, + address(lidoFixedPriceMulltiLpARM), + IERC20(token).balanceOf(address(lidoFixedPriceMulltiLpARM)) + uint256(assetGain) + ); + } else { + deal( + token, + address(lidoFixedPriceMulltiLpARM), + IERC20(token).balanceOf(address(lidoFixedPriceMulltiLpARM)) - uint256(assetGain) + ); + } + + if (mode == VmSafe.CallerMode.Prank) { + vm.prank(_address, _origin); + } else if (mode == VmSafe.CallerMode.RecurrentPrank) { + vm.startPrank(_address, _origin); + } + _; + } + + /// @notice Collect fees on LidoFixedPriceMultiLpARM contract. + modifier collectFeesOnLidoFixedPriceMultiLpARM() { + lidoFixedPriceMulltiLpARM.collectFees(); + _; + } + + /// @notice Skip time by a given delay. modifier skipTime(uint256 delay) { skip(delay); _;