diff --git a/contracts/adapters/interests-strategies/CurveLpReserveInterestRateStratregy.sol.keep b/contracts/adapters/interests-strategies/CurveLpReserveInterestRateStratregy.sol.keep new file mode 100644 index 00000000..ac154c12 --- /dev/null +++ b/contracts/adapters/interests-strategies/CurveLpReserveInterestRateStratregy.sol.keep @@ -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 CurveLPReserveInterestRateStrategy 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/curve/ICurveFeeDistributor.sol b/contracts/adapters/interfaces/curve/ICurveFeeDistributor.sol new file mode 100644 index 00000000..a7870a54 --- /dev/null +++ b/contracts/adapters/interfaces/curve/ICurveFeeDistributor.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +interface ICurveFeeDistributor { + function claim() external; +} diff --git a/contracts/adapters/interfaces/curve/IVotingEscrow.sol b/contracts/adapters/interfaces/curve/IVotingEscrow.sol new file mode 100644 index 00000000..610c3387 --- /dev/null +++ b/contracts/adapters/interfaces/curve/IVotingEscrow.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +interface IVotingEscrow { + function create_lock(uint256 value, uint256 time) external; + + function increase_amount(uint256 value) external; + + function increase_unlock_time(uint256 time) external; + + function withdraw() external; +} diff --git a/contracts/adapters/rewards/CurveRewardsAwareAToken.sol b/contracts/adapters/rewards/CurveGaugeRewardsAwareAToken.sol similarity index 99% rename from contracts/adapters/rewards/CurveRewardsAwareAToken.sol rename to contracts/adapters/rewards/CurveGaugeRewardsAwareAToken.sol index 9feeaa27..2d1b1004 100644 --- a/contracts/adapters/rewards/CurveRewardsAwareAToken.sol +++ b/contracts/adapters/rewards/CurveGaugeRewardsAwareAToken.sol @@ -14,7 +14,7 @@ import {IAaveIncentivesController} from '../../interfaces/IAaveIncentivesControl * @notice AToken aware to claim and distribute rewards from an external Curve Gauge controller. * @author Aave */ -contract CurveRewardsAwareAToken is RewardsAwareAToken { +contract CurveGaugeRewardsAwareAToken is RewardsAwareAToken { // CRV token address address internal immutable CRV_TOKEN; diff --git a/contracts/adapters/rewards/CurveTreasury.sol b/contracts/adapters/rewards/CurveTreasury.sol new file mode 100644 index 00000000..81428fb3 --- /dev/null +++ b/contracts/adapters/rewards/CurveTreasury.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +import {IERC20} from '../../dependencies/openzeppelin/contracts/IERC20.sol'; +import {SafeERC20} from '../../dependencies/openzeppelin/contracts/SafeERC20.sol'; +import {ICurveGauge, ICurveGaugeView} from '../interfaces/curve/ICurveGauge.sol'; +import {IVotingEscrow} from '../interfaces/curve/IVotingEscrow.sol'; +import { + VersionedInitializable +} from '../../protocol/libraries/aave-upgradeability/VersionedInitializable.sol'; +import {ICurveFeeDistributor} from '../interfaces/curve/ICurveFeeDistributor.sol'; + +/** + * @title Curve Treasury that holds Curve LP and Gauge tokens + * @notice The treasury holds Curve assets like LP or Gauge tokens and can lock veCRV for boosting Curve yields + * @author Aave + */ +contract CurveTreasury is VersionedInitializable { + using SafeERC20 for IERC20; + + address immutable VOTING_ESCROW; + address immutable CRV_TOKEN; + address immutable FEE_DISTRIBUTOR; + address private _owner; + + uint256 public constant TREASURY_REVISION = 0x1; + + mapping(address => mapping(address => bool)) internal _entityTokenWhitelist; + mapping(address => mapping(address => address)) internal _entityTokenGauge; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + constructor( + address _votingEscrow, + address _crvToken, + address _curveFeeDistributor + ) public { + VOTING_ESCROW = _votingEscrow; + CRV_TOKEN = _crvToken; + FEE_DISTRIBUTOR = _curveFeeDistributor; + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(_owner == msg.sender, 'Ownable: caller is not the owner'); + _; + } + + modifier onlyWhitelistedEntity(address token) { + require(_entityTokenWhitelist[msg.sender][token] == true, 'ENTITY_NOT_WHITELISTED'); + _; + } + + function initialize( + address[] calldata entities, + address[] calldata tokens, + address[] calldata gauges, + address owner + ) external virtual initializer { + _owner = owner; + bool[] memory whitelisted = new bool[](entities.length); + _setWhitelist(entities, tokens, gauges, whitelisted); + } + + function getRevision() internal pure virtual override returns (uint256) { + return TREASURY_REVISION; + } + + function deposit( + address token, + uint256 amount, + bool useGauge + ) external onlyWhitelistedEntity(token) { + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + + if (useGauge && _entityTokenGauge[msg.sender][token] != address(0)) { + stakeGauge(_entityTokenGauge[msg.sender][token], amount); + } + } + + function withdraw( + address token, + uint256 amount, + bool useGauge + ) external onlyWhitelistedEntity(token) { + if (useGauge && _entityTokenGauge[msg.sender][token] != address(0)) { + unstakeGauge(_entityTokenGauge[msg.sender][token], amount); + } + IERC20(token).safeTransfer(msg.sender, amount); + } + + function stakeGauge(address gauge, uint256 amount) internal { + ICurveGauge(gauge).deposit(amount); + } + + function unstakeGauge(address gauge, uint256 amount) internal { + ICurveGauge(gauge).withdraw(amount); + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() external view returns (address) { + return _owner; + } + + /** Owner methods */ + function approve( + address token, + address to, + uint256 amount + ) external onlyOwner { + IERC20(token).safeApprove(to, amount); + } + + function transferFrom( + address token, + address from, + address to, + uint256 amount + ) external onlyOwner { + IERC20(token).safeTransferFrom(from, to, amount); + } + + function setWhitelist( + address[] calldata entities, + address[] calldata tokens, + address[] calldata gauges, + bool[] memory whitelisted + ) external onlyOwner { + _setWhitelist(entities, tokens, gauges, whitelisted); + } + + function _setWhitelist( + address[] calldata entities, + address[] calldata tokens, + address[] calldata gauges, + bool[] memory whitelisted + ) internal { + for (uint256 e; e < entities.length; e++) { + _entityTokenWhitelist[entities[e]][tokens[e]] = whitelisted[e]; + IERC20(tokens[e]).safeApprove(entities[e], type(uint256).max); + if (gauges[e] != address(0)) { + IERC20(tokens[e]).safeApprove(gauges[e], type(uint256).max); + _entityTokenGauge[entities[e]][tokens[e]] = gauges[e]; + } + } + } + + function claimCurveFees() external onlyOwner { + ICurveFeeDistributor(FEE_DISTRIBUTOR).claim(); + } + + /** Owner methods related with veCRV to interact with Voting Escrow Curve contract */ + function lockCrv(uint256 amount, uint256 unlockTime) external onlyOwner { + IERC20(CRV_TOKEN).safeApprove(VOTING_ESCROW, amount); + IVotingEscrow(VOTING_ESCROW).create_lock(amount, unlockTime); + } + + function unlockCrv(uint256 amount, uint256 unlockTime) external onlyOwner { + IVotingEscrow(VOTING_ESCROW).withdraw(); + } + + function increaseLockedCrv(uint256 amount) external onlyOwner { + IERC20(CRV_TOKEN).safeApprove(VOTING_ESCROW, amount); + IVotingEscrow(VOTING_ESCROW).increase_amount(amount); + } + + function increaseUnlockTimeCrv(uint256 unlockTime) external onlyOwner { + IVotingEscrow(VOTING_ESCROW).increase_unlock_time(unlockTime); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) external onlyOwner { + require(newOwner != address(0), 'Ownable: new owner is the zero address'); + emit OwnershipTransferred(_owner, newOwner); + _owner = newOwner; + } +} diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index bc55fb26..37519a38 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -51,7 +51,7 @@ import { FlashLiquidationAdapterFactory, RewardsTokenFactory, RewardsATokenMockFactory, - CurveRewardsAwareATokenFactory, + CurveGaugeRewardsAwareATokenFactory, } from '../types'; import { withSaveAndVerify, @@ -664,8 +664,8 @@ export const chooseATokenDeployment = (id: eContractid) => { return deployDelegationAwareATokenImpl; case eContractid.RewardsATokenMock: return deployRewardATokenMock; - case eContractid.CurveRewardsAwareAToken: - return deployCurveRewardsAwareATokenByNetwork; + case eContractid.CurveGaugeRewardsAwareAToken: + return deployCurveGaugeRewardsAwareATokenByNetwork; default: throw Error(`Missing aToken implementation deployment script for: ${id}`); } @@ -719,20 +719,20 @@ export const deployATokenImplementations = async ( } }; -export const deployCurveRewardsAwareAToken = async ( +export const deployCurveGaugeRewardsAwareAToken = async ( crvToken: tEthereumAddress, verify?: boolean ) => { const args: [tEthereumAddress] = [crvToken]; return withSaveAndVerify( - await new CurveRewardsAwareATokenFactory(await getFirstSigner()).deploy(...args), - eContractid.CurveRewardsAwareAToken, + await new CurveGaugeRewardsAwareATokenFactory(await getFirstSigner()).deploy(...args), + eContractid.CurveGaugeRewardsAwareAToken, args, verify ); }; -export const deployCurveRewardsAwareATokenByNetwork = async (verify?: boolean) => { +export const deployCurveGaugeRewardsAwareATokenByNetwork = async (verify?: boolean) => { const network = DRE.network.name as eEthereumNetwork; - return deployCurveRewardsAwareAToken(CRV_TOKEN[network], verify); + return deployCurveGaugeRewardsAwareAToken(CRV_TOKEN[network], verify); }; diff --git a/helpers/types.ts b/helpers/types.ts index 73f4b264..db888f9b 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -89,7 +89,7 @@ export enum eContractid { FlashLiquidationAdapter = 'FlashLiquidationAdapter', RewardsATokenMock = 'RewardsATokenMock', RewardsToken = 'RewardsToken', - CurveRewardsAwareAToken = 'CurveRewardsAwareAToken', + CurveGaugeRewardsAwareAToken = 'CurveGaugeRewardsAwareAToken', } /* diff --git a/package-lock.json b/package-lock.json index 6642f59e..ed0a6356 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14654,7 +14654,7 @@ } }, "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#1a27c59c15ab1e95ee8e5c4ed6ad814c49cc439e", + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#ee3994657fa7a427238e6ba92a84d0b529bbcde0", "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "dev": true, "requires": { diff --git a/test-suites/test-aave/mainnet/atoken-curve-rewards.main.ts b/test-suites/test-aave/mainnet/atoken-curve-rewards.main.ts index 419296e8..09407bb4 100644 --- a/test-suites/test-aave/mainnet/atoken-curve-rewards.main.ts +++ b/test-suites/test-aave/mainnet/atoken-curve-rewards.main.ts @@ -17,7 +17,7 @@ import { import { deployDefaultReserveInterestRateStrategy } from '../../../helpers/contracts-deployments'; import { IERC20Factory } from '../../../types/IERC20Factory'; import BigNumberJs from 'bignumber.js'; -import { CurveRewardsAwareATokenFactory } from '../../../types'; +import { CurveGaugeRewardsAwareATokenFactory } from '../../../types'; import { eContractid, eEthereumNetwork, tEthereumAddress } from '../../../helpers/types'; import { strategyWBTC } from '../../../markets/aave/reservesConfigs'; import { checkRewards } from '../helpers/rewards-distribution/verify'; @@ -87,7 +87,7 @@ const listGauge = async (gauge: GaugeInfo) => { eEthereumNetwork.main ); const aTokenImpl = ( - await new CurveRewardsAwareATokenFactory(await getFirstSigner()).deploy(CRV_TOKEN) + await new CurveGaugeRewardsAwareATokenFactory(await getFirstSigner()).deploy(CRV_TOKEN) ).address; const stableDebtTokenImpl = await getContractAddressWithJsonFallback( eContractid.StableDebtToken,