@0x/asset-swapper: Provide more accurate best quote price.

This commit is contained in:
Lawrence Forman
2020-01-16 14:37:15 -05:00
committed by Jacob Evans
parent 160c91f908
commit 09d05d09c9
10 changed files with 376 additions and 247 deletions

View File

@@ -17,6 +17,10 @@
{ {
"note": "Add exponential sampling distribution and `sampleDistributionBase` option to `SwapQuoter`", "note": "Add exponential sampling distribution and `sampleDistributionBase` option to `SwapQuoter`",
"pr": 2427 "pr": 2427
},
{
"note": "Compute more accurate best quote price",
"pr": 2427
} }
] ]
}, },

View File

@@ -54,6 +54,11 @@ export {
SwapQuoteConsumerError, SwapQuoteConsumerError,
SignedOrderWithFillableAmounts, SignedOrderWithFillableAmounts,
} from './types'; } from './types';
export { ERC20BridgeSource } from './utils/market_operation_utils/types'; export {
ERC20BridgeSource,
CollapsedFill,
NativeCollapsedFill,
OptimizedMarketOrder,
} from './utils/market_operation_utils/types';
export { affiliateFeeUtils } from './utils/affiliate_fee_utils'; export { affiliateFeeUtils } from './utils/affiliate_fee_utils';
export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; export { ProtocolFeeUtils } from './utils/protocol_fee_utils';

View File

