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

* Remove old JS router and add a FillAdjustor

Clean up JS router and unused functions

Remove more unused functions, add adjustment of fills

Comment on why we use fill over sample

update CODEOWNERS

lint

Clean up Fill removing unused properties

Remove CollapsedFills, omit flags bigint

Create GasSchedule vs FeeSchedule, return Fill and gas on OptimizedOrder

Use Fill Adjustment in Phase2 of routing

Fix Limit orders being treated as VIP

* Fix case where dex liquidity is empty

* Use best gas adjusted pricing for fee sources

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

View File

@ -1,18 +1,20 @@
# See https://help.github.com/articles/about-codeowners/
# 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

View File

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

View File

@ -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": []
}

View File

@ -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';

View File

@ -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(

View File

@ -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],
},
},
};

View File

@ -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);

View File

@ -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<FeeSchedule> = {
export const DEFAULT_GAS_SCHEDULE: Required<GasSchedule> = {
[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<FeeSchedule> = {
[ERC20BridgeSource.Velodrome]: () => 160e3,
};
export const DEFAULT_FEE_SCHEDULE: Required<FeeSchedule> = { ...DEFAULT_GAS_SCHEDULE };
export const DEFAULT_FEE_SCHEDULE: Required<FeeSchedule> = Object.keys(DEFAULT_GAS_SCHEDULE).reduce((acc, key) => {
acc[key as ERC20BridgeSource] = (fillData: FillData) => {
return {
gas: DEFAULT_GAS_SCHEDULE[key as ERC20BridgeSource](fillData),
fee: ZERO_AMOUNT,
};
};
return acc;
// tslint:disable-next-line:no-object-literal-type-assertion
}, {} as Required<FeeSchedule>);
export const POSITIVE_SLIPPAGE_FEE_TRANSFORMER_GAS = new BigNumber(20000);
export const DEFAULT_FEE_ESTIMATE = { gas: 0, fee: ZERO_AMOUNT };
// tslint:enable:custom-no-magic-numbers
export const DEFAULT_GET_MARKET_ORDERS_OPTS: Omit<GetMarketOrdersOpts, 'gasPrice'> = {
@ -2593,4 +2606,5 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: Omit<GetMarketOrdersOpts, 'gasPrice
shouldIncludePriceComparisonsReport: false,
tokenAdjacencyGraph: { default: [] },
neonRouterNumSamples: 14,
fillAdjustor: new IdentityFillAdjustor(),
};

View File

@ -3,74 +3,17 @@ import { BigNumber, hexUtils } from '@0x/utils';
import { MarketOperation, NativeOrderWithFillableAmounts } from '../../types';
import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants';
import { DEFAULT_FEE_ESTIMATE, POSITIVE_INF, SOURCE_FLAGS } from './constants';
import { DexSample, ERC20BridgeSource, FeeSchedule, Fill } from './types';
// tslint:disable: prefer-for-of no-bitwise completed-docs
/**
* Create `Fill` objects from orders and dex quotes.
* Converts the ETH value to an amount in output tokens.
*
* By default this prefers the outputAmountPerEth, but if this value
* is zero it will utilize the inputAmountPerEth and input.
*/
export function createFills(opts: {
side: MarketOperation;
orders?: NativeOrderWithFillableAmounts[];
dexQuotes?: DexSample[][];
targetInput?: BigNumber;
outputAmountPerEth?: BigNumber;
inputAmountPerEth?: BigNumber;
excludedSources?: ERC20BridgeSource[];
feeSchedule?: FeeSchedule;
}): Fill[][] {
const { side } = opts;
const excludedSources = opts.excludedSources || [];
const feeSchedule = opts.feeSchedule || {};
const orders = opts.orders || [];
const dexQuotes = opts.dexQuotes || [];
const outputAmountPerEth = opts.outputAmountPerEth || ZERO_AMOUNT;
const inputAmountPerEth = opts.inputAmountPerEth || ZERO_AMOUNT;
// Create native fills.
const nativeFills = nativeOrdersToFills(
side,
orders.filter(o => o.fillableTakerAmount.isGreaterThan(0)),
opts.targetInput,
outputAmountPerEth,
inputAmountPerEth,
feeSchedule,
);
// Create DEX fills.
const dexFills = dexQuotes.map(singleSourceSamples =>
dexSamplesToFills(side, singleSourceSamples, outputAmountPerEth, inputAmountPerEth, feeSchedule),
);
return [...dexFills, nativeFills]
.map(p => clipFillsToInput(p, opts.targetInput))
.filter(fills => hasLiquidity(fills) && !excludedSources.includes(fills[0].source));
}
function clipFillsToInput(fills: Fill[], targetInput: BigNumber = POSITIVE_INF): Fill[] {
const clipped: Fill[] = [];
let input = ZERO_AMOUNT;
for (const fill of fills) {
if (input.gte(targetInput)) {
break;
}
input = input.plus(fill.input);
clipped.push(fill);
}
return clipped;
}
function hasLiquidity(fills: Fill[]): boolean {
if (fills.length === 0) {
return false;
}
const totalInput = BigNumber.sum(...fills.map(fill => fill.input));
const totalOutput = BigNumber.sum(...fills.map(fill => fill.output));
if (totalInput.isZero() || totalOutput.isZero()) {
return false;
}
return true;
}
export function ethToOutputAmount({
input,
output,
@ -85,29 +28,28 @@ 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<Fill & { adjustedRate: BigNumber }> = [];
for (const o of orders) {
const { fillableTakerAmount, fillableTakerFeeAmount, fillableMakerAmount, type } = 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 = fees[ERC20BridgeSource.Native] === undefined ? 0 : fees[ERC20BridgeSource.Native]!(o);
const { fee, gas } =
fees[ERC20BridgeSource.Native] === undefined ? DEFAULT_FEE_ESTIMATE : fees[ERC20BridgeSource.Native]!(order);
const outputPenalty = ethToOutputAmount({
input,
output,
@ -129,78 +71,63 @@ export function nativeOrdersToFills(
side === MarketOperation.Sell ? adjustedOutput.div(clippedInput) : clippedInput.div(adjustedOutput);
// Optionally skip orders with rates that are <= 0.
if (filterNegativeAdjustedRateOrders && adjustedRate.lte(0)) {
continue;
return undefined;
}
fills.push({
return {
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 },
});
}
// 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;
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({
const input = sample.input;
const output = sample.output;
const { fee, gas } =
fees[source] === undefined ? DEFAULT_FEE_ESTIMATE : fees[source]!(sample.fillData) || DEFAULT_FEE_ESTIMATE;
const penalty = ethToOutputAmount({
input,
output,
inputAmountPerEth,
outputAmountPerEth,
ethAmount: fee,
});
}
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
fills.push({
return {
sourcePathId,
input,
output,
adjustedOutput,
adjustedOutput: adjustOutput(side, output, penalty),
source,
fillData,
type: FillQuoteTransformerOrderType.Bridge,
index: i,
parent: i !== 0 ? fills[fills.length - 1] : undefined,
flags: SOURCE_FLAGS[source],
});
}
return fills;
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);
}

View File

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

View File

@ -41,19 +41,17 @@ import {
SOURCE_FLAGS,
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,12 +518,9 @@ 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(
optimalPath = findOptimalPathFromSamples(
side,
dexQuotes,
[...nativeOrders, ...augmentedRfqtIndicativeQuotes],
@ -530,46 +529,27 @@ export class MarketOperationUtils {
opts.feeSchedule,
this._sampler.chainId,
opts.neonRouterNumSamples,
opts.fillAdjustor,
opts.samplerMetrics,
);
} else {
// Convert native orders and dex quotes into `Fill` objects.
fills = createFills({
side,
orders: [...nativeOrders, ...augmentedRfqtIndicativeQuotes],
dexQuotes,
targetInput: inputAmount,
outputAmountPerEth,
inputAmountPerEth,
excludedSources: opts.excludedSources,
feeSchedule: opts.feeSchedule,
});
optimalPath = await findOptimalPathJSAsync(
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<void> {
const maxFallbackSlippage = opts.maxFallbackSlippage || 0;
const optimalPathRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT;
// Generate a fallback path if sources requiring a fallback (fragile) are in the optimal path.
// Native is relatively fragile (limit order collision, expiry, or lack of available maker balance)
// LiquidityProvider is relatively fragile (collision)
const fragileSources = [ERC20BridgeSource.Native, ERC20BridgeSource.LiquidityProvider];
const fragileFills = optimalPath.fills.filter(f => fragileSources.includes(f.source));
if (opts.allowFallback && fragileFills.length !== 0) {
// We create a fallback path that is exclusive of Native liquidity
// This is the optimal on-chain path for the entire input amount
const sturdyPenaltyOpts = {
...penaltyOpts,
exchangeProxyOverhead: (sourceFlags: bigint) =>
// tslint:disable-next-line: no-bitwise
penaltyOpts.exchangeProxyOverhead(sourceFlags | optimalPath.sourceFlags),
};
let sturdyOptimalPath: Path | undefined;
if (SHOULD_USE_RUST_ROUTER) {
const sturdySamples = dexQuotes.filter(
samples => samples.length > 0 && !fragileSources.includes(samples[0].source),
);
sturdyOptimalPath = findOptimalRustPathFromSamples(
side,
sturdySamples,
[],
inputAmount,
sturdyPenaltyOpts,
opts.feeSchedule,
this._sampler.chainId,
opts.neonRouterNumSamples,
undefined, // hack: set sampler metrics to undefined to avoid fallback timings
);
} else {
const sturdyFills = fills.filter(p => p.length > 0 && !fragileSources.includes(p[0].source));
sturdyOptimalPath = await findOptimalPathJSAsync(
side,
sturdyFills,
inputAmount,
opts.runLimit,
undefined, // hack: set sampler metrics to undefined to avoid fallback timings
sturdyPenaltyOpts,
);
}
// Calculate the slippage of on-chain sources compared to the most optimal path
// if within an acceptable threshold we enable a fallback to prevent reverts
if (
sturdyOptimalPath !== undefined &&
(fragileFills.length === optimalPath.fills.length ||
sturdyOptimalPath.adjustedSlippage(optimalPathRate) <= maxFallbackSlippage)
) {
optimalPath.addFallback(sturdyOptimalPath);
}
}
}
*/
}
// tslint:disable: max-file-line-count

View File

@ -9,6 +9,7 @@ import {
DexSample,
ExchangeProxyOverhead,
FeeSchedule,
FillAdjustor,
MarketSideLiquidity,
MultiHopFillData,
TokenAdjacencyGraph,
@ -38,6 +39,7 @@ export function getBestTwoHopQuote(
marketSideLiquidity: Omit<MarketSideLiquidity, 'makerTokenDecimals' | 'takerTokenDecimals'>,
feeSchedule?: FeeSchedule,
exchangeProxyOverhead?: ExchangeProxyOverhead,
fillAdjustor?: FillAdjustor,
): { quote: DexSample<MultiHopFillData> | 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],
},

View File

@ -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<NativeFillData>,
side: MarketOperation,
): OptimizedMarketOrderBase<NativeLimitOrderFillData> | OptimizedMarketOrderBase<NativeRfqOrderFillData> {
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];
}

View File

@ -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<CollapsedFill>;
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<Fill>,
@ -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<NativeFillData>, opts.side));
} else {
this.orders.push(createBridgeOrder(normalizedFill, makerToken, takerToken, opts.side));
}
this.orders.push(createBridgeOrder(collapsedFills[i], makerToken, takerToken, opts.side));
i += 1;
}
return this as CollapsedPath;
}
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<CollapsedFill> {
this.collapsedFills = [];
for (const fill of this.fills) {
const source = fill.source;
if (this.collapsedFills.length !== 0 && source !== ERC20BridgeSource.Native) {
const prevFill = this.collapsedFills[this.collapsedFills.length - 1];
// If the last fill is from the same source, merge them.
if (prevFill.sourcePathId === fill.sourcePathId) {
prevFill.input = prevFill.input.plus(fill.input);
prevFill.output = prevFill.output.plus(fill.output);
prevFill.fillData = fill.fillData;
prevFill.subFills.push(fill);
continue;
}
}
(this.collapsedFills as CollapsedFill[]).push({
sourcePathId: fill.sourcePathId,
source: fill.source,
type: fill.type,
fillData: fill.fillData,
input: fill.input,
output: fill.output,
subFills: [fill],
});
}
return this.collapsedFills;
}
private _addFillSize(fill: Fill): void {
@ -285,7 +160,6 @@ export class Path {
}
}
export interface CollapsedPath extends Path {
readonly collapsedFills: ReadonlyArray<CollapsedFill>;
export interface FinalizedPath extends Path {
readonly orders: OptimizedMarketOrder[];
}

View File

@ -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<ERC20BridgeSource>,
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<DexSample<FillData>>;
// Descend to approach a closer fill for fillData which may not be consistent
// throughout the path (UniswapV3) and for a closer guesstimate at
// gas used
// From the output of the router, find the closest Sample in terms of input.
// The Router may have chosen an amount to fill that we do not have a measured sample of
// Choosing this accurately is required in some sources where the `FillData` may change depending
// on the size of the trade. For example, UniswapV3 has variable gas cost
// which increases with input.
assert.assert(routeSamples.length >= 1, 'Found no sample to use for source');
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<SerializedPath>(
(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)
// 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(),
);
.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<Path | undefined> {
const beforeTimeMs = performance.now();
// Sort fill arrays by descending adjusted completed rate.
// Remove any paths which cannot impact the optimal path
const sortedPaths = reducePaths(fillsToSortedPaths(fills, side, targetInput, opts), side);
if (sortedPaths.length === 0) {
return undefined;
}
const rates = rateBySourcePathId(sortedPaths);
let optimalPath = sortedPaths[0];
for (const [i, path] of sortedPaths.slice(1).entries()) {
optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit * RUN_LIMIT_DECAY_FACTOR ** i, rates);
// Yield to event loop.
await Promise.resolve();
}
const finalPath = optimalPath.isComplete() ? optimalPath : undefined;
// tslint:disable-next-line: no-unused-expression
samplerMetrics &&
samplerMetrics.logRouterDetails({
router: 'js',
type: 'total',
timingMs: performance.now() - beforeTimeMs,
});
return finalPath;
}
// Sort fill arrays by descending adjusted completed rate.
export function fillsToSortedPaths(
fills: Fill[][],
side: MarketOperation,
targetInput: BigNumber,
opts: PathPenaltyOpts,
): Path[] {
const paths = fills.map(singleSourceFills => Path.create(side, singleSourceFills, targetInput, opts));
const sortedPaths = paths.sort((a, b) => {
const aRate = a.adjustedCompleteRate();
const bRate = b.adjustedCompleteRate();
// There is a case where the adjusted completed rate isn't sufficient for the desired amount
// resulting in a NaN div by 0 (output)
if (bRate.isNaN()) {
return -1;
}
if (aRate.isNaN()) {
return 1;
}
return bRate.comparedTo(aRate);
});
return sortedPaths;
}
// Remove paths which have no impact on the optimal path
export function reducePaths(sortedPaths: Path[], side: MarketOperation): Path[] {
// Any path which has a min rate that is less than the best adjusted completed rate has no chance of improving
// the overall route.
const bestNonNativeCompletePath = sortedPaths.filter(
p => p.isComplete() && p.fills[0].source !== ERC20BridgeSource.Native,
)[0];
// If there is no complete path then just go ahead with the sorted paths
// I.e if the token only exists on sources which cannot sell to infinity
// or buys where X is greater than all the tokens available in the pools
if (!bestNonNativeCompletePath) {
return sortedPaths;
}
const bestNonNativeCompletePathAdjustedRate = bestNonNativeCompletePath.adjustedCompleteRate();
if (!bestNonNativeCompletePathAdjustedRate.isGreaterThan(0)) {
return sortedPaths;
}
const filteredPaths = sortedPaths.filter(p =>
p.bestRate().isGreaterThanOrEqualTo(bestNonNativeCompletePathAdjustedRate),
);
return filteredPaths;
}
function mixPaths(
side: MarketOperation,
pathA: Path,
pathB: Path,
targetInput: BigNumber,
maxSteps: number,
rates: { [id: string]: BigNumber },
): Path {
const _maxSteps = Math.max(maxSteps, 32);
let steps = 0;
// We assume pathA is the better of the two initially.
let bestPath: Path = pathA;
const _walk = (path: Path, remainingFills: Fill[]) => {
steps += 1;
if (path.isBetterThan(bestPath)) {
bestPath = path;
}
const remainingInput = targetInput.minus(path.size().input);
if (remainingInput.isGreaterThan(0)) {
for (let i = 0; i < remainingFills.length && steps < _maxSteps; ++i) {
const fill = remainingFills[i];
// Only walk valid paths.
if (!path.isValidNextFill(fill)) {
continue;
}
// Remove this fill from the next list of candidate fills.
const nextRemainingFills = remainingFills.slice();
nextRemainingFills.splice(i, 1);
// Recurse.
_walk(Path.clone(path).append(fill), nextRemainingFills);
}
}
};
const allFills = [...pathA.fills, ...pathB.fills];
// Sort subpaths by rate and keep fills contiguous to improve our
// chances of walking ideal, valid paths first.
const sortedFills = allFills.sort((a, b) => {
if (a.sourcePathId !== b.sourcePathId) {
return rates[b.sourcePathId].comparedTo(rates[a.sourcePathId]);
}
return a.index - b.index;
});
_walk(Path.create(side, [], targetInput, pathA.pathPenaltyOpts), sortedFills);
if (!bestPath.isValid()) {
throw new Error('nooope');
}
return bestPath;
}
function rateBySourcePathId(paths: Path[]): { [id: string]: BigNumber } {
return _.fromPairs(paths.map(p => [p.fills[0].sourcePathId, p.adjustedRate()]));
}

View File

@ -1,9 +1,20 @@
import { FillQuoteTransformerOrderType } from '@0x/protocol-utils';
import { BigNumber } from '@0x/utils';
import { 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(
// Flags to indicate which sources are used
const flags =
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);
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)) {

View File

@ -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<BigNumber> {
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 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 medianSample = flatSortedSamples[Math.floor(flatSortedSamples.length / 2)];
return medianSample.div(takerFillAmount);
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,
);

View File

@ -401,45 +401,10 @@ export interface Fill<TFillData extends FillData = FillData> {
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<TFillData extends FillData = FillData> {
source: ERC20BridgeSource;
type: FillQuoteTransformerOrderType; // should correspond with TFillData
fillData: TFillData;
// Unique ID of the original source path this fill belongs to.
// This is generated when the path is generated and is useful to distinguish
// paths that have the same `source` IDs but are distinct (e.g., Curves).
sourcePathId: string;
/**
* Total input amount (sum of `subFill`s)
*/
input: BigNumber;
/**
* Total output amount (sum of `subFill`s)
*/
output: BigNumber;
/**
* Quantities of all the fills that were collapsed.
*/
subFills: Array<{
input: BigNumber;
output: BigNumber;
}>;
}
/**
* A `CollapsedFill` wrapping a native order.
*/
export interface NativeCollapsedFill extends CollapsedFill<NativeFillData> {}
export interface OptimizedMarketOrderBase<TFillData extends FillData = FillData> {
source: ERC20BridgeSource;
fillData: TFillData;
@ -448,24 +413,21 @@ export interface OptimizedMarketOrderBase<TFillData extends FillData = FillData>
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<Fill, 'flags' | 'fillData' | 'sourcePathId' | 'source' | 'type'>; // Remove duplicates which have been brought into the OrderBase interface
}
export interface OptimizedMarketBridgeOrder<TFillData extends FillData = FillData>
extends OptimizedMarketOrderBase<TFillData> {
type: FillQuoteTransformerOrderType.Bridge;
fillData: TFillData;
sourcePathId: string;
}
export interface OptimizedLimitOrder extends OptimizedMarketOrderBase<NativeLimitOrderFillData> {
type: FillQuoteTransformerOrderType.Limit;
fillData: NativeLimitOrderFillData;
}
export interface OptimizedRfqOrder extends OptimizedMarketOrderBase<NativeRfqOrderFillData> {
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<TFillData extends FillData = FillData> ext
export interface OptimizerResult {
optimizedOrders: OptimizedMarketOrder[];
sourceFlags: bigint;
liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>;
liquidityDelivered: Readonly<Fill[] | DexSample<MultiHopFillData>>;
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[];
}

View File

@ -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<CollapsedFill> | DexSample<MultiHopFillData>,
liquidityDelivered: ReadonlyArray<Fill> | DexSample<MultiHopFillData>,
comparisonPrice?: BigNumber | undefined,
quoteRequestor?: QuoteRequestor,
): QuoteReport {
@ -174,7 +173,7 @@ export function generateQuoteReport(
export function generateExtendedQuoteReportSources(
marketOperation: MarketOperation,
quotes: RawQuotes,
liquidityDelivered: ReadonlyArray<CollapsedFill> | DexSample<MultiHopFillData>,
liquidityDelivered: ReadonlyArray<Fill> | DexSample<MultiHopFillData>,
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<NativeFillData> {
const { type } = cf;
return type === FillQuoteTransformerOrderType.Limit || type === FillQuoteTransformerOrderType.Rfq;
}

View File

@ -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,28 +151,18 @@ 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 { 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.order.fill.input,
fo.totalOrderInput,
fo.totalOrderInputFee,
);
const filledOutput = subFill.output.times(filledInput.div(subFill.input));
const filledOutput = fo.order.fill.output.times(filledInput.div(fo.order.fill.input));
const filledInputFee = filledInput.div(fo.totalOrderInput).times(fo.totalOrderInputFee);
const filledOutputFee = filledOutput.div(fo.totalOrderOutput).times(fo.totalOrderOutputFee);
@ -182,8 +172,6 @@ export function fillQuoteOrders(
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);

View File

@ -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) => {

View File

@ -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<NativeFillData>,
...optimizerFields,
};
}

View File

@ -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

View File

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

View File

@ -9,11 +9,10 @@ import * as TypeMoq from 'typemoq';
import { MarketOperation, NativeOrderWithFillableAmounts } from '../src/types';
import {
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<NativeFillData> {
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<QuoteRequestor>();
@ -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);

View File

@ -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<NativeLimitOrderFillData> {
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);
function createOrderFill(input: BigNumber, output: BigNumber): Fill {
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],
})),
input,
output,
flags: BigInt(0),
adjustedOutput: output,
gas: 1,
};
});
}
function countCollapsedFills(fillOrders: QuoteFillOrderCall[] | OptimizedMarketOrder[]): number {
let count = 0;
if ((fillOrders as any)[0].fills) {
const orders = (fillOrders as any) as OptimizedMarketOrder[];
for (const o of orders) {
count += o.fills.length;
}
} else {
const orders = (fillOrders as any) as QuoteFillOrderCall[];
for (const fo of orders) {
count += fo.order.fills.length;
}
}
return count;
}
function randomSide(): MarketOperation {
@ -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);