update pool membership simulation to use multiple makers and takers, partial fills

This commit is contained in:
Michael Zhu
2019-11-28 12:25:58 -08:00
parent 4b7434d1e8
commit fff3c1eb36
16 changed files with 225 additions and 107 deletions

View File

@@ -47,7 +47,10 @@ export class Actor {
this.name = config.name || this.address;
this.deployment = config.deployment;
this.privateKey = constants.TESTRPC_PRIVATE_KEYS[config.deployment.accounts.indexOf(this.address)];
this.simulationEnvironment = config.simulationEnvironment;
if (config.simulationEnvironment !== undefined) {
this.simulationEnvironment = config.simulationEnvironment;
this.simulationEnvironment.actors.push(this);
}
this._transactionFactory = new TransactionFactory(
this.privateKey,
config.deployment.exchange.address,
@@ -123,7 +126,6 @@ export class Actor {
if (logs.length !== 1) {
throw new Error('Invalid number of `TransferSingle` logs');
}
const { id } = logs[0];
// Mint the token

View File

@@ -18,7 +18,7 @@ export interface FeeRecipientInterface {
}
/**
* This mixin encapsulates functionaltiy associated with fee recipients within the 0x ecosystem.
* This mixin encapsulates functionality associated with fee recipients within the 0x ecosystem.
* As of writing, the only extra functionality provided is signing Coordinator approvals.
*/
export function FeeRecipientMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<FeeRecipientInterface> {

View File

@@ -1,4 +1,8 @@
import { IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs, TestStakingEvents } from '@0x/contracts-staking';
import {
IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs,
TestStakingContract,
TestStakingEvents,
} from '@0x/contracts-staking';
import { filterLogsToArguments, web3Wrapper } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { BlockParamLiteral, TransactionReceiptWithDecodedLogs } from 'ethereum-types';
@@ -10,8 +14,19 @@ export interface KeeperInterface {
finalizePoolsAsync: (poolIds?: string[]) => Promise<TransactionReceiptWithDecodedLogs[]>;
}
async function fastForwardToNextEpochAsync(stakingContract: TestStakingContract): Promise<void> {
// increase timestamp of next block by how many seconds we need to
// get to the next epoch.
const epochEndTime = await stakingContract.getCurrentEpochEarliestEndTimeInSeconds().callAsync();
const lastBlockTime = await web3Wrapper.getBlockTimestampAsync('latest');
const dt = Math.max(0, epochEndTime.minus(lastBlockTime).toNumber());
await web3Wrapper.increaseTimeAsync(dt);
// mine next block
await web3Wrapper.mineBlockAsync();
}
/**
* This mixin encapsulates functionaltiy associated with keepers within the 0x ecosystem.
* This mixin encapsulates functionality associated with keepers within the 0x ecosystem.
* This includes ending epochs sand finalizing pools in the staking system.
*/
export function KeeperMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<KeeperInterface> {
@@ -35,14 +50,7 @@ export function KeeperMixin<TBase extends Constructor>(Base: TBase): TBase & Con
public async endEpochAsync(shouldFastForward: boolean = true): Promise<TransactionReceiptWithDecodedLogs> {
const { stakingWrapper } = this.actor.deployment.staking;
if (shouldFastForward) {
// increase timestamp of next block by how many seconds we need to
// get to the next epoch.
const epochEndTime = await stakingWrapper.getCurrentEpochEarliestEndTimeInSeconds().callAsync();
const lastBlockTime = await web3Wrapper.getBlockTimestampAsync('latest');
const dt = Math.max(0, epochEndTime.minus(lastBlockTime).toNumber());
await web3Wrapper.increaseTimeAsync(dt);
// mine next block
await web3Wrapper.mineBlockAsync();
await fastForwardToNextEpochAsync(stakingWrapper);
}
return stakingWrapper.endEpoch().awaitTransactionSuccessAsync({ from: this.actor.address });
}

View File

@@ -21,7 +21,7 @@ export interface MakerInterface {
}
/**
* This mixin encapsulates functionaltiy associated with makers within the 0x ecosystem.
* This mixin encapsulates functionality associated with makers within the 0x ecosystem.
* This includes signing and canceling orders, as well as joining a staking pool as a maker.
*/
export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<MakerInterface> {
@@ -90,7 +90,7 @@ export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
while (true) {
const poolId = Pseudorandom.sample(Object.keys(stakingPools));
if (poolId === undefined) {
yield undefined;
yield;
} else {
yield assertion.executeAsync([poolId], { from: this.actor.address });
}

View File

@@ -19,7 +19,7 @@ export interface PoolOperatorInterface {
}
/**
* This mixin encapsulates functionaltiy associated with pool operators within the 0x ecosystem.
* This mixin encapsulates functionality associated with pool operators within the 0x ecosystem.
* This includes creating staking pools and decreasing the operator share of a pool.
*/
export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<PoolOperatorInterface> {

View File

@@ -16,7 +16,7 @@ export interface StakerInterface {
}
/**
* This mixin encapsulates functionaltiy associated with stakers within the 0x ecosystem.
* This mixin encapsulates functionality associated with stakers within the 0x ecosystem.
* This includes staking ZRX (and optionally delegating it to a specific pool).
*/
export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<StakerInterface> {

View File

@@ -1,14 +1,17 @@
import { DummyERC20TokenContract } from '@0x/contracts-erc20';
import { constants } from '@0x/contracts-test-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
import { validFillOrderCompleteFillAssertion } from '../assertions/fillOrder';
import { validFillOrderAssertion } from '../assertions/fillOrder';
import { AssertionResult } from '../assertions/function_assertion';
import { DeploymentManager } from '../deployment_manager';
import { Pseudorandom } from '../utils/pseudorandom';
import { Actor, Constructor } from './base';
import { Maker } from './maker';
import { filterActorsByRole } from './utils';
export interface TakerInterface {
fillOrderAsync: (
@@ -19,7 +22,7 @@ export interface TakerInterface {
}
/**
* This mixin encapsulates functionaltiy associated with takers within the 0x ecosystem.
* This mixin encapsulates functionality associated with takers within the 0x ecosystem.
* As of writing, the only extra functionality provided is a utility wrapper around `fillOrder`,
*/
export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<TakerInterface> {
@@ -39,7 +42,7 @@ export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
// Register this mixin's assertion generators
this.actor.simulationActions = {
...this.actor.simulationActions,
validFillOrderCompleteFill: this._validFillOrderCompleteFill(),
validFillOrder: this._validFillOrder(),
};
}
@@ -61,31 +64,66 @@ export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
});
}
private async *_validFillOrderCompleteFill(): AsyncIterableIterator<AssertionResult | void> {
const { marketMakers } = this.actor.simulationEnvironment!;
const assertion = validFillOrderCompleteFillAssertion(this.actor.deployment);
private async *_validFillOrder(): AsyncIterableIterator<AssertionResult | void> {
const { actors, balanceStore } = this.actor.simulationEnvironment!;
const assertion = validFillOrderAssertion(this.actor.deployment);
while (true) {
const maker = Pseudorandom.sample(marketMakers);
const maker = Pseudorandom.sample(filterActorsByRole(actors, Maker));
if (maker === undefined) {
yield undefined;
yield;
} 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),
await balanceStore.updateErc20BalancesAsync();
const [makerToken, makerFeeToken, takerToken, takerFeeToken] = Pseudorandom.sampleSize(
this.actor.deployment.tokens.erc20,
4, // tslint:disable-line:custom-no-magic-numbers
);
const configureOrderAssetAsync = async (
owner: Actor,
token: DummyERC20TokenContract,
): Promise<BigNumber> => {
let balance = balanceStore.balances.erc20[owner.address][token.address];
if (balance === undefined || balance.isZero()) {
await owner.configureERC20TokenAsync(token);
balance = balanceStore.balances.erc20[owner.address][token.address] =
constants.INITIAL_ERC20_BALANCE;
}
return Pseudorandom.integer(balance.dividedToIntegerBy(2));
};
const [makerAssetAmount, makerFee, takerAssetAmount, takerFee] = await Promise.all(
[
[maker, makerToken],
[maker, makerFeeToken],
[this.actor, takerToken],
[this.actor, takerFeeToken],
].map(async ([owner, token]) =>
configureOrderAssetAsync(owner as Actor, token as DummyERC20TokenContract),
),
this.actor.configureERC20TokenAsync(
this.actor.deployment.tokens.weth,
this.actor.deployment.staking.stakingProxy.address,
),
]);
);
const [makerAssetData, makerFeeAssetData, takerAssetData, takerFeeAssetData] = [
makerToken,
makerFeeToken,
takerToken,
takerFeeToken,
].map(token =>
this.actor.deployment.assetDataEncoder.ERC20Token(token.address).getABIEncodedTransactionData(),
);
const order = await maker.signOrderAsync({
makerAssetAmount: Pseudorandom.integer(constants.INITIAL_ERC20_BALANCE),
takerAssetAmount: Pseudorandom.integer(constants.INITIAL_ERC20_BALANCE),
makerAssetData,
takerAssetData,
makerFeeAssetData,
takerFeeAssetData,
makerAssetAmount,
takerAssetAmount,
makerFee,
takerFee,
feeRecipientAddress: Pseudorandom.sample(actors)!.address,
});
yield assertion.executeAsync([order, order.takerAssetAmount, order.signature], {
const fillAmount = Pseudorandom.integer(order.takerAssetAmount);
yield assertion.executeAsync([order, fillAmount, order.signature], {
from: this.actor.address,
});
}

View File

@@ -1,7 +1,7 @@
import { ObjectMap } from '@0x/types';
import * as _ from 'lodash';
import { Actor } from './base';
import { Actor, Constructor } from './base';
/**
* Utility function to convert Actors into an object mapping readable names to addresses.
@@ -10,3 +10,13 @@ import { Actor } from './base';
export function actorAddressesByName(actors: Actor[]): ObjectMap<string> {
return _.zipObject(actors.map(actor => actor.name), actors.map(actor => actor.address));
}
/**
* Filters the given actors by class.
*/
export function filterActorsByRole<TClass extends Constructor>(
actors: Actor[],
role: TClass,
): Array<InstanceType<typeof role>> {
return actors.filter(actor => actor instanceof role) as InstanceType<typeof role>;
}

View File

@@ -1,4 +1,4 @@
import { StakingPoolById, StoredBalance } from '@0x/contracts-staking';
import { StakingPool, StakingPoolById } from '@0x/contracts-staking';
import { expect } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
@@ -44,11 +44,7 @@ export function validCreateStakingPoolAssertion(
expect(actualPoolId).to.equal(expectedPoolId);
// Adds the new pool to local state
pools[actualPoolId] = {
operator: txData.from!,
operatorShare,
delegatedStake: new StoredBalance(),
};
pools[actualPoolId] = new StakingPool(txData.from!, operatorShare);
},
});
}

View File

@@ -1,6 +1,7 @@
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 { ReferenceFunctions } from '@0x/contracts-exchange-libs';
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';
@@ -15,7 +16,14 @@ function verifyFillEvents(
order: Order,
receipt: TransactionReceiptWithDecodedLogs,
deployment: DeploymentManager,
takerAssetFillAmount: BigNumber,
): void {
const fillResults = ReferenceFunctions.calculateFillResults(
order,
takerAssetFillAmount,
DeploymentManager.protocolFeeMultiplier,
DeploymentManager.gasPrice,
);
// Ensure that the fill event was correct.
verifyEvents<ExchangeFillEventArgs>(
receipt,
@@ -30,11 +38,7 @@ function verifyFillEvents(
orderHash: orderHashUtils.getOrderHashHex(order),
takerAddress,
senderAddress: takerAddress,
makerAssetFilledAmount: order.makerAssetAmount,
takerAssetFilledAmount: order.takerAssetAmount,
makerFeePaid: constants.ZERO_AMOUNT,
takerFeePaid: constants.ZERO_AMOUNT,
protocolFeePaid: DeploymentManager.protocolFee,
...fillResults,
},
],
ExchangeEvents.Fill,
@@ -47,12 +51,22 @@ function verifyFillEvents(
{
_from: takerAddress,
_to: order.makerAddress,
_value: order.takerAssetAmount,
_value: fillResults.takerAssetFilledAmount,
},
{
_from: order.makerAddress,
_to: takerAddress,
_value: order.makerAssetAmount,
_value: fillResults.makerAssetFilledAmount,
},
{
_from: takerAddress,
_to: order.feeRecipientAddress,
_value: fillResults.takerFeePaid,
},
{
_from: order.makerAddress,
_to: order.feeRecipientAddress,
_value: fillResults.makerFeePaid,
},
{
_from: takerAddress,
@@ -69,7 +83,7 @@ function verifyFillEvents(
*/
/* tslint:disable:no-unnecessary-type-assertion */
/* tslint:disable:no-non-null-assertion */
export function validFillOrderCompleteFillAssertion(
export function validFillOrderAssertion(
deployment: DeploymentManager,
): FunctionAssertion<[Order, BigNumber, string], {}, FillResults> {
const exchange = deployment.exchange;
@@ -81,13 +95,13 @@ export function validFillOrderCompleteFillAssertion(
args: [Order, BigNumber, string],
txData: Partial<TxData>,
) => {
const [order] = args;
const [order, fillAmount] = args;
// Ensure that the tx succeeded.
expect(result.success).to.be.true();
expect(result.success, `Error: ${result.data}`).to.be.true();
// Ensure that the correct events were emitted.
verifyFillEvents(txData.from!, order, result.receipt!, deployment);
verifyFillEvents(txData.from!, order, result.receipt!, deployment, fillAmount);
// TODO: Add validation for on-chain state (like balances)
},

View File

@@ -1,6 +1,13 @@
import { GlobalStakeByStatus, StakeStatus, StakingPoolById, StoredBalance } from '@0x/contracts-staking';
import {
constants as stakingConstants,
GlobalStakeByStatus,
StakeStatus,
StakingPoolById,
StoredBalance,
} from '@0x/contracts-staking';
import { BigNumber } from '@0x/utils';
import { Maker } from './actors/maker';
import { Actor } from './actors/base';
import { AssertionResult } from './assertions/function_assertion';
import { BlockchainBalanceStore } from './balances/blockchain_balance_store';
import { DeploymentManager } from './deployment_manager';
@@ -14,11 +21,12 @@ export class SimulationEnvironment {
[StakeStatus.Delegated]: new StoredBalance(),
};
public stakingPools: StakingPoolById = {};
public currentEpoch: BigNumber = stakingConstants.INITIAL_EPOCH;
public constructor(
public readonly deployment: DeploymentManager,
public balanceStore: BlockchainBalanceStore,
public marketMakers: Maker[] = [],
public actors: Actor[] = [],
) {}
public state(): any {

View File

@@ -18,6 +18,21 @@ class PRNGWrapper {
return arr[index];
}
/*
* Pseudorandom version of _.sampleSize. Returns an array of `n` samples from the given array
* (with replacement), chosen with uniform probability. Return undefined if the array is empty.
*/
public sampleSize<T>(arr: T[], n: number): T[] | undefined {
if (arr.length === 0) {
return undefined;
}
const samples = [];
for (let i = 0; i < n; i++) {
samples.push(this.sample(arr) as T);
}
return samples;
}
// tslint:disable:unified-signatures
/*
* Pseudorandom version of getRandomPortion/getRandomInteger. If two arguments are provided,

View File

@@ -1,7 +1,10 @@
import { blockchainTests, constants } from '@0x/contracts-test-utils';
import { blockchainTests } from '@0x/contracts-test-utils';
import { Actor } from '../framework/actors/base';
import { MakerTaker } from '../framework/actors/hybrids';
import { Maker } from '../framework/actors/maker';
import { Taker } from '../framework/actors/taker';
import { filterActorsByRole } from '../framework/actors/utils';
import { AssertionResult } from '../framework/assertions/function_assertion';
import { BlockchainBalanceStore } from '../framework/balances/blockchain_balance_store';
import { DeploymentManager } from '../framework/deployment_manager';
@@ -12,19 +15,15 @@ import { PoolManagementSimulation } from './pool_management_test';
class PoolMembershipSimulation extends Simulation {
protected async *_assertionGenerator(): AsyncIterableIterator<AssertionResult | void> {
const { deployment } = this.environment;
const { actors } = this.environment;
const makers = filterActorsByRole(actors, Maker);
const takers = filterActorsByRole(actors, Taker);
const poolManagement = new PoolManagementSimulation(this.environment);
const member = new MakerTaker({
name: 'member',
deployment,
simulationEnvironment: this.environment,
});
const actions = [
member.simulationActions.validJoinStakingPool,
member.simulationActions.validFillOrderCompleteFill,
...makers.map(maker => maker.simulationActions.validJoinStakingPool),
...takers.map(taker => taker.simulationActions.validFillOrder),
poolManagement.generator,
];
@@ -36,45 +35,23 @@ class PoolMembershipSimulation extends Simulation {
}
blockchainTests('pool membership fuzz test', env => {
let deployment: DeploymentManager;
let maker: Maker;
before(async function(): Promise<void> {
if (process.env.FUZZ_TEST !== 'pool_membership') {
this.skip();
}
});
deployment = await DeploymentManager.deployAsync(env, {
numErc20TokensToDeploy: 2,
after(async () => {
Actor.count = 0;
});
it('fuzz', async () => {
const deployment = await DeploymentManager.deployAsync(env, {
numErc20TokensToDeploy: 4,
numErc721TokensToDeploy: 0,
numErc1155TokensToDeploy: 0,
});
const makerToken = deployment.tokens.erc20[0];
const takerToken = deployment.tokens.erc20[1];
const orderConfig = {
feeRecipientAddress: constants.NULL_ADDRESS,
makerAssetData: deployment.assetDataEncoder.ERC20Token(makerToken.address).getABIEncodedTransactionData(),
takerAssetData: deployment.assetDataEncoder.ERC20Token(takerToken.address).getABIEncodedTransactionData(),
makerFeeAssetData: deployment.assetDataEncoder
.ERC20Token(makerToken.address)
.getABIEncodedTransactionData(),
takerFeeAssetData: deployment.assetDataEncoder
.ERC20Token(takerToken.address)
.getABIEncodedTransactionData(),
makerFee: constants.ZERO_AMOUNT,
takerFee: constants.ZERO_AMOUNT,
};
maker = new Maker({
name: 'maker',
deployment,
orderConfig,
});
});
it('fuzz', async () => {
const balanceStore = new BlockchainBalanceStore(
{
StakingProxy: deployment.staking.stakingProxy.address,
@@ -82,9 +59,25 @@ blockchainTests('pool membership fuzz test', env => {
},
{ erc20: { ZRX: deployment.tokens.zrx } },
);
const simulationEnvironment = new SimulationEnvironment(deployment, balanceStore);
const simulationEnv = new SimulationEnvironment(deployment, balanceStore, [maker]);
const simulation = new PoolMembershipSimulation(simulationEnv);
const actors = [
new Maker({ deployment, simulationEnvironment, name: 'Maker 1' }),
new Maker({ deployment, simulationEnvironment, name: 'Maker 2' }),
new Taker({ deployment, simulationEnvironment, name: 'Taker 1' }),
new Taker({ deployment, simulationEnvironment, name: 'Taker 2' }),
new MakerTaker({ deployment, simulationEnvironment, name: 'Maker/Taker' }),
];
const takers = filterActorsByRole(actors, Taker);
for (const taker of takers) {
await taker.configureERC20TokenAsync(deployment.tokens.weth, deployment.staking.stakingProxy.address);
}
for (const actor of actors) {
balanceStore.registerTokenOwner(actor.address, actor.name);
}
const simulation = new PoolMembershipSimulation(simulationEnvironment);
return simulation.fuzzAsync();
});
});