import { ethers } from "hardhat" ; import { expect } from "chai" ; import { utils } from "ethers" ; import { simpleToExactAmount , BN } from "@utils/math" ; import { assertBNClose , assertBNClosePercent , assertBNSlightlyGT } from "@utils/assertions" ; import { StandardAccounts , MassetMachine } from "@utils/machines" ; import { fullScale , ZERO_ADDRESS , ONE_DAY , FIVE_DAYS , ONE_WEEK , DEAD_ADDRESS } from "@utils/constants" ; import { getTimestamp , increaseTime } from "@utils/time" ; import { MockERC20 , MockERC20__factory , ImmutableModule , MockStakingContract , MockStakingContract__factory , InitializableRewardsDistributionRecipient , BoostedVault , BoostedVault__factory , MockNexus , MockNexus__factory , AssetProxy__factory , BoostDirector__factory , BoostDirector , MockBoostedVault , MockBoostedVault__factory , } from "types/generated" ; import { Account } from "types" ; import { shouldBehaveLikeDistributionRecipient , IRewardsDistributionRecipientContext , } from "../shared/RewardsDistributionRecipient.behaviour" ; interface StakingBalance { raw : BN ; balance : BN ; totalSupply : BN ; } interface TokenBalance { sender : BN ; contract : BN ; } interface UserData { rewardPerTokenPaid : BN ; rewards : BN ; lastAction : BN ; rewardCount : number ; userClaim : BN ; } interface ContractData { rewardPerTokenStored : BN ; rewardRate : BN ; lastUpdateTime : BN ; lastTimeRewardApplicable : BN ; periodFinishTime : BN ; } interface Reward { start : BN ; finish : BN ; rate : BN ; } interface StakingData { boostBalance : StakingBalance ; tokenBalance : TokenBalance ; vMTABalance : BN ; userData : UserData ; userRewards : Reward [ ] ; contractData : ContractData ; } async function getUserReward ( savingsVault , beneficiary , i ) { const userReward = await savingsVault . userRewards ( beneficiary . address , i ) ; const [ start , finish , rate ] = userReward ; return { start , finish , rate } ; } describe ( "BoostedVault" , async ( ) => { const ctx = { } ; let sa ; let mAssetMachine ; let rewardsDistributor ; let rewardToken ; let imAsset ; let nexus ; let savingsVault ; let stakingContract ; let boostDirector ; const maxVMTA = simpleToExactAmount ( 600000 , 18 ) ; const maxBoost = simpleToExactAmount ( 3 , 18 ) ; const minBoost = simpleToExactAmount ( 1 , 18 ) ; const floor = simpleToExactAmount ( 98 , 16 ) ; const coeff = BN . from ( 9 ) ; const priceCoeff = simpleToExactAmount ( 1 , 17 ) ; const lockupPeriod = ONE_WEEK . mul ( 26 ) ; const boost = ( raw , boostAmt ) : BN => raw . mul ( boostAmt ) . div ( fullScale ) ; const calcBoost = ( raw , vMTA , priceCoefficient = priceCoeff ) : BN => { const scaledBalance = raw . mul ( priceCoefficient ) . div ( simpleToExactAmount ( 1 , 18 ) ) ; if ( scaledBalance . lt ( simpleToExactAmount ( 1 , 18 ) ) ) return minBoost ; let denom = parseFloat ( utils . formatUnits ( scaledBalance ) ) ; denom **= 0.75 ; const flooredMTA = vMTA . gt ( maxVMTA ) ? maxVMTA : vMTA ; let rhs = floor . add ( flooredMTA . mul ( coeff ) . div ( 10 ) . mul ( fullScale ) . div ( simpleToExactAmount ( denom ) ) ) ; rhs = rhs . gt ( minBoost ) ? rhs : minBoost ; return rhs . gt ( maxBoost ) ? maxBoost : rhs ; } ; const unlockedRewards = ( total ) : BN => total . div ( 100 ) . mul ( 33 ) ; const lockedRewards = ( total ) : BN => total . div ( 100 ) . mul ( 67 ) ; const redeployRewards = async ( priceCoefficient = priceCoeff ) : Promise < BoostedVault > => { nexus = await new MockNexus__factory ( sa . default . signer ) . deploy ( sa . governor . address , DEAD_ADDRESS , DEAD_ADDRESS ) ; rewardToken = await new MockERC20__factory ( sa . default . signer ) . deploy ( "Reward" , "RWD" , 18 , rewardsDistributor . address , 10000000 ) ; imAsset = await new MockERC20__factory ( sa . default . signer ) . deploy ( "Interest bearing mUSD" , "imUSD" , 18 , sa . default . address , 1000000 ) ; stakingContract = await new MockStakingContract__factory ( sa . default . signer ) . deploy ( ) ; boostDirector = await new BoostDirector__factory ( sa . default . signer ) . deploy ( nexus . address , stakingContract . address ) ; const vaultFactory = await new BoostedVault__factory ( sa . default . signer ) ; const vaultImpl = await vaultFactory . deploy ( nexus . address , imAsset . address , boostDirector . address , priceCoefficient , coeff , rewardToken . address ) ; const data = vaultImpl . interface . encodeFunctionData ( "initialize" , [ rewardsDistributor . address , "Vault A" , "vA" ] ) ; const vaultProxy = await new AssetProxy__factory ( sa . default . signer ) . deploy ( vaultImpl . address , sa . dummy4 . address , data ) ; await boostDirector . initialize ( [ vaultProxy . address ] ) ; return vaultFactory . attach ( vaultProxy . address ) ; } ; const snapshotStakingData = async ( sender = sa . default , beneficiary = sa . default ) : Promise < StakingData > => { const [ rewardPerTokenPaid , rewards , lastAction , rewardCount ] = await savingsVault . userData ( beneficiary . address ) ; const userRewards = [ ] ; for ( let i = 0 ; i < rewardCount . toNumber ( ) ; i += 1 ) { userRewards . push ( getUserReward ( savingsVault , beneficiary , i ) ) ; } return { boostBalance : { raw : await savingsVault . rawBalanceOf ( beneficiary . address ) , balance : await savingsVault . balanceOf ( beneficiary . address ) , totalSupply : await savingsVault . totalSupply ( ) , } , tokenBalance : { sender : await imAsset . balanceOf ( sender . address ) , contract : await imAsset . balanceOf ( savingsVault . address ) , } , vMTABalance : await stakingContract . balanceOf ( beneficiary . address ) , userData : { rewardPerTokenPaid , rewards , lastAction , rewardCount : rewardCount . toNumber ( ) , userClaim : await savingsVault . userClaim ( beneficiary . address ) , } , userRewards : await Promise . all ( userRewards ) , contractData : { rewardPerTokenStored : await savingsVault . rewardPerTokenStored ( ) , rewardRate : await savingsVault . rewardRate ( ) , lastUpdateTime : await savingsVault . lastUpdateTime ( ) , lastTimeRewardApplicable : await savingsVault . lastTimeRewardApplicable ( ) , periodFinishTime : await savingsVault . periodFinish ( ) , } , } ; } ; before ( async ( ) => { const accounts = await ethers . getSigners ( ) ; mAssetMachine = await new MassetMachine ( ) . initAccounts ( accounts ) ; sa = mAssetMachine . sa ; rewardsDistributor = sa . fundManager ; savingsVault = await redeployRewards ( ) ; ctx . recipient = savingsVault as unknown as InitializableRewardsDistributionRecipient ; ctx . module = savingsVault as ImmutableModule ; ctx . sa = sa ; } ) ; describe ( "constructor & settings" , async ( ) => { beforeEach ( async ( ) => { savingsVault = await redeployRewards ( ) ; } ) ; it ( "should set all initial state" , async ( ) => { expect ( await savingsVault . nexus ( ) ) . to . eq ( nexus . address ) ; expect ( await savingsVault . stakingToken ( ) ) . to . eq ( imAsset . address ) ; expect ( await savingsVault . boostDirector ( ) ) . to . eq ( boostDirector . address ) ; expect ( await savingsVault . rewardsToken ( ) ) . to . eq ( rewardToken . address ) ; expect ( await savingsVault . rewardsDistributor ( ) ) . to . eq ( rewardsDistributor . address ) ; expect ( await savingsVault . totalSupply ( ) ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( await savingsVault . periodFinish ( ) ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( await savingsVault . rewardRate ( ) ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( await savingsVault . lastUpdateTime ( ) ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( await savingsVault . rewardPerTokenStored ( ) ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( await savingsVault . lastTimeRewardApplicable ( ) ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( await savingsVault . rewardPerToken ( ) ) . to . be . eq ( BN . from ( 0 ) ) ; } ) ; shouldBehaveLikeDistributionRecipient ( ctx as IRewardsDistributionRecipientContext ) ; } ) ; 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 . contractData . periodFinishTime ) ; const lastUpdateTokenTime = beforeData . contractData . rewardPerTokenStored . eq ( 0 ) && beforeData . boostBalance . totalSupply . eq ( 0 ) ? beforeData . contractData . lastUpdateTime : timeAfter ; expect ( periodIsFinished ? beforeData . contractData . periodFinishTime : lastUpdateTokenTime ) . eq ( afterData . contractData . lastUpdateTime ) ; expect ( beforeData . contractData . rewardRate ) . to . be . eq ( afterData . contractData . rewardRate ) ; expect ( afterData . contractData . rewardPerTokenStored ) . to . be . gte ( beforeData . contractData . rewardPerTokenStored ) ; const timeApplicableToRewards = periodIsFinished ? beforeData . contractData . periodFinishTime . sub ( beforeData . contractData . lastUpdateTime ) : timeAfter . sub ( beforeData . contractData . lastUpdateTime ) ; const increaseInRewardPerToken = beforeData . boostBalance . totalSupply . eq ( BN . from ( 0 ) ) ? BN . from ( 0 ) : beforeData . contractData . rewardRate . mul ( timeApplicableToRewards ) . mul ( fullScale ) . div ( beforeData . boostBalance . totalSupply ) ; expect ( beforeData . contractData . rewardPerTokenStored . add ( increaseInRewardPerToken ) ) . to . be . eq ( afterData . contractData . rewardPerTokenStored ) ; expect ( afterData . userData . rewardPerTokenPaid ) . to . be . eq ( afterData . userData . rewardPerTokenPaid ) ; const increaseInUserRewardPerToken = afterData . contractData . rewardPerTokenStored . sub ( beforeData . userData . rewardPerTokenPaid ) ; const assignment = beforeData . boostBalance . balance . mul ( increaseInUserRewardPerToken ) . div ( fullScale ) ; if ( shouldResetRewards ) { expect ( afterData . userData . rewards ) . to . be . eq ( BN . from ( 0 ) ) ; } else if ( isExistingStaker ) { expect ( beforeData . userData . rewards . add ( unlockedRewards ( assignment ) ) ) . to . be . eq ( afterData . userData . rewards ) ; } else { expect ( beforeData . userData . rewards ) . to . be . eq ( afterData . userData . rewards ) ; } const newRewards = afterData . contractData . rewardPerTokenStored . gt ( beforeData . userData . rewardPerTokenPaid ) ; if ( isExistingStaker && newRewards ) { const newLockEntry = afterData . userRewards [ afterData . userData . rewardCount - 1 ] ; expect ( newLockEntry . start ) . to . be . eq ( beforeData . userData . lastAction . add ( lockupPeriod ) ) ; expect ( newLockEntry . finish ) . to . be . eq ( afterData . userData . lastAction . add ( lockupPeriod ) ) ; const elapsed = afterData . userData . lastAction . sub ( beforeData . userData . lastAction ) ; expect ( newLockEntry . rate ) . to . be . eq ( lockedRewards ( assignment ) . div ( elapsed ) ) ; expect ( afterData . userData . lastAction ) . to . be . eq ( timeAfter ) ; } else { expect ( beforeData . userRewards . length ) . eq ( afterData . userRewards . length ) ; expect ( beforeData . userData . rewardCount ) . eq ( afterData . userData . rewardCount ) ; expect ( afterData . userData . lastAction ) . to . be . eq ( timeAfter ) ; expect ( beforeData . userData . userClaim ) . to . be . eq ( afterData . userData . userClaim ) ; } } ; 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 . boostBalance . raw . gt ( BN . from ( 0 ) ) ; if ( confirmExistingStaker ) { expect ( isExistingStaker ) . eq ( true ) ; } await imAsset . connect ( sender . signer ) . approve ( savingsVault . address , stakeAmount ) ; const tx = senderIsBeneficiary ? savingsVault . connect ( sender . signer ) [ "stake(uint256)" ] ( stakeAmount ) : savingsVault . connect ( sender . signer ) [ "stake(address,uint256)" ] ( beneficiary . address , stakeAmount ) ; await expect ( tx ) . to . emit ( savingsVault , "Staked" ) . withArgs ( beneficiary . address , stakeAmount , sender . address ) ; const afterData = await snapshotStakingData ( sender , beneficiary ) ; const expectedBoost = boost ( afterData . boostBalance . raw , calcBoost ( afterData . boostBalance . raw , afterData . vMTABalance ) ) ; await assertRewardsAssigned ( beforeData , afterData , isExistingStaker ) ; expect ( beforeData . tokenBalance . sender . sub ( stakeAmount ) ) . to . be . eq ( afterData . tokenBalance . sender ) ; expect ( beforeData . tokenBalance . contract . add ( stakeAmount ) ) . to . be . eq ( afterData . tokenBalance . contract ) ; expect ( beforeData . boostBalance . totalSupply . sub ( beforeData . boostBalance . balance ) . add ( expectedBoost ) , "Boost should affect total supply" ) . to . be . eq ( afterData . boostBalance . totalSupply ) ; } ; dev param rewardUnits const expectSuccesfulFunding = async ( rewardUnits ) : Promise < void > => { const beforeData = await snapshotStakingData ( ) ; const tx = savingsVault . connect ( rewardsDistributor . signer ) . notifyRewardAmount ( rewardUnits ) ; await expect ( tx ) . to . emit ( savingsVault , "RewardAdded" ) . withArgs ( rewardUnits ) ; const cur = BN . from ( await getTimestamp ( ) ) ; const leftOverRewards = beforeData . contractData . rewardRate . mul ( beforeData . contractData . periodFinishTime . sub ( beforeData . contractData . lastTimeRewardApplicable ) ) ; const afterData = await snapshotStakingData ( ) ; expect ( cur ) . to . be . eq ( afterData . contractData . lastTimeRewardApplicable ) ; expect ( cur ) . to . be . eq ( afterData . contractData . lastUpdateTime ) ; expect ( cur . add ( ONE_WEEK ) ) . to . be . eq ( afterData . contractData . periodFinishTime ) ; if ( leftOverRewards . gt ( 0 ) ) { const total = rewardUnits . add ( leftOverRewards ) ; assertBNClose ( total . div ( ONE_WEEK ) , afterData . contractData . rewardRate , beforeData . contractData . rewardRate . div ( ONE_WEEK ) . mul ( 5 ) ) ; } else { expect ( rewardUnits . div ( ONE_WEEK ) ) . to . be . eq ( afterData . contractData . rewardRate ) ; } } ; dev param withdrawAmount param sender const expectStakingWithdrawal = async ( withdrawAmount , sender = sa . default ) : Promise < void > => { const beforeData = await snapshotStakingData ( sender ) ; const isExistingStaker = beforeData . boostBalance . raw . gt ( BN . from ( 0 ) ) ; expect ( isExistingStaker ) . eq ( true ) ; expect ( withdrawAmount ) . to . be . gte ( beforeData . boostBalance . raw ) ; const tx = savingsVault . connect ( sender . signer ) . withdraw ( withdrawAmount ) ; await expect ( tx ) . to . emit ( savingsVault , "Withdrawn" ) . withArgs ( sender . address , withdrawAmount ) ; const afterData = await snapshotStakingData ( sender ) ; await assertRewardsAssigned ( beforeData , afterData , isExistingStaker ) ; expect ( beforeData . tokenBalance . sender . add ( withdrawAmount ) ) . to . be . eq ( afterData . tokenBalance . sender ) ; expect ( beforeData . boostBalance . raw . sub ( withdrawAmount ) ) . to . be . eq ( afterData . boostBalance . raw ) ; expect ( beforeData . boostBalance . totalSupply . sub ( beforeData . boostBalance . balance ) . add ( afterData . boostBalance . balance ) ) . to . be . eq ( afterData . boostBalance . totalSupply ) ; } ; context ( "initialising and staking in a new pool" , ( ) => { describe ( "notifying the pool of reward" , ( ) => { it ( "should begin a new period through" , async ( ) => { const rewardUnits = simpleToExactAmount ( 1 , 18 ) ; await expectSuccesfulFunding ( rewardUnits ) ; } ) ; } ) ; describe ( "staking in the new period" , ( ) => { it ( "should assign rewards to the staker" , async ( ) => { const rewardRate = await savingsVault . rewardRate ( ) ; const stakeAmount = simpleToExactAmount ( 100 , 18 ) ; const boosted = boost ( stakeAmount , minBoost ) ; await expectSuccessfulStake ( stakeAmount ) ; expect ( boosted ) . to . be . eq ( await savingsVault . balanceOf ( sa . default . address ) ) ; await increaseTime ( ONE_DAY ) ; const rewardPerToken = await savingsVault . rewardPerToken ( ) ; const rewardPerSecond = rewardRate . mul ( fullScale ) . div ( boosted ) ; assertBNClose ( rewardPerToken , ONE_DAY . mul ( rewardPerSecond ) , rewardPerSecond . mul ( 10 ) ) ; const earned = await savingsVault . earned ( sa . default . address ) ; expect ( unlockedRewards ( boosted . mul ( rewardPerToken ) . div ( fullScale ) ) ) . to . be . eq ( earned ) ; await stakingContract . setBalanceOf ( sa . default . address , simpleToExactAmount ( 1 , 21 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; } ) ; 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 ( savingsVault [ "stake(uint256)" ] ( 0 ) ) . to . be . revertedWith ( "Cannot stake 0" ) ; } ) ; it ( "should fail if beneficiary is empty" , async ( ) => { await expect ( savingsVault . connect ( sa . default . signer ) [ "stake(address,uint256)" ] ( ZERO_ADDRESS , 1 ) ) . to . be . revertedWith ( "Invalid beneficiary address" ) ; } ) ; it ( "should fail if staker has insufficient balance" , async ( ) => { await imAsset . connect ( sa . dummy2 . signer ) . approve ( savingsVault . address , 1 ) ; await expect ( savingsVault . connect ( sa . dummy2 . signer ) [ "stake(uint256)" ] ( 1 ) ) . to . be . revertedWith ( "VM Exception" ) ; } ) ; } ) ; } ) ; context ( "funding with too much rewards" , ( ) => { before ( async ( ) => { savingsVault = await redeployRewards ( ) ; } ) ; it ( "should fail" , async ( ) => { await expect ( savingsVault . connect ( sa . fundManager . signer ) . notifyRewardAmount ( simpleToExactAmount ( 1 , 25 ) ) ) . to . be . revertedWith ( "Cannot notify with more than a million units" ) ; } ) ; } ) ; context ( "staking before rewards are added" , ( ) => { before ( async ( ) => { savingsVault = await redeployRewards ( ) ; } ) ; it ( "should assign no rewards" , async ( ) => { const stakeAmount = simpleToExactAmount ( 100 , 18 ) ; const beforeData = await snapshotStakingData ( ) ; expect ( beforeData . contractData . rewardRate ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( beforeData . contractData . rewardPerTokenStored ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( beforeData . userData . rewards ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( beforeData . boostBalance . totalSupply ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( beforeData . contractData . lastTimeRewardApplicable ) . to . be . eq ( BN . from ( 0 ) ) ; await expectSuccessfulStake ( stakeAmount ) ; await increaseTime ( ONE_DAY ) ; await expectSuccessfulStake ( stakeAmount ) ; const afterData = await snapshotStakingData ( ) ; expect ( afterData . contractData . rewardRate ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( afterData . contractData . rewardPerTokenStored ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( afterData . userData . rewards ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( afterData . boostBalance . totalSupply ) . to . be . eq ( stakeAmount . mul ( 2 ) ) ; expect ( afterData . contractData . lastTimeRewardApplicable ) . to . be . eq ( BN . from ( 0 ) ) ; } ) ; } ) ; context ( "calculating a users boost" , async ( ) => { context ( "with a price coefficient of 30000 bucks" , ( ) => { const priceCoeffOverride = simpleToExactAmount ( 3000 , 18 ) ; beforeEach ( async ( ) => { savingsVault = await redeployRewards ( priceCoeffOverride ) ; } ) ; it ( "should calculate boost for 10k imUSD stake and 250 vMTA" , async ( ) => { const deposit = simpleToExactAmount ( 3333 , 14 ) ; const stake = simpleToExactAmount ( 250 , 18 ) ; const expectedBoost = simpleToExactAmount ( 7483 , 14 ) ; await expectSuccessfulStake ( deposit ) ; await stakingContract . setBalanceOf ( sa . default . address , stake ) ; await savingsVault . pokeBoost ( sa . default . address ) ; const balance = await savingsVault . balanceOf ( sa . default . address ) ; assertBNClosePercent ( balance , expectedBoost , "0.1" ) ; assertBNClosePercent ( boost ( deposit , calcBoost ( deposit , stake , priceCoeffOverride ) ) , expectedBoost , "0.1" ) ; const ratio = await savingsVault . getBoost ( sa . default . address ) ; assertBNClosePercent ( ratio , simpleToExactAmount ( 2.2453 ) ) ; } ) ; it ( "should calculate boost for 10k imUSD stake and 50 vMTA" , async ( ) => { const deposit = simpleToExactAmount ( 3333 , 14 ) ; const stake = simpleToExactAmount ( 50 , 18 ) ; const expectedBoost = simpleToExactAmount ( 4110 , 14 ) ; await expectSuccessfulStake ( deposit ) ; await stakingContract . setBalanceOf ( sa . default . address , stake ) ; await savingsVault . pokeBoost ( sa . default . address ) ; const balance = await savingsVault . balanceOf ( sa . default . address ) ; assertBNClosePercent ( balance , expectedBoost , "1" ) ; assertBNClosePercent ( boost ( deposit , calcBoost ( deposit , stake , priceCoeffOverride ) ) , expectedBoost , "0.1" ) ; const ratio = await savingsVault . getBoost ( sa . default . address ) ; assertBNClosePercent ( ratio , simpleToExactAmount ( 1.2331 ) , "0.1" ) ; } ) ; it ( "should calculate boost for 100k imUSD stake and 500 vMTA" , async ( ) => { const deposit = simpleToExactAmount ( 3333 , 15 ) ; const stake = simpleToExactAmount ( 500 , 18 ) ; const expectedBoost = simpleToExactAmount ( "4766.19" , 15 ) ; await expectSuccessfulStake ( deposit ) ; await stakingContract . setBalanceOf ( sa . default . address , stake ) ; await savingsVault . pokeBoost ( sa . default . address ) ; const balance = await savingsVault . balanceOf ( sa . default . address ) ; assertBNClosePercent ( balance , expectedBoost , "1" ) ; assertBNClosePercent ( boost ( deposit , calcBoost ( deposit , stake , priceCoeffOverride ) ) , expectedBoost , "0.1" ) ; const ratio = await savingsVault . getBoost ( sa . default . address ) ; assertBNClosePercent ( ratio , simpleToExactAmount ( 1.43 , 18 ) , "0.1" ) ; } ) ; } ) ; context ( "with a price coefficient of 10 cents" , ( ) => { beforeEach ( async ( ) => { savingsVault = await redeployRewards ( ) ; } ) ; describe ( "when saving and with staking balance" , ( ) => { it ( "should calculate boost for 10k imUSD stake and 250 vMTA" , async ( ) => { const deposit = simpleToExactAmount ( 10000 ) ; const stake = simpleToExactAmount ( 250 , 18 ) ; const expectedBoost = simpleToExactAmount ( 22453 ) ; await expectSuccessfulStake ( deposit ) ; await stakingContract . setBalanceOf ( sa . default . address , stake ) ; await savingsVault . pokeBoost ( sa . default . address ) ; const balance = await savingsVault . balanceOf ( sa . default . address ) ; assertBNClosePercent ( balance , expectedBoost ) ; assertBNClosePercent ( boost ( deposit , calcBoost ( deposit , stake ) ) , expectedBoost , 0.1 ) ; const ratio = await savingsVault . getBoost ( sa . default . address ) ; assertBNClosePercent ( ratio , simpleToExactAmount ( 2.2453 ) ) ; } ) ; it ( "should calculate boost for 10k imUSD stake and 50 vMTA" , async ( ) => { const deposit = simpleToExactAmount ( 10000 , 18 ) ; const stake = simpleToExactAmount ( 50 , 18 ) ; const expectedBoost = simpleToExactAmount ( 12331 , 18 ) ; await expectSuccessfulStake ( deposit ) ; await stakingContract . setBalanceOf ( sa . default . address , stake ) ; await savingsVault . pokeBoost ( sa . default . address ) ; const balance = await savingsVault . balanceOf ( sa . default . address ) ; assertBNClosePercent ( balance , expectedBoost , "1" ) ; assertBNClosePercent ( boost ( deposit , calcBoost ( deposit , stake ) ) , expectedBoost , "0.1" ) ; const ratio = await savingsVault . getBoost ( sa . default . address ) ; assertBNClosePercent ( ratio , simpleToExactAmount ( 1.2331 ) , "0.1" ) ; } ) ; it ( "should calculate boost for 100k imUSD stake and 500 vMTA" , async ( ) => { const deposit = simpleToExactAmount ( 100000 , 18 ) ; const stake = simpleToExactAmount ( 500 , 18 ) ; const expectedBoost = simpleToExactAmount ( 143000 , 18 ) ; await expectSuccessfulStake ( deposit ) ; await stakingContract . setBalanceOf ( sa . default . address , stake ) ; await savingsVault . pokeBoost ( sa . default . address ) ; const balance = await savingsVault . balanceOf ( sa . default . address ) ; assertBNClosePercent ( balance , expectedBoost , "1" ) ; assertBNClosePercent ( boost ( deposit , calcBoost ( deposit , stake ) ) , expectedBoost , "0.1" ) ; const ratio = await savingsVault . getBoost ( sa . default . address ) ; assertBNClosePercent ( ratio , simpleToExactAmount ( 1.43 , 18 ) , "0.1" ) ; } ) ; } ) ; describe ( "when saving with low staking balance and high vMTA" , ( ) => { it ( "should give no boost due to below min threshold" , async ( ) => { const deposit = simpleToExactAmount ( 5 , 17 ) ; const stake = simpleToExactAmount ( 800 , 18 ) ; const expectedBoost = simpleToExactAmount ( 5 , 17 ) ; await expectSuccessfulStake ( deposit ) ; await stakingContract . setBalanceOf ( sa . default . address , stake ) ; await savingsVault . pokeBoost ( sa . default . address ) ; const balance = await savingsVault . balanceOf ( sa . default . address ) ; assertBNClosePercent ( balance , expectedBoost , "1" ) ; assertBNClosePercent ( boost ( deposit , calcBoost ( deposit , stake ) ) , expectedBoost , "0.1" ) ; const ratio = await savingsVault . getBoost ( sa . default . address ) ; assertBNClosePercent ( ratio , minBoost , "0.1" ) ; } ) ; } ) ; describe ( "when saving and with staking balance = 0" , ( ) => { it ( "should give no boost" , async ( ) => { const deposit = simpleToExactAmount ( 100 , 18 ) ; await expectSuccessfulStake ( deposit ) ; const balance = await savingsVault . balanceOf ( sa . default . address ) ; assertBNClosePercent ( balance , deposit , "1" ) ; assertBNClosePercent ( boost ( deposit , minBoost ) , deposit , "0.1" ) ; const ratio = await savingsVault . getBoost ( sa . default . address ) ; assertBNClosePercent ( ratio , minBoost , "0.1" ) ; } ) ; } ) ; describe ( "when withdrawing and with staking balance" , ( ) => { it ( "should set boost to 0 and update total supply" , async ( ) => { const deposit = simpleToExactAmount ( 100 , 18 ) ; const stake = simpleToExactAmount ( 800 , 18 ) ; await expectSuccessfulStake ( deposit ) ; await stakingContract . setBalanceOf ( sa . default . address , stake ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await increaseTime ( ONE_WEEK ) ; await savingsVault [ "exit()" ] ( ) ; const balance = await savingsVault . balanceOf ( sa . default . address ) ; const raw = await savingsVault . rawBalanceOf ( sa . default . address ) ; const supply = await savingsVault . totalSupply ( ) ; expect ( balance ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( raw ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( supply ) . to . be . eq ( BN . from ( 0 ) ) ; } ) ; } ) ; describe ( "when staking and then updating vMTA balance" , ( ) => { it ( "should start accruing more rewards" , async ( ) => { const alice = sa . default ; const bob = sa . dummy1 ; const hunnit = simpleToExactAmount ( 100 , 18 ) ; await rewardToken . connect ( rewardsDistributor . signer ) . transfer ( savingsVault . address , hunnit ) ; await expectSuccesfulFunding ( hunnit ) ; await expectSuccessfulStake ( hunnit ) ; await expectSuccessfulStake ( hunnit , sa . default , bob ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await stakingContract . setBalanceOf ( alice . address , hunnit ) ; await savingsVault . pokeBoost ( alice . address ) ; await savingsVault . pokeBoost ( bob . address ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await savingsVault . pokeBoost ( alice . address ) ; await savingsVault . pokeBoost ( bob . address ) ; const aliceData = await snapshotStakingData ( alice , alice ) ; const bobData = await snapshotStakingData ( bob , bob ) ; assertBNClosePercent ( aliceData . userRewards [ 1 ] . rate , bobData . userRewards [ 1 ] . rate . mul ( 3 ) , "0.1" ) ; } ) ; } ) ; } ) ; } ) ; context ( "adding first stake days after funding" , ( ) => { before ( async ( ) => { savingsVault = await redeployRewards ( ) ; } ) ; it ( "should retrospectively assign rewards to the first staker" , async ( ) => { await expectSuccesfulFunding ( simpleToExactAmount ( 100 , 18 ) ) ; const rewardRate = await savingsVault . rewardRate ( ) ; await increaseTime ( FIVE_DAYS ) ; const stakeAmount = simpleToExactAmount ( 100 , 18 ) ; const boosted = boost ( stakeAmount , minBoost ) ; await expectSuccessfulStake ( stakeAmount ) ; const rewardPerToken = await savingsVault . rewardPerToken ( ) ; const rewardPerSecond = rewardRate . mul ( fullScale ) . div ( boosted ) ; assertBNClosePercent ( rewardPerToken , FIVE_DAYS . mul ( rewardPerSecond ) , "0.01" ) ; const earnedAfterConsequentStake = await savingsVault . earned ( sa . default . address ) ; expect ( unlockedRewards ( boosted . mul ( rewardPerToken ) . div ( fullScale ) ) ) . to . be . eq ( earnedAfterConsequentStake ) ; await stakingContract . setBalanceOf ( sa . default . address , simpleToExactAmount ( 1 , 21 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; } ) ; } ) ; context ( "staking over multiple funded periods" , ( ) => { context ( "with a single staker" , ( ) => { before ( async ( ) => { savingsVault = await redeployRewards ( ) ; } ) ; it ( "should assign all the rewards from the periods" , async ( ) => { const fundAmount1 = simpleToExactAmount ( 100 , 18 ) ; const fundAmount2 = simpleToExactAmount ( 200 , 18 ) ; await expectSuccesfulFunding ( fundAmount1 ) ; const stakeAmount = simpleToExactAmount ( 1 , 18 ) ; await expectSuccessfulStake ( stakeAmount ) ; await increaseTime ( ONE_WEEK . mul ( 2 ) ) ; await expectSuccesfulFunding ( fundAmount2 ) ; await increaseTime ( ONE_WEEK . mul ( 2 ) ) ; const earned = await savingsVault . earned ( sa . default . address ) ; assertBNSlightlyGT ( unlockedRewards ( fundAmount1 . add ( fundAmount2 ) ) , earned , BN . from ( 1000000 ) , false ) ; await stakingContract . setBalanceOf ( sa . default . address , simpleToExactAmount ( 1 , 21 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; } ) ; } ) ; context ( "with multiple stakers coming in and out" , ( ) => { const fundAmount1 = simpleToExactAmount ( 100 , 21 ) ; const fundAmount2 = simpleToExactAmount ( 200 , 21 ) ; let staker2 ; let staker3 ; const staker1Stake1 = simpleToExactAmount ( 100 , 18 ) ; const staker1Stake2 = simpleToExactAmount ( 200 , 18 ) ; const staker2Stake = simpleToExactAmount ( 100 , 18 ) ; const staker3Stake = simpleToExactAmount ( 100 , 18 ) ; before ( async ( ) => { savingsVault = await redeployRewards ( ) ; staker2 = sa . dummy1 ; staker3 = sa . dummy2 ; await imAsset . transfer ( staker2 . address , staker2Stake ) ; await imAsset . transfer ( staker3 . address , staker3Stake ) ; } ) ; it ( "should accrue rewards on a pro rata basis" , async ( ) => { await expectSuccessfulStake ( staker1Stake1 ) ; await expectSuccessfulStake ( staker3Stake , staker3 , staker3 ) ; await expectSuccesfulFunding ( fundAmount1 ) ; await increaseTime ( ONE_WEEK . div ( 2 ) . add ( 1 ) ) ; await expectSuccessfulStake ( staker2Stake , staker2 , staker2 ) ; await increaseTime ( ONE_WEEK . div ( 2 ) . add ( 1 ) ) ; await expectSuccesfulFunding ( fundAmount2 ) ; await savingsVault . connect ( staker3 . signer ) . withdraw ( staker3Stake ) ; await expectSuccessfulStake ( staker1Stake2 , sa . default , sa . default , true ) ; await increaseTime ( ONE_WEEK ) ; const earned1 = await savingsVault . earned ( sa . default . address ) ; assertBNClose ( earned1 , unlockedRewards ( simpleToExactAmount ( "191.66" , 21 ) ) , simpleToExactAmount ( 1 , 19 ) ) ; const earned2 = await savingsVault . earned ( staker2 . address ) ; assertBNClose ( earned2 , unlockedRewards ( simpleToExactAmount ( "66.66" , 21 ) ) , simpleToExactAmount ( 1 , 19 ) ) ; const earned3 = await savingsVault . earned ( staker3 . address ) ; assertBNClose ( earned3 , unlockedRewards ( simpleToExactAmount ( "41.66" , 21 ) ) , simpleToExactAmount ( 1 , 19 ) ) ; expect ( fundAmount1 . add ( fundAmount2 ) ) . to . be . gte ( earned1 . add ( earned2 ) . add ( earned3 ) ) ; } ) ; } ) ; } ) ; context ( "staking after period finish" , ( ) => { const fundAmount1 = simpleToExactAmount ( 100 , 21 ) ; before ( async ( ) => { savingsVault = await redeployRewards ( ) ; } ) ; it ( "should stop accruing rewards after the period is over" , async ( ) => { await expectSuccessfulStake ( simpleToExactAmount ( 1 , 18 ) ) ; await expectSuccesfulFunding ( fundAmount1 ) ; await increaseTime ( ONE_WEEK . add ( 1 ) ) ; const earnedAfterWeek = await savingsVault . earned ( sa . default . address ) ; await increaseTime ( ONE_WEEK . add ( 1 ) ) ; const now = await getTimestamp ( ) ; const earnedAfterTwoWeeks = await savingsVault . earned ( sa . default . address ) ; expect ( earnedAfterWeek ) . to . be . eq ( earnedAfterTwoWeeks ) ; const lastTimeRewardApplicable = await savingsVault . 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 ( ) => { savingsVault = await redeployRewards ( ) ; beneficiary = sa . dummy1 ; await expectSuccesfulFunding ( fundAmount ) ; await expectSuccessfulStake ( stakeAmount , sa . default , beneficiary ) ; await increaseTime ( 10 ) ; } ) ; it ( "should update the beneficiaries reward details" , async ( ) => { const earned = await savingsVault . earned ( beneficiary . address ) ; expect ( earned ) . to . be . gt ( BN . from ( 0 ) ) ; const rawBalance = await savingsVault . rawBalanceOf ( beneficiary . address ) ; expect ( rawBalance ) . to . be . eq ( stakeAmount ) ; const balance = await savingsVault . balanceOf ( beneficiary . address ) ; expect ( balance ) . to . be . eq ( boost ( stakeAmount , minBoost ) ) ; } ) ; it ( "should not update the senders details" , async ( ) => { const earned = await savingsVault . earned ( sa . default . address ) ; expect ( earned ) . to . be . eq ( BN . from ( 0 ) ) ; const balance = await savingsVault . balanceOf ( sa . default . address ) ; expect ( balance ) . to . be . eq ( BN . from ( 0 ) ) ; } ) ; } ) ; context ( "using staking / reward tokens with diff decimals" , ( ) => { before ( async ( ) => { rewardToken = await new MockERC20__factory ( sa . default . signer ) . deploy ( "Reward" , "RWD" , 12 , rewardsDistributor . address , 10000000 ) ; imAsset = await new MockERC20__factory ( sa . default . signer ) . deploy ( "Interest bearing mUSD" , "imUSD" , 16 , sa . default . address , 1000000 ) ; stakingContract = await new MockStakingContract__factory ( sa . default . signer ) . deploy ( ) ; boostDirector = await new BoostDirector__factory ( sa . default . signer ) . deploy ( nexus . address , stakingContract . address ) ; const vaultFactory = new BoostedVault__factory ( sa . default . signer ) ; const impl = await vaultFactory . deploy ( nexus . address , imAsset . address , boostDirector . address , priceCoeff , coeff , rewardToken . address ) ; const data = impl . interface . encodeFunctionData ( "initialize" , [ rewardsDistributor . address , "Vault A" , "vA" ] ) ; const proxy = await new AssetProxy__factory ( sa . default . signer ) . deploy ( impl . address , sa . dummy4 . address , data ) ; savingsVault = vaultFactory . attach ( proxy . address ) ; await boostDirector . initialize ( [ proxy . address ] ) ; } ) ; it ( "should not affect the pro rata payouts" , async ( ) => { await expectSuccesfulFunding ( simpleToExactAmount ( 100 , 12 ) ) ; const rewardRate = await savingsVault . rewardRate ( ) ; const stakeAmount = simpleToExactAmount ( 100 , 16 ) ; const boosted = boost ( stakeAmount , minBoost ) ; await expectSuccessfulStake ( stakeAmount ) ; await increaseTime ( ONE_WEEK . add ( 1 ) ) ; const rewardPerToken = await savingsVault . rewardPerToken ( ) ; assertBNClose ( rewardPerToken , ONE_WEEK . mul ( rewardRate ) . mul ( fullScale ) . div ( boosted ) , BN . from ( 1 ) . mul ( rewardRate ) . mul ( fullScale ) . div ( boosted ) ) ; const earnedAfterConsequentStake = await savingsVault . earned ( sa . default . address ) ; assertBNSlightlyGT ( unlockedRewards ( simpleToExactAmount ( 100 , 12 ) ) , earnedAfterConsequentStake , simpleToExactAmount ( 1 , 9 ) ) ; } ) ; } ) ; context ( "claiming rewards" , async ( ) => { const fundAmount = simpleToExactAmount ( 100 , 21 ) ; const stakeAmount = simpleToExactAmount ( 100 , 18 ) ; const unlocked = unlockedRewards ( fundAmount ) ; before ( async ( ) => { savingsVault = await redeployRewards ( ) ; await expectSuccesfulFunding ( fundAmount ) ; await rewardToken . connect ( rewardsDistributor . signer ) . transfer ( savingsVault . 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 savingsVault . connect ( sa . dummy1 . signer ) . claimReward ( ) ; const afterData = await snapshotStakingData ( sa . dummy1 , sa . dummy1 ) ; expect ( beforeData . userData . rewards ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( afterData . userData . rewards ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( afterData . tokenBalance . sender ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( afterData . userData . rewardPerTokenPaid ) . to . be . eq ( afterData . contractData . rewardPerTokenStored ) ; } ) ; it ( "should send all UNLOCKED rewards to the rewardee" , async ( ) => { const beforeData = await snapshotStakingData ( sa . dummy2 , sa . dummy2 ) ; const rewardeeBalanceBefore = await rewardToken . balanceOf ( sa . dummy2 . address ) ; expect ( rewardeeBalanceBefore ) . to . be . eq ( BN . from ( 0 ) ) ; const tx = savingsVault . connect ( sa . dummy2 . signer ) . claimReward ( ) ; await expect ( tx ) . to . emit ( savingsVault , "RewardPaid" ) ; const afterData = await snapshotStakingData ( sa . dummy2 , sa . dummy2 ) ; await assertRewardsAssigned ( beforeData , afterData , true , true ) ; const rewardeeBalanceAfter = await rewardToken . balanceOf ( sa . dummy2 . address ) ; assertBNClose ( rewardeeBalanceAfter , unlocked , simpleToExactAmount ( 1 , 16 ) ) ; expect ( afterData . userData . rewards ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( afterData . userData . rewardPerTokenPaid ) . to . be . eq ( afterData . contractData . rewardPerTokenStored ) ; expect ( afterData . tokenBalance . sender ) . to . be . eq ( beforeData . tokenBalance . sender ) ; expect ( beforeData . boostBalance . balance ) . to . be . eq ( afterData . boostBalance . balance ) ; } ) ; } ) ; context ( "claiming locked rewards" , ( ) => { const hunnit = simpleToExactAmount ( 100 , 21 ) ; const sum = hunnit . mul ( 4 ) ; const unlocked = unlockedRewards ( sum ) ; beforeEach ( async ( ) => { savingsVault = await redeployRewards ( ) ; await rewardToken . connect ( rewardsDistributor . signer ) . transfer ( savingsVault . address , hunnit . mul ( 5 ) ) ; await expectSuccesfulFunding ( hunnit ) ; await expectSuccessfulStake ( hunnit ) ; await increaseTime ( ONE_WEEK . add ( 1 ) ) ; await expectSuccesfulFunding ( hunnit ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await increaseTime ( ONE_WEEK . add ( 1 ) ) ; await expectSuccesfulFunding ( hunnit . mul ( 2 ) ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await expectSuccesfulFunding ( hunnit ) ; } ) ; it ( "should fetch the unclaimed tranche data" , async ( ) => { await expectStakingWithdrawal ( hunnit ) ; await increaseTime ( ONE_WEEK . mul ( 23 ) ) ; let [ amount , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; assertBNClosePercent ( amount , unlocked , "0.01" ) ; expect ( first ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 0 ) ) ; await increaseTime ( ONE_WEEK . mul ( 3 ) . div ( 2 ) ) ; [ amount , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 1 ) ) ; assertBNClosePercent ( amount , unlocked . add ( lockedRewards ( simpleToExactAmount ( 166.666 , 21 ) ) ) , "0.01" ) ; await increaseTime ( ONE_WEEK . mul ( 5 ) . div ( 2 ) ) ; [ amount , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 2 ) ) ; assertBNClosePercent ( amount , unlocked . add ( lockedRewards ( simpleToExactAmount ( 400 , 21 ) ) ) , "0.01" ) ; } ) ; it ( "should claim all unlocked rewards over the tranches, and any immediate unlocks" , async ( ) => { await expectStakingWithdrawal ( hunnit ) ; await increaseTime ( ONE_WEEK . mul ( 23 ) ) ; await increaseTime ( ONE_WEEK . mul ( 3 ) . div ( 2 ) ) ; const expected = lockedRewards ( simpleToExactAmount ( 166.666 , 21 ) ) ; const allRewards = unlocked . add ( expected ) ; let [ amount , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 1 ) ) ; assertBNClosePercent ( amount , allRewards , "0.01" ) ; const dataBefore = await snapshotStakingData ( ) ; const t27x5 = await getTimestamp ( ) ; const tx = savingsVault [ "claimRewards(uint256,uint256)" ] ( first , last ) ; await expect ( tx ) . to . emit ( savingsVault , "RewardPaid" ) ; [ amount , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 1 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 1 ) ) ; expect ( amount ) . to . be . eq ( BN . from ( 0 ) ) ; const dataAfter = await snapshotStakingData ( ) ; expect ( dataAfter . boostBalance . totalSupply ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( dataAfter . tokenBalance . sender ) . to . be . eq ( dataBefore . tokenBalance . sender . add ( amount ) ) ; expect ( dataAfter . userData . lastAction ) . to . be . eq ( dataAfter . userData . userClaim ) ; assertBNClose ( t27x5 , dataAfter . userData . lastAction , 5 ) ; expect ( dataAfter . userData . rewards ) . to . be . eq ( BN . from ( 0 ) ) ; await expect ( savingsVault [ "claimRewards(uint256,uint256)" ] ( 0 , 0 ) ) . to . be . revertedWith ( "Invalid epoch" ) ; await increaseTime ( 100 ) ; [ amount , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 1 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 1 ) ) ; assertBNClose ( amount , dataAfter . userRewards [ 1 ] . rate . mul ( 100 ) , dataAfter . userRewards [ 1 ] . rate . mul ( 3 ) ) ; await savingsVault [ "claimRewards(uint256,uint256)" ] ( 1 , 1 ) ; await increaseTime ( ONE_DAY . mul ( 10 ) ) ; await savingsVault [ "claimRewards(uint256,uint256)" ] ( 1 , 1 ) ; const d3 = await snapshotStakingData ( ) ; expect ( d3 . userData . userClaim ) . to . be . eq ( d3 . userRewards [ 1 ] . finish ) ; await savingsVault [ "claimRewards(uint256,uint256)" ] ( 1 , 1 ) ; const d4 = await snapshotStakingData ( ) ; expect ( d4 . userData . userClaim ) . to . be . eq ( d4 . userRewards [ 1 ] . finish ) ; expect ( d4 . tokenBalance . sender ) . to . be . eq ( d3 . tokenBalance . sender ) ; } ) ; it ( "should claim rewards without being passed the params" , async ( ) => { await expectStakingWithdrawal ( hunnit ) ; await increaseTime ( ONE_WEEK . mul ( 23 ) ) ; await increaseTime ( ONE_WEEK . mul ( 3 ) . div ( 2 ) ) ; const expected = lockedRewards ( simpleToExactAmount ( 166.666 , 21 ) ) ; const allRewards = unlocked . add ( expected ) ; let [ amount , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 1 ) ) ; assertBNClosePercent ( amount , allRewards , "0.01" ) ; const dataBefore = await snapshotStakingData ( ) ; const t27x5 = await getTimestamp ( ) ; const tx = savingsVault [ "claimRewards()" ] ( ) ; await expect ( tx ) . to . emit ( savingsVault , "RewardPaid" ) ; [ amount , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 1 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 1 ) ) ; expect ( amount ) . to . be . eq ( BN . from ( 0 ) ) ; const dataAfter = await snapshotStakingData ( ) ; expect ( dataAfter . boostBalance . totalSupply ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( dataAfter . tokenBalance . sender ) . to . be . eq ( dataBefore . tokenBalance . sender . add ( amount ) ) ; expect ( dataAfter . userData . lastAction ) . to . be . eq ( dataAfter . userData . userClaim ) ; assertBNClose ( t27x5 , dataAfter . userData . lastAction , 5 ) ; expect ( dataAfter . userData . rewards ) . to . be . eq ( BN . from ( 0 ) ) ; } ) ; it ( "should unlock all rewards after sufficient time has elapsed" , async ( ) => { await expectStakingWithdrawal ( hunnit ) ; await increaseTime ( ONE_WEEK . mul ( 27 ) ) ; const expected = lockedRewards ( simpleToExactAmount ( 400 , 21 ) ) ; const allRewards = unlocked . add ( expected ) ; let [ amount , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 2 ) ) ; assertBNClosePercent ( amount , allRewards , "0.01" ) ; const dataBefore = await snapshotStakingData ( ) ; const t30 = await getTimestamp ( ) ; const tx = savingsVault [ "claimRewards()" ] ( ) ; await expect ( tx ) . to . emit ( savingsVault , "RewardPaid" ) ; [ amount , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 2 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 2 ) ) ; expect ( amount ) . to . be . eq ( BN . from ( 0 ) ) ; const dataAfter = await snapshotStakingData ( ) ; expect ( dataAfter . boostBalance . totalSupply ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( dataAfter . tokenBalance . sender ) . to . be . eq ( dataBefore . tokenBalance . sender . add ( amount ) ) ; expect ( dataAfter . userData . userClaim ) . to . be . eq ( dataAfter . userRewards [ 2 ] . finish ) ; assertBNClose ( t30 , dataAfter . userData . lastAction , 5 ) ; expect ( dataAfter . userData . rewards ) . to . be . eq ( BN . from ( 0 ) ) ; } ) ; it ( "should break if we leave rewards unclaimed at the start or end" , async ( ) => { await expectStakingWithdrawal ( hunnit ) ; await increaseTime ( ONE_WEEK . mul ( 25 ) ) ; let [ , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 1 ) ) ; await expect ( savingsVault [ "claimRewards(uint256,uint256)" ] ( 1 , 1 ) ) . to . be . revertedWith ( "Invalid _first arg: Must claim earlier entries" ) ; await increaseTime ( ONE_WEEK . mul ( 3 ) ) ; [ , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 2 ) ) ; await savingsVault [ "claimRewards(uint256,uint256)" ] ( 0 , 1 ) ; await savingsVault [ "claimRewards(uint256,uint256)" ] ( 1 , 2 ) ; const unclaimed = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( unclaimed [ 0 ] ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( unclaimed [ 1 ] ) . to . be . eq ( BN . from ( 2 ) ) ; expect ( unclaimed [ 2 ] ) . to . be . eq ( BN . from ( 2 ) ) ; const dataBefore = await snapshotStakingData ( ) ; await expect ( savingsVault [ "claimRewards(uint256,uint256)" ] ( 0 , 2 ) ) . to . be . revertedWith ( "Invalid epoch" ) ; const dataAfter = await snapshotStakingData ( ) ; expect ( dataAfter . tokenBalance . sender ) . to . be . eq ( dataBefore . tokenBalance . sender ) ; expect ( dataAfter . userData . userClaim ) . to . be . eq ( dataBefore . userData . userClaim ) ; } ) ; describe ( "with many array entries" , ( ) => { it ( "should allow them all to be searched and claimed" , async ( ) => { await rewardToken . connect ( rewardsDistributor . signer ) . transfer ( savingsVault . address , hunnit . mul ( 6 ) ) ; await increaseTime ( ONE_WEEK ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await expectSuccesfulFunding ( hunnit ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await expectSuccesfulFunding ( hunnit ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await expectSuccesfulFunding ( hunnit ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await expectSuccesfulFunding ( hunnit ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await expectSuccesfulFunding ( hunnit ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await expectSuccesfulFunding ( hunnit ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await increaseTime ( ONE_WEEK . mul ( 18 ) ) ; let [ amt , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 1 ) ) ; const data28 = await snapshotStakingData ( ) ; expect ( data28 . userData . userClaim ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( data28 . userData . rewardCount ) . eq ( 15 ) ; await increaseTime ( ONE_WEEK . mul ( 4 ) . sub ( 100 ) ) ; [ amt , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 6 ) ) ; await savingsVault [ "claimRewards(uint256,uint256)" ] ( 0 , 6 ) ; const data32 = await snapshotStakingData ( ) ; expect ( data32 . userData . userClaim ) . to . be . eq ( data32 . userData . lastAction ) ; [ amt , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( amt ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( first ) . to . be . eq ( BN . from ( 6 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 6 ) ) ; await increaseTime ( ONE_WEEK . mul ( 3 ) ) ; [ amt , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 6 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 12 ) ) ; await savingsVault [ "claimRewards(uint256,uint256)" ] ( 6 , 12 ) ; const data35 = await snapshotStakingData ( ) ; expect ( data35 . userData . userClaim ) . to . be . eq ( data35 . userData . lastAction ) ; [ amt , , ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( amt ) . to . be . eq ( BN . from ( 0 ) ) ; await expect ( savingsVault [ "claimRewards(uint256,uint256)" ] ( 0 , 1 ) ) . to . be . revertedWith ( "Invalid epoch" ) ; } ) ; } ) ; describe ( "with a one second entry" , ( ) => { it ( "should allow it to be claimed" , async ( ) => { await rewardToken . connect ( rewardsDistributor . signer ) . transfer ( savingsVault . address , hunnit ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await increaseTime ( ONE_WEEK ) ; await expectSuccesfulFunding ( hunnit ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await savingsVault . pokeBoost ( sa . default . address ) ; await increaseTime ( ONE_WEEK . mul ( 26 ) . sub ( 10 ) ) ; const data = await snapshotStakingData ( ) ; expect ( data . userData . rewardCount ) . eq ( 10 ) ; const r4 = data . userRewards [ 4 ] ; const r5 = data . userRewards [ 5 ] ; expect ( r4 . finish ) . to . be . eq ( r5 . start ) ; expect ( r5 . finish ) . to . be . lte ( r5 . start . add ( 2 ) ) ; expect ( r4 . rate ) . to . be . eq ( r5 . rate ) ; assertBNClosePercent ( r4 . rate , lockedRewards ( data . contractData . rewardRate ) , "0.001" ) ; let [ , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 3 ) ) ; await savingsVault [ "claimRewards(uint256,uint256)" ] ( 0 , 3 ) ; await increaseTime ( 20 ) ; [ , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 3 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 10 ) ) ; await expect ( savingsVault [ "claimRewards(uint256,uint256)" ] ( 0 , 8 ) ) . to . be . revertedWith ( "Invalid epoch" ) ; await savingsVault [ "claimRewards(uint256,uint256)" ] ( 3 , 8 ) ; await expect ( savingsVault [ "claimRewards(uint256,uint256)" ] ( 6 , 9 ) ) . to . be . revertedWith ( "Invalid epoch" ) ; await savingsVault [ "claimRewards()" ] ; } ) ; } ) ; } ) ; context ( "getting the reward token" , ( ) => { before ( async ( ) => { savingsVault = await redeployRewards ( ) ; } ) ; it ( "should simply return the rewards Token" , async ( ) => { const readToken = await savingsVault . getRewardToken ( ) ; expect ( readToken ) . eq ( rewardToken . address ) ; expect ( readToken ) . eq ( await savingsVault . rewardsToken ( ) ) ; } ) ; } ) ; context ( "calling exit" , ( ) => { const hunnit = simpleToExactAmount ( 100 , 18 ) ; beforeEach ( async ( ) => { savingsVault = await redeployRewards ( ) ; await rewardToken . connect ( rewardsDistributor . signer ) . transfer ( savingsVault . address , hunnit ) ; await expectSuccesfulFunding ( hunnit ) ; await expectSuccessfulStake ( hunnit ) ; await increaseTime ( ONE_WEEK . add ( 1 ) ) ; } ) ; context ( "with no raw balance but rewards unlocked" , ( ) => { it ( "errors" , async ( ) => { await savingsVault . withdraw ( hunnit ) ; const beforeData = await snapshotStakingData ( ) ; expect ( beforeData . boostBalance . totalSupply ) . to . be . eq ( BN . from ( 0 ) ) ; await expect ( savingsVault [ "exit()" ] ( ) ) . to . be . revertedWith ( "Cannot withdraw 0" ) ; } ) ; } ) ; context ( "with raw balance" , async ( ) => { it ( "withdraws everything and claims unlocked rewards" , async ( ) => { const beforeData = await snapshotStakingData ( ) ; expect ( beforeData . boostBalance . totalSupply ) . to . be . eq ( simpleToExactAmount ( 100 , 18 ) ) ; await savingsVault [ "exit()" ] ( ) ; const afterData = await snapshotStakingData ( ) ; expect ( afterData . userData . userClaim ) . to . be . eq ( afterData . userData . lastAction ) ; expect ( afterData . userData . rewards ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( afterData . boostBalance . totalSupply ) . to . be . eq ( BN . from ( 0 ) ) ; } ) ; } ) ; context ( "with unlocked rewards" , ( ) => { it ( "claims unlocked epochs" , async ( ) => { await savingsVault . pokeBoost ( sa . default . address ) ; await increaseTime ( ONE_WEEK . mul ( 27 ) ) ; const [ amount , first , last ] = await savingsVault . unclaimedRewards ( sa . default . address ) ; expect ( first ) . to . be . eq ( BN . from ( 0 ) ) ; expect ( last ) . to . be . eq ( BN . from ( 0 ) ) ; assertBNClosePercent ( amount , hunnit , "0.01" ) ; const tx = savingsVault [ "exit(uint256,uint256)" ] ( first , last ) ; await expect ( tx ) . to . emit ( savingsVault , "RewardPaid" ) ; await expect ( tx ) . to . emit ( savingsVault , "Withdrawn" ) . withArgs ( sa . default . address , hunnit ) ; } ) ; } ) ; } ) ; context ( "withdrawing stake or rewards" , ( ) => { context ( "withdrawing a stake amount" , ( ) => { const fundAmount = simpleToExactAmount ( 100 , 21 ) ; const stakeAmount = simpleToExactAmount ( 100 , 18 ) ; before ( async ( ) => { savingsVault = await redeployRewards ( ) ; await expectSuccesfulFunding ( fundAmount ) ; await expectSuccessfulStake ( stakeAmount ) ; await increaseTime ( 10 ) ; } ) ; it ( "should revert for a non-staker" , async ( ) => { await expect ( savingsVault . connect ( sa . dummy1 . signer ) . withdraw ( 1 ) ) . to . be . revertedWith ( "VM Exception" ) ; } ) ; it ( "should revert if insufficient balance" , async ( ) => { await expect ( savingsVault . connect ( sa . default . signer ) . withdraw ( stakeAmount . add ( 1 ) ) ) . to . be . revertedWith ( "VM Exception" ) ; } ) ; it ( "should fail if trying to withdraw 0" , async ( ) => { await expect ( savingsVault . connect ( sa . default . signer ) . withdraw ( 0 ) ) . to . be . revertedWith ( "Cannot withdraw 0" ) ; } ) ; it ( "should withdraw the stake and update the existing reward accrual" , async ( ) => { const earnedBefore = await savingsVault . earned ( sa . default . address ) ; expect ( earnedBefore ) . to . be . gt ( BN . from ( 0 ) ) ; const dataBefore = await snapshotStakingData ( ) ; expect ( dataBefore . userData . rewards ) . to . be . eq ( BN . from ( 0 ) ) ; await expectStakingWithdrawal ( stakeAmount ) ; const earnedAfter = await savingsVault . earned ( sa . default . address ) ; expect ( earnedAfter ) . to . be . gte ( earnedBefore ) ; const dataAfter = await snapshotStakingData ( ) ; expect ( dataAfter . userData . rewards ) . to . be . eq ( earnedAfter ) ; await increaseTime ( 10 ) ; const earnedEnd = await savingsVault . earned ( sa . default . address ) ; expect ( earnedEnd ) . to . be . eq ( earnedAfter ) ; const dataEnd = await snapshotStakingData ( ) ; expect ( dataEnd . userData . rewards ) . to . be . eq ( dataAfter . userData . rewards ) ; await expect ( savingsVault . connect ( sa . default . signer ) . withdraw ( stakeAmount . add ( 1 ) ) ) . to . be . revertedWith ( "VM Exception" ) ; } ) ; } ) ; } ) ; context ( "notifying new reward amount" , ( ) => { context ( "from someone other than the distributor" , ( ) => { before ( async ( ) => { savingsVault = await redeployRewards ( ) ; } ) ; it ( "should fail" , async ( ) => { await expect ( savingsVault . connect ( sa . default . signer ) . notifyRewardAmount ( 1 ) ) . to . be . revertedWith ( "Caller is not reward distributor" ) ; await expect ( savingsVault . connect ( sa . dummy1 . signer ) . notifyRewardAmount ( 1 ) ) . to . be . revertedWith ( "Caller is not reward distributor" ) ; await expect ( savingsVault . connect ( sa . governor . signer ) . notifyRewardAmount ( 1 ) ) . to . be . revertedWith ( "Caller is not reward distributor" ) ; } ) ; } ) ; context ( "before current period finish" , async ( ) => { const funding1 = simpleToExactAmount ( 100 , 18 ) ; const funding2 = simpleToExactAmount ( 200 , 18 ) ; beforeEach ( async ( ) => { savingsVault = await redeployRewards ( ) ; } ) ; it ( "should factor in unspent units to the new rewardRate" , async ( ) => { await expectSuccesfulFunding ( funding1 ) ; const actualRewardRate = await savingsVault . rewardRate ( ) ; const expectedRewardRate = funding1 . div ( ONE_WEEK ) ; expect ( expectedRewardRate ) . to . be . eq ( actualRewardRate ) ; await increaseTime ( ONE_WEEK . div ( 2 ) ) ; const expectedLeftoverReward = funding1 . div ( 2 ) ; await expectSuccesfulFunding ( funding2 ) ; const actualRewardRateAfter = await savingsVault . rewardRate ( ) ; 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 expectSuccesfulFunding ( funding1 ) ; const actualRewardRate = await savingsVault . rewardRate ( ) ; const expectedRewardRate = funding1 . div ( ONE_WEEK ) ; expect ( expectedRewardRate ) . to . be . eq ( actualRewardRate ) ; await increaseTime ( 1 ) ; await expectSuccesfulFunding ( funding2 ) ; const actualRewardRateAfter = await savingsVault . rewardRate ( ) ; 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 ( ) => { savingsVault = await redeployRewards ( ) ; } ) ; it ( "should start a new period with the correct rewardRate" , async ( ) => { await expectSuccesfulFunding ( funding1 ) ; const actualRewardRate = await savingsVault . rewardRate ( ) ; const expectedRewardRate = funding1 . div ( ONE_WEEK ) ; expect ( expectedRewardRate ) . to . be . eq ( actualRewardRate ) ; await increaseTime ( ONE_WEEK . add ( 1 ) ) ; await expectSuccesfulFunding ( funding1 . mul ( 2 ) ) ; const actualRewardRateAfter = await savingsVault . rewardRate ( ) ; const expectedRewardRateAfter = expectedRewardRate . mul ( 2 ) ; expect ( actualRewardRateAfter ) . to . be . eq ( expectedRewardRateAfter ) ; } ) ; } ) ; } ) ; context ( "Govern boost director" , ( ) => { let vaultA ; let vaultB ; let vaultC ; let vaultD ; let vaultUnlisted ; let user1NoStake ; let user2Staked ; let user3Staked ; before ( async ( ) => { vaultA = sa . dummy1 ; vaultB = sa . dummy2 ; vaultC = sa . dummy3 ; vaultD = sa . dummy4 ; vaultUnlisted = sa . all [ 10 ] ; user1NoStake = sa . all [ 11 ] ; user2Staked = sa . all [ 12 ] ; user3Staked = sa . all [ 13 ] ; rewardToken = await new MockERC20__factory ( sa . default . signer ) . deploy ( "Reward" , "RWD" , 18 , rewardsDistributor . address , 10000000 ) ; stakingContract = await new MockStakingContract__factory ( sa . default . signer ) . deploy ( ) ; await stakingContract . setBalanceOf ( user2Staked . address , 20000 ) ; await stakingContract . setBalanceOf ( user3Staked . address , 30000 ) ; } ) ; context ( "Whitelisting boost savings vaults" , ( ) => { before ( async ( ) => { boostDirector = await new BoostDirector__factory ( sa . default . signer ) . deploy ( nexus . address , stakingContract . address ) ; await boostDirector . initialize ( [ vaultA . address ] ) ; } ) ; it ( "should get first vault A" , async ( ) => { expect ( await boostDirector . _vaults ( vaultA . address ) ) . to . eq ( 1 ) ; } ) ; it ( "should fail if not governor" , async ( ) => { let tx = boostDirector . connect ( sa . default . signer ) . whitelistVaults ( [ vaultB . address ] ) ; await expect ( tx ) . to . revertedWith ( "Only governor can execute" ) ; tx = boostDirector . connect ( sa . fundManager . signer ) . whitelistVaults ( [ vaultB . address ] ) ; await expect ( tx ) . to . revertedWith ( "Only governor can execute" ) ; } ) ; it ( "should succeed in whitelisting no boost savings vault" , async ( ) => { const tx = boostDirector . connect ( sa . governor . signer ) . whitelistVaults ( [ ] ) ; await expect ( tx ) . to . revertedWith ( "Must be at least one vault" ) ; } ) ; it ( "should succeed in whitelisting one boost savings vault" , async ( ) => { const tx = boostDirector . connect ( sa . governor . signer ) . whitelistVaults ( [ vaultB . address ] ) ; await expect ( tx ) . to . emit ( boostDirector , "Whitelisted" ) . withArgs ( vaultB . address , 2 ) ; expect ( await boostDirector . _vaults ( vaultB . address ) ) . to . eq ( 2 ) ; } ) ; it ( "should fail if already whitelisted" , async ( ) => { const tx = boostDirector . connect ( sa . governor . signer ) . whitelistVaults ( [ vaultB . address ] ) ; await expect ( tx ) . to . revertedWith ( "Vault already whitelisted" ) ; } ) ; it ( "should succeed in whitelisting two boost savings vault" , async ( ) => { const tx = boostDirector . connect ( sa . governor . signer ) . whitelistVaults ( [ vaultC . address , vaultD . address ] ) ; await expect ( tx ) . to . emit ( boostDirector , "Whitelisted" ) . withArgs ( vaultC . address , 3 ) ; await expect ( tx ) . to . emit ( boostDirector , "Whitelisted" ) . withArgs ( vaultD . address , 4 ) ; expect ( await boostDirector . _vaults ( vaultC . address ) ) . to . eq ( 3 ) ; expect ( await boostDirector . _vaults ( vaultD . address ) ) . to . eq ( 4 ) ; } ) ; } ) ; context ( "get boost balance" , ( ) => { let boostDirectorVaultA ; let boostDirectorVaultB ; let boostDirectorVaultC ; let boostDirectorVaultD ; before ( async ( ) => { boostDirector = await new BoostDirector__factory ( sa . default . signer ) . deploy ( nexus . address , stakingContract . address ) ; await boostDirector . initialize ( [ vaultA . address , vaultB . address , vaultC . address , vaultD . address ] ) ; boostDirectorVaultA = boostDirector . connect ( vaultA . signer ) ; boostDirectorVaultB = boostDirector . connect ( vaultB . signer ) ; boostDirectorVaultC = boostDirector . connect ( vaultC . signer ) ; boostDirectorVaultD = boostDirector . connect ( vaultD . signer ) ; } ) ; context ( "called from vault A" , ( ) => { context ( "for user 1 with nothing staked" , ( ) => { it ( "should get zero balance" , async ( ) => { const bal = await boostDirectorVaultA . callStatic . getBalance ( user1NoStake . address ) ; expect ( bal ) . to . eq ( 0 ) ; } ) ; it ( "should add user to boost director" , async ( ) => { const tx = boostDirectorVaultA . getBalance ( user1NoStake . address ) ; await expect ( tx ) . to . emit ( boostDirector , "Directed" ) . withArgs ( user1NoStake . address , vaultA . address ) ; } ) ; it ( "should fail to add user to boost director again" , async ( ) => { const tx = boostDirectorVaultA . getBalance ( user1NoStake . address ) ; await expect ( tx ) . to . not . emit ( boostDirector , "Directed" ) ; } ) ; it ( "should get user zero balance after being added" , async ( ) => { const bal = await boostDirectorVaultA . callStatic . getBalance ( user1NoStake . address ) ; expect ( bal ) . to . eq ( 0 ) ; } ) ; } ) ; context ( "for user 2 with 20,000 staked" , ( ) => { it ( "should get user 2 balance" , async ( ) => { const bal = await boostDirectorVaultA . callStatic . getBalance ( user2Staked . address ) ; expect ( bal ) . to . eq ( 20000 ) ; } ) ; it ( "should add user 2 to boost director" , async ( ) => { const tx = boostDirectorVaultA . getBalance ( user2Staked . address ) ; await expect ( tx ) . to . emit ( boostDirector , "Directed" ) . withArgs ( user2Staked . address , vaultA . address ) ; } ) ; it ( "should fail to add user to boost director again" , async ( ) => { const tx = boostDirectorVaultA . getBalance ( user2Staked . address ) ; await expect ( tx ) . to . not . emit ( boostDirector , "Directed" ) ; } ) ; it ( "should get user 2 balance after being added" , async ( ) => { const bal = await boostDirectorVaultA . callStatic . getBalance ( user2Staked . address ) ; expect ( bal ) . to . eq ( 20000 ) ; } ) ; } ) ; } ) ; context ( "user 3 with 30,000 staked added to vaults A, B and C but not D" , ( ) => { const userStakedBalance = 30000 ; it ( "vault A should get user balance before being added to any vaults" , async ( ) => { const bal = await boostDirectorVaultA . callStatic . getBalance ( user3Staked . address ) ; expect ( bal ) . to . eq ( userStakedBalance ) ; } ) ; it ( "vault A should add user to boost director" , async ( ) => { const tx = boostDirectorVaultA . getBalance ( user3Staked . address ) ; await expect ( tx ) . to . emit ( boostDirector , "Directed" ) . withArgs ( user3Staked . address , vaultA . address ) ; } ) ; it ( "vault B should add user to boost director" , async ( ) => { const tx = boostDirectorVaultB . getBalance ( user3Staked . address ) ; await expect ( tx ) . to . emit ( boostDirector , "Directed" ) . withArgs ( user3Staked . address , vaultB . address ) ; } ) ; it ( "vault C should add user to boost director" , async ( ) => { const tx = boostDirectorVaultC . getBalance ( user3Staked . address ) ; await expect ( tx ) . to . emit ( boostDirector , "Directed" ) . withArgs ( user3Staked . address , vaultC . address ) ; } ) ; it ( "vault C should get user balance after user added" , async ( ) => { const bal = await boostDirectorVaultC . callStatic . getBalance ( user3Staked . address ) ; expect ( bal ) . to . eq ( userStakedBalance ) ; } ) ; it ( "vault D should fail to add user as its the fourth" , async ( ) => { const tx = boostDirectorVaultD . getBalance ( user3Staked . address ) ; await expect ( tx ) . to . not . emit ( boostDirector , "Directed" ) ; } ) ; it ( "vault D should get zero balance for the user" , async ( ) => { const bal = await boostDirectorVaultD . callStatic . getBalance ( user3Staked . address ) ; expect ( bal ) . to . eq ( 0 ) ; } ) ; it ( "vault A should still user balance" , async ( ) => { const bal = await boostDirectorVaultA . callStatic . getBalance ( user3Staked . address ) ; expect ( bal ) . to . eq ( userStakedBalance ) ; } ) ; it ( "vault B should still fer user balance" , async ( ) => { const bal = await boostDirectorVaultB . callStatic . getBalance ( user3Staked . address ) ; expect ( bal ) . to . eq ( userStakedBalance ) ; } ) ; it ( "vault C should still user balance" , async ( ) => { const bal = await boostDirectorVaultC . callStatic . getBalance ( user3Staked . address ) ; expect ( bal ) . to . eq ( userStakedBalance ) ; } ) ; } ) ; context ( "adding non whitelisted vaults" , ( ) => { it ( "should fail to add user from unlisted vault" , async ( ) => { const tx = boostDirector . connect ( vaultUnlisted . signer ) . getBalance ( user2Staked . address ) ; await expect ( tx ) . to . not . emit ( boostDirector , "Directed" ) ; } ) ; it ( "should get zero balance for unlisted vault" , async ( ) => { const bal = await boostDirector . connect ( vaultUnlisted . signer ) . callStatic . getBalance ( user3Staked . address ) ; expect ( bal ) . to . eq ( 0 ) ; } ) ; it ( "should fail for user to add themselves as a vault" , async ( ) => { const tx = boostDirector . connect ( user2Staked . signer ) . getBalance ( user2Staked . address ) ; await expect ( tx ) . to . not . emit ( boostDirector , "Directed" ) ; } ) ; } ) ; } ) ; context ( "redirect staked rewards to new boost savings vault" , ( ) => { let mockedVaults ; before ( async ( ) => { boostDirector = await new BoostDirector__factory ( sa . default . signer ) . deploy ( nexus . address , stakingContract . address ) ; const mockedVaultsPromises = [ 1 , 2 , 3 , 4 , 5 ] . map ( ( ) => new MockBoostedVault__factory ( sa . default . signer ) . deploy ( boostDirector . address ) ) ; mockedVaults = await Promise . all ( mockedVaultsPromises ) ; const mockedVaultAddresses = mockedVaults . map ( ( vault ) => vault . address ) ; await boostDirector . initialize ( mockedVaultAddresses ) ; await mockedVaults [ 0 ] . testGetBalance ( user1NoStake . address ) ; await mockedVaults [ 1 ] . testGetBalance ( user1NoStake . address ) ; await mockedVaults [ 2 ] . testGetBalance ( user1NoStake . address ) ; await mockedVaults [ 0 ] . testGetBalance ( user2Staked . address ) ; await mockedVaults [ 1 ] . testGetBalance ( user2Staked . address ) ; await mockedVaults [ 2 ] . testGetBalance ( user2Staked . address ) ; await mockedVaults [ 0 ] . testGetBalance ( user3Staked . address ) ; } ) ; it ( "should get initial balancers" , async ( ) => { expect ( await mockedVaults [ 0 ] . callStatic . testGetBalance ( user1NoStake . address ) ) . to . eq ( 0 ) ; expect ( await mockedVaults [ 1 ] . callStatic . testGetBalance ( user1NoStake . address ) ) . to . eq ( 0 ) ; expect ( await mockedVaults [ 2 ] . callStatic . testGetBalance ( user1NoStake . address ) ) . to . eq ( 0 ) ; expect ( await mockedVaults [ 3 ] . callStatic . testGetBalance ( user1NoStake . address ) ) . to . eq ( 0 ) ; expect ( await mockedVaults [ 4 ] . callStatic . testGetBalance ( user1NoStake . address ) ) . to . eq ( 0 ) ; expect ( await mockedVaults [ 0 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 1 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 2 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 3 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 0 ) ; expect ( await mockedVaults [ 4 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 0 ) ; expect ( await mockedVaults [ 0 ] . callStatic . testGetBalance ( user3Staked . address ) ) . to . eq ( 30000 ) ; expect ( await mockedVaults [ 1 ] . callStatic . testGetBalance ( user3Staked . address ) ) . to . eq ( 30000 ) ; expect ( await mockedVaults [ 2 ] . callStatic . testGetBalance ( user3Staked . address ) ) . to . eq ( 30000 ) ; expect ( await mockedVaults [ 3 ] . callStatic . testGetBalance ( user3Staked . address ) ) . to . eq ( 30000 ) ; expect ( await mockedVaults [ 4 ] . callStatic . testGetBalance ( user3Staked . address ) ) . to . eq ( 30000 ) ; } ) ; it ( "should fail as old vault is not whitelisted" , async ( ) => { const tx = boostDirector . connect ( user1NoStake . signer ) . setDirection ( sa . dummy1 . address , mockedVaults [ 3 ] . address , false ) ; await expect ( tx ) . to . revertedWith ( "Vaults not whitelisted" ) ; } ) ; it ( "should fail as user 1 has not been added to the old vault 4" , async ( ) => { const tx = boostDirector . connect ( user1NoStake . signer ) . setDirection ( mockedVaults [ 4 ] . address , mockedVaults [ 3 ] . address , false ) ; await expect ( tx ) . to . revertedWith ( "No need to replace old" ) ; } ) ; it ( "should fail as new vault is not whitelisted" , async ( ) => { const tx = boostDirector . connect ( user1NoStake . signer ) . setDirection ( mockedVaults [ 0 ] . address , sa . dummy1 . address , false ) ; await expect ( tx ) . to . revertedWith ( "Vaults not whitelisted" ) ; } ) ; it ( "user 1 should succeed in replacing vault 1 with vault 4 that is not poked" , async ( ) => { const tx = boostDirector . connect ( user1NoStake . signer ) . setDirection ( mockedVaults [ 0 ] . address , mockedVaults [ 3 ] . address , false ) ; await expect ( tx ) . to . emit ( mockedVaults [ 0 ] , "Poked" ) . withArgs ( user1NoStake . address ) ; await expect ( tx ) . to . not . emit ( mockedVaults [ 3 ] , "Poked" ) ; await expect ( tx ) . to . emit ( boostDirector , "RedirectedBoost" ) . withArgs ( user1NoStake . address , mockedVaults [ 3 ] . address , mockedVaults [ 0 ] . address ) ; } ) ; it ( "user 1 should succeed in replacing vault 2 vault 5 that is poked" , async ( ) => { const tx = boostDirector . connect ( user1NoStake . signer ) . setDirection ( mockedVaults [ 1 ] . address , mockedVaults [ 4 ] . address , true ) ; await expect ( tx ) . to . emit ( mockedVaults [ 1 ] , "Poked" ) . withArgs ( user1NoStake . address ) ; await expect ( tx ) . to . emit ( mockedVaults [ 4 ] , "Poked" ) . withArgs ( user1NoStake . address ) ; await expect ( tx ) . to . emit ( boostDirector , "RedirectedBoost" ) . withArgs ( user1NoStake . address , mockedVaults [ 4 ] . address , mockedVaults [ 1 ] . address ) ; } ) ; it ( "user 2 should succeed in replacing vault 1 with vault 4 that is not poked" , async ( ) => { expect ( await mockedVaults [ 0 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 1 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 2 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 3 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 0 ) ; expect ( await mockedVaults [ 4 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 0 ) ; const tx = boostDirector . connect ( user2Staked . signer ) . setDirection ( mockedVaults [ 0 ] . address , mockedVaults [ 3 ] . address , false ) ; await expect ( tx ) . to . emit ( mockedVaults [ 0 ] , "Poked" ) . withArgs ( user2Staked . address ) ; await expect ( tx ) . to . not . emit ( mockedVaults [ 3 ] , "Poked" ) ; await expect ( tx ) . to . emit ( boostDirector , "RedirectedBoost" ) . withArgs ( user2Staked . address , mockedVaults [ 3 ] . address , mockedVaults [ 0 ] . address ) ; expect ( await mockedVaults [ 0 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 0 ) ; expect ( await mockedVaults [ 1 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 2 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 3 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 4 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 0 ) ; } ) ; it ( "user 2 should succeed in replacing vault 2 vault 5 that is poked" , async ( ) => { expect ( await mockedVaults [ 0 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 0 ) ; expect ( await mockedVaults [ 1 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 2 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 3 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 4 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 0 ) ; const tx = boostDirector . connect ( user2Staked . signer ) . setDirection ( mockedVaults [ 1 ] . address , mockedVaults [ 4 ] . address , true ) ; await expect ( tx ) . to . emit ( mockedVaults [ 1 ] , "Poked" ) . withArgs ( user2Staked . address ) ; await expect ( tx ) . to . emit ( mockedVaults [ 4 ] , "Poked" ) . withArgs ( user2Staked . address ) ; await expect ( tx ) . to . emit ( boostDirector , "RedirectedBoost" ) . withArgs ( user2Staked . address , mockedVaults [ 4 ] . address , mockedVaults [ 1 ] . address ) ; expect ( await mockedVaults [ 0 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 0 ) ; expect ( await mockedVaults [ 1 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 0 ) ; expect ( await mockedVaults [ 2 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 3 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 4 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; } ) ; it ( "user 2 should succeed in replacing vault 5 back to vault 2 that is poked" , async ( ) => { expect ( await mockedVaults [ 0 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 0 ) ; expect ( await mockedVaults [ 1 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 0 ) ; expect ( await mockedVaults [ 2 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 3 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 4 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; const tx = boostDirector . connect ( user2Staked . signer ) . setDirection ( mockedVaults [ 4 ] . address , mockedVaults [ 1 ] . address , true ) ; await expect ( tx ) . to . emit ( mockedVaults [ 4 ] , "Poked" ) . withArgs ( user2Staked . address ) ; await expect ( tx ) . to . emit ( mockedVaults [ 1 ] , "Poked" ) . withArgs ( user2Staked . address ) ; await expect ( tx ) . to . emit ( boostDirector , "RedirectedBoost" ) . withArgs ( user2Staked . address , mockedVaults [ 1 ] . address , mockedVaults [ 4 ] . address ) ; expect ( await mockedVaults [ 0 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 0 ) ; expect ( await mockedVaults [ 1 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 2 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 3 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 20000 ) ; expect ( await mockedVaults [ 4 ] . callStatic . testGetBalance ( user2Staked . address ) ) . to . eq ( 0 ) ; } ) ; it ( "should fail as user 3 only has 1 vault" , async ( ) => { const tx = boostDirector . connect ( user3Staked . signer ) . setDirection ( mockedVaults [ 0 ] . address , mockedVaults [ 3 ] . address , false ) ; await expect ( tx ) . to . revertedWith ( "No need to replace old" ) ; } ) ; } ) ; } ) ; } ) ;