@@ -29,9 +29,9 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
excludedSources: [], excludedSources: [],
bridgeSlippage: 0.0005, bridgeSlippage: 0.0005,
dustFractionThreshold: 0.0025, dustFractionThreshold: 0.0025,
numSamples: 12, numSamples: 13,
noConflicts: true, noConflicts: true,
sampleDistributionBase: 1.25, sampleDistributionBase: 1.05,
}; };
export const constants = { export const constants = {

View File

@@ -3,11 +3,17 @@ import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils';
import { AbiEncoder, BigNumber } from '@0x/utils'; import { AbiEncoder, BigNumber } from '@0x/utils';
import { constants } from '../../constants'; import { constants } from '../../constants';
import { SignedOrderWithFillableAmounts } from '../../types';
import { sortingUtils } from '../../utils/sorting_utils'; import { sortingUtils } from '../../utils/sorting_utils';
import { constants as marketOperationUtilConstants } from './constants'; import { constants as marketOperationUtilConstants } from './constants';
import { AggregationError, ERC20BridgeSource, Fill, FillData, NativeFillData, OrderDomain } from './types'; import {
AggregationError,
CollapsedFill,
ERC20BridgeSource,
NativeCollapsedFill,
OptimizedMarketOrder,
OrderDomain,
} from './types';
const { NULL_BYTES, NULL_ADDRESS, ZERO_AMOUNT } = constants; const { NULL_BYTES, NULL_ADDRESS, ZERO_AMOUNT } = constants;
const { INFINITE_TIMESTAMP_SEC, WALLET_SIGNATURE } = marketOperationUtilConstants; const { INFINITE_TIMESTAMP_SEC, WALLET_SIGNATURE } = marketOperationUtilConstants;
@@ -24,23 +30,21 @@ export class CreateOrderUtils {
orderDomain: OrderDomain, orderDomain: OrderDomain,
inputToken: string, inputToken: string,
outputToken: string, outputToken: string,
path: Fill[], path: CollapsedFill[],
bridgeSlippage: number, bridgeSlippage: number,
): SignedOrderWithFillableAmounts[] { ): OptimizedMarketOrder[] {
const orders: SignedOrderWithFillableAmounts[] = []; const orders: OptimizedMarketOrder[] = [];
for (const fill of path) { for (const fill of path) {
const source = (fill.fillData as FillData).source; if (fill.source === ERC20BridgeSource.Native) {
if (source === ERC20BridgeSource.Native) { orders.push(createNativeOrder(fill));
orders.push((fill.fillData as NativeFillData).order);
} else { } else {
orders.push( orders.push(
createBridgeOrder( createBridgeOrder(
orderDomain, orderDomain,
this._getBridgeAddressFromSource(source), fill,
this._getBridgeAddressFromSource(fill.source),
outputToken, outputToken,
inputToken, inputToken,
fill.output,
fill.input,
bridgeSlippage, bridgeSlippage,
), ),
); );
@@ -54,23 +58,21 @@ export class CreateOrderUtils {
orderDomain: OrderDomain, orderDomain: OrderDomain,
inputToken: string, inputToken: string,
outputToken: string, outputToken: string,
path: Fill[], path: CollapsedFill[],
bridgeSlippage: number, bridgeSlippage: number,
): SignedOrderWithFillableAmounts[] { ): OptimizedMarketOrder[] {
const orders: SignedOrderWithFillableAmounts[] = []; const orders: OptimizedMarketOrder[] = [];
for (const fill of path) { for (const fill of path) {
const source = (fill.fillData as FillData).source; if (fill.source === ERC20BridgeSource.Native) {
if (source === ERC20BridgeSource.Native) { orders.push(createNativeOrder(fill));
orders.push((fill.fillData as NativeFillData).order);
} else { } else {
orders.push( orders.push(
createBridgeOrder( createBridgeOrder(
orderDomain, orderDomain,
this._getBridgeAddressFromSource(source), fill,
this._getBridgeAddressFromSource(fill.source),
inputToken, inputToken,
outputToken, outputToken,
fill.input,
fill.output,
bridgeSlippage, bridgeSlippage,
true, true,
), ),
@@ -97,14 +99,13 @@ export class CreateOrderUtils {
function createBridgeOrder( function createBridgeOrder(
orderDomain: OrderDomain, orderDomain: OrderDomain,
fill: CollapsedFill,
bridgeAddress: string, bridgeAddress: string,
makerToken: string, makerToken: string,
takerToken: string, takerToken: string,
makerAssetAmount: BigNumber,
takerAssetAmount: BigNumber,
slippage: number, slippage: number,
isBuy: boolean = false, isBuy: boolean = false,
): SignedOrderWithFillableAmounts { ): OptimizedMarketOrder {
return { return {
makerAddress: bridgeAddress, makerAddress: bridgeAddress,
makerAssetData: assetDataUtils.encodeERC20BridgeAssetData( makerAssetData: assetDataUtils.encodeERC20BridgeAssetData(
@@ -113,7 +114,7 @@ function createBridgeOrder(
createBridgeData(takerToken), createBridgeData(takerToken),
), ),
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken), takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken),
...createCommonOrderFields(orderDomain, makerAssetAmount, takerAssetAmount, slippage, isBuy), ...createCommonOrderFields(orderDomain, fill, slippage, isBuy),
}; };
} }
@@ -123,24 +124,24 @@ function createBridgeData(tokenAddress: string): string {
} }
type CommonOrderFields = Pick< type CommonOrderFields = Pick<
SignedOrderWithFillableAmounts, OptimizedMarketOrder,
Exclude<keyof SignedOrderWithFillableAmounts, 'makerAddress' | 'makerAssetData' | 'takerAssetData'> Exclude<keyof OptimizedMarketOrder, 'makerAddress' | 'makerAssetData' | 'takerAssetData'>
>; >;
function createCommonOrderFields( function createCommonOrderFields(
orderDomain: OrderDomain, orderDomain: OrderDomain,
makerAssetAmount: BigNumber, fill: CollapsedFill,
takerAssetAmount: BigNumber,
slippage: number, slippage: number,
isBuy: boolean = false, isBuy: boolean = false,
): CommonOrderFields { ): CommonOrderFields {
const makerAssetAmountAdjustedWithSlippage = isBuy const makerAssetAmountAdjustedWithSlippage = isBuy
? makerAssetAmount ? fill.totalMakerAssetAmount
: makerAssetAmount.times(1 - slippage).integerValue(BigNumber.ROUND_DOWN); : fill.totalMakerAssetAmount.times(1 - slippage).integerValue(BigNumber.ROUND_DOWN);
const takerAssetAmountAdjustedWithSlippage = isBuy const takerAssetAmountAdjustedWithSlippage = isBuy
? takerAssetAmount.times(slippage + 1).integerValue(BigNumber.ROUND_UP) ? fill.totalTakerAssetAmount.times(slippage + 1).integerValue(BigNumber.ROUND_UP)
: takerAssetAmount; : fill.totalTakerAssetAmount;
return { return {
fill,
takerAddress: NULL_ADDRESS, takerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS, senderAddress: NULL_ADDRESS,
feeRecipientAddress: NULL_ADDRESS, feeRecipientAddress: NULL_ADDRESS,
@@ -159,3 +160,15 @@ function createCommonOrderFields(
...orderDomain, ...orderDomain,
}; };
} }
function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder {
return {
fill: {
source: fill.source,
totalMakerAssetAmount: fill.totalMakerAssetAmount,
totalTakerAssetAmount: fill.totalTakerAssetAmount,
subFills: fill.subFills,
},
...(fill as NativeCollapsedFill).nativeOrder,
};
}

View File

@@ -14,12 +14,16 @@ import { comparePathOutputs, FillsOptimizer, getPathOutput } from './fill_optimi
import { DexOrderSampler } from './sampler'; import { DexOrderSampler } from './sampler';
import { import {
AggregationError, AggregationError,
CollapsedFill,
DexSample, DexSample,
ERC20BridgeSource, ERC20BridgeSource,
Fill, Fill,
FillData, FillData,
FillFlags, FillFlags,
GetMarketOrdersOpts, GetMarketOrdersOpts,
NativeCollapsedFill,
NativeFillData,
OptimizedMarketOrder,
OrderDomain, OrderDomain,
} from './types'; } from './types';
@@ -53,7 +57,7 @@ export class MarketOperationUtils {
nativeOrders: SignedOrder[], nativeOrders: SignedOrder[],
takerAmount: BigNumber, takerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>, opts?: Partial<GetMarketOrdersOpts>,
): Promise<SignedOrderWithFillableAmounts[]> { ): Promise<OptimizedMarketOrder[]> {
if (nativeOrders.length === 0) { if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders); throw new Error(AggregationError.EmptyOrders);
} }
@@ -77,7 +81,7 @@ export class MarketOperationUtils {
takerAmount, takerAmount,
_opts.dustFractionThreshold, _opts.dustFractionThreshold,
); );
const clippedNativePath = clipPathToInput(prunedNativePath, takerAmount); const clippedNativePath = clipPathToInput(sortFillsByPrice(prunedNativePath), takerAmount);
const dexPaths = createPathsFromDexQuotes(dexQuotes, _opts.noConflicts); const dexPaths = createPathsFromDexQuotes(dexQuotes, _opts.noConflicts);
const allPaths = [...dexPaths]; const allPaths = [...dexPaths];
const allFills = flattenDexPaths(dexPaths); const allFills = flattenDexPaths(dexPaths);
@@ -105,7 +109,7 @@ export class MarketOperationUtils {
this._orderDomain, this._orderDomain,
inputToken, inputToken,
outputToken, outputToken,
simplifyPath(optimalPath), collapsePath(optimalPath, false),
_opts.bridgeSlippage, _opts.bridgeSlippage,
); );
} }
@@ -122,7 +126,7 @@ export class MarketOperationUtils {
nativeOrders: SignedOrder[], nativeOrders: SignedOrder[],
makerAmount: BigNumber, makerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>, opts?: Partial<GetMarketOrdersOpts>,
): Promise<SignedOrderWithFillableAmounts[]> { ): Promise<OptimizedMarketOrder[]> {
if (nativeOrders.length === 0) { if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders); throw new Error(AggregationError.EmptyOrders);
} }
@@ -161,7 +165,7 @@ export class MarketOperationUtils {
batchNativeOrders: SignedOrder[][], batchNativeOrders: SignedOrder[][],
makerAmounts: BigNumber[], makerAmounts: BigNumber[],
opts?: Partial<GetMarketOrdersOpts>, opts?: Partial<GetMarketOrdersOpts>,
): Promise<Array<SignedOrderWithFillableAmounts[] | undefined>> { ): Promise<Array<OptimizedMarketOrder[] | undefined>> {
if (batchNativeOrders.length === 0) { if (batchNativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders); throw new Error(AggregationError.EmptyOrders);
} }
@@ -192,7 +196,7 @@ export class MarketOperationUtils {
nativeOrderFillableAmounts: BigNumber[], nativeOrderFillableAmounts: BigNumber[],
dexQuotes: DexSample[][], dexQuotes: DexSample[][],
opts: GetMarketOrdersOpts, opts: GetMarketOrdersOpts,
): SignedOrderWithFillableAmounts[] | undefined { ): OptimizedMarketOrder[] | undefined {
const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts( const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts(
nativeOrders, nativeOrders,
nativeOrderFillableAmounts, nativeOrderFillableAmounts,
@@ -203,7 +207,7 @@ export class MarketOperationUtils {
makerAmount, makerAmount,
opts.dustFractionThreshold, opts.dustFractionThreshold,
); );
const clippedNativePath = clipPathToInput(prunedNativePath, makerAmount); const clippedNativePath = clipPathToInput(sortFillsByPrice(prunedNativePath).reverse(), makerAmount);
const dexPaths = createPathsFromDexQuotes(dexQuotes, opts.noConflicts); const dexPaths = createPathsFromDexQuotes(dexQuotes, opts.noConflicts);
const allPaths = [...dexPaths]; const allPaths = [...dexPaths];
const allFills = flattenDexPaths(dexPaths); const allFills = flattenDexPaths(dexPaths);
@@ -230,7 +234,7 @@ export class MarketOperationUtils {
this._orderDomain, this._orderDomain,
inputToken, inputToken,
outputToken, outputToken,
simplifyPath(optimalPath), collapsePath(optimalPath, true),
opts.bridgeSlippage, opts.bridgeSlippage,
); );
} }
@@ -242,26 +246,24 @@ function createSignedOrdersWithFillableAmounts(
operation: MarketOperation, operation: MarketOperation,
): SignedOrderWithFillableAmounts[] { ): SignedOrderWithFillableAmounts[] {
return signedOrders return signedOrders
.map( .map((order: SignedOrder, i: number) => {
(order: SignedOrder, i: number): SignedOrderWithFillableAmounts => { const fillableAmount = fillableAmounts[i];
const fillableAmount = fillableAmounts[i]; const fillableMakerAssetAmount =
const fillableMakerAssetAmount = operation === MarketOperation.Buy
operation === MarketOperation.Buy ? fillableAmount
? fillableAmount : orderCalculationUtils.getMakerFillAmount(order, fillableAmount);
: orderCalculationUtils.getMakerFillAmount(order, fillableAmount); const fillableTakerAssetAmount =
const fillableTakerAssetAmount = operation === MarketOperation.Sell
operation === MarketOperation.Sell ? fillableAmount
? fillableAmount : orderCalculationUtils.getTakerFillAmount(order, fillableAmount);
: orderCalculationUtils.getTakerFillAmount(order, fillableAmount); const fillableTakerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, fillableTakerAssetAmount);
const fillableTakerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, fillableTakerAssetAmount); return {
return { fillableMakerAssetAmount,
fillableMakerAssetAmount, fillableTakerAssetAmount,
fillableTakerAssetAmount, fillableTakerFeeAmount,
fillableTakerFeeAmount, ...order,
...order, };
}; })
},
)
.filter(order => { .filter(order => {
return !order.fillableMakerAssetAmount.isZero() && !order.fillableTakerAssetAmount.isZero(); return !order.fillableMakerAssetAmount.isZero() && !order.fillableTakerAssetAmount.isZero();
}); });
@@ -412,23 +414,31 @@ function getPathInput(path: Fill[]): BigNumber {
} }
// Merges contiguous fills from the same DEX. // Merges contiguous fills from the same DEX.
function simplifyPath(path: Fill[]): Fill[] { function collapsePath(path: Fill[], isBuy: boolean): CollapsedFill[] {
const simplified: Fill[] = []; const collapsed: Array<CollapsedFill | NativeCollapsedFill> = [];
for (const fill of path) { for (const fill of path) {
const makerAssetAmount = isBuy ? fill.input : fill.output;
const takerAssetAmount = isBuy ? fill.output : fill.input;
const source = (fill.fillData as FillData).source; const source = (fill.fillData as FillData).source;
if (simplified.length !== 0 && source !== ERC20BridgeSource.Native) { if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) {
const prevFill = simplified[simplified.length - 1]; const prevFill = collapsed[collapsed.length - 1];
const prevSource = (prevFill.fillData as FillData).source;
// If the last fill is from the same source, merge them. // If the last fill is from the same source, merge them.
if (prevSource === source) { if (prevFill.source === source) {
prevFill.input = prevFill.input.plus(fill.input); prevFill.totalMakerAssetAmount = prevFill.totalMakerAssetAmount.plus(makerAssetAmount);
prevFill.output = prevFill.output.plus(fill.output); prevFill.totalTakerAssetAmount = prevFill.totalTakerAssetAmount.plus(takerAssetAmount);
prevFill.subFills.push({ makerAssetAmount, takerAssetAmount });
continue; continue;
} }
} }
simplified.push(fill); collapsed.push({
source: fill.fillData.source,
totalMakerAssetAmount: makerAssetAmount,
totalTakerAssetAmount: takerAssetAmount,
subFills: [{ makerAssetAmount, takerAssetAmount }],
nativeOrder: (fill.fillData as NativeFillData).order,
});
} }
return simplified; return collapsed;
} }
// Sort fills by descending price. // Sort fills by descending price.

View File

@@ -79,6 +79,48 @@ export interface Fill {
fillData: FillData | NativeFillData; fillData: FillData | NativeFillData;
} }
/**
* Represents continguous fills on a path that have been merged together.
*/
export interface CollapsedFill {
/**
* The source DEX.
*/
source: ERC20BridgeSource;
/**
* Total maker asset amount.
*/
totalMakerAssetAmount: BigNumber;
/**
* Total taker asset amount.
*/
totalTakerAssetAmount: BigNumber;
/**
* All the fill asset amounts that were collapsed into this node.
*/
subFills: Array<{
makerAssetAmount: BigNumber;
takerAssetAmount: BigNumber;
}>;
}
/**
* A `CollapsedFill` wrapping a native order.
*/
export interface NativeCollapsedFill extends CollapsedFill {
nativeOrder: SignedOrderWithFillableAmounts;
}
/**
* Optimized orders to fill.
*/
export interface OptimizedMarketOrder extends SignedOrderWithFillableAmounts {
/**
* The optimized fills that generated this order.
*/
fill: CollapsedFill;
}
/** /**
* Options for `getMarketSellOrdersAsync()` and `getMarketBuyOrdersAsync()`. * Options for `getMarketSellOrdersAsync()` and `getMarketBuyOrdersAsync()`.
*/ */

View File

@@ -16,6 +16,7 @@ import {
import { fillableAmountsUtils } from './fillable_amounts_utils'; import { fillableAmountsUtils } from './fillable_amounts_utils';
import { MarketOperationUtils } from './market_operation_utils'; import { MarketOperationUtils } from './market_operation_utils';
import { ERC20BridgeSource, OptimizedMarketOrder } from './market_operation_utils/types';
import { ProtocolFeeUtils } from './protocol_fee_utils'; import { ProtocolFeeUtils } from './protocol_fee_utils';
import { utils } from './utils'; import { utils } from './utils';
@@ -79,6 +80,7 @@ export class SwapQuoteCalculator {
opts, opts,
)) as Array<MarketBuySwapQuote | undefined>; )) as Array<MarketBuySwapQuote | undefined>;
} }
private async _calculateBatchBuySwapQuoteAsync( private async _calculateBatchBuySwapQuoteAsync(
batchPrunedOrders: SignedOrder[][], batchPrunedOrders: SignedOrder[][],
assetFillAmounts: BigNumber[], assetFillAmounts: BigNumber[],
@@ -106,7 +108,6 @@ export class SwapQuoteCalculator {
operation, operation,
assetFillAmounts[i], assetFillAmounts[i],
gasPrice, gasPrice,
opts,
); );
} else { } else {
return undefined; return undefined;
@@ -126,7 +127,7 @@ export class SwapQuoteCalculator {
// since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled // since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled
const slippageBufferAmount = assetFillAmount.multipliedBy(slippagePercentage).integerValue(); const slippageBufferAmount = assetFillAmount.multipliedBy(slippagePercentage).integerValue();
let resultOrders: SignedOrderWithFillableAmounts[] = []; let resultOrders: OptimizedMarketOrder[] = [];
if (operation === MarketOperation.Buy) { if (operation === MarketOperation.Buy) {
resultOrders = await this._marketOperationUtils.getMarketBuyOrdersAsync( resultOrders = await this._marketOperationUtils.getMarketBuyOrdersAsync(
@@ -151,37 +152,35 @@ export class SwapQuoteCalculator {
operation, operation,
assetFillAmount, assetFillAmount,
gasPrice, gasPrice,
opts,
); );
} }
private async _createSwapQuoteAsync( private async _createSwapQuoteAsync(
makerAssetData: string, makerAssetData: string,
takerAssetData: string, takerAssetData: string,
resultOrders: SignedOrderWithFillableAmounts[], resultOrders: OptimizedMarketOrder[],
operation: MarketOperation, operation: MarketOperation,
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
gasPrice: BigNumber, gasPrice: BigNumber,
opts: CalculateSwapQuoteOpts,
): Promise<SwapQuote> { ): Promise<SwapQuote> {
const bestCaseQuoteInfo = await this._calculateQuoteInfoAsync( const bestCaseQuoteInfo = await this._calculateQuoteInfoAsync(
createBestCaseOrders(resultOrders, operation, opts.bridgeSlippage), resultOrders,
assetFillAmount, assetFillAmount,
gasPrice, gasPrice,
operation, operation,
); );
// in order to calculate the maxRate, reverse the ordersAndFillableAmounts
// such that they are sorted from worst rate to best rate
const worstCaseQuoteInfo = await this._calculateQuoteInfoAsync( const worstCaseQuoteInfo = await this._calculateQuoteInfoAsync(
_.reverse(resultOrders.slice()), resultOrders,
assetFillAmount, assetFillAmount,
gasPrice, gasPrice,
operation, operation,
true,
); );
const quoteBase = { const quoteBase = {
takerAssetData, takerAssetData,
makerAssetData, makerAssetData,
orders: resultOrders, // Remove fill metadata.
orders: resultOrders.map(o => _.omit(o, 'fill')) as SignedOrderWithFillableAmounts[],
bestCaseQuoteInfo, bestCaseQuoteInfo,
worstCaseQuoteInfo, worstCaseQuoteInfo,
gasPrice, gasPrice,
@@ -204,31 +203,39 @@ export class SwapQuoteCalculator {
// tslint:disable-next-line: prefer-function-over-method // tslint:disable-next-line: prefer-function-over-method
private async _calculateQuoteInfoAsync( private async _calculateQuoteInfoAsync(
prunedOrders: SignedOrderWithFillableAmounts[], orders: OptimizedMarketOrder[],
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
gasPrice: BigNumber, gasPrice: BigNumber,
operation: MarketOperation, operation: MarketOperation,
worstCase: boolean = false,
): Promise<SwapQuoteInfo> { ): Promise<SwapQuoteInfo> {
if (operation === MarketOperation.Buy) { if (operation === MarketOperation.Buy) {
return this._calculateMarketBuyQuoteInfoAsync(prunedOrders, assetFillAmount, gasPrice); return this._calculateMarketBuyQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase);
} else { } else {
return this._calculateMarketSellQuoteInfoAsync(prunedOrders, assetFillAmount, gasPrice); return this._calculateMarketSellQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase);
} }
} }
private async _calculateMarketSellQuoteInfoAsync( private async _calculateMarketSellQuoteInfoAsync(
prunedOrders: SignedOrderWithFillableAmounts[], orders: OptimizedMarketOrder[],
takerAssetSellAmount: BigNumber, takerAssetSellAmount: BigNumber,
gasPrice: BigNumber, gasPrice: BigNumber,
worstCase: boolean = false,
): Promise<SwapQuoteInfo> { ): Promise<SwapQuoteInfo> {
const result = prunedOrders.reduce( let totalMakerAssetAmount = constants.ZERO_AMOUNT;
(acc, order) => { let totalTakerAssetAmount = constants.ZERO_AMOUNT;
const { let totalFeeTakerAssetAmount = constants.ZERO_AMOUNT;
totalMakerAssetAmount, let remainingTakerAssetFillAmount = takerAssetSellAmount;
totalTakerAssetAmount, const filledOrders = [] as OptimizedMarketOrder[];
totalFeeTakerAssetAmount, const _orders = !worstCase ? orders : orders.slice().reverse();
remainingTakerAssetFillAmount, for (const order of _orders) {
} = acc; 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.getMakerAssetAmountSwappedAfterFees( const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(
order, order,
); );
@@ -239,99 +246,155 @@ export class SwapQuoteCalculator {
remainingTakerAssetFillAmount, remainingTakerAssetFillAmount,
adjustedFillableTakerAssetAmount, adjustedFillableTakerAssetAmount,
); );
const { takerAssetAmount, feeTakerAssetAmount } = getTakerAssetAmountBreakDown( const takerAssetAmountBreakDown = getTakerAssetAmountBreakDown(order, takerAssetAmountWithFees);
order, takerAssetAmount = takerAssetAmountBreakDown.takerAssetAmount;
takerAssetAmountWithFees, feeTakerAssetAmount = takerAssetAmountBreakDown.feeTakerAssetAmount;
); makerAssetAmount = takerAssetAmountWithFees
const makerAssetAmount = takerAssetAmountWithFees
.div(adjustedFillableTakerAssetAmount) .div(adjustedFillableTakerAssetAmount)
.multipliedBy(adjustedFillableMakerAssetAmount) .times(adjustedFillableMakerAssetAmount)
.integerValue(BigNumber.ROUND_DOWN); .integerValue(BigNumber.ROUND_DOWN);
return { } else {
totalMakerAssetAmount: totalMakerAssetAmount.plus(makerAssetAmount), // This is a collapsed bridge order.
totalTakerAssetAmount: totalTakerAssetAmount.plus(takerAssetAmount), // Because collapsed bridge orders actually fill at different rates,
totalFeeTakerAssetAmount: totalFeeTakerAssetAmount.plus(feeTakerAssetAmount), // we can iterate over the uncollapsed fills to get the actual
remainingTakerAssetFillAmount: BigNumber.max( // asset amounts transfered.
constants.ZERO_AMOUNT, // We can also assume there are no fees and the order is not
remainingTakerAssetFillAmount.minus(takerAssetAmountWithFees), // partially filled.
),
}; // Infer the bridge slippage from the difference between the fill
}, // size and the atual order asset amounts.
{ const makerAssetBridgeSlippage = !worstCase
totalMakerAssetAmount: constants.ZERO_AMOUNT, ? constants.ONE_AMOUNT
totalTakerAssetAmount: constants.ZERO_AMOUNT, : order.makerAssetAmount.div(order.fill.totalMakerAssetAmount);
totalFeeTakerAssetAmount: constants.ZERO_AMOUNT, const takerAssetBridgeSlippage = !worstCase
remainingTakerAssetFillAmount: takerAssetSellAmount, ? 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( const protocolFeeInWeiAmount = await this._protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(
prunedOrders, filledOrders,
gasPrice, gasPrice,
); );
return { return {
feeTakerAssetAmount: result.totalFeeTakerAssetAmount, feeTakerAssetAmount: totalFeeTakerAssetAmount,
takerAssetAmount: result.totalTakerAssetAmount, takerAssetAmount: totalTakerAssetAmount,
totalTakerAssetAmount: result.totalFeeTakerAssetAmount.plus(result.totalTakerAssetAmount), totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount),
makerAssetAmount: result.totalMakerAssetAmount, makerAssetAmount: totalMakerAssetAmount,
protocolFeeInWeiAmount, protocolFeeInWeiAmount,
}; };
} }
private async _calculateMarketBuyQuoteInfoAsync( private async _calculateMarketBuyQuoteInfoAsync(
prunedOrders: SignedOrderWithFillableAmounts[], orders: OptimizedMarketOrder[],
makerAssetBuyAmount: BigNumber, makerAssetBuyAmount: BigNumber,
gasPrice: BigNumber, gasPrice: BigNumber,
worstCase: boolean = false,
): Promise<SwapQuoteInfo> { ): Promise<SwapQuoteInfo> {
const result = prunedOrders.reduce( let totalMakerAssetAmount = constants.ZERO_AMOUNT;
(acc, order) => { let totalTakerAssetAmount = constants.ZERO_AMOUNT;
const { let totalFeeTakerAssetAmount = constants.ZERO_AMOUNT;
totalMakerAssetAmount, let remainingMakerAssetFillAmount = makerAssetBuyAmount;
totalTakerAssetAmount, const filledOrders = [] as OptimizedMarketOrder[];
totalFeeTakerAssetAmount, const _orders = !worstCase ? orders : orders.slice().reverse();
remainingMakerAssetFillAmount, for (const order of _orders) {
} = acc; 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.getMakerAssetAmountSwappedAfterFees( const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(
order, order,
); );
const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees( const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees(
order, order,
); );
const makerFillAmount = BigNumber.min(remainingMakerAssetFillAmount, adjustedFillableMakerAssetAmount); makerAssetAmount = BigNumber.min(remainingMakerAssetFillAmount, adjustedFillableMakerAssetAmount);
const takerAssetAmountWithFees = makerFillAmount const takerAssetAmountWithFees = makerAssetAmount
.div(adjustedFillableMakerAssetAmount) .div(adjustedFillableMakerAssetAmount)
.multipliedBy(adjustedFillableTakerAssetAmount) .multipliedBy(adjustedFillableTakerAssetAmount)
.integerValue(BigNumber.ROUND_UP); .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.
const { takerAssetAmount, feeTakerAssetAmount } = getTakerAssetAmountBreakDown( // Infer the bridge slippage from the difference between the fill
order, // size and the atual order asset amounts.
takerAssetAmountWithFees, const makerAssetBridgeSlippage = !worstCase
); ? constants.ONE_AMOUNT
return { : order.makerAssetAmount.div(order.fill.totalMakerAssetAmount);
totalMakerAssetAmount: totalMakerAssetAmount.plus(makerFillAmount), const takerAssetBridgeSlippage = !worstCase
totalTakerAssetAmount: totalTakerAssetAmount.plus(takerAssetAmount), ? constants.ONE_AMOUNT
totalFeeTakerAssetAmount: totalFeeTakerAssetAmount.plus(feeTakerAssetAmount), : order.takerAssetAmount.div(order.fill.totalTakerAssetAmount);
remainingMakerAssetFillAmount: BigNumber.max( // Consecutively fill the subfills in this order.
constants.ZERO_AMOUNT, const subFills = !worstCase ? order.fill.subFills : order.fill.subFills.slice().reverse();
remainingMakerAssetFillAmount.minus(makerFillAmount), for (const subFill of subFills) {
), if (remainingMakerAssetFillAmount.minus(makerAssetAmount).lte(0)) {
}; break;
}, }
{ const partialTakerAssetAmount = subFill.takerAssetAmount.times(takerAssetBridgeSlippage);
totalMakerAssetAmount: constants.ZERO_AMOUNT, const partialMakerAssetAmount = subFill.makerAssetAmount.times(makerAssetBridgeSlippage);
totalTakerAssetAmount: constants.ZERO_AMOUNT, const partialMakerAssetFillAmount = BigNumber.min(
totalFeeTakerAssetAmount: constants.ZERO_AMOUNT, partialMakerAssetAmount,
remainingMakerAssetFillAmount: makerAssetBuyAmount, 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( const protocolFeeInWeiAmount = await this._protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(
prunedOrders, filledOrders,
gasPrice, gasPrice,
); );
return { return {
feeTakerAssetAmount: result.totalFeeTakerAssetAmount, feeTakerAssetAmount: totalFeeTakerAssetAmount,
takerAssetAmount: result.totalTakerAssetAmount, takerAssetAmount: totalTakerAssetAmount,
totalTakerAssetAmount: result.totalFeeTakerAssetAmount.plus(result.totalTakerAssetAmount), totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount),
makerAssetAmount: result.totalMakerAssetAmount, makerAssetAmount: totalMakerAssetAmount,
protocolFeeInWeiAmount, protocolFeeInWeiAmount,
}; };
} }
@@ -372,35 +435,3 @@ function getTakerAssetAmountBreakDown(
takerAssetAmount: takerAssetAmountWithFees, takerAssetAmount: takerAssetAmountWithFees,
}; };
} }
function createBestCaseOrders(
orders: SignedOrderWithFillableAmounts[],
operation: MarketOperation,
bridgeSlippage: number,
): SignedOrderWithFillableAmounts[] {
// Scale the asset amounts to undo the bridge slippage applied to
// bridge orders.
const bestCaseOrders: SignedOrderWithFillableAmounts[] = [];
for (const order of orders) {
if (!order.makerAssetData.startsWith(constants.BRIDGE_ASSET_DATA_PREFIX)) {
bestCaseOrders.push(order);
continue;
}
bestCaseOrders.push({
...order,
...(operation === MarketOperation.Sell
? {
makerAssetAmount: order.makerAssetAmount
.dividedBy(1 - bridgeSlippage)
.integerValue(BigNumber.ROUND_DOWN),
}
: // Buy operation
{
takerAssetAmount: order.takerAssetAmount
.dividedBy(bridgeSlippage + 1)
.integerValue(BigNumber.ROUND_UP),
}),
});
}
return bestCaseOrders;
}

View File

@@ -23,7 +23,7 @@ describe('#calculateLiquidity', () => {
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity( const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders, prunedSignedOrders,
); );
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(10)); expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(11));
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(9)); expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(9));
}); });
it('should provide correct liquidity result with orders with takerFees in takerAsset', () => { it('should provide correct liquidity result with orders with takerFees in takerAsset', () => {
@@ -31,7 +31,7 @@ describe('#calculateLiquidity', () => {
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity( const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders, prunedSignedOrders,
); );
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(10)); expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(11));
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(15)); expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(15));
}); });
it('should provide correct liquidity result with orders with takerFees in makerAsset', () => { it('should provide correct liquidity result with orders with takerFees in makerAsset', () => {
@@ -51,7 +51,7 @@ describe('#calculateLiquidity', () => {
const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity( const { makerAssetAvailableInBaseUnits, takerAssetAvailableInBaseUnits } = calculateLiquidity(
prunedSignedOrders, prunedSignedOrders,
); );
expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(25)); expect(makerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(27));
expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(33)); expect(takerAssetAvailableInBaseUnits).to.bignumber.eq(baseUnitAmount(33));
}); });
}); });

