701 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			701 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
 | |
| import { blockchainTests, constants, describe, expect, shortZip, toBaseUnitAmount } from '@0x/contracts-test-utils';
 | |
| import { BigNumber, StakingRevertErrors } from '@0x/utils';
 | |
| import * as _ from 'lodash';
 | |
| 
 | |
| import { DelegatorsByPoolId, OperatorByPoolId, StakeInfo, StakeStatus } from '../src/types';
 | |
| 
 | |
| import { FinalizerActor } from './actors/finalizer_actor';
 | |
| import { PoolOperatorActor } from './actors/pool_operator_actor';
 | |
| import { StakerActor } from './actors/staker_actor';
 | |
| import { deployAndConfigureContractsAsync, StakingApiWrapper } from './utils/api_wrapper';
 | |
| 
 | |
| // tslint:disable:no-unnecessary-type-assertion
 | |
| // tslint:disable:max-file-line-count
 | |
| blockchainTests.resets('Testing Rewards', env => {
 | |
|     // tokens & addresses
 | |
|     let accounts: string[];
 | |
|     let owner: string;
 | |
|     let actors: string[];
 | |
|     let exchangeAddress: string;
 | |
|     let takerAddress: string;
 | |
|     // wrappers
 | |
|     let stakingApiWrapper: StakingApiWrapper;
 | |
|     // let testWrapper: TestRewardBalancesContract;
 | |
|     let erc20Wrapper: ERC20Wrapper;
 | |
|     // test parameters
 | |
|     let stakers: StakerActor[];
 | |
|     let poolOperatorStaker: StakerActor;
 | |
|     let poolId: string;
 | |
|     let poolOperator: PoolOperatorActor;
 | |
|     let finalizer: FinalizerActor;
 | |
|     // tests
 | |
|     before(async () => {
 | |
|         // create accounts
 | |
|         accounts = await env.getAccountAddressesAsync();
 | |
|         owner = accounts[0];
 | |
|         exchangeAddress = accounts[1];
 | |
|         takerAddress = accounts[2];
 | |
|         actors = accounts.slice(3);
 | |
|         // set up ERC20Wrapper
 | |
|         erc20Wrapper = new ERC20Wrapper(env.provider, accounts, owner);
 | |
|         // deploy staking contracts
 | |
|         stakingApiWrapper = await deployAndConfigureContractsAsync(env, owner, erc20Wrapper);
 | |
|         // set up staking parameters
 | |
|         await stakingApiWrapper.utils.setParamsAsync({
 | |
|             minimumPoolStake: new BigNumber(2),
 | |
|             cobbDouglasAlphaNumerator: new BigNumber(1),
 | |
|             cobbDouglasAlphaDenominator: new BigNumber(6),
 | |
|         });
 | |
|         // setup stakers
 | |
|         stakers = actors.slice(0, 2).map(a => new StakerActor(a, stakingApiWrapper));
 | |
|         // setup pools
 | |
|         poolOperator = new PoolOperatorActor(actors[2], stakingApiWrapper);
 | |
|         // Create a pool where all rewards go to members.
 | |
|         poolId = await poolOperator.createStakingPoolAsync(0, true);
 | |
|         // Stake something in the pool or else it won't get any rewards.
 | |
|         poolOperatorStaker = new StakerActor(poolOperator.getOwner(), stakingApiWrapper);
 | |
|         await poolOperatorStaker.stakeWithPoolAsync(poolId, new BigNumber(2));
 | |
|         // set exchange address
 | |
|         await stakingApiWrapper.stakingContract.addExchangeAddress(exchangeAddress).awaitTransactionSuccessAsync();
 | |
|         // associate operators for tracking in Finalizer
 | |
|         const operatorByPoolId: OperatorByPoolId = {};
 | |
|         operatorByPoolId[poolId] = poolOperator.getOwner();
 | |
|         // associate actors with pools for tracking in Finalizer
 | |
|         const stakersByPoolId: DelegatorsByPoolId = {};
 | |
|         stakersByPoolId[poolId] = actors.slice(0, 3);
 | |
|         // create Finalizer actor
 | |
|         finalizer = new FinalizerActor(actors[3], stakingApiWrapper, [poolId], operatorByPoolId, stakersByPoolId);
 | |
|         // Skip to next epoch so operator stake is realized.
 | |
|         await stakingApiWrapper.utils.skipToNextEpochAndFinalizeAsync();
 | |
|     });
 | |
|     describe('Reward Simulation', () => {
 | |
|         interface EndBalances {
 | |
|             // staker 1
 | |
|             stakerRewardBalance_1?: BigNumber;
 | |
|             stakerWethBalance_1?: BigNumber;
 | |
|             // staker 2
 | |
|             stakerRewardBalance_2?: BigNumber;
 | |
|             stakerWethBalance_2?: BigNumber;
 | |
|             // operator
 | |
|             operatorWethBalance?: BigNumber;
 | |
|             // undivided balance in reward pool
 | |
|             poolRewardBalance?: BigNumber;
 | |
|             membersRewardBalance?: BigNumber;
 | |
|         }
 | |
|         const validateEndBalances = async (_expectedEndBalances: EndBalances): Promise<void> => {
 | |
|             const expectedEndBalances = {
 | |
|                 // staker 1
 | |
|                 stakerRewardBalance_1:
 | |
|                     _expectedEndBalances.stakerRewardBalance_1 !== undefined
 | |
|                         ? _expectedEndBalances.stakerRewardBalance_1
 | |
|                         : constants.ZERO_AMOUNT,
 | |
|                 stakerWethBalance_1:
 | |
|                     _expectedEndBalances.stakerWethBalance_1 !== undefined
 | |
|                         ? _expectedEndBalances.stakerWethBalance_1
 | |
|                         : constants.ZERO_AMOUNT,
 | |
|                 // staker 2
 | |
|                 stakerRewardBalance_2:
 | |
|                     _expectedEndBalances.stakerRewardBalance_2 !== undefined
 | |
|                         ? _expectedEndBalances.stakerRewardBalance_2
 | |
|                         : constants.ZERO_AMOUNT,
 | |
|                 stakerWethBalance_2:
 | |
|                     _expectedEndBalances.stakerWethBalance_2 !== undefined
 | |
|                         ? _expectedEndBalances.stakerWethBalance_2
 | |
|                         : constants.ZERO_AMOUNT,
 | |
|                 // operator
 | |
|                 operatorWethBalance:
 | |
|                     _expectedEndBalances.operatorWethBalance !== undefined
 | |
|                         ? _expectedEndBalances.operatorWethBalance
 | |
|                         : constants.ZERO_AMOUNT,
 | |
|                 // undivided balance in reward pool
 | |
|                 poolRewardBalance:
 | |
|                     _expectedEndBalances.poolRewardBalance !== undefined
 | |
|                         ? _expectedEndBalances.poolRewardBalance
 | |
|                         : constants.ZERO_AMOUNT,
 | |
|             };
 | |
|             const finalEndBalancesAsArray = await Promise.all([
 | |
|                 // staker 1
 | |
|                 stakingApiWrapper.stakingContract
 | |
|                     .computeRewardBalanceOfDelegator(poolId, stakers[0].getOwner())
 | |
|                     .callAsync(),
 | |
|                 stakingApiWrapper.wethContract.balanceOf(stakers[0].getOwner()).callAsync(),
 | |
|                 // staker 2
 | |
|                 stakingApiWrapper.stakingContract
 | |
|                     .computeRewardBalanceOfDelegator(poolId, stakers[1].getOwner())
 | |
|                     .callAsync(),
 | |
|                 stakingApiWrapper.wethContract.balanceOf(stakers[1].getOwner()).callAsync(),
 | |
|                 // operator
 | |
|                 stakingApiWrapper.wethContract.balanceOf(poolOperator.getOwner()).callAsync(),
 | |
|                 // undivided balance in reward pool
 | |
|                 stakingApiWrapper.stakingContract.rewardsByPoolId(poolId).callAsync(),
 | |
|             ]);
 | |
|             expect(finalEndBalancesAsArray[0], 'stakerRewardBalance_1').to.be.bignumber.equal(
 | |
|                 expectedEndBalances.stakerRewardBalance_1,
 | |
|             );
 | |
|             expect(finalEndBalancesAsArray[1], 'stakerWethBalance_1').to.be.bignumber.equal(
 | |
|                 expectedEndBalances.stakerWethBalance_1,
 | |
|             );
 | |
|             expect(finalEndBalancesAsArray[2], 'stakerRewardBalance_2').to.be.bignumber.equal(
 | |
|                 expectedEndBalances.stakerRewardBalance_2,
 | |
|             );
 | |
|             expect(finalEndBalancesAsArray[3], 'stakerWethBalance_2').to.be.bignumber.equal(
 | |
|                 expectedEndBalances.stakerWethBalance_2,
 | |
|             );
 | |
|             expect(finalEndBalancesAsArray[4], 'operatorWethBalance').to.be.bignumber.equal(
 | |
|                 expectedEndBalances.operatorWethBalance,
 | |
|             );
 | |
|             expect(finalEndBalancesAsArray[5], 'poolRewardBalance').to.be.bignumber.equal(
 | |
|                 expectedEndBalances.poolRewardBalance,
 | |
|             );
 | |
|         };
 | |
|         const payProtocolFeeAndFinalize = async (_fee?: BigNumber) => {
 | |
|             const fee = _fee !== undefined ? _fee : constants.ZERO_AMOUNT;
 | |
|             if (!fee.eq(constants.ZERO_AMOUNT)) {
 | |
|                 await stakingApiWrapper.stakingContract
 | |
|                     .payProtocolFee(poolOperator.getOwner(), takerAddress, fee)
 | |
|                     .awaitTransactionSuccessAsync({ from: exchangeAddress, value: fee });
 | |
|             }
 | |
|             await finalizer.finalizeAsync();
 | |
|         };
 | |
|         it('Reward balance should be zero if not delegated', async () => {
 | |
|             // sanity balances - all zero
 | |
|             await validateEndBalances({});
 | |
|         });
 | |
|         it('Reward balance should be zero if not delegated, when epoch is greater than 0', async () => {
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // sanity balances - all zero
 | |
|             await validateEndBalances({});
 | |
|         });
 | |
|         it('Reward balance should be zero in same epoch as delegation', async () => {
 | |
|             const amount = toBaseUnitAmount(4);
 | |
|             await stakers[0].stakeAsync(amount);
 | |
|             await stakers[0].moveStakeAsync(
 | |
|                 new StakeInfo(StakeStatus.Undelegated),
 | |
|                 new StakeInfo(StakeStatus.Delegated, poolId),
 | |
|                 amount,
 | |
|             );
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // sanit check final balances - all zero
 | |
|             await validateEndBalances({});
 | |
|         });
 | |
|         it('Operator should receive entire reward if no delegators in their pool', async () => {
 | |
|             const reward = toBaseUnitAmount(10);
 | |
|             await payProtocolFeeAndFinalize(reward);
 | |
|             // sanity check final balances - all zero
 | |
|             await validateEndBalances({
 | |
|                 operatorWethBalance: reward,
 | |
|             });
 | |
|         });
 | |
|         it(`Operator should receive entire reward if no delegators in their pool
 | |
|             (staker joins this epoch but is active next epoch)`, async () => {
 | |
|             // delegate
 | |
|             const amount = toBaseUnitAmount(4);
 | |
|             await stakers[0].stakeWithPoolAsync(poolId, amount);
 | |
|             // finalize
 | |
|             const reward = toBaseUnitAmount(10);
 | |
|             await payProtocolFeeAndFinalize(reward);
 | |
|             // sanity check final balances
 | |
|             await validateEndBalances({
 | |
|                 operatorWethBalance: reward,
 | |
|             });
 | |
|         });
 | |
|         it('Should give pool reward to delegator', async () => {
 | |
|             // delegate
 | |
|             const amount = toBaseUnitAmount(4);
 | |
|             await stakers[0].stakeWithPoolAsync(poolId, amount);
 | |
|             // skip epoch, so staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // finalize
 | |
|             const reward = toBaseUnitAmount(10);
 | |
|             await payProtocolFeeAndFinalize(reward);
 | |
|             // sanity check final balances
 | |
|             await validateEndBalances({
 | |
|                 stakerRewardBalance_1: reward,
 | |
|                 poolRewardBalance: reward,
 | |
|                 membersRewardBalance: reward,
 | |
|             });
 | |
|         });
 | |
|         it('Should split pool reward between delegators', async () => {
 | |
|             const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
 | |
|             const totalStakeAmount = toBaseUnitAmount(10);
 | |
|             // first staker delegates
 | |
|             await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]);
 | |
|             // second staker delegates
 | |
|             await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]);
 | |
|             // skip epoch, so staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // finalize
 | |
|             const reward = toBaseUnitAmount(10);
 | |
|             await payProtocolFeeAndFinalize(reward);
 | |
|             // sanity check final balances
 | |
|             await validateEndBalances({
 | |
|                 stakerRewardBalance_1: reward.times(stakeAmounts[0]).dividedToIntegerBy(totalStakeAmount),
 | |
|                 stakerRewardBalance_2: reward.times(stakeAmounts[1]).dividedToIntegerBy(totalStakeAmount),
 | |
|                 poolRewardBalance: reward,
 | |
|                 membersRewardBalance: reward,
 | |
|             });
 | |
|         });
 | |
