diff --git a/contracts/adapters/interests-strategies/SushiAmmReserveInterestRateStrategy.sol b/contracts/adapters/interests-strategies/SushiAmmReserveInterestRateStrategy.sol new file mode 100644 index 00000000..a06b6665 --- /dev/null +++ b/contracts/adapters/interests-strategies/SushiAmmReserveInterestRateStrategy.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +import {SafeMath} from '../../dependencies/openzeppelin/contracts/SafeMath.sol'; +import {WadRayMath} from '../../protocol/libraries/math/WadRayMath.sol'; +import {PercentageMath} from '../../protocol/libraries/math/PercentageMath.sol'; +import {ILendingPoolAddressesProvider} from '../../interfaces/ILendingPoolAddressesProvider.sol'; +import {ILendingRateOracle} from '../../interfaces/ILendingRateOracle.sol'; +import {IERC20} from '../../dependencies/openzeppelin/contracts/IERC20.sol'; +import {ISushiRewardsAwareAToken} from '../interfaces/sushi/ISushiRewardsAwareAToken.sol'; +import {IMasterChef} from '../../adapters/interfaces/sushi/IMasterChef.sol'; +import { + DefaultReserveInterestRateStrategy +} from '../../protocol/lendingpool/DefaultReserveInterestRateStrategy.sol'; + +/** + * @title DefaultReserveInterestRateStrategy contract + * @notice Implements the calculation of the interest rates depending on the reserve state + * @dev The model of interest rate is based on 2 slopes, one before the `OPTIMAL_UTILIZATION_RATE` + * point of utilization and another from that one to 100% + * - An instance of this same contract, can't be used across different Aave markets, due to the caching + * of the LendingPoolAddressesProvider + * @author Aave + **/ +contract SushiAmmReserveInterestRateStrategy is DefaultReserveInterestRateStrategy { + using WadRayMath for uint256; + using SafeMath for uint256; + using PercentageMath for uint256; + + constructor( + ILendingPoolAddressesProvider provider, + uint256 optimalUtilizationRate, + uint256 baseVariableBorrowRate, + uint256 variableRateSlope1, + uint256 variableRateSlope2, + uint256 stableRateSlope1, + uint256 stableRateSlope2 + ) + public + DefaultReserveInterestRateStrategy( + provider, + optimalUtilizationRate, + baseVariableBorrowRate, + variableRateSlope1, + variableRateSlope2, + stableRateSlope1, + stableRateSlope2 + ) + {} + + /** + * @dev Calculates the interest rates depending on the reserve's state and configurations + * @param reserve The address of the reserve + * @param liquidityAdded The liquidity added during the operation + * @param liquidityTaken The liquidity taken during the operation + * @param totalStableDebt The total borrowed from the reserve a stable rate + * @param totalVariableDebt The total borrowed from the reserve at a variable rate + * @param averageStableBorrowRate The weighted average of all the stable rate loans + * @param reserveFactor The reserve portion of the interest that goes to the treasury of the market + * @return The liquidity rate, the stable borrow rate and the variable borrow rate + **/ + function calculateInterestRates( + address reserve, + address aToken, + uint256 liquidityAdded, + uint256 liquidityTaken, + uint256 totalStableDebt, + uint256 totalVariableDebt, + uint256 averageStableBorrowRate, + uint256 reserveFactor + ) + external + view + override + returns ( + uint256, + uint256, + uint256 + ) + { + uint256 poolId = ISushiRewardsAwareAToken(aToken).getMasterChefPoolId(); + (uint256 stakedBalance, ) = + IMasterChef(ISushiRewardsAwareAToken(aToken).getMasterChef()).userInfo(poolId, aToken); + + //avoid stack too deep + uint256 availableLiquidity = stakedBalance.add(liquidityAdded).sub(liquidityTaken); + + return + calculateInterestRates( + reserve, + availableLiquidity, + totalStableDebt, + totalVariableDebt, + averageStableBorrowRate, + reserveFactor + ); + } + + /** + * @dev Calculates the interest rates depending on the reserve's state and configurations. + * NOTE This function is kept for compatibility with the previous DefaultInterestRateStrategy interface. + * New protocol implementation uses the new calculateInterestRates() interface + * @param reserve The address of the reserve + * @param availableLiquidity The liquidity available in the corresponding aToken + * @param totalStableDebt The total borrowed from the reserve a stable rate + * @param totalVariableDebt The total borrowed from the reserve at a variable rate + * @param averageStableBorrowRate The weighted average of all the stable rate loans + * @param reserveFactor The reserve portion of the interest that goes to the treasury of the market + * @return The liquidity rate, the stable borrow rate and the variable borrow rate + **/ + function calculateInterestRates( + address reserve, + uint256 availableLiquidity, + uint256 totalStableDebt, + uint256 totalVariableDebt, + uint256 averageStableBorrowRate, + uint256 reserveFactor + ) + public + view + virtual + override + returns ( + uint256, + uint256, + uint256 + ) + { + CalcInterestRatesLocalVars memory vars; + + vars.totalDebt = totalStableDebt.add(totalVariableDebt); + vars.currentVariableBorrowRate = 0; + vars.currentStableBorrowRate = 0; + vars.currentLiquidityRate = 0; + + vars.utilizationRate = vars.totalDebt == 0 + ? 0 + : vars.totalDebt.rayDiv(availableLiquidity.add(vars.totalDebt)); + + vars.currentStableBorrowRate = ILendingRateOracle(addressesProvider.getLendingRateOracle()) + .getMarketBorrowRate(reserve); + + if (vars.utilizationRate > OPTIMAL_UTILIZATION_RATE) { + uint256 excessUtilizationRateRatio = + vars.utilizationRate.sub(OPTIMAL_UTILIZATION_RATE).rayDiv(EXCESS_UTILIZATION_RATE); + + vars.currentStableBorrowRate = vars.currentStableBorrowRate.add(_stableRateSlope1).add( + _stableRateSlope2.rayMul(excessUtilizationRateRatio) + ); + + vars.currentVariableBorrowRate = _baseVariableBorrowRate.add(_variableRateSlope1).add( + _variableRateSlope2.rayMul(excessUtilizationRateRatio) + ); + } else { + vars.currentStableBorrowRate = vars.currentStableBorrowRate.add( + _stableRateSlope1.rayMul(vars.utilizationRate.rayDiv(OPTIMAL_UTILIZATION_RATE)) + ); + vars.currentVariableBorrowRate = _baseVariableBorrowRate.add( + vars.utilizationRate.rayMul(_variableRateSlope1).rayDiv(OPTIMAL_UTILIZATION_RATE) + ); + } + + vars.currentLiquidityRate = _getOverallBorrowRate( + totalStableDebt, + totalVariableDebt, + vars + .currentVariableBorrowRate, + averageStableBorrowRate + ) + .rayMul(vars.utilizationRate) + .percentMul(PercentageMath.PERCENTAGE_FACTOR.sub(reserveFactor)); + + return ( + vars.currentLiquidityRate, + vars.currentStableBorrowRate, + vars.currentVariableBorrowRate + ); + } +} diff --git a/contracts/adapters/interfaces/sushi/IMasterChef.sol b/contracts/adapters/interfaces/sushi/IMasterChef.sol new file mode 100644 index 00000000..1249b3b1 --- /dev/null +++ b/contracts/adapters/interfaces/sushi/IMasterChef.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +interface IMasterChef { + struct PoolInfo { + address lpToken; + } + + struct UserInfo { + uint256 amount; + uint256 rewardDebt; + } + + function deposit(uint256, uint256) external; + + function withdraw(uint256, uint256) external; + + function sushi() external view returns (address); + + function poolInfo(uint256) external view returns (PoolInfo memory); + + function userInfo(uint256, address) external view returns (uint256, uint256); + + function pendingSushi(uint256, address) external view returns (uint256); +} diff --git a/contracts/adapters/interfaces/sushi/ISushiBar.sol b/contracts/adapters/interfaces/sushi/ISushiBar.sol new file mode 100644 index 00000000..b0cce0ba --- /dev/null +++ b/contracts/adapters/interfaces/sushi/ISushiBar.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +interface ISushiBar { + function enter(uint256 _amount) external; + + function leave(uint256 _share) external; +} diff --git a/contracts/adapters/interfaces/sushi/ISushiRewardsAwareAToken.sol b/contracts/adapters/interfaces/sushi/ISushiRewardsAwareAToken.sol new file mode 100644 index 00000000..7f36d2ae --- /dev/null +++ b/contracts/adapters/interfaces/sushi/ISushiRewardsAwareAToken.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +interface ISushiRewardsAwareAToken { + function getMasterChef() external view returns (address); + + function getSushiBar() external view returns (address); + + function getSushiToken() external view returns (address); + + function getMasterChefPoolId() external view returns (uint256); +} diff --git a/contracts/adapters/rewards/SushiRewardsAwareAToken.sol b/contracts/adapters/rewards/SushiRewardsAwareAToken.sol new file mode 100644 index 00000000..94d20cd8 --- /dev/null +++ b/contracts/adapters/rewards/SushiRewardsAwareAToken.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +import {ILendingPool} from '../../interfaces/ILendingPool.sol'; +import {RewardsAwareAToken} from '../../protocol/tokenization/RewardsAwareAToken.sol'; +import {SafeERC20} from '../../dependencies/openzeppelin/contracts/SafeERC20.sol'; +import {IERC20} from '../../dependencies/openzeppelin/contracts/IERC20.sol'; +import {IMasterChef} from '../interfaces/sushi/IMasterChef.sol'; +import {ISushiBar} from '../interfaces/sushi/ISushiBar.sol'; +import {IAaveIncentivesController} from '../../interfaces/IAaveIncentivesController.sol'; +import {ISushiRewardsAwareAToken} from '../interfaces/sushi/ISushiRewardsAwareAToken.sol'; + +/** + * @title Sushi LP Rewards Aware AToken + * @notice AToken aware to claim and distribute XSUSHI rewards from MasterChef farm and SushiBar. + * @author Aave + */ +contract SushiRewardsAwareAToken is RewardsAwareAToken, ISushiRewardsAwareAToken { + address internal immutable MASTER_CHEF; + address internal immutable SUSHI_BAR; + address internal immutable SUSHI_TOKEN; + + uint256 internal _poolId; + uint256 internal _pendingXSushiRewards; + + /** + * @param masterChef The address of Master Chef LP staking contract + * @param sushiBar The address of Sushi Bar xSUSHI staking contract + * @param sushiToken The address of SUSHI token + */ + constructor( + address masterChef, + address sushiBar, + address sushiToken + ) public { + MASTER_CHEF = masterChef; + SUSHI_BAR = sushiBar; + SUSHI_TOKEN = sushiToken; + } + + /** + * @dev Initializes the aToken + * @param pool The address of the lending pool where this aToken will be used + * @param treasury The address of the Aave treasury, receiving the fees on this aToken + * @param underlyingAsset The address of the underlying asset of this aToken (E.g. WETH for aWETH) + * @param incentivesController The smart contract managing potential incentives distribution + * @param aTokenDecimals The decimals of the aToken, same as the underlying asset's + * @param aTokenName The name of the aToken + * @param aTokenSymbol The symbol of the aToken + * @param params Additional variadic field to include extra params. Expected parameters: + * uint256 poolId The id of the Master Chef pool + */ + function initialize( + ILendingPool pool, + address treasury, + address underlyingAsset, + IAaveIncentivesController incentivesController, + uint8 aTokenDecimals, + string calldata aTokenName, + string calldata aTokenSymbol, + bytes calldata params + ) external virtual override initializer { + uint256 chainId; + + //solium-disable-next-line + assembly { + chainId := chainid() + } + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + EIP712_DOMAIN, + keccak256(bytes(aTokenName)), + keccak256(EIP712_REVISION), + chainId, + address(this) + ) + ); + + _setName(aTokenName); + _setSymbol(aTokenSymbol); + _setDecimals(aTokenDecimals); + + _pool = pool; + _treasury = treasury; + _underlyingAsset = underlyingAsset; + _incentivesController = incentivesController; + + // Sushi LP RewardsAwareToken init + _poolId = _decodeParamsPoolId(params); + + // Set reward token as XSUSHI + _rewardTokens[0] = SUSHI_BAR; + + // Approve moving our SLP into the master chef contract. + IERC20(UNDERLYING_ASSET_ADDRESS()).approve(MASTER_CHEF, uint256(-1)); + IERC20(SUSHI_TOKEN).approve(SUSHI_BAR, uint256(-1)); + } + + /** Start of Sushi implementation */ + function _stakeSushi(uint256 amount) internal { + uint256 priorXSushiBalance = _xSushiBalance(); + + // Stake SUSHI rewards to Sushi Bar + ISushiBar(SUSHI_BAR).enter(amount); + + // Pending XSUSHI to reward, will be claimed at `_updateDistribution` call + _pendingXSushiRewards = _pendingXSushiRewards.add((_xSushiBalance()).sub(priorXSushiBalance)); + } + + function _unstakeSushi(uint256 amount) internal { + ISushiBar(SUSHI_BAR).leave(amount); + } + + function _sushiBalance() internal view returns (uint256) { + return IERC20(SUSHI_TOKEN).balanceOf(address(this)); + } + + function _xSushiBalance() internal view returns (uint256) { + return IERC20(SUSHI_BAR).balanceOf(address(this)); + } + + function _stakeMasterChef(uint256 amount) internal { + uint256 priorSushiBalance = _sushiBalance(); + + // Deposit to Master Chef and retrieve farmed SUSHI + IMasterChef(MASTER_CHEF).deposit(_poolId, amount); + + uint256 balance = (_sushiBalance()).sub(priorSushiBalance); + + // Stake SUSHI rewards to Sushi Bar + if (balance > 0) { + _stakeSushi(balance); + } + } + + function _unstakeMasterChef(uint256 amount) internal { + uint256 priorSushiBalance = _sushiBalance(); + + // Deposit to Master Chef and retrieve farmed SUSHI + IMasterChef(MASTER_CHEF).withdraw(_poolId, amount); + + uint256 balance = (_sushiBalance()).sub(priorSushiBalance); + + // Stake SUSHI rewards to Sushi Bar + if (balance > 0) { + _stakeSushi(balance); + } + } + + /** End of Sushi implementation */ + + /** Start of Rewards Aware AToken implementation */ + + /** + * @dev Param decoder to get Master Chef, Sushi bar and $SUSHI token addresses. + * @param params Additional variadic field to include extra params. Expected parameters: + * uint256 poolId The id of the Master Chef pool + * @return uint256 The pool id + */ + function _decodeParamsPoolId(bytes memory params) internal pure returns (uint256) { + uint256 poolId = abi.decode(params, (uint256)); + + return poolId; + } + + /** + * @dev External call to retrieve the lifetime accrued rewards of the aToken contract to the external Rewards Controller contract + */ + function _computeExternalLifetimeRewards(address) + internal + override + returns (uint256 lifetimeRewards) + { + uint256 pendingRewards = _pendingXSushiRewards; + _pendingXSushiRewards = 0; + return _getLifetimeRewards(SUSHI_BAR).add(pendingRewards); + } + + /** + * @dev External call to retrieve the lifetime accrued rewards of the aToken contract to the external Rewards Controller contract + */ + function _getExternalLifetimeRewards(address) + internal + view + override + returns (uint256 lifetimeRewards) + { + return _getLifetimeRewards(SUSHI_BAR).add(_pendingXSushiRewards); + } + + /** + * @dev External call to claim and stake SUSHI rewards + */ + function _claimRewardsFromController() internal override { + _stakeMasterChef(0); + } + + function _stake(address token, uint256 amount) internal override returns (uint256) { + if (token == UNDERLYING_ASSET_ADDRESS()) { + _stakeMasterChef(amount); + } + return amount; + } + + function _unstake(address token, uint256 amount) internal override returns (uint256) { + if (token == UNDERLYING_ASSET_ADDRESS()) { + _unstakeMasterChef(amount); + return amount; + } + return amount; + } + + /** End of Rewards Aware AToken functions */ + + /** Start of External getters */ + function getMasterChef() external view override returns (address) { + return MASTER_CHEF; + } + + function getSushiBar() external view override returns (address) { + return SUSHI_BAR; + } + + function getSushiToken() external view override returns (address) { + return SUSHI_TOKEN; + } + + function getMasterChefPoolId() external view override returns (uint256) { + return _poolId; + } + /** End of External getters */ +} diff --git a/helpers/constants.ts b/helpers/constants.ts index 79c50814..3bc2ee54 100644 --- a/helpers/constants.ts +++ b/helpers/constants.ts @@ -1,4 +1,5 @@ import BigNumber from 'bignumber.js'; +import { eEthereumNetwork } from './types'; // ---------------- // MATH @@ -72,3 +73,11 @@ export const MOCK_CHAINLINK_AGGREGATORS_PRICES = { USD: '5848466240000000', REW: oneEther.multipliedBy('0.00137893825230').toFixed(), }; + +export const MASTER_CHEF = { + [eEthereumNetwork.main]: '0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd', +}; +export const SUSHI_BAR = { [eEthereumNetwork.main]: '0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272' }; +export const SUSHI_TOKEN = { + [eEthereumNetwork.main]: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2', +}; diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 4aeb6d56..461c9dc0 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -51,6 +51,8 @@ import { FlashLiquidationAdapterFactory, RewardsTokenFactory, RewardsATokenMockFactory, + SushiAmmReserveInterestRateStrategyFactory, + SushiRewardsAwareATokenFactory, } from '../types'; import { withSaveAndVerify, @@ -68,6 +70,7 @@ import { readArtifact as buidlerReadArtifact } from '@nomiclabs/buidler/plugins' import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { LendingPoolLibraryAddresses } from '../types/LendingPoolFactory'; import { UiPoolDataProvider } from '../types'; +import { MASTER_CHEF, SUSHI_TOKEN } from './constants'; export const deployUiPoolDataProvider = async ( [incentivesController, aaveOracle]: [tEthereumAddress, tEthereumAddress], @@ -662,6 +665,8 @@ export const chooseATokenDeployment = (id: eContractid) => { return deployDelegationAwareATokenImpl; case eContractid.RewardsATokenMock: return deployRewardATokenMock; + case eContractid.SushiRewardsAwareAToken: + return deploySushiRewardAwareATokenByNetwork; default: throw Error(`Missing aToken implementation deployment script for: ${id}`); } @@ -714,3 +719,43 @@ export const deployATokenImplementations = async ( await deployGenericVariableDebtToken(verify); } }; + +export const deploySushiAmmReserveInterestRateStrategy = async ( + args: [tEthereumAddress, string, string, string, string, string, string], + verify: boolean +) => + withSaveAndVerify( + await new SushiAmmReserveInterestRateStrategyFactory(await getFirstSigner()).deploy(...args), + eContractid.SushiAmmReserveInterestRateStrategy, + args, + verify + ); + +export const deploySushiRewardsAwareAToken = async ( + masterChef: tEthereumAddress, + sushiBar: tEthereumAddress, + sushiToken: tEthereumAddress, + verify?: boolean +) => { + const args: [tEthereumAddress, tEthereumAddress, tEthereumAddress] = [ + masterChef, + sushiBar, + sushiToken, + ]; + return withSaveAndVerify( + await new SushiRewardsAwareATokenFactory(await getFirstSigner()).deploy(...args), + eContractid.SushiRewardsAwareAToken, + args, + verify + ); +}; + +export const deploySushiRewardAwareATokenByNetwork = async (verify?: boolean) => { + const network = DRE.network.name as eEthereumNetwork; + return deploySushiRewardsAwareAToken( + MASTER_CHEF[network], + SUSHI_TOKEN[network], + SUSHI_TOKEN[network], + verify + ); +}; diff --git a/helpers/misc-utils.ts b/helpers/misc-utils.ts index 54d5fa44..55cde7f2 100644 --- a/helpers/misc-utils.ts +++ b/helpers/misc-utils.ts @@ -9,6 +9,7 @@ import { BuidlerRuntimeEnvironment } from '@nomiclabs/buidler/types'; import { tEthereumAddress } from './types'; import { isAddress } from 'ethers/lib/utils'; import { isZeroAddress } from 'ethereumjs-util'; +import { SignerWithAddress } from '../test-suites/test-aave/helpers/make-suite'; export const toWad = (value: string | number) => new BigNumber(value).times(WAD).toFixed(); @@ -115,3 +116,16 @@ export const notFalsyOrZeroAddress = (address: tEthereumAddress | null | undefin } return isAddress(address) && !isZeroAddress(address); }; + +export const impersonateAddress = async (address: tEthereumAddress): Promise => { + await (DRE as HardhatRuntimeEnvironment).network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [address], + }); + const signer = await DRE.ethers.provider.getSigner(address); + + return { + signer, + address, + }; +}; diff --git a/helpers/types.ts b/helpers/types.ts index 8b210516..792989eb 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -89,6 +89,8 @@ export enum eContractid { FlashLiquidationAdapter = 'FlashLiquidationAdapter', RewardsATokenMock = 'RewardsATokenMock', RewardsToken = 'RewardsToken', + SushiAmmReserveInterestRateStrategy = 'SushiAmmReserveInterestRateStrategy', + SushiRewardsAwareAToken = 'SushiRewardsAwareAToken', } /* diff --git a/package.json b/package.json index 1caff7c1..3b569695 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "test-scenarios": "npm run compile && npx hardhat test test-suites/test-aave/__setup.spec.ts test-suites/test-aave/scenario.spec.ts", "test-subgraph:scenarios": "npm run compile && hardhat --network hardhatevm_docker test test-suites/test-aave/__setup.spec.ts test-suites/test-aave/subgraph-scenarios.spec.ts", "test:main:check-list": "npm run compile && FORK=main TS_NODE_TRANSPILE_ONLY=1 hardhat test test-suites/test-aave/__setup.spec.ts test-suites/test-aave/mainnet/check-list.spec.ts", + "test:main:sushi-rewards": "FORK_BLOCK_NUMBER=11919516 FORK=main TS_NODE_TRANSPILE_ONLY=1 hardhat test test-suites/test-aave/__setup.spec.ts test-suites/test-aave/mainnet/atoken-sushi-rewards.main.ts", "dev:coverage": "buidler compile --force && buidler coverage --network coverage", "aave:evm:dev:migration": "npm run compile && hardhat aave:dev", "aave:docker:full:migration": "npm run compile && npm run hardhat:docker -- aave:mainnet --skip-registry", diff --git a/test-suites/test-aave/mainnet/atoken-sushi-rewards.main.ts b/test-suites/test-aave/mainnet/atoken-sushi-rewards.main.ts new file mode 100644 index 00000000..d24fe7b2 --- /dev/null +++ b/test-suites/test-aave/mainnet/atoken-sushi-rewards.main.ts @@ -0,0 +1,568 @@ +import { + MASTER_CHEF, + MAX_UINT_AMOUNT, + SUSHI_BAR, + SUSHI_TOKEN, + ZERO_ADDRESS, +} from '../../../helpers/constants'; +import { makeSuite, SignerWithAddress, TestEnv } from '../helpers/make-suite'; +import { + evmRevert, + evmSnapshot, + increaseTime, + impersonateAddress, + waitForTx, + DRE, +} from '../../../helpers/misc-utils'; +import { + getAaveOracle, + getATokensAndRatesHelper, + getFirstSigner, + getLendingPool, + getLendingPoolAddressesProvider, + getLendingPoolConfiguratorProxy, +} from '../../../helpers/contracts-getters'; +import { + deploySushiAmmReserveInterestRateStrategy, + deploySushiRewardsAwareAToken, +} from '../../../helpers/contracts-deployments'; +import { IERC20Factory } from '../../../types/IERC20Factory'; +import BigNumberJs from 'bignumber.js'; +import { eContractid, eEthereumNetwork, RateMode, tEthereumAddress } from '../../../helpers/types'; +import { strategyWETH } from '../../../markets/aave/reservesConfigs'; +import { checkRewards } from '../helpers/rewards-distribution/verify'; +import { IRewardsAwareAToken } from '../../../types/IRewardsAwareAToken'; +import { IRewardsAwareATokenFactory } from '../../../types/IRewardsAwareATokenFactory'; +import { BigNumber, BigNumberish } from 'ethers'; +import { parseEther } from 'ethers/lib/utils'; +import { IERC20 } from '../../../types/IERC20'; +import { IMasterChefFactory } from '../../../types/IMasterChefFactory'; +import { loadPoolConfig, ConfigNames } from '../../../helpers/configuration'; +import { + getParamPerNetwork, + getContractAddressWithJsonFallback, +} from '../../../helpers/contracts-helpers'; +import { IMasterChef } from '../../../types/IMasterChef'; + +const ONE_DAY = 86400; +const { expect } = require('chai'); + +interface SLPInfo { + address: tEthereumAddress; + name: string; + symbol: string; +} + +const poolId = '12'; +const USER_ADDRESS = '0x40e234f653Ac53e94B1F097a6f67756A164Cdb2D'; + +const LP_SUSHI_WETH: SLPInfo = { + address: '0x795065dCc9f64b5614C407a6EFDC400DA6221FB0', + name: 'Sushi SLP SUSHI-WETH', + symbol: 'SLP-SUSHI-WETH', +}; + +const listSushiShareLP = async (slp: SLPInfo) => { + const { symbol } = slp; + const poolConfig = loadPoolConfig(ConfigNames.Aave); + const aTokenAndRatesDeployer = await getATokensAndRatesHelper(); + const aaveOracle = await getAaveOracle(); + + const { + SymbolPrefix: symbolPrefix, + ATokenNamePrefix: aTokenNamePrefix, + StableDebtTokenNamePrefix: stableDebtTokenNamePrefix, + VariableDebtTokenNamePrefix: variableDebtTokenNamePrefix, + } = poolConfig; + const addressProvider = await getLendingPoolAddressesProvider(); + const poolConfigurator = await getLendingPoolConfiguratorProxy(); + const admin = await addressProvider.getPoolAdmin(); + + const treasury = await getParamPerNetwork( + poolConfig.ReserveFactorTreasuryAddress, + eEthereumNetwork.main + ); + const aTokenImpl = ( + await deploySushiRewardsAwareAToken( + MASTER_CHEF[eEthereumNetwork.main], + SUSHI_BAR[eEthereumNetwork.main], + SUSHI_TOKEN[eEthereumNetwork.main] + ) + ).address; + const stableDebtTokenImpl = await getContractAddressWithJsonFallback( + eContractid.StableDebtToken, + ConfigNames.Aave + ); + const variableDebtTokenImpl = await getContractAddressWithJsonFallback( + eContractid.VariableDebtToken, + ConfigNames.Aave + ); + const interestStrategy = await deploySushiAmmReserveInterestRateStrategy( + [ + addressProvider.address, + strategyWETH.strategy.optimalUtilizationRate, + strategyWETH.strategy.baseVariableBorrowRate, + strategyWETH.strategy.variableRateSlope1, + strategyWETH.strategy.variableRateSlope2, + strategyWETH.strategy.stableRateSlope1, + strategyWETH.strategy.stableRateSlope2, + ], + false + ); + const interestRateStrategyAddress = interestStrategy.address; + + const sushiParams = DRE.ethers.utils.defaultAbiCoder.encode(['uint256'], [poolId]); + + const curveReserveInitParams = [ + { + aTokenImpl, + stableDebtTokenImpl, + variableDebtTokenImpl, + underlyingAssetDecimals: '18', + interestRateStrategyAddress, + underlyingAsset: slp.address, + treasury, + incentivesController: ZERO_ADDRESS, + underlyingAssetName: slp.symbol, + aTokenName: `${aTokenNamePrefix} ${symbol}`, + aTokenSymbol: `a${symbolPrefix}${symbol}`, + variableDebtTokenName: `${variableDebtTokenNamePrefix} ${symbolPrefix}${symbol}`, + variableDebtTokenSymbol: `variableDebt${symbolPrefix}${symbol}`, + stableDebtTokenName: `${stableDebtTokenNamePrefix} ${symbol}`, + stableDebtTokenSymbol: `stableDebt${symbolPrefix}${symbol}`, + params: sushiParams, + }, + ]; + const reserveConfig = [ + { + asset: slp.address, + baseLTV: strategyWETH.baseLTVAsCollateral, + liquidationThreshold: strategyWETH.liquidationThreshold, + liquidationBonus: strategyWETH.liquidationBonus, + reserveFactor: strategyWETH.reserveFactor, + stableBorrowingEnabled: strategyWETH.stableBorrowRateEnabled, + borrowingEnabled: strategyWETH.borrowingEnabled, + }, + ]; + // Set SUSHI LP as WBTC price until proper oracle aggregator deployment + await aaveOracle.setAssetSources( + [slp.address], + [getParamPerNetwork(poolConfig.ChainlinkAggregator, eEthereumNetwork.main).WBTC.toString()] + ); + + // Init reserve + await waitForTx(await poolConfigurator.batchInitReserve(curveReserveInitParams)); + + // Configure reserve + await waitForTx(await addressProvider.setPoolAdmin(aTokenAndRatesDeployer.address)); + await waitForTx(await aTokenAndRatesDeployer.configureReserves(reserveConfig)); + await waitForTx(await addressProvider.setPoolAdmin(admin)); +}; + +const deposit = async ( + key: SignerWithAddress, + slp: SLPInfo, + aGaugeAddress: tEthereumAddress, + amount: BigNumber, + shouldReward?: boolean +) => { + const pool = await getLendingPool(); + const slpErc20 = IERC20Factory.connect(slp.address, key.signer); + + await slpErc20.connect(key.signer).approve(pool.address, amount); + + const txDeposit = await waitForTx( + await pool.connect(key.signer).deposit(slp.address, amount, key.address, '0') + ); + + await checkRewards(key, aGaugeAddress, txDeposit.blockNumber, shouldReward); +}; + +const withdraw = async ( + key: SignerWithAddress, + slp: SLPInfo, + aSLPAdress: tEthereumAddress, + amount?: BigNumberish, + shouldReward = true +) => { + const pool = await getLendingPool(); + const aSLP = IRewardsAwareATokenFactory.connect(aSLPAdress, key.signer); + + const withdrawAmount = amount ? amount : await aSLP.balanceOf(key.address); + await aSLP.connect(key.signer).approve(pool.address, withdrawAmount); + + const txWithdraw = await waitForTx( + await pool.connect(key.signer).withdraw(slp.address, withdrawAmount, key.address) + ); + + await checkRewards(key, aSLPAdress, txWithdraw.blockNumber, shouldReward); +}; + +const withdrawFarm = async (key: SignerWithAddress) => { + const masterChef = IMasterChefFactory.connect(MASTER_CHEF[eEthereumNetwork.main], key.signer); + const { 0: amount } = await masterChef.userInfo(poolId, key.address); + + await masterChef.withdraw(poolId, amount); +}; + +const claim = async (key: SignerWithAddress, aSLPAdress: tEthereumAddress, shouldReward = true) => { + const aSLP = IRewardsAwareATokenFactory.connect(aSLPAdress, key.signer); + const rewardTokens = await aSLP.getRewardsTokenAddressList(); + + for (let x = 0; x < rewardTokens.length; x++) { + if (rewardTokens[x] == ZERO_ADDRESS) break; + const balanceBefore = await IERC20Factory.connect(rewardTokens[x], key.signer).balanceOf( + key.address + ); + const txClaim = await waitForTx(await aSLP.claim(rewardTokens[x])); + + await checkRewards( + key, + aSLPAdress, + txClaim.blockNumber, + shouldReward, + rewardTokens[x], + balanceBefore + ); + } +}; + +makeSuite('Sushi LP Rewards Aware aToken', (testEnv: TestEnv) => { + let evmSnapshotId; + let depositor: SignerWithAddress; + let secondDepositor: SignerWithAddress; + let thirdDepositor: SignerWithAddress; + + let aSLP_SUSHI_WETH: IRewardsAwareAToken; + + let sushiToken: IERC20; + + let xSushiToken: IERC20; + + let masterChef: IMasterChef; + + let LP_SUSHI_WETH_TOKEN: IERC20; + + before('Initializing configuration', async () => { + // Sets BigNumber for this suite, instead of globally + BigNumberJs.config({ DECIMAL_PLACES: 0, ROUNDING_MODE: BigNumberJs.ROUND_DOWN }); + + // Set local vars + depositor = await impersonateAddress(USER_ADDRESS); + secondDepositor = testEnv.users[2]; + thirdDepositor = testEnv.users[3]; + + // Gauge tokens should be listed at Aave test deployment + await listSushiShareLP(LP_SUSHI_WETH); + + const allTokens = await testEnv.helpersContract.getAllATokens(); + + LP_SUSHI_WETH_TOKEN = await IERC20Factory.connect(LP_SUSHI_WETH.address, depositor.signer); + + aSLP_SUSHI_WETH = IRewardsAwareATokenFactory.connect( + allTokens.find((aToken) => aToken.symbol.includes(LP_SUSHI_WETH.symbol))?.tokenAddress || + ZERO_ADDRESS, + await getFirstSigner() + ); + + sushiToken = IERC20Factory.connect( + await IMasterChefFactory.connect( + MASTER_CHEF[eEthereumNetwork.main], + depositor.signer + ).sushi(), + depositor.signer + ); + + xSushiToken = IERC20Factory.connect(SUSHI_BAR[eEthereumNetwork.main], depositor.signer); + + masterChef = IMasterChefFactory.connect(MASTER_CHEF[eEthereumNetwork.main], depositor.signer); + + // Retrieve LP tokens from farm + await withdrawFarm(depositor); + + // Set reserve factor to 20% + await aSLP_SUSHI_WETH.connect(testEnv.deployer.signer).setRewardsReserveFactor('2000'); + }); + + after('Reset', () => { + // Reset BigNumber + BigNumberJs.config({ DECIMAL_PLACES: 20, ROUNDING_MODE: BigNumberJs.ROUND_HALF_UP }); + }); + + describe('AToken with Sushi rewards: deposit, claim and withdraw SUSHI-WETH', () => { + let DEPOSIT_AMOUNT: BigNumber; + + before(async () => { + evmSnapshotId = await evmSnapshot(); + DEPOSIT_AMOUNT = await IERC20Factory.connect( + LP_SUSHI_WETH.address, + depositor.signer + ).balanceOf(depositor.address); + }); + + after(async () => { + await evmRevert(evmSnapshotId); + }); + + it('Deposit and generate user reward checkpoints', async () => { + // Deposits + await deposit(depositor, LP_SUSHI_WETH, aSLP_SUSHI_WETH.address, DEPOSIT_AMOUNT); + const rewardATokenBalance = await sushiToken.balanceOf(aSLP_SUSHI_WETH.address); + const depositorBalance = await IERC20Factory.connect( + LP_SUSHI_WETH.address, + depositor.signer + ).balanceOf(depositor.address); + const xSUSHIATokenBalance = await xSushiToken.balanceOf(aSLP_SUSHI_WETH.address); + expect(rewardATokenBalance).to.be.eq('0', 'SUSHI rewards should be zero at contract'); + expect(xSUSHIATokenBalance).to.be.eq('0', 'xSUSHI should be zero at contract'); + expect(depositorBalance).to.be.eq( + '0', + 'Depositor ERC20 balance should be zero after deposit' + ); + }); + + it('Increase time and claim SUSHI', async () => { + // Pass time to generate rewards + await increaseTime(ONE_DAY); + + // Claim + await claim(depositor, aSLP_SUSHI_WETH.address); + const rewardATokenBalance = await sushiToken.balanceOf(aSLP_SUSHI_WETH.address); + + expect(rewardATokenBalance).to.be.eq( + '0', + 'SUSHI Balance should be zero as there is only one aToken holder' + ); + }); + + it('Pass time and withdraw SUSHI-WETH', async () => { + // Pass time to generate rewards + await increaseTime(ONE_DAY); + + // Withdraw + await withdraw(depositor, LP_SUSHI_WETH, aSLP_SUSHI_WETH.address); + const rewardATokenBalance = await sushiToken.balanceOf(aSLP_SUSHI_WETH.address); + const depositorBalance = await IERC20Factory.connect( + LP_SUSHI_WETH.address, + depositor.signer + ).balanceOf(depositor.address); + const xSUSHIATokenBalance = await xSushiToken.balanceOf(aSLP_SUSHI_WETH.address); + expect(rewardATokenBalance).to.be.eq('0', 'SUSHI rewards should be zero at contract'); + expect(xSUSHIATokenBalance).to.be.gt('0', 'Should be staking xSUSHI at contract'); + expect(depositorBalance).to.be.eq( + DEPOSIT_AMOUNT, + 'Depositor should had initial ERC20 balance' + ); + }); + + it('Claim the remaining Sushi', async () => { + await claim(depositor, aSLP_SUSHI_WETH.address); + const rewardATokenBalance = await sushiToken.balanceOf(aSLP_SUSHI_WETH.address); + const depositorBalance = await IERC20Factory.connect( + LP_SUSHI_WETH.address, + depositor.signer + ).balanceOf(depositor.address); + const xSUSHIContractBalance = await xSushiToken.balanceOf(aSLP_SUSHI_WETH.address); + expect(rewardATokenBalance).to.be.eq('0', 'SUSHI rewards should be zero at contract'); + expect(depositorBalance).to.be.eq( + DEPOSIT_AMOUNT, + 'Depositor should had initial ERC20 balance' + ); + expect(xSUSHIContractBalance).to.be.lte( + '10', + 'XSUSHI balance should be near zero at contract' + ); + }); + }); + describe('AToken with Sushi rewards: deposit SUSHI-WETH, borrow and repay', () => { + let DEPOSIT_AMOUNT: BigNumber; + let DEPOSIT_AMOUNT_2: BigNumber; + let DEPOSIT_AMOUNT_3: BigNumber; + + before(async () => { + evmSnapshotId = await evmSnapshot(); + const balanceToShare = (await LP_SUSHI_WETH_TOKEN.balanceOf(depositor.address)).div(2); + + DEPOSIT_AMOUNT_2 = balanceToShare.div(3); + DEPOSIT_AMOUNT_3 = balanceToShare.sub(DEPOSIT_AMOUNT_2); + + await LP_SUSHI_WETH_TOKEN.transfer(secondDepositor.address, DEPOSIT_AMOUNT_2); + await LP_SUSHI_WETH_TOKEN.transfer(thirdDepositor.address, DEPOSIT_AMOUNT_3); + + DEPOSIT_AMOUNT = (await LP_SUSHI_WETH_TOKEN.balanceOf(depositor.address)).div(2); + }); + + after(async () => { + await evmRevert(evmSnapshotId); + }); + + it('Deposit', async () => { + const priorDepositorBalance = await IERC20Factory.connect( + LP_SUSHI_WETH.address, + depositor.signer + ).balanceOf(depositor.address); + + // Deposits + await deposit(depositor, LP_SUSHI_WETH, aSLP_SUSHI_WETH.address, DEPOSIT_AMOUNT); + await deposit(secondDepositor, LP_SUSHI_WETH, aSLP_SUSHI_WETH.address, DEPOSIT_AMOUNT_2); + await deposit(thirdDepositor, LP_SUSHI_WETH, aSLP_SUSHI_WETH.address, DEPOSIT_AMOUNT_3); + + const rewardATokenBalance = await sushiToken.balanceOf(aSLP_SUSHI_WETH.address); + const depositorBalance = await IERC20Factory.connect( + LP_SUSHI_WETH.address, + depositor.signer + ).balanceOf(depositor.address); + const { 0: masterChefBalanceAfter } = await masterChef.userInfo( + '12', + aSLP_SUSHI_WETH.address + ); + + expect(masterChefBalanceAfter).to.be.eq( + DEPOSIT_AMOUNT.add(DEPOSIT_AMOUNT_2).add(DEPOSIT_AMOUNT_3), + 'Deposit amount should be staked into Master Chef to receive $SUSHI' + ); + expect(rewardATokenBalance).to.be.eq('0', 'xSUSHI rewards should be zero at contract'); + expect(depositorBalance).to.be.eq( + priorDepositorBalance.sub(DEPOSIT_AMOUNT), + 'Depositor ERC20 balance should be correct after deposit' + ); + }); + + it('Depositor 1 Borrow 1 LP_SUSHI_WETH', async () => { + const lendingPool = await getLendingPool(); + const borrowAmount = parseEther('1'); + + // Pass time to generate rewards + await increaseTime(ONE_DAY); + const { 0: masterChefBalancePrior } = await masterChef.userInfo( + '12', + aSLP_SUSHI_WETH.address + ); + const priorDepositorBalance = await IERC20Factory.connect( + LP_SUSHI_WETH.address, + depositor.signer + ).balanceOf(depositor.address); + + // Borrow LP_SUSHI_WETH + await waitForTx( + await lendingPool + .connect(depositor.signer) + .borrow(LP_SUSHI_WETH.address, borrowAmount, RateMode.Variable, '0', depositor.address) + ); + + const rewardATokenBalance = await sushiToken.balanceOf(aSLP_SUSHI_WETH.address); + const depositorBalance = await IERC20Factory.connect( + LP_SUSHI_WETH.address, + depositor.signer + ).balanceOf(depositor.address); + const xSUSHIATokenBalance = await xSushiToken.balanceOf(aSLP_SUSHI_WETH.address); + const { 0: masterChefBalanceAfter } = await masterChef.userInfo( + '12', + aSLP_SUSHI_WETH.address + ); + + expect(rewardATokenBalance).to.be.eq('0', 'SUSHI rewards should be zero at contract'); + expect(xSUSHIATokenBalance).to.be.gt( + '0', + 'xSUSHI should be gt 0 at contract due not claimed' + ); + expect(depositorBalance).to.be.eq( + priorDepositorBalance.add(borrowAmount), + 'Depositor ERC20 balance should be equal to prior balance + borrowed amount ' + ); + expect(masterChefBalanceAfter).to.be.eq( + masterChefBalancePrior.sub(borrowAmount), + 'Staked tokens balance should subtract borrowed balance' + ); + }); + + it('Depositor 1 Repay 1 LP_SUSHI_WETH', async () => { + const lendingPool = await getLendingPool(); + const priorDepositorBalance = await IERC20Factory.connect( + LP_SUSHI_WETH.address, + depositor.signer + ).balanceOf(depositor.address); + + const { 0: masterChefBalancePrior } = await masterChef.userInfo( + '12', + aSLP_SUSHI_WETH.address + ); + + // Pass time to generate rewards + await increaseTime(ONE_DAY); + + // Approve Repay + await waitForTx( + await IERC20Factory.connect(LP_SUSHI_WETH.address, depositor.signer).approve( + lendingPool.address, + MAX_UINT_AMOUNT + ) + ); + // Repay + await waitForTx( + await lendingPool + .connect(depositor.signer) + .repay(LP_SUSHI_WETH.address, MAX_UINT_AMOUNT, RateMode.Variable, depositor.address) + ); + + const rewardATokenBalance = await sushiToken.balanceOf(aSLP_SUSHI_WETH.address); + const depositorBalance = await IERC20Factory.connect( + LP_SUSHI_WETH.address, + depositor.signer + ).balanceOf(depositor.address); + const xSUSHIATokenBalance = await xSushiToken.balanceOf(aSLP_SUSHI_WETH.address); + const { 0: masterChefBalanceAfter } = await masterChef.userInfo( + '12', + aSLP_SUSHI_WETH.address + ); + + expect(masterChefBalanceAfter).to.be.gt( + masterChefBalancePrior, + 'Master chef balance should be greater after repayment' + ); + expect(rewardATokenBalance).to.be.eq('0', 'SUSHI rewards should be zero at contract'); + expect(xSUSHIATokenBalance).to.be.gt( + '0', + 'xSUSHI should be gt 0 at contract due not claimed' + ); + expect(depositorBalance).to.be.lt( + priorDepositorBalance, + 'Depositor ERC20 balance should be less than prior balance due repayment' + ); + }); + it('Claim SUSHI', async () => { + // Pass time to generate rewards + await increaseTime(ONE_DAY); + + // Claim + await claim(thirdDepositor, aSLP_SUSHI_WETH.address); + await claim(secondDepositor, aSLP_SUSHI_WETH.address); + await claim(depositor, aSLP_SUSHI_WETH.address); + + const rewardATokenBalance = await sushiToken.balanceOf(aSLP_SUSHI_WETH.address); + expect(rewardATokenBalance).to.be.eq('0', 'SUSHI rewards should be always zero at contract'); + }); + it('Depositor 1 Withdraw SUSHI-WETH', async () => { + // Pass time to generate rewards + await increaseTime(ONE_DAY); + + // Withdraw + await withdraw(depositor, LP_SUSHI_WETH, aSLP_SUSHI_WETH.address, parseEther('10')); + const rewardATokenBalance = await sushiToken.balanceOf(aSLP_SUSHI_WETH.address); + expect(rewardATokenBalance).to.be.eq('0', 'SUSHI rewards should be zero at contract'); + }); + + it('All depositors claim the remaining SUSHI', async () => { + // Pass time to generate rewards + await increaseTime(ONE_DAY); + + // Claim + await claim(thirdDepositor, aSLP_SUSHI_WETH.address); + await claim(secondDepositor, aSLP_SUSHI_WETH.address); + await claim(depositor, aSLP_SUSHI_WETH.address); + + const rewardATokenBalance = await sushiToken.balanceOf(aSLP_SUSHI_WETH.address); + expect(rewardATokenBalance).to.be.eq('0', 'SUSHI rewards should be always zero at contract'); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 9974c3b6..20f37d18 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ "noImplicitAny": false, "resolveJsonModule": true }, - "include": ["./scripts", "./test", "./tasks", "test-suites/test-aave/uniswapAdapters.repay.spec.ts", "test-suites/test-aave/upgradeability.spec.ts", "test-suites/test-aave/variable-debt-token.spec.ts", "test-suites/test-aave/weth-gateway.spec.ts"], + "include": ["./scripts", "./tasks", "./test-suites", "./helpers"], "files": [ "./hardhat.config.ts", "./modules/tenderly/tenderly.d.ts",