From 8c1403f6df594a155e4d3489e0dbaae42792f657 Mon Sep 17 00:00:00 2001 From: The3D Date: Thu, 11 Feb 2021 16:58:56 +0100 Subject: [PATCH 1/7] Added stakeWithPermit function, tests --- contracts/interfaces/IERC20WithPermit.sol | 16 ++ contracts/interfaces/ISlashableStakeToken.sol | 18 -- contracts/interfaces/IStakedTokenV3.sol | 38 +++ contracts/stake/StakedTokenV3.sol | 242 ++++++++++++------ contracts/utils/MintableErc20.sol | 15 ++ package.json | 2 +- .../stakedAave-V3.spec.ts} | 65 ++++- 7 files changed, 294 insertions(+), 102 deletions(-) create mode 100644 contracts/interfaces/IERC20WithPermit.sol delete mode 100644 contracts/interfaces/ISlashableStakeToken.sol create mode 100644 contracts/interfaces/IStakedTokenV3.sol rename test/{Slashing/stakedAave-slashing.spec.ts => StakedAaveV3/stakedAave-V3.spec.ts} (92%) 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..364ef70 --- /dev/null +++ b/contracts/interfaces/IStakedTokenV3.sol @@ -0,0 +1,38 @@ +// 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 user, + address onBehalfOf, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + function claimRewardsOnBehalf( + address user, + address to, + uint256 amount + ) external; + + function claimRewardsAndStake(uint256 amount) external; + + function claimRewardsAndStakeOnBehalf(address user, uint256 amount) external; +} diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index 0ed6d60..11b1a8e 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,29 @@ 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 onBehalfOf, + 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 +89,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 +119,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 +147,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 +165,37 @@ 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 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) + { + _stake(msg.sender, onBehalfOf, amount); + } - emit Staked(msg.sender, onBehalfOf, amount, sharesToMint); + /** + * @dev Allows a user to stake STAKED_TOKEN with gasless approvals (permit) + * @param onBehalfOf Address of the user 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 user, + address onBehalfOf, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external override { + IERC20WithPermit(address(STAKED_TOKEN)).permit(user, address(this), amount, deadline, v, r, s); + _stake(user, onBehalfOf, amount); } /** @@ -176,8 +203,8 @@ 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'); + 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]; @@ -208,12 +235,44 @@ contract StakedTokenV3 is StakedTokenV2, emit Redeem(msg.sender, to, amountToRedeem, underlyingToRedeem); } + /** + * @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 user The address of the user + * @param to Address to claim for + * @param amount Amount to claim + **/ + function claimRewardsOnBehalf(address user, address to, uint256 amount) external override onlyClaimHelper { + uint256 newTotalRewards = + _updateCurrentUnclaimedRewards(user, balanceOf(user), false); + uint256 amountToClaim = (amount == type(uint256).max) ? newTotalRewards : amount; + + stakerRewardsToClaim[user] = newTotalRewards.sub(amountToClaim, 'INVALID_AMOUNT'); + REWARD_TOKEN.safeTransferFrom(REWARDS_VAULT, to, amountToClaim); + + emit RewardsClaimed(user, to, amountToClaim); + } + + /** + * @dev Claims an `amount` of `REWARD_TOKEN` amd restakes + * @param amount Amount to claim + **/ + function claimRewardsAndStake(uint256 amount) external override { + } + + /** + * @dev Claims an `amount` of `REWARD_TOKEN` and restakes. Only the claim helper contract is allowed to call this function + * @param user The address of the user + * @param amount Amount to claim + **/ + function claimRewardsAndStakeOnBehalf(address user, uint256 amount) external override onlyClaimHelper { + } + /** * @dev Calculates the exchange rate between the amount of STAKED_TOKEN and the the StakeToken total supply. * 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 +286,87 @@ 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 _stake( + address user, + address onBehalfOf, + uint256 amount + ) internal { + 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); + + STAKED_TOKEN.safeTransferFrom(user, address(this), amount); + + emit Staked(user, onBehalfOf, amount, sharesToMint); + } } 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/package.json b/package.json index 1680275..115663f 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "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", diff --git a/test/Slashing/stakedAave-slashing.spec.ts b/test/StakedAaveV3/stakedAave-V3.spec.ts similarity index 92% rename from test/Slashing/stakedAave-slashing.spec.ts rename to test/StakedAaveV3/stakedAave-V3.spec.ts index ccafcc9..6f60689 100644 --- a/test/Slashing/stakedAave-slashing.spec.ts +++ b/test/StakedAaveV3/stakedAave-V3.spec.ts @@ -1,15 +1,28 @@ 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 { + waitForTx, + timeLatest, + advanceBlock, + increaseTimeAndMine, + DRE, +} from '../../helpers/misc-utils'; import { ethers } from 'ethers'; import BigNumber from 'bignumber.js'; -import { getContract, getEthersSigners } from '../../helpers/contracts-helpers'; +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'); @@ -41,9 +54,10 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { //initialize the stake instance - await stakeV3['initialize(address,address,uint256,string,string,uint8)']( + await stakeV3['initialize(address,address,address,uint256,string,string,uint8)']( users[0].address, users[1].address, + users[1].address, '2000', 'Staked AAVE', 'stkAAVE', @@ -418,7 +432,6 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { expect(newAdmin).to.be.equal(users[3].address); }); - it('Pauses the cooldown', async () => { const { users } = testEnv; @@ -506,4 +519,48 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { 'INVALID_ZERO_AMOUNT' ); }); + + it('Stakes using permit', async () => { + + const { + users: [, staker], + } = 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').toString(); + + + const msgParams = buildPermitParams( + chainId, + stakeV3.address, + staker.address, + stakeV3.address, + nonce, + amount, + expiration.toFixed() + ); + + + const stakerPrivateKey = require('../../test-wallets').accounts[0].secretKey; + if (!stakerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const { v, r, s } = getSignatureFromTypedData(stakerPrivateKey, msgParams); + + + await stakeV3 + .connect(staker.signer) + .stakeWithPermit(staker.address, staker.address, amount, expiration, v, r, s); + }); }); From b8d366ac1bcfe1b0c03ca1953480ba6d47904702 Mon Sep 17 00:00:00 2001 From: dhadrien Date: Mon, 15 Feb 2021 11:32:00 +0100 Subject: [PATCH 2/7] Added features and tests (1/2) --- contracts/interfaces/IStakedTokenV3.sol | 16 +- contracts/stake/StakedTokenV2.sol | 11 +- contracts/stake/StakedTokenV3.sol | 156 +++++-- package.json | 1 + test/StakedAaveV3/stakedAave-V3.spec.ts | 568 +++++++++++++++++++++++- 5 files changed, 693 insertions(+), 59 deletions(-) diff --git a/contracts/interfaces/IStakedTokenV3.sol b/contracts/interfaces/IStakedTokenV3.sol index 364ef70..162b238 100644 --- a/contracts/interfaces/IStakedTokenV3.sol +++ b/contracts/interfaces/IStakedTokenV3.sol @@ -32,7 +32,19 @@ interface IStakedTokenV3 is IStakedToken { uint256 amount ) external; - function claimRewardsAndStake(uint256 amount) external; + function claimRewardsAndStake(address to, uint256 amount) external; - function claimRewardsAndStakeOnBehalf(address user, uint256 amount) external; + function claimRewardsAndUnstake(address to, uint256 amount) external; + + function claimRewardsAndStakeOnBehalf( + address user, + address to, + uint256 amount + ) external; + + function claimRewardsAndUnstakeOnBehalf( + address user, + address to, + uint256 amount + ) external; } diff --git a/contracts/stake/StakedTokenV2.sol b/contracts/stake/StakedTokenV2.sol index 4a0630c..0347cb1 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; @@ -331,7 +332,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 11b1a8e..1469123 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -58,7 +58,6 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { _; } - event Staked( address indexed from, address indexed onBehalfOf, @@ -162,6 +161,8 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { _initAdmins(adminsRoles, adminsAddresses); _maxSlashablePercentage = maxSlashablePercentage; + + IERC20(STAKED_TOKEN).approve(address(this), type(uint256).max); } /** @@ -204,67 +205,80 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { * @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); - - uint256 amountToRedeem = (amount > balanceOfMessageSender) ? balanceOfMessageSender : amount; - - _updateCurrentUnclaimedRewards(msg.sender, balanceOfMessageSender, true); - - uint256 underlyingToRedeem = amountToRedeem.mul(exchangeRate()).div(1e18); - - _burn(msg.sender, amountToRedeem); - - if (balanceOfMessageSender.sub(amountToRedeem) == 0) { - stakersCooldowns[msg.sender] = 0; - } - - IERC20(STAKED_TOKEN).safeTransfer(to, underlyingToRedeem); - - emit Redeem(msg.sender, to, amountToRedeem, underlyingToRedeem); + _redeem(msg.sender, to, amount); } - /** + /** * @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 user The address of the user * @param to Address to claim for * @param amount Amount to claim **/ - function claimRewardsOnBehalf(address user, address to, uint256 amount) external override onlyClaimHelper { - uint256 newTotalRewards = - _updateCurrentUnclaimedRewards(user, balanceOf(user), false); - uint256 amountToClaim = (amount == type(uint256).max) ? newTotalRewards : amount; - - stakerRewardsToClaim[user] = newTotalRewards.sub(amountToClaim, 'INVALID_AMOUNT'); - REWARD_TOKEN.safeTransferFrom(REWARDS_VAULT, to, amountToClaim); + function claimRewardsOnBehalf( + address user, + address to, + uint256 amount + ) external override onlyClaimHelper { + _claimRewards(user, to, amount); + } - emit RewardsClaimed(user, to, amountToClaim); + /** + * @dev Claims an `amount` of `REWARD_TOKEN` to the address `to` + * @param to Address to stake for + * @param amount Amount to stake + **/ + function claimRewards(address to, uint256 amount) external override(StakedTokenV2, IStakedToken) { + _claimRewards(msg.sender, to, amount); } /** * @dev Claims an `amount` of `REWARD_TOKEN` amd restakes + * @param to Address to stake to * @param amount Amount to claim **/ - function claimRewardsAndStake(uint256 amount) external override { + function claimRewardsAndStake(address to, uint256 amount) external override { + uint256 rewardsClaimed = _claimRewards(msg.sender, address(this), amount); + _stake(address(this), to, rewardsClaimed); } /** * @dev Claims an `amount` of `REWARD_TOKEN` and restakes. Only the claim helper contract is allowed to call this function + * @param user The address of the user from which to claim + * @param to Address to stake to + * @param amount Amount to claim + **/ + function claimRewardsAndStakeOnBehalf( + address user, + address to, + uint256 amount + ) external override onlyClaimHelper { + uint256 rewardsClaimed = _claimRewards(user, address(this), amount); + _stake(address(this), to, rewardsClaimed); + } + + /** + * @dev Claims an `amount` of `REWARD_TOKEN` amd unstakes + * @param amount Amount to claim + * @param to Address to claim and unstake to + **/ + function claimRewardsAndUnstake(address to, uint256 amount) external override { + _claimRewards(msg.sender, to, amount); + _redeem(msg.sender, to, amount); + } + + /** + * @dev Claims an `amount` of `REWARD_TOKEN` and unstakes. Only the claim helper contract is allowed to call this function * @param user The address of the user + * @param to Address to claim and unstake to * @param amount Amount to claim **/ - function claimRewardsAndStakeOnBehalf(address user, uint256 amount) external override onlyClaimHelper { + function claimRewardsAndUnstakeOnBehalf( + address user, + address to, + uint256 amount + ) external override onlyClaimHelper { + _claimRewards(user, to, amount); + _redeem(user, to, amount); } /** @@ -289,7 +303,6 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { * @param amount the amount **/ function slash(address destination, uint256 amount) external override onlySlashingAdmin { - uint256 balance = STAKED_TOKEN.balanceOf(address(this)); uint256 maxSlashable = balance.percentMul(_maxSlashablePercentage); @@ -343,6 +356,22 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { 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 user, address onBehalfOf, @@ -368,5 +397,46 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { STAKED_TOKEN.safeTransferFrom(user, address(this), amount); emit Staked(user, onBehalfOf, 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/package.json b/package.json index 115663f..1e8a2cd 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "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", diff --git a/test/StakedAaveV3/stakedAave-V3.spec.ts b/test/StakedAaveV3/stakedAave-V3.spec.ts index 6f60689..f65503a 100644 --- a/test/StakedAaveV3/stakedAave-V3.spec.ts +++ b/test/StakedAaveV3/stakedAave-V3.spec.ts @@ -28,6 +28,7 @@ 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; @@ -57,7 +58,7 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { await stakeV3['initialize(address,address,address,uint256,string,string,uint8)']( users[0].address, users[1].address, - users[1].address, + users[2].address, '2000', 'Staked AAVE', 'stkAAVE', @@ -66,9 +67,11 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { 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 () => { @@ -521,9 +524,9 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { }); it('Stakes using permit', async () => { - const { - users: [, staker], + aaveToken, + users: [, staker, someone], } = testEnv; const { chainId } = await DRE.ethers.provider.getNetwork(); @@ -531,14 +534,13 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { fail("Current network doesn't have CHAIN ID"); } - console.log("Staker address is ",staker.address); + console.log('Staker address is ', staker.address); const expiration = 0; const nonce = (await stakeV3._nonces(staker.address)).toNumber(); - const amount = parseEther('0.1').toString(); - + const amount = parseEther('0.1'); const msgParams = buildPermitParams( chainId, @@ -546,10 +548,12 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { staker.address, stakeV3.address, nonce, - amount, + 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) { @@ -558,9 +562,555 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { const { v, r, s } = getSignatureFromTypedData(stakerPrivateKey, msgParams); + const balanceBefore = await stakeV3.balanceOf(staker.address); + const exchangeRate = await stakeV3.exchangeRate(); + const ether = parseEther('1.0'); + waitForTx( + await stakeV3 + .connect(someone.signer) + .stakeWithPermit(staker.address, staker.address, amount, expiration, v, r, s) + ); + + expect(await stakeV3.balanceOf(staker.address)).to.be.eql( + balanceBefore.add(amount.mul(ether).div(exchangeRate)) + ); + }); + 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(staker.signer) - .stakeWithPermit(staker.address, staker.address, amount, expiration, v, r, s); + .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(); + + stakeV3.connect(staker.signer).claimRewardsAndStake(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('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) + ) + ); }); }); From baf9f326a36b8d22d54349dae6aa52c424d50889 Mon Sep 17 00:00:00 2001 From: dhadrien Date: Tue, 16 Feb 2021 09:39:19 +0100 Subject: [PATCH 3/7] added redeemOnBehalf claimRewardsOnBehalf claimRewardsAndStake claimRewardsAndStakeOnBehalf claimRewardsAndRedeem claimRewardsAndRedeemOnBehalf code and tests --- contracts/interfaces/IStakedTokenV3.sol | 27 +- contracts/stake/StakedTokenV3.sol | 119 +++++---- test/StakedAaveV3/stakedAave-V3.spec.ts | 326 ++++++++++++++++++++++++ 3 files changed, 411 insertions(+), 61 deletions(-) diff --git a/contracts/interfaces/IStakedTokenV3.sol b/contracts/interfaces/IStakedTokenV3.sol index 162b238..45d626a 100644 --- a/contracts/interfaces/IStakedTokenV3.sol +++ b/contracts/interfaces/IStakedTokenV3.sol @@ -17,8 +17,8 @@ interface IStakedTokenV3 is IStakedToken { function setMaxSlashablePercentage(uint256 percentage) external; function stakeWithPermit( - address user, - address onBehalfOf, + address from, + address to, uint256 amount, uint256 deadline, uint8 v, @@ -27,24 +27,35 @@ interface IStakedTokenV3 is IStakedToken { ) external; function claimRewardsOnBehalf( - address user, + address from, + address to, + uint256 amount + ) external; + + function redeemOnBehalf( + address from, address to, uint256 amount ) external; function claimRewardsAndStake(address to, uint256 amount) external; - function claimRewardsAndUnstake(address to, uint256 amount) external; + function claimRewardsAndRedeem( + address to, + uint256 claimAmount, + uint256 redeemAmount + ) external; function claimRewardsAndStakeOnBehalf( - address user, + address from, address to, uint256 amount ) external; - function claimRewardsAndUnstakeOnBehalf( - address user, + function claimRewardsAndRedeemOnBehalf( + address from, address to, - uint256 amount + uint256 claimAmount, + uint256 redeemAmount ) external; } diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index 1469123..b060195 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -58,12 +58,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { _; } - event Staked( - address indexed from, - address indexed onBehalfOf, - uint256 amount, - uint256 sharesMinted - ); + event Staked(address indexed from, address indexed to, uint256 amount, uint256 sharesMinted); event Redeem( address indexed from, address indexed to, @@ -166,20 +161,17 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { } /** - * @dev Allows a user to stake STAKED_TOKEN - * @param onBehalfOf Address of the user that will receive stake token shares + * @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 onBehalfOf, uint256 amount) - external - override(IStakedToken, StakedTokenV2) - { - _stake(msg.sender, onBehalfOf, amount); + function stake(address to, uint256 amount) external override(IStakedToken, StakedTokenV2) { + _stake(msg.sender, to, amount); } /** - * @dev Allows a user to stake STAKED_TOKEN with gasless approvals (permit) - * @param onBehalfOf Address of the user that will receive stake token shares + * @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 @@ -187,16 +179,16 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { * @param s The s component of the signed message **/ function stakeWithPermit( - address user, - address onBehalfOf, + address from, + address to, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external override { - IERC20WithPermit(address(STAKED_TOKEN)).permit(user, address(this), amount, deadline, v, r, s); - _stake(user, onBehalfOf, amount); + IERC20WithPermit(address(STAKED_TOKEN)).permit(from, address(this), amount, deadline, v, r, s); + _stake(from, to, amount); } /** @@ -209,28 +201,42 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { } /** - * @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 user The address of the user - * @param to Address to claim for - * @param amount Amount to claim + * @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 claimRewardsOnBehalf( - address user, + function redeemOnBehalf( + address from, address to, uint256 amount ) external override onlyClaimHelper { - _claimRewards(user, to, amount); + _redeem(from, to, amount); } /** * @dev Claims an `amount` of `REWARD_TOKEN` to the address `to` - * @param to Address to stake for + * @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); } + /** + * @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); + } + /** * @dev Claims an `amount` of `REWARD_TOKEN` amd restakes * @param to Address to stake to @@ -243,42 +249,49 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { /** * @dev Claims an `amount` of `REWARD_TOKEN` and restakes. Only the claim helper contract is allowed to call this function - * @param user The address of the user from which to claim + * @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 user, + address from, address to, uint256 amount ) external override onlyClaimHelper { - uint256 rewardsClaimed = _claimRewards(user, address(this), amount); + uint256 rewardsClaimed = _claimRewards(from, address(this), amount); _stake(address(this), to, rewardsClaimed); } /** - * @dev Claims an `amount` of `REWARD_TOKEN` amd unstakes - * @param amount Amount to claim + * @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 claimRewardsAndUnstake(address to, uint256 amount) external override { - _claimRewards(msg.sender, to, amount); - _redeem(msg.sender, to, amount); + function claimRewardsAndRedeem( + address to, + uint256 claimAmount, + uint256 redeemAmount + ) external override { + _claimRewards(msg.sender, to, claimAmount); + _redeem(msg.sender, to, redeemAmount); } /** - * @dev Claims an `amount` of `REWARD_TOKEN` and unstakes. Only the claim helper contract is allowed to call this function - * @param user The address of the user + * @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 amount Amount to claim + * @param claimAmount Amount to claim + * @param redeemAmount Amount to redeem **/ - function claimRewardsAndUnstakeOnBehalf( - address user, + function claimRewardsAndRedeemOnBehalf( + address from, address to, - uint256 amount + uint256 claimAmount, + uint256 redeemAmount ) external override onlyClaimHelper { - _claimRewards(user, to, amount); - _redeem(user, to, amount); + _claimRewards(from, to, claimAmount); + _redeem(from, to, redeemAmount); } /** @@ -373,30 +386,30 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { } function _stake( - address user, - address onBehalfOf, + address from, + address to, uint256 amount ) internal { require(amount != 0, 'INVALID_ZERO_AMOUNT'); - uint256 balanceOfUser = balanceOf(onBehalfOf); + uint256 balanceOfUser = balanceOf(to); uint256 accruedRewards = - _updateUserAssetInternal(onBehalfOf, address(this), balanceOfUser, totalSupply()); + _updateUserAssetInternal(to, address(this), balanceOfUser, totalSupply()); if (accruedRewards != 0) { - emit RewardsAccrued(onBehalfOf, accruedRewards); - stakerRewardsToClaim[onBehalfOf] = stakerRewardsToClaim[onBehalfOf].add(accruedRewards); + emit RewardsAccrued(to, accruedRewards); + stakerRewardsToClaim[to] = stakerRewardsToClaim[to].add(accruedRewards); } - stakersCooldowns[onBehalfOf] = getNextCooldownTimestamp(0, amount, onBehalfOf, balanceOfUser); + stakersCooldowns[to] = getNextCooldownTimestamp(0, amount, to, balanceOfUser); uint256 sharesToMint = amount.mul(1e18).div(exchangeRate()); - _mint(onBehalfOf, sharesToMint); + _mint(to, sharesToMint); - STAKED_TOKEN.safeTransferFrom(user, address(this), amount); + STAKED_TOKEN.safeTransferFrom(from, address(this), amount); - emit Staked(user, onBehalfOf, amount, sharesToMint); + emit Staked(from, to, amount, sharesToMint); } /** diff --git a/test/StakedAaveV3/stakedAave-V3.spec.ts b/test/StakedAaveV3/stakedAave-V3.spec.ts index f65503a..fff3c4d 100644 --- a/test/StakedAaveV3/stakedAave-V3.spec.ts +++ b/test/StakedAaveV3/stakedAave-V3.spec.ts @@ -6,6 +6,8 @@ import { advanceBlock, increaseTimeAndMine, DRE, + evmRevert, + evmSnapshot, } from '../../helpers/misc-utils'; import { ethers } from 'ethers'; import BigNumber from 'bignumber.js'; @@ -32,6 +34,7 @@ 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; @@ -1113,4 +1116,327 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { ) ); }); + + 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; + }); }); From 5bd671a5b3d730c383dc55a35241136cbd1ef70b Mon Sep 17 00:00:00 2001 From: eboado Date: Fri, 19 Feb 2021 10:08:11 +0100 Subject: [PATCH 4/7] Change on StakedTokenV2 --- contracts/stake/StakedTokenV2.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/stake/StakedTokenV2.sol b/contracts/stake/StakedTokenV2.sol index 0347cb1..96b967e 100644 --- a/contracts/stake/StakedTokenV2.sol +++ b/contracts/stake/StakedTokenV2.sol @@ -281,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; @@ -307,7 +307,6 @@ contract StakedTokenV2 is .div(amountToReceive.add(toBalance)); } } - stakersCooldowns[toAddress] = toCooldownTimestamp; return toCooldownTimestamp; } From 586d02f4ee5a48e823a3ccdd21d0843bd23a8bed Mon Sep 17 00:00:00 2001 From: dhadrien Date: Tue, 16 Feb 2021 15:25:35 +0100 Subject: [PATCH 5/7] removed useless transfers in claimAndStake --- contracts/stake/StakedTokenV3.sol | 53 ++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index b060195..f66015e 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -58,6 +58,11 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { _; } + modifier onlyStakedTokenIsRewardToken { + require((REWARD_TOKEN == STAKED_TOKEN), 'REWARD_TOKEN_IS_NOT_STAKED_TOKEN'); + _; + } + event Staked(address indexed from, address indexed to, uint256 amount, uint256 sharesMinted); event Redeem( address indexed from, @@ -156,8 +161,6 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { _initAdmins(adminsRoles, adminsAddresses); _maxSlashablePercentage = maxSlashablePercentage; - - IERC20(STAKED_TOKEN).approve(address(this), type(uint256).max); } /** @@ -166,7 +169,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { * @param amount The amount to be staked **/ function stake(address to, uint256 amount) external override(IStakedToken, StakedTokenV2) { - _stake(msg.sender, to, amount); + _stake(msg.sender, to, amount, false); } /** @@ -188,7 +191,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { bytes32 s ) external override { IERC20WithPermit(address(STAKED_TOKEN)).permit(from, address(this), amount, deadline, v, r, s); - _stake(from, to, amount); + _stake(from, to, amount, false); } /** @@ -220,7 +223,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { * @param amount Amount to stake **/ function claimRewards(address to, uint256 amount) external override(StakedTokenV2, IStakedToken) { - _claimRewards(msg.sender, to, amount); + _claimRewards(msg.sender, to, amount, false); } /** @@ -234,7 +237,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { address to, uint256 amount ) external override onlyClaimHelper { - _claimRewards(from, to, amount); + _claimRewards(from, to, amount, false); } /** @@ -242,9 +245,13 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { * @param to Address to stake to * @param amount Amount to claim **/ - function claimRewardsAndStake(address to, uint256 amount) external override { - uint256 rewardsClaimed = _claimRewards(msg.sender, address(this), amount); - _stake(address(this), to, rewardsClaimed); + function claimRewardsAndStake(address to, uint256 amount) + external + override + onlyStakedTokenIsRewardToken + { + uint256 rewardsClaimed = _claimRewards(msg.sender, address(this), amount, true); + _stake(address(this), to, rewardsClaimed, true); } /** @@ -257,9 +264,9 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { address from, address to, uint256 amount - ) external override onlyClaimHelper { - uint256 rewardsClaimed = _claimRewards(from, address(this), amount); - _stake(address(this), to, rewardsClaimed); + ) external override onlyStakedTokenIsRewardToken onlyClaimHelper { + uint256 rewardsClaimed = _claimRewards(from, address(this), amount, true); + _stake(address(this), to, rewardsClaimed, true); } /** @@ -273,7 +280,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { uint256 claimAmount, uint256 redeemAmount ) external override { - _claimRewards(msg.sender, to, claimAmount); + _claimRewards(msg.sender, to, claimAmount, false); _redeem(msg.sender, to, redeemAmount); } @@ -290,7 +297,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { uint256 claimAmount, uint256 redeemAmount ) external override onlyClaimHelper { - _claimRewards(from, to, claimAmount); + _claimRewards(from, to, claimAmount, false); _redeem(from, to, redeemAmount); } @@ -372,14 +379,17 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { function _claimRewards( address from, address to, - uint256 amount + uint256 amount, + bool claimToReserve ) 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); + if (!claimToReserve) { + REWARD_TOKEN.safeTransferFrom(REWARDS_VAULT, to, amountToClaim); + } emit RewardsClaimed(from, to, amountToClaim); return (amountToClaim); @@ -388,7 +398,8 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { function _stake( address from, address to, - uint256 amount + uint256 amount, + bool stakeFromReserve ) internal { require(amount != 0, 'INVALID_ZERO_AMOUNT'); @@ -407,7 +418,13 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { uint256 sharesToMint = amount.mul(1e18).div(exchangeRate()); _mint(to, sharesToMint); - STAKED_TOKEN.safeTransferFrom(from, address(this), amount); + if (stakeFromReserve) { + // redondant in current implementation + require((REWARD_TOKEN == STAKED_TOKEN), 'SHOULD_BE_SETTLED'); + REWARD_TOKEN.safeTransferFrom(REWARDS_VAULT, address(this), amount); + } else { + STAKED_TOKEN.safeTransferFrom(from, address(this), amount); + } emit Staked(from, to, amount, sharesToMint); } From 57bd352f63b5d8cfd81f32700c7bee3ca5a9ad71 Mon Sep 17 00:00:00 2001 From: The3D Date: Tue, 16 Feb 2021 16:46:49 +0100 Subject: [PATCH 6/7] updated behavior on claimAndStake --- contracts/stake/StakedTokenV3.sol | 47 ++++++++++++------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index f66015e..deb9d85 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -58,11 +58,6 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { _; } - modifier onlyStakedTokenIsRewardToken { - require((REWARD_TOKEN == STAKED_TOKEN), 'REWARD_TOKEN_IS_NOT_STAKED_TOKEN'); - _; - } - event Staked(address indexed from, address indexed to, uint256 amount, uint256 sharesMinted); event Redeem( address indexed from, @@ -169,7 +164,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { * @param amount The amount to be staked **/ function stake(address to, uint256 amount) external override(IStakedToken, StakedTokenV2) { - _stake(msg.sender, to, amount, false); + _stake(msg.sender, to, amount, true); } /** @@ -223,7 +218,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { * @param amount Amount to stake **/ function claimRewards(address to, uint256 amount) external override(StakedTokenV2, IStakedToken) { - _claimRewards(msg.sender, to, amount, false); + _claimRewards(msg.sender, to, amount); } /** @@ -237,7 +232,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { address to, uint256 amount ) external override onlyClaimHelper { - _claimRewards(from, to, amount, false); + _claimRewards(from, to, amount); } /** @@ -248,10 +243,11 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { function claimRewardsAndStake(address to, uint256 amount) external override - onlyStakedTokenIsRewardToken { - uint256 rewardsClaimed = _claimRewards(msg.sender, address(this), amount, true); - _stake(address(this), to, rewardsClaimed, true); + require(REWARD_TOKEN == STAKED_TOKEN, 'REWARD_TOKEN_IS_NOT_STAKED_TOKEN'); + + uint256 rewardsClaimed = _claimRewards(msg.sender, address(this), amount); + _stake(address(this), to, rewardsClaimed, false); } /** @@ -264,9 +260,11 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { address from, address to, uint256 amount - ) external override onlyStakedTokenIsRewardToken onlyClaimHelper { - uint256 rewardsClaimed = _claimRewards(from, address(this), amount, true); - _stake(address(this), to, rewardsClaimed, true); + ) 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); } /** @@ -280,7 +278,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { uint256 claimAmount, uint256 redeemAmount ) external override { - _claimRewards(msg.sender, to, claimAmount, false); + _claimRewards(msg.sender, to, claimAmount); _redeem(msg.sender, to, redeemAmount); } @@ -297,7 +295,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { uint256 claimAmount, uint256 redeemAmount ) external override onlyClaimHelper { - _claimRewards(from, to, claimAmount, false); + _claimRewards(from, to, claimAmount); _redeem(from, to, redeemAmount); } @@ -379,18 +377,13 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { function _claimRewards( address from, address to, - uint256 amount, - bool claimToReserve + 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'); - - if (!claimToReserve) { - REWARD_TOKEN.safeTransferFrom(REWARDS_VAULT, to, amountToClaim); - } - + REWARD_TOKEN.safeTransferFrom(REWARDS_VAULT, to, amountToClaim); emit RewardsClaimed(from, to, amountToClaim); return (amountToClaim); } @@ -399,7 +392,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { address from, address to, uint256 amount, - bool stakeFromReserve + bool pullFunds ) internal { require(amount != 0, 'INVALID_ZERO_AMOUNT'); @@ -418,11 +411,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { uint256 sharesToMint = amount.mul(1e18).div(exchangeRate()); _mint(to, sharesToMint); - if (stakeFromReserve) { - // redondant in current implementation - require((REWARD_TOKEN == STAKED_TOKEN), 'SHOULD_BE_SETTLED'); - REWARD_TOKEN.safeTransferFrom(REWARDS_VAULT, address(this), amount); - } else { + if (pullFunds) { STAKED_TOKEN.safeTransferFrom(from, address(this), amount); } From 1bb2173fe6fb62d7b4b3c70979fe951c8944ae83 Mon Sep 17 00:00:00 2001 From: The3D Date: Tue, 16 Feb 2021 18:53:28 +0100 Subject: [PATCH 7/7] Updated tests, added gas reporter --- contracts/stake/StakedTokenV3.sol | 12 +- hardhat.config.ts | 1 + package-lock.json | 492 ++++++++++++++++++++++++ package.json | 3 +- test/StakedAaveV3/stakedAave-V3.spec.ts | 35 +- 5 files changed, 530 insertions(+), 13 deletions(-) diff --git a/contracts/stake/StakedTokenV3.sol b/contracts/stake/StakedTokenV3.sol index deb9d85..6ed3162 100644 --- a/contracts/stake/StakedTokenV3.sol +++ b/contracts/stake/StakedTokenV3.sol @@ -186,7 +186,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { bytes32 s ) external override { IERC20WithPermit(address(STAKED_TOKEN)).permit(from, address(this), amount, deadline, v, r, s); - _stake(from, to, amount, false); + _stake(from, to, amount, true); } /** @@ -240,14 +240,13 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { * @param to Address to stake to * @param amount Amount to claim **/ - function claimRewardsAndStake(address to, uint256 amount) - external - override - { + function claimRewardsAndStake(address to, uint256 amount) external override { require(REWARD_TOKEN == STAKED_TOKEN, 'REWARD_TOKEN_IS_NOT_STAKED_TOKEN'); uint256 rewardsClaimed = _claimRewards(msg.sender, address(this), amount); - _stake(address(this), to, rewardsClaimed, false); + if (rewardsClaimed != 0) { + _stake(address(this), to, rewardsClaimed, false); + } } /** @@ -380,6 +379,7 @@ contract StakedTokenV3 is StakedTokenV2, IStakedTokenV3, RoleManager { 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'); 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 1e8a2cd..c4c933d 100644 --- a/package.json +++ b/package.json @@ -67,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/StakedAaveV3/stakedAave-V3.spec.ts b/test/StakedAaveV3/stakedAave-V3.spec.ts index fff3c4d..023d4da 100644 --- a/test/StakedAaveV3/stakedAave-V3.spec.ts +++ b/test/StakedAaveV3/stakedAave-V3.spec.ts @@ -568,15 +568,25 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { 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 { @@ -679,6 +689,7 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { 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, @@ -689,24 +700,35 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { 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(staker.signer).claimRewardsAndStake(staker.address, halfRewards); + 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].eq(saveUserBalance[0])).to.be.ok; + + + expect(userBalanceAfterActions[0]).to.be.eq(saveUserBalance[0], "Invalid aave user balance after action"); + expect( - userBalanceAfterActions[1].eq( - saveUserBalance[1].add(halfRewards.mul(ether).div(currentExchangeRate)) - ) - ).to.be.ok; + 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 { @@ -1020,6 +1042,7 @@ makeSuite('StakedAave V3 slashing tests', (testEnv: TestEnv) => { 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,