UNPKG

@uniswap/smart-order-router

Version:
187 lines 20.3 kB
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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