Skip to content

Commit

Permalink
fix(router-sdk): Address wrapped/native currency conditions in mixed …
Browse files Browse the repository at this point in the history
…routes with v4 (#81)
  • Loading branch information
ewilz authored Sep 5, 2024
1 parent 9068d73 commit df6348a
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 9 deletions.
28 changes: 27 additions & 1 deletion sdks/router-sdk/src/entities/mixedRoute/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('MixedRoute', () => {
expect(route.chainId).toEqual(1)
})

it('wraps mixed route object with v4 route successfully constructs a pth from the tokens', () => {
it('wraps mixed route object with v4 route successfully constructs a path from the tokens', () => {
const route = new MixedRouteSDK([pool_v3_0_1, pool_v4_0_weth], token1, weth)
expect(route.pools).toEqual([pool_v3_0_1, pool_v4_0_weth])
expect(route.path).toEqual([token1, token0, weth])
Expand All @@ -81,6 +81,32 @@ describe('MixedRoute', () => {
expect(route.chainId).toEqual(1)
})

it('wraps mixed route object with mixed v4 route that converts WETH -> ETH ', () => {
const route = new MixedRouteSDK([pool_v3_0_weth, pool_v4_1_eth], token0, token1)
expect(route.pools).toEqual([pool_v3_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 mixed route object with mixed v4 route that converts ETH -> WETH ', () => {
const route = new MixedRouteSDK([pool_v4_1_eth, pool_v3_0_weth], token1, token0)
expect(route.pools).toEqual([pool_v4_1_eth, pool_v3_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 ETH -> WETH ', () => {
expect(() => new MixedRouteSDK([pool_v4_1_eth, pool_v4_0_weth], token1, token0)).toThrow('PATH')
})

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')
})

it('wraps complex mixed route object and successfully constructs a path from the tokens', () => {
const route = new MixedRouteSDK([pool_v3_0_1, pair_1_weth, pair_weth_2], token0, token2)
expect(route.pools).toEqual([pool_v3_0_1, pair_1_weth, pair_weth_2])
Expand Down
16 changes: 11 additions & 5 deletions sdks/router-sdk/src/entities/mixedRoute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Currency, Price, Token } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { Pool as V3Pool } from '@uniswap/v3-sdk'
import { Pool as V4Pool } from '@uniswap/v4-sdk'
import { isValidTokenPath } from '../../utils/isValidTokenPath'

type TPool = Pair | V3Pool | V4Pool

Expand Down Expand Up @@ -52,11 +53,16 @@ export class MixedRouteSDK<TInput extends Currency, TOutput extends Currency> {
* Normalizes token0-token1 order and selects the next token/fee step to add to the path
* */
const tokenPath: Currency[] = [this.adjustedInput]
for (const [i, pool] of pools.entries()) {
const currentInputToken = tokenPath[i]
invariant(currentInputToken.equals(pool.token0) || currentInputToken.equals(pool.token1), 'PATH')
const nextToken = currentInputToken.equals(pool.token0) ? pool.token1 : pool.token0
tokenPath.push(nextToken)
pools[0].token0.equals(this.adjustedInput) ? 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.token0.wrapped.equals(inputToken.wrapped) ? pool.token1 : pool.token0

invariant(isValidTokenPath(prevPool, pool, inputToken), 'PATH')
tokenPath.push(outputToken)
}

this.pools = pools
Expand Down
57 changes: 56 additions & 1 deletion sdks/router-sdk/src/entities/mixedRoute/trade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ describe('MixedRouteTrade', () => {
CurrencyAmount.fromRawAmount(WETH9[1], 130000),
true
)
const pool_v4_0_eth = v2StylePool(
CurrencyAmount.fromRawAmount(token0, 120000),
CurrencyAmount.fromRawAmount(ETHER, 130000),
true
)

const pool_v3_0_1 = v2StylePool(
CurrencyAmount.fromRawAmount(token0, 100000),
Expand Down Expand Up @@ -1381,7 +1386,7 @@ describe('MixedRouteTrade', () => {
})

describe('multihop v2 + v3 + v4', () => {
it('can be constructed with an eth output from a v4 pool', async () => {
it('can be constructed with a weth output from a v4 pool', async () => {
const trade = await MixedRouteTrade.fromRoute(
new MixedRouteSDK([pool_v3_0_1, pool_v4_0_weth], token1, WETH9[1]),
CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(100)),
Expand All @@ -1390,5 +1395,55 @@ describe('MixedRouteTrade', () => {
expect(trade.inputAmount.currency).toEqual(token1)
expect(trade.outputAmount.currency).toEqual(WETH9[1])
})

it('can be constructed with an eth output from a v4 pool', async () => {
const trade = await MixedRouteTrade.fromRoute(
new MixedRouteSDK([pool_v3_0_1, pool_v4_0_eth], token1, ETHER),
CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(100)),
TradeType.EXACT_INPUT
)
expect(trade.inputAmount.currency).toEqual(token1)
expect(trade.outputAmount.currency).toEqual(ETHER)
})

it('can be constructed with an eth output from a v4 weth pool', async () => {
const trade = await MixedRouteTrade.fromRoute(
new MixedRouteSDK([pool_v3_0_1, pool_v4_0_weth], token1, ETHER),
CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(100)),
TradeType.EXACT_INPUT
)
expect(trade.inputAmount.currency).toEqual(token1)
expect(trade.outputAmount.currency).toEqual(ETHER)
})

it('can be constructed with an intermediate conversion WETH->ETH when trading to v4 pool', async () => {
const trade = await MixedRouteTrade.fromRoute(
new MixedRouteSDK([pool_v3_weth_0, pool_v4_0_eth], token0, ETHER),
CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)),
TradeType.EXACT_INPUT
)
expect(trade.inputAmount.currency).toEqual(token0)
expect(trade.outputAmount.currency).toEqual(ETHER)
})

it('can be constructed with an intermediate conversion ETH->WETH when trading from a v4 pool', async () => {
const trade = await MixedRouteTrade.fromRoute(
new MixedRouteSDK([pool_v4_0_eth, pool_v3_weth_0], token0, WETH9[1]),
CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)),
TradeType.EXACT_INPUT
)
expect(trade.inputAmount.currency).toEqual(token0)
expect(trade.outputAmount.currency).toEqual(WETH9[1])
})

it('can be constructed with an intermediate conversion ETH->WETH when trading from a v4 pool with ETH output', async () => {
const trade = await MixedRouteTrade.fromRoute(
new MixedRouteSDK([pool_v4_0_eth, pool_v3_weth_0], token0, ETHER),
CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100)),
TradeType.EXACT_INPUT
)
expect(trade.inputAmount.currency).toEqual(token0)
expect(trade.outputAmount.currency).toEqual(ETHER)
})
})
})
9 changes: 7 additions & 2 deletions sdks/router-sdk/src/utils/getOutputAmount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ export async function getOutputAmount(
if (pool instanceof V4Pool) {
if (pool.involvesCurrency(amountIn.currency)) {
return await pool.getOutputAmount(amountIn)
} else if (pool.involvesCurrency(amountIn.currency.wrapped)) {
return await pool.getOutputAmount(amountIn.wrapped)
}
if (pool.token0.wrapped.equals(amountIn.currency)) {
return await pool.getOutputAmount(CurrencyAmount.fromRawAmount(pool.token0, amountIn.quotient))
}
if (pool.token1.wrapped.equals(amountIn.currency)) {
return await pool.getOutputAmount(CurrencyAmount.fromRawAmount(pool.token1, amountIn.quotient))
}
}

return await pool.getOutputAmount(amountIn.wrapped)
}
25 changes: 25 additions & 0 deletions sdks/router-sdk/src/utils/isValidTokenPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Currency, Token } from '@uniswap/sdk-core'
import { Pool as V4Pool } from '@uniswap/v4-sdk'
import { Pair } from '@uniswap/v2-sdk'
import { Pool as V3Pool } from '@uniswap/v3-sdk'

type TPool = Pair | V3Pool | V4Pool

export function isValidTokenPath(prevPool: TPool, currentPool: TPool, inputToken: Currency): boolean {
if (currentPool.involvesToken(inputToken as Token)) 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
}

0 comments on commit df6348a

Please sign in to comment.