diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 00e8b912c..b29b214dc 100755 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,18 @@ jobs: with: version: stable + # Restore Foundry RPC cache to avoid hitting paid RPC endpoints on every CI run. + # This cache stores responses from all networks (Ethereum, Arbitrum, Base, etc.) + # and allows forge to reuse previously fetched block/state data. + # Without this step, every CI run would re-query RPC providers from scratch, + # quickly exhausting rate limits on paid RPC keys. + - name: Restore RPC cache + uses: actions/cache@v3 + with: + path: ~/.foundry/cache/rpc + key: rpc-cache + restore-keys: rpc-cache + - name: Run forge build run: | forge --version diff --git a/chains/EthereumLib.sol b/chains/EthereumLib.sol index c96d29516..aae36fc16 100644 --- a/chains/EthereumLib.sol +++ b/chains/EthereumLib.sol @@ -16,6 +16,7 @@ import {StrategyDeveloperLib} from "../src/strategies/libs/StrategyDeveloperLib. import {CVault} from "../src/core/vaults/CVault.sol"; import {VaultTypeLib} from "../src/core/libs/VaultTypeLib.sol"; import {PriceReader, IPriceReader} from "../src/core/PriceReader.sol"; +import {AaveLeverageMerklFarmStrategy} from "../src/strategies/AaveLeverageMerklFarmStrategy.sol"; /// @dev Ethereum network [chainId: 1] data library /// EEEEEEEEEE TTTTTTTTTT HHH HHH EEEEEEEEEE RRRRRRRR EEEEEEEEEE UU UU M M @@ -48,6 +49,7 @@ library EthereumLib { address public constant POOL_UNISWAPV3_COMP_WETH_3000 = 0xea4Ba4CE14fdd287f380b55419B1C5b6c3f22ab6; address public constant POOL_UNISWAPV3_SHFL_USDC_3000 = 0xD0A4c8A1a14530C7C9EfDaD0BA37E8cF4204d230; address public constant POOL_UNISWAPV3_WBTC_EBTC_500 = 0xEf9b4FddD861aa2F00eE039C323b7FAbd7AFE239; + address public constant POOL_UNISWAPV3_WBTC_USDC_3000 = 0x99ac8cA7087fA4A2A1FB6357269965A2014ABc35; // Oracles address public constant ORACLE_CHAINLINK_USDC_USD = 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6; @@ -136,6 +138,7 @@ library EthereumLib { //region ----- Deploy strategy logics ----- factory.setStrategyImplementation(StrategyIdLib.COMPOUND_FARM, address(new CompoundFarmStrategy())); + factory.setStrategyImplementation(StrategyIdLib.AAVE_LEVERAGE_MERKL_FARM, address(new AaveLeverageMerklFarmStrategy())); LogDeployLib.logDeployStrategies(platform, showLog); //endregion @@ -158,14 +161,15 @@ library EthereumLib { //endregion //region ----- Pools ----- - pools = new ISwapper.AddPoolData[](6); + pools = new ISwapper.AddPoolData[](7); uint i; pools[i++] = _makePoolData(POOL_UNISWAPV3_USDC_WETH_500, AmmAdapterIdLib.UNISWAPV3, TOKEN_USDC, TOKEN_WETH); pools[i++] = _makePoolData(POOL_UNISWAPV3_WETH_weETH_500, AmmAdapterIdLib.UNISWAPV3, TOKEN_WETH, TOKEN_weETH); pools[i++] = _makePoolData(POOL_UNISWAPV3_wstETH_WETH_100, AmmAdapterIdLib.UNISWAPV3, TOKEN_wstETH, TOKEN_WETH); pools[i++] = _makePoolData(POOL_UNISWAPV3_COMP_WETH_3000, AmmAdapterIdLib.UNISWAPV3, TOKEN_COMP, TOKEN_WETH); pools[i++] = _makePoolData(POOL_UNISWAPV3_SHFL_USDC_3000, AmmAdapterIdLib.UNISWAPV3, TOKEN_SHFL, TOKEN_USDC); - pools[i++] = _makePoolData(POOL_UNISWAPV3_WBTC_EBTC_500, AmmAdapterIdLib.UNISWAPV3, TOKEN_WBTC, TOKEN_EBTC); + pools[i++] = _makePoolData(POOL_UNISWAPV3_WBTC_EBTC_500, AmmAdapterIdLib.UNISWAPV3, TOKEN_EBTC, TOKEN_WBTC); + pools[i++] = _makePoolData(POOL_UNISWAPV3_WBTC_USDC_3000, AmmAdapterIdLib.UNISWAPV3, TOKEN_WBTC, TOKEN_USDC); //endregion } diff --git a/chains/plasma/PlasmaConstantsLib.sol b/chains/plasma/PlasmaConstantsLib.sol index ed7c478bf..9cdbfb79b 100644 --- a/chains/plasma/PlasmaConstantsLib.sol +++ b/chains/plasma/PlasmaConstantsLib.sol @@ -30,6 +30,8 @@ library PlasmaConstantsLib { // Balancer address public constant BALANCER_V3_ROUTER = 0x9dA18982a33FD0c7051B19F0d7C76F2d5E7e017c; + address public constant POOL_BALANCER_V3_STABLE_WETH_WEETH = 0xda51975D78Cb172b46d7292cEC9fa9E74723eF3b; + address public constant POOL_BALANCER_V3_STABLE_SUSDE_USDT0 = 0xd9c4e277c93374a9f8C877a9D06707a88092E8F0; // Euler: todo we need new strategy /// @notice https://app.euler.finance/earn/0xe818ad0D20D504C55601b9d5e0E137314414dec4?network=plasma @@ -37,6 +39,28 @@ library PlasmaConstantsLib { address public constant EULER_MERKL_USDT0_RE7 = 0xa5EeD1615cd883dD6883ca3a385F525e3bEB4E79; // AAVE + address public constant AAVE_V3_ADDRESS_PROVIDER = 0x061D8e131F26512348ee5FA42e2DF1bA9d6505E9; + address public constant AAVE_V3_POOL_DATA_PROVIDER = 0xf2D6E38B407e31E7E7e4a16E6769728b76c7419F; address public constant AAVE_V3_POOL = 0x925a2A7214Ed92428B5b1B090F80b25700095e12; address public constant AAVE_V3_POOL_USDT0 = 0x5D72a9d9A9510Cd8cBdBA12aC62593A58930a948; + address public constant AAVE_V3_POOL_WETH = 0xf1aB7f60128924d69f6d7dE25A20eF70bBd43d07; + address public constant AAVE_V3_POOL_SUSDE = 0xC1A318493fF07a68fE438Cee60a7AD0d0DBa300E; + address public constant AAVE_V3_POOL_WEETH = 0xAf1a7a488c8348b41d5860C04162af7d3D38A996; + address public constant AAVE_V3_POOL_USDE = 0x7519403E12111ff6b710877Fcd821D0c12CAF43A; + + // DEX + address internal constant POOL_OKU_TRADE_USDT0_WETH = 0xCe4Ac514CA6a9db357CcCc105B7848d7fd37445d; + address internal constant POOL_CURVE_USDE_USDT0 = 0x2D84D79C852f6842AbE0304b70bBaA1506AdD457; + address internal constant POOL_CURVE_SUSDE_USDT0 = 0x1E8D78e9b3f0152D54d32904B7933f1cFE439Df1; + address internal constant POOL_WXPL_USDT0 = 0x8603C67B7Cc056ef6981a9C709854c53b699Fa66; + // address internal constant POOL_USDE_USDT0 = 0x01b968C1b663C3921Da5BE3C99Ee3c9B89a40B54; + + // Wrapped AAVE tokens + address public constant TOKEN_WAPLAWETH = 0xa047fdFb3420A27a5f926735b475fE5a1E968786; + address public constant TOKEN_WAPLAUSDT0 = 0xE0126F0c4451B2B917064A93040fd4770D6774b5; + /// @notice aPlaUSDe, see https://app.merkl.xyz/opportunities/plasma/MULTILOG_DUTCH/0x0e4366ce92ab4e9b011f77234922b1a04a9b6ec8BORROW_BL + address public constant TOKEN_WAPLAUSDE = 0x63dC02BB25E7BF7Eaa0E42E71D785a388AcD740b; + + address public constant POOL_SOLIDLY_WETH_WEETH = 0x355705857c9548E71E866087b01bB5b0A1fd671b; + } diff --git a/chains/plasma/PlasmaFarmMakerLib.sol b/chains/plasma/PlasmaFarmMakerLib.sol index 8f4226fee..748c47e99 100644 --- a/chains/plasma/PlasmaFarmMakerLib.sol +++ b/chains/plasma/PlasmaFarmMakerLib.sol @@ -24,4 +24,4 @@ library PlasmaFarmMakerLib { } function testFarmMakerLib() external {} -} +} \ No newline at end of file diff --git a/chains/plasma/PlasmaLib.sol b/chains/plasma/PlasmaLib.sol index c8c7dda35..1c7b57b78 100644 --- a/chains/plasma/PlasmaLib.sol +++ b/chains/plasma/PlasmaLib.sol @@ -18,6 +18,7 @@ import {Proxy} from "../../src/core/proxy/Proxy.sol"; import {StrategyDeveloperLib} from "../../src/strategies/libs/StrategyDeveloperLib.sol"; import {StrategyIdLib} from "../../src/strategies/libs/StrategyIdLib.sol"; import {VaultTypeLib} from "../../src/core/libs/VaultTypeLib.sol"; +import {AaveLeverageMerklFarmStrategy} from "../../src/strategies/AaveLeverageMerklFarmStrategy.sol"; library PlasmaLib { function platformDeployParams() internal pure returns (IPlatformDeployer.DeployPlatformParams memory p) { @@ -57,6 +58,10 @@ library PlasmaLib { DeployAdapterLib.deployAmmAdapter(platform, AmmAdapterIdLib.BALANCER_V3_RECLAMM); IBalancerAdapter(IPlatform(platform).ammAdapter(keccak256(bytes(AmmAdapterIdLib.BALANCER_V3_RECLAMM))).proxy) .setupHelpers(PlasmaConstantsLib.BALANCER_V3_ROUTER); + DeployAdapterLib.deployAmmAdapter(platform, AmmAdapterIdLib.UNISWAPV3); + IBalancerAdapter(DeployAdapterLib.deployAmmAdapter(platform, AmmAdapterIdLib.BALANCER_V3_STABLE)).setupHelpers(PlasmaConstantsLib.BALANCER_V3_ROUTER); + DeployAdapterLib.deployAmmAdapter(platform, AmmAdapterIdLib.CURVE); + DeployAdapterLib.deployAmmAdapter(platform, AmmAdapterIdLib.ERC_4626); //endregion -- Deploy AMM adapters ---- //region ----- Setup Swapper ----- @@ -73,6 +78,7 @@ library PlasmaLib { //region ----- Deploy strategies ----- factory.setStrategyImplementation(StrategyIdLib.AAVE_MERKL_FARM, address(new AaveMerklFarmStrategy())); + factory.setStrategyImplementation(StrategyIdLib.AAVE_LEVERAGE_MERKL_FARM, address(new AaveLeverageMerklFarmStrategy())); //endregion //region ----- Add DeX aggregators ----- @@ -80,7 +86,7 @@ library PlasmaLib { } function routes() public pure returns (ISwapper.AddPoolData[] memory pools) { - pools = new ISwapper.AddPoolData[](2); + pools = new ISwapper.AddPoolData[](6); uint i; pools[i++] = _makePoolData( PlasmaConstantsLib.POOL_BALANCER_V3_RECLAMM_WXPL_USDT0, @@ -94,6 +100,30 @@ library PlasmaLib { PlasmaConstantsLib.TOKEN_WXPL, PlasmaConstantsLib.TOKEN_USDT0 ); + pools[i++] = _makePoolData( + PlasmaConstantsLib.POOL_OKU_TRADE_USDT0_WETH, + AmmAdapterIdLib.UNISWAPV3, + PlasmaConstantsLib.TOKEN_WETH, + PlasmaConstantsLib.TOKEN_USDT0 + ); + pools[i++] = _makePoolData( + PlasmaConstantsLib.POOL_BALANCER_V3_STABLE_WETH_WEETH, + AmmAdapterIdLib.BALANCER_V3_STABLE, + PlasmaConstantsLib.TOKEN_WEETH, + PlasmaConstantsLib.TOKEN_WAPLAWETH + ); + pools[i++] = _makePoolData( + PlasmaConstantsLib.POOL_CURVE_SUSDE_USDT0, + AmmAdapterIdLib.CURVE, + PlasmaConstantsLib.TOKEN_SUSDE, + PlasmaConstantsLib.TOKEN_USDT0 + ); + pools[i++] = _makePoolData( + PlasmaConstantsLib.POOL_CURVE_USDE_USDT0, + AmmAdapterIdLib.CURVE, + PlasmaConstantsLib.TOKEN_USDE, + PlasmaConstantsLib.TOKEN_USDT0 + ); } function farms() public pure returns (IFactory.Farm[] memory _farms) { diff --git a/chains/shared/SharedFarmMarketLib.sol b/chains/shared/SharedFarmMarketLib.sol new file mode 100644 index 000000000..d4acec504 --- /dev/null +++ b/chains/shared/SharedFarmMarketLib.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {StrategyIdLib} from "../../src/strategies/libs/StrategyIdLib.sol"; +import {IFactory} from "../../src/interfaces/IFactory.sol"; +import {SiloManagedMerklFarmStrategy} from "../../src/strategies/SiloManagedMerklFarmStrategy.sol"; + +/// @notice Shared implementation of farms +library SharedFarmMakerLib { + /// @notice Creates Aave Leverage Merkl Farm configuration + /// @param aTokenCollateral Address of aToken used as collateral + /// @param aTokenBorrow Address of aToken used as borrowed asset + /// @param flashLoanVault Address of the vault used for flash loans + /// @param rewardAssets Array of reward token addresses + /// @param minTargetLtv Minimum target loan-to-value ratio (LTV) for leverage management, 85_00 = 0.85 + /// @param maxTargetLtv Maximum target loan-to-value ratio (LTV) for leverage management, 85_00 = 0.85 + /// @param flashLoanKind Type of flash loan to be used (see ILeverageLendingStrategy.FlashLoanKind) + /// @param eModeCategoryId EMode category ID for the farm (optional, can be 0) + /// @param revenueBaseAssetIndex Index of the asset for share price calculations. 0 - collateral asset, 1 - borrow asset + function _makeAaveLeverageMerklFarm( + address aTokenCollateral, + address aTokenBorrow, + address flashLoanVault, + address[] memory rewardAssets, + uint minTargetLtv, + uint maxTargetLtv, + uint flashLoanKind, + uint8 eModeCategoryId, + uint8 revenueBaseAssetIndex + ) internal pure returns (IFactory.Farm memory) { + IFactory.Farm memory farm; + farm.status = 0; + farm.strategyLogicId = StrategyIdLib.AAVE_LEVERAGE_MERKL_FARM; + farm.rewardAssets = rewardAssets; + + farm.addresses = new address[](3); + farm.addresses[0] = aTokenCollateral; + farm.addresses[1] = aTokenBorrow; + farm.addresses[2] = flashLoanVault; + + farm.nums = new uint[](5); + farm.nums[0] = minTargetLtv; + farm.nums[1] = maxTargetLtv; + farm.nums[2] = flashLoanKind; + farm.nums[3] = eModeCategoryId; + farm.nums[4] = revenueBaseAssetIndex; + + return farm; + } + +} \ No newline at end of file diff --git a/chains/sonic/SonicConstantsLib.sol b/chains/sonic/SonicConstantsLib.sol index 4c1cfcf64..d6956cfa3 100644 --- a/chains/sonic/SonicConstantsLib.sol +++ b/chains/sonic/SonicConstantsLib.sol @@ -533,6 +533,13 @@ library SonicConstantsLib { address public constant BRUNCH_GEN2_ATOKEN_SBUSD = 0xeB9bB589C12A0433B274760E657D549a6973C787; address public constant BRUNCH_GEN2_ATOKEN_USDC = 0x958d930E61bdaebbBc0270D88FdBAEE9A13Dc6fd; + // -------------------------- STBL-USDC isolated market + address public constant STABILITY_STBL_USDC_MARKET_ADDRESS_PROVIDER = 0x234888531C4a6AFeF6935DD7DB9F9D5325b68715; + address public constant STABILITY_STBL_USDC_MARKET_POOL = 0xb0A06303085aB2F73212C8846CA5388Da5697c31; + address public constant STABILITY_STBL_USDC_MARKET_TOKEN_STBL = 0x00886bC6a12d8D5ad0ef51e041a8AB37A0E59251; + address public constant STABILITY_STBL_USDC_MARKET_TOKEN_USDC = 0x46b2E96725F03873Cb586a7f84c22545F2835F31; + + // -------------------------- Shadow // address public constant SHADOW_NFT = 0x12E66C8F215DdD5d48d150c8f46aD0c6fB0F4406; address public constant SHADOW_ROUTER = 0x1D368773735ee1E678950B7A97bcA2CafB330CDc; diff --git a/chains/sonic/SonicLib.sol b/chains/sonic/SonicLib.sol index bfa22ea50..5b3dd4f72 100644 --- a/chains/sonic/SonicLib.sol +++ b/chains/sonic/SonicLib.sol @@ -46,6 +46,7 @@ import {IPriceAggregator} from "../../src/interfaces/IPriceAggregator.sol"; import {EulerMerklFarmStrategy} from "../../src/strategies/EulerMerklFarmStrategy.sol"; import {SiloManagedMerklFarmStrategy} from "../../src/strategies/SiloManagedMerklFarmStrategy.sol"; import {SiloMerklFarmStrategy} from "../../src/strategies/SiloMerklFarmStrategy.sol"; +import {AaveLeverageMerklFarmStrategy} from "../../src/strategies/AaveLeverageMerklFarmStrategy.sol"; /// @dev Sonic network [chainId: 146] data library // _____ _ @@ -245,6 +246,7 @@ library SonicLib { factory.setStrategyImplementation(StrategyIdLib.COMPOUND_V2, address(new CompoundV2Strategy())); factory.setStrategyImplementation(StrategyIdLib.SILO_MANAGED_MERKL_FARM, address(new SiloManagedMerklFarmStrategy())); factory.setStrategyImplementation(StrategyIdLib.SILO_MERKL_FARM, address(new SiloMerklFarmStrategy())); + factory.setStrategyImplementation(StrategyIdLib.AAVE_LEVERAGE_MERKL_FARM, address(new AaveLeverageMerklFarmStrategy())); LogDeployLib.logDeployStrategies(platform, showLog); //endregion diff --git a/guides/AllDeployments.md b/guides/AllDeployments.md index 8eff5d9b8..1e8375798 100644 --- a/guides/AllDeployments.md +++ b/guides/AllDeployments.md @@ -99,6 +99,15 @@ ### Periphery * **Frontend** `0x70e804364175e23F1c30dFa03BFb19d936E5E81c` [plasmascan](https://plasmascan.to/address/0x70e804364175e23F1c30dFa03BFb19d936E5E81c) +* **UpgradeHelper** `0x932D7bd758214fDF58f7824c09503D9FcD36C089` [plasmascan](https://plasmascan.to/address/0x932D7bd758214fDF58f7824c09503D9FcD36C089) + +### AMM adapters +* **BalancerV3ReCLAMM** `0x54Ea393aAc117d67B913F1cC63Df143761519A63` [plasmascan](https://plasmascan.to/address/0x54Ea393aAc117d67B913F1cC63Df143761519A63) +* **BalancerV3Stable** `0xc73DBCFFADd43F87c0a37d39FA1460F5D80E3bca` [plasmascan](https://plasmascan.to/address/0xc73DBCFFADd43F87c0a37d39FA1460F5D80E3bca) +* **Curve** `0xF93Ee7e1B1C5679974a004ce59DBdECEcB45475e` [plasmascan](https://plasmascan.to/address/0xF93Ee7e1B1C5679974a004ce59DBdECEcB45475e) +* **ERC4626** `0x63f83f6807C77FA33ccA12D9422B912C78f6F8ed` [plasmascan](https://plasmascan.to/address/0x63f83f6807C77FA33ccA12D9422B912C78f6F8ed) +* **UniswapV3** `0xd901ceCbcD3493C98c9569ea7a41295a180333de` [plasmascan](https://plasmascan.to/address/0xd901ceCbcD3493C98c9569ea7a41295a180333de) +* **Solidly** `0x35CcD976BD574fEa00b97d58095bB8e90D26a455` [plasmascan](https://plasmascan.to/address/0x35CcD976BD574fEa00b97d58095bB8e90D26a455) ## Polygon [137] diff --git a/lib/forge-std b/lib/forge-std index 8bbcf6e3f..7117c90c8 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 8bbcf6e3f8f62f419e5429a0bd89331c85c37824 +Subproject commit 7117c90c8cf6c68e5acce4f09a6b24715cea4de6 diff --git a/script/deploy-adapter/BalancerV3Stable.Plasma.s.sol b/script/deploy-adapter/BalancerV3Stable.Plasma.s.sol new file mode 100644 index 000000000..828e6ba5a --- /dev/null +++ b/script/deploy-adapter/BalancerV3Stable.Plasma.s.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Script} from "forge-std/Script.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {BalancerV3StableAdapter} from "../../src/adapters/BalancerV3StableAdapter.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; + +contract DeployBalancerV3StableAdapterPlasma is Script { + address public constant PLATFORM = PlasmaConstantsLib.PLATFORM; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + Proxy proxy = new Proxy(); + proxy.initProxy(address(new BalancerV3StableAdapter())); + BalancerV3StableAdapter(address(proxy)).init(PLATFORM); + vm.stopBroadcast(); + } + + function testDeployAdapter() external {} +} diff --git a/script/deploy-adapter/Curve.Plasma.s.sol b/script/deploy-adapter/Curve.Plasma.s.sol new file mode 100644 index 000000000..a7958185f --- /dev/null +++ b/script/deploy-adapter/Curve.Plasma.s.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Script} from "forge-std/Script.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {CurveAdapter} from "../../src/adapters/CurveAdapter.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; + +contract DeployCurveAdapterPlasma is Script { + address public constant PLATFORM = PlasmaConstantsLib.PLATFORM; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + Proxy proxy = new Proxy(); + proxy.initProxy(address(new CurveAdapter())); + CurveAdapter(address(proxy)).init(PLATFORM); + vm.stopBroadcast(); + } + + function testDeployAdapter() external {} +} diff --git a/script/deploy-adapter/ERC4626.Plasma.s.sol b/script/deploy-adapter/ERC4626.Plasma.s.sol new file mode 100644 index 000000000..881a4aa42 --- /dev/null +++ b/script/deploy-adapter/ERC4626.Plasma.s.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Script} from "forge-std/Script.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {ERC4626Adapter} from "../../src/adapters/ERC4626Adapter.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; + +contract DeployERC4626AdapterPlasma is Script { + address public constant PLATFORM = PlasmaConstantsLib.PLATFORM; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + Proxy proxy = new Proxy(); + proxy.initProxy(address(new ERC4626Adapter())); + ERC4626Adapter(address(proxy)).init(PLATFORM); + vm.stopBroadcast(); + } + + function testDeployAdapter() external {} +} diff --git a/script/deploy-adapter/Solidly.Plasma.s.sol b/script/deploy-adapter/Solidly.Plasma.s.sol new file mode 100644 index 000000000..fa06208ef --- /dev/null +++ b/script/deploy-adapter/Solidly.Plasma.s.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Script} from "forge-std/Script.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {SolidlyAdapter} from "../../src/adapters/SolidlyAdapter.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; + +contract DeploySolidlyAdapterSonic is Script { + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + Proxy proxy = new Proxy(); + proxy.initProxy(address(new SolidlyAdapter())); + SolidlyAdapter(address(proxy)).init(PlasmaConstantsLib.PLATFORM); + vm.stopBroadcast(); + } + + function testDeployAdapter() external {} +} diff --git a/script/deploy-adapter/UniswapV3Adapter.Plasma.s.sol b/script/deploy-adapter/UniswapV3Adapter.Plasma.s.sol new file mode 100644 index 000000000..8ab2d71e2 --- /dev/null +++ b/script/deploy-adapter/UniswapV3Adapter.Plasma.s.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Script} from "forge-std/Script.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {UniswapV3Adapter} from "../../src/adapters/UniswapV3Adapter.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; + +contract DeployUniswapV3AdapterPlasma is Script { + address public constant PLATFORM = PlasmaConstantsLib.PLATFORM; + + function run() external { + uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + Proxy proxy = new Proxy(); + proxy.initProxy(address(new UniswapV3Adapter())); + UniswapV3Adapter(address(proxy)).init(PLATFORM); + + vm.stopBroadcast(); + } + + function testDeployAdapter() external {} +} diff --git a/script/deploy-periphery/UpgradeHelper.Sonic.s.sol b/script/deploy-strategy/ALMF.s.sol similarity index 50% rename from script/deploy-periphery/UpgradeHelper.Sonic.s.sol rename to script/deploy-strategy/ALMF.s.sol index 8c0a5c3e4..5ed042253 100644 --- a/script/deploy-periphery/UpgradeHelper.Sonic.s.sol +++ b/script/deploy-strategy/ALMF.s.sol @@ -2,17 +2,15 @@ pragma solidity ^0.8.28; import {Script} from "forge-std/Script.sol"; -import {UpgradeHelper} from "../../src/periphery/UpgradeHelper.sol"; - -contract DeployFrontendSonic is Script { - address public constant PLATFORM = 0x4Aca671A420eEB58ecafE83700686a2AD06b20D8; +import {AaveLeverageMerklFarmStrategy} from "../../src/strategies/AaveLeverageMerklFarmStrategy.sol"; +contract DeployALMF is Script { function run() external { uint deployerPrivateKey = vm.envUint("PRIVATE_KEY"); vm.startBroadcast(deployerPrivateKey); - new UpgradeHelper(PLATFORM); + new AaveLeverageMerklFarmStrategy(); vm.stopBroadcast(); } - function testDeployPeriphery() external {} + function testDeployStrategy() external {} } diff --git a/src/adapters/AaveV3Adapter.sol b/src/adapters/AaveV3Adapter.sol new file mode 100644 index 000000000..ce5423ee3 --- /dev/null +++ b/src/adapters/AaveV3Adapter.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +// import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IControllable} from "../interfaces/IControllable.sol"; +import {IAmmAdapter} from "../interfaces/IAmmAdapter.sol"; +import {Controllable} from "../core/base/Controllable.sol"; +import {AmmAdapterIdLib} from "./libs/AmmAdapterIdLib.sol"; +import {IPool} from "../integrations/aave/IPool.sol"; +import {IAToken} from "../integrations/aave/IAToken.sol"; + +/// @title Adapter to wrap/unwrap AToken +/// @author omriss (https://github.com/omriss) +/// Changelog: +contract AaveV3Adapter is Controllable, IAmmAdapter { + using SafeERC20 for IERC20; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IControllable + string public constant VERSION = "1.0.0"; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IAmmAdapter + function init(address platform_) external initializer { + __Controllable_init(platform_); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* USER ACTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IAmmAdapter + //slither-disable-next-line reentrancy-events + /// @dev pool is the AToken address + function swap( + address atoken, + address tokenIn, + address tokenOut, + address recipient, + uint priceImpactTolerance + ) external { + uint amountIn = IERC20(tokenIn).balanceOf(address(this)); + uint amountOut; + + IPool pool = IPool(IAToken(atoken).POOL()); + if (atoken == tokenIn) { + amountOut = pool.withdraw(tokenOut, amountIn, recipient); + } else { + IERC20(tokenIn).forceApprove(address(pool), amountIn); + pool.supply(tokenIn, amountIn, recipient, 0); + // AToken is rebased token - 1:1, same decimals + amountOut = amountIn; + } + + emit SwapInPool(atoken, tokenIn, tokenOut, recipient, priceImpactTolerance, amountIn, amountOut); + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* VIEW FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IAmmAdapter + function ammAdapterId() external pure returns (string memory) { + return AmmAdapterIdLib.AAVE_V3; + } + + /// @inheritdoc IAmmAdapter + /// @dev pool is the AToken address + function poolTokens(address pool) public view returns (address[] memory tokens) { + tokens = new address[](2); + tokens[0] = IAToken(pool).UNDERLYING_ASSET_ADDRESS(); + tokens[1] = pool; + } + + /// @inheritdoc IAmmAdapter + function getLiquidityForAmounts(address, uint[] memory) external pure returns (uint, uint[] memory) { + revert("Not supported"); + } + + /// @inheritdoc IAmmAdapter + function getProportions(address) external pure returns (uint[] memory) { + revert("Not supported"); + } + + /// @inheritdoc IAmmAdapter + /// @dev pool is the AToken address + function getPrice(address pool, address tokenIn, address tokenOut, uint amount) public pure returns (uint) { + return tokenIn == pool || tokenOut == pool + // atoken is rebase token, so 1:1 price + ? amount + : 0; + } + + /// @inheritdoc IAmmAdapter + function getTwaPrice( + address, + /*pool*/ + address, + /*tokenIn*/ + address, + /*tokenOut*/ + uint, + /*amount*/ + uint32 /*period*/ + ) external pure returns (uint) { + revert("Not supported"); + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view override(Controllable, IERC165) returns (bool) { + return interfaceId == type(IAmmAdapter).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/src/adapters/libs/AmmAdapterIdLib.sol b/src/adapters/libs/AmmAdapterIdLib.sol index 60c3567e7..b201c6ac0 100644 --- a/src/adapters/libs/AmmAdapterIdLib.sol +++ b/src/adapters/libs/AmmAdapterIdLib.sol @@ -16,4 +16,5 @@ library AmmAdapterIdLib { string public constant META_VAULT = "MetaUSD"; string public constant BALANCER_V3_RECLAMM = "BalancerV3ReCLAMM"; string public constant BRUNCH = "Brunch"; + string public constant AAVE_V3 = "AaveV3"; } diff --git a/src/integrations/aave/IPool.sol b/src/integrations/aave/IPool.sol index d6e96774e..f7b6402b7 100644 --- a/src/integrations/aave/IPool.sol +++ b/src/integrations/aave/IPool.sol @@ -168,6 +168,28 @@ interface IPool { function deposit(address asset, uint amount, address onBehalfOf, uint16 referralCode) external; struct ReserveConfigurationMap { + //bit 0-15: LTV + //bit 16-31: Liq. threshold + //bit 32-47: Liq. bonus + //bit 48-55: Decimals + //bit 56: reserve is active + //bit 57: reserve is frozen + //bit 58: borrowing is enabled + //bit 59: DEPRECATED: stable rate borrowing enabled + //bit 60: asset is paused + //bit 61: borrowing in isolation mode is enabled + //bit 62: siloed borrowing enabled + //bit 63: flashloaning enabled + //bit 64-79: reserve factor + //bit 80-115: borrow cap in whole tokens, borrowCap == 0 => no cap + //bit 116-151: supply cap in whole tokens, supplyCap == 0 => no cap + //bit 152-167: liquidation protocol fee + //bit 168-175: DEPRECATED: eMode category + //bit 176-211: unbacked mint cap in whole tokens, unbackedMintCap == 0 => minting disabled + //bit 212-251: debt ceiling for isolation mode with (ReserveConfiguration::DEBT_CEILING_DECIMALS) decimals + //bit 252: virtual accounting is enabled for the reserve + //bit 253-255 unused + uint256 data; } diff --git a/src/integrations/aave31/IAavePoolConfigurator31.sol b/src/integrations/aave31/IAavePoolConfigurator31.sol new file mode 100644 index 000000000..c905066c5 --- /dev/null +++ b/src/integrations/aave31/IAavePoolConfigurator31.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +interface IAavePoolConfigurator31 { + struct InitReserveInput { + address aTokenImpl; + address variableDebtTokenImpl; + address underlyingAsset; + string aTokenName; + string aTokenSymbol; + string variableDebtTokenName; + string variableDebtTokenSymbol; + bytes params; + bytes interestRateData; + } + + struct UpdateATokenInput { + address asset; + string name; + string symbol; + address implementation; + bytes params; + } + + struct UpdateDebtTokenInput { + address asset; + string name; + string symbol; + address implementation; + bytes params; + } + + + function CONFIGURATOR_REVISION() external view returns (uint256); + + function MAX_GRACE_PERIOD() external view returns (uint40); + + function configureReserveAsCollateral( + address asset, + uint256 ltv, + uint256 liquidationThreshold, + uint256 liquidationBonus + ) external; + + function disableLiquidationGracePeriod(address asset) external; + + function dropReserve(address asset) external; + + function getConfiguratorLogic() external pure returns (address); + + function getPendingLtv(address asset) external view returns (uint256); + + function initReserves( + InitReserveInput[] memory input + ) external; + + function initialize(address provider) external; + + function setAssetBorrowableInEMode( + address asset, + uint8 categoryId, + bool borrowable + ) external; + + function setAssetCollateralInEMode( + address asset, + uint8 categoryId, + bool allowed + ) external; + + function setBorrowCap(address asset, uint256 newBorrowCap) external; + + function setBorrowableInIsolation(address asset, bool borrowable) external; + + function setDebtCeiling(address asset, uint256 newDebtCeiling) external; + + function setEModeCategory( + uint8 categoryId, + uint16 ltv, + uint16 liquidationThreshold, + uint16 liquidationBonus, + string memory label + ) external; + + function setLiquidationProtocolFee(address asset, uint256 newFee) external; + + function setPoolPause(bool paused, uint40 gracePeriod) external; + + function setPoolPause(bool paused) external; + + function setReserveActive(address asset, bool active) external; + + function setReserveBorrowing(address asset, bool enabled) external; + + function setReserveFactor(address asset, uint256 newReserveFactor) external; + + function setReserveFlashLoaning(address asset, bool enabled) external; + + function setReserveFreeze(address asset, bool freeze) external; + + function setReserveInterestRateData(address asset, bytes memory rateData) external; + + function setReservePause(address asset, bool paused) external; + + function setReservePause( + address asset, + bool paused, + uint40 gracePeriod + ) external; + + function setSiloedBorrowing(address asset, bool newSiloed) external; + + function setSupplyCap(address asset, uint256 newSupplyCap) external; + + function updateAToken(UpdateATokenInput memory input) + external; + + function updateFlashloanPremium(uint128 newFlashloanPremium) external; + + function updateVariableDebtToken( + UpdateDebtTokenInput memory input + ) external; +} \ No newline at end of file diff --git a/src/integrations/aave32/IPool32.sol b/src/integrations/aave32/IPool32.sol new file mode 100644 index 000000000..dd5de6929 --- /dev/null +++ b/src/integrations/aave32/IPool32.sol @@ -0,0 +1,444 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +interface IAavePool32 { + error AddressEmptyCode(address target); + error AssetNotListed(); + error CallerNotAToken(); + error CallerNotPoolAdmin(); + error CallerNotPoolConfigurator(); + error CallerNotPositionManager(); + error CallerNotUmbrella(); + error EModeCategoryReserved(); + error FailedCall(); + error InvalidAddressesProvider(); + error ZeroAddressNotValid(); + event Borrow( + address indexed reserve, + address user, + address indexed onBehalfOf, + uint256 amount, + uint8 interestRateMode, + uint256 borrowRate, + uint16 indexed referralCode + ); + event DeficitCovered( + address indexed reserve, + address caller, + uint256 amountCovered + ); + event DeficitCreated( + address indexed user, + address indexed debtAsset, + uint256 amountCreated + ); + event FlashLoan( + address indexed target, + address initiator, + address indexed asset, + uint256 amount, + uint8 interestRateMode, + uint256 premium, + uint16 indexed referralCode + ); + event IsolationModeTotalDebtUpdated( + address indexed asset, + uint256 totalDebt + ); + event LiquidationCall( + address indexed collateralAsset, + address indexed debtAsset, + address indexed user, + uint256 debtToCover, + uint256 liquidatedCollateralAmount, + address liquidator, + bool receiveAToken + ); + event MintedToTreasury(address indexed reserve, uint256 amountMinted); + event PositionManagerApproved( + address indexed user, + address indexed positionManager + ); + event PositionManagerRevoked( + address indexed user, + address indexed positionManager + ); + event Repay( + address indexed reserve, + address indexed user, + address indexed repayer, + uint256 amount, + bool useATokens + ); + event ReserveDataUpdated( + address indexed reserve, + uint256 liquidityRate, + uint256 stableBorrowRate, + uint256 variableBorrowRate, + uint256 liquidityIndex, + uint256 variableBorrowIndex + ); + event ReserveUsedAsCollateralDisabled( + address indexed reserve, + address indexed user + ); + event ReserveUsedAsCollateralEnabled( + address indexed reserve, + address indexed user + ); + event Supply( + address indexed reserve, + address user, + address indexed onBehalfOf, + uint256 amount, + uint16 indexed referralCode + ); + event UserEModeSet(address indexed user, uint8 categoryId); + event Withdraw( + address indexed reserve, + address indexed user, + address indexed to, + uint256 amount + ); + + function ADDRESSES_PROVIDER() external view returns (address); + + function FLASHLOAN_PREMIUM_TOTAL() external view returns (uint128); + + function FLASHLOAN_PREMIUM_TO_PROTOCOL() external view returns (uint128); + + function MAX_NUMBER_RESERVES() external view returns (uint16); + + function POOL_REVISION() external view returns (uint256); + + function RESERVE_INTEREST_RATE_STRATEGY() external view returns (address); + + function UMBRELLA() external view returns (bytes32); + + function approvePositionManager(address positionManager, bool approve) + external; + + function borrow( + address asset, + uint256 amount, + uint256 interestRateMode, + uint16 referralCode, + address onBehalfOf + ) external; + + function configureEModeCategory( + uint8 id, + EModeCategoryBaseConfiguration memory category + ) external; + + function configureEModeCategoryBorrowableBitmap( + uint8 id, + uint128 borrowableBitmap + ) external; + + function configureEModeCategoryCollateralBitmap( + uint8 id, + uint128 collateralBitmap + ) external; + + function deposit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) external; + + function dropReserve(address asset) external; + + function eliminateReserveDeficit(address asset, uint256 amount) + external + returns (uint256); + + function finalizeTransfer( + address asset, + address from, + address to, + uint256 scaledAmount, + uint256 scaledBalanceFromBefore, + uint256 scaledBalanceToBefore + ) external; + + function flashLoan( + address receiverAddress, + address[] memory assets, + uint256[] memory amounts, + uint256[] memory interestRateModes, + address onBehalfOf, + bytes memory params, + uint16 referralCode + ) external; + + function flashLoanSimple( + address receiverAddress, + address asset, + uint256 amount, + bytes memory params, + uint16 referralCode + ) external; + + function getBorrowLogic() external pure returns (address); + + function getConfiguration(address asset) + external + view + returns (ReserveConfigurationMap memory); + + function getEModeCategoryBorrowableBitmap(uint8 id) + external + view + returns (uint128); + + function getEModeCategoryCollateralBitmap(uint8 id) + external + view + returns (uint128); + + function getEModeCategoryCollateralConfig(uint8 id) + external + view + returns (CollateralConfig memory res); + + function getEModeCategoryData(uint8 id) + external + view + returns (EModeCategoryLegacy memory); + + function getEModeCategoryLabel(uint8 id) + external + view + returns (string memory); + + function getEModeLogic() external pure returns (address); + + function getFlashLoanLogic() external pure returns (address); + + function getLiquidationGracePeriod(address asset) + external + view + returns (uint40); + + function getLiquidationLogic() external pure returns (address); + + function getPoolLogic() external pure returns (address); + + function getReserveAToken(address asset) external view returns (address); + + function getReserveAddressById(uint16 id) external view returns (address); + + function getReserveData(address asset) + external + view + returns (ReserveDataLegacy memory res); + + function getReserveDeficit(address asset) external view returns (uint256); + + function getReserveNormalizedIncome(address asset) + external + view + returns (uint256); + + function getReserveNormalizedVariableDebt(address asset) + external + view + returns (uint256); + + function getReserveVariableDebtToken(address asset) + external + view + returns (address); + + function getReservesCount() external view returns (uint256); + + function getReservesList() external view returns (address[] memory); + + function getSupplyLogic() external pure returns (address); + + function getUserAccountData(address user) + external + view + returns ( + uint256 totalCollateralBase, + uint256 totalDebtBase, + uint256 availableBorrowsBase, + uint256 currentLiquidationThreshold, + uint256 ltv, + uint256 healthFactor + ); + + function getUserConfiguration(address user) + external + view + returns (UserConfigurationMap memory); + + function getUserEMode(address user) external view returns (uint256); + + function getVirtualUnderlyingBalance(address asset) + external + view + returns (uint128); + + function initReserve( + address asset, + address aTokenAddress, + address variableDebtAddress + ) external; + + function initialize(address provider) external; + + function isApprovedPositionManager(address user, address positionManager) + external + view + returns (bool); + + function liquidationCall( + address collateralAsset, + address debtAsset, + address borrower, + uint256 debtToCover, + bool receiveAToken + ) external; + + function mintToTreasury(address[] memory assets) external; + + function multicall(bytes[] memory data) + external + returns (bytes[] memory results); + + function renouncePositionManagerRole(address user) external; + + function repay( + address asset, + uint256 amount, + uint256 interestRateMode, + address onBehalfOf + ) external returns (uint256); + + function repayWithATokens( + address asset, + uint256 amount, + uint256 interestRateMode + ) external returns (uint256); + + function repayWithPermit( + address asset, + uint256 amount, + uint256 interestRateMode, + address onBehalfOf, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external returns (uint256); + + function rescueTokens( + address token, + address to, + uint256 amount + ) external; + + function resetIsolationModeTotalDebt(address asset) external; + + function setConfiguration( + address asset, + ReserveConfigurationMap memory configuration + ) external; + + function setLiquidationGracePeriod(address asset, uint40 until) external; + + function setUserEMode(uint8 categoryId) external; + + function setUserEModeOnBehalfOf(uint8 categoryId, address onBehalfOf) + external; + + function setUserUseReserveAsCollateral(address asset, bool useAsCollateral) + external; + + function setUserUseReserveAsCollateralOnBehalfOf( + address asset, + bool useAsCollateral, + address onBehalfOf + ) external; + + function supply( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode + ) external; + + function supplyWithPermit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external; + + function syncIndexesState(address asset) external; + + function syncRatesState(address asset) external; + + function updateFlashloanPremium(uint128 flashLoanPremium) external; + + function withdraw( + address asset, + uint256 amount, + address to + ) external returns (uint256); + + struct EModeCategoryBaseConfiguration { + uint16 ltv; + uint16 liquidationThreshold; + uint16 liquidationBonus; + string label; + } + + struct ReserveConfigurationMap { + uint256 data; + } + + struct CollateralConfig { + uint16 ltv; + uint16 liquidationThreshold; + uint16 liquidationBonus; + } + + struct EModeCategoryLegacy { + uint16 ltv; + uint16 liquidationThreshold; + uint16 liquidationBonus; + address priceSource; + string label; + } + + struct ReserveDataLegacy { + ReserveConfigurationMap configuration; + uint128 liquidityIndex; + uint128 currentLiquidityRate; + uint128 variableBorrowIndex; + uint128 currentVariableBorrowRate; + uint128 currentStableBorrowRate; + uint40 lastUpdateTimestamp; + uint16 id; + address aTokenAddress; + address stableDebtTokenAddress; + address variableDebtTokenAddress; + address interestRateStrategyAddress; + uint128 accruedToTreasury; + uint128 unbacked; + uint128 isolationModeTotalDebt; + } + + struct UserConfigurationMap { + uint256 data; + } +} + diff --git a/src/integrations/balancer/IBComposableStablePoolMinimal.sol b/src/integrations/balancer/IBComposableStablePoolMinimal.sol index c53d683e0..d7fb8281a 100644 --- a/src/integrations/balancer/IBComposableStablePoolMinimal.sol +++ b/src/integrations/balancer/IBComposableStablePoolMinimal.sol @@ -11,6 +11,7 @@ interface IBComposableStablePoolMinimal { function getScalingFactors() external view returns (uint[] memory); function getBptIndex() external view returns (uint); function getVault() external view returns (address); + function getFlashLoanFeePercentage() external view returns (uint256); function updateTokenRateCache(address token) external; } diff --git a/src/integrations/balancer/IBVault.sol b/src/integrations/balancer/IBVault.sol index c1ca8e8da..90ba665d5 100644 --- a/src/integrations/balancer/IBVault.sol +++ b/src/integrations/balancer/IBVault.sol @@ -588,4 +588,7 @@ interface IBVault { uint[] memory amounts, bytes memory userData ) external; + + /// @dev Returns the current protocol fee module. + function getProtocolFeesCollector() external view returns (address); } diff --git a/src/strategies/AaveLeverageMerklFarmStrategy.sol b/src/strategies/AaveLeverageMerklFarmStrategy.sol new file mode 100644 index 000000000..c7da7d8ad --- /dev/null +++ b/src/strategies/AaveLeverageMerklFarmStrategy.sol @@ -0,0 +1,476 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {ALMFLib} from "./libs/ALMFLib.sol"; +import {ALMFLib2} from "./libs/ALMFLib2.sol"; +import {FarmMechanicsLib} from "./libs/FarmMechanicsLib.sol"; +import {FarmingStrategyBase} from "./base/FarmingStrategyBase.sol"; +import {IAToken} from "../integrations/aave/IAToken.sol"; +import {IAlgebraFlashCallback} from "../integrations/algebrav4/callback/IAlgebraFlashCallback.sol"; +import {IBalancerV3FlashCallback} from "../integrations/balancerv3/IBalancerV3FlashCallback.sol"; +import {IControllable} from "../interfaces/IControllable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IFactory} from "../interfaces/IFactory.sol"; +import {IFarmingStrategy} from "../interfaces/IFarmingStrategy.sol"; +import {IFlashLoanRecipient} from "../integrations/balancer/IFlashLoanRecipient.sol"; +import {IMerklStrategy} from "../interfaces/IMerklStrategy.sol"; +import {ILeverageLendingStrategy} from "../interfaces/ILeverageLendingStrategy.sol"; +import {IStrategy} from "../interfaces/IStrategy.sol"; +import {IUniswapV3FlashCallback} from "../integrations/uniswapv3/IUniswapV3FlashCallback.sol"; +import {LeverageLendingBase} from "./base/LeverageLendingBase.sol"; +import {MerklStrategyBase} from "./base/MerklStrategyBase.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {StrategyBase} from "./base/StrategyBase.sol"; +import {VaultTypeLib} from "../core/libs/VaultTypeLib.sol"; + +/// @title Earns APR by lending assets on AAVE with leverage +/// @dev ALMF strategy +/// Changelog: +/// 1.3.0: New strategy param revenueBaseAsset. 0 - share price is calculated in collateral asset, 1 - in borrow asset +/// 1.2.0: share price is calculated in collateral asset, not in usd +/// 1.1.0: add support of e-mode +/// @author omriss (https://github.com/omriss) +contract AaveLeverageMerklFarmStrategy is + FarmingStrategyBase, + MerklStrategyBase, + LeverageLendingBase, + IFlashLoanRecipient, + IUniswapV3FlashCallback, + IBalancerV3FlashCallback, + IAlgebraFlashCallback +{ + using SafeERC20 for IERC20; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IControllable + string public constant VERSION = "1.3.0"; + + //region ----------------------------------- Initialization and restricted actions + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INITIALIZATION */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IStrategy + function initialize(address[] memory addresses, uint[] memory nums, int24[] memory ticks) public initializer { + if (addresses.length != 2 || nums.length != 1 || ticks.length != 0) { + revert IControllable.IncorrectInitParams(); + } + IFactory.Farm memory farm = _getFarm(addresses[0], nums[0]); + if (farm.addresses.length != 3 || farm.nums.length != 5 || farm.ticks.length != 0) { + revert IFarmingStrategy.BadFarm(); + } + + // slither-disable-next-line uninitialized-local + LeverageLendingStrategyBaseInitParams memory params; + + params.platform = addresses[0]; + params.strategyId = ALMFLib2.STRATEGY_LOGIC_ID; + params.vault = addresses[1]; + params.collateralAsset = + IAToken(farm.addresses[ALMFLib.FARM_ADDRESS_LENDING_VAULT_INDEX]).UNDERLYING_ASSET_ADDRESS(); + params.borrowAsset = + IAToken(farm.addresses[ALMFLib.FARM_ADDRESS_BORROWING_VAULT_INDEX]).UNDERLYING_ASSET_ADDRESS(); + params.lendingVault = farm.addresses[ALMFLib.FARM_ADDRESS_LENDING_VAULT_INDEX]; + params.borrowingVault = farm.addresses[ALMFLib.FARM_ADDRESS_BORROWING_VAULT_INDEX]; + params.flashLoanVault = farm.addresses[ALMFLib.FARM_ADDRESS_FLASH_LOAN_VAULT_INDEX]; + // params.helper = address(0); // not used + // params.targetLeveragePercent = 0; // not used + + __LeverageLendingBase_init(params); // __StrategyBase_init is called inside + __FarmingStrategyBase_init(addresses[0], nums[0]); + + // autoCompoundingByUnderlyingProtocol is false, + // we need to overwrite exchangeAssetIndex written inside __LeverageLendingBase_init + _getStrategyBaseStorage()._exchangeAssetIndex = 0; + + // set up params and approves, switch to e-mode + ALMFLib2._postInit( + _getLeverageLendingBaseStorage(), + params.platform, + params.lendingVault, + params.collateralAsset, + params.borrowAsset, + farm + ); + } + + //endregion ----------------------------------- Initialization and restricted actions + + //region ----------------------------------- Flash loan + + /// @inheritdoc IFlashLoanRecipient + /// @dev Support of FLASH_LOAN_KIND_BALANCER_V2 + function receiveFlashLoan( + address[] memory tokens, + uint[] memory amounts, + uint[] memory feeAmounts, + bytes memory /*userData*/ + ) external { + // Flash loan is performed upon deposit and withdrawal + LeverageLendingBaseStorage storage $ = _getLeverageLendingBaseStorage(); + ALMFLib.receiveFlashLoanBalancerV2(platform(), $, tokens, amounts, feeAmounts); + } + + /// @inheritdoc IBalancerV3FlashCallback + function receiveFlashLoanV3( + address token, + uint amount, + bytes memory /*userData*/ + ) external { + LeverageLendingBaseStorage storage $ = _getLeverageLendingBaseStorage(); + ALMFLib.receiveFlashLoanV3(platform(), $, token, amount); + } + + /// @inheritdoc IUniswapV3FlashCallback + function uniswapV3FlashCallback(uint fee0, uint fee1, bytes calldata userData) external { + LeverageLendingBaseStorage storage $ = _getLeverageLendingBaseStorage(); + ALMFLib.uniswapV3FlashCallback(platform(), $, fee0, fee1, userData); + } + + /// @inheritdoc IAlgebraFlashCallback + function algebraFlashCallback(uint fee0, uint fee1, bytes calldata userData) external { + LeverageLendingBaseStorage storage $ = _getLeverageLendingBaseStorage(); + ALMFLib.uniswapV3FlashCallback(platform(), $, fee0, fee1, userData); + } + + //endregion ----------------------------------- Flash loan + + //region ----------------------------------- View functions + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* VIEW FUNCTIONS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc IStrategy + function isHardWorkOnDepositAllowed() external pure returns (bool) { + return false; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(FarmingStrategyBase, LeverageLendingBase, MerklStrategyBase) + returns (bool) + { + return interfaceId == type(IFarmingStrategy).interfaceId || interfaceId == type(IMerklStrategy).interfaceId + || interfaceId == type(ILeverageLendingStrategy).interfaceId || super.supportsInterface(interfaceId); + } + + /// @inheritdoc IStrategy + function strategyLogicId() public pure override returns (string memory) { + return ALMFLib2.STRATEGY_LOGIC_ID; + } + + /// @inheritdoc IStrategy + function description() external view returns (string memory) { + return ALMFLib2.genDesc(_getFarm()); + } + + /// @inheritdoc IStrategy + function getSpecificName() external view override returns (string memory name, bool showInVaultSymbol) { + (name, showInVaultSymbol) = ALMFLib2.getSpecificName(_getLeverageLendingBaseStorage(), _getFarm()); + } + + /// @inheritdoc IStrategy + function supportedVaultTypes() + external + pure + override(LeverageLendingBase, StrategyBase) + returns (string[] memory types) + { + types = new string[](1); + types[0] = VaultTypeLib.COMPOUNDING; + } + + /// @inheritdoc IStrategy + function initVariants(address platform_) + external + view + returns (string[] memory variants, address[] memory addresses, uint[] memory nums, int24[] memory ticks) + { + (variants, addresses, nums, ticks) = ALMFLib2.initVariants(platform_); + } + + /// @inheritdoc IStrategy + function total() public view override returns (uint) { + return ALMFLib.total(_getLeverageLendingBaseStorage()); + } + + /// @inheritdoc IStrategy + function getRevenue() + public + view + override(IStrategy, LeverageLendingBase) + returns (address[] memory assets_, uint[] memory amounts) + { + LeverageLendingBaseStorage storage $ = _getLeverageLendingBaseStorage(); + ALMFLib.AlmfStrategyStorage storage $a = ALMFLib._getStorage(); + (assets_, amounts) = ALMFLib.getRevenue($, $a.lastSharePrice, vault(), _getRevenueBaseAssetIndex()); + } + + /// @inheritdoc IStrategy + function isReadyForHardWork() external pure override(IStrategy, LeverageLendingBase) returns (bool isReady) { + isReady = true; + } + + /// @inheritdoc IStrategy + function poolTvl() public view override returns (uint tvlUsd) { + return ALMFLib2._poolTvl(platform(), _getAToken()); + } + + /// @inheritdoc IStrategy + /// @dev Assume that all amount can be withdrawn always for simplicity. Implement later. + function maxWithdrawAssets( + uint /*mode*/ + ) public pure override returns (uint[] memory amounts) { + // for simplicity of v.1.0: any amount can be withdrawn + return amounts; + } + + /// @inheritdoc IStrategy + /// @dev Assume that any amount can be deposit always for simplicity. Implement later. + function maxDepositAssets() public pure override returns (uint[] memory amounts) { + // in real implementation we should take into account both borrow and supply cap + // result amount should take leverage into account + // max deposit is limited by amount available to borrow from the borrow pool + + // for simplicity of v1.0: any amount can be deposited + return amounts; + } + + /// @notice Get prices of collateral and borrow assets from Aave price oracle in USD, decimals 18 + /// @return collateralPrice Price of collateral asset ($/collateral asset, decimals 18) + /// @return borrowPrice Price of borrow asset ($/borrow asset, decimals 18) + function getPrices() public view returns (uint collateralPrice, uint borrowPrice) { + LeverageLendingBaseStorage storage $ = _getLeverageLendingBaseStorage(); + (collateralPrice, borrowPrice) = ALMFLib.getPrices($); + } + + //endregion ----------------------------------- View functions + + //region ----------------------------------- Additional functionality + /// @notice Get current threshold for the asset + function threshold(address asset_) external view returns (uint) { + return ALMFLib._getStorage().thresholds[asset_]; + } + + /// @notice Set threshold for the asset + function setThreshold(address asset_, uint threshold_) external onlyOperator { + ALMFLib.setThreshold(asset_, threshold_); + } + + /// @notice Reset share price after changing value of baseAsset in farm configuration + function resetSharePrice() external onlyOperator { + ALMFLib.resetSharePrice(); + } + + //endregion ----------------------------------- Additional functionality + + //region ----------------------------------- ILeverageLendingStrategy + /// @inheritdoc ILeverageLendingStrategy + function realTvl() public view returns (uint tvl, bool trusted) { + LeverageLendingBaseStorage storage $ = _getLeverageLendingBaseStorage(); + (tvl, trusted) = ALMFLib.realTvl($); + } + + function _realSharePrice() internal view override returns (uint sharePrice, bool trusted) { + LeverageLendingBaseStorage storage $ = _getLeverageLendingBaseStorage(); + (sharePrice, trusted) = ALMFLib._realSharePrice($, vault()); + } + + /// @inheritdoc ILeverageLendingStrategy + function health() + external + view + returns ( + uint ltv, + uint maxLtv, + uint leverage, + uint collateralAmount, + uint debtAmount, + uint targetLeveragePercent + ) + { + (ltv, maxLtv, leverage, collateralAmount, debtAmount, targetLeveragePercent) = + ALMFLib.health(platform(), _getLeverageLendingBaseStorage(), _getFarm()); + } + + /// @inheritdoc ILeverageLendingStrategy + function getSupplyAndBorrowAprs() external view returns (uint supplyApr, uint borrowApr) { + LeverageLendingBaseStorage storage $ = _getLeverageLendingBaseStorage(); + (supplyApr, borrowApr) = ALMFLib._getDepositAndBorrowAprs($.lendingVault, $.collateralAsset, $.borrowAsset); + } + + function _rebalanceDebt(uint newLtv) internal override returns (uint resultLtv) { + return ALMFLib.rebalanceDebt(platform(), newLtv, _getLeverageLendingBaseStorage(), _getFarm()); + } + + //endregion ----------------------------------- ILeverageLendingStrategy + + //region ----------------------------------- Strategy base + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STRATEGY BASE */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc StrategyBase + function _assetsAmounts() internal view override returns (address[] memory assets_, uint[] memory amounts_) { + LeverageLendingBaseStorage storage $ = _getLeverageLendingBaseStorage(); + StrategyBaseStorage storage $base = _getStrategyBaseStorage(); + assets_ = $base._assets; + amounts_ = new uint[](1); + amounts_[0] = ALMFLib.totalCollateral($.lendingVault); + } + + /// @inheritdoc StrategyBase + //slither-disable-next-line unused-return + function _depositAssets(uint[] memory amounts, bool) internal override returns (uint value) { + LeverageLendingBaseStorage storage $ = _getLeverageLendingBaseStorage(); + + value = ALMFLib.depositAssets(platform(), $, _getFarm(), amounts[0]); + } + + /// @inheritdoc StrategyBase + function _withdrawAssets(uint value, address receiver) internal override returns (uint[] memory amountsOut) { + LeverageLendingBaseStorage storage $ = _getLeverageLendingBaseStorage(); + + amountsOut = ALMFLib.withdrawAssets(platform(), $, _getFarm(), value, receiver); + } + + /// @inheritdoc StrategyBase + function _claimRevenue() + internal + override + returns ( + address[] memory __assets, + uint[] memory __amounts, + address[] memory __rewardAssets, + uint[] memory __rewardAmounts + ) + { + LeverageLendingBaseStorage storage $ = _getLeverageLendingBaseStorage(); + ALMFLib.AlmfStrategyStorage storage $a = ALMFLib._getStorage(); + FarmingStrategyBaseStorage storage $f = _getFarmingStrategyBaseStorage(); + StrategyBaseStorage storage $base = _getStrategyBaseStorage(); + + (__assets, __amounts, __rewardAssets, __rewardAmounts) = + ALMFLib.claimRevenue($, $a, $f, $base, vault(), _getRevenueBaseAssetIndex()); + } + + /// @inheritdoc StrategyBase + function _compound() internal override(LeverageLendingBase, StrategyBase) { + address _platform = platform(); + return ALMFLib.compound( + _platform, _getLeverageLendingBaseStorage(), _getStrategyBaseStorage(), _getFarm(_platform, farmId()) + ); + } + + /// @inheritdoc StrategyBase + function _depositUnderlying( + uint /*amount*/ + ) + internal + pure + override + returns ( + uint[] memory /*amountsConsumed*/ + ) + { + revert("no underlying"); + } + + /// @inheritdoc StrategyBase + function _withdrawUnderlying( + uint, + /*amount*/ + address /*receiver*/ + ) internal pure override { + revert("no underlying"); + } + + /// @inheritdoc IStrategy + function autoCompoundingByUnderlyingProtocol() + public + view + virtual + override(LeverageLendingBase, StrategyBase) + returns (bool) + { + return false; + } + + /// @inheritdoc StrategyBase + function _previewDepositAssets(uint[] memory amountsMax) + internal + view + override + returns (uint[] memory amountsConsumed, uint value) + { + (amountsConsumed, value) = ALMFLib.previewDepositValue(_getLeverageLendingBaseStorage(), amountsMax); + } + + /// @inheritdoc StrategyBase + function _beforeDoHardWork() internal override { + super._beforeDoHardWork(); + + // all strategies created before 1.2.0 we created with incorrect value of exchangeAssetIndex + // let's update it on the fly + StrategyBaseStorage storage $ = _getStrategyBaseStorage(); + if ($._exchangeAssetIndex == type(uint).max) { + $._exchangeAssetIndex = 0; + } + } + + //endregion ----------------------------------- Strategy base + + //region ----------------------------------- FarmingStrategy + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* FARMING STRATEGY */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @inheritdoc FarmingStrategyBase + function _liquidateRewards( + address exchangeAsset, + address[] memory rewardAssets_, + uint[] memory rewardAmounts_ + ) internal override(FarmingStrategyBase, StrategyBase, LeverageLendingBase) returns (uint earnedExchangeAsset) { + return ALMFLib.liquidateRewards( + platform(), exchangeAsset, rewardAssets_, rewardAmounts_, customPriceImpactTolerance() + ); + } + + /// @inheritdoc IFarmingStrategy + function canFarm() external view override returns (bool) { + IFactory.Farm memory farm = _getFarm(); + return farm.status == 0; + } + + /// @inheritdoc IFarmingStrategy + function farmMechanics() external pure returns (string memory) { + return FarmMechanicsLib.MERKL; + } + + //endregion ----------------------------------- FarmingStrategy + + //region ----------------------------------- Internal logic + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* INTERNAL LOGIC */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + function _getAToken() internal view returns (address) { + FarmingStrategyBaseStorage storage $f = _getFarmingStrategyBaseStorage(); + return _getFarm(platform(), $f.farmId).addresses[0]; + } + + /// @notice Return revenue-base-asset-index from farm configuration. 0 - collateral asset, 1 - borrow asset + function _getRevenueBaseAssetIndex() internal view returns (uint) { + IFactory.Farm memory f = _getFarm(); + return f.nums.length < 5 ? ALMFLib.REVENUE_BASE_ASSET_0 : f.nums[4]; + } + + //endregion ----------------------------------- Internal logic +} diff --git a/src/strategies/base/LeverageLendingBase.sol b/src/strategies/base/LeverageLendingBase.sol index aa75c22c8..315540168 100644 --- a/src/strategies/base/LeverageLendingBase.sol +++ b/src/strategies/base/LeverageLendingBase.sol @@ -150,7 +150,7 @@ abstract contract LeverageLendingBase is StrategyBase, ILeverageLendingStrategy } /// @inheritdoc IStrategy - function getRevenue() external pure virtual returns (address[] memory assets_, uint[] memory amounts) {} + function getRevenue() external view virtual returns (address[] memory assets_, uint[] memory amounts) {} /// @inheritdoc ILeverageLendingStrategy function getUniversalParams() external view returns (uint[] memory params, address[] memory addresses) { diff --git a/src/strategies/libs/ALMFCalcLib.sol b/src/strategies/libs/ALMFCalcLib.sol new file mode 100644 index 000000000..67bf57c42 --- /dev/null +++ b/src/strategies/libs/ALMFCalcLib.sol @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {ISwapper} from "../../interfaces/ISwapper.sol"; +import {IPlatform} from "../../interfaces/IPlatform.sol"; +import {StrategyLib} from "./StrategyLib.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +library ALMFCalcLib { + /// @dev 100_00 is 100% + uint public constant INTERNAL_PRECISION = 100_00; + + /// @notice Alpha = (1 + f) / (1 - s) coeff in calcWithdrawAmounts calculations. + /// It's always 1 because withdraw is calculated in assumption that all fees are covered by user (= 0 in calc) + uint internal constant ALPHA = INTERNAL_PRECISION; + + /// @notice Static data required to make deposit/withdraw calculations + struct StaticData { + address platform; + + /// @notice Address provider of AAVE. Assume that both assets have the same pool and the same provider + address addressProvider; + + address collateralAsset; + address borrowAsset; + address lendingVault; + address borrowingVault; + address flashLoanVault; + uint flashLoanKind; + + /// @notice Price of collateral asset in USD, decimals 18 + uint priceC18; + /// @notice Price of borrow asset in USD, decimals 18 + uint priceB18; + + /// @notice Decimals of collateral asset + uint8 decimalsC; + /// @notice Decimals of borrow asset + uint8 decimalsB; + + /// @notice max swap fee from strategy config, decimals 18 + uint swapFee18; + /// @notice flash loan fee from selected flash loan vault, decimals 18 + uint flashFee18; + + /// @notice minimum target leverage from farm config, INTERNAL_PRECISION + uint minTargetLeverage; + + /// @notice maximum target leverage from farm config, INTERNAL_PRECISION + uint maxTargetLeverage; + } + + struct State { + /// @notice collateral amount in base asset (USD, 18 decimals) + uint collateralBase; + + /// @notice debt amount in base asset (USD, 18 decimals) + uint debtBase; + + /// @notice Max allowed LTV for the user, INTERNAL_PRECISION + uint maxLtv; + + /// @notice Health factor, decimals 18; unhealthy if less than 1e18 + uint healthFactor; + } + + //region ------------------------------------- Deposit logic + + /// @notice Split deposit amount on two parts: amount to deposit as collateral and amount to be used to repay + /// @param amount Total amount to deposit in base asset + /// @param targetLeverage Target leverage, INTERNAL_PRECISION + /// @param collateralBase Current collateral amount in base asset + /// @param debtBase Current debt amount in base asset + /// @param swapFee18 Swap fee (percent), decimals 18 + /// @return aD Amount to deposit as collateral in base asset + /// @return aR Amount to be used to repay debt in base asset + /// @dev Formula: A_r = [ TL*D0 - (TL - 1)*(C0 + A) ] / [ 1 - TL*s ] + function splitDepositAmount( + uint amount, + uint targetLeverage, + uint collateralBase, + uint debtBase, + uint swapFee18 + ) internal pure returns (uint aD, uint aR) { + int arInt = + (int(targetLeverage * debtBase) - int(targetLeverage - INTERNAL_PRECISION) * int(collateralBase + amount)) + / (int(INTERNAL_PRECISION) - int(targetLeverage * swapFee18 / 1e18)); + aR = arInt > 0 ? Math.min(uint(arInt), amount) : 0; + aD = amount > aR ? amount - aR : 0; + } + + //endregion ------------------------------------- Deposit logic + + //region ------------------------------------- Withdraw logic + /// @notice Calculate F and C1 amounts in assumption that all fees are zero (= user takes all losses on himself) + /// @param valueToWithdraw Value that user is going to withdraw, in USD, decimals 18 + /// @return flashAmount Flash loan amount in borrow asset + /// @return collateralToWithdraw Amount of collateral to withdraw from aave in collateral asset + function calcWithdrawAmounts( + uint valueToWithdraw, + uint leverageAdj, + StaticData memory data, + State memory state + ) internal pure returns (uint flashAmount, uint collateralToWithdraw) { + // state.collateralBase — initial collateral (in base units) + // state.debtBase — initial debt (same units) + // valueToWithdraw — amount the user must receive (user payout, formerly “value”) + // La — adjusted target leverage (L_adj) + // LTVa — target post-operation LTV = (La - 1) / La + // s — swap loss fraction (e.g. 0.015) + // f — flash-loan fee fraction (e.g. 0.005) + // α — coefficient linking required collateral for swap and F: + // α = (1 + f) / (1 - s) = 1 (all losses belong to the user, so we should use s = 0, f = 0 here) + // β = α * LTVa + // C1 — amount of collateral withdrawn from the pool + // F — flash-loan size in borrow-asset units + + uint ltvAdj = leverageToLtv(leverageAdj); + + uint beta = ALPHA * ltvAdj / INTERNAL_PRECISION; + + int c1 = + (int(INTERNAL_PRECISION * valueToWithdraw) + int(ALPHA * state.debtBase) - int(beta * state.collateralBase)) + / int(INTERNAL_PRECISION - beta); + + int f = + (int(INTERNAL_PRECISION * state.debtBase) + - int(ltvAdj * state.collateralBase) + + int(ltvAdj * valueToWithdraw)) / int(INTERNAL_PRECISION - beta); + + if (f < 0 || c1 < 0) { + return (0, _baseToCollateral(valueToWithdraw, data.priceC18, data.decimalsC)); + } else { + return ( + _baseToBorrow(uint(f), data.priceB18, data.decimalsB), + _baseToCollateral(uint(c1), data.priceC18, data.decimalsC) + ); + } + } + + /// @notice Estimate amount of collateral to swap to receive {amountToRepay} on balance + /// @param priceImpactTolerance Price impact tolerance. Must include fees at least. Denominator is 100_000. + function estimateSwapAmount( + address platform, + uint amountToRepay, + address collateralAsset, + address token, + uint priceImpactTolerance, + uint rewardsBalance + ) internal view returns (uint) { + // We have collateral C = C1 + C2 where C1 is amount to withdraw, C2 is amount to swap to B (to repay) + // We don't need to swap whole C, we can swap only C2 with same addon (i.e. 10%) for safety + + ISwapper swapper = ISwapper(IPlatform(platform).swapper()); + uint requiredAmount = amountToRepay - balanceWithoutRewards(token, rewardsBalance); + + // we use higher (x2) price impact then required for safety + uint minCollateralToSwap = + swapper.getPrice(token, collateralAsset, requiredAmount * (100_000 + 2 * priceImpactTolerance) / 100_000); // priceImpactTolerance has its own denominator + + return Math.min(minCollateralToSwap, StrategyLib.balance(collateralAsset)); + } + + function balanceWithoutRewards(address borrowAsset, uint rewardsAmount) internal view returns (uint) { + uint balance = StrategyLib.balance(borrowAsset); + return balance > rewardsAmount ? balance - rewardsAmount : 0; + } + + function getLimitedAmount(uint amount, uint optionalLimit) internal pure returns (uint) { + if (optionalLimit == 0) return amount; + return Math.min(amount, optionalLimit); + } + + /// @notice Adjust leverage towards target range using leverage coefficient k if necessary: L_adj = L + k (TL - L) + /// @param leverage Current leverage, INTERNAL_PRECISION + /// @param minTargetLeverage Minimum target leverage, INTERNAL_PRECISION + /// @param maxTargetLeverage Maximum target leverage, INTERNAL_PRECISION + /// @param k Coefficient for adjustment [0...INTERNAL_PRECISION) + /// @return leverageAdj Adjusted leverage, INTERNAL_PRECISION + function adjustLeverage( + uint leverage, + uint minTargetLeverage, + uint maxTargetLeverage, + uint k + ) internal pure returns (uint leverageAdj) { + // calculate target leverage as average value + uint targetLeverage = (minTargetLeverage + maxTargetLeverage) / 2; + + // calculate adjusted leverage + if (leverage < minTargetLeverage) { + leverageAdj = leverage + k * (targetLeverage - leverage) / ALMFCalcLib.INTERNAL_PRECISION; + } else if (leverage > maxTargetLeverage) { + leverageAdj = leverage - k * (leverage - targetLeverage) / ALMFCalcLib.INTERNAL_PRECISION; + } else { + leverageAdj = leverage; + } + } + + //endregion ------------------------------------- Withdraw logic + + //region ------------------------------------- State + /// @notice Calculate current leverage + /// @param collateralBase Current collateral amount in base asset + /// @param debtBase Current debt amount in base asset + /// @return Current leverage, INTERNAL_PRECISION + function getLeverage(uint collateralBase, uint debtBase) internal pure returns (uint) { + if (collateralBase == 0) { + return 0; + } + return (collateralBase * INTERNAL_PRECISION) / (collateralBase - debtBase); + } + + /// @notice Calculate loan-to-value ratio (LTV) + /// @param collateralBase Current collateral amount in base asset + /// @param debtBase Current debt amount in base asset + /// @return LTV, INTERNAL_PRECISION + function getLtv(uint collateralBase, uint debtBase) internal pure returns (uint) { + if (collateralBase == 0) { + return 0; + } + return (debtBase * INTERNAL_PRECISION) / collateralBase; + } + + /// @notice Calculate loan-to-value ratio (LTV) from leverage + /// @param leverage Leverage, INTERNAL_PRECISION + /// @return LTV, INTERNAL_PRECISION + function leverageToLtv(uint leverage) internal pure returns (uint) { + // assume here that leverage always greater than INTERNAL_PRECISION + return INTERNAL_PRECISION - INTERNAL_PRECISION * INTERNAL_PRECISION / leverage; + } + + function ltvToLeverage(uint ltv) internal pure returns (uint) { + // assume here that ltv always less than INTERNAL_PRECISION + return INTERNAL_PRECISION * INTERNAL_PRECISION / (INTERNAL_PRECISION - ltv); + } + + /// @notice Calculate collateral balance in base asset (USD, 18 decimals) + function collateralToBase(uint amount, ALMFCalcLib.StaticData memory data) internal pure returns (uint balance) { + balance = _collateralToBase(amount, data.priceC18, data.decimalsC); + } + + function borrowToBase(uint amount, ALMFCalcLib.StaticData memory data) internal pure returns (uint balance) { + balance = _borrowToBase(amount, data.priceB18, data.decimalsB); + } + + function baseToCollateral(uint amountBase, ALMFCalcLib.StaticData memory data) internal pure returns (uint) { + return _baseToCollateral(amountBase, data.priceC18, data.decimalsC); + } + + function baseToBorrow(uint amountBase, ALMFCalcLib.StaticData memory data) internal pure returns (uint) { + return _baseToBorrow(amountBase, data.priceB18, data.decimalsB); + } + + function _collateralToBase(uint amountC, uint priceC18, uint8 decimalsC) internal pure returns (uint) { + return Math.mulDiv(amountC, priceC18, 10 ** decimalsC); + } + + function _borrowToBase(uint amountB, uint priceB18, uint8 decimalsB) internal pure returns (uint) { + return Math.mulDiv(amountB, priceB18, 10 ** decimalsB); + } + + function _baseToCollateral(uint amountBase, uint priceC18, uint8 decimalsC) internal pure returns (uint) { + return Math.mulDiv(amountBase, 10 ** decimalsC, priceC18); + } + + function _baseToBorrow(uint amountBase, uint priceB18, uint8 decimalsB) internal pure returns (uint) { + return Math.mulDiv(amountBase, 10 ** decimalsB, priceB18); + } + //endregion ------------------------------------- State +} diff --git a/src/strategies/libs/ALMFLib.sol b/src/strategies/libs/ALMFLib.sol new file mode 100644 index 000000000..e527bf144 --- /dev/null +++ b/src/strategies/libs/ALMFLib.sol @@ -0,0 +1,1028 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {ALMFCalcLib} from "./ALMFCalcLib.sol"; +import {ConstantsLib} from "../../core/libs/ConstantsLib.sol"; +import {IAToken} from "../../integrations/aave/IAToken.sol"; +import {IAaveAddressProvider} from "../../integrations/aave/IAaveAddressProvider.sol"; +import {IAavePriceOracle} from "../../integrations/aave/IAavePriceOracle.sol"; +import {IControllable} from "../../interfaces/IControllable.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IFactory} from "../../interfaces/IFactory.sol"; +import {IFarmingStrategy} from "../../interfaces/IFarmingStrategy.sol"; +import {ILeverageLendingStrategy} from "../../interfaces/ILeverageLendingStrategy.sol"; +import {IPlatform} from "../../interfaces/IPlatform.sol"; +import {IPool} from "../../integrations/aave/IPool.sol"; +import {IStrategy} from "../../interfaces/IStrategy.sol"; +import {ISwapper} from "../../interfaces/ISwapper.sol"; +import {IVaultMainV3} from "../../integrations/balancerv3/IVaultMainV3.sol"; +import {IVault} from "../../interfaces/IVault.sol"; +import {LeverageLendingLib} from "./LeverageLendingLib.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {StrategyLib} from "./StrategyLib.sol"; + +library ALMFLib { + using SafeERC20 for IERC20; + + // keccak256(abi.encode(uint256(keccak256("erc7201:stability.AaveLeverageMerklFarmStrategy")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 internal constant AAVE_MERKL_FARM_STRATEGY_STORAGE_LOCATION = + 0x735fb8abe13487f936dfcaad40428cb37101f887b7e375bd6616c095d1050600; + + uint public constant FARM_ADDRESS_LENDING_VAULT_INDEX = 0; + uint public constant FARM_ADDRESS_BORROWING_VAULT_INDEX = 1; + uint public constant FARM_ADDRESS_FLASH_LOAN_VAULT_INDEX = 2; + + uint public constant INTEREST_RATE_MODE_VARIABLE = 2; + + /// @notice Value of revenueBaseAssetIndex param indicating that share price is calculated in collateral asset + uint internal constant REVENUE_BASE_ASSET_0 = 0; + + /// @notice Value of revenueBaseAssetIndex param indicating that share price is calculated in borrow asset + uint internal constant REVENUE_BASE_ASSET_1 = 1; + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* DATA TYPES */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @custom:storage-location erc7201:stability.AaveLeverageMerklFarmStrategy + struct AlmfStrategyStorage { + /// @dev Deprecated since 1.2.0 + uint lastSharePriceInUSD; + + /// @notice Deposit threshold. Amounts less than the threshold are deposited directly without leverage + mapping(address asset => uint) thresholds; + + /// @notice Last share price used to calculate profit and loss = [total in collateral/borrow asset] / vault.totalSupply() + /// @dev This value is initialized at first claim revenue (not at first deposit) + /// @dev Share price is calculated in collateral asset if revenueBaseAsset == 0 else in borrow asset + uint lastSharePrice; + } + + error CollateralAssetThresholdTooLow(); + + event SetThreshold(address asset, uint value); + event ResetSharePrice(); + + //region ------------------------------------- Flash loan + /// @notice token Borrow asset + /// @notice amount Flash loan amount in borrow asset + function _receiveFlashLoan( + address platform, + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + address token, + uint amount, + uint feeAmount + ) internal { + address collateralAsset = $.collateralAsset; + address flashLoanVault = $.flashLoanVault; + require(msg.sender == flashLoanVault, IControllable.IncorrectMsgSender()); + + // Reward asset can be equal to the borrow asset. Rewards can be transferred to the strategy at any moment. + // If any borrow asset is on the balance before taking flash loan it can be only rewards. + // All rewards are processed by hardwork and cannot be used before hardwork. + // So, we need to keep reward amount on balance after exit this function. + uint tokenBalance0 = IERC20(token).balanceOf(address(this)); + tokenBalance0 = tokenBalance0 > amount ? tokenBalance0 - amount : 0; + + if ($.tempAction == ILeverageLendingStrategy.CurrentAction.Deposit) { + // swap + _swap(platform, token, collateralAsset, amount, $.swapPriceImpactTolerance0); + + // supply: assume here that rewards in collateral are not possible + IPool(IAToken($.lendingVault).POOL()) + .supply(collateralAsset, IERC20(collateralAsset).balanceOf(address(this)), address(this), 0); + + // borrow + IPool(IAToken($.borrowingVault).POOL()) + .borrow(token, amount + feeAmount, INTEREST_RATE_MODE_VARIABLE, 0, address(this)); + + // pay flash loan + IERC20(token).safeTransfer(flashLoanVault, amount + feeAmount); + } + + if ($.tempAction == ILeverageLendingStrategy.CurrentAction.Withdraw) { + uint tempCollateralAmount = $.tempCollateralAmount; + uint swapPriceImpactTolerance0 = $.swapPriceImpactTolerance0; + + // repay debt + IPool(IAToken($.borrowingVault).POOL()).repay(token, amount, INTEREST_RATE_MODE_VARIABLE, address(this)); + + // withdraw + { + address lendingVault = $.lendingVault; + uint collateralAmountTotal = totalCollateral(lendingVault); + + IPool(IAToken(lendingVault).POOL()) + .withdraw(collateralAsset, Math.min(tempCollateralAmount, collateralAmountTotal), address(this)); + } + + // swap + _swap( + platform, + collateralAsset, + token, + ALMFCalcLib.estimateSwapAmount( + platform, amount + feeAmount, collateralAsset, token, swapPriceImpactTolerance0, tokenBalance0 + ), + // Math.min(tempCollateralAmount, StrategyLib.balance(collateralAsset)), + swapPriceImpactTolerance0 + ); + + // explicit error for the case when _estimateSwapAmount gives incorrect amount + require( + ALMFCalcLib.balanceWithoutRewards(token, tokenBalance0) >= amount + feeAmount, + IControllable.InsufficientBalance() + ); + + // pay flash loan + IERC20(token).safeTransfer(flashLoanVault, amount + feeAmount); + + // swap unnecessary borrow asset back to collateral + _swap( + platform, + token, + collateralAsset, + ALMFCalcLib.balanceWithoutRewards(token, tokenBalance0), + swapPriceImpactTolerance0 + ); + + // reset temp vars + $.tempCollateralAmount = 0; + } + + if ($.tempAction == ILeverageLendingStrategy.CurrentAction.DecreaseLtv) { + address lendingVault = $.lendingVault; + + // repay + IPool(IAToken($.borrowingVault).POOL()) + .repay( + token, + ALMFCalcLib.balanceWithoutRewards(token, tokenBalance0), + INTEREST_RATE_MODE_VARIABLE, + address(this) + ); + + // withdraw amount + IPool(IAToken(lendingVault).POOL()).withdraw(collateralAsset, $.tempCollateralAmount, address(this)); + + // swap + _swap(platform, collateralAsset, token, $.tempCollateralAmount, $.swapPriceImpactTolerance1); + + // pay flash loan + IERC20(token).safeTransfer(flashLoanVault, amount + feeAmount); + + // repay remaining balance + IPool(IAToken($.borrowingVault).POOL()) + .repay( + token, + ALMFCalcLib.balanceWithoutRewards(token, tokenBalance0), + INTEREST_RATE_MODE_VARIABLE, + address(this) + ); + + $.tempCollateralAmount = 0; + } + + if ($.tempAction == ILeverageLendingStrategy.CurrentAction.IncreaseLtv) { + // swap + _swap( + platform, + token, + collateralAsset, + ALMFCalcLib.balanceWithoutRewards(token, tokenBalance0) * $.increaseLtvParam1 + / ALMFCalcLib.INTERNAL_PRECISION, + $.swapPriceImpactTolerance1 + ); + + // supply + IPool(IAToken($.lendingVault).POOL()) + .deposit(collateralAsset, IERC20(collateralAsset).balanceOf(address(this)), address(this), 0); + + // borrow + IPool(IAToken($.borrowingVault).POOL()) + .borrow(token, amount + feeAmount, INTEREST_RATE_MODE_VARIABLE, 0, address(this)); + + // pay flash loan + IERC20(token).safeTransfer(flashLoanVault, amount + feeAmount); + + // repay not used borrow + uint tokenBalance = ALMFCalcLib.balanceWithoutRewards(token, tokenBalance0); + if (tokenBalance != 0) { + IPool(IAToken($.borrowingVault).POOL()) + .repay(token, tokenBalance, INTEREST_RATE_MODE_VARIABLE, address(this)); + } + } + + // ensure that all rewards are still exist on the balance + require(tokenBalance0 == IERC20(token).balanceOf(address(this)), IControllable.IncorrectBalance()); + + _emitLeverageLendingHealth($); + + $.tempAction = ILeverageLendingStrategy.CurrentAction.None; + } + + function _emitLeverageLendingHealth(ILeverageLendingStrategy.LeverageLendingBaseStorage storage $) internal { + (uint collateralAmountBase, uint debtAmountBase,,,,) = + IPool(IAToken($.lendingVault).POOL()).getUserAccountData(address(this)); + uint ltv = ALMFCalcLib.getLtv(collateralAmountBase, debtAmountBase); + emit ILeverageLendingStrategy.LeverageLendingHealth(ltv, ALMFCalcLib.ltvToLeverage(ltv)); + } + + function receiveFlashLoanBalancerV2( + address platform, + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + address[] memory tokens, + uint[] memory amounts, + uint[] memory feeAmounts + ) external { + // Flash loan is performed upon deposit and withdrawal + ALMFLib._receiveFlashLoan(platform, $, tokens[0], amounts[0], feeAmounts[0]); + } + + function receiveFlashLoanV3( + address platform, + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + address token, + uint amount + ) external { + // sender is vault, it's checked inside receiveFlashLoan + // we can use msg.sender below but $.flashLoanVault looks more safe + IVaultMainV3 vault = IVaultMainV3(payable($.flashLoanVault)); + + // ensure that the vault has available amount + require(IERC20(token).balanceOf(address(vault)) >= amount, IControllable.InsufficientBalance()); + + // receive flash loan from the vault + vault.sendTo(token, address(this), amount); + + // Flash loan is performed upon deposit and withdrawal + ALMFLib._receiveFlashLoan(platform, $, token, amount, 0); // assume that flash loan is free, fee is 0 + + // return flash loan back to the vault + // assume that the amount was transferred back to the vault inside receiveFlashLoan() + // we need only to register this transferring + vault.settle(token, amount); + } + + function uniswapV3FlashCallback( + address platform, + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + uint fee0, + uint fee1, + bytes calldata userData + ) external { + // sender is the pool, it's checked inside receiveFlashLoan + (address token, uint amount, bool isToken0) = abi.decode(userData, (address, uint, bool)); + ALMFLib._receiveFlashLoan(platform, $, token, amount, isToken0 ? fee0 : fee1); + } + + //endregion ------------------------------------- Flash loan + + //region ------------------------------------- Deposit + /// @notice Deposit {amount} of the collateral asset + /// @param amount Amount of collateral asset to deposit + /// @return value Value is calculated as a delta of (total collateral - total debt) in base assets (USDC, 18 decimals) + function depositAssets( + address platform_, + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + IFactory.Farm memory farm, + uint amount + ) external returns (uint value) { + return _depositAssets(platform_, $, farm, amount); + } + + /// @notice Deposit {amount} of the collateral asset + /// @param amount Amount of collateral asset to deposit + /// @return value Value is calculated as a delta of (total collateral - total debt) in base assets (USDC, 18 decimals) + function _depositAssets( + address platform_, + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + IFactory.Farm memory farm, + uint amount + ) internal returns (uint value) { + ALMFCalcLib.StaticData memory data = _getStaticData(platform_, $, farm); + ALMFCalcLib.State memory state = _getState(data); + + uint valueWas = ALMFCalcLib.collateralToBase(StrategyLib.balance(data.collateralAsset), data) + calcTotal(state); + + uint threshold = _getStorage().thresholds[data.collateralAsset]; + if (amount > threshold) { + _deposit(platform_, $, data, amount, state); + } else { + // tiny amounts are supplied without leverage + IPool(IAToken(data.lendingVault).POOL()).supply(data.collateralAsset, amount, address(this), 0); + } + + state = _getState(data); // refresh state after deposit + uint valueNow = ALMFCalcLib.collateralToBase(StrategyLib.balance(data.collateralAsset), data) + calcTotal(state); + + if (valueNow > valueWas) { + value = ALMFCalcLib.collateralToBase(amount, data) + (valueNow - valueWas); + } else { + // there is a chance that threshold is set too low + // in that case we can have negative amount below ... make explicit revert in this case + uint base = ALMFCalcLib.collateralToBase(amount, data); + require(base >= (valueWas - valueNow), CollateralAssetThresholdTooLow()); + value = base - (valueWas - valueNow); + } + + _ensureLtvValid(state); + } + + /// @notice Deposit with leverage: if current leverage is above target, first repay debt directly, then deposit with flash loan; + function _deposit( + address platform_, + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + ALMFCalcLib.StaticData memory data, + uint amountToDeposit, + ALMFCalcLib.State memory state + ) internal { + uint leverage = ALMFCalcLib.getLeverage(state.collateralBase, state.debtBase); + if (leverage > data.maxTargetLeverage) { + (uint amountDepositBase, uint amountRepayBase) = ALMFCalcLib.splitDepositAmount( + ALMFCalcLib.collateralToBase(amountToDeposit, data), + (data.minTargetLeverage + data.maxTargetLeverage) / 2, + state.collateralBase, + state.debtBase, + data.swapFee18 + ); + bool repayRequired = amountRepayBase > _getStorage().thresholds[data.borrowAsset]; + if (repayRequired) { + // restore leverage using direct repay + _directRepay(platform_, data, ALMFCalcLib.baseToCollateral(amountRepayBase, data)); + } + if (amountDepositBase != 0) { + if (repayRequired) { + state = _getState(data); // refresh state after direct repay + } + // deposit remain amount with leverage + _depositWithFlash($, data, ALMFCalcLib.baseToCollateral(amountDepositBase, data), state); + } + } else { + _depositWithFlash($, data, amountToDeposit, state); + } + } + + /// @notice Directly repay debt by swapping a given part of collateral to borrow asset + function _directRepay(address platform_, ALMFCalcLib.StaticData memory data, uint amountToDeposit) internal { + // we need to remember balance to exclude possible rewards (provided in borrow asset) from the amount to repay + uint borrowBalanceBefore = StrategyLib.balance(data.borrowAsset); + + // swap amount to borrow asset + _swap( + platform_, + data.collateralAsset, + data.borrowAsset, + amountToDeposit, + data.swapFee18 * ConstantsLib.DENOMINATOR / 1e18 + ); + + // use all balance of borrow asset to repay debt + address pool = IAToken(data.borrowingVault).POOL(); + uint amountToRepay = StrategyLib.balance(data.borrowAsset) - borrowBalanceBefore; + if (amountToRepay != 0) { + IPool(pool).repay(data.borrowAsset, amountToRepay, INTEREST_RATE_MODE_VARIABLE, address(this)); + } + } + + /// @notice Deposit with flash loan + function _depositWithFlash( + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + ALMFCalcLib.StaticData memory data, + uint amountToDeposit, + ALMFCalcLib.State memory state + ) internal { + uint borrowAmount = _getDepositFlashAmount(amountToDeposit, data, state); + (address[] memory flashAssets, uint[] memory flashAmounts) = + _getFlashLoanAmounts(borrowAmount, data.borrowAsset); + + $.tempAction = ILeverageLendingStrategy.CurrentAction.Deposit; + LeverageLendingLib.requestFlashLoan($, flashAssets, flashAmounts); + } + + /// @notice Calculate amount to borrow in flash loan for deposit + /// @param amountToDeposit Amount of collateral asset to deposit + function _getDepositFlashAmount( + uint amountToDeposit, + ALMFCalcLib.StaticData memory data, + ALMFCalcLib.State memory state + ) internal pure returns (uint flashAmount) { + uint targetLeverage = (data.minTargetLeverage + data.maxTargetLeverage) / 2; + uint amountBase = ALMFCalcLib._collateralToBase(amountToDeposit, data.priceC18, data.decimalsC); + uint den = + (targetLeverage + * (data.swapFee18 + data.flashFee18) + + (1e18 - data.swapFee18) + * ALMFCalcLib.INTERNAL_PRECISION) / 1e18; + uint num = targetLeverage * (state.collateralBase + amountBase - state.debtBase) + - (state.collateralBase + amountBase) * ALMFCalcLib.INTERNAL_PRECISION; + + // assume here that den > 0; it's safer to revert in other case + flashAmount = ALMFCalcLib._baseToBorrow(num / den, data.priceB18, data.decimalsB); + } + + //endregion ------------------------------------- Deposit + + //region ------------------------------------- Withdraw + /// @notice Withdraw {value} from the strategy to {receiver} + /// @param value Value to withdraw in base asset (USD, 18 decimals) + function withdrawAssets( + address platform, + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + IFactory.Farm memory farm, + uint value, + address receiver + ) external returns (uint[] memory amountsOut) { + ALMFCalcLib.StaticData memory data = _getStaticData(platform, $, farm); + ALMFCalcLib.State memory state = _getState(data); + + uint collateralBalanceBase = ALMFCalcLib.collateralToBase(StrategyLib.balance(data.collateralAsset), data); + uint valueWas = collateralBalanceBase + calcTotal(state); + + // ---------------------- withdraw from the lending vault - only if amount on the balance is not enough + if (value > collateralBalanceBase) { + // it's too dangerous to ask to withdraw (value - state.collateralBalanceStrategy) + // because current balance is used in multiple places inside receiveFlashLoan + // so we ask to withdraw full required amount + _withdrawRequiredAmountOnBalance($, data, state, value); + state = _getState(data); + } + + // ---------------------- Transfer required amount to the user + uint balance = StrategyLib.balance(data.collateralAsset); + uint valueNow = ALMFCalcLib.collateralToBase(balance, data) + calcTotal(state); + + amountsOut = new uint[](1); + if (valueWas > valueNow) { + amountsOut[0] = Math.min(ALMFCalcLib.baseToCollateral(value - (valueWas - valueNow), data), balance); + } else { + amountsOut[0] = Math.min(ALMFCalcLib.baseToCollateral(value + (valueNow - valueWas), data), balance); + } + + // we can have dust amounts of collateral on strategy balance here + + if (receiver != address(this)) { + IERC20(data.collateralAsset).safeTransfer(receiver, amountsOut[0]); + } + + _ensureLtvValid(state); + } + + /// @notice Withdraw required amount on balance as collateral asset + /// @param value Value to withdraw in base asset (USD, 18 decimals) + function _withdrawRequiredAmountOnBalance( + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + ALMFCalcLib.StaticData memory data, + ALMFCalcLib.State memory state, + uint value + ) internal { + if (0 == state.debtBase) { + // zero debt, positive supply - we can just withdraw missed amount from the lending pool + + // collateral amount on balance in base asset + uint balance = StrategyLib.balance(data.collateralAsset); + uint collateralBalanceBase = ALMFCalcLib.collateralToBase(balance, data); + + // collateral amount required to withdraw from lending pool + uint amountToWithdraw = + ALMFCalcLib.baseToCollateral(value > collateralBalanceBase ? value - collateralBalanceBase : 0, data); + + if (amountToWithdraw != 0) { + IPool(IAToken(data.lendingVault).POOL()).withdraw(data.collateralAsset, amountToWithdraw, address(this)); + } + } else { + _withdrawUsingFlash($, data, state, value); + } + } + + /// @notice Withdraw required amount of collateral on balance using flash loan + function _withdrawUsingFlash( + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + ALMFCalcLib.StaticData memory data, + ALMFCalcLib.State memory state, + uint value + ) internal { + uint leverage = ALMFCalcLib.adjustLeverage( + ALMFCalcLib.getLeverage(state.collateralBase, state.debtBase), + data.minTargetLeverage, + data.maxTargetLeverage, + $.withdrawParam0 + ); + + (uint flashAmount, uint collateralToWithdraw) = ALMFCalcLib.calcWithdrawAmounts(value, leverage, data, state); + + if (value == state.collateralBase - state.debtBase) { + // full withdraw (emergency) + // we can use flashAmount calculated above but need to override collateralToWithdraw to withdraw fully + collateralToWithdraw = totalCollateral(data.lendingVault); + } + + if (flashAmount == 0) { + // special case: don't use flash, just withdraw required amount from aave and send it to the user + IPool(IAToken(data.lendingVault).POOL()).withdraw(data.collateralAsset, collateralToWithdraw, address(this)); + } else { + (address[] memory flashAssets, uint[] memory flashAmounts) = + _getFlashLoanAmounts(flashAmount, data.borrowAsset); + + $.tempCollateralAmount = collateralToWithdraw; + + $.tempAction = ILeverageLendingStrategy.CurrentAction.Withdraw; + LeverageLendingLib.requestFlashLoan($, flashAssets, flashAmounts); + } + } + + //endregion ------------------------------------- Withdraw + + //region ------------------------------------- View + /// @notice Calculate total value: collateral - debt in base asset (USD, 18 decimals) + /// Balance on the strategy is NOT included. + function calcTotal(ALMFCalcLib.State memory state) internal pure returns (uint totalValue) { + totalValue = state.collateralBase - state.debtBase; + } + + /// @notice Get prices of collateral and borrow assets from Aave price oracle in USD, decimals 18 + function getPrices( + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $ + ) external view returns (uint collateralPrice, uint borrowPrice) { + (collateralPrice, borrowPrice) = ALMFLib.getPrices( + IPool(IAToken($.lendingVault).POOL()).ADDRESSES_PROVIDER(), $.collateralAsset, $.borrowAsset + ); + } + + /// @notice Get prices of collateral and borrow assets from Aave price oracle in USD, decimals 18 + function getPrices( + address aaveAddressProvider, + address collateralAsset, + address borrowAsset + ) internal view returns (uint priceC, uint priceB) { + address[] memory assets = new address[](2); + assets[0] = collateralAsset; + assets[1] = borrowAsset; + + uint[] memory prices = + IAavePriceOracle(IAaveAddressProvider(aaveAddressProvider).getPriceOracle()).getAssetsPrices(assets); + return (prices[0] * 1e10, prices[1] * 1e10); // Aave prices have 8 decimals, we need 18 + } + + /// @notice Get static data for deposit/withdraw calculations + function _getStaticData( + address platform_, + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + IFactory.Farm memory farm + ) internal view returns (ALMFCalcLib.StaticData memory data) { + data.platform = platform_; + + data.collateralAsset = $.collateralAsset; + data.borrowAsset = $.borrowAsset; + data.lendingVault = $.lendingVault; + data.borrowingVault = $.borrowingVault; + + data.addressProvider = IPool(IAToken(data.lendingVault).POOL()).ADDRESSES_PROVIDER(); + + data.flashLoanVault = $.flashLoanVault; + data.flashLoanKind = $.flashLoanKind; + + data.swapFee18 = $.swapPriceImpactTolerance0 * 1e18 / ConstantsLib.DENOMINATOR; + data.flashFee18 = LeverageLendingLib.getFlashFee18(data.flashLoanVault, data.flashLoanKind); + + data.decimalsC = IERC20Metadata(data.collateralAsset).decimals(); + data.decimalsB = IERC20Metadata(data.borrowAsset).decimals(); + (data.priceC18, data.priceB18) = ALMFLib.getPrices(data.addressProvider, data.collateralAsset, data.borrowAsset); + + (data.minTargetLeverage, data.maxTargetLeverage) = _getFarmLeverageConfig(farm); + + return data; + } + + /// @return targetMinLeverage Minimum target leverage, INTERNAL_PRECISION + /// @return targetMaxLeverage Maximum target leverage, INTERNAL_PRECISION + function _getFarmLeverageConfig( + IFactory.Farm memory farm + ) internal pure returns (uint targetMinLeverage, uint targetMaxLeverage) { + return (ALMFCalcLib.ltvToLeverage(farm.nums[0]), ALMFCalcLib.ltvToLeverage(farm.nums[1])); + } + + /// @notice Get current state: collateral and debt in base asset (USD, 18 decimals) + function _getState(address pool_) internal view returns (ALMFCalcLib.State memory state) { + (uint totalCollateralBase, uint totalDebtBase,,, uint maxLtv, uint healthFactor) = + IPool(pool_).getUserAccountData(address(this)); + + state = ALMFCalcLib.State({ + collateralBase: totalCollateralBase * 1e10, + debtBase: totalDebtBase * 1e10, + maxLtv: maxLtv, + healthFactor: healthFactor + }); + } + + /// @notice Get current state: collateral and debt in base asset (USD, 18 decimals) + function _getState(ALMFCalcLib.StaticData memory data) internal view returns (ALMFCalcLib.State memory state) { + return _getState(IAaveAddressProvider(data.addressProvider).getPool()); + } + + function totalCollateral(address lendingVault) public view returns (uint) { + return IAToken(lendingVault).balanceOf(address(this)); + } + + /// @dev not optimal by gas, but it's ok for view function + function health( + address platform, + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + IFactory.Farm memory farm + ) + external + view + returns ( + uint ltv, + uint maxLtv, + uint leverage, + uint collateralAmount, + uint debtAmount, + uint targetLeveragePercent + ) + { + ALMFCalcLib.StaticData memory data = _getStaticData(platform, $, farm); + IPool pool = IPool(IAToken(data.lendingVault).POOL()); + + // Maximum LTV with 4 decimals + uint collateralAmountBase; + uint debtAmountBase; + (collateralAmountBase, debtAmountBase,,, maxLtv,) = pool.getUserAccountData(address(this)); + + // convert from aave base (decimals USD, 1e8) to our base asset (USD, 18 decimals) + collateralAmountBase *= 1e10; + debtAmountBase *= 1e10; + + // Current amount of collateral asset (strategy asset) + collateralAmount = ALMFCalcLib.baseToCollateral(collateralAmountBase, data); + + // Current debt of borrowed asset + debtAmount = ALMFCalcLib.baseToBorrow(debtAmountBase, data); + + // Current LTV with 4 decimals + ltv = ALMFCalcLib.getLtv(collateralAmountBase, debtAmountBase); + + // Current leverage multiplier with 4 decimals + leverage = ALMFCalcLib.ltvToLeverage(ltv); + + // targetLeveragePercent Configurable percent of max leverage with 4 decimals + uint maxLeverage = ALMFCalcLib.ltvToLeverage(maxLtv); + uint targetLeverage = (data.minTargetLeverage + data.maxTargetLeverage) / 2; + targetLeveragePercent = targetLeverage * ALMFCalcLib.INTERNAL_PRECISION / maxLeverage; + } + + function total( + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $ + ) internal view returns (uint totalValue) { + totalValue = calcTotal(_getState(IAToken($.lendingVault).POOL())); + } + + function previewDepositValue( + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + uint[] memory amountsMax + ) external view returns (uint[] memory amountsConsumed, uint value) { + amountsConsumed = new uint[](1); + amountsConsumed[0] = amountsMax[0]; + + address collateralAsset = $.collateralAsset; + + // value is [total collateral - total debt] in USD, 18 decimals + uint price8 = IAavePriceOracle( + IAaveAddressProvider(IPool(IAToken($.lendingVault).POOL()).ADDRESSES_PROVIDER()).getPriceOracle() + ).getAssetPrice(collateralAsset); + + value = amountsMax[0] * price8 * 1e10 / (10 ** IERC20Metadata(collateralAsset).decimals()); + } + + //endregion ------------------------------------- View + + //region ------------------------------------- Swap + function _swap( + address platform, + address tokenIn, + address tokenOut, + uint amount, + uint priceImpactTolerance + ) internal { + ISwapper swapper = ISwapper(IPlatform(platform).swapper()); + swapper.swap(tokenIn, tokenOut, amount, priceImpactTolerance); + } + + //endregion ------------------------------------- Swap + + //region ------------------------------------- Rebalance debt + function rebalanceDebt( + address platform, + uint newLtv, + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + IFactory.Farm memory farm + ) external returns (uint resultLtv) { + ALMFCalcLib.StaticData memory data = _getStaticData(platform, $, farm); + ALMFCalcLib.State memory state = _getState(data); + + // here is the math that works: + // collateral_value - debt_value = real_TVL + // debt_value * PRECISION / collateral_value = LTV + // --- + // new_collateral_value = real_TVL * PRECISION / (PRECISION - LTV) + // new_debt_value = new_collateral_value * LTV / PRECISION + // real_TVL is not changed if current strategy balance of collateral is zero + + uint tvlBase = ALMFCalcLib.collateralToBase(StrategyLib.balance(data.collateralAsset), data) + calcTotal(state); + + uint newCollateralValueBase = + tvlBase * ALMFCalcLib.INTERNAL_PRECISION / (ALMFCalcLib.INTERNAL_PRECISION - newLtv); + uint newDebtAmountBase = newCollateralValueBase * newLtv / ALMFCalcLib.INTERNAL_PRECISION; + + uint debtDiff; + if (newLtv < ALMFCalcLib.getLtv(state.collateralBase, state.debtBase)) { + // need decrease debt and collateral + $.tempAction = ILeverageLendingStrategy.CurrentAction.DecreaseLtv; + + debtDiff = ALMFCalcLib.baseToBorrow(state.debtBase - newDebtAmountBase, data); + + $.tempCollateralAmount = (ALMFCalcLib.baseToCollateral(state.collateralBase - newCollateralValueBase, data)) + * $.decreaseLtvParam0 / ALMFCalcLib.INTERNAL_PRECISION; + } else { + // need increase debt and collateral + $.tempAction = ILeverageLendingStrategy.CurrentAction.IncreaseLtv; + + debtDiff = (ALMFCalcLib.baseToBorrow(newDebtAmountBase - state.debtBase, data)) * $.increaseLtvParam0 + / ALMFCalcLib.INTERNAL_PRECISION; + } + + (address[] memory flashAssets, uint[] memory flashAmounts) = _getFlashLoanAmounts(debtDiff, data.borrowAsset); + + LeverageLendingLib.requestFlashLoan($, flashAssets, flashAmounts); + + $.tempAction = ILeverageLendingStrategy.CurrentAction.None; + + state = _getState(data); + resultLtv = ALMFCalcLib.getLtv(state.collateralBase, state.debtBase); + } + + //endregion ------------------------------------- Rebalance debt + + //region ------------------------------------- Real tvl + + /// @notice Calculate real TVL in USD, decimals 18 + function realTvl( + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $ + ) public view returns (uint tvl, bool trusted) { + return _realTvl(_getState(IAToken($.lendingVault).POOL())); + } + + /// @notice Calculate real TVL in USD, decimals 18 + function _realTvl(ALMFCalcLib.State memory state) internal pure returns (uint tvl, bool trusted) { + tvl = state.collateralBase - state.debtBase; + trusted = true; + } + + /// @notice Calculate real share price as USD18 / vault-shares + function _realSharePrice( + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + address vault_ + ) internal view returns (uint sharePrice, bool trusted) { + uint __realTvl; + (__realTvl, trusted) = realTvl($); + uint totalSupply = IERC20(vault_).totalSupply(); + if (totalSupply != 0) { + sharePrice = __realTvl * 1e18 / totalSupply; + } + return (sharePrice, trusted); + } + + //endregion ------------------------------------- Real tvl + + //region ------------------------------------- Revenue + function _getDepositAndBorrowAprs( + address lendingVault, + address collateralAsset, + address borrowAsset + ) external view returns (uint depositApr, uint borrowApr) { + IPool pool = IPool(IAToken(lendingVault).POOL()); + IPool.ReserveData memory collateralData = pool.getReserveData(collateralAsset); + IPool.ReserveData memory borrowData = pool.getReserveData(borrowAsset); + + // liquidityRate and variableBorrowRate are in Ray (1e27) + // To convert to percentage with 5 decimals (1e5), use: + // rate(1e27) * 1e5 / 1e27 = rate / 1e22 + depositApr = uint(collateralData.currentLiquidityRate) * ConstantsLib.DENOMINATOR / 1e27; + borrowApr = uint(borrowData.currentVariableBorrowRate) * ConstantsLib.DENOMINATOR / 1e27; + } + + function claimRevenue( + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + AlmfStrategyStorage storage $a, + IFarmingStrategy.FarmingStrategyBaseStorage storage $f, + IStrategy.StrategyBaseStorage storage $base, + address vault_, + uint revenueBaseAssetIndex_ + ) + external + returns ( + address[] memory __assets, + uint[] memory __amounts, + address[] memory __rewardAssets, + uint[] memory __rewardAmounts + ) + { + /// @dev New price in base asset + uint newPrice = _sharePrice($, vault_, revenueBaseAssetIndex_); + + /// @dev Previous price in base asset + uint oldPrice = $a.lastSharePrice; + + if (oldPrice == 0) { + // initialization of share price (first one or after reset) + // we cannot do it in deposit() because total supply is used for calculation + $a.lastSharePrice = newPrice; + oldPrice = newPrice; + } + + (__assets, __amounts) = _getRevenue($, oldPrice, newPrice, vault_, revenueBaseAssetIndex_); + $a.lastSharePrice = newPrice; + + // ---------------------- collect Merkl rewards + __rewardAssets = $f._rewardAssets; + uint rwLen = __rewardAssets.length; + __rewardAmounts = new uint[](rwLen); + for (uint i; i < rwLen; ++i) { + // Reward asset can be equal to the borrow asset. + // The borrow asset is never left on the balance, see _receiveFlashLoan(). + // So, any borrow asset on balance can be considered as a reward. + __rewardAmounts[i] = StrategyLib.balance(__rewardAssets[i]); + } + + // This strategy doesn't use $base.total at all + // but StrategyBase expects it to be set in doHardWork in order to calculate aprCompound + // so, we set it twice: here (old value) and in _compound (new value) + $base.total = total($); + } + + /// @notice revenueBaseAssetIndex_ What asset is used in share price calculation: 0 - collateral asset, 1 - borrow asset + function getRevenue( + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + uint oldPrice, + address vault_, + uint revenueBaseAssetIndex_ + ) external view returns (address[] memory assets, uint[] memory amounts) { + uint newPrice = _sharePrice($, vault_, revenueBaseAssetIndex_); + return _getRevenue($, oldPrice, newPrice, vault_, revenueBaseAssetIndex_); + } + + /// @notice Calculate revenue based on share price change in terms of strategy asset (== collateral asset) + /// @param oldPrice Previous share price in collateral asset + /// @param newPrice New share price in collateral asset + /// @param revenueBaseAssetIndex_ What asset is used in share price calculation: 0 - collateral asset, 1 - borrow asset + /// @return assets List of strategy assets (collateral asset only in our case) + /// @return amounts Corresponding amounts of strategy assets as revenue. If revenue negative return 0. + function _getRevenue( + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + uint oldPrice, + uint newPrice, + address vault_, + uint revenueBaseAssetIndex_ + ) internal view returns (address[] memory assets, uint[] memory amounts) { + amounts = new uint[](1); + assets = new address[](1); + + // @dev The strategy assets is array with collateral asset only + assets[0] = $.collateralAsset; + + if (newPrice > oldPrice && oldPrice != 0) { + uint _totalSupply = IVault(vault_).totalSupply(); + + // share price already takes into account accumulated interest + uint revenueBaseAmount = (newPrice - oldPrice) * _totalSupply / 1e18; + + // convert revenue-base-asset to collateral asset + if (revenueBaseAssetIndex_ == REVENUE_BASE_ASSET_0) { + // revenueBaseAmount is already in collateral asset + amounts[0] = revenueBaseAmount; + } else { + // revenueBaseAmount is in borrow asset + address borrowAsset = $.borrowAsset; + address addressProvider = IPool(IAToken($.lendingVault).POOL()).ADDRESSES_PROVIDER(); + (uint priceC18, uint priceB18) = ALMFLib.getPrices(addressProvider, assets[0], borrowAsset); + uint8 decimalsC = IERC20Metadata(assets[0]).decimals(); + uint8 decimalsB = IERC20Metadata(borrowAsset).decimals(); + + // convert amount in borrow asset to amount in collateral asset + amounts[0] = revenueBaseAmount * priceB18 * 10 ** decimalsC / priceC18 / 10 ** decimalsB; + } + } + } + + function liquidateRewards( + address platform_, + address exchangeAsset, + address[] memory rewardAssets_, + uint[] memory rewardAmounts_, + uint priceImpactTolerance + ) external returns (uint earnedExchangeAsset) { + earnedExchangeAsset = StrategyLib.liquidateRewards( + platform_, exchangeAsset, rewardAssets_, rewardAmounts_, priceImpactTolerance + ); + } + + function compound( + address platform_, + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + IStrategy.StrategyBaseStorage storage $base, + IFactory.Farm memory farm + ) external { + // assume below that there is only 1 asset - collateral asset + address asset = $.collateralAsset; + uint amount = StrategyLib.balance(asset); + + if (amount != 0) { + _depositAssets(platform_, $, farm, amount); + } + + // This strategy doesn't use $base.total at all + // but StrategyBase expects it to be set in doHardWork in order to calculate aprCompound + // so, we set it twice: here (new value) and in _claimRevenue (old value) + $base.total = total($); + } + + /// @notice Get share price in collateral or borrow asset + /// @param revenueBaseAssetIndex_ If equals to 0, share price is returned in collateral asset; otherwise in borrow asset + function _sharePrice( + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + address vault_, + uint revenueBaseAssetIndex_ + ) internal view returns (uint sharePrice) { + uint totalSupply = IERC20(vault_).totalSupply(); + if (totalSupply != 0) { + /// @dev Asset in which share price is calculated + address revenueBaseAsset = + revenueBaseAssetIndex_ == REVENUE_BASE_ASSET_0 ? $.collateralAsset : $.borrowAsset; + + /// @dev Real tvl in USD, decimals 18 + (uint __realTvl,) = realTvl($); + + /// @dev Collateral price from AAVE oracle, decimals 8 + uint revenueBaseAssetPrice8 = IAavePriceOracle( + IAaveAddressProvider(IPool(IAToken($.lendingVault).POOL()).ADDRESSES_PROVIDER()).getPriceOracle() + ).getAssetPrice(revenueBaseAsset); + + /// @dev Real tvl in base asset + uint amount = + __realTvl * 1e8 * 10 ** IERC20Metadata(revenueBaseAsset).decimals() / revenueBaseAssetPrice8 / 1e18; + + /// @dev Share price: base asset per vault-share, decimals = decimals of base asset + sharePrice = amount * 1e18 / totalSupply; + } + + return sharePrice; + } + + //endregion ------------------------------------- Revenue + + //region ------------------------------------- Additional functionality + /// @notice Set threshold for the asset + function setThreshold(address asset_, uint threshold_) external { + _getStorage().thresholds[asset_] = threshold_; + + emit SetThreshold(asset_, threshold_); + } + + /// @notice Reset share price after changing value of revenueBaseAsset in farm configuration + function resetSharePrice() external { + AlmfStrategyStorage storage $ = _getStorage(); + $.lastSharePrice = 0; + + emit ResetSharePrice(); + } + + //endregion ------------------------------------- Additional functionality + + //region ------------------------------------- Internal utils + function _getFlashLoanAmounts( + uint borrowAmount, + address borrowAsset + ) internal pure returns (address[] memory flashAssets, uint[] memory flashAmounts) { + flashAssets = new address[](1); + flashAssets[0] = borrowAsset; + flashAmounts = new uint[](1); + flashAmounts[0] = borrowAmount; + } + + function _ensureLtvValid(ALMFCalcLib.State memory state) internal pure { + if (state.debtBase != 0) { + uint ltv = ALMFCalcLib.getLtv(state.collateralBase, state.debtBase); + require(state.healthFactor > 1e18 && ltv < state.maxLtv, IControllable.IncorrectLtv(ltv)); + } + } + + function _getStorage() internal pure returns (AlmfStrategyStorage storage $) { + //slither-disable-next-line assembly + assembly { + $.slot := AAVE_MERKL_FARM_STRATEGY_STORAGE_LOCATION + } + } + //endregion ------------------------------------- Internal utils +} diff --git a/src/strategies/libs/ALMFLib2.sol b/src/strategies/libs/ALMFLib2.sol new file mode 100644 index 000000000..ba073525c --- /dev/null +++ b/src/strategies/libs/ALMFLib2.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {IPool} from "../../integrations/aave/IPool.sol"; +import {IAToken} from "../../integrations/aave/IAToken.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IFactory} from "../../interfaces/IFactory.sol"; +import {ILeverageLendingStrategy} from "../../interfaces/ILeverageLendingStrategy.sol"; +import {IPlatform} from "../../interfaces/IPlatform.sol"; +import {IPriceReader} from "../../interfaces/IPriceReader.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {SharedLib} from "./SharedLib.sol"; +import {StrategyIdLib} from "./StrategyIdLib.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +/// @notice Several standalone functions were moved here to reduce size of ALMFLib +library ALMFLib2 { + using SafeERC20 for IERC20; + + string internal constant STRATEGY_LOGIC_ID = StrategyIdLib.AAVE_LEVERAGE_MERKL_FARM; + + //region ------------------------------------- View + function _poolTvl(address platform_, address aToken) external view returns (uint tvlUsd) { + address asset = IAToken(aToken).UNDERLYING_ASSET_ADDRESS(); + + IPriceReader priceReader = IPriceReader(IPlatform(platform_).priceReader()); + + // get price of 1 amount of asset in USD with decimals 18 + // assume that {trusted} value doesn't matter here + // slither-disable-next-line unused-return + (uint price,) = priceReader.getPrice(asset); + + return IAToken(aToken).totalSupply() * price / (10 ** IERC20Metadata(asset).decimals()); + } + + /// @return targetMinLtv Minimum target ltv, INTERNAL_PRECISION + /// @return targetMaxLtv Maximum target ltv, INTERNAL_PRECISION + function _getFarmLtvConfig(IFactory.Farm memory farm) internal pure returns (uint targetMinLtv, uint targetMaxLtv) { + return (farm.nums[0], farm.nums[1]); + } + + //endregion ------------------------------------- View + + //region ------------------------------------- Init vars, desc + function genDesc(IFactory.Farm memory farm) external view returns (string memory) { + return _genDesc(farm); + } + + function _genDesc(IFactory.Farm memory farm) internal view returns (string memory) { + address aToken = farm.addresses[0]; + //slither-disable-next-line calls-loop + return string.concat( + "Supply ", + IERC20Metadata(IAToken(aToken).UNDERLYING_ASSET_ADDRESS()).symbol(), + " to AAVE ", + SharedLib.shortAddress(IAToken(aToken).POOL()), + " with leverage, Merkl rewards" + ); + } + + /// @dev See IStrategy.initVariants + function initVariants(address platform_) + external + view + returns (string[] memory variants, address[] memory addresses, uint[] memory nums, int24[] memory ticks) + { + return SharedLib.initVariantsForFarm(platform_, STRATEGY_LOGIC_ID, _genDesc); + } + + function getSpecificName( + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + IFactory.Farm memory farm + ) external view returns (string memory, bool) { + (uint targetMinLtv, uint targetMaxLtv) = _getFarmLtvConfig(farm); + + return ( + string.concat( + IERC20Metadata($.borrowAsset).symbol(), + " ", + Strings.toString(targetMinLtv / 100), + "-", + Strings.toString(targetMaxLtv / 100) + ), + true + ); + } + + /// @dev A part of initialization code moved here to reduce size of the strategy + function _postInit( + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + address platform_, + address lendingVault_, + address collateralAsset_, + address borrowAsset_, + IFactory.Farm memory farm + ) external { + address pool = IAToken(lendingVault_).POOL(); + IERC20(collateralAsset_).forceApprove(pool, type(uint).max); + IERC20(borrowAsset_).forceApprove(pool, type(uint).max); + + address swapper = IPlatform(platform_).swapper(); + IERC20(collateralAsset_).forceApprove(swapper, type(uint).max); + IERC20(borrowAsset_).forceApprove(swapper, type(uint).max); + + // ------------------------------ Enable E-Mode for the user account in AAVE if necessary + uint8 eModeCategoryId = uint8(farm.nums[3]); + if (eModeCategoryId != 0) { + // E-mode is activated once here + // Assume here that collateral and borrow assets are always the same in belong to the given E-category + // E-mode is never reset because the strategy doesn't use any other borrow assets + IPool(pool).setUserEMode(eModeCategoryId); + } + + // ------------------------------ Set up all params in use + // // Multiplier of flash amount for borrow on deposit. Default is 100_00 = 100% + // $.depositParam0 = 100_00; + // // Multiplier of borrow amount to take into account max flash loan fee in maxDeposit. Default is 99_80 = 99.8% + // $.depositParam1 = 99_80; + + // Multiplier of debt diff + $.increaseLtvParam0 = 100_80; + // Multiplier of swap borrow asset to collateral in flash loan callback + $.increaseLtvParam1 = 99_00; + // Multiplier of collateral diff + $.decreaseLtvParam0 = 101_00; + // + // Swap price impact tolerance, ConstantsLib.DENOMINATOR + $.swapPriceImpactTolerance0 = 1_000; + $.swapPriceImpactTolerance1 = 1_000; + + // Leverage correction coefficient, INTERNAL_PRECISION. Default is 300 = 0.03 + $.withdrawParam0 = 300; + + // // Multiplier of amount allowed to be deposited after withdraw. Default is 100_00 == 100% (deposit forbidden) + // $.withdrawParam1 = 100_00; + // // withdrawParam2 allows to disable withdraw through increasing ltv if leverage is near to target + // $.withdrawParam2 = 100_00; + + $.flashLoanKind = farm.nums[2]; + } + + //endregion ------------------------------------- Init vars, desc +} diff --git a/src/strategies/libs/LeverageLendingLib.sol b/src/strategies/libs/LeverageLendingLib.sol index be794bb74..3e6a035a1 100644 --- a/src/strategies/libs/LeverageLendingLib.sol +++ b/src/strategies/libs/LeverageLendingLib.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.28; import {IBVault} from "../../integrations/balancer/IBVault.sol"; +import {IBComposableStablePoolMinimal} from "../../integrations/balancer/IBComposableStablePoolMinimal.sol"; import {IControllable} from "../../interfaces/IControllable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ILeverageLendingStrategy} from "../../interfaces/ILeverageLendingStrategy.sol"; @@ -70,4 +71,22 @@ library LeverageLendingLib { IBVault(flashLoanVault).flashLoan(address(this), flashAssets, flashAmounts, ""); } } + + /// @notice Get flash loan fee, decimals 18 + function getFlashFee18(address flashLoanVault, uint flashLoanKind) internal view returns (uint) { + if (flashLoanKind == uint(ILeverageLendingStrategy.FlashLoanKind.Default_0)) { + return IBComposableStablePoolMinimal(IBVault(flashLoanVault).getProtocolFeesCollector()) + .getFlashLoanFeePercentage(); // decimals 18 + } else if (flashLoanKind == uint(ILeverageLendingStrategy.FlashLoanKind.BalancerV3_1)) { + // flash loan in balancer v3 is free + return 0; + } else if ( + flashLoanKind == uint(ILeverageLendingStrategy.FlashLoanKind.UniswapV3_2) + || flashLoanKind == uint(ILeverageLendingStrategy.FlashLoanKind.AlgebraV4_3) + ) { + // fee is in hundredths of a bip, i.e. 100_00 = 1% + return uint(IUniswapV3PoolImmutables(flashLoanVault).fee()) * 1e12; + } + return 0; // unknown + } } diff --git a/src/strategies/libs/SiloALMFLib.sol b/src/strategies/libs/SiloALMFLib.sol index 05c7eea0f..dc2460daa 100644 --- a/src/strategies/libs/SiloALMFLib.sol +++ b/src/strategies/libs/SiloALMFLib.sol @@ -1053,5 +1053,5 @@ library SiloALMFLib { // assume that collateral asset is always WrappedMetaVault, i.e. wmetaUSD return IMetaVault(IWrappedMetaVault(wrappedMetaVault).metaVault()); } - //region ------------------------------------- Transient prices cache + //endregion ------------------------------------- Transient prices cache } diff --git a/src/strategies/libs/StrategyDeveloperLib.sol b/src/strategies/libs/StrategyDeveloperLib.sol index 91bced985..c3778b10f 100644 --- a/src/strategies/libs/StrategyDeveloperLib.sol +++ b/src/strategies/libs/StrategyDeveloperLib.sol @@ -107,6 +107,9 @@ library StrategyDeveloperLib { if (CommonLib.eq(strategyId, StrategyIdLib.SILO_MERKL_FARM)) { return 0xcd18A818f2eC5C21EEF6771183eD5641B15da247; } + if (CommonLib.eq(strategyId, StrategyIdLib.AAVE_LEVERAGE_MERKL_FARM)) { + return 0xD1807F5A04a7bd11C26aA14F076054f75d8C6255; + } return address(0); } } diff --git a/src/strategies/libs/StrategyIdLib.sol b/src/strategies/libs/StrategyIdLib.sol index 1770affef..fe5878c15 100644 --- a/src/strategies/libs/StrategyIdLib.sol +++ b/src/strategies/libs/StrategyIdLib.sol @@ -37,4 +37,5 @@ library StrategyIdLib { string internal constant COMPOUND_V2 = "Compound V2"; string internal constant SILO_MANAGED_MERKL_FARM = "Silo Managed Merkl Farm"; string internal constant SILO_MERKL_FARM = "Silo Merkl Farm"; // SiMerklF + string internal constant AAVE_LEVERAGE_MERKL_FARM = "Aave Leverage Merkl Farm"; } diff --git a/src/strategies/libs/UniswapV3MathLib.sol b/src/strategies/libs/UniswapV3MathLib.sol index 2ecf974df..1efad1c32 100644 --- a/src/strategies/libs/UniswapV3MathLib.sol +++ b/src/strategies/libs/UniswapV3MathLib.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {FixedPointMathLib} from "../../../lib/solady/src/utils/FixedPointMathLib.sol"; +import {FixedPointMathLib} from "@solady/utils/FixedPointMathLib.sol"; library UniswapV3MathLib { uint8 internal constant RESOLUTION = 96; diff --git a/src/tokenomics/libs/RecoveryLib.sol b/src/tokenomics/libs/RecoveryLib.sol index 197a7dd8a..cafd9308b 100755 --- a/src/tokenomics/libs/RecoveryLib.sol +++ b/src/tokenomics/libs/RecoveryLib.sol @@ -10,7 +10,7 @@ import {ISwapper} from "../../interfaces/ISwapper.sol"; import {IPriceReader} from "../../interfaces/IPriceReader.sol"; import {IUniswapV3Pool} from "../../integrations/uniswapv3/IUniswapV3Pool.sol"; import {IWrappedMetaVault} from "../../interfaces/IWrappedMetaVault.sol"; -import {LibPRNG} from "../../../lib/solady/src/utils/LibPRNG.sol"; +import {LibPRNG} from "@solady/utils/LibPRNG.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; library RecoveryLib { diff --git a/test/adapters/AaveV3Adapter.Plasma.t.sol b/test/adapters/AaveV3Adapter.Plasma.t.sol new file mode 100644 index 000000000..e5ef0a4dd --- /dev/null +++ b/test/adapters/AaveV3Adapter.Plasma.t.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Swapper} from "../../src/core/Swapper.sol"; +// import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {AaveV3Adapter} from "../../src/adapters/AaveV3Adapter.sol"; +import {AmmAdapterIdLib} from "../../src/adapters/libs/AmmAdapterIdLib.sol"; +import {ERC4626Adapter} from "../../src/adapters/ERC4626Adapter.sol"; +import {IAmmAdapter} from "../../src/interfaces/IAmmAdapter.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {ISwapper} from "../../src/interfaces/ISwapper.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; +import {Proxy} from "../../src/core/proxy/Proxy.sol"; +import {Test} from "forge-std/Test.sol"; +import {UniswapV3Adapter} from "../../src/adapters/UniswapV3Adapter.sol"; +// import {console} from "forge-std/console.sol"; + +contract AaveV3AdapterTest is Test { + address public constant PLATFORM = PlasmaConstantsLib.PLATFORM; + + bytes32 public _hash; + IAmmAdapter public adapter; + + uint internal constant FORK_BLOCK = 6452516; // Nov-17-2025 12:36:59 UTC + + constructor() { + vm.selectFork(vm.createFork(vm.envString("PLASMA_RPC_URL"), FORK_BLOCK)); + + // _upgradePlatform(); + } + + //region ------------------ Tests + function testSwaps() public { + _addAdapter(); + _addAdapterErc4626(); + _addAdapterErcUniswapV3(); + + ISwapper swapper = ISwapper(IPlatform(PLATFORM).swapper()); + address multisig = IPlatform(PLATFORM).multisig(); + + ISwapper.AddPoolData[] memory bcPools = new ISwapper.AddPoolData[](1); + bcPools[0] = _makePoolData( + PlasmaConstantsLib.POOL_OKU_TRADE_USDT0_WETH, + AmmAdapterIdLib.UNISWAPV3, + PlasmaConstantsLib.TOKEN_WETH, + PlasmaConstantsLib.TOKEN_USDT0 + ); + + ISwapper.AddPoolData[] memory pools = new ISwapper.AddPoolData[](3); + pools[0] = _makePoolData( + PlasmaConstantsLib.AAVE_V3_POOL_WETH, + AmmAdapterIdLib.AAVE_V3, + PlasmaConstantsLib.AAVE_V3_POOL_WETH, + PlasmaConstantsLib.TOKEN_WETH + ); + pools[1] = _makePoolData( + PlasmaConstantsLib.TOKEN_WAPLAWETH, + AmmAdapterIdLib.ERC_4626, + PlasmaConstantsLib.TOKEN_WAPLAWETH, + PlasmaConstantsLib.TOKEN_WETH + ); + pools[2] = _makePoolData( + PlasmaConstantsLib.POOL_OKU_TRADE_USDT0_WETH, + AmmAdapterIdLib.UNISWAPV3, + PlasmaConstantsLib.TOKEN_WETH, + PlasmaConstantsLib.TOKEN_USDT0 + ); + + vm.prank(multisig); + swapper.addPools(pools, false); + + { + uint snapshotId = vm.snapshotState(); + // ------------------------------- WETH => aPlaWETH + deal(PlasmaConstantsLib.TOKEN_WETH, address(this), 1e18); + + IERC20(PlasmaConstantsLib.TOKEN_WETH).approve(address(swapper), type(uint).max); + swapper.swap(PlasmaConstantsLib.TOKEN_WETH, PlasmaConstantsLib.AAVE_V3_POOL_WETH, 1e18, 1000); + + uint balanceAToken = IERC20(PlasmaConstantsLib.AAVE_V3_POOL_WETH).balanceOf(address(this)); + assertApproxEqRel(balanceAToken, 1e18, 1e16, "WETH => aPlaWETH"); + + // ------------------------------- aPlaWETH => WETH + IERC20(PlasmaConstantsLib.AAVE_V3_POOL_WETH).approve(address(swapper), type(uint).max); + swapper.swap(PlasmaConstantsLib.AAVE_V3_POOL_WETH, PlasmaConstantsLib.TOKEN_WETH, balanceAToken, 0); + + uint finalBalance = IERC20(PlasmaConstantsLib.TOKEN_WETH).balanceOf(address(this)); + assertApproxEqAbs(finalBalance, 1e18, 1, "aPlaWETH => WETH"); + + vm.revertToState(snapshotId); + } + + { + uint snapshotId = vm.snapshotState(); + // ------------------------------- WETH => waPlaWETH + deal(PlasmaConstantsLib.TOKEN_WETH, address(this), 1e18); + + IERC20(PlasmaConstantsLib.TOKEN_WETH).approve(address(swapper), type(uint).max); + swapper.swap(PlasmaConstantsLib.TOKEN_WETH, PlasmaConstantsLib.TOKEN_WAPLAWETH, 1e18, 1000); + + uint balanceWrappedAToken = IERC20(PlasmaConstantsLib.TOKEN_WAPLAWETH).balanceOf(address(this)); + assertApproxEqRel(balanceWrappedAToken, 1e18, 1e16, "WETH => waPlaWETH"); + + // ------------------------------- waPlaWETH => WETH + IERC20(PlasmaConstantsLib.TOKEN_WAPLAWETH).approve(address(swapper), type(uint).max); + swapper.swap(PlasmaConstantsLib.TOKEN_WAPLAWETH, PlasmaConstantsLib.TOKEN_WETH, balanceWrappedAToken, 0); + + uint finalBalance = IERC20(PlasmaConstantsLib.TOKEN_WETH).balanceOf(address(this)); + assertApproxEqAbs(finalBalance, 1e18, 1, "waPlaWETH => WETH"); + vm.revertToState(snapshotId); + } + } + + function testViewMethods() public { + _addAdapter(); + + assertEq(keccak256(bytes(adapter.ammAdapterId())), _hash, "hash"); + + uint price = adapter.getPrice( + PlasmaConstantsLib.AAVE_V3_POOL_USDT0, PlasmaConstantsLib.AAVE_V3_POOL_USDT0, address(0), 1e6 + ); + assertEq(price, 1e6, "getPrice atoken aUSDT0"); + + price = adapter.getPrice( + PlasmaConstantsLib.AAVE_V3_POOL_WETH, PlasmaConstantsLib.AAVE_V3_POOL_WETH, address(0), 1e18 + ); + assertEq(price, 1e18, "getPrice atoken aWETH"); + + price = adapter.getPrice( + PlasmaConstantsLib.AAVE_V3_POOL_WETH, + PlasmaConstantsLib.TOKEN_WETH, + PlasmaConstantsLib.AAVE_V3_POOL_WETH, + 1e18 + ); + assertEq(price, 1e18, "getPrice WETH"); + + price = adapter.getPrice( + PlasmaConstantsLib.AAVE_V3_POOL_WETH, PlasmaConstantsLib.AAVE_V3_POOL_WETH, address(0), 1e18 + ); + assertEq(price, 1e18, "atoken=>0"); + + price = adapter.getPrice(PlasmaConstantsLib.AAVE_V3_POOL_WETH, PlasmaConstantsLib.TOKEN_WETH, address(0), 1e18); + assertEq(price, 0, "asset=>0"); + + price = adapter.getPrice( + PlasmaConstantsLib.AAVE_V3_POOL_WETH, + PlasmaConstantsLib.AAVE_V3_POOL_SUSDE, + PlasmaConstantsLib.TOKEN_SUSDE, + 1e18 + ); + assertEq(price, 0, "atoken != pool"); + + vm.expectRevert("Not supported"); + adapter.getLiquidityForAmounts(address(0), new uint[](2)); + + vm.expectRevert("Not supported"); + adapter.getProportions(address(0)); + + address[] memory poolTokens = adapter.poolTokens(PlasmaConstantsLib.AAVE_V3_POOL_WETH); + assertEq(poolTokens.length, 2); + assertEq(poolTokens[0], PlasmaConstantsLib.TOKEN_WETH, "pool tokens 0"); + assertEq(poolTokens[1], PlasmaConstantsLib.AAVE_V3_POOL_WETH, "pool tokens 1"); + + assertEq(adapter.supportsInterface(type(IAmmAdapter).interfaceId), true, "IAmmAdapter"); + assertEq(adapter.supportsInterface(type(IERC165).interfaceId), true, "IERC165"); + } + + function testGetTwaPrice() public { + _addAdapter(); + + vm.expectRevert("Not supported"); + adapter.getTwaPrice(address(0), address(0), address(0), 0, 0); + } + + //endregion ------------------ Tests + + //region ------------------ Helpers + function _makePoolData( + address pool, + string memory ammAdapterId, + address tokenIn, + address tokenOut + ) internal pure returns (ISwapper.AddPoolData memory) { + return ISwapper.AddPoolData({pool: pool, ammAdapterId: ammAdapterId, tokenIn: tokenIn, tokenOut: tokenOut}); + } + + function _addAdapter() internal { + _hash = keccak256(bytes(AmmAdapterIdLib.AAVE_V3)); + + Proxy proxy = new Proxy(); + proxy.initProxy(address(new AaveV3Adapter())); + + adapter = IAmmAdapter(address(proxy)); + adapter.init(PLATFORM); + + string memory id = AmmAdapterIdLib.AAVE_V3; + vm.prank(IPlatform(PLATFORM).multisig()); + IPlatform(PLATFORM).addAmmAdapter(id, address(proxy)); + } + + function _addAdapterErc4626() internal { + _hash = keccak256(bytes(AmmAdapterIdLib.ERC_4626)); + + Proxy proxy = new Proxy(); + proxy.initProxy(address(new ERC4626Adapter())); + + adapter = IAmmAdapter(address(proxy)); + adapter.init(PLATFORM); + + string memory id = AmmAdapterIdLib.ERC_4626; + vm.prank(IPlatform(PLATFORM).multisig()); + IPlatform(PLATFORM).addAmmAdapter(id, address(proxy)); + } + + function _addAdapterErcUniswapV3() internal { + _hash = keccak256(bytes(AmmAdapterIdLib.UNISWAPV3)); + + Proxy proxy = new Proxy(); + proxy.initProxy(address(new UniswapV3Adapter())); + + adapter = IAmmAdapter(address(proxy)); + adapter.init(PLATFORM); + + string memory id = AmmAdapterIdLib.UNISWAPV3; + vm.prank(IPlatform(PLATFORM).multisig()); + IPlatform(PLATFORM).addAmmAdapter(id, address(proxy)); + } + //endregion ------------------ Helpers + + function _upgradePlatform() internal { + rewind(1 days); + + IPlatform platform = IPlatform(PLATFORM); + + address[] memory proxies = new address[](1); + address[] memory implementations = new address[](1); + + proxies[0] = platform.swapper(); + implementations[0] = address(new Swapper()); + + if (platform.pendingPlatformUpgrade().proxies.length != 0) { + vm.startPrank(platform.multisig()); + platform.cancelUpgrade(); + } + + vm.startPrank(platform.multisig()); + platform.announcePlatformUpgrade("2025.10.01-alpha", proxies, implementations); + + skip(1 days); + platform.upgrade(); + vm.stopPrank(); + } +} diff --git a/test/base/UniversalTest.sol b/test/base/UniversalTest.sol index 2bef2baf7..fb2938df5 100644 --- a/test/base/UniversalTest.sol +++ b/test/base/UniversalTest.sol @@ -46,6 +46,8 @@ abstract contract UniversalTest is Test, ChainSetup, Utils { bool public allowZeroApr = false; uint public poolVolumeSwapAmount0Multiplier = 2; uint public poolVolumeSwapAmount1Multiplier = 2; + /// @notice If true, then zero totalRevenueUSD won't cause test failure. False by default. + bool internal allowZeroTotalRevenueUSD; mapping(address pool => uint multiplier) public poolVolumeSwapAmount0MultiplierForPool; mapping(address pool => uint multiplier) public poolVolumeSwapAmount1MultiplierForPool; @@ -412,7 +414,9 @@ abstract contract UniversalTest is Test, ChainSetup, Utils { if (__assets.length > 0) { (uint totalRevenueUSD,,,) = IPriceReader(platform.priceReader()).getAssetsPrice(__assets, amounts); - assertGt(totalRevenueUSD, 0, "Universal test: estimated totalRevenueUSD is zero"); + if (!allowZeroTotalRevenueUSD) { + assertGt(totalRevenueUSD, 0, "Universal test: estimated totalRevenueUSD is zero"); + } assertGt(__assets.length, 0, "Universal test: getRevenue assets length is zero"); if (totalRevenueUSD == 0) { for (uint x; x < __assets.length; ++x) { @@ -741,7 +745,7 @@ abstract contract UniversalTest is Test, ChainSetup, Utils { vm.prank(platform.multisig()); strategy.emergencyStopInvesting(); - assertEq(strategy.total(), 0); + assertEq(strategy.total(), 0, "emergency total 0"); IVault(vars.vault) .withdrawAssets(assets, IERC20(vars.vault).balanceOf(address(this)), new uint[](assets.length)); diff --git a/test/base/chains/EthereumSetup.sol b/test/base/chains/EthereumSetup.sol index 418559ebc..3f065726a 100644 --- a/test/base/chains/EthereumSetup.sol +++ b/test/base/chains/EthereumSetup.sol @@ -10,10 +10,10 @@ import {DeployCore} from "../../../script/base/DeployCore.sol"; abstract contract EthereumSetup is ChainSetup, DeployCore { bool public showDeployLog; - uint internal constant FORK_BLOCK = 21680000; // Jan-22-2025 12:22:23 PM + uint internal constant FORK_BLOCK_DEFAULT = 21680000; // Jan-22-2025 12:22:23 PM constructor() { - vm.selectFork(vm.createFork(vm.envString("ETHEREUM_RPC_URL"), FORK_BLOCK)); + vm.selectFork(vm.createFork(vm.envString("ETHEREUM_RPC_URL"), FORK_BLOCK_DEFAULT)); } function testSetupStub() external {} diff --git a/test/strategies/ALMF.Ethereum.t.sol b/test/strategies/ALMF.Ethereum.t.sol new file mode 100644 index 000000000..3498956c2 --- /dev/null +++ b/test/strategies/ALMF.Ethereum.t.sol @@ -0,0 +1,660 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IControllable} from "../../src/interfaces/IControllable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IFarmingStrategy} from "../../src/interfaces/IFarmingStrategy.sol"; +import {ILeverageLendingStrategy} from "../../src/interfaces/ILeverageLendingStrategy.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {IFactory} from "../../src/interfaces/IFactory.sol"; +import {IStabilityVault} from "../../src/interfaces/IStabilityVault.sol"; +import {IStrategy} from "../../src/interfaces/IStrategy.sol"; +import {IVault} from "../../src/interfaces/IVault.sol"; +import {EthereumLib} from "../../chains/EthereumLib.sol"; +import {EthereumSetup} from "../base/chains/EthereumSetup.sol"; +import {StrategyIdLib} from "../../src/strategies/libs/StrategyIdLib.sol"; +import {UniversalTest} from "../base/UniversalTest.sol"; +import {PriceReader} from "../../src/core/PriceReader.sol"; +import {IAaveAddressProvider} from "../../src/integrations/aave/IAaveAddressProvider.sol"; +import {IAavePriceOracle} from "../../src/integrations/aave/IAavePriceOracle.sol"; +import {IPool} from "../../src/integrations/aave/IPool.sol"; +import {AaveLeverageMerklFarmStrategy} from "../../src/strategies/AaveLeverageMerklFarmStrategy.sol"; +import {console} from "forge-std/console.sol"; +import {SharedFarmMakerLib} from "../../chains/shared/SharedFarmMarketLib.sol"; + +contract ALMFStrategyEthereumTest is EthereumSetup, UniversalTest { + uint public constant REVERT_NO = 0; + uint public constant REVERT_NOT_ENOUGH_LIQUIDITY = 1; + uint public constant REVERT_INSUFFICIENT_BALANCE = 2; + + uint internal constant INDEX_INIT_0 = 0; + uint internal constant INDEX_AFTER_DEPOSIT_1 = 1; + uint internal constant INDEX_AFTER_WAIT_2 = 2; + uint internal constant INDEX_AFTER_HARDWORK_3 = 3; + uint internal constant INDEX_AFTER_WITHDRAW_4 = 4; + + struct State { + uint ltv; + uint maxLtv; + uint leverage; + uint maxLeverage; + uint targetLeverage; + uint targetLeveragePercent; + uint collateralAmount; + uint debtAmount; + uint total; + uint sharePrice; + uint strategyBalanceAsset; + uint userBalanceAsset; + uint realTvl; + uint realSharePrice; + uint vaultBalance; + address[] revenueAssets; + uint[] revenueAmounts; + } + + uint internal constant FORK_BLOCK = 23819383; // Nov-17-2025 02:07:23 PM +UTC + + address internal constant POOL = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; + address internal constant ATOKEN_USDC = 0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c; + address internal constant ATOKEN_WBTC = 0x5Ee5bf7ae06D1Be5997A1A72006FE6C607eC6DE8; + + uint internal constant DEFAULT_AMOUNT = 0.1e8; + + constructor() { + vm.selectFork(vm.createFork(vm.envString("ETHEREUM_RPC_URL"), FORK_BLOCK)); + + allowZeroApr = true; + duration1 = 0.1 hours; + duration2 = 0.1 hours; + duration3 = 0.1 hours; + + // ALMF uses real share price as share price + // so it cannot initialize share price during deposit. + // It sets initial value of share price in first claimRevenue. + // As result, following check is failed in universal test: + // "Universal test: estimated totalRevenueUSD is zero" + // So, we should disable it by setting allowZeroTotalRevenueUSD. + // And make all checks in additional tests instead. + allowZeroTotalRevenueUSD = true; + + // _upgradePlatform(platform.multisig(), IPlatform(PLATFORM).priceReader()); + } + + //region --------------------------------------- Universal test + function testALMFEthereum() public universalTest { + _addStrategy(_addFarm()); + } + + function _addStrategy(uint farmId) internal { + strategies.push( + Strategy({ + id: StrategyIdLib.AAVE_LEVERAGE_MERKL_FARM, + pool: address(0), + farmId: farmId, + strategyInitAddresses: new address[](0), + strategyInitNums: new uint[](0) + }) + ); + } + + function _addFarm() internal returns (uint farmId) { + address[] memory rewards = new address[](1); + rewards[0] = EthereumLib.TOKEN_USDC; + // todo rewards[1] = EthereumLib.TOKEN_WXPL; + + IFactory.Farm[] memory farms = new IFactory.Farm[](1); + farms[0] = SharedFarmMakerLib._makeAaveLeverageMerklFarm( + ATOKEN_WBTC, + ATOKEN_USDC, + EthereumLib.POOL_UNISWAPV3_USDC_WETH_500, + rewards, + 49_00, // min target ltv + 50_97, // max target ltv + uint(ILeverageLendingStrategy.FlashLoanKind.UniswapV3_2), + 0, // eMode is not used + 0 // share price is calculated in collateral asset per USD + ); + + vm.startPrank(platform.multisig()); + factory.addFarms(farms); + + return factory.farmsLength() - 1; + } + + function _preDeposit() internal override { + // thresholds + vm.prank(platform.multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(EthereumLib.TOKEN_WBTC, 1e2); + vm.prank(platform.multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(EthereumLib.TOKEN_USDC, 1e6); + + // ---------------------------------- Make additional tests + uint snapshot = vm.snapshotState(); + + // initial supply + _tryToDepositToVault( + IStrategy(currentStrategy).vault(), DEFAULT_AMOUNT, REVERT_NO, makeAddr("initial supplier") + ); + + // check revenue (replacement for "Universal test: estimated totalRevenueUSD is zero") + _testDepositTwoHardworks(); + + // set TL, deposit, change TL, withdraw/deposit => leverage was changed toward new TL + _testDepositChangeLtvWithdraw(); + _testDepositChangeLtvDeposit(); + + // check deposit-wait 30 days-hardwork-withdraw results + _testDepositWaitHardworkWithdraw(); + + vm.revertToState(snapshot); + } + + function _preHardWork() internal override { + // emulate merkl rewards + deal(EthereumLib.TOKEN_USDC, currentStrategy, 1e6); + deal(EthereumLib.TOKEN_WETH, currentStrategy, 1e18); + } + + //endregion --------------------------------------- Universal test + + //region --------------------------------------- Additional tests + function _testDepositTwoHardworks() internal { + uint snapshot = vm.snapshotState(); + uint amount = DEFAULT_AMOUNT; + + IStrategy strategy = IStrategy(currentStrategy); + + uint priceCollateral8 = _getCollateralPrice8(strategy.assets()[0]); + + // --------------------------------------------- Deposit + State memory stateAfterDeposit = _getState(); + _tryToDepositToVault(strategy.vault(), amount, REVERT_NO, address(this)); + vm.roll(block.number + 6); + + // --------------------------------------------- Hardwork 1 + _skip(1 days, 0); + deal(EthereumLib.TOKEN_USDC, currentStrategy, 100e6); + + vm.prank(platform.multisig()); + IVault(strategy.vault()).doHardWork(); + + State memory stateAfterHW1 = _getState(); + + // --------------------------------------------- Hardwork 2 + _skip(1 days, 0); + deal(EthereumLib.TOKEN_USDC, currentStrategy, 300e6); + + vm.prank(platform.multisig()); + IVault(strategy.vault()).doHardWork(); + + State memory stateAfterHW2 = _getState(); + + assertEq( + stateAfterDeposit.revenueAmounts[0], + 0, + "Revenue before first claimReview is 0 because share price is not initialized yet" + ); + assertApproxEqRel( + stateAfterHW1.revenueAmounts[0] * priceCollateral8 * 1e6 / 1e8 / 1e8, + 100e6, + 20e16, + "Revenue after first hardwork is ~$100" + ); + assertApproxEqRel( + stateAfterHW2.revenueAmounts[0] * priceCollateral8 * 1e6 / 1e8 / 1e8, + 300e6, + 20e16, + "Revenue after first hardwork is ~$300" + ); + vm.revertToState(snapshot); + } + + function _testDepositChangeLtvWithdraw() internal { + { + (, State memory stateAfterDeposit, State memory stateAfterWithdraw) = + _depositChangeLtvWithdraw(49_00, 50_97, 52_00, 51_97); + + assertApproxEqRel( + stateAfterDeposit.leverage, + stateAfterDeposit.targetLeverage, + 1e16, + "Leverage after deposit should be equal to target 111" + ); + assertLt( + stateAfterDeposit.leverage, + stateAfterWithdraw.targetLeverage, + "leverage before withdraw less than target" + ); + assertGt(stateAfterWithdraw.leverage, stateAfterDeposit.leverage, "withdraw increased the leverage"); + } + { + (, State memory stateAfterDeposit, State memory stateAfterWithdraw) = + _depositChangeLtvWithdraw(49_00, 50_97, 47_00, 48_97); + + assertApproxEqRel( + stateAfterDeposit.leverage, + stateAfterDeposit.targetLeverage, + 1e16, + "Leverage after deposit should be equal to target 222" + ); + assertGt( + stateAfterDeposit.leverage, + stateAfterWithdraw.targetLeverage, + "leverage before withdraw greater than target" + ); + assertLt(stateAfterWithdraw.leverage, stateAfterDeposit.leverage, "withdraw decreased the leverage"); + } + } + + function _testDepositChangeLtvDeposit() internal { + { + (, State memory stateAfterDeposit, State memory stateAfterDeposit2) = + _depositChangeLtvDeposit(49_00, 50_97, 52_00, 51_97); + + assertApproxEqRel( + stateAfterDeposit.leverage, + stateAfterDeposit.targetLeverage, + 1e16, + "Leverage after deposit should be equal to target 333" + ); + assertLt( + stateAfterDeposit.leverage, + stateAfterDeposit2.targetLeverage, + "leverage before withdraw less than target" + ); + assertGt(stateAfterDeposit2.leverage, stateAfterDeposit.leverage, "deposit2 increased the leverage"); + } + { + (, State memory stateAfterDeposit, State memory stateAfterDeposit2) = + _depositChangeLtvDeposit(49_00, 50_97, 47_00, 48_97); + + assertApproxEqRel( + stateAfterDeposit.leverage, + stateAfterDeposit.targetLeverage, + 1e16, + "Leverage after deposit should be equal to target 444" + ); + assertGt( + stateAfterDeposit.leverage, + stateAfterDeposit2.targetLeverage, + "leverage before deposit2 greater than target" + ); + assertLt(stateAfterDeposit2.leverage, stateAfterDeposit.leverage, "deposit2 decreased the leverage"); + } + } + + function _testDepositWithdrawUsingFlashLoan( + address flashLoanVault, + ILeverageLendingStrategy.FlashLoanKind kind_ + ) internal { + uint snapshot = vm.snapshotState(); + _setUpFlashLoanVault(flashLoanVault, kind_); + + uint amount = DEFAULT_AMOUNT; + State[] memory states = _depositWithdraw(amount, EthereumLib.TOKEN_USDC, 0, 0, false); + vm.revertToState(snapshot); + + assertApproxEqRel( + states[INDEX_AFTER_WITHDRAW_4].total, + states[INDEX_INIT_0].total, + states[INDEX_INIT_0].total / 100_000, + "Total should return back to prev value" + ); + assertApproxEqRel(states[4].userBalanceAsset, amount, amount / 50, "User shouldn't loss more than 2%"); + } + + function _testDepositWaitHardworkWithdraw() internal { + uint amount = DEFAULT_AMOUNT; + + // --------------------------------------------- Deposit+withdraw without hardwork + uint snapshot = vm.snapshotState(); + State[] memory statesInstant = _depositWithdraw(amount, EthereumLib.TOKEN_USDC, 0, 0, true); + vm.revertToState(snapshot); + + // --------------------------------------------- Deposit, wait, [no rewards], hardwork, withdraw + snapshot = vm.snapshotState(); + State[] memory statesHW1 = _depositWithdraw(amount, EthereumLib.TOKEN_USDC, 0, 1 days, true); + vm.revertToState(snapshot); + + // --------------------------------------------- Deposit, wait, rewards, hardwork, withdraw + snapshot = vm.snapshotState(); + State[] memory statesHW2 = _depositWithdraw(amount, EthereumLib.TOKEN_USDC, 100e6, 1 days, true); + vm.revertToState(snapshot); + + // --------------------------------------------- Get WETH price + uint collateralPrice8 = _getCollateralPrice8(IStrategy(currentStrategy).assets()[0]); + + // --------------------------------------------- Compare results + assertApproxEqAbs( + statesHW2[INDEX_AFTER_HARDWORK_3].total - statesInstant[INDEX_AFTER_HARDWORK_3].total, + 100e18 * 90 / 100, // todo check platform fee value + 6e18, + "total is increased on rewards amount - fees" + ); + assertLt( + statesHW1[INDEX_AFTER_HARDWORK_3].total, + statesInstant[INDEX_AFTER_HARDWORK_3].total, + "total is decreased because the borrow rate exceeds supply rate" + ); + + assertLt( + statesHW1[INDEX_AFTER_WITHDRAW_4].userBalanceAsset, + statesInstant[INDEX_AFTER_WITHDRAW_4].userBalanceAsset, + "user lost some amount because of borrow rate" + ); + assertApproxEqRel( + statesHW2[INDEX_AFTER_WITHDRAW_4].userBalanceAsset, + 100e8 * 90 / 100 / collateralPrice8 * 1e8 + statesInstant[INDEX_AFTER_WITHDRAW_4].userBalanceAsset, + 6e15, // < 0.6% + "user received almost all rewards 1" + ); + } + + function _testMaxDepositAndMaxWithdraw() internal view { + assertEq(IStrategy(currentStrategy).maxDepositAssets().length, 0, "any amount can be deposited"); + assertEq(IStrategy(currentStrategy).maxWithdrawAssets(0).length, 0, "any amount can be withdrawn"); + } + + //endregion --------------------------------------- Additional tests + + //region --------------------------------------- Test implementations + function _depositChangeLtvWithdraw( + uint minLtv0, + uint maxLtv0, + uint minLtv1, + uint maxLtv1 + ) internal returns (State memory stateInitial, State memory stateAfterDeposit, State memory stateAfterWithdraw) { + uint snapshot = vm.snapshotState(); + address vault = IStrategy(currentStrategy).vault(); + _setMinMaxLtv(minLtv0, maxLtv0); + + stateInitial = _getState(); + + _tryToDepositToVault(vault, DEFAULT_AMOUNT, 0, address(this)); + stateAfterDeposit = _getState(); + + vm.roll(block.number + 6); + + _setMinMaxLtv(minLtv1, maxLtv1); + + _tryToWithdrawFromVault(vault, IVault(vault).balanceOf(address(this))); + stateAfterWithdraw = _getState(); + + vm.revertToState(snapshot); + } + + function _depositChangeLtvDeposit( + uint minLtv0, + uint maxLtv0, + uint minLtv1, + uint maxLtv1 + ) internal returns (State memory stateInitial, State memory stateAfterDeposit, State memory stateAfterDeposit2) { + uint snapshot = vm.snapshotState(); + address vault = IStrategy(currentStrategy).vault(); + _setMinMaxLtv(minLtv0, maxLtv0); + + stateInitial = _getState(); + + _tryToDepositToVault(vault, DEFAULT_AMOUNT, 0, address(this)); + stateAfterDeposit = _getState(); + + vm.roll(block.number + 6); + + _setMinMaxLtv(minLtv1, maxLtv1); + + _tryToDepositToVault(vault, DEFAULT_AMOUNT, 0, address(this)); + stateAfterDeposit2 = _getState(); + + vm.revertToState(snapshot); + } + + /// @notice Deposit, check state, withdraw all, check state + /// @return states [initial state, state after deposit, state after waiting, state after hardwork, state after withdraw] + function _depositWithdraw( + uint amount, + address rewards, + uint rewardsAmount, + uint waitSec, + bool hardworkBeforeWithdraw + ) internal returns (State[] memory states) { + uint snapshot = vm.snapshotState(); + states = new State[](5); + + IStrategy strategy = IStrategy(currentStrategy); + + // --------------------------------------------- Deposit + states[0] = _getState(); + (uint depositedAssets,) = _tryToDepositToVault(strategy.vault(), amount, REVERT_NO, address(this)); + vm.roll(block.number + 6); + states[1] = _getState(); + + _skip(waitSec, 0); + states[2] = _getState(); + + // --------------------------------------------- Hardwork + if (rewardsAmount != 0) { + // emulate merkl rewards + deal(rewards, currentStrategy, rewardsAmount); + } + + if (hardworkBeforeWithdraw) { + vm.prank(platform.multisig()); + IVault(strategy.vault()).doHardWork(); + } + states[3] = _getState(); + + // --------------------------------------------- Withdraw + _tryToWithdrawFromVault(strategy.vault(), states[1].vaultBalance - states[0].vaultBalance); + vm.roll(block.number + 6); + states[4] = _getState(); + + vm.revertToState(snapshot); + + assertLt(states[0].total, states[1].total, "Total should increase after deposit"); + assertEq(depositedAssets, amount, "Deposited amount should be equal to amountsToDeposit"); + } + + //endregion --------------------------------------- Test implementations + + //region --------------------------------------- Internal logic + function _currentFarmId() internal view returns (uint) { + return IFarmingStrategy(currentStrategy).farmId(); + } + + function _tryToDepositToVault( + address vault, + uint amount, + uint revertKind, + address user + ) internal returns (uint deposited, uint depositedValue) { + address[] memory assets = IVault(vault).assets(); + uint[] memory amountsToDeposit = new uint[](1); + amountsToDeposit[0] = amount; + + // ----------------------------- Prepare amount on user's balance + _dealAndApprove(user, vault, assets, amountsToDeposit); + // console.log("Deposit to vault", assets[0], amounts_[0]); + + uint balanceBefore = IVault(vault).balanceOf(user); + // ----------------------------- Try to deposit assets to the vault + // todo + // if (revertKind == REVERT_NOT_ENOUGH_LIQUIDITY) { + // vm.expectRevert(ISilo.NotEnoughLiquidity.selector); + // } + if (revertKind == REVERT_INSUFFICIENT_BALANCE) { + vm.expectRevert(IControllable.InsufficientBalance.selector); + } + vm.prank(user); + IStabilityVault(vault).depositAssets(assets, amountsToDeposit, 0, user); + + return (amountsToDeposit[0], IVault(vault).balanceOf(user) - balanceBefore); + } + + function _tryToWithdrawFromVault(address vault, uint values) internal returns (uint withdrawn) { + address[] memory _assets = IVault(vault).assets(); + + uint balanceBefore = IERC20(_assets[0]).balanceOf(address(this)); + + vm.prank(address(this)); + IStabilityVault(vault).withdrawAssets(_assets, values, new uint[](1)); + + return IERC20(_assets[0]).balanceOf(address(this)) - balanceBefore; + } + + function _dealAndApprove(address user, address spender, address[] memory assets, uint[] memory amounts) internal { + for (uint j; j < assets.length; ++j) { + deal(assets[j], user, amounts[j]); + + vm.prank(user); + IERC20(assets[j]).approve(spender, amounts[j]); + } + } + + /// @param depositParam0 - Multiplier of flash amount for borrow on deposit. + /// @param depositParam1 - Multiplier of borrow amount to take into account max flash loan fee in maxDeposit + function _setDepositParams(uint depositParam0, uint depositParam1) internal { + ILeverageLendingStrategy strategy = ILeverageLendingStrategy(currentStrategy); + (uint[] memory params, address[] memory addresses) = strategy.getUniversalParams(); + + params[0] = depositParam0; + params[1] = depositParam1; + + vm.prank(platform.multisig()); + strategy.setUniversalParams(params, addresses); + } + + /// @param withdrawParam0 - Multiplier of flash amount for borrow on withdraw. + /// @param withdrawParam1 - Multiplier of amount allowed to be deposited after withdraw. Default is 100_00 == 100% (deposit forbidden) + /// @param withdrawParam2 - allows to disable withdraw through increasing ltv if leverage is near to target + function _setWithdrawParams(uint withdrawParam0, uint withdrawParam1, uint withdrawParam2) internal { + ILeverageLendingStrategy strategy = ILeverageLendingStrategy(currentStrategy); + (uint[] memory params, address[] memory addresses) = strategy.getUniversalParams(); + + params[2] = withdrawParam0; + params[3] = withdrawParam1; + params[11] = withdrawParam2; + + vm.prank(platform.multisig()); + strategy.setUniversalParams(params, addresses); + } + + function _getState() internal view returns (State memory state) { + ILeverageLendingStrategy strategy = ILeverageLendingStrategy(address(currentStrategy)); + + (state.sharePrice,) = strategy.realSharePrice(); + + ( + state.ltv, + state.maxLtv, + state.leverage, + state.collateralAmount, + state.debtAmount, + state.targetLeveragePercent + ) = strategy.health(); + + state.total = IStrategy(currentStrategy).total(); + state.maxLeverage = 100_00 * 1e4 / (1e4 - state.maxLtv); + state.targetLeverage = state.maxLeverage * state.targetLeveragePercent / 100_00; + state.strategyBalanceAsset = + IERC20(IStrategy(address(strategy)).assets()[0]).balanceOf(address(currentStrategy)); + state.userBalanceAsset = IERC20(IStrategy(address(strategy)).assets()[0]).balanceOf(address(address(this))); + (state.realTvl,) = strategy.realTvl(); + (state.realSharePrice,) = strategy.realSharePrice(); + state.vaultBalance = IVault(IStrategy(address(strategy)).vault()).balanceOf(address(this)); + (state.revenueAssets, state.revenueAmounts) = IStrategy(currentStrategy).getRevenue(); + + // _printState(state); + return state; + } + + function _printState(State memory state) internal pure { + console.log("state **************************************************"); + console.log("ltv", state.ltv); + console.log("maxLtv", state.maxLtv); + console.log("targetLeverage", state.targetLeverage); + console.log("leverage", state.leverage); + console.log("total", state.total); + console.log("collateralAmount", state.collateralAmount); + console.log("debtAmount", state.debtAmount); + console.log("targetLeveragePercent", state.targetLeveragePercent); + console.log("maxLeverage", state.maxLeverage); + console.log("realTvl", state.realTvl); + console.log("realSharePrice", state.realSharePrice); + console.log("vaultBalance", state.vaultBalance); + console.log("strategyBalanceAsset", state.strategyBalanceAsset); + console.log("userBalanceAsset", state.userBalanceAsset); + for (uint i = 0; i < state.revenueAssets.length; i++) { + console.log("revenueAsset", i, state.revenueAssets[i], state.revenueAmounts[i]); + } + } + + function _setMinMaxLtv(uint minLtv, uint maxLtv) internal { + IFarmingStrategy strategy = IFarmingStrategy(currentStrategy); + uint farmId = strategy.farmId(); + IFactory factory = IFactory(IPlatform(IControllable(currentStrategy).platform()).factory()); + + IFactory.Farm memory farm = factory.farm(farmId); + farm.nums[0] = minLtv; + farm.nums[1] = maxLtv; + + vm.prank(platform.multisig()); + factory.updateFarm(farmId, farm); + } + + //endregion --------------------------------------- Internal logic + + //region --------------------------------------- Helper functions + function _upgradePlatform(address multisig, address priceReader_) internal { + // we need to skip 1 day to update the swapper + // but we cannot simply skip 1 day, because the silo oracle will start to revert with InvalidPrice + // vm.warp(block.timestamp - 86400); + rewind(86400); + + IPlatform platform = IPlatform(IControllable(priceReader_).platform()); + + address[] memory proxies = new address[](1); + address[] memory implementations = new address[](1); + + proxies[0] = address(priceReader_); + //proxies[1] = platform.swapper(); + //proxies[2] = platform.ammAdapter(keccak256(bytes(AmmAdapterIdLib.META_VAULT))).proxy; + + implementations[0] = address(new PriceReader()); + //implementations[1] = address(new Swapper()); + //implementations[2] = address(new MetaVaultAdapter()); + + //vm.prank(multisig); + // platform.cancelUpgrade(); + + vm.startPrank(multisig); + platform.announcePlatformUpgrade("2025.07.22-alpha", proxies, implementations); + + skip(1 days); + platform.upgrade(); + vm.stopPrank(); + } + + function _setUpFlashLoanVault(address flashLoanVault, ILeverageLendingStrategy.FlashLoanKind kind_) internal { + _setFlashLoanVault(ILeverageLendingStrategy(currentStrategy), flashLoanVault, uint(kind_)); + } + + function _setFlashLoanVault(ILeverageLendingStrategy strategy, address flashLoanVault, uint kind) internal { + address multisig = platform.multisig(); + + (uint[] memory params, address[] memory addresses) = strategy.getUniversalParams(); + params[10] = kind; + addresses[0] = flashLoanVault; + + vm.prank(multisig); + strategy.setUniversalParams(params, addresses); + } + + function _getCollateralPrice8(address asset_) internal view returns (uint) { + return + IAavePriceOracle(IAaveAddressProvider(IPool(POOL).ADDRESSES_PROVIDER()).getPriceOracle()) + .getAssetPrice(asset_); + } + + //endregion --------------------------------------- Helper functions +} diff --git a/test/strategies/ALMF.Plasma.Upgrade.431.t.sol b/test/strategies/ALMF.Plasma.Upgrade.431.t.sol new file mode 100644 index 000000000..8c314182e --- /dev/null +++ b/test/strategies/ALMF.Plasma.Upgrade.431.t.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Test, Vm, console} from "forge-std/Test.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {AaveLeverageMerklFarmStrategy} from "../../src/strategies/AaveLeverageMerklFarmStrategy.sol"; +import {IStrategy} from "../../src/interfaces/IStrategy.sol"; +import {IVault} from "../../src/interfaces/IVault.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {IStabilityVault} from "../../src/interfaces/IStabilityVault.sol"; +import {IFactory} from "../../src/interfaces/IFactory.sol"; +import {IFarmingStrategy} from "../../src/interfaces/IFarmingStrategy.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; +import {StrategyIdLib} from "../../src/strategies/libs/StrategyIdLib.sol"; +import {AmmAdapterIdLib} from "../../src/adapters/libs/AmmAdapterIdLib.sol"; +import {ALMFLib} from "../../src/strategies/libs/ALMFLib.sol"; + +/// @notice Add revenueBaseAssetIndex param to farm, reset share price +contract ALMFUpgrade431PlasmaTest is Test { + uint internal constant FORK_BLOCK = 8423845; // Dec-10-2025 08:15:16 UTC + + /// @notice Stability weETH Aave Leverage Merkl Farm USDT0 + address internal constant VAULT = 0xab0087D6fbC877246A4Ba33636f80E5dCbd5BE01; + + address internal multisig; + + constructor() { + vm.selectFork(vm.createFork(vm.envString("PLASMA_RPC_URL"), FORK_BLOCK)); + + _upgradeStrategy(address(IVault(VAULT).strategy())); + } + + function testUpgradeStrategy() public { + IStrategy strategy = IVault(VAULT).strategy(); + + // --------------------------------------------- Hardwork before reset + deal(PlasmaConstantsLib.TOKEN_WXPL, address(strategy), 1e18); // emulate merkl rewards + + uint earned1 = _doHardWork(strategy); + assertNotEq(earned1, 0, "earned 1"); + + // --------------------------------------------- Change farm params and reset share price + { + IFactory factory = IFactory(IPlatform(PlasmaConstantsLib.PLATFORM).factory()); + uint farmId = IFarmingStrategy(address(strategy)).farmId(); + IFactory.Farm memory farm = factory.farm(farmId); + assertEq(farm.nums.length, 4, "farm is not updated"); + uint[] memory nums = new uint[](5); + for (uint i; i < 4; ++i) { + nums[i] = farm.nums[i]; + } + nums[4] = ALMFLib.REVENUE_BASE_ASSET_1; // 1 - revenue-base-asset is borrow asset + farm.nums = nums; + + vm.prank(PlasmaConstantsLib.MULTISIG); + factory.updateFarm(farmId, farm); + } + + // --------------------------------------------- Reset internal share price + vm.prank(IPlatform(PlasmaConstantsLib.PLATFORM).multisig()); + AaveLeverageMerklFarmStrategy(address(strategy)).resetSharePrice(); + skip(5 days); + + // --------------------------------------------- Hardwork after reset + deal(PlasmaConstantsLib.TOKEN_WXPL, address(strategy), 1e18); // emulate merkl rewards + + uint earned2 = _doHardWork(strategy); + assertNotEq(earned2, 0, "earned 2"); + skip(5 days); + + // --------------------------------------------- Next hardworks + uint[] memory earned = new uint[](5); + for (uint i; i < 5; ++i) { + deal(PlasmaConstantsLib.TOKEN_WXPL, address(strategy), 1e18); // emulate merkl rewards + + earned[i] = _doHardWork(strategy); + assertNotEq(earned[i], 0, "earned 3"); + skip(5 days); + } + + // todo probably we need to compare here earned amounts with and without switching revenueBaseAssetIndex and resetting price + // console.log(earned1, earned2, earned[0], earned[1]); + // console.log(earned[2], earned[3], earned[4]); + } + + function _doHardWork(IStrategy strategy) internal returns (uint earnedUSD18) { + address vault = strategy.vault(); + + vm.recordLogs(); + vm.prank(PlasmaConstantsLib.MULTISIG); + IVault(vault).doHardWork(); + + // extract data from event IStrategy.HardWork + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes32 eventSignature = keccak256("HardWork(uint256,uint256,uint256,uint256,uint256,uint256,uint256[])"); + for (uint i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == eventSignature) { + (,, earnedUSD18,,,,) = abi.decode(logs[i].data, (uint, uint, uint, uint, uint, uint, uint[])); + break; + } + } + + return earnedUSD18; + } + + function _tryToDepositToVault( + address vault, + uint amount, + address user + ) internal returns (uint deposited, uint depositedValue) { + address[] memory assets = IVault(vault).assets(); + uint[] memory amountsToDeposit = new uint[](1); + amountsToDeposit[0] = amount; + + // ----------------------------- Prepare amount on user's balance + _dealAndApprove(user, vault, assets, amountsToDeposit); + // console.log("Deposit to vault", assets[0], amounts_[0]); + + uint balanceBefore = IVault(vault).balanceOf(user); + + // ----------------------------- Try to deposit assets to the vault + vm.prank(user); + IStabilityVault(vault).depositAssets(assets, amountsToDeposit, 0, user); + + return (amountsToDeposit[0], IVault(vault).balanceOf(user) - balanceBefore); + } + + function _dealAndApprove(address user, address spender, address[] memory assets, uint[] memory amounts) internal { + for (uint j; j < assets.length; ++j) { + deal(assets[j], user, amounts[j]); + + vm.prank(user); + IERC20(assets[j]).approve(spender, amounts[j]); + } + } + + function _upgradeStrategy(address strategyAddress) internal { + address strategyImplementation = address(new AaveLeverageMerklFarmStrategy()); + + IFactory factory = IFactory(IPlatform(PlasmaConstantsLib.PLATFORM).factory()); + + vm.prank(PlasmaConstantsLib.MULTISIG); + factory.setStrategyImplementation(StrategyIdLib.AAVE_LEVERAGE_MERKL_FARM, strategyImplementation); + + factory.upgradeStrategyProxy(strategyAddress); + } +} diff --git a/test/strategies/ALMF.Plasma.t.sol b/test/strategies/ALMF.Plasma.t.sol new file mode 100644 index 000000000..40ed06d3c --- /dev/null +++ b/test/strategies/ALMF.Plasma.t.sol @@ -0,0 +1,1107 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Vm} from "forge-std/Test.sol"; +import {IControllable} from "../../src/interfaces/IControllable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IFarmingStrategy} from "../../src/interfaces/IFarmingStrategy.sol"; +import {ILeverageLendingStrategy} from "../../src/interfaces/ILeverageLendingStrategy.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {IFactory} from "../../src/interfaces/IFactory.sol"; +import {ISwapper} from "../../src/interfaces/ISwapper.sol"; +import {IStabilityVault} from "../../src/interfaces/IStabilityVault.sol"; +import {IStrategy} from "../../src/interfaces/IStrategy.sol"; +import {IVault} from "../../src/interfaces/IVault.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; +import {PlasmaSetup} from "../base/chains/PlasmaSetup.sol"; +import {StrategyIdLib} from "../../src/strategies/libs/StrategyIdLib.sol"; +import {UniversalTest} from "../base/UniversalTest.sol"; +import {PriceReader} from "../../src/core/PriceReader.sol"; +import {IAaveAddressProvider} from "../../src/integrations/aave/IAaveAddressProvider.sol"; +import {IAavePriceOracle} from "../../src/integrations/aave/IAavePriceOracle.sol"; +import {IPool} from "../../src/integrations/aave/IPool.sol"; +import {FixedPointMathLib} from "../../lib/solady/src/utils/FixedPointMathLib.sol"; +import {AmmAdapterIdLib} from "../../src/adapters/libs/AmmAdapterIdLib.sol"; +// import {IAaveDataProvider} from "../../src/integrations/aave/IAaveDataProvider.sol"; +import {AaveLeverageMerklFarmStrategy} from "../../src/strategies/AaveLeverageMerklFarmStrategy.sol"; +import {ALMFLib} from "../../src/strategies/libs/ALMFLib.sol"; +import {console} from "forge-std/console.sol"; +import {SharedFarmMakerLib} from "../../chains/shared/SharedFarmMarketLib.sol"; +import {IAavePool32} from "../../src/integrations/aave32/IPool32.sol"; + +contract ALMFStrategyPlasmaTest is PlasmaSetup, UniversalTest { + uint public constant REVERT_NO = 0; + uint public constant REVERT_NOT_ENOUGH_LIQUIDITY = 1; + uint public constant REVERT_INSUFFICIENT_BALANCE = 2; + + uint internal constant INDEX_INIT_0 = 0; + uint internal constant INDEX_AFTER_DEPOSIT_1 = 1; + uint internal constant INDEX_AFTER_WAIT_2 = 2; + uint internal constant INDEX_AFTER_HARDWORK_3 = 3; + uint internal constant INDEX_AFTER_WITHDRAW_4 = 4; + + uint internal constant DEFAULT_TARGET_LEVERAGE_2 = 2_0000; // 2x + uint internal constant DEFAULT_LTV1_MINUS_LTV0_2 = 500; // 5.00% + + uint internal constant DEFAULT_TARGET_LEVERAGE_3 = 3_0000; // 3x + uint internal constant DEFAULT_LTV1_MINUS_LTV0_3 = 300; // 3.00% + + uint internal constant DEFAULT_TARGET_LEVERAGE_9 = 9_0000; // 9x + uint internal constant DEFAULT_LTV1_MINUS_LTV0_9 = 15; // 0.15% + + uint internal constant DEFAULT_TARGET_LEVERAGE_10 = 10_0000; // 10x + uint internal constant DEFAULT_LTV1_MINUS_LTV0_10 = 10; // 0.10% + + struct State { + uint ltv; + uint maxLtv; + uint leverage; + uint maxLeverage; + uint targetLeverage; + uint targetLeveragePercent; + uint collateralAmount; + uint debtAmount; + uint total; + uint sharePrice; + uint strategyBalanceAsset; + uint userBalanceAsset; + uint realTvl; + uint realSharePrice; + uint vaultBalance; + address[] revenueAssets; + uint[] revenueAmounts; + } + + uint8 internal constant E_MODE_CATEGORY_ID_NOT_USED = 0; + uint8 internal constant E_MODE_CATEGORY_ID_USDE_STABLECOINS = 1; + uint8 internal constant E_MODE_CATEGORY_ID_SUSDE_STABLECOINS = 2; + uint8 internal constant E_MODE_CATEGORY_ID_WEETH_WETH = 3; + uint8 internal constant E_MODE_CATEGORY_ID_WEETH_STABLECOINS = 4; + + // uint internal constant FORK_BLOCK = 6452516; // Nov-17-2025 12:36:59 UTC + // uint internal constant FORK_BLOCK = 7041595; // Nov-24-2025 08:16:08 UTC + uint internal constant FORK_BLOCK = 8423845; // Dec-10-2025 08:15:16 UTC + + /// @notice Farm Id of the farm WETH-USDT0, leverage 3 + uint internal farmIdWethUsdt3; + uint internal farmWeethWeth10; + uint internal farmSusdeUsdt9; + uint internal farmWeethUsdt2; + + constructor() { + vm.selectFork(vm.createFork(vm.envString("PLASMA_RPC_URL"), FORK_BLOCK)); + + allowZeroApr = true; + duration1 = 0.1 hours; + duration2 = 0.1 hours; + duration3 = 0.1 hours; + + // ALMF uses real share price as share price + // so it cannot initialize share price during deposit. + // It sets initial value of share price in first claimRevenue. + // As result, following check is failed in universal test: + // "Universal test: estimated totalRevenueUSD is zero" + // So, we should disable it by setting allowZeroTotalRevenueUSD. + // And make all checks in additional tests instead. + allowZeroTotalRevenueUSD = true; + + // _upgradePlatform(platform.multisig(), IPlatform(platform).priceReader()); + } + + //region --------------------------------------- Universal test + function testALMFPlasma() public universalTest { + _addRoutes(); + + farmIdWethUsdt3 = _addFarmWethUsdt3NoEMode(); + farmWeethWeth10 = _addFarmWeethWeth10(); + farmSusdeUsdt9 = _addFarmSusdeUsdt9(); + farmWeethUsdt2 = _addFarmWeethUsdt2NoRewards(); + + _addStrategy(farmWeethWeth10); + _addStrategy(farmIdWethUsdt3); + _addStrategy(farmSusdeUsdt9); + _addStrategy(farmWeethUsdt2); + } + + function _addStrategy(uint farmId) internal { + strategies.push( + Strategy({ + id: StrategyIdLib.AAVE_LEVERAGE_MERKL_FARM, + pool: address(0), + farmId: farmId, + strategyInitAddresses: new address[](0), + strategyInitNums: new uint[](0) + }) + ); + } + + function _preDeposit() internal override { + uint farmId = IFarmingStrategy(currentStrategy).farmId(); + if (farmId == farmIdWethUsdt3) { + console.log("FARM WETH-USDT0 leverage 3"); + _preDepositForFarmWethUsdt3(); + } else if (farmId == farmWeethWeth10) { + console.log("FARM WEETH-WETH leverage 10"); + _preDepositForFarmWeethWeth10(); + } else if (farmId == farmSusdeUsdt9) { + console.log("FARM SUSDE-USDT leverage 9"); + _preDepositForFarmSusdeUsdt9(); + } else if (farmId == farmWeethUsdt2) { + console.log("FARM WEETH-USDT leverage 2 no rewards"); + _preDepositForFarmWeethUsdt2NoRewards(); + } + } + + function _preHardWork() internal override { + for (uint i; i < 2; ++i) { + // emulate merkl rewards + uint farmId = IFarmingStrategy(currentStrategy).farmId(); + if (farmId == farmIdWethUsdt3) { + _preHardWorkForFarmWethUsdt3(); + } else if (farmId == farmWeethWeth10) { + _preHardWorkForFarmWeethWeth10(); + } else if (farmId == farmSusdeUsdt9) { + _preHardWorkForFarmSusdeUsdt9(); + } else if (farmId == farmWeethUsdt2) { + _preHardWorkForFarmWeethUsdt2NoRewards(); + } + + if (i == 0) { + // Make first hardwork to initialize share price, APR is 0 + // Next hardwork in universal test will be able to show not zero APR + vm.prank(platform.multisig()); + IVault(IStrategy(currentStrategy).vault()).doHardWork(); + _skip(duration1 + duration2, 0); + } + } + } + + //endregion --------------------------------------- Universal test + + //region --------------------------------------- _preDeposit overrides for farms + function _preDepositForFarmWethUsdt3() internal { + // thresholds + vm.prank(platform.multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(PlasmaConstantsLib.TOKEN_WETH, 1e12); + vm.prank(platform.multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(PlasmaConstantsLib.TOKEN_USDT0, 1e6); + + uint snapshot = vm.snapshotState(); + // initial supply + _tryToDepositToVault(IStrategy(currentStrategy).vault(), 0.1e18, REVERT_NO, makeAddr("initial supplier")); + + // check revenue (replacement for "Universal test: estimated totalRevenueUSD is zero") + _testDepositTwoHardworks(); + + // set TL, deposit, change TL, withdraw/deposit => leverage was changed toward new TL + _testDepositChangeLtvWithdraw(); + _testDepositChangeLtvDeposit(); + + // check deposit-wait 30 days-hardwork-withdraw results + _testDepositWaitHardworkWithdraw(); + + // explicitly check possible swaps + assertGt(_swap(PlasmaConstantsLib.TOKEN_SUSDE, PlasmaConstantsLib.TOKEN_USDE, 1e18), 0, "susde=>usde"); + assertGt(_swap(PlasmaConstantsLib.TOKEN_USDE, PlasmaConstantsLib.TOKEN_USDT0, 1e18), 0, "usde=>usdt0"); + assertGt(_swap(PlasmaConstantsLib.TOKEN_WETH, PlasmaConstantsLib.TOKEN_WEETH, 0.1e18), 0, "weth=>weeth"); + assertGt(_swap(PlasmaConstantsLib.TOKEN_WEETH, PlasmaConstantsLib.TOKEN_WETH, 0.1e18), 0, "weeth=>weth"); + assertGt(_swap(PlasmaConstantsLib.TOKEN_WEETH, PlasmaConstantsLib.TOKEN_USDT0, 0.1e18), 0, "weeth=>usdt0"); + + vm.revertToState(snapshot); + } + + function _preDepositForFarmSusdeUsdt9() internal { + // ---------------- Setup thresholds required by universal test + vm.prank(platform.multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(PlasmaConstantsLib.TOKEN_SUSDE, 1e12); + vm.prank(platform.multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(PlasmaConstantsLib.TOKEN_USDT0, 1e6); + + // ---------------- Additional tests + uint snapshot = vm.snapshotState(); + + _tryToDepositToVault(IStrategy(currentStrategy).vault(), 100e18, REVERT_NO, address(this)); + + IAavePool32.EModeCategoryLegacy memory eModeData = + IAavePool32(PlasmaConstantsLib.AAVE_V3_POOL).getEModeCategoryData(E_MODE_CATEGORY_ID_SUSDE_STABLECOINS); + + (, uint maxLtv,,,,) = AaveLeverageMerklFarmStrategy(currentStrategy).health(); + assertEq(maxLtv, eModeData.ltv, "max ltv for e-mode matches"); + + // see https://app.aave.com/reserve-overview/?underlyingAsset=0x211cc4dd073734da055fbf44a2b4667d5e5fe5d2&marketName=proto_plasma_v3 + assertEq(maxLtv, 90_00, "max ltv is 90%"); + + vm.revertToState(snapshot); + } + + function _preDepositForFarmWeethWeth10() internal { + // ---------------- thresholds + vm.prank(platform.multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(PlasmaConstantsLib.TOKEN_WEETH, 1e15); + vm.prank(platform.multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(PlasmaConstantsLib.TOKEN_WETH, 1e15); + + // ---------------- Additional test + { + uint snapshotId = vm.snapshotState(); + + _tryToDepositToVault(IStrategy(currentStrategy).vault(), 1e18, REVERT_NO, address(this)); + + IAavePool32.EModeCategoryLegacy memory eModeData = + IAavePool32(PlasmaConstantsLib.AAVE_V3_POOL).getEModeCategoryData(E_MODE_CATEGORY_ID_WEETH_WETH); + + (, uint maxLtv,,,,) = AaveLeverageMerklFarmStrategy(currentStrategy).health(); + assertEq(maxLtv, eModeData.ltv, "max ltv for e-mode matches"); + + // see https://app.aave.com/reserve-overview/?underlyingAsset=0x211cc4dd073734da055fbf44a2b4667d5e5fe5d2&marketName=proto_plasma_v3 + assertEq(maxLtv, 93_00, "max ltv is 93%"); + + vm.revertToState(snapshotId); + } + + // ---------------- More additional tests + _testCollateralAssetThresholdTooLow(); + _testRevenueBaseAsset(); + } + + function _preDepositForFarmWeethUsdt2NoRewards() internal { + // ---------------- thresholds + vm.prank(platform.multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(PlasmaConstantsLib.TOKEN_WEETH, 1e12); + vm.prank(platform.multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(PlasmaConstantsLib.TOKEN_USDT0, 1e6); + + // ---------------- Additional tests + uint snapshot = vm.snapshotState(); + + _tryToDepositToVault(IStrategy(currentStrategy).vault(), 1e18, REVERT_NO, address(this)); + + IAavePool32.EModeCategoryLegacy memory eModeData = + IAavePool32(PlasmaConstantsLib.AAVE_V3_POOL).getEModeCategoryData(E_MODE_CATEGORY_ID_WEETH_STABLECOINS); + + (, uint maxLtv,,,,) = AaveLeverageMerklFarmStrategy(currentStrategy).health(); + assertEq(maxLtv, eModeData.ltv, "max ltv for e-mode matches"); + + // see https://app.aave.com/reserve-overview/?underlyingAsset=0x211cc4dd073734da055fbf44a2b4667d5e5fe5d2&marketName=proto_plasma_v3 + assertEq(maxLtv, 75_00, "max ltv is 75%"); + + vm.revertToState(snapshot); + } + + //endregion --------------------------------------- _preDeposit overrides for farms + + //region --------------------------------------- _preHardWork overrides for farms + function _preHardWorkForFarmWethUsdt3() internal { + deal(PlasmaConstantsLib.TOKEN_USDT0, currentStrategy, 1e6); + deal(PlasmaConstantsLib.TOKEN_WXPL, currentStrategy, 0.1e18); + } + + function _preHardWorkForFarmSusdeUsdt9() internal { + // !TODO: deal(PlasmaConstantsLib.TOKEN_WAPLAUSDE, currentStrategy, 100e18); + } + + function _preHardWorkForFarmWeethWeth10() internal { + deal(PlasmaConstantsLib.TOKEN_WXPL, currentStrategy, 10e18); + } + + function _preHardWorkForFarmWeethUsdt2NoRewards() internal { + deal(PlasmaConstantsLib.TOKEN_WXPL, currentStrategy, 10e18); + } + + //endregion --------------------------------------- _preHardWork overrides for farms + + //region --------------------------------------- Farms + /// @notice WETH-USDT0, leverage 3 + function _addFarmWethUsdt3NoEMode() internal returns (uint farmId) { + address[] memory rewards = new address[](2); + rewards[0] = PlasmaConstantsLib.TOKEN_USDT0; + rewards[1] = PlasmaConstantsLib.TOKEN_WXPL; + + (uint minLtv, uint maxLtv) = getMinMaxLtv(DEFAULT_TARGET_LEVERAGE_3, DEFAULT_LTV1_MINUS_LTV0_3); + + IFactory.Farm[] memory farms = new IFactory.Farm[](1); + farms[0] = SharedFarmMakerLib._makeAaveLeverageMerklFarm( + PlasmaConstantsLib.AAVE_V3_POOL_WETH, + PlasmaConstantsLib.AAVE_V3_POOL_USDT0, + PlasmaConstantsLib.POOL_WXPL_USDT0, + rewards, + minLtv, + maxLtv, + uint(ILeverageLendingStrategy.FlashLoanKind.UniswapV3_2), + E_MODE_CATEGORY_ID_NOT_USED, + 0 // share price is calculated in collateral asset per USD + ); + + vm.startPrank(platform.multisig()); + factory.addFarms(farms); + + return factory.farmsLength() - 1; + } + + function _addFarmWeethWeth10() internal returns (uint farmId) { + address[] memory rewards = new address[](1); + rewards[0] = PlasmaConstantsLib.TOKEN_WXPL; + + (uint minLtv, uint maxLtv) = getMinMaxLtv(DEFAULT_TARGET_LEVERAGE_10, DEFAULT_LTV1_MINUS_LTV0_10); + + IFactory.Farm[] memory farms = new IFactory.Farm[](1); + farms[0] = SharedFarmMakerLib._makeAaveLeverageMerklFarm( + PlasmaConstantsLib.AAVE_V3_POOL_WEETH, + PlasmaConstantsLib.AAVE_V3_POOL_WETH, + PlasmaConstantsLib.POOL_OKU_TRADE_USDT0_WETH, + rewards, + minLtv, + maxLtv, + uint(ILeverageLendingStrategy.FlashLoanKind.UniswapV3_2), + E_MODE_CATEGORY_ID_WEETH_WETH, + 0 // share price is calculated in collateral asset per USD + ); + + vm.startPrank(platform.multisig()); + factory.addFarms(farms); + + return factory.farmsLength() - 1; + } + + function _addFarmSusdeUsdt9() internal returns (uint farmId) { + // address[] memory rewards = new address[](1); + // rewards[0] = PlasmaConstantsLib.TOKEN_WAPLAUSDE; + address[] memory rewards; + + (uint minLtv, uint maxLtv) = getMinMaxLtv(DEFAULT_TARGET_LEVERAGE_9, DEFAULT_LTV1_MINUS_LTV0_9); + + IFactory.Farm[] memory farms = new IFactory.Farm[](1); + farms[0] = SharedFarmMakerLib._makeAaveLeverageMerklFarm( + PlasmaConstantsLib.AAVE_V3_POOL_SUSDE, + PlasmaConstantsLib.AAVE_V3_POOL_USDT0, + PlasmaConstantsLib.POOL_WXPL_USDT0, + rewards, + minLtv, + maxLtv, + uint(ILeverageLendingStrategy.FlashLoanKind.UniswapV3_2), + E_MODE_CATEGORY_ID_SUSDE_STABLECOINS, + 0 // share price is calculated in collateral asset per USD + ); + + vm.startPrank(platform.multisig()); + factory.addFarms(farms); + + return factory.farmsLength() - 1; + } + + function _addFarmSusdeUsde9() internal returns (uint farmId) { + address[] memory rewards = new address[](1); + rewards[0] = PlasmaConstantsLib.TOKEN_WAPLAUSDE; + + (uint minLtv, uint maxLtv) = getMinMaxLtv(DEFAULT_TARGET_LEVERAGE_9, DEFAULT_LTV1_MINUS_LTV0_9); + + IFactory.Farm[] memory farms = new IFactory.Farm[](1); + farms[0] = SharedFarmMakerLib._makeAaveLeverageMerklFarm( + PlasmaConstantsLib.AAVE_V3_POOL_SUSDE, + PlasmaConstantsLib.AAVE_V3_POOL_USDE, + PlasmaConstantsLib.POOL_WXPL_USDT0, // todo we need to get flash in USDe + rewards, + minLtv, + maxLtv, + uint(ILeverageLendingStrategy.FlashLoanKind.UniswapV3_2), + E_MODE_CATEGORY_ID_SUSDE_STABLECOINS, + 0 // share price is calculated in collateral asset per USD + ); + + vm.startPrank(platform.multisig()); + factory.addFarms(farms); + + return factory.farmsLength() - 1; + } + + /// @notice Test real farm but without rewards + function _addFarmWeethUsdt2NoRewards() internal returns (uint farmId) { + address[] memory rewards; // no rewards - for test purposes + + (uint minLtv, uint maxLtv) = getMinMaxLtv(DEFAULT_TARGET_LEVERAGE_2, DEFAULT_LTV1_MINUS_LTV0_2); + + IFactory.Farm[] memory farms = new IFactory.Farm[](1); + farms[0] = SharedFarmMakerLib._makeAaveLeverageMerklFarm( + PlasmaConstantsLib.AAVE_V3_POOL_WEETH, + PlasmaConstantsLib.AAVE_V3_POOL_USDT0, + PlasmaConstantsLib.POOL_WXPL_USDT0, + rewards, + minLtv, + maxLtv, + uint(ILeverageLendingStrategy.FlashLoanKind.UniswapV3_2), + E_MODE_CATEGORY_ID_WEETH_STABLECOINS, + 0 // share price is calculated in collateral asset per USD + ); + + vm.startPrank(platform.multisig()); + factory.addFarms(farms); + + return factory.farmsLength() - 1; + } + + //endregion --------------------------------------- Farms + + //region --------------------------------------- Unit tests + function testGetMinMaxLtv() public pure { + (uint minLtv, uint maxLtv) = getMinMaxLtv(10_0000, 10); + assertEq(minLtv, 8995, "min ltv 10"); + assertEq(maxLtv, 9005, "max ltv 10"); + + (minLtv, maxLtv) = getMinMaxLtv(3_0000, 500); + assertEq(minLtv, 6399, "min ltv 3"); + assertEq(maxLtv, 6899, "max ltv 3"); + } + + //endregion --------------------------------------- Unit tests + + //region --------------------------------------- Additional tests + function _testCollateralAssetThresholdTooLow() internal { + uint snapshotId = vm.snapshotState(); + vm.prank(platform.multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(PlasmaConstantsLib.TOKEN_WEETH, 1e10); + + IStrategy strategy = IStrategy(currentStrategy); + + // --------------------------------------------- Deposit + _tryToDepositToVault(strategy.vault(), 1e18, REVERT_NO, address(this)); + vm.roll(block.number + 6); + + _skip(1 days, 0); + + // --------------------------------------------- First hardwork to initialize share price + vm.prank(platform.multisig()); + IVault(strategy.vault()).doHardWork(); + _skip(5 days, 0); + + // --------------------------------------------- Second hardwork to utilize rewards + // emulate merkl rewards - tiny amount + deal(PlasmaConstantsLib.TOKEN_WXPL, currentStrategy, 1e18); + + address multisig = platform.multisig(); + address vault = strategy.vault(); + + vm.expectRevert(ALMFLib.CollateralAssetThresholdTooLow.selector); + vm.prank(multisig); + IVault(vault).doHardWork(); + + vm.revertToState(snapshotId); + } + + function _testRevenueBaseAsset() internal { + uint snapshotId = vm.snapshotState(); + uint earnedUsd18Before = _testRevenueAmount(PlasmaConstantsLib.TOKEN_WXPL, 1e18); + + // change farm params + { + IFactory factory = IFactory(IPlatform(platform).factory()); + uint farmId = IFarmingStrategy(currentStrategy).farmId(); + IFactory.Farm memory farm = factory.farm(farmId); + farm.nums[4] = ALMFLib.REVENUE_BASE_ASSET_1; // 1 - revenue-base-asset is borrow asset + factory.updateFarm(farmId, farm); + + vm.prank(IPlatform(platform).multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).resetSharePrice(); + } + + uint earnedUsd18After = _testRevenueAmount(PlasmaConstantsLib.TOKEN_WXPL, 1e18); + + assertEq(earnedUsd18Before, earnedUsd18After, "revenue USD same for both revenue base assets"); + + vm.revertToState(snapshotId); + } + + function _testDepositTwoHardworks() internal { + uint snapshot = vm.snapshotState(); + uint amount = 1e18; + + uint priceWeth8 = _getWethPrice8(); + + IStrategy strategy = IStrategy(currentStrategy); + + // --------------------------------------------- Deposit + State memory stateAfterDeposit = _getState(); + _tryToDepositToVault(strategy.vault(), amount, REVERT_NO, address(this)); + vm.roll(block.number + 6); + + // --------------------------------------------- Hardwork 1 + _skip(1 days, 0); + deal(PlasmaConstantsLib.TOKEN_USDT0, currentStrategy, 100e6); + + vm.prank(platform.multisig()); + IVault(strategy.vault()).doHardWork(); + + State memory stateAfterHW1 = _getState(); + + // --------------------------------------------- Hardwork 2 + _skip(1 days, 0); + deal(PlasmaConstantsLib.TOKEN_USDT0, currentStrategy, 300e6); + + vm.prank(platform.multisig()); + IVault(strategy.vault()).doHardWork(); + + State memory stateAfterHW2 = _getState(); + + assertEq( + stateAfterDeposit.revenueAmounts[0], + 0, + "Revenue before first claimReview is 0 because share price is not initialized yet" + ); + assertApproxEqRel( + stateAfterHW1.revenueAmounts[0] * priceWeth8 * 1e6 / 1e8 / 1e18, + 100e6 * 70 / 100, + 20e16, + "Revenue after first hardwork is ~$100" + ); + assertApproxEqRel( + stateAfterHW2.revenueAmounts[0] * priceWeth8 * 1e6 / 1e8 / 1e18, + 300e6 * 70 / 100, + 20e16, + "Revenue after first hardwork is ~$300" + ); + vm.revertToState(snapshot); + } + + function _testDepositChangeLtvWithdraw() internal { + { + (, State memory stateAfterDeposit, State memory stateAfterWithdraw) = + _depositChangeLtvWithdraw(49_00, 50_97, 52_00, 51_97); + + assertApproxEqRel( + stateAfterDeposit.leverage, + stateAfterDeposit.targetLeverage, + 1e16, + "Leverage after deposit should be equal to target 111" + ); + assertLt( + stateAfterDeposit.leverage, + stateAfterWithdraw.targetLeverage, + "leverage before withdraw less than target" + ); + assertGt(stateAfterWithdraw.leverage, stateAfterDeposit.leverage, "withdraw increased the leverage"); + } + { + (, State memory stateAfterDeposit, State memory stateAfterWithdraw) = + _depositChangeLtvWithdraw(49_00, 50_97, 47_00, 48_97); + + assertApproxEqRel( + stateAfterDeposit.leverage, + stateAfterDeposit.targetLeverage, + 1e16, + "Leverage after deposit should be equal to target 222" + ); + assertGt( + stateAfterDeposit.leverage, + stateAfterWithdraw.targetLeverage, + "leverage before withdraw greater than target" + ); + assertLt(stateAfterWithdraw.leverage, stateAfterDeposit.leverage, "withdraw decreased the leverage"); + } + } + + function _testDepositChangeLtvDeposit() internal { + { + (, State memory stateAfterDeposit, State memory stateAfterDeposit2) = + _depositChangeLtvDeposit(49_00, 50_97, 52_00, 51_97); + + assertApproxEqRel( + stateAfterDeposit.leverage, + stateAfterDeposit.targetLeverage, + 1e16, + "Leverage after deposit should be equal to target 333" + ); + assertLt( + stateAfterDeposit.leverage, + stateAfterDeposit2.targetLeverage, + "leverage before withdraw less than target" + ); + assertGt(stateAfterDeposit2.leverage, stateAfterDeposit.leverage, "deposit2 increased the leverage"); + } + { + (, State memory stateAfterDeposit, State memory stateAfterDeposit2) = + _depositChangeLtvDeposit(49_00, 50_97, 47_00, 48_97); + + assertApproxEqRel( + stateAfterDeposit.leverage, + stateAfterDeposit.targetLeverage, + 1e16, + "Leverage after deposit should be equal to target 444" + ); + assertGt( + stateAfterDeposit.leverage, + stateAfterDeposit2.targetLeverage, + "leverage before deposit2 greater than target" + ); + assertLt(stateAfterDeposit2.leverage, stateAfterDeposit.leverage, "deposit2 decreased the leverage"); + } + } + + function _testDepositWithdrawUsingFlashLoan( + address flashLoanVault, + ILeverageLendingStrategy.FlashLoanKind kind_ + ) internal { + uint snapshot = vm.snapshotState(); + _setUpFlashLoanVault(flashLoanVault, kind_); + + uint amount = 1e18; + State[] memory states = _depositWithdraw(amount, PlasmaConstantsLib.TOKEN_USDT0, 0, 0, false); + vm.revertToState(snapshot); + + assertApproxEqRel( + states[INDEX_AFTER_WITHDRAW_4].total, + states[INDEX_INIT_0].total, + states[INDEX_INIT_0].total / 100_000, + "Total should return back to prev value" + ); + assertApproxEqRel(states[4].userBalanceAsset, amount, amount / 50, "User shouldn't loss more than 2%"); + } + + function _testDepositWaitHardworkWithdraw() internal { + uint amount = 1e18; + + // --------------------------------------------- Deposit+withdraw without hardwork + uint snapshot = vm.snapshotState(); + State[] memory statesInstant = _depositWithdraw(amount, PlasmaConstantsLib.TOKEN_USDT0, 0, 0, true); + vm.revertToState(snapshot); + + // --------------------------------------------- Deposit, wait, [no rewards], hardwork, withdraw + snapshot = vm.snapshotState(); + State[] memory statesHW1 = _depositWithdraw(amount, PlasmaConstantsLib.TOKEN_USDT0, 0, 1 days, true); + vm.revertToState(snapshot); + + // --------------------------------------------- Deposit, wait, rewards, hardwork, withdraw + snapshot = vm.snapshotState(); + State[] memory statesHW2 = _depositWithdraw(amount, PlasmaConstantsLib.TOKEN_USDT0, 100e6, 1 days, true); + vm.revertToState(snapshot); + + // --------------------------------------------- Get WETH price + uint wethPrice = _getWethPrice8(); + + // --------------------------------------------- Compare results + assertApproxEqAbs( + statesHW2[INDEX_AFTER_HARDWORK_3].total - statesInstant[INDEX_AFTER_HARDWORK_3].total, + 100e18 * 70 / 100, + 10e18, + "_testDepositWaitHardworkWithdraw.total is increased on rewards amount - fees" + ); + assertLt( + statesHW1[INDEX_AFTER_HARDWORK_3].total, + statesInstant[INDEX_AFTER_HARDWORK_3].total, + "_testDepositWaitHardworkWithdraw.total is decreased because the borrow rate exceeds supply rate" + ); + + assertLt( + statesHW1[INDEX_AFTER_WITHDRAW_4].userBalanceAsset, + statesInstant[INDEX_AFTER_WITHDRAW_4].userBalanceAsset, + "_testDepositWaitHardworkWithdraw.user lost some amount because of borrow rate" + ); + assertApproxEqRel( + statesHW2[INDEX_AFTER_WITHDRAW_4].userBalanceAsset, + 100e18 * 70 / 100 / wethPrice * 1e8 + statesInstant[INDEX_AFTER_WITHDRAW_4].userBalanceAsset, + 3e15, // < 0.3% + "_testDepositWaitHardworkWithdraw.user received almost all rewards" + ); + } + + function _testMaxDepositAndMaxWithdraw() internal view { + assertEq(IStrategy(currentStrategy).maxDepositAssets().length, 0, "any amount can be deposited"); + assertEq(IStrategy(currentStrategy).maxWithdrawAssets(0).length, 0, "any amount can be withdrawn"); + } + + //endregion --------------------------------------- Additional tests + + //region --------------------------------------- Test implementations + function _testRevenueAmount(address rewards, uint amountRewards) internal returns (uint earnedUSD18) { + uint snapshotId = vm.snapshotState(); + + IStrategy strategy = IStrategy(currentStrategy); + + // --------------------------------------------- Deposit + _tryToDepositToVault(strategy.vault(), 1e18, REVERT_NO, address(this)); + vm.roll(block.number + 6); + + _skip(1 days, 0); + + // --------------------------------------------- First hardwork to initialize share price + vm.prank(platform.multisig()); + IVault(strategy.vault()).doHardWork(); + _skip(5 days, 0); + + // --------------------------------------------- Second hardwork to utilize rewards + // emulate merkl rewards + deal(rewards, currentStrategy, amountRewards); + + vm.recordLogs(); + vm.prank(platform.multisig()); + IVault(strategy.vault()).doHardWork(); + + // extract data from event IStrategy.HardWork + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes32 eventSignature = keccak256("HardWork(uint256,uint256,uint256,uint256,uint256,uint256,uint256[])"); + for (uint i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == eventSignature) { + (,, earnedUSD18,,,,) = abi.decode(logs[i].data, (uint, uint, uint, uint, uint, uint, uint[])); + break; + } + } + vm.revertToState(snapshotId); + + return earnedUSD18; + } + + function _depositChangeLtvWithdraw( + uint minLtv0, + uint maxLtv0, + uint minLtv1, + uint maxLtv1 + ) internal returns (State memory stateInitial, State memory stateAfterDeposit, State memory stateAfterWithdraw) { + uint snapshot = vm.snapshotState(); + address vault = IStrategy(currentStrategy).vault(); + _setMinMaxLtv(minLtv0, maxLtv0); + + stateInitial = _getState(); + + _tryToDepositToVault(vault, 1e18, 0, address(this)); + stateAfterDeposit = _getState(); + + vm.roll(block.number + 6); + + _setMinMaxLtv(minLtv1, maxLtv1); + + _tryToWithdrawFromVault(vault, IVault(vault).balanceOf(address(this))); + stateAfterWithdraw = _getState(); + + vm.revertToState(snapshot); + } + + function _depositChangeLtvDeposit( + uint minLtv0, + uint maxLtv0, + uint minLtv1, + uint maxLtv1 + ) internal returns (State memory stateInitial, State memory stateAfterDeposit, State memory stateAfterDeposit2) { + uint snapshot = vm.snapshotState(); + address vault = IStrategy(currentStrategy).vault(); + _setMinMaxLtv(minLtv0, maxLtv0); + + stateInitial = _getState(); + + _tryToDepositToVault(vault, 1e18, 0, address(this)); + stateAfterDeposit = _getState(); + + vm.roll(block.number + 6); + + _setMinMaxLtv(minLtv1, maxLtv1); + + _tryToDepositToVault(vault, 1e18, 0, address(this)); + stateAfterDeposit2 = _getState(); + + vm.revertToState(snapshot); + } + + /// @notice Deposit, check state, withdraw all, check state + /// @return states [initial state, state after deposit, state after waiting, state after hardwork, state after withdraw] + function _depositWithdraw( + uint amount, + address rewards, + uint rewardsAmount, + uint waitSec, + bool hardworkBeforeWithdraw + ) internal returns (State[] memory states) { + uint snapshot = vm.snapshotState(); + states = new State[](5); + + IStrategy strategy = IStrategy(currentStrategy); + + // --------------------------------------------- Deposit + states[0] = _getState(); + (uint depositedAssets,) = _tryToDepositToVault(strategy.vault(), amount, REVERT_NO, address(this)); + vm.roll(block.number + 6); + states[1] = _getState(); + + _skip(waitSec, 0); + states[2] = _getState(); + + // --------------------------------------------- Hardwork + if (rewardsAmount != 0) { + // emulate merkl rewards + deal(rewards, currentStrategy, rewardsAmount); + } + + if (hardworkBeforeWithdraw) { + vm.prank(platform.multisig()); + IVault(strategy.vault()).doHardWork(); + } + states[3] = _getState(); + + // --------------------------------------------- Withdraw + _tryToWithdrawFromVault(strategy.vault(), states[1].vaultBalance - states[0].vaultBalance); + vm.roll(block.number + 6); + states[4] = _getState(); + + vm.revertToState(snapshot); + + assertLt(states[0].total, states[1].total, "Total should increase after deposit"); + assertEq(depositedAssets, amount, "Deposited amount should be equal to amountsToDeposit"); + } + + //endregion --------------------------------------- Test implementations + + //region --------------------------------------- Internal logic + function _swap(address from, address to, uint amountIn) internal returns (uint amountOut) { + uint balanceBefore = IERC20(to).balanceOf(address(this)); + deal(from, address(this), amountIn); + ISwapper swapper = ISwapper(IPlatform(platform).swapper()); + + IERC20(from).approve(address(swapper), amountIn); + + swapper.swap(from, to, amountIn, 1000); + return IERC20(to).balanceOf(address(this)) - balanceBefore; + } + + function _currentFarmId() internal view returns (uint) { + return IFarmingStrategy(currentStrategy).farmId(); + } + + function _tryToDepositToVault( + address vault, + uint amount, + uint revertKind, + address user + ) internal returns (uint deposited, uint depositedValue) { + address[] memory assets = IVault(vault).assets(); + uint[] memory amountsToDeposit = new uint[](1); + amountsToDeposit[0] = amount; + + // ----------------------------- Prepare amount on user's balance + _dealAndApprove(user, vault, assets, amountsToDeposit); + // console.log("Deposit to vault", assets[0], amounts_[0]); + + uint balanceBefore = IVault(vault).balanceOf(user); + // ----------------------------- Try to deposit assets to the vault + // todo + // if (revertKind == REVERT_NOT_ENOUGH_LIQUIDITY) { + // vm.expectRevert(ISilo.NotEnoughLiquidity.selector); + // } + if (revertKind == REVERT_INSUFFICIENT_BALANCE) { + vm.expectRevert(IControllable.InsufficientBalance.selector); + } + vm.prank(user); + IStabilityVault(vault).depositAssets(assets, amountsToDeposit, 0, user); + + return (amountsToDeposit[0], IVault(vault).balanceOf(user) - balanceBefore); + } + + function _tryToWithdrawFromVault(address vault, uint values) internal returns (uint withdrawn) { + address[] memory _assets = IVault(vault).assets(); + + uint balanceBefore = IERC20(_assets[0]).balanceOf(address(this)); + + vm.prank(address(this)); + IStabilityVault(vault).withdrawAssets(_assets, values, new uint[](1)); + + return IERC20(_assets[0]).balanceOf(address(this)) - balanceBefore; + } + + function _dealAndApprove(address user, address spender, address[] memory assets, uint[] memory amounts) internal { + for (uint j; j < assets.length; ++j) { + deal(assets[j], user, amounts[j]); + + vm.prank(user); + IERC20(assets[j]).approve(spender, amounts[j]); + } + } + + /// @param depositParam0 - Multiplier of flash amount for borrow on deposit. + /// @param depositParam1 - Multiplier of borrow amount to take into account max flash loan fee in maxDeposit + function _setDepositParams(uint depositParam0, uint depositParam1) internal { + ILeverageLendingStrategy strategy = ILeverageLendingStrategy(currentStrategy); + (uint[] memory params, address[] memory addresses) = strategy.getUniversalParams(); + + params[0] = depositParam0; + params[1] = depositParam1; + + vm.prank(platform.multisig()); + strategy.setUniversalParams(params, addresses); + } + + /// @param withdrawParam0 - Multiplier of flash amount for borrow on withdraw. + /// @param withdrawParam1 - Multiplier of amount allowed to be deposited after withdraw. Default is 100_00 == 100% (deposit forbidden) + /// @param withdrawParam2 - allows to disable withdraw through increasing ltv if leverage is near to target + function _setWithdrawParams(uint withdrawParam0, uint withdrawParam1, uint withdrawParam2) internal { + ILeverageLendingStrategy strategy = ILeverageLendingStrategy(currentStrategy); + (uint[] memory params, address[] memory addresses) = strategy.getUniversalParams(); + + params[2] = withdrawParam0; + params[3] = withdrawParam1; + params[11] = withdrawParam2; + + vm.prank(platform.multisig()); + strategy.setUniversalParams(params, addresses); + } + + function _getState() internal view returns (State memory state) { + ILeverageLendingStrategy strategy = ILeverageLendingStrategy(address(currentStrategy)); + + (state.sharePrice,) = strategy.realSharePrice(); + + ( + state.ltv, + state.maxLtv, + state.leverage, + state.collateralAmount, + state.debtAmount, + state.targetLeveragePercent + ) = strategy.health(); + + state.total = IStrategy(currentStrategy).total(); + state.maxLeverage = 100_00 * 1e4 / (1e4 - state.maxLtv); + state.targetLeverage = state.maxLeverage * state.targetLeveragePercent / 100_00; + state.strategyBalanceAsset = + IERC20(IStrategy(address(strategy)).assets()[0]).balanceOf(address(currentStrategy)); + state.userBalanceAsset = IERC20(IStrategy(address(strategy)).assets()[0]).balanceOf(address(address(this))); + (state.realTvl,) = strategy.realTvl(); + (state.realSharePrice,) = strategy.realSharePrice(); + state.vaultBalance = IVault(IStrategy(address(strategy)).vault()).balanceOf(address(this)); + (state.revenueAssets, state.revenueAmounts) = IStrategy(currentStrategy).getRevenue(); + + // _printState(state); + return state; + } + + function _printState(State memory state) internal pure { + console.log("state **************************************************"); + console.log("ltv", state.ltv); + console.log("maxLtv", state.maxLtv); + console.log("targetLeverage", state.targetLeverage); + console.log("leverage", state.leverage); + console.log("total", state.total); + console.log("collateralAmount", state.collateralAmount); + console.log("debtAmount", state.debtAmount); + console.log("targetLeveragePercent", state.targetLeveragePercent); + console.log("maxLeverage", state.maxLeverage); + console.log("realTvl", state.realTvl); + console.log("realSharePrice", state.realSharePrice); + console.log("vaultBalance", state.vaultBalance); + console.log("strategyBalanceAsset", state.strategyBalanceAsset); + console.log("userBalanceAsset", state.userBalanceAsset); + for (uint i = 0; i < state.revenueAssets.length; i++) { + console.log("revenueAsset", i, state.revenueAssets[i], state.revenueAmounts[i]); + } + } + + function _setMinMaxLtv(uint minLtv, uint maxLtv) internal { + IFarmingStrategy strategy = IFarmingStrategy(currentStrategy); + uint farmId = strategy.farmId(); + IFactory factory = IFactory(IPlatform(IControllable(currentStrategy).platform()).factory()); + + IFactory.Farm memory farm = factory.farm(farmId); + farm.nums[0] = minLtv; + farm.nums[1] = maxLtv; + + vm.prank(platform.multisig()); + factory.updateFarm(farmId, farm); + } + + function getMinMaxLtv(uint targetLeverage, uint delta) internal pure returns (uint minLtv, uint maxLtv) { + // a = (-1 + sqrt(1 + delta^2 TL^2)) / delta + // L0 = TL - a, L1 = TL + a + // LTVmin = 1 - 1/L0, LTVmax = 1 - 1/L1 + + delta = delta * 1e6; + targetLeverage = targetLeverage * 1e6; + uint a = uint( + (-int(1e10 ** 2) + int(FixedPointMathLib.sqrt(1e10 ** 4 + delta * delta * targetLeverage * targetLeverage))) + / int(delta) + ); + uint leverage0 = targetLeverage - a; + uint leverage1 = targetLeverage + a; + + minLtv = 1e4 - 1e4 * 1e10 / leverage0; + maxLtv = 1e4 - 1e4 * 1e10 / leverage1; + } + + //endregion --------------------------------------- Internal logic + + //region --------------------------------------- Helper functions + function _upgradePlatform(address multisig, address priceReader_) internal { + // we need to skip 1 day to update the swapper + // but we cannot simply skip 1 day, because the silo oracle will start to revert with InvalidPrice + // vm.warp(block.timestamp - 86400); + rewind(86400); + + IPlatform platform = IPlatform(IControllable(priceReader_).platform()); + + address[] memory proxies = new address[](1); + address[] memory implementations = new address[](1); + + proxies[0] = address(priceReader_); + //proxies[1] = platform.swapper(); + //proxies[2] = platform.ammAdapter(keccak256(bytes(AmmAdapterIdLib.META_VAULT))).proxy; + + implementations[0] = address(new PriceReader()); + //implementations[1] = address(new Swapper()); + //implementations[2] = address(new MetaVaultAdapter()); + + //vm.prank(multisig); + // platform.cancelUpgrade(); + + vm.startPrank(multisig); + platform.announcePlatformUpgrade("2025.07.22-alpha", proxies, implementations); + + skip(1 days); + platform.upgrade(); + vm.stopPrank(); + } + + function _setUpFlashLoanVault(address flashLoanVault, ILeverageLendingStrategy.FlashLoanKind kind_) internal { + _setFlashLoanVault(ILeverageLendingStrategy(currentStrategy), flashLoanVault, uint(kind_)); + } + + function _setFlashLoanVault(ILeverageLendingStrategy strategy, address flashLoanVault, uint kind) internal { + address multisig = platform.multisig(); + + (uint[] memory params, address[] memory addresses) = strategy.getUniversalParams(); + params[10] = kind; + addresses[0] = flashLoanVault; + + vm.prank(multisig); + strategy.setUniversalParams(params, addresses); + } + + function _getWethPrice8() internal view returns (uint) { + return IAavePriceOracle( + IAaveAddressProvider(IPool(PlasmaConstantsLib.AAVE_V3_POOL).ADDRESSES_PROVIDER()).getPriceOracle() + ).getAssetPrice(PlasmaConstantsLib.TOKEN_WETH); + } + + function _addRoutes() internal { + // add routes + ISwapper swapper = ISwapper(IPlatform(platform).swapper()); + ISwapper.PoolData[] memory pools = new ISwapper.PoolData[](1); + pools[0] = ISwapper.PoolData({ + pool: PlasmaConstantsLib.TOKEN_WAPLAWETH, + ammAdapter: (IPlatform(platform).ammAdapter(keccak256(bytes(AmmAdapterIdLib.ERC_4626)))).proxy, + tokenIn: PlasmaConstantsLib.TOKEN_WAPLAWETH, + tokenOut: PlasmaConstantsLib.TOKEN_WETH + }); + // pools[1] = ISwapper.PoolData({ + // pool: PlasmaConstantsLib.TOKEN_WAPLAUSDE, + // ammAdapter: (IPlatform(platform).ammAdapter(keccak256(bytes(AmmAdapterIdLib.ERC_4626)))).proxy, + // tokenIn: PlasmaConstantsLib.TOKEN_WAPLAUSDE, + // tokenOut: PlasmaConstantsLib.TOKEN_USDE + // }); + swapper.addPools(pools, false); + } + + // function displayAssetsData() internal view { + // IAaveDataProvider.TokenData[] memory tokens = IAaveDataProvider(PlasmaConstantsLib.AAVE_V3_POOL_DATA_PROVIDER).getAllReservesTokens(); + // for (uint i = 0; i < tokens.length; i++) { + // IPool.ReserveConfigurationMap memory data = IPool(PlasmaConstantsLib.AAVE_V3_POOL).getReserveData(tokens[i].tokenAddress).configuration; + // uint256 eModeCategoryId = (data.data >> 168) & 0xFF; + // console.log(tokens[i].symbol, tokens[i].tokenAddress, eModeCategoryId); + // console.log(data.data); + // } + // } + + //endregion --------------------------------------- Helper functions +} diff --git a/test/strategies/ALMF.Sonic.t.sol b/test/strategies/ALMF.Sonic.t.sol new file mode 100755 index 000000000..3c25ebd78 --- /dev/null +++ b/test/strategies/ALMF.Sonic.t.sol @@ -0,0 +1,1038 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {Vm} from "forge-std/Test.sol"; +import {IControllable} from "../../src/interfaces/IControllable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IFarmingStrategy} from "../../src/interfaces/IFarmingStrategy.sol"; +import {ILeverageLendingStrategy} from "../../src/interfaces/ILeverageLendingStrategy.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {IFactory} from "../../src/interfaces/IFactory.sol"; +import {IStabilityVault} from "../../src/interfaces/IStabilityVault.sol"; +import {IStrategy} from "../../src/interfaces/IStrategy.sol"; +import {IVault} from "../../src/interfaces/IVault.sol"; +import {SonicConstantsLib} from "../../chains/sonic/SonicConstantsLib.sol"; +import {SonicSetup} from "../base/chains/SonicSetup.sol"; +import {StrategyIdLib} from "../../src/strategies/libs/StrategyIdLib.sol"; +import {UniversalTest} from "../base/UniversalTest.sol"; +import {PriceReader} from "../../src/core/PriceReader.sol"; +import {AaveLeverageMerklFarmStrategy} from "../../src/strategies/AaveLeverageMerklFarmStrategy.sol"; +import {IAaveAddressProvider} from "../../src/integrations/aave/IAaveAddressProvider.sol"; +import {IAavePriceOracle} from "../../src/integrations/aave/IAavePriceOracle.sol"; +import {IPool} from "../../src/integrations/aave/IPool.sol"; +import {ALMFLib} from "../../src/strategies/libs/ALMFLib.sol"; +import {IFlashLoanRecipient} from "../../src/integrations/balancer/IFlashLoanRecipient.sol"; +import {console} from "forge-std/console.sol"; +import {SharedFarmMakerLib} from "../../chains/shared/SharedFarmMarketLib.sol"; + +contract ALMFStrategySonicTest is SonicSetup, UniversalTest { + uint public constant REVERT_NO = 0; + uint public constant REVERT_NOT_ENOUGH_LIQUIDITY = 1; + uint public constant REVERT_INSUFFICIENT_BALANCE = 2; + + uint internal constant INDEX_INIT_0 = 0; + uint internal constant INDEX_AFTER_DEPOSIT_1 = 1; + uint internal constant INDEX_AFTER_WAIT_2 = 2; + uint internal constant INDEX_AFTER_HARDWORK_3 = 3; + uint internal constant INDEX_AFTER_WITHDRAW_4 = 4; + + uint internal constant INDEX_D1 = 0; + uint internal constant INDEX_D2 = 1; + uint internal constant INDEX_W1 = 2; + uint internal constant INDEX_W2 = 3; + + uint internal constant DEFAULT_MIN_LTV = 49_00; + uint internal constant DEFAULT_MAX_LTV = 50_97; + + struct State { + uint ltv; + uint maxLtv; + uint leverage; + uint maxLeverage; + uint targetLeverage; + uint targetLeveragePercent; + uint collateralAmount; + uint debtAmount; + uint total; + uint sharePrice; + uint strategyBalanceCollateralAsset; + uint strategyBalanceBorrowAsset; + uint userBalanceAsset; + uint realTvl; + uint realSharePrice; + uint vaultBalance; + address[] revenueAssets; + uint[] revenueAmounts; + } + + uint internal constant FORK_BLOCK = 55057575; // Nov-13-2025 02:19:01 AM +UTC + + address internal constant POOL = 0x5362dBb1e601abF3a4c14c22ffEdA64042E5eAA3; + address internal constant ATOKEN_USDC = 0x578Ee1ca3a8E1b54554Da1Bf7C583506C4CD11c6; + address internal constant ATOKEN_WETH = 0xe18Ab82c81E7Eecff32B8A82B1b7d2d23F1EcE96; + + constructor() { + vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"), FORK_BLOCK)); + + allowZeroApr = true; + duration1 = 0.1 hours; + duration2 = 0.1 hours; + duration3 = 0.1 hours; + + // ALMF uses real share price as share price + // so it cannot initialize share price during deposit. + // It sets initial value of share price in first claimRevenue. + // As result, following check is failed in universal test: + // "Universal test: estimated totalRevenueUSD is zero" + // So, we should disable it by setting allowZeroTotalRevenueUSD. + // And make all checks in additional tests instead. + allowZeroTotalRevenueUSD = true; + + // _upgradePlatform(platform.multisig(), IPlatform(PLATFORM).priceReader()); + } + + //region --------------------------------------- Universal test + function testStorage() public pure { + bytes32 h = keccak256(abi.encode(uint(keccak256("erc7201:stability.AaveLeverageMerklFarmStrategy")) - 1)) + & ~bytes32(uint(0xff)); + // console.log("erc7201:stability.AaveLeverageMerklFarmStrategy"); + // console.logBytes32(h); + assertEq(ALMFLib.AAVE_MERKL_FARM_STRATEGY_STORAGE_LOCATION, h, "ALMFLib storage location"); + } + + function testALMFSonic() public universalTest { + _addStrategy(_addFarm()); + } + + function _addStrategy(uint farmId) internal { + strategies.push( + Strategy({ + id: StrategyIdLib.AAVE_LEVERAGE_MERKL_FARM, + pool: address(0), + farmId: farmId, + strategyInitAddresses: new address[](0), + strategyInitNums: new uint[](0) + }) + ); + } + + function _addFarm() internal returns (uint farmId) { + address[] memory rewards = new address[](2); + rewards[0] = SonicConstantsLib.TOKEN_USDC; + rewards[1] = SonicConstantsLib.TOKEN_USDT; + + IFactory.Farm[] memory farms = new IFactory.Farm[](1); + farms[0] = SharedFarmMakerLib._makeAaveLeverageMerklFarm( + ATOKEN_WETH, + ATOKEN_USDC, + SonicConstantsLib.BEETS_VAULT, + rewards, + DEFAULT_MIN_LTV, // min target ltv + DEFAULT_MAX_LTV, // max target ltv + 0, // beets v2 flash loan kind + 0, // eMode is not used + 0 // share price is calculated in collateral asset per USD + ); + + vm.startPrank(platform.multisig()); + factory.addFarms(farms); + + return factory.farmsLength() - 1; + } + + function _preDeposit() internal override { + // --------- bad paths + vm.expectRevert(IControllable.IncorrectMsgSender.selector); + IFlashLoanRecipient(currentStrategy).receiveFlashLoan(new address[](1), new uint[](1), new uint[](1), ""); + + // --------- thresholds + vm.expectRevert(IControllable.NotOperator.selector); + vm.prank(makeAddr("1")); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(SonicConstantsLib.TOKEN_WETH, 1e12); + + vm.prank(platform.multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(SonicConstantsLib.TOKEN_WETH, 1e12); + vm.prank(platform.multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(SonicConstantsLib.TOKEN_USDC, 1e6); + + assertEq(AaveLeverageMerklFarmStrategy(currentStrategy).threshold(SonicConstantsLib.TOKEN_WETH), 1e12); + assertEq(AaveLeverageMerklFarmStrategy(currentStrategy).threshold(SonicConstantsLib.TOKEN_USDC), 1e6); + + // ---------------------------------- Make additional tests + uint snapshot = vm.snapshotState(); + + // --------- any tests with zero initial supply + _testWithdrawWithZeroDebt(); + + // --------- initial supply + _tryToDepositToVault(IStrategy(currentStrategy).vault(), 0.1e18, REVERT_NO, makeAddr("initial supplier")); + + // --------- various deposit - withdraw tests + _testRevenueAmount(); + + _testDepositWithdrawWithRewardsOnBalance(); + + // check direct deposit of small amount without leverage + _testDepositAmountLessThanThreshold(); + + // check deposit with direct repay before depositing with the leverage + _testDepositWithPartialDirectRepay(); + _testDepositWithFullDirectRepay(); + + // check revenue (replacement for "Universal test: estimated totalRevenueUSD is zero") + _testDepositTwoHardworks(); + + // check deposit-wait 30 days-hardwork-withdraw results + _testDepositWaitHardworkWithdraw(); + + // --------- Target LTV + // set TL, deposit, change TL, withdraw/deposit => leverage was changed toward new TL + _testDepositChangeLtvWithdraw(); + _testDepositChangeLtvDeposit(); + + // --------- check flash loan vault of various kinds + _testDepositWithdrawUsingFlashLoan( + SonicConstantsLib.BEETS_VAULT, ILeverageLendingStrategy.FlashLoanKind.Default_0 + ); + _testDepositWithdrawUsingFlashLoan( + SonicConstantsLib.BEETS_VAULT_V3, ILeverageLendingStrategy.FlashLoanKind.BalancerV3_1 + ); + _testDepositWithdrawUsingFlashLoan( + SonicConstantsLib.POOL_SHADOW_CL_USDC_USDT, ILeverageLendingStrategy.FlashLoanKind.UniswapV3_2 + ); + _testDepositWithdrawUsingFlashLoan( + SonicConstantsLib.POOL_ALGEBRA_WS_USDC, ILeverageLendingStrategy.FlashLoanKind.AlgebraV4_3 + ); + + _testGetPrices(); + + vm.revertToState(snapshot); + } + + function _preHardWork() internal override { + // emulate merkl rewards + deal(SonicConstantsLib.TOKEN_USDC, currentStrategy, 1e6); + deal(SonicConstantsLib.TOKEN_USDT, currentStrategy, 1e6); + } + + //endregion --------------------------------------- Universal test + + //region --------------------------------------- Additional tests + function _testRevenueAmount() internal { + uint snapshotId = vm.snapshotState(); + + IStrategy strategy = IStrategy(currentStrategy); + + // --------------------------------------------- Deposit + _tryToDepositToVault(strategy.vault(), 1e18, REVERT_NO, address(this)); + vm.roll(block.number + 6); + + _skip(1 days, 0); + + // --------------------------------------------- First hardwork to initialize share price + vm.prank(platform.multisig()); + IVault(strategy.vault()).doHardWork(); + _skip(5 days, 0); + + // --------------------------------------------- Second hardwork to utilize rewards + // emulate merkl rewards + deal( + SonicConstantsLib.TOKEN_USDC, // rewards are in USDC (borrow asset) + currentStrategy, + 100e6 + ); + + vm.recordLogs(); + vm.prank(platform.multisig()); + IVault(strategy.vault()).doHardWork(); + + // extract data from event IStrategy.HardWork + uint earnedUSD18; + + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes32 eventSignature = keccak256("HardWork(uint256,uint256,uint256,uint256,uint256,uint256,uint256[])"); + for (uint i = 0; i < logs.length; i++) { + if (logs[i].topics[0] == eventSignature) { + (,, earnedUSD18,,,,) = abi.decode(logs[i].data, (uint, uint, uint, uint, uint, uint, uint[])); + break; + } + } + assertApproxEqRel( + earnedUSD18, 100e18, 1e16, "_testRevenueAmount.Earned in HardWork event is approximately 100 USDC" + ); + + vm.revertToState(snapshotId); + } + + function _testDepositTwoHardworks() internal { + uint snapshot = vm.snapshotState(); + uint amount = 1e18; + + uint priceWeth8 = _getWethPrice8(); + + IStrategy strategy = IStrategy(currentStrategy); + + // --------------------------------------------- Deposit + State memory stateAfterDeposit = _getState(); + _tryToDepositToVault(strategy.vault(), amount, REVERT_NO, address(this)); + vm.roll(block.number + 6); + + // --------------------------------------------- Hardwork 1 + _skip(1 days, 0); + deal(SonicConstantsLib.TOKEN_USDT, currentStrategy, 100e6); + + vm.prank(platform.multisig()); + IVault(strategy.vault()).doHardWork(); + + State memory stateAfterHW1 = _getState(); + + // --------------------------------------------- Hardwork 2 + _skip(1 days, 0); + deal(SonicConstantsLib.TOKEN_USDT, currentStrategy, 300e6); + + vm.prank(platform.multisig()); + IVault(strategy.vault()).doHardWork(); + + State memory stateAfterHW2 = _getState(); + + assertEq( + stateAfterDeposit.revenueAmounts[0], + 0, + "_testDepositTwoHardworks.Revenue before first claimReview is 0 because share price is not initialized yet" + ); + assertApproxEqRel( + stateAfterHW1.revenueAmounts[0] * priceWeth8 * 1e6 / 1e8 / 1e18, + 100e6 * 70 / 100, // 70%, platform fee + 20e16, + "_testDepositTwoHardworks.Revenue after first hardwork is ~$100" + ); + assertApproxEqRel( + stateAfterHW2.revenueAmounts[0] * priceWeth8 * 1e6 / 1e8 / 1e18, + 300e6 * 70 / 100, // 70%, platform fee + 20e16, + "_testDepositTwoHardworks.Revenue after first hardwork is ~$300" + ); + vm.revertToState(snapshot); + } + + function _testDepositChangeLtvWithdraw() internal { + { + (, State memory stateAfterDeposit, State memory stateAfterWithdraw) = + _depositChangeLtvWithdraw(49_00, 50_97, 52_00, 51_97); + + assertApproxEqRel( + stateAfterDeposit.leverage, + stateAfterDeposit.targetLeverage, + 1e16, + "_testDepositChangeLtvWithdraw.Leverage after deposit should be equal to target 111" + ); + assertLt( + stateAfterDeposit.leverage, + stateAfterWithdraw.targetLeverage, + "_testDepositChangeLtvWithdraw.leverage before withdraw less than target" + ); + assertGt( + stateAfterWithdraw.leverage, + stateAfterDeposit.leverage, + "_testDepositChangeLtvWithdraw.withdraw increased the leverage" + ); + } + { + (, State memory stateAfterDeposit, State memory stateAfterWithdraw) = + _depositChangeLtvWithdraw(49_00, 50_97, 47_00, 48_97); + + assertApproxEqRel( + stateAfterDeposit.leverage, + stateAfterDeposit.targetLeverage, + 1e16, + "_testDepositChangeLtvWithdraw.Leverage after deposit should be equal to target 222" + ); + assertGt( + stateAfterDeposit.leverage, + stateAfterWithdraw.targetLeverage, + "_testDepositChangeLtvWithdraw.leverage before withdraw greater than target" + ); + assertLt( + stateAfterWithdraw.leverage, + stateAfterDeposit.leverage, + "_testDepositChangeLtvWithdraw.withdraw decreased the leverage" + ); + } + } + + function _testDepositChangeLtvDeposit() internal { + { + (, State memory stateAfterDeposit, State memory stateAfterDeposit2) = + _depositChangeLtvDeposit(49_00, 50_97, 52_00, 51_97); + + assertApproxEqRel( + stateAfterDeposit.leverage, + stateAfterDeposit.targetLeverage, + 1e16, + "_testDepositChangeLtvDeposit.Leverage after deposit should be equal to target 333" + ); + assertLt( + stateAfterDeposit.leverage, + stateAfterDeposit2.targetLeverage, + "_testDepositChangeLtvDeposit.leverage before withdraw less than target" + ); + assertGt( + stateAfterDeposit2.leverage, + stateAfterDeposit.leverage, + "_testDepositChangeLtvDeposit.deposit2 increased the leverage" + ); + } + { + (, State memory stateAfterDeposit, State memory stateAfterDeposit2) = + _depositChangeLtvDeposit(49_00, 50_97, 47_00, 48_97); + + assertApproxEqRel( + stateAfterDeposit.leverage, + stateAfterDeposit.targetLeverage, + 1e16, + "_testDepositChangeLtvDeposit.Leverage after deposit should be equal to target 444" + ); + assertGt( + stateAfterDeposit.leverage, + stateAfterDeposit2.targetLeverage, + "_testDepositChangeLtvDeposit.leverage before deposit2 greater than target" + ); + assertLt( + stateAfterDeposit2.leverage, + stateAfterDeposit.leverage, + "_testDepositChangeLtvDeposit.deposit2 decreased the leverage" + ); + } + } + + function _testDepositWithdrawUsingFlashLoan( + address flashLoanVault, + ILeverageLendingStrategy.FlashLoanKind kind_ + ) internal { + uint snapshot = vm.snapshotState(); + _setUpFlashLoanVault(flashLoanVault, kind_); + + if (kind_ == ILeverageLendingStrategy.FlashLoanKind.BalancerV3_1) { + uint snapshot2 = vm.snapshotState(); + _tryToDepositToVault( + IStrategy(currentStrategy).vault(), 100_000e18, REVERT_INSUFFICIENT_BALANCE, address(this) + ); + vm.revertToState(snapshot2); + } + + uint amount = 1e18; + State[] memory states = _depositWithdraw(amount, SonicConstantsLib.TOKEN_USDC, 0, 0, false); + vm.revertToState(snapshot); + + assertApproxEqRel( + states[INDEX_AFTER_WITHDRAW_4].total, + states[INDEX_INIT_0].total, + states[INDEX_INIT_0].total / 100_000, + "_testDepositWithdrawUsingFlashLoan.Total should return back to prev value" + ); + assertApproxEqRel( + states[4].userBalanceAsset, + amount, + amount / 50, + "_testDepositWithdrawUsingFlashLoan.User shouldn't loss more than 2%" + ); + } + + function _testDepositWaitHardworkWithdraw() internal { + uint amount = 1e18; + + // --------------------------------------------- Deposit+withdraw without hardwork + uint snapshot = vm.snapshotState(); + State[] memory statesInstant = _depositWithdraw(amount, SonicConstantsLib.TOKEN_USDC, 0, 0, true); + vm.revertToState(snapshot); + + // --------------------------------------------- Deposit, wait, [no rewards], hardwork, withdraw + snapshot = vm.snapshotState(); + State[] memory statesHW1 = _depositWithdraw(amount, SonicConstantsLib.TOKEN_USDC, 0, 1 days, true); + vm.revertToState(snapshot); + + // --------------------------------------------- Deposit, wait, rewards, hardwork, withdraw + snapshot = vm.snapshotState(); + State[] memory statesHW2 = _depositWithdraw(amount, SonicConstantsLib.TOKEN_USDC, 100e6, 1 days, true); + vm.revertToState(snapshot); + + // --------------------------------------------- Get WETH price + uint wethPrice = _getWethPrice8(); + + // --------------------------------------------- Compare results + + assertApproxEqAbs( + statesHW2[INDEX_AFTER_HARDWORK_3].total - statesInstant[INDEX_AFTER_HARDWORK_3].total, + 100e18 * 70 / 100, + 3e18, + "_testDepositWaitHardworkWithdraw.total is increased on rewards amount - fees" + ); + assertLt( + statesHW1[INDEX_AFTER_HARDWORK_3].total, + statesInstant[INDEX_AFTER_HARDWORK_3].total, + "_testDepositWaitHardworkWithdraw.total is decreased because the borrow rate exceeds supply rate" + ); + + assertLt( + statesHW1[INDEX_AFTER_WITHDRAW_4].userBalanceAsset, + statesInstant[INDEX_AFTER_WITHDRAW_4].userBalanceAsset, + "_testDepositWaitHardworkWithdraw.user lost some amount because of borrow rate" + ); + assertApproxEqRel( + statesHW2[INDEX_AFTER_WITHDRAW_4].userBalanceAsset, + 70e18 / wethPrice * 1e8 + statesInstant[INDEX_AFTER_WITHDRAW_4].userBalanceAsset, + 3e15, // < 0.3% + "_testDepositWaitHardworkWithdraw.user received almost all rewards" + ); + } + + function _testMaxDepositAndMaxWithdraw() internal view { + assertEq( + IStrategy(currentStrategy).maxDepositAssets().length, + 0, + "_testMaxDepositAndMaxWithdraw.any amount can be deposited" + ); + assertEq( + IStrategy(currentStrategy).maxWithdrawAssets(0).length, + 0, + "_testMaxDepositAndMaxWithdraw.any amount can be withdrawn" + ); + } + + /// @notice ar != 0, ad == 0 + function _testDepositWithFullDirectRepay() internal { + uint snapshot = vm.snapshotState(); + (State memory state0, State memory state1, State memory state2) = + _testDepositWithDirectRepay([uint(1e18), 0.1e18]); + + uint priceCollateral8 = _getWethPrice8(); + uint priceDebt8 = IAavePriceOracle(IAaveAddressProvider(IPool(POOL).ADDRESSES_PROVIDER()).getPriceOracle()) + .getAssetPrice(SonicConstantsLib.TOKEN_USDC); + uint amountToRepay = 0.1e18 * priceCollateral8 * 1e6 / priceDebt8 / 1e18; + + assertEq( + state1.collateralAmount, + state0.collateralAmount, + "_testDepositWithFullDirectRepay.collateral should not change" + ); + assertApproxEqRel( + state1.debtAmount, + state0.debtAmount - amountToRepay, + 1e16, + "_testDepositWithFullDirectRepay.debt should be reduced on the deposited value (1% can be loss on swap)" + ); + + assertApproxEqRel( + state2.userBalanceAsset, + state0.userBalanceAsset + 1e18 + 0.1e18, + 1e16, + "_testDepositWithFullDirectRepay.user received deposited amounts back" + ); + vm.revertToState(snapshot); + } + + /// @notice ar != 0, ad != 0 + function _testDepositWithPartialDirectRepay() internal { + uint snapshot = vm.snapshotState(); + (State memory state0, State memory state1, State memory state2) = + _testDepositWithDirectRepay([uint(1e18), 0.3e18]); + + assertGt( + state1.collateralAmount, + state0.collateralAmount, + "_testDepositWithPartialDirectRepay.collateral is increased" + ); + assertApproxEqRel( + state1.leverage, + state1.targetLeverage, + 1e16, + "_testDepositWithPartialDirectRepay.leverage should be equal to target" + ); + + assertApproxEqRel( + state2.userBalanceAsset, + state0.userBalanceAsset + 1e18 + 0.3e18, + 1e16, + "_testDepositWithPartialDirectRepay.user received deposited amounts back" + ); + vm.revertToState(snapshot); + } + + /// @dev Improve coverage of _withdrawRequiredAmountOnBalance + function _testWithdrawWithZeroDebt() internal { + uint snapshot = vm.snapshotState(); + + address vault = IStrategy(currentStrategy).vault(); + uint amount = 0.1e18; + + // ---------- Set the threshold higher than the amount + vm.prank(platform.multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(SonicConstantsLib.TOKEN_WETH, 2 * amount); + + // ---------- Deposit + State memory state0 = _getState(); + _tryToDepositToVault(vault, amount, REVERT_NO, address(this)); + State memory state1 = _getState(); + + // ---------- Withdraw all + _tryToWithdrawFromVault(vault, IStabilityVault(vault).balanceOf(address(this))); + State memory state2 = _getState(); + + // _printState(state0); + // _printState(state1); + // _printState(state2); + + // ---------- Check results + assertGt(state1.collateralAmount, state0.collateralAmount, "_testWithdrawWithZeroDebt.collateral is increased"); + assertEq( + state1.debtAmount, + state0.debtAmount, + "_testWithdrawWithZeroDebt.debt is NOT increased (there was direct supply)" + ); + + assertGt(state2.realTvl, 0, "_testWithdrawWithZeroDebt.result tvl is not 0 (initial shares)"); + assertApproxEqRel( + state2.userBalanceAsset, + state0.userBalanceAsset + amount, + 1e16, + "_testWithdrawWithZeroDebt.user received deposited amounts back (without initial shares)" + ); + + vm.revertToState(snapshot); + } + + function _testDepositAmountLessThanThreshold() internal { + uint snapshot = vm.snapshotState(); + + address vault = IStrategy(currentStrategy).vault(); + uint amount = 0.01e18; + + // ---------- Set the threshold higher than the amount + vm.prank(platform.multisig()); + AaveLeverageMerklFarmStrategy(currentStrategy).setThreshold(SonicConstantsLib.TOKEN_WETH, 2 * amount); + + // ---------- Deposit and withdraw + State memory state0 = _getState(); + _tryToDepositToVault(vault, amount, REVERT_NO, address(this)); + State memory state1 = _getState(); + _tryToWithdrawFromVault(vault, IStabilityVault(vault).balanceOf(address(this)) * 9999 / 10000); + //State memory state15 = _getState(); + _tryToWithdrawFromVault(vault, IStabilityVault(vault).balanceOf(address(this))); + State memory state2 = _getState(); + + // _printState(state0); + // _printState(state1); + // _printState(state15); + // _printState(state2); + + // ---------- Check results + assertGt( + state1.collateralAmount, + state0.collateralAmount, + "_testDepositAmountLessThanThreshold.collateral is increased after direct deposit without leverage" + ); + assertEq( + state1.debtAmount, + state0.debtAmount, + "_testDepositAmountLessThanThreshold.debt is NOT changed after direct deposit without leverage" + ); + + assertApproxEqRel( + state2.realTvl, state0.realTvl, 1e10, "_testDepositAmountLessThanThreshold.result tvl should not change" + ); + assertApproxEqRel( + state2.userBalanceAsset, + state0.userBalanceAsset + amount, + 1e16, + "_testDepositAmountLessThanThreshold.user received deposited amounts back" + ); + + vm.revertToState(snapshot); + } + + function _testDepositWithdrawWithRewardsOnBalance() internal { + uint snapshot = vm.snapshotState(); + uint amount = 1e18; + + // -------- Put rewards on strategy balance + deal(SonicConstantsLib.TOKEN_USDC, currentStrategy, 171e6); + + // -------- Deposit + State memory state0 = _getState(); + IStrategy strategy = IStrategy(currentStrategy); + _tryToDepositToVault(strategy.vault(), amount, REVERT_NO, address(this)); + vm.roll(block.number + 6); + + // -------- Withdraw all + _tryToWithdrawFromVault(strategy.vault(), IStabilityVault(strategy.vault()).balanceOf(address(this))); + State memory state2 = _getState(); + vm.roll(block.number + 6); + + vm.revertToState(snapshot); + + // _printState(state0); + // _printState(state1); + // _printState(state2); + + assertApproxEqRel( + state2.realTvl, + state0.realTvl, + 1e10, + "_testDepositWithdrawWithRewardsOnBalance.result tvl should not change" + ); + assertApproxEqRel( + state2.userBalanceAsset, + state0.userBalanceAsset + amount, + 1.1e16, + "_testDepositWithdrawWithRewardsOnBalance.user received deposited amounts back" + ); + assertEq( + state2.strategyBalanceBorrowAsset, + state0.strategyBalanceBorrowAsset, + "_testDepositWithdrawWithRewardsOnBalance.Rewards should stay on strategy balance" + ); + } + + function _testGetPrices() internal view { + (uint priceC, uint priceB) = AaveLeverageMerklFarmStrategy(currentStrategy).getPrices(); + + IAavePriceOracle oracle = + IAavePriceOracle(IAaveAddressProvider(IPool(POOL).ADDRESSES_PROVIDER()).getPriceOracle()); + + // WETH - collateral, USDC - borrow asset + uint priceWeth8 = oracle.getAssetPrice(SonicConstantsLib.TOKEN_WETH); + assertEq(priceC, priceWeth8 * 1e10, "_testGetPrices.Collateral price should be equal to Aave price"); + + uint priceUsdc8 = oracle.getAssetPrice(SonicConstantsLib.TOKEN_USDC); + assertEq(priceB, priceUsdc8 * 1e10, "_testGetPrices.Borrow price should be equal to Aave price"); + } + + //endregion --------------------------------------- Additional tests + + //region --------------------------------------- Test implementations + function _depositChangeLtvWithdraw( + uint minLtv0, + uint maxLtv0, + uint minLtv1, + uint maxLtv1 + ) internal returns (State memory stateInitial, State memory stateAfterDeposit, State memory stateAfterWithdraw) { + uint snapshot = vm.snapshotState(); + address vault = IStrategy(currentStrategy).vault(); + _setMinMaxLtv(minLtv0, maxLtv0); + + stateInitial = _getState(); + + _tryToDepositToVault(vault, 1e18, 0, address(this)); + stateAfterDeposit = _getState(); + + vm.roll(block.number + 6); + + _setMinMaxLtv(minLtv1, maxLtv1); + + _tryToWithdrawFromVault(vault, IVault(vault).balanceOf(address(this))); + stateAfterWithdraw = _getState(); + + vm.revertToState(snapshot); + } + + function _depositChangeLtvDeposit( + uint minLtv0, + uint maxLtv0, + uint minLtv1, + uint maxLtv1 + ) internal returns (State memory stateInitial, State memory stateAfterDeposit, State memory stateAfterDeposit2) { + uint snapshot = vm.snapshotState(); + address vault = IStrategy(currentStrategy).vault(); + _setMinMaxLtv(minLtv0, maxLtv0); + + stateInitial = _getState(); + + _tryToDepositToVault(vault, 1e18, 0, address(this)); + stateAfterDeposit = _getState(); + + vm.roll(block.number + 6); + + _setMinMaxLtv(minLtv1, maxLtv1); + + _tryToDepositToVault(vault, 1e18, 0, address(this)); + stateAfterDeposit2 = _getState(); + + vm.revertToState(snapshot); + } + + /// @notice Deposit, check state, withdraw all, check state + /// @return states [initial state, state after deposit, state after waiting, state after hardwork, state after withdraw] + function _depositWithdraw( + uint amount, + address rewards, + uint rewardsAmount, + uint waitSec, + bool hardworkBeforeWithdraw + ) internal returns (State[] memory states) { + uint snapshot = vm.snapshotState(); + states = new State[](5); + + IStrategy strategy = IStrategy(currentStrategy); + + // --------------------------------------------- Deposit + states[0] = _getState(); + (uint depositedAssets,) = _tryToDepositToVault(strategy.vault(), amount, REVERT_NO, address(this)); + vm.roll(block.number + 6); + states[1] = _getState(); + + _skip(waitSec, 0); + states[2] = _getState(); + + // --------------------------------------------- Hardwork + if (rewardsAmount != 0) { + // emulate merkl rewards + deal(rewards, currentStrategy, rewardsAmount); + } + + if (hardworkBeforeWithdraw) { + vm.prank(platform.multisig()); + IVault(strategy.vault()).doHardWork(); + } + states[3] = _getState(); + + // --------------------------------------------- Withdraw + _tryToWithdrawFromVault(strategy.vault(), states[1].vaultBalance - states[0].vaultBalance); + vm.roll(block.number + 6); + states[4] = _getState(); + vm.revertToState(snapshot); + + assertLt(states[0].total, states[1].total, "Total should increase after deposit"); + assertEq(depositedAssets, amount, "Deposited amount should be equal to amountsToDeposit"); + } + + function _testDepositWithDirectRepay(uint[2] memory amounts) + internal + returns (State memory state0, State memory state1, State memory state2) + { + address vault = IStrategy(currentStrategy).vault(); + + // ---------- Special case: 100% direct repay is required on the second deposit + _tryToDepositToVault(vault, amounts[0], REVERT_NO, address(this)); + state0 = _getState(); + // _printState(state0); + + _setMinMaxLtv(4100, 4200); + _tryToDepositToVault(vault, amounts[1], REVERT_NO, address(this)); + state1 = _getState(); + // _printState(state1); + + // ---------- Withdraw all + _tryToWithdrawFromVault(vault, IStabilityVault(vault).balanceOf(address(this))); + state2 = _getState(); + + return (state0, state1, state2); + } + + //endregion --------------------------------------- Test implementations + + //region --------------------------------------- Internal logic + function _currentFarmId() internal view returns (uint) { + return IFarmingStrategy(currentStrategy).farmId(); + } + + function _tryToDepositToVault( + address vault, + uint amount, + uint revertKind, + address user + ) internal returns (uint deposited, uint depositedValue) { + address[] memory assets = IVault(vault).assets(); + uint[] memory amountsToDeposit = new uint[](1); + amountsToDeposit[0] = amount; + + // ----------------------------- Prepare amount on user's balance + _dealAndApprove(user, vault, assets, amountsToDeposit); + // console.log("Deposit to vault", assets[0], amounts_[0]); + + uint balanceBefore = IVault(vault).balanceOf(user); + // ----------------------------- Try to deposit assets to the vault + // todo + // if (revertKind == REVERT_NOT_ENOUGH_LIQUIDITY) { + // vm.expectRevert(ISilo.NotEnoughLiquidity.selector); + // } + if (revertKind == REVERT_INSUFFICIENT_BALANCE) { + vm.expectRevert(IControllable.InsufficientBalance.selector); + } + vm.prank(user); + IStabilityVault(vault).depositAssets(assets, amountsToDeposit, 0, user); + + vm.roll(block.number + 6); + + return (amountsToDeposit[0], IVault(vault).balanceOf(user) - balanceBefore); + } + + function _tryToWithdrawFromVault(address vault, uint values) internal returns (uint withdrawn) { + address[] memory _assets = IVault(vault).assets(); + + uint balanceBefore = IERC20(_assets[0]).balanceOf(address(this)); + + vm.prank(address(this)); + IStabilityVault(vault).withdrawAssets(_assets, values, new uint[](1)); + + vm.roll(block.number + 6); + + return IERC20(_assets[0]).balanceOf(address(this)) - balanceBefore; + } + + function _dealAndApprove(address user, address spender, address[] memory assets, uint[] memory amounts) internal { + for (uint j; j < assets.length; ++j) { + deal(assets[j], user, amounts[j]); + + vm.prank(user); + IERC20(assets[j]).approve(spender, amounts[j]); + } + } + + /// @param depositParam0 - Multiplier of flash amount for borrow on deposit. + /// @param depositParam1 - Multiplier of borrow amount to take into account max flash loan fee in maxDeposit + function _setDepositParams(uint depositParam0, uint depositParam1) internal { + ILeverageLendingStrategy strategy = ILeverageLendingStrategy(currentStrategy); + (uint[] memory params, address[] memory addresses) = strategy.getUniversalParams(); + + params[0] = depositParam0; + params[1] = depositParam1; + + vm.prank(platform.multisig()); + strategy.setUniversalParams(params, addresses); + } + + /// @param withdrawParam0 - Multiplier of flash amount for borrow on withdraw. + /// @param withdrawParam1 - Multiplier of amount allowed to be deposited after withdraw. Default is 100_00 == 100% (deposit forbidden) + /// @param withdrawParam2 - allows to disable withdraw through increasing ltv if leverage is near to target + function _setWithdrawParams(uint withdrawParam0, uint withdrawParam1, uint withdrawParam2) internal { + ILeverageLendingStrategy strategy = ILeverageLendingStrategy(currentStrategy); + (uint[] memory params, address[] memory addresses) = strategy.getUniversalParams(); + + params[2] = withdrawParam0; + params[3] = withdrawParam1; + params[11] = withdrawParam2; + + vm.prank(platform.multisig()); + strategy.setUniversalParams(params, addresses); + } + + function _getState() internal view returns (State memory state) { + ILeverageLendingStrategy strategy = ILeverageLendingStrategy(address(currentStrategy)); + + (state.sharePrice,) = strategy.realSharePrice(); + + ( + state.ltv, + state.maxLtv, + state.leverage, + state.collateralAmount, + state.debtAmount, + state.targetLeveragePercent + ) = strategy.health(); + + state.total = IStrategy(currentStrategy).total(); + state.maxLeverage = 100_00 * 1e4 / (1e4 - state.maxLtv); + state.targetLeverage = state.maxLeverage * state.targetLeveragePercent / 100_00; + state.strategyBalanceCollateralAsset = + IERC20(IStrategy(address(strategy)).assets()[0]).balanceOf(address(currentStrategy)); + state.strategyBalanceBorrowAsset = IERC20(SonicConstantsLib.TOKEN_USDC).balanceOf(address(currentStrategy)); + state.userBalanceAsset = IERC20(IStrategy(address(strategy)).assets()[0]).balanceOf(address(address(this))); + (state.realTvl,) = strategy.realTvl(); + (state.realSharePrice,) = strategy.realSharePrice(); + state.vaultBalance = IVault(IStrategy(address(strategy)).vault()).balanceOf(address(this)); + (state.revenueAssets, state.revenueAmounts) = IStrategy(currentStrategy).getRevenue(); + + // _printState(state); + return state; + } + + function _printState(State memory state) internal pure { + console.log("state **************************************************"); + console.log("ltv", state.ltv); + console.log("maxLtv", state.maxLtv); + console.log("targetLeverage", state.targetLeverage); + console.log("leverage", state.leverage); + console.log("total", state.total); + console.log("collateralAmount", state.collateralAmount); + console.log("debtAmount", state.debtAmount); + console.log("targetLeveragePercent", state.targetLeveragePercent); + console.log("maxLeverage", state.maxLeverage); + console.log("realTvl", state.realTvl); + console.log("realSharePrice", state.realSharePrice); + console.log("vaultBalance", state.vaultBalance); + console.log("strategyBalanceCollateralAsset", state.strategyBalanceCollateralAsset); + console.log("strategyBalanceBorrowAsset", state.strategyBalanceBorrowAsset); + console.log("userBalanceAsset", state.userBalanceAsset); + for (uint i = 0; i < state.revenueAssets.length; i++) { + console.log("revenueAsset", i, state.revenueAssets[i], state.revenueAmounts[i]); + } + } + + function _setMinMaxLtv(uint minLtv, uint maxLtv) internal { + IFarmingStrategy strategy = IFarmingStrategy(currentStrategy); + uint farmId = strategy.farmId(); + IFactory factory = IFactory(IPlatform(IControllable(currentStrategy).platform()).factory()); + + IFactory.Farm memory farm = factory.farm(farmId); + farm.nums[0] = minLtv; + farm.nums[1] = maxLtv; + + vm.prank(platform.multisig()); + factory.updateFarm(farmId, farm); + } + + //endregion --------------------------------------- Internal logic + + //region --------------------------------------- Helper functions + function _upgradePlatform(address multisig, address priceReader_) internal { + // we need to skip 1 day to update the swapper + // but we cannot simply skip 1 day, because the silo oracle will start to revert with InvalidPrice + // vm.warp(block.timestamp - 86400); + rewind(86400); + + IPlatform platform = IPlatform(IControllable(priceReader_).platform()); + + address[] memory proxies = new address[](1); + address[] memory implementations = new address[](1); + + proxies[0] = address(priceReader_); + //proxies[1] = platform.swapper(); + //proxies[2] = platform.ammAdapter(keccak256(bytes(AmmAdapterIdLib.META_VAULT))).proxy; + + implementations[0] = address(new PriceReader()); + //implementations[1] = address(new Swapper()); + //implementations[2] = address(new MetaVaultAdapter()); + + //vm.prank(multisig); + // platform.cancelUpgrade(); + + vm.startPrank(multisig); + platform.announcePlatformUpgrade("2025.07.22-alpha", proxies, implementations); + + skip(1 days); + platform.upgrade(); + vm.stopPrank(); + } + + function _setUpFlashLoanVault(address flashLoanVault, ILeverageLendingStrategy.FlashLoanKind kind_) internal { + _setFlashLoanVault(ILeverageLendingStrategy(currentStrategy), flashLoanVault, uint(kind_)); + } + + function _setFlashLoanVault(ILeverageLendingStrategy strategy, address flashLoanVault, uint kind) internal { + address multisig = platform.multisig(); + + (uint[] memory params, address[] memory addresses) = strategy.getUniversalParams(); + params[10] = kind; + addresses[0] = flashLoanVault; + + vm.prank(multisig); + strategy.setUniversalParams(params, addresses); + } + + function _getWethPrice8() internal view returns (uint) { + return IAavePriceOracle(IAaveAddressProvider(IPool(POOL).ADDRESSES_PROVIDER()).getPriceOracle()) + .getAssetPrice(SonicConstantsLib.TOKEN_WETH); + } + + //endregion --------------------------------------- Helper functions +} diff --git a/test/strategies/ALMF.Upgrade.431.Plasma.t.sol b/test/strategies/ALMF.Upgrade.431.Plasma.t.sol new file mode 100644 index 000000000..10a86c91a --- /dev/null +++ b/test/strategies/ALMF.Upgrade.431.Plasma.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {AaveLeverageMerklFarmStrategy} from "../../src/strategies/AaveLeverageMerklFarmStrategy.sol"; +import {Test} from "forge-std/Test.sol"; +import {PlasmaConstantsLib} from "../../chains/plasma/PlasmaConstantsLib.sol"; +import {IFactory} from "../../src/interfaces/IFactory.sol"; +import {IStrategy} from "../../src/interfaces/IStrategy.sol"; +import {IPlatform} from "../../src/interfaces/IPlatform.sol"; +import {StrategyIdLib} from "../../src/strategies/libs/StrategyIdLib.sol"; + +contract ALMFStrategyUpdate431PlasmaTest is Test { + uint internal constant FORK_BLOCK = 7733125; // Dec-2-2025 08:23:16 UTC + IFactory internal factory; + + /// @notice weeth-usdt x2 + address public constant ALMF_STRATEGY = 0x711119916bdF5edD4244b2d0462a0CEf16D2411f; + + constructor() { + vm.selectFork(vm.createFork(vm.envString("PLASMA_RPC_URL"), FORK_BLOCK)); + factory = IFactory(IPlatform(PlasmaConstantsLib.PLATFORM).factory()); + } + + /// @notice Coverage: ensure that exchange asset index is updated inside _beforeHardwork + function testHardworkAfterUpdate() public { + _upgradeStrategy(); + + // emulate Merkl rewards + deal(PlasmaConstantsLib.TOKEN_WXPL, ALMF_STRATEGY, 1e18); + skip(1 days); + + address vault = IStrategy(ALMF_STRATEGY).vault(); + + uint totalBefore = IStrategy(ALMF_STRATEGY).total(); + + vm.prank(address(vault)); + IStrategy(ALMF_STRATEGY).doHardWork(); + + uint totalAfter = IStrategy(ALMF_STRATEGY).total(); + assertGt(totalAfter, totalBefore, "total after hardwork should be greater than before"); + } + + function _upgradeStrategy() internal { + address strategyImplementation = address(new AaveLeverageMerklFarmStrategy()); + + vm.prank(PlasmaConstantsLib.MULTISIG); + factory.setStrategyImplementation(StrategyIdLib.AAVE_LEVERAGE_MERKL_FARM, strategyImplementation); + + factory.upgradeStrategyProxy(address(ALMF_STRATEGY)); + } +} diff --git a/test/strategies/libs/ALMFCalcLib.t.sol b/test/strategies/libs/ALMFCalcLib.t.sol new file mode 100644 index 000000000..9270ea02b --- /dev/null +++ b/test/strategies/libs/ALMFCalcLib.t.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {ALMFCalcLib} from "../../../src/strategies/libs/ALMFCalcLib.sol"; +import {Test} from "forge-std/Test.sol"; + +contract ALMFCalcLibTest is Test { + uint internal constant FORK_BLOCK = 55065335; // Nov-13-2025 03:53:58 AM +UTC + + constructor() { + vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"), FORK_BLOCK)); + } + + function testSplitDepositAmount() public pure { + (uint aD, uint aR) = ALMFCalcLib.splitDepositAmount(400, 20000, 1000, 550, 0.015e18); + assertEq(aD, 400, "1.ad"); + assertEq(aR, 0, "1.ar"); + + (aD, aR) = ALMFCalcLib.splitDepositAmount(400e2, 20000, 1000e2, 800e2, 0.015e18); + assertEq(aD, 193.82e2, "2.ad"); + assertEq(aR, 206.18e2, "2.ar"); + + (aD, aR) = ALMFCalcLib.splitDepositAmount(400e2, 20000, 1000e2, 900e2, 0.015e18); + assertEq(aD, 0, "3.ad"); + assertEq(aR, 400e2, "3.ar"); + + (aD, aR) = ALMFCalcLib.splitDepositAmount(400e18, 20000, 1000e18, 810e18, 0); + assertEq(aD, 180e18, "4.ad"); + assertEq(aR, 220e18, "4.ar"); + } + + function testCalcWithdrawAmountsUnitPrices() public pure { + ALMFCalcLib.StaticData memory data; + data.decimalsC = 18; + data.decimalsB = 6; + data.priceC18 = 1e18; + data.priceB18 = 1e18; + + (uint flashAmount, uint collateralToWithdraw) = + ALMFCalcLib.calcWithdrawAmounts(200e18, 32700, data, state(1000e18, 700e18)); + assertApproxEqRel(flashAmount, 473.33e6, 1e18 / 100, "1.F"); + assertApproxEqRel(collateralToWithdraw, 673.33e18, 1e18 / 100, "1.C1"); + + (flashAmount, collateralToWithdraw) = + ALMFCalcLib.calcWithdrawAmounts(200e18, 14571, data, state(1000e18, 300e18)); + assertApproxEqRel(flashAmount, 71.43e6, 1e18 / 100, "2.F"); + assertApproxEqRel(collateralToWithdraw, 271.43e18, 1e18 / 100, "2.C1"); + + (flashAmount, collateralToWithdraw) = + ALMFCalcLib.calcWithdrawAmounts(0.0001e18, 14571, data, state(1000e18, 300e18)); + assertEq(flashAmount, 0, "3.F"); + assertEq(collateralToWithdraw, 0.0001e18, "3.C1"); + + (flashAmount, collateralToWithdraw) = + ALMFCalcLib.calcWithdrawAmounts(700e18, 14571, data, state(1000e18, 300e18)); + assertApproxEqRel(flashAmount, 300.0e6, 1e18 / 100, "4.F"); + assertApproxEqRel(collateralToWithdraw, 1000e18, 1e18 / 100, "4.C1"); + + (flashAmount, collateralToWithdraw) = + ALMFCalcLib.calcWithdrawAmounts(99.99e18, 96000, data, state(1000e18, 900e18)); + assertApproxEqRel(flashAmount, 899.91e6, 1e18 / 100, "5.F"); + assertApproxEqRel(collateralToWithdraw, 999.9e18, 1e18 / 100, "5.C1"); + + // ----------------- negative F + (flashAmount, collateralToWithdraw) = + ALMFCalcLib.calcWithdrawAmounts(100e18, 19400, data, state(1000e18, 200e18)); + assertEq(flashAmount, 0, "6.F"); + assertEq(collateralToWithdraw, 100e18, "6.C1"); + + // ----------------- zero F + (flashAmount, collateralToWithdraw) = + ALMFCalcLib.calcWithdrawAmounts(100e18, 20000, data, state(1000e18, 450e18)); + assertEq(flashAmount, 0, "6.F"); + assertEq(collateralToWithdraw, 100e18, "6.C1"); + + // ----------------- very large debt + (flashAmount, collateralToWithdraw) = + ALMFCalcLib.calcWithdrawAmounts(100e18, 20000, data, state(1000e18, 900e18)); + assertEq(flashAmount, 900e6, "6.F"); + assertEq(collateralToWithdraw, 1000e18, "6.C1"); + } + + function testGetLimitedAmount() public pure { + // optionalLimit == 0 -> returns full amount + assertEq(ALMFCalcLib.getLimitedAmount(100, 0), 100, "limit0 returns amount"); + + // optionalLimit greater than amount -> returns amount + assertEq(ALMFCalcLib.getLimitedAmount(100, 200), 100, "limit>amount returns amount"); + + // optionalLimit less than amount -> returns optionalLimit + assertEq(ALMFCalcLib.getLimitedAmount(100, 50), 50, "limit leverage = 1000*10000/(1000-500) = 20000 + assertEq(ALMFCalcLib.getLeverage(1000, 500), 20000, "getLeverage basic"); + // zero collateral -> 0 + assertEq(ALMFCalcLib.getLeverage(0, 0), 0, "getLeverage zero collateral"); + // collateral 1000, debt 800 -> 1000*10000/(200) = 50000 + assertEq(ALMFCalcLib.getLeverage(1000, 800), 50000, "getLeverage high debt"); + + // getLtv + // collateral 1000, debt 500 -> ltv = 500*10000/1000 = 5000 + assertEq(ALMFCalcLib.getLtv(1000, 500), 5000, "getLtv basic"); + // zero collateral -> 0 + assertEq(ALMFCalcLib.getLtv(0, 100), 0, "getLtv zero collateral"); + + // leverageToLtv + // leverage 20000 -> ltv = 10000 - 10000*10000/20000 = 5000 + assertEq(ALMFCalcLib.leverageToLtv(20000), 5000, "leverageToLtv basic"); + // leverage equal to INTERNAL_PRECISION (10000) -> 0 + assertEq(ALMFCalcLib.leverageToLtv(10000), 0, "leverageToLtv <= INTERNAL_PRECISION"); + + // ltvToLeverage + // ltv 5000 -> leverage = 10000*10000/(10000-5000) = 20000 + assertEq(ALMFCalcLib.ltvToLeverage(5000), 20000, "ltvToLeverage basic"); + } + + function testBaseConversions() public pure { + ALMFCalcLib.StaticData memory data; + + // collateralToBase / baseToCollateral with decimalsC = 6 and priceC18 = 2e18 + data.decimalsC = 6; + data.priceC18 = 2e18; + + // 1 token (1e6 with 6 decimals) -> base = 1e6 * 2e18 / 1e6 = 2e18 + assertEq(ALMFCalcLib.collateralToBase(1e6, data), 2e18, "collateralToBase basic"); + // inverse: base 2e18 -> token = 2e18 * 1e6 / 2e18 = 1e6 + assertEq(ALMFCalcLib.baseToCollateral(2e18, data), 1e6, "baseToCollateral basic"); + + // borrowToBase / baseToBorrow with decimalsB = 8 and priceB18 = 5e18 + data.decimalsB = 8; + data.priceB18 = 5e18; + + // 3 tokens (3e8 with 8 decimals) -> base = 3e8 * 5e18 / 1e8 = 15e18 + assertEq(ALMFCalcLib.borrowToBase(3e8, data), 15e18, "borrowToBase basic"); + // inverse: base 15e18 -> token = 15e18 * 1e8 / 5e18 = 3e8 + assertEq(ALMFCalcLib.baseToBorrow(15e18, data), 3e8, "baseToBorrow basic"); + + // edge cases: zero + data.decimalsC = 18; + data.priceC18 = 1e18; + assertEq(ALMFCalcLib.collateralToBase(0, data), 0, "collateralToBase zero"); + assertEq(ALMFCalcLib.baseToCollateral(0, data), 0, "baseToCollateral zero"); + } + + function testCollateralBaseRounding() public pure { + ALMFCalcLib.StaticData memory data; + + // Case A: decimalsC = 6, price slightly above 1e18 to force rounding loss for tiny amounts + data.decimalsC = 6; + data.priceC18 = 1e18 + 1; + + // smallest unit amount = 1 -> base = floor((1 * (1e18+1)) / 1e6) = 1e12 + uint base1 = ALMFCalcLib.collateralToBase(1, data); + assertEq(base1, 1e12, "base1 expected"); + + // converting back loses precision: floor((1e12 * 1e6) / (1e18+1)) = 0 + uint recovered1 = ALMFCalcLib.baseToCollateral(base1, data); + assertEq(recovered1, 0, "recovered1 expected 0 due to rounding"); + + // Case B: full token equal to 1e6 (with decimals 6) should be invertible + uint amountToken = 1e6; // 1.0 token in 6 decimals + uint base2 = ALMFCalcLib.collateralToBase(amountToken, data); + assertEq(base2, 1e18 + 1, "base2 expected exact price * amount"); + + uint recovered2 = ALMFCalcLib.baseToCollateral(base2, data); + assertEq(recovered2, amountToken, "recovered2 should equal original amountToken"); + + // sanity property: recovered <= original for any amount (rounding down) + uint someAmount = 999999; + uint b = ALMFCalcLib.collateralToBase(someAmount, data); + uint r = ALMFCalcLib.baseToCollateral(b, data); + assertTrue(r <= someAmount, "round-trip recovered <= original"); + } + + // solidity + function testAdjustLeverage() public pure { + // inside range -> no change + uint res = ALMFCalcLib.adjustLeverage(10000, 9000, 11000, 2000); + assertEq(res, 10000, "1.within range"); + + // below min -> increase by k * (target - leverage) / INTERNAL_PRECISION + // target = (9000 + 11000) / 2 = 10000 + // diff = 10000 - 8000 = 2000 + // add = 5000 * 2000 / 10000 = 1000 + // result = 8000 + 1000 = 9000 + res = ALMFCalcLib.adjustLeverage(8000, 9000, 11000, 5000); + assertEq(res, 9000, "2.below min"); + + // above max -> decrease by k * (leverage - target) / INTERNAL_PRECISION + // target = 10000, diff = 12000 - 10000 = 2000, sub = 5000 * 2000 / 10000 = 1000 + // result = 12000 - 1000 = 11000 + res = ALMFCalcLib.adjustLeverage(12000, 9000, 11000, 5000); + assertEq(res, 11000, "3.above max"); + + // k = 0 -> no adjustment even when outside range + res = ALMFCalcLib.adjustLeverage(7000, 9000, 11000, 0); + assertEq(res, 7000, "4.k zero"); + + // fractional division (flooring) case + // target = 10000, diff = 10000 - 8001 = 1999 + // add = floor(3333 * 1999 / 10000) = 666 + // result = 8001 + 666 = 8667 + res = ALMFCalcLib.adjustLeverage(8001, 9000, 11000, 3333); + assertEq(res, 8667, "5.fractional"); + } + + //region -------------------------------------- Internal logic + function state(uint collateralBase, uint debtBase) internal pure returns (ALMFCalcLib.State memory) { + ALMFCalcLib.State memory _state; + _state.collateralBase = collateralBase; + _state.debtBase = debtBase; + return _state; + } + + //endregion -------------------------------------- Internal logic +} diff --git a/test/strategies/libs/LeverageLendingLib.t.sol b/test/strategies/libs/LeverageLendingLib.t.sol new file mode 100644 index 000000000..00d8cbc05 --- /dev/null +++ b/test/strategies/libs/LeverageLendingLib.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Test} from "forge-std/Test.sol"; +import {ILeverageLendingStrategy} from "../../../src/interfaces/ILeverageLendingStrategy.sol"; +import {LeverageLendingLib} from "../../../src/strategies/libs/LeverageLendingLib.sol"; +import {SonicConstantsLib} from "../../../chains/sonic/SonicConstantsLib.sol"; + +contract LeverageLendingLibTests is Test { + uint internal constant FORK_BLOCK = 55065335; // Nov-13-2025 03:53:58 AM +UTC + + constructor() { + vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"), FORK_BLOCK)); + } + + function testGetFlashFee18() public view { + // ------------------------------------- BalancerV2 + assertEq( + LeverageLendingLib.getFlashFee18( + SonicConstantsLib.BEETS_VAULT, uint(ILeverageLendingStrategy.FlashLoanKind.Default_0) + ), + 300000000000000, // 0.0003 = 0.03% + "beets v2" + ); + + // ------------------------------------- BalancerV3_1 + assertEq( + LeverageLendingLib.getFlashFee18( + SonicConstantsLib.BEETS_VAULT_V3, uint(ILeverageLendingStrategy.FlashLoanKind.BalancerV3_1) + ), + 0, + "beets v3 flash fee" + ); + + // ------------------------------------- UniswapV3_2 + assertEq( + LeverageLendingLib.getFlashFee18( + SonicConstantsLib.POOL_SHADOW_CL_USDC_WETH, uint(ILeverageLendingStrategy.FlashLoanKind.UniswapV3_2) + ), + 1658 * 1e12, // 0.0001658 = 0.01658% + "uniswap-v3 flash fee" + ); + + // ------------------------------------- AlgebraV4_3 + assertEq( + LeverageLendingLib.getFlashFee18( + SonicConstantsLib.POOL_ALGEBRA_WS_USDC, uint(ILeverageLendingStrategy.FlashLoanKind.AlgebraV4_3) + ), + 5000 * 1e12, // 0.0005 = 0.05% + "algebra-v4 flash fee" + ); + } +} diff --git a/test/tokenomics/Recovery.Upgrade.Routes.For.Metavaults.t.sol b/test/tokenomics/Recovery.Upgrade.Routes.For.Metavaults.t.sol index 72629a7f7..a446f597d 100644 --- a/test/tokenomics/Recovery.Upgrade.Routes.For.Metavaults.t.sol +++ b/test/tokenomics/Recovery.Upgrade.Routes.For.Metavaults.t.sol @@ -26,8 +26,7 @@ contract SwapperUpgradeRoutesForMetaVaultsSonicTest is Test { // wstkscETH function testUpgradeRoutesForWans() public { - vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"))); - vm.rollFork(49150536); // Oct-03-2025 12:00:30 AM UTC + vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"), 49150536)); // Oct-03-2025 12:00:30 AM UTC address multisig = IPlatform(PLATFORM).multisig(); IRecovery recovery = IRecovery(IPlatform(PLATFORM).recovery()); @@ -43,8 +42,7 @@ contract SwapperUpgradeRoutesForMetaVaultsSonicTest is Test { } function testUpgradeRoutesForWstkscUSD() public { - vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"))); - vm.rollFork(49150536); // Oct-03-2025 12:00:30 AM UTC + vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"), 49150536)); // Oct-03-2025 12:00:30 AM UTC address multisig = IPlatform(PLATFORM).multisig(); IRecovery recovery = IRecovery(IPlatform(PLATFORM).recovery()); @@ -60,8 +58,7 @@ contract SwapperUpgradeRoutesForMetaVaultsSonicTest is Test { } function testUpgradeRoutesForWstkscETH() public { - vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"))); - vm.rollFork(49150536); // Oct-03-2025 12:00:30 AM UTC + vm.selectFork(vm.createFork(vm.envString("SONIC_RPC_URL"), 49150536)); // Oct-03-2025 12:00:30 AM UTC address multisig = IPlatform(PLATFORM).multisig(); IRecovery recovery = IRecovery(IPlatform(PLATFORM).recovery());