From cf98bfbbb40bbe3e6b9ae5a43f826dd490e4a592 Mon Sep 17 00:00:00 2001 From: omriss Date: Mon, 9 Feb 2026 14:32:54 +0700 Subject: [PATCH 1/3] #441: ALMF - depositParam1 and withdrawParam1 are allowed to set deposit/withdraw fee percent (decimals 1e4) --- chains/plasma/PlasmaConstantsLib.sol | 3 + .../AaveLeverageMerklFarmStrategy.sol | 3 +- src/strategies/libs/ALMFLib.sol | 55 +++++++- src/strategies/libs/ALMFLib2.sol | 10 +- test/strategies/ALMF.Plasma.Upgrade.441.t.sol | 131 ++++++++++++++++++ 5 files changed, 190 insertions(+), 12 deletions(-) create mode 100644 test/strategies/ALMF.Plasma.Upgrade.441.t.sol diff --git a/chains/plasma/PlasmaConstantsLib.sol b/chains/plasma/PlasmaConstantsLib.sol index f908eb87..c01b3410 100644 --- a/chains/plasma/PlasmaConstantsLib.sol +++ b/chains/plasma/PlasmaConstantsLib.sol @@ -17,6 +17,9 @@ library PlasmaConstantsLib { address public constant TOKEN_EUL = 0xca632FA58397391C750c13F935DAA61AbBe0BaA6; address public constant TOKEN_REUL = 0xe2011F2bF6556863c3bacE991Efc8DaC26CD84c2; + // Stability vaults + address public constant STABILITY_VAULT_ALMF_WEETH_USDT0 = 0xab0087D6fbC877246A4Ba33636f80E5dCbd5BE01; + // Oracles // address public constant ORACLE_CHAINLINK_USDT0_USD = 0xdBbB0b5DD13E7AC9C56624834ef193df87b022c3; address public constant ORACLE_CHAINLINK_USDT0_USD = 0x70b77FcdbE2293423e41AdD2FB599808396807BC; diff --git a/src/strategies/AaveLeverageMerklFarmStrategy.sol b/src/strategies/AaveLeverageMerklFarmStrategy.sol index c7da7d8a..459c67ca 100644 --- a/src/strategies/AaveLeverageMerklFarmStrategy.sol +++ b/src/strategies/AaveLeverageMerklFarmStrategy.sol @@ -27,6 +27,7 @@ import {VaultTypeLib} from "../core/libs/VaultTypeLib.sol"; /// @title Earns APR by lending assets on AAVE with leverage /// @dev ALMF strategy /// Changelog: +/// 1.4.0: depositParam1 and withdrawParam1 are used to set deposit/withdraw fee % (decimals 1e4) - #441 /// 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 @@ -47,7 +48,7 @@ contract AaveLeverageMerklFarmStrategy is /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ /// @inheritdoc IControllable - string public constant VERSION = "1.3.0"; + string public constant VERSION = "1.4.0"; //region ----------------------------------- Initialization and restricted actions /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ diff --git a/src/strategies/libs/ALMFLib.sol b/src/strategies/libs/ALMFLib.sol index e527bf14..cda4f4da 100644 --- a/src/strategies/libs/ALMFLib.sol +++ b/src/strategies/libs/ALMFLib.sol @@ -18,6 +18,7 @@ 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 {IRevenueRouter} from "../../interfaces/IRevenueRouter.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"; @@ -305,6 +306,9 @@ library ALMFLib { ALMFCalcLib.StaticData memory data = _getStaticData(platform_, $, farm); ALMFCalcLib.State memory state = _getState(data); + /// @dev Reduce input amount on deposit fee, send fee amount to revenue router + amount = _takeFee($, amount, true, data.collateralAsset, platform_); + uint valueWas = ALMFCalcLib.collateralToBase(StrategyLib.balance(data.collateralAsset), data) + calcTotal(state); uint threshold = _getStorage().thresholds[data.collateralAsset]; @@ -451,19 +455,24 @@ library ALMFLib { } // ---------------------- Transfer required amount to the user - uint balance = StrategyLib.balance(data.collateralAsset); - uint valueNow = ALMFCalcLib.collateralToBase(balance, data) + calcTotal(state); + { + 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); + 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)) { + /// @dev Reduce output amount on withdraw fee, send fee amount to revenue router + amountsOut[0] = _takeFee($, amountsOut[0], false, data.collateralAsset, platform); + IERC20(data.collateralAsset).safeTransfer(receiver, amountsOut[0]); } @@ -1001,6 +1010,38 @@ library ALMFLib { //endregion ------------------------------------- Additional functionality //region ------------------------------------- Internal utils + /// @dev Take deposit/withdraw fee from amount, send fee to revenue router and return amount after fee deduction. + /// Fee percent should be set in depositParam1/withdrawParam1 with precision 1e4. + /// @param amount Amount of collateral asset to take fee from + /// @param isDeposit If true, deposit fee is taken; otherwise, withdraw fee is taken. + /// @param collateralAsset_ Collateral asset address, needed for fee transfer + /// @return amountOut Amount after fee deduction. If fee is 0, amountOut is equal to amount. + function _takeFee( + ILeverageLendingStrategy.LeverageLendingBaseStorage storage $, + uint amount, + bool isDeposit, + address collateralAsset_, + address platform_ + ) internal returns (uint amountOut) { + amountOut = amount; + + /// @dev Fee percent with precision 1e4, should be set in depositParam1/withdrawParam1 + uint feePercent = isDeposit ? $.depositParam1 : $.withdrawParam1; + + /// @dev Fee amount in collateral asset + uint feeAmount = amount * feePercent / ALMFCalcLib.INTERNAL_PRECISION; + + // assume below that feePercent < ALMFCalcLib.INTERNAL_PRECISION and so amount > feeAmount always + + /// @dev Take a fee in the same way as doHardwork does + if (feeAmount != 0) { + amountOut = amount - feeAmount; + address revenueRouter = IPlatform(platform_).revenueRouter(); + IERC20(collateralAsset_).forceApprove(revenueRouter, feeAmount); + IRevenueRouter(revenueRouter).processFeeAsset(collateralAsset_, feeAmount); + } + } + function _getFlashLoanAmounts( uint borrowAmount, address borrowAsset diff --git a/src/strategies/libs/ALMFLib2.sol b/src/strategies/libs/ALMFLib2.sol index ba073525..af7c6337 100644 --- a/src/strategies/libs/ALMFLib2.sol +++ b/src/strategies/libs/ALMFLib2.sol @@ -115,8 +115,9 @@ library ALMFLib2 { // ------------------------------ 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; + + // Deposit fee, percent, 1e4; i.e. 0001 = 0.01% + // $.depositParam1 = 0; // Multiplier of debt diff $.increaseLtvParam0 = 100_80; @@ -132,8 +133,9 @@ library ALMFLib2 { // 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; + // Withdraw fee, percent, 1e4; i.e. 0001 = 0.01% + // $.withdrawParam1 = 0; + // // withdrawParam2 allows to disable withdraw through increasing ltv if leverage is near to target // $.withdrawParam2 = 100_00; diff --git a/test/strategies/ALMF.Plasma.Upgrade.441.t.sol b/test/strategies/ALMF.Plasma.Upgrade.441.t.sol new file mode 100644 index 00000000..76cb5634 --- /dev/null +++ b/test/strategies/ALMF.Plasma.Upgrade.441.t.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {console} from "forge-std/console.sol"; +import {Test, Vm} 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 {ILeverageLendingStrategy} from "../../src/interfaces/ILeverageLendingStrategy.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 {ALMFLib} from "../../src/strategies/libs/ALMFLib.sol"; + +/// @notice #441: Use depositParam1 and withdrawParam1 to set deposit/withdraw fee +contract ALMFUpgrade441PlasmaTest is Test { + uint internal constant FORK_BLOCK = 13688033; // Feb-09-2026 06:49:49 AM +UTC + + /// @notice Stability weETH Aave Leverage Merkl Farm USDT0 + address internal constant VAULT = PlasmaConstantsLib.STABILITY_VAULT_ALMF_WEETH_USDT0; + + address internal multisig; + + constructor() { + vm.selectFork(vm.createFork(vm.envString("PLASMA_RPC_URL"), FORK_BLOCK)); + + _upgradeStrategy(address(IVault(VAULT).strategy())); + } + + function testUpgradeStrategy() public { + uint[4] memory depositParam1 = [uint(0), 10, 0, 10]; // 0.1% for deposit + uint[4] memory withdrawParam1 = [uint(0), 0, 20, 20]; // 0.2 % for withdraw + uint[4] memory withdrawn; + uint[4] memory receivedByRevenueRouter; + + // --------------------------------- zero fee + + for (uint i; i < 4; ++i) { + uint snapshot = vm.snapshotState(); + { + ILeverageLendingStrategy strategy = ILeverageLendingStrategy(address(IVault(VAULT).strategy())); + (uint[] memory params, address[] memory addresses) = strategy.getUniversalParams(); + params[1] = depositParam1[i]; + params[3] = withdrawParam1[i]; + vm.prank(PlasmaConstantsLib.MULTISIG); + strategy.setUniversalParams(params, addresses); + } + + uint before = IERC20(PlasmaConstantsLib.TOKEN_WEETH) + .balanceOf(IPlatform(PlasmaConstantsLib.PLATFORM).revenueRouter()); + + // deposit 1 weeth + _depositToVault(VAULT, 1e18, address(this)); + + vm.roll(block.number + 6); + + // withdraw all + withdrawn[i] = _withdrawFromVault(VAULT, IVault(VAULT).balanceOf(address(this)), address(this)); + + receivedByRevenueRouter[i] = IERC20(PlasmaConstantsLib.TOKEN_WEETH) + .balanceOf(IPlatform(PlasmaConstantsLib.PLATFORM).revenueRouter()) - before; + vm.revertToState(snapshot); + } + + assertApproxEqAbs(withdrawn[0], withdrawn[1], 0.001e18, "~0.1% for deposit"); + assertApproxEqAbs(withdrawn[0], withdrawn[2], 0.002e18, "~0.2% for withdraw"); + assertApproxEqAbs(withdrawn[0], withdrawn[3], 0.003e18, "~0.3% for deposit and withdraw"); + + assertEq(receivedByRevenueRouter[0], 0, "no fees for deposit and withdraw"); + assertEq(receivedByRevenueRouter[1], 0.001e18, "0.1% for deposit"); + assertApproxEqAbs(receivedByRevenueRouter[2], 0.002e18, 1e14, "0.2% for withdraw"); + assertApproxEqAbs(receivedByRevenueRouter[3], 0.003e18, 1e14, "0.3% for deposit and withdraw"); + } + + function _depositToVault( + 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 _withdrawFromVault(address vault, uint value, address user) internal returns (uint withdrawn) { + address[] memory _assets = IVault(vault).assets(); + + uint balanceBefore = IERC20(_assets[0]).balanceOf(user); + + vm.prank(user); + IStabilityVault(vault).withdrawAssets(_assets, value, new uint[](1)); + + return IERC20(_assets[0]).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); + } +} From 70f39a56e3a36ce11ef20e105c872f492368455f Mon Sep 17 00:00:00 2001 From: omriss Date: Mon, 9 Feb 2026 15:52:51 +0700 Subject: [PATCH 2/3] remove dependencies --- test/strategies/ALMF.Plasma.Upgrade.441.t.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/strategies/ALMF.Plasma.Upgrade.441.t.sol b/test/strategies/ALMF.Plasma.Upgrade.441.t.sol index 76cb5634..032f3884 100644 --- a/test/strategies/ALMF.Plasma.Upgrade.441.t.sol +++ b/test/strategies/ALMF.Plasma.Upgrade.441.t.sol @@ -1,20 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {console} from "forge-std/console.sol"; -import {Test, Vm} from "forge-std/Test.sol"; +import {Test} 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 {ILeverageLendingStrategy} from "../../src/interfaces/ILeverageLendingStrategy.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 {ALMFLib} from "../../src/strategies/libs/ALMFLib.sol"; /// @notice #441: Use depositParam1 and withdrawParam1 to set deposit/withdraw fee contract ALMFUpgrade441PlasmaTest is Test { From 3fa95bd957742e274cec50dd7213b7b3d0b3b5ee Mon Sep 17 00:00:00 2001 From: omriss Date: Mon, 9 Feb 2026 16:22:30 +0700 Subject: [PATCH 3/3] Move liquidateRewards from ALMFLib to ALMFLib2 to reduce size of ALMFLib --- src/strategies/AaveLeverageMerklFarmStrategy.sol | 2 +- src/strategies/libs/ALMFLib.sol | 12 ------------ src/strategies/libs/ALMFLib2.sol | 13 +++++++++++++ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/strategies/AaveLeverageMerklFarmStrategy.sol b/src/strategies/AaveLeverageMerklFarmStrategy.sol index 459c67ca..67f37ce1 100644 --- a/src/strategies/AaveLeverageMerklFarmStrategy.sol +++ b/src/strategies/AaveLeverageMerklFarmStrategy.sol @@ -439,7 +439,7 @@ contract AaveLeverageMerklFarmStrategy is address[] memory rewardAssets_, uint[] memory rewardAmounts_ ) internal override(FarmingStrategyBase, StrategyBase, LeverageLendingBase) returns (uint earnedExchangeAsset) { - return ALMFLib.liquidateRewards( + return ALMFLib2.liquidateRewards( platform(), exchangeAsset, rewardAssets_, rewardAmounts_, customPriceImpactTolerance() ); } diff --git a/src/strategies/libs/ALMFLib.sol b/src/strategies/libs/ALMFLib.sol index cda4f4da..d6fef30b 100644 --- a/src/strategies/libs/ALMFLib.sol +++ b/src/strategies/libs/ALMFLib.sol @@ -925,18 +925,6 @@ library ALMFLib { } } - 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 $, diff --git a/src/strategies/libs/ALMFLib2.sol b/src/strategies/libs/ALMFLib2.sol index af7c6337..d23fb274 100644 --- a/src/strategies/libs/ALMFLib2.sol +++ b/src/strategies/libs/ALMFLib2.sol @@ -13,6 +13,7 @@ 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"; +import {StrategyLib} from "./StrategyLib.sol"; /// @notice Several standalone functions were moved here to reduce size of ALMFLib library ALMFLib2 { @@ -143,4 +144,16 @@ library ALMFLib2 { } //endregion ------------------------------------- Init vars, desc + + 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 + ); + } }