diff --git a/contracts/interfaces/IERC20WithPermit.sol b/contracts/interfaces/IERC20WithPermit.sol new file mode 100644 index 0000000..5556f03 --- /dev/null +++ b/contracts/interfaces/IERC20WithPermit.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.7.5; + +import {IERC20} from './IERC20.sol'; + +interface IERC20WithPermit is IERC20 { + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} diff --git a/contracts/interfaces/ISlashableStakeToken.sol b/contracts/interfaces/ISlashableStakeToken.sol deleted file mode 100644 index 51e2de3..0000000 --- a/contracts/interfaces/ISlashableStakeToken.sol +++ /dev/null @@ -1,18 +0,0 @@ - -// SPDX-License-Identifier: agpl-3.0 -pragma solidity 0.7.5; - -import {IStakedToken} from "./IStakedToken.sol"; - -interface ISlashableStakeToken is IStakedToken { - - function exchangeRate() external view returns(uint256); - - function getCooldownPaused() external view returns(bool); - function setCooldownPause(bool paused) external; - - function slash(address destination, uint256 amount) external; - - function getMaxSlashablePercentage() external view returns(uint256); - function setMaxSlashablePercentage(uint256 percentage) external; -} diff --git a/contracts/interfaces/IStakedTokenV3.sol b/contracts/interfaces/IStakedTokenV3.sol new file mode 100644 index 0000000..45d626a --- /dev/null +++ b/contracts/interfaces/IStakedTokenV3.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.7.5; + +import {IStakedToken} from './IStakedToken.sol'; + +interface IStakedTokenV3 is IStakedToken { + function exchangeRate() external view returns (uint256); + + function getCooldownPaused() external view returns (bool); + + function setCooldownPause(bool paused) external; + + function slash(address destination, uint256 amount) external; + + function getMaxSlashablePercentage() external view returns (uint256); + + function setMaxSlashablePercentage(uint256 percentage) external; + + function stakeWithPermit( + address from, + address to, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + function claimRewardsOnBehalf( + address from, + address to, + uint256 amount + ) external; + + function redeemOnBehalf( + address from, + address to, + uint256 amount + ) external; + + function claimRewardsAndStake(address to, uint256 amount) external; + + function claimRewardsAndRedeem( + address to, + uint256 claimAmount, + uint256 redeemAmount + ) external; + + function claimRewardsAndStakeOnBehalf( + address from, + address to, + uint256 amount + ) external; + + function claimRewardsAndRedeemOnBehalf( + address from, + address to, + uint256 claimAmount, + uint256 redeemAmount + ) external; +} diff --git a/contracts/stake/StakedTokenV2.sol b/contracts/stake/StakedTokenV2.sol index 4a0630c..96b967e 100644 --- a/contracts/stake/StakedTokenV2.sol +++ b/contracts/stake/StakedTokenV2.sol @@ -30,9 +30,10 @@ contract StakedTokenV2 is using SafeMath for uint256; using SafeERC20 for IERC20; - function REVISION() public virtual pure returns(uint256) { + function REVISION() public pure virtual returns (uint256) { return 2; } + IERC20 public immutable STAKED_TOKEN; IERC20 public immutable REWARD_TOKEN; uint256 public immutable COOLDOWN_SECONDS; @@ -117,7 +118,7 @@ contract StakedTokenV2 is ); } - function stake(address onBehalfOf, uint256 amount) external override virtual { + function stake(address onBehalfOf, uint256 amount) external virtual override { require(amount != 0, 'INVALID_ZERO_AMOUNT'); uint256 balanceOfUser = balanceOf(onBehalfOf); @@ -141,7 +142,7 @@ contract StakedTokenV2 is * @param to Address to redeem to * @param amount Amount to redeem **/ - function redeem(address to, uint256 amount) external override virtual { + function redeem(address to, uint256 amount) external virtual override { require(amount != 0, 'INVALID_ZERO_AMOUNT'); //solium-disable-next-line uint256 cooldownStartTimestamp = stakersCooldowns[msg.sender]; @@ -187,7 +188,7 @@ contract StakedTokenV2 is * @param to Address to stake for * @param amount Amount to stake **/ - function claimRewards(address to, uint256 amount) external override { + function claimRewards(address to, uint256 amount) external virtual override { uint256 newTotalRewards = _updateCurrentUnclaimedRewards(msg.sender, balanceOf(msg.sender), false); uint256 amountToClaim = (amount == type(uint256).max) ? newTotalRewards : amount; @@ -280,7 +281,7 @@ contract StakedTokenV2 is uint256 amountToReceive, address toAddress, uint256 toBalance - ) public returns (uint256) { + ) public view returns (uint256) { uint256 toCooldownTimestamp = stakersCooldowns[toAddress]; if (toCooldownTimestamp == 0) { return 0; @@ -306,7 +307,6 @@ contract StakedTokenV2 is .div(amountToReceive.add(toBalance)); } } - stakersCooldowns[toAddress] = toCooldownTimestamp; return toCooldownTimestamp; } @@ -331,7 +331,7 @@ contract StakedTokenV2 is * @dev returns the revision of the implementation contract * @return The revision */ - function getRevision() internal virtual pure override returns (uint256) { + function getRevision() internal pure virtual override returns (uint256) { return REVISION(); } diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index 0ed6d60..6ed3162 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -5,7 +5,8 @@ pragma experimental ABIEncoderV2; import {ERC20} from '@aave/aave-token/contracts/open-zeppelin/ERC20.sol'; import {IERC20} from '../interfaces/IERC20.sol'; -import {ISlashableStakeToken} from '../interfaces/ISlashableStakeToken.sol'; +import {IERC20WithPermit} from '../interfaces/IERC20WithPermit.sol'; +import {IStakedTokenV3} from '../interfaces/IStakedTokenV3.sol'; import {IStakedToken} from '../interfaces/IStakedToken.sol'; import {ITransferHook} from '../interfaces/ITransferHook.sol'; @@ -25,23 +26,21 @@ import {RoleManager} from '../utils/RoleManager.sol'; * @notice Contract to stake Aave token, tokenize the position and get rewards, inheriting from a distribution manager contract * @author Aave **/ -contract StakedTokenV3 is StakedTokenV2, - ISlashableStakeToken, - RoleManager -{ +contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { using SafeMath for uint256; using SafeERC20 for IERC20; using PercentageMath for uint256; uint256 public constant SLASH_ADMIN_ROLE = 0; uint256 public constant COOLDOWN_ADMIN_ROLE = 1; - - function REVISION() public virtual override pure returns(uint256) { + uint256 public constant CLAIM_HELPER_ROLE = 2; + + function REVISION() public pure virtual override returns (uint256) { return 3; } - - //maximum percentage of the underlying that can be slashed in a single realization event - uint256 internal _maxSlashablePercentage; + + //maximum percentage of the underlying that can be slashed in a single realization event + uint256 internal _maxSlashablePercentage; bool _cooldownPaused; modifier onlySlashingAdmin { @@ -49,14 +48,23 @@ contract StakedTokenV3 is StakedTokenV2, _; } - modifier onlyCooldownAdmin { + modifier onlyCooldownAdmin { require(msg.sender == getAdmin(COOLDOWN_ADMIN_ROLE), 'CALLER_NOT_COOLDOWN_ADMIN'); _; } + modifier onlyClaimHelper { + require(msg.sender == getAdmin(CLAIM_HELPER_ROLE), 'CALLER_NOT_CLAIM_HELPER'); + _; + } - event Staked(address indexed from, address indexed onBehalfOf, uint256 amount, uint256 sharesMinted); - event Redeem(address indexed from, address indexed to, uint256 amount, uint256 underlyingTransferred); + event Staked(address indexed from, address indexed to, uint256 amount, uint256 sharesMinted); + event Redeem( + address indexed from, + address indexed to, + uint256 amount, + uint256 underlyingTransferred + ); event CooldownPauseChanged(bool pause); event MaxSlashablePercentageChanged(uint256 newPercentage); event Slashed(address indexed destination, uint256 amount); @@ -75,23 +83,28 @@ contract StakedTokenV3 is StakedTokenV2, string memory symbol, uint8 decimals, address governance - ) public StakedTokenV2(stakedToken, - rewardToken, - cooldownSeconds, - unstakeWindow, - rewardsVault, - emissionManager, - distributionDuration, + ) + public + StakedTokenV2( + stakedToken, + rewardToken, + cooldownSeconds, + unstakeWindow, + rewardsVault, + emissionManager, + distributionDuration, name, symbol, - decimals, - governance) { - } + decimals, + governance + ) + {} + /** * @dev Inherited from StakedTokenV2, deprecated **/ function initialize() external override { - revert("DEPRECATED"); + revert('DEPRECATED'); } /** @@ -100,10 +113,12 @@ contract StakedTokenV3 is StakedTokenV2, function initialize( address slashingAdmin, address cooldownPauseAdmin, + address claimHelper, uint256 maxSlashablePercentage, string calldata name, string calldata symbol, - uint8 decimals) external initializer { + uint8 decimals + ) external initializer { uint256 chainId; //solium-disable-next-line @@ -126,15 +141,17 @@ contract StakedTokenV3 is StakedTokenV2, _symbol = symbol; _setupDecimals(decimals); } - - address[] memory adminsAddresses = new address[](2); - uint256[] memory adminsRoles = new uint256[](2); + + address[] memory adminsAddresses = new address[](3); + uint256[] memory adminsRoles = new uint256[](3); adminsAddresses[0] = slashingAdmin; adminsAddresses[1] = cooldownPauseAdmin; + adminsAddresses[2] = claimHelper; adminsRoles[0] = SLASH_ADMIN_ROLE; adminsRoles[1] = COOLDOWN_ADMIN_ROLE; + adminsRoles[2] = CLAIM_HELPER_ROLE; _initAdmins(adminsRoles, adminsAddresses); @@ -142,33 +159,34 @@ contract StakedTokenV3 is StakedTokenV2, } /** - * @dev allows a user to stake STAKED_TOKEN - * @param onBehalfOf address of the user that will receive stake token shares - * @param amount the amount to be staked - **/ - function stake(address onBehalfOf, uint256 amount) external override(IStakedToken,StakedTokenV2) { - require(amount != 0, 'INVALID_ZERO_AMOUNT'); - uint256 balanceOfUser = balanceOf(onBehalfOf); - - uint256 accruedRewards = _updateUserAssetInternal( - onBehalfOf, - address(this), - balanceOfUser, - totalSupply() - ); - if (accruedRewards != 0) { - emit RewardsAccrued(onBehalfOf, accruedRewards); - stakerRewardsToClaim[onBehalfOf] = stakerRewardsToClaim[onBehalfOf].add(accruedRewards); - } - - stakersCooldowns[onBehalfOf] = getNextCooldownTimestamp(0, amount, onBehalfOf, balanceOfUser); - - uint256 sharesToMint = amount.mul(1e18).div(exchangeRate()); - _mint(onBehalfOf, sharesToMint); - - IERC20(STAKED_TOKEN).safeTransferFrom(msg.sender, address(this), amount); + * @dev Allows a from to stake STAKED_TOKEN + * @param to Address of the from that will receive stake token shares + * @param amount The amount to be staked + **/ + function stake(address to, uint256 amount) external override(IStakedToken, StakedTokenV2) { + _stake(msg.sender, to, amount, true); + } - emit Staked(msg.sender, onBehalfOf, amount, sharesToMint); + /** + * @dev Allows a from to stake STAKED_TOKEN with gasless approvals (permit) + * @param to Address of the from that will receive stake token shares + * @param amount The amount to be staked + * @param deadline The permit execution deadline + * @param v The v component of the signed message + * @param r The r component of the signed message + * @param s The s component of the signed message + **/ + function stakeWithPermit( + address from, + address to, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external override { + IERC20WithPermit(address(STAKED_TOKEN)).permit(from, address(this), amount, deadline, v, r, s); + _stake(from, to, amount, true); } /** @@ -176,36 +194,108 @@ contract StakedTokenV3 is StakedTokenV2, * @param to Address to redeem to * @param amount Amount to redeem **/ - function redeem(address to, uint256 amount) external override(IStakedToken,StakedTokenV2) { - require(amount != 0, 'INVALID_ZERO_AMOUNT'); - //solium-disable-next-line - uint256 cooldownStartTimestamp = stakersCooldowns[msg.sender]; - - require( - !_cooldownPaused && block.timestamp > cooldownStartTimestamp.add(COOLDOWN_SECONDS), - 'INSUFFICIENT_COOLDOWN' - ); - require( - block.timestamp.sub(cooldownStartTimestamp.add(COOLDOWN_SECONDS)) <= UNSTAKE_WINDOW, - 'UNSTAKE_WINDOW_FINISHED' - ); - uint256 balanceOfMessageSender = balanceOf(msg.sender); + function redeem(address to, uint256 amount) external override(IStakedToken, StakedTokenV2) { + _redeem(msg.sender, to, amount); + } - uint256 amountToRedeem = (amount > balanceOfMessageSender) ? balanceOfMessageSender : amount; + /** + * @dev Redeems staked tokens for a user. Only the claim helper contract is allowed to call this function + * @param from Address to redeem from + * @param to Address to redeem to + * @param amount Amount to redeem + **/ + function redeemOnBehalf( + address from, + address to, + uint256 amount + ) external override onlyClaimHelper { + _redeem(from, to, amount); + } - _updateCurrentUnclaimedRewards(msg.sender, balanceOfMessageSender, true); + /** + * @dev Claims an `amount` of `REWARD_TOKEN` to the address `to` + * @param to Address to send the claimed rewards + * @param amount Amount to stake + **/ + function claimRewards(address to, uint256 amount) external override(StakedTokenV2, IStakedToken) { + _claimRewards(msg.sender, to, amount); + } - uint256 underlyingToRedeem = amountToRedeem.mul(exchangeRate()).div(1e18); + /** + * @dev Claims an `amount` of `REWARD_TOKEN` to the address `to` on behalf of the user. Only the claim helper contract is allowed to call this function + * @param from The address of the user from to claim + * @param to Address to send the claimed rewards + * @param amount Amount to claim + **/ + function claimRewardsOnBehalf( + address from, + address to, + uint256 amount + ) external override onlyClaimHelper { + _claimRewards(from, to, amount); + } - _burn(msg.sender, amountToRedeem); + /** + * @dev Claims an `amount` of `REWARD_TOKEN` amd restakes + * @param to Address to stake to + * @param amount Amount to claim + **/ + function claimRewardsAndStake(address to, uint256 amount) external override { + require(REWARD_TOKEN == STAKED_TOKEN, 'REWARD_TOKEN_IS_NOT_STAKED_TOKEN'); - if (balanceOfMessageSender.sub(amountToRedeem) == 0) { - stakersCooldowns[msg.sender] = 0; + uint256 rewardsClaimed = _claimRewards(msg.sender, address(this), amount); + if (rewardsClaimed != 0) { + _stake(address(this), to, rewardsClaimed, false); } + } - IERC20(STAKED_TOKEN).safeTransfer(to, underlyingToRedeem); + /** + * @dev Claims an `amount` of `REWARD_TOKEN` and restakes. Only the claim helper contract is allowed to call this function + * @param from The address of the from from which to claim + * @param to Address to stake to + * @param amount Amount to claim + **/ + function claimRewardsAndStakeOnBehalf( + address from, + address to, + uint256 amount + ) external override onlyClaimHelper { + require(REWARD_TOKEN == STAKED_TOKEN, 'REWARD_TOKEN_IS_NOT_STAKED_TOKEN'); + + uint256 rewardsClaimed = _claimRewards(from, address(this), amount); + _stake(address(this), to, rewardsClaimed, false); + } + + /** + * @dev Claims an `amount` of `REWARD_TOKEN` amd redeem + * @param claimAmount Amount to claim + * @param redeemAmount Amount to redeem + * @param to Address to claim and unstake to + **/ + function claimRewardsAndRedeem( + address to, + uint256 claimAmount, + uint256 redeemAmount + ) external override { + _claimRewards(msg.sender, to, claimAmount); + _redeem(msg.sender, to, redeemAmount); + } - emit Redeem(msg.sender, to, amountToRedeem, underlyingToRedeem); + /** + * @dev Claims an `amount` of `REWARD_TOKEN` and redeem. Only the claim helper contract is allowed to call this function + * @param from The address of the from + * @param to Address to claim and unstake to + * @param claimAmount Amount to claim + * @param redeemAmount Amount to redeem + **/ + function claimRewardsAndRedeemOnBehalf( + address from, + address to, + uint256 claimAmount, + uint256 redeemAmount + ) external override onlyClaimHelper { + _claimRewards(from, to, claimAmount); + _redeem(from, to, redeemAmount); } /** @@ -213,7 +303,7 @@ contract StakedTokenV3 is StakedTokenV2, * Slashing will reduce the exchange rate. Supplying STAKED_TOKEN to the stake contract * can replenish the slashed STAKED_TOKEN and bring the exchange rate back to 1 **/ - function exchangeRate() public override view returns (uint256) { + function exchangeRate() public view override returns (uint256) { uint256 currentSupply = totalSupply(); if (currentSupply == 0) { @@ -227,62 +317,145 @@ contract StakedTokenV3 is StakedTokenV2, * @dev Executes a slashing of the underlying of a certain amount, transferring the seized funds * to destination. Decreasing the amount of underlying will automatically adjust the exchange rate * @param destination the address where seized funds will be transferred - * @param amount the amount + * @param amount the amount **/ function slash(address destination, uint256 amount) external override onlySlashingAdmin { - - uint256 balance = IERC20(STAKED_TOKEN).balanceOf(address(this)); + uint256 balance = STAKED_TOKEN.balanceOf(address(this)); uint256 maxSlashable = balance.percentMul(_maxSlashablePercentage); - require(amount <= maxSlashable, "INVALID_SLASHING_AMOUNT"); + require(amount <= maxSlashable, 'INVALID_SLASHING_AMOUNT'); - IERC20(STAKED_TOKEN).safeTransfer(destination, amount); + STAKED_TOKEN.safeTransfer(destination, amount); emit Slashed(destination, amount); } /** - * @dev returns true if the unstake cooldown is paused - */ - function getCooldownPaused() external override view returns(bool) { - return _cooldownPaused; + * @dev returns true if the unstake cooldown is paused + */ + function getCooldownPaused() external view override returns (bool) { + return _cooldownPaused; } /** - * @dev sets the state of the cooldown pause - * @param paused true if the cooldown needs to be paused, false otherwise - */ + * @dev sets the state of the cooldown pause + * @param paused true if the cooldown needs to be paused, false otherwise + */ function setCooldownPause(bool paused) external override onlyCooldownAdmin { _cooldownPaused = paused; emit CooldownPauseChanged(paused); } /** - * @dev sets the admin of the slashing pausing function - * @param percentage the new maximum slashable percentage - */ + * @dev sets the admin of the slashing pausing function + * @param percentage the new maximum slashable percentage + */ function setMaxSlashablePercentage(uint256 percentage) external override onlySlashingAdmin { - require(percentage <= PercentageMath.PERCENTAGE_FACTOR, "INVALID_SLASHING_PERCENTAGE"); + require(percentage <= PercentageMath.PERCENTAGE_FACTOR, 'INVALID_SLASHING_PERCENTAGE'); _maxSlashablePercentage = percentage; emit MaxSlashablePercentageChanged(percentage); } /** - * @dev returns the current maximum slashable percentage of the stake - */ - function getMaxSlashablePercentage() external override view returns(uint256) { + * @dev returns the current maximum slashable percentage of the stake + */ + function getMaxSlashablePercentage() external view override returns (uint256) { return _maxSlashablePercentage; } - - /** + /** * @dev returns the revision of the implementation contract * @return The revision */ - function getRevision() internal virtual pure override returns (uint256) { + function getRevision() internal pure virtual override returns (uint256) { return REVISION(); } + function _claimRewards( + address from, + address to, + uint256 amount + ) internal returns (uint256) { + uint256 newTotalRewards = _updateCurrentUnclaimedRewards(from, balanceOf(from), false); + + uint256 amountToClaim = (amount == type(uint256).max) ? newTotalRewards : amount; + + stakerRewardsToClaim[from] = newTotalRewards.sub(amountToClaim, 'INVALID_AMOUNT'); + REWARD_TOKEN.safeTransferFrom(REWARDS_VAULT, to, amountToClaim); + emit RewardsClaimed(from, to, amountToClaim); + return (amountToClaim); + } + + function _stake( + address from, + address to, + uint256 amount, + bool pullFunds + ) internal { + require(amount != 0, 'INVALID_ZERO_AMOUNT'); + + uint256 balanceOfUser = balanceOf(to); + + uint256 accruedRewards = + _updateUserAssetInternal(to, address(this), balanceOfUser, totalSupply()); + + if (accruedRewards != 0) { + emit RewardsAccrued(to, accruedRewards); + stakerRewardsToClaim[to] = stakerRewardsToClaim[to].add(accruedRewards); + } + + stakersCooldowns[to] = getNextCooldownTimestamp(0, amount, to, balanceOfUser); + + uint256 sharesToMint = amount.mul(1e18).div(exchangeRate()); + _mint(to, sharesToMint); + + if (pullFunds) { + STAKED_TOKEN.safeTransferFrom(from, address(this), amount); + } + + emit Staked(from, to, amount, sharesToMint); + } + + /** + * @dev Redeems staked tokens, and stop earning rewards + * @param to Address to redeem to + * @param amount Amount to redeem + **/ + function _redeem( + address from, + address to, + uint256 amount + ) internal { + require(amount != 0, 'INVALID_ZERO_AMOUNT'); + //solium-disable-next-line + uint256 cooldownStartTimestamp = stakersCooldowns[from]; + + require( + !_cooldownPaused && block.timestamp > cooldownStartTimestamp.add(COOLDOWN_SECONDS), + 'INSUFFICIENT_COOLDOWN' + ); + require( + block.timestamp.sub(cooldownStartTimestamp.add(COOLDOWN_SECONDS)) <= UNSTAKE_WINDOW, + 'UNSTAKE_WINDOW_FINISHED' + ); + uint256 balanceOfFrom = balanceOf(from); + + uint256 amountToRedeem = (amount > balanceOfFrom) ? balanceOfFrom : amount; + + _updateCurrentUnclaimedRewards(from, balanceOfFrom, true); + + uint256 underlyingToRedeem = amountToRedeem.mul(exchangeRate()).div(1e18); + + _burn(from, amountToRedeem); + + if (balanceOfFrom.sub(amountToRedeem) == 0) { + stakersCooldowns[from] = 0; + } + + IERC20(STAKED_TOKEN).safeTransfer(to, underlyingToRedeem); + + emit Redeem(from, to, amountToRedeem, underlyingToRedeem); + } } diff --git a/contracts/utils/MintableErc20.sol b/contracts/utils/MintableErc20.sol index 9cbc73d..bf44d35 100644 --- a/contracts/utils/MintableErc20.sol +++ b/contracts/utils/MintableErc20.sol @@ -23,4 +23,19 @@ contract MintableErc20 is ERC20 { _mint(msg.sender, value); return true; } + + /** + * @dev implements a mock permit feature + **/ +function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + _approve(owner, spender, value); + } } diff --git a/hardhat.config.ts b/hardhat.config.ts index 06a9c6e..7692493 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -9,6 +9,7 @@ import '@nomiclabs/hardhat-waffle'; import '@nomiclabs/hardhat-etherscan'; import path from 'path'; import fs from 'fs'; +import 'hardhat-gas-reporter'; export const BUIDLEREVM_CHAIN_ID = 31337; diff --git a/package-lock.json b/package-lock.json index 268deb6..a3d3ef1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1103,6 +1103,24 @@ "integrity": "sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw==", "dev": true }, + "@types/concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-OU2+C7X+5Gs42JZzXoto7yOQ0A0=", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/form-data": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", + "integrity": "sha1-yayFsqX9GENbjIXZ7LUObWyJP/g=", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", @@ -1205,6 +1223,12 @@ "integrity": "sha512-6gOkRe7OIioWAXfnO/2lFiv+SJichKVSys1mSsgyrYHSEjk8Ctv4tSR/Odvnu+HWlH2C8j53dahU03XmQdd5fA==", "dev": true }, + "@types/qs": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", + "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==", + "dev": true + }, "@types/resolve": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", @@ -1479,6 +1503,12 @@ "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", "dev": true }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -1677,6 +1707,24 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, "blakejs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.1.0.tgz", @@ -2069,6 +2117,12 @@ "supports-color": "^5.3.0" } }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "dev": true + }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -2211,6 +2265,50 @@ } } }, + "cli-table3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", + "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "dev": true, + "requires": { + "colors": "^1.1.2", + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", @@ -2262,6 +2360,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2312,6 +2416,50 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -2489,6 +2637,12 @@ } } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "dev": true + }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -2735,6 +2889,17 @@ "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", "dev": true }, + "drbg.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", + "integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=", + "dev": true, + "requires": { + "browserify-aes": "^1.0.6", + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4" + } + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -3028,6 +3193,134 @@ "js-sha3": "^0.5.7" } }, + "eth-gas-reporter": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/eth-gas-reporter/-/eth-gas-reporter-0.2.20.tgz", + "integrity": "sha512-gp/PhKrr3hYEEFg5emIQxbhQkVH2mg+iHcM6GvqhzFx5IkZGeQx+5oNzYDEfBXQefcA90rwWHId6eCty6jbdDA==", + "dev": true, + "requires": { + "@ethersproject/abi": "^5.0.0-beta.146", + "@solidity-parser/parser": "^0.8.2", + "cli-table3": "^0.5.0", + "colors": "^1.1.2", + "ethereumjs-util": "6.2.0", + "ethers": "^4.0.40", + "fs-readdir-recursive": "^1.1.0", + "lodash": "^4.17.14", + "markdown-table": "^1.1.3", + "mocha": "^7.1.1", + "req-cwd": "^2.0.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.5", + "sha1": "^1.1.1", + "sync-request": "^6.0.0" + }, + "dependencies": { + "@solidity-parser/parser": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.8.2.tgz", + "integrity": "sha512-8LySx3qrNXPgB5JiULfG10O3V7QTxI/TLzSw5hFQhXWSkVxZBAv4rZQ0sYgLEbc8g3L2lmnujj1hKul38Eu5NQ==", + "dev": true + }, + "@types/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "ethereumjs-util": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.2.0.tgz", + "integrity": "sha512-vb0XN9J2QGdZGIEKG2vXM+kUdEivUfU6Wmi5y0cg+LRhDYKnXIZ/Lz7XjFbHRR9VIKq2lVGLzGBkA++y2nOdOQ==", + "dev": true, + "requires": { + "@types/bn.js": "^4.11.3", + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "0.1.6", + "keccak": "^2.0.0", + "rlp": "^2.2.3", + "secp256k1": "^3.0.1" + } + }, + "ethers": { + "version": "4.0.48", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-4.0.48.tgz", + "integrity": "sha512-sZD5K8H28dOrcidzx9f8KYh8083n5BexIO3+SbE4jK83L85FxtpXZBCQdXb8gkg+7sBqomcLhhkU7UHL+F7I2g==", + "dev": true, + "requires": { + "aes-js": "3.0.0", + "bn.js": "^4.4.0", + "elliptic": "6.5.3", + "hash.js": "1.1.3", + "js-sha3": "0.5.7", + "scrypt-js": "2.0.4", + "setimmediate": "1.0.4", + "uuid": "2.0.1", + "xmlhttprequest": "1.8.0" + } + }, + "hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.0" + } + }, + "keccak": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-2.1.0.tgz", + "integrity": "sha512-m1wbJRTo+gWbctZWay9i26v5fFnYkOn7D5PCxJ3fZUGUEb49dE1Pm4BREUYCt/aoO6di7jeoGmhvqN9Nzylm3Q==", + "dev": true, + "requires": { + "bindings": "^1.5.0", + "inherits": "^2.0.4", + "nan": "^2.14.0", + "safe-buffer": "^5.2.0" + } + }, + "scrypt-js": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-2.0.4.tgz", + "integrity": "sha512-4KsaGcPnuhtCZQCxFxN3GVYIhKFPTdLd8PLC552XwbMndtD0cjRFAhDuuydXQ0h08ZfPgzqe6EKHozpuH74iDw==", + "dev": true + }, + "secp256k1": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.0.tgz", + "integrity": "sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw==", + "dev": true, + "requires": { + "bindings": "^1.5.0", + "bip66": "^1.1.5", + "bn.js": "^4.11.8", + "create-hash": "^1.2.0", + "drbg.js": "^1.0.1", + "elliptic": "^6.5.2", + "nan": "^2.14.0", + "safe-buffer": "^5.1.2" + } + }, + "setimmediate": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.4.tgz", + "integrity": "sha1-IOgd5iLUoCWIzgyNqJc8vPHTE48=", + "dev": true + }, + "uuid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz", + "integrity": "sha1-wqMN7bPlNdcsz4LjQ5QaULqFM6w=", + "dev": true + } + } + }, "eth-lib": { "version": "0.2.8", "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.8.tgz", @@ -3876,6 +4169,12 @@ "reusify": "^1.0.4" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -4085,6 +4384,12 @@ "minipass": "^2.6.0" } }, + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -13782,6 +14087,12 @@ "has-symbols": "^1.0.1" } }, + "get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=", + "dev": true + }, "get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -14238,6 +14549,16 @@ } } }, + "hardhat-gas-reporter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hardhat-gas-reporter/-/hardhat-gas-reporter-1.0.4.tgz", + "integrity": "sha512-G376zKh81G3K9WtDA+SoTLWsoygikH++tD1E7llx+X7J+GbIqfwhDKKgvJjcnEesMrtR9UqQHK02lJuXY1RTxw==", + "dev": true, + "requires": { + "eth-gas-reporter": "^0.2.20", + "sha1": "^1.1.1" + } + }, "hardhat-typechain": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/hardhat-typechain/-/hardhat-typechain-0.3.4.tgz", @@ -14356,6 +14677,18 @@ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", "dev": true }, + "http-basic": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz", + "integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==", + "dev": true, + "requires": { + "caseless": "^0.12.0", + "concat-stream": "^1.6.2", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + } + }, "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -14381,6 +14714,23 @@ "integrity": "sha1-L5CN1fHbQGjAWM1ubUzjkskTOJs=", "dev": true }, + "http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "dev": true, + "requires": { + "@types/node": "^10.0.3" + }, + "dependencies": { + "@types/node": { + "version": "10.17.52", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.52.tgz", + "integrity": "sha512-bKnO8Rcj03i6JTzweabq96k29uVNcXGB0bkwjVQTFagDgxxNged18281AZ0nTMHl+aFpPPWyPrk4Z3+NtW/z5w==", + "dev": true + } + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -15435,6 +15785,12 @@ "object-visit": "^1.0.0" } }, + "markdown-table": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", + "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==", + "dev": true + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -16155,6 +16511,12 @@ "minimatch": "^3.0.4" } }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true + }, "nano-json-stream-parser": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz", @@ -16587,6 +16949,12 @@ "safe-buffer": "^5.1.1" } }, + "parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104=", + "dev": true + }, "parse-headers": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz", @@ -17026,6 +17394,15 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "promise": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz", + "integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==", + "dev": true, + "requires": { + "asap": "~2.0.6" + } + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -17219,6 +17596,32 @@ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", "dev": true }, + "req-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/req-cwd/-/req-cwd-2.0.0.tgz", + "integrity": "sha1-1AgrTURZgDZkD7c93qAe1T20nrw=", + "dev": true, + "requires": { + "req-from": "^2.0.0" + } + }, + "req-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/req-from/-/req-from-2.0.0.tgz", + "integrity": "sha1-10GI5H+TeW9Kpx327jWuaJ8+DnA=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, "request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -17247,6 +17650,26 @@ "uuid": "^3.3.2" } }, + "request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dev": true, + "requires": { + "lodash": "^4.17.19" + } + }, + "request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "dev": true, + "requires": { + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -17585,6 +18008,16 @@ "safe-buffer": "^5.0.1" } }, + "sha1": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz", + "integrity": "sha1-rdqnqTFo85PxnrKxUJFhjicA+Eg=", + "dev": true, + "requires": { + "charenc": ">= 0.0.1", + "crypt": ">= 0.0.1" + } + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -18034,6 +18467,12 @@ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, "steno": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", @@ -18255,6 +18694,26 @@ } } }, + "sync-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz", + "integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==", + "dev": true, + "requires": { + "http-response-object": "^3.0.1", + "sync-rpc": "^1.2.1", + "then-request": "^6.0.0" + } + }, + "sync-rpc": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz", + "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==", + "dev": true, + "requires": { + "get-port": "^3.1.0" + } + }, "tar": { "version": "4.4.13", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", @@ -18297,6 +18756,33 @@ "integrity": "sha512-afH1hO+SQ/VPlmaLUFj2636QMeDvPCeQMc/9RBMW0IfjNe9gFD9Ra3ShqYkB7py0do1ZcCna/9acHyzTJ+GcNA==", "dev": true }, + "then-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz", + "integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==", + "dev": true, + "requires": { + "@types/concat-stream": "^1.6.0", + "@types/form-data": "0.0.33", + "@types/node": "^8.0.0", + "@types/qs": "^6.2.31", + "caseless": "~0.12.0", + "concat-stream": "^1.6.0", + "form-data": "^2.2.0", + "http-basic": "^8.1.1", + "http-response-object": "^3.0.1", + "promise": "^8.0.0", + "qs": "^6.4.0" + }, + "dependencies": { + "@types/node": { + "version": "8.10.66", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", + "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==", + "dev": true + } + } + }, "timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", @@ -18625,6 +19111,12 @@ } } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", diff --git a/package.json b/package.json index 1680275..c4c933d 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,12 @@ "compile": "SKIP_LOAD=true hardhat compile", "compile:force": "npm run compile -- --force", "compile:force:quiet": "npm run compile:force -- --quiet", - "test": "npm run compile:force:quiet && hardhat test test/__setup.spec.ts test/AaveIncentivesController/*.spec.ts test/StakedAave/*.spec.ts test/StakedAaveV2/*.spec.ts test/Slashing/*.spec.ts", + "test": "npm run compile:force:quiet && hardhat test test/__setup.spec.ts test/AaveIncentivesController/*.spec.ts test/StakedAave/*.spec.ts test/StakedAaveV2/*.spec.ts test/StakedAaveV3/*.spec.ts", "test:ci": "npm run compile:force:quiet && npm run test-pei && npm run test-psi && npm run test-psi2 && npm run test-bpt", "test-pei": "npm run test test/__setup.spec.ts test/AaveIncentivesController/*.spec.ts", "test-psi": "npm run test test/__setup.spec.ts test/StakedAave/*.spec.ts", "test-psi2": "npm run test test/__setup.spec.ts test/StakedAaveV2/*.spec.ts", + "test-stk-aave-3": "hardhat test test/__setup.spec.ts test/StakedAaveV3/*.spec.ts", "test-bpt": "npm run compile:force:quiet && FORKING_BLOCK=11730175 MAINNET_FORK=true hardhat test test/StakedBPT/create-bpt-and-stakebpt.spec.ts", "coverage": "SKIP_LOAD=true npx hardhat typechain && node --max_old_space_size=6144 node_modules/.bin/hardhat coverage", "dev:deployment": "hardhat dev-deployment", @@ -66,7 +67,8 @@ "tslint-config-prettier": "^1.18.0", "tslint-plugin-prettier": "^2.3.0", "typechain": "^3.0.0", - "typescript": "^4.1.2" + "typescript": "^4.1.2", + "hardhat-gas-reporter": "^1.0.0" }, "husky": { "hooks": { diff --git a/test/Slashing/stakedAave-slashing.spec.ts b/test/Slashing/stakedAave-slashing.spec.ts deleted file mode 100644 index ccafcc9..0000000 --- a/test/Slashing/stakedAave-slashing.spec.ts +++ /dev/null @@ -1,509 +0,0 @@ -import { makeSuite, TestEnv } from '../helpers/make-suite'; -import { COOLDOWN_SECONDS, UNSTAKE_WINDOW, MAX_UINT_AMOUNT, WAD } from '../../helpers/constants'; -import { waitForTx, timeLatest, advanceBlock, increaseTimeAndMine } from '../../helpers/misc-utils'; -import { ethers } from 'ethers'; -import BigNumber from 'bignumber.js'; -import { getContract, getEthersSigners } from '../../helpers/contracts-helpers'; -import { 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'; - -const { expect } = require('chai'); - -const SLASHING_ADMIN = 0; -const COOLDOWN_ADMIN = 1; - -makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { - let stakeV3: StakedAaveV3; - - it('Deploys StakedAaveV3', async () => { - const { aaveToken, users } = testEnv; - - const [deployer, rewardsVault] = await getEthersSigners(); - - const rewardsVaultAddress = (await rewardsVault.getAddress()).toString(); - const emissionManager = await deployer.getAddress(); - - stakeV3 = await deployStakedAaveV3([ - aaveToken.address, - aaveToken.address, - COOLDOWN_SECONDS, - UNSTAKE_WINDOW, - rewardsVaultAddress, - emissionManager, - (1000 * 60 * 60).toString(), - ]); - - await aaveToken.connect(rewardsVault).approve(stakeV3.address, MAX_UINT_AMOUNT); - - //initialize the stake instance - - await stakeV3['initialize(address,address,uint256,string,string,uint8)']( - users[0].address, - users[1].address, - '2000', - 'Staked AAVE', - 'stkAAVE', - 18 - ); - - const slashingAdmin = await stakeV3.getAdmin(SLASHING_ADMIN); //slash admin - const cooldownAdmin = await stakeV3.getAdmin(COOLDOWN_ADMIN); //cooldown admin - - expect(slashingAdmin).to.be.equal(users[0].address); - expect(cooldownAdmin).to.be.equal(users[1].address); - }); - - it('Reverts trying to stake 0 amount', async () => { - const { - users: [, staker], - } = testEnv; - const amount = '0'; - - await expect(stakeV3.connect(staker.signer).stake(staker.address, amount)).to.be.revertedWith( - 'INVALID_ZERO_AMOUNT' - ); - }); - - it('User 1 stakes 10 AAVE: receives 10 stkAAVE, StakedAave balance of AAVE is 10 and his rewards to claim are 0', async () => { - const { - aaveToken, - users: [, staker], - } = testEnv; - const amount = ethers.utils.parseEther('10'); - - const saveBalanceBefore = new BigNumber((await stakeV3.balanceOf(staker.address)).toString()); - - // Prepare actions for the test case - const actions = () => [ - aaveToken.connect(staker.signer).approve(stakeV3.address, amount), - stakeV3.connect(staker.signer).stake(staker.address, amount), - ]; - - // Check rewards - await compareRewardsAtAction(stakeV3, staker.address, actions); - - // Stake token tests - expect((await stakeV3.balanceOf(staker.address)).toString()).to.be.equal( - saveBalanceBefore.plus(amount.toString()).toString() - ); - expect((await aaveToken.balanceOf(stakeV3.address)).toString()).to.be.equal( - saveBalanceBefore.plus(amount.toString()).toString() - ); - expect((await stakeV3.balanceOf(staker.address)).toString()).to.be.equal(amount); - expect((await aaveToken.balanceOf(stakeV3.address)).toString()).to.be.equal(amount); - }); - - it('User 1 stakes 10 AAVE more: his total SAAVE balance increases, StakedAave balance of Aave increases and his reward until now get accumulated', async () => { - const { - aaveToken, - users: [, staker], - } = testEnv; - const amount = ethers.utils.parseEther('10'); - - const saveBalanceBefore = new BigNumber((await stakeV3.balanceOf(staker.address)).toString()); - const actions = () => [ - aaveToken.connect(staker.signer).approve(stakeV3.address, amount), - stakeV3.connect(staker.signer).stake(staker.address, amount), - ]; - - // Checks rewards - await compareRewardsAtAction(stakeV3, staker.address, actions, true); - - // Extra test checks - expect((await stakeV3.balanceOf(staker.address)).toString()).to.be.equal( - saveBalanceBefore.plus(amount.toString()).toString() - ); - expect((await aaveToken.balanceOf(stakeV3.address)).toString()).to.be.equal( - saveBalanceBefore.plus(amount.toString()).toString() - ); - }); - - it('User 1 claim half rewards', async () => { - const { - aaveToken, - users: [, staker], - } = testEnv; - // Increase time for bigger rewards - await increaseTimeAndMine(1000); - - const halfRewards = (await stakeV3.stakerRewardsToClaim(staker.address)).div(2); - const saveUserBalance = await aaveToken.balanceOf(staker.address); - - await stakeV3.connect(staker.signer).claimRewards(staker.address, halfRewards); - - const userBalanceAfterActions = await aaveToken.balanceOf(staker.address); - expect(userBalanceAfterActions.eq(saveUserBalance.add(halfRewards))).to.be.ok; - }); - - it('User 1 tries to claim higher reward than current rewards balance', async () => { - const { - aaveToken, - users: [, staker], - } = testEnv; - - const saveUserBalance = await aaveToken.balanceOf(staker.address); - - // Try to claim more amount than accumulated - await expect( - stakeV3.connect(staker.signer).claimRewards(staker.address, ethers.utils.parseEther('10000')) - ).to.be.revertedWith('INVALID_AMOUNT'); - - const userBalanceAfterActions = await aaveToken.balanceOf(staker.address); - expect(userBalanceAfterActions.eq(saveUserBalance)).to.be.ok; - }); - - it('User 1 claim all rewards', async () => { - const { - stakedAaveV2, - aaveToken, - users: [, staker], - } = testEnv; - - const userAddress = staker.address; - const underlyingAsset = stakedAaveV2.address; - - const userBalance = await stakedAaveV2.balanceOf(userAddress); - const userAaveBalance = await aaveToken.balanceOf(userAddress); - const userRewards = await stakedAaveV2.stakerRewardsToClaim(userAddress); - // Get index before actions - const userIndexBefore = await getUserIndex(stakedAaveV2, userAddress, underlyingAsset); - - // Claim rewards - await expect(stakedAaveV2.connect(staker.signer).claimRewards(staker.address, MAX_UINT_AMOUNT)); - - // Get index after actions - const userIndexAfter = await getUserIndex(stakedAaveV2, userAddress, underlyingAsset); - - const expectedAccruedRewards = getRewards( - userBalance, - userIndexAfter, - userIndexBefore - ).toString(); - const userAaveBalanceAfterAction = (await aaveToken.balanceOf(userAddress)).toString(); - - expect(userAaveBalanceAfterAction).to.be.equal( - userAaveBalance.add(userRewards).add(expectedAccruedRewards).toString() - ); - }); - - it('Verifies that the initial exchange rate is 1:1', async () => { - const currentExchangeRate = await stakeV3.exchangeRate(); - - expect(currentExchangeRate.toString()).to.be.equal(WAD); - }); - - it('Verifies that after a deposit the initial exchange rate is still 1:1', async () => { - const { - aaveToken, - users: [, staker], - } = testEnv; - const amount = ethers.utils.parseEther('50'); - - await aaveToken.connect(staker.signer).approve(stakeV3.address, amount); - await stakeV3.connect(staker.signer).stake(staker.address, amount); - - const currentExchangeRate = await stakeV3.exchangeRate(); - - expect(currentExchangeRate.toString()).to.be.equal(WAD); - }); - - it('Executes a slash of 20% of the asset', async () => { - const { aaveToken, users } = testEnv; - - const fundsReceiver = users[3].address; - - const userBalanceBeforeSlash = new BigNumber( - (await aaveToken.balanceOf(fundsReceiver)).toString() - ); - - const currentStakeBalance = new BigNumber( - (await aaveToken.balanceOf(stakeV3.address)).toString() - ); - - const amountToSlash = currentStakeBalance.times(0.2).toFixed(0); - - await stakeV3.connect(users[0].signer).slash(fundsReceiver, amountToSlash); - - const newStakeBalance = new BigNumber((await aaveToken.balanceOf(stakeV3.address)).toString()); - - const userBalanceAfterSlash = new BigNumber( - (await aaveToken.balanceOf(fundsReceiver)).toString() - ); - - const exchangeRate = new BigNumber((await stakeV3.exchangeRate()).toString()).toString(); - - expect(newStakeBalance.toString()).to.be.equal( - currentStakeBalance.minus(amountToSlash).toFixed(0) - ); - expect(userBalanceAfterSlash.toString()).to.be.equal( - userBalanceBeforeSlash.plus(amountToSlash).toFixed(0) - ); - expect(exchangeRate).to.be.equal(ethers.utils.parseEther('0.8')); - }); - - it('Redeems 1 stkAAVE after slashing - expected to receive 0.8 AAVE', async () => { - const { - aaveToken, - users: [, staker], - } = testEnv; - - const userBalanceBeforeRedeem = new BigNumber( - (await aaveToken.balanceOf(staker.address)).toString() - ); - const exchangeRateBeforeRedeem = new BigNumber((await stakeV3.exchangeRate()).toString()); - - const amountToRedeem = ethers.utils.parseEther('1').toString(); - - //activates cooldown - await stakeV3.connect(staker.signer).cooldown(); - - //moves forward to enter the unstake window - const cooldownActivationTimestamp = await timeLatest(); - - await advanceBlock( - cooldownActivationTimestamp.plus(new BigNumber(COOLDOWN_SECONDS).plus(1000)).toNumber() - ); - //redeem - await stakeV3.connect(staker.signer).redeem(staker.address, amountToRedeem); - - const userBalanceAfterRedeem = new BigNumber( - (await aaveToken.balanceOf(staker.address)).toString() - ); - const exchangeRateAfterRedeem = new BigNumber((await stakeV3.exchangeRate()).toString()); - - const expectedUserBalanceAfterRedeem = userBalanceBeforeRedeem.plus( - exchangeRateBeforeRedeem.times(amountToRedeem).div(10 ** 18) - ); - - expect(userBalanceAfterRedeem.toString()).to.be.equal( - expectedUserBalanceAfterRedeem.toString(), - 'Invalid user balance after redeem' - ); - - expect(exchangeRateAfterRedeem.toString()).to.be.equal( - exchangeRateBeforeRedeem.toString(), - 'Invalid exchange rate after redeem' - ); - }); - - it('Stakes 1 AAVE more - expected to receive 1.25 stkAAVE', async () => { - const { - aaveToken, - users: [, staker], - } = testEnv; - - const userBalanceBeforeStake = new BigNumber( - (await stakeV3.balanceOf(staker.address)).toString() - ); - const exchangeRateBeforeStake = new BigNumber((await stakeV3.exchangeRate()).toString()); - - const amountToStake = new BigNumber(ethers.utils.parseEther('1').toString()); - - //stake - await aaveToken.connect(staker.signer).approve(stakeV3.address, amountToStake.toString()); - await stakeV3.connect(staker.signer).stake(staker.address, amountToStake.toString()); - - const userBalanceAfterStake = new BigNumber( - (await stakeV3.balanceOf(staker.address)).toString() - ); - const exchangeRateAfterStake = new BigNumber((await stakeV3.exchangeRate()).toString()); - - const expectedUserBalanceAfterStake = userBalanceBeforeStake.plus( - amountToStake - .times(10 ** 18) - .div(exchangeRateBeforeStake) - .toFixed(0) - ); - - expect(userBalanceAfterStake.toString()).to.be.equal( - expectedUserBalanceAfterStake.toString(), - 'Invalid user balance after stake' - ); - - expect(exchangeRateAfterStake.toString()).to.be.equal( - exchangeRateBeforeStake.toString(), - 'Invalid exchange rate after stake' - ); - }); - - it('Tries to slash with an account that is not the slashing admin', async () => { - const { users } = testEnv; - - await expect(stakeV3.slash(users[2].address, '1')).to.be.revertedWith( - 'CALLER_NOT_SLASHING_ADMIN' - ); - }); - - it('Tries to pause the cooldown with an account that is not the cooldown admin', async () => { - const { users } = testEnv; - - await expect(stakeV3.connect(users[3].signer).setCooldownPause(true)).to.be.revertedWith( - 'CALLER_NOT_COOLDOWN_ADMIN' - ); - }); - - it('Tries to change the slash admin not being the slash admin', async () => { - const { users } = testEnv; - - await expect(stakeV3.setPendingAdmin(SLASHING_ADMIN, users[2].address)).to.be.revertedWith( - 'CALLER_NOT_ROLE_ADMIN' - ); - }); - - it('Tries to change the cooldown admin not being the cooldown admin', async () => { - const { users } = testEnv; - - await expect( - stakeV3.connect(users[3].signer).setPendingAdmin(COOLDOWN_ADMIN, users[3].address) - ).to.be.revertedWith('CALLER_NOT_ROLE_ADMIN'); - }); - - it('Changes the pending slashing admin', async () => { - const { users } = testEnv; - - await stakeV3.connect(users[0].signer).setPendingAdmin(SLASHING_ADMIN, users[3].address); - - const newPendingAdmin = await stakeV3.getPendingAdmin(SLASHING_ADMIN); - - expect(newPendingAdmin).to.be.equal(users[3].address); - }); - - it('Tries to claim the pending slashing admin not being the pending admin', async () => { - const { users } = testEnv; - - await expect( - stakeV3.connect(users[0].signer).claimRoleAdmin(SLASHING_ADMIN) - ).to.be.revertedWith('CALLER_NOT_PENDING_ROLE_ADMIN'); - }); - - it('Claim the slashing admin role', async () => { - const { users } = testEnv; - - await stakeV3.connect(users[3].signer).claimRoleAdmin(SLASHING_ADMIN); - - const newAdmin = await stakeV3.getAdmin(SLASHING_ADMIN); - - expect(newAdmin).to.be.equal(users[3].address); - }); - - it('Changes the cooldown pending admin', async () => { - const { users } = testEnv; - - await stakeV3.connect(users[1].signer).setPendingAdmin(COOLDOWN_ADMIN, users[3].address); - - const newPendingAdmin = await stakeV3.getPendingAdmin(COOLDOWN_ADMIN); - - expect(newPendingAdmin).to.be.equal(users[3].address); - }); - - it('Tries to claim the pending cooldown admin not being the pending admin', async () => { - const { users } = testEnv; - - await expect( - stakeV3.connect(users[0].signer).claimRoleAdmin(COOLDOWN_ADMIN) - ).to.be.revertedWith('CALLER_NOT_PENDING_ROLE_ADMIN'); - }); - - it('Claim the cooldown admin role', async () => { - const { users } = testEnv; - - await stakeV3.connect(users[3].signer).claimRoleAdmin(COOLDOWN_ADMIN); - - const newAdmin = await stakeV3.getAdmin(COOLDOWN_ADMIN); - - expect(newAdmin).to.be.equal(users[3].address); - }); - - - it('Pauses the cooldown', async () => { - const { users } = testEnv; - - await stakeV3.connect(users[3].signer).setCooldownPause(true); - - const cooldownPaused = await stakeV3.getCooldownPaused(); - - expect(cooldownPaused).to.be.equal(true); - }); - - it('Checks that users cannot redeem even during the unstake window', async () => { - const { - users: [, staker], - } = testEnv; - - //activates cooldown - await stakeV3.connect(staker.signer).cooldown(); - - //moves forward to enter the unstake window - const cooldownActivationTimestamp = await timeLatest(); - - await advanceBlock( - cooldownActivationTimestamp.plus(new BigNumber(COOLDOWN_SECONDS).plus(1000)).toNumber() - ); - - await expect(stakeV3.redeem(staker.address, '1000')).to.be.revertedWith( - 'INSUFFICIENT_COOLDOWN' - ); - }); - - it('Checks that initialize cannot be called', async () => { - const { - users: [, staker], - } = testEnv; - - await expect(stakeV3['initialize()']()).to.be.revertedWith('DEPRECATED'); - }); - - it('Sets the slashing percentage to 30%', async () => { - const { users } = testEnv; - - await stakeV3.connect(users[3].signer).setMaxSlashablePercentage('3000'); - - const currentSlashingPercentage = await stakeV3.getMaxSlashablePercentage(); - - expect(currentSlashingPercentage.toString()).to.be.equal('3000'); - }); - - it('Sets an invalid slashing percentage', async () => { - const { users } = testEnv; - - await expect( - stakeV3.connect(users[3].signer).setMaxSlashablePercentage('20000') - ).to.be.revertedWith('INVALID_SLASHING_PERCENTAGE'); - }); - - it('Tried to slash for a percentage bigger than the max percentage', async () => { - const { aaveToken, users } = testEnv; - - const fundsReceiver = users[3].address; - - const userBalanceBeforeSlash = new BigNumber( - (await aaveToken.balanceOf(fundsReceiver)).toString() - ); - - const currentStakeBalance = new BigNumber( - (await aaveToken.balanceOf(stakeV3.address)).toString() - ); - - const amountToSlash = currentStakeBalance.times(0.4).toFixed(0); - - await expect( - stakeV3.connect(users[3].signer).slash(fundsReceiver, amountToSlash) - ).to.be.revertedWith('INVALID_SLASHING_AMOUNT'); - }); - - it('Reverts trying to redeem 0 amount', async () => { - const { - users: [, staker], - } = testEnv; - - const amount = '0'; - - await expect(stakeV3.connect(staker.signer).redeem(staker.address, amount)).to.be.revertedWith( - 'INVALID_ZERO_AMOUNT' - ); - }); -}); diff --git a/test/StakedAaveV3/stakedAave-V3.spec.ts b/test/StakedAaveV3/stakedAave-V3.spec.ts new file mode 100644 index 0000000..023d4da --- /dev/null +++ b/test/StakedAaveV3/stakedAave-V3.spec.ts @@ -0,0 +1,1465 @@ +import { makeSuite, TestEnv } from '../helpers/make-suite'; +import { COOLDOWN_SECONDS, UNSTAKE_WINDOW, MAX_UINT_AMOUNT, 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 { 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'; + +const { expect } = require('chai'); + +const SLASHING_ADMIN = 0; +const COOLDOWN_ADMIN = 1; +const CLAIM_HELPER_ROLE = 2; + +makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { + let stakeV3: StakedAaveV3; + let snap: string; + + it('Deploys StakedAaveV3', async () => { + const { aaveToken, users } = testEnv; + + const [deployer, rewardsVault] = await getEthersSigners(); + + const rewardsVaultAddress = (await rewardsVault.getAddress()).toString(); + const emissionManager = await deployer.getAddress(); + + stakeV3 = await deployStakedAaveV3([ + aaveToken.address, + aaveToken.address, + COOLDOWN_SECONDS, + UNSTAKE_WINDOW, + rewardsVaultAddress, + emissionManager, + (1000 * 60 * 60).toString(), + ]); + + await aaveToken.connect(rewardsVault).approve(stakeV3.address, MAX_UINT_AMOUNT); + + //initialize the stake instance + + await stakeV3['initialize(address,address,address,uint256,string,string,uint8)']( + users[0].address, + users[1].address, + users[2].address, + '2000', + 'Staked AAVE', + 'stkAAVE', + 18 + ); + + const slashingAdmin = await stakeV3.getAdmin(SLASHING_ADMIN); //slash admin + const cooldownAdmin = await stakeV3.getAdmin(COOLDOWN_ADMIN); //cooldown admin + const claimAdmin = await stakeV3.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(users[2].address); + }); + + it('Reverts trying to stake 0 amount', async () => { + const { + users: [, staker], + } = testEnv; + const amount = '0'; + + await expect(stakeV3.connect(staker.signer).stake(staker.address, amount)).to.be.revertedWith( + 'INVALID_ZERO_AMOUNT' + ); + }); + + it('User 1 stakes 10 AAVE: receives 10 stkAAVE, StakedAave balance of AAVE is 10 and his rewards to claim are 0', async () => { + const { + aaveToken, + users: [, staker], + } = testEnv; + const amount = ethers.utils.parseEther('10'); + + const saveBalanceBefore = new BigNumber((await stakeV3.balanceOf(staker.address)).toString()); + + // Prepare actions for the test case + const actions = () => [ + aaveToken.connect(staker.signer).approve(stakeV3.address, amount), + stakeV3.connect(staker.signer).stake(staker.address, amount), + ]; + + // Check rewards + await compareRewardsAtAction(stakeV3, staker.address, actions); + + // Stake token tests + expect((await stakeV3.balanceOf(staker.address)).toString()).to.be.equal( + saveBalanceBefore.plus(amount.toString()).toString() + ); + expect((await aaveToken.balanceOf(stakeV3.address)).toString()).to.be.equal( + saveBalanceBefore.plus(amount.toString()).toString() + ); + expect((await stakeV3.balanceOf(staker.address)).toString()).to.be.equal(amount); + expect((await aaveToken.balanceOf(stakeV3.address)).toString()).to.be.equal(amount); + }); + + it('User 1 stakes 10 AAVE more: his total SAAVE balance increases, StakedAave balance of Aave increases and his reward until now get accumulated', async () => { + const { + aaveToken, + users: [, staker], + } = testEnv; + const amount = ethers.utils.parseEther('10'); + + const saveBalanceBefore = new BigNumber((await stakeV3.balanceOf(staker.address)).toString()); + const actions = () => [ + aaveToken.connect(staker.signer).approve(stakeV3.address, amount), + stakeV3.connect(staker.signer).stake(staker.address, amount), + ]; + + // Checks rewards + await compareRewardsAtAction(stakeV3, staker.address, actions, true); + + // Extra test checks + expect((await stakeV3.balanceOf(staker.address)).toString()).to.be.equal( + saveBalanceBefore.plus(amount.toString()).toString() + ); + expect((await aaveToken.balanceOf(stakeV3.address)).toString()).to.be.equal( + saveBalanceBefore.plus(amount.toString()).toString() + ); + }); + + it('User 1 claim half rewards', async () => { + const { + aaveToken, + users: [, staker], + } = testEnv; + // Increase time for bigger rewards + await increaseTimeAndMine(1000); + + const halfRewards = (await stakeV3.stakerRewardsToClaim(staker.address)).div(2); + const saveUserBalance = await aaveToken.balanceOf(staker.address); + + await stakeV3.connect(staker.signer).claimRewards(staker.address, halfRewards); + + const userBalanceAfterActions = await aaveToken.balanceOf(staker.address); + expect(userBalanceAfterActions.eq(saveUserBalance.add(halfRewards))).to.be.ok; + }); + + it('User 1 tries to claim higher reward than current rewards balance', async () => { + const { + aaveToken, + users: [, staker], + } = testEnv; + + const saveUserBalance = await aaveToken.balanceOf(staker.address); + + // Try to claim more amount than accumulated + await expect( + stakeV3.connect(staker.signer).claimRewards(staker.address, ethers.utils.parseEther('10000')) + ).to.be.revertedWith('INVALID_AMOUNT'); + + const userBalanceAfterActions = await aaveToken.balanceOf(staker.address); + expect(userBalanceAfterActions.eq(saveUserBalance)).to.be.ok; + }); + + it('User 1 claim all rewards', async () => { + const { + stakedAaveV2, + aaveToken, + users: [, staker], + } = testEnv; + + const userAddress = staker.address; + const underlyingAsset = stakedAaveV2.address; + + const userBalance = await stakedAaveV2.balanceOf(userAddress); + const userAaveBalance = await aaveToken.balanceOf(userAddress); + const userRewards = await stakedAaveV2.stakerRewardsToClaim(userAddress); + // Get index before actions + const userIndexBefore = await getUserIndex(stakedAaveV2, userAddress, underlyingAsset); + + // Claim rewards + await expect(stakedAaveV2.connect(staker.signer).claimRewards(staker.address, MAX_UINT_AMOUNT)); + + // Get index after actions + const userIndexAfter = await getUserIndex(stakedAaveV2, userAddress, underlyingAsset); + + const expectedAccruedRewards = getRewards( + userBalance, + userIndexAfter, + userIndexBefore + ).toString(); + const userAaveBalanceAfterAction = (await aaveToken.balanceOf(userAddress)).toString(); + + expect(userAaveBalanceAfterAction).to.be.equal( + userAaveBalance.add(userRewards).add(expectedAccruedRewards).toString() + ); + }); + + it('Verifies that the initial exchange rate is 1:1', async () => { + const currentExchangeRate = await stakeV3.exchangeRate(); + + expect(currentExchangeRate.toString()).to.be.equal(WAD); + }); + + it('Verifies that after a deposit the initial exchange rate is still 1:1', async () => { + const { + aaveToken, + users: [, staker], + } = testEnv; + const amount = ethers.utils.parseEther('50'); + + await aaveToken.connect(staker.signer).approve(stakeV3.address, amount); + await stakeV3.connect(staker.signer).stake(staker.address, amount); + + const currentExchangeRate = await stakeV3.exchangeRate(); + + expect(currentExchangeRate.toString()).to.be.equal(WAD); + }); + + it('Executes a slash of 20% of the asset', async () => { + const { aaveToken, users } = testEnv; + + const fundsReceiver = users[3].address; + + const userBalanceBeforeSlash = new BigNumber( + (await aaveToken.balanceOf(fundsReceiver)).toString() + ); + + const currentStakeBalance = new BigNumber( + (await aaveToken.balanceOf(stakeV3.address)).toString() + ); + + const amountToSlash = currentStakeBalance.times(0.2).toFixed(0); + + await stakeV3.connect(users[0].signer).slash(fundsReceiver, amountToSlash); + + const newStakeBalance = new BigNumber((await aaveToken.balanceOf(stakeV3.address)).toString()); + + const userBalanceAfterSlash = new BigNumber( + (await aaveToken.balanceOf(fundsReceiver)).toString() + ); + + const exchangeRate = new BigNumber((await stakeV3.exchangeRate()).toString()).toString(); + + expect(newStakeBalance.toString()).to.be.equal( + currentStakeBalance.minus(amountToSlash).toFixed(0) + ); + expect(userBalanceAfterSlash.toString()).to.be.equal( + userBalanceBeforeSlash.plus(amountToSlash).toFixed(0) + ); + expect(exchangeRate).to.be.equal(ethers.utils.parseEther('0.8')); + }); + + it('Redeems 1 stkAAVE after slashing - expected to receive 0.8 AAVE', async () => { + const { + aaveToken, + users: [, staker], + } = testEnv; + + const userBalanceBeforeRedeem = new BigNumber( + (await aaveToken.balanceOf(staker.address)).toString() + ); + const exchangeRateBeforeRedeem = new BigNumber((await stakeV3.exchangeRate()).toString()); + + const amountToRedeem = ethers.utils.parseEther('1').toString(); + + //activates cooldown + await stakeV3.connect(staker.signer).cooldown(); + + //moves forward to enter the unstake window + const cooldownActivationTimestamp = await timeLatest(); + + await advanceBlock( + cooldownActivationTimestamp.plus(new BigNumber(COOLDOWN_SECONDS).plus(1000)).toNumber() + ); + //redeem + await stakeV3.connect(staker.signer).redeem(staker.address, amountToRedeem); + + const userBalanceAfterRedeem = new BigNumber( + (await aaveToken.balanceOf(staker.address)).toString() + ); + const exchangeRateAfterRedeem = new BigNumber((await stakeV3.exchangeRate()).toString()); + + const expectedUserBalanceAfterRedeem = userBalanceBeforeRedeem.plus( + exchangeRateBeforeRedeem.times(amountToRedeem).div(10 ** 18) + ); + + expect(userBalanceAfterRedeem.toString()).to.be.equal( + expectedUserBalanceAfterRedeem.toString(), + 'Invalid user balance after redeem' + ); + + expect(exchangeRateAfterRedeem.toString()).to.be.equal( + exchangeRateBeforeRedeem.toString(), + 'Invalid exchange rate after redeem' + ); + }); + + it('Stakes 1 AAVE more - expected to receive 1.25 stkAAVE', async () => { + const { + aaveToken, + users: [, staker], + } = testEnv; + + const userBalanceBeforeStake = new BigNumber( + (await stakeV3.balanceOf(staker.address)).toString() + ); + const exchangeRateBeforeStake = new BigNumber((await stakeV3.exchangeRate()).toString()); + + const amountToStake = new BigNumber(ethers.utils.parseEther('1').toString()); + + //stake + await aaveToken.connect(staker.signer).approve(stakeV3.address, amountToStake.toString()); + await stakeV3.connect(staker.signer).stake(staker.address, amountToStake.toString()); + + const userBalanceAfterStake = new BigNumber( + (await stakeV3.balanceOf(staker.address)).toString() + ); + const exchangeRateAfterStake = new BigNumber((await stakeV3.exchangeRate()).toString()); + + const expectedUserBalanceAfterStake = userBalanceBeforeStake.plus( + amountToStake + .times(10 ** 18) + .div(exchangeRateBeforeStake) + .toFixed(0) + ); + + expect(userBalanceAfterStake.toString()).to.be.equal( + expectedUserBalanceAfterStake.toString(), + 'Invalid user balance after stake' + ); + + expect(exchangeRateAfterStake.toString()).to.be.equal( + exchangeRateBeforeStake.toString(), + 'Invalid exchange rate after stake' + ); + }); + + it('Tries to slash with an account that is not the slashing admin', async () => { + const { users } = testEnv; + + await expect(stakeV3.slash(users[2].address, '1')).to.be.revertedWith( + 'CALLER_NOT_SLASHING_ADMIN' + ); + }); + + it('Tries to pause the cooldown with an account that is not the cooldown admin', async () => { + const { users } = testEnv; + + await expect(stakeV3.connect(users[3].signer).setCooldownPause(true)).to.be.revertedWith( + 'CALLER_NOT_COOLDOWN_ADMIN' + ); + }); + + it('Tries to change the slash admin not being the slash admin', async () => { + const { users } = testEnv; + + await expect(stakeV3.setPendingAdmin(SLASHING_ADMIN, users[2].address)).to.be.revertedWith( + 'CALLER_NOT_ROLE_ADMIN' + ); + }); + + it('Tries to change the cooldown admin not being the cooldown admin', async () => { + const { users } = testEnv; + + await expect( + stakeV3.connect(users[3].signer).setPendingAdmin(COOLDOWN_ADMIN, users[3].address) + ).to.be.revertedWith('CALLER_NOT_ROLE_ADMIN'); + }); + + it('Changes the pending slashing admin', async () => { + const { users } = testEnv; + + await stakeV3.connect(users[0].signer).setPendingAdmin(SLASHING_ADMIN, users[3].address); + + const newPendingAdmin = await stakeV3.getPendingAdmin(SLASHING_ADMIN); + + expect(newPendingAdmin).to.be.equal(users[3].address); + }); + + it('Tries to claim the pending slashing admin not being the pending admin', async () => { + const { users } = testEnv; + + await expect( + stakeV3.connect(users[0].signer).claimRoleAdmin(SLASHING_ADMIN) + ).to.be.revertedWith('CALLER_NOT_PENDING_ROLE_ADMIN'); + }); + + it('Claim the slashing admin role', async () => { + const { users } = testEnv; + + await stakeV3.connect(users[3].signer).claimRoleAdmin(SLASHING_ADMIN); + + const newAdmin = await stakeV3.getAdmin(SLASHING_ADMIN); + + expect(newAdmin).to.be.equal(users[3].address); + }); + + it('Changes the cooldown pending admin', async () => { + const { users } = testEnv; + + await stakeV3.connect(users[1].signer).setPendingAdmin(COOLDOWN_ADMIN, users[3].address); + + const newPendingAdmin = await stakeV3.getPendingAdmin(COOLDOWN_ADMIN); + + expect(newPendingAdmin).to.be.equal(users[3].address); + }); + + it('Tries to claim the pending cooldown admin not being the pending admin', async () => { + const { users } = testEnv; + + await expect( + stakeV3.connect(users[0].signer).claimRoleAdmin(COOLDOWN_ADMIN) + ).to.be.revertedWith('CALLER_NOT_PENDING_ROLE_ADMIN'); + }); + + it('Claim the cooldown admin role', async () => { + const { users } = testEnv; + + await stakeV3.connect(users[3].signer).claimRoleAdmin(COOLDOWN_ADMIN); + + const newAdmin = await stakeV3.getAdmin(COOLDOWN_ADMIN); + + expect(newAdmin).to.be.equal(users[3].address); + }); + + it('Pauses the cooldown', async () => { + const { users } = testEnv; + + await stakeV3.connect(users[3].signer).setCooldownPause(true); + + const cooldownPaused = await stakeV3.getCooldownPaused(); + + expect(cooldownPaused).to.be.equal(true); + }); + + it('Checks that users cannot redeem even during the unstake window', async () => { + const { + users: [, staker], + } = testEnv; + + //activates cooldown + await stakeV3.connect(staker.signer).cooldown(); + + //moves forward to enter the unstake window + const cooldownActivationTimestamp = await timeLatest(); + + await advanceBlock( + cooldownActivationTimestamp.plus(new BigNumber(COOLDOWN_SECONDS).plus(1000)).toNumber() + ); + + await expect(stakeV3.redeem(staker.address, '1000')).to.be.revertedWith( + 'INSUFFICIENT_COOLDOWN' + ); + }); + + it('Checks that initialize cannot be called', async () => { + const { + users: [, staker], + } = testEnv; + + await expect(stakeV3['initialize()']()).to.be.revertedWith('DEPRECATED'); + }); + + it('Sets the slashing percentage to 30%', async () => { + const { users } = testEnv; + + await stakeV3.connect(users[3].signer).setMaxSlashablePercentage('3000'); + + const currentSlashingPercentage = await stakeV3.getMaxSlashablePercentage(); + + expect(currentSlashingPercentage.toString()).to.be.equal('3000'); + }); + + it('Sets an invalid slashing percentage', async () => { + const { users } = testEnv; + + await expect( + stakeV3.connect(users[3].signer).setMaxSlashablePercentage('20000') + ).to.be.revertedWith('INVALID_SLASHING_PERCENTAGE'); + }); + + it('Tried to slash for a percentage bigger than the max percentage', async () => { + const { aaveToken, users } = testEnv; + + const fundsReceiver = users[3].address; + + const userBalanceBeforeSlash = new BigNumber( + (await aaveToken.balanceOf(fundsReceiver)).toString() + ); + + const currentStakeBalance = new BigNumber( + (await aaveToken.balanceOf(stakeV3.address)).toString() + ); + + const amountToSlash = currentStakeBalance.times(0.4).toFixed(0); + + await expect( + stakeV3.connect(users[3].signer).slash(fundsReceiver, amountToSlash) + ).to.be.revertedWith('INVALID_SLASHING_AMOUNT'); + }); + + it('Reverts trying to redeem 0 amount', async () => { + const { + users: [, staker], + } = testEnv; + + const amount = '0'; + + await expect(stakeV3.connect(staker.signer).redeem(staker.address, amount)).to.be.revertedWith( + 'INVALID_ZERO_AMOUNT' + ); + }); + + it('Stakes using permit', async () => { + const { + aaveToken, + users: [, staker, someone], + } = testEnv; + + const { chainId } = await DRE.ethers.provider.getNetwork(); + if (!chainId) { + fail("Current network doesn't have CHAIN ID"); + } + + console.log('Staker address is ', staker.address); + + const expiration = 0; + + const nonce = (await stakeV3._nonces(staker.address)).toNumber(); + + const amount = parseEther('0.1'); + + const msgParams = buildPermitParams( + chainId, + stakeV3.address, + staker.address, + stakeV3.address, + nonce, + amount.toString(), + expiration.toFixed() + ); + + // reset approval + waitForTx(await aaveToken.connect(staker.signer).approve(stakeV3.address, 0)); + + const stakerPrivateKey = require('../../test-wallets').accounts[0].secretKey; + if (!stakerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const { v, r, s } = getSignatureFromTypedData(stakerPrivateKey, msgParams); + + const balanceBefore = await stakeV3.balanceOf(staker.address); + const exchangeRate = await stakeV3.exchangeRate(); + const ether = parseEther('1.0'); + + const aaveStakedBefore = await aaveToken.balanceOf(stakeV3.address); + + waitForTx( + await stakeV3 + .connect(someone.signer) + .stakeWithPermit(staker.address, staker.address, amount, expiration, v, r, s) + ); + + const aaveStakedAfter = await aaveToken.balanceOf(stakeV3.address); + + expect(await stakeV3.balanceOf(staker.address)).to.be.eql( + balanceBefore.add(amount.mul(ether).div(exchangeRate)) + ); + + expect(aaveStakedAfter).to.be.eql( + aaveStakedBefore.add(amount) + ); + + }); + it('Fails claim rewards for someone using claimRewardsOnBehalf if not helper', async () => { + const { + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + // Increase time for bigger rewards + await increaseTimeAndMine(1000); + + const halfRewards = (await stakeV3.stakerRewardsToClaim(staker.address)).div(2); + const saveUserBalance = await aaveToken.balanceOf(someone.address); + + await expect( + stakeV3 + .connect(staker.signer) + .claimRewardsOnBehalf(staker.address, someone.address, halfRewards) + ).to.be.revertedWith('CALLER_NOT_CLAIM_HELPER'); + const userBalanceAfterActions = await aaveToken.balanceOf(someone.address); + expect(userBalanceAfterActions.eq(saveUserBalance)).to.be.ok; + }); + it('Helper claim half rewards for staker to someone using claimRewardsOnBehalf', async () => { + const { + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + // Increase time for bigger rewards + await increaseTimeAndMine(1000); + + const halfRewards = (await stakeV3.stakerRewardsToClaim(staker.address)).div(2); + console.log(halfRewards.toString()); + const saveUserBalance = await aaveToken.balanceOf(someone.address); + + await stakeV3 + .connect(helper.signer) + .claimRewardsOnBehalf(staker.address, someone.address, halfRewards); + + const userBalanceAfterActions = await aaveToken.balanceOf(someone.address); + expect(userBalanceAfterActions.eq(saveUserBalance.add(halfRewards))).to.be.ok; + }); + it('Helper tries to claim higher reward than current rewards balance', async () => { + const { + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + + const saveUserBalance = await aaveToken.balanceOf(someone.address); + + // Try to claim more amount than accumulated + await expect( + stakeV3 + .connect(helper.signer) + .claimRewardsOnBehalf(staker.address, someone.address, ethers.utils.parseEther('10000')) + ).to.be.revertedWith('INVALID_AMOUNT'); + + const userBalanceAfterActions = await aaveToken.balanceOf(someone.address); + expect(userBalanceAfterActions.eq(saveUserBalance)).to.be.ok; + }); + it('Helper 1 claim all for staker to someone', async () => { + const { + stakedAaveV2, + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + + const userAddress = staker.address; + const userBalance = await stakeV3.balanceOf(userAddress); + const userAaveBalance = await aaveToken.balanceOf(someone.address); + const userRewards = await stakeV3.stakerRewardsToClaim(userAddress); + // // Get index before actions + const userIndexBefore = await getUserIndex(stakeV3, userAddress, stakeV3.address); + + await waitForTx( + await stakeV3 + .connect(helper.signer) + .claimRewardsOnBehalf(staker.address, someone.address, MAX_UINT_AMOUNT) + ); + // Get index after actions + const userIndexAfter = await getUserIndex(stakeV3, userAddress, stakeV3.address); + + const expectedAccruedRewards = getRewards( + userBalance, + userIndexAfter, + userIndexBefore + ).toString(); + // Claim rewards + + const userAaveBalanceAfterAction = (await aaveToken.balanceOf(someone.address)).toString(); + + expect(userAaveBalanceAfterAction).to.be.equal( + userAaveBalance.add(userRewards).add(expectedAccruedRewards) + ); + }); + it('Stakes a bit more', async () => { + const { + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + const amount = parseEther('0.1'); + const balanceBefore = await stakeV3.balanceOf(staker.address); + waitForTx(await aaveToken.connect(staker.signer).approve(stakeV3.address, MAX_UINT_AMOUNT)); + waitForTx(await stakeV3.connect(staker.signer).stake(staker.address, amount)); + }); + + it('Claim & stake half rewards', async () => { + const { + aaveToken, + users: [, staker], + } = testEnv; + const ether = parseEther('1.0'); + // Increase time for bigger rewards + await increaseTimeAndMine(1000); + + const halfRewards = (await stakeV3.stakerRewardsToClaim(staker.address)).div(2); + + const saveUserBalance = [ + await aaveToken.balanceOf(staker.address), + await stakeV3.balanceOf(staker.address), + ]; + const currentExchangeRate = await stakeV3.exchangeRate(); + + const aaveStakedBefore = await aaveToken.balanceOf(stakeV3.address); + + await stakeV3.connect(staker.signer).claimRewardsAndStake(staker.address, halfRewards); + + const aaveStakedAfter = await aaveToken.balanceOf(stakeV3.address); + + const userBalanceAfterActions = [ + await aaveToken.balanceOf(staker.address), + await stakeV3.balanceOf(staker.address), + ]; + + + expect(userBalanceAfterActions[0]).to.be.eq(saveUserBalance[0], "Invalid aave user balance after action"); + + expect( + userBalanceAfterActions[1] + ).to.be.eq( + saveUserBalance[1].add(halfRewards.mul(ether).div(currentExchangeRate)) + ,"invalid stkAAVE user balance after action") + + expect(aaveStakedAfter).to.be.equal(aaveStakedBefore.add(halfRewards), "Invalid underlying balance"); + + }); + it('Claim & stake higher reward than current rewards balance', async () => { + const { + aaveToken, + users: [, staker], + } = testEnv; + + const saveUserBalance = await aaveToken.balanceOf(staker.address); + + // Try to claim more amount than accumulated + await expect( + stakeV3 + .connect(staker.signer) + .claimRewardsAndStake(staker.address, ethers.utils.parseEther('10000')) + ).to.be.revertedWith('INVALID_AMOUNT'); + + const userBalanceAfterActions = await aaveToken.balanceOf(staker.address); + expect(userBalanceAfterActions.eq(saveUserBalance)).to.be.ok; + }); + + it('Claim & stake all', async () => { + const { + stakedAaveV2, + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + + const ether = parseEther('1.0'); + + const userAddress = staker.address; + const userBalance = await stakeV3.balanceOf(userAddress); + const saveUserBalance = [ + await aaveToken.balanceOf(staker.address), + await stakeV3.balanceOf(staker.address), + ]; + const userRewards = await stakeV3.stakerRewardsToClaim(userAddress); + // // Get index before actions + const userIndexBefore = await getUserIndex(stakeV3, userAddress, stakeV3.address); + const currentExchangeRate = await stakeV3.exchangeRate(); + + await waitForTx( + await stakeV3.connect(staker.signer).claimRewardsAndStake(staker.address, MAX_UINT_AMOUNT) + ); + // Get index after actions + const userIndexAfter = await getUserIndex(stakeV3, userAddress, stakeV3.address); + + const expectedAccruedRewards = getRewards(userBalance, userIndexAfter, userIndexBefore); + // Claim rewards + + const userBalanceAfterActions = [ + await aaveToken.balanceOf(staker.address), + await stakeV3.balanceOf(staker.address), + ]; + + expect(userBalanceAfterActions[0]).to.be.equal(saveUserBalance[0]); + expect(userBalanceAfterActions[1]).to.be.equal( + saveUserBalance[1].add( + expectedAccruedRewards.add(userRewards).mul(ether).div(currentExchangeRate) + ) + ); + }); + it('Stakes a bit more', async () => { + const { + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + const amount = parseEther('0.1'); + const balanceBefore = await stakeV3.balanceOf(staker.address); + waitForTx(await aaveToken.connect(staker.signer).approve(stakeV3.address, MAX_UINT_AMOUNT)); + waitForTx(await stakeV3.connect(staker.signer).stake(staker.address, amount)); + }); + it('Claim & stake half rewards to someone else', async () => { + const { + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + const ether = parseEther('1.0'); + // Increase time for bigger rewards + await increaseTimeAndMine(1000); + + const halfRewards = (await stakeV3.stakerRewardsToClaim(staker.address)).div(2); + const saveUserBalance = [ + await aaveToken.balanceOf(someone.address), + await stakeV3.balanceOf(someone.address), + ]; + const currentExchangeRate = await stakeV3.exchangeRate(); + + stakeV3.connect(staker.signer).claimRewardsAndStake(someone.address, halfRewards); + + const userBalanceAfterActions = [ + await aaveToken.balanceOf(someone.address), + await stakeV3.balanceOf(someone.address), + ]; + expect(userBalanceAfterActions[0].eq(saveUserBalance[0])).to.be.ok; + expect( + userBalanceAfterActions[1].eq( + saveUserBalance[1].add(halfRewards.mul(ether).div(currentExchangeRate)) + ) + ).to.be.ok; + }); + it('Claim & stake higher reward than current rewards balance to someone else', async () => { + const { + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + + const saveUserBalance = await aaveToken.balanceOf(someone.address); + + // Try to claim more amount than accumulated + await expect( + stakeV3 + .connect(staker.signer) + .claimRewardsAndStake(someone.address, ethers.utils.parseEther('10000')) + ).to.be.revertedWith('INVALID_AMOUNT'); + + const userBalanceAfterActions = await aaveToken.balanceOf(someone.address); + expect(userBalanceAfterActions.eq(saveUserBalance)).to.be.ok; + }); + + it('Claim & stake all to someone else', async () => { + const { + stakedAaveV2, + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + + const ether = parseEther('1.0'); + + const userAddress = staker.address; + const userBalance = await stakeV3.balanceOf(userAddress); + const saveUserBalance = [ + await aaveToken.balanceOf(someone.address), + await stakeV3.balanceOf(someone.address), + ]; + const userRewards = await stakeV3.stakerRewardsToClaim(userAddress); + // // Get index before actions + const userIndexBefore = await getUserIndex(stakeV3, userAddress, stakeV3.address); + const currentExchangeRate = await stakeV3.exchangeRate(); + + await waitForTx( + await stakeV3.connect(staker.signer).claimRewardsAndStake(someone.address, MAX_UINT_AMOUNT) + ); + // Get index after actions + const userIndexAfter = await getUserIndex(stakeV3, userAddress, stakeV3.address); + + const expectedAccruedRewards = getRewards(userBalance, userIndexAfter, userIndexBefore); + // Claim rewards + + const userBalanceAfterActions = [ + await aaveToken.balanceOf(someone.address), + await stakeV3.balanceOf(someone.address), + ]; + + expect(userBalanceAfterActions[0]).to.be.equal(saveUserBalance[0]); + expect(userBalanceAfterActions[1]).to.be.equal( + saveUserBalance[1].add( + expectedAccruedRewards.add(userRewards).mul(ether).div(currentExchangeRate) + ) + ); + }); + it('Stakes a bit more', async () => { + const { + aaveToken, + users: [, staker, helper], + } = testEnv; + const amount = parseEther('0.1'); + waitForTx(await aaveToken.connect(staker.signer).approve(stakeV3.address, MAX_UINT_AMOUNT)); + waitForTx(await stakeV3.connect(staker.signer).stake(staker.address, amount)); + }); + it('Fails claim rewards and stake by non helper for staker using claimRewardsOnBehalf', async () => { + const { + aaveToken, + users: [, staker, helper], + } = testEnv; + // Increase time for bigger rewards + await increaseTimeAndMine(1000); + + const halfRewards = (await stakeV3.stakerRewardsToClaim(staker.address)).div(2); + const saveUserBalance = await aaveToken.balanceOf(staker.address); + + await expect( + stakeV3 + .connect(staker.signer) + .claimRewardsAndStakeOnBehalf(staker.address, staker.address, halfRewards) + ).to.be.revertedWith('CALLER_NOT_CLAIM_HELPER'); + const userBalanceAfterActions = await aaveToken.balanceOf(staker.address); + expect(userBalanceAfterActions.eq(saveUserBalance)).to.be.ok; + }); + it('Helper claim & stake half rewards for staker using claimRewardsOnBehalf', async () => { + const { + aaveToken, + users: [, staker, helper], + } = testEnv; + const ether = parseEther('1.0'); + // Increase time for bigger rewards + await increaseTimeAndMine(1000); + + const halfRewards = (await stakeV3.stakerRewardsToClaim(staker.address)).div(2); + const saveUserBalance = [ + await aaveToken.balanceOf(staker.address), + await stakeV3.balanceOf(staker.address), + ]; + const currentExchangeRate = await stakeV3.exchangeRate(); + + stakeV3 + .connect(helper.signer) + .claimRewardsAndStakeOnBehalf(staker.address, staker.address, halfRewards); + + const userBalanceAfterActions = [ + await aaveToken.balanceOf(staker.address), + await stakeV3.balanceOf(staker.address), + ]; + expect(userBalanceAfterActions[0].eq(saveUserBalance[0])).to.be.ok; + expect( + userBalanceAfterActions[1].eq( + saveUserBalance[1].add(halfRewards.mul(ether).div(currentExchangeRate)) + ) + ).to.be.ok; + }); + it('Helper tries to claim & stake higher reward than current rewards balance', async () => { + const { + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + + const saveUserBalance = await aaveToken.balanceOf(staker.address); + + // Try to claim more amount than accumulated + await expect( + stakeV3 + .connect(helper.signer) + .claimRewardsAndStakeOnBehalf( + staker.address, + staker.address, + ethers.utils.parseEther('10000') + ) + ).to.be.revertedWith('INVALID_AMOUNT'); + + const userBalanceAfterActions = await aaveToken.balanceOf(staker.address); + expect(userBalanceAfterActions.eq(saveUserBalance)).to.be.ok; + }); + + it('Helper 1 claims & stakes all for staker', async () => { + const { + stakedAaveV2, + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + + const ether = parseEther('1.0'); + const userAddress = staker.address; + const userBalance = await stakeV3.balanceOf(userAddress); + const saveUserBalance = [ + await aaveToken.balanceOf(staker.address), + await stakeV3.balanceOf(staker.address), + ]; + const userRewards = await stakeV3.stakerRewardsToClaim(userAddress); + // // Get index before actions + const userIndexBefore = await getUserIndex(stakeV3, userAddress, stakeV3.address); + const currentExchangeRate = await stakeV3.exchangeRate(); + + await waitForTx( + await stakeV3 + .connect(helper.signer) + .claimRewardsAndStakeOnBehalf(staker.address, staker.address, MAX_UINT_AMOUNT) + ); + // Get index after actions + const userIndexAfter = await getUserIndex(stakeV3, userAddress, stakeV3.address); + + const expectedAccruedRewards = getRewards(userBalance, userIndexAfter, userIndexBefore); + // Claim rewards + + const userBalanceAfterActions = [ + await aaveToken.balanceOf(staker.address), + await stakeV3.balanceOf(staker.address), + ]; + + expect(userBalanceAfterActions[0]).to.be.equal(saveUserBalance[0]); + expect(userBalanceAfterActions[1]).to.be.equal( + saveUserBalance[1].add( + expectedAccruedRewards.add(userRewards).mul(ether).div(currentExchangeRate) + ) + ); + }); + it('Stakes a bit more', async () => { + const { + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + const amount = parseEther('0.1'); + const balanceBefore = await stakeV3.balanceOf(staker.address); + waitForTx(await aaveToken.connect(staker.signer).approve(stakeV3.address, MAX_UINT_AMOUNT)); + waitForTx(await stakeV3.connect(staker.signer).stake(staker.address, amount)); + }); + it('Fails to claim and reward by non helper from staker to someone using claimRewardsOnBehalf', async () => { + const { + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + // Increase time for bigger rewards + await increaseTimeAndMine(1000); + + const halfRewards = (await stakeV3.stakerRewardsToClaim(staker.address)).div(2); + const saveUserBalance = await aaveToken.balanceOf(someone.address); + + await expect( + stakeV3 + .connect(staker.signer) + .claimRewardsAndStakeOnBehalf(staker.address, someone.address, halfRewards) + ).to.be.revertedWith('CALLER_NOT_CLAIM_HELPER'); + const userBalanceAfterActions = await aaveToken.balanceOf(someone.address); + expect(userBalanceAfterActions.eq(saveUserBalance)).to.be.ok; + }); + + it('Helper claim & stake half rewards for staker to someone using claimRewardsOnBehalf', async () => { + const { + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + const ether = parseEther('1.0'); + // Increase time for bigger rewards + await increaseTimeAndMine(1000); + + const halfRewards = (await stakeV3.stakerRewardsToClaim(staker.address)).div(2); + const saveUserBalance = [ + await aaveToken.balanceOf(someone.address), + await stakeV3.balanceOf(someone.address), + ]; + const currentExchangeRate = await stakeV3.exchangeRate(); + + stakeV3 + .connect(helper.signer) + .claimRewardsAndStakeOnBehalf(staker.address, someone.address, halfRewards); + + const userBalanceAfterActions = [ + await aaveToken.balanceOf(someone.address), + await stakeV3.balanceOf(someone.address), + ]; + expect(userBalanceAfterActions[0].eq(saveUserBalance[0])).to.be.ok; + expect( + userBalanceAfterActions[1].eq( + saveUserBalance[1].add(halfRewards.mul(ether).div(currentExchangeRate)) + ) + ).to.be.ok; + }); + it('Helper tries to claim & stake higher reward than current rewards balance', async () => { + const { + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + + const saveUserBalance = await aaveToken.balanceOf(someone.address); + + // Try to claim more amount than accumulated + await expect( + stakeV3 + .connect(helper.signer) + .claimRewardsAndStakeOnBehalf( + staker.address, + someone.address, + ethers.utils.parseEther('10000') + ) + ).to.be.revertedWith('INVALID_AMOUNT'); + + const userBalanceAfterActions = await aaveToken.balanceOf(someone.address); + expect(userBalanceAfterActions.eq(saveUserBalance)).to.be.ok; + }); + + it('Helper 1 claim & staker all for staker to someone', async () => { + const { + stakedAaveV2, + aaveToken, + users: [, staker, helper, someone], + } = testEnv; + + const ether = parseEther('1.0'); + const userAddress = staker.address; + const userBalance = await stakeV3.balanceOf(userAddress); + const saveUserBalance = [ + await aaveToken.balanceOf(someone.address), + await stakeV3.balanceOf(someone.address), + ]; + const userRewards = await stakeV3.stakerRewardsToClaim(userAddress); + // // Get index before actions + const userIndexBefore = await getUserIndex(stakeV3, userAddress, stakeV3.address); + const currentExchangeRate = await stakeV3.exchangeRate(); + + await waitForTx( + await stakeV3 + .connect(helper.signer) + .claimRewardsAndStakeOnBehalf(staker.address, someone.address, MAX_UINT_AMOUNT) + ); + // Get index after actions + const userIndexAfter = await getUserIndex(stakeV3, userAddress, stakeV3.address); + + const expectedAccruedRewards = getRewards(userBalance, userIndexAfter, userIndexBefore); + // Claim rewards + + const userBalanceAfterActions = [ + await aaveToken.balanceOf(someone.address), + await stakeV3.balanceOf(someone.address), + ]; + + expect(userBalanceAfterActions[0]).to.be.equal(saveUserBalance[0]); + expect(userBalanceAfterActions[1]).to.be.equal( + saveUserBalance[1].add( + expectedAccruedRewards.add(userRewards).mul(ether).div(currentExchangeRate) + ) + ); + }); + + it('Stakes a bit more, prepare window and take snapshots', async () => { + const { + aaveToken, + users: [, , helper, admin, staker], + } = testEnv; + const amount = parseEther('10'); + const balanceBefore = await stakeV3.balanceOf(staker.address); + await stakeV3.connect(admin.signer).setCooldownPause(false); + + waitForTx(await aaveToken.connect(staker.signer).approve(stakeV3.address, MAX_UINT_AMOUNT)); + waitForTx(await stakeV3.connect(staker.signer).stake(staker.address, amount)); + waitForTx(await stakeV3.connect(staker.signer).stake(staker.address, amount)); + await stakeV3.connect(staker.signer).cooldown(); + await increaseTimeAndMine(new BigNumber(COOLDOWN_SECONDS).plus(1000).toNumber()); + snap = await evmSnapshot(); + }); + it('Fails to redeem on behalf by non helper', async () => { + const { + aaveToken, + users: [, , helper, someone, staker], + } = testEnv; + // Increase time for bigger rewards + await evmRevert(snap); + snap = await evmSnapshot(); + + const halfRedeem = (await stakeV3.balanceOf(staker.address)).div(2); + const saveUserBalance = await aaveToken.balanceOf(someone.address); + + await expect( + stakeV3.connect(staker.signer).redeemOnBehalf(staker.address, someone.address, halfRedeem) + ).to.be.revertedWith('CALLER_NOT_CLAIM_HELPER'); + const userBalanceAfterActions = await aaveToken.balanceOf(someone.address); + expect(userBalanceAfterActions.eq(saveUserBalance)).to.be.ok; + }); + it('Fails to claim and unstake by non helper from staker to someone using claimRewardsAndRedeemOnBehalf', async () => { + const { + aaveToken, + users: [, , helper, someone, staker], + } = testEnv; + // Increase time for bigger rewards + await evmRevert(snap); + snap = await evmSnapshot(); + + const halfRewards = (await stakeV3.stakerRewardsToClaim(staker.address)).div(2); + const halfRedeem = (await stakeV3.balanceOf(staker.address)).div(2); + const saveUserBalance = await aaveToken.balanceOf(someone.address); + + await expect( + stakeV3 + .connect(staker.signer) + .claimRewardsAndRedeemOnBehalf(staker.address, someone.address, halfRewards, halfRedeem) + ).to.be.revertedWith('CALLER_NOT_CLAIM_HELPER'); + const userBalanceAfterActions = await aaveToken.balanceOf(someone.address); + expect(userBalanceAfterActions.eq(saveUserBalance)).to.be.ok; + }); + it('Helper succeeds to redeem half on behalf of staker to someone using redeemOnBehalf', async () => { + const { + aaveToken, + users: [, , helper, someone, staker], + } = testEnv; + // Increase time for bigger rewards + await evmRevert(snap); + snap = await evmSnapshot(); + + const ether = parseEther('1.0'); + const halfRedeem = (await stakeV3.balanceOf(staker.address)).div(2); + const receiverAaveBalance = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalance = await stakeV3.balanceOf(staker.address); + const currentExchangeRate = await stakeV3.exchangeRate(); + + waitForTx( + await stakeV3 + .connect(helper.signer) + .redeemOnBehalf(staker.address, someone.address, halfRedeem) + ); + + const receiverAaveBalanceAfter = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalancerAfter = await stakeV3.balanceOf(staker.address); + expect(stakerStkAaveBalancerAfter.eq(stakerStkAaveBalance.sub(halfRedeem))).to.be.ok; + expect( + receiverAaveBalanceAfter.eq( + receiverAaveBalance.add(halfRedeem.mul(currentExchangeRate).div(ether)) + ) + ).to.be.ok; + }); + it('Staker claims half & unstake half to someone using claimRewardsAndRedeem', async () => { + const { + aaveToken, + users: [, , helper, someone, staker], + } = testEnv; + const ether = parseEther('1.0'); + await evmRevert(snap); + snap = await evmSnapshot(); + + const halfRewards = (await stakeV3.stakerRewardsToClaim(staker.address)).div(2); + const halfRedeem = (await stakeV3.balanceOf(staker.address)).div(2); + const receiverAaveBalance = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalance = await stakeV3.balanceOf(staker.address); + const currentExchangeRate = await stakeV3.exchangeRate(); + + waitForTx( + await stakeV3 + .connect(staker.signer) + .claimRewardsAndRedeem(someone.address, halfRewards, halfRedeem) + ); + + const receiverAaveBalanceAfter = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalancerAfter = await stakeV3.balanceOf(staker.address); + expect(stakerStkAaveBalancerAfter.eq(stakerStkAaveBalance.sub(halfRedeem))).to.be.ok; + expect( + receiverAaveBalanceAfter.eq( + receiverAaveBalance.add(halfRewards.add(halfRedeem.mul(currentExchangeRate).div(ether))) + ) + ).to.be.ok; + }); + it('Helper claim half & unstake half for staker to someone using claimRewardsAndRedeemOnBehalf', async () => { + const { + aaveToken, + users: [, , helper, someone, staker], + } = testEnv; + const ether = parseEther('1.0'); + await evmRevert(snap); + snap = await evmSnapshot(); + + const halfRewards = (await stakeV3.stakerRewardsToClaim(staker.address)).div(2); + const halfRedeem = (await stakeV3.balanceOf(staker.address)).div(2); + const receiverAaveBalance = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalance = await stakeV3.balanceOf(staker.address); + const currentExchangeRate = await stakeV3.exchangeRate(); + + waitForTx( + await stakeV3 + .connect(helper.signer) + .claimRewardsAndRedeemOnBehalf(staker.address, someone.address, halfRewards, halfRedeem) + ); + + const receiverAaveBalanceAfter = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalancerAfter = await stakeV3.balanceOf(staker.address); + expect(stakerStkAaveBalancerAfter.eq(stakerStkAaveBalance.sub(halfRedeem))).to.be.ok; + expect( + receiverAaveBalanceAfter.eq( + receiverAaveBalance.add(halfRewards.add(halfRedeem.mul(currentExchangeRate).div(ether))) + ) + ).to.be.ok; + }); + it('Staker claim half & unstake full to someone using claimRewardsAndRedeem', async () => { + const { + aaveToken, + users: [, , helper, someone, staker], + } = testEnv; + const ether = parseEther('1.0'); + await evmRevert(snap); + snap = await evmSnapshot(); + + const halfRewards = (await stakeV3.stakerRewardsToClaim(staker.address)).div(2); + const receiverAaveBalance = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalance = await stakeV3.balanceOf(staker.address); + const currentExchangeRate = await stakeV3.exchangeRate(); + + waitForTx( + await stakeV3 + .connect(staker.signer) + .claimRewardsAndRedeem(someone.address, halfRewards, MAX_UINT_AMOUNT) + ); + + const receiverAaveBalanceAfter = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalancerAfter = await stakeV3.balanceOf(staker.address); + expect(stakerStkAaveBalancerAfter.eq(parseEther('0'))).to.be.ok; + expect( + receiverAaveBalanceAfter.eq( + receiverAaveBalance.add( + halfRewards.add(stakerStkAaveBalance.mul(currentExchangeRate).div(ether)) + ) + ) + ).to.be.ok; + }); + it('Helper claim half & unstake full for staker to someone using claimRewardsAndRedeemOnBehalf', async () => { + const { + aaveToken, + users: [, , helper, someone, staker], + } = testEnv; + const ether = parseEther('1.0'); + await evmRevert(snap); + snap = await evmSnapshot(); + + const halfRewards = (await stakeV3.stakerRewardsToClaim(staker.address)).div(2); + const receiverAaveBalance = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalance = await stakeV3.balanceOf(staker.address); + const currentExchangeRate = await stakeV3.exchangeRate(); + + waitForTx( + await stakeV3 + .connect(helper.signer) + .claimRewardsAndRedeemOnBehalf( + staker.address, + someone.address, + halfRewards, + MAX_UINT_AMOUNT + ) + ); + + const receiverAaveBalanceAfter = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalancerAfter = await stakeV3.balanceOf(staker.address); + expect(stakerStkAaveBalancerAfter.eq(parseEther('0'))).to.be.ok; + expect( + receiverAaveBalanceAfter.eq( + receiverAaveBalance.add( + halfRewards.add(stakerStkAaveBalance.mul(currentExchangeRate).div(ether)) + ) + ) + ).to.be.ok; + }); + it('Helper succeeds to redeem full on behalf of staker to someone using redeemOnBehalf', async () => { + const { + aaveToken, + users: [, , helper, someone, staker], + } = testEnv; + // Increase time for bigger rewards + await evmRevert(snap); + snap = await evmSnapshot(); + + const ether = parseEther('1.0'); + const receiverAaveBalance = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalance = await stakeV3.balanceOf(staker.address); + const currentExchangeRate = await stakeV3.exchangeRate(); + + waitForTx( + await stakeV3 + .connect(helper.signer) + .redeemOnBehalf(staker.address, someone.address, MAX_UINT_AMOUNT) + ); + + const receiverAaveBalanceAfter = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalancerAfter = await stakeV3.balanceOf(staker.address); + expect(stakerStkAaveBalancerAfter.eq(parseEther('0'))).to.be.ok; + expect( + receiverAaveBalanceAfter.eq( + receiverAaveBalance.add(stakerStkAaveBalance.mul(currentExchangeRate).div(ether)) + ) + ).to.be.ok; + }); + it('Staker claim full & unstake full to someone using claimRewardsAndRedeem', async () => { + const { + aaveToken, + users: [, , helper, someone, staker], + } = testEnv; + const ether = parseEther('1.0'); + await evmRevert(snap); + snap = await evmSnapshot(); + + const fullRewards = await stakeV3.stakerRewardsToClaim(staker.address); + const receiverAaveBalance = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalance = await stakeV3.balanceOf(staker.address); + const userIndexBefore = await getUserIndex(stakeV3, staker.address, stakeV3.address); + const currentExchangeRate = await stakeV3.exchangeRate(); + waitForTx( + await stakeV3 + .connect(staker.signer) + .claimRewardsAndRedeem(someone.address, MAX_UINT_AMOUNT, MAX_UINT_AMOUNT) + ); + + const userIndexAfter = await getUserIndex(stakeV3, staker.address, stakeV3.address); + const expectedAccruedRewards = getRewards( + stakerStkAaveBalance, + userIndexAfter, + userIndexBefore + ); + const receiverAaveBalanceAfter = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalancerAfter = await stakeV3.balanceOf(staker.address); + expect(stakerStkAaveBalancerAfter.eq(parseEther('0'))).to.be.ok; + expect( + receiverAaveBalanceAfter.eq( + receiverAaveBalance + .add(fullRewards) + .add(expectedAccruedRewards) + .add(stakerStkAaveBalance.mul(currentExchangeRate).div(ether)) + ) + ).to.be.ok; + }); + it('Helper claim full & unstake full for staker to someone using claimRewardsAndRedeemOnBehalf', async () => { + const { + aaveToken, + users: [, , helper, someone, staker], + } = testEnv; + const ether = parseEther('1.0'); + await evmRevert(snap); + snap = await evmSnapshot(); + + const fullRewards = await stakeV3.stakerRewardsToClaim(staker.address); + const receiverAaveBalance = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalance = await stakeV3.balanceOf(staker.address); + const userIndexBefore = await getUserIndex(stakeV3, staker.address, stakeV3.address); + const currentExchangeRate = await stakeV3.exchangeRate(); + waitForTx( + await stakeV3 + .connect(helper.signer) + .claimRewardsAndRedeemOnBehalf( + staker.address, + someone.address, + MAX_UINT_AMOUNT, + MAX_UINT_AMOUNT + ) + ); + + const userIndexAfter = await getUserIndex(stakeV3, staker.address, stakeV3.address); + const expectedAccruedRewards = getRewards( + stakerStkAaveBalance, + userIndexAfter, + userIndexBefore + ); + const receiverAaveBalanceAfter = await aaveToken.balanceOf(someone.address); + const stakerStkAaveBalancerAfter = await stakeV3.balanceOf(staker.address); + expect(stakerStkAaveBalancerAfter.eq(parseEther('0'))).to.be.ok; + expect( + receiverAaveBalanceAfter.eq( + receiverAaveBalance + .add(fullRewards) + .add(expectedAccruedRewards) + .add(stakerStkAaveBalance.mul(currentExchangeRate).div(ether)) + ) + ).to.be.ok; + }); +});