diff --git a/src/helpers/AddressBook.sol b/src/helpers/AddressBook.sol index 0c5e604..e0e815e 100644 --- a/src/helpers/AddressBook.sol +++ b/src/helpers/AddressBook.sol @@ -19,6 +19,7 @@ address constant RETH_MAINNET = 0xae78736Cd615f374D3085123A210448E74Fc6393; address constant ETHX_MAINNET = 0xA35b1B31Ce002FBF2058D22F30f95D405200A15b; address constant GHO_MAINNET = 0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f; address constant COMP_MAINNET = 0xc00e94Cb662C3520282E6f5717214004A7f26888; +address constant CRV3POOL_MAINNET = 0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490; // Main protocols contracts address constant CONVEX_BOOSTER_MAINNET = 0xF403C135812408BFbE8713b5A23a04b3D48AAE31; @@ -65,6 +66,8 @@ address constant COMPOUND_USDT_V3_COMMET_MAINNET = 0x3Afdc9BCA9213A35503b077a607 address constant COMPOUND_USDT_V3_REWARDS_MAINNET = 0x1B0e765F6224C21223AeA2af16c1C46E38885a40; address constant CURVE_ALTETH_FRXETH_MAINNET = 0xB657B895B265C38c53FFF00166cF7F6A3C70587d; address constant BEEFY_ALTETH_FRXETH_MAINNET = 0x26F44884D9744C0EDaB6283930DF325c200020A3; +address constant CURVE_THUSD_DAI_USDC_USDT_MAINNET = 0x91553BAD9Fbc8bD69Ff5d5678Cbf7D514d00De0b; +address constant BEEFY_THUSD_DAI_USDC_USDT_MAINNET = 0x3f5e39bf80798cB94846B48f1c635001a2E43066; //////////////////////////////// POLYGON //////////////////////////////// // Tokens diff --git a/src/interfaces/ICurve.sol b/src/interfaces/ICurve.sol index 6113f75..69f8cc0 100644 --- a/src/interfaces/ICurve.sol +++ b/src/interfaces/ICurve.sol @@ -99,6 +99,16 @@ interface ICurveTriPool { function get_dy(int128 i, int128 j, uint256 dx) external view returns (uint256); function exchange(int128 i, int128 j, uint256 _dx, uint256 _min_dy) external; + + function get_virtual_price() external view returns (uint256); + + function add_liquidity(uint256[3] calldata amounts, uint256 min_mint_amount) external; + + function remove_liquidity_one_coin(uint256 token_amount, int128 i, uint256 min_amount) external; + + function calc_token_amount(uint256[3] memory amounts, bool deposit) external view returns (uint256); + + function calc_withdraw_one_coin(uint256 token_amount, int128 i) external view returns (uint256); } interface ICurveAtriCryptoZapper { diff --git a/src/strategies/base/BaseBeefyCurveMetaPoolStrategy.sol b/src/strategies/base/BaseBeefyCurveMetaPoolStrategy.sol new file mode 100644 index 0000000..3e82a71 --- /dev/null +++ b/src/strategies/base/BaseBeefyCurveMetaPoolStrategy.sol @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.19; + +import { ERC20 } from "solady/tokens/ERC20.sol"; +import { FixedPointMathLib as Math } from "solady/utils/FixedPointMathLib.sol"; +import { CRV3POOL_MAINNET } from "src/helpers/AddressBook.sol"; +import { IBeefyVault } from "src/interfaces/IBeefyVault.sol"; +import { ICurveLpPool, ICurveTriPool } from "src/interfaces/ICurve.sol"; +import { BaseBeefyStrategy, IMaxApyVault, SafeTransferLib } from "src/strategies/base/BaseBeefyStrategy.sol"; + +/// @title BaseBeefyCurveMetaPoolStrategy +/// @author Adapted from https://github.com/Grandthrax/yearn-steth-acc/blob/master/contracts/strategies.sol +/// @notice `BaseBeefyCurveMetaPoolStrategy` supplies an underlying token into a generic Beefy Vault, +/// earning the Beefy Vault's yield +contract BaseBeefyCurveMetaPoolStrategy is BaseBeefyStrategy { + using SafeTransferLib for address; + + //////////////////////////////////////////////////////////////// + /// STRATEGY GLOBAL STATE VARIABLES /// + //////////////////////////////////////////////////////////////// + + /*==================CURVE-RELATED STORAGE VARIABLES==================*/ + /// @notice Curve Meta pool for this Strategy + ICurveLpPool public curveLpPool; + /// @notice Curve 3pool for this Strategy - DAI/USDC/USDT Pool + ICurveTriPool public curveTriPool; + + address public crvTriPoolToken; + + //////////////////////////////////////////////////////////////// + /// INITIALIZATION /// + //////////////////////////////////////////////////////////////// + constructor() initializer { } + + /// @notice Initialize the Strategy + /// @param _vault The address of the MaxApy Vault associated to the strategy + /// @param _keepers The addresses of the keepers to be added as valid keepers to the strategy + /// @param _strategyName the name of the strategy + /// @param _curveLpPool The address of the strategy's main Curve pool + /// @param _beefyVault The address of the strategy's Beefy vault + /// @param _curveTriPool The address of the strategy's Curve Tripool + /// @param _crvTriPoolToken The address of the Curve Tripool's token + function initialize( + IMaxApyVault _vault, + address[] calldata _keepers, + bytes32 _strategyName, + address _strategist, + ICurveLpPool _curveLpPool, + IBeefyVault _beefyVault, + ICurveTriPool _curveTriPool, + address _crvTriPoolToken + ) + public + virtual + initializer + { + super.initialize(_vault, _keepers, _strategyName, _strategist, _beefyVault); + + // Curve init + curveLpPool = _curveLpPool; + curveTriPool = _curveTriPool; + crvTriPoolToken = _crvTriPoolToken; + + underlyingAsset.safeApprove(address(curveTriPool), type(uint256).max); + crvTriPoolToken.safeApprove(address(curveLpPool), type(uint256).max); + address(curveLpPool).safeApprove(address(beefyVault), type(uint256).max); + /// min single trade by default + minSingleTrade = 10e6; + /// Unlimited max single trade by default + maxSingleTrade = 100_000e6; + } + + //////////////////////////////////////////////////////////////// + /// INTERNAL CORE FUNCTIONS /// + //////////////////////////////////////////////////////////////// + /// @notice Invests `amount` of underlying into the Beefy vault + /// @dev + /// @param amount The amount of underlying to be deposited in the pool + /// @param minOutputAfterInvestment minimum expected output after `_invest()` (designated in Curve LP tokens) + /// @return The amount of tokens received, in terms of underlying + function _invest(uint256 amount, uint256 minOutputAfterInvestment) internal virtual override returns (uint256) { + // Don't do anything if amount to invest is 0 + if (amount == 0) return 0; + + uint256 underlyingBalance = _underlyingBalance(); + + assembly ("memory-safe") { + if gt(amount, underlyingBalance) { + // throw the `NotEnoughFundsToInvest` error + mstore(0x00, 0xb2ff68ae) + revert(0x1c, 0x04) + } + } + + amount = Math.min(maxSingleTrade, amount); + + uint256[3] memory amountsUsdc; + amountsUsdc[1] = amount; + + uint256 _before = ERC20(crvTriPoolToken).balanceOf(address(this)); + // uint256 _before = ERC20(_3crvToken).balanceOf(address(this)); + // Add liquidity to the curveTriPool in underlying token [coin1 -> usdc] + curveTriPool.add_liquidity(amountsUsdc, 0); + uint256 _after = ERC20(crvTriPoolToken).balanceOf(address(this)); + // uint256 _after = ERC20(_3crvToken).balanceOf(address(this)); + + uint256 _3crvTokenReceived; + assembly ("memory-safe") { + _3crvTokenReceived := sub(_after, _before) + } + + uint256[2] memory amounts; + amounts[1] = _3crvTokenReceived; + // Add liquidity to the curve Metapool in 3crv token [coin1 -> 3crv] + uint256 lpReceived = curveLpPool.add_liquidity(amounts, 0, address(this)); + + _before = beefyVault.balanceOf(address(this)); + + // Deposit Curve LP tokens to Beefy vault + beefyVault.deposit(lpReceived); + + _after = beefyVault.balanceOf(address(this)); + uint256 shares; + + assembly ("memory-safe") { + shares := sub(_after, _before) + if lt(shares, minOutputAfterInvestment) { + // throw the `MinOutputAmountNotReached` error + mstore(0x00, 0xf7c67a48) + revert(0x1c, 0x04) + } + } + emit Invested(address(this), amount); + + return _shareValue(shares); + } + + /// @dev care should be taken, as the `amount` parameter is not in terms of underlying, + /// but in terms of Beefy's moo tokens + /// Note that if minimum withdrawal amount is not reached, funds will not be divested, and this + /// will be accounted as a loss later. + /// @return amountDivested the total amount divested, in terms of underlying asset + function _divest(uint256 amount) internal virtual override returns (uint256 amountDivested) { + if (amount == 0) return 0; + + uint256 _before = beefyVault.want().balanceOf(address(this)); + + // Withdraw from Beefy and unwrap directly to Curve LP tokens + beefyVault.withdraw(amount); + + uint256 _after = beefyVault.want().balanceOf(address(this)); + + uint256 lptokens = _after - _before; + + // Remove liquidity and obtain usdce + uint256 _3crvTokenReceived = curveLpPool.remove_liquidity_one_coin( + lptokens, + 1, + //usdce + 0, + address(this) + ); + + _before = underlyingAsset.balanceOf(address(this)); + + curveTriPool.remove_liquidity_one_coin( + _3crvTokenReceived, + 1, + //usdce + 0 + ); + + amountDivested = underlyingAsset.balanceOf(address(this)) - _before; + } + + ///////////////////////////////////////////////////////////////// + /// VIEW FUNCTIONS /// + //////////////////////////////////////////////////////////////// + + /// @notice This function is meant to be called from the vault + /// @dev calculates the estimated real output of a withdrawal(including losses) for a @param requestedAmount + /// for the vault to be able to provide an accurate amount when calling `previewRedeem` + /// @return liquidatedAmount output in assets + function previewLiquidate(uint256 requestedAmount) + public + view + virtual + override + returns (uint256 liquidatedAmount) + { + uint256 loss; + uint256 underlyingBalance = _underlyingBalance(); + // If underlying balance currently held by strategy is not enough to cover + // the requested amount, we divest from the beefy Vault + if (underlyingBalance < requestedAmount) { + uint256 amountToWithdraw; + unchecked { + amountToWithdraw = requestedAmount - underlyingBalance; + } + uint256 shares = _sharesForAmount(amountToWithdraw); + uint256 withdrawn = _shareValue(shares); + if (withdrawn < amountToWithdraw) loss = amountToWithdraw - withdrawn; + } + liquidatedAmount = (requestedAmount - loss); + } + + //////////////////////////////////////////////////////////////// + /// INTERNAL VIEW FUNCTIONS /// + //////////////////////////////////////////////////////////////// + + /// @notice Determines the current value of `shares`. + /// @return _assets the estimated amount of underlying computed from shares `shares` + function _shareValue(uint256 shares) internal view virtual override returns (uint256 _assets) { + uint256 expectedCurveLp = shares * beefyVault.balance() / beefyVault.totalSupply(); + if (expectedCurveLp > 0) { + uint256 expected3Crv = curveLpPool.calc_withdraw_one_coin(expectedCurveLp, 1); + if (expected3Crv > 0) { + _assets = curveTriPool.calc_withdraw_one_coin(expected3Crv, 1); + } + } + } + + /// @notice Determines how many shares depositor of `amount` of underlying would receive. + /// @return shares the estimated amount of shares computed in exchange for underlying `amount` + function _sharesForAmount(uint256 amount) internal view virtual override returns (uint256 shares) { + uint256[3] memory amounts; + amounts[1] = amount; + + uint256 lpTokenAmount = curveTriPool.calc_token_amount(amounts, true); + + uint256[2] memory _amounts; + _amounts[1] = lpTokenAmount; + + lpTokenAmount = curveLpPool.calc_token_amount(_amounts, true); + shares = super._sharesForAmount(lpTokenAmount); + } + + /// @notice Returns the estimated price for the strategy's curve's LP token + /// @return returns the estimated lp token price + function _lpPrice() internal view returns (uint256) { + return ((curveLpPool.get_virtual_price() * curveLpPool.get_dy(0, 1, 1 ether)) / 1 ether); + } + + /// @notice Returns the estimated price for the strategy's curve's Tri pool LP token + /// @return returns the estimated lp token price + function _lpTriPoolPrice() internal view returns (uint256) { + return ( + ( + curveTriPool.get_virtual_price() + * Math.min(curveTriPool.get_dy(0, 1, 1 ether), curveTriPool.get_dy(1, 0, 1e6)) + ) / 1 ether + ); + } +} diff --git a/src/strategies/mainnet/USDC/beefy/BeefythUSDDAIUSDCUSDTStrategy.sol b/src/strategies/mainnet/USDC/beefy/BeefythUSDDAIUSDCUSDTStrategy.sol new file mode 100644 index 0000000..d6c7473 --- /dev/null +++ b/src/strategies/mainnet/USDC/beefy/BeefythUSDDAIUSDCUSDTStrategy.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.19; + +import { FixedPointMathLib as Math } from "solady/utils/FixedPointMathLib.sol"; + +import { BaseBeefyCurveMetaPoolStrategy } from "src/strategies/base/BaseBeefyCurveMetaPoolStrategy.sol"; +import { BaseBeefyStrategy, IMaxApyVault, SafeTransferLib } from "src/strategies/base/BaseBeefyStrategy.sol"; + + +/// @title BeefythUSDDAIUSDCUSDTStrategy +/// @author Adapted from https://github.com/Grandthrax/yearn-steth-acc/blob/master/contracts/strategies.sol +/// @notice `BeefythUSDDAIUSDCUSDTStrategy` supplies an underlying token into a generic Beefy Vault, +/// earning the Beefy Vault's yield +contract BeefythUSDDAIUSDCUSDTStrategy is BaseBeefyCurveMetaPoolStrategy { } diff --git a/src/strategies/mainnet/WETH/beefy/BeefyaltETHfrxETHStrategy.sol b/src/strategies/mainnet/WETH/beefy/BeefyaltETHfrxETHStrategy.sol index b52c74a..83f83fa 100644 --- a/src/strategies/mainnet/WETH/beefy/BeefyaltETHfrxETHStrategy.sol +++ b/src/strategies/mainnet/WETH/beefy/BeefyaltETHfrxETHStrategy.sol @@ -113,7 +113,6 @@ contract BeefyaltETHfrxETHStrategy is BaseBeefyCurveStrategy { // Add liquidity to the altETH<>frxETH pool in frxETH [coin1 -> frxETH] lpReceived = curveLpPool.add_liquidity(amounts, 0, address(this)); - uint256 _before = beefyVault.balanceOf(address(this)); // Deposit Curve LP tokens to Beefy vault diff --git a/test/fuzz/MaxApyTestMainnetUSDC.integration.fuzz.t.sol b/test/fuzz/MaxApyTestMainnetUSDC.integration.fuzz.t.sol index 0390d75..2be6e48 100644 --- a/test/fuzz/MaxApyTestMainnetUSDC.integration.fuzz.t.sol +++ b/test/fuzz/MaxApyTestMainnetUSDC.integration.fuzz.t.sol @@ -22,6 +22,9 @@ import { StrategyEvents } from "test/helpers/StrategyEvents.sol"; // Compound v3 import { CompoundV3USDTStrategyWrapper } from "../mock/CompoundV3USDTStrategyWrapper.sol"; +// Beefy +import { BeefythUSDDAIUSDCUSDTStrategyWrapper } from "../mock/BeefythUSDDAIUSDCUSDTStrategyWrapper.sol"; + // Vault fuzzer import { MaxApyVaultFuzzer } from "./fuzzers/MaxApyVaultFuzzer.t.sol"; import { StrategyFuzzer } from "./fuzzers/StrategyFuzzer.t.sol"; @@ -52,6 +55,7 @@ contract MaxApyIntegrationTestMainnet is BaseTest, StrategyEvents { //////////////////////////////////////////////////////////////// ICompoundV3StrategyWrapper public strategy1; // yearn weth + IStrategyWrapper public strategy2; IMaxApyVault public vault; ITransparentUpgradeableProxy public proxy; @@ -77,6 +81,11 @@ contract MaxApyIntegrationTestMainnet is BaseTest, StrategyEvents { address[] memory keepers = new address[](1); keepers[0] = users.keeper; + ///////////////////////////////////////////////////////////////////////// + /// STRATEGIES /// + ///////////////////////////////////////////////////////////////////////// + /// Deploy transparent upgradeable proxy admin + // Deploy strategy1 CompoundV3USDTStrategyWrapper implementation1 = new CompoundV3USDTStrategyWrapper(); TransparentUpgradeableProxy _proxy = new TransparentUpgradeableProxy( @@ -94,18 +103,37 @@ contract MaxApyIntegrationTestMainnet is BaseTest, StrategyEvents { UNISWAP_V3_ROUTER_MAINNET ) ); - proxy = ITransparentUpgradeableProxy(address(_proxy)); vm.label(COMPOUND_USDT_V3_COMMET_MAINNET, "CompoundV3USDT"); - vm.label(address(proxy), "CompoundV3USDTStrategy"); - strategy1 = ICompoundV3StrategyWrapper(address(_proxy)); - address[] memory strategyList = new address[](1); + // Deploy strategy2 + BeefythUSDDAIUSDCUSDTStrategyWrapper implementation2 = new BeefythUSDDAIUSDCUSDTStrategyWrapper(); + _proxy = new TransparentUpgradeableProxy( + address(implementation2), + address(proxyAdmin), + abi.encodeWithSignature( + "initialize(address,address[],bytes32,address,address,address,address,address)", + address(vault), + keepers, + bytes32("MaxApy thUSDDAIUSDCUSDT Strategy"), + users.alice, + CURVE_THUSD_DAI_USDC_USDT_MAINNET, + BEEFY_THUSD_DAI_USDC_USDT_MAINNET, + CURVE_3POOL_POOL_MAINNET, + CRV3POOL_MAINNET + ) + ); + vm.label(BEEFY_THUSD_DAI_USDC_USDT_MAINNET, "BeefythUSDTDAIUSDCUSDT"); + strategy2 = IStrategyWrapper(address(_proxy)); + + address[] memory strategyList = new address[](2); strategyList[0] = address(strategy1); + strategyList[1] = address(strategy2); // Add all the strategies vault.addStrategy(address(strategy1), 700, type(uint72).max, 0, 0); + vault.addStrategy(address(strategy2), 700, type(uint72).max, 0, 0); vm.label(address(USDC_MAINNET), "USDC"); /// Alice approves vault for deposits @@ -123,6 +151,7 @@ contract MaxApyIntegrationTestMainnet is BaseTest, StrategyEvents { uint256 _keeperRole = strategy1.KEEPER_ROLE(); strategy1.grantRoles(address(strategyFuzzer), _keeperRole); + strategy2.grantRoles(address(strategyFuzzer), _keeperRole); } function testFuzzMaxApyIntegrationMainnet__DepositAndRedeemWithoutHarvests( diff --git a/test/interfaces/IStrategyWrapper.sol b/test/interfaces/IStrategyWrapper.sol index 8d361c1..e96b513 100644 --- a/test/interfaces/IStrategyWrapper.sol +++ b/test/interfaces/IStrategyWrapper.sol @@ -53,6 +53,8 @@ interface IStrategyWrapper is IStrategy { function curveLpPool() external view returns (address); + function curveTriPool() external view returns (address); + function curveEthFrxEthPool() external view returns (address); function curveUsdcCrvUsdPool() external view returns (address); diff --git a/test/mock/BeefythUSDDAIUSDCUSDTStrategyWrapper.sol b/test/mock/BeefythUSDDAIUSDCUSDTStrategyWrapper.sol new file mode 100644 index 0000000..d9d0895 --- /dev/null +++ b/test/mock/BeefythUSDDAIUSDCUSDTStrategyWrapper.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.19; + +import { + BeefythUSDDAIUSDCUSDTStrategy, + SafeTransferLib +} from "src/strategies/mainnet/USDC/beefy/BeefythUSDDAIUSDCUSDTStrategy.sol"; + +contract BeefythUSDDAIUSDCUSDTStrategyWrapper is BeefythUSDDAIUSDCUSDTStrategy { + using SafeTransferLib for address; + + function triggerLoss(uint256 amount) external { + underlyingAsset.safeTransfer(address(underlyingAsset), amount); + } + + function mockReport(uint128 gain, uint128 loss, uint128 debtPayment, address treasury) external { + vault.report(gain, loss, debtPayment, treasury); + } + + function prepareReturn( + uint256 debtOutstanding, + uint256 minExpectedBalance + ) + external + returns (uint256 unrealizedProfit, uint256 loss, uint256 debtPayment) + { + (unrealizedProfit, loss, debtPayment) = _prepareReturn(debtOutstanding, minExpectedBalance); + } + + function adjustPosition() external { + _adjustPosition(0, 0); + } + + function invest(uint256 amount, uint256 minOutputAfterInvestment) external returns (uint256) { + return _invest(amount, minOutputAfterInvestment); + } + + function divest(uint256 shares) external returns (uint256) { + return _divest(shares); + } + + function liquidatePosition(uint256 amountNeeded) external returns (uint256, uint256) { + return _liquidatePosition(amountNeeded); + } + + function liquidateAllPositions() external returns (uint256) { + return _liquidateAllPositions(); + } + + function shareValue(uint256 shares) external view returns (uint256) { + return _shareValue(shares); + } + + function sharesForAmount(uint256 amount) external view returns (uint256) { + return _sharesForAmount(amount); + } + + function shareBalance() external view returns (uint256) { + return _shareBalance(); + } + + function lpPrice() external view returns (uint256) { + return _lpPrice(); + } +} diff --git a/test/unit/strategies/BeefythUSDDAIUSDCUSDTStrategy.t.sol b/test/unit/strategies/BeefythUSDDAIUSDCUSDTStrategy.t.sol new file mode 100644 index 0000000..a1aeb4c --- /dev/null +++ b/test/unit/strategies/BeefythUSDDAIUSDCUSDTStrategy.t.sol @@ -0,0 +1,642 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.19; + +import { ProxyAdmin } from "openzeppelin/proxy/transparent/ProxyAdmin.sol"; +import { + ITransparentUpgradeableProxy, + TransparentUpgradeableProxy +} from "openzeppelin/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import { BaseTest, IERC20, Vm, console2 } from "../../base/BaseTest.t.sol"; + +import { IStrategyWrapper } from "../../interfaces/IStrategyWrapper.sol"; +import { ICurveLpPool } from "src/interfaces/ICurve.sol"; +import { IMaxApyVault } from "src/interfaces/IMaxApyVault.sol"; + +import { ConvexdETHFrxETHStrategyEvents } from "../../helpers/ConvexdETHFrxETHStrategyEvents.sol"; + +import { BeefythUSDDAIUSDCUSDTStrategyWrapper } from "../../mock/BeefythUSDDAIUSDCUSDTStrategyWrapper.sol"; + +import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; +import { MaxApyVault } from "src/MaxApyVault.sol"; +import "src/helpers/AddressBook.sol"; +import { StrategyData } from "src/helpers/VaultTypes.sol"; +import { _1_USDC } from "test/helpers/Tokens.sol"; + +contract BeefythUSDDAIUSDCUSDTStrategyTest is BaseTest, ConvexdETHFrxETHStrategyEvents { + using SafeTransferLib for address; + + address public TREASURY; + IStrategyWrapper public strategy; + BeefythUSDDAIUSDCUSDTStrategyWrapper public implementation; + MaxApyVault public vaultDeployment; + IMaxApyVault public vault; + ITransparentUpgradeableProxy public proxy; + ProxyAdmin public proxyAdmin; + + function setUp() public { + super._setUp("MAINNET"); + vm.rollFork(21_367_091); + + TREASURY = makeAddr("treasury"); + + vaultDeployment = new MaxApyVault(users.alice, USDC_MAINNET, "MaxApyUSDCVault", "maxUSDC", TREASURY); + + vault = IMaxApyVault(address(vaultDeployment)); + + proxyAdmin = new ProxyAdmin(users.alice); + implementation = new BeefythUSDDAIUSDCUSDTStrategyWrapper(); + + address[] memory keepers = new address[](1); + keepers[0] = users.keeper; + TransparentUpgradeableProxy _proxy = new TransparentUpgradeableProxy( + address(implementation), + address(proxyAdmin), + abi.encodeWithSignature( + "initialize(address,address[],bytes32,address,address,address,address,address)", + address(vault), + keepers, + bytes32("MaxApy thUSDDAIUSDCUSDT Strategy"), + users.alice, + CURVE_THUSD_DAI_USDC_USDT_MAINNET, + BEEFY_THUSD_DAI_USDC_USDT_MAINNET, + CURVE_3POOL_POOL_MAINNET, + CRV3POOL_MAINNET + ) + ); + proxy = ITransparentUpgradeableProxy(address(_proxy)); + + strategy = IStrategyWrapper(address(_proxy)); + USDC_MAINNET.safeApprove(address(vault), type(uint256).max); + + vm.label(USDC_MAINNET, "USDC_MAINNET"); + } + + /*==================INITIALIZATION TESTS==================*/ + + function testBeefythUSDDAIUSDCUSDT__Initialization() public { + MaxApyVault _vault = new MaxApyVault(users.alice, USDC_MAINNET, "MaxApyUSDCVault", "maxUSDC", TREASURY); + + ProxyAdmin _proxyAdmin = new ProxyAdmin(users.alice); + BeefythUSDDAIUSDCUSDTStrategyWrapper _implementation = new BeefythUSDDAIUSDCUSDTStrategyWrapper(); + + address[] memory keepers = new address[](1); + keepers[0] = users.keeper; + + TransparentUpgradeableProxy _proxy = new TransparentUpgradeableProxy( + address(_implementation), + address(_proxyAdmin), + abi.encodeWithSignature( + "initialize(address,address[],bytes32,address,address,address,address,address)", + address(_vault), + keepers, + bytes32("MaxApy thUSDDAIUSDCUSDT Strategy"), + users.alice, + CURVE_THUSD_DAI_USDC_USDT_MAINNET, + BEEFY_THUSD_DAI_USDC_USDT_MAINNET, + CURVE_3POOL_POOL_MAINNET, + CRV3POOL_MAINNET + ) + ); + + IStrategyWrapper _strategy = IStrategyWrapper(address(_proxy)); + assertEq(_strategy.vault(), address(_vault)); + + assertEq(_strategy.hasAnyRole(address(_vault), _strategy.VAULT_ROLE()), true); + assertEq(_strategy.underlyingAsset(), USDC_MAINNET); + assertEq(IERC20(USDC_MAINNET).allowance(address(_strategy), address(_vault)), type(uint256).max); + assertEq(_strategy.hasAnyRole(users.keeper, _strategy.KEEPER_ROLE()), true); + assertEq(_strategy.hasAnyRole(users.alice, _strategy.ADMIN_ROLE()), true); + + assertEq(_strategy.owner(), users.alice); + assertEq(_strategy.strategyName(), bytes32("MaxApy thUSDDAIUSDCUSDT Strategy")); + + assertEq(_strategy.curveLpPool(), CURVE_THUSD_DAI_USDC_USDT_MAINNET, "hereee"); + assertEq(_strategy.curveTriPool(), CURVE_3POOL_POOL_MAINNET, "hereee"); + assertEq(IERC20(USDC_MAINNET).allowance(address(_strategy), CURVE_3POOL_POOL_MAINNET), type(uint256).max); + + assertEq(_proxyAdmin.owner(), users.alice); + vm.startPrank(address(_proxyAdmin)); + vm.stopPrank(); + + vm.startPrank(users.alice); + } + + /*==================STRATEGY CONFIGURATION TESTS==================*/ + + function testBeefythUSDDAIUSDCUSDT__SetEmergencyExit() public { + vm.stopPrank(); + vm.startPrank(users.bob); + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + strategy.setEmergencyExit(2); + vm.stopPrank(); + vm.startPrank(address(vault)); + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + strategy.setEmergencyExit(2); + + vm.stopPrank(); + vm.startPrank(users.alice); + vm.expectEmit(); + emit StrategyEmergencyExitUpdated(address(strategy), 2); + strategy.setEmergencyExit(2); + } + + function testBeefythUSDDAIUSDCUSDT__SetMinSingleTrade() public { + vm.stopPrank(); + vm.startPrank(users.bob); + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + strategy.setMinSingleTrade(1 * _1_USDC); + + vm.stopPrank(); + vm.startPrank(address(vault)); + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + strategy.setMinSingleTrade(1 * _1_USDC); + + vm.stopPrank(); + vm.startPrank(users.alice); + vm.expectEmit(); + emit MinSingleTradeUpdated(1 * _1_USDC); + strategy.setMinSingleTrade(1 * _1_USDC); + assertEq(strategy.minSingleTrade(), 1 * _1_USDC); + } + + function testBeefythUSDDAIUSDCUSDT__IsActive() public { + vault.addStrategy(address(strategy), 10_000, 0, 0, 0); + assertEq(strategy.isActive(), false); + + deal(USDC_MAINNET, address(strategy), 1 * _1_USDC); + assertEq(strategy.isActive(), false); + + vm.startPrank(users.keeper); + strategy.harvest(0, 0, address(0), block.timestamp); + assertEq(strategy.isActive(), true); + vm.stopPrank(); + + strategy.divest(IERC20(BEEFY_THUSD_DAI_USDC_USDT_MAINNET).balanceOf(address(strategy))); + vm.startPrank(address(strategy)); + IERC20(USDC_MAINNET).transfer(makeAddr("random"), IERC20(USDC_MAINNET).balanceOf(address(strategy))); + assertEq(strategy.isActive(), false); + + deal(USDC_MAINNET, address(strategy), 1 * _1_USDC); + vm.startPrank(users.keeper); + strategy.harvest(0, 0, address(0), block.timestamp); + assertEq(strategy.isActive(), true); + } + + function testBeefythUSDDAIUSDCUSDT__SetStrategist() public { + // Negatives + vm.startPrank(users.bob); + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + strategy.setStrategist(address(0)); + + vm.startPrank(users.alice); + vm.expectRevert(abi.encodeWithSignature("InvalidZeroAddress()")); + strategy.setStrategist(address(0)); + + // Positives + address random = makeAddr("random"); + vm.expectEmit(); + emit StrategistUpdated(address(strategy), random); + strategy.setStrategist(random); + assertEq(strategy.strategist(), random); + } + + /*==================STRATEGY CORE LOGIC TESTS==================*/ + function testBeefythUSDDAIUSDCUSDT__InvestmentSlippage() public { + vault.addStrategy(address(strategy), 4000, type(uint72).max, 0, 0); + + vault.deposit(100 * _1_USDC, users.alice); + + vm.startPrank(users.keeper); + + // Expect revert if output amount is gt amount obtained + vm.expectRevert(abi.encodeWithSignature("MinOutputAmountNotReached()")); + strategy.harvest(0, type(uint256).max, address(0), block.timestamp); + } + + function testBeefythUSDDAIUSDCUSDT__PrepareReturn() public { + uint256 snapshotId = vm.snapshot(); + + vault.addStrategy(address(strategy), 4000, type(uint72).max, 0, 0); + + vault.deposit(100 * _1_USDC, users.alice); + + strategy.mockReport(0, 0, 0, TREASURY); + + (uint256 unrealizedProfit, uint256 loss, uint256 debtPayment) = strategy.prepareReturn(1 * _1_USDC, 0); + + assertEq(loss, 0); + assertEq(debtPayment, 1 * _1_USDC); + + vm.revertTo(snapshotId); + + snapshotId = vm.snapshot(); + deal({ token: USDC_MAINNET, to: address(strategy), give: 60 * _1_USDC }); + + strategy.adjustPosition(); + + vault.addStrategy(address(strategy), 4000, type(uint72).max, 0, 0); + + vault.deposit(100 * _1_USDC, users.alice); + + (unrealizedProfit, loss, debtPayment) = strategy.prepareReturn(0, 0); + + assertApproxEq(unrealizedProfit, 60 * _1_USDC, 1 * _1_USDC); + assertEq(loss, 0); + assertEq(debtPayment, 0); + + vm.revertTo(snapshotId); + + snapshotId = vm.snapshot(); + + vault.addStrategy(address(strategy), 4000, type(uint72).max, 0, 0); + + vault.deposit(100 * _1_USDC, users.alice); + + strategy.mockReport(0, 0, 0, TREASURY); + + strategy.triggerLoss(10 * _1_USDC); + + (unrealizedProfit, loss, debtPayment) = strategy.prepareReturn(0, 0); + + assertEq(unrealizedProfit, 0); + assertEq(loss, 10 * _1_USDC); + assertEq(debtPayment, 0); + + vm.revertTo(snapshotId); + + snapshotId = vm.snapshot(); + + deal({ token: USDC_MAINNET, to: address(strategy), give: 80 * _1_USDC }); + + strategy.adjustPosition(); + + vault.addStrategy(address(strategy), 4000, type(uint72).max, 0, 0); + + vault.deposit(100 * _1_USDC, users.alice); + + strategy.mockReport(0, 0, 0, TREASURY); + + (unrealizedProfit, loss, debtPayment) = strategy.prepareReturn(0, 0); + + assertEq(loss, 0); + assertEq(debtPayment, 0); + } + + function testBeefythUSDDAIUSDCUSDT__Invest() public { + uint256 returned = strategy.invest(0, 0); + assertEq(returned, 0); + assertEq(IERC20(BEEFY_THUSD_DAI_USDC_USDT_MAINNET).balanceOf(address(strategy)), 0); + + vm.expectRevert(abi.encodeWithSignature("NotEnoughFundsToInvest()")); + returned = strategy.invest(1, 0); + + deal({ token: USDC_MAINNET, to: address(strategy), give: 10 * _1_USDC }); + uint256 expectedShares = strategy.sharesForAmount(10 * _1_USDC); + + vm.expectEmit(); + emit Invested(address(strategy), 10 * _1_USDC); + strategy.invest(10 * _1_USDC, 0); + + assertApproxEq( + expectedShares, IERC20(BEEFY_THUSD_DAI_USDC_USDT_MAINNET).balanceOf(address(strategy)), expectedShares / 10 + ); + } + + function testBeefythUSDDAIUSDCUSDT__Divest() public { + deal({ token: USDC_MAINNET, to: address(strategy), give: 10 * _1_USDC }); + uint256 expectedShares = strategy.sharesForAmount(10 * _1_USDC); + + vm.expectEmit(); + emit Invested(address(strategy), 10 * _1_USDC); + strategy.invest(10 * _1_USDC, 0); + + assertApproxEq( + expectedShares, IERC20(BEEFY_THUSD_DAI_USDC_USDT_MAINNET).balanceOf(address(strategy)), expectedShares / 10 + ); + + uint256 strategyBalanceBefore = IERC20(USDC_MAINNET).balanceOf(address(strategy)); + uint256 amountDivested = strategy.divest(IERC20(BEEFY_THUSD_DAI_USDC_USDT_MAINNET).balanceOf(address(strategy))); + + assertEq(IERC20(USDC_MAINNET).balanceOf(address(strategy)), strategyBalanceBefore + amountDivested); + } + + function testBeefythUSDDAIUSDCUSDT__LiquidatePosition() public { + deal({ token: USDC_MAINNET, to: address(strategy), give: 10 * _1_USDC }); + (uint256 liquidatedAmount, uint256 loss) = strategy.liquidatePosition(1 * _1_USDC); + assertEq(liquidatedAmount, 1 * _1_USDC); + assertEq(loss, 0); + + (liquidatedAmount, loss) = strategy.liquidatePosition(10 * _1_USDC); + assertEq(liquidatedAmount, 10 * _1_USDC); + assertEq(loss, 0); + + deal({ token: USDC_MAINNET, to: address(strategy), give: 5 * _1_USDC }); + uint256 invested = strategy.invest(5 * _1_USDC, 0); + + deal({ token: USDC_MAINNET, to: address(strategy), give: 10 * _1_USDC }); + + (liquidatedAmount, loss) = strategy.liquidatePosition(149 * _1_USDC / 10); + + assertApproxEq(liquidatedAmount, 149 * _1_USDC / 10, 5 * _1_USDC / 1000); + assertLt(loss, 1 * _1_USDC / 5); + + deal({ token: USDC_MAINNET, to: address(strategy), give: 50 * _1_USDC }); + invested = strategy.invest(50 * _1_USDC, 0); + + (liquidatedAmount, loss) = strategy.liquidatePosition(495 * _1_USDC / 10); + + assertApproxEq(liquidatedAmount, 495 * _1_USDC / 10, 5 * _1_USDC / 100); + assertLt(loss, 1 * _1_USDC / 5); + } + + function testBeefythUSDDAIUSDCUSDT__LiquidateAllPositions() public { + uint256 snapshotId = vm.snapshot(); + + deal({ token: USDC_MAINNET, to: address(strategy), give: 10 * _1_USDC }); + uint256 shares = strategy.sharesForAmount(10 * _1_USDC); + vm.expectEmit(); + emit Invested(address(strategy), 10 * _1_USDC); + strategy.invest(10 * _1_USDC, 0); + + assertApproxEq(IERC20(BEEFY_THUSD_DAI_USDC_USDT_MAINNET).balanceOf(address(strategy)), shares, shares / 10); + + uint256 strategyBalanceBefore = IERC20(USDC_MAINNET).balanceOf(address(strategy)); + uint256 amountFreed = strategy.liquidateAllPositions(); + + assertApproxEq(amountFreed, 10 * _1_USDC, 3 * _1_USDC / 100); + + assertEq(IERC20(USDC_MAINNET).balanceOf(address(strategy)), strategyBalanceBefore + amountFreed); + assertEq(IERC20(BEEFY_THUSD_DAI_USDC_USDT_MAINNET).balanceOf(address(strategy)), 0); + + vm.revertTo(snapshotId); + + deal({ token: USDC_MAINNET, to: address(strategy), give: 100 * _1_USDC }); + shares = strategy.sharesForAmount(100 * _1_USDC); + + vm.expectEmit(); + emit Invested(address(strategy), 100 * _1_USDC); + strategy.invest(100 * _1_USDC, 0); + assertApproxEq(IERC20(BEEFY_THUSD_DAI_USDC_USDT_MAINNET).balanceOf(address(strategy)), shares, 0.006 ether); + + strategyBalanceBefore = IERC20(USDC_MAINNET).balanceOf(address(strategy)); + amountFreed = strategy.liquidateAllPositions(); + + assertApproxEq(amountFreed, 100 * _1_USDC, _1_USDC); + + assertEq(IERC20(USDC_MAINNET).balanceOf(address(strategy)), strategyBalanceBefore + amountFreed); + assertEq(IERC20(BEEFY_THUSD_DAI_USDC_USDT_MAINNET).balanceOf(address(strategy)), 0); + } + + function testBeefythUSDDAIUSDCUSDT__Harvest() public { + vm.expectRevert(abi.encodeWithSignature("Unauthorized()")); + strategy.harvest(0, 0, address(0), block.timestamp); + + uint256 snapshotId = vm.snapshot(); + + vault.addStrategy(address(strategy), 4000, type(uint72).max, 0, 0); + + vault.deposit(100 * _1_USDC, users.alice); + + vm.startPrank(users.keeper); + + vm.expectEmit(); + emit StrategyReported(address(strategy), 0, 0, 0, 0, 0, uint128(40 * _1_USDC), uint128(40 * _1_USDC), 4000); + + vm.expectEmit(); + emit Harvested(0, 0, 0, 0); + + strategy.harvest(0, 0, address(0), block.timestamp); + + uint256 expectedStrategyShareBalance = strategy.sharesForAmount(40 * _1_USDC); + assertEq(IERC20(USDC_MAINNET).balanceOf(address(vault)), 60 * _1_USDC); + assertEq(IERC20(USDC_MAINNET).balanceOf(address(strategy)), 0); + + deal({ token: USDC_MAINNET, to: address(strategy), give: 10 * _1_USDC }); + vm.warp(block.timestamp + 1 days); + + strategy.harvest(0, 0, address(0), block.timestamp); + assertEq(IERC20(USDC_MAINNET).balanceOf(address(vault)), 60 * _1_USDC); + + vm.revertTo(snapshotId); + snapshotId = vm.snapshot(); + + vm.startPrank(users.alice); + + vault.addStrategy(address(strategy), 4000, type(uint72).max, 0, 0); + + vault.deposit(100 * _1_USDC, users.alice); + + vm.startPrank(users.keeper); + + vm.expectEmit(); + emit StrategyReported(address(strategy), 0, 0, 0, 0, 0, uint128(40 * _1_USDC), uint128(40 * _1_USDC), 4000); + + vm.expectEmit(); + emit Harvested(0, 0, 0, 0); + + strategy.harvest(0, 0, address(0), block.timestamp); + + expectedStrategyShareBalance = strategy.sharesForAmount(40 * _1_USDC); + assertEq(IERC20(USDC_MAINNET).balanceOf(address(vault)), 60 * _1_USDC); + + vm.startPrank(users.alice); + strategy.setEmergencyExit(2); + + vm.startPrank(users.keeper); + + deal({ token: USDC_MAINNET, to: address(strategy), give: 10 * _1_USDC }); + vm.warp(block.timestamp + 1 days); + + strategy.harvest(0, 0, address(0), block.timestamp); + assertEq(IERC20(USDC_MAINNET).balanceOf(address(vault)), 109_971_091); + assertEq(IERC20(BEEFY_THUSD_DAI_USDC_USDT_MAINNET).balanceOf(address(strategy)), 0); + vm.revertTo(snapshotId); + + vm.startPrank(users.alice); + + vault.addStrategy(address(strategy), 4000, type(uint72).max, 0, 0); + + expectedStrategyShareBalance = strategy.sharesForAmount(40 * _1_USDC); + vault.deposit(100 * _1_USDC, users.alice); + + vm.startPrank(users.keeper); + + vm.expectEmit(); + emit StrategyReported(address(strategy), 0, 0, 0, 0, 0, uint128(40 * _1_USDC), uint128(40 * _1_USDC), 4000); + + vm.expectEmit(); + emit Harvested(0, 0, 0, 0); + strategy.harvest(0, 0, address(0), block.timestamp); + + assertEq(IERC20(USDC_MAINNET).balanceOf(address(vault)), 60 * _1_USDC); + + expectedStrategyShareBalance = strategy.sharesForAmount(10 * _1_USDC); + + vm.startPrank(address(strategy)); + uint256 withdrawn = strategy.divest(expectedStrategyShareBalance); + + IERC20(USDC_MAINNET).transfer(makeAddr("random"), withdrawn); + vm.startPrank(users.keeper); + + strategy.harvest(0, 0, address(0), block.timestamp); + + StrategyData memory data = vault.strategies(address(strategy)); + + assertEq(vault.debtRatio(), 3001); + assertEq(data.strategyDebtRatio, 3001); + } + + function testBeefythUSDDAIUSDCUSDT__PreviewLiquidate() public { + vault.addStrategy(address(strategy), 4000, type(uint72).max, 0, 0); + vault.deposit(100 * _1_USDC, users.alice); + vm.startPrank(users.keeper); + + strategy.harvest(0, 0, address(0), block.timestamp); + + vm.stopPrank(); + uint256 expected = strategy.previewLiquidate(30 * _1_USDC); + + vm.startPrank(address(vault)); + + uint256 loss = strategy.liquidate(30 * _1_USDC); + + assertLe(expected, 30 * _1_USDC - loss); + } + + function testBeefythUSDDAIUSDCUSDT__PreviewLiquidateExact() public { + vault.addStrategy(address(strategy), 4000, type(uint72).max, 0, 0); + vault.deposit(100 * _1_USDC, users.alice); + vm.startPrank(users.keeper); + strategy.harvest(0, 0, address(0), block.timestamp); + vm.stopPrank(); + uint256 requestedAmount = strategy.previewLiquidateExact(30 * _1_USDC); + + vm.startPrank(address(vault)); + uint256 balanceBefore = IERC20(USDC_MAINNET).balanceOf(address(vault)); + + strategy.liquidateExact(30 * _1_USDC); + uint256 withdrawn = IERC20(USDC_MAINNET).balanceOf(address(vault)) - balanceBefore; + + // withdraw exactly what requested + assertEq(withdrawn, 30 * _1_USDC); + // losses are equal or fewer than expected + assertLe(withdrawn - 30 * _1_USDC, requestedAmount - 30 * _1_USDC); + } + + function testBeefythUSDDAIUSDCUSDT__maxLiquidateExact() public { + vault.addStrategy(address(strategy), 9000, type(uint72).max, 0, 0); + vault.deposit(100 * _1_USDC, users.alice); + vm.startPrank(users.keeper); + strategy.harvest(0, 0, address(0), block.timestamp); + vm.stopPrank(); + uint256 maxLiquidateExact = strategy.maxLiquidateExact(); + uint256 balanceBefore = IERC20(USDC_MAINNET).balanceOf(address(vault)); + uint256 requestedAmount = strategy.previewLiquidateExact(maxLiquidateExact); + vm.startPrank(address(vault)); + uint256 losses = strategy.liquidateExact(maxLiquidateExact); + uint256 withdrawn = IERC20(USDC_MAINNET).balanceOf(address(vault)) - balanceBefore; + // withdraw exactly what requested + assertEq(withdrawn, maxLiquidateExact); + // losses are equal or fewer than expected + assertLe(losses, requestedAmount - maxLiquidateExact); + } + + function testBeefythUSDDAIUSDCUSDT__MaxLiquidate() public { + vault.addStrategy(address(strategy), 9000, type(uint72).max, 0, 0); + vault.deposit(100 * _1_USDC, users.alice); + vm.startPrank(users.keeper); + strategy.harvest(0, 0, address(0), block.timestamp); + vm.stopPrank(); + uint256 maxWithdraw = strategy.maxLiquidate(); + uint256 balanceBefore = IERC20(USDC_MAINNET).balanceOf(address(vault)); + vm.startPrank(address(vault)); + strategy.liquidate(maxWithdraw); + uint256 withdrawn = IERC20(USDC_MAINNET).balanceOf(address(vault)) - balanceBefore; + assertLe(withdrawn, maxWithdraw); + } + + function testBeefythUSDDAIUSDCUSDT___SimulateHarvest() public { + vault.addStrategy(address(strategy), 4000, type(uint72).max, 0, 0); + vault.deposit(100 * _1_USDC, users.alice); + vm.startPrank(users.keeper); + (uint256 expectedBalance, uint256 outputAfterInvestment,,,,) = strategy.simulateHarvest(); + strategy.harvest(expectedBalance, outputAfterInvestment, address(0), block.timestamp); + } + + function testBeefythUSDDAIUSDCUSDT__PreviewLiquidate__FUZZY(uint256 amount) public { + vm.assume(amount > 1 * _1_USDC && amount < 100 * _1_USDC); + deal(USDC_MAINNET, users.alice, amount); + vault.addStrategy(address(strategy), 4000, type(uint72).max, 0, 0); + vault.deposit(amount, users.alice); + vm.startPrank(users.keeper); + + strategy.harvest(0, 0, address(0), block.timestamp); + + vm.stopPrank(); + uint256 expected = strategy.previewLiquidate(amount / 3); + vm.startPrank(address(vault)); + + uint256 loss = strategy.liquidate(amount / 3); + + assertLe(expected, amount / 3 - loss); + } + + function testBeefythUSDDAIUSDCUSDT__PreviewLiquidateExact__FUZZY(uint256 amount) public { + vm.assume(amount > 1 * _1_USDC && amount < 100 * _1_USDC); + deal(USDC_MAINNET, users.alice, amount); + vault.addStrategy(address(strategy), 4000, type(uint72).max, 0, 0); + vault.deposit(amount, users.alice); + vm.startPrank(users.keeper); + strategy.harvest(0, 0, address(0), block.timestamp); + vm.stopPrank(); + uint256 requestedAmount = strategy.previewLiquidateExact(amount / 3); + + vm.startPrank(address(vault)); + uint256 balanceBefore = IERC20(USDC_MAINNET).balanceOf(address(vault)); + + strategy.liquidateExact(amount / 3); + uint256 withdrawn = IERC20(USDC_MAINNET).balanceOf(address(vault)) - balanceBefore; + + // withdraw exactly what requested + assertGe(withdrawn, amount / 3); + // losses are equal or fewer than expected + assertLe(withdrawn - (amount / 3), requestedAmount - (amount / 3)); + } + + function testBeefythUSDDAIUSDCUSDT__maxLiquidateExact_FUZZY(uint256 amount) public { + vm.assume(amount > 1 * _1_USDC && amount < 100 * _1_USDC); + deal(USDC_MAINNET, users.alice, amount); + vault.addStrategy(address(strategy), 9000, type(uint72).max, 0, 0); + vault.deposit(amount, users.alice); + vm.startPrank(users.keeper); + strategy.harvest(0, 0, address(0), block.timestamp); + vm.stopPrank(); + uint256 maxLiquidateExact = strategy.maxLiquidateExact(); + uint256 balanceBefore = IERC20(USDC_MAINNET).balanceOf(address(vault)); + uint256 requestedAmount = strategy.previewLiquidateExact(maxLiquidateExact); + vm.startPrank(address(vault)); + uint256 losses = strategy.liquidateExact(maxLiquidateExact); + uint256 withdrawn = IERC20(USDC_MAINNET).balanceOf(address(vault)) - balanceBefore; + // withdraw exactly what requested + assertGe(withdrawn, maxLiquidateExact); + // losses are equal or fewer than expected + assertLe(losses, requestedAmount - maxLiquidateExact); + } + + function testBeefythUSDDAIUSDCUSDT__MaxLiquidate_FUZZY(uint256 amount) public { + vm.assume(amount > 1 * _1_USDC && amount < 100 * _1_USDC); + deal(USDC_MAINNET, users.alice, amount); + vault.addStrategy(address(strategy), 9000, type(uint72).max, 0, 0); + vault.deposit(amount, users.alice); + vm.startPrank(users.keeper); + strategy.harvest(0, 0, address(0), block.timestamp); + vm.stopPrank(); + uint256 maxWithdraw = strategy.maxLiquidate(); + uint256 balanceBefore = IERC20(USDC_MAINNET).balanceOf(address(vault)); + vm.startPrank(address(vault)); + strategy.liquidate(maxWithdraw); + uint256 withdrawn = IERC20(USDC_MAINNET).balanceOf(address(vault)) - balanceBefore; + assertLe(withdrawn, maxWithdraw); + } +}