diff --git a/src/contracts/AbstractARM.sol b/src/contracts/AbstractARM.sol index a64441c..1398410 100644 --- a/src/contracts/AbstractARM.sol +++ b/src/contracts/AbstractARM.sol @@ -366,7 +366,10 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable { } else { revert("ARM: Invalid in token"); } - amountIn = ((amountOut * PRICE_SCALE) / price) + 1; // +1 to always round in our favor + // always round in our favor + // +1 for truncation when dividing integers + // +2 to cover stETH transfers being up to 2 wei short of the requested transfer amount + amountIn = ((amountOut * PRICE_SCALE) / price) + 3; // Transfer the input tokens from the caller to this ARM contract _transferAssetFrom(address(inToken), msg.sender, address(this), amountIn); diff --git a/src/contracts/CapManager.sol b/src/contracts/CapManager.sol index 4e27895..756c74c 100644 --- a/src/contracts/CapManager.sol +++ b/src/contracts/CapManager.sol @@ -35,7 +35,7 @@ contract CapManager is Initializable, OwnableOperable { function initialize(address _operator) external initializer { _initOwnableOperable(_operator); - accountCapEnabled = true; + accountCapEnabled = false; } function postDepositHook(address liquidityProvider, uint256 assets) external { diff --git a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol index 8e866e4..6b9c3d0 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/ClaimRedeem.t.sol @@ -9,6 +9,7 @@ import {IERC20} from "contracts/Interfaces.sol"; import {AbstractARM} from "contracts/AbstractARM.sol"; contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { + bool private ac; uint256 private delay; ////////////////////////////////////////////////////// /// --- SETUP @@ -20,6 +21,8 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { delay = lidoARM.claimDelay(); deal(address(weth), address(this), 1_000 ether); + + ac = capManager.accountCapEnabled(); } ////////////////////////////////////////////////////// @@ -124,7 +127,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, 0, 1); assertEqUserRequest(0, address(this), false, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); assertEq(lidoARM.claimable(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT); @@ -146,7 +149,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); assertEq(assets, DEFAULT_AMOUNT); @@ -191,7 +194,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 1); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT, DEFAULT_AMOUNT); assertEq(assets, DEFAULT_AMOUNT); @@ -215,7 +218,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT / 2, 2); assertEqUserRequest(0, address(this), true, block.timestamp, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); assertEqUserRequest(1, address(this), false, block.timestamp + delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT); @@ -239,7 +242,7 @@ contract Fork_Concrete_LidoARM_ClaimRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(DEFAULT_AMOUNT, DEFAULT_AMOUNT, 2); assertEqUserRequest(0, address(this), true, block.timestamp - delay, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT / 2); assertEqUserRequest(1, address(this), true, block.timestamp, DEFAULT_AMOUNT / 2, DEFAULT_AMOUNT); diff --git a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol index 7429549..320e71c 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Deposit.t.sol @@ -11,8 +11,9 @@ import {IStETHWithdrawal} from "contracts/Interfaces.sol"; import {Mainnet} from "contracts/utils/Addresses.sol"; contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { - uint256[] amounts1 = new uint256[](1); - IStETHWithdrawal public stETHWithdrawal = IStETHWithdrawal(Mainnet.LIDO_WITHDRAWAL); + bool private ac; + uint256[] private amounts1 = new uint256[](1); + IStETHWithdrawal private stETHWithdrawal = IStETHWithdrawal(Mainnet.LIDO_WITHDRAWAL); ////////////////////////////////////////////////////// /// --- SETUP @@ -30,6 +31,8 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Amounts arrays amounts1[0] = DEFAULT_AMOUNT; + + ac = capManager.accountCapEnabled(); } ////////////////////////////////////////////////////// @@ -37,6 +40,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// function test_RevertWhen_Deposit_Because_LiquidityProviderCapExceeded_WithCapNull() public + enableCaps setLiquidityProviderCap(address(this), 0) { vm.expectRevert("LPC: LP cap exceeded"); @@ -45,6 +49,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { function test_RevertWhen_Deposit_Because_LiquidityProviderCapExceeded_WithCapNotNull() public + enableCaps setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { vm.expectRevert("LPC: LP cap exceeded"); @@ -53,6 +58,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { function test_RevertWhen_Deposit_Because_LiquidityProviderCapExceeded_WithCapReached() public + enableCaps setLiquidityProviderCap(address(this), DEFAULT_AMOUNT) { // Initial deposit @@ -108,7 +114,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0); assertEq(lidoARM.feesAccrued(), 0); // No perfs so no fees - assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); + if (ac) 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"); @@ -120,8 +126,10 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { emit IERC20.Transfer(address(this), address(lidoARM), amount); vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), address(this), amount); // shares == amount here - vm.expectEmit({emitter: address(capManager)}); - emit CapManager.LiquidityProviderCap(address(this), 0); + if (ac) { + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(address(this), 0); + } // Main call uint256 shares = lidoARM.deposit(amount); @@ -135,7 +143,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { 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"); - assertEq(capManager.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used assertEqQueueMetadata(0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e12) / totalAssets (1e12) = 1 ether } @@ -149,6 +157,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { depositInLidoARM(address(this), DEFAULT_AMOUNT) { uint256 amount = DEFAULT_AMOUNT; + // Assertions Before assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); @@ -158,7 +167,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { 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); - assertEq(capManager.liquidityProviderCaps(address(this)), amount); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), amount); assertEqQueueMetadata(0, 0, 0); // Expected events @@ -166,8 +175,10 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { emit IERC20.Transfer(address(this), address(lidoARM), amount); vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), address(this), amount); // shares == amount here - vm.expectEmit({emitter: address(capManager)}); - emit CapManager.LiquidityProviderCap(address(this), 0); + if (ac) { + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(address(this), 0); + } // Main call uint256 shares = lidoARM.deposit(amount); @@ -181,7 +192,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(address(this)), shares * 2); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); // All the caps are used + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } @@ -196,6 +207,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { depositInLidoARM(address(this), DEFAULT_AMOUNT) { uint256 amount = DEFAULT_AMOUNT; + // Assertions Before assertEq(steth.balanceOf(address(lidoARM)), 0); assertEq(weth.balanceOf(address(lidoARM)), MIN_TOTAL_SUPPLY + amount); @@ -205,7 +217,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(alice), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount); // Minted to dead on deploy assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount); - assertEq(capManager.liquidityProviderCaps(alice), amount); + if (ac) assertEq(capManager.liquidityProviderCaps(alice), amount); assertEqQueueMetadata(0, 0, 0); // Expected events @@ -213,8 +225,10 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { emit IERC20.Transfer(alice, address(lidoARM), amount); vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), alice, amount); // shares == amount here - vm.expectEmit({emitter: address(capManager)}); - emit CapManager.LiquidityProviderCap(alice, 0); + if (ac) { + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(alice, 0); + } vm.prank(alice); // Main call @@ -229,7 +243,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.balanceOf(alice), shares); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY + amount * 2); assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + amount * 2); - assertEq(capManager.liquidityProviderCaps(alice), 0); // All the caps are used + if (ac) assertEq(capManager.liquidityProviderCaps(alice), 0); // All the caps are used assertEqQueueMetadata(0, 0, 0); assertEq(shares, amount); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } @@ -260,7 +274,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { 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"); - assertEq(capManager.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT * 20, "lp cap before"); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT * 20, "lp cap before"); assertEqQueueMetadata(0, 0, 0); uint256 depositedAssets = DEFAULT_AMOUNT * 20; @@ -270,8 +284,10 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { emit IERC20.Transfer(address(this), address(lidoARM), depositedAssets); vm.expectEmit({emitter: address(lidoARM)}); emit IERC20.Transfer(address(0), address(this), expectedShares); - vm.expectEmit({emitter: address(capManager)}); - emit CapManager.LiquidityProviderCap(address(this), 0); + if (ac) { + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(address(this), 0); + } // deposit assets uint256 shares = lidoARM.deposit(depositedAssets); @@ -288,7 +304,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { 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(capManager.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0, "lp cap after"); // All the caps are used assertEqQueueMetadata(0, 0, 0); } @@ -305,9 +321,19 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { lidoARM.setCrossPrice(1e36); lidoARM.setPrices(1e36 - 1, 1e36); + assertEq(lidoARM.totalAssets(), MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT, "total assets before swap"); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT), "last available before swap"); + // User Swap stETH for 3/4 of WETH in the ARM deal(address(steth), address(this), DEFAULT_AMOUNT); lidoARM.swapTokensForExactTokens(steth, weth, 3 * DEFAULT_AMOUNT / 4, DEFAULT_AMOUNT, address(this)); + assertApproxEqAbs( + lidoARM.totalAssets(), + MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT + 2, + STETH_ERROR_ROUNDING, + "total assets after swap" + ); + assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT), "last available after swap"); // First user requests a full withdrawal uint256 firstUserShares = lidoARM.balanceOf(address(this)); @@ -316,34 +342,42 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { // Assertions Before uint256 stethBalanceBefore = 3 * DEFAULT_AMOUNT / 4; assertApproxEqAbs( - steth.balanceOf(address(lidoARM)), stethBalanceBefore, STETH_ERROR_ROUNDING, "stETH ARM balance before" + steth.balanceOf(address(lidoARM)), + stethBalanceBefore, + STETH_ERROR_ROUNDING, + "stETH ARM balance before deposit" ); uint256 wethBalanceBefore = MIN_TOTAL_SUPPLY + DEFAULT_AMOUNT - 3 * DEFAULT_AMOUNT / 4; - assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore, "WETH ARM balance before"); + assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore, "WETH ARM balance before deposit"); assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "Outstanding ether before"); - assertEq(lidoARM.feesAccrued(), 0, "Fees accrued before"); + assertEq(lidoARM.feesAccrued(), 0, "Fees accrued before deposit"); assertApproxEqAbs( lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), STETH_ERROR_ROUNDING, "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"); - assertEq(capManager.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 5, "lp cap 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"); + 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"); + assertApproxEqAbs(assetsRedeem, DEFAULT_AMOUNT, STETH_ERROR_ROUNDING, "assets redeem before deposit"); uint256 amount = DEFAULT_AMOUNT * 2; + // Expected values + uint256 expectedShares = amount * MIN_TOTAL_SUPPLY / (MIN_TOTAL_SUPPLY + 1); + // Expected events vm.expectEmit({emitter: address(weth)}); emit IERC20.Transfer(alice, address(lidoARM), amount); vm.expectEmit({emitter: address(lidoARM)}); - emit IERC20.Transfer(address(0), alice, amount); // shares == amount here - vm.expectEmit({emitter: address(capManager)}); - emit CapManager.LiquidityProviderCap(alice, DEFAULT_AMOUNT * 3); + emit IERC20.Transfer(address(0), alice, expectedShares); + if (ac) { + vm.expectEmit({emitter: address(capManager)}); + emit CapManager.LiquidityProviderCap(alice, DEFAULT_AMOUNT * 3); + } vm.prank(alice); // Main call @@ -353,22 +387,22 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertApproxEqAbs( steth.balanceOf(address(lidoARM)), stethBalanceBefore, STETH_ERROR_ROUNDING, "stETH ARM balance after" ); - assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore + amount, "WETH ARM balance after"); - assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "Outstanding ether after"); - assertEq(lidoARM.feesAccrued(), 0, "Fees accrued after"); // No perfs so no fees + assertEq(weth.balanceOf(address(lidoARM)), wethBalanceBefore + amount, "WETH ARM balance after deposit"); + assertEq(lidoARM.lidoWithdrawalQueueAmount(), 0, "Outstanding ether after deposit"); + assertEq(lidoARM.feesAccrued(), 0, "Fees accrued after deposit"); // No perfs so no fees assertApproxEqAbs( lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY + amount), STETH_ERROR_ROUNDING, - "last available assets after" + "last available assets after deposit" ); - 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"); - assertEq(capManager.liquidityProviderCaps(alice), DEFAULT_AMOUNT * 3, "alice cap after"); // All the caps are used + 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"); + 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); - assertApproxEqAbs(shares, amount, STETH_ERROR_ROUNDING, "shares after"); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether + assertApproxEqAbs(shares, expectedShares, STETH_ERROR_ROUNDING, "shares after deposit"); // No perfs, so 1 ether * totalSupply (1e18 + 1e12) / totalAssets (1e18 + 1e12) = 1 ether } /// @notice Test the following scenario: @@ -396,7 +430,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { 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(capManager.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), DEFAULT_AMOUNT); assertEqQueueMetadata(0, 0, 0); // Expected values = 1249998437501 @@ -452,7 +486,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY), 4e6, "last available assets after redeem" ); assertEq(lidoARM.balanceOf(address(this)), 0, "User shares after redeem"); - assertEq(capManager.liquidityProviderCaps(address(this)), 0, "all user cap used"); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0, "all user cap used"); assertEqQueueMetadata(receivedAssets, 0, 1); // 6. collect fees @@ -510,7 +544,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { 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(capManager.liquidityProviderCaps(address(this)), 0); // All the caps are used + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); // All the caps are used assertEqQueueMetadata(receivedAssets, 0, 1); assertEq(receivedAssets, DEFAULT_AMOUNT, "received assets"); } @@ -579,7 +613,7 @@ contract Fork_Concrete_LidoARM_Deposit_Test_ is Fork_Shared_Test_ { ); 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(capManager.liquidityProviderCaps(address(this)), 0, "user cap"); // All the caps are used + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0, "user cap"); // All the caps are used assertEqQueueMetadata(receivedAssets, 0, 1); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol index bef9214..740b87f 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/RequestRedeem.t.sol @@ -9,6 +9,8 @@ import {IERC20} from "contracts/Interfaces.sol"; import {AbstractARM} from "contracts/AbstractARM.sol"; contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { + bool private ac; + ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// @@ -16,6 +18,8 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { super.setUp(); deal(address(weth), address(this), 1_000 ether); + + ac = capManager.accountCapEnabled(); } ////////////////////////////////////////////////////// @@ -36,7 +40,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { 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(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(0, 0, 0); uint256 delay = lidoARM.claimDelay(); @@ -60,7 +64,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { assertEq(lidoARM.lastAvailableAssets(), int256(MIN_TOTAL_SUPPLY)); assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); } /// @notice Test the `requestRedeem` function when there are no profits and the first deposit is made. @@ -79,7 +83,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { 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(capManager.liquidityProviderCaps(address(this)), 0); // Down only + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); // Down only assertEqQueueMetadata(DEFAULT_AMOUNT / 4, 0, 1); uint256 delay = lidoARM.claimDelay(); @@ -107,7 +111,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { 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(capManager.liquidityProviderCaps(address(this)), 0); // Down only + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); // Down only } /// @notice Test the `requestRedeem` function when there are profits and the first deposit is already made. @@ -152,7 +156,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { ); // 1 wei of error assertEq(lidoARM.balanceOf(address(this)), 0); assertEq(lidoARM.totalSupply(), MIN_TOTAL_SUPPLY); - assertEq(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 1); assertEqUserRequest( 0, @@ -204,7 +208,7 @@ contract Fork_Concrete_LidoARM_RequestRedeem_Test_ is Fork_Shared_Test_ { 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(capManager.liquidityProviderCaps(address(this)), 0); + if (ac) assertEq(capManager.liquidityProviderCaps(address(this)), 0); assertEqQueueMetadata(expectedAssetsFromRedeem, 0, 1); assertEqUserRequest( 0, address(this), false, block.timestamp + delay, expectedAssetsFromRedeem, expectedAssetsFromRedeem diff --git a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol index 1a71cfc..9de35b6 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/Setters.t.sol @@ -212,7 +212,7 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { capManager.setAccountCapEnabled(false); } - function test_RevertWhen_CapManager_SetAccountCapEnabled_Because_AlreadySet() public asLidoARMOwner { + function test_RevertWhen_CapManager_SetAccountCapEnabled_Because_AlreadySet() public enableCaps asLidoARMOwner { vm.expectRevert("LPC: Account cap already set"); capManager.setAccountCapEnabled(true); } @@ -220,7 +220,7 @@ contract Fork_Concrete_lidoARM_Setters_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- AccountCapEnabled - PASSING TESTS ////////////////////////////////////////////////////// - function test_CapManager_SetAccountCapEnabled() public asLidoARMOwner { + function test_CapManager_SetAccountCapEnabled() public enableCaps asLidoARMOwner { vm.expectEmit({emitter: address(capManager)}); emit CapManager.AccountCapEnabled(false); capManager.setAccountCapEnabled(false); diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol index 062f137..d1db159 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapExactTokensForTokens.t.sol @@ -10,12 +10,11 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test ////////////////////////////////////////////////////// /// --- CONSTANTS ////////////////////////////////////////////////////// - uint256 private constant MIN_PRICE0 = 980e33; // 0.98 + uint256 private constant MIN_PRICE0 = 998e33; // 0.998 uint256 private constant MAX_PRICE0 = 1_000e33 - 1; // just under 1.00 uint256 private constant MIN_PRICE1 = 1_000e33; // 1.00 uint256 private constant MAX_PRICE1 = 1_020e33; // 1.02 - uint256 private constant MAX_WETH_RESERVE = 1_000_000 ether; // 1M WETH, no limit, but need to be consistent. - uint256 private constant MAX_STETH_RESERVE = 2_000_000 ether; // 2M stETH, limited by wsteth balance of steth. + uint256 private constant INITIAL_BALANCE = 1_000 ether; ////////////////////////////////////////////////////// /// --- SETUP @@ -23,11 +22,14 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test function setUp() public override { super.setUp(); - deal(address(weth), address(this), 1_000 ether); - deal(address(steth), address(this), 1_000 ether); + deal(address(weth), address(this), INITIAL_BALANCE); + deal(address(steth), address(this), INITIAL_BALANCE); - deal(address(weth), address(lidoARM), 1_000 ether); - deal(address(steth), address(lidoARM), 1_000 ether); + deal(address(weth), address(lidoARM), INITIAL_BALANCE); + deal(address(steth), address(lidoARM), INITIAL_BALANCE); + + // We are artificially adding assets so collect the performance fees to reset the fees collected + lidoARM.collectFees(); } ////////////////////////////////////////////////////// @@ -293,92 +295,78 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test assertEq(outputs[1], minAmount); } - /// @notice Test the following scenario: - /// - Trader swap stETH for wETH - /// - wETH ARM Balance > wETH swap out + outstanding withdrawals - /// - wETH ARM Balance < wETH swap out + outstanding withdrawals + accrued fees - function test_SwapExactTokensForTokens_StETH_To_WETH_When_WETHBalanceIsLowerThanSwapOutAndWithdrawalAndFees() + /// @notice If the buy and sell prices are very close together and the stETH transferred into + /// the ARM is truncated, then there should be enough rounding protection against losing total assets. + function test_SwapExactTokensForTokens_Steth_Transfer_Truncated() public - setLiquidityProviderCap(address(this), type(uint256).max) - setTotalAssetsCap(type(uint256).max) + disableCaps + setArmBalances(MIN_TOTAL_SUPPLY, 0) + setPrices(1e36 - 1, 1e36, 1e36) + depositInLidoARM(address(this), DEFAULT_AMOUNT) { - // Deposit to calculate fees - lidoARM.deposit(DEFAULT_AMOUNT); - - // Estimated amount out - uint256 amountIn = 500 ether; - uint256 estimatedAmountOut = amountIn * lidoARM.traderate1() / lidoARM.PRICE_SCALE(); + // The exact amount of stETH to send to the ARM + uint256 amountIn = 3 * DEFAULT_AMOUNT / 4; + // Get minimum amount of WETH to receive + uint256 amountOutMin = amountIn * (1e36 - 1) / 1e36; - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); - uint256 feesAccrued = lidoARM.feesAccrued(); - // Increase lidoWithdrawalQueueAmount - uint256[] memory amounts = new uint256[](1); - amounts[0] = 1 ether + balanceWETHBeforeARM - feesAccrued - estimatedAmountOut; - vm.prank(lidoARM.owner()); - lidoARM.requestLidoWithdrawals(amounts); - uint256 lidoWithdrawalQueueAmount = lidoARM.lidoWithdrawalQueueAmount(); - - // Ensure test scenario is correct - require(balanceWETHBeforeARM > estimatedAmountOut + lidoWithdrawalQueueAmount, "Balance too low"); - require(balanceWETHBeforeARM < estimatedAmountOut + lidoWithdrawalQueueAmount + feesAccrued, "Balance too high"); + deal(address(steth), address(this), amountIn); // State before - uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); - uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); + uint256 totalAssetsBefore = lidoARM.totalAssets(); + + // Expected events + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(lidoARM), address(this), amountOutMin); - // Perfom swap + // Main call lidoARM.swapExactTokensForTokens( steth, // inToken weth, // outToken - amountIn, // amountIn - 0, // amountOutMin + amountIn, + amountOutMin, address(this) // to ); - // State after - assertEq(weth.balanceOf(address(this)), balanceWETHBeforeThis + estimatedAmountOut, "user WETH balance"); - assertEq(weth.balanceOf(address(lidoARM)), balanceWETHBeforeARM - estimatedAmountOut, "ARM WETH balance"); - assertApproxEqAbs( - steth.balanceOf(address(this)), - balanceSTETHBeforeThis - amountIn, - STETH_ERROR_ROUNDING, - "user stETH balance" - ); - assertApproxEqAbs( - steth.balanceOf(address(lidoARM)), - balanceSTETHBeforeARM + amountIn, - STETH_ERROR_ROUNDING, - "ARM stETH balance" - ); + // Assertions + assertGe(lidoARM.totalAssets(), totalAssetsBefore, "total assets after"); } ////////////////////////////////////////////////////// /// --- FUZZING TESTS ////////////////////////////////////////////////////// /// @notice Fuzz test for swapExactTokensForTokens(IERC20,IERC20,uint256,uint256,address), with WETH to stETH. - /// @param amountIn Amount of WETH to swap. Fuzzed between 0 and steth in the ARM. - /// @param stethReserve Amount of stETH in the ARM. Fuzzed between 0 and MAX_STETH_RESERVE. + /// @param amountIn Amount of WETH to swap into the ARM. Fuzzed between 0 and steth in the ARM. + /// @param stethReserveGrowth Amount of stETH has grown in the ARM. Fuzzed between 0 and 1% of the INITIAL_BALANCE. /// @param price Price of the stETH in WETH. Fuzzed between 0.98 and 1. - function test_SwapExactTokensForTokens_Weth_To_Steth(uint256 amountIn, uint256 stethReserve, uint256 price) - public - { - // Use random stETH/WETH sell price between 0.98 and 1, + /// @param collectFees Whether to collect the accrued performance fees before the swap. + function test_SwapExactTokensForTokens_Weth_To_Steth( + uint256 amountIn, + uint256 stethReserveGrowth, + uint256 price, + bool collectFees + ) public { + // Use random stETH/WETH sell price between 1 and 1.02, // the buy price doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE1, MAX_PRICE1); lidoARM.setCrossPrice(1e36); lidoARM.setPrices(MIN_PRICE0, price); // Set random amount of stETH in the ARM - stethReserve = _bound(stethReserve, 0, MAX_STETH_RESERVE); - deal(address(steth), address(lidoARM), stethReserve + (2 * STETH_ERROR_ROUNDING)); + stethReserveGrowth = _bound(stethReserveGrowth, 0, INITIAL_BALANCE / 100); + deal(address(steth), address(lidoARM), INITIAL_BALANCE + stethReserveGrowth); - // Calculate maximum amount of WETH to swap + if (collectFees) { + // Collect and accrued performance fees before the swap + lidoARM.collectFees(); + } + + // Random amount of WETH to swap into the ARM // It is ok to take 100% of the balance of stETH of the ARM as the price is below 1. - amountIn = _bound(amountIn, 0, stethReserve); + amountIn = _bound(amountIn, 0, steth.balanceOf(address(lidoARM))); deal(address(weth), address(this), amountIn); // State before + uint256 totalAssetsBefore = lidoARM.totalAssets(); uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); @@ -405,81 +393,107 @@ contract Fork_Concrete_LidoARM_SwapExactTokensForTokens_Test is Fork_Shared_Test address(this) // to ); - // State after - uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); - uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); - // Assertions - assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn, "user WETH balance"); + assertGe(lidoARM.totalAssets(), totalAssetsBefore, "total assets"); + assertEq(weth.balanceOf(address(this)), balanceWETHBeforeThis - amountIn, "user WETH balance"); assertApproxEqAbs( - balanceSTETHBeforeThis + amountOutMin, balanceSTETHAfterThis, STETH_ERROR_ROUNDING * 2, "user stETH balance" + steth.balanceOf(address(this)), + balanceSTETHBeforeThis + amountOutMin, + STETH_ERROR_ROUNDING * 2, + "user stETH balance" ); - assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM, "ARM WETH balance"); + assertEq(weth.balanceOf(address(lidoARM)), balanceWETHBeforeARM + amountIn, "ARM WETH balance"); assertApproxEqAbs( - balanceSTETHBeforeARM, balanceSTETHAfterARM + amountOutMin, STETH_ERROR_ROUNDING * 2, "ARM stETH balance" + steth.balanceOf(address(lidoARM)), + balanceSTETHBeforeARM - amountOutMin, + STETH_ERROR_ROUNDING * 2, + "ARM stETH balance" ); } /// @notice Fuzz test for swapExactTokensForTokens(IERC20,IERC20,uint256,uint256,address), with stETH to WETH. - /// @param amountIn Amount of stETH to swap. Fuzzed between 0 and weth in the ARM. - /// @param wethReserve Amount of WETH in the ARM. Fuzzed between 0 and MAX_WETH_RESERVE. + /// @param amountIn Amount of stETH to swap into the ARM. Fuzzed between 0 and WETH in the ARM. + /// @param wethReserveGrowth The amount WETH has grown in the ARM. Fuzzed between 0 and 1% of the INITIAL_BALANCE. + /// @param stethReserveGrowth Amount of stETH has grown in the ARM. Fuzzed between 0 and 1% of the INITIAL_BALANCE. /// @param price Price of the stETH in WETH. Fuzzed between 1 and 1.02. - function test_SwapExactTokensForTokens_Steth_To_Weth(uint256 amountIn, uint256 wethReserve, uint256 price) public { + /// @param userStethBalance The amount of stETH the user has before the swap. + /// @param collectFees Whether to collect the accrued performance fees before the swap. + function test_SwapExactTokensForTokens_Steth_To_Weth( + uint256 amountIn, + uint256 wethReserveGrowth, + uint256 stethReserveGrowth, + uint256 price, + uint256 userStethBalance, + bool collectFees + ) public { // Use random stETH/WETH buy price between MIN_PRICE0 and MAX_PRICE0, // the sell price doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE0, MAX_PRICE0); - lidoARM.setCrossPrice(1e36); lidoARM.setPrices(price, MAX_PRICE1); - // Set random amount of WETH in the ARM - wethReserve = _bound(wethReserve, 0, MAX_WETH_RESERVE); - deal(address(weth), address(lidoARM), wethReserve); + // Set random amount of WETH growth in the ARM + wethReserveGrowth = _bound(wethReserveGrowth, 0, INITIAL_BALANCE / 100); + deal(address(weth), address(lidoARM), INITIAL_BALANCE + wethReserveGrowth); + + // Set random amount of stETH growth in the ARM + stethReserveGrowth = _bound(stethReserveGrowth, 0, INITIAL_BALANCE / 100); + deal(address(steth), address(lidoARM), INITIAL_BALANCE + stethReserveGrowth); - // Calculate maximum amount of stETH to swap + if (collectFees) { + // Collect and accrued performance fees before the swap + lidoARM.collectFees(); + } + + // Random amount of stETH to swap into the ARM // As the price is below 1, we can take 100% of the balance of WETH of the ARM. - amountIn = _bound(amountIn, 0, wethReserve); + amountIn = _bound(amountIn, 0, weth.balanceOf(address(lidoARM)) * 1e36 / price); deal(address(steth), address(this), amountIn); + // Fuzz the user's stETH balance + userStethBalance = _bound(userStethBalance, amountIn, amountIn + 1 ether); + deal(address(steth), address(this), userStethBalance); + // State before - uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); - uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); - uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); - uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); + uint256 totalAssetsBefore = lidoARM.totalAssets(); + uint256 userBalanceWETHBefore = weth.balanceOf(address(this)); + uint256 userBalanceSTETHBefore = steth.balanceOf(address(this)); + uint256 armBalanceWETHBefore = weth.balanceOf(address(lidoARM)); + uint256 armBalanceSTETHBefore = steth.balanceOf(address(lidoARM)); - // Get minimum amount of WETH to receive - uint256 minAmount = amountIn * price / 1e36; + // Get minimum amount of WETH swapped out of the ARM + uint256 amountOutMin = amountIn * price / 1e36; // Expected events vm.expectEmit({emitter: address(steth)}); emit IERC20.Transfer(address(this), address(lidoARM), amountIn); vm.expectEmit({emitter: address(weth)}); - emit IERC20.Transfer(address(lidoARM), address(this), minAmount); + emit IERC20.Transfer(address(lidoARM), address(this), amountOutMin); // Main call lidoARM.swapExactTokensForTokens( steth, // inToken weth, // outToken - amountIn, // amountIn - minAmount, // amountOutMin + amountIn, + amountOutMin, address(this) // to ); - // State after - uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); - uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); - // Assertions - assertEq(balanceWETHBeforeThis + minAmount, balanceWETHAfterThis, "user WETH balance"); + // TODO change the ARM so it doesn't lose 1 wei of assets on any swaps + assertGe(lidoARM.totalAssets() + 1, totalAssetsBefore, "total assets"); + assertEq(weth.balanceOf(address(this)), userBalanceWETHBefore + amountOutMin, "user WETH balance"); assertApproxEqAbs( - balanceSTETHBeforeThis, balanceSTETHAfterThis + amountIn, STETH_ERROR_ROUNDING, "user stETH balance" + steth.balanceOf(address(this)), + userBalanceSTETHBefore - amountIn, + STETH_ERROR_ROUNDING * 2, + "user stETH balance" ); - assertEq(balanceWETHBeforeARM, balanceWETHAfterARM + minAmount, "ARM WETH balance"); + assertEq(weth.balanceOf(address(lidoARM)), armBalanceWETHBefore - amountOutMin, "ARM WETH balance"); assertApproxEqAbs( - balanceSTETHBeforeARM + amountIn, balanceSTETHAfterARM, STETH_ERROR_ROUNDING, "ARM stETH balance" + steth.balanceOf(address(lidoARM)), + armBalanceSTETHBefore + amountIn, + STETH_ERROR_ROUNDING * 2, + "ARM stETH balance" ); } } diff --git a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol index 230b676..8a86f17 100644 --- a/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol +++ b/test/fork/LidoFixedPriceMultiLpARM/SwapTokensForExactTokens.t.sol @@ -14,8 +14,7 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test uint256 private constant MAX_PRICE0 = 1_000e33 - 1; // just under 1.00 uint256 private constant MIN_PRICE1 = 1_000e33; // 1.00 uint256 private constant MAX_PRICE1 = 1_020e33; // 1.02 - uint256 private constant MAX_WETH_RESERVE = 1_000_000 ether; // 1M WETH, no limit, but need to be consistent. - uint256 private constant MAX_STETH_RESERVE = 2_000_000 ether; // 2M stETH, limited by wsteth balance of steth. + uint256 private constant INITIAL_BALANCE = 1_000 ether; ////////////////////////////////////////////////////// /// --- SETUP @@ -23,11 +22,14 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test function setUp() public override { super.setUp(); - deal(address(weth), address(this), 1_000 ether); - deal(address(steth), address(this), 1_000 ether); + deal(address(weth), address(this), INITIAL_BALANCE); + deal(address(steth), address(this), INITIAL_BALANCE); - deal(address(weth), address(lidoARM), 1_000 ether); - deal(address(steth), address(lidoARM), 1_000 ether); + deal(address(weth), address(lidoARM), INITIAL_BALANCE); + deal(address(steth), address(lidoARM), INITIAL_BALANCE); + + // We are artificially adding assets so collect the performance fees to reset the fees collected + lidoARM.collectFees(); } ////////////////////////////////////////////////////// @@ -191,7 +193,7 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test // Get maximum amount of WETH to send to the ARM uint256 traderates0 = lidoARM.traderate0(); - uint256 amountIn = (amountOut * 1e36 / traderates0) + 1; + uint256 amountIn = (amountOut * 1e36 / traderates0) + 3; // Expected events: Already checked in fuzz tests @@ -238,7 +240,7 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test // Get maximum amount of stETH to send to the ARM uint256 traderates1 = lidoARM.traderate1(); - uint256 amountIn = (amountOut * 1e36 / traderates1) + 1; + uint256 amountIn = (amountOut * 1e36 / traderates1) + 3; // Expected events: Already checked in fuzz tests @@ -271,45 +273,97 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test assertEq(outputs[1], amountOut, "Amount out"); } + /// @notice If the buy and sell prices are very close together and the stETH transferred into + /// the ARM is truncated, then there should be enough rounding protection against losing total assets. + function test_SwapTokensForExactTokens_Steth_Transfer_Truncated() + public + disableCaps + setArmBalances(MIN_TOTAL_SUPPLY, 0) + setPrices(1e36 - 1, 1e36, 1e36) + depositInLidoARM(address(this), DEFAULT_AMOUNT) + { + // The exact amount of WETH to receive + uint256 amountOut = DEFAULT_AMOUNT; + // The max amount of stETH to send + uint256 amountInMax = amountOut + 3; + deal(address(steth), address(this), amountInMax); // Deal more as AmountIn is greater than AmountOut + + // State before + uint256 totalAssetsBefore = lidoARM.totalAssets(); + + // Expected events + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(lidoARM), address(this), amountOut); + + // Main call + lidoARM.swapTokensForExactTokens( + steth, // inToken + weth, // outToken + amountOut, // amountOut + amountInMax, // amountInMax + address(this) // to + ); + + // Assertions + assertGe(lidoARM.totalAssets(), totalAssetsBefore, "total assets after"); + } + ////////////////////////////////////////////////////// /// --- FUZZING TESTS ////////////////////////////////////////////////////// /// @notice Fuzz test for swapTokensForExactTokens(IERC20,IERC20,uint256,uint256,address), with WETH to stETH. - /// @param amountOut Amount of WETH to swap. Fuzzed between 0 and steth in the ARM. - /// @param stethReserve Amount of stETH in the ARM. Fuzzed between 0 and MAX_STETH_RESERVE. - /// @param price Price of the stETH in WETH. Fuzzed between 0.98 and 1. - function test_SwapTokensForExactTokens_Weth_To_Steth(uint256 amountOut, uint256 stethReserve, uint256 price) - public - { - // Use random sell price between 0.98 and 1 for the stETH/WETH price, + /// @param amountOut Exact amount of stETH to swap out of the ARM. Fuzzed between 0 and stETH in the ARM. + /// @param wethReserveGrowth The amount WETH has grown in the ARM. Fuzzed between 0 and 1% of the INITIAL_BALANCE. + /// @param stethReserveGrowth Amount of stETH has grown in the ARM. Fuzzed between 0 and 1% of the INITIAL_BALANCE. + /// @param price Sell price of the stETH in WETH (stETH/WETH). Fuzzed between 1 and 1.02. + /// @param collectFees Whether to collect the accrued performance fees before the swap. + function test_SwapTokensForExactTokens_Weth_To_Steth( + uint256 amountOut, + uint256 wethReserveGrowth, + uint256 stethReserveGrowth, + uint256 price, + bool collectFees + ) public { + // Use random sell price between 1 and 1.02 for the stETH/WETH price, // The buy price doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE1, MAX_PRICE1); lidoARM.setPrices(MIN_PRICE0, price); + // Set random amount of WETH in the ARM + wethReserveGrowth = _bound(wethReserveGrowth, 0, INITIAL_BALANCE / 100); + deal(address(weth), address(lidoARM), INITIAL_BALANCE + wethReserveGrowth); + // Set random amount of stETH in the ARM - stethReserve = _bound(stethReserve, 0, MAX_STETH_RESERVE); - deal(address(steth), address(lidoARM), stethReserve); + stethReserveGrowth = _bound(stethReserveGrowth, 0, INITIAL_BALANCE / 100); + deal(address(steth), address(lidoARM), INITIAL_BALANCE + stethReserveGrowth); + + if (collectFees) { + // Collect and accrued performance fees before the swap + lidoARM.collectFees(); + } - // Calculate maximum amount of WETH to swap - // It is ok to take 100% of the balance of stETH of the ARM as the price is below 1. - amountOut = _bound(amountOut, 0, stethReserve); - deal(address(weth), address(this), amountOut * 2 + 1); // // Deal more as AmountIn is greater than AmountOut + // Calculate the amount of stETH to swap out of the ARM + amountOut = _bound(amountOut, 0, steth.balanceOf(address(lidoARM))); + + // Get the maximum amount of WETH to swap into the ARM + // weth = steth * stETH/WETH price + uint256 amountIn = (amountOut * price / 1e36) + 3; + + deal(address(weth), address(this), amountIn); // State before + uint256 totalAssetsBefore = lidoARM.totalAssets(); uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); - // Get minimum amount of STETH to receive - // weth = steth * stETH/WETH price - uint256 amountIn = (amountOut * price / 1e36) + 1; - // Expected events vm.expectEmit({emitter: address(weth)}); emit IERC20.Transfer(address(this), address(lidoARM), amountIn); vm.expectEmit({emitter: address(steth)}); emit IERC20.Transfer(address(lidoARM), address(this), amountOut); + // Main call lidoARM.swapTokensForExactTokens( weth, // inToken @@ -319,54 +373,77 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test address(this) // to ); - // State after - uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); - uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); - // Assertions - assertEq(balanceWETHBeforeThis, balanceWETHAfterThis + amountIn, "WETH user balance"); + assertGe(lidoARM.totalAssets(), totalAssetsBefore, "total assets after"); + assertEq(weth.balanceOf(address(this)), balanceWETHBeforeThis - amountIn, "WETH user balance"); assertApproxEqAbs( - balanceSTETHBeforeThis + amountOut, balanceSTETHAfterThis, STETH_ERROR_ROUNDING, "STETH user balance" + steth.balanceOf(address(this)), + balanceSTETHBeforeThis + amountOut, + STETH_ERROR_ROUNDING, + "STETH user balance" ); - assertEq(balanceWETHBeforeARM + amountIn, balanceWETHAfterARM, "WETH ARM balance"); + assertEq(weth.balanceOf(address(lidoARM)), balanceWETHBeforeARM + amountIn, "WETH ARM balance"); assertApproxEqAbs( - balanceSTETHBeforeARM, balanceSTETHAfterARM + amountOut, STETH_ERROR_ROUNDING, "STETH ARM balance" + steth.balanceOf(address(lidoARM)), + balanceSTETHBeforeARM - amountOut, + STETH_ERROR_ROUNDING, + "STETH ARM balance" ); } /// @notice Fuzz test for swapTokensForExactTokens(IERC20,IERC20,uint256,uint256,address), with stETH to WETH. - /// @param amountOut Amount of stETH to swap. Fuzzed between 0 and weth in the ARM. - /// @param wethReserve Amount of WETH in the ARM. Fuzzed between 0 and MAX_WETH_RESERVE. - /// @param price Price of the stETH in WETH. Fuzzed between 1 and 1.02. - function test_SwapTokensForExactTokens_Steth_To_Weth(uint256 amountOut, uint256 wethReserve, uint256 price) - public - { + /// @param amountOut Exact amount of WETH to swap out of the ARM. Fuzzed between 0 and WETH in the ARM. + /// @param wethReserveGrowth The amount WETH has grown in the ARM. Fuzzed between 0 and 1% of the INITIAL_BALANCE. + /// @param stethReserveGrowth Amount of stETH has grown in the ARM. Fuzzed between 0 and 1% of the INITIAL_BALANCE. + /// @param price Buy price of the stETH in WETH (stETH/WETH). Fuzzed between 0.998 and 1.02. + /// @param userStethBalance The amount of stETH the user has before the swap. + /// @param collectFees Whether to collect the accrued performance fees before the swap. + function test_SwapTokensForExactTokens_Steth_To_Weth( + uint256 amountOut, + uint256 wethReserveGrowth, + uint256 stethReserveGrowth, + uint256 price, + uint256 userStethBalance, + bool collectFees + ) public { + lidoARM.collectFees(); + // Use random stETH/WETH buy price between 0.98 and 1, // sell price doesn't matter as it is not used in this test. price = _bound(price, MIN_PRICE0, MAX_PRICE0); lidoARM.setPrices(price, MAX_PRICE1); - // Set random amount of WETH in the ARM - wethReserve = _bound(wethReserve, 0, MAX_WETH_RESERVE); - deal(address(weth), address(lidoARM), wethReserve); + // Set random amount of WETH growth in the ARM + wethReserveGrowth = _bound(wethReserveGrowth, 0, INITIAL_BALANCE / 100); + deal(address(weth), address(lidoARM), INITIAL_BALANCE + wethReserveGrowth); + + // Set random amount of stETH growth in the ARM + stethReserveGrowth = _bound(stethReserveGrowth, 0, INITIAL_BALANCE / 100); + deal(address(steth), address(lidoARM), INITIAL_BALANCE + stethReserveGrowth); - // Calculate maximum amount of stETH to swap - // As the price is below 1, we can take 100% of the balance of WETH of the ARM. - amountOut = _bound(amountOut, 0, wethReserve); - deal(address(steth), address(this), amountOut * 2 + 1); // Deal more as AmountIn is greater than AmountOut + if (collectFees) { + // Collect and accrued performance fees before the swap + lidoARM.collectFees(); + } + + // Calculate the amount of WETH to swap out of the ARM + // Can take up to 100% of the WETH in the ARM even if there is some for the performance fee. + amountOut = _bound(amountOut, 0, weth.balanceOf(address(lidoARM))); + // Get the maximum amount of stETH to swap into of the ARM + // stETH = WETH / stETH/WETH price + uint256 amountIn = (amountOut * 1e36 / price) + 3; + + // Fuzz the user's stETH balance + userStethBalance = _bound(userStethBalance, amountIn, amountIn + 1 ether); + deal(address(steth), address(this), userStethBalance); // State before + uint256 totalAssetsBefore = lidoARM.totalAssets(); uint256 balanceWETHBeforeThis = weth.balanceOf(address(this)); uint256 balanceSTETHBeforeThis = steth.balanceOf(address(this)); uint256 balanceWETHBeforeARM = weth.balanceOf(address(lidoARM)); uint256 balanceSTETHBeforeARM = steth.balanceOf(address(lidoARM)); - // Get minimum amount of WETH to receive - // stETH = WETH / stETH/WETH price - uint256 amountIn = (amountOut * 1e36 / price); - // Expected events // TODO hard to check the exact amount of stETH due to rounding // vm.expectEmit({emitter: address(steth)}); @@ -383,20 +460,21 @@ contract Fork_Concrete_LidoARM_SwapTokensForExactTokens_Test is Fork_Shared_Test address(this) // to ); - // State after - uint256 balanceWETHAfterThis = weth.balanceOf(address(this)); - uint256 balanceSTETHAfterThis = steth.balanceOf(address(this)); - uint256 balanceWETHAfterARM = weth.balanceOf(address(lidoARM)); - uint256 balanceSTETHAfterARM = steth.balanceOf(address(lidoARM)); - // Assertions - assertEq(balanceWETHBeforeThis + amountOut, balanceWETHAfterThis, "WETH user balance"); + assertGe(lidoARM.totalAssets(), totalAssetsBefore, "total assets after"); + assertEq(weth.balanceOf(address(this)), balanceWETHBeforeThis + amountOut, "WETH user balance"); assertApproxEqAbs( - balanceSTETHBeforeThis, balanceSTETHAfterThis + amountIn, STETH_ERROR_ROUNDING, "STETH user balance" + steth.balanceOf(address(this)), + balanceSTETHBeforeThis - amountIn, + STETH_ERROR_ROUNDING, + "STETH user balance" ); - assertEq(balanceWETHBeforeARM, balanceWETHAfterARM + amountOut, "WETH ARM balance"); + assertEq(weth.balanceOf(address(lidoARM)), balanceWETHBeforeARM - amountOut, "WETH ARM balance"); assertApproxEqAbs( - balanceSTETHBeforeARM + amountIn, balanceSTETHAfterARM, STETH_ERROR_ROUNDING, "STETH ARM balance" + steth.balanceOf(address(lidoARM)), + balanceSTETHBeforeARM + amountIn, + STETH_ERROR_ROUNDING, + "STETH ARM balance" ); } } diff --git a/test/fork/Zapper/Deposit.t.sol b/test/fork/Zapper/Deposit.t.sol index eb54f1c..f2f1522 100644 --- a/test/fork/Zapper/Deposit.t.sol +++ b/test/fork/Zapper/Deposit.t.sol @@ -15,7 +15,7 @@ contract Fork_Concrete_ZapperLidoARM_Deposit_Test_ is Fork_Shared_Test_ { vm.deal(address(this), DEFAULT_AMOUNT); } - function test_Deposit_ViaFunction() public { + function test_Deposit_ViaFunction() public enableCaps { assertEq(lidoARM.balanceOf(address(this)), 0); uint256 expectedShares = lidoARM.previewDeposit(DEFAULT_AMOUNT); uint256 capBefore = capManager.liquidityProviderCaps(address(this)); @@ -32,7 +32,7 @@ contract Fork_Concrete_ZapperLidoARM_Deposit_Test_ is Fork_Shared_Test_ { assertEq(capManager.liquidityProviderCaps(address(this)), capBefore - DEFAULT_AMOUNT); } - function test_Deposit_ViaCall() public { + function test_Deposit_ViaCall() public enableCaps { assertEq(lidoARM.balanceOf(address(this)), 0); uint256 expectedShares = lidoARM.previewDeposit(DEFAULT_AMOUNT); uint256 capBefore = capManager.liquidityProviderCaps(address(this)); diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index 5081900..f55933d 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -71,6 +71,38 @@ abstract contract Modifiers is Helpers { _; } + /// @notice disable both the total assets and liquidity provider caps + modifier disableCaps() { + lidoARM.setCapManager(address(0)); + _; + } + + /// @notice Enable the total assets cap on the CapManager contract. + modifier enableCaps() { + require(address(capManager) != address(0), "CapManager not set"); + vm.prank(lidoARM.owner()); + lidoARM.setCapManager(address(capManager)); + + if (!capManager.accountCapEnabled()) { + vm.prank(capManager.owner()); + capManager.setAccountCapEnabled(true); + } + _; + } + + /// @notice Set the stETH/WETH swap prices on the LidoARM contract. + modifier setPrices(uint256 buyPrice, uint256 crossPrice, uint256 sellPrice) { + lidoARM.setCrossPrice(crossPrice); + lidoARM.setPrices(buyPrice, sellPrice); + _; + } + + modifier setArmBalances(uint256 wethBalance, uint256 stethBalance) { + deal(address(weth), address(lidoARM), wethBalance); + deal(address(steth), address(lidoARM), stethBalance); + _; + } + /// @notice Set the total assets cap on the CapManager contract. modifier setTotalAssetsCap(uint256 cap) { capManager.setTotalAssetsCap(uint248(cap)); diff --git a/test/smoke/LidoARMSmokeTest.t.sol b/test/smoke/LidoARMSmokeTest.t.sol index 83b25e0..97f8841 100644 --- a/test/smoke/LidoARMSmokeTest.t.sol +++ b/test/smoke/LidoARMSmokeTest.t.sol @@ -54,7 +54,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { assertEq(lidoARM.claimDelay(), 10 minutes, "claim delay"); assertEq(lidoARM.crossPrice(), 0.9998e36, "cross price"); - assertEq(capManager.accountCapEnabled(), true, "account cap enabled"); + assertEq(capManager.accountCapEnabled(), false, "account cap enabled"); assertEq(capManager.operator(), Mainnet.ARM_RELAYER, "Operator"); assertEq(capManager.arm(), address(lidoARM), "arm"); } @@ -142,7 +142,7 @@ contract Fork_LidoARM_Smoke_Test is AbstractSmokeTest { deal(address(weth), address(lidoARM), 1000 ether); // _dealWETH(address(lidoARM), 1000 ether); - expectedIn = amountOut * 1e36 / price; + expectedIn = amountOut * 1e36 / price + 3; vm.prank(Mainnet.ARM_RELAYER); uint256 sellPrice = price < 9996e32 ? 9998e32 : price + 2e32;