Asset-Swapper: Incorporate fees into fill path optimization (#2481)

* `@0x/asset-swapper`: Incorporate fees into fill optimization.

* `@0x/asset-swapper`: Address review comments.

* `@0x/asset-swapper`: Rebase and update tests for curve.

* `@0x/asset-swapper`: Bring back a form of native order pruning.
`@0x/asset-swapper`: Bring back dust thresholds.
`@0x/asset-swapper`: Avoid calling `getMedianSellRate()` if output token is ETH.

* Update devdoc for `fees` option

Co-authored-by: Lawrence Forman <me@merklejerk.com>
This commit is contained in:
Lawrence Forman
2020-02-24 22:47:33 -05:00
committed by GitHub
parent cbb23a42e2
commit 0f1c15a6ca
12 changed files with 487 additions and 182 deletions

View File

@@ -1,4 +1,17 @@
[ [
{
"version": "4.3.0",
"changes": [
{
"note": "Add `fees` to `GetMarketOrdersOpts`",
"pr": 2481
},
{
"note": "Incorporate fees into fill optimization",
"pr": 2481
}
]
},
{ {
"version": "4.2.0", "version": "4.2.0",
"changes": [ "changes": [

View File

@@ -6,14 +6,14 @@ import { SignedOrderWithFillableAmounts } from '../types';
import { utils } from './utils'; import { utils } from './utils';
export const fillableAmountsUtils = { export const fillableAmountsUtils = {
getTakerAssetAmountSwappedAfterFees(order: SignedOrderWithFillableAmounts): BigNumber { getTakerAssetAmountSwappedAfterOrderFees(order: SignedOrderWithFillableAmounts): BigNumber {
if (utils.isOrderTakerFeePayableWithTakerAsset(order)) { if (utils.isOrderTakerFeePayableWithTakerAsset(order)) {
return order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount); return order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount);
} else { } else {
return order.fillableTakerAssetAmount; return order.fillableTakerAssetAmount;
} }
}, },
getMakerAssetAmountSwappedAfterFees(order: SignedOrderWithFillableAmounts): BigNumber { getMakerAssetAmountSwappedAfterOrderFees(order: SignedOrderWithFillableAmounts): BigNumber {
if (utils.isOrderTakerFeePayableWithMakerAsset(order)) { if (utils.isOrderTakerFeePayableWithMakerAsset(order)) {
return order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount); return order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount);
} else { } else {

View File

@@ -30,13 +30,21 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
numSamples: 13, numSamples: 13,
noConflicts: true, noConflicts: true,
sampleDistributionBase: 1.05, sampleDistributionBase: 1.05,
fees: {},
}; };
/**
* Sources to poll for ETH fee price estimates.
*/
export const FEE_QUOTE_SOURCES = SELL_SOURCES;
export const constants = { export const constants = {
INFINITE_TIMESTAMP_SEC, INFINITE_TIMESTAMP_SEC,
SELL_SOURCES, SELL_SOURCES,
BUY_SOURCES, BUY_SOURCES,
DEFAULT_GET_MARKET_ORDERS_OPTS, DEFAULT_GET_MARKET_ORDERS_OPTS,
ERC20_PROXY_ID: '0xf47261b0', ERC20_PROXY_ID: '0xf47261b0',
FEE_QUOTE_SOURCES,
WALLET_SIGNATURE: '0x04', WALLET_SIGNATURE: '0x04',
ONE_ETHER: new BigNumber(1e18),
}; };

View File

@@ -3,7 +3,6 @@ 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 { sortingUtils } from '../../utils/sorting_utils';
import { constants as marketOperationUtilConstants } from './constants'; import { constants as marketOperationUtilConstants } from './constants';
import { import {
@@ -50,7 +49,7 @@ export class CreateOrderUtils {
); );
} }
} }
return sortingUtils.sortOrders(orders); return orders;
} }
// Convert buy fills into orders. // Convert buy fills into orders.
@@ -79,7 +78,7 @@ export class CreateOrderUtils {
); );
} }
} }
return sortingUtils.sortOrders(orders); return orders;
} }
private _getBridgeAddressFromSource(source: ERC20BridgeSource): string { private _getBridgeAddressFromSource(source: ERC20BridgeSource): string {

View File

@@ -10,7 +10,7 @@ const { ZERO_AMOUNT } = constants;
interface FillsOptimizerContext { interface FillsOptimizerContext {
currentPath: Fill[]; currentPath: Fill[];
currentPathInput: BigNumber; currentPathInput: BigNumber;
currentPathOutput: BigNumber; currentPathAdjustedOutput: BigNumber;
currentPathFlags: number; currentPathFlags: number;
} }
@@ -22,7 +22,7 @@ export class FillsOptimizer {
private readonly _shouldMinimize: boolean; private readonly _shouldMinimize: boolean;
private _currentRunCount: number = 0; private _currentRunCount: number = 0;
private _optimalPath?: Fill[] = undefined; private _optimalPath?: Fill[] = undefined;
private _optimalPathOutput: BigNumber = ZERO_AMOUNT; private _optimalPathAdjustedOutput: BigNumber = ZERO_AMOUNT;
constructor(runLimit: number, shouldMinimize?: boolean) { constructor(runLimit: number, shouldMinimize?: boolean) {
this._runLimit = runLimit; this._runLimit = runLimit;
@@ -32,24 +32,27 @@ export class FillsOptimizer {
public optimize(fills: Fill[], input: BigNumber, upperBoundPath?: Fill[]): Fill[] | undefined { public optimize(fills: Fill[], input: BigNumber, upperBoundPath?: Fill[]): Fill[] | undefined {
this._currentRunCount = 0; this._currentRunCount = 0;
this._optimalPath = upperBoundPath; this._optimalPath = upperBoundPath;
this._optimalPathOutput = upperBoundPath ? getPathOutput(upperBoundPath, input) : ZERO_AMOUNT; this._optimalPathAdjustedOutput = upperBoundPath ? getPathAdjustedOutput(upperBoundPath, input) : ZERO_AMOUNT;
const ctx = { const ctx = {
currentPath: [], currentPath: [],
currentPathInput: ZERO_AMOUNT, currentPathInput: ZERO_AMOUNT,
currentPathOutput: ZERO_AMOUNT, currentPathAdjustedOutput: ZERO_AMOUNT,
currentPathFlags: 0, currentPathFlags: 0,
}; };
// Visit all valid combinations of fills to find the optimal path. // Visit all valid combinations of fills to find the optimal path.
this._walk(fills, input, ctx); this._walk(fills, input, ctx);
return this._optimalPath; if (this._optimalPath) {
return sortFillsByAdjustedRate(this._optimalPath, this._shouldMinimize);
}
return undefined;
} }
private _walk(fills: Fill[], input: BigNumber, ctx: FillsOptimizerContext): void { private _walk(fills: Fill[], input: BigNumber, ctx: FillsOptimizerContext): void {
const { currentPath, currentPathInput, currentPathOutput, currentPathFlags } = ctx; const { currentPath, currentPathInput, currentPathAdjustedOutput, currentPathFlags } = ctx;
// Stop if the current path is already complete. // Stop if the current path is already complete.
if (currentPathInput.gte(input)) { if (currentPathInput.gte(input)) {
this._updateOptimalPath(currentPath, currentPathOutput); this._updateOptimalPath(currentPath, currentPathAdjustedOutput);
return; return;
} }
@@ -67,8 +70,8 @@ export class FillsOptimizer {
} }
const nextPath = [...currentPath, nextFill]; const nextPath = [...currentPath, nextFill];
const nextPathInput = BigNumber.min(input, currentPathInput.plus(nextFill.input)); const nextPathInput = BigNumber.min(input, currentPathInput.plus(nextFill.input));
const nextPathOutput = currentPathOutput.plus( const nextPathAdjustedOutput = currentPathAdjustedOutput.plus(
getPartialFillOutput(nextFill, nextPathInput.minus(currentPathInput)), getPartialFillOutput(nextFill, nextPathInput.minus(currentPathInput)).minus(nextFill.fillPenalty),
); );
// tslint:disable-next-line: no-bitwise // tslint:disable-next-line: no-bitwise
const nextPathFlags = currentPathFlags | nextFill.flags; const nextPathFlags = currentPathFlags | nextFill.flags;
@@ -80,7 +83,7 @@ export class FillsOptimizer {
{ {
currentPath: nextPath, currentPath: nextPath,
currentPathInput: nextPathInput, currentPathInput: nextPathInput,
currentPathOutput: nextPathOutput, currentPathAdjustedOutput: nextPathAdjustedOutput,
// tslint:disable-next-line: no-bitwise // tslint:disable-next-line: no-bitwise
currentPathFlags: nextPathFlags, currentPathFlags: nextPathFlags,
}, },
@@ -88,10 +91,10 @@ export class FillsOptimizer {
} }
} }
private _updateOptimalPath(path: Fill[], output: BigNumber): void { private _updateOptimalPath(path: Fill[], adjustedOutput: BigNumber): void {
if (!this._optimalPath || this._compareOutputs(output, this._optimalPathOutput) === 1) { if (!this._optimalPath || this._compareOutputs(adjustedOutput, this._optimalPathAdjustedOutput) === 1) {
this._optimalPath = path; this._optimalPath = path;
this._optimalPathOutput = output; this._optimalPathAdjustedOutput = adjustedOutput;
} }
} }
@@ -101,13 +104,15 @@ export class FillsOptimizer {
} }
/** /**
* Compute the total output for a fill path, optionally clipping the input * Compute the total output minus penalty for a fill path, optionally clipping the input
* to `maxInput`. * to `maxInput`.
*/ */
export function getPathOutput(path: Fill[], maxInput?: BigNumber): BigNumber { export function getPathAdjustedOutput(path: Fill[], maxInput?: BigNumber): BigNumber {
let currentInput = ZERO_AMOUNT; let currentInput = ZERO_AMOUNT;
let currentOutput = ZERO_AMOUNT; let currentOutput = ZERO_AMOUNT;
let currentPenalty = ZERO_AMOUNT;
for (const fill of path) { for (const fill of path) {
currentPenalty = currentPenalty.plus(fill.fillPenalty);
if (maxInput && currentInput.plus(fill.input).gte(maxInput)) { if (maxInput && currentInput.plus(fill.input).gte(maxInput)) {
const partialInput = maxInput.minus(currentInput); const partialInput = maxInput.minus(currentInput);
currentOutput = currentOutput.plus(getPartialFillOutput(fill, partialInput)); currentOutput = currentOutput.plus(getPartialFillOutput(fill, partialInput));
@@ -118,7 +123,7 @@ export function getPathOutput(path: Fill[], maxInput?: BigNumber): BigNumber {
currentOutput = currentOutput.plus(fill.output); currentOutput = currentOutput.plus(fill.output);
} }
} }
return currentOutput; return currentOutput.minus(currentPenalty);
} }
/** /**
@@ -131,7 +136,46 @@ export function comparePathOutputs(a: BigNumber, b: BigNumber, shouldMinimize: b
// Get the partial output earned by a fill at input `partialInput`. // Get the partial output earned by a fill at input `partialInput`.
function getPartialFillOutput(fill: Fill, partialInput: BigNumber): BigNumber { function getPartialFillOutput(fill: Fill, partialInput: BigNumber): BigNumber {
return BigNumber.min(fill.output, fill.output.div(fill.input).times(partialInput)).integerValue( return BigNumber.min(fill.output, fill.output.div(fill.input).times(partialInput));
BigNumber.ROUND_DOWN, }
);
/**
* Sort a path by adjusted input -> output rate while keeping sub-fills contiguous.
*/
export function sortFillsByAdjustedRate(path: Fill[], shouldMinimize: boolean = false): Fill[] {
return path.slice(0).sort((a, b) => {
const rootA = getFillRoot(a);
const rootB = getFillRoot(b);
const adjustedRateA = rootA.output.minus(rootA.fillPenalty).div(rootA.input);
const adjustedRateB = rootB.output.minus(rootB.fillPenalty).div(rootB.input);
if ((!a.parent && !b.parent) || a.fillData.source !== b.fillData.source) {
return shouldMinimize ? adjustedRateA.comparedTo(adjustedRateB) : adjustedRateB.comparedTo(adjustedRateA);
}
if (isFillAncestorOf(a, b)) {
return -1;
}
if (isFillAncestorOf(b, a)) {
return 1;
}
return 0;
});
}
function getFillRoot(fill: Fill): Fill {
let root = fill;
while (root.parent) {
root = root.parent;
}
return root;
}
function isFillAncestorOf(ancestor: Fill, fill: Fill): boolean {
let currFill = fill.parent;
while (currFill) {
if (currFill === ancestor) {
return true;
}
currFill = currFill.parent;
}
return false;
} }

View File

@@ -9,7 +9,7 @@ import { fillableAmountsUtils } from '../fillable_amounts_utils';
import { constants as marketOperationUtilConstants } from './constants'; import { constants as marketOperationUtilConstants } from './constants';
import { CreateOrderUtils } from './create_order'; import { CreateOrderUtils } from './create_order';
import { comparePathOutputs, FillsOptimizer, getPathOutput } from './fill_optimizer'; import { comparePathOutputs, FillsOptimizer, getPathAdjustedOutput, sortFillsByAdjustedRate } from './fill_optimizer';
import { DexOrderSampler, getSampleAmounts } from './sampler'; import { DexOrderSampler, getSampleAmounts } from './sampler';
import { import {
AggregationError, AggregationError,
@@ -29,10 +29,18 @@ import {
export { DexOrderSampler } from './sampler'; export { DexOrderSampler } from './sampler';
const { ZERO_AMOUNT } = constants; const { ZERO_AMOUNT } = constants;
const { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, ERC20_PROXY_ID, SELL_SOURCES } = marketOperationUtilConstants; const {
BUY_SOURCES,
DEFAULT_GET_MARKET_ORDERS_OPTS,
ERC20_PROXY_ID,
FEE_QUOTE_SOURCES,
ONE_ETHER,
SELL_SOURCES,
} = marketOperationUtilConstants;
export class MarketOperationUtils { export class MarketOperationUtils {
private readonly _createOrderUtils: CreateOrderUtils; private readonly _createOrderUtils: CreateOrderUtils;
private readonly _wethAddress: string;
constructor( constructor(
private readonly _sampler: DexOrderSampler, private readonly _sampler: DexOrderSampler,
@@ -40,6 +48,7 @@ export class MarketOperationUtils {
private readonly _orderDomain: OrderDomain, private readonly _orderDomain: OrderDomain,
) { ) {
this._createOrderUtils = new CreateOrderUtils(contractAddresses); this._createOrderUtils = new CreateOrderUtils(contractAddresses);
this._wethAddress = contractAddresses.etherToken;
} }
/** /**
@@ -63,8 +72,16 @@ export class MarketOperationUtils {
...opts, ...opts,
}; };
const [makerToken, takerToken] = getOrderTokens(nativeOrders[0]); const [makerToken, takerToken] = getOrderTokens(nativeOrders[0]);
const [fillableAmounts, dexQuotes] = await this._sampler.executeAsync( const [fillableAmounts, ethToMakerAssetRate, dexQuotes] = await this._sampler.executeAsync(
DexOrderSampler.ops.getOrderFillableTakerAmounts(nativeOrders), DexOrderSampler.ops.getOrderFillableTakerAmounts(nativeOrders),
makerToken.toLowerCase() === this._wethAddress.toLowerCase()
? DexOrderSampler.ops.constant(new BigNumber(1))
: DexOrderSampler.ops.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
makerToken,
this._wethAddress,
ONE_ETHER,
),
DexOrderSampler.ops.getSellQuotes( DexOrderSampler.ops.getSellQuotes(
difference(SELL_SOURCES, _opts.excludedSources), difference(SELL_SOURCES, _opts.excludedSources),
makerToken, makerToken,
@@ -78,19 +95,20 @@ export class MarketOperationUtils {
MarketOperation.Sell, MarketOperation.Sell,
); );
const prunedNativePath = pruneDustFillsFromNativePath( const nativeFills = pruneNativeFills(
createSellPathFromNativeOrders(nativeOrdersWithFillableAmounts), sortFillsByAdjustedRate(
createSellPathFromNativeOrders(nativeOrdersWithFillableAmounts, ethToMakerAssetRate, _opts),
),
takerAmount, takerAmount,
_opts.dustFractionThreshold, _opts.dustFractionThreshold,
); );
const clippedNativePath = clipPathToInput(sortFillsByPrice(prunedNativePath), takerAmount); const dexPaths = createSellPathsFromDexQuotes(dexQuotes, ethToMakerAssetRate, _opts);
const dexPaths = createPathsFromDexQuotes(dexQuotes, _opts.noConflicts);
const allPaths = [...dexPaths]; const allPaths = [...dexPaths];
const allFills = flattenDexPaths(dexPaths); const allFills = flattenDexPaths(dexPaths);
// If native orders are allowed, splice them in. // If native orders are allowed, splice them in.
if (!_opts.excludedSources.includes(ERC20BridgeSource.Native)) { if (!_opts.excludedSources.includes(ERC20BridgeSource.Native)) {
allPaths.splice(0, 0, clippedNativePath); allPaths.splice(0, 0, nativeFills);
allFills.splice(0, 0, ...clippedNativePath); allFills.splice(0, 0, ...nativeFills);
} }
const optimizer = new FillsOptimizer(_opts.runLimit); const optimizer = new FillsOptimizer(_opts.runLimit);
@@ -99,7 +117,7 @@ export class MarketOperationUtils {
// Sorting the orders by price effectively causes the optimizer to walk // Sorting the orders by price effectively causes the optimizer to walk
// the greediest solution first, which is the optimal solution in most // the greediest solution first, which is the optimal solution in most
// cases. // cases.
sortFillsByPrice(allFills), sortFillsByAdjustedRate(allFills),
takerAmount, takerAmount,
upperBoundPath, upperBoundPath,
); );
@@ -136,8 +154,16 @@ export class MarketOperationUtils {
...opts, ...opts,
}; };
const [makerToken, takerToken] = getOrderTokens(nativeOrders[0]); const [makerToken, takerToken] = getOrderTokens(nativeOrders[0]);
const [fillableAmounts, dexQuotes] = await this._sampler.executeAsync( const [fillableAmounts, ethToTakerAssetRate, dexQuotes] = await this._sampler.executeAsync(
DexOrderSampler.ops.getOrderFillableMakerAmounts(nativeOrders), DexOrderSampler.ops.getOrderFillableMakerAmounts(nativeOrders),
takerToken.toLowerCase() === this._wethAddress.toLowerCase()
? DexOrderSampler.ops.constant(new BigNumber(1))
: DexOrderSampler.ops.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
takerToken,
this._wethAddress,
ONE_ETHER,
),
DexOrderSampler.ops.getBuyQuotes( DexOrderSampler.ops.getBuyQuotes(
difference(BUY_SOURCES, _opts.excludedSources), difference(BUY_SOURCES, _opts.excludedSources),
makerToken, makerToken,
@@ -150,6 +176,7 @@ export class MarketOperationUtils {
makerAmount, makerAmount,
fillableAmounts, fillableAmounts,
dexQuotes, dexQuotes,
ethToTakerAssetRate,
_opts, _opts,
); );
if (!signedOrderWithFillableAmounts) { if (!signedOrderWithFillableAmounts) {
@@ -182,6 +209,14 @@ export class MarketOperationUtils {
const sources = difference(BUY_SOURCES, _opts.excludedSources); const sources = difference(BUY_SOURCES, _opts.excludedSources);
const ops = [ const ops = [
...batchNativeOrders.map(orders => DexOrderSampler.ops.getOrderFillableMakerAmounts(orders)), ...batchNativeOrders.map(orders => DexOrderSampler.ops.getOrderFillableMakerAmounts(orders)),
...batchNativeOrders.map(orders =>
DexOrderSampler.ops.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
this._wethAddress,
getOrderTokens(orders[0])[1],
ONE_ETHER,
),
),
...batchNativeOrders.map((orders, i) => ...batchNativeOrders.map((orders, i) =>
DexOrderSampler.ops.getBuyQuotes(sources, getOrderTokens(orders[0])[0], getOrderTokens(orders[0])[1], [ DexOrderSampler.ops.getBuyQuotes(sources, getOrderTokens(orders[0])[0], getOrderTokens(orders[0])[1], [
makerAmounts[i], makerAmounts[i],
@@ -189,8 +224,9 @@ export class MarketOperationUtils {
), ),
]; ];
const executeResults = await this._sampler.executeBatchAsync(ops); const executeResults = await this._sampler.executeBatchAsync(ops);
const batchFillableAmounts = executeResults.slice(0, batchNativeOrders.length) as BigNumber[][]; const batchFillableAmounts = executeResults.splice(0, batchNativeOrders.length) as BigNumber[][];
const batchDexQuotes = executeResults.slice(batchNativeOrders.length) as DexSample[][][]; const batchEthToTakerAssetRate = executeResults.splice(0, batchNativeOrders.length) as BigNumber[];
const batchDexQuotes = executeResults.splice(0, batchNativeOrders.length) as DexSample[][][];
return batchFillableAmounts.map((fillableAmounts, i) => return batchFillableAmounts.map((fillableAmounts, i) =>
this._createBuyOrdersPathFromSamplerResultIfExists( this._createBuyOrdersPathFromSamplerResultIfExists(
@@ -198,6 +234,7 @@ export class MarketOperationUtils {
makerAmounts[i], makerAmounts[i],
fillableAmounts, fillableAmounts,
batchDexQuotes[i], batchDexQuotes[i],
batchEthToTakerAssetRate[i],
_opts, _opts,
), ),
); );
@@ -208,6 +245,7 @@ export class MarketOperationUtils {
makerAmount: BigNumber, makerAmount: BigNumber,
nativeOrderFillableAmounts: BigNumber[], nativeOrderFillableAmounts: BigNumber[],
dexQuotes: DexSample[][], dexQuotes: DexSample[][],
ethToTakerAssetRate: BigNumber,
opts: GetMarketOrdersOpts, opts: GetMarketOrdersOpts,
): OptimizedMarketOrder[] | undefined { ): OptimizedMarketOrder[] | undefined {
const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts( const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts(
@@ -215,19 +253,21 @@ export class MarketOperationUtils {
nativeOrderFillableAmounts, nativeOrderFillableAmounts,
MarketOperation.Buy, MarketOperation.Buy,
); );
const prunedNativePath = pruneDustFillsFromNativePath( const nativeFills = pruneNativeFills(
createBuyPathFromNativeOrders(nativeOrdersWithFillableAmounts), sortFillsByAdjustedRate(
createBuyPathFromNativeOrders(nativeOrdersWithFillableAmounts, ethToTakerAssetRate, opts),
true,
),
makerAmount, makerAmount,
opts.dustFractionThreshold, opts.dustFractionThreshold,
); );
const clippedNativePath = clipPathToInput(sortFillsByPrice(prunedNativePath).reverse(), makerAmount); const dexPaths = createBuyPathsFromDexQuotes(dexQuotes, ethToTakerAssetRate, opts);
const dexPaths = createPathsFromDexQuotes(dexQuotes, opts.noConflicts);
const allPaths = [...dexPaths]; const allPaths = [...dexPaths];
const allFills = flattenDexPaths(dexPaths); const allFills = flattenDexPaths(dexPaths);
// If native orders are allowed, splice them in. // If native orders are allowed, splice them in.
if (!opts.excludedSources.includes(ERC20BridgeSource.Native)) { if (!opts.excludedSources.includes(ERC20BridgeSource.Native)) {
allPaths.splice(0, 0, clippedNativePath); allPaths.splice(0, 0, nativeFills);
allFills.splice(0, 0, ...clippedNativePath); allFills.splice(0, 0, ...nativeFills);
} }
const optimizer = new FillsOptimizer(opts.runLimit, true); const optimizer = new FillsOptimizer(opts.runLimit, true);
const upperBoundPath = pickBestUpperBoundPath(allPaths, makerAmount, true); const upperBoundPath = pickBestUpperBoundPath(allPaths, makerAmount, true);
@@ -235,7 +275,7 @@ export class MarketOperationUtils {
// Sorting the orders by price effectively causes the optimizer to walk // Sorting the orders by price effectively causes the optimizer to walk
// the greediest solution first, which is the optimal solution in most // the greediest solution first, which is the optimal solution in most
// cases. // cases.
sortFillsByPrice(allFills), sortFillsByAdjustedRate(allFills, true),
makerAmount, makerAmount,
upperBoundPath, upperBoundPath,
); );
@@ -287,19 +327,25 @@ function difference<T>(a: T[], b: T[]): T[] {
return a.filter(x => b.indexOf(x) === -1); return a.filter(x => b.indexOf(x) === -1);
} }
function createSellPathFromNativeOrders(orders: SignedOrderWithFillableAmounts[]): Fill[] { function createSellPathFromNativeOrders(
orders: SignedOrderWithFillableAmounts[],
ethToOutputAssetRate: BigNumber,
opts: GetMarketOrdersOpts,
): Fill[] {
const path: Fill[] = []; const path: Fill[] = [];
// tslint:disable-next-line: prefer-for-of // tslint:disable-next-line: prefer-for-of
for (let i = 0; i < orders.length; i++) { for (let i = 0; i < orders.length; i++) {
const order = orders[i]; const order = orders[i];
const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(order); const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(order);
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees(order); const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order);
// Native orders can be filled in any order, so they're all root nodes. // Native orders can be filled in any order, so they're all root nodes.
path.push({ path.push({
flags: FillFlags.SourceNative, flags: FillFlags.SourceNative,
exclusionMask: 0, exclusionMask: 0,
input: takerAmount, input: takerAmount,
output: makerAmount, output: makerAmount,
// Every fill from native orders incurs a penalty.
fillPenalty: ethToOutputAssetRate.times(opts.fees[ERC20BridgeSource.Native] || 0),
fillData: { fillData: {
source: ERC20BridgeSource.Native, source: ERC20BridgeSource.Native,
order, order,
@@ -309,19 +355,26 @@ function createSellPathFromNativeOrders(orders: SignedOrderWithFillableAmounts[]
return path; return path;
} }
function createBuyPathFromNativeOrders(orders: SignedOrderWithFillableAmounts[]): Fill[] { function createBuyPathFromNativeOrders(
orders: SignedOrderWithFillableAmounts[],
ethToOutputAssetRate: BigNumber,
opts: GetMarketOrdersOpts,
): Fill[] {
const path: Fill[] = []; const path: Fill[] = [];
// tslint:disable-next-line: prefer-for-of // tslint:disable-next-line: prefer-for-of
for (let i = 0; i < orders.length; i++) { for (let i = 0; i < orders.length; i++) {
const order = orders[i]; const order = orders[i];
const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(order); const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(order);
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees(order); const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order);
// Native orders can be filled in any order, so they're all root nodes. // Native orders can be filled in any order, so they're all root nodes.
path.push({ path.push({
flags: FillFlags.SourceNative, flags: FillFlags.SourceNative,
exclusionMask: 0, exclusionMask: 0,
input: makerAmount, input: makerAmount,
output: takerAmount, output: takerAmount,
// Every fill from native orders incurs a penalty.
// Negated because we try to minimize the output in buys.
fillPenalty: ethToOutputAssetRate.times(opts.fees[ERC20BridgeSource.Native] || 0).negated(),
fillData: { fillData: {
source: ERC20BridgeSource.Native, source: ERC20BridgeSource.Native,
order, order,
@@ -331,31 +384,75 @@ function createBuyPathFromNativeOrders(orders: SignedOrderWithFillableAmounts[])
return path; return path;
} }
function createPathsFromDexQuotes(dexQuotes: DexSample[][], noConflicts: boolean): Fill[][] { function pruneNativeFills(fills: Fill[], fillAmount: BigNumber, dustFractionThreshold: number): Fill[] {
const minInput = fillAmount.times(dustFractionThreshold);
const totalInput = ZERO_AMOUNT;
const pruned = [];
for (const fill of fills) {
if (totalInput.gte(fillAmount)) {
break;
}
if (fill.input.lt(minInput)) {
continue;
}
pruned.push(fill);
}
return pruned;
}
function createSellPathsFromDexQuotes(
dexQuotes: DexSample[][],
ethToOutputAssetRate: BigNumber,
opts: GetMarketOrdersOpts,
): Fill[][] {
return createPathsFromDexQuotes(dexQuotes, ethToOutputAssetRate, opts);
}
function createBuyPathsFromDexQuotes(
dexQuotes: DexSample[][],
ethToOutputAssetRate: BigNumber,
opts: GetMarketOrdersOpts,
): Fill[][] {
return createPathsFromDexQuotes(
dexQuotes,
// Negated because we try to minimize the output in buys.
ethToOutputAssetRate.negated(),
opts,
);
}
function createPathsFromDexQuotes(
dexQuotes: DexSample[][],
ethToOutputAssetRate: BigNumber,
opts: GetMarketOrdersOpts,
): Fill[][] {
const paths: Fill[][] = []; const paths: Fill[][] = [];
for (const quote of dexQuotes) { for (const quote of dexQuotes) {
// Native orders can be filled in any order, so they're all root nodes.
const path: Fill[] = []; const path: Fill[] = [];
let prevSample: DexSample | undefined; let prevSample: DexSample | undefined;
// tslint:disable-next-line: prefer-for-of // tslint:disable-next-line: prefer-for-of
for (let i = 0; i < quote.length; i++) { for (let i = 0; i < quote.length; i++) {
const sample = quote[i]; const sample = quote[i];
if (sample.output.eq(0) || (prevSample && prevSample.output.gte(sample.output))) { // Stop of the sample has zero output, which can occur if the source
// Stop if the output is zero or does not increase. // cannot fill the full amount.
if (sample.output.isZero()) {
break; break;
} }
path.push({ path.push({
parent: path.length !== 0 ? path[path.length - 1] : undefined,
flags: sourceToFillFlags(sample.source),
exclusionMask: noConflicts ? sourceToExclusionMask(sample.source) : 0,
input: sample.input.minus(prevSample ? prevSample.input : 0), input: sample.input.minus(prevSample ? prevSample.input : 0),
output: sample.output.minus(prevSample ? prevSample.output : 0), output: sample.output.minus(prevSample ? prevSample.output : 0),
fillPenalty: ZERO_AMOUNT,
parent: path.length !== 0 ? path[path.length - 1] : undefined,
flags: sourceToFillFlags(sample.source),
exclusionMask: opts.noConflicts ? sourceToExclusionMask(sample.source) : 0,
fillData: { source: sample.source }, fillData: { source: sample.source },
}); });
prevSample = quote[i]; prevSample = quote[i];
} }
if (path.length > 0) {
// Don't push empty paths. // Don't push empty paths.
if (path.length > 0) {
// Only the first fill in a DEX path incurs a penalty.
path[0].fillPenalty = ethToOutputAssetRate.times(opts.fees[path[0].fillData.source] || 0);
paths.push(path); paths.push(path);
} }
} }
@@ -389,11 +486,6 @@ function sourceToExclusionMask(source: ERC20BridgeSource): number {
return 0; return 0;
} }
function pruneDustFillsFromNativePath(path: Fill[], fillAmount: BigNumber, dustFractionThreshold: number): Fill[] {
const dustAmount = fillAmount.times(dustFractionThreshold);
return path.filter(f => f.input.gt(dustAmount));
}
// Convert a list of DEX paths to a flattened list of `Fills`. // Convert a list of DEX paths to a flattened list of `Fills`.
function flattenDexPaths(dexFills: Fill[][]): Fill[] { function flattenDexPaths(dexFills: Fill[][]): Fill[] {
const fills: Fill[] = []; const fills: Fill[] = [];
@@ -411,7 +503,7 @@ function pickBestUpperBoundPath(paths: Fill[][], maxInput: BigNumber, shouldMini
let optimalPathOutput: BigNumber = ZERO_AMOUNT; let optimalPathOutput: BigNumber = ZERO_AMOUNT;
for (const path of paths) { for (const path of paths) {
if (getPathInput(path).gte(maxInput)) { if (getPathInput(path).gte(maxInput)) {
const output = getPathOutput(path, maxInput); const output = getPathAdjustedOutput(path, maxInput);
if (!optimalPath || comparePathOutputs(output, optimalPathOutput, !!shouldMinimize) === 1) { if (!optimalPath || comparePathOutputs(output, optimalPathOutput, !!shouldMinimize) === 1) {
optimalPath = path; optimalPath = path;
optimalPathOutput = output; optimalPathOutput = output;
@@ -454,19 +546,6 @@ function collapsePath(path: Fill[], isBuy: boolean): CollapsedFill[] {
return collapsed; return collapsed;
} }
// Sort fills by descending price.
function sortFillsByPrice(fills: Fill[]): Fill[] {
return fills.sort((a, b) => {
const d = b.output.div(b.input).minus(a.output.div(a.input));
if (d.gt(0)) {
return 1;
} else if (d.lt(0)) {
return -1;
}
return 0;
});
}
function getOrderTokens(order: SignedOrder): [string, string] { function getOrderTokens(order: SignedOrder): [string, string] {
const assets = [order.makerAssetData, order.takerAssetData].map(a => assetDataUtils.decodeAssetDataOrThrow(a)) as [ const assets = [order.makerAssetData, order.takerAssetData].map(a => assetDataUtils.decodeAssetDataOrThrow(a)) as [
ERC20AssetData, ERC20AssetData,
@@ -478,15 +557,4 @@ function getOrderTokens(order: SignedOrder): [string, string] {
return assets.map(a => a.tokenAddress) as [string, string]; return assets.map(a => a.tokenAddress) as [string, string];
} }
function clipPathToInput(path: Fill[], assetAmount: BigNumber): Fill[] { // tslint:disable: max-file-line-count
const clipped = [];
let totalInput = ZERO_AMOUNT;
for (const fill of path) {
if (totalInput.gte(assetAmount)) {
break;
}
clipped.push(fill);
totalInput = totalInput.plus(fill.input);
}
return clipped;
}

View File

@@ -145,6 +145,45 @@ const samplerOperations = {
}, },
}; };
}, },
getMedianSellRate(
sources: ERC20BridgeSource[],
makerToken: string,
takerToken: string,
takerFillAmount: BigNumber,
): BatchedOperation<BigNumber> {
const getSellQuotes = samplerOperations.getSellQuotes(sources, makerToken, takerToken, [takerFillAmount]);
return {
encodeCall: contract => {
const subCalls = [getSellQuotes.encodeCall(contract)];
return contract.batchCall(subCalls).getABIEncodedTransactionData();
},
handleCallResultsAsync: async (contract, callResults) => {
const rawSubCallResults = contract.getABIDecodedReturnData<string[]>('batchCall', callResults);
const samples = await getSellQuotes.handleCallResultsAsync(contract, rawSubCallResults[0]);
if (samples.length === 0) {
return new BigNumber(0);
}
const flatSortedSamples = samples
.reduce((acc, v) => acc.concat(...v))
.sort((a, b) => a.output.comparedTo(b.output));
if (flatSortedSamples.length === 0) {
return new BigNumber(0);
}
const medianSample = flatSortedSamples[Math.floor(flatSortedSamples.length / 2)];
return medianSample.output.div(medianSample.input);
},
};
},
constant<T>(result: T): BatchedOperation<T> {
return {
encodeCall: contract => {
return '0x';
},
handleCallResultsAsync: async (contract, callResults) => {
return result;
},
};
},
getSellQuotes( getSellQuotes(
sources: ERC20BridgeSource[], sources: ERC20BridgeSource[],
makerToken: string, makerToken: string,
@@ -374,7 +413,17 @@ export class DexOrderSampler {
*/ */
public async executeBatchAsync<T extends Array<BatchedOperation<any>>>(ops: T): Promise<any[]> { public async executeBatchAsync<T extends Array<BatchedOperation<any>>>(ops: T): Promise<any[]> {
const callDatas = ops.map(o => o.encodeCall(this._samplerContract)); const callDatas = ops.map(o => o.encodeCall(this._samplerContract));
const callResults = await this._samplerContract.batchCall(callDatas).callAsync(); // Execute all non-empty calldatas.
return Promise.all(callResults.map(async (r, i) => ops[i].handleCallResultsAsync(this._samplerContract, r))); const rawCallResults = await this._samplerContract.batchCall(callDatas.filter(cd => cd !== '0x')).callAsync();
// Return the parsed results.
let rawCallResultsIdx = 0;
return Promise.all(
callDatas.map(async (callData, i) => {
if (callData !== '0x') {
return ops[i].handleCallResultsAsync(this._samplerContract, rawCallResults[rawCallResultsIdx++]);
}
return ops[i].handleCallResultsAsync(this._samplerContract, '0x');
}),
);
} }
} }

View File

@@ -75,6 +75,8 @@ export interface Fill {
input: BigNumber; input: BigNumber;
// Output fill amount (maker asset amount in a sell, taker asset amount in a buy). // Output fill amount (maker asset amount in a sell, taker asset amount in a buy).
output: BigNumber; output: BigNumber;
// Output penalty for this fill.
fillPenalty: BigNumber;
// Fill that must precede this one. This enforces certain fills to be contiguous. // Fill that must precede this one. This enforces certain fills to be contiguous.
parent?: Fill; parent?: Fill;
// Data associated with this this Fill object. Used to reconstruct orders // Data associated with this this Fill object. Used to reconstruct orders
@@ -167,4 +169,8 @@ export interface GetMarketOrdersOpts {
* Default: 1.25. * Default: 1.25.
*/ */
sampleDistributionBase: number; sampleDistributionBase: number;
/**
* Fees for each liquidity source, expressed in gas.
*/
fees: { [source: string]: BigNumber };
} }

View File

@@ -131,19 +131,26 @@ export class SwapQuoteCalculator {
const slippageBufferAmount = assetFillAmount.multipliedBy(slippagePercentage).integerValue(); const slippageBufferAmount = assetFillAmount.multipliedBy(slippagePercentage).integerValue();
let resultOrders: OptimizedMarketOrder[] = []; let resultOrders: OptimizedMarketOrder[] = [];
{
// Scale fees by gas price.
const _opts = {
...opts,
fees: _.mapValues(opts.fees, (v, k) => v.times(gasPrice)),
};
if (operation === MarketOperation.Buy) { if (operation === MarketOperation.Buy) {
resultOrders = await this._marketOperationUtils.getMarketBuyOrdersAsync( resultOrders = await this._marketOperationUtils.getMarketBuyOrdersAsync(
prunedOrders, prunedOrders,
assetFillAmount.plus(slippageBufferAmount), assetFillAmount.plus(slippageBufferAmount),
opts, _opts,
); );
} else { } else {
resultOrders = await this._marketOperationUtils.getMarketSellOrdersAsync( resultOrders = await this._marketOperationUtils.getMarketSellOrdersAsync(
prunedOrders, prunedOrders,
assetFillAmount.plus(slippageBufferAmount), assetFillAmount.plus(slippageBufferAmount),
opts, _opts,
); );
} }
}
// assetData information for the result // assetData information for the result
const { makerAssetData, takerAssetData } = prunedOrders[0]; const { makerAssetData, takerAssetData } = prunedOrders[0];
@@ -241,10 +248,10 @@ export class SwapQuoteCalculator {
break; break;
} }
if (order.fill.source === ERC20BridgeSource.Native) { if (order.fill.source === ERC20BridgeSource.Native) {
const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees( const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(
order, order,
); );
const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees( const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(
order, order,
); );
const takerAssetAmountWithFees = BigNumber.min( const takerAssetAmountWithFees = BigNumber.min(
@@ -335,10 +342,10 @@ export class SwapQuoteCalculator {
break; break;
} }
if (order.fill.source === ERC20BridgeSource.Native) { if (order.fill.source === ERC20BridgeSource.Native) {
const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees( const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(
order, order,
); );
const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees( const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(
order, order,
); );
makerAssetAmount = BigNumber.min(remainingMakerAssetFillAmount, adjustedFillableMakerAssetAmount); makerAssetAmount = BigNumber.min(remainingMakerAssetFillAmount, adjustedFillableMakerAssetAmount);

View File

@@ -36,9 +36,9 @@ const MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER = testOrderFactory.generateTestSig
}); });
describe('fillableAmountsUtils', () => { describe('fillableAmountsUtils', () => {
describe('getTakerAssetAmountSwappedAfterFees', () => { describe('getTakerAssetAmountSwappedAfterOrderFees', () => {
it('should return fillableTakerAssetAmount if takerFee is not denominated in taker', () => { it('should return fillableTakerAssetAmount if takerFee is not denominated in taker', () => {
const availableAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees( const availableAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(
MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER, MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER,
); );
expect(availableAssetAmount).to.bignumber.eq( expect(availableAssetAmount).to.bignumber.eq(
@@ -47,15 +47,15 @@ describe('fillableAmountsUtils', () => {
}); });
it('should return fillableTakerAssetAmount + fillableTakerFeeAmount if takerFee is not denominated in maker', () => { it('should return fillableTakerAssetAmount + fillableTakerFeeAmount if takerFee is not denominated in maker', () => {
const availableAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees( const availableAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(
TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER, TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER,
); );
expect(availableAssetAmount).to.bignumber.eq(baseUnitAmount(12)); expect(availableAssetAmount).to.bignumber.eq(baseUnitAmount(12));
}); });
}); });
describe('getMakerAssetAmountSwappedAfterFees', () => { describe('getMakerAssetAmountSwappedAfterOrderFees', () => {
it('should return fillableMakerAssetAmount if takerFee is not denominated in maker', () => { it('should return fillableMakerAssetAmount if takerFee is not denominated in maker', () => {
const availableAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees( const availableAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(
TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER, TAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER,
); );
expect(availableAssetAmount).to.bignumber.eq( expect(availableAssetAmount).to.bignumber.eq(
@@ -64,7 +64,7 @@ describe('fillableAmountsUtils', () => {
}); });
it('should return fillableMakerAssetAmount - fillableTakerFeeif takerFee is denominated in maker', () => { it('should return fillableMakerAssetAmount - fillableTakerFeeif takerFee is denominated in maker', () => {
const availableAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees( const availableAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(
MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER, MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER,
); );
expect(availableAssetAmount).to.bignumber.eq(baseUnitAmount(8)); expect(availableAssetAmount).to.bignumber.eq(baseUnitAmount(8));

View File

@@ -167,6 +167,19 @@ describe('MarketOperationUtils tests', () => {
}; };
} }
type GetMedianRateOperation = (
sources: ERC20BridgeSource[],
makerToken: string,
takerToken: string,
fillAmounts: BigNumber[],
) => BigNumber;
function createGetMedianSellRate(rate: Numberish): GetMedianRateOperation {
return (sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => {
return new BigNumber(rate);
};
}
function createDecreasingRates(count: number): BigNumber[] { function createDecreasingRates(count: number): BigNumber[] {
const rates: BigNumber[] = []; const rates: BigNumber[] = [];
const initialRate = getRandomFloat(1e-3, 1e2); const initialRate = getRandomFloat(1e-3, 1e2);
@@ -188,9 +201,9 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Eth2Dai]: createDecreasingRates(NUM_SAMPLES), [ERC20BridgeSource.Eth2Dai]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.Kyber]: createDecreasingRates(NUM_SAMPLES), [ERC20BridgeSource.Kyber]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.Uniswap]: createDecreasingRates(NUM_SAMPLES), [ERC20BridgeSource.Uniswap]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.CurveUsdcDai]: createDecreasingRates(NUM_SAMPLES), [ERC20BridgeSource.CurveUsdcDai]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.CurveUsdcDaiUsdt]: createDecreasingRates(NUM_SAMPLES), [ERC20BridgeSource.CurveUsdcDaiUsdt]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: createDecreasingRates(NUM_SAMPLES), [ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: _.times(NUM_SAMPLES, () => 0),
}; };
function findSourceWithMaxOutput(rates: RatesBySource): ERC20BridgeSource { function findSourceWithMaxOutput(rates: RatesBySource): ERC20BridgeSource {
@@ -224,6 +237,7 @@ describe('MarketOperationUtils tests', () => {
getCurveSellQuotes: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.CurveUsdcDai]), getCurveSellQuotes: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.CurveUsdcDai]),
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES),
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES),
getMedianSellRate: createGetMedianSellRate(1),
}; };
function replaceSamplerOps(ops: Partial<typeof DEFAULT_OPS> = {}): void { function replaceSamplerOps(ops: Partial<typeof DEFAULT_OPS> = {}): void {
@@ -255,7 +269,17 @@ describe('MarketOperationUtils tests', () => {
FILL_AMOUNT, FILL_AMOUNT,
_.times(NUM_SAMPLES, i => DEFAULT_RATES[ERC20BridgeSource.Native][i]), _.times(NUM_SAMPLES, i => DEFAULT_RATES[ERC20BridgeSource.Native][i]),
); );
const DEFAULT_OPTS = { numSamples: NUM_SAMPLES, runLimit: 0, sampleDistributionBase: 1 }; const DEFAULT_OPTS = {
numSamples: NUM_SAMPLES,
runLimit: 0,
sampleDistributionBase: 1,
bridgeSlippage: 0,
excludedSources: [
ERC20BridgeSource.CurveUsdcDai,
ERC20BridgeSource.CurveUsdcDaiUsdt,
ERC20BridgeSource.CurveUsdcDaiUsdtTusd,
],
};
beforeEach(() => { beforeEach(() => {
replaceSamplerOps(); replaceSamplerOps();
@@ -372,36 +396,12 @@ describe('MarketOperationUtils tests', () => {
} }
}); });
it('ignores native orders below `dustFractionThreshold`', async () => {
const dustFractionThreshold = 0.01;
const dustAmount = FILL_AMOUNT.times(dustFractionThreshold).integerValue(BigNumber.ROUND_DOWN);
const maxRate = BigNumber.max(...ORDERS.map(o => o.makerAssetAmount.div(o.takerAssetAmount)));
// Pass in an order with the globally best rate but with a dust input amount.
const dustOrder = createOrder({
makerAssetAmount: dustAmount.times(maxRate.plus(0.01)),
takerAssetAmount: dustAmount,
});
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
_.shuffle([dustOrder, ...ORDERS]),
FILL_AMOUNT,
// Ignore all DEX sources so only native orders are returned.
{ ...DEFAULT_OPTS, dustFractionThreshold, excludedSources: SELL_SOURCES },
);
expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) {
expect(order.takerAssetAmount).to.bignumber.gt(dustAmount);
}
});
it('can mix convex sources', async () => { it('can mix convex sources', async () => {
const rates: RatesBySource = {}; const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1]; rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1];
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.CurveUsdcDai] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdt] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdtTusd] = [0, 0, 0, 0];
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
@@ -410,8 +410,7 @@ describe('MarketOperationUtils tests', () => {
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false }, { ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false },
); );
expect(improvedOrders).to.be.length(4); const orderSources = improvedOrders.map(o => o.fill.source);
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData));
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Kyber, ERC20BridgeSource.Kyber,
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
@@ -427,9 +426,6 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0.4, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Kyber] = [0.4, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.CurveUsdcDai] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdt] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdtTusd] = [0, 0, 0, 0];
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
@@ -438,8 +434,7 @@ describe('MarketOperationUtils tests', () => {
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true }, { ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true },
); );
expect(improvedOrders).to.be.length(4); const orderSources = improvedOrders.map(o => o.fill.source);
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData));
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Uniswap, ERC20BridgeSource.Uniswap,
@@ -455,9 +450,6 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Uniswap] = [0.15, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Uniswap] = [0.15, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.15, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Eth2Dai] = [0.15, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.CurveUsdcDai] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdt] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdtTusd] = [0, 0, 0, 0];
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
@@ -466,8 +458,7 @@ describe('MarketOperationUtils tests', () => {
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true }, { ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true },
); );
expect(improvedOrders).to.be.length(4); const orderSources = improvedOrders.map(o => o.fill.source);
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData));
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Kyber, ERC20BridgeSource.Kyber,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
@@ -476,6 +467,72 @@ describe('MarketOperationUtils tests', () => {
]; ];
expect(orderSources).to.deep.eq(expectedSources); expect(orderSources).to.deep.eq(expectedSources);
}); });
const ETH_TO_MAKER_RATE = 1.5;
it('factors in fees for native orders', async () => {
// Native orders will have the best rates but have fees,
// dropping their effective rates.
const nativeFeeRate = 0.06;
const rates: RatesBySource = {
[ERC20BridgeSource.Native]: [1, 0.99, 0.98, 0.97], // Effectively [0.94, ~0.93, ~0.92, ~0.91]
[ERC20BridgeSource.Uniswap]: [0.96, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
};
const fees = {
[ERC20BridgeSource.Native]: FILL_AMOUNT.div(4)
.times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE),
};
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE),
});
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false, fees },
);
const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
];
expect(orderSources).to.deep.eq(expectedSources);
});
it('factors in fees for dexes', async () => {
// Kyber will have the best rates but will have fees,
// dropping its effective rates.
const kyberFeeRate = 0.2;
const rates: RatesBySource = {
[ERC20BridgeSource.Native]: [0.95, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Uniswap]: [0.1, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1],
// Effectively [0.8, ~0.5, ~0, ~0]
[ERC20BridgeSource.Kyber]: [1, 0.7, 0.2, 0.2],
};
const fees = {
[ERC20BridgeSource.Kyber]: FILL_AMOUNT.div(4)
.times(kyberFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE),
};
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE),
});
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false, fees },
);
const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber];
expect(orderSources).to.deep.eq(expectedSources);
});
}); });
describe('getMarketBuyOrdersAsync()', () => { describe('getMarketBuyOrdersAsync()', () => {
@@ -484,7 +541,16 @@ describe('MarketOperationUtils tests', () => {
FILL_AMOUNT, FILL_AMOUNT,
_.times(NUM_SAMPLES, () => DEFAULT_RATES[ERC20BridgeSource.Native][0]), _.times(NUM_SAMPLES, () => DEFAULT_RATES[ERC20BridgeSource.Native][0]),
); );
const DEFAULT_OPTS = { numSamples: NUM_SAMPLES, runLimit: 0, sampleDistributionBase: 1 }; const DEFAULT_OPTS = {
numSamples: NUM_SAMPLES,
runLimit: 0,
sampleDistributionBase: 1,
excludedSources: [
ERC20BridgeSource.CurveUsdcDai,
ERC20BridgeSource.CurveUsdcDaiUsdt,
ERC20BridgeSource.CurveUsdcDaiUsdtTusd,
],
};
beforeEach(() => { beforeEach(() => {
replaceSamplerOps(); replaceSamplerOps();
@@ -609,27 +675,6 @@ describe('MarketOperationUtils tests', () => {
} }
}); });
it('Ignores native orders below `dustFractionThreshold`', async () => {
const dustFractionThreshold = 0.01;
const dustAmount = FILL_AMOUNT.times(dustFractionThreshold).integerValue(BigNumber.ROUND_DOWN);
const maxRate = BigNumber.max(...ORDERS.map(o => o.makerAssetAmount.div(o.takerAssetAmount)));
// Pass in an order with the globally best rate but with a dust input amount.
const dustOrder = createOrder({
makerAssetAmount: dustAmount,
takerAssetAmount: dustAmount.div(maxRate.plus(0.01)).integerValue(BigNumber.ROUND_DOWN),
});
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
_.shuffle([dustOrder, ...ORDERS]),
FILL_AMOUNT,
// Ignore all DEX sources so only native orders are returned.
{ ...DEFAULT_OPTS, dustFractionThreshold, excludedSources: BUY_SOURCES },
);
expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) {
expect(order.makerAssetAmount).to.bignumber.gt(dustAmount);
}
});
it('can mix convex sources', async () => { it('can mix convex sources', async () => {
const rates: RatesBySource = {}; const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1]; rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1];
@@ -643,8 +688,7 @@ describe('MarketOperationUtils tests', () => {
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512 }, { ...DEFAULT_OPTS, numSamples: 4, runLimit: 512 },
); );
expect(improvedOrders).to.be.length(4); const orderSources = improvedOrders.map(o => o.fill.source);
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData));
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Uniswap, ERC20BridgeSource.Uniswap,
@@ -653,6 +697,75 @@ describe('MarketOperationUtils tests', () => {
]; ];
expect(orderSources).to.deep.eq(expectedSources); expect(orderSources).to.deep.eq(expectedSources);
}); });
const ETH_TO_TAKER_RATE = 1.5;
it('factors in fees for native orders', async () => {
// Native orders will have the best rates but have fees,
// dropping their effective rates.
const nativeFeeRate = 0.06;
const rates: RatesBySource = {
[ERC20BridgeSource.Native]: [1, 0.99, 0.98, 0.97], // Effectively [0.94, ~0.93, ~0.92, ~0.91]
[ERC20BridgeSource.Uniswap]: [0.96, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
};
const fees = {
[ERC20BridgeSource.Native]: FILL_AMOUNT.div(4)
.times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE),
};
replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE),
});
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, fees },
);
const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
];
expect(orderSources).to.deep.eq(expectedSources);
});
it('factors in fees for dexes', async () => {
// Uniswap will have the best rates but will have fees,
// dropping its effective rates.
const uniswapFeeRate = 0.2;
const rates: RatesBySource = {
[ERC20BridgeSource.Native]: [0.95, 0.1, 0.1, 0.1],
// Effectively [0.8, ~0.5, ~0, ~0]
[ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2],
[ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1],
};
const fees = {
[ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4)
.times(uniswapFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE),
};
replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE),
});
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, fees },
);
const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [
ERC20BridgeSource.Native,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Uniswap,
];
expect(orderSources).to.deep.eq(expectedSources);
});
}); });
}); });
}); });

View File

@@ -64,12 +64,11 @@ function createSamplerFromSignedOrdersWithFillableAmounts(
); );
} }
// 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', () => { // TODO(dorothy-zbornak): Skipping these tests for now because they're a
// nightmare to maintain. We should replace them with simpler unit tests.
describe.skip('swapQuoteCalculator', () => {
let protocolFeeUtils: ProtocolFeeUtils; let protocolFeeUtils: ProtocolFeeUtils;
let contractAddresses: ContractAddresses; let contractAddresses: ContractAddresses;
@@ -294,7 +293,6 @@ 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[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);