import { StandardAccounts } from "@utils/machines" ; import { BN , simpleToExactAmount } from "@utils/math" ; import { FIVE_DAYS , fullScale , ONE_DAY , ONE_WEEK } from "@utils/constants" ; import { getTimestamp , increaseTime } from "@utils/time" ; import { AssetProxy__factory , MockERC20 , MockERC20__factory , MockNexus , MockNexus__factory , StakingRewards , StakingRewards__factory , } from "types/generated" ; import { ethers } from "hardhat" ; import { expect } from "chai" ; import { assertBNClose , assertBNSlightlyGT } from "@utils/assertions" ; import { Account } from "types" ; describe ( "StakingRewards" , async ( ) => { let sa ; let rewardsDistributor ; let nexus ; let rewardToken ; let stakingToken ; let stakingRewards ; const redeployRewards = async ( nexusAddress = nexus . address , rewardDecimals = 18 , stakingDecimals = 18 ) : Promise < StakingRewards > => { const deployer = sa . default . signer ; rewardToken = await new MockERC20__factory ( deployer ) . deploy ( "Reward" , "RWD" , rewardDecimals , rewardsDistributor . address , 1000000 ) ; stakingToken = await new MockERC20__factory ( deployer ) . deploy ( "Staking" , "ST8k" , stakingDecimals , sa . default . address , 1000000 ) ; const stakingRewardsImpl = await new StakingRewards__factory ( deployer ) . deploy ( nexusAddress , stakingToken . address , rewardToken . address , ONE_DAY . mul ( 7 ) ) ; const initializeData = stakingRewardsImpl . interface . encodeFunctionData ( "initialize" , [ rewardsDistributor . address , "StakingToken" , "ST8k" , ] ) ; const proxy = await new AssetProxy__factory ( deployer ) . deploy ( stakingRewardsImpl . address , sa . governor . address , initializeData ) ; stakingRewards = StakingRewards__factory . connect ( proxy . address , deployer ) ; return stakingRewards ; } ; interface StakingData { totalSupply : BN ; userStakingBalance : BN ; senderStakingTokenBalance : BN ; contractStakingTokenBalance : BN ; userRewardPerTokenPaid : BN ; beneficiaryRewardsEarned : BN ; rewardPerTokenStored : BN ; rewardRate : BN ; lastUpdateTime : BN ; lastTimeRewardApplicable : BN ; periodFinishTime : BN ; } const snapshotStakingData = async ( sender = sa . default , beneficiary = sa . default ) : Promise < StakingData > => { const userData = await stakingRewards . userData ( beneficiary . address ) ; const globalData = await stakingRewards . globalData ( ) ; return { totalSupply : await stakingRewards . totalSupply ( ) , userStakingBalance : await stakingRewards . balanceOf ( beneficiary . address ) , userRewardPerTokenPaid : await userData [ 0 ] , beneficiaryRewardsEarned : await userData [ 1 ] , senderStakingTokenBalance : await stakingToken . balanceOf ( sender . address ) , contractStakingTokenBalance : await stakingToken . balanceOf ( stakingRewards . address ) , periodFinishTime : BN . from ( globalData [ 0 ] ) , lastUpdateTime : BN . from ( globalData [ 1 ] ) , rewardRate : globalData [ 2 ] , rewardPerTokenStored : globalData [ 3 ] , lastTimeRewardApplicable : await stakingRewards . lastTimeRewardApplicable ( ) , } ; } ; before ( async ( ) => { const accounts = await ethers . getSigners ( ) ; sa = await new StandardAccounts ( ) . initAccounts ( accounts ) ; rewardsDistributor = sa . fundManager ; nexus = await new MockNexus__factory ( sa . default . signer ) . deploy ( sa . governor . address , sa . mockSavingsManager . address , sa . mockInterestValidator . address ) ; } ) ; describe ( "constructor & settings" , async ( ) => { before ( async ( ) => { await redeployRewards ( ) ; } ) ; it ( "should set all initial state" , async ( ) => { const data = await snapshotStakingData ( ) ; expect ( await stakingRewards . nexus ( ) , nexus . address ) ; expect ( await stakingRewards . stakingToken ( ) , stakingToken . address ) ; expect ( await stakingRewards . rewardsToken ( ) , rewardToken . address ) ; expect ( await stakingRewards . rewardsDistributor ( ) , rewardsDistributor . address ) ; expect ( data . totalSupply ) . eq ( 0 ) ; expect ( data . periodFinishTime ) . eq ( 0 ) ; expect ( data . rewardRate ) . eq ( 0 ) ; expect ( data . lastUpdateTime ) . eq ( 0 ) ; expect ( data . rewardPerTokenStored ) . eq ( 0 ) ; expect ( data . lastTimeRewardApplicable ) . eq ( 0 ) ; expect ( await stakingRewards . rewardPerToken ( ) ) . eq ( 0 ) ; } ) ; } ) ; dev param beforeData param afterData param isExistingStaker const assertRewardsAssigned = async ( beforeData , afterData , isExistingStaker , shouldResetRewards = false ) : Promise < void > => { const timeAfter = await getTimestamp ( ) ; const periodIsFinished = timeAfter . gt ( beforeData . periodFinishTime ) ; const beforeDataLastUpdateTime = beforeData . rewardPerTokenStored . eq ( 0 ) && beforeData . totalSupply . eq ( 0 ) ? beforeData . lastUpdateTime : timeAfter ; expect ( periodIsFinished ? beforeData . periodFinishTime : beforeDataLastUpdateTime ) . eq ( afterData . lastUpdateTime ) ; expect ( beforeData . rewardRate ) . eq ( afterData . rewardRate ) ; expect ( afterData . rewardPerTokenStored ) . gte ( beforeData . rewardPerTokenStored ) ; const timeApplicableToRewards = periodIsFinished ? beforeData . periodFinishTime . sub ( beforeData . lastUpdateTime ) : timeAfter . sub ( beforeData . lastUpdateTime ) ; const increaseInRewardPerToken = beforeData . totalSupply . eq ( 0 ) ? 0 : beforeData . rewardRate . mul ( timeApplicableToRewards ) . mul ( fullScale ) . div ( beforeData . totalSupply ) ; expect ( beforeData . rewardPerTokenStored . add ( increaseInRewardPerToken ) ) . eq ( afterData . rewardPerTokenStored ) ; expect ( afterData . userRewardPerTokenPaid ) . eq ( afterData . rewardPerTokenStored ) ; if ( shouldResetRewards ) { expect ( afterData . beneficiaryRewardsEarned ) . eq ( 0 ) ; } else if ( isExistingStaker ) { const increaseInUserRewardPerToken = afterData . rewardPerTokenStored . sub ( beforeData . userRewardPerTokenPaid ) ; const assignment = beforeData . userStakingBalance . mul ( increaseInUserRewardPerToken ) . div ( fullScale ) ; expect ( beforeData . beneficiaryRewardsEarned . add ( assignment ) ) . eq ( afterData . beneficiaryRewardsEarned ) ; } else { expect ( beforeData . beneficiaryRewardsEarned ) . eq ( afterData . beneficiaryRewardsEarned ) ; } } ; dev param stakeAmount param sender param beneficiary param confirmExistingStaker const expectSuccessfulStake = async ( stakeAmount , sender = sa . default , beneficiary = sa . default , confirmExistingStaker = false ) : Promise < void > => { const senderIsBeneficiary = sender === beneficiary ; const beforeData = await snapshotStakingData ( sender , beneficiary ) ; const isExistingStaker = beforeData . userStakingBalance . gt ( 0 ) ; if ( confirmExistingStaker ) { expect ( isExistingStaker ) . eq ( true ) ; } await stakingToken . connect ( sender . signer ) . approve ( stakingRewards . address , stakeAmount ) ; const tx = await ( senderIsBeneficiary ? stakingRewards . connect ( sender . signer ) [ "stake(uint256)" ] ( stakeAmount ) : stakingRewards . connect ( sender . signer ) [ "stake(address,uint256)" ] ( beneficiary . address , stakeAmount ) ) ; await expect ( tx ) . to . emit ( stakingRewards , "Staked" ) . withArgs ( beneficiary . address , stakeAmount , sender . address ) ; const afterData = await snapshotStakingData ( sender , beneficiary ) ; await assertRewardsAssigned ( beforeData , afterData , isExistingStaker ) ; expect ( beforeData . senderStakingTokenBalance . sub ( stakeAmount ) ) . eq ( afterData . senderStakingTokenBalance ) ; expect ( beforeData . contractStakingTokenBalance . add ( stakeAmount ) ) . eq ( afterData . contractStakingTokenBalance ) ; expect ( beforeData . totalSupply . add ( stakeAmount ) ) . eq ( afterData . totalSupply ) ; } ; dev param rewardUnits const expectSuccessfulFunding = async ( rewardUnits ) : Promise < void > => { const beforeData = await snapshotStakingData ( ) ; const tx = await stakingRewards . connect ( rewardsDistributor . signer ) . notifyRewardAmount ( rewardUnits ) ; await expect ( tx ) . to . emit ( stakingRewards , "RewardAdded" ) . withArgs ( rewardUnits ) ; const cur = await getTimestamp ( ) ; const leftOverRewards = beforeData . rewardRate . mul ( beforeData . periodFinishTime . sub ( beforeData . lastTimeRewardApplicable ) ) ; const afterData = await snapshotStakingData ( ) ; expect ( cur ) . eq ( afterData . lastTimeRewardApplicable ) ; expect ( cur ) . eq ( afterData . lastUpdateTime ) ; expect ( cur . add ( ONE_WEEK ) ) . eq ( afterData . periodFinishTime ) ; if ( leftOverRewards . gt ( 0 ) ) { const total = rewardUnits . add ( leftOverRewards ) ; assertBNClose ( total . div ( ONE_WEEK ) , afterData . rewardRate , beforeData . rewardRate . div ( ONE_WEEK ) . mul ( 5 ) ) ; } else { expect ( rewardUnits . div ( ONE_WEEK ) ) . eq ( afterData . rewardRate ) ; } } ; dev param withdrawAmount param sender const expectStakingWithdrawal = async ( withdrawAmount , sender = sa . default ) : Promise < void > => { const beforeData = await snapshotStakingData ( sender ) ; const isExistingStaker = beforeData . userStakingBalance . gt ( 0 ) ; expect ( isExistingStaker ) . eq ( true ) ; expect ( withdrawAmount ) . gte ( beforeData . userStakingBalance ) ; const tx = await stakingRewards . connect ( sender . signer ) . withdraw ( withdrawAmount ) ; await expect ( tx ) . to . emit ( stakingRewards , "Withdrawn" ) . withArgs ( sender . address , withdrawAmount ) ; const afterData = await snapshotStakingData ( sender ) ; await assertRewardsAssigned ( beforeData , afterData , isExistingStaker ) ; expect ( beforeData . senderStakingTokenBalance . add ( withdrawAmount ) ) . eq ( afterData . senderStakingTokenBalance ) ; expect ( beforeData . userStakingBalance . sub ( withdrawAmount ) ) . eq ( afterData . userStakingBalance ) ; expect ( beforeData . totalSupply . sub ( withdrawAmount ) ) . eq ( afterData . totalSupply ) ; } ; context ( "initialising and staking in a new pool" , ( ) => { before ( async ( ) => { await redeployRewards ( ) ; } ) ; describe ( "notifying the pool of reward" , ( ) => { it ( "should begin a new period through" , async ( ) => { const rewardUnits = simpleToExactAmount ( 1 , 18 ) ; await expectSuccessfulFunding ( rewardUnits ) ; } ) ; } ) ; describe ( "staking in the new period" , ( ) => { it ( "should assign rewards to the staker" , async ( ) => { const data = await snapshotStakingData ( ) ; const { rewardRate } = data ; const stakeAmount = simpleToExactAmount ( 100 , 18 ) ; await expectSuccessfulStake ( stakeAmount ) ; await increaseTime ( ONE_DAY ) ; const rewardPerToken = await stakingRewards . rewardPerToken ( ) ; const rewardPerSecond = BN . from ( rewardRate ) . mul ( fullScale ) . div ( stakeAmount ) ; assertBNClose ( rewardPerToken , ONE_DAY . mul ( rewardPerSecond ) , rewardPerSecond . mul ( 10 ) ) ; const earned = await stakingRewards . earned ( sa . default . address ) ; expect ( stakeAmount . mul ( rewardPerToken ) . div ( fullScale ) ) . eq ( earned ) ; } ) ; it ( "should update stakers rewards after consequent stake" , async ( ) => { const stakeAmount = simpleToExactAmount ( 100 , 18 ) ; await expectSuccessfulStake ( stakeAmount , sa . default , sa . default , true ) ; } ) ; it ( "should fail if stake amount is 0" , async ( ) => { await expect ( stakingRewards . connect ( sa . default . signer ) [ "stake(uint256)" ] ( 0 ) ) . to . revertedWith ( "Cannot stake 0" ) ; } ) ; it ( "should fail if staker has insufficient balance" , async ( ) => { await stakingToken . connect ( sa . dummy2 . signer ) . approve ( stakingRewards . address , 1 ) ; await expect ( stakingRewards . connect ( sa . dummy2 . signer ) [ "stake(uint256)" ] ( 1 ) ) . to . revertedWith ( "ERC20: transfer amount exceeds balance" ) ; } ) ; } ) ; } ) ; context ( "funding with too much rewards" , ( ) => { before ( async ( ) => { await redeployRewards ( ) ; } ) ; it ( "should fail" , async ( ) => { await expect ( stakingRewards . connect ( sa . fundManager . signer ) . notifyRewardAmount ( simpleToExactAmount ( 1 , 25 ) ) ) . to . revertedWith ( "Cannot notify with more than a million units" ) ; } ) ; } ) ; context ( "staking before rewards are added" , ( ) => { before ( async ( ) => { await redeployRewards ( ) ; } ) ; it ( "should assign no rewards" , async ( ) => { const stakeAmount = simpleToExactAmount ( 100 , 18 ) ; const beforeData = await snapshotStakingData ( ) ; expect ( beforeData . rewardRate ) . eq ( 0 ) ; expect ( beforeData . rewardPerTokenStored ) . eq ( 0 ) ; expect ( beforeData . beneficiaryRewardsEarned ) . eq ( 0 ) ; expect ( beforeData . totalSupply ) . eq ( 0 ) ; expect ( beforeData . lastTimeRewardApplicable ) . eq ( 0 ) ; await expectSuccessfulStake ( stakeAmount ) ; await increaseTime ( ONE_DAY ) ; await expectSuccessfulStake ( stakeAmount ) ; const afterData = await snapshotStakingData ( ) ; expect ( afterData . rewardRate ) . eq ( 0 ) ; expect ( afterData . rewardPerTokenStored ) . eq ( 0 ) ; expect ( afterData . beneficiaryRewardsEarned ) . eq ( 0 ) ; expect ( afterData . totalSupply ) . eq ( stakeAmount . mul ( 2 ) ) ; expect ( afterData . lastTimeRewardApplicable ) . eq ( 0 ) ; } ) ; } ) ; context ( "adding first stake days after funding" , ( ) => { before ( async ( ) => { await redeployRewards ( ) ; } ) ; it ( "should retrospectively assign rewards to the first staker" , async ( ) => { await expectSuccessfulFunding ( simpleToExactAmount ( 100 , 18 ) ) ; const { rewardRate } = await stakingRewards . globalData ( ) ; await increaseTime ( FIVE_DAYS ) ; const stakeAmount = simpleToExactAmount ( 100 , 18 ) ; await expectSuccessfulStake ( stakeAmount ) ; const rewardPerToken = await stakingRewards . rewardPerToken ( ) ; const rewardPerSecond = BN . from ( rewardRate ) . mul ( fullScale ) . div ( stakeAmount ) ; assertBNClose ( rewardPerToken , FIVE_DAYS . mul ( rewardPerSecond ) , rewardPerSecond . mul ( 4 ) ) ; const earnedAfterConsequentStake = await stakingRewards . earned ( sa . default . address ) ; expect ( stakeAmount . mul ( rewardPerToken ) . div ( fullScale ) ) . eq ( earnedAfterConsequentStake ) ; } ) ; } ) ; context ( "staking over multiple funded periods" , ( ) => { context ( "with a single staker" , ( ) => { before ( async ( ) => { await redeployRewards ( ) ; } ) ; it ( "should assign all the rewards from the periods" , async ( ) => { const fundAmount1 = simpleToExactAmount ( 100 , 18 ) ; const fundAmount2 = simpleToExactAmount ( 200 , 18 ) ; await expectSuccessfulFunding ( fundAmount1 ) ; const stakeAmount = simpleToExactAmount ( 1 , 18 ) ; await expectSuccessfulStake ( stakeAmount ) ; await increaseTime ( ONE_WEEK . mul ( 2 ) ) ; await expectSuccessfulFunding ( fundAmount2 ) ; await increaseTime ( ONE_WEEK . mul ( 2 ) ) ; const earned = await stakingRewards . earned ( sa . default . address ) ; assertBNSlightlyGT ( fundAmount1 . add ( fundAmount2 ) , earned , BN . from ( 1000000 ) , false ) ; } ) ; } ) ; context ( "with multiple stakers coming in and out" , ( ) => { let staker2 ; let staker3 ; const fundAmount1 = simpleToExactAmount ( 100 , 21 ) ; const fundAmount2 = simpleToExactAmount ( 200 , 21 ) ; const staker1Stake1 = simpleToExactAmount ( 100 , 18 ) ; const staker1Stake2 = simpleToExactAmount ( 200 , 18 ) ; const staker2Stake = simpleToExactAmount ( 100 , 18 ) ; const staker3Stake = simpleToExactAmount ( 100 , 18 ) ; before ( async ( ) => { await redeployRewards ( ) ; staker2 = sa . dummy1 ; staker3 = sa . dummy2 ; await stakingToken . transfer ( staker2 . address , staker2Stake ) ; await stakingToken . transfer ( staker3 . address , staker3Stake ) ; } ) ; it ( "should accrue rewards on a pro rata basis" , async ( ) => { await expectSuccessfulStake ( staker1Stake1 ) ; await expectSuccessfulStake ( staker3Stake , staker3 , staker3 ) ; await expectSuccessfulFunding ( fundAmount1 ) ; await increaseTime ( ONE_WEEK . div ( 2 ) . add ( 1 ) ) ; await expectSuccessfulStake ( staker2Stake , staker2 , staker2 ) ; await increaseTime ( ONE_WEEK . div ( 2 ) . add ( 1 ) ) ; await expectSuccessfulFunding ( fundAmount2 ) ; await stakingRewards . connect ( staker3 . signer ) . withdraw ( staker3Stake ) ; await expectSuccessfulStake ( staker1Stake2 , sa . default , sa . default , true ) ; await increaseTime ( ONE_WEEK ) ; const earned1 = await stakingRewards . earned ( sa . default . address ) ; assertBNClose ( earned1 , simpleToExactAmount ( "191.66" , 21 ) , simpleToExactAmount ( 1 , 19 ) ) ; const earned2 = await stakingRewards . earned ( staker2 . address ) ; assertBNClose ( earned2 , simpleToExactAmount ( "66.66" , 21 ) , simpleToExactAmount ( 1 , 19 ) ) ; const earned3 = await stakingRewards . earned ( staker3 . address ) ; assertBNClose ( earned3 , simpleToExactAmount ( "41.66" , 21 ) , simpleToExactAmount ( 1 , 19 ) ) ; expect ( fundAmount1 . add ( fundAmount2 ) ) . gte ( earned1 . add ( earned2 ) . add ( earned3 ) ) ; } ) ; } ) ; } ) ; context ( "staking after period finish" , ( ) => { const fundAmount1 = simpleToExactAmount ( 100 , 21 ) ; before ( async ( ) => { await redeployRewards ( ) ; } ) ; it ( "should stop accruing rewards after the period is over" , async ( ) => { await expectSuccessfulStake ( simpleToExactAmount ( 1 , 18 ) ) ; await expectSuccessfulFunding ( fundAmount1 ) ; await increaseTime ( ONE_WEEK . add ( 1 ) ) ; const earnedAfterWeek = await stakingRewards . earned ( sa . default . address ) ; await increaseTime ( ONE_WEEK . add ( 1 ) ) ; const now = await getTimestamp ( ) ; const earnedAfterTwoWeeks = await stakingRewards . earned ( sa . default . address ) ; expect ( earnedAfterWeek ) . eq ( earnedAfterTwoWeeks ) ; const lastTimeRewardApplicable = await stakingRewards . lastTimeRewardApplicable ( ) ; assertBNClose ( lastTimeRewardApplicable , now . sub ( ONE_WEEK ) . sub ( 2 ) , BN . from ( 2 ) ) ; } ) ; } ) ; context ( "staking on behalf of a beneficiary" , ( ) => { const fundAmount = simpleToExactAmount ( 100 , 21 ) ; let beneficiary ; const stakeAmount = simpleToExactAmount ( 100 , 18 ) ; before ( async ( ) => { await redeployRewards ( ) ; beneficiary = sa . dummy1 ; await expectSuccessfulFunding ( fundAmount ) ; await expectSuccessfulStake ( stakeAmount , sa . default , beneficiary ) ; await increaseTime ( 10 ) ; } ) ; it ( "should update the beneficiaries reward details" , async ( ) => { const earned = await stakingRewards . earned ( beneficiary . address ) ; expect ( earned ) . gt ( 0 ) ; const balance = await stakingRewards . balanceOf ( beneficiary . address ) ; expect ( balance ) . eq ( stakeAmount ) ; } ) ; it ( "should not update the senders details" , async ( ) => { const earned = await stakingRewards . earned ( sa . default . address ) ; expect ( earned ) . eq ( 0 ) ; const balance = await stakingRewards . balanceOf ( sa . default . address ) ; expect ( balance ) . eq ( 0 ) ; } ) ; } ) ; context ( "using staking / reward tokens with diff decimals" , ( ) => { before ( async ( ) => { await redeployRewards ( nexus . address , 12 , 16 ) ; } ) ; it ( "should not affect the pro rata payouts" , async ( ) => { await expectSuccessfulFunding ( simpleToExactAmount ( 100 , 12 ) ) ; const { rewardRate } = await stakingRewards . globalData ( ) ; const stakeAmount = simpleToExactAmount ( 100 , 16 ) ; await expectSuccessfulStake ( stakeAmount ) ; await increaseTime ( ONE_WEEK . add ( 1 ) ) ; const rewardPerToken = await stakingRewards . rewardPerToken ( ) ; assertBNClose ( rewardPerToken , ONE_WEEK . mul ( rewardRate ) . mul ( fullScale ) . div ( stakeAmount ) , rewardRate . mul ( fullScale ) . div ( stakeAmount ) ) ; const earnedAfterConsequentStake = await stakingRewards . earned ( sa . default . address ) ; assertBNSlightlyGT ( simpleToExactAmount ( 100 , 12 ) , earnedAfterConsequentStake , simpleToExactAmount ( 1 , 9 ) ) ; } ) ; } ) ; context ( "getting the reward token" , ( ) => { before ( async ( ) => { await redeployRewards ( ) ; } ) ; it ( "should simply return the rewards Token" , async ( ) => { const readToken = await stakingRewards . getRewardToken ( ) ; expect ( readToken ) . eq ( rewardToken . address ) ; expect ( readToken ) . eq ( await stakingRewards . rewardsToken ( ) ) ; } ) ; } ) ; context ( "notifying new reward amount" , ( ) => { context ( "from someone other than the distributor" , ( ) => { before ( async ( ) => { await redeployRewards ( ) ; } ) ; it ( "should fail using default signer" , async ( ) => { await expect ( stakingRewards . connect ( sa . default . signer ) . notifyRewardAmount ( 1 ) ) . to . revertedWith ( "Caller is not reward distributor" ) ; } ) ; it ( "should fail using dummy1" , async ( ) => { await expect ( stakingRewards . connect ( sa . dummy1 . signer ) . notifyRewardAmount ( 1 ) ) . to . revertedWith ( "Caller is not reward distributor" ) ; } ) ; it ( "should fail using governor" , async ( ) => { await expect ( stakingRewards . connect ( sa . governor . signer ) . notifyRewardAmount ( 1 ) ) . to . revertedWith ( "TransparentUpgradeableProxy: admin cannot fallback to proxy target" ) ; } ) ; } ) ; context ( "before current period finish" , async ( ) => { const funding1 = simpleToExactAmount ( 100 , 18 ) ; const funding2 = simpleToExactAmount ( 200 , 18 ) ; beforeEach ( async ( ) => { await redeployRewards ( ) ; } ) ; it ( "should factor in unspent units to the new rewardRate" , async ( ) => { await expectSuccessfulFunding ( funding1 ) ; const { rewardRate : actualRewardRate } = await stakingRewards . globalData ( ) ; const expectedRewardRate = funding1 . div ( ONE_WEEK ) ; expect ( expectedRewardRate ) . eq ( actualRewardRate ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; const expectedLeftoverReward = funding1 . div ( 2 ) ; await expectSuccessfulFunding ( funding2 ) ; const { rewardRate : actualRewardRateAfter } = await stakingRewards . globalData ( ) ; const totalRewardsForWeek = funding2 . add ( expectedLeftoverReward ) ; const expectedRewardRateAfter = totalRewardsForWeek . div ( ONE_WEEK ) ; assertBNClose ( actualRewardRateAfter , expectedRewardRateAfter , actualRewardRate . div ( ONE_WEEK ) . mul ( 20 ) ) ; } ) ; it ( "should factor in unspent units to the new rewardRate if instant" , async ( ) => { await expectSuccessfulFunding ( funding1 ) ; const { rewardRate : actualRewardRate } = await stakingRewards . globalData ( ) ; const expectedRewardRate = funding1 . div ( ONE_WEEK ) ; expect ( expectedRewardRate ) . eq ( actualRewardRate ) ; await increaseTime ( 1 ) ; await expectSuccessfulFunding ( funding2 ) ; const { rewardRate : actualRewardRateAfter } = await stakingRewards . globalData ( ) ; const expectedRewardRateAfter = funding1 . add ( funding2 ) . div ( ONE_WEEK ) ; assertBNClose ( actualRewardRateAfter , expectedRewardRateAfter , actualRewardRate . div ( ONE_WEEK ) . mul ( 20 ) ) ; } ) ; } ) ; context ( "after current period finish" , ( ) => { const funding1 = simpleToExactAmount ( 100 , 18 ) ; before ( async ( ) => { await redeployRewards ( ) ; } ) ; it ( "should start a new period with the correct rewardRate" , async ( ) => { await expectSuccessfulFunding ( funding1 ) ; const { rewardRate : actualRewardRate } = await stakingRewards . globalData ( ) ; const expectedRewardRate = funding1 . div ( ONE_WEEK ) ; expect ( expectedRewardRate ) . eq ( actualRewardRate ) ; await increaseTime ( ONE_WEEK . add ( 1 ) ) ; await expectSuccessfulFunding ( funding1 . mul ( 2 ) ) ; const { rewardRate : actualRewardRateAfter } = await stakingRewards . globalData ( ) ; const expectedRewardRateAfter = expectedRewardRate . mul ( 2 ) ; expect ( actualRewardRateAfter ) . eq ( expectedRewardRateAfter ) ; } ) ; } ) ; } ) ; context ( "withdrawing stake or rewards" , ( ) => { context ( "withdrawing a stake amount" , ( ) => { const fundAmount = simpleToExactAmount ( 100 , 21 ) ; const stakeAmount = simpleToExactAmount ( 100 , 18 ) ; before ( async ( ) => { await redeployRewards ( ) ; await expectSuccessfulFunding ( fundAmount ) ; await expectSuccessfulStake ( stakeAmount ) ; await increaseTime ( 10 ) ; } ) ; it ( "should revert for a non-staker" , async ( ) => { await expect ( stakingRewards . connect ( sa . dummy1 . signer ) . withdraw ( 1 ) ) . to . revertedWith ( "Not enough user rewards" ) ; } ) ; it ( "should revert if insufficient balance" , async ( ) => { await expect ( stakingRewards . connect ( sa . default . signer ) . withdraw ( stakeAmount . add ( 1 ) ) ) . to . revertedWith ( "Not enough user rewards" ) ; } ) ; it ( "should fail if trying to withdraw 0" , async ( ) => { await expect ( stakingRewards . connect ( sa . default . signer ) . withdraw ( 0 ) ) . to . revertedWith ( "Cannot withdraw 0" ) ; } ) ; it ( "should withdraw the stake and update the existing reward accrual" , async ( ) => { const earnedBefore = await stakingRewards . earned ( sa . default . address ) ; expect ( earnedBefore ) . gt ( 0 ) ; const { rewards : rewardsBefore } = await stakingRewards . userData ( sa . default . address ) ; expect ( rewardsBefore ) . eq ( 0 ) ; await expectStakingWithdrawal ( stakeAmount ) ; const earnedAfter = await stakingRewards . earned ( sa . default . address ) ; expect ( earnedAfter ) . gte ( earnedBefore ) ; const { rewards : rewardsAfter } = await stakingRewards . userData ( sa . default . address ) ; expect ( rewardsAfter ) . eq ( earnedAfter ) ; await increaseTime ( 10 ) ; const earnedEnd = await stakingRewards . earned ( sa . default . address ) ; expect ( earnedEnd ) . eq ( earnedAfter ) ; const { rewards : rewardsEnd } = await stakingRewards . userData ( sa . default . address ) ; expect ( rewardsEnd ) . eq ( rewardsAfter ) ; await expect ( stakingRewards . connect ( sa . default . signer ) . withdraw ( stakeAmount . add ( 1 ) ) ) . to . revertedWith ( "Not enough user rewards" ) ; } ) ; } ) ; context ( "claiming rewards" , async ( ) => { const fundAmount = simpleToExactAmount ( 100 , 21 ) ; const stakeAmount = simpleToExactAmount ( 100 , 18 ) ; before ( async ( ) => { await redeployRewards ( ) ; await expectSuccessfulFunding ( fundAmount ) ; await rewardToken . connect ( rewardsDistributor . signer ) . transfer ( stakingRewards . address , fundAmount ) ; await expectSuccessfulStake ( stakeAmount , sa . default , sa . dummy2 ) ; await increaseTime ( ONE_WEEK . add ( 1 ) ) ; } ) ; it ( "should do nothing for a non-staker" , async ( ) => { const beforeData = await snapshotStakingData ( sa . dummy1 , sa . dummy1 ) ; await stakingRewards . connect ( sa . dummy1 . signer ) . claimReward ( ) ; const afterData = await snapshotStakingData ( sa . dummy1 , sa . dummy1 ) ; expect ( beforeData . beneficiaryRewardsEarned ) . eq ( 0 ) ; expect ( afterData . beneficiaryRewardsEarned ) . eq ( 0 ) ; expect ( afterData . senderStakingTokenBalance ) . eq ( 0 ) ; expect ( afterData . userRewardPerTokenPaid ) . eq ( afterData . rewardPerTokenStored ) ; } ) ; it ( "should send all accrued rewards to the rewardee" , async ( ) => { const beforeData = await snapshotStakingData ( sa . dummy2 , sa . dummy2 ) ; const rewardeeBalanceBefore = await rewardToken . balanceOf ( sa . dummy2 . address ) ; expect ( rewardeeBalanceBefore ) . eq ( 0 ) ; const tx = await stakingRewards . connect ( sa . dummy2 . signer ) . claimReward ( ) ; await expect ( tx ) . to . emit ( stakingRewards , "RewardPaid" ) ; const afterData = await snapshotStakingData ( sa . dummy2 , sa . dummy2 ) ; await assertRewardsAssigned ( beforeData , afterData , false , true ) ; const rewardeeBalanceAfter = await rewardToken . balanceOf ( sa . dummy2 . address ) ; assertBNClose ( rewardeeBalanceAfter , fundAmount , simpleToExactAmount ( 1 , 16 ) ) ; expect ( afterData . beneficiaryRewardsEarned ) . eq ( 0 ) ; expect ( afterData . userRewardPerTokenPaid ) . eq ( afterData . rewardPerTokenStored ) ; expect ( afterData . senderStakingTokenBalance ) . eq ( beforeData . senderStakingTokenBalance ) ; expect ( beforeData . userStakingBalance ) . eq ( afterData . userStakingBalance ) ; } ) ; } ) ; context ( "completely 'exiting' the system" , ( ) => { const fundAmount = simpleToExactAmount ( 100 , 21 ) ; const stakeAmount = simpleToExactAmount ( 100 , 18 ) ; before ( async ( ) => { await redeployRewards ( ) ; await expectSuccessfulFunding ( fundAmount ) ; await rewardToken . connect ( rewardsDistributor . signer ) . transfer ( stakingRewards . address , fundAmount ) ; await expectSuccessfulStake ( stakeAmount ) ; await increaseTime ( ONE_WEEK . add ( 1 ) ) ; } ) ; it ( "should fail if the sender has no stake" , async ( ) => { await expect ( stakingRewards . connect ( sa . dummy1 . signer ) . exit ( ) ) . to . revertedWith ( "Cannot withdraw 0" ) ; } ) ; it ( "should withdraw all senders stake and send outstanding rewards to the staker" , async ( ) => { const beforeData = await snapshotStakingData ( ) ; const rewardeeBalanceBefore = await rewardToken . balanceOf ( sa . default . address ) ; const tx = await stakingRewards . exit ( ) ; await expect ( tx ) . to . emit ( stakingRewards , "Withdrawn" ) . withArgs ( sa . default . address , stakeAmount ) ; await expect ( tx ) . to . emit ( stakingRewards , "RewardPaid" ) ; const afterData = await snapshotStakingData ( ) ; const rewardeeBalanceAfter = await rewardToken . balanceOf ( sa . default . address ) ; assertBNClose ( rewardeeBalanceAfter . sub ( rewardeeBalanceBefore ) , fundAmount , simpleToExactAmount ( 1 , 16 ) ) ; await assertRewardsAssigned ( beforeData , afterData , false , true ) ; expect ( beforeData . senderStakingTokenBalance . add ( stakeAmount ) ) . eq ( afterData . senderStakingTokenBalance ) ; expect ( beforeData . userStakingBalance . sub ( stakeAmount ) ) . eq ( afterData . userStakingBalance ) ; expect ( beforeData . totalSupply . sub ( stakeAmount ) ) . eq ( afterData . totalSupply ) ; await expect ( stakingRewards . exit ( ) ) . to . revertedWith ( "Cannot withdraw 0" ) ; } ) ; } ) ; } ) ; context ( "running a full integration test" , ( ) => { const fundAmount = simpleToExactAmount ( 100 , 21 ) ; const stakeAmount = simpleToExactAmount ( 100 , 18 ) ; before ( async ( ) => { await redeployRewards ( ) ; } ) ; it ( "1. should allow the rewardsDistributor to fund the pool" , async ( ) => { await rewardToken . connect ( rewardsDistributor . signer ) . transfer ( stakingRewards . address , fundAmount ) ; await expectSuccessfulFunding ( fundAmount ) ; } ) ; it ( "2. should allow stakers to stake and earn rewards" , async ( ) => { await expectSuccessfulStake ( stakeAmount ) ; await increaseTime ( ONE_WEEK . add ( 1 ) ) ; } ) ; it ( "3. should credit earnings directly to beneficiary" , async ( ) => { const beforeData = await snapshotStakingData ( ) ; const beneficiaryBalanceBefore = await rewardToken . balanceOf ( sa . default . address ) ; await stakingRewards . exit ( ) ; const afterData = await snapshotStakingData ( ) ; const beneficiaryBalanceAfter = await rewardToken . balanceOf ( sa . default . address ) ; assertBNClose ( beneficiaryBalanceAfter . sub ( beneficiaryBalanceBefore ) , fundAmount , simpleToExactAmount ( 1 , 16 ) ) ; await assertRewardsAssigned ( beforeData , afterData , false , true ) ; } ) ; } ) ; } ) ;