Merge pull request #2372 from 0xProject/feature/fuzz/prng

`@0x/contracts-integrations`: Seeded RNG and simulation logging
This commit is contained in:
mzhu25 2019-12-11 10:23:08 -08:00 committed by GitHub
commit 1283232144
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 335 additions and 227 deletions

View File

@ -19,6 +19,7 @@
"test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html",
"test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha",
"run_mocha": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/**/*.js' --timeout 100000 --bail --exit",
"test:fuzz": "mocha --require source-map-support/register --require make-promises-safe 'lib/test/fuzz_tests/*.js' --timeout 0 --bail --exit",
"compile": "sol-compiler",
"watch": "sol-compiler -w",
"clean": "shx rm -rf lib test/generated-artifacts test/generated-wrappers generated-artifacts generated-wrappers",
@ -69,6 +70,7 @@
"@types/lodash": "4.14.104",
"@types/mocha": "^5.2.7",
"@types/node": "*",
"@types/seedrandom": "^2.4.28",
"chai": "^4.0.1",
"chai-as-promised": "^7.1.0",
"chai-bignumber": "^3.0.0",
@ -78,6 +80,7 @@
"mocha": "^6.2.0",
"nock": "^10.0.6",
"npm-run-all": "^4.1.2",
"seedrandom": "^3.0.5",
"shx": "^0.2.2",
"solhint": "^1.4.1",
"truffle": "^5.0.32",

View File

@ -33,7 +33,7 @@ import { AssetProxyId } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
import { AssetProxyDispatcher, Authorizable, Ownable } from './framework/wrapper_interfaces';
import { AssetProxyDispatcher, Authorizable, Ownable } from './framework/utils/wrapper_interfaces';
// tslint:disable:no-unnecessary-type-assertion
blockchainTests('Deployment and Configuration End to End Tests', env => {

View File

@ -1,10 +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 { Pseudorandom } from '../utils/pseudorandom';
import { Actor, ActorConfig, Constructor } from './base';
@ -88,7 +88,7 @@ export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
const { stakingPools } = this.actor.simulationEnvironment!;
const assertion = validJoinStakingPoolAssertion(this.actor.deployment);
while (true) {
const poolId = _.sample(Object.keys(stakingPools));
const poolId = Pseudorandom.sample(Object.keys(stakingPools));
if (poolId === undefined) {
yield undefined;
} else {

View File

@ -1,5 +1,4 @@
import { constants, StakingPoolById } from '@0x/contracts-staking';
import { getRandomInteger } from '@0x/contracts-test-utils';
import '@azure/core-asynciterator-polyfill';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
@ -7,6 +6,7 @@ import * as _ from 'lodash';
import { validCreateStakingPoolAssertion } from '../assertions/createStakingPool';
import { validDecreaseStakingPoolOperatorShareAssertion } from '../assertions/decreaseStakingPoolOperatorShare';
import { AssertionResult } from '../assertions/function_assertion';
import { Pseudorandom } from '../utils/pseudorandom';
import { Actor, Constructor } from './base';
@ -83,7 +83,7 @@ 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).toNumber();
const operatorShare = Pseudorandom.integer(constants.PPM).toNumber();
yield assertion.executeAsync([operatorShare, false], { from: this.actor.address });
}
}
@ -92,11 +92,11 @@ export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase
const { stakingPools } = this.actor.simulationEnvironment!;
const assertion = validDecreaseStakingPoolOperatorShareAssertion(this.actor.deployment, stakingPools);
while (true) {
const poolId = _.sample(this._getOperatorPoolIds(stakingPools));
const poolId = Pseudorandom.sample(this._getOperatorPoolIds(stakingPools));
if (poolId === undefined) {
yield undefined;
} else {
const operatorShare = getRandomInteger(0, stakingPools[poolId].operatorShare).toNumber();
const operatorShare = Pseudorandom.integer(stakingPools[poolId].operatorShare).toNumber();
yield assertion.executeAsync([poolId, operatorShare], { from: this.actor.address });
}
}

View File

@ -1,5 +1,4 @@
import { OwnerStakeByStatus, StakeInfo, StakeStatus, StoredBalance } from '@0x/contracts-staking';
import { getRandomInteger } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import '@azure/core-asynciterator-polyfill';
import * as _ from 'lodash';
@ -8,6 +7,7 @@ import { AssertionResult } from '../assertions/function_assertion';
import { validMoveStakeAssertion } from '../assertions/moveStake';
import { validStakeAssertion } from '../assertions/stake';
import { validUnstakeAssertion } from '../assertions/unstake';
import { Pseudorandom } from '../utils/pseudorandom';
import { Actor, Constructor } from './base';
@ -75,7 +75,7 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
while (true) {
await balanceStore.updateErc20BalancesAsync();
const zrxBalance = balanceStore.balances.erc20[this.actor.address][zrx.address];
const amount = getRandomInteger(0, zrxBalance);
const amount = Pseudorandom.integer(zrxBalance);
yield assertion.executeAsync([amount], { from: this.actor.address });
}
}
@ -94,7 +94,7 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
undelegatedStake.currentEpochBalance,
undelegatedStake.nextEpochBalance,
);
const amount = getRandomInteger(0, withdrawableStake);
const amount = Pseudorandom.integer(withdrawableStake);
yield assertion.executeAsync([amount], { from: this.actor.address });
}
}
@ -104,25 +104,27 @@ export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Con
const assertion = validMoveStakeAssertion(deployment, globalStake, this.stake, stakingPools);
while (true) {
const fromPoolId = _.sample(Object.keys(_.omit(this.stake[StakeStatus.Delegated], ['total'])));
const fromPoolId = Pseudorandom.sample(
Object.keys(_.omit(this.stake[StakeStatus.Delegated], ['total'])),
);
const fromStatus =
fromPoolId === undefined
? StakeStatus.Undelegated
: (_.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus);
: (Pseudorandom.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus);
const from = new StakeInfo(fromStatus, fromPoolId);
const toPoolId = _.sample(Object.keys(stakingPools));
const toPoolId = Pseudorandom.sample(Object.keys(stakingPools));
const toStatus =
toPoolId === undefined
? StakeStatus.Undelegated
: (_.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus);
: (Pseudorandom.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus);
const to = new StakeInfo(toStatus, toPoolId);
const moveableStake =
from.status === StakeStatus.Undelegated
? this.stake[StakeStatus.Undelegated].nextEpochBalance
: this.stake[StakeStatus.Delegated][from.poolId].nextEpochBalance;
const amount = getRandomInteger(0, moveableStake);
const amount = Pseudorandom.integer(moveableStake);
yield assertion.executeAsync([from, to, amount], { from: this.actor.address });
}