|         it('Should split pool reward between delegators, when they join in different epochs', async () => {
 | |
|             // first staker delegates (epoch 1)
 | |
| 
 | |
|             const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
 | |
|             const totalStakeAmount = toBaseUnitAmount(10);
 | |
|             await stakers[0].stakeAsync(stakeAmounts[0]);
 | |
|             await stakers[0].moveStakeAsync(
 | |
|                 new StakeInfo(StakeStatus.Undelegated),
 | |
|                 new StakeInfo(StakeStatus.Delegated, poolId),
 | |
|                 stakeAmounts[0],
 | |
|             );
 | |
| 
 | |
|             // skip epoch, so staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
| 
 | |
|             // second staker delegates (epoch 2)
 | |
|             await stakers[1].stakeAsync(stakeAmounts[1]);
 | |
|             await stakers[1].moveStakeAsync(
 | |
|                 new StakeInfo(StakeStatus.Undelegated),
 | |
|                 new StakeInfo(StakeStatus.Delegated, poolId),
 | |
|                 stakeAmounts[1],
 | |
|             );
 | |
| 
 | |
|             // skip epoch, so staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // finalize
 | |
| 
 | |
|             const reward = toBaseUnitAmount(10);
 | |
|             await payProtocolFeeAndFinalize(reward);
 | |
|             // sanity check final balances
 | |
|             await validateEndBalances({
 | |
|                 stakerRewardBalance_1: reward.times(stakeAmounts[0]).dividedToIntegerBy(totalStakeAmount),
 | |
|                 stakerRewardBalance_2: reward.times(stakeAmounts[1]).dividedToIntegerBy(totalStakeAmount),
 | |
|                 poolRewardBalance: reward,
 | |
|                 membersRewardBalance: reward,
 | |
|             });
 | |
|         });
 | |
