Merge pull request #2513 from 0xProject/feat/asset-swapper/death-to-reverts

Asset-swapper: Fallback orders + refactors
This commit is contained in:
Lawrence Forman
2020-03-12 18:45:27 -04:00
committed by GitHub
29 changed files with 1364 additions and 1388 deletions

View File

@@ -5,6 +5,26 @@
{ {
"note": "Add support for private liquidity providers", "note": "Add support for private liquidity providers",
"pr": 2505 "pr": 2505
},
{
"note": "Big refactor of market operation utils",
"pr": 2513
},
{
"note": "Remove `dustFractionThreshold`, `noConflicts` options.",
"pr": 2513
},
{
"note": "Revamp fill optimization algorithm",
"pr": 2513
},
{
"note": "Add fallback orders to quotes via `allowFallback` option.",
"pr": 2513
},
{
"note": "Add `maxFallbackSlippage` option.",
"pr": 2513
} }
] ]
}, },

View File

@@ -11,8 +11,7 @@ import {
SwapQuoterOpts, SwapQuoterOpts,
} from './types'; } from './types';
import { constants as marketOperationUtilConstants } from './utils/market_operation_utils/constants'; import { DEFAULT_GET_MARKET_ORDERS_OPTS } from './utils/market_operation_utils/constants';
import { ERC20BridgeSource } from './utils/market_operation_utils/types';
const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info'; const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info';
const NULL_BYTES = '0x'; const NULL_BYTES = '0x';
@@ -43,7 +42,7 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
orderRefreshIntervalMs: 10000, // 10 seconds orderRefreshIntervalMs: 10000, // 10 seconds
}, },
...DEFAULT_ORDER_PRUNER_OPTS, ...DEFAULT_ORDER_PRUNER_OPTS,
samplerGasLimit: 59e6, samplerGasLimit: 250e6,
}; };
const DEFAULT_FORWARDER_EXTENSION_CONTRACT_OPTS: ForwarderExtensionContractOpts = { const DEFAULT_FORWARDER_EXTENSION_CONTRACT_OPTS: ForwarderExtensionContractOpts = {
@@ -59,48 +58,7 @@ const DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS: SwapQuoteGetOutputOpts = {
const DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS: SwapQuoteExecutionOpts = DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS; const DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS: SwapQuoteExecutionOpts = DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS;
const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = { const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = {
...{ ...DEFAULT_GET_MARKET_ORDERS_OPTS,
slippagePercentage: 0.2, // 20% slippage protection,
},
...marketOperationUtilConstants.DEFAULT_GET_MARKET_ORDERS_OPTS,
};
// Mainnet Curve configuration
const DEFAULT_CURVE_OPTS: { [source: string]: { version: number; curveAddress: string; tokens: string[] } } = {
[ERC20BridgeSource.CurveUsdcDai]: {
version: 1,
curveAddress: '0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56',
tokens: ['0x6b175474e89094c44da98b954eedeac495271d0f', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'],
},
[ERC20BridgeSource.CurveUsdcDaiUsdt]: {
version: 1,
curveAddress: '0x52ea46506b9cc5ef470c5bf89f17dc28bb35d85c',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
],
},
[ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: {
version: 1,
curveAddress: '0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
'0x0000000000085d4780b73119b644ae5ecd22b376',
],
},
[ERC20BridgeSource.CurveUsdcDaiUsdtBusd]: {
version: 1,
curveAddress: '0x79a8c46dea5ada233abaffd40f3a0a2b1e5a4f27',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
'0x4fabb145d64652a948d72533023f6e7a623c7c53',
],
},
}; };
export const constants = { export const constants = {
@@ -123,5 +81,4 @@ export const constants = {
PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS,
MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE, MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE,
BRIDGE_ASSET_DATA_PREFIX: '0xdc1600f3', BRIDGE_ASSET_DATA_PREFIX: '0xdc1600f3',
DEFAULT_CURVE_OPTS,
}; };

View File

@@ -22,7 +22,7 @@ import {
import { assert } from './utils/assert'; import { assert } from './utils/assert';
import { calculateLiquidity } from './utils/calculate_liquidity'; import { calculateLiquidity } from './utils/calculate_liquidity';
import { MarketOperationUtils } from './utils/market_operation_utils'; import { MarketOperationUtils } from './utils/market_operation_utils';
import { dummyOrderUtils } from './utils/market_operation_utils/dummy_order_utils'; import { createDummyOrderForSampler } from './utils/market_operation_utils/orders';
import { DexOrderSampler } from './utils/market_operation_utils/sampler'; import { DexOrderSampler } from './utils/market_operation_utils/sampler';
import { orderPrunerUtils } from './utils/order_prune_utils'; import { orderPrunerUtils } from './utils/order_prune_utils';
import { OrderStateUtils } from './utils/order_state_utils'; import { OrderStateUtils } from './utils/order_state_utils';
@@ -242,11 +242,7 @@ export class SwapQuoter {
): Promise<Array<MarketBuySwapQuote | undefined>> { ): Promise<Array<MarketBuySwapQuote | undefined>> {
makerAssetBuyAmount.map((a, i) => assert.isBigNumber(`makerAssetBuyAmount[${i}]`, a)); makerAssetBuyAmount.map((a, i) => assert.isBigNumber(`makerAssetBuyAmount[${i}]`, a));
let gasPrice: BigNumber; let gasPrice: BigNumber;
const { slippagePercentage, ...calculateSwapQuoteOpts } = _.merge( const calculateSwapQuoteOpts = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options);
{},
constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
options,
);
if (!!options.gasPrice) { if (!!options.gasPrice) {
gasPrice = options.gasPrice; gasPrice = options.gasPrice;
assert.isBigNumber('gasPrice', gasPrice); assert.isBigNumber('gasPrice', gasPrice);
@@ -264,7 +260,7 @@ export class SwapQuoter {
); );
if (prunedOrders.length === 0) { if (prunedOrders.length === 0) {
return [ return [
dummyOrderUtils.createDummyOrderForSampler( createDummyOrderForSampler(
makerAssetDatas[i], makerAssetDatas[i],
takerAssetData, takerAssetData,
this._contractAddresses.uniswapBridge, this._contractAddresses.uniswapBridge,
@@ -278,7 +274,6 @@ export class SwapQuoter {
const swapQuotes = await this._swapQuoteCalculator.calculateBatchMarketBuySwapQuoteAsync( const swapQuotes = await this._swapQuoteCalculator.calculateBatchMarketBuySwapQuoteAsync(
allPrunedOrders, allPrunedOrders,
makerAssetBuyAmount, makerAssetBuyAmount,
slippagePercentage,
gasPrice, gasPrice,
calculateSwapQuoteOpts, calculateSwapQuoteOpts,
); );
@@ -517,14 +512,9 @@ export class SwapQuoter {
marketOperation: MarketOperation, marketOperation: MarketOperation,
options: Partial<SwapQuoteRequestOpts>, options: Partial<SwapQuoteRequestOpts>,
): Promise<SwapQuote> { ): Promise<SwapQuote> {
const { slippagePercentage, ...calculateSwapQuoteOpts } = _.merge( const calculateSwapQuoteOpts = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options);
{},
constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
options,
);
assert.isString('makerAssetData', makerAssetData); assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData); assert.isString('takerAssetData', takerAssetData);
assert.isNumber('slippagePercentage', slippagePercentage);
let gasPrice: BigNumber; let gasPrice: BigNumber;
if (!!options.gasPrice) { if (!!options.gasPrice) {
gasPrice = options.gasPrice; gasPrice = options.gasPrice;
@@ -537,11 +527,7 @@ export class SwapQuoter {
// if no native orders, pass in a dummy order for the sampler to have required metadata for sampling // if no native orders, pass in a dummy order for the sampler to have required metadata for sampling
if (prunedOrders.length === 0) { if (prunedOrders.length === 0) {
prunedOrders = [ prunedOrders = [
dummyOrderUtils.createDummyOrderForSampler( createDummyOrderForSampler(makerAssetData, takerAssetData, this._contractAddresses.uniswapBridge),
makerAssetData,
takerAssetData,
this._contractAddresses.uniswapBridge,
),
]; ];
} }
@@ -551,7 +537,6 @@ export class SwapQuoter {
swapQuote = await this._swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( swapQuote = await this._swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
prunedOrders, prunedOrders,
assetFillAmount, assetFillAmount,
slippagePercentage,
gasPrice, gasPrice,
calculateSwapQuoteOpts, calculateSwapQuoteOpts,
); );
@@ -559,7 +544,6 @@ export class SwapQuoter {
swapQuote = await this._swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( swapQuote = await this._swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
prunedOrders, prunedOrders,
assetFillAmount, assetFillAmount,
slippagePercentage,
gasPrice, gasPrice,
calculateSwapQuoteOpts, calculateSwapQuoteOpts,
); );

View File

@@ -169,6 +169,7 @@ export interface MarketBuySwapQuote extends SwapQuoteBase {
* totalTakerAssetAmount: The total amount of takerAsset required to complete the swap (filling orders, and paying takerFees). * totalTakerAssetAmount: The total amount of takerAsset required to complete the swap (filling orders, and paying takerFees).
* makerAssetAmount: The amount of makerAsset that will be acquired through the swap. * makerAssetAmount: The amount of makerAsset that will be acquired through the swap.
* protocolFeeInWeiAmount: The amount of ETH to pay (in WEI) as protocol fee to perform the swap for desired asset. * protocolFeeInWeiAmount: The amount of ETH to pay (in WEI) as protocol fee to perform the swap for desired asset.
* gas: Amount of estimated gas needed to fill the quote.
*/ */
export interface SwapQuoteInfo { export interface SwapQuoteInfo {
feeTakerAssetAmount: BigNumber; feeTakerAssetAmount: BigNumber;
@@ -176,6 +177,7 @@ export interface SwapQuoteInfo {
totalTakerAssetAmount: BigNumber; totalTakerAssetAmount: BigNumber;
makerAssetAmount: BigNumber; makerAssetAmount: BigNumber;
protocolFeeInWeiAmount: BigNumber; protocolFeeInWeiAmount: BigNumber;
gas: number;
} }
/** /**
@@ -186,11 +188,9 @@ export interface SwapQuoteOrdersBreakdown {
} }
/** /**
* slippagePercentage: The percentage buffer to add to account for slippage. Affects max ETH price estimates. Defaults to 0.2 (20%).
* gasPrice: gas price to determine protocolFee amount, default to ethGasStation fast amount * gasPrice: gas price to determine protocolFee amount, default to ethGasStation fast amount
*/ */
export interface SwapQuoteRequestOpts extends CalculateSwapQuoteOpts { export interface SwapQuoteRequestOpts extends CalculateSwapQuoteOpts {
slippagePercentage: number;
gasPrice?: BigNumber; gasPrice?: BigNumber;
} }

View File

@@ -6,7 +6,12 @@ import * as _ from 'lodash';
import { MarketOperation, OrderProviderRequest, SwapQuote, SwapQuoteInfo } from '../types'; import { MarketOperation, OrderProviderRequest, SwapQuote, SwapQuoteInfo } from '../types';
import { utils } from './utils'; import {
isAssetDataEquivalent,
isExactAssetData,
isOrderTakerFeePayableWithMakerAsset,
isOrderTakerFeePayableWithTakerAsset,
} from './utils';
export const assert = { export const assert = {
...sharedAssert, ...sharedAssert,
@@ -36,13 +41,13 @@ export const assert = {
): void { ): void {
_.every(orders, (order: SignedOrder, index: number) => { _.every(orders, (order: SignedOrder, index: number) => {
assert.assert( assert.assert(
utils.isAssetDataEquivalent(takerAssetData, order.takerAssetData), isAssetDataEquivalent(takerAssetData, order.takerAssetData),
`Expected ${variableName}[${index}].takerAssetData to be ${takerAssetData} but found ${ `Expected ${variableName}[${index}].takerAssetData to be ${takerAssetData} but found ${
order.takerAssetData order.takerAssetData
}`, }`,
); );
assert.assert( assert.assert(
utils.isAssetDataEquivalent(makerAssetData, order.makerAssetData), isAssetDataEquivalent(makerAssetData, order.makerAssetData),
`Expected ${variableName}[${index}].makerAssetData to be ${makerAssetData} but found ${ `Expected ${variableName}[${index}].makerAssetData to be ${makerAssetData} but found ${
order.makerAssetData order.makerAssetData
}`, }`,
@@ -53,8 +58,8 @@ export const assert = {
_.every(orders, (order: T, index: number) => { _.every(orders, (order: T, index: number) => {
assert.assert( assert.assert(
order.takerFee.isZero() || order.takerFee.isZero() ||
utils.isOrderTakerFeePayableWithTakerAsset(order) || isOrderTakerFeePayableWithTakerAsset(order) ||
utils.isOrderTakerFeePayableWithMakerAsset(order), isOrderTakerFeePayableWithMakerAsset(order),
`Expected ${variableName}[${index}].takerFeeAssetData to be ${order.makerAssetData} or ${ `Expected ${variableName}[${index}].takerFeeAssetData to be ${order.makerAssetData} or ${
order.takerAssetData order.takerAssetData
} but found ${order.takerFeeAssetData}`, } but found ${order.takerFeeAssetData}`,
@@ -72,11 +77,12 @@ export const assert = {
}, },
isValidForwarderSignedOrder(variableName: string, order: SignedOrder, wethAssetData: string): void { isValidForwarderSignedOrder(variableName: string, order: SignedOrder, wethAssetData: string): void {
assert.assert( assert.assert(
utils.isExactAssetData(order.takerAssetData, wethAssetData), isExactAssetData(order.takerAssetData, wethAssetData),
`Expected ${variableName} to have takerAssetData set as ${wethAssetData}, but is ${order.takerAssetData}`, `Expected ${variableName} to have takerAssetData set as ${wethAssetData}, but is ${order.takerAssetData}`,
); );
}, },
isValidSwapQuoteInfo(variableName: string, swapQuoteInfo: SwapQuoteInfo): void { isValidSwapQuoteInfo(variableName: string, swapQuoteInfo: SwapQuoteInfo): void {
sharedAssert.isNumber(`${variableName}.gas`, swapQuoteInfo.gas);
sharedAssert.isBigNumber(`${variableName}.feeTakerAssetAmount`, swapQuoteInfo.feeTakerAssetAmount); sharedAssert.isBigNumber(`${variableName}.feeTakerAssetAmount`, swapQuoteInfo.feeTakerAssetAmount);
sharedAssert.isBigNumber(`${variableName}.totalTakerAssetAmount`, swapQuoteInfo.totalTakerAssetAmount); sharedAssert.isBigNumber(`${variableName}.totalTakerAssetAmount`, swapQuoteInfo.totalTakerAssetAmount);
sharedAssert.isBigNumber(`${variableName}.takerAssetAmount`, swapQuoteInfo.takerAssetAmount); sharedAssert.isBigNumber(`${variableName}.takerAssetAmount`, swapQuoteInfo.takerAssetAmount);

View File

@@ -2,17 +2,17 @@ import { BigNumber } from '@0x/utils';
import { LiquidityForTakerMakerAssetDataPair, SignedOrderWithFillableAmounts } from '../types'; import { LiquidityForTakerMakerAssetDataPair, SignedOrderWithFillableAmounts } from '../types';
import { utils } from './utils'; import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils';
export const calculateLiquidity = ( export const calculateLiquidity = (
prunedOrders: SignedOrderWithFillableAmounts[], prunedOrders: SignedOrderWithFillableAmounts[],
): LiquidityForTakerMakerAssetDataPair => { ): LiquidityForTakerMakerAssetDataPair => {
const liquidityInBigNumbers = prunedOrders.reduce( const liquidityInBigNumbers = prunedOrders.reduce(
(acc, order) => { (acc, order) => {
const fillableMakerAssetAmount = utils.isOrderTakerFeePayableWithMakerAsset(order) const fillableMakerAssetAmount = isOrderTakerFeePayableWithMakerAsset(order)
? order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount) ? order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount)
: order.fillableMakerAssetAmount; : order.fillableMakerAssetAmount;
const fillableTakerAssetAmount = utils.isOrderTakerFeePayableWithTakerAsset(order) const fillableTakerAssetAmount = isOrderTakerFeePayableWithTakerAsset(order)
? order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount) ? order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount)
: order.fillableTakerAssetAmount; : order.fillableTakerAssetAmount;
return { return {

View File

@@ -3,18 +3,18 @@ import * as _ from 'lodash';
import { SignedOrderWithFillableAmounts } from '../types'; import { SignedOrderWithFillableAmounts } from '../types';
import { utils } from './utils'; import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils';
export const fillableAmountsUtils = { export const fillableAmountsUtils = {
getTakerAssetAmountSwappedAfterOrderFees(order: SignedOrderWithFillableAmounts): BigNumber { getTakerAssetAmountSwappedAfterOrderFees(order: SignedOrderWithFillableAmounts): BigNumber {
if (utils.isOrderTakerFeePayableWithTakerAsset(order)) { if (isOrderTakerFeePayableWithTakerAsset(order)) {
return order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount); return order.fillableTakerAssetAmount.plus(order.fillableTakerFeeAmount);
} else { } else {
return order.fillableTakerAssetAmount; return order.fillableTakerAssetAmount;
} }
}, },
getMakerAssetAmountSwappedAfterOrderFees(order: SignedOrderWithFillableAmounts): BigNumber { getMakerAssetAmountSwappedAfterOrderFees(order: SignedOrderWithFillableAmounts): BigNumber {
if (utils.isOrderTakerFeePayableWithMakerAsset(order)) { if (isOrderTakerFeePayableWithMakerAsset(order)) {
return order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount); return order.fillableMakerAssetAmount.minus(order.fillableTakerFeeAmount);
} else { } else {
return order.fillableMakerAssetAmount; return order.fillableMakerAssetAmount;

View File

@@ -2,7 +2,7 @@ import { BigNumber } from '@0x/utils';
import { ERC20BridgeSource, GetMarketOrdersOpts } from './types'; import { ERC20BridgeSource, GetMarketOrdersOpts } from './types';
const INFINITE_TIMESTAMP_SEC = new BigNumber(2524604400); // tslint:disable: custom-no-magic-numbers
/** /**
* Valid sources for market sell. * Valid sources for market sell.
@@ -27,12 +27,13 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
// tslint:disable-next-line: custom-no-magic-numbers // tslint:disable-next-line: custom-no-magic-numbers
runLimit: 2 ** 15, runLimit: 2 ** 15,
excludedSources: [], excludedSources: [],
bridgeSlippage: 0.0005, bridgeSlippage: 0.005,
dustFractionThreshold: 0.0025, maxFallbackSlippage: 0.05,
numSamples: 13, numSamples: 13,
noConflicts: true,
sampleDistributionBase: 1.05, sampleDistributionBase: 1.05,
fees: {}, feeSchedule: {},
gasSchedule: {},
allowFallback: true,
}; };
/** /**
@@ -40,13 +41,53 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
*/ */
export const FEE_QUOTE_SOURCES = SELL_SOURCES; export const FEE_QUOTE_SOURCES = SELL_SOURCES;
export const constants = { /**
INFINITE_TIMESTAMP_SEC, * Mainnet Curve configuration
SELL_SOURCES, */
BUY_SOURCES, export const DEFAULT_CURVE_OPTS: { [source: string]: { version: number; curveAddress: string; tokens: string[] } } = {
DEFAULT_GET_MARKET_ORDERS_OPTS, [ERC20BridgeSource.CurveUsdcDai]: {
ERC20_PROXY_ID: '0xf47261b0', version: 1,
FEE_QUOTE_SOURCES, curveAddress: '0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56',
WALLET_SIGNATURE: '0x04', tokens: ['0x6b175474e89094c44da98b954eedeac495271d0f', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'],
ONE_ETHER: new BigNumber(1e18), },
[ERC20BridgeSource.CurveUsdcDaiUsdt]: {
version: 1,
curveAddress: '0x52ea46506b9cc5ef470c5bf89f17dc28bb35d85c',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
],
},
[ERC20BridgeSource.CurveUsdcDaiUsdtTusd]: {
version: 1,
curveAddress: '0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
'0x0000000000085d4780b73119b644ae5ecd22b376',
],
},
[ERC20BridgeSource.CurveUsdcDaiUsdtBusd]: {
version: 1,
curveAddress: '0x79a8c46dea5ada233abaffd40f3a0a2b1e5a4f27',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
'0x4fabb145d64652a948d72533023f6e7a623c7c53',
],
},
}; };
export const ERC20_PROXY_ID = '0xf47261b0';
export const WALLET_SIGNATURE = '0x04';
export const ONE_ETHER = new BigNumber(1e18);
export const NEGATIVE_INF = new BigNumber('-Infinity');
export const POSITIVE_INF = new BigNumber('Infinity');
export const ZERO_AMOUNT = new BigNumber(0);
export const ONE_HOUR_IN_SECONDS = 60 * 60;
export const ONE_SECOND_MS = 1000;
export const NULL_BYTES = '0x';
export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';

View File

@@ -1,234 +0,0 @@
import { assert } from '@0x/assert';
import { ContractAddresses } from '@0x/contract-addresses';
import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { AbiEncoder, BigNumber } from '@0x/utils';
import { constants } from '../../constants';
import { constants as marketOperationUtilConstants } from './constants';
import {
AggregationError,
CollapsedFill,
ERC20BridgeSource,
NativeCollapsedFill,
OptimizedMarketOrder,
OrderDomain,
} from './types';
const { NULL_BYTES, NULL_ADDRESS, ZERO_AMOUNT } = constants;
const { INFINITE_TIMESTAMP_SEC, WALLET_SIGNATURE } = marketOperationUtilConstants;
export class CreateOrderUtils {
private readonly _contractAddress: ContractAddresses;
// utility function for asset-swapper to ignore market operation utils for specific asset types
public static convertNativeOrderToFullyFillableOptimizedOrders(order: SignedOrder): OptimizedMarketOrder {
return {
...order,
fillableMakerAssetAmount: order.makerAssetAmount,
fillableTakerAssetAmount: order.takerAssetAmount,
fillableTakerFeeAmount: order.takerFee,
fill: {
source: ERC20BridgeSource.Native,
totalMakerAssetAmount: order.makerAssetAmount,
totalTakerAssetAmount: order.takerAssetAmount,
subFills: [],
},
};
}
constructor(contractAddress: ContractAddresses) {
this._contractAddress = contractAddress;
}
// Convert sell fills into orders.
public createSellOrdersFromPath(
orderDomain: OrderDomain,
inputToken: string,
outputToken: string,
path: CollapsedFill[],
bridgeSlippage: number,
liquidityProviderAddress?: string,
): OptimizedMarketOrder[] {
const orders: OptimizedMarketOrder[] = [];
for (const fill of path) {
if (fill.source === ERC20BridgeSource.Native) {
orders.push(createNativeOrder(fill));
} else {
orders.push(
createBridgeOrder(
orderDomain,
fill,
this._getBridgeAddressFromSource(fill.source, liquidityProviderAddress),
outputToken,
inputToken,
bridgeSlippage,
),
);
}
}
return orders;
}
// Convert buy fills into orders.
public createBuyOrdersFromPath(
orderDomain: OrderDomain,
inputToken: string,
outputToken: string,
path: CollapsedFill[],
bridgeSlippage: number,
liquidityProviderAddress?: string,
): OptimizedMarketOrder[] {
const orders: OptimizedMarketOrder[] = [];
for (const fill of path) {
if (fill.source === ERC20BridgeSource.Native) {
orders.push(createNativeOrder(fill));
} else {
orders.push(
createBridgeOrder(
orderDomain,
fill,
this._getBridgeAddressFromSource(fill.source, liquidityProviderAddress),
inputToken,
outputToken,
bridgeSlippage,
true,
),
);
}
}
return orders;
}
private _getBridgeAddressFromSource(source: ERC20BridgeSource, liquidityProviderAddress?: string): string {
switch (source) {
case ERC20BridgeSource.Eth2Dai:
return this._contractAddress.eth2DaiBridge;
case ERC20BridgeSource.Kyber:
return this._contractAddress.kyberBridge;
case ERC20BridgeSource.Uniswap:
return this._contractAddress.uniswapBridge;
case ERC20BridgeSource.CurveUsdcDai:
case ERC20BridgeSource.CurveUsdcDaiUsdt:
case ERC20BridgeSource.CurveUsdcDaiUsdtTusd:
case ERC20BridgeSource.CurveUsdcDaiUsdtBusd:
return this._contractAddress.curveBridge;
case ERC20BridgeSource.LiquidityProvider:
if (liquidityProviderAddress === undefined) {
throw new Error(
'Cannot create a LiquidityProvider order without a LiquidityProvider pool address.',
);
}
assert.isETHAddressHex('liquidityProviderAddress', liquidityProviderAddress);
return liquidityProviderAddress;
default:
break;
}
throw new Error(AggregationError.NoBridgeForSource);
}
}
function createBridgeOrder(
orderDomain: OrderDomain,
fill: CollapsedFill,
bridgeAddress: string,
makerToken: string,
takerToken: string,
slippage: number,
isBuy: boolean = false,
): OptimizedMarketOrder {
let makerAssetData;
if (Object.keys(constants.DEFAULT_CURVE_OPTS).includes(fill.source)) {
const { curveAddress, tokens, version } = constants.DEFAULT_CURVE_OPTS[fill.source];
const fromTokenIdx = tokens.indexOf(takerToken);
const toTokenIdx = tokens.indexOf(makerToken);
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createCurveBridgeData(curveAddress, fromTokenIdx, toTokenIdx, version),
);
} else {
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createBridgeData(takerToken),
);
}
return {
makerAddress: bridgeAddress,
makerAssetData,
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken),
...createCommonOrderFields(orderDomain, fill, slippage, isBuy),
};
}
function createBridgeData(tokenAddress: string): string {
const encoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]);
return encoder.encode({ tokenAddress });
}
function createCurveBridgeData(
curveAddress: string,
fromTokenIdx: number,
toTokenIdx: number,
version: number,
): string {
const curveBridgeDataEncoder = AbiEncoder.create([
{ name: 'curveAddress', type: 'address' },
{ name: 'fromTokenIdx', type: 'int128' },
{ name: 'toTokenIdx', type: 'int128' },
{ name: 'version', type: 'int128' },
]);
return curveBridgeDataEncoder.encode([curveAddress, fromTokenIdx, toTokenIdx, version]);
}
type CommonOrderFields = Pick<
OptimizedMarketOrder,
Exclude<keyof OptimizedMarketOrder, 'makerAddress' | 'makerAssetData' | 'takerAssetData'>
>;
function createCommonOrderFields(
orderDomain: OrderDomain,
fill: CollapsedFill,
slippage: number,
isBuy: boolean = false,
): CommonOrderFields {
const makerAssetAmountAdjustedWithSlippage = isBuy
? fill.totalMakerAssetAmount
: fill.totalMakerAssetAmount.times(1 - slippage).integerValue(BigNumber.ROUND_DOWN);
const takerAssetAmountAdjustedWithSlippage = isBuy
? fill.totalTakerAssetAmount.times(slippage + 1).integerValue(BigNumber.ROUND_UP)
: fill.totalTakerAssetAmount;
return {
fill,
takerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
feeRecipientAddress: NULL_ADDRESS,
salt: generatePseudoRandomSalt(),
expirationTimeSeconds: INFINITE_TIMESTAMP_SEC,
makerFeeAssetData: NULL_BYTES,
takerFeeAssetData: NULL_BYTES,
makerFee: ZERO_AMOUNT,
takerFee: ZERO_AMOUNT,
makerAssetAmount: makerAssetAmountAdjustedWithSlippage,
fillableMakerAssetAmount: makerAssetAmountAdjustedWithSlippage,
takerAssetAmount: takerAssetAmountAdjustedWithSlippage,
fillableTakerAssetAmount: takerAssetAmountAdjustedWithSlippage,
fillableTakerFeeAmount: ZERO_AMOUNT,
signature: WALLET_SIGNATURE,
...orderDomain,
};
}
function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder {
return {
fill: {
source: fill.source,
totalMakerAssetAmount: fill.totalMakerAssetAmount,
totalTakerAssetAmount: fill.totalTakerAssetAmount,
subFills: fill.subFills,
},
...(fill as NativeCollapsedFill).nativeOrder,
};
}

View File

@@ -1,32 +0,0 @@
import { SignedOrder } from '@0x/types';
import { constants } from '../../constants';
import { constants as marketOperationUtilConstants } from './constants';
const { NULL_ADDRESS, NULL_BYTES, ZERO_AMOUNT } = constants;
const { INFINITE_TIMESTAMP_SEC } = marketOperationUtilConstants;
export const dummyOrderUtils = {
createDummyOrderForSampler(makerAssetData: string, takerAssetData: string, makerAddress: string): SignedOrder {
return {
makerAddress,
takerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
feeRecipientAddress: NULL_ADDRESS,
salt: ZERO_AMOUNT,
expirationTimeSeconds: INFINITE_TIMESTAMP_SEC,
makerAssetData,
takerAssetData,
makerFeeAssetData: NULL_BYTES,
takerFeeAssetData: NULL_BYTES,
makerFee: ZERO_AMOUNT,
takerFee: ZERO_AMOUNT,
makerAssetAmount: ZERO_AMOUNT,
takerAssetAmount: ZERO_AMOUNT,
signature: NULL_BYTES,
chainId: 1,
exchangeAddress: NULL_ADDRESS,
};
},
};

View File

@@ -1,181 +0,0 @@
import { BigNumber } from '@0x/utils';
import { constants } from '../../constants';
import { Fill } from './types';
const { ZERO_AMOUNT } = constants;
// Used internally by `FillsOptimizer`.
interface FillsOptimizerContext {
currentPath: Fill[];
currentPathInput: BigNumber;
currentPathAdjustedOutput: BigNumber;
currentPathFlags: number;
}
/**
* Class for finding optimized fill paths.
*/
export class FillsOptimizer {
private readonly _runLimit: number;
private readonly _shouldMinimize: boolean;
private _currentRunCount: number = 0;
private _optimalPath?: Fill[] = undefined;
private _optimalPathAdjustedOutput: BigNumber = ZERO_AMOUNT;
constructor(runLimit: number, shouldMinimize?: boolean) {
this._runLimit = runLimit;
this._shouldMinimize = !!shouldMinimize;
}
public optimize(fills: Fill[], input: BigNumber, upperBoundPath?: Fill[]): Fill[] | undefined {
this._currentRunCount = 0;
this._optimalPath = upperBoundPath;
this._optimalPathAdjustedOutput = upperBoundPath ? getPathAdjustedOutput(upperBoundPath, input) : ZERO_AMOUNT;
const ctx = {
currentPath: [],
currentPathInput: ZERO_AMOUNT,
currentPathAdjustedOutput: ZERO_AMOUNT,
currentPathFlags: 0,
};
// Visit all valid combinations of fills to find the optimal path.
this._walk(fills, input, ctx);
if (this._optimalPath) {
return sortFillsByAdjustedRate(this._optimalPath, this._shouldMinimize);
}
return undefined;
}
private _walk(fills: Fill[], input: BigNumber, ctx: FillsOptimizerContext): void {
const { currentPath, currentPathInput, currentPathAdjustedOutput, currentPathFlags } = ctx;
// Stop if the current path is already complete.
if (currentPathInput.gte(input)) {
this._updateOptimalPath(currentPath, currentPathAdjustedOutput);
return;
}
const lastNode = currentPath.length !== 0 ? currentPath[currentPath.length - 1] : undefined;
// Visit next fill candidates.
for (const nextFill of fills) {
// Subsequent fills must be a root node or be preceded by its parent,
// enforcing contiguous fills.
if (nextFill.parent && nextFill.parent !== lastNode) {
continue;
}
// Stop if we've hit our run limit.
if (this._currentRunCount++ >= this._runLimit) {
break;
}
const nextPath = [...currentPath, nextFill];
const nextPathInput = BigNumber.min(input, currentPathInput.plus(nextFill.input));
const nextPathAdjustedOutput = currentPathAdjustedOutput.plus(
getPartialFillOutput(nextFill, nextPathInput.minus(currentPathInput)).minus(nextFill.fillPenalty),
);
// tslint:disable-next-line: no-bitwise
const nextPathFlags = currentPathFlags | nextFill.flags;
this._walk(
// Filter out incompatible fills.
// tslint:disable-next-line no-bitwise
fills.filter(f => f !== nextFill && (nextPathFlags & f.exclusionMask) === 0),
input,
{
currentPath: nextPath,
currentPathInput: nextPathInput,
currentPathAdjustedOutput: nextPathAdjustedOutput,
// tslint:disable-next-line: no-bitwise
currentPathFlags: nextPathFlags,
},
);
}
}
private _updateOptimalPath(path: Fill[], adjustedOutput: BigNumber): void {
if (!this._optimalPath || this._compareOutputs(adjustedOutput, this._optimalPathAdjustedOutput) === 1) {
this._optimalPath = path;
this._optimalPathAdjustedOutput = adjustedOutput;
}
}
private _compareOutputs(a: BigNumber, b: BigNumber): number {
return comparePathOutputs(a, b, this._shouldMinimize);
}
}
/**
* Compute the total output minus penalty for a fill path, optionally clipping the input
* to `maxInput`.
*/
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));
currentInput = partialInput;
break;
} else {
currentInput = currentInput.plus(fill.input);
currentOutput = currentOutput.plus(fill.output);
}
}
return currentOutput.minus(currentPenalty);
}
/**
* Compares two rewards, returning -1, 0, or 1
* if `a` is less than, equal to, or greater than `b`.
*/
export function comparePathOutputs(a: BigNumber, b: BigNumber, shouldMinimize: boolean): number {
return shouldMinimize ? b.comparedTo(a) : a.comparedTo(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));
}
/**
* 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

@@ -0,0 +1,304 @@
import { BigNumber } from '@0x/utils';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils';
import { POSITIVE_INF, ZERO_AMOUNT } from './constants';
import {
CollapsedFill,
DexSample,
ERC20BridgeSource,
Fill,
FillFlags,
NativeCollapsedFill,
NativeFillData,
} from './types';
// tslint:disable: prefer-for-of no-bitwise completed-docs
/**
* Create fill paths from orders and dex quotes.
*/
export function createFillPaths(opts: {
side: MarketOperation;
orders?: SignedOrderWithFillableAmounts[];
dexQuotes?: DexSample[][];
targetInput?: BigNumber;
ethToOutputRate?: BigNumber;
excludedSources?: ERC20BridgeSource[];
feeSchedule?: { [source: string]: BigNumber };
}): Fill[][] {
const { side } = opts;
const excludedSources = opts.excludedSources || [];
const feeSchedule = opts.feeSchedule || {};
const orders = opts.orders || [];
const dexQuotes = opts.dexQuotes || [];
const ethToOutputRate = opts.ethToOutputRate || ZERO_AMOUNT;
// Create native fill paths.
const nativePath = nativeOrdersToPath(side, orders, ethToOutputRate, feeSchedule);
// Create DEX fill paths.
const dexPaths = dexQuotesToPaths(side, dexQuotes, ethToOutputRate, feeSchedule);
return filterPaths([...dexPaths, nativePath].map(p => clipPathToInput(p, opts.targetInput)), excludedSources);
}
function filterPaths(paths: Fill[][], excludedSources: ERC20BridgeSource[]): Fill[][] {
return paths.filter(path => {
if (path.length === 0) {
return false;
}
const [input, output] = getPathSize(path);
if (input.eq(0) || output.eq(0)) {
return false;
}
if (excludedSources.includes(path[0].source)) {
return false;
}
return true;
});
}
function nativeOrdersToPath(
side: MarketOperation,
orders: SignedOrderWithFillableAmounts[],
ethToOutputRate: BigNumber,
fees: { [source: string]: BigNumber },
): Fill[] {
// Create a single path from all orders.
let path: Fill[] = [];
for (const order of orders) {
const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(order);
const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order);
const input = side === MarketOperation.Sell ? takerAmount : makerAmount;
const output = side === MarketOperation.Sell ? makerAmount : takerAmount;
const penalty = ethToOutputRate.times(fees[ERC20BridgeSource.Native] || 0);
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
const rate = makerAmount.div(takerAmount);
const adjustedRate =
side === MarketOperation.Sell
? makerAmount.minus(penalty).div(takerAmount)
: makerAmount.div(takerAmount.plus(penalty));
// Skip orders with rates that are <= 0.
if (adjustedRate.lte(0)) {
continue;
}
path.push({
input,
output,
rate,
adjustedRate,
adjustedOutput,
flags: 0,
index: 0, // TBD
parent: undefined, // TBD
source: ERC20BridgeSource.Native,
fillData: { order },
});
}
// Sort by descending adjusted rate.
path = path.sort((a, b) => b.adjustedRate.comparedTo(a.adjustedRate));
// Re-index fills.
for (let i = 0; i < path.length; ++i) {
path[i].parent = i === 0 ? undefined : path[i - 1];
path[i].index = i;
}
return path;
}
function dexQuotesToPaths(
side: MarketOperation,
dexQuotes: DexSample[][],
ethToOutputRate: BigNumber,
fees: { [source: string]: BigNumber },
): Fill[][] {
const paths: Fill[][] = [];
for (const quote of dexQuotes) {
const path: Fill[] = [];
for (let i = 0; i < quote.length; i++) {
const sample = quote[i];
const prevSample = i === 0 ? undefined : quote[i - 1];
const source = sample.source;
// Stop of the sample has zero output, which can occur if the source
// cannot fill the full amount.
// TODO(dorothy-zbornak): Sometimes Kyber will dip to zero then pick back up.
if (sample.output.eq(0)) {
break;
}
const input = sample.input.minus(prevSample ? prevSample.input : 0);
const output = sample.output.minus(prevSample ? prevSample.output : 0);
const penalty =
i === 0 // Only the first fill in a DEX path incurs a penalty.
? ethToOutputRate.times(fees[source] || 0)
: ZERO_AMOUNT;
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
const rate = side === MarketOperation.Sell ? output.div(input) : input.div(output);
const adjustedRate = side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput);
path.push({
input,
output,
rate,
adjustedRate,
adjustedOutput,
source,
index: i,
parent: i !== 0 ? path[path.length - 1] : undefined,
flags: sourceToFillFlags(source),
});
}
paths.push(path);
}
return paths;
}
function sourceToFillFlags(source: ERC20BridgeSource): number {
if (source === ERC20BridgeSource.Kyber) {
return FillFlags.Kyber;
}
if (source === ERC20BridgeSource.Eth2Dai) {
return FillFlags.ConflictsWithKyber;
}
if (source === ERC20BridgeSource.Uniswap) {
return FillFlags.ConflictsWithKyber;
}
return 0;
}
export function getPathSize(path: Fill[], targetInput: BigNumber = POSITIVE_INF): [BigNumber, BigNumber] {
let input = ZERO_AMOUNT;
let output = ZERO_AMOUNT;
for (const fill of path) {
if (input.plus(fill.input).gte(targetInput)) {
const di = targetInput.minus(input);
input = input.plus(di);
output = output.plus(fill.output.times(di.div(fill.input)));
break;
} else {
input = input.plus(fill.input);
output = output.plus(fill.output);
}
}
return [input.integerValue(), output.integerValue()];
}
export function getPathAdjustedSize(path: Fill[], targetInput: BigNumber = POSITIVE_INF): [BigNumber, BigNumber] {
let input = ZERO_AMOUNT;
let output = ZERO_AMOUNT;
for (const fill of path) {
if (input.plus(fill.input).gte(targetInput)) {
const di = targetInput.minus(input);
input = input.plus(di);
output = output.plus(fill.adjustedOutput.times(di.div(fill.input)));
break;
} else {
input = input.plus(fill.input);
output = output.plus(fill.adjustedOutput);
}
}
return [input.integerValue(), output.integerValue()];
}
export function isValidPath(path: Fill[]): boolean {
let flags = 0;
for (let i = 0; i < path.length; ++i) {
// Fill must immediately follow its parent.
if (path[i].parent) {
if (i === 0 || path[i - 1] !== path[i].parent) {
return false;
}
}
// Fill must not be duplicated.
for (let j = 0; j < i; ++j) {
if (path[i] === path[j]) {
return false;
}
}
flags |= path[i].flags;
}
const conflictFlags = FillFlags.Kyber | FillFlags.ConflictsWithKyber;
return (flags & conflictFlags) !== conflictFlags;
}
export function clipPathToInput(path: Fill[], targetInput: BigNumber = POSITIVE_INF): Fill[] {
const clipped: Fill[] = [];
let input = ZERO_AMOUNT;
for (const fill of path) {
if (input.gte(targetInput)) {
break;
}
input = input.plus(fill.input);
clipped.push(fill);
}
return clipped;
}
export function collapsePath(side: MarketOperation, path: Fill[]): CollapsedFill[] {
const collapsed: Array<CollapsedFill | NativeCollapsedFill> = [];
for (const fill of path) {
const makerAssetAmount = side === MarketOperation.Sell ? fill.output : fill.input;
const takerAssetAmount = side === MarketOperation.Sell ? fill.input : fill.output;
const source = fill.source;
if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) {
const prevFill = collapsed[collapsed.length - 1];
// If the last fill is from the same source, merge them.
if (prevFill.source === source) {
prevFill.totalMakerAssetAmount = prevFill.totalMakerAssetAmount.plus(makerAssetAmount);
prevFill.totalTakerAssetAmount = prevFill.totalTakerAssetAmount.plus(takerAssetAmount);
prevFill.subFills.push({ makerAssetAmount, takerAssetAmount });
continue;
}
}
collapsed.push({
source: fill.source,
totalMakerAssetAmount: makerAssetAmount,
totalTakerAssetAmount: takerAssetAmount,
subFills: [{ makerAssetAmount, takerAssetAmount }],
nativeOrder: fill.source === ERC20BridgeSource.Native ? (fill.fillData as NativeFillData).order : undefined,
});
}
return collapsed;
}
export function getFallbackSourcePaths(optimalPath: Fill[], allPaths: Fill[][]): Fill[][] {
const optimalSources: ERC20BridgeSource[] = [];
for (const fill of optimalPath) {
if (!optimalSources.includes(fill.source)) {
optimalSources.push(fill.source);
}
}
const fallbackPaths: Fill[][] = [];
for (const path of allPaths) {
if (optimalSources.includes(path[0].source)) {
continue;
}
// HACK(dorothy-zbornak): We *should* be filtering out paths that
// conflict with the optimal path (i.e., Kyber conflicts), but in
// practice we often end up not being able to find a fallback path
// because we've lost 2 major liquiduty sources. The end result is
// we end up with many more reverts than what would be actually caused
// by conflicts.
fallbackPaths.push(path);
}
return fallbackPaths;
}
export function getPathAdjustedRate(side: MarketOperation, path: Fill[], targetInput: BigNumber): BigNumber {
const [input, output] = getPathAdjustedSize(path, targetInput);
if (input.eq(0) || output.eq(0)) {
return ZERO_AMOUNT;
}
return side === MarketOperation.Sell ? output.div(input) : input.div(output);
}
export function getPathAdjustedSlippage(
side: MarketOperation,
path: Fill[],
inputAmount: BigNumber,
maxRate: BigNumber,
): number {
if (maxRate.eq(0)) {
return 0;
}
const totalRate = getPathAdjustedRate(side, path, inputAmount);
const rateChange = maxRate.minus(totalRate);
return rateChange.div(maxRate).toNumber();
}

View File

@@ -1,55 +1,41 @@
import { ContractAddresses } from '@0x/contract-addresses'; import { ContractAddresses } from '@0x/contract-addresses';
import { assetDataUtils, ERC20AssetData, orderCalculationUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types'; import { SignedOrder } from '@0x/types';
import { BigNumber, NULL_ADDRESS } from '@0x/utils'; import { BigNumber, NULL_ADDRESS } from '@0x/utils';
import { constants } from '../../constants'; import { MarketOperation } from '../../types';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; import { difference } from '../utils';
import { fillableAmountsUtils } from '../fillable_amounts_utils';
import { constants as marketOperationUtilConstants } from './constants'; import { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, FEE_QUOTE_SOURCES, ONE_ETHER, SELL_SOURCES } from './constants';
import { CreateOrderUtils } from './create_order'; import {
import { comparePathOutputs, FillsOptimizer, getPathAdjustedOutput, sortFillsByAdjustedRate } from './fill_optimizer'; createFillPaths,
getFallbackSourcePaths,
getPathAdjustedRate,
getPathAdjustedSlippage,
getPathSize,
} from './fills';
import { createOrdersFromPath, createSignedOrdersWithFillableAmounts, getNativeOrderTokens } from './orders';
import { findOptimalPath } from './path_optimizer';
import { DexOrderSampler, getSampleAmounts } from './sampler'; import { DexOrderSampler, getSampleAmounts } from './sampler';
import { import {
AggregationError, AggregationError,
CollapsedFill,
DexSample, DexSample,
ERC20BridgeSource, ERC20BridgeSource,
Fill, Fill,
FillData,
FillFlags,
GetMarketOrdersOpts, GetMarketOrdersOpts,
NativeCollapsedFill,
NativeFillData,
OptimizedMarketOrder, OptimizedMarketOrder,
OrderDomain, OrderDomain,
} from './types'; } from './types';
export { DexOrderSampler } from './sampler';
const { ZERO_AMOUNT } = constants;
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 _wethAddress: string; private readonly _wethAddress: string;
constructor( constructor(
private readonly _sampler: DexOrderSampler, private readonly _sampler: DexOrderSampler,
contractAddresses: ContractAddresses, private readonly contractAddresses: ContractAddresses,
private readonly _orderDomain: OrderDomain, private readonly _orderDomain: OrderDomain,
private readonly _liquidityProviderRegistry: string = NULL_ADDRESS, private readonly _liquidityProviderRegistry: string = NULL_ADDRESS,
) { ) {
this._createOrderUtils = new CreateOrderUtils(contractAddresses); this._wethAddress = contractAddresses.etherToken.toLowerCase();
this._wethAddress = contractAddresses.etherToken;
} }
/** /**
@@ -68,34 +54,34 @@ export class MarketOperationUtils {
if (nativeOrders.length === 0) { if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders); throw new Error(AggregationError.EmptyOrders);
} }
const _opts = { const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
...DEFAULT_GET_MARKET_ORDERS_OPTS, const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
...opts, // Call the sampler contract.
};
const [makerToken, takerToken] = getOrderTokens(nativeOrders[0]);
const [ const [
fillableAmounts, orderFillableAmounts,
liquidityProviderAddress, liquidityProviderAddress,
ethToMakerAssetRate, ethToMakerAssetRate,
dexQuotes, dexQuotes,
] = await this._sampler.executeAsync( ] = await this._sampler.executeAsync(
// Get native order fillable amounts.
DexOrderSampler.ops.getOrderFillableTakerAmounts(nativeOrders), DexOrderSampler.ops.getOrderFillableTakerAmounts(nativeOrders),
// Get the custom liquidity provider from registry.
DexOrderSampler.ops.getLiquidityProviderFromRegistry( DexOrderSampler.ops.getLiquidityProviderFromRegistry(
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
takerToken,
makerToken, makerToken,
takerToken,
), ),
makerToken.toLowerCase() === this._wethAddress.toLowerCase() // Get ETH -> maker token price.
? DexOrderSampler.ops.constant(new BigNumber(1)) DexOrderSampler.ops.getMedianSellRate(
: DexOrderSampler.ops.getMedianSellRate( difference(FEE_QUOTE_SOURCES, _opts.excludedSources).concat(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources).concat( this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
this._liquidityProviderSourceIfAvailable(_opts.excludedSources), ),
), makerToken,
makerToken, this._wethAddress,
this._wethAddress, ONE_ETHER,
ONE_ETHER, this._liquidityProviderRegistry,
this._liquidityProviderRegistry, ),
), // Get sell quotes for taker -> maker.
DexOrderSampler.ops.getSellQuotes( DexOrderSampler.ops.getSellQuotes(
difference(SELL_SOURCES, _opts.excludedSources).concat( difference(SELL_SOURCES, _opts.excludedSources).concat(
this._liquidityProviderSourceIfAvailable(_opts.excludedSources), this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
@@ -106,50 +92,22 @@ export class MarketOperationUtils {
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
), ),
); );
return this._generateOptimizedOrders({
const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts( orderFillableAmounts,
nativeOrders, nativeOrders,
fillableAmounts, dexQuotes,
MarketOperation.Sell,
);
const nativeFills = pruneNativeFills(
sortFillsByAdjustedRate(
createSellPathFromNativeOrders(nativeOrdersWithFillableAmounts, ethToMakerAssetRate, _opts),
),
takerAmount,
_opts.dustFractionThreshold,
);
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, nativeFills);
allFills.splice(0, 0, ...nativeFills);
}
const optimizer = new FillsOptimizer(_opts.runLimit);
const upperBoundPath = pickBestUpperBoundPath(allPaths, takerAmount);
const optimalPath = optimizer.optimize(
// Sorting the orders by price effectively causes the optimizer to walk
// the greediest solution first, which is the optimal solution in most
// cases.
sortFillsByAdjustedRate(allFills),
takerAmount,
upperBoundPath,
);
if (!optimalPath) {
throw new Error(AggregationError.NoOptimalPath);
}
return this._createOrderUtils.createSellOrdersFromPath(
this._orderDomain,
takerToken,
makerToken,
collapsePath(optimalPath, false),
_opts.bridgeSlippage,
liquidityProviderAddress, liquidityProviderAddress,
); inputToken: takerToken,
outputToken: makerToken,
side: MarketOperation.Sell,
inputAmount: takerAmount,
ethToOutputRate: ethToMakerAssetRate,
bridgeSlippage: _opts.bridgeSlippage,
maxFallbackSlippage: _opts.maxFallbackSlippage,
excludedSources: _opts.excludedSources,
feeSchedule: _opts.feeSchedule,
allowFallback: _opts.allowFallback,
});
} }
/** /**
@@ -168,34 +126,34 @@ export class MarketOperationUtils {
if (nativeOrders.length === 0) { if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders); throw new Error(AggregationError.EmptyOrders);
} }
const _opts = { const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
...DEFAULT_GET_MARKET_ORDERS_OPTS, const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
...opts, // Call the sampler contract.
};
const [makerToken, takerToken] = getOrderTokens(nativeOrders[0]);
const [ const [
fillableAmounts, orderFillableAmounts,
liquidityProviderAddress, liquidityProviderAddress,
ethToTakerAssetRate, ethToTakerAssetRate,
dexQuotes, dexQuotes,
] = await this._sampler.executeAsync( ] = await this._sampler.executeAsync(
// Get native order fillable amounts.
DexOrderSampler.ops.getOrderFillableMakerAmounts(nativeOrders), DexOrderSampler.ops.getOrderFillableMakerAmounts(nativeOrders),
// Get the custom liquidity provider from registry.
DexOrderSampler.ops.getLiquidityProviderFromRegistry( DexOrderSampler.ops.getLiquidityProviderFromRegistry(
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
takerToken,
makerToken, makerToken,
takerToken,
), ),
takerToken.toLowerCase() === this._wethAddress.toLowerCase() // Get ETH -> taker token price.
? DexOrderSampler.ops.constant(new BigNumber(1)) DexOrderSampler.ops.getMedianSellRate(
: DexOrderSampler.ops.getMedianSellRate( difference(FEE_QUOTE_SOURCES, _opts.excludedSources).concat(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources).concat( this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
this._liquidityProviderSourceIfAvailable(_opts.excludedSources), ),
), takerToken,
takerToken, this._wethAddress,
this._wethAddress, ONE_ETHER,
ONE_ETHER, this._liquidityProviderRegistry,
this._liquidityProviderRegistry, ),
), // Get buy quotes for taker -> maker.
DexOrderSampler.ops.getBuyQuotes( DexOrderSampler.ops.getBuyQuotes(
difference(BUY_SOURCES, _opts.excludedSources).concat( difference(BUY_SOURCES, _opts.excludedSources).concat(
this._liquidityProviderSourceIfAvailable(_opts.excludedSources), this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
@@ -206,19 +164,23 @@ export class MarketOperationUtils {
this._liquidityProviderRegistry, this._liquidityProviderRegistry,
), ),
); );
const signedOrderWithFillableAmounts = this._createBuyOrdersPathFromSamplerResultIfExists(
return this._generateOptimizedOrders({
orderFillableAmounts,
nativeOrders, nativeOrders,
makerAmount,
fillableAmounts,
dexQuotes, dexQuotes,
ethToTakerAssetRate,
_opts,
liquidityProviderAddress, liquidityProviderAddress,
); inputToken: makerToken,
if (!signedOrderWithFillableAmounts) { outputToken: takerToken,
throw new Error(AggregationError.NoOptimalPath); side: MarketOperation.Buy,
} inputAmount: makerAmount,
return signedOrderWithFillableAmounts; ethToOutputRate: ethToTakerAssetRate,
bridgeSlippage: _opts.bridgeSlippage,
maxFallbackSlippage: _opts.maxFallbackSlippage,
excludedSources: _opts.excludedSources,
feeSchedule: _opts.feeSchedule,
allowFallback: _opts.allowFallback,
});
} }
/** /**
@@ -240,10 +202,7 @@ export class MarketOperationUtils {
if (batchNativeOrders.length === 0) { if (batchNativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders); throw new Error(AggregationError.EmptyOrders);
} }
const _opts = { const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
...DEFAULT_GET_MARKET_ORDERS_OPTS,
...opts,
};
const sources = difference(BUY_SOURCES, _opts.excludedSources); const sources = difference(BUY_SOURCES, _opts.excludedSources);
const ops = [ const ops = [
@@ -251,32 +210,118 @@ export class MarketOperationUtils {
...batchNativeOrders.map(orders => ...batchNativeOrders.map(orders =>
DexOrderSampler.ops.getMedianSellRate( DexOrderSampler.ops.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources), difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
getNativeOrderTokens(orders[0])[1],
this._wethAddress, this._wethAddress,
getOrderTokens(orders[0])[1],
ONE_ETHER, 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(
makerAmounts[i], sources,
]), getNativeOrderTokens(orders[0])[0],
getNativeOrderTokens(orders[0])[1],
[makerAmounts[i]],
),
), ),
]; ];
const executeResults = await this._sampler.executeBatchAsync(ops); const executeResults = await this._sampler.executeBatchAsync(ops);
const batchFillableAmounts = executeResults.splice(0, batchNativeOrders.length) as BigNumber[][]; const batchOrderFillableAmounts = executeResults.splice(0, batchNativeOrders.length) as BigNumber[][];
const batchEthToTakerAssetRate = 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[][][]; const batchDexQuotes = executeResults.splice(0, batchNativeOrders.length) as DexSample[][][];
return batchFillableAmounts.map((fillableAmounts, i) => return batchNativeOrders.map((nativeOrders, i) => {
this._createBuyOrdersPathFromSamplerResultIfExists( if (nativeOrders.length === 0) {
batchNativeOrders[i], throw new Error(AggregationError.EmptyOrders);
makerAmounts[i], }
fillableAmounts, const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
batchDexQuotes[i], const orderFillableAmounts = batchOrderFillableAmounts[i];
batchEthToTakerAssetRate[i], const ethToTakerAssetRate = batchEthToTakerAssetRate[i];
_opts, const dexQuotes = batchDexQuotes[i];
), const makerAmount = makerAmounts[i];
); return this._generateOptimizedOrders({
orderFillableAmounts,
nativeOrders,
dexQuotes,
inputToken: makerToken,
outputToken: takerToken,
side: MarketOperation.Buy,
inputAmount: makerAmount,
ethToOutputRate: ethToTakerAssetRate,
bridgeSlippage: _opts.bridgeSlippage,
maxFallbackSlippage: _opts.maxFallbackSlippage,
excludedSources: _opts.excludedSources,
feeSchedule: _opts.feeSchedule,
allowFallback: _opts.allowFallback,
});
});
}
private _generateOptimizedOrders(opts: {
side: MarketOperation;
inputToken: string;
outputToken: string;
inputAmount: BigNumber;
nativeOrders: SignedOrder[];
orderFillableAmounts: BigNumber[];
dexQuotes: DexSample[][];
runLimit?: number;
ethToOutputRate?: BigNumber;
bridgeSlippage?: number;
maxFallbackSlippage?: number;
excludedSources?: ERC20BridgeSource[];
feeSchedule?: { [source: string]: BigNumber };
allowFallback?: boolean;
liquidityProviderAddress?: string;
}): OptimizedMarketOrder[] {
const { inputToken, outputToken, side, inputAmount } = opts;
const maxFallbackSlippage = opts.maxFallbackSlippage || 0;
// Convert native orders and dex quotes into fill paths.
const paths = createFillPaths({
side,
// Augment native orders with their fillable amounts.
orders: createSignedOrdersWithFillableAmounts(side, opts.nativeOrders, opts.orderFillableAmounts),
dexQuotes: opts.dexQuotes,
targetInput: inputAmount,
ethToOutputRate: opts.ethToOutputRate,
excludedSources: opts.excludedSources,
feeSchedule: opts.feeSchedule,
});
// Find the optimal path.
const optimalPath = findOptimalPath(side, paths, inputAmount, opts.runLimit) || [];
// TODO(dorothy-zbornak): Ensure the slippage on the optimal path is <= maxFallbackSlippage
// once we decide on a good baseline.
if (optimalPath.length === 0) {
throw new Error(AggregationError.NoOptimalPath);
}
// Generate a fallback path if native orders are in the optimal paath.
let fallbackPath: Fill[] = [];
const nativeSubPath = optimalPath.filter(f => f.source === ERC20BridgeSource.Native);
if (opts.allowFallback && nativeSubPath.length !== 0) {
// The fallback path is, at most, as large as the native path.
const fallbackInputAmount = BigNumber.min(inputAmount, getPathSize(nativeSubPath, inputAmount)[0]);
fallbackPath =
findOptimalPath(side, getFallbackSourcePaths(optimalPath, paths), fallbackInputAmount, opts.runLimit) ||
[];
const fallbackSlippage = getPathAdjustedSlippage(
side,
fallbackPath,
fallbackInputAmount,
getPathAdjustedRate(side, optimalPath, inputAmount),
);
if (fallbackSlippage > maxFallbackSlippage) {
fallbackPath = [];
}
}
return createOrdersFromPath([...optimalPath, ...fallbackPath], {
side,
inputToken,
outputToken,
orderDomain: this._orderDomain,
contractAddresses: this.contractAddresses,
bridgeSlippage: opts.bridgeSlippage || 0,
liquidityProviderAddress: opts.liquidityProviderAddress,
});
} }
private _liquidityProviderSourceIfAvailable(excludedSources: ERC20BridgeSource[]): ERC20BridgeSource[] { private _liquidityProviderSourceIfAvailable(excludedSources: ERC20BridgeSource[]): ERC20BridgeSource[] {
@@ -285,328 +330,6 @@ export class MarketOperationUtils {
? [ERC20BridgeSource.LiquidityProvider] ? [ERC20BridgeSource.LiquidityProvider]
: []; : [];
} }
private _createBuyOrdersPathFromSamplerResultIfExists(
nativeOrders: SignedOrder[],
makerAmount: BigNumber,
nativeOrderFillableAmounts: BigNumber[],
dexQuotes: DexSample[][],
ethToTakerAssetRate: BigNumber,
opts: GetMarketOrdersOpts,
liquidityProviderAddress?: string,
): OptimizedMarketOrder[] | undefined {
const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts(
nativeOrders,
nativeOrderFillableAmounts,
MarketOperation.Buy,
);
const nativeFills = pruneNativeFills(
sortFillsByAdjustedRate(
createBuyPathFromNativeOrders(nativeOrdersWithFillableAmounts, ethToTakerAssetRate, opts),
true,
),
makerAmount,
opts.dustFractionThreshold,
);
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, nativeFills);
allFills.splice(0, 0, ...nativeFills);
}
const optimizer = new FillsOptimizer(opts.runLimit, true);
const upperBoundPath = pickBestUpperBoundPath(allPaths, makerAmount, true);
const optimalPath = optimizer.optimize(
// Sorting the orders by price effectively causes the optimizer to walk
// the greediest solution first, which is the optimal solution in most
// cases.
sortFillsByAdjustedRate(allFills, true),
makerAmount,
upperBoundPath,
);
if (!optimalPath) {
return undefined;
}
const [inputToken, outputToken] = getOrderTokens(nativeOrders[0]);
return this._createOrderUtils.createBuyOrdersFromPath(
this._orderDomain,
inputToken,
outputToken,
collapsePath(optimalPath, true),
opts.bridgeSlippage,
liquidityProviderAddress,
);
}
}
function createSignedOrdersWithFillableAmounts(
signedOrders: SignedOrder[],
fillableAmounts: BigNumber[],
operation: MarketOperation,
): SignedOrderWithFillableAmounts[] {
return signedOrders
.map((order: SignedOrder, i: number) => {
const fillableAmount = fillableAmounts[i];
const fillableMakerAssetAmount =
operation === MarketOperation.Buy
? fillableAmount
: orderCalculationUtils.getMakerFillAmount(order, fillableAmount);
const fillableTakerAssetAmount =
operation === MarketOperation.Sell
? fillableAmount
: orderCalculationUtils.getTakerFillAmount(order, fillableAmount);
const fillableTakerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, fillableTakerAssetAmount);
return {
fillableMakerAssetAmount,
fillableTakerAssetAmount,
fillableTakerFeeAmount,
...order,
};
})
.filter(order => {
return !order.fillableMakerAssetAmount.isZero() && !order.fillableTakerAssetAmount.isZero();
});
}
// Gets the difference between two sets.
function difference<T>(a: T[], b: T[]): T[] {
return a.filter(x => b.indexOf(x) === -1);
}
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.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,
},
});
}
return path;
}
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.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,
},
});
}
return path;
}
function pruneNativeFills(fills: Fill[], fillAmount: BigNumber, dustFractionThreshold: number): Fill[] {
const minInput = fillAmount.times(dustFractionThreshold);
const pruned = [];
let totalInput = ZERO_AMOUNT;
for (const fill of fills) {
if (totalInput.gte(fillAmount)) {
break;
}
if (fill.input.lt(minInput)) {
continue;
}
totalInput = totalInput.plus(fill.input);
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) {
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];
// 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({
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];
}
// 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);
}
}
return paths;
}
function sourceToFillFlags(source: ERC20BridgeSource): number {
if (source === ERC20BridgeSource.Kyber) {
return FillFlags.SourceKyber;
}
if (source === ERC20BridgeSource.Eth2Dai) {
return FillFlags.SourceEth2Dai;
}
if (source === ERC20BridgeSource.Uniswap) {
return FillFlags.SourceUniswap;
}
if (source === ERC20BridgeSource.LiquidityProvider) {
return FillFlags.SourceLiquidityPool;
}
return FillFlags.SourceNative;
}
function sourceToExclusionMask(source: ERC20BridgeSource): number {
if (source === ERC20BridgeSource.Kyber) {
// tslint:disable-next-line: no-bitwise
return FillFlags.SourceEth2Dai | FillFlags.SourceUniswap;
}
if (source === ERC20BridgeSource.Eth2Dai) {
return FillFlags.SourceKyber;
}
if (source === ERC20BridgeSource.Uniswap) {
return FillFlags.SourceKyber;
}
return 0;
}
// Convert a list of DEX paths to a flattened list of `Fills`.
function flattenDexPaths(dexFills: Fill[][]): Fill[] {
const fills: Fill[] = [];
for (const quote of dexFills) {
for (const fill of quote) {
fills.push(fill);
}
}
return fills;
}
// Picks the path with the highest (or lowest if `shouldMinimize` is true) output.
function pickBestUpperBoundPath(paths: Fill[][], maxInput: BigNumber, shouldMinimize?: boolean): Fill[] | undefined {
let optimalPath: Fill[] | undefined;
let optimalPathOutput: BigNumber = ZERO_AMOUNT;
for (const path of paths) {
if (getPathInput(path).gte(maxInput)) {
const output = getPathAdjustedOutput(path, maxInput);
if (!optimalPath || comparePathOutputs(output, optimalPathOutput, !!shouldMinimize) === 1) {
optimalPath = path;
optimalPathOutput = output;
}
}
}
return optimalPath;
}
// Gets the total input of a path.
function getPathInput(path: Fill[]): BigNumber {
return BigNumber.sum(...path.map(p => p.input));
}
// Merges contiguous fills from the same DEX.
function collapsePath(path: Fill[], isBuy: boolean): CollapsedFill[] {
const collapsed: Array<CollapsedFill | NativeCollapsedFill> = [];
for (const fill of path) {
const makerAssetAmount = isBuy ? fill.input : fill.output;
const takerAssetAmount = isBuy ? fill.output : fill.input;
const source = (fill.fillData as FillData).source;
if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) {
const prevFill = collapsed[collapsed.length - 1];
// If the last fill is from the same source, merge them.
if (prevFill.source === source) {
prevFill.totalMakerAssetAmount = prevFill.totalMakerAssetAmount.plus(makerAssetAmount);
prevFill.totalTakerAssetAmount = prevFill.totalTakerAssetAmount.plus(takerAssetAmount);
prevFill.subFills.push({ makerAssetAmount, takerAssetAmount });
continue;
}
}
collapsed.push({
source: fill.fillData.source,
totalMakerAssetAmount: makerAssetAmount,
totalTakerAssetAmount: takerAssetAmount,
subFills: [{ makerAssetAmount, takerAssetAmount }],
nativeOrder: (fill.fillData as NativeFillData).order,
});
}
return collapsed;
}
function getOrderTokens(order: SignedOrder): [string, string] {
const assets = [order.makerAssetData, order.takerAssetData].map(a => assetDataUtils.decodeAssetDataOrThrow(a)) as [
ERC20AssetData,
ERC20AssetData
];
if (assets.some(a => a.assetProxyId !== ERC20_PROXY_ID)) {
throw new Error(AggregationError.NotERC20AssetData);
}
return assets.map(a => a.tokenAddress) as [string, string];
} }
// tslint:disable: max-file-line-count // tslint:disable: max-file-line-count

View File

@@ -0,0 +1,258 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { AbiEncoder, BigNumber } from '@0x/utils';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import {
DEFAULT_CURVE_OPTS,
ERC20_PROXY_ID,
NULL_ADDRESS,
NULL_BYTES,
ONE_HOUR_IN_SECONDS,
ONE_SECOND_MS,
WALLET_SIGNATURE,
ZERO_AMOUNT,
} from './constants';
import { collapsePath } from './fills';
import {
AggregationError,
CollapsedFill,
ERC20BridgeSource,
Fill,
NativeCollapsedFill,
OptimizedMarketOrder,
OrderDomain,
} from './types';
// tslint:disable completed-docs
export function createDummyOrderForSampler(
makerAssetData: string,
takerAssetData: string,
makerAddress: string,
): SignedOrder {
return {
makerAddress,
takerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
feeRecipientAddress: NULL_ADDRESS,
salt: ZERO_AMOUNT,
expirationTimeSeconds: ZERO_AMOUNT,
makerAssetData,
takerAssetData,
makerFeeAssetData: NULL_BYTES,
takerFeeAssetData: NULL_BYTES,
makerFee: ZERO_AMOUNT,
takerFee: ZERO_AMOUNT,
makerAssetAmount: ZERO_AMOUNT,
takerAssetAmount: ZERO_AMOUNT,
signature: NULL_BYTES,
chainId: 1,
exchangeAddress: NULL_ADDRESS,
};
}
export function getNativeOrderTokens(order: SignedOrder): [string, string] {
const assets = [order.makerAssetData, order.takerAssetData].map(a => assetDataUtils.decodeAssetDataOrThrow(a)) as [
ERC20AssetData,
ERC20AssetData
];
if (assets.some(a => a.assetProxyId !== ERC20_PROXY_ID)) {
throw new Error(AggregationError.NotERC20AssetData);
}
return assets.map(a => a.tokenAddress.toLowerCase()) as [string, string];
}
export function convertNativeOrderToFullyFillableOptimizedOrders(order: SignedOrder): OptimizedMarketOrder {
return {
...order,
fillableMakerAssetAmount: order.makerAssetAmount,
fillableTakerAssetAmount: order.takerAssetAmount,
fillableTakerFeeAmount: order.takerFee,
fill: {
source: ERC20BridgeSource.Native,
totalMakerAssetAmount: order.makerAssetAmount,
totalTakerAssetAmount: order.takerAssetAmount,
subFills: [],
},
};
}
/**
* Augments native orders with fillable amounts and filters out unfillable orders.
*/
export function createSignedOrdersWithFillableAmounts(
side: MarketOperation,
orders: SignedOrder[],
fillableAmounts: BigNumber[],
): SignedOrderWithFillableAmounts[] {
return orders
.map((order: SignedOrder, i: number) => {
const fillableAmount = fillableAmounts[i];
const fillableMakerAssetAmount =
side === MarketOperation.Buy
? fillableAmount
: orderCalculationUtils.getMakerFillAmount(order, fillableAmount);
const fillableTakerAssetAmount =
side === MarketOperation.Sell
? fillableAmount
: orderCalculationUtils.getTakerFillAmount(order, fillableAmount);
const fillableTakerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, fillableTakerAssetAmount);
return {
...order,
fillableMakerAssetAmount,
fillableTakerAssetAmount,
fillableTakerFeeAmount,
};
})
.filter(order => {
return !order.fillableMakerAssetAmount.isZero() && !order.fillableTakerAssetAmount.isZero();
});
}
export interface CreateOrderFromPathOpts {
side: MarketOperation;
inputToken: string;
outputToken: string;
orderDomain: OrderDomain;
contractAddresses: ContractAddresses;
bridgeSlippage: number;
liquidityProviderAddress?: string;
}
// Convert sell fills into orders.
export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] {
const collapsedPath = collapsePath(opts.side, path);
const orders: OptimizedMarketOrder[] = [];
for (const fill of collapsedPath) {
if (fill.source === ERC20BridgeSource.Native) {
orders.push(createNativeOrder(fill));
} else {
orders.push(createBridgeOrder(fill, opts));
}
}
return orders;
}
function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrderFromPathOpts): string {
switch (source) {
case ERC20BridgeSource.Eth2Dai:
return opts.contractAddresses.eth2DaiBridge;
case ERC20BridgeSource.Kyber:
return opts.contractAddresses.kyberBridge;
case ERC20BridgeSource.Uniswap:
return opts.contractAddresses.uniswapBridge;
case ERC20BridgeSource.CurveUsdcDai:
case ERC20BridgeSource.CurveUsdcDaiUsdt:
case ERC20BridgeSource.CurveUsdcDaiUsdtTusd:
case ERC20BridgeSource.CurveUsdcDaiUsdtBusd:
return opts.contractAddresses.curveBridge;
case ERC20BridgeSource.LiquidityProvider:
if (opts.liquidityProviderAddress === undefined) {
throw new Error('Cannot create a LiquidityProvider order without a LiquidityProvider pool address.');
}
return opts.liquidityProviderAddress;
default:
break;
}
throw new Error(AggregationError.NoBridgeForSource);
}
function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): OptimizedMarketOrder {
const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken;
const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken;
const bridgeAddress = getBridgeAddressFromSource(fill.source, opts);
let makerAssetData;
if (Object.keys(DEFAULT_CURVE_OPTS).includes(fill.source)) {
const { curveAddress, tokens, version } = DEFAULT_CURVE_OPTS[fill.source];
const fromTokenIdx = tokens.indexOf(takerToken);
const toTokenIdx = tokens.indexOf(makerToken);
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createCurveBridgeData(curveAddress, fromTokenIdx, toTokenIdx, version),
);
} else {
makerAssetData = assetDataUtils.encodeERC20BridgeAssetData(
makerToken,
bridgeAddress,
createBridgeData(takerToken),
);
}
return {
makerAddress: bridgeAddress,
makerAssetData,
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken),
...createCommonBridgeOrderFields(fill, opts),
};
}
function createBridgeData(tokenAddress: string): string {
const encoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]);
return encoder.encode({ tokenAddress });
}
function createCurveBridgeData(
curveAddress: string,
fromTokenIdx: number,
toTokenIdx: number,
version: number,
): string {
const curveBridgeDataEncoder = AbiEncoder.create([
{ name: 'curveAddress', type: 'address' },
{ name: 'fromTokenIdx', type: 'int128' },
{ name: 'toTokenIdx', type: 'int128' },
{ name: 'version', type: 'int128' },
]);
return curveBridgeDataEncoder.encode([curveAddress, fromTokenIdx, toTokenIdx, version]);
}
type CommonBridgeOrderFields = Pick<
OptimizedMarketOrder,
Exclude<keyof OptimizedMarketOrder, 'makerAddress' | 'makerAssetData' | 'takerAssetData'>
>;
function createCommonBridgeOrderFields(fill: CollapsedFill, opts: CreateOrderFromPathOpts): CommonBridgeOrderFields {
const makerAssetAmountAdjustedWithSlippage =
opts.side === MarketOperation.Sell
? fill.totalMakerAssetAmount.times(1 - opts.bridgeSlippage).integerValue(BigNumber.ROUND_DOWN)
: fill.totalMakerAssetAmount;
const takerAssetAmountAdjustedWithSlippage =
opts.side === MarketOperation.Sell
? fill.totalTakerAssetAmount
: fill.totalTakerAssetAmount.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP);
return {
fill,
takerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
feeRecipientAddress: NULL_ADDRESS,
salt: generatePseudoRandomSalt(),
expirationTimeSeconds: new BigNumber(Math.floor(Date.now() / ONE_SECOND_MS) + ONE_HOUR_IN_SECONDS),
makerFeeAssetData: NULL_BYTES,
takerFeeAssetData: NULL_BYTES,
makerFee: ZERO_AMOUNT,
takerFee: ZERO_AMOUNT,
makerAssetAmount: makerAssetAmountAdjustedWithSlippage,
fillableMakerAssetAmount: makerAssetAmountAdjustedWithSlippage,
takerAssetAmount: takerAssetAmountAdjustedWithSlippage,
fillableTakerAssetAmount: takerAssetAmountAdjustedWithSlippage,
fillableTakerFeeAmount: ZERO_AMOUNT,
signature: WALLET_SIGNATURE,
...opts.orderDomain,
};
}
function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder {
return {
fill: {
source: fill.source,
totalMakerAssetAmount: fill.totalMakerAssetAmount,
totalTakerAssetAmount: fill.totalTakerAssetAmount,
subFills: fill.subFills,
},
...(fill as NativeCollapsedFill).nativeOrder,
};
}

View File

@@ -0,0 +1,88 @@
import { BigNumber } from '@0x/utils';
import { MarketOperation } from '../../types';
import { ZERO_AMOUNT } from './constants';
import { getPathSize, isValidPath } from './fills';
import { Fill } from './types';
// tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs
/**
* Find the optimal mixture of paths that maximizes (for sells) or minimizes
* (for buys) output, while meeting the input requirement.
*/
export function findOptimalPath(
side: MarketOperation,
paths: Fill[][],
targetInput: BigNumber,
runLimit?: number,
): Fill[] | undefined {
let optimalPath = paths[0] || [];
// TODO(dorothy-zbornak): Convex paths (like kyber) should technically always be
// inserted at the front of the path because a partial fill can invalidate them.
for (const path of paths.slice(1)) {
optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit);
}
return isPathComplete(optimalPath, targetInput) ? optimalPath : undefined;
}
function mixPaths(
side: MarketOperation,
pathA: Fill[],
pathB: Fill[],
targetInput: BigNumber,
maxSteps: number = 2 ** 15,
): Fill[] {
const allFills = [...pathA, ...pathB].sort((a, b) => b.rate.comparedTo(a.rate));
let bestPath: Fill[] = [];
let bestPathInput = ZERO_AMOUNT;
let bestPathRate = ZERO_AMOUNT;
let steps = 0;
const _isBetterPath = (input: BigNumber, rate: BigNumber) => {
if (bestPathInput.lt(targetInput)) {
return input.gt(bestPathInput);
} else if (input.gte(bestPathInput)) {
return rate.gt(bestPathRate);
}
return false;
};
const _walk = (path: Fill[], input: BigNumber, output: BigNumber) => {
steps += 1;
const rate = getRate(side, input, output);
if (_isBetterPath(input, rate)) {
bestPath = path;
bestPathInput = input;
bestPathRate = rate;
}
if (input.lt(targetInput)) {
for (const fill of allFills) {
if (steps >= maxSteps) {
break;
}
const childPath = [...path, fill];
if (!isValidPath(childPath)) {
continue;
}
_walk(childPath, input.plus(fill.input), output.plus(fill.adjustedOutput));
}
}
};
_walk(bestPath, ZERO_AMOUNT, ZERO_AMOUNT);
return bestPath;
}
function isPathComplete(path: Fill[], targetInput: BigNumber): boolean {
const [input] = getPathSize(path);
return input.gte(targetInput);
}
function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber {
if (input.eq(0) || output.eq(0)) {
return ZERO_AMOUNT;
}
if (side === MarketOperation.Sell) {
return output.div(input);
}
return input.div(output);
}

View File

@@ -1,6 +1,6 @@
import { BigNumber, ERC20BridgeSource, SignedOrder } from '../..'; import { BigNumber, ERC20BridgeSource, SignedOrder } from '../..';
import { constants } from '../../constants';
import { DEFAULT_CURVE_OPTS } from './constants';
import { BatchedOperation, DexSample } from './types'; import { BatchedOperation, DexSample } from './types';
/** /**
@@ -191,6 +191,9 @@ export const samplerOperations = {
takerFillAmount: BigNumber, takerFillAmount: BigNumber,
liquidityProviderRegistryAddress?: string | undefined, liquidityProviderRegistryAddress?: string | undefined,
): BatchedOperation<BigNumber> { ): BatchedOperation<BigNumber> {
if (makerToken.toLowerCase() === takerToken.toLowerCase()) {
return samplerOperations.constant(new BigNumber(1));
}
const getSellQuotes = samplerOperations.getSellQuotes( const getSellQuotes = samplerOperations.getSellQuotes(
sources, sources,
makerToken, makerToken,
@@ -211,6 +214,7 @@ export const samplerOperations = {
} }
const flatSortedSamples = samples const flatSortedSamples = samples
.reduce((acc, v) => acc.concat(...v)) .reduce((acc, v) => acc.concat(...v))
.filter(v => !v.output.isZero())
.sort((a, b) => a.output.comparedTo(b.output)); .sort((a, b) => a.output.comparedTo(b.output));
if (flatSortedSamples.length === 0) { if (flatSortedSamples.length === 0) {
return new BigNumber(0); return new BigNumber(0);
@@ -232,8 +236,8 @@ export const samplerOperations = {
}, },
getLiquidityProviderFromRegistry( getLiquidityProviderFromRegistry(
registryAddress: string, registryAddress: string,
takerToken: string,
makerToken: string, makerToken: string,
takerToken: string,
): BatchedOperation<string> { ): BatchedOperation<string> {
return { return {
encodeCall: contract => { encodeCall: contract => {
@@ -262,8 +266,8 @@ export const samplerOperations = {
batchedOperation = samplerOperations.getUniswapSellQuotes(makerToken, takerToken, takerFillAmounts); batchedOperation = samplerOperations.getUniswapSellQuotes(makerToken, takerToken, takerFillAmounts);
} else if (source === ERC20BridgeSource.Kyber) { } else if (source === ERC20BridgeSource.Kyber) {
batchedOperation = samplerOperations.getKyberSellQuotes(makerToken, takerToken, takerFillAmounts); batchedOperation = samplerOperations.getKyberSellQuotes(makerToken, takerToken, takerFillAmounts);
} else if (Object.keys(constants.DEFAULT_CURVE_OPTS).includes(source)) { } else if (Object.keys(DEFAULT_CURVE_OPTS).includes(source)) {
const { curveAddress, tokens } = constants.DEFAULT_CURVE_OPTS[source]; const { curveAddress, tokens } = DEFAULT_CURVE_OPTS[source];
const fromTokenIdx = tokens.indexOf(takerToken); const fromTokenIdx = tokens.indexOf(takerToken);
const toTokenIdx = tokens.indexOf(makerToken); const toTokenIdx = tokens.indexOf(makerToken);
if (fromTokenIdx !== -1 && toTokenIdx !== -1) { if (fromTokenIdx !== -1 && toTokenIdx !== -1) {

View File

@@ -37,9 +37,7 @@ export enum ERC20BridgeSource {
} }
// Internal `fillData` field for `Fill` objects. // Internal `fillData` field for `Fill` objects.
export interface FillData { export interface FillData {}
source: ERC20BridgeSource;
}
// `FillData` for native fills. // `FillData` for native fills.
export interface NativeFillData extends FillData { export interface NativeFillData extends FillData {
@@ -59,11 +57,8 @@ export interface DexSample {
* Flags for `Fill` objects. * Flags for `Fill` objects.
*/ */
export enum FillFlags { export enum FillFlags {
SourceNative = 0x1, ConflictsWithKyber = 0x1,
SourceUniswap = 0x2, Kyber = 0x2,
SourceEth2Dai = 0x4,
SourceKyber = 0x8,
SourceLiquidityPool = 0x10,
} }
/** /**
@@ -72,20 +67,25 @@ export enum FillFlags {
export interface Fill { export interface Fill {
// See `FillFlags`. // See `FillFlags`.
flags: FillFlags; flags: FillFlags;
// `FillFlags` that are incompatible with this fill, e.g., to prevent
// Kyber from mixing with Uniswap and Eth2Dai and vice versa.
exclusionMask: number;
// Input fill amount (taker asset amount in a sell, maker asset amount in a buy). // Input fill amount (taker asset amount in a sell, maker asset amount in a buy).
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. // The maker/taker rate.
fillPenalty: BigNumber; rate: BigNumber;
// The maker/taker rate, adjusted by fees.
adjustedRate: BigNumber;
// The output fill amount, ajdusted by fees.
adjustedOutput: 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;
// The index of the fill in the original path.
index: number;
// The source of the fill. See `ERC20BridgeSource`.
source: ERC20BridgeSource;
// Data associated with this this Fill object. Used to reconstruct orders // Data associated with this this Fill object. Used to reconstruct orders
// from paths. // from paths.
fillData: FillData | NativeFillData; fillData?: FillData | NativeFillData;
} }
/** /**
@@ -138,10 +138,6 @@ export interface GetMarketOrdersOpts {
* Liquidity sources to exclude. Default is none. * Liquidity sources to exclude. Default is none.
*/ */
excludedSources: ERC20BridgeSource[]; excludedSources: ERC20BridgeSource[];
/**
* Whether to prevent mixing Kyber orders with Uniswap and Eth2Dai orders.
*/
noConflicts: boolean;
/** /**
* Complexity limit on the search algorithm, i.e., maximum number of * Complexity limit on the search algorithm, i.e., maximum number of
* nodes to visit. Default is 1024. * nodes to visit. Default is 1024.
@@ -156,15 +152,16 @@ export interface GetMarketOrdersOpts {
* Default is 0.0005 (5 basis points). * Default is 0.0005 (5 basis points).
*/ */
bridgeSlippage: number; bridgeSlippage: number;
/**
* The maximum price slippage allowed in the fallback quote. If the slippage
* between the optimal quote and the fallback quote is greater than this
* percentage, no fallback quote will be provided.
*/
maxFallbackSlippage: number;
/** /**
* Number of samples to take for each DEX quote. * Number of samples to take for each DEX quote.
*/ */
numSamples: number; numSamples: number;
/**
* Dust amount, as a fraction of the fill amount.
* Default is 0.01 (100 basis points).
*/
dustFractionThreshold: number;
/** /**
* The exponential sampling distribution base. * The exponential sampling distribution base.
* A value of 1 will result in evenly spaced samples. * A value of 1 will result in evenly spaced samples.
@@ -176,7 +173,16 @@ export interface GetMarketOrdersOpts {
/** /**
* Fees for each liquidity source, expressed in gas. * Fees for each liquidity source, expressed in gas.
*/ */
fees: { [source: string]: BigNumber }; feeSchedule: { [source: string]: BigNumber };
/**
* Estimated gas consumed by each liquidity source.
*/
gasSchedule: { [source: string]: number };
/**
* Whether to pad the quote with a redundant fallback quote using different
* sources.
*/
allowFallback: boolean;
} }
/** /**

View File

@@ -4,7 +4,7 @@ import * as _ from 'lodash';
import { constants } from '../constants'; import { constants } from '../constants';
import { OrderPrunerPermittedFeeTypes } from '../types'; import { OrderPrunerPermittedFeeTypes } from '../types';
import { utils } from '../utils/utils'; import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from '../utils/utils';
export const orderPrunerUtils = { export const orderPrunerUtils = {
pruneForUsableSignedOrders( pruneForUsableSignedOrders(
@@ -19,9 +19,9 @@ export const orderPrunerUtils = {
((permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.NoFees) && ((permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.NoFees) &&
order.takerFee.eq(constants.ZERO_AMOUNT)) || order.takerFee.eq(constants.ZERO_AMOUNT)) ||
(permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee) && (permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee) &&
utils.isOrderTakerFeePayableWithTakerAsset(order)) || isOrderTakerFeePayableWithTakerAsset(order)) ||
(permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee) && (permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee) &&
utils.isOrderTakerFeePayableWithMakerAsset(order))) isOrderTakerFeePayableWithMakerAsset(order)))
); );
}); });
return result; return result;

View File

@@ -4,7 +4,7 @@ import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { assert } from './assert'; import { assert } from './assert';
import { utils } from './utils'; import { getAdjustedMakerAndTakerAmountsFromTakerFees } from './utils';
export const sortingUtils = { export const sortingUtils = {
sortOrders<T extends Order>(orders: T[]): T[] { sortOrders<T extends Order>(orders: T[]): T[] {
@@ -21,9 +21,7 @@ export const sortingUtils = {
}; };
function getTakerFeeAdjustedRateOfOrder(order: Order): BigNumber { function getTakerFeeAdjustedRateOfOrder(order: Order): BigNumber {
const [adjustedMakerAssetAmount, adjustedTakerAssetAmount] = utils.getAdjustedMakerAndTakerAmountsFromTakerFees( const [adjustedMakerAssetAmount, adjustedTakerAssetAmount] = getAdjustedMakerAndTakerAmountsFromTakerFees(order);
order,
);
const rate = adjustedTakerAssetAmount.div(adjustedMakerAssetAmount); const rate = adjustedTakerAssetAmount.div(adjustedMakerAssetAmount);
return rate; return rate;
} }

View File

@@ -19,10 +19,14 @@ import {
import { fillableAmountsUtils } from './fillable_amounts_utils'; import { fillableAmountsUtils } from './fillable_amounts_utils';
import { MarketOperationUtils } from './market_operation_utils'; import { MarketOperationUtils } from './market_operation_utils';
import { CreateOrderUtils } from './market_operation_utils/create_order'; import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
import { ERC20BridgeSource, OptimizedMarketOrder } from './market_operation_utils/types'; import { ERC20BridgeSource, OptimizedMarketOrder } from './market_operation_utils/types';
import { ProtocolFeeUtils } from './protocol_fee_utils'; import { ProtocolFeeUtils } from './protocol_fee_utils';
import { utils } from './utils'; import {
isOrderTakerFeePayableWithMakerAsset,
isOrderTakerFeePayableWithTakerAsset,
isSupportedAssetDataInOrders,
} from './utils';
// TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError? // TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError?
export class SwapQuoteCalculator { export class SwapQuoteCalculator {
@@ -37,14 +41,12 @@ export class SwapQuoteCalculator {
public async calculateMarketSellSwapQuoteAsync( public async calculateMarketSellSwapQuoteAsync(
prunedOrders: SignedOrder[], prunedOrders: SignedOrder[],
takerAssetFillAmount: BigNumber, takerAssetFillAmount: BigNumber,
slippagePercentage: number,
gasPrice: BigNumber, gasPrice: BigNumber,
opts: CalculateSwapQuoteOpts, opts: CalculateSwapQuoteOpts,
): Promise<MarketSellSwapQuote> { ): Promise<MarketSellSwapQuote> {
return (await this._calculateSwapQuoteAsync( return (await this._calculateSwapQuoteAsync(
prunedOrders, prunedOrders,
takerAssetFillAmount, takerAssetFillAmount,
slippagePercentage,
gasPrice, gasPrice,
MarketOperation.Sell, MarketOperation.Sell,
opts, opts,
@@ -54,14 +56,12 @@ export class SwapQuoteCalculator {
public async calculateMarketBuySwapQuoteAsync( public async calculateMarketBuySwapQuoteAsync(
prunedOrders: SignedOrder[], prunedOrders: SignedOrder[],
takerAssetFillAmount: BigNumber, takerAssetFillAmount: BigNumber,
slippagePercentage: number,
gasPrice: BigNumber, gasPrice: BigNumber,
opts: CalculateSwapQuoteOpts, opts: CalculateSwapQuoteOpts,
): Promise<MarketBuySwapQuote> { ): Promise<MarketBuySwapQuote> {
return (await this._calculateSwapQuoteAsync( return (await this._calculateSwapQuoteAsync(
prunedOrders, prunedOrders,
takerAssetFillAmount, takerAssetFillAmount,
slippagePercentage,
gasPrice, gasPrice,
MarketOperation.Buy, MarketOperation.Buy,
opts, opts,
@@ -71,14 +71,12 @@ export class SwapQuoteCalculator {
public async calculateBatchMarketBuySwapQuoteAsync( public async calculateBatchMarketBuySwapQuoteAsync(
batchPrunedOrders: SignedOrder[][], batchPrunedOrders: SignedOrder[][],
takerAssetFillAmounts: BigNumber[], takerAssetFillAmounts: BigNumber[],
slippagePercentage: number,
gasPrice: BigNumber, gasPrice: BigNumber,
opts: CalculateSwapQuoteOpts, opts: CalculateSwapQuoteOpts,
): Promise<Array<MarketBuySwapQuote | undefined>> { ): Promise<Array<MarketBuySwapQuote | undefined>> {
return (await this._calculateBatchBuySwapQuoteAsync( return (await this._calculateBatchBuySwapQuoteAsync(
batchPrunedOrders, batchPrunedOrders,
takerAssetFillAmounts, takerAssetFillAmounts,
slippagePercentage,
gasPrice, gasPrice,
MarketOperation.Buy, MarketOperation.Buy,
opts, opts,
@@ -88,17 +86,13 @@ export class SwapQuoteCalculator {
private async _calculateBatchBuySwapQuoteAsync( private async _calculateBatchBuySwapQuoteAsync(
batchPrunedOrders: SignedOrder[][], batchPrunedOrders: SignedOrder[][],
assetFillAmounts: BigNumber[], assetFillAmounts: BigNumber[],
slippagePercentage: number,
gasPrice: BigNumber, gasPrice: BigNumber,
operation: MarketOperation, operation: MarketOperation,
opts: CalculateSwapQuoteOpts, opts: CalculateSwapQuoteOpts,
): Promise<Array<SwapQuote | undefined>> { ): Promise<Array<SwapQuote | undefined>> {
const assetFillAmountsWithSlippage = assetFillAmounts.map(a =>
a.plus(a.multipliedBy(slippagePercentage).integerValue()),
);
const batchSignedOrders = await this._marketOperationUtils.getBatchMarketBuyOrdersAsync( const batchSignedOrders = await this._marketOperationUtils.getBatchMarketBuyOrdersAsync(
batchPrunedOrders, batchPrunedOrders,
assetFillAmountsWithSlippage, assetFillAmounts,
opts, opts,
); );
const batchSwapQuotes = await Promise.all( const batchSwapQuotes = await Promise.all(
@@ -112,6 +106,7 @@ export class SwapQuoteCalculator {
operation, operation,
assetFillAmounts[i], assetFillAmounts[i],
gasPrice, gasPrice,
opts.gasSchedule,
); );
} else { } else {
return undefined; return undefined;
@@ -123,25 +118,23 @@ export class SwapQuoteCalculator {
private async _calculateSwapQuoteAsync( private async _calculateSwapQuoteAsync(
prunedOrders: SignedOrder[], prunedOrders: SignedOrder[],
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
slippagePercentage: number,
gasPrice: BigNumber, gasPrice: BigNumber,
operation: MarketOperation, operation: MarketOperation,
opts: CalculateSwapQuoteOpts, opts: CalculateSwapQuoteOpts,
): Promise<SwapQuote> { ): Promise<SwapQuote> {
// checks if maker asset is ERC721 or ERC20 and taker asset is ERC20 // checks if maker asset is ERC721 or ERC20 and taker asset is ERC20
if (!utils.isSupportedAssetDataInOrders(prunedOrders)) { if (!isSupportedAssetDataInOrders(prunedOrders)) {
throw Error(SwapQuoterError.AssetDataUnsupported); throw Error(SwapQuoterError.AssetDataUnsupported);
} }
// since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled // since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled
const slippageBufferAmount = assetFillAmount.multipliedBy(slippagePercentage).integerValue();
let resultOrders: OptimizedMarketOrder[] = []; let resultOrders: OptimizedMarketOrder[] = [];
{ {
// Scale fees by gas price. // Scale fees by gas price.
const _opts = { const _opts = {
...opts, ...opts,
fees: _.mapValues(opts.fees, (v, k) => v.times(gasPrice)), fees: _.mapValues(opts.feeSchedule, v => v.times(gasPrice)),
}; };
const firstOrderMakerAssetData = !!prunedOrders[0] const firstOrderMakerAssetData = !!prunedOrders[0]
@@ -150,20 +143,18 @@ export class SwapQuoteCalculator {
if (firstOrderMakerAssetData.assetProxyId === AssetProxyId.ERC721) { if (firstOrderMakerAssetData.assetProxyId === AssetProxyId.ERC721) {
// HACK: to conform ERC721 orders to the output of market operation utils, assumes complete fillable // HACK: to conform ERC721 orders to the output of market operation utils, assumes complete fillable
resultOrders = prunedOrders.map(o => resultOrders = prunedOrders.map(o => convertNativeOrderToFullyFillableOptimizedOrders(o));
CreateOrderUtils.convertNativeOrderToFullyFillableOptimizedOrders(o),
);
} else { } else {
if (operation === MarketOperation.Buy) { if (operation === MarketOperation.Buy) {
resultOrders = await this._marketOperationUtils.getMarketBuyOrdersAsync( resultOrders = await this._marketOperationUtils.getMarketBuyOrdersAsync(
prunedOrders, prunedOrders,
assetFillAmount.plus(slippageBufferAmount), assetFillAmount,
_opts, _opts,
); );
} else { } else {
resultOrders = await this._marketOperationUtils.getMarketSellOrdersAsync( resultOrders = await this._marketOperationUtils.getMarketSellOrdersAsync(
prunedOrders, prunedOrders,
assetFillAmount.plus(slippageBufferAmount), assetFillAmount,
_opts, _opts,
); );
} }
@@ -179,6 +170,7 @@ export class SwapQuoteCalculator {
operation, operation,
assetFillAmount, assetFillAmount,
gasPrice, gasPrice,
opts.gasSchedule,
); );
} }
private async _createSwapQuoteAsync( private async _createSwapQuoteAsync(
@@ -188,22 +180,25 @@ export class SwapQuoteCalculator {
operation: MarketOperation, operation: MarketOperation,
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
gasPrice: BigNumber, gasPrice: BigNumber,
gasSchedule: { [source: string]: number },
): Promise<SwapQuote> { ): Promise<SwapQuote> {
const bestCaseQuoteInfo = await this._calculateQuoteInfoAsync( const bestCaseQuoteInfo = await this._calculateQuoteInfoAsync(
resultOrders, resultOrders,
assetFillAmount, assetFillAmount,
gasPrice, gasPrice,
gasSchedule,
operation, operation,
); );
const worstCaseQuoteInfo = await this._calculateQuoteInfoAsync( const worstCaseQuoteInfo = await this._calculateQuoteInfoAsync(
resultOrders, resultOrders,
assetFillAmount, assetFillAmount,
gasPrice, gasPrice,
gasSchedule,
operation, operation,
true, true,
); );
const breakdown = this._getSwapQuoteOrdersBreakdown(resultOrders, operation); const breakdown = getSwapQuoteOrdersBreakdown(resultOrders, operation);
const quoteBase: SwapQuoteBase = { const quoteBase: SwapQuoteBase = {
takerAssetData, takerAssetData,
@@ -236,14 +231,16 @@ export class SwapQuoteCalculator {
orders: OptimizedMarketOrder[], orders: OptimizedMarketOrder[],
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
gasPrice: BigNumber, gasPrice: BigNumber,
gasSchedule: { [source: string]: number },
operation: MarketOperation, operation: MarketOperation,
worstCase: boolean = false, worstCase: boolean = false,
): Promise<SwapQuoteInfo> { ): Promise<SwapQuoteInfo> {
if (operation === MarketOperation.Buy) { return {
return this._calculateMarketBuyQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase); ...(operation === MarketOperation.Buy
} else { ? await this._calculateMarketBuyQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase)
return this._calculateMarketSellQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase); : await this._calculateMarketSellQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase)),
} gas: getGasUsedByOrders(orders, gasSchedule),
};
} }
private async _calculateMarketSellQuoteInfoAsync( private async _calculateMarketSellQuoteInfoAsync(
@@ -337,6 +334,7 @@ export class SwapQuoteCalculator {
totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount), totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount),
makerAssetAmount: totalMakerAssetAmount, makerAssetAmount: totalMakerAssetAmount,
protocolFeeInWeiAmount, protocolFeeInWeiAmount,
gas: 0,
}; };
} }
@@ -426,45 +424,37 @@ export class SwapQuoteCalculator {
totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount), totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount),
makerAssetAmount: totalMakerAssetAmount, makerAssetAmount: totalMakerAssetAmount,
protocolFeeInWeiAmount, protocolFeeInWeiAmount,
gas: 0,
}; };
} }
}
// tslint:disable-next-line: prefer-function-over-method function getSwapQuoteOrdersBreakdown(
private _getSwapQuoteOrdersBreakdown( orders: OptimizedMarketOrder[],
orders: OptimizedMarketOrder[], operation: MarketOperation,
operation: MarketOperation, ): SwapQuoteOrdersBreakdown {
): SwapQuoteOrdersBreakdown { const orderAmounts =
// HACK: to shut up linter operation === MarketOperation.Buy
const breakdown: SwapQuoteOrdersBreakdown = {}; ? orders.map(o => o.fill.totalMakerAssetAmount)
: orders.map(o => o.fill.totalTakerAssetAmount);
// total asset amount (accounting for slippage protection) const amountsBySource: SwapQuoteOrdersBreakdown = {};
const totalAssetAmount = BigNumber.sum( orders.forEach((o, i) => {
...[ const source = o.fill.source;
constants.ZERO_AMOUNT, amountsBySource[source] = orderAmounts[i].plus(amountsBySource[source] || 0);
...orders.map(o => (operation === MarketOperation.Buy ? o.makerAssetAmount : o.takerAssetAmount)), });
], const totalAmount = BigNumber.sum(0, ...orderAmounts);
); const breakdown: SwapQuoteOrdersBreakdown = {};
for (const [source, amount] of Object.entries(amountsBySource)) {
return orders.reduce((acc: SwapQuoteOrdersBreakdown, order: OptimizedMarketOrder): SwapQuoteOrdersBreakdown => { breakdown[source] = amount.div(totalAmount);
const assetAmount = operation === MarketOperation.Buy ? order.makerAssetAmount : order.takerAssetAmount;
const { source } = order.fill;
return {
...acc,
...{
[source]: !!acc[source]
? acc[source].plus(assetAmount.dividedBy(totalAssetAmount))
: assetAmount.dividedBy(totalAssetAmount),
},
};
}, breakdown);
} }
return breakdown;
} }
function getTakerAssetAmountBreakDown( function getTakerAssetAmountBreakDown(
order: SignedOrderWithFillableAmounts, order: SignedOrderWithFillableAmounts,
takerAssetAmountWithFees: BigNumber, takerAssetAmountWithFees: BigNumber,
): { feeTakerAssetAmount: BigNumber; takerAssetAmount: BigNumber } { ): { feeTakerAssetAmount: BigNumber; takerAssetAmount: BigNumber } {
if (utils.isOrderTakerFeePayableWithTakerAsset(order)) { if (isOrderTakerFeePayableWithTakerAsset(order)) {
const adjustedTakerAssetAmount = order.takerAssetAmount.plus(order.takerFee); const adjustedTakerAssetAmount = order.takerAssetAmount.plus(order.takerFee);
const filledRatio = takerAssetAmountWithFees.div(adjustedTakerAssetAmount); const filledRatio = takerAssetAmountWithFees.div(adjustedTakerAssetAmount);
const takerAssetAmount = filledRatio.multipliedBy(order.takerAssetAmount).integerValue(BigNumber.ROUND_CEIL); const takerAssetAmount = filledRatio.multipliedBy(order.takerAssetAmount).integerValue(BigNumber.ROUND_CEIL);
@@ -472,7 +462,7 @@ function getTakerAssetAmountBreakDown(
takerAssetAmount, takerAssetAmount,
feeTakerAssetAmount: takerAssetAmountWithFees.minus(takerAssetAmount), feeTakerAssetAmount: takerAssetAmountWithFees.minus(takerAssetAmount),
}; };
} else if (utils.isOrderTakerFeePayableWithMakerAsset(order)) { } else if (isOrderTakerFeePayableWithMakerAsset(order)) {
if (takerAssetAmountWithFees.isZero()) { if (takerAssetAmountWithFees.isZero()) {
return { return {
takerAssetAmount: constants.ZERO_AMOUNT, takerAssetAmount: constants.ZERO_AMOUNT,
@@ -495,3 +485,12 @@ function getTakerAssetAmountBreakDown(
takerAssetAmount: takerAssetAmountWithFees, takerAssetAmount: takerAssetAmountWithFees,
}; };
} }
function getGasUsedByOrders(orders: OptimizedMarketOrder[], gasSchedule: { [source: string]: number }): number {
let totalUsage = 0;
for (const order of orders) {
totalUsage += gasSchedule[order.fill.source] || 0;
}
return totalUsage;
}
// tslint:disable: max-file-line-count

View File

@@ -15,9 +15,9 @@ import {
SwapQuoteConsumerError, SwapQuoteConsumerError,
SwapQuoteExecutionOpts, SwapQuoteExecutionOpts,
} from '../types'; } from '../types';
import { utils } from '../utils/utils';
import { assert } from './assert'; import { assert } from './assert';
import { isExactAssetData } from './utils';
export const swapQuoteConsumerUtils = { export const swapQuoteConsumerUtils = {
async getTakerAddressOrThrowAsync( async getTakerAddressOrThrowAsync(
@@ -66,7 +66,7 @@ export const swapQuoteConsumerUtils = {
return _.every(orders, order => swapQuoteConsumerUtils.isValidForwarderSignedOrder(order, wethAssetData)); return _.every(orders, order => swapQuoteConsumerUtils.isValidForwarderSignedOrder(order, wethAssetData));
}, },
isValidForwarderSignedOrder(order: SignedOrder, wethAssetData: string): boolean { isValidForwarderSignedOrder(order: SignedOrder, wethAssetData: string): boolean {
return utils.isExactAssetData(order.takerAssetData, wethAssetData); return isExactAssetData(order.takerAssetData, wethAssetData);
}, },
async getExtensionContractTypeForSwapQuoteAsync( async getExtensionContractTypeForSwapQuoteAsync(
quote: SwapQuote, quote: SwapQuote,

View File

@@ -5,103 +5,111 @@ import { Web3Wrapper } from '@0x/web3-wrapper';
import { constants } from '../constants'; import { constants } from '../constants';
// tslint:disable:no-unnecessary-type-assertion // tslint:disable: no-unnecessary-type-assertion completed-docs
export const utils = {
isSupportedAssetDataInOrders(orders: SignedOrder[]): boolean {
const firstOrderMakerAssetData = !!orders[0]
? assetDataUtils.decodeAssetDataOrThrow(orders[0].makerAssetData)
: { assetProxyId: '' };
return orders.every(o => {
const takerAssetData = assetDataUtils.decodeAssetDataOrThrow(o.takerAssetData);
const makerAssetData = assetDataUtils.decodeAssetDataOrThrow(o.makerAssetData);
return (
(makerAssetData.assetProxyId === AssetProxyId.ERC20 ||
makerAssetData.assetProxyId === AssetProxyId.ERC721) &&
takerAssetData.assetProxyId === AssetProxyId.ERC20 &&
firstOrderMakerAssetData.assetProxyId === makerAssetData.assetProxyId
); // checks that all native order maker assets are of the same type
});
},
numberPercentageToEtherTokenAmountPercentage(percentage: number): BigNumber {
return Web3Wrapper.toBaseUnitAmount(constants.ONE_AMOUNT, constants.ETHER_TOKEN_DECIMALS).multipliedBy(
percentage,
);
},
isOrderTakerFeePayableWithMakerAsset<T extends Order>(order: T): boolean {
return !order.takerFee.isZero() && utils.isAssetDataEquivalent(order.takerFeeAssetData, order.makerAssetData);
},
isOrderTakerFeePayableWithTakerAsset<T extends Order>(order: T): boolean {
return !order.takerFee.isZero() && utils.isAssetDataEquivalent(order.takerFeeAssetData, order.takerAssetData);
},
getAdjustedMakerAndTakerAmountsFromTakerFees<T extends Order>(order: T): [BigNumber, BigNumber] {
const adjustedMakerAssetAmount = utils.isOrderTakerFeePayableWithMakerAsset(order)
? order.makerAssetAmount.minus(order.takerFee)
: order.makerAssetAmount;
const adjustedTakerAssetAmount = utils.isOrderTakerFeePayableWithTakerAsset(order)
? order.takerAssetAmount.plus(order.takerFee)
: order.takerAssetAmount;
return [adjustedMakerAssetAmount, adjustedTakerAssetAmount];
},
isExactAssetData(expectedAssetData: string, actualAssetData: string): boolean {
return expectedAssetData === actualAssetData;
},
/**
* Compare the Asset Data for equivalency. Expected is the asset data the user provided (wanted),
* actual is the asset data found or created.
*/
isAssetDataEquivalent(expectedAssetData: string, actualAssetData: string): boolean {
if (utils.isExactAssetData(expectedAssetData, actualAssetData)) {
return true;
}
const decodedExpectedAssetData = assetDataUtils.decodeAssetDataOrThrow(expectedAssetData);
const decodedActualAssetData = assetDataUtils.decodeAssetDataOrThrow(actualAssetData);
// ERC20 === ERC20, ERC20 === ERC20Bridge
if (
utils.isERC20EquivalentAssetData(decodedExpectedAssetData) &&
utils.isERC20EquivalentAssetData(decodedActualAssetData)
) {
const doesTokenAddressMatch = decodedExpectedAssetData.tokenAddress === decodedActualAssetData.tokenAddress;
return doesTokenAddressMatch;
}
// ERC1155 === ERC1155
if (
assetDataUtils.isERC1155TokenAssetData(decodedExpectedAssetData) &&
assetDataUtils.isERC1155TokenAssetData(decodedActualAssetData)
) {
const doesTokenAddressMatch = decodedExpectedAssetData.tokenAddress === decodedActualAssetData.tokenAddress;
// IDs may be out of order yet still equivalent
// i.e (["a", "b"], [1,2]) === (["b", "a"], [2, 1])
// (["a", "b"], [2,1]) !== (["b", "a"], [2, 1])
const hasAllIds = decodedExpectedAssetData.tokenIds.every(
id => decodedActualAssetData.tokenIds.findIndex(v => id.eq(v)) !== -1,
);
const hasAllValues = decodedExpectedAssetData.tokenIds.every((id, i) =>
decodedExpectedAssetData.tokenValues[i].eq(
decodedActualAssetData.tokenValues[decodedActualAssetData.tokenIds.findIndex(v => id.eq(v))],
),
);
// If expected contains callback data, ensure it is present
// if actual has callbackdata and expected provided none then ignore it
const hasEquivalentCallback =
decodedExpectedAssetData.callbackData === NULL_BYTES ||
decodedExpectedAssetData.callbackData === decodedActualAssetData.callbackData;
return doesTokenAddressMatch && hasAllIds && hasAllValues && hasEquivalentCallback;
}
// ERC721 === ERC721
if (
assetDataUtils.isERC721TokenAssetData(decodedExpectedAssetData) ||
assetDataUtils.isERC721TokenAssetData(decodedActualAssetData)
) {
// Asset Data should exactly match for ERC721
return utils.isExactAssetData(expectedAssetData, actualAssetData);
}
// TODO(dekz): Unsupported cases export function isSupportedAssetDataInOrders(orders: SignedOrder[]): boolean {
// ERCXX(token) === MAP(token, staticCall) const firstOrderMakerAssetData = !!orders[0]
// MAP(a, b) === MAP(b, a) === MAP(b, a, staticCall) ? assetDataUtils.decodeAssetDataOrThrow(orders[0].makerAssetData)
return false; : { assetProxyId: '' };
}, return orders.every(o => {
isERC20EquivalentAssetData(assetData: AssetData): assetData is ERC20AssetData | ERC20BridgeAssetData { const takerAssetData = assetDataUtils.decodeAssetDataOrThrow(o.takerAssetData);
return assetDataUtils.isERC20TokenAssetData(assetData) || assetDataUtils.isERC20BridgeAssetData(assetData); const makerAssetData = assetDataUtils.decodeAssetDataOrThrow(o.makerAssetData);
}, return (
}; (makerAssetData.assetProxyId === AssetProxyId.ERC20 ||
makerAssetData.assetProxyId === AssetProxyId.ERC721) &&
takerAssetData.assetProxyId === AssetProxyId.ERC20 &&
firstOrderMakerAssetData.assetProxyId === makerAssetData.assetProxyId
); // checks that all native order maker assets are of the same type
});
}
export function numberPercentageToEtherTokenAmountPercentage(percentage: number): BigNumber {
return Web3Wrapper.toBaseUnitAmount(constants.ONE_AMOUNT, constants.ETHER_TOKEN_DECIMALS).multipliedBy(percentage);
}
export function isOrderTakerFeePayableWithMakerAsset<T extends Order>(order: T): boolean {
return !order.takerFee.isZero() && isAssetDataEquivalent(order.takerFeeAssetData, order.makerAssetData);
}
export function isOrderTakerFeePayableWithTakerAsset<T extends Order>(order: T): boolean {
return !order.takerFee.isZero() && isAssetDataEquivalent(order.takerFeeAssetData, order.takerAssetData);
}
export function getAdjustedMakerAndTakerAmountsFromTakerFees<T extends Order>(order: T): [BigNumber, BigNumber] {
const adjustedMakerAssetAmount = isOrderTakerFeePayableWithMakerAsset(order)
? order.makerAssetAmount.minus(order.takerFee)
: order.makerAssetAmount;
const adjustedTakerAssetAmount = isOrderTakerFeePayableWithTakerAsset(order)
? order.takerAssetAmount.plus(order.takerFee)
: order.takerAssetAmount;
return [adjustedMakerAssetAmount, adjustedTakerAssetAmount];
}
export function isExactAssetData(expectedAssetData: string, actualAssetData: string): boolean {
return expectedAssetData === actualAssetData;
}
/**
* Compare the Asset Data for equivalency. Expected is the asset data the user provided (wanted),
* actual is the asset data found or created.
*/
export function isAssetDataEquivalent(expectedAssetData: string, actualAssetData: string): boolean {
if (isExactAssetData(expectedAssetData, actualAssetData)) {
return true;
}
const decodedExpectedAssetData = assetDataUtils.decodeAssetDataOrThrow(expectedAssetData);
const decodedActualAssetData = assetDataUtils.decodeAssetDataOrThrow(actualAssetData);
// ERC20 === ERC20, ERC20 === ERC20Bridge
if (isERC20EquivalentAssetData(decodedExpectedAssetData) && isERC20EquivalentAssetData(decodedActualAssetData)) {
const doesTokenAddressMatch = decodedExpectedAssetData.tokenAddress === decodedActualAssetData.tokenAddress;
return doesTokenAddressMatch;
}
// ERC1155 === ERC1155
if (
assetDataUtils.isERC1155TokenAssetData(decodedExpectedAssetData) &&
assetDataUtils.isERC1155TokenAssetData(decodedActualAssetData)
) {
const doesTokenAddressMatch = decodedExpectedAssetData.tokenAddress === decodedActualAssetData.tokenAddress;
// IDs may be out of order yet still equivalent
// i.e (["a", "b"], [1,2]) === (["b", "a"], [2, 1])
// (["a", "b"], [2,1]) !== (["b", "a"], [2, 1])
const hasAllIds = decodedExpectedAssetData.tokenIds.every(
id => decodedActualAssetData.tokenIds.findIndex(v => id.eq(v)) !== -1,
);
const hasAllValues = decodedExpectedAssetData.tokenIds.every((id, i) =>
decodedExpectedAssetData.tokenValues[i].eq(
decodedActualAssetData.tokenValues[decodedActualAssetData.tokenIds.findIndex(v => id.eq(v))],
),
);
// If expected contains callback data, ensure it is present
// if actual has callbackdata and expected provided none then ignore it
const hasEquivalentCallback =
decodedExpectedAssetData.callbackData === NULL_BYTES ||
decodedExpectedAssetData.callbackData === decodedActualAssetData.callbackData;
return doesTokenAddressMatch && hasAllIds && hasAllValues && hasEquivalentCallback;
}
// ERC721 === ERC721
if (
assetDataUtils.isERC721TokenAssetData(decodedExpectedAssetData) ||
assetDataUtils.isERC721TokenAssetData(decodedActualAssetData)
) {
// Asset Data should exactly match for ERC721
return isExactAssetData(expectedAssetData, actualAssetData);
}
// TODO(dekz): Unsupported cases
// ERCXX(token) === MAP(token, staticCall)
// MAP(a, b) === MAP(b, a) === MAP(b, a, staticCall)
return false;
}
export function isERC20EquivalentAssetData(assetData: AssetData): assetData is ERC20AssetData | ERC20BridgeAssetData {
return assetDataUtils.isERC20TokenAssetData(assetData) || assetDataUtils.isERC20BridgeAssetData(assetData);
}
/**
* Gets the difference between two sets.
*/
export function difference<T>(a: T[], b: T[]): T[] {
return a.filter(x => b.indexOf(x) === -1);
}

View File

@@ -14,14 +14,11 @@ import { AssetProxyId, ERC20BridgeAssetData, SignedOrder } from '@0x/types';
import { BigNumber, fromTokenUnitAmount, hexUtils, NULL_ADDRESS } from '@0x/utils'; import { BigNumber, fromTokenUnitAmount, hexUtils, NULL_ADDRESS } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { constants as assetSwapperConstants } from '../src/constants';
import { MarketOperationUtils } from '../src/utils/market_operation_utils/'; import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { constants as marketOperationUtilConstants } from '../src/utils/market_operation_utils/constants'; import { BUY_SOURCES, DEFAULT_CURVE_OPTS, SELL_SOURCES } from '../src/utils/market_operation_utils/constants';
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
import { DexSample, ERC20BridgeSource } from '../src/utils/market_operation_utils/types'; import { DexSample, ERC20BridgeSource } from '../src/utils/market_operation_utils/types';
const { BUY_SOURCES, SELL_SOURCES } = marketOperationUtilConstants;
// tslint:disable: custom-no-magic-numbers // tslint:disable: custom-no-magic-numbers
describe('MarketOperationUtils tests', () => { describe('MarketOperationUtils tests', () => {
const CHAIN_ID = 1; const CHAIN_ID = 1;
@@ -81,8 +78,8 @@ describe('MarketOperationUtils tests', () => {
case UNISWAP_BRIDGE_ADDRESS.toLowerCase(): case UNISWAP_BRIDGE_ADDRESS.toLowerCase():
return ERC20BridgeSource.Uniswap; return ERC20BridgeSource.Uniswap;
case CURVE_BRIDGE_ADDRESS.toLowerCase(): case CURVE_BRIDGE_ADDRESS.toLowerCase():
const curveSource = Object.keys(assetSwapperConstants.DEFAULT_CURVE_OPTS).filter( const curveSource = Object.keys(DEFAULT_CURVE_OPTS).filter(
k => assetData.indexOf(assetSwapperConstants.DEFAULT_CURVE_OPTS[k].curveAddress.slice(2)) !== -1, k => assetData.indexOf(DEFAULT_CURVE_OPTS[k].curveAddress.slice(2)) !== -1,
); );
return curveSource[0] as ERC20BridgeSource; return curveSource[0] as ERC20BridgeSource;
default: default:
@@ -120,20 +117,21 @@ describe('MarketOperationUtils tests', () => {
chainId: CHAIN_ID, chainId: CHAIN_ID,
}; };
type GetQuotesOperation = (makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => BigNumber[]; function createSamplesFromRates(source: ERC20BridgeSource, inputs: Numberish[], rates: Numberish[]): DexSample[] {
const samples: DexSample[] = [];
function createGetSellQuotesOperationFromRates(rates: Numberish[]): GetQuotesOperation { inputs.forEach((input, i) => {
return (...args) => { const rate = rates[i];
const fillAmounts = args.pop() as BigNumber[]; samples.push({
return fillAmounts.map((a, i) => a.times(rates[i]).integerValue()); source,
}; input: new BigNumber(input),
} output: new BigNumber(input)
.minus(i === 0 ? 0 : samples[i - 1].input)
function createGetBuyQuotesOperationFromRates(rates: Numberish[]): GetQuotesOperation { .times(rate)
return (...args) => { .plus(i === 0 ? 0 : samples[i - 1].output)
const fillAmounts = args.pop() as BigNumber[]; .integerValue(),
return fillAmounts.map((a, i) => a.div(rates[i]).integerValue()); });
}; });
return samples;
} }
type GetMultipleQuotesOperation = ( type GetMultipleQuotesOperation = (
@@ -146,13 +144,7 @@ describe('MarketOperationUtils tests', () => {
function createGetMultipleSellQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation { function createGetMultipleSellQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation {
return (sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => { return (sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => {
return sources.map(s => return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s]));
fillAmounts.map((a, i) => ({
source: s,
input: a,
output: a.times(rates[s][i]).integerValue(),
})),
);
}; };
} }
@@ -180,13 +172,7 @@ describe('MarketOperationUtils tests', () => {
function createGetMultipleBuyQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation { function createGetMultipleBuyQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation {
return (sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => { return (sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => {
return sources.map(s => return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r))));
fillAmounts.map((a, i) => ({
source: s,
input: a,
output: a.div(rates[s][i]).integerValue(),
})),
);
}; };
} }
@@ -264,22 +250,6 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.LiquidityProvider]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.LiquidityProvider]: _.times(NUM_SAMPLES, () => 0),
}; };
function findSourceWithMaxOutput(rates: RatesBySource): ERC20BridgeSource {
const minSourceRates = Object.keys(rates).map(s => _.last(rates[s]) as BigNumber);
const bestSourceRate = BigNumber.max(...minSourceRates);
let source = Object.keys(rates)[_.findIndex(minSourceRates, t => bestSourceRate.eq(t))] as ERC20BridgeSource;
// Native order rates play by different rules.
if (source !== ERC20BridgeSource.Native) {
const nativeTotalRate = BigNumber.sum(...rates[ERC20BridgeSource.Native]).div(
rates[ERC20BridgeSource.Native].length,
);
if (nativeTotalRate.gt(bestSourceRate)) {
source = ERC20BridgeSource.Native;
}
}
return source;
}
const DEFAULT_OPS = { const DEFAULT_OPS = {
getOrderFillableTakerAmounts(orders: SignedOrder[]): BigNumber[] { getOrderFillableTakerAmounts(orders: SignedOrder[]): BigNumber[] {
return orders.map(o => o.takerAssetAmount); return orders.map(o => o.takerAssetAmount);
@@ -287,12 +257,6 @@ describe('MarketOperationUtils tests', () => {
getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] { getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] {
return orders.map(o => o.makerAssetAmount); return orders.map(o => o.makerAssetAmount);
}, },
getKyberSellQuotes: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Kyber]),
getUniswapSellQuotes: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Uniswap]),
getEth2DaiSellQuotes: createGetSellQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Eth2Dai]),
getUniswapBuyQuotes: createGetBuyQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Uniswap]),
getEth2DaiBuyQuotes: createGetBuyQuotesOperationFromRates(DEFAULT_RATES[ERC20BridgeSource.Eth2Dai]),
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), getMedianSellRate: createGetMedianSellRate(1),
@@ -323,17 +287,18 @@ describe('MarketOperationUtils tests', () => {
}); });
describe('getMarketSellOrdersAsync()', () => { describe('getMarketSellOrdersAsync()', () => {
const FILL_AMOUNT = getRandomInteger(1, 1e18); const FILL_AMOUNT = new BigNumber('100e18');
const ORDERS = createOrdersFromSellRates( const ORDERS = createOrdersFromSellRates(
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 = { const DEFAULT_OPTS = {
numSamples: NUM_SAMPLES, numSamples: NUM_SAMPLES,
runLimit: 0,
sampleDistributionBase: 1, sampleDistributionBase: 1,
bridgeSlippage: 0, bridgeSlippage: 0,
excludedSources: Object.keys(assetSwapperConstants.DEFAULT_CURVE_OPTS) as ERC20BridgeSource[], maxFallbackSlippage: 100,
excludedSources: Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
allowFallback: false,
}; };
beforeEach(() => { beforeEach(() => {
@@ -341,7 +306,7 @@ describe('MarketOperationUtils tests', () => {
}); });
it('queries `numSamples` samples', async () => { it('queries `numSamples` samples', async () => {
const numSamples = _.random(1, 16); const numSamples = _.random(1, NUM_SAMPLES);
let actualNumSamples = 0; let actualNumSamples = 0;
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts) => { getSellQuotes: (sources, makerToken, takerToken, amounts) => {
@@ -412,18 +377,6 @@ describe('MarketOperationUtils tests', () => {
expect(sourcesPolled.sort()).to.deep.eq(_.without(SELL_SOURCES, ...excludedSources).sort()); expect(sourcesPolled.sort()).to.deep.eq(_.without(SELL_SOURCES, ...excludedSources).sort());
}); });
it('returns the most cost-effective single source if `runLimit == 0`', async () => {
const bestSource = findSourceWithMaxOutput(DEFAULT_RATES);
expect(bestSource).to.exist('');
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS,
runLimit: 0,
});
const uniqueAssetDatas = _.uniq(improvedOrders.map(o => o.makerAssetData));
expect(uniqueAssetDatas).to.be.length(1);
expect(getSourceFromAssetData(uniqueAssetDatas[0])).to.be.eq(bestSource);
});
it('generates bridge orders with correct asset data', async () => { it('generates bridge orders with correct asset data', async () => {
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
// Pass in empty orders to prevent native orders from being used. // Pass in empty orders to prevent native orders from being used.
@@ -469,10 +422,9 @@ describe('MarketOperationUtils tests', () => {
); );
expect(improvedOrders).to.not.be.length(0); expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) { for (const order of improvedOrders) {
const source = getSourceFromAssetData(order.makerAssetData); const expectedMakerAmount = order.fill.totalMakerAssetAmount;
const expectedMakerAmount = FILL_AMOUNT.times(_.last(DEFAULT_RATES[source]) as BigNumber);
const slippage = 1 - order.makerAssetAmount.div(expectedMakerAmount.plus(1)).toNumber(); const slippage = 1 - order.makerAssetAmount.div(expectedMakerAmount.plus(1)).toNumber();
assertRoughlyEquals(slippage, bridgeSlippage, 8); assertRoughlyEquals(slippage, bridgeSlippage, 1);
} }
}); });
@@ -481,26 +433,26 @@ describe('MarketOperationUtils tests', () => {
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, 0, 0, 0]; // unused
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
}); });
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false }, { ...DEFAULT_OPTS, numSamples: 4 },
); );
const orderSources = improvedOrders.map(o => o.fill.source); const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Kyber,
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Uniswap, ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
]; ];
expect(orderSources).to.deep.eq(expectedSources); expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
}); });
it('excludes Kyber when `noConflicts` enabled and Uniswap or Eth2Dai are used first', async () => { it('Kyber is exclusive against Uniswap and Eth2Dai', async () => {
const rates: RatesBySource = {}; const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.3, 0.2, 0.1, 0.05]; rates[ERC20BridgeSource.Native] = [0.3, 0.2, 0.1, 0.05];
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05]; rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
@@ -512,40 +464,15 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true }, { ...DEFAULT_OPTS, numSamples: 4 },
); );
const orderSources = improvedOrders.map(o => o.fill.source); const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [ if (orderSources.includes(ERC20BridgeSource.Kyber)) {
ERC20BridgeSource.Eth2Dai, expect(orderSources).to.not.include(ERC20BridgeSource.Uniswap);
ERC20BridgeSource.Uniswap, expect(orderSources).to.not.include(ERC20BridgeSource.Eth2Dai);
ERC20BridgeSource.Native, } else {
ERC20BridgeSource.Native, expect(orderSources).to.not.include(ERC20BridgeSource.Kyber);
]; }
expect(orderSources).to.deep.eq(expectedSources);
});
it('excludes Uniswap and Eth2Dai when `noConflicts` enabled and Kyber is used first', async () => {
const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.1, 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.Kyber] = [0.7, 0.05, 0.05, 0.05];
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
});
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true },
);
const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [
ERC20BridgeSource.Kyber,
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
];
expect(orderSources).to.deep.eq(expectedSources);
}); });
const ETH_TO_MAKER_RATE = 1.5; const ETH_TO_MAKER_RATE = 1.5;
@@ -555,12 +482,12 @@ describe('MarketOperationUtils tests', () => {
// dropping their effective rates. // dropping their effective rates.
const nativeFeeRate = 0.06; const nativeFeeRate = 0.06;
const rates: RatesBySource = { const rates: RatesBySource = {
[ERC20BridgeSource.Native]: [1, 0.99, 0.98, 0.97], // Effectively [0.94, ~0.93, ~0.92, ~0.91] [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.Uniswap]: [0.96, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Eth2Dai]: [0.95, 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], [ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
}; };
const fees = { const feeSchedule = {
[ERC20BridgeSource.Native]: FILL_AMOUNT.div(4) [ERC20BridgeSource.Native]: FILL_AMOUNT.div(4)
.times(nativeFeeRate) .times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE), .dividedToIntegerBy(ETH_TO_MAKER_RATE),
@@ -572,32 +499,32 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false, fees }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const orderSources = improvedOrders.map(o => o.fill.source); const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Native,
ERC20BridgeSource.Uniswap, ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
]; ];
expect(orderSources).to.deep.eq(expectedSources); expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
}); });
it('factors in fees for dexes', async () => { it('factors in fees for dexes', async () => {
// Kyber will have the best rates but will have fees, // Kyber will have the best rates but will have fees,
// dropping its effective rates. // dropping its effective rates.
const kyberFeeRate = 0.2; const uniswapFeeRate = 0.2;
const rates: RatesBySource = { const rates: RatesBySource = {
[ERC20BridgeSource.Native]: [0.95, 0.1, 0.1, 0.1], [ERC20BridgeSource.Native]: [0.95, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Uniswap]: [0.1, 0.1, 0.1, 0.1], [ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1], [ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1],
// Effectively [0.8, ~0.5, ~0, ~0] // Effectively [0.8, ~0.5, ~0, ~0]
[ERC20BridgeSource.Kyber]: [1, 0.7, 0.2, 0.2], [ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2],
}; };
const fees = { const feeSchedule = {
[ERC20BridgeSource.Kyber]: FILL_AMOUNT.div(4) [ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4)
.times(kyberFeeRate) .times(uniswapFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE), .dividedToIntegerBy(ETH_TO_MAKER_RATE),
}; };
replaceSamplerOps({ replaceSamplerOps({
@@ -607,11 +534,87 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false, fees }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const orderSources = improvedOrders.map(o => o.fill.source); const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber]; const expectedSources = [
expect(orderSources).to.deep.eq(expectedSources); ERC20BridgeSource.Native,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Uniswap,
];
expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
});
it('can mix one concave source', async () => {
const rates: RatesBySource = {
[ERC20BridgeSource.Kyber]: [0, 0, 0, 0], // Won't use
[ERC20BridgeSource.Eth2Dai]: [0.5, 0.85, 0.75, 0.75], // Concave
[ERC20BridgeSource.Uniswap]: [0.96, 0.2, 0.1, 0.1],
[ERC20BridgeSource.Native]: [0.95, 0.2, 0.2, 0.1],
};
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 },
);
const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Native,
];
expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
});
it('fallback orders use different sources', async () => {
const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.9, 0.8, 0.5, 0.5];
rates[ERC20BridgeSource.Uniswap] = [0.6, 0.05, 0.01, 0.01];
rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01];
rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01];
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
});
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true },
);
const orderSources = improvedOrders.map(o => o.fill.source);
const firstSources = [
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
ERC20BridgeSource.Uniswap,
];
const secondSources = [ERC20BridgeSource.Eth2Dai];
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
});
it('does not create a fallback if below maxFallbackSlippage', async () => {
const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [1, 1, 0.01, 0.01];
rates[ERC20BridgeSource.Uniswap] = [1, 1, 0.01, 0.01];
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49];
rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01];
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
});
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.5 },
);
const orderSources = improvedOrders.map(o => o.fill.source);
const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
const secondSources: ERC20BridgeSource[] = [];
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
}); });
it('is able to create a order from LiquidityProvider', async () => { it('is able to create a order from LiquidityProvider', async () => {
@@ -651,7 +654,7 @@ describe('MarketOperationUtils tests', () => {
}), }),
], ],
Web3Wrapper.toBaseUnitAmount(10, 18), Web3Wrapper.toBaseUnitAmount(10, 18),
{ excludedSources: SELL_SOURCES, numSamples: 4 }, { excludedSources: SELL_SOURCES, numSamples: 4, bridgeSlippage: 0 },
); );
expect(result.length).to.eql(1); expect(result.length).to.eql(1);
expect(result[0].makerAddress).to.eql(liquidityProviderAddress); expect(result[0].makerAddress).to.eql(liquidityProviderAddress);
@@ -666,22 +669,24 @@ describe('MarketOperationUtils tests', () => {
expect(getSellQuotesParams.sources).contains(ERC20BridgeSource.LiquidityProvider); expect(getSellQuotesParams.sources).contains(ERC20BridgeSource.LiquidityProvider);
expect(getSellQuotesParams.liquidityProviderAddress).is.eql(registryAddress); expect(getSellQuotesParams.liquidityProviderAddress).is.eql(registryAddress);
expect(getLiquidityProviderParams.registryAddress).is.eql(registryAddress); expect(getLiquidityProviderParams.registryAddress).is.eql(registryAddress);
expect(getLiquidityProviderParams.makerToken).is.eql(xAsset); expect(getLiquidityProviderParams.makerToken).is.eql(yAsset);
expect(getLiquidityProviderParams.takerToken).is.eql(yAsset); expect(getLiquidityProviderParams.takerToken).is.eql(xAsset);
}); });
}); });
describe('getMarketBuyOrdersAsync()', () => { describe('getMarketBuyOrdersAsync()', () => {
const FILL_AMOUNT = getRandomInteger(1, 1e18); const FILL_AMOUNT = new BigNumber('100e18');
const ORDERS = createOrdersFromBuyRates( const ORDERS = createOrdersFromBuyRates(
FILL_AMOUNT, FILL_AMOUNT,
_.times(NUM_SAMPLES, () => DEFAULT_RATES[ERC20BridgeSource.Native][0]), _.times(NUM_SAMPLES, () => DEFAULT_RATES[ERC20BridgeSource.Native][0]),
); );
const DEFAULT_OPTS = { const DEFAULT_OPTS = {
numSamples: NUM_SAMPLES, numSamples: NUM_SAMPLES,
runLimit: 0,
sampleDistributionBase: 1, sampleDistributionBase: 1,
excludedSources: Object.keys(assetSwapperConstants.DEFAULT_CURVE_OPTS) as ERC20BridgeSource[], bridgeSlippage: 0,
maxFallbackSlippage: 100,
excludedSources: Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
allowFallback: false,
}; };
beforeEach(() => { beforeEach(() => {
@@ -760,26 +765,6 @@ describe('MarketOperationUtils tests', () => {
expect(sourcesPolled).to.deep.eq(_.without(BUY_SOURCES, ...excludedSources)); expect(sourcesPolled).to.deep.eq(_.without(BUY_SOURCES, ...excludedSources));
}); });
it('returns the most cost-effective single source if `runLimit == 0`', async () => {
const bestSource = findSourceWithMaxOutput(
_.omit(
DEFAULT_RATES,
ERC20BridgeSource.Kyber,
ERC20BridgeSource.CurveUsdcDai,
ERC20BridgeSource.CurveUsdcDaiUsdt,
ERC20BridgeSource.CurveUsdcDaiUsdtTusd,
),
);
expect(bestSource).to.exist('');
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS,
runLimit: 0,
});
const uniqueAssetDatas = _.uniq(improvedOrders.map(o => o.makerAssetData));
expect(uniqueAssetDatas).to.be.length(1);
expect(getSourceFromAssetData(uniqueAssetDatas[0])).to.be.eq(bestSource);
});
it('generates bridge orders with correct asset data', async () => { it('generates bridge orders with correct asset data', async () => {
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
// Pass in empty orders to prevent native orders from being used. // Pass in empty orders to prevent native orders from being used.
@@ -825,10 +810,9 @@ describe('MarketOperationUtils tests', () => {
); );
expect(improvedOrders).to.not.be.length(0); expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) { for (const order of improvedOrders) {
const source = getSourceFromAssetData(order.makerAssetData); const expectedTakerAmount = order.fill.totalTakerAssetAmount;
const expectedTakerAmount = FILL_AMOUNT.div(_.last(DEFAULT_RATES[source]) as BigNumber);
const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).toNumber() - 1; const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).toNumber() - 1;
assertRoughlyEquals(slippage, bridgeSlippage, 8); assertRoughlyEquals(slippage, bridgeSlippage, 1);
} }
}); });
@@ -843,7 +827,7 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512 }, { ...DEFAULT_OPTS, numSamples: 4 },
); );
const orderSources = improvedOrders.map(o => o.fill.source); const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [ const expectedSources = [
@@ -852,7 +836,7 @@ describe('MarketOperationUtils tests', () => {
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
]; ];
expect(orderSources).to.deep.eq(expectedSources); expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
}); });
const ETH_TO_TAKER_RATE = 1.5; const ETH_TO_TAKER_RATE = 1.5;
@@ -867,7 +851,7 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Eth2Dai]: [0.95, 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], [ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
}; };
const fees = { const feeSchedule = {
[ERC20BridgeSource.Native]: FILL_AMOUNT.div(4) [ERC20BridgeSource.Native]: FILL_AMOUNT.div(4)
.times(nativeFeeRate) .times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE), .dividedToIntegerBy(ETH_TO_TAKER_RATE),
@@ -879,7 +863,7 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, fees }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const orderSources = improvedOrders.map(o => o.fill.source); const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [ const expectedSources = [
@@ -888,7 +872,7 @@ describe('MarketOperationUtils tests', () => {
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
]; ];
expect(orderSources).to.deep.eq(expectedSources); expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
}); });
it('factors in fees for dexes', async () => { it('factors in fees for dexes', async () => {
@@ -901,7 +885,7 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2], [ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2],
[ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1], [ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1],
}; };
const fees = { const feeSchedule = {
[ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4) [ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4)
.times(uniswapFeeRate) .times(uniswapFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE), .dividedToIntegerBy(ETH_TO_TAKER_RATE),
@@ -913,7 +897,7 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT, FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, fees }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const orderSources = improvedOrders.map(o => o.fill.source); const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [ const expectedSources = [
@@ -921,7 +905,52 @@ describe('MarketOperationUtils tests', () => {
ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Uniswap, ERC20BridgeSource.Uniswap,
]; ];
expect(orderSources).to.deep.eq(expectedSources); expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
});
it('fallback orders use different sources', async () => {
const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [0.9, 0.8, 0.5, 0.5];
rates[ERC20BridgeSource.Uniswap] = [0.6, 0.05, 0.01, 0.01];
rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01];
replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
});
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true },
);
const orderSources = improvedOrders.map(o => o.fill.source);
const firstSources = [
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
ERC20BridgeSource.Uniswap,
];
const secondSources = [ERC20BridgeSource.Eth2Dai];
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
});
it('does not create a fallback if below maxFallbackSlippage', async () => {
const rates: RatesBySource = {};
rates[ERC20BridgeSource.Native] = [1, 1, 0.01, 0.01];
rates[ERC20BridgeSource.Uniswap] = [1, 1, 0.01, 0.01];
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49];
replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
});
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.5 },
);
const orderSources = improvedOrders.map(o => o.fill.source);
const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
const secondSources: ERC20BridgeSource[] = [];
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
}); });
}); });
}); });

