UNPKG

@xspswap/smart-order-router

Version:
215 lines 21.3 kB
import { BigNumber } from '@ethersproject/bignumber'; import { Price, WETH9 } from '@x-swap-protocol/sdk-core'; import _ from 'lodash'; import { CurrencyAmount } from '../../../../util/amounts'; import { getHighestLiquidityV3NativePool, getHighestLiquidityV3USDPool, } from '../../../../util/gas-factory-helpers'; import { log } from '../../../../util/log'; import { IOnChainGasModelFactory, } from '../gas-model'; import { BASE_SWAP_COST, COST_PER_HOP, COST_PER_INIT_TICK, COST_PER_UNINIT_TICK, } from './gas-costs'; /** * Computes a gas estimate for a V3 swap using heuristics. * Considers number of hops in the route, number of ticks crossed * and the typical base cost for a swap. * * We get the number of ticks crossed in a swap from the QuoterV2 * contract. * * We compute gas estimates off-chain because * 1/ Calling eth_estimateGas for a swaps requires the caller to have * the full balance token being swapped, and approvals. * 2/ Tracking gas used using a wrapper contract is not accurate with Multicall * due to EIP-2929. We would have to make a request for every swap we wanted to estimate. * 3/ For V2 we simulate all our swaps off-chain so have no way to track gas used. * * @export * @class V3HeuristicGasModelFactory */ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { constructor() { super(); } async buildGasModel({ chainId, gasPriceWei, v3poolProvider: poolProvider, amountToken, quoteToken, }) { const usdPool = await getHighestLiquidityV3USDPool(chainId, poolProvider); const calculateL1GasFees = async () => { let l1Used = BigNumber.from(0); let l1FeeInWei = BigNumber.from(0); // wrap fee to native currency const nativeCurrency = WETH9[chainId]; const costNativeCurrency = CurrencyAmount.fromRawAmount(nativeCurrency, l1FeeInWei.toString()); // convert fee into usd const nativeTokenPrice = usdPool.token0.address == nativeCurrency.address ? usdPool.token0Price : usdPool.token1Price; const gasCostL1USD = nativeTokenPrice.quote(costNativeCurrency); let gasCostL1QuoteToken = costNativeCurrency; // if the inputted token is not in the native currency, quote a native/quote token pool to get the gas cost in terms of the quote token if (!quoteToken.equals(nativeCurrency)) { const nativePool = await getHighestLiquidityV3NativePool(quoteToken, poolProvider); if (!nativePool) { log.info('Could not find a pool to convert the cost into the quote token'); gasCostL1QuoteToken = CurrencyAmount.fromRawAmount(quoteToken, 0); } else { const nativeTokenPrice = nativePool.token0.address == nativeCurrency.address ? nativePool.token0Price : nativePool.token1Price; gasCostL1QuoteToken = nativeTokenPrice.quote(costNativeCurrency); } } // gasUsedL1 is the gas units used calculated from the bytes of the calldata // gasCostL1USD and gasCostL1QuoteToken is the cost of gas in each of those tokens return { gasUsedL1: l1Used, gasCostL1USD, gasCostL1QuoteToken, }; }; // If our quote token is WETH, we don't need to convert our gas use to be in terms // of the quote token in order to produce a gas adjusted amount. // We do return a gas use in USD however, so we still convert to usd. const nativeCurrency = WETH9[chainId]; if (quoteToken.equals(nativeCurrency)) { const estimateGasCost = (routeWithValidQuote) => { const { totalGasCostNativeCurrency, baseGasUse } = this.estimateGas(routeWithValidQuote, gasPriceWei, chainId); const token0 = usdPool.token0.address == nativeCurrency.address; const nativeTokenPrice = token0 ? usdPool.token0Price : usdPool.token1Price; const gasCostInTermsOfUSD = nativeTokenPrice.quote(totalGasCostNativeCurrency); return { gasEstimate: baseGasUse, gasCostInToken: totalGasCostNativeCurrency, gasCostInUSD: gasCostInTermsOfUSD, }; }; return { estimateGasCost, calculateL1GasFees, }; } // If 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 nativePool = await getHighestLiquidityV3NativePool(quoteToken, poolProvider); let nativeAmountPool = null; if (!amountToken.equals(nativeCurrency)) { nativeAmountPool = await getHighestLiquidityV3NativePool(amountToken, poolProvider); } const usdToken = usdPool.token0.address == nativeCurrency.address ? usdPool.token1 : usdPool.token0; const estimateGasCost = (routeWithValidQuote) => { const { totalGasCostNativeCurrency, baseGasUse } = this.estimateGas(routeWithValidQuote, gasPriceWei, chainId); let gasCostInTermsOfQuoteToken = null; if (nativePool) { const token0 = nativePool.token0.address == nativeCurrency.address; // returns mid price in terms of the native currency (the ratio of quoteToken/nativeToken) const nativeTokenPrice = token0 ? nativePool.token0Price : nativePool.token1Price; try { // native token is base currency gasCostInTermsOfQuoteToken = nativeTokenPrice.quote(totalGasCostNativeCurrency); } catch (err) { log.info({ nativeTokenPriceBase: nativeTokenPrice.baseCurrency, nativeTokenPriceQuote: nativeTokenPrice.quoteCurrency, gasCostInEth: totalGasCostNativeCurrency.currency, }, 'Debug eth price token issue'); throw err; } } // we 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.`); } // 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 nativeAmountTokenPrice = inputIsToken0 ? nativeAmountPool.token0Price : nativeAmountPool.token1Price; const gasCostInTermsOfAmountToken = nativeAmountTokenPrice.quote(totalGasCostNativeCurrency); // Convert gasCostInTermsOfAmountToken to quote token using execution price const syntheticGasCostInTermsOfQuoteToken = executionPrice.quote(gasCostInTermsOfAmountToken); // 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 if (gasCostInTermsOfQuoteToken === null || syntheticGasCostInTermsOfQuoteToken.lessThan(gasCostInTermsOfQuoteToken.asFraction)) { log.info({ nativeAmountTokenPrice: nativeAmountTokenPrice.toSignificant(6), gasCostInTermsOfQuoteToken: gasCostInTermsOfQuoteToken ? gasCostInTermsOfQuoteToken.toExact() : 0, gasCostInTermsOfAmountToken: gasCostInTermsOfAmountToken.toExact(), executionPrice: executionPrice.toSignificant(6), syntheticGasCostInTermsOfQuoteToken: syntheticGasCostInTermsOfQuoteToken.toSignificant(6), }, 'New gasCostInTermsOfQuoteToken calculated with synthetic quote token price is less than original'); gasCostInTermsOfQuoteToken = syntheticGasCostInTermsOfQuoteToken; } } // true if token0 is the native currency const token0USDPool = usdPool.token0.address == nativeCurrency.address; // gets the mid price of the pool in terms of the native token const nativeTokenPriceUSDPool = token0USDPool ? usdPool.token0Price : usdPool.token1Price; let gasCostInTermsOfUSD; try { gasCostInTermsOfUSD = nativeTokenPriceUSDPool.quote(totalGasCostNativeCurrency); } catch (err) { log.info({ usdT1: usdPool.token0.symbol, usdT2: usdPool.token1.symbol, gasCostInNativeToken: totalGasCostNativeCurrency.currency.symbol, }, 'Failed to compute USD gas price'); throw err; } // 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, }; }; return { estimateGasCost: estimateGasCost.bind(this), calculateL1GasFees, }; } estimateGas(routeWithValidQuote, gasPriceWei, chainId) { const totalInitializedTicksCrossed = BigNumber.from(Math.max(1, _.sum(routeWithValidQuote.initializedTicksCrossedList))); const totalHops = BigNumber.from(routeWithValidQuote.route.pools.length); const hopsGasUse = COST_PER_HOP(chainId).mul(totalHops); const tickGasUse = COST_PER_INIT_TICK(chainId).mul(totalInitializedTicksCrossed); const uninitializedTickGasUse = COST_PER_UNINIT_TICK.mul(0); // base estimate gas used based on chainId estimates for hops and ticks gas useage const baseGasUse = BASE_SWAP_COST(chainId) .add(hopsGasUse) .add(tickGasUse) .add(uninitializedTickGasUse); const baseGasCostWei = gasPriceWei.mul(baseGasUse); const wrappedCurrency = WETH9[chainId]; const totalGasCostNativeCurrency = CurrencyAmount.fromRawAmount(wrappedCurrency, baseGasCostWei.toString()); return { totalGasCostNativeCurrency, totalInitializedTicksCrossed, baseGasUse, }; } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidjMtaGV1cmlzdGljLWdhcy1tb2RlbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL3NyYy9yb3V0ZXJzL2FscGhhLXJvdXRlci9nYXMtbW9kZWxzL3YzL3YzLWhldXJpc3RpYy1nYXMtbW9kZWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBQ3JELE9BQU8sRUFBVyxLQUFLLEVBQUUsS0FBSyxFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFFbEUsT0FBTyxDQUFDLE1BQU0sUUFBUSxDQUFDO0FBRXZCLE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQztBQUMxRCxPQUFPLEVBQ0wsK0JBQStCLEVBQy9CLDRCQUE0QixHQUM3QixNQUFNLHNDQUFzQyxDQUFDO0FBQzlDLE9BQU8sRUFBRSxHQUFHLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUUzQyxPQUFPLEVBR0wsdUJBQXVCLEdBQ3hCLE1BQU0sY0FBYyxDQUFDO0FBRXRCLE9BQU8sRUFDTCxjQUFjLEVBQ2QsWUFBWSxFQUNaLGtCQUFrQixFQUNsQixvQkFBb0IsR0FDckIsTUFBTSxhQUFhLENBQUM7QUFFckI7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBaUJHO0FBQ0gsTUFBTSxPQUFPLDBCQUEyQixTQUFRLHVCQUF1QjtJQUNyRTtRQUNFLEtBQUssRUFBRSxDQUFDO0lBQ1YsQ0FBQztJQUVNLEtBQUssQ0FBQyxhQUFhLENBQUMsRUFDekIsT0FBTyxFQUNQLFdBQVcsRUFDWCxjQUFjLEVBQUUsWUFBWSxFQUM1QixXQUFXLEVBQ1gsVUFBVSxHQUVtQjtRQUM3QixNQUFNLE9BQU8sR0FBUyxNQUFNLDRCQUE0QixDQUN0RCxPQUFPLEVBQ1AsWUFBWSxDQUNiLENBQUM7UUFFRixNQUFNLGtCQUFrQixHQUFHLEtBQUssSUFJN0IsRUFBRTtZQUNILElBQUksTUFBTSxHQUFHLFNBQVMsQ0FBQyxJ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