From 07f62c4867fbcbf324804fd1d10f89490976ec79 Mon Sep 17 00:00:00 2001 From: jsy1218 <91580504+jsy1218@users.noreply.github.com> Date: Tue, 4 Feb 2025 17:06:39 -0800 Subject: [PATCH] fix(router-sdk): fix mixed route ETH <-> WETH wrong route.path --- .../src/entities/mixedRoute/route.test.ts | 25 +++++++-- .../src/entities/mixedRoute/route.ts | 51 ++++++++++++++----- sdks/router-sdk/src/index.ts | 1 - .../src/utils/encodeMixedRouteToPath.test.ts | 3 +- .../src/utils/isValidTokenPath.test.ts | 19 ------- sdks/router-sdk/src/utils/isValidTokenPath.ts | 24 --------- .../router-sdk/src/utils/pathCurrency.test.ts | 17 +++++++ 7 files changed, 77 insertions(+), 63 deletions(-) delete mode 100644 sdks/router-sdk/src/utils/isValidTokenPath.test.ts delete mode 100644 sdks/router-sdk/src/utils/isValidTokenPath.ts create mode 100644 sdks/router-sdk/src/utils/pathCurrency.test.ts diff --git a/sdks/router-sdk/src/entities/mixedRoute/route.test.ts b/sdks/router-sdk/src/entities/mixedRoute/route.test.ts index 4ca7b0b67..753d1bf4d 100644 --- a/sdks/router-sdk/src/entities/mixedRoute/route.test.ts +++ b/sdks/router-sdk/src/entities/mixedRoute/route.test.ts @@ -37,7 +37,14 @@ describe('MixedRoute', () => { describe('path', () => { it('real v3 weth pool and fake v4 eth/weth pool', () => { const route = new MixedRouteSDK([pool_v3_0_weth, pool_v4_weth_eth], token0, ETHER) - expect(route.path).toEqual([token0, weth, ETHER]) + expect(route.path).toEqual([token0, weth]) + expect(route.pools).toEqual([pool_v3_0_weth]) + }) + + it('real v3 weth pool and real v4 eth pool', () => { + const route = new MixedRouteSDK([pool_v3_0_weth, pool_v4_weth_eth, pool_v4_1_eth], token0, token1) + expect(route.path).toEqual([token0, weth, token1]) + expect(route.pools).toEqual([pool_v3_0_weth, pool_v4_1_eth]) }) it('wraps pure v3 route object and successfully constructs a path from the tokens', () => { @@ -90,7 +97,7 @@ describe('MixedRoute', () => { }) it('wraps mixed route object with mixed v4 route that converts WETH -> ETH ', () => { - const route = new MixedRouteSDK([pool_v3_0_weth, pool_v4_weth_eth, pool_v4_1_eth], token0, token1) + const route = new MixedRouteSDK([pool_v3_0_weth, pool_v4_weth_eth, pool_v4_1_eth], token0, token1, true) expect(route.pools).toEqual([pool_v3_0_weth, pool_v4_weth_eth, pool_v4_1_eth]) expect(route.path).toEqual([token0, weth, ETHER, token1]) expect(route.input).toEqual(token0) @@ -110,11 +117,21 @@ describe('MixedRoute', () => { }) it('cannot wrap mixed route object with pure v4 route that converts ETH -> WETH ', () => { - expect(() => new MixedRouteSDK([pool_v4_1_eth, pool_v4_0_weth], token1, token0)).toThrow('PATH') + const route = new MixedRouteSDK([pool_v4_1_eth, pool_v4_0_weth], token1, token0) + expect(route.pools).toEqual([pool_v4_1_eth, pool_v4_0_weth]) + expect(route.path).toEqual([token1, ETHER, token0]) + expect(route.input).toEqual(token1) + expect(route.output).toEqual(token0) + expect(route.chainId).toEqual(1) }) it('cannot wrap mixed route object with pure v4 route that converts WETH -> ETH ', () => { - expect(() => new MixedRouteSDK([pool_v4_0_weth, pool_v4_1_eth], token0, token1)).toThrow('PATH') + const route = new MixedRouteSDK([pool_v4_0_weth, pool_v4_1_eth], token0, token1) + expect(route.pools).toEqual([pool_v4_0_weth, pool_v4_1_eth]) + expect(route.path).toEqual([token0, weth, token1]) + expect(route.input).toEqual(token0) + expect(route.output).toEqual(token1) + expect(route.chainId).toEqual(1) }) it('wraps complex mixed route object and successfully constructs a path from the tokens', () => { diff --git a/sdks/router-sdk/src/entities/mixedRoute/route.ts b/sdks/router-sdk/src/entities/mixedRoute/route.ts index 0ed0dff84..bd781da30 100644 --- a/sdks/router-sdk/src/entities/mixedRoute/route.ts +++ b/sdks/router-sdk/src/entities/mixedRoute/route.ts @@ -1,7 +1,6 @@ import invariant from 'tiny-invariant' import { Currency, Price, Token } from '@uniswap/sdk-core' import { Pool as V4Pool } from '@uniswap/v4-sdk' -import { isValidTokenPath } from '../../utils/isValidTokenPath' import { getPathCurrency } from '../../utils/pathCurrency' import { TPool } from '../../utils/TPool' @@ -25,9 +24,12 @@ export class MixedRouteSDK { * @param pools An array of `TPool` objects (pools or pairs), ordered by the route the swap will take * @param input The input token * @param output The output token + * @param retainsFakePool Set to true to filter out a pool that has a fake eth-weth pool */ - public constructor(pools: TPool[], input: TInput, output: TOutput) { + public constructor(pools: TPool[], input: TInput, output: TOutput, retainFakePools = false) { + pools = retainFakePools ? pools : pools.filter((pool) => !(pool instanceof V4Pool && pool.tickSpacing === 0)) invariant(pools.length > 0, 'POOLS') + // there is a pool mismatched to the path if we do not retain the fake eth-weth pools const chainId = pools[0].chainId const allOnSameChain = pools.every((pool) => pool.chainId === chainId) @@ -36,7 +38,11 @@ export class MixedRouteSDK { this.pathInput = getPathCurrency(input, pools[0]) this.pathOutput = getPathCurrency(output, pools[pools.length - 1]) - invariant(pools[0].involvesToken(this.pathInput as Token), 'INPUT') + if (!(pools[0] instanceof V4Pool)) { + invariant(pools[0].involvesToken(this.pathInput as Token), 'INPUT') + } else { + invariant((pools[0] as V4Pool).v4InvolvesToken(this.pathInput), 'INPUT') + } const lastPool = pools[pools.length - 1] if (lastPool instanceof V4Pool) { invariant(lastPool.v4InvolvesToken(output) || lastPool.v4InvolvesToken(output.wrapped), 'OUTPUT') @@ -51,20 +57,37 @@ export class MixedRouteSDK { pools[0].token0.equals(this.pathInput) ? tokenPath.push(pools[0].token1) : tokenPath.push(pools[0].token0) for (let i = 1; i < pools.length; i++) { - const prevPool = pools[i - 1] const pool = pools[i] const inputToken = tokenPath[i] - const outputToken = - pool instanceof V4Pool - ? pool.token0.equals(inputToken) - ? pool.token1 - : pool.token0 - : pool.token0.wrapped.equals(inputToken.wrapped) - ? pool.token1 - : pool.token0 - - invariant(isValidTokenPath(prevPool, pool, inputToken), `PATH`) + let outputToken + if ( + // we hit an edge case if it's a v4 pool and neither of the tokens are in the pool OR it is not a v4 pool but the input currency is eth + (pool instanceof V4Pool && !pool.involvesToken(inputToken)) || + (!(pool instanceof V4Pool) && inputToken.isNative) + ) { + // We handle the case where the inputToken =/= pool.token0 or pool.token1. There are 2 specific cases. + if (inputToken.equals(pool.token0.wrapped)) { + // 1) the inputToken is WETH and the current pool has ETH + // for example, pools: USDC-WETH, ETH-PEPE, path: USDC, WETH, PEPE + // second pool is a v4 pool, the first could be any version + outputToken = pool.token1 + } else if (inputToken.wrapped.equals(pool.token0) || inputToken.wrapped.equals(pool.token1)) { + // 2) the inputToken is ETH and the current pool has WETH + // for example, pools: USDC-ETH, WETH-PEPE, path: USDC, ETH, PEPE + // first pool is a v4 pool, the second could be any version + outputToken = inputToken.wrapped.equals(pool.token0) ? pool.token1 : pool.token0 + } else { + throw new Error(`POOL_MISMATCH pool: ${JSON.stringify(pool)} inputToken: ${JSON.stringify(inputToken)}`) + } + } else { + // then the input token must equal either token0 or token1 + invariant( + inputToken.equals(pool.token0) || inputToken.equals(pool.token1), + `PATH pool ${JSON.stringify(pool)} inputToken ${JSON.stringify(inputToken)}` + ) + outputToken = inputToken.equals(pool.token0) ? pool.token1 : pool.token0 + } tokenPath.push(outputToken) } diff --git a/sdks/router-sdk/src/index.ts b/sdks/router-sdk/src/index.ts index 1ab290cb5..47d5ccc11 100644 --- a/sdks/router-sdk/src/index.ts +++ b/sdks/router-sdk/src/index.ts @@ -12,4 +12,3 @@ export * from './utils/encodeMixedRouteToPath' export * from './utils/TPool' export * from './utils/pathCurrency' export * from './utils' -export * from './utils/isValidTokenPath' diff --git a/sdks/router-sdk/src/utils/encodeMixedRouteToPath.test.ts b/sdks/router-sdk/src/utils/encodeMixedRouteToPath.test.ts index a409ae009..3fc0d8f91 100644 --- a/sdks/router-sdk/src/utils/encodeMixedRouteToPath.test.ts +++ b/sdks/router-sdk/src/utils/encodeMixedRouteToPath.test.ts @@ -74,7 +74,8 @@ describe('#encodeMixedRouteToPath', () => { const route_1_v2_weth_v0_eth_v4_token0 = new MixedRouteSDK( [pair_1_weth, fake_v4_eth_weth_pool, pool_V4_0_eth], token1, - token0 + token0, + true ) describe('pure V3', () => { diff --git a/sdks/router-sdk/src/utils/isValidTokenPath.test.ts b/sdks/router-sdk/src/utils/isValidTokenPath.test.ts deleted file mode 100644 index c9b6b9c55..000000000 --- a/sdks/router-sdk/src/utils/isValidTokenPath.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Ether, Token, WETH9 } from '@uniswap/sdk-core' -import { FeeAmount, encodeSqrtRatioX96 } from '@uniswap/v3-sdk' -import { Pool as V4Pool } from '@uniswap/v4-sdk' -import { ADDRESS_ZERO } from '../constants' -import { isValidTokenPath } from './isValidTokenPath' - -describe('#isValidTokenPath', () => { - const SQRT_RATIO_ONE = encodeSqrtRatioX96(1, 1) - const ETHER = Ether.onChain(1) - const token1 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 't1') - const weth = WETH9[1] - - const pool_v4_weth_eth = new V4Pool(weth, ETHER, 0, 0, ADDRESS_ZERO, 79228162514264337593543950336, 0, 0) - const pool_v4_1_eth = new V4Pool(token1, ETHER, FeeAmount.MEDIUM, 60, ADDRESS_ZERO, SQRT_RATIO_ONE, 0, 0) - - it('v3 pool and v4 pool, with WETH and ETH unwrap', () => { - expect(isValidTokenPath(pool_v4_weth_eth, pool_v4_1_eth, ETHER)).toBe(true) - }) -}) diff --git a/sdks/router-sdk/src/utils/isValidTokenPath.ts b/sdks/router-sdk/src/utils/isValidTokenPath.ts deleted file mode 100644 index 577edb61a..000000000 --- a/sdks/router-sdk/src/utils/isValidTokenPath.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Currency, Token } from '@uniswap/sdk-core' -import { Pool as V4Pool } from '@uniswap/v4-sdk' -import { TPool } from './TPool' - -export function isValidTokenPath(prevPool: TPool, currentPool: TPool, inputToken: Currency): boolean { - if (inputToken instanceof Token && currentPool.involvesToken(inputToken)) return true - - if (currentPool instanceof V4Pool && currentPool.involvesToken(inputToken)) return true - - // throw if both v4 pools, native/wrapped tokens not interchangeable in v4 - if (prevPool instanceof V4Pool && currentPool instanceof V4Pool) return false - - // v2/v3 --> v4 valid if v2/v3 output is the wrapped version of the v4 pool native currency - if (currentPool instanceof V4Pool) { - if (currentPool.token0.wrapped.equals(inputToken) || currentPool.token1.wrapped.equals(inputToken)) return true - } - - // v4 --> v2/v3 valid if v4 output is the native version of the v2/v3 wrapped token - if (prevPool instanceof V4Pool) { - if (currentPool.involvesToken(inputToken.wrapped)) return true - } - - return false -} diff --git a/sdks/router-sdk/src/utils/pathCurrency.test.ts b/sdks/router-sdk/src/utils/pathCurrency.test.ts new file mode 100644 index 000000000..5c2bc23a9 --- /dev/null +++ b/sdks/router-sdk/src/utils/pathCurrency.test.ts @@ -0,0 +1,17 @@ +import { Ether, Token } from '@uniswap/sdk-core' +import { encodeSqrtRatioX96 } from '@uniswap/v3-sdk' +import { Pool as V4Pool } from '@uniswap/v4-sdk' +import { ADDRESS_ZERO } from '../constants' +import { getPathCurrency } from './pathCurrency' + +describe('#getPathCurrency', () => { + const SQRT_RATIO_ONE = encodeSqrtRatioX96(1, 1) + const ETHER = Ether.onChain(1) + const token1 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 't1') + + const pool_v4_eth_token1 = new V4Pool(token1, ETHER, 0, 0, ADDRESS_ZERO, SQRT_RATIO_ONE, 0, 0) + + it('returns eth input', () => { + expect(getPathCurrency(ETHER, pool_v4_eth_token1)).toEqual(ETHER) + }) +})