mirror of
https://github.com/Instadapp/aave-protocol-v2.git
synced 2024-07-29 21:47:30 +00:00
feat: Added support for Reward Aware AToken for Sushi LP shares
This commit is contained in:
parent
512877af44
commit
eed776ca86
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
26
contracts/adapters/interfaces/sushi/IMasterChef.sol
Normal file
26
contracts/adapters/interfaces/sushi/IMasterChef.sol
Normal file
|
@ -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);
|
||||||
|
}
|
8
contracts/adapters/interfaces/sushi/ISushiBar.sol
Normal file
8
contracts/adapters/interfaces/sushi/ISushiBar.sol
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
233
contracts/adapters/rewards/SushiRewardsAwareAToken.sol
Normal file
233
contracts/adapters/rewards/SushiRewardsAwareAToken.sol
Normal file
|
@ -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 */
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { eEthereumNetwork } from './types';
|
||||||
|
|
||||||
// ----------------
|
// ----------------
|
||||||
// MATH
|
// MATH
|
||||||
|
@ -72,3 +73,11 @@ export const MOCK_CHAINLINK_AGGREGATORS_PRICES = {
|
||||||
USD: '5848466240000000',
|
USD: '5848466240000000',
|
||||||
REW: oneEther.multipliedBy('0.00137893825230').toFixed(),
|
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',
|
||||||
|
};
|
||||||
|
|
|
@ -51,6 +51,8 @@ import {
|
||||||
FlashLiquidationAdapterFactory,
|
FlashLiquidationAdapterFactory,
|
||||||
RewardsTokenFactory,
|
RewardsTokenFactory,
|
||||||
RewardsATokenMockFactory,
|
RewardsATokenMockFactory,
|
||||||
|
SushiAmmReserveInterestRateStrategyFactory,
|
||||||
|
SushiRewardsAwareATokenFactory,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import {
|
import {
|
||||||
withSaveAndVerify,
|
withSaveAndVerify,
|
||||||
|
@ -68,6 +70,7 @@ import { readArtifact as buidlerReadArtifact } from '@nomiclabs/buidler/plugins'
|
||||||
import { HardhatRuntimeEnvironment } from 'hardhat/types';
|
import { HardhatRuntimeEnvironment } from 'hardhat/types';
|
||||||
import { LendingPoolLibraryAddresses } from '../types/LendingPoolFactory';
|
import { LendingPoolLibraryAddresses } from '../types/LendingPoolFactory';
|
||||||
import { UiPoolDataProvider } from '../types';
|
import { UiPoolDataProvider } from '../types';
|
||||||
|
import { MASTER_CHEF, SUSHI_TOKEN } from './constants';
|
||||||
|
|
||||||
export const deployUiPoolDataProvider = async (
|
export const deployUiPoolDataProvider = async (
|
||||||
[incentivesController, aaveOracle]: [tEthereumAddress, tEthereumAddress],
|
[incentivesController, aaveOracle]: [tEthereumAddress, tEthereumAddress],
|
||||||
|
@ -662,6 +665,8 @@ export const chooseATokenDeployment = (id: eContractid) => {
|
||||||
return deployDelegationAwareATokenImpl;
|
return deployDelegationAwareATokenImpl;
|
||||||
case eContractid.RewardsATokenMock:
|
case eContractid.RewardsATokenMock:
|
||||||
return deployRewardATokenMock;
|
return deployRewardATokenMock;
|
||||||
|
case eContractid.SushiRewardsAwareAToken:
|
||||||
|
return deploySushiRewardAwareATokenByNetwork;
|
||||||
default:
|
default:
|
||||||
throw Error(`Missing aToken implementation deployment script for: ${id}`);
|
throw Error(`Missing aToken implementation deployment script for: ${id}`);
|
||||||
}
|
}
|
||||||
|
@ -714,3 +719,43 @@ export const deployATokenImplementations = async (
|
||||||
await deployGenericVariableDebtToken(verify);
|
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
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { BuidlerRuntimeEnvironment } from '@nomiclabs/buidler/types';
|
||||||
import { tEthereumAddress } from './types';
|
import { tEthereumAddress } from './types';
|
||||||
import { isAddress } from 'ethers/lib/utils';
|
import { isAddress } from 'ethers/lib/utils';
|
||||||
import { isZeroAddress } from 'ethereumjs-util';
|
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();
|
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);
|
return isAddress(address) && !isZeroAddress(address);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const impersonateAddress = async (address: tEthereumAddress): Promise<SignerWithAddress> => {
|
||||||
|
await (DRE as HardhatRuntimeEnvironment).network.provider.request({
|
||||||
|
method: 'hardhat_impersonateAccount',
|
||||||
|
params: [address],
|
||||||
|
});
|
||||||
|
const signer = await DRE.ethers.provider.getSigner(address);
|
||||||
|
|
||||||
|
return {
|
||||||
|
signer,
|
||||||
|
address,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -89,6 +89,8 @@ export enum eContractid {
|
||||||
FlashLiquidationAdapter = 'FlashLiquidationAdapter',
|
FlashLiquidationAdapter = 'FlashLiquidationAdapter',
|
||||||
RewardsATokenMock = 'RewardsATokenMock',
|
RewardsATokenMock = 'RewardsATokenMock',
|
||||||
RewardsToken = 'RewardsToken',
|
RewardsToken = 'RewardsToken',
|
||||||
|
SushiAmmReserveInterestRateStrategy = 'SushiAmmReserveInterestRateStrategy',
|
||||||
|
SushiRewardsAwareAToken = 'SushiRewardsAwareAToken',
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -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-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-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: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",
|
"dev:coverage": "buidler compile --force && buidler coverage --network coverage",
|
||||||
"aave:evm:dev:migration": "npm run compile && hardhat aave:dev",
|
"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",
|
"aave:docker:full:migration": "npm run compile && npm run hardhat:docker -- aave:mainnet --skip-registry",
|
||||||
|
|
568
test-suites/test-aave/mainnet/atoken-sushi-rewards.main.ts
Normal file
568
test-suites/test-aave/mainnet/atoken-sushi-rewards.main.ts
Normal file
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -8,7 +8,7 @@
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"resolveJsonModule": true
|
"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": [
|
"files": [
|
||||||
"./hardhat.config.ts",
|
"./hardhat.config.ts",
|
||||||
"./modules/tenderly/tenderly.d.ts",
|
"./modules/tenderly/tenderly.d.ts",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user