From 75c5ffe87e75b76a002ff4d02464fc0452d7f250 Mon Sep 17 00:00:00 2001 From: Emily Williams Date: Wed, 28 Aug 2024 15:35:28 -0400 Subject: [PATCH 1/5] Address wrapped/native currency conditions in mixed routes with v4 --- .../src/entities/mixedRoute/route.test.ts | 28 ++++++++- .../src/entities/mixedRoute/route.ts | 16 +++-- .../src/entities/mixedRoute/trade.test.ts | 62 ++++++++++++++++++- sdks/router-sdk/src/utils/getOutputAmount.ts | 10 ++- sdks/router-sdk/src/utils/isValidTokenPath.ts | 25 ++++++++ 5 files changed, 133 insertions(+), 8 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..7f670f009 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 == 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..9ab5f0b4e 100644 --- a/sdks/router-sdk/src/entities/mixedRoute/trade.test.ts +++ b/sdks/router-sdk/src/entities/mixedRoute/trade.test.ts @@ -57,6 +57,16 @@ 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_v4_1_eth = v2StylePool( + // CurrencyAmount.fromRawAmount(token1, 120000), + // CurrencyAmount.fromRawAmount(ETHER, 130000), + // true + // ) const pool_v3_0_1 = v2StylePool( CurrencyAmount.fromRawAmount(token0, 100000), @@ -1381,7 +1391,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 +1400,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..228476fd1 100644 --- a/sdks/router-sdk/src/utils/getOutputAmount.ts +++ b/sdks/router-sdk/src/utils/getOutputAmount.ts @@ -12,9 +12,17 @@ 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)) { + } + 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..58dd36865 --- /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 +} From 3025d642fd311dd841d7e088ff64d61bd0e17da7 Mon Sep 17 00:00:00 2001 From: Emily Williams Date: Wed, 28 Aug 2024 15:41:04 -0400 Subject: [PATCH 2/5] take out incorrect condition --- sdks/router-sdk/src/utils/getOutputAmount.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/sdks/router-sdk/src/utils/getOutputAmount.ts b/sdks/router-sdk/src/utils/getOutputAmount.ts index 228476fd1..c9abf7b68 100644 --- a/sdks/router-sdk/src/utils/getOutputAmount.ts +++ b/sdks/router-sdk/src/utils/getOutputAmount.ts @@ -13,9 +13,6 @@ export async function getOutputAmount( if (pool.involvesCurrency(amountIn.currency)) { return await pool.getOutputAmount(amountIn) } - 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)) } From d6636dcf6c0d0141454ebddfbae68001ae64ef2f Mon Sep 17 00:00:00 2001 From: Emily Williams Date: Wed, 28 Aug 2024 15:41:16 -0400 Subject: [PATCH 3/5] lint --- sdks/router-sdk/src/entities/mixedRoute/route.ts | 2 +- sdks/router-sdk/src/utils/isValidTokenPath.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdks/router-sdk/src/entities/mixedRoute/route.ts b/sdks/router-sdk/src/entities/mixedRoute/route.ts index 7f670f009..0b3f1a1b6 100644 --- a/sdks/router-sdk/src/entities/mixedRoute/route.ts +++ b/sdks/router-sdk/src/entities/mixedRoute/route.ts @@ -56,7 +56,7 @@ export class MixedRouteSDK { pools[0].token0 == 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 prevPool = pools[i - 1] const pool = pools[i] const inputToken = tokenPath[i] const outputToken = pool.token0.wrapped.equals(inputToken.wrapped) ? pool.token1 : pool.token0 diff --git a/sdks/router-sdk/src/utils/isValidTokenPath.ts b/sdks/router-sdk/src/utils/isValidTokenPath.ts index 58dd36865..af71b9764 100644 --- a/sdks/router-sdk/src/utils/isValidTokenPath.ts +++ b/sdks/router-sdk/src/utils/isValidTokenPath.ts @@ -12,7 +12,7 @@ export function isValidTokenPath(prevPool: TPool, currentPool: TPool, inputToken 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 instanceof V4Pool) { if (currentPool.token0.wrapped.equals(inputToken) || currentPool.token1.wrapped.equals(inputToken)) return true } From 2c80b9f8576c358d0e26f43889873bdb531e5159 Mon Sep 17 00:00:00 2001 From: Emily Williams Date: Wed, 28 Aug 2024 15:49:35 -0400 Subject: [PATCH 4/5] equals --- sdks/router-sdk/src/entities/mixedRoute/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/router-sdk/src/entities/mixedRoute/route.ts b/sdks/router-sdk/src/entities/mixedRoute/route.ts index 0b3f1a1b6..a8dcfa2cc 100644 --- a/sdks/router-sdk/src/entities/mixedRoute/route.ts +++ b/sdks/router-sdk/src/entities/mixedRoute/route.ts @@ -53,7 +53,7 @@ export class MixedRouteSDK { * Normalizes token0-token1 order and selects the next token/fee step to add to the path * */ const tokenPath: Currency[] = [this.adjustedInput] - pools[0].token0 == this.adjustedInput ? tokenPath.push(pools[0].token1) : tokenPath.push(pools[0].token0) + 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] From f52d5ffd5184efd89bcd9b2ff7584da0e37af693 Mon Sep 17 00:00:00 2001 From: Emily Williams Date: Thu, 29 Aug 2024 14:02:40 -0400 Subject: [PATCH 5/5] get rid of code comment --- sdks/router-sdk/src/entities/mixedRoute/trade.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/sdks/router-sdk/src/entities/mixedRoute/trade.test.ts b/sdks/router-sdk/src/entities/mixedRoute/trade.test.ts index 9ab5f0b4e..b27ff008b 100644 --- a/sdks/router-sdk/src/entities/mixedRoute/trade.test.ts +++ b/sdks/router-sdk/src/entities/mixedRoute/trade.test.ts @@ -62,11 +62,6 @@ describe('MixedRouteTrade', () => { CurrencyAmount.fromRawAmount(ETHER, 130000), true ) - // const pool_v4_1_eth = v2StylePool( - // CurrencyAmount.fromRawAmount(token1, 120000), - // CurrencyAmount.fromRawAmount(ETHER, 130000), - // true - // ) const pool_v3_0_1 = v2StylePool( CurrencyAmount.fromRawAmount(token0, 100000),