import { AggregatedStats, IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs, TestStakingEvents, } from '@0x/contracts-staking'; import { filterLogsToArguments, web3Wrapper } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; import { BlockParamLiteral, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import { endEpochTooEarlyAssertion, endEpochUnfinalizedPoolsAssertion, validEndEpochAssertion, } from '../assertions/endEpoch'; import { validFinalizePoolAssertion } from '../assertions/finalizePool'; import { AssertionResult } from '../assertions/function_assertion'; import { Pseudorandom } from '../utils/pseudorandom'; import { Actor, Constructor } from './base'; export interface KeeperInterface { endEpochAsync: (shouldFastForward?: boolean) => Promise; finalizePoolsAsync: (poolIds?: string[]) => Promise; } /** * This mixin encapsulates functionality associated with keepers within the 0x ecosystem. * This includes ending epochs sand finalizing pools in the staking system. */ export function KeeperMixin(Base: TBase): TBase & Constructor { return class extends Base { public readonly actor: Actor; /** * The mixin pattern requires that this constructor uses `...args: any[]`, but this class * really expects a single `Actor` parameter (assuming `Actor` is used as the base * class). */ constructor(...args: any[]) { // tslint:disable-next-line:no-inferred-empty-object-type super(...args); this.actor = (this as any) as Actor; this.actor.mixins.push('Keeper'); // Register this mixin's assertion generators this.actor.simulationActions = { ...this.actor.simulationActions, validFinalizePool: this._validFinalizePool(), validEndEpoch: this._validEndEpoch(), invalidEndEpoch: this._invalidEndEpoch(), }; } /** * Ends the current epoch, fast-forwarding to the end of the epoch by default. */ public async endEpochAsync(shouldFastForward: boolean = true): Promise { const { stakingWrapper } = this.actor.deployment.staking; if (shouldFastForward) { await this._fastForwardToNextEpochAsync(); } return stakingWrapper.endEpoch().awaitTransactionSuccessAsync({ from: this.actor.address }); } /** * Finalizes staking pools corresponding to the given `poolIds`. If none are provided, * finalizes all pools that earned rewards in the previous epoch. */ public async finalizePoolsAsync(poolIds: string[] = []): Promise { const { stakingWrapper } = this.actor.deployment.staking; // If no poolIds provided, finalize all active pools from the previous epoch if (poolIds.length === 0) { const previousEpoch = (await stakingWrapper.currentEpoch().callAsync()).minus(1); const events = filterLogsToArguments( await stakingWrapper.getLogsAsync( TestStakingEvents.StakingPoolEarnedRewardsInEpoch, { fromBlock: BlockParamLiteral.Earliest, toBlock: BlockParamLiteral.Latest }, { epoch: new BigNumber(previousEpoch) }, ), TestStakingEvents.StakingPoolEarnedRewardsInEpoch, ); poolIds.concat(events.map(event => event.poolId)); } return Promise.all( poolIds.map(async poolId => stakingWrapper.finalizePool(poolId).awaitTransactionSuccessAsync({ from: this.actor.address, }), ), ); } private async *_validFinalizePool(): AsyncIterableIterator { const { stakingPools } = this.actor.simulationEnvironment!; const assertion = validFinalizePoolAssertion(this.actor.deployment, this.actor.simulationEnvironment!); while (true) { // Finalize a random pool, or do nothing if there are no pools in the simulation yet. const poolId = Pseudorandom.sample(Object.keys(stakingPools)); if (poolId === undefined) { yield; } else { yield assertion.executeAsync([poolId], { from: this.actor.address }); } } } private async *_validEndEpoch(): AsyncIterableIterator { const assertion = validEndEpochAssertion(this.actor.deployment, this.actor.simulationEnvironment!); const { stakingWrapper } = this.actor.deployment.staking; while (true) { const { currentEpoch } = this.actor.simulationEnvironment!; const aggregatedStats = AggregatedStats.fromArray( await stakingWrapper.aggregatedStatsByEpoch(currentEpoch.minus(1)).callAsync(), ); if (aggregatedStats.numPoolsToFinalize.isGreaterThan(0)) { // Can't end the epoch if the previous epoch is not fully finalized. yield; } else { await this._fastForwardToNextEpochAsync(); yield assertion.executeAsync([], { from: this.actor.address }); } } } private async *_invalidEndEpoch(): AsyncIterableIterator { const { stakingWrapper } = this.actor.deployment.staking; while (true) { const { simulationEnvironment } = this.actor; const aggregatedStats = AggregatedStats.fromArray( await stakingWrapper .aggregatedStatsByEpoch(simulationEnvironment!.currentEpoch.minus(1)) .callAsync(), ); const assertion = aggregatedStats.numPoolsToFinalize.isGreaterThan(0) ? endEpochUnfinalizedPoolsAssertion( this.actor.deployment, simulationEnvironment!, aggregatedStats.numPoolsToFinalize, ) : endEpochTooEarlyAssertion(this.actor.deployment); yield assertion.executeAsync([], { from: this.actor.address }); } } private async _fastForwardToNextEpochAsync(): Promise { const { stakingWrapper } = this.actor.deployment.staking; // increase timestamp of next block by how many seconds we need to // get to the next epoch. const epochEndTime = await stakingWrapper.getCurrentEpochEarliestEndTimeInSeconds().callAsync(); const lastBlockTime = await web3Wrapper.getBlockTimestampAsync('latest'); const dt = Math.max(0, epochEndTime.minus(lastBlockTime).toNumber()); await web3Wrapper.increaseTimeAsync(dt); // mine next block await web3Wrapper.mineBlockAsync(); } }; } export class Keeper extends KeeperMixin(Actor) {}