From fc5f8a1fa1f4b3c19cc7f77c96db1193d5ae2a1a Mon Sep 17 00:00:00 2001 From: "Siyu Jiang (See-You John)" <91580504+jsy1218@users.noreply.github.com> Date: Wed, 28 Aug 2024 19:21:09 -0700 Subject: [PATCH] feat: implement best swap route with v4 routes (#680) - **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) feature - **What is the current behavior?** (You can also link to an open issue here) best swap route does not account for v4 routes - **What is the new behavior (if this is a feature change)?** best swap route is now implemented to account for v4 routes. - **Other information**: I added v4 routing integ-test against alpha router, in [alpha-router.integration.test.ts](https://github.com/Uniswap/smart-order-router/pull/680/files#diff-b1a09e5a7156f19225eba6ca4e2e78d0942580ff1118bab472bf0ea3b1294904). I can see the alpha router can quote 1 OP -> 997404.407521 USDC against pool id 0xa40318dea5fabf21971f683f641b54d6d7d86f5b083cd6f0af9332c5c7a9ec06. Tenderly simulation does not work yet, because universal router doesn't support v4 swap commands yet. --- src/providers/subgraph-provider.ts | 45 ++----- src/providers/token-provider.ts | 16 +++ src/providers/v3/subgraph-provider.ts | 45 +++++++ src/providers/v4/subgraph-provider.ts | 49 ++++++++ src/routers/alpha-router/alpha-router.ts | 41 ++++-- .../alpha-router/functions/best-swap-route.ts | 75 ++++++++++- .../functions/get-candidate-pools.ts | 4 +- .../alpha-router/gas-models/gas-costs.ts | 2 +- .../alpha-router/gas-models/gas-model.ts | 4 +- .../mixed-route-heuristic-gas-model.ts | 12 +- .../tick-based-heuristic-gas-model.ts | 14 +-- src/util/chains.ts | 2 + src/util/methodParameters.ts | 82 ++++++++++-- .../alpha-router.integration.test.ts | 118 +++++++++++++----- .../providers/on-chain-quote-provider.test.ts | 18 +-- 15 files changed, 405 insertions(+), 122 deletions(-) diff --git a/src/providers/subgraph-provider.ts b/src/providers/subgraph-provider.ts index f15d7299a..76d4aebde 100644 --- a/src/providers/subgraph-provider.ts +++ b/src/providers/subgraph-provider.ts @@ -85,28 +85,7 @@ export abstract class SubgraphProvider< : undefined; const query = gql` - query getPools($pageSize: Int!, $id: String) { - pools( - first: $pageSize - ${blockNumber ? `block: { number: ${blockNumber} }` : ``} - where: { id_gt: $id } - ) { - id - token0 { - symbol - id - } - token1 { - symbol - id - } - feeTier - liquidity - totalValueLockedUSD - totalValueLockedETH - totalValueLockedUSDUntracked - } - } + ${this.subgraphQuery(blockNumber)} `; let pools: TRawSubgraphPool[] = []; @@ -244,21 +223,7 @@ export abstract class SubgraphProvider< parseFloat(pool.totalValueLockedETH) > this.trackedEthThreshold ) .map((pool) => { - const { totalValueLockedETH, totalValueLockedUSD } = pool; - - return { - id: pool.id.toLowerCase(), - feeTier: pool.feeTier, - token0: { - id: pool.token0.id.toLowerCase(), - }, - token1: { - id: pool.token1.id.toLowerCase(), - }, - liquidity: pool.liquidity, - tvlETH: parseFloat(totalValueLockedETH), - tvlUSD: parseFloat(totalValueLockedUSD), - } as TSubgraphPool; + return this.mapSubgraphPool(pool); }); metric.putMetric( @@ -288,4 +253,10 @@ export abstract class SubgraphProvider< return poolsSanitized; } + + protected abstract subgraphQuery(blockNumber?: number): string; + + protected abstract mapSubgraphPool( + rawSubgraphPool: TRawSubgraphPool + ): TSubgraphPool; } diff --git a/src/providers/token-provider.ts b/src/providers/token-provider.ts index 99b6fdafb..713282d35 100644 --- a/src/providers/token-provider.ts +++ b/src/providers/token-provider.ts @@ -977,3 +977,19 @@ export const USDC_ON = (chainId: ChainId): Token => { export const WNATIVE_ON = (chainId: ChainId): Token => { return WRAPPED_NATIVE_CURRENCY[chainId]; }; + +export const V4_SEPOLIA_TEST_OP = new Token( + ChainId.SEPOLIA, + '0xc268035619873d85461525f5fdb792dd95982161', + 18, + 'OP', + 'Optimism' +); + +export const V4_SEPOLIA_TEST_USDC = new Token( + ChainId.SEPOLIA, + '0xbe2a7f5acecdc293bf34445a0021f229dd2edd49', + 6, + 'USDC', + 'USD' +); diff --git a/src/providers/v3/subgraph-provider.ts b/src/providers/v3/subgraph-provider.ts index 5fc09f46b..94b998269 100644 --- a/src/providers/v3/subgraph-provider.ts +++ b/src/providers/v3/subgraph-provider.ts @@ -100,4 +100,49 @@ export class V3SubgraphProvider subgraphUrlOverride ?? SUBGRAPH_URL_BY_CHAIN[chainId] ); } + + protected override subgraphQuery(blockNumber?: number): string { + return ` + query getPools($pageSize: Int!, $id: String) { + pools( + first: $pageSize + ${blockNumber ? `block: { number: ${blockNumber} }` : ``} + where: { id_gt: $id } + ) { + id + token0 { + symbol + id + } + token1 { + symbol + id + } + feeTier + liquidity + totalValueLockedUSD + totalValueLockedETH + totalValueLockedUSDUntracked + } + } + `; + } + + protected override mapSubgraphPool( + rawPool: V3RawSubgraphPool + ): V3SubgraphPool { + return { + id: rawPool.id, + feeTier: rawPool.feeTier, + liquidity: rawPool.liquidity, + token0: { + id: rawPool.token0.id, + }, + token1: { + id: rawPool.token1.id, + }, + tvlETH: parseFloat(rawPool.totalValueLockedETH), + tvlUSD: parseFloat(rawPool.totalValueLockedUSD), + }; + } } diff --git a/src/providers/v4/subgraph-provider.ts b/src/providers/v4/subgraph-provider.ts index f3524dc1e..827d1781c 100644 --- a/src/providers/v4/subgraph-provider.ts +++ b/src/providers/v4/subgraph-provider.ts @@ -81,4 +81,53 @@ export class V4SubgraphProvider subgraphUrlOverride ?? SUBGRAPH_URL_BY_CHAIN[chainId] ); } + + protected override subgraphQuery(blockNumber?: number): string { + return ` + query getPools($pageSize: Int!, $id: String) { + pools( + first: $pageSize + ${blockNumber ? `block: { number: ${blockNumber} }` : ``} + where: { id_gt: $id } + ) { + id + token0 { + symbol + id + } + token1 { + symbol + id + } + feeTier + tickSpacing + hooks + liquidity + totalValueLockedUSD + totalValueLockedETH + totalValueLockedUSDUntracked + } + } + `; + } + + protected override mapSubgraphPool( + rawPool: V4RawSubgraphPool + ): V4SubgraphPool { + return { + id: rawPool.id, + feeTier: rawPool.feeTier, + tickSpacing: rawPool.tickSpacing, + hooks: rawPool.hooks, + liquidity: rawPool.liquidity, + token0: { + id: rawPool.token0.id, + }, + token1: { + id: rawPool.token1.id, + }, + tvlETH: parseFloat(rawPool.totalValueLockedETH), + tvlUSD: parseFloat(rawPool.totalValueLockedUSD), + }; + } } diff --git a/src/routers/alpha-router/alpha-router.ts b/src/routers/alpha-router/alpha-router.ts index 15c0f7e61..cdeae3fdc 100644 --- a/src/routers/alpha-router/alpha-router.ts +++ b/src/routers/alpha-router/alpha-router.ts @@ -86,7 +86,11 @@ import { } from '../../providers/v3/pool-provider'; import { IV3SubgraphProvider } from '../../providers/v3/subgraph-provider'; import { Erc20__factory } from '../../types/other/factories/Erc20__factory'; -import { SWAP_ROUTER_02_ADDRESSES, WRAPPED_NATIVE_CURRENCY } from '../../util'; +import { + SWAP_ROUTER_02_ADDRESSES, + V4_SUPPORTED, + WRAPPED_NATIVE_CURRENCY +} from '../../util'; import { CurrencyAmount } from '../../util/amounts'; import { ID_TO_CHAIN_ID, @@ -147,7 +151,8 @@ import { MixedRouteWithValidQuote, RouteWithValidQuote, V2RouteWithValidQuote, - V3RouteWithValidQuote, V4RouteWithValidQuote + V3RouteWithValidQuote, + V4RouteWithValidQuote, } from './entities/route-with-valid-quote'; import { BestSwapRoute, getBestSwapRoute } from './functions/best-swap-route'; import { calculateRatioAmountIn } from './functions/calculate-ratio-amount-in'; @@ -162,6 +167,7 @@ import { V3CandidatePools, V4CandidatePools, } from './functions/get-candidate-pools'; +import { NATIVE_OVERHEAD } from './gas-models/gas-costs'; import { GasModelProviderConfig, GasModelType, @@ -172,7 +178,6 @@ import { } from './gas-models/gas-model'; import { MixedRouteHeuristicGasModelFactory } from './gas-models/mixedRoute/mixed-route-heuristic-gas-model'; import { V2HeuristicGasModelFactory } from './gas-models/v2/v2-heuristic-gas-model'; -import { NATIVE_OVERHEAD } from './gas-models/gas-costs'; import { V3HeuristicGasModelFactory } from './gas-models/v3/v3-heuristic-gas-model'; import { V4HeuristicGasModelFactory } from './gas-models/v4/v4-heuristic-gas-model'; import { GetQuotesResult, MixedQuoter, V2Quoter, V3Quoter } from './quoters'; @@ -302,6 +307,11 @@ export type AlphaRouterParams = { * All the supported v2 chains configuration */ v2Supported?: ChainId[]; + + /** + * All the supported v4 chains configuration + */ + v4Supported?: ChainId[]; }; export class MapWithLowerCaseKey extends Map { @@ -508,6 +518,7 @@ export class AlphaRouter protected tokenPropertiesProvider: ITokenPropertiesProvider; protected portionProvider: IPortionProvider; protected v2Supported?: ChainId[]; + protected v4Supported?: ChainId[]; constructor({ chainId, @@ -536,6 +547,7 @@ export class AlphaRouter tokenPropertiesProvider, portionProvider, v2Supported, + v4Supported, }: AlphaRouterParams) { this.chainId = chainId; this.provider = provider; @@ -961,6 +973,7 @@ export class AlphaRouter ); this.v2Supported = v2Supported ?? V2_SUPPORTED; + this.v4Supported = v4Supported ?? V4_SUPPORTED; } public async routeToRatio( @@ -1438,6 +1451,7 @@ export class AlphaRouter tradeType, routingConfig, v3GasModel, + v4GasModel, mixedRouteGasModel, gasPriceWei, v2GasModel, @@ -1978,6 +1992,7 @@ export class AlphaRouter this.portionProvider, v2GasModel, v3GasModel, + v4GasModel, swapConfig, providerConfig ); @@ -1992,6 +2007,7 @@ export class AlphaRouter tradeType: TradeType, routingConfig: AlphaRouterConfig, v3GasModel: IGasModel, + v4GasModel: IGasModel, mixedRouteGasModel: IGasModel, gasPriceWei: BigNumber, v2GasModel?: IGasModel, @@ -2027,6 +2043,7 @@ export class AlphaRouter const v3ProtocolSpecified = protocols.includes(Protocol.V3); const v2ProtocolSpecified = protocols.includes(Protocol.V2); const v2SupportedInChain = this.v2Supported?.includes(this.chainId); + const v4SupportedInChain = this.v4Supported?.includes(this.chainId); const shouldQueryMixedProtocol = protocols.includes(Protocol.MIXED) || (noProtocolsSpecified && v2SupportedInChain); @@ -2040,7 +2057,7 @@ export class AlphaRouter Promise.resolve(undefined); // we are explicitly requiring people to specify v4 for now - if (v4ProtocolSpecified) { + if (v4ProtocolSpecified && v4SupportedInChain) { // if (v4ProtocolSpecified || noProtocolsSpecified) { v4CandidatePoolsPromise = getV4CandidatePools({ tokenIn, @@ -2123,7 +2140,7 @@ export class AlphaRouter const quotePromises: Promise[] = []; // for v4, for now we explicitly require people to specify - if (v4ProtocolSpecified) { + if (v4SupportedInChain && v4ProtocolSpecified) { log.info({ protocols, tradeType }, 'Routing across V4'); metric.putMetric( @@ -2325,6 +2342,7 @@ export class AlphaRouter this.portionProvider, v2GasModel, v3GasModel, + v4GasModel, swapConfig, providerConfig ); @@ -2498,12 +2516,13 @@ export class AlphaRouter providerConfig: providerConfig, }); - const [v2GasModel, v3GasModel, V4GasModel, mixedRouteGasModel] = await Promise.all([ - v2GasModelPromise, - v3GasModelPromise, - v4GasModelPromise, - mixedRouteGasModelPromise, - ]); + const [v2GasModel, v3GasModel, V4GasModel, mixedRouteGasModel] = + await Promise.all([ + v2GasModelPromise, + v3GasModelPromise, + v4GasModelPromise, + mixedRouteGasModelPromise, + ]); metric.putMetric( 'GasModelCreation', diff --git a/src/routers/alpha-router/functions/best-swap-route.ts b/src/routers/alpha-router/functions/best-swap-route.ts index 24417ca1a..641811ff0 100644 --- a/src/routers/alpha-router/functions/best-swap-route.ts +++ b/src/routers/alpha-router/functions/best-swap-route.ts @@ -7,7 +7,7 @@ import FixedReverseHeap from 'mnemonist/fixed-reverse-heap'; import Queue from 'mnemonist/queue'; import { IPortionProvider } from '../../../providers/portion-provider'; -import { HAS_L1_FEE, V2_SUPPORTED } from '../../../util'; +import { HAS_L1_FEE, V2_SUPPORTED, V4_SUPPORTED } from '../../../util'; import { CurrencyAmount } from '../../../util/amounts'; import { log } from '../../../util/log'; import { metric, MetricLoggerUnit } from '../../../util/metric'; @@ -21,6 +21,7 @@ import { RouteWithValidQuote, V2RouteWithValidQuote, V3RouteWithValidQuote, + V4RouteWithValidQuote, } from './../entities/route-with-valid-quote'; export type BestSwapRoute = { @@ -43,6 +44,7 @@ export async function getBestSwapRoute( portionProvider: IPortionProvider, v2GasModel?: IGasModel, v3GasModel?: IGasModel, + v4GasModel?: IGasModel, swapConfig?: SwapOptions, providerConfig?: ProviderConfig ): Promise { @@ -93,6 +95,7 @@ export async function getBestSwapRoute( portionProvider, v2GasModel, v3GasModel, + v4GasModel, swapConfig, providerConfig ); @@ -159,6 +162,7 @@ export async function getBestSwapRouteBy( portionProvider: IPortionProvider, v2GasModel?: IGasModel, v3GasModel?: IGasModel, + v4GasModel?: IGasModel, swapConfig?: SwapOptions, providerConfig?: ProviderConfig ): Promise { @@ -362,9 +366,14 @@ export async function getBestSwapRouteBy( ); if (HAS_L1_FEE.includes(chainId)) { - if (v2GasModel == undefined && v3GasModel == undefined) { + if ( + v2GasModel == undefined && + v3GasModel == undefined && + v4GasModel == undefined + ) { throw new Error("Can't compute L1 gas fees."); } else { + // ROUTE-249: consoliate L1 + L2 gas fee adjustment within best-swap-route const v2Routes = curRoutesNew.filter( (routes) => routes.protocol === Protocol.V2 ); @@ -391,6 +400,19 @@ export async function getBestSwapRouteBy( ); } } + const v4Routes = curRoutesNew.filter( + (routes) => routes.protocol === Protocol.V4 + ); + if (v4Routes.length > 0 && V4_SUPPORTED.includes(chainId)) { + if (v4GasModel) { + const v4GasCostL1 = await v4GasModel.calculateL1GasFees!( + v4Routes as V4RouteWithValidQuote[] + ); + gasCostL1QuoteToken = gasCostL1QuoteToken.add( + v4GasCostL1.gasCostL1QuoteToken + ); + } + } } } @@ -477,7 +499,8 @@ export async function getBestSwapRouteBy( }; // If swapping on an L2 that includes a L1 security fee, calculate the fee and include it in the gas adjusted quotes if (HAS_L1_FEE.includes(chainId)) { - if (v2GasModel == undefined && v3GasModel == undefined) { + // ROUTE-249: consoliate L1 + L2 gas fee adjustment within best-swap-route + if (v2GasModel == undefined && v3GasModel == undefined && v4GasModel == undefined) { throw new Error("Can't compute L1 gas fees."); } else { // Before v2 deploy everywhere, a quote on L2 can only go through v3 protocol, @@ -577,6 +600,52 @@ export async function getBestSwapRouteBy( ); } } + const v4Routes = bestSwap.filter( + (routes) => routes.protocol === Protocol.V4 + ); + if (v4Routes.length > 0 && V4_SUPPORTED.includes(chainId)) { + if (v4GasModel) { + const v4GasCostL1 = await v4GasModel.calculateL1GasFees!( + v4Routes as V4RouteWithValidQuote[] + ); + gasCostsL1ToL2.gasUsedL1 = gasCostsL1ToL2.gasUsedL1.add( + v4GasCostL1.gasUsedL1 + ); + gasCostsL1ToL2.gasUsedL1OnL2 = gasCostsL1ToL2.gasUsedL1OnL2.add( + v4GasCostL1.gasUsedL1OnL2 + ); + if ( + gasCostsL1ToL2.gasCostL1USD.currency.equals( + v4GasCostL1.gasCostL1USD.currency + ) + ) { + gasCostsL1ToL2.gasCostL1USD = gasCostsL1ToL2.gasCostL1USD.add( + v4GasCostL1.gasCostL1USD + ); + } else { + // This is to handle the case where gasCostsL1ToL2.gasCostL1USD and v4GasCostL1.gasCostL1USD have different currencies. + // + // gasCostsL1ToL2.gasCostL1USD was initially hardcoded to CurrencyAmount.fromRawAmount(usdGasTokensByChain[chainId]![0]!, 0) + // (https://github.com/Uniswap/smart-order-router/blob/main/src/routers/alpha-router/functions/best-swap-route.ts#L438) + // , where usdGasTokensByChain is coded in the descending order of decimals per chain, + // e.g. Arbitrum_one DAI (18 decimals), USDC bridged (6 decimals), USDC native (6 decimals) + // so gasCostsL1ToL2.gasCostL1USD will have DAI as currency. + // + // For v4GasCostL1.gasCostL1USD, it's calculated within getHighestLiquidityV3USDPool among usdGasTokensByChain[chainId]!, + // (https://github.com/Uniswap/smart-order-router/blob/1c93e133c46af545f8a3d8af7fca3f1f2dcf597d/src/util/gas-factory-helpers.ts#L110) + // , so the code will actually see which USD pool has the highest liquidity, if any. + // e.g. Arbitrum_one on v3 has highest liquidity on USDC native + // so v4GasCostL1.gasCostL1USD will have USDC native as currency. + // + // We will re-assign gasCostsL1ToL2.gasCostL1USD to v3GasCostL1.gasCostL1USD in this case. + gasCostsL1ToL2.gasCostL1USD = v4GasCostL1.gasCostL1USD; + } + gasCostsL1ToL2.gasCostL1QuoteToken = + gasCostsL1ToL2.gasCostL1QuoteToken.add( + v4GasCostL1.gasCostL1QuoteToken + ); + } + } } } diff --git a/src/routers/alpha-router/functions/get-candidate-pools.ts b/src/routers/alpha-router/functions/get-candidate-pools.ts index 84d0119f1..0815763cd 100644 --- a/src/routers/alpha-router/functions/get-candidate-pools.ts +++ b/src/routers/alpha-router/functions/get-candidate-pools.ts @@ -756,13 +756,13 @@ export async function getV4CandidatePools({ const tokenPairsRaw = _.map< V4SubgraphPool, - [Token, Token, FeeAmount, number, string] | undefined + [Token, Token, number, number, string] | undefined >(subgraphPools, (subgraphPool) => { const tokenA = tokenAccessor.getTokenByAddress(subgraphPool.token0.id); const tokenB = tokenAccessor.getTokenByAddress(subgraphPool.token1.id); let fee: FeeAmount; try { - fee = parseFeeAmount(subgraphPool.feeTier); + fee = Number(subgraphPool.feeTier); } catch (err) { log.info( { subgraphPool }, diff --git a/src/routers/alpha-router/gas-models/gas-costs.ts b/src/routers/alpha-router/gas-models/gas-costs.ts index 057be51cf..e10cd456c 100644 --- a/src/routers/alpha-router/gas-models/gas-costs.ts +++ b/src/routers/alpha-router/gas-models/gas-costs.ts @@ -1,9 +1,9 @@ import { BigNumber } from '@ethersproject/bignumber'; import { ChainId, Currency } from '@uniswap/sdk-core'; +import { Protocol } from '@uniswap/router-sdk'; import { AAVE_MAINNET, LIDO_MAINNET } from '../../../providers'; import { V3Route, V4Route } from '../../router'; -import { Protocol } from '@uniswap/router-sdk'; // Cost for crossing an uninitialized tick. export const COST_PER_UNINIT_TICK = BigNumber.from(0); diff --git a/src/routers/alpha-router/gas-models/gas-model.ts b/src/routers/alpha-router/gas-models/gas-model.ts index c71e02873..622704f5a 100644 --- a/src/routers/alpha-router/gas-models/gas-model.ts +++ b/src/routers/alpha-router/gas-models/gas-model.ts @@ -23,6 +23,7 @@ import { DAI_SEPOLIA, DAI_ZKSYNC, USDB_BLAST, + USDCE_ZKSYNC, USDC_ARBITRUM, USDC_ARBITRUM_GOERLI, USDC_ARBITRUM_SEPOLIA, @@ -49,7 +50,6 @@ import { USDC_WORMHOLE_CELO, USDC_ZKSYNC, USDC_ZORA, - USDCE_ZKSYNC, USDT_ARBITRUM, USDT_BNB, USDT_GOERLI, @@ -71,7 +71,7 @@ import { RouteWithValidQuote, V2RouteWithValidQuote, V3RouteWithValidQuote, - V4RouteWithValidQuote + V4RouteWithValidQuote, } from '../entities/route-with-valid-quote'; // When adding new usd gas tokens, ensure the tokens are ordered 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 55e265c39..99f1c61b4 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 @@ -11,6 +11,12 @@ 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 { + BASE_SWAP_COST, + COST_PER_HOP, + COST_PER_INIT_TICK, + COST_PER_UNINIT_TICK, +} from '../gas-costs'; import { BuildOnChainGasModelFactoryType, GasModelProviderConfig, @@ -22,12 +28,6 @@ import { BASE_SWAP_COST as BASE_SWAP_COST_V2, COST_PER_EXTRA_HOP as COST_PER_EXTRA_HOP_V2, } from '../v2/v2-heuristic-gas-model'; -import { - BASE_SWAP_COST, - COST_PER_HOP, - COST_PER_INIT_TICK, - COST_PER_UNINIT_TICK, -} from '../gas-costs'; /** * Computes a gas estimate for a mixed route swap using heuristics. diff --git a/src/routers/alpha-router/gas-models/tick-based-heuristic-gas-model.ts b/src/routers/alpha-router/gas-models/tick-based-heuristic-gas-model.ts index bb01ee48e..159167eb9 100644 --- a/src/routers/alpha-router/gas-models/tick-based-heuristic-gas-model.ts +++ b/src/routers/alpha-router/gas-models/tick-based-heuristic-gas-model.ts @@ -5,13 +5,6 @@ import { Pool } from '@uniswap/v3-sdk'; import { CurrencyAmount, log, WRAPPED_NATIVE_CURRENCY } from '../../../util'; import { calculateL1GasFeesHelper } from '../../../util/gas-factory-helpers'; import { V3RouteWithValidQuote, V4RouteWithValidQuote } from '../entities'; -import { - BuildOnChainGasModelFactoryType, - GasModelProviderConfig, - getQuoteThroughNativePool, - IGasModel, - IOnChainGasModelFactory, -} from './gas-model'; import { BASE_SWAP_COST, COST_PER_HOP, @@ -20,6 +13,13 @@ import { SINGLE_HOP_OVERHEAD, TOKEN_OVERHEAD, } from './gas-costs'; +import { + BuildOnChainGasModelFactoryType, + GasModelProviderConfig, + getQuoteThroughNativePool, + IGasModel, + IOnChainGasModelFactory, +} from './gas-model'; export abstract class TickBasedHeuristicGasModelFactory< TRouteWithValidQuote extends V3RouteWithValidQuote | V4RouteWithValidQuote diff --git a/src/util/chains.ts b/src/util/chains.ts index 78a26a5a4..ad2856d40 100644 --- a/src/util/chains.ts +++ b/src/util/chains.ts @@ -40,6 +40,8 @@ export const V2_SUPPORTED = [ ChainId.AVALANCHE, ]; +export const V4_SUPPORTED = [ChainId.SEPOLIA]; + export const HAS_L1_FEE = [ ChainId.OPTIMISM, ChainId.OPTIMISM_GOERLI, diff --git a/src/util/methodParameters.ts b/src/util/methodParameters.ts index 9a4614fd5..1d5683f87 100644 --- a/src/util/methodParameters.ts +++ b/src/util/methodParameters.ts @@ -7,12 +7,11 @@ import { import { ChainId, Currency, - CurrencyAmount as SDKCurrentAmount, TradeType, } from '@uniswap/sdk-core'; import { - SwapRouter as UniversalRouter, UNIVERSAL_ROUTER_ADDRESS, + SwapRouter as UniversalRouter, } from '@uniswap/universal-router-sdk'; import { Route as V2RouteRaw } from '@uniswap/v2-sdk'; import { Route as V3RouteRaw } from '@uniswap/v3-sdk'; @@ -24,11 +23,12 @@ import { MethodParameters, MixedRouteWithValidQuote, RouteWithValidQuote, + SWAP_ROUTER_02_ADDRESSES, SwapOptions, SwapType, - SWAP_ROUTER_02_ADDRESSES, V2RouteWithValidQuote, V3RouteWithValidQuote, + V4RouteWithValidQuote } from '..'; export function buildTrade( @@ -38,6 +38,10 @@ export function buildTrade( routeAmounts: RouteWithValidQuote[] ): Trade { /// Removed partition because of new mixedRoutes + const v4RouteAmounts = _.filter( + routeAmounts, + (routeAmount) => routeAmount.protocol === Protocol.V4 + ); const v3RouteAmounts = _.filter( routeAmounts, (routeAmount) => routeAmount.protocol === Protocol.V3 @@ -51,12 +55,72 @@ export function buildTrade( (routeAmount) => routeAmount.protocol === Protocol.MIXED ); - // TODO: populate v4Routes - const v4Routes: { - routev4: V4RouteRaw; - inputAmount: SDKCurrentAmount; - outputAmount: SDKCurrentAmount; - }[] = []; + // TODO: ROUTE-248 - refactor route objects for the trade object composition + const v4Routes = _.map< + V4RouteWithValidQuote, + { + routev4: V4RouteRaw; + inputAmount: CurrencyAmount; + outputAmount: CurrencyAmount; + } + >( + v4RouteAmounts as V4RouteWithValidQuote[], + (routeAmount: V4RouteWithValidQuote) => { + const { route, amount, quote } = routeAmount; + + // The route, amount and quote are all in terms of wrapped tokens. + // When constructing the Trade object the inputAmount/outputAmount must + // use native currencies if specified by the user. This is so that the Trade knows to wrap/unwrap. + if (tradeType == TradeType.EXACT_INPUT) { + const amountCurrency = CurrencyAmount.fromFractionalAmount( + tokenInCurrency, + amount.numerator, + amount.denominator + ); + const quoteCurrency = CurrencyAmount.fromFractionalAmount( + tokenOutCurrency, + quote.numerator, + quote.denominator + ); + + const routeRaw = new V4RouteRaw( + route.pools, + amountCurrency.currency, + quoteCurrency.currency + ); + + return { + routev4: routeRaw, + inputAmount: amountCurrency, + outputAmount: quoteCurrency, + }; + } else { + const quoteCurrency = CurrencyAmount.fromFractionalAmount( + tokenInCurrency, + quote.numerator, + quote.denominator + ); + + const amountCurrency = CurrencyAmount.fromFractionalAmount( + tokenOutCurrency, + amount.numerator, + amount.denominator + ); + + const routeCurrency = new V4RouteRaw( + route.pools, + quoteCurrency.currency, + amountCurrency.currency + ); + + return { + routev4: routeCurrency, + inputAmount: quoteCurrency, + outputAmount: amountCurrency, + }; + } + } + ); const v3Routes = _.map< V3RouteWithValidQuote, 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 a317cd2ac..f9660cd46 100644 --- a/test/integ/routers/alpha-router/alpha-router.integration.test.ts +++ b/test/integ/routers/alpha-router/alpha-router.integration.test.ts @@ -3,7 +3,11 @@ */ import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; -import { AllowanceTransfer, permit2Address, PermitSingle } from '@uniswap/permit2-sdk'; +import { + AllowanceTransfer, + permit2Address, + PermitSingle +} from '@uniswap/permit2-sdk'; import { Protocol } from '@uniswap/router-sdk'; import { ChainId, @@ -16,8 +20,12 @@ import { Token, TradeType } from '@uniswap/sdk-core'; -import { UNIVERSAL_ROUTER_ADDRESS as UNIVERSAL_ROUTER_ADDRESS_BY_CHAIN } from '@uniswap/universal-router-sdk'; -import { Permit2Permit } from '@uniswap/universal-router-sdk/dist/utils/inputTokens'; +import { + UNIVERSAL_ROUTER_ADDRESS as UNIVERSAL_ROUTER_ADDRESS_BY_CHAIN +} from '@uniswap/universal-router-sdk'; +import { + Permit2Permit +} from '@uniswap/universal-router-sdk/dist/utils/inputTokens'; import { Pair } from '@uniswap/v2-sdk'; import { encodeSqrtRatioX96, FeeAmount, Pool } from '@uniswap/v3-sdk'; import bunyan from 'bunyan'; @@ -32,6 +40,7 @@ import { AlphaRouterConfig, CachingV2PoolProvider, CachingV3PoolProvider, + CachingV4PoolProvider, CEUR_CELO, CEUR_CELO_ALFAJORES, CUSD_CELO, @@ -78,18 +87,25 @@ import { V2Route, V3PoolProvider, V3Route, + V4_SEPOLIA_TEST_OP, + V4_SEPOLIA_TEST_USDC, + V4PoolProvider, WBTC_GNOSIS, WBTC_MOONBEAM, WETH9, WNATIVE_ON, - WRAPPED_NATIVE_CURRENCY, - CachingV4PoolProvider, - V4PoolProvider + WRAPPED_NATIVE_CURRENCY } from '../../../../src'; import { PortionProvider } from '../../../../src/providers/portion-provider'; -import { OnChainTokenFeeFetcher } from '../../../../src/providers/token-fee-fetcher'; -import { DEFAULT_ROUTING_CONFIG_BY_CHAIN } from '../../../../src/routers/alpha-router/config'; -import { Permit2__factory } from '../../../../src/types/other/factories/Permit2__factory'; +import { + OnChainTokenFeeFetcher +} from '../../../../src/providers/token-fee-fetcher'; +import { + DEFAULT_ROUTING_CONFIG_BY_CHAIN +} from '../../../../src/routers/alpha-router/config'; +import { + Permit2__factory +} from '../../../../src/types/other/factories/Permit2__factory'; import { getBalanceAndApprove } from '../../../test-util/getBalanceAndApprove'; import { BULLET, @@ -100,6 +116,7 @@ import { Portion } from '../../../test-util/mock-data'; import { WHALES } from '../../../test-util/whales'; +import { V4SubgraphProvider } from '../../../../build/main'; const FORK_BLOCK = 20413900; const UNIVERSAL_ROUTER_ADDRESS = UNIVERSAL_ROUTER_ADDRESS_BY_CHAIN(1); @@ -733,6 +750,7 @@ describe('alpha router integration', () => { multicall2Provider, v2PoolProvider, v3PoolProvider, + v4PoolProvider, simulator, }); @@ -744,6 +762,7 @@ describe('alpha router integration', () => { multicall2Provider, v2PoolProvider, v3PoolProvider, + v4PoolProvider, simulator: ethEstimateGasSimulator, }); @@ -3378,6 +3397,7 @@ describe('quote for other networks', () => { [ChainId.MAINNET]: () => USDC_ON(ChainId.MAINNET), [ChainId.GOERLI]: () => UNI_GOERLI, [ChainId.SEPOLIA]: () => USDC_ON(ChainId.SEPOLIA), + [ChainId.SEPOLIA]: () => V4_SEPOLIA_TEST_OP, [ChainId.OPTIMISM]: () => USDC_ON(ChainId.OPTIMISM), [ChainId.OPTIMISM]: () => USDC_NATIVE_OPTIMISM, [ChainId.OPTIMISM_GOERLI]: () => USDC_ON(ChainId.OPTIMISM_GOERLI), @@ -3409,6 +3429,7 @@ describe('quote for other networks', () => { [ChainId.MAINNET]: () => DAI_ON(1), [ChainId.GOERLI]: () => DAI_ON(ChainId.GOERLI), [ChainId.SEPOLIA]: () => DAI_ON(ChainId.SEPOLIA), + [ChainId.SEPOLIA]: () => V4_SEPOLIA_TEST_USDC, [ChainId.OPTIMISM]: () => DAI_ON(ChainId.OPTIMISM), [ChainId.OPTIMISM_GOERLI]: () => DAI_ON(ChainId.OPTIMISM_GOERLI), [ChainId.OPTIMISM_SEPOLIA]: () => USDC_ON(ChainId.OPTIMISM_SEPOLIA), @@ -3516,13 +3537,36 @@ describe('quote for other networks', () => { tenderlySimulator, ethEstimateGasSimulator ); + const SUBGRAPH_URL_BY_CHAIN: { [chainId in ChainId]?: string } = { + [ChainId.SEPOLIA]: process.env.SUBGRAPH_URL_SEPOLIA, + }; - alphaRouter = new AlphaRouter({ - chainId: chain, - provider, - multicall2Provider, - simulator, - }); + if (SUBGRAPH_URL_BY_CHAIN[chain]) { + const v4SubgraphProvider = new V4SubgraphProvider( + chain, + 2, + 30000, + true, + 0.01, + Number.MAX_VALUE, + SUBGRAPH_URL_BY_CHAIN[chain], + ); + + alphaRouter = new AlphaRouter({ + chainId: chain, + provider, + multicall2Provider, + v4SubgraphProvider, + simulator, + }); + } else { + alphaRouter = new AlphaRouter({ + chainId: chain, + provider, + multicall2Provider, + simulator, + }); + } }); if (chain === ChainId.MAINNET && tradeType === TradeType.EXACT_INPUT) { @@ -3559,6 +3603,10 @@ describe('quote for other networks', () => { describe(`Swap`, function() { it(`${wrappedNative.symbol} -> erc20`, async () => { + if (erc1.equals(V4_SEPOLIA_TEST_OP)) { + return; + } + const tokenIn = wrappedNative; const tokenOut = erc1; const amount = @@ -3614,8 +3662,8 @@ describe('quote for other networks', () => { expect(swap).not.toBeNull(); }); - it(`erc20 -> erc20`, async () => { - if (chain === ChainId.SEPOLIA) { + it(`${erc1.symbol} -> ${erc2.symbol}`, async () => { + if (chain === ChainId.SEPOLIA && !erc1.equals(V4_SEPOLIA_TEST_OP)) { // Sepolia doesn't have sufficient liquidity on DAI pools yet return; } @@ -3624,7 +3672,7 @@ describe('quote for other networks', () => { const tokenOut = erc2; // Current WETH/USDB pool (https://blastscan.io/address/0xf52b4b69123cbcf07798ae8265642793b2e8990c) has low WETH amount - const exactOutAmount = chain === ChainId.BLAST || chain === ChainId.ZORA ? '0.002' : '1'; + const exactOutAmount = '1'; const amount = tradeType == TradeType.EXACT_INPUT ? parseAmount('1', tokenIn) @@ -3638,9 +3686,10 @@ describe('quote for other networks', () => { { // @ts-ignore[TS7053] - complaining about switch being non exhaustive ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], - protocols: [Protocol.V3, Protocol.V2], + protocols: [Protocol.V4, Protocol.V3, Protocol.V2], } ); + console.log(`quote ${swap!.quote.toExact().toString()}`) expect(swap).toBeDefined(); expect(swap).not.toBeNull(); }); @@ -3693,7 +3742,7 @@ describe('quote for other networks', () => { }); it(`has quoteGasAdjusted values`, async () => { - if (chain === ChainId.SEPOLIA) { + if (chain === ChainId.SEPOLIA && !erc1.equals(V4_SEPOLIA_TEST_OP)) { // Sepolia doesn't have sufficient liquidity on DAI pools yet return; } @@ -3716,7 +3765,7 @@ describe('quote for other networks', () => { { // @ts-ignore[TS7053] - complaining about switch being non exhaustive ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], - protocols: [Protocol.V3, Protocol.V2], + protocols: [Protocol.V4, Protocol.V3, Protocol.V2], } ); expect(swap).toBeDefined(); @@ -3734,7 +3783,8 @@ describe('quote for other networks', () => { }); it(`does not error when protocols array is empty`, async () => { - if (chain === ChainId.SEPOLIA) { + // V4 protocol requires explicit Protocol.V4 in the input array + if (chain === ChainId.SEPOLIA && erc1.equals(V4_SEPOLIA_TEST_OP)) { // Sepolia doesn't have sufficient liquidity on DAI pools yet return; } @@ -3795,13 +3845,17 @@ describe('quote for other networks', () => { if ([ ChainId.CELO, ChainId.CELO_ALFAJORES, - ChainId.SEPOLIA, ChainId.BLAST, ChainId.ZKSYNC ].includes(chain)) { return; } it(`${wrappedNative.symbol} -> erc20`, async () => { + if (chain === ChainId.SEPOLIA) { + // Sepolia doesn't have sufficient liquidity on DAI pools yet + return; + } + const tokenIn = wrappedNative; const tokenOut = erc1; const amount = @@ -3877,7 +3931,7 @@ describe('quote for other networks', () => { // due to gas cost per compressed calldata byte dropping from 16 to 3. // Relying on Tenderly gas estimate is the only way our github CI can auto catch this. const percentDiff = gasEstimateDiff.mul(BigNumber.from(100)).div(swapWithSimulation!.estimatedGasUsed); - console.log(`chain ${chain} GAS_ESTIMATE_DEVIATION_PERCENT ${percentDiff.toNumber()}`); + console.log(`chain ${chain} GAS_ESTIMATE_DEVIATION_PERCENT ${percentDiff.toNumber()} expected ${GAS_ESTIMATE_DEVIATION_PERCENT[chain]}`); expect(percentDiff.lte(BigNumber.from(GAS_ESTIMATE_DEVIATION_PERCENT[chain]))).toBe(true); if (swapWithSimulation) { @@ -3897,6 +3951,11 @@ describe('quote for other networks', () => { }); it(`${wrappedNative.symbol} -> ${erc1.symbol} v2 only`, async () => { + if (chain === ChainId.SEPOLIA) { + // Sepolia doesn't have sufficient liquidity on DAI pools yet + return; + } + const tokenIn = wrappedNative; const tokenOut = erc1; @@ -3999,8 +4058,9 @@ describe('quote for other networks', () => { // Scope limited for non mainnet network tests to validating the swap }); - it(`erc20 -> erc20`, async () => { - if (chain === ChainId.SEPOLIA) { + it(`${erc1.symbol} -> ${erc2.symbol}`, async () => { + // TOOD: re-enable sepolia OP -> USDC swap with simulation, once universal router supports v4 swap commands + if (chain === ChainId.SEPOLIA && erc1.equals(V4_SEPOLIA_TEST_OP)) { // Sepolia doesn't have sufficient liquidity on DAI pools yet return; } @@ -4038,7 +4098,7 @@ describe('quote for other networks', () => { { // @ts-ignore[TS7053] - complaining about switch being non exhaustive ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], - protocols: [Protocol.V3, Protocol.V2], + protocols: [Protocol.V4, Protocol.V3, Protocol.V2], saveTenderlySimulationIfFailed: true, } ); @@ -4062,7 +4122,7 @@ describe('quote for other networks', () => { { // @ts-ignore[TS7053] - complaining about switch being non exhaustive ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], - protocols: [Protocol.V3, Protocol.V2], + protocols: [Protocol.V4, Protocol.V3, Protocol.V2], saveTenderlySimulationIfFailed: true, } ); @@ -4080,7 +4140,7 @@ describe('quote for other networks', () => { // due to gas cost per compressed calldata byte dropping from 16 to 3. // Relying on Tenderly gas estimate is the only way our github CI can auto catch this. const percentDiff = gasEstimateDiff.mul(BigNumber.from(100)).div(swapWithSimulation!.estimatedGasUsed); - console.log(`chain ${chain} GAS_ESTIMATE_DEVIATION_PERCENT ${percentDiff.toNumber()}`); + console.log(`chain ${chain} GAS_ESTIMATE_DEVIATION_PERCENT ${percentDiff.toNumber()} expected ${GAS_ESTIMATE_DEVIATION_PERCENT[chain]}`); expect(percentDiff.lte(BigNumber.from(GAS_ESTIMATE_DEVIATION_PERCENT[chain]))).toBe(true); diff --git a/test/unit/providers/on-chain-quote-provider.test.ts b/test/unit/providers/on-chain-quote-provider.test.ts index 48729be3b..aa35df48f 100644 --- a/test/unit/providers/on-chain-quote-provider.test.ts +++ b/test/unit/providers/on-chain-quote-provider.test.ts @@ -3,7 +3,7 @@ import { CurrencyAmount, ID_TO_PROVIDER, OnChainQuoteProvider, parseAmount, - UniswapMulticallProvider, V4Route + UniswapMulticallProvider, V4_SEPOLIA_TEST_OP, V4_SEPOLIA_TEST_USDC, V4Route } from '../../../src'; import { JsonRpcProvider } from '@ethersproject/providers'; import { Pool } from '@uniswap/v4-sdk'; @@ -28,20 +28,8 @@ describe('on chain quote provider', () => { provider, multicall2Provider, ); - const op = new Token( - chain, - '0xc268035619873d85461525f5fdb792dd95982161', - 18, - 'OP', - 'Optimism' - ) - const usdc = new Token( - chain, - '0xbe2a7f5acecdc293bf34445a0021f229dd2edd49', - 6, - 'USDC', - 'USD' - ) + const op = V4_SEPOLIA_TEST_OP + const usdc = V4_SEPOLIA_TEST_USDC const amountIns = [ parseAmount("0.01", op) ]