|         it('Should give pool reward to delegators only for the epoch during which they delegated', async () => {
 | |
|             const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
 | |
|             const totalStakeAmount = toBaseUnitAmount(10);
 | |
|             // first staker delegates (epoch 1)
 | |
|             await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]);
 | |
|             // skip epoch, so first staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // second staker delegates (epoch 2)
 | |
|             await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]);
 | |
|             // only the first staker will get this reward
 | |
|             const rewardForOnlyFirstDelegator = toBaseUnitAmount(10);
 | |
|             await payProtocolFeeAndFinalize(rewardForOnlyFirstDelegator);
 | |
|             // finalize
 | |
|             const rewardForBothDelegators = toBaseUnitAmount(20);
 | |
|             await payProtocolFeeAndFinalize(rewardForBothDelegators);
 | |
|             // sanity check final balances
 | |
|             await validateEndBalances({
 | |
|                 stakerRewardBalance_1: rewardForOnlyFirstDelegator.plus(
 | |
|                     rewardForBothDelegators.times(stakeAmounts[0]).dividedToIntegerBy(totalStakeAmount),
 | |
|                 ),
 | |
|                 stakerRewardBalance_2: rewardForBothDelegators
 | |
|                     .times(stakeAmounts[1])
 | |
|                     .dividedToIntegerBy(totalStakeAmount),
 | |
|                 poolRewardBalance: rewardForOnlyFirstDelegator.plus(rewardForBothDelegators),
 | |
|                 membersRewardBalance: rewardForOnlyFirstDelegator.plus(rewardForBothDelegators),
 | |
|             });
 | |
