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,