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
commit baf6372179
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1364 additions and 1388 deletions

View File

@ -5,6 +5,26 @@
{
"note": "Add support for private liquidity providers",
"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,
} from './types';
import { constants as marketOperationUtilConstants } from './utils/market_operation_utils/constants';
import { ERC20BridgeSource } from './utils/market_operation_utils/types';
import { DEFAULT_GET_MARKET_ORDERS_OPTS } from './utils/market_operation_utils/constants';
const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info';
const NULL_BYTES = '0x';
@ -43,7 +42,7 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
orderRefreshIntervalMs: 10000, // 10 seconds
},
...DEFAULT_ORDER_PRUNER_OPTS,
samplerGasLimit: 59e6,
samplerGasLimit: 250e6,
};
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_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = {
...{
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',
],
},
...DEFAULT_GET_MARKET_ORDERS_OPTS,
};
export const constants = {
@ -123,5 +81,4 @@ export const constants = {
PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS,
MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE,
BRIDGE_ASSET_DATA_PREFIX: '0xdc1600f3',
DEFAULT_CURVE_OPTS,
};

View File

@ -22,7 +22,7 @@ import {
import { assert } from './utils/assert';
import { calculateLiquidity } from './utils/calculate_liquidity';
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 { orderPrunerUtils } from './utils/order_prune_utils';
import { OrderStateUtils } from './utils/order_state_utils';
@ -242,11 +242,7 @@ export class SwapQuoter {
): Promise<Array<MarketBuySwapQuote | undefined>> {
makerAssetBuyAmount.map((a, i) => assert.isBigNumber(`makerAssetBuyAmount[${i}]`, a));
let gasPrice: BigNumber;
const { slippagePercentage, ...calculateSwapQuoteOpts } = _.merge(
{},
constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
options,
);
const calculateSwapQuoteOpts = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options);
if (!!options.gasPrice) {
gasPrice = options.gasPrice;
assert.isBigNumber('gasPrice', gasPrice);
@ -264,7 +260,7 @@ export class SwapQuoter {
);
if (prunedOrders.length === 0) {
return [
dummyOrderUtils.createDummyOrderForSampler(
createDummyOrderForSampler(
makerAssetDatas[i],
takerAssetData,
this._contractAddresses.uniswapBridge,
@ -278,7 +274,6 @@ export class SwapQuoter {
const swapQuotes = await this._swapQuoteCalculator.calculateBatchMarketBuySwapQuoteAsync(
allPrunedOrders,
makerAssetBuyAmount,
slippagePercentage,
gasPrice,
calculateSwapQuoteOpts,
);
@ -517,14 +512,9 @@ export class SwapQuoter {
marketOperation: MarketOperation,
options: Partial<SwapQuoteRequestOpts>,
): Promise<SwapQuote> {
const { slippagePercentage, ...calculateSwapQuoteOpts } = _.merge(
{},
constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS,
options,
);
const calculateSwapQuoteOpts = _.merge({}, constants.DEFAULT_SWAP_QUOTE_REQUEST_OPTS, options);
assert.isString('makerAssetData', makerAssetData);
assert.isString('takerAssetData', takerAssetData);
assert.isNumber('slippagePercentage', slippagePercentage);
let gasPrice: BigNumber;
if (!!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 (prunedOrders.length === 0) {
prunedOrders = [
dummyOrderUtils.createDummyOrderForSampler(
makerAssetData,
takerAssetData,
this._contractAddresses.uniswapBridge,
),
createDummyOrderForSampler(makerAssetData, takerAssetData, this._contractAddresses.uniswapBridge),
];
}
@ -551,7 +537,6 @@ export class SwapQuoter {
swapQuote = await this._swapQuoteCalculator.calculateMarketBuySwapQuoteAsync(
prunedOrders,
assetFillAmount,
slippagePercentage,
gasPrice,
calculateSwapQuoteOpts,
);
@ -559,7 +544,6 @@ export class SwapQuoter {
swapQuote = await this._swapQuoteCalculator.calculateMarketSellSwapQuoteAsync(
prunedOrders,
assetFillAmount,
slippagePercentage,
gasPrice,
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).
* 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.
* gas: Amount of estimated gas needed to fill the quote.
*/
export interface SwapQuoteInfo {
feeTakerAssetAmount: BigNumber;
@ -176,6 +177,7 @@ export interface SwapQuoteInfo {
totalTakerAssetAmount: BigNumber;
makerAssetAmount: 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
*/
export interface SwapQuoteRequestOpts extends CalculateSwapQuoteOpts {
slippagePercentage: number;
gasPrice?: BigNumber;
}

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { BigNumber } from '@0x/utils';
import { ERC20BridgeSource, GetMarketOrdersOpts } from './types';
const INFINITE_TIMESTAMP_SEC = new BigNumber(2524604400);
// tslint:disable: custom-no-magic-numbers
/**
* 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
runLimit: 2 ** 15,
excludedSources: [],
bridgeSlippage: 0.0005,
dustFractionThreshold: 0.0025,
bridgeSlippage: 0.005,
maxFallbackSlippage: 0.05,
numSamples: 13,
noConflicts: true,
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 constants = {
INFINITE_TIMESTAMP_SEC,
SELL_SOURCES,
BUY_SOURCES,
DEFAULT_GET_MARKET_ORDERS_OPTS,
ERC20_PROXY_ID: '0xf47261b0',
FEE_QUOTE_SOURCES,
WALLET_SIGNATURE: '0x04',
ONE_ETHER: new BigNumber(1e18),
/**
* Mainnet Curve configuration
*/
export 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 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 { assetDataUtils, ERC20AssetData, orderCalculationUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber, NULL_ADDRESS } from '@0x/utils';
import { constants } from '../../constants';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import { fillableAmountsUtils } from '../fillable_amounts_utils';
import { MarketOperation } from '../../types';
import { difference } from '../utils';
import { constants as marketOperationUtilConstants } from './constants';
import { CreateOrderUtils } from './create_order';
import { comparePathOutputs, FillsOptimizer, getPathAdjustedOutput, sortFillsByAdjustedRate } from './fill_optimizer';
import { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, FEE_QUOTE_SOURCES, ONE_ETHER, SELL_SOURCES } from './constants';
import {
createFillPaths,
getFallbackSourcePaths,
getPathAdjustedRate,
getPathAdjustedSlippage,
getPathSize,
} from './fills';
import { createOrdersFromPath, createSignedOrdersWithFillableAmounts, getNativeOrderTokens } from './orders';
import { findOptimalPath } from './path_optimizer';
import { DexOrderSampler, getSampleAmounts } from './sampler';
import {
AggregationError,
CollapsedFill,
DexSample,
ERC20BridgeSource,
Fill,
FillData,
FillFlags,
GetMarketOrdersOpts,
NativeCollapsedFill,
NativeFillData,
OptimizedMarketOrder,
OrderDomain,
} 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 {
private readonly _createOrderUtils: CreateOrderUtils;
private readonly _wethAddress: string;
constructor(
private readonly _sampler: DexOrderSampler,
contractAddresses: ContractAddresses,
private readonly contractAddresses: ContractAddresses,
private readonly _orderDomain: OrderDomain,
private readonly _liquidityProviderRegistry: string = NULL_ADDRESS,
) {
this._createOrderUtils = new CreateOrderUtils(contractAddresses);
this._wethAddress = contractAddresses.etherToken;
this._wethAddress = contractAddresses.etherToken.toLowerCase();
}
/**
@ -68,34 +54,34 @@ export class MarketOperationUtils {
if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders);
}
const _opts = {
...DEFAULT_GET_MARKET_ORDERS_OPTS,
...opts,
};
const [makerToken, takerToken] = getOrderTokens(nativeOrders[0]);
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
// Call the sampler contract.
const [
fillableAmounts,
orderFillableAmounts,
liquidityProviderAddress,
ethToMakerAssetRate,
dexQuotes,
] = await this._sampler.executeAsync(
// Get native order fillable amounts.
DexOrderSampler.ops.getOrderFillableTakerAmounts(nativeOrders),
// Get the custom liquidity provider from registry.
DexOrderSampler.ops.getLiquidityProviderFromRegistry(
this._liquidityProviderRegistry,
takerToken,
makerToken,
takerToken,
),
makerToken.toLowerCase() === this._wethAddress.toLowerCase()
? DexOrderSampler.ops.constant(new BigNumber(1))
: DexOrderSampler.ops.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources).concat(
this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
),
makerToken,
this._wethAddress,
ONE_ETHER,
this._liquidityProviderRegistry,
),
// Get ETH -> maker token price.
DexOrderSampler.ops.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources).concat(
this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
),
makerToken,
this._wethAddress,
ONE_ETHER,
this._liquidityProviderRegistry,
),
// Get sell quotes for taker -> maker.
DexOrderSampler.ops.getSellQuotes(
difference(SELL_SOURCES, _opts.excludedSources).concat(
this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
@ -106,50 +92,22 @@ export class MarketOperationUtils {
this._liquidityProviderRegistry,
),
);
const nativeOrdersWithFillableAmounts = createSignedOrdersWithFillableAmounts(
return this._generateOptimizedOrders({
orderFillableAmounts,
nativeOrders,
fillableAmounts,
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,
dexQuotes,
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) {
throw new Error(AggregationError.EmptyOrders);
}
const _opts = {
...DEFAULT_GET_MARKET_ORDERS_OPTS,
...opts,
};
const [makerToken, takerToken] = getOrderTokens(nativeOrders[0]);
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
// Call the sampler contract.
const [
fillableAmounts,
orderFillableAmounts,
liquidityProviderAddress,
ethToTakerAssetRate,
dexQuotes,
] = await this._sampler.executeAsync(
// Get native order fillable amounts.
DexOrderSampler.ops.getOrderFillableMakerAmounts(nativeOrders),
// Get the custom liquidity provider from registry.
DexOrderSampler.ops.getLiquidityProviderFromRegistry(
this._liquidityProviderRegistry,
takerToken,
makerToken,
takerToken,
),
takerToken.toLowerCase() === this._wethAddress.toLowerCase()
? DexOrderSampler.ops.constant(new BigNumber(1))
: DexOrderSampler.ops.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources).concat(
this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
),
takerToken,
this._wethAddress,
ONE_ETHER,
this._liquidityProviderRegistry,
),
// Get ETH -> taker token price.
DexOrderSampler.ops.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources).concat(
this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
),
takerToken,
this._wethAddress,
ONE_ETHER,
this._liquidityProviderRegistry,
),
// Get buy quotes for taker -> maker.
DexOrderSampler.ops.getBuyQuotes(
difference(BUY_SOURCES, _opts.excludedSources).concat(
this._liquidityProviderSourceIfAvailable(_opts.excludedSources),
@ -206,19 +164,23 @@ export class MarketOperationUtils {
this._liquidityProviderRegistry,
),
);
const signedOrderWithFillableAmounts = this._createBuyOrdersPathFromSamplerResultIfExists(
return this._generateOptimizedOrders({
orderFillableAmounts,
nativeOrders,
makerAmount,
fillableAmounts,
dexQuotes,
ethToTakerAssetRate,
_opts,
liquidityProviderAddress,
);
if (!signedOrderWithFillableAmounts) {
throw new Error(AggregationError.NoOptimalPath);
}
return signedOrderWithFillableAmounts;
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,
});
}
/**
@ -240,10 +202,7 @@ export class MarketOperationUtils {
if (batchNativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders);
}
const _opts = {
...DEFAULT_GET_MARKET_ORDERS_OPTS,
...opts,
};
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const sources = difference(BUY_SOURCES, _opts.excludedSources);
const ops = [
@ -251,32 +210,118 @@ export class MarketOperationUtils {
...batchNativeOrders.map(orders =>
DexOrderSampler.ops.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
getNativeOrderTokens(orders[0])[1],
this._wethAddress,
getOrderTokens(orders[0])[1],
ONE_ETHER,
),
),
...batchNativeOrders.map((orders, i) =>
DexOrderSampler.ops.getBuyQuotes(sources, getOrderTokens(orders[0])[0], getOrderTokens(orders[0])[1], [
makerAmounts[i],
]),
DexOrderSampler.ops.getBuyQuotes(
sources,
getNativeOrderTokens(orders[0])[0],
getNativeOrderTokens(orders[0])[1],
[makerAmounts[i]],
),
),
];
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 batchDexQuotes = executeResults.splice(0, batchNativeOrders.length) as DexSample[][][];
return batchFillableAmounts.map((fillableAmounts, i) =>
this._createBuyOrdersPathFromSamplerResultIfExists(
batchNativeOrders[i],
makerAmounts[i],
fillableAmounts,
batchDexQuotes[i],
batchEthToTakerAssetRate[i],
_opts,
),
);
return batchNativeOrders.map((nativeOrders, i) => {
if (nativeOrders.length === 0) {
throw new Error(AggregationError.EmptyOrders);
}
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
const orderFillableAmounts = batchOrderFillableAmounts[i];
const ethToTakerAssetRate = batchEthToTakerAssetRate[i];
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[] {
@ -285,328 +330,6 @@ export class MarketOperationUtils {
? [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

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

View File

@ -37,9 +37,7 @@ export enum ERC20BridgeSource {
}
// Internal `fillData` field for `Fill` objects.
export interface FillData {
source: ERC20BridgeSource;
}
export interface FillData {}
// `FillData` for native fills.
export interface NativeFillData extends FillData {
@ -59,11 +57,8 @@ export interface DexSample {
* Flags for `Fill` objects.
*/
export enum FillFlags {
SourceNative = 0x1,
SourceUniswap = 0x2,
SourceEth2Dai = 0x4,
SourceKyber = 0x8,
SourceLiquidityPool = 0x10,
ConflictsWithKyber = 0x1,
Kyber = 0x2,
}
/**
@ -72,20 +67,25 @@ export enum FillFlags {
export interface Fill {
// See `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: BigNumber;
// Output fill amount (maker asset amount in a sell, taker asset amount in a buy).
output: BigNumber;
// Output penalty for this fill.
fillPenalty: BigNumber;
// The maker/taker rate.
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.
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
// from paths.
fillData: FillData | NativeFillData;
fillData?: FillData | NativeFillData;
}
/**
@ -138,10 +138,6 @@ export interface GetMarketOrdersOpts {
* Liquidity sources to exclude. Default is none.
*/
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
* nodes to visit. Default is 1024.
@ -156,15 +152,16 @@ export interface GetMarketOrdersOpts {
* Default is 0.0005 (5 basis points).
*/
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.
*/
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.
* 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: { [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 { OrderPrunerPermittedFeeTypes } from '../types';
import { utils } from '../utils/utils';
import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from '../utils/utils';
export const orderPrunerUtils = {
pruneForUsableSignedOrders(
@ -19,9 +19,9 @@ export const orderPrunerUtils = {
((permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.NoFees) &&
order.takerFee.eq(constants.ZERO_AMOUNT)) ||
(permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.TakerDenominatedTakerFee) &&
utils.isOrderTakerFeePayableWithTakerAsset(order)) ||
isOrderTakerFeePayableWithTakerAsset(order)) ||
(permittedOrderFeeTypes.has(OrderPrunerPermittedFeeTypes.MakerDenominatedTakerFee) &&
utils.isOrderTakerFeePayableWithMakerAsset(order)))
isOrderTakerFeePayableWithMakerAsset(order)))
);
});
return result;

View File

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

View File

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

View File

@ -5,103 +5,111 @@ import { Web3Wrapper } from '@0x/web3-wrapper';
import { constants } from '../constants';
// tslint:disable:no-unnecessary-type-assertion
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);
}
// tslint:disable: no-unnecessary-type-assertion completed-docs
// TODO(dekz): Unsupported cases
// ERCXX(token) === MAP(token, staticCall)
// MAP(a, b) === MAP(b, a) === MAP(b, a, staticCall)
return false;
},
isERC20EquivalentAssetData(assetData: AssetData): assetData is ERC20AssetData | ERC20BridgeAssetData {
return assetDataUtils.isERC20TokenAssetData(assetData) || assetDataUtils.isERC20BridgeAssetData(assetData);
},
};
export function 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
});
}
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 * as _ from 'lodash';
import { constants as assetSwapperConstants } from '../src/constants';
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 { DexSample, ERC20BridgeSource } from '../src/utils/market_operation_utils/types';
const { BUY_SOURCES, SELL_SOURCES } = marketOperationUtilConstants;
// tslint:disable: custom-no-magic-numbers
describe('MarketOperationUtils tests', () => {
const CHAIN_ID = 1;
@ -81,8 +78,8 @@ describe('MarketOperationUtils tests', () => {
case UNISWAP_BRIDGE_ADDRESS.toLowerCase():
return ERC20BridgeSource.Uniswap;
case CURVE_BRIDGE_ADDRESS.toLowerCase():
const curveSource = Object.keys(assetSwapperConstants.DEFAULT_CURVE_OPTS).filter(
k => assetData.indexOf(assetSwapperConstants.DEFAULT_CURVE_OPTS[k].curveAddress.slice(2)) !== -1,
const curveSource = Object.keys(DEFAULT_CURVE_OPTS).filter(
k => assetData.indexOf(DEFAULT_CURVE_OPTS[k].curveAddress.slice(2)) !== -1,
);
return curveSource[0] as ERC20BridgeSource;
default:
@ -120,20 +117,21 @@ describe('MarketOperationUtils tests', () => {
chainId: CHAIN_ID,
};
type GetQuotesOperation = (makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => BigNumber[];
function createGetSellQuotesOperationFromRates(rates: Numberish[]): GetQuotesOperation {
return (...args) => {
const fillAmounts = args.pop() as BigNumber[];
return fillAmounts.map((a, i) => a.times(rates[i]).integerValue());
};
}
function createGetBuyQuotesOperationFromRates(rates: Numberish[]): GetQuotesOperation {
return (...args) => {
const fillAmounts = args.pop() as BigNumber[];
return fillAmounts.map((a, i) => a.div(rates[i]).integerValue());
};
function createSamplesFromRates(source: ERC20BridgeSource, inputs: Numberish[], rates: Numberish[]): DexSample[] {
const samples: DexSample[] = [];
inputs.forEach((input, i) => {
const rate = rates[i];
samples.push({
source,
input: new BigNumber(input),
output: new BigNumber(input)
.minus(i === 0 ? 0 : samples[i - 1].input)
.times(rate)
.plus(i === 0 ? 0 : samples[i - 1].output)
.integerValue(),
});
});
return samples;
}
type GetMultipleQuotesOperation = (
@ -146,13 +144,7 @@ describe('MarketOperationUtils tests', () => {
function createGetMultipleSellQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation {
return (sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => {
return sources.map(s =>
fillAmounts.map((a, i) => ({
source: s,
input: a,
output: a.times(rates[s][i]).integerValue(),
})),
);
return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s]));
};
}
@ -180,13 +172,7 @@ describe('MarketOperationUtils tests', () => {
function createGetMultipleBuyQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation {
return (sources: ERC20BridgeSource[], makerToken: string, takerToken: string, fillAmounts: BigNumber[]) => {
return sources.map(s =>
fillAmounts.map((a, i) => ({
source: s,
input: a,
output: a.div(rates[s][i]).integerValue(),
})),
);
return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r))));
};
}
@ -264,22 +250,6 @@ describe('MarketOperationUtils tests', () => {
[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 = {
getOrderFillableTakerAmounts(orders: SignedOrder[]): BigNumber[] {
return orders.map(o => o.takerAssetAmount);
@ -287,12 +257,6 @@ describe('MarketOperationUtils tests', () => {
getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] {
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),
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES),
getMedianSellRate: createGetMedianSellRate(1),
@ -323,17 +287,18 @@ describe('MarketOperationUtils tests', () => {
});
describe('getMarketSellOrdersAsync()', () => {
const FILL_AMOUNT = getRandomInteger(1, 1e18);
const FILL_AMOUNT = new BigNumber('100e18');
const ORDERS = createOrdersFromSellRates(
FILL_AMOUNT,
_.times(NUM_SAMPLES, i => DEFAULT_RATES[ERC20BridgeSource.Native][i]),
);
const DEFAULT_OPTS = {
numSamples: NUM_SAMPLES,
runLimit: 0,
sampleDistributionBase: 1,
bridgeSlippage: 0,
excludedSources: Object.keys(assetSwapperConstants.DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
maxFallbackSlippage: 100,
excludedSources: Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
allowFallback: false,
};
beforeEach(() => {
@ -341,7 +306,7 @@ describe('MarketOperationUtils tests', () => {
});
it('queries `numSamples` samples', async () => {
const numSamples = _.random(1, 16);
const numSamples = _.random(1, NUM_SAMPLES);
let actualNumSamples = 0;
replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts) => {
@ -412,18 +377,6 @@ describe('MarketOperationUtils tests', () => {
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 () => {
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
// 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);
for (const order of improvedOrders) {
const source = getSourceFromAssetData(order.makerAssetData);
const expectedMakerAmount = FILL_AMOUNT.times(_.last(DEFAULT_RATES[source]) as BigNumber);
const expectedMakerAmount = order.fill.totalMakerAssetAmount;
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.Uniswap] = [0.5, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0, 0, 0, 0]; // unused
replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
});
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false },
{ ...DEFAULT_OPTS, numSamples: 4 },
);
const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [
ERC20BridgeSource.Kyber,
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Uniswap,
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 = {};
rates[ERC20BridgeSource.Native] = [0.3, 0.2, 0.1, 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(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true },
{ ...DEFAULT_OPTS, numSamples: 4 },
);
const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [
ERC20BridgeSource.Eth2Dai,
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Native,
ERC20BridgeSource.Native,
];
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);
if (orderSources.includes(ERC20BridgeSource.Kyber)) {
expect(orderSources).to.not.include(ERC20BridgeSource.Uniswap);
expect(orderSources).to.not.include(ERC20BridgeSource.Eth2Dai);
} else {
expect(orderSources).to.not.include(ERC20BridgeSource.Kyber);
}
});
const ETH_TO_MAKER_RATE = 1.5;
@ -555,12 +482,12 @@ describe('MarketOperationUtils tests', () => {
// dropping their effective rates.
const nativeFeeRate = 0.06;
const rates: RatesBySource = {
[ERC20BridgeSource.Native]: [1, 0.99, 0.98, 0.97], // Effectively [0.94, ~0.93, ~0.92, ~0.91]
[ERC20BridgeSource.Native]: [1, 0.99, 0.98, 0.97], // Effectively [0.94, 0.93, 0.92, 0.91]
[ERC20BridgeSource.Uniswap]: [0.96, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Eth2Dai]: [0.95, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
};
const fees = {
const feeSchedule = {
[ERC20BridgeSource.Native]: FILL_AMOUNT.div(4)
.times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE),
@ -572,32 +499,32 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
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 expectedSources = [
ERC20BridgeSource.Native,
ERC20BridgeSource.Uniswap,
ERC20BridgeSource.Eth2Dai,
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 () => {
// Kyber will have the best rates but will have fees,
// dropping its effective rates.
const kyberFeeRate = 0.2;
const uniswapFeeRate = 0.2;
const rates: RatesBySource = {
[ERC20BridgeSource.Native]: [0.95, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Uniswap]: [0.1, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Kyber]: [0.1, 0.1, 0.1, 0.1],
[ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1],
// Effectively [0.8, ~0.5, ~0, ~0]
[ERC20BridgeSource.Kyber]: [1, 0.7, 0.2, 0.2],
[ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2],
};
const fees = {
[ERC20BridgeSource.Kyber]: FILL_AMOUNT.div(4)
.times(kyberFeeRate)
const feeSchedule = {
[ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4)
.times(uniswapFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE),
};
replaceSamplerOps({
@ -607,11 +534,87 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
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 expectedSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber];
expect(orderSources).to.deep.eq(expectedSources);
const 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 () => {
@ -651,7 +654,7 @@ describe('MarketOperationUtils tests', () => {
}),
],
Web3Wrapper.toBaseUnitAmount(10, 18),
{ excludedSources: SELL_SOURCES, numSamples: 4 },
{ excludedSources: SELL_SOURCES, numSamples: 4, bridgeSlippage: 0 },
);
expect(result.length).to.eql(1);
expect(result[0].makerAddress).to.eql(liquidityProviderAddress);
@ -666,22 +669,24 @@ describe('MarketOperationUtils tests', () => {
expect(getSellQuotesParams.sources).contains(ERC20BridgeSource.LiquidityProvider);
expect(getSellQuotesParams.liquidityProviderAddress).is.eql(registryAddress);
expect(getLiquidityProviderParams.registryAddress).is.eql(registryAddress);
expect(getLiquidityProviderParams.makerToken).is.eql(xAsset);
expect(getLiquidityProviderParams.takerToken).is.eql(yAsset);
expect(getLiquidityProviderParams.makerToken).is.eql(yAsset);
expect(getLiquidityProviderParams.takerToken).is.eql(xAsset);
});
});
describe('getMarketBuyOrdersAsync()', () => {
const FILL_AMOUNT = getRandomInteger(1, 1e18);
const FILL_AMOUNT = new BigNumber('100e18');
const ORDERS = createOrdersFromBuyRates(
FILL_AMOUNT,
_.times(NUM_SAMPLES, () => DEFAULT_RATES[ERC20BridgeSource.Native][0]),
);
const DEFAULT_OPTS = {
numSamples: NUM_SAMPLES,
runLimit: 0,
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(() => {
@ -760,26 +765,6 @@ describe('MarketOperationUtils tests', () => {
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 () => {
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
// 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);
for (const order of improvedOrders) {
const source = getSourceFromAssetData(order.makerAssetData);
const expectedTakerAmount = FILL_AMOUNT.div(_.last(DEFAULT_RATES[source]) as BigNumber);
const expectedTakerAmount = order.fill.totalTakerAssetAmount;
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(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512 },
{ ...DEFAULT_OPTS, numSamples: 4 },
);
const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [
@ -852,7 +836,7 @@ describe('MarketOperationUtils tests', () => {
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;
@ -867,7 +851,7 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Eth2Dai]: [0.95, 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)
.times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE),
@ -879,7 +863,7 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, fees },
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
);
const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [
@ -888,7 +872,7 @@ describe('MarketOperationUtils tests', () => {
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 () => {
@ -901,7 +885,7 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2],
[ERC20BridgeSource.Eth2Dai]: [0.92, 0.1, 0.1, 0.1],
};
const fees = {
const feeSchedule = {
[ERC20BridgeSource.Uniswap]: FILL_AMOUNT.div(4)
.times(uniswapFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE),
@ -913,7 +897,7 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
FILL_AMOUNT,
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, fees },
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
);
const orderSources = improvedOrders.map(o => o.fill.source);
const expectedSources = [
@ -921,7 +905,52 @@ describe('MarketOperationUtils tests', () => {
ERC20BridgeSource.Eth2Dai,
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 { BlockchainLifecycle } from '@0x/dev-utils';
import { ContractAddresses, migrateOnceAsync } from '@0x/migrations';
@ -8,8 +12,9 @@ import 'mocha';
import { constants } from '../src/constants';
import { CalculateSwapQuoteOpts, SignedOrderWithFillableAmounts } from '../src/types';
import { DexOrderSampler, MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { constants as marketOperationUtilConstants } from '../src/utils/market_operation_utils/constants';
import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
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 { 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 { DEFAULT_GET_MARKET_ORDERS_OPTS, SELL_SOURCES } = marketOperationUtilConstants;
// Excludes all non native sources
const CALCULATE_SWAP_QUOTE_OPTS: CalculateSwapQuoteOpts = {
...DEFAULT_GET_MARKET_ORDERS_OPTS,
@ -64,10 +67,7 @@ function createSamplerFromSignedOrdersWithFillableAmounts(
);
}
// tslint:disable:max-file-line-count
// 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', () => {
let protocolFeeUtils: ProtocolFeeUtils;
let contractAddresses: ContractAddresses;
@ -905,3 +905,4 @@ describe.skip('swapQuoteCalculator', () => {
});
});
});
*/

View File

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

View File

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

View File

@ -9,6 +9,10 @@
{
"note": "Fix ERC721 asset support",
"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 GIT_SHA = process.env.GIT_SHA;
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 DEFAULT_UNKOWN_ASSET_NAME = '???';
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 { Dispatch } from 'redux';
import { ERC20_SWAP_QUOTE_SLIPPAGE_PERCENTAGE, ERC721_SWAP_QUOTE_SLIPPAGE_PERCENTAGE } from '../constants';
import { Action, actions } from '../redux/actions';
import { Asset, QuoteFetchOrigin } from '../types';
@ -37,10 +36,6 @@ export const swapQuoteUpdater = {
}
const wethAssetData = await swapQuoter.getEtherTokenAssetDataOrThrowAsync();
let newSwapQuote: MarketBuySwapQuote | undefined;
const slippagePercentage =
asset.metaData.assetProxyId === AssetProxyId.ERC20
? ERC20_SWAP_QUOTE_SLIPPAGE_PERCENTAGE
: ERC721_SWAP_QUOTE_SLIPPAGE_PERCENTAGE;
try {
const gasInfo = await gasPriceEstimator.getGasInfoAsync();
newSwapQuote = await swapQuoter.getMarketBuySwapQuoteForAssetDataAsync(
@ -48,7 +43,6 @@ export const swapQuoteUpdater = {
wethAssetData,
baseUnitValue,
{
slippagePercentage,
gasPrice: gasInfo.gasPriceInWei,
// Only use native orders
// excludedSources: [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber, ERC20BridgeSource.Uniswap],