View File

@@ -1,3 +1,7 @@
// tslint:disable:max-file-line-count
// TODO(dorothy-zbornak): Skipping these tests for now because they're a
// nightmare to maintain. We should replace them with simpler unit tests.
/*
import { constants as devConstants } from '@0x/contracts-test-utils'; import { constants as devConstants } from '@0x/contracts-test-utils';
import { BlockchainLifecycle } from '@0x/dev-utils'; import { BlockchainLifecycle } from '@0x/dev-utils';
import { ContractAddresses, migrateOnceAsync } from '@0x/migrations'; import { ContractAddresses, migrateOnceAsync } from '@0x/migrations';
@@ -8,8 +12,9 @@ import 'mocha';
import { constants } from '../src/constants'; import { constants } from '../src/constants';
import { CalculateSwapQuoteOpts, SignedOrderWithFillableAmounts } from '../src/types'; import { CalculateSwapQuoteOpts, SignedOrderWithFillableAmounts } from '../src/types';
import { DexOrderSampler, MarketOperationUtils } from '../src/utils/market_operation_utils/'; import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { constants as marketOperationUtilConstants } from '../src/utils/market_operation_utils/constants'; import { DEFAULT_GET_MARKET_ORDERS_OPTS, SELL_SOURCES } from '../src/utils/market_operation_utils/constants';
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils'; import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils';
import { SwapQuoteCalculator } from '../src/utils/swap_quote_calculator'; import { SwapQuoteCalculator } from '../src/utils/swap_quote_calculator';
@@ -33,8 +38,6 @@ const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000);
// ); // );
const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID; const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID;
const { DEFAULT_GET_MARKET_ORDERS_OPTS, SELL_SOURCES } = marketOperationUtilConstants;
// Excludes all non native sources // Excludes all non native sources
const CALCULATE_SWAP_QUOTE_OPTS: CalculateSwapQuoteOpts = { const CALCULATE_SWAP_QUOTE_OPTS: CalculateSwapQuoteOpts = {
...DEFAULT_GET_MARKET_ORDERS_OPTS, ...DEFAULT_GET_MARKET_ORDERS_OPTS,
@@ -64,10 +67,7 @@ function createSamplerFromSignedOrdersWithFillableAmounts(
); );
} }
// tslint:disable:max-file-line-count
// tslint:disable:custom-no-magic-numbers // tslint:disable:custom-no-magic-numbers
// 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', () => { describe.skip('swapQuoteCalculator', () => {
let protocolFeeUtils: ProtocolFeeUtils; let protocolFeeUtils: ProtocolFeeUtils;
let contractAddresses: ContractAddresses; let contractAddresses: ContractAddresses;
@@ -905,3 +905,4 @@ describe.skip('swapQuoteCalculator', () => {
}); });
}); });
}); });
*/

