diff --git a/.gitignore b/.gitignore index 8e7e999ac..3811e8cb8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ # dependencies node_modules + +# Intellij IDEA artifacts +.idea/ \ No newline at end of file diff --git a/sdks/router-sdk/src/constants.ts b/sdks/router-sdk/src/constants.ts index 4b246f089..d75fa6cd8 100644 --- a/sdks/router-sdk/src/constants.ts +++ b/sdks/router-sdk/src/constants.ts @@ -8,8 +8,17 @@ export const ADDRESS_THIS = '0x0000000000000000000000000000000000000002' export const ZERO = JSBI.BigInt(0) export const ONE = JSBI.BigInt(1) -// = 1 << 23 or 100000000000000000000000 -export const V2_FEE_PATH_PLACEHOLDER = 8388608 +// = 1 << 23 or 0b0100000000000000000000000 +export const MIXED_QUOTER_V1_V2_FEE_PATH_PLACEHOLDER = 1 << 23 + +// = 10 << 4 or 0b00100000 +export const MIXED_QUOTER_V2_V2_FEE_PATH_PLACEHOLDER = 2 << 4 + +// = 11 << 20 or 0b001100000000000000000000 +export const MIXED_QUOTER_V2_V3_FEE_PATH_PLACEHOLDER = 3 << 20 + +// = 100 << 20 or 0b010000000000000000000000 +export const MIXED_QUOTER_V2_V4_FEE_PATH_PLACEHOLDER = 4 << 20 export const ZERO_PERCENT = new Percent(ZERO) export const ONE_HUNDRED_PERCENT = new Percent(100, 100) diff --git a/sdks/router-sdk/src/utils/encodeMixedRouteToPath.test.ts b/sdks/router-sdk/src/utils/encodeMixedRouteToPath.test.ts index 0baadf57e..500f5ee18 100644 --- a/sdks/router-sdk/src/utils/encodeMixedRouteToPath.test.ts +++ b/sdks/router-sdk/src/utils/encodeMixedRouteToPath.test.ts @@ -20,6 +20,17 @@ describe('#encodeMixedRouteToPath', () => { const pool_V3_1_weth = new V3Pool(token1, weth, FeeAmount.MEDIUM, encodeSqrtRatioX96(1, 1), 0, 0, []) const pool_V4_0_1 = new V4Pool(token0, token1, FeeAmount.MEDIUM, 30, ADDRESS_ZERO, encodeSqrtRatioX96(1, 1), 0, 0, []) + const pool_V4_0_eth = new V4Pool( + token0, + ETHER, + FeeAmount.MEDIUM, + 30, + ADDRESS_ZERO, + encodeSqrtRatioX96(1, 1), + 0, + 0, + [] + ) const pair_0_1 = new Pair(CurrencyAmount.fromRawAmount(token0, '100'), CurrencyAmount.fromRawAmount(token1, '200')) const pair_1_2 = new Pair(CurrencyAmount.fromRawAmount(token1, '150'), CurrencyAmount.fromRawAmount(token2, '150')) @@ -47,6 +58,8 @@ describe('#encodeMixedRouteToPath', () => { const route_0_V3_weth_V2_1_V2_2 = new MixedRouteSDK([pool_V3_0_weth, pair_1_weth, pair_1_2], token0, token2) const route_0_V3_1_v3_weth_V2_2 = new MixedRouteSDK([pool_V3_0_1_medium, pool_V3_1_weth, pair_2_weth], token0, token2) const route_0_V3_weth_V4_1 = new MixedRouteSDK([pool_V3_0_weth, pool_V4_0_1], ETHER, token1) + const route_eth_V4_0_V3_1 = new MixedRouteSDK([pool_V4_0_eth, pool_V3_0_1_medium], ETHER, token1) + const route_eth_V3_0_V4_1 = new MixedRouteSDK([pool_V3_0_weth, pool_V4_0_1], ETHER, token1) describe('pure V3', () => { it('packs them for exact input single hop', () => { @@ -87,8 +100,10 @@ describe('#encodeMixedRouteToPath', () => { }) describe('pure v4', () => { - it('throws if MixedRouteSDK is a pure v4 route', () => { - expect(() => encodeMixedRouteToPath(route_0_V4_1)).toThrow('Encoding mixed routes with V4 not supported') + it('packs them for exact input single hop', () => { + expect(encodeMixedRouteToPath(route_0_V4_1)).toEqual( + '0x0000000000000000000000000000000000000001400bb800001e00000000000000000000000000000000000000000000000000000000000000000000000000000002' + ) }) }) @@ -149,8 +164,22 @@ describe('#encodeMixedRouteToPath', () => { ) }) - it('throws if it contains a v4 pool', () => { - expect(() => encodeMixedRouteToPath(route_0_V3_weth_V4_1)).toThrow('Encoding mixed routes with V4 not supported') + it('packs them for exact input v3 -> v4', () => { + expect(encodeMixedRouteToPath(route_0_V3_weth_V4_1)).toEqual( + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2300bb80000000000000000000000000000000000000001400bb800001e00000000000000000000000000000000000000000000000000000000000000000000000000000002' + ) + }) + + it('packs them for exact input native eth v4 -> v3', () => { + expect(encodeMixedRouteToPath(route_eth_V4_0_V3_1)).toEqual( + '0x0000000000000000000000000000000000000000400bb800001e00000000000000000000000000000000000000000000000000000000000000000000000000000001300bb80000000000000000000000000000000000000002' + ) + }) + + it('packs them for exact input native eth v3 -> v4', () => { + expect(encodeMixedRouteToPath(route_eth_V3_0_V4_1)).toEqual( + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2300bb80000000000000000000000000000000000000001400bb800001e00000000000000000000000000000000000000000000000000000000000000000000000000000002' + ) }) }) }) diff --git a/sdks/router-sdk/src/utils/encodeMixedRouteToPath.ts b/sdks/router-sdk/src/utils/encodeMixedRouteToPath.ts index 845403427..dabcf12b1 100644 --- a/sdks/router-sdk/src/utils/encodeMixedRouteToPath.ts +++ b/sdks/router-sdk/src/utils/encodeMixedRouteToPath.ts @@ -1,9 +1,16 @@ import { pack } from '@ethersproject/solidity' import { Currency } 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 { + ADDRESS_ZERO, + MIXED_QUOTER_V2_V2_FEE_PATH_PLACEHOLDER, + MIXED_QUOTER_V2_V3_FEE_PATH_PLACEHOLDER, + MIXED_QUOTER_V2_V4_FEE_PATH_PLACEHOLDER, + MIXED_QUOTER_V1_V2_FEE_PATH_PLACEHOLDER, +} from '../constants' import { MixedRouteSDK } from '../entities/mixedRoute/route' -import { V2_FEE_PATH_PLACEHOLDER } from '../constants' import { TPool } from './TPool' /** @@ -13,36 +20,81 @@ import { TPool } from './TPool' * @returns the exactIn encoded path */ export function encodeMixedRouteToPath(route: MixedRouteSDK): string { - const firstInputToken: Currency = route.input.wrapped - - const { path, types } = route.pools.reduce( - ( - { inputToken, path, types }: { inputToken: Currency; path: (string | number)[]; types: string[] }, - pool: TPool, - index - ): { inputToken: Currency; path: (string | number)[]; types: string[] } => { - if (pool instanceof V4Pool) throw 'Encoding mixed routes with V4 not supported' - const outputToken: Currency = pool.token0.equals(inputToken) ? pool.token1 : pool.token0 - if (index === 0) { - return { - inputToken: outputToken, - types: ['address', 'uint24', 'address'], - path: [ - inputToken.wrapped.address, - pool instanceof V3Pool ? pool.fee : V2_FEE_PATH_PLACEHOLDER, - outputToken.wrapped.address, - ], - } + const containsV4Pool = route.pools.some((pool) => pool instanceof V4Pool) + + let path: (string | number)[] + let types: string[] + + if (containsV4Pool) { + path = [route.adjustedInput.isNative ? ADDRESS_ZERO : route.adjustedInput.address] + types = ['address'] + let currencyIn = route.adjustedInput + + for (const pool of route.pools) { + const currencyOut = currencyIn.equals(pool.token0) ? pool.token1 : pool.token0 + + if (pool instanceof V4Pool) { + const v4Fee = pool.fee + MIXED_QUOTER_V2_V4_FEE_PATH_PLACEHOLDER + path.push( + v4Fee, + pool.tickSpacing, + pool.hooks, + currencyOut.isNative ? ADDRESS_ZERO : currencyOut.wrapped.address + ) + types.push('uint24', 'uint24', 'address', 'address') + } else if (pool instanceof V3Pool) { + const v3Fee = pool.fee + MIXED_QUOTER_V2_V3_FEE_PATH_PLACEHOLDER + path.push(v3Fee, currencyOut.wrapped.address) + types.push('uint24', 'address') + } else if (pool instanceof Pair) { + const v2Fee = MIXED_QUOTER_V2_V2_FEE_PATH_PLACEHOLDER + path.push(v2Fee, currencyOut.wrapped.address) + types.push('uint8', 'address') } else { - return { - inputToken: outputToken, - types: [...types, 'uint24', 'address'], - path: [...path, pool instanceof V3Pool ? pool.fee : V2_FEE_PATH_PLACEHOLDER, outputToken.wrapped.address], - } + throw new Error(`Unsupported pool type ${JSON.stringify(pool)}`) } - }, - { inputToken: firstInputToken, path: [], types: [] } - ) + + currencyIn = currencyOut + } + } else { + // TODO: ROUTE-276 - delete this else block + // We introduced this else block as a safety measure to prevent non-v4 mixed routes from potentially regressing + // We'd like to gain more confidence in the new implementation before removing this block + const result = route.pools.reduce( + ( + { inputToken, path, types }: { inputToken: Currency; path: (string | number)[]; types: string[] }, + pool: TPool, + index + ): { inputToken: Currency; path: (string | number)[]; types: string[] } => { + const outputToken: Currency = pool.token0.equals(inputToken) ? pool.token1 : pool.token0 + if (index === 0) { + return { + inputToken: outputToken, + types: ['address', 'uint24', 'address'], + path: [ + inputToken.wrapped.address, + pool instanceof V3Pool ? pool.fee : MIXED_QUOTER_V1_V2_FEE_PATH_PLACEHOLDER, + outputToken.wrapped.address, + ], + } + } else { + return { + inputToken: outputToken, + types: [...types, 'uint24', 'address'], + path: [ + ...path, + pool instanceof V3Pool ? pool.fee : MIXED_QUOTER_V1_V2_FEE_PATH_PLACEHOLDER, + outputToken.wrapped.address, + ], + } + } + }, + { inputToken: route.input, path: [], types: [] } + ) + + path = result.path + types = result.types + } return pack(types, path) }