Add function assertions required for staking rewards fuzzing: withdrawDelegatorRewards, finalizePool, and endEpoch. Also adds payProtocolFee-related assertions to fillOrder

This commit is contained in:
Michael Zhu
2019-12-04 14:44:19 -08:00
parent fff3c1eb36
commit 4663eec950
27 changed files with 817 additions and 239 deletions

View File

@@ -80,8 +80,7 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase
}
private async *_validCreateStakingPool(): AsyncIterableIterator<AssertionResult> {
const { stakingPools } = this.actor.simulationEnvironment!;
const assertion = validCreateStakingPoolAssertion(this.actor.deployment, stakingPools);
const assertion = validCreateStakingPoolAssertion(this.actor.deployment, this.actor.simulationEnvironment!);
while (true) {
const operatorShare = Pseudorandom.integer(constants.PPM).toNumber();
yield assertion.executeAsync([operatorShare, false], { from: this.actor.address });

View File

@@ -66,7 +66,7 @@ export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
private async *_validFillOrder(): AsyncIterableIterator<AssertionResult | void> {
const { actors, balanceStore } = this.actor.simulationEnvironment!;
const assertion = validFillOrderAssertion(this.actor.deployment);
const assertion = validFillOrderAssertion(this.actor.deployment, this.actor.simulationEnvironment!);
while (true) {
const maker = Pseudorandom.sample(filterActorsByRole(actors, Maker));
if (maker === undefined) {

View File

@@ -1,9 +1,10 @@
import { StakingPool, StakingPoolById } from '@0x/contracts-staking';
import { StoredBalance } from '@0x/contracts-staking';
import { expect } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
import { FunctionAssertion, FunctionResult } from './function_assertion';
@@ -16,7 +17,7 @@ import { FunctionAssertion, FunctionResult } from './function_assertion';
/* tslint:disable:no-non-null-assertion */
export function validCreateStakingPoolAssertion(
deployment: DeploymentManager,
pools: StakingPoolById,
simulationEnvironment: SimulationEnvironment,
): FunctionAssertion<[number, boolean], string, string> {
const { stakingWrapper } = deployment.staking;
@@ -44,7 +45,12 @@ export function validCreateStakingPoolAssertion(
expect(actualPoolId).to.equal(expectedPoolId);
// Adds the new pool to local state
pools[actualPoolId] = new StakingPool(txData.from!, operatorShare);
simulationEnvironment.stakingPools[actualPoolId] = {
operator: txData.from!,
operatorShare,
delegatedStake: new StoredBalance(),
lastFinalized: simulationEnvironment.currentEpoch,
};
},
});
}

View File

@@ -0,0 +1,93 @@
import { WETH9DepositEventArgs, WETH9Events } from '@0x/contracts-erc20';
import { AggregatedStats, StakingEvents, StakingEpochEndedEventArgs } from '@0x/contracts-staking';
import { expect, verifyEventsFromLogs } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
import { FunctionAssertion, FunctionResult } from './function_assertion';
interface EndEpochBeforeInfo {
wethReservedForPoolRewards: BigNumber;
aggregatedStatsBefore: AggregatedStats;
}
/**
* Returns a FunctionAssertion for `stake` which assumes valid input is provided. The
* FunctionAssertion checks that the staker and zrxVault's balances of ZRX decrease and increase,
* respectively, by the input amount.
*/
/* tslint:disable:no-unnecessary-type-assertion */
export function validEndEpochAssertion(
deployment: DeploymentManager,
simulationEnvironment: SimulationEnvironment,
): FunctionAssertion<[], EndEpochBeforeInfo, void> {
const { stakingWrapper } = deployment.staking;
const { balanceStore, currentEpoch } = simulationEnvironment;
return new FunctionAssertion(stakingWrapper, 'endEpoch', {
before: async () => {
await balanceStore.updateEthBalancesAsync();
const aggregatedStatsBefore = AggregatedStats.fromArray(
await stakingWrapper.aggregatedStatsByEpoch(currentEpoch).callAsync(),
);
const wethReservedForPoolRewards = await stakingWrapper.wethReservedForPoolRewards().callAsync();
return { wethReservedForPoolRewards, aggregatedStatsBefore };
},
after: async (beforeInfo: EndEpochBeforeInfo, result: FunctionResult, _args: [], _txData: Partial<TxData>) => {
// Check WETH deposit event
const previousEthBalance = balanceStore.balances.eth[stakingWrapper.address];
if (previousEthBalance.isGreaterThan(0)) {
verifyEventsFromLogs<WETH9DepositEventArgs>(
result.receipt!.logs,
[
{
_owner: deployment.staking.stakingProxy.address,
_value: previousEthBalance,
},
],
WETH9Events.Deposit,
);
}
await balanceStore.updateErc20BalancesAsync();
const { wethReservedForPoolRewards, aggregatedStatsBefore } = beforeInfo;
const expectedAggregatedStats = {
...aggregatedStatsBefore,
rewardsAvailable: balanceStore.balances.erc20[stakingWrapper.address][
deployment.tokens.weth.address
].minus(wethReservedForPoolRewards),
};
const aggregatedStatsAfter = AggregatedStats.fromArray(
await stakingWrapper.aggregatedStatsByEpoch(currentEpoch).callAsync(),
);
expect(aggregatedStatsAfter).to.deep.equal(expectedAggregatedStats);
const expectedEpochEndedEvents = aggregatedStatsAfter.numPoolsToFinalize.isZero()
? [
{
epoch: currentEpoch,
numPoolsToFinalize: aggregatedStatsAfter.numPoolsToFinalize,
rewardsAvailable: aggregatedStatsAfter.rewardsAvailable,
totalFeesCollected: aggregatedStatsAfter.totalFeesCollected,
totalWeightedStake: aggregatedStatsAfter.totalWeightedStake,
},
]
: [];
verifyEventsFromLogs<StakingEpochEndedEventArgs>(
result.receipt!.logs,
expectedEpochEndedEvents,
StakingEvents.EpochEnded,
);
expect(result.data, 'endEpoch should return the number of unfinalized pools').to.bignumber.equal(
aggregatedStatsAfter.numPoolsToFinalize,
);
simulationEnvironment.currentEpoch = currentEpoch.plus(1);
},
});
}
/* tslint:enable:no-unnecessary-type-assertion */

View File

@@ -1,18 +1,22 @@
import { ERC20TokenEvents, ERC20TokenTransferEventArgs } from '@0x/contracts-erc20';
import { ExchangeEvents, ExchangeFillEventArgs } from '@0x/contracts-exchange';
import { ReferenceFunctions } from '@0x/contracts-exchange-libs';
import { AggregatedStats, constants as stakingConstants, PoolStats } from '@0x/contracts-staking';
import { expect, orderHashUtils, verifyEvents } from '@0x/contracts-test-utils';
import { FillResults, Order } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
import * as _ from 'lodash';
import { Maker } from '../actors/maker';
import { filterActorsByRole } from '../actors/utils';
import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
import { FunctionAssertion, FunctionResult } from './function_assertion';
function verifyFillEvents(
takerAddress: string,
txData: Partial<TxData>,
order: Order,
receipt: TransactionReceiptWithDecodedLogs,
deployment: DeploymentManager,
@@ -24,6 +28,8 @@ function verifyFillEvents(
DeploymentManager.protocolFeeMultiplier,
DeploymentManager.gasPrice,
);
const takerAddress = txData.from!;
const value = new BigNumber(txData.value || 0);
// Ensure that the fill event was correct.
verifyEvents<ExchangeFillEventArgs>(
receipt,
@@ -44,38 +50,48 @@ function verifyFillEvents(
ExchangeEvents.Fill,
);
const expectedTransferEvents = [
{
_from: takerAddress,
_to: order.makerAddress,
_value: fillResults.takerAssetFilledAmount,
},
{
_from: order.makerAddress,
_to: takerAddress,
_value: fillResults.makerAssetFilledAmount,
},
{
_from: takerAddress,
_to: order.feeRecipientAddress,
_value: fillResults.takerFeePaid,
},
{
_from: order.makerAddress,
_to: order.feeRecipientAddress,
_value: fillResults.makerFeePaid,
},
];
// If not enough wei is sent to cover the protocol fee, there will be an additional WETH transfer event
if (value.isLessThan(DeploymentManager.protocolFee)) {
expectedTransferEvents.push({
_from: takerAddress,
_to: deployment.staking.stakingProxy.address,
_value: DeploymentManager.protocolFee,
});
}
// Ensure that the transfer events were correctly emitted.
verifyEvents<ERC20TokenTransferEventArgs>(
receipt,
[
{
_from: takerAddress,
_to: order.makerAddress,
_value: fillResults.takerAssetFilledAmount,
},
{
_from: order.makerAddress,
_to: takerAddress,
_value: fillResults.makerAssetFilledAmount,
},
{
_from: takerAddress,
_to: order.feeRecipientAddress,
_value: fillResults.takerFeePaid,
},
{
_from: order.makerAddress,
_to: order.feeRecipientAddress,
_value: fillResults.makerFeePaid,
},
{
_from: takerAddress,
_to: deployment.staking.stakingProxy.address,
_value: DeploymentManager.protocolFee,
},
],
ERC20TokenEvents.Transfer,
);
verifyEvents<ERC20TokenTransferEventArgs>(receipt, expectedTransferEvents, ERC20TokenEvents.Transfer);
}
interface FillOrderBeforeInfo {
poolStats: PoolStats;
aggregatedStats: AggregatedStats;
poolStake: BigNumber;
operatorStake: BigNumber;
poolId: string;
}
/**
@@ -85,27 +101,96 @@ function verifyFillEvents(
/* tslint:disable:no-non-null-assertion */
export function validFillOrderAssertion(
deployment: DeploymentManager,
): FunctionAssertion<[Order, BigNumber, string], {}, FillResults> {
const exchange = deployment.exchange;
simulationEnvironment: SimulationEnvironment,
): FunctionAssertion<[Order, BigNumber, string], FillOrderBeforeInfo | void, FillResults> {
const { stakingWrapper } = deployment.staking;
const { actors, currentEpoch } = simulationEnvironment;
return new FunctionAssertion<[Order, BigNumber, string], {}, FillResults>(exchange, 'fillOrder', {
after: async (
_beforeInfo,
result: FunctionResult,
args: [Order, BigNumber, string],
txData: Partial<TxData>,
) => {
const [order, fillAmount] = args;
return new FunctionAssertion<[Order, BigNumber, string], FillOrderBeforeInfo | void, FillResults>(
deployment.exchange,
'fillOrder',
{
before: async (args: [Order, BigNumber, string]) => {
const [order] = args;
const maker = filterActorsByRole(actors, Maker).find(maker => maker.address === order.makerAddress);
// Ensure that the tx succeeded.
expect(result.success, `Error: ${result.data}`).to.be.true();
const poolId = maker!.makerPoolId;
if (poolId === undefined) {
return;
} else {
const poolStats = PoolStats.fromArray(
await stakingWrapper.poolStatsByEpoch(poolId, currentEpoch).callAsync(),
);
const aggregatedStats = AggregatedStats.fromArray(
await stakingWrapper.aggregatedStatsByEpoch(currentEpoch).callAsync(),
);
const { currentEpochBalance: poolStake } = await stakingWrapper
.getTotalStakeDelegatedToPool(poolId)
.callAsync();
const { currentEpochBalance: operatorStake } = await stakingWrapper
.getStakeDelegatedToPoolByOwner(simulationEnvironment.stakingPools[poolId].operator, poolId)
.callAsync();
return { poolStats, aggregatedStats, poolStake, poolId, operatorStake };
}
},
after: async (
beforeInfo: FillOrderBeforeInfo | void,
result: FunctionResult,
args: [Order, BigNumber, string],
txData: Partial<TxData>,
) => {
const [order, fillAmount] = args;
// Ensure that the correct events were emitted.
verifyFillEvents(txData.from!, order, result.receipt!, deployment, fillAmount);
// Ensure that the tx succeeded.
expect(result.success, `Error: ${result.data}`).to.be.true();
// TODO: Add validation for on-chain state (like balances)
// Ensure that the correct events were emitted.
verifyFillEvents(txData, order, result.receipt!, deployment, fillAmount);
if (beforeInfo !== undefined) {
const expectedPoolStats = { ...beforeInfo.poolStats };
const expectedAggregatedStats = { ...beforeInfo.aggregatedStats };
if (beforeInfo.poolStake.isGreaterThanOrEqualTo(stakingConstants.DEFAULT_PARAMS.minimumPoolStake)) {
if (beforeInfo.poolStats.feesCollected.isZero()) {
const membersStakeInPool = beforeInfo.poolStake.minus(beforeInfo.operatorStake);
const weightedStakeInPool = beforeInfo.operatorStake.plus(
ReferenceFunctions.getPartialAmountFloor(
stakingConstants.DEFAULT_PARAMS.rewardDelegatedStakeWeight,
new BigNumber(stakingConstants.PPM),
membersStakeInPool,
),
);
expectedPoolStats.membersStake = membersStakeInPool;
expectedPoolStats.weightedStake = weightedStakeInPool;
expectedAggregatedStats.totalWeightedStake = beforeInfo.aggregatedStats.totalWeightedStake.plus(
weightedStakeInPool,
);
expectedAggregatedStats.numPoolsToFinalize = beforeInfo.aggregatedStats.numPoolsToFinalize.plus(
1,
);
// TODO: Check event
}
expectedPoolStats.feesCollected = beforeInfo.poolStats.feesCollected.plus(
DeploymentManager.protocolFee,
);
expectedAggregatedStats.totalFeesCollected = beforeInfo.aggregatedStats.totalFeesCollected.plus(
DeploymentManager.protocolFee,
);
}
const poolStats = PoolStats.fromArray(
await stakingWrapper.poolStatsByEpoch(beforeInfo.poolId, currentEpoch).callAsync(),
);
const aggregatedStats = AggregatedStats.fromArray(
await stakingWrapper.aggregatedStatsByEpoch(currentEpoch).callAsync(),
);
expect(poolStats).to.deep.equal(expectedPoolStats);
expect(aggregatedStats).to.deep.equal(expectedAggregatedStats);
}
},
},
});
);
}
/* tslint:enable:no-non-null-assertion */
/* tslint:enable:no-unnecessary-type-assertion */

View File

@@ -0,0 +1,267 @@
import { WETH9Events, WETH9TransferEventArgs } from '@0x/contracts-erc20';
import { ReferenceFunctions } from '@0x/contracts-exchange-libs';
import {
AggregatedStats,
constants as stakingConstants,
PoolStats,
StakingEvents,
StakingEpochFinalizedEventArgs,
StakingRewardsPaidEventArgs,
} from '@0x/contracts-staking';
import {
assertRoughlyEquals,
constants,
expect,
filterLogsToArguments,
toDecimal,
verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
import { FunctionAssertion, FunctionResult } from './function_assertion';
const COBB_DOUGLAS_PRECISION = 15;
const ALPHA_NUMERATOR = 1;
const ALPHA_DENOMINATOR = 3;
const COBB_DOUGLAS_ALPHA = toDecimal(ALPHA_NUMERATOR).dividedBy(toDecimal(ALPHA_DENOMINATOR));
function cobbDouglas(poolStats: PoolStats, aggregatedStats: AggregatedStats): BigNumber {
const { feesCollected, weightedStake } = poolStats;
const { rewardsAvailable, totalFeesCollected, totalWeightedStake } = aggregatedStats;
const feeRatio = toDecimal(feesCollected).dividedBy(toDecimal(totalFeesCollected));
const stakeRatio = toDecimal(weightedStake).dividedBy(toDecimal(totalWeightedStake));
// totalRewards * feeRatio ^ alpha * stakeRatio ^ (1-alpha)
return new BigNumber(
feeRatio
.pow(COBB_DOUGLAS_ALPHA)
.times(stakeRatio.pow(toDecimal(1).minus(COBB_DOUGLAS_ALPHA)))
.times(toDecimal(rewardsAvailable))
.toFixed(0, BigNumber.ROUND_FLOOR),
);
}
interface FinalizePoolBeforeInfo {
poolStats: PoolStats;
aggregatedStats: AggregatedStats;
poolRewards: BigNumber;
cumulativeRewardsLastStored: BigNumber;
mostRecentCumulativeRewards: {
numerator: BigNumber;
denominator: BigNumber;
};
}
/**
* Returns a FunctionAssertion for `moveStake` which assumes valid input is provided. The
* FunctionAssertion checks that the staker's
*/
/* tslint:disable:no-unnecessary-type-assertion */
export function validFinalizePoolAssertion(
deployment: DeploymentManager,
simulationEnvironment: SimulationEnvironment,
): FunctionAssertion<[string], FinalizePoolBeforeInfo, void> {
const { stakingWrapper } = deployment.staking;
const { currentEpoch } = simulationEnvironment;
const prevEpoch = currentEpoch.minus(1);
return new FunctionAssertion<[string], FinalizePoolBeforeInfo, void>(stakingWrapper, 'finalizePool', {
before: async (args: [string]) => {
const [poolId] = args;
const poolStats = PoolStats.fromArray(await stakingWrapper.poolStatsByEpoch(poolId, prevEpoch).callAsync());
const aggregatedStats = AggregatedStats.fromArray(
await stakingWrapper.aggregatedStatsByEpoch(prevEpoch).callAsync(),
);
const poolRewards = await stakingWrapper.rewardsByPoolId(poolId).callAsync();
const [
mostRecentCumulativeRewards,
cumulativeRewardsLastStored,
] = await stakingWrapper.getMostRecentCumulativeReward(poolId).callAsync();
return {
poolStats,
aggregatedStats,
poolRewards,
cumulativeRewardsLastStored,
mostRecentCumulativeRewards,
};
},
after: async (beforeInfo: FinalizePoolBeforeInfo, result: FunctionResult, args: [string]) => {
// // Compute relevant epochs
// uint256 currentEpoch_ = currentEpoch;
// uint256 prevEpoch = currentEpoch_.safeSub(1);
const { stakingPools } = simulationEnvironment;
const [poolId] = args;
const pool = stakingPools[poolId];
// // Load the aggregated stats into memory; noop if no pools to finalize.
// IStructs.AggregatedStats memory aggregatedStats = aggregatedStatsByEpoch[prevEpoch];
// if (aggregatedStats.numPoolsToFinalize == 0) {
// return;
// }
//
// // Noop if the pool did not earn rewards or already finalized (has no fees).
// IStructs.PoolStats memory poolStats = poolStatsByEpoch[poolId][prevEpoch];
// if (poolStats.feesCollected == 0) {
// return;
// }
if (beforeInfo.aggregatedStats.numPoolsToFinalize.isZero() || beforeInfo.poolStats.feesCollected.isZero()) {
expect(result.receipt!.logs.length, 'Expect no events to be emitted').to.equal(0);
return;
}
// // Clear the pool stats so we don't finalize it again, and to recoup
// // some gas.
// delete poolStatsByEpoch[poolId][prevEpoch];
const poolStats = PoolStats.fromArray(await stakingWrapper.poolStatsByEpoch(poolId, prevEpoch).callAsync());
expect(poolStats).to.deep.equal({
feesCollected: constants.ZERO_AMOUNT,
weightedStake: constants.ZERO_AMOUNT,
membersStake: constants.ZERO_AMOUNT,
});
// // Compute the rewards.
// uint256 rewards = _getUnfinalizedPoolRewardsFromPoolStats(poolStats, aggregatedStats);
const rewards = BigNumber.min(
cobbDouglas(beforeInfo.poolStats, beforeInfo.aggregatedStats),
beforeInfo.aggregatedStats.rewardsAvailable.minus(beforeInfo.aggregatedStats.totalRewardsFinalized),
);
// // Pay the operator and update rewards for the pool.
// // Note that we credit at the CURRENT epoch even though these rewards
// // were earned in the previous epoch.
// (uint256 operatorReward, uint256 membersReward) = _syncPoolRewards(
// poolId,
// rewards,
// poolStats.membersStake
// );
//
// // Emit an event.
// emit RewardsPaid(
// currentEpoch_,
// poolId,
// operatorReward,
// membersReward
// );
//
// uint256 totalReward = operatorReward.safeAdd(membersReward);
const events = filterLogsToArguments<StakingRewardsPaidEventArgs>(
result.receipt!.logs,
StakingEvents.RewardsPaid,
);
expect(events.length, 'Number of RewardsPaid events emitted').to.equal(1);
const [rewardsPaidEvent] = events;
expect(rewardsPaidEvent.currentEpoch_, 'RewardsPaid event: currentEpoch_').to.bignumber.equal(currentEpoch);
expect(rewardsPaidEvent.poolId, 'RewardsPaid event: poolId').to.bignumber.equal(poolId);
expect(rewardsPaidEvent.currentEpoch_, 'RewardsPaid event: currentEpoch_').to.bignumber.equal(currentEpoch);
const { operatorReward, membersReward } = rewardsPaidEvent;
const totalReward = operatorReward.plus(membersReward);
assertRoughlyEquals(totalReward, rewards, COBB_DOUGLAS_PRECISION);
// See _computePoolRewardsSplit
if (beforeInfo.poolStats.membersStake.isZero()) {
expect(
operatorReward,
"operatorReward should equal totalReward if pool's membersStake is 0",
).to.bignumber.equal(totalReward);
} else {
expect(operatorReward).to.bignumber.equal(
ReferenceFunctions.getPartialAmountCeil(
new BigNumber(pool.operatorShare),
new BigNumber(stakingConstants.PPM),
totalReward,
),
);
}
// See _syncPoolRewards
const expectedTransferEvents = operatorReward.isGreaterThan(0)
? [
{
_from: deployment.staking.stakingProxy.address,
_to: pool.operator,
_value: operatorReward,
},
]
: [];
// Check for WETH transfer event emitted when paying out operator's reward.
verifyEventsFromLogs<WETH9TransferEventArgs>(
result.receipt!.logs,
expectedTransferEvents,
WETH9Events.Transfer,
);
// Check that pool rewards have increased.
const poolRewards = await stakingWrapper.rewardsByPoolId(poolId).callAsync();
expect(poolRewards).to.bignumber.equal(beforeInfo.poolRewards.plus(membersReward));
// Check that cumulative rewards have increased.
const [
mostRecentCumulativeRewards,
cumulativeRewardsLastStored,
] = await stakingWrapper.getMostRecentCumulativeReward(poolId).callAsync();
expect(cumulativeRewardsLastStored).to.bignumber.equal(currentEpoch);
let [numerator, denominator] = ReferenceFunctions.LibFractions.add(
beforeInfo.mostRecentCumulativeRewards.numerator,
beforeInfo.mostRecentCumulativeRewards.denominator,
membersReward,
beforeInfo.poolStats.membersStake,
);
[numerator, denominator] = ReferenceFunctions.LibFractions.normalize(numerator, denominator);
expect(mostRecentCumulativeRewards).to.deep.equal({
numerator,
denominator,
});
// // Increase `totalRewardsFinalized`.
// aggregatedStatsByEpoch[prevEpoch].totalRewardsFinalized =
// aggregatedStats.totalRewardsFinalized =
// aggregatedStats.totalRewardsFinalized.safeAdd(totalReward);
//
// // Decrease the number of unfinalized pools left.
// aggregatedStatsByEpoch[prevEpoch].numPoolsToFinalize =
// aggregatedStats.numPoolsToFinalize =
// aggregatedStats.numPoolsToFinalize.safeSub(1);
const aggregatedStats = AggregatedStats.fromArray(
await stakingWrapper.aggregatedStatsByEpoch(prevEpoch).callAsync(),
);
expect(aggregatedStats).to.deep.equal({
...beforeInfo.aggregatedStats,
totalRewardsFinalized: beforeInfo.aggregatedStats.totalRewardsFinalized.plus(totalReward),
numPoolsToFinalize: beforeInfo.aggregatedStats.numPoolsToFinalize.minus(1),
});
// // If there are no more unfinalized pools remaining, the epoch is
// // finalized.
// if (aggregatedStats.numPoolsToFinalize == 0) {
// emit EpochFinalized(
// prevEpoch,
// aggregatedStats.totalRewardsFinalized,
// aggregatedStats.rewardsAvailable.safeSub(aggregatedStats.totalRewardsFinalized)
// );
// }
const expectedEpochFinalizedEvents = aggregatedStats.numPoolsToFinalize.isZero()
? [
{
epoch: currentEpoch.minus(1),
rewardsPaid: aggregatedStats.totalRewardsFinalized,
rewardsRemaining: aggregatedStats.rewardsAvailable.minus(
aggregatedStats.totalRewardsFinalized,
),
},
]
: [];
verifyEventsFromLogs<StakingEpochFinalizedEventArgs>(
result.receipt!.logs,
expectedEpochFinalizedEvents,
StakingEvents.EpochFinalized,
);
pool.lastFinalized = currentEpoch;
},
});
}
/* tslint:enable:no-unnecessary-type-assertion */

View File

@@ -0,0 +1,97 @@
import { WETH9TransferEventArgs, WETH9Events } from '@0x/contracts-erc20';
import { StoredBalance } from '@0x/contracts-staking';
import { expect, verifyEventsFromLogs } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
import { FunctionAssertion, FunctionResult } from './function_assertion';
interface WithdrawDelegatorRewardsBeforeInfo {
delegatorStake: StoredBalance;
poolRewards: BigNumber;
wethReservedForPoolRewards: BigNumber;
delegatorReward: BigNumber;
}
/**
* Returns a FunctionAssertion for `stake` which assumes valid input is provided. The
* FunctionAssertion checks that the staker and zrxVault's balances of ZRX decrease and increase,
* respectively, by the input amount.
*/
/* tslint:disable:no-unnecessary-type-assertion */
export function validWithdrawDelegatorRewardsAssertion(
deployment: DeploymentManager,
simulationEnvironment: SimulationEnvironment,
): FunctionAssertion<[string], WithdrawDelegatorRewardsBeforeInfo, void> {
const { stakingWrapper } = deployment.staking;
const { currentEpoch } = simulationEnvironment;
return new FunctionAssertion(stakingWrapper, 'withdrawDelegatorRewards', {
before: async (args: [string], txData: Partial<TxData>) => {
const [poolId] = args;
const delegatorStake = await stakingWrapper
.getStakeDelegatedToPoolByOwner(txData.from!, poolId)
.callAsync();
const poolRewards = await stakingWrapper.rewardsByPoolId(poolId).callAsync();
const wethReservedForPoolRewards = await stakingWrapper.wethReservedForPoolRewards().callAsync();
const delegatorReward = BigNumber.sum(
await stakingWrapper
.computeMemberRewardOverInterval(
poolId,
delegatorStake.currentEpochBalance,
delegatorStake.currentEpoch,
delegatorStake.currentEpoch.plus(1),
)
.callAsync(),
await stakingWrapper
.computeMemberRewardOverInterval(
poolId,
delegatorStake.nextEpochBalance,
delegatorStake.currentEpoch.plus(1),
currentEpoch,
)
.callAsync(),
); // TODO: Test the reward computation more robustly
return { delegatorStake, poolRewards, wethReservedForPoolRewards, delegatorReward };
},
after: async (
beforeInfo: WithdrawDelegatorRewardsBeforeInfo,
result: FunctionResult,
args: [string],
txData: Partial<TxData>,
) => {
const [poolId] = args;
const expectedDelegatorStake = {
...beforeInfo.delegatorStake,
currentEpoch: currentEpoch,
currentEpochBalance: currentEpoch.isGreaterThan(beforeInfo.delegatorStake.currentEpoch)
? beforeInfo.delegatorStake.nextEpochBalance
: beforeInfo.delegatorStake.currentEpochBalance,
};
const delegatorStake = await stakingWrapper
.getStakeDelegatedToPoolByOwner(txData.from!, poolId)
.callAsync();
expect(delegatorStake).to.deep.equal(expectedDelegatorStake);
const expectedPoolRewards = beforeInfo.poolRewards.minus(beforeInfo.delegatorReward);
const poolRewards = await stakingWrapper.rewardsByPoolId(poolId).callAsync();
expect(poolRewards).to.bignumber.equal(expectedPoolRewards);
const expectedTransferEvents = beforeInfo.delegatorReward.isZero()
? []
: [{ _from: stakingWrapper.address, _to: txData.from!, _value: beforeInfo.delegatorReward }];
verifyEventsFromLogs<WETH9TransferEventArgs>(
result.receipt!.logs,
expectedTransferEvents,
WETH9Events.Transfer,
);
// TODO: Check CR
},
});
}
/* tslint:enable:no-unnecessary-type-assertion */

View File

@@ -34,6 +34,7 @@ export class SimulationEnvironment {
globalStake: this.globalStake,
stakingPools: this.stakingPools,
balanceStore: this.balanceStore.toReadable(),
currentEpoch: this.currentEpoch,
};
}
}