diff --git a/CODEOWNERS b/CODEOWNERS index 531b3fc000..e6d4bd8d36 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,18 +1,20 @@ # See https://help.github.com/articles/about-codeowners/ + # for more info about CODEOWNERS file # It uses the same pattern rule for gitignore file + # https://git-scm.com/docs/gitignore#_pattern_format -# Website -packages/asset-swapper/ @BMillman19 @fragosti @dave4506 -packages/instant/ @BMillman19 @fragosti @dave4506 +packages/asset-swapper/ @dekz @mzhu25 @dextracker @kh-chang # Dev tools & setup -.circleci/ @dorothy-zbornak -packages/contract-addresses/ @abandeali1 -packages/contract-artifacts/ @abandeali1 -packages/order-utils/ @dorothy-zbornak + +.circleci/ @dekz @mzhu25 +packages/contract-addresses/ @dekz @mzhu25 @dextracker @kh-chang +packages/contract-artifacts/ @dekz @mzhu25 +packages/protocol-utils/ @dekz @mzhu25 # Protocol/smart contracts -contracts/ @abandeali1 @hysz @dorothy-zbornak @mzhu25 + +contracts/ @dekz @mzhu25 @dextracker diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 97e399f7b5..5ff40b0f45 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -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", "changes": [ diff --git a/packages/asset-swapper/package.json b/packages/asset-swapper/package.json index eb83be5070..9436dc27a9 100644 --- a/packages/asset-swapper/package.json +++ b/packages/asset-swapper/package.json @@ -40,7 +40,7 @@ "config": { "publicInterfaceContracts": "ERC20BridgeSampler,BalanceChecker,FakeTaker", "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": { "assets": [] } diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index c13b2d942e..78ff8bec14 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -4,7 +4,7 @@ export { ContractTxFunctionObj, SendTransactionOpts, } from '@0x/base-contract'; -export { ContractAddresses } from '@0x/contract-addresses'; +export { ContractAddresses, ChainId, getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; export { V4RFQFirmQuote, V4RFQIndicativeQuote, @@ -132,6 +132,7 @@ export { BUY_SOURCE_FILTER_BY_CHAIN_ID, SELL_SOURCE_FILTER_BY_CHAIN_ID, NATIVE_FEE_TOKEN_BY_CHAIN_ID, + ZERO_AMOUNT, } from './utils/market_operation_utils/constants'; export { Parameters, @@ -141,7 +142,6 @@ export { export { BalancerFillData, BancorFillData, - CollapsedFill, CurveFillData, CurveFunctionSelectors, CurveInfo, @@ -150,7 +150,9 @@ export { ERC20BridgeSource, ExchangeProxyOverhead, FeeSchedule, + GasSchedule, Fill, + FillAdjustor, FillData, GetMarketOrdersRfqOpts, LiquidityProviderFillData, @@ -159,7 +161,6 @@ export { MarketDepthSide, MooniswapFillData, MultiHopFillData, - NativeCollapsedFill, NativeRfqOrderFillData, NativeLimitOrderFillData, NativeFillData, @@ -168,6 +169,7 @@ export { TokenAdjacencyGraph, UniswapV2FillData, } from './utils/market_operation_utils/types'; +export { IdentityFillAdjustor } from './utils/market_operation_utils/identity_fill_adjustor'; export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; export { BridgeQuoteReportEntry, @@ -191,3 +193,5 @@ export type Native = ERC20BridgeSource.Native; export type MultiHop = ERC20BridgeSource.MultiHop; export { rfqtMocker, RfqtQuoteEndpoint } from './utils/rfqt_mocker'; + +export { adjustOutput } from './utils/market_operation_utils/fills'; diff --git a/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts index 9c1d474f99..da43fbced3 100644 --- a/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts +++ b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts @@ -282,7 +282,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { // ETH buy/sell is supported ![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 { calldataHexString: this._exchangeProxy .sellToLiquidityProvider( @@ -311,7 +311,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { this.chainId === ChainId.Mainnet && isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.Mooniswap]) ) { - const fillData = slippedOrders[0].fills[0].fillData as MooniswapFillData; + const fillData = slippedOrders[0].fillData as MooniswapFillData; return { calldataHexString: this._exchangeProxy .sellToLiquidityProvider( diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index c53c8cc8c5..1d1b88a8b7 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -33,8 +33,8 @@ import { DexOrderSampler } from './utils/market_operation_utils/sampler'; import { SourceFilters } from './utils/market_operation_utils/source_filters'; import { ERC20BridgeSource, - FeeSchedule, FillData, + GasSchedule, GetMarketOrdersOpts, MarketDepth, MarketDepthSide, @@ -366,9 +366,11 @@ export class SwapQuoter { const calcOpts: GetMarketOrdersOpts = { ...cloneOpts, gasPrice, - feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData: FillData) => - gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)), - ), + feeSchedule: _.mapValues(opts.gasSchedule, gasCost => (fillData: FillData) => { + const gas = gasCost ? gasCost(fillData) : 0; + const fee = gasPrice.times(gas); + return { gas, fee }; + }), exchangeProxyOverhead: flags => gasPrice.times(opts.exchangeProxyOverhead(flags)), }; // pass the QuoteRequestor on if rfqt enabled @@ -502,7 +504,7 @@ function createSwapQuote( operation: MarketOperation, assetFillAmount: BigNumber, gasPrice: BigNumber, - gasSchedule: FeeSchedule, + gasSchedule: GasSchedule, slippage: number, ): SwapQuote { const { @@ -562,7 +564,7 @@ function calculateQuoteInfo( operation: MarketOperation, assetFillAmount: BigNumber, gasPrice: BigNumber, - gasSchedule: FeeSchedule, + gasSchedule: GasSchedule, slippage: number, ): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } { const bestCaseFillResult = simulateBestCaseFill({ @@ -591,25 +593,23 @@ function calculateQuoteInfo( function calculateTwoHopQuoteInfo( optimizedOrders: OptimizedMarketOrder[], operation: MarketOperation, - gasSchedule: FeeSchedule, + gasSchedule: GasSchedule, slippage: number, ): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } { const [firstHopOrder, secondHopOrder] = optimizedOrders; - const [firstHopFill] = firstHopOrder.fills; - const [secondHopFill] = secondHopOrder.fills; const gas = new BigNumber( gasSchedule[ERC20BridgeSource.MultiHop]!({ - firstHopSource: _.pick(firstHopFill, 'source', 'fillData'), - secondHopSource: _.pick(secondHopFill, 'source', 'fillData'), + firstHopSource: _.pick(firstHopOrder, 'source', 'fillData'), + secondHopSource: _.pick(secondHopOrder, 'source', 'fillData'), }), ).toNumber(); const isSell = operation === MarketOperation.Sell; return { bestCaseQuoteInfo: { - makerAmount: isSell ? secondHopFill.output : secondHopFill.input, - takerAmount: isSell ? firstHopFill.input : firstHopFill.output, - totalTakerAmount: isSell ? firstHopFill.input : firstHopFill.output, + makerAmount: isSell ? secondHopOrder.fill.output : secondHopOrder.fill.input, + takerAmount: isSell ? firstHopOrder.fill.input : firstHopOrder.fill.output, + totalTakerAmount: isSell ? firstHopOrder.fill.input : firstHopOrder.fill.output, feeTakerTokenAmount: constants.ZERO_AMOUNT, protocolFeeInWeiAmount: constants.ZERO_AMOUNT, gas, @@ -635,7 +635,7 @@ function calculateTwoHopQuoteInfo( [ERC20BridgeSource.MultiHop]: { proportion: new BigNumber(1), intermediateToken: secondHopOrder.takerToken, - hops: [firstHopFill.source, secondHopFill.source], + hops: [firstHopOrder.source, secondHopOrder.source], }, }, }; diff --git a/packages/asset-swapper/src/utils/market_operation_utils/comparison_price.ts b/packages/asset-swapper/src/utils/market_operation_utils/comparison_price.ts index d6411fc27d..2fbbe05605 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/comparison_price.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/comparison_price.ts @@ -48,7 +48,7 @@ export function getComparisonPrices( } else { try { 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)); feeInEth = fillFeeInEth.plus(exchangeProxyOverheadInEth); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts index c8e26062f8..83e12e3913 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -5,6 +5,7 @@ import { formatBytes32String } from '@ethersproject/strings'; import { TokenAdjacencyGraphBuilder } from '../token_adjacency_graph_builder'; +import { IdentityFillAdjustor } from './identity_fill_adjustor'; import { SourceFilters } from './source_filters'; import { AaveV2FillData, @@ -19,6 +20,7 @@ import { FeeSchedule, FillData, FinalUniswapV3FillData, + GasSchedule, GeistFillData, GetMarketOrdersOpts, isFinalUniswapV3FillData, @@ -2381,7 +2383,7 @@ const uniswapV2CloneGasSchedule = (fillData?: FillData) => { * the ethereum transaction cost (21k) */ // tslint:disable:custom-no-magic-numbers -export const DEFAULT_GAS_SCHEDULE: Required = { +export const DEFAULT_GAS_SCHEDULE: Required = { [ERC20BridgeSource.Native]: fillData => { // TODO jacob re-order imports so there is no circular rependency with SignedNativeOrder const nativeFillData = fillData as { type: FillQuoteTransformerOrderType }; @@ -2569,10 +2571,21 @@ export const DEFAULT_GAS_SCHEDULE: Required = { [ERC20BridgeSource.Velodrome]: () => 160e3, }; -export const DEFAULT_FEE_SCHEDULE: Required = { ...DEFAULT_GAS_SCHEDULE }; +export const DEFAULT_FEE_SCHEDULE: Required = 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); 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 export const DEFAULT_GET_MARKET_ORDERS_OPTS: Omit = { @@ -2593,4 +2606,5 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: Omit 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({ input, output, @@ -85,122 +28,106 @@ export function ethToOutputAmount({ ethAmount: BigNumber | number; }): BigNumber { return !outputAmountPerEth.isZero() - ? outputAmountPerEth.times(ethAmount) + ? outputAmountPerEth.times(ethAmount).integerValue() : inputAmountPerEth.times(ethAmount).times(output.dividedToIntegerBy(input)); } -export function nativeOrdersToFills( +export function nativeOrderToFill( side: MarketOperation, - orders: NativeOrderWithFillableAmounts[], + order: NativeOrderWithFillableAmounts, targetInput: BigNumber = POSITIVE_INF, outputAmountPerEth: BigNumber, inputAmountPerEth: BigNumber, fees: FeeSchedule, filterNegativeAdjustedRateOrders: boolean = true, -): Fill[] { +): Fill | undefined { const sourcePathId = hexUtils.random(); // Create a single path from all orders. - let fills: Array = []; - for (const o of orders) { - const { fillableTakerAmount, fillableTakerFeeAmount, fillableMakerAmount, type } = o; - const makerAmount = fillableMakerAmount; - const takerAmount = fillableTakerAmount.plus(fillableTakerFeeAmount); - const input = side === MarketOperation.Sell ? takerAmount : makerAmount; - const output = side === MarketOperation.Sell ? makerAmount : takerAmount; - const fee = fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(o); - const outputPenalty = ethToOutputAmount({ - input, - output, - inputAmountPerEth, - outputAmountPerEth, - ethAmount: fee, - }); - // targetInput can be less than the order size - // whilst the penalty is constant, it affects the adjusted output - // only up until the target has been exhausted. - // A large order and an order at the exact target should be penalized - // the same. - const clippedInput = BigNumber.min(targetInput, input); - // scale the clipped output inline with the input - const clippedOutput = clippedInput.dividedBy(input).times(output); - const adjustedOutput = - side === MarketOperation.Sell ? clippedOutput.minus(outputPenalty) : clippedOutput.plus(outputPenalty); - const adjustedRate = - side === MarketOperation.Sell ? adjustedOutput.div(clippedInput) : clippedInput.div(adjustedOutput); - // Optionally skip orders with rates that are <= 0. - if (filterNegativeAdjustedRateOrders && adjustedRate.lte(0)) { - continue; - } - fills.push({ - sourcePathId, - adjustedRate, - adjustedOutput, - input: clippedInput, - output: clippedOutput, - flags: SOURCE_FLAGS[type === FillQuoteTransformerOrderType.Rfq ? 'RfqOrder' : 'LimitOrder'], - index: 0, // TBD - parent: undefined, // TBD - source: ERC20BridgeSource.Native, - type, - fillData: { ...o }, - }); + const { fillableTakerAmount, fillableTakerFeeAmount, fillableMakerAmount, type } = order; + const makerAmount = fillableMakerAmount; + const takerAmount = fillableTakerAmount.plus(fillableTakerFeeAmount); + const input = side === MarketOperation.Sell ? takerAmount : makerAmount; + const output = side === MarketOperation.Sell ? makerAmount : takerAmount; + const { fee, gas } = + fees[ERC20BridgeSource.Native] === undefined ? DEFAULT_FEE_ESTIMATE : fees[ERC20BridgeSource.Native]!(order); + const outputPenalty = ethToOutputAmount({ + input, + output, + inputAmountPerEth, + outputAmountPerEth, + ethAmount: fee, + }); + // targetInput can be less than the order size + // whilst the penalty is constant, it affects the adjusted output + // only up until the target has been exhausted. + // A large order and an order at the exact target should be penalized + // the same. + const clippedInput = BigNumber.min(targetInput, input); + // scale the clipped output inline with the input + const clippedOutput = clippedInput.dividedBy(input).times(output); + const adjustedOutput = + side === MarketOperation.Sell ? clippedOutput.minus(outputPenalty) : clippedOutput.plus(outputPenalty); + const adjustedRate = + side === MarketOperation.Sell ? adjustedOutput.div(clippedInput) : clippedInput.div(adjustedOutput); + // Optionally skip orders with rates that are <= 0. + if (filterNegativeAdjustedRateOrders && adjustedRate.lte(0)) { + return undefined; } - // 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; + + return { + sourcePathId, + adjustedOutput, + input: clippedInput, + output: clippedOutput, + flags: SOURCE_FLAGS[type === FillQuoteTransformerOrderType.Rfq ? 'RfqOrder' : 'LimitOrder'], + source: ERC20BridgeSource.Native, + type, + fillData: { ...order }, + gas, + }; } -export function dexSamplesToFills( +export function dexSampleToFill( side: MarketOperation, - samples: DexSample[], + sample: DexSample, outputAmountPerEth: BigNumber, inputAmountPerEth: BigNumber, fees: FeeSchedule, -): Fill[] { +): Fill { 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 input = sample.input.minus(prevSample ? prevSample.input : 0); - const output = sample.output.minus(prevSample ? prevSample.output : 0); - let penalty = ZERO_AMOUNT; - if (i === 0) { - const fee = fees[source] === undefined ? 0 : fees[source]!(sample.fillData) || 0; - // Only the first fill in a DEX path incurs a penalty. - penalty = ethToOutputAmount({ - input, - output, - inputAmountPerEth, - outputAmountPerEth, - ethAmount: fee, - }); - } - const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); + const { source, fillData } = sample; + const input = sample.input; + const output = sample.output; + const { fee, gas } = + fees[source] === undefined ? DEFAULT_FEE_ESTIMATE : fees[source]!(sample.fillData) || DEFAULT_FEE_ESTIMATE; - fills.push({ - sourcePathId, - input, - output, - adjustedOutput, - source, - fillData, - type: FillQuoteTransformerOrderType.Bridge, - index: i, - parent: i !== 0 ? fills[fills.length - 1] : undefined, - flags: SOURCE_FLAGS[source], - }); - } - return fills; + const penalty = ethToOutputAmount({ + input, + output, + inputAmountPerEth, + outputAmountPerEth, + ethAmount: fee, + }); + + return { + sourcePathId, + input, + output, + adjustedOutput: adjustOutput(side, output, penalty), + source, + fillData, + type: FillQuoteTransformerOrderType.Bridge, + flags: SOURCE_FLAGS[source], + gas, + }; +} + +/** + * 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); } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/identity_fill_adjustor.ts b/packages/asset-swapper/src/utils/market_operation_utils/identity_fill_adjustor.ts new file mode 100644 index 0000000000..fafba4afc7 --- /dev/null +++ b/packages/asset-swapper/src/utils/market_operation_utils/identity_fill_adjustor.ts @@ -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; + } +} diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 8f2d084ec2..00c635f5c9 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -41,19 +41,17 @@ import { SOURCE_FLAGS, ZERO_AMOUNT, } from './constants'; -import { createFills } from './fills'; +import { IdentityFillAdjustor } from './identity_fill_adjustor'; import { getBestTwoHopQuote } from './multihop_utils'; import { createOrdersFromTwoHopSample } from './orders'; import { Path, PathPenaltyOpts } from './path'; -import { findOptimalPathJSAsync, findOptimalRustPathFromSamples } from './path_optimizer'; +import { findOptimalPathFromSamples } from './path_optimizer'; import { DexOrderSampler, getSampleAmounts } from './sampler'; import { SourceFilters } from './source_filters'; import { AggregationError, - CollapsedFill, DexSample, ERC20BridgeSource, - Fill, GenerateOptimizedOrdersOpts, GetMarketOrdersOpts, MarketSideLiquidity, @@ -62,8 +60,6 @@ import { OrderDomain, } from './types'; -const SHOULD_USE_RUST_ROUTER = process.env.RUST_ROUTER === 'true'; - // tslint:disable:boolean-naming export class MarketOperationUtils { @@ -167,18 +163,20 @@ export class MarketOperationUtils { // Get native order fillable amounts. this._sampler.getLimitOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchangeProxy), // Get ETH -> maker token price. - this._sampler.getMedianSellRate( + this._sampler.getBestNativeTokenSellRate( feeSourceFilters.sources, makerToken, this._nativeFeeToken, this._nativeFeeTokenAmount, + _opts.feeSchedule, ), // Get ETH -> taker token price. - this._sampler.getMedianSellRate( + this._sampler.getBestNativeTokenSellRate( feeSourceFilters.sources, takerToken, this._nativeFeeToken, this._nativeFeeTokenAmount, + _opts.feeSchedule, ), // Get sell quotes for taker -> maker. this._sampler.getSellQuotes(quoteSourceFilters.sources, makerToken, takerToken, sampleAmounts), @@ -278,18 +276,20 @@ export class MarketOperationUtils { // Get native order fillable amounts. this._sampler.getLimitOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchangeProxy), // Get ETH -> makerToken token price. - this._sampler.getMedianSellRate( + this._sampler.getBestNativeTokenSellRate( feeSourceFilters.sources, makerToken, this._nativeFeeToken, this._nativeFeeTokenAmount, + _opts.feeSchedule, ), // Get ETH -> taker token price. - this._sampler.getMedianSellRate( + this._sampler.getBestNativeTokenSellRate( feeSourceFilters.sources, takerToken, this._nativeFeeToken, this._nativeFeeTokenAmount, + _opts.feeSchedule, ), // Get buy quotes for taker -> maker. this._sampler.getBuyQuotes(quoteSourceFilters.sources, makerToken, takerToken, sampleAmounts), @@ -384,11 +384,12 @@ export class MarketOperationUtils { this._sampler.getLimitOrderFillableMakerAmounts(orders, this.contractAddresses.exchangeProxy), ), ...batchNativeOrders.map(orders => - this._sampler.getMedianSellRate( + this._sampler.getBestNativeTokenSellRate( feeSourceFilters.sources, orders[0].order.takerToken, this._nativeFeeToken, this._nativeFeeTokenAmount, + _opts.feeSchedule, ), ), ...batchNativeOrders.map((orders, i) => @@ -455,6 +456,7 @@ export class MarketOperationUtils { allowFallback: _opts.allowFallback, gasPrice: _opts.gasPrice, neonRouterNumSamples: _opts.neonRouterNumSamples, + fillAdjustor: _opts.fillAdjustor, }, ); return optimizerResult; @@ -516,60 +518,38 @@ export class MarketOperationUtils { const takerAmountPerEth = side === MarketOperation.Sell ? inputAmountPerEth : outputAmountPerEth; const makerAmountPerEth = side === MarketOperation.Sell ? outputAmountPerEth : inputAmountPerEth; - let fills: Fill[][]; // Find the optimal path using Rust router if enabled, otherwise fallback to JS Router let optimalPath: Path | undefined; - if (SHOULD_USE_RUST_ROUTER) { - fills = [[]]; - optimalPath = findOptimalRustPathFromSamples( - side, - dexQuotes, - [...nativeOrders, ...augmentedRfqtIndicativeQuotes], - inputAmount, - penaltyOpts, - opts.feeSchedule, - this._sampler.chainId, - opts.neonRouterNumSamples, - 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 = findOptimalPathFromSamples( + side, + dexQuotes, + [...nativeOrders, ...augmentedRfqtIndicativeQuotes], + inputAmount, + penaltyOpts, + opts.feeSchedule, + this._sampler.chainId, + opts.neonRouterNumSamples, + opts.fillAdjustor, + opts.samplerMetrics, + ); - optimalPath = await findOptimalPathJSAsync( - side, - fills, - inputAmount, - opts.runLimit, - opts.samplerMetrics, - penaltyOpts, - ); - } + const optimalPathAdjustedRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT; - const optimalPathRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT; - - const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote( + const { adjustedRate: bestTwoHopAdjustedRate, quote: bestTwoHopQuote } = getBestTwoHopQuote( marketSideLiquidity, opts.feeSchedule, opts.exchangeProxyOverhead, + opts.fillAdjustor, ); - if (bestTwoHopQuote && bestTwoHopRate.isGreaterThan(optimalPathRate)) { + + if (bestTwoHopQuote && bestTwoHopAdjustedRate.isGreaterThan(optimalPathAdjustedRate)) { const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote, orderOpts); return { optimizedOrders: twoHopOrders, liquidityDelivered: bestTwoHopQuote, sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop], marketSideLiquidity, - adjustedRate: bestTwoHopRate, + adjustedRate: bestTwoHopAdjustedRate, takerAmountPerEth, makerAmountPerEth, }; @@ -580,19 +560,14 @@ export class MarketOperationUtils { throw new Error(AggregationError.NoOptimalPath); } - // Generate a fallback path if required - // 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); + const finalizedPath = optimalPath.finalize(orderOpts); return { - optimizedOrders: collapsedPath.orders, - liquidityDelivered: collapsedPath.collapsedFills as CollapsedFill[], - sourceFlags: collapsedPath.sourceFlags, + optimizedOrders: finalizedPath.orders, + liquidityDelivered: finalizedPath.fills, + sourceFlags: finalizedPath.sourceFlags, marketSideLiquidity, - adjustedRate: optimalPathRate, + adjustedRate: optimalPathAdjustedRate, takerAmountPerEth, makerAmountPerEth, }; @@ -618,6 +593,7 @@ export class MarketOperationUtils { gasPrice: _opts.gasPrice, neonRouterNumSamples: _opts.neonRouterNumSamples, samplerMetrics: _opts.samplerMetrics, + fillAdjustor: _opts.fillAdjustor, }; if (nativeOrders.length === 0) { @@ -630,9 +606,15 @@ export class MarketOperationUtils { ? this.getMarketSellLiquidityAsync.bind(this) : this.getMarketBuyLiquidityAsync.bind(this); 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; try { - optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, optimizerOpts); + optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, { + ...optimizerOpts, + fillAdjustor: new IdentityFillAdjustor(), + }); } catch (e) { // 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 @@ -656,6 +638,17 @@ export class MarketOperationUtils { } // 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; if ( marketSideLiquidity.isRfqSupported && @@ -716,8 +709,28 @@ export class MarketOperationUtils { }); // Re-run optimizer with the new indicative quote if (indicativeQuotes.length > 0) { + // Attach the indicative quotes to the market side liquidity 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 { // A firm quote is being requested, and firm quotes price-aware enabled. @@ -775,6 +788,8 @@ export class MarketOperationUtils { fillableTakerFeeAmount: ZERO_AMOUNT, }), ); + + // Attach the firm RFQt quotes to the market side liquidity marketSideLiquidity.quotes.nativeOrders = [ ...quotesWithOrderFillableAmounts, ...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 // 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. - 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 { - 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 diff --git a/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts index 3734d6feb1..32ff79f87f 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts @@ -9,6 +9,7 @@ import { DexSample, ExchangeProxyOverhead, FeeSchedule, + FillAdjustor, MarketSideLiquidity, MultiHopFillData, TokenAdjacencyGraph, @@ -38,6 +39,7 @@ export function getBestTwoHopQuote( marketSideLiquidity: Omit, feeSchedule?: FeeSchedule, exchangeProxyOverhead?: ExchangeProxyOverhead, + fillAdjustor?: FillAdjustor, ): { quote: DexSample | undefined; adjustedRate: BigNumber } { const { side, inputAmount, outputAmountPerEth, quotes } = marketSideLiquidity; const { twoHopQuotes } = quotes; @@ -57,7 +59,15 @@ export function getBestTwoHopQuote( } const best = filteredQuotes .map(quote => - getTwoHopAdjustedRate(side, quote, inputAmount, outputAmountPerEth, feeSchedule, exchangeProxyOverhead), + getTwoHopAdjustedRate( + side, + quote, + inputAmount, + outputAmountPerEth, + feeSchedule, + exchangeProxyOverhead, + fillAdjustor, + ), ) .reduce( (prev, curr, i) => @@ -70,6 +80,7 @@ export function getBestTwoHopQuote( outputAmountPerEth, feeSchedule, exchangeProxyOverhead, + fillAdjustor, ), quote: filteredQuotes[0], }, diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index f3849b9384..23867c23bf 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -1,5 +1,6 @@ import { BridgeProtocol, encodeBridgeSourceId, FillQuoteTransformerOrderType } from '@0x/protocol-utils'; import { AbiEncoder, BigNumber } from '@0x/utils'; +import _ = require('lodash'); import { AssetSwapperContractAddresses, MarketOperation } from '../../types'; @@ -11,12 +12,12 @@ import { BalancerV2BatchSwapFillData, BalancerV2FillData, BancorFillData, - CollapsedFill, CompoundFillData, CurveFillData, DexSample, DODOFillData, ERC20BridgeSource, + Fill, FillData, FinalUniswapV3FillData, GeistFillData, @@ -28,7 +29,7 @@ import { MakerPsmFillData, MooniswapFillData, MultiHopFillData, - NativeCollapsedFill, + NativeFillData, NativeLimitOrderFillData, NativeRfqOrderFillData, OptimizedMarketBridgeOrder, @@ -60,23 +61,27 @@ export function createOrdersFromTwoHopSample( ): OptimizedMarketOrder[] { const [makerToken, takerToken] = getMakerTakerTokens(opts); const { firstHopSource, secondHopSource, intermediateToken } = sample.fillData; - const firstHopFill: CollapsedFill = { + const firstHopFill: Fill = { sourcePathId: '', source: firstHopSource.source, type: FillQuoteTransformerOrderType.Bridge, input: opts.side === MarketOperation.Sell ? sample.input : ZERO_AMOUNT, output: opts.side === MarketOperation.Sell ? ZERO_AMOUNT : sample.output, - subFills: [], + adjustedOutput: opts.side === MarketOperation.Sell ? ZERO_AMOUNT : sample.output, fillData: firstHopSource.fillData, + flags: BigInt(0), + gas: 1, }; - const secondHopFill: CollapsedFill = { + const secondHopFill: Fill = { sourcePathId: '', source: secondHopSource.source, type: FillQuoteTransformerOrderType.Bridge, input: opts.side === MarketOperation.Sell ? MAX_UINT256 : sample.input, output: opts.side === MarketOperation.Sell ? sample.output : MAX_UINT256, - subFills: [], + adjustedOutput: opts.side === MarketOperation.Sell ? sample.output : MAX_UINT256, fillData: secondHopSource.fillData, + flags: BigInt(0), + gas: 1, }; return [ createBridgeOrder(firstHopFill, intermediateToken, takerToken, opts.side), @@ -392,68 +397,6 @@ export function createBridgeDataForBridgeOrder(order: OptimizedMarketBridgeOrder 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' }]); const curveEncoder = AbiEncoder.create([ { name: 'curveAddress', type: 'address' }, @@ -576,7 +519,7 @@ export const BRIDGE_ENCODERS: { [ERC20BridgeSource.Velodrome]: AbiEncoder.create('(address,bool)'), }; -function getFillTokenAmounts(fill: CollapsedFill, side: MarketOperation): [BigNumber, BigNumber] { +function getFillTokenAmounts(fill: Fill, side: MarketOperation): [BigNumber, BigNumber] { return [ // Maker asset amount. 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( - fill: NativeCollapsedFill, + fill: Fill, side: MarketOperation, ): OptimizedMarketOrderBase | OptimizedMarketOrderBase { const fillData = fill.fillData; @@ -598,10 +541,76 @@ export function createNativeOptimizedOrder( takerToken: fillData.order.takerToken, makerAmount, takerAmount, - fills: [fill], fillData, + fill: cleanFillForExport(fill), }; return fill.type === FillQuoteTransformerOrderType.Rfq ? { ...base, type: FillQuoteTransformerOrderType.Rfq, fillData: fillData as NativeRfqOrderFillData } : { ...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]; +} diff --git a/packages/asset-swapper/src/utils/market_operation_utils/path.ts b/packages/asset-swapper/src/utils/market_operation_utils/path.ts index 77d6007e43..aaa94646f8 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/path.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/path.ts @@ -1,4 +1,5 @@ import { BigNumber } from '@0x/utils'; +import _ = require('lodash'); import { MarketOperation } from '../../types'; @@ -6,14 +7,7 @@ import { POSITIVE_INF, ZERO_AMOUNT } from './constants'; import { ethToOutputAmount } from './fills'; import { createBridgeOrder, createNativeOptimizedOrder, CreateOrderFromPathOpts, getMakerTakerTokens } from './orders'; import { getCompleteRate, getRate } from './rate_utils'; -import { - CollapsedFill, - ERC20BridgeSource, - ExchangeProxyOverhead, - Fill, - NativeCollapsedFill, - OptimizedMarketOrder, -} from './types'; +import { ERC20BridgeSource, ExchangeProxyOverhead, Fill, NativeFillData, OptimizedMarketOrder } from './types'; // tslint:disable: prefer-for-of no-bitwise completed-docs @@ -37,7 +31,6 @@ export const DEFAULT_PATH_PENALTY_OPTS: PathPenaltyOpts = { }; export class Path { - public collapsedFills?: ReadonlyArray; public orders?: OptimizedMarketOrder[]; public sourceFlags: bigint = BigInt(0); protected _size: PathSize = { input: ZERO_AMOUNT, output: ZERO_AMOUNT }; @@ -57,16 +50,6 @@ export class 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 readonly side: MarketOperation, public fills: ReadonlyArray, @@ -74,68 +57,33 @@ export class Path { 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 - * Fallback must contain exclusive fills that are - * not present in this path + * Finalizes this path, creating fillable orders with the information required + * for settlement */ - public addFallback(fallback: Path): this { - // 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 { + public finalize(opts: CreateOrderFromPathOpts): FinalizedPath { const [makerToken, takerToken] = getMakerTakerTokens(opts); - const collapsedFills = this.collapsedFills === undefined ? this._collapseFills() : this.collapsedFills; this.orders = []; - for (let i = 0; i < collapsedFills.length; ) { - if (collapsedFills[i].source === ERC20BridgeSource.Native) { - this.orders.push(createNativeOptimizedOrder(collapsedFills[i] as NativeCollapsedFill, opts.side)); - ++i; - continue; + for (const fill of this.fills) { + // internal BigInt flag field is not supported JSON and is tricky + // to remove upstream. Since it's not needed in a FinalizedPath we just drop it. + const normalizedFill = _.omit(fill, 'flags') as Fill; + if (fill.source === ERC20BridgeSource.Native) { + this.orders.push(createNativeOptimizedOrder(normalizedFill as Fill, 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; - } - - public size(): PathSize { - return this._size; + return this as FinalizedPath; } 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 { 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 pathPenalty = ethToOutputAmount({ input, @@ -155,6 +103,10 @@ export class Path { 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 { const { input, output } = this.adjustedSize(); return getRate(this.side, input, output); @@ -171,16 +123,11 @@ export class Path { return best; } - public adjustedSlippage(maxRate: BigNumber): number { - if (maxRate.eq(0)) { - return 0; - } - const totalRate = this.adjustedRate(); - const rateChange = maxRate.minus(totalRate); - return rateChange.div(maxRate).toNumber(); - } - - public isBetterThan(other: Path): boolean { + /** + * Compares two paths returning if this adjusted path + * is better than the other adjusted path + */ + public isAdjustedBetterThan(other: Path): boolean { if (!this.targetInput.isEqualTo(other.targetInput)) { throw new Error(`Target input mismatch: ${this.targetInput} !== ${other.targetInput}`); } @@ -192,78 +139,6 @@ export class Path { } else { 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 { - 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 { @@ -285,7 +160,6 @@ export class Path { } } -export interface CollapsedPath extends Path { - readonly collapsedFills: ReadonlyArray; +export interface FinalizedPath extends Path { readonly orders: OptimizedMarketOrder[]; } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts b/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts index 016440721c..530dd4ff94 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts @@ -1,6 +1,7 @@ import { assert } from '@0x/assert'; import { ChainId } from '@0x/contract-addresses'; import { OptimizerCapture, route, SerializedPath } from '@0x/neon-router'; +import { FillQuoteTransformerOrderType } from '@0x/protocol-utils'; import { BigNumber, hexUtils } from '@0x/utils'; import * as _ from 'lodash'; import { performance } from 'perf_hooks'; @@ -9,13 +10,12 @@ import { DEFAULT_WARNING_LOGGER } from '../../constants'; import { MarketOperation, NativeOrderWithFillableAmounts } from '../../types'; import { VIP_ERC20_BRIDGE_SOURCES_BY_CHAIN_ID, ZERO_AMOUNT } from './constants'; -import { dexSamplesToFills, ethToOutputAmount, nativeOrdersToFills } from './fills'; -import { DEFAULT_PATH_PENALTY_OPTS, Path, PathPenaltyOpts } from './path'; -import { DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillData, SamplerMetrics } from './types'; +import { dexSampleToFill, ethToOutputAmount, nativeOrderToFill } from './fills'; +import { Path, PathPenaltyOpts } from './path'; +import { DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillAdjustor, FillData, SamplerMetrics } from './types'; // 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 const MIN_NUM_SAMPLE_INPUTS = 3; @@ -45,7 +45,7 @@ function calculateOuputFee( ): BigNumber { if (isDexSample(sampleOrNativeOrder)) { const { input, output, source, fillData } = sampleOrNativeOrder; - const fee = fees[source]?.(fillData) || 0; + const fee = fees[source]?.(fillData).fee || ZERO_AMOUNT; const outputFee = ethToOutputAmount({ input, output, @@ -56,7 +56,7 @@ function calculateOuputFee( return outputFee; } else { 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({ input, output, @@ -77,6 +77,7 @@ function findRoutesAndCreateOptimalPath( fees: FeeSchedule, neonRouterNumSamples: number, vipSourcesSet: Set, + fillAdjustor: FillAdjustor, ): { allSourcesPath: Path | undefined; vipSourcesPath: Path | undefined } | undefined { // 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 @@ -85,31 +86,44 @@ function findRoutesAndCreateOptimalPath( return undefined; } - const createFill = (sample: DexSample): Fill | undefined => { - const fills = dexSamplesToFills(side, [sample], opts.outputAmountPerEth, opts.inputAmountPerEth, fees); - // NOTE: If the sample has 0 output dexSamplesToFills will return [] because no fill can be created - if (fills.length === 0) { - return undefined; - } - - return fills[0]; + // Create a `Fill` from a dex sample and adjust it with any passed in + // adjustor + const createFillFromDexSample = (sample: DexSample): Fill => { + const fill = dexSampleToFill(side, sample, opts.outputAmountPerEth, opts.inputAmountPerEth, fees); + const adjustedFills = fillAdjustor.adjustFills(side, [fill], input); + return adjustedFills[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( - sourcesRustRoute, + optimalRouteInputs, + optimalRouteOutputs, samplesAndNativeOrdersWithResults, - sourcesOutputAmounts, sampleSourcePathIds, ); 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 [ routeInput, - routeSamplesAndNativeOrders, outputAmount, + routeSamplesAndNativeOrders, sourcePathId, ] of routesAndSamplesAndOutputs) { if (!Number.isFinite(outputAmount)) { @@ -119,26 +133,27 @@ function findRoutesAndCreateOptimalPath( if (!routeInput || !routeSamplesAndNativeOrders || !outputAmount) { 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 - const rustInputAdjusted = BigNumber.min( - new BigNumber(routeInput).multipliedBy(scale).integerValue(BigNumber.ROUND_CEIL), + const routeInputCorrected = BigNumber.min( + precisionErrorScalar.multipliedBy(routeInput).integerValue(BigNumber.ROUND_CEIL), input, ); 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)) { - const nativeFill = nativeOrdersToFills( + const nativeFill = nativeOrderToFill( side, - [current], - rustInputAdjusted, + current, + routeInputCorrected, opts.outputAmountPerEth, opts.inputAmountPerEth, fees, false, - )[0] as Fill | undefined; - // Note: If the order has an adjusted rate of less than or equal to 0 it will be skipped - // and nativeFill will be `undefined` + ); + // Note: If the order has an adjusted rate of less than or equal to 0 it will be undefined if (nativeFill) { // NOTE: For Limit/RFQ orders we are done here. No need to scale output adjustedFills.push({ ...nativeFill, sourcePathId: sourcePathId ?? hexUtils.random() }); @@ -147,62 +162,54 @@ function findRoutesAndCreateOptimalPath( } // NOTE: For DexSamples only - let fill = createFill(current); + let fill = createFillFromDexSample(current); if (!fill) { continue; } const routeSamples = routeSamplesAndNativeOrders as Array>; - // 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'); 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) { - 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 right = routeSamples[k + 1]; if (left && right) { fill = - createFill({ + createFillFromDexSample({ ...right, // default to the greater (for gas used) - input: rustInputAdjusted, - output: new BigNumber(outputAmount), + input: routeInputCorrected, + output: new BigNumber(outputAmount).integerValue(), }) ?? fill; } else { assert.assert(Boolean(left || right), 'No valid sample to use'); - fill = createFill(left || right) ?? fill; + fill = createFillFromDexSample(left || right) ?? fill; } break; } } - // TODO(kimpers): remove once we have solved the rounding/precision loss issues in the Rust router - const maxSampledOutput = BigNumber.max(...routeSamples.map(s => s.output)); + // TODO: remove once we have solved the rounding/precision loss issues in the Rust router + 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) const scaleOutput = (output: BigNumber) => { - // Don't try to scale 0 output as it will be clamped to 1 - 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; - + const capped = BigNumber.min(output.integerValue(), maxSampledOutput); return BigNumber.max(capped, 1); }; adjustedFills.push({ ...fill, - input: rustInputAdjusted, + input: routeInputCorrected, output: scaleOutput(fill.output), adjustedOutput: scaleOutput(fill.adjustedOutput), - index: 0, - parent: undefined, sourcePathId: sourcePathId ?? hexUtils.random(), }); } @@ -224,7 +231,6 @@ function findRoutesAndCreateOptimalPath( continue; } - const sourcePathId = hexUtils.random(); const singleSourceSamplesWithOutput = [...singleSourceSamples]; for (let i = singleSourceSamples.length - 1; i >= 0; i--) { const currentOutput = singleSourceSamples[i].output; @@ -240,17 +246,23 @@ function findRoutesAndCreateOptimalPath( 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( (memo, sample, sampleIdx) => { - memo.ids.push(`${sample.source}-${serializedPaths.length}-${sampleIdx}`); - memo.inputs.push(sample.input.integerValue().toNumber()); - memo.outputs.push(sample.output.integerValue().toNumber()); - memo.outputFees.push( - calculateOuputFee(side, sample, opts.outputAmountPerEth, opts.inputAmountPerEth, fees) - .integerValue() - .toNumber(), - ); + // Use the fill from createFillFromDexSample to apply + // any user supplied adjustments + const f = createFillFromDexSample(sample); + memo.ids.push(`${f.source}-${serializedPaths.length}-${sampleIdx}`); + 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() + .toNumber(); + memo.outputFees.push(outputFee); return memo; }, @@ -265,6 +277,8 @@ function findRoutesAndCreateOptimalPath( samplesAndNativeOrdersWithResults.push(singleSourceSamplesWithOutput); serializedPaths.push(serializedPath); + + const sourcePathId = hexUtils.random(); sampleSourcePathIds.push(sourcePathId); } @@ -306,19 +320,22 @@ function findRoutesAndCreateOptimalPath( normalizedOrderOutput.times(scaleToInput).times(fraction), normalizedOrderOutput, ); - const id = `${ERC20BridgeSource.Native}-${serializedPaths.length}-${idx}-${i}`; + const id = `${ERC20BridgeSource.Native}-${nativeOrder.type}-${serializedPaths.length}-${idx}-${i}`; inputs.push(currentInput.integerValue().toNumber()); outputs.push(currentOutput.integerValue().toNumber()); outputFees.push(fee); 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 = { ids, inputs, outputs, outputFees, - isVip: true, + isVip, }; samplesAndNativeOrdersWithResults.push([nativeOrder]); @@ -375,7 +392,7 @@ function findRoutesAndCreateOptimalPath( }; } -export function findOptimalRustPathFromSamples( +export function findOptimalPathFromSamples( side: MarketOperation, samples: DexSample[][], nativeOrders: NativeOrderWithFillableAmounts[], @@ -384,6 +401,7 @@ export function findOptimalRustPathFromSamples( fees: FeeSchedule, chainId: ChainId, neonRouterNumSamples: number, + fillAdjustor: FillAdjustor, samplerMetrics?: SamplerMetrics, ): Path | undefined { const beforeTimeMs = performance.now(); @@ -406,6 +424,7 @@ export function findOptimalRustPathFromSamples( fees, neonRouterNumSamples, vipSourcesSet, + fillAdjustor, ); if (!paths) { @@ -415,7 +434,7 @@ export function findOptimalRustPathFromSamples( const { allSourcesPath, vipSourcesPath } = paths; - if (!allSourcesPath || vipSourcesPath?.isBetterThan(allSourcesPath)) { + if (!allSourcesPath || vipSourcesPath?.isAdjustedBetterThan(allSourcesPath)) { sendMetrics(); return vipSourcesPath; } @@ -423,143 +442,3 @@ export function findOptimalRustPathFromSamples( sendMetrics(); 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 { - 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()])); -} diff --git a/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts index 87f309a695..dc7072caa4 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts @@ -1,9 +1,20 @@ +import { FillQuoteTransformerOrderType } from '@0x/protocol-utils'; import { BigNumber } from '@0x/utils'; import { MarketOperation } from '../../types'; 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 @@ -18,20 +29,55 @@ export function getTwoHopAdjustedRate( outputAmountPerEth: BigNumber, fees: FeeSchedule = {}, exchangeProxyOverhead: ExchangeProxyOverhead = () => ZERO_AMOUNT, + fillAdjustor: FillAdjustor = new IdentityFillAdjustor(), ): BigNumber { const { output, input, fillData } = twoHopQuote; if (input.isLessThan(targetInput) || output.isZero()) { return ZERO_AMOUNT; } - const penalty = outputAmountPerEth.times( - exchangeProxyOverhead( - SOURCE_FLAGS.MultiHop | - SOURCE_FLAGS[fillData.firstHopSource.source] | - SOURCE_FLAGS[fillData.secondHopSource.source], - ).plus(fees[ERC20BridgeSource.MultiHop]!(fillData)), - ); - const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); - return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput); + + // Flags to indicate which sources are used + const flags = + SOURCE_FLAGS.MultiHop | + SOURCE_FLAGS[fillData.firstHopSource.source] | + SOURCE_FLAGS[fillData.secondHopSource.source]; + + // Penalty of going to those sources in terms of output + const sourcePenalty = outputAmountPerEth.times(fees[ERC20BridgeSource.MultiHop]!(fillData).fee).integerValue(); + + // 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. + * + * If it is a sell, output/input. If it is a buy, input/output. */ export function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber { if (input.eq(0) || output.eq(0)) { diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts index d9ce87e3d2..79cd22fdf1 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts @@ -75,6 +75,7 @@ import { DexSample, DODOFillData, ERC20BridgeSource, + FeeSchedule, GeistFillData, GeistInfo, 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[], makerToken: string, - takerToken: string, - takerFillAmount: BigNumber, + nativeToken: string, + nativeFillAmount: BigNumber, + feeSchedule: FeeSchedule, ): BatchedOperation { - if (makerToken.toLowerCase() === takerToken.toLowerCase()) { + if (makerToken.toLowerCase() === nativeToken.toLowerCase()) { return SamplerOperations.constant(new BigNumber(1)); } - const subOps = this._getSellQuoteOperations(sources, makerToken, takerToken, [takerFillAmount], { + const subOps = this._getSellQuoteOperations(sources, makerToken, nativeToken, [nativeFillAmount], { default: [], }); return this._createBatch( @@ -1327,15 +1334,35 @@ export class SamplerOperations { if (samples.length === 0) { return ZERO_AMOUNT; } - const flatSortedSamples = samples - .reduce((acc, v) => acc.concat(...v)) - .filter(v => !v.isZero()) - .sort((a, b) => a.comparedTo(b)); - if (flatSortedSamples.length === 0) { - return ZERO_AMOUNT; - } - const medianSample = flatSortedSamples[Math.floor(flatSortedSamples.length / 2)]; - return medianSample.div(takerFillAmount); + + const adjustedPrices = subOps.map((s, i) => { + // If the source gave us nothing, skip it and return a default + if (samples[i].length === 0 || samples[i][0].isZero()) { + return { adjustedPrice: ZERO_AMOUNT, source: s.source, price: ZERO_AMOUNT }; + } + const v = samples[i][0]; + 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, ); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index 3d281b67ec..abae2c72a3 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -401,45 +401,10 @@ export interface Fill { output: BigNumber; // The output fill amount, adjusted by fees. adjustedOutput: BigNumber; - // Fill that must precede this one. This enforces certain fills to be contiguous. - parent?: Fill; - // The index of the fill in the original path. - index: number; + // The expected gas cost of this fill + gas: number; } -/** - * Represents continguous fills on a path that have been merged together. - */ -export interface CollapsedFill { - 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 {} - export interface OptimizedMarketOrderBase { source: ERC20BridgeSource; fillData: TFillData; @@ -448,24 +413,21 @@ export interface OptimizedMarketOrderBase takerToken: string; 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 - fills: CollapsedFill[]; + fill: Omit; // Remove duplicates which have been brought into the OrderBase interface } export interface OptimizedMarketBridgeOrder extends OptimizedMarketOrderBase { type: FillQuoteTransformerOrderType.Bridge; - fillData: TFillData; sourcePathId: string; } export interface OptimizedLimitOrder extends OptimizedMarketOrderBase { type: FillQuoteTransformerOrderType.Limit; - fillData: NativeLimitOrderFillData; } export interface OptimizedRfqOrder extends OptimizedMarketOrderBase { type: FillQuoteTransformerOrderType.Rfq; - fillData: NativeRfqOrderFillData; } /** @@ -482,8 +444,12 @@ export interface GetMarketOrdersRfqOpts extends RfqRequestOpts { 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 GasEstimate = (fillData: FillData) => number; +export type GasSchedule = Partial<{ [key in ERC20BridgeSource]: GasEstimate }>; + export type ExchangeProxyOverhead = (sourceFlags: bigint) => BigNumber; /** @@ -547,7 +513,7 @@ export interface GetMarketOrdersOpts { /** * Estimated gas consumed by each liquidity source. */ - gasSchedule: FeeSchedule; + gasSchedule: GasSchedule; exchangeProxyOverhead: ExchangeProxyOverhead; /** * 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 */ samplerMetrics?: SamplerMetrics; + + /** + * Adjusts fills individual fills based on caller supplied criteria + */ + fillAdjustor: FillAdjustor; } export interface SamplerMetrics { @@ -627,7 +598,7 @@ export interface SourceQuoteOperation ext export interface OptimizerResult { optimizedOrders: OptimizedMarketOrder[]; sourceFlags: bigint; - liquidityDelivered: CollapsedFill[] | DexSample; + liquidityDelivered: Readonly>; marketSideLiquidity: MarketSideLiquidity; adjustedRate: BigNumber; takerAmountPerEth: BigNumber; @@ -695,8 +666,13 @@ export interface GenerateOptimizedOrdersOpts { gasPrice: BigNumber; neonRouterNumSamples: number; samplerMetrics?: SamplerMetrics; + fillAdjustor: FillAdjustor; } export interface ComparisonPrice { wholeOrder: BigNumber | undefined; } + +export interface FillAdjustor { + adjustFills: (side: MarketOperation, fills: Fill[], amount: BigNumber) => Fill[]; +} diff --git a/packages/asset-swapper/src/utils/quote_report_generator.ts b/packages/asset-swapper/src/utils/quote_report_generator.ts index f49a095a25..1fe8f74d62 100644 --- a/packages/asset-swapper/src/utils/quote_report_generator.ts +++ b/packages/asset-swapper/src/utils/quote_report_generator.ts @@ -5,12 +5,11 @@ import _ = require('lodash'); import { MarketOperation, NativeOrderWithFillableAmounts } from '../types'; import { - CollapsedFill, DexSample, ERC20BridgeSource, + Fill, FillData, MultiHopFillData, - NativeCollapsedFill, NativeFillData, NativeLimitOrderFillData, NativeRfqOrderFillData, @@ -123,7 +122,7 @@ export interface PriceComparisonsReport { export function generateQuoteReport( marketOperation: MarketOperation, nativeOrders: NativeOrderWithFillableAmounts[], - liquidityDelivered: ReadonlyArray | DexSample, + liquidityDelivered: ReadonlyArray | DexSample, comparisonPrice?: BigNumber | undefined, quoteRequestor?: QuoteRequestor, ): QuoteReport { @@ -174,7 +173,7 @@ export function generateQuoteReport( export function generateExtendedQuoteReportSources( marketOperation: MarketOperation, quotes: RawQuotes, - liquidityDelivered: ReadonlyArray | DexSample, + liquidityDelivered: ReadonlyArray | DexSample, amount: BigNumber, comparisonPrice?: BigNumber | undefined, quoteRequestor?: QuoteRequestor, @@ -343,7 +342,7 @@ export function multiHopSampleToReportSource( } } -function _isNativeOrderFromCollapsedFill(cf: CollapsedFill): cf is NativeCollapsedFill { +function _isNativeOrderFromCollapsedFill(cf: Fill): cf is Fill { const { type } = cf; return type === FillQuoteTransformerOrderType.Limit || type === FillQuoteTransformerOrderType.Rfq; } diff --git a/packages/asset-swapper/src/utils/quote_simulation.ts b/packages/asset-swapper/src/utils/quote_simulation.ts index 231ea2e832..d8ef361fe5 100644 --- a/packages/asset-swapper/src/utils/quote_simulation.ts +++ b/packages/asset-swapper/src/utils/quote_simulation.ts @@ -4,7 +4,7 @@ import { BigNumber } from '@0x/utils'; import { constants } from '../constants'; 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'; const { PROTOCOL_FEE_MULTIPLIER, ZERO_AMOUNT } = constants; @@ -72,7 +72,7 @@ export interface QuoteFillInfo { } export interface QuoteFillInfoOpts { - gasSchedule: FeeSchedule; + gasSchedule: GasSchedule; protocolFeeMultiplier: BigNumber; slippage: number; } @@ -140,7 +140,7 @@ export function fillQuoteOrders( fillOrders: QuoteFillOrderCall[], inputAmount: BigNumber, protocolFeePerFillOrder: BigNumber, - gasSchedule: FeeSchedule, + gasSchedule: GasSchedule, ): IntermediateQuoteFillResult { const result: IntermediateQuoteFillResult = { ...EMPTY_QUOTE_INTERMEDIATE_FILL_RESULT, @@ -151,39 +151,27 @@ export function fillQuoteOrders( if (remainingInput.lte(0)) { break; } - for (const fill of fo.order.fills) { - if (remainingInput.lte(0)) { - break; - } - const { source, fillData } = fill; - const gas = gasSchedule[source] === undefined ? 0 : gasSchedule[source]!(fillData); - result.gas += new BigNumber(gas).toNumber(); - result.inputBySource[source] = result.inputBySource[source] || ZERO_AMOUNT; + const { source, fillData } = fo.order; + const gas = gasSchedule[source] === undefined ? 0 : gasSchedule[source]!(fillData); + result.gas += new BigNumber(gas).toNumber(); + 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( - remainingInput, - subFill.input, - fo.totalOrderInput, - fo.totalOrderInputFee, - ); - const filledOutput = subFill.output.times(filledInput.div(subFill.input)); - const filledInputFee = filledInput.div(fo.totalOrderInput).times(fo.totalOrderInputFee); - const filledOutputFee = filledOutput.div(fo.totalOrderOutput).times(fo.totalOrderOutputFee); + const filledInput = solveForInputFillAmount( + remainingInput, + fo.order.fill.input, + fo.totalOrderInput, + fo.totalOrderInputFee, + ); + const filledOutput = fo.order.fill.output.times(filledInput.div(fo.order.fill.input)); + const filledInputFee = filledInput.div(fo.totalOrderInput).times(fo.totalOrderInputFee); + const filledOutputFee = filledOutput.div(fo.totalOrderOutput).times(fo.totalOrderOutputFee); - result.inputBySource[source] = result.inputBySource[source].plus(filledInput); - result.input = result.input.plus(filledInput); - result.output = result.output.plus(filledOutput); - result.inputFee = result.inputFee.plus(filledInputFee); - result.outputFee = result.outputFee.plus(filledOutputFee); - remainingInput = remainingInput.minus(filledInput.plus(filledInputFee)); - } - } + result.inputBySource[source] = result.inputBySource[source].plus(filledInput); + result.input = result.input.plus(filledInput); + result.output = result.output.plus(filledOutput); + result.inputFee = result.inputFee.plus(filledInputFee); + result.outputFee = result.outputFee.plus(filledOutputFee); + remainingInput = remainingInput.minus(filledInput.plus(filledInputFee)); // NOTE: V4 Limit orders have Protocol fees const protocolFee = hasProtocolFee(fo.order) ? protocolFeePerFillOrder : ZERO_AMOUNT; 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; for (const f of fills) { const fee = gasSchedule[f.source] === undefined ? 0 : gasSchedule[f.source]!(f.fillData); diff --git a/packages/asset-swapper/test/comparison_price_test.ts b/packages/asset-swapper/test/comparison_price_test.ts index 1076b11967..3f2a9e286d 100644 --- a/packages/asset-swapper/test/comparison_price_test.ts +++ b/packages/asset-swapper/test/comparison_price_test.ts @@ -18,7 +18,7 @@ const expect = chai.expect; const DAI_TOKEN = '0x6b175474e89094c44da98b954eedeac495271d0f'; const ETH_TOKEN = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; 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 const curveSample: DexSample = { @@ -36,7 +36,10 @@ const uniswapSample1: DexSample = { const dexQuotes: DexSample[] = [curveSample, uniswapSample1]; 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) => { diff --git a/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts b/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts index aa8778f582..a0241166e7 100644 --- a/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts +++ b/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts @@ -23,6 +23,8 @@ import { ExchangeProxySwapQuoteConsumer } from '../src/quote_consumers/exchange_ import { AffiliateFeeType, MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types'; import { ERC20BridgeSource, + Fill, + NativeFillData, OptimizedLimitOrder, OptimizedMarketOrder, } from '../src/utils/market_operation_utils/types'; @@ -100,7 +102,8 @@ describe('ExchangeProxySwapQuoteConsumer', () => { takerToken: order.takerToken, makerAmount: order.makerAmount, takerAmount: order.takerAmount, - fills: [], + // tslint:disable-next-line:no-object-literal-type-assertion + fill: {} as Fill, ...optimizerFields, }; } diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index fe67b381a7..7d01bef5ef 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -16,16 +16,14 @@ import * as _ from 'lodash'; import * as TypeMoq from 'typemoq'; 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 { BUY_SOURCE_FILTER_BY_CHAIN_ID, - POSITIVE_INF, SELL_SOURCE_FILTER_BY_CHAIN_ID, SOURCE_FLAGS, ZERO_AMOUNT, } 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 { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations'; @@ -39,7 +37,6 @@ import { GetMarketOrdersOpts, LiquidityProviderFillData, MarketSideLiquidity, - NativeFillData, OptimizedMarketBridgeOrder, OptimizerResultWithReport, TokenAdjacencyGraph, @@ -272,7 +269,7 @@ describe('MarketOperationUtils tests', () => { }; } - type GetMedianRateOperation = ( + type GetBestNativeTokenSellRateOperation = ( sources: ERC20BridgeSource[], makerToken: string, takerToken: string, @@ -281,7 +278,7 @@ describe('MarketOperationUtils tests', () => { liquidityProviderAddress?: string, ) => BigNumber; - function createGetMedianSellRate(rate: Numberish): GetMedianRateOperation { + function createGetBestNativeSellRate(rate: Numberish): GetBestNativeTokenSellRateOperation { return ( _sources: ERC20BridgeSource[], _makerToken: string, @@ -388,7 +385,7 @@ describe('MarketOperationUtils tests', () => { }, getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES), getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES), - getMedianSellRate: createGetMedianSellRate(1), + getBestNativeTokenSellRate: createGetBestNativeSellRate(1), getTwoHopSellQuotes: (..._params: any[]) => [], getTwoHopBuyQuotes: (..._params: any[]) => [], isAddressContract: (..._params: any[]) => false, @@ -621,7 +618,7 @@ describe('MarketOperationUtils tests', () => { // to get a comparisonPrice, you need a feeschedule for a native order const feeSchedule = { - [ERC20BridgeSource.Native]: _.constant(new BigNumber(1)), + [ERC20BridgeSource.Native]: _.constant({ gas: 1, fee: new BigNumber(1) }), }; mockedQuoteRequestor .setup(mqr => mqr.getMakerUriForSignature(TypeMoq.It.isValue(SIGNATURE))) @@ -1011,7 +1008,7 @@ describe('MarketOperationUtils tests', () => { const improvedOrders = improvedOrdersResponse.optimizedOrders; expect(improvedOrders).to.not.be.length(0); 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))); assertRoughlyEquals(slippage, bridgeSlippage, 1); } @@ -1033,7 +1030,7 @@ describe('MarketOperationUtils tests', () => { { ...DEFAULT_OPTS, numSamples: 4 }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; - const orderSources = improvedOrders.map(o => o.fills[0].source); + const orderSources = improvedOrders.map(o => o.source); const expectedSources = [ ERC20BridgeSource.SushiSwap, ERC20BridgeSource.Uniswap, @@ -1056,15 +1053,16 @@ describe('MarketOperationUtils tests', () => { [ERC20BridgeSource.SushiSwap]: [0.95, 0.1, 0.1, 0.1], }; const feeSchedule = { - [ERC20BridgeSource.Native]: _.constant( - FILL_AMOUNT.div(4) + [ERC20BridgeSource.Native]: _.constant({ + gas: 1, + fee: FILL_AMOUNT.div(4) .times(nativeFeeRate) .dividedToIntegerBy(ETH_TO_MAKER_RATE), - ), + }), }; replaceSamplerOps({ getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), - getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), + getBestNativeTokenSellRate: createGetBestNativeSellRate(ETH_TO_MAKER_RATE), }); const improvedOrdersResponse = await getMarketSellOrdersAsync( marketOperationUtils, @@ -1073,7 +1071,7 @@ describe('MarketOperationUtils tests', () => { { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; - const orderSources = improvedOrders.map(o => o.fills[0].source); + const orderSources = improvedOrders.map(o => o.source); const expectedSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap, @@ -1093,15 +1091,16 @@ describe('MarketOperationUtils tests', () => { [ERC20BridgeSource.Uniswap]: [1, 0.7, 0.2, 0.2], }; const feeSchedule = { - [ERC20BridgeSource.Uniswap]: _.constant( - FILL_AMOUNT.div(4) + [ERC20BridgeSource.Uniswap]: _.constant({ + gas: 1, + fee: FILL_AMOUNT.div(4) .times(uniswapFeeRate) .dividedToIntegerBy(ETH_TO_MAKER_RATE), - ), + }), }; replaceSamplerOps({ getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), - getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), + getBestNativeTokenSellRate: createGetBestNativeSellRate(ETH_TO_MAKER_RATE), }); const improvedOrdersResponse = await getMarketSellOrdersAsync( marketOperationUtils, @@ -1110,7 +1109,7 @@ describe('MarketOperationUtils tests', () => { { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; - const orderSources = improvedOrders.map(o => o.fills[0].source); + const orderSources = improvedOrders.map(o => o.source); const expectedSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.SushiSwap, @@ -1128,7 +1127,7 @@ describe('MarketOperationUtils tests', () => { }; replaceSamplerOps({ getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), - getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), + getBestNativeTokenSellRate: createGetBestNativeSellRate(ETH_TO_MAKER_RATE), }); const improvedOrdersResponse = await getMarketSellOrdersAsync( marketOperationUtils, @@ -1137,7 +1136,7 @@ describe('MarketOperationUtils tests', () => { { ...DEFAULT_OPTS, numSamples: 4 }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; - const orderSources = improvedOrders.map(o => o.fills[0].source); + const orderSources = improvedOrders.map(o => o.source); const expectedSources = [ ERC20BridgeSource.SushiSwap, ERC20BridgeSource.Uniswap, @@ -1164,7 +1163,7 @@ describe('MarketOperationUtils tests', () => { { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, ); 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 secondSources: ERC20BridgeSource[] = []; expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); @@ -1237,7 +1236,7 @@ describe('MarketOperationUtils tests', () => { }; replaceSamplerOps({ 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 gasPrice = 100e9; // 100 gwei @@ -1262,7 +1261,7 @@ describe('MarketOperationUtils tests', () => { }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; - const orderSources = improvedOrders.map(o => o.fills[0].source); + const orderSources = improvedOrders.map(o => o.source); const expectedSources = [ERC20BridgeSource.LiquidityProvider]; expect(orderSources).to.deep.eq(expectedSources); }); @@ -1458,7 +1457,7 @@ describe('MarketOperationUtils tests', () => { const improvedOrders = improvedOrdersResponse.optimizedOrders; expect(improvedOrders).to.not.be.length(0); 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); assertRoughlyEquals(slippage, bridgeSlippage, 1); } @@ -1480,7 +1479,7 @@ describe('MarketOperationUtils tests', () => { { ...DEFAULT_OPTS, numSamples: 4 }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; - const orderSources = improvedOrders.map(o => o.fills[0].source); + const orderSources = improvedOrders.map(o => o.source); const expectedSources = [ ERC20BridgeSource.SushiSwap, ERC20BridgeSource.Uniswap, @@ -1505,15 +1504,16 @@ describe('MarketOperationUtils tests', () => { [ERC20BridgeSource.Curve]: [0.1, 0.1, 0.1, 0.1], }; const feeSchedule = { - [ERC20BridgeSource.Native]: _.constant( - FILL_AMOUNT.div(4) + [ERC20BridgeSource.Native]: _.constant({ + gas: 1, + fee: FILL_AMOUNT.div(4) .times(nativeFeeRate) .dividedToIntegerBy(ETH_TO_TAKER_RATE), - ), + }), }; replaceSamplerOps({ getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), - getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE), + getBestNativeTokenSellRate: createGetBestNativeSellRate(ETH_TO_TAKER_RATE), }); const improvedOrdersResponse = await getMarketBuyOrdersAsync( marketOperationUtils, @@ -1522,7 +1522,7 @@ describe('MarketOperationUtils tests', () => { { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; - const orderSources = improvedOrders.map(o => o.fills[0].source); + const orderSources = improvedOrders.map(o => o.source); const expectedSources = [ ERC20BridgeSource.Uniswap, ERC20BridgeSource.SushiSwap, @@ -1544,15 +1544,16 @@ describe('MarketOperationUtils tests', () => { [ERC20BridgeSource.SushiSwap]: [0.92, 0.1, 0.1, 0.1], }; const feeSchedule = { - [ERC20BridgeSource.Uniswap]: _.constant( - FILL_AMOUNT.div(4) + [ERC20BridgeSource.Uniswap]: _.constant({ + gas: 1, + fee: FILL_AMOUNT.div(4) .times(uniswapFeeRate) .dividedToIntegerBy(ETH_TO_TAKER_RATE), - ), + }), }; replaceSamplerOps({ getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), - getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE), + getBestNativeTokenSellRate: createGetBestNativeSellRate(ETH_TO_TAKER_RATE), }); const improvedOrdersResponse = await getMarketBuyOrdersAsync( marketOperationUtils, @@ -1561,7 +1562,7 @@ describe('MarketOperationUtils tests', () => { { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; - const orderSources = improvedOrders.map(o => o.fills[0].source); + const orderSources = improvedOrders.map(o => o.source); const expectedSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.SushiSwap, @@ -1587,7 +1588,7 @@ describe('MarketOperationUtils tests', () => { { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 }, ); 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 secondSources: ERC20BridgeSource[] = []; expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); @@ -1608,7 +1609,7 @@ describe('MarketOperationUtils tests', () => { }; replaceSamplerOps({ 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 exchangeProxyOverhead = (sourceFlags: bigint) => @@ -1632,77 +1633,11 @@ describe('MarketOperationUtils tests', () => { }, ); const improvedOrders = improvedOrdersResponse.optimizedOrders; - const orderSources = improvedOrders.map(o => o.fills[0].source); + const orderSources = improvedOrders.map(o => o.source); const expectedSources = [ERC20BridgeSource.LiquidityProvider]; 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 diff --git a/packages/asset-swapper/test/path_test.ts b/packages/asset-swapper/test/path_test.ts deleted file mode 100644 index 38cb5559c6..0000000000 --- a/packages/asset-swapper/test/path_test.ts +++ /dev/null @@ -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]); - }); -}); diff --git a/packages/asset-swapper/test/quote_report_generator_test.ts b/packages/asset-swapper/test/quote_report_generator_test.ts index df7e50058f..3f29dd22d6 100644 --- a/packages/asset-swapper/test/quote_report_generator_test.ts +++ b/packages/asset-swapper/test/quote_report_generator_test.ts @@ -9,11 +9,10 @@ import * as TypeMoq from 'typemoq'; import { MarketOperation, NativeOrderWithFillableAmounts } from '../src/types'; import { - CollapsedFill, DexSample, ERC20BridgeSource, + Fill, MultiHopFillData, - NativeCollapsedFill, NativeFillData, NativeLimitOrderFillData, NativeRfqOrderFillData, @@ -34,7 +33,7 @@ import { getRandomAmount, getRandomSignature } from './utils/utils'; chaiSetup.configure(); const expect = chai.expect; -function collapsedFillFromNativeOrder(order: NativeOrderWithFillableAmounts): NativeCollapsedFill { +function fillFromNativeOrder(order: NativeOrderWithFillableAmounts): Fill { const fillData = { order: order.order, signature: order.signature, @@ -50,7 +49,9 @@ function collapsedFillFromNativeOrder(order: NativeOrderWithFillableAmounts): Na order.type === FillQuoteTransformerOrderType.Limit ? (fillData as NativeLimitOrderFillData) : (fillData as NativeRfqOrderFillData), - subFills: [], + adjustedOutput: order.order.makerAmount, + flags: BigInt(0), + gas: 1, }; } @@ -111,21 +112,25 @@ describe('generateQuoteReport', async () => { ]; // generate path - const uniswap2Fill: CollapsedFill = { + const uniswap2Fill: Fill = { ...uniswapSample2, - subFills: [], sourcePathId: hexUtils.random(), type: FillQuoteTransformerOrderType.Bridge, + adjustedOutput: uniswapSample2.output, + flags: BigInt(0), + gas: 1, }; - const balancer2Fill: CollapsedFill = { + const balancer2Fill: Fill = { ...balancerSample2, - subFills: [], sourcePathId: hexUtils.random(), type: FillQuoteTransformerOrderType.Bridge, + adjustedOutput: balancerSample2.output, + flags: BigInt(0), + gas: 1, }; - const orderbookOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder2); - const rfqtOrder2Fill: CollapsedFill = collapsedFillFromNativeOrder(rfqtOrder2); - const pathGenerated: CollapsedFill[] = [rfqtOrder2Fill, orderbookOrder2Fill, uniswap2Fill, balancer2Fill]; + const orderbookOrder2Fill: Fill = fillFromNativeOrder(orderbookOrder2); + const rfqtOrder2Fill: Fill = fillFromNativeOrder(rfqtOrder2); + const pathGenerated: Fill[] = [rfqtOrder2Fill, orderbookOrder2Fill, uniswap2Fill, balancer2Fill]; // quote generator mock const quoteRequestor = TypeMoq.Mock.ofType(); @@ -241,20 +246,24 @@ describe('generateQuoteReport', async () => { const nativeOrders = [orderbookOrder1, orderbookOrder2]; // generate path - const orderbookOrder1Fill: CollapsedFill = collapsedFillFromNativeOrder(orderbookOrder1); - const uniswap1Fill: CollapsedFill = { + const orderbookOrder1Fill: Fill = fillFromNativeOrder(orderbookOrder1); + const uniswap1Fill: Fill = { ...uniswapSample1, - subFills: [], sourcePathId: hexUtils.random(), type: FillQuoteTransformerOrderType.Bridge, + adjustedOutput: uniswapSample1.output, + flags: BigInt(0), + gas: 1, }; - const balancer1Fill: CollapsedFill = { + const balancer1Fill: Fill = { ...balancerSample1, - subFills: [], sourcePathId: hexUtils.random(), 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); diff --git a/packages/asset-swapper/test/quote_simulation_test.ts b/packages/asset-swapper/test/quote_simulation_test.ts index b68cd2811a..1b476bbf37 100644 --- a/packages/asset-swapper/test/quote_simulation_test.ts +++ b/packages/asset-swapper/test/quote_simulation_test.ts @@ -5,8 +5,8 @@ import * as _ from 'lodash'; import { MarketOperation } from '../src/types'; import { - CollapsedFill, ERC20BridgeSource, + Fill, NativeLimitOrderFillData, OptimizedMarketOrder, OptimizedMarketOrderBase, @@ -45,18 +45,16 @@ describe('quote_simulation tests', async () => { inputFeeRate: number; outputFeeRate: number; count: number; - fillsCount: number; side: MarketOperation; type?: FillQuoteTransformerOrderType; }> = {}, ): QuoteFillOrderCall[] { - const { fillableInput, fillableOutput, inputFeeRate, outputFeeRate, count, fillsCount, side, type } = { + const { fillableInput, fillableOutput, inputFeeRate, outputFeeRate, count, side, type } = { fillableInput: getRandomOrderSize(), fillableOutput: getRandomOrderSize(), inputFeeRate: 0, outputFeeRate: 0, count: 3, - fillsCount: 3, side: MarketOperation.Sell, ...opts, }; @@ -83,7 +81,6 @@ describe('quote_simulation tests', async () => { return { order: createQuoteFillOrderOrder(totalInputs[i], totalOutputs[i], { side, - fillsCount, filledInput: filledInputs[i], takerInputFee: inputFees[i].abs(), takerOutputFee: outputFees[i].abs(), @@ -102,19 +99,17 @@ describe('quote_simulation tests', async () => { output: BigNumber, opts: Partial<{ filledInput: BigNumber; - fillsCount: number; side: MarketOperation; takerInputFee: BigNumber; takerOutputFee: BigNumber; type: FillQuoteTransformerOrderType; }> = {}, ): OptimizedMarketOrderBase { - const { filledInput, fillsCount, side, takerInputFee, takerOutputFee, type } = _.merge( + const { filledInput, side, takerInputFee, takerOutputFee, type } = _.merge( {}, { side: MarketOperation.Sell, filledInput: ZERO, - fillsCount: 3, takerInputFee: ZERO, takerOutputFee: ZERO, type: FillQuoteTransformerOrderType.Limit, @@ -160,46 +155,23 @@ describe('quote_simulation tests', async () => { maxTakerTokenFillAmount: fillableTakerAmount, }, type, - fills: createOrderCollapsedFills(fillableInput, fillableOutput, fillsCount), + fill: createOrderFill(fillableInput, fillableOutput), }; return order; } const nativeSourcePathId = hexUtils.random(); - function createOrderCollapsedFills(input: BigNumber, output: BigNumber, count: number): CollapsedFill[] { - 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 { - type: FillQuoteTransformerOrderType.Bridge, - sourcePathId: nativeSourcePathId, - source: ERC20BridgeSource.Uniswap, - fillData: {}, - input: inputs[i], - output: outputs[i], - subFills: _.times(count, j => ({ - input: subFillInputs[j], - output: subFillOutputs[j], - })), - }; - }); - } - - 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 createOrderFill(input: BigNumber, output: BigNumber): Fill { + return { + type: FillQuoteTransformerOrderType.Bridge, + sourcePathId: nativeSourcePathId, + source: ERC20BridgeSource.Uniswap, + fillData: {}, + input, + output, + flags: BigInt(0), + adjustedOutput: output, + gas: 1, + }; } function randomSide(): MarketOperation { @@ -237,14 +209,12 @@ describe('quote_simulation tests', async () => { describe('single order', () => { it('can exactly fill one order', () => { const side = randomSide(); - const fillsCount = _.random(1, 3); const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const fillOrders = createQuoteFillOrders({ fillableInput, fillableOutput, side, - fillsCount, count: 1, }); const result = fillQuoteOrders(fillOrders, fillableInput, ONE, GAS_SCHEDULE); @@ -253,19 +223,16 @@ describe('quote_simulation tests', async () => { expect(totalFilledInput).to.bignumber.eq(fillableInput); assertRoughlyEquals(totalFilledOutput, fillableOutput); expect(result.protocolFee).to.bignumber.eq(1); - expect(result.gas).to.eq(fillsCount); }); it('can partially fill one simple order', () => { const side = randomSide(); - const fillsCount = 1; const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const fillOrders = createQuoteFillOrders({ fillableInput, fillableOutput, side, - fillsCount, count: 1, }); const inputFillAmount = fillableInput.times(2 / 3).integerValue(); @@ -279,19 +246,16 @@ describe('quote_simulation tests', async () => { .integerValue(); assertRoughlyEquals(totalFilledOutput, expectedOutputFilledAmount); expect(result.protocolFee).to.bignumber.eq(1); - expect(result.gas).to.eq(1); }); it('can partially fill one batched order', () => { const side = randomSide(); - const fillsCount = 3; const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const fillOrders = createQuoteFillOrders({ fillableInput, fillableOutput, side, - fillsCount, count: 1, }); const inputFillAmount = fillableInput.times(2 / 3).integerValue(); @@ -301,20 +265,16 @@ describe('quote_simulation tests', async () => { expect(totalFilledInput).to.bignumber.eq(inputFillAmount); expect(totalFilledOutput).to.bignumber.lt(fillableOutput); 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', () => { const side = randomSide(); - const fillsCount = _.random(1, 3); const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const fillOrders = createQuoteFillOrders({ fillableInput, fillableOutput, side, - fillsCount, count: 1, }); const inputFillAmount = fillableInput.times(3 / 2).integerValue(); @@ -324,12 +284,10 @@ describe('quote_simulation tests', async () => { expect(totalFilledInput).to.bignumber.eq(fillableInput); assertRoughlyEquals(totalFilledOutput, fillableOutput); expect(result.protocolFee).to.bignumber.eq(1); - expect(result.gas).to.eq(fillsCount); }); it('can exactly fill one order with input fees', () => { const side = randomSide(); - const fillsCount = _.random(1, 3); const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const inputFeeRate = getRandomFeeRate(); @@ -338,7 +296,6 @@ describe('quote_simulation tests', async () => { fillableOutput, inputFeeRate, side, - fillsCount, count: 1, }); const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; @@ -350,12 +307,10 @@ describe('quote_simulation tests', async () => { assertRoughlyEquals(totalFilledOutput, fillableOutput); assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); expect(result.protocolFee).to.bignumber.eq(1); - expect(result.gas).to.eq(fillsCount); }); it('can partially fill one order with input fees', () => { const side = randomSide(); - const fillsCount = _.random(1, 3); const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const inputFeeRate = getRandomFeeRate(); @@ -364,7 +319,6 @@ describe('quote_simulation tests', async () => { fillableOutput, inputFeeRate, side, - fillsCount, count: 1, }); const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; @@ -377,12 +331,10 @@ describe('quote_simulation tests', async () => { expect(totalFilledOutput).to.bignumber.lt(fillableOutput); assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); expect(result.protocolFee).to.bignumber.eq(1); - expect(result.gas).to.lte(fillsCount); }); it('does not over fill one order with input fees', () => { const side = randomSide(); - const fillsCount = _.random(1, 3); const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const inputFeeRate = getRandomFeeRate(); @@ -391,7 +343,6 @@ describe('quote_simulation tests', async () => { fillableOutput, inputFeeRate, side, - fillsCount, count: 1, }); const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; @@ -404,12 +355,10 @@ describe('quote_simulation tests', async () => { assertRoughlyEquals(totalFilledOutput, fillableOutput); assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); expect(result.protocolFee).to.bignumber.eq(1); - expect(result.gas).to.eq(fillsCount); }); it('can exactly fill one order with output fees', () => { const side = randomSide(); - const fillsCount = _.random(1, 3); const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const outputFeeRate = getRandomFeeRate(); @@ -418,7 +367,6 @@ describe('quote_simulation tests', async () => { fillableOutput, outputFeeRate, side, - fillsCount, count: 1, }); const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; @@ -430,12 +378,10 @@ describe('quote_simulation tests', async () => { assertRoughlyEquals(totalFilledOutput, totalFillableOutput); assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); expect(result.protocolFee).to.bignumber.eq(1); - expect(result.gas).to.eq(fillsCount); }); it('can partial fill one order with output fees', () => { const side = randomSide(); - const fillsCount = _.random(1, 3); const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const outputFeeRate = getRandomFeeRate(); @@ -444,7 +390,6 @@ describe('quote_simulation tests', async () => { fillableOutput, outputFeeRate, side, - fillsCount, count: 1, }); const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; @@ -457,12 +402,10 @@ describe('quote_simulation tests', async () => { expect(totalFilledOutput).to.bignumber.lt(totalFillableOutput); assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); expect(result.protocolFee).to.bignumber.eq(1); - expect(result.gas).to.lte(fillsCount); }); it('does not over fill one order with output fees', () => { const side = randomSide(); - const fillsCount = _.random(1, 3); const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const outputFeeRate = getRandomFeeRate(); @@ -471,7 +414,6 @@ describe('quote_simulation tests', async () => { fillableOutput, outputFeeRate, side, - fillsCount, count: 1, }); const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; @@ -484,19 +426,16 @@ describe('quote_simulation tests', async () => { assertRoughlyEquals(totalFilledOutput, totalFillableOutput); assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); expect(result.protocolFee).to.bignumber.eq(1); - expect(result.gas).to.eq(fillsCount); }); it('does not charge a protocol fee for rfq orders', () => { const side = randomSide(); - const fillsCount = _.random(1, 3); const fillableInput = getRandomOrderSize(); const fillableOutput = getRandomOrderSize(); const fillOrders = createQuoteFillOrders({ fillableInput, fillableOutput, side, - fillsCount, count: 1, type: FillQuoteTransformerOrderType.Rfq, }); @@ -506,7 +445,6 @@ describe('quote_simulation tests', async () => { expect(totalFilledInput).to.bignumber.eq(fillableInput); assertRoughlyEquals(totalFilledOutput, fillableOutput); 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(totalFilledOutput).to.bignumber.eq(fillableOutput); expect(result.protocolFee).to.bignumber.eq(fillOrders.length); - expect(result.gas).to.eq(countCollapsedFills(fillOrders)); }); it('can partial fill orders', () => { @@ -551,7 +488,6 @@ describe('quote_simulation tests', async () => { expect(totalFilledInput).to.bignumber.eq(fillableInput); expect(totalFilledOutput).to.bignumber.eq(fillableOutput); expect(result.protocolFee).to.bignumber.eq(fillOrders.length); - expect(result.gas).to.eq(countCollapsedFills(fillOrders)); }); it('can exactly fill orders with input fees', () => { @@ -574,7 +510,6 @@ describe('quote_simulation tests', async () => { assertRoughlyEquals(totalFilledOutput, fillableOutput); assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); expect(result.protocolFee).to.bignumber.eq(fillOrders.length); - expect(result.gas).to.eq(countCollapsedFills(fillOrders)); }); it('can partial fill orders with input fees', () => { @@ -598,7 +533,6 @@ describe('quote_simulation tests', async () => { expect(totalFilledOutput).to.bignumber.lt(fillableOutput); assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); expect(result.protocolFee).to.bignumber.lte(fillOrders.length); - expect(result.gas).to.lte(countCollapsedFills(fillOrders)); }); it('does not over fill orders with input fees', () => { @@ -622,7 +556,6 @@ describe('quote_simulation tests', async () => { assertRoughlyEquals(totalFilledOutput, fillableOutput); assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); expect(result.protocolFee).to.bignumber.eq(fillOrders.length); - expect(result.gas).to.eq(countCollapsedFills(fillOrders)); }); it('can exactly fill orders with output fees', () => { @@ -645,7 +578,6 @@ describe('quote_simulation tests', async () => { assertRoughlyEquals(totalFilledOutput, totalFillableOutput); assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); expect(result.protocolFee).to.bignumber.eq(fillOrders.length); - expect(result.gas).to.eq(countCollapsedFills(fillOrders)); }); it('can partial fill orders with output fees', () => { @@ -669,7 +601,6 @@ describe('quote_simulation tests', async () => { expect(totalFilledOutput).to.bignumber.lt(totalFillableOutput); assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); expect(result.protocolFee).to.bignumber.lte(fillOrders.length); - expect(result.gas).to.lte(countCollapsedFills(fillOrders)); }); it('does not over fill orders with output fees', () => { @@ -693,7 +624,6 @@ describe('quote_simulation tests', async () => { assertRoughlyEquals(totalFilledOutput, totalFillableOutput); assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); 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, opts: { gasSchedule: GAS_SCHEDULE, protocolFeeMultiplier: ONE }, }); - expect(result.gas).to.eq(countCollapsedFills(orders)); expect(result.protocolFeeAmount).to.bignumber.eq(orders.length); expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); @@ -895,7 +824,6 @@ describe('quote_simulation tests', async () => { gasPrice: ONE, opts: { gasSchedule: GAS_SCHEDULE, protocolFeeMultiplier: ONE }, }); - expect(result.gas).to.eq(countCollapsedFills(orders)); expect(result.protocolFeeAmount).to.bignumber.eq(orders.length); assertRoughlyEquals(result.makerAssetAmount, fillableInput);