diff --git a/cli/base-command.ts b/cli/base-command.ts index 7e6624fd2..42d8ce169 100644 --- a/cli/base-command.ts +++ b/cli/base-command.ts @@ -355,11 +355,12 @@ export abstract class BaseCommand extends Command { quoteGasAdjusted: CurrencyAmount, estimatedGasUsedQuoteToken: CurrencyAmount, estimatedGasUsedUSD: CurrencyAmount, + estimatedGasUsedGasToken: CurrencyAmount | undefined, methodParameters: MethodParameters | undefined, blockNumber: BigNumber, estimatedGasUsed: BigNumber, gasPriceWei: BigNumber, - simulationStatus?: SimulationStatus + simulationStatus?: SimulationStatus, ) { this.logger.info(`Best Route:`); this.logger.info(`${routeAmountsToString(routeAmounts)}`); @@ -385,6 +386,13 @@ export abstract class BaseCommand extends Command { Math.min(estimatedGasUsedUSD.currency.decimals, 6) )}` ); + if(estimatedGasUsedGasToken) { + this.logger.info( + `Gas Used gas token: ${estimatedGasUsedGasToken.toFixed( + Math.min(estimatedGasUsedGasToken.currency.decimals, 6) + )}` + ); + } this.logger.info(`Calldata: ${methodParameters?.calldata}`); this.logger.info(`Value: ${methodParameters?.value}`); this.logger.info({ diff --git a/cli/commands/quote-to-ratio.ts b/cli/commands/quote-to-ratio.ts index 9b99636ef..eec350e02 100644 --- a/cli/commands/quote-to-ratio.ts +++ b/cli/commands/quote-to-ratio.ts @@ -146,6 +146,7 @@ export class QuoteToRatio extends BaseCommand { estimatedGasUsed, estimatedGasUsedQuoteToken, estimatedGasUsedUSD, + estimatedGasUsedGasToken, gasPriceWei, methodParameters, quote, @@ -159,6 +160,7 @@ export class QuoteToRatio extends BaseCommand { quoteGasAdjusted, estimatedGasUsedQuoteToken, estimatedGasUsedUSD, + estimatedGasUsedGasToken, methodParameters, blockNumber, estimatedGasUsed, diff --git a/cli/commands/quote.ts b/cli/commands/quote.ts index 29e5e842d..4b935a96e 100644 --- a/cli/commands/quote.ts +++ b/cli/commands/quote.ts @@ -37,6 +37,7 @@ export class Quote extends BaseCommand { debugRouting: flags.boolean({ required: false, default: true }), enableFeeOnTransferFeeFetching: flags.boolean({ required: false, default: false }), requestBlockNumber: flags.integer({ required: false }), + gasToken: flags.string({ required: false }), }; async run() { @@ -68,7 +69,8 @@ export class Quote extends BaseCommand { simulate, debugRouting, enableFeeOnTransferFeeFetching, - requestBlockNumber + requestBlockNumber, + gasToken } = flags; const topNSecondHopForTokenAddress = new MapWithLowerCaseKey(); @@ -159,6 +161,7 @@ export class Quote extends BaseCommand { forceMixedRoutes, debugRouting, enableFeeOnTransferFeeFetching, + gasToken } ); } else { @@ -196,6 +199,7 @@ export class Quote extends BaseCommand { forceMixedRoutes, debugRouting, enableFeeOnTransferFeeFetching, + gasToken } ); } @@ -214,6 +218,7 @@ export class Quote extends BaseCommand { estimatedGasUsed, estimatedGasUsedQuoteToken, estimatedGasUsedUSD, + estimatedGasUsedGasToken, gasPriceWei, methodParameters, quote, @@ -228,6 +233,7 @@ export class Quote extends BaseCommand { quoteGasAdjusted, estimatedGasUsedQuoteToken, estimatedGasUsedUSD, + estimatedGasUsedGasToken, methodParameters, blockNumber, estimatedGasUsed, diff --git a/src/providers/caching-gas-provider.ts b/src/providers/caching-gas-provider.ts index c5cbfdea5..24b7288d6 100644 --- a/src/providers/caching-gas-provider.ts +++ b/src/providers/caching-gas-provider.ts @@ -12,7 +12,8 @@ import { GasPrice, IGasPriceProvider } from './gas-price-provider'; * @class CachingV3SubgraphProvider */ export class CachingGasStationProvider extends IGasPriceProvider { - private GAS_KEY = (chainId: ChainId, blockNumber: number) => `gasPrice-${chainId}-${blockNumber}`; + private GAS_KEY = (chainId: ChainId, blockNumber: number) => + `gasPrice-${chainId}-${blockNumber}`; /** * Creates an instance of CachingGasStationProvider. @@ -28,11 +29,16 @@ export class CachingGasStationProvider extends IGasPriceProvider { super(); } - public override async getGasPrice(latestBlockNumber: number, requestBlockNumber?: number): Promise { + public override async getGasPrice( + latestBlockNumber: number, + requestBlockNumber?: number + ): Promise { // If block number is specified in the request, we have to use that block number find any potential cache hits. // Otherwise, we can use the latest block number. const targetBlockNumber = requestBlockNumber ?? latestBlockNumber; - const cachedGasPrice = await this.cache.get(this.GAS_KEY(this.chainId, targetBlockNumber)); + const cachedGasPrice = await this.cache.get( + this.GAS_KEY(this.chainId, targetBlockNumber) + ); if (cachedGasPrice) { log.info( @@ -43,8 +49,14 @@ export class CachingGasStationProvider extends IGasPriceProvider { return cachedGasPrice; } - const gasPrice = await this.gasPriceProvider.getGasPrice(latestBlockNumber, requestBlockNumber); - await this.cache.set(this.GAS_KEY(this.chainId, targetBlockNumber), gasPrice); + const gasPrice = await this.gasPriceProvider.getGasPrice( + latestBlockNumber, + requestBlockNumber + ); + await this.cache.set( + this.GAS_KEY(this.chainId, targetBlockNumber), + gasPrice + ); return gasPrice; } diff --git a/src/providers/eip-1559-gas-price-provider.ts b/src/providers/eip-1559-gas-price-provider.ts index 9027d1015..a8ff23828 100644 --- a/src/providers/eip-1559-gas-price-provider.ts +++ b/src/providers/eip-1559-gas-price-provider.ts @@ -43,7 +43,10 @@ export class EIP1559GasPriceProvider extends IGasPriceProvider { super(); } - public override async getGasPrice(_latestBlockNumber: number, requestBlockNumber?: number): Promise { + public override async getGasPrice( + _latestBlockNumber: number, + requestBlockNumber?: number + ): Promise { const feeHistoryRaw = (await this.provider.send('eth_feeHistory', [ /** * @fix Use BigNumber.from(this.blocksToConsider).toHexString() after hardhat adds support @@ -53,7 +56,9 @@ export class EIP1559GasPriceProvider extends IGasPriceProvider { // If the block number is not specified, we have to send hardcoded 'latest' to infura RPC // because Infura node pool is eventually consistent and may not have the latest block from our block number. // See https://uniswapteam.slack.com/archives/C023A7JDTJP/p1702485038251449?thread_ts=1702471203.519869&cid=C023A7JDTJP - requestBlockNumber ? BigNumber.from(requestBlockNumber).toHexString().replace('0x0', '0x') : 'latest', + requestBlockNumber + ? BigNumber.from(requestBlockNumber).toHexString().replace('0x0', '0x') + : 'latest', [this.priorityFeePercentile], ])) as RawFeeHistoryResponse; diff --git a/src/providers/eth-estimate-gas-provider.ts b/src/providers/eth-estimate-gas-provider.ts index 65ccf12e3..d2a33115f 100644 --- a/src/providers/eth-estimate-gas-provider.ts +++ b/src/providers/eth-estimate-gas-provider.ts @@ -2,7 +2,12 @@ import { BigNumber } from '@ethersproject/bignumber'; import { JsonRpcProvider } from '@ethersproject/providers'; import { ChainId } from '@uniswap/sdk-core'; -import { SwapOptions, SwapRoute, SwapType } from '../routers'; +import { + GasModelProviderConfig, + SwapOptions, + SwapRoute, + SwapType, +} from '../routers'; import { BEACON_CHAIN_DEPOSIT_ADDRESS, log } from '../util'; import { calculateGasUsed, @@ -107,6 +112,7 @@ export class EthEstimateGasSimulator extends Simulator { const { estimatedGasUsedUSD, estimatedGasUsedQuoteToken, + estimatedGasUsedGasToken, quoteGasAdjusted, } = await calculateGasUsed( route.quote.currency.chainId, @@ -127,7 +133,8 @@ export class EthEstimateGasSimulator extends Simulator { estimatedGasUsed, estimatedGasUsedQuoteToken, estimatedGasUsedUSD, - swapOptions + swapOptions, + estimatedGasUsedGasToken ), simulationStatus: SimulationStatus.Succeeded, }; @@ -150,8 +157,7 @@ export class EthEstimateGasSimulator extends Simulator { swapOptions: SwapOptions, swapRoute: SwapRoute, l2GasData?: OptimismGasData | ArbitrumGasData | undefined, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _providerConfig?: ProviderConfig | undefined + _providerConfig?: GasModelProviderConfig | undefined ): Promise { const inputAmount = swapRoute.trade.inputAmount; if ( diff --git a/src/providers/eth-gas-station-info-gas-price-provider.ts b/src/providers/eth-gas-station-info-gas-price-provider.ts index b1876be56..8697d221a 100644 --- a/src/providers/eth-gas-station-info-gas-price-provider.ts +++ b/src/providers/eth-gas-station-info-gas-price-provider.ts @@ -28,8 +28,10 @@ export class ETHGasStationInfoProvider extends IGasPriceProvider { this.url = url; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public override async getGasPrice(_latestBlockNumber: number, _requestBlockNumber?: number): Promise { + public override async getGasPrice( + _latestBlockNumber: number, + _requestBlockNumber?: number + ): Promise { const response = await retry( async () => { return axios.get(this.url); diff --git a/src/providers/gas-price-provider.ts b/src/providers/gas-price-provider.ts index f71eeb78f..b7b3912c2 100644 --- a/src/providers/gas-price-provider.ts +++ b/src/providers/gas-price-provider.ts @@ -8,5 +8,8 @@ export type GasPrice = { * Provider for getting gas prices. */ export abstract class IGasPriceProvider { - public abstract getGasPrice(latestBlockNumber: number, requestBlockNumber?: number): Promise; + public abstract getGasPrice( + latestBlockNumber: number, + requestBlockNumber?: number + ): Promise; } diff --git a/src/providers/legacy-gas-price-provider.ts b/src/providers/legacy-gas-price-provider.ts index ca1e46956..292098289 100644 --- a/src/providers/legacy-gas-price-provider.ts +++ b/src/providers/legacy-gas-price-provider.ts @@ -7,8 +7,10 @@ export class LegacyGasPriceProvider extends IGasPriceProvider { super(); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public override async getGasPrice(_latestBlockNumber: number, _requestBlockNumber?: number): Promise { + public override async getGasPrice( + _latestBlockNumber: number, + _requestBlockNumber?: number + ): Promise { const gasPriceWei = await this.provider.getGasPrice(); return { gasPriceWei, diff --git a/src/providers/on-chain-gas-price-provider.ts b/src/providers/on-chain-gas-price-provider.ts index da402d58b..414bb697f 100644 --- a/src/providers/on-chain-gas-price-provider.ts +++ b/src/providers/on-chain-gas-price-provider.ts @@ -27,11 +27,20 @@ export class OnChainGasPriceProvider extends IGasPriceProvider { super(); } - public override async getGasPrice(latestBlockNumber: number, requestBlockNumber?: number): Promise { + public override async getGasPrice( + latestBlockNumber: number, + requestBlockNumber?: number + ): Promise { if (this.eipChains.includes(this.chainId)) { - return this.eip1559GasPriceProvider.getGasPrice(latestBlockNumber, requestBlockNumber); + return this.eip1559GasPriceProvider.getGasPrice( + latestBlockNumber, + requestBlockNumber + ); } - return this.legacyGasPriceProvider.getGasPrice(latestBlockNumber, requestBlockNumber); + return this.legacyGasPriceProvider.getGasPrice( + latestBlockNumber, + requestBlockNumber + ); } } diff --git a/src/providers/provider.ts b/src/providers/provider.ts index 07c0feec2..1b5ed44ba 100644 --- a/src/providers/provider.ts +++ b/src/providers/provider.ts @@ -1,14 +1,8 @@ -import { BigNumber } from '@ethersproject/bignumber'; - export type ProviderConfig = { /** * The block number to use when getting data on-chain. */ blockNumber?: number | Promise; - /* - * Any additional overhead to add to the gas estimate - */ - additionalGasOverhead?: BigNumber; /* * Debug flag to test some codepaths */ diff --git a/src/providers/simulation-provider.ts b/src/providers/simulation-provider.ts index f8c46c518..434889106 100644 --- a/src/providers/simulation-provider.ts +++ b/src/providers/simulation-provider.ts @@ -3,13 +3,17 @@ import { ChainId, TradeType } from '@uniswap/sdk-core'; import { PERMIT2_ADDRESS } from '@uniswap/universal-router-sdk'; import { BigNumber } from 'ethers/lib/ethers'; -import { SwapOptions, SwapRoute, SwapType } from '../routers'; +import { + GasModelProviderConfig, + SwapOptions, + SwapRoute, + SwapType, +} from '../routers'; import { Erc20__factory } from '../types/other/factories/Erc20__factory'; import { Permit2__factory } from '../types/other/factories/Permit2__factory'; import { CurrencyAmount, log, SWAP_ROUTER_02_ADDRESSES } from '../util'; import { IPortionProvider } from './portion-provider'; -import { ProviderConfig } from './provider'; import { ArbitrumGasData, OptimismGasData } from './v3/gas-data-provider'; export type SimulationResult = { @@ -60,7 +64,7 @@ export abstract class Simulator { amount: CurrencyAmount, quote: CurrencyAmount, l2GasData?: OptimismGasData | ArbitrumGasData, - providerConfig?: ProviderConfig + providerConfig?: GasModelProviderConfig ): Promise { const neededBalance = swapRoute.trade.tradeType == TradeType.EXACT_INPUT ? amount : quote; @@ -105,7 +109,7 @@ export abstract class Simulator { swapOptions: SwapOptions, swapRoute: SwapRoute, l2GasData?: OptimismGasData | ArbitrumGasData, - providerConfig?: ProviderConfig + providerConfig?: GasModelProviderConfig ): Promise; protected async userHasSufficientBalance( diff --git a/src/providers/static-gas-price-provider.ts b/src/providers/static-gas-price-provider.ts index cda2ee9dd..7b22ecd91 100644 --- a/src/providers/static-gas-price-provider.ts +++ b/src/providers/static-gas-price-provider.ts @@ -5,8 +5,11 @@ import { GasPrice, IGasPriceProvider } from './gas-price-provider'; export class StaticGasPriceProvider implements IGasPriceProvider { constructor(private gasPriceWei: BigNumber) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async getGasPrice(_latestBlockNumber: number, _requestBlockNumber?: number): Promise { + + async getGasPrice( + _latestBlockNumber: number, + _requestBlockNumber?: number + ): Promise { return { gasPriceWei: this.gasPriceWei }; } } diff --git a/src/providers/tenderly-simulation-provider.ts b/src/providers/tenderly-simulation-provider.ts index 04f2bbdfe..4fa345904 100644 --- a/src/providers/tenderly-simulation-provider.ts +++ b/src/providers/tenderly-simulation-provider.ts @@ -12,6 +12,7 @@ import axios, { AxiosRequestConfig } from 'axios'; import { BigNumber } from 'ethers/lib/ethers'; import { + GasModelProviderConfig, metric, MetricLoggerUnit, SwapOptions, @@ -34,7 +35,6 @@ import { import { EthEstimateGasSimulator } from './eth-estimate-gas-provider'; import { IPortionProvider } from './portion-provider'; -import { ProviderConfig } from './provider'; import { SimulationResult, SimulationStatus, @@ -115,7 +115,7 @@ export class FallbackTenderlySimulator extends Simulator { swapOptions: SwapOptions, swapRoute: SwapRoute, l2GasData?: ArbitrumGasData | OptimismGasData, - providerConfig?: ProviderConfig + providerConfig?: GasModelProviderConfig ): Promise { // Make call to eth estimate gas if possible // For erc20s, we must check if the token allowance is sufficient @@ -162,7 +162,11 @@ export class FallbackTenderlySimulator extends Simulator { log.error({ err: err }, 'Failed to simulate via Tenderly'); if (err instanceof Error && err.message.includes('timeout')) { - metric.putMetric('TenderlySimulationTimeouts', 1, MetricLoggerUnit.Count); + metric.putMetric( + 'TenderlySimulationTimeouts', + 1, + MetricLoggerUnit.Count + ); } return { ...swapRoute, simulationStatus: SimulationStatus.Failed }; } @@ -214,7 +218,7 @@ export class TenderlySimulator extends Simulator { swapOptions: SwapOptions, swapRoute: SwapRoute, l2GasData?: ArbitrumGasData | OptimismGasData, - providerConfig?: ProviderConfig + providerConfig?: GasModelProviderConfig ): Promise { const currencyIn = swapRoute.trade.inputAmount.currency; const tokenIn = currencyIn.wrapped; @@ -331,21 +335,23 @@ export class TenderlySimulator extends Simulator { const before = Date.now(); const { data: resp, status: httpStatus } = - await this.tenderlyServiceInstance.post( - url, - body, - opts - ).finally(() => { - metric.putMetric( - 'TenderlySimulationLatencies', - Date.now() - before, - MetricLoggerUnit.Milliseconds - ); - }); + await this.tenderlyServiceInstance + .post(url, body, opts) + .finally(() => { + metric.putMetric( + 'TenderlySimulationLatencies', + Date.now() - before, + MetricLoggerUnit.Milliseconds + ); + }); const latencies = Date.now() - before; log.info( - `Tenderly simulation universal router request body: ${JSON.stringify(body, null, 2)}, having latencies ${latencies} in milliseconds.` + `Tenderly simulation universal router request body: ${JSON.stringify( + body, + null, + 2 + )}, having latencies ${latencies} in milliseconds.` ); metric.putMetric( 'TenderlySimulationUniversalRouterLatencies', @@ -516,6 +522,7 @@ export class TenderlySimulator extends Simulator { const { estimatedGasUsedUSD, estimatedGasUsedQuoteToken, + estimatedGasUsedGasToken, quoteGasAdjusted, } = await calculateGasUsed( chainId, @@ -536,7 +543,8 @@ export class TenderlySimulator extends Simulator { estimatedGasUsed, estimatedGasUsedQuoteToken, estimatedGasUsedUSD, - swapOptions + swapOptions, + estimatedGasUsedGasToken ), simulationStatus: SimulationStatus.Succeeded, }; diff --git a/src/providers/v3/caching-pool-provider.ts b/src/providers/v3/caching-pool-provider.ts index bddcbf108..6f84020f4 100644 --- a/src/providers/v3/caching-pool-provider.ts +++ b/src/providers/v3/caching-pool-provider.ts @@ -18,8 +18,14 @@ import { IV3PoolProvider, V3PoolAccessor } from './pool-provider'; * @class CachingV3PoolProvider */ export class CachingV3PoolProvider implements IV3PoolProvider { - private POOL_KEY = (chainId: ChainId, address: string, blockNumber?: number) => - blockNumber ? `pool-${chainId}-${address}-${blockNumber}` : `pool-${chainId}-${address}`; + private POOL_KEY = ( + chainId: ChainId, + address: string, + blockNumber?: number + ) => + blockNumber + ? `pool-${chainId}-${address}-${blockNumber}` + : `pool-${chainId}-${address}`; /** * Creates an instance of CachingV3PoolProvider. @@ -41,7 +47,7 @@ export class CachingV3PoolProvider implements IV3PoolProvider { const poolsToGetTokenPairs: Array<[Token, Token, FeeAmount]> = []; const poolsToGetAddresses: string[] = []; const poolAddressToPool: { [poolAddress: string]: Pool } = {}; - const blockNumber = await providerConfig?.blockNumber + const blockNumber = await providerConfig?.blockNumber; for (const [tokenA, tokenB, feeAmount] of tokenPairs) { const { poolAddress, token0, token1 } = this.getPoolAddress( @@ -106,7 +112,10 @@ export class CachingV3PoolProvider implements IV3PoolProvider { if (pool) { poolAddressToPool[address] = pool; // We don't want to wait for this caching to complete before returning the pools. - this.cache.set(this.POOL_KEY(this.chainId, address, blockNumber), pool); + this.cache.set( + this.POOL_KEY(this.chainId, address, blockNumber), + pool + ); } } } diff --git a/src/routers/alpha-router/alpha-router.ts b/src/routers/alpha-router/alpha-router.ts index 777f7763c..3ae088093 100644 --- a/src/routers/alpha-router/alpha-router.ts +++ b/src/routers/alpha-router/alpha-router.ts @@ -60,7 +60,6 @@ import { IPortionProvider, PortionProvider, } from '../../providers/portion-provider'; -import { ProviderConfig } from '../../providers/provider'; import { OnChainTokenFeeFetcher } from '../../providers/token-fee-fetcher'; import { ITokenProvider, TokenProvider } from '../../providers/token-provider'; import { @@ -138,6 +137,7 @@ import { V3CandidatePools, } from './functions/get-candidate-pools'; import { + GasModelProviderConfig, IGasModel, IOnChainGasModelFactory, IV2GasModelFactory, @@ -419,6 +419,11 @@ export type AlphaRouterConfig = { * we need this as a pass-through flag to enable/disable this feature. */ saveTenderlySimulationIfFailed?: boolean; + /** + * Include an additional response field specifying the swap gas estimation in terms of a specific gas token. + * This requires a suitable Native/GasToken pool to exist on V3. If one does not exist this field will return null. + */ + gasToken?: string; }; export class AlphaRouter @@ -1080,10 +1085,20 @@ export class AlphaRouter log.warn(`Finalized routing config is ${JSON.stringify(routingConfig)}`); } - const gasPriceWei = await this.getGasPriceWei(await blockNumber, await partialRoutingConfig.blockNumber); + const gasPriceWei = await this.getGasPriceWei( + await blockNumber, + await partialRoutingConfig.blockNumber + ); const quoteToken = quoteCurrency.wrapped; - const providerConfig: ProviderConfig = { + // const gasTokenAccessor = await this.tokenProvider.getTokens([routingConfig.gasToken!]); + const gasToken = routingConfig.gasToken + ? ( + await this.tokenProvider.getTokens([routingConfig.gasToken]) + ).getTokenByAddress(routingConfig.gasToken) + : undefined; + + const providerConfig: GasModelProviderConfig = { ...routingConfig, blockNumber, additionalGasOverhead: NATIVE_OVERHEAD( @@ -1091,6 +1106,7 @@ export class AlphaRouter amount.currency, quoteCurrency ), + gasToken, }; const [v3GasModel, mixedRouteGasModel] = await this.getGasModels( @@ -1321,6 +1337,7 @@ export class AlphaRouter routes: routeAmounts, estimatedGasUsedQuoteToken, estimatedGasUsedUSD, + estimatedGasUsedGasToken, } = swapRouteRaw; if ( @@ -1448,6 +1465,7 @@ export class AlphaRouter estimatedGasUsed, estimatedGasUsedQuoteToken, estimatedGasUsedUSD, + estimatedGasUsedGasToken, gasPriceWei, route: routeAmounts, trade, @@ -1468,7 +1486,14 @@ export class AlphaRouter throw new Error('Simulator not initialized!'); } - log.info(JSON.stringify({ swapConfig, methodParameters, providerConfig }, null, 2), `Starting simulation`); + log.info( + JSON.stringify( + { swapConfig, methodParameters, providerConfig }, + null, + 2 + ), + `Starting simulation` + ); const fromAddress = swapConfig.simulate.fromAddress; const beforeSimulate = Date.now(); const swapRouteWithSimulation = await this.simulator.simulate( @@ -1949,12 +1974,18 @@ export class AlphaRouter } } - private async getGasPriceWei(latestBlockNumber: number, requestBlockNumber?: number): Promise { + private async getGasPriceWei( + latestBlockNumber: number, + requestBlockNumber?: number + ): Promise { // Track how long it takes to resolve this async call. const beforeGasTimestamp = Date.now(); // Get an estimate of the gas price to use when estimating gas cost of different routes. - const { gasPriceWei } = await this.gasPriceProvider.getGasPrice(latestBlockNumber, requestBlockNumber); + const { gasPriceWei } = await this.gasPriceProvider.getGasPrice( + latestBlockNumber, + requestBlockNumber + ); metric.putMetric( 'GasPriceLoad', @@ -1969,7 +2000,7 @@ export class AlphaRouter gasPriceWei: BigNumber, amountToken: Token, quoteToken: Token, - providerConfig?: ProviderConfig + providerConfig?: GasModelProviderConfig ): Promise< [IGasModel, IGasModel] > { @@ -1981,14 +2012,16 @@ export class AlphaRouter providerConfig ); const nativeCurrency = WRAPPED_NATIVE_CURRENCY[this.chainId]; - const nativeQuoteTokenV3PoolPromise = !quoteToken.equals(nativeCurrency) + const nativeAndQuoteTokenV3PoolPromise = !quoteToken.equals(nativeCurrency) ? getHighestLiquidityV3NativePool( quoteToken, this.v3PoolProvider, providerConfig ) : Promise.resolve(null); - const nativeAmountTokenV3PoolPromise = !amountToken.equals(nativeCurrency) + const nativeAndAmountTokenV3PoolPromise = !amountToken.equals( + nativeCurrency + ) ? getHighestLiquidityV3NativePool( amountToken, this.v3PoolProvider, @@ -1996,17 +2029,35 @@ export class AlphaRouter ) : Promise.resolve(null); - const [usdPool, nativeQuoteTokenV3Pool, nativeAmountTokenV3Pool] = - await Promise.all([ - usdPoolPromise, - nativeQuoteTokenV3PoolPromise, - nativeAmountTokenV3PoolPromise, - ]); + // If a specific gas token is specified in the provider config + // fetch the highest liq V3 pool with it and the native currency + const nativeAndSpecifiedGasTokenV3PoolPromise = + providerConfig?.gasToken && + !providerConfig?.gasToken.equals(nativeCurrency) + ? getHighestLiquidityV3NativePool( + providerConfig?.gasToken, + this.v3PoolProvider, + providerConfig + ) + : Promise.resolve(null); + + const [ + usdPool, + nativeAndQuoteTokenV3Pool, + nativeAndAmountTokenV3Pool, + nativeAndSpecifiedGasTokenV3Pool, + ] = await Promise.all([ + usdPoolPromise, + nativeAndQuoteTokenV3PoolPromise, + nativeAndAmountTokenV3PoolPromise, + nativeAndSpecifiedGasTokenV3PoolPromise, + ]); const pools: LiquidityCalculationPools = { usdPool: usdPool, - nativeQuoteTokenV3Pool: nativeQuoteTokenV3Pool, - nativeAmountTokenV3Pool: nativeAmountTokenV3Pool, + nativeAndQuoteTokenV3Pool: nativeAndQuoteTokenV3Pool, + nativeAndAmountTokenV3Pool: nativeAndAmountTokenV3Pool, + nativeAndSpecifiedGasTokenV3Pool: nativeAndSpecifiedGasTokenV3Pool, }; const v3GasModelPromise = this.v3GasModelFactory.buildGasModel({ diff --git a/src/routers/alpha-router/entities/route-with-valid-quote.ts b/src/routers/alpha-router/entities/route-with-valid-quote.ts index 0b220acd5..3c9403456 100644 --- a/src/routers/alpha-router/entities/route-with-valid-quote.ts +++ b/src/routers/alpha-router/entities/route-with-valid-quote.ts @@ -32,6 +32,7 @@ export interface IRouteWithValidQuote< // The gas cost in terms of the quote token. gasCostInToken: CurrencyAmount; gasCostInUSD: CurrencyAmount; + gasCostInGasToken?: CurrencyAmount; tradeType: TradeType; poolAddresses: string[]; tokenPath: Token[]; @@ -87,6 +88,7 @@ export class V2RouteWithValidQuote implements IV2RouteWithValidQuote { public gasEstimate: BigNumber; public gasCostInToken: CurrencyAmount; public gasCostInUSD: CurrencyAmount; + public gasCostInGasToken?: CurrencyAmount; public tradeType: TradeType; public poolAddresses: string[]; public tokenPath: Token[]; @@ -118,12 +120,13 @@ export class V2RouteWithValidQuote implements IV2RouteWithValidQuote { this.quoteToken = quoteToken; this.tradeType = tradeType; - const { gasEstimate, gasCostInToken, gasCostInUSD } = + const { gasEstimate, gasCostInToken, gasCostInUSD, gasCostInGasToken } = this.gasModel.estimateGasCost(this); this.gasCostInToken = gasCostInToken; this.gasCostInUSD = gasCostInUSD; this.gasEstimate = gasEstimate; + this.gasCostInGasToken = gasCostInGasToken; // If its exact out, we need to request *more* of the input token to account for the gas. if (this.tradeType == TradeType.EXACT_INPUT) { @@ -181,6 +184,7 @@ export class V3RouteWithValidQuote implements IV3RouteWithValidQuote { public gasEstimate: BigNumber; public gasCostInToken: CurrencyAmount; public gasCostInUSD: CurrencyAmount; + public gasCostInGasToken?: CurrencyAmount; public tradeType: TradeType; public poolAddresses: string[]; public tokenPath: Token[]; @@ -218,12 +222,13 @@ export class V3RouteWithValidQuote implements IV3RouteWithValidQuote { this.quoteToken = quoteToken; this.tradeType = tradeType; - const { gasEstimate, gasCostInToken, gasCostInUSD } = + const { gasEstimate, gasCostInToken, gasCostInUSD, gasCostInGasToken } = this.gasModel.estimateGasCost(this); this.gasCostInToken = gasCostInToken; this.gasCostInUSD = gasCostInUSD; this.gasEstimate = gasEstimate; + this.gasCostInGasToken = gasCostInGasToken; // If its exact out, we need to request *more* of the input token to account for the gas. if (this.tradeType == TradeType.EXACT_INPUT) { @@ -283,6 +288,7 @@ export class MixedRouteWithValidQuote implements IMixedRouteWithValidQuote { public gasEstimate: BigNumber; public gasCostInToken: CurrencyAmount; public gasCostInUSD: CurrencyAmount; + public gasCostInGasToken?: CurrencyAmount; public tradeType: TradeType; public poolAddresses: string[]; public tokenPath: Token[]; @@ -321,12 +327,13 @@ export class MixedRouteWithValidQuote implements IMixedRouteWithValidQuote { this.quoteToken = quoteToken; this.tradeType = tradeType; - const { gasEstimate, gasCostInToken, gasCostInUSD } = + const { gasEstimate, gasCostInToken, gasCostInUSD, gasCostInGasToken } = this.gasModel.estimateGasCost(this); this.gasCostInToken = gasCostInToken; this.gasCostInUSD = gasCostInUSD; this.gasEstimate = gasEstimate; + this.gasCostInGasToken = gasCostInGasToken; // If its exact out, we need to request *more* of the input token to account for the gas. if (this.tradeType == TradeType.EXACT_INPUT) { diff --git a/src/routers/alpha-router/functions/best-swap-route.ts b/src/routers/alpha-router/functions/best-swap-route.ts index 1c3c5869d..54c28d951 100644 --- a/src/routers/alpha-router/functions/best-swap-route.ts +++ b/src/routers/alpha-router/functions/best-swap-route.ts @@ -27,6 +27,7 @@ export type BestSwapRoute = { estimatedGasUsed: BigNumber; estimatedGasUsedUSD: CurrencyAmount; estimatedGasUsedQuoteToken: CurrencyAmount; + estimatedGasUsedGasToken?: CurrencyAmount; routes: RouteWithValidQuote[]; }; @@ -524,6 +525,35 @@ export async function getBestSwapRouteBy( _.map(bestSwap, (routeWithValidQuote) => routeWithValidQuote.gasCostInToken) ).add(gasCostL1QuoteToken); + let estimatedGasUsedGasToken: CurrencyAmount | undefined; + if (routingConfig.gasToken) { + // sum the gas costs in the gas token across all routes + // if there is a route with undefined gasCostInGasToken, throw an error + if ( + bestSwap.some( + (routeWithValidQuote) => + routeWithValidQuote.gasCostInGasToken === undefined + ) + ) { + log.info( + { + bestSwap, + routingConfig, + }, + 'Could not find gasCostInGasToken for a route in bestSwap' + ); + throw new Error("Can't compute estimatedGasUsedGasToken"); + } + estimatedGasUsedGasToken = sumFn( + _.map( + bestSwap, + // ok to type cast here because we throw above if any are not defined + (routeWithValidQuote) => + routeWithValidQuote.gasCostInGasToken as CurrencyAmount + ) + ); + } + const quote = sumFn( _.map(bestSwap, (routeWithValidQuote) => routeWithValidQuote.quote) ); @@ -553,6 +583,7 @@ export async function getBestSwapRouteBy( estimatedGasUsed, estimatedGasUsedUSD, estimatedGasUsedQuoteToken, + estimatedGasUsedGasToken, routes: portionProvider.getRouteWithQuotePortionAdjusted( routeType, routeWithQuotes, diff --git a/src/routers/alpha-router/gas-models/gas-model.ts b/src/routers/alpha-router/gas-models/gas-model.ts index 14a99b752..a29dac112 100644 --- a/src/routers/alpha-router/gas-models/gas-model.ts +++ b/src/routers/alpha-router/gas-models/gas-model.ts @@ -1,5 +1,10 @@ import { BigNumber } from '@ethersproject/bignumber'; -import { ChainId, Token } from '@uniswap/sdk-core'; +import { + ChainId, + CurrencyAmount as CurrencyAmountRaw, + Token, +} from '@uniswap/sdk-core'; +import { Pair } from '@uniswap/v2-sdk'; import { Pool } from '@uniswap/v3-sdk'; import { ProviderConfig } from '../../../providers/provider'; @@ -45,6 +50,7 @@ import { IL2GasDataProvider, OptimismGasData, } from '../../../providers/v3/gas-data-provider'; +import { WRAPPED_NATIVE_CURRENCY } from '../../../util'; import { CurrencyAmount } from '../../../util/amounts'; import { MixedRouteWithValidQuote, @@ -82,6 +88,15 @@ export type L1ToL2GasCosts = { gasCostL1QuoteToken: CurrencyAmount; }; +export type GasModelProviderConfig = ProviderConfig & { + /* + * Any additional overhead to add to the gas estimate + */ + additionalGasOverhead?: BigNumber; + + gasToken?: Token; +}; + export type BuildOnChainGasModelFactoryType = { chainId: ChainId; gasPriceWei: BigNumber; @@ -92,7 +107,7 @@ export type BuildOnChainGasModelFactoryType = { l2GasDataProvider?: | IL2GasDataProvider | IL2GasDataProvider; - providerConfig?: ProviderConfig; + providerConfig?: GasModelProviderConfig; }; export type BuildV2GasModelFactoryType = { @@ -100,13 +115,14 @@ export type BuildV2GasModelFactoryType = { gasPriceWei: BigNumber; poolProvider: IV2PoolProvider; token: Token; - providerConfig?: ProviderConfig; + providerConfig?: GasModelProviderConfig; }; export type LiquidityCalculationPools = { usdPool: Pool; - nativeQuoteTokenV3Pool: Pool | null; - nativeAmountTokenV3Pool: Pool | null; + nativeAndQuoteTokenV3Pool: Pool | null; + nativeAndAmountTokenV3Pool: Pool | null; + nativeAndSpecifiedGasTokenV3Pool: Pool | null; }; /** @@ -130,6 +146,7 @@ export type IGasModel = { gasEstimate: BigNumber; gasCostInToken: CurrencyAmount; gasCostInUSD: CurrencyAmount; + gasCostInGasToken?: CurrencyAmount; }; calculateL1GasFees?(routes: TRouteWithValidQuote[]): Promise; }; @@ -170,13 +187,31 @@ export abstract class IOnChainGasModelFactory { public abstract buildGasModel({ chainId, gasPriceWei, - pools: LiquidityCalculationPools, + pools, amountToken, quoteToken, - v2poolProvider: V2poolProvider, + v2poolProvider, l2GasDataProvider, providerConfig, }: BuildOnChainGasModelFactoryType): Promise< IGasModel >; } + +// Determines if native currency is token0 +// Gets the native price of the pool, dependent on 0 or 1 +// quotes across the pool +export const getQuoteThroughNativePool = ( + chainId: ChainId, + nativeTokenAmount: CurrencyAmountRaw, + nativeTokenPool: Pool | Pair +): CurrencyAmount => { + const nativeCurrency = WRAPPED_NATIVE_CURRENCY[chainId]; + const isToken0 = nativeTokenPool.token0.equals(nativeCurrency); + // returns mid price in terms of the native currency (the ratio of token/nativeToken) + const nativeTokenPrice = isToken0 + ? nativeTokenPool.token0Price + : nativeTokenPool.token1Price; + // return gas cost in terms of the non native currency + return nativeTokenPrice.quote(nativeTokenAmount) as CurrencyAmount; +}; diff --git a/src/routers/alpha-router/gas-models/mixedRoute/mixed-route-heuristic-gas-model.ts b/src/routers/alpha-router/gas-models/mixedRoute/mixed-route-heuristic-gas-model.ts index 470667783..5990a239e 100644 --- a/src/routers/alpha-router/gas-models/mixedRoute/mixed-route-heuristic-gas-model.ts +++ b/src/routers/alpha-router/gas-models/mixedRoute/mixed-route-heuristic-gas-model.ts @@ -7,13 +7,14 @@ import JSBI from 'jsbi'; import _ from 'lodash'; import { WRAPPED_NATIVE_CURRENCY } from '../../../..'; -import { ProviderConfig } from '../../../../providers/provider'; import { log } from '../../../../util'; import { CurrencyAmount } from '../../../../util/amounts'; import { getV2NativePool } from '../../../../util/gas-factory-helpers'; import { MixedRouteWithValidQuote } from '../../entities/route-with-valid-quote'; import { BuildOnChainGasModelFactoryType, + GasModelProviderConfig, + getQuoteThroughNativePool, IGasModel, IOnChainGasModelFactory, } from '../gas-model'; @@ -61,55 +62,15 @@ export class MixedRouteHeuristicGasModelFactory extends IOnChainGasModelFactory }: BuildOnChainGasModelFactoryType): Promise< IGasModel > { - const usdPool: Pool = pools.usdPool; - - // 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 = WRAPPED_NATIVE_CURRENCY[chainId]!; - if (quoteToken.equals(nativeCurrency)) { - const estimateGasCost = ( - routeWithValidQuote: MixedRouteWithValidQuote - ): { - gasEstimate: BigNumber; - gasCostInToken: CurrencyAmount; - gasCostInUSD: CurrencyAmount; - } => { - const { totalGasCostNativeCurrency, baseGasUse } = this.estimateGas( - routeWithValidQuote, - gasPriceWei, - chainId, - providerConfig - ); - - const token0 = usdPool.token0.address == nativeCurrency.address; - - const nativeTokenPrice = token0 - ? usdPool.token0Price - : usdPool.token1Price; - - const gasCostInTermsOfUSD: CurrencyAmount = nativeTokenPrice.quote( - totalGasCostNativeCurrency - ) as CurrencyAmount; - - return { - gasEstimate: baseGasUse, - gasCostInToken: totalGasCostNativeCurrency, - gasCostInUSD: gasCostInTermsOfUSD, - }; - }; - - return { - estimateGasCost, - }; - } - - // 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 / pool. eg. /ETH pool. - const nativeV3Pool: Pool | null = pools.nativeQuoteTokenV3Pool; + const usdPool: Pool = pools.usdPool; + const usdToken = usdPool.token0.equals(nativeCurrency) + ? usdPool.token1 + : usdPool.token0; let nativeV2Pool: Pair | null; - if (V2poolProvider) { + // Avoid fetching for a (WETH,WETH) pool here, we handle the quoteToken = wrapped native case in estimateGasCost + if (!quoteToken.equals(nativeCurrency) && V2poolProvider) { /// MixedRoutes nativeV2Pool = await getV2NativePool( quoteToken, @@ -118,17 +79,13 @@ export class MixedRouteHeuristicGasModelFactory extends IOnChainGasModelFactory ); } - const usdToken = - usdPool.token0.address == nativeCurrency.address - ? usdPool.token1 - : usdPool.token0; - const estimateGasCost = ( routeWithValidQuote: MixedRouteWithValidQuote ): { gasEstimate: BigNumber; gasCostInToken: CurrencyAmount; gasCostInUSD: CurrencyAmount; + gasCostInGasToken?: CurrencyAmount; } => { const { totalGasCostNativeCurrency, baseGasUse } = this.estimateGas( routeWithValidQuote, @@ -137,6 +94,45 @@ export class MixedRouteHeuristicGasModelFactory extends IOnChainGasModelFactory providerConfig ); + /** ------ MARK: USD Logic -------- */ + const gasCostInTermsOfUSD = getQuoteThroughNativePool( + chainId, + totalGasCostNativeCurrency, + usdPool + ); + + /** ------ MARK: Conditional logic run if gasToken is specified -------- */ + const nativeAndSpecifiedGasTokenPool: Pool | null = + pools.nativeAndSpecifiedGasTokenV3Pool; + let gasCostInTermsOfGasToken: CurrencyAmount | undefined = undefined; + if (nativeAndSpecifiedGasTokenPool) { + gasCostInTermsOfGasToken = getQuoteThroughNativePool( + chainId, + totalGasCostNativeCurrency, + nativeAndSpecifiedGasTokenPool + ); + } + // if the gasToken is the native currency, we can just use the totalGasCostNativeCurrency + else if (providerConfig?.gasToken?.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 -------- */ + + // 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 / pool. eg. /ETH pool. + const nativeV3Pool: Pool | null = pools.nativeAndQuoteTokenV3Pool; + if (!nativeV3Pool && !nativeV2Pool) { log.info( `Unable to find ${nativeCurrency.symbol} pool with the quote token, ${quoteToken.symbol} to produce gas adjusted costs. Route will not account for gas.` @@ -156,60 +152,17 @@ export class MixedRouteHeuristicGasModelFactory extends IOnChainGasModelFactory ? nativeV2Pool : nativeV3Pool!; - 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; - - let gasCostInTermsOfQuoteToken: CurrencyAmount; - try { - // native token is base currency - gasCostInTermsOfQuoteToken = nativeTokenPrice.quote( - totalGasCostNativeCurrency - ) as CurrencyAmount; - } catch (err) { - log.info( - { - nativeTokenPriceBase: nativeTokenPrice.baseCurrency, - nativeTokenPriceQuote: nativeTokenPrice.quoteCurrency, - gasCostInEth: totalGasCostNativeCurrency.currency, - }, - 'Debug eth price token issue' - ); - throw err; - } - - // 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: CurrencyAmount; - try { - gasCostInTermsOfUSD = nativeTokenPriceUSDPool.quote( - totalGasCostNativeCurrency - ) as CurrencyAmount; - } catch (err) { - log.info( - { - usdT1: usdPool.token0.symbol, - usdT2: usdPool.token1.symbol, - gasCostInNativeToken: totalGasCostNativeCurrency.currency.symbol, - }, - 'Failed to compute USD gas price' - ); - throw err; - } + const gasCostInTermsOfQuoteToken = getQuoteThroughNativePool( + chainId, + totalGasCostNativeCurrency, + nativePool + ); return { gasEstimate: baseGasUse, gasCostInToken: gasCostInTermsOfQuoteToken, gasCostInUSD: gasCostInTermsOfUSD!, + gasCostInGasToken: gasCostInTermsOfGasToken, }; }; @@ -222,7 +175,7 @@ export class MixedRouteHeuristicGasModelFactory extends IOnChainGasModelFactory routeWithValidQuote: MixedRouteWithValidQuote, gasPriceWei: BigNumber, chainId: ChainId, - providerConfig?: ProviderConfig + providerConfig?: GasModelProviderConfig ) { const totalInitializedTicksCrossed = BigNumber.from( Math.max(1, _.sum(routeWithValidQuote.initializedTicksCrossedList)) diff --git a/src/routers/alpha-router/gas-models/v2/v2-heuristic-gas-model.ts b/src/routers/alpha-router/gas-models/v2/v2-heuristic-gas-model.ts index 16cba4ef9..0431961c2 100644 --- a/src/routers/alpha-router/gas-models/v2/v2-heuristic-gas-model.ts +++ b/src/routers/alpha-router/gas-models/v2/v2-heuristic-gas-model.ts @@ -10,6 +10,8 @@ import { CurrencyAmount } from '../../../../util/amounts'; import { V2RouteWithValidQuote } from '../../entities/route-with-valid-quote'; import { BuildV2GasModelFactoryType, + GasModelProviderConfig, + getQuoteThroughNativePool, IGasModel, IV2GasModelFactory, usdGasTokensByChain, @@ -50,144 +52,110 @@ export class V2HeuristicGasModelFactory extends IV2GasModelFactory { token, providerConfig, }: BuildV2GasModelFactoryType): Promise> { - if (token.equals(WRAPPED_NATIVE_CURRENCY[chainId]!)) { - const usdPool: Pair = await this.getHighestLiquidityUSDPool( - chainId, - poolProvider, - providerConfig - ); - - return { - estimateGasCost: (routeWithValidQuote: V2RouteWithValidQuote) => { - const { gasCostInEth, gasUse } = this.estimateGas( - routeWithValidQuote, - gasPriceWei, - chainId, - providerConfig - ); - - const ethToken0 = - usdPool.token0.address == WRAPPED_NATIVE_CURRENCY[chainId]!.address; - - const ethTokenPrice = ethToken0 - ? usdPool.token0Price - : usdPool.token1Price; - - const gasCostInTermsOfUSD: CurrencyAmount = ethTokenPrice.quote( - gasCostInEth - ) as CurrencyAmount; - - return { - gasEstimate: gasUse, - gasCostInToken: gasCostInEth, - gasCostInUSD: gasCostInTermsOfUSD, - }; - }, - }; - } - - // If the quote token is not WETH, we convert the gas cost to be in terms of the quote token. - // We do this by getting the highest liquidity /ETH pool. - const ethPoolPromise = this.getEthPool( + const usdPoolPromise: Promise = this.getHighestLiquidityUSDPool( chainId, - token, poolProvider, providerConfig ); - const usdPoolPromise = this.getHighestLiquidityUSDPool( - chainId, - poolProvider, - providerConfig - ); + // Only fetch the native gasToken pool if specified by the config AND the gas token is not the native currency. + const nativeAndSpecifiedGasTokenPoolPromise = + providerConfig?.gasToken && + !providerConfig?.gasToken.equals(WRAPPED_NATIVE_CURRENCY[chainId]!) + ? this.getEthPool( + chainId, + providerConfig.gasToken, + poolProvider, + providerConfig + ) + : Promise.resolve(null); - const [ethPool, usdPool] = await Promise.all([ - ethPoolPromise, + const [usdPool, nativeAndSpecifiedGasTokenPool] = await Promise.all([ usdPoolPromise, + nativeAndSpecifiedGasTokenPoolPromise, ]); - if (!ethPool) { - log.info( - 'Unable to find ETH pool with the quote token to produce gas adjusted costs. Route will not account for gas.' + let ethPool: Pair | null = null; + if (!token.equals(WRAPPED_NATIVE_CURRENCY[chainId]!)) { + ethPool = await this.getEthPool( + chainId, + token, + poolProvider, + providerConfig ); } + const usdToken = + usdPool.token0.address == WRAPPED_NATIVE_CURRENCY[chainId]!.address + ? usdPool.token1 + : usdPool.token0; + return { estimateGasCost: (routeWithValidQuote: V2RouteWithValidQuote) => { - const usdToken = - usdPool.token0.address == WRAPPED_NATIVE_CURRENCY[chainId]!.address - ? usdPool.token1 - : usdPool.token0; - const { gasCostInEth, gasUse } = this.estimateGas( routeWithValidQuote, gasPriceWei, chainId, - { - ...providerConfig, - } + providerConfig ); - if (!ethPool) { + /** ------ MARK: USD logic -------- */ + const gasCostInTermsOfUSD = getQuoteThroughNativePool( + chainId, + gasCostInEth, + usdPool + ); + + /** ------ MARK: Conditional logic run if gasToken is specified -------- */ + let gasCostInTermsOfGasToken: CurrencyAmount | undefined = undefined; + if (nativeAndSpecifiedGasTokenPool) { + gasCostInTermsOfGasToken = getQuoteThroughNativePool( + chainId, + gasCostInEth, + nativeAndSpecifiedGasTokenPool + ); + } + // if the gasToken is the native currency, we can just use the gasCostInEth + else if ( + providerConfig?.gasToken?.equals(WRAPPED_NATIVE_CURRENCY[chainId]!) + ) { + gasCostInTermsOfGasToken = gasCostInEth; + } + + /** ------ MARK: return early if quoteToken is wrapped native currency ------- */ + if (token.equals(WRAPPED_NATIVE_CURRENCY[chainId]!)) { return { gasEstimate: gasUse, - gasCostInToken: CurrencyAmount.fromRawAmount(token, 0), - gasCostInUSD: CurrencyAmount.fromRawAmount(usdToken, 0), + gasCostInToken: gasCostInEth, + gasCostInUSD: gasCostInTermsOfUSD, + gasCostInGasToken: gasCostInTermsOfGasToken, }; } - const ethToken0 = - ethPool.token0.address == WRAPPED_NATIVE_CURRENCY[chainId]!.address; - - const ethTokenPrice = ethToken0 - ? ethPool.token0Price - : ethPool.token1Price; - - let gasCostInTermsOfQuoteToken: CurrencyAmount; - try { - gasCostInTermsOfQuoteToken = ethTokenPrice.quote( - gasCostInEth - ) as CurrencyAmount; - } catch (err) { - log.error( - { - ethTokenPriceBase: ethTokenPrice.baseCurrency, - ethTokenPriceQuote: ethTokenPrice.quoteCurrency, - gasCostInEth: gasCostInEth.currency, - }, - 'Debug eth price token issue' + // If the quote token is not WETH, we convert the gas cost to be in terms of the quote token. + // We do this by getting the highest liquidity /ETH pool. + if (!ethPool) { + log.info( + 'Unable to find ETH pool with the quote token to produce gas adjusted costs. Route will not account for gas.' ); - throw err; + return { + gasEstimate: gasUse, + gasCostInToken: CurrencyAmount.fromRawAmount(token, 0), + gasCostInUSD: CurrencyAmount.fromRawAmount(usdToken, 0), + }; } - const ethToken0USDPool = - usdPool.token0.address == WRAPPED_NATIVE_CURRENCY[chainId]!.address; - - const ethTokenPriceUSDPool = ethToken0USDPool - ? usdPool.token0Price - : usdPool.token1Price; - - let gasCostInTermsOfUSD: CurrencyAmount; - try { - gasCostInTermsOfUSD = ethTokenPriceUSDPool.quote( - gasCostInEth - ) as CurrencyAmount; - } catch (err) { - log.error( - { - usdT1: usdPool.token0.symbol, - usdT2: usdPool.token1.symbol, - gasCostInEthToken: gasCostInEth.currency.symbol, - }, - 'Failed to compute USD gas price' - ); - throw err; - } + const gasCostInTermsOfQuoteToken = getQuoteThroughNativePool( + chainId, + gasCostInEth, + ethPool + ); return { gasEstimate: gasUse, gasCostInToken: gasCostInTermsOfQuoteToken, gasCostInUSD: gasCostInTermsOfUSD!, + gasCostInGasToken: gasCostInTermsOfGasToken, }; }, }; @@ -197,7 +165,7 @@ export class V2HeuristicGasModelFactory extends IV2GasModelFactory { routeWithValidQuote: V2RouteWithValidQuote, gasPriceWei: BigNumber, chainId: ChainId, - providerConfig?: ProviderConfig + providerConfig?: GasModelProviderConfig ) { const hops = routeWithValidQuote.route.pairs.length; let gasUse = BASE_SWAP_COST.add(COST_PER_EXTRA_HOP.mul(hops - 1)); @@ -270,7 +238,12 @@ export class V2HeuristicGasModelFactory extends IV2GasModelFactory { const poolsRaw = poolAccessor.getAllPools(); const pools = _.filter( poolsRaw, - (pool) => pool.reserve0.greaterThan(0) && pool.reserve1.greaterThan(0) + (pool) => + pool.reserve0.greaterThan(0) && + pool.reserve1.greaterThan(0) && + // this case should never happen in production, but when we mock the pool provider it may return non native pairs + (pool.token0.equals(WRAPPED_NATIVE_CURRENCY[chainId]!) || + pool.token1.equals(WRAPPED_NATIVE_CURRENCY[chainId]!)) ); if (pools.length == 0) { diff --git a/src/routers/alpha-router/gas-models/v3/v3-heuristic-gas-model.ts b/src/routers/alpha-router/gas-models/v3/v3-heuristic-gas-model.ts index 8b8129dd5..d9d58f309 100644 --- a/src/routers/alpha-router/gas-models/v3/v3-heuristic-gas-model.ts +++ b/src/routers/alpha-router/gas-models/v3/v3-heuristic-gas-model.ts @@ -8,7 +8,6 @@ import { SwapType, WRAPPED_NATIVE_CURRENCY, } from '../../../..'; -import { ProviderConfig } from '../../../../providers/provider'; import { ArbitrumGasData, OptimismGasData, @@ -23,6 +22,8 @@ import { import { V3RouteWithValidQuote } from '../../entities/route-with-valid-quote'; import { BuildOnChainGasModelFactoryType, + GasModelProviderConfig, + getQuoteThroughNativePool, IGasModel, IOnChainGasModelFactory, } from '../gas-model'; @@ -134,7 +135,7 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { 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: Pool | null = pools.nativeQuoteTokenV3Pool; + const nativePool: Pool | null = pools.nativeAndQuoteTokenV3Pool; if (!nativePool) { log.info( 'Could not find a pool to convert the cost into the quote token' @@ -157,61 +158,15 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { }; }; - // 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 = WRAPPED_NATIVE_CURRENCY[chainId]!; - if (quoteToken.equals(nativeCurrency)) { - const estimateGasCost = ( - routeWithValidQuote: V3RouteWithValidQuote - ): { - gasEstimate: BigNumber; - gasCostInToken: CurrencyAmount; - gasCostInUSD: CurrencyAmount; - } => { - const { totalGasCostNativeCurrency, baseGasUse } = this.estimateGas( - routeWithValidQuote, - gasPriceWei, - chainId, - providerConfig - ); - - const token0 = usdPool.token0.address == nativeCurrency.address; - - const nativeTokenPrice = token0 - ? usdPool.token0Price - : usdPool.token1Price; - - const gasCostInTermsOfUSD: CurrencyAmount = nativeTokenPrice.quote( - totalGasCostNativeCurrency - ) as CurrencyAmount; - - 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 / pool. eg. /ETH pool. - const nativePool: Pool | null = pools.nativeQuoteTokenV3Pool; - let nativeAmountPool: Pool | null = null; if (!amountToken.equals(nativeCurrency)) { - nativeAmountPool = pools.nativeAmountTokenV3Pool; + nativeAmountPool = pools.nativeAndAmountTokenV3Pool; } - const usdToken = - usdPool.token0.address == nativeCurrency.address - ? usdPool.token1 - : usdPool.token0; + const usdToken = usdPool.token0.equals(nativeCurrency) + ? usdPool.token1 + : usdPool.token0; const estimateGasCost = ( routeWithValidQuote: V3RouteWithValidQuote @@ -219,6 +174,7 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { gasEstimate: BigNumber; gasCostInToken: CurrencyAmount; gasCostInUSD: CurrencyAmount; + gasCostInGasToken?: CurrencyAmount; } => { const { totalGasCostNativeCurrency, baseGasUse } = this.estimateGas( routeWithValidQuote, @@ -227,39 +183,65 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { providerConfig ); + /** ------ MARK: USD logic -------- */ + const gasCostInTermsOfUSD = getQuoteThroughNativePool( + chainId, + totalGasCostNativeCurrency, + usdPool + ); + + /** ------ MARK: Conditional logic run if gasToken is specified -------- */ + const nativeAndSpecifiedGasTokenPool: Pool | null = + pools.nativeAndSpecifiedGasTokenV3Pool; + let gasCostInTermsOfGasToken: CurrencyAmount | undefined = undefined; + // we don't want to fetch the gasToken pool if the gasToken is the native currency + if (nativeAndSpecifiedGasTokenPool) { + gasCostInTermsOfGasToken = getQuoteThroughNativePool( + chainId, + totalGasCostNativeCurrency, + nativeAndSpecifiedGasTokenPool + ); + } + // if the gasToken is the native currency, we can just use the totalGasCostNativeCurrency + else if (providerConfig?.gasToken?.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 / pool. eg. /ETH pool. + const nativeAndQuoteTokenPool: Pool | null = + pools.nativeAndQuoteTokenV3Pool; + let gasCostInTermsOfQuoteToken: CurrencyAmount | null = 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 - ) as CurrencyAmount; - } catch (err) { - log.info( - { - nativeTokenPriceBase: nativeTokenPrice.baseCurrency, - nativeTokenPriceQuote: nativeTokenPrice.quoteCurrency, - gasCostInEth: totalGasCostNativeCurrency.currency, - }, - 'Debug eth price token issue' - ); - throw err; - } + if (nativeAndQuoteTokenPool) { + gasCostInTermsOfQuoteToken = getQuoteThroughNativePool( + chainId, + totalGasCostNativeCurrency, + nativeAndQuoteTokenPool + ); } - // we have a nativeAmountPool, but not a nativePool + // 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 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) { @@ -274,11 +256,11 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { const inputIsToken0 = nativeAmountPool.token0.address == nativeCurrency.address; // ratio of input / native - const nativeAmountTokenPrice = inputIsToken0 + const nativeAndAmountTokenPrice = inputIsToken0 ? nativeAmountPool.token0Price : nativeAmountPool.token1Price; - const gasCostInTermsOfAmountToken = nativeAmountTokenPrice.quote( + const gasCostInTermsOfAmountToken = nativeAndAmountTokenPrice.quote( totalGasCostNativeCurrency ) as CurrencyAmount; @@ -298,7 +280,8 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { ) { log.info( { - nativeAmountTokenPrice: nativeAmountTokenPrice.toSignificant(6), + nativeAndAmountTokenPrice: + nativeAndAmountTokenPrice.toSignificant(6), gasCostInTermsOfQuoteToken: gasCostInTermsOfQuoteToken ? gasCostInTermsOfQuoteToken.toExact() : 0, @@ -315,31 +298,6 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { } } - // 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: CurrencyAmount; - try { - gasCostInTermsOfUSD = nativeTokenPriceUSDPool.quote( - totalGasCostNativeCurrency - ) as CurrencyAmount; - } 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( @@ -356,6 +314,7 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { gasEstimate: baseGasUse, gasCostInToken: gasCostInTermsOfQuoteToken, gasCostInUSD: gasCostInTermsOfUSD!, + gasCostInGasToken: gasCostInTermsOfGasToken, }; }; @@ -369,7 +328,7 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { routeWithValidQuote: V3RouteWithValidQuote, gasPriceWei: BigNumber, chainId: ChainId, - providerConfig?: ProviderConfig + providerConfig?: GasModelProviderConfig ) { const totalInitializedTicksCrossed = BigNumber.from( Math.max(1, _.sum(routeWithValidQuote.initializedTicksCrossedList)) diff --git a/src/routers/alpha-router/quoters/v2-quoter.ts b/src/routers/alpha-router/quoters/v2-quoter.ts index 0e281d496..f3fb438fd 100644 --- a/src/routers/alpha-router/quoters/v2-quoter.ts +++ b/src/routers/alpha-router/quoters/v2-quoter.ts @@ -152,6 +152,11 @@ export class V2Quoter extends BaseQuoter { } // safe to force unwrap here because we throw if there are no amounts const amountToken = amounts[0]!.currency; + const gasToken = _routingConfig.gasToken + ? ( + await this.tokenProvider.getTokens([_routingConfig.gasToken]) + ).getTokenByAddress(_routingConfig.gasToken) + : undefined; if (routes.length == 0) { return { routesWithValidQuotes: [], candidatePools }; @@ -182,6 +187,7 @@ export class V2Quoter extends BaseQuoter { amountToken, quoteToken ), + gasToken, }, }); diff --git a/src/routers/router.ts b/src/routers/router.ts index 1ef31f94a..0efeae4b4 100644 --- a/src/routers/router.ts +++ b/src/routers/router.ts @@ -71,6 +71,11 @@ export type SwapRoute = { */ estimatedGasUsedUSD: CurrencyAmount; /** + * The estimate of the gas used by the swap in terms of the gas token if specified. + * will be undefined if no gas token is specified in the AlphaRouter config + */ + estimatedGasUsedGasToken?: CurrencyAmount; + /* * The gas price used when computing quoteGasAdjusted, estimatedGasUsedQuoteToken, etc. */ gasPriceWei: BigNumber; diff --git a/src/util/gas-factory-helpers.ts b/src/util/gas-factory-helpers.ts index a0638ba91..1c6ddc6b0 100644 --- a/src/util/gas-factory-helpers.ts +++ b/src/util/gas-factory-helpers.ts @@ -1,12 +1,6 @@ import { BigNumber } from '@ethersproject/bignumber'; import { Protocol } from '@uniswap/router-sdk'; -import { - ChainId, - Currency, - CurrencyAmount, - Token, - TradeType, -} from '@uniswap/sdk-core'; +import { ChainId, Token, TradeType } from '@uniswap/sdk-core'; import { Pair } from '@uniswap/v2-sdk/dist/entities'; import { FeeAmount, Pool } from '@uniswap/v3-sdk'; import JSBI from 'jsbi'; @@ -14,13 +8,14 @@ import _ from 'lodash'; import { IV2PoolProvider } from '../providers'; import { IPortionProvider } from '../providers/portion-provider'; -import { ProviderConfig } from '../providers/provider'; import { ArbitrumGasData, OptimismGasData, } from '../providers/v3/gas-data-provider'; import { IV3PoolProvider } from '../providers/v3/pool-provider'; import { + GasModelProviderConfig, + getQuoteThroughNativePool, MethodParameters, MixedRouteWithValidQuote, SwapOptions, @@ -29,14 +24,14 @@ import { V2RouteWithValidQuote, V3RouteWithValidQuote, } from '../routers'; -import { log, WRAPPED_NATIVE_CURRENCY } from '../util'; +import { CurrencyAmount, log, WRAPPED_NATIVE_CURRENCY } from '../util'; import { buildTrade } from './methodParameters'; export async function getV2NativePool( token: Token, poolProvider: IV2PoolProvider, - providerConfig?: ProviderConfig + providerConfig?: GasModelProviderConfig ): Promise { const chainId = token.chainId as ChainId; const weth = WRAPPED_NATIVE_CURRENCY[chainId]!; @@ -67,7 +62,7 @@ export async function getV2NativePool( export async function getHighestLiquidityV3NativePool( token: Token, poolProvider: IV3PoolProvider, - providerConfig?: ProviderConfig + providerConfig?: GasModelProviderConfig ): Promise { const nativeCurrency = WRAPPED_NATIVE_CURRENCY[token.chainId as ChainId]!; @@ -115,7 +110,7 @@ export async function getHighestLiquidityV3NativePool( export async function getHighestLiquidityV3USDPool( chainId: ChainId, poolProvider: IV3PoolProvider, - providerConfig?: ProviderConfig + providerConfig?: GasModelProviderConfig ): Promise { const usdTokens = usdGasTokensByChain[chainId]; const wrappedCurrency = WRAPPED_NATIVE_CURRENCY[chainId]!; @@ -177,21 +172,6 @@ export async function getHighestLiquidityV3USDPool( return maxPool; } -export function getGasCostInUSD( - usdPool: Pool, - costNativeCurrency: CurrencyAmount -) { - const nativeCurrency = costNativeCurrency.currency; - // convert fee into usd - const nativeTokenPrice = - usdPool.token0.address == nativeCurrency.address - ? usdPool.token0Price - : usdPool.token1Price; - - const gasCostUSD = nativeTokenPrice.quote(costNativeCurrency); - return gasCostUSD; -} - export function getGasCostInNativeCurrency( nativeCurrency: Token, gasCostInWei: BigNumber @@ -204,19 +184,6 @@ export function getGasCostInNativeCurrency( return costNativeCurrency; } -export async function getGasCostInQuoteToken( - quoteToken: Token, - nativePool: Pool | Pair, - costNativeCurrency: CurrencyAmount -) { - const nativeTokenPrice = - nativePool.token0.address == quoteToken.address - ? nativePool.token1Price - : nativePool.token0Price; - const gasCostQuoteToken = nativeTokenPrice.quote(costNativeCurrency); - return gasCostQuoteToken; -} - export function calculateArbitrumToL1FeeFromCalldata( calldata: string, gasData: ArbitrumGasData @@ -272,8 +239,13 @@ export async function calculateGasUsed( v2PoolProvider: IV2PoolProvider, v3PoolProvider: IV3PoolProvider, l2GasData?: ArbitrumGasData | OptimismGasData, - providerConfig?: ProviderConfig -) { + providerConfig?: GasModelProviderConfig +): Promise<{ + estimatedGasUsedUSD: CurrencyAmount; + estimatedGasUsedQuoteToken: CurrencyAmount; + estimatedGasUsedGasToken?: CurrencyAmount; + quoteGasAdjusted: CurrencyAmount; +}> { const quoteToken = route.quote.currency.wrapped; const gasPriceWei = route.gasPriceWei; // calculate L2 to L1 security fee if relevant @@ -311,11 +283,47 @@ export async function calculateGasUsed( providerConfig ); - const gasCostUSD = await getGasCostInUSD(usdPool, costNativeCurrency); + /** ------ MARK: USD logic -------- */ + const gasCostUSD = getQuoteThroughNativePool( + chainId, + costNativeCurrency, + usdPool + ); + + /** ------ MARK: Conditional logic run if gasToken is specified -------- */ + let gasCostInTermsOfGasToken: CurrencyAmount | undefined = undefined; + if (providerConfig?.gasToken) { + if (providerConfig.gasToken.equals(nativeCurrency)) { + gasCostInTermsOfGasToken = costNativeCurrency; + } else { + const nativeAndSpecifiedGasTokenPool = + await getHighestLiquidityV3NativePool( + providerConfig.gasToken, + v3PoolProvider, + providerConfig + ); + if (nativeAndSpecifiedGasTokenPool) { + gasCostInTermsOfGasToken = getQuoteThroughNativePool( + chainId, + costNativeCurrency, + nativeAndSpecifiedGasTokenPool + ); + } else { + log.info( + `Could not find a V3 pool for gas token ${providerConfig.gasToken.symbol}` + ); + } + } + } - let gasCostQuoteToken = costNativeCurrency; + /** ------ MARK: Main gas logic in terms of quote token -------- */ + let gasCostQuoteToken: CurrencyAmount | undefined = undefined; + // shortcut if quote token is native currency + if (quoteToken.equals(nativeCurrency)) { + gasCostQuoteToken = costNativeCurrency; + } // get fee in terms of quote token - if (!quoteToken.equals(nativeCurrency)) { + else { const nativePools = await Promise.all([ getHighestLiquidityV3NativePool( quoteToken, @@ -332,10 +340,10 @@ export async function calculateGasUsed( ); gasCostQuoteToken = CurrencyAmount.fromRawAmount(quoteToken, 0); } else { - gasCostQuoteToken = await getGasCostInQuoteToken( - quoteToken, - nativePool, - costNativeCurrency + gasCostQuoteToken = getQuoteThroughNativePool( + chainId, + costNativeCurrency, + nativePool ); } } @@ -353,6 +361,7 @@ export async function calculateGasUsed( return { estimatedGasUsedUSD: gasCostUSD, estimatedGasUsedQuoteToken: gasCostQuoteToken, + estimatedGasUsedGasToken: gasCostInTermsOfGasToken, quoteGasAdjusted: quoteGasAdjusted, }; } @@ -362,11 +371,12 @@ export function initSwapRouteFromExisting( v2PoolProvider: IV2PoolProvider, v3PoolProvider: IV3PoolProvider, portionProvider: IPortionProvider, - quoteGasAdjusted: CurrencyAmount, + quoteGasAdjusted: CurrencyAmount, estimatedGasUsed: BigNumber, - estimatedGasUsedQuoteToken: CurrencyAmount, - estimatedGasUsedUSD: CurrencyAmount, - swapOptions: SwapOptions + estimatedGasUsedQuoteToken: CurrencyAmount, + estimatedGasUsedUSD: CurrencyAmount, + swapOptions: SwapOptions, + estimatedGasUsedGasToken?: CurrencyAmount ): SwapRoute { const currencyIn = swapRoute.trade.inputAmount.currency; const currencyOut = swapRoute.trade.outputAmount.currency; @@ -478,6 +488,7 @@ export function initSwapRouteFromExisting( quoteGasAndPortionAdjusted, estimatedGasUsed, estimatedGasUsedQuoteToken, + estimatedGasUsedGasToken, estimatedGasUsedUSD, gasPriceWei: BigNumber.from(swapRoute.gasPriceWei), trade, diff --git a/test/integ/routers/alpha-router/alpha-router.integration.test.ts b/test/integ/routers/alpha-router/alpha-router.integration.test.ts index c5b613028..8572d10ad 100644 --- a/test/integ/routers/alpha-router/alpha-router.integration.test.ts +++ b/test/integ/routers/alpha-router/alpha-router.integration.test.ts @@ -78,7 +78,8 @@ import { WBTC_GNOSIS, WBTC_MOONBEAM, WETH9, - WNATIVE_ON + WNATIVE_ON, + WRAPPED_NATIVE_CURRENCY } from '../../../../src'; import { PortionProvider } from '../../../../src/providers/portion-provider'; import { OnChainTokenFeeFetcher } from '../../../../src/providers/token-fee-fetcher'; @@ -1546,6 +1547,99 @@ describe('alpha router integration', () => { 100 ); }); + + it('erc20 -> erc20 gas token specified', async () => { + // declaring these to reduce confusion + const tokenIn = USDC_MAINNET; + const tokenOut = USDT_MAINNET; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('100', tokenIn) + : parseAmount('100', tokenOut); + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + { + type: SwapType.UNIVERSAL_ROUTER, + recipient: alice._address, + slippageTolerance: SLIPPAGE, + deadlineOrPreviousBlockhash: parseDeadline(360), + }, + { + ...ROUTING_CONFIG, + gasToken: DAI_MAINNET.address + } + ); + + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + + const { quote, quoteGasAdjusted, methodParameters, estimatedGasUsedGasToken } = swap!; + + expect(estimatedGasUsedGasToken).toBeDefined(); + expect(estimatedGasUsedGasToken?.currency.equals(DAI_MAINNET)).toBe(true); + + await validateSwapRoute(quote, quoteGasAdjusted, tradeType, 100, 10); + + await validateExecuteSwap( + SwapType.UNIVERSAL_ROUTER, + quote, + tokenIn, + tokenOut, + methodParameters, + tradeType, + 100, + 100 + ); + }); + + it('erc20 -> eth gas token as weth', async () => { + // declaring these to reduce confusion + const tokenIn = USDC_MAINNET; + const tokenOut = Ether.onChain(1) as Currency; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('1000000', tokenIn) + : parseAmount('10', tokenOut); + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + { + type: SwapType.UNIVERSAL_ROUTER, + recipient: alice._address, + slippageTolerance: SLIPPAGE, + deadlineOrPreviousBlockhash: parseDeadline(360), + }, + { + ...ROUTING_CONFIG, + gasToken: WRAPPED_NATIVE_CURRENCY[1]!.address + } + ); + + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + + const { quote, quoteGasAdjusted, methodParameters, estimatedGasUsedGasToken } = swap!; + + expect(estimatedGasUsedGasToken).toBeDefined(); + expect(estimatedGasUsedGasToken?.currency.equals(WRAPPED_NATIVE_CURRENCY[1]!)).toBe(true); + + await validateSwapRoute(quote, quoteGasAdjusted, tradeType); + + await validateExecuteSwap( + SwapType.UNIVERSAL_ROUTER, + quote, + tokenIn, + tokenOut, + methodParameters, + tradeType, + 1000000 + ); + }); }); if (isTenderlyEnvironmentSet()) { @@ -2414,6 +2508,105 @@ describe('alpha router integration', () => { expect(simulationStatus).toEqual(SimulationStatus.Succeeded); }); + it('erc20 -> erc20 gas token specified', async () => { + // declaring these to reduce confusion + const tokenIn = USDC_MAINNET; + const tokenOut = USDT_MAINNET; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('100', tokenIn) + : parseAmount('100', tokenOut); + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + { + type: SwapType.UNIVERSAL_ROUTER, + recipient: alice._address, + slippageTolerance: SLIPPAGE, + deadlineOrPreviousBlockhash: parseDeadline(360), + simulate: { fromAddress: WHALES(tokenIn) }, + }, + { + ...ROUTING_CONFIG, + gasToken: DAI_MAINNET.address + } + ); + + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + + const { quote, quoteGasAdjusted, methodParameters, estimatedGasUsedGasToken, simulationStatus } = swap!; + + expect(simulationStatus).toBeDefined(); + expect(simulationStatus).toEqual(SimulationStatus.Succeeded); + expect(estimatedGasUsedGasToken).toBeDefined(); + expect(estimatedGasUsedGasToken?.currency.equals(DAI_MAINNET)).toBe(true); + + await validateSwapRoute(quote, quoteGasAdjusted, tradeType, 100, 10); + + await validateExecuteSwap( + SwapType.UNIVERSAL_ROUTER, + quote, + tokenIn, + tokenOut, + methodParameters, + tradeType, + 100, + 100 + ); + }); + + it('erc20 -> eth gas token as weth', async () => { + // declaring these to reduce confusion + const tokenIn = USDC_MAINNET; + const tokenOut = Ether.onChain(1) as Currency; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('1000000', tokenIn) + : parseAmount('10', tokenOut); + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + { + type: SwapType.UNIVERSAL_ROUTER, + recipient: alice._address, + slippageTolerance: SLIPPAGE, + deadlineOrPreviousBlockhash: parseDeadline(360), + simulate: { fromAddress: WHALES(tokenIn) }, + }, + { + ...ROUTING_CONFIG, + gasToken: WRAPPED_NATIVE_CURRENCY[1]!.address + } + ); + + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + + const { quote, quoteGasAdjusted, methodParameters, estimatedGasUsedGasToken, simulationStatus } = swap!; + + expect(simulationStatus).toBeDefined(); + expect(simulationStatus).toEqual(SimulationStatus.Succeeded); + expect(estimatedGasUsedGasToken).toBeDefined(); + expect(estimatedGasUsedGasToken?.currency.equals(WRAPPED_NATIVE_CURRENCY[1]!)).toBe(true); + + await validateSwapRoute(quote, quoteGasAdjusted, tradeType); + + await validateExecuteSwap( + SwapType.UNIVERSAL_ROUTER, + quote, + tokenIn, + tokenOut, + methodParameters, + tradeType, + 1000000 + ); + }); + GREENLIST_TOKEN_PAIRS.forEach(([tokenIn, tokenOut]) => { it(`${tokenIn.symbol} -> ${tokenOut.symbol} with portion`, async () => { const originalAmount = (tokenIn.symbol === 'WBTC' && tradeType === TradeType.EXACT_INPUT) || diff --git a/test/test-util/mock-data.ts b/test/test-util/mock-data.ts index 578e6c6e3..712776035 100644 --- a/test/test-util/mock-data.ts +++ b/test/test-util/mock-data.ts @@ -9,6 +9,7 @@ import { CurrencyAmount, DAI_MAINNET as DAI, TokenAccessor, + UNI_MAINNET, USDC_MAINNET as USDC, USDT_MAINNET as USDT, V2SubgraphPool, @@ -162,6 +163,14 @@ export const DAI_USDT_MEDIUM = new Pool( 10, 0 ); +export const DAI_WETH_MEDIUM = new Pool( + DAI, + WRAPPED_NATIVE_CURRENCY[1]!, + FeeAmount.MEDIUM, + encodeSqrtRatioX96(1, 1), + 10, + 0 +); export const WBTC_USDT_MEDIUM = new Pool( USDT, WBTC, @@ -178,6 +187,14 @@ export const WBTC_WETH_MEDIUM = new Pool( 500, 0 ); +export const UNI_WETH_MEDIUM = new Pool( + WRAPPED_NATIVE_CURRENCY[1]!, + UNI_MAINNET, + FeeAmount.MEDIUM, + encodeSqrtRatioX96(1, 1), + 500, + 0 +); // Mock V2 Pools export const DAI_USDT = new Pair( @@ -185,6 +202,11 @@ export const DAI_USDT = new Pair( CurrencyAmount.fromRawAmount(USDT, 10000000000) ); +export const DAI_WETH = new Pair( + CurrencyAmount.fromRawAmount(DAI, 10000000000), + CurrencyAmount.fromRawAmount(WRAPPED_NATIVE_CURRENCY[1]!, 10000000000) +); + export const USDC_WETH = new Pair( CurrencyAmount.fromRawAmount(USDC, 10000000000), CurrencyAmount.fromRawAmount(WRAPPED_NATIVE_CURRENCY[1]!, 10000000000) diff --git a/test/unit/routers/alpha-router/gas-models/mixed-route-gas-model.test.ts b/test/unit/routers/alpha-router/gas-models/mixed-route-gas-model.test.ts index f61c14712..767ea0a07 100644 --- a/test/unit/routers/alpha-router/gas-models/mixed-route-gas-model.test.ts +++ b/test/unit/routers/alpha-router/gas-models/mixed-route-gas-model.test.ts @@ -1,19 +1,16 @@ import { partitionMixedRouteByProtocol } from '@uniswap/router-sdk'; -import { Currency, CurrencyAmount, Ether, Token } from '@uniswap/sdk-core'; +import { Currency, CurrencyAmount, Ether } from '@uniswap/sdk-core'; import { Pair } from '@uniswap/v2-sdk'; import { Pool } from '@uniswap/v3-sdk'; import { BigNumber } from 'ethers'; import _ from 'lodash'; import { DAI_MAINNET, - LiquidityCalculationPools, MixedRoute, MixedRouteWithValidQuote, USDC_MAINNET, - V3PoolProvider, WRAPPED_NATIVE_CURRENCY, } from '../../../../../src'; -import { ProviderConfig } from '../../../../../src/providers/provider'; import { MixedRouteHeuristicGasModelFactory } from '../../../../../src/routers/alpha-router/gas-models/mixedRoute/mixed-route-heuristic-gas-model'; import { BASE_SWAP_COST as BASE_SWAP_COST_V2, @@ -28,10 +25,6 @@ import { NATIVE_UNWRAP_OVERHEAD, NATIVE_WRAP_OVERHEAD, } from '../../../../../src/routers/alpha-router/gas-models/v3/gas-costs'; -import { - getHighestLiquidityV3NativePool, - getHighestLiquidityV3USDPool, -} from '../../../../../src/util/gas-factory-helpers'; import { USDC_DAI, USDC_DAI_MEDIUM, @@ -43,6 +36,7 @@ import { getMockedV2PoolProvider, getMockedV3PoolProvider, } from './test-util/mocked-dependencies'; +import { getPools } from './test-util/helpers'; describe('mixed route gas model tests', () => { const gasPriceWei = BigNumber.from(1000000000); @@ -52,49 +46,6 @@ describe('mixed route gas model tests', () => { const mockedV3PoolProvider = getMockedV3PoolProvider(); const mockedV2PoolProvider = getMockedV2PoolProvider(); - // helper function to get pools for building gas model - async function getPools( - amountToken: Token, - quoteToken: Token, - v3PoolProvider: V3PoolProvider, - providerConfig: ProviderConfig - ): Promise { - const usdPoolPromise = getHighestLiquidityV3USDPool( - chainId, - v3PoolProvider, - providerConfig - ); - const nativeCurrency = WRAPPED_NATIVE_CURRENCY[chainId]; - const nativeQuoteTokenV3PoolPromise = !quoteToken.equals(nativeCurrency) - ? getHighestLiquidityV3NativePool( - quoteToken, - v3PoolProvider, - providerConfig - ) - : Promise.resolve(null); - const nativeAmountTokenV3PoolPromise = !amountToken.equals(nativeCurrency) - ? getHighestLiquidityV3NativePool( - amountToken, - v3PoolProvider, - providerConfig - ) - : Promise.resolve(null); - - const [usdPool, nativeQuoteTokenV3Pool, nativeAmountTokenV3Pool] = - await Promise.all([ - usdPoolPromise, - nativeQuoteTokenV3PoolPromise, - nativeAmountTokenV3PoolPromise, - ]); - - const pools: LiquidityCalculationPools = { - usdPool: usdPool, - nativeQuoteTokenV3Pool: nativeQuoteTokenV3Pool, - nativeAmountTokenV3Pool: nativeAmountTokenV3Pool, - }; - return pools; - } - function calculateGasEstimate(routeWithValidQuote: MixedRouteWithValidQuote) { // copied from mixed route heuristic gas model let baseGasUse = BigNumber.from(0); diff --git a/test/unit/routers/alpha-router/gas-models/test-util/helpers.ts b/test/unit/routers/alpha-router/gas-models/test-util/helpers.ts new file mode 100644 index 000000000..c1ff30590 --- /dev/null +++ b/test/unit/routers/alpha-router/gas-models/test-util/helpers.ts @@ -0,0 +1,63 @@ +import _ from 'lodash'; +import { + GasModelProviderConfig, + LiquidityCalculationPools, + V3PoolProvider, + WRAPPED_NATIVE_CURRENCY, +} from '../../../../../../src'; +import { + getHighestLiquidityV3NativePool, + getHighestLiquidityV3USDPool, +} from '../../../../../../src/util/gas-factory-helpers'; +import { ChainId, Token } from '@uniswap/sdk-core'; + +export async function getPools( + amountToken: Token, + quoteToken: Token, + v3PoolProvider: V3PoolProvider, + providerConfig: GasModelProviderConfig, + gasToken?: Token, + chainId: ChainId = 1 + ): Promise { + const usdPoolPromise = getHighestLiquidityV3USDPool( + chainId, + v3PoolProvider, + providerConfig + ); + const nativeCurrency = WRAPPED_NATIVE_CURRENCY[chainId]; + const nativeAndQuoteTokenV3PoolPromise = !quoteToken.equals(nativeCurrency) + ? getHighestLiquidityV3NativePool( + quoteToken, + v3PoolProvider, + providerConfig + ) + : Promise.resolve(null); + const nativeAndAmountTokenV3PoolPromise = !amountToken.equals(nativeCurrency) + ? getHighestLiquidityV3NativePool( + amountToken, + v3PoolProvider, + providerConfig + ) + : Promise.resolve(null); + const nativeAndSpecifiedGasTokenV3PoolPromise = gasToken ? getHighestLiquidityV3NativePool( + gasToken, + v3PoolProvider, + providerConfig + ) : Promise.resolve(null); + + const [usdPool, nativeAndQuoteTokenV3Pool, nativeAndAmountTokenV3Pool, nativeAndSpecifiedGasTokenV3Pool] = + await Promise.all([ + usdPoolPromise, + nativeAndQuoteTokenV3PoolPromise, + nativeAndAmountTokenV3PoolPromise, + nativeAndSpecifiedGasTokenV3PoolPromise + ]); + + const pools: LiquidityCalculationPools = { + usdPool: usdPool, + nativeAndQuoteTokenV3Pool: nativeAndQuoteTokenV3Pool, + nativeAndAmountTokenV3Pool: nativeAndAmountTokenV3Pool, + nativeAndSpecifiedGasTokenV3Pool: nativeAndSpecifiedGasTokenV3Pool + }; + return pools; + } \ No newline at end of file diff --git a/test/unit/routers/alpha-router/gas-models/test-util/mocked-dependencies.ts b/test/unit/routers/alpha-router/gas-models/test-util/mocked-dependencies.ts index fb2f130da..fcd26a48b 100644 --- a/test/unit/routers/alpha-router/gas-models/test-util/mocked-dependencies.ts +++ b/test/unit/routers/alpha-router/gas-models/test-util/mocked-dependencies.ts @@ -17,6 +17,9 @@ import { buildMockV3PoolAccessor, DAI_USDT, DAI_USDT_LOW, + DAI_WETH, + DAI_WETH_MEDIUM, + UNI_WETH_MEDIUM, USDC_DAI, USDC_DAI_LOW, USDC_DAI_MEDIUM, @@ -70,6 +73,8 @@ export function getMockedV3PoolProvider(): V3PoolProvider { WETH9_USDT_LOW, DAI_USDT_LOW, USDC_USDT_MEDIUM, + UNI_WETH_MEDIUM, + DAI_WETH_MEDIUM ]; mockV3PoolProvider.getPools.resolves(buildMockV3PoolAccessor(v3MockPools)); @@ -100,7 +105,7 @@ export function getMockedV2GasModel(): IGasModel { export function getMockedV2PoolProvider(): V2PoolProvider { const mockV2PoolProvider = sinon.createStubInstance(V2PoolProvider); - const v2MockPools = [DAI_USDT, USDC_WETH, WETH_USDT, USDC_DAI, WBTC_WETH]; + const v2MockPools: Pair[] = [DAI_USDT, USDC_WETH, WETH_USDT, USDC_DAI, WBTC_WETH, DAI_WETH]; mockV2PoolProvider.getPools.resolves(buildMockV2PoolAccessor(v2MockPools)); mockV2PoolProvider.getPoolAddress.callsFake((tA, tB) => ({ poolAddress: Pair.getAddress(tA, tB), diff --git a/test/unit/routers/alpha-router/gas-models/v2-gas-model.test.ts b/test/unit/routers/alpha-router/gas-models/v2-gas-model.test.ts index 68839fb9e..870de9514 100644 --- a/test/unit/routers/alpha-router/gas-models/v2-gas-model.test.ts +++ b/test/unit/routers/alpha-router/gas-models/v2-gas-model.test.ts @@ -1,6 +1,6 @@ import { Currency, Ether } from '@uniswap/sdk-core'; import { BigNumber } from 'ethers'; -import { DAI_MAINNET, V2Route } from '../../../../../src'; +import { DAI_MAINNET, USDC_MAINNET, V2Route } from '../../../../../src'; import { BASE_SWAP_COST, COST_PER_EXTRA_HOP, @@ -36,12 +36,14 @@ describe('v2 gas model tests', () => { gasModel: v2GasModel, }); - const { gasEstimate } = v2GasModel.estimateGasCost(v2RouteWithQuote); + const { gasEstimate, gasCostInToken, gasCostInUSD } = v2GasModel.estimateGasCost(v2RouteWithQuote); const hops = v2RouteWithQuote.route.pairs.length; let expectedGasCost = BASE_SWAP_COST.add(COST_PER_EXTRA_HOP.mul(hops - 1)); expect(gasEstimate.toNumber()).toEqual(expectedGasCost.toNumber()); + expect(gasCostInToken).toBeDefined(); + expect(gasCostInUSD).toBeDefined(); }); it('applies overhead when token in is native eth', async () => { @@ -73,7 +75,7 @@ describe('v2 gas model tests', () => { gasModel: v2GasModel, }); - const { gasEstimate } = v2GasModel.estimateGasCost(v2RouteWithQuote); + const { gasEstimate, gasCostInToken, gasCostInUSD } = v2GasModel.estimateGasCost(v2RouteWithQuote); const hops = v2RouteWithQuote.route.pairs.length; let expectedGasCost = BASE_SWAP_COST.add( @@ -81,6 +83,39 @@ describe('v2 gas model tests', () => { ).add(NATIVE_WRAP_OVERHEAD(chainId)); expect(gasEstimate.toNumber()).toEqual(expectedGasCost.toNumber()); + expect(gasCostInToken).toBeDefined(); + expect(gasCostInUSD).toBeDefined(); + }); + + it('returns gas estimate for specified gasToken', async () => { + // copied from 'returns correct gas estimate for a v2 route | hops: 1' + const quoteToken = DAI_MAINNET; + const gasToken = USDC_MAINNET + + const v2GasModel = await v2GasModelFactory.buildGasModel({ + chainId: chainId, + gasPriceWei, + poolProvider: mockedV2PoolProvider, + token: quoteToken, + providerConfig: { + gasToken: gasToken + }, + }); + + const v2RouteWithQuote = getV2RouteWithValidQuoteStub({ + gasModel: v2GasModel, + }); + + const { gasEstimate, gasCostInToken, gasCostInUSD, gasCostInGasToken } = v2GasModel.estimateGasCost(v2RouteWithQuote); + + const hops = v2RouteWithQuote.route.pairs.length; + let expectedGasCost = BASE_SWAP_COST.add(COST_PER_EXTRA_HOP.mul(hops - 1)); + + expect(gasEstimate.toNumber()).toEqual(expectedGasCost.toNumber()); + expect(gasCostInToken).toBeDefined(); + expect(gasCostInUSD).toBeDefined(); + expect(gasCostInGasToken).toBeDefined(); + expect(gasCostInGasToken?.currency.equals(gasToken)).toBe(true); }); // TODO: splits, multiple hops, token overheads, gasCostInToken, gasCostInUSD diff --git a/test/unit/routers/alpha-router/gas-models/v3-gas-model.test.ts b/test/unit/routers/alpha-router/gas-models/v3-gas-model.test.ts index 8f2a378c4..1b1efce15 100644 --- a/test/unit/routers/alpha-router/gas-models/v3-gas-model.test.ts +++ b/test/unit/routers/alpha-router/gas-models/v3-gas-model.test.ts @@ -1,16 +1,14 @@ -import { Currency, CurrencyAmount, Ether, Token } from '@uniswap/sdk-core'; +import { Currency, CurrencyAmount, Ether } from '@uniswap/sdk-core'; import { BigNumber } from 'ethers'; import _ from 'lodash'; import { DAI_MAINNET, - LiquidityCalculationPools, + UNI_MAINNET, USDC_MAINNET, V3HeuristicGasModelFactory, - V3PoolProvider, V3Route, WRAPPED_NATIVE_CURRENCY, } from '../../../../../src'; -import { ProviderConfig } from '../../../../../src/providers/provider'; import { BASE_SWAP_COST, COST_PER_HOP, @@ -20,12 +18,10 @@ import { NATIVE_WRAP_OVERHEAD, SINGLE_HOP_OVERHEAD, } from '../../../../../src/routers/alpha-router/gas-models/v3/gas-costs'; -import { - getHighestLiquidityV3NativePool, - getHighestLiquidityV3USDPool, -} from '../../../../../src/util/gas-factory-helpers'; import { DAI_USDT_LOW, + DAI_WETH_MEDIUM, + UNI_WETH_MEDIUM, USDC_USDT_MEDIUM, USDC_WETH_MEDIUM, } from '../../../../test-util/mock-data'; @@ -34,6 +30,7 @@ import { getMockedV2PoolProvider, getMockedV3PoolProvider, } from './test-util/mocked-dependencies'; +import { getPools } from './test-util/helpers'; describe('v3 gas model tests', () => { const gasPriceWei = BigNumber.from(1000000000); @@ -43,49 +40,6 @@ describe('v3 gas model tests', () => { const mockedV3PoolProvider = getMockedV3PoolProvider(); const mockedV2PoolProvider = getMockedV2PoolProvider(); - // helper function to get pools for building gas model - async function getPools( - amountToken: Token, - quoteToken: Token, - v3PoolProvider: V3PoolProvider, - providerConfig: ProviderConfig - ): Promise { - const usdPoolPromise = getHighestLiquidityV3USDPool( - chainId, - v3PoolProvider, - providerConfig - ); - const nativeCurrency = WRAPPED_NATIVE_CURRENCY[chainId]; - const nativeQuoteTokenV3PoolPromise = !quoteToken.equals(nativeCurrency) - ? getHighestLiquidityV3NativePool( - quoteToken, - v3PoolProvider, - providerConfig - ) - : Promise.resolve(null); - const nativeAmountTokenV3PoolPromise = !amountToken.equals(nativeCurrency) - ? getHighestLiquidityV3NativePool( - amountToken, - v3PoolProvider, - providerConfig - ) - : Promise.resolve(null); - - const [usdPool, nativeQuoteTokenV3Pool, nativeAmountTokenV3Pool] = - await Promise.all([ - usdPoolPromise, - nativeQuoteTokenV3PoolPromise, - nativeAmountTokenV3PoolPromise, - ]); - - const pools: LiquidityCalculationPools = { - usdPool: usdPool, - nativeQuoteTokenV3Pool: nativeQuoteTokenV3Pool, - nativeAmountTokenV3Pool: nativeAmountTokenV3Pool, - }; - return pools; - } - it('returns correct gas estimate for a v3 route | hops: 1 | ticks 1', async () => { const amountToken = USDC_MAINNET; const quoteToken = DAI_MAINNET; @@ -94,7 +48,7 @@ describe('v3 gas model tests', () => { amountToken, quoteToken, mockedV3PoolProvider, - {} + {}, ); const v3GasModel = await v3GasModelFactory.buildGasModel({ @@ -308,5 +262,119 @@ describe('v3 gas model tests', () => { expect(gasEstimate.toNumber()).toEqual(expectedGasCost.toNumber()); }); + it('returns gas estimate for specified gasToken', async () => { + // copied from `returns correct gas estimate for a v3 route | hops: 1 | ticks 1` test above + + const amountToken = USDC_MAINNET; + const quoteToken = DAI_MAINNET; + const gasToken = UNI_MAINNET + const providerConfig = { + gasToken + } + + const pools = await getPools( + amountToken, + quoteToken, + mockedV3PoolProvider, + providerConfig, + gasToken + ); + + expect(pools.nativeAndSpecifiedGasTokenV3Pool).toStrictEqual(UNI_WETH_MEDIUM); + + const v3GasModel = await v3GasModelFactory.buildGasModel({ + chainId: chainId, + gasPriceWei, + pools, + amountToken, + quoteToken, + v2poolProvider: mockedV2PoolProvider, + l2GasDataProvider: undefined, + providerConfig + }); + + const v3RouteWithQuote = getV3RouteWithValidQuoteStub({ + gasModel: v3GasModel, + initializedTicksCrossedList: [1], + }); + + const totalInitializedTicksCrossed = BigNumber.from( + Math.max(1, _.sum(v3RouteWithQuote.initializedTicksCrossedList)) + ); + + const gasOverheadFromTicks = COST_PER_INIT_TICK(chainId).mul( + totalInitializedTicksCrossed + ); + + const { gasEstimate, gasCostInToken, gasCostInUSD, gasCostInGasToken } = v3GasModel.estimateGasCost(v3RouteWithQuote); + + const expectedGasCost = BASE_SWAP_COST(chainId) + .add(COST_PER_HOP(chainId)) + .add(SINGLE_HOP_OVERHEAD(chainId)) + .add(gasOverheadFromTicks); + + expect(gasEstimate.toNumber()).toEqual(expectedGasCost.toNumber()); + expect(gasCostInToken).toBeDefined(); + expect(gasCostInUSD).toBeDefined(); + expect(gasCostInGasToken).toBeDefined(); + }) + + it('if gasToken == quoteToken returned values are equal', async () => { + // copied from `returns correct gas estimate for a v3 route | hops: 1 | ticks 1` test above + const amountToken = USDC_MAINNET; + const quoteToken = DAI_MAINNET; + const gasToken = DAI_MAINNET // same as quoteToken + const providerConfig = { + gasToken + } + + const pools = await getPools( + amountToken, + quoteToken, + mockedV3PoolProvider, + providerConfig, + gasToken + ); + + expect(pools.nativeAndSpecifiedGasTokenV3Pool).toStrictEqual(DAI_WETH_MEDIUM); + + const v3GasModel = await v3GasModelFactory.buildGasModel({ + chainId: chainId, + gasPriceWei, + pools, + amountToken, + quoteToken, + v2poolProvider: mockedV2PoolProvider, + l2GasDataProvider: undefined, + providerConfig + }); + + const v3RouteWithQuote = getV3RouteWithValidQuoteStub({ + gasModel: v3GasModel, + initializedTicksCrossedList: [1], + }); + + const totalInitializedTicksCrossed = BigNumber.from( + Math.max(1, _.sum(v3RouteWithQuote.initializedTicksCrossedList)) + ); + + const gasOverheadFromTicks = COST_PER_INIT_TICK(chainId).mul( + totalInitializedTicksCrossed + ); + + const { gasEstimate, gasCostInToken, gasCostInUSD, gasCostInGasToken } = v3GasModel.estimateGasCost(v3RouteWithQuote); + + const expectedGasCost = BASE_SWAP_COST(chainId) + .add(COST_PER_HOP(chainId)) + .add(SINGLE_HOP_OVERHEAD(chainId)) + .add(gasOverheadFromTicks); + + expect(gasEstimate.toNumber()).toEqual(expectedGasCost.toNumber()); + expect(gasCostInToken).toBeDefined(); + expect(gasCostInUSD).toBeDefined(); + expect(gasCostInGasToken).toBeDefined(); + expect(gasCostInToken.equalTo(gasCostInGasToken!)).toBeTruthy(); + }) + // TODO: splits, multiple hops, token overheads, gasCostInToken, gasCostInUSD }); diff --git a/test/unit/routers/alpha-router/util/gas-factory-helpers.test.ts b/test/unit/routers/alpha-router/util/gas-factory-helpers.test.ts index 1948991da..34cc6e9ba 100644 --- a/test/unit/routers/alpha-router/util/gas-factory-helpers.test.ts +++ b/test/unit/routers/alpha-router/util/gas-factory-helpers.test.ts @@ -1,20 +1,35 @@ import sinon from 'sinon'; import { + CurrencyAmount, + DAI_MAINNET, + SimulationStatus, + SwapRoute, USDC_MAINNET, + V3HeuristicGasModelFactory, V3PoolProvider, + V3Route, + V3RouteWithValidQuote, WRAPPED_NATIVE_CURRENCY, } from '../../../../../src'; import { + calculateGasUsed, getHighestLiquidityV3NativePool, getHighestLiquidityV3USDPool, } from '../../../../../src/util/gas-factory-helpers'; import { buildMockV3PoolAccessor, + DAI_WETH_MEDIUM, USDC_DAI_LOW, USDC_WETH_HIGH_LIQ_HIGH, USDC_WETH_LOW_LIQ_LOW, USDC_WETH_MED_LIQ_MEDIUM, } from '../../../../test-util/mock-data'; +import { BigNumber } from 'ethers'; +import { getMockedV2PoolProvider, getMockedV3PoolProvider } from '../gas-models/test-util/mocked-dependencies'; +import { TradeType } from '@uniswap/sdk-core'; +import { Trade } from '@uniswap/router-sdk'; +import { Route } from '@uniswap/v3-sdk'; +import { getPools } from '../gas-models/test-util/helpers'; const mockUSDCNativePools = [ USDC_WETH_LOW_LIQ_LOW, @@ -22,13 +37,22 @@ const mockUSDCNativePools = [ USDC_WETH_HIGH_LIQ_HIGH, ]; -describe('getHighestLiquidity pool tests', () => { +const mockGasTokenNativePools = [ + DAI_WETH_MEDIUM +] + +describe('gas factory helpers tests', () => { + const gasPriceWei = BigNumber.from(1000000000); // 1 gwei + const chainId = 1; let mockPoolProvider: sinon.SinonStubbedInstance; beforeEach(() => { mockPoolProvider = sinon.createStubInstance(V3PoolProvider); mockPoolProvider.getPools.resolves( - buildMockV3PoolAccessor(mockUSDCNativePools) + buildMockV3PoolAccessor([ + ...mockUSDCNativePools, + ...mockGasTokenNativePools, + ]) ); }); @@ -78,4 +102,93 @@ describe('getHighestLiquidity pool tests', () => { ); }); }); + + describe('calculateGasUsed', () => { + it('should return correct estimated gas values and quoteGasAdjusted', async () => { + const mockPoolProvider = getMockedV3PoolProvider(); + + const amountToken = WRAPPED_NATIVE_CURRENCY[1]; + const quoteToken = DAI_MAINNET; + const gasToken = USDC_MAINNET; + const providerConfig = { + gasToken + } + + const pools = await getPools( + amountToken, + quoteToken, + mockPoolProvider, + providerConfig, + gasToken + ); + + const v3GasModel = await (new V3HeuristicGasModelFactory()).buildGasModel({ + chainId: chainId, + gasPriceWei, + pools, + amountToken, + quoteToken, + v2poolProvider: getMockedV2PoolProvider(), + l2GasDataProvider: undefined, + providerConfig + }); + + const mockSwapRoute: SwapRoute = { + quote: CurrencyAmount.fromRawAmount(quoteToken, 100), + quoteGasAdjusted: CurrencyAmount.fromRawAmount(quoteToken, 100), + // these are all 0 before the function is called + estimatedGasUsed: BigNumber.from(0), + estimatedGasUsedQuoteToken: CurrencyAmount.fromRawAmount(quoteToken, 0), + estimatedGasUsedUSD: CurrencyAmount.fromRawAmount(quoteToken, 0), + estimatedGasUsedGasToken: undefined, + gasPriceWei, + trade: new Trade({ + v3Routes: [{ + routev3: new Route([DAI_WETH_MEDIUM], amountToken, quoteToken), + inputAmount: CurrencyAmount.fromRawAmount(amountToken, 1), + outputAmount: CurrencyAmount.fromRawAmount(quoteToken, 100), + }], + v2Routes: [], + mixedRoutes: [], + tradeType: TradeType.EXACT_INPUT, + }), + route: [new V3RouteWithValidQuote({ + amount: CurrencyAmount.fromRawAmount(amountToken, 1), + rawQuote: BigNumber.from('100'), + quoteToken, + sqrtPriceX96AfterList: [], + initializedTicksCrossedList: [1], + quoterGasEstimate: BigNumber.from(100000), + percent: 100, + route: new V3Route([DAI_WETH_MEDIUM], amountToken, quoteToken), + tradeType: TradeType.EXACT_INPUT, + v3PoolProvider: mockPoolProvider, + gasModel: v3GasModel, + })], + blockNumber: BigNumber.from(123456), + simulationStatus: SimulationStatus.Succeeded, + methodParameters: { + calldata: '0x0', + value: '0x0', + to: '0x0', + }, + }; + + const simulatedGasUsed = BigNumber.from(100_000); + + const { + estimatedGasUsedQuoteToken, + estimatedGasUsedUSD, + estimatedGasUsedGasToken, + quoteGasAdjusted + } = await calculateGasUsed(chainId, mockSwapRoute, simulatedGasUsed, getMockedV2PoolProvider(), mockPoolProvider, undefined, providerConfig); + + expect(estimatedGasUsedQuoteToken.currency.equals(quoteToken)).toBe(true); + expect(estimatedGasUsedQuoteToken.toExact()).not.toEqual('0'); + expect(estimatedGasUsedUSD.toExact()).not.toEqual('0'); + expect(estimatedGasUsedGasToken?.currency.equals(gasToken)).toBe(true); + expect(estimatedGasUsedGasToken?.toExact()).not.toEqual('0'); + expect(quoteGasAdjusted.lessThan(mockSwapRoute.quote)).toBe(true); + }) + }) });