View File

@@ -25,6 +25,7 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync(
takerAssetAmount: totalTakerAssetAmount, takerAssetAmount: totalTakerAssetAmount,
totalTakerAssetAmount, totalTakerAssetAmount,
protocolFeeInWeiAmount: await protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(orders, gasPrice), protocolFeeInWeiAmount: await protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(orders, gasPrice),
gas: 200e3,
}; };
const breakdown = { const breakdown = {

View File

@@ -4,7 +4,7 @@ import { BigNumber, NULL_ADDRESS, NULL_BYTES } from '@0x/utils';
import * as chai from 'chai'; import * as chai from 'chai';
import 'mocha'; import 'mocha';
import { utils } from '../src/utils/utils'; import { isAssetDataEquivalent } from '../src/utils/utils';
import { chaiSetup } from './utils/chai_setup'; import { chaiSetup } from './utils/chai_setup';
@@ -16,35 +16,35 @@ describe('utils', () => {
describe('ERC20', () => { describe('ERC20', () => {
const [tokenA, tokenB] = tokenUtils.getDummyERC20TokenAddresses(); const [tokenA, tokenB] = tokenUtils.getDummyERC20TokenAddresses();
it('should succeed ERC20 to be ERC20Bridge', () => { it('should succeed ERC20 to be ERC20Bridge', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC20AssetData(tokenA), assetDataUtils.encodeERC20AssetData(tokenA),
assetDataUtils.encodeERC20BridgeAssetData(tokenA, NULL_ADDRESS, NULL_BYTES), assetDataUtils.encodeERC20BridgeAssetData(tokenA, NULL_ADDRESS, NULL_BYTES),
); );
expect(isEquivalent).to.be.true(); expect(isEquivalent).to.be.true();
}); });
it('should succeed ERC20Bridge to be ERC20', () => { it('should succeed ERC20Bridge to be ERC20', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC20BridgeAssetData(tokenA, NULL_ADDRESS, NULL_BYTES), assetDataUtils.encodeERC20BridgeAssetData(tokenA, NULL_ADDRESS, NULL_BYTES),
assetDataUtils.encodeERC20AssetData(tokenA), assetDataUtils.encodeERC20AssetData(tokenA),
); );
expect(isEquivalent).to.be.true(); expect(isEquivalent).to.be.true();
}); });
it('should succeed ERC20 to be ERC20', () => { it('should succeed ERC20 to be ERC20', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC20AssetData(tokenA), assetDataUtils.encodeERC20AssetData(tokenA),
assetDataUtils.encodeERC20AssetData(tokenA), assetDataUtils.encodeERC20AssetData(tokenA),
); );
expect(isEquivalent).to.be.true(); expect(isEquivalent).to.be.true();
}); });
it('should fail if ERC20Bridge is not the same ERC20 token', () => { it('should fail if ERC20Bridge is not the same ERC20 token', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC20AssetData(tokenA), assetDataUtils.encodeERC20AssetData(tokenA),
assetDataUtils.encodeERC20BridgeAssetData(tokenB, NULL_ADDRESS, NULL_BYTES), assetDataUtils.encodeERC20BridgeAssetData(tokenB, NULL_ADDRESS, NULL_BYTES),
); );
expect(isEquivalent).to.be.false(); expect(isEquivalent).to.be.false();
}); });
it('should fail if ERC20 is not the same ERC20 token', () => { it('should fail if ERC20 is not the same ERC20 token', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC20AssetData(tokenA), assetDataUtils.encodeERC20AssetData(tokenA),
assetDataUtils.encodeERC20AssetData(tokenB), assetDataUtils.encodeERC20AssetData(tokenB),
); );
@@ -56,28 +56,28 @@ describe('utils', () => {
const tokenIdA = new BigNumber(1); const tokenIdA = new BigNumber(1);
const tokenIdB = new BigNumber(2); const tokenIdB = new BigNumber(2);
it('should succeed if ERC721 the same ERC721 token and id', () => { it('should succeed if ERC721 the same ERC721 token and id', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA), assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA),
assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA), assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA),
); );
expect(isEquivalent).to.be.true(); expect(isEquivalent).to.be.true();
}); });
it('should fail if ERC721 is not the same ERC721 token', () => { it('should fail if ERC721 is not the same ERC721 token', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA), assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA),
assetDataUtils.encodeERC721AssetData(tokenB, tokenIdA), assetDataUtils.encodeERC721AssetData(tokenB, tokenIdA),
); );
expect(isEquivalent).to.be.false(); expect(isEquivalent).to.be.false();
}); });
it('should fail if ERC721 is not the same ERC721 id', () => { it('should fail if ERC721 is not the same ERC721 id', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA), assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA),
assetDataUtils.encodeERC721AssetData(tokenA, tokenIdB), assetDataUtils.encodeERC721AssetData(tokenA, tokenIdB),
); );
expect(isEquivalent).to.be.false(); expect(isEquivalent).to.be.false();
}); });
it('should fail if ERC721 is compared with ERC20', () => { it('should fail if ERC721 is compared with ERC20', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA), assetDataUtils.encodeERC721AssetData(tokenA, tokenIdA),
assetDataUtils.encodeERC20AssetData(tokenA), assetDataUtils.encodeERC20AssetData(tokenA),
); );
@@ -91,49 +91,49 @@ describe('utils', () => {
const valueA = new BigNumber(1); const valueA = new BigNumber(1);
const valueB = new BigNumber(2); const valueB = new BigNumber(2);
it('should succeed if ERC1155 is the same ERC1155 token and id', () => { it('should succeed if ERC1155 is the same ERC1155 token and id', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA], [valueA], NULL_BYTES), assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA], [valueA], NULL_BYTES),
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA], [valueA], NULL_BYTES), assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA], [valueA], NULL_BYTES),
); );
expect(isEquivalent).to.be.true(); expect(isEquivalent).to.be.true();
}); });
it('should succeed if ERC1155 is the same ERC1155 token and ids', () => { it('should succeed if ERC1155 is the same ERC1155 token and ids', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES), assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES),
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES), assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES),
); );
expect(isEquivalent).to.be.true(); expect(isEquivalent).to.be.true();
}); });
it('should succeed if ERC1155 is the same ERC1155 token and ids in different orders', () => { it('should succeed if ERC1155 is the same ERC1155 token and ids in different orders', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES), assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES),
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES), assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES),
); );
expect(isEquivalent).to.be.true(); expect(isEquivalent).to.be.true();
}); });
it('should succeed if ERC1155 is the same ERC1155 token and ids with different callback data', () => { it('should succeed if ERC1155 is the same ERC1155 token and ids with different callback data', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES), assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES),
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], tokenA), assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], tokenA),
); );
expect(isEquivalent).to.be.true(); expect(isEquivalent).to.be.true();
}); });
it('should fail if ERC1155 contains different ids', () => { it('should fail if ERC1155 contains different ids', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES), assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES),
assetDataUtils.encodeERC1155AssetData(tokenB, [tokenIdA], [valueB], NULL_BYTES), assetDataUtils.encodeERC1155AssetData(tokenB, [tokenIdA], [valueB], NULL_BYTES),
); );
expect(isEquivalent).to.be.false(); expect(isEquivalent).to.be.false();
}); });
it('should fail if ERC1155 is a different ERC1155 token', () => { it('should fail if ERC1155 is a different ERC1155 token', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES), assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], NULL_BYTES),
assetDataUtils.encodeERC1155AssetData(tokenB, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES), assetDataUtils.encodeERC1155AssetData(tokenB, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES),
); );
expect(isEquivalent).to.be.false(); expect(isEquivalent).to.be.false();
}); });
it('should fail if expected ERC1155 has different callback data', () => { it('should fail if expected ERC1155 has different callback data', () => {
const isEquivalent = utils.isAssetDataEquivalent( const isEquivalent = isAssetDataEquivalent(
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], tokenA), assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdB, tokenIdA], [valueB, valueA], tokenA),
assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES), assetDataUtils.encodeERC1155AssetData(tokenA, [tokenIdA, tokenIdB], [valueA, valueB], NULL_BYTES),
); );

