diff --git a/.forge-snapshots/EthAaveLeverageStrategyTest_test_claimExitedAssets1.snap b/.forge-snapshots/EthAaveLeverageStrategyTest_test_claimExitedAssets1.snap index f716520..4a0b2cf 100644 --- a/.forge-snapshots/EthAaveLeverageStrategyTest_test_claimExitedAssets1.snap +++ b/.forge-snapshots/EthAaveLeverageStrategyTest_test_claimExitedAssets1.snap @@ -1 +1 @@ -566045 \ No newline at end of file +566111 \ No newline at end of file diff --git a/.forge-snapshots/EthAaveLeverageStrategyTest_test_claimExitedAssets2.snap b/.forge-snapshots/EthAaveLeverageStrategyTest_test_claimExitedAssets2.snap index 54a9ade..9a47ebb 100644 --- a/.forge-snapshots/EthAaveLeverageStrategyTest_test_claimExitedAssets2.snap +++ b/.forge-snapshots/EthAaveLeverageStrategyTest_test_claimExitedAssets2.snap @@ -1 +1 @@ -551330 \ No newline at end of file +551396 \ No newline at end of file diff --git a/.forge-snapshots/EthAaveLeverageStrategyTest_test_claimExitedAssets3.snap b/.forge-snapshots/EthAaveLeverageStrategyTest_test_claimExitedAssets3.snap index aafe54d..e4fe359 100644 --- a/.forge-snapshots/EthAaveLeverageStrategyTest_test_claimExitedAssets3.snap +++ b/.forge-snapshots/EthAaveLeverageStrategyTest_test_claimExitedAssets3.snap @@ -1 +1 @@ -509091 \ No newline at end of file +509144 \ No newline at end of file diff --git a/.forge-snapshots/EthAaveLeverageStrategyTest_test_deposit_HasPosition.snap b/.forge-snapshots/EthAaveLeverageStrategyTest_test_deposit_HasPosition.snap index f666083..1c5ef75 100644 --- a/.forge-snapshots/EthAaveLeverageStrategyTest_test_deposit_HasPosition.snap +++ b/.forge-snapshots/EthAaveLeverageStrategyTest_test_deposit_HasPosition.snap @@ -1 +1 @@ -777310 \ No newline at end of file +778147 \ No newline at end of file diff --git a/.forge-snapshots/EthAaveLeverageStrategyTest_test_deposit_NoPosition.snap b/.forge-snapshots/EthAaveLeverageStrategyTest_test_deposit_NoPosition.snap index 6e22a51..ce628bf 100644 --- a/.forge-snapshots/EthAaveLeverageStrategyTest_test_deposit_NoPosition.snap +++ b/.forge-snapshots/EthAaveLeverageStrategyTest_test_deposit_NoPosition.snap @@ -1 +1 @@ -904108 \ No newline at end of file +904945 \ No newline at end of file diff --git a/.forge-snapshots/EthAaveLeverageStrategyTest_test_enterExitQueue.snap b/.forge-snapshots/EthAaveLeverageStrategyTest_test_enterExitQueue.snap index dd6100a..34633d1 100644 --- a/.forge-snapshots/EthAaveLeverageStrategyTest_test_enterExitQueue.snap +++ b/.forge-snapshots/EthAaveLeverageStrategyTest_test_enterExitQueue.snap @@ -1 +1 @@ -229074 \ No newline at end of file +229118 \ No newline at end of file diff --git a/.forge-snapshots/EthAaveLeverageStrategyTest_test_forceEnterExitQueue_borrowForceExitLtvPercent.snap b/.forge-snapshots/EthAaveLeverageStrategyTest_test_forceEnterExitQueue_borrowForceExitLtvPercent.snap index ae483a7..716cc1b 100644 --- a/.forge-snapshots/EthAaveLeverageStrategyTest_test_forceEnterExitQueue_borrowForceExitLtvPercent.snap +++ b/.forge-snapshots/EthAaveLeverageStrategyTest_test_forceEnterExitQueue_borrowForceExitLtvPercent.snap @@ -1 +1 @@ -275190 \ No newline at end of file +275237 \ No newline at end of file diff --git a/.forge-snapshots/EthAaveLeverageStrategyTest_test_forceEnterExitQueue_vaultForceExitLtvPercent.snap b/.forge-snapshots/EthAaveLeverageStrategyTest_test_forceEnterExitQueue_vaultForceExitLtvPercent.snap index 22128f1..df86726 100644 --- a/.forge-snapshots/EthAaveLeverageStrategyTest_test_forceEnterExitQueue_vaultForceExitLtvPercent.snap +++ b/.forge-snapshots/EthAaveLeverageStrategyTest_test_forceEnterExitQueue_vaultForceExitLtvPercent.snap @@ -1 +1 @@ -248522 \ No newline at end of file +248569 \ No newline at end of file diff --git a/.forge-snapshots/EthAaveLeverageStrategyTest_test_permit.snap b/.forge-snapshots/EthAaveLeverageStrategyTest_test_permit.snap index ede243d..469ec2b 100644 --- a/.forge-snapshots/EthAaveLeverageStrategyTest_test_permit.snap +++ b/.forge-snapshots/EthAaveLeverageStrategyTest_test_permit.snap @@ -1 +1 @@ -370257 \ No newline at end of file +370279 \ No newline at end of file diff --git a/.forge-snapshots/EthAaveLeverageStrategyTest_test_rescueLendingAssets1.snap b/.forge-snapshots/EthAaveLeverageStrategyTest_test_rescueLendingAssets1.snap index ff9d4ac..1f6f5b4 100644 --- a/.forge-snapshots/EthAaveLeverageStrategyTest_test_rescueLendingAssets1.snap +++ b/.forge-snapshots/EthAaveLeverageStrategyTest_test_rescueLendingAssets1.snap @@ -1 +1 @@ -611636 \ No newline at end of file +611702 \ No newline at end of file diff --git a/.forge-snapshots/EthAaveLeverageStrategyTest_test_rescueLendingAssets2.snap b/.forge-snapshots/EthAaveLeverageStrategyTest_test_rescueLendingAssets2.snap index 6f53861..796f1d0 100644 --- a/.forge-snapshots/EthAaveLeverageStrategyTest_test_rescueLendingAssets2.snap +++ b/.forge-snapshots/EthAaveLeverageStrategyTest_test_rescueLendingAssets2.snap @@ -1 +1 @@ -499998 \ No newline at end of file +500051 \ No newline at end of file diff --git a/.forge-snapshots/EthAaveLeverageStrategyTest_test_rescueVaultAssets.snap b/.forge-snapshots/EthAaveLeverageStrategyTest_test_rescueVaultAssets.snap index fa9cc4b..d7ef156 100644 --- a/.forge-snapshots/EthAaveLeverageStrategyTest_test_rescueVaultAssets.snap +++ b/.forge-snapshots/EthAaveLeverageStrategyTest_test_rescueVaultAssets.snap @@ -1 +1 @@ -649991 \ No newline at end of file +650027 \ No newline at end of file diff --git a/.forge-snapshots/EthAaveLeverageStrategyTest_test_upgradeProxy.snap b/.forge-snapshots/EthAaveLeverageStrategyTest_test_upgradeProxy.snap index 02e4cbb..554dc5c 100644 --- a/.forge-snapshots/EthAaveLeverageStrategyTest_test_upgradeProxy.snap +++ b/.forge-snapshots/EthAaveLeverageStrategyTest_test_upgradeProxy.snap @@ -1 +1 @@ -55699 \ No newline at end of file +55721 \ No newline at end of file diff --git a/script/DeployStakeHelpers.s.sol b/script/DeployStakeHelpers.s.sol index 83038f9..94ea484 100644 --- a/script/DeployStakeHelpers.s.sol +++ b/script/DeployStakeHelpers.s.sol @@ -10,15 +10,15 @@ contract DeployStakeHelpers is Script { struct ConfigParams { address keeper; address osTokenConfigV1; - address osTokenConfigV2; - address osTokenController; + address osTokenConfig; + address osTokenVaultController; } function _readEnvVariables() internal view returns (ConfigParams memory params) { params.keeper = vm.envAddress('KEEPER'); params.osTokenConfigV1 = vm.envAddress('OS_TOKEN_CONFIG_V1'); - params.osTokenConfigV2 = vm.envAddress('OS_TOKEN_CONFIG_V2'); - params.osTokenController = vm.envAddress('OS_TOKEN_CONTROLLER'); + params.osTokenConfig = vm.envAddress('OS_TOKEN_CONFIG'); + params.osTokenVaultController = vm.envAddress('OS_TOKEN_VAULT_CONTROLLER'); } function run() external { @@ -31,7 +31,7 @@ contract DeployStakeHelpers is Script { // Deploy StakeHelpers. StakeHelpers stakeHelpers = - new StakeHelpers(params.keeper, params.osTokenConfigV1, params.osTokenConfigV2, params.osTokenController); + new StakeHelpers(params.keeper, params.osTokenConfigV1, params.osTokenConfig, params.osTokenVaultController); console.log('StakeHelpers deployed at: ', address(stakeHelpers)); vm.stopBroadcast(); diff --git a/src/MerkleDistributor.sol b/src/MerkleDistributor.sol index 17e66e0..d544669 100644 --- a/src/MerkleDistributor.sol +++ b/src/MerkleDistributor.sol @@ -91,7 +91,11 @@ contract MerkleDistributor is Ownable2Step, EIP712, IMerkleDistributor { rewardsRoot = newRewardsRoot; // cannot overflow on human timescales lastUpdateTimestamp = uint64(block.timestamp); - nonce += 1; + + unchecked { + // cannot realistically overflow + nonce += 1; + } // emit event emit RewardsRootUpdated(msg.sender, newRewardsRoot, newRewardsIpfsHash); diff --git a/src/StakeHelpers.sol b/src/StakeHelpers.sol index 958ad81..68543a5 100644 --- a/src/StakeHelpers.sol +++ b/src/StakeHelpers.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.26; -import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; import {IOsTokenConfig as IOsTokenConfigV2} from '@stakewise-core/interfaces/IOsTokenConfig.sol'; import {IOsTokenVaultController} from '@stakewise-core/interfaces/IOsTokenVaultController.sol'; @@ -145,12 +144,6 @@ contract StakeHelpers is Multicall { if (stakeAssets < outputData.receivedAssets) { outputData.receivedAssets = stakeAssets; } - stakeAssets -= outputData.receivedAssets; - - // if less than 1% of stake assets left, add them to received assets - if (Math.mulDiv(outputData.receivedAssets, 0.01 ether, 1 ether) >= stakeAssets) { - outputData.receivedAssets += stakeAssets; - } outputData.exitQueueShares = Math.min(stakeShares, IVaultState(inputData.vault).convertToShares(outputData.receivedAssets)); } @@ -192,12 +185,6 @@ contract StakeHelpers is Multicall { if (stakeAssets < receivedAssets) { receivedAssets = stakeAssets; } - stakeAssets -= receivedAssets; - - // if less than 1% of stake assets left, add them to received assets - if (Math.mulDiv(receivedAssets, 0.01 ether, 1 ether) >= stakeAssets) { - receivedAssets += stakeAssets; - } receivedAssets += _osTokenController.convertToAssets(balanceOsTokenShares); } diff --git a/src/leverage/AaveLeverageStrategy.sol b/src/leverage/AaveLeverageStrategy.sol index 8c6eca6..a41a318 100644 --- a/src/leverage/AaveLeverageStrategy.sol +++ b/src/leverage/AaveLeverageStrategy.sol @@ -3,12 +3,11 @@ pragma solidity ^0.8.26; import {Math} from '@openzeppelin/contracts/utils/math/Math.sol'; -import {SafeCast} from '@openzeppelin/contracts/utils/math/SafeCast.sol'; import {IPool} from '@aave-core/interfaces/IPool.sol'; import {IScaledBalanceToken} from '@aave-core/interfaces/IScaledBalanceToken.sol'; import {WadRayMath} from '@aave-core/protocol/libraries/math/WadRayMath.sol'; import {IStrategyProxy} from '../interfaces/IStrategyProxy.sol'; -import {LeverageStrategy} from './LeverageStrategy.sol'; +import {LeverageStrategy, ILeverageStrategy} from './LeverageStrategy.sol'; /** * @title AaveLeverageStrategy @@ -68,8 +67,8 @@ abstract contract AaveLeverageStrategy is LeverageStrategy { _aaveVarDebtAssetToken = IScaledBalanceToken(aaveVarDebtAssetToken); } - /// @inheritdoc LeverageStrategy - function _getBorrowLtv() internal view override returns (uint256) { + /// @inheritdoc ILeverageStrategy + function getBorrowLtv() public view override returns (uint256) { // convert to 1e18 precision uint256 aaveLtv = uint256(_aavePool.getEModeCategoryCollateralConfig(_emodeCategory).ltv) * 1e14; @@ -82,10 +81,10 @@ abstract contract AaveLeverageStrategy is LeverageStrategy { return Math.min(aaveLtv, abi.decode(maxBorrowLtvPercentConfig, (uint256))); } - /// @inheritdoc LeverageStrategy - function _getBorrowState( + /// @inheritdoc ILeverageStrategy + function getBorrowState( address proxy - ) internal view override returns (uint256 borrowedAssets, uint256 suppliedOsTokenShares) { + ) public view override returns (uint256 borrowedAssets, uint256 suppliedOsTokenShares) { suppliedOsTokenShares = _aaveOsToken.scaledBalanceOf(proxy); if (suppliedOsTokenShares != 0) { uint256 normalizedIncome = _aavePool.getReserveNormalizedIncome(address(_osToken)); diff --git a/src/leverage/LeverageStrategy.sol b/src/leverage/LeverageStrategy.sol index 49e3263..2fcfe5d 100644 --- a/src/leverage/LeverageStrategy.sol +++ b/src/leverage/LeverageStrategy.sol @@ -122,7 +122,88 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { } /// @inheritdoc ILeverageStrategy - function deposit(address vault, uint256 osTokenShares) external { + function getFlashloanOsTokenShares(address vault, uint256 osTokenShares) public view returns (uint256) { + // fetch deposit and borrow LTVs + uint256 vaultLtv = getVaultLtv(vault); + uint256 borrowLtv = getBorrowLtv(); + + // calculate the amount of osToken shares that can be leveraged + uint256 totalLtv = Math.mulDiv(vaultLtv, borrowLtv, _wad); + return Math.mulDiv(osTokenShares, _wad, _wad - totalLtv) - osTokenShares; + } + + /// @inheritdoc ILeverageStrategy + function getVaultLtv( + address vault + ) public view returns (uint256) { + uint256 vaultLtvPercent = _osTokenConfig.getConfig(vault).ltvPercent; + // check whether there is max vault LTV percent set in the strategy config + bytes memory vaultMaxLtvPercentConfig = + _strategiesRegistry.getStrategyConfig(strategyId(), _maxVaultLtvPercentConfigName); + if (vaultMaxLtvPercentConfig.length == 0) { + return vaultLtvPercent; + } + return Math.min(vaultLtvPercent, abi.decode(vaultMaxLtvPercentConfig, (uint256))); + } + + /// @inheritdoc ILeverageStrategy + function getVaultState( + address vault, + address proxy + ) public view returns (uint256 stakedAssets, uint256 mintedOsTokenShares) { + // check harvested + if (IVaultState(vault).isStateUpdateRequired()) { + revert Errors.NotHarvested(); + } + + // fetch staked assets + uint256 stakedShares = IVaultState(vault).getShares(proxy); + if (stakedShares != 0) { + stakedAssets = IVaultState(vault).convertToAssets(stakedShares); + } + + // fetch minted osToken shares + mintedOsTokenShares = IVaultOsToken(vault).osTokenPositions(proxy); + } + + /// @inheritdoc ILeverageStrategy + function canForceEnterExitQueue(address vault, address user) public view returns (bool) { + address proxy = getStrategyProxy(vault, user); + bytes32 _strategyId = strategyId(); + + // check whether force exit vault LTV is set in the strategy config + bytes memory vaultForceExitLtvPercentConfig = + _strategiesRegistry.getStrategyConfig(_strategyId, _vaultForceExitLtvPercentConfigName); + if ( + vaultForceExitLtvPercentConfig.length != 0 + && _osTokenConfig.getConfig(vault).liqThresholdPercent != _vaultDisabledLiqThreshold + ) { + (uint256 stakedAssets, uint256 mintedOsTokenShares) = getVaultState(vault, proxy); + uint256 mintedOsTokenAssets = _osTokenVaultController.convertToAssets(mintedOsTokenShares); + uint256 vaultForceExitLtvPercent = abi.decode(vaultForceExitLtvPercentConfig, (uint256)); + // check whether approaching vault liquidation + if (Math.mulDiv(stakedAssets, vaultForceExitLtvPercent, _wad) <= mintedOsTokenAssets) { + return true; + } + } + + // check whether force exit borrow LTV is set in the strategy config + bytes memory borrowForceExitLtvPercentConfig = + _strategiesRegistry.getStrategyConfig(_strategyId, _borrowForceExitLtvPercentConfigName); + if (borrowForceExitLtvPercentConfig.length != 0) { + (uint256 borrowedAssets, uint256 suppliedOsTokenShares) = getBorrowState(proxy); + uint256 suppliedOsTokenAssets = _osTokenVaultController.convertToAssets(suppliedOsTokenShares); + uint256 borrowForceExitLtvPercent = abi.decode(borrowForceExitLtvPercentConfig, (uint256)); + // check whether approaching borrow liquidation + if (Math.mulDiv(suppliedOsTokenAssets, borrowForceExitLtvPercent, _wad) <= borrowedAssets) { + return true; + } + } + return false; + } + + /// @inheritdoc ILeverageStrategy + function deposit(address vault, uint256 osTokenShares, address referrer) external { if (osTokenShares == 0) revert Errors.InvalidShares(); // fetch strategy proxy @@ -135,8 +216,8 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { ); // fetch vault state and lending protocol state - (uint256 stakedAssets, uint256 mintedOsTokenShares) = _getVaultState(vault, proxy); - (uint256 borrowedAssets, uint256 suppliedOsTokenShares) = _getBorrowState(proxy); + (uint256 stakedAssets, uint256 mintedOsTokenShares) = getVaultState(vault, proxy); + (uint256 borrowedAssets, uint256 suppliedOsTokenShares) = getBorrowState(proxy); // check whether any of the positions exist uint256 leverageOsTokenShares = osTokenShares; @@ -147,10 +228,10 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { // borrow max amount of assets from the lending protocol uint256 maxBorrowAssets = - Math.mulDiv(_osTokenVaultController.convertToAssets(suppliedOsTokenShares), _getBorrowLtv(), _wad); + Math.mulDiv(_osTokenVaultController.convertToAssets(suppliedOsTokenShares), getBorrowLtv(), _wad); if (borrowedAssets >= maxBorrowAssets) { // nothing to borrow - emit Deposited(vault, msg.sender, osTokenShares, 0); + emit Deposited(vault, msg.sender, osTokenShares, 0, referrer); return; } uint256 assetsToBorrow; @@ -165,10 +246,10 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { } // calculate flash loaned osToken shares - uint256 flashloanOsTokenShares = _getFlashloanOsTokenShares(vault, leverageOsTokenShares); + uint256 flashloanOsTokenShares = getFlashloanOsTokenShares(vault, leverageOsTokenShares); if (flashloanOsTokenShares == 0) { // no osToken shares to leverage - emit Deposited(vault, msg.sender, osTokenShares, 0); + emit Deposited(vault, msg.sender, osTokenShares, 0, referrer); return; } @@ -176,7 +257,7 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { _osTokenFlashLoans.flashLoan(flashloanOsTokenShares, abi.encode(FlashloanAction.Deposit, vault, proxy)); // emit event - emit Deposited(vault, msg.sender, osTokenShares, flashloanOsTokenShares); + emit Deposited(vault, msg.sender, osTokenShares, flashloanOsTokenShares, referrer); } /// @inheritdoc ILeverageStrategy @@ -186,7 +267,7 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { /// @inheritdoc ILeverageStrategy function forceEnterExitQueue(address vault, address user) external returns (uint256 positionTicket) { - if (!_canForceEnterExitQueue(vault, user)) revert Errors.AccessDenied(); + if (!canForceEnterExitQueue(vault, user)) revert Errors.AccessDenied(); return _enterExitQueue(vault, user, _wad); } @@ -276,7 +357,7 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { // fetch borrowed assets address proxy = getStrategyProxy(vault, msg.sender); - (uint256 borrowedAssets,) = _getBorrowState(proxy); + (uint256 borrowedAssets,) = getBorrowState(proxy); if (assets == 0 || assets > borrowedAssets) revert Errors.InvalidAssets(); // calculate osToken shares to flashloan @@ -373,7 +454,7 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { if (isStrategyProxyExiting[proxy]) revert Errors.ExitRequestNotProcessed(); // calculate the minted OsToken shares to transfer to the escrow - (, uint256 mintedOsTokenShares) = _getVaultState(vault, proxy); + (, uint256 mintedOsTokenShares) = getVaultState(vault, proxy); uint256 osTokenShares = Math.mulDiv(mintedOsTokenShares, positionPercent, _wad); if (osTokenShares == 0) revert Errors.InvalidPosition(); @@ -390,22 +471,6 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { emit ExitQueueEntered(vault, user, positionTicket, block.timestamp, osTokenShares, positionPercent); } - /** - * @dev Calculates the amount of osToken shares to flashloan - * @param vault The address of the vault - * @param osTokenShares The amount of osToken shares at hand - * @return The amount of osToken shares to flashloan - */ - function _getFlashloanOsTokenShares(address vault, uint256 osTokenShares) private view returns (uint256) { - // fetch deposit and borrow LTVs - uint256 vaultLtv = _getVaultLtv(vault); - uint256 borrowLtv = _getBorrowLtv(); - - // calculate the amount of osToken shares that can be leveraged - uint256 totalLtv = Math.mulDiv(vaultLtv, borrowLtv, _wad); - return Math.mulDiv(osTokenShares, _wad, _wad - totalLtv) - osTokenShares; - } - /** * @dev Processes the deposit flashloan * @param vault The address of the vault @@ -421,7 +486,7 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { // calculate assets to borrow uint256 borrowAssets = - Math.mulDiv(_osTokenVaultController.convertToAssets(flashloanOsTokenShares), _wad, _getVaultLtv(vault)); + Math.mulDiv(_osTokenVaultController.convertToAssets(flashloanOsTokenShares), _wad, getVaultLtv(vault)); borrowAssets += 2; // add 2 wei to avoid rounding errors // borrow assets from the lending protocol @@ -457,7 +522,7 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { uint256 claimedAssets = _claimOsTokenVaultEscrowAssets(vault, proxy, exitPositionTicket, flashloanOsTokenShares); // repay borrowed assets - (uint256 borrowedAssets, uint256 suppliedOsTokenShares) = _getBorrowState(proxy); + (uint256 borrowedAssets, uint256 suppliedOsTokenShares) = getBorrowState(proxy); uint256 repayAssets = Math.min(borrowedAssets, claimedAssets); _repayAssets(proxy, repayAssets); @@ -469,7 +534,7 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { // deduct reserved osToken shares from the supplied osToken shares if (borrowedAssets != 0) { suppliedOsTokenShares -= - _osTokenVaultController.convertToShares(Math.mulDiv(borrowedAssets, _wad, _getBorrowLtv())); + _osTokenVaultController.convertToShares(Math.mulDiv(borrowedAssets, _wad, getBorrowLtv())); } // withdraw osToken shares @@ -580,10 +645,10 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { _repayAssets(proxy, repayAssets); // calculate osToken shares to withdraw - (uint256 borrowedAssets, uint256 suppliedOsTokenShares) = _getBorrowState(proxy); + (uint256 borrowedAssets, uint256 suppliedOsTokenShares) = getBorrowState(proxy); if (borrowedAssets != 0) { suppliedOsTokenShares -= - _osTokenVaultController.convertToShares(Math.mulDiv(borrowedAssets, _wad, _getBorrowLtv())); + _osTokenVaultController.convertToShares(Math.mulDiv(borrowedAssets, _wad, getBorrowLtv())); } // withdraw osToken shares @@ -596,50 +661,6 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { ); } - /** - * @dev Returns the vault LTV. - * @param vault The address of the vault - * @return The vault LTV - */ - function _getVaultLtv( - address vault - ) internal view returns (uint256) { - uint256 vaultLtvPercent = _osTokenConfig.getConfig(vault).ltvPercent; - // check whether there is max vault LTV percent set in the strategy config - bytes memory vaultMaxLtvPercentConfig = - _strategiesRegistry.getStrategyConfig(strategyId(), _maxVaultLtvPercentConfigName); - if (vaultMaxLtvPercentConfig.length == 0) { - return vaultLtvPercent; - } - return Math.min(vaultLtvPercent, abi.decode(vaultMaxLtvPercentConfig, (uint256))); - } - - /** - * @dev Returns the vault state - * @param vault The address of the vault - * @param proxy The address of the strategy proxy - * @return stakedAssets The amount of staked assets - * @return mintedOsTokenShares The amount of minted osToken shares - */ - function _getVaultState( - address vault, - address proxy - ) internal view returns (uint256 stakedAssets, uint256 mintedOsTokenShares) { - // check harvested - if (IVaultState(vault).isStateUpdateRequired()) { - revert Errors.NotHarvested(); - } - - // fetch staked assets - uint256 stakedShares = IVaultState(vault).getShares(proxy); - if (stakedShares != 0) { - stakedAssets = IVaultState(vault).convertToAssets(stakedShares); - } - - // fetch minted osToken shares - mintedOsTokenShares = IVaultOsToken(vault).osTokenPositions(proxy); - } - /** * @dev Returns the strategy proxy or creates a new one * @param vault The address of the vault @@ -719,50 +740,17 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { } } - /** - * @dev Checks whether the user can be forced to the exit queue - * @param vault The address of the vault - * @param user The address of the user - * @return True if the user can be forced to the exit queue, otherwise false - */ - function _canForceEnterExitQueue(address vault, address user) private view returns (bool) { - address proxy = getStrategyProxy(vault, user); - bytes32 _strategyId = strategyId(); - - // check whether force exit vault LTV is set in the strategy config - bytes memory vaultForceExitLtvPercentConfig = - _strategiesRegistry.getStrategyConfig(_strategyId, _vaultForceExitLtvPercentConfigName); - if ( - vaultForceExitLtvPercentConfig.length != 0 - && _osTokenConfig.getConfig(vault).liqThresholdPercent != _vaultDisabledLiqThreshold - ) { - (uint256 stakedAssets, uint256 mintedOsTokenShares) = _getVaultState(vault, proxy); - uint256 mintedOsTokenAssets = _osTokenVaultController.convertToAssets(mintedOsTokenShares); - uint256 vaultForceExitLtvPercent = abi.decode(vaultForceExitLtvPercentConfig, (uint256)); - // check whether approaching vault liquidation - if (Math.mulDiv(stakedAssets, vaultForceExitLtvPercent, _wad) <= mintedOsTokenAssets) { - return true; - } - } - - // check whether force exit borrow LTV is set in the strategy config - bytes memory borrowForceExitLtvPercentConfig = - _strategiesRegistry.getStrategyConfig(_strategyId, _borrowForceExitLtvPercentConfigName); - if (borrowForceExitLtvPercentConfig.length != 0) { - (uint256 borrowedAssets, uint256 suppliedOsTokenShares) = _getBorrowState(proxy); - uint256 suppliedOsTokenAssets = _osTokenVaultController.convertToAssets(suppliedOsTokenShares); - uint256 borrowForceExitLtvPercent = abi.decode(borrowForceExitLtvPercentConfig, (uint256)); - // check whether approaching borrow liquidation - if (Math.mulDiv(suppliedOsTokenAssets, borrowForceExitLtvPercent, _wad) <= borrowedAssets) { - return true; - } - } - return false; - } - /// @inheritdoc IStrategy function strategyId() public pure virtual returns (bytes32); + /// @inheritdoc ILeverageStrategy + function getBorrowLtv() public view virtual returns (uint256); + + /// @inheritdoc ILeverageStrategy + function getBorrowState( + address proxy + ) public view virtual returns (uint256 borrowedAssets, uint256 suppliedOsTokenShares); + /** * @dev Deposits assets to the vault and mints osToken shares * @param vault The address of the vault @@ -778,22 +766,6 @@ abstract contract LeverageStrategy is Multicall, ILeverageStrategy { uint256 mintOsTokenShares ) internal virtual returns (uint256); - /** - * @dev Returns the borrow LTV. - * @return The borrow LTV - */ - function _getBorrowLtv() internal view virtual returns (uint256); - - /** - * @dev Returns the borrow position state - * @param proxy The address of the strategy proxy - * @return borrowedAssets The amount of borrowed assets - * @return suppliedOsTokenShares The amount of supplied osToken shares - */ - function _getBorrowState( - address proxy - ) internal view virtual returns (uint256 borrowedAssets, uint256 suppliedOsTokenShares); - /** * @dev Locks OsToken shares to the lending protocol * @param proxy The address of the strategy proxy diff --git a/src/leverage/interfaces/ILeverageStrategy.sol b/src/leverage/interfaces/ILeverageStrategy.sol index 7c58536..b8fd791 100644 --- a/src/leverage/interfaces/ILeverageStrategy.sol +++ b/src/leverage/interfaces/ILeverageStrategy.sol @@ -62,8 +62,15 @@ interface ILeverageStrategy is IOsTokenFlashLoanRecipient, IStrategy { * @param user The address of the user * @param osTokenShares Amount of osToken shares to deposit * @param leverageOsTokenShares Amount of osToken shares leveraged + * @param referrer The address of the referrer */ - event Deposited(address indexed vault, address indexed user, uint256 osTokenShares, uint256 leverageOsTokenShares); + event Deposited( + address indexed vault, + address indexed user, + uint256 osTokenShares, + uint256 leverageOsTokenShares, + address referrer + ); /** * @notice Enter the OsToken escrow exit queue @@ -124,6 +131,51 @@ interface ILeverageStrategy is IOsTokenFlashLoanRecipient, IStrategy { */ function getStrategyProxy(address vault, address user) external view returns (address proxy); + /** + * @notice Returns the vault LTV. + * @param vault The address of the vault + * @return The vault LTV + */ + function getVaultLtv( + address vault + ) external view returns (uint256); + + /** + * @notice Returns the borrow LTV. + * @return The borrow LTV + */ + function getBorrowLtv() external view returns (uint256); + + /** + * @notice Returns the borrow position state for the proxy + * @param proxy The address of the strategy proxy + * @return borrowedAssets The amount of borrowed assets + * @return suppliedOsTokenShares The amount of supplied osToken shares + */ + function getBorrowState( + address proxy + ) external view returns (uint256 borrowedAssets, uint256 suppliedOsTokenShares); + + /** + * @notice Returns the vault position state for the proxy + * @param vault The address of the vault + * @param proxy The address of the strategy proxy + * @return stakedAssets The amount of staked assets + * @return mintedOsTokenShares The amount of minted osToken shares + */ + function getVaultState( + address vault, + address proxy + ) external view returns (uint256 stakedAssets, uint256 mintedOsTokenShares); + + /** + * @dev Checks whether the user can be forced to the exit queue + * @param vault The address of the vault + * @param user The address of the user + * @return True if the user can be forced to the exit queue, otherwise false + */ + function canForceEnterExitQueue(address vault, address user) external view returns (bool); + /** * @notice Checks if the proxy is exiting * @param proxy The address of the proxy @@ -133,6 +185,14 @@ interface ILeverageStrategy is IOsTokenFlashLoanRecipient, IStrategy { address proxy ) external view returns (bool isExiting); + /** + * @notice Calculates the amount of osToken shares to flashloan + * @param vault The address of the vault + * @param osTokenShares The amount of osToken shares at hand + * @return The amount of osToken shares to flashloan + */ + function getFlashloanOsTokenShares(address vault, uint256 osTokenShares) external view returns (uint256); + /** * @notice Updates the vault state * @param vault The address of the vault @@ -155,8 +215,9 @@ interface ILeverageStrategy is IOsTokenFlashLoanRecipient, IStrategy { * @notice Deposit assets to the strategy * @param vault The address of the vault * @param osTokenShares Amount of osToken shares to deposit + * @param referrer The address of the referrer */ - function deposit(address vault, uint256 osTokenShares) external; + function deposit(address vault, uint256 osTokenShares, address referrer) external; /** * @notice Enter the OsToken escrow exit queue. Can only be called by the position owner. diff --git a/test/MerkleDistributor.t.sol b/test/MerkleDistributor.t.sol index 6230a2d..57efc84 100644 --- a/test/MerkleDistributor.t.sol +++ b/test/MerkleDistributor.t.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: AGPL-3.0-only + pragma solidity ^0.8.26; import {Test} from 'forge-std/Test.sol'; diff --git a/test/StakeHelpers.t.sol b/test/StakeHelpers.t.sol index 54bcce1..fbb1895 100644 --- a/test/StakeHelpers.t.sol +++ b/test/StakeHelpers.t.sol @@ -1,14 +1,12 @@ -// SPDX-License-Identifier: BUSL-1.1 +// SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.22; import {IKeeperRewards} from '@stakewise-core/interfaces/IKeeperRewards.sol'; import {IOsTokenVaultController} from '@stakewise-core/interfaces/IOsTokenVaultController.sol'; import {IEthVault} from '@stakewise-core/interfaces/IEthVault.sol'; -import {Test, console} from 'forge-std/Test.sol'; -import {Vm} from 'forge-std/Vm.sol'; +import {Test} from 'forge-std/Test.sol'; import {StakeHelpers} from '../src/StakeHelpers.sol'; -import {ECDSA} from '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; contract StakeHelpersTest is Test { uint256 constant forkBlockNumber = 20_928_188; diff --git a/test/leverage/EthAaveLeverageStrategy.t.sol b/test/leverage/EthAaveLeverageStrategy.t.sol index 523a449..fb83716 100644 --- a/test/leverage/EthAaveLeverageStrategy.t.sol +++ b/test/leverage/EthAaveLeverageStrategy.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: BUSL-1.1 +// SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.26; @@ -352,7 +352,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { // deposit to strategy vm.prank(signer); - strategy.deposit(vault, osTokenShares1); + strategy.deposit(vault, osTokenShares1, address(0)); } function test_receiveFlashLoan_InvalidCaller() public { @@ -365,22 +365,22 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { vm.expectRevert( abi.encodeWithSelector(IERC20Errors.ERC20InsufficientAllowance.selector, strategyProxy, 0, osTokenShares) ); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); } function test_deposit_ZeroShares() public { vm.expectRevert(Errors.InvalidShares.selector); - strategy.deposit(vault, 0); + strategy.deposit(vault, 0, address(0)); } function test_deposit_ExitingProxy() public { address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares / 2); + strategy.deposit(vault, osTokenShares / 2, address(0)); strategy.enterExitQueue(vault, 1 ether); vm.expectRevert(Errors.ExitRequestNotProcessed.selector); - strategy.deposit(vault, osTokenShares / 2); + strategy.deposit(vault, osTokenShares / 2, address(0)); } function test_deposit_NoPosition() public { @@ -388,10 +388,10 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { IERC20(osToken).approve(strategyProxy, osTokenShares); vm.expectEmit(true, true, false, false); - emit ILeverageStrategy.Deposited(vault, address(this), osTokenShares, 0); + emit ILeverageStrategy.Deposited(vault, address(this), osTokenShares, 0, address(0)); snapStart('EthAaveLeverageStrategyTest_test_deposit_NoPosition'); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); snapEnd(); State memory state = _getState(); @@ -408,7 +408,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { function test_deposit_HasPosition() public { address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); State memory state1 = _getState(); int256 reward = SafeCast.toInt256(IEthVault(vault).totalAssets() * 0.03 ether / 1 ether / 12); @@ -438,9 +438,9 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { IERC20(osToken).approve(strategyProxy, type(uint256).max); vm.expectEmit(true, true, false, false); - emit ILeverageStrategy.Deposited(vault, address(this), newOsTokenShares, 0); + emit ILeverageStrategy.Deposited(vault, address(this), newOsTokenShares, 0, address(0)); snapStart('EthAaveLeverageStrategyTest_test_deposit_HasPosition'); - strategy.deposit(vault, newOsTokenShares); + strategy.deposit(vault, newOsTokenShares, address(0)); snapEnd(); State memory state3 = _getState(); @@ -459,7 +459,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { function test_enterExitQueue_InvalidPositionPercent() public { address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); vm.expectRevert(ILeverageStrategy.InvalidExitQueuePercent.selector); strategy.enterExitQueue(vault, 0); @@ -471,7 +471,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { function test_enterExitQueue_ExitingProxy() public { address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); strategy.enterExitQueue(vault, 0.5 ether); vm.expectRevert(Errors.ExitRequestNotProcessed.selector); @@ -486,7 +486,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { function test_enterExitQueue() public { address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); vm.warp(vm.getBlockTimestamp() + 30 days); uint256 avgRewardPerSecond = IOsTokenVaultController(osTokenVaultController).avgRewardPerSecond(); @@ -504,7 +504,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { function test_forceEnterExitQueue_NoForceExitConfig() public { address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); vm.prank(address(1)); vm.expectRevert(Errors.AccessDenied.selector); @@ -514,7 +514,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { function test_forceEnterExitQueue_ForceExitConfigChecksNotPassed() public { address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); StrategiesRegistry(strategiesRegistry).setStrategyConfig( strategy.strategyId(), 'vaultForceExitLtvPercent', abi.encode(0.918 ether) @@ -531,7 +531,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { function test_forceEnterExitQueue_vaultForceExitLtvPercent() public { address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); StrategiesRegistry(strategiesRegistry).setStrategyConfig( strategy.strategyId(), 'vaultForceExitLtvPercent', abi.encode(0.899 ether) @@ -548,7 +548,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { function test_forceEnterExitQueue_borrowForceExitLtvPercent() public { address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); StrategiesRegistry(strategiesRegistry).setStrategyConfig( strategy.strategyId(), 'borrowForceExitLtvPercent', abi.encode(0.929 ether) @@ -573,7 +573,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { // deposit address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); strategy.enterExitQueue(vault, 1 ether); vm.expectRevert(ILeverageStrategy.InvalidExitQueueTicket.selector); @@ -587,7 +587,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { // deposit address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); State memory state1 = _getState(); // earn some rewards @@ -707,7 +707,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { // deposit address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); strategy.enterExitQueue(vault, 1 ether); ILeverageStrategy.ExitPosition memory exitPosition = ILeverageStrategy.ExitPosition({positionTicket: 100, timestamp: 0, exitQueueIndex: 0}); @@ -719,7 +719,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { // deposit address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); uint256 positionTicket = strategy.enterExitQueue(vault, 1 ether); vm.expectRevert(Errors.ExitRequestNotProcessed.selector); @@ -735,7 +735,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { // deposit address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); uint256 positionTicket = strategy.enterExitQueue(vault, 1 ether); uint256 timestamp = vm.getBlockTimestamp(); @@ -760,7 +760,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { // deposit address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); uint256 positionTicket = strategy.enterExitQueue(vault, 1 ether); uint256 timestamp = vm.getBlockTimestamp(); @@ -835,7 +835,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { // deposit address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); State memory state = _getState(); vm.expectRevert(ILeverageStrategy.InvalidBalancerPoolId.selector); @@ -846,7 +846,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { // deposit address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); IStrategiesRegistry(strategiesRegistry).setStrategyConfig( strategy.strategyId(), 'balancerPoolId', abi.encode(balancerPoolId) @@ -884,7 +884,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { // deposit address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); strategy.enterExitQueue(vault, 1 ether); vm.expectRevert(Errors.ExitRequestNotProcessed.selector); @@ -899,7 +899,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { function test_upgradeProxy_NoVaultUpgradeConfig() public { address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); vm.expectRevert(Errors.UpgradeFailed.selector); strategy.upgradeProxy(vault); @@ -908,7 +908,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { function test_upgradeProxy_VaultUpgradeConfigZeroAddress() public { address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); IStrategiesRegistry(strategiesRegistry).setStrategyConfig( strategy.strategyId(), 'upgradeV1', abi.encode(address(0)) @@ -921,7 +921,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { function test_upgradeProxy_VaultUpgradeConfigSameAddress() public { address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); IStrategiesRegistry(strategiesRegistry).setStrategyConfig( strategy.strategyId(), 'upgradeV1', abi.encode(address(strategy)) @@ -934,7 +934,7 @@ contract EthAaveLeverageStrategyTest is Test, GasSnapshot { function test_upgradeProxy() public { address strategyProxy = strategy.getStrategyProxy(vault, address(this)); IERC20(osToken).approve(strategyProxy, osTokenShares); - strategy.deposit(vault, osTokenShares); + strategy.deposit(vault, osTokenShares, address(0)); address newStrategy = address(1); IStrategiesRegistry(strategiesRegistry).setStrategyConfig(