Refactored finalization state.

1. Removes state variables:
   - totalWeightedStakeThisEpoch
   - totalFeesCollectedThisEpoch
   - numActivePoolsThisEpoch

2. No longer indexes by epoch % 2

3. Renamed event StakingPoolActivated → StakingPoolEarnedRewards.

4. Renamed structs:
   - ActivePool → PoolStats. This holds stats for a pool that earned rewards.
   - UnfinalizedState → AggregatedStats. This aggregates stats from the former struct.
This commit is contained in:
Greg Hysen
2019-10-21 10:07:06 -07:00
parent e7dc7167d0
commit 6617ad9531
13 changed files with 222 additions and 274 deletions

View File

@@ -238,7 +238,7 @@ export class FinalizerActor extends BaseActor {
private async _getRewardByPoolIdAsync(poolIds: string[]): Promise<RewardByPoolId> {
const activePools = await Promise.all(
poolIds.map(async poolId =>
this._stakingApiWrapper.stakingContract.getActiveStakingPoolThisEpoch.callAsync(poolId),
this._stakingApiWrapper.stakingContract.getStakingPoolStatsThisEpoch.callAsync(poolId),
),
);
const totalRewards = await this._stakingApiWrapper.utils.getAvailableRewardsBalanceAsync();

View File

@@ -87,21 +87,14 @@ blockchainTests.resets('Finalizer unit tests', env => {
interface UnfinalizedState {
rewardsAvailable: Numberish;
poolsRemaining: number;
poolsToFinalize: Numberish;
totalFeesCollected: Numberish;
totalWeightedStake: Numberish;
totalRewardsFinalized: Numberish;
}
async function getUnfinalizedStateAsync(): Promise<UnfinalizedState> {
const r = await testContract.unfinalizedState.callAsync();
return {
rewardsAvailable: r[0],
poolsRemaining: r[1].toNumber(),
totalFeesCollected: r[2],
totalWeightedStake: r[3],
totalRewardsFinalized: r[4],
};
return testContract.getAggregatedStatsForPreviousEpoch.callAsync();
}
async function finalizePoolsAsync(poolIds: string[]): Promise<LogEntry[]> {
@@ -142,16 +135,16 @@ blockchainTests.resets('Finalizer unit tests', env => {
async function assertFinalizationLogsAndBalancesAsync(
rewardsAvailable: Numberish,
activePools: ActivePoolOpts[],
poolsToFinalize: ActivePoolOpts[],
finalizationLogs: LogEntry[],
): Promise<void> {
const currentEpoch = await getCurrentEpochAsync();
// Compute the expected rewards for each pool.
const poolsWithStake = activePools.filter(p => !new BigNumber(p.weightedStake).isZero());
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(activePools, poolRewards);
const [totalOperatorRewards, totalMembersRewards] = getTotalSplitRewards(poolsToFinalize, poolRewards);
// Assert the `RewardsPaid` logs.
const rewardsPaidEvents = getRewardsPaidEvents(finalizationLogs);
@@ -203,13 +196,13 @@ blockchainTests.resets('Finalizer unit tests', env => {
async function calculatePoolRewardsAsync(
rewardsAvailable: Numberish,
activePools: ActivePoolOpts[],
poolsToFinalize: ActivePoolOpts[],
): Promise<BigNumber[]> {
const totalFees = BigNumber.sum(...activePools.map(p => p.feesCollected));
const totalStake = BigNumber.sum(...activePools.map(p => p.weightedStake));
const poolRewards = _.times(activePools.length, () => constants.ZERO_AMOUNT);
for (const i of _.times(activePools.length)) {
const pool = activePools[i];
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;
@@ -307,32 +300,48 @@ blockchainTests.resets('Finalizer unit tests', env => {
expect(events).to.deep.eq([]);
});
it("clears the next epoch's finalization state", async () => {
// Add a pool so there is state to clear.
await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync();
const epoch = await testContract.currentEpoch.callAsync();
expect(epoch).to.bignumber.eq(stakingConstants.INITIAL_EPOCH.plus(1));
const numActivePools = await testContract.numActivePoolsThisEpoch.callAsync();
const totalFees = await testContract.totalFeesCollectedThisEpoch.callAsync();
const totalStake = await testContract.totalWeightedStakeThisEpoch.callAsync();
expect(numActivePools).to.bignumber.eq(0);
expect(totalFees).to.bignumber.eq(0);
expect(totalStake).to.bignumber.eq(0);
});
it('prepares unfinalized state', async () => {
// Add a pool so there is state to clear.
const pool = await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync();
return assertUnfinalizedStateAsync({
poolsRemaining: 1,
poolsToFinalize: 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.callAsync(epoch);
expect(aggregatedStats).to.be.deep.equal([
INITIAL_BALANCE,
new BigNumber(1), // pools to finalize
pool.feesCollected,
pool.weightedStake,
new BigNumber(0), // rewards finalized
]);
});
it("correctly clear an epoch's aggregated stats after it is finalized", async () => {
const pool = await addActivePoolAsync();
const epoch = await testContract.currentEpoch.callAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync();
await testContract.finalizePool.awaitTransactionSuccessAsync(pool.poolId);
await testContract.endEpoch.awaitTransactionSuccessAsync();
const aggregatedStats = await testContract.aggregatedStatsByEpoch.callAsync(epoch);
expect(aggregatedStats).to.be.deep.equal([
new BigNumber(0),
new BigNumber(0),
new BigNumber(0),
new BigNumber(0),
new BigNumber(0),
]);
});
it('reverts if the prior epoch is unfinalized', async () => {
await addActivePoolAsync();
await testContract.endEpoch.awaitTransactionSuccessAsync();
@@ -382,7 +391,7 @@ blockchainTests.resets('Finalizer unit tests', env => {
const pool = _.sample(pools) as ActivePoolOpts;
await testContract.endEpoch.awaitTransactionSuccessAsync();
await finalizePoolsAsync([pool.poolId]);
const poolState = await testContract.getActivePoolFromEpoch.callAsync(
const poolState = await testContract.getPoolStatsFromEpoch.callAsync(
stakingConstants.INITIAL_EPOCH,
pool.poolId,
);

View File

@@ -15,7 +15,7 @@ import * as _ from 'lodash';
import {
artifacts,
IStakingEventsEvents,
IStakingEventsStakingPoolActivatedEventArgs,
IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs,
TestProtocolFeesContract,
TestProtocolFeesERC20ProxyTransferFromEventArgs,
TestProtocolFeesEvents,
@@ -152,7 +152,7 @@ blockchainTests('Protocol Fees unit tests', env => {
});
async function getProtocolFeesAsync(poolId: string): Promise<BigNumber> {
return (await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId)).feesCollected;
return (await testContract.getStakingPoolStatsThisEpoch.callAsync(poolId)).feesCollected;
}
describe('ETH fees', () => {
@@ -369,21 +369,22 @@ blockchainTests('Protocol Fees unit tests', env => {
});
interface FinalizationState {
numActivePools: BigNumber;
poolsToFinalize: BigNumber;
totalFeesCollected: BigNumber;
totalWeightedStake: BigNumber;
}
async function getFinalizationStateAsync(): Promise<FinalizationState> {
const aggregatedStats = await testContract.getAggregatedStatsForCurrentEpoch.callAsync();
return {
numActivePools: await testContract.numActivePoolsThisEpoch.callAsync(),
totalFeesCollected: await testContract.totalFeesCollectedThisEpoch.callAsync(),
totalWeightedStake: await testContract.totalWeightedStakeThisEpoch.callAsync(),
poolsToFinalize: aggregatedStats.poolsToFinalize,
totalFeesCollected: aggregatedStats.totalFeesCollected,
totalWeightedStake: aggregatedStats.totalWeightedStake,
};
}
interface PayToMakerResult {
poolActivatedEvents: IStakingEventsStakingPoolActivatedEventArgs[];
poolEarnedRewardsEvents: IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs[];
fee: BigNumber;
}
@@ -395,13 +396,13 @@ blockchainTests('Protocol Fees unit tests', env => {
new BigNumber(_fee),
{ from: exchangeAddress, value: _fee },
);
const events = filterLogsToArguments<IStakingEventsStakingPoolActivatedEventArgs>(
const events = filterLogsToArguments<IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs>(
receipt.logs,
IStakingEventsEvents.StakingPoolActivated,
IStakingEventsEvents.StakingPoolEarnedRewardsInEpoch,
);
return {
fee: new BigNumber(_fee),
poolActivatedEvents: events,
poolEarnedRewardsEvents: events,
};
}
@@ -414,14 +415,14 @@ blockchainTests('Protocol Fees unit tests', env => {
it('no active pools to start', async () => {
const state = await getFinalizationStateAsync();
expect(state.numActivePools).to.bignumber.eq(0);
expect(state.poolsToFinalize).to.bignumber.eq(0);
expect(state.totalFeesCollected).to.bignumber.eq(0);
expect(state.totalWeightedStake).to.bignumber.eq(0);
});
it('pool is not registered to start', async () => {
const { poolId } = await createTestPoolAsync();
const pool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId);
const pool = await testContract.getStakingPoolStatsThisEpoch.callAsync(poolId);
expect(pool.feesCollected).to.bignumber.eq(0);
expect(pool.membersStake).to.bignumber.eq(0);
expect(pool.weightedStake).to.bignumber.eq(0);
@@ -433,16 +434,16 @@ blockchainTests('Protocol Fees unit tests', env => {
poolId,
makers: [poolMaker],
} = pool;
const { fee, poolActivatedEvents } = await payToMakerAsync(poolMaker);
expect(poolActivatedEvents.length).to.eq(1);
expect(poolActivatedEvents[0].poolId).to.eq(poolId);
const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId);
const { fee, poolEarnedRewardsEvents } = await payToMakerAsync(poolMaker);
expect(poolEarnedRewardsEvents.length).to.eq(1);
expect(poolEarnedRewardsEvents[0].poolId).to.eq(poolId);
const actualPoolStats = await testContract.getStakingPoolStatsThisEpoch.callAsync(poolId);
const expectedWeightedStake = toWeightedStake(pool.operatorStake, pool.membersStake);
expect(actualPool.feesCollected).to.bignumber.eq(fee);
expect(actualPool.membersStake).to.bignumber.eq(pool.membersStake);
expect(actualPool.weightedStake).to.bignumber.eq(expectedWeightedStake);
expect(actualPoolStats.feesCollected).to.bignumber.eq(fee);
expect(actualPoolStats.membersStake).to.bignumber.eq(pool.membersStake);
expect(actualPoolStats.weightedStake).to.bignumber.eq(expectedWeightedStake);
const state = await getFinalizationStateAsync();
expect(state.numActivePools).to.bignumber.eq(1);
expect(state.poolsToFinalize).to.bignumber.eq(1);
expect(state.totalFeesCollected).to.bignumber.eq(fee);
expect(state.totalWeightedStake).to.bignumber.eq(expectedWeightedStake);
});
@@ -454,16 +455,16 @@ blockchainTests('Protocol Fees unit tests', env => {
makers: [poolMaker],
} = pool;
const { fee: fee1 } = await payToMakerAsync(poolMaker);
const { fee: fee2, poolActivatedEvents } = await payToMakerAsync(poolMaker);
expect(poolActivatedEvents).to.deep.eq([]);
const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId);
const { fee: fee2, poolEarnedRewardsEvents } = await payToMakerAsync(poolMaker);
expect(poolEarnedRewardsEvents).to.deep.eq([]);
const actualPoolStats = await testContract.getStakingPoolStatsThisEpoch.callAsync(poolId);
const expectedWeightedStake = toWeightedStake(pool.operatorStake, pool.membersStake);
const fees = BigNumber.sum(fee1, fee2);
expect(actualPool.feesCollected).to.bignumber.eq(fees);
expect(actualPool.membersStake).to.bignumber.eq(pool.membersStake);
expect(actualPool.weightedStake).to.bignumber.eq(expectedWeightedStake);
expect(actualPoolStats.feesCollected).to.bignumber.eq(fees);
expect(actualPoolStats.membersStake).to.bignumber.eq(pool.membersStake);
expect(actualPoolStats.weightedStake).to.bignumber.eq(expectedWeightedStake);
const state = await getFinalizationStateAsync();
expect(state.numActivePools).to.bignumber.eq(1);
expect(state.poolsToFinalize).to.bignumber.eq(1);
expect(state.totalFeesCollected).to.bignumber.eq(fees);
expect(state.totalWeightedStake).to.bignumber.eq(expectedWeightedStake);
});
@@ -477,19 +478,19 @@ blockchainTests('Protocol Fees unit tests', env => {
poolId,
makers: [poolMaker],
} = pool;
const { fee, poolActivatedEvents } = await payToMakerAsync(poolMaker);
expect(poolActivatedEvents.length).to.eq(1);
expect(poolActivatedEvents[0].poolId).to.eq(poolId);
const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId);
const { fee, poolEarnedRewardsEvents } = await payToMakerAsync(poolMaker);
expect(poolEarnedRewardsEvents.length).to.eq(1);
expect(poolEarnedRewardsEvents[0].poolId).to.eq(poolId);
const actualPoolStats = await testContract.getStakingPoolStatsThisEpoch.callAsync(poolId);
const expectedWeightedStake = toWeightedStake(pool.operatorStake, pool.membersStake);
expect(actualPool.feesCollected).to.bignumber.eq(fee);
expect(actualPool.membersStake).to.bignumber.eq(pool.membersStake);
expect(actualPool.weightedStake).to.bignumber.eq(expectedWeightedStake);
expect(actualPoolStats.feesCollected).to.bignumber.eq(fee);
expect(actualPoolStats.membersStake).to.bignumber.eq(pool.membersStake);
expect(actualPoolStats.weightedStake).to.bignumber.eq(expectedWeightedStake);
totalFees = totalFees.plus(fee);
totalWeightedStake = totalWeightedStake.plus(expectedWeightedStake);
}
const state = await getFinalizationStateAsync();
expect(state.numActivePools).to.bignumber.eq(pools.length);
expect(state.poolsToFinalize).to.bignumber.eq(pools.length);
expect(state.totalFeesCollected).to.bignumber.eq(totalFees);
expect(state.totalWeightedStake).to.bignumber.eq(totalWeightedStake);
});
@@ -502,10 +503,10 @@ blockchainTests('Protocol Fees unit tests', env => {
} = pool;
await payToMakerAsync(poolMaker);
await testContract.advanceEpoch.awaitTransactionSuccessAsync();
const actualPool = await testContract.getActiveStakingPoolThisEpoch.callAsync(poolId);
expect(actualPool.feesCollected).to.bignumber.eq(0);
expect(actualPool.membersStake).to.bignumber.eq(0);
expect(actualPool.weightedStake).to.bignumber.eq(0);
const actualPoolStats = await testContract.getStakingPoolStatsThisEpoch.callAsync(poolId);
expect(actualPoolStats.feesCollected).to.bignumber.eq(0);
expect(actualPoolStats.membersStake).to.bignumber.eq(0);
expect(actualPoolStats.weightedStake).to.bignumber.eq(0);
});
describe('Multiple makers', () => {

View File

@@ -9,7 +9,7 @@ import * as _ from 'lodash';
import {
artifacts,
IStakingEventsEpochEndedEventArgs,
IStakingEventsStakingPoolActivatedEventArgs,
IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs,
ReadOnlyProxyContract,
StakingProxyContract,
TestCobbDouglasContract,
@@ -76,13 +76,13 @@ export class StakingApiWrapper {
findActivePoolIdsAsync: async (epoch?: number): Promise<string[]> => {
const _epoch = epoch !== undefined ? epoch : await this.stakingContract.currentEpoch.callAsync();
const events = filterLogsToArguments<IStakingEventsStakingPoolActivatedEventArgs>(
const events = filterLogsToArguments<IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs>(
await this.stakingContract.getLogsAsync(
TestStakingEvents.StakingPoolActivated,
TestStakingEvents.StakingPoolEarnedRewardsInEpoch,
{ fromBlock: BlockParamLiteral.Earliest, toBlock: BlockParamLiteral.Latest },
{ epoch: new BigNumber(_epoch) },
),
TestStakingEvents.StakingPoolActivated,
TestStakingEvents.StakingPoolEarnedRewardsInEpoch,
);
return events.map(e => e.poolId);
},