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:
parent
cbb23a42e2
commit
0f1c15a6ca
@ -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": [
|
||||||
|
@ -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 {
|
||||||
|
@ -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),
|
||||||
};
|
};
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
@ -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');
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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));
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user