|         });
 | |
|         it('Should split pool reward between delegators, over several consecutive epochs', async () => {
 | |
|             const rewardForOnlyFirstDelegator = toBaseUnitAmount(10);
 | |
|             const sharedRewards = [
 | |
|                 toBaseUnitAmount(20),
 | |
|                 toBaseUnitAmount(16),
 | |
|                 toBaseUnitAmount(24),
 | |
|                 toBaseUnitAmount(5),
 | |
|                 toBaseUnitAmount(0),
 | |
|                 toBaseUnitAmount(17),
 | |
|             ];
 | |
|             const totalSharedRewardsAsNumber = _.sumBy(sharedRewards, v => {
 | |
|                 return v.toNumber();
 | |
|             });
 | |
|             const totalSharedRewards = new BigNumber(totalSharedRewardsAsNumber);
 | |
|             const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
 | |
|             const totalStakeAmount = toBaseUnitAmount(10);
 | |
|             // first staker delegates (epoch 1)
 | |
|             await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]);
 | |
|             // skip epoch, so first staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // second staker delegates (epoch 2)
 | |
|             await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]);
 | |
|             // only the first staker will get this reward
 | |
|             await payProtocolFeeAndFinalize(rewardForOnlyFirstDelegator);
 | |
|             // earn a bunch of rewards
 | |
|             for (const reward of sharedRewards) {
 | |
|                 await payProtocolFeeAndFinalize(reward);
 | |
|             }
 | |
|             // sanity check final balances
 | |
|             await validateEndBalances({
 | |
|                 stakerRewardBalance_1: rewardForOnlyFirstDelegator.plus(
 | |
|                     totalSharedRewards.times(stakeAmounts[0]).dividedToIntegerBy(totalStakeAmount),
 | |
|                 ),
 | |
|                 stakerRewardBalance_2: totalSharedRewards.times(stakeAmounts[1]).dividedToIntegerBy(totalStakeAmount),
 | |
|                 poolRewardBalance: rewardForOnlyFirstDelegator.plus(totalSharedRewards),
 | |
|                 membersRewardBalance: rewardForOnlyFirstDelegator.plus(totalSharedRewards),
 | |
|             });
 | |
|         });
 | |
|         it('Should withdraw existing rewards when undelegating stake', async () => {
 | |
|             const stakeAmount = toBaseUnitAmount(4);
 | |
|             // first staker delegates (epoch 1)
 | |
|             await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
 | |
|             // skip epoch, so first staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // earn reward
 | |
|             const reward = toBaseUnitAmount(10);
 | |
|             await payProtocolFeeAndFinalize(reward);
 | |
|             // undelegate (withdraws delegator's rewards)
 | |
|             await stakers[0].moveStakeAsync(
 | |
|                 new StakeInfo(StakeStatus.Delegated, poolId),
 | |
|                 new StakeInfo(StakeStatus.Undelegated),
 | |
|                 stakeAmount,
 | |
|             );
 | |
|             // sanity check final balances
 | |
|             await validateEndBalances({
 | |
|                 stakerRewardBalance_1: constants.ZERO_AMOUNT,
 | |
|                 stakerWethBalance_1: reward,
 | |
|             });
 | |
|         });
 | |
