Fuzz testing for matchOrders and matchOrdersWithMaximalFill.

This commit is contained in:
Greg Hysen
2020-01-10 17:09:00 -08:00
parent e01d32ef1a
commit c09ac58ac0
6 changed files with 471 additions and 85 deletions

View File

@@ -1,13 +1,7 @@
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,
StakingEvents,
StakingStakingPoolEarnedRewardsInEpochEventArgs,
} from '@0x/contracts-staking';
import { AggregatedStats, 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';
@@ -18,6 +12,7 @@ import { Maker } from '../actors/maker';
import { filterActorsByRole } from '../actors/utils';
import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
import { assertProtocolFeePaidAsync, getPoolInfoAsync } from '../utils/assert_protocol_fee';
import { FunctionAssertion, FunctionResult } from './function_assertion';
@@ -109,8 +104,8 @@ export function validFillOrderAssertion(
deployment: DeploymentManager,
simulationEnvironment: SimulationEnvironment,
): FunctionAssertion<[Order, BigNumber, string], FillOrderBeforeInfo | void, FillResults> {
const { stakingWrapper } = deployment.staking;
const { actors } = simulationEnvironment;
const expectedProtocolFee = DeploymentManager.protocolFee;
return new FunctionAssertion<[Order, BigNumber, string], FillOrderBeforeInfo | void, FillResults>(
deployment.exchange,
@@ -118,27 +113,9 @@ export function validFillOrderAssertion(
{
before: async (args: [Order, BigNumber, string]) => {
const [order] = args;
const { currentEpoch } = simulationEnvironment;
const maker = filterActorsByRole(actors, Maker).find(actor => actor.address === order.makerAddress);
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 };
}
const poolInfo = getPoolInfoAsync(maker!, simulationEnvironment, deployment);
return poolInfo;
},
after: async (
beforeInfo: FillOrderBeforeInfo | void,
@@ -150,69 +127,20 @@ export function validFillOrderAssertion(
expect(result.success, `Error: ${result.data}`).to.be.true();
const [order, fillAmount] = args;
const { currentEpoch } = simulationEnvironment;
// Ensure that the correct events were emitted.
verifyFillEvents(txData, order, result.receipt!, deployment, fillAmount);
// If the maker is not in a staking pool, there's nothing to check
if (beforeInfo === undefined) {
return;
}
const expectedPoolStats = { ...beforeInfo.poolStats };
const expectedAggregatedStats = { ...beforeInfo.aggregatedStats };
const expectedEvents = [];
// Refer to `payProtocolFee`
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,
);
// StakingPoolEarnedRewardsInEpoch event emitted
expectedEvents.push({
epoch: currentEpoch,
poolId: beforeInfo.poolId,
});
}
// Credit a protocol fee to the maker's staking pool
expectedPoolStats.feesCollected = beforeInfo.poolStats.feesCollected.plus(
DeploymentManager.protocolFee,
);
// Update aggregated stats
expectedAggregatedStats.totalFeesCollected = beforeInfo.aggregatedStats.totalFeesCollected.plus(
DeploymentManager.protocolFee,
// If the maker is in a staking pool then validate the protocol fee.
if (beforeInfo !== undefined) {
await assertProtocolFeePaidAsync(
beforeInfo,
result,
simulationEnvironment,
deployment,
expectedProtocolFee,
);
}
// Check for updated stats and event
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);
verifyEvents<StakingStakingPoolEarnedRewardsInEpochEventArgs>(
result.receipt!,
expectedEvents,
StakingEvents.StakingPoolEarnedRewardsInEpoch,
);
},
},
);

View File

