chore: FillAdjustor and clean up JS router and unused functions [TKR-403] (#480)

* Remove old JS router and add a FillAdjustor

Clean up JS router and unused functions

Remove more unused functions, add adjustment of fills

Comment on why we use fill over sample

update CODEOWNERS

lint

Clean up Fill removing unused properties

Remove CollapsedFills, omit flags bigint

Create GasSchedule vs FeeSchedule, return Fill and gas on OptimizedOrder

Use Fill Adjustment in Phase2 of routing

Fix Limit orders being treated as VIP

* Fix case where dex liquidity is empty

* Use best gas adjusted pricing for fee sources

* CHANGELOG
This commit is contained in:
Jacob Evans 2022-06-29 18:10:56 +10:00 committed by GitHub
parent 2aadbda527
commit ee985240fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 709 additions and 1171 deletions

View File

@ -1,18 +1,20 @@
# See https://help.github.com/articles/about-codeowners/ # See https://help.github.com/articles/about-codeowners/
# for more info about CODEOWNERS file # for more info about CODEOWNERS file
# It uses the same pattern rule for gitignore file # It uses the same pattern rule for gitignore file
# https://git-scm.com/docs/gitignore#_pattern_format # https://git-scm.com/docs/gitignore#_pattern_format
# Website packages/asset-swapper/ @dekz @mzhu25 @dextracker @kh-chang
packages/asset-swapper/ @BMillman19 @fragosti @dave4506
packages/instant/ @BMillman19 @fragosti @dave4506
# Dev tools & setup # Dev tools & setup
.circleci/ @dorothy-zbornak
packages/contract-addresses/ @abandeali1 .circleci/ @dekz @mzhu25
packages/contract-artifacts/ @abandeali1 packages/contract-addresses/ @dekz @mzhu25 @dextracker @kh-chang
packages/order-utils/ @dorothy-zbornak packages/contract-artifacts/ @dekz @mzhu25
packages/protocol-utils/ @dekz @mzhu25
# Protocol/smart contracts # Protocol/smart contracts
contracts/ @abandeali1 @hysz @dorothy-zbornak @mzhu25
contracts/ @dekz @mzhu25 @dextracker

View File

@ -1,4 +1,17 @@
[ [
{
"version": "16.63.0",
"changes": [
{
"note": "Remove JS router",
"pr": 480
},
{
"note": "Removed Median price in favour of best gas adjusted price",
"pr": 480
}
]
},
{ {
"version": "16.62.2", "version": "16.62.2",
"changes": [ "changes": [

View File

@ -40,7 +40,7 @@
"config": { "config": {
"publicInterfaceContracts": "ERC20BridgeSampler,BalanceChecker,FakeTaker", "publicInterfaceContracts": "ERC20BridgeSampler,BalanceChecker,FakeTaker",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./test/generated-artifacts/@(ApproximateBuys|BalanceChecker|BalancerSampler|BalancerV2BatchSampler|BalancerV2Common|BalancerV2Sampler|BancorSampler|BancorV3Sampler|CompoundSampler|CurveSampler|DODOSampler|DODOV2Sampler|ERC20BridgeSampler|FakeTaker|GMXSampler|IBalancer|IBalancerV2Vault|IBancor|IBancorV3|ICurve|IGMX|IMStable|IMooniswap|IMultiBridge|IPlatypus|IShell|IUniswapExchangeQuotes|IUniswapV2Router01|KyberDmmSampler|LidoSampler|LiquidityProviderSampler|MStableSampler|MakerPSMSampler|MooniswapSampler|NativeOrderSampler|PlatypusSampler|SamplerUtils|ShellSampler|SmoothySampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler|UniswapV3Sampler|UtilitySampler|VelodromeSampler).json", "abis": "./test/generated-artifacts/@(ApproximateBuys|BalanceChecker|BalancerSampler|BalancerV2BatchSampler|BalancerV2Common|BalancerV2Sampler|BancorSampler|BancorV3Sampler|CompoundSampler|CurveSampler|DODOSampler|DODOV2Sampler|ERC20BridgeSampler|FakeTaker|GMXSampler|IBalancer|IBalancerV2Vault|IBancor|IBancorV3|ICurve|IGMX|IMStable|IMooniswap|IMultiBridge|IPlatypus|IShell|IUniswapExchangeQuotes|IUniswapV2Router01|KyberDmmSampler|LidoSampler|LiquidityProviderSampler|MStableSampler|MakerPSMSampler|MooniswapSampler|NativeOrderSampler|PlatypusSampler|SamplerUtils|ShellSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler|UniswapV3Sampler|UtilitySampler|VelodromeSampler).json",
"postpublish": { "postpublish": {
"assets": [] "assets": []
} }

View File

@ -4,7 +4,7 @@ export {
ContractTxFunctionObj, ContractTxFunctionObj,
SendTransactionOpts, SendTransactionOpts,
} from '@0x/base-contract'; } from '@0x/base-contract';
export { ContractAddresses } from '@0x/contract-addresses'; export { ContractAddresses, ChainId, getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
export { export {
V4RFQFirmQuote, V4RFQFirmQuote,
V4RFQIndicativeQuote, V4RFQIndicativeQuote,
@ -132,6 +132,7 @@ export {
BUY_SOURCE_FILTER_BY_CHAIN_ID, BUY_SOURCE_FILTER_BY_CHAIN_ID,
SELL_SOURCE_FILTER_BY_CHAIN_ID, SELL_SOURCE_FILTER_BY_CHAIN_ID,
NATIVE_FEE_TOKEN_BY_CHAIN_ID, NATIVE_FEE_TOKEN_BY_CHAIN_ID,
ZERO_AMOUNT,
} from './utils/market_operation_utils/constants'; } from './utils/market_operation_utils/constants';
export { export {
Parameters, Parameters,
@ -141,7 +142,6 @@ export {
export { export {
BalancerFillData, BalancerFillData,
BancorFillData, BancorFillData,
CollapsedFill,
CurveFillData, CurveFillData,
CurveFunctionSelectors, CurveFunctionSelectors,
CurveInfo, CurveInfo,
@ -150,7 +150,9 @@ export {
ERC20BridgeSource, ERC20BridgeSource,
ExchangeProxyOverhead, ExchangeProxyOverhead,
FeeSchedule, FeeSchedule,
GasSchedule,
Fill, Fill,
FillAdjustor,
FillData, FillData,
GetMarketOrdersRfqOpts, GetMarketOrdersRfqOpts,
LiquidityProviderFillData, LiquidityProviderFillData,
@ -159,7 +161,6 @@ export {
MarketDepthSide, MarketDepthSide,
MooniswapFillData, MooniswapFillData,
MultiHopFillData, MultiHopFillData,
NativeCollapsedFill,
NativeRfqOrderFillData, NativeRfqOrderFillData,
NativeLimitOrderFillData, NativeLimitOrderFillData,
NativeFillData, NativeFillData,
@ -168,6 +169,7 @@ export {
TokenAdjacencyGraph, TokenAdjacencyGraph,
UniswapV2FillData, UniswapV2FillData,
} from './utils/market_operation_utils/types'; } from './utils/market_operation_utils/types';
export { IdentityFillAdjustor } from './utils/market_operation_utils/identity_fill_adjustor';
export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; export { ProtocolFeeUtils } from './utils/protocol_fee_utils';
export { export {
BridgeQuoteReportEntry, BridgeQuoteReportEntry,
@ -191,3 +193,5 @@ export type Native = ERC20BridgeSource.Native;
export type MultiHop = ERC20BridgeSource.MultiHop; export type MultiHop = ERC20BridgeSource.MultiHop;
export { rfqtMocker, RfqtQuoteEndpoint } from './utils/rfqt_mocker'; export { rfqtMocker, RfqtQuoteEndpoint } from './utils/rfqt_mocker';
export { adjustOutput } from './utils/market_operation_utils/fills';

View File

@ -282,7 +282,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
// ETH buy/sell is supported // ETH buy/sell is supported
![sellToken, buyToken].includes(NATIVE_FEE_TOKEN_BY_CHAIN_ID[ChainId.Mainnet]) ![sellToken, buyToken].includes(NATIVE_FEE_TOKEN_BY_CHAIN_ID[ChainId.Mainnet])
) { ) {
const fillData = slippedOrders[0].fills[0].fillData as CurveFillData; const fillData = slippedOrders[0].fillData as CurveFillData;
return { return {
calldataHexString: this._exchangeProxy calldataHexString: this._exchangeProxy
.sellToLiquidityProvider( .sellToLiquidityProvider(
@ -311,7 +311,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
this.chainId === ChainId.Mainnet && this.chainId === ChainId.Mainnet &&
isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.Mooniswap]) isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.Mooniswap])
) { ) {
const fillData = slippedOrders[0].fills[0].fillData as MooniswapFillData; const fillData = slippedOrders[0].fillData as MooniswapFillData;
return { return {
calldataHexString: this._exchangeProxy calldataHexString: this._exchangeProxy
.sellToLiquidityProvider( .sellToLiquidityProvider(

View File

@ -33,8 +33,8 @@ import { DexOrderSampler } from './utils/market_operation_utils/sampler';
import { SourceFilters } from './utils/market_operation_utils/source_filters'; import { SourceFilters } from './utils/market_operation_utils/source_filters';
import { import {
ERC20BridgeSource, ERC20BridgeSource,
FeeSchedule,
FillData, FillData,
GasSchedule,
GetMarketOrdersOpts, GetMarketOrdersOpts,
MarketDepth, MarketDepth,
MarketDepthSide, MarketDepthSide,
@ -366,9 +366,11 @@ export class SwapQuoter {
const calcOpts: GetMarketOrdersOpts = { const calcOpts: GetMarketOrdersOpts = {
...cloneOpts, ...cloneOpts,
gasPrice, gasPrice,
feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData: FillData) => feeSchedule: _.mapValues(opts.gasSchedule, gasCost => (fillData: FillData) => {
gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)), const gas = gasCost ? gasCost(fillData) : 0;
), const fee = gasPrice.times(gas);
return { gas, fee };
}),
exchangeProxyOverhead: flags => gasPrice.times(opts.exchangeProxyOverhead(flags)), exchangeProxyOverhead: flags => gasPrice.times(opts.exchangeProxyOverhead(flags)),
}; };
// pass the QuoteRequestor on if rfqt enabled // pass the QuoteRequestor on if rfqt enabled
@ -502,7 +504,7 @@ function createSwapQuote(
operation: MarketOperation, operation: MarketOperation,
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
gasPrice: BigNumber, gasPrice: BigNumber,
gasSchedule: FeeSchedule, gasSchedule: GasSchedule,
slippage: number, slippage: number,
): SwapQuote { ): SwapQuote {
const { const {
@ -562,7 +564,7 @@ function calculateQuoteInfo(
operation: MarketOperation, operation: MarketOperation,
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
gasPrice: BigNumber, gasPrice: BigNumber,
gasSchedule: FeeSchedule, gasSchedule: GasSchedule,
slippage: number, slippage: number,
): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } { ): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } {
const bestCaseFillResult = simulateBestCaseFill({ const bestCaseFillResult = simulateBestCaseFill({
@ -591,25 +593,23 @@ function calculateQuoteInfo(
function calculateTwoHopQuoteInfo( function calculateTwoHopQuoteInfo(
optimizedOrders: OptimizedMarketOrder[], optimizedOrders: OptimizedMarketOrder[],
operation: MarketOperation, operation: MarketOperation,
gasSchedule: FeeSchedule, gasSchedule: GasSchedule,
slippage: number, slippage: number,
): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } { ): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } {
const [firstHopOrder, secondHopOrder] = optimizedOrders; const [firstHopOrder, secondHopOrder] = optimizedOrders;
const [firstHopFill] = firstHopOrder.fills;
const [secondHopFill] = secondHopOrder.fills;
const gas = new BigNumber( const gas = new BigNumber(
gasSchedule[ERC20BridgeSource.MultiHop]!({ gasSchedule[ERC20BridgeSource.MultiHop]!({
firstHopSource: _.pick(firstHopFill, 'source', 'fillData'), firstHopSource: _.pick(firstHopOrder, 'source', 'fillData'),
secondHopSource: _.pick(secondHopFill, 'source', 'fillData'), secondHopSource: _.pick(secondHopOrder, 'source', 'fillData'),
}), }),
).toNumber(); ).toNumber();
const isSell = operation === MarketOperation.Sell; const isSell = operation === MarketOperation.Sell;
return { return {
bestCaseQuoteInfo: { bestCaseQuoteInfo: {
makerAmount: isSell ? secondHopFill.output : secondHopFill.input, makerAmount: isSell ? secondHopOrder.fill.output : secondHopOrder.fill.input,
takerAmount: isSell ? firstHopFill.input : firstHopFill.output, takerAmount: isSell ? firstHopOrder.fill.input : firstHopOrder.fill.output,
totalTakerAmount: isSell ? firstHopFill.input : firstHopFill.output, totalTakerAmount: isSell ? firstHopOrder.fill.input : firstHopOrder.fill.output,
feeTakerTokenAmount: constants.ZERO_AMOUNT, feeTakerTokenAmount: constants.ZERO_AMOUNT,
protocolFeeInWeiAmount: constants.ZERO_AMOUNT, protocolFeeInWeiAmount: constants.ZERO_AMOUNT,
gas, gas,
@ -635,7 +635,7 @@ function calculateTwoHopQuoteInfo(
[ERC20BridgeSource.MultiHop]: { [ERC20BridgeSource.MultiHop]: {
proportion: new BigNumber(1), proportion: new BigNumber(1),
intermediateToken: secondHopOrder.takerToken, intermediateToken: secondHopOrder.takerToken,
hops: [firstHopFill.source, secondHopFill.source], hops: [firstHopOrder.source, secondHopOrder.source],
}, },
}, },
}; };

View File

@ -48,7 +48,7 @@ export function getComparisonPrices(
} else { } else {
try { try {
const fillFeeInEth = new BigNumber( const fillFeeInEth = new BigNumber(
(feeSchedule[ERC20BridgeSource.Native] as FeeEstimate)({ type: FillQuoteTransformerOrderType.Rfq }), (feeSchedule[ERC20BridgeSource.Native] as FeeEstimate)({ type: FillQuoteTransformerOrderType.Rfq }).fee,
); );
const exchangeProxyOverheadInEth = new BigNumber(exchangeProxyOverhead(SOURCE_FLAGS.RfqOrder)); const exchangeProxyOverheadInEth = new BigNumber(exchangeProxyOverhead(SOURCE_FLAGS.RfqOrder));
feeInEth = fillFeeInEth.plus(exchangeProxyOverheadInEth); feeInEth = fillFeeInEth.plus(exchangeProxyOverheadInEth);

View File

@ -5,6 +5,7 @@ import { formatBytes32String } from '@ethersproject/strings';
import { TokenAdjacencyGraphBuilder } from '../token_adjacency_graph_builder'; import { TokenAdjacencyGraphBuilder } from '../token_adjacency_graph_builder';
import { IdentityFillAdjustor } from './identity_fill_adjustor';
import { SourceFilters } from './source_filters'; import { SourceFilters } from './source_filters';
import { import {
AaveV2FillData, AaveV2FillData,
@ -19,6 +20,7 @@ import {
FeeSchedule, FeeSchedule,
FillData, FillData,
FinalUniswapV3FillData, FinalUniswapV3FillData,
GasSchedule,
GeistFillData, GeistFillData,
GetMarketOrdersOpts, GetMarketOrdersOpts,
isFinalUniswapV3FillData, isFinalUniswapV3FillData,
@ -2381,7 +2383,7 @@ const uniswapV2CloneGasSchedule = (fillData?: FillData) => {
* the ethereum transaction cost (21k) * the ethereum transaction cost (21k)
*/ */
// tslint:disable:custom-no-magic-numbers // tslint:disable:custom-no-magic-numbers
export const DEFAULT_GAS_SCHEDULE: Required<FeeSchedule> = { export const DEFAULT_GAS_SCHEDULE: Required<GasSchedule> = {
[ERC20BridgeSource.Native]: fillData => { [ERC20BridgeSource.Native]: fillData => {
// TODO jacob re-order imports so there is no circular rependency with SignedNativeOrder // TODO jacob re-order imports so there is no circular rependency with SignedNativeOrder
const nativeFillData = fillData as { type: FillQuoteTransformerOrderType }; const nativeFillData = fillData as { type: FillQuoteTransformerOrderType };
@ -2569,10 +2571,21 @@ export const DEFAULT_GAS_SCHEDULE: Required<FeeSchedule> = {
[ERC20BridgeSource.Velodrome]: () => 160e3, [ERC20BridgeSource.Velodrome]: () => 160e3,
}; };
export const DEFAULT_FEE_SCHEDULE: Required<FeeSchedule> = { ...DEFAULT_GAS_SCHEDULE }; export const DEFAULT_FEE_SCHEDULE: Required<FeeSchedule> = Object.keys(DEFAULT_GAS_SCHEDULE).reduce((acc, key) => {
acc[key as ERC20BridgeSource] = (fillData: FillData) => {
return {
gas: DEFAULT_GAS_SCHEDULE[key as ERC20BridgeSource](fillData),
fee: ZERO_AMOUNT,
};
};
return acc;
// tslint:disable-next-line:no-object-literal-type-assertion
}, {} as Required<FeeSchedule>);
export const POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS = new BigNumber(20000); export const POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS = new BigNumber(20000);
export const DEFAULT_FEE_ESTIMATE = { gas: 0, fee: ZERO_AMOUNT };
// tslint:enable:custom-no-magic-numbers // tslint:enable:custom-no-magic-numbers
export const DEFAULT_GET_MARKET_ORDERS_OPTS: Omit<GetMarketOrdersOpts, 'gasPrice'> = { export const DEFAULT_GET_MARKET_ORDERS_OPTS: Omit<GetMarketOrdersOpts, 'gasPrice'> = {
@ -2593,4 +2606,5 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: Omit<GetMarketOrdersOpts, 'gasPrice
shouldIncludePriceComparisonsReport: false, shouldIncludePriceComparisonsReport: false,
tokenAdjacencyGraph: { default: [] }, tokenAdjacencyGraph: { default: [] },
neonRouterNumSamples: 14, neonRouterNumSamples: 14,
fillAdjustor: new IdentityFillAdjustor(),
}; };

View File

@ -3,74 +3,17 @@ import { BigNumber, hexUtils } from '@0x/utils';
import { MarketOperation, NativeOrderWithFillableAmounts } from '../../types'; import { MarketOperation, NativeOrderWithFillableAmounts } from '../../types';
import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants'; import { DEFAULT_FEE_ESTIMATE, POSITIVE_INF, SOURCE_FLAGS } from './constants';
import { DexSample, ERC20BridgeSource, FeeSchedule, Fill } from './types'; import { DexSample, ERC20BridgeSource, FeeSchedule, Fill } from './types';
// tslint:disable: prefer-for-of no-bitwise completed-docs // tslint:disable: prefer-for-of no-bitwise completed-docs
/** /**
* Create `Fill` objects from orders and dex quotes. * Converts the ETH value to an amount in output tokens.
*
* By default this prefers the outputAmountPerEth, but if this value
* is zero it will utilize the inputAmountPerEth and input.
*/ */
export function createFills(opts: {
side: MarketOperation;
orders?: NativeOrderWithFillableAmounts[];
dexQuotes?: DexSample[][];
targetInput?: BigNumber;
outputAmountPerEth?: BigNumber;
inputAmountPerEth?: BigNumber;
excludedSources?: ERC20BridgeSource[];
feeSchedule?: FeeSchedule;
}): Fill[][] {
const { side } = opts;
const excludedSources = opts.excludedSources || [];
const feeSchedule = opts.feeSchedule || {};
const orders = opts.orders || [];
const dexQuotes = opts.dexQuotes || [];
const outputAmountPerEth = opts.outputAmountPerEth || ZERO_AMOUNT;
const inputAmountPerEth = opts.inputAmountPerEth || ZERO_AMOUNT;
// Create native fills.
const nativeFills = nativeOrdersToFills(
side,
orders.filter(o => o.fillableTakerAmount.isGreaterThan(0)),
opts.targetInput,
outputAmountPerEth,
inputAmountPerEth,
feeSchedule,
);
// Create DEX fills.
const dexFills = dexQuotes.map(singleSourceSamples =>
dexSamplesToFills(side, singleSourceSamples, outputAmountPerEth, inputAmountPerEth, feeSchedule),
);
return [...dexFills, nativeFills]
.map(p => clipFillsToInput(p, opts.targetInput))
.filter(fills => hasLiquidity(fills) && !excludedSources.includes(fills[0].source));
}
function clipFillsToInput(fills: Fill[], targetInput: BigNumber = POSITIVE_INF): Fill[] {
const clipped: Fill[] = [];
let input = ZERO_AMOUNT;
for (const fill of fills) {
if (input.gte(targetInput)) {
break;
}
input = input.plus(fill.input);
clipped.push(fill);
}
return clipped;
}
function hasLiquidity(fills: Fill[]): boolean {
if (fills.length === 0) {
return false;
}
const totalInput = BigNumber.sum(...fills.map(fill => fill.input));
const totalOutput = BigNumber.sum(...fills.map(fill => fill.output));
if (totalInput.isZero() || totalOutput.isZero()) {
return false;
}
return true;
}
export function ethToOutputAmount({ export function ethToOutputAmount({
input, input,
output, output,
@ -85,29 +28,28 @@ export function ethToOutputAmount({
ethAmount: BigNumber | number; ethAmount: BigNumber | number;
}): BigNumber { }): BigNumber {
return !outputAmountPerEth.isZero() return !outputAmountPerEth.isZero()
? outputAmountPerEth.times(ethAmount) ? outputAmountPerEth.times(ethAmount).integerValue()
: inputAmountPerEth.times(ethAmount).times(output.dividedToIntegerBy(input)); : inputAmountPerEth.times(ethAmount).times(output.dividedToIntegerBy(input));
} }
export function nativeOrdersToFills( export function nativeOrderToFill(
side: MarketOperation, side: MarketOperation,
orders: NativeOrderWithFillableAmounts[], order: NativeOrderWithFillableAmounts,
targetInput: BigNumber = POSITIVE_INF, targetInput: BigNumber = POSITIVE_INF,
outputAmountPerEth: BigNumber, outputAmountPerEth: BigNumber,
inputAmountPerEth: BigNumber, inputAmountPerEth: BigNumber,
fees: FeeSchedule, fees: FeeSchedule,
filterNegativeAdjustedRateOrders: boolean = true, filterNegativeAdjustedRateOrders: boolean = true,
): Fill[] { ): Fill | undefined {
const sourcePathId = hexUtils.random(); const sourcePathId = hexUtils.random();
// Create a single path from all orders. // Create a single path from all orders.
let fills: Array<Fill & { adjustedRate: BigNumber }> = []; const { fillableTakerAmount, fillableTakerFeeAmount, fillableMakerAmount, type } = order;
for (const o of orders) {
const { fillableTakerAmount, fillableTakerFeeAmount, fillableMakerAmount, type } = o;
const makerAmount = fillableMakerAmount; const makerAmount = fillableMakerAmount;
const takerAmount = fillableTakerAmount.plus(fillableTakerFeeAmount); const takerAmount = fillableTakerAmount.plus(fillableTakerFeeAmount);
const input = side === MarketOperation.Sell ? takerAmount : makerAmount; const input = side === MarketOperation.Sell ? takerAmount : makerAmount;
const output = side === MarketOperation.Sell ? makerAmount : takerAmount; const output = side === MarketOperation.Sell ? makerAmount : takerAmount;
const fee = fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(o); const { fee, gas } =
fees[ERC20BridgeSource.Native] === undefined ? DEFAULT_FEE_ESTIMATE : fees[ERC20BridgeSource.Native]!(order);
const outputPenalty = ethToOutputAmount({ const outputPenalty = ethToOutputAmount({
input, input,
output, output,
@ -129,78 +71,63 @@ export function nativeOrdersToFills(
side === MarketOperation.Sell ? adjustedOutput.div(clippedInput) : clippedInput.div(adjustedOutput); side === MarketOperation.Sell ? adjustedOutput.div(clippedInput) : clippedInput.div(adjustedOutput);
// Optionally skip orders with rates that are <= 0. // Optionally skip orders with rates that are <= 0.
if (filterNegativeAdjustedRateOrders && adjustedRate.lte(0)) { if (filterNegativeAdjustedRateOrders && adjustedRate.lte(0)) {
continue; return undefined;
} }
fills.push({
return {
sourcePathId, sourcePathId,
adjustedRate,
adjustedOutput, adjustedOutput,
input: clippedInput, input: clippedInput,
output: clippedOutput, output: clippedOutput,
flags: SOURCE_FLAGS[type === FillQuoteTransformerOrderType.Rfq ? 'RfqOrder' : 'LimitOrder'], flags: SOURCE_FLAGS[type === FillQuoteTransformerOrderType.Rfq ? 'RfqOrder' : 'LimitOrder'],
index: 0, // TBD
parent: undefined, // TBD
source: ERC20BridgeSource.Native, source: ERC20BridgeSource.Native,
type, type,
fillData: { ...o }, fillData: { ...order },
}); gas,
} };
// Sort by descending adjusted rate.
fills = fills.sort((a, b) => b.adjustedRate.comparedTo(a.adjustedRate));
// Re-index fills.
for (let i = 0; i < fills.length; ++i) {
fills[i].parent = i === 0 ? undefined : fills[i - 1];
fills[i].index = i;
}
return fills;
} }
export function dexSamplesToFills( export function dexSampleToFill(
side: MarketOperation, side: MarketOperation,
samples: DexSample[], sample: DexSample,
outputAmountPerEth: BigNumber, outputAmountPerEth: BigNumber,
inputAmountPerEth: BigNumber, inputAmountPerEth: BigNumber,
fees: FeeSchedule, fees: FeeSchedule,
): Fill[] { ): Fill {
const sourcePathId = hexUtils.random(); const sourcePathId = hexUtils.random();
const fills: Fill[] = [];
// Drop any non-zero entries. This can occur if the any fills on Kyber were UniswapReserves
// We need not worry about Kyber fills going to UniswapReserve as the input amount
// we fill is the same as we sampled. I.e we received [0,20,30] output from [1,2,3] input
// and we only fill [2,3] on Kyber (as 1 returns 0 output)
const nonzeroSamples = samples.filter(q => !q.output.isZero());
for (let i = 0; i < nonzeroSamples.length; i++) {
const sample = nonzeroSamples[i];
const prevSample = i === 0 ? undefined : nonzeroSamples[i - 1];
const { source, fillData } = sample; const { source, fillData } = sample;
const input = sample.input.minus(prevSample ? prevSample.input : 0); const input = sample.input;
const output = sample.output.minus(prevSample ? prevSample.output : 0); const output = sample.output;
let penalty = ZERO_AMOUNT; const { fee, gas } =
if (i === 0) { fees[source] === undefined ? DEFAULT_FEE_ESTIMATE : fees[source]!(sample.fillData) || DEFAULT_FEE_ESTIMATE;
const fee = fees[source] === undefined ? 0 : fees[source]!(sample.fillData) || 0;
// Only the first fill in a DEX path incurs a penalty. const penalty = ethToOutputAmount({
penalty = ethToOutputAmount({
input, input,
output, output,
inputAmountPerEth, inputAmountPerEth,
outputAmountPerEth, outputAmountPerEth,
ethAmount: fee, ethAmount: fee,
}); });
}
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
fills.push({ return {
sourcePathId, sourcePathId,
input, input,
output, output,
adjustedOutput, adjustedOutput: adjustOutput(side, output, penalty),
source, source,
fillData, fillData,
type: FillQuoteTransformerOrderType.Bridge, type: FillQuoteTransformerOrderType.Bridge,
index: i,
parent: i !== 0 ? fills[fills.length - 1] : undefined,
flags: SOURCE_FLAGS[source], flags: SOURCE_FLAGS[source],
}); gas,
};
} }
return fills;
/**
* Adjusts the output depending on whether this is a buy or a sell.
*
* If it is a sell, than output is lowered by the adjustment.
* If it is a buy, than output is increased by adjustment.
*/
export function adjustOutput(side: MarketOperation, output: BigNumber, penalty: BigNumber): BigNumber {
return side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
} }

View File

@ -0,0 +1,13 @@
import { BigNumber } from '@0x/utils';
import { MarketOperation } from '../../types';
import { Fill, FillAdjustor } from './types';
// tslint:disable:prefer-function-over-method
export class IdentityFillAdjustor implements FillAdjustor {
public adjustFills(side: MarketOperation, fills: Fill[], amount: BigNumber): Fill[] {
return fills;
}
}

View File

@ -41,19 +41,17 @@ import {
SOURCE_FLAGS, SOURCE_FLAGS,
ZERO_AMOUNT, ZERO_AMOUNT,
} from './constants'; } from './constants';
import { createFills } from './fills'; import { IdentityFillAdjustor } from './identity_fill_adjustor';
import { getBestTwoHopQuote } from './multihop_utils'; import { getBestTwoHopQuote } from './multihop_utils';
import { createOrdersFromTwoHopSample } from './orders'; import { createOrdersFromTwoHopSample } from './orders';
import { Path, PathPenaltyOpts } from './path'; import { Path, PathPenaltyOpts } from './path';
import { findOptimalPathJSAsync, findOptimalRustPathFromSamples } from './path_optimizer'; import { findOptimalPathFromSamples } from './path_optimizer';
import { DexOrderSampler, getSampleAmounts } from './sampler'; import { DexOrderSampler, getSampleAmounts } from './sampler';
import { SourceFilters } from './source_filters'; import { SourceFilters } from './source_filters';
import { import {
AggregationError, AggregationError,
CollapsedFill,
DexSample, DexSample,
ERC20BridgeSource, ERC20BridgeSource,
Fill,
GenerateOptimizedOrdersOpts, GenerateOptimizedOrdersOpts,
GetMarketOrdersOpts, GetMarketOrdersOpts,
MarketSideLiquidity, MarketSideLiquidity,
@ -62,8 +60,6 @@ import {
OrderDomain, OrderDomain,
} from './types'; } from './types';
const SHOULD_USE_RUST_ROUTER = process.env.RUST_ROUTER === 'true';
// tslint:disable:boolean-naming // tslint:disable:boolean-naming
export class MarketOperationUtils { export class MarketOperationUtils {
@ -167,18 +163,20 @@ export class MarketOperationUtils {
// Get native order fillable amounts. // Get native order fillable amounts.
this._sampler.getLimitOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchangeProxy), this._sampler.getLimitOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchangeProxy),
// Get ETH -> maker token price. // Get ETH -> maker token price.
this._sampler.getMedianSellRate( this._sampler.getBestNativeTokenSellRate(
feeSourceFilters.sources, feeSourceFilters.sources,
makerToken, makerToken,
this._nativeFeeToken, this._nativeFeeToken,
this._nativeFeeTokenAmount, this._nativeFeeTokenAmount,
_opts.feeSchedule,
), ),
// Get ETH -> taker token price. // Get ETH -> taker token price.
this._sampler.getMedianSellRate( this._sampler.getBestNativeTokenSellRate(
feeSourceFilters.sources, feeSourceFilters.sources,
takerToken, takerToken,
this._nativeFeeToken, this._nativeFeeToken,
this._nativeFeeTokenAmount, this._nativeFeeTokenAmount,
_opts.feeSchedule,
), ),
// Get sell quotes for taker -> maker. // Get sell quotes for taker -> maker.
this._sampler.getSellQuotes(quoteSourceFilters.sources, makerToken, takerToken, sampleAmounts), this._sampler.getSellQuotes(quoteSourceFilters.sources, makerToken, takerToken, sampleAmounts),
@ -278,18 +276,20 @@ export class MarketOperationUtils {
// Get native order fillable amounts. // Get native order fillable amounts.
this._sampler.getLimitOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchangeProxy), this._sampler.getLimitOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchangeProxy),
// Get ETH -> makerToken token price. // Get ETH -> makerToken token price.
this._sampler.getMedianSellRate( this._sampler.getBestNativeTokenSellRate(
feeSourceFilters.sources, feeSourceFilters.sources,
makerToken, makerToken,
this._nativeFeeToken, this._nativeFeeToken,
this._nativeFeeTokenAmount, this._nativeFeeTokenAmount,
_opts.feeSchedule,
), ),
// Get ETH -> taker token price. // Get ETH -> taker token price.
this._sampler.getMedianSellRate( this._sampler.getBestNativeTokenSellRate(
feeSourceFilters.sources, feeSourceFilters.sources,
takerToken, takerToken,
this._nativeFeeToken, this._nativeFeeToken,
this._nativeFeeTokenAmount, this._nativeFeeTokenAmount,
_opts.feeSchedule,
), ),
// Get buy quotes for taker -> maker. // Get buy quotes for taker -> maker.
this._sampler.getBuyQuotes(quoteSourceFilters.sources, makerToken, takerToken, sampleAmounts), this._sampler.getBuyQuotes(quoteSourceFilters.sources, makerToken, takerToken, sampleAmounts),
@ -384,11 +384,12 @@ export class MarketOperationUtils {
this._sampler.getLimitOrderFillableMakerAmounts(orders, this.contractAddresses.exchangeProxy), this._sampler.getLimitOrderFillableMakerAmounts(orders, this.contractAddresses.exchangeProxy),
), ),
...batchNativeOrders.map(orders => ...batchNativeOrders.map(orders =>
this._sampler.getMedianSellRate( this._sampler.getBestNativeTokenSellRate(
feeSourceFilters.sources, feeSourceFilters.sources,
orders[0].order.takerToken, orders[0].order.takerToken,
this._nativeFeeToken, this._nativeFeeToken,
this._nativeFeeTokenAmount, this._nativeFeeTokenAmount,
_opts.feeSchedule,
), ),
), ),
...batchNativeOrders.map((orders, i) => ...batchNativeOrders.map((orders, i) =>
@ -455,6 +456,7 @@ export class MarketOperationUtils {
allowFallback: _opts.allowFallback, allowFallback: _opts.allowFallback,
gasPrice: _opts.gasPrice, gasPrice: _opts.gasPrice,
neonRouterNumSamples: _opts.neonRouterNumSamples, neonRouterNumSamples: _opts.neonRouterNumSamples,
fillAdjustor: _opts.fillAdjustor,
}, },
); );
return optimizerResult; return optimizerResult;
@ -516,12 +518,9 @@ export class MarketOperationUtils {
const takerAmountPerEth = side === MarketOperation.Sell ? inputAmountPerEth : outputAmountPerEth; const takerAmountPerEth = side === MarketOperation.Sell ? inputAmountPerEth : outputAmountPerEth;
const makerAmountPerEth = side === MarketOperation.Sell ? outputAmountPerEth : inputAmountPerEth; const makerAmountPerEth = side === MarketOperation.Sell ? outputAmountPerEth : inputAmountPerEth;
let fills: Fill[][];
// Find the optimal path using Rust router if enabled, otherwise fallback to JS Router // Find the optimal path using Rust router if enabled, otherwise fallback to JS Router
let optimalPath: Path | undefined; let optimalPath: Path | undefined;
if (SHOULD_USE_RUST_ROUTER) { optimalPath = findOptimalPathFromSamples(
fills = [[]];
optimalPath = findOptimalRustPathFromSamples(
side, side,
dexQuotes, dexQuotes,
[...nativeOrders, ...augmentedRfqtIndicativeQuotes], [...nativeOrders, ...augmentedRfqtIndicativeQuotes],
@ -530,46 +529,27 @@ export class MarketOperationUtils {
opts.feeSchedule, opts.feeSchedule,
this._sampler.chainId, this._sampler.chainId,
opts.neonRouterNumSamples, opts.neonRouterNumSamples,
opts.fillAdjustor,
opts.samplerMetrics, opts.samplerMetrics,
); );
} else {
// Convert native orders and dex quotes into `Fill` objects.
fills = createFills({
side,
orders: [...nativeOrders, ...augmentedRfqtIndicativeQuotes],
dexQuotes,
targetInput: inputAmount,
outputAmountPerEth,
inputAmountPerEth,
excludedSources: opts.excludedSources,
feeSchedule: opts.feeSchedule,
});
optimalPath = await findOptimalPathJSAsync( const optimalPathAdjustedRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT;
side,
fills,
inputAmount,
opts.runLimit,
opts.samplerMetrics,
penaltyOpts,
);
}
const optimalPathRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT; const { adjustedRate: bestTwoHopAdjustedRate, quote: bestTwoHopQuote } = getBestTwoHopQuote(
const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote(
marketSideLiquidity, marketSideLiquidity,
opts.feeSchedule, opts.feeSchedule,
opts.exchangeProxyOverhead, opts.exchangeProxyOverhead,
opts.fillAdjustor,
); );
if (bestTwoHopQuote && bestTwoHopRate.isGreaterThan(optimalPathRate)) {
if (bestTwoHopQuote && bestTwoHopAdjustedRate.isGreaterThan(optimalPathAdjustedRate)) {
const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote, orderOpts); const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote, orderOpts);
return { return {
optimizedOrders: twoHopOrders, optimizedOrders: twoHopOrders,
liquidityDelivered: bestTwoHopQuote, liquidityDelivered: bestTwoHopQuote,
sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop], sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop],
marketSideLiquidity, marketSideLiquidity,
adjustedRate: bestTwoHopRate, adjustedRate: bestTwoHopAdjustedRate,
takerAmountPerEth, takerAmountPerEth,
makerAmountPerEth, makerAmountPerEth,
}; };
@ -580,19 +560,14 @@ export class MarketOperationUtils {
throw new Error(AggregationError.NoOptimalPath); throw new Error(AggregationError.NoOptimalPath);
} }
// Generate a fallback path if required const finalizedPath = optimalPath.finalize(orderOpts);
// TODO(kimpers): Will experiment with disabling this and see how it affects revert rate
// to avoid yet another router roundtrip
// TODO: clean this up if we don't need it
// await this._addOptionalFallbackAsync(side, inputAmount, optimalPath, dexQuotes, fills, opts, penaltyOpts);
const collapsedPath = optimalPath.collapse(orderOpts);
return { return {
optimizedOrders: collapsedPath.orders, optimizedOrders: finalizedPath.orders,
liquidityDelivered: collapsedPath.collapsedFills as CollapsedFill[], liquidityDelivered: finalizedPath.fills,
sourceFlags: collapsedPath.sourceFlags, sourceFlags: finalizedPath.sourceFlags,
marketSideLiquidity, marketSideLiquidity,
adjustedRate: optimalPathRate, adjustedRate: optimalPathAdjustedRate,
takerAmountPerEth, takerAmountPerEth,
makerAmountPerEth, makerAmountPerEth,
}; };
@ -618,6 +593,7 @@ export class MarketOperationUtils {
gasPrice: _opts.gasPrice, gasPrice: _opts.gasPrice,
neonRouterNumSamples: _opts.neonRouterNumSamples, neonRouterNumSamples: _opts.neonRouterNumSamples,
samplerMetrics: _opts.samplerMetrics, samplerMetrics: _opts.samplerMetrics,
fillAdjustor: _opts.fillAdjustor,
}; };
if (nativeOrders.length === 0) { if (nativeOrders.length === 0) {
@ -630,9 +606,15 @@ export class MarketOperationUtils {
? this.getMarketSellLiquidityAsync.bind(this) ? this.getMarketSellLiquidityAsync.bind(this)
: this.getMarketBuyLiquidityAsync.bind(this); : this.getMarketBuyLiquidityAsync.bind(this);
const marketSideLiquidity: MarketSideLiquidity = await marketLiquidityFnAsync(nativeOrders, amount, _opts); const marketSideLiquidity: MarketSideLiquidity = await marketLiquidityFnAsync(nativeOrders, amount, _opts);
// Phase 1 Routing
// We find an optimized path for ALL the DEX and open-orderbook liquidity
let optimizerResult: OptimizerResult | undefined; let optimizerResult: OptimizerResult | undefined;
try { try {
optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, optimizerOpts); optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, {
...optimizerOpts,
fillAdjustor: new IdentityFillAdjustor(),
});
} catch (e) { } catch (e) {
// If no on-chain or off-chain Open Orderbook orders are present, a `NoOptimalPath` will be thrown. // If no on-chain or off-chain Open Orderbook orders are present, a `NoOptimalPath` will be thrown.
// If this happens at this stage, there is still a chance that an RFQ order is fillable, therefore // If this happens at this stage, there is still a chance that an RFQ order is fillable, therefore
@ -656,6 +638,17 @@ export class MarketOperationUtils {
} }
// If RFQ liquidity is enabled, make a request to check RFQ liquidity against the first optimizer result // If RFQ liquidity is enabled, make a request to check RFQ liquidity against the first optimizer result
// Phase 2 Routing
// Mix in any off-chain RFQ quotes
// Apply any fill adjustments i
const phaseTwoOptimizerOpts = {
...optimizerOpts,
// Pass in the FillAdjustor for Phase 2 adjustment, in the future we may perform this adjustment
// in Phase 1.
fillAdjustor: _opts.fillAdjustor,
};
const { rfqt } = _opts; const { rfqt } = _opts;
if ( if (
marketSideLiquidity.isRfqSupported && marketSideLiquidity.isRfqSupported &&
@ -716,8 +709,28 @@ export class MarketOperationUtils {
}); });
// Re-run optimizer with the new indicative quote // Re-run optimizer with the new indicative quote
if (indicativeQuotes.length > 0) { if (indicativeQuotes.length > 0) {
// Attach the indicative quotes to the market side liquidity
marketSideLiquidity.quotes.rfqtIndicativeQuotes = indicativeQuotes; marketSideLiquidity.quotes.rfqtIndicativeQuotes = indicativeQuotes;
optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, optimizerOpts);
// Phase 2 Routing
const phase1OptimalSources = optimizerResult
? optimizerResult.optimizedOrders.map(o => o.source)
: [];
const phase2MarketSideLiquidity: MarketSideLiquidity = {
...marketSideLiquidity,
quotes: {
...marketSideLiquidity.quotes,
// Select only the quotes that were chosen in Phase 1
dexQuotes: marketSideLiquidity.quotes.dexQuotes.filter(
q => q.length > 0 && phase1OptimalSources.includes(q[0].source),
),
},
};
optimizerResult = await this._generateOptimizedOrdersAsync(
phase2MarketSideLiquidity,
phaseTwoOptimizerOpts,
);
} }
} else { } else {
// A firm quote is being requested, and firm quotes price-aware enabled. // A firm quote is being requested, and firm quotes price-aware enabled.
@ -775,6 +788,8 @@ export class MarketOperationUtils {
fillableTakerFeeAmount: ZERO_AMOUNT, fillableTakerFeeAmount: ZERO_AMOUNT,
}), }),
); );
// Attach the firm RFQt quotes to the market side liquidity
marketSideLiquidity.quotes.nativeOrders = [ marketSideLiquidity.quotes.nativeOrders = [
...quotesWithOrderFillableAmounts, ...quotesWithOrderFillableAmounts,
...marketSideLiquidity.quotes.nativeOrders, ...marketSideLiquidity.quotes.nativeOrders,
@ -783,7 +798,27 @@ export class MarketOperationUtils {
// Re-run optimizer with the new firm quote. This is the second and last time // Re-run optimizer with the new firm quote. This is the second and last time
// we run the optimized in a block of code. In this case, we don't catch a potential `NoOptimalPath` exception // we run the optimized in a block of code. In this case, we don't catch a potential `NoOptimalPath` exception
// and we let it bubble up if it happens. // and we let it bubble up if it happens.
optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, optimizerOpts);
// Phase 2 Routing
// Optimization: Filter by what is already currently in the Phase1 output as it doesn't
// seem possible that inclusion of RFQT could impact the sources chosen from Phase 1.
const phase1OptimalSources = optimizerResult
? optimizerResult.optimizedOrders.map(o => o.source)
: [];
const phase2MarketSideLiquidity: MarketSideLiquidity = {
...marketSideLiquidity,
quotes: {
...marketSideLiquidity.quotes,
// Select only the quotes that were chosen in Phase 1
dexQuotes: marketSideLiquidity.quotes.dexQuotes.filter(
q => q.length > 0 && phase1OptimalSources.includes(q[0].source),
),
},
};
optimizerResult = await this._generateOptimizedOrdersAsync(
phase2MarketSideLiquidity,
phaseTwoOptimizerOpts,
);
} }
} }
} }
@ -836,75 +871,6 @@ export class MarketOperationUtils {
}), }),
); );
} }
/*
* TODO(kimpers): Remove this when we know that it's safe to drop the fallbacks on native orders
// tslint:disable-next-line: prefer-function-over-method
private async _addOptionalFallbackAsync(
side: MarketOperation,
inputAmount: BigNumber,
optimalPath: Path,
dexQuotes: DexSample[][],
fills: Fill[][],
opts: GenerateOptimizedOrdersOpts,
penaltyOpts: PathPenaltyOpts,
): Promise<void> {
const maxFallbackSlippage = opts.maxFallbackSlippage || 0;
const optimalPathRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT;
// Generate a fallback path if sources requiring a fallback (fragile) are in the optimal path.
// Native is relatively fragile (limit order collision, expiry, or lack of available maker balance)
// LiquidityProvider is relatively fragile (collision)
const fragileSources = [ERC20BridgeSource.Native, ERC20BridgeSource.LiquidityProvider];
const fragileFills = optimalPath.fills.filter(f => fragileSources.includes(f.source));
if (opts.allowFallback && fragileFills.length !== 0) {
// We create a fallback path that is exclusive of Native liquidity
// This is the optimal on-chain path for the entire input amount
const sturdyPenaltyOpts = {
...penaltyOpts,
exchangeProxyOverhead: (sourceFlags: bigint) =>
// tslint:disable-next-line: no-bitwise
penaltyOpts.exchangeProxyOverhead(sourceFlags | optimalPath.sourceFlags),
};
let sturdyOptimalPath: Path | undefined;
if (SHOULD_USE_RUST_ROUTER) {
const sturdySamples = dexQuotes.filter(
samples => samples.length > 0 && !fragileSources.includes(samples[0].source),
);
sturdyOptimalPath = findOptimalRustPathFromSamples(
side,
sturdySamples,
[],
inputAmount,
sturdyPenaltyOpts,
opts.feeSchedule,
this._sampler.chainId,
opts.neonRouterNumSamples,
undefined, // hack: set sampler metrics to undefined to avoid fallback timings
);
} else {
const sturdyFills = fills.filter(p => p.length > 0 && !fragileSources.includes(p[0].source));
sturdyOptimalPath = await findOptimalPathJSAsync(
side,
sturdyFills,
inputAmount,
opts.runLimit,
undefined, // hack: set sampler metrics to undefined to avoid fallback timings
sturdyPenaltyOpts,
);
}
// Calculate the slippage of on-chain sources compared to the most optimal path
// if within an acceptable threshold we enable a fallback to prevent reverts
if (
sturdyOptimalPath !== undefined &&
(fragileFills.length === optimalPath.fills.length ||
sturdyOptimalPath.adjustedSlippage(optimalPathRate) <= maxFallbackSlippage)
) {
optimalPath.addFallback(sturdyOptimalPath);
}
}
}
*/
} }
// tslint:disable: max-file-line-count // tslint:disable: max-file-line-count

View File

@ -9,6 +9,7 @@ import {
DexSample, DexSample,
ExchangeProxyOverhead, ExchangeProxyOverhead,
FeeSchedule, FeeSchedule,
FillAdjustor,
MarketSideLiquidity, MarketSideLiquidity,
MultiHopFillData, MultiHopFillData,
TokenAdjacencyGraph, TokenAdjacencyGraph,
@ -38,6 +39,7 @@ export function getBestTwoHopQuote(
marketSideLiquidity: Omit<MarketSideLiquidity, 'makerTokenDecimals' | 'takerTokenDecimals'>, marketSideLiquidity: Omit<MarketSideLiquidity, 'makerTokenDecimals' | 'takerTokenDecimals'>,
feeSchedule?: FeeSchedule, feeSchedule?: FeeSchedule,
exchangeProxyOverhead?: ExchangeProxyOverhead, exchangeProxyOverhead?: ExchangeProxyOverhead,
fillAdjustor?: FillAdjustor,
): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } { ): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } {
const { side, inputAmount, outputAmountPerEth, quotes } = marketSideLiquidity; const { side, inputAmount, outputAmountPerEth, quotes } = marketSideLiquidity;
const { twoHopQuotes } = quotes; const { twoHopQuotes } = quotes;
@ -57,7 +59,15 @@ export function getBestTwoHopQuote(
} }
const best = filteredQuotes const best = filteredQuotes
.map(quote => .map(quote =>
getTwoHopAdjustedRate(side, quote, inputAmount, outputAmountPerEth, feeSchedule, exchangeProxyOverhead), getTwoHopAdjustedRate(
side,
quote,
inputAmount,
outputAmountPerEth,
feeSchedule,
exchangeProxyOverhead,
fillAdjustor,
),
) )
.reduce( .reduce(
(prev, curr, i) => (prev, curr, i) =>
@ -70,6 +80,7 @@ export function getBestTwoHopQuote(
outputAmountPerEth, outputAmountPerEth,
feeSchedule, feeSchedule,
exchangeProxyOverhead, exchangeProxyOverhead,
fillAdjustor,
), ),
quote: filteredQuotes[0], quote: filteredQuotes[0],
}, },

View File

@ -1,5 +1,6 @@
import { BridgeProtocol, encodeBridgeSourceId, FillQuoteTransformerOrderType } from '@0x/protocol-utils'; import { BridgeProtocol, encodeBridgeSourceId, FillQuoteTransformerOrderType } from '@0x/protocol-utils';
import { AbiEncoder, BigNumber } from '@0x/utils'; import { AbiEncoder, BigNumber } from '@0x/utils';
import _ = require('lodash');
import { AssetSwapperContractAddresses, MarketOperation } from '../../types'; import { AssetSwapperContractAddresses, MarketOperation } from '../../types';
@ -11,12 +12,12 @@ import {
BalancerV2BatchSwapFillData, BalancerV2BatchSwapFillData,
BalancerV2FillData, BalancerV2FillData,
BancorFillData, BancorFillData,
CollapsedFill,
CompoundFillData, CompoundFillData,
CurveFillData, CurveFillData,
DexSample, DexSample,
DODOFillData, DODOFillData,
ERC20BridgeSource, ERC20BridgeSource,
Fill,
FillData, FillData,
FinalUniswapV3FillData, FinalUniswapV3FillData,
GeistFillData, GeistFillData,
@ -28,7 +29,7 @@ import {
MakerPsmFillData, MakerPsmFillData,
MooniswapFillData, MooniswapFillData,
MultiHopFillData, MultiHopFillData,
NativeCollapsedFill, NativeFillData,
NativeLimitOrderFillData, NativeLimitOrderFillData,
NativeRfqOrderFillData, NativeRfqOrderFillData,
OptimizedMarketBridgeOrder, OptimizedMarketBridgeOrder,
@ -60,23 +61,27 @@ export function createOrdersFromTwoHopSample(
): OptimizedMarketOrder[] { ): OptimizedMarketOrder[] {
const [makerToken, takerToken] = getMakerTakerTokens(opts); const [makerToken, takerToken] = getMakerTakerTokens(opts);
const { firstHopSource, secondHopSource, intermediateToken } = sample.fillData; const { firstHopSource, secondHopSource, intermediateToken } = sample.fillData;
const firstHopFill: CollapsedFill = { const firstHopFill: Fill = {
sourcePathId: '', sourcePathId: '',
source: firstHopSource.source, source: firstHopSource.source,
type: FillQuoteTransformerOrderType.Bridge, type: FillQuoteTransformerOrderType.Bridge,
input: opts.side === MarketOperation.Sell ? sample.input : ZERO_AMOUNT, input: opts.side === MarketOperation.Sell ? sample.input : ZERO_AMOUNT,
output: opts.side === MarketOperation.Sell ? ZERO_AMOUNT : sample.output, output: opts.side === MarketOperation.Sell ? ZERO_AMOUNT : sample.output,
subFills: [], adjustedOutput: opts.side === MarketOperation.Sell ? ZERO_AMOUNT : sample.output,
fillData: firstHopSource.fillData, fillData: firstHopSource.fillData,
flags: BigInt(0),
gas: 1,
}; };
const secondHopFill: CollapsedFill = { const secondHopFill: Fill = {
sourcePathId: '', sourcePathId: '',
source: secondHopSource.source, source: secondHopSource.source,
type: FillQuoteTransformerOrderType.Bridge, type: FillQuoteTransformerOrderType.Bridge,
input: opts.side === MarketOperation.Sell ? MAX_UINT256 : sample.input, input: opts.side === MarketOperation.Sell ? MAX_UINT256 : sample.input,
output: opts.side === MarketOperation.Sell ? sample.output : MAX_UINT256, output: opts.side === MarketOperation.Sell ? sample.output : MAX_UINT256,
subFills: [], adjustedOutput: opts.side === MarketOperation.Sell ? sample.output : MAX_UINT256,
fillData: secondHopSource.fillData, fillData: secondHopSource.fillData,
flags: BigInt(0),
gas: 1,
}; };
return [ return [
createBridgeOrder(firstHopFill, intermediateToken, takerToken, opts.side), createBridgeOrder(firstHopFill, intermediateToken, takerToken, opts.side),
@ -392,68 +397,6 @@ export function createBridgeDataForBridgeOrder(order: OptimizedMarketBridgeOrder
return bridgeData; return bridgeData;
} }
export function createBridgeOrder(
fill: CollapsedFill,
makerToken: string,
takerToken: string,
side: MarketOperation,
): OptimizedMarketBridgeOrder {
const [makerAmount, takerAmount] = getFillTokenAmounts(fill, side);
return {
makerToken,
takerToken,
makerAmount,
takerAmount,
fillData: createFinalBridgeOrderFillDataFromCollapsedFill(fill),
source: fill.source,
sourcePathId: fill.sourcePathId,
type: FillQuoteTransformerOrderType.Bridge,
fills: [fill],
};
}
function createFinalBridgeOrderFillDataFromCollapsedFill(fill: CollapsedFill): FillData {
switch (fill.source) {
case ERC20BridgeSource.UniswapV3: {
const fd = fill.fillData as UniswapV3FillData;
const { uniswapPath, gasUsed } = getBestUniswapV3PathAmountForInputAmount(fd, fill.input);
const finalFillData: FinalUniswapV3FillData = {
router: fd.router,
tokenAddressPath: fd.tokenAddressPath,
uniswapPath,
gasUsed,
};
return finalFillData;
}
default:
break;
}
return fill.fillData;
}
function getBestUniswapV3PathAmountForInputAmount(
fillData: UniswapV3FillData,
inputAmount: BigNumber,
): UniswapV3PathAmount {
if (fillData.pathAmounts.length === 0) {
throw new Error(`No Uniswap V3 paths`);
}
// Find the best path that can satisfy `inputAmount`.
// Assumes `fillData.pathAmounts` is sorted ascending.
for (const pathAmount of fillData.pathAmounts) {
if (pathAmount.inputAmount.gte(inputAmount)) {
return pathAmount;
}
}
return fillData.pathAmounts[fillData.pathAmounts.length - 1];
}
export function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] {
const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken;
const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken;
return [makerToken, takerToken];
}
export const poolEncoder = AbiEncoder.create([{ name: 'poolAddress', type: 'address' }]); export const poolEncoder = AbiEncoder.create([{ name: 'poolAddress', type: 'address' }]);
const curveEncoder = AbiEncoder.create([ const curveEncoder = AbiEncoder.create([
{ name: 'curveAddress', type: 'address' }, { name: 'curveAddress', type: 'address' },
@ -576,7 +519,7 @@ export const BRIDGE_ENCODERS: {
[ERC20BridgeSource.Velodrome]: AbiEncoder.create('(address,bool)'), [ERC20BridgeSource.Velodrome]: AbiEncoder.create('(address,bool)'),
}; };
function getFillTokenAmounts(fill: CollapsedFill, side: MarketOperation): [BigNumber, BigNumber] { function getFillTokenAmounts(fill: Fill, side: MarketOperation): [BigNumber, BigNumber] {
return [ return [
// Maker asset amount. // Maker asset amount.
side === MarketOperation.Sell ? fill.output.integerValue(BigNumber.ROUND_DOWN) : fill.input, side === MarketOperation.Sell ? fill.output.integerValue(BigNumber.ROUND_DOWN) : fill.input,
@ -586,7 +529,7 @@ function getFillTokenAmounts(fill: CollapsedFill, side: MarketOperation): [BigNu
} }
export function createNativeOptimizedOrder( export function createNativeOptimizedOrder(
fill: NativeCollapsedFill, fill: Fill<NativeFillData>,
side: MarketOperation, side: MarketOperation,
): OptimizedMarketOrderBase<NativeLimitOrderFillData> | OptimizedMarketOrderBase<NativeRfqOrderFillData> { ): OptimizedMarketOrderBase<NativeLimitOrderFillData> | OptimizedMarketOrderBase<NativeRfqOrderFillData> {
const fillData = fill.fillData; const fillData = fill.fillData;
@ -598,10 +541,76 @@ export function createNativeOptimizedOrder(
takerToken: fillData.order.takerToken, takerToken: fillData.order.takerToken,
makerAmount, makerAmount,
takerAmount, takerAmount,
fills: [fill],
fillData, fillData,
fill: cleanFillForExport(fill),
}; };
return fill.type === FillQuoteTransformerOrderType.Rfq return fill.type === FillQuoteTransformerOrderType.Rfq
? { ...base, type: FillQuoteTransformerOrderType.Rfq, fillData: fillData as NativeRfqOrderFillData } ? { ...base, type: FillQuoteTransformerOrderType.Rfq, fillData: fillData as NativeRfqOrderFillData }
: { ...base, type: FillQuoteTransformerOrderType.Limit, fillData: fillData as NativeLimitOrderFillData }; : { ...base, type: FillQuoteTransformerOrderType.Limit, fillData: fillData as NativeLimitOrderFillData };
} }
export function createBridgeOrder(
fill: Fill,
makerToken: string,
takerToken: string,
side: MarketOperation,
): OptimizedMarketBridgeOrder {
const [makerAmount, takerAmount] = getFillTokenAmounts(fill, side);
return {
type: FillQuoteTransformerOrderType.Bridge,
source: fill.source,
makerToken,
takerToken,
makerAmount,
takerAmount,
fillData: createFinalBridgeOrderFillDataFromCollapsedFill(fill),
fill: cleanFillForExport(fill),
sourcePathId: fill.sourcePathId,
};
}
function cleanFillForExport(fill: Fill): Fill {
return _.omit(fill, ['flags', 'fillData', 'sourcePathId', 'source', 'type']) as Fill;
}
function createFinalBridgeOrderFillDataFromCollapsedFill(fill: Fill): FillData {
switch (fill.source) {
case ERC20BridgeSource.UniswapV3: {
const fd = fill.fillData as UniswapV3FillData;
const { uniswapPath, gasUsed } = getBestUniswapV3PathAmountForInputAmount(fd, fill.input);
const finalFillData: FinalUniswapV3FillData = {
router: fd.router,
tokenAddressPath: fd.tokenAddressPath,
uniswapPath,
gasUsed,
};
return finalFillData;
}
default:
break;
}
return fill.fillData;
}
function getBestUniswapV3PathAmountForInputAmount(
fillData: UniswapV3FillData,
inputAmount: BigNumber,
): UniswapV3PathAmount {
if (fillData.pathAmounts.length === 0) {
throw new Error(`No Uniswap V3 paths`);
}
// Find the best path that can satisfy `inputAmount`.
// Assumes `fillData.pathAmounts` is sorted ascending.
for (const pathAmount of fillData.pathAmounts) {
if (pathAmount.inputAmount.gte(inputAmount)) {
return pathAmount;
}
}
return fillData.pathAmounts[fillData.pathAmounts.length - 1];
}
export function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] {
const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken;
const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken;
return [makerToken, takerToken];
}

View File

@ -1,4 +1,5 @@
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import _ = require('lodash');
import { MarketOperation } from '../../types'; import { MarketOperation } from '../../types';
@ -6,14 +7,7 @@ import { POSITIVE_INF, ZERO_AMOUNT } from './constants';
import { ethToOutputAmount } from './fills'; import { ethToOutputAmount } from './fills';
import { createBridgeOrder, createNativeOptimizedOrder, CreateOrderFromPathOpts, getMakerTakerTokens } from './orders'; import { createBridgeOrder, createNativeOptimizedOrder, CreateOrderFromPathOpts, getMakerTakerTokens } from './orders';
import { getCompleteRate, getRate } from './rate_utils'; import { getCompleteRate, getRate } from './rate_utils';
import { import { ERC20BridgeSource, ExchangeProxyOverhead, Fill, NativeFillData, OptimizedMarketOrder } from './types';
CollapsedFill,
ERC20BridgeSource,
ExchangeProxyOverhead,
Fill,
NativeCollapsedFill,
OptimizedMarketOrder,
} from './types';
// tslint:disable: prefer-for-of no-bitwise completed-docs // tslint:disable: prefer-for-of no-bitwise completed-docs
@ -37,7 +31,6 @@ export const DEFAULT_PATH_PENALTY_OPTS: PathPenaltyOpts = {
}; };
export class Path { export class Path {
public collapsedFills?: ReadonlyArray<CollapsedFill>;
public orders?: OptimizedMarketOrder[]; public orders?: OptimizedMarketOrder[];
public sourceFlags: bigint = BigInt(0); public sourceFlags: bigint = BigInt(0);
protected _size: PathSize = { input: ZERO_AMOUNT, output: ZERO_AMOUNT }; protected _size: PathSize = { input: ZERO_AMOUNT, output: ZERO_AMOUNT };
@ -57,16 +50,6 @@ export class Path {
return path; return path;
} }
public static clone(base: Path): Path {
const clonedPath = new Path(base.side, base.fills.slice(), base.targetInput, base.pathPenaltyOpts);
clonedPath.sourceFlags = base.sourceFlags;
clonedPath._size = { ...base._size };
clonedPath._adjustedSize = { ...base._adjustedSize };
clonedPath.collapsedFills = base.collapsedFills === undefined ? undefined : base.collapsedFills.slice();
clonedPath.orders = base.orders === undefined ? undefined : base.orders.slice();
return clonedPath;
}
protected constructor( protected constructor(
protected readonly side: MarketOperation, protected readonly side: MarketOperation,
public fills: ReadonlyArray<Fill>, public fills: ReadonlyArray<Fill>,
@ -74,68 +57,33 @@ export class Path {
public readonly pathPenaltyOpts: PathPenaltyOpts, public readonly pathPenaltyOpts: PathPenaltyOpts,
) {} ) {}
public append(fill: Fill): this {
(this.fills as Fill[]).push(fill);
this.sourceFlags |= fill.flags;
this._addFillSize(fill);
return this;
}
/** /**
* Add a fallback path to the current path * Finalizes this path, creating fillable orders with the information required
* Fallback must contain exclusive fills that are * for settlement
* not present in this path
*/ */
public addFallback(fallback: Path): this { public finalize(opts: CreateOrderFromPathOpts): FinalizedPath {
// We pre-pend the sources which have a higher probability of failure
// This allows us to continue on to the remaining fills
// If the "flakey" sources like Native were at the end, we may have a failure
// as the last fill and then either revert, or go back to a source we previously
// filled against
const nativeFills = this.fills.filter(f => f.source === ERC20BridgeSource.Native);
const otherFills = this.fills.filter(f => f.source !== ERC20BridgeSource.Native);
// Map to the unique source id and the index to represent a unique fill
const fillToFillId = (fill: Fill) => `${fill.sourcePathId}${fill.index}`;
const otherFillIds = otherFills.map(f => fillToFillId(f));
this.fills = [
// Append all of the native fills first
...nativeFills,
// Add the other fills that are not native in the optimal path
...otherFills,
// Add the fills to the end that aren't already included
...fallback.fills.filter(f => !otherFillIds.includes(fillToFillId(f))),
];
// Recompute the source flags
this.sourceFlags = this.fills.reduce((flags, fill) => flags | fill.flags, BigInt(0));
return this;
}
public collapse(opts: CreateOrderFromPathOpts): CollapsedPath {
const [makerToken, takerToken] = getMakerTakerTokens(opts); const [makerToken, takerToken] = getMakerTakerTokens(opts);
const collapsedFills = this.collapsedFills === undefined ? this._collapseFills() : this.collapsedFills;
this.orders = []; this.orders = [];
for (let i = 0; i < collapsedFills.length; ) { for (const fill of this.fills) {
if (collapsedFills[i].source === ERC20BridgeSource.Native) { // internal BigInt flag field is not supported JSON and is tricky
this.orders.push(createNativeOptimizedOrder(collapsedFills[i] as NativeCollapsedFill, opts.side)); // to remove upstream. Since it's not needed in a FinalizedPath we just drop it.
++i; const normalizedFill = _.omit(fill, 'flags') as Fill;
continue; if (fill.source === ERC20BridgeSource.Native) {
this.orders.push(createNativeOptimizedOrder(normalizedFill as Fill<NativeFillData>, opts.side));
} else {
this.orders.push(createBridgeOrder(normalizedFill, makerToken, takerToken, opts.side));
} }
this.orders.push(createBridgeOrder(collapsedFills[i], makerToken, takerToken, opts.side));
i += 1;
} }
return this as CollapsedPath; return this as FinalizedPath;
}
public size(): PathSize {
return this._size;
} }
public adjustedSize(): PathSize { public adjustedSize(): PathSize {
// Adjusted input/output has been adjusted by the cost of the DEX, but not by any
// overhead added by the exchange proxy.
const { input, output } = this._adjustedSize; const { input, output } = this._adjustedSize;
const { exchangeProxyOverhead, outputAmountPerEth, inputAmountPerEth } = this.pathPenaltyOpts; const { exchangeProxyOverhead, outputAmountPerEth, inputAmountPerEth } = this.pathPenaltyOpts;
// Calculate the additional penalty from the ways this path can be filled
// by the exchange proxy, e.g VIPs (small) or FillQuoteTransformer (large)
const gasOverhead = exchangeProxyOverhead(this.sourceFlags); const gasOverhead = exchangeProxyOverhead(this.sourceFlags);
const pathPenalty = ethToOutputAmount({ const pathPenalty = ethToOutputAmount({
input, input,
@ -155,6 +103,10 @@ export class Path {
return getCompleteRate(this.side, input, output, this.targetInput); return getCompleteRate(this.side, input, output, this.targetInput);
} }
/**
* Calculates the rate of this path, where the output has been
* adjusted for penalties (e.g cost)
*/
public adjustedRate(): BigNumber { public adjustedRate(): BigNumber {
const { input, output } = this.adjustedSize(); const { input, output } = this.adjustedSize();
return getRate(this.side, input, output); return getRate(this.side, input, output);
@ -171,16 +123,11 @@ export class Path {
return best; return best;
} }
public adjustedSlippage(maxRate: BigNumber): number { /**
if (maxRate.eq(0)) { * Compares two paths returning if this adjusted path
return 0; * is better than the other adjusted path
} */
const totalRate = this.adjustedRate(); public isAdjustedBetterThan(other: Path): boolean {
const rateChange = maxRate.minus(totalRate);
return rateChange.div(maxRate).toNumber();
}
public isBetterThan(other: Path): boolean {
if (!this.targetInput.isEqualTo(other.targetInput)) { if (!this.targetInput.isEqualTo(other.targetInput)) {
throw new Error(`Target input mismatch: ${this.targetInput} !== ${other.targetInput}`); throw new Error(`Target input mismatch: ${this.targetInput} !== ${other.targetInput}`);
} }
@ -192,78 +139,6 @@ export class Path {
} else { } else {
return this.adjustedCompleteRate().isGreaterThan(other.adjustedCompleteRate()); return this.adjustedCompleteRate().isGreaterThan(other.adjustedCompleteRate());
} }
// if (otherInput.isLessThan(targetInput)) {
// return input.isGreaterThan(otherInput);
// } else if (input.isGreaterThanOrEqualTo(targetInput)) {
// return this.adjustedCompleteRate().isGreaterThan(other.adjustedCompleteRate());
// }
// return false;
}
public isComplete(): boolean {
const { input } = this._size;
return input.gte(this.targetInput);
}
public isValid(skipDuplicateCheck: boolean = false): boolean {
for (let i = 0; i < this.fills.length; ++i) {
// Fill must immediately follow its parent.
if (this.fills[i].parent) {
if (i === 0 || this.fills[i - 1] !== this.fills[i].parent) {
return false;
}
}
if (!skipDuplicateCheck) {
// Fill must not be duplicated.
for (let j = 0; j < i; ++j) {
if (this.fills[i] === this.fills[j]) {
return false;
}
}
}
}
return true;
}
public isValidNextFill(fill: Fill): boolean {
if (this.fills.length === 0) {
return !fill.parent;
}
if (this.fills[this.fills.length - 1] === fill.parent) {
return true;
}
if (fill.parent) {
return false;
}
return true;
}
private _collapseFills(): ReadonlyArray<CollapsedFill> {
this.collapsedFills = [];
for (const fill of this.fills) {
const source = fill.source;
if (this.collapsedFills.length !== 0 && source !== ERC20BridgeSource.Native) {
const prevFill = this.collapsedFills[this.collapsedFills.length - 1];
// If the last fill is from the same source, merge them.
if (prevFill.sourcePathId === fill.sourcePathId) {
prevFill.input = prevFill.input.plus(fill.input);
prevFill.output = prevFill.output.plus(fill.output);
prevFill.fillData = fill.fillData;
prevFill.subFills.push(fill);
continue;
}
}
(this.collapsedFills as CollapsedFill[]).push({
sourcePathId: fill.sourcePathId,
source: fill.source,
type: fill.type,
fillData: fill.fillData,
input: fill.input,
output: fill.output,
subFills: [fill],
});
}
return this.collapsedFills;
} }
private _addFillSize(fill: Fill): void { private _addFillSize(fill: Fill): void {
@ -285,7 +160,6 @@ export class Path {
} }
} }
export interface CollapsedPath extends Path { export interface FinalizedPath extends Path {
readonly collapsedFills: ReadonlyArray<CollapsedFill>;
readonly orders: OptimizedMarketOrder[]; readonly orders: OptimizedMarketOrder[];
} }

View File

@ -1,6 +1,7 @@
import { assert } from '@0x/assert'; import { assert } from '@0x/assert';
import { ChainId } from '@0x/contract-addresses'; import { ChainId } from '@0x/contract-addresses';
import { OptimizerCapture, route, SerializedPath } from '@0x/neon-router'; import { OptimizerCapture, route, SerializedPath } from '@0x/neon-router';
import { FillQuoteTransformerOrderType } from '@0x/protocol-utils';
import { BigNumber, hexUtils } from '@0x/utils'; import { BigNumber, hexUtils } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
@ -9,13 +10,12 @@ import { DEFAULT_WARNING_LOGGER } from '../../constants';
import { MarketOperation, NativeOrderWithFillableAmounts } from '../../types'; import { MarketOperation, NativeOrderWithFillableAmounts } from '../../types';
import { VIP_ERC20_BRIDGE_SOURCES_BY_CHAIN_ID, ZERO_AMOUNT } from './constants'; import { VIP_ERC20_BRIDGE_SOURCES_BY_CHAIN_ID, ZERO_AMOUNT } from './constants';
import { dexSamplesToFills, ethToOutputAmount, nativeOrdersToFills } from './fills'; import { dexSampleToFill, ethToOutputAmount, nativeOrderToFill } from './fills';
import { DEFAULT_PATH_PENALTY_OPTS, Path, PathPenaltyOpts } from './path'; import { Path, PathPenaltyOpts } from './path';
import { DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillData, SamplerMetrics } from './types'; import { DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillAdjustor, FillData, SamplerMetrics } from './types';
// tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs no-bitwise // tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs no-bitwise
const RUN_LIMIT_DECAY_FACTOR = 0.5;
// NOTE: The Rust router will panic with less than 3 samples // NOTE: The Rust router will panic with less than 3 samples
const MIN_NUM_SAMPLE_INPUTS = 3; const MIN_NUM_SAMPLE_INPUTS = 3;
@ -45,7 +45,7 @@ function calculateOuputFee(
): BigNumber { ): BigNumber {
if (isDexSample(sampleOrNativeOrder)) { if (isDexSample(sampleOrNativeOrder)) {
const { input, output, source, fillData } = sampleOrNativeOrder; const { input, output, source, fillData } = sampleOrNativeOrder;
const fee = fees[source]?.(fillData) || 0; const fee = fees[source]?.(fillData).fee || ZERO_AMOUNT;
const outputFee = ethToOutputAmount({ const outputFee = ethToOutputAmount({
input, input,
output, output,
@ -56,7 +56,7 @@ function calculateOuputFee(
return outputFee; return outputFee;
} else { } else {
const { input, output } = nativeOrderToNormalizedAmounts(side, sampleOrNativeOrder); const { input, output } = nativeOrderToNormalizedAmounts(side, sampleOrNativeOrder);
const fee = fees[ERC20BridgeSource.Native]?.(sampleOrNativeOrder) || 0; const fee = fees[ERC20BridgeSource.Native]?.(sampleOrNativeOrder).fee || ZERO_AMOUNT;
const outputFee = ethToOutputAmount({ const outputFee = ethToOutputAmount({
input, input,
output, output,
@ -77,6 +77,7 @@ function findRoutesAndCreateOptimalPath(
fees: FeeSchedule, fees: FeeSchedule,
neonRouterNumSamples: number, neonRouterNumSamples: number,
vipSourcesSet: Set<ERC20BridgeSource>, vipSourcesSet: Set<ERC20BridgeSource>,
fillAdjustor: FillAdjustor,
): { allSourcesPath: Path | undefined; vipSourcesPath: Path | undefined } | undefined { ): { allSourcesPath: Path | undefined; vipSourcesPath: Path | undefined } | undefined {
// Currently the rust router is unable to handle 1 base unit sized quotes and will error out // Currently the rust router is unable to handle 1 base unit sized quotes and will error out
// To avoid flooding the logs with these errors we just return an insufficient liquidity error // To avoid flooding the logs with these errors we just return an insufficient liquidity error
@ -85,31 +86,44 @@ function findRoutesAndCreateOptimalPath(
return undefined; return undefined;
} }
const createFill = (sample: DexSample): Fill | undefined => { // Create a `Fill` from a dex sample and adjust it with any passed in
const fills = dexSamplesToFills(side, [sample], opts.outputAmountPerEth, opts.inputAmountPerEth, fees); // adjustor
// NOTE: If the sample has 0 output dexSamplesToFills will return [] because no fill can be created const createFillFromDexSample = (sample: DexSample): Fill => {
if (fills.length === 0) { const fill = dexSampleToFill(side, sample, opts.outputAmountPerEth, opts.inputAmountPerEth, fees);
return undefined; const adjustedFills = fillAdjustor.adjustFills(side, [fill], input);
} return adjustedFills[0];
return fills[0];
}; };
const createPathFromStrategy = (sourcesRustRoute: Float64Array, sourcesOutputAmounts: Float64Array) => { const createPathFromStrategy = (optimalRouteInputs: Float64Array, optimalRouteOutputs: Float64Array) => {
/**
* inputs are the amounts to fill at each source index
* e.g fill 2076 at index 4
* [ 0, 0, 0, 0, 2076, 464, 230,
* 230, 0, 0, 0 ]
* the sum represents the total input amount
*
* outputs are the amounts we expect out at each source index
* [ 0, 0, 0, 0, 42216, 9359, 4677,
* 4674, 0, 0, 0 ]
* the sum represents the total expected output amount
*/
const routesAndSamplesAndOutputs = _.zip( const routesAndSamplesAndOutputs = _.zip(
sourcesRustRoute, optimalRouteInputs,
optimalRouteOutputs,
samplesAndNativeOrdersWithResults, samplesAndNativeOrdersWithResults,
sourcesOutputAmounts,
sampleSourcePathIds, sampleSourcePathIds,
); );
const adjustedFills: Fill[] = []; const adjustedFills: Fill[] = [];
const totalRoutedAmount = BigNumber.sum(...sourcesRustRoute); const totalRoutedAmount = BigNumber.sum(...optimalRouteInputs);
// Due to precision errors we can end up with a totalRoutedAmount that is not exactly equal to the input
const precisionErrorScalar = input.dividedBy(totalRoutedAmount);
const scale = input.dividedBy(totalRoutedAmount);
for (const [ for (const [
routeInput, routeInput,
routeSamplesAndNativeOrders,
outputAmount, outputAmount,
routeSamplesAndNativeOrders,
sourcePathId, sourcePathId,
] of routesAndSamplesAndOutputs) { ] of routesAndSamplesAndOutputs) {
if (!Number.isFinite(outputAmount)) { if (!Number.isFinite(outputAmount)) {
@ -119,26 +133,27 @@ function findRoutesAndCreateOptimalPath(
if (!routeInput || !routeSamplesAndNativeOrders || !outputAmount) { if (!routeInput || !routeSamplesAndNativeOrders || !outputAmount) {
continue; continue;
} }
// TODO(kimpers): [TKR-241] amounts are sometimes clipped in the router due to precision loss for number/f64 // TODO: [TKR-241] amounts are sometimes clipped in the router due to precision loss for number/f64
// we can work around it by scaling it and rounding up. However now we end up with a total amount of a couple base units too much // we can work around it by scaling it and rounding up. However now we end up with a total amount of a couple base units too much
const rustInputAdjusted = BigNumber.min( const routeInputCorrected = BigNumber.min(
new BigNumber(routeInput).multipliedBy(scale).integerValue(BigNumber.ROUND_CEIL), precisionErrorScalar.multipliedBy(routeInput).integerValue(BigNumber.ROUND_CEIL),
input, input,
); );
const current = routeSamplesAndNativeOrders[routeSamplesAndNativeOrders.length - 1]; const current = routeSamplesAndNativeOrders[routeSamplesAndNativeOrders.length - 1];
// If it is a native single order we only have one Input/output
// we want to convert this to an array of samples
if (!isDexSample(current)) { if (!isDexSample(current)) {
const nativeFill = nativeOrdersToFills( const nativeFill = nativeOrderToFill(
side, side,
[current], current,
rustInputAdjusted, routeInputCorrected,
opts.outputAmountPerEth, opts.outputAmountPerEth,
opts.inputAmountPerEth, opts.inputAmountPerEth,
fees, fees,
false, false,
)[0] as Fill | undefined; );
// Note: If the order has an adjusted rate of less than or equal to 0 it will be skipped // Note: If the order has an adjusted rate of less than or equal to 0 it will be undefined
// and nativeFill will be `undefined`
if (nativeFill) { if (nativeFill) {
// NOTE: For Limit/RFQ orders we are done here. No need to scale output // NOTE: For Limit/RFQ orders we are done here. No need to scale output
adjustedFills.push({ ...nativeFill, sourcePathId: sourcePathId ?? hexUtils.random() }); adjustedFills.push({ ...nativeFill, sourcePathId: sourcePathId ?? hexUtils.random() });
@ -147,62 +162,54 @@ function findRoutesAndCreateOptimalPath(
} }
// NOTE: For DexSamples only // NOTE: For DexSamples only
let fill = createFill(current); let fill = createFillFromDexSample(current);
if (!fill) { if (!fill) {
continue; continue;
} }
const routeSamples = routeSamplesAndNativeOrders as Array<DexSample<FillData>>; const routeSamples = routeSamplesAndNativeOrders as Array<DexSample<FillData>>;
// Descend to approach a closer fill for fillData which may not be consistent
// throughout the path (UniswapV3) and for a closer guesstimate at
// gas used
// From the output of the router, find the closest Sample in terms of input.
// The Router may have chosen an amount to fill that we do not have a measured sample of
// Choosing this accurately is required in some sources where the `FillData` may change depending
// on the size of the trade. For example, UniswapV3 has variable gas cost
// which increases with input.
assert.assert(routeSamples.length >= 1, 'Found no sample to use for source'); assert.assert(routeSamples.length >= 1, 'Found no sample to use for source');
for (let k = routeSamples.length - 1; k >= 0; k--) { for (let k = routeSamples.length - 1; k >= 0; k--) {
// If we're at the last remaining sample that's all we have left to use
if (k === 0) { if (k === 0) {
fill = createFill(routeSamples[0]) ?? fill; fill = createFillFromDexSample(routeSamples[0]) ?? fill;
} }
if (rustInputAdjusted.isGreaterThan(routeSamples[k].input)) { if (routeInputCorrected.isGreaterThan(routeSamples[k].input)) {
const left = routeSamples[k]; const left = routeSamples[k];
const right = routeSamples[k + 1]; const right = routeSamples[k + 1];
if (left && right) { if (left && right) {
fill = fill =
createFill({ createFillFromDexSample({
...right, // default to the greater (for gas used) ...right, // default to the greater (for gas used)
input: rustInputAdjusted, input: routeInputCorrected,
output: new BigNumber(outputAmount), output: new BigNumber(outputAmount).integerValue(),
}) ?? fill; }) ?? fill;
} else { } else {
assert.assert(Boolean(left || right), 'No valid sample to use'); assert.assert(Boolean(left || right), 'No valid sample to use');
fill = createFill(left || right) ?? fill; fill = createFillFromDexSample(left || right) ?? fill;
} }
break; break;
} }
} }
// TODO(kimpers): remove once we have solved the rounding/precision loss issues in the Rust router // TODO: remove once we have solved the rounding/precision loss issues in the Rust router
const maxSampledOutput = BigNumber.max(...routeSamples.map(s => s.output)); const maxSampledOutput = BigNumber.max(...routeSamples.map(s => s.output)).integerValue();
// Scale output by scale factor but never go above the largest sample in sell quotes (unknown liquidity) or below 1 base unit (unfillable) // Scale output by scale factor but never go above the largest sample in sell quotes (unknown liquidity) or below 1 base unit (unfillable)
const scaleOutput = (output: BigNumber) => { const scaleOutput = (output: BigNumber) => {
// Don't try to scale 0 output as it will be clamped to 1 const capped = BigNumber.min(output.integerValue(), maxSampledOutput);
if (output.eq(ZERO_AMOUNT)) {
return output;
}
const scaled = output
.times(scale)
.decimalPlaces(0, side === MarketOperation.Sell ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL);
const capped = MarketOperation.Sell ? BigNumber.min(scaled, maxSampledOutput) : scaled;
return BigNumber.max(capped, 1); return BigNumber.max(capped, 1);
}; };
adjustedFills.push({ adjustedFills.push({
...fill, ...fill,
input: rustInputAdjusted, input: routeInputCorrected,
output: scaleOutput(fill.output), output: scaleOutput(fill.output),
adjustedOutput: scaleOutput(fill.adjustedOutput), adjustedOutput: scaleOutput(fill.adjustedOutput),
index: 0,
parent: undefined,
sourcePathId: sourcePathId ?? hexUtils.random(), sourcePathId: sourcePathId ?? hexUtils.random(),
}); });
} }
@ -224,7 +231,6 @@ function findRoutesAndCreateOptimalPath(
continue; continue;
} }
const sourcePathId = hexUtils.random();
const singleSourceSamplesWithOutput = [...singleSourceSamples]; const singleSourceSamplesWithOutput = [...singleSourceSamples];
for (let i = singleSourceSamples.length - 1; i >= 0; i--) { for (let i = singleSourceSamples.length - 1; i >= 0; i--) {
const currentOutput = singleSourceSamples[i].output; const currentOutput = singleSourceSamples[i].output;
@ -240,17 +246,23 @@ function findRoutesAndCreateOptimalPath(
continue; continue;
} }
// TODO(kimpers): Do we need to handle 0 entries, from eg Kyber? // TODO: Do we need to handle 0 entries, from eg Kyber?
const serializedPath = singleSourceSamplesWithOutput.reduce<SerializedPath>( const serializedPath = singleSourceSamplesWithOutput.reduce<SerializedPath>(
(memo, sample, sampleIdx) => { (memo, sample, sampleIdx) => {
memo.ids.push(`${sample.source}-${serializedPaths.length}-${sampleIdx}`); // Use the fill from createFillFromDexSample to apply
memo.inputs.push(sample.input.integerValue().toNumber()); // any user supplied adjustments
memo.outputs.push(sample.output.integerValue().toNumber()); const f = createFillFromDexSample(sample);
memo.outputFees.push( memo.ids.push(`${f.source}-${serializedPaths.length}-${sampleIdx}`);
calculateOuputFee(side, sample, opts.outputAmountPerEth, opts.inputAmountPerEth, fees) memo.inputs.push(f.input.integerValue().toNumber());
memo.outputs.push(f.output.integerValue().toNumber());
// Calculate the penalty of this sample as the diff between the
// output and the adjusted output
const outputFee = f.output
.minus(f.adjustedOutput)
.absoluteValue()
.integerValue() .integerValue()
.toNumber(), .toNumber();
); memo.outputFees.push(outputFee);
return memo; return memo;
}, },
@ -265,6 +277,8 @@ function findRoutesAndCreateOptimalPath(
samplesAndNativeOrdersWithResults.push(singleSourceSamplesWithOutput); samplesAndNativeOrdersWithResults.push(singleSourceSamplesWithOutput);
serializedPaths.push(serializedPath); serializedPaths.push(serializedPath);
const sourcePathId = hexUtils.random();
sampleSourcePathIds.push(sourcePathId); sampleSourcePathIds.push(sourcePathId);
} }
@ -306,19 +320,22 @@ function findRoutesAndCreateOptimalPath(
normalizedOrderOutput.times(scaleToInput).times(fraction), normalizedOrderOutput.times(scaleToInput).times(fraction),
normalizedOrderOutput, normalizedOrderOutput,
); );
const id = `${ERC20BridgeSource.Native}-${serializedPaths.length}-${idx}-${i}`; const id = `${ERC20BridgeSource.Native}-${nativeOrder.type}-${serializedPaths.length}-${idx}-${i}`;
inputs.push(currentInput.integerValue().toNumber()); inputs.push(currentInput.integerValue().toNumber());
outputs.push(currentOutput.integerValue().toNumber()); outputs.push(currentOutput.integerValue().toNumber());
outputFees.push(fee); outputFees.push(fee);
ids.push(id); ids.push(id);
} }
// We have a VIP for the Rfq order type, Limit order currently goes through FQT
const isVip = nativeOrder.type !== FillQuoteTransformerOrderType.Limit;
const serializedPath: SerializedPath = { const serializedPath: SerializedPath = {
ids, ids,
inputs, inputs,
outputs, outputs,
outputFees, outputFees,
isVip: true, isVip,
}; };
samplesAndNativeOrdersWithResults.push([nativeOrder]); samplesAndNativeOrdersWithResults.push([nativeOrder]);
@ -375,7 +392,7 @@ function findRoutesAndCreateOptimalPath(
}; };
} }
export function findOptimalRustPathFromSamples( export function findOptimalPathFromSamples(
side: MarketOperation, side: MarketOperation,
samples: DexSample[][], samples: DexSample[][],
nativeOrders: NativeOrderWithFillableAmounts[], nativeOrders: NativeOrderWithFillableAmounts[],
@ -384,6 +401,7 @@ export function findOptimalRustPathFromSamples(
fees: FeeSchedule, fees: FeeSchedule,
chainId: ChainId, chainId: ChainId,
neonRouterNumSamples: number, neonRouterNumSamples: number,
fillAdjustor: FillAdjustor,
samplerMetrics?: SamplerMetrics, samplerMetrics?: SamplerMetrics,
): Path | undefined { ): Path | undefined {
const beforeTimeMs = performance.now(); const beforeTimeMs = performance.now();
@ -406,6 +424,7 @@ export function findOptimalRustPathFromSamples(
fees, fees,
neonRouterNumSamples, neonRouterNumSamples,
vipSourcesSet, vipSourcesSet,
fillAdjustor,
); );
if (!paths) { if (!paths) {
@ -415,7 +434,7 @@ export function findOptimalRustPathFromSamples(
const { allSourcesPath, vipSourcesPath } = paths; const { allSourcesPath, vipSourcesPath } = paths;
if (!allSourcesPath || vipSourcesPath?.isBetterThan(allSourcesPath)) { if (!allSourcesPath || vipSourcesPath?.isAdjustedBetterThan(allSourcesPath)) {
sendMetrics(); sendMetrics();
return vipSourcesPath; return vipSourcesPath;
} }
@ -423,143 +442,3 @@ export function findOptimalRustPathFromSamples(
sendMetrics(); sendMetrics();
return allSourcesPath; return allSourcesPath;
} }
/**
* Find the optimal mixture of fills that maximizes (for sells) or minimizes
* (for buys) output, while meeting the input requirement.
*/
export async function findOptimalPathJSAsync(
side: MarketOperation,
fills: Fill[][],
targetInput: BigNumber,
runLimit: number = 2 ** 8,
samplerMetrics?: SamplerMetrics,
opts: PathPenaltyOpts = DEFAULT_PATH_PENALTY_OPTS,
): Promise<Path | undefined> {
const beforeTimeMs = performance.now();
// Sort fill arrays by descending adjusted completed rate.
// Remove any paths which cannot impact the optimal path
const sortedPaths = reducePaths(fillsToSortedPaths(fills, side, targetInput, opts), side);
if (sortedPaths.length === 0) {
return undefined;
}
const rates = rateBySourcePathId(sortedPaths);
let optimalPath = sortedPaths[0];
for (const [i, path] of sortedPaths.slice(1).entries()) {
optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit * RUN_LIMIT_DECAY_FACTOR ** i, rates);
// Yield to event loop.
await Promise.resolve();
}
const finalPath = optimalPath.isComplete() ? optimalPath : undefined;
// tslint:disable-next-line: no-unused-expression
samplerMetrics &&
samplerMetrics.logRouterDetails({
router: 'js',
type: 'total',
timingMs: performance.now() - beforeTimeMs,
});
return finalPath;
}
// Sort fill arrays by descending adjusted completed rate.
export function fillsToSortedPaths(
fills: Fill[][],
side: MarketOperation,
targetInput: BigNumber,
opts: PathPenaltyOpts,
): Path[] {
const paths = fills.map(singleSourceFills => Path.create(side, singleSourceFills, targetInput, opts));
const sortedPaths = paths.sort((a, b) => {
const aRate = a.adjustedCompleteRate();
const bRate = b.adjustedCompleteRate();
// There is a case where the adjusted completed rate isn't sufficient for the desired amount
// resulting in a NaN div by 0 (output)
if (bRate.isNaN()) {
return -1;
}
if (aRate.isNaN()) {
return 1;
}
return bRate.comparedTo(aRate);
});
return sortedPaths;
}
// Remove paths which have no impact on the optimal path
export function reducePaths(sortedPaths: Path[], side: MarketOperation): Path[] {
// Any path which has a min rate that is less than the best adjusted completed rate has no chance of improving
// the overall route.
const bestNonNativeCompletePath = sortedPaths.filter(
p => p.isComplete() && p.fills[0].source !== ERC20BridgeSource.Native,
)[0];
// If there is no complete path then just go ahead with the sorted paths
// I.e if the token only exists on sources which cannot sell to infinity
// or buys where X is greater than all the tokens available in the pools
if (!bestNonNativeCompletePath) {
return sortedPaths;
}
const bestNonNativeCompletePathAdjustedRate = bestNonNativeCompletePath.adjustedCompleteRate();
if (!bestNonNativeCompletePathAdjustedRate.isGreaterThan(0)) {
return sortedPaths;
}
const filteredPaths = sortedPaths.filter(p =>
p.bestRate().isGreaterThanOrEqualTo(bestNonNativeCompletePathAdjustedRate),
);
return filteredPaths;
}
function mixPaths(
side: MarketOperation,
pathA: Path,
pathB: Path,
targetInput: BigNumber,
maxSteps: number,
rates: { [id: string]: BigNumber },
): Path {
const _maxSteps = Math.max(maxSteps, 32);
let steps = 0;
// We assume pathA is the better of the two initially.
let bestPath: Path = pathA;
const _walk = (path: Path, remainingFills: Fill[]) => {
steps += 1;
if (path.isBetterThan(bestPath)) {
bestPath = path;
}
const remainingInput = targetInput.minus(path.size().input);
if (remainingInput.isGreaterThan(0)) {
for (let i = 0; i < remainingFills.length && steps < _maxSteps; ++i) {
const fill = remainingFills[i];
// Only walk valid paths.
if (!path.isValidNextFill(fill)) {
continue;
}
// Remove this fill from the next list of candidate fills.
const nextRemainingFills = remainingFills.slice();
nextRemainingFills.splice(i, 1);
// Recurse.
_walk(Path.clone(path).append(fill), nextRemainingFills);
}
}
};
const allFills = [...pathA.fills, ...pathB.fills];
// Sort subpaths by rate and keep fills contiguous to improve our
// chances of walking ideal, valid paths first.
const sortedFills = allFills.sort((a, b) => {
if (a.sourcePathId !== b.sourcePathId) {
return rates[b.sourcePathId].comparedTo(rates[a.sourcePathId]);
}
return a.index - b.index;
});
_walk(Path.create(side, [], targetInput, pathA.pathPenaltyOpts), sortedFills);
if (!bestPath.isValid()) {
throw new Error('nooope');
}
return bestPath;
}
function rateBySourcePathId(paths: Path[]): { [id: string]: BigNumber } {
return _.fromPairs(paths.map(p => [p.fills[0].sourcePathId, p.adjustedRate()]));
}

View File

@ -1,9 +1,20 @@
import { FillQuoteTransformerOrderType } from '@0x/protocol-utils';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { MarketOperation } from '../../types'; import { MarketOperation } from '../../types';
import { SOURCE_FLAGS, ZERO_AMOUNT } from './constants'; import { SOURCE_FLAGS, ZERO_AMOUNT } from './constants';
import { DexSample, ERC20BridgeSource, ExchangeProxyOverhead, FeeSchedule, MultiHopFillData } from './types'; import { adjustOutput } from './fills';
import { IdentityFillAdjustor } from './identity_fill_adjustor';
import {
DexSample,
ERC20BridgeSource,
ExchangeProxyOverhead,
FeeSchedule,
Fill,
FillAdjustor,
MultiHopFillData,
} from './types';
// tslint:disable:no-bitwise // tslint:disable:no-bitwise
@ -18,20 +29,55 @@ export function getTwoHopAdjustedRate(
outputAmountPerEth: BigNumber, outputAmountPerEth: BigNumber,
fees: FeeSchedule = {}, fees: FeeSchedule = {},
exchangeProxyOverhead: ExchangeProxyOverhead = () => ZERO_AMOUNT, exchangeProxyOverhead: ExchangeProxyOverhead = () => ZERO_AMOUNT,
fillAdjustor: FillAdjustor = new IdentityFillAdjustor(),
): BigNumber { ): BigNumber {
const { output, input, fillData } = twoHopQuote; const { output, input, fillData } = twoHopQuote;
if (input.isLessThan(targetInput) || output.isZero()) { if (input.isLessThan(targetInput) || output.isZero()) {
return ZERO_AMOUNT; return ZERO_AMOUNT;
} }
const penalty = outputAmountPerEth.times(
exchangeProxyOverhead( // Flags to indicate which sources are used
const flags =
SOURCE_FLAGS.MultiHop | SOURCE_FLAGS.MultiHop |
SOURCE_FLAGS[fillData.firstHopSource.source] | SOURCE_FLAGS[fillData.firstHopSource.source] |
SOURCE_FLAGS[fillData.secondHopSource.source], SOURCE_FLAGS[fillData.secondHopSource.source];
).plus(fees[ERC20BridgeSource.MultiHop]!(fillData)),
); // Penalty of going to those sources in terms of output
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); const sourcePenalty = outputAmountPerEth.times(fees[ERC20BridgeSource.MultiHop]!(fillData).fee).integerValue();
return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput);
// Create a Fill so it can be adjusted by the `FillAdjustor`
const fill: Fill = {
...twoHopQuote,
flags,
type: FillQuoteTransformerOrderType.Bridge,
adjustedOutput: adjustOutput(side, twoHopQuote.output, sourcePenalty),
sourcePathId: `${ERC20BridgeSource.MultiHop}-${fillData.firstHopSource.source}-${fillData.secondHopSource.source}`,
// We don't have this information at this stage
gas: 0,
};
// Adjust the individual Fill
// HACK: Chose the worst of slippage between the two sources in multihop
const adjustedOutputLeft = fillAdjustor.adjustFills(
side,
[{ ...fill, source: fillData.firstHopSource.source }],
targetInput,
)[0].adjustedOutput;
const adjustedOutputRight = fillAdjustor.adjustFills(
side,
[{ ...fill, source: fillData.secondHopSource.source }],
targetInput,
)[0].adjustedOutput;
// In Sells, output smaller is worse (you're getting less out)
// In Buys, output larger is worse (it's costing you more)
const fillAdjustedOutput =
side === MarketOperation.Sell
? BigNumber.min(adjustedOutputLeft, adjustedOutputRight)
: BigNumber.max(adjustedOutputLeft, adjustedOutputRight);
const pathPenalty = outputAmountPerEth.times(exchangeProxyOverhead(flags)).integerValue();
const pathAdjustedOutput = adjustOutput(side, fillAdjustedOutput, pathPenalty);
return getRate(side, input, pathAdjustedOutput);
} }
/** /**
@ -59,6 +105,8 @@ export function getCompleteRate(
/** /**
* Computes the rate given the input/output of a path. * Computes the rate given the input/output of a path.
*
* If it is a sell, output/input. If it is a buy, input/output.
*/ */
export function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber { export function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber {
if (input.eq(0) || output.eq(0)) { if (input.eq(0) || output.eq(0)) {

View File

@ -75,6 +75,7 @@ import {
DexSample, DexSample,
DODOFillData, DODOFillData,
ERC20BridgeSource, ERC20BridgeSource,
FeeSchedule,
GeistFillData, GeistFillData,
GeistInfo, GeistInfo,
GenericRouterFillData, GenericRouterFillData,
@ -1309,16 +1310,22 @@ export class SamplerOperations {
}); });
} }
public getMedianSellRate( /**
* Returns the best price for the native token
* Best is calculated according to the fee schedule, so the price of the
* best source, fee adjusted, will be returned.
*/
public getBestNativeTokenSellRate(
sources: ERC20BridgeSource[], sources: ERC20BridgeSource[],
makerToken: string, makerToken: string,
takerToken: string, nativeToken: string,
takerFillAmount: BigNumber, nativeFillAmount: BigNumber,
feeSchedule: FeeSchedule,
): BatchedOperation<BigNumber> { ): BatchedOperation<BigNumber> {
if (makerToken.toLowerCase() === takerToken.toLowerCase()) { if (makerToken.toLowerCase() === nativeToken.toLowerCase()) {
return SamplerOperations.constant(new BigNumber(1)); return SamplerOperations.constant(new BigNumber(1));
} }
const subOps = this._getSellQuoteOperations(sources, makerToken, takerToken, [takerFillAmount], { const subOps = this._getSellQuoteOperations(sources, makerToken, nativeToken, [nativeFillAmount], {
default: [], default: [],
}); });
return this._createBatch( return this._createBatch(
@ -1327,15 +1334,35 @@ export class SamplerOperations {
if (samples.length === 0) { if (samples.length === 0) {
return ZERO_AMOUNT; return ZERO_AMOUNT;
} }
const flatSortedSamples = samples
.reduce((acc, v) => acc.concat(...v)) const adjustedPrices = subOps.map((s, i) => {
.filter(v => !v.isZero()) // If the source gave us nothing, skip it and return a default
.sort((a, b) => a.comparedTo(b)); if (samples[i].length === 0 || samples[i][0].isZero()) {
if (flatSortedSamples.length === 0) { return { adjustedPrice: ZERO_AMOUNT, source: s.source, price: ZERO_AMOUNT };
return ZERO_AMOUNT;
} }
const medianSample = flatSortedSamples[Math.floor(flatSortedSamples.length / 2)]; const v = samples[i][0];
return medianSample.div(takerFillAmount); const price = v.dividedBy(nativeFillAmount);
// Create an adjusted price to avoid selecting the following:
// * a source that is too expensive to arbitrage given the gas environment
// * when a number of sources are poorly priced or liquidity is low
// Fee is already gas * gasPrice
const fee = feeSchedule[subOps[i].source]
? feeSchedule[subOps[i].source]!(subOps[i].fillData).fee
: ZERO_AMOUNT;
const adjustedNativeAmount = nativeFillAmount.plus(fee);
const adjustedPrice = v.div(adjustedNativeAmount);
return {
adjustedPrice,
source: subOps[i].source,
price,
};
});
const sortedPrices = adjustedPrices.sort((a, b) => a.adjustedPrice.comparedTo(b.adjustedPrice));
const selectedPrice = sortedPrices[sortedPrices.length - 1].price;
return selectedPrice;
}, },
() => ZERO_AMOUNT, () => ZERO_AMOUNT,
); );

View File

@ -401,45 +401,10 @@ export interface Fill<TFillData extends FillData = FillData> {
output: BigNumber; output: BigNumber;
// The output fill amount, adjusted by fees. // The output fill amount, adjusted by fees.
adjustedOutput: BigNumber; adjustedOutput: BigNumber;
// Fill that must precede this one. This enforces certain fills to be contiguous. // The expected gas cost of this fill
parent?: Fill; gas: number;
// The index of the fill in the original path.
index: number;
} }
/**
* Represents continguous fills on a path that have been merged together.
*/
export interface CollapsedFill<TFillData extends FillData = FillData> {
source: ERC20BridgeSource;
type: FillQuoteTransformerOrderType; // should correspond with TFillData
fillData: TFillData;
// Unique ID of the original source path this fill belongs to.
// This is generated when the path is generated and is useful to distinguish
// paths that have the same `source` IDs but are distinct (e.g., Curves).
sourcePathId: string;
/**
* Total input amount (sum of `subFill`s)
*/
input: BigNumber;
/**
* Total output amount (sum of `subFill`s)
*/
output: BigNumber;
/**
* Quantities of all the fills that were collapsed.
*/
subFills: Array<{
input: BigNumber;
output: BigNumber;
}>;
}
/**
* A `CollapsedFill` wrapping a native order.
*/
export interface NativeCollapsedFill extends CollapsedFill<NativeFillData> {}
export interface OptimizedMarketOrderBase<TFillData extends FillData = FillData> { export interface OptimizedMarketOrderBase<TFillData extends FillData = FillData> {
source: ERC20BridgeSource; source: ERC20BridgeSource;
fillData: TFillData; fillData: TFillData;
@ -448,24 +413,21 @@ export interface OptimizedMarketOrderBase<TFillData extends FillData = FillData>
takerToken: string; takerToken: string;
makerAmount: BigNumber; // The amount we wish to buy from this order, e.g inclusive of any previous partial fill makerAmount: BigNumber; // The amount we wish to buy from this order, e.g inclusive of any previous partial fill
takerAmount: BigNumber; // The amount we wish to fill this for, e.g inclusive of any previous partial fill takerAmount: BigNumber; // The amount we wish to fill this for, e.g inclusive of any previous partial fill
fills: CollapsedFill[]; fill: Omit<Fill, 'flags' | 'fillData' | 'sourcePathId' | 'source' | 'type'>; // Remove duplicates which have been brought into the OrderBase interface
} }
export interface OptimizedMarketBridgeOrder<TFillData extends FillData = FillData> export interface OptimizedMarketBridgeOrder<TFillData extends FillData = FillData>
extends OptimizedMarketOrderBase<TFillData> { extends OptimizedMarketOrderBase<TFillData> {
type: FillQuoteTransformerOrderType.Bridge; type: FillQuoteTransformerOrderType.Bridge;
fillData: TFillData;
sourcePathId: string; sourcePathId: string;
} }
export interface OptimizedLimitOrder extends OptimizedMarketOrderBase<NativeLimitOrderFillData> { export interface OptimizedLimitOrder extends OptimizedMarketOrderBase<NativeLimitOrderFillData> {
type: FillQuoteTransformerOrderType.Limit; type: FillQuoteTransformerOrderType.Limit;
fillData: NativeLimitOrderFillData;
} }
export interface OptimizedRfqOrder extends OptimizedMarketOrderBase<NativeRfqOrderFillData> { export interface OptimizedRfqOrder extends OptimizedMarketOrderBase<NativeRfqOrderFillData> {
type: FillQuoteTransformerOrderType.Rfq; type: FillQuoteTransformerOrderType.Rfq;
fillData: NativeRfqOrderFillData;
} }
/** /**
@ -482,8 +444,12 @@ export interface GetMarketOrdersRfqOpts extends RfqRequestOpts {
firmQuoteValidator?: RfqFirmQuoteValidator; firmQuoteValidator?: RfqFirmQuoteValidator;
} }
export type FeeEstimate = (fillData: FillData) => number | BigNumber; export type FeeEstimate = (fillData: FillData) => { gas: number; fee: BigNumber };
export type FeeSchedule = Partial<{ [key in ERC20BridgeSource]: FeeEstimate }>; export type FeeSchedule = Partial<{ [key in ERC20BridgeSource]: FeeEstimate }>;
export type GasEstimate = (fillData: FillData) => number;
export type GasSchedule = Partial<{ [key in ERC20BridgeSource]: GasEstimate }>;
export type ExchangeProxyOverhead = (sourceFlags: bigint) => BigNumber; export type ExchangeProxyOverhead = (sourceFlags: bigint) => BigNumber;
/** /**
@ -547,7 +513,7 @@ export interface GetMarketOrdersOpts {
/** /**
* Estimated gas consumed by each liquidity source. * Estimated gas consumed by each liquidity source.
*/ */
gasSchedule: FeeSchedule; gasSchedule: GasSchedule;
exchangeProxyOverhead: ExchangeProxyOverhead; exchangeProxyOverhead: ExchangeProxyOverhead;
/** /**
* Whether to pad the quote with a redundant fallback quote using different * Whether to pad the quote with a redundant fallback quote using different
@ -582,6 +548,11 @@ export interface GetMarketOrdersOpts {
* Sampler metrics for recording data on the sampler service and operations * Sampler metrics for recording data on the sampler service and operations
*/ */
samplerMetrics?: SamplerMetrics; samplerMetrics?: SamplerMetrics;
/**
* Adjusts fills individual fills based on caller supplied criteria
*/
fillAdjustor: FillAdjustor;
} }
export interface SamplerMetrics { export interface SamplerMetrics {
@ -627,7 +598,7 @@ export interface SourceQuoteOperation<TFillData extends FillData = FillData> ext
export interface OptimizerResult { export interface OptimizerResult {
optimizedOrders: OptimizedMarketOrder[]; optimizedOrders: OptimizedMarketOrder[];
sourceFlags: bigint; sourceFlags: bigint;
liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>; liquidityDelivered: Readonly<Fill[] | DexSample<MultiHopFillData>>;
marketSideLiquidity: MarketSideLiquidity; marketSideLiquidity: MarketSideLiquidity;
adjustedRate: BigNumber; adjustedRate: BigNumber;
takerAmountPerEth: BigNumber; takerAmountPerEth: BigNumber;
@ -695,8 +666,13 @@ export interface GenerateOptimizedOrdersOpts {
gasPrice: BigNumber; gasPrice: BigNumber;
neonRouterNumSamples: number; neonRouterNumSamples: number;
samplerMetrics?: SamplerMetrics; samplerMetrics?: SamplerMetrics;
fillAdjustor: FillAdjustor;
} }
export interface ComparisonPrice { export interface ComparisonPrice {
wholeOrder: BigNumber | undefined; wholeOrder: BigNumber | undefined;
} }
export interface FillAdjustor {
adjustFills: (side: MarketOperation, fills: Fill[], amount: BigNumber) => Fill[];
}

View File

@ -5,12 +5,11 @@ import _ = require('lodash');
import { MarketOperation, NativeOrderWithFillableAmounts } from '../types'; import { MarketOperation, NativeOrderWithFillableAmounts } from '../types';
import { import {
CollapsedFill,
DexSample, DexSample,
ERC20BridgeSource, ERC20BridgeSource,
Fill,
FillData, FillData,
MultiHopFillData, MultiHopFillData,
NativeCollapsedFill,
NativeFillData, NativeFillData,
NativeLimitOrderFillData, NativeLimitOrderFillData,
NativeRfqOrderFillData, NativeRfqOrderFillData,
@ -123,7 +122,7 @@ export interface PriceComparisonsReport {
export function generateQuoteReport( export function generateQuoteReport(
marketOperation: MarketOperation, marketOperation: MarketOperation,
nativeOrders: NativeOrderWithFillableAmounts[], nativeOrders: NativeOrderWithFillableAmounts[],
liquidityDelivered: ReadonlyArray<CollapsedFill> | DexSample<MultiHopFillData>, liquidityDelivered: ReadonlyArray<Fill> | DexSample<MultiHopFillData>,
comparisonPrice?: BigNumber | undefined, comparisonPrice?: BigNumber | undefined,
quoteRequestor?: QuoteRequestor, quoteRequestor?: QuoteRequestor,
): QuoteReport { ): QuoteReport {
@ -174,7 +173,7 @@ export function generateQuoteReport(
export function generateExtendedQuoteReportSources( export function generateExtendedQuoteReportSources(
marketOperation: MarketOperation, marketOperation: MarketOperation,
quotes: RawQuotes, quotes: RawQuotes,
liquidityDelivered: ReadonlyArray<CollapsedFill> | DexSample<MultiHopFillData>, liquidityDelivered: ReadonlyArray<Fill> | DexSample<MultiHopFillData>,
amount: BigNumber, amount: BigNumber,
comparisonPrice?: BigNumber | undefined, comparisonPrice?: BigNumber | undefined,
quoteRequestor?: QuoteRequestor, quoteRequestor?: QuoteRequestor,
@ -343,7 +342,7 @@ export function multiHopSampleToReportSource(
} }
} }
function _isNativeOrderFromCollapsedFill(cf: CollapsedFill): cf is NativeCollapsedFill { function _isNativeOrderFromCollapsedFill(cf: Fill): cf is Fill<NativeFillData> {
const { type } = cf; const { type } = cf;
return type === FillQuoteTransformerOrderType.Limit || type === FillQuoteTransformerOrderType.Rfq; return type === FillQuoteTransformerOrderType.Limit || type === FillQuoteTransformerOrderType.Rfq;
} }

View File

@ -4,7 +4,7 @@ import { BigNumber } from '@0x/utils';
import { constants } from '../constants'; import { constants } from '../constants';
import { MarketOperation } from '../types'; import { MarketOperation } from '../types';
import { FeeSchedule, NativeLimitOrderFillData, OptimizedMarketOrder } from './market_operation_utils/types'; import { GasSchedule, NativeLimitOrderFillData, OptimizedMarketOrder } from './market_operation_utils/types';
import { getNativeAdjustedTakerFeeAmount } from './utils'; import { getNativeAdjustedTakerFeeAmount } from './utils';
const { PROTOCOL_FEE_MULTIPLIER, ZERO_AMOUNT } = constants; const { PROTOCOL_FEE_MULTIPLIER, ZERO_AMOUNT } = constants;
@ -72,7 +72,7 @@ export interface QuoteFillInfo {
} }
export interface QuoteFillInfoOpts { export interface QuoteFillInfoOpts {
gasSchedule: FeeSchedule; gasSchedule: GasSchedule;
protocolFeeMultiplier: BigNumber; protocolFeeMultiplier: BigNumber;
slippage: number; slippage: number;
} }
@ -140,7 +140,7 @@ export function fillQuoteOrders(
fillOrders: QuoteFillOrderCall[], fillOrders: QuoteFillOrderCall[],
inputAmount: BigNumber, inputAmount: BigNumber,
protocolFeePerFillOrder: BigNumber, protocolFeePerFillOrder: BigNumber,
gasSchedule: FeeSchedule, gasSchedule: GasSchedule,
): IntermediateQuoteFillResult { ): IntermediateQuoteFillResult {
const result: IntermediateQuoteFillResult = { const result: IntermediateQuoteFillResult = {
...EMPTY_QUOTE_INTERMEDIATE_FILL_RESULT, ...EMPTY_QUOTE_INTERMEDIATE_FILL_RESULT,
@ -151,28 +151,18 @@ export function fillQuoteOrders(
if (remainingInput.lte(0)) { if (remainingInput.lte(0)) {
break; break;
} }
for (const fill of fo.order.fills) { const { source, fillData } = fo.order;
if (remainingInput.lte(0)) {
break;
}
const { source, fillData } = fill;
const gas = gasSchedule[source] === undefined ? 0 : gasSchedule[source]!(fillData); const gas = gasSchedule[source] === undefined ? 0 : gasSchedule[source]!(fillData);
result.gas += new BigNumber(gas).toNumber(); result.gas += new BigNumber(gas).toNumber();
result.inputBySource[source] = result.inputBySource[source] || ZERO_AMOUNT; result.inputBySource[source] = result.inputBySource[source] || ZERO_AMOUNT;
// Actual rates are rarely linear, so fill subfills individually to
// get a better approximation of fill size.
for (const subFill of fill.subFills) {
if (remainingInput.lte(0)) {
break;
}
const filledInput = solveForInputFillAmount( const filledInput = solveForInputFillAmount(
remainingInput, remainingInput,
subFill.input, fo.order.fill.input,
fo.totalOrderInput, fo.totalOrderInput,
fo.totalOrderInputFee, fo.totalOrderInputFee,
); );
const filledOutput = subFill.output.times(filledInput.div(subFill.input)); const filledOutput = fo.order.fill.output.times(filledInput.div(fo.order.fill.input));
const filledInputFee = filledInput.div(fo.totalOrderInput).times(fo.totalOrderInputFee); const filledInputFee = filledInput.div(fo.totalOrderInput).times(fo.totalOrderInputFee);
const filledOutputFee = filledOutput.div(fo.totalOrderOutput).times(fo.totalOrderOutputFee); const filledOutputFee = filledOutput.div(fo.totalOrderOutput).times(fo.totalOrderOutputFee);
@ -182,8 +172,6 @@ export function fillQuoteOrders(
result.inputFee = result.inputFee.plus(filledInputFee); result.inputFee = result.inputFee.plus(filledInputFee);
result.outputFee = result.outputFee.plus(filledOutputFee); result.outputFee = result.outputFee.plus(filledOutputFee);
remainingInput = remainingInput.minus(filledInput.plus(filledInputFee)); remainingInput = remainingInput.minus(filledInput.plus(filledInputFee));
}
}
// NOTE: V4 Limit orders have Protocol fees // NOTE: V4 Limit orders have Protocol fees
const protocolFee = hasProtocolFee(fo.order) ? protocolFeePerFillOrder : ZERO_AMOUNT; const protocolFee = hasProtocolFee(fo.order) ? protocolFeePerFillOrder : ZERO_AMOUNT;
result.protocolFee = result.protocolFee.plus(protocolFee); result.protocolFee = result.protocolFee.plus(protocolFee);
@ -314,7 +302,7 @@ function fromIntermediateQuoteFillResult(ir: IntermediateQuoteFillResult, quoteI
}; };
} }
function getTotalGasUsedByFills(fills: OptimizedMarketOrder[], gasSchedule: FeeSchedule): number { function getTotalGasUsedByFills(fills: OptimizedMarketOrder[], gasSchedule: GasSchedule): number {
let gasUsed = 0; let gasUsed = 0;
for (const f of fills) { for (const f of fills) {
const fee = gasSchedule[f.source] === undefined ? 0 : gasSchedule[f.source]!(f.fillData); const fee = gasSchedule[f.source] === undefined ? 0 : gasSchedule[f.source]!(f.fillData);

View File

@ -18,7 +18,7 @@ const expect = chai.expect;
const DAI_TOKEN = '0x6b175474e89094c44da98b954eedeac495271d0f'; const DAI_TOKEN = '0x6b175474e89094c44da98b954eedeac495271d0f';
const ETH_TOKEN = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; const ETH_TOKEN = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
const GAS_PRICE = new BigNumber(50e9); // 50 gwei const GAS_PRICE = new BigNumber(50e9); // 50 gwei
const NATIVE_ORDER_FEE = new BigNumber(220e3); // 220K gas const NATIVE_ORDER_GAS = 220e3; // 220K gas
// DEX samples to fill in MarketSideLiquidity // DEX samples to fill in MarketSideLiquidity
const curveSample: DexSample = { const curveSample: DexSample = {
@ -36,7 +36,10 @@ const uniswapSample1: DexSample = {
const dexQuotes: DexSample[] = [curveSample, uniswapSample1]; const dexQuotes: DexSample[] = [curveSample, uniswapSample1];
const feeSchedule = { const feeSchedule = {
[ERC20BridgeSource.Native]: _.constant(GAS_PRICE.times(NATIVE_ORDER_FEE)), [ERC20BridgeSource.Native]: _.constant({
gas: NATIVE_ORDER_GAS,
fee: GAS_PRICE.times(NATIVE_ORDER_GAS),
}),
}; };
const exchangeProxyOverhead = (sourceFlags: bigint) => { const exchangeProxyOverhead = (sourceFlags: bigint) => {

View File

@ -23,6 +23,8 @@ import { ExchangeProxySwapQuoteConsumer } from '../src/quote_consumers/exchange_
import { AffiliateFeeType, MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types'; import { AffiliateFeeType, MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types';
import { import {
ERC20BridgeSource, ERC20BridgeSource,
Fill,
NativeFillData,
OptimizedLimitOrder, OptimizedLimitOrder,
OptimizedMarketOrder, OptimizedMarketOrder,
} from '../src/utils/market_operation_utils/types'; } from '../src/utils/market_operation_utils/types';
@ -100,7 +102,8 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
takerToken: order.takerToken, takerToken: order.takerToken,
makerAmount: order.makerAmount, makerAmount: order.makerAmount,
takerAmount: order.takerAmount, takerAmount: order.takerAmount,
fills: [], // tslint:disable-next-line:no-object-literal-type-assertion
fill: {} as Fill<NativeFillData>,
...optimizerFields, ...optimizerFields,
}; };
} }

View File

@ -16,16 +16,14 @@ import * as _ from 'lodash';
import * as TypeMoq from 'typemoq'; import * as TypeMoq from 'typemoq';
import { MarketOperation, QuoteRequestor, RfqRequestOpts, SignedNativeOrder } from '../src'; import { MarketOperation, QuoteRequestor, RfqRequestOpts, SignedNativeOrder } from '../src';
import { Integrator, NativeOrderWithFillableAmounts } from '../src/types'; import { Integrator } from '../src/types';
import { MarketOperationUtils } from '../src/utils/market_operation_utils/'; import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { import {
BUY_SOURCE_FILTER_BY_CHAIN_ID, BUY_SOURCE_FILTER_BY_CHAIN_ID,
POSITIVE_INF,
SELL_SOURCE_FILTER_BY_CHAIN_ID, SELL_SOURCE_FILTER_BY_CHAIN_ID,
SOURCE_FLAGS, SOURCE_FLAGS,
ZERO_AMOUNT, ZERO_AMOUNT,
} from '../src/utils/market_operation_utils/constants'; } from '../src/utils/market_operation_utils/constants';
import { createFills } from '../src/utils/market_operation_utils/fills';
import { PoolsCache } from '../src/utils/market_operation_utils/pools_cache'; import { PoolsCache } from '../src/utils/market_operation_utils/pools_cache';
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations'; import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations';
@ -39,7 +37,6 @@ import {
GetMarketOrdersOpts, GetMarketOrdersOpts,
LiquidityProviderFillData, LiquidityProviderFillData,
MarketSideLiquidity, MarketSideLiquidity,
NativeFillData,
OptimizedMarketBridgeOrder, OptimizedMarketBridgeOrder,
OptimizerResultWithReport, OptimizerResultWithReport,
TokenAdjacencyGraph, TokenAdjacencyGraph,
@ -272,7 +269,7 @@ describe('MarketOperationUtils tests', () => {
}; };
} }
type GetMedianRateOperation = ( type GetBestNativeTokenSellRateOperation = (
sources: ERC20BridgeSource[], sources: ERC20BridgeSource[],
makerToken: string, makerToken: string,
takerToken: string, takerToken: string,
@ -281,7 +278,7 @@ describe('MarketOperationUtils tests', () => {
liquidityProviderAddress?: string, liquidityProviderAddress?: string,
) => BigNumber; ) => BigNumber;
function createGetMedianSellRate(rate: Numberish): GetMedianRateOperation { function createGetBestNativeSellRate(rate: Numberish): GetBestNativeTokenSellRateOperation {
return ( return (
_sources: ERC20BridgeSource[], _sources: ERC20BridgeSource[],
_makerToken: string, _makerToken: string,
@ -388,7 +385,7 @@ describe('MarketOperationUtils tests', () => {
}, },
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES),
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES),
getMedianSellRate: createGetMedianSellRate(1), getBestNativeTokenSellRate: createGetBestNativeSellRate(1),
getTwoHopSellQuotes: (..._params: any[]) => [], getTwoHopSellQuotes: (..._params: any[]) => [],
getTwoHopBuyQuotes: (..._params: any[]) => [], getTwoHopBuyQuotes: (..._params: any[]) => [],
isAddressContract: (..._params: any[]) => false, isAddressContract: (..._params: any[]) => false,
@ -621,7 +618,7 @@ describe('MarketOperationUtils tests', () => {
// to get a comparisonPrice, you need a feeschedule for a native order // to get a comparisonPrice, you need a feeschedule for a native order
const feeSchedule = { const feeSchedule = {
[ERC20BridgeSource.Native]: _.constant(new BigNumber(1)), [ERC20BridgeSource.Native]: _.constant({ gas: 1, fee: new BigNumber(1) }),
}; };
mockedQuoteRequestor mockedQuoteRequestor
.setup(mqr => mqr.getMakerUriForSignature(TypeMoq.It.isValue(SIGNATURE))) .setup(mqr => mqr.getMakerUriForSignature(TypeMoq.It.isValue(SIGNATURE)))
@ -1011,7 +1008,7 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
expect(improvedOrders).to.not.be.length(0); expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) { for (const order of improvedOrders) {
const expectedMakerAmount = order.fills[0].output; const expectedMakerAmount = order.fill.output;
const slippage = new BigNumber(1).minus(order.makerAmount.div(expectedMakerAmount.plus(1))); const slippage = new BigNumber(1).minus(order.makerAmount.div(expectedMakerAmount.plus(1)));
assertRoughlyEquals(slippage, bridgeSlippage, 1); assertRoughlyEquals(slippage, bridgeSlippage, 1);
} }
@ -1033,7 +1030,7 @@ describe('MarketOperationUtils tests', () => {
{ ...DEFAULT_OPTS, numSamples: 4 }, { ...DEFAULT_OPTS, numSamples: 4 },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.SushiSwap, ERC20BridgeSource.SushiSwap,
ERC20BridgeSource.Uniswap, ERC20BridgeSource.Uniswap,
@ -1056,15 +1053,16 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.SushiSwap]: [0.95, 0.1, 0.1, 0.1], [ERC20BridgeSource.SushiSwap]: [0.95, 0.1, 0.1, 0.1],
}; };
const feeSchedule = { const feeSchedule = {
[ERC20BridgeSource.Native]: _.constant( [ERC20BridgeSource.Native]: _.constant({
FILL_AMOUNT.div(4) gas: 1,
fee: FILL_AMOUNT.div(4)
.times(nativeFeeRate) .times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE), .dividedToIntegerBy(ETH_TO_MAKER_RATE),
), }),
}; };
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), getBestNativeTokenSellRate: createGetBestNativeSellRate(ETH_TO_MAKER_RATE),
}); });
const improvedOrdersResponse = await getMarketSellOrdersAsync( const improvedOrdersResponse = await getMarketSellOrdersAsync(
marketOperationUtils, marketOperationUtils,
@ -1073,7 +1071,7 @@ describe('MarketOperationUtils tests', () => {
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.Uniswap, ERC20BridgeSource.Uniswap,
@ -1093,15 +1091,16 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2], [ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2],
}; };
const feeSchedule = { const feeSchedule = {
[ERC20BridgeSource.Uniswap]: _.constant( [ERC20BridgeSource.Uniswap]: _.constant({
FILL_AMOUNT.div(4) gas: 1,
fee: FILL_AMOUNT.div(4)
.times(uniswapFeeRate) .times(uniswapFeeRate)
.dividedToIntegerBy(ETH_TO_MAKER_RATE), .dividedToIntegerBy(ETH_TO_MAKER_RATE),
), }),
}; };
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), getBestNativeTokenSellRate: createGetBestNativeSellRate(ETH_TO_MAKER_RATE),
}); });
const improvedOrdersResponse = await getMarketSellOrdersAsync( const improvedOrdersResponse = await getMarketSellOrdersAsync(
marketOperationUtils, marketOperationUtils,
@ -1110,7 +1109,7 @@ describe('MarketOperationUtils tests', () => {
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.SushiSwap, ERC20BridgeSource.SushiSwap,
@ -1128,7 +1127,7 @@ describe('MarketOperationUtils tests', () => {
}; };
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), getBestNativeTokenSellRate: createGetBestNativeSellRate(ETH_TO_MAKER_RATE),
}); });
const improvedOrdersResponse = await getMarketSellOrdersAsync( const improvedOrdersResponse = await getMarketSellOrdersAsync(
marketOperationUtils, marketOperationUtils,
@ -1137,7 +1136,7 @@ describe('MarketOperationUtils tests', () => {
{ ...DEFAULT_OPTS, numSamples: 4 }, { ...DEFAULT_OPTS, numSamples: 4 },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.SushiSwap, ERC20BridgeSource.SushiSwap,
ERC20BridgeSource.Uniswap, ERC20BridgeSource.Uniswap,
@ -1164,7 +1163,7 @@ describe('MarketOperationUtils tests', () => {
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.source);
const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap]; const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
const secondSources: ERC20BridgeSource[] = []; const secondSources: ERC20BridgeSource[] = [];
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
@ -1237,7 +1236,7 @@ describe('MarketOperationUtils tests', () => {
}; };
replaceSamplerOps({ replaceSamplerOps({
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), getBestNativeTokenSellRate: createGetBestNativeSellRate(ETH_TO_MAKER_RATE),
}); });
const optimizer = new MarketOperationUtils(MOCK_SAMPLER, contractAddresses, ORDER_DOMAIN); const optimizer = new MarketOperationUtils(MOCK_SAMPLER, contractAddresses, ORDER_DOMAIN);
const gasPrice = 100e9; // 100 gwei const gasPrice = 100e9; // 100 gwei
@ -1262,7 +1261,7 @@ describe('MarketOperationUtils tests', () => {
}, },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.source);
const expectedSources = [ERC20BridgeSource.LiquidityProvider]; const expectedSources = [ERC20BridgeSource.LiquidityProvider];
expect(orderSources).to.deep.eq(expectedSources); expect(orderSources).to.deep.eq(expectedSources);
}); });
@ -1458,7 +1457,7 @@ describe('MarketOperationUtils tests', () => {
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
expect(improvedOrders).to.not.be.length(0); expect(improvedOrders).to.not.be.length(0);
for (const order of improvedOrders) { for (const order of improvedOrders) {
const expectedTakerAmount = order.fills[0].output; const expectedTakerAmount = order.fill.output;
const slippage = order.takerAmount.div(expectedTakerAmount.plus(1)).minus(1); const slippage = order.takerAmount.div(expectedTakerAmount.plus(1)).minus(1);
assertRoughlyEquals(slippage, bridgeSlippage, 1); assertRoughlyEquals(slippage, bridgeSlippage, 1);
} }
@ -1480,7 +1479,7 @@ describe('MarketOperationUtils tests', () => {
{ ...DEFAULT_OPTS, numSamples: 4 }, { ...DEFAULT_OPTS, numSamples: 4 },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.SushiSwap, ERC20BridgeSource.SushiSwap,
ERC20BridgeSource.Uniswap, ERC20BridgeSource.Uniswap,
@ -1505,15 +1504,16 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Curve]: [0.1, 0.1, 0.1, 0.1], [ERC20BridgeSource.Curve]: [0.1, 0.1, 0.1, 0.1],
}; };
const feeSchedule = { const feeSchedule = {
[ERC20BridgeSource.Native]: _.constant( [ERC20BridgeSource.Native]: _.constant({
FILL_AMOUNT.div(4) gas: 1,
fee: FILL_AMOUNT.div(4)
.times(nativeFeeRate) .times(nativeFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE), .dividedToIntegerBy(ETH_TO_TAKER_RATE),
), }),
}; };
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE), getBestNativeTokenSellRate: createGetBestNativeSellRate(ETH_TO_TAKER_RATE),
}); });
const improvedOrdersResponse = await getMarketBuyOrdersAsync( const improvedOrdersResponse = await getMarketBuyOrdersAsync(
marketOperationUtils, marketOperationUtils,
@ -1522,7 +1522,7 @@ describe('MarketOperationUtils tests', () => {
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Uniswap, ERC20BridgeSource.Uniswap,
ERC20BridgeSource.SushiSwap, ERC20BridgeSource.SushiSwap,
@ -1544,15 +1544,16 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.SushiSwap]: [0.92, 0.1, 0.1, 0.1], [ERC20BridgeSource.SushiSwap]: [0.92, 0.1, 0.1, 0.1],
}; };
const feeSchedule = { const feeSchedule = {
[ERC20BridgeSource.Uniswap]: _.constant( [ERC20BridgeSource.Uniswap]: _.constant({
FILL_AMOUNT.div(4) gas: 1,
fee: FILL_AMOUNT.div(4)
.times(uniswapFeeRate) .times(uniswapFeeRate)
.dividedToIntegerBy(ETH_TO_TAKER_RATE), .dividedToIntegerBy(ETH_TO_TAKER_RATE),
), }),
}; };
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE), getBestNativeTokenSellRate: createGetBestNativeSellRate(ETH_TO_TAKER_RATE),
}); });
const improvedOrdersResponse = await getMarketBuyOrdersAsync( const improvedOrdersResponse = await getMarketBuyOrdersAsync(
marketOperationUtils, marketOperationUtils,
@ -1561,7 +1562,7 @@ describe('MarketOperationUtils tests', () => {
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.source);
const expectedSources = [ const expectedSources = [
ERC20BridgeSource.Native, ERC20BridgeSource.Native,
ERC20BridgeSource.SushiSwap, ERC20BridgeSource.SushiSwap,
@ -1587,7 +1588,7 @@ describe('MarketOperationUtils tests', () => {
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.source);
const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap]; const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
const secondSources: ERC20BridgeSource[] = []; const secondSources: ERC20BridgeSource[] = [];
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
@ -1608,7 +1609,7 @@ describe('MarketOperationUtils tests', () => {
}; };
replaceSamplerOps({ replaceSamplerOps({
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE), getBestNativeTokenSellRate: createGetBestNativeSellRate(ETH_TO_TAKER_RATE),
}); });
const optimizer = new MarketOperationUtils(MOCK_SAMPLER, contractAddresses, ORDER_DOMAIN); const optimizer = new MarketOperationUtils(MOCK_SAMPLER, contractAddresses, ORDER_DOMAIN);
const exchangeProxyOverhead = (sourceFlags: bigint) => const exchangeProxyOverhead = (sourceFlags: bigint) =>
@ -1632,77 +1633,11 @@ describe('MarketOperationUtils tests', () => {
}, },
); );
const improvedOrders = improvedOrdersResponse.optimizedOrders; const improvedOrders = improvedOrdersResponse.optimizedOrders;
const orderSources = improvedOrders.map(o => o.fills[0].source); const orderSources = improvedOrders.map(o => o.source);
const expectedSources = [ERC20BridgeSource.LiquidityProvider]; const expectedSources = [ERC20BridgeSource.LiquidityProvider];
expect(orderSources).to.deep.eq(expectedSources); expect(orderSources).to.deep.eq(expectedSources);
}); });
}); });
}); });
describe('createFills', () => {
const takerAmount = new BigNumber(5000000);
const outputAmountPerEth = new BigNumber(0.5);
// tslint:disable-next-line:no-object-literal-type-assertion
const smallOrder: NativeOrderWithFillableAmounts = {
order: {
...new LimitOrder({
chainId: 1,
maker: 'SMALL_ORDER',
takerAmount,
makerAmount: takerAmount.times(2),
}),
},
fillableMakerAmount: takerAmount.times(2),
fillableTakerAmount: takerAmount,
fillableTakerFeeAmount: new BigNumber(0),
type: FillQuoteTransformerOrderType.Limit,
signature: SIGNATURE,
};
const largeOrder: NativeOrderWithFillableAmounts = {
order: {
...new LimitOrder({
chainId: 1,
maker: 'LARGE_ORDER',
takerAmount: smallOrder.order.takerAmount.times(2),
makerAmount: smallOrder.order.makerAmount.times(2),
}),
},
fillableTakerAmount: smallOrder.fillableTakerAmount.times(2),
fillableMakerAmount: smallOrder.fillableMakerAmount.times(2),
fillableTakerFeeAmount: new BigNumber(0),
type: FillQuoteTransformerOrderType.Limit,
signature: SIGNATURE,
};
const orders = [smallOrder, largeOrder];
const feeSchedule = {
[ERC20BridgeSource.Native]: _.constant(2e5),
};
it('penalizes native fill based on target amount when target is smaller', () => {
const path = createFills({
side: MarketOperation.Sell,
orders,
dexQuotes: [],
targetInput: takerAmount.minus(1),
outputAmountPerEth,
feeSchedule,
});
expect((path[0][0].fillData as NativeFillData).order.maker).to.eq(smallOrder.order.maker);
expect(path[0][0].input).to.be.bignumber.eq(takerAmount.minus(1));
});
it('penalizes native fill based on available amount when target is larger', () => {
const path = createFills({
side: MarketOperation.Sell,
orders,
dexQuotes: [],
targetInput: POSITIVE_INF,
outputAmountPerEth,
feeSchedule,
});
expect((path[0][0].fillData as NativeFillData).order.maker).to.eq(largeOrder.order.maker);
expect((path[0][1].fillData as NativeFillData).order.maker).to.eq(smallOrder.order.maker);
});
});
}); });
// tslint:disable-next-line: max-file-line-count // tslint:disable-next-line: max-file-line-count

View File

@ -1,90 +0,0 @@
import { expect } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { MarketOperation } from '../src/types';
import { Path } from '../src/utils/market_operation_utils/path';
import { ERC20BridgeSource, Fill } from '../src/utils/market_operation_utils/types';
const createFill = (
source: ERC20BridgeSource,
index: number = 0,
input: BigNumber = new BigNumber(100),
output: BigNumber = new BigNumber(100),
): Fill =>
// tslint:disable-next-line: no-object-literal-type-assertion
({
source,
input,
output,
adjustedOutput: output,
flags: BigInt(0),
sourcePathId: source,
index,
} as Fill);
describe('Path', () => {
it('Adds a fallback', () => {
const targetInput = new BigNumber(100);
const path = Path.create(
MarketOperation.Sell,
[createFill(ERC20BridgeSource.Native), createFill(ERC20BridgeSource.Native)],
targetInput,
);
const fallback = Path.create(MarketOperation.Sell, [createFill(ERC20BridgeSource.Uniswap)], targetInput);
path.addFallback(fallback);
const sources = path.fills.map(f => f.source);
expect(sources).to.deep.eq([ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap]);
});
it('Adds a fallback with LiquidityProvider', () => {
const targetInput = new BigNumber(100);
const path = Path.create(
MarketOperation.Sell,
[createFill(ERC20BridgeSource.Native), createFill(ERC20BridgeSource.LiquidityProvider)],
targetInput,
);
const fallback = Path.create(MarketOperation.Sell, [createFill(ERC20BridgeSource.Uniswap)], targetInput);
path.addFallback(fallback);
const sources = path.fills.map(f => f.source);
expect(sources).to.deep.eq([
ERC20BridgeSource.Native,
ERC20BridgeSource.LiquidityProvider,
ERC20BridgeSource.Uniswap,
]);
});
it('Handles duplicates', () => {
const targetInput = new BigNumber(100);
const path = Path.create(
MarketOperation.Sell,
[createFill(ERC20BridgeSource.Uniswap), createFill(ERC20BridgeSource.LiquidityProvider)],
targetInput,
);
const fallback = Path.create(MarketOperation.Sell, [createFill(ERC20BridgeSource.Uniswap)], targetInput);
path.addFallback(fallback);
const sources = path.fills.map(f => f.source);
expect(sources).to.deep.eq([ERC20BridgeSource.Uniswap, ERC20BridgeSource.LiquidityProvider]);
});
it('Moves Native orders to the front and appends with unused fills', () => {
const targetInput = new BigNumber(100);
const path = Path.create(
MarketOperation.Sell,
[
createFill(ERC20BridgeSource.Uniswap, 0, new BigNumber(50)),
createFill(ERC20BridgeSource.Native, 0, new BigNumber(50)),
],
targetInput,
);
const fallback = Path.create(
MarketOperation.Sell,
[
createFill(ERC20BridgeSource.Uniswap, 0, new BigNumber(50)),
createFill(ERC20BridgeSource.Uniswap, 1, new BigNumber(50)),
],
targetInput,
);
path.addFallback(fallback);
const sources = path.fills.map(f => f.source);
expect(sources).to.deep.eq([ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap, ERC20BridgeSource.Uniswap]);
});
});

View File

@ -9,11 +9,10 @@ import * as TypeMoq from 'typemoq';
import { MarketOperation, NativeOrderWithFillableAmounts } from '../src/types'; import { MarketOperation, NativeOrderWithFillableAmounts } from '../src/types';
import { import {
CollapsedFill,
DexSample, DexSample,
ERC20BridgeSource, ERC20BridgeSource,
Fill,
MultiHopFillData, MultiHopFillData,
NativeCollapsedFill,
NativeFillData, NativeFillData,
NativeLimitOrderFillData, NativeLimitOrderFillData,
NativeRfqOrderFillData, NativeRfqOrderFillData,
@ -34,7 +33,7 @@ import { getRandomAmount, getRandomSignature } from './utils/utils';
chaiSetup.configure(); chaiSetup.configure();
const expect = chai.expect; const expect = chai.expect;
function collapsedFillFromNativeOrder(order: NativeOrderWithFillableAmounts): NativeCollapsedFill { function fillFromNativeOrder(order: NativeOrderWithFillableAmounts): Fill<NativeFillData> {
const fillData = { const fillData = {
order: order.order, order: order.order,
signature: order.signature, signature: order.signature,
@ -50,7 +49,9 @@ function collapsedFillFromNativeOrder(order: NativeOrderWithFillableAmounts): Na
order.type === FillQuoteTransformerOrderType.Limit order.type === FillQuoteTransformerOrderType.Limit
? (fillData as NativeLimitOrderFillData) ? (fillData as NativeLimitOrderFillData)
: (fillData as NativeRfqOrderFillData), : (fillData as NativeRfqOrderFillData),
subFills: [], adjustedOutput: order.order.makerAmount,
flags: BigInt(0),
gas: 1,
}; };
} }
@ -111,21 +112,25 @@ describe('generateQuoteReport', async () => {
]; ];
// generate path // generate path
const uniswap2Fill: CollapsedFill = { const uniswap2Fill: Fill = {
...uniswapSample2, ...uniswapSample2,
subFills: [],
sourcePathId: hexUtils.random(), sourcePathId: hexUtils.random(),
type: FillQuoteTransformerOrderType.Bridge, type: FillQuoteTransformerOrderType.Bridge,
adjustedOutput: uniswapSample2.output,
flags: BigInt(0),
gas: 1,
}; };
const balancer2Fill: CollapsedFill = { const balancer2Fill: Fill = {
...balancerSample2, ...balancerSample2,
subFills: [],
sourcePathId: hexUtils.random(), sourcePathId: hexUtils.random(),
type: FillQuoteTransformerOrderType.Bridge, type: FillQuoteTransformerOrderType.Bridge,
adjustedOutput: balancerSample2.output,
flags: BigInt(0),
gas: 1,
}; };
const orderbookOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder2); const orderbookOrder2Fill: Fill = fillFromNativeOrder(orderbookOrder2);
const rfqtOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(rfqtOrder2); const rfqtOrder2Fill: Fill = fillFromNativeOrder(rfqtOrder2);
const pathGenerated: CollapsedFill[] = [rfqtOrder2Fill, orderbookOrder2Fill, uniswap2Fill, balancer2Fill]; const pathGenerated: Fill[] = [rfqtOrder2Fill, orderbookOrder2Fill, uniswap2Fill, balancer2Fill];
// quote generator mock // quote generator mock
const quoteRequestor = TypeMoq.Mock.ofType<QuoteRequestor>(); const quoteRequestor = TypeMoq.Mock.ofType<QuoteRequestor>();
@ -241,20 +246,24 @@ describe('generateQuoteReport', async () => {
const nativeOrders = [orderbookOrder1, orderbookOrder2]; const nativeOrders = [orderbookOrder1, orderbookOrder2];
// generate path // generate path
const orderbookOrder1Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder1); const orderbookOrder1Fill: Fill = fillFromNativeOrder(orderbookOrder1);
const uniswap1Fill: CollapsedFill = { const uniswap1Fill: Fill = {
...uniswapSample1, ...uniswapSample1,
subFills: [],
sourcePathId: hexUtils.random(), sourcePathId: hexUtils.random(),
type: FillQuoteTransformerOrderType.Bridge, type: FillQuoteTransformerOrderType.Bridge,
adjustedOutput: uniswapSample1.output,
flags: BigInt(0),
gas: 1,
}; };
const balancer1Fill: CollapsedFill = { const balancer1Fill: Fill = {
...balancerSample1, ...balancerSample1,
subFills: [],
sourcePathId: hexUtils.random(), sourcePathId: hexUtils.random(),
type: FillQuoteTransformerOrderType.Bridge, type: FillQuoteTransformerOrderType.Bridge,
adjustedOutput: balancerSample1.output,
flags: BigInt(0),
gas: 1,
}; };
const pathGenerated: CollapsedFill[] = [orderbookOrder1Fill, uniswap1Fill, balancer1Fill]; const pathGenerated: Fill[] = [orderbookOrder1Fill, uniswap1Fill, balancer1Fill];
const orderReport = generateQuoteReport(marketOperation, nativeOrders, pathGenerated); const orderReport = generateQuoteReport(marketOperation, nativeOrders, pathGenerated);

View File

@ -5,8 +5,8 @@ import * as _ from 'lodash';
import { MarketOperation } from '../src/types'; import { MarketOperation } from '../src/types';
import { import {
CollapsedFill,
ERC20BridgeSource, ERC20BridgeSource,
Fill,
NativeLimitOrderFillData, NativeLimitOrderFillData,
OptimizedMarketOrder, OptimizedMarketOrder,
OptimizedMarketOrderBase, OptimizedMarketOrderBase,
@ -45,18 +45,16 @@ describe('quote_simulation tests', async () => {
inputFeeRate: number; inputFeeRate: number;
outputFeeRate: number; outputFeeRate: number;
count: number; count: number;
fillsCount: number;
side: MarketOperation; side: MarketOperation;
type?: FillQuoteTransformerOrderType; type?: FillQuoteTransformerOrderType;
}> = {}, }> = {},
): QuoteFillOrderCall[] { ): QuoteFillOrderCall[] {
const { fillableInput, fillableOutput, inputFeeRate, outputFeeRate, count, fillsCount, side, type } = { const { fillableInput, fillableOutput, inputFeeRate, outputFeeRate, count, side, type } = {
fillableInput: getRandomOrderSize(), fillableInput: getRandomOrderSize(),
fillableOutput: getRandomOrderSize(), fillableOutput: getRandomOrderSize(),
inputFeeRate: 0, inputFeeRate: 0,
outputFeeRate: 0, outputFeeRate: 0,
count: 3, count: 3,
fillsCount: 3,
side: MarketOperation.Sell, side: MarketOperation.Sell,
...opts, ...opts,
}; };
@ -83,7 +81,6 @@ describe('quote_simulation tests', async () => {
return { return {
order: createQuoteFillOrderOrder(totalInputs[i], totalOutputs[i], { order: createQuoteFillOrderOrder(totalInputs[i], totalOutputs[i], {
side, side,
fillsCount,
filledInput: filledInputs[i], filledInput: filledInputs[i],
takerInputFee: inputFees[i].abs(), takerInputFee: inputFees[i].abs(),
takerOutputFee: outputFees[i].abs(), takerOutputFee: outputFees[i].abs(),
@ -102,19 +99,17 @@ describe('quote_simulation tests', async () => {
output: BigNumber, output: BigNumber,
opts: Partial<{ opts: Partial<{
filledInput: BigNumber; filledInput: BigNumber;
fillsCount: number;
side: MarketOperation; side: MarketOperation;
takerInputFee: BigNumber; takerInputFee: BigNumber;
takerOutputFee: BigNumber; takerOutputFee: BigNumber;
type: FillQuoteTransformerOrderType; type: FillQuoteTransformerOrderType;
}> = {}, }> = {},
): OptimizedMarketOrderBase<NativeLimitOrderFillData> { ): OptimizedMarketOrderBase<NativeLimitOrderFillData> {
const { filledInput, fillsCount, side, takerInputFee, takerOutputFee, type } = _.merge( const { filledInput, side, takerInputFee, takerOutputFee, type } = _.merge(
{}, {},
{ {
side: MarketOperation.Sell, side: MarketOperation.Sell,
filledInput: ZERO, filledInput: ZERO,
fillsCount: 3,
takerInputFee: ZERO, takerInputFee: ZERO,
takerOutputFee: ZERO, takerOutputFee: ZERO,
type: FillQuoteTransformerOrderType.Limit, type: FillQuoteTransformerOrderType.Limit,
@ -160,46 +155,23 @@ describe('quote_simulation tests', async () => {
maxTakerTokenFillAmount: fillableTakerAmount, maxTakerTokenFillAmount: fillableTakerAmount,
}, },
type, type,
fills: createOrderCollapsedFills(fillableInput, fillableOutput, fillsCount), fill: createOrderFill(fillableInput, fillableOutput),
}; };
return order; return order;
} }
const nativeSourcePathId = hexUtils.random(); const nativeSourcePathId = hexUtils.random();
function createOrderCollapsedFills(input: BigNumber, output: BigNumber, count: number): CollapsedFill[] { function createOrderFill(input: BigNumber, output: BigNumber): Fill {
const inputs = subdivideAmount(input, count);
const outputs = subdivideAmount(output, count);
return _.times(count, i => {
const subFillInputs = subdivideAmount(inputs[i], count);
const subFillOutputs = subdivideAmount(outputs[i], count);
return { return {
type: FillQuoteTransformerOrderType.Bridge, type: FillQuoteTransformerOrderType.Bridge,
sourcePathId: nativeSourcePathId, sourcePathId: nativeSourcePathId,
source: ERC20BridgeSource.Uniswap, source: ERC20BridgeSource.Uniswap,
fillData: {}, fillData: {},
input: inputs[i], input,
output: outputs[i], output,
subFills: _.times(count, j => ({ flags: BigInt(0),
input: subFillInputs[j], adjustedOutput: output,
output: subFillOutputs[j], gas: 1,
})),
}; };
});
}
function countCollapsedFills(fillOrders: QuoteFillOrderCall[] | OptimizedMarketOrder[]): number {
let count = 0;
if ((fillOrders as any)[0].fills) {
const orders = (fillOrders as any) as OptimizedMarketOrder[];
for (const o of orders) {
count += o.fills.length;
}
} else {
const orders = (fillOrders as any) as QuoteFillOrderCall[];
for (const fo of orders) {
count += fo.order.fills.length;
}
}
return count;
} }
function randomSide(): MarketOperation { function randomSide(): MarketOperation {
@ -237,14 +209,12 @@ describe('quote_simulation tests', async () => {
describe('single order', () => { describe('single order', () => {
it('can exactly fill one order', () => { it('can exactly fill one order', () => {
const side = randomSide(); const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize(); const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize();
const fillOrders = createQuoteFillOrders({ const fillOrders = createQuoteFillOrders({
fillableInput, fillableInput,
fillableOutput, fillableOutput,
side, side,
fillsCount,
count: 1, count: 1,
}); });
const result = fillQuoteOrders(fillOrders, fillableInput, ONE, GAS_SCHEDULE); const result = fillQuoteOrders(fillOrders, fillableInput, ONE, GAS_SCHEDULE);
@ -253,19 +223,16 @@ describe('quote_simulation tests', async () => {
expect(totalFilledInput).to.bignumber.eq(fillableInput); expect(totalFilledInput).to.bignumber.eq(fillableInput);
assertRoughlyEquals(totalFilledOutput, fillableOutput); assertRoughlyEquals(totalFilledOutput, fillableOutput);
expect(result.protocolFee).to.bignumber.eq(1); expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.eq(fillsCount);
}); });
it('can partially fill one simple order', () => { it('can partially fill one simple order', () => {
const side = randomSide(); const side = randomSide();
const fillsCount = 1;
const fillableInput = getRandomOrderSize(); const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize();
const fillOrders = createQuoteFillOrders({ const fillOrders = createQuoteFillOrders({
fillableInput, fillableInput,
fillableOutput, fillableOutput,
side, side,
fillsCount,
count: 1, count: 1,
}); });
const inputFillAmount = fillableInput.times(2 / 3).integerValue(); const inputFillAmount = fillableInput.times(2 / 3).integerValue();
@ -279,19 +246,16 @@ describe('quote_simulation tests', async () => {
.integerValue(); .integerValue();
assertRoughlyEquals(totalFilledOutput, expectedOutputFilledAmount); assertRoughlyEquals(totalFilledOutput, expectedOutputFilledAmount);
expect(result.protocolFee).to.bignumber.eq(1); expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.eq(1);
}); });
it('can partially fill one batched order', () => { it('can partially fill one batched order', () => {
const side = randomSide(); const side = randomSide();
const fillsCount = 3;
const fillableInput = getRandomOrderSize(); const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize();
const fillOrders = createQuoteFillOrders({ const fillOrders = createQuoteFillOrders({
fillableInput, fillableInput,
fillableOutput, fillableOutput,
side, side,
fillsCount,
count: 1, count: 1,
}); });
const inputFillAmount = fillableInput.times(2 / 3).integerValue(); const inputFillAmount = fillableInput.times(2 / 3).integerValue();
@ -301,20 +265,16 @@ describe('quote_simulation tests', async () => {
expect(totalFilledInput).to.bignumber.eq(inputFillAmount); expect(totalFilledInput).to.bignumber.eq(inputFillAmount);
expect(totalFilledOutput).to.bignumber.lt(fillableOutput); expect(totalFilledOutput).to.bignumber.lt(fillableOutput);
expect(result.protocolFee).to.bignumber.eq(1); expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.gte(1);
expect(result.gas).to.lte(fillsCount);
}); });
it('does not over fill one order', () => { it('does not over fill one order', () => {
const side = randomSide(); const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize(); const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize();
const fillOrders = createQuoteFillOrders({ const fillOrders = createQuoteFillOrders({
fillableInput, fillableInput,
fillableOutput, fillableOutput,
side, side,
fillsCount,
count: 1, count: 1,
}); });
const inputFillAmount = fillableInput.times(3 / 2).integerValue(); const inputFillAmount = fillableInput.times(3 / 2).integerValue();
@ -324,12 +284,10 @@ describe('quote_simulation tests', async () => {
expect(totalFilledInput).to.bignumber.eq(fillableInput); expect(totalFilledInput).to.bignumber.eq(fillableInput);
assertRoughlyEquals(totalFilledOutput, fillableOutput); assertRoughlyEquals(totalFilledOutput, fillableOutput);
expect(result.protocolFee).to.bignumber.eq(1); expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.eq(fillsCount);
}); });
it('can exactly fill one order with input fees', () => { it('can exactly fill one order with input fees', () => {
const side = randomSide(); const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize(); const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize();
const inputFeeRate = getRandomFeeRate(); const inputFeeRate = getRandomFeeRate();
@ -338,7 +296,6 @@ describe('quote_simulation tests', async () => {
fillableOutput, fillableOutput,
inputFeeRate, inputFeeRate,
side, side,
fillsCount,
count: 1, count: 1,
}); });
const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate;
@ -350,12 +307,10 @@ describe('quote_simulation tests', async () => {
assertRoughlyEquals(totalFilledOutput, fillableOutput); assertRoughlyEquals(totalFilledOutput, fillableOutput);
assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate);
expect(result.protocolFee).to.bignumber.eq(1); expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.eq(fillsCount);
}); });
it('can partially fill one order with input fees', () => { it('can partially fill one order with input fees', () => {
const side = randomSide(); const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize(); const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize();
const inputFeeRate = getRandomFeeRate(); const inputFeeRate = getRandomFeeRate();
@ -364,7 +319,6 @@ describe('quote_simulation tests', async () => {
fillableOutput, fillableOutput,
inputFeeRate, inputFeeRate,
side, side,
fillsCount,
count: 1, count: 1,
}); });
const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate;
@ -377,12 +331,10 @@ describe('quote_simulation tests', async () => {
expect(totalFilledOutput).to.bignumber.lt(fillableOutput); expect(totalFilledOutput).to.bignumber.lt(fillableOutput);
assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate);
expect(result.protocolFee).to.bignumber.eq(1); expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.lte(fillsCount);
}); });
it('does not over fill one order with input fees', () => { it('does not over fill one order with input fees', () => {
const side = randomSide(); const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize(); const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize();
const inputFeeRate = getRandomFeeRate(); const inputFeeRate = getRandomFeeRate();
@ -391,7 +343,6 @@ describe('quote_simulation tests', async () => {
fillableOutput, fillableOutput,
inputFeeRate, inputFeeRate,
side, side,
fillsCount,
count: 1, count: 1,
}); });
const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate;
@ -404,12 +355,10 @@ describe('quote_simulation tests', async () => {
assertRoughlyEquals(totalFilledOutput, fillableOutput); assertRoughlyEquals(totalFilledOutput, fillableOutput);
assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate);
expect(result.protocolFee).to.bignumber.eq(1); expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.eq(fillsCount);
}); });
it('can exactly fill one order with output fees', () => { it('can exactly fill one order with output fees', () => {
const side = randomSide(); const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize(); const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize();
const outputFeeRate = getRandomFeeRate(); const outputFeeRate = getRandomFeeRate();
@ -418,7 +367,6 @@ describe('quote_simulation tests', async () => {
fillableOutput, fillableOutput,
outputFeeRate, outputFeeRate,
side, side,
fillsCount,
count: 1, count: 1,
}); });
const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate;
@ -430,12 +378,10 @@ describe('quote_simulation tests', async () => {
assertRoughlyEquals(totalFilledOutput, totalFillableOutput); assertRoughlyEquals(totalFilledOutput, totalFillableOutput);
assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate);
expect(result.protocolFee).to.bignumber.eq(1); expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.eq(fillsCount);
}); });
it('can partial fill one order with output fees', () => { it('can partial fill one order with output fees', () => {
const side = randomSide(); const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize(); const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize();
const outputFeeRate = getRandomFeeRate(); const outputFeeRate = getRandomFeeRate();
@ -444,7 +390,6 @@ describe('quote_simulation tests', async () => {
fillableOutput, fillableOutput,
outputFeeRate, outputFeeRate,
side, side,
fillsCount,
count: 1, count: 1,
}); });
const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate;
@ -457,12 +402,10 @@ describe('quote_simulation tests', async () => {
expect(totalFilledOutput).to.bignumber.lt(totalFillableOutput); expect(totalFilledOutput).to.bignumber.lt(totalFillableOutput);
assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate);
expect(result.protocolFee).to.bignumber.eq(1); expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.lte(fillsCount);
}); });
it('does not over fill one order with output fees', () => { it('does not over fill one order with output fees', () => {
const side = randomSide(); const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize(); const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize();
const outputFeeRate = getRandomFeeRate(); const outputFeeRate = getRandomFeeRate();
@ -471,7 +414,6 @@ describe('quote_simulation tests', async () => {
fillableOutput, fillableOutput,
outputFeeRate, outputFeeRate,
side, side,
fillsCount,
count: 1, count: 1,
}); });
const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate;
@ -484,19 +426,16 @@ describe('quote_simulation tests', async () => {
assertRoughlyEquals(totalFilledOutput, totalFillableOutput); assertRoughlyEquals(totalFilledOutput, totalFillableOutput);
assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate);
expect(result.protocolFee).to.bignumber.eq(1); expect(result.protocolFee).to.bignumber.eq(1);
expect(result.gas).to.eq(fillsCount);
}); });
it('does not charge a protocol fee for rfq orders', () => { it('does not charge a protocol fee for rfq orders', () => {
const side = randomSide(); const side = randomSide();
const fillsCount = _.random(1, 3);
const fillableInput = getRandomOrderSize(); const fillableInput = getRandomOrderSize();
const fillableOutput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize();
const fillOrders = createQuoteFillOrders({ const fillOrders = createQuoteFillOrders({
fillableInput, fillableInput,
fillableOutput, fillableOutput,
side, side,
fillsCount,
count: 1, count: 1,
type: FillQuoteTransformerOrderType.Rfq, type: FillQuoteTransformerOrderType.Rfq,
}); });
@ -506,7 +445,6 @@ describe('quote_simulation tests', async () => {
expect(totalFilledInput).to.bignumber.eq(fillableInput); expect(totalFilledInput).to.bignumber.eq(fillableInput);
assertRoughlyEquals(totalFilledOutput, fillableOutput); assertRoughlyEquals(totalFilledOutput, fillableOutput);
expect(result.protocolFee).to.bignumber.eq(0); expect(result.protocolFee).to.bignumber.eq(0);
expect(result.gas).to.eq(fillsCount);
}); });
}); });
@ -522,7 +460,6 @@ describe('quote_simulation tests', async () => {
expect(totalFilledInput).to.bignumber.eq(fillableInput); expect(totalFilledInput).to.bignumber.eq(fillableInput);
expect(totalFilledOutput).to.bignumber.eq(fillableOutput); expect(totalFilledOutput).to.bignumber.eq(fillableOutput);
expect(result.protocolFee).to.bignumber.eq(fillOrders.length); expect(result.protocolFee).to.bignumber.eq(fillOrders.length);
expect(result.gas).to.eq(countCollapsedFills(fillOrders));
}); });
it('can partial fill orders', () => { it('can partial fill orders', () => {
@ -551,7 +488,6 @@ describe('quote_simulation tests', async () => {
expect(totalFilledInput).to.bignumber.eq(fillableInput); expect(totalFilledInput).to.bignumber.eq(fillableInput);
expect(totalFilledOutput).to.bignumber.eq(fillableOutput); expect(totalFilledOutput).to.bignumber.eq(fillableOutput);
expect(result.protocolFee).to.bignumber.eq(fillOrders.length); expect(result.protocolFee).to.bignumber.eq(fillOrders.length);
expect(result.gas).to.eq(countCollapsedFills(fillOrders));
}); });
it('can exactly fill orders with input fees', () => { it('can exactly fill orders with input fees', () => {
@ -574,7 +510,6 @@ describe('quote_simulation tests', async () => {
assertRoughlyEquals(totalFilledOutput, fillableOutput); assertRoughlyEquals(totalFilledOutput, fillableOutput);
assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate);
expect(result.protocolFee).to.bignumber.eq(fillOrders.length); expect(result.protocolFee).to.bignumber.eq(fillOrders.length);
expect(result.gas).to.eq(countCollapsedFills(fillOrders));
}); });
it('can partial fill orders with input fees', () => { it('can partial fill orders with input fees', () => {
@ -598,7 +533,6 @@ describe('quote_simulation tests', async () => {
expect(totalFilledOutput).to.bignumber.lt(fillableOutput); expect(totalFilledOutput).to.bignumber.lt(fillableOutput);
assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate);
expect(result.protocolFee).to.bignumber.lte(fillOrders.length); expect(result.protocolFee).to.bignumber.lte(fillOrders.length);
expect(result.gas).to.lte(countCollapsedFills(fillOrders));
}); });
it('does not over fill orders with input fees', () => { it('does not over fill orders with input fees', () => {
@ -622,7 +556,6 @@ describe('quote_simulation tests', async () => {
assertRoughlyEquals(totalFilledOutput, fillableOutput); assertRoughlyEquals(totalFilledOutput, fillableOutput);
assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate);
expect(result.protocolFee).to.bignumber.eq(fillOrders.length); expect(result.protocolFee).to.bignumber.eq(fillOrders.length);
expect(result.gas).to.eq(countCollapsedFills(fillOrders));
}); });
it('can exactly fill orders with output fees', () => { it('can exactly fill orders with output fees', () => {
@ -645,7 +578,6 @@ describe('quote_simulation tests', async () => {
assertRoughlyEquals(totalFilledOutput, totalFillableOutput); assertRoughlyEquals(totalFilledOutput, totalFillableOutput);
assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate);
expect(result.protocolFee).to.bignumber.eq(fillOrders.length); expect(result.protocolFee).to.bignumber.eq(fillOrders.length);
expect(result.gas).to.eq(countCollapsedFills(fillOrders));
}); });
it('can partial fill orders with output fees', () => { it('can partial fill orders with output fees', () => {
@ -669,7 +601,6 @@ describe('quote_simulation tests', async () => {
expect(totalFilledOutput).to.bignumber.lt(totalFillableOutput); expect(totalFilledOutput).to.bignumber.lt(totalFillableOutput);
assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate);
expect(result.protocolFee).to.bignumber.lte(fillOrders.length); expect(result.protocolFee).to.bignumber.lte(fillOrders.length);
expect(result.gas).to.lte(countCollapsedFills(fillOrders));
}); });
it('does not over fill orders with output fees', () => { it('does not over fill orders with output fees', () => {
@ -693,7 +624,6 @@ describe('quote_simulation tests', async () => {
assertRoughlyEquals(totalFilledOutput, totalFillableOutput); assertRoughlyEquals(totalFilledOutput, totalFillableOutput);
assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate);
expect(result.protocolFee).to.bignumber.eq(fillOrders.length); expect(result.protocolFee).to.bignumber.eq(fillOrders.length);
expect(result.gas).to.eq(countCollapsedFills(fillOrders));
}); });
}); });
}); });
@ -771,7 +701,6 @@ describe('quote_simulation tests', async () => {
gasPrice: ONE, gasPrice: ONE,
opts: { gasSchedule: GAS_SCHEDULE, protocolFeeMultiplier: ONE }, opts: { gasSchedule: GAS_SCHEDULE, protocolFeeMultiplier: ONE },
}); });
expect(result.gas).to.eq(countCollapsedFills(orders));
expect(result.protocolFeeAmount).to.bignumber.eq(orders.length); expect(result.protocolFeeAmount).to.bignumber.eq(orders.length);
expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0);
expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0);
@ -895,7 +824,6 @@ describe('quote_simulation tests', async () => {
gasPrice: ONE, gasPrice: ONE,
opts: { gasSchedule: GAS_SCHEDULE, protocolFeeMultiplier: ONE }, opts: { gasSchedule: GAS_SCHEDULE, protocolFeeMultiplier: ONE },
}); });
expect(result.gas).to.eq(countCollapsedFills(orders));
expect(result.protocolFeeAmount).to.bignumber.eq(orders.length); expect(result.protocolFeeAmount).to.bignumber.eq(orders.length);
assertRoughlyEquals(result.makerAssetAmount, fillableInput); assertRoughlyEquals(result.makerAssetAmount, fillableInput);