|         it('Should withdraw existing rewards correctly when delegating more stake', async () => {
 | |
|             const stakeAmount = toBaseUnitAmount(4);
 | |
|             // first staker delegates (epoch 1)
 | |
|             await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
 | |
|             // skip epoch, so first staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // earn reward
 | |
|             const reward = toBaseUnitAmount(10);
 | |
|             await payProtocolFeeAndFinalize(reward);
 | |
|             // add more stake
 | |
|             await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
 | |
|             // sanity check final balances
 | |
|             await validateEndBalances({
 | |
|                 stakerRewardBalance_1: constants.ZERO_AMOUNT,
 | |
|                 stakerWethBalance_1: reward,
 | |
|             });
 | |
|         });
 | |
|         it('Should continue earning rewards after adding more stake and progressing several epochs', async () => {
 | |
|             const rewardBeforeAddingMoreStake = toBaseUnitAmount(10);
 | |
|             const rewardsAfterAddingMoreStake = [
 | |
|                 toBaseUnitAmount(20),
 | |
|                 toBaseUnitAmount(16),
 | |
|                 toBaseUnitAmount(24),
 | |
|                 toBaseUnitAmount(5),
 | |
|                 toBaseUnitAmount(0),
 | |
|                 toBaseUnitAmount(17),
 | |
|             ];
 | |
|             const totalRewardsAfterAddingMoreStake = BigNumber.sum(...rewardsAfterAddingMoreStake);
 | |
|             const stakeAmounts = [toBaseUnitAmount(4), toBaseUnitAmount(6)];
 | |
|             const totalStake = BigNumber.sum(...stakeAmounts);
 | |
|             // first staker delegates (epoch 1)
 | |
|             await stakers[0].stakeWithPoolAsync(poolId, stakeAmounts[0]);
 | |
|             // skip epoch, so first staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // second staker delegates (epoch 2)
 | |
|             await stakers[1].stakeWithPoolAsync(poolId, stakeAmounts[1]);
 | |
|             // only the first staker will get this reward
 | |
|             await payProtocolFeeAndFinalize(rewardBeforeAddingMoreStake);
 | |
|             // earn a bunch of rewards
 | |
|             for (const reward of rewardsAfterAddingMoreStake) {
 | |
|                 await payProtocolFeeAndFinalize(reward);
 | |
|             }
 | |
|             // sanity check final balances
 | |
|             await validateEndBalances({
 | |
|                 stakerRewardBalance_1: rewardBeforeAddingMoreStake.plus(
 | |
|                     totalRewardsAfterAddingMoreStake
 | |
|                         .times(stakeAmounts[0])
 | |
|                         .dividedBy(totalStake)
 | |
|                         .integerValue(BigNumber.ROUND_DOWN),
 | |
|                 ),
 | |
|                 stakerRewardBalance_2: totalRewardsAfterAddingMoreStake
 | |
|                     .times(stakeAmounts[1])
 | |
|                     .dividedBy(totalStake)
 | |
|                     .integerValue(BigNumber.ROUND_DOWN),
 | |
|                 poolRewardBalance: rewardBeforeAddingMoreStake.plus(totalRewardsAfterAddingMoreStake),
 | |
|                 membersRewardBalance: rewardBeforeAddingMoreStake.plus(totalRewardsAfterAddingMoreStake),
 | |
|             });
 | |
|         });
 | |
|         it('Should stop collecting rewards after undelegating', async () => {
 | |
|             // first staker delegates (epoch 1)
 | |
|             const rewardForDelegator = toBaseUnitAmount(10);
 | |
|             const rewardNotForDelegator = toBaseUnitAmount(7);
 | |
|             const stakeAmount = toBaseUnitAmount(4);
 | |
|             await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
 | |
|             // skip epoch, so first staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // earn reward
 | |
|             await payProtocolFeeAndFinalize(rewardForDelegator);
 | |
| 
 | |
|             // undelegate stake and finalize epoch
 | |
|             await stakers[0].moveStakeAsync(
 | |
|                 new StakeInfo(StakeStatus.Delegated, poolId),
 | |
|                 new StakeInfo(StakeStatus.Undelegated),
 | |
|                 stakeAmount,
 | |
|             );
 | |
| 
 | |
|             await payProtocolFeeAndFinalize();
 | |
| 
 | |
|             // this should not go do the delegator
 | |
|             await payProtocolFeeAndFinalize(rewardNotForDelegator);
 | |
| 
 | |
|             // sanity check final balances
 | |
|             await validateEndBalances({
 | |
|                 stakerWethBalance_1: rewardForDelegator,
 | |
|                 operatorWethBalance: rewardNotForDelegator,
 | |
|             });
 | |
|         });
 | |
