Files
protocol/packages/asset-swapper/test/quote_simulation_test.ts
Jacob Evans 3f4bb933d1 feat: v4 final (#136)
* v4 FillQuoteTransformer (#104)

* Update FQT to support v4 orders

* `@0x/contracts-zero-ex`: Tweak FQT
`@0x/contracts-zero-ex`: Drop `ERC20BridgeTransfer` event and add `PartialQuoteFill` event.

* `@0x/contracts-utils`: Add `LibSafeMathV06.downcastToUint128()`

* `@0x/protocol-utils`: Update transformer utils for V4 FQT

* `@0x/contracts-zero-ex`: Fixing FQT tests...

* `@0x/contracts-zero-ex`: rename FQT bridge event

* `@0x/contracts-zero-ex`: Un-`only` tests

* `@0x/migrations`: Update `BridgeAdapter` deployment

* `@0x/contracts-integrations`: Delete `mtx_tests`

* `@0x/protocol-utils`: Address review comments

* `@0x/contracts-zero-ex`: Address review comments

* `@0x/migrations`: Update migrations

Co-authored-by: Michael Zhu <mchl.zhu.96@gmail.com>
Co-authored-by: Lawrence Forman <me@merklejerk.com>

* v4: Asset-swapper (main branch) (#113)

* refactor quote_requestor

* WIP v4/asset-swapper: Clean up SwapQuoter and remove @0x/orderbook

* Start replacing SignedOrder everywhere

* wip: new order type

* wip

* remove order-utils from most places

* hack: Play around with VerboseX types (#119)

* hack: Play around with VerboseX types

* More hacks

* Fix up the bridgeData encodings

* Rework Orderbook return type

* feat: Don't charge a protocol fee for RFQ orders WIP (#121)

* fix simple build errors

* simplify types a little

* remove SwapQuoteCalculator: unnecessary abstraction

* Fix all ./src build errors; make types consistent

* export more types for use in 0x API; modify Orderbook interface

* stop overriding APIOrder

* feat: RFQ v4 + consolidated bridge encoders (#125)

* feat: check if taker address is contract

* Rework bridge data

* Worst case adjustments

* RFQT v4

* Future/v4 validate orders (#126)

* RFQT v4

* v4 validate native orders

* use default invalid signature

* refactor rfqt validations in swap quoter

* fix types

* fix RFQT unlisted api key

* remove priceAwareRFQFlag

* adjust maker/taker amounts

* update JSON schemas

* filter zero fillable orders

Co-authored-by: xianny <xianny@gmail.com>

* fix type export

Co-authored-by: xianny <xianny@gmail.com>

* remove order-utils as much as possible

* work on tests compile

* Comment out quote reporter test

* updated tests

* restore order-utils accidental changes

* some lints

* Remove old fill_test

* ts lint disable for now

* update quote report

* Re-enable quote report tests

* make fill data required field

* fix lint

* type guards

* force fillData as required

* fix lint

* fix naming

* exports

* adjust MultiBridge by slippage

* cleanups (checkpoint 1)

* cleanup types (checkpoint #2)

* remove unused deps

* `@0x/contract-addresses`: Deploy new FQT (#129)

Co-authored-by: Lawrence Forman <me@merklejerk.com>

* commit bump to republish

* DRY up the rfqt mocker

* fix: Balancer load top pools (#131)

* fix: Balancer load top 250 pools

* refetch top pools on an interval

Co-authored-by: Jacob Evans <jacob@dekz.net>
Co-authored-by: Kim Persson <kimpers@users.noreply.github.com>
Co-authored-by: Lawrence Forman <lawrence@0xproject.com>
Co-authored-by: Lawrence Forman <me@merklejerk.com>

* Update post rebase

* prettier

* Remove test helpers exported in asset-swapper

* Clean up from review comments

* prettier

* lint

* recreate rfqt mocker

* change merge and INVALID_SIGNATURE

Co-authored-by: Lawrence Forman <lawrence@0xproject.com>
Co-authored-by: Michael Zhu <mchl.zhu.96@gmail.com>
Co-authored-by: Lawrence Forman <me@merklejerk.com>
Co-authored-by: Xianny <8582774+xianny@users.noreply.github.com>
Co-authored-by: Kim Persson <kimpers@users.noreply.github.com>
2021-02-10 19:20:15 +10:00

1000 lines
48 KiB
TypeScript

import { constants, expect, getRandomInteger, randomAddress } from '@0x/contracts-test-utils';
import { FillQuoteTransformerOrderType, SignatureType } from '@0x/protocol-utils';
import { BigNumber, hexUtils, NULL_BYTES } from '@0x/utils';
import * as _ from 'lodash';
import { MarketOperation } from '../src/types';
import {
CollapsedFill,
ERC20BridgeSource,
NativeLimitOrderFillData,
OptimizedMarketOrder,
OptimizedMarketOrderBase,
} from '../src/utils/market_operation_utils/types';
import {
fillQuoteOrders,
QuoteFillOrderCall,
simulateBestCaseFill,
simulateWorstCaseFill,
} from '../src/utils/quote_simulation';
// tslint:disable: custom-no-magic-numbers
describe('quote_simulation tests', async () => {
const { NULL_ADDRESS } = constants;
const ZERO = new BigNumber(0);
const ONE = new BigNumber(1);
const MAKER_TOKEN = randomAddress();
const TAKER_TOKEN = randomAddress();
const GAS_SCHEDULE = { [ERC20BridgeSource.Uniswap]: _.constant(1), [ERC20BridgeSource.Native]: _.constant(1) };
// Check if two numbers are within `maxError` error rate within each other.
function assertRoughlyEquals(n1: BigNumber, n2: BigNumber, maxError: BigNumber | number = 1e-10): void {
// |n2-n1| / max(|n1|, |n2|)
const err = n2
.minus(n1)
.abs()
.div(BigNumber.max(n1.abs(), n2.abs()));
expect(err).to.bignumber.lt(maxError);
}
function createQuoteFillOrders(
opts: Partial<{
fillableInput: BigNumber;
fillableOutput: BigNumber;
inputFeeRate: number;
outputFeeRate: number;
count: number;
fillsCount: number;
side: MarketOperation;
type?: FillQuoteTransformerOrderType;
}> = {},
): QuoteFillOrderCall[] {
const { fillableInput, fillableOutput, inputFeeRate, outputFeeRate, count, fillsCount, side, type } = {
fillableInput: getRandomOrderSize(),
fillableOutput: getRandomOrderSize(),
inputFeeRate: 0,
outputFeeRate: 0,
count: 3,
fillsCount: 3,
side: MarketOperation.Sell,
...opts,
};
const _inputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate;
const _outputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate;
const fillableInputs = subdivideAmount(fillableInput, count);
const fillableOutputs = subdivideAmount(fillableOutput, count);
const filledInputs = subdivideAmount(fillableInput.times(0.5), count);
const filledOutputs: BigNumber[] = [];
const totalInputs: BigNumber[] = [];
const totalOutputs: BigNumber[] = [];
const inputFees: BigNumber[] = [];
const outputFees: BigNumber[] = [];
_.times(count).forEach(i => {
const f = filledInputs[i].div(fillableInputs[i]);
filledOutputs.push(fillableOutputs[i].times(f).integerValue(BigNumber.ROUND_DOWN));
totalInputs.push(fillableInputs[i].plus(filledInputs[i]));
totalOutputs.push(fillableOutputs[i].plus(filledOutputs[i]));
inputFees.push(totalInputs[i].times(_inputFeeRate).integerValue());
outputFees.push(totalOutputs[i].times(_outputFeeRate).integerValue());
});
return _.times(count, i => {
return {
order: createQuoteFillOrderOrder(totalInputs[i], totalOutputs[i], {
side,
fillsCount,
filledInput: filledInputs[i],
takerInputFee: inputFees[i].abs(),
takerOutputFee: outputFees[i].abs(),
type,
}),
totalOrderInput: totalInputs[i],
totalOrderOutput: totalOutputs[i],
totalOrderInputFee: inputFees[i],
totalOrderOutputFee: outputFees[i],
};
});
}
function createQuoteFillOrderOrder(
input: BigNumber,
output: BigNumber,
opts: Partial<{
filledInput: BigNumber;
fillsCount: number;
side: MarketOperation;
takerInputFee: BigNumber;
takerOutputFee: BigNumber;
type: FillQuoteTransformerOrderType;
}> = {},
): OptimizedMarketOrderBase<NativeLimitOrderFillData> {
const { filledInput, fillsCount, side, takerInputFee, takerOutputFee, type } = _.merge(
{},
{
side: MarketOperation.Sell,
filledInput: ZERO,
fillsCount: 3,
takerInputFee: ZERO,
takerOutputFee: ZERO,
type: FillQuoteTransformerOrderType.Limit,
},
opts,
);
const filledOutput = filledInput
.div(input)
.times(output)
.integerValue(BigNumber.ROUND_DOWN);
const fillableInput = input.minus(filledInput);
const fillableOutput = output.minus(filledOutput);
const makerAmount = side === MarketOperation.Sell ? output : input;
const takerAmount = side === MarketOperation.Sell ? input : output;
const fillableMakerAmount = side === MarketOperation.Sell ? fillableOutput : fillableInput;
const fillableTakerAmount = side === MarketOperation.Sell ? fillableInput : fillableOutput;
const takerFee = BigNumber.max(takerInputFee, takerOutputFee);
const order: OptimizedMarketOrderBase<NativeLimitOrderFillData> = {
source: ERC20BridgeSource.Native,
makerToken: MAKER_TOKEN,
takerToken: TAKER_TOKEN,
makerAmount: fillableMakerAmount,
takerAmount: fillableTakerAmount,
fillData: {
order: {
makerToken: MAKER_TOKEN,
makerAmount,
takerToken: TAKER_TOKEN,
takerAmount,
maker: NULL_ADDRESS,
taker: NULL_ADDRESS,
sender: NULL_ADDRESS,
salt: ZERO,
chainId: 1,
pool: NULL_BYTES,
verifyingContract: NULL_ADDRESS,
expiry: ZERO,
feeRecipient: NULL_ADDRESS,
takerTokenFeeAmount: takerFee,
},
signature: { v: 1, r: NULL_BYTES, s: NULL_BYTES, signatureType: SignatureType.EthSign },
maxTakerTokenFillAmount: fillableTakerAmount,
},
type,
fills: createOrderCollapsedFills(fillableInput, fillableOutput, fillsCount),
};
return order;
}
const nativeSourcePathId = hexUtils.random();
function createOrderCollapsedFills(input: BigNumber, output: BigNumber, count: number): CollapsedFill[] {
const inputs = subdivideAmount(input, count);
const outputs = subdivideAmount(output, count);
return _.times(count, i => {
const subFillInputs = subdivideAmount(inputs[i], count);
const subFillOutputs = subdivideAmount(outputs[i], count);
return {
type: FillQuoteTransformerOrderType.Bridge,
sourcePathId: nativeSourcePathId,
source: ERC20BridgeSource.Uniswap,
fillData: {},
input: inputs[i],
output: outputs[i],
subFills: _.times(count, j => ({
input: subFillInputs[j],
output: subFillOutputs[j],
})),
};
});
}
function countCollapsedFills(fillOrders: QuoteFillOrderCall[] | OptimizedMarketOrder[]): number {
let count = 0;
if ((fillOrders as any)[0].fills) {
const orders = (fillOrders as any) as OptimizedMarketOrder[];
for (const o of orders) {
count += o.fills.length;
}
} else {
const orders = (fillOrders as any) as QuoteFillOrderCall[];
for (const fo of orders) {
count += fo.order.fills.length;
}
}
return count;
}
function randomSide(): MarketOperation {
return _.sampleSize(Object.values(MarketOperation), 1)[0];
}
function getRandomOrderSize(): BigNumber {
return getRandomInteger('100e18', '1000e18');
}
function getRandomFeeRate(): number {
return _.random(0.01, 0.25, true);
}
function assertEqualRates(actual: number | BigNumber, expected: number | BigNumber): void {
expect(new BigNumber(actual).times(1e4).integerValue()).to.bignumber.eq(
new BigNumber(expected).times(1e4).integerValue(),
);
}
function subdivideAmount(amount: BigNumber, count: number): BigNumber[] {
const amounts = [];
for (let i = 0; i < count; ++i) {
const remaining = amount.minus(BigNumber.sum(0, ...amounts));
if (i !== count - 1) {
amounts.push(remaining.times(Math.random()).integerValue());
} else {
amounts.push(remaining.integerValue());
}
}
return amounts;
}
describe('fillQuoteOrders()', () => {
describe('single order', () => {
it('can exactly fill one order', () => {
const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
side,
fillsCount,
count: 1,
});
const result = fillQuoteOrders(fillOrders, fillableInput, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
expect(totalFilledInput).to.bignumber.eq(fillableInput);
assertRoughlyEquals(totalFilledOutput, fillableOutput);
expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.eq(fillsCount);
});
it('can partially fill one simple order', () => {
const side = randomSide();
const fillsCount = 1;
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
side,
fillsCount,
count: 1,
});
const inputFillAmount = fillableInput.times(2 / 3).integerValue();
const result = fillQuoteOrders(fillOrders, inputFillAmount, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
expect(totalFilledInput).to.bignumber.eq(inputFillAmount);
const expectedOutputFilledAmount = inputFillAmount
.div(fillableInput)
.times(fillableOutput)
.integerValue();
assertRoughlyEquals(totalFilledOutput, expectedOutputFilledAmount);
expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.eq(1);
});
it('can partially fill one batched order', () => {
const side = randomSide();
const fillsCount = 3;
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
side,
fillsCount,
count: 1,
});
const inputFillAmount = fillableInput.times(2 / 3).integerValue();
const result = fillQuoteOrders(fillOrders, inputFillAmount, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
expect(totalFilledInput).to.bignumber.eq(inputFillAmount);
expect(totalFilledOutput).to.bignumber.lt(fillableOutput);
expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.gte(1);
expect(result.gas).to.lte(fillsCount);
});
it('does not over fill one order', () => {
const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
side,
fillsCount,
count: 1,
});
const inputFillAmount = fillableInput.times(3 / 2).integerValue();
const result = fillQuoteOrders(fillOrders, inputFillAmount, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
expect(totalFilledInput).to.bignumber.eq(fillableInput);
assertRoughlyEquals(totalFilledOutput, fillableOutput);
expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.eq(fillsCount);
});
it('can exactly fill one order with input fees', () => {
const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const inputFeeRate = getRandomFeeRate();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
inputFeeRate,
side,
fillsCount,
count: 1,
});
const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate;
const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue();
const result = fillQuoteOrders(fillOrders, totalFillableInput, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
assertRoughlyEquals(totalFilledInput, totalFillableInput);
assertRoughlyEquals(totalFilledOutput, fillableOutput);
assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate);
expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.eq(fillsCount);
});
it('can partially fill one order with input fees', () => {
const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const inputFeeRate = getRandomFeeRate();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
inputFeeRate,
side,
fillsCount,
count: 1,
});
const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate;
const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue();
const inputFillAmount = totalFillableInput.times(2 / 3).integerValue();
const result = fillQuoteOrders(fillOrders, inputFillAmount, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
assertRoughlyEquals(totalFilledInput, inputFillAmount);
expect(totalFilledOutput).to.bignumber.lt(fillableOutput);
assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate);
expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.lte(fillsCount);
});
it('does not over fill one order with input fees', () => {
const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const inputFeeRate = getRandomFeeRate();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
inputFeeRate,
side,
fillsCount,
count: 1,
});
const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate;
const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue();
const inputFillAmount = totalFillableInput.times(3 / 2).integerValue();
const result = fillQuoteOrders(fillOrders, inputFillAmount, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
assertRoughlyEquals(totalFilledInput, totalFillableInput);
assertRoughlyEquals(totalFilledOutput, fillableOutput);
assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate);
expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.eq(fillsCount);
});
it('can exactly fill one order with output fees', () => {
const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const outputFeeRate = getRandomFeeRate();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
outputFeeRate,
side,
fillsCount,
count: 1,
});
const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate;
const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue();
const result = fillQuoteOrders(fillOrders, fillableInput, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
assertRoughlyEquals(totalFilledInput, fillableInput);
assertRoughlyEquals(totalFilledOutput, totalFillableOutput);
assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate);
expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.eq(fillsCount);
});
it('can partial fill one order with output fees', () => {
const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const outputFeeRate = getRandomFeeRate();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
outputFeeRate,
side,
fillsCount,
count: 1,
});
const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate;
const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue();
const inputFillAmount = fillableInput.times(2 / 3).integerValue();
const result = fillQuoteOrders(fillOrders, inputFillAmount, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
assertRoughlyEquals(totalFilledInput, inputFillAmount);
expect(totalFilledOutput).to.bignumber.lt(totalFillableOutput);
assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate);
expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.lte(fillsCount);
});
it('does not over fill one order with output fees', () => {
const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const outputFeeRate = getRandomFeeRate();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
outputFeeRate,
side,
fillsCount,
count: 1,
});
const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate;
const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue();
const inputFillAmount = fillableInput.times(3 / 2).integerValue();
const result = fillQuoteOrders(fillOrders, inputFillAmount, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
assertRoughlyEquals(totalFilledInput, fillableInput);
assertRoughlyEquals(totalFilledOutput, totalFillableOutput);
assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate);
expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.eq(fillsCount);
});
it('does not charge a protocol fee for rfq orders', () => {
const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
side,
fillsCount,
count: 1,
type: FillQuoteTransformerOrderType.Rfq,
});
const result = fillQuoteOrders(fillOrders, fillableInput, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
expect(totalFilledInput).to.bignumber.eq(fillableInput);
assertRoughlyEquals(totalFilledOutput, fillableOutput);
expect(result.protocolFee).to.bignumber.eq(0);
expect(result.gas).to.eq(fillsCount);
});
});
describe('multiple orders', () => {
it('can exactly fill orders', () => {
const side = randomSide();
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const fillOrders = createQuoteFillOrders({ fillableInput, fillableOutput, side });
const result = fillQuoteOrders(fillOrders, fillableInput, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
expect(totalFilledInput).to.bignumber.eq(fillableInput);
expect(totalFilledOutput).to.bignumber.eq(fillableOutput);
expect(result.protocolFee).to.bignumber.eq(fillOrders.length);
expect(result.gas).to.eq(countCollapsedFills(fillOrders));
});
it('can partial fill orders', () => {
const side = randomSide();
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const inputFillAmount = fillableInput.times(2 / 3).integerValue();
const fillOrders = createQuoteFillOrders({ fillableInput, fillableOutput, side });
const result = fillQuoteOrders(fillOrders, inputFillAmount, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
expect(totalFilledInput).to.bignumber.eq(inputFillAmount);
expect(totalFilledOutput).to.bignumber.lt(fillableOutput);
expect(result.protocolFee).to.bignumber.gte(1);
});
it('does not over fill orders', () => {
const side = randomSide();
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const inputFillAmount = fillableInput.times(3 / 2).integerValue();
const fillOrders = createQuoteFillOrders({ fillableInput, fillableOutput, side });
const result = fillQuoteOrders(fillOrders, inputFillAmount, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
expect(totalFilledInput).to.bignumber.eq(fillableInput);
expect(totalFilledOutput).to.bignumber.eq(fillableOutput);
expect(result.protocolFee).to.bignumber.eq(fillOrders.length);
expect(result.gas).to.eq(countCollapsedFills(fillOrders));
});
it('can exactly fill orders with input fees', () => {
const side = randomSide();
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const inputFeeRate = getRandomFeeRate();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
inputFeeRate,
side,
});
const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate;
const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue();
const result = fillQuoteOrders(fillOrders, totalFillableInput, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
assertRoughlyEquals(totalFilledInput, totalFillableInput);
assertRoughlyEquals(totalFilledOutput, fillableOutput);
assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate);
expect(result.protocolFee).to.bignumber.eq(fillOrders.length);
expect(result.gas).to.eq(countCollapsedFills(fillOrders));
});
it('can partial fill orders with input fees', () => {
const side = randomSide();
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const inputFeeRate = getRandomFeeRate();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
inputFeeRate,
side,
});
const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate;
const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue();
const inputFillAmount = totalFillableInput.times(2 / 3).integerValue();
const result = fillQuoteOrders(fillOrders, inputFillAmount, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
assertRoughlyEquals(totalFilledInput, inputFillAmount);
expect(totalFilledOutput).to.bignumber.lt(fillableOutput);
assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate);
expect(result.protocolFee).to.bignumber.lte(fillOrders.length);
expect(result.gas).to.lte(countCollapsedFills(fillOrders));
});
it('does not over fill orders with input fees', () => {
const side = randomSide();
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const inputFeeRate = getRandomFeeRate();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
inputFeeRate,
side,
});
const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate;
const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue();
const inputFillAmount = totalFillableInput.times(3 / 2).integerValue();
const result = fillQuoteOrders(fillOrders, inputFillAmount, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
assertRoughlyEquals(totalFilledInput, totalFillableInput);
assertRoughlyEquals(totalFilledOutput, fillableOutput);
assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate);
expect(result.protocolFee).to.bignumber.eq(fillOrders.length);
expect(result.gas).to.eq(countCollapsedFills(fillOrders));
});
it('can exactly fill orders with output fees', () => {
const side = randomSide();
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const outputFeeRate = getRandomFeeRate();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
outputFeeRate,
side,
});
const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate;
const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue();
const result = fillQuoteOrders(fillOrders, fillableInput, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
assertRoughlyEquals(totalFilledInput, fillableInput);
assertRoughlyEquals(totalFilledOutput, totalFillableOutput);
assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate);
expect(result.protocolFee).to.bignumber.eq(fillOrders.length);
expect(result.gas).to.eq(countCollapsedFills(fillOrders));
});
it('can partial fill orders with output fees', () => {
const side = randomSide();
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const outputFeeRate = getRandomFeeRate();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
outputFeeRate,
side,
});
const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate;
const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue();
const inputFillAmount = fillableInput.times(2 / 3).integerValue();
const result = fillQuoteOrders(fillOrders, inputFillAmount, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
assertRoughlyEquals(totalFilledInput, inputFillAmount);
expect(totalFilledOutput).to.bignumber.lt(totalFillableOutput);
assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate);
expect(result.protocolFee).to.bignumber.lte(fillOrders.length);
expect(result.gas).to.lte(countCollapsedFills(fillOrders));
});
it('does not over fill orders with output fees', () => {
const side = randomSide();
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const outputFeeRate = getRandomFeeRate();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
outputFeeRate,
side,
});
const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate;
const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue();
const inputFillAmount = fillableInput.times(3 / 2).integerValue();
const result = fillQuoteOrders(fillOrders, inputFillAmount, ONE, GAS_SCHEDULE);
const totalFilledInput = result.input.plus(result.inputFee);
const totalFilledOutput = result.output.plus(result.outputFee);
assertRoughlyEquals(totalFilledInput, fillableInput);
assertRoughlyEquals(totalFilledOutput, totalFillableOutput);
assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate);
expect(result.protocolFee).to.bignumber.eq(fillOrders.length);
expect(result.gas).to.eq(countCollapsedFills(fillOrders));
});
});
});
function slipOrder(
order: OptimizedMarketOrderBase<NativeLimitOrderFillData>,
orderSlippage: number,
side: MarketOperation,
): OptimizedMarketOrder {
const makerScaling = side === MarketOperation.Sell ? 1 - orderSlippage : 1;
const takerScaling = side === MarketOperation.Sell ? 1 : orderSlippage + 1;
// tslint:disable:next-line no-unnecessary-type-assertion
const nativeFillData = order.fillData!;
const slippedFillData = {
order: {
...nativeFillData.order,
takerAmount: nativeFillData.order.takerAmount.times(takerScaling),
makerAmount: nativeFillData.order.makerAmount.times(makerScaling),
},
signature: nativeFillData.signature,
maxTakerTokenFillAmount: nativeFillData.maxTakerTokenFillAmount.times(takerScaling),
};
return {
...order,
makerAmount: order.makerAmount.times(makerScaling),
takerAmount: order.takerAmount.times(takerScaling),
fillData: slippedFillData,
};
}
describe('simulateBestCaseFill()', () => {
it('ignores order slippage', async () => {
const side = randomSide();
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const orderSlippage = getRandomFeeRate();
const fillOrders = createQuoteFillOrders({
fillableInput,
fillableOutput,
side,
});
const orders = fillOrders.map(fo =>
slipOrder(fo.order as OptimizedMarketOrderBase<NativeLimitOrderFillData>, orderSlippage, side),
);
const result = simulateBestCaseFill({
orders,
side,
fillAmount: fillableInput,
gasPrice: ONE,
opts: { gasSchedule: GAS_SCHEDULE },
});
if (side === MarketOperation.Sell) {
expect(result.totalMakerAssetAmount).to.be.bignumber.eq(fillableOutput);
expect(result.totalTakerAssetAmount).to.be.bignumber.eq(fillableInput);
} else {
expect(result.totalMakerAssetAmount).to.be.bignumber.eq(fillableInput);
expect(result.totalTakerAssetAmount).to.be.bignumber.eq(fillableOutput);
}
});
it('can fully fill orders', async () => {
const side = randomSide();
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const orders = createQuoteFillOrders({
fillableInput,
fillableOutput,
side,
}).map(fo => fo.order);
const result = simulateBestCaseFill({
orders,
side,
fillAmount: fillableInput,
gasPrice: ONE,
opts: { gasSchedule: GAS_SCHEDULE },
});
expect(result.gas).to.eq(countCollapsedFills(orders));
expect(result.protocolFeeAmount).to.bignumber.gt(orders.length);
expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0);
expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0);
expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount);
expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount);
if (side === MarketOperation.Sell) {
expect(result.totalMakerAssetAmount).to.be.bignumber.eq(fillableOutput);
expect(result.totalTakerAssetAmount).to.be.bignumber.eq(fillableInput);
} else {
expect(result.totalMakerAssetAmount).to.be.bignumber.eq(fillableInput);
expect(result.totalTakerAssetAmount).to.be.bignumber.eq(fillableOutput);
}
});
it('can partial fill orders', async () => {
const side = randomSide();
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const orders = createQuoteFillOrders({
fillableInput,
fillableOutput,
side,
}).map(fo => fo.order);
const inputFillAmount = fillableInput.times(Math.random()).integerValue();
const result = simulateBestCaseFill({
orders,
side,
fillAmount: inputFillAmount,
gasPrice: ONE,
opts: { gasSchedule: GAS_SCHEDULE },
});
expect(result.gas).to.gt(0);
expect(result.protocolFeeAmount).to.bignumber.gt(0);
expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0);
expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0);
expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount);
expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount);
if (side === MarketOperation.Sell) {
expect(result.totalMakerAssetAmount).to.be.bignumber.lt(fillableOutput);
expect(result.totalTakerAssetAmount).to.be.bignumber.eq(inputFillAmount);
} else {
expect(result.totalMakerAssetAmount).to.be.bignumber.eq(inputFillAmount);
expect(result.totalTakerAssetAmount).to.be.bignumber.lt(fillableOutput);
}
});
it('can fully fill sell orders with "input" fees', async () => {
const side = MarketOperation.Sell;
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const inputFeeRate = getRandomFeeRate();
const orders = createQuoteFillOrders({
fillableInput,
fillableOutput,
inputFeeRate,
}).map(fo => fo.order);
const signedInputFeeRate = inputFeeRate;
const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue();
const result = simulateBestCaseFill({
orders,
side,
fillAmount: totalFillableInput,
gasPrice: ONE,
opts: { gasSchedule: GAS_SCHEDULE },
});
assertRoughlyEquals(result.takerAssetAmount, fillableInput);
assertRoughlyEquals(result.totalTakerAssetAmount, totalFillableInput);
assertRoughlyEquals(result.makerAssetAmount, fillableOutput);
assertRoughlyEquals(result.totalMakerAssetAmount, fillableOutput);
expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount);
expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0);
});
it('can partially fill sell orders with "input" fees', async () => {
const side = MarketOperation.Sell;
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const inputFeeRate = getRandomFeeRate();
const orders = createQuoteFillOrders({
fillableInput,
fillableOutput,
inputFeeRate,
side,
}).map(fo => fo.order);
const signedInputFeeRate = inputFeeRate;
const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue();
const inputFillAmount = totalFillableInput.times(2 / 3).integerValue();
const result = simulateBestCaseFill({
orders,
side,
fillAmount: inputFillAmount,
gasPrice: ONE,
opts: { gasSchedule: GAS_SCHEDULE },
});
expect(result.gas).to.gt(0);
expect(result.protocolFeeAmount).to.bignumber.gt(0);
assertRoughlyEquals(result.totalTakerAssetAmount, inputFillAmount);
expect(result.makerAssetAmount).to.bignumber.lt(fillableOutput);
expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount);
expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0);
});
it('can fully fill buy orders with "output" fees', async () => {
const side = MarketOperation.Buy;
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const outputFeeRate = getRandomFeeRate();
const orders = createQuoteFillOrders({
fillableInput,
fillableOutput,
outputFeeRate,
side,
}).map(fo => fo.order);
const signedOutputFeeRate = outputFeeRate;
const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue();
const result = simulateBestCaseFill({
orders,
side,
fillAmount: fillableInput,
gasPrice: ONE,
opts: { gasSchedule: GAS_SCHEDULE },
});
expect(result.gas).to.eq(countCollapsedFills(orders));
expect(result.protocolFeeAmount).to.bignumber.gt(orders.length);
assertRoughlyEquals(result.makerAssetAmount, fillableInput);
assertRoughlyEquals(result.totalMakerAssetAmount, fillableInput);
assertRoughlyEquals(result.takerAssetAmount, fillableOutput);
assertRoughlyEquals(result.totalTakerAssetAmount, totalFillableOutput);
expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount);
expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0);
});
it('can partially fill buy orders with "output" fees', async () => {
const side = MarketOperation.Buy;
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const outputFeeRate = getRandomFeeRate();
const orders = createQuoteFillOrders({
fillableInput,
fillableOutput,
outputFeeRate,
side,
}).map(fo => fo.order);
const inputFillAmount = fillableInput.times(2 / 3).integerValue();
const result = simulateBestCaseFill({
orders,
side,
fillAmount: inputFillAmount,
gasPrice: ONE,
opts: { gasSchedule: GAS_SCHEDULE },
});
expect(result.gas).to.gt(0);
expect(result.protocolFeeAmount).to.bignumber.gt(0);
assertRoughlyEquals(result.totalMakerAssetAmount, inputFillAmount);
expect(result.takerAssetAmount).to.bignumber.lt(fillableOutput);
expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount);
expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0);
});
});
describe('simulateWorstCaseFill()', () => {
it('includes order slippage', async () => {
const side = randomSide();
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const slippage = getRandomFeeRate();
const orders = createQuoteFillOrders({
fillableInput,
fillableOutput,
side,
}).map(fo => fo.order);
const result = simulateWorstCaseFill({
orders,
side,
fillAmount: fillableInput,
gasPrice: ONE,
opts: { gasSchedule: GAS_SCHEDULE, slippage },
});
if (side === MarketOperation.Sell) {
const slippedOutput = fillableOutput.times(1 - slippage).integerValue();
assertRoughlyEquals(result.totalMakerAssetAmount, slippedOutput);
assertRoughlyEquals(result.totalTakerAssetAmount, fillableInput);
} else {
const slippedOutput = fillableOutput.times(slippage + 1).integerValue();
assertRoughlyEquals(result.totalMakerAssetAmount, fillableInput);
assertRoughlyEquals(result.totalTakerAssetAmount, slippedOutput);
}
});
it('expects worse price than the best case, even if orders are unsorted', async () => {
const side = randomSide();
const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize();
const orderSlippage = getRandomFeeRate();
let orders = createQuoteFillOrders({
fillableInput,
fillableOutput,
side,
}).map(fo =>
slipOrder(fo.order as OptimizedMarketOrderBase<NativeLimitOrderFillData>, orderSlippage, side),
);
orders = [...orders.slice(1), orders[0]];
const bestCase = simulateBestCaseFill({
orders,
side,
fillAmount: fillableInput,
gasPrice: ONE,
opts: { gasSchedule: GAS_SCHEDULE },
});
const worstCase = simulateWorstCaseFill({
orders,
side,
fillAmount: fillableInput,
gasPrice: ONE,
opts: { gasSchedule: GAS_SCHEDULE, slippage: orderSlippage },
});
const bestPrice = bestCase.makerAssetAmount.div(bestCase.totalTakerAssetAmount);
const worstPrice = worstCase.makerAssetAmount.div(worstCase.totalTakerAssetAmount);
expect(worstPrice).to.be.bignumber.lt(bestPrice);
});
});
}); // tslint:disable: max-file-line-count