View File

@@ -65,6 +65,9 @@ const createSamplerFromSignedOrdersWithFillableAmounts = (
return sampler; return sampler;
}; };
// TODO(dorothy-zbornak): Replace these tests entirely with unit tests because
// omg they're a nightmare to maintain.
// tslint:disable:max-file-line-count // tslint:disable:max-file-line-count
// tslint:disable:custom-no-magic-numbers // tslint:disable:custom-no-magic-numbers
describe('swapQuoteCalculator', () => { describe('swapQuoteCalculator', () => {
@@ -272,7 +275,7 @@ describe('swapQuoteCalculator', () => {
}); });
}); });
it('calculates a correct swapQuote with slippage (feeless orders)', async () => { it('calculates a correct swapQuote with slippage (feeless orders)', async () => {
const assetSellAmount = baseUnitAmount(1); const assetSellAmount = baseUnitAmount(4);
const slippagePercentage = 0.2; const slippagePercentage = 0.2;
const sampler = createSamplerFromSignedOrdersWithFillableAmounts( const sampler = createSamplerFromSignedOrdersWithFillableAmounts(
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS,
@@ -289,10 +292,10 @@ describe('swapQuoteCalculator', () => {
GAS_PRICE, GAS_PRICE,
CALCULATE_SWAP_QUOTE_OPTS, CALCULATE_SWAP_QUOTE_OPTS,
); );
// test if orders are correct // test if orders are correct
expect(swapQuote.orders).to.deep.equal([ expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0], testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[2],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[1], testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[1],
]); ]);
expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount); expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount);
@@ -301,15 +304,15 @@ describe('swapQuoteCalculator', () => {
feeTakerAssetAmount: baseUnitAmount(0), feeTakerAssetAmount: baseUnitAmount(0),
takerAssetAmount: assetSellAmount, takerAssetAmount: assetSellAmount,
totalTakerAssetAmount: assetSellAmount, totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(6), makerAssetAmount: baseUnitAmount(9),
protocolFeeInWeiAmount: baseUnitAmount(30, 4), protocolFeeInWeiAmount: baseUnitAmount(30, 4),
}); });
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(0), feeTakerAssetAmount: baseUnitAmount(0),
takerAssetAmount: assetSellAmount, takerAssetAmount: assetSellAmount,
totalTakerAssetAmount: assetSellAmount, totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(0.4), makerAssetAmount: baseUnitAmount(1.6),
protocolFeeInWeiAmount: baseUnitAmount(30, 4), protocolFeeInWeiAmount: baseUnitAmount(15, 4),
}); });
}); });
it('calculates a correct swapQuote with no slippage (takerAsset denominated fee orders)', async () => { it('calculates a correct swapQuote with no slippage (takerAsset denominated fee orders)', async () => {
@@ -372,7 +375,7 @@ describe('swapQuoteCalculator', () => {
// test if orders are correct // test if orders are correct
expect(swapQuote.orders).to.deep.equal([ 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[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[1], testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[2],
]); ]);
expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount); expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount);
// test if rates are correct // test if rates are correct
@@ -381,14 +384,14 @@ describe('swapQuoteCalculator', () => {
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2.25)), takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2.25)),
totalTakerAssetAmount: assetSellAmount, totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(4.5), makerAssetAmount: baseUnitAmount(4.5),
protocolFeeInWeiAmount: baseUnitAmount(30, 4), protocolFeeInWeiAmount: baseUnitAmount(15, 4),
}); });
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(0.5), feeTakerAssetAmount: baseUnitAmount(1.2),
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(0.5)), takerAssetAmount: assetSellAmount.minus(baseUnitAmount(1.2)),
totalTakerAssetAmount: assetSellAmount, totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(1), makerAssetAmount: baseUnitAmount(1.8),
protocolFeeInWeiAmount: baseUnitAmount(30, 4), protocolFeeInWeiAmount: baseUnitAmount(15, 4),
}); });
}); });
it('calculates a correct swapQuote with no slippage (makerAsset denominated fee orders)', async () => { it('calculates a correct swapQuote with no slippage (makerAsset denominated fee orders)', async () => {
@@ -411,23 +414,24 @@ describe('swapQuoteCalculator', () => {
); );
// test if orders are correct // test if orders are correct
expect(swapQuote.orders).to.deep.equal([ expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[0], 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); expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount);
// test if rates are correct // test if rates are correct
expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2), feeTakerAssetAmount: baseUnitAmount(1.5).minus(1),
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2)), takerAssetAmount: assetSellAmount.minus(baseUnitAmount(1.5)).plus(1),
totalTakerAssetAmount: assetSellAmount, totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(0.8), makerAssetAmount: baseUnitAmount(4),
protocolFeeInWeiAmount: baseUnitAmount(15, 4), protocolFeeInWeiAmount: baseUnitAmount(30, 4),
}); });
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2), feeTakerAssetAmount: baseUnitAmount(1.5).minus(1),
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2)), takerAssetAmount: assetSellAmount.minus(baseUnitAmount(1.5)).plus(1),
totalTakerAssetAmount: assetSellAmount, totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(0.8), makerAssetAmount: baseUnitAmount(4),
protocolFeeInWeiAmount: baseUnitAmount(15, 4), protocolFeeInWeiAmount: baseUnitAmount(30, 4),
}); });
}); });
it('calculates a correct swapQuote with slippage (makerAsset denominated fee orders)', async () => { it('calculates a correct swapQuote with slippage (makerAsset denominated fee orders)', async () => {
@@ -451,24 +455,24 @@ describe('swapQuoteCalculator', () => {
// test if orders are correct // test if orders are correct
expect(swapQuote.orders).to.deep.equal([ 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[1],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[2],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[0], testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[0],
]); ]);
expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount); expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount);
// test if rates are correct // test if rates are correct
// 50 takerAsset units to fill the first order + 100 takerAsset units for fees 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({ expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2), feeTakerAssetAmount: baseUnitAmount(2),
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2)), takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2)),
totalTakerAssetAmount: assetSellAmount, totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(0.8), makerAssetAmount: baseUnitAmount(0.8),
protocolFeeInWeiAmount: baseUnitAmount(30, 4), protocolFeeInWeiAmount: baseUnitAmount(15, 4),
});
expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2),
takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2)),
totalTakerAssetAmount: assetSellAmount,
makerAssetAmount: baseUnitAmount(3.6),
protocolFeeInWeiAmount: baseUnitAmount(30, 4),
}); });
}); });
}); });
@@ -678,7 +682,7 @@ describe('swapQuoteCalculator', () => {
// test if orders are correct // test if orders are correct
expect(swapQuote.orders).to.deep.equal([ expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0], testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[1], testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[2],
]); ]);
expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount);
@@ -692,12 +696,16 @@ describe('swapQuoteCalculator', () => {
takerAssetAmount, takerAssetAmount,
totalTakerAssetAmount: takerAssetAmount, totalTakerAssetAmount: takerAssetAmount,
makerAssetAmount: assetBuyAmount, makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(30, 4), protocolFeeInWeiAmount: baseUnitAmount(15, 4),
}); });
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(0), feeTakerAssetAmount: baseUnitAmount(0),
takerAssetAmount: baseUnitAmount(5.5), takerAssetAmount: baseUnitAmount(20)
totalTakerAssetAmount: baseUnitAmount(5.5), .div(6)
.integerValue(BigNumber.ROUND_UP),
totalTakerAssetAmount: baseUnitAmount(20)
.div(6)
.integerValue(BigNumber.ROUND_UP),
makerAssetAmount: assetBuyAmount, makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(30, 4), protocolFeeInWeiAmount: baseUnitAmount(30, 4),
}); });
@@ -766,7 +774,7 @@ describe('swapQuoteCalculator', () => {
// test if orders are correct // test if orders are correct
expect(swapQuote.orders).to.deep.equal([ 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[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[1], testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[2],
]); ]);
expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount);
// test if rates are correct // test if rates are correct
@@ -775,12 +783,16 @@ describe('swapQuoteCalculator', () => {
takerAssetAmount: fiveSixthEthInWei, takerAssetAmount: fiveSixthEthInWei,
totalTakerAssetAmount: baseUnitAmount(2.5).plus(fiveSixthEthInWei), totalTakerAssetAmount: baseUnitAmount(2.5).plus(fiveSixthEthInWei),
makerAssetAmount: assetBuyAmount, makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(30, 4), protocolFeeInWeiAmount: baseUnitAmount(15, 4),
}); });
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2.5), feeTakerAssetAmount: baseUnitAmount(3),
takerAssetAmount: baseUnitAmount(5.5), takerAssetAmount: baseUnitAmount(10)
totalTakerAssetAmount: baseUnitAmount(8), .div(3)
.integerValue(BigNumber.ROUND_UP), // 3.3333...
totalTakerAssetAmount: baseUnitAmount(19)
.div(3)
.integerValue(BigNumber.ROUND_UP), // 6.3333...
makerAssetAmount: assetBuyAmount, makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(30, 4), protocolFeeInWeiAmount: baseUnitAmount(30, 4),
}); });
@@ -805,21 +817,33 @@ describe('swapQuoteCalculator', () => {
); );
// test if orders are correct // test if orders are correct
expect(swapQuote.orders).to.deep.equal([ expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[0], testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[1],
]); ]);
expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount);
// test if rates are correct // test if rates are correct
expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2.5), feeTakerAssetAmount: baseUnitAmount(0.5)
takerAssetAmount: baseUnitAmount(2.5), .div(3)
totalTakerAssetAmount: baseUnitAmount(5), .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, makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(15, 4), protocolFeeInWeiAmount: baseUnitAmount(15, 4),
}); });
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2.5), feeTakerAssetAmount: baseUnitAmount(0.5)
takerAssetAmount: baseUnitAmount(2.5), .div(3)
totalTakerAssetAmount: baseUnitAmount(5), .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, makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(15, 4), protocolFeeInWeiAmount: baseUnitAmount(15, 4),
}); });
@@ -845,14 +869,14 @@ describe('swapQuoteCalculator', () => {
// test if orders are correct // test if orders are correct
expect(swapQuote.orders).to.deep.equal([ 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[1],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[0], testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[2],
]); ]);
expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount);
// test if rates are correct // test if rates are correct
expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({
feeTakerAssetAmount: baseUnitAmount(2.75), feeTakerAssetAmount: baseUnitAmount(1.25).minus(1),
takerAssetAmount: baseUnitAmount(2.75), takerAssetAmount: baseUnitAmount(2.25).plus(1),
totalTakerAssetAmount: baseUnitAmount(5.5), totalTakerAssetAmount: baseUnitAmount(3.5),
makerAssetAmount: assetBuyAmount, makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(30, 4), protocolFeeInWeiAmount: baseUnitAmount(30, 4),
}); });
@@ -879,7 +903,7 @@ describe('swapQuoteCalculator', () => {
.multipliedBy(0.1) .multipliedBy(0.1)
.integerValue(BigNumber.ROUND_CEIL), .integerValue(BigNumber.ROUND_CEIL),
makerAssetAmount: assetBuyAmount, makerAssetAmount: assetBuyAmount,
protocolFeeInWeiAmount: baseUnitAmount(30, 4), protocolFeeInWeiAmount: baseUnitAmount(15, 4),
}); });
}); });
}); });

