From e8e94885236beb612d5dcee3da1e372b422305d9 Mon Sep 17 00:00:00 2001 From: David Racero Date: Mon, 17 May 2021 13:09:13 +0200 Subject: [PATCH] feat: Added Curve LP support for reward aware aTokens --- .../adapters/interfaces/curve/ICurveGauge.sol | 30 ++ .../interfaces/curve/ICurveMinter.sol | 6 + .../interfaces/curve/IMultiRewards.sol | 6 + .../rewards/CurveRewardsAwareAToken.sol | 233 ++++++++++ package.json | 1 + .../mainnet/atoken-curve-rewards.main.ts | 398 ++++++++++++++++++ 6 files changed, 674 insertions(+) create mode 100644 contracts/adapters/interfaces/curve/ICurveGauge.sol create mode 100644 contracts/adapters/interfaces/curve/ICurveMinter.sol create mode 100644 contracts/adapters/interfaces/curve/IMultiRewards.sol create mode 100644 contracts/adapters/rewards/CurveRewardsAwareAToken.sol create mode 100644 test-suites/test-aave/mainnet/atoken-curve-rewards.main.ts diff --git a/contracts/adapters/interfaces/curve/ICurveGauge.sol b/contracts/adapters/interfaces/curve/ICurveGauge.sol new file mode 100644 index 00000000..8f39088d --- /dev/null +++ b/contracts/adapters/interfaces/curve/ICurveGauge.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +interface ICurveGauge { + function claim_rewards(address _addr) external; + + function claim_rewards() external; + + function deposit(uint256 _value) external; + + function deposit(uint256 _value, address _addr) external; + + function withdraw(uint256 _value) external; + + function user_checkpoint(address _value) external returns (bool); +} + +interface ICurveGaugeView { + function crv_token() external view returns (address); + + function reward_tokens(uint256 arg0) external view returns (address); + + function reward_contract() external view returns (address); + + function minter() external view returns (address); + + function integrate_fraction(address user) external view returns (uint256); + + function claimable_reward(address user, address token) external view returns (uint256); +} diff --git a/contracts/adapters/interfaces/curve/ICurveMinter.sol b/contracts/adapters/interfaces/curve/ICurveMinter.sol new file mode 100644 index 00000000..972061ca --- /dev/null +++ b/contracts/adapters/interfaces/curve/ICurveMinter.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +interface ICurveMinter { + function mint(address gauge) external; +} diff --git a/contracts/adapters/interfaces/curve/IMultiRewards.sol b/contracts/adapters/interfaces/curve/IMultiRewards.sol new file mode 100644 index 00000000..cfc00141 --- /dev/null +++ b/contracts/adapters/interfaces/curve/IMultiRewards.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +interface IMultiRewards { + function notifyRewardAmount(address _rewardsToken, uint256 reward) external; +} diff --git a/contracts/adapters/rewards/CurveRewardsAwareAToken.sol b/contracts/adapters/rewards/CurveRewardsAwareAToken.sol new file mode 100644 index 00000000..13f2842e --- /dev/null +++ b/contracts/adapters/rewards/CurveRewardsAwareAToken.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 {ICurveMinter} from '../interfaces/curve/ICurveMinter.sol'; +import {ICurveGauge, ICurveGaugeView} from '../interfaces/curve/ICurveGauge.sol'; +import {IAaveIncentivesController} from '../../interfaces/IAaveIncentivesController.sol'; + +/** + * @title Curve Rewards Aware AToken + * @notice AToken aware to claim and distribute rewards from an external Curve Gauge controller. + * @author Aave + */ +contract CurveRewardsAwareAToken is RewardsAwareAToken { + // CRV token address + address internal immutable CRV_TOKEN; + + // Gauge contract address + address internal _gaugeController; + + // reward address => pending reward to be distributed; + mapping(address => uint256) internal _pendingRewards; + + /** + * @param crvToken The address of the $CRV token + */ + constructor(address crvToken) public { + CRV_TOKEN = crvToken; + } + + /** + * @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 + */ + 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; + + // Initialize Curve Gauge rewards + _gaugeController = underlyingAsset; + _rewardTokens[0] = CRV_TOKEN; + + // Start for loop with index = 1 to not rewrite first element of rewardTokens + for (uint256 index = 1; index < MAX_REWARD_TOKENS; index++) { + address reward = ICurveGaugeView(underlyingAsset).reward_tokens(index - 1); + if (reward == address(0)) break; + + _rewardTokens[index] = reward; + } + + emit Initialized( + underlyingAsset, + address(pool), + treasury, + address(incentivesController), + aTokenDecimals, + aTokenName, + aTokenSymbol, + params + ); + } + + /** Start of Curve external calls functions */ + + /** + * @dev External call to retrieve the lifetime accrued $CRV token rewards from the external Rewards Controller contract + */ + function _getExternalLifetimeCurve() internal view returns (uint256) { + return ICurveGaugeView(_gaugeController).integrate_fraction(address(this)); + } + + /** + * @dev External call to retrieve the current claimable extra token rewards from the external Rewards Controller contract + */ + function _getExternalClaimableCurveExtraRewards(address token) internal view returns (uint256) { + return ICurveGaugeView(_gaugeController).claimable_reward(address(this), token); + } + + /** End of Curve Specific functions */ + + /** Start of Curve Gauge extra reward functions */ + + /** + * @dev External call to retrieve the extra rewards of the aToken contract from the external Rewards Controller contract + * @param token the reward token to retrieve lifetime rewards and accrued since last call + */ + function _getExternalLifetimeExtraRewards(address token) internal returns (uint256) { + uint256[MAX_REWARD_TOKENS] memory priorTokenBalances; + + for (uint256 index = 1; index < MAX_REWARD_TOKENS; index++) { + address rewardToken = _getRewardsTokenAddress(index); + if (rewardToken == address(0)) break; + if (rewardToken == CRV_TOKEN) continue; + priorTokenBalances[index] = IERC20(rewardToken).balanceOf(address(this)); + } + + ICurveGauge(_gaugeController).claim_rewards(address(this)); + + for (uint256 index = 1; index < MAX_REWARD_TOKENS; index++) { + address rewardToken = _getRewardsTokenAddress(index); + if (rewardToken == address(0)) break; + if (rewardToken == CRV_TOKEN) continue; + uint256 balance = IERC20(rewardToken).balanceOf(address(this)); + + _pendingRewards[rewardToken] = _pendingRewards[rewardToken].add( + balance.sub(priorTokenBalances[index]) + ); + } + + uint256 accrued = _pendingRewards[token]; + _pendingRewards[token] = 0; + return (_getLifetimeRewards(token).add(accrued)); + } + + /** + * @dev External call to retrieve the extra rewards of the aToken contract from the external Rewards Controller contract + * @param token the reward token to retrieve lifetime rewards and accrued since last call + */ + function _getExternalLifetimeExtraRewardsView(address token) internal view returns (uint256) { + return _getLifetimeRewards(token).add(_getExternalClaimableCurveExtraRewards(token)); + } + + /** End of Curve Gauge extra reward functions */ + + /** Start of Rewards Aware AToken functions */ + + /** + * @dev External call to retrieve the lifetime accrued rewards of the aToken contract to the external Rewards Controller contract + * @param token the reward token to retrieve lifetime rewards and accrued since last call + */ + function _computeExternalLifetimeRewards(address token) internal override returns (uint256) { + // The Curve Gauge can give exact lifetime rewards and accrued rewards for the CRV token + if (token == CRV_TOKEN) { + ICurveGauge(_gaugeController).user_checkpoint(address(this)); + return _getExternalLifetimeCurve(); + } + // Other rewards from the Curve Gauge can not get the lifetime rewards externally, only at the moment of claim, due they are external rewards outside the Curve ecosystem. + return _getExternalLifetimeExtraRewards(token); + } + + /** + * @dev External call to retrieve the lifetime accrued rewards of the aToken contract to the external Rewards Controller contract + * @param token the reward token to retrieve lifetime rewards and accrued since last call + */ + function _getExternalLifetimeRewards(address token) internal view override returns (uint256) { + // The Curve Gauge can give exact lifetime rewards and accrued rewards for the CRV token + if (token == CRV_TOKEN) { + return _getExternalLifetimeCurve(); + } + // Other rewards from the Curve Gauge can not get the lifetime rewards externally, only at the moment of claim, due they are external rewards outside the Curve ecosystem. + return _getExternalLifetimeExtraRewardsView(token); + } + + /** + * @dev External call to claim the lifetime accrued rewards of the aToken contract to the external Rewards Controller contract + */ + function _claimRewardsFromController() internal override { + // Mint CRV to aToken + ICurveMinter(ICurveGaugeView(_gaugeController).minter()).mint(_gaugeController); + + // Claim other Curve gauge tokens, and track the pending rewards to distribute at `_retrieveAvailableReward` call + uint256[MAX_REWARD_TOKENS] memory priorTokenBalances; + + for (uint256 index = 1; index < MAX_REWARD_TOKENS; index++) { + address rewardToken = _getRewardsTokenAddress(index); + if (rewardToken == address(0)) break; + if (rewardToken == CRV_TOKEN) continue; + priorTokenBalances[index] = IERC20(rewardToken).balanceOf(address(this)); + } + // Mint other rewards to aToken + ICurveGauge(_gaugeController).claim_rewards(address(this)); + + for (uint256 index = 1; index < MAX_REWARD_TOKENS; index++) { + address rewardToken = _getRewardsTokenAddress(index); + if (rewardToken == address(0)) break; + if (rewardToken == CRV_TOKEN) continue; + uint256 balance = IERC20(rewardToken).balanceOf(address(this)); + + _pendingRewards[rewardToken] = _pendingRewards[rewardToken].add( + balance.sub(priorTokenBalances[index]) + ); + } + } + + /** End of Rewards Aware AToken functions */ + + /** Start of External getters */ + function getCrvToken() external view returns (address) { + return CRV_TOKEN; + } + /** End of External getters */ +} diff --git a/package.json b/package.json index 1caff7c1..7f91961b 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:crv-rewards": "FORK=main TS_NODE_TRANSPILE_ONLY=1 hardhat test test-suites/test-aave/__setup.spec.ts test-suites/test-aave/mainnet/atoken-curve-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-curve-rewards.main.ts b/test-suites/test-aave/mainnet/atoken-curve-rewards.main.ts new file mode 100644 index 00000000..6be2d191 --- /dev/null +++ b/test-suites/test-aave/mainnet/atoken-curve-rewards.main.ts @@ -0,0 +1,398 @@ +import { ZERO_ADDRESS } from '../../../helpers/constants'; +import { makeSuite, SignerWithAddress, TestEnv } from '../helpers/make-suite'; +import { + advanceTimeAndBlock, + evmRevert, + evmSnapshot, + increaseTime, + waitForTx, +} from '../../../helpers/misc-utils'; +import { + getFirstSigner, + getLendingPool, + getLendingPoolAddressesProvider, + getLendingPoolConfiguratorProxy, +} from '../../../helpers/contracts-getters'; +import { deployDefaultReserveInterestRateStrategy } from '../../../helpers/contracts-deployments'; +import { IERC20Factory } from '../../../types/IERC20Factory'; +import BigNumberJs from 'bignumber.js'; +import { CurveRewardsAwareATokenFactory } from '../../../types'; +import { eContractid, eEthereumNetwork, tEthereumAddress } from '../../../helpers/types'; +import { strategyWBTC } from '../../../markets/aave/reservesConfigs'; +import { checkRewards } from '../helpers/rewards-distribution/verify'; +import { IRewardsAwareAToken } from '../../../types/IRewardsAwareAToken'; +import { IRewardsAwareATokenFactory } from '../../../types/IRewardsAwareATokenFactory'; +import { BigNumber } from 'ethers'; +import { parseEther } from 'ethers/lib/utils'; +import { IERC20 } from '../../../types/IERC20'; +import { + getContractAddressWithJsonFallback, + getParamPerNetwork, +} from '../../../helpers/contracts-helpers'; +import { ConfigNames, loadPoolConfig } from '../../../helpers/configuration'; + +const ONE_DAY = 86400; +const { expect } = require('chai'); + +interface GaugeInfo { + address: tEthereumAddress; + name: string; + symbol: string; + rewardTokens: tEthereumAddress[]; +} +const USER_ADDRESS = '0x9c5083dd4838E120Dbeac44C052179692Aa5dAC5'; + +const CRV_TOKEN = '0xd533a949740bb3306d119cc777fa900ba034cd52'; +const SNX_TOKEN = '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f'; + +const GAUGE_AAVE3: GaugeInfo = { + address: '0xd662908ADA2Ea1916B3318327A97eB18aD588b5d', + name: 'aToken a3CRV Gauge Deposit', + symbol: 'a-a3CRV-gauge', + rewardTokens: [], +}; + +const GAUGE_EURS: GaugeInfo = { + address: '0x90Bb609649E0451E5aD952683D64BD2d1f245840', + name: 'aToken eursCRV Gauge Deposit', + symbol: 'a-eursCRV-gauge', + rewardTokens: ['0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F'], +}; + +const GAUGE_ANKR: GaugeInfo = { + address: '0x6d10ed2cf043e6fcf51a0e7b4c2af3fa06695707', + name: 'aToken ankrCRV Gauge Deposit', + symbol: 'a-ankrCRV-gauge', + rewardTokens: [ + '0xE0aD1806Fd3E7edF6FF52Fdb822432e847411033', + '0x8290333ceF9e6D528dD5618Fb97a76f268f3EDD4', + ], +}; + +const listGauge = async (gauge: GaugeInfo) => { + const { symbol } = gauge; + const poolConfig = loadPoolConfig(ConfigNames.Aave); + const { + SymbolPrefix: symbolPrefix, + ATokenNamePrefix: aTokenNamePrefix, + StableDebtTokenNamePrefix: stableDebtTokenNamePrefix, + VariableDebtTokenNamePrefix: variableDebtTokenNamePrefix, + } = poolConfig; + const addressProvider = await getLendingPoolAddressesProvider(); + const poolConfigurator = await getLendingPoolConfiguratorProxy(); + + const treasury = await getParamPerNetwork( + poolConfig.ReserveFactorTreasuryAddress, + eEthereumNetwork.main + ); + const aTokenImpl = ( + await new CurveRewardsAwareATokenFactory(await getFirstSigner()).deploy(CRV_TOKEN) + ).address; + const stableDebtTokenImpl = await getContractAddressWithJsonFallback( + eContractid.StableDebtToken, + ConfigNames.Aave + ); + const variableDebtTokenImpl = await getContractAddressWithJsonFallback( + eContractid.VariableDebtToken, + ConfigNames.Aave + ); + // WBTC Strategy used as a template for tests scenario + const interestStrategy = await deployDefaultReserveInterestRateStrategy( + [ + addressProvider.address, + strategyWBTC.strategy.optimalUtilizationRate, + strategyWBTC.strategy.baseVariableBorrowRate, + strategyWBTC.strategy.variableRateSlope1, + strategyWBTC.strategy.variableRateSlope2, + strategyWBTC.strategy.stableRateSlope1, + strategyWBTC.strategy.stableRateSlope2, + ], + false + ); + const interestRateStrategyAddress = interestStrategy.address; + + const curveReserveInitParams = [ + { + aTokenImpl, + stableDebtTokenImpl, + variableDebtTokenImpl, + underlyingAssetDecimals: '18', + interestRateStrategyAddress, + underlyingAsset: gauge.address, + treasury, + incentivesController: ZERO_ADDRESS, + underlyingAssetName: gauge.symbol, + aTokenName: `${aTokenNamePrefix} ${symbol}`, + aTokenSymbol: `a${symbolPrefix}${symbol}`, + variableDebtTokenName: `${variableDebtTokenNamePrefix} ${symbolPrefix}${symbol}`, + variableDebtTokenSymbol: `variableDebt${symbolPrefix}${symbol}`, + stableDebtTokenName: `${stableDebtTokenNamePrefix} ${symbol}`, + stableDebtTokenSymbol: `stableDebt${symbolPrefix}${symbol}`, + params: '0x10', + }, + ]; + + await waitForTx(await poolConfigurator.batchInitReserve(curveReserveInitParams)); +}; + +const depositGauge = async ( + key: SignerWithAddress, + gauge: GaugeInfo, + aGaugeAddress: tEthereumAddress, + amount: BigNumber, + shouldReward?: boolean +) => { + const pool = await getLendingPool(); + const gaugeErc20 = IERC20Factory.connect(gauge.address, key.signer); + + await gaugeErc20.connect(key.signer).approve(pool.address, amount); + + const txDeposit = await waitForTx( + await pool.connect(key.signer).deposit(gauge.address, amount, key.address, '0') + ); + + await checkRewards(key, aGaugeAddress, txDeposit.blockNumber, shouldReward); +}; + +const withdrawGauge = async ( + key: SignerWithAddress, + gauge: GaugeInfo, + aGaugeAddress: tEthereumAddress, + shouldReward = true +) => { + const pool = await getLendingPool(); + const aGauge = IRewardsAwareATokenFactory.connect(aGaugeAddress, key.signer); + + const entireBalance = await aGauge.balanceOf(key.address); + + await aGauge.connect(key.signer).approve(pool.address, entireBalance); + + const txWithdraw = await waitForTx( + await pool.connect(key.signer).withdraw(gauge.address, entireBalance, key.address) + ); + + await checkRewards(key, aGaugeAddress, txWithdraw.blockNumber, shouldReward); +}; + +const claimFromGauge = async ( + key: SignerWithAddress, + gauge: GaugeInfo, + aGaugeAddress: tEthereumAddress, + shouldReward = true +) => { + const aGauge = IRewardsAwareATokenFactory.connect(aGaugeAddress, key.signer); + const rewardTokens = await aGauge.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 aGauge.claim(rewardTokens[x])); + + await checkRewards( + key, + aGaugeAddress, + txClaim.blockNumber, + shouldReward, + rewardTokens[x], + balanceBefore + ); + } +}; + +makeSuite('Curve Rewards Aware aToken', (testEnv: TestEnv) => { + let evmSnapshotId; + let depositor: SignerWithAddress; + + let gaugeEursErc20: IERC20; + let gaugeAave3Erc20: IERC20; + let gaugeAnkrErc20: IERC20; + + let aEURS: IRewardsAwareAToken; + let aAAVE3: IRewardsAwareAToken; + let aANKR: IRewardsAwareAToken; + + let crvToken: IERC20; + let snxToken: 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); + + gaugeEursErc20 = IERC20Factory.connect(GAUGE_EURS.address, depositor.signer); + gaugeAave3Erc20 = IERC20Factory.connect(GAUGE_AAVE3.address, depositor.signer); + gaugeAnkrErc20 = IERC20Factory.connect(GAUGE_ANKR.address, depositor.signer); + crvToken = IERC20Factory.connect(CRV_TOKEN, depositor.signer); + snxToken = IERC20Factory.connect(SNX_TOKEN, depositor.signer); + + // Depositor should have EURS, AAVE3, and ANKR gauges balance + const gaugeEursBalance = await gaugeEursErc20.balanceOf(USER_ADDRESS); + const gaugeAave3Balance = await gaugeAave3Erc20.balanceOf(USER_ADDRESS); + const gaugeAnkrBalance = await gaugeAnkrErc20.balanceOf(USER_ADDRESS); + + expect(gaugeEursBalance).to.be.gt('0'); + expect(gaugeAave3Balance).to.be.gt('0'); + expect(gaugeAnkrBalance).to.be.gt('0'); + + // Gauge tokens should be listed at Aave test deployment + await listGauge(GAUGE_EURS); + await listGauge(GAUGE_AAVE3); + await listGauge(GAUGE_ANKR); + + const allTokens = await testEnv.helpersContract.getAllATokens(); + + aEURS = IRewardsAwareATokenFactory.connect( + allTokens.find((aToken) => aToken.symbol.includes('eurs'))?.tokenAddress || ZERO_ADDRESS, + await getFirstSigner() + ); + aAAVE3 = IRewardsAwareATokenFactory.connect( + allTokens.find((aToken) => aToken.symbol.includes('a3CRV'))?.tokenAddress || ZERO_ADDRESS, + await getFirstSigner() + ); + aANKR = IRewardsAwareATokenFactory.connect( + allTokens.find((aToken) => aToken.symbol.includes('ankr'))?.tokenAddress || ZERO_ADDRESS, + + await getFirstSigner() + ); + }); + + after('Reset', () => { + // Reset BigNumber + BigNumberJs.config({ DECIMAL_PLACES: 20, ROUNDING_MODE: BigNumberJs.ROUND_HALF_UP }); + }); + + describe('AToken with only CRV rewards - AAVE3 Gauge', () => { + before(async () => { + evmSnapshotId = await evmSnapshot(); + }); + + after(async () => { + await evmRevert(evmSnapshotId); + }); + + it('Deposit and generate user reward checkpoints', async () => { + // Deposits + await depositGauge(depositor, GAUGE_AAVE3, aAAVE3.address, parseEther('100000')); + const curveATokenBalance = await crvToken.balanceOf(aAAVE3.address); + expect(curveATokenBalance).to.be.eq('0', 'CRV rewards should be zero'); + }); + + it('Increase time and claim CRV', async () => { + // Pass time to generate rewards + await increaseTime(ONE_DAY); + + // Claim + await claimFromGauge(depositor, GAUGE_AAVE3, aAAVE3.address); + const curveATokenBalance = await crvToken.balanceOf(aAAVE3.address); + expect(curveATokenBalance).to.be.eq( + '0', + 'CRV Balance should be zero as there is only one aToken holder' + ); + }); + + it('Pass time and withdraw Staked AAVE3', async () => { + // Pass time to generate rewards + await increaseTime(ONE_DAY); + + // Withdraw + await withdrawGauge(depositor, GAUGE_AAVE3, aAAVE3.address); + const curveATokenBalance = await crvToken.balanceOf(aAAVE3.address); + expect(curveATokenBalance).to.be.eq('0', 'CRV rewards should be zero'); + }); + + it('Claim the remaining CRV', async () => { + // Claim + await claimFromGauge(depositor, GAUGE_AAVE3, aAAVE3.address); + const curveATokenBalance = await crvToken.balanceOf(aAAVE3.address); + expect(curveATokenBalance).to.be.eq( + '0', + 'CRV Balance should be zero as there is only one aToken holder' + ); + }); + }); + + describe('AToken with CRV and 1 extra rewards - EURS Gauge', () => { + before(async () => { + evmSnapshotId = await evmSnapshot(); + }); + + after(async () => { + await evmRevert(evmSnapshotId); + }); + + it('Deposit and generate user reward checkpoints', async () => { + // Deposits + + await depositGauge(depositor, GAUGE_EURS, aEURS.address, parseEther('100000')); + const curveATokenBalance = await crvToken.balanceOf(aEURS.address); + expect(curveATokenBalance).to.be.eq('0', 'CRV should be zero'); + }); + + it('Increase time and claim CRV and SNX', async () => { + // Pass time to generate rewards + await advanceTimeAndBlock(ONE_DAY * 14); + + // Claim + await claimFromGauge(depositor, GAUGE_EURS, aEURS.address); + }); + + it('Pass time and withdraw Staked EURS', async () => { + // Pass time to generate rewards + await increaseTime(ONE_DAY); + + // Withdraw + await withdrawGauge(depositor, GAUGE_EURS, aEURS.address); + }); + + it('Claim the remaining CRV and SNX', async () => { + // Claim + await claimFromGauge(depositor, GAUGE_EURS, aEURS.address); + }); + }); + + describe('AToken with CRV and 2 extra rewards - ANKR Gauge', () => { + before(async () => { + evmSnapshotId = await evmSnapshot(); + }); + + after(async () => { + await evmRevert(evmSnapshotId); + }); + + it('Deposit and generate user reward checkpoints', async () => { + // Deposits + await depositGauge( + depositor, + GAUGE_ANKR, + aANKR.address, + parseEther('2002.018841813024963468') + ); + }); + + it('Increase time and claim CRV with extra rewards', async () => { + // Pass time to generate rewards + await increaseTime(ONE_DAY * 30); + + // Claim + await claimFromGauge(depositor, GAUGE_ANKR, aANKR.address); + }); + + it('Pass time and withdraw Staked ANKR', async () => { + // Pass time to generate rewards + await increaseTime(ONE_DAY * 30); + + // Withdraw + await withdrawGauge(depositor, GAUGE_ANKR, aANKR.address); + }); + + it('Claim the CRV with extra rewards', async () => { + // Claim + await claimFromGauge(depositor, GAUGE_ANKR, aANKR.address); + }); + }); +});