Skip to content
This repository has been archived by the owner on Jul 9, 2021. It is now read-only.

Commit

Permalink
Merge pull request #2366 from 0xProject/feature/fuzz/makers-and-takers
Browse files Browse the repository at this point in the history
Pool Member Fuzz Tests
  • Loading branch information
moodlezoup authored Dec 4, 2019
2 parents 4f17a25 + 3d79fe2 commit b86d190
Show file tree
Hide file tree
Showing 17 changed files with 670 additions and 156 deletions.
2 changes: 2 additions & 0 deletions contracts/integrations/test/framework/actors/hybrids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { KeeperMixin } from './keeper';
import { MakerMixin } from './maker';
import { PoolOperatorMixin } from './pool_operator';
import { StakerMixin } from './staker';
import { TakerMixin } from './taker';

export class OperatorMaker extends PoolOperatorMixin(MakerMixin(Actor)) {}
export class StakerMaker extends StakerMixin(MakerMixin(Actor)) {}
export class StakerOperator extends StakerMixin(PoolOperatorMixin(Actor)) {}
export class OperatorStakerMaker extends PoolOperatorMixin(StakerMixin(MakerMixin(Actor))) {}
export class StakerKeeper extends StakerMixin(KeeperMixin(Actor)) {}
export class MakerTaker extends MakerMixin(TakerMixin(Actor)) {}
23 changes: 23 additions & 0 deletions contracts/integrations/test/framework/actors/maker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { constants, OrderFactory } from '@0x/contracts-test-utils';
import { Order, SignedOrder } from '@0x/types';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';

import { AssertionResult } from '../assertions/function_assertion';
import { validJoinStakingPoolAssertion } from '../assertions/joinStakingPool';

import { Actor, ActorConfig, Constructor } from './base';

Expand Down Expand Up @@ -45,6 +49,12 @@ export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
...orderConfig,
};
this.orderFactory = new OrderFactory(this.actor.privateKey, defaultOrderParams);

// Register this mixin's assertion generators
this.actor.simulationActions = {
...this.actor.simulationActions,
validJoinStakingPool: this._validJoinStakingPool(),
};
}