View File

@ -1,12 +1,12 @@
import { constants, getRandomInteger } from '@0x/contracts-test-utils';
import { constants } 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 { Pseudorandom } from '../utils/pseudorandom';
import { Actor, Constructor } from './base';
@ -65,7 +65,7 @@ export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
const { marketMakers } = this.actor.simulationEnvironment!;
const assertion = validFillOrderCompleteFillAssertion(this.actor.deployment);
while (true) {
const maker = _.sample(marketMakers);
const maker = Pseudorandom.sample(marketMakers);
if (maker === undefined) {
yield undefined;
} else {
@ -82,8 +82,8 @@ export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
]);
const order = await maker.signOrderAsync({
makerAssetAmount: getRandomInteger(constants.ZERO_AMOUNT, constants.INITIAL_ERC20_BALANCE),
takerAssetAmount: getRandomInteger(constants.ZERO_AMOUNT, constants.INITIAL_ERC20_BALANCE),
makerAssetAmount: Pseudorandom.integer(constants.INITIAL_ERC20_BALANCE),
takerAssetAmount: Pseudorandom.integer(constants.INITIAL_ERC20_BALANCE),
});
yield assertion.executeAsync([order, order.takerAssetAmount, order.signature], {
from: this.actor.address,

View File

@ -1,6 +1,6 @@
import { StakingPoolById, StoredBalance } from '@0x/contracts-staking';
import { expect } from '@0x/contracts-test-utils';
import { BigNumber, logUtils } from '@0x/utils';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
import { DeploymentManager } from '../deployment_manager';
@ -20,9 +20,7 @@ export function validCreateStakingPoolAssertion(
): FunctionAssertion<[number, boolean], string, string> {
const { stakingWrapper } = deployment.staking;
return new FunctionAssertion<[number, boolean], string, string>(
stakingWrapper.createStakingPool.bind(stakingWrapper),
{
return new FunctionAssertion<[number, boolean], string, string>(stakingWrapper, 'createStakingPool', {
// Returns the expected ID of th created pool
before: async () => {
const lastPoolId = await stakingWrapper.lastPoolId().callAsync();
@ -38,9 +36,7 @@ export function validCreateStakingPoolAssertion(
args: [number, boolean],
txData: Partial<TxData>,
) => {
const [operatorShare, shouldAddMakerAsOperator] = args;
logUtils.log(`createStakingPool(${operatorShare}, ${shouldAddMakerAsOperator}) => ${expectedPoolId}`);
const [operatorShare] = args;
// Checks the logs for the new poolId, verifies that it is as expected
const log = result.receipt!.logs[0];
@ -54,7 +50,6 @@ export function validCreateStakingPoolAssertion(
delegatedStake: new StoredBalance(),
};
},
},
);
});
}
/* tslint:enable:no-non-null-assertion*/

View File

@ -1,6 +1,5 @@
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';
@ -17,14 +16,10 @@ export function validDecreaseStakingPoolOperatorShareAssertion(
): FunctionAssertion<[string, number], {}, void> {
const { stakingWrapper } = deployment.staking;
return new FunctionAssertion<[string, number], {}, void>(
stakingWrapper.decreaseStakingPoolOperatorShare.bind(stakingWrapper),
{
after: async (_beforeInfo, _result: FunctionResult, args: [string, number], txData: Partial<TxData>) => {
return new FunctionAssertion<[string, number], {}, void>(stakingWrapper, 'decreaseStakingPoolOperatorShare', {
after: async (_beforeInfo, _result: FunctionResult, args: [string, number], _txData: Partial<TxData>) => {
const [poolId, expectedOperatorShare] = args;
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);
@ -32,6 +27,5 @@ export function validDecreaseStakingPoolOperatorShareAssertion(
// Updates the pool in local state.
pools[poolId].operatorShare = operatorShare;
},
},
);
});
}

View File

@ -2,7 +2,7 @@ import { ERC20TokenEvents, ERC20TokenTransferEventArgs } from '@0x/contracts-erc
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 { BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
import * as _ from 'lodash';
@ -74,7 +74,7 @@ export function validFillOrderCompleteFillAssertion(
): FunctionAssertion<[Order, BigNumber, string], {}, FillResults> {
const exchange = deployment.exchange;
return new FunctionAssertion<[Order, BigNumber, string], {}, FillResults>(exchange.fillOrder.bind(exchange), {
return new FunctionAssertion<[Order, BigNumber, string], {}, FillResults>(exchange, 'fillOrder', {
after: async (
_beforeInfo,
result: FunctionResult,
@ -89,8 +89,6 @@ export function validFillOrderCompleteFillAssertion(
// 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)
},
});

View File

@ -1,7 +1,9 @@
import { ContractFunctionObj, ContractTxFunctionObj } from '@0x/base-contract';
import { BaseContract, ContractFunctionObj, ContractTxFunctionObj } from '@0x/base-contract';
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
import * as _ from 'lodash';
import { logger } from '../utils/logger';
// tslint:disable:max-classes-per-file
export type GenericContractFunction<T> = (...args: any[]) => ContractFunctionObj<T>;
@ -48,29 +50,22 @@ export interface AssertionResult<TBefore = unknown> {
*/
export class FunctionAssertion<TArgs extends any[], TBefore, ReturnDataType> implements Assertion<TArgs> {
// A condition that will be applied to `wrapperFunction`.
public condition: Condition<TArgs, TBefore>;
// The wrapper function that will be wrapped in assertions.
public wrapperFunction: (
...args: TArgs // tslint:disable-line:trailing-comma
) => ContractTxFunctionObj<ReturnDataType> | ContractFunctionObj<ReturnDataType>;
public readonly condition: Condition<TArgs, TBefore>;
constructor(
wrapperFunction: (
...args: TArgs // tslint:disable-line:trailing-comma
) => ContractTxFunctionObj<ReturnDataType> | ContractFunctionObj<ReturnDataType>,
private readonly _contractWrapper: BaseContract,
private readonly _functionName: string,
condition: Partial<Condition<TArgs, TBefore>> = {},
) {
this.condition = {
before: async (args: TArgs, txData: Partial<TxData>) => {
before: async (_args: TArgs, _txData: Partial<TxData>) => {
return ({} as any) as TBefore;
},
after: async (beforeInfo: TBefore, result: FunctionResult, args: TArgs, txData: Partial<TxData>) => {
after: async (_beforeInfo: TBefore, _result: FunctionResult, _args: TArgs, _txData: Partial<TxData>) => {
return ({} as any) as TBefore;
},
...condition,
};
this.wrapperFunction = wrapperFunction;
}
/**
@ -87,7 +82,10 @@ export class FunctionAssertion<TArgs extends any[], TBefore, ReturnDataType> imp
// Try to make the call to the function. If it is successful, pass the
// result and receipt to the after condition.
try {
const functionWithArgs = this.wrapperFunction(...args) as ContractTxFunctionObj<ReturnDataType>;
const functionWithArgs = (this._contractWrapper as any)[this._functionName](
...args,
) as ContractTxFunctionObj<ReturnDataType>;
logger.logFunctionAssertion(this._functionName, args, txData);
callResult.data = await functionWithArgs.callAsync(txData);
callResult.receipt =
functionWithArgs.awaitTransactionSuccessAsync !== undefined

View File

@ -1,6 +1,5 @@
import { StakingEvents, StakingMakerStakingPoolSetEventArgs } from '@0x/contracts-staking';
import { expect, filterLogsToArguments } from '@0x/contracts-test-utils';
import { logUtils } from '@0x/utils';
import { TxData } from 'ethereum-types';
import { DeploymentManager } from '../deployment_manager';
@ -15,7 +14,7 @@ import { FunctionAssertion, FunctionResult } from './function_assertion';
export function validJoinStakingPoolAssertion(deployment: DeploymentManager): FunctionAssertion<[string], {}, void> {
const { stakingWrapper } = deployment.staking;
return new FunctionAssertion<[string], {}, void>(stakingWrapper.joinStakingPoolAsMaker.bind(stakingWrapper), {
return new FunctionAssertion<[string], {}, void>(stakingWrapper, 'joinStakingPoolAsMaker', {
after: async (_beforeInfo, _result: FunctionResult, args: [string], txData: Partial<TxData>) => {
const [poolId] = args;
@ -34,8 +33,6 @@ export function validJoinStakingPoolAssertion(deployment: DeploymentManager): Fu
]);
const joinedPoolId = await deployment.staking.stakingWrapper.poolIdByMaker(txData.from!).callAsync();
expect(joinedPoolId).to.be.eq(poolId);
logUtils.log(`Pool ${poolId} joined by ${txData.from}`);
},
});
}

View File

@ -7,7 +7,7 @@ import {
StoredBalance,
} from '@0x/contracts-staking';
import { constants, expect } from '@0x/contracts-test-utils';
import { BigNumber, logUtils } from '@0x/utils';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
import * as _ from 'lodash';
@ -86,9 +86,7 @@ export function validMoveStakeAssertion(
): FunctionAssertion<[StakeInfo, StakeInfo, BigNumber], {}, void> {
const { stakingWrapper } = deployment.staking;
return new FunctionAssertion<[StakeInfo, StakeInfo, BigNumber], {}, void>(
stakingWrapper.moveStake.bind(stakingWrapper),
{
return new FunctionAssertion<[StakeInfo, StakeInfo, BigNumber], {}, void>(stakingWrapper, 'moveStake', {
after: async (
_beforeInfo: {},
_result: FunctionResult,
@ -97,12 +95,6 @@ export function validMoveStakeAssertion(
) => {
const [from, to, amount] = args;
logUtils.log(
`moveStake({status: ${StakeStatus[from.status]}, poolId: ${from.poolId} }, { status: ${
StakeStatus[to.status]
}, poolId: ${to.poolId} }, ${amount})`,
);
const owner = txData.from!; // tslint:disable-line:no-non-null-assertion
// Update local balances to match the expected result of this `moveStake` operation
@ -124,9 +116,7 @@ export function validMoveStakeAssertion(
const globalUndelegatedStake = await stakingWrapper
.getGlobalStakeByStatus(StakeStatus.Undelegated)
.callAsync();
const globalDelegatedStake = await stakingWrapper
.getGlobalStakeByStatus(StakeStatus.Delegated)
.callAsync();
const globalDelegatedStake = await stakingWrapper.getGlobalStakeByStatus(StakeStatus.Delegated).callAsync();
expect(globalUndelegatedStake).to.deep.equal(globalStake[StakeStatus.Undelegated]);
expect(globalDelegatedStake).to.deep.equal(globalStake[StakeStatus.Delegated]);
@ -140,7 +130,6 @@ export function validMoveStakeAssertion(
expect(totalStakeDelegated).to.deep.equal(pools[poolId].delegatedStake);
}
},
},
);
});
}
/* tslint:enable:no-unnecessary-type-assertion */

View File

@ -1,6 +1,6 @@
import { GlobalStakeByStatus, OwnerStakeByStatus, StakeStatus, StoredBalance } from '@0x/contracts-staking';
import { expect } from '@0x/contracts-test-utils';
import { BigNumber, logUtils } from '@0x/utils';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
import { BlockchainBalanceStore } from '../balances/blockchain_balance_store';
@ -34,7 +34,7 @@ export function validStakeAssertion(
): FunctionAssertion<[BigNumber], LocalBalanceStore, void> {
const { stakingWrapper, zrxVault } = deployment.staking;
return new FunctionAssertion(stakingWrapper.stake.bind(stakingWrapper), {
return new FunctionAssertion(stakingWrapper, 'stake', {
before: async (args: [BigNumber], txData: Partial<TxData>) => {
const [amount] = args;
@ -56,8 +56,6 @@ export function validStakeAssertion(
) => {
const [amount] = args;
logUtils.log(`stake(${amount})`);
// Checks that the ZRX transfer updated balances as expected.
await balanceStore.updateErc20BalancesAsync();
balanceStore.assertEquals(expectedBalances);

View File

@ -1,6 +1,6 @@
import { GlobalStakeByStatus, OwnerStakeByStatus, StakeStatus, StoredBalance } from '@0x/contracts-staking';
import { expect } from '@0x/contracts-test-utils';
import { BigNumber, logUtils } from '@0x/utils';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
import { BlockchainBalanceStore } from '../balances/blockchain_balance_store';
@ -35,7 +35,7 @@ export function validUnstakeAssertion(
): FunctionAssertion<[BigNumber], LocalBalanceStore, void> {
const { stakingWrapper, zrxVault } = deployment.staking;
return new FunctionAssertion(stakingWrapper.unstake.bind(stakingWrapper), {
return new FunctionAssertion(stakingWrapper, 'unstake', {
before: async (args: [BigNumber], txData: Partial<TxData>) => {
const [amount] = args;
@ -57,8 +57,6 @@ export function validUnstakeAssertion(
) => {
const [amount] = args;
logUtils.log(`unstake(${amount})`);
// Checks that the ZRX transfer updated balances as expected.
await balanceStore.updateErc20BalancesAsync();
balanceStore.assertEquals(expectedBalances);

View File

@ -1,5 +1,5 @@
import { BaseContract } from '@0x/base-contract';
import { constants, expect, TokenBalances } from '@0x/contracts-test-utils';
import { constants, expect, replaceKeysDeep, TokenBalances } from '@0x/contracts-test-utils';
import * as _ from 'lodash';
import { TokenAddresses, TokenContractsByName, TokenOwnersByName } from './types';
@ -68,6 +68,14 @@ export class BalanceStore {
this._addressNames = _.cloneDeep(balanceStore._addressNames);
}
/**
* Returns a version of balances where keys are replaced with their readable counterparts, if
* they exist.
*/
public toReadable(): _.Dictionary<{}> {
return replaceKeysDeep(this.balances, this._readableAddressName.bind(this));
}
/**
* Returns the human-readable name for the given address, if it exists.
* @param address The address to get the name for.

View File

@ -25,7 +25,7 @@ import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
import * as _ from 'lodash';
import { AssetProxyDispatcher, Authorizable, Ownable } from './wrapper_interfaces';
import { AssetProxyDispatcher, Authorizable, Ownable } from './utils/wrapper_interfaces';
/**
* Adds a batch of authorities to a list of authorizable contracts.

View File

@ -1,10 +1,10 @@
import { GlobalStakeByStatus, StakeStatus, StakingPoolById, StoredBalance } from '@0x/contracts-staking';
import * as _ from 'lodash';
import { Maker } from './actors/maker';
import { AssertionResult } from './assertions/function_assertion';
import { BlockchainBalanceStore } from './balances/blockchain_balance_store';
import { DeploymentManager } from './deployment_manager';
import { logger } from './utils/logger';
// tslint:disable:max-classes-per-file
@ -20,6 +20,14 @@ export class SimulationEnvironment {
public balanceStore: BlockchainBalanceStore,
public marketMakers: Maker[] = [],
) {}
public state(): any {
return {
globalStake: this.globalStake,
stakingPools: this.stakingPools,
balanceStore: this.balanceStore.toReadable(),
};
}
}
export abstract class Simulation {
@ -30,14 +38,23 @@ export abstract class Simulation {
public async fuzzAsync(steps?: number): Promise<void> {
if (steps !== undefined) {
for (let i = 0; i < steps; i++) {
await this.generator.next();
await this._stepAsync();
}
} else {
while (true) {
await this.generator.next();
await this._stepAsync();
}
}
}
protected abstract _assertionGenerator(): AsyncIterableIterator<AssertionResult | void>;
private async _stepAsync(): Promise<void> {
try {
await this.generator.next();
} catch (error) {
logger.logFailure(error, this.environment.state());
throw error;
}
}
}

View File

@ -2,7 +2,7 @@ import { constants as stakingConstants } from '@0x/contracts-staking';
import { blockchainTests, expect } from '@0x/contracts-test-utils';
import { DeploymentManager } from '../deployment_manager';
import { Authorizable, Ownable } from '../wrapper_interfaces';
import { Authorizable, Ownable } from '../utils/wrapper_interfaces';
blockchainTests('Deployment Manager', env => {
let owner: string;

View File

@ -23,14 +23,11 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
describe('executeAsync', () => {
it('should call the before function with the provided arguments', async () => {
let sideEffectTarget = ZERO_AMOUNT;
const assertion = new FunctionAssertion<[BigNumber], void, BigNumber>(
exampleContract.returnInteger.bind(exampleContract),
{
const assertion = new FunctionAssertion<[BigNumber], void, BigNumber>(exampleContract, 'returnInteger', {
before: async (args: [BigNumber], txData: Partial<TxData>) => {
sideEffectTarget = randomInput;
},
},
);
});
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
await assertion.executeAsync([randomInput], {});
expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
@ -38,9 +35,7 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
it('should call the after function with the provided arguments', async () => {
let sideEffectTarget = ZERO_AMOUNT;
const assertion = new FunctionAssertion<[BigNumber], void, BigNumber>(
exampleContract.returnInteger.bind(exampleContract),
{
const assertion = new FunctionAssertion<[BigNumber], void, BigNumber>(exampleContract, 'returnInteger', {
after: async (
_beforeInfo: any,
_result: FunctionResult,
@ -49,15 +44,14 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
) => {
[sideEffectTarget] = args;
},
},
);
});
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
await assertion.executeAsync([randomInput], {});
expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
});
it('should not fail immediately if the wrapped function fails', async () => {
const assertion = new FunctionAssertion<[], {}, void>(exampleContract.emptyRevert.bind(exampleContract));
const assertion = new FunctionAssertion<[], {}, void>(exampleContract, 'emptyRevert');
await assertion.executeAsync([], {});
});
@ -65,7 +59,8 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
let sideEffectTarget = ZERO_AMOUNT;
const assertion = new FunctionAssertion<[BigNumber], BigNumber, BigNumber>(
exampleContract.returnInteger.bind(exampleContract),
exampleContract,
'returnInteger',
{
before: async (_args: [BigNumber], _txData: Partial<TxData>) => {
return randomInput;
@ -86,9 +81,7 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
it('should pass the result from the function call to "after"', async () => {
let sideEffectTarget = ZERO_AMOUNT;
const assertion = new FunctionAssertion<[BigNumber], void, BigNumber>(
exampleContract.returnInteger.bind(exampleContract),
{
const assertion = new FunctionAssertion<[BigNumber], void, BigNumber>(exampleContract, 'returnInteger', {
after: async (
_beforeInfo: any,
result: FunctionResult,
@ -97,8 +90,7 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
) => {
sideEffectTarget = result.data;
},
},
);
});
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
await assertion.executeAsync([randomInput], {});
expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
@ -106,21 +98,13 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
it('should pass the receipt from the function call to "after"', async () => {
let sideEffectTarget: TransactionReceiptWithDecodedLogs;
const assertion = new FunctionAssertion<[string], void, void>(
exampleContract.emitEvent.bind(exampleContract),
{
after: async (
_beforeInfo: any,
result: FunctionResult,
_args: [string],
_txData: Partial<TxData>,
) => {
const assertion = new FunctionAssertion<[string], void, void>(exampleContract, 'emitEvent', {
after: async (_beforeInfo: any, result: FunctionResult, _args: [string], _txData: Partial<TxData>) => {
if (result.receipt) {
sideEffectTarget = result.receipt;
}
},
},
);
});
const input = 'emitted data';
await assertion.executeAsync([input], {});
@ -135,19 +119,11 @@ blockchainTests.resets('FunctionAssertion Unit Tests', env => {
it('should pass the error to "after" if the function call fails', async () => {
let sideEffectTarget: Error;
const assertion = new FunctionAssertion<[string], void, void>(
exampleContract.stringRevert.bind(exampleContract),
{
after: async (
_beforeInfo: any,
result: FunctionResult,
_args: [string],
_txData: Partial<TxData>,
) => {
const assertion = new FunctionAssertion<[string], void, void>(exampleContract, 'stringRevert', {
after: async (_beforeInfo: any, result: FunctionResult, _args: [string], _txData: Partial<TxData>) => {
sideEffectTarget = result.data;
},
},
);
});
const message = 'error message';
await assertion.executeAsync([message], {});

View File

@ -0,0 +1,55 @@
import { TxData } from 'ethereum-types';
import { Pseudorandom } from '../utils/pseudorandom';
// tslint:disable:no-console
class Logger {
private _step = 0;
constructor() {
console.warn(
JSON.stringify({
level: 'info',
time: new Date(),
msg: `Pseudorandom seed: ${Pseudorandom.seed}`,
}),
);
}
/*
* Logs the name of the function executed, the arguments and transaction data it was
* called with, and the current step of the simulation.
*/
public logFunctionAssertion(functionName: string, functionArgs: any[], txData: Partial<TxData>): void {
console.warn(
JSON.stringify({
level: 'info',
time: new Date(),
msg: `Function called: ${functionName}(${functionArgs
.map(arg => JSON.stringify(arg).replace(/"/g, "'"))
.join(', ')})`,
step: this._step++,
txData,
}),
);
}
/*
* Logs information about a assertion failure. Dumps the error thrown and arbitrary data from
* the calling context.
*/
public logFailure(error: Error, data: string): void {
console.warn(
JSON.stringify({
level: 'error',
time: new Date(),
step: this._step,
error,
data,
}),
);
}
}
export const logger = new Logger();

View File

@ -0,0 +1,39 @@
import { Numberish } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import * as seedrandom from 'seedrandom';
class PRNGWrapper {
public readonly seed = process.env.UUID || Math.random().toString();
private readonly _rng = seedrandom(this.seed);
/*
* Pseudorandom version of _.sample. Picks an element of the given array with uniform probability.
* Return undefined if the array is empty.
*/
public sample<T>(arr: T[]): T | undefined {
if (arr.length === 0) {
return undefined;
}
const index = Math.abs(this._rng.int32()) % arr.length;
return arr[index];
}
// tslint:disable:unified-signatures
/*
* Pseudorandom version of getRandomPortion/getRandomInteger. If two arguments are provided,
* treats those arguments as the min and max (inclusive) of the desired range. If only one
* argument is provided, picks an integer between 0 and the argument.
*/
public integer(max: Numberish): BigNumber;
public integer(min: Numberish, max: Numberish): BigNumber;
public integer(a: Numberish, b?: Numberish): BigNumber {
if (b === undefined) {
return new BigNumber(this._rng()).times(a).integerValue(BigNumber.ROUND_HALF_UP);
} else {
const range = new BigNumber(b).minus(a);
return this.integer(range).plus(a);
}
}
}
export const Pseudorandom = new PRNGWrapper();

View File

@ -1,5 +1,4 @@
import { blockchainTests } from '@0x/contracts-test-utils';
import * as _ from 'lodash';
import { Actor } from '../framework/actors/base';
import { PoolOperator } from '../framework/actors/pool_operator';
@ -7,6 +6,7 @@ import { AssertionResult } from '../framework/assertions/function_assertion';
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
import { DeploymentManager } from '../framework/deployment_manager';
import { Simulation, SimulationEnvironment } from '../framework/simulation';
import { Pseudorandom } from '../framework/utils/pseudorandom';
export class PoolManagementSimulation extends Simulation {
protected async *_assertionGenerator(): AsyncIterableIterator<AssertionResult | void> {
@ -22,13 +22,18 @@ export class PoolManagementSimulation extends Simulation {
operator.simulationActions.validDecreaseStakingPoolOperatorShare,
];
while (true) {
const action = _.sample(actions);
const action = Pseudorandom.sample(actions);
yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion
}
}
}
blockchainTests.skip('Pool management fuzz test', env => {
blockchainTests('Pool management fuzz test', env => {
before(function(): void {
if (process.env.FUZZ_TEST !== 'pool_management') {
this.skip();
}
});
after(async () => {
Actor.reset();
});

View File

@ -1,5 +1,4 @@
import { blockchainTests, constants } from '@0x/contracts-test-utils';
import * as _ from 'lodash';
import { MakerTaker } from '../framework/actors/hybrids';
import { Maker } from '../framework/actors/maker';
@ -7,6 +6,7 @@ import { AssertionResult } from '../framework/assertions/function_assertion';
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
import { DeploymentManager } from '../framework/deployment_manager';
import { Simulation, SimulationEnvironment } from '../framework/simulation';
import { Pseudorandom } from '../framework/utils/pseudorandom';
import { PoolManagementSimulation } from './pool_management_test';
@ -29,17 +29,21 @@ class PoolMembershipSimulation extends Simulation {
];
while (true) {
const action = _.sample(actions);
const action = Pseudorandom.sample(actions);
yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion
}
}
}
blockchainTests.skip('pool membership fuzz test', env => {
blockchainTests('pool membership fuzz test', env => {
let deployment: DeploymentManager;
let maker: Maker;
before(async () => {
before(async function(): Promise<void> {
if (process.env.FUZZ_TEST !== 'pool_membership') {
this.skip();
}
deployment = await DeploymentManager.deployAsync(env, {
numErc20TokensToDeploy: 2,
numErc721TokensToDeploy: 0,

View File

@ -1,5 +1,4 @@
import { blockchainTests } from '@0x/contracts-test-utils';
import * as _ from 'lodash';
import { Actor } from '../framework/actors/base';
import { Staker } from '../framework/actors/staker';
@ -7,6 +6,7 @@ import { AssertionResult } from '../framework/assertions/function_assertion';
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
import { DeploymentManager } from '../framework/deployment_manager';
import { Simulation, SimulationEnvironment } from '../framework/simulation';
import { Pseudorandom } from '../framework/utils/pseudorandom';
import { PoolManagementSimulation } from './pool_management_test';
@ -26,13 +26,19 @@ export class StakeManagementSimulation extends Simulation {
poolManagement.generator,
];
while (true) {
const action = _.sample(actions);
const action = Pseudorandom.sample(actions);
yield (await action!.next()).value; // tslint:disable-line:no-non-null-assertion
}
}
}
blockchainTests.skip('Stake management fuzz test', env => {
blockchainTests('Stake management fuzz test', env => {
before(function(): void {
if (process.env.FUZZ_TEST !== 'stake_management') {
this.skip();
}
});
after(async () => {
Actor.reset();
});

View File

@ -0,0 +1,6 @@
{
"extends": ["@0x/tslint-config"],
"rules": {
"no-invalid-this": false
}
}

View File

@ -50,7 +50,7 @@ export {
export { blockchainTests, BlockchainTestsEnvironment, describe } from './mocha_blockchain';
export { chaiSetup, expect } from './chai_setup';
export { getCodesizeFromArtifact } from './codesize';
export { shortZip } from './lang_utils';
export { replaceKeysDeep, shortZip } from './lang_utils';
export {
assertIntegerRoughlyEquals,
assertRoughlyEquals,

View File

@ -7,3 +7,13 @@ export function shortZip<T1, T2>(a: T1[], b: T2[]): Array<[T1, T2]> {
const minLength = Math.min(a.length, b.length);
return _.zip(a.slice(0, minLength), b.slice(0, minLength)) as Array<[T1, T2]>;
}
/**
* Replaces the keys in a deeply nested object. Adapted from https://stackoverflow.com/a/39126851
*/
export function replaceKeysDeep(obj: {}, mapKeys: (key: string) => string | void): _.Dictionary<{}> {
return _.transform(obj, (result, value, key) => {
const currentKey = mapKeys(key) || key;
result[currentKey] = _.isObject(value) ? replaceKeysDeep(value, mapKeys) : value;
});
}

View File

@ -2347,6 +2347,11 @@
"@types/glob" "*"
"@types/node" "*"
"@types/seedrandom@^2.4.28":
version "2.4.28"
resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f"
integrity sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA==
"@types/semver@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45"
@ -14672,6 +14677,11 @@ seedrandom@2.4.4:
version "2.4.4"
resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-2.4.4.tgz#b25ea98632c73e45f58b77cfaa931678df01f9ba"
seedrandom@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7"
integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==
seek-bzip@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.5.tgz#cfe917cb3d274bcffac792758af53173eb1fabdc"