|         it('Should stop collecting rewards after undelegating, after several epochs', async () => {
 | |
|             // first staker delegates (epoch 1)
 | |
|             const rewardForDelegator = toBaseUnitAmount(10);
 | |
|             const rewardsNotForDelegator = [
 | |
|                 toBaseUnitAmount(20),
 | |
|                 toBaseUnitAmount(16),
 | |
|                 toBaseUnitAmount(24),
 | |
|                 toBaseUnitAmount(5),
 | |
|                 toBaseUnitAmount(0),
 | |
|                 toBaseUnitAmount(17),
 | |
|             ];
 | |
|             const totalRewardsNotForDelegator = BigNumber.sum(...rewardsNotForDelegator);
 | |
|             const stakeAmount = toBaseUnitAmount(4);
 | |
|             await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
 | |
|             // skip epoch, so first staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // earn reward
 | |
|             await payProtocolFeeAndFinalize(rewardForDelegator);
 | |
|             // undelegate stake and finalize epoch
 | |
|             await stakers[0].moveStakeAsync(
 | |
|                 new StakeInfo(StakeStatus.Delegated, poolId),
 | |
|                 new StakeInfo(StakeStatus.Undelegated),
 | |
|                 stakeAmount,
 | |
|             );
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // this should not go do the delegator
 | |
|             for (const reward of rewardsNotForDelegator) {
 | |
|                 await payProtocolFeeAndFinalize(reward);
 | |
|             }
 | |
|             // sanity check final balances
 | |
|             await validateEndBalances({
 | |
|                 stakerWethBalance_1: rewardForDelegator,
 | |
|                 operatorWethBalance: totalRewardsNotForDelegator,
 | |
|             });
 | |
|         });
 | |
|         it('Should collect fees correctly when leaving and returning to a pool', async () => {
 | |
|             // first staker delegates (epoch 1)
 | |
|             const rewardsForDelegator = [toBaseUnitAmount(10), toBaseUnitAmount(15)];
 | |
|             const rewardNotForDelegator = toBaseUnitAmount(7);
 | |
|             const stakeAmount = toBaseUnitAmount(4);
 | |
|             await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
 | |
|             // skip epoch, so first staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // earn reward
 | |
|             await payProtocolFeeAndFinalize(rewardsForDelegator[0]);
 | |
|             // undelegate stake and finalize epoch
 | |
|             await stakers[0].moveStakeAsync(
 | |
|                 new StakeInfo(StakeStatus.Delegated, poolId),
 | |
|                 new StakeInfo(StakeStatus.Undelegated),
 | |
|                 stakeAmount,
 | |
|             );
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // this should not go do the delegator
 | |
|             await payProtocolFeeAndFinalize(rewardNotForDelegator);
 | |
|             // delegate stake and go to next epoch
 | |
|             await stakers[0].moveStakeAsync(
 | |
|                 new StakeInfo(StakeStatus.Undelegated),
 | |
|                 new StakeInfo(StakeStatus.Delegated, poolId),
 | |
|                 stakeAmount,
 | |
|             );
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // this reward should go to delegator
 | |
|             await payProtocolFeeAndFinalize(rewardsForDelegator[1]);
 | |
|             // sanity check final balances
 | |
|             await validateEndBalances({
 | |
|                 stakerRewardBalance_1: rewardsForDelegator[1],
 | |
|                 stakerWethBalance_1: rewardsForDelegator[0],
 | |
|                 operatorWethBalance: rewardNotForDelegator,
 | |
|                 poolRewardBalance: rewardsForDelegator[1],
 | |
|             });
 | |
|         });
 | |