@@ -0,0 +1,79 @@
import { MatchedFillResults, Order } from '@0x/types';
import { 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 { assertProtocolFeePaidAsync, getPoolInfoAsync, PoolInfo } from '../utils/assert_protocol_fee';
import { verifyMatchEvents } from '../utils/verify_match_events';
import { FunctionAssertion, FunctionResult } from './function_assertion';
export const matchOrderRuntimeAssertion = (
deployment: DeploymentManager,
simulationEnvironment: SimulationEnvironment,
withMaximalFill: boolean,
) => {
const { actors } = simulationEnvironment;
const expectedProtocolFee = DeploymentManager.protocolFee.times(2);
return {
before: async (args: [Order, Order, string, string]) => {
const [order] = args;
const maker = filterActorsByRole(actors, Maker).find(actor => actor.address === order.makerAddress);
// tslint:disable-next-line no-non-null-assertion
const poolInfo = getPoolInfoAsync(maker!, simulationEnvironment, deployment);
return poolInfo;
},
after: async (
beforeInfo: PoolInfo | void,
result: FunctionResult,
args: [Order, Order, string, string],
txData: Partial<TxData>,
) => {
// Ensure that the correct events were emitted.
const [leftOrder, rightOrder] = args;
verifyMatchEvents(
txData,
leftOrder,
rightOrder,
// tslint:disable-next-line no-non-null-assertion no-unnecessary-type-assertion
result.receipt!,
deployment,
withMaximalFill,
);
// If the maker is in a staking pool then validate the protocol fee.
if (beforeInfo !== undefined) {
await assertProtocolFeePaidAsync(
beforeInfo,
result,
simulationEnvironment,
deployment,
expectedProtocolFee,
);
}
},
};
};
/**
* A function assertion that verifies that a complete and valid `matchOrders` succeeded and emitted the correct logs.
*/
/* tslint:disable:no-unnecessary-type-assertion */
/* tslint:disable:no-non-null-assertion */
export function validMatchOrdersAssertion(
deployment: DeploymentManager,
simulationEnvironment: SimulationEnvironment,
): FunctionAssertion<[Order, Order, string, string], PoolInfo | void, MatchedFillResults> {
return new FunctionAssertion<[Order, Order, string, string], PoolInfo | void, MatchedFillResults>(
deployment.exchange,
'matchOrders',
matchOrderRuntimeAssertion(deployment, simulationEnvironment, false),
);
}
/* tslint:enable:no-non-null-assertion */
/* tslint:enable:no-unnecessary-type-assertion */

View File

@@ -0,0 +1,27 @@
import { MatchedFillResults, Order } from '@0x/types';
import * as _ from 'lodash';
import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
import { PoolInfo } from '../utils/assert_protocol_fee';
import { FunctionAssertion } from './function_assertion';
import { matchOrderRuntimeAssertion } from './matchOrders';
/**
* A function assertion that verifies that a complete and valid `matchOrdersWithMaximalFill` succeeded and emitted the correct logs.
*/
/* tslint:disable:no-unnecessary-type-assertion */
/* tslint:disable:no-non-null-assertion */
export function validMatchOrdersWithMaximalFillAssertion(
deployment: DeploymentManager,
simulationEnvironment: SimulationEnvironment,
): FunctionAssertion<[Order, Order, string, string], PoolInfo | void, MatchedFillResults> {
return new FunctionAssertion<[Order, Order, string, string], PoolInfo | void, MatchedFillResults>(
deployment.exchange,
'matchOrdersWithMaximalFill',
matchOrderRuntimeAssertion(deployment, simulationEnvironment, true),
);
}
/* tslint:enable:no-non-null-assertion */
/* tslint:enable:no-unnecessary-type-assertion */

View File

@@ -0,0 +1,118 @@
import { ReferenceFunctions } from '@0x/contracts-exchange-libs';
import {
AggregatedStats,
constants as stakingConstants,
PoolStats,
StakingEvents,
StakingStakingPoolEarnedRewardsInEpochEventArgs,
} from '@0x/contracts-staking';
import { expect, verifyEvents } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { Maker } from '../actors/maker';
import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
import { FunctionResult } from '../assertions/function_assertion';
export interface PoolInfo {
poolStats: PoolStats;
aggregatedStats: AggregatedStats;
poolStake: BigNumber;
operatorStake: BigNumber;
poolId: string;
}
/**
* Gets info for a given maker's pool.
*/
export async function getPoolInfoAsync(
maker: Maker,
simulationEnvironment: SimulationEnvironment,
deployment: DeploymentManager,
): Promise<PoolInfo | undefined> {
const { stakingWrapper } = deployment.staking;
// tslint:disable-next-line no-non-null-assertion no-unnecessary-type-assertion
const poolId = maker!.makerPoolId;
const { currentEpoch } = simulationEnvironment;
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 };
}
}
/**
* Asserts that a protocol fee was paid.
*/
export async function assertProtocolFeePaidAsync(
poolInfo: PoolInfo,
result: FunctionResult,
simulationEnvironment: SimulationEnvironment,
deployment: DeploymentManager,
expectedProtocolFee: BigNumber,
): Promise<void> {
const { currentEpoch } = simulationEnvironment;
const { stakingWrapper } = deployment.staking;
const expectedPoolStats = { ...poolInfo.poolStats };
const expectedAggregatedStats = { ...poolInfo.aggregatedStats };
const expectedEvents = [];
// Refer to `payProtocolFee`
if (poolInfo.poolStake.isGreaterThanOrEqualTo(stakingConstants.DEFAULT_PARAMS.minimumPoolStake)) {
if (poolInfo.poolStats.feesCollected.isZero()) {
const membersStakeInPool = poolInfo.poolStake.minus(poolInfo.operatorStake);
const weightedStakeInPool = poolInfo.operatorStake.plus(
ReferenceFunctions.getPartialAmountFloor(
stakingConstants.DEFAULT_PARAMS.rewardDelegatedStakeWeight,
new BigNumber(stakingConstants.PPM),
membersStakeInPool,
),
);
expectedPoolStats.membersStake = membersStakeInPool;
expectedPoolStats.weightedStake = weightedStakeInPool;
expectedAggregatedStats.totalWeightedStake = poolInfo.aggregatedStats.totalWeightedStake.plus(
weightedStakeInPool,
);
expectedAggregatedStats.numPoolsToFinalize = poolInfo.aggregatedStats.numPoolsToFinalize.plus(1);
// StakingPoolEarnedRewardsInEpoch event emitted
expectedEvents.push({
epoch: currentEpoch,
poolId: poolInfo.poolId,
});
}
// Credit a protocol fee to the maker's staking pool
expectedPoolStats.feesCollected = poolInfo.poolStats.feesCollected.plus(expectedProtocolFee);
// Update aggregated stats
expectedAggregatedStats.totalFeesCollected = poolInfo.aggregatedStats.totalFeesCollected.plus(
expectedProtocolFee,
);
}
// Check for updated stats and event
const poolStats = PoolStats.fromArray(
await stakingWrapper.poolStatsByEpoch(poolInfo.poolId, currentEpoch).callAsync(),
);
const aggregatedStats = AggregatedStats.fromArray(
await stakingWrapper.aggregatedStatsByEpoch(currentEpoch).callAsync(),
);
expect(poolStats).to.deep.equal(expectedPoolStats);
expect(aggregatedStats).to.deep.equal(expectedAggregatedStats);
verifyEvents<StakingStakingPoolEarnedRewardsInEpochEventArgs>(
// tslint:disable-next-line no-non-null-assertion no-unnecessary-type-assertion
result.receipt!,
expectedEvents,
StakingEvents.StakingPoolEarnedRewardsInEpoch,
);
}

