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 { 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 = { 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, 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, orderSlippage, side), ); const result = simulateBestCaseFill({ orders, side, fillAmount: fillableInput, gasPrice: ONE, opts: { gasSchedule: GAS_SCHEDULE, protocolFeeMultiplier: ONE }, }); 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, protocolFeeMultiplier: ONE }, }); expect(result.gas).to.eq(countCollapsedFills(orders)); expect(result.protocolFeeAmount).to.bignumber.eq(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, protocolFeeMultiplier: ONE }, }); 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, protocolFeeMultiplier: ONE }, }); 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, protocolFeeMultiplier: ONE }, }); 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, protocolFeeMultiplier: ONE }, }); expect(result.gas).to.eq(countCollapsedFills(orders)); expect(result.protocolFeeAmount).to.bignumber.eq(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, protocolFeeMultiplier: ONE }, }); 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, protocolFeeMultiplier: ONE, 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, orderSlippage, side), ); orders = [...orders.slice(1), orders[0]]; const bestCase = simulateBestCaseFill({ orders, side, fillAmount: fillableInput, gasPrice: ONE, opts: { gasSchedule: GAS_SCHEDULE, protocolFeeMultiplier: ONE }, }); 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