|         it('Should collect fees correctly when re-delegating after un-delegating', async () => {
 | |
|             // Note - there are two ranges over which payouts are computed (see _computeRewardBalanceOfDelegator).
 | |
|             // This triggers the first range (rewards for `delegatedStake.currentEpoch`), but not the second.
 | |
|             // first staker delegates (epoch 1)
 | |
|             const rewardForDelegator = toBaseUnitAmount(10);
 | |
|             const stakeAmount = toBaseUnitAmount(4);
 | |
|             await stakers[0].stakeAsync(stakeAmount);
 | |
|             await stakers[0].moveStakeAsync(
 | |
|                 new StakeInfo(StakeStatus.Undelegated),
 | |
|                 new StakeInfo(StakeStatus.Delegated, poolId),
 | |
|                 stakeAmount,
 | |
|             );
 | |
|             // skip epoch, so staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // undelegate stake and finalize epoch
 | |
|             await stakers[0].moveStakeAsync(
 | |
|                 new StakeInfo(StakeStatus.Delegated, poolId),
 | |
|                 new StakeInfo(StakeStatus.Undelegated),
 | |
|                 stakeAmount,
 | |
|             );
 | |
|             // this should go to the delegator
 | |
|             await payProtocolFeeAndFinalize(rewardForDelegator);
 | |
|             // delegate stake ~ this will result in a payout where rewards are computed on
 | |
|             // the balance's `currentEpochBalance` field but not the `nextEpochBalance` field.
 | |
|             await stakers[0].moveStakeAsync(
 | |
|                 new StakeInfo(StakeStatus.Undelegated),
 | |
|                 new StakeInfo(StakeStatus.Delegated, poolId),
 | |
|                 stakeAmount,
 | |
|             );
 | |
|             // sanity check final balances
 | |
|             await validateEndBalances({
 | |
|                 stakerRewardBalance_1: constants.ZERO_AMOUNT,
 | |
|                 stakerWethBalance_1: rewardForDelegator,
 | |
|                 operatorWethBalance: constants.ZERO_AMOUNT,
 | |
|                 poolRewardBalance: constants.ZERO_AMOUNT,
 | |
|             });
 | |
|         });
 | |
|         it('Should withdraw delegator rewards when calling `withdrawDelegatorRewards`', async () => {
 | |
|             // first staker delegates (epoch 1)
 | |
|             const rewardForDelegator = toBaseUnitAmount(10);
 | |
|             const stakeAmount = toBaseUnitAmount(4);
 | |
|             await stakers[0].stakeAsync(stakeAmount);
 | |
|             await stakers[0].moveStakeAsync(
 | |
|                 new StakeInfo(StakeStatus.Undelegated),
 | |
|                 new StakeInfo(StakeStatus.Delegated, poolId),
 | |
|                 stakeAmount,
 | |
|             );
 | |
|             // skip epoch, so staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // this should go to the delegator
 | |
|             await payProtocolFeeAndFinalize(rewardForDelegator);
 | |
|             await stakingApiWrapper.stakingContract.withdrawDelegatorRewards(poolId).awaitTransactionSuccessAsync({
 | |
|                 from: stakers[0].getOwner(),
 | |
|             });
 | |
|             // sanity check final balances
 | |
|             await validateEndBalances({
 | |
|                 stakerRewardBalance_1: constants.ZERO_AMOUNT,
 | |
|                 stakerWethBalance_1: rewardForDelegator,
 | |
|                 operatorWethBalance: constants.ZERO_AMOUNT,
 | |
|                 poolRewardBalance: constants.ZERO_AMOUNT,
 | |
|             });
 | |
|         });
 | |
|         it('should fail to withdraw delegator rewards if the pool has not been finalized for the previous epoch', async () => {
 | |
|             const rewardForDelegator = toBaseUnitAmount(10);
 | |
|             const stakeAmount = toBaseUnitAmount(4);
 | |
|             await stakers[0].stakeAsync(stakeAmount);
 | |
|             await stakers[0].moveStakeAsync(
 | |
|                 new StakeInfo(StakeStatus.Undelegated),
 | |
|                 new StakeInfo(StakeStatus.Delegated, poolId),
 | |
|                 stakeAmount,
 | |
|             );
 | |
|             await stakingApiWrapper.stakingContract
 | |
|                 .payProtocolFee(poolOperator.getOwner(), takerAddress, rewardForDelegator)
 | |
|                 .awaitTransactionSuccessAsync({ from: exchangeAddress, value: rewardForDelegator });
 | |
|             const currentEpoch = await stakingApiWrapper.stakingContract.currentEpoch().callAsync();
 | |
|             await stakingApiWrapper.utils.fastForwardToNextEpochAsync();
 | |
|             await stakingApiWrapper.utils.endEpochAsync();
 | |
|             const expectedError = new StakingRevertErrors.PoolNotFinalizedError(poolId, currentEpoch);
 | |
|             expect(
 | |
|                 stakingApiWrapper.stakingContract.withdrawDelegatorRewards(poolId).awaitTransactionSuccessAsync({
 | |
|                     from: stakers[0].getOwner(),
 | |
|                 }),
 | |
|             ).to.revertWith(expectedError);
 | |
|         });
 | |
|         it(`payout should be based on stake at the time of rewards`, async () => {
 | |
|             const staker = stakers[0];
 | |
|             const stakeAmount = toBaseUnitAmount(5);
 | |
|             // stake and delegate
 | |
|             await stakers[0].stakeWithPoolAsync(poolId, stakeAmount);
 | |
|             // skip epoch, so staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // undelegate some stake
 | |
|             const undelegateAmount = toBaseUnitAmount(2.5);
 | |
|             await staker.moveStakeAsync(
 | |
|                 new StakeInfo(StakeStatus.Delegated, poolId),
 | |
|                 new StakeInfo(StakeStatus.Undelegated),
 | |
|                 undelegateAmount,
 | |
|             );
 | |
|             // finalize
 | |
|             const reward = toBaseUnitAmount(10);
 | |
|             await payProtocolFeeAndFinalize(reward);
 | |
|             // withdraw rewards
 | |
|             await staker.withdrawDelegatorRewardsAsync(poolId);
 | |
|             await validateEndBalances({
 | |
|                 stakerRewardBalance_1: toBaseUnitAmount(0),
 | |
|                 stakerWethBalance_1: reward,
 | |
|             });
 | |
|         });
 | |
