From da1e9c2d976a8138cedf8ccc2173f98b91ef3afc Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 1 Apr 2020 12:52:02 -0400 Subject: [PATCH 01/13] `@0x/asset-swapper`: Add DFB support + refactor swap quote calculation utils --- packages/asset-swapper/CHANGELOG.json | 4 + packages/asset-swapper/src/swap_quoter.ts | 2 +- .../utils/market_operation_utils/constants.ts | 1 + .../src/utils/market_operation_utils/fills.ts | 16 +- .../src/utils/market_operation_utils/index.ts | 5 + .../utils/market_operation_utils/orders.ts | 144 +++++-- .../src/utils/market_operation_utils/types.ts | 23 +- .../src/utils/protocol_fee_utils.ts | 18 - .../src/utils/quote_simulation.ts | 344 ++++++++++++++++ .../src/utils/swap_quote_calculator.ts | 378 +++--------------- .../test/exchange_swap_quote_consumer_test.ts | 5 - .../forwarder_swap_quote_consumer_test.ts | 6 - .../test/market_operation_utils_test.ts | 94 ++++- .../test/swap_quote_consumer_utils_test.ts | 7 - packages/asset-swapper/test/utils/mocks.ts | 4 - .../asset-swapper/test/utils/swap_quote.ts | 5 +- 16 files changed, 628 insertions(+), 428 deletions(-) create mode 100644 packages/asset-swapper/src/utils/quote_simulation.ts diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 21f2552386..0ba1b4975d 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -37,6 +37,10 @@ { "note": "Fix `getBatchMarketBuyOrdersAsync` throwing NO_OPTIMAL_PATH", "pr": 2533 + }, + { + "note": "Add DFB support + refactor swap quote calculator utils", + "pr": 2536 } ] }, diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index 1ddbe95ad2..d87bef6a5b 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -179,7 +179,7 @@ export class SwapQuoter { }, liquidityProviderRegistryAddress, ); - this._swapQuoteCalculator = new SwapQuoteCalculator(this._protocolFeeUtils, this._marketOperationUtils); + this._swapQuoteCalculator = new SwapQuoteCalculator(this._marketOperationUtils); } /** diff --git a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts index 6a37a77850..392edbe432 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -34,6 +34,7 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = { feeSchedule: {}, gasSchedule: {}, allowFallback: true, + shouldBatchBridgeOrders: true, }; /** diff --git a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts index 11a472caef..a5090acb3a 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts @@ -233,27 +233,25 @@ export function clipPathToInput(path: Fill[], targetInput: BigNumber = POSITIVE_ return clipped; } -export function collapsePath(side: MarketOperation, path: Fill[]): CollapsedFill[] { +export function collapsePath(path: Fill[]): CollapsedFill[] { const collapsed: Array = []; for (const fill of path) { - const makerAssetAmount = side === MarketOperation.Sell ? fill.output : fill.input; - const takerAssetAmount = side === MarketOperation.Sell ? fill.input : fill.output; const source = fill.source; if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) { const prevFill = collapsed[collapsed.length - 1]; // If the last fill is from the same source, merge them. if (prevFill.source === source) { - prevFill.totalMakerAssetAmount = prevFill.totalMakerAssetAmount.plus(makerAssetAmount); - prevFill.totalTakerAssetAmount = prevFill.totalTakerAssetAmount.plus(takerAssetAmount); - prevFill.subFills.push({ makerAssetAmount, takerAssetAmount }); + prevFill.input = prevFill.input.plus(fill.input); + prevFill.output = prevFill.output.plus(fill.output); + prevFill.subFills.push(fill); continue; } } collapsed.push({ source: fill.source, - totalMakerAssetAmount: makerAssetAmount, - totalTakerAssetAmount: takerAssetAmount, - subFills: [{ makerAssetAmount, takerAssetAmount }], + input: fill.input, + output: fill.output, + subFills: [fill], nativeOrder: fill.source === ERC20BridgeSource.Native ? (fill.fillData as NativeFillData).order : undefined, }); } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 5f8ed0b3ec..371fcce13f 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -107,6 +107,7 @@ export class MarketOperationUtils { excludedSources: _opts.excludedSources, feeSchedule: _opts.feeSchedule, allowFallback: _opts.allowFallback, + shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, }); } @@ -180,6 +181,7 @@ export class MarketOperationUtils { excludedSources: _opts.excludedSources, feeSchedule: _opts.feeSchedule, allowFallback: _opts.allowFallback, + shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, }); } @@ -254,6 +256,7 @@ export class MarketOperationUtils { excludedSources: _opts.excludedSources, feeSchedule: _opts.feeSchedule, allowFallback: _opts.allowFallback, + shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, }); } catch (e) { // It's possible for one of the pairs to have no path @@ -278,6 +281,7 @@ export class MarketOperationUtils { excludedSources?: ERC20BridgeSource[]; feeSchedule?: { [source: string]: BigNumber }; allowFallback?: boolean; + shouldBatchBridgeOrders?: boolean; liquidityProviderAddress?: string; }): OptimizedMarketOrder[] { const { inputToken, outputToken, side, inputAmount } = opts; @@ -327,6 +331,7 @@ export class MarketOperationUtils { contractAddresses: this.contractAddresses, bridgeSlippage: opts.bridgeSlippage || 0, liquidityProviderAddress: opts.liquidityProviderAddress, + shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders, }); } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index 6bef4ba01b..28c1e066df 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -1,7 +1,8 @@ import { ContractAddresses } from '@0x/contract-addresses'; +import { DexForwaderBridgeData, dexForwarderBridgeDataEncoder } from '@0x/contracts-asset-proxy'; import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils'; -import { SignedOrder } from '@0x/types'; -import { AbiEncoder, BigNumber } from '@0x/utils'; +import { ERC20BridgeAssetData, SignedOrder } from '@0x/types'; +import { AbiEncoder, BigNumber, hexUtils } from '@0x/utils'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; @@ -71,12 +72,7 @@ export function convertNativeOrderToFullyFillableOptimizedOrders(order: SignedOr fillableMakerAssetAmount: order.makerAssetAmount, fillableTakerAssetAmount: order.takerAssetAmount, fillableTakerFeeAmount: order.takerFee, - fill: { - source: ERC20BridgeSource.Native, - totalMakerAssetAmount: order.makerAssetAmount, - totalTakerAssetAmount: order.takerAssetAmount, - subFills: [], - }, + fills: [], }; } @@ -119,18 +115,34 @@ export interface CreateOrderFromPathOpts { orderDomain: OrderDomain; contractAddresses: ContractAddresses; bridgeSlippage: number; + shouldBatchBridgeOrders: boolean; liquidityProviderAddress?: string; } // Convert sell fills into orders. export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] { - const collapsedPath = collapsePath(opts.side, path); + const collapsedPath = collapsePath(path); const orders: OptimizedMarketOrder[] = []; - for (const fill of collapsedPath) { - if (fill.source === ERC20BridgeSource.Native) { - orders.push(createNativeOrder(fill)); + for (let i = 0; i < collapsedPath.length;) { + if (collapsedPath[i].source === ERC20BridgeSource.Native) { + orders.push(createNativeOrder(collapsedPath[i])); + ++i; + continue; + } + // If there are contiguous bridge orders, we can batch them together. + const contiguousBridgeFills = [collapsedPath[i]]; + for (let j = i + 1; j < collapsedPath.length; ++j) { + if (collapsedPath[j].source === ERC20BridgeSource.Native) { + break; + } + contiguousBridgeFills.push(collapsedPath[j]); + } + if (contiguousBridgeFills.length === 1 || !opts.shouldBatchBridgeOrders) { + orders.push(createBridgeOrder(contiguousBridgeFills[0], opts)); + i += 1; } else { - orders.push(createBridgeOrder(fill, opts)); + orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts)); + i += contiguousBridgeFills.length; } } return orders; @@ -161,8 +173,7 @@ function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrder } function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): OptimizedMarketOrder { - const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken; - const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken; + const [makerToken, takerToken] = getMakerTakerTokens(opts); const bridgeAddress = getBridgeAddressFromSource(fill.source, opts); let makerAssetData; @@ -182,14 +193,66 @@ function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): createBridgeData(takerToken), ); } + const [slippedMakerAssetAmount, slippedTakerAssetAmount] = getSlippedBridgeAssetAmounts(fill, opts); return { - makerAddress: bridgeAddress, + fills: [fill], makerAssetData, takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken), - ...createCommonBridgeOrderFields(fill, opts), + makerAddress: bridgeAddress, + makerAssetAmount: slippedMakerAssetAmount, + takerAssetAmount: slippedTakerAssetAmount, + fillableMakerAssetAmount: slippedMakerAssetAmount, + fillableTakerAssetAmount: slippedTakerAssetAmount, + ...createCommonBridgeOrderFields(opts), }; } +function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder { + const [makerToken, takerToken] = getMakerTakerTokens(opts); + let totalMakerAssetAmount = ZERO_AMOUNT; + let totalTakerAssetAmount = ZERO_AMOUNT; + const batchedBridgeData: DexForwaderBridgeData = { + inputToken: takerToken, + calls: [], + }; + for (const fill of fills) { + const bridgeOrder = createBridgeOrder(fill, opts); + totalMakerAssetAmount = totalMakerAssetAmount.plus(bridgeOrder.makerAssetAmount); + totalTakerAssetAmount = totalTakerAssetAmount.plus(bridgeOrder.takerAssetAmount); + const { bridgeAddress, bridgeData: orderBridgeData } = + assetDataUtils.decodeAssetDataOrThrow(bridgeOrder.makerAssetData) as ERC20BridgeAssetData; + batchedBridgeData.calls.push({ + target: bridgeAddress, + bridgeData: orderBridgeData, + inputTokenAmount: bridgeOrder.takerAssetAmount, + outputTokenAmount: bridgeOrder.makerAssetAmount, + }); + } + const batchedBridgeAddress = opts.contractAddresses.dexForwarderBridge; + const batchedMakerAssetData = assetDataUtils.encodeERC20BridgeAssetData( + makerToken, + batchedBridgeAddress, + dexForwarderBridgeDataEncoder.encode(batchedBridgeData), + ); + return { + fills, + makerAssetData: batchedMakerAssetData, + takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken), + makerAddress: batchedBridgeAddress, + makerAssetAmount: totalMakerAssetAmount, + takerAssetAmount: totalTakerAssetAmount, + fillableMakerAssetAmount: totalMakerAssetAmount, + fillableTakerAssetAmount: totalTakerAssetAmount, + ...createCommonBridgeOrderFields(opts), + }; +} + +function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] { + const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken; + const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken; + return [makerToken, takerToken]; +} + function createBridgeData(tokenAddress: string): string { const encoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]); return encoder.encode({ tokenAddress }); @@ -210,22 +273,36 @@ function createCurveBridgeData( return curveBridgeDataEncoder.encode([curveAddress, fromTokenIdx, toTokenIdx, version]); } +function getSlippedBridgeAssetAmounts(fill: CollapsedFill, opts: CreateOrderFromPathOpts): [BigNumber, BigNumber] { + return [ + // Maker asset amount. + opts.side === MarketOperation.Sell + ? fill.output.times(1 - opts.bridgeSlippage).integerValue(BigNumber.ROUND_DOWN) + : fill.input, + // Taker asset amount. + opts.side === MarketOperation.Sell + ? fill.input + : fill.output.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP), + ]; +} + type CommonBridgeOrderFields = Pick< OptimizedMarketOrder, - Exclude + Exclude< + keyof OptimizedMarketOrder, + 'fills' + | 'makerAddress' + | 'makerAssetData' + | 'takerAssetData' + | 'makerAssetAmount' + | 'takerAssetAmount' + | 'fillableMakerAssetAmount' + | 'fillableTakerAssetAmount' + > >; -function createCommonBridgeOrderFields(fill: CollapsedFill, opts: CreateOrderFromPathOpts): CommonBridgeOrderFields { - const makerAssetAmountAdjustedWithSlippage = - opts.side === MarketOperation.Sell - ? fill.totalMakerAssetAmount.times(1 - opts.bridgeSlippage).integerValue(BigNumber.ROUND_DOWN) - : fill.totalMakerAssetAmount; - const takerAssetAmountAdjustedWithSlippage = - opts.side === MarketOperation.Sell - ? fill.totalTakerAssetAmount - : fill.totalTakerAssetAmount.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP); +function createCommonBridgeOrderFields(opts: CreateOrderFromPathOpts): CommonBridgeOrderFields { return { - fill, takerAddress: NULL_ADDRESS, senderAddress: NULL_ADDRESS, feeRecipientAddress: NULL_ADDRESS, @@ -235,10 +312,6 @@ function createCommonBridgeOrderFields(fill: CollapsedFill, opts: CreateOrderFro takerFeeAssetData: NULL_BYTES, makerFee: ZERO_AMOUNT, takerFee: ZERO_AMOUNT, - makerAssetAmount: makerAssetAmountAdjustedWithSlippage, - fillableMakerAssetAmount: makerAssetAmountAdjustedWithSlippage, - takerAssetAmount: takerAssetAmountAdjustedWithSlippage, - fillableTakerAssetAmount: takerAssetAmountAdjustedWithSlippage, fillableTakerFeeAmount: ZERO_AMOUNT, signature: WALLET_SIGNATURE, ...opts.orderDomain, @@ -247,12 +320,7 @@ function createCommonBridgeOrderFields(fill: CollapsedFill, opts: CreateOrderFro function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder { return { - fill: { - source: fill.source, - totalMakerAssetAmount: fill.totalMakerAssetAmount, - totalTakerAssetAmount: fill.totalTakerAssetAmount, - subFills: fill.subFills, - }, + fills: [fill], ...(fill as NativeCollapsedFill).nativeOrder, }; } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index 323297c7ca..b2d372a6e3 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -97,19 +97,19 @@ export interface CollapsedFill { */ source: ERC20BridgeSource; /** - * Total maker asset amount. + * Total input amount (sum of `subFill`s) */ - totalMakerAssetAmount: BigNumber; + input: BigNumber; /** - * Total taker asset amount. + * Total output amount (sum of `subFill`s) */ - totalTakerAssetAmount: BigNumber; + output: BigNumber; /** - * All the fill asset amounts that were collapsed into this node. + * Quantities of all the fills that were collapsed. */ subFills: Array<{ - makerAssetAmount: BigNumber; - takerAssetAmount: BigNumber; + input: BigNumber; + output: BigNumber; }>; } @@ -127,7 +127,7 @@ export interface OptimizedMarketOrder extends SignedOrderWithFillableAmounts { /** * The optimized fills that generated this order. */ - fill: CollapsedFill; + fills: CollapsedFill[]; } /** @@ -180,9 +180,14 @@ export interface GetMarketOrdersOpts { gasSchedule: { [source: string]: number }; /** * Whether to pad the quote with a redundant fallback quote using different - * sources. + * sources. Defaults to `true`. */ allowFallback: boolean; + /** + * Whether to combine contiguous bridge orders into a single DexForwarderBridge + * order. Defaults to `true`. + */ + shouldBatchBridgeOrders: boolean; } /** diff --git a/packages/asset-swapper/src/utils/protocol_fee_utils.ts b/packages/asset-swapper/src/utils/protocol_fee_utils.ts index a505c7e2c2..f0f3e4458d 100644 --- a/packages/asset-swapper/src/utils/protocol_fee_utils.ts +++ b/packages/asset-swapper/src/utils/protocol_fee_utils.ts @@ -15,12 +15,6 @@ export class ProtocolFeeUtils { this._initializeHeartBeat(); } - // TODO(dave4506) at some point, we should add a heart beat to the multiplier, or some RPC call to fetch latest multiplier. - // tslint:disable-next-line:prefer-function-over-method - public async getProtocolFeeMultiplierAsync(): Promise { - return constants.PROTOCOL_FEE_MULTIPLIER; - } - public async getGasPriceEstimationOrThrowAsync(shouldHardRefresh?: boolean): Promise { if (this.gasPriceEstimation.eq(constants.ZERO_AMOUNT)) { return this._getGasPriceFromGasStationOrThrowAsync(); @@ -39,18 +33,6 @@ export class ProtocolFeeUtils { this._gasPriceHeart.kill(); } - /** - * Calculates protocol fee with protofol fee multiplier for each fill. - */ - public async calculateWorstCaseProtocolFeeAsync( - orders: T[], - gasPrice: BigNumber, - ): Promise { - const protocolFeeMultiplier = await this.getProtocolFeeMultiplierAsync(); - const protocolFee = new BigNumber(orders.length).times(protocolFeeMultiplier).times(gasPrice); - return protocolFee; - } - // tslint:disable-next-line: prefer-function-over-method private async _getGasPriceFromGasStationOrThrowAsync(): Promise { try { diff --git a/packages/asset-swapper/src/utils/quote_simulation.ts b/packages/asset-swapper/src/utils/quote_simulation.ts new file mode 100644 index 0000000000..521f24805b --- /dev/null +++ b/packages/asset-swapper/src/utils/quote_simulation.ts @@ -0,0 +1,344 @@ +import { BigNumber } from '@0x/utils'; + +import { constants } from '../constants'; +import { MarketOperation } from '../types'; + +import { CollapsedFill, ERC20BridgeSource, OptimizedMarketOrder } from './market_operation_utils/types'; +import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils'; + +const { PROTOCOL_FEE_MULTIPLIER, ZERO_AMOUNT } = constants; +const { ROUND_DOWN, ROUND_UP } = BigNumber; + +export interface QuoteFillResult { + // Maker asset bought. + makerAssetAmount: BigNumber; + // Taker asset sold. + takerAssetAmount: BigNumber; + // Taker fees that can be paid with the maker asset. + takerFeeMakerAssetAmount: BigNumber; + // Taker fees that can be paid with the taker asset. + takerFeeTakerAssetAmount: BigNumber; + // Total maker asset amount bought (including fees). + totalMakerAssetAmount: BigNumber; + // Total taker asset amount sold (including fees). + totalTakerAssetAmount: BigNumber; + // Protocol fees paid. + protocolFeeAmount: BigNumber; + // (Estimated) gas used. + gas: number; + // Fill amounts by source. + // For sells, this is the taker assets sold. + // For buys, this is the maker assets bought. + fillAmountBySource: { [source: string]: BigNumber }; +} + +interface IntermediateQuoteFillResult { + // Input tokens filled. Taker asset for sells, maker asset for buys. + input: BigNumber; + // Output tokens filled. Maker asset for sells, taker asset for buys. + output: BigNumber; + // Taker fees that can be paid with the output token. + outputFee: BigNumber; + // Taker fees that can be paid with the input token. + inputFee: BigNumber; + // Protocol fees paid. + protocolFee: BigNumber; + // (Estimated) gas used. + gas: number; + // Input amounts filled by sources. + inputBySource: { [source: string]: BigNumber }; +} + +const EMPTY_QUOTE_INTERMEDIATE_FILL_RESULT = { + input: ZERO_AMOUNT, + output: ZERO_AMOUNT, + outputFee: ZERO_AMOUNT, + inputFee: ZERO_AMOUNT, + protocolFee: ZERO_AMOUNT, + gas: 0, +}; + +export interface QuoteFillInfo { + orders: OptimizedMarketOrder[]; + fillAmount: BigNumber; + gasPrice: BigNumber; + side: MarketOperation; + opts: Partial; +} + +export interface QuoteFillInfoOpts { + gasSchedule: { [soruce: string]: number }; + protocolFeeMultiplier: BigNumber; +} + +const DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS: QuoteFillInfoOpts = { + gasSchedule: {}, + protocolFeeMultiplier: PROTOCOL_FEE_MULTIPLIER, +}; + +export interface QuoteFillOrderCall { + order: OptimizedMarketOrder; + // Fillable input amount defined in the order. + fillableOrderInput: BigNumber; + // Fillable fees payable with input token. + // Positive for sells, negative for buys. + fillableOrderInputFee: BigNumber; + // Fillable fees payable with output token. + // Negative for sells, positive for buys. + fillableOrderOutputFee: BigNumber; +} + +// Simulates filling a quote in the best case. +export function simulateBestCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult { + const opts = { + ...DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS, + ...quoteInfo.opts, + }; + const result = fillQuoteOrders( + createBestCaseFillOrderCalls(quoteInfo), + quoteInfo.fillAmount, + quoteInfo.gasPrice.times(opts.protocolFeeMultiplier), + opts.gasSchedule, + ); + return fromIntermediateQuoteFillResult(result, quoteInfo); +} + +// Simulates filling a quote in the worst case. +export function simulateWorstCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult { + const opts = { + ...DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS, + ...quoteInfo.opts, + }; + const protocolFeePerFillOrder = quoteInfo.gasPrice.times(opts.protocolFeeMultiplier); + const result = { + ...fillQuoteOrders( + createWorstCaseFillOrderCalls(quoteInfo), + quoteInfo.fillAmount, + protocolFeePerFillOrder, + opts.gasSchedule, + ), + // Worst case gas and protocol fee is hitting all orders. + gas: getTotalGasUsedBySources( + getFlattenedFillsFromOrders(quoteInfo.orders).map(s => s.source), + opts.gasSchedule, + ), + protocolFee: protocolFeePerFillOrder.times(quoteInfo.orders.length), + }; + return fromIntermediateQuoteFillResult(result, quoteInfo); +} + +export function fillQuoteOrders( + fillOrders: QuoteFillOrderCall[], + inputAmount: BigNumber, + protocolFeePerFillOrder: BigNumber, + gasSchedule: { [source: string]: number }, +): IntermediateQuoteFillResult { + const result: IntermediateQuoteFillResult = { + ...EMPTY_QUOTE_INTERMEDIATE_FILL_RESULT, + inputBySource: {}, + }; + let remainingInput = inputAmount; + for (const fo of fillOrders) { + if (remainingInput.lte(0)) { + break; + } + for (const fill of fo.order.fills) { + if (remainingInput.lte(0)) { + break; + } + const { source } = fill; + result.gas += gasSchedule[source] || 0; + result.inputBySource[source] = result.inputBySource[source] || ZERO_AMOUNT; + + // Actual rates are rarely linear, so fill subfills individually to + // get a better approximation of fill size. + for (const subFill of fill.subFills) { + if (remainingInput.lte(0)) { + break; + } + const filledInput = solveForInputFillAmount( + remainingInput, + subFill.input, + fo.fillableOrderInput, + fo.fillableOrderInputFee, + ); + const filledOutput = subFill.output.times(filledInput.div(subFill.input)); + + result.inputBySource[source] = result.inputBySource[source].plus(filledInput); + result.input = result.input + .plus(filledInput); + result.output = result.input + .plus(filledOutput); + const orderFillFrac = filledInput.div(fo.fillableOrderInput); + result.inputFee = result.inputFee + .plus(orderFillFrac.times(fo.fillableOrderInputFee)); + result.outputFee = result.outputFee + .plus(orderFillFrac.times(fo.fillableOrderOutputFee)); + remainingInput = inputAmount + .minus(result.input.plus(result.inputFee)); + } + } + result.protocolFee = result.protocolFee.plus(protocolFeePerFillOrder); + } + return result; +} + +function solveForInputFillAmount( + remainingInput: BigNumber, + fillableInput: BigNumber, + fillableOrderInput: BigNumber, + fillableOrderInputFee: BigNumber, +): BigNumber { + // When accounting for input token taker fees, the effective input amount is + // given by: + // i' = i + f * i / o + // where: + // i' - The effective input amount, including fees + // i - An input amount + // f - fillableOrderInputFee + // o - fillableOrderInput + // Solving for i we get: + // i = (i' * o) / (f + o) + const denom = fillableOrderInput.plus(fillableOrderInputFee); + if (denom.lte(0)) { + // A zero denominator would imply an order whose fees are >= the input + // token amount. + // For sells, takerFeeAmount >= takerAssetAmount (technically OK but really undesirable). + // For buys, takerFeeAmount >= makerAssetAmount (losing all your returns to fees). + throw new Error(`Cannot solve for input amount with order input ${fillableOrderInput} and order fee ${fillableOrderInputFee}.`); + } + // i' = remainingInput + return BigNumber.min(fillableInput, remainingInput.times(fillableOrderInput).div(denom)); +} + +function createBestCaseFillOrderCalls(quoteInfo: QuoteFillInfo): QuoteFillOrderCall[] { + const { orders, side } = quoteInfo; + return orders.map(o => ({ + order: o, + ...(side === MarketOperation.Sell + ? { + fillableOrderInput: o.fillableTakerAssetAmount, + fillableOrderInputFee: isOrderTakerFeePayableWithTakerAsset(o) + ? o.fillableTakerFeeAmount + : ZERO_AMOUNT, + fillableOrderOutputFee: isOrderTakerFeePayableWithMakerAsset(o) + ? o.fillableTakerFeeAmount.negated() + : ZERO_AMOUNT, + } + // Buy + : { + fillableOrderInput: o.fillableMakerAssetAmount, + fillableOrderInputFee: isOrderTakerFeePayableWithMakerAsset(o) + ? o.fillableTakerFeeAmount.negated() + : ZERO_AMOUNT, + fillableOrderOutputFee: isOrderTakerFeePayableWithTakerAsset(o) + ? o.fillableTakerFeeAmount + : ZERO_AMOUNT, + } + ), + })); +} + +function createWorstCaseFillOrderCalls(quoteInfo: QuoteFillInfo): QuoteFillOrderCall[] { + // Reuse best case fill orders. + return createBestCaseFillOrderCalls(quoteInfo).map(fo => ({ + ...fo, + order: { + ...fo.order, + // Apply slippage to order fills and reverse them. + fills: getSlippedOrderFills(fo.order, quoteInfo.side).reverse(), + }, + // Reverse the orders. + })).reverse(); +} + +// Apply order slippage to its fill paths. +function getSlippedOrderFills(order: OptimizedMarketOrder, side: MarketOperation): CollapsedFill[] { + const totalInput = BigNumber.sum(...order.fills.map(f => f.input)); + const totalOutput = BigNumber.sum(...order.fills.map(f => f.output)); + const inputScaling = side === MarketOperation.Sell + ? order.fillableTakerAssetAmount.div(totalInput) // Should be 1 + : order.fillableMakerAssetAmount.div(totalOutput); + const outputScaling = side === MarketOperation.Sell + ? order.fillableMakerAssetAmount.div(totalOutput) + : order.fillableTakerAssetAmount.div(totalInput); // Should be 1 + return order.fills.map(f => ({ + ...f, + input: f.input.times(inputScaling), + output: f.output.times(outputScaling), + subFills: f.subFills.map(sf => ({ + ...sf, + input: sf.input.times(inputScaling), + output: sf.output.times(outputScaling), + })), + })); +} + +function fromIntermediateQuoteFillResult( + ir: IntermediateQuoteFillResult, + quoteInfo: QuoteFillInfo, +): QuoteFillResult { + const { side } = quoteInfo; + // Round to integers. + const inputRounding = side === MarketOperation.Sell + ? ROUND_UP : ROUND_DOWN; + const outputRounding = side === MarketOperation.Sell + ? ROUND_DOWN : ROUND_UP; + const _ir = { + input: ir.input.integerValue(inputRounding), + output: ir.output.integerValue(outputRounding), + inputFee: ir.inputFee.integerValue(inputRounding), + outputFee: ir.outputFee.integerValue(outputRounding), + protocolFee: ir.protocolFee.integerValue(ROUND_UP), + gas: Math.ceil(ir.gas), + inputBySource: Object.assign( + {}, + ...Object.entries(ir.inputBySource) + .map(([k, v]) => ({ [k]: v.integerValue(inputRounding) })), + ), + }; + return { + ...(side === MarketOperation.Sell + // Sell + ? { + makerAssetAmount: _ir.output, + takerAssetAmount: _ir.input, + takerFeeMakerAssetAmount: _ir.outputFee, + takerFeeTakerAssetAmount: _ir.inputFee, + totalMakerAssetAmount: _ir.output.plus(_ir.outputFee), + totalTakerAssetAmount: _ir.input, + } + // Buy + : { + makerAssetAmount: _ir.input, + takerAssetAmount: _ir.output, + takerFeeMakerAssetAmount: _ir.inputFee, + takerFeeTakerAssetAmount: _ir.outputFee, + totalMakerAssetAmount: _ir.input, + totalTakerAssetAmount: _ir.output.plus(_ir.outputFee), + } + ), + protocolFeeAmount: _ir.protocolFee, + gas: _ir.gas, + fillAmountBySource: _ir.inputBySource, + }; +} + +export function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): CollapsedFill[] { + const fills = []; + for (const o of orders) { + fills.push(...o.fills); + } + return fills; +} + +function getTotalGasUsedBySources( + sources: ERC20BridgeSource[], + gasSchedule: { [source: string]: number }, +): number { + let gasUsed = 0; + for (const s of sources) { + gasUsed += gasSchedule[s] || 0; + } + return gasUsed; +} diff --git a/packages/asset-swapper/src/utils/swap_quote_calculator.ts b/packages/asset-swapper/src/utils/swap_quote_calculator.ts index 8f08cc1454..5e164fc5e4 100644 --- a/packages/asset-swapper/src/utils/swap_quote_calculator.ts +++ b/packages/asset-swapper/src/utils/swap_quote_calculator.ts @@ -1,9 +1,8 @@ -import { assetDataUtils, orderCalculationUtils } from '@0x/order-utils'; +import { assetDataUtils } from '@0x/order-utils'; import { AssetProxyId, SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { constants } from '../constants'; import { CalculateSwapQuoteOpts, MarketBuySwapQuote, @@ -17,24 +16,24 @@ import { SwapQuoterError, } from '../types'; -import { fillableAmountsUtils } from './fillable_amounts_utils'; import { MarketOperationUtils } from './market_operation_utils'; import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders'; -import { ERC20BridgeSource, GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types'; -import { ProtocolFeeUtils } from './protocol_fee_utils'; +import { GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types'; import { - isOrderTakerFeePayableWithMakerAsset, - isOrderTakerFeePayableWithTakerAsset, isSupportedAssetDataInOrders, } from './utils'; +import { + QuoteFillResult, + simulateBestCaseFill, + simulateWorstCaseFill, +} from './quote_simulation'; + // TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError? export class SwapQuoteCalculator { - private readonly _protocolFeeUtils: ProtocolFeeUtils; private readonly _marketOperationUtils: MarketOperationUtils; - constructor(protocolFeeUtils: ProtocolFeeUtils, marketOperationUtils: MarketOperationUtils) { - this._protocolFeeUtils = protocolFeeUtils; + constructor(marketOperationUtils: MarketOperationUtils) { this._marketOperationUtils = marketOperationUtils; } @@ -99,7 +98,7 @@ export class SwapQuoteCalculator { batchSignedOrders.map(async (orders, i) => { if (orders) { const { makerAssetData, takerAssetData } = batchPrunedOrders[i][0]; - return this._createSwapQuoteAsync( + return createSwapQuote( makerAssetData, takerAssetData, orders, @@ -163,7 +162,7 @@ export class SwapQuoteCalculator { // assetData information for the result const { makerAssetData, takerAssetData } = prunedOrders[0]; - return this._createSwapQuoteAsync( + return createSwapQuote( makerAssetData, takerAssetData, resultOrders, @@ -173,324 +172,77 @@ export class SwapQuoteCalculator { opts.gasSchedule, ); } - private async _createSwapQuoteAsync( - makerAssetData: string, - takerAssetData: string, - resultOrders: OptimizedMarketOrder[], - operation: MarketOperation, - assetFillAmount: BigNumber, - gasPrice: BigNumber, - gasSchedule: { [source: string]: number }, - ): Promise { - const bestCaseQuoteInfo = await this._calculateQuoteInfoAsync( - resultOrders, - assetFillAmount, - gasPrice, - gasSchedule, - operation, - ); - const worstCaseQuoteInfo = await this._calculateQuoteInfoAsync( - resultOrders, - assetFillAmount, - gasPrice, - gasSchedule, - operation, - true, - ); +} - const breakdown = getSwapQuoteOrdersBreakdown(resultOrders, operation); +function createSwapQuote( + makerAssetData: string, + takerAssetData: string, + resultOrders: OptimizedMarketOrder[], + operation: MarketOperation, + assetFillAmount: BigNumber, + gasPrice: BigNumber, + gasSchedule: { [source: string]: number }, +): SwapQuote { + const bestCaseFillResult = simulateBestCaseFill({ + gasPrice, + orders: resultOrders, + side: operation, + fillAmount: assetFillAmount, + opts: { gasSchedule }, + }); - const quoteBase: SwapQuoteBase = { - takerAssetData, - makerAssetData, - // Remove fill metadata. - orders: resultOrders.map(o => _.omit(o, 'fill')) as SignedOrderWithFillableAmounts[], - bestCaseQuoteInfo, - worstCaseQuoteInfo, - gasPrice, - sourceBreakdown: breakdown, - }; + const worstCaseFillResult = simulateWorstCaseFill({ + gasPrice, + orders: resultOrders, + side: operation, + fillAmount: assetFillAmount, + opts: { gasSchedule }, + }); - if (operation === MarketOperation.Buy) { - return { - ...quoteBase, - type: MarketOperation.Buy, - makerAssetFillAmount: assetFillAmount, - }; - } else { - return { - ...quoteBase, - type: MarketOperation.Sell, - takerAssetFillAmount: assetFillAmount, - }; - } - } + const quoteBase: SwapQuoteBase = { + takerAssetData, + makerAssetData, + // Remove fill metadata. + orders: resultOrders.map(o => _.omit(o, 'fills')) as SignedOrderWithFillableAmounts[], + bestCaseQuoteInfo: fillResultsToQuoteInfo(bestCaseFillResult), + worstCaseQuoteInfo: fillResultsToQuoteInfo(worstCaseFillResult), + gasPrice, + sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource), + }; - // tslint:disable-next-line: prefer-function-over-method - private async _calculateQuoteInfoAsync( - orders: OptimizedMarketOrder[], - assetFillAmount: BigNumber, - gasPrice: BigNumber, - gasSchedule: { [source: string]: number }, - operation: MarketOperation, - worstCase: boolean = false, - ): Promise { + if (operation === MarketOperation.Buy) { return { - ...(operation === MarketOperation.Buy - ? await this._calculateMarketBuyQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase) - : await this._calculateMarketSellQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase)), - gas: getGasUsedByOrders(orders, gasSchedule), + ...quoteBase, + type: MarketOperation.Buy, + makerAssetFillAmount: assetFillAmount, }; - } - - private async _calculateMarketSellQuoteInfoAsync( - orders: OptimizedMarketOrder[], - takerAssetSellAmount: BigNumber, - gasPrice: BigNumber, - worstCase: boolean = false, - ): Promise { - let totalMakerAssetAmount = constants.ZERO_AMOUNT; - let totalTakerAssetAmount = constants.ZERO_AMOUNT; - let totalFeeTakerAssetAmount = constants.ZERO_AMOUNT; - let remainingTakerAssetFillAmount = takerAssetSellAmount; - const filledOrders = [] as OptimizedMarketOrder[]; - const _orders = !worstCase ? orders : orders.slice().reverse(); - for (const order of _orders) { - let makerAssetAmount = constants.ZERO_AMOUNT; - let takerAssetAmount = constants.ZERO_AMOUNT; - let feeTakerAssetAmount = constants.ZERO_AMOUNT; - if (remainingTakerAssetFillAmount.lte(0)) { - break; - } - if (order.fill.source === ERC20BridgeSource.Native) { - const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees( - order, - ); - const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees( - order, - ); - const takerAssetAmountWithFees = BigNumber.min( - remainingTakerAssetFillAmount, - adjustedFillableTakerAssetAmount, - ); - const takerAssetAmountBreakDown = getTakerAssetAmountBreakDown(order, takerAssetAmountWithFees); - takerAssetAmount = takerAssetAmountBreakDown.takerAssetAmount; - feeTakerAssetAmount = takerAssetAmountBreakDown.feeTakerAssetAmount; - makerAssetAmount = takerAssetAmountWithFees - .div(adjustedFillableTakerAssetAmount) - .times(adjustedFillableMakerAssetAmount) - .integerValue(BigNumber.ROUND_DOWN); - } else { - // This is a collapsed bridge order. - // Because collapsed bridge orders actually fill at different rates, - // we can iterate over the uncollapsed fills to get the actual - // asset amounts transfered. - // We can also assume there are no fees and the order is not - // partially filled. - - // Infer the bridge slippage from the difference between the fill - // size and the actual order asset amounts. - const makerAssetBridgeSlippage = !worstCase - ? constants.ONE_AMOUNT - : order.makerAssetAmount.div(order.fill.totalMakerAssetAmount); - const takerAssetBridgeSlippage = !worstCase - ? constants.ONE_AMOUNT - : order.takerAssetAmount.div(order.fill.totalTakerAssetAmount); - // Consecutively fill the subfills in this order. - const subFills = !worstCase ? order.fill.subFills : order.fill.subFills.slice().reverse(); - for (const subFill of subFills) { - if (remainingTakerAssetFillAmount.minus(takerAssetAmount).lte(0)) { - break; - } - const partialTakerAssetAmount = subFill.takerAssetAmount.times(takerAssetBridgeSlippage); - const partialMakerAssetAmount = subFill.makerAssetAmount.times(makerAssetBridgeSlippage); - const partialTakerAssetFillAmount = BigNumber.min( - partialTakerAssetAmount, - remainingTakerAssetFillAmount.minus(takerAssetAmount), - ); - const partialMakerAssetFillAmount = partialTakerAssetFillAmount - .div(partialTakerAssetAmount) - .times(partialMakerAssetAmount) - .integerValue(BigNumber.ROUND_DOWN); - takerAssetAmount = takerAssetAmount.plus(partialTakerAssetFillAmount); - makerAssetAmount = makerAssetAmount.plus(partialMakerAssetFillAmount); - } - } - totalMakerAssetAmount = totalMakerAssetAmount.plus(makerAssetAmount); - totalTakerAssetAmount = totalTakerAssetAmount.plus(takerAssetAmount); - totalFeeTakerAssetAmount = totalFeeTakerAssetAmount.plus(feeTakerAssetAmount); - remainingTakerAssetFillAmount = remainingTakerAssetFillAmount - .minus(takerAssetAmount) - .minus(feeTakerAssetAmount); - filledOrders.push(order); - } - const protocolFeeInWeiAmount = await this._protocolFeeUtils.calculateWorstCaseProtocolFeeAsync( - !worstCase ? filledOrders : orders, - gasPrice, - ); + } else { return { - feeTakerAssetAmount: totalFeeTakerAssetAmount, - takerAssetAmount: totalTakerAssetAmount, - totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount), - makerAssetAmount: totalMakerAssetAmount, - protocolFeeInWeiAmount, - gas: 0, - }; - } - - private async _calculateMarketBuyQuoteInfoAsync( - orders: OptimizedMarketOrder[], - makerAssetBuyAmount: BigNumber, - gasPrice: BigNumber, - worstCase: boolean = false, - ): Promise { - let totalMakerAssetAmount = constants.ZERO_AMOUNT; - let totalTakerAssetAmount = constants.ZERO_AMOUNT; - let totalFeeTakerAssetAmount = constants.ZERO_AMOUNT; - let remainingMakerAssetFillAmount = makerAssetBuyAmount; - const filledOrders = [] as OptimizedMarketOrder[]; - const _orders = !worstCase ? orders : orders.slice().reverse(); - for (const order of _orders) { - let makerAssetAmount = constants.ZERO_AMOUNT; - let takerAssetAmount = constants.ZERO_AMOUNT; - let feeTakerAssetAmount = constants.ZERO_AMOUNT; - if (remainingMakerAssetFillAmount.lte(0)) { - break; - } - if (order.fill.source === ERC20BridgeSource.Native) { - const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees( - order, - ); - const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees( - order, - ); - makerAssetAmount = BigNumber.min(remainingMakerAssetFillAmount, adjustedFillableMakerAssetAmount); - const takerAssetAmountWithFees = makerAssetAmount - .div(adjustedFillableMakerAssetAmount) - .multipliedBy(adjustedFillableTakerAssetAmount) - .integerValue(BigNumber.ROUND_UP); - const takerAssetAmountBreakDown = getTakerAssetAmountBreakDown(order, takerAssetAmountWithFees); - takerAssetAmount = takerAssetAmountBreakDown.takerAssetAmount; - feeTakerAssetAmount = takerAssetAmountBreakDown.feeTakerAssetAmount; - } else { - // This is a collapsed bridge order. - // Because collapsed bridge orders actually fill at different rates, - // we can iterate over the uncollapsed fills to get the actual - // asset amounts transfered. - // We can also assume there are no fees and the order is not - // partially filled. - - // Infer the bridge slippage from the difference between the fill - // size and the actual order asset amounts. - const makerAssetBridgeSlippage = !worstCase - ? constants.ONE_AMOUNT - : order.makerAssetAmount.div(order.fill.totalMakerAssetAmount); - const takerAssetBridgeSlippage = !worstCase - ? constants.ONE_AMOUNT - : order.takerAssetAmount.div(order.fill.totalTakerAssetAmount); - // Consecutively fill the subfills in this order. - const subFills = !worstCase ? order.fill.subFills : order.fill.subFills.slice().reverse(); - for (const subFill of subFills) { - if (remainingMakerAssetFillAmount.minus(makerAssetAmount).lte(0)) { - break; - } - const partialTakerAssetAmount = subFill.takerAssetAmount.times(takerAssetBridgeSlippage); - const partialMakerAssetAmount = subFill.makerAssetAmount.times(makerAssetBridgeSlippage); - const partialMakerAssetFillAmount = BigNumber.min( - partialMakerAssetAmount, - remainingMakerAssetFillAmount.minus(makerAssetAmount), - ); - const partialTakerAssetFillAmount = partialMakerAssetFillAmount - .div(partialMakerAssetAmount) - .times(partialTakerAssetAmount) - .integerValue(BigNumber.ROUND_UP); - takerAssetAmount = takerAssetAmount.plus(partialTakerAssetFillAmount); - makerAssetAmount = makerAssetAmount.plus(partialMakerAssetFillAmount); - } - } - totalMakerAssetAmount = totalMakerAssetAmount.plus(makerAssetAmount); - totalTakerAssetAmount = totalTakerAssetAmount.plus(takerAssetAmount); - totalFeeTakerAssetAmount = totalFeeTakerAssetAmount.plus(feeTakerAssetAmount); - remainingMakerAssetFillAmount = remainingMakerAssetFillAmount.minus(makerAssetAmount); - filledOrders.push(order); - } - const protocolFeeInWeiAmount = await this._protocolFeeUtils.calculateWorstCaseProtocolFeeAsync( - !worstCase ? filledOrders : orders, - gasPrice, - ); - return { - feeTakerAssetAmount: totalFeeTakerAssetAmount, - takerAssetAmount: totalTakerAssetAmount, - totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount), - makerAssetAmount: totalMakerAssetAmount, - protocolFeeInWeiAmount, - gas: 0, + ...quoteBase, + type: MarketOperation.Sell, + takerAssetFillAmount: assetFillAmount, }; } } function getSwapQuoteOrdersBreakdown( - orders: OptimizedMarketOrder[], - operation: MarketOperation, + fillAmountBySource: { [source: string]: BigNumber }, ): SwapQuoteOrdersBreakdown { - const orderAmounts = - operation === MarketOperation.Buy - ? orders.map(o => o.fill.totalMakerAssetAmount) - : orders.map(o => o.fill.totalTakerAssetAmount); - const amountsBySource: SwapQuoteOrdersBreakdown = {}; - orders.forEach((o, i) => { - const source = o.fill.source; - amountsBySource[source] = orderAmounts[i].plus(amountsBySource[source] || 0); - }); - const totalAmount = BigNumber.sum(0, ...orderAmounts); + const totalFillAmount = BigNumber.sum(...Object.values(fillAmountBySource)); const breakdown: SwapQuoteOrdersBreakdown = {}; - for (const [source, amount] of Object.entries(amountsBySource)) { - breakdown[source] = amount.div(totalAmount); - } + Object.entries(fillAmountBySource).forEach(([source, fillAmount]) => { + breakdown[source] = fillAmount.div(totalFillAmount); + }); return breakdown; } -function getTakerAssetAmountBreakDown( - order: SignedOrderWithFillableAmounts, - takerAssetAmountWithFees: BigNumber, -): { feeTakerAssetAmount: BigNumber; takerAssetAmount: BigNumber } { - if (isOrderTakerFeePayableWithTakerAsset(order)) { - const adjustedTakerAssetAmount = order.takerAssetAmount.plus(order.takerFee); - const filledRatio = takerAssetAmountWithFees.div(adjustedTakerAssetAmount); - const takerAssetAmount = filledRatio.multipliedBy(order.takerAssetAmount).integerValue(BigNumber.ROUND_CEIL); - return { - takerAssetAmount, - feeTakerAssetAmount: takerAssetAmountWithFees.minus(takerAssetAmount), - }; - } else if (isOrderTakerFeePayableWithMakerAsset(order)) { - if (takerAssetAmountWithFees.isZero()) { - return { - takerAssetAmount: constants.ZERO_AMOUNT, - feeTakerAssetAmount: constants.ZERO_AMOUNT, - }; - } - const takerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, takerAssetAmountWithFees); - const makerAssetFillAmount = orderCalculationUtils.getMakerFillAmount(order, takerAssetAmountWithFees); - const takerAssetAmount = takerFeeAmount - .div(makerAssetFillAmount) - .multipliedBy(takerAssetAmountWithFees) - .integerValue(BigNumber.ROUND_UP); - return { - takerAssetAmount, - feeTakerAssetAmount: takerAssetAmountWithFees.minus(takerAssetAmount), - }; - } +function fillResultsToQuoteInfo(fr: QuoteFillResult): SwapQuoteInfo { return { - feeTakerAssetAmount: constants.ZERO_AMOUNT, - takerAssetAmount: takerAssetAmountWithFees, + makerAssetAmount: fr.totalMakerAssetAmount, + takerAssetAmount: fr.takerAssetAmount, + totalTakerAssetAmount: fr.totalTakerAssetAmount, + feeTakerAssetAmount: fr.takerFeeTakerAssetAmount, + protocolFeeInWeiAmount: fr.protocolFeeAmount, + gas: fr.gas, }; } - -function getGasUsedByOrders(orders: OptimizedMarketOrder[], gasSchedule: { [source: string]: number }): number { - let totalUsage = 0; - for (const order of orders) { - totalUsage += gasSchedule[order.fill.source] || 0; - } - return totalUsage; -} -// tslint:disable: max-file-line-count diff --git a/packages/asset-swapper/test/exchange_swap_quote_consumer_test.ts b/packages/asset-swapper/test/exchange_swap_quote_consumer_test.ts index 3321aaac5f..06254a924b 100644 --- a/packages/asset-swapper/test/exchange_swap_quote_consumer_test.ts +++ b/packages/asset-swapper/test/exchange_swap_quote_consumer_test.ts @@ -12,7 +12,6 @@ import { SwapQuote } from '../src'; import { constants } from '../src/constants'; import { ExchangeSwapQuoteConsumer } from '../src/quote_consumers/exchange_swap_quote_consumer'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types'; -import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils'; import { chaiSetup } from './utils/chai_setup'; import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote'; @@ -60,7 +59,6 @@ const expectMakerAndTakerBalancesAsyncFactory = ( }; describe('ExchangeSwapQuoteConsumer', () => { - let protocolFeeUtils: ProtocolFeeUtils; let userAddresses: string[]; let erc20MakerTokenContract: ERC20TokenContract; let erc20TakerTokenContract: ERC20TokenContract; @@ -123,7 +121,6 @@ describe('ExchangeSwapQuoteConsumer', () => { }; const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; orderFactory = new OrderFactory(privateKey, defaultOrderParams); - protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, new BigNumber(1)); expectMakerAndTakerBalancesForTakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory( erc20TakerTokenContract, makerAddress, @@ -156,7 +153,6 @@ describe('ExchangeSwapQuoteConsumer', () => { orders, MarketOperation.Sell, GAS_PRICE, - protocolFeeUtils, ); marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( @@ -165,7 +161,6 @@ describe('ExchangeSwapQuoteConsumer', () => { orders, MarketOperation.Buy, GAS_PRICE, - protocolFeeUtils, ); swapQuoteConsumer = new ExchangeSwapQuoteConsumer(provider, contractAddresses, { diff --git a/packages/asset-swapper/test/forwarder_swap_quote_consumer_test.ts b/packages/asset-swapper/test/forwarder_swap_quote_consumer_test.ts index 0113fe86ee..3c80e630e3 100644 --- a/packages/asset-swapper/test/forwarder_swap_quote_consumer_test.ts +++ b/packages/asset-swapper/test/forwarder_swap_quote_consumer_test.ts @@ -12,7 +12,6 @@ import { SwapQuote } from '../src'; import { constants } from '../src/constants'; import { ForwarderSwapQuoteConsumer } from '../src/quote_consumers/forwarder_swap_quote_consumer'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types'; -import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils'; import { chaiSetup } from './utils/chai_setup'; import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote'; @@ -61,7 +60,6 @@ const expectMakerAndTakerBalancesAsyncFactory = ( }; describe('ForwarderSwapQuoteConsumer', () => { - let protocolFeeUtils: ProtocolFeeUtils; let userAddresses: string[]; let coinbaseAddress: string; let makerAddress: string; @@ -126,7 +124,6 @@ describe('ForwarderSwapQuoteConsumer', () => { }; const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; orderFactory = new OrderFactory(privateKey, defaultOrderParams); - protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, new BigNumber(1)); expectMakerAndTakerBalancesAsync = expectMakerAndTakerBalancesAsyncFactory( erc20TokenContract, makerAddress, @@ -179,7 +176,6 @@ describe('ForwarderSwapQuoteConsumer', () => { orders, MarketOperation.Sell, GAS_PRICE, - protocolFeeUtils, ); marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( @@ -188,7 +184,6 @@ describe('ForwarderSwapQuoteConsumer', () => { orders, MarketOperation.Buy, GAS_PRICE, - protocolFeeUtils, ); invalidMarketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( @@ -197,7 +192,6 @@ describe('ForwarderSwapQuoteConsumer', () => { invalidOrders, MarketOperation.Buy, GAS_PRICE, - protocolFeeUtils, ); swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, contractAddresses, { diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 87a809204c..b731bac836 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -299,6 +299,7 @@ describe('MarketOperationUtils tests', () => { maxFallbackSlippage: 100, excludedSources: Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[], allowFallback: false, + shouldBatchBridgeOrders: false, }; beforeEach(() => { @@ -422,7 +423,7 @@ describe('MarketOperationUtils tests', () => { ); expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { - const expectedMakerAmount = order.fill.totalMakerAssetAmount; + const expectedMakerAmount = order.fills[0].output; const slippage = 1 - order.makerAssetAmount.div(expectedMakerAmount.plus(1)).toNumber(); assertRoughlyEquals(slippage, bridgeSlippage, 1); } @@ -442,7 +443,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, @@ -466,7 +467,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); if (orderSources.includes(ERC20BridgeSource.Kyber)) { expect(orderSources).to.not.include(ERC20BridgeSource.Uniswap); expect(orderSources).to.not.include(ERC20BridgeSource.Eth2Dai); @@ -501,7 +502,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap, @@ -536,7 +537,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.Eth2Dai, @@ -561,7 +562,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, @@ -584,7 +585,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const firstSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.Native, @@ -610,7 +611,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.5 }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap]; const secondSources: ERC20BridgeSource[] = []; expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); @@ -672,6 +673,40 @@ describe('MarketOperationUtils tests', () => { expect(getLiquidityProviderParams.makerToken).is.eql(yAsset); expect(getLiquidityProviderParams.takerToken).is.eql(xAsset); }); + + it('batches contiguous bridge sources', async () => { + const rates: RatesBySource = {}; + rates[ERC20BridgeSource.Uniswap] = [1, 0.01, 0.01, 0.01]; + rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; + rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01]; + rates[ERC20BridgeSource.CurveUsdcDai] = [0.48, 0.01, 0.01, 0.01]; + replaceSamplerOps({ + getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), + }); + const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( + createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), + FILL_AMOUNT, + { + ...DEFAULT_OPTS, + numSamples: 4, + excludedSources: [ + ERC20BridgeSource.Kyber, + ..._.without(DEFAULT_OPTS.excludedSources, ERC20BridgeSource.CurveUsdcDai), + ] as ERC20BridgeSource[], + shouldBatchBridgeOrders: true, + }, + ); + expect(improvedOrders).to.be.length(3); + const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); + expect(orderFillSources).to.deep.eq([ + [ERC20BridgeSource.Uniswap], + [ERC20BridgeSource.Native], + [ + ERC20BridgeSource.Eth2Dai, + ERC20BridgeSource.CurveUsdcDai, + ], + ]); + }); }); describe('getMarketBuyOrdersAsync()', () => { @@ -687,6 +722,7 @@ describe('MarketOperationUtils tests', () => { maxFallbackSlippage: 100, excludedSources: Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[], allowFallback: false, + shouldBatchBridgeOrders: false, }; beforeEach(() => { @@ -789,7 +825,7 @@ describe('MarketOperationUtils tests', () => { } }); - it('generates bridge orders with correct taker amount', async () => { + it('generates bridge orders with correct maker amount', async () => { const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( // Pass in empty orders to prevent native orders from being used. ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), @@ -810,7 +846,7 @@ describe('MarketOperationUtils tests', () => { ); expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { - const expectedTakerAmount = order.fill.totalTakerAssetAmount; + const expectedTakerAmount = order.fills[0].output; const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).toNumber() - 1; assertRoughlyEquals(slippage, bridgeSlippage, 1); } @@ -829,7 +865,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, @@ -865,7 +901,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai, @@ -899,7 +935,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.Eth2Dai, @@ -921,7 +957,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const firstSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.Native, @@ -946,12 +982,40 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.5 }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap]; const secondSources: ERC20BridgeSource[] = []; expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort()); }); + + it('batches contiguous bridge sources', async () => { + const rates: RatesBySource = {}; + rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; + rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01]; + rates[ERC20BridgeSource.Uniswap] = [0.48, 0.47, 0.01, 0.01]; + replaceSamplerOps({ + getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), + }); + const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( + createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), + FILL_AMOUNT, + { + ...DEFAULT_OPTS, + numSamples: 4, + shouldBatchBridgeOrders: true, + }, + ); + expect(improvedOrders).to.be.length(2); + const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); + expect(orderFillSources).to.deep.eq([ + [ERC20BridgeSource.Native], + [ + ERC20BridgeSource.Eth2Dai, + ERC20BridgeSource.Uniswap, + ], + ]); + }); }); }); }); diff --git a/packages/asset-swapper/test/swap_quote_consumer_utils_test.ts b/packages/asset-swapper/test/swap_quote_consumer_utils_test.ts index 5c80acaee0..3fe2fe4919 100644 --- a/packages/asset-swapper/test/swap_quote_consumer_utils_test.ts +++ b/packages/asset-swapper/test/swap_quote_consumer_utils_test.ts @@ -11,7 +11,6 @@ import 'mocha'; import { SwapQuote, SwapQuoteConsumer } from '../src'; import { constants } from '../src/constants'; import { ExtensionContractType, MarketOperation, SignedOrderWithFillableAmounts } from '../src/types'; -import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils'; import { chaiSetup } from './utils/chai_setup'; import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote'; @@ -69,7 +68,6 @@ const PARTIAL_LARGE_PRUNED_SIGNED_ORDERS: Array { let wethContract: WETH9Contract; - let protocolFeeUtils: ProtocolFeeUtils; let userAddresses: string[]; let makerAddress: string; let takerAddress: string; @@ -119,7 +117,6 @@ describe('swapQuoteConsumerUtils', () => { }; const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; orderFactory = new OrderFactory(privateKey, defaultOrderParams); - protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, new BigNumber(1)); forwarderOrderFactory = new OrderFactory(privateKey, defaultForwarderOrderParams); swapQuoteConsumer = new SwapQuoteConsumer(provider, { @@ -128,7 +125,6 @@ describe('swapQuoteConsumerUtils', () => { }); after(async () => { await blockchainLifecycle.revertAsync(); - await protocolFeeUtils.destroyAsync(); }); beforeEach(async () => { await blockchainLifecycle.startAsync(); @@ -182,7 +178,6 @@ describe('swapQuoteConsumerUtils', () => { forwarderOrders, MarketOperation.Sell, GAS_PRICE, - protocolFeeUtils, ); largeForwarderSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( @@ -191,7 +186,6 @@ describe('swapQuoteConsumerUtils', () => { largeForwarderOrders, MarketOperation.Sell, GAS_PRICE, - protocolFeeUtils, ); exchangeSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( @@ -200,7 +194,6 @@ describe('swapQuoteConsumerUtils', () => { exchangeOrders, MarketOperation.Sell, GAS_PRICE, - protocolFeeUtils, ); }); diff --git a/packages/asset-swapper/test/utils/mocks.ts b/packages/asset-swapper/test/utils/mocks.ts index 4b21904040..d7be2985ec 100644 --- a/packages/asset-swapper/test/utils/mocks.ts +++ b/packages/asset-swapper/test/utils/mocks.ts @@ -57,10 +57,6 @@ const partiallyMockedSwapQuoter = (provider: Web3ProviderEngine, orderbook: Orde }; class ProtocolFeeUtilsClass extends ProtocolFeeUtils { - // tslint:disable-next-line:prefer-function-over-method - public async getProtocolFeeMultiplierAsync(): Promise { - return new BigNumber(PROTOCOL_FEE_MULTIPLIER); - } // tslint:disable-next-line:prefer-function-over-method public async getGasPriceEstimationOrThrowAsync(_shouldHardRefresh?: boolean): Promise { return new BigNumber(devConstants.DEFAULT_GAS_PRICE); diff --git a/packages/asset-swapper/test/utils/swap_quote.ts b/packages/asset-swapper/test/utils/swap_quote.ts index c347602961..de3bde6e72 100644 --- a/packages/asset-swapper/test/utils/swap_quote.ts +++ b/packages/asset-swapper/test/utils/swap_quote.ts @@ -4,7 +4,6 @@ import * as _ from 'lodash'; import { ERC20BridgeSource } from '../../src'; import { constants } from '../../src/constants'; import { MarketOperation, SignedOrderWithFillableAmounts, SwapQuote } from '../../src/types'; -import { ProtocolFeeUtils } from '../../src/utils/protocol_fee_utils'; /** * Creates a swap quote given orders. @@ -15,16 +14,16 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync( orders: SignedOrderWithFillableAmounts[], operation: MarketOperation, gasPrice: BigNumber, - protocolFeeUtils: ProtocolFeeUtils, ): Promise { const makerAssetFillAmount = BigNumber.sum(...[0, ...orders.map(o => o.makerAssetAmount)]); const totalTakerAssetAmount = BigNumber.sum(...[0, ...orders.map(o => o.takerAssetAmount)]); + const protocolFeePerOrder = constants.PROTOCOL_FEE_MULTIPLIER.times(gasPrice); const quoteInfo = { makerAssetAmount: makerAssetFillAmount, feeTakerAssetAmount: constants.ZERO_AMOUNT, takerAssetAmount: totalTakerAssetAmount, totalTakerAssetAmount, - protocolFeeInWeiAmount: await protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(orders, gasPrice), + protocolFeeInWeiAmount: protocolFeePerOrder.times(orders.length), gas: 200e3, }; From 4df81a0b9e16f83827857cf603aedeb1b358109b Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 2 Apr 2020 18:03:11 -0400 Subject: [PATCH 02/13] `@0x/asset-swapper`: add quote fill simulation tests --- packages/asset-swapper/package.json | 1 + .../src/utils/quote_simulation.ts | 132 ++- .../test/quote_simulation_test.ts | 964 ++++++++++++++++++ 3 files changed, 1042 insertions(+), 55 deletions(-) create mode 100644 packages/asset-swapper/test/quote_simulation_test.ts diff --git a/packages/asset-swapper/package.json b/packages/asset-swapper/package.json index 3df0a94cb7..2962024d82 100644 --- a/packages/asset-swapper/package.json +++ b/packages/asset-swapper/package.json @@ -48,6 +48,7 @@ "@0x/assert": "^3.0.7", "@0x/contract-addresses": "^4.9.0", "@0x/contract-wrappers": "^13.6.3", + "@0x/contracts-asset-proxy": "^3.2.5", "@0x/json-schemas": "^5.0.7", "@0x/order-utils": "^10.2.4", "@0x/orderbook": "^2.2.5", diff --git a/packages/asset-swapper/src/utils/quote_simulation.ts b/packages/asset-swapper/src/utils/quote_simulation.ts index 521f24805b..1363da5d12 100644 --- a/packages/asset-swapper/src/utils/quote_simulation.ts +++ b/packages/asset-swapper/src/utils/quote_simulation.ts @@ -37,10 +37,12 @@ interface IntermediateQuoteFillResult { input: BigNumber; // Output tokens filled. Maker asset for sells, taker asset for buys. output: BigNumber; - // Taker fees that can be paid with the output token. - outputFee: BigNumber; // Taker fees that can be paid with the input token. + // Positive for sells, negative for buys. inputFee: BigNumber; + // Taker fees that can be paid with the output token. + // Negative for sells, positive for buys. + outputFee: BigNumber; // Protocol fees paid. protocolFee: BigNumber; // (Estimated) gas used. @@ -78,14 +80,16 @@ const DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS: QuoteFillInfoOpts = { export interface QuoteFillOrderCall { order: OptimizedMarketOrder; - // Fillable input amount defined in the order. - fillableOrderInput: BigNumber; - // Fillable fees payable with input token. + // Total input amount defined in the order. + totalOrderInput: BigNumber; + // Total output amount defined in the order. + totalOrderOutput: BigNumber; + // Total fees payable with input token, defined in the order. // Positive for sells, negative for buys. - fillableOrderInputFee: BigNumber; - // Fillable fees payable with output token. + totalOrderInputFee: BigNumber; + // Total fees payable with output token, defined in the order. // Negative for sells, positive for buys. - fillableOrderOutputFee: BigNumber; + totalOrderOutputFee: BigNumber; } // Simulates filling a quote in the best case. @@ -95,6 +99,7 @@ export function simulateBestCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult ...quoteInfo.opts, }; const result = fillQuoteOrders( + quoteInfo.side, createBestCaseFillOrderCalls(quoteInfo), quoteInfo.fillAmount, quoteInfo.gasPrice.times(opts.protocolFeeMultiplier), @@ -112,6 +117,7 @@ export function simulateWorstCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult const protocolFeePerFillOrder = quoteInfo.gasPrice.times(opts.protocolFeeMultiplier); const result = { ...fillQuoteOrders( + quoteInfo.side, createWorstCaseFillOrderCalls(quoteInfo), quoteInfo.fillAmount, protocolFeePerFillOrder, @@ -128,6 +134,7 @@ export function simulateWorstCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult } export function fillQuoteOrders( + side: MarketOperation, fillOrders: QuoteFillOrderCall[], inputAmount: BigNumber, protocolFeePerFillOrder: BigNumber, @@ -159,23 +166,23 @@ export function fillQuoteOrders( const filledInput = solveForInputFillAmount( remainingInput, subFill.input, - fo.fillableOrderInput, - fo.fillableOrderInputFee, + fo.totalOrderInput, + fo.totalOrderInputFee, ); const filledOutput = subFill.output.times(filledInput.div(subFill.input)); + const filledInputFee = filledInput.div(fo.totalOrderInput).times(fo.totalOrderInputFee); + const filledOutputFee = filledOutput.div(fo.totalOrderOutput).times(fo.totalOrderOutputFee); result.inputBySource[source] = result.inputBySource[source].plus(filledInput); result.input = result.input .plus(filledInput); - result.output = result.input + result.output = result.output .plus(filledOutput); - const orderFillFrac = filledInput.div(fo.fillableOrderInput); result.inputFee = result.inputFee - .plus(orderFillFrac.times(fo.fillableOrderInputFee)); + .plus(filledInputFee); result.outputFee = result.outputFee - .plus(orderFillFrac.times(fo.fillableOrderOutputFee)); - remainingInput = inputAmount - .minus(result.input.plus(result.inputFee)); + .plus(filledOutputFee); + remainingInput = remainingInput.minus(filledInput.plus(filledInputFee)); } } result.protocolFee = result.protocolFee.plus(protocolFeePerFillOrder); @@ -186,8 +193,8 @@ export function fillQuoteOrders( function solveForInputFillAmount( remainingInput: BigNumber, fillableInput: BigNumber, - fillableOrderInput: BigNumber, - fillableOrderInputFee: BigNumber, + totalOrderInput: BigNumber, + totalOrderInputFee: BigNumber, ): BigNumber { // When accounting for input token taker fees, the effective input amount is // given by: @@ -195,20 +202,23 @@ function solveForInputFillAmount( // where: // i' - The effective input amount, including fees // i - An input amount - // f - fillableOrderInputFee - // o - fillableOrderInput + // f - totalOrderInputFee + // o - totalOrderInput // Solving for i we get: // i = (i' * o) / (f + o) - const denom = fillableOrderInput.plus(fillableOrderInputFee); - if (denom.lte(0)) { + const denom = totalOrderInput.plus(totalOrderInputFee); + if (denom.eq(0)) { // A zero denominator would imply an order whose fees are >= the input // token amount. // For sells, takerFeeAmount >= takerAssetAmount (technically OK but really undesirable). // For buys, takerFeeAmount >= makerAssetAmount (losing all your returns to fees). - throw new Error(`Cannot solve for input amount with order input ${fillableOrderInput} and order fee ${fillableOrderInputFee}.`); + return fillableInput; } - // i' = remainingInput - return BigNumber.min(fillableInput, remainingInput.times(fillableOrderInput).div(denom)); + return BigNumber.min( + fillableInput, + // let i' = remainingInput + remainingInput.times(totalOrderInput).div(denom), + ); } function createBestCaseFillOrderCalls(quoteInfo: QuoteFillInfo): QuoteFillOrderCall[] { @@ -217,22 +227,24 @@ function createBestCaseFillOrderCalls(quoteInfo: QuoteFillInfo): QuoteFillOrderC order: o, ...(side === MarketOperation.Sell ? { - fillableOrderInput: o.fillableTakerAssetAmount, - fillableOrderInputFee: isOrderTakerFeePayableWithTakerAsset(o) - ? o.fillableTakerFeeAmount + totalOrderInput: o.takerAssetAmount, + totalOrderOutput: o.makerAssetAmount, + totalOrderInputFee: isOrderTakerFeePayableWithTakerAsset(o) + ? o.takerFee : ZERO_AMOUNT, - fillableOrderOutputFee: isOrderTakerFeePayableWithMakerAsset(o) - ? o.fillableTakerFeeAmount.negated() + totalOrderOutputFee: isOrderTakerFeePayableWithMakerAsset(o) + ? o.takerFee.negated() : ZERO_AMOUNT, } // Buy : { - fillableOrderInput: o.fillableMakerAssetAmount, - fillableOrderInputFee: isOrderTakerFeePayableWithMakerAsset(o) - ? o.fillableTakerFeeAmount.negated() + totalOrderInput: o.makerAssetAmount, + totalOrderOutput: o.takerAssetAmount, + totalOrderInputFee: isOrderTakerFeePayableWithMakerAsset(o) + ? o.takerFee.negated() : ZERO_AMOUNT, - fillableOrderOutputFee: isOrderTakerFeePayableWithTakerAsset(o) - ? o.fillableTakerFeeAmount + totalOrderOutputFee: isOrderTakerFeePayableWithTakerAsset(o) + ? o.takerFee : ZERO_AMOUNT, } ), @@ -257,11 +269,11 @@ function getSlippedOrderFills(order: OptimizedMarketOrder, side: MarketOperation const totalInput = BigNumber.sum(...order.fills.map(f => f.input)); const totalOutput = BigNumber.sum(...order.fills.map(f => f.output)); const inputScaling = side === MarketOperation.Sell - ? order.fillableTakerAssetAmount.div(totalInput) // Should be 1 - : order.fillableMakerAssetAmount.div(totalOutput); + ? order.fillableTakerAssetAmount.div(totalInput) + : order.fillableMakerAssetAmount.div(totalInput); const outputScaling = side === MarketOperation.Sell ? order.fillableMakerAssetAmount.div(totalOutput) - : order.fillableTakerAssetAmount.div(totalInput); // Should be 1 + : order.fillableTakerAssetAmount.div(totalOutput); return order.fills.map(f => ({ ...f, input: f.input.times(inputScaling), @@ -274,29 +286,39 @@ function getSlippedOrderFills(order: OptimizedMarketOrder, side: MarketOperation })); } -function fromIntermediateQuoteFillResult( +function roundInputAmount(amount: BigNumber, side: MarketOperation): BigNumber { + return amount.integerValue(side === MarketOperation.Sell ? ROUND_UP : ROUND_DOWN); +} + +function roundOutputAmount(amount: BigNumber, side: MarketOperation): BigNumber { + return amount.integerValue(side === MarketOperation.Sell ? ROUND_DOWN : ROUND_UP); +} + +function roundIntermediateFillResult( ir: IntermediateQuoteFillResult, - quoteInfo: QuoteFillInfo, -): QuoteFillResult { - const { side } = quoteInfo; - // Round to integers. - const inputRounding = side === MarketOperation.Sell - ? ROUND_UP : ROUND_DOWN; - const outputRounding = side === MarketOperation.Sell - ? ROUND_DOWN : ROUND_UP; - const _ir = { - input: ir.input.integerValue(inputRounding), - output: ir.output.integerValue(outputRounding), - inputFee: ir.inputFee.integerValue(inputRounding), - outputFee: ir.outputFee.integerValue(outputRounding), + side: MarketOperation, +): IntermediateQuoteFillResult { + return { + input: roundInputAmount(ir.input, side), + output: roundOutputAmount(ir.output, side), + inputFee: roundInputAmount(ir.inputFee, side), + outputFee: roundOutputAmount(ir.outputFee, side), protocolFee: ir.protocolFee.integerValue(ROUND_UP), gas: Math.ceil(ir.gas), inputBySource: Object.assign( {}, ...Object.entries(ir.inputBySource) - .map(([k, v]) => ({ [k]: v.integerValue(inputRounding) })), + .map(([k, v]) => ({ [k]: roundInputAmount(v, side) })), ), }; +} + +function fromIntermediateQuoteFillResult( + ir: IntermediateQuoteFillResult, + quoteInfo: QuoteFillInfo, +): QuoteFillResult { + const { side } = quoteInfo; + const _ir = roundIntermediateFillResult(ir, side); return { ...(side === MarketOperation.Sell // Sell @@ -306,7 +328,7 @@ function fromIntermediateQuoteFillResult( takerFeeMakerAssetAmount: _ir.outputFee, takerFeeTakerAssetAmount: _ir.inputFee, totalMakerAssetAmount: _ir.output.plus(_ir.outputFee), - totalTakerAssetAmount: _ir.input, + totalTakerAssetAmount: _ir.input.plus(_ir.inputFee), } // Buy : { @@ -314,7 +336,7 @@ function fromIntermediateQuoteFillResult( takerAssetAmount: _ir.output, takerFeeMakerAssetAmount: _ir.inputFee, takerFeeTakerAssetAmount: _ir.outputFee, - totalMakerAssetAmount: _ir.input, + totalMakerAssetAmount: _ir.input.plus(_ir.inputFee), totalTakerAssetAmount: _ir.output.plus(_ir.outputFee), } ), diff --git a/packages/asset-swapper/test/quote_simulation_test.ts b/packages/asset-swapper/test/quote_simulation_test.ts new file mode 100644 index 0000000000..91db264b31 --- /dev/null +++ b/packages/asset-swapper/test/quote_simulation_test.ts @@ -0,0 +1,964 @@ +import { + assertIntegerRoughlyEquals, + constants, + expect, + getRandomInteger, + randomAddress, +} from '@0x/contracts-test-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { BigNumber } from '@0x/utils'; +import * as _ from 'lodash'; + +import { MarketOperation } from '../src/types'; +import { CollapsedFill, ERC20BridgeSource, OptimizedMarketOrder } from '../src/utils/market_operation_utils/types'; +import { fillQuoteOrders, QuoteFillOrderCall, simulateBestCaseFill, simulateWorstCaseFill } from '../src/utils/quote_simulation'; + +// tslint:disable: custom-no-magic-numbers +const { NULL_ADDRESS } = constants; +const ZERO = new BigNumber(0); +const ONE = new BigNumber(1); + +describe('quote_simulation tests', async () => { + + const MAKER_TOKEN = randomAddress(); + const TAKER_TOKEN = randomAddress(); + const DEFAULT_MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); + const DEFAULT_TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); + const EPS = 1e6; + const GAS_SCHEDULE = { [ERC20BridgeSource.Native]: 1 }; + + function createQuoteFillOrders(opts: Partial<{ + fillableInput: BigNumber; + fillableOutput: BigNumber; + inputFeeRate: number; + outputFeeRate: number; + count: number; + fillsCount: number; + side: MarketOperation; + }> = {}): QuoteFillOrderCall[] { + const { fillableInput, fillableOutput, inputFeeRate, outputFeeRate, count, fillsCount, side } = { + 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(), + }, + ), + 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; + }> = {}): OptimizedMarketOrder { + const { + filledInput, + fillsCount, + side, + takerInputFee, + takerOutputFee, + } = { + side: MarketOperation.Sell, + filledInput: ZERO, + fillsCount: 3, + takerInputFee: ZERO, + takerOutputFee: ZERO, + ...opts, + }; + const filledOutput = filledInput.div(input).times(output).integerValue(BigNumber.ROUND_DOWN); + const fillableInput = input.minus(filledInput); + const fillableOutput = output.minus(filledOutput); + const makerAssetAmount = side === MarketOperation.Sell + ? output : input; + const takerAssetAmount = side === MarketOperation.Sell + ? input : output; + const fillableMakerAssetAmount = side === MarketOperation.Sell + ? fillableOutput : fillableInput; + const fillableTakerAssetAmount = side === MarketOperation.Sell + ? fillableInput : fillableOutput; + const takerFee = BigNumber.max(takerInputFee, takerOutputFee); + let takerFeeAssetData = '0x'; + if (!takerInputFee.eq(0)) { + takerFeeAssetData = side === MarketOperation.Sell + ? DEFAULT_TAKER_ASSET_DATA : DEFAULT_MAKER_ASSET_DATA; + } else if (!takerOutputFee.eq(0)) { + takerFeeAssetData = side === MarketOperation.Sell + ? DEFAULT_MAKER_ASSET_DATA : DEFAULT_TAKER_ASSET_DATA; + } + const fillableTakerFeeAmount = fillableTakerAssetAmount + .div(takerAssetAmount) + .times(takerFee) + .integerValue(BigNumber.ROUND_DOWN); + return { + makerAssetAmount, + takerAssetAmount, + fillableTakerAssetAmount, + fillableMakerAssetAmount, + fillableTakerFeeAmount, + takerFee, + takerFeeAssetData, + fills: createOrderCollapsedFills(fillableInput, fillableOutput, fillsCount), + chainId: 1, + exchangeAddress: NULL_ADDRESS, + expirationTimeSeconds: ZERO, + feeRecipientAddress: NULL_ADDRESS, + senderAddress: NULL_ADDRESS, + makerAddress: NULL_ADDRESS, + takerAddress: NULL_ADDRESS, + makerAssetData: DEFAULT_MAKER_ASSET_DATA, + takerAssetData: DEFAULT_TAKER_ASSET_DATA, + makerFeeAssetData: '0x', + salt: ZERO, + makerFee: ZERO, + signature: '0x', + }; + } + + 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 { + source: ERC20BridgeSource.Native, + 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(side, 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); + assertIntegerRoughlyEquals(totalFilledOutput, fillableOutput, EPS); + 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(side, 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(); + assertIntegerRoughlyEquals(totalFilledOutput, expectedOutputFilledAmount, EPS); + 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(side, 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(side, 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); + assertIntegerRoughlyEquals(totalFilledOutput, fillableOutput, EPS); + 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(side, fillOrders, totalFillableInput, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, totalFillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, fillableOutput, EPS); + 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(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, inputFillAmount, EPS); + 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(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, totalFillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, fillableOutput, EPS); + 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(side, fillOrders, fillableInput, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, fillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, totalFillableOutput, EPS); + 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(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, inputFillAmount, EPS); + 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(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, fillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, totalFillableOutput, EPS); + assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); + expect(result.protocolFee).to.bignumber.eq(1); + 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(side, 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(side, 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(side, 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(side, fillOrders, totalFillableInput, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, totalFillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, fillableOutput, EPS); + 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(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, inputFillAmount, EPS); + 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(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, totalFillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, fillableOutput, EPS); + 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(side, fillOrders, fillableInput, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, fillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, totalFillableOutput, EPS); + 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(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, inputFillAmount, EPS); + 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(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, fillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, totalFillableOutput, EPS); + 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: OptimizedMarketOrder, + orderSlippage: number, + side: MarketOperation, + ): OptimizedMarketOrder { + const makerScaling = side === MarketOperation.Sell ? 1 - orderSlippage : 1; + const takerScaling = side === MarketOperation.Sell ? 1 : orderSlippage + 1; + return { + ...order, + makerAssetAmount: order.makerAssetAmount.times(makerScaling), + fillableMakerAssetAmount: order.fillableMakerAssetAmount.times(makerScaling), + takerAssetAmount: order.takerAssetAmount.times(takerScaling), + fillableTakerAssetAmount: order.fillableTakerAssetAmount.times(takerScaling), + }; + } + + describe('simulateBestCaseFill()', () => { + it('ignores order slippage', async () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const orderSlippage = getRandomFeeRate(); + const orders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + side, + }).map(fo => slipOrder(fo.order, 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 orders with input fees', async () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const inputFeeRate = getRandomFeeRate(); + const orders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + inputFeeRate, + side, + }).map(fo => fo.order); + const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; + const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue(); + const result = simulateBestCaseFill({ + orders, + side, + fillAmount: totalFillableInput, + gasPrice: ONE, + opts: { gasSchedule: GAS_SCHEDULE }, + }); + expect(result.gas).to.eq(countCollapsedFills(orders)); + expect(result.protocolFeeAmount).to.bignumber.gt(orders.length); + if (side === MarketOperation.Sell) { + assertIntegerRoughlyEquals(result.takerAssetAmount, fillableInput, EPS); + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, totalFillableInput, EPS); + assertIntegerRoughlyEquals(result.makerAssetAmount, fillableOutput, EPS); + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, fillableOutput, EPS); + expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); + expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); + } else { + assertIntegerRoughlyEquals(result.makerAssetAmount, fillableInput, EPS); + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, totalFillableInput, EPS); + assertIntegerRoughlyEquals(result.takerAssetAmount, fillableOutput, EPS); + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, fillableOutput, EPS); + expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount); + expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); + } + }); + + it('can partially fill orders with input fees', async () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const inputFeeRate = getRandomFeeRate(); + const orders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + inputFeeRate, + side, + }).map(fo => fo.order); + const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -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); + if (side === MarketOperation.Sell) { + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, inputFillAmount, EPS); + expect(result.makerAssetAmount).to.bignumber.lt(fillableOutput); + expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); + expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); + } else { + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, inputFillAmount, EPS); + expect(result.takerAssetAmount).to.bignumber.lt(fillableOutput); + expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount); + expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); + } + }); + + it('can fully fill orders with output fees', async () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const outputFeeRate = getRandomFeeRate(); + const orders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + outputFeeRate, + side, + }).map(fo => fo.order); + const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : 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); + if (side === MarketOperation.Sell) { + assertIntegerRoughlyEquals(result.takerAssetAmount, fillableInput, EPS); + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, fillableInput, EPS); + assertIntegerRoughlyEquals(result.makerAssetAmount, fillableOutput, EPS); + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, totalFillableOutput, EPS); + expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount); + expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); + } else { + assertIntegerRoughlyEquals(result.makerAssetAmount, fillableInput, EPS); + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, fillableInput, EPS); + assertIntegerRoughlyEquals(result.takerAssetAmount, fillableOutput, EPS); + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, totalFillableOutput, EPS); + expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); + expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); + } + }); + + it('can partially fill orders with output fees', async () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const outputFeeRate = getRandomFeeRate(); + const orders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + outputFeeRate, + side, + }).map(fo => fo.order); + const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; + const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue(); + 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); + if (side === MarketOperation.Sell) { + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, inputFillAmount, EPS); + expect(result.makerAssetAmount).to.bignumber.lt(fillableOutput); + expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount); + expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); + } else { + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, inputFillAmount, EPS); + 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 orderSlippage = getRandomFeeRate(); + const orders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + side, + }).map(fo => slipOrder(fo.order, orderSlippage, side)); + const result = simulateWorstCaseFill({ + orders, + side, + fillAmount: fillableInput, + gasPrice: ONE, + opts: { gasSchedule: GAS_SCHEDULE }, + }); + if (side === MarketOperation.Sell) { + const slippedOutput = fillableOutput.times(1 - orderSlippage).integerValue(); + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, slippedOutput); + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, fillableInput); + } else { + const slippedOutput = fillableOutput.times(orderSlippage + 1).integerValue(); + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, fillableInput); + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, slippedOutput); + } + }); + }); +}); From 481136166cbb3f129e68ecb11f345e17ca85542d Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 3 Apr 2020 00:45:14 -0400 Subject: [PATCH 03/13] `@0x/asset-swapper`: Remove unused `Order` export from `index.ts`. --- packages/asset-swapper/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index fd5d5a2a4c..d1d918c563 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -12,7 +12,7 @@ export { SRAPollingOrderProviderOpts, SRAWebsocketOrderProviderOpts, } from '@0x/orderbook'; -export { APIOrder, Asset, AssetPairsItem, Order, SignedOrder } from '@0x/types'; +export { APIOrder, Asset, AssetPairsItem, SignedOrder } from '@0x/types'; export { BigNumber } from '@0x/utils'; export { DataItem, From d19bb3de8d63fb71c9fabc8c3568a34c9e052b59 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 3 Apr 2020 00:50:42 -0400 Subject: [PATCH 04/13] `@0x/asset-swapper`: Run prettier --- .../utils/market_operation_utils/orders.ts | 9 +- .../src/utils/quote_simulation.ts | 129 ++++++++---------- .../src/utils/swap_quote_calculator.ts | 14 +- .../test/market_operation_utils_test.ts | 10 +- .../test/quote_simulation_test.ts | 108 +++++++-------- 5 files changed, 113 insertions(+), 157 deletions(-) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index 28c1e066df..228fef46f5 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -123,7 +123,7 @@ export interface CreateOrderFromPathOpts { export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] { const collapsedPath = collapsePath(path); const orders: OptimizedMarketOrder[] = []; - for (let i = 0; i < collapsedPath.length;) { + for (let i = 0; i < collapsedPath.length; ) { if (collapsedPath[i].source === ERC20BridgeSource.Native) { orders.push(createNativeOrder(collapsedPath[i])); ++i; @@ -219,8 +219,9 @@ function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromP const bridgeOrder = createBridgeOrder(fill, opts); totalMakerAssetAmount = totalMakerAssetAmount.plus(bridgeOrder.makerAssetAmount); totalTakerAssetAmount = totalTakerAssetAmount.plus(bridgeOrder.takerAssetAmount); - const { bridgeAddress, bridgeData: orderBridgeData } = - assetDataUtils.decodeAssetDataOrThrow(bridgeOrder.makerAssetData) as ERC20BridgeAssetData; + const { bridgeAddress, bridgeData: orderBridgeData } = assetDataUtils.decodeAssetDataOrThrow( + bridgeOrder.makerAssetData, + ) as ERC20BridgeAssetData; batchedBridgeData.calls.push({ target: bridgeAddress, bridgeData: orderBridgeData, @@ -290,7 +291,7 @@ type CommonBridgeOrderFields = Pick< OptimizedMarketOrder, Exclude< keyof OptimizedMarketOrder, - 'fills' + | 'fills' | 'makerAddress' | 'makerAssetData' | 'takerAssetData' diff --git a/packages/asset-swapper/src/utils/quote_simulation.ts b/packages/asset-swapper/src/utils/quote_simulation.ts index 1363da5d12..fd9fccde25 100644 --- a/packages/asset-swapper/src/utils/quote_simulation.ts +++ b/packages/asset-swapper/src/utils/quote_simulation.ts @@ -174,14 +174,10 @@ export function fillQuoteOrders( const filledOutputFee = filledOutput.div(fo.totalOrderOutput).times(fo.totalOrderOutputFee); result.inputBySource[source] = result.inputBySource[source].plus(filledInput); - result.input = result.input - .plus(filledInput); - result.output = result.output - .plus(filledOutput); - result.inputFee = result.inputFee - .plus(filledInputFee); - result.outputFee = result.outputFee - .plus(filledOutputFee); + result.input = result.input.plus(filledInput); + result.output = result.output.plus(filledOutput); + result.inputFee = result.inputFee.plus(filledInputFee); + result.outputFee = result.outputFee.plus(filledOutputFee); remainingInput = remainingInput.minus(filledInput.plus(filledInputFee)); } } @@ -227,53 +223,48 @@ function createBestCaseFillOrderCalls(quoteInfo: QuoteFillInfo): QuoteFillOrderC order: o, ...(side === MarketOperation.Sell ? { - totalOrderInput: o.takerAssetAmount, - totalOrderOutput: o.makerAssetAmount, - totalOrderInputFee: isOrderTakerFeePayableWithTakerAsset(o) - ? o.takerFee - : ZERO_AMOUNT, - totalOrderOutputFee: isOrderTakerFeePayableWithMakerAsset(o) - ? o.takerFee.negated() - : ZERO_AMOUNT, - } - // Buy - : { - totalOrderInput: o.makerAssetAmount, - totalOrderOutput: o.takerAssetAmount, - totalOrderInputFee: isOrderTakerFeePayableWithMakerAsset(o) - ? o.takerFee.negated() - : ZERO_AMOUNT, - totalOrderOutputFee: isOrderTakerFeePayableWithTakerAsset(o) - ? o.takerFee - : ZERO_AMOUNT, - } - ), + totalOrderInput: o.takerAssetAmount, + totalOrderOutput: o.makerAssetAmount, + totalOrderInputFee: isOrderTakerFeePayableWithTakerAsset(o) ? o.takerFee : ZERO_AMOUNT, + totalOrderOutputFee: isOrderTakerFeePayableWithMakerAsset(o) ? o.takerFee.negated() : ZERO_AMOUNT, + } + : // Buy + { + totalOrderInput: o.makerAssetAmount, + totalOrderOutput: o.takerAssetAmount, + totalOrderInputFee: isOrderTakerFeePayableWithMakerAsset(o) ? o.takerFee.negated() : ZERO_AMOUNT, + totalOrderOutputFee: isOrderTakerFeePayableWithTakerAsset(o) ? o.takerFee : ZERO_AMOUNT, + }), })); } function createWorstCaseFillOrderCalls(quoteInfo: QuoteFillInfo): QuoteFillOrderCall[] { // Reuse best case fill orders. - return createBestCaseFillOrderCalls(quoteInfo).map(fo => ({ - ...fo, - order: { - ...fo.order, - // Apply slippage to order fills and reverse them. - fills: getSlippedOrderFills(fo.order, quoteInfo.side).reverse(), - }, - // Reverse the orders. - })).reverse(); + return createBestCaseFillOrderCalls(quoteInfo) + .map(fo => ({ + ...fo, + order: { + ...fo.order, + // Apply slippage to order fills and reverse them. + fills: getSlippedOrderFills(fo.order, quoteInfo.side).reverse(), + }, + // Reverse the orders. + })) + .reverse(); } // Apply order slippage to its fill paths. function getSlippedOrderFills(order: OptimizedMarketOrder, side: MarketOperation): CollapsedFill[] { const totalInput = BigNumber.sum(...order.fills.map(f => f.input)); const totalOutput = BigNumber.sum(...order.fills.map(f => f.output)); - const inputScaling = side === MarketOperation.Sell - ? order.fillableTakerAssetAmount.div(totalInput) - : order.fillableMakerAssetAmount.div(totalInput); - const outputScaling = side === MarketOperation.Sell - ? order.fillableMakerAssetAmount.div(totalOutput) - : order.fillableTakerAssetAmount.div(totalOutput); + const inputScaling = + side === MarketOperation.Sell + ? order.fillableTakerAssetAmount.div(totalInput) + : order.fillableMakerAssetAmount.div(totalInput); + const outputScaling = + side === MarketOperation.Sell + ? order.fillableMakerAssetAmount.div(totalOutput) + : order.fillableTakerAssetAmount.div(totalOutput); return order.fills.map(f => ({ ...f, input: f.input.times(inputScaling), @@ -307,39 +298,34 @@ function roundIntermediateFillResult( gas: Math.ceil(ir.gas), inputBySource: Object.assign( {}, - ...Object.entries(ir.inputBySource) - .map(([k, v]) => ({ [k]: roundInputAmount(v, side) })), + ...Object.entries(ir.inputBySource).map(([k, v]) => ({ [k]: roundInputAmount(v, side) })), ), }; } -function fromIntermediateQuoteFillResult( - ir: IntermediateQuoteFillResult, - quoteInfo: QuoteFillInfo, -): QuoteFillResult { +function fromIntermediateQuoteFillResult(ir: IntermediateQuoteFillResult, quoteInfo: QuoteFillInfo): QuoteFillResult { const { side } = quoteInfo; const _ir = roundIntermediateFillResult(ir, side); return { ...(side === MarketOperation.Sell - // Sell - ? { - makerAssetAmount: _ir.output, - takerAssetAmount: _ir.input, - takerFeeMakerAssetAmount: _ir.outputFee, - takerFeeTakerAssetAmount: _ir.inputFee, - totalMakerAssetAmount: _ir.output.plus(_ir.outputFee), - totalTakerAssetAmount: _ir.input.plus(_ir.inputFee), - } - // Buy - : { - makerAssetAmount: _ir.input, - takerAssetAmount: _ir.output, - takerFeeMakerAssetAmount: _ir.inputFee, - takerFeeTakerAssetAmount: _ir.outputFee, - totalMakerAssetAmount: _ir.input.plus(_ir.inputFee), - totalTakerAssetAmount: _ir.output.plus(_ir.outputFee), - } - ), + ? // Sell + { + makerAssetAmount: _ir.output, + takerAssetAmount: _ir.input, + takerFeeMakerAssetAmount: _ir.outputFee, + takerFeeTakerAssetAmount: _ir.inputFee, + totalMakerAssetAmount: _ir.output.plus(_ir.outputFee), + totalTakerAssetAmount: _ir.input.plus(_ir.inputFee), + } + : // Buy + { + makerAssetAmount: _ir.input, + takerAssetAmount: _ir.output, + takerFeeMakerAssetAmount: _ir.inputFee, + takerFeeTakerAssetAmount: _ir.outputFee, + totalMakerAssetAmount: _ir.input.plus(_ir.inputFee), + totalTakerAssetAmount: _ir.output.plus(_ir.outputFee), + }), protocolFeeAmount: _ir.protocolFee, gas: _ir.gas, fillAmountBySource: _ir.inputBySource, @@ -354,10 +340,7 @@ export function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): Col return fills; } -function getTotalGasUsedBySources( - sources: ERC20BridgeSource[], - gasSchedule: { [source: string]: number }, -): number { +function getTotalGasUsedBySources(sources: ERC20BridgeSource[], gasSchedule: { [source: string]: number }): number { let gasUsed = 0; for (const s of sources) { gasUsed += gasSchedule[s] || 0; diff --git a/packages/asset-swapper/src/utils/swap_quote_calculator.ts b/packages/asset-swapper/src/utils/swap_quote_calculator.ts index 5e164fc5e4..ceff4fe4f3 100644 --- a/packages/asset-swapper/src/utils/swap_quote_calculator.ts +++ b/packages/asset-swapper/src/utils/swap_quote_calculator.ts @@ -19,15 +19,9 @@ import { import { MarketOperationUtils } from './market_operation_utils'; import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders'; import { GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types'; -import { - isSupportedAssetDataInOrders, -} from './utils'; +import { isSupportedAssetDataInOrders } from './utils'; -import { - QuoteFillResult, - simulateBestCaseFill, - simulateWorstCaseFill, -} from './quote_simulation'; +import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation'; // TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError? export class SwapQuoteCalculator { @@ -225,9 +219,7 @@ function createSwapQuote( } } -function getSwapQuoteOrdersBreakdown( - fillAmountBySource: { [source: string]: BigNumber }, -): SwapQuoteOrdersBreakdown { +function getSwapQuoteOrdersBreakdown(fillAmountBySource: { [source: string]: BigNumber }): SwapQuoteOrdersBreakdown { const totalFillAmount = BigNumber.sum(...Object.values(fillAmountBySource)); const breakdown: SwapQuoteOrdersBreakdown = {}; Object.entries(fillAmountBySource).forEach(([source, fillAmount]) => { diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index b731bac836..dc26a74818 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -701,10 +701,7 @@ describe('MarketOperationUtils tests', () => { expect(orderFillSources).to.deep.eq([ [ERC20BridgeSource.Uniswap], [ERC20BridgeSource.Native], - [ - ERC20BridgeSource.Eth2Dai, - ERC20BridgeSource.CurveUsdcDai, - ], + [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.CurveUsdcDai], ]); }); }); @@ -1010,10 +1007,7 @@ describe('MarketOperationUtils tests', () => { const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); expect(orderFillSources).to.deep.eq([ [ERC20BridgeSource.Native], - [ - ERC20BridgeSource.Eth2Dai, - ERC20BridgeSource.Uniswap, - ], + [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap], ]); }); }); diff --git a/packages/asset-swapper/test/quote_simulation_test.ts b/packages/asset-swapper/test/quote_simulation_test.ts index 91db264b31..baa6883044 100644 --- a/packages/asset-swapper/test/quote_simulation_test.ts +++ b/packages/asset-swapper/test/quote_simulation_test.ts @@ -11,7 +11,12 @@ import * as _ from 'lodash'; import { MarketOperation } from '../src/types'; import { CollapsedFill, ERC20BridgeSource, OptimizedMarketOrder } from '../src/utils/market_operation_utils/types'; -import { fillQuoteOrders, QuoteFillOrderCall, simulateBestCaseFill, simulateWorstCaseFill } from '../src/utils/quote_simulation'; +import { + fillQuoteOrders, + QuoteFillOrderCall, + simulateBestCaseFill, + simulateWorstCaseFill, +} from '../src/utils/quote_simulation'; // tslint:disable: custom-no-magic-numbers const { NULL_ADDRESS } = constants; @@ -19,7 +24,6 @@ const ZERO = new BigNumber(0); const ONE = new BigNumber(1); describe('quote_simulation tests', async () => { - const MAKER_TOKEN = randomAddress(); const TAKER_TOKEN = randomAddress(); const DEFAULT_MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); @@ -27,15 +31,17 @@ describe('quote_simulation tests', async () => { const EPS = 1e6; const GAS_SCHEDULE = { [ERC20BridgeSource.Native]: 1 }; - function createQuoteFillOrders(opts: Partial<{ - fillableInput: BigNumber; - fillableOutput: BigNumber; - inputFeeRate: number; - outputFeeRate: number; - count: number; - fillsCount: number; - side: MarketOperation; - }> = {}): QuoteFillOrderCall[] { + function createQuoteFillOrders( + opts: Partial<{ + fillableInput: BigNumber; + fillableOutput: BigNumber; + inputFeeRate: number; + outputFeeRate: number; + count: number; + fillsCount: number; + side: MarketOperation; + }> = {}, + ): QuoteFillOrderCall[] { const { fillableInput, fillableOutput, inputFeeRate, outputFeeRate, count, fillsCount, side } = { fillableInput: getRandomOrderSize(), fillableOutput: getRandomOrderSize(), @@ -46,12 +52,10 @@ describe('quote_simulation tests', async () => { side: MarketOperation.Sell, ...opts, }; - const _inputFeeRate = side === MarketOperation.Sell - ? inputFeeRate : -inputFeeRate; - const _outputFeeRate = side === MarketOperation.Sell - ? -outputFeeRate : outputFeeRate; + const _inputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; + const _outputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; - const fillableInputs = subdivideAmount(fillableInput, count); + const fillableInputs = subdivideAmount(fillableInput, count); const fillableOutputs = subdivideAmount(fillableOutput, count); const filledInputs = subdivideAmount(fillableInput.times(0.5), count); const filledOutputs: BigNumber[] = []; @@ -61,10 +65,7 @@ describe('quote_simulation tests', async () => { const outputFees: BigNumber[] = []; _.times(count).forEach(i => { const f = filledInputs[i].div(fillableInputs[i]); - filledOutputs.push(fillableOutputs[i] - .times(f) - .integerValue(BigNumber.ROUND_DOWN), - ); + 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()); @@ -72,17 +73,13 @@ describe('quote_simulation tests', async () => { }); return _.times(count, i => { return { - order: createQuoteFillOrderOrder( - totalInputs[i], - totalOutputs[i], - { - side, - fillsCount, - filledInput: filledInputs[i], - takerInputFee: inputFees[i].abs(), - takerOutputFee: outputFees[i].abs(), - }, - ), + order: createQuoteFillOrderOrder(totalInputs[i], totalOutputs[i], { + side, + fillsCount, + filledInput: filledInputs[i], + takerInputFee: inputFees[i].abs(), + takerOutputFee: outputFees[i].abs(), + }), totalOrderInput: totalInputs[i], totalOrderOutput: totalOutputs[i], totalOrderInputFee: inputFees[i], @@ -100,14 +97,9 @@ describe('quote_simulation tests', async () => { side: MarketOperation; takerInputFee: BigNumber; takerOutputFee: BigNumber; - }> = {}): OptimizedMarketOrder { - const { - filledInput, - fillsCount, - side, - takerInputFee, - takerOutputFee, - } = { + }> = {}, + ): OptimizedMarketOrder { + const { filledInput, fillsCount, side, takerInputFee, takerOutputFee } = { side: MarketOperation.Sell, filledInput: ZERO, fillsCount: 3, @@ -115,25 +107,22 @@ describe('quote_simulation tests', async () => { takerOutputFee: ZERO, ...opts, }; - const filledOutput = filledInput.div(input).times(output).integerValue(BigNumber.ROUND_DOWN); + const filledOutput = filledInput + .div(input) + .times(output) + .integerValue(BigNumber.ROUND_DOWN); const fillableInput = input.minus(filledInput); const fillableOutput = output.minus(filledOutput); - const makerAssetAmount = side === MarketOperation.Sell - ? output : input; - const takerAssetAmount = side === MarketOperation.Sell - ? input : output; - const fillableMakerAssetAmount = side === MarketOperation.Sell - ? fillableOutput : fillableInput; - const fillableTakerAssetAmount = side === MarketOperation.Sell - ? fillableInput : fillableOutput; + const makerAssetAmount = side === MarketOperation.Sell ? output : input; + const takerAssetAmount = side === MarketOperation.Sell ? input : output; + const fillableMakerAssetAmount = side === MarketOperation.Sell ? fillableOutput : fillableInput; + const fillableTakerAssetAmount = side === MarketOperation.Sell ? fillableInput : fillableOutput; const takerFee = BigNumber.max(takerInputFee, takerOutputFee); let takerFeeAssetData = '0x'; if (!takerInputFee.eq(0)) { - takerFeeAssetData = side === MarketOperation.Sell - ? DEFAULT_TAKER_ASSET_DATA : DEFAULT_MAKER_ASSET_DATA; + takerFeeAssetData = side === MarketOperation.Sell ? DEFAULT_TAKER_ASSET_DATA : DEFAULT_MAKER_ASSET_DATA; } else if (!takerOutputFee.eq(0)) { - takerFeeAssetData = side === MarketOperation.Sell - ? DEFAULT_MAKER_ASSET_DATA : DEFAULT_TAKER_ASSET_DATA; + takerFeeAssetData = side === MarketOperation.Sell ? DEFAULT_MAKER_ASSET_DATA : DEFAULT_TAKER_ASSET_DATA; } const fillableTakerFeeAmount = fillableTakerAssetAmount .div(takerAssetAmount) @@ -164,11 +153,7 @@ describe('quote_simulation tests', async () => { }; } - function createOrderCollapsedFills( - input: BigNumber, - output: BigNumber, - count: number, - ): CollapsedFill[] { + function createOrderCollapsedFills(input: BigNumber, output: BigNumber, count: number): CollapsedFill[] { const inputs = subdivideAmount(input, count); const outputs = subdivideAmount(output, count); return _.times(count, i => { @@ -189,12 +174,12 @@ describe('quote_simulation tests', async () => { function countCollapsedFills(fillOrders: QuoteFillOrderCall[] | OptimizedMarketOrder[]): number { let count = 0; if ((fillOrders as any)[0].fills) { - const orders = fillOrders as any as OptimizedMarketOrder[]; + const orders = (fillOrders as any) as OptimizedMarketOrder[]; for (const o of orders) { count += o.fills.length; } } else { - const orders = fillOrders as any as QuoteFillOrderCall[]; + const orders = (fillOrders as any) as QuoteFillOrderCall[]; for (const fo of orders) { count += fo.order.fills.length; } @@ -215,8 +200,9 @@ describe('quote_simulation tests', async () => { } 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()); + expect(new BigNumber(actual).times(1e4).integerValue()).to.bignumber.eq( + new BigNumber(expected).times(1e4).integerValue(), + ); } function subdivideAmount(amount: BigNumber, count: number): BigNumber[] { From fafaa3e69b714ebcae8dd93b9bb5394321019512 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 3 Apr 2020 01:26:40 -0400 Subject: [PATCH 05/13] `@0x/asset-swapper`: Lint and remove `@0x/contracts-asset-proxy` dep. --- packages/asset-swapper/package.json | 1 - .../utils/market_operation_utils/orders.ts | 31 ++++++++++++++++--- .../src/utils/protocol_fee_utils.ts | 1 - .../src/utils/quote_simulation.ts | 2 ++ .../test/market_operation_utils_test.ts | 2 +- .../test/quote_simulation_test.ts | 4 +-- packages/asset-swapper/test/utils/mocks.ts | 2 -- 7 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/asset-swapper/package.json b/packages/asset-swapper/package.json index 2962024d82..3df0a94cb7 100644 --- a/packages/asset-swapper/package.json +++ b/packages/asset-swapper/package.json @@ -48,7 +48,6 @@ "@0x/assert": "^3.0.7", "@0x/contract-addresses": "^4.9.0", "@0x/contract-wrappers": "^13.6.3", - "@0x/contracts-asset-proxy": "^3.2.5", "@0x/json-schemas": "^5.0.7", "@0x/order-utils": "^10.2.4", "@0x/orderbook": "^2.2.5", diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index 228fef46f5..680c484138 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -1,8 +1,7 @@ import { ContractAddresses } from '@0x/contract-addresses'; -import { DexForwaderBridgeData, dexForwarderBridgeDataEncoder } from '@0x/contracts-asset-proxy'; import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils'; -import { ERC20BridgeAssetData, SignedOrder } from '@0x/types'; -import { AbiEncoder, BigNumber, hexUtils } from '@0x/utils'; +import { SignedOrder } from '@0x/types'; +import { AbiEncoder, BigNumber } from '@0x/utils'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; @@ -29,6 +28,30 @@ import { // tslint:disable completed-docs +interface DexForwaderBridgeData { + inputToken: string; + calls: Array<{ + target: string; + inputTokenAmount: BigNumber; + outputTokenAmount: BigNumber; + bridgeData: string; + }>; +} + +const dexForwarderBridgeDataEncoder = AbiEncoder.create([ + { name: 'inputToken', type: 'address' }, + { + name: 'calls', + type: 'tuple[]', + components: [ + { name: 'target', type: 'address' }, + { name: 'inputTokenAmount', type: 'uint256' }, + { name: 'outputTokenAmount', type: 'uint256' }, + { name: 'bridgeData', type: 'bytes' }, + ], + }, +]); + export function createDummyOrderForSampler( makerAssetData: string, takerAssetData: string, @@ -221,7 +244,7 @@ function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromP totalTakerAssetAmount = totalTakerAssetAmount.plus(bridgeOrder.takerAssetAmount); const { bridgeAddress, bridgeData: orderBridgeData } = assetDataUtils.decodeAssetDataOrThrow( bridgeOrder.makerAssetData, - ) as ERC20BridgeAssetData; + ); batchedBridgeData.calls.push({ target: bridgeAddress, bridgeData: orderBridgeData, diff --git a/packages/asset-swapper/src/utils/protocol_fee_utils.ts b/packages/asset-swapper/src/utils/protocol_fee_utils.ts index f0f3e4458d..11c42cae9b 100644 --- a/packages/asset-swapper/src/utils/protocol_fee_utils.ts +++ b/packages/asset-swapper/src/utils/protocol_fee_utils.ts @@ -1,4 +1,3 @@ -import { Order } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as heartbeats from 'heartbeats'; diff --git a/packages/asset-swapper/src/utils/quote_simulation.ts b/packages/asset-swapper/src/utils/quote_simulation.ts index fd9fccde25..0b96f1729e 100644 --- a/packages/asset-swapper/src/utils/quote_simulation.ts +++ b/packages/asset-swapper/src/utils/quote_simulation.ts @@ -9,6 +9,8 @@ import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAs const { PROTOCOL_FEE_MULTIPLIER, ZERO_AMOUNT } = constants; const { ROUND_DOWN, ROUND_UP } = BigNumber; +// tslint:disable completed-docs + export interface QuoteFillResult { // Maker asset bought. makerAssetAmount: BigNumber; diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index dc26a74818..05635af16c 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -692,7 +692,7 @@ describe('MarketOperationUtils tests', () => { excludedSources: [ ERC20BridgeSource.Kyber, ..._.without(DEFAULT_OPTS.excludedSources, ERC20BridgeSource.CurveUsdcDai), - ] as ERC20BridgeSource[], + ], shouldBatchBridgeOrders: true, }, ); diff --git a/packages/asset-swapper/test/quote_simulation_test.ts b/packages/asset-swapper/test/quote_simulation_test.ts index baa6883044..edfca87650 100644 --- a/packages/asset-swapper/test/quote_simulation_test.ts +++ b/packages/asset-swapper/test/quote_simulation_test.ts @@ -892,8 +892,6 @@ describe('quote_simulation tests', async () => { outputFeeRate, side, }).map(fo => fo.order); - const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; - const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue(); const inputFillAmount = fillableInput.times(2 / 3).integerValue(); const result = simulateBestCaseFill({ orders, @@ -947,4 +945,4 @@ describe('quote_simulation tests', async () => { } }); }); -}); +}); // tslint:disable: max-file-line-count diff --git a/packages/asset-swapper/test/utils/mocks.ts b/packages/asset-swapper/test/utils/mocks.ts index d7be2985ec..109c4b7d09 100644 --- a/packages/asset-swapper/test/utils/mocks.ts +++ b/packages/asset-swapper/test/utils/mocks.ts @@ -9,8 +9,6 @@ import { SwapQuoter } from '../../src/swap_quoter'; import { SignedOrderWithFillableAmounts } from '../../src/types'; import { ProtocolFeeUtils } from '../../src/utils/protocol_fee_utils'; -const PROTOCOL_FEE_MULTIPLIER = 150000; - // tslint:disable: max-classes-per-file class OrderbookClass extends Orderbook { From 0c8aec30bdf6aed67201980349270c591a1a394f Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 3 Apr 2020 01:43:53 -0400 Subject: [PATCH 06/13] `@0x/asset-swapper`: Fix linter lies. --- .../src/utils/market_operation_utils/orders.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index 680c484138..9c220b03b8 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -1,6 +1,6 @@ import { ContractAddresses } from '@0x/contract-addresses'; import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils'; -import { SignedOrder } from '@0x/types'; +import { ERC20BridgeAssetData, SignedOrder } from '@0x/types'; import { AbiEncoder, BigNumber } from '@0x/utils'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; @@ -26,7 +26,7 @@ import { OrderDomain, } from './types'; -// tslint:disable completed-docs +// tslint:disable completed-docs no-unnecessary-type-assertion interface DexForwaderBridgeData { inputToken: string; @@ -244,7 +244,7 @@ function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromP totalTakerAssetAmount = totalTakerAssetAmount.plus(bridgeOrder.takerAssetAmount); const { bridgeAddress, bridgeData: orderBridgeData } = assetDataUtils.decodeAssetDataOrThrow( bridgeOrder.makerAssetData, - ); + ) as ERC20BridgeAssetData; batchedBridgeData.calls.push({ target: bridgeAddress, bridgeData: orderBridgeData, From b2047b90b35cc52228f57748c1046cace118a859 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 3 Apr 2020 02:18:56 -0400 Subject: [PATCH 07/13] `@0x/asset-proxy`: Fix failing test. --- packages/asset-swapper/src/utils/swap_quote_calculator.ts | 6 +++--- packages/asset-swapper/test/quote_simulation_test.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/asset-swapper/src/utils/swap_quote_calculator.ts b/packages/asset-swapper/src/utils/swap_quote_calculator.ts index ceff4fe4f3..7ce60afbbc 100644 --- a/packages/asset-swapper/src/utils/swap_quote_calculator.ts +++ b/packages/asset-swapper/src/utils/swap_quote_calculator.ts @@ -196,12 +196,12 @@ function createSwapQuote( const quoteBase: SwapQuoteBase = { takerAssetData, makerAssetData, - // Remove fill metadata. - orders: resultOrders.map(o => _.omit(o, 'fills')) as SignedOrderWithFillableAmounts[], + gasPrice, bestCaseQuoteInfo: fillResultsToQuoteInfo(bestCaseFillResult), worstCaseQuoteInfo: fillResultsToQuoteInfo(worstCaseFillResult), - gasPrice, sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource), + // Remove fill metadata. + orders: resultOrders.map(o => _.omit(o, 'fills')) as SignedOrderWithFillableAmounts[], }; if (operation === MarketOperation.Buy) { diff --git a/packages/asset-swapper/test/quote_simulation_test.ts b/packages/asset-swapper/test/quote_simulation_test.ts index edfca87650..45b3d5a74b 100644 --- a/packages/asset-swapper/test/quote_simulation_test.ts +++ b/packages/asset-swapper/test/quote_simulation_test.ts @@ -19,16 +19,16 @@ import { } from '../src/utils/quote_simulation'; // tslint:disable: custom-no-magic-numbers -const { NULL_ADDRESS } = constants; -const ZERO = new BigNumber(0); -const ONE = new BigNumber(1); 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 DEFAULT_MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); const DEFAULT_TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); - const EPS = 1e6; + const EPS = 1e7; // Some precision lost when crafting these orders. const GAS_SCHEDULE = { [ERC20BridgeSource.Native]: 1 }; function createQuoteFillOrders( From b09c751942449dab688271d00c509fc27ccce2d0 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 6 Apr 2020 11:56:03 -0400 Subject: [PATCH 08/13] `@0x/asset-swapper`: Remove deprecated swap_quote_calculator tests --- .../test/swap_quote_calculator_test.ts | 908 ------------------ 1 file changed, 908 deletions(-) delete mode 100644 packages/asset-swapper/test/swap_quote_calculator_test.ts diff --git a/packages/asset-swapper/test/swap_quote_calculator_test.ts b/packages/asset-swapper/test/swap_quote_calculator_test.ts deleted file mode 100644 index 4c77a8b38b..0000000000 --- a/packages/asset-swapper/test/swap_quote_calculator_test.ts +++ /dev/null @@ -1,908 +0,0 @@ -// tslint:disable:max-file-line-count -// TODO(dorothy-zbornak): Skipping these tests for now because they're a -// nightmare to maintain. We should replace them with simpler unit tests. -/* -import { constants as devConstants } from '@0x/contracts-test-utils'; -import { BlockchainLifecycle } from '@0x/dev-utils'; -import { ContractAddresses, migrateOnceAsync } from '@0x/migrations'; -import { BigNumber } from '@0x/utils'; -import * as chai from 'chai'; -import * as _ from 'lodash'; -import 'mocha'; - -import { constants } from '../src/constants'; -import { CalculateSwapQuoteOpts, SignedOrderWithFillableAmounts } from '../src/types'; -import { MarketOperationUtils } from '../src/utils/market_operation_utils/'; -import { DEFAULT_GET_MARKET_ORDERS_OPTS, SELL_SOURCES } from '../src/utils/market_operation_utils/constants'; -import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; -import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils'; -import { SwapQuoteCalculator } from '../src/utils/swap_quote_calculator'; - -import { chaiSetup } from './utils/chai_setup'; -import { MockSamplerContract } from './utils/mock_sampler_contract'; -import { protocolFeeUtilsMock } from './utils/mocks'; -import { testOrders } from './utils/test_orders'; -import { baseUnitAmount } from './utils/utils'; -import { provider, web3Wrapper } from './utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE); -const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000); -// const MIXED_TEST_ORDERS = _.concat( -// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, -// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, -// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, -// ); -const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID; - -// Excludes all non native sources -const CALCULATE_SWAP_QUOTE_OPTS: CalculateSwapQuoteOpts = { - ...DEFAULT_GET_MARKET_ORDERS_OPTS, - ...{ - excludedSources: SELL_SOURCES, - }, -}; - -function createSamplerFromSignedOrdersWithFillableAmounts( - signedOrders: SignedOrderWithFillableAmounts[], -): DexOrderSampler { - const sampleDexHandler = (takerToken: string, makerToken: string, amounts: BigNumber[]) => { - return amounts.map(() => constants.ZERO_AMOUNT); - }; - return new DexOrderSampler( - new MockSamplerContract({ - getOrderFillableMakerAssetAmounts: (orders, signatures) => - orders.map((o, i) => signedOrders[i].fillableMakerAssetAmount), - getOrderFillableTakerAssetAmounts: (orders, signatures) => - orders.map((o, i) => signedOrders[i].fillableTakerAssetAmount), - sampleSellsFromEth2Dai: sampleDexHandler, - sampleSellsFromKyberNetwork: sampleDexHandler, - sampleSellsFromUniswap: sampleDexHandler, - sampleBuysFromEth2Dai: sampleDexHandler, - sampleBuysFromUniswap: sampleDexHandler, - }), - ); -} - -// tslint:disable:custom-no-magic-numbers -describe.skip('swapQuoteCalculator', () => { - let protocolFeeUtils: ProtocolFeeUtils; - let contractAddresses: ContractAddresses; - - before(async () => { - contractAddresses = await migrateOnceAsync(provider); - protocolFeeUtils = protocolFeeUtilsMock().object; - await blockchainLifecycle.startAsync(); - }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - - describe('#calculateMarketSellSwapQuote', () => { - // TODO(dave4506) InsufficientLiquidityError is not thrown anymore, consider how to test for insufficient liquidity - // describe('InsufficientLiquidityError', () => { - // it('should throw if not enough taker asset liquidity (multiple feeless orders)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - // baseUnitAmount(10), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(9)); - // }); - // it('should throw if not enough taker asset liquidity (multiple feeless orders with 20% slippage)', async () => { - // const errorFunction = async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - // baseUnitAmount(10), - // 0.2, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(7.5)); - // }); - // it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with no slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - // baseUnitAmount(20), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(15)); - // }); - // it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with 20% slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - // baseUnitAmount(20), - // 0.2, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(12.5)); - // }); - // it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with no slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - // baseUnitAmount(10), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(9)); - // }); - // it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with 20% slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - // baseUnitAmount(10), - // 0.2, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(7.5)); - // }); - // it('should throw if not enough taker asset liquidity (multiple mixed feeType orders with no slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(MIXED_TEST_ORDERS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // MIXED_TEST_ORDERS, - // baseUnitAmount(40), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(33)); - // }); - // it('should throw if not enough taker asset liquidity (multiple mixed feeTyoe orders with 20% slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(MIXED_TEST_ORDERS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // MIXED_TEST_ORDERS, - // baseUnitAmount(40), - // 0.2, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(27.5)); - // }); - // }); - it('calculates a correct swapQuote with no slippage (feeless orders)', async () => { - const assetSellAmount = baseUnitAmount(0.5); - const slippagePercentage = 0; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - assetSellAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0]]); - expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount: assetSellAmount, - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(3), - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount: assetSellAmount, - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(3), - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - }); - it('calculates a correct swapQuote with slippage (feeless orders)', async () => { - const assetSellAmount = baseUnitAmount(4); - const slippagePercentage = 0.2; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - assetSellAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[1], - ]); - expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount: assetSellAmount, - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(9), - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount: assetSellAmount, - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(1.6), - protocolFeeInWeiAmount: baseUnitAmount(45, 4), - }); - }); - it('calculates a correct swapQuote with no slippage (takerAsset denominated fee orders)', async () => { - const assetSellAmount = baseUnitAmount(4); - const slippagePercentage = 0; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - assetSellAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[0], - ]); - expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(3), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(3)), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(6), - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(3), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(3)), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(6), - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - }); - it('calculates a correct swapQuote with slippage (takerAsset denominated fee orders)', async () => { - const assetSellAmount = baseUnitAmount(3); - const slippagePercentage = 0.5; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - assetSellAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[0], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[2], - ]); - expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(2.25), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2.25)), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(4.5), - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(1.2), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(1.2)), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(1.8), - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - }); - it('calculates a correct swapQuote with no slippage (makerAsset denominated fee orders)', async () => { - const assetSellAmount = baseUnitAmount(4); - const slippagePercentage = 0; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - assetSellAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[1], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[2], - ]); - expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(1.5).minus(1), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(1.5)).plus(1), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(4), - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(1.5).minus(1), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(1.5)).plus(1), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(4), - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - }); - it('calculates a correct swapQuote with slippage (makerAsset denominated fee orders)', async () => { - const assetSellAmount = baseUnitAmount(4); - const slippagePercentage = 0.5; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - assetSellAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[1], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[2], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[0], - ]); - expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(1.5).minus(1), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(1.5)).plus(1), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(4), - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(2), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2)), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(0.8), - protocolFeeInWeiAmount: baseUnitAmount(45, 4), - }); - }); - }); - describe('#calculateMarketBuySwapQuoteAsync', () => { - // TODO(dave4506) InsufficientLiquidityError is not thrown anymore, consider how to test for insufficient liquidity - // describe('InsufficientLiquidityError', () => { - // it('should throw if not enough maker asset liquidity (multiple feeless orders)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - // baseUnitAmount(12), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(10)); - // }); - // it('should throw if not enough taker asset liquidity (multiple feeless orders with 20% slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - // baseUnitAmount(10), - // 0.6, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(6.25)); - // }); - // it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with no slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - // baseUnitAmount(12), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(10)); - // }); - // it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with 20% slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - // baseUnitAmount(12), - // 0.6, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(6.25)); - // }); - // it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with no slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - // baseUnitAmount(6), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(5)); - // }); - // it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with 20% slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - // baseUnitAmount(6), - // 0.6, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(3.125)); - // }); - // it('should throw if not enough taker asset liquidity (multiple mixed feeType orders with no slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(MIXED_TEST_ORDERS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // MIXED_TEST_ORDERS, - // baseUnitAmount(40), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(25)); - // }); - // it('should throw if not enough taker asset liquidity (multiple mixed feeTyoe orders with 20% slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(MIXED_TEST_ORDERS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // MIXED_TEST_ORDERS, - // baseUnitAmount(40), - // 0.6, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(15.625)); - // }); - // }); - it('calculates a correct swapQuote with no slippage (feeless orders)', async () => { - const assetBuyAmount = baseUnitAmount(3); - const slippagePercentage = 0; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - assetBuyAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0]]); - expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount: baseUnitAmount(0.5), - totalTakerAssetAmount: baseUnitAmount(0.5), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount: baseUnitAmount(0.5), - totalTakerAssetAmount: baseUnitAmount(0.5), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - }); - it('calculates a correct swapQuote with slippage (feeless orders)', async () => { - const assetBuyAmount = baseUnitAmount(5); - const slippagePercentage = 0.5; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - assetBuyAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[2], - ]); - expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); - - const takerAssetAmount = new BigNumber(5) - .div(new BigNumber(6)) - .multipliedBy(ONE_ETH_IN_WEI) - .integerValue(BigNumber.ROUND_CEIL); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount, - totalTakerAssetAmount: takerAssetAmount, - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount: baseUnitAmount(20) - .div(6) - .integerValue(BigNumber.ROUND_UP), - totalTakerAssetAmount: baseUnitAmount(20) - .div(6) - .integerValue(BigNumber.ROUND_UP), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - }); - it('calculates a correct swapQuote with no slippage (takerAsset denominated fee orders)', async () => { - const assetBuyAmount = baseUnitAmount(3); - const slippagePercentage = 0; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - assetBuyAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[0], - ]); - expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(1.5), - takerAssetAmount: baseUnitAmount(0.5), - totalTakerAssetAmount: baseUnitAmount(2), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(1.5), - takerAssetAmount: baseUnitAmount(0.5), - totalTakerAssetAmount: baseUnitAmount(2), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - }); - it('calculates a correct swapQuote with slippage (takerAsset denominated fee orders)', async () => { - const assetBuyAmount = baseUnitAmount(5); - const slippagePercentage = 0.5; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - assetBuyAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - const fiveSixthEthInWei = new BigNumber(5) - .div(new BigNumber(6)) - .multipliedBy(ONE_ETH_IN_WEI) - .integerValue(BigNumber.ROUND_CEIL); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[0], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[2], - ]); - expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(2.5), - takerAssetAmount: fiveSixthEthInWei, - totalTakerAssetAmount: baseUnitAmount(2.5).plus(fiveSixthEthInWei), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(3), - takerAssetAmount: baseUnitAmount(10) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 3.3333... - totalTakerAssetAmount: baseUnitAmount(19) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 6.3333... - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - }); - it('calculates a correct swapQuote with no slippage (makerAsset denominated fee orders)', async () => { - const assetBuyAmount = baseUnitAmount(1); - const slippagePercentage = 0; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - assetBuyAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[1], - ]); - expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0.5) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 0.16666... - takerAssetAmount: baseUnitAmount(0.5) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 0.1666... - totalTakerAssetAmount: baseUnitAmount(1) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 0.3333... - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0.5) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 0.16666... - takerAssetAmount: baseUnitAmount(0.5) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 0.1666... - totalTakerAssetAmount: baseUnitAmount(1) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 0.3333... - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - }); - it('calculates a correct swapQuote with slippage (makerAsset denominated fee orders)', async () => { - const assetBuyAmount = baseUnitAmount(2.5); - const slippagePercentage = 0.48; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - assetBuyAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[1], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[2], - ]); - expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); - // test if rates are correct - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(1.25).minus(1), - takerAssetAmount: baseUnitAmount(2.25).plus(1), - totalTakerAssetAmount: baseUnitAmount(3.5), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - - const oneThirdEthInWei = new BigNumber(1) - .div(new BigNumber(3)) - .multipliedBy(ONE_ETH_IN_WEI) - .integerValue(BigNumber.ROUND_CEIL); - const oneSixthEthInWei = new BigNumber(1) - .div(new BigNumber(6)) - .multipliedBy(ONE_ETH_IN_WEI) - .integerValue(BigNumber.ROUND_CEIL); - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(4) - .plus(oneSixthEthInWei) - .multipliedBy(0.1) - .integerValue(BigNumber.ROUND_CEIL), - takerAssetAmount: baseUnitAmount(4) - .plus(oneSixthEthInWei) - .multipliedBy(0.1) - .integerValue(BigNumber.ROUND_CEIL), - totalTakerAssetAmount: baseUnitAmount(8) - .plus(oneThirdEthInWei) - .multipliedBy(0.1) - .integerValue(BigNumber.ROUND_CEIL), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - }); - }); -}); -*/ From 282930cb9b83347edac050b994475bdaee50a118 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Mon, 6 Apr 2020 13:55:05 +1000 Subject: [PATCH 09/13] Add GST support to DFB --- contracts/asset-proxy/CHANGELOG.json | 3 +++ .../src/bridges/DexForwarderBridge.sol | 18 +++++++++++------- .../contracts/test/TestDexForwarderBridge.sol | 9 +++++++++ .../src/utils/market_operation_utils/orders.ts | 11 ++++++++++- packages/contract-addresses/CHANGELOG.json | 6 ++++++ packages/contract-addresses/addresses.json | 4 ++-- 6 files changed, 41 insertions(+), 10 deletions(-) diff --git a/contracts/asset-proxy/CHANGELOG.json b/contracts/asset-proxy/CHANGELOG.json index b05613fb2a..b14c13f75c 100644 --- a/contracts/asset-proxy/CHANGELOG.json +++ b/contracts/asset-proxy/CHANGELOG.json @@ -21,6 +21,9 @@ { "note": "Add `DexForwaderBridge` bridge contract.", "pr": 2525 + }, + { + "note": "Add Gas Token freeing to `DexForwaderBridge` contract." } ] }, diff --git a/contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol b/contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol index 70409719db..a6cd09fee4 100644 --- a/contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol @@ -23,15 +23,19 @@ import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol"; import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; +import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; import "@0x/contracts-utils/contracts/src/LibBytes.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "../interfaces/IERC20Bridge.sol"; +import "./MixinGasToken.sol"; // solhint-disable space-after-comma, indent contract DexForwarderBridge is IERC20Bridge, - IWallet + IWallet, + DeploymentConstants, + MixinGasToken { using LibSafeMath for uint256; @@ -68,6 +72,7 @@ contract DexForwarderBridge is bytes calldata bridgeData ) external + freesGasTokensFromCollector returns (bytes4 success) { TransferFromState memory state; @@ -84,16 +89,15 @@ contract DexForwarderBridge is break; } - BridgeCall memory call = state.calls[i]; // Compute token amounts. state.callInputTokenAmount = LibSafeMath.min256( - call.inputTokenAmount, + state.calls[i].inputTokenAmount, state.initialInputTokenBalance.safeSub(state.totalInputTokenSold) ); state.callOutputTokenAmount = LibMath.getPartialAmountFloor( state.callInputTokenAmount, - call.inputTokenAmount, - call.outputTokenAmount + state.calls[i].inputTokenAmount, + state.calls[i].outputTokenAmount ); // Execute the call in a new context so we can recoup transferred @@ -101,13 +105,13 @@ contract DexForwarderBridge is (bool didSucceed, ) = address(this) .call(abi.encodeWithSelector( this.executeBridgeCall.selector, - call.target, + state.calls[i].target, to, state.inputToken, outputToken, state.callInputTokenAmount, state.callOutputTokenAmount, - call.bridgeData + state.calls[i].bridgeData )); if (didSucceed) { diff --git a/contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol b/contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol index c0ada6c30b..dcb72fe093 100644 --- a/contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol +++ b/contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol @@ -156,6 +156,7 @@ contract TestDexForwarderBridge is ITestDexForwarderBridge, DexForwarderBridge { + function createBridge( bytes4 returnCode, string memory revertError @@ -217,4 +218,12 @@ contract TestDexForwarderBridge is function balanceOf(address token, address owner) public view returns (uint256) { return TestDexForwarderBridgeTestToken(token).balanceOf(owner); } + + function _getGstAddress() + internal + view + returns (address gst) + { + return address(0); + } } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index 9c220b03b8..30c2f16d8c 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -152,10 +152,19 @@ export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts ++i; continue; } + // Liquidity Provider must be called by ERC20BridgeProxy + if (collapsedPath[i].source === ERC20BridgeSource.LiquidityProvider) { + orders.push(createBridgeOrder(collapsedPath[i], opts)); + ++i; + continue; + } // If there are contiguous bridge orders, we can batch them together. const contiguousBridgeFills = [collapsedPath[i]]; for (let j = i + 1; j < collapsedPath.length; ++j) { - if (collapsedPath[j].source === ERC20BridgeSource.Native) { + if ( + collapsedPath[j].source === ERC20BridgeSource.Native || + collapsedPath[j].source === ERC20BridgeSource.LiquidityProvider + ) { break; } contiguousBridgeFills.push(collapsedPath[j]); diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index e1739a3faa..f0d7a7f954 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -29,6 +29,12 @@ { "note": "Redeploy `Forwarder` on all networks", "pr": 2521 + }, + { + "note": "Redeploy `DexForwarderBridge` on Mainnet with Gas Token freeing" + }, + { + "note": "Revert to older Curve Bridge (without Gas Tokens)" } ] }, diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index 45e9820fa8..123a1d2202 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -28,9 +28,9 @@ "godsUnchainedValidator": "0x09a379ef7218bcfd8913faa8b281ebc5a2e0bc04", "broker": "0xd4690a51044db77d91d7aa8f7a3a5ad5da331af0", "chainlinkStopLimit": "0xeb27220f95f364e1d9531992c48613f231839f53", - "curveBridge": "0x1cf6ccc7e15d0d99a9498f37e16ba65b5c54bdd0", + "curveBridge": "0x6dc7950423ada9f56fb2c93a23edb787f1e29088", "maximumGasPrice": "0xe2bfd35306495d11e3c9db0d8de390cda24563cf", - "dexForwarderBridge": "0xa96844087062acf8556ca06a27702c6d19f87e57" + "dexForwarderBridge": "0x2b135c732110be20db72e44ab2a4b149fa213599" }, "3": { "erc20Proxy": "0xb1408f4c245a23c31b98d2c626777d4c0d766caa", From cdbcada49b06cd4a30d07f2b383f61fe73c42f69 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Mon, 6 Apr 2020 19:58:42 +1000 Subject: [PATCH 10/13] Authorize sender adddress --- .../contracts/src/bridges/DexForwarderBridge.sol | 1 + .../contracts/test/TestDexForwarderBridge.sol | 15 +++++++++++++++ .../asset-proxy/test/dex_forwarder_bridge.ts | 16 +++++++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol b/contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol index a6cd09fee4..9faf278041 100644 --- a/contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol @@ -75,6 +75,7 @@ contract DexForwarderBridge is freesGasTokensFromCollector returns (bytes4 success) { + require(msg.sender == _getERC20BridgeProxyAddress(), "DexForwarderBridge/SENDER_NOT_AUTHORIZED"); TransferFromState memory state; ( state.inputToken, diff --git a/contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol b/contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol index dcb72fe093..21755ac591 100644 --- a/contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol +++ b/contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol @@ -156,6 +156,13 @@ contract TestDexForwarderBridge is ITestDexForwarderBridge, DexForwarderBridge { + address private AUTHORIZED_ADDRESS; // solhint-disable-line var-name-mixedcase + + function setAuthorized(address authorized) + public + { + AUTHORIZED_ADDRESS = authorized; + } function createBridge( bytes4 returnCode, @@ -226,4 +233,12 @@ contract TestDexForwarderBridge is { return address(0); } + + function _getERC20BridgeProxyAddress() + internal + view + returns (address erc20BridgeProxyAddress) + { + return AUTHORIZED_ADDRESS; + } } diff --git a/contracts/asset-proxy/test/dex_forwarder_bridge.ts b/contracts/asset-proxy/test/dex_forwarder_bridge.ts index e5032f558a..687dc02c1e 100644 --- a/contracts/asset-proxy/test/dex_forwarder_bridge.ts +++ b/contracts/asset-proxy/test/dex_forwarder_bridge.ts @@ -8,7 +8,7 @@ import { randomAddress, shortZip, } from '@0x/contracts-test-utils'; -import { BigNumber, hexUtils } from '@0x/utils'; +import { BigNumber, hexUtils, NULL_ADDRESS } from '@0x/utils'; import { DecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; @@ -31,6 +31,7 @@ blockchainTests.resets('DexForwarderBridge unit tests', env => { const BRIDGE_FAILURE = '0xffffffff'; const BRIDGE_REVERT_ERROR = 'oopsie'; const INCOMPLETE_FILL_REVERT = 'DexForwarderBridge/INCOMPLETE_FILL'; + const NOT_AUTHORIZED_REVERT = 'DexForwarderBridge/SENDER_NOT_AUTHORIZED'; const DEFAULTS = { toAddress: randomAddress(), }; @@ -47,6 +48,7 @@ blockchainTests.resets('DexForwarderBridge unit tests', env => { await callAndTransactAsync(testContract.createToken()), await callAndTransactAsync(testContract.createToken()), ]; + await callAndTransactAsync(testContract.setAuthorized(env.txDefaults.from as string)); }); async function callAndTransactAsync(fnCall: ContractTxFunctionObj): Promise { @@ -186,6 +188,18 @@ blockchainTests.resets('DexForwarderBridge unit tests', env => { ).to.revertWith(INCOMPLETE_FILL_REVERT); }); + it('fails if not authorized', async () => { + const calls = goodBridgeCalls.slice(0, 1); + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls, + }); + await callAndTransactAsync(testContract.setAuthorized(NULL_ADDRESS)); + return expect(callBridgeTransferFromAsync({ bridgeData, sellAmount: new BigNumber(1) })).to.revertWith( + NOT_AUTHORIZED_REVERT, + ); + }); + it('succeeds with one bridge call', async () => { const calls = goodBridgeCalls.slice(0, 1); const bridgeData = dexForwarderBridgeDataEncoder.encode({ From c6379ca1d4d7621b7c80ea3675485027f07fe6cf Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Tue, 7 Apr 2020 15:58:26 +1000 Subject: [PATCH 11/13] Update DexForwarderBridge address --- packages/contract-addresses/addresses.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index 123a1d2202..273b87f4f5 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -30,7 +30,7 @@ "chainlinkStopLimit": "0xeb27220f95f364e1d9531992c48613f231839f53", "curveBridge": "0x6dc7950423ada9f56fb2c93a23edb787f1e29088", "maximumGasPrice": "0xe2bfd35306495d11e3c9db0d8de390cda24563cf", - "dexForwarderBridge": "0x2b135c732110be20db72e44ab2a4b149fa213599" + "dexForwarderBridge": "0x5591360f8c7640fea5771c9682d6b5ecb776e1f8" }, "3": { "erc20Proxy": "0xb1408f4c245a23c31b98d2c626777d4c0d766caa", From 434af0ae64068fe795a3386d4b10e7bf069ab021 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 7 Apr 2020 13:38:35 -0400 Subject: [PATCH 12/13] `@0x/contracts-asset-proxy`: Update CHANGELOG --- contracts/asset-proxy/CHANGELOG.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/asset-proxy/CHANGELOG.json b/contracts/asset-proxy/CHANGELOG.json index b14c13f75c..278f85db45 100644 --- a/contracts/asset-proxy/CHANGELOG.json +++ b/contracts/asset-proxy/CHANGELOG.json @@ -23,7 +23,8 @@ "pr": 2525 }, { - "note": "Add Gas Token freeing to `DexForwaderBridge` contract." + "note": "Add Gas Token freeing to `DexForwaderBridge` contract.", + "pr": 2536 } ] }, From c541340ef50c72ecac2924bcbc80fbdfbfa4f96e Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 7 Apr 2020 13:41:37 -0400 Subject: [PATCH 13/13] Update CHANGELOG.json `@0x/contract-addresses`: Redeploy `DexForwarderBridge` on Mainnet with Gas Token freeing `@0x/contract-addresses`: Revert to older Curve Bridge (without Gas Tokens) --- packages/contract-addresses/CHANGELOG.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index f0d7a7f954..f19125053b 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -31,10 +31,12 @@ "pr": 2521 }, { - "note": "Redeploy `DexForwarderBridge` on Mainnet with Gas Token freeing" + "note": "Redeploy `DexForwarderBridge` on Mainnet with Gas Token freeing", + "pr": 2536 }, { - "note": "Revert to older Curve Bridge (without Gas Tokens)" + "note": "Revert to older Curve Bridge (without Gas Tokens)", + "pr": 2536 } ] },