diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fda7b8..f6e63bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,3 +65,9 @@ jobs: # This is required separately from yarn test because it generates the typechain definitions - name: Compile run: yarn compile + - name: Run unit tests + run: yarn test + env: + ENABLE_GAS_REPORT: true + CI: true + ETH_NODE_URI_FORK: ${{secrets.ETH_NODE_URI_FORK}} diff --git a/.solcover.js b/.solcover.js index fe43167..6eab21e 100644 --- a/.solcover.js +++ b/.solcover.js @@ -2,40 +2,7 @@ module.exports = { norpc: true, testCommand: 'yarn test', compileCommand: 'yarn compile:hardhat', - skipFiles: [ - 'mock', - 'external', - 'interfaces', - 'sigp', - 'bondingCurve/BondingCurveEvents.sol', - 'collateralSettler/CollateralSettlerVeANGLEEvents.sol', - 'dao/Governor.sol', - 'dao/GovernorStorage.sol', - 'dao/GovernorEvents.sol', - 'dao/TimelockEvents.sol', - 'dao/Temporary.sol', - 'poolManager/PoolManagerEvents.sol', - 'poolManager/PoolManagerStorageV1.sol', - 'poolManager/PoolManagerStorageV2.sol', - 'poolManager/PoolManagerStorageV3.sol', - 'feeManager/FeeManagerEvents.sol', - 'feeManager/FeeManagerStorage.sol', - 'oracle/OracleChainlinkMulti.sol', - 'stableMaster/StableMasterEvents.sol', - 'stableMaster/StableMasterStorage.sol', - 'staking/RewardsDistributorEvents.sol', - 'staking/StakingRewardsEvents.sol', - 'staking/StrategyWETH.sol', - 'strategies/BaseStrategyEvents.sol', - 'utils/OracleMath.sol', - 'utils/PoolAddress.sol', - 'perpetualManager/PerpetualManagerEvents.sol', - 'perpetualManager/PerpetualManagerStorage.sol', - 'genericLender/GenericDyDx.sol', - 'genericLender/GenericCompoundRinkeby.sol', - 'genericLender/GenericCompoundRinkebyETH.sol', - 'webscrap', - ], + skipFiles: ['mock', 'external', 'interfaces', 'utils'], providerOptions: { default_balance_ether: '10000000000000000000000000', }, diff --git a/contracts/strategies/OptimizerAPR/genericLender/GenericAaveFraxStaker.sol b/contracts/strategies/OptimizerAPR/genericLender/GenericAaveFraxStaker.sol index a88c69e..eb2e570 100644 --- a/contracts/strategies/OptimizerAPR/genericLender/GenericAaveFraxStaker.sol +++ b/contracts/strategies/OptimizerAPR/genericLender/GenericAaveFraxStaker.sol @@ -7,12 +7,13 @@ import "./GenericAaveUpgradeable.sol"; /// @title GenericAaveFraxStaker /// @author Angle Core Team -/// @notice Allow to stake aFRAX on FRAX contracts to earn their incentives +/// @notice `GenericAaveUpgradeable` implementation for FRAX where aFRAX obtained from Aave are staked on a FRAX contract +/// to earn FXS incentives contract GenericAaveFraxStaker is GenericAaveUpgradeable { using SafeERC20 for IERC20; using Address for address; - // // ========================== Protocol Addresses ========================== + // ============================= Protocol Addresses ============================ AggregatorV3Interface private constant oracleFXS = AggregatorV3Interface(0x6Ebc52C8C1089be9eB3945C4350B68B8E4C2233f); @@ -20,31 +21,37 @@ contract GenericAaveFraxStaker is GenericAaveUpgradeable { IFraxUnifiedFarmTemplate(0x02577b426F223A6B4f2351315A19ecD6F357d65c); uint256 private constant FRAX_IDX = 0; - // ==================== Parameters ============================= + // ================================ Variables ================================== - // hash representing the position on Frax staker + /// @notice Hash representing the position on Frax staker bytes32 public kekId; - // used to track the current liquidity (staked + interests) + /// @notice Used to track the current liquidity (staked + interests) from Aave uint256 public lastAaveReserveNormalizedIncome; - // Last liquidity recorded on Frax staking contract + /// @notice Tracks the amount of FRAX controlled by the protocol and lent as aFRAX on Frax staking contract + /// This quantity increases due to the Aave native yield uint256 private lastLiquidity; - // Last time a staker has been created + /// @notice Last time a staker has been created uint256 public lastCreatedStake; + + // ================================ Parameters ================================= + + /// @notice Minimum amount of aFRAX to stake uint256 private constant minStakingAmount = 1000 * 1e18; // 100 aFrax + /// @notice Staking duration uint256 public stakingPeriod; + // ==================================== Errors ================================= + error NoLockedLiquidity(); error TooSmallStakingPeriod(); error StakingPeriodTooSmall(); error UnstakedTooSoon(); - // ============================= Constructor ============================= + // ============================= Constructor =================================== - /// @notice Initializer of the `GenericAave` - /// @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 + /// @notice Wrapper built on top of the `initializeAave` method to initialize the contract + /// @param _stakingPeriod Amount of time aFRAX must remain staked + /// @dev This function also initialized some FRAX related parameters like the staking period function initialize( address _strategy, string memory name, @@ -60,7 +67,7 @@ contract GenericAaveFraxStaker is GenericAaveUpgradeable { lastAaveReserveNormalizedIncome = _lendingPool.getReserveNormalizedIncome(address(want)); } - // ========================= External Functions =========================== + // =========================== 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 @@ -68,32 +75,32 @@ contract GenericAaveFraxStaker is GenericAaveUpgradeable { return aFraxStakingContract.getReward(address(this)); } - // ========================= Governance Functions =========================== + // =========================== Governance Functions ============================ - /// @notice Function to update the staking period + /// @notice Updates the staking period on the aFRAX staking contract function setLockTime(uint256 _stakingPeriod) external onlyRole(GUARDIAN_ROLE) { if (_stakingPeriod < aFraxStakingContract.lock_time_min()) revert StakingPeriodTooSmall(); stakingPeriod = _stakingPeriod; } - /// @notice Function to set a proxy on the staking contract to have a delegation on their boosting - /// @dev We can have a multiplier if we ask for someone with boosting power + /// @notice Sets a proxy on the staking contract to obtain a delegation from an address with a boost + /// @dev Contract can have a multiplier on its FXS rewards if granted by someone with boosting power /// @dev Can only be called after Frax governance called `aFraxStakingContract.toggleValidVeFXSProxy(proxy)` /// and proxy called `aFraxStakingContract.proxyToggleStaker(address(this))` function setProxyBoost(address proxy) external onlyRole(GUARDIAN_ROLE) { aFraxStakingContract.stakerSetVeFXSProxy(proxy); } - // ========================= Virtual Functions =========================== + // ============================ Virtual Functions ============================== - /// @notice Allow the lender to stake its aTokens in the external staking contract - /// @param amount Amount of aToken wanted to be stake - /// @dev If there is an existent locker already on Frax staking contract (keckId != null) --> then add to it - /// otherwise (first time w deposit or last action was a withdraw) we need to create a new locker + /// @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 + /// otherwise (if it's the first time we deposit or if last action was a withdraw) we need to create a new locker /// @dev Currently there is no additional reward to stake more than the minimum period as there is no multiplier function _stake(uint256 amount) internal override returns (uint256 stakedAmount) { uint256 pastReserveNormalizedIncome = lastAaveReserveNormalizedIncome; - lastAaveReserveNormalizedIncome = _lendingPool.getReserveNormalizedIncome(address(want)); + uint256 newReserveNormalizedIncome = _lendingPool.getReserveNormalizedIncome(address(want)); + lastAaveReserveNormalizedIncome = newReserveNormalizedIncome; IERC20(address(_aToken)).safeApprove(address(aFraxStakingContract), amount); if (kekId == bytes32(0)) { @@ -102,33 +109,33 @@ contract GenericAaveFraxStaker is GenericAaveUpgradeable { kekId = aFraxStakingContract.stakeLocked(amount, stakingPeriod); } else { aFraxStakingContract.lockAdditional(kekId, amount); - lastLiquidity = (lastLiquidity * lastAaveReserveNormalizedIncome) / pastReserveNormalizedIncome + amount; + // Updating the `lastLiquidity` value + lastLiquidity = (lastLiquidity * newReserveNormalizedIncome) / pastReserveNormalizedIncome + amount; } - stakedAmount = amount; } - /// @notice Allow the lender to unstake its aTokens from the external staking contract - /// @param amount Amount of aToken wanted to be unstake - /// @dev If minimum staking period is not finished the function will revert - /// @dev We suppose there is no loss on staking contract --> only if the funds get hacked + /// @notice Implementation of the `_unstake` function + /// @dev If the minimum staking period is not finished, the function will revert + /// @dev This implementation assumes that there cannot any loss when staking on FRAX function _unstake(uint256 amount) internal override returns (uint256 freedAmount) { if (kekId == bytes32(0)) return 0; if (block.timestamp - lastCreatedStake < stakingPeriod) revert UnstakedTooSoon(); lastAaveReserveNormalizedIncome = _lendingPool.getReserveNormalizedIncome(address(want)); - lastCreatedStake = block.timestamp; - freedAmount = aFraxStakingContract.withdrawLocked(kekId, address(this)); if (amount + minStakingAmount < freedAmount) { - // too much has been withdrawn we must create back a locker - lastLiquidity = freedAmount - amount; - IERC20(address(_aToken)).safeApprove(address(aFraxStakingContract), lastLiquidity); - kekId = aFraxStakingContract.stakeLocked(lastLiquidity, stakingPeriod); - - // - 1 because there values are rounded when transfering aTokens so we may end up with - // with a little bit less, instead of making multiple call just play it safe and withdraw 1 in all cases + // If too much has been withdrawn, we must create back a locker + lastCreatedStake = block.timestamp; + uint256 amountFRAXControlled = freedAmount - amount; + lastLiquidity = amountFRAXControlled; + IERC20(address(_aToken)).safeApprove(address(aFraxStakingContract), amountFRAXControlled); + kekId = aFraxStakingContract.stakeLocked(amountFRAXControlled, stakingPeriod); + + // We need to round down the `freedAmount` value because values can be rounded down when transfering aTokens + // and we may stake slightly less than desired: to play it safe in all cases and avoid multiple calls, we + // systematically round down freedAmount = amount - 1; } else { lastLiquidity = 0; @@ -146,14 +153,14 @@ 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) { - // These computations are made possible only because there will be only one staker + // 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)); uint256 newBalance; - // if we didn't stake we need and we don't have anything to give, then stakingApr can only be 0 + // If we didn't stake anything and we don't have anything to give, then stakingApr can only be 0 if (lastLiquidity == 0 && amount == 0) return 0; - // if we didn't stake we need an extra info on the multiplier per staking period + // 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; @@ -165,26 +172,27 @@ contract GenericAaveFraxStaker is GenericAaveUpgradeable { newCombinedWeight = (newBalance * newCombinedWeight) / lastLiquidity; } - // if we arrive up until here the totalCombinedWeight can only be non null + // If we arrive up until here the `totalCombinedWeight` can only be non null uint256 totalCombinedWeight = aFraxStakingContract.totalCombinedWeight() + newCombinedWeight - oldCombinedWeight; - uint256 rewardRate = (newCombinedWeight * aFraxStakingContract.rewardRates(FRAX_IDX) * 1 ether) / - (totalCombinedWeight * 1 ether); + uint256 rewardRate = (newCombinedWeight * aFraxStakingContract.rewardRates(FRAX_IDX)) / totalCombinedWeight; - // APRs are in 1e18 and 95% of estimated APR to avoid overestimations - apr = (_estimatedFXSToWant(rewardRate) * _SECONDS_IN_YEAR * 9500 * 1 ether) / 10000 / newBalance; + // APRs are in 1e18 and a 5% penalty on the FXS price is taken to avoid overestimations + apr = (_estimatedFXSToWant(rewardRate * _SECONDS_IN_YEAR) * 9500 * 1 ether) / 10000 / newBalance; } - // ========================= Internal Functions =========================== + // ============================ Internal Functions ============================= - /// @notice Estimate the amount of `want` we will get out by swapping it for FXS + /// @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) - /// @return swappedAmount Amount of `want` we are getting but in a gloabl base 18 - /// @dev Uses Chainlink spot price. Return value will be in base 18 + /// @return swappedAmount Amount of `want` we are getting but in a global base 18 + /// @dev Uses Chainlink spot price + /// @dev This implementation assumes that 1 FRAX = 1 USD, as it does not do any FRAX -> USD conversion function _estimatedFXSToWant(uint256 amount) internal view returns (uint256) { - (, int256 fxsPriceUSD, , , ) = oracleFXS.latestRoundData(); // fxsPriceUSD is in base 8 + (, int256 fxsPriceUSD, , , ) = oracleFXS.latestRoundData(); + // fxsPriceUSD is in base 8 return (uint256(fxsPriceUSD) * amount) / 1e8; } } diff --git a/contracts/strategies/OptimizerAPR/genericLender/GenericAaveNoStaker.sol b/contracts/strategies/OptimizerAPR/genericLender/GenericAaveNoStaker.sol index ec2f7d6..8549034 100644 --- a/contracts/strategies/OptimizerAPR/genericLender/GenericAaveNoStaker.sol +++ b/contracts/strategies/OptimizerAPR/genericLender/GenericAaveNoStaker.sol @@ -7,14 +7,12 @@ import "./GenericAaveUpgradeable.sol"; /// @title GenericAaveNoStaker /// @author Angle Core Team /// @notice Only deposit onto Aave lendingPool with no staking +/// @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 Initializer of the `GenericAave` - /// @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 + /// @notice Wrapper on top of the `initializeAave` method function initialize( address _strategy, string memory name, @@ -26,7 +24,7 @@ contract GenericAaveNoStaker is GenericAaveUpgradeable { initializeAave(_strategy, name, _isIncentivised, governorList, guardian, keeperList); } - // ========================= Virtual Functions =========================== + // =========================== Virtual Functions =============================== function _stake(uint256) internal override returns (uint256) {} @@ -34,12 +32,13 @@ contract GenericAaveNoStaker is GenericAaveUpgradeable { return amount; } - /// @notice Get current staked Frax balance (counting interest receive since last update) + /// @notice Gets current staked balance (e.g 0 if nothing is staked) function _stakedBalance() internal pure override returns (uint256) { return 0; } - /// @notice Get stakingAPR after staking an additional `amount` + /// @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) { return 0; } diff --git a/contracts/strategies/OptimizerAPR/genericLender/GenericAaveUpgradeable.sol b/contracts/strategies/OptimizerAPR/genericLender/GenericAaveUpgradeable.sol index 890e638..70f0371 100644 --- a/contracts/strategies/OptimizerAPR/genericLender/GenericAaveUpgradeable.sol +++ b/contracts/strategies/OptimizerAPR/genericLender/GenericAaveUpgradeable.sol @@ -11,24 +11,19 @@ import "../../../interfaces/external/aave/IProtocolDataProvider.sol"; import "../../../interfaces/external/aave/ILendingPool.sol"; import "./GenericLenderBaseUpgradeable.sol"; -struct AaveReferences { - IAToken _aToken; - IProtocolDataProvider _protocolDataProvider; - IStakedAave _stkAave; - address _aave; -} - /// @title GenericAave /// @author Forked from https://github.com/Grandthrax/yearnV2-generic-lender-strat/blob/master/contracts/GenericLender/GenericAave.sol -/// @notice A contract to lend any ERC20 to Aave +/// @notice A contract to lend any supported ERC20 to Aave and potentially stake them in an external staking contract +/// @dev This contract is just a base implementation which can be overriden depending on the staking contract on which to stake +/// or not the aTokens abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { using SafeERC20 for IERC20; using Address for address; - // ==================== References to contracts ============================= + // ======================== Reference to contract ============================== AggregatorV3Interface private constant oracle = AggregatorV3Interface(0x547a514d5e3769680Ce22B2361c10Ea13619e8a9); - // // ========================== Aave Protocol Addresses ========================== + // ========================== Aave Protocol Addresses ========================== address private constant _aave = 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9; IStakedAave private constant _stkAave = IStakedAave(0x4da27a545c0c5B758a6BA100e3a049001de870f5); @@ -38,27 +33,31 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { IProtocolDataProvider private constant _protocolDataProvider = IProtocolDataProvider(0x057835Ad21a177dbdd3090bB1CAE03EaCF78Fc6d); - // ==================== Parameters ============================= + // ========================= Constants and Parameters ========================== uint256 public cooldownSeconds; uint256 public unstakeWindow; - uint256 public wantBase; bool public cooldownStkAave; bool public isIncentivised; IAToken internal _aToken; - uint256 internal constant _SECONDS_IN_YEAR = 365 days; + // =================================== Event =================================== + event IncentivisedUpdated(bool _isIncentivised); + // =================================== Error =================================== + error PoolNotIncentivized(); - // ============================= Constructor ============================= + // ================================ Constructor ================================ /// @notice Initializer of the `GenericAave` /// @param _strategy Reference to the strategy using this lender + /// @param name Name of the lender + /// @param _isIncentivised Whether the corresponding token is incentivized on Aave or not /// @param governorList List of addresses with governor privilege - /// @param keeperList List of addresses with keeper privilege /// @param guardian Address of the guardian + /// @param keeperList List of addresses with keeper privilege function initializeAave( address _strategy, string memory name, @@ -74,10 +73,12 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { isIncentivised = _isIncentivised; cooldownStkAave = true; IERC20(address(want)).safeApprove(address(_lendingPool), type(uint256).max); - wantBase = 10**IERC20Metadata(address(want)).decimals(); + // Approve swap router spend + IERC20(address(_stkAave)).safeApprove(oneInch, type(uint256).max); + IERC20(address(_aave)).safeApprove(oneInch, type(uint256).max); } - // ============================= External Functions ============================= + // ============================= External Functions ============================ /// @notice Deposits the current balance to the lending platform function deposit() external override onlyRole(STRATEGY_ROLE) { @@ -85,7 +86,7 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { // Aave doesn't allow null deposits if (balance == 0) return; _deposit(balance); - // we don't stake balance but the whole aTokenBalance + // We don't stake balance but the whole aTokenBalance // if some dust has been kept idle _stake(_balanceAtoken()); } @@ -113,58 +114,41 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { return returned >= invested; } + /// @notice Claim earned stkAAVE + /// @dev stkAAVE require a "cooldown" period of 10 days before being claimed function claimRewards() external onlyRole(KEEPER_ROLE) { _claimRewards(); } - /// @notice Retrieves lending pool variables for `want`. Those variables are mostly used in the function - /// to compute the optimal borrow amount - /// @dev No access control needed because they fetch the values from Aave directly. - /// If it changes there, it will need to be updated here too + /// @notice Triggers the cooldown on Aave for this contract + function cooldown() external onlyRole(KEEPER_ROLE) { + _stkAave.cooldown(); + } + + /// @notice Retrieves lending pool variables like the `COOLDOWN_SECONDS` or the `UNSTAKE_WINDOW` on Aave + /// @dev No access control is needed here because values are fetched from Aave directly /// @dev We expect the values concerned not to be often modified function setAavePoolVariables() external { _setAavePoolVariables(); } - // ============================= External Setter Functions ============================= + // ========================== External Setter Functions ======================== /// @notice Toggle isIncentivised state, to let know the lender if it should harvest aave rewards function toggleIsIncentivised() external onlyRole(GUARDIAN_ROLE) { isIncentivised = !isIncentivised; } - /// @notice Toggle cooldownStkAave state, which allow or not to call the coolDown + /// @notice Toggle cooldownStkAave state, which allow or not to call the coolDown stkAave each time rewards are claimed function toggleCooldownStkAave() external onlyRole(GUARDIAN_ROLE) { cooldownStkAave = !cooldownStkAave; } - // ============================= External View Functions ============================= - - /// @notice Checks if assets are currently managed by this contract - function hasAssets() external view override returns (bool) { - return _nav() > 0; - } - - /// @notice Returns the current total of assets managed - function nav() external view override returns (uint256) { - return _nav(); - } + // =========================== External View Functions ========================= /// @notice Returns the current balance of aTokens - function underlyingBalanceStored() public view returns (uint256 balance) { - balance = _balanceAtoken(); - balance += _stakedBalance(); - } - - /// @notice Returns an estimation of the current Annual Percentage Rate - function apr() external view override returns (uint256) { - return _apr(); - } - - /// @notice Returns an estimation of the current Annual Percentage Rate weighted by a factor - function weightedApr() external view override returns (uint256) { - uint256 a = _apr(); - return a * _nav(); + function underlyingBalanceStored() public view override returns (uint256 balance) { + balance = _balanceAtoken() + _stakedBalance(); } /// @notice Returns an estimation of the current Annual Percentage Rate after a new deposit @@ -205,19 +189,13 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { return newLiquidityRate / 1e9 + incentivesRate + stakingApr; // divided by 1e9 to go from Ray to Wad } - // ========================= Internal Functions =========================== + // =========================== Internal Functions ============================== - /// @notice Claim earned stkAAVE (only called at `harvest`) - /// @dev stkAAVE require a "cooldown" period of 10 days before being claimed + /// @notice Internal version of the `claimRewards` function function _claimRewards() internal returns (uint256 stkAaveBalance) { stkAaveBalance = _balanceOfStkAave(); - uint256 cooldownStatus; - if (stkAaveBalance > 0) { - cooldownStatus = _checkCooldown(); // don't check status if we have no _stkAave - } - // If it's the claim period claim - if (stkAaveBalance > 0 && cooldownStatus == 1) { + if (stkAaveBalance > 0 && _checkCooldown() == 1) { // redeem AAVE from _stkAave _stkAave.claimRewards(address(this), type(uint256).max); _stkAave.redeem(address(this), stkAaveBalance); @@ -257,7 +235,7 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { } /// @notice See `apr` - function _apr() internal view returns (uint256) { + function _apr() internal view override returns (uint256) { ( uint256 availableLiquidity, uint256 totalStableDebt, @@ -278,11 +256,9 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { /// @notice Calculates APR from Liquidity Mining Program /// @param totalLiquidity Total liquidity available in the pool - /// @dev At Angle, compared with Yearn implementation, we have decided to add a check - /// about the `totalLiquidity` before entering the `if` branch 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 is no incentives + // it will fail if the 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)); @@ -296,11 +272,6 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { return 0; } - /// @notice See `nav` - function _nav() internal view returns (uint256) { - return want.balanceOf(address(this)) + underlyingBalanceStored(); - } - /// @notice See `withdraw` function _withdraw(uint256 amount) internal returns (uint256) { uint256 stakedBalance = _stakedBalance(); @@ -318,7 +289,7 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { return amount; } - // not state changing but OK because of previous call + // Not state changing but OK because of previous call uint256 liquidity = want.balanceOf(address(_aToken)); if (liquidity > 1) { @@ -363,7 +334,7 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { /// @notice Verifies the cooldown status for earned stkAAVE /// @return cooldownStatus Status of the coolDown: if it is 0 then there is no cooldown Status, if it is 1 then - /// the strategy should claim + /// the strategy should claim the stkAave function _checkCooldown() internal view returns (uint256 cooldownStatus) { uint256 cooldownStartTimestamp = IStakedAave(_stkAave).stakersCooldowns(address(this)); uint256 nextClaimStartTimestamp = cooldownStartTimestamp + cooldownSeconds; @@ -388,11 +359,21 @@ abstract contract GenericAaveUpgradeable is GenericLenderBaseUpgradeable { // ========================= Virtual Functions =========================== + /// @notice Allows the lender to stake its aTokens in an external staking contract + /// @param amount Amount of aTokens to stake + /// @return Amount of aTokens actually staked function _stake(uint256 amount) internal virtual returns (uint256); + /// @notice Allows the lender to unstake its aTokens from an external staking contract + /// @param amount Amount of aToken to unstake + /// @return Amount of aTokens actually unstaked function _unstake(uint256 amount) internal virtual returns (uint256); + /// @notice Gets the amount of aTokens currently staked function _stakedBalance() internal view virtual returns (uint256); + /// @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); } diff --git a/contracts/strategies/OptimizerAPR/genericLender/GenericCompoundUpgradeable.sol b/contracts/strategies/OptimizerAPR/genericLender/GenericCompoundUpgradeable.sol index caee721..c4455af 100644 --- a/contracts/strategies/OptimizerAPR/genericLender/GenericCompoundUpgradeable.sol +++ b/contracts/strategies/OptimizerAPR/genericLender/GenericCompoundUpgradeable.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.12; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "../../../interfaces/external/compound/CErc20I.sol"; import "../../../interfaces/external/compound/IComptroller.sol"; @@ -22,14 +21,20 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { AggregatorV3Interface public constant oracle = AggregatorV3Interface(0xdbd020CAeF83eFd542f4De03e3cF0C28A4428bd5); IComptroller public constant comptroller = IComptroller(0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B); address public constant comp = 0xc00e94Cb662C3520282E6f5717214004A7f26888; - address public constant oneInch = 0x1111111254fb6c44bAC0beD2854e76F90643097d; - // ==================== References to contracts ============================= + // ======================== References to contracts ============================ CErc20I public cToken; - uint256 public wantBase; - // ============================= Constructor ============================= + // =============================== 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 @@ -47,17 +52,9 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { ) external { _initialize(_strategy, _name, governorList, guardian, keeperList); - _setupRole(KEEPER_ROLE, guardian); - for (uint256 i = 0; i < keeperList.length; i++) { - _setupRole(KEEPER_ROLE, keeperList[i]); - } - - _setRoleAdmin(KEEPER_ROLE, GUARDIAN_ROLE); - cToken = CErc20I(_cToken); - require(CErc20I(_cToken).underlying() == address(want), "wrong cToken"); + if (CErc20I(_cToken).underlying() != address(want)) revert WrongCToken(); - wantBase = 10**IERC20Metadata(address(want)).decimals(); want.safeApprove(_cToken, type(uint256).max); IERC20(comp).safeApprove(oneInch, type(uint256).max); } @@ -67,7 +64,7 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { /// @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)); - require(cToken.mint(balance) == 0, "mint fail"); + if (cToken.mint(balance) != 0) revert FailedToMint(); } /// @notice Withdraws a given amount from lender @@ -85,15 +82,10 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { return returned >= invested; } - // ============================= External View Functions ============================= - - /// @notice Helper function to get the current total of assets managed by the lender. - function nav() external view override returns (uint256) { - return _nav(); - } + // ========================== External View Functions ========================== /// @notice Helper function the current balance of cTokens - function underlyingBalanceStored() public view returns (uint256 balance) { + function underlyingBalanceStored() public view override returns (uint256 balance) { uint256 currentCr = cToken.balanceOf(address(this)); if (currentCr == 0) { balance = 0; @@ -103,17 +95,6 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { } } - /// @notice Returns an estimation of the current Annual Percentage Rate - function apr() external view override returns (uint256) { - return _apr(); - } - - /// @notice Returns an estimation of the current Annual Percentage Rate weighted by a factor - function weightedApr() external view override returns (uint256) { - uint256 a = _apr(); - return a * _nav(); - } - /// @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 @@ -135,12 +116,7 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { return supplyRate * BLOCKS_PER_YEAR + _incentivesRate(amount); } - /// @notice Check if assets are currently managed by this contract - function hasAssets() external view override returns (bool) { - return cToken.balanceOf(address(this)) > 0 || want.balanceOf(address(this)) > 0; - } - - // ============================= Governance ============================= + // ================================= Governance ================================ /// @notice Withdraws as much as possible in case of emergency and sends it to the `PoolManager` /// @param amount Amount to withdraw @@ -152,18 +128,13 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { want.safeTransfer(address(poolManager), want.balanceOf(address(this))); } - // ============================= Internal Functions ============================= + // ============================= Internal Functions ============================ /// @notice See `apr` - function _apr() internal view returns (uint256) { + function _apr() internal view override returns (uint256) { return cToken.supplyRatePerBlock() * BLOCKS_PER_YEAR + _incentivesRate(0); } - /// @notice See `nav` - function _nav() internal view returns (uint256) { - return want.balanceOf(address(this)) + underlyingBalanceStored(); - } - /// @notice See `withdraw` function _withdraw(uint256 amount) internal returns (uint256) { uint256 balanceUnderlying = cToken.balanceOfUnderlying(address(this)); @@ -188,10 +159,10 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { if (toWithdraw <= liquidity) { // We can take all - require(cToken.redeemUnderlying(toWithdraw) == 0, "redeemUnderlying fail"); + if (cToken.redeemUnderlying(toWithdraw) != 0) revert FailedToRedeem(); } else { // Take all we can - require(cToken.redeemUnderlying(liquidity) == 0, "redeemUnderlying fail"); + if (cToken.redeemUnderlying(liquidity) != 0) revert FailedToRedeem(); } } address[] memory holders = new address[](1); @@ -231,7 +202,7 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { return 0; } (uint80 roundId, int256 ratio, , , uint80 answeredInRound) = oracle.latestRoundData(); - require(ratio > 0 && roundId <= answeredInRound, "100"); + if (ratio == 0 || roundId > answeredInRound) revert InvalidOracleValue(); uint256 castedRatio = uint256(ratio); // Checking whether we should multiply or divide by the ratio computed @@ -249,7 +220,7 @@ contract GenericCompoundUpgradeable is GenericLenderBaseUpgradeable { /// @notice Recovers ETH from the contract /// @param amount Amount to be recovered function recoverETH(address to, uint256 amount) external onlyRole(GUARDIAN_ROLE) { - require(payable(to).send(amount), "98"); + if (!payable(to).send(amount)) revert FailedToRecoverETH(); } receive() external payable {} diff --git a/contracts/strategies/OptimizerAPR/genericLender/GenericLenderBaseUpgradeable.sol b/contracts/strategies/OptimizerAPR/genericLender/GenericLenderBaseUpgradeable.sol index 0ed58a3..42cc3e8 100644 --- a/contracts/strategies/OptimizerAPR/genericLender/GenericLenderBaseUpgradeable.sol +++ b/contracts/strategies/OptimizerAPR/genericLender/GenericLenderBaseUpgradeable.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.12; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/Address.sol"; @@ -12,9 +13,9 @@ import "../../../interfaces/IGenericLender.sol"; import "../../../interfaces/IPoolManager.sol"; import "../../../interfaces/IStrategy.sol"; -/// @title GenericLenderBase +/// @title GenericLenderBaseUpgradeable /// @author Forked from https://github.com/Grandthrax/yearnV2-generic-lender-strat/tree/master/contracts/GenericLender -/// @notice A base contract to build contracts to lend assets +/// @notice A base contract to build contracts that lend assets to protocols abstract contract GenericLenderBaseUpgradeable is IGenericLender, AccessControlUpgradeable { using SafeERC20 for IERC20; @@ -22,30 +23,37 @@ abstract contract GenericLenderBaseUpgradeable is IGenericLender, AccessControlU bytes32 public constant STRATEGY_ROLE = keccak256("STRATEGY_ROLE"); bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE"); - // ==================== References to contracts ============================= - address private constant oneInch = 0x1111111254fb6c44bAC0beD2854e76F90643097d; + // ======================= References to contracts ============================= + + address internal constant oneInch = 0x1111111254fb6c44bAC0beD2854e76F90643097d; + + // ========================= References and Parameters ========================= - // ==================== References to parameters ============================ string public override lenderName; /// @notice Reference to the protocol's collateral poolManager IPoolManager public poolManager; - /// @notice Reference to the `Strategy` address public override strategy; - /// @notice Reference to the token lent IERC20 public want; + /// @notice Base of the asset handled by the lender + uint256 public wantBase; + + // ================================ Errors ===================================== error ErrorSwap(); error IncompatibleLengths(); + error ProtectedToken(); error TooSmallAmount(); - // ============================= Constructor ============================= + // ================================ Initializer ================================ /// @notice Initalizer of the `GenericLenderBase` /// @param _strategy Reference to the strategy using this lender + /// @param _name Name of the lender /// @param governorList List of addresses with governor privilege /// @param guardian Address of the guardian + /// @param keeperList List of keeper addresses function _initialize( address _strategy, string memory _name, @@ -75,14 +83,51 @@ abstract contract GenericLenderBaseUpgradeable is IGenericLender, AccessControlU _setupRole(STRATEGY_ROLE, _strategy); _setRoleAdmin(GUARDIAN_ROLE, STRATEGY_ROLE); _setRoleAdmin(STRATEGY_ROLE, GUARDIAN_ROLE); - + wantBase = 10**IERC20Metadata(address(want)).decimals(); want.safeApprove(_strategy, type(uint256).max); } /// @custom:oz-upgrades-unsafe-allow constructor constructor() initializer {} - // ============================= Governance ============================= + // ============================ View Functions ================================= + + /// @notice Returns an estimation of the current Annual Percentage Rate + function apr() external view override returns (uint256) { + return _apr(); + } + + /// @notice Returns an estimation of the current Annual Percentage Rate weighted by a factor + function weightedApr() external view override returns (uint256) { + uint256 a = _apr(); + return a * _nav(); + } + + /// @notice Helper function to get the current total of assets managed by the lender. + function nav() external view override returns (uint256) { + return _nav(); + } + + /// @notice Check if assets are currently managed by the lender + /// @dev We're considering that the strategy has no assets if it has less than 10 of the + /// underlying asset in total to avoid the case where there is dust remaining on the lending market we cannot + /// withdraw everything + function hasAssets() external view override returns (bool) { + return _nav() > 10 * wantBase; + } + + /// @notice See `nav` + function _nav() internal view returns (uint256) { + return want.balanceOf(address(this)) + underlyingBalanceStored(); + } + + /// @notice See `apr` + function _apr() internal view virtual returns (uint256); + + /// @notice Returns the current balance invested on the lender and related staking contracts + function underlyingBalanceStored() public view virtual returns (uint256 balance); + + // ============================ Governance Functions =========================== /// @notice Override this to add all tokens/tokenized positions this contract /// manages on a *persistent* basis (e.g. not just for swapping back to @@ -118,13 +163,14 @@ abstract contract GenericLenderBaseUpgradeable is IGenericLender, AccessControlU /// should be protected from sweeping in addition to `want`. function sweep(address _token, address to) external override onlyRole(GUARDIAN_ROLE) { address[] memory __protectedTokens = _protectedTokens(); - for (uint256 i = 0; i < __protectedTokens.length; i++) require(_token != __protectedTokens[i], "93"); + for (uint256 i = 0; i < __protectedTokens.length; i++) + if (_token == __protectedTokens[i]) revert ProtectedToken(); IERC20(_token).safeTransfer(to, IERC20(_token).balanceOf(address(this))); } - /// @notice Changes allowance for a contract - /// @param tokens Addresses of the tokens for which approvals should be madee + /// @notice Changes allowance of a set of tokens to addresses + /// @param tokens Addresses of the tokens for which approvals should be made /// @param spenders Addresses to approve /// @param amounts Approval amounts for each address function changeAllowance( @@ -145,7 +191,8 @@ abstract contract GenericLenderBaseUpgradeable is IGenericLender, AccessControlU /// @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. Tokens swapped should be: _stkAave -> `want` or Aave -> `want` + /// @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); diff --git a/contracts/strategies/StETHStrategy/StETHConvexStrategy.sol b/contracts/strategies/StETHStrategy/StETHConvexStrategy.sol deleted file mode 100644 index a482ad8..0000000 --- a/contracts/strategies/StETHStrategy/StETHConvexStrategy.sol +++ /dev/null @@ -1,575 +0,0 @@ -// /** -// *Submitted for verification at Etherscan.io on 2021-05-24 -// */ - -// // SPDX-License-Identifier: AGPL-3.0 - -// pragma solidity 0.6.12; -// pragma experimental ABIEncoderV2; - -// // Global Enums and Structs - -// struct StrategyParams { -// uint256 performanceFee; -// uint256 activation; -// uint256 debtRatio; -// uint256 rateLimit; -// uint256 lastReport; -// uint256 totalDebt; -// uint256 totalGain; -// uint256 totalLoss; -// } - -// // Part: IConvexDeposit - -// interface IConvexDeposit { -// // deposit into convex, receive a tokenized deposit. parameter to stake immediately (we always do this). -// function deposit( -// uint256 _pid, -// uint256 _amount, -// bool _stake -// ) external returns (bool); - -// // burn a tokenized deposit (Convex deposit tokens) to receive curve lp tokens back -// function withdraw(uint256 _pid, uint256 _amount) external returns (bool); -// } - -// // Part: IConvexRewards - -// interface IConvexRewards { -// // strategy's staked balance in the synthetix staking contract -// function balanceOf(address account) external view returns (uint256); - -// // read how much claimable CRV a strategy has -// function earned(address account) external view returns (uint256); - -// // stake a convex tokenized deposit -// function stake(uint256 _amount) external returns (bool); - -// // withdraw to a convex tokenized deposit, probably never need to use this -// function withdraw(uint256 _amount, bool _claim) external returns (bool); - -// // withdraw directly to curve LP token, this is what we primarily use -// function withdrawAndUnwrap(uint256 _amount, bool _claim) external returns (bool); - -// // claim rewards, with an option to claim extra rewards or not -// function getReward(address _account, bool _claimExtras) external returns (bool); -// } - -// // Part: ICurveFi - -// interface ICurveFi { -// function add_liquidity(uint256[2] calldata amounts, uint256 min_mint_amount) external payable; - -// function remove_liquidity_imbalance(uint256[2] calldata amounts, uint256 max_burn_amount) external; - -// function remove_liquidity_one_coin( -// uint256 _token_amount, -// int128 i, -// uint256 min_amount -// ) external; - -// function calc_token_amount(uint256[2] calldata amounts, bool is_deposit) external view returns (uint256); - -// function calc_withdraw_one_coin(uint256 amount, int128 i) external view returns (uint256); -// } - -// // Part: IExtraRewards - -// interface IExtraRewards { -// // read how much claimable LDO our strategy has -// function earned(address account) external view returns (uint256); -// } - -// // File: StrategyConvexstETH.sol - -// /* ========== CONTRACT ========== */ - -// contract StrategyConvexstETH is BaseStrategyEdited { -// using SafeERC20 for IERC20; -// using Address for address; -// using SafeMath for uint256; - -// ICurveFi public constant curve = ICurveFi(0xDC24316b9AE028F1497c275EB9192a3Ea0f67022); // Curve stETH Pool, need this for buying more pool tokens, updated -// address public crvRouter = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; // default to sushiswap, more CRV liquidity there -// address public cvxRouter = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; // default to sushiswap, more CVX liquidity there -// address public constant voter = 0xF147b8125d2ef93FB6965Db97D6746952a133934; // Yearn's veCRV voter, we send some extra CRV here -// address[] public crvPath; // path to sell CRV -// address[] public convexTokenPath; // path to sell CVX -// address public ldoRouter = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; // default to sushiswap -// address[] public ldoPath; - -// address public depositContract = 0xF403C135812408BFbE8713b5A23a04b3D48AAE31; // this is the deposit contract that all pools use, aka booster -// address public rewardsContract = 0x0A760466E1B4621579a82a39CB56Dda2F4E70f03; // This is unique to each curve pool, this one is for stETH pool -// address public extraRewardsContract = 0x008aEa5036b819B4FEAEd10b2190FBb3954981E8; // this is where LDO is stashed before it is distributed -// uint256 public pid = 25; // this is unique to each pool, this is the one for stETH, aka steCRV - -// // Swap stuff -// uint256 public keepCRV = 1000; // the percentage of CRV we re-lock for boost (in basis points) -// uint256 public constant FEE_DENOMINATOR = 10000; // with this and the above, sending 10% of our CRV yield to our voter - -// ICrvV3 public constant crv = ICrvV3(0xD533a949740bb3306d119CC777fa900bA034cd52); -// IERC20 public constant convexToken = IERC20(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); -// IERC20 public constant weth = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); -// IERC20 public constant dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); -// IERC20 public constant lido = IERC20(0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32); // updated -// IERC20 public constant stETH = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); // updated - -// uint256 public USE_SUSHI = 1; // if 1, use sushiswap as our router for CRV or CVX sells -// address private constant sushiswapRouter = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; -// address private constant uniswapRouter = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; -// address public oneInchPool = 0x1f629794B34FFb3B29FF206Be5478A52678b47ae; -// address public referral = 0x8Ef63b525fceF7f8662D98F77f5C9A86ae7dFE09; - -// // convex-specific variables -// bool public harvestExtras = true; // boolean to determine if we should always claim extra rewards during getReward (generally this should be true) -// bool public claimRewards = false; // boolean if we should always claim rewards when withdrawing, usually withdrawAndUnwrap (generally this should be false) - -// // Keep3r stuff -// uint256 public manualKeep3rHarvest; // this is used in case we want to manually trigger a keep3r harvest since they are cheaper than a strategist harvest -// uint256 public harvestProfitFactor; // the multiple that our harvest profit needs to be compared to harvest cost for it to trigger -// uint256 public tendCounter; // track our tendies -// uint256 public tendsPerHarvest; // how many tends we call before we harvest. set to 0 to never call tends. -// uint256 internal harvestNow; // 0 for false, 1 for true if we are mid-harvest. this is used to differentiate tends vs harvests in adjustPosition - -// constructor(address _vault) public BaseStrategyEdited(_vault) { -// // You can set these parameters on deployment to whatever you want -// maxReportDelay = 172800; // 2 days in seconds, if we hit this then harvestTrigger = True -// debtThreshold = 1000 * 1e18; // we shouldn't ever have debt, but set a bit of a buffer -// profitFactor = 4000; // in this strategy, profitFactor is only used for telling keep3rs when to move funds from vault to strategy (what previously was an earn call) - -// // want = crvSETH, sETH curve pool (sETH + ETH) -// want.safeApprove(address(depositContract), type(uint256).max); - -// // add approvals for crv on sushiswap and uniswap due to weird crv approval issues for setCrvRouter -// // add approvals on all tokens. since we use ETH to deposit to Curve pool, we don't need approvals for it -// IERC20(address(crv)).safeApprove(uniswapRouter, type(uint256).max); -// IERC20(address(crv)).safeApprove(sushiswapRouter, type(uint256).max); -// convexToken.safeApprove(uniswapRouter, type(uint256).max); -// convexToken.safeApprove(sushiswapRouter, type(uint256).max); -// stETH.safeApprove(address(curve), type(uint256).max); -// lido.safeApprove(uniswapRouter, type(uint256).max); -// lido.safeApprove(sushiswapRouter, type(uint256).max); -// lido.safeApprove(oneInchPool, type(uint256).max); - -// // crv token path -// crvPath = new address[](2); -// crvPath[0] = address(crv); -// crvPath[1] = address(weth); - -// // convex token path -// convexTokenPath = new address[](2); -// convexTokenPath[0] = address(convexToken); -// convexTokenPath[1] = address(weth); - -// // lido token path -// ldoPath = new address[](2); -// ldoPath[0] = address(lido); -// ldoPath[1] = address(weth); -// } - -// function name() external view override returns (string memory) { -// return "StrategyConvexstETH"; -// } - -// // total assets held by strategy. loose funds in strategy and all staked funds -// function estimatedTotalAssets() public view override returns (uint256) { -// return IConvexRewards(rewardsContract).balanceOf(address(this)).add(want.balanceOf(address(this))); -// } - -// function prepareReturn(uint256 _debtOutstanding) -// internal -// override -// returns ( -// uint256 _profit, -// uint256 _loss, -// uint256 _debtPayment -// ) -// { -// // TODO: Do stuff here to free up any returns back into `want` -// // NOTE: Return `_profit` which is value generated by all positions, priced in `want` -// // NOTE: Should try to free up at least `_debtOutstanding` of underlying position - -// // if we have anything staked, then harvest CRV and CVX from the rewards contract -// uint256 stakedTokens = IConvexRewards(rewardsContract).balanceOf(address(this)); -// uint256 claimableTokens = IConvexRewards(rewardsContract).earned(address(this)); -// if (stakedTokens > 0 && claimableTokens > 0) { -// // this claims our CRV, CVX, and any extra tokens like SNX or ANKR -// // if for some reason we don't want extra rewards, make sure we don't harvest them -// IConvexRewards(rewardsContract).getReward(address(this), harvestExtras); - -// uint256 crvBalance = crv.balanceOf(address(this)); -// uint256 convexBalance = convexToken.balanceOf(address(this)); -// uint256 lidoBalance = lido.balanceOf(address(this)); - -// uint256 _keepCRV = crvBalance.mul(keepCRV).div(FEE_DENOMINATOR); -// IERC20(address(crv)).safeTransfer(voter, _keepCRV); -// uint256 crvRemainder = crvBalance.sub(_keepCRV); - -// _sellCrv(crvRemainder); -// if (convexBalance > 0) _sellConvex(convexBalance); -// if (lidoBalance > 0) _sellLido(lidoBalance); - -// uint256 ethBalance = address(this).balance; -// uint256 stETHBalance = stETH.balanceOf(address(this)); - -// if (ethBalance > 0 || stETHBalance > 0) { -// curve.add_liquidity{ value: ethBalance }([ethBalance, stETHBalance], 0); -// } -// } -// // this is a harvest, so set our switch equal to 1 so this -// // performs as a harvest the whole way through -// harvestNow = 1; - -// // if this was the result of a manual keep3r harvest, then reset our trigger -// if (manualKeep3rHarvest == 1) manualKeep3rHarvest = 0; - -// // serious loss should never happen, but if it does (for instance, if Curve is hacked), let's record it accurately -// uint256 assets = estimatedTotalAssets(); -// uint256 debt = vault.strategies(address(this)).totalDebt; - -// // if assets are greater than debt, things are working great! -// if (assets > debt) { -// _profit = want.balanceOf(address(this)); -// } else { -// // if assets are less than debt, we are in trouble -// _loss = debt.sub(assets); -// _profit = 0; -// } - -// // debtOustanding will only be > 0 in the event of revoking or lowering debtRatio of a strategy -// if (_debtOutstanding > 0) { -// IConvexRewards(rewardsContract).withdrawAndUnwrap(Math.min(stakedTokens, _debtOutstanding), claimRewards); - -// _debtPayment = Math.min(_debtOutstanding, want.balanceOf(address(this))); -// } -// } - -// function adjustPosition(uint256 _debtOutstanding) internal override { -// if (emergencyExit) { -// return; -// } - -// if (harvestNow == 1) { -// // if this is part of a harvest call, send all of our Iron Bank pool tokens to be deposited -// uint256 _toInvest = want.balanceOf(address(this)); -// // deposit into convex and stake immediately but only if we have something to invest -// if (_toInvest > 0) IConvexDeposit(depositContract).deposit(pid, _toInvest, true); -// // since we've completed our harvest call, reset our tend counter and our harvest now -// tendCounter = 0; -// harvestNow = 0; -// } else { -// // This is our tend call. If we have anything staked, then harvest CRV and CVX from the rewards contract -// uint256 stakedTokens = IConvexRewards(rewardsContract).balanceOf(address(this)); -// uint256 claimableTokens = IConvexRewards(rewardsContract).earned(address(this)); -// if (stakedTokens > 0 && claimableTokens > 0) { -// // if for some reason we don't want extra rewards, make sure we don't harvest them -// IConvexRewards(rewardsContract).getReward(address(this), harvestExtras); - -// uint256 crvBalance = crv.balanceOf(address(this)); -// uint256 convexBalance = convexToken.balanceOf(address(this)); -// uint256 lidoBalance = lido.balanceOf(address(this)); - -// uint256 _keepCRV = crvBalance.mul(keepCRV).div(FEE_DENOMINATOR); -// IERC20(address(crv)).safeTransfer(voter, _keepCRV); -// uint256 crvRemainder = crvBalance.sub(_keepCRV); - -// _sellCrv(crvRemainder); -// if (convexBalance > 0) _sellConvex(convexBalance); -// if (lidoBalance > 0) _sellLido(lidoBalance); - -// // increase our tend counter by 1 so we can know when we should harvest again -// uint256 previousTendCounter = tendCounter; -// tendCounter = previousTendCounter.add(1); -// } -// } -// } - -// function liquidatePosition(uint256 _amountNeeded) -// internal -// override -// returns (uint256 _liquidatedAmount, uint256 _loss) -// { -// uint256 wantBal = want.balanceOf(address(this)); -// if (_amountNeeded > wantBal) { -// uint256 stakedTokens = IConvexRewards(rewardsContract).balanceOf(address(this)); -// IConvexRewards(rewardsContract).withdrawAndUnwrap( -// Math.min(stakedTokens, _amountNeeded - wantBal), -// claimRewards -// ); - -// uint256 withdrawnBal = want.balanceOf(address(this)); -// _liquidatedAmount = Math.min(_amountNeeded, withdrawnBal); - -// // if _amountNeeded != withdrawnBal, then we have an error -// if (_amountNeeded != withdrawnBal) { -// uint256 assets = estimatedTotalAssets(); -// uint256 debt = vault.strategies(address(this)).totalDebt; -// _loss = debt.sub(assets); -// } -// } else { -// // we have enough balance to cover the liquidation available -// return (_amountNeeded, 0); -// } -// } - -// // Sells our harvested CRV into the selected output (ETH). -// function _sellCrv(uint256 _crvAmount) internal { -// IUniswapV2Router02(crvRouter).swapExactTokensForETH(_crvAmount, uint256(0), crvPath, address(this), now); -// } - -// // Sells our harvested CVX into the selected output (ETH). -// function _sellConvex(uint256 _convexAmount) internal { -// IUniswapV2Router02(cvxRouter).swapExactTokensForETH( -// _convexAmount, -// uint256(0), -// convexTokenPath, -// address(this), -// now -// ); -// } - -// // Sells our harvested LDO into the selected output (ETH or stETH). -// function _sellLido(uint256 _lidoAmount) internal { -// if (ldoRouter == oneInchPool) { -// //we sell to stETH -// IOneInch(oneInchPool).swap(address(lido), address(stETH), _lidoAmount, 1, referral); -// } else { -// IUniswapV2Router02(ldoRouter).swapExactTokensForETH(_lidoAmount, uint256(0), ldoPath, address(this), now); -// } -// } - -// // in case we need to exit into the convex deposit token, this will allow us to do that -// // make sure to check claimRewards before this step if needed -// // plan to have gov sweep convex deposit tokens from strategy after this -// function withdrawToConvexDepositTokens() external onlyAuthorized { -// uint256 stakedTokens = IConvexRewards(rewardsContract).balanceOf(address(this)); -// IConvexRewards(rewardsContract).withdraw(stakedTokens, claimRewards); -// } - -// // migrate our want token to a new strategy if needed, make sure to check claimRewards first -// // also send over any CRV or CVX that is claimed; for migrations we definitely want to claim -// function prepareMigration(address _newStrategy) internal override { -// uint256 stakedTokens = IConvexRewards(rewardsContract).balanceOf(address(this)); -// if (stakedTokens > 0) { -// IConvexRewards(rewardsContract).withdrawAndUnwrap(stakedTokens, claimRewards); -// } -// IERC20(address(crv)).safeTransfer(_newStrategy, crv.balanceOf(address(this))); -// IERC20(address(convexToken)).safeTransfer(_newStrategy, convexToken.balanceOf(address(this))); -// IERC20(address(lido)).safeTransfer(_newStrategy, lido.balanceOf(address(this))); -// } - -// // we don't want for these tokens to be swept out. We allow gov to sweep out cvx vault tokens; we would only be holding these if things were really, really rekt. -// function protectedTokens() internal view override returns (address[] memory) { -// address[] memory protected = new address[](3); -// protected[0] = address(convexToken); -// protected[1] = address(crv); -// protected[2] = address(lido); - -// return protected; -// } - -// /* ========== KEEP3RS ========== */ - -// function harvestTrigger(uint256 callCostinEth) public view override returns (bool) { -// StrategyParams memory params = vault.strategies(address(this)); - -// // have a manual toggle switch if needed since keep3rs are more efficient than manual harvest -// if (manualKeep3rHarvest == 1) return true; - -// // Should not trigger if Strategy is not activated -// if (params.activation == 0) return false; - -// // Should not trigger if we haven't waited long enough since previous harvest -// if (block.timestamp.sub(params.lastReport) < minReportDelay) return false; - -// // Should trigger if hasn't been called in a while -// if (block.timestamp.sub(params.lastReport) >= maxReportDelay) return true; - -// // If some amount is owed, pay it back -// // NOTE: Since debt is based on deposits, it makes sense to guard against large -// // changes to the value from triggering a harvest directly through user -// // behavior. This should ensure reasonable resistance to manipulation -// // from user-initiated withdrawals as the outstanding debt fluctuates. -// uint256 outstanding = vault.debtOutstanding(); -// if (outstanding > debtThreshold) return true; - -// // Check for profits and losses -// uint256 total = estimatedTotalAssets(); -// // Trigger if we have a loss to report -// if (total.add(debtThreshold) < params.totalDebt) return true; - -// // no need to spend the gas to harvest every time; tend is much cheaper -// if (tendCounter < tendsPerHarvest) return false; - -// // Trigger if it makes sense for the vault to send funds idle funds from the vault to the strategy, or to harvest. -// uint256 profit = 0; -// if (total > params.totalDebt) profit = total.sub(params.totalDebt); // We've earned a profit! - -// // calculate how much the call costs in dollars (converted from ETH) -// uint256 callCost = ethToDollaBill(callCostinEth); - -// // check if it makes sense to send funds from vault to strategy -// uint256 credit = vault.creditAvailable(); -// if (profitFactor.mul(callCost) < credit.add(profit)) return true; - -// // calculate how much profit we'll make if we harvest -// uint256 harvestProfit = claimableProfitInDolla(); - -// // check if we make enough from this to justify the harvest call -// return (harvestProfitFactor.mul(callCost)) < harvestProfit; -// } - -// // set what will trigger keepers to call tend, which will harvest and sell CRV for optimal asset but not deposit or report profits -// function tendTrigger(uint256 callCostinEth) public view override returns (bool) { -// // we need to call a harvest every once in a while, every tendsPerHarvest number of tends -// if (tendCounter >= tendsPerHarvest) return false; - -// StrategyParams memory params = vault.strategies(address(this)); -// // Tend should trigger once it has been the minimum time between harvests divided by 1+tendsPerHarvest to space out tends equally -// // we multiply this number by the current tendCounter+1 to know where we are in time -// // we are assuming here that keepers will essentially call tend as soon as this is true -// if ( -// block.timestamp.sub(params.lastReport) > -// (minReportDelay.div((tendCounter.add(1)).mul(tendsPerHarvest.add(1)))) -// ) return true; -// } - -// // convert our keeper's eth cost into dai -// function ethToDollaBill(uint256 _ethAmount) internal view returns (uint256) { -// address[] memory ethPath = new address[](2); -// ethPath[0] = address(weth); -// ethPath[1] = address(dai); - -// uint256[] memory callCostInDai = IUniswapV2Router02(crvRouter).getAmountsOut(_ethAmount, ethPath); - -// return callCostInDai[callCostInDai.length - 1]; -// } - -// // convert our unsold CRV and CVX into USD profit for our keep3r -// function claimableProfitInDolla() internal view returns (uint256) { -// uint256 claimableCrv = IConvexRewards(rewardsContract).earned(address(this)); // how much CRV we can claim from the staking contract - -// // calculations pulled directly from CVX's contract for minting CVX per CRV claimed -// uint256 totalCliffs = 1000; -// uint256 maxSupply = 100 * 1000000 * 1e18; // 100mil -// uint256 reductionPerCliff = 100000000000000000000000; // 100,000 -// uint256 supply = convexToken.totalSupply(); -// uint256 mintableCvx; - -// uint256 cliff = supply.div(reductionPerCliff); -// //mint if below total cliffs -// if (cliff < totalCliffs) { -// //for reduction% take inverse of current cliff -// uint256 reduction = totalCliffs.sub(cliff); -// //reduce -// mintableCvx = claimableCrv.mul(reduction).div(totalCliffs); - -// //supply cap check -// uint256 amtTillMax = maxSupply.sub(supply); -// if (mintableCvx > amtTillMax) { -// mintableCvx = amtTillMax; -// } -// } - -// uint256 crvValue; -// if (claimableCrv > 0) { -// uint256[] memory crvSwap = IUniswapV2Router02(crvRouter).getAmountsOut(claimableCrv, crvPath); -// crvValue = crvSwap[1]; -// } - -// uint256 cvxValue; -// if (mintableCvx > 0) { -// uint256[] memory cvxSwap = IUniswapV2Router02(cvxRouter).getAmountsOut(mintableCvx, convexTokenPath); -// cvxValue = cvxSwap[1]; -// } - -// uint256 ldoValue; -// uint256 claimableLdo = IExtraRewards(extraRewardsContract).earned(address(this)); -// if (claimableLdo > 0) { -// uint256[] memory ldoSwap = IUniswapV2Router02(sushiswapRouter).getAmountsOut(claimableLdo, ldoPath); -// ldoValue = ldoSwap[1]; -// } -// return (crvValue.add(cvxValue).add(ldoValue)).mul(ethToDollaBill(1e18).div(1e18)); // dollar value of our harvest -// } - -// // set number of tends before we call our next harvest -// function setTendsPerHarvest(uint256 _tendsPerHarvest) external onlyAuthorized { -// tendsPerHarvest = _tendsPerHarvest; -// } - -// // set this to 1 if we want our keep3rs to manually harvest the strategy; keep3r harvest is more cost-efficient than strategist harvest -// function setKeep3rHarvest(uint256 _setKeep3rHarvest) external onlyAuthorized { -// manualKeep3rHarvest = _setKeep3rHarvest; -// } - -// /* ========== SETTERS ========== */ - -// // These functions are useful for setting parameters of the strategy that may need to be adjusted. - -// // Set the amount of CRV to be locked in Yearn's veCRV voter from each harvest. Default is 10%. -// function setKeepCRV(uint256 _keepCRV) external onlyGovernance { -// keepCRV = _keepCRV; -// } - -// // 1 is for TRUE value and 0 for FALSE to keep in sync with binary convention -// // Use SushiSwap for CRV Router = 1; -// // Use Uniswap for CRV Router = 0 (or anything else); -// function setCrvRouter(uint256 _isSushiswap) external onlyAuthorized { -// if (_isSushiswap == USE_SUSHI) { -// crvRouter = sushiswapRouter; -// } else { -// crvRouter = uniswapRouter; -// } -// } - -// // 1 is for TRUE value and 0 for FALSE to keep in sync with binary convention -// // Use SushiSwap for CVX Router = 1; -// // Use Uniswap for CVX Router = 0 (or anything else); -// function setCvxRouter(uint256 _isSushiswap) external onlyAuthorized { -// if (_isSushiswap == USE_SUSHI) { -// cvxRouter = sushiswapRouter; -// } else { -// cvxRouter = uniswapRouter; -// } -// } - -// // 0 for sushi (default), 1 for 1inch, 2 for uniswap (probably will never use) -// function setLdoRouter(uint256 _router) external onlyAuthorized { -// if (_router == 0) { -// ldoRouter = sushiswapRouter; -// } else if (_router == 1) { -// ldoRouter = oneInchPool; -// } else { -// ldoRouter = uniswapRouter; -// } -// } - -// // update the address for our 1inch pool (used to swap LDO to stETH) -// function update1InchPoolAddress(address newAddress) public onlyGovernance { -// oneInchPool = newAddress; -// } - -// // update our referral address for 1inch -// function updateReferral(address _referral) public onlyAuthorized { -// referral = _referral; -// } - -// // Unless contract is borked for some reason, we should always harvest extra tokens -// function setHarvestExtras(bool _harvestExtras) external onlyAuthorized { -// harvestExtras = _harvestExtras; -// } - -// // We usually don't need to claim rewards on withdrawals, but might change our mind for migrations etc -// function setClaimRewards(bool _claimRewards) external onlyAuthorized { -// claimRewards = _claimRewards; -// } - -// // set this to the multiple we want to make on our harvests vs the cost -// function setHarvestProfitFactor(uint256 _harvestProfitFactor) external onlyAuthorized { -// harvestProfitFactor = _harvestProfitFactor; -// } - -// // allow our contract to receive ETH (gimme dat future of france, plz) -// receive() external payable {} -// } diff --git a/deploy/GenericAaveFraxStaker.ts b/deploy/GenericAaveFraxStaker.ts index bc38979..c39988f 100644 --- a/deploy/GenericAaveFraxStaker.ts +++ b/deploy/GenericAaveFraxStaker.ts @@ -18,8 +18,7 @@ const func: DeployFunction = async ({ deployments, ethers }) => { let keeper: string; let proxyAdmin: string; - // if fork we suppose that we are in mainnet - let json = (await import('./networks/mainnet.json')) as any; + // If fork we suppose that we are in mainnet if (!network.live) { guardian = CONTRACTS_ADDRESSES[ChainId.MAINNET].Guardian!; governor = CONTRACTS_ADDRESSES[ChainId.MAINNET].GovernanceMultiSig! as string; @@ -33,7 +32,6 @@ const func: DeployFunction = async ({ deployments, ethers }) => { proxyAdmin = CONTRACTS_ADDRESSES[network.config.chainId as ChainId].ProxyAdmin! as string; strategyAddress = CONTRACTS_ADDRESSES[network.config.chainId as ChainId].agEUR?.collaterals?.[collateralName] ?.Strategies?.GenericOptimisedLender as string; - json = await import('./networks/' + network.name + '.json'); keeper = fakeKeeper.address; } diff --git a/deploy/StETHStrategy.ts b/deploy/StETHStrategy.ts index 5738ff4..7d53198 100644 --- a/deploy/StETHStrategy.ts +++ b/deploy/StETHStrategy.ts @@ -11,20 +11,18 @@ const func: DeployFunction = async ({ deployments, ethers }) => { const collats = ['WETH']; let guardian: string; - let ANGLE: string; let governor: string; let proxyAdmin: string; // if fork we suppose that we are in mainnet + // eslint-disable-next-line let json = (await import('./networks/mainnet.json')) as any; if (!network.live) { guardian = CONTRACTS_ADDRESSES[ChainId.MAINNET].Guardian!; - ANGLE = CONTRACTS_ADDRESSES[ChainId.MAINNET].ANGLE!; governor = CONTRACTS_ADDRESSES[ChainId.MAINNET].GovernanceMultiSig! as string; proxyAdmin = CONTRACTS_ADDRESSES[ChainId.MAINNET].ProxyAdmin! as string; } else { guardian = CONTRACTS_ADDRESSES[network.config.chainId as ChainId].Guardian!; - ANGLE = CONTRACTS_ADDRESSES[network.config.chainId as ChainId].ANGLE!; governor = CONTRACTS_ADDRESSES[network.config.chainId as ChainId].GovernanceMultiSig! as string; proxyAdmin = CONTRACTS_ADDRESSES[network.config.chainId as ChainId].ProxyAdmin! as string; json = await import('./networks/' + network.name + '.json'); diff --git a/package.json b/package.json index b5cf605..5a2236d 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "node:fork:strategy:fraxStaking": "hardhat deploy --network localhost --tags lenderFraxStaking", "prettier": "yarn prettier:js && yarn prettier:sol", "prettier:sol": "prettier --write 'contracts/**/*.sol'", - "prettier:js": "prettier --write 'scripts/**/*.ts' 'scripts/*.ts' 'test/**/*.{js,ts}' 'deploy/*.{js,ts}' '*.{js,ts}'", + "prettier:js": "prettier --write 'scripts/**/*.ts' 'test/**/*.{js,ts}' 'deploy/*.{js,ts}' '*.{js,ts}'", "size": "hardhat size-contracts", "test": "hardhat test", "test:fork:stethStrat": "hardhat run --network localhost ./scripts/mainnet-fork/stETHStrategy/testDeploy.ts", diff --git a/scripts/mainnet-fork/checkUpgradeImplementation.ts b/scripts/mainnet-fork/checkUpgradeImplementation.ts index 36180e5..7bbb861 100644 --- a/scripts/mainnet-fork/checkUpgradeImplementation.ts +++ b/scripts/mainnet-fork/checkUpgradeImplementation.ts @@ -10,7 +10,9 @@ import { expect } from 'chai'; export async function deploy( contractName: string, + // eslint-disable-next-line args: any[] = [], + // eslint-disable-next-line options: Record & { libraries?: Record } = {}, ): Promise { const factory = await ethers.getContractFactory(contractName, options); diff --git a/scripts/mainnet-fork/stETHStrategy/imbalanceCurvePool.ts b/scripts/mainnet-fork/stETHStrategy/imbalanceCurvePool.ts index 76d9b25..d8b8d5d 100644 --- a/scripts/mainnet-fork/stETHStrategy/imbalanceCurvePool.ts +++ b/scripts/mainnet-fork/stETHStrategy/imbalanceCurvePool.ts @@ -44,6 +44,7 @@ async function main() { const { deployer, user: richStETH } = await ethers.getNamedSigners(); // If we're in mainnet fork, we're using the json.mainnet address + // eslint-disable-next-line const json = (await import('../../../deploy/networks/mainnet.json')) as any; const governance = CONTRACTS_ADDRESSES[ChainId.MAINNET].GovernanceMultiSig! as string; diff --git a/scripts/mainnet-fork/stETHStrategy/testDeploy.ts b/scripts/mainnet-fork/stETHStrategy/testDeploy.ts index 2bf0189..e1b85ed 100644 --- a/scripts/mainnet-fork/stETHStrategy/testDeploy.ts +++ b/scripts/mainnet-fork/stETHStrategy/testDeploy.ts @@ -26,13 +26,14 @@ import { randomWithdraw, wait, } from '../../../test/utils-interaction'; -import { ERC20, ERC20__factory, StETHStrategy, StETHStrategy__factory } from '../../../typechain'; +import { StETHStrategy, StETHStrategy__factory } from '../../../typechain'; async function main() { // =============== Simulation parameters ==================== const { deployer } = await ethers.getNamedSigners(); // If we're in mainnet fork, we're using the json.mainnet address + // eslint-disable-next-line let json = (await import('../../../deploy/networks/mainnet.json')) as any; if (network.live) { json = await import('../../../deploy/networks/' + network.name + '.json'); diff --git a/scripts/mainnet-fork/upgradeFraxAaveLender.ts b/scripts/mainnet-fork/upgradeFraxAaveLender.ts index c3dc827..535d875 100644 --- a/scripts/mainnet-fork/upgradeFraxAaveLender.ts +++ b/scripts/mainnet-fork/upgradeFraxAaveLender.ts @@ -9,9 +9,8 @@ import { StableMasterFront__factory, } from '@angleprotocol/sdk/dist/constants/interfaces'; -import { expect } from '../../test/test-utils/chai-setup'; import { CONTRACTS_ADDRESSES, ChainId } from '@angleprotocol/sdk'; -import { network, ethers, deployments } from 'hardhat'; +import { network, ethers } from 'hardhat'; import { parseUnits } from 'ethers/lib/utils'; import { logGeneralInfo, @@ -21,42 +20,21 @@ import { randomWithdraw, wait, } from '../../test/utils-interaction'; -import { - GenericAaveFraxStaker, - GenericAaveFraxStaker__factory, - GenericAaveNoStaker, - GenericAaveNoStaker__factory, - OptimizerAPRStrategy, - OptimizerAPRStrategy__factory, -} from '../../typechain'; +import { OptimizerAPRStrategy, OptimizerAPRStrategy__factory } from '../../typechain'; import { time } from '../../test/test-utils/helpers'; import { DAY } from '../../test/contants'; async function main() { // =============== Simulation parameters ==================== - const { deployer, keeper: fakeKeeper } = await ethers.getNamedSigners(); - const collat = 'FRAX'; - - const stableName = 'EUR'; + const { deployer } = await ethers.getNamedSigners(); const collateralName = 'FRAX'; let strategyAddress: string; - let oldLenderAddress: string; - let newLenderAddress: string; let stableMasterAddress: string; let poolManagerAddress: string; let perpetualManagerAddress: string; - let guardian: string; - let governor: string; - let keeper: string; - let proxyAdmin: string; - // if fork we suppose that we are in mainnet - let json = (await import('../../deploy/networks/mainnet.json')) as any; if (!network.live) { - guardian = CONTRACTS_ADDRESSES[ChainId.MAINNET].Guardian!; - governor = CONTRACTS_ADDRESSES[ChainId.MAINNET].GovernanceMultiSig! as string; - proxyAdmin = CONTRACTS_ADDRESSES[ChainId.MAINNET].ProxyAdmin! as string; stableMasterAddress = CONTRACTS_ADDRESSES[ChainId.MAINNET].agEUR?.StableMaster as string; perpetualManagerAddress = CONTRACTS_ADDRESSES[ChainId.MAINNET].agEUR?.collaterals?.[collateralName] ?.PerpetualManager as string; @@ -64,12 +42,7 @@ async function main() { ?.PoolManager as string; strategyAddress = CONTRACTS_ADDRESSES[ChainId.MAINNET].agEUR?.collaterals?.[collateralName]?.Strategies ?.GenericOptimisedLender as string; - oldLenderAddress = CONTRACTS_ADDRESSES[ChainId.MAINNET].agEUR?.collaterals?.[collat]?.GenericAave as string; - keeper = '0xcC617C6f9725eACC993ac626C7efC6B96476916E'; } else { - guardian = CONTRACTS_ADDRESSES[network.config.chainId as ChainId].Guardian!; - governor = CONTRACTS_ADDRESSES[network.config.chainId as ChainId].GovernanceMultiSig! as string; - proxyAdmin = CONTRACTS_ADDRESSES[network.config.chainId as ChainId].ProxyAdmin! as string; stableMasterAddress = CONTRACTS_ADDRESSES[network.config.chainId as ChainId].agEUR?.StableMaster as string; perpetualManagerAddress = CONTRACTS_ADDRESSES[network.config.chainId as ChainId].agEUR?.collaterals?.[ collateralName @@ -78,14 +51,9 @@ async function main() { ?.PoolManager as string; strategyAddress = CONTRACTS_ADDRESSES[network.config.chainId as ChainId].agEUR?.collaterals?.[collateralName] ?.Strategies?.GenericOptimisedLender as string; - oldLenderAddress = CONTRACTS_ADDRESSES[network.config.chainId as ChainId].agEUR?.collaterals?.[collat] - ?.GenericAave as string; - - json = await import('./networks/' + network.name + '.json'); - keeper = fakeKeeper.address; } - newLenderAddress = (await deployments.get(`GenericAave_${stableName}_${collateralName}_Staker`)).address; + // const newLenderAddress = (await deployments.get(`GenericAave_${stableName}_${collateralName}_Staker`)).address; const stableMaster = new ethers.Contract( stableMasterAddress, @@ -102,6 +70,7 @@ async function main() { OptimizerAPRStrategy__factory.createInterface(), deployer, ) as OptimizerAPRStrategy; + /* const oldLender = new ethers.Contract( oldLenderAddress, GenericAaveNoStaker__factory.createInterface(), @@ -112,6 +81,7 @@ async function main() { GenericAaveFraxStaker__factory.createInterface(), deployer, ) as GenericAaveFraxStaker; + */ const poolManager = new ethers.Contract(poolManagerAddress, PoolManager_Interface, deployer) as PoolManager; await network.provider.send('hardhat_setBalance', [deployer.address, parseUnits('1000000', 18).toHexString()]); diff --git a/test/optimizerApr/fraxStaking.test.ts b/test/optimizerApr/fraxStaking.test.ts index 25f4bd9..860bb7a 100644 --- a/test/optimizerApr/fraxStaking.test.ts +++ b/test/optimizerApr/fraxStaking.test.ts @@ -21,10 +21,10 @@ import { gwei } from '../../utils/bignumber'; import { deploy, deployUpgradeable, impersonate } from '../test-utils'; import { ethers, network } from 'hardhat'; import { expect } from '../test-utils/chai-setup'; -import { parseUnits } from 'ethers/lib/utils'; +import { parseUnits, parseEther } from 'ethers/lib/utils'; import { logBN, setTokenBalanceFor } from '../utils-interaction'; import { DAY } from '../contants'; -import { latestTime, time } from '../test-utils/helpers'; +import { latestTime, time, ZERO_ADDRESS } from '../test-utils/helpers'; async function initStrategy( governor: SignerWithAddress, @@ -69,6 +69,7 @@ let governor: SignerWithAddress, guardian: SignerWithAddress, user: SignerWithAd let strategy: OptimizerAPRStrategy; let token: ERC20; let aToken: ERC20; +let frax: ERC20; let nativeRewardToken: MockToken; let tokenDecimal: number; let manager: PoolManager; @@ -77,11 +78,15 @@ let stkAave: IStakedAave; let aFraxStakingContract: IMockFraxUnifiedFarm; let oracleNativeReward: AggregatorV3Interface; let oracleStkAave: AggregatorV3Interface; +let oneInch: string; const lockerStakeDAO = '0xCd3a267DE09196C48bbB1d9e842D7D7645cE448f'; const fraxTimelock = '0x8412ebf45bAC1B340BbE8F318b928C466c4E39CA'; const guardianRole = ethers.utils.solidityKeccak256(['string'], ['GUARDIAN_ROLE']); +const keeperRole = ethers.utils.solidityKeccak256(['string'], ['KEEPER_ROLE']); let guardianError: string; +let keeperError: string; +let stkAaveHolder: string; // Start test block describe('OptimizerAPR - lenderAaveFraxStaker', () => { @@ -98,9 +103,11 @@ describe('OptimizerAPR - lenderAaveFraxStaker', () => { ], }); ({ governor, guardian, user, keeper } = await ethers.getNamedSigners()); + stkAaveHolder = '0x32B61Bb22Cbe4834bc3e73DcE85280037D944a4D'; 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', @@ -129,6 +136,7 @@ describe('OptimizerAPR - lenderAaveFraxStaker', () => { )) as AggregatorV3Interface; guardianError = `AccessControl: account ${user.address.toLowerCase()} is missing role ${guardianRole}`; + keeperError = `AccessControl: account ${user.address.toLowerCase()} is missing role ${keeperRole}`; manager = (await deploy('PoolManager', [token.address, governor.address, guardian.address])) as PoolManager; @@ -143,116 +151,301 @@ describe('OptimizerAPR - lenderAaveFraxStaker', () => { true, DAY, )); + oneInch = '0x1111111254fb6c44bAC0beD2854e76F90643097d'; }); describe('Contructor', () => { - it('reverts - too small saking period', async () => { + 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), ).to.be.revertedWith('TooSmallStakingPeriod()'); + await expect( + lenderAave.initialize( + strategy.address, + 'test', + true, + [governor.address], + guardian.address, + [keeper.address], + 0, + ), + ).to.be.revertedWith('Initializable: contract is already initialized'); }); }); describe('Parameters', () => { - it('stakingPeriod', async () => { + it('success - well set', async () => { expect(await lenderAave.stakingPeriod()).to.be.equal(BigNumber.from(DAY.toString())); + expect(await lenderAave.cooldownSeconds()).to.be.equal(await stkAave.COOLDOWN_SECONDS()); + expect(await lenderAave.unstakeWindow()).to.be.equal(await stkAave.UNSTAKE_WINDOW()); + expect(await lenderAave.isIncentivised()).to.be.equal(true); + expect(await lenderAave.cooldownStkAave()).to.be.equal(true); + expect(await lenderAave.poolManager()).to.be.equal(manager.address); + expect(await lenderAave.want()).to.be.equal(token.address); + expect(await lenderAave.wantBase()).to.be.equal(parseUnits('1', await token.decimals())); + expect(await stkAave.allowance(lenderAave.address, oneInch)).to.be.equal(ethers.constants.MaxUint256); }); }); describe('AccessControl', () => { - it('setLockTime - reverts Guardian', async () => { + it('reverts - guardian only functions', async () => { await expect(lenderAave.connect(user).setLockTime(ethers.constants.Zero)).to.be.revertedWith(guardianError); - }); - it('setProxyBoost - reverts Guardian', async () => { await expect(lenderAave.connect(user).setProxyBoost(ethers.constants.AddressZero)).to.be.revertedWith( guardianError, ); - }); - it('changeAllowance - reverts Guardian', async () => { await expect( lenderAave .connect(user) .changeAllowance([aToken.address], [aFraxStakingContract.address], [ethers.constants.Zero]), ).to.be.revertedWith(guardianError); + await expect(lenderAave.connect(user).sweep(ZERO_ADDRESS, governor.address)).to.be.revertedWith(guardianError); }); - it('changeAllowance - reverts length', async () => { - await expect( - lenderAave.connect(user).changeAllowance([], [aFraxStakingContract.address], [ethers.constants.Zero]), - ).to.be.revertedWith(guardianError); + }); + + describe('Keeper functions', () => { + describe('setAavePoolVariables', () => { + it('success - values well set', async () => { + await lenderAave.setAavePoolVariables(); + expect(await lenderAave.cooldownSeconds()).to.be.equal(await stkAave.COOLDOWN_SECONDS()); + expect(await lenderAave.unstakeWindow()).to.be.equal(await stkAave.UNSTAKE_WINDOW()); + }); }); - it('changeAllowance - reverts length', async () => { - await expect(lenderAave.connect(user).changeAllowance([], [], [ethers.constants.Zero])).to.be.revertedWith( - guardianError, - ); + describe('cooldown', () => { + it('reverts - when not keeper', async () => { + await expect(lenderAave.connect(user).cooldown()).to.be.revertedWith(keeperError); + await expect(lenderAave.connect(keeper).cooldown()).to.be.revertedWith('INVALID_BALANCE_ON_COOLDOWN'); + }); + it('success - cooldown activated', async () => { + await impersonate(stkAaveHolder, async acc => { + await network.provider.send('hardhat_setBalance', [ + stkAaveHolder, + utils.parseEther('1').toHexString().replace('0x0', '0x'), + ]); + await (await stkAave.connect(acc).transfer(lenderAave.address, parseEther('1'))).wait(); + }); + await lenderAave.connect(keeper).cooldown(); + expect(await stkAave.stakersCooldowns(lenderAave.address)).to.be.equal(await latestTime()); + }); + }); + describe('sellRewards', () => { + it('reverts - when not keeper or incorrect payload', async () => { + await expect(lenderAave.connect(user).sellRewards(0, '0x')).to.be.revertedWith(keeperError); + await expect(lenderAave.connect(keeper).sellRewards(0, '0x')).to.be.reverted; + // Payload for swapping 10**(-6) AAVE to USDC + const payload = + '0x2e95b6c80000000000000000000000007fc66500c84a76ad7e9c93437bfc5ac33e2ddae9000000000000000000000000000000000000000000000000000000e8d4a510' + + '00000000000000000000000000000000000000000000000000000000000000007' + + '200000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002000' + + '00000000000003b6d03409909d09656fce21d1904f662b99382b887a9c5da80000000000000003b6d0340466d82b7d15af812fb6c788d7b15c635fa933499cfee7c08'; + await expect(lenderAave.connect(keeper).sellRewards(0, payload)).to.be.reverted; + }); + it('reverts - when 1Inch router was not approved', async () => { + await setTokenBalanceFor(token, strategy.address, 1000000); + await (await strategy.connect(keeper)['harvest()']()).wait(); + + // let days pass to have a non negligible gain + await time.increase(DAY * 7); + + await (await lenderAave.connect(user).claimRewardsExternal()).wait(); + expect(await nativeRewardToken.balanceOf(lenderAave.address)).to.be.gte(parseUnits('0', tokenDecimal)); + expect(await stkAave.balanceOf(lenderAave.address)).to.be.gte(parseUnits('0', tokenDecimal)); + // Payload to swap 100 FXS to FRAX + const payload = + '0x2e95b6c80000000000000000000000003432b6a60d23ca0dfca7761b7ab56459d9c964d00000000000000000000000000000000000000000000000056bc7' + + '5e2d6310000000000000000000000000000000000000000000000000005b0bf8af86b3d154cc00000000000000000000000000000000000000000000000000' + + '00000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000003b6d0340e1573b9d29e2183' + + 'b1af0e743dc2754979a40d237cfee7c08'; + await expect(lenderAave.connect(keeper).sellRewards(0, payload)).to.be.reverted; + }); + it('success - FXS token swap', async () => { + await setTokenBalanceFor(token, strategy.address, 1000000); + await (await strategy.connect(keeper)['harvest()']()).wait(); + + // let days pass to have a non negligible gain + await time.increase(DAY * 7); + + await (await lenderAave.connect(user).claimRewardsExternal()).wait(); + const balanceBefore = await nativeRewardToken.balanceOf(lenderAave.address); + expect(balanceBefore).to.be.gte(parseUnits('0', tokenDecimal)); + expect(await stkAave.balanceOf(lenderAave.address)).to.be.gte(parseUnits('0', tokenDecimal)); + // Payload to swap 100 FXS to FRAX + const payload = + '0x2e95b6c80000000000000000000000003432b6a60d23ca0dfca7761b7ab56459d9c964d00000000000000000000000000000000000000000000000056bc7' + + '5e2d6310000000000000000000000000000000000000000000000000005b0bf8af86b3d154cc00000000000000000000000000000000000000000000000000' + + '00000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000003b6d0340e1573b9d29e2183' + + 'b1af0e743dc2754979a40d237cfee7c08'; + await lenderAave + .connect(guardian) + .changeAllowance( + [nativeRewardToken.address], + ['0x1111111254fb6c44bAC0beD2854e76F90643097d'], + [parseEther('1000')], + ); + await lenderAave.connect(keeper).sellRewards(0, payload); + expect(await nativeRewardToken.balanceOf(lenderAave.address)).to.be.equal(balanceBefore.sub(parseEther('100'))); + expect(await frax.balanceOf(lenderAave.address)).to.be.gt(parseEther('100')); + }); + it('reverts - FXS token swap but looses from slippage protection', async () => { + await setTokenBalanceFor(token, strategy.address, 1000000); + await (await strategy.connect(keeper)['harvest()']()).wait(); + + // let days pass to have a non negligible gain + await time.increase(DAY * 7); + + await (await lenderAave.connect(user).claimRewardsExternal()).wait(); + const balanceBefore = await nativeRewardToken.balanceOf(lenderAave.address); + expect(balanceBefore).to.be.gte(parseUnits('0', tokenDecimal)); + expect(await stkAave.balanceOf(lenderAave.address)).to.be.gte(parseUnits('0', tokenDecimal)); + // Payload to swap 100 FXS to FRAX + const payload = + '0x2e95b6c80000000000000000000000003432b6a60d23ca0dfca7761b7ab56459d9c964d00000000000000000000000000000000000000000000000056bc7' + + '5e2d6310000000000000000000000000000000000000000000000000005b0bf8af86b3d154cc00000000000000000000000000000000000000000000000000' + + '00000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000003b6d0340e1573b9d29e2183' + + 'b1af0e743dc2754979a40d237cfee7c08'; + await lenderAave + .connect(guardian) + .changeAllowance( + [nativeRewardToken.address], + ['0x1111111254fb6c44bAC0beD2854e76F90643097d'], + [parseEther('1000')], + ); + await expect(lenderAave.connect(keeper).sellRewards(parseEther('10000000'), payload)).to.be.revertedWith( + 'TooSmallAmount', + ); + }); }); }); describe('Governance functions', () => { - it('setLockTime - revert', async () => { - await expect(lenderAave.connect(guardian).setLockTime(ethers.constants.Zero)).to.be.revertedWith( - 'StakingPeriodTooSmall', - ); + describe('setLockTime', () => { + it('reverts - too small staking period', async () => { + await expect(lenderAave.connect(guardian).setLockTime(ethers.constants.Zero)).to.be.revertedWith( + 'StakingPeriodTooSmall', + ); + }); + it('success - staking period updated', async () => { + await lenderAave.connect(guardian).setLockTime(parseUnits((2 * DAY).toString(), 0)); + expect(await lenderAave.stakingPeriod()).to.be.equal(parseUnits((2 * DAY).toString(), 0)); + }); }); - it('setLockTime', async () => { - await lenderAave.connect(guardian).setLockTime(parseUnits((2 * DAY).toString(), 0)); - expect(await lenderAave.stakingPeriod()).to.be.equal(parseUnits((2 * DAY).toString(), 0)); + describe('setProxyBoost', () => { + it('success - proxy boost set', async () => { + const veFXSMultiplierBefore = await aFraxStakingContract.veFXSMultiplier(lenderAave.address); + await impersonate(fraxTimelock, async acc => { + await network.provider.send('hardhat_setBalance', [ + fraxTimelock, + utils.parseEther('1').toHexString().replace('0x0', '0x'), + ]); + await (await aFraxStakingContract.connect(acc).toggleValidVeFXSProxy(lockerStakeDAO)).wait(); + }); + await impersonate(lockerStakeDAO, async acc => { + await network.provider.send('hardhat_setBalance', [ + lockerStakeDAO, + utils.parseEther('1').toHexString().replace('0x0', '0x'), + ]); + await (await aFraxStakingContract.connect(acc).proxyToggleStaker(lenderAave.address)).wait(); + }); + await lenderAave.connect(guardian).setProxyBoost(lockerStakeDAO); + + const veFXSMultiplierAfter = await aFraxStakingContract.veFXSMultiplier(lenderAave.address); + expect(veFXSMultiplierAfter).to.be.gt(veFXSMultiplierBefore); + }); }); - it('setProxyBoost', async () => { - const veFXSMultiplierBefore = await aFraxStakingContract.veFXSMultiplier(lenderAave.address); - await impersonate(fraxTimelock, async acc => { - await network.provider.send('hardhat_setBalance', [ - fraxTimelock, - utils.parseEther('1').toHexString().replace('0x0', '0x'), - ]); - await (await aFraxStakingContract.connect(acc).toggleValidVeFXSProxy(lockerStakeDAO)).wait(); + describe('sweep', () => { + it('reverts - protected token', async () => { + await expect(lenderAave.connect(guardian).sweep(token.address, guardian.address)).to.be.revertedWith( + 'ProtectedToken', + ); }); - await impersonate(lockerStakeDAO, async acc => { - await network.provider.send('hardhat_setBalance', [ - lockerStakeDAO, - utils.parseEther('1').toHexString().replace('0x0', '0x'), - ]); - await (await aFraxStakingContract.connect(acc).proxyToggleStaker(lenderAave.address)).wait(); + it('success - balance correctly swept', async () => { + await setTokenBalanceFor(token, strategy.address, 1000000); + await (await strategy.connect(keeper)['harvest()']()).wait(); + + // let days pass to have a non negligible gain + await time.increase(DAY * 7); + // Accumulating stkAave and FXS + await (await lenderAave.connect(user).claimRewardsExternal()).wait(); + const balanceBefore = await nativeRewardToken.balanceOf(lenderAave.address); + expect(balanceBefore).to.be.gte(parseUnits('0', tokenDecimal)); + expect(await stkAave.balanceOf(lenderAave.address)).to.be.gte(parseUnits('0', tokenDecimal)); + expect(await nativeRewardToken.balanceOf(guardian.address)).to.be.equal(0); + expect(await stkAave.balanceOf(guardian.address)).to.be.equal(0); + await lenderAave.connect(guardian).sweep(nativeRewardToken.address, guardian.address); + await lenderAave.connect(guardian).sweep(stkAave.address, guardian.address); + expect(await nativeRewardToken.balanceOf(guardian.address)).to.be.gte(parseUnits('0', tokenDecimal)); + expect(await stkAave.balanceOf(guardian.address)).to.be.gte(parseUnits('0', tokenDecimal)); }); - await lenderAave.connect(guardian).setProxyBoost(lockerStakeDAO); - - const veFXSMultiplierAfter = await aFraxStakingContract.veFXSMultiplier(lenderAave.address); - expect(veFXSMultiplierAfter).to.be.gt(veFXSMultiplierBefore); }); - it('changeAllowance', async () => { - await lenderAave - .connect(guardian) - .changeAllowance([aToken.address], [aFraxStakingContract.address], [ethers.constants.Zero]); - expect(await aToken.allowance(lenderAave.address, aFraxStakingContract.address)).to.be.equal( - ethers.constants.Zero, - ); - await lenderAave - .connect(guardian) - .changeAllowance( - [aToken.address], - [aFraxStakingContract.address], - [ethers.constants.MaxUint256.div(BigNumber.from('2'))], + describe('changeAllowance', () => { + it('reverts - incompatible length', async () => { + await expect( + lenderAave.connect(guardian).changeAllowance([], [aFraxStakingContract.address], [ethers.constants.Zero]), + ).to.be.revertedWith('IncompatibleLengths'); + await expect(lenderAave.connect(guardian).changeAllowance([], [], [ethers.constants.Zero])).to.be.revertedWith( + 'IncompatibleLengths', ); - expect(await aToken.allowance(lenderAave.address, aFraxStakingContract.address)).to.be.equal( - ethers.constants.MaxUint256.div(BigNumber.from('2')), - ); - // doesn't change anything - await lenderAave - .connect(guardian) - .changeAllowance( - [aToken.address], - [aFraxStakingContract.address], - [ethers.constants.MaxUint256.div(BigNumber.from('2'))], + await expect( + lenderAave + .connect(guardian) + .changeAllowance([ZERO_ADDRESS], [ZERO_ADDRESS, ZERO_ADDRESS], [ethers.constants.Zero]), + ).to.be.revertedWith('IncompatibleLengths'); + await expect( + lenderAave + .connect(guardian) + .changeAllowance([ZERO_ADDRESS], [ZERO_ADDRESS], [ethers.constants.Zero, ethers.constants.Zero]), + ).to.be.revertedWith('IncompatibleLengths'); + await expect( + lenderAave + .connect(guardian) + .changeAllowance([ZERO_ADDRESS, ZERO_ADDRESS], [ZERO_ADDRESS], [ethers.constants.Zero]), + ).to.be.revertedWith('IncompatibleLengths'); + }); + it('success - allowance changed', async () => { + await lenderAave + .connect(guardian) + .changeAllowance([aToken.address], [aFraxStakingContract.address], [ethers.constants.Zero]); + expect(await aToken.allowance(lenderAave.address, aFraxStakingContract.address)).to.be.equal( + ethers.constants.Zero, ); - expect(await aToken.allowance(lenderAave.address, aFraxStakingContract.address)).to.be.equal( - ethers.constants.MaxUint256.div(BigNumber.from('2')), - ); - await lenderAave - .connect(guardian) - .changeAllowance([aToken.address], [aFraxStakingContract.address], [ethers.constants.MaxUint256]); - expect(await aToken.allowance(lenderAave.address, aFraxStakingContract.address)).to.be.equal( - ethers.constants.MaxUint256, - ); + await lenderAave + .connect(guardian) + .changeAllowance( + [aToken.address], + [aFraxStakingContract.address], + [ethers.constants.MaxUint256.div(BigNumber.from('2'))], + ); + expect(await aToken.allowance(lenderAave.address, aFraxStakingContract.address)).to.be.equal( + ethers.constants.MaxUint256.div(BigNumber.from('2')), + ); + // doesn't change anything + await lenderAave + .connect(guardian) + .changeAllowance( + [aToken.address], + [aFraxStakingContract.address], + [ethers.constants.MaxUint256.div(BigNumber.from('2'))], + ); + expect(await aToken.allowance(lenderAave.address, aFraxStakingContract.address)).to.be.equal( + ethers.constants.MaxUint256.div(BigNumber.from('2')), + ); + await lenderAave + .connect(guardian) + .changeAllowance([aToken.address], [aFraxStakingContract.address], [ethers.constants.MaxUint256]); + expect(await aToken.allowance(lenderAave.address, aFraxStakingContract.address)).to.be.equal( + ethers.constants.MaxUint256, + ); + await lenderAave + .connect(guardian) + .changeAllowance( + [aToken.address], + [aFraxStakingContract.address], + [ethers.constants.MaxUint256.div(BigNumber.from('2'))], + ); + expect(await aToken.allowance(lenderAave.address, aFraxStakingContract.address)).to.be.equal( + ethers.constants.MaxUint256.div(BigNumber.from('2')), + ); + }); }); }); @@ -263,6 +456,10 @@ describe('OptimizerAPR - lenderAaveFraxStaker', () => { // at mainnet fork time there is 1.22% coming from liquidity rate, 0.05% coming from incentives // and 0% as no funds deposited yet on the strat expect(apr).to.be.closeTo(parseUnits('0.0127', 18), parseUnits('0.001', 18)); + const weightedAPR = await lenderAave.weightedApr(); + const nav = await lenderAave.nav(); + expect(nav).to.be.equal(0); + expect(weightedAPR).to.be.equal(0); }); it('apr - no boost', async () => { await setTokenBalanceFor(token, strategy.address, 1000000); @@ -271,6 +468,9 @@ describe('OptimizerAPR - lenderAaveFraxStaker', () => { // at mainnet fork time there is 1.22% coming from liquidity rate, 0.05% coming from incentives // and 11.58% (computed by hand because apr displyed on Frax front is wrong) expect(apr).to.be.closeTo(parseUnits('0.1285', 18), parseUnits('0.005', 18)); + const weightedAPR = await lenderAave.weightedApr(); + const nav = await lenderAave.nav(); + expect(weightedAPR).to.be.equal(nav.mul(apr)); }); it('apr - with boost', async () => { await impersonate(fraxTimelock, async acc => { @@ -289,7 +489,7 @@ describe('OptimizerAPR - lenderAaveFraxStaker', () => { }); await lenderAave.connect(guardian).setProxyBoost(lockerStakeDAO); - const veFXSMultiplierAfter = await aFraxStakingContract.veFXSMultiplier(lenderAave.address); + // const veFXSMultiplierAfter = await aFraxStakingContract.veFXSMultiplier(lenderAave.address); await setTokenBalanceFor(token, strategy.address, 1000000); await (await strategy.connect(keeper)['harvest()']()).wait(); const apr = await lenderAave.connect(keeper).apr(); @@ -312,7 +512,7 @@ describe('OptimizerAPR - lenderAaveFraxStaker', () => { }); describe('Strategy deposits', () => { - it('deposit - success - no previous lock', async () => { + it('success - no previous lock', async () => { expect(await lenderAave.kekId()).to.be.equal(ethers.constants.HashZero); // expect(await lenderAave.lastAaveLiquidityIndex()).to.be.equal(ethers.constants.Zero); expect(await lenderAave.lastCreatedStake()).to.be.equal(ethers.constants.Zero); @@ -331,7 +531,27 @@ describe('OptimizerAPR - lenderAaveFraxStaker', () => { expect(underlyingBalance).to.be.closeTo(parseUnits('1000000', tokenDecimal), parseUnits('10', tokenDecimal)); expect(balanceTokenStrat).to.be.equal(parseUnits('0', tokenDecimal)); }); - it('deposit - success - with previous lock', async () => { + it('success - very small amount deposited and hence considering that strategy has no assets', async () => { + expect(await lenderAave.kekId()).to.be.equal(ethers.constants.HashZero); + // expect(await lenderAave.lastAaveLiquidityIndex()).to.be.equal(ethers.constants.Zero); + expect(await lenderAave.lastCreatedStake()).to.be.equal(ethers.constants.Zero); + + await setTokenBalanceFor(token, strategy.address, 1); + + const timestamp = await latestTime(); + await (await strategy.connect(keeper)['harvest()']()).wait(); + expect(await lenderAave.kekId()).to.not.eq(''); + expect(await lenderAave.lastCreatedStake()).to.be.gte(timestamp); + + const underlyingBalance = await lenderAave.underlyingBalanceStored(); + const balanceToken = await lenderAave.nav(); + const balanceTokenStrat = await token.balanceOf(strategy.address); + expect(balanceToken).to.be.equal(parseUnits('1', tokenDecimal)); + expect(underlyingBalance).to.be.closeTo(parseUnits('1', tokenDecimal), parseUnits('10', tokenDecimal)); + expect(balanceTokenStrat).to.be.equal(parseUnits('0', tokenDecimal)); + expect(await lenderAave.hasAssets()).to.be.equal(false); + }); + it('success - with previous lock', async () => { // going through the poolManager to not have to withdraw funds (because it would think we made a huge profit) await setTokenBalanceFor(token, manager.address, 1000000); await (await strategy.connect(keeper)['harvest()']()).wait(); @@ -355,13 +575,13 @@ describe('OptimizerAPR - lenderAaveFraxStaker', () => { }); describe('Strategy withdraws', () => { - it('withdraw - revert - too soon', async () => { + it('withdraw - reverts - too soon', async () => { await setTokenBalanceFor(token, strategy.address, 1000000); await (await strategy.connect(keeper)['harvest()']()).wait(); await setTokenBalanceFor(token, strategy.address, 1000000); await expect(strategy.connect(keeper)['harvest()']()).to.be.rejectedWith('UnstakedTooSoon'); }); - it('emergencyWithdraw - revert - nothing to remove', async () => { + it('emergencyWithdraw - reverts - nothing to remove', async () => { await expect(lenderAave.connect(guardian).emergencyWithdraw(parseUnits('1000000', 18))).to.be.reverted; }); it('emergencyWithdraw - success', async () => { diff --git a/test/optimizerApr/lenderAave.test.ts b/test/optimizerApr/lenderAave.test.ts index b817fed..ae68068 100644 --- a/test/optimizerApr/lenderAave.test.ts +++ b/test/optimizerApr/lenderAave.test.ts @@ -1,5 +1,5 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; -import { BigNumber } from 'ethers'; +import { BigNumber, utils } from 'ethers'; import { ERC20, ERC20__factory, @@ -14,12 +14,13 @@ import { PoolManager, } from '../../typechain'; import { gwei } from '../../utils/bignumber'; -import { deploy, deployUpgradeable } from '../test-utils'; +import { deploy, deployUpgradeable, latestTime, impersonate } from '../test-utils'; import hre, { ethers, network } from 'hardhat'; import { expect } from '../test-utils/chai-setup'; import { BASE_TOKENS } from '../utils'; -import { parseUnits } from 'ethers/lib/utils'; +import { parseUnits, parseEther } from 'ethers/lib/utils'; import { logBN, setTokenBalanceFor } from '../utils-interaction'; +import { ZERO_ADDRESS } from '../test-utils/helpers'; async function initStrategy( governor: SignerWithAddress, @@ -70,32 +71,17 @@ const keeperRole = ethers.utils.solidityKeccak256(['string'], ['KEEPER_ROLE']); let guardianError: string; let strategyError: string; let keeperError: string; +let oneInch: string; // Start test block describe('OptimizerAPR - lenderAave', () => { before(async () => { - await network.provider.request({ - method: 'hardhat_reset', - params: [ - { - forking: { - jsonRpcUrl: process.env.ETH_NODE_URI_FORK, - blockNumber: hre.config.networks.hardhat.forking?.blockNumber, - }, - }, - ], - }); - ({ governor, guardian, user, keeper } = await ethers.getNamedSigners()); - // currently FRAX token = (await ethers.getContractAt(ERC20__factory.abi, '0x853d955aCEf822Db058eb8505911ED77F175b99e')) as ERC20; // USDC = (await ethers.getContractAt(ERC20__factory.abi, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48')) as ERC20; // DAI = (await ethers.getContractAt(ERC20__factory.abi, '0x6B175474E89094C44Da98b954EedeAC495271d0F')) as ERC20; FEI = (await ethers.getContractAt(ERC20__factory.abi, '0x956F47F50A910163D8BF957Cf5846D573E7f87CA')) as ERC20; - - tokenDecimal = await token.decimals(); - aave = (await ethers.getContractAt(ERC20__factory.abi, '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9')) as ERC20; stkAave = (await ethers.getContractAt( IStakedAave__factory.abi, @@ -109,9 +95,22 @@ describe('OptimizerAPR - lenderAave', () => { 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 = '0x1111111254fb6c44bAC0beD2854e76F90643097d'; }); beforeEach(async () => { + await network.provider.request({ + method: 'hardhat_reset', + params: [ + { + forking: { + jsonRpcUrl: process.env.ETH_NODE_URI_FORK, + blockNumber: hre.config.networks.hardhat.forking?.blockNumber, + }, + }, + ], + }); + tokenDecimal = await token.decimals(); manager = (await deploy('PoolManager', [token.address, governor.address, guardian.address])) as PoolManager; // managerDAI = (await deploy('PoolManager', [DAI.address, governor.address, guardian.address])) as PoolManager; @@ -121,157 +120,172 @@ describe('OptimizerAPR - lenderAave', () => { ({ lender: lenderAave } = await initLenderAave(governor, guardian, keeper, strategy, 'genericAave', true)); // ({ lender: lenderAaveDAI } = await initLenderAave(governor, guardian, keeper, strategyDAI, 'genericAaveDAI', true)); }); - describe('Initialization', () => { - describe('Parameters', () => { - it('poolManager', async () => { - expect(await lenderAave.poolManager()).to.be.equal(manager.address); - }); - it('want', async () => { - expect(await lenderAave.want()).to.be.equal(token.address); - }); - it('wantBase', async () => { - expect(await lenderAave.wantBase()).to.be.equal(parseUnits('1', await token.decimals())); - }); - it('isIncentivised', async () => { - expect(await lenderAave.isIncentivised()).to.be.equal(true); - }); - it('cooldownStkAave', async () => { - expect(await lenderAave.cooldownStkAave()).to.be.equal(true); - }); - it('cooldownSeconds', async () => { - expect(await lenderAave.cooldownSeconds()).to.be.equal(BigNumber.from('864000')); - }); - it('unstakeWindow', async () => { - expect(await lenderAave.unstakeWindow()).to.be.equal(BigNumber.from('172800')); - }); - it('allowance - lendingPool', async () => { - expect(await token.allowance(lenderAave.address, lendingPool.address)).to.be.equal(ethers.constants.MaxUint256); - }); - it('allowance - strategy', async () => { - expect(await token.allowance(lenderAave.address, strategy.address)).to.be.equal(ethers.constants.MaxUint256); - }); + it('success - parameters correctly initialized and allowances granted', async () => { + expect(await lenderAave.poolManager()).to.be.equal(manager.address); + expect(await lenderAave.want()).to.be.equal(token.address); + expect(await lenderAave.wantBase()).to.be.equal(parseUnits('1', await token.decimals())); + expect(await lenderAave.isIncentivised()).to.be.equal(true); + expect(await lenderAave.cooldownStkAave()).to.be.equal(true); + expect(await lenderAave.cooldownSeconds()).to.be.equal(BigNumber.from('864000')); + expect(await lenderAave.unstakeWindow()).to.be.equal(BigNumber.from('172800')); + expect(await token.allowance(lenderAave.address, lendingPool.address)).to.be.equal(ethers.constants.MaxUint256); + expect(await token.allowance(lenderAave.address, strategy.address)).to.be.equal(ethers.constants.MaxUint256); + expect(await aave.allowance(lenderAave.address, oneInch)).to.be.equal(ethers.constants.MaxUint256); + expect(await stkAave.allowance(lenderAave.address, oneInch)).to.be.equal(ethers.constants.MaxUint256); + }); + it('reverts - no incentives on FEI or already initialized', async () => { + const managerFEI = (await deploy('PoolManager', [ + FEI.address, + governor.address, + guardian.address, + ])) as PoolManager; + const { strategy: strategyFEI } = await initStrategy(governor, guardian, keeper, managerFEI); + + const lender = (await deployUpgradeable(new GenericAaveNoStaker__factory(guardian))) as GenericAaveNoStaker; + await expect( + lender.initialize(strategyFEI.address, 'lender FEI', true, [governor.address], guardian.address, [ + keeper.address, + ]), + ).to.be.reverted; + await expect( + lenderAave.initialize(strategy.address, 'test', true, [governor.address], guardian.address, [keeper.address]), + ).to.be.revertedWith('Initializable: contract is already initialized'); }); + }); - describe('constructor', () => { - it('reverts - no incentives on FEI', async () => { - const managerFEI = (await deploy('PoolManager', [ - FEI.address, - governor.address, - guardian.address, - ])) as PoolManager; - const { strategy: strategyFEI } = await initStrategy(governor, guardian, keeper, managerFEI); - - const lender = (await deployUpgradeable(new GenericAaveNoStaker__factory(guardian))) as GenericAaveNoStaker; - await expect( - lender.initialize(strategyFEI.address, 'lender FEI', true, [governor.address], guardian.address, [ - keeper.address, - ]), - ).to.be.reverted; - }); + describe('AccessControl', () => { + it('success - guardian role - strategy', async () => { + expect(await strategy.hasRole(guardianRole, guardian.address)).to.be.equal(true); + expect(await strategy.hasRole(guardianRole, governor.address)).to.be.equal(true); + }); + it('success - keeper role - lender', async () => { + expect(await lenderAave.hasRole(keeperRole, keeper.address)).to.be.equal(true); + expect(await lenderAave.hasRole(keeperRole, user.address)).to.be.equal(false); + expect(await lenderAave.getRoleAdmin(keeperRole)).to.be.equal(guardianRole); + await expect(lenderAave.connect(user).claimRewards()).to.be.revertedWith(keeperError); + await expect(lenderAave.connect(user).cooldown()).to.be.revertedWith(keeperError); + await expect(lenderAave.connect(user).sellRewards(0, '0x')).to.be.revertedWith(keeperError); }); + it('success - guardian role - lender', async () => { + expect(await lenderAave.hasRole(guardianRole, guardian.address)).to.be.equal(true); + expect(await lenderAave.hasRole(guardianRole, user.address)).to.be.equal(false); + expect(await lenderAave.hasRole(guardianRole, governor.address)).to.be.equal(true); + expect(await lenderAave.getRoleAdmin(guardianRole)).to.be.equal(strategyRole); + await expect(lenderAave.connect(user).grantRole(keeperRole, user.address)).to.be.revertedWith(guardianRole); + await expect(lenderAave.connect(user).revokeRole(keeperRole, keeper.address)).to.be.revertedWith(guardianRole); + await expect(lenderAave.connect(user).changeAllowance([], [], [])).to.be.revertedWith(guardianError); + await expect(lenderAave.connect(user).sweep(ZERO_ADDRESS, ZERO_ADDRESS)).to.be.revertedWith(guardianError); + await expect(lenderAave.connect(user).emergencyWithdraw(BASE_TOKENS)).to.be.revertedWith(guardianError); + await expect(lenderAave.connect(user).toggleIsIncentivised()).to.be.revertedWith(guardianError); + await expect(lenderAave.connect(user).toggleCooldownStkAave()).to.be.revertedWith(guardianError); + }); + it('success - strategy role - lender', async () => { + expect(await lenderAave.hasRole(strategyRole, strategy.address)).to.be.equal(true); + expect(await lenderAave.hasRole(strategyRole, user.address)).to.be.equal(false); + expect(await lenderAave.getRoleAdmin(strategyRole)).to.be.equal(guardianRole); + await expect(lenderAave.connect(user).deposit()).to.be.revertedWith(strategyError); + await expect(lenderAave.connect(user).withdraw(BASE_TOKENS)).to.be.revertedWith(strategyError); + await expect(lenderAave.connect(user).withdrawAll()).to.be.revertedWith(strategyError); + }); + }); - describe('AccessControl', () => { - it('guardian role - strategy', async () => { - expect(await strategy.hasRole(guardianRole, guardian.address)).to.be.equal(true); - expect(await strategy.hasRole(guardianRole, governor.address)).to.be.equal(true); - }); - it('keeper role - lender', async () => { - expect(await lenderAave.hasRole(keeperRole, keeper.address)).to.be.equal(true); - expect(await lenderAave.hasRole(keeperRole, user.address)).to.be.equal(false); - }); - it('guardian role - lender', async () => { - expect(await lenderAave.hasRole(guardianRole, guardian.address)).to.be.equal(true); - expect(await lenderAave.hasRole(guardianRole, user.address)).to.be.equal(false); - expect(await lenderAave.hasRole(guardianRole, governor.address)).to.be.equal(true); - }); - it('strategy role', async () => { - expect(await lenderAave.hasRole(strategyRole, strategy.address)).to.be.equal(true); - expect(await lenderAave.hasRole(strategyRole, user.address)).to.be.equal(false); - }); - it('addKeeper - reverts nonGuardian', async () => { - await expect(lenderAave.connect(user).grantRole(keeperRole, user.address)).to.be.revertedWith(guardianRole); - }); - it('remove Keeper - reverts nonGuardian', async () => { - await expect(lenderAave.connect(user).revokeRole(keeperRole, keeper.address)).to.be.revertedWith(guardianRole); - }); - it('deposit - reverts nonStrategy', async () => { - await expect(lenderAave.connect(user).deposit()).to.be.revertedWith(strategyError); - }); - it('withdraw - reverts nonStrategy', async () => { - await expect(lenderAave.connect(user).withdraw(BASE_TOKENS)).to.be.revertedWith(strategyError); - }); - it('withdraw - reverts nonStrategy', async () => { - await expect(lenderAave.connect(user).withdrawAll()).to.be.revertedWith(strategyError); - }); - it('claimRewards - reverts non keeper', async () => { - await expect(lenderAave.connect(user).claimRewards()).to.be.revertedWith(keeperError); - }); - it('emergencyWithdraw - reverts guardian', async () => { - await expect(lenderAave.connect(user).emergencyWithdraw(BASE_TOKENS)).to.be.revertedWith(guardianError); - }); - it('emergencyWithdraw - reverts guardian', async () => { - await expect(lenderAave.connect(user).emergencyWithdraw(BASE_TOKENS)).to.be.revertedWith(guardianError); - }); - it('toggleIsIncentivised - reverts guardian', async () => { - await expect(lenderAave.connect(user).toggleIsIncentivised()).to.be.revertedWith(guardianError); - }); - it('toggleCooldownStkAave - reverts guardian', async () => { - await expect(lenderAave.connect(user).toggleCooldownStkAave()).to.be.revertedWith(guardianError); - }); + describe('toggle boolean', () => { + it('cooldownStkAave ', async () => { + await (await lenderAave.connect(guardian).toggleCooldownStkAave()).wait(); + expect(await lenderAave.cooldownStkAave()).to.be.equal(false); + await (await lenderAave.connect(guardian).toggleCooldownStkAave()).wait(); + expect(await lenderAave.cooldownStkAave()).to.be.equal(true); + }); + it('isIncentivised', async () => { + await (await lenderAave.connect(guardian).toggleIsIncentivised()).wait(); + expect(await lenderAave.isIncentivised()).to.be.equal(false); + await (await lenderAave.connect(guardian).toggleIsIncentivised()).wait(); + expect(await lenderAave.isIncentivised()).to.be.equal(true); }); + }); + describe('View functions', () => { + it('apr', async () => { + const apr = await lenderAave.connect(keeper).apr(); + // at mainnet fork time there is 1.193% coming from liquidity rate and 0.050% coming from incentives + expect(apr).to.be.closeTo(parseUnits('0.0124', 18), parseUnits('0.001', 18)); + expect(await lenderAave.weightedApr()).to.be.equal(0); + }); + it('apr - when no incentives', async () => { + const managerFEI = (await deploy('PoolManager', [ + FEI.address, + governor.address, + guardian.address, + ])) as PoolManager; + const { strategy: strategyFEI } = await initStrategy(governor, guardian, keeper, managerFEI); + + const lender = (await deployUpgradeable(new GenericAaveNoStaker__factory(guardian))) as GenericAaveNoStaker; + await lender.initialize(strategyFEI.address, 'lender FEI', false, [governor.address], guardian.address, [ + keeper.address, + ]); + 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)); + expect(await lender.weightedApr()).to.be.equal(0); + }); + it('aprAfterDeposit', async () => { + const aprAfterDepositSupposed = await lenderAave + .connect(keeper) + .aprAfterDeposit(parseUnits('10000000', tokenDecimal)); - describe('toggle boolean', () => { - it('cooldownStkAave ', async () => { - await (await lenderAave.connect(guardian).toggleCooldownStkAave()).wait(); - expect(await lenderAave.cooldownStkAave()).to.be.equal(false); - await (await lenderAave.connect(guardian).toggleCooldownStkAave()).wait(); - expect(await lenderAave.cooldownStkAave()).to.be.equal(true); - }); - it('isIncentivised ', async () => { - await (await lenderAave.connect(guardian).toggleIsIncentivised()).wait(); - expect(await lenderAave.isIncentivised()).to.be.equal(false); - await (await lenderAave.connect(guardian).toggleIsIncentivised()).wait(); - expect(await lenderAave.isIncentivised()).to.be.equal(true); - }); + // Do the deposit and see if the values are indeed equals + await setTokenBalanceFor(token, strategy.address, 10000000); + await (await strategy.connect(keeper)['harvest()']()).wait(); + const aprReal = await lenderAave.connect(keeper).apr(); + + expect(aprAfterDepositSupposed).to.be.closeTo(aprReal, parseUnits('0.001', 18)); }); + }); - describe('View functions', () => { - it('apr ', async () => { - const apr = await lenderAave.connect(keeper).apr(); - // at mainnet fork time there is 1.193% coming from liquidity rate and 0.050% coming from incentives - expect(apr).to.be.closeTo(parseUnits('0.0124', 18), parseUnits('0.001', 18)); + describe('Strategy deposits and withdraw', () => { + describe('deposit', () => { + it('success - normal amount', async () => { + expect(await lenderAave.hasAssets()).to.be.equal(false); + await setTokenBalanceFor(token, strategy.address, 1000000); + await (await strategy.connect(keeper)['harvest()']()).wait(); + const balanceToken = await lenderAave.nav(); + const balanceTokenStrat = await token.balanceOf(strategy.address); + expect(balanceToken).to.be.equal(parseUnits('1000000', tokenDecimal)); + expect(balanceTokenStrat).to.be.equal(parseUnits('0', tokenDecimal)); + expect(await lenderAave.hasAssets()).to.be.equal(true); }); - it('apr ', async () => { - const aprAfterDepositSupposed = await lenderAave - .connect(keeper) - .aprAfterDeposit(parseUnits('10000000', tokenDecimal)); - - // Do the deposit and see if the values are indeed equals - await setTokenBalanceFor(token, strategy.address, 10000000); + it('success - too small dusty amount', async () => { + expect(await lenderAave.hasAssets()).to.be.equal(false); + await setTokenBalanceFor(token, strategy.address, 1); await (await strategy.connect(keeper)['harvest()']()).wait(); - const aprReal = await lenderAave.connect(keeper).apr(); - - expect(aprAfterDepositSupposed).to.be.closeTo(aprReal, parseUnits('0.001', 18)); + const balanceToken = await lenderAave.nav(); + const balanceTokenStrat = await token.balanceOf(strategy.address); + expect(balanceToken).to.be.equal(parseUnits('1', tokenDecimal)); + expect(balanceTokenStrat).to.be.equal(parseUnits('0', tokenDecimal)); + expect(await lenderAave.hasAssets()).to.be.equal(false); }); - }); - - describe('Strategy deposits and withdraw', () => { - it('deposit -success ', async () => { + it('success - when allowance has been revoked', async () => { + await lenderAave.connect(guardian).changeAllowance([token.address], [lendingPool.address], [0]); + expect(await token.allowance(lenderAave.address, lendingPool.address)).to.be.equal(0); await setTokenBalanceFor(token, strategy.address, 1000000); await (await strategy.connect(keeper)['harvest()']()).wait(); + expect(await token.allowance(lenderAave.address, lendingPool.address)).to.be.gt(0); const balanceToken = await lenderAave.nav(); const balanceTokenStrat = await token.balanceOf(strategy.address); expect(balanceToken).to.be.equal(parseUnits('1000000', tokenDecimal)); expect(balanceTokenStrat).to.be.equal(parseUnits('0', tokenDecimal)); + expect(await lenderAave.hasAssets()).to.be.equal(true); }); - it('withdrawEmergency - success', async () => { + }); + describe('withdraw', () => { + it('success - emergencyWithdraw funds pulled', async () => { await setTokenBalanceFor(token, strategy.address, 1000000); await (await strategy.connect(keeper)['harvest()']()).wait(); await (await lenderAave.connect(guardian).emergencyWithdraw(parseUnits('1000000', 18))).wait(); expect(await token.balanceOf(manager.address)).to.be.equal(parseUnits('1000000', tokenDecimal)); + expect(await lenderAave.hasAssets()).to.be.equal(false); }); - it('withdraw - success', async () => { + it('success - withdraw works fine', async () => { await setTokenBalanceFor(token, strategy.address, 1000000); await (await strategy.connect(keeper)['harvest()']()).wait(); await ( @@ -286,67 +300,132 @@ describe('OptimizerAPR - lenderAave', () => { expect(balanceTokenManager).to.be.closeTo(parseUnits('1000000', tokenDecimal), parseUnits('1', tokenDecimal)); }); }); + }); - describe('Handle rewards', () => { - it('claimRewards - cooldown triggered', async () => { - expect(await stkAave.balanceOf(lenderAave.address)).to.equal(0); - expect(await aave.balanceOf(lenderAave.address)).to.equal(0); - - await setTokenBalanceFor(token, strategy.address, 1000000); - await (await strategy.connect(keeper)['harvest()']()).wait(); - - await network.provider.send('evm_increaseTime', [3600 * 24 * 365]); // forward 1 year - await network.provider.send('evm_mine'); - // start coolDown - await lenderAave.connect(keeper).claimRewards(); - - const currentBalanceStkAave = await stkAave.balanceOf(lenderAave.address); - - await network.provider.send('evm_increaseTime', [3600 * 24 * 10]); // forward 10 days after the cooldown finished - await network.provider.send('evm_mine'); - - // will change stkAave into Aave - await lenderAave.connect(keeper).claimRewards(); + describe('claimRewards', () => { + it('success - cooldown triggered', async () => { + expect(await stkAave.balanceOf(lenderAave.address)).to.equal(0); + expect(await aave.balanceOf(lenderAave.address)).to.equal(0); + + await setTokenBalanceFor(token, strategy.address, 1000000); + await (await strategy.connect(keeper)['harvest()']()).wait(); + + await network.provider.send('evm_increaseTime', [3600 * 24 * 365]); // forward 1 year + await network.provider.send('evm_mine'); + // start coolDown + await lenderAave.connect(keeper).claimRewards(); + expect(await stkAave.stakersCooldowns(lenderAave.address)).to.be.equal(await latestTime()); + const currentBalanceStkAave = await stkAave.balanceOf(lenderAave.address); + await network.provider.send('evm_increaseTime', [3600 * 24 * 10]); // forward 10 days after the cooldown finished + await network.provider.send('evm_mine'); + + // will change stkAave into Aave + await lenderAave.connect(keeper).claimRewards(); + + expect(ethers.constants.Zero).to.be.closeTo( + await stkAave.balanceOf(lenderAave.address), + parseUnits('0.001', tokenDecimal), + ); + expect(currentBalanceStkAave).to.be.closeTo( + await aave.balanceOf(lenderAave.address), + parseUnits('0.001', tokenDecimal), + ); + await lenderAave.connect(guardian).sweep(stkAave.address, guardian.address); + await lenderAave.connect(guardian).sweep(aave.address, guardian.address); + await network.provider.send('evm_increaseTime', [3600 * 24 * 365]); // Passing to another time + }); + it('success - claim too soon with no stkAave', async () => { + expect(await stkAave.balanceOf(lenderAave.address)).to.equal(0); + expect(await aave.balanceOf(lenderAave.address)).to.equal(0); - expect(ethers.constants.Zero).to.be.closeTo( - await stkAave.balanceOf(lenderAave.address), - parseUnits('0.001', tokenDecimal), - ); - expect(currentBalanceStkAave).to.be.closeTo( - await aave.balanceOf(lenderAave.address), - parseUnits('0.001', tokenDecimal), - ); - }); - it('claimRewards - claim too soon', async () => { - expect(await stkAave.balanceOf(lenderAave.address)).to.equal(0); - expect(await aave.balanceOf(lenderAave.address)).to.equal(0); + await setTokenBalanceFor(token, strategy.address, 1000000); + await (await strategy.connect(keeper)['harvest()']()).wait(); - await setTokenBalanceFor(token, strategy.address, 1000000); - await (await strategy.connect(keeper)['harvest()']()).wait(); + await network.provider.send('evm_increaseTime', [3600 * 24 * 365]); // forward 1 year + await network.provider.send('evm_mine'); + // start coolDown + await lenderAave.connect(keeper).claimRewards(); + expect(await stkAave.stakersCooldowns(lenderAave.address)).to.be.equal(await latestTime()); - await network.provider.send('evm_increaseTime', [3600 * 24 * 365]); // forward 1 year - await network.provider.send('evm_mine'); - // start coolDown - await lenderAave.connect(keeper).claimRewards(); + const currentBalanceStkAave = await stkAave.balanceOf(lenderAave.address); - const currentBalanceStkAave = await stkAave.balanceOf(lenderAave.address); + await network.provider.send('evm_increaseTime', [3600 * 24 * 5]); // forward 5 days before the cooldown finished + await network.provider.send('evm_mine'); - await network.provider.send('evm_increaseTime', [3600 * 24 * 5]); // forward 5 days before the cooldown finished - await network.provider.send('evm_mine'); + // will change stkAave into Aave + await lenderAave.connect(keeper).claimRewards(); - // will change stkAave into Aave - await lenderAave.connect(keeper).claimRewards(); + const futureBalanceStkAave = await stkAave.balanceOf(lenderAave.address); - const futureBalanceStkAave = await stkAave.balanceOf(lenderAave.address); + console.log(`${logBN(currentBalanceStkAave, { base: 18 })} --> ${logBN(futureBalanceStkAave, { base: 18 })}`); - console.log(`${logBN(currentBalanceStkAave, { base: 18 })} --> ${logBN(futureBalanceStkAave, { base: 18 })}`); + expect(currentBalanceStkAave.lte(futureBalanceStkAave)).to.be.equal(true); + expect(ethers.constants.Zero).to.be.closeTo( + await aave.balanceOf(lenderAave.address), + parseUnits('0.001', tokenDecimal), + ); + await lenderAave.connect(guardian).sweep(stkAave.address, guardian.address); + await lenderAave.connect(guardian).sweep(aave.address, guardian.address); + await network.provider.send('evm_increaseTime', [3600 * 24 * 365]); // Passing to another time + }); + it('success - cooldown triggered but waits too much to claim other rewards so restarts cooldown', async () => { + expect(await stkAave.balanceOf(lenderAave.address)).to.equal(0); + expect(await aave.balanceOf(lenderAave.address)).to.equal(0); + + await setTokenBalanceFor(token, strategy.address, 1000000); + await (await strategy.connect(keeper)['harvest()']()).wait(); + + await network.provider.send('evm_increaseTime', [3600 * 24 * 365]); // forward 1 year + await network.provider.send('evm_mine'); + // start coolDown + await lenderAave.connect(keeper).claimRewards(); + expect(await stkAave.stakersCooldowns(lenderAave.address)).to.be.equal(await latestTime()); + + const currentBalanceStkAave = await stkAave.balanceOf(lenderAave.address); + + await network.provider.send('evm_increaseTime', [3600 * 24 * 100]); // forward 100 days + await network.provider.send('evm_mine'); + + // will change stkAave into Aave + await lenderAave.connect(keeper).claimRewards(); + const futureBalanceStkAave = await stkAave.balanceOf(lenderAave.address); + expect(currentBalanceStkAave.lte(futureBalanceStkAave)).to.be.equal(true); + + expect(ethers.constants.Zero).to.be.closeTo( + await aave.balanceOf(lenderAave.address), + parseUnits('0.001', tokenDecimal), + ); + await lenderAave.connect(guardian).sweep(stkAave.address, guardian.address); + await lenderAave.connect(guardian).sweep(aave.address, guardian.address); + await network.provider.send('evm_increaseTime', [3600 * 24 * 365]); // Passing to another time + }); - expect(currentBalanceStkAave.lte(futureBalanceStkAave)).to.be.equal(true); - expect(ethers.constants.Zero).to.be.closeTo( - await aave.balanceOf(lenderAave.address), - parseUnits('0.001', tokenDecimal), - ); - }); + it('success - claim too soon with a positive stkAave balance', async () => { + await setTokenBalanceFor(token, strategy.address, 1000000); + await (await strategy.connect(keeper)['harvest()']()).wait(); + + await network.provider.send('evm_increaseTime', [3600 * 24 * 365]); // forward 1 year + await network.provider.send('evm_mine'); + // start coolDown + + await lenderAave.connect(keeper).claimRewards(); + const cooldownTimestamp = await stkAave.stakersCooldowns(lenderAave.address); + expect(cooldownTimestamp).to.be.equal(await latestTime()); + const stkAaveHolder = '0x32B61Bb22Cbe4834bc3e73DcE85280037D944a4D'; + await impersonate(stkAaveHolder, async acc => { + await network.provider.send('hardhat_setBalance', [ + stkAaveHolder, + utils.parseEther('1').toHexString().replace('0x0', '0x'), + ]); + await (await stkAave.connect(acc).transfer(lenderAave.address, parseEther('1'))).wait(); + }); + // will change stkAave into Aave + await lenderAave.connect(keeper).claimRewards(); + expect(cooldownTimestamp).to.be.equal(await stkAave.stakersCooldowns(lenderAave.address)); + expect(ethers.constants.Zero).to.be.closeTo( + await aave.balanceOf(lenderAave.address), + parseUnits('0.001', tokenDecimal), + ); }); }); }); diff --git a/test/strategyAaveFlashLoan/aaveFlashloanStrategy1.test.ts b/test/strategyAaveFlashLoan/aaveFlashloanStrategy1.test.ts index 7f10479..0313c5c 100644 --- a/test/strategyAaveFlashLoan/aaveFlashloanStrategy1.test.ts +++ b/test/strategyAaveFlashLoan/aaveFlashloanStrategy1.test.ts @@ -23,7 +23,7 @@ import { } from '../../typechain'; import { parseUnits } from 'ethers/lib/utils'; -describe('AaveFlashloan Strat', () => { +describe('AaveFlashloan Strategy1', () => { // ATokens let aToken: ERC20, debtToken: ERC20; diff --git a/test/strategyAaveFlashLoan/aaveFlashloanStrategy2.test.ts b/test/strategyAaveFlashLoan/aaveFlashloanStrategy2.test.ts index 832fec1..b8e9007 100644 --- a/test/strategyAaveFlashLoan/aaveFlashloanStrategy2.test.ts +++ b/test/strategyAaveFlashLoan/aaveFlashloanStrategy2.test.ts @@ -5,7 +5,7 @@ import { expect } from '../test-utils/chai-setup'; import { setup } from './setup_tests'; -describe('AaveFlashloan strategy', () => { +describe('AaveFlashloan Strategy2', () => { it('scenario static', async () => { const { strategy, diff --git a/test/strategyAaveFlashLoan/aaveFlashloanStrategyCoverage.test.ts b/test/strategyAaveFlashLoan/aaveFlashloanStrategyCoverage.test.ts index a5cecfe..8ce6f95 100644 --- a/test/strategyAaveFlashLoan/aaveFlashloanStrategyCoverage.test.ts +++ b/test/strategyAaveFlashLoan/aaveFlashloanStrategyCoverage.test.ts @@ -19,12 +19,12 @@ import { } from '../../typechain'; import { parseUnits } from 'ethers/lib/utils'; -describe('AaveFlashloan Strat - coverage', () => { +describe('AaveFlashloan Strat - Coverage', () => { // ATokens let aToken: ERC20, debtToken: ERC20; // Tokens - let wantToken: ERC20, dai: ERC20, aave: ERC20, stkAave: IStakedAave; + let wantToken: ERC20, aave: ERC20, stkAave: IStakedAave; // Guardians let deployer: SignerWithAddress, @@ -59,7 +59,6 @@ describe('AaveFlashloan Strat - coverage', () => { }); wantToken = (await ethers.getContractAt(ERC20__factory.abi, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48')) as ERC20; - dai = (await ethers.getContractAt(ERC20__factory.abi, '0x6B175474E89094C44Da98b954EedeAC495271d0F')) as ERC20; aave = (await ethers.getContractAt(ERC20__factory.abi, '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9')) as ERC20; stkAave = (await ethers.getContractAt( IStakedAave__factory.abi, diff --git a/test/strategyAaveFlashLoan/aaveFlashloanStrategyHarvestHint.test.ts b/test/strategyAaveFlashLoan/aaveFlashloanStrategyHarvestHint.test.ts index e25f047..0076384 100644 --- a/test/strategyAaveFlashLoan/aaveFlashloanStrategyHarvestHint.test.ts +++ b/test/strategyAaveFlashLoan/aaveFlashloanStrategyHarvestHint.test.ts @@ -25,7 +25,7 @@ import { getParamsOptim } from '../utils'; const PRECISION = 3; const toOriginalBase = (n: BigNumber, base = 6) => n.mul(utils.parseUnits('1', base)).div(utils.parseUnits('1', 27)); -describe('AaveFlashloan Strat', () => { +describe('AaveFlashloanHarvestHint', () => { // ATokens let aToken: ERC20, debtToken: ERC20; let aDAIToken: ERC20, debtDAIToken: ERC20; diff --git a/test/strategyAaveFlashLoan/aaveFlashloanStrategy_random.test.ts b/test/strategyAaveFlashLoan/aaveFlashloanStrategy_random.test.ts index c4b7ac0..681ca15 100644 --- a/test/strategyAaveFlashLoan/aaveFlashloanStrategy_random.test.ts +++ b/test/strategyAaveFlashLoan/aaveFlashloanStrategy_random.test.ts @@ -3,7 +3,7 @@ import { network } from 'hardhat'; import { utils } from 'ethers'; import { setup } from './setup_tests'; -describe('AaveFlashloan strategy random (USDC)', () => { +describe('AaveFlashloan strategy - Random USDC', () => { it('scenario random', async () => { const { _wantToken, strategy, lendingPool, poolManager, oldStrategy, realGuardian, richUSDCUser, aToken, harvest } = await setup(14456160); diff --git a/test/strategyAaveFlashLoan/aaveFlashloanStrategy_random_DAI.test.ts b/test/strategyAaveFlashLoan/aaveFlashloanStrategy_random_DAI.test.ts index 2c0d9cd..a7787de 100644 --- a/test/strategyAaveFlashLoan/aaveFlashloanStrategy_random_DAI.test.ts +++ b/test/strategyAaveFlashLoan/aaveFlashloanStrategy_random_DAI.test.ts @@ -13,7 +13,7 @@ async function setDaiBalanceFor(account: string, amount: number) { ]); } -describe('AaveFlashloan strategy random (DAI)', () => { +describe('AaveFlashloan strategy - Random DAI', () => { it('scenario random', async () => { const { _wantToken, strategy, lendingPool, poolManager, oldStrategy, realGuardian, richUSDCUser, aToken, harvest } = await setup(14456160, 'DAI'); diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index b108017..875bd53 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -5,7 +5,9 @@ import { TransparentUpgradeableProxy__factory } from '../../typechain'; export async function deploy( contractName: string, + // eslint-disable-next-line args: any[] = [], + // eslint-disable-next-line options: Record & { libraries?: Record } = {}, ): Promise { const factory = await ethers.getContractFactory(contractName, options); @@ -13,6 +15,12 @@ export async function deploy( return contract; } +export async function latestTime(): Promise { + const { timestamp } = await ethers.provider.getBlock(await ethers.provider.getBlockNumber()); + + return timestamp as number; +} + export const randomAddress = () => Wallet.createRandom().address; export async function impersonate( diff --git a/test/utils-interaction.ts b/test/utils-interaction.ts index 04e3b7d..eb2212d 100644 --- a/test/utils-interaction.ts +++ b/test/utils-interaction.ts @@ -311,6 +311,7 @@ export const randomOpenPerp = async ( }) .map(x => { return x.args?._perpetualID; + // eslint-disable-next-line }) as any[]; const perpData = await perpetualManager.perpetualData(perpId[0]); @@ -343,6 +344,7 @@ export const closePerp = async ( }; export async function findBalancesSlot(tokenAddress: string): Promise { + // eslint-disable-next-line const encode = (types: string[], values: any[]) => ethers.utils.defaultAbiCoder.encode(types, values); const account = ethers.constants.AddressZero; const probeA = encode(['uint'], [1]);