diff --git a/contracts/interfaces/IClaimStakingRewardsHelper.sol b/contracts/interfaces/IClaimStakingRewardsHelper.sol new file mode 100644 index 0000000..4c18aca --- /dev/null +++ b/contracts/interfaces/IClaimStakingRewardsHelper.sol @@ -0,0 +1,9 @@ +pragma solidity ^0.7.5; + +interface IClaimStakingRewardsHelper { + function claimAllRewards(address to) external returns (uint256); + + function claimAllRewardsAndStake(address to) external; + + function claimAndStake(address to, address stakeToken) external; +} diff --git a/contracts/interfaces/IERC20WithNonce.sol b/contracts/interfaces/IERC20WithNonce.sol new file mode 100644 index 0000000..b28ea81 --- /dev/null +++ b/contracts/interfaces/IERC20WithNonce.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.5; + +import {IERC20} from './IERC20.sol'; + +interface IERC20WithNonce is IERC20 { + function _nonces(address user) external view returns (uint256); +} diff --git a/contracts/interfaces/IPriceOracle.sol b/contracts/interfaces/IPriceOracle.sol new file mode 100644 index 0000000..e7610fc --- /dev/null +++ b/contracts/interfaces/IPriceOracle.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.5; + +interface IPriceOracle { + function getAssetPrice(address asset) external view returns (uint256); +} diff --git a/contracts/interfaces/IStakeUIHelper.sol b/contracts/interfaces/IStakeUIHelper.sol new file mode 100644 index 0000000..06822e5 --- /dev/null +++ b/contracts/interfaces/IStakeUIHelper.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.5; +pragma experimental ABIEncoderV2; + +interface IStakeUIHelper { + struct AssetUIData { + uint256 stakeTokenTotalSupply; + uint256 stakeCooldownSeconds; + uint256 stakeUnstakeWindow; + uint256 stakeTokenPriceEth; + uint256 rewardTokenPriceEth; + uint256 stakeApy; + uint128 distributionPerSecond; + uint256 distributionEnd; + uint256 stakeTokenUserBalance; + uint256 underlyingTokenUserBalance; + uint256 userCooldown; + uint256 userIncentivesToClaim; + uint256 userPermitNonce; + } + + function getStkAaveData(address user) external view returns (AssetUIData memory); + + function getStkBptData(address user) external view returns (AssetUIData memory); + + function getUserUIData(address user) + external + view + returns ( + AssetUIData memory, + AssetUIData memory, + uint256 + ); +} diff --git a/contracts/misc/ClaimStakingRewardsHelper.sol b/contracts/misc/ClaimStakingRewardsHelper.sol new file mode 100644 index 0000000..428a46d --- /dev/null +++ b/contracts/misc/ClaimStakingRewardsHelper.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.7.5; + +import {IClaimStakingRewardsHelper} from '../interfaces/IClaimStakingRewardsHelper.sol'; +import {IStakedTokenV3} from '../interfaces/IStakedTokenV3.sol'; +import {IERC20} from '../interfaces/IERC20.sol'; +import {SafeMath} from '../lib/SafeMath.sol'; + +/** + * @title ClaimStakingRewardsHelper + * @notice Contract to claim all rewards on the different stake pools + * or claim all and stake Aave token + * @author Aave + **/ +contract ClaimStakingRewardsHelper is IClaimStakingRewardsHelper { + using SafeMath for uint256; + address public immutable aaveStakeToken; + address public immutable bptStakeToken; + + constructor( + address _aaveStakeToken, + address _bptStakeToken, + address aaveToken + ) { + aaveStakeToken = _aaveStakeToken; + bptStakeToken = _bptStakeToken; + + IERC20(aaveToken).approve(_aaveStakeToken, type(uint256).max); + } + + /** + * @dev Claims all reward for an user, on all the different staked assets. + * @param to Address that will be receiving the rewards + **/ + function claimAllRewards(address to) external override returns (uint256) { + uint256 claimedFromAave = + IStakedTokenV3(aaveStakeToken).claimRewardsOnBehalf(msg.sender, to, type(uint256).max); + uint256 claimedFromBPT = + IStakedTokenV3(bptStakeToken).claimRewardsOnBehalf(msg.sender, to, type(uint256).max); + return claimedFromAave.add(claimedFromBPT); + } + + /** + * @dev Claims all reward for an user, on all the different staked assets, and stakes this amount on the aave stake pool. + * @param to Address that will be receiving the stk Token representing the staked amount + **/ + function claimAllRewardsAndStake(address to) external override { + _claimAndStake(msg.sender, to, aaveStakeToken); + _claimAndStake(msg.sender, to, bptStakeToken); + } + + /** + * @dev Claims reward from stakedToken and stakes it into the aave stake pool + * @param to Address that will be receiving the stk Token representing the staked amount + * @param stakeToken Address of the stake token where to claim the rewards + **/ + function claimAndStake(address to, address stakeToken) external override { + require( + stakeToken == aaveStakeToken || stakeToken == bptStakeToken, + 'Staked Token address must exists' + ); + _claimAndStake(msg.sender, to, stakeToken); + } + + /** + * @dev Claims reward from stakedToken and stakes it into the aave stake pool + * @param to Address that will be receiving the stk Token representing the staked amount + **/ + function _claimAndStake( + address from, + address to, + address stakeToken + ) internal { + uint256 rewardsClaimed = + IStakedTokenV3(stakeToken).claimRewardsOnBehalf(from, address(this), type(uint256).max); + if (rewardsClaimed > 0) { + IStakedTokenV3(aaveStakeToken).stake(to, rewardsClaimed); + } + } +} diff --git a/contracts/misc/IStakedToken.sol b/contracts/misc/IStakedToken.sol new file mode 100644 index 0000000..c93b470 --- /dev/null +++ b/contracts/misc/IStakedToken.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.7.5; +pragma experimental ABIEncoderV2; + +interface IStakedToken { + struct AssetData { + uint128 emissionPerSecond; + uint128 lastUpdateTimestamp; + uint256 index; + } + + function totalSupply() external view returns (uint256); + + function COOLDOWN_SECONDS() external view returns (uint256); + + function UNSTAKE_WINDOW() external view returns (uint256); + + function DISTRIBUTION_END() external view returns (uint256); + + function assets(address asset) external view returns (AssetData memory); + + function balanceOf(address user) external view returns (uint256); + + function getTotalRewardsBalance(address user) external view returns (uint256); + + function stakersCooldowns(address user) external view returns (uint256); + + function stake(address to, uint256 amount) external; + + function redeem(address to, uint256 amount) external; + + function cooldown() external; + + function claimRewards(address to, uint256 amount) external; +} diff --git a/contracts/misc/StakeUIHelper.sol b/contracts/misc/StakeUIHelper.sol new file mode 100644 index 0000000..42ee9e5 --- /dev/null +++ b/contracts/misc/StakeUIHelper.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.5; +pragma experimental ABIEncoderV2; + +import {IStakedToken} from './IStakedToken.sol'; +import {IStakeUIHelper} from '../interfaces/IStakeUIHelper.sol'; +import {IERC20WithNonce} from '../interfaces/IERC20WithNonce.sol'; +import {IERC20} from '../interfaces/IERC20.sol'; +import {IPriceOracle} from '../interfaces/IPriceOracle.sol'; + +interface BPTPriceFeedI { + function latestAnswer() external view returns (uint256); +} + +contract StakeUIHelper is IStakeUIHelper { + IPriceOracle public immutable PRICE_ORACLE; + BPTPriceFeedI public immutable BPT_PRICE_FEED; + + address public immutable AAVE; + IStakedToken public immutable STAKED_AAVE; + + address public immutable BPT; + IStakedToken public immutable STAKED_BPT; + + uint256 constant SECONDS_PER_YEAR = 365 * 24 * 60 * 60; + uint256 constant APY_PRECISION = 10000; + address constant MOCK_USD_ADDRESS = 0x10F7Fc1F91Ba351f9C629c5947AD69bD03C05b96; + uint256 internal constant USD_BASE = 1e26; + + constructor( + IPriceOracle priceOracle, + BPTPriceFeedI bptPriceFeed, + address aave, + IStakedToken stkAave, + address bpt, + IStakedToken stkBpt + ) public { + PRICE_ORACLE = priceOracle; + BPT_PRICE_FEED = bptPriceFeed; + + AAVE = aave; + STAKED_AAVE = stkAave; + + BPT = bpt; + STAKED_BPT = stkBpt; + } + + function _getStakedAssetData( + IStakedToken stakeToken, + address underlyingToken, + address user, + bool isNonceAvailable + ) internal view returns (AssetUIData memory) { + AssetUIData memory data; + + data.stakeTokenTotalSupply = stakeToken.totalSupply(); + data.stakeCooldownSeconds = stakeToken.COOLDOWN_SECONDS(); + data.stakeUnstakeWindow = stakeToken.UNSTAKE_WINDOW(); + data.rewardTokenPriceEth = PRICE_ORACLE.getAssetPrice(AAVE); + data.distributionEnd = stakeToken.DISTRIBUTION_END(); + if (block.timestamp < data.distributionEnd) { + data.distributionPerSecond = stakeToken.assets(address(stakeToken)).emissionPerSecond; + } + + if (user != address(0)) { + data.underlyingTokenUserBalance = IERC20(underlyingToken).balanceOf(user); + data.stakeTokenUserBalance = stakeToken.balanceOf(user); + data.userIncentivesToClaim = stakeToken.getTotalRewardsBalance(user); + data.userCooldown = stakeToken.stakersCooldowns(user); + data.userPermitNonce = isNonceAvailable ? IERC20WithNonce(underlyingToken)._nonces(user) : 0; + } + return data; + } + + function _calculateApy(uint256 distributionPerSecond, uint256 stakeTokenTotalSupply) + internal + pure + returns (uint256) + { + return (distributionPerSecond * SECONDS_PER_YEAR * APY_PRECISION) / stakeTokenTotalSupply; + } + + function getStkAaveData(address user) public view override returns (AssetUIData memory) { + AssetUIData memory data = _getStakedAssetData(STAKED_AAVE, AAVE, user, true); + + data.stakeTokenPriceEth = data.rewardTokenPriceEth; + data.stakeApy = _calculateApy(data.distributionPerSecond, data.stakeTokenTotalSupply); + return data; + } + + function getStkBptData(address user) public view override returns (AssetUIData memory) { + AssetUIData memory data = _getStakedAssetData(STAKED_BPT, BPT, user, false); + + data.stakeTokenPriceEth = address(BPT_PRICE_FEED) != address(0) + ? BPT_PRICE_FEED.latestAnswer() + : PRICE_ORACLE.getAssetPrice(BPT); + data.stakeApy = _calculateApy( + data.distributionPerSecond * data.rewardTokenPriceEth, + data.stakeTokenTotalSupply * data.stakeTokenPriceEth + ); + + return data; + } + + function getUserUIData(address user) + external + view + override + returns ( + AssetUIData memory, + AssetUIData memory, + uint256 + ) + { + return ( + getStkAaveData(user), + getStkBptData(user), + USD_BASE / PRICE_ORACLE.getAssetPrice(MOCK_USD_ADDRESS) + ); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 6672ed8..897a51a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,3 +16,4 @@ services: INFURA_KEY: ${INFURA_KEY} ETHERSCAN_NETWORK: ${ETHERSCAN_NETWORK} GITLAB_ACCESS_TOKEN: ${GITLAB_ACCESS_TOKEN} + ALCHEMY_KEY: ${ALCHEMY_KEY} diff --git a/helpers/contracts-accessors.ts b/helpers/contracts-accessors.ts index cd36d41..698d0f1 100644 --- a/helpers/contracts-accessors.ts +++ b/helpers/contracts-accessors.ts @@ -3,7 +3,7 @@ import { eContractid, tEthereumAddress } from './types'; import { MintableErc20 } from '../types/MintableErc20'; import { StakedAave } from '../types/StakedAave'; import { StakedAaveV2 } from '../types/StakedAaveV2'; -import {StakedAaveV3 } from '../types/StakedAaveV3'; +import { StakedAaveV3 } from '../types/StakedAaveV3'; import { IcrpFactory } from '../types/IcrpFactory'; // Configurable right pool factory import { IConfigurableRightsPool } from '../types/IConfigurableRightsPool'; import { IControllerAaveEcosystemReserve } from '../types/IControllerAaveEcosystemReserve'; @@ -22,6 +22,45 @@ import { DoubleTransferHelper } from '../types/DoubleTransferHelper'; import { zeroAddress } from 'ethereumjs-util'; import { ZERO_ADDRESS } from './constants'; import { Signer } from 'ethers'; +import { ClaimStakingRewardsHelper } from '../types'; +import { StakeUiHelper } from '../types'; + +export const deployStakeUIHelper = async ( + [priceOracle, bptPriceFeed, aave, stkAave, bpt, stkBpt]: [ + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + tEthereumAddress + ], + verify?: boolean +) => { + const id = eContractid.StakeUIHelper; + const args: string[] = [priceOracle, bptPriceFeed, aave, stkAave, bpt, stkBpt]; + const instance = await deployContract(id, args); + if (verify) { + await verifyContract(instance.address, args); + } + return instance; +}; + +export const deployClaimHelper = async ( + [aaveStakeTokenAddress, bptStakeTokenAddress, aaveToken]: [ + tEthereumAddress, + tEthereumAddress, + tEthereumAddress + ], + verify?: boolean +) => { + const id = eContractid.ClaimStakingRewardsHelper; + const args: string[] = [aaveStakeTokenAddress, bptStakeTokenAddress, aaveToken]; + const instance = await deployContract(id, args); + if (verify) { + await verifyContract(instance.address, args); + } + return instance; +}; export const deployStakedAave = async ( [ @@ -198,7 +237,6 @@ export const deployStakedTokenV3 = async ( return instance; }; - export const deployStakedAaveV3 = async ( [ stakedToken, diff --git a/helpers/types.ts b/helpers/types.ts index 37ffaf2..b279453 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -30,6 +30,8 @@ export enum eContractid { IBPool = 'IBPool', IControllerAaveEcosystemReserve = 'IControllerAaveEcosystemReserve', MockSelfDestruct = 'SelfdestructTransfer', + ClaimStakingRewardsHelper = 'ClaimStakingRewardsHelper', + StakeUIHelper = 'StakeUIHelper', } export type tEthereumAddress = string; diff --git a/package-lock.json b/package-lock.json index a3d3ef1..52718ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13509,7 +13509,7 @@ } }, "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#1ce6a1d64235fabe2aaf827fd606def55693508f", + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#1a27c59c15ab1e95ee8e5c4ed6ad814c49cc439e", "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "dev": true, "requires": { diff --git a/package.json b/package.json index c4c933d..9c2ca94 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,9 @@ "ropsten:deployment": "npm run hardhat-ropsten -- common-deployment --verify", "kovan:deployment": "npm run hardhat-kovan -- common-deployment --verify", "main:deployment": "npm run hardhat-main -- common-deployment --verify", + "kovan:deploy-ClaimHelper": "hardhat --network kovan deploy-ClaimHelper --verify", + "main:deploy-ClaimHelper": "hardhat --network main deploy-OracleAnchor --verify", + "kovan:deploy-StakeUIHelper": "hardhat --network kovan deploy-StakeUIHelper --verify", "prettier:check": "npx prettier -c 'tasks/**/*.ts' 'contracts/**/*.sol' 'helpers/**/*.ts' 'test/**/*.ts'", "prettier:write": "prettier --write 'tasks/**/*.ts' 'contracts/**/*.sol' 'helpers/**/*.ts' 'test/**/*.ts'", "ci:clean": "rm -rf types/ cache/ artifacts/", diff --git a/tasks/deployments/deploy-ClaimHelper.ts b/tasks/deployments/deploy-ClaimHelper.ts new file mode 100644 index 0000000..deac9f0 --- /dev/null +++ b/tasks/deployments/deploy-ClaimHelper.ts @@ -0,0 +1,44 @@ +import { task } from 'hardhat/config'; +import { deployClaimHelper } from '../../helpers/contracts-accessors'; +import { eEthereumNetwork } from '../../helpers/types'; + +task(`deploy-ClaimHelper`, `Deploys the ClaimStakingRewardsHelper contract`) + .addFlag('verify', 'Verify ClaimStakingRewardsHelper contract via Etherscan API.') + .setAction(async ({ verify }, localBRE) => { + await localBRE.run('set-dre'); + + if (!localBRE.network.config.chainId) { + throw new Error('INVALID_CHAIN_ID'); + } + + const stakeTokens: { + [network: string]: { + aaveStakeTokenAddress: string; + bptStakeTokenAddress: string; + aaveToken: string; + }; + } = { + [eEthereumNetwork.kovan]: { + aaveStakeTokenAddress: '0xf2fbf9A6710AfDa1c4AaB2E922DE9D69E0C97fd2', + bptStakeTokenAddress: '0xCe7021eDabaf82D28adBBea449Bc4dF70261F33E', // mock, need aave to stake + aaveToken: '0xb597cd8d3217ea6477232f9217fa70837ff667af', + }, + [eEthereumNetwork.main]: { + aaveStakeTokenAddress: '0x4da27a545c0c5b758a6ba100e3a049001de870f5', + bptStakeTokenAddress: '0xa1116930326D21fB917d5A27F1E9943A9595fb47', + aaveToken: '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', + }, + }; + + console.log(`\tDeploying ClaimHelper ...`); + const ClaimHelper = await deployClaimHelper( + [ + stakeTokens[localBRE.network.name].aaveStakeTokenAddress, + stakeTokens[localBRE.network.name].bptStakeTokenAddress, + stakeTokens[localBRE.network.name].aaveToken, + ], + verify + ); + + console.log(`\tFinished ClaimHelper deployment: ${ClaimHelper.address}`); + }); diff --git a/tasks/deployments/deploy-StakeUIHelper.ts b/tasks/deployments/deploy-StakeUIHelper.ts new file mode 100644 index 0000000..20b42e9 --- /dev/null +++ b/tasks/deployments/deploy-StakeUIHelper.ts @@ -0,0 +1,56 @@ +import { task } from 'hardhat/config'; +import { deployStakeUIHelper } from '../../helpers/contracts-accessors'; +import { eEthereumNetwork } from '../../helpers/types'; + +task(`deploy-StakeUIHelper`, `Deploys the StakeUIHelper contract`) + .addFlag('verify', 'Verify StakeUIHelper contract via Etherscan API.') + .setAction(async ({ verify }, localBRE) => { + await localBRE.run('set-dre'); + + if (!localBRE.network.config.chainId) { + throw new Error('INVALID_CHAIN_ID'); + } + + const stakeTokens: { + [network: string]: { + priceOracle: string; + bptPriceFeed: string; + aave: string; + stkAave: string; + bpt: string; + stkBpt: string; + }; + } = { + [eEthereumNetwork.kovan]: { + priceOracle: '0x276c4793f2ee3d5bf18c5b879529dd4270ba4814', + bptPriceFeed: '0x0000000000000000000000000000000000000000', + aave: '0xb597cd8d3217ea6477232f9217fa70837ff667af', + stkAave: '0xf2fbf9a6710afda1c4aab2e922de9d69e0c97fd2', + bpt: '0xb597cd8d3217ea6477232f9217fa70837ff667af', + stkBpt: '0xCe7021eDabaf82D28adBBea449Bc4dF70261F33E', + }, + [eEthereumNetwork.main]: { + priceOracle: '0x0000000000000000000000000000000000000000', + bptPriceFeed: '0x0000000000000000000000000000000000000000', + aave: '0x0000000000000000000000000000000000000000', + stkAave: '0x0000000000000000000000000000000000000000', + bpt: '0x0000000000000000000000000000000000000000', + stkBpt: '0x0000000000000000000000000000000000000000', + }, + }; + + console.log(`\tDeploying StakeUIHelper ...`); + const StakeUIHelper = await deployStakeUIHelper( + [ + stakeTokens[localBRE.network.name].priceOracle, + stakeTokens[localBRE.network.name].bptPriceFeed, + stakeTokens[localBRE.network.name].aave, + stakeTokens[localBRE.network.name].stkAave, + stakeTokens[localBRE.network.name].bpt, + stakeTokens[localBRE.network.name].stkBpt, + ], + verify + ); + + console.log(`\tFinished StakeUIHelper deployment: ${StakeUIHelper.address}`); + }); diff --git a/test/DistributionManager/data-helpers/asset-user-data.ts b/test/DistributionManager/data-helpers/asset-user-data.ts index ee8f26e..7209188 100644 --- a/test/DistributionManager/data-helpers/asset-user-data.ts +++ b/test/DistributionManager/data-helpers/asset-user-data.ts @@ -3,6 +3,7 @@ import { AaveDistributionManager } from '../../../types/AaveDistributionManager' import { StakedAave } from '../../../types/StakedAave'; import { AaveIncentivesController } from '../../../types/AaveIncentivesController'; import { StakedAaveV2 } from '../../../types/StakedAaveV2'; +import { StakedAaveV3 } from '../../../types/StakedAaveV3'; export type UserStakeInput = { underlyingAsset: string; @@ -18,7 +19,8 @@ export async function getUserIndex( | AaveDistributionManager | AaveIncentivesController | StakedAave - | StakedAaveV2, + | StakedAaveV2 + | StakedAaveV3, user: string, asset: string ): Promise { diff --git a/test/StakedAaveV3/claimHelper.spec.ts b/test/StakedAaveV3/claimHelper.spec.ts new file mode 100644 index 0000000..7a44aad --- /dev/null +++ b/test/StakedAaveV3/claimHelper.spec.ts @@ -0,0 +1,376 @@ +import { makeSuite, TestEnv } from '../helpers/make-suite'; +import { + COOLDOWN_SECONDS, + UNSTAKE_WINDOW, + MAX_UINT_AMOUNT, + SHORT_EXECUTOR, + WAD, +} from '../../helpers/constants'; +import { + waitForTx, + timeLatest, + advanceBlock, + increaseTimeAndMine, + DRE, + evmRevert, + evmSnapshot, +} from '../../helpers/misc-utils'; +import { ethers } from 'ethers'; +import BigNumber from 'bignumber.js'; +import { + buildPermitParams, + getContract, + getEthersSigners, + getSignatureFromTypedData, +} from '../../helpers/contracts-helpers'; +import { + deployClaimHelper, + deployStakedAaveV3, + getStakedAaveProxy, +} from '../../helpers/contracts-accessors'; +import { StakedTokenV3 } from '../../types/StakedTokenV3'; +import { StakedAaveV3 } from '../../types/StakedAaveV3'; +import { getUserIndex } from '../DistributionManager/data-helpers/asset-user-data'; +import { getRewards } from '../DistributionManager/data-helpers/base-math'; +import { compareRewardsAtAction } from '../StakedAaveV2/data-helpers/reward'; +import { fail } from 'assert'; +import { parseEther } from 'ethers/lib/utils'; +import { ClaimStakingRewardsHelper } from '../../types'; + +const { expect } = require('chai'); + +const SLASHING_ADMIN = 0; +const COOLDOWN_ADMIN = 1; +const CLAIM_HELPER_ROLE = 2; + +makeSuite('StakedAave V3 Claim Helper', (testEnv: TestEnv) => { + let stakeAaveV3: StakedAaveV3; + let stakeAave2V3: StakedAaveV3; + let claimHelper: ClaimStakingRewardsHelper; + let snap: string; + // it('Deploys 2 stake tokens with claimHelper address', async () => { + beforeEach(async () => { + const { aaveToken, users } = testEnv; + + const [deployer, rewardsVault] = await getEthersSigners(); + + const rewardsVaultAddress = (await rewardsVault.getAddress()).toString(); + const emissionManager = await deployer.getAddress(); + + stakeAaveV3 = await deployStakedAaveV3([ + aaveToken.address, + aaveToken.address, + COOLDOWN_SECONDS, + UNSTAKE_WINDOW, + rewardsVaultAddress, + emissionManager, + (1000 * 60 * 60).toString(), + ]); + + stakeAave2V3 = await deployStakedAaveV3([ + aaveToken.address, + aaveToken.address, + COOLDOWN_SECONDS, + UNSTAKE_WINDOW, + rewardsVaultAddress, + emissionManager, + (1000 * 60 * 60).toString(), + ]); + + await aaveToken.connect(rewardsVault).approve(stakeAaveV3.address, MAX_UINT_AMOUNT); + await aaveToken.connect(rewardsVault).approve(stakeAave2V3.address, MAX_UINT_AMOUNT); + + // deploy claim helper contract + claimHelper = await deployClaimHelper( + [stakeAaveV3.address, stakeAave2V3.address, aaveToken.address], + false + ); + + //initialize the stake instance + + await stakeAaveV3['initialize(address,address,address,uint256,string,string,uint8)']( + users[0].address, + users[1].address, + claimHelper.address, + '2000', + 'Staked AAVE', + 'stkAAVE', + 18 + ); + await stakeAave2V3['initialize(address,address,address,uint256,string,string,uint8)']( + users[0].address, + users[1].address, + claimHelper.address, + '2000', + 'Staked AAVE', + 'stkAAVE', + 18 + ); + + await waitForTx( + await stakeAaveV3.connect(deployer).configureAssets([ + { + emissionPerSecond: parseEther('0.001').toString(), + totalStaked: parseEther('0').toString(), + underlyingAsset: stakeAaveV3.address, + }, + ]) + ); + + await waitForTx( + await stakeAave2V3.connect(deployer).configureAssets([ + { + emissionPerSecond: parseEther('0.001').toString(), + totalStaked: parseEther('0').toString(), + underlyingAsset: stakeAave2V3.address, + }, + ]) + ); + const slashingAdmin = await stakeAaveV3.getAdmin(SLASHING_ADMIN); //slash admin + const cooldownAdmin = await stakeAaveV3.getAdmin(COOLDOWN_ADMIN); //cooldown admin + const claimAdmin = await stakeAave2V3.getAdmin(CLAIM_HELPER_ROLE); //claim admin // helper contract + const slashingAdmin2 = await stakeAave2V3.getAdmin(SLASHING_ADMIN); //slash admin + const cooldownAdmin2 = await stakeAave2V3.getAdmin(COOLDOWN_ADMIN); //cooldown admin + const claimAdmin2 = await stakeAave2V3.getAdmin(CLAIM_HELPER_ROLE); //claim admin // helper contract + + expect(slashingAdmin).to.be.equal(users[0].address); + expect(cooldownAdmin).to.be.equal(users[1].address); + expect(claimAdmin).to.be.equal(claimHelper.address); + expect(slashingAdmin2).to.be.equal(users[0].address); + expect(cooldownAdmin2).to.be.equal(users[1].address); + expect(claimAdmin2).to.be.equal(claimHelper.address); + }); + it('Claims all rewards from both stakes', async () => { + const { + aaveToken, + users: [, , , , staker], + } = testEnv; + const amount = ethers.utils.parseEther('10'); + + // Prepare actions for the test case + await aaveToken.connect(staker.signer).approve(stakeAaveV3.address, amount); + await aaveToken.connect(staker.signer).approve(stakeAave2V3.address, amount); + + await stakeAaveV3.connect(staker.signer).stake(staker.address, amount); + await stakeAave2V3.connect(staker.signer).stake(staker.address, amount); + + const userAaveBalance = await aaveToken.balanceOf(staker.address); + + const stakeBalance = await stakeAaveV3.balanceOf(staker.address); + const stakeBalance2 = await stakeAave2V3.balanceOf(staker.address); + + // Increase time for bigger rewards + await increaseTimeAndMine(1000); + + // user1 claims all + const userIndexBefore = await getUserIndex(stakeAaveV3, staker.address, aaveToken.address); + const userIndexBefore2 = await getUserIndex(stakeAave2V3, staker.address, aaveToken.address); + + await claimHelper.connect(staker.signer).claimAllRewards(staker.address); + + const userIndexAfter = await getUserIndex(stakeAaveV3, staker.address, stakeAaveV3.address); + const userIndexAfter2 = await getUserIndex(stakeAave2V3, staker.address, stakeAave2V3.address); + + const expectedAccruedRewards = getRewards( + stakeBalance, + userIndexAfter, + userIndexBefore + ).toString(); + + const expectedAccruedRewards2 = getRewards( + stakeBalance2, + userIndexAfter2, + userIndexBefore2 + ).toString(); + + const userBalanceAfterActions = await aaveToken.balanceOf(staker.address); + + // console.log('balance total: ', saveUserBalance.add(rewards.add(rewards2)).toString()); + expect(userBalanceAfterActions).to.be.equal( + userAaveBalance.add(expectedAccruedRewards).add(expectedAccruedRewards2).toString() + ); + }); + it('Claims all rewards from both stakes even if we have no rewards on one of them', async () => { + const { + aaveToken, + users: [, , , , staker], + } = testEnv; + const amount = ethers.utils.parseEther('10'); + + // Prepare actions for the test case + await aaveToken.connect(staker.signer).approve(stakeAaveV3.address, amount); + + await stakeAaveV3.connect(staker.signer).stake(staker.address, amount); + + const userAaveBalance = await aaveToken.balanceOf(staker.address); + + const stakeBalance = await stakeAaveV3.balanceOf(staker.address); + const stakeBalance2 = await stakeAave2V3.balanceOf(staker.address); + expect(stakeBalance2).to.be.equal(0); + + // Increase time for bigger rewards + await increaseTimeAndMine(1000); + + // user1 claims all + const userIndexBefore = await getUserIndex(stakeAaveV3, staker.address, aaveToken.address); + + await claimHelper.connect(staker.signer).claimAllRewards(staker.address); + + const userIndexAfter = await getUserIndex(stakeAaveV3, staker.address, stakeAaveV3.address); + + const expectedAccruedRewards = getRewards( + stakeBalance, + userIndexAfter, + userIndexBefore + ).toString(); + + const userBalanceAfterActions = await aaveToken.balanceOf(staker.address); + + // console.log('balance total: ', saveUserBalance.add(rewards.add(rewards2)).toString()); + expect(userBalanceAfterActions).to.be.equal(userAaveBalance.add(expectedAccruedRewards)); + }); + it('Claims all rewards from both stakes and stakes claimed amount', async () => { + const { + aaveToken, + users: [, staker], + } = testEnv; + const amount = ethers.utils.parseEther('10'); + + // Prepare actions for the test case + await aaveToken.connect(staker.signer).approve(stakeAaveV3.address, amount); + await aaveToken.connect(staker.signer).approve(stakeAave2V3.address, amount); + + await stakeAaveV3.connect(staker.signer).stake(staker.address, amount); + await stakeAave2V3.connect(staker.signer).stake(staker.address, amount); + + // Increase time for bigger rewards + await increaseTimeAndMine(1000); + + const saveUserBalance = await aaveToken.balanceOf(staker.address); + + const stakeBalance = await stakeAaveV3.balanceOf(staker.address); + const stakeBalance2 = await stakeAave2V3.balanceOf(staker.address); + + const userIndexBefore = await getUserIndex(stakeAaveV3, staker.address, aaveToken.address); + const userIndexBefore2 = await getUserIndex(stakeAave2V3, staker.address, aaveToken.address); + + // claim and stake + await claimHelper.connect(staker.signer).claimAllRewardsAndStake(staker.address); + + const userIndexAfter = await getUserIndex(stakeAaveV3, staker.address, stakeAaveV3.address); + const userIndexAfter2 = await getUserIndex(stakeAave2V3, staker.address, stakeAave2V3.address); + + const expectedAccruedRewards = getRewards( + stakeBalance, + userIndexAfter, + userIndexBefore + ).toString(); + + const expectedAccruedRewards2 = getRewards( + stakeBalance2, + userIndexAfter2, + userIndexBefore2 + ).toString(); + + // current state + const userBalanceAfterActions = await aaveToken.balanceOf(staker.address); + + const stakeBalanceAfter = await stakeAaveV3.balanceOf(staker.address); + + expect(userBalanceAfterActions).to.be.equal(saveUserBalance); + expect(stakeBalanceAfter).to.be.equal( + stakeBalance.add(expectedAccruedRewards).add(expectedAccruedRewards2) + ); + }); + it('Claims all rewards from both stakes and stakes claimed amount even if it has no stake in one of the stakes', async () => { + const { + aaveToken, + users: [, staker], + } = testEnv; + const amount = ethers.utils.parseEther('10'); + + // Prepare actions for the test case + await aaveToken.connect(staker.signer).approve(stakeAaveV3.address, amount); + + await stakeAaveV3.connect(staker.signer).stake(staker.address, amount); + + // Increase time for bigger rewards + await increaseTimeAndMine(1000); + + const saveUserBalance = await aaveToken.balanceOf(staker.address); + + const stakeBalance = await stakeAaveV3.balanceOf(staker.address); + const stakeBalance2 = await stakeAave2V3.balanceOf(staker.address); + + const userIndexBefore = await getUserIndex(stakeAaveV3, staker.address, aaveToken.address); + const userIndexBefore2 = await getUserIndex(stakeAave2V3, staker.address, aaveToken.address); + + // claim and stake + await claimHelper.connect(staker.signer).claimAllRewardsAndStake(staker.address); + + const userIndexAfter = await getUserIndex(stakeAaveV3, staker.address, stakeAaveV3.address); + const userIndexAfter2 = await getUserIndex(stakeAave2V3, staker.address, stakeAave2V3.address); + + const expectedAccruedRewards = getRewards( + stakeBalance, + userIndexAfter, + userIndexBefore + ).toString(); + + const expectedAccruedRewards2 = getRewards( + stakeBalance2, + userIndexAfter2, + userIndexBefore2 + ).toString(); + + // current state + const userBalanceAfterActions = await aaveToken.balanceOf(staker.address); + + const stakeBalanceAfter = await stakeAaveV3.balanceOf(staker.address); + + expect(userBalanceAfterActions).to.be.equal(saveUserBalance); + expect(stakeBalanceAfter).to.be.equal( + stakeBalance.add(expectedAccruedRewards).add(expectedAccruedRewards2) + ); + }); + it('Claims the rewards from a stake and stakes claimed amount', async () => { + const { + aaveToken, + users: [, staker], + } = testEnv; + const amount = ethers.utils.parseEther('10'); + + // Prepare actions for the test case + await aaveToken.connect(staker.signer).approve(stakeAaveV3.address, amount); + + await stakeAaveV3.connect(staker.signer).stake(staker.address, amount); + + // Increase time for bigger rewards + await increaseTimeAndMine(1000); + + const saveUserBalance = await aaveToken.balanceOf(staker.address); + + const stakeBalance = await stakeAaveV3.balanceOf(staker.address); + + const userIndexBefore = await getUserIndex(stakeAaveV3, staker.address, aaveToken.address); + + // claim and stake + await claimHelper.connect(staker.signer).claimAndStake(staker.address, stakeAaveV3.address); + + const userIndexAfter = await getUserIndex(stakeAaveV3, staker.address, stakeAaveV3.address); + + const expectedAccruedRewards = getRewards( + stakeBalance, + userIndexAfter, + userIndexBefore + ).toString(); + + // current state + const userBalanceAfterActions = await aaveToken.balanceOf(staker.address); + + const stakeBalanceAfter = await stakeAaveV3.balanceOf(staker.address); + + expect(userBalanceAfterActions).to.be.equal(saveUserBalance); + expect(stakeBalanceAfter).to.be.equal(stakeBalance.add(expectedAccruedRewards)); + }); +});