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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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",
"changes": [

View File

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

View File

@ -30,13 +30,21 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
numSamples: 13,
noConflicts: true,
sampleDistributionBase: 1.05,
fees: {},
};
/**
* Sources to poll for ETH fee price estimates.
*/
export const FEE_QUOTE_SOURCES = SELL_SOURCES;
export const constants = {
INFINITE_TIMESTAMP_SEC,
SELL_SOURCES,
BUY_SOURCES,
DEFAULT_GET_MARKET_ORDERS_OPTS,
ERC20_PROXY_ID: '0xf47261b0',
FEE_QUOTE_SOURCES,
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 { constants } from '../../constants';
import { sortingUtils } from '../../utils/sorting_utils';
import { constants as marketOperationUtilConstants } from './constants';
import {
@ -50,7 +49,7 @@ export class CreateOrderUtils {
);
}
}
return sortingUtils.sortOrders(orders);
return orders;
}
// Convert buy fills into orders.
@ -79,7 +78,7 @@ export class CreateOrderUtils {
);
}
}
return sortingUtils.sortOrders(orders);
return orders;
}
private _getBridgeAddressFromSource(source: ERC20BridgeSource): string {

View File

@ -10,7 +10,7 @@ const { ZERO_AMOUNT } = constants;
interface FillsOptimizerContext {
currentPath: Fill[];
currentPathInput: BigNumber;
currentPathOutput: BigNumber;
currentPathAdjustedOutput: BigNumber;
currentPathFlags: number;
}
@ -22,7 +22,7 @@ export class FillsOptimizer {
private readonly _shouldMinimize: boolean;
private _currentRunCount: number = 0;
private _optimalPath?: Fill[] = undefined;
private _optimalPathOutput: BigNumber = ZERO_AMOUNT;
private _optimalPathAdjustedOutput: BigNumber = ZERO_AMOUNT;
constructor(runLimit: number, shouldMinimize?: boolean) {
this._runLimit = runLimit;
@ -32,24 +32,27 @@ export class FillsOptimizer {
public optimize(fills: Fill[], input: BigNumber, upperBoundPath?: Fill[]): Fill[] | undefined {
this._currentRunCount = 0;
this._optimalPath = upperBoundPath;
this._optimalPathOutput = upperBoundPath ? getPathOutput(upperBoundPath, input) : ZERO_AMOUNT;
this._optimalPathAdjustedOutput = upperBoundPath ? getPathAdjustedOutput(upperBoundPath, input) : ZERO_AMOUNT;
const ctx = {
currentPath: [],
currentPathInput: ZERO_AMOUNT,
currentPathOutput: ZERO_AMOUNT,
currentPathAdjustedOutput: ZERO_AMOUNT,
currentPathFlags: 0,
};
// Visit all valid combinations of fills to find the optimal path.
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 {
const { currentPath, currentPathInput, currentPathOutput, currentPathFlags } = ctx;
const { currentPath, currentPathInput, currentPathAdjustedOutput, currentPathFlags } = ctx;
// Stop if the current path is already complete.
if (currentPathInput.gte(input)) {
this._updateOptimalPath(currentPath, currentPathOutput);
this._updateOptimalPath(currentPath, currentPathAdjustedOutput);
return;
}
@ -67,8 +70,8 @@ export class FillsOptimizer {
}
const nextPath = [...currentPath, nextFill];
const nextPathInput = BigNumber.min(input, currentPathInput.plus(nextFill.input));
const nextPathOutput = currentPathOutput.plus(
getPartialFillOutput(nextFill, nextPathInput.minus(currentPathInput)),
const nextPathAdjustedOutput = currentPathAdjustedOutput.plus(
getPartialFillOutput(nextFill, nextPathInput.minus(currentPathInput)).minus(nextFill.fillPenalty),
);
// tslint:disable-next-line: no-bitwise
const nextPathFlags = currentPathFlags | nextFill.flags;
@ -80,7 +83,7 @@ export class FillsOptimizer {
{
currentPath: nextPath,
currentPathInput: nextPathInput,
currentPathOutput: nextPathOutput,
currentPathAdjustedOutput: nextPathAdjustedOutput,
// tslint:disable-next-line: no-bitwise
currentPathFlags: nextPathFlags,
},
@ -88,10 +91,10 @@ export class FillsOptimizer {
}
}
private _updateOptimalPath(path: Fill[], output: BigNumber): void {
if (!this._optimalPath || this._compareOutputs(output, this._optimalPathOutput) === 1) {
private _updateOptimalPath(path: Fill[], adjustedOutput: BigNumber): void {
if (!this._optimalPath || this._compareOutputs(adjustedOutput, this._optimalPathAdjustedOutput) === 1) {
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`.
*/
export function getPathOutput(path: Fill[], maxInput?: BigNumber): BigNumber {
export function getPathAdjustedOutput(path: Fill[], maxInput?: BigNumber): BigNumber {
let currentInput = ZERO_AMOUNT;
let currentOutput = ZERO_AMOUNT;
let currentPenalty = ZERO_AMOUNT;
for (const fill of path) {
currentPenalty = currentPenalty.plus(fill.fillPenalty);
if (maxInput && currentInput.plus(fill.input).gte(maxInput)) {
const partialInput = maxInput.minus(currentInput);
currentOutput = currentOutput.plus(getPartialFillOutput(fill, partialInput));
@ -118,7 +123,7 @@ export function getPathOutput(path: Fill[], maxInput?: BigNumber): BigNumber {
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`.
function getPartialFillOutput(fill: Fill, partialInput: BigNumber): BigNumber {
return BigNumber.min(fill.output, fill.output.div(fill.input).times(partialInput)).integerValue(
BigNumber.ROUND_DOWN,
);
return BigNumber.min(fill.output, fill.output.div(fill.input).times(partialInput));
}
/**
* 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 { 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 {
AggregationError,
@ -29,10 +29,18 @@ import {
export { DexOrderSampler } from './sampler';
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 {
private readonly _createOrderUtils: CreateOrderUtils;
private readonly _wethAddress: string;
constructor(
private readonly _sampler: DexOrderSampler,
@ -40,6 +48,7 @@ export class MarketOperationUtils {
private readonly _orderDomain: OrderDomain,
) {
this._createOrderUtils = new CreateOrderUtils(contractAddresses);
this._wethAddress = contractAddresses.etherToken;
}
/**
@ -63,8 +72,16 @@ export class MarketOperationUtils {
...opts,
};
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),
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(
difference(SELL_SOURCES, _opts.excludedSources),
makerToken,
@ -78,19 +95,20 @@ export class MarketOperationUtils {
MarketOperation.Sell,
);
const prunedNativePath = pruneDustFillsFromNativePath(
createSellPathFromNativeOrders(nativeOrdersWithFillableAmounts),
const nativeFills = pruneNativeFills(
sortFillsByAdjustedRate(
createSellPathFromNativeOrders(nativeOrdersWithFillableAmounts, ethToMakerAssetRate, _opts),
),
takerAmount,
_opts.dustFractionThreshold,
);
const clippedNativePath = clipPathToInput(sortFillsByPrice(prunedNativePath), takerAmount);
const dexPaths = createPathsFromDexQuotes(dexQuotes, _opts.noConflicts);
const dexPaths = createSellPathsFromDexQuotes(dexQuotes, ethToMakerAssetRate, _opts);
const allPaths = [...dexPaths];
const allFills = flattenDexPaths(dexPaths);
// If native orders are allowed, splice them in.
if (!_opts.excludedSources.includes(ERC20BridgeSource.Native)) {
allPaths.splice(0, 0, clippedNativePath);
allFills.splice(0, 0, ...clippedNativePath);
allPaths.splice(0, 0, nativeFills);
allFills.splice(0, 0, ...nativeFills);
}
const optimizer = new FillsOptimizer(_opts.runLimit);
@ -99,7 +117,7 @@ export class MarketOperationUtils {
// Sorting the orders by price effectively causes the optimizer to walk
// the greediest solution first, which is the optimal solution in most
// cases.
sortFillsByPrice(allFills),
sortFillsByAdjustedRate(allFills),
takerAmount,
upperBoundPath,
);
@ -136,8 +154,16 @@ export class MarketOperationUtils {
...opts,
};
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),
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(
difference(BUY_SOURCES, _opts.excludedSources),
makerToken,
@ -150,6 +176,7 @@ export class MarketOperationUtils {
makerAmount,
fillableAmounts,
dexQuotes,
ethToTakerAssetRate,
_opts,
);
if (!signedOrderWithFillableAmounts) {
@ -182,6 +209,14 @@ export class MarketOperationUtils {
const sources = difference(BUY_SOURCES, _opts.excludedSources);
const ops = [
...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) =>
DexOrderSampler.ops.getBuyQuotes(sources, getOrderTokens(orders[0])[0], getOrderTokens(orders[0])[1], [
makerAmounts[i],
@ -189,8 +224,9 @@ export class MarketOperationUtils {
),
];
const executeResults = await this._sampler.executeBatchAsync(ops);
const batchFillableAmounts = executeResults.slice(0, batchNativeOrders.length) as BigNumber[][];
const batchDexQuotes = executeResults.slice(batchNativeOrders.length) as DexSample[][][];
const batchFillableAmounts = executeResults.splice(0, batchNativeOrders.length) as BigNumber[][];
const batchEthToTakerAssetRate = executeResults.splice(0, batchNativeOrders.length) as BigNumber[];
const batchDexQuotes = executeResults.splice(0, batchNativeOrders.length) as DexSample[][][];
return batchFillableAmounts.map((fillableAmounts, i) =>
this._createBuyOrdersPathFromSamplerResultIfExists(
@ -198,6 +234,7 @@ export class MarketOperationUtils {
makerAmounts[i],
fillableAmounts,
batchDexQuotes[i],
batchEthToTakerAssetRate[i],
_opts,
),
);
@ -208,6 +245,7 @@ export class MarketOperationUtils {
makerAmount: BigNumber,
nativeOrderFillableAmounts: BigNumber[],
dexQuotes: DexSample[][],
ethToTakerAssetRate: BigNumber,
opts: GetMarketOrdersOpts,
): OptimizedMarketOrder[] | undefined {
const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts(
@ -215,19 +253,21 @@ export class MarketOperationUtils {
nativeOrderFillableAmounts,
MarketOperation.Buy,
);
const prunedNativePath = pruneDustFillsFromNativePath(
createBuyPathFromNativeOrders(nativeOrdersWithFillableAmounts),
const nativeFills = pruneNativeFills(
sortFillsByAdjustedRate(
createBuyPathFromNativeOrders(nativeOrdersWithFillableAmounts, ethToTakerAssetRate, opts),
true,
),
makerAmount,
opts.dustFractionThreshold,
);
const clippedNativePath = clipPathToInput(sortFillsByPrice(prunedNativePath).reverse(), makerAmount);
const dexPaths = createPathsFromDexQuotes(dexQuotes, opts.noConflicts);
const dexPaths = createBuyPathsFromDexQuotes(dexQuotes, ethToTakerAssetRate, opts);
const allPaths = [...dexPaths];
const allFills = flattenDexPaths(dexPaths);
// If native orders are allowed, splice them in.
if (!opts.excludedSources.includes(ERC20BridgeSource.Native)) {
allPaths.splice(0, 0, clippedNativePath);
allFills.splice(0, 0, ...clippedNativePath);
allPaths.splice(0, 0, nativeFills);
allFills.splice(0, 0, ...nativeFills);
}
const optimizer = new FillsOptimizer(opts.runLimit, 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
// the greediest solution first, which is the optimal solution in most
// cases.
sortFillsByPrice(allFills),
sortFillsByAdjustedRate(allFills, true),
makerAmount,
upperBoundPath,
);
@ -287,19 +327,25 @@ function difference<T>(a: T[], b: T[]): T[] {
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[] = [];
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < orders.length; i++) {
const order = orders[i];
const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(order);
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees(order);
const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(order);
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order);
// Native orders can be filled in any order, so they're all root nodes.
path.push({
flags: FillFlags.SourceNative,
exclusionMask: 0,
input: takerAmount,
output: makerAmount,
// Every fill from native orders incurs a penalty.
fillPenalty: ethToOutputAssetRate.times(opts.fees[ERC20BridgeSource.Native] || 0),
fillData: {
source: ERC20BridgeSource.Native,
order,
@ -309,19 +355,26 @@ function createSellPathFromNativeOrders(orders: SignedOrderWithFillableAmounts[]
return path;
}
function createBuyPathFromNativeOrders(orders: SignedOrderWithFillableAmounts[]): Fill[] {
function createBuyPathFromNativeOrders(
orders: SignedOrderWithFillableAmounts[],
ethToOutputAssetRate: BigNumber,
opts: GetMarketOrdersOpts,
): Fill[] {
const path: Fill[] = [];
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < orders.length; i++) {
const order = orders[i];
const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(order);
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees(order);
const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(order);
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order);
// Native orders can be filled in any order, so they're all root nodes.
path.push({
flags: FillFlags.SourceNative,
exclusionMask: 0,
input: makerAmount,
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: {
source: ERC20BridgeSource.Native,
order,
@ -331,31 +384,75 @@ function createBuyPathFromNativeOrders(orders: SignedOrderWithFillableAmounts[])
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[][] = [];
for (const quote of dexQuotes) {
// Native orders can be filled in any order, so they're all root nodes.
const path: Fill[] = [];
let prevSample: DexSample | undefined;
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < quote.length; i++) {
const sample = quote[i];
if (sample.output.eq(0) || (prevSample && prevSample.output.gte(sample.output))) {
// Stop if the output is zero or does not increase.
// Stop of the sample has zero output, which can occur if the source
// cannot fill the full amount.
if (sample.output.isZero()) {
break;
}
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),
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 },
});
prevSample = quote[i];
}
if (path.length > 0) {
// 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);
}
}
@ -389,11 +486,6 @@ function sourceToExclusionMask(source: ERC20BridgeSource): number {
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`.
function flattenDexPaths(dexFills: Fill[][]): Fill[] {
const fills: Fill[] = [];
@ -411,7 +503,7 @@ function pickBestUpperBoundPath(paths: Fill[][], maxInput: BigNumber, shouldMini
let optimalPathOutput: BigNumber = ZERO_AMOUNT;
for (const path of paths) {
if (getPathInput(path).gte(maxInput)) {
const output = getPathOutput(path, maxInput);
const output = getPathAdjustedOutput(path, maxInput);
if (!optimalPath || comparePathOutputs(output, optimalPathOutput, !!shouldMinimize) === 1) {
optimalPath = path;
optimalPathOutput = output;
@ -454,19 +546,6 @@ function collapsePath(path: Fill[], isBuy: boolean): CollapsedFill[] {
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] {
const assets = [order.makerAssetData, order.takerAssetData].map(a => assetDataUtils.decodeAssetDataOrThrow(a)) as [
ERC20AssetData,
@ -478,15 +557,4 @@ function getOrderTokens(order: SignedOrder): [string, string] {
return assets.map(a => a.tokenAddress) as [string, string];
}
function clipPathToInput(path: Fill[], assetAmount: BigNumber): Fill[] {
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;
}
// tslint:disable: max-file-line-count

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(
sources: ERC20BridgeSource[],
makerToken: string,
@ -374,7 +413,17 @@ export class DexOrderSampler {
*/
public async executeBatchAsync<T extends Array<BatchedOperation<any>>>(ops: T): Promise<any[]> {
const callDatas = ops.map(o => o.encodeCall(this._samplerContract));
const callResults = await this._samplerContract.batchCall(callDatas).callAsync();
return Promise.all(callResults.map(async (r, i) => ops[i].handleCallResultsAsync(this._samplerContract, r)));
// Execute all non-empty calldatas.
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;
// Output fill amount (maker asset amount in a sell, taker asset amount in a buy).
output: BigNumber;
// Output penalty for this fill.
fillPenalty: BigNumber;
// Fill that must precede this one. This enforces certain fills to be contiguous.
parent?: Fill;
// Data associated with this this Fill object. Used to reconstruct orders
@ -167,4 +169,8 @@ export interface GetMarketOrdersOpts {
* Default: 1.25.
*/
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();
let resultOrders: OptimizedMarketOrder[] = [];
{
// Scale fees by gas price.
const _opts = {
...opts,
fees: _.mapValues(opts.fees, (v, k) => v.times(gasPrice)),
};
if (operation === MarketOperation.Buy) {
resultOrders = await this._marketOperationUtils.getMarketBuyOrdersAsync(
prunedOrders,
assetFillAmount.plus(slippageBufferAmount),
opts,
_opts,
);
} else {
resultOrders = await this._marketOperationUtils.getMarketSellOrdersAsync(
prunedOrders,
assetFillAmount.plus(slippageBufferAmount),
opts,
_opts,
);
}
}
// assetData information for the result
const { makerAssetData, takerAssetData } = prunedOrders[0];
@ -241,10 +248,10 @@ export class SwapQuoteCalculator {
break;
}
if (order.fill.source === ERC20BridgeSource.Native) {
const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(
const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(
order,
);
const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees(
const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(
order,
);
const takerAssetAmountWithFees = BigNumber.min(
@ -335,10 +342,10 @@ export class SwapQuoteCalculator {
break;
}
if (order.fill.source === ERC20BridgeSource.Native) {
const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(
const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(
order,
);
const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterFees(
const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(
order,
);
makerAssetAmount = BigNumber.min(remainingMakerAssetFillAmount, adjustedFillableMakerAssetAmount);

View File

@ -36,9 +36,9 @@ const MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER = testOrderFactory.generateTestSig
});
describe('fillableAmountsUtils', () => {
describe('getTakerAssetAmountSwappedAfterFees', () => {
describe('getTakerAssetAmountSwappedAfterOrderFees', () => {
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,
);
expect(availableAssetAmount).to.bignumber.eq(
@ -47,15 +47,15 @@ describe('fillableAmountsUtils', () => {
});
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,
);
expect(availableAssetAmount).to.bignumber.eq(baseUnitAmount(12));
});
});
describe('getMakerAssetAmountSwappedAfterFees', () => {
describe('getMakerAssetAmountSwappedAfterOrderFees', () => {
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,
);
expect(availableAssetAmount).to.bignumber.eq(
@ -64,7 +64,7 @@ describe('fillableAmountsUtils', () => {
});
it('should return fillableMakerAssetAmount - fillableTakerFeeif takerFee is denominated in maker', () => {
const availableAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterFees(
const availableAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(
MAKER_ASSET_DENOMINATED_TAKER_FEE_ORDER,
);
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[] {
const rates: BigNumber[] = [];
const initialRate = getRandomFloat(1e-3, 1e2);
@ -188,9 +201,9 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Eth2Dai]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.Kyber]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.Uniswap]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.CurveUsdcDai]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.CurveUsdcDaiUsdt]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: createDecreasingRates(NUM_SAMPLES),
[ERC20BridgeSource.CurveUsdcDai]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.CurveUsdcDaiUsdt]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: _.times(NUM_SAMPLES, () => 0),
};
function findSourceWithMaxOutput(rates: RatesBySource): ERC20BridgeSource {
@ -224,6 +237,7 @@ describe('MarketOperationUtils tests', () => {
getCurveSellQuotes: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.CurveUsdcDai]),
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES),
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES),
getMedianSellRate: createGetMedianSellRate(1),
};
function replaceSamplerOps(ops: Partial<typeof DEFAULT_OPS> = {}): void {
@ -255,7 +269,17 @@ describe('MarketOperationUtils tests', () => {
FILL_AMOUNT,
_.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(() => {
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 () => {
const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1];
rates[ERC20BridgeSource.Uniswap] = [0.5, 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.CurveUsdcDai] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdt] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdtTusd] = [0, 0, 0, 0];
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
});
@ -410,8 +410,7 @@ describe('MarketOperationUtils tests', () => {
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false },
);
expect(improvedOrders).to.be.length(4);
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData));
const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [
ERC20BridgeSource.Kyber,
ERC20BridgeSource.Eth2Dai,
@ -427,9 +426,6 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Uniswap] = [0.5, 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.CurveUsdcDai] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdt] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdtTusd] = [0, 0, 0, 0];
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
});
@ -438,8 +434,7 @@ describe('MarketOperationUtils tests', () => {
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true },
);
expect(improvedOrders).to.be.length(4);
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData));
const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Uniswap,
@ -455,9 +450,6 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Uniswap] = [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.CurveUsdcDai] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdt] = [0, 0, 0, 0];
rates[ERC20BridgeSource.CurveUsdcDaiUsdtTusd] = [0, 0, 0, 0];
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
});
@ -466,8 +458,7 @@ describe('MarketOperationUtils tests', () => {
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true },
);
expect(improvedOrders).to.be.length(4);
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData));
const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [
ERC20BridgeSource.Kyber,
ERC20BridgeSource.Native,
@ -476,6 +467,72 @@ describe('MarketOperationUtils tests', () => {
];
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()', () => {
@ -484,7 +541,16 @@ describe('MarketOperationUtils tests', () => {
FILL_AMOUNT,
_.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(() => {
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 () => {
const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1];
@ -643,8 +688,7 @@ describe('MarketOperationUtils tests', () => {
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512 },
);
expect(improvedOrders).to.be.length(4);
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData));
const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Uniswap,
@ -653,6 +697,75 @@ describe('MarketOperationUtils tests', () => {
];
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: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 contractAddresses: ContractAddresses;
@ -294,7 +293,6 @@ describe('swapQuoteCalculator', () => {
// test if orders are correct
expect(swapQuote.orders).to.deep.equal([
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[2],
testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[1],
]);
expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount);