protocol/contracts/staking/test/rewards_test.ts
F. Eugene Aumson f11d8a5bd8
@0x/order-utils refactors for v3: orderParsingUtils, signatureUtils, orderHashUtils, RevertErrors, transactionHashUtils (#2321)
* move orderParsingUtils from order-utils to connect

* Remove many functions from signatureUtils

Removed from the exported object, that is.  All of them are used in
other existing code, so they were all moved to be as local to their
usage as possible.

* remove orderHashUtils.isValidOrderHash()

* Move all *RevertErrors from order-utils...

...into their respective @0x/contracts- packages.

* Refactor @0x/order-utils' orderHashUtils away

- Move existing routines into @0x/contracts-test-utils

- Migrate non-contract-test callers to a newly-exposed getOrderHash()
method in DevUtils.

* Move all *RevertErrors from @0x/utils...

...into their respective @0x/contracts- packages.

* rm transactionHashUtils.isValidTransactionHash()

* DevUtils.sol: Fail yarn test if too big to deploy

* Refactor @0x/order-utils transactionHashUtils away

- Move existing routines into @0x/contracts-test-utils

- Migrate non-contract-test callers to a newly-exposed
getTransactionHash() method in DevUtils.

* Consolidate `Removed export...` CHANGELOG entries

* Rm EthBalanceChecker from devutils wrapper exports

* Stop importing from '.' or '.../src'

* fix builds

* fix prettier; dangling promise

* increase max bundle size
2019-11-14 17:14:24 -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 { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import StakingRevertErrors = require('../src/staking_revert_errors');
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