diff --git a/.vscode/settings.json b/.vscode/settings.json index 155ceb6..2fcf3e0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,12 @@ { - "python.terminal.activateEnvironment": true, - "python.defaultInterpreterPath": "/opt/anaconda3/envs/angle/bin/python", - "python.terminal.activateEnvInCurrentTerminal": false, - "python.terminal.executeInFileDir": false + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[typescript]": { + "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" + }, + "[solidity]": { + "editor.defaultFormatter": "JuanBlanco.solidity" + }, + "editor.codeActionsOnSave": { + "source.fixAll": true + } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..216fc85 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,20 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Generate Header", + "type": "shell", + "command": "headers ${input:header}", + "presentation": { + "reveal": "never" + } + } + ], + "inputs": [ + { + "id": "header", + "description": "Header", + "type": "promptString" + } + ] +} diff --git a/contracts/deprecated/GenericCompoundUpgradeableOld.sol b/contracts/deprecated/GenericCompoundUpgradeableOld.sol deleted file mode 100644 index 0f17dd0..0000000 --- a/contracts/deprecated/GenericCompoundUpgradeableOld.sol +++ /dev/null @@ -1,230 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity ^0.8.17; - -import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; - -import "../interfaces/external/compound/CErc20I.sol"; -import "../interfaces/external/compound/IComptroller.sol"; -import "../interfaces/external/compound/InterestRateModel.sol"; - -import "../strategies/OptimizerAPR/genericLender/GenericLenderBaseUpgradeable.sol"; - -/// @title GenericCompoundV2 -/// @author Forked from here: https://github.com/Grandthrax/yearnV2-generic-lender-strat/blob/master/contracts/GenericLender/GenericCompound.sol -contract GenericCompoundUpgradeableOld is GenericLenderBaseUpgradeable { - using SafeERC20 for IERC20; - using Address for address; - - uint256 public constant BLOCKS_PER_YEAR = 2_350_000; - - // solhint-disable-next-line - AggregatorV3Interface public constant oracle = AggregatorV3Interface(0xdbd020CAeF83eFd542f4De03e3cF0C28A4428bd5); - // solhint-disable-next-line - IComptroller public constant comptroller = IComptroller(0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B); - // solhint-disable-next-line - address public constant comp = 0xc00e94Cb662C3520282E6f5717214004A7f26888; - - // ======================== References to contracts ============================ - - CErc20I public cToken; - - // =============================== Errors ====================================== - - error FailedToMint(); - error FailedToRecoverETH(); - error FailedToRedeem(); - error InvalidOracleValue(); - error WrongCToken(); - - // ============================= Constructor =================================== - - /// @notice Initializer of the `GenericCompound` - /// @param _strategy Reference to the strategy using this lender - /// @param _cToken Address of the cToken - /// @param governorList List of addresses with governor privilege - /// @param keeperList List of addresses with keeper privilege - /// @param guardian Address of the guardian - function initialize( - address _strategy, - string memory _name, - address _cToken, - address[] memory governorList, - address[] memory keeperList, - address guardian - ) external { - _initialize(_strategy, _name, governorList, guardian, keeperList); - - cToken = CErc20I(_cToken); - if (CErc20I(_cToken).underlying() != address(want)) revert WrongCToken(); - - want.safeApprove(_cToken, type(uint256).max); - IERC20(comp).safeApprove(oneInch, type(uint256).max); - } - - // ===================== External Strategy Functions =========================== - - /// @notice Deposits the current balance of the contract to the lending platform - function deposit() external override onlyRole(STRATEGY_ROLE) { - uint256 balance = want.balanceOf(address(this)); - if (cToken.mint(balance) != 0) revert FailedToMint(); - } - - /// @notice Withdraws a given amount from lender - /// @param amount The amount the caller wants to withdraw - /// @return Amount actually withdrawn - function withdraw(uint256 amount) external override onlyRole(STRATEGY_ROLE) returns (uint256) { - return _withdraw(amount); - } - - /// @notice Withdraws as much as possible from the lending platform - /// @return Whether everything was withdrawn or not - function withdrawAll() external override onlyRole(STRATEGY_ROLE) returns (bool) { - uint256 invested = _nav(); - uint256 returned = _withdraw(invested); - return returned >= invested; - } - - // ========================== External View Functions ========================== - - /// @notice Helper function the current balance of cTokens - function underlyingBalanceStored() public view override returns (uint256 balance) { - uint256 currentCr = cToken.balanceOf(address(this)); - if (currentCr == 0) { - balance = 0; - } else { - //The current exchange rate as an unsigned integer, scaled by 1e18. - balance = (currentCr * cToken.exchangeRateStored()) / 1e18; - } - } - - /// @notice Returns an estimation of the current Annual Percentage Rate after a new deposit - /// of `amount` - /// @param amount Amount to add to the lending platform, and that we want to take into account - /// in the apr computation - function aprAfterDeposit(uint256 amount) external view override returns (uint256) { - uint256 cashPrior = want.balanceOf(address(cToken)); - - uint256 borrows = cToken.totalBorrows(); - - uint256 reserves = cToken.totalReserves(); - - uint256 reserverFactor = cToken.reserveFactorMantissa(); - - InterestRateModel model = cToken.interestRateModel(); - - // The supply rate is derived from the borrow rate, reserve factor and the amount of total borrows. - uint256 supplyRate = model.getSupplyRate(cashPrior + amount, borrows, reserves, reserverFactor); - // Adding the yield from comp - return supplyRate * BLOCKS_PER_YEAR + _incentivesRate(amount); - } - - // ================================= Governance ================================ - - /// @notice Withdraws as much as possible in case of emergency and sends it to the `PoolManager` - /// @param amount Amount to withdraw - /// @dev Does not check if any error occurs or if the amount withdrawn is correct - function emergencyWithdraw(uint256 amount) external override onlyRole(GUARDIAN_ROLE) { - // Do not care about errors here, what is important is to withdraw what is possible - cToken.redeemUnderlying(amount); - - want.safeTransfer(address(poolManager), want.balanceOf(address(this))); - } - - // ============================= Internal Functions ============================ - - /// @notice See `apr` - function _apr() internal view override returns (uint256) { - return cToken.supplyRatePerBlock() * BLOCKS_PER_YEAR + _incentivesRate(0); - } - - /// @notice See `withdraw` - function _withdraw(uint256 amount) internal returns (uint256) { - uint256 balanceUnderlying = cToken.balanceOfUnderlying(address(this)); - uint256 looseBalance = want.balanceOf(address(this)); - uint256 total = balanceUnderlying + looseBalance; - - if (amount > total) { - // Can't withdraw more than we own - amount = total; - } - - if (looseBalance >= amount) { - want.safeTransfer(address(strategy), amount); - return amount; - } - - // Not state changing but OK because of previous call - uint256 liquidity = want.balanceOf(address(cToken)); - - if (liquidity > 1) { - uint256 toWithdraw = amount - looseBalance; - - if (toWithdraw <= liquidity) { - // We can take all - if (cToken.redeemUnderlying(toWithdraw) != 0) revert FailedToRedeem(); - } else { - // Take all we can - if (cToken.redeemUnderlying(liquidity) != 0) revert FailedToRedeem(); - } - } - address[] memory holders = new address[](1); - CTokenI[] memory cTokens = new CTokenI[](1); - holders[0] = address(this); - cTokens[0] = cToken; - comptroller.claimComp(holders, cTokens, true, true); - - looseBalance = want.balanceOf(address(this)); - want.safeTransfer(address(strategy), looseBalance); - return looseBalance; - } - - /// @notice Calculates APR from Compound's Liquidity Mining Program - /// @param amountToAdd Amount to add to the `totalSupplyInWant` (for the `aprAfterDeposit` function) - function _incentivesRate(uint256 amountToAdd) internal view returns (uint256) { - uint256 supplySpeed = comptroller.compSupplySpeeds(address(cToken)); - uint256 totalSupplyInWant = (cToken.totalSupply() * cToken.exchangeRateStored()) / 1e18 + amountToAdd; - // `supplySpeed` is in `COMP` unit -> the following operation is going to put it in `want` unit - supplySpeed = _comptoWant(supplySpeed); - uint256 incentivesRate; - // Added for testing purposes and to handle the edge case where there is nothing left in a market - if (totalSupplyInWant == 0) { - incentivesRate = supplySpeed * BLOCKS_PER_YEAR; - } else { - // `incentivesRate` is expressed in base 18 like all APR - incentivesRate = (supplySpeed * BLOCKS_PER_YEAR * 1e18) / totalSupplyInWant; - } - return (incentivesRate * 9500) / 10000; // 95% of estimated APR to avoid overestimations - } - - /// @notice Estimates the value of `_amount` COMP tokens - /// @param _amount Amount of comp to compute the `want` price of - /// @dev This function uses a ChainLink oracle to easily compute the price - function _comptoWant(uint256 _amount) internal view returns (uint256) { - if (_amount == 0) { - return 0; - } - (uint80 roundId, int256 ratio, , , uint80 answeredInRound) = oracle.latestRoundData(); - if (ratio == 0 || roundId > answeredInRound) revert InvalidOracleValue(); - uint256 castedRatio = uint256(ratio); - - // Checking whether we should multiply or divide by the ratio computed - return (_amount * castedRatio * wantBase) / 1e26; - } - - /// @notice Specifies the token managed by this contract during normal operation - function _protectedTokens() internal view override returns (address[] memory) { - address[] memory protected = new address[](2); - protected[0] = address(want); - protected[1] = address(cToken); - return protected; - } - - /// @notice Recovers ETH from the contract - /// @param amount Amount to be recovered - function recoverETH(address to, uint256 amount) external onlyRole(GUARDIAN_ROLE) { - if (!payable(to).send(amount)) revert FailedToRecoverETH(); - } - - receive() external payable {} -} diff --git a/contracts/external/FullMath.sol b/contracts/external/FullMath.sol index 47cd14a..1da6927 100644 --- a/contracts/external/FullMath.sol +++ b/contracts/external/FullMath.sol @@ -7,6 +7,7 @@ pragma solidity >=0.4.0; /// @dev Handles "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 bits /// @dev This contract was forked from Uniswap V3's contract `FullMath.sol` available here /// https://github.com/Uniswap/uniswap-v3-core/blob/main/contracts/libraries/FullMath.sol +//solhint-disable abstract contract FullMath { /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 /// @param a The multiplicand diff --git a/contracts/interfaces/IGenericLender.sol b/contracts/interfaces/IGenericLender.sol index aff088a..5ea3f85 100644 --- a/contracts/interfaces/IGenericLender.sol +++ b/contracts/interfaces/IGenericLender.sol @@ -52,7 +52,7 @@ interface IGenericLender is IAccessControlAngle { /// of `amount` /// @param amount Amount to add to the lending platform, and that we want to take into account /// in the apr computation - function aprAfterDeposit(uint256 amount) external view returns (uint256); + function aprAfterDeposit(int256 amount) external view returns (uint256); /// @notice /// Removes tokens from this Strategy that are not the type of tokens @@ -71,4 +71,7 @@ interface IGenericLender is IAccessControlAngle { /// Implement `_protectedTokens()` to specify any additional tokens that /// should be protected from sweeping in addition to `want`. function sweep(address _token, address to) external; + + /// @notice Returns the current balance invested on the lender and related staking contracts + function underlyingBalanceStored() external view returns (uint256 balance); } diff --git a/contracts/interfaces/IStrategy.sol b/contracts/interfaces/IStrategy.sol index 1f65bf0..7fe5d93 100644 --- a/contracts/interfaces/IStrategy.sol +++ b/contracts/interfaces/IStrategy.sol @@ -4,6 +4,13 @@ pragma solidity ^0.8.17; import "./IAccessControlAngle.sol"; +struct LendStatus { + string name; + uint256 assets; + uint256 rate; + address add; +} + /// @title IStrategy /// @author Inspired by Yearn with slight changes from Angle Core Team /// @notice Interface for yield farming strategies diff --git a/contracts/interfaces/external/compound/CErc20I.sol b/contracts/interfaces/external/compound/CErc20I.sol index 8c8f8cf..c1a75cf 100755 --- a/contracts/interfaces/external/compound/CErc20I.sol +++ b/contracts/interfaces/external/compound/CErc20I.sol @@ -14,4 +14,6 @@ interface CErc20I is CTokenI { function borrow(uint256 borrowAmount) external returns (uint256); function decimals() external returns (uint8); + + function accrueInterest() external returns (uint256); } diff --git a/contracts/mock/MockLender.sol b/contracts/mock/MockLender.sol new file mode 100644 index 0000000..1cb4bdb --- /dev/null +++ b/contracts/mock/MockLender.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.17; + +import "./../strategies/OptimizerAPR/genericLender/GenericLenderBaseUpgradeable.sol"; + +/// @title GenericEuler +/// @author Angle Core Team +/// @notice Simple supplier to Euler markets +contract MockLender is GenericLenderBaseUpgradeable { + using SafeERC20 for IERC20; + using Address for address; + + uint256 private constant _BPS = 10**4; + + uint256 public r0; + uint256 public slope1; + uint256 public totalBorrow; + uint256 public biasSupply; + uint256 public propWithdrawable; + + // ================================ CONSTRUCTOR ================================ + + /// @notice Initializer of the `GenericEuler` + /// @param _strategy Reference to the strategy using this lender + /// @param governorList List of addresses with governor privilege + /// @param keeperList List of addresses with keeper privilege + /// @param guardian Address of the guardian + function initialize( + address _strategy, + string memory _name, + address[] memory governorList, + address guardian, + address[] memory keeperList, + address oneInch_, + uint256 _propWithdrawable + ) public { + _initialize(_strategy, _name, governorList, guardian, keeperList, oneInch_); + propWithdrawable = _propWithdrawable; + } + + function setPropWithdrawable(uint256 _propWithdrawable) external { + propWithdrawable = _propWithdrawable; + } + + // ======================== EXTERNAL STRATEGY FUNCTIONS ======================== + + /// @inheritdoc IGenericLender + function deposit() external view override onlyRole(STRATEGY_ROLE) { + want.balanceOf(address(this)); + } + + /// @inheritdoc IGenericLender + function withdraw(uint256 amount) external override onlyRole(STRATEGY_ROLE) returns (uint256) { + return _withdraw(amount); + } + + /// @inheritdoc IGenericLender + function withdrawAll() external override onlyRole(STRATEGY_ROLE) returns (bool) { + uint256 invested = _nav(); + uint256 returned = _withdraw(invested); + return returned >= invested; + } + + // ========================== EXTERNAL VIEW FUNCTIONS ========================== + + /// @inheritdoc GenericLenderBaseUpgradeable + function underlyingBalanceStored() public pure override returns (uint256) { + return 0; + } + + /// @inheritdoc IGenericLender + function aprAfterDeposit(int256 amount) external view override returns (uint256) { + return _aprAfterDeposit(amount); + } + + // ================================= GOVERNANCE ================================ + + /// @inheritdoc IGenericLender + function emergencyWithdraw(uint256 amount) external override onlyRole(GUARDIAN_ROLE) { + want.safeTransfer(address(poolManager), amount); + } + + // ============================= INTERNAL FUNCTIONS ============================ + + /// @inheritdoc GenericLenderBaseUpgradeable + function _apr() internal view override returns (uint256) { + return _aprAfterDeposit(0); + } + + /// @notice Internal version of the `aprAfterDeposit` function + function _aprAfterDeposit(int256 amount) internal view returns (uint256 supplyAPY) { + uint256 totalSupply = want.balanceOf(address(this)); + if (amount >= 0) totalSupply += uint256(amount); + else totalSupply -= uint256(-amount); + if (totalSupply > 0) supplyAPY = _computeAPYs(totalSupply); + } + + /// @notice Computes APYs based on the interest rate, reserve fee, borrow + /// @param totalSupply Interest rate paid per second by borrowers + /// @return supplyAPY The annual percentage yield received as a supplier with current settings + function _computeAPYs(uint256 totalSupply) internal view returns (uint256 supplyAPY) { + // All rates are in base 18 on Angle strategies + supplyAPY = r0 + (slope1 * totalBorrow) / (totalSupply + biasSupply); + } + + /// @notice See `withdraw` + function _withdraw(uint256 amount) internal returns (uint256) { + uint256 looseBalance = want.balanceOf(address(this)); + uint256 total = looseBalance; + + if (amount > total) { + // Can't withdraw more than we own + amount = total; + } + + // Limited in what we can withdraw + amount = (amount * propWithdrawable) / _BPS; + want.safeTransfer(address(strategy), amount); + return amount; + } + + /// @notice Internal version of the `setEulerPoolVariables` + function setLenderPoolVariables( + uint256 _r0, + uint256 _slope1, + uint256 _totalBorrow, + uint256 _biasSupply + ) external { + r0 = _r0; + slope1 = _slope1; + totalBorrow = _totalBorrow; + biasSupply = _biasSupply; + } + + // ============================= VIRTUAL FUNCTIONS ============================= + + /// @inheritdoc IGenericLender + function hasAssets() external view override returns (bool) { + return _nav() > 0; + } + + function _protectedTokens() internal pure override returns (address[] memory) { + return new address[](0); + } +} diff --git a/contracts/strategies/AaveFlashloanStrategy/AaveFlashloanStrategy.sol b/contracts/strategies/AaveFlashloanStrategy/AaveFlashloanStrategy.sol index 98a288b..96c5c77 100644 --- a/contracts/strategies/AaveFlashloanStrategy/AaveFlashloanStrategy.sol +++ b/contracts/strategies/AaveFlashloanStrategy/AaveFlashloanStrategy.sol @@ -119,15 +119,6 @@ contract AaveFlashloanStrategy is BaseStrategyUpgradeable, IERC3156FlashBorrower IAToken private _aToken; IVariableDebtToken private _debtToken; - // ================================== Errors =================================== - - error ErrorSwap(); - error InvalidSender(); - error InvalidSetOfParameters(); - error InvalidWithdrawCheck(); - error TooSmallAmountOut(); - error TooHighParameterValue(); - // ============================ Initializer ==================================== /// @notice Constructor of the `Strategy` diff --git a/contracts/strategies/BaseStrategyEvents.sol b/contracts/strategies/BaseStrategyEvents.sol index 8a781da..2937f3f 100644 --- a/contracts/strategies/BaseStrategyEvents.sol +++ b/contracts/strategies/BaseStrategyEvents.sol @@ -11,11 +11,12 @@ import "../external/AccessControlAngleUpgradeable.sol"; import "../interfaces/IStrategy.sol"; import "../interfaces/IPoolManager.sol"; +import "../utils/Errors.sol"; + /// @title BaseStrategyEvents /// @author Angle Core Team /// @notice Events used in the abstract `BaseStrategy` contract contract BaseStrategyEvents { - // So indexers can keep track of this event Harvested(uint256 profit, uint256 loss, uint256 debtPayment, uint256 debtOutstanding); event UpdatedMinReportDelayed(uint256 delay); diff --git a/contracts/strategies/BaseStrategyUpgradeable.sol b/contracts/strategies/BaseStrategyUpgradeable.sol index 957642b..1637ca1 100644 --- a/contracts/strategies/BaseStrategyUpgradeable.sol +++ b/contracts/strategies/BaseStrategyUpgradeable.sol @@ -48,16 +48,17 @@ abstract contract BaseStrategyUpgradeable is BaseStrategyEvents, AccessControlAn using SafeERC20 for IERC20; uint256 public constant BASE = 10**18; - uint256 public constant SECONDSPERYEAR = 31556952; + uint256 public constant SECONDS_PER_YEAR = 31556952; - /// @notice Role for `PoolManager` only - bytes32 public constant POOLMANAGER_ROLE = keccak256("POOLMANAGER_ROLE"); - /// @notice Role for guardians and governors - bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE"); - /// @notice Role for keepers - bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE"); + /// @notice Role for `PoolManager` only - keccak256("POOLMANAGER_ROLE") + bytes32 public constant POOLMANAGER_ROLE = 0x5916f72c85af4ac6f7e34636ecc97619c4b2085da099a5d28f3e58436cfbe562; + /// @notice Role for guardians and governors - keccak256("GUARDIAN_ROLE") + bytes32 public constant GUARDIAN_ROLE = 0x55435dd261a4b9b3364963f7738a7a662ad9c84396d64be3365284bb7f0a5041; + /// @notice Role for keepers - keccak256("KEEPER_ROLE") + bytes32 public constant KEEPER_ROLE = 0xfc8737ab85eb45125971625a9ebdb75cc78e01d5c1fa80c4c6e5203f47bc4fab; + + // ================================= REFERENCES ================================ - // ======================== References to contracts ============================ /// @notice See note on `setEmergencyExit()` bool public emergencyExit; @@ -70,7 +71,7 @@ abstract contract BaseStrategyUpgradeable is BaseStrategyEvents, AccessControlAn /// @notice Base of the ERC20 token farmed by this strategy uint256 public wantBase; - // ============================ Parameters ===================================== + // ================================= PARAMETERS ================================ /// @notice Use this to adjust the threshold at which running a debt causes a /// harvest trigger. See `setDebtThreshold()` for more details @@ -78,12 +79,7 @@ abstract contract BaseStrategyUpgradeable is BaseStrategyEvents, AccessControlAn uint256[46] private __gapBaseStrategy; - // ============================ Errors ========================================= - - error InvalidToken(); - error ZeroAddress(); - - // ============================ Constructor ==================================== + // ================================ CONSTRUCTOR ================================ /// @custom:oz-upgrades-unsafe-allow constructor constructor() initializer {} @@ -111,7 +107,8 @@ abstract contract BaseStrategyUpgradeable is BaseStrategyEvents, AccessControlAn _setRoleAdmin(GUARDIAN_ROLE, POOLMANAGER_ROLE); // Initializing roles first - for (uint256 i = 0; i < keepers.length; i++) { + uint256 keepersLength = keepers.length; + for (uint256 i; i < keepersLength; ++i) { if (keepers[i] == address(0)) revert ZeroAddress(); _setupRole(KEEPER_ROLE, keepers[i]); } @@ -123,7 +120,7 @@ abstract contract BaseStrategyUpgradeable is BaseStrategyEvents, AccessControlAn want.safeIncreaseAllowance(address(poolManager), type(uint256).max); } - // ========================== Core functions =================================== + // =============================== CORE FUNCTIONS ============================== /// @notice Harvests the Strategy, recognizing any profits or losses and adjusting /// the Strategy's position. @@ -133,13 +130,18 @@ abstract contract BaseStrategyUpgradeable is BaseStrategyEvents, AccessControlAn _adjustPosition(); } - /// @notice Harvests the Strategy, recognizing any profits or losses and adjusting - /// the Strategy's position. - /// @param borrowInit Approximate optimal borrows to have faster convergence on the NR method + /// @notice Same as the function above with a `data` parameter to help adjust the position + /// @dev Since this function is permissionless, strategy implementations should be made + /// to remain safe regardless of the data that is passed in the call + function harvest(bytes memory data) external { + _report(); + _adjustPosition(data); + } + + /// @notice Same as above with a `borrowInit` parameter to help in case of the convergence of the `adjustPosition` + /// method function harvest(uint256 borrowInit) external onlyRole(KEEPER_ROLE) { _report(); - // Check if free returns are left, and re-invest them, gives an hint on the borrow amount to the NR method - // to maximise revenue _adjustPosition(borrowInit); } @@ -160,7 +162,7 @@ abstract contract BaseStrategyUpgradeable is BaseStrategyEvents, AccessControlAn // NOTE: Reinvest anything leftover on next `tend`/`harvest` } - // ============================ View functions ================================= + // =============================== VIEW FUNCTIONS ============================== /// @notice Provides an accurate estimate for the total amount of assets /// (principle + return) that this Strategy is currently managing, @@ -183,10 +185,10 @@ abstract contract BaseStrategyUpgradeable is BaseStrategyEvents, AccessControlAn /// events can be tracked externally by indexing agents. /// @return True if the strategy is actively managing a position. function isActive() public view returns (bool) { - return estimatedTotalAssets() > 0; + return estimatedTotalAssets() != 0; } - // ============================ Internal Functions ============================= + // ============================= INTERNAL FUNCTIONS ============================ /// @notice PrepareReturn the Strategy, recognizing any profits or losses /// @dev In the rare case the Strategy is in emergency shutdown, this will exit @@ -201,10 +203,10 @@ abstract contract BaseStrategyUpgradeable is BaseStrategyEvents, AccessControlAn /// we may have to put an access control logic for this function to only allow white-listed addresses to act /// as keepers for the protocol function _report() internal { - uint256 profit = 0; - uint256 loss = 0; + uint256 profit; + uint256 loss; uint256 debtOutstanding = poolManager.debtOutstanding(); - uint256 debtPayment = 0; + uint256 debtPayment; if (emergencyExit) { // Free up as much capital as possible uint256 amountFreed = _liquidateAllPositions(); @@ -264,9 +266,14 @@ abstract contract BaseStrategyUpgradeable is BaseStrategyEvents, AccessControlAn /// could be 0, and you should handle that scenario accordingly. function _adjustPosition() internal virtual; - /// @notice same as _adjustPosition but with an initial parameters + /// @notice same as _adjustPosition but with an initial parameter function _adjustPosition(uint256) internal virtual; + /// @notice same as _adjustPosition but with permisionless parameters + function _adjustPosition(bytes memory) internal virtual { + _adjustPosition(); + } + /// @notice Liquidates up to `_amountNeeded` of `want` of this strategy's positions, /// irregardless of slippage. Any excess will be re-invested with `_adjustPosition()`. /// This function should return the amount of `want` tokens made available by the @@ -303,7 +310,7 @@ abstract contract BaseStrategyUpgradeable is BaseStrategyEvents, AccessControlAn /// ``` function _protectedTokens() internal view virtual returns (address[] memory); - // ============================== Governance =================================== + // ================================= GOVERNANCE ================================ /// @notice Activates emergency exit. Once activated, the Strategy will exit its /// position upon the next harvest, depositing all funds into the Manager as @@ -346,7 +353,8 @@ abstract contract BaseStrategyUpgradeable is BaseStrategyEvents, AccessControlAn if (_token == address(want)) revert InvalidToken(); address[] memory __protectedTokens = _protectedTokens(); - for (uint256 i = 0; i < __protectedTokens.length; i++) + uint256 protectedTokensLength = __protectedTokens.length; + for (uint256 i; i < protectedTokensLength; ++i) // In the strategy we use so far, the only protectedToken is the want token // and this has been checked above if (_token == __protectedTokens[i]) revert InvalidToken(); @@ -354,7 +362,7 @@ abstract contract BaseStrategyUpgradeable is BaseStrategyEvents, AccessControlAn IERC20(_token).safeTransfer(to, IERC20(_token).balanceOf(address(this))); } - // ============================ Manager functions ============================== + // ============================= MANAGER FUNCTIONS ============================= /// @notice Adds a new guardian address and echoes the change to the contracts /// that interact with this collateral `PoolManager` diff --git a/contracts/strategies/OptimizerAPR/OptimizerAPRGreedyStrategy.sol b/contracts/strategies/OptimizerAPR/OptimizerAPRGreedyStrategy.sol new file mode 100644 index 0000000..9d0282e --- /dev/null +++ b/contracts/strategies/OptimizerAPR/OptimizerAPRGreedyStrategy.sol @@ -0,0 +1,491 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.17; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + +import "../BaseStrategyUpgradeable.sol"; + +import "../../interfaces/IGenericLender.sol"; + +/// @title OptimizerAPRGreedyStrategy +/// @author Forked from https://github.com/Grandthrax/yearnV2-generic-lender-strat +/// @notice A lender optimisation strategy for any ERC20 asset +/// @dev This strategy works by taking plugins designed for standard lending platforms +/// It automatically chooses the best yield generating platform and adjusts accordingly +/// The adjustment is sub optimal so there is an additional option to manually set position +contract OptimizerAPRGreedyStrategy is BaseStrategyUpgradeable { + using SafeERC20 for IERC20; + using Address for address; + + // ================================= REFERENCES ================================ + + IGenericLender[] public lenders; + + // ================================= PARAMETERS ================================ + + uint256 public withdrawalThreshold; + + // =================================== EVENTS ================================== + + event AddLender(address indexed lender); + event RemoveLender(address indexed lender); + + // ================================ CONSTRUCTOR ================================ + + /// @notice Constructor of the `Strategy` + /// @param _poolManager Address of the `PoolManager` lending to this strategy + /// @param governor Address with governor privilege + /// @param guardian Address of the guardian + function initialize( + address _poolManager, + address governor, + address guardian, + address[] memory keepers + ) external { + _initialize(_poolManager, governor, guardian, keepers); + withdrawalThreshold = 1000 * wantBase; + } + + // ============================= INTERNAL MECHANICS ============================ + + /// @notice Frees up profit plus `_debtOutstanding`. + /// @param _debtOutstanding Amount to withdraw + /// @return _profit Profit freed by the call + /// @return _loss Loss discovered by the call + /// @return _debtPayment Amount freed to reimburse the debt + /// @dev If `_debtOutstanding` is more than we can free we get as much as possible. + function _prepareReturn(uint256 _debtOutstanding) + internal + override + returns ( + uint256 _profit, + uint256 _loss, + uint256 _debtPayment + ) + { + _profit = 0; //default assignments for clarity + _loss = 0; + _debtPayment = _debtOutstanding; + + uint256 lentAssets = lentTotalAssets(); + + uint256 looseAssets = want.balanceOf(address(this)); + + uint256 total = looseAssets + lentAssets; + + if (lentAssets == 0) { + // No position to harvest or profit to report + if (_debtPayment > looseAssets) { + // We can only return looseAssets + _debtPayment = looseAssets; + } + + return (_profit, _loss, _debtPayment); + } + + uint256 debt = poolManager.strategies(address(this)).totalStrategyDebt; + + if (total > debt) { + _profit = total - debt; + + uint256 amountToFree = _profit + _debtPayment; + // We need to add outstanding to our profit + // don't need to do logic if there is nothing to free + if (amountToFree != 0 && looseAssets < amountToFree) { + // Withdraw what we can withdraw + _withdrawSome(amountToFree - looseAssets); + uint256 newLoose = want.balanceOf(address(this)); + + // If we dont have enough money adjust _debtOutstanding and only change profit if needed + if (newLoose < amountToFree) { + if (_profit > newLoose) { + _profit = newLoose; + _debtPayment = 0; + } else { + _debtPayment = Math.min(newLoose - _profit, _debtPayment); + } + } + } + } else { + // Serious loss should never happen but if it does lets record it accurately + _loss = debt - total; + + uint256 amountToFree = _loss + _debtPayment; + if (amountToFree != 0 && looseAssets < amountToFree) { + // Withdraw what we can withdraw + + _withdrawSome(amountToFree - looseAssets); + uint256 newLoose = want.balanceOf(address(this)); + + // If we dont have enough money adjust `_debtOutstanding` and only change profit if needed + if (newLoose < amountToFree) { + if (_loss > newLoose) { + _loss = newLoose; + _debtPayment = 0; + } else { + _debtPayment = Math.min(newLoose - _loss, _debtPayment); + } + } + } + } + } + + /// @notice Estimates highest and lowest apr lenders among a `lendersList` + /// @param lendersList List of all the lender contracts associated to this strategy + /// @return _lowest The index of the lender in the `lendersList` with lowest apr + /// @return _lowestApr The lowest apr + /// @return _highest The index of the lender with highest apr + /// @return _potential The potential apr of this lender if funds are moved from lowest to highest + /// @dev `lendersList` is kept as a parameter to avoid multiplying reads in storage to the `lenders` + /// array + function _estimateAdjustPosition(IGenericLender[] memory lendersList) + internal + view + returns ( + uint256 _lowest, + uint256 _lowestApr, + uint256 _highest, + uint256 _potential + ) + { + // All loose assets are to be invested + uint256 looseAssets = want.balanceOf(address(this)); + + // Simple algo: + // - Get the lowest apr strat + // - Cycle through and see who could take its funds plus want for the highest apr + _lowestApr = type(uint256).max; + _lowest = 0; + uint256 lowestNav = 0; + uint256 highestApr = 0; + _highest = 0; + uint256 length = lendersList.length; + for (uint256 i = 0; i < length; i++) { + uint256 aprAfterDeposit = lendersList[i].aprAfterDeposit(int256(looseAssets)); + if (aprAfterDeposit > highestApr) { + highestApr = aprAfterDeposit; + _highest = i; + } + + if (lendersList[i].hasAssets()) { + uint256 apr = lendersList[i].apr(); + if (apr < _lowestApr) { + _lowestApr = apr; + _lowest = i; + lowestNav = lendersList[i].nav(); + } + } + } + + //if we can improve apr by withdrawing we do so + _potential = lendersList[_highest].aprAfterDeposit(int256(lowestNav + looseAssets)); + } + + /// @notice Function called by keepers to adjust the position + /// @dev The algorithm moves assets from lowest return to highest + /// like a very slow idiot bubble sort + function _adjustPosition() internal override { + // Emergency exit is dealt with at beginning of harvest + if (emergencyExit) { + return; + } + // Storing the `lenders` array in a cache variable + IGenericLender[] memory lendersList = lenders; + // We just keep all money in want if we dont have any lenders + if (lendersList.length == 0) { + return; + } + + (uint256 lowest, uint256 lowestApr, uint256 highest, uint256 potential) = _estimateAdjustPosition(lendersList); + + if (potential > lowestApr) { + // Apr should go down after deposit so won't be withdrawing from self + lendersList[lowest].withdrawAll(); + } + + uint256 bal = want.balanceOf(address(this)); + if (bal > 0) { + want.safeTransfer(address(lendersList[highest]), bal); + lendersList[highest].deposit(); + } + } + + /// @notice Function needed to inherit the baseStrategyUpgradeable + function _adjustPosition(uint256) internal override { + _adjustPosition(); + } + + /// @notice Withdraws a given amount from lenders + /// @param _amount The amount to withdraw + /// @dev Cycle through withdrawing from worst rate first + function _withdrawSome(uint256 _amount) internal returns (uint256 amountWithdrawn) { + IGenericLender[] memory lendersList = lenders; + if (lendersList.length == 0) { + return 0; + } + + // Don't withdraw dust + if (_amount < withdrawalThreshold) { + return 0; + } + + amountWithdrawn = 0; + // In most situations this will only run once. Only big withdrawals will be a gas guzzler + uint256 j = 0; + while (amountWithdrawn < _amount - withdrawalThreshold) { + uint256 lowestApr = type(uint256).max; + uint256 lowest = 0; + for (uint256 i = 0; i < lendersList.length; i++) { + if (lendersList[i].hasAssets()) { + uint256 apr = lendersList[i].apr(); + if (apr < lowestApr) { + lowestApr = apr; + lowest = i; + } + } + } + if (!lendersList[lowest].hasAssets()) { + return amountWithdrawn; + } + uint256 amountWithdrawnFromStrat = lendersList[lowest].withdraw(_amount - amountWithdrawn); + // To avoid staying on the same strat if we can't withdraw anythin from it + amountWithdrawn = amountWithdrawn + amountWithdrawnFromStrat; + j++; + // not best solution because it would be better to move to the 2nd lowestAPR instead of quiting + if (amountWithdrawnFromStrat == 0) { + return amountWithdrawn; + } + // To avoid want infinite loop + if (j >= 6) { + return amountWithdrawn; + } + } + } + + /// @notice Liquidates up to `_amountNeeded` of `want` of this strategy's positions, + /// irregardless of slippage. Any excess will be re-invested with `_adjustPosition()`. + /// This function should return the amount of `want` tokens made available by the + /// liquidation. If there is a difference between them, `_loss` indicates whether the + /// difference is due to a realized loss, or if there is some other sitution at play + /// (e.g. locked funds) where the amount made available is less than what is needed. + /// + /// NOTE: The invariant `_liquidatedAmount + _loss <= _amountNeeded` should always be maintained + function _liquidatePosition(uint256 _amountNeeded) internal override returns (uint256 _amountFreed, uint256 _loss) { + uint256 _balance = want.balanceOf(address(this)); + + if (_balance >= _amountNeeded) { + //if we don't set reserve here withdrawer will be sent our full balance + return (_amountNeeded, 0); + } else { + uint256 received = _withdrawSome(_amountNeeded - _balance) + (_balance); + if (received >= _amountNeeded) { + return (_amountNeeded, 0); + } else { + return (received, 0); + } + } + } + + /// @notice Liquidates everything and returns the amount that got freed. + /// This function is used during emergency exit instead of `_prepareReturn()` to + /// liquidate all of the Strategy's positions back to the Manager. + function _liquidateAllPositions() internal override returns (uint256 _amountFreed) { + (_amountFreed, ) = _liquidatePosition(estimatedTotalAssets()); + } + + // =============================== VIEW FUNCTIONS ============================== + + /// @notice View function to check the current state of the strategy + /// @return Returns the status of all lenders attached the strategy + function lendStatuses() external view returns (LendStatus[] memory) { + uint256 lendersListLength = lenders.length; + LendStatus[] memory statuses = new LendStatus[](lendersListLength); + for (uint256 i = 0; i < lendersListLength; i++) { + LendStatus memory s; + s.name = lenders[i].lenderName(); + s.add = address(lenders[i]); + s.assets = lenders[i].nav(); + s.rate = lenders[i].apr(); + statuses[i] = s; + } + return statuses; + } + + /// @notice View function to check the total assets lent + function lentTotalAssets() public view returns (uint256) { + uint256 nav = 0; + for (uint256 i = 0; i < lenders.length; i++) { + nav = nav + lenders[i].nav(); + } + return nav; + } + + /// @notice View function to check the total assets managed by the strategy + function estimatedTotalAssets() public view override returns (uint256 nav) { + nav = lentTotalAssets() + want.balanceOf(address(this)); + } + + /// @notice View function to check the number of lending platforms + function numLenders() external view returns (uint256) { + return lenders.length; + } + + /// @notice The weighted apr of all lenders. sum(nav * apr)/totalNav + function estimatedAPR() external view returns (uint256) { + uint256 bal = estimatedTotalAssets(); + if (bal == 0) { + return 0; + } + + uint256 weightedAPR = 0; + + for (uint256 i = 0; i < lenders.length; i++) { + weightedAPR = weightedAPR + lenders[i].weightedApr(); + } + + return weightedAPR / bal; + } + + /// @notice Prevents the governance from withdrawing want tokens + function _protectedTokens() internal view override returns (address[] memory) { + address[] memory protected = new address[](1); + protected[0] = address(want); + return protected; + } + + // ================================= GOVERNANCE ================================ + + struct LenderRatio { + address lender; + //share x 1000 + uint16 share; + } + + /// @notice Reallocates all funds according to a new distributions + /// @param _newPositions List of shares to specify the new allocation + /// @dev Share must add up to 1000. 500 means 50% etc + /// @dev This code has been forked, so we have not thoroughly tested it on our own + function manualAllocation(LenderRatio[] memory _newPositions) external onlyRole(GUARDIAN_ROLE) { + IGenericLender[] memory lendersList = lenders; + uint256 share = 0; + for (uint256 i = 0; i < lendersList.length; i++) { + lendersList[i].withdrawAll(); + } + + uint256 assets = want.balanceOf(address(this)); + + for (uint256 i = 0; i < _newPositions.length; i++) { + bool found = false; + + //might be annoying and expensive to do this second loop but worth it for safety + for (uint256 j = 0; j < lendersList.length; j++) { + if (address(lendersList[j]) == _newPositions[i].lender) { + found = true; + } + } + require(found, "94"); + + share = share + _newPositions[i].share; + uint256 toSend = (assets * _newPositions[i].share) / 1000; + want.safeTransfer(_newPositions[i].lender, toSend); + IGenericLender(_newPositions[i].lender).deposit(); + } + + require(share == 1000, "95"); + } + + /// @notice Changes the withdrawal threshold + /// @param _threshold The new withdrawal threshold + /// @dev governor, guardian or `PoolManager` only + function setWithdrawalThreshold(uint256 _threshold) external onlyRole(GUARDIAN_ROLE) { + withdrawalThreshold = _threshold; + } + + /// @notice Add lenders for the strategy to choose between + /// @param newLender The adapter to the added lending platform + /// @dev Governor, guardian or `PoolManager` only + function addLender(IGenericLender newLender) external onlyRole(GUARDIAN_ROLE) { + require(newLender.strategy() == address(this), "96"); + + for (uint256 i = 0; i < lenders.length; i++) { + require(address(newLender) != address(lenders[i]), "97"); + } + lenders.push(newLender); + + emit AddLender(address(newLender)); + } + + /// @notice Removes a lending platform and fails if total withdrawal is impossible + /// @param lender The address of the adapter to the lending platform to remove + function safeRemoveLender(address lender) external onlyRole(GUARDIAN_ROLE) { + _removeLender(lender, false); + } + + /// @notice Removes a lending platform and even if total withdrawal is impossible + /// @param lender The address of the adapter to the lending platform to remove + function forceRemoveLender(address lender) external onlyRole(GUARDIAN_ROLE) { + _removeLender(lender, true); + } + + /// @notice Internal function to handle lending platform removing + /// @param lender The address of the adapter for the lending platform to remove + /// @param force Whether it is required that all the funds are withdrawn prior to removal + function _removeLender(address lender, bool force) internal { + IGenericLender[] memory lendersList = lenders; + for (uint256 i = 0; i < lendersList.length; i++) { + if (lender == address(lendersList[i])) { + bool allWithdrawn = lendersList[i].withdrawAll(); + + if (!force) { + require(allWithdrawn, "98"); + } + + // Put the last index here + // then remove last index + if (i != lendersList.length - 1) { + lenders[i] = lendersList[lendersList.length - 1]; + } + + // Pop shortens array by 1 thereby deleting the last index + lenders.pop(); + + // If balance to spend we might as well put it into the best lender + if (want.balanceOf(address(this)) > 0) { + _adjustPosition(); + } + + emit RemoveLender(lender); + + return; + } + } + require(false, "94"); + } + + // ============================= MANAGER FUNCTIONS ============================= + + /// @notice Adds a new guardian address and echoes the change to the contracts + /// that interact with this collateral `PoolManager` + /// @param _guardian New guardian address + /// @dev This internal function has to be put in this file because `AccessControl` is not defined + /// in `PoolManagerInternal` + function addGuardian(address _guardian) external override onlyRole(POOLMANAGER_ROLE) { + // Granting the new role + // Access control for this contract + _grantRole(GUARDIAN_ROLE, _guardian); + // Propagating the new role in other contract + for (uint256 i = 0; i < lenders.length; i++) { + lenders[i].grantRole(GUARDIAN_ROLE, _guardian); + } + } + + /// @notice Revokes the guardian role and propagates the change to other contracts + /// @param guardian Old guardian address to revoke + function revokeGuardian(address guardian) external override onlyRole(POOLMANAGER_ROLE) { + _revokeRole(GUARDIAN_ROLE, guardian); + for (uint256 i = 0; i < lenders.length; i++) { + lenders[i].revokeRole(GUARDIAN_ROLE, guardian); + } + } +} diff --git a/contracts/strategies/OptimizerAPR/OptimizerAPRStrategy.sol b/contracts/strategies/OptimizerAPR/OptimizerAPRStrategy.sol index 1d6bfeb..c933b74 100644 --- a/contracts/strategies/OptimizerAPR/OptimizerAPRStrategy.sol +++ b/contracts/strategies/OptimizerAPR/OptimizerAPRStrategy.sol @@ -3,32 +3,40 @@ pragma solidity ^0.8.17; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + import "../BaseStrategyUpgradeable.sol"; + import "../../interfaces/IGenericLender.sol"; -/// @title Strategy -/// @author Forked from https://github.com/Grandthrax/yearnV2-generic-lender-strat -/// @notice A lender optimisation strategy for any ERC20 asset -/// @dev This strategy works by taking plugins designed for standard lending platforms -/// It automatically chooses the best yield generating platform and adjusts accordingly -/// The adjustment is sub optimal so there is an additional option to manually set position +/// @title OptimizerAPRStrategy +/// @author Angle Labs, Inc. +/// @notice A lender optimisation strategy for any ERC20 asset, leveraging multiple lenders at once +/// @dev This strategy works by taking plugins designed for standard lending platforms and automatically +/// chooses to invest its funds in the best platforms to generate yield. +/// The allocation is greedy and may be sub-optimal so there is an additional option to manually set positions contract OptimizerAPRStrategy is BaseStrategyUpgradeable { using SafeERC20 for IERC20; using Address for address; - // ======================== References to contracts ============================ + // ================================= CONSTANTS ================================= + + uint64 internal constant _BPS = 10000; + + // ============================ CONTRACTS REFERENCES =========================== IGenericLender[] public lenders; - // ======================== Parameters ========================================= + // ================================= PARAMETERS ================================ uint256 public withdrawalThreshold; - event AddLender(address indexed lender); + // =================================== ERRORS ================================== + error IncorrectDistribution(); - event RemoveLender(address indexed lender); + // =================================== EVENTS ================================== - // ============================== Constructor ================================== + event AddLender(address indexed lender); + event RemoveLender(address indexed lender); /// @notice Constructor of the `Strategy` /// @param _poolManager Address of the `PoolManager` lending to this strategy @@ -44,7 +52,7 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { withdrawalThreshold = 1000 * wantBase; } - // ========================== Internal Mechanics =============================== + // ============================= INTERNAL FUNCTIONS ============================ /// @notice Frees up profit plus `_debtOutstanding`. /// @param _debtOutstanding Amount to withdraw @@ -61,8 +69,6 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { uint256 _debtPayment ) { - _profit = 0; - _loss = 0; //for clarity _debtPayment = _debtOutstanding; uint256 lentAssets = lentTotalAssets(); @@ -89,7 +95,7 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { uint256 amountToFree = _profit + _debtPayment; // We need to add outstanding to our profit // don't need to do logic if there is nothing to free - if (amountToFree > 0 && looseAssets < amountToFree) { + if (amountToFree != 0 && looseAssets < amountToFree) { // Withdraw what we can withdraw _withdrawSome(amountToFree - looseAssets); uint256 newLoose = want.balanceOf(address(this)); @@ -109,7 +115,7 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { _loss = debt - total; uint256 amountToFree = _loss + _debtPayment; - if (amountToFree > 0 && looseAssets < amountToFree) { + if (amountToFree != 0 && looseAssets < amountToFree) { // Withdraw what we can withdraw _withdrawSome(amountToFree - looseAssets); @@ -131,87 +137,151 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { /// @notice Estimates highest and lowest apr lenders among a `lendersList` /// @param lendersList List of all the lender contracts associated to this strategy /// @return _lowest The index of the lender in the `lendersList` with lowest apr - /// @return _lowestApr The lowest apr /// @return _highest The index of the lender with highest apr - /// @return _potential The potential apr of this lender if funds are moved from lowest to highest - /// @dev `lendersList` is kept as a parameter to avoid multiplying reads in storage to the `lenders` - /// array - function _estimateAdjustPosition(IGenericLender[] memory lendersList) + /// @return _investmentStrategy Whether we should invest from the lowest to the highest yielding strategy or simply invest loose assets + /// @return _totalApr The APR computed according to (greedy) heuristics that will determine whether positions should be adjusted + /// according to the solution proposed by the caller or according to the greedy method + /// @dev `lendersList` is kept as a parameter to avoid multiplying reads in storage to the `lenders` array + function _estimateGreedyAdjustPosition(IGenericLender[] memory lendersList) internal view returns ( uint256 _lowest, - uint256 _lowestApr, uint256 _highest, - uint256 _potential + bool _investmentStrategy, + uint256 _totalApr ) { - //all loose assets are to be invested + // All loose assets are to be invested uint256 looseAssets = want.balanceOf(address(this)); - // our simple algo - // get the lowest apr strat - // cycle through and see who could take its funds plus want for the highest apr - _lowestApr = type(uint256).max; - _lowest = 0; - uint256 lowestNav = 0; - - uint256 highestApr = 0; - _highest = 0; - - for (uint256 i = 0; i < lendersList.length; i++) { - uint256 aprAfterDeposit = lendersList[i].aprAfterDeposit(looseAssets); - if (aprAfterDeposit > highestApr) { - highestApr = aprAfterDeposit; - _highest = i; + // Simple greedy algo: + // - Get the lowest apr strat + // - Cycle through and see who could take its funds to improve the overall highest APR + uint256 lowestNav; + uint256 highestApr; + uint256 highestLenderNav; + uint256 totalNav = looseAssets; + uint256[] memory weightedAprs = new uint256[](lendersList.length); + uint256 lendersListLength = lendersList.length; + { + uint256 lowestApr = type(uint256).max; + for (uint256 i; i < lendersListLength; ++i) { + uint256 aprAfterDeposit = lendersList[i].aprAfterDeposit(int256(looseAssets)); + uint256 nav = lendersList[i].nav(); + totalNav += nav; + if (aprAfterDeposit > highestApr) { + highestApr = aprAfterDeposit; + highestLenderNav = nav; + _highest = i; + } + // Checking strategies that have assets + if (nav > 10 * wantBase) { + uint256 apr = lendersList[i].apr(); + weightedAprs[i] = apr * nav; + if (apr < lowestApr) { + lowestApr = apr; + lowestNav = nav; + _lowest = i; + } + } } + } - if (lendersList[i].hasAssets()) { - uint256 apr = lendersList[i].apr(); - if (apr < _lowestApr) { - _lowestApr = apr; - _lowest = i; - lowestNav = lendersList[i].nav(); + // Comparing if we are better off removing from the lowest APR yielding strategy to invest in the highest or just invest + // the loose assets in the highest yielding strategy + if (totalNav > 0) { + // Case where only loose assets are invested + uint256 weightedApr1; + // Case where funds are divested from the strategy with the lowest APR to be invested in the one with the highest APR + uint256 weightedApr2; + for (uint256 i; i < lendersListLength; ++i) { + if (i == _highest) { + weightedApr1 += (highestLenderNav + looseAssets) * highestApr; + if (lowestNav != 0 && lendersListLength > 1) + weightedApr2 += + (highestLenderNav + looseAssets + lowestNav) * + lendersList[_highest].aprAfterDeposit(int256(lowestNav + looseAssets)); + } else if (i == _lowest) { + weightedApr1 += weightedAprs[i]; + // In the second case funds are divested so the lowest strat does not contribute to the highest APR case + } else { + weightedApr1 += weightedAprs[i]; + weightedApr2 += weightedAprs[i]; } } + if (weightedApr2 > weightedApr1 && lendersList.length > 1) { + _investmentStrategy = true; + _totalApr = weightedApr2 / totalNav; + } else _totalApr = weightedApr1 / totalNav; } - - //if we can improve apr by withdrawing we do so - _potential = lendersList[_highest].aprAfterDeposit(lowestNav + looseAssets); } - /// @notice Function called by keepers to adjust the position - /// @dev The algorithm moves assets from lowest return to highest - /// like a very slow idiot bubble sort - function _adjustPosition() internal override { + /// @inheritdoc BaseStrategyUpgradeable + function _adjustPosition(bytes memory data) internal override { // Emergency exit is dealt with at beginning of harvest - if (emergencyExit) { - return; - } + if (emergencyExit) return; + // Storing the `lenders` array in a cache variable IGenericLender[] memory lendersList = lenders; - // We just keep all money in want if we dont have any lenders - if (lendersList.length == 0) { - return; - } + uint256 lendersListLength = lendersList.length; + // We just keep all money in `want` if we dont have any lenders + if (lendersListLength == 0) return; + + uint64[] memory lenderSharesHint = abi.decode(data, (uint64[])); + + uint256 estimatedAprHint; + int256[] memory lenderAdjustedAmounts; + if (lenderSharesHint.length != 0) (estimatedAprHint, lenderAdjustedAmounts) = estimatedAPR(lenderSharesHint); + (uint256 lowest, uint256 highest, bool _investmentStrategy, uint256 _totalApr) = _estimateGreedyAdjustPosition( + lendersList + ); + + // The hint was successful --> we find a better allocation than the current one + if (_totalApr < estimatedAprHint) { + uint256 deltaWithdraw; + for (uint256 i; i < lendersListLength; ++i) { + if (lenderAdjustedAmounts[i] < 0) { + deltaWithdraw += + uint256(-lenderAdjustedAmounts[i]) - + lendersList[i].withdraw(uint256(-lenderAdjustedAmounts[i])); + } + } - (uint256 lowest, uint256 lowestApr, uint256 highest, uint256 potential) = _estimateAdjustPosition(lendersList); + // If the strategy didn't succeed to withdraw the intended funds -> revert and force the greedy path + if (deltaWithdraw > withdrawalThreshold) revert IncorrectDistribution(); + + for (uint256 i; i < lendersListLength; ++i) { + // As `deltaWithdraw` is inferior to `withdrawalThreshold` (a dust) + // It is not critical to compensate on an arbitrary lender as it will only slightly impact global APR + if (lenderAdjustedAmounts[i] > int256(deltaWithdraw)) { + lenderAdjustedAmounts[i] -= int256(deltaWithdraw); + deltaWithdraw = 0; + want.safeTransfer(address(lendersList[i]), uint256(lenderAdjustedAmounts[i])); + lendersList[i].deposit(); + } else if (lenderAdjustedAmounts[i] > 0) deltaWithdraw -= uint256(lenderAdjustedAmounts[i]); + } + } else { + if (_investmentStrategy) { + lendersList[lowest].withdrawAll(); + } - if (potential > lowestApr) { - // Apr should go down after deposit so won't be withdrawing from self - lendersList[lowest].withdrawAll(); + uint256 bal = want.balanceOf(address(this)); + if (bal != 0) { + want.safeTransfer(address(lendersList[highest]), bal); + lendersList[highest].deposit(); + } } + } - uint256 bal = want.balanceOf(address(this)); - if (bal > 0) { - want.safeTransfer(address(lendersList[highest]), bal); - lendersList[highest].deposit(); - } + /// @inheritdoc BaseStrategyUpgradeable + function _adjustPosition() internal override { + _adjustPosition(abi.encode(new uint64[](0))); } - /// @notice Function needed to inherit the baseStrategyUpgradeable + /// @inheritdoc BaseStrategyUpgradeable function _adjustPosition(uint256) internal override { - _adjustPosition(); + _adjustPosition(abi.encode(new uint64[](0))); } /// @notice Withdraws a given amount from lenders @@ -219,22 +289,24 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { /// @dev Cycle through withdrawing from worst rate first function _withdrawSome(uint256 _amount) internal returns (uint256 amountWithdrawn) { IGenericLender[] memory lendersList = lenders; - if (lendersList.length == 0) { + uint256 lendersListLength = lendersList.length; + if (lendersListLength == 0) { return 0; } // Don't withdraw dust - if (_amount < withdrawalThreshold) { + uint256 _withdrawalThreshold = withdrawalThreshold; + if (_amount < _withdrawalThreshold) { return 0; } - amountWithdrawn = 0; + amountWithdrawn; // In most situations this will only run once. Only big withdrawals will be a gas guzzler - uint256 j = 0; - while (amountWithdrawn < _amount - withdrawalThreshold) { + uint256 j; + while (amountWithdrawn < _amount - _withdrawalThreshold) { uint256 lowestApr = type(uint256).max; - uint256 lowest = 0; - for (uint256 i = 0; i < lendersList.length; i++) { + uint256 lowest; + for (uint256 i; i < lendersListLength; ++i) { if (lendersList[i].hasAssets()) { uint256 apr = lendersList[i].apr(); if (apr < lowestApr) { @@ -248,8 +320,8 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { } uint256 amountWithdrawnFromStrat = lendersList[lowest].withdraw(_amount - amountWithdrawn); // To avoid staying on the same strat if we can't withdraw anythin from it - amountWithdrawn = amountWithdrawn + amountWithdrawnFromStrat; - j++; + amountWithdrawn += amountWithdrawnFromStrat; + ++j; // not best solution because it would be better to move to the 2nd lowestAPR instead of quiting if (amountWithdrawnFromStrat == 0) { return amountWithdrawn; @@ -292,21 +364,14 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { (_amountFreed, ) = _liquidatePosition(estimatedTotalAssets()); } - // ========================== View Functions =================================== - - struct LendStatus { - string name; - uint256 assets; - uint256 rate; - address add; - } + // =============================== VIEW FUNCTIONS ============================== /// @notice View function to check the current state of the strategy /// @return Returns the status of all lenders attached the strategy function lendStatuses() external view returns (LendStatus[] memory) { - uint256 lendersListLength = lenders.length; - LendStatus[] memory statuses = new LendStatus[](lendersListLength); - for (uint256 i = 0; i < lendersListLength; i++) { + uint256 lendersLength = lenders.length; + LendStatus[] memory statuses = new LendStatus[](lendersLength); + for (uint256 i; i < lendersLength; ++i) { LendStatus memory s; s.name = lenders[i].lenderName(); s.add = address(lenders[i]); @@ -319,9 +384,10 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { /// @notice View function to check the total assets lent function lentTotalAssets() public view returns (uint256) { - uint256 nav = 0; - for (uint256 i = 0; i < lenders.length; i++) { - nav = nav + lenders[i].nav(); + uint256 nav; + uint256 lendersLength = lenders.length; + for (uint256 i; i < lendersLength; ++i) { + nav += lenders[i].nav(); } return nav; } @@ -336,72 +402,63 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { return lenders.length; } - /// @notice The weighted apr of all lenders. sum(nav * apr)/totalNav + /// @notice Returns the weighted apr of all lenders + /// @dev It's computed by doing: `sum(nav * apr) / totalNav` function estimatedAPR() external view returns (uint256) { uint256 bal = estimatedTotalAssets(); if (bal == 0) { return 0; } - uint256 weightedAPR = 0; - - for (uint256 i = 0; i < lenders.length; i++) { - weightedAPR = weightedAPR + lenders[i].weightedApr(); + uint256 weightedAPR; + uint256 lendersLength = lenders.length; + for (uint256 i; i < lendersLength; ++i) { + weightedAPR += lenders[i].weightedApr(); } return weightedAPR / bal; } - /// @notice Prevents the governance from withdrawing want tokens + /// @notice Returns the eighted apr in an hypothetical world where the strategy splits its nav + /// in respect to shares + /// @param shares List of shares (in bps of the nav) that should be allocated to each lender + function estimatedAPR(uint64[] memory shares) + public + view + returns (uint256 weightedAPR, int256[] memory lenderAdjustedAmounts) + { + uint256 lenderLength = lenders.length; + lenderAdjustedAmounts = new int256[](lenderLength); + if (lenderLength != shares.length) revert IncorrectListLength(); + + uint256 bal = estimatedTotalAssets(); + if (bal == 0) return (weightedAPR, lenderAdjustedAmounts); + + uint256 share; + for (uint256 i; i < lenderLength; ++i) { + share += shares[i]; + uint256 futureDeposit = (bal * shares[i]) / _BPS; + // It won't overflow for `decimals <= 18`, as it would mean gigantic amounts + int256 adjustedAmount = int256(futureDeposit) - int256(lenders[i].nav()); + lenderAdjustedAmounts[i] = adjustedAmount; + weightedAPR += futureDeposit * lenders[i].aprAfterDeposit(adjustedAmount); + } + if (share != 10000) revert InvalidShares(); + + weightedAPR /= bal; + } + + /// @notice Prevents governance from withdrawing `want` tokens function _protectedTokens() internal view override returns (address[] memory) { address[] memory protected = new address[](1); protected[0] = address(want); return protected; } - // ============================ Governance ===================================== - - struct LenderRatio { - address lender; - //share x 1000 - uint16 share; - } - - /// @notice Reallocates all funds according to a new distributions - /// @param _newPositions List of shares to specify the new allocation - /// @dev Share must add up to 1000. 500 means 50% etc - /// @dev This code has been forked, so we have not thoroughly tested it on our own - function manualAllocation(LenderRatio[] memory _newPositions) external onlyRole(GUARDIAN_ROLE) { - IGenericLender[] memory lendersList = lenders; - uint256 share = 0; - for (uint256 i = 0; i < lendersList.length; i++) { - lendersList[i].withdrawAll(); - } - - uint256 assets = want.balanceOf(address(this)); - - for (uint256 i = 0; i < _newPositions.length; i++) { - bool found = false; - - //might be annoying and expensive to do this second loop but worth it for safety - for (uint256 j = 0; j < lendersList.length; j++) { - if (address(lendersList[j]) == _newPositions[i].lender) { - found = true; - } - } - require(found, "94"); - - share = share + _newPositions[i].share; - uint256 toSend = (assets * _newPositions[i].share) / 1000; - want.safeTransfer(_newPositions[i].lender, toSend); - IGenericLender(_newPositions[i].lender).deposit(); - } - - require(share == 1000, "95"); - } + // ================================= GOVERNANCE ================================ /// @notice Changes the withdrawal threshold - /// @param _threshold The new withdrawal threshold + /// @param _threshold New withdrawal threshold /// @dev governor, guardian or `PoolManager` only function setWithdrawalThreshold(uint256 _threshold) external onlyRole(GUARDIAN_ROLE) { withdrawalThreshold = _threshold; @@ -411,10 +468,10 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { /// @param newLender The adapter to the added lending platform /// @dev Governor, guardian or `PoolManager` only function addLender(IGenericLender newLender) external onlyRole(GUARDIAN_ROLE) { - require(newLender.strategy() == address(this), "96"); - - for (uint256 i = 0; i < lenders.length; i++) { - require(address(newLender) != address(lenders[i]), "97"); + if (newLender.strategy() != address(this)) revert UndockedLender(); + uint256 lendersLength = lenders.length; + for (uint256 i; i < lendersLength; ++i) { + if (address(newLender) == address(lenders[i])) revert LenderAlreadyAdded(); } lenders.push(newLender); @@ -423,7 +480,7 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { /// @notice Removes a lending platform and fails if total withdrawal is impossible /// @param lender The address of the adapter to the lending platform to remove - function safeRemoveLender(address lender) external onlyRole(GUARDIAN_ROLE) { + function safeRemoveLender(address lender) external onlyRole(KEEPER_ROLE) { _removeLender(lender, false); } @@ -438,25 +495,24 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { /// @param force Whether it is required that all the funds are withdrawn prior to removal function _removeLender(address lender, bool force) internal { IGenericLender[] memory lendersList = lenders; - for (uint256 i = 0; i < lendersList.length; i++) { + uint256 lendersListLength = lendersList.length; + for (uint256 i; i < lendersListLength; ++i) { if (lender == address(lendersList[i])) { bool allWithdrawn = lendersList[i].withdrawAll(); - if (!force) { - require(allWithdrawn, "98"); - } + if (!force && !allWithdrawn) revert FailedWithdrawal(); // Put the last index here // then remove last index - if (i != lendersList.length - 1) { - lenders[i] = lendersList[lendersList.length - 1]; + if (i != lendersListLength - 1) { + lenders[i] = lendersList[lendersListLength - 1]; } // Pop shortens array by 1 thereby deleting the last index lenders.pop(); // If balance to spend we might as well put it into the best lender - if (want.balanceOf(address(this)) > 0) { + if (want.balanceOf(address(this)) != 0) { _adjustPosition(); } @@ -465,10 +521,10 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { return; } } - require(false, "94"); + revert NonExistentLender(); } - // ========================== Manager functions ================================ + // ============================= MANAGER FUNCTIONS ============================= /// @notice Adds a new guardian address and echoes the change to the contracts /// that interact with this collateral `PoolManager` @@ -479,8 +535,9 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { // Granting the new role // Access control for this contract _grantRole(GUARDIAN_ROLE, _guardian); - // Propagating the new role in other contract - for (uint256 i = 0; i < lenders.length; i++) { + // Propagating the new role to underyling lenders + uint256 lendersLength = lenders.length; + for (uint256 i; i < lendersLength; ++i) { lenders[i].grantRole(GUARDIAN_ROLE, _guardian); } } @@ -489,7 +546,8 @@ contract OptimizerAPRStrategy is BaseStrategyUpgradeable { /// @param guardian Old guardian address to revoke function revokeGuardian(address guardian) external override onlyRole(POOLMANAGER_ROLE) { _revokeRole(GUARDIAN_ROLE, guardian); - for (uint256 i = 0; i < lenders.length; i++) { + uint256 lendersLength = lenders.length; + for (uint256 i; i < lendersLength; ++i) { lenders[i].revokeRole(GUARDIAN_ROLE, guardian); } } diff --git a/contracts/strategies/OptimizerAPR/genericLender/GenericLenderBaseUpgradeable.sol b/contracts/strategies/OptimizerAPR/genericLender/GenericLenderBaseUpgradeable.sol index d0292e3..d9b5c29 100644 --- a/contracts/strategies/OptimizerAPR/genericLender/GenericLenderBaseUpgradeable.sol +++ b/contracts/strategies/OptimizerAPR/genericLender/GenericLenderBaseUpgradeable.sol @@ -13,22 +13,19 @@ import "../../../interfaces/IGenericLender.sol"; import "../../../interfaces/IPoolManager.sol"; import "../../../interfaces/IStrategy.sol"; +import "../../../utils/Errors.sol"; + /// @title GenericLenderBaseUpgradeable /// @author Forked from https://github.com/Grandthrax/yearnV2-generic-lender-strat/tree/master/contracts/GenericLender /// @notice A base contract to build contracts that lend assets to protocols abstract contract GenericLenderBaseUpgradeable is IGenericLender, AccessControlAngleUpgradeable { using SafeERC20 for IERC20; - bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE"); - bytes32 public constant STRATEGY_ROLE = keccak256("STRATEGY_ROLE"); - bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE"); - - // ======================= References to contracts ============================= + bytes32 public constant GUARDIAN_ROLE = 0x55435dd261a4b9b3364963f7738a7a662ad9c84396d64be3365284bb7f0a5041; + bytes32 public constant STRATEGY_ROLE = 0x928286c473ded01ff8bf61a1986f14a0579066072fa8261442d9fea514d93a4c; + bytes32 public constant KEEPER_ROLE = 0xfc8737ab85eb45125971625a9ebdb75cc78e01d5c1fa80c4c6e5203f47bc4fab; - // solhint-disable-next-line - address internal constant oneInch = 0x1111111254EEB25477B68fb85Ed929f73A960582; - - // ========================= References and Parameters ========================= + // ========================= REFERENCES AND PARAMETERS ========================= /// @inheritdoc IGenericLender string public override lenderName; @@ -40,17 +37,12 @@ abstract contract GenericLenderBaseUpgradeable is IGenericLender, AccessControlA IERC20 public want; /// @notice Base of the asset handled by the lender uint256 public wantBase; + /// @notice 1inch Aggregattion router + address internal _oneInch; - uint256[45] private __gapBaseLender; - - // ================================ Errors ===================================== + uint256[44] private __gapBaseLender; - error ErrorSwap(); - error IncompatibleLengths(); - error ProtectedToken(); - error TooSmallAmount(); - - // ================================ Initializer ================================ + // ================================ INITIALIZER ================================ /// @notice Initalizer of the `GenericLenderBase` /// @param _strategy Reference to the strategy using this lender @@ -63,8 +55,10 @@ abstract contract GenericLenderBaseUpgradeable is IGenericLender, AccessControlA string memory _name, address[] memory governorList, address guardian, - address[] memory keeperList + address[] memory keeperList, + address oneInch_ ) internal initializer { + _oneInch = oneInch_; strategy = _strategy; // The corresponding `PoolManager` is inferred from the `Strategy` poolManager = IPoolManager(IStrategy(strategy).poolManager()); @@ -72,12 +66,14 @@ abstract contract GenericLenderBaseUpgradeable is IGenericLender, AccessControlA lenderName = _name; _setupRole(GUARDIAN_ROLE, address(poolManager)); - for (uint256 i = 0; i < governorList.length; i++) { + uint256 governorListLength = governorList.length; + for (uint256 i; i < governorListLength; ++i) { _setupRole(GUARDIAN_ROLE, governorList[i]); } _setupRole(KEEPER_ROLE, guardian); - for (uint256 i = 0; i < keeperList.length; i++) { + uint256 keeperListLength = keeperList.length; + for (uint256 i; i < keeperListLength; ++i) { _setupRole(KEEPER_ROLE, keeperList[i]); } @@ -94,7 +90,7 @@ abstract contract GenericLenderBaseUpgradeable is IGenericLender, AccessControlA /// @custom:oz-upgrades-unsafe-allow constructor constructor() initializer {} - // ============================ View Functions ================================= + // =============================== VIEW FUNCTIONS ============================== /// @inheritdoc IGenericLender function apr() external view override returns (uint256) { @@ -113,7 +109,7 @@ abstract contract GenericLenderBaseUpgradeable is IGenericLender, AccessControlA } /// @inheritdoc IGenericLender - function hasAssets() external view override returns (bool) { + function hasAssets() external view virtual override returns (bool) { return _nav() > 10 * wantBase; } @@ -128,7 +124,7 @@ abstract contract GenericLenderBaseUpgradeable is IGenericLender, AccessControlA /// @notice Returns the current balance invested on the lender and related staking contracts function underlyingBalanceStored() public view virtual returns (uint256 balance); - // ============================ Governance Functions =========================== + // ================================= GOVERNANCE ================================ /// @notice Override this to add all tokens/tokenized positions this contract /// manages on a *persistent* basis (e.g. not just for swapping back to @@ -149,9 +145,8 @@ abstract contract GenericLenderBaseUpgradeable is IGenericLender, AccessControlA /// @inheritdoc IGenericLender function sweep(address _token, address to) external override onlyRole(GUARDIAN_ROLE) { address[] memory __protectedTokens = _protectedTokens(); - - for (uint256 i = 0; i < __protectedTokens.length; i++) - if (_token == __protectedTokens[i]) revert ProtectedToken(); + uint256 protectedTokensLength = __protectedTokens.length; + for (uint256 i; i < protectedTokensLength; ++i) if (_token == __protectedTokens[i]) revert ProtectedToken(); IERC20(_token).safeTransfer(to, IERC20(_token).balanceOf(address(this))); } @@ -166,18 +161,25 @@ abstract contract GenericLenderBaseUpgradeable is IGenericLender, AccessControlA uint256[] calldata amounts ) external onlyRole(GUARDIAN_ROLE) { if (tokens.length != spenders.length || tokens.length != amounts.length) revert IncompatibleLengths(); - for (uint256 i = 0; i < tokens.length; i++) { + uint256 tokensLength = tokens.length; + for (uint256 i; i < tokensLength; ++i) { _changeAllowance(tokens[i], spenders[i], amounts[i]); } } + /// @notice Changes oneInch contract address + /// @param oneInch_ Addresses of the new 1inch api endpoint contract + function set1Inch(address oneInch_) external onlyRole(GUARDIAN_ROLE) { + _oneInch = oneInch_; + } + /// @notice Swap earned _stkAave or Aave for `want` through 1Inch /// @param minAmountOut Minimum amount of `want` to receive for the swap to happen /// @param payload Bytes needed for 1Inch API /// @dev In the case of a contract lending to Aave, tokens swapped should typically be: _stkAave -> `want` or Aave -> `want` function sellRewards(uint256 minAmountOut, bytes memory payload) external onlyRole(KEEPER_ROLE) { //solhint-disable-next-line - (bool success, bytes memory result) = oneInch.call(payload); + (bool success, bytes memory result) = _oneInch.call(payload); if (!success) _revertBytes(result); uint256 amountOut = abi.decode(result, (uint256)); @@ -186,7 +188,7 @@ abstract contract GenericLenderBaseUpgradeable is IGenericLender, AccessControlA /// @notice Internal function used for error handling function _revertBytes(bytes memory errMsg) internal pure { - if (errMsg.length > 0) { + if (errMsg.length != 0) { //solhint-disable-next-line assembly { revert(add(32, errMsg), mload(errMsg)) diff --git a/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveFraxConvexStaker.sol b/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveFraxConvexStaker.sol index df2f3b4..0d0ed4b 100644 --- a/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveFraxConvexStaker.sol +++ b/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveFraxConvexStaker.sol @@ -17,7 +17,8 @@ import "./GenericAaveUpgradeable.sol"; contract GenericAaveFraxConvexStaker is GenericAaveUpgradeable { using SafeERC20 for IERC20; - // ============================= Protocols Addresses ============================ + // ============================= PROTOCOL ADDRESSES ============================ + // solhint-disable-next-line AggregatorV3Interface private constant oracleFXS = AggregatorV3Interface(0x6Ebc52C8C1089be9eB3945C4350B68B8E4C2233f); @@ -37,7 +38,7 @@ contract GenericAaveFraxConvexStaker is GenericAaveUpgradeable { // solhint-disable-next-line uint256 internal constant RAY = 1e27; - // ================================ Variables ================================== + // ================================= VARIABLES ================================= IStakingProxyERC20 public vault; /// @notice Hash representing the position on Frax staker @@ -51,7 +52,7 @@ contract GenericAaveFraxConvexStaker is GenericAaveUpgradeable { /// @notice Last time a staker has been created uint256 public lastCreatedStake; - // ================================ Parameters ================================= + // ================================= PARAMETERS ================================ /// @notice Minimum amount of aFRAX to stake // solhint-disable-next-line @@ -59,12 +60,7 @@ contract GenericAaveFraxConvexStaker is GenericAaveUpgradeable { /// @notice Staking duration uint256 public stakingPeriod; - // ==================================== Errors ================================= - - error NoLockedLiquidity(); - error TooSmallStakingPeriod(); - - // ============================= Constructor =================================== + // ================================ CONSTRUCTOR ================================ /// @notice Wrapper built on top of the `initializeAave` method to initialize the contract /// @param _stakingPeriod Amount of time aFRAX must remain staked @@ -76,9 +72,10 @@ contract GenericAaveFraxConvexStaker is GenericAaveUpgradeable { address[] memory governorList, address guardian, address[] memory keeperList, + address oneInch_, uint256 _stakingPeriod ) external { - initializeAave(_strategy, name, _isIncentivised, governorList, guardian, keeperList); + initializeAave(_strategy, name, _isIncentivised, governorList, guardian, keeperList, oneInch_); if (_stakingPeriod < aFraxStakingContract.lock_time_min()) revert TooSmallStakingPeriod(); stakingPeriod = _stakingPeriod; lastAaveReserveNormalizedIncome = _lendingPool.getReserveNormalizedIncome(address(want)); @@ -88,7 +85,7 @@ contract GenericAaveFraxConvexStaker is GenericAaveUpgradeable { vault = IStakingProxyERC20(poolRegistry.vaultMap(convexPid, address(this))); } - // =========================== External Function =============================== + // ============================= EXTERNAL FUNCTION ============================= /// @notice Can be called before `claimRewardsExternal` to check the available rewards to be claimed function earned() external view returns (address[] memory tokenAddresses, uint256[] memory totalEarned) { @@ -101,7 +98,7 @@ contract GenericAaveFraxConvexStaker is GenericAaveUpgradeable { return vault.getReward(true); } - // =========================== Governance Functions ============================ + // ============================ GOVERNANCE FUNCTIONS =========================== /// @notice Updates the staking period on the aFRAX staking contract function setLockTime(uint256 _stakingPeriod) external onlyRole(GUARDIAN_ROLE) { @@ -109,7 +106,7 @@ contract GenericAaveFraxConvexStaker is GenericAaveUpgradeable { stakingPeriod = _stakingPeriod; } - // ============================ Virtual Functions ============================== + // ============================= VIRTUAL FUNCTIONS ============================= /// @notice Implementation of the `_stake` function to stake aFRAX in the FRAX staking contract /// @dev If there is an existent locker already on Frax/Convex staking contract (keckId != null), then this function adds to it @@ -195,7 +192,7 @@ contract GenericAaveFraxConvexStaker is GenericAaveUpgradeable { /// @notice Get stakingAPR after staking an additional `amount` /// @param amount Virtual amount to be staked - function _stakingApr(uint256 amount) internal view override returns (uint256 apr) { + function _stakingApr(int256 amount) internal view override returns (uint256 apr) { // These computations are made possible only because there can only be one staker in the contract (uint256 oldCombinedWeight, uint256 newVefxsMultiplier, uint256 newCombinedWeight) = aFraxStakingContract .calcCurCombinedWeight(address(vault)); @@ -206,12 +203,14 @@ contract GenericAaveFraxConvexStaker is GenericAaveUpgradeable { // If we didn't stake we need an extra info on the multiplier per staking period // otherwise we reverse engineer the function else if (lastLiquidity == 0) { - newBalance = amount; + newBalance = uint256(amount); newCombinedWeight = (newBalance * (aFraxStakingContract.lockMultiplier(stakingPeriod) + newVefxsMultiplier)) / 1 ether; } else { - newBalance = (_stakedBalance() + amount); + newBalance = _stakedBalance(); + if (amount >= 0) newBalance += uint256(amount); + else newBalance -= uint256(-amount); newCombinedWeight = (newBalance * newCombinedWeight) / lastLiquidity; } @@ -230,7 +229,7 @@ contract GenericAaveFraxConvexStaker is GenericAaveUpgradeable { apr = (_estimatedFXSToWant(rewardRate * _SECONDS_IN_YEAR) * 9500 * 1 ether) / 10000 / newBalance; } - // ============================ Internal Functions ============================= + // ============================= INTERNAL FUNCTION ============================= /// @notice Estimates the amount of `want` we will get out by swapping it for FXS /// @param amount Amount of FXS we want to exchange (in base 18) diff --git a/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveFraxStaker.sol b/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveFraxStaker.sol index 53b28d9..c8a89eb 100644 --- a/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveFraxStaker.sol +++ b/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveFraxStaker.sol @@ -13,7 +13,8 @@ contract GenericAaveFraxStaker is GenericAaveUpgradeable { using SafeERC20 for IERC20; using Address for address; - // ============================= Protocol Addresses ============================ + // ============================= PROTOCOL ADDRESSES ============================ + // solhint-disable-next-line AggregatorV3Interface private constant oracleFXS = AggregatorV3Interface(0x6Ebc52C8C1089be9eB3945C4350B68B8E4C2233f); @@ -23,7 +24,7 @@ contract GenericAaveFraxStaker is GenericAaveUpgradeable { // solhint-disable-next-line uint256 private constant FRAX_IDX = 0; - // ================================ Variables ================================== + // ================================= VARIABLES ================================= /// @notice Hash representing the position on Frax staker bytes32 public kekId; @@ -36,7 +37,7 @@ contract GenericAaveFraxStaker is GenericAaveUpgradeable { /// @notice Last time a staker has been created uint256 public lastCreatedStake; - // ================================ Parameters ================================= + // ================================= PARAMETERS ================================ /// @notice Minimum amount of aFRAX to stake // solhint-disable-next-line @@ -44,12 +45,7 @@ contract GenericAaveFraxStaker is GenericAaveUpgradeable { /// @notice Staking duration uint256 public stakingPeriod; - // ==================================== Errors ================================= - - error NoLockedLiquidity(); - error TooSmallStakingPeriod(); - - // ============================= Constructor =================================== + // ================================ CONSTRUCTOR ================================ /// @notice Wrapper built on top of the `initializeAave` method to initialize the contract /// @param _stakingPeriod Amount of time aFRAX must remain staked @@ -61,15 +57,16 @@ contract GenericAaveFraxStaker is GenericAaveUpgradeable { address[] memory governorList, address guardian, address[] memory keeperList, + address oneInch_, uint256 _stakingPeriod ) external { - initializeAave(_strategy, name, _isIncentivised, governorList, guardian, keeperList); + initializeAave(_strategy, name, _isIncentivised, governorList, guardian, keeperList, oneInch_); if (_stakingPeriod < aFraxStakingContract.lock_time_min()) revert TooSmallStakingPeriod(); stakingPeriod = _stakingPeriod; lastAaveReserveNormalizedIncome = _lendingPool.getReserveNormalizedIncome(address(want)); } - // =========================== External Function =============================== + // ============================= EXTERNAL FUNCTION ============================= /// @notice Permisionless function to claim rewards, reward tokens are directly sent to the contract and keeper/governance /// can handle them via a `sweep` or a `sellRewards` call @@ -77,7 +74,7 @@ contract GenericAaveFraxStaker is GenericAaveUpgradeable { return aFraxStakingContract.getReward(address(this)); } - // =========================== Governance Functions ============================ + // ============================ GOVERNANCE FUNCTIONS =========================== /// @notice Updates the staking period on the aFRAX staking contract function setLockTime(uint256 _stakingPeriod) external onlyRole(GUARDIAN_ROLE) { @@ -93,7 +90,7 @@ contract GenericAaveFraxStaker is GenericAaveUpgradeable { aFraxStakingContract.stakerSetVeFXSProxy(proxy); } - // ============================ Virtual Functions ============================== + // ============================= VIRTUAL FUNCTIONS ============================= /// @notice Implementation of the `_stake` function to stake aFRAX in the FRAX staking contract /// @dev If there is an existent locker already on Frax staking contract (keckId != null), then this function adds to it @@ -153,7 +150,7 @@ contract GenericAaveFraxStaker is GenericAaveUpgradeable { /// @notice Get stakingAPR after staking an additional `amount` /// @param amount Virtual amount to be staked - function _stakingApr(uint256 amount) internal view override returns (uint256 apr) { + function _stakingApr(int256 amount) internal view override returns (uint256 apr) { // These computations are made possible only because there can only be one staker in the contract (uint256 oldCombinedWeight, uint256 newVefxsMultiplier, uint256 newCombinedWeight) = aFraxStakingContract .calcCurCombinedWeight(address(this)); @@ -164,12 +161,15 @@ contract GenericAaveFraxStaker is GenericAaveUpgradeable { // If we didn't stake we need an extra info on the multiplier per staking period // otherwise we reverse engineer the function else if (lastLiquidity == 0) { - newBalance = amount; + // Amount can only be positive as we can't withdraw if there is nothing to this strat + newBalance = uint256(amount); newCombinedWeight = (newBalance * (aFraxStakingContract.lockMultiplier(stakingPeriod) + newVefxsMultiplier)) / 1 ether; } else { - newBalance = (_stakedBalance() + amount); + newBalance = _stakedBalance(); + if (amount >= 0) newBalance += uint256(amount); + else newBalance -= uint256(-amount); newCombinedWeight = (newBalance * newCombinedWeight) / lastLiquidity; } @@ -184,7 +184,7 @@ contract GenericAaveFraxStaker is GenericAaveUpgradeable { apr = (_estimatedFXSToWant(rewardRate * _SECONDS_IN_YEAR) * 9500 * 1 ether) / 10000 / newBalance; } - // ============================ Internal Functions ============================= + // ============================= INTERNAL FUNCTIONS ============================ /// @notice Estimates the amount of `want` we will get out by swapping it for FXS /// @param amount Amount of FXS we want to exchange (in base 18) diff --git a/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveNoStaker.sol b/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveNoStaker.sol index 6f72de8..6168ddb 100644 --- a/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveNoStaker.sol +++ b/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveNoStaker.sol @@ -10,7 +10,7 @@ import "./GenericAaveUpgradeable.sol"; /// @dev In this implementation, we just have to override the base functions with constant amounts as nothing is /// staked in an external contract contract GenericAaveNoStaker is GenericAaveUpgradeable { - // ================================ Constructor ================================ + // ================================ CONSTRUCTOR ================================ /// @notice Wrapper on top of the `initializeAave` method function initialize( @@ -19,12 +19,13 @@ contract GenericAaveNoStaker is GenericAaveUpgradeable { bool _isIncentivised, address[] memory governorList, address guardian, - address[] memory keeperList + address[] memory keeperList, + address oneInch_ ) external { - initializeAave(_strategy, name, _isIncentivised, governorList, guardian, keeperList); + initializeAave(_strategy, name, _isIncentivised, governorList, guardian, keeperList, oneInch_); } - // =========================== Virtual Functions =============================== + // ============================= VIRTUAL FUNCTIONS ============================= function _stake(uint256) internal override returns (uint256) {} @@ -39,7 +40,7 @@ contract GenericAaveNoStaker is GenericAaveUpgradeable { /// @notice Get stakingAPR after staking an additional `amount`: in this case since nothing /// is staked, it simply returns 0 - function _stakingApr(uint256) internal pure override returns (uint256) { + function _stakingApr(int256) internal pure override returns (uint256) { return 0; } } diff --git a/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveUpgradeable.sol b/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveUpgradeable.sol index 48dc0fe..5e55665 100644 --- a/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveUpgradeable.sol +++ b/contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveUpgradeable.sol @@ -21,12 +21,11 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { using SafeERC20 for IERC20; using Address for address; - // ======================== Reference to contract ============================== + // ================================= REFERENCES ================================ + // solhint-disable-next-line AggregatorV3Interface private constant oracle = AggregatorV3Interface(0x547a514d5e3769680Ce22B2361c10Ea13619e8a9); - // ========================== Aave Protocol Addresses ========================== - // solhint-disable-next-line address private constant _aave = 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9; // solhint-disable-next-line @@ -40,7 +39,8 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { IProtocolDataProvider private constant _protocolDataProvider = IProtocolDataProvider(0x057835Ad21a177dbdd3090bB1CAE03EaCF78Fc6d); - // ========================= Constants and Parameters ========================== + // ================================= CONSTANTS ================================= + uint256 internal constant _SECONDS_IN_YEAR = 365 days; uint256 public cooldownSeconds; uint256 public unstakeWindow; @@ -50,15 +50,11 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { uint256[47] private __gapAaveLender; - // =================================== Event =================================== + // =================================== EVENT =================================== event IncentivisedUpdated(bool _isIncentivised); - // =================================== Error =================================== - - error PoolNotIncentivized(); - - // ================================ Constructor ================================ + // ================================ CONSTRUCTOR ================================ /// @notice Initializer of the `GenericAave` /// @param _strategy Reference to the strategy using this lender @@ -73,9 +69,10 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { bool _isIncentivised, address[] memory governorList, address guardian, - address[] memory keeperList + address[] memory keeperList, + address oneInch_ ) public { - _initialize(_strategy, name, governorList, guardian, keeperList); + _initialize(_strategy, name, governorList, guardian, keeperList, oneInch_); _setAavePoolVariables(); if (_isIncentivised && address(_aToken.getIncentivesController()) == address(0)) revert PoolNotIncentivized(); @@ -83,11 +80,11 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { cooldownStkAave = true; IERC20(address(want)).safeApprove(address(_lendingPool), type(uint256).max); // Approve swap router spend - IERC20(address(_stkAave)).safeApprove(oneInch, type(uint256).max); - IERC20(address(_aave)).safeApprove(oneInch, type(uint256).max); + IERC20(address(_stkAave)).safeApprove(oneInch_, type(uint256).max); + IERC20(address(_aave)).safeApprove(oneInch_, type(uint256).max); } - // ============================= External Functions ============================ + // ============================= EXTERNAL FUNCTIONS ============================ /// @inheritdoc IGenericLender function deposit() external override onlyRole(STRATEGY_ROLE) { @@ -137,7 +134,7 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { _setAavePoolVariables(); } - // ========================== External Setter Functions ======================== + // ================================== SETTERS ================================== /// @notice Toggle isIncentivised state, to let know the lender if it should harvest aave rewards function toggleIsIncentivised() external onlyRole(GUARDIAN_ROLE) { @@ -149,7 +146,7 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { cooldownStkAave = !cooldownStkAave; } - // =========================== External View Functions ========================= + // ========================== EXTERNAL VIEW FUNCTIONS ========================== /// @inheritdoc GenericLenderBaseUpgradeable function underlyingBalanceStored() public view override returns (uint256 balance) { @@ -157,7 +154,7 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { } /// @inheritdoc IGenericLender - function aprAfterDeposit(uint256 extraAmount) external view override returns (uint256) { + function aprAfterDeposit(int256 amount) external view override returns (uint256) { // i need to calculate new supplyRate after Deposit (when deposit has not been done yet) DataTypes.ReserveData memory reserveData = _lendingPool.getReserveData(address(want)); @@ -174,7 +171,9 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { ) = _protocolDataProvider.getReserveData(address(want)); - uint256 newLiquidity = availableLiquidity + extraAmount; + uint256 newLiquidity = availableLiquidity; + if (amount >= 0) newLiquidity += uint256(amount); + else newLiquidity -= uint256(-amount); (, , , , uint256 reserveFactor, , , , , ) = _protocolDataProvider.getReserveConfigurationData(address(want)); @@ -188,18 +187,18 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { reserveFactor ); uint256 incentivesRate = _incentivesRate(newLiquidity + totalStableDebt + totalVariableDebt); // total supplied liquidity in Aave v2 - uint256 stakingApr = _stakingApr(extraAmount); + uint256 stakingApr = _stakingApr(amount); return newLiquidityRate / 1e9 + incentivesRate + stakingApr; // divided by 1e9 to go from Ray to Wad } - // =========================== Internal Functions ============================== + // ============================= INTERNAL FUNCTIONS ============================ /// @notice Internal version of the `claimRewards` function function _claimRewards() internal returns (uint256 stkAaveBalance) { stkAaveBalance = _balanceOfStkAave(); // If it's the claim period claim - if (stkAaveBalance > 0 && _checkCooldown() == 1) { + if (stkAaveBalance != 0 && _checkCooldown() == 1) { // redeem AAVE from _stkAave _stkAave.claimRewards(address(this), type(uint256).max); _stkAave.redeem(address(this), stkAaveBalance); @@ -213,7 +212,7 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { stkAaveBalance = _balanceOfStkAave(); // request start of cooldown period, if there's no cooldown in progress - if (cooldownStkAave && stkAaveBalance > 0 && _checkCooldown() == 0) { + if (cooldownStkAave && stkAaveBalance != 0 && _checkCooldown() == 0) { _stkAave.cooldown(); } } @@ -261,12 +260,12 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { /// @notice Calculates APR from Liquidity Mining Program /// @param totalLiquidity Total liquidity available in the pool function _incentivesRate(uint256 totalLiquidity) internal view returns (uint256) { - // only returns != 0 if the incentives are in place at the moment. - // it will fail if the isIncentivised is set to true but there are no incentives - if (isIncentivised && block.timestamp < _incentivesController.getDistributionEnd() && totalLiquidity > 0) { + // Only returns != 0 if the incentives are in place at the moment. + // It will fail if `isIncentivised` is set to true but there are no incentives + if (isIncentivised && block.timestamp < _incentivesController.getDistributionEnd() && totalLiquidity != 0) { uint256 _emissionsPerSecond; (, _emissionsPerSecond, ) = _incentivesController.getAssetData(address(_aToken)); - if (_emissionsPerSecond > 0) { + if (_emissionsPerSecond != 0) { uint256 emissionsInWant = _estimatedStkAaveToWant(_emissionsPerSecond); // amount of emissions in want uint256 incentivesRate = (emissionsInWant * _SECONDS_IN_YEAR * 1e18) / totalLiquidity; // APRs are in 1e18 @@ -361,7 +360,7 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { return protected; } - // ========================= Virtual Functions =========================== + // ============================= VIRTUAL FUNCTIONS ============================= /// @notice Allows the lender to stake its aTokens in an external staking contract /// @param amount Amount of aTokens to stake @@ -379,5 +378,5 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { /// @notice Gets the APR from staking additional `amount` of aTokens in the associated staking /// contract /// @param amount Virtual amount to be staked - function _stakingApr(uint256 amount) internal view virtual returns (uint256); + function _stakingApr(int256 amount) internal view virtual returns (uint256); } diff --git a/contracts/strategies/OptimizerAPR/genericLender/compound/GenericCompoundUpgradeable.sol b/contracts/strategies/OptimizerAPR/genericLender/compound/GenericCompoundUpgradeable.sol index bf46bc0..30ac20b 100644 --- a/contracts/strategies/OptimizerAPR/genericLender/compound/GenericCompoundUpgradeable.sol +++ b/contracts/strategies/OptimizerAPR/genericLender/compound/GenericCompoundUpgradeable.sol @@ -25,20 +25,12 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { // solhint-disable-next-line address public constant comp = 0xc00e94Cb662C3520282E6f5717214004A7f26888; - // ======================== References to contracts ============================ + // ================================= REFERENCES ================================ CErc20I public cToken; // solhint-disable-next-line uint256 private dust; - // =============================== Errors ====================================== - - error FailedToMint(); - error FailedToRecoverETH(); - error FailedToRedeem(); - error InvalidOracleValue(); - error WrongCToken(); - // ============================= Constructor =================================== /// @notice Initializer of the `GenericCompound` @@ -53,18 +45,19 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { address _cToken, address[] memory governorList, address guardian, - address[] memory keeperList + address[] memory keeperList, + address oneInch_ ) external { - _initialize(_strategy, _name, governorList, guardian, keeperList); + _initialize(_strategy, _name, governorList, guardian, keeperList, oneInch_); cToken = CErc20I(_cToken); if (CErc20I(_cToken).underlying() != address(want)) revert WrongCToken(); want.safeApprove(_cToken, type(uint256).max); - IERC20(comp).safeApprove(oneInch, type(uint256).max); + IERC20(comp).safeApprove(oneInch_, type(uint256).max); } - // ===================== External Strategy Functions =========================== + // ======================== EXTERNAL STRATEGY FUNCTIONS ======================== /// @inheritdoc IGenericLender function deposit() external override onlyRole(STRATEGY_ROLE) { @@ -84,7 +77,7 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { return returned >= invested; } - // ========================== External View Functions ========================== + // ========================== EXTERNAL VIEW FUNCTIONS ========================== /// @inheritdoc GenericLenderBaseUpgradeable function underlyingBalanceStored() public view override returns (uint256 balance) { @@ -98,7 +91,7 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { } /// @inheritdoc IGenericLender - function aprAfterDeposit(uint256 amount) external view override returns (uint256) { + function aprAfterDeposit(int256 amount) external view override returns (uint256) { uint256 cashPrior = want.balanceOf(address(cToken)); uint256 borrows = cToken.totalBorrows(); @@ -109,13 +102,22 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { InterestRateModel model = cToken.interestRateModel(); + uint256 newCashPrior = cashPrior; + uint256 totalSupplyInWant = (cToken.totalSupply() * cToken.exchangeRateStored()) / 1e18; + if (amount >= 0) { + newCashPrior += uint256(amount); + totalSupplyInWant += uint256(amount); + } else { + newCashPrior -= uint256(-amount); + totalSupplyInWant -= uint256(-amount); + } // The supply rate is derived from the borrow rate, reserve factor and the amount of total borrows. - uint256 supplyRate = model.getSupplyRate(cashPrior + amount, borrows, reserves, reserverFactor); + uint256 supplyRate = model.getSupplyRate(newCashPrior, borrows, reserves, reserverFactor); // Adding the yield from comp - return supplyRate * BLOCKS_PER_YEAR + _incentivesRate(amount); + return supplyRate * BLOCKS_PER_YEAR + _incentivesRate(totalSupplyInWant); } - // ================================= Governance ================================ + // ================================= GOVERNANCE ================================ /// @inheritdoc IGenericLender function emergencyWithdraw(uint256 amount) external override onlyRole(GUARDIAN_ROLE) { @@ -132,11 +134,12 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { dust = dust_; } - // ============================= Internal Functions ============================ + // ============================= INTERNAL FUNCTIONS ============================ /// @notice See `apr` function _apr() internal view override returns (uint256) { - return cToken.supplyRatePerBlock() * BLOCKS_PER_YEAR + _incentivesRate(0); + uint256 totalSupplyInWant = (cToken.totalSupply() * cToken.exchangeRateStored()) / 1e18; + return cToken.supplyRatePerBlock() * BLOCKS_PER_YEAR + _incentivesRate(totalSupplyInWant); } /// @notice See `withdraw` @@ -184,10 +187,9 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { } /// @notice Calculates APR from Compound's Liquidity Mining Program - /// @param amountToAdd Amount to add to the `totalSupplyInWant` (for the `aprAfterDeposit` function) - function _incentivesRate(uint256 amountToAdd) internal view returns (uint256) { + /// @param totalSupplyInWant Total supply in want for this market (for the `aprAfterDeposit` function) + function _incentivesRate(uint256 totalSupplyInWant) internal view returns (uint256) { uint256 supplySpeed = comptroller.compSupplySpeeds(address(cToken)); - uint256 totalSupplyInWant = (cToken.totalSupply() * cToken.exchangeRateStored()) / 1e18 + amountToAdd; // `supplySpeed` is in `COMP` unit -> the following operation is going to put it in `want` unit supplySpeed = _comptoWant(supplySpeed); uint256 incentivesRate; diff --git a/contracts/strategies/OptimizerAPR/genericLender/euler/GenericEuler.sol b/contracts/strategies/OptimizerAPR/genericLender/euler/GenericEuler.sol index e2c0150..f1d419f 100644 --- a/contracts/strategies/OptimizerAPR/genericLender/euler/GenericEuler.sol +++ b/contracts/strategies/OptimizerAPR/genericLender/euler/GenericEuler.sol @@ -24,7 +24,7 @@ contract GenericEuler is GenericLenderBaseUpgradeable { // solhint-disable-next-line IEulerMarkets private constant _eulerMarkets = IEulerMarkets(0x3520d5a913427E6F0D6A83E07ccD4A4da316e4d3); // solhint-disable-next-line - uint256 private constant SECONDS_PER_YEAR = 365.2425 * 86400; + uint256 internal constant _SECONDS_IN_YEAR = 365 days; // solhint-disable-next-line uint256 private constant RESERVE_FEE_SCALE = 4_000_000_000; @@ -53,9 +53,10 @@ contract GenericEuler is GenericLenderBaseUpgradeable { string memory _name, address[] memory governorList, address guardian, - address[] memory keeperList + address[] memory keeperList, + address oneInch_ ) public { - _initialize(_strategy, _name, governorList, guardian, keeperList); + _initialize(_strategy, _name, governorList, guardian, keeperList, oneInch_); eToken = IEulerEToken(_eulerMarkets.underlyingToEToken(address(want))); dToken = IEulerDToken(_eulerMarkets.underlyingToDToken(address(want))); @@ -106,7 +107,7 @@ contract GenericEuler is GenericLenderBaseUpgradeable { } /// @inheritdoc IGenericLender - function aprAfterDeposit(uint256 amount) external view override returns (uint256) { + function aprAfterDeposit(int256 amount) external view override returns (uint256) { return _aprAfterDeposit(amount); } @@ -127,13 +128,16 @@ contract GenericEuler is GenericLenderBaseUpgradeable { } /// @notice Internal version of the `aprAfterDeposit` function - function _aprAfterDeposit(uint256 amount) internal view returns (uint256) { + function _aprAfterDeposit(int256 amount) internal view returns (uint256) { uint256 totalBorrows = dToken.totalSupply(); // Total supply is current supply + added liquidity - uint256 totalSupply = eToken.totalSupplyUnderlying() + amount; + + uint256 totalSupply = eToken.totalSupplyUnderlying(); + if (amount >= 0) totalSupply += uint256(amount); + else totalSupply -= uint256(-amount); uint256 supplyAPY; - if (totalSupply > 0) { + if (totalSupply != 0) { uint32 futureUtilisationRate = uint32( (totalBorrows * (uint256(type(uint32).max) * 1e18)) / totalSupply / 1e18 ); @@ -158,11 +162,11 @@ contract GenericEuler is GenericLenderBaseUpgradeable { uint32 _reserveFee ) internal pure returns (uint256 supplyAPY) { // Not useful for the moment - // uint256 borrowAPY = (ComputePower.computePower(borrowSPY, SECONDS_PER_YEAR) - ComputePower.BASE_INTEREST) / 1e9; + // uint256 borrowAPY = (ComputePower.computePower(borrowSPY, _SECONDS_IN_YEAR) - ComputePower.BASE_INTEREST) / 1e9; uint256 supplySPY = (borrowSPY * totalBorrows) / totalSupplyUnderlying; supplySPY = (supplySPY * (RESERVE_FEE_SCALE - _reserveFee)) / RESERVE_FEE_SCALE; // All rates are in base 18 on Angle strategies - supplyAPY = (ComputePower.computePower(supplySPY, SECONDS_PER_YEAR, BASE_INTEREST) - BASE_INTEREST) / 1e9; + supplyAPY = (ComputePower.computePower(supplySPY, _SECONDS_IN_YEAR, BASE_INTEREST) - BASE_INTEREST) / 1e9; } /// @notice See `withdraw` @@ -198,7 +202,7 @@ contract GenericEuler is GenericLenderBaseUpgradeable { toUnstake = availableLiquidity > balanceUnderlying ? availableLiquidity - balanceUnderlying : 0; toWithdraw = availableLiquidity; } - if (toUnstake > 0) _unstake(toUnstake); + if (toUnstake != 0) _unstake(toUnstake); eToken.withdraw(0, toWithdraw); } @@ -241,7 +245,7 @@ contract GenericEuler is GenericLenderBaseUpgradeable { /// @notice Calculates APR from Liquidity Mining Program /// @dev amountToAdd Amount to add to the currently supplied liquidity (for the `aprAfterDeposit` function) - function _stakingApr(uint256) internal view virtual returns (uint256) { + function _stakingApr(int256) internal view virtual returns (uint256) { return 0; } } diff --git a/contracts/strategies/OptimizerAPR/genericLender/euler/GenericEulerStaker.sol b/contracts/strategies/OptimizerAPR/genericLender/euler/GenericEulerStaker.sol index ab319ed..75a0010 100644 --- a/contracts/strategies/OptimizerAPR/genericLender/euler/GenericEulerStaker.sol +++ b/contracts/strategies/OptimizerAPR/genericLender/euler/GenericEulerStaker.sol @@ -16,9 +16,11 @@ contract GenericEulerStaker is GenericEuler, OracleMath { using Address for address; // ================================= CONSTANTS ================================= - uint256 internal constant _SECONDS_IN_YEAR = 365 days; uint32 internal constant _TWAP_PERIOD = 1 minutes; + /// @notice EUL token address + IERC20 private constant _EUL = IERC20(0xd9Fcd98c322942075A5C3860693e9f4f03AAE07b); + // ================================= VARIABLES ================================= IEulerStakingRewards public eulerStakingContract; AggregatorV3Interface public chainlinkOracle; @@ -34,17 +36,19 @@ contract GenericEulerStaker is GenericEuler, OracleMath { address[] memory governorList, address guardian, address[] memory keeperList, + address oneInch_, IEulerStakingRewards _eulerStakingContract, AggregatorV3Interface _chainlinkOracle, IUniswapV3Pool _pool, uint8 _isUniMultiplied ) external { - initializeEuler(_strategy, _name, governorList, guardian, keeperList); + initializeEuler(_strategy, _name, governorList, guardian, keeperList, oneInch_); eulerStakingContract = _eulerStakingContract; chainlinkOracle = _chainlinkOracle; pool = _pool; isUniMultiplied = _isUniMultiplied; IERC20(address(eToken)).safeApprove(address(_eulerStakingContract), type(uint256).max); + IERC20(_EUL).safeApprove(oneInch_, type(uint256).max); } // ============================= EXTERNAL FUNCTION ============================= @@ -75,9 +79,11 @@ contract GenericEulerStaker is GenericEuler, OracleMath { } /// @inheritdoc GenericEuler - function _stakingApr(uint256 amount) internal view override returns (uint256 apr) { + function _stakingApr(int256 amount) internal view override returns (uint256 apr) { uint256 periodFinish = eulerStakingContract.periodFinish(); - uint256 newTotalSupply = eulerStakingContract.totalSupply() + eToken.convertUnderlyingToBalance(amount); + uint256 newTotalSupply = eulerStakingContract.totalSupply(); + if (amount >= 0) newTotalSupply += eToken.convertUnderlyingToBalance(uint256(amount)); + else newTotalSupply -= eToken.convertUnderlyingToBalance(uint256(-amount)); if (periodFinish <= block.timestamp || newTotalSupply == 0) return 0; // APRs are in 1e18 and a 5% penalty on the EUL price is taken to avoid overestimations // `_estimatedEulToWant()` and eTokens are in base 18 diff --git a/contracts/utils/Errors.sol b/contracts/utils/Errors.sol new file mode 100644 index 0000000..ead1f19 --- /dev/null +++ b/contracts/utils/Errors.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.17; + +error ErrorSwap(); +error FailedToMint(); +error FailedToRecoverETH(); +error FailedToRedeem(); +error FailedWithdrawal(); +error IncompatibleLengths(); +error IncorrectListLength(); +error InvalidOracleValue(); +error InvalidSender(); +error InvalidSetOfParameters(); +error InvalidShares(); +error InvalidToken(); +error InvalidWithdrawCheck(); +error LenderAlreadyAdded(); +error NoLockedLiquidity(); +error NonExistentLender(); +error PoolNotIncentivized(); +error ProtectedToken(); +error TooHighParameterValue(); +error TooSmallAmount(); +error TooSmallAmountOut(); +error TooSmallStakingPeriod(); +error UndockedLender(); +error WrongCToken(); +error ZeroAddress(); diff --git a/deploy/GenericAaveFraxConvexStaker.ts b/deploy/GenericAaveFraxConvexStaker.ts index 124a0b9..81aafb8 100644 --- a/deploy/GenericAaveFraxConvexStaker.ts +++ b/deploy/GenericAaveFraxConvexStaker.ts @@ -1,7 +1,7 @@ import hre, { network } from 'hardhat'; import { DeployFunction } from 'hardhat-deploy/types'; import { CONTRACTS_ADDRESSES, ChainId } from '@angleprotocol/sdk'; -import { GenericAaveFraxStaker__factory, OptimizerAPRStrategy, OptimizerAPRStrategy__factory } from '../typechain'; +import { GenericAaveFraxStaker__factory, OptimizerAPRGreedyStrategy, OptimizerAPRGreedyStrategy__factory } from '../typechain'; import { DAY } from '../test/hardhat/contants'; import { BigNumber } from 'ethers'; @@ -42,9 +42,9 @@ const func: DeployFunction = async ({ deployments, ethers }) => { const strategy = new ethers.Contract( strategyAddress, - OptimizerAPRStrategy__factory.createInterface(), + OptimizerAPRGreedyStrategy__factory.createInterface(), deployer, - ) as OptimizerAPRStrategy; + ) as OptimizerAPRGreedyStrategy; let lenderImplementation = await deployments.getOrNull( `GenericAave_${stableName}_${collateralName}_Convex_Staker_Implementation`, diff --git a/deploy/GenericAaveFraxStaker.ts b/deploy/GenericAaveFraxStaker.ts index a863ee1..612b77b 100644 --- a/deploy/GenericAaveFraxStaker.ts +++ b/deploy/GenericAaveFraxStaker.ts @@ -1,7 +1,7 @@ import hre, { network } from 'hardhat'; import { DeployFunction } from 'hardhat-deploy/types'; import { CONTRACTS_ADDRESSES, ChainId } from '@angleprotocol/sdk'; -import { GenericAaveFraxStaker__factory, OptimizerAPRStrategy, OptimizerAPRStrategy__factory } from '../typechain'; +import { GenericAaveFraxStaker__factory, OptimizerAPRGreedyStrategy, OptimizerAPRGreedyStrategy__factory } from '../typechain'; import { DAY } from '../test/hardhat/contants'; import { BigNumber } from 'ethers'; @@ -37,9 +37,9 @@ const func: DeployFunction = async ({ deployments, ethers }) => { const strategy = new ethers.Contract( strategyAddress, - OptimizerAPRStrategy__factory.createInterface(), + OptimizerAPRGreedyStrategy__factory.createInterface(), deployer, - ) as OptimizerAPRStrategy; + ) as OptimizerAPRGreedyStrategy; let lenderImplementation = await deployments.getOrNull( `GenericAave_${stableName}_${collateralName}_Staker_Implementation`, diff --git a/deploy/lenders/GenericAave.ts b/deploy/lenders/GenericAave.ts index e47c3d4..d7d4c72 100644 --- a/deploy/lenders/GenericAave.ts +++ b/deploy/lenders/GenericAave.ts @@ -2,7 +2,7 @@ import { network } from 'hardhat'; import { DeployFunction } from 'hardhat-deploy/types'; import { CONTRACTS_ADDRESSES, ChainId } from '@angleprotocol/sdk'; import { BigNumber } from 'ethers'; -import { GenericAaveNoStaker__factory, OptimizerAPRStrategy, OptimizerAPRStrategy__factory } from '../../typechain'; +import { GenericAaveNoStaker__factory, OptimizerAPRGreedyStrategy, OptimizerAPRGreedyStrategy__factory } from '../../typechain'; import { impersonate } from '../../test/hardhat/test-utils'; const func: DeployFunction = async ({ deployments, ethers }) => { @@ -70,9 +70,9 @@ const func: DeployFunction = async ({ deployments, ethers }) => { if (!network.live) { const strategy = new ethers.Contract( strategyAddress, - OptimizerAPRStrategy__factory.createInterface(), + OptimizerAPRGreedyStrategy__factory.createInterface(), deployer, - ) as OptimizerAPRStrategy; + ) as OptimizerAPRGreedyStrategy; await impersonate(guardian, async acc => { await network.provider.send('hardhat_setBalance', [guardian, '0x10000000000000000000000000000']); diff --git a/deploy/lenders/GenericCompoundV3.ts b/deploy/lenders/GenericCompoundV3.ts index 566ee0b..9683c65 100644 --- a/deploy/lenders/GenericCompoundV3.ts +++ b/deploy/lenders/GenericCompoundV3.ts @@ -7,8 +7,8 @@ import { ERC20__factory, GenericCompoundUpgradeable, GenericCompoundUpgradeable__factory, - OptimizerAPRStrategy, - OptimizerAPRStrategy__factory, + OptimizerAPRGreedyStrategy, + OptimizerAPRGreedyStrategy__factory, } from '../../typechain'; import { parseUnits } from 'ethers/lib/utils'; import { impersonate } from '../../test/hardhat/test-utils'; @@ -98,9 +98,9 @@ const func: DeployFunction = async ({ deployments, ethers }) => { const strategy = new ethers.Contract( strategyAddress, - OptimizerAPRStrategy__factory.createInterface(), + OptimizerAPRGreedyStrategy__factory.createInterface(), deployer, - ) as OptimizerAPRStrategy; + ) as OptimizerAPRGreedyStrategy; await impersonate(guardian, async acc => { await network.provider.send('hardhat_setBalance', [guardian, '0x10000000000000000000000000000']); diff --git a/deploy/lenders/GenericEuler.ts b/deploy/lenders/GenericEuler.ts index 04c9f2d..f24d965 100644 --- a/deploy/lenders/GenericEuler.ts +++ b/deploy/lenders/GenericEuler.ts @@ -2,7 +2,7 @@ import { network } from 'hardhat'; import { DeployFunction } from 'hardhat-deploy/types'; import { CONTRACTS_ADDRESSES, ChainId } from '@angleprotocol/sdk'; import { BigNumber } from 'ethers'; -import { GenericEuler__factory, OptimizerAPRStrategy, OptimizerAPRStrategy__factory } from '../../typechain'; +import { GenericEuler__factory, OptimizerAPRGreedyStrategy, OptimizerAPRGreedyStrategy__factory } from '../../typechain'; import { impersonate } from '../../test/hardhat/test-utils'; const func: DeployFunction = async ({ deployments, ethers }) => { @@ -77,9 +77,9 @@ const func: DeployFunction = async ({ deployments, ethers }) => { if (!network.live) { const strategy = new ethers.Contract( strategyAddress, - OptimizerAPRStrategy__factory.createInterface(), + OptimizerAPRGreedyStrategy__factory.createInterface(), deployer, - ) as OptimizerAPRStrategy; + ) as OptimizerAPRGreedyStrategy; await impersonate(guardian, async acc => { await network.provider.send('hardhat_setBalance', [guardian, '0x10000000000000000000000000000']); diff --git a/deploy/lenders/saveFunds.ts b/deploy/lenders/saveFunds.ts index 065e355..83a3cc9 100644 --- a/deploy/lenders/saveFunds.ts +++ b/deploy/lenders/saveFunds.ts @@ -9,8 +9,8 @@ import { IAaveIncentivesController, IStakedAave, IStakedAave__factory, - OptimizerAPRStrategy, - OptimizerAPRStrategy__factory, + OptimizerAPRGreedyStrategy, + OptimizerAPRGreedyStrategy__factory, } from '../../typechain'; import { impersonate } from '../../test/hardhat/test-utils'; import { @@ -82,9 +82,9 @@ const func: DeployFunction = async ({ deployments, ethers }) => { const strategy = new ethers.Contract( strategyAddress, - OptimizerAPRStrategy__factory.createInterface(), + OptimizerAPRGreedyStrategy__factory.createInterface(), deployer, - ) as OptimizerAPRStrategy; + ) as OptimizerAPRGreedyStrategy; const stkAave = (await ethers.getContractAt(IStakedAave__factory.abi, stkAaveAddress)) as IStakedAave; const aave = (await ethers.getContractAt(ERC20__factory.abi, aaveAddress)) as ERC20; diff --git a/foundry.toml b/foundry.toml index 88037e2..ac29185 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,6 +6,7 @@ libs = ['node_modules', 'lib'] script = 'scripts/foundry' cache_path = 'cache-forge' gas_reports = ["*"] +via_ir = true # solc_version = '0.8.17' diff --git a/hardhat.config.ts b/hardhat.config.ts index 617500e..792c551 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -46,6 +46,7 @@ const config: HardhatUserConfig = { { version: '0.8.17', settings: { + viaIR: true, optimizer: { enabled: true, runs: 1000000, @@ -57,6 +58,7 @@ const config: HardhatUserConfig = { 'contracts/strategies/AaveFlashloanStrategy/AaveFlashloanStrategy.sol': { version: '0.8.17', settings: { + viaIR: true, optimizer: { enabled: true, runs: 3000, @@ -214,9 +216,6 @@ const config: HardhatUserConfig = { flat: true, spacing: 2, }, - etherscan: { - apiKey: process.env.ETHERSCAN_API_KEY, - }, typechain: { outDir: 'typechain', target: 'ethers-v5', diff --git a/scripts/mainnet-fork/genericLender/addEulerLender.ts b/scripts/mainnet-fork/genericLender/addEulerLender.ts index 8d0f1e3..3544eb5 100644 --- a/scripts/mainnet-fork/genericLender/addEulerLender.ts +++ b/scripts/mainnet-fork/genericLender/addEulerLender.ts @@ -10,8 +10,8 @@ import { GenericCompoundUpgradeable__factory, GenericEuler, GenericEuler__factory, - OptimizerAPRStrategy, - OptimizerAPRStrategy__factory, + OptimizerAPRGreedyStrategy, + OptimizerAPRGreedyStrategy__factory, PoolManager, PoolManager__factory, } from '../../../typechain'; @@ -56,9 +56,9 @@ async function main() { const strategy = new ethers.Contract( strategyAddress, - OptimizerAPRStrategy__factory.createInterface(), + OptimizerAPRGreedyStrategy__factory.createInterface(), deployer, - ) as OptimizerAPRStrategy; + ) as OptimizerAPRGreedyStrategy; const poolManager = new ethers.Contract( poolManagerAddress, PoolManager__factory.createInterface(), diff --git a/scripts/mainnet-fork/genericLender/fraxConvexStaker.ts b/scripts/mainnet-fork/genericLender/fraxConvexStaker.ts index 4770cfa..d89f528 100644 --- a/scripts/mainnet-fork/genericLender/fraxConvexStaker.ts +++ b/scripts/mainnet-fork/genericLender/fraxConvexStaker.ts @@ -8,7 +8,7 @@ import { import { CONTRACTS_ADDRESSES, ChainId } from '@angleprotocol/sdk'; import { network, ethers } from 'hardhat'; import { parseUnits } from 'ethers/lib/utils'; -import { OptimizerAPRStrategy, OptimizerAPRStrategy__factory } from '../../../typechain'; +import { OptimizerAPRGreedyStrategy, OptimizerAPRGreedyStrategy__factory } from '../../../typechain'; import { DAY } from '../../../test/hardhat/contants'; async function main() { @@ -40,9 +40,9 @@ async function main() { const strategy = new ethers.Contract( strategyAddress, - OptimizerAPRStrategy__factory.createInterface(), + OptimizerAPRGreedyStrategy__factory.createInterface(), deployer, - ) as OptimizerAPRStrategy; + ) as OptimizerAPRGreedyStrategy; const poolManager = new ethers.Contract( poolManagerAddress, PoolManager_Interface, diff --git a/scripts/mainnet-fork/genericLender/removeGenericCompound.ts b/scripts/mainnet-fork/genericLender/removeGenericCompound.ts index 1a1838a..2e9c47e 100644 --- a/scripts/mainnet-fork/genericLender/removeGenericCompound.ts +++ b/scripts/mainnet-fork/genericLender/removeGenericCompound.ts @@ -4,8 +4,8 @@ import { parseUnits } from 'ethers/lib/utils'; import { GenericCompoundUpgradeable, GenericCompoundUpgradeable__factory, - OptimizerAPRStrategy, - OptimizerAPRStrategy__factory, + OptimizerAPRGreedyStrategy, + OptimizerAPRGreedyStrategy__factory, } from '../../../typechain'; async function main() { @@ -37,9 +37,9 @@ async function main() { const strategy = new ethers.Contract( strategyAddress, - OptimizerAPRStrategy__factory.createInterface(), + OptimizerAPRGreedyStrategy__factory.createInterface(), deployer, - ) as OptimizerAPRStrategy; + ) as OptimizerAPRGreedyStrategy; const lenderCompound = new ethers.Contract( lenderAddress, diff --git a/scripts/mainnet-fork/harvest.ts b/scripts/mainnet-fork/harvest.ts index debc19b..a3342ed 100644 --- a/scripts/mainnet-fork/harvest.ts +++ b/scripts/mainnet-fork/harvest.ts @@ -8,7 +8,7 @@ import { import { CONTRACTS_ADDRESSES, ChainId } from '@angleprotocol/sdk'; import { network, ethers } from 'hardhat'; import { parseUnits } from 'ethers/lib/utils'; -import { ERC20, ERC20__factory, OptimizerAPRStrategy, OptimizerAPRStrategy__factory } from '../../typechain'; +import { ERC20, ERC20__factory, OptimizerAPRGreedyStrategy, OptimizerAPRGreedyStrategy__factory } from '../../typechain'; import { logBN } from '../../test/hardhat/utils-interaction'; async function main() { @@ -37,9 +37,9 @@ async function main() { const strategy = new ethers.Contract( strategyAddress, - OptimizerAPRStrategy__factory.createInterface(), + OptimizerAPRGreedyStrategy__factory.createInterface(), deployer, - ) as OptimizerAPRStrategy; + ) as OptimizerAPRGreedyStrategy; const poolManager = new ethers.Contract( poolManagerAddress, PoolManager_Interface, diff --git a/scripts/mainnet-fork/upgradeFraxAaveLender.ts b/scripts/mainnet-fork/upgradeFraxAaveLender.ts index c610471..5b0fc2e 100644 --- a/scripts/mainnet-fork/upgradeFraxAaveLender.ts +++ b/scripts/mainnet-fork/upgradeFraxAaveLender.ts @@ -20,7 +20,7 @@ import { randomWithdraw, wait, } from '../../test/hardhat/utils-interaction'; -import { OptimizerAPRStrategy, OptimizerAPRStrategy__factory } from '../../typechain'; +import { OptimizerAPRGreedyStrategy, OptimizerAPRGreedyStrategy__factory } from '../../typechain'; import { time } from '../../test/hardhat/test-utils/helpers'; import { DAY } from '../../test/hardhat/contants'; @@ -67,9 +67,9 @@ async function main() { ) as PerpetualManagerFront; const strategy = new ethers.Contract( strategyAddress, - OptimizerAPRStrategy__factory.createInterface(), + OptimizerAPRGreedyStrategy__factory.createInterface(), deployer, - ) as OptimizerAPRStrategy; + ) as OptimizerAPRGreedyStrategy; /* const oldLender = new ethers.Contract( oldLenderAddress, diff --git a/test/foundry/BaseTest.test.sol b/test/foundry/BaseTest.test.sol index 5c41462..c53f6eb 100644 --- a/test/foundry/BaseTest.test.sol +++ b/test/foundry/BaseTest.test.sol @@ -15,6 +15,8 @@ contract BaseTest is Test { address internal constant _KEEPER = address(uint160(uint256(keccak256(abi.encodePacked("_keeper"))))); address internal constant _ANGLE = 0x31429d1856aD1377A8A0079410B297e1a9e214c2; address internal constant _GOVERNOR_POLYGON = 0xdA2D2f638D6fcbE306236583845e5822554c02EA; + address internal constant _1INCH_V5 = 0x1111111254EEB25477B68fb85Ed929f73A960582; + address internal constant _1INCH_V4 = 0x1111111254fb6c44bAC0beD2854e76F90643097d; address internal constant _ALICE = address(uint160(uint256(keccak256(abi.encodePacked("_alice"))))); address internal constant _BOB = address(uint160(uint256(keccak256(abi.encodePacked("_bob"))))); diff --git a/test/foundry/DebugTest.test.sol b/test/foundry/DebugTest.test.sol new file mode 100644 index 0000000..ffd690b --- /dev/null +++ b/test/foundry/DebugTest.test.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "./BaseTest.test.sol"; +import { IEuler, IEulerMarkets, IEulerEToken, IEulerDToken } from "../../contracts/interfaces/external/euler/IEuler.sol"; +import { IReserveInterestRateStrategy } from "../../contracts/interfaces/external/aave/IAave.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IBaseIRM { + function baseRate() external view returns (uint256); + + function kink() external view returns (uint256); + + function slope1() external view returns (uint256); + + function slope2() external view returns (uint256); +} + +contract DebugTest is BaseTest { + using stdStorage for StdStorage; + + function setUp() public override { + _ethereum = vm.createFork(vm.envString("ETH_NODE_URI_ETH_FOUNDRY")); + vm.selectFork(_ethereum); + + super.setUp(); + } + + // ================================== DEPOSIT ================================== + + function testBorrow() public { + address payable sender = payable(0x120afC8541F58cf78bE57553Adabe73CA0ee4B5d); + IERC20 asset = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + address _contract = 0x4579709627CA36BCe92f51ac975746f431890930; + bytes + memory data = hex"848c48da000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000e1a6d84604c5b17f5fd1fccba4c385a8b9670266000000000000000000000000000000000000000000000000000000174876e8000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000c68421f20bf6f0eb475f00b9c5484f7d0ac0331e0000000000000000000000000652b4b3d205300f9848f0431296d67ca4397f3b000000000000000000000000e1a6d84604c5b17f5fd1fccba4c385a8b9670266000000000000000000000000e1a6d84604c5b17f5fd1fccba4c385a8b967026600000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000002e000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000120afc8541f58cf78be57553adabe73ca0ee4b5d00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004380df26d30c14b5d3e000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002b19210715ad62a3c0010000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000005600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000e1a6d84604c5b17f5fd1fccba4c385a8b9670266000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000004e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000004c00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004200000000000000000000000001a7e4e63778b4f12a199c062f3efdd288afcbce8000000000000000000000000000000000000000000000000000000333227c0b10000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000038812aa3caf0000000000000000000000007122db0ebe4eb9b434a9f2ffe6760bc03bfbd0e00000000000000000000000001a7e4e63778b4f12a199c062f3efdd288afcbce8000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000007122db0ebe4eb9b434a9f2ffe6760bc03bfbd0e0000000000000000000000000e1a6d84604c5b17f5fd1fccba4c385a8b9670266000000000000000000000000000000000000000000002b19210715ad62a3c00000000000000000000000000000000000000000000000000000000033152e91fa000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e30000000000000000000000000000000000000000000001c500019700014d00a0c9e75c480000000000000000240e00000000000000000000000000000000000000000000000000011f0000d051005addc89785d75c86ab939e9e15bfbbb7fc086a871a7e4e63778b4f12a199c062f3efdd288afcbce800046d10421600000000000000000000000000000000000000000000000000000000000000000000000000000000000000007122db0ebe4eb9b434a9f2ffe6760bc03bfbd0e00000000000000000000000007122db0ebe4eb9b434a9f2ffe6760bc03bfbd0e0000000000000000000000000e9f183fc656656f1f17af1f2b0df79b8ff9ad8ed000000000000000000000000000000000000000000000000000000000000000102a000000000000000000000000000000000000000000000000000000024cc4c56bfee63c1e501735a26a57a0a0069dfabd41595a970faf5e1ee8b1a7e4e63778b4f12a199c062f3efdd288afcbce800a0f2fa6b66a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000003399465038000000000000000000000000004c687c80a06c4eca27a0b86991c6218b36c1d19d4a2e9eb0ce3606eb481111111254eeb25477b68fb85ed929f73a9605820000000000000000000000000000000000000000000000000000000000cfee7c08000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000c68421f20bf6f0eb475f00b9c5484f7d0ac0331e0000000000000000000000000652b4b3d205300f9848f0431296d67ca4397f3b000000000000000000000000120afc8541f58cf78be57553adabe73ca0ee4b5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000"; + vm.startPrank(sender); + asset.approve(_contract, type(uint256).max); + + (bool success, bytes memory result) = _contract.call(data); + vm.stopPrank(); + } + + // function testAave() public { + // IReserveInterestRateStrategy ir = IReserveInterestRateStrategy(0x27182842E098f60e3D576794A5bFFb0777E025d3); + // console.log("baseRate ", ir.baseVariableBorrowRate()); + // // console.log("kink ", ir.kink()); + // console.log("slope1 ", ir.variableRateSlope1()); + // console.log("slope2 ", ir.variableRateSlope1()); + // } +} diff --git a/test/foundry/optimizerAPR/OptimizerAPRStrategyForkTest.test.sol b/test/foundry/optimizerAPR/OptimizerAPRStrategyForkTest.test.sol new file mode 100644 index 0000000..36826a5 --- /dev/null +++ b/test/foundry/optimizerAPR/OptimizerAPRStrategyForkTest.test.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "../BaseTest.test.sol"; +import { CErc20I, CTokenI } from "../../../contracts/interfaces/external/compound/CErc20I.sol"; +import { IComptroller } from "../../../contracts/interfaces/external/compound/IComptroller.sol"; +import { PoolManager, IStrategy } from "../../../contracts/mock/MockPoolManager2.sol"; +import { OptimizerAPRStrategy } from "../../../contracts/strategies/OptimizerAPR/OptimizerAPRStrategy.sol"; +import { OptimizerAPRGreedyStrategy } from "../../../contracts/strategies/OptimizerAPR/OptimizerAPRGreedyStrategy.sol"; +import { GenericAaveNoStaker, IERC20, IERC20Metadata, IGenericLender } from "../../../contracts/strategies/OptimizerAPR/genericLender/aave/GenericAaveNoStaker.sol"; +import { GenericCompoundUpgradeable } from "../../../contracts/strategies/OptimizerAPR/genericLender/compound/GenericCompoundUpgradeable.sol"; +import { GenericEulerStaker, IEulerStakingRewards, IEuler, IEulerEToken, IEulerDToken, IGenericLender, AggregatorV3Interface, IUniswapV3Pool } from "../../../contracts/strategies/OptimizerAPR/genericLender/euler/GenericEulerStaker.sol"; + +contract OptimizerAPRStrategyForkTest is BaseTest { + using stdStorage for StdStorage; + + uint256 internal constant _BASE_TOKEN = 10**18; + uint256 internal constant _BASE_APR = 10**18; + uint64 internal constant _BPS = 10**4; + IERC20 public token = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + PoolManager public manager = PoolManager(0xe9f183FC656656f1F17af1F2b0dF79b8fF9ad8eD); + OptimizerAPRGreedyStrategy internal _oldStrat = + OptimizerAPRGreedyStrategy(0x5fE0E497Ac676d8bA78598FC8016EBC1E6cE14a3); + GenericAaveNoStaker internal _oldLenderAave = GenericAaveNoStaker(0xbe67bb1aa7baCFC5D40d963D47E11e3d382a56Bd); + GenericCompoundUpgradeable internal _oldLenderCompound = + GenericCompoundUpgradeable(payable(0x6D7cCd6d3E4948579891f90e98C1bb09a8c677ea)); + + IComptroller internal constant _COMPTROLLER = IComptroller(0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B); + IERC20 internal constant _COMP = IERC20(0xc00e94Cb662C3520282E6f5717214004A7f26888); + CErc20I internal _cUSDC = CErc20I(0x39AA39c021dfbaE8faC545936693aC917d5E7563); + // solhint-disable-next-line + IERC20 private constant _aave = IERC20(0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9); + // solhint-disable-next-line + IERC20 private constant _stkAave = IERC20(0x4da27a545c0c5B758a6BA100e3a049001de870f5); + IEulerStakingRewards internal constant _STAKER = IEulerStakingRewards(0xE5aFE81e63f0A52a3a03B922b30f73B8ce74D570); + IEuler private constant _EULER = IEuler(0x27182842E098f60e3D576794A5bFFb0777E025d3); + IEulerEToken internal constant _EUSDC = IEulerEToken(0xEb91861f8A4e1C12333F42DCE8fB0Ecdc28dA716); + IEulerDToken internal constant _DUSDC = IEulerDToken(0x84721A3dB22EB852233AEAE74f9bC8477F8bcc42); + IUniswapV3Pool private constant _POOL = IUniswapV3Pool(0xB003DF4B243f938132e8CAdBEB237AbC5A889FB4); + uint8 private constant _IS_UNI_MULTIPLIED = 0; + AggregatorV3Interface private constant _CHAINLINK = + AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419); + uint256 internal constant _PROP_INVESTED = 95 * 10**7; + + uint256 public maxTokenAmount; + uint256 public minTokenAmount; + uint256 public marginAmount; + uint8 internal _decimalToken; + OptimizerAPRStrategy public stratImplementation; + OptimizerAPRStrategy public strat; + GenericCompoundUpgradeable public lenderCompoundImplementation; + GenericCompoundUpgradeable public lenderCompound; + GenericAaveNoStaker public lenderAaveImplementation; + GenericAaveNoStaker public lenderAave; + GenericEulerStaker public lenderEulerImplementation; + GenericEulerStaker public lenderEuler; + + uint256 public constant BACKTEST_LENGTH = 30; + uint256 public constant IMPROVE_LENGTH = 2; + + function setUp() public override { + super.setUp(); + + _ethereum = vm.createFork(vm.envString("ETH_NODE_URI_ETH_FOUNDRY"), 16420445); + vm.selectFork(_ethereum); + + _decimalToken = IERC20Metadata(address(token)).decimals(); + maxTokenAmount = 10**(_decimalToken + 6); + minTokenAmount = 10**(_decimalToken - 1); + marginAmount = 10**(_decimalToken + 1); + + address[] memory keeperList = new address[](1); + address[] memory governorList = new address[](1); + keeperList[0] = _KEEPER; + governorList[0] = _GOVERNOR; + + stratImplementation = new OptimizerAPRStrategy(); + strat = OptimizerAPRStrategy( + deployUpgradeable( + address(stratImplementation), + abi.encodeWithSelector(strat.initialize.selector, address(manager), _GOVERNOR, _GUARDIAN, keeperList) + ) + ); + + lenderCompoundImplementation = new GenericCompoundUpgradeable(); + lenderCompound = GenericCompoundUpgradeable( + payable( + deployUpgradeable( + address(lenderCompoundImplementation), + abi.encodeWithSelector( + lenderCompoundImplementation.initialize.selector, + address(strat), + "lender Compound", + address(_cUSDC), + governorList, + _GUARDIAN, + keeperList, + _1INCH_V5 + ) + ) + ) + ); + lenderAaveImplementation = new GenericAaveNoStaker(); + lenderAave = GenericAaveNoStaker( + deployUpgradeable( + address(lenderAaveImplementation), + abi.encodeWithSelector( + lenderAaveImplementation.initialize.selector, + address(strat), + "lender Aave", + false, + governorList, + _GUARDIAN, + keeperList, + _1INCH_V5 + ) + ) + ); + lenderEulerImplementation = new GenericEulerStaker(); + lenderEuler = GenericEulerStaker( + deployUpgradeable( + address(lenderEulerImplementation), + abi.encodeWithSelector( + lenderEulerImplementation.initialize.selector, + address(strat), + "lender Euler", + governorList, + _GUARDIAN, + keeperList, + _1INCH_V5, + _STAKER, + _CHAINLINK, + _POOL, + _IS_UNI_MULTIPLIED + ) + ) + ); + + vm.startPrank(_GOVERNOR); + strat.addLender(IGenericLender(address(lenderCompound))); + strat.addLender(IGenericLender(address(lenderAave))); + strat.addLender(IGenericLender(address(lenderEuler))); + manager.updateStrategyDebtRatio(address(_oldStrat), 0); + manager.addStrategy(address(strat), _PROP_INVESTED); + vm.stopPrank(); + } + + // =============================== MIGRATE FUNDS =============================== + + function testMigrationFundsSuccess() public { + { + // do a claimComp first and sell the rewards + address[] memory holders = new address[](1); + CTokenI[] memory cTokens = new CTokenI[](1); + holders[0] = address(_oldLenderCompound); + cTokens[0] = CTokenI(address(_cUSDC)); + _COMPTROLLER.claimComp(holders, cTokens, true, true); + uint256 compReward = _COMP.balanceOf(address(_oldLenderCompound)); + console.log("compReward ", compReward); + vm.prank(0xcC617C6f9725eACC993ac626C7efC6B96476916E); + // TODO when selling simulate back at current block how many rewards we received + _oldLenderCompound.sellRewards( + 0, + hex"7c02520000000000000000000000000053222470cdcfb8081c0e3a50fd106f0d69e63f2000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000180000000000000000000000000c00e94cb662c3520282e6f5717214004a7f26888000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000053222470cdcfb8081c0e3a50fd106f0d69e63f200000000000000000000000006d7ccd6d3e4948579891f90e98c1bb09a8c677ea000000000000000000000000000000000000000000000003fd86cc0b67bf0e1000000000000000000000000000000000000000000000000000000000e14eae3900000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003f90000000000000000000000000000000000000000000000000003bb00038d00a0860a32ec000000000000000000000000000000000000000000000003fd86cc0b67bf0e100003645520080bf510fcbf18b91105470639e9561022937712c00e94cb662c3520282e6f5717214004a7f2688895e6f48254609a6ee006f7d493c8e5fb97094cef0024b4be83d50000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000056178a0d5f301baf6cf3e1cd53d9863437345bf900000000000000000000000053222470cdcfb8081c0e3a50fd106f0d69e63f2000000000000000000000000055662e225a3376759c24331a9aed764f8f0c9fbb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e3848506000000000000000000000000000000000000000000000003fd86cc0b67bf0e10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000063c5773401ffffffffffffffffffffffffffffffffffffff3862771d63c576bc00000026000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c00e94cb662c3520282e6f5717214004a7f268880000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000421bae301d9fe2af6c418cfff3f37a9cd0d52af754376421faedd5754993b730b6ab563cbb70cd4b6dde036dca82d2bab83eecc82dd700d5e256d65d008f937fe7ed0300000000000000000000000000000000000000000000000000000000000080a06c4eca27a0b86991c6218b36c1d19d4a2e9eb0ce3606eb481111111254fb6c44bac0bed2854e76f90643097d000000000000000000000000000000000000000000000003fd86cc0b67bf0e1000000000000000cfee7c08" + ); + + // do a claimRewards first and sell the rewards + vm.prank(0xcC617C6f9725eACC993ac626C7efC6B96476916E); + _oldLenderAave.claimRewards(); + // there shouldn't be any + uint256 stkAaveOldLender = _stkAave.balanceOf(address(_oldLenderAave)); + uint256 aaveOldLender = _aave.balanceOf(address(_oldLenderAave)); + assertEq(stkAaveOldLender, 0); + assertEq(aaveOldLender, 0); + } + // Update the rate so that we have the true rate and we don't underestimate the rate on chain + _cUSDC.accrueInterest(); + // remove funds from previous strat + vm.startPrank(_GOVERNOR); + // It would have been more efficient but it doesn't account for profits + // _oldStrat.safeRemoveLender(address(_oldLenderAave)); + // _oldStrat.forceRemoveLender(address(_oldLenderCompound)); + _oldStrat.harvest(); + manager.withdrawFromStrategy(IStrategy(address(_oldStrat)), token.balanceOf(address(_oldStrat))); + vm.stopPrank(); + + // There shouldn't be any funds left on the old strat + assertEq(token.balanceOf(address(_oldLenderCompound)), 0); + assertApproxEqAbs(_cUSDC.balanceOf(address(_oldLenderCompound)), 0, 10**_decimalToken); + assertEq(_oldLenderCompound.nav(), 0); + assertEq(token.balanceOf(address(_oldLenderAave)), 0); + assertEq(_oldLenderAave.nav(), 0); + assertEq(token.balanceOf(address(_oldStrat)), 0); + assertEq(_oldStrat.estimatedTotalAssets(), 0); + assertEq(_oldStrat.lentTotalAssets(), 0); + + // Then we add the new strategy + uint64[] memory lenderShares = new uint64[](3); + lenderShares[0] = (_BPS * 2) / 5; + lenderShares[2] = (_BPS * 3) / 5; + strat.harvest(abi.encode(lenderShares)); + uint256 totalAssetsInvested = (manager.getTotalAsset() * _PROP_INVESTED) / 10**9; + assertApproxEqAbs(lenderCompound.nav(), (totalAssetsInvested * lenderShares[0]) / _BPS, marginAmount); + assertApproxEqAbs(lenderEuler.nav(), (totalAssetsInvested * lenderShares[2]) / _BPS, marginAmount); + assertApproxEqAbs(lenderAave.nav(), (totalAssetsInvested * lenderShares[1]) / _BPS, marginAmount); + assertApproxEqAbs(strat.estimatedTotalAssets(), totalAssetsInvested, marginAmount); + + console.log("strat apr ", strat.estimatedAPR()); + console.log("compound apr ", lenderCompound.apr()); + console.log("aave apr ", lenderAave.apr()); + console.log("euler apr ", lenderEuler.apr()); + } +} diff --git a/test/foundry/optimizerAPR/OptimizerAPRStrategyTest.test.sol b/test/foundry/optimizerAPR/OptimizerAPRStrategyTest.test.sol new file mode 100644 index 0000000..d95ba1f --- /dev/null +++ b/test/foundry/optimizerAPR/OptimizerAPRStrategyTest.test.sol @@ -0,0 +1,677 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "../BaseTest.test.sol"; +import { PoolManager } from "../../../contracts/mock/MockPoolManager2.sol"; +import { OptimizerAPRStrategy } from "../../../contracts/strategies/OptimizerAPR/OptimizerAPRStrategy.sol"; +import { MockLender, IERC20, IGenericLender } from "../../../contracts/mock/MockLender.sol"; +import { MockToken } from "../../../contracts/mock/MockToken.sol"; +import "../../../contracts/utils/Errors.sol"; + +contract OptimizerAPRStrategyTest is BaseTest { + using stdStorage for StdStorage; + + address internal _hacker = address(uint160(uint256(keccak256(abi.encodePacked("hacker"))))); + + uint256 internal constant _BASE_TOKEN = 10**18; + uint256 internal constant _BASE_APR = 10**18; + uint64 internal constant _BPS = 10**4; + uint8 internal constant _DECIMAL_TOKEN = 6; + MockToken public token; + PoolManager public manager; + OptimizerAPRStrategy public stratImplementation; + OptimizerAPRStrategy public strat; + MockLender public lenderImplementation; + MockLender public lender1; + MockLender public lender2; + MockLender public lender3; + // just to test the old features + PoolManager public managerTmp; + OptimizerAPRStrategy public stratTmp; + MockLender public lenderTmp1; + MockLender public lenderTmp2; + MockLender public lenderTmp3; + uint256 public maxTokenAmount = 10**(_DECIMAL_TOKEN + 6); + uint256 public minTokenAmount = 10**(_DECIMAL_TOKEN - 1); + + uint256 public constant BACKTEST_LENGTH = 30; + uint256 public constant IMPROVE_LENGTH = 2; + + function setUp() public override { + super.setUp(); + + address[] memory keeperList = new address[](1); + address[] memory governorList = new address[](1); + keeperList[0] = _KEEPER; + governorList[0] = _GOVERNOR; + + token = new MockToken("token", "token", _DECIMAL_TOKEN); + manager = new PoolManager(address(token), _GOVERNOR, _GUARDIAN); + stratImplementation = new OptimizerAPRStrategy(); + strat = OptimizerAPRStrategy( + deployUpgradeable( + address(stratImplementation), + abi.encodeWithSelector(strat.initialize.selector, address(manager), _GOVERNOR, _GUARDIAN, keeperList) + ) + ); + vm.prank(_GOVERNOR); + manager.addStrategy(address(strat), 10**9); + + lenderImplementation = new MockLender(); + lender1 = MockLender( + deployUpgradeable( + address(lenderImplementation), + abi.encodeWithSelector( + lender1.initialize.selector, + address(strat), + "lender 1", + governorList, + _GUARDIAN, + keeperList, + _1INCH_V5, + _BPS + ) + ) + ); + lender2 = MockLender( + deployUpgradeable( + address(lenderImplementation), + abi.encodeWithSelector( + lender1.initialize.selector, + address(strat), + "lender 2", + governorList, + _GUARDIAN, + keeperList, + _1INCH_V5, + _BPS + ) + ) + ); + lender3 = MockLender( + deployUpgradeable( + address(lenderImplementation), + abi.encodeWithSelector( + lender1.initialize.selector, + address(strat), + "lender 3", + governorList, + _GUARDIAN, + keeperList, + _1INCH_V5, + _BPS + ) + ) + ); + + vm.startPrank(_GOVERNOR); + strat.addLender(IGenericLender(address(lender1))); + strat.addLender(IGenericLender(address(lender2))); + strat.addLender(IGenericLender(address(lender3))); + + managerTmp = new PoolManager(address(token), _GOVERNOR, _GUARDIAN); + stratTmp = OptimizerAPRStrategy( + deployUpgradeable( + address(stratImplementation), + abi.encodeWithSelector(strat.initialize.selector, address(managerTmp), _GOVERNOR, _GUARDIAN, keeperList) + ) + ); + + managerTmp.addStrategy(address(stratTmp), 10**9); + lenderTmp1 = MockLender( + deployUpgradeable( + address(lenderImplementation), + abi.encodeWithSelector( + lenderTmp1.initialize.selector, + address(stratTmp), + "lendertmp 1", + governorList, + _GUARDIAN, + keeperList, + _1INCH_V5, + _BPS + ) + ) + ); + lenderTmp2 = MockLender( + deployUpgradeable( + address(lenderImplementation), + abi.encodeWithSelector( + lenderTmp2.initialize.selector, + address(stratTmp), + "lenderTmp 2", + governorList, + _GUARDIAN, + keeperList, + _1INCH_V5, + _BPS + ) + ) + ); + lenderTmp3 = MockLender( + deployUpgradeable( + address(lenderImplementation), + abi.encodeWithSelector( + lenderTmp3.initialize.selector, + address(stratTmp), + "lenderTmp 3", + governorList, + _GUARDIAN, + keeperList, + _1INCH_V5, + _BPS + ) + ) + ); + stratTmp.addLender(IGenericLender(address(lenderTmp1))); + stratTmp.addLender(IGenericLender(address(lenderTmp2))); + stratTmp.addLender(IGenericLender(address(lenderTmp3))); + vm.stopPrank(); + } + + // ======================= BACKTEST PREVIOUS OPTIMIZERAPR ====================== + function testStabilityPreviouOptimizerSuccess( + uint256[BACKTEST_LENGTH] memory amounts, + uint256[BACKTEST_LENGTH] memory isWithdraw, + uint32[3 * 3 * BACKTEST_LENGTH] memory paramsLender, + uint64[BACKTEST_LENGTH] memory elapseTime + ) public { + for (uint256 i = 0; i < amounts.length; ++i) { + MockLender[3] memory listLender = [lender1, lender2, lender3]; + MockLender[3] memory listLenderTmp = [lenderTmp1, lenderTmp2, lenderTmp3]; + for (uint256 k = 0; k < listLender.length; ++k) { + listLender[k].setLenderPoolVariables( + paramsLender[i * 9 + k * 3], + paramsLender[i * 9 + k * 3 + 1], + paramsLender[i * 9 + k * 3 + 2], + 0 + ); + listLenderTmp[k].setLenderPoolVariables( + paramsLender[i * 9 + k * 3], + paramsLender[i * 9 + k * 3 + 1], + paramsLender[i * 9 + k * 3 + 2], + 0 + ); + } + if ( + (isWithdraw[i] == 1 && lender1.nav() == 0) || + (isWithdraw[i] == 2 && lender2.nav() == 0) || + (isWithdraw[i] == 3 && lender3.nav() == 0) + ) isWithdraw[i] = 0; + if (isWithdraw[i] == 0) { + uint256 amount = bound(amounts[i], minTokenAmount, maxTokenAmount); + token.mint(address(manager), amount); + token.mint(address(managerTmp), amount); + } else if (isWithdraw[i] == 1) { + uint256 amount = bound(amounts[i], 0, _BASE_TOKEN); + uint256 toBurn = (amount * lender1.nav()) / _BASE_TOKEN; + token.burn(address(lender1), toBurn); + token.burn(address(lenderTmp1), toBurn); + } else if (isWithdraw[i] == 2) { + uint256 amount = bound(amounts[i], 0, _BASE_TOKEN); + uint256 toBurn = (amount * lender2.nav()) / _BASE_TOKEN; + token.burn(address(lender2), toBurn); + token.burn(address(lenderTmp2), toBurn); + } else if (isWithdraw[i] == 3) { + uint256 amount = bound(amounts[i], 0, _BASE_TOKEN); + uint256 toBurn = (amount * lender3.nav()) / _BASE_TOKEN; + token.burn(address(lender3), toBurn); + token.burn(address(lenderTmp3), toBurn); + } + strat.harvest(); + stratTmp.harvest(); + // advance in time for rewards to be taken into account + elapseTime[i] = uint64(bound(elapseTime[i], 1, 86400 * 7)); + elapseTime[i] = 86400 * 14; + vm.warp(block.timestamp + elapseTime[i]); + { + assertEq(lender1.nav(), lenderTmp1.nav()); + assertEq(lender2.nav(), lenderTmp2.nav()); + assertEq(lender3.nav(), lenderTmp3.nav()); + assertEq(lender1.apr(), lenderTmp1.apr()); + assertEq(lender2.apr(), lenderTmp2.apr()); + assertEq(lender3.apr(), lenderTmp3.apr()); + assertEq(strat.estimatedTotalAssets(), stratTmp.estimatedTotalAssets()); + assertEq(strat.estimatedAPR(), stratTmp.estimatedAPR()); + } + } + } + + // // ================================== DEPOSIT ================================== + + function testDepositInvalidLength() public { + uint256 amount = maxTokenAmount; + + lender1.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, maxTokenAmount, 0); + lender2.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, maxTokenAmount, 0); + lender3.setLenderPoolVariables(0, _BASE_APR / 2, maxTokenAmount, 0); + + token.mint(address(manager), 2 * amount); + uint64[] memory lenderShares = new uint64[](2); + lenderShares[0] = _BPS / 2; + lenderShares[1] = _BPS / 2; + vm.expectRevert(IncorrectListLength.selector); + strat.harvest(abi.encode(lenderShares)); + } + + function testDepositWrongAddsUpHint() public { + uint256 amount = maxTokenAmount; + + lender1.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, maxTokenAmount, 0); + lender2.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, maxTokenAmount, 0); + lender3.setLenderPoolVariables(0, _BASE_APR / 2, maxTokenAmount, 0); + + token.mint(address(manager), 2 * amount); + uint64[] memory lenderShares = new uint64[](3); + lenderShares[0] = _BPS / 2; + lenderShares[1] = _BPS / 3; + lenderShares[2] = 0; + vm.expectRevert(InvalidShares.selector); + strat.harvest(abi.encode(lenderShares)); + } + + function testDepositNoFundsWithHintSuccess(uint256 borrow) public { + // MockLender[3] memory listLender = [lender1, lender2, lender3]; + lender1.setLenderPoolVariables(0, _BASE_APR, borrow, 0); + lender2.setLenderPoolVariables(0, _BASE_APR / 2, borrow, 0); + lender3.setLenderPoolVariables(0, _BASE_APR / 2, borrow, 0); + + uint64[] memory lenderShares = new uint64[](3); + lenderShares[0] = _BPS / 2; + lenderShares[1] = _BPS / 2; + lenderShares[2] = 0; + strat.harvest(abi.encode(lenderShares)); + { + assertEq(lender1.nav(), 0); + assertEq(lender2.nav(), 0); + assertEq(lender3.nav(), 0); + assertEq(lender1.apr(), 0); + assertEq(lender2.apr(), 0); + assertEq(lender3.apr(), 0); + assertEq(strat.estimatedTotalAssets(), 0); + assertEq(strat.estimatedAPR(), 0); + } + } + + function testDepositAllInOneSuccess(uint256 amount, uint256[3] memory borrows) public { + amount = bound(amount, 1, maxTokenAmount); + borrows[0] = bound(borrows[0], 0, amount); + borrows[1] = bound(borrows[1], 0, amount); + borrows[2] = bound(borrows[2], 0, amount); + + // MockLender[3] memory listLender = [lender1, lender2, lender3]; + lender1.setLenderPoolVariables(0, _BASE_APR, borrows[0], 0); + lender2.setLenderPoolVariables(0, _BASE_APR / 2, borrows[0], 0); + lender3.setLenderPoolVariables(0, _BASE_APR / 2, borrows[0], 0); + + token.mint(address(manager), amount); + uint64[] memory lenderShares = new uint64[](3); + lenderShares[0] = _BPS; + lenderShares[1] = 0; + lenderShares[2] = 0; + strat.harvest(abi.encode(lenderShares)); + { + uint256 estimatedAPR = _computeAPY(amount, borrows[0], 0, _BASE_APR, 0); + assertEq(lender1.nav(), amount); + assertEq(lender2.nav(), 0); + assertEq(lender3.nav(), 0); + assertEq(lender1.apr(), estimatedAPR); + assertEq(lender2.apr(), 0); + assertEq(lender3.apr(), 0); + assertEq(strat.estimatedTotalAssets(), amount); + assertEq(strat.estimatedAPR(), estimatedAPR); + } + } + + function testDepositAllSplitIn2Success(uint256 amount, uint256[3] memory borrows) public { + amount = bound(amount, 1, maxTokenAmount); + borrows[0] = bound(borrows[0], 1, amount); + borrows[1] = bound(borrows[1], 1, amount); + borrows[2] = bound(borrows[2], 1, amount); + + // MockLender[3] memory listLender = [lender1, lender2, lender3]; + lender1.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[0], 0); + lender2.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[0], 0); + lender3.setLenderPoolVariables(0, _BASE_APR / 2, borrows[0], 0); + + token.mint(address(manager), 2 * amount); + uint64[] memory lenderShares = new uint64[](3); + lenderShares[0] = _BPS / 2; + lenderShares[1] = _BPS / 2; + lenderShares[2] = 0; + strat.harvest(abi.encode(lenderShares)); + { + uint256 estimatedAPR = _computeAPY(amount, borrows[0], _BASE_APR / 100, _BASE_APR, 0); + assertEq(lender1.nav(), amount); + assertEq(lender2.nav(), amount); + assertEq(lender3.nav(), 0); + assertEq(lender1.apr(), estimatedAPR); + assertEq(lender2.apr(), estimatedAPR); + assertEq(lender3.apr(), 0); + assertEq(strat.estimatedTotalAssets(), 2 * amount); + assertEq(strat.estimatedAPR(), estimatedAPR); + } + } + + function testDepositAllSplitIn3Success(uint256 amount, uint256[3] memory borrows) public { + amount = bound(amount, 1, maxTokenAmount); + borrows[0] = bound(borrows[0], 1, amount); + borrows[1] = bound(borrows[1], 1, amount); + borrows[2] = bound(borrows[2], 1, amount); + + // MockLender[3] memory listLender = [lender1, lender2, lender3]; + lender1.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[0], 0); + lender2.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[0], 0); + lender3.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[0], 0); + + token.mint(address(manager), 4 * amount); + uint64[] memory lenderShares = new uint64[](3); + lenderShares[0] = _BPS / 2; + lenderShares[1] = _BPS / 4; + lenderShares[2] = _BPS / 4; + strat.harvest(abi.encode(lenderShares)); + { + uint256 estimatedAPRHalf = _computeAPY(2 * amount, borrows[0], _BASE_APR / 100, _BASE_APR, 0); + uint256 estimatedAPRFourth = _computeAPY(amount, borrows[0], _BASE_APR / 100, _BASE_APR, 0); + uint256 estimatedAPRGlobal = (estimatedAPRHalf + estimatedAPRFourth) / 2; + assertEq(lender1.nav(), 2 * amount); + assertEq(lender2.nav(), amount); + assertEq(lender3.nav(), amount); + assertEq(lender1.apr(), estimatedAPRHalf); + assertEq(lender2.apr(), estimatedAPRFourth); + assertEq(lender3.apr(), estimatedAPRFourth); + assertEq(strat.estimatedTotalAssets(), 4 * amount); + assertEq(strat.estimatedAPR(), estimatedAPRGlobal); + } + } + + function testDeposit2HopSuccess(uint256[3] memory amounts, uint256[3] memory borrows) public { + amounts[0] = bound(amounts[0], 1, maxTokenAmount); + amounts[1] = bound(amounts[1], 1, maxTokenAmount); + amounts[2] = bound(amounts[2], 1, maxTokenAmount); + uint256 sumAmounts = 2 * (amounts[0] + amounts[1] + amounts[2]); + + borrows[0] = bound(borrows[0], 1, amounts[0]); + borrows[1] = bound(borrows[1], 1, amounts[1]); + borrows[2] = sumAmounts; + + lender1.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[0], 0); + lender2.setLenderPoolVariables(0, 0, borrows[0], 0); + lender3.setLenderPoolVariables(0, 0, borrows[0], 0); + + token.mint(address(manager), 2 * amounts[0]); + strat.harvest(); + // to not withdraw what has been put on lender1 previously (because _potential is lower than highest) + lender3.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR + 1, borrows[0], 0); + token.mint(address(manager), 2 * amounts[1]); + strat.harvest(); + token.mint(address(manager), 2 * amounts[2]); + lender1.setLenderPoolVariables(0, 0, borrows[2], 0); + lender2.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[2], 0); + lender3.setLenderPoolVariables(0, 0, borrows[2], 0); + uint64[] memory lenderShares = new uint64[](3); + lenderShares[1] = _BPS; + strat.harvest(abi.encode(lenderShares)); + { + uint256 estimatedAPRHint = _computeAPY(sumAmounts, borrows[2], _BASE_APR / 100, _BASE_APR, 0); + assertEq(lender1.nav(), 0); + assertEq(lender2.nav(), sumAmounts); + assertEq(lender3.nav(), 0); + assertEq(lender1.apr(), 0); + assertEq(lender2.apr(), estimatedAPRHint); + assertEq(lender3.apr(), 0); + assertEq(strat.estimatedTotalAssets(), sumAmounts); + assertEq(strat.estimatedAPR(), estimatedAPRHint); + } + } + + function testDeposit2HopMultiSharesSuccess(uint256[3] memory amounts, uint256[3] memory borrows) public { + amounts[0] = bound(amounts[0], 1, maxTokenAmount); + amounts[1] = bound(amounts[1], 1, maxTokenAmount); + amounts[2] = bound(amounts[2], 1, maxTokenAmount); + // Because in this special case my best estimate won't be better than the greedy, because the distribution + // will be closer to te true optimum. This is just by chance for the greedy and the fuzzing is "searching for that chance" + uint256 sumAmounts = (amounts[0] + amounts[1] + amounts[2]); + if ((amounts[0] * _BPS) / sumAmounts > _BPS / 4 && (amounts[0] * _BPS) / sumAmounts < (_BPS * 44) / 100) return; + sumAmounts *= 4; + + borrows[0] = bound(borrows[0], 1, amounts[0]); + borrows[1] = bound(borrows[1], 1, amounts[1]); + borrows[2] = sumAmounts; + + lender1.setLenderPoolVariables(0, 0, borrows[0], 0); + lender2.setLenderPoolVariables(0, 0, borrows[0], 0); + lender3.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[0], 0); + + token.mint(address(manager), 4 * amounts[0]); + strat.harvest(); + // to not withdraw what has been put on lender3 previously (because _potential is lower than highest) + lender1.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR + 1, borrows[0], 0); + token.mint(address(manager), 4 * amounts[1]); + strat.harvest(); + token.mint(address(manager), 4 * amounts[2]); + lender1.setLenderPoolVariables(0, 0, borrows[2], 0); + lender2.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[2], sumAmounts); + lender3.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[2], 2 * sumAmounts); + uint64[] memory lenderShares = new uint64[](3); + lenderShares[1] = (_BPS * 3) / 4; + lenderShares[2] = _BPS / 4; + strat.harvest(abi.encode(lenderShares)); + { + uint256 estimatedAPRHintLender2 = _computeAPY( + (sumAmounts * lenderShares[1]) / _BPS, + borrows[2], + _BASE_APR / 100, + _BASE_APR, + sumAmounts + ); + uint256 estimatedAPRHintLender3 = _computeAPY( + (sumAmounts * lenderShares[2]) / _BPS, + borrows[2], + _BASE_APR / 100, + _BASE_APR, + 2 * sumAmounts + ); + uint256 estimatedAPRHint = (sumAmounts * + lenderShares[1] * + estimatedAPRHintLender2 + + sumAmounts * + lenderShares[2] * + estimatedAPRHintLender3) / (_BPS * sumAmounts); + assertEq(lender1.nav(), 0); + assertEq(lender2.nav(), (sumAmounts * lenderShares[1]) / _BPS); + assertEq(lender3.nav(), (sumAmounts * lenderShares[2]) / _BPS); + assertEq(lender1.apr(), 0); + assertEq(lender2.apr(), estimatedAPRHintLender2); + assertEq(lender3.apr(), estimatedAPRHintLender3); + assertEq(strat.estimatedTotalAssets(), sumAmounts); + assertEq(strat.estimatedAPR(), estimatedAPRHint); + } + // should stay the same + strat.harvest(); + { + uint256 estimatedAPRHintLender2 = _computeAPY( + (sumAmounts * lenderShares[1]) / _BPS, + borrows[2], + _BASE_APR / 100, + _BASE_APR, + sumAmounts + ); + uint256 estimatedAPRHintLender3 = _computeAPY( + (sumAmounts * lenderShares[2]) / _BPS, + borrows[2], + _BASE_APR / 100, + _BASE_APR, + 2 * sumAmounts + ); + uint256 estimatedAPRHint = (sumAmounts * + lenderShares[1] * + estimatedAPRHintLender2 + + sumAmounts * + lenderShares[2] * + estimatedAPRHintLender3) / (_BPS * sumAmounts); + assertEq(lender1.nav(), 0); + assertEq(lender2.nav(), (sumAmounts * lenderShares[1]) / _BPS); + assertEq(lender3.nav(), (sumAmounts * lenderShares[2]) / _BPS); + assertEq(lender1.apr(), 0); + assertEq(lender2.apr(), estimatedAPRHintLender2); + assertEq(lender3.apr(), estimatedAPRHintLender3); + assertEq(strat.estimatedTotalAssets(), sumAmounts); + assertEq(strat.estimatedAPR(), estimatedAPRHint); + } + } + + function testDeposit2HopMultiSharesRevertMissingLiquidity( + uint256[3] memory amounts, + uint256[3] memory borrows, + uint256 propWithdraw + ) public { + amounts[0] = bound(amounts[0], 2 * 1000 * 10**_DECIMAL_TOKEN, maxTokenAmount); + amounts[1] = bound(amounts[1], 2 * 1000 * 10**_DECIMAL_TOKEN, maxTokenAmount); + amounts[2] = bound(amounts[2], 2 * 1000 * 10**_DECIMAL_TOKEN, maxTokenAmount); + propWithdraw = bound(propWithdraw, 0, _BPS / 4); + // Because in this special case my best estimate won't be better than the greedy, because the distribution + // will be closer to te true optimum. This is just by chance for the greedy and the fuzzing is "searching for that chance" + uint256 sumAmounts = (amounts[0] + amounts[1] + amounts[2]); + if ((amounts[0] * _BPS) / sumAmounts > _BPS / 4 && (amounts[0] * _BPS) / sumAmounts < (_BPS * 44) / 100) return; + sumAmounts *= 4; + + borrows[0] = bound(borrows[0], 1, amounts[0]); + borrows[1] = bound(borrows[1], 1, amounts[1]); + borrows[2] = sumAmounts; + + lender1.setLenderPoolVariables(0, 0, borrows[0], 0); + lender2.setLenderPoolVariables(0, 0, borrows[0], 0); + lender3.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[0], 0); + + token.mint(address(manager), 4 * amounts[0]); + strat.harvest(); + // to not withdraw what has been put on lender3 previously (because _potential is lower than highest) + lender1.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR + 1, borrows[0], 0); + token.mint(address(manager), 4 * amounts[1]); + strat.harvest(); + token.mint(address(manager), 4 * amounts[2]); + lender1.setLenderPoolVariables(0, 0, borrows[2], 0); + lender2.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[2], sumAmounts); + lender3.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[2], 2 * sumAmounts); + //change liquidity on lender used + lender1.setPropWithdrawable(propWithdraw); + uint64[] memory lenderShares = new uint64[](3); + lenderShares[1] = (_BPS * 3) / 4; + lenderShares[2] = _BPS / 4; + vm.expectRevert(OptimizerAPRStrategy.IncorrectDistribution.selector); + strat.harvest(abi.encode(lenderShares)); + } + + function testHarvest2SharesWithLossSuccess(uint256[5] memory amounts, uint256[3] memory borrows) public { + amounts[0] = bound(amounts[0], 1, maxTokenAmount); + amounts[1] = bound(amounts[1], 1, maxTokenAmount); + amounts[2] = bound(amounts[2], 1, maxTokenAmount); + // Because in this special case my best estimate won't be better than the greedy, because the distribution + // will be closer to te true optimum. This is just by chance for the greedy and the fuzzing is "searching for that chance" + uint256 sumAmounts = (amounts[0] + amounts[1] + amounts[2]); + sumAmounts *= 4; + + borrows[0] = bound(borrows[0], 1, amounts[0]); + borrows[1] = bound(borrows[1], 1, amounts[1]); + borrows[2] = sumAmounts; + + lender1.setLenderPoolVariables(0, 0, borrows[0], 0); + lender2.setLenderPoolVariables(0, 0, borrows[0], 0); + lender3.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[0], 0); + + token.mint(address(manager), 4 * amounts[0]); + strat.harvest(); + // to not withdraw what has been put on lender3 previously (because _potential is lower than highest) + lender1.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR + 1, borrows[0], 0); + token.mint(address(manager), 4 * amounts[1]); + strat.harvest(); + { + int256 delta1 = _makeLossGainLender(lender1, amounts[3], 4); + int256 delta3 = _makeLossGainLender(lender3, amounts[4], 4); + sumAmounts = uint256(int256(sumAmounts) + delta1 + delta3); + + uint256 amountOnLender3AfterPrepareReturn = uint256(int256(4 * amounts[0]) + delta3); + uint256 toWithdraw = (delta1 + delta3 >= 0) ? uint256(delta1 + delta3) : uint256(-(delta1 + delta3)); + if (uint256(int256(4 * amounts[1]) + delta1) < toWithdraw) + amountOnLender3AfterPrepareReturn = uint256(int256(4 * amounts[0]) + delta3) > + (toWithdraw - uint256(int256(4 * amounts[1]) + delta1)) + ? uint256(int256(4 * amounts[0]) + delta3) - (toWithdraw - uint256(int256(4 * amounts[1]) + delta1)) + : 0; + + // Because in this special case my best estimate won't be better than the greedy, because the distribution + // will be closer to te true optimum. This is just by chance for the greedy and the fuzzing is "searching for that chance" + if ( + (amountOnLender3AfterPrepareReturn * _BPS) / sumAmounts > _BPS / 4 && + (amountOnLender3AfterPrepareReturn * _BPS) / sumAmounts < (_BPS * 44) / 100 + ) return; + } + token.mint(address(manager), 4 * amounts[2]); + lender1.setLenderPoolVariables(0, 0, borrows[2], 0); + lender2.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[2], sumAmounts); + lender3.setLenderPoolVariables(_BASE_APR / 100, _BASE_APR, borrows[2], 2 * sumAmounts); + uint64[] memory lenderShares = new uint64[](3); + lenderShares[1] = (_BPS * 3) / 4; + lenderShares[2] = _BPS / 4; + strat.harvest(abi.encode(lenderShares)); + { + uint256 estimatedAPRHintLender2 = _computeAPY( + (sumAmounts * lenderShares[1]) / _BPS, + borrows[2], + _BASE_APR / 100, + _BASE_APR, + sumAmounts + ); + uint256 estimatedAPRHintLender3 = _computeAPY( + (sumAmounts * lenderShares[2]) / _BPS, + borrows[2], + _BASE_APR / 100, + _BASE_APR, + 2 * sumAmounts + ); + uint256 estimatedAPRHint = (sumAmounts * + lenderShares[1] * + estimatedAPRHintLender2 + + sumAmounts * + lenderShares[2] * + estimatedAPRHintLender3) / (_BPS * sumAmounts); + assertEq(lender1.nav(), 0); + assertEq(lender2.nav(), (sumAmounts * lenderShares[1]) / _BPS); + assertEq(lender3.nav(), (sumAmounts * lenderShares[2]) / _BPS); + assertEq(lender1.apr(), 0); + assertEq(lender2.apr(), estimatedAPRHintLender2); + assertEq(lender3.apr(), estimatedAPRHintLender3); + assertEq(strat.estimatedTotalAssets(), sumAmounts); + assertEq(strat.estimatedAPR(), estimatedAPRHint); + } + } + + // ================================== INTERNAL ================================= + + function _computeAPY( + uint256 supply, + uint256 borrow, + uint256 r0, + uint256 slope1, + uint256 biasSupply + ) internal pure returns (uint256) { + return r0 + (slope1 * borrow) / (supply + biasSupply); + } + + function _makeLossGainLender( + MockLender lender, + uint256 amount, + uint256 multiplier + ) internal returns (int256 delta) { + amount = bound(amount, 0, 2 * _BASE_TOKEN); + if (amount <= _BASE_TOKEN) { + uint256 toBurn = multiplier * ((amount * lender.nav()) / (multiplier * _BASE_TOKEN)); + token.burn(address(lender), toBurn); + delta = -int256(toBurn); + } else { + uint256 toMint = multiplier * (((amount - _BASE_TOKEN) * lender.nav()) / (multiplier * _BASE_TOKEN)); + token.mint(address(lender), toMint); + delta = int256(toMint); + } + } +} diff --git a/test/foundry/optimizerAPR/euler/GenericEulerStakerTest.test.sol b/test/foundry/optimizerAPR/euler/GenericEulerStakerTest.test.sol index 296ddb6..b8fbfc2 100644 --- a/test/foundry/optimizerAPR/euler/GenericEulerStakerTest.test.sol +++ b/test/foundry/optimizerAPR/euler/GenericEulerStakerTest.test.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.17; import "../../BaseTest.test.sol"; import { OracleMath } from "../../../../contracts/utils/OracleMath.sol"; import { PoolManager } from "../../../../contracts/mock/MockPoolManager2.sol"; -import { OptimizerAPRStrategy } from "../../../../contracts/strategies/OptimizerAPR/OptimizerAPRStrategy.sol"; +import { OptimizerAPRGreedyStrategy } from "../../../../contracts/strategies/OptimizerAPR/OptimizerAPRGreedyStrategy.sol"; import { GenericEulerStaker, IERC20, IEulerStakingRewards, IEuler, IEulerEToken, IEulerDToken, IGenericLender, AggregatorV3Interface, IUniswapV3Pool } from "../../../../contracts/strategies/OptimizerAPR/genericLender/euler/GenericEulerStaker.sol"; interface IMinimalLiquidityGauge { @@ -36,8 +36,8 @@ contract GenericEulerStakerTest is BaseTest, OracleMath { uint256 internal constant _ONE_MINUS_RESERVE = 75 * 10**16; PoolManager public manager; - OptimizerAPRStrategy public stratImplementation; - OptimizerAPRStrategy public strat; + OptimizerAPRGreedyStrategy public stratImplementation; + OptimizerAPRGreedyStrategy public strat; GenericEulerStaker public lenderImplementation; GenericEulerStaker public lender; uint256 public maxTokenAmount = 10**(_DECIMAL_TOKEN + 6); @@ -58,8 +58,8 @@ contract GenericEulerStakerTest is BaseTest, OracleMath { governorList[0] = _GOVERNOR; manager = new PoolManager(address(_TOKEN), _GOVERNOR, _GUARDIAN); - stratImplementation = new OptimizerAPRStrategy(); - strat = OptimizerAPRStrategy( + stratImplementation = new OptimizerAPRGreedyStrategy(); + strat = OptimizerAPRGreedyStrategy( deployUpgradeable( address(stratImplementation), abi.encodeWithSelector(strat.initialize.selector, address(manager), _GOVERNOR, _GUARDIAN, keeperList) @@ -79,6 +79,7 @@ contract GenericEulerStakerTest is BaseTest, OracleMath { governorList, _GUARDIAN, keeperList, + _1INCH_V5, _STAKER, _CHAINLINK, _POOL, diff --git a/test/hardhat/optimizerApr/fraxConvexStaking.test.ts b/test/hardhat/optimizerApr/fraxConvexStaking.test.ts index 83b2dac..5bba982 100644 --- a/test/hardhat/optimizerApr/fraxConvexStaking.test.ts +++ b/test/hardhat/optimizerApr/fraxConvexStaking.test.ts @@ -17,8 +17,8 @@ import { IStakedAave__factory, MockToken, MockToken__factory, - OptimizerAPRStrategy, - OptimizerAPRStrategy__factory, + OptimizerAPRGreedyStrategy, + OptimizerAPRGreedyStrategy__factory, PoolManager, } from '../../../typechain'; import { gwei } from '../../../utils/bignumber'; @@ -33,9 +33,11 @@ async function initStrategy( keeper: SignerWithAddress, manager: PoolManager, ): Promise<{ - strategy: OptimizerAPRStrategy; + strategy: OptimizerAPRGreedyStrategy; }> { - const strategy = (await deployUpgradeable(new OptimizerAPRStrategy__factory(guardian))) as OptimizerAPRStrategy; + const strategy = (await deployUpgradeable( + new OptimizerAPRGreedyStrategy__factory(guardian), + )) as OptimizerAPRGreedyStrategy; await strategy.initialize(manager.address, governor.address, guardian.address, [keeper.address]); await manager.connect(governor).addStrategy(strategy.address, gwei('0.99999')); return { strategy }; @@ -45,7 +47,7 @@ async function initLenderAaveFraxStaker( governor: SignerWithAddress, guardian: SignerWithAddress, keeper: SignerWithAddress, - strategy: OptimizerAPRStrategy, + strategy: OptimizerAPRGreedyStrategy, name: string, isIncentivized: boolean, stakingPeriod: number, @@ -62,6 +64,7 @@ async function initLenderAaveFraxStaker( [governor.address], guardian.address, [keeper.address], + oneInch, stakingPeriod, ); await strategy.connect(governor).addLender(lender.address); @@ -69,7 +72,7 @@ async function initLenderAaveFraxStaker( } let governor: SignerWithAddress, guardian: SignerWithAddress, user: SignerWithAddress, keeper: SignerWithAddress; -let strategy: OptimizerAPRStrategy; +let strategy: OptimizerAPRGreedyStrategy; let token: ERC20; let aToken: ERC20; let nativeRewardToken: MockToken; @@ -141,6 +144,7 @@ describe('OptimizerAPR - lenderAaveFraxConvexStaker', () => { // IPoolRegistryFrax__factory.abi, // '0x41a5881c17185383e19Df6FA4EC158a6F4851A69', // )) as IPoolRegistryFrax; + oneInch = '0x1111111254EEB25477B68fb85Ed929f73A960582'; guardianError = `AccessControl: account ${user.address.toLowerCase()} is missing role ${guardianRole}`; keeperError = `AccessControl: account ${user.address.toLowerCase()} is missing role ${keeperRole}`; @@ -158,7 +162,6 @@ describe('OptimizerAPR - lenderAaveFraxConvexStaker', () => { true, DAY, )); - oneInch = '0x1111111254EEB25477B68fb85Ed929f73A960582'; amountStorage = ethers.utils.hexStripZeros(utils.parseEther('1').toHexString()); }); @@ -167,8 +170,9 @@ describe('OptimizerAPR - lenderAaveFraxConvexStaker', () => { const lender = (await deployUpgradeable( new GenericAaveFraxConvexStaker__factory(guardian), )) as GenericAaveFraxConvexStaker; + console.log("oneInch ", oneInch); await expect( - lender.initialize(strategy.address, 'test', true, [governor.address], guardian.address, [keeper.address], 0), + lender.initialize(strategy.address, 'test', true, [governor.address], guardian.address, [keeper.address],oneInch, 0), ).to.be.revertedWithCustomError(lender, 'TooSmallStakingPeriod'); await expect( lenderAave.initialize( @@ -178,6 +182,7 @@ describe('OptimizerAPR - lenderAaveFraxConvexStaker', () => { [governor.address], guardian.address, [keeper.address], + oneInch, 0, ), ).to.be.revertedWith('Initializable: contract is already initialized'); diff --git a/test/hardhat/optimizerApr/fraxStaking.test.ts b/test/hardhat/optimizerApr/fraxStaking.test.ts index 8a432d2..74f2eff 100644 --- a/test/hardhat/optimizerApr/fraxStaking.test.ts +++ b/test/hardhat/optimizerApr/fraxStaking.test.ts @@ -17,8 +17,8 @@ import { IStakedAave__factory, MockToken, MockToken__factory, - OptimizerAPRStrategy, - OptimizerAPRStrategy__factory, + OptimizerAPRGreedyStrategy, + OptimizerAPRGreedyStrategy__factory, PoolManager, } from '../../../typechain'; import { gwei } from '../../../utils/bignumber'; @@ -33,9 +33,11 @@ async function initStrategy( keeper: SignerWithAddress, manager: PoolManager, ): Promise<{ - strategy: OptimizerAPRStrategy; + strategy: OptimizerAPRGreedyStrategy; }> { - const strategy = (await deployUpgradeable(new OptimizerAPRStrategy__factory(guardian))) as OptimizerAPRStrategy; + const strategy = (await deployUpgradeable( + new OptimizerAPRGreedyStrategy__factory(guardian), + )) as OptimizerAPRGreedyStrategy; await strategy.initialize(manager.address, governor.address, guardian.address, [keeper.address]); await manager.connect(governor).addStrategy(strategy.address, gwei('0.99999')); return { strategy }; @@ -45,7 +47,7 @@ async function initLenderAaveFraxStaker( governor: SignerWithAddress, guardian: SignerWithAddress, keeper: SignerWithAddress, - strategy: OptimizerAPRStrategy, + strategy: OptimizerAPRGreedyStrategy, name: string, isIncentivized: boolean, stakingPeriod: number, @@ -60,6 +62,7 @@ async function initLenderAaveFraxStaker( [governor.address], guardian.address, [keeper.address], + oneInch, stakingPeriod, ); await strategy.connect(governor).addLender(lender.address); @@ -67,10 +70,9 @@ async function initLenderAaveFraxStaker( } let governor: SignerWithAddress, guardian: SignerWithAddress, user: SignerWithAddress, keeper: SignerWithAddress; -let strategy: OptimizerAPRStrategy; +let strategy: OptimizerAPRGreedyStrategy; let token: ERC20; let aToken: ERC20; -let frax: ERC20; let nativeRewardToken: MockToken; let tokenDecimal: number; let manager: PoolManager; @@ -109,7 +111,6 @@ describe('OptimizerAPR - lenderAaveFraxStaker', () => { token = (await ethers.getContractAt(ERC20__factory.abi, '0x853d955aCEf822Db058eb8505911ED77F175b99e')) as ERC20; aToken = (await ethers.getContractAt(ERC20__factory.abi, '0xd4937682df3C8aEF4FE912A96A74121C0829E664')) as ERC20; - frax = (await ethers.getContractAt(ERC20__factory.abi, '0x853d955aCEf822Db058eb8505911ED77F175b99e')) as ERC20; nativeRewardToken = (await ethers.getContractAt( MockToken__factory.abi, '0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0', @@ -136,6 +137,8 @@ describe('OptimizerAPR - lenderAaveFraxStaker', () => { AggregatorV3Interface__factory.abi, '0x547a514d5e3769680Ce22B2361c10Ea13619e8a9', )) as AggregatorV3Interface; + oneInch = '0x1111111254EEB25477B68fb85Ed929f73A960582'; + guardianError = `AccessControl: account ${user.address.toLowerCase()} is missing role ${guardianRole}`; keeperError = `AccessControl: account ${user.address.toLowerCase()} is missing role ${keeperRole}`; @@ -153,7 +156,6 @@ describe('OptimizerAPR - lenderAaveFraxStaker', () => { true, DAY, )); - oneInch = '0x1111111254EEB25477B68fb85Ed929f73A960582'; amountStorage = ethers.utils.hexStripZeros(utils.parseEther('1').toHexString()); }); @@ -161,7 +163,7 @@ describe('OptimizerAPR - lenderAaveFraxStaker', () => { it('reverts - too small saking period and already initialized', async () => { const lender = (await deployUpgradeable(new GenericAaveFraxStaker__factory(guardian))) as GenericAaveFraxStaker; await expect( - lender.initialize(strategy.address, 'test', true, [governor.address], guardian.address, [keeper.address], 0), + lender.initialize(strategy.address, 'test', true, [governor.address], guardian.address, [keeper.address], oneInch,0), ).to.be.revertedWithCustomError(lender, 'TooSmallStakingPeriod'); await expect( lenderAave.initialize( @@ -171,6 +173,7 @@ describe('OptimizerAPR - lenderAaveFraxStaker', () => { [governor.address], guardian.address, [keeper.address], + oneInch, 0, ), ).to.be.revertedWith('Initializable: contract is already initialized'); diff --git a/test/hardhat/optimizerApr/lenderAave.test.ts b/test/hardhat/optimizerApr/lenderAave.test.ts index 875c961..a36479b 100644 --- a/test/hardhat/optimizerApr/lenderAave.test.ts +++ b/test/hardhat/optimizerApr/lenderAave.test.ts @@ -13,8 +13,8 @@ import { ILendingPool__factory, IStakedAave, IStakedAave__factory, - OptimizerAPRStrategy, - OptimizerAPRStrategy__factory, + OptimizerAPRGreedyStrategy, + OptimizerAPRGreedyStrategy__factory, PoolManager, } from '../../../typechain'; import { gwei } from '../../../utils/bignumber'; @@ -29,9 +29,11 @@ async function initStrategy( keeper: SignerWithAddress, manager: PoolManager, ): Promise<{ - strategy: OptimizerAPRStrategy; + strategy: OptimizerAPRGreedyStrategy; }> { - const strategy = (await deployUpgradeable(new OptimizerAPRStrategy__factory(guardian))) as OptimizerAPRStrategy; + const strategy = (await deployUpgradeable( + new OptimizerAPRGreedyStrategy__factory(guardian), + )) as OptimizerAPRGreedyStrategy; await strategy.initialize(manager.address, governor.address, guardian.address, [keeper.address]); await manager.connect(governor).addStrategy(strategy.address, gwei('0.8')); return { strategy }; @@ -41,7 +43,7 @@ async function initLenderAave( governor: SignerWithAddress, guardian: SignerWithAddress, keeper: SignerWithAddress, - strategy: OptimizerAPRStrategy, + strategy: OptimizerAPRGreedyStrategy, name: string, isIncentivized: boolean, ): Promise<{ @@ -50,13 +52,13 @@ async function initLenderAave( const lender = (await deployUpgradeable(new GenericAaveNoStaker__factory(guardian))) as GenericAaveNoStaker; await lender.initialize(strategy.address, name, isIncentivized, [governor.address], guardian.address, [ keeper.address, - ]); + ], oneInch); await strategy.connect(governor).addLender(lender.address); return { lender }; } let governor: SignerWithAddress, guardian: SignerWithAddress, user: SignerWithAddress, keeper: SignerWithAddress; -let strategy: OptimizerAPRStrategy; +let strategy: OptimizerAPRGreedyStrategy; let token: ERC20; let tokenDecimal: number; let FEI: ERC20; @@ -148,10 +150,10 @@ describe('OptimizerAPR - lenderAave', () => { await expect( lender.initialize(strategyFEI.address, 'lender FEI', true, [governor.address], guardian.address, [ keeper.address, - ]), + ],oneInch), ).to.be.reverted; await expect( - lenderAave.initialize(strategy.address, 'test', true, [governor.address], guardian.address, [keeper.address]), + lenderAave.initialize(strategy.address, 'test', true, [governor.address], guardian.address, [keeper.address], oneInch), ).to.be.revertedWith('Initializable: contract is already initialized'); }); }); @@ -224,7 +226,7 @@ describe('OptimizerAPR - lenderAave', () => { const lender = (await deployUpgradeable(new GenericAaveNoStaker__factory(guardian))) as GenericAaveNoStaker; await lender.initialize(strategyFEI.address, 'lender FEI', false, [governor.address], guardian.address, [ keeper.address, - ]); + ], oneInch); const apr = await lender.connect(keeper).apr(); // at mainnet fork time there is 23% coming from liquidity rate and there is therefore no incentive expect(apr).to.be.closeTo(parseUnits('0.02334511', 18), parseUnits('0.1', 18)); diff --git a/test/hardhat/optimizerApr/lenderCompound.test.ts b/test/hardhat/optimizerApr/lenderCompound.test.ts index 8bca0bf..d490d6f 100644 --- a/test/hardhat/optimizerApr/lenderCompound.test.ts +++ b/test/hardhat/optimizerApr/lenderCompound.test.ts @@ -13,8 +13,8 @@ import { GenericCompoundUpgradeable__factory, IComptroller, IComptroller__factory, - OptimizerAPRStrategy, - OptimizerAPRStrategy__factory, + OptimizerAPRGreedyStrategy, + OptimizerAPRGreedyStrategy__factory, PoolManager, } from '../../../typechain'; import { gwei } from '../../../utils/bignumber'; @@ -29,9 +29,11 @@ async function initStrategy( keeper: SignerWithAddress, manager: PoolManager, ): Promise<{ - strategy: OptimizerAPRStrategy; + strategy: OptimizerAPRGreedyStrategy; }> { - const strategy = (await deployUpgradeable(new OptimizerAPRStrategy__factory(guardian))) as OptimizerAPRStrategy; + const strategy = (await deployUpgradeable( + new OptimizerAPRGreedyStrategy__factory(guardian), + )) as OptimizerAPRGreedyStrategy; await strategy.initialize(manager.address, governor.address, guardian.address, [keeper.address]); await manager.connect(governor).addStrategy(strategy.address, gwei('0.8')); return { strategy }; @@ -41,7 +43,7 @@ async function initLenderCompound( governor: SignerWithAddress, guardian: SignerWithAddress, keeper: SignerWithAddress, - strategy: OptimizerAPRStrategy, + strategy: OptimizerAPRGreedyStrategy, name: string, cToken: string, ): Promise<{ @@ -50,13 +52,13 @@ async function initLenderCompound( const lender = (await deployUpgradeable( new GenericCompoundUpgradeable__factory(guardian), )) as GenericCompoundUpgradeable; - await lender.initialize(strategy.address, name, cToken, [governor.address], guardian.address, [keeper.address]); + await lender.initialize(strategy.address, name, cToken, [governor.address], guardian.address, [keeper.address], oneInch); await strategy.connect(governor).addLender(lender.address); return { lender }; } let governor: SignerWithAddress, guardian: SignerWithAddress, user: SignerWithAddress, keeper: SignerWithAddress; -let strategy: OptimizerAPRStrategy; +let strategy: OptimizerAPRGreedyStrategy; let token: ERC20; let tokenDecimal: number; let balanceSlot: number; @@ -65,6 +67,7 @@ let manager: PoolManager; let lenderCompound: GenericCompoundUpgradeable; let comptroller: IComptroller; let cToken: CErc20I; +let oneInch: string; const guardianRole = ethers.utils.solidityKeccak256(['string'], ['GUARDIAN_ROLE']); const strategyRole = ethers.utils.solidityKeccak256(['string'], ['STRATEGY_ROLE']); @@ -101,7 +104,7 @@ describe('OptimizerAPR - lenderCompound', () => { guardianError = `AccessControl: account ${user.address.toLowerCase()} is missing role ${guardianRole}`; strategyError = `AccessControl: account ${user.address.toLowerCase()} is missing role ${strategyRole}`; keeperError = `AccessControl: account ${user.address.toLowerCase()} is missing role ${keeperRole}`; - // oneInch = '0x1111111254EEB25477B68fb85Ed929f73A960582'; + oneInch = '0x1111111254EEB25477B68fb85Ed929f73A960582'; }); beforeEach(async () => { @@ -149,7 +152,7 @@ describe('OptimizerAPR - lenderCompound', () => { await expect( lender.initialize(strategy.address, 'wrong lender', wrongCToken.address, [governor.address], guardian.address, [ keeper.address, - ]), + ], oneInch), ).to.be.revertedWithCustomError(lender, 'WrongCToken'); }); it('Parameters', async () => { diff --git a/test/hardhat/optimizerApr/lenderEuler.test.ts b/test/hardhat/optimizerApr/lenderEuler.test.ts index f445848..3793dad 100644 --- a/test/hardhat/optimizerApr/lenderEuler.test.ts +++ b/test/hardhat/optimizerApr/lenderEuler.test.ts @@ -15,8 +15,8 @@ import { IEulerEToken__factory, IGovernance, IGovernance__factory, - OptimizerAPRStrategy, - OptimizerAPRStrategy__factory, + OptimizerAPRGreedyStrategy, + OptimizerAPRGreedyStrategy__factory, PoolManager, } from '../../../typechain'; import { gwei } from '../../../utils/bignumber'; @@ -31,9 +31,11 @@ async function initStrategy( keeper: SignerWithAddress, manager: PoolManager, ): Promise<{ - strategy: OptimizerAPRStrategy; + strategy: OptimizerAPRGreedyStrategy; }> { - const strategy = (await deployUpgradeable(new OptimizerAPRStrategy__factory(guardian))) as OptimizerAPRStrategy; + const strategy = (await deployUpgradeable( + new OptimizerAPRGreedyStrategy__factory(guardian), + )) as OptimizerAPRGreedyStrategy; await strategy.initialize(manager.address, governor.address, guardian.address, [keeper.address]); await manager.connect(governor).addStrategy(strategy.address, gwei('0.8')); return { strategy }; @@ -43,19 +45,19 @@ async function initLenderEuler( governor: SignerWithAddress, guardian: SignerWithAddress, keeper: SignerWithAddress, - strategy: OptimizerAPRStrategy, + strategy: OptimizerAPRGreedyStrategy, name: string, ): Promise<{ lender: GenericEuler; }> { const lender = (await deployUpgradeable(new GenericEuler__factory(guardian))) as GenericEuler; - await lender.initializeEuler(strategy.address, name, [governor.address], guardian.address, [keeper.address]); + await lender.initializeEuler(strategy.address, name, [governor.address], guardian.address, [keeper.address], oneInch); await strategy.connect(governor).addLender(lender.address); return { lender }; } let governor: SignerWithAddress, guardian: SignerWithAddress, user: SignerWithAddress, keeper: SignerWithAddress; -let strategy: OptimizerAPRStrategy; +let strategy: OptimizerAPRGreedyStrategy; let token: ERC20; let tokenDecimal: number; let balanceSlot: number; @@ -65,6 +67,8 @@ let eToken: IEulerEToken; let euler: IEuler; // let eulerMarkets: IEulerMarkets; let governance: IGovernance; +let oneInch = '0x1111111254EEB25477B68fb85Ed929f73A960582'; + const guardianRole = ethers.utils.solidityKeccak256(['string'], ['GUARDIAN_ROLE']); const strategyRole = ethers.utils.solidityKeccak256(['string'], ['STRATEGY_ROLE']); @@ -132,7 +136,9 @@ describe('OptimizerAPR - lenderEuler', () => { manager = (await deploy('PoolManager', [token.address, governor.address, guardian.address])) as PoolManager; ({ strategy } = await initStrategy(governor, guardian, keeper, manager)); const lender = (await deployUpgradeable(new GenericEuler__factory(guardian))) as GenericEuler; - await lender.initializeEuler(strategy.address, 'wrong lender', [governor.address], guardian.address, [keeper.address]); + await lender.initializeEuler(strategy.address, 'wrong lender', [governor.address], guardian.address, [ + keeper.address, + ], oneInch); expect(await lender.eToken()).to.not.equal(wrongEToken.address); }); it('Parameters', async () => { diff --git a/test/hardhat/strategyAaveFlashLoan/aaveFlashloanStrategy1.test.ts b/test/hardhat/strategyAaveFlashLoan/aaveFlashloanStrategy1.test.ts index 87d92dd..87f8cec 100644 --- a/test/hardhat/strategyAaveFlashLoan/aaveFlashloanStrategy1.test.ts +++ b/test/hardhat/strategyAaveFlashLoan/aaveFlashloanStrategy1.test.ts @@ -45,7 +45,7 @@ describe('AaveFlashloanStrategy - Main test file', () => { let lendingPool: ILendingPool; let flashMintLib: FlashMintLib; let stkAaveHolder: string; - let oneInch = "0x1111111254EEB25477B68fb85Ed929f73A960582" + const oneInch = '0x1111111254EEB25477B68fb85Ed929f73A960582'; let strategy: AaveFlashloanStrategy; const impersonatedSigners: { [key: string]: Signer } = {}; @@ -185,12 +185,8 @@ describe('AaveFlashloanStrategy - Main test file', () => { expect(await dai.allowance(strategy.address, lendingPool.address)).to.equal(constants.MaxUint256); expect(await dai.allowance(strategy.address, await flashMintLib.LENDER())).to.equal(constants.MaxUint256); - expect(await aave.allowance(strategy.address, oneInch)).to.equal( - constants.MaxUint256, - ); - expect(await stkAave.allowance(strategy.address, oneInch)).to.equal( - constants.MaxUint256, - ); + expect(await aave.allowance(strategy.address, oneInch)).to.equal(constants.MaxUint256); + expect(await stkAave.allowance(strategy.address, oneInch)).to.equal(constants.MaxUint256); }); }); diff --git a/test/hardhat/strategyAaveFlashLoan/aaveFlashloanStrategyCoverage.test.ts b/test/hardhat/strategyAaveFlashLoan/aaveFlashloanStrategyCoverage.test.ts index 61eed45..12419a2 100644 --- a/test/hardhat/strategyAaveFlashLoan/aaveFlashloanStrategyCoverage.test.ts +++ b/test/hardhat/strategyAaveFlashLoan/aaveFlashloanStrategyCoverage.test.ts @@ -17,7 +17,6 @@ import { PoolManager, } from '../../../typechain'; import { deploy, impersonate, latestTime } from '../test-utils'; -import { time } from '../test-utils/helpers'; import { BASE_PARAMS } from '../utils'; import { findBalancesSlot, setTokenBalanceFor } from '../utils-interaction'; @@ -141,7 +140,7 @@ describe('AaveFlashloanStrategy - Coverage', () => { // add aUSDC to strategy balance sheet to not have revert errors if the delay between blocks is to large when testing balanceSlot = await findBalancesSlot(aToken.address); await setTokenBalanceFor(aToken, user.address, 1, balanceSlot); - await aToken.connect(user).transfer(strategy.address, utils.parseUnits("1", await aToken.decimals())) + await aToken.connect(user).transfer(strategy.address, utils.parseUnits('1', await aToken.decimals())); }); describe('adjustPosition', () => { diff --git a/test/hardhat/strategyAaveFlashLoan/setup_tests.ts b/test/hardhat/strategyAaveFlashLoan/setup_tests.ts index 30e55ad..c1bd972 100644 --- a/test/hardhat/strategyAaveFlashLoan/setup_tests.ts +++ b/test/hardhat/strategyAaveFlashLoan/setup_tests.ts @@ -148,9 +148,9 @@ export async function setup(startBlocknumber?: number, collat = 'USDC') { Balance USDC: ${logBN(await wantToken.balanceOf(strategy.address), { base: wantTokenBase })} Balance stkAave: ${logBN(await stkAave.balanceOf(strategy.address), { base: 18 })} Rewards: ${logBN( - await incentivesController.getRewardsBalance([aToken.address, debtToken.address], strategy.address), - { base: 18 }, - )} + await incentivesController.getRewardsBalance([aToken.address, debtToken.address], strategy.address), + { base: 18 }, + )} `); const logPosition = async () => diff --git a/test/hardhat/strategyStETH/strategyStETHlocal.ts b/test/hardhat/strategyStETH/strategyStETHlocal.ts index 5f49705..e3c63f8 100644 --- a/test/hardhat/strategyStETH/strategyStETHlocal.ts +++ b/test/hardhat/strategyStETH/strategyStETHlocal.ts @@ -90,8 +90,8 @@ describe('StrategyStETH', () => { it('apr', async () => { expect(await strategy.apr()).to.be.equal(parseUnits('3', 9)); }); - it('SECONDSPERYEAR', async () => { - expect(await strategy.SECONDSPERYEAR()).to.be.equal(BigNumber.from('31556952')); + it('SECONDS_PER_YEAR', async () => { + expect(await strategy.SECONDS_PER_YEAR()).to.be.equal(BigNumber.from('31556952')); }); it('DENOMINATOR', async () => { expect(await strategy.DENOMINATOR()).to.be.equal(BigNumber.from('10000')); diff --git a/test/hardhat/utils-interaction.ts b/test/hardhat/utils-interaction.ts index 706b5fc..cf922c3 100644 --- a/test/hardhat/utils-interaction.ts +++ b/test/hardhat/utils-interaction.ts @@ -15,7 +15,7 @@ import { BigNumber, BigNumberish, utils } from 'ethers'; import { formatUnits, parseUnits } from 'ethers/lib/utils'; import { ethers, network } from 'hardhat'; -import { ERC20, ERC20__factory, OptimizerAPRStrategy, StETHStrategy } from '../../typechain'; +import { ERC20, ERC20__factory, OptimizerAPRGreedyStrategy, StETHStrategy } from '../../typechain'; export const wait = (n = 1000): Promise => { return new Promise(resolve => { @@ -106,7 +106,7 @@ export const logStETHInfo = async ( export const logOptimizerInfo = async ( stableMaster: StableMasterFront, poolManager: PoolManager, - strategy: OptimizerAPRStrategy, + strategy: OptimizerAPRGreedyStrategy, ): Promise => { const collatAddress = (await stableMaster.collateralMap(poolManager.address)).token; const collat = (await ethers.getContractAt(ERC20__factory.abi, collatAddress)) as ERC20;