From df6348ad13ba99823758949589e8ec3a8c6c281c Mon Sep 17 00:00:00 2001 From: Emily Williams Date: Thu, 5 Sep 2024 10:29:26 -0400 Subject: [PATCH] fix(router-sdk): Address wrapped/native currency conditions in mixed routes with v4 (#81) --- .../src/entities/mixedRoute/route.test.ts | 28 ++++++++- .../src/entities/mixedRoute/route.ts | 16 ++++-- .../src/entities/mixedRoute/trade.test.ts | 57 ++++++++++++++++++- sdks/router-sdk/src/utils/getOutputAmount.ts | 9 ++- sdks/router-sdk/src/utils/isValidTokenPath.ts | 25 ++++++++ 5 files changed, 126 insertions(+), 9 deletions(-) create mode 100644 sdks/router-sdk/src/utils/isValidTokenPath.ts diff --git a/sdks/router-sdk/src/entities/mixedRoute/route.test.ts b/sdks/router-sdk/src/entities/mixedRoute/route.test.ts index c38cb0a3f..d650eee1d 100644 --- a/sdks/router-sdk/src/entities/mixedRoute/route.test.ts +++ b/sdks/router-sdk/src/entities/mixedRoute/route.test.ts @@ -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]) @@ -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]) diff --git a/sdks/router-sdk/src/entities/mixedRoute/route.ts b/sdks/router-sdk/src/entities/mixedRoute/route.ts index e7d31c2dd..a8dcfa2cc 100644 --- a/sdks/router-sdk/src/entities/mixedRoute/route.ts +++ b/sdks/router-sdk/src/entities/mixedRoute/route.ts @@ -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 @@ -52,11 +53,16 @@ export class MixedRouteSDK { * 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 diff --git a/sdks/router-sdk/src/entities/mixedRoute/trade.test.ts b/sdks/router-sdk/src/entities/mixedRoute/trade.test.ts index 6697ffc3b..b27ff008b 100644 --- a/sdks/router-sdk/src/entities/mixedRoute/trade.test.ts +++ b/sdks/router-sdk/src/entities/mixedRoute/trade.test.ts @@ -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), @@ -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)), @@ -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) + }) }) }) diff --git a/sdks/router-sdk/src/utils/getOutputAmount.ts b/sdks/router-sdk/src/utils/getOutputAmount.ts index ee751c197..c9abf7b68 100644 --- a/sdks/router-sdk/src/utils/getOutputAmount.ts +++ b/sdks/router-sdk/src/utils/getOutputAmount.ts @@ -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) } diff --git a/sdks/router-sdk/src/utils/isValidTokenPath.ts b/sdks/router-sdk/src/utils/isValidTokenPath.ts new file mode 100644 index 000000000..af71b9764 --- /dev/null +++ b/sdks/router-sdk/src/utils/isValidTokenPath.ts @@ -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 +}