View File

@@ -9,6 +9,10 @@
{ {
"note": "Fix ERC721 asset support", "note": "Fix ERC721 asset support",
"pr": 2491 "pr": 2491
},
{
"note": "Remove `slippagePercentage` SwapQuoter config.",
"pr": 2513
} }
] ]
}, },

View File

@@ -30,8 +30,6 @@ export const ONE_SECOND_MS = 1000;
export const ONE_MINUTE_MS = ONE_SECOND_MS * 60; export const ONE_MINUTE_MS = ONE_SECOND_MS * 60;
export const GIT_SHA = process.env.GIT_SHA; export const GIT_SHA = process.env.GIT_SHA;
export const NODE_ENV = process.env.NODE_ENV; export const NODE_ENV = process.env.NODE_ENV;
export const ERC20_SWAP_QUOTE_SLIPPAGE_PERCENTAGE = 0.2;
export const ERC721_SWAP_QUOTE_SLIPPAGE_PERCENTAGE = 0;
export const NPM_PACKAGE_VERSION = process.env.NPM_PACKAGE_VERSION; export const NPM_PACKAGE_VERSION = process.env.NPM_PACKAGE_VERSION;
export const DEFAULT_UNKOWN_ASSET_NAME = '???'; export const DEFAULT_UNKOWN_ASSET_NAME = '???';
export const ACCOUNT_UPDATE_INTERVAL_TIME_MS = ONE_SECOND_MS * 5; export const ACCOUNT_UPDATE_INTERVAL_TIME_MS = ONE_SECOND_MS * 5;

