protocol/contracts/staking/test/rewards_test.ts
Xianny f0d7d10fe7
update abi-gen with new method interfaces (#2325)
* update abi-gen with new method interfaces

* wip: get all packages to build

* wip: get all packages to build

* Fix two contract wrapper calls

* Export necessary types part of the contract wrapper public interfaces

* Revive and fix wrapper_unit_tests

* Remove duplicate type

* Fix lib_exchange_rich_error_decoder tests

* Fix remaining test failures in contracts-* packages

* Prettier fixes

* remove transactionHelper

* lint and update changelogs

* Fix prettier

* Revert changes to reference docs

* Add back changelog already published and add revert changelog entry

* Add missing CHANGELOG entries

* Add missing comma

* Update mesh-rpc-client dep

* Update Mesh RPC logic in @0x/orderbook to v6.0.1-beta

* Align package versions
2019-11-14 11:22:29 -05:00

703 lines
35 KiB
TypeScript

import { ERC20Wrapper } from '@0x/contracts-asset-proxy';
import { blockchainTests, constants, describe, expect, shortZip } from '@0x/contracts-test-utils';
import { StakingRevertErrors } from '@0x/order-utils';
import { BigNumber } 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';
import { toBaseUnitAmount } from './utils/number_utils';
// 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