|         it(`should split payout between two delegators when syncing rewards`, async () => {
 | |
|             const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)];
 | |
|             const totalStakeAmount = BigNumber.sum(...stakeAmounts);
 | |
|             // stake and delegate both
 | |
|             const stakersAndStake = shortZip(stakers, stakeAmounts);
 | |
|             for (const [staker, stakeAmount] of stakersAndStake) {
 | |
|                 await staker.stakeWithPoolAsync(poolId, stakeAmount);
 | |
|             }
 | |
|             // skip epoch, so stakers can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // finalize
 | |
|             const reward = toBaseUnitAmount(10);
 | |
|             await payProtocolFeeAndFinalize(reward);
 | |
|             // withdraw rewards
 | |
|             for (const [staker] of _.reverse(stakersAndStake)) {
 | |
|                 await staker.withdrawDelegatorRewardsAsync(poolId);
 | |
|             }
 | |
|             const expectedStakerRewards = stakeAmounts.map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount));
 | |
|             await validateEndBalances({
 | |
|                 stakerRewardBalance_1: toBaseUnitAmount(0),
 | |
|                 stakerRewardBalance_2: toBaseUnitAmount(0),
 | |
|                 stakerWethBalance_1: expectedStakerRewards[0],
 | |
|                 stakerWethBalance_2: expectedStakerRewards[1],
 | |
|                 poolRewardBalance: new BigNumber(1), // Rounding error
 | |
|                 membersRewardBalance: new BigNumber(1), // Rounding error
 | |
|             });
 | |
|         });
 | |
|         it(`delegator should not be credited payout twice by syncing rewards twice`, async () => {
 | |
|             const stakeAmounts = [toBaseUnitAmount(5), toBaseUnitAmount(10)];
 | |
|             const totalStakeAmount = BigNumber.sum(...stakeAmounts);
 | |
|             // stake and delegate both
 | |
|             const stakersAndStake = shortZip(stakers, stakeAmounts);
 | |
|             for (const [staker, stakeAmount] of stakersAndStake) {
 | |
|                 await staker.stakeWithPoolAsync(poolId, stakeAmount);
 | |
|             }
 | |
|             // skip epoch, so staker can start earning rewards
 | |
|             await payProtocolFeeAndFinalize();
 | |
|             // finalize
 | |
|             const reward = toBaseUnitAmount(10);
 | |
|             await payProtocolFeeAndFinalize(reward);
 | |
|             const expectedStakerRewards = stakeAmounts.map(n => reward.times(n).dividedToIntegerBy(totalStakeAmount));
 | |
|             await validateEndBalances({
 | |
|                 stakerRewardBalance_1: expectedStakerRewards[0],
 | |
|                 stakerRewardBalance_2: expectedStakerRewards[1],
 | |
|                 stakerWethBalance_1: toBaseUnitAmount(0),
 | |
|                 stakerWethBalance_2: toBaseUnitAmount(0),
 | |
|                 poolRewardBalance: reward,
 | |
|                 membersRewardBalance: reward,
 | |
|             });
 | |
|             // First staker will withdraw rewards.
 | |
|             const sneakyStaker = stakers[0];
 | |
|             const sneakyStakerExpectedWethBalance = expectedStakerRewards[0];
 | |
|             await sneakyStaker.withdrawDelegatorRewardsAsync(poolId);
 | |
|             // Should have been credited the correct amount of rewards.
 | |
|             let sneakyStakerWethBalance = await stakingApiWrapper.wethContract
 | |
|                 .balanceOf(sneakyStaker.getOwner())
 | |
|                 .callAsync();
 | |
|             expect(sneakyStakerWethBalance, 'WETH balance after first undelegate').to.bignumber.eq(
 | |
|                 sneakyStakerExpectedWethBalance,
 | |
|             );
 | |
|             // Now he'll try to do it again to see if he gets credited twice.
 | |
|             await sneakyStaker.withdrawDelegatorRewardsAsync(poolId);
 | |
|             /// The total amount credited should remain the same.
 | |
|             sneakyStakerWethBalance = await stakingApiWrapper.wethContract
 | |
|                 .balanceOf(sneakyStaker.getOwner())
 | |
|                 .callAsync();
 | |
|             expect(sneakyStakerWethBalance, 'WETH balance after second undelegate').to.bignumber.eq(
 | |
|                 sneakyStakerExpectedWethBalance,
 | |
|             );
 | |
|         });
 | |
|     });
 | |
| });
 | |
| // tslint:enable:no-unnecessary-type-assertion
 |