View File

@@ -0,0 +1,151 @@
import { ERC20TokenEvents, ERC20TokenTransferEventArgs } from '@0x/contracts-erc20';
import { ExchangeEvents, ExchangeFillEventArgs } from '@0x/contracts-exchange';
import { ReferenceFunctions } from '@0x/contracts-exchange-libs';
import { orderHashUtils, verifyEvents } from '@0x/contracts-test-utils';
import { MatchedFillResults, Order } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
import * as _ from 'lodash';
import { DeploymentManager } from '../deployment_manager';
/**
* Verifies `Fill` and `Transfer` events emitted by `matchOrders` or `matchOrdersWithMaximalFill`.
*/
export function verifyMatchEvents(
txData: Partial<TxData>,
leftOrder: Order,
rightOrder: Order,
receipt: TransactionReceiptWithDecodedLogs,
deployment: DeploymentManager,
withMaximalFill: boolean,
): void {
const matchResults = ReferenceFunctions.calculateMatchResults(
leftOrder,
rightOrder,
DeploymentManager.protocolFeeMultiplier,
DeploymentManager.gasPrice,
withMaximalFill,
);
const takerAddress = txData.from as string;
const value = new BigNumber(txData.value || 0);
verifyMatchFilledEvents(leftOrder, rightOrder, receipt, matchResults, takerAddress);
verifyMatchTransferEvents(leftOrder, rightOrder, receipt, matchResults, takerAddress, value, deployment);
}
/**
* Verifies `Fill` events emitted by `matchOrders` or `matchOrdersWithMaximalFill`.
*/
const verifyMatchFilledEvents = (
leftOrder: Order,
rightOrder: Order,
receipt: TransactionReceiptWithDecodedLogs,
matchResults: MatchedFillResults,
takerAddress: string,
) => {
const expectedFillEvents = [
{
makerAddress: leftOrder.makerAddress,
feeRecipientAddress: leftOrder.feeRecipientAddress,
makerAssetData: leftOrder.makerAssetData,
takerAssetData: leftOrder.takerAssetData,
makerFeeAssetData: leftOrder.makerFeeAssetData,
takerFeeAssetData: leftOrder.takerFeeAssetData,
orderHash: orderHashUtils.getOrderHashHex(leftOrder),
takerAddress,
senderAddress: takerAddress,
...matchResults.left,
},
{
makerAddress: rightOrder.makerAddress,
feeRecipientAddress: rightOrder.feeRecipientAddress,
makerAssetData: rightOrder.makerAssetData,
takerAssetData: rightOrder.takerAssetData,
makerFeeAssetData: rightOrder.makerFeeAssetData,
takerFeeAssetData: rightOrder.takerFeeAssetData,
orderHash: orderHashUtils.getOrderHashHex(rightOrder),
takerAddress,
senderAddress: takerAddress,
...matchResults.right,
},
];
verifyEvents<ExchangeFillEventArgs>(receipt, expectedFillEvents, ExchangeEvents.Fill);
};
/**
* Verifies `Transfer` events emitted by `matchOrders` or `matchOrdersWithMaximalFill`.
*/
const verifyMatchTransferEvents = (
leftOrder: Order,
rightOrder: Order,
receipt: TransactionReceiptWithDecodedLogs,
matchResults: MatchedFillResults,
takerAddress: string,
value: BigNumber,
deployment: DeploymentManager,
) => {
const expectedTransferEvents = [
{
_from: rightOrder.makerAddress,
_to: leftOrder.makerAddress,
_value: matchResults.left.takerAssetFilledAmount,
},
{
_from: leftOrder.makerAddress,
_to: rightOrder.makerAddress,
_value: matchResults.right.takerAssetFilledAmount,
},
{
_from: rightOrder.makerAddress,
_to: rightOrder.feeRecipientAddress,
_value: matchResults.right.makerFeePaid,
},
{
_from: leftOrder.makerAddress,
_to: leftOrder.feeRecipientAddress,
_value: matchResults.left.makerFeePaid,
},
{
_from: leftOrder.makerAddress,
_to: takerAddress,
_value: matchResults.left.makerAssetFilledAmount.minus(matchResults.right.takerAssetFilledAmount),
},
{
_from: rightOrder.makerAddress,
_to: takerAddress,
_value: matchResults.right.makerAssetFilledAmount.minus(matchResults.left.takerAssetFilledAmount),
},
{
_from: takerAddress,
_to: deployment.staking.stakingProxy.address,
_value: value.isLessThan(DeploymentManager.protocolFee.times(2))
? DeploymentManager.protocolFee
: new BigNumber(0),
},
{
_from: takerAddress,
_to: deployment.staking.stakingProxy.address,
_value: value.isLessThan(DeploymentManager.protocolFee) ? DeploymentManager.protocolFee : new BigNumber(0),
},
{
_from: takerAddress,
_to: rightOrder.feeRecipientAddress,
_value:
leftOrder.feeRecipientAddress === rightOrder.feeRecipientAddress
? new BigNumber(0)
: matchResults.right.takerFeePaid,
},
{
_from: takerAddress,
_to: leftOrder.feeRecipientAddress,
_value:
leftOrder.feeRecipientAddress === rightOrder.feeRecipientAddress
? matchResults.left.takerFeePaid.plus(matchResults.right.takerFeePaid)
: matchResults.left.takerFeePaid,
},
].filter(event => event._value.isGreaterThan(0));
verifyEvents<ERC20TokenTransferEventArgs>(receipt, expectedTransferEvents, ERC20TokenEvents.Transfer);
};