fix gas/overhead evaluation

This commit is contained in:
Lawrence Forman
2022-03-15 19:09:04 -04:00
parent bd0d672012
commit 35c0cafd6b
8 changed files with 107 additions and 32 deletions

View File

@@ -324,7 +324,7 @@ export class SwapQuoter {
const calcOpts: GetMarketOrdersOpts = { const calcOpts: GetMarketOrdersOpts = {
...cloneOpts, ...cloneOpts,
gasPrice, gasPrice,
exchangeProxyOverhead: flags => gasPrice.times(opts.exchangeProxyOverhead(flags)), exchangeProxyOverhead: opts.exchangeProxyOverhead,
}; };
// pass the QuoteRequestor on if rfqt enabled // pass the QuoteRequestor on if rfqt enabled
if (calcOpts.rfqt !== undefined) { if (calcOpts.rfqt !== undefined) {

View File

@@ -1,10 +1,10 @@
import { Web3Wrapper } from '@0x/dev-utils'; import { Web3Wrapper } from '@0x/dev-utils';
import { BigNumber, logUtils } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { MarketOperation } from '../../types'; import { MarketOperation } from '../../types';
import { COMPARISON_PRICE_DECIMALS, SOURCE_FLAGS } from './constants'; import { COMPARISON_PRICE_DECIMALS } from './constants';
import { import {
ComparisonPrice, ComparisonPrice,
MarketSideLiquidity, MarketSideLiquidity,

View File

@@ -189,8 +189,10 @@ export function dexSamplesToFills(
const input = sample.input.minus(prevSample ? prevSample.input : 0); const input = sample.input.minus(prevSample ? prevSample.input : 0);
const output = sample.output.minus(prevSample ? prevSample.output : 0); const output = sample.output.minus(prevSample ? prevSample.output : 0);
const fee = gasPrice.times(sample.gasCost); const fee = gasPrice.times(sample.gasCost);
let penalty = ZERO_AMOUNT; let penalty = ZERO_AMOUNT;
if (i === 0) { if (i === 0) {
console.log(`fee: ${fee}, gasCost: ${sample.gasCost}, gasPrice: ${gasPrice}`);
// Only the first fill in a DEX path incurs a penalty. // Only the first fill in a DEX path incurs a penalty.
penalty = ethToOutputAmount({ penalty = ethToOutputAmount({
input, input,

View File

@@ -535,16 +535,21 @@ export class MarketOperationUtils {
throw new Error(AggregationError.NoOptimalPath); throw new Error(AggregationError.NoOptimalPath);
} }
// TODO: Find the unoptimized best rate to calculate savings from optimizer
const [takerToken, makerToken] = side === MarketOperation.Sell const [takerToken, makerToken] = side === MarketOperation.Sell
? [inputToken, outputToken] ? [inputToken, outputToken]
: [outputToken, inputToken]; : [outputToken, inputToken];
const adjustedRate = getHopRouteOverallAdjustedTakerToMakerRate(bestHopRoute, {
side,
exchangeProxyOverhead: opts.exchangeProxyOverhead,
gasPrice: opts.gasPrice,
inputAmountPerEth: tokenAmountPerEth[inputToken],
outputAmountPerEth: tokenAmountPerEth[outputToken],
});
return { return {
hops: bestHopRoute, adjustedRate,
adjustedRate: getHopRouteOverallRate(bestHopRoute),
// liquidityDelivered: collapsedPath.collapsedFills as CollapsedFill[],
marketSideLiquidity, marketSideLiquidity,
hops: bestHopRoute,
// liquidityDelivered: collapsedPath.collapsedFills as CollapsedFill[],
takerAmountPerEth: tokenAmountPerEth[takerToken], takerAmountPerEth: tokenAmountPerEth[takerToken],
makerAmountPerEth: tokenAmountPerEth[makerToken], makerAmountPerEth: tokenAmountPerEth[makerToken],
}; };
@@ -755,6 +760,7 @@ export class MarketOperationUtils {
return null; return null;
} }
const preFallbackPath = path;
if (doesPathNeedFallback(path)) { if (doesPathNeedFallback(path)) {
path = await this._addFallbackToPath({ path = await this._addFallbackToPath({
path, path,
@@ -779,14 +785,15 @@ export class MarketOperationUtils {
outputToken: opts.outputToken, outputToken: opts.outputToken,
}).orders; }).orders;
const completePathSize = preFallbackPath.completeSize();
return { return {
orders, orders,
inputToken: opts.inputToken, inputToken: opts.inputToken,
outputToken: opts.outputToken, outputToken: opts.outputToken,
inputAmount: path.size().input, inputAmount: completePathSize.input,
outputAmount: path.size().output, outputAmount: completePathSize.output,
adjustedCompleteRate: path.adjustedCompleteMakerToTakerRate(), sourceFlags: preFallbackPath.sourceFlags,
sourceFlags: path.sourceFlags, gasCost: preFallbackPath.gasCost(),
}; };
} }
@@ -810,10 +817,12 @@ export class MarketOperationUtils {
inputAmountPerEth: opts.inputAmountPerEth, inputAmountPerEth: opts.inputAmountPerEth,
gasPrice: opts.gasPrice, gasPrice: opts.gasPrice,
exchangeProxyOverhead: (sourceFlags: bigint) => { exchangeProxyOverhead: (sourceFlags: bigint) => {
if (!opts.exchangeProxyOverhead) { if (!opts.exchangeProxyOverhead || opts.isMultiHop) {
// Multihop routes get their overhead factored in after
// path-finding.
return ZERO_AMOUNT; return ZERO_AMOUNT;
} }
return opts.exchangeProxyOverhead(sourceFlags | (opts.isMultiHop ? SOURCE_FLAGS.MultiHop : BigInt(0))); return opts.exchangeProxyOverhead(sourceFlags);
}, },
}; };
@@ -979,9 +988,18 @@ export class MarketOperationUtils {
// Pick the route with the best rate. // Pick the route with the best rate.
let bestHopRoute; let bestHopRoute;
let bestHopRouteTotalRate; let bestHopRouteTotalRate;
const rateOpts = {
side,
gasPrice,
exchangeProxyOverhead,
inputAmountPerEth: tokenAmountPerEth[inputToken],
outputAmountPerEth: tokenAmountPerEth[outputToken],
};
for (const route of hopRoutes) { for (const route of hopRoutes) {
const rate = getHopRouteOverallRate(route); console.log(route.map(r => [r.inputToken, r.outputToken]));
const rate = getHopRouteOverallAdjustedTakerToMakerRate(route, rateOpts);
if (!bestHopRouteTotalRate || rate.gt(bestHopRouteTotalRate)) { if (!bestHopRouteTotalRate || rate.gt(bestHopRouteTotalRate)) {
console.log(`new best route: <\n\t${route.map(r => `path: ${r.inputToken}->${r.outputToken}, ${r.orders.map(o => o.source)}, output: ${r.outputAmount}`).join(',\n\t')}\n>, overall rate: ${rate}, overall gas cost: ${route.reduce((a,v) => a + v.gasCost, 0)}`);
bestHopRoute = route; bestHopRoute = route;
bestHopRouteTotalRate = rate; bestHopRouteTotalRate = rate;
} }
@@ -998,11 +1016,40 @@ function doesPathNeedFallback(path: Path): boolean {
// Compute the overall adjusted rate for a multihop path. // Compute the overall adjusted rate for a multihop path.
function getHopRouteOverallRate(hops: OptimizedHop[]): BigNumber { function getHopRouteOverallAdjustedTakerToMakerRate(
return hops.reduce( hops: OptimizedHop[],
(a, h) => a = a.times(h.adjustedCompleteRate), opts: {
new BigNumber(1), side: MarketOperation;
gasPrice: BigNumber;
exchangeProxyOverhead?: ExchangeProxyOverhead;
inputAmountPerEth: BigNumber;
outputAmountPerEth: BigNumber;
}): BigNumber
{
// Note for buys, order of hops will be reversed (maker -> taker), so this still works
// (ie, first hop has the true input amount and last hop has the true output amount).
const inputAmount = hops[0].inputAmount;
const outputAmount = hops[hops.length - 1].outputAmount;
let sourceFlags = hops.reduce((a, h) => a | h.sourceFlags, BigInt(0));
if (hops.length > 1) {
// Multi-hop.
sourceFlags |= SOURCE_FLAGS.MultiHop;
}
const exchangeProxyOverhead = opts.exchangeProxyOverhead || (() => ZERO_AMOUNT);
const totalEthCost = opts.gasPrice.times(
exchangeProxyOverhead(sourceFlags).plus(hops.reduce((a, h) => a + h.gasCost, 0)),
); );
console.log(totalEthCost, hops.length, exchangeProxyOverhead(sourceFlags));
const ethCostAsOutputAmount = ethToOutputAmount({
ethAmount: totalEthCost,
input: inputAmount,
output: outputAmount,
inputAmountPerEth: opts.inputAmountPerEth,
outputAmountPerEth: opts.outputAmountPerEth,
});
return opts.side === MarketOperation.Sell
? outputAmount.minus(ethCostAsOutputAmount).div(inputAmount)
: inputAmount.div(outputAmount.plus(ethCostAsOutputAmount));
} }
function indicativeRfqQuoteToSignedNativeOrder(iq: V4RFQIndicativeQuote): SignedRfqOrder { function indicativeRfqQuoteToSignedNativeOrder(iq: V4RFQIndicativeQuote): SignedRfqOrder {

View File

@@ -5,7 +5,7 @@ import { Address, MarketOperation } from '../../types';
import { POSITIVE_INF, ZERO_AMOUNT } from './constants'; import { POSITIVE_INF, ZERO_AMOUNT } from './constants';
import { ethToOutputAmount } from './fills'; import { ethToOutputAmount } from './fills';
import { createBridgeOrder, createNativeOptimizedOrder } from './orders'; import { createBridgeOrder, createNativeOptimizedOrder } from './orders';
import { getCompleteRate, getRate } from './rate_utils'; import { getCompleteTakerToMakerRate, getRate } from './rate_utils';
import { import {
CollapsedGenericBridgeFill, CollapsedGenericBridgeFill,
CollapsedFill, CollapsedFill,
@@ -136,10 +136,24 @@ export class Path {
return this._size; return this._size;
} }
public completeSize(): PathSize {
const { input, output } = this.size();
if (this.targetInput.eq(0) || input.eq(0) || output.eq(0)) {
return { input: ZERO_AMOUNT, output: ZERO_AMOUNT };
}
if (input.eq(this.targetInput)) {
return { input, output };
}
return {
input: this.targetInput,
output: this.targetInput.times(output.div(input)),
};
}
public adjustedSize(): PathSize { public adjustedSize(): PathSize {
const { input, output } = this._adjustedSize; const { input, output } = this._adjustedSize;
const { exchangeProxyOverhead, outputAmountPerEth, inputAmountPerEth } = this.pathPenaltyOpts; const { exchangeProxyOverhead, outputAmountPerEth, inputAmountPerEth, gasPrice } = this.pathPenaltyOpts;
const gasOverhead = exchangeProxyOverhead(this.sourceFlags); const gasOverhead = exchangeProxyOverhead(this.sourceFlags).times(gasPrice);
const pathPenalty = ethToOutputAmount({ const pathPenalty = ethToOutputAmount({
input, input,
output, output,
@@ -153,9 +167,9 @@ export class Path {
}; };
} }
public adjustedCompleteMakerToTakerRate(): BigNumber { public adjustedCompleteTakerToMakerRate(): BigNumber {
const { input, output } = this.adjustedSize(); const { input, output } = this.adjustedSize();
return getCompleteRate(this.side, input, output, this.targetInput); return getCompleteTakerToMakerRate(this.side, input, output, this.targetInput);
} }
public adjustedRate(): BigNumber { public adjustedRate(): BigNumber {
@@ -163,6 +177,18 @@ export class Path {
return getRate(this.side, input, output); return getRate(this.side, input, output);
} }
public gasCost(): number {
let lastSource;
let gas = 0;
for (const f of this.fills) {
if (lastSource !== f.source) {
lastSource = f.source;
gas += f.gasCost;
}
}
return gas;
}
/** /**
* Returns the best possible rate this path can offer, given the fills. * Returns the best possible rate this path can offer, given the fills.
*/ */
@@ -193,7 +219,7 @@ export class Path {
if (input.isLessThan(targetInput) || otherInput.isLessThan(targetInput)) { if (input.isLessThan(targetInput) || otherInput.isLessThan(targetInput)) {
return input.isGreaterThan(otherInput); return input.isGreaterThan(otherInput);
} else { } else {
return this.adjustedCompleteMakerToTakerRate().isGreaterThan(other.adjustedCompleteMakerToTakerRate()); return this.adjustedCompleteTakerToMakerRate().isGreaterThan(other.adjustedCompleteTakerToMakerRate());
} }
// if (otherInput.isLessThan(targetInput)) { // if (otherInput.isLessThan(targetInput)) {
// return input.isGreaterThan(otherInput); // return input.isGreaterThan(otherInput);

View File

@@ -437,7 +437,7 @@ export async function findOptimalPathJSAsync(
// Sort fill arrays by descending adjusted completed rate. // Sort fill arrays by descending adjusted completed rate.
// Remove any paths which cannot impact the optimal path // Remove any paths which cannot impact the optimal path
const sortedPaths = reducePaths(fillsToSortedPaths(fills, side, targetInput, opts)); const sortedPaths = reducePaths(fillsToSortedPaths(fills, side, targetInput, opts));
console.log(sortedPaths.map(p => ({ source: p.fills[0].source, data: p.fills[0].data, rate: p.adjustedCompleteMakerToTakerRate() }))); console.log(sortedPaths.map(p => ({ source: p.fills[0].source, data: p.fills[0].data, output: p.size().output, adjustedOutput: p.adjustedSize().output, rate: p.adjustedCompleteTakerToMakerRate() })));
if (sortedPaths.length === 0) { if (sortedPaths.length === 0) {
return undefined; return undefined;
} }
@@ -449,7 +449,7 @@ export async function findOptimalPathJSAsync(
await Promise.resolve(); await Promise.resolve();
} }
const finalPath = optimalPath.isComplete() ? optimalPath : undefined; const finalPath = optimalPath.isComplete() ? optimalPath : undefined;
console.log(finalPath?.fills.map(f => f.source), finalPath?.adjustedCompleteMakerToTakerRate()); console.log(finalPath?.fills.map(f => f.source), finalPath?.adjustedCompleteTakerToMakerRate());
// tslint:disable-next-line: no-unused-expression // tslint:disable-next-line: no-unused-expression
samplerMetrics && samplerMetrics &&
samplerMetrics.logRouterDetails({ samplerMetrics.logRouterDetails({
@@ -469,8 +469,8 @@ export function fillsToSortedPaths(
): Path[] { ): Path[] {
const paths = fills.map(singleSourceFills => Path.create(side, singleSourceFills, targetInput, opts)); const paths = fills.map(singleSourceFills => Path.create(side, singleSourceFills, targetInput, opts));
const sortedPaths = paths.sort((a, b) => { const sortedPaths = paths.sort((a, b) => {
const aRate = a.adjustedCompleteMakerToTakerRate(); const aRate = a.adjustedCompleteTakerToMakerRate();
const bRate = b.adjustedCompleteMakerToTakerRate(); const bRate = b.adjustedCompleteTakerToMakerRate();
// There is a case where the adjusted completed rate isn't sufficient for the desired amount // There is a case where the adjusted completed rate isn't sufficient for the desired amount
// resulting in a NaN div by 0 (output) // resulting in a NaN div by 0 (output)
if (bRate.isNaN()) { if (bRate.isNaN()) {
@@ -498,7 +498,7 @@ export function reducePaths(sortedPaths: Path[]): Path[] {
if (!bestNonNativeCompletePath) { if (!bestNonNativeCompletePath) {
return sortedPaths; return sortedPaths;
} }
const bestNonNativeCompletePathAdjustedRate = bestNonNativeCompletePath.adjustedCompleteMakerToTakerRate(); const bestNonNativeCompletePathAdjustedRate = bestNonNativeCompletePath.adjustedCompleteTakerToMakerRate();
if (!bestNonNativeCompletePathAdjustedRate.isGreaterThan(0)) { if (!bestNonNativeCompletePathAdjustedRate.isGreaterThan(0)) {
return sortedPaths; return sortedPaths;
} }

View File

@@ -10,7 +10,7 @@ import { ZERO_AMOUNT } from './constants';
* Computes the "complete" rate given the input/output of a path. * Computes the "complete" rate given the input/output of a path.
* This value penalizes the path if it falls short of the target input. * This value penalizes the path if it falls short of the target input.
*/ */
export function getCompleteRate( export function getCompleteTakerToMakerRate(
side: MarketOperation, side: MarketOperation,
input: BigNumber, input: BigNumber,
output: BigNumber, output: BigNumber,

View File

@@ -479,8 +479,8 @@ export interface OptimizedHop {
inputAmount: BigNumber; inputAmount: BigNumber;
outputAmount: BigNumber; outputAmount: BigNumber;
sourceFlags: bigint; sourceFlags: bigint;
gasCost: number;
orders: OptimizedOrder[]; orders: OptimizedOrder[];
adjustedCompleteRate: BigNumber;
} }
export interface OptimizerResult { export interface OptimizerResult {