From dc785e55e35bcc1f284d3a288137a24451424965 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 5 Sep 2024 11:42:03 +0200 Subject: [PATCH 01/10] Fix withdraw dust - add tests --- src/strategies/stader/MaticXLooper.sol | 22 +++++----- test/strategies/stader/MaticXLooper.t.sol | 40 +++++++++++++++---- .../stader/MaticXLooperTestConfig.json | 2 +- 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/strategies/stader/MaticXLooper.sol b/src/strategies/stader/MaticXLooper.sol index a5fbfa8e..74e091fa 100644 --- a/src/strategies/stader/MaticXLooper.sol +++ b/src/strategies/stader/MaticXLooper.sol @@ -22,7 +22,7 @@ struct LooperInitValues { } /// @title Leveraged maticX yield adapter -/// @author ADN +/// @author Vaultcraft /// @notice ERC4626 wrapper for leveraging maticX yield /// @dev The strategy takes MaticX and deposits it into a lending protocol (aave). /// Then it borrows Matic, swap for MaticX and redeposits it @@ -46,9 +46,6 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { IERC20 public debtToken; // aave wmatic debt token IERC20 public interestToken; // aave MaticX - uint256 private constant maticXIndex = 1; // TODO - uint256 private constant wMaticIndex = 0; // TODO - IBalancerVault public balancerVault; bytes32 public balancerPoolId; @@ -396,7 +393,7 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { Math.Rounding.Floor ); - // if the withdraw amount with buffers to total assets withdraw all + // if the withdraw amount with buffers to total assets withdraw all if (flashLoanMaticXAmount + maticXBuffer + toWithdraw >= _totalAssets()) isFullWithdraw = true; @@ -637,10 +634,17 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { function withdrawDust(address recipient) public onlyOwner { // send matic dust to recipient - (bool sent, ) = address(recipient).call{value: address(this).balance}( - "" - ); - require(sent, "Failed to send Matic"); + uint256 maticBalance = address(this).balance; + if (maticBalance > 0) { + (bool sent,) = address(recipient).call{value: address(this).balance}(""); + require(sent, "Failed to send Matic"); + } + + // send maticX + uint256 maticXBalance = IERC20(asset()).balanceOf(address(this)); + if(totalSupply() == 0 && maticXBalance > 0) { + IERC20(asset()).transfer(recipient, maticXBalance); + } } function setLeverageValues( diff --git a/test/strategies/stader/MaticXLooper.t.sol b/test/strategies/stader/MaticXLooper.t.sol index da3f1b99..ed71bc45 100644 --- a/test/strategies/stader/MaticXLooper.t.sol +++ b/test/strategies/stader/MaticXLooper.t.sol @@ -439,15 +439,10 @@ contract MaticXLooperTest is BaseStrategyTest { vm.stopPrank(); // check total assets - assertEq(strategy.totalAssets(), 0, "TA"); + uint256 expDust = amountDeposit.mulDiv(slippage, 1e18, Math.Rounding.Floor); + assertApproxEqAbs(strategy.totalAssets(), expDust, _delta_, "TA"); - // should not hold any maticX - assertApproxEqAbs( - maticX.balanceOf(address(strategy)), - 0, - _delta_, - string.concat("more maticX dust than expected") - ); + assertEq(IERC20(address(strategy)).totalSupply(), 0); // should not hold any maticX aToken assertEq(aMaticX.balanceOf(address(strategy)), 0); @@ -456,6 +451,35 @@ contract MaticXLooperTest is BaseStrategyTest { assertEq(vdWMatic.balanceOf(address(strategy)), 0); } + function test_withdraw_dust() public { + // manager can withdraw maticX balance when vault total supply is 0 + deal(address(maticX), address(strategy), 10e18); + + vm.prank(address(this)); + strategyContract.withdrawDust(address(this)); + + assertEq(strategy.totalAssets(), 0, "TA"); + } + + function test_withdraw_dust_invalid() public { + // manager can not withdraw maticX balance when vault total supply is > 0 + deal(address(maticX), address(bob), 10e18); + + vm.startPrank(bob); + maticX.approve(address(strategy), 10e18); + strategy.deposit(10e18, bob); + vm.stopPrank(); + + uint256 totAssetsBefore = strategy.totalAssets(); + uint256 maticXOwnerBefore = IERC20(address(maticX)).balanceOf(address(this)); + + vm.prank(address(this)); + strategyContract.withdrawDust(address(this)); + + assertEq(strategy.totalAssets(), totAssetsBefore, "TA DUST"); + assertEq(IERC20(address(maticX)).balanceOf(address(this)), maticXOwnerBefore, "OWNER DUST"); + } + function test__setLeverageValues_lever_up() public { uint256 amountMint = 10e18; uint256 amountDeposit = 1e18; diff --git a/test/strategies/stader/MaticXLooperTestConfig.json b/test/strategies/stader/MaticXLooperTestConfig.json index 22cab0c3..b4613ee9 100644 --- a/test/strategies/stader/MaticXLooperTestConfig.json +++ b/test/strategies/stader/MaticXLooperTestConfig.json @@ -22,7 +22,7 @@ "maxLTV": 900000000000000000, "poolAddressesProvider": "0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb", "poolId": "0xcd78a20c597e367a4e478a2411ceb790604d7c8f000000000000000000000c22", - "slippage": 8000000000000000, + "slippage": 5000000000000000, "targetLTV": 800000000000000000 } } From dc8e6437f3f0c3b9fb615ccd6c66c5282badc9bf Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 9 Sep 2024 14:21:09 +0200 Subject: [PATCH 02/10] WIP withdraw with data --- src/interfaces/IStrategyWithData.sol | 13 +++ src/strategies/stader/MaticXLooper.sol | 101 ++++++++++++++++-- src/vaults/MultiStrategyVault.sol | 141 ++++++++++++++++++++----- 3 files changed, 216 insertions(+), 39 deletions(-) create mode 100644 src/interfaces/IStrategyWithData.sol diff --git a/src/interfaces/IStrategyWithData.sol b/src/interfaces/IStrategyWithData.sol new file mode 100644 index 00000000..48ffdbaa --- /dev/null +++ b/src/interfaces/IStrategyWithData.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +interface IStrategyWithData { + function withdrawWithData( + uint256 assets, + address receiver, + address owner, + bytes calldata extraData + ) external returns (uint256 shares); +} diff --git a/src/strategies/stader/MaticXLooper.sol b/src/strategies/stader/MaticXLooper.sol index 74e091fa..78e082e4 100644 --- a/src/strategies/stader/MaticXLooper.sol +++ b/src/strategies/stader/MaticXLooper.sol @@ -7,8 +7,22 @@ import {BaseStrategy, IERC20, IERC20Metadata, SafeERC20, ERC20, Math} from "src/ import {IMaticXPool} from "./IMaticX.sol"; import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; import {IWETH as IWMatic} from "src/interfaces/external/IWETH.sol"; -import {ILendingPool, IAToken, IFlashLoanReceiver, IProtocolDataProvider, IAaveIncentives, IPoolAddressesProvider, DataTypes} from "src/interfaces/external/aave/IAaveV3.sol"; -import {IBalancerVault, SwapKind, SingleSwap, FundManagement} from "src/interfaces/external/balancer/IBalancer.sol"; +import {IStrategyWithData} from "src/interfaces/IStrategyWithData.sol"; +import { + ILendingPool, + IAToken, + IFlashLoanReceiver, + IProtocolDataProvider, + IPoolAddressesProvider, + DataTypes +} from "src/interfaces/external/aave/IAaveV3.sol"; +import { + IBalancerVault, + SwapKind, + SingleSwap, + FundManagement +} from "src/interfaces/external/balancer/IBalancer.sol"; + struct LooperInitValues { address aaveDataProvider; @@ -26,7 +40,7 @@ struct LooperInitValues { /// @notice ERC4626 wrapper for leveraging maticX yield /// @dev The strategy takes MaticX and deposits it into a lending protocol (aave). /// Then it borrows Matic, swap for MaticX and redeposits it -contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { +contract MaticXLooper is BaseStrategy, IStrategyWithData, IFlashLoanReceiver { // using FixedPointMathLib for uint256; using SafeERC20 for IERC20; using Math for uint256; @@ -246,12 +260,17 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { revert NotFlashLoan(); } +<<<<<<< HEAD ( bool isWithdraw, bool isFullWithdraw, uint256 assetsToWithdraw, uint256 depositAmount ) = abi.decode(params, (bool, bool, uint256, uint256)); +======= + (bool isWithdraw, bool isFullWithdraw, uint256 assetsToWithdraw, uint256 depositAmount, uint256 chosenSlippage) = + abi.decode(params, (bool, bool, uint256, uint256, uint256)); +>>>>>>> 59eca30 (WIP withdraw with data) if (isWithdraw) { // flash loan is to repay Matic debt as part of a withdrawal @@ -261,7 +280,7 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { lendingPool.repay(address(wMatic), amounts[0], 2, address(this)); // withdraw collateral, swap, repay flashloan - _reduceLeverage(isFullWithdraw, assetsToWithdraw, flashLoanDebt); + _reduceLeverage(isFullWithdraw, assetsToWithdraw, flashLoanDebt, chosenSlippage); } else { // flash loan is to leverage UP _redepositAsset(amounts[0], depositAmount); @@ -270,6 +289,36 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { return true; } + /*////////////////////////////////////////////////////////////// + IStrategyWithData LOGIC + //////////////////////////////////////////////////////////////*/ + function withdrawWithData( + uint256 assets, + address receiver, + address owner, + bytes calldata extraData + ) external override(IStrategyWithData) returns (uint256 shares) { + if (shares == 0 || assets == 0) revert ZeroAmount(); + address caller = _msgSender(); + + if (caller != owner) { + _spendAllowance(owner, caller, shares); + } + + shares = _convertToShares(assets, Math.Rounding.Ceil); + + uint256 assetBalBefore = IERC20(asset()).balanceOf(address(this)); + + _protocolWithdraw(assets, shares, extraData); + + uint256 assetBalAfter = IERC20(asset()).balanceOf(address(this)); + + // transfer surplus here TODO get balance before and after + + _burn(owner, shares); + } + + /*////////////////////////////////////////////////////////////// INTERNAL HOOKS LOGIC //////////////////////////////////////////////////////////////*/ @@ -285,11 +334,15 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { } /// @notice repay part of the vault debt and withdraw maticX +<<<<<<< HEAD function _protocolWithdraw( uint256 assets, uint256, bytes memory ) internal override { +======= + function _protocolWithdraw(uint256 assets, uint256, bytes memory extraData) internal override { +>>>>>>> 59eca30 (WIP withdraw with data) (, uint256 currentDebt, uint256 currentCollateral) = _getCurrentLTV(); (uint256 maticAssetsValue, , ) = maticXPool.convertMaticXToMatic( assets @@ -298,12 +351,24 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { bool isFullWithdraw; uint256 ratioDebtToRepay; + uint256 chosenSlippage; + if(extraData.length > 0) + chosenSlippage = abi.decode(extraData, (uint256)); + + // user cannot provide a higher slippage than default + if (chosenSlippage > slippage) + chosenSlippage = slippage; + { +<<<<<<< HEAD uint256 debtSlippage = currentDebt.mulDiv( slippage, 1e18, Math.Rounding.Ceil ); +======= + uint256 debtSlippage = currentDebt.mulDiv(chosenSlippage, 1e18, Math.Rounding.Ceil); +>>>>>>> 59eca30 (WIP withdraw with data) // find the % of debt to repay as the % of collateral being withdrawn ratioDebtToRepay = maticAssetsValue.mulDiv( @@ -341,7 +406,7 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { ); // flash loan debtToRepay - mode 0 - flash loan is repaid at the end - _flashLoanMatic(debtToRepay, 0, assets, 0, isFullWithdraw); + _flashLoanMatic(debtToRepay, 0, assets, 0, isFullWithdraw, chosenSlippage); } // reverts if LTV got above max @@ -375,9 +440,16 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { // reduce leverage by withdrawing maticX, swapping to Matic repaying Matic debt function _reduceLeverage( +<<<<<<< HEAD bool isFullWithdraw, uint256 toWithdraw, uint256 flashLoanDebt +======= + bool isFullWithdraw, + uint256 toWithdraw, + uint256 flashLoanDebt, + uint256 chosenSlippage +>>>>>>> 59eca30 (WIP withdraw with data) ) internal { address asset = asset(); @@ -387,11 +459,15 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { ); // get slippage buffer for swapping with flashLoanDebt as minAmountOut +<<<<<<< HEAD uint256 maticXBuffer = flashLoanMaticXAmount.mulDiv( slippage, 1e18, Math.Rounding.Floor ); +======= + uint256 maticXBuffer = flashLoanMaticXAmount.mulDiv(chosenSlippage, 1e18, Math.Rounding.Floor); +>>>>>>> 59eca30 (WIP withdraw with data) // if the withdraw amount with buffers to total assets withdraw all if (flashLoanMaticXAmount + maticXBuffer + toWithdraw >= _totalAssets()) @@ -463,7 +539,8 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { uint256 depositAmount, uint256 assetsToWithdraw, uint256 interestRateMode, - bool isFullWithdraw + bool isFullWithdraw, + uint256 chosenSlippage ) internal { uint256 depositAmount_ = depositAmount; // avoids stack too deep @@ -482,12 +559,16 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { amounts, interestRateModes, address(this), +<<<<<<< HEAD abi.encode( interestRateMode == 0 ? true : false, isFullWithdraw, assetsToWithdraw, depositAmount_ ), +======= + abi.encode(interestRateMode == 0 ? true : false, isFullWithdraw, assetsToWithdraw, depositAmount_, chosenSlippage), +>>>>>>> 59eca30 (WIP withdraw with data) 0 ); } @@ -601,8 +682,8 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { ) )).mulDiv(1e18, (1e18 - targetLTV), Math.Rounding.Ceil); - // flash loan matic to repay part of the debt - _flashLoanMatic(amountMatic, 0, 0, 0, false); + // flash loan matic to repay part of the debt - use default slippage + _flashLoanMatic(amountMatic, 0, 0, 0, false, slippage); } else { uint256 amountMatic = (targetLTV.mulDiv( currentCollateral, @@ -619,8 +700,8 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { // flashloan but use eventual Matic dust remained in the contract as well uint256 borrowAmount = amountMatic - dustBalance; - // flash loan wMatic from lending protocol and add to cdp - _flashLoanMatic(borrowAmount, amountMatic, 0, 2, false); + // flash loan wMatic from lending protocol and add to cdp - slippage not used in this case, pass 0 + _flashLoanMatic(borrowAmount, amountMatic, 0, 2, false, 0); } else { // deposit the dust as collateral- borrow amount is zero // leverage naturally decreases diff --git a/src/vaults/MultiStrategyVault.sol b/src/vaults/MultiStrategyVault.sol index 7261dfdb..adc8d96a 100644 --- a/src/vaults/MultiStrategyVault.sol +++ b/src/vaults/MultiStrategyVault.sol @@ -9,6 +9,7 @@ import {ReentrancyGuardUpgradeable} from "openzeppelin-contracts-upgradeable/uti import {PausableUpgradeable} from "openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol"; import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; import {OwnedUpgradeable} from "../utils/OwnedUpgradeable.sol"; +import {IStrategyWithData} from "../interfaces/IStrategyWithData.sol"; struct Allocation { uint256 index; @@ -359,8 +360,48 @@ contract MultiStrategyVault is // Get the Vault's floating balance. uint256 float = asset_.balanceOf(address(this)); + bytes[] memory extraData = new bytes[](0); // empty extraData + if (withdrawalQueue_.length > 0 && assets > float) { - _withdrawStrategyFunds(assets, float, withdrawalQueue_); + _withdrawStrategyFunds(assets, float, withdrawalQueue_, extraData); + } + + asset_.safeTransfer(receiver, assets); + + emit Withdraw(caller, receiver, owner, assets, shares); + } + + function withdrawWithData( + address receiver, + address owner, + uint256 assets, + bytes[] memory extraData + ) external nonReentrant { + uint256 maxAssets = maxWithdraw(owner); + if (assets > maxAssets) { + revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets); + } + + address caller = _msgSender(); + uint256 shares = previewWithdraw(assets); + + if (shares == 0 || assets == 0) revert ZeroAmount(); + if (caller != owner) { + _spendAllowance(owner, caller, shares); + } + + _takeFees(); + + _burn(owner, shares); + + IERC20 asset_ = IERC20(asset()); + uint256[] memory withdrawalQueue_ = withdrawalQueue; + + // Get the Vault's floating balance. + uint256 float = asset_.balanceOf(address(this)); + + if (withdrawalQueue_.length > 0 && assets > float) { + _withdrawStrategyFunds(assets, float, withdrawalQueue_, extraData); } asset_.safeTransfer(receiver, assets); @@ -371,7 +412,8 @@ contract MultiStrategyVault is function _withdrawStrategyFunds( uint256 amount, uint256 float, - uint256[] memory queue + uint256[] memory queue, + bytes[] memory extraData ) internal { // Iterate the withdrawal queue and get indexes // Will revert due to underflow if we empty the stack before pulling the desired amount. @@ -386,25 +428,55 @@ contract MultiStrategyVault is ); if (withdrawableAssets >= missing) { - try strategy.withdraw(missing, address(this), address(this)) { - break; - } catch { - emit StrategyWithdrawalFailed(address(strategy), missing); + if(extraData[i].length > 0) { + // withdraw with data + try IStrategyWithData(address(strategy)).withdrawWithData(missing, address(this), address(this), extraData[i]) { + break; + } catch { + emit StrategyWithdrawalFailed(address(strategy), missing); + } + } else { + // regular withdraw + try strategy.withdraw(missing, address(this), address(this)) { + break; + } catch { + emit StrategyWithdrawalFailed(address(strategy), missing); + } } } else if (withdrawableAssets > 0) { - try - strategy.withdraw( - withdrawableAssets, - address(this), - address(this) - ) - { - float += withdrawableAssets; - } catch { - emit StrategyWithdrawalFailed( - address(strategy), - withdrawableAssets - ); + if(extraData[i].length > 0) { + try + // withdraw with data + IStrategyWithData(address(strategy)).withdrawWithData( + withdrawableAssets, + address(this), + address(this), + extraData[i] + ) + { + float += withdrawableAssets; + } catch { + emit StrategyWithdrawalFailed( + address(strategy), + withdrawableAssets + ); + } + } else { + // regular withdraw + try + strategy.withdraw( + withdrawableAssets, + address(this), + address(this) + ) + { + float += withdrawableAssets; + } catch { + emit StrategyWithdrawalFailed( + address(strategy), + withdrawableAssets + ); + } } } } @@ -472,6 +544,7 @@ contract MultiStrategyVault is error InvalidIndex(); error InvalidWithdrawalQueue(); error NotPassedQuitPeriod(uint256 quitPeriod_); + error InvalidExtraData(); function getStrategies() external view returns (IERC4626[] memory) { return strategies; @@ -658,20 +731,30 @@ contract MultiStrategyVault is /** * @notice Pull funds out of strategies to be reallocated into different strategies. Caller must be Owner. * @param allocations An array of structs each including the strategyIndex to withdraw from and the amount of assets + * @param extraData An array of encoded bytes to pass down to strategy for extra-logic, if present */ - function pullFunds(Allocation[] calldata allocations) external onlyOwner { - _pullFunds(allocations); - } - - function _pullFunds(Allocation[] calldata allocations) internal { + function pullFunds(Allocation[] calldata allocations, bytes[] calldata extraData) external onlyOwner { uint256 len = allocations.length; + + if(extraData.length != len) + revert InvalidExtraData(); + for (uint256 i; i < len; i++) { if (allocations[i].amount > 0) { - strategies[allocations[i].index].withdraw( - allocations[i].amount, - address(this), - address(this) - ); + if(extraData[i].length > 0) { + IStrategyWithData(address(strategies[allocations[i].index])).withdrawWithData( + allocations[i].amount, + address(this), + address(this), + extraData[i] + ); + } else { + strategies[allocations[i].index].withdraw( + allocations[i].amount, + address(this), + address(this) + ); + } } } } From 0340e2e6361cbed60fdc075b2fecf0ea174c85d4 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 10 Sep 2024 13:47:38 +0200 Subject: [PATCH 03/10] Move logic to base strategy - WIP --- src/strategies/BaseStrategy.sol | 53 ++++++++++++++++++++++++++ src/strategies/stader/MaticXLooper.sol | 40 ++++++++----------- 2 files changed, 69 insertions(+), 24 deletions(-) diff --git a/src/strategies/BaseStrategy.sol b/src/strategies/BaseStrategy.sol index 90e7408d..085f26b8 100644 --- a/src/strategies/BaseStrategy.sol +++ b/src/strategies/BaseStrategy.sol @@ -9,6 +9,7 @@ import {ReentrancyGuardUpgradeable} from "openzeppelin-contracts-upgradeable/uti import {PausableUpgradeable} from "openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol"; import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; import {OwnedUpgradeable} from "src/utils/OwnedUpgradeable.sol"; +import {IStrategyWithData} from "src/interfaces/IStrategyWithData.sol"; /** * @title BaseStrategy @@ -22,6 +23,7 @@ import {OwnedUpgradeable} from "src/utils/OwnedUpgradeable.sol"; */ abstract contract BaseStrategy is ERC4626Upgradeable, + IStrategyWithData, PausableUpgradeable, OwnedUpgradeable, ReentrancyGuardUpgradeable @@ -152,6 +154,50 @@ abstract contract BaseStrategy is emit Withdraw(caller, receiver, owner, assets, shares); } + /** + * @dev Withdraw workflow with custom data + */ + function withdrawWithData( + uint256 assets, + address receiver, + address owner, + bytes calldata extraData + ) external override(IStrategyWithData) returns (uint256 shares) { + uint256 maxAssets = maxWithdraw(owner); + if (assets > maxAssets) { + revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets); + } + + uint256 shares = previewWithdraw(assets); + address caller = _msgSender(); + + if (shares == 0 || assets == 0) revert ZeroAmount(); + if (caller != owner) { + _spendAllowance(owner, caller, shares); + } + + uint256 assetSurplus; + // We call this before the `burn` to allow for normal calculations with shares before they get burned + // Since we transfer assets after the burn the function should remain safe + if (!paused()) { + uint256 float = IERC20(asset()).balanceOf(address(this)); + if (assets > float) { + uint256 missing = assets - float; + assetSurplus = _protocolWithdrawWithData(missing, convertToShares(missing), extraData); + } + } + + _burn(owner, shares); + + IERC20(asset()).safeTransfer(receiver, assets); + + // transfer eventual surplus to owner directly + if(assetSurplus > 0) + IERC20(asset()).safeTransfer(owner, assetSurplus); + + emit Withdraw(caller, receiver, owner, assets, shares); + } + /*////////////////////////////////////////////////////////////// ACCOUNTING LOGIC //////////////////////////////////////////////////////////////*/ @@ -227,6 +273,13 @@ abstract contract BaseStrategy is bytes memory data ) internal virtual; + /// @notice Override in implementation strategy to have custom protocol logic during withdraw based on data + function _protocolWithdrawWithData( + uint256 assets, + uint256 shares, + bytes memory data + ) internal virtual returns (uint256 assetSurplus) {} + /*////////////////////////////////////////////////////////////// STRATEGY LOGIC //////////////////////////////////////////////////////////////*/ diff --git a/src/strategies/stader/MaticXLooper.sol b/src/strategies/stader/MaticXLooper.sol index 78e082e4..a29c472b 100644 --- a/src/strategies/stader/MaticXLooper.sol +++ b/src/strategies/stader/MaticXLooper.sol @@ -7,7 +7,6 @@ import {BaseStrategy, IERC20, IERC20Metadata, SafeERC20, ERC20, Math} from "src/ import {IMaticXPool} from "./IMaticX.sol"; import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; import {IWETH as IWMatic} from "src/interfaces/external/IWETH.sol"; -import {IStrategyWithData} from "src/interfaces/IStrategyWithData.sol"; import { ILendingPool, IAToken, @@ -40,7 +39,7 @@ struct LooperInitValues { /// @notice ERC4626 wrapper for leveraging maticX yield /// @dev The strategy takes MaticX and deposits it into a lending protocol (aave). /// Then it borrows Matic, swap for MaticX and redeposits it -contract MaticXLooper is BaseStrategy, IStrategyWithData, IFlashLoanReceiver { +contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { // using FixedPointMathLib for uint256; using SafeERC20 for IERC20; using Math for uint256; @@ -292,33 +291,25 @@ contract MaticXLooper is BaseStrategy, IStrategyWithData, IFlashLoanReceiver { /*////////////////////////////////////////////////////////////// IStrategyWithData LOGIC //////////////////////////////////////////////////////////////*/ - function withdrawWithData( - uint256 assets, - address receiver, - address owner, - bytes calldata extraData - ) external override(IStrategyWithData) returns (uint256 shares) { - if (shares == 0 || assets == 0) revert ZeroAmount(); - address caller = _msgSender(); - - if (caller != owner) { - _spendAllowance(owner, caller, shares); - } - shares = _convertToShares(assets, Math.Rounding.Ceil); - - uint256 assetBalBefore = IERC20(asset()).balanceOf(address(this)); + /// @notice user can pass a preferred slippage %, which will be used against the default one + /// @notice surplus of assets released is then transferred + function _protocolWithdrawWithData( + uint256 assets, + uint256 shares, + bytes memory extraData + ) internal override returns (uint256 extraAmountToTransfer) { + uint256 assetBalanceBefore = IERC20(asset()).balanceOf(address(this)); _protocolWithdraw(assets, shares, extraData); - uint256 assetBalAfter = IERC20(asset()).balanceOf(address(this)); - - // transfer surplus here TODO get balance before and after - - _burn(owner, shares); + uint256 assetBalanceAfter = IERC20(asset()).balanceOf(address(this)); + + // calculate eventual surplus + if(assetBalanceAfter - assetBalanceBefore > assets) + extraAmountToTransfer = assetBalanceAfter - assetBalanceBefore - assets; } - /*////////////////////////////////////////////////////////////// INTERNAL HOOKS LOGIC //////////////////////////////////////////////////////////////*/ @@ -351,12 +342,13 @@ contract MaticXLooper is BaseStrategy, IStrategyWithData, IFlashLoanReceiver { bool isFullWithdraw; uint256 ratioDebtToRepay; + // user can provide a desired slippage uint256 chosenSlippage; if(extraData.length > 0) chosenSlippage = abi.decode(extraData, (uint256)); // user cannot provide a higher slippage than default - if (chosenSlippage > slippage) + if (chosenSlippage > slippage || chosenSlippage == 0) chosenSlippage = slippage; { From 41fc9dd0e6bf0c631e7d284a587428590e404c06 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 18 Sep 2024 11:33:03 +0200 Subject: [PATCH 04/10] Remove withdraw with data logic --- src/interfaces/IStrategyWithData.sol | 13 --- src/strategies/BaseStrategy.sol | 53 ---------- src/strategies/stader/MaticXLooper.sol | 48 --------- src/vaults/MultiStrategyVault.sol | 138 +++++-------------------- 4 files changed, 26 insertions(+), 226 deletions(-) delete mode 100644 src/interfaces/IStrategyWithData.sol diff --git a/src/interfaces/IStrategyWithData.sol b/src/interfaces/IStrategyWithData.sol deleted file mode 100644 index 48ffdbaa..00000000 --- a/src/interfaces/IStrategyWithData.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -// Docgen-SOLC: 0.8.25 - -pragma solidity ^0.8.25; - -interface IStrategyWithData { - function withdrawWithData( - uint256 assets, - address receiver, - address owner, - bytes calldata extraData - ) external returns (uint256 shares); -} diff --git a/src/strategies/BaseStrategy.sol b/src/strategies/BaseStrategy.sol index 085f26b8..90e7408d 100644 --- a/src/strategies/BaseStrategy.sol +++ b/src/strategies/BaseStrategy.sol @@ -9,7 +9,6 @@ import {ReentrancyGuardUpgradeable} from "openzeppelin-contracts-upgradeable/uti import {PausableUpgradeable} from "openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol"; import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; import {OwnedUpgradeable} from "src/utils/OwnedUpgradeable.sol"; -import {IStrategyWithData} from "src/interfaces/IStrategyWithData.sol"; /** * @title BaseStrategy @@ -23,7 +22,6 @@ import {IStrategyWithData} from "src/interfaces/IStrategyWithData.sol"; */ abstract contract BaseStrategy is ERC4626Upgradeable, - IStrategyWithData, PausableUpgradeable, OwnedUpgradeable, ReentrancyGuardUpgradeable @@ -154,50 +152,6 @@ abstract contract BaseStrategy is emit Withdraw(caller, receiver, owner, assets, shares); } - /** - * @dev Withdraw workflow with custom data - */ - function withdrawWithData( - uint256 assets, - address receiver, - address owner, - bytes calldata extraData - ) external override(IStrategyWithData) returns (uint256 shares) { - uint256 maxAssets = maxWithdraw(owner); - if (assets > maxAssets) { - revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets); - } - - uint256 shares = previewWithdraw(assets); - address caller = _msgSender(); - - if (shares == 0 || assets == 0) revert ZeroAmount(); - if (caller != owner) { - _spendAllowance(owner, caller, shares); - } - - uint256 assetSurplus; - // We call this before the `burn` to allow for normal calculations with shares before they get burned - // Since we transfer assets after the burn the function should remain safe - if (!paused()) { - uint256 float = IERC20(asset()).balanceOf(address(this)); - if (assets > float) { - uint256 missing = assets - float; - assetSurplus = _protocolWithdrawWithData(missing, convertToShares(missing), extraData); - } - } - - _burn(owner, shares); - - IERC20(asset()).safeTransfer(receiver, assets); - - // transfer eventual surplus to owner directly - if(assetSurplus > 0) - IERC20(asset()).safeTransfer(owner, assetSurplus); - - emit Withdraw(caller, receiver, owner, assets, shares); - } - /*////////////////////////////////////////////////////////////// ACCOUNTING LOGIC //////////////////////////////////////////////////////////////*/ @@ -273,13 +227,6 @@ abstract contract BaseStrategy is bytes memory data ) internal virtual; - /// @notice Override in implementation strategy to have custom protocol logic during withdraw based on data - function _protocolWithdrawWithData( - uint256 assets, - uint256 shares, - bytes memory data - ) internal virtual returns (uint256 assetSurplus) {} - /*////////////////////////////////////////////////////////////// STRATEGY LOGIC //////////////////////////////////////////////////////////////*/ diff --git a/src/strategies/stader/MaticXLooper.sol b/src/strategies/stader/MaticXLooper.sol index a29c472b..e800674e 100644 --- a/src/strategies/stader/MaticXLooper.sol +++ b/src/strategies/stader/MaticXLooper.sol @@ -259,17 +259,8 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { revert NotFlashLoan(); } -<<<<<<< HEAD - ( - bool isWithdraw, - bool isFullWithdraw, - uint256 assetsToWithdraw, - uint256 depositAmount - ) = abi.decode(params, (bool, bool, uint256, uint256)); -======= (bool isWithdraw, bool isFullWithdraw, uint256 assetsToWithdraw, uint256 depositAmount, uint256 chosenSlippage) = abi.decode(params, (bool, bool, uint256, uint256, uint256)); ->>>>>>> 59eca30 (WIP withdraw with data) if (isWithdraw) { // flash loan is to repay Matic debt as part of a withdrawal @@ -325,15 +316,7 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { } /// @notice repay part of the vault debt and withdraw maticX -<<<<<<< HEAD - function _protocolWithdraw( - uint256 assets, - uint256, - bytes memory - ) internal override { -======= function _protocolWithdraw(uint256 assets, uint256, bytes memory extraData) internal override { ->>>>>>> 59eca30 (WIP withdraw with data) (, uint256 currentDebt, uint256 currentCollateral) = _getCurrentLTV(); (uint256 maticAssetsValue, , ) = maticXPool.convertMaticXToMatic( assets @@ -352,15 +335,7 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { chosenSlippage = slippage; { -<<<<<<< HEAD - uint256 debtSlippage = currentDebt.mulDiv( - slippage, - 1e18, - Math.Rounding.Ceil - ); -======= uint256 debtSlippage = currentDebt.mulDiv(chosenSlippage, 1e18, Math.Rounding.Ceil); ->>>>>>> 59eca30 (WIP withdraw with data) // find the % of debt to repay as the % of collateral being withdrawn ratioDebtToRepay = maticAssetsValue.mulDiv( @@ -432,16 +407,10 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { // reduce leverage by withdrawing maticX, swapping to Matic repaying Matic debt function _reduceLeverage( -<<<<<<< HEAD - bool isFullWithdraw, - uint256 toWithdraw, - uint256 flashLoanDebt -======= bool isFullWithdraw, uint256 toWithdraw, uint256 flashLoanDebt, uint256 chosenSlippage ->>>>>>> 59eca30 (WIP withdraw with data) ) internal { address asset = asset(); @@ -451,15 +420,7 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { ); // get slippage buffer for swapping with flashLoanDebt as minAmountOut -<<<<<<< HEAD - uint256 maticXBuffer = flashLoanMaticXAmount.mulDiv( - slippage, - 1e18, - Math.Rounding.Floor - ); -======= uint256 maticXBuffer = flashLoanMaticXAmount.mulDiv(chosenSlippage, 1e18, Math.Rounding.Floor); ->>>>>>> 59eca30 (WIP withdraw with data) // if the withdraw amount with buffers to total assets withdraw all if (flashLoanMaticXAmount + maticXBuffer + toWithdraw >= _totalAssets()) @@ -551,16 +512,7 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { amounts, interestRateModes, address(this), -<<<<<<< HEAD - abi.encode( - interestRateMode == 0 ? true : false, - isFullWithdraw, - assetsToWithdraw, - depositAmount_ - ), -======= abi.encode(interestRateMode == 0 ? true : false, isFullWithdraw, assetsToWithdraw, depositAmount_, chosenSlippage), ->>>>>>> 59eca30 (WIP withdraw with data) 0 ); } diff --git a/src/vaults/MultiStrategyVault.sol b/src/vaults/MultiStrategyVault.sol index adc8d96a..6cecdd34 100644 --- a/src/vaults/MultiStrategyVault.sol +++ b/src/vaults/MultiStrategyVault.sol @@ -9,7 +9,6 @@ import {ReentrancyGuardUpgradeable} from "openzeppelin-contracts-upgradeable/uti import {PausableUpgradeable} from "openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol"; import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; import {OwnedUpgradeable} from "../utils/OwnedUpgradeable.sol"; -import {IStrategyWithData} from "../interfaces/IStrategyWithData.sol"; struct Allocation { uint256 index; @@ -360,48 +359,8 @@ contract MultiStrategyVault is // Get the Vault's floating balance. uint256 float = asset_.balanceOf(address(this)); - bytes[] memory extraData = new bytes[](0); // empty extraData - if (withdrawalQueue_.length > 0 && assets > float) { - _withdrawStrategyFunds(assets, float, withdrawalQueue_, extraData); - } - - asset_.safeTransfer(receiver, assets); - - emit Withdraw(caller, receiver, owner, assets, shares); - } - - function withdrawWithData( - address receiver, - address owner, - uint256 assets, - bytes[] memory extraData - ) external nonReentrant { - uint256 maxAssets = maxWithdraw(owner); - if (assets > maxAssets) { - revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets); - } - - address caller = _msgSender(); - uint256 shares = previewWithdraw(assets); - - if (shares == 0 || assets == 0) revert ZeroAmount(); - if (caller != owner) { - _spendAllowance(owner, caller, shares); - } - - _takeFees(); - - _burn(owner, shares); - - IERC20 asset_ = IERC20(asset()); - uint256[] memory withdrawalQueue_ = withdrawalQueue; - - // Get the Vault's floating balance. - uint256 float = asset_.balanceOf(address(this)); - - if (withdrawalQueue_.length > 0 && assets > float) { - _withdrawStrategyFunds(assets, float, withdrawalQueue_, extraData); + _withdrawStrategyFunds(assets, float, withdrawalQueue_); } asset_.safeTransfer(receiver, assets); @@ -412,8 +371,7 @@ contract MultiStrategyVault is function _withdrawStrategyFunds( uint256 amount, uint256 float, - uint256[] memory queue, - bytes[] memory extraData + uint256[] memory queue ) internal { // Iterate the withdrawal queue and get indexes // Will revert due to underflow if we empty the stack before pulling the desired amount. @@ -428,56 +386,26 @@ contract MultiStrategyVault is ); if (withdrawableAssets >= missing) { - if(extraData[i].length > 0) { - // withdraw with data - try IStrategyWithData(address(strategy)).withdrawWithData(missing, address(this), address(this), extraData[i]) { - break; - } catch { - emit StrategyWithdrawalFailed(address(strategy), missing); - } - } else { - // regular withdraw - try strategy.withdraw(missing, address(this), address(this)) { - break; - } catch { - emit StrategyWithdrawalFailed(address(strategy), missing); - } + try strategy.withdraw(missing, address(this), address(this)) { + break; + } catch { + emit StrategyWithdrawalFailed(address(strategy), missing); } } else if (withdrawableAssets > 0) { - if(extraData[i].length > 0) { - try - // withdraw with data - IStrategyWithData(address(strategy)).withdrawWithData( - withdrawableAssets, - address(this), - address(this), - extraData[i] - ) - { - float += withdrawableAssets; - } catch { - emit StrategyWithdrawalFailed( - address(strategy), - withdrawableAssets - ); - } - } else { - // regular withdraw - try - strategy.withdraw( - withdrawableAssets, - address(this), - address(this) - ) - { - float += withdrawableAssets; - } catch { - emit StrategyWithdrawalFailed( - address(strategy), - withdrawableAssets - ); - } - } + try + strategy.withdraw( + withdrawableAssets, + address(this), + address(this) + ) + { + float += withdrawableAssets; + } catch { + emit StrategyWithdrawalFailed( + address(strategy), + withdrawableAssets + ); + } } } } @@ -544,7 +472,6 @@ contract MultiStrategyVault is error InvalidIndex(); error InvalidWithdrawalQueue(); error NotPassedQuitPeriod(uint256 quitPeriod_); - error InvalidExtraData(); function getStrategies() external view returns (IERC4626[] memory) { return strategies; @@ -731,30 +658,17 @@ contract MultiStrategyVault is /** * @notice Pull funds out of strategies to be reallocated into different strategies. Caller must be Owner. * @param allocations An array of structs each including the strategyIndex to withdraw from and the amount of assets - * @param extraData An array of encoded bytes to pass down to strategy for extra-logic, if present */ - function pullFunds(Allocation[] calldata allocations, bytes[] calldata extraData) external onlyOwner { + function pullFunds(Allocation[] calldata allocations) external onlyOwner { uint256 len = allocations.length; - - if(extraData.length != len) - revert InvalidExtraData(); for (uint256 i; i < len; i++) { if (allocations[i].amount > 0) { - if(extraData[i].length > 0) { - IStrategyWithData(address(strategies[allocations[i].index])).withdrawWithData( - allocations[i].amount, - address(this), - address(this), - extraData[i] - ); - } else { - strategies[allocations[i].index].withdraw( - allocations[i].amount, - address(this), - address(this) - ); - } + strategies[allocations[i].index].withdraw( + allocations[i].amount, + address(this), + address(this) + ); } } } From 7c6dbe47861edc3198cd756dde7ecff2f841e0a1 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 18 Sep 2024 11:34:28 +0200 Subject: [PATCH 05/10] Add base leverage strategy - refactor MaticX looper --- script/deploy/stader/MaticXLooper.s.sol | 23 +- .../stader/MaticXLooperDeployConfig.json | 17 +- src/strategies/BaseAaveLeverageStrategy.sol | 584 +++++++++++++++ src/strategies/stader/MaticXLooper.sol | 671 ++---------------- test/strategies/stader/MaticXLooper.t.sol | 80 ++- .../stader/MaticXLooperTestConfig.json | 13 +- 6 files changed, 714 insertions(+), 674 deletions(-) create mode 100644 src/strategies/BaseAaveLeverageStrategy.sol diff --git a/script/deploy/stader/MaticXLooper.s.sol b/script/deploy/stader/MaticXLooper.s.sol index 1f566f87..99e7129e 100644 --- a/script/deploy/stader/MaticXLooper.s.sol +++ b/script/deploy/stader/MaticXLooper.s.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.25; import {Script, console} from "forge-std/Script.sol"; import {stdJson} from "forge-std/StdJson.sol"; -import {MaticXLooper, LooperInitValues, IERC20} from "../../../src/strategies/stader/MaticXLooper.sol"; +import {MaticXLooper, LooperValues, LooperBaseValues, IERC20} from "../../../src/strategies/stader/MaticXLooper.sol"; contract DeployStrategy is Script { using stdJson for string; @@ -25,9 +25,14 @@ contract DeployStrategy is Script { // Deploy Strategy strategy = new MaticXLooper(); - LooperInitValues memory looperValues = abi.decode( - json.parseRaw(".strategyInit"), - (LooperInitValues) + LooperBaseValues memory baseValues = abi.decode( + json.parseRaw(".baseLeverage"), + (LooperBaseValues) + ); + + LooperValues memory looperInitValues = abi.decode( + json.parseRaw(".strategy"), + (LooperValues) ); address asset = json.readAddress(".baseInit.asset"); @@ -37,14 +42,8 @@ contract DeployStrategy is Script { json.readAddress(".baseInit.owner"), json.readBool(".baseInit.autoDeposit"), abi.encode( - looperValues.aaveDataProvider, - looperValues.balancerVault, - looperValues.maticXPool, - looperValues.maxLTV, - looperValues.poolAddressesProvider, - looperValues.poolId, - looperValues.slippage, - looperValues.targetLTV + baseValues, + looperInitValues ) ); diff --git a/script/deploy/stader/MaticXLooperDeployConfig.json b/script/deploy/stader/MaticXLooperDeployConfig.json index b8397a50..1b15618b 100644 --- a/script/deploy/stader/MaticXLooperDeployConfig.json +++ b/script/deploy/stader/MaticXLooperDeployConfig.json @@ -4,15 +4,18 @@ "owner": "", "autoDeposit": false }, - "strategyInit": { + "baseLeverage": { "aaveDataProvider": "0x7deEB8aCE4220643D8edeC871a23807E4d006eE5", - "balancerVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", - "maticXPool": "0xfd225C9e6601C9d38d8F98d8731BF59eFcF8C0E3", - "maxLTV": 850000000000000000, + "borrowAsset": "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", + "maxLTV": 900000000000000000, + "maxSlippage": 5000000000000000, "poolAddressesProvider": "0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb", - "poolId": "0xcd78a20c597e367a4e478a2411ceb790604d7c8f000000000000000000000c22", - "slippage": 10000000000000000, "targetLTV": 800000000000000000 + }, + "strategy": { + "balancerVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + "maticXPool": "0xfd225C9e6601C9d38d8F98d8731BF59eFcF8C0E3", + "poolId": "0xcd78a20c597e367a4e478a2411ceb790604d7c8f000000000000000000000c22" } - } +} \ No newline at end of file diff --git a/src/strategies/BaseAaveLeverageStrategy.sol b/src/strategies/BaseAaveLeverageStrategy.sol new file mode 100644 index 00000000..d8e34e5a --- /dev/null +++ b/src/strategies/BaseAaveLeverageStrategy.sol @@ -0,0 +1,584 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {BaseStrategy, IERC20, IERC20Metadata, SafeERC20, ERC20, Math} from "src/strategies/BaseStrategy.sol"; +import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; +import {ILendingPool, IAToken, IFlashLoanReceiver, IProtocolDataProvider, IPoolAddressesProvider, DataTypes} from "src/interfaces/external/aave/IAaveV3.sol"; + +struct LooperBaseValues { + address aaveDataProvider; + address borrowAsset; // asset to borrow (ie WETH - wMATIC) + uint256 maxLTV; + uint256 maxSlippage; + address poolAddressesProvider; + uint256 targetLTV; +} + +struct FlashLoanCache { + bool isWithdraw; + bool isFullWithdraw; + uint256 assetsToWithdraw; + uint256 depositAmount; + uint256 slippage; +} + +abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { + using SafeERC20 for IERC20; + using Math for uint256; + + string internal _name; + string internal _symbol; + + ILendingPool public lendingPool; // aave router + IPoolAddressesProvider public poolAddressesProvider; // aave pool provider + + IERC20 public borrowAsset; // asset to borrow + IERC20 public debtToken; // aave debt token + IERC20 public interestToken; // aave deposit token + + uint256 public slippage; // 1e18 = 100% slippage, 1e14 = 1 BPS slippage + uint256 public targetLTV; // in 18 decimals - 1e17 being 0.1% + uint256 public maxLTV; // max ltv the vault can reach + uint256 public protocolMaxLTV; // underlying money market max LTV + + error InvalidLTV(uint256 targetLTV, uint256 maxLTV, uint256 protocolLTV); + error InvalidSlippage(uint256 slippage, uint256 slippageCap); + error BadLTV(uint256 currentLTV, uint256 maxLTV); + + /** + * @notice Initialize a new Strategy. + * @param asset_ The underlying asset used for deposit/withdraw and accounting + * @param owner_ Owner of the contract. Controls management functions. + * @param autoDeposit_ Controls if `protocolDeposit` gets called on deposit + * @param initValues Encoded data for this specific strategy + */ + function __BaseLeverageStrategy_init( + address asset_, + address owner_, + bool autoDeposit_, + LooperBaseValues memory initValues + ) internal onlyInitializing { + __BaseStrategy_init(asset_, owner_, autoDeposit_); + + // retrieve and set deposit aToken, lending pool + (address _aToken, , ) = IProtocolDataProvider( + initValues.aaveDataProvider + ).getReserveTokensAddresses(asset_); + interestToken = IERC20(_aToken); + lendingPool = ILendingPool(IAToken(_aToken).POOL()); + + // set efficiency mode + _setEfficiencyMode(); + + protocolMaxLTV = _getMaxLTV(); + + // check ltv init values are correct + _verifyLTV(initValues.targetLTV, initValues.maxLTV, protocolMaxLTV); + + targetLTV = initValues.targetLTV; + maxLTV = initValues.maxLTV; + + poolAddressesProvider = IPoolAddressesProvider( + initValues.poolAddressesProvider + ); + + // retrieve and set aave variable debt token + borrowAsset = IERC20(initValues.borrowAsset); + (, , address _variableDebtToken) = IProtocolDataProvider( + initValues.aaveDataProvider + ).getReserveTokensAddresses(address(borrowAsset)); + + debtToken = IERC20(_variableDebtToken); // variable debt token + + _name = string.concat( + "VaultCraft Leveraged ", + IERC20Metadata(asset_).name(), + " Strategy" + ); + _symbol = string.concat("vc-", IERC20Metadata(asset_).symbol()); + + // approve aave router to pull asset + IERC20(asset_).approve(address(lendingPool), type(uint256).max); + + // approve aave pool to pull borrow asset as part of a flash loan + IERC20(address(borrowAsset)).approve( + address(lendingPool), + type(uint256).max + ); + + // set slippage + if (initValues.maxSlippage > 2e17) { + revert InvalidSlippage(initValues.maxSlippage, 2e17); + } + + slippage = initValues.maxSlippage; + } + + receive() external payable {} + + function name() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _name; + } + + function symbol() + public + view + override(IERC20Metadata, ERC20) + returns (string memory) + { + return _symbol; + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + function _totalAssets() internal view override returns (uint256) { + // get value of debt in collateral tokens + uint256 debtValue = _toCollateralValue( + debtToken.balanceOf(address(this)) + ); + + // get collateral amount + uint256 collateral = interestToken.balanceOf(address(this)); + + if (debtValue >= collateral) return 0; + + uint256 total = collateral; + if (debtValue > 0) { + total -= debtValue; + + // if there's debt, apply slippage to repay it + uint256 slippageDebt = debtValue.mulDiv( + slippage, + 1e18, + Math.Rounding.Ceil + ); + + if (slippageDebt >= total) return 0; + + total -= slippageDebt; + } + if (total > 0) return total - 1; + else return 0; + } + + function getLTV() public view returns (uint256 ltv) { + (ltv, , ) = _getCurrentLTV(); + } + + /*////////////////////////////////////////////////////////////// + MANAGEMENT LOGIC + //////////////////////////////////////////////////////////////*/ + function adjustLeverage() public { + // get vault current leverage : debt/collateral + ( + uint256 currentLTV, + uint256 currentDebt, + uint256 currentCollateral + ) = _getCurrentLTV(); + + if (currentLTV > targetLTV) { + // de-leverage if vault LTV is higher than target + uint256 borrowAmount = (currentDebt - + ( + targetLTV.mulDiv( + (currentCollateral), + 1e18, + Math.Rounding.Floor + ) + )).mulDiv(1e18, (1e18 - targetLTV), Math.Rounding.Ceil); + + // flash loan debt asset to repay part of the debt + _flashLoan(borrowAmount, 0, 0, 0, false, slippage); + } else { + uint256 depositAmount = (targetLTV.mulDiv( + currentCollateral, + 1e18, + Math.Rounding.Ceil + ) - currentDebt).mulDiv( + 1e18, + (1e18 - targetLTV), + Math.Rounding.Ceil + ); + + uint256 dustBalance = address(this).balance; + + if (dustBalance < depositAmount) { + // flashloan but use eventual collateral dust remained in the contract as well + uint256 borrowAmount = depositAmount - dustBalance; + + // flash loan debt asset from lending protocol and add to cdp - slippage not used in this case, pass 0 + _flashLoan(borrowAmount, depositAmount, 0, 2, false, 0); + } else { + // deposit the dust as collateral- borrow amount is zero + // leverage naturally decreases + _redepositAsset(0, dustBalance, asset()); + } + } + + // reverts if LTV got above max + _assertHealthyLTV(); + } + + function harvest(bytes memory) external override onlyKeeperOrOwner { + adjustLeverage(); + + emit Harvested(); + } + + function setHarvestValues(bytes memory harvestValues) external onlyOwner { + _setHarvestValues(harvestValues); + } + + function setLeverageValues( + uint256 targetLTV_, + uint256 maxLTV_ + ) external onlyOwner { + // reverts if targetLTV < maxLTV < protocolLTV is not satisfied + _verifyLTV(targetLTV_, maxLTV_, protocolMaxLTV); + + targetLTV = targetLTV_; + maxLTV = maxLTV_; + + adjustLeverage(); + } + + function setSlippage(uint256 slippage_) external onlyOwner { + if (slippage_ > 2e17) revert InvalidSlippage(slippage_, 2e17); + + slippage = slippage_; + } + + bool internal initCollateral; + + function setUserUseReserveAsCollateral(uint256 amount) external onlyOwner { + if (initCollateral) revert InvalidInitialization(); + address asset_ = asset(); + + IERC20(asset_).safeTransferFrom(msg.sender, address(this), amount); + lendingPool.supply(asset_, amount, address(this), 0); + + lendingPool.setUserUseReserveAsCollateral(asset_, true); + + initCollateral = true; + } + + function withdrawDust(address recipient) public onlyOwner { + _withdrawDust(recipient); + } + + /*////////////////////////////////////////////////////////////// + FLASH LOAN LOGIC + //////////////////////////////////////////////////////////////*/ + + error NotFlashLoan(); + + function ADDRESSES_PROVIDER() + external + view + returns (IPoolAddressesProvider) + { + return poolAddressesProvider; + } + + function POOL() external view returns (ILendingPool) { + return lendingPool; + } + + // this is triggered after the flash loan is given, ie contract has loaned assets at this point + function executeOperation( + address[] calldata, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata params + ) external override returns (bool) { + if (initiator != address(this) || msg.sender != address(lendingPool)) { + revert NotFlashLoan(); + } + + FlashLoanCache memory cache = abi.decode(params, (FlashLoanCache)); + + if (cache.isWithdraw) { + // flash loan is to repay debt as part of a withdrawal + uint256 flashLoanDebt = amounts[0] + premiums[0]; + + // repay cdp debt + lendingPool.repay( + address(borrowAsset), + amounts[0], + 2, + address(this) + ); + + // withdraw collateral, swap, repay flashloan + _reduceLeverage( + cache.isFullWithdraw, + asset(), + cache.assetsToWithdraw, + flashLoanDebt, + cache.slippage + ); + } else { + // flash loan is to leverage UP + _redepositAsset(amounts[0], cache.depositAmount, asset()); + } + + return true; + } + + // borrow asset from lending protocol + // interestRateMode = 2 -> flash loan asset and deposit into cdp, don't repay + // interestRateMode = 0 -> flash loan asset to repay cdp, have to repay flash loan at the end + function _flashLoan( + uint256 borrowAmount, + uint256 depositAmount, + uint256 assetsToWithdraw, + uint256 interestRateMode, + bool isFullWithdraw, + uint256 slippage + ) internal { + // uint256 depositAmount_ = depositAmount; // avoids stack too deep + FlashLoanCache memory cache = FlashLoanCache( + interestRateMode == 0, + isFullWithdraw, + assetsToWithdraw, + depositAmount, + slippage + ); + + address[] memory assets = new address[](1); + assets[0] = address(borrowAsset); + + uint256[] memory amounts = new uint256[](1); + amounts[0] = borrowAmount; + + uint256[] memory interestRateModes = new uint256[](1); + interestRateModes[0] = interestRateMode; + + lendingPool.flashLoan( + address(this), + assets, + amounts, + interestRateModes, + address(this), + abi.encode(cache), + 0 + ); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HOOKS LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Deposit asset into lending protocol - receive aToken here + function _protocolDeposit( + uint256 assets, + uint256, + bytes memory + ) internal override { + lendingPool.supply(asset(), assets, address(this), 0); + } + + /// @notice repay part of the vault debt if necessary and withdraw asset + function _protocolWithdraw( + uint256 assets, + uint256, + bytes memory extraData + ) internal override { + ( + , + uint256 totalDebt, + uint256 totalCollateralDebtValue + ) = _getCurrentLTV(); + + uint256 assetsDebtValue = _toDebtValue(assets); + + bool isFullWithdraw; + uint256 ratioDebtToRepay; + + { + uint256 debtSlippage = totalDebt.mulDiv( + slippage, + 1e18, + Math.Rounding.Ceil + ); + + // find the % of debt to repay as the % of collateral being withdrawn + ratioDebtToRepay = assetsDebtValue.mulDiv( + 1e18, + (totalCollateralDebtValue - totalDebt - debtSlippage), + Math.Rounding.Floor + ); + + isFullWithdraw = + assets == _totalAssets() || + ratioDebtToRepay >= 1e18; + } + + // get the LTV we would have without repaying debt + uint256 futureLTV = isFullWithdraw + ? type(uint256).max + : totalDebt.mulDiv( + 1e18, + (totalCollateralDebtValue - assetsDebtValue), + Math.Rounding.Floor + ); + + if (futureLTV <= maxLTV || totalDebt == 0) { + // 1 - withdraw any asset amount with no debt + // 2 - withdraw assets with debt but the change doesn't take LTV above max + lendingPool.withdraw(asset(), assets, address(this)); + } else { + // 1 - withdraw assets but repay debt + uint256 debtToRepay = isFullWithdraw + ? totalDebt + : totalDebt.mulDiv(ratioDebtToRepay, 1e18, Math.Rounding.Floor); + + // flash loan debtToRepay - mode 0 - flash loan is repaid at the end + _flashLoan(debtToRepay, 0, assets, 0, isFullWithdraw, slippage); + } + + // reverts if LTV got above max + _assertHealthyLTV(); + } + + ///@notice called after a flash loan to repay cdp + function _reduceLeverage( + bool isFullWithdraw, + address asset, + uint256 toWithdraw, + uint256 flashLoanDebt, + uint256 slippage + ) internal { + // get flash loan amount converted in collateral value + uint256 flashLoanCollateralValue = _toCollateralValue(flashLoanDebt); + + // get slippage buffer for swapping and repaying flashLoanDebt + uint256 swapBuffer = flashLoanCollateralValue.mulDiv( + slippage, + 1e18, + Math.Rounding.Floor + ); + + // if the withdraw amount with buffer is greater than total assets withdraw all + if ( + flashLoanCollateralValue + swapBuffer + toWithdraw >= _totalAssets() + ) isFullWithdraw = true; + + if (isFullWithdraw) { + // withdraw all + lendingPool.withdraw(asset, type(uint256).max, address(this)); + } else { + lendingPool.withdraw( + asset, + flashLoanCollateralValue + swapBuffer + toWithdraw, + address(this) + ); + } + + // swap collateral to exact debt asset - will be pulled by AAVE pool as flash loan repayment + _convertCollateralToDebt( + flashLoanCollateralValue + swapBuffer, + flashLoanDebt, + asset + ); + } + + // deposit back into the protocol + // either from flash loan or simply collateral dust held by the strategy + function _redepositAsset( + uint256 borrowAmount, + uint256 totCollateralAmount, + address asset + ) internal { + // use borrow asset to get more collateral + _convertDebtToCollateral(borrowAmount, totCollateralAmount); // TODO improve + + // deposit collateral balance into lending protocol + // may include eventual dust held by contract somehow + // in that case it will just add more collateral + _protocolDeposit(IERC20(asset).balanceOf(address(this)), 0, hex""); + } + + // returns current loan to value, + // debt and collateral amounts in debt value + function _getCurrentLTV() + internal + view + returns (uint256 loanToValue, uint256 debt, uint256 collateral) + { + debt = debtToken.balanceOf(address(this)); // debt + collateral = _toDebtValue(interestToken.balanceOf(address(this))); // collateral converted into debt amount; + + (debt == 0 || collateral == 0) ? loanToValue = 0 : loanToValue = debt + .mulDiv(1e18, collateral, Math.Rounding.Ceil); + } + + // reverts if targetLTV < maxLTV < protocolLTV is not satisfied + function _verifyLTV( + uint256 _targetLTV, + uint256 _maxLTV, + uint256 _protocolLTV + ) internal pure { + if (_targetLTV >= _maxLTV || _maxLTV >= _protocolLTV) { + revert InvalidLTV(_targetLTV, _maxLTV, _protocolLTV); + } + } + + // verify that currentLTV is not above maxLTV + function _assertHealthyLTV() internal view { + (uint256 currentLTV, , ) = _getCurrentLTV(); + + if (currentLTV > maxLTV) { + revert BadLTV(currentLTV, maxLTV); + } + } + + /*////////////////////////////////////////////////////////////// + TO OVERRIDE IN IMPLEMENTATION + //////////////////////////////////////////////////////////////*/ + + // must provide conversion from debt asset to vault (collateral) asset + function _toCollateralValue( + uint256 debtAmount + ) internal view virtual returns (uint256 collateralAmount); + + // must provide conversion from vault (collateral) asset to debt asset + function _toDebtValue( + uint256 collateralAmount + ) internal view virtual returns (uint256 debtAmount); + + // must provide logic to go from collateral to debt assets + function _convertCollateralToDebt( + uint256 maxCollateralIn, + uint256 exactDebtAmont, + address asset + ) internal virtual; + + // must provide logic to use borrowed debt assets to get collateral + function _convertDebtToCollateral( + uint256 debtAmount, + uint256 totCollateralAmount + ) internal virtual; + + // must provide logic to decode and assign harvest values + function _setHarvestValues(bytes memory harvestValues) internal virtual; + + // must provide logic to set the efficiency mode on aave + function _setEfficiencyMode() internal virtual; + + // must provide logic to retrieve the money market max ltv + function _getMaxLTV() internal virtual returns (uint256 protocolMaxLTV); + + // must provide logic to withdraw dust assets + function _withdrawDust(address recipient) internal virtual; +} diff --git a/src/strategies/stader/MaticXLooper.sol b/src/strategies/stader/MaticXLooper.sol index e800674e..8fc94a8d 100644 --- a/src/strategies/stader/MaticXLooper.sol +++ b/src/strategies/stader/MaticXLooper.sol @@ -3,18 +3,9 @@ pragma solidity ^0.8.25; -import {BaseStrategy, IERC20, IERC20Metadata, SafeERC20, ERC20, Math} from "src/strategies/BaseStrategy.sol"; +import {BaseAaveLeverageStrategy, LooperBaseValues, DataTypes, IERC20} from "src/strategies/BaseAaveLeverageStrategy.sol"; import {IMaticXPool} from "./IMaticX.sol"; -import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; import {IWETH as IWMatic} from "src/interfaces/external/IWETH.sol"; -import { - ILendingPool, - IAToken, - IFlashLoanReceiver, - IProtocolDataProvider, - IPoolAddressesProvider, - DataTypes -} from "src/interfaces/external/aave/IAaveV3.sol"; import { IBalancerVault, SwapKind, @@ -22,551 +13,77 @@ import { FundManagement } from "src/interfaces/external/balancer/IBalancer.sol"; - -struct LooperInitValues { - address aaveDataProvider; +struct LooperValues { address balancerVault; address maticXPool; - uint256 maxLTV; - address poolAddressesProvider; bytes32 poolId; - uint256 slippage; - uint256 targetLTV; } -/// @title Leveraged maticX yield adapter -/// @author Vaultcraft -/// @notice ERC4626 wrapper for leveraging maticX yield -/// @dev The strategy takes MaticX and deposits it into a lending protocol (aave). -/// Then it borrows Matic, swap for MaticX and redeposits it -contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { - // using FixedPointMathLib for uint256; - using SafeERC20 for IERC20; - using Math for uint256; - - string internal _name; - string internal _symbol; - - // address of the aave/spark router - ILendingPool public lendingPool; - IPoolAddressesProvider public poolAddressesProvider; - IAaveIncentives public aaveIncentives; - - IWMatic public constant wMatic = - IWMatic(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270); // wmatic borrow asset +contract MaticXLooper is BaseAaveLeverageStrategy { IMaticXPool public maticXPool; // stader pool for wrapping - converting - IERC20 public debtToken; // aave wmatic debt token - IERC20 public interestToken; // aave MaticX - + // swap logic IBalancerVault public balancerVault; bytes32 public balancerPoolId; - uint256 public slippage; // 1e18 = 100% slippage, 1e14 = 1 BPS slippage - - uint256 public targetLTV; // in 18 decimals - 1e17 being 0.1% - uint256 public maxLTV; // max ltv the vault can reach - uint256 public protocolMaxLTV; // underlying money market max LTV - - error InvalidLTV(uint256 targetLTV, uint256 maxLTV, uint256 protocolLTV); - error InvalidSlippage(uint256 slippage, uint256 slippageCap); - error BadLTV(uint256 currentLTV, uint256 maxLTV); - /*////////////////////////////////////////////////////////////// - INITIALIZATION - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Initialize a new Strategy. - * @param asset_ The underlying asset used for deposit/withdraw and accounting - * @param owner_ Owner of the contract. Controls management functions. - * @param autoDeposit_ Controls if `protocolDeposit` gets called on deposit - * @param strategyInitData_ Encoded data for this specific strategy - */ - function initialize( - address asset_, - address owner_, - bool autoDeposit_, - bytes memory strategyInitData_ - ) public initializer { - __BaseStrategy_init(asset_, owner_, autoDeposit_); - - LooperInitValues memory initValues = abi.decode( - strategyInitData_, - (LooperInitValues) - ); - - maticXPool = IMaticXPool(initValues.maticXPool); - - // retrieve and set maticX aToken, lending pool - (address _aToken, , ) = IProtocolDataProvider( - initValues.aaveDataProvider - ).getReserveTokensAddresses(asset_); - interestToken = IERC20(_aToken); - lendingPool = ILendingPool(IAToken(_aToken).POOL()); - aaveIncentives = IAaveIncentives( - IAToken(_aToken).getIncentivesController() - ); - - // set efficiency mode - Matic correlated - lendingPool.setUserEMode(uint8(2)); - - // get protocol LTV - DataTypes.EModeData memory emodeData = lendingPool.getEModeCategoryData( - uint8(1) - ); - protocolMaxLTV = uint256(emodeData.maxLTV) * 1e14; // make it 18 decimals to compare; + function initialize(address asset_, address owner_, bool autoDeposit_, bytes memory strategyInitData_) public initializer{ + (LooperBaseValues memory baseValues, LooperValues memory strategyValues) = abi.decode(strategyInitData_, (LooperBaseValues, LooperValues)); - // check ltv init values are correct - _verifyLTV(initValues.targetLTV, initValues.maxLTV, protocolMaxLTV); + // init base leverage strategy + __BaseLeverageStrategy_init(asset_, owner_, autoDeposit_, baseValues); - targetLTV = initValues.targetLTV; - maxLTV = initValues.maxLTV; + // maticX pool as oracle + maticXPool = IMaticXPool(strategyValues.maticXPool); - poolAddressesProvider = IPoolAddressesProvider( - initValues.poolAddressesProvider - ); - - // retrieve and set wMatic variable debt token - (, , address _variableDebtToken) = IProtocolDataProvider( - initValues.aaveDataProvider - ).getReserveTokensAddresses(address(wMatic)); - - debtToken = IERC20(_variableDebtToken); // variable debt wMatic token - - _name = string.concat( - "VaultCraft Leveraged ", - IERC20Metadata(asset_).name(), - " Adapter" - ); - _symbol = string.concat("vc-", IERC20Metadata(asset_).symbol()); - - // approve aave router to pull maticX - IERC20(asset_).approve(address(lendingPool), type(uint256).max); - - // approve aave pool to pull wMatic as part of a flash loan - IERC20(address(wMatic)).approve( - address(lendingPool), - type(uint256).max - ); - - balancerPoolId = initValues.poolId; - - // approve balancer vault to trade MaticX - balancerVault = IBalancerVault(initValues.balancerVault); + // balancer - swap logic + balancerPoolId = strategyValues.poolId; + balancerVault = IBalancerVault(strategyValues.balancerVault); IERC20(asset_).approve(address(balancerVault), type(uint256).max); - - // set slippage - if (initValues.slippage > 2e17) { - revert InvalidSlippage(initValues.slippage, 2e17); - } - - slippage = initValues.slippage; - } - - receive() external payable {} - - function name() - public - view - override(IERC20Metadata, ERC20) - returns (string memory) - { - return _name; - } - - function symbol() - public - view - override(IERC20Metadata, ERC20) - returns (string memory) - { - return _symbol; - } - - /*////////////////////////////////////////////////////////////// - ACCOUNTING LOGIC - //////////////////////////////////////////////////////////////*/ - function _totalAssets() internal view override returns (uint256) { - (uint256 debt, , ) = maticXPool.convertMaticToMaticX( - debtToken.balanceOf(address(this)) - ); // matic debt converted in maticX amount - uint256 collateral = interestToken.balanceOf(address(this)); // maticX collateral - - if (debt >= collateral) return 0; - - uint256 total = collateral; - if (debt > 0) { - total -= debt; - - // if there's debt, apply slippage to repay it - uint256 slippageDebt = debt.mulDiv( - slippage, - 1e18, - Math.Rounding.Ceil - ); - - if (slippageDebt >= total) return 0; - - total -= slippageDebt; - } - if (total > 0) return total - 1; - else return 0; - } - - function getLTV() public view returns (uint256 ltv) { - (ltv, , ) = _getCurrentLTV(); - } - - /// @notice The token rewarded if the aave liquidity mining is active - function rewardTokens() external view override returns (address[] memory) { - return aaveIncentives.getRewardsByAsset(asset()); - } - - function convertToUnderlyingShares( - uint256 assets, - uint256 shares - ) public view override returns (uint256) { - revert(); } - /*////////////////////////////////////////////////////////////// - FLASH LOAN LOGIC - //////////////////////////////////////////////////////////////*/ - - error NotFlashLoan(); - - function ADDRESSES_PROVIDER() - external - view - returns (IPoolAddressesProvider) - { - return poolAddressesProvider; + // provides conversion from matic to maticX + function _toCollateralValue(uint256 maticAmount) internal view override returns (uint256 maticXAmount) { + (maticXAmount,,) = maticXPool.convertMaticToMaticX(maticAmount); } - function POOL() external view returns (ILendingPool) { - return lendingPool; + // provides conversion from maticX to matic + function _toDebtValue(uint256 maticXAmount) internal view override returns (uint256 maticAmount) { + (maticAmount,,) + = maticXPool.convertMaticXToMatic(maticXAmount); } - // this is triggered after the flash loan is given, ie contract has loaned assets at this point - function executeOperation( - address[] calldata, - uint256[] calldata amounts, - uint256[] calldata premiums, - address initiator, - bytes calldata params - ) external override returns (bool) { - if (initiator != address(this) || msg.sender != address(lendingPool)) { - revert NotFlashLoan(); - } - - (bool isWithdraw, bool isFullWithdraw, uint256 assetsToWithdraw, uint256 depositAmount, uint256 chosenSlippage) = - abi.decode(params, (bool, bool, uint256, uint256, uint256)); - - if (isWithdraw) { - // flash loan is to repay Matic debt as part of a withdrawal - uint256 flashLoanDebt = amounts[0] + premiums[0]; - - // repay cdp wMatic debt - lendingPool.repay(address(wMatic), amounts[0], 2, address(this)); - - // withdraw collateral, swap, repay flashloan - _reduceLeverage(isFullWithdraw, assetsToWithdraw, flashLoanDebt, chosenSlippage); - } else { - // flash loan is to leverage UP - _redepositAsset(amounts[0], depositAmount); - } - - return true; - } - - /*////////////////////////////////////////////////////////////// - IStrategyWithData LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @notice user can pass a preferred slippage %, which will be used against the default one - /// @notice surplus of assets released is then transferred - function _protocolWithdrawWithData( - uint256 assets, - uint256 shares, - bytes memory extraData - ) internal override returns (uint256 extraAmountToTransfer) { - uint256 assetBalanceBefore = IERC20(asset()).balanceOf(address(this)); - - _protocolWithdraw(assets, shares, extraData); - - uint256 assetBalanceAfter = IERC20(asset()).balanceOf(address(this)); - - // calculate eventual surplus - if(assetBalanceAfter - assetBalanceBefore > assets) - extraAmountToTransfer = assetBalanceAfter - assetBalanceBefore - assets; - } - - /*////////////////////////////////////////////////////////////// - INTERNAL HOOKS LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @notice Deposit maticX into lending protocol - function _protocolDeposit( - uint256 assets, - uint256, - bytes memory - ) internal override { - // deposit maticX into aave - receive aToken here - lendingPool.supply(asset(), assets, address(this), 0); - } - - /// @notice repay part of the vault debt and withdraw maticX - function _protocolWithdraw(uint256 assets, uint256, bytes memory extraData) internal override { - (, uint256 currentDebt, uint256 currentCollateral) = _getCurrentLTV(); - (uint256 maticAssetsValue, , ) = maticXPool.convertMaticXToMatic( - assets - ); - - bool isFullWithdraw; - uint256 ratioDebtToRepay; - - // user can provide a desired slippage - uint256 chosenSlippage; - if(extraData.length > 0) - chosenSlippage = abi.decode(extraData, (uint256)); - - // user cannot provide a higher slippage than default - if (chosenSlippage > slippage || chosenSlippage == 0) - chosenSlippage = slippage; - - { - uint256 debtSlippage = currentDebt.mulDiv(chosenSlippage, 1e18, Math.Rounding.Ceil); - - // find the % of debt to repay as the % of collateral being withdrawn - ratioDebtToRepay = maticAssetsValue.mulDiv( - 1e18, - (currentCollateral - currentDebt - debtSlippage), - Math.Rounding.Floor - ); - - isFullWithdraw = - assets == _totalAssets() || - ratioDebtToRepay >= 1e18; - } - - // get the LTV we would have without repaying debt - uint256 futureLTV = isFullWithdraw - ? type(uint256).max - : currentDebt.mulDiv( - 1e18, - (currentCollateral - maticAssetsValue), - Math.Rounding.Floor - ); - - if (futureLTV <= maxLTV || currentDebt == 0) { - // 1 - withdraw any asset amount with no debt - // 2 - withdraw assets with debt but the change doesn't take LTV above max - lendingPool.withdraw(asset(), assets, address(this)); - } else { - // 1 - withdraw assets but repay debt - uint256 debtToRepay = isFullWithdraw - ? currentDebt - : currentDebt.mulDiv( - ratioDebtToRepay, - 1e18, - Math.Rounding.Floor - ); - - // flash loan debtToRepay - mode 0 - flash loan is repaid at the end - _flashLoanMatic(debtToRepay, 0, assets, 0, isFullWithdraw, chosenSlippage); - } - - // reverts if LTV got above max - _assertHealthyLTV(); - } - - // deposit back into the protocol - // either from flash loan or simply Matic dust held by the adapter - function _redepositAsset( - uint256 borrowAmount, - uint256 depositAmount - ) internal { - address maticX = asset(); - - if (borrowAmount > 0) { - // unwrap into Matic the flash loaned amount - wMatic.withdraw(borrowAmount); - } - - // stake borrowed matic and receive maticX - maticXPool.swapMaticForMaticXViaInstantPool{value: depositAmount}(); - - // get maticX balance after staking - // may include eventual maticX dust held by contract somehow - // in that case it will just add more collateral - uint256 maticXAmount = IERC20(maticX).balanceOf(address(this)); - - // deposit maticX into lending protocol - _protocolDeposit(maticXAmount, 0, hex""); - } - - // reduce leverage by withdrawing maticX, swapping to Matic repaying Matic debt - function _reduceLeverage( - bool isFullWithdraw, - uint256 toWithdraw, - uint256 flashLoanDebt, - uint256 chosenSlippage - ) internal { - address asset = asset(); - - // get flash loan amount converted in maticX - (uint256 flashLoanMaticXAmount, , ) = maticXPool.convertMaticToMaticX( - flashLoanDebt - ); - - // get slippage buffer for swapping with flashLoanDebt as minAmountOut - uint256 maticXBuffer = flashLoanMaticXAmount.mulDiv(chosenSlippage, 1e18, Math.Rounding.Floor); - - // if the withdraw amount with buffers to total assets withdraw all - if (flashLoanMaticXAmount + maticXBuffer + toWithdraw >= _totalAssets()) - isFullWithdraw = true; - - // withdraw maticX from aave - if (isFullWithdraw) { - // withdraw all - lendingPool.withdraw(asset, type(uint256).max, address(this)); - } else { - lendingPool.withdraw( - asset, - flashLoanMaticXAmount + maticXBuffer + toWithdraw, - address(this) - ); - } - - // swap maticX to exact wMatic on Balancer - will be pulled by AAVE pool as flash loan repayment - _swapToWMatic( - flashLoanMaticXAmount + maticXBuffer, - flashLoanDebt, - asset - ); - } - - // returns current loan to value, debt and collateral (token) amounts - function _getCurrentLTV() - internal - view - returns (uint256 loanToValue, uint256 debt, uint256 collateral) - { - debt = debtToken.balanceOf(address(this)); // wmatic DEBT - (collateral, , ) = maticXPool.convertMaticXToMatic( - interestToken.balanceOf(address(this)) - ); // converted into Matic amount; - - (debt == 0 || collateral == 0) ? loanToValue = 0 : loanToValue = debt - .mulDiv(1e18, collateral, Math.Rounding.Ceil); - } - - // reverts if targetLTV < maxLTV < protocolLTV is not satisfied - function _verifyLTV( - uint256 _targetLTV, - uint256 _maxLTV, - uint256 _protocolLTV - ) internal pure { - if (_targetLTV >= _maxLTV) { - revert InvalidLTV(_targetLTV, _maxLTV, _protocolLTV); - } - if (_maxLTV >= _protocolLTV) { - revert InvalidLTV(_targetLTV, _maxLTV, _protocolLTV); - } - } - - // verify that currentLTV is not above maxLTV - function _assertHealthyLTV() internal view { - (uint256 currentLTV, , ) = _getCurrentLTV(); - - if (currentLTV > maxLTV) { - revert BadLTV(currentLTV, maxLTV); - } - } - - // borrow wMatic from lending protocol - // interestRateMode = 2 -> flash loan matic and deposit into cdp, don't repay - // interestRateMode = 0 -> flash loan matic to repay cdp, have to repay flash loan at the end - function _flashLoanMatic( - uint256 borrowAmount, - uint256 depositAmount, - uint256 assetsToWithdraw, - uint256 interestRateMode, - bool isFullWithdraw, - uint256 chosenSlippage - ) internal { - uint256 depositAmount_ = depositAmount; // avoids stack too deep - - address[] memory assets = new address[](1); - assets[0] = address(wMatic); - - uint256[] memory amounts = new uint256[](1); - amounts[0] = borrowAmount; - - uint256[] memory interestRateModes = new uint256[](1); - interestRateModes[0] = interestRateMode; - - lendingPool.flashLoan( - address(this), - assets, - amounts, - interestRateModes, - address(this), - abi.encode(interestRateMode == 0 ? true : false, isFullWithdraw, assetsToWithdraw, depositAmount_, chosenSlippage), - 0 - ); - } - - // swaps MaticX to exact wMatic - function _swapToWMatic( - uint256 maxAmountIn, - uint256 exactAmountOut, - address asset - ) internal { + // swaps MaticX to exact wMatic + function _convertCollateralToDebt(uint256 maxCollateralIn, uint256 exactDebtAmont, address asset) internal override { SingleSwap memory swap = SingleSwap( balancerPoolId, SwapKind.GIVEN_OUT, asset, - address(wMatic), - exactAmountOut, + address(borrowAsset), + exactDebtAmont, hex"" ); balancerVault.swap( swap, FundManagement(address(this), false, payable(address(this)), false), - maxAmountIn, + maxCollateralIn, block.timestamp ); } - /*////////////////////////////////////////////////////////////// - MANAGEMENT LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @notice Claim additional rewards given that it's active. - function claim() internal override returns (bool success) { - if (address(aaveIncentives) == address(0)) return false; + // unwrap wMatic and stakes into maticX + function _convertDebtToCollateral(uint256 debtAmount, uint256 totCollateralAmount) internal override { + if(debtAmount > 0) + IWMatic(address(borrowAsset)).withdraw(debtAmount); - address[] memory _assets = new address[](1); - _assets[0] = address(interestToken); - - try - aaveIncentives.claimAllRewardsOnBehalf( - _assets, - address(this), - address(this) - ) - { - success = true; - } catch {} + maticXPool.swapMaticForMaticXViaInstantPool{value: totCollateralAmount}(); } - function setHarvestValues( - address newBalancerVault, - bytes32 newBalancerPoolId - ) external onlyOwner { - if (newBalancerVault != address(balancerVault)) { + // assign balancer data for swaps + function _setHarvestValues(bytes memory harvestValues) internal override { + (address newBalancerVault, bytes32 newBalancerPoolId) = abi.decode(harvestValues, (address, bytes32)); + + if(newBalancerVault != address(balancerVault)) { address asset_ = asset(); // reset old pool @@ -581,83 +98,20 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { balancerPoolId = newBalancerPoolId; } - function harvest(bytes memory data) external override onlyKeeperOrOwner { - claim(); - - address[] memory _rewardTokens = aaveIncentives.getRewardsByAsset( - asset() - ); - - for (uint256 i; i < _rewardTokens.length; i++) { - uint256 balance = IERC20(_rewardTokens[i]).balanceOf(address(this)); - if (balance > 0) { - IERC20(_rewardTokens[i]).transfer(msg.sender, balance); - } - } - - uint256 assetAmount = abi.decode(data, (uint256)); - - if (assetAmount == 0) revert ZeroAmount(); - - IERC20(asset()).transferFrom(msg.sender, address(this), assetAmount); - - _protocolDeposit(assetAmount, 0, bytes("")); - - emit Harvested(); + function _setEfficiencyMode() internal override { + // Matic correlated + lendingPool.setUserEMode(uint8(2)); } - - // amount of wMatic to borrow OR amount of wMatic to repay (converted into maticX amount internally) - function adjustLeverage() public { - // get vault current leverage : debt/collateral - ( - uint256 currentLTV, - uint256 currentDebt, - uint256 currentCollateral - ) = _getCurrentLTV(); - - // de-leverage if vault LTV is higher than target - if (currentLTV > targetLTV) { - uint256 amountMatic = (currentDebt - - ( - targetLTV.mulDiv( - (currentCollateral), - 1e18, - Math.Rounding.Floor - ) - )).mulDiv(1e18, (1e18 - targetLTV), Math.Rounding.Ceil); - - // flash loan matic to repay part of the debt - use default slippage - _flashLoanMatic(amountMatic, 0, 0, 0, false, slippage); - } else { - uint256 amountMatic = (targetLTV.mulDiv( - currentCollateral, - 1e18, - Math.Rounding.Ceil - ) - currentDebt).mulDiv( - 1e18, - (1e18 - targetLTV), - Math.Rounding.Ceil - ); - - uint256 dustBalance = address(this).balance; - if (dustBalance < amountMatic) { - // flashloan but use eventual Matic dust remained in the contract as well - uint256 borrowAmount = amountMatic - dustBalance; - - // flash loan wMatic from lending protocol and add to cdp - slippage not used in this case, pass 0 - _flashLoanMatic(borrowAmount, amountMatic, 0, 2, false, 0); - } else { - // deposit the dust as collateral- borrow amount is zero - // leverage naturally decreases - _redepositAsset(0, dustBalance); - } - } - - // reverts if LTV got above max - _assertHealthyLTV(); + + // reads max ltv on efficiency mode + function _getMaxLTV() internal override returns (uint256 protocolMaxLTV) { + // get protocol LTV + DataTypes.EModeData memory emodeData = lendingPool.getEModeCategoryData(uint8(2)); + protocolMaxLTV = uint256(emodeData.maxLTV) * 1e14; // make it 18 decimals to compare; } - function withdrawDust(address recipient) public onlyOwner { + + function _withdrawDust(address recipient) internal override { // send matic dust to recipient uint256 maticBalance = address(this).balance; if (maticBalance > 0) { @@ -671,37 +125,4 @@ contract MaticXLooper is BaseStrategy, IFlashLoanReceiver { IERC20(asset()).transfer(recipient, maticXBalance); } } - - function setLeverageValues( - uint256 targetLTV_, - uint256 maxLTV_ - ) external onlyOwner { - // reverts if targetLTV < maxLTV < protocolLTV is not satisfied - _verifyLTV(targetLTV_, maxLTV_, protocolMaxLTV); - - targetLTV = targetLTV_; - maxLTV = maxLTV_; - - adjustLeverage(); - } - - function setSlippage(uint256 slippage_) external onlyOwner { - if (slippage_ > 2e17) revert InvalidSlippage(slippage_, 2e17); - - slippage = slippage_; - } - - bool internal initCollateral; - - function setUserUseReserveAsCollateral(uint256 amount) external onlyOwner { - if (initCollateral) revert InvalidInitialization(); - address asset_ = asset(); - - IERC20(asset_).safeTransferFrom(msg.sender, address(this), amount); - lendingPool.supply(asset_, amount, address(this), 0); - - lendingPool.setUserUseReserveAsCollateral(asset_, true); - - initCollateral = true; - } } diff --git a/test/strategies/stader/MaticXLooper.t.sol b/test/strategies/stader/MaticXLooper.t.sol index ed71bc45..7d781b86 100644 --- a/test/strategies/stader/MaticXLooper.t.sol +++ b/test/strategies/stader/MaticXLooper.t.sol @@ -3,8 +3,11 @@ pragma solidity ^0.8.25; -import {MaticXLooper, LooperInitValues, IERC20, IERC20Metadata, IMaticXPool, ILendingPool, Math} from "src/strategies/stader/MaticXLooper.sol"; +import {MaticXLooper, BaseAaveLeverageStrategy, LooperValues, LooperBaseValues, IERC20, IMaticXPool} from "src/strategies/stader/MaticXLooper.sol"; +import {IERC20Metadata, ILendingPool, Math} from "src/strategies/BaseAaveLeverageStrategy.sol"; + import {BaseStrategyTest, IBaseStrategy, TestConfig, stdJson, Math} from "../BaseStrategyTest.sol"; +import "forge-std/console.sol"; contract MaticXLooperTest is BaseStrategyTest { using stdJson for string; @@ -34,11 +37,18 @@ contract MaticXLooperTest is BaseStrategyTest { TestConfig memory testConfig_ ) internal override returns (IBaseStrategy) { // Read strategy init values - LooperInitValues memory looperInitValues = abi.decode( + LooperBaseValues memory baseValues = abi.decode( + json_.parseRaw( + string.concat(".configs[", index_, "].specific.base") + ), + (LooperBaseValues) + ); + + LooperValues memory looperInitValues = abi.decode( json_.parseRaw( string.concat(".configs[", index_, "].specific.init") ), - (LooperInitValues) + (LooperValues) ); // Deploy Strategy @@ -48,7 +58,7 @@ contract MaticXLooperTest is BaseStrategyTest { testConfig_.asset, address(this), true, - abi.encode(looperInitValues) + abi.encode(baseValues, looperInitValues) ); strategyContract = MaticXLooper(payable(strategy)); @@ -95,9 +105,16 @@ contract MaticXLooperTest is BaseStrategyTest { } function test__initialization() public override { - LooperInitValues memory looperInitValues = abi.decode( + LooperBaseValues memory baseValues = abi.decode( + json.parseRaw( + string.concat(".configs[0].specific.base") + ), + (LooperBaseValues) + ); + + LooperValues memory looperInitValues = abi.decode( json.parseRaw(string.concat(".configs[0].specific.init")), - (LooperInitValues) + (LooperValues) ); // Deploy Strategy @@ -107,7 +124,7 @@ contract MaticXLooperTest is BaseStrategyTest { testConfig.asset, address(this), true, - abi.encode(looperInitValues) + abi.encode(baseValues, looperInitValues) ); verify_adapterInit(); @@ -184,7 +201,7 @@ contract MaticXLooperTest is BaseStrategyTest { address newPool = address(0x85dE3ADd465a219EE25E04d22c39aB027cF5C12E); address asset = strategy.asset(); - strategyContract.setHarvestValues(newPool, poolId); + strategyContract.setHarvestValues(abi.encode(newPool, poolId)); uint256 oldAllowance = IERC20(asset).allowance( address(strategy), oldPool @@ -439,7 +456,11 @@ contract MaticXLooperTest is BaseStrategyTest { vm.stopPrank(); // check total assets - uint256 expDust = amountDeposit.mulDiv(slippage, 1e18, Math.Rounding.Floor); + uint256 expDust = amountDeposit.mulDiv( + slippage, + 1e18, + Math.Rounding.Floor + ); assertApproxEqAbs(strategy.totalAssets(), expDust, _delta_, "TA"); assertEq(IERC20(address(strategy)).totalSupply(), 0); @@ -454,10 +475,10 @@ contract MaticXLooperTest is BaseStrategyTest { function test_withdraw_dust() public { // manager can withdraw maticX balance when vault total supply is 0 deal(address(maticX), address(strategy), 10e18); - + vm.prank(address(this)); strategyContract.withdrawDust(address(this)); - + assertEq(strategy.totalAssets(), 0, "TA"); } @@ -471,13 +492,19 @@ contract MaticXLooperTest is BaseStrategyTest { vm.stopPrank(); uint256 totAssetsBefore = strategy.totalAssets(); - uint256 maticXOwnerBefore = IERC20(address(maticX)).balanceOf(address(this)); - + uint256 maticXOwnerBefore = IERC20(address(maticX)).balanceOf( + address(this) + ); + vm.prank(address(this)); strategyContract.withdrawDust(address(this)); - + assertEq(strategy.totalAssets(), totAssetsBefore, "TA DUST"); - assertEq(IERC20(address(maticX)).balanceOf(address(this)), maticXOwnerBefore, "OWNER DUST"); + assertEq( + IERC20(address(maticX)).balanceOf(address(this)), + maticXOwnerBefore, + "OWNER DUST" + ); } function test__setLeverageValues_lever_up() public { @@ -530,7 +557,7 @@ contract MaticXLooperTest is BaseStrategyTest { // protocolLTV < targetLTV < maxLTV vm.expectRevert( abi.encodeWithSelector( - MaticXLooper.InvalidLTV.selector, + BaseAaveLeverageStrategy.InvalidLTV.selector, 3e18, 4e18, strategyContract.protocolMaxLTV() @@ -541,7 +568,7 @@ contract MaticXLooperTest is BaseStrategyTest { // maxLTV < targetLTV < protocolLTV vm.expectRevert( abi.encodeWithSelector( - MaticXLooper.InvalidLTV.selector, + BaseAaveLeverageStrategy.InvalidLTV.selector, 4e17, 3e17, strategyContract.protocolMaxLTV() @@ -564,7 +591,7 @@ contract MaticXLooperTest is BaseStrategyTest { vm.expectRevert( abi.encodeWithSelector( - MaticXLooper.InvalidSlippage.selector, + BaseAaveLeverageStrategy.InvalidSlippage.selector, newSlippage, 2e17 ) @@ -578,7 +605,7 @@ contract MaticXLooperTest is BaseStrategyTest { uint256[] memory premiums = new uint256[](1); // reverts with invalid msg.sender and valid initiator - vm.expectRevert(MaticXLooper.NotFlashLoan.selector); + vm.expectRevert(BaseAaveLeverageStrategy.NotFlashLoan.selector); vm.prank(bob); strategyContract.executeOperation( assets, @@ -589,7 +616,7 @@ contract MaticXLooperTest is BaseStrategyTest { ); // reverts with invalid initiator and valid msg.sender - vm.expectRevert(MaticXLooper.NotFlashLoan.selector); + vm.expectRevert(BaseAaveLeverageStrategy.NotFlashLoan.selector); vm.prank(address(lendingPool)); strategyContract.executeOperation( assets, @@ -610,10 +637,13 @@ contract MaticXLooperTest is BaseStrategyTest { uint256 oldTa = strategy.totalAssets(); - _mintAssetAndApproveForStrategy(10e18, address(this)); - strategy.harvest(abi.encode(10e18)); - - assertGt(strategy.totalAssets(), oldTa); + // LTV should be at target now + assertApproxEqAbs( + strategyContract.targetLTV(), + strategyContract.getLTV(), + _delta_, + string.concat("ltv != expected") + ); } /*////////////////////////////////////////////////////////////// @@ -627,7 +657,7 @@ contract MaticXLooperTest is BaseStrategyTest { string.concat( "VaultCraft Leveraged ", IERC20Metadata(address(maticX)).name(), - " Adapter" + " Strategy" ), "name" ); diff --git a/test/strategies/stader/MaticXLooperTestConfig.json b/test/strategies/stader/MaticXLooperTestConfig.json index b4613ee9..e1649fcc 100644 --- a/test/strategies/stader/MaticXLooperTestConfig.json +++ b/test/strategies/stader/MaticXLooperTestConfig.json @@ -15,15 +15,18 @@ "testId": "MaticXLooper" }, "specific": { - "init": { + "base": { "aaveDataProvider": "0x7deEB8aCE4220643D8edeC871a23807E4d006eE5", - "balancerVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", - "maticXPool": "0xfd225C9e6601C9d38d8F98d8731BF59eFcF8C0E3", + "borrowAsset": "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", "maxLTV": 900000000000000000, + "maxSlippage": 5000000000000000, "poolAddressesProvider": "0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb", - "poolId": "0xcd78a20c597e367a4e478a2411ceb790604d7c8f000000000000000000000c22", - "slippage": 5000000000000000, "targetLTV": 800000000000000000 + }, + "init": { + "balancerVault": "0xBA12222222228d8Ba445958a75a0704d566BF2C8", + "maticXPool": "0xfd225C9e6601C9d38d8F98d8731BF59eFcF8C0E3", + "poolId": "0xcd78a20c597e367a4e478a2411ceb790604d7c8f000000000000000000000c22" } } } From 0c29ea266c1f9991307ca922e7ee9cabcf4e9434 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 18 Sep 2024 13:51:26 +0200 Subject: [PATCH 06/10] add claim harvesting --- src/strategies/BaseAaveLeverageStrategy.sol | 46 ++++++++++++- src/strategies/stader/MaticXLooper.sol | 76 +++++++++++++-------- test/strategies/stader/MaticXLooper.t.sol | 29 ++++---- 3 files changed, 106 insertions(+), 45 deletions(-) diff --git a/src/strategies/BaseAaveLeverageStrategy.sol b/src/strategies/BaseAaveLeverageStrategy.sol index d8e34e5a..e6521e7a 100644 --- a/src/strategies/BaseAaveLeverageStrategy.sol +++ b/src/strategies/BaseAaveLeverageStrategy.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.25; import {BaseStrategy, IERC20, IERC20Metadata, SafeERC20, ERC20, Math} from "src/strategies/BaseStrategy.sol"; import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; -import {ILendingPool, IAToken, IFlashLoanReceiver, IProtocolDataProvider, IPoolAddressesProvider, DataTypes} from "src/interfaces/external/aave/IAaveV3.sol"; +import {ILendingPool, IAaveIncentives, IAToken, IFlashLoanReceiver, IProtocolDataProvider, IPoolAddressesProvider, DataTypes} from "src/interfaces/external/aave/IAaveV3.sol"; struct LooperBaseValues { address aaveDataProvider; @@ -33,6 +33,7 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { ILendingPool public lendingPool; // aave router IPoolAddressesProvider public poolAddressesProvider; // aave pool provider + IAaveIncentives public aaveIncentives; // aave incentives IERC20 public borrowAsset; // asset to borrow IERC20 public debtToken; // aave debt token @@ -68,6 +69,9 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { ).getReserveTokensAddresses(asset_); interestToken = IERC20(_aToken); lendingPool = ILendingPool(IAToken(_aToken).POOL()); + aaveIncentives = IAaveIncentives( + IAToken(_aToken).getIncentivesController() + ); // set efficiency mode _setEfficiencyMode(); @@ -228,8 +232,44 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { _assertHealthyLTV(); } - function harvest(bytes memory) external override onlyKeeperOrOwner { - adjustLeverage(); + /// @notice The token rewarded if the aave liquidity mining is active + function rewardTokens() external view override returns (address[] memory) { + return aaveIncentives.getRewardsByAsset(asset()); + } + + /// @notice Claim additional rewards given that it's active. + function claim() internal override returns (bool success) { + if (address(aaveIncentives) == address(0)) return false; + + address[] memory _assets = new address[](1); + _assets[0] = address(interestToken); + + try aaveIncentives.claimAllRewardsToSelf(_assets) { + success = true; + } catch {} + } + + function harvest(bytes memory data) external override onlyKeeperOrOwner { + claim(); + + address[] memory _rewardTokens = aaveIncentives.getRewardsByAsset( + asset() + ); + + for (uint256 i; i < _rewardTokens.length; i++) { + uint256 balance = IERC20(_rewardTokens[i]).balanceOf(address(this)); + if (balance > 0) { + IERC20(_rewardTokens[i]).transfer(msg.sender, balance); + } + } + + uint256 assetAmount = abi.decode(data, (uint256)); + + if (assetAmount == 0) revert ZeroAmount(); + + IERC20(asset()).transferFrom(msg.sender, address(this), assetAmount); + + _protocolDeposit(assetAmount, 0, bytes("")); emit Harvested(); } diff --git a/src/strategies/stader/MaticXLooper.sol b/src/strategies/stader/MaticXLooper.sol index 8fc94a8d..37464132 100644 --- a/src/strategies/stader/MaticXLooper.sol +++ b/src/strategies/stader/MaticXLooper.sol @@ -6,12 +6,7 @@ pragma solidity ^0.8.25; import {BaseAaveLeverageStrategy, LooperBaseValues, DataTypes, IERC20} from "src/strategies/BaseAaveLeverageStrategy.sol"; import {IMaticXPool} from "./IMaticX.sol"; import {IWETH as IWMatic} from "src/interfaces/external/IWETH.sol"; -import { - IBalancerVault, - SwapKind, - SingleSwap, - FundManagement -} from "src/interfaces/external/balancer/IBalancer.sol"; +import {IBalancerVault, SwapKind, SingleSwap, FundManagement} from "src/interfaces/external/balancer/IBalancer.sol"; struct LooperValues { address balancerVault; @@ -26,8 +21,16 @@ contract MaticXLooper is BaseAaveLeverageStrategy { IBalancerVault public balancerVault; bytes32 public balancerPoolId; - function initialize(address asset_, address owner_, bool autoDeposit_, bytes memory strategyInitData_) public initializer{ - (LooperBaseValues memory baseValues, LooperValues memory strategyValues) = abi.decode(strategyInitData_, (LooperBaseValues, LooperValues)); + function initialize( + address asset_, + address owner_, + bool autoDeposit_, + bytes memory strategyInitData_ + ) public initializer { + ( + LooperBaseValues memory baseValues, + LooperValues memory strategyValues + ) = abi.decode(strategyInitData_, (LooperBaseValues, LooperValues)); // init base leverage strategy __BaseLeverageStrategy_init(asset_, owner_, autoDeposit_, baseValues); @@ -42,18 +45,25 @@ contract MaticXLooper is BaseAaveLeverageStrategy { } // provides conversion from matic to maticX - function _toCollateralValue(uint256 maticAmount) internal view override returns (uint256 maticXAmount) { - (maticXAmount,,) = maticXPool.convertMaticToMaticX(maticAmount); + function _toCollateralValue( + uint256 maticAmount + ) internal view override returns (uint256 maticXAmount) { + (maticXAmount, , ) = maticXPool.convertMaticToMaticX(maticAmount); } // provides conversion from maticX to matic - function _toDebtValue(uint256 maticXAmount) internal view override returns (uint256 maticAmount) { - (maticAmount,,) - = maticXPool.convertMaticXToMatic(maticXAmount); + function _toDebtValue( + uint256 maticXAmount + ) internal view override returns (uint256 maticAmount) { + (maticAmount, , ) = maticXPool.convertMaticXToMatic(maticXAmount); } - // swaps MaticX to exact wMatic - function _convertCollateralToDebt(uint256 maxCollateralIn, uint256 exactDebtAmont, address asset) internal override { + // swaps MaticX to exact wMatic + function _convertCollateralToDebt( + uint256 maxCollateralIn, + uint256 exactDebtAmont, + address asset + ) internal override { SingleSwap memory swap = SingleSwap( balancerPoolId, SwapKind.GIVEN_OUT, @@ -72,18 +82,25 @@ contract MaticXLooper is BaseAaveLeverageStrategy { } // unwrap wMatic and stakes into maticX - function _convertDebtToCollateral(uint256 debtAmount, uint256 totCollateralAmount) internal override { - if(debtAmount > 0) - IWMatic(address(borrowAsset)).withdraw(debtAmount); - - maticXPool.swapMaticForMaticXViaInstantPool{value: totCollateralAmount}(); + function _convertDebtToCollateral( + uint256 debtAmount, + uint256 totCollateralAmount + ) internal override { + if (debtAmount > 0) IWMatic(address(borrowAsset)).withdraw(debtAmount); + + maticXPool.swapMaticForMaticXViaInstantPool{ + value: totCollateralAmount + }(); } // assign balancer data for swaps function _setHarvestValues(bytes memory harvestValues) internal override { - (address newBalancerVault, bytes32 newBalancerPoolId) = abi.decode(harvestValues, (address, bytes32)); + (address newBalancerVault, bytes32 newBalancerPoolId) = abi.decode( + harvestValues, + (address, bytes32) + ); - if(newBalancerVault != address(balancerVault)) { + if (newBalancerVault != address(balancerVault)) { address asset_ = asset(); // reset old pool @@ -102,26 +119,29 @@ contract MaticXLooper is BaseAaveLeverageStrategy { // Matic correlated lendingPool.setUserEMode(uint8(2)); } - + // reads max ltv on efficiency mode function _getMaxLTV() internal override returns (uint256 protocolMaxLTV) { // get protocol LTV - DataTypes.EModeData memory emodeData = lendingPool.getEModeCategoryData(uint8(2)); + DataTypes.EModeData memory emodeData = lendingPool.getEModeCategoryData( + uint8(2) + ); protocolMaxLTV = uint256(emodeData.maxLTV) * 1e14; // make it 18 decimals to compare; } - function _withdrawDust(address recipient) internal override { // send matic dust to recipient uint256 maticBalance = address(this).balance; if (maticBalance > 0) { - (bool sent,) = address(recipient).call{value: address(this).balance}(""); + (bool sent, ) = address(recipient).call{ + value: address(this).balance + }(""); require(sent, "Failed to send Matic"); } - // send maticX + // send maticX uint256 maticXBalance = IERC20(asset()).balanceOf(address(this)); - if(totalSupply() == 0 && maticXBalance > 0) { + if (totalSupply() == 0 && maticXBalance > 0) { IERC20(asset()).transfer(recipient, maticXBalance); } } diff --git a/test/strategies/stader/MaticXLooper.t.sol b/test/strategies/stader/MaticXLooper.t.sol index 7d781b86..56875ea7 100644 --- a/test/strategies/stader/MaticXLooper.t.sol +++ b/test/strategies/stader/MaticXLooper.t.sol @@ -627,24 +627,25 @@ contract MaticXLooperTest is BaseStrategyTest { ); } - function test__harvest() public override { - _mintAssetAndApproveForStrategy(100e18, bob); + // function test__harvest() public override { + // _mintAssetAndApproveForStrategy(100e18, bob); - vm.prank(bob); - strategy.deposit(100e18, bob); + // vm.prank(bob); + // strategy.deposit(100e18, bob); - vm.warp(block.timestamp + 30 days); + // // LTV should be 0 + // assertEq(strategyContract.getLTV(), 0); - uint256 oldTa = strategy.totalAssets(); + // strategy.harvest(hex""); - // LTV should be at target now - assertApproxEqAbs( - strategyContract.targetLTV(), - strategyContract.getLTV(), - _delta_, - string.concat("ltv != expected") - ); - } + // // LTV should be at target now + // assertApproxEqAbs( + // strategyContract.targetLTV(), + // strategyContract.getLTV(), + // _delta_, + // string.concat("ltv != expected") + // ); + // } /*////////////////////////////////////////////////////////////// INITIALIZATION From 88501e4ed94d3b6b564ee4be15419a9d26b7e146 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 18 Sep 2024 14:49:09 +0200 Subject: [PATCH 07/10] fix missing argument --- src/strategies/BaseAaveLeverageStrategy.sol | 22 +++++++++++++-------- src/strategies/stader/MaticXLooper.sol | 3 ++- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/strategies/BaseAaveLeverageStrategy.sol b/src/strategies/BaseAaveLeverageStrategy.sol index e6521e7a..7ad14b79 100644 --- a/src/strategies/BaseAaveLeverageStrategy.sol +++ b/src/strategies/BaseAaveLeverageStrategy.sol @@ -33,7 +33,8 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { ILendingPool public lendingPool; // aave router IPoolAddressesProvider public poolAddressesProvider; // aave pool provider - IAaveIncentives public aaveIncentives; // aave incentives + IAaveIncentives public aaveIncentives; // aave incentives + IProtocolDataProvider public protocolDataProvider; // aave data provider IERC20 public borrowAsset; // asset to borrow IERC20 public debtToken; // aave debt token @@ -64,9 +65,13 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { __BaseStrategy_init(asset_, owner_, autoDeposit_); // retrieve and set deposit aToken, lending pool - (address _aToken, , ) = IProtocolDataProvider( + protocolDataProvider = IProtocolDataProvider( initValues.aaveDataProvider - ).getReserveTokensAddresses(asset_); + ); + + (address _aToken, , ) = protocolDataProvider.getReserveTokensAddresses( + asset_ + ); interestToken = IERC20(_aToken); lendingPool = ILendingPool(IAToken(_aToken).POOL()); aaveIncentives = IAaveIncentives( @@ -90,9 +95,8 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { // retrieve and set aave variable debt token borrowAsset = IERC20(initValues.borrowAsset); - (, , address _variableDebtToken) = IProtocolDataProvider( - initValues.aaveDataProvider - ).getReserveTokensAddresses(address(borrowAsset)); + (, , address _variableDebtToken) = protocolDataProvider + .getReserveTokensAddresses(address(borrowAsset)); debtToken = IERC20(_variableDebtToken); // variable debt token @@ -529,7 +533,8 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { _convertCollateralToDebt( flashLoanCollateralValue + swapBuffer, flashLoanDebt, - asset + asset, + toWithdraw ); } @@ -601,7 +606,8 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { function _convertCollateralToDebt( uint256 maxCollateralIn, uint256 exactDebtAmont, - address asset + address asset, + uint256 toWithdraw ) internal virtual; // must provide logic to use borrowed debt assets to get collateral diff --git a/src/strategies/stader/MaticXLooper.sol b/src/strategies/stader/MaticXLooper.sol index 37464132..6588f30f 100644 --- a/src/strategies/stader/MaticXLooper.sol +++ b/src/strategies/stader/MaticXLooper.sol @@ -62,7 +62,8 @@ contract MaticXLooper is BaseAaveLeverageStrategy { function _convertCollateralToDebt( uint256 maxCollateralIn, uint256 exactDebtAmont, - address asset + address asset, + uint256 ) internal override { SingleSwap memory swap = SingleSwap( balancerPoolId, From e21cd93adafc4610c5aabfedf73b6fdd06b5e4d6 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 18 Sep 2024 14:49:35 +0200 Subject: [PATCH 08/10] Refactor ETHx looper --- script/deploy/stader/EthXLooper.s.sol | 22 +- .../deploy/stader/EthXLooperDeployConfig.json | 13 +- src/strategies/stader/ETHxLooper.sol | 703 ++---------------- test/strategies/stader/EthXLooper.t.sol | 116 ++- .../stader/EthXLooperTestConfig.json | 11 +- 5 files changed, 147 insertions(+), 718 deletions(-) diff --git a/script/deploy/stader/EthXLooper.s.sol b/script/deploy/stader/EthXLooper.s.sol index 53579e80..e35b888a 100644 --- a/script/deploy/stader/EthXLooper.s.sol +++ b/script/deploy/stader/EthXLooper.s.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.25; import {Script, console} from "forge-std/Script.sol"; import {stdJson} from "forge-std/StdJson.sol"; -import {ETHXLooper, LooperInitValues, IERC20} from "../../../src/strategies/stader/ETHxLooper.sol"; +import {ETHXLooper, LooperValues, LooperBaseValues, IERC20} from "../../../src/strategies/stader/ETHxLooper.sol"; contract DeployStrategy is Script { using stdJson for string; @@ -25,9 +25,14 @@ contract DeployStrategy is Script { // Deploy Strategy strategy = new ETHXLooper(); - LooperInitValues memory looperValues = abi.decode( - json.parseRaw(".strategyInit"), - (LooperInitValues) + LooperBaseValues memory baseValues = abi.decode( + json.parseRaw(".baseLeverage"), + (LooperBaseValues) + ); + + LooperValues memory looperInitValues = abi.decode( + json.parseRaw(".strategy"), + (LooperValues) ); address asset = json.readAddress(".baseInit.asset"); @@ -37,13 +42,8 @@ contract DeployStrategy is Script { json.readAddress(".baseInit.owner"), json.readBool(".baseInit.autoDeposit"), abi.encode( - looperValues.aaveDataProvider, - looperValues.curvePool, - looperValues.maxLTV, - looperValues.poolAddressesProvider, - looperValues.slippage, - looperValues.stakingPool, - looperValues.targetLTV + baseValues, + looperInitValues ) ); diff --git a/script/deploy/stader/EthXLooperDeployConfig.json b/script/deploy/stader/EthXLooperDeployConfig.json index d91728e5..b04b86b0 100644 --- a/script/deploy/stader/EthXLooperDeployConfig.json +++ b/script/deploy/stader/EthXLooperDeployConfig.json @@ -4,14 +4,17 @@ "owner": "", "autoDeposit": false }, - "strategyInit": { + "baseLeverage": { "aaveDataProvider": "0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3", - "curvePool": "0x59ab5a5b5d617e478a2479b0cad80da7e2831492", - "maxLTV": 850000000000000000, + "borrowAsset": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "maxLTV": 900000000000000000, + "maxSlippage": 5000000000000000, "poolAddressesProvider": "0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e", - "slippage": 10000000000000000, - "stakingPool": "0xcf5ea1b38380f6af39068375516daf40ed70d299", "targetLTV": 800000000000000000 + }, + "strategy": { + "curvePool": "0x59ab5a5b5d617e478a2479b0cad80da7e2831492", + "stakingPool": "0xcf5ea1b38380f6af39068375516daf40ed70d299" } } \ No newline at end of file diff --git a/src/strategies/stader/ETHxLooper.sol b/src/strategies/stader/ETHxLooper.sol index 394f95ed..4e13734f 100644 --- a/src/strategies/stader/ETHxLooper.sol +++ b/src/strategies/stader/ETHxLooper.sol @@ -3,183 +3,49 @@ pragma solidity ^0.8.25; -import {BaseStrategy, IERC20, IERC20Metadata, SafeERC20, ERC20, Math} from "src/strategies/BaseStrategy.sol"; -import {ICurveMetapool} from "src/interfaces/external/curve/ICurveMetapool.sol"; +import {BaseAaveLeverageStrategy, LooperBaseValues, DataTypes, IERC20, Math} from "src/strategies/BaseAaveLeverageStrategy.sol"; import {IETHxStaking} from "./IETHxStaking.sol"; -import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; import {IWETH} from "src/interfaces/external/IWETH.sol"; -import {ILendingPool, IAToken, IFlashLoanReceiver, IProtocolDataProvider, IAaveIncentives, IPoolAddressesProvider, DataTypes} from "src/interfaces/external/aave/IAaveV3.sol"; +import {ICurveMetapool} from "src/interfaces/external/curve/ICurveMetapool.sol"; -struct LooperInitValues { - address aaveDataProvider; +struct LooperValues { address curvePool; - uint256 maxLTV; - address poolAddressesProvider; - uint256 slippage; address stakingPool; - uint256 targetLTV; -} - -struct FlashLoanCache { - bool isWithdraw; - bool isFullWithdraw; - uint256 assetsToWithdraw; - uint256 depositAmount; - uint256 exchangeRate; } -/// @title Leveraged ETHx yield adapter -/// @author ADN -/// @notice ERC4626 wrapper for leveraging ETHx yield -/// @dev The strategy takes ETHx and deposits it into a lending protocol (aave). -/// Then it borrows WETH, swap for ETHx and redeposits it -contract ETHXLooper is BaseStrategy, IFlashLoanReceiver { - // using FixedPointMathLib for uint256; - using SafeERC20 for IERC20; +contract ETHXLooper is BaseAaveLeverageStrategy { using Math for uint256; - string internal _name; - string internal _symbol; - - // address of the aave/spark router - ILendingPool public lendingPool; - IPoolAddressesProvider public poolAddressesProvider; - IProtocolDataProvider public protocolDataProvider; - IAaveIncentives public aaveIncentives; - - IWETH public constant weth = - IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); IETHxStaking public stakingPool; // stader pool for wrapping - converting - IERC20 public debtToken; // aave eht debt token - IERC20 public interestToken; // aave aETHx - + // swap logic int128 private constant WETHID = 0; int128 private constant ETHxID = 1; ICurveMetapool public stableSwapPool; - uint256 public slippage; // 1e18 = 100% slippage, 1e14 = 1 BPS slippage - - uint256 public targetLTV; // in 18 decimals - 1e17 being 0.1% - uint256 public maxLTV; // max ltv the vault can reach - uint256 public protocolMaxLTV; // underlying money market max LTV - - error InvalidLTV(uint256 targetLTV, uint256 maxLTV, uint256 protocolLTV); - error InvalidSlippage(uint256 slippage, uint256 slippageCap); - error BadLTV(uint256 currentLTV, uint256 maxLTV); - /*////////////////////////////////////////////////////////////// - INITIALIZATION - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Initialize a new Strategy. - * @param asset_ The underlying asset used for deposit/withdraw and accounting - * @param owner_ Owner of the contract. Controls management functions. - * @param autoDeposit_ Controls if `protocolDeposit` gets called on deposit - * @param strategyInitData_ Encoded data for this specific strategy - */ function initialize( address asset_, address owner_, bool autoDeposit_, bytes memory strategyInitData_ ) public initializer { - __BaseStrategy_init(asset_, owner_, autoDeposit_); - - LooperInitValues memory initValues = abi.decode( - strategyInitData_, - (LooperInitValues) - ); - - stakingPool = IETHxStaking(initValues.stakingPool); - protocolDataProvider = IProtocolDataProvider( - initValues.aaveDataProvider - ); - poolAddressesProvider = IPoolAddressesProvider( - initValues.poolAddressesProvider - ); - - // retrieve and set ethX aToken, lending pool - (address _aToken, , ) = protocolDataProvider.getReserveTokensAddresses( - asset_ - ); - interestToken = IERC20(_aToken); - lendingPool = ILendingPool(IAToken(_aToken).POOL()); - aaveIncentives = IAaveIncentives( - IAToken(_aToken).getIncentivesController() - ); - - // set efficiency mode - ETH correlated - lendingPool.setUserEMode(uint8(1)); - - // get protocol LTV - DataTypes.EModeData memory emodeData = lendingPool.getEModeCategoryData( - uint8(1) - ); - protocolMaxLTV = uint256(emodeData.maxLTV) * 1e14; // make it 18 decimals to compare; - - // check ltv init values are correct - _verifyLTV(initValues.targetLTV, initValues.maxLTV, protocolMaxLTV); - - targetLTV = initValues.targetLTV; - maxLTV = initValues.maxLTV; - - // retrieve and set weth variable debt token - (, , address _variableDebtToken) = protocolDataProvider - .getReserveTokensAddresses(address(weth)); - - debtToken = IERC20(_variableDebtToken); // variable debt weth token - - _name = string.concat( - "VaultCraft Leveraged ", - IERC20Metadata(asset_).name(), - " Adapter" - ); - _symbol = string.concat("vc-", IERC20Metadata(asset_).symbol()); + ( + LooperBaseValues memory baseValues, + LooperValues memory strategyValues + ) = abi.decode(strategyInitData_, (LooperBaseValues, LooperValues)); - // approve aave router to pull EThx - IERC20(asset_).approve(address(lendingPool), type(uint256).max); + // init base leverage strategy + __BaseLeverageStrategy_init(asset_, owner_, autoDeposit_, baseValues); - // approve aave pool to pull WETH as part of a flash loan - IERC20(address(weth)).approve(address(lendingPool), type(uint256).max); + // ethx pool + stakingPool = IETHxStaking(strategyValues.stakingPool); - // approve curve to pull ETHx for swapping - stableSwapPool = ICurveMetapool(initValues.curvePool); + // swap logic - curve + stableSwapPool = ICurveMetapool(strategyValues.curvePool); IERC20(asset_).approve(address(stableSwapPool), type(uint256).max); - - // set slippage - if (initValues.slippage > 2e17) { - revert InvalidSlippage(initValues.slippage, 2e17); - } - - slippage = initValues.slippage; - } - - receive() external payable {} - - function name() - public - view - override(IERC20Metadata, ERC20) - returns (string memory) - { - return _name; - } - - function symbol() - public - view - override(IERC20Metadata, ERC20) - returns (string memory) - { - return _symbol; } - /*////////////////////////////////////////////////////////////// - ACCOUNTING LOGIC - //////////////////////////////////////////////////////////////*/ - function maxDeposit(address) public view override returns (uint256) { if (paused()) return 0; @@ -190,377 +56,45 @@ contract ETHXLooper is BaseStrategy, IFlashLoanReceiver { return (supplyCap * 1e18) - interestToken.totalSupply(); } - function _totalAssets() internal view override returns (uint256) { - uint256 ethToEthXRate = stakingPool.getExchangeRate(); - uint256 debt = debtToken.balanceOf(address(this)).mulDiv( - 1e18, - ethToEthXRate, - Math.Rounding.Ceil - ); // weth debt converted in ethX amount - - uint256 collateral = interestToken.balanceOf(address(this)); // ethX collateral - - if (debt >= collateral) return 0; - - uint256 total = collateral; - if (debt > 0) { - total -= debt; - - // if there's debt, apply slippage to repay it - uint256 slippageDebt = debt.mulDiv( - slippage, - 1e18, - Math.Rounding.Ceil - ); - - if (slippageDebt >= total) return 0; - - total -= slippageDebt; - } - if (total > 0) return total - 1; - else return 0; - } - - function getLTV() public view returns (uint256 ltv) { - (ltv, , ) = _getCurrentLTV(stakingPool.getExchangeRate()); - } - - /// @notice The token rewarded if the aave liquidity mining is active - function rewardTokens() external view override returns (address[] memory) { - return aaveIncentives.getRewardsByAsset(asset()); - } - - function convertToUnderlyingShares( - uint256 assets, - uint256 shares - ) public view override returns (uint256) { - revert(); - } - - /*////////////////////////////////////////////////////////////// - FLASH LOAN LOGIC - //////////////////////////////////////////////////////////////*/ - - error NotFlashLoan(); - - function ADDRESSES_PROVIDER() - external - view - returns (IPoolAddressesProvider) - { - return poolAddressesProvider; - } - - function POOL() external view returns (ILendingPool) { - return lendingPool; - } - - // this is triggered after the flash loan is given, ie contract has loaned assets at this point - function executeOperation( - address[] calldata, - uint256[] calldata amounts, - uint256[] calldata premiums, - address initiator, - bytes calldata params - ) external override returns (bool) { - if (initiator != address(this) || msg.sender != address(lendingPool)) { - revert NotFlashLoan(); - } - - FlashLoanCache memory cache = abi.decode(params, (FlashLoanCache)); - - if (cache.isWithdraw) { - // flash loan is to repay weth debt as part of a withdrawal - uint256 flashLoanDebt = amounts[0] + premiums[0]; - - // repay cdp weth debt - lendingPool.repay(address(weth), amounts[0], 2, address(this)); - - // withdraw collateral, swap, repay flashloan - _reduceLeverage( - cache.isFullWithdraw, - cache.assetsToWithdraw, - flashLoanDebt, - cache.exchangeRate - ); - } else { - // flash loan is to leverage UP - _redepositAsset(amounts[0], cache.depositAmount); - } - - return true; - } - - /*////////////////////////////////////////////////////////////// - INTERNAL HOOKS LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @notice Deposit ethX into lending protocol - function _protocolDeposit( - uint256 assets, - uint256, - bytes memory - ) internal override { - // deposit ethX into aave - receive aToken here - lendingPool.supply(asset(), assets, address(this), 0); - } - - /// @notice repay part of the vault debt and withdraw ethX - function _protocolWithdraw( - uint256 assets, - uint256, - bytes memory - ) internal override { - uint256 ethToEthXRate = stakingPool.getExchangeRate(); - - (, uint256 currentDebt, uint256 currentCollateral) = _getCurrentLTV( - ethToEthXRate - ); - uint256 ethAssetsValue = assets.mulDiv( - ethToEthXRate, - 1e18, - Math.Rounding.Ceil - ); - - bool isFullWithdraw; - uint256 ratioDebtToRepay; - - { - uint256 debtSlippage = currentDebt.mulDiv( - slippage, - 1e18, - Math.Rounding.Ceil - ); - - // find the % of debt to repay as the % of collateral being withdrawn - ratioDebtToRepay = ethAssetsValue.mulDiv( - 1e18, - (currentCollateral - currentDebt - debtSlippage), - Math.Rounding.Floor - ); - - isFullWithdraw = - assets == _totalAssets() || - ratioDebtToRepay >= 1e18; - } - - // get the LTV we would have without repaying debt - uint256 futureLTV = isFullWithdraw - ? type(uint256).max - : currentDebt.mulDiv( - 1e18, - (currentCollateral - ethAssetsValue), - Math.Rounding.Floor - ); - - if (futureLTV <= maxLTV || currentDebt == 0) { - // 1 - withdraw any asset amount with no debt - // 2 - withdraw assets with debt but the change doesn't take LTV above max - lendingPool.withdraw(asset(), assets, address(this)); - } else { - // 1 - withdraw assets but repay debt - uint256 debtToRepay = isFullWithdraw - ? currentDebt - : currentDebt.mulDiv( - ratioDebtToRepay, - 1e18, - Math.Rounding.Floor - ); - - // flash loan debtToRepay - mode 0 - flash loan is repaid at the end - _flashLoanETH( - debtToRepay, - 0, - assets, - 0, - isFullWithdraw, - ethToEthXRate - ); - } - - // reverts if LTV got above max - _assertHealthyLTV(ethToEthXRate); - } - - // deposit back into the protocol - // either from flash loan or simply ETH dust held by the adapter - function _redepositAsset( - uint256 borrowAmount, - uint256 depositAmount - ) internal { - address ethX = asset(); - - if (borrowAmount > 0) { - // unwrap into ETH the flash loaned amount - weth.withdraw(borrowAmount); - } - - // stake borrowed eth and receive ethX - stakingPool.deposit{value: depositAmount}(address(this)); - - // get ethX balance after staking - // may include eventual ethX dust held by contract somehow - // in that case it will just add more collateral - uint256 ethXAmount = IERC20(ethX).balanceOf(address(this)); - - // deposit ethX into lending protocol - _protocolDeposit(ethXAmount, 0, hex""); - } - - // reduce leverage by withdrawing ethX, swapping to ETH repaying weth debt - function _reduceLeverage( - bool isFullWithdraw, - uint256 toWithdraw, - uint256 flashLoanDebt, - uint256 exchangeRate - ) internal { - address asset = asset(); - - // get flash loan amount converted in ethX - uint256 flashLoanEthXAmount = flashLoanDebt.mulDiv( - 1e18, - exchangeRate, - Math.Rounding.Ceil - ); - - // get slippage buffer for swapping with flashLoanDebt as minAmountOut - uint256 ethXBuffer = flashLoanEthXAmount.mulDiv( - slippage, - 1e18, - Math.Rounding.Floor - ); + // provides conversion from eth to ethX + function _toCollateralValue( + uint256 ethAmount + ) internal view override returns (uint256 ethXAmount) { + uint256 ethxToEthRate = stakingPool.getExchangeRate(); - // if the withdraw amount with buffers to total assets withdraw all - if (flashLoanEthXAmount + ethXBuffer + toWithdraw >= _totalAssets()) - isFullWithdraw = true; - - // withdraw ethX from aave - if (isFullWithdraw) { - // withdraw all - lendingPool.withdraw(asset, type(uint256).max, address(this)); - } else { - lendingPool.withdraw( - asset, - flashLoanEthXAmount + ethXBuffer + toWithdraw, - address(this) - ); - } - - // swap ethX to weth on Curve- will be pulled by AAVE pool as flash loan repayment - _swapToWETH( - flashLoanEthXAmount + ethXBuffer, - flashLoanDebt, - asset, - toWithdraw, - exchangeRate - ); + ethXAmount = ethAmount.mulDiv(1e18, ethxToEthRate, Math.Rounding.Ceil); } - // returns current loan to value, debt and collateral (token) amounts - function _getCurrentLTV( - uint256 exchangeRate - ) - internal - view - returns (uint256 loanToValue, uint256 debt, uint256 collateral) - { - debt = debtToken.balanceOf(address(this)); // WETH DEBT - collateral = interestToken.balanceOf(address(this)).mulDiv( - exchangeRate, - 1e18, - Math.Rounding.Floor - ); // converted into ETH amount; - - (debt == 0 || collateral == 0) ? loanToValue = 0 : loanToValue = debt - .mulDiv(1e18, collateral, Math.Rounding.Ceil); - } + // provides conversion from ethX to eth + function _toDebtValue( + uint256 ethXAmount + ) internal view override returns (uint256 ethAmount) { + uint256 ethxToEthRate = stakingPool.getExchangeRate(); - // reverts if targetLTV < maxLTV < protocolLTV is not satisfied - function _verifyLTV( - uint256 _targetLTV, - uint256 _maxLTV, - uint256 _protocolLTV - ) internal pure { - if (_targetLTV >= _maxLTV) { - revert InvalidLTV(_targetLTV, _maxLTV, _protocolLTV); - } - if (_maxLTV >= _protocolLTV) { - revert InvalidLTV(_targetLTV, _maxLTV, _protocolLTV); - } - } - - // verify that currentLTV is not above maxLTV - function _assertHealthyLTV(uint256 exchangeRate) internal view { - (uint256 currentLTV, , ) = _getCurrentLTV(exchangeRate); - - if (currentLTV > maxLTV) { - revert BadLTV(currentLTV, maxLTV); - } - } - - // borrow weth from lending protocol - // interestRateMode = 2 -> flash loan eth and deposit into cdp, don't repay - // interestRateMode = 0 -> flash loan eth to repay cdp, have to repay flash loan at the end - function _flashLoanETH( - uint256 borrowAmount, - uint256 depositAmount, - uint256 assetsToWithdraw, - uint256 interestRateMode, - bool isFullWithdraw, - uint256 exchangeRate - ) internal { - // uint256 depositAmount_ = depositAmount; // avoids stack too deep - - address[] memory assets = new address[](1); - assets[0] = address(weth); - - uint256[] memory amounts = new uint256[](1); - amounts[0] = borrowAmount; - - uint256[] memory interestRateModes = new uint256[](1); - interestRateModes[0] = interestRateMode; - - bool isWithdraw = interestRateMode == 0 ? true : false; - - FlashLoanCache memory cache = FlashLoanCache( - isWithdraw, - isFullWithdraw, - assetsToWithdraw, - depositAmount, - exchangeRate - ); - - lendingPool.flashLoan( - address(this), - assets, - amounts, - interestRateModes, - address(this), - abi.encode(cache), - 0 - ); + ethAmount = ethXAmount.mulDiv(ethxToEthRate, 1e18, Math.Rounding.Ceil); } - // swaps ETHx to exact WETH - redeposits extra ETH - function _swapToWETH( + // swaps ethX to exact weth + function _convertCollateralToDebt( uint256 amount, uint256 minAmount, address asset, - uint256 toWithdraw, - uint256 exchangeRate - ) internal { + uint256 assetsToWithdraw + ) internal override { + uint256 ethxToEthRate = stakingPool.getExchangeRate(); + // swap to ETH stableSwapPool.exchange(ETHxID, WETHID, amount, minAmount); // wrap precise amount of eth for flash loan repayment - weth.deposit{value: minAmount}(); + IWETH(address(borrowAsset)).deposit{value: minAmount}(); // restake the eth needed to reach the ETHx amount the user is withdrawing - uint256 missingETHx = toWithdraw - + uint256 missingETHx = assetsToWithdraw - IERC20(asset).balanceOf(address(this)); if (missingETHx > 0) { uint256 missingETHAmount = missingETHx.mulDiv( - exchangeRate, + ethxToEthRate, 1e18, Math.Rounding.Ceil ); @@ -570,23 +104,19 @@ contract ETHXLooper is BaseStrategy, IFlashLoanReceiver { } } - /*////////////////////////////////////////////////////////////// - MANAGEMENT LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @notice Claim additional rewards given that it's active. - function claim() internal override returns (bool success) { - if (address(aaveIncentives) == address(0)) return false; - - address[] memory _assets = new address[](1); - _assets[0] = address(interestToken); + // unwrap weth and stakes into ethX + function _convertDebtToCollateral( + uint256 debtAmount, + uint256 totCollateralAmount + ) internal override { + if (debtAmount > 0) IWETH(address(borrowAsset)).withdraw(debtAmount); - try aaveIncentives.claimAllRewardsToSelf(_assets) { - success = true; - } catch {} + stakingPool.deposit{value: totCollateralAmount}(address(this)); } - function setHarvestValues(address curveSwapPool) external onlyOwner { + // assign balancer data for swaps + function _setHarvestValues(bytes memory harvestValues) internal override { + address curveSwapPool = abi.decode(harvestValues, (address)); if (curveSwapPool != address(stableSwapPool)) { address asset_ = asset(); @@ -599,129 +129,34 @@ contract ETHXLooper is BaseStrategy, IFlashLoanReceiver { } } - function harvest(bytes memory data) external override onlyKeeperOrOwner { - claim(); - - address[] memory _rewardTokens = aaveIncentives.getRewardsByAsset( - asset() - ); - - for (uint256 i; i < _rewardTokens.length; i++) { - uint256 balance = IERC20(_rewardTokens[i]).balanceOf(address(this)); - if (balance > 0) { - IERC20(_rewardTokens[i]).transfer(msg.sender, balance); - } - } - - uint256 assetAmount = abi.decode(data, (uint256)); - - if (assetAmount == 0) revert ZeroAmount(); - - IERC20(asset()).transferFrom(msg.sender, address(this), assetAmount); - - _protocolDeposit(assetAmount, 0, bytes("")); - - emit Harvested(); - } - - // amount of weth to borrow OR amount of weth to repay (converted into ethX amount internally) - function adjustLeverage() public { - uint256 ethToEthXRate = stakingPool.getExchangeRate(); - - // get vault current leverage : debt/collateral - ( - uint256 currentLTV, - uint256 currentDebt, - uint256 currentCollateral - ) = _getCurrentLTV(ethToEthXRate); - - // de-leverage if vault LTV is higher than target - if (currentLTV > targetLTV) { - uint256 amountETH = (currentDebt - - ( - targetLTV.mulDiv( - (currentCollateral), - 1e18, - Math.Rounding.Floor - ) - )).mulDiv(1e18, (1e18 - targetLTV), Math.Rounding.Ceil); - - // flash loan eth to repay part of the debt - _flashLoanETH(amountETH, 0, 0, 0, false, ethToEthXRate); - } else { - uint256 amountETH = (targetLTV.mulDiv( - currentCollateral, - 1e18, - Math.Rounding.Ceil - ) - currentDebt).mulDiv( - 1e18, - (1e18 - targetLTV), - Math.Rounding.Ceil - ); - - uint256 dustBalance = address(this).balance; - if (dustBalance < amountETH) { - // flashloan but use eventual ETH dust remained in the contract as well - uint256 borrowAmount = amountETH - dustBalance; - - // flash loan weth from lending protocol and add to cdp - _flashLoanETH( - borrowAmount, - amountETH, - 0, - 2, - false, - ethToEthXRate - ); - } else { - // deposit the dust as collateral- borrow amount is zero - // leverage naturally decreases - _redepositAsset(0, dustBalance); - } - } - - // reverts if LTV got above max - _assertHealthyLTV(ethToEthXRate); + function _setEfficiencyMode() internal override { + // eth correlated + lendingPool.setUserEMode(uint8(1)); } - function withdrawDust(address recipient) public onlyOwner { - // send eth dust to recipient - (bool sent, ) = address(recipient).call{value: address(this).balance}( - "" + // reads max ltv on efficiency mode + function _getMaxLTV() internal override returns (uint256 protocolMaxLTV) { + // get protocol LTV + DataTypes.EModeData memory emodeData = lendingPool.getEModeCategoryData( + uint8(1) ); - require(sent, "Failed to send ETH"); - } - - function setLeverageValues( - uint256 targetLTV_, - uint256 maxLTV_ - ) external onlyOwner { - // reverts if targetLTV < maxLTV < protocolLTV is not satisfied - _verifyLTV(targetLTV_, maxLTV_, protocolMaxLTV); - - targetLTV = targetLTV_; - maxLTV = maxLTV_; - - adjustLeverage(); - } - - function setSlippage(uint256 slippage_) external onlyOwner { - if (slippage_ > 2e17) revert InvalidSlippage(slippage_, 2e17); - - slippage = slippage_; + protocolMaxLTV = uint256(emodeData.maxLTV) * 1e14; // make it 18 decimals to compare; } - bool internal initCollateral; - - function setUserUseReserveAsCollateral(uint256 amount) external onlyOwner { - if (initCollateral) revert InvalidInitialization(); - address asset_ = asset(); - - IERC20(asset_).safeTransferFrom(msg.sender, address(this), amount); - lendingPool.supply(asset_, amount, address(this), 0); - - lendingPool.setUserUseReserveAsCollateral(asset_, true); + function _withdrawDust(address recipient) internal override { + // send eth dust to recipient + uint256 ethBalance = address(this).balance; + if (ethBalance > 0 && totalSupply() == 0) { + (bool sent, ) = address(recipient).call{ + value: address(this).balance + }(""); + require(sent, "Failed to send eth"); + } - initCollateral = true; + // send ethX + uint256 ethXBalance = IERC20(asset()).balanceOf(address(this)); + if (totalSupply() == 0 && ethXBalance > 0) { + IERC20(asset()).transfer(recipient, ethXBalance); + } } } diff --git a/test/strategies/stader/EthXLooper.t.sol b/test/strategies/stader/EthXLooper.t.sol index e90c1ae7..763ee068 100644 --- a/test/strategies/stader/EthXLooper.t.sol +++ b/test/strategies/stader/EthXLooper.t.sol @@ -3,7 +3,16 @@ pragma solidity ^0.8.25; -import {ETHXLooper, LooperInitValues, IERC20, IERC20Metadata, IETHxStaking, ILendingPool, IProtocolDataProvider, Math} from "src/strategies/stader/ETHxLooper.sol"; +import { + ETHXLooper, + BaseAaveLeverageStrategy, + LooperBaseValues, + LooperValues, + IERC20, + IETHxStaking +} from "src/strategies/stader/ETHxLooper.sol"; +import {IERC20Metadata, ILendingPool, IProtocolDataProvider, Math} from "src/strategies/BaseAaveLeverageStrategy.sol"; + import {BaseStrategyTest, IBaseStrategy, TestConfig, stdJson, Math} from "../BaseStrategyTest.sol"; contract ETHXLooperTest is BaseStrategyTest { @@ -32,22 +41,24 @@ contract ETHXLooperTest is BaseStrategyTest { TestConfig memory testConfig_ ) internal override returns (IBaseStrategy) { // Read strategy init values - LooperInitValues memory looperInitValues = abi.decode( + LooperBaseValues memory baseValues = abi.decode( + json_.parseRaw( + string.concat(".configs[", index_, "].specific.base") + ), + (LooperBaseValues) + ); + + LooperValues memory looperInitValues = abi.decode( json_.parseRaw( string.concat(".configs[", index_, "].specific.init") ), - (LooperInitValues) + (LooperValues) ); // Deploy Strategy ETHXLooper strategy = new ETHXLooper(); - strategy.initialize( - testConfig_.asset, - address(this), - true, - abi.encode(looperInitValues) - ); + strategy.initialize(testConfig_.asset, address(this), true, abi.encode(baseValues, looperInitValues)); strategyContract = ETHXLooper(payable(strategy)); @@ -93,20 +104,24 @@ contract ETHXLooperTest is BaseStrategyTest { } function test__initialization() public override { - LooperInitValues memory looperInitValues = abi.decode( - json.parseRaw(string.concat(".configs[0].specific.init")), - (LooperInitValues) + LooperBaseValues memory baseValues = abi.decode( + json.parseRaw( + string.concat(".configs[0].specific.base") + ), + (LooperBaseValues) + ); + + LooperValues memory looperInitValues = abi.decode( + json.parseRaw( + string.concat(".configs[0].specific.init") + ), + (LooperValues) ); // Deploy Strategy ETHXLooper strategy = new ETHXLooper(); - strategy.initialize( - testConfig.asset, - address(this), - true, - abi.encode(looperInitValues) - ); + strategy.initialize(testConfig.asset, address(this), true, abi.encode(baseValues, looperInitValues)); verify_adapterInit(); } @@ -199,15 +214,9 @@ contract ETHXLooperTest is BaseStrategyTest { address newPool = address(0x85dE3ADd465a219EE25E04d22c39aB027cF5C12E); address asset = strategy.asset(); - strategyContract.setHarvestValues(newPool); - uint256 oldAllowance = IERC20(asset).allowance( - address(strategy), - oldPool - ); - uint256 newAllowance = IERC20(asset).allowance( - address(strategy), - newPool - ); + strategyContract.setHarvestValues(abi.encode(newPool)); + uint256 oldAllowance = IERC20(asset).allowance(address(strategy), oldPool); + uint256 newAllowance = IERC20(asset).allowance(address(strategy), newPool); assertEq(address(strategyContract.stableSwapPool()), newPool); assertEq(oldAllowance, 0); @@ -522,23 +531,13 @@ contract ETHXLooperTest is BaseStrategyTest { function test__setLeverageValues_invalidInputs() public { // protocolLTV < targetLTV < maxLTV vm.expectRevert( - abi.encodeWithSelector( - ETHXLooper.InvalidLTV.selector, - 3e18, - 4e18, - strategyContract.protocolMaxLTV() - ) + abi.encodeWithSelector(BaseAaveLeverageStrategy.InvalidLTV.selector, 3e18, 4e18, strategyContract.protocolMaxLTV()) ); strategyContract.setLeverageValues(3e18, 4e18); // maxLTV < targetLTV < protocolLTV vm.expectRevert( - abi.encodeWithSelector( - ETHXLooper.InvalidLTV.selector, - 4e17, - 3e17, - strategyContract.protocolMaxLTV() - ) + abi.encodeWithSelector(BaseAaveLeverageStrategy.InvalidLTV.selector, 4e17, 3e17, strategyContract.protocolMaxLTV()) ); strategyContract.setLeverageValues(4e17, 3e17); } @@ -555,13 +554,7 @@ contract ETHXLooperTest is BaseStrategyTest { function test__setSlippage_invalidValue() public { uint256 newSlippage = 1e18; // 100% - vm.expectRevert( - abi.encodeWithSelector( - ETHXLooper.InvalidSlippage.selector, - newSlippage, - 2e17 - ) - ); + vm.expectRevert(abi.encodeWithSelector(BaseAaveLeverageStrategy.InvalidSlippage.selector, newSlippage, 2e17)); strategyContract.setSlippage(newSlippage); } @@ -571,7 +564,7 @@ contract ETHXLooperTest is BaseStrategyTest { uint256[] memory premiums = new uint256[](1); // reverts with invalid msg.sender and valid initiator - vm.expectRevert(ETHXLooper.NotFlashLoan.selector); + vm.expectRevert(BaseAaveLeverageStrategy.NotFlashLoan.selector); vm.prank(bob); strategyContract.executeOperation( assets, @@ -582,7 +575,7 @@ contract ETHXLooperTest is BaseStrategyTest { ); // reverts with invalid initiator and valid msg.sender - vm.expectRevert(ETHXLooper.NotFlashLoan.selector); + vm.expectRevert(BaseAaveLeverageStrategy.NotFlashLoan.selector); vm.prank(address(lendingPool)); strategyContract.executeOperation( assets, @@ -593,21 +586,20 @@ contract ETHXLooperTest is BaseStrategyTest { ); } - function test__harvest() public override { - _mintAssetAndApproveForStrategy(100e18, bob); + // function test__harvest() public override { + // _mintAssetAndApproveForStrategy(100e18, bob); - vm.prank(bob); - strategy.deposit(100e18, bob); + // vm.prank(bob); + // strategy.deposit(100e18, bob); - vm.warp(block.timestamp + 30 days); + // // LTV should be 0 + // assertEq(strategyContract.getLTV(), 0); - uint256 oldTa = strategy.totalAssets(); + // strategy.harvest(hex""); - _mintAssetAndApproveForStrategy(10e18, address(this)); - strategy.harvest(abi.encode(10e18)); - - assertGt(strategy.totalAssets(), oldTa); - } + // // LTV should be at target now + // assertApproxEqAbs(strategyContract.targetLTV(), strategyContract.getLTV(), _delta_, string.concat("ltv != expected")); + // } /*////////////////////////////////////////////////////////////// INITIALIZATION @@ -617,11 +609,7 @@ contract ETHXLooperTest is BaseStrategyTest { assertEq(strategy.asset(), address(ethX), "asset"); assertEq( IERC20Metadata(address(strategy)).name(), - string.concat( - "VaultCraft Leveraged ", - IERC20Metadata(address(ethX)).name(), - " Adapter" - ), + string.concat("VaultCraft Leveraged ", IERC20Metadata(address(ethX)).name(), " Strategy"), "name" ); assertEq( diff --git a/test/strategies/stader/EthXLooperTestConfig.json b/test/strategies/stader/EthXLooperTestConfig.json index 76ddf5bb..46472597 100644 --- a/test/strategies/stader/EthXLooperTestConfig.json +++ b/test/strategies/stader/EthXLooperTestConfig.json @@ -15,14 +15,17 @@ "testId": "EthXLooper" }, "specific": { - "init": { + "base": { "aaveDataProvider": "0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3", - "curvePool": "0x59ab5a5b5d617e478a2479b0cad80da7e2831492", + "borrowAsset": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "maxLTV": 900000000000000000, + "maxSlippage": 10000000000000000, "poolAddressesProvider": "0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e", - "slippage": 10000000000000000, - "stakingPool": "0xcf5ea1b38380f6af39068375516daf40ed70d299", "targetLTV": 800000000000000000 + }, + "init": { + "curvePool": "0x59ab5a5b5d617e478a2479b0cad80da7e2831492", + "stakingPool": "0xcf5ea1b38380f6af39068375516daf40ed70d299" } } } From 904222b21fcea825b5a410cdde4cb7b255ebc0b0 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 19 Sep 2024 16:21:04 +0200 Subject: [PATCH 09/10] Refactor wstETH looper --- script/deploy/lido/WstETHLooper.s.sol | 20 +- .../deploy/lido/WstETHLooperDeployConfig.json | 9 +- src/strategies/BaseAaveLeverageStrategy.sol | 2 +- src/strategies/lido/WstETHLooper.sol | 632 +++--------------- test/strategies/lido/WstETHLooper.t.sol | 84 ++- .../lido/WstETHLooperTestConfig.json | 35 +- 6 files changed, 159 insertions(+), 623 deletions(-) diff --git a/script/deploy/lido/WstETHLooper.s.sol b/script/deploy/lido/WstETHLooper.s.sol index 678f555d..d2592201 100644 --- a/script/deploy/lido/WstETHLooper.s.sol +++ b/script/deploy/lido/WstETHLooper.s.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.25; import {Script, console} from "forge-std/Script.sol"; import {stdJson} from "forge-std/StdJson.sol"; -import {WstETHLooper, LooperInitValues, IERC20} from "../../../src/strategies/lido/WstETHLooper.sol"; +import {WstETHLooper, LooperValues, LooperBaseValues, IERC20} from "../../../src/strategies/lido/WstETHLooper.sol"; contract Deploy is Script { using stdJson for string; @@ -25,7 +25,15 @@ contract Deploy is Script { // Deploy Strategy strategy = new WstETHLooper(); - LooperInitValues memory looperValues = abi.decode(json.parseRaw(".strategyInit"), (LooperInitValues)); + LooperBaseValues memory baseValues = abi.decode( + json.parseRaw(".baseLeverage"), + (LooperBaseValues) + ); + + LooperValues memory looperInitValues = abi.decode( + json.parseRaw(".strategy"), + (LooperValues) + ); address asset = json.readAddress(".baseInit.asset"); @@ -34,12 +42,8 @@ contract Deploy is Script { json.readAddress(".baseInit.owner"), json.readBool(".baseInit.autoDeposit"), abi.encode( - looperValues.aaveDataProvider, - looperValues.curvePool, - looperValues.maxLTV, - looperValues.poolAddressesProvider, - looperValues.slippage, - looperValues.targetLTV + baseValues, + looperInitValues ) ); diff --git a/script/deploy/lido/WstETHLooperDeployConfig.json b/script/deploy/lido/WstETHLooperDeployConfig.json index ba033a19..271c91af 100644 --- a/script/deploy/lido/WstETHLooperDeployConfig.json +++ b/script/deploy/lido/WstETHLooperDeployConfig.json @@ -4,12 +4,15 @@ "owner": "", "autoDeposit": false }, - "strategyInit": { + "baseLeverage": { "aaveDataProvider": "0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3", - "curvePool": "0xDC24316b9AE028F1497c275EB9192a3Ea0f67022", + "borrowAsset": "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", "maxLTV": 850000000000000000, + "maxSlippage": 10000000000000000, "poolAddressesProvider": "0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e", - "slippage": 10000000000000000, "targetLTV": 800000000000000000 + }, + "strategy": { + "curvePool": "0xDC24316b9AE028F1497c275EB9192a3Ea0f67022" } } diff --git a/src/strategies/BaseAaveLeverageStrategy.sol b/src/strategies/BaseAaveLeverageStrategy.sol index 7ad14b79..79a6b2b2 100644 --- a/src/strategies/BaseAaveLeverageStrategy.sol +++ b/src/strategies/BaseAaveLeverageStrategy.sol @@ -546,7 +546,7 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { address asset ) internal { // use borrow asset to get more collateral - _convertDebtToCollateral(borrowAmount, totCollateralAmount); // TODO improve + _convertDebtToCollateral(borrowAmount, totCollateralAmount); // deposit collateral balance into lending protocol // may include eventual dust held by contract somehow diff --git a/src/strategies/lido/WstETHLooper.sol b/src/strategies/lido/WstETHLooper.sol index 83ec3609..6469ce2e 100644 --- a/src/strategies/lido/WstETHLooper.sol +++ b/src/strategies/lido/WstETHLooper.sol @@ -3,499 +3,82 @@ pragma solidity ^0.8.25; -import {BaseStrategy, IERC20, IERC20Metadata, SafeERC20, ERC20, Math} from "src/strategies/BaseStrategy.sol"; +import {BaseAaveLeverageStrategy, LooperBaseValues, DataTypes, IERC20, Math} from "src/strategies/BaseAaveLeverageStrategy.sol"; import {IwstETH} from "./IwstETH.sol"; -import {ILido} from "./ILido.sol"; -import {Math} from "openzeppelin-contracts/utils/math/Math.sol"; import {IWETH} from "src/interfaces/external/IWETH.sol"; +import {ILido} from "./ILido.sol"; import {ICurveMetapool} from "src/interfaces/external/curve/ICurveMetapool.sol"; -import {ILendingPool, IAToken, IFlashLoanReceiver, IProtocolDataProvider, IPoolAddressesProvider, DataTypes} from "src/interfaces/external/aave/IAaveV3.sol"; -struct LooperInitValues { - address aaveDataProvider; +struct LooperValues { address curvePool; - uint256 maxLTV; - address poolAddressesProvider; - uint256 slippage; - uint256 targetLTV; } -/// @title Leveraged wstETH yield adapter -/// @author Andrea Di Nenno -/// @notice ERC4626 wrapper for leveraging stETH yield -/// @dev The strategy takes wstETH and deposits it into a lending protocol (aave). -/// Then it borrows ETH, swap for wstETH and redeposits it -contract WstETHLooper is BaseStrategy, IFlashLoanReceiver { - // using FixedPointMathLib for uint256; - using SafeERC20 for IERC20; +contract WstETHLooper is BaseAaveLeverageStrategy { using Math for uint256; - string internal _name; - string internal _symbol; - - // address of the aave/spark router - ILendingPool public lendingPool; - IPoolAddressesProvider public poolAddressesProvider; - - IWETH public constant weth = - IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - address public constant stETH = - address(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); - - IERC20 public debtToken; // aave eth debt token - IERC20 public interestToken; // aave awstETH - + // swap logic int128 private constant WETHID = 0; int128 private constant STETHID = 1; - ICurveMetapool public stableSwapStETH; - - uint256 public slippage; // 1e18 = 100% slippage, 1e14 = 1 BPS slippage - - uint256 public targetLTV; // in 18 decimals - 1e17 being 0.1% - uint256 public maxLTV; // max ltv the vault can reach - uint256 public protocolMaxLTV; // underlying money market max LTV - - error InvalidLTV(uint256 targetLTV, uint256 maxLTV, uint256 protocolLTV); - error InvalidSlippage(uint256 slippage, uint256 slippageCap); - error BadLTV(uint256 currentLTV, uint256 maxLTV); - - /*////////////////////////////////////////////////////////////// - INITIALIZATION - //////////////////////////////////////////////////////////////*/ + ICurveMetapool public stableSwapPool; + address public constant stETH = + address(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); - /** - * @notice Initialize a new Strategy. - * @param asset_ The underlying asset used for deposit/withdraw and accounting - * @param owner_ Owner of the contract. Controls management functions. - * @param autoDeposit_ Controls if `protocolDeposit` gets called on deposit - * @param strategyInitData_ Encoded data for this specific strategy - */ function initialize( address asset_, address owner_, bool autoDeposit_, bytes memory strategyInitData_ ) public initializer { - __BaseStrategy_init(asset_, owner_, autoDeposit_); - - LooperInitValues memory initValues = abi.decode( - strategyInitData_, - (LooperInitValues) - ); - - // retrieve and set wstETH aToken, lending pool - (address _aToken, , ) = IProtocolDataProvider( - initValues.aaveDataProvider - ).getReserveTokensAddresses(asset_); - - interestToken = IERC20(_aToken); - lendingPool = ILendingPool(IAToken(_aToken).POOL()); - - // set efficiency mode - ETH correlated - lendingPool.setUserEMode(uint8(1)); - - // get protocol LTV - DataTypes.EModeData memory emodeData = lendingPool.getEModeCategoryData( - uint8(1) - ); - protocolMaxLTV = uint256(emodeData.maxLTV) * 1e14; // make it 18 decimals to compare; - - // check ltv init values are correct - _verifyLTV(initValues.targetLTV, initValues.maxLTV, protocolMaxLTV); - - targetLTV = initValues.targetLTV; - maxLTV = initValues.maxLTV; - - poolAddressesProvider = IPoolAddressesProvider( - initValues.poolAddressesProvider - ); - - // retrieve and set WETH variable debt token - (, , address _variableDebtToken) = IProtocolDataProvider( - initValues.aaveDataProvider - ).getReserveTokensAddresses(address(weth)); - - debtToken = IERC20(_variableDebtToken); // variable debt WETH token - - _name = string.concat( - "VaultCraft Leveraged ", - IERC20Metadata(asset_).name(), - " Adapter" - ); - _symbol = string.concat("vc-", IERC20Metadata(asset_).symbol()); - - // approve aave router to pull wstETH - IERC20(asset_).approve(address(lendingPool), type(uint256).max); - - // approve aave pool to pull WETH as part of a flash loan - IERC20(address(weth)).approve(address(lendingPool), type(uint256).max); - - // approve curve router to pull stETH for swapping - stableSwapStETH = ICurveMetapool(initValues.curvePool); - IERC20(stETH).approve(address(stableSwapStETH), type(uint256).max); - - // set slippage - if (initValues.slippage > 2e17) { - revert InvalidSlippage(initValues.slippage, 2e17); - } - - slippage = initValues.slippage; - } - - receive() external payable {} - - function name() - public - view - override(IERC20Metadata, ERC20) - returns (string memory) - { - return _name; - } - - function symbol() - public - view - override(IERC20Metadata, ERC20) - returns (string memory) - { - return _symbol; - } - - /*////////////////////////////////////////////////////////////// - ACCOUNTING LOGIC - //////////////////////////////////////////////////////////////*/ - - function _totalAssets() internal view override returns (uint256) { - uint256 debt = ILido(stETH).getSharesByPooledEth( - debtToken.balanceOf(address(this)) - ); // wstETH DEBT - uint256 collateral = interestToken.balanceOf(address(this)); // wstETH collateral - - if (debt >= collateral) return 0; - - uint256 total = collateral; - if (debt > 0) { - total -= debt; - - // if there's debt, apply slippage to repay it - uint256 slippageDebt = debt.mulDiv( - slippage, - 1e18, - Math.Rounding.Ceil - ); - - if (slippageDebt >= total) return 0; - - total -= slippageDebt; - } - if (total > 0) return total - 1; - else return 0; - } - - function getLTV() public view returns (uint256 ltv) { - (ltv, , ) = _getCurrentLTV(); - } - - /*////////////////////////////////////////////////////////////// - FLASH LOAN LOGIC - //////////////////////////////////////////////////////////////*/ - - error NotFlashLoan(); - - function ADDRESSES_PROVIDER() - external - view - returns (IPoolAddressesProvider) - { - return poolAddressesProvider; - } - - function POOL() external view returns (ILendingPool) { - return lendingPool; - } - - // this is triggered after the flash loan is given, ie contract has loaned assets at this point - function executeOperation( - address[] calldata, - uint256[] calldata amounts, - uint256[] calldata premiums, - address initiator, - bytes calldata params - ) external override returns (bool) { - if (initiator != address(this) || msg.sender != address(lendingPool)) { - revert NotFlashLoan(); - } - ( - bool isWithdraw, - bool isFullWithdraw, - uint256 assetsToWithdraw, - uint256 depositAmount - ) = abi.decode(params, (bool, bool, uint256, uint256)); - - if (isWithdraw) { - // flash loan is to repay ETH debt as part of a withdrawal - uint256 flashLoanDebt = amounts[0] + premiums[0]; + LooperBaseValues memory baseValues, + LooperValues memory strategyValues + ) = abi.decode(strategyInitData_, (LooperBaseValues, LooperValues)); - // repay cdp WETH debt - lendingPool.repay(address(weth), amounts[0], 2, address(this)); + // init base leverage strategy + __BaseLeverageStrategy_init(asset_, owner_, autoDeposit_, baseValues); - // withdraw collateral, swap, repay flashloan - _reduceLeverage(isFullWithdraw, assetsToWithdraw, flashLoanDebt); - } else { - // flash loan is to leverage UP - _redepositAsset(amounts[0], depositAmount); - } - - return true; + // swap logic - curve + stableSwapPool = ICurveMetapool(strategyValues.curvePool); + IERC20(stETH).approve(address(stableSwapPool), type(uint256).max); } - /*////////////////////////////////////////////////////////////// - INTERNAL HOOKS LOGIC - //////////////////////////////////////////////////////////////*/ - - /// @notice Deposit wstETH into lending protocol - function _protocolDeposit( - uint256 assets, - uint256, - bytes memory - ) internal override { - // deposit wstETH into aave - receive aToken here - lendingPool.supply(asset(), assets, address(this), 0); + // provides conversion from ETH to wstETH + function _toCollateralValue( + uint256 ethAmount + ) internal view override returns (uint256 wstETHAmount) { + wstETHAmount = ILido(stETH).getSharesByPooledEth(ethAmount); } - /// @notice repay part of the vault debt and withdraw wstETH - function _protocolWithdraw( - uint256 assets, - uint256, - bytes memory - ) internal override { - (, uint256 currentDebt, uint256 currentCollateral) = _getCurrentLTV(); - uint256 ethAssetsValue = ILido(stETH).getPooledEthByShares(assets); - bool isFullWithdraw; - uint256 ratioDebtToRepay; - - { - uint256 debtSlippage = currentDebt.mulDiv( - slippage, - 1e18, - Math.Rounding.Ceil - ); - - // find the % of debt to repay as the % of collateral being withdrawn - ratioDebtToRepay = ethAssetsValue.mulDiv( - 1e18, - (currentCollateral - currentDebt - debtSlippage), - Math.Rounding.Floor - ); - - isFullWithdraw = - assets == _totalAssets() || - ratioDebtToRepay >= 1e18; - } - - // get the LTV we would have without repaying debt - uint256 futureLTV = isFullWithdraw - ? type(uint256).max - : currentDebt.mulDiv( - 1e18, - (currentCollateral - ethAssetsValue), - Math.Rounding.Floor - ); - - if (futureLTV <= maxLTV || currentDebt == 0) { - // 1 - withdraw any asset amount with no debt - // 2 - withdraw assets with debt but the change doesn't take LTV above max - lendingPool.withdraw(asset(), assets, address(this)); - } else { - // 1 - withdraw assets but repay debt - uint256 debtToRepay = isFullWithdraw - ? currentDebt - : currentDebt.mulDiv( - ratioDebtToRepay, - 1e18, - Math.Rounding.Floor - ); - - // flash loan debtToRepay - mode 0 - flash loan is repaid at the end - _flashLoanETH(debtToRepay, 0, assets, 0, isFullWithdraw); - } - - // reverts if LTV got above max - _assertHealthyLTV(); - } - - // deposit back into the protocol - // either from flash loan or simply ETH dust held by the adapter - function _redepositAsset( - uint256 borrowAmount, - uint256 depositAmount - ) internal { - address wstETH = asset(); - - if (borrowAmount > 0) { - // unwrap into ETH the flash loaned amount - weth.withdraw(borrowAmount); - } - - // stake borrowed eth and receive wstETH - (bool sent, ) = wstETH.call{value: depositAmount}(""); - require(sent, "Fail to send eth to wstETH"); - - // get wstETH balance after staking - // may include eventual wstETH dust held by contract somehow - // in that case it will just add more collateral - uint256 wstETHAmount = IERC20(wstETH).balanceOf(address(this)); - - // deposit wstETH into lending protocol - _protocolDeposit(wstETHAmount, 0, hex""); - } - - // reduce leverage by withdrawing wstETH, swapping to ETH repaying ETH debt - // repayAmount is a ETH (wETH) amount - function _reduceLeverage( - bool isFullWithdraw, - uint256 toWithdraw, - uint256 flashLoanDebt - ) internal { - address asset = asset(); - - // get flash loan amount converted in wstETH - uint256 flashLoanWstETHAmount = ILido(stETH).getSharesByPooledEth( - flashLoanDebt - ); - - // get slippage buffer for swapping with flashLoanDebt as minAmountOut - uint256 wstETHBuffer = flashLoanWstETHAmount.mulDiv( - slippage, - 1e18, - Math.Rounding.Floor - ); - - // withdraw wstETH from aave - if (isFullWithdraw) { - // withdraw all - lendingPool.withdraw(asset, type(uint256).max, address(this)); - } else { - lendingPool.withdraw( - asset, - flashLoanWstETHAmount + wstETHBuffer + toWithdraw, - address(this) - ); - } - - // unwrap wstETH into stETH - uint256 stETHAmount = IwstETH(asset).unwrap( - flashLoanWstETHAmount + wstETHBuffer - ); - - // swap stETH for ETH and deposit into WETH - will be pulled by AAVE pool as flash loan repayment - _swapToWETH(stETHAmount, flashLoanDebt, asset, toWithdraw); - } - - // returns current loan to value, debt and collateral (token) amounts - function _getCurrentLTV() - internal - view - returns (uint256 loanToValue, uint256 debt, uint256 collateral) - { - debt = debtToken.balanceOf(address(this)); // ETH DEBT - collateral = ILido(stETH).getPooledEthByShares( - interestToken.balanceOf(address(this)) - ); // converted into ETH amount; - - (debt == 0 || collateral == 0) ? loanToValue = 0 : loanToValue = debt - .mulDiv(1e18, collateral, Math.Rounding.Ceil); - } - - // reverts if targetLTV < maxLTV < protocolLTV is not satisfied - function _verifyLTV( - uint256 _targetLTV, - uint256 _maxLTV, - uint256 _protocolLTV - ) internal pure { - if (_targetLTV >= _maxLTV) { - revert InvalidLTV(_targetLTV, _maxLTV, _protocolLTV); - } - if (_maxLTV >= _protocolLTV) { - revert InvalidLTV(_targetLTV, _maxLTV, _protocolLTV); - } - } - - // verify that currentLTV is not above maxLTV - function _assertHealthyLTV() internal view { - (uint256 currentLTV, , ) = _getCurrentLTV(); - - if (currentLTV > maxLTV) { - revert BadLTV(currentLTV, maxLTV); - } - } - - // borrow WETH from lending protocol - // interestRateMode = 2 -> flash loan eth and deposit into cdp, don't repay - // interestRateMode = 0 -> flash loan eth to repay cdp, have to repay flash loan at the end - function _flashLoanETH( - uint256 borrowAmount, - uint256 depositAmount, - uint256 assetsToWithdraw, - uint256 interestRateMode, - bool isFullWithdraw - ) internal { - uint256 depositAmount_ = depositAmount; // avoids stack too deep - - address[] memory assets = new address[](1); - assets[0] = address(weth); - - uint256[] memory amounts = new uint256[](1); - amounts[0] = borrowAmount; - - uint256[] memory interestRateModes = new uint256[](1); - interestRateModes[0] = interestRateMode; - - lendingPool.flashLoan( - address(this), - assets, - amounts, - interestRateModes, - address(this), - abi.encode( - interestRateMode == 0 ? true : false, - isFullWithdraw, - assetsToWithdraw, - depositAmount_ - ), - 0 - ); + // provides conversion from wstETH to ETH + function _toDebtValue( + uint256 wstETHAmount + ) internal view override returns (uint256 ethAmount) { + ethAmount = ILido(stETH).getPooledEthByShares(wstETHAmount); } - // swaps stETH to WETH - function _swapToWETH( + // wstETH to exact weth + function _convertCollateralToDebt( uint256 amount, uint256 minAmount, address asset, - uint256 wstETHToWithdraw - ) internal returns (uint256 amountETHReceived) { + uint256 assetsToWithdraw + ) internal override { + // unwrap wstETH into stETH + uint256 stETHAmount = IwstETH(asset).unwrap(amount); + // swap to ETH - amountETHReceived = stableSwapStETH.exchange( - STETHID, - WETHID, - amount, - minAmount - ); + stableSwapPool.exchange(STETHID, WETHID, stETHAmount, minAmount); - // wrap precise amount of eth for flash loan repayment - weth.deposit{value: minAmount}(); + // wrap precise amount of ETH for flash loan repayment + IWETH(address(borrowAsset)).deposit{value: minAmount}(); // restake the eth needed to reach the wstETH amount the user is withdrawing - uint256 missingWstETH = wstETHToWithdraw - + uint256 missingWstETH = assetsToWithdraw - IERC20(asset).balanceOf(address(this)) + 1; if (missingWstETH > 0) { - uint256 ethAmount = ILido(stETH).getPooledEthByShares( - missingWstETH - ); + uint256 ethAmount = _toDebtValue(missingWstETH); // stake eth to receive wstETH (bool sent, ) = asset.call{value: ethAmount}(""); @@ -503,115 +86,60 @@ contract WstETHLooper is BaseStrategy, IFlashLoanReceiver { } } - /*////////////////////////////////////////////////////////////// - MANAGEMENT LOGIC - //////////////////////////////////////////////////////////////*/ - - function setHarvestValues(address curveSwapPool) external onlyOwner { - // reset old pool - IERC20(stETH).approve(address(stableSwapStETH), 0); - - // set and approve new one - stableSwapStETH = ICurveMetapool(curveSwapPool); - IERC20(stETH).approve(address(stableSwapStETH), type(uint256).max); - } - - function harvest(bytes memory) external override onlyKeeperOrOwner { - adjustLeverage(); + // unwrap weth and stakes into wstETH + function _convertDebtToCollateral( + uint256 debtAmount, + uint256 totCollateralAmount + ) internal override { + if (debtAmount > 0) IWETH(address(borrowAsset)).withdraw(debtAmount); - emit Harvested(); + // stake borrowed eth and receive wstETH + (bool sent, ) = asset().call{value: totCollateralAmount}(""); + require(sent, "Fail to send eth to wstETH"); } - // amount of WETH to borrow OR amount of WETH to repay (converted into wstETH amount internally) - function adjustLeverage() public { - // get vault current leverage : debt/collateral - ( - uint256 currentLTV, - uint256 currentDebt, - uint256 currentCollateral - ) = _getCurrentLTV(); - - // de-leverage if vault LTV is higher than target - if (currentLTV > targetLTV) { - uint256 amountETH = (currentDebt - - ( - targetLTV.mulDiv( - (currentCollateral), - 1e18, - Math.Rounding.Floor - ) - )).mulDiv(1e18, (1e18 - targetLTV), Math.Rounding.Ceil); + // assign balancer data for swaps + function _setHarvestValues(bytes memory harvestValues) internal override { + address curveSwapPool = abi.decode(harvestValues, (address)); + if (curveSwapPool != address(stableSwapPool)) { + // reset old pool + IERC20(stETH).approve(address(stableSwapPool), 0); - // flash loan eth to repay part of the debt - _flashLoanETH(amountETH, 0, 0, 0, false); - } else { - uint256 amountETH = (targetLTV.mulDiv( - currentCollateral, - 1e18, - Math.Rounding.Ceil - ) - currentDebt).mulDiv( - 1e18, - (1e18 - targetLTV), - Math.Rounding.Ceil - ); - - uint256 dustBalance = address(this).balance; - if (dustBalance < amountETH) { - // flashloan but use eventual ETH dust remained in the contract as well - uint256 borrowAmount = amountETH - dustBalance; - - // flash loan WETH from lending protocol and add to cdp - _flashLoanETH(borrowAmount, amountETH, 0, 2, false); - } else { - // deposit the dust as collateral- borrow amount is zero - // leverage naturally decreases - _redepositAsset(0, dustBalance); - } + // set and approve new one + stableSwapPool = ICurveMetapool(curveSwapPool); + IERC20(stETH).approve(address(stableSwapPool), type(uint256).max); } - - // reverts if LTV got above max - _assertHealthyLTV(); - } - - function withdrawDust(address recipient) public onlyOwner { - // send eth dust to recipient - (bool sent, ) = address(recipient).call{value: address(this).balance}( - "" - ); - require(sent, "Failed to send ETH"); } - function setLeverageValues( - uint256 targetLTV_, - uint256 maxLTV_ - ) external onlyOwner { - // reverts if targetLTV < maxLTV < protocolLTV is not satisfied - _verifyLTV(targetLTV_, maxLTV_, protocolMaxLTV); - - targetLTV = targetLTV_; - maxLTV = maxLTV_; - - adjustLeverage(); + function _setEfficiencyMode() internal override { + // ETH correlated + lendingPool.setUserEMode(uint8(1)); } - function setSlippage(uint256 slippage_) external onlyOwner { - if (slippage_ > 2e17) revert InvalidSlippage(slippage_, 2e17); - - slippage = slippage_; + // reads max ltv on efficiency mode + function _getMaxLTV() internal override returns (uint256 protocolMaxLTV) { + // get protocol LTV + DataTypes.EModeData memory emodeData = lendingPool.getEModeCategoryData( + uint8(1) + ); + protocolMaxLTV = uint256(emodeData.maxLTV) * 1e14; // make it 18 decimals to compare; } - bool internal initCollateral; - - function setUserUseReserveAsCollateral(uint256 amount) external onlyOwner { - if (initCollateral) revert InvalidInitialization(); - address asset_ = asset(); - - IERC20(asset_).safeTransferFrom(msg.sender, address(this), amount); - lendingPool.supply(asset_, amount, address(this), 0); - - lendingPool.setUserUseReserveAsCollateral(asset_, true); + function _withdrawDust(address recipient) internal override { + // send ETH dust to recipient + uint256 ethBalance = address(this).balance; + if (ethBalance > 0 && totalSupply() == 0) { + (bool sent, ) = address(recipient).call{ + value: address(this).balance + }(""); + require(sent, "Failed to send ETH"); + } - initCollateral = true; + // send wstETH + uint256 wstETHBalance = IERC20(asset()).balanceOf(address(this)); + if (totalSupply() == 0 && wstETHBalance > 0) { + IERC20(asset()).transfer(recipient, wstETHBalance); + } } /*////////////////////////////////////////////////////////////// diff --git a/test/strategies/lido/WstETHLooper.t.sol b/test/strategies/lido/WstETHLooper.t.sol index eefc0274..42ec82d3 100644 --- a/test/strategies/lido/WstETHLooper.t.sol +++ b/test/strategies/lido/WstETHLooper.t.sol @@ -5,14 +5,14 @@ pragma solidity ^0.8.25; import { WstETHLooper, - LooperInitValues, + BaseAaveLeverageStrategy, + LooperValues, + LooperBaseValues, IERC20, - IERC20Metadata, - IwstETH, - ILendingPool, - Math + IwstETH } from "src/strategies/lido/WstETHLooper.sol"; import {BaseStrategyTest, IBaseStrategy, TestConfig, stdJson, Math} from "../BaseStrategyTest.sol"; +import {IERC20Metadata, ILendingPool, Math} from "src/strategies/BaseAaveLeverageStrategy.sol"; contract WstETHLooperTest is BaseStrategyTest { using stdJson for string; @@ -28,7 +28,7 @@ contract WstETHLooperTest is BaseStrategyTest { uint256 slippage; function setUp() public { - _setUpBaseTest(1, "./test/strategies/lido/WstETHLooperTestConfig.json"); + _setUpBaseTest(0, "./test/strategies/lido/WstETHLooperTestConfig.json"); } function _setUpStrategy(string memory json_, string memory index_, TestConfig memory testConfig_) @@ -37,13 +37,24 @@ contract WstETHLooperTest is BaseStrategyTest { returns (IBaseStrategy) { // Read strategy init values - LooperInitValues memory looperInitValues = - abi.decode(json_.parseRaw(string.concat(".configs[", index_, "].specific.init")), (LooperInitValues)); + LooperBaseValues memory baseValues = abi.decode( + json_.parseRaw( + string.concat(".configs[", index_, "].specific.base") + ), + (LooperBaseValues) + ); + LooperValues memory looperInitValues = abi.decode( + json_.parseRaw( + string.concat(".configs[", index_, "].specific.init") + ), + (LooperValues) + ); // Deploy Strategy WstETHLooper strategy = new WstETHLooper(); - strategy.initialize(testConfig_.asset, address(this), true, abi.encode(looperInitValues)); + strategy.initialize(testConfig_.asset, address(this), true, abi.encode(baseValues, + looperInitValues)); strategyContract = WstETHLooper(payable(strategy)); @@ -86,13 +97,24 @@ contract WstETHLooperTest is BaseStrategyTest { } function test__initialization() public override { - LooperInitValues memory looperInitValues = - abi.decode(json.parseRaw(string.concat(".configs[1].specific.init")), (LooperInitValues)); + LooperBaseValues memory baseValues = abi.decode( + json.parseRaw( + string.concat(".configs[0].specific.base") + ), + (LooperBaseValues) + ); + + LooperValues memory looperInitValues = abi.decode( + json.parseRaw( + string.concat(".configs[0].specific.init") + ), + (LooperValues) + ); // Deploy Strategy WstETHLooper strategy = new WstETHLooper(); - strategy.initialize(testConfig.asset, address(this), true, abi.encode(looperInitValues)); + strategy.initialize(testConfig.asset, address(this), true, abi.encode(baseValues, looperInitValues)); verify_adapterInit(); } @@ -143,15 +165,15 @@ contract WstETHLooperTest is BaseStrategyTest { } function test__setHarvestValues() public { - address oldPool = address(strategyContract.stableSwapStETH()); + address oldPool = address(strategyContract.stableSwapPool()); address newPool = address(0x85dE3ADd465a219EE25E04d22c39aB027cF5C12E); address stETH = strategyContract.stETH(); - strategyContract.setHarvestValues(newPool); + strategyContract.setHarvestValues(abi.encode(newPool)); uint256 oldAllowance = IERC20(stETH).allowance(address(strategy), oldPool); uint256 newAllowance = IERC20(stETH).allowance(address(strategy), newPool); - assertEq(address(strategyContract.stableSwapStETH()), newPool); + assertEq(address(strategyContract.stableSwapPool()), newPool); assertEq(oldAllowance, 0); assertEq(newAllowance, type(uint256).max); } @@ -459,13 +481,13 @@ contract WstETHLooperTest is BaseStrategyTest { function test__setLeverageValues_invalidInputs() public { // protocolLTV < targetLTV < maxLTV vm.expectRevert( - abi.encodeWithSelector(WstETHLooper.InvalidLTV.selector, 3e18, 4e18, strategyContract.protocolMaxLTV()) + abi.encodeWithSelector(BaseAaveLeverageStrategy.InvalidLTV.selector, 3e18, 4e18, strategyContract.protocolMaxLTV()) ); strategyContract.setLeverageValues(3e18, 4e18); // maxLTV < targetLTV < protocolLTV vm.expectRevert( - abi.encodeWithSelector(WstETHLooper.InvalidLTV.selector, 4e17, 3e17, strategyContract.protocolMaxLTV()) + abi.encodeWithSelector(BaseAaveLeverageStrategy.InvalidLTV.selector, 4e17, 3e17, strategyContract.protocolMaxLTV()) ); strategyContract.setLeverageValues(4e17, 3e17); } @@ -482,7 +504,7 @@ contract WstETHLooperTest is BaseStrategyTest { function test__setSlippage_invalidValue() public { uint256 newSlippage = 1e18; // 100% - vm.expectRevert(abi.encodeWithSelector(WstETHLooper.InvalidSlippage.selector, newSlippage, 2e17)); + vm.expectRevert(abi.encodeWithSelector(BaseAaveLeverageStrategy.InvalidSlippage.selector, newSlippage, 2e17)); strategyContract.setSlippage(newSlippage); } @@ -492,30 +514,30 @@ contract WstETHLooperTest is BaseStrategyTest { uint256[] memory premiums = new uint256[](1); // reverts with invalid msg.sender and valid initiator - vm.expectRevert(WstETHLooper.NotFlashLoan.selector); + vm.expectRevert(BaseAaveLeverageStrategy.NotFlashLoan.selector); vm.prank(bob); strategyContract.executeOperation(assets, amounts, premiums, address(strategy), ""); // reverts with invalid initiator and valid msg.sender - vm.expectRevert(WstETHLooper.NotFlashLoan.selector); + vm.expectRevert(BaseAaveLeverageStrategy.NotFlashLoan.selector); vm.prank(address(lendingPool)); strategyContract.executeOperation(assets, amounts, premiums, address(bob), ""); } - function test__harvest() public override { - _mintAssetAndApproveForStrategy(100e18, bob); + // function test__harvest() public override { + // _mintAssetAndApproveForStrategy(100e18, bob); - vm.prank(bob); - strategy.deposit(100e18, bob); + // vm.prank(bob); + // strategy.deposit(100e18, bob); - // LTV should be 0 - assertEq(strategyContract.getLTV(), 0); + // // LTV should be 0 + // assertEq(strategyContract.getLTV(), 0); - strategy.harvest(hex""); + // strategy.harvest(hex""); - // LTV should be at target now - assertApproxEqAbs(strategyContract.targetLTV(), strategyContract.getLTV(), 1, string.concat("ltv != expected")); - } + // // LTV should be at target now + // assertApproxEqAbs(strategyContract.targetLTV(), strategyContract.getLTV(), 1, string.concat("ltv != expected")); + // } /*////////////////////////////////////////////////////////////// INITIALIZATION @@ -525,7 +547,7 @@ contract WstETHLooperTest is BaseStrategyTest { assertEq(strategy.asset(), address(wstETH), "asset"); assertEq( IERC20Metadata(address(strategy)).name(), - string.concat("VaultCraft Leveraged ", IERC20Metadata(address(wstETH)).name(), " Adapter"), + string.concat("VaultCraft Leveraged ", IERC20Metadata(address(wstETH)).name(), " Strategy"), "name" ); assertEq( diff --git a/test/strategies/lido/WstETHLooperTestConfig.json b/test/strategies/lido/WstETHLooperTestConfig.json index 060edc60..a8588821 100644 --- a/test/strategies/lido/WstETHLooperTestConfig.json +++ b/test/strategies/lido/WstETHLooperTestConfig.json @@ -1,5 +1,5 @@ { - "length": 2, + "length": 1, "configs": [ { "base": { @@ -15,37 +15,16 @@ "testId": "WstETHLooper" }, "specific": { - "init": { - "aaveDataProvider": "0xFc21d6d146E6086B8359705C8b28512a983db0cb", - "curvePool": "0xDC24316b9AE028F1497c275EB9192a3Ea0f67022", - "maxLTV": 850000000000000000, - "poolAddressesProvider": "0x02C3eA4e34C0cBd694D2adFa2c690EECbC1793eE", - "slippage": 10000000000000000, - "targetLTV": 800000000000000000 - } - } - }, - { - "base": { - "asset": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", - "blockNumber": 19333530, - "defaultAmount": 1000000000000000000, - "delta": 10, - "maxDeposit": 1000000000000000000000, - "maxWithdraw": 1000000000000000000000, - "minDeposit": 1000000000000000, - "minWithdraw": 1000000000000000, - "network": "mainnet", - "testId": "WstETHLooper" - }, - "specific": { - "init": { + "base": { "aaveDataProvider": "0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3", - "curvePool": "0xDC24316b9AE028F1497c275EB9192a3Ea0f67022", + "borrowAsset": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "maxLTV": 850000000000000000, + "maxSlippage": 10000000000000000, "poolAddressesProvider": "0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e", - "slippage": 10000000000000000, "targetLTV": 800000000000000000 + }, + "init": { + "curvePool": "0xDC24316b9AE028F1497c275EB9192a3Ea0f67022" } } } From e7a70da1789f88d1012b80e662d07600fdc32426 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 20 Sep 2024 11:16:22 +0200 Subject: [PATCH 10/10] Fix rebase issues --- src/strategies/BaseAaveLeverageStrategy.sol | 7 +++++++ src/strategies/lido/WstETHLooper.sol | 19 ------------------- src/vaults/MultiStrategyVault.sol | 9 ++++++--- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/strategies/BaseAaveLeverageStrategy.sol b/src/strategies/BaseAaveLeverageStrategy.sol index 79a6b2b2..cfab6c84 100644 --- a/src/strategies/BaseAaveLeverageStrategy.sol +++ b/src/strategies/BaseAaveLeverageStrategy.sol @@ -182,6 +182,13 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { (ltv, , ) = _getCurrentLTV(); } + function convertToUnderlyingShares( + uint256 assets, + uint256 shares + ) public view override returns (uint256) { + revert(); + } + /*////////////////////////////////////////////////////////////// MANAGEMENT LOGIC //////////////////////////////////////////////////////////////*/ diff --git a/src/strategies/lido/WstETHLooper.sol b/src/strategies/lido/WstETHLooper.sol index 6469ce2e..0e9918b1 100644 --- a/src/strategies/lido/WstETHLooper.sol +++ b/src/strategies/lido/WstETHLooper.sol @@ -141,23 +141,4 @@ contract WstETHLooper is BaseAaveLeverageStrategy { IERC20(asset()).transfer(recipient, wstETHBalance); } } - - /*////////////////////////////////////////////////////////////// - NOT IMPLEMENTED - //////////////////////////////////////////////////////////////*/ - - function convertToUnderlyingShares( - uint256 assets, - uint256 shares - ) public view override returns (uint256) { - revert(); - } - - function claim() internal override returns (bool success) { - revert(); - } - - function rewardTokens() external view override returns (address[] memory) { - revert(); - } } diff --git a/src/vaults/MultiStrategyVault.sol b/src/vaults/MultiStrategyVault.sol index 6cecdd34..e30d31ee 100644 --- a/src/vaults/MultiStrategyVault.sol +++ b/src/vaults/MultiStrategyVault.sol @@ -405,7 +405,7 @@ contract MultiStrategyVault is address(strategy), withdrawableAssets ); - } + } } } } @@ -660,8 +660,11 @@ contract MultiStrategyVault is * @param allocations An array of structs each including the strategyIndex to withdraw from and the amount of assets */ function pullFunds(Allocation[] calldata allocations) external onlyOwner { + _pullFunds(allocations); + } + + function _pullFunds(Allocation[] calldata allocations) internal { uint256 len = allocations.length; - for (uint256 i; i < len; i++) { if (allocations[i].amount > 0) { strategies[allocations[i].index].withdraw( @@ -893,4 +896,4 @@ contract MultiStrategyVault is ) ); } -} +} \ No newline at end of file