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; + }); });