@uniswap/smart-order-router
Version:
Uniswap Smart Order Router
187 lines • 20.3 kB
JavaScript
import { BigNumber } from '@ethersproject/bignumber';
import { Percent, Price } from '@uniswap/sdk-core';
import { CurrencyAmount, log, WRAPPED_NATIVE_CURRENCY } from '../../../util';
import { calculateL1GasFeesHelper } from '../../../util/gas-factory-helpers';
import { BASE_SWAP_COST, COST_PER_HOP, COST_PER_INIT_TICK, COST_PER_UNINIT_TICK, SINGLE_HOP_OVERHEAD, TOKEN_OVERHEAD, } from './gas-costs';
import { getQuoteThroughNativePool, IOnChainGasModelFactory, } from './gas-model';
export class TickBasedHeuristicGasModelFactory extends IOnChainGasModelFactory {
constructor(provider) {
super();
this.provider = provider;
}
async buildGasModelInternal({ chainId, gasPriceWei, pools, amountToken, quoteToken, l2GasDataProvider, providerConfig, }) {
const l2GasData = l2GasDataProvider
? await l2GasDataProvider.getGasData(providerConfig)
: undefined;
const usdPool = pools.usdPool;
const calculateL1GasFees = async (route) => {
return await calculateL1GasFeesHelper(route, chainId, usdPool, quoteToken, pools.nativeAndQuoteTokenV3Pool, this.provider, l2GasData, providerConfig);
};
const nativeCurrency = WRAPPED_NATIVE_CURRENCY[chainId];
let nativeAmountPool = null;
if (!amountToken.equals(nativeCurrency)) {
nativeAmountPool = pools.nativeAndAmountTokenV3Pool;
}
const usdToken = usdPool.token0.equals(nativeCurrency)
? usdPool.token1
: usdPool.token0;
const estimateGasCost = (routeWithValidQuote) => {
var _a;
const { totalGasCostNativeCurrency, baseGasUse } = this.estimateGas(routeWithValidQuote, gasPriceWei, chainId, providerConfig);
/** ------ MARK: USD logic -------- */
// We only need to go through V2 and V3 USD pools for now,
// because v4 pools don't have deep liquidity yet.
// If one day, we see v3 usd pools have much deeper liquidity than v2/v3 usd pools, then we will add v4 pools for gas cost
const gasCostInTermsOfUSD = getQuoteThroughNativePool(chainId, totalGasCostNativeCurrency, usdPool);
/** ------ MARK: Conditional logic run if gasToken is specified -------- */
const nativeAndSpecifiedGasTokenPool = pools.nativeAndSpecifiedGasTokenV3Pool;
let gasCostInTermsOfGasToken = undefined;
// we don't want to fetch the gasToken pool if the gasToken is the native currency
if (nativeAndSpecifiedGasTokenPool) {
// We only need to go through V2 and V3 USD pools for now,
// because v4 pools don't have deep liquidity yet.
// If one day, we see v3 usd pools have much deeper liquidity than v2/v3 usd pools, then we will add v4 pools for gas cost
gasCostInTermsOfGasToken = getQuoteThroughNativePool(chainId, totalGasCostNativeCurrency, nativeAndSpecifiedGasTokenPool);
}
// if the gasToken is the native currency, we can just use the totalGasCostNativeCurrency
else if ((_a = providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.gasToken) === null || _a === void 0 ? void 0 : _a.equals(nativeCurrency)) {
gasCostInTermsOfGasToken = totalGasCostNativeCurrency;
}
/** ------ MARK: return early if quoteToken is wrapped native currency ------- */
if (quoteToken.equals(nativeCurrency)) {
return {
gasEstimate: baseGasUse,
gasCostInToken: totalGasCostNativeCurrency,
gasCostInUSD: gasCostInTermsOfUSD,
gasCostInGasToken: gasCostInTermsOfGasToken,
};
}
/** ------ MARK: Main gas logic in terms of quote token -------- */
// Since the quote token is not in the native currency, we convert the gas cost to be in terms of the quote token.
// We do this by getting the highest liquidity <quoteToken>/<nativeCurrency> pool. eg. <quoteToken>/ETH pool.
const nativeAndQuoteTokenPool = pools.nativeAndQuoteTokenV3Pool;
let gasCostInTermsOfQuoteToken = null;
if (nativeAndQuoteTokenPool) {
// We only need to go through V2 and V3 USD pools for now,
// because v4 pools don't have deep liquidity yet.
// If one day, we see v3 usd pools have much deeper liquidity than v2/v3 usd pools, then we will add v4 pools for gas cost
gasCostInTermsOfQuoteToken = getQuoteThroughNativePool(chainId, totalGasCostNativeCurrency, nativeAndQuoteTokenPool);
}
// We may have a nativeAmountPool, but not a nativePool
else {
log.info(`Unable to find ${nativeCurrency.symbol} pool with the quote token, ${quoteToken.symbol} to produce gas adjusted costs. Using amountToken to calculate gas costs.`);
}
/** ------ MARK: (V3 and V4 ONLY) Logic for calculating synthetic gas cost in terms of amount token -------- */
// TODO: evaluate effectiveness and potentially refactor
// Highest liquidity pool for the non quote token / ETH
// A pool with the non quote token / ETH should not be required and errors should be handled separately
if (nativeAmountPool) {
// get current execution price (amountToken / quoteToken)
const executionPrice = new Price(routeWithValidQuote.amount.currency, routeWithValidQuote.quote.currency, routeWithValidQuote.amount.quotient, routeWithValidQuote.quote.quotient);
const inputIsToken0 = nativeAmountPool.token0.address == nativeCurrency.address;
// ratio of input / native
const nativeAndAmountTokenPrice = inputIsToken0
? nativeAmountPool.token0Price
: nativeAmountPool.token1Price;
const gasCostInTermsOfAmountToken = nativeAndAmountTokenPrice.quote(totalGasCostNativeCurrency);
// Convert gasCostInTermsOfAmountToken to quote token using execution price
let syntheticGasCostInTermsOfQuoteToken;
try {
syntheticGasCostInTermsOfQuoteToken = executionPrice.quote(gasCostInTermsOfAmountToken);
}
catch (err) {
if (err instanceof RangeError &&
err.message.includes('Division by zero')) {
// If the quote fails (division by zero), set syntheticGasCostInTermsOfQuoteToken to null
syntheticGasCostInTermsOfQuoteToken = null;
}
else {
// any other error, throw
throw err;
}
}
// Note that the syntheticGasCost being lessThan the original quoted value is not always strictly better
// e.g. the scenario where the amountToken/ETH pool is very illiquid as well and returns an extremely small number
// however, it is better to have the gasEstimation be almost 0 than almost infinity, as the user will still receive a quote
// Only use syntheticGasCostInTermsOfQuoteToken if it's within 30% of the original gasCostInTermsOfQuoteToken as a safeguard.
if (syntheticGasCostInTermsOfQuoteToken !== null &&
(gasCostInTermsOfQuoteToken === null ||
(syntheticGasCostInTermsOfQuoteToken.lessThan(gasCostInTermsOfQuoteToken.asFraction) &&
gasCostInTermsOfQuoteToken
.subtract(syntheticGasCostInTermsOfQuoteToken)
.lessThan(gasCostInTermsOfQuoteToken.multiply(new Percent(30, 100))
.asFraction)))) {
log.info({
nativeAndAmountTokenPrice: nativeAndAmountTokenPrice.toSignificant(6),
gasCostInTermsOfQuoteToken: gasCostInTermsOfQuoteToken
? gasCostInTermsOfQuoteToken.toExact()
: 0,
gasCostInTermsOfAmountToken: gasCostInTermsOfAmountToken.toExact(),
executionPrice: executionPrice.toSignificant(6),
syntheticGasCostInTermsOfQuoteToken: syntheticGasCostInTermsOfQuoteToken === null || syntheticGasCostInTermsOfQuoteToken === void 0 ? void 0 : syntheticGasCostInTermsOfQuoteToken.toSignificant(6),
}, 'New gasCostInTermsOfQuoteToken calculated with synthetic quote token price is less than original');
gasCostInTermsOfQuoteToken = syntheticGasCostInTermsOfQuoteToken;
}
}
// If gasCostInTermsOfQuoteToken is null, both attempts to calculate gasCostInTermsOfQuoteToken failed (nativePool and amountNativePool)
if (gasCostInTermsOfQuoteToken === null) {
log.info(`Unable to find ${nativeCurrency.symbol} pool with the quote token, ${quoteToken.symbol}, or amount Token, ${amountToken.symbol} to produce gas adjusted costs. Route will not account for gas.`);
return {
gasEstimate: baseGasUse,
gasCostInToken: CurrencyAmount.fromRawAmount(quoteToken, 0),
gasCostInUSD: CurrencyAmount.fromRawAmount(usdToken, 0),
};
}
return {
gasEstimate: baseGasUse,
gasCostInToken: gasCostInTermsOfQuoteToken,
gasCostInUSD: gasCostInTermsOfUSD,
gasCostInGasToken: gasCostInTermsOfGasToken,
};
};
return {
estimateGasCost: estimateGasCost.bind(this),
calculateL1GasFees,
};
}
estimateGas(routeWithValidQuote, gasPriceWei, chainId, providerConfig) {
var _a;
const totalInitializedTicksCrossed = this.totalInitializedTicksCrossed(routeWithValidQuote.initializedTicksCrossedList);
const totalHops = BigNumber.from(routeWithValidQuote.route.pools.length);
let hopsGasUse = COST_PER_HOP(chainId).mul(totalHops);
// We have observed that this algorithm tends to underestimate single hop swaps.
// We add a buffer in the case of a single hop swap.
if (totalHops.eq(1)) {
hopsGasUse = hopsGasUse.add(SINGLE_HOP_OVERHEAD(chainId));
}
// Some tokens have extremely expensive transferFrom functions, which causes
// us to underestimate them by a large amount. For known tokens, we apply an
// adjustment.
const tokenOverhead = TOKEN_OVERHEAD(chainId, routeWithValidQuote.route);
const tickGasUse = COST_PER_INIT_TICK(chainId).mul(totalInitializedTicksCrossed);
const uninitializedTickGasUse = COST_PER_UNINIT_TICK.mul(0);
/*
// Eventually we can just use the quoter gas estimate for the base gas use
// It will be more accurate than doing the offchain gas estimate like below
// It will become more critical when we are going to support v4 hookful routing,
// where we have no idea how much gas the hook(s) will cost.
// const baseGasUse = routeWithValidQuote.quoterGasEstimate
*/
// base estimate gas used based on chainId estimates for hops and ticks gas useage
const baseGasUse = BASE_SWAP_COST(chainId)
.add(hopsGasUse)
.add(tokenOverhead)
.add(tickGasUse)
.add(uninitializedTickGasUse)
.add((_a = providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.additionalGasOverhead) !== null && _a !== void 0 ? _a : BigNumber.from(0));
const baseGasCostWei = gasPriceWei.mul(baseGasUse);
const wrappedCurrency = WRAPPED_NATIVE_CURRENCY[chainId];
const totalGasCostNativeCurrency = CurrencyAmount.fromRawAmount(wrappedCurrency, baseGasCostWei.toString());
return {
totalGasCostNativeCurrency,
totalInitializedTicksCrossed,
baseGasUse,
};
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGljay1iYXNlZC1oZXVyaXN0aWMtZ2FzLW1vZGVsLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vLi4vc3JjL3JvdXRlcnMvYWxwaGEtcm91dGVyL2dhcy1tb2RlbHMvdGljay1iYXNlZC1oZXVyaXN0aWMtZ2FzLW1vZGVsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUVyRCxPQUFPLEVBQVcsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBRTVELE9BQU8sRUFBRSxjQUFjLEVBQUUsR0FBRyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sZUFBZSxDQUFDO0FBQzdFLE9BQU8sRUFBRSx3QkFBd0IsRUFBRSxNQUFNLG1DQUFtQyxDQUFDO0FBRTdFLE9BQU8sRUFDTCxjQUFjLEVBQ2QsWUFBWSxFQUNaLGtCQUFrQixFQUNsQixvQkFBb0IsRUFDcEIsbUJBQW1CLEVBQ25CLGNBQWMsR0FDZixNQUFNLGFBQWEsQ0FBQztBQUNyQixPQUFPLEVBR0wseUJBQXlCLEVBRXpCLHVCQUF1QixHQUN4QixNQUFNLGFBQWEsQ0FBQztBQUVyQixNQUFNLE9BQWdCLGlDQUVwQixTQUFRLHVCQUE2QztJQUdyRCxZQUFzQixRQUFzQjtRQUMxQyxLQUFLLEVBQUUsQ0FBQztRQUNSLElBQUksQ0FBQyxRQUFRLEdBQUcsUUFBUSxDQUFDO0lBQzNCLENBQUM7SUFFUyxLQUFLLENBQUMscUJBQXFCLENBQUMsRUFDcEMsT0FBTyxFQUNQLFdBQVcsRUFDWCxLQUFLLEVBQ0wsV0FBVyxFQUNYLFVBQVUsRUFDVixpQkFBaUIsRUFDakIsY0FBYyxHQUNrQjtRQUdoQyxNQUFNLFNBQVMsR0FBRyxpQkFBaUI7WUFDakMsQ0FBQyxDQUFDLE1BQU0saUJBQWlCLENBQUMsVUFBVSxDQUFDLGNBQWMsQ0FBQztZQUNwRCxDQUFDLENBQUMsU0FBUyxDQUFDO1FBRWQsTUFBTSxP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