View File

@@ -51,7 +51,7 @@ const PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS: Array<Partial<SignedOrderWit
takerAssetAmount: baseUnitAmount(6), takerAssetAmount: baseUnitAmount(6),
makerAssetAmount: baseUnitAmount(6), makerAssetAmount: baseUnitAmount(6),
fillableTakerAssetAmount: baseUnitAmount(3), fillableTakerAssetAmount: baseUnitAmount(3),
fillableMakerAssetAmount: baseUnitAmount(2), fillableMakerAssetAmount: baseUnitAmount(3),
}, },
...PARTIAL_ORDER, ...PARTIAL_ORDER,
}, },
@@ -86,7 +86,7 @@ const PARTIAL_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET: Array<Partial<Sig
makerAssetAmount: baseUnitAmount(6), makerAssetAmount: baseUnitAmount(6),
takerFee: baseUnitAmount(4), takerFee: baseUnitAmount(4),
fillableTakerAssetAmount: baseUnitAmount(3), fillableTakerAssetAmount: baseUnitAmount(3),
fillableMakerAssetAmount: baseUnitAmount(2), fillableMakerAssetAmount: baseUnitAmount(3),
fillableTakerFeeAmount: baseUnitAmount(2), fillableTakerFeeAmount: baseUnitAmount(2),
}, },
...PARTIAL_ORDER_FEE_IN_TAKER_ASSET, ...PARTIAL_ORDER_FEE_IN_TAKER_ASSET,