Skip to content

Commit

Permalink
fix(router-sdk): fix mixed route ETH <-> WETH wrong route.path (#299)
Browse files Browse the repository at this point in the history
## PR Scope

Please title your PR according to the following types and scopes following [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/):

- `fix(SDK name):` will trigger a patch version
- `chore(<type>):` will not trigger any release and should be used for internal repo changes
- `<type>(public):` will trigger a patch version for non-code changes (e.g. README changes)
- `feat(SDK name):` will trigger a minor version
- `feat(breaking):` will trigger a major version for a breaking change

## Description

_[Summary of the change, motivation, and context]_

## How Has This Been Tested?

_[e.g. Manually, E2E tests, unit tests, Storybook]_

## Are there any breaking changes?

_[e.g. Type definitions, API definitions]_

If there are breaking changes, please ensure you bump the major version Bump the major version (by using the title `feat(breaking): ...`), post a notice in #eng-sdks, and explicitly notify all Uniswap Labs consumers of the SDK.

## (Optional) Feedback Focus

_[Specific parts of this PR you'd like feedback on, or that reviewers should pay closer attention to]_

## (Optional) Follow Ups

_[Things that weren't addressed in this PR, ways you plan to build on this work, or other ways this work could be extended]_
  • Loading branch information
jsy1218 authored Feb 6, 2025
1 parent 210d614 commit edf1c7b
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 63 deletions.
25 changes: 21 additions & 4 deletions sdks/router-sdk/src/entities/mixedRoute/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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)
Expand All @@ -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', () => {
Expand Down
51 changes: 37 additions & 14 deletions sdks/router-sdk/src/entities/mixedRoute/route.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -25,9 +24,12 @@ export class MixedRouteSDK<TInput extends Currency, TOutput extends Currency> {
* @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)
Expand All @@ -36,7 +38,11 @@ export class MixedRouteSDK<TInput extends Currency, TOutput extends Currency> {
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')
Expand All @@ -51,20 +57,37 @@ export class MixedRouteSDK<TInput extends Currency, TOutput extends Currency> {
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)
}

Expand Down
1 change: 0 additions & 1 deletion sdks/router-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,3 @@ export * from './utils/encodeMixedRouteToPath'
export * from './utils/TPool'
export * from './utils/pathCurrency'
export * from './utils'
export * from './utils/isValidTokenPath'
3 changes: 2 additions & 1 deletion sdks/router-sdk/src/utils/encodeMixedRouteToPath.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
19 changes: 0 additions & 19 deletions sdks/router-sdk/src/utils/isValidTokenPath.test.ts

This file was deleted.

24 changes: 0 additions & 24 deletions sdks/router-sdk/src/utils/isValidTokenPath.ts

This file was deleted.

17 changes: 17 additions & 0 deletions sdks/router-sdk/src/utils/pathCurrency.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})

0 comments on commit edf1c7b

Please sign in to comment.