/**
Expand Down Expand Up @@ -73,6 +83,19 @@ export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
from: this.actor.address,
});
}

private async *_validJoinStakingPool(): AsyncIterableIterator<AssertionResult | void> {
const { stakingPools } = this.actor.simulationEnvironment!;
const assertion = validJoinStakingPoolAssertion(this.actor.deployment);
while (true) {
const poolId = _.sample(Object.keys(stakingPools));
if (poolId === undefined) {
yield undefined;
} else {
yield assertion.executeAsync([poolId], { from: this.actor.address });
}
}
}
};
}

Expand Down
8 changes: 4 additions & 4 deletions contracts/integrations/test/framework/actors/pool_operator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase
const { stakingPools } = this.actor.simulationEnvironment!;
const assertion = validCreateStakingPoolAssertion(this.actor.deployment, stakingPools);
while (true) {
const operatorShare = getRandomInteger(0, constants.PPM);
yield assertion.executeAsync(operatorShare, false, { from: this.actor.address });
const operatorShare = getRandomInteger(0, constants.PPM).toNumber();
yield assertion.executeAsync([operatorShare, false], { from: this.actor.address });
}
}

Expand All @@ -96,8 +96,8 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase
if (poolId === undefined) {
yield undefined;
} else {
const operatorShare = getRandomInteger(0, stakingPools[poolId].operatorShare);
yield assertion.executeAsync(poolId, operatorShare, { from: this.actor.address });
const operatorShare = getRandomInteger(0, stakingPools[poolId].operatorShare).toNumber();
yield assertion.executeAsync([poolId, operatorShare], { from: this.actor.address });
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions contracts/integrations/test/framework/actors/staker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
await balanceStore.updateErc20BalancesAsync();
const zrxBalance = balanceStore.balances.erc20[this.actor.address][zrx.address];
const amount = getRandomInteger(0, zrxBalance);
yield assertion.executeAsync(amount, { from: this.actor.address });
yield assertion.executeAsync([amount], { from: this.actor.address });
}
}

Expand All @@ -95,7 +95,7 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
undelegatedStake.nextEpochBalance,
);
const amount = getRandomInteger(0, withdrawableStake);
yield assertion.executeAsync(amount, { from: this.actor.address });
yield assertion.executeAsync([amount], { from: this.actor.address });
}
}

Expand Down Expand Up @@ -124,7 +124,7 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
: this.stake[StakeStatus.Delegated][from.poolId].nextEpochBalance;
const amount = getRandomInteger(0, moveableStake);

yield assertion.executeAsync(from, to, amount, { from: this.actor.address });
yield assertion.executeAsync([from, to, amount], { from: this.actor.address });
}
}
};
Expand Down
41 changes: 41 additions & 0 deletions contracts/integrations/test/framework/actors/taker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { constants, getRandomInteger } from '@0x/contracts-test-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
import * as _ from 'lodash';

import { validFillOrderCompleteFillAssertion } from '../assertions/fillOrder';
import { AssertionResult } from '../assertions/function_assertion';
import { DeploymentManager } from '../deployment_manager';

import { Actor, Constructor } from './base';
Expand Down Expand Up @@ -31,6 +35,12 @@ export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
// tslint:disable-next-line:no-inferred-empty-object-type
super(...args);
this.actor = (this as any) as Actor;

// Register this mixin's assertion generators
this.actor.simulationActions = {
...this.actor.simulationActions,
validFillOrderCompleteFill: this._validFillOrderCompleteFill(),
};
}

/**
Expand All @@ -50,6 +60,37 @@ export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
...txData,
});
}

private async *_validFillOrderCompleteFill(): AsyncIterableIterator<AssertionResult | void> {
const { marketMakers } = this.actor.simulationEnvironment!;
const assertion = validFillOrderCompleteFillAssertion(this.actor.deployment);
while (true) {
const maker = _.sample(marketMakers);
if (maker === undefined) {
yield undefined;
} else {
// Configure the maker's token balances so that the order will definitely be fillable.
await Promise.all([
...this.actor.deployment.tokens.erc20.map(async token => maker.configureERC20TokenAsync(token)),
...this.actor.deployment.tokens.erc20.map(async token =>
this.actor.configureERC20TokenAsync(token),
),
this.actor.configureERC20TokenAsync(
this.actor.deployment.tokens.weth,
this.actor.deployment.staking.stakingProxy.address,
),
]);

const order = await maker.signOrderAsync({
makerAssetAmount: getRandomInteger(constants.ZERO_AMOUNT, constants.INITIAL_ERC20_BALANCE),
takerAssetAmount: getRandomInteger(constants.ZERO_AMOUNT, constants.INITIAL_ERC20_BALANCE),
});
yield assertion.executeAsync([order, order.takerAssetAmount, order.signature], {
from: this.actor.address,
});
}
}
}
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,42 +13,48 @@ import { FunctionAssertion, FunctionResult } from './function_assertion';
* Returns a FunctionAssertion for `createStakingPool` which assumes valid input is provided. The
* FunctionAssertion checks that the new poolId is one more than the last poolId.
*/
/* tslint:disable:no-non-null-assertion */
export function validCreateStakingPoolAssertion(
deployment: DeploymentManager,
pools: StakingPoolById,
): FunctionAssertion<string, string> {
): FunctionAssertion<[number, boolean], string, string> {
const { stakingWrapper } = deployment.staking;

return new FunctionAssertion(stakingWrapper.createStakingPool, {
// Returns the expected ID of th created pool
before: async () => {
const lastPoolId = await stakingWrapper.lastPoolId().callAsync();
// Effectively the last poolId + 1, but as a bytestring
return `0x${new BigNumber(lastPoolId)
.plus(1)
.toString(16)
.padStart(64, '0')}`;
},
after: async (
expectedPoolId: string,
result: FunctionResult,
operatorShare: number,
addOperatorAsMaker: boolean,
txData: Partial<TxData>,
) => {
logUtils.log(`createStakingPool(${operatorShare}, ${addOperatorAsMaker}) => ${expectedPoolId}`);

// Checks the logs for the new poolId, verifies that it is as expected
const log = result.receipt!.logs[0]; // tslint:disable-line:no-non-null-assertion
const actualPoolId = (log as any).args.poolId;
expect(actualPoolId).to.equal(expectedPoolId);

// Adds the new pool to local state
pools[actualPoolId] = {
operator: txData.from as string,
operatorShare,
delegatedStake: new StoredBalance(),
};
return new FunctionAssertion<[number, boolean], string, string>(
stakingWrapper.createStakingPool.bind(stakingWrapper),
{
// Returns the expected ID of th created pool
before: async () => {
const lastPoolId = await stakingWrapper.lastPoolId().callAsync();
// Effectively the last poolId + 1, but as a bytestring
return `0x${new BigNumber(lastPoolId)
.plus(1)
.toString(16)
.padStart(64, '0')}`;
},
after: async (
expectedPoolId: string,
result: FunctionResult,
args: [number, boolean],
txData: Partial<TxData>,
) => {
const [operatorShare, shouldAddMakerAsOperator] = args;

logUtils.log(`createStakingPool(${operatorShare}, ${shouldAddMakerAsOperator}) => ${expectedPoolId}`);

// Checks the logs for the new poolId, verifies that it is as expected
const log = result.receipt!.logs[0];
const actualPoolId = (log as any).args.poolId;
expect(actualPoolId).to.equal(expectedPoolId);

// Adds the new pool to local state
pools[actualPoolId] = {
operator: txData.from!,
operatorShare,
delegatedStake: new StoredBalance(),
};
},
},
});
);
}
/* tslint:enable:no-non-null-assertion*/
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { StakingPoolById } from '@0x/contracts-staking';
import { expect } from '@0x/contracts-test-utils';
import { logUtils } from '@0x/utils';
import { TxData } from 'ethereum-types';

import { DeploymentManager } from '../deployment_manager';

Expand All @@ -13,18 +14,24 @@ import { FunctionAssertion, FunctionResult } from './function_assertion';
export function validDecreaseStakingPoolOperatorShareAssertion(
deployment: DeploymentManager,
pools: StakingPoolById,
): FunctionAssertion<{}, void> {
): FunctionAssertion<[string, number], {}, void> {
const { stakingWrapper } = deployment.staking;

return new FunctionAssertion<{}, void>(stakingWrapper.decreaseStakingPoolOperatorShare, {
after: async (_beforeInfo, _result: FunctionResult, poolId: string, expectedOperatorShare: number) => {
logUtils.log(`decreaseStakingPoolOperatorShare(${poolId}, ${expectedOperatorShare})`);
return new FunctionAssertion<[string, number], {}, void>(
stakingWrapper.decreaseStakingPoolOperatorShare.bind(stakingWrapper),
{
after: async (_beforeInfo, _result: FunctionResult, args: [string, number], txData: Partial<TxData>) => {
const [poolId, expectedOperatorShare] = args;

// Checks that the on-chain pool's operator share has been updated.
const { operatorShare } = await stakingWrapper.getStakingPool(poolId).callAsync();
expect(operatorShare).to.bignumber.equal(expectedOperatorShare);
// Updates the pool in local state.
pools[poolId].operatorShare = operatorShare;
logUtils.log(`decreaseStakingPoolOperatorShare(${poolId}, ${expectedOperatorShare})`);

// Checks that the on-chain pool's operator share has been updated.
const { operatorShare } = await stakingWrapper.getStakingPool(poolId).callAsync();
expect(operatorShare).to.bignumber.equal(expectedOperatorShare);

// Updates the pool in local state.
pools[poolId].operatorShare = operatorShare;
},
},
});
);
}
99 changes: 99 additions & 0 deletions contracts/integrations/test/framework/assertions/fillOrder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { ERC20TokenEvents, ERC20TokenTransferEventArgs } from '@0x/contracts-erc20';
import { ExchangeEvents, ExchangeFillEventArgs } from '@0x/contracts-exchange';
import { constants, expect, orderHashUtils, verifyEvents } from '@0x/contracts-test-utils';
import { FillResults, Order } from '@0x/types';
import { BigNumber, logUtils } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
import * as _ from 'lodash';

import { DeploymentManager } from '../deployment_manager';

import { FunctionAssertion, FunctionResult } from './function_assertion';

function verifyFillEvents(
takerAddress: string,
order: Order,
receipt: TransactionReceiptWithDecodedLogs,
deployment: DeploymentManager,
): void {
// Ensure that the fill event was correct.
verifyEvents<ExchangeFillEventArgs>(
receipt,
[
{
makerAddress: order.makerAddress,
feeRecipientAddress: order.feeRecipientAddress,
makerAssetData: order.makerAssetData,
takerAssetData: order.takerAssetData,
makerFeeAssetData: order.makerFeeAssetData,
takerFeeAssetData: order.takerFeeAssetData,
orderHash: orderHashUtils.getOrderHashHex(order),
takerAddress,
senderAddress: takerAddress,
makerAssetFilledAmount: order.makerAssetAmount,
takerAssetFilledAmount: order.takerAssetAmount,
makerFeePaid: constants.ZERO_AMOUNT,
takerFeePaid: constants.ZERO_AMOUNT,
protocolFeePaid: DeploymentManager.protocolFee,
},
],
ExchangeEvents.Fill,
);

// Ensure that the transfer events were correctly emitted.
verifyEvents<ERC20TokenTransferEventArgs>(
receipt,
[
{
_from: takerAddress,
_to: order.makerAddress,
_value: order.takerAssetAmount,
},
{
_from: order.makerAddress,
_to: takerAddress,
_value: order.makerAssetAmount,
},
{
_from: takerAddress,
_to: deployment.staking.stakingProxy.address,
_value: DeploymentManager.protocolFee,
},
],
ERC20TokenEvents.Transfer,
);
}

/**
* A function assertion that verifies that a complete and valid fill succeeded and emitted the correct logs.
*/
/* tslint:disable:no-unnecessary-type-assertion */
/* tslint:disable:no-non-null-assertion */
export function validFillOrderCompleteFillAssertion(
deployment: DeploymentManager,
): FunctionAssertion<[Order, BigNumber, string], {}, FillResults> {
const exchange = deployment.exchange;

return new FunctionAssertion<[Order, BigNumber, string], {}, FillResults>(exchange.fillOrder.bind(exchange), {
after: async (
_beforeInfo,
result: FunctionResult,
args: [Order, BigNumber, string],
txData: Partial<TxData>,
) => {
const [order] = args;

// Ensure that the tx succeeded.
expect(result.success).to.be.true();

// Ensure that the correct events were emitted.
verifyFillEvents(txData.from!, order, result.receipt!, deployment);

logUtils.log(`Order filled by ${txData.from}`);

// TODO: Add validation for on-chain state (like balances)
},
});
}
/* tslint:enable:no-non-null-assertion */
/* tslint:enable:no-unnecessary-type-assertion */
Loading

0 comments on commit b86d190

Please sign in to comment.