diff --git a/src/strategies/BaseAaveLeverageStrategy.sol b/src/strategies/BaseAaveLeverageStrategy.sol index 785fb8f1..f244035b 100644 --- a/src/strategies/BaseAaveLeverageStrategy.sol +++ b/src/strategies/BaseAaveLeverageStrategy.sol @@ -190,9 +190,15 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { revert(); } + /// @notice The token rewarded if the aave liquidity mining is active + function rewardTokens() external view override returns (address[] memory) { + return aaveIncentives.getRewardsByAsset(asset()); + } + /*////////////////////////////////////////////////////////////// - MANAGEMENT LOGIC + LEVERAGE LOGIC //////////////////////////////////////////////////////////////*/ + function adjustLeverage() public { // get vault current leverage : debt/collateral ( @@ -212,8 +218,7 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { ) )).mulDiv(1e18, (1e18 - targetLTV), Math.Rounding.Ceil); - // flash loan debt asset to repay part of the debt - _flashLoan(borrowAmount, 0, 0, 0, false, slippage); + _leverDown(borrowAmount, slippage); } else { uint256 depositAmount = (targetLTV.mulDiv( currentCollateral, @@ -225,30 +230,33 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { Math.Rounding.Ceil ); - uint256 dustBalance = address(this).balance; + _leverUp(depositAmount); + } - if (dustBalance < depositAmount) { - // flashloan but use eventual collateral dust remained in the contract as well - uint256 borrowAmount = depositAmount - dustBalance; + // reverts if LTV got above max + _assertHealthyLTV(); + } - // 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()); - } - } + function leverUp(uint256 depositAmount) public onlyKeeperOrOwner { + _leverUp(depositAmount); // reverts if LTV got above max _assertHealthyLTV(); } - /// @notice The token rewarded if the aave liquidity mining is active - function rewardTokens() external view override returns (address[] memory) { - return aaveIncentives.getRewardsByAsset(asset()); + function leverDown( + uint256 borrowAmount, + uint256 slippage + ) public onlyKeeperOrOwner { + _leverDown(borrowAmount, slippage); + // reverts if LTV got above max + _assertHealthyLTV(); } + /*////////////////////////////////////////////////////////////// + HARVEST LOGIC + //////////////////////////////////////////////////////////////*/ + /// @notice Claim additional rewards given that it's active. function claim() internal override returns (bool success) { if (address(aaveIncentives) == address(0)) return false; @@ -286,6 +294,10 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { emit Harvested(); } + /*////////////////////////////////////////////////////////////// + MANAGEMENT LOGIC + //////////////////////////////////////////////////////////////*/ + function setHarvestValues(bytes memory harvestValues) external onlyOwner { _setHarvestValues(harvestValues); } @@ -323,10 +335,6 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { initCollateral = true; } - function withdrawDust(address recipient) public onlyOwner { - _withdrawDust(recipient); - } - /*////////////////////////////////////////////////////////////// FLASH LOAN LOGIC //////////////////////////////////////////////////////////////*/ @@ -427,6 +435,14 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { ); } + /*////////////////////////////////////////////////////////////// + OTHER LOGIC + //////////////////////////////////////////////////////////////*/ + + function withdrawDust(address recipient) public onlyOwner { + _withdrawDust(recipient); + } + /*////////////////////////////////////////////////////////////// INTERNAL HOOKS LOGIC //////////////////////////////////////////////////////////////*/ @@ -503,6 +519,27 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { _assertHealthyLTV(); } + function _leverUp(uint256 depositAmount) internal { + 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()); + } + } + + function _leverDown(uint256 borrowAmount, uint256 slippage) internal { + // flash loan debt asset to repay part of the debt + _flashLoan(borrowAmount, 0, 0, 0, false, slippage); + } + ///@notice called after a flash loan to repay cdp function _reduceLeverage( bool isFullWithdraw, @@ -607,7 +644,7 @@ abstract contract BaseAaveLeverageStrategy is BaseStrategy, IFlashLoanReceiver { } /*////////////////////////////////////////////////////////////// - TO OVERRIDE IN IMPLEMENTATION + TO OVERRIDE IN IMPLEMENTATION //////////////////////////////////////////////////////////////*/ // must provide conversion from debt asset to vault (collateral) asset diff --git a/test/strategies/stader/EthXLooper.t.sol b/test/strategies/stader/EthXLooper.t.sol index 763ee068..37875ce6 100644 --- a/test/strategies/stader/EthXLooper.t.sol +++ b/test/strategies/stader/EthXLooper.t.sol @@ -3,14 +3,7 @@ pragma solidity ^0.8.25; -import { - ETHXLooper, - BaseAaveLeverageStrategy, - LooperBaseValues, - LooperValues, - IERC20, - IETHxStaking -} 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"; @@ -58,7 +51,12 @@ contract ETHXLooperTest is BaseStrategyTest { // Deploy Strategy ETHXLooper strategy = new ETHXLooper(); - strategy.initialize(testConfig_.asset, address(this), true, abi.encode(baseValues, looperInitValues)); + strategy.initialize( + testConfig_.asset, + address(this), + true, + abi.encode(baseValues, looperInitValues) + ); strategyContract = ETHXLooper(payable(strategy)); @@ -105,23 +103,24 @@ contract ETHXLooperTest is BaseStrategyTest { function test__initialization() public override { LooperBaseValues memory baseValues = abi.decode( - json.parseRaw( - string.concat(".configs[0].specific.base") - ), + json.parseRaw(string.concat(".configs[0].specific.base")), (LooperBaseValues) ); LooperValues memory looperInitValues = abi.decode( - json.parseRaw( - string.concat(".configs[0].specific.init") - ), + json.parseRaw(string.concat(".configs[0].specific.init")), (LooperValues) ); // Deploy Strategy ETHXLooper strategy = new ETHXLooper(); - strategy.initialize(testConfig.asset, address(this), true, abi.encode(baseValues, looperInitValues)); + strategy.initialize( + testConfig.asset, + address(this), + true, + abi.encode(baseValues, looperInitValues) + ); verify_adapterInit(); } @@ -215,8 +214,14 @@ contract ETHXLooperTest is BaseStrategyTest { address asset = strategy.asset(); strategyContract.setHarvestValues(abi.encode(newPool)); - uint256 oldAllowance = IERC20(asset).allowance(address(strategy), oldPool); - uint256 newAllowance = IERC20(asset).allowance(address(strategy), 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); @@ -531,13 +536,23 @@ contract ETHXLooperTest is BaseStrategyTest { function test__setLeverageValues_invalidInputs() public { // protocolLTV < targetLTV < maxLTV vm.expectRevert( - abi.encodeWithSelector(BaseAaveLeverageStrategy.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(BaseAaveLeverageStrategy.InvalidLTV.selector, 4e17, 3e17, strategyContract.protocolMaxLTV()) + abi.encodeWithSelector( + BaseAaveLeverageStrategy.InvalidLTV.selector, + 4e17, + 3e17, + strategyContract.protocolMaxLTV() + ) ); strategyContract.setLeverageValues(4e17, 3e17); } @@ -554,7 +569,13 @@ contract ETHXLooperTest is BaseStrategyTest { function test__setSlippage_invalidValue() public { uint256 newSlippage = 1e18; // 100% - vm.expectRevert(abi.encodeWithSelector(BaseAaveLeverageStrategy.InvalidSlippage.selector, newSlippage, 2e17)); + vm.expectRevert( + abi.encodeWithSelector( + BaseAaveLeverageStrategy.InvalidSlippage.selector, + newSlippage, + 2e17 + ) + ); strategyContract.setSlippage(newSlippage); } @@ -601,6 +622,84 @@ contract ETHXLooperTest is BaseStrategyTest { // assertApproxEqAbs(strategyContract.targetLTV(), strategyContract.getLTV(), _delta_, string.concat("ltv != expected")); // } + function test__leverUp() public { + uint256 amountDeposit = 1e18; + deal(address(ethX), bob, amountDeposit); + + vm.startPrank(bob); + ethX.approve(address(strategy), amountDeposit); + strategy.deposit(amountDeposit, bob); + vm.stopPrank(); + + uint256 initialABalance = aEthX.balanceOf(address(strategy)); + uint256 initialLTV = strategyContract.getLTV(); + uint256 depositAmount = 0.5e18; // Example deposit amount + + // Call leverUp with a specific deposit amount + strategyContract.leverUp(depositAmount); + + uint256 finalABalance = aEthX.balanceOf(address(strategy)); + uint256 finalLTV = strategyContract.getLTV(); + + // Check that the aToken balance has increased + assertGt( + finalABalance, + initialABalance, + "aToken balance should increase after leverUp" + ); + + // Check that the LTV has increased + assertGt(finalLTV, initialLTV, "LTV should increase after leverUp"); + + // Check that the final LTV is not above the max LTV + assertLe( + finalLTV, + strategyContract.maxLTV(), + "Final LTV should not exceed max LTV" + ); + } + + function test__leverDown() public { + uint256 amountDeposit = 1e18; + deal(address(ethX), bob, amountDeposit); + + vm.startPrank(bob); + ethX.approve(address(strategy), amountDeposit); + strategy.deposit(amountDeposit, bob); + vm.stopPrank(); + + // First, lever up to create some debt + strategyContract.leverUp(0.5e18); + + uint256 initialABalance = aEthX.balanceOf(address(strategy)); + uint256 initialLTV = strategyContract.getLTV(); + uint256 borrowAmount = 0.2e18; // Example borrow amount to reduce + uint256 slippage = 1e16; // 1% slippage + + // Now call leverDown + strategyContract.leverDown(borrowAmount, slippage); + + uint256 finalABalance = aEthX.balanceOf(address(strategy)); + uint256 finalLTV = strategyContract.getLTV(); + + // Check that the aToken balance has decreased + assertLt( + finalABalance, + initialABalance, + "aToken balance should decrease after leverDown" + ); + + // Check that the LTV has decreased + assertLt(finalLTV, initialLTV, "LTV should decrease after leverDown"); + + // Check that the final LTV is not above the max LTV + assertLe( + finalLTV, + strategyContract.maxLTV(), + "Final LTV should not exceed max LTV" + ); + } + /*////////////////////////////////////////////////////////////// INITIALIZATION //////////////////////////////////////////////////////////////*/ @@ -609,7 +708,11 @@ contract ETHXLooperTest is BaseStrategyTest { assertEq(strategy.asset(), address(ethX), "asset"); assertEq( IERC20Metadata(address(strategy)).name(), - string.concat("VaultCraft Leveraged ", IERC20Metadata(address(ethX)).name(), " Strategy"), + string.concat( + "VaultCraft Leveraged ", + IERC20Metadata(address(ethX)).name(), + " Strategy" + ), "name" ); assertEq( diff --git a/test/strategies/stader/MaticXLooper.t.sol b/test/strategies/stader/MaticXLooper.t.sol index 56875ea7..b0517a11 100644 --- a/test/strategies/stader/MaticXLooper.t.sol +++ b/test/strategies/stader/MaticXLooper.t.sol @@ -647,6 +647,84 @@ contract MaticXLooperTest is BaseStrategyTest { // ); // } + function test__leverUp() public { + uint256 amountDeposit = 1e18; + deal(address(maticX), bob, amountDeposit); + + vm.startPrank(bob); + maticX.approve(address(strategy), amountDeposit); + strategy.deposit(amountDeposit, bob); + vm.stopPrank(); + + uint256 initialABalance = aMaticX.balanceOf(address(strategy)); + uint256 initialLTV = strategyContract.getLTV(); + uint256 depositAmount = 0.5e18; // Example deposit amount + + // Call leverUp with a specific deposit amount + strategyContract.leverUp(depositAmount); + + uint256 finalABalance = aMaticX.balanceOf(address(strategy)); + uint256 finalLTV = strategyContract.getLTV(); + + // Check that the aToken balance has increased + assertGt( + finalABalance, + initialABalance, + "aToken balance should increase after leverUp" + ); + + // Check that the LTV has increased + assertGt(finalLTV, initialLTV, "LTV should increase after leverUp"); + + // Check that the final LTV is not above the max LTV + assertLe( + finalLTV, + strategyContract.maxLTV(), + "Final LTV should not exceed max LTV" + ); + } + + function test__leverDown() public { + uint256 amountDeposit = 1e18; + deal(address(maticX), bob, amountDeposit); + + vm.startPrank(bob); + maticX.approve(address(strategy), amountDeposit); + strategy.deposit(amountDeposit, bob); + vm.stopPrank(); + + // First, lever up to create some debt + strategyContract.leverUp(0.5e18); + + uint256 initialABalance = aMaticX.balanceOf(address(strategy)); + uint256 initialLTV = strategyContract.getLTV(); + uint256 borrowAmount = 0.2e18; // Example borrow amount to reduce + uint256 slippage = 1e16; // 1% slippage + + // Now call leverDown + strategyContract.leverDown(borrowAmount, slippage); + + uint256 finalABalance = aMaticX.balanceOf(address(strategy)); + uint256 finalLTV = strategyContract.getLTV(); + + // Check that the aToken balance has decreased + assertLt( + finalABalance, + initialABalance, + "aToken balance should decrease after leverDown" + ); + + // Check that the LTV has decreased + assertLt(finalLTV, initialLTV, "LTV should decrease after leverDown"); + + // Check that the final LTV is not above the max LTV + assertLe( + finalLTV, + strategyContract.maxLTV(), + "Final LTV should not exceed max LTV" + ); + } + /*////////////////////////////////////////////////////////////// INITIALIZATION //////////////////////////////////////////////////////////////*/