update pool membership simulation to use multiple makers and takers, partial fills
This commit is contained in:
@@ -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
|
||||
|
@@ -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> {
|
||||
|
@@ -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 });
|
||||
}
|
||||
|
@@ -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 });
|
||||
}
|
||||
|
@@ -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> {
|
||||
|
@@ -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> {
|
||||
|
@@ -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,
|
||||
});
|
||||
}
|
||||
|
@@ -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>;
|
||||
}
|
||||
|
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -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)
|
||||
},
|
||||
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
|
Reference in New Issue
Block a user