601 lines
29 KiB
TypeScript
601 lines
29 KiB
TypeScript
import { ERC20ProxyContract, ERC20Wrapper } from '@0x/contracts-asset-proxy';
|
|
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
|
|
import { blockchainTests, describe, expect, provider, web3Wrapper } from '@0x/contracts-test-utils';
|
|
import { BigNumber } from '@0x/utils';
|
|
import * as _ from 'lodash';
|
|
|
|
import { FinalizerActor } from './actors/finalizer_actor';
|
|
import { StakerActor } from './actors/staker_actor';
|
|
import { StakingWrapper } from './utils/staking_wrapper';
|
|
import { MembersByPoolId, OperatorByPoolId, StakeState } from './utils/types';
|
|
|
|
// tslint:disable:no-unnecessary-type-assertion
|
|
// tslint:disable:max-file-line-count
|
|
blockchainTests.resets('Testing Rewards', () => {
|
|
// constants
|
|
const ZRX_TOKEN_DECIMALS = new BigNumber(18);
|
|
// tokens & addresses
|
|
let accounts: string[];
|
|
let owner: string;
|
|
let actors: string[];
|
|
let exchangeAddress: string;
|
|
let takerAddress: string;
|
|
let zrxTokenContract: DummyERC20TokenContract;
|
|
let erc20ProxyContract: ERC20ProxyContract;
|
|
// wrappers
|
|
let stakingWrapper: StakingWrapper;
|
|
// let testWrapper: TestRewardBalancesContract;
|
|
let erc20Wrapper: ERC20Wrapper;
|
|
// test parameters
|
|
let stakers: StakerActor[];
|
|
let poolId: string;
|
|
let poolOperator: string;
|
|
let finalizer: FinalizerActor;
|
|
// tests
|
|
before(async () => {
|
|
// create accounts
|
|
accounts = await web3Wrapper.getAvailableAddressesAsync();
|
|
owner = accounts[0];
|
|
exchangeAddress = accounts[1];
|
|
takerAddress = accounts[2];
|
|
actors = accounts.slice(3);
|
|
// deploy erƒsc20 proxy
|
|
erc20Wrapper = new ERC20Wrapper(provider, accounts, owner);
|
|
erc20ProxyContract = await erc20Wrapper.deployProxyAsync();
|
|
// deploy zrx token
|
|
[zrxTokenContract] = await erc20Wrapper.deployDummyTokensAsync(1, ZRX_TOKEN_DECIMALS);
|
|
await erc20Wrapper.setBalancesAndAllowancesAsync();
|
|
// deploy staking contracts
|
|
stakingWrapper = new StakingWrapper(provider, owner, erc20ProxyContract, zrxTokenContract, accounts);
|
|
await stakingWrapper.deployAndConfigureContractsAsync();
|
|
// setup stakers
|
|
stakers = [new StakerActor(actors[0], stakingWrapper), new StakerActor(actors[1], stakingWrapper)];
|
|
// setup pools
|
|
poolOperator = actors[2];
|
|
poolId = await stakingWrapper.createStakingPoolAsync(poolOperator, 0);
|
|
// add operator as maker
|
|
const approvalMessage = stakingWrapper.signApprovalForStakingPool(poolId, poolOperator);
|
|
await stakingWrapper.addMakerToStakingPoolAsync(poolId, poolOperator, approvalMessage.signature, poolOperator);
|
|
// set exchange address
|
|
await stakingWrapper.addExchangeAddressAsync(exchangeAddress);
|
|
// associate operators for tracking in Finalizer
|
|
const operatorByPoolId: OperatorByPoolId = {};
|
|
operatorByPoolId[poolId] = poolOperator;
|
|
operatorByPoolId[poolId] = poolOperator;
|
|
// associate actors with pools for tracking in Finalizer
|
|
const membersByPoolId: MembersByPoolId = {};
|
|
membersByPoolId[poolId] = [actors[0], actors[1]];
|
|
membersByPoolId[poolId] = [actors[0], actors[1]];
|
|
// create Finalizer actor
|
|
finalizer = new FinalizerActor(actors[3], stakingWrapper, [poolId], operatorByPoolId, membersByPoolId);
|
|
});
|
|
describe('Reward Simulation', () => {
|
|
interface EndBalances {
|
|
// staker 1
|
|
stakerRewardVaultBalance_1?: BigNumber;
|
|
stakerEthVaultBalance_1?: BigNumber;
|
|
// staker 2
|
|
stakerRewardVaultBalance_2?: BigNumber;
|
|
stakerEthVaultBalance_2?: BigNumber;
|
|
// operator
|
|
operatorRewardVaultBalance?: BigNumber;
|
|
operatorEthVaultBalance?: BigNumber;
|
|
// undivided balance in reward pool
|
|
poolRewardVaultBalance?: BigNumber;
|
|
membersRewardVaultBalance?: BigNumber;
|
|
}
|
|
const validateEndBalances = async (_expectedEndBalances: EndBalances): Promise<void> => {
|
|
const expectedEndBalances = {
|
|
// staker 1
|
|
stakerRewardVaultBalance_1:
|
|
_expectedEndBalances.stakerRewardVaultBalance_1 !== undefined
|
|
? _expectedEndBalances.stakerRewardVaultBalance_1
|
|
: ZERO,
|
|
stakerEthVaultBalance_1:
|
|
_expectedEndBalances.stakerEthVaultBalance_1 !== undefined
|
|
? _expectedEndBalances.stakerEthVaultBalance_1
|
|
: ZERO,
|
|
// staker 2
|
|
stakerRewardVaultBalance_2:
|
|
_expectedEndBalances.stakerRewardVaultBalance_2 !== undefined
|
|
? _expectedEndBalances.stakerRewardVaultBalance_2
|
|
: ZERO,
|
|
stakerEthVaultBalance_2:
|
|
_expectedEndBalances.stakerEthVaultBalance_2 !== undefined
|
|
? _expectedEndBalances.stakerEthVaultBalance_2
|
|
: ZERO,
|
|
// operator
|
|
operatorRewardVaultBalance:
|
|
_expectedEndBalances.operatorRewardVaultBalance !== undefined
|
|
? _expectedEndBalances.operatorRewardVaultBalance
|
|
: ZERO,
|
|
operatorEthVaultBalance:
|
|
_expectedEndBalances.operatorEthVaultBalance !== undefined
|
|
? _expectedEndBalances.operatorEthVaultBalance
|
|
: ZERO,
|
|
// undivided balance in reward pool
|
|
poolRewardVaultBalance:
|
|
_expectedEndBalances.poolRewardVaultBalance !== undefined
|
|
? _expectedEndBalances.poolRewardVaultBalance
|
|
: ZERO,
|
|
membersRewardVaultBalance:
|
|
_expectedEndBalances.membersRewardVaultBalance !== undefined
|
|
? _expectedEndBalances.membersRewardVaultBalance
|
|
: ZERO,
|
|
};
|
|
const finalEndBalancesAsArray = await Promise.all([
|
|
// staker 1
|
|
stakingWrapper.computeRewardBalanceOfStakingPoolMemberAsync(poolId, stakers[0].getOwner()),
|
|
stakingWrapper.getEthVaultContract().balanceOf.callAsync(stakers[0].getOwner()),
|
|
// staker 2
|
|
stakingWrapper.computeRewardBalanceOfStakingPoolMemberAsync(poolId, stakers[1].getOwner()),
|
|
stakingWrapper.getEthVaultContract().balanceOf.callAsync(stakers[1].getOwner()),
|
|
// operator
|
|
stakingWrapper.rewardVaultBalanceOfOperatorAsync(poolId),
|
|
stakingWrapper.getEthVaultContract().balanceOf.callAsync(poolOperator),
|
|
// undivided balance in reward pool
|
|
stakingWrapper.rewardVaultBalanceOfAsync(poolId),
|
|
stakingWrapper.rewardVaultBalanceOfMembersAsync(poolId),
|
|
]);
|
|
expect(finalEndBalancesAsArray[0], 'stakerRewardVaultBalance_1').to.be.bignumber.equal(
|
|
expectedEndBalances.stakerRewardVaultBalance_1,
|
|
);
|
|
expect(finalEndBalancesAsArray[1], 'stakerEthVaultBalance_1').to.be.bignumber.equal(
|
|
expectedEndBalances.stakerEthVaultBalance_1,
|
|
);
|
|
expect(finalEndBalancesAsArray[2], 'stakerRewardVaultBalance_2').to.be.bignumber.equal(
|
|
expectedEndBalances.stakerRewardVaultBalance_2,
|
|
);
|
|
expect(finalEndBalancesAsArray[3], 'stakerEthVaultBalance_2').to.be.bignumber.equal(
|
|
expectedEndBalances.stakerEthVaultBalance_2,
|
|
);
|
|
expect(finalEndBalancesAsArray[4], 'operatorRewardVaultBalance').to.be.bignumber.equal(
|
|
expectedEndBalances.operatorRewardVaultBalance,
|
|
);
|
|
expect(finalEndBalancesAsArray[5], 'operatorEthVaultBalance').to.be.bignumber.equal(
|
|
expectedEndBalances.operatorEthVaultBalance,
|
|
);
|
|
expect(finalEndBalancesAsArray[6], 'poolRewardVaultBalance').to.be.bignumber.equal(
|
|
expectedEndBalances.poolRewardVaultBalance,
|
|
);
|
|
expect(finalEndBalancesAsArray[7], 'membersRewardVaultBalance').to.be.bignumber.equal(
|
|
expectedEndBalances.membersRewardVaultBalance,
|
|
);
|
|
};
|
|
const payProtocolFeeAndFinalize = async (_fee?: BigNumber) => {
|
|
const fee = _fee !== undefined ? _fee : ZERO;
|
|
if (!fee.eq(ZERO)) {
|
|
await stakingWrapper.payProtocolFeeAsync(poolOperator, takerAddress, fee, fee, exchangeAddress);
|
|
}
|
|
await finalizer.finalizeAsync([{ reward: fee, poolId }]);
|
|
};
|
|
const ZERO = new BigNumber(0);
|
|
it('Reward balance should be zero in same epoch as delegation', async () => {
|
|
const amount = StakingWrapper.toBaseUnitAmount(4);
|
|
await stakers[0].stakeAsync(amount);
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.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 = StakingWrapper.toBaseUnitAmount(10);
|
|
await payProtocolFeeAndFinalize(reward);
|
|
// sanity check final balances - all zero
|
|
await validateEndBalances({
|
|
operatorRewardVaultBalance: reward,
|
|
poolRewardVaultBalance: 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 = StakingWrapper.toBaseUnitAmount(4);
|
|
await stakers[0].stakeAsync(amount);
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, poolId },
|
|
amount,
|
|
);
|
|
// finalize
|
|
const reward = StakingWrapper.toBaseUnitAmount(10);
|
|
await payProtocolFeeAndFinalize(reward);
|
|
// sanity check final balances
|
|
await validateEndBalances({
|
|
operatorRewardVaultBalance: reward,
|
|
poolRewardVaultBalance: reward,
|
|
});
|
|
});
|
|
it('Should give pool reward to delegator', async () => {
|
|
// delegate
|
|
const amount = StakingWrapper.toBaseUnitAmount(4);
|
|
await stakers[0].stakeAsync(amount);
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, poolId },
|
|
amount,
|
|
);
|
|
// skip epoch, so staker can start earning rewards
|
|
await payProtocolFeeAndFinalize();
|
|
// finalize
|
|
const reward = StakingWrapper.toBaseUnitAmount(10);
|
|
await payProtocolFeeAndFinalize(reward);
|
|
// sanity check final balances
|
|
await validateEndBalances({
|
|
stakerRewardVaultBalance_1: reward,
|
|
poolRewardVaultBalance: reward,
|
|
membersRewardVaultBalance: reward,
|
|
});
|
|
});
|
|
it('Should split pool reward between delegators', async () => {
|
|
// first staker delegates
|
|
const stakeAmounts = [StakingWrapper.toBaseUnitAmount(4), StakingWrapper.toBaseUnitAmount(6)];
|
|
const totalStakeAmount = StakingWrapper.toBaseUnitAmount(10);
|
|
await stakers[0].stakeAsync(stakeAmounts[0]);
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, poolId },
|
|
stakeAmounts[0],
|
|
);
|
|
// second staker delegates
|
|
await stakers[1].stakeAsync(stakeAmounts[1]);
|
|
await stakers[1].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, poolId },
|
|
stakeAmounts[1],
|
|
);
|
|
// skip epoch, so staker can start earning rewards
|
|
await payProtocolFeeAndFinalize();
|
|
// finalize
|
|
const reward = StakingWrapper.toBaseUnitAmount(10);
|
|
await payProtocolFeeAndFinalize(reward);
|
|
// sanity check final balances
|
|
await validateEndBalances({
|
|
stakerRewardVaultBalance_1: reward.times(stakeAmounts[0]).dividedToIntegerBy(totalStakeAmount),
|
|
stakerRewardVaultBalance_2: reward.times(stakeAmounts[1]).dividedToIntegerBy(totalStakeAmount),
|
|
poolRewardVaultBalance: reward,
|
|
membersRewardVaultBalance: reward,
|
|
});
|
|
});
|
|
it('Should split pool reward between delegators, when they join in different epochs', async () => {
|
|
// first staker delegates (epoch 0)
|
|
const stakeAmounts = [StakingWrapper.toBaseUnitAmount(4), StakingWrapper.toBaseUnitAmount(6)];
|
|
const totalStakeAmount = StakingWrapper.toBaseUnitAmount(10);
|
|
await stakers[0].stakeAsync(stakeAmounts[0]);
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, poolId },
|
|
stakeAmounts[0],
|
|
);
|
|
// skip epoch, so staker can start earning rewards
|
|
await payProtocolFeeAndFinalize();
|
|
// second staker delegates (epoch 1)
|
|
await stakers[1].stakeAsync(stakeAmounts[1]);
|
|
await stakers[1].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, poolId },
|
|
stakeAmounts[1],
|
|
);
|
|
// skip epoch, so staker can start earning rewards
|
|
await payProtocolFeeAndFinalize();
|
|
// finalize
|
|
const reward = StakingWrapper.toBaseUnitAmount(10);
|
|
await payProtocolFeeAndFinalize(reward);
|
|
// sanity check final balances
|
|
await validateEndBalances({
|
|
stakerRewardVaultBalance_1: reward.times(stakeAmounts[0]).dividedToIntegerBy(totalStakeAmount),
|
|
stakerRewardVaultBalance_2: reward.times(stakeAmounts[1]).dividedToIntegerBy(totalStakeAmount),
|
|
poolRewardVaultBalance: reward,
|
|
membersRewardVaultBalance: reward,
|
|
});
|
|
});
|
|
it('Should give pool reward to delegators only for the epoch during which they delegated', async () => {
|
|
// first staker delegates (epoch 0)
|
|
const stakeAmounts = [StakingWrapper.toBaseUnitAmount(4), StakingWrapper.toBaseUnitAmount(6)];
|
|
const totalStakeAmount = StakingWrapper.toBaseUnitAmount(10);
|
|
await stakers[0].stakeAsync(stakeAmounts[0]);
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, poolId },
|
|
stakeAmounts[0],
|
|
);
|
|
// skip epoch, so first staker can start earning rewards
|
|
await payProtocolFeeAndFinalize();
|
|
// second staker delegates (epoch 1)
|
|
await stakers[1].stakeAsync(stakeAmounts[1]);
|
|
await stakers[1].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, poolId },
|
|
stakeAmounts[1],
|
|
);
|
|
// only the first staker will get this reward
|
|
const rewardForOnlyFirstDelegator = StakingWrapper.toBaseUnitAmount(10);
|
|
await payProtocolFeeAndFinalize(rewardForOnlyFirstDelegator);
|
|
// finalize
|
|
const rewardForBothDelegators = StakingWrapper.toBaseUnitAmount(20);
|
|
await payProtocolFeeAndFinalize(rewardForBothDelegators);
|
|
// sanity check final balances
|
|
await validateEndBalances({
|
|
stakerRewardVaultBalance_1: rewardForOnlyFirstDelegator.plus(
|
|
rewardForBothDelegators.times(stakeAmounts[0]).dividedToIntegerBy(totalStakeAmount),
|
|
),
|
|
stakerRewardVaultBalance_2: rewardForBothDelegators
|
|
.times(stakeAmounts[1])
|
|
.dividedToIntegerBy(totalStakeAmount),
|
|
poolRewardVaultBalance: rewardForOnlyFirstDelegator.plus(rewardForBothDelegators),
|
|
membersRewardVaultBalance: rewardForOnlyFirstDelegator.plus(rewardForBothDelegators),
|
|
});
|
|
});
|
|
it('Should split pool reward between delegators, over several consecutive epochs', async () => {
|
|
const rewardForOnlyFirstDelegator = StakingWrapper.toBaseUnitAmount(10);
|
|
const sharedRewards = [
|
|
StakingWrapper.toBaseUnitAmount(20),
|
|
StakingWrapper.toBaseUnitAmount(16),
|
|
StakingWrapper.toBaseUnitAmount(24),
|
|
StakingWrapper.toBaseUnitAmount(5),
|
|
StakingWrapper.toBaseUnitAmount(0),
|
|
StakingWrapper.toBaseUnitAmount(17),
|
|
];
|
|
const totalSharedRewardsAsNumber = _.sumBy(sharedRewards, v => {
|
|
return v.toNumber();
|
|
});
|
|
const totalSharedRewards = new BigNumber(totalSharedRewardsAsNumber);
|
|
// first staker delegates (epoch 0)
|
|
const stakeAmounts = [StakingWrapper.toBaseUnitAmount(4), StakingWrapper.toBaseUnitAmount(6)];
|
|
const totalStakeAmount = StakingWrapper.toBaseUnitAmount(10);
|
|
await stakers[0].stakeAsync(stakeAmounts[0]);
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, poolId },
|
|
stakeAmounts[0],
|
|
);
|
|
// skip epoch, so first staker can start earning rewards
|
|
await payProtocolFeeAndFinalize();
|
|
// second staker delegates (epoch 1)
|
|
await stakers[1].stakeAsync(stakeAmounts[1]);
|
|
await stakers[1].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, 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({
|
|
stakerRewardVaultBalance_1: rewardForOnlyFirstDelegator.plus(
|
|
totalSharedRewards.times(stakeAmounts[0]).dividedToIntegerBy(totalStakeAmount),
|
|
),
|
|
stakerRewardVaultBalance_2: totalSharedRewards
|
|
.times(stakeAmounts[1])
|
|
.dividedToIntegerBy(totalStakeAmount),
|
|
poolRewardVaultBalance: rewardForOnlyFirstDelegator.plus(totalSharedRewards),
|
|
membersRewardVaultBalance: rewardForOnlyFirstDelegator.plus(totalSharedRewards),
|
|
});
|
|
});
|
|
it('Should send existing rewards from reward vault to eth vault correctly when undelegating stake', async () => {
|
|
// first staker delegates (epoch 0)
|
|
const stakeAmount = StakingWrapper.toBaseUnitAmount(4);
|
|
await stakers[0].stakeAsync(stakeAmount);
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, poolId },
|
|
stakeAmount,
|
|
);
|
|
// skip epoch, so first staker can start earning rewards
|
|
await payProtocolFeeAndFinalize();
|
|
// earn reward
|
|
const reward = StakingWrapper.toBaseUnitAmount(10);
|
|
await payProtocolFeeAndFinalize(reward);
|
|
// undelegate (moves delegator's from the transient reward vault into the eth vault)
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Delegated, poolId },
|
|
{ state: StakeState.Active },
|
|
stakeAmount,
|
|
);
|
|
// sanity check final balances
|
|
await validateEndBalances({
|
|
stakerRewardVaultBalance_1: ZERO,
|
|
stakerEthVaultBalance_1: reward,
|
|
});
|
|
});
|
|
it('Should send existing rewards from reward vault to eth vault correctly when delegating more stake', async () => {
|
|
// first staker delegates (epoch 0)
|
|
const stakeAmount = StakingWrapper.toBaseUnitAmount(4);
|
|
await stakers[0].stakeAsync(stakeAmount);
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, poolId },
|
|
stakeAmount,
|
|
);
|
|
// skip epoch, so first staker can start earning rewards
|
|
await payProtocolFeeAndFinalize();
|
|
// earn reward
|
|
const reward = StakingWrapper.toBaseUnitAmount(10);
|
|
await payProtocolFeeAndFinalize(reward);
|
|
// add more stake
|
|
await stakers[0].stakeAsync(stakeAmount);
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, poolId },
|
|
stakeAmount,
|
|
);
|
|
// sanity check final balances
|
|
await validateEndBalances({
|
|
stakerRewardVaultBalance_1: ZERO,
|
|
stakerEthVaultBalance_1: reward,
|
|
});
|
|
});
|
|
it('Should continue earning rewards after adding more stake and progressing several epochs', async () => {
|
|
const rewardBeforeAddingMoreStake = StakingWrapper.toBaseUnitAmount(10);
|
|
const rewardsAfterAddingMoreStake = [
|
|
StakingWrapper.toBaseUnitAmount(20),
|
|
StakingWrapper.toBaseUnitAmount(16),
|
|
StakingWrapper.toBaseUnitAmount(24),
|
|
StakingWrapper.toBaseUnitAmount(5),
|
|
StakingWrapper.toBaseUnitAmount(0),
|
|
StakingWrapper.toBaseUnitAmount(17),
|
|
];
|
|
const totalRewardsAfterAddingMoreStake = new BigNumber(
|
|
_.sumBy(rewardsAfterAddingMoreStake, v => {
|
|
return v.toNumber();
|
|
}),
|
|
);
|
|
// first staker delegates (epoch 0)
|
|
const stakeAmounts = [StakingWrapper.toBaseUnitAmount(4), StakingWrapper.toBaseUnitAmount(6)];
|
|
await stakers[0].stakeAsync(stakeAmounts[0]);
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, poolId },
|
|
stakeAmounts[0],
|
|
);
|
|
// skip epoch, so first staker can start earning rewards
|
|
await payProtocolFeeAndFinalize();
|
|
// second staker delegates (epoch 1)
|
|
await stakers[0].stakeAsync(stakeAmounts[1]);
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, 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({
|
|
stakerRewardVaultBalance_1: rewardBeforeAddingMoreStake.plus(totalRewardsAfterAddingMoreStake),
|
|
poolRewardVaultBalance: rewardBeforeAddingMoreStake.plus(totalRewardsAfterAddingMoreStake),
|
|
membersRewardVaultBalance: rewardBeforeAddingMoreStake.plus(totalRewardsAfterAddingMoreStake),
|
|
});
|
|
});
|
|
it('Should stop collecting rewards after undelegating', async () => {
|
|
// first staker delegates (epoch 0)
|
|
const rewardForDelegator = StakingWrapper.toBaseUnitAmount(10);
|
|
const rewardNotForDelegator = StakingWrapper.toBaseUnitAmount(7);
|
|
const stakeAmount = StakingWrapper.toBaseUnitAmount(4);
|
|
await stakers[0].stakeAsync(stakeAmount);
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, 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(
|
|
{ state: StakeState.Delegated, poolId },
|
|
{ state: StakeState.Active },
|
|
stakeAmount,
|
|
);
|
|
await payProtocolFeeAndFinalize();
|
|
// this should not go do the delegator
|
|
await payProtocolFeeAndFinalize(rewardNotForDelegator);
|
|
// sanity check final balances
|
|
await validateEndBalances({
|
|
stakerEthVaultBalance_1: rewardForDelegator,
|
|
poolRewardVaultBalance: rewardNotForDelegator,
|
|
operatorRewardVaultBalance: rewardNotForDelegator,
|
|
});
|
|
});
|
|
it('Should stop collecting rewards after undelegating, after several epochs', async () => {
|
|
// first staker delegates (epoch 0)
|
|
const rewardForDelegator = StakingWrapper.toBaseUnitAmount(10);
|
|
const rewardsNotForDelegator = [
|
|
StakingWrapper.toBaseUnitAmount(20),
|
|
StakingWrapper.toBaseUnitAmount(16),
|
|
StakingWrapper.toBaseUnitAmount(24),
|
|
StakingWrapper.toBaseUnitAmount(5),
|
|
StakingWrapper.toBaseUnitAmount(0),
|
|
StakingWrapper.toBaseUnitAmount(17),
|
|
];
|
|
const totalRewardsNotForDelegator = new BigNumber(
|
|
_.sumBy(rewardsNotForDelegator, v => {
|
|
return v.toNumber();
|
|
}),
|
|
);
|
|
const stakeAmount = StakingWrapper.toBaseUnitAmount(4);
|
|
await stakers[0].stakeAsync(stakeAmount);
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, 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(
|
|
{ state: StakeState.Delegated, poolId },
|
|
{ state: StakeState.Active },
|
|
stakeAmount,
|
|
);
|
|
await payProtocolFeeAndFinalize();
|
|
// this should not go do the delegator
|
|
for (const reward of rewardsNotForDelegator) {
|
|
await payProtocolFeeAndFinalize(reward);
|
|
}
|
|
// sanity check final balances
|
|
await validateEndBalances({
|
|
stakerEthVaultBalance_1: rewardForDelegator,
|
|
poolRewardVaultBalance: totalRewardsNotForDelegator,
|
|
operatorRewardVaultBalance: totalRewardsNotForDelegator,
|
|
});
|
|
});
|
|
it('Should collect fees correctly when leaving and returning to a pool', async () => {
|
|
// first staker delegates (epoch 0)
|
|
const rewardsForDelegator = [StakingWrapper.toBaseUnitAmount(10), StakingWrapper.toBaseUnitAmount(15)];
|
|
const rewardNotForDelegator = StakingWrapper.toBaseUnitAmount(7);
|
|
const stakeAmount = StakingWrapper.toBaseUnitAmount(4);
|
|
await stakers[0].stakeAsync(stakeAmount);
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, 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(
|
|
{ state: StakeState.Delegated, poolId },
|
|
{ state: StakeState.Active },
|
|
stakeAmount,
|
|
);
|
|
await payProtocolFeeAndFinalize();
|
|
// this should not go do the delegator
|
|
await payProtocolFeeAndFinalize(rewardNotForDelegator);
|
|
// delegate stake and go to next epoch
|
|
await stakers[0].moveStakeAsync(
|
|
{ state: StakeState.Active },
|
|
{ state: StakeState.Delegated, poolId },
|
|
stakeAmount,
|
|
);
|
|
await payProtocolFeeAndFinalize();
|
|
// this reward should go to delegator
|
|
await payProtocolFeeAndFinalize(rewardsForDelegator[1]);
|
|
// sanity check final balances
|
|
await validateEndBalances({
|
|
stakerRewardVaultBalance_1: rewardsForDelegator[1],
|
|
stakerEthVaultBalance_1: rewardsForDelegator[0],
|
|
operatorRewardVaultBalance: rewardNotForDelegator,
|
|
poolRewardVaultBalance: rewardNotForDelegator.plus(rewardsForDelegator[1]),
|
|
membersRewardVaultBalance: rewardsForDelegator[1],
|
|
});
|
|
});
|
|
});
|
|
});
|
|
// tslint:enable:no-unnecessary-type-assertion
|