* 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
580 lines
27 KiB
TypeScript
580 lines
27 KiB
TypeScript
import {
|
|
blockchainTests,
|
|
constants,
|
|
expect,
|
|
filterLogsToArguments,
|
|
hexRandom,
|
|
Numberish,
|
|
shortZip,
|
|
} from '@0x/contracts-test-utils';
|
|
import { BigNumber } from '@0x/utils';
|
|
import { LogEntry } from 'ethereum-types';
|
|
import * as _ from 'lodash';
|
|
|
|
import { constants as stakingConstants } from '../../src/constants';
|
|
import StakingRevertErrors = require('../../src/staking_revert_errors');
|
|
|
|
import { artifacts } from '../artifacts';
|
|
import { assertIntegerRoughlyEquals, getRandomInteger, toBaseUnitAmount } from '../utils/number_utils';
|
|
|
|
import {
|
|
IStakingEventsEpochEndedEventArgs,
|
|
IStakingEventsEpochFinalizedEventArgs,
|
|
IStakingEventsEvents,
|
|
IStakingEventsRewardsPaidEventArgs,
|
|
TestFinalizerContract,
|
|
TestFinalizerDepositStakingPoolRewardsEventArgs as DepositStakingPoolRewardsEventArgs,
|
|
TestFinalizerEvents,
|
|
} from '../wrappers';
|
|
|
|
blockchainTests.resets('Finalizer unit tests', env => {
|
|
const { ZERO_AMOUNT } = constants;
|
|
const INITIAL_BALANCE = toBaseUnitAmount(32);
|
|
let operatorRewardsReceiver: string;
|
|
let membersRewardsReceiver: string;
|
|
let testContract: TestFinalizerContract;
|
|
|
|
before(async () => {
|
|
operatorRewardsReceiver = hexRandom(constants.ADDRESS_LENGTH);
|
|
membersRewardsReceiver = hexRandom(constants.ADDRESS_LENGTH);
|
|
testContract = await TestFinalizerContract.deployFrom0xArtifactAsync(
|
|
artifacts.TestFinalizer,
|
|
env.provider,
|
|
env.txDefaults,
|
|
artifacts,
|
|
operatorRewardsReceiver,
|
|
membersRewardsReceiver,
|
|
);
|
|
// Give the contract a balance.
|
|
await sendEtherAsync(testContract.address, INITIAL_BALANCE);
|
|
});
|
|
|
|
async function sendEtherAsync(to: string, amount: Numberish): Promise<void> {
|
|
await env.web3Wrapper.awaitTransactionSuccessAsync(
|
|
await env.web3Wrapper.sendTransactionAsync({
|
|
from: (await env.getAccountAddressesAsync())[0],
|
|
to,
|
|
value: new BigNumber(amount),
|
|
}),
|
|
);
|
|
}
|
|
|
|
interface ActivePoolOpts {
|
|
poolId: string;
|
|
operatorShare: number;
|
|
feesCollected: Numberish;
|
|
membersStake: Numberish;
|
|
weightedStake: Numberish;
|
|
}
|
|
|
|
async function addActivePoolAsync(opts?: Partial<ActivePoolOpts>): Promise<ActivePoolOpts> {
|
|
const maxAmount = toBaseUnitAmount(1e9);
|
|
const _opts = {
|
|
poolId: hexRandom(),
|
|
operatorShare: Math.floor(Math.random() * constants.PPM_DENOMINATOR) / constants.PPM_DENOMINATOR,
|
|
feesCollected: getRandomInteger(0, maxAmount),
|
|
membersStake: getRandomInteger(0, maxAmount),
|
|
weightedStake: getRandomInteger(0, maxAmount),
|
|
...opts,
|
|
};
|
|
await testContract
|
|
.addActivePool(
|
|
_opts.poolId,
|
|
new BigNumber(_opts.operatorShare * constants.PPM_DENOMINATOR).integerValue(),
|
|
new BigNumber(_opts.feesCollected),
|
|
new BigNumber(_opts.membersStake),
|
|
new BigNumber(_opts.weightedStake),
|
|
)
|
|
.awaitTransactionSuccessAsync();
|
|
return _opts;
|
|
}
|
|
|
|
interface UnfinalizedState {
|
|
rewardsAvailable: Numberish;
|
|
numPoolsToFinalize: Numberish;
|
|
totalFeesCollected: Numberish;
|
|
totalWeightedStake: Numberish;
|
|
totalRewardsFinalized: Numberish;
|
|
}
|
|
|
|
async function getUnfinalizedStateAsync(): Promise<UnfinalizedState> {
|
|
return testContract.getAggregatedStatsForPreviousEpoch().callAsync();
|
|
}
|
|
|
|
async function finalizePoolsAsync(poolIds: string[]): Promise<LogEntry[]> {
|
|
const logs = [] as LogEntry[];
|
|
for (const poolId of poolIds) {
|
|
const receipt = await testContract.finalizePool(poolId).awaitTransactionSuccessAsync();
|
|
logs.splice(logs.length, 0, ...receipt.logs);
|
|
}
|
|
return logs;
|
|
}
|
|
|
|
async function assertUnfinalizedStateAsync(expected: Partial<UnfinalizedState>): Promise<void> {
|
|
const actual = await getUnfinalizedStateAsync();
|
|
assertEqualNumberFields(actual, expected);
|
|
}
|
|
|
|
function assertEpochEndedEvent(logs: LogEntry[], args: Partial<IStakingEventsEpochEndedEventArgs>): void {
|
|
const events = getEpochEndedEvents(logs);
|
|
expect(events.length).to.eq(1);
|
|
assertEqualNumberFields(events[0], args);
|
|
}
|
|
|
|
function assertEpochFinalizedEvent(logs: LogEntry[], args: Partial<IStakingEventsEpochFinalizedEventArgs>): void {
|
|
const events = getEpochFinalizedEvents(logs);
|
|
expect(events.length).to.eq(1);
|
|
assertEqualNumberFields(events[0], args);
|
|
}
|
|
|
|
function assertEqualNumberFields<T>(actual: T, expected: Partial<T>): void {
|
|
for (const key of Object.keys(actual)) {
|
|
const a = (actual as any)[key] as BigNumber;
|
|
const e = (expected as any)[key] as Numberish;
|
|
if (e !== undefined) {
|
|
expect(a, key).to.bignumber.eq(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function assertFinalizationLogsAndBalancesAsync(
|
|
rewardsAvailable: Numberish,
|
|
poolsToFinalize: ActivePoolOpts[],
|
|
finalizationLogs: LogEntry[],
|
|
): Promise<void> {
|
|
const currentEpoch = await getCurrentEpochAsync();
|
|
// Compute the expected rewards for each pool.
|
|
const poolsWithStake = poolsToFinalize.filter(p => !new BigNumber(p.weightedStake).isZero());
|
|
const poolRewards = await calculatePoolRewardsAsync(rewardsAvailable, poolsWithStake);
|
|
const totalRewards = BigNumber.sum(...poolRewards);
|
|
const rewardsRemaining = new BigNumber(rewardsAvailable).minus(totalRewards);
|
|
const [totalOperatorRewards, totalMembersRewards] = getTotalSplitRewards(poolsToFinalize, poolRewards);
|
|
|
|
// Assert the `RewardsPaid` logs.
|
|
const rewardsPaidEvents = getRewardsPaidEvents(finalizationLogs);
|
|
expect(rewardsPaidEvents.length).to.eq(poolsWithStake.length);
|
|
for (const i of _.times(rewardsPaidEvents.length)) {
|
|
const event = rewardsPaidEvents[i];
|
|
const pool = poolsWithStake[i];
|
|
const reward = poolRewards[i];
|
|
const [operatorReward, membersReward] = splitRewards(pool, reward);
|
|
expect(event.epoch).to.bignumber.eq(currentEpoch);
|
|
assertIntegerRoughlyEquals(event.operatorReward, operatorReward);
|
|
assertIntegerRoughlyEquals(event.membersReward, membersReward);
|
|
}
|
|
|
|
// Assert the `DepositStakingPoolRewards` logs.
|
|
const depositStakingPoolRewardsEvents = getDepositStakingPoolRewardsEvents(finalizationLogs);
|
|
expect(depositStakingPoolRewardsEvents.length).to.eq(poolsWithStake.length);
|
|
for (const i of _.times(depositStakingPoolRewardsEvents.length)) {
|
|
const event = depositStakingPoolRewardsEvents[i];
|
|
const pool = poolsWithStake[i];
|
|
const reward = poolRewards[i];
|
|
expect(event.poolId).to.eq(pool.poolId);
|
|
assertIntegerRoughlyEquals(event.reward, reward);
|
|
assertIntegerRoughlyEquals(event.membersStake, pool.membersStake);
|
|
}
|
|
// Make sure they all sum up to the totals.
|
|
if (depositStakingPoolRewardsEvents.length > 0) {
|
|
const totalDepositRewards = BigNumber.sum(...depositStakingPoolRewardsEvents.map(e => e.reward));
|
|
assertIntegerRoughlyEquals(totalDepositRewards, totalRewards);
|
|
}
|
|
|
|
// Assert the `EpochFinalized` logs.
|
|
const epochFinalizedEvents = getEpochFinalizedEvents(finalizationLogs);
|
|
expect(epochFinalizedEvents.length).to.eq(1);
|
|
expect(epochFinalizedEvents[0].epoch).to.bignumber.eq(currentEpoch.minus(1));
|
|
assertIntegerRoughlyEquals(epochFinalizedEvents[0].rewardsPaid, totalRewards);
|
|
assertIntegerRoughlyEquals(epochFinalizedEvents[0].rewardsRemaining, rewardsRemaining);
|
|
|
|
// Assert the receiver balances.
|
|
await assertReceiverBalancesAsync(totalOperatorRewards, totalMembersRewards);
|
|
}
|
|
|
|
async function assertReceiverBalancesAsync(operatorRewards: Numberish, membersRewards: Numberish): Promise<void> {
|
|
const operatorRewardsBalance = await getBalanceOfAsync(operatorRewardsReceiver);
|
|
assertIntegerRoughlyEquals(operatorRewardsBalance, operatorRewards);
|
|
const membersRewardsBalance = await getBalanceOfAsync(membersRewardsReceiver);
|
|
assertIntegerRoughlyEquals(membersRewardsBalance, membersRewards);
|
|
}
|
|
|
|
async function calculatePoolRewardsAsync(
|
|
rewardsAvailable: Numberish,
|
|
poolsToFinalize: ActivePoolOpts[],
|
|
): Promise<BigNumber[]> {
|
|
const totalFees = BigNumber.sum(...poolsToFinalize.map(p => p.feesCollected));
|
|
const totalStake = BigNumber.sum(...poolsToFinalize.map(p => p.weightedStake));
|
|
const poolRewards = _.times(poolsToFinalize.length, () => constants.ZERO_AMOUNT);
|
|
for (const i of _.times(poolsToFinalize.length)) {
|
|
const pool = poolsToFinalize[i];
|
|
const feesCollected = new BigNumber(pool.feesCollected);
|
|
if (feesCollected.isZero()) {
|
|
continue;
|
|
}
|
|
poolRewards[i] = await testContract
|
|
.cobbDouglas(
|
|
new BigNumber(rewardsAvailable),
|
|
new BigNumber(feesCollected),
|
|
new BigNumber(totalFees),
|
|
new BigNumber(pool.weightedStake),
|
|
new BigNumber(totalStake),
|
|
)
|
|
.callAsync();
|
|
}
|
|
return poolRewards;
|
|
}
|
|
|
|
function splitRewards(pool: ActivePoolOpts, totalReward: Numberish): [BigNumber, BigNumber] {
|
|
if (new BigNumber(pool.membersStake).isZero()) {
|
|
return [new BigNumber(totalReward), ZERO_AMOUNT];
|
|
}
|
|
const operatorShare = new BigNumber(totalReward).times(pool.operatorShare).integerValue(BigNumber.ROUND_UP);
|
|
const membersShare = new BigNumber(totalReward).minus(operatorShare);
|
|
return [operatorShare, membersShare];
|
|
}
|
|
|
|
// Calculates the split rewards for every pool and returns the operator
|
|
// and member sums.
|
|
function getTotalSplitRewards(pools: ActivePoolOpts[], rewards: Numberish[]): [BigNumber, BigNumber] {
|
|
const _rewards = _.times(pools.length).map(i => splitRewards(pools[i], rewards[i]));
|
|
const totalOperatorRewards = BigNumber.sum(..._rewards.map(([o]) => o));
|
|
const totalMembersRewards = BigNumber.sum(..._rewards.map(([, m]) => m));
|
|
return [totalOperatorRewards, totalMembersRewards];
|
|
}
|
|
|
|
function getEpochEndedEvents(logs: LogEntry[]): IStakingEventsEpochEndedEventArgs[] {
|
|
return filterLogsToArguments<IStakingEventsEpochEndedEventArgs>(logs, IStakingEventsEvents.EpochEnded);
|
|
}
|
|
|
|
function getEpochFinalizedEvents(logs: LogEntry[]): IStakingEventsEpochFinalizedEventArgs[] {
|
|
return filterLogsToArguments<IStakingEventsEpochFinalizedEventArgs>(logs, IStakingEventsEvents.EpochFinalized);
|
|
}
|
|
|
|
function getDepositStakingPoolRewardsEvents(logs: LogEntry[]): DepositStakingPoolRewardsEventArgs[] {
|
|
return filterLogsToArguments<DepositStakingPoolRewardsEventArgs>(
|
|
logs,
|
|
TestFinalizerEvents.DepositStakingPoolRewards,
|
|
);
|
|
}
|
|
|
|
function getRewardsPaidEvents(logs: LogEntry[]): IStakingEventsRewardsPaidEventArgs[] {
|
|
return filterLogsToArguments<IStakingEventsRewardsPaidEventArgs>(logs, IStakingEventsEvents.RewardsPaid);
|
|
}
|
|
|
|
async function getCurrentEpochAsync(): Promise<BigNumber> {
|
|
return testContract.currentEpoch().callAsync();
|
|
}
|
|
|
|
async function getBalanceOfAsync(whom: string): Promise<BigNumber> {
|
|
return env.web3Wrapper.getBalanceInWeiAsync(whom);
|
|
}
|
|
|
|
describe('endEpoch()', () => {
|
|
it('advances the epoch', async () => {
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const currentEpoch = await testContract.currentEpoch().callAsync();
|
|
expect(currentEpoch).to.bignumber.eq(stakingConstants.INITIAL_EPOCH.plus(1));
|
|
});
|
|
|
|
it('emits an `EpochEnded` event', async () => {
|
|
const receipt = await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
assertEpochEndedEvent(receipt.logs, {
|
|
epoch: stakingConstants.INITIAL_EPOCH,
|
|
numActivePools: ZERO_AMOUNT,
|
|
rewardsAvailable: INITIAL_BALANCE,
|
|
totalFeesCollected: ZERO_AMOUNT,
|
|
totalWeightedStake: ZERO_AMOUNT,
|
|
});
|
|
});
|
|
|
|
it('immediately finalizes if there are no pools to finalize', async () => {
|
|
const receipt = await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
assertEpochFinalizedEvent(receipt.logs, {
|
|
epoch: stakingConstants.INITIAL_EPOCH,
|
|
rewardsPaid: ZERO_AMOUNT,
|
|
rewardsRemaining: INITIAL_BALANCE,
|
|
});
|
|
});
|
|
|
|
it('does not immediately finalize if there is a pool to finalize', async () => {
|
|
await addActivePoolAsync();
|
|
const receipt = await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const events = filterLogsToArguments<IStakingEventsEpochFinalizedEventArgs>(
|
|
receipt.logs,
|
|
IStakingEventsEvents.EpochFinalized,
|
|
);
|
|
expect(events).to.deep.eq([]);
|
|
});
|
|
|
|
it('prepares unfinalized state', async () => {
|
|
// Add a pool so there is state to clear.
|
|
const pool = await addActivePoolAsync();
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
return assertUnfinalizedStateAsync({
|
|
numPoolsToFinalize: 1,
|
|
rewardsAvailable: INITIAL_BALANCE,
|
|
totalFeesCollected: pool.feesCollected,
|
|
totalWeightedStake: pool.weightedStake,
|
|
});
|
|
});
|
|
|
|
it("correctly stores the epoch's aggregated stats after ending the epoch", async () => {
|
|
const pool = await addActivePoolAsync();
|
|
const epoch = await testContract.currentEpoch().callAsync();
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const aggregatedStats = await testContract.aggregatedStatsByEpoch(epoch).callAsync();
|
|
expect(aggregatedStats).to.be.deep.equal([
|
|
INITIAL_BALANCE,
|
|
new BigNumber(1), // pools to finalize
|
|
pool.feesCollected,
|
|
pool.weightedStake,
|
|
new BigNumber(0), // rewards finalized
|
|
]);
|
|
});
|
|
|
|
it('reverts if the prior epoch is unfinalized', async () => {
|
|
await addActivePoolAsync();
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const tx = testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const expectedError = new StakingRevertErrors.PreviousEpochNotFinalizedError(
|
|
stakingConstants.INITIAL_EPOCH,
|
|
1,
|
|
);
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
});
|
|
|
|
describe('_finalizePool()', () => {
|
|
it('does nothing if there were no pools to finalize', async () => {
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const poolId = hexRandom();
|
|
const logs = await finalizePoolsAsync([poolId]);
|
|
expect(logs).to.deep.eq([]);
|
|
});
|
|
|
|
it('can finalize a pool', async () => {
|
|
const pool = await addActivePoolAsync();
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const logs = await finalizePoolsAsync([pool.poolId]);
|
|
return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, [pool], logs);
|
|
});
|
|
|
|
it('can finalize multiple pools over multiple transactions', async () => {
|
|
const pools = await Promise.all(_.times(2, async () => addActivePoolAsync()));
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const logs = await finalizePoolsAsync(pools.map(p => p.poolId));
|
|
return assertFinalizationLogsAndBalancesAsync(INITIAL_BALANCE, pools, logs);
|
|
});
|
|
|
|
it('ignores a finalized pool', async () => {
|
|
const pools = await Promise.all(_.times(3, async () => addActivePoolAsync()));
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const [finalizedPool] = _.sampleSize(pools, 1);
|
|
await finalizePoolsAsync([finalizedPool.poolId]);
|
|
const logs = await finalizePoolsAsync([finalizedPool.poolId]);
|
|
const rewardsPaidEvents = getRewardsPaidEvents(logs);
|
|
expect(rewardsPaidEvents).to.deep.eq([]);
|
|
});
|
|
|
|
it('resets pool state after finalizing it', async () => {
|
|
const pools = await Promise.all(_.times(3, async () => addActivePoolAsync()));
|
|
const pool = _.sample(pools) as ActivePoolOpts;
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
await finalizePoolsAsync([pool.poolId]);
|
|
const poolState = await testContract
|
|
.getPoolStatsFromEpoch(stakingConstants.INITIAL_EPOCH, pool.poolId)
|
|
.callAsync();
|
|
expect(poolState.feesCollected).to.bignumber.eq(0);
|
|
expect(poolState.weightedStake).to.bignumber.eq(0);
|
|
expect(poolState.membersStake).to.bignumber.eq(0);
|
|
});
|
|
|
|
it('`rewardsPaid` <= `rewardsAvailable` <= contract balance at the end of the epoch', async () => {
|
|
const pools = await Promise.all(_.times(3, async () => addActivePoolAsync()));
|
|
const receipt = await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0];
|
|
expect(rewardsAvailable).to.bignumber.lte(INITIAL_BALANCE);
|
|
const logs = await finalizePoolsAsync(pools.map(r => r.poolId));
|
|
const { rewardsPaid } = getEpochFinalizedEvents(logs)[0];
|
|
expect(rewardsPaid).to.bignumber.lte(rewardsAvailable);
|
|
});
|
|
|
|
it('`rewardsPaid` <= `rewardsAvailable` with two equal pools', async () => {
|
|
const pool1 = await addActivePoolAsync();
|
|
const pool2 = await addActivePoolAsync(_.omit(pool1, 'poolId'));
|
|
const receipt = await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0];
|
|
const logs = await finalizePoolsAsync([pool1, pool2].map(r => r.poolId));
|
|
const { rewardsPaid } = getEpochFinalizedEvents(logs)[0];
|
|
expect(rewardsPaid).to.bignumber.lte(rewardsAvailable);
|
|
});
|
|
|
|
blockchainTests.optional('`rewardsPaid` fuzzing', async () => {
|
|
const numTests = 32;
|
|
for (const i of _.times(numTests)) {
|
|
const numPools = _.random(1, 32);
|
|
it(`${i + 1}/${numTests} \`rewardsPaid\` <= \`rewardsAvailable\` (${numPools} pools)`, async () => {
|
|
const pools = await Promise.all(_.times(numPools, async () => addActivePoolAsync()));
|
|
const receipt = await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const { rewardsAvailable } = getEpochEndedEvents(receipt.logs)[0];
|
|
const logs = await finalizePoolsAsync(pools.map(r => r.poolId));
|
|
const { rewardsPaid } = getEpochFinalizedEvents(logs)[0];
|
|
expect(rewardsPaid).to.bignumber.lte(rewardsAvailable);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('lifecycle', () => {
|
|
it('can advance the epoch after the prior epoch is finalized', async () => {
|
|
const pool = await addActivePoolAsync();
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
await finalizePoolsAsync([pool.poolId]);
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
return expect(getCurrentEpochAsync()).to.become(stakingConstants.INITIAL_EPOCH.plus(2));
|
|
});
|
|
|
|
it('does not reward a pool that only earned rewards 2 epochs ago', async () => {
|
|
const pool1 = await addActivePoolAsync();
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
await finalizePoolsAsync([pool1.poolId]);
|
|
await addActivePoolAsync();
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
expect(getCurrentEpochAsync()).to.become(stakingConstants.INITIAL_EPOCH.plus(2));
|
|
const logs = await finalizePoolsAsync([pool1.poolId]);
|
|
const rewardsPaidEvents = getRewardsPaidEvents(logs);
|
|
expect(rewardsPaidEvents).to.deep.eq([]);
|
|
});
|
|
|
|
it('does not reward a pool that only earned rewards 3 epochs ago', async () => {
|
|
const pool1 = await addActivePoolAsync();
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
await finalizePoolsAsync([pool1.poolId]);
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
await addActivePoolAsync();
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
expect(getCurrentEpochAsync()).to.become(stakingConstants.INITIAL_EPOCH.plus(3));
|
|
const logs = await finalizePoolsAsync([pool1.poolId]);
|
|
const rewardsPaidEvents = getRewardsPaidEvents(logs);
|
|
expect(rewardsPaidEvents).to.deep.eq([]);
|
|
});
|
|
|
|
it('rolls over leftover rewards into the next epoch', async () => {
|
|
const poolIds = _.times(3, () => hexRandom());
|
|
await Promise.all(poolIds.map(async id => addActivePoolAsync({ poolId: id })));
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const finalizeLogs = await finalizePoolsAsync(poolIds);
|
|
const { rewardsRemaining: rolledOverRewards } = getEpochFinalizedEvents(finalizeLogs)[0];
|
|
await Promise.all(poolIds.map(async id => addActivePoolAsync({ poolId: id })));
|
|
const { logs: endEpochLogs } = await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const { rewardsAvailable } = getEpochEndedEvents(endEpochLogs)[0];
|
|
expect(rewardsAvailable).to.bignumber.eq(rolledOverRewards);
|
|
});
|
|
});
|
|
|
|
interface FinalizedPoolRewards {
|
|
totalReward: Numberish;
|
|
membersStake: Numberish;
|
|
}
|
|
|
|
async function assertUnfinalizedPoolRewardsAsync(
|
|
poolId: string,
|
|
expected: Partial<FinalizedPoolRewards>,
|
|
): Promise<void> {
|
|
const actual = await testContract.getUnfinalizedPoolRewards(poolId).callAsync();
|
|
if (expected.totalReward !== undefined) {
|
|
expect(actual.totalReward).to.bignumber.eq(expected.totalReward);
|
|
}
|
|
if (expected.membersStake !== undefined) {
|
|
expect(actual.membersStake).to.bignumber.eq(expected.membersStake);
|
|
}
|
|
}
|
|
|
|
describe('_getUnfinalizedPoolReward()', () => {
|
|
const ZERO_REWARDS = {
|
|
totalReward: 0,
|
|
membersStake: 0,
|
|
};
|
|
|
|
it('returns empty if epoch is 1', async () => {
|
|
const poolId = hexRandom();
|
|
return assertUnfinalizedPoolRewardsAsync(poolId, ZERO_REWARDS);
|
|
});
|
|
|
|
it('returns empty if pool did not earn rewards', async () => {
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const poolId = hexRandom();
|
|
return assertUnfinalizedPoolRewardsAsync(poolId, ZERO_REWARDS);
|
|
});
|
|
|
|
it('returns empty if pool is earned rewards only in the current epoch', async () => {
|
|
const pool = await addActivePoolAsync();
|
|
return assertUnfinalizedPoolRewardsAsync(pool.poolId, ZERO_REWARDS);
|
|
});
|
|
|
|
it('returns empty if pool only earned rewards in the 2 epochs ago', async () => {
|
|
const pool = await addActivePoolAsync();
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
await finalizePoolsAsync([pool.poolId]);
|
|
return assertUnfinalizedPoolRewardsAsync(pool.poolId, ZERO_REWARDS);
|
|
});
|
|
|
|
it('returns empty if pool was already finalized', async () => {
|
|
const pools = await Promise.all(_.times(3, async () => addActivePoolAsync()));
|
|
const [pool] = _.sampleSize(pools, 1);
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
await finalizePoolsAsync([pool.poolId]);
|
|
return assertUnfinalizedPoolRewardsAsync(pool.poolId, ZERO_REWARDS);
|
|
});
|
|
|
|
it('computes one reward among one pool', async () => {
|
|
const pool = await addActivePoolAsync();
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const expectedTotalRewards = INITIAL_BALANCE;
|
|
return assertUnfinalizedPoolRewardsAsync(pool.poolId, {
|
|
totalReward: expectedTotalRewards,
|
|
membersStake: pool.membersStake,
|
|
});
|
|
});
|
|
|
|
it('computes one reward among multiple pools', async () => {
|
|
const pools = await Promise.all(_.times(3, async () => addActivePoolAsync()));
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
const expectedPoolRewards = await calculatePoolRewardsAsync(INITIAL_BALANCE, pools);
|
|
const [pool, reward] = _.sampleSize(shortZip(pools, expectedPoolRewards), 1)[0];
|
|
return assertUnfinalizedPoolRewardsAsync(pool.poolId, {
|
|
totalReward: (reward as any) as BigNumber,
|
|
membersStake: pool.membersStake,
|
|
});
|
|
});
|
|
|
|
it('computes a reward with 0% operatorShare', async () => {
|
|
const pool = await addActivePoolAsync({ operatorShare: 0 });
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
return assertUnfinalizedPoolRewardsAsync(pool.poolId, {
|
|
totalReward: INITIAL_BALANCE,
|
|
membersStake: pool.membersStake,
|
|
});
|
|
});
|
|
|
|
it('computes a reward with 0% < operatorShare < 100%', async () => {
|
|
const pool = await addActivePoolAsync({ operatorShare: Math.random() });
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
return assertUnfinalizedPoolRewardsAsync(pool.poolId, {
|
|
totalReward: INITIAL_BALANCE,
|
|
membersStake: pool.membersStake,
|
|
});
|
|
});
|
|
|
|
it('computes a reward with 100% operatorShare', async () => {
|
|
const pool = await addActivePoolAsync({ operatorShare: 1 });
|
|
await testContract.endEpoch().awaitTransactionSuccessAsync();
|
|
return assertUnfinalizedPoolRewardsAsync(pool.poolId, {
|
|
totalReward: INITIAL_BALANCE,
|
|
membersStake: pool.membersStake,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
// tslint:disable: max-file-line-count
|