View File

@@ -4,7 +4,6 @@ import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper'; import { Web3Wrapper } from '@0x/web3-wrapper';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { ERC20_SWAP_QUOTE_SLIPPAGE_PERCENTAGE, ERC721_SWAP_QUOTE_SLIPPAGE_PERCENTAGE } from '../constants';
import { Action, actions } from '../redux/actions'; import { Action, actions } from '../redux/actions';
import { Asset, QuoteFetchOrigin } from '../types'; import { Asset, QuoteFetchOrigin } from '../types';
@@ -37,10 +36,6 @@ export const swapQuoteUpdater = {
} }
const wethAssetData = await swapQuoter.getEtherTokenAssetDataOrThrowAsync(); const wethAssetData = await swapQuoter.getEtherTokenAssetDataOrThrowAsync();
let newSwapQuote: MarketBuySwapQuote | undefined; let newSwapQuote: MarketBuySwapQuote | undefined;
const slippagePercentage =
asset.metaData.assetProxyId === AssetProxyId.ERC20
? ERC20_SWAP_QUOTE_SLIPPAGE_PERCENTAGE
: ERC721_SWAP_QUOTE_SLIPPAGE_PERCENTAGE;
try { try {
const gasInfo = await gasPriceEstimator.getGasInfoAsync(); const gasInfo = await gasPriceEstimator.getGasInfoAsync();
newSwapQuote = await swapQuoter.getMarketBuySwapQuoteForAssetDataAsync( newSwapQuote = await swapQuoter.getMarketBuySwapQuoteForAssetDataAsync(
@@ -48,7 +43,6 @@ export const swapQuoteUpdater = {
wethAssetData, wethAssetData,
baseUnitValue, baseUnitValue,
{ {
slippagePercentage,
gasPrice: gasInfo.gasPriceInWei, gasPrice: gasInfo.gasPriceInWei,
// Only use native orders // Only use native orders
// excludedSources: [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber, ERC20BridgeSource.Uniswap], // excludedSources: [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber, ERC20BridgeSource.Uniswap],