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
 |