From 8848f727e5d24c0a54acba9148c07a5c83391282 Mon Sep 17 00:00:00 2001 From: Emily Williams Date: Mon, 30 Sep 2024 09:32:05 -0400 Subject: [PATCH] fix(universal-router-sdk): Add V4 Exact-In Swaps (#111) Co-authored-by: gretzke Co-authored-by: Cody Born Co-authored-by: Alice Henshaw Co-authored-by: Mark Toda --- sdks/universal-router-sdk/package.json | 4 +- .../src/entities/actions/uniswap.ts | 77 +++++- .../src/utils/routerCommands.ts | 5 + .../test/forge/SwapERC20CallParameters.t.sol | 92 +++++++ .../test/forge/interop.json | 24 ++ .../test/forge/utils/DeployRouter.sol | 87 ++++++- .../test/uniswapTrades.test.ts | 241 +++++++++++++++--- .../test/utils/uniswapData.ts | 11 +- yarn.lock | 22 +- 9 files changed, 510 insertions(+), 53 deletions(-) diff --git a/sdks/universal-router-sdk/package.json b/sdks/universal-router-sdk/package.json index b32e46132..70dcd2228 100644 --- a/sdks/universal-router-sdk/package.json +++ b/sdks/universal-router-sdk/package.json @@ -31,14 +31,14 @@ "dependencies": { "@openzeppelin/contracts": "4.7.0", "@uniswap/permit2-sdk": "^1.3.0", - "@uniswap/router-sdk": "^1.11.0", + "@uniswap/router-sdk": "^1.12.1", "@uniswap/sdk-core": "^5.3.1", "@uniswap/universal-router": "2.0.0-beta.1", "@uniswap/v2-core": "^1.0.1", "@uniswap/v2-sdk": "^4.4.1", "@uniswap/v3-core": "1.0.0", "@uniswap/v3-sdk": "^3.13.1", - "@uniswap/v4-sdk": "^1.0.0", + "@uniswap/v4-sdk": "^1.6.3", "bignumber.js": "^9.0.2", "ethers": "^5.7.0" }, diff --git a/sdks/universal-router-sdk/src/entities/actions/uniswap.ts b/sdks/universal-router-sdk/src/entities/actions/uniswap.ts index b45484c97..8ca7e4159 100644 --- a/sdks/universal-router-sdk/src/entities/actions/uniswap.ts +++ b/sdks/universal-router-sdk/src/entities/actions/uniswap.ts @@ -1,7 +1,7 @@ import { RoutePlanner, CommandType } from '../../utils/routerCommands' import { Trade as V2Trade, Pair } from '@uniswap/v2-sdk' import { Trade as V3Trade, Pool as V3Pool, encodeRouteToPath } from '@uniswap/v3-sdk' -import { Pool as V4Pool } from '@uniswap/v4-sdk' +import { Trade as V4Trade, V4Planner } from '@uniswap/v4-sdk' import { Trade as RouterTrade, MixedRouteTrade, @@ -9,6 +9,7 @@ import { IRoute, RouteV2, RouteV3, + RouteV4, MixedRouteSDK, MixedRoute, SwapOptions as RouterSwapOptions, @@ -22,6 +23,7 @@ import { Command, RouterActionType, TradeConfig } from '../Command' import { SENDER_AS_RECIPIENT, ROUTER_AS_RECIPIENT, CONTRACT_BALANCE, ETH_ADDRESS } from '../../utils/constants' import { encodeFeeBips } from '../../utils/numbers' import { BigNumber, BigNumberish } from 'ethers' +import { TPool } from '@uniswap/router-sdk/dist/utils/TPool' export type FlatFeeOptions = { amount: BigNumberish @@ -42,7 +44,7 @@ export type SwapOptions = Omit & { const REFUND_ETH_PRICE_IMPACT_THRESHOLD = new Percent(50, 100) interface Swap { - route: IRoute + route: IRoute inputAmount: CurrencyAmount outputAmount: CurrencyAmount } @@ -60,8 +62,31 @@ export class UniswapTrade implements Command { else this.payerIsUser = true } + get isAllV4(): boolean { + let result = true + for (const swap of this.trade.swaps) { + result = result && swap.route.protocol == Protocol.V4 + } + return result + } + get inputRequiresWrap(): boolean { - return this.trade.inputAmount.currency.isNative + if (!this.isAllV4) { + return this.trade.inputAmount.currency.isNative + } else { + // We only support wrapping all ETH or no ETH currently. We cannot support splitting where half needs to be wrapped + // If the input currency is ETH and the input of the first path is not ETH it must be WETH that needs wrapping + return this.trade.inputAmount.currency.isNative && !this.trade.swaps[0].route.input.isNative + } + } + + get outputRequiresUnwrap(): boolean { + if (!this.isAllV4) { + return this.trade.outputAmount.currency.isNative + } else { + // If the output currency is ETH and the output of the swap is not ETH it must be WETH that needs unwrapping + return this.trade.outputAmount.currency.isNative && !this.trade.swaps[0].route.output.isNative + } } encode(planner: RoutePlanner, _config: TradeConfig): void { @@ -82,8 +107,7 @@ export class UniswapTrade implements Command { // in that the reversion probability is lower const performAggregatedSlippageCheck = this.trade.tradeType === TradeType.EXACT_INPUT && this.trade.routes.length > 2 - const outputIsNative = this.trade.outputAmount.currency.isNative - const routerMustCustody = performAggregatedSlippageCheck || outputIsNative || hasFeeOption(this.options) + const routerMustCustody = performAggregatedSlippageCheck || this.outputRequiresUnwrap || hasFeeOption(this.options) for (const swap of this.trade.swaps) { switch (swap.route.protocol) { @@ -93,6 +117,17 @@ export class UniswapTrade implements Command { case Protocol.V3: addV3Swap(planner, swap, this.trade.tradeType, this.options, this.payerIsUser, routerMustCustody) break + case Protocol.V4: + addV4Swap( + planner, + swap, + this.trade.tradeType, + this.options, + this.payerIsUser, + routerMustCustody, + performAggregatedSlippageCheck + ) + break case Protocol.MIXED: addMixedSwap(planner, swap, this.trade.tradeType, this.options, this.payerIsUser, routerMustCustody) break @@ -145,7 +180,7 @@ export class UniswapTrade implements Command { // The remaining tokens that need to be sent to the user after the fee is taken will be caught // by this if-else clause. - if (outputIsNative) { + if (this.outputRequiresUnwrap) { planner.addCommand(CommandType.UNWRAP_WETH, [this.options.recipient, minimumAmountOut]) } else { planner.addCommand(CommandType.SWEEP, [ @@ -238,6 +273,36 @@ function addV3Swap( } } +function addV4Swap( + planner: RoutePlanner, + { route, inputAmount, outputAmount }: Swap, + tradeType: TradeType, + options: SwapOptions, + payerIsUser: boolean, + routerMustCustody: boolean, + performAggregatedSlippageCheck: boolean +): void { + const trade = V4Trade.createUncheckedTrade({ + route: route as RouteV4, + inputAmount, + outputAmount, + tradeType, + }) + const slippageToleranceOnSwap = performAggregatedSlippageCheck ? undefined : options.slippageTolerance + + const inputWethFromRouter = inputAmount.currency.isNative && !route.input.isNative + if (inputWethFromRouter && !payerIsUser) throw new Error('Inconsistent payer') + + const v4Planner = new V4Planner() + v4Planner.addTrade(trade, slippageToleranceOnSwap) + v4Planner.addSettle(inputWethFromRouter ? inputAmount.currency.wrapped : inputAmount.currency, payerIsUser) + + options.recipient = options.recipient ?? SENDER_AS_RECIPIENT + v4Planner.addTake(outputAmount.currency, routerMustCustody ? ROUTER_AS_RECIPIENT : options.recipient) + + planner.addCommand(CommandType.V4_SWAP, [v4Planner.finalize()]) +} + // encode a mixed route swap, i.e. including both v2 and v3 pools function addMixedSwap( planner: RoutePlanner, diff --git a/sdks/universal-router-sdk/src/utils/routerCommands.ts b/sdks/universal-router-sdk/src/utils/routerCommands.ts index 0000042d9..384dfd5a0 100644 --- a/sdks/universal-router-sdk/src/utils/routerCommands.ts +++ b/sdks/universal-router-sdk/src/utils/routerCommands.ts @@ -22,6 +22,7 @@ export enum CommandType { PERMIT2_TRANSFER_FROM_BATCH = 0x0d, BALANCE_CHECK_ERC20 = 0x0e, + V4_SWAP = 0x10, V3_POSITION_MANAGER_PERMIT = 0x11, V3_POSITION_MANAGER_CALL = 0x12, V4_POSITION_CALL = 0x13, @@ -56,6 +57,7 @@ const ABI_DEFINITION: { [key in CommandType]: string[] } = { [CommandType.V3_SWAP_EXACT_OUT]: ['address', 'uint256', 'uint256', 'bytes', 'bool'], [CommandType.V2_SWAP_EXACT_IN]: ['address', 'uint256', 'uint256', 'address[]', 'bool'], [CommandType.V2_SWAP_EXACT_OUT]: ['address', 'uint256', 'uint256', 'address[]', 'bool'], + [CommandType.V4_SWAP]: ['bytes'], // Token Actions and Checks [CommandType.WRAP_ETH]: ['address', 'uint256'], @@ -104,6 +106,9 @@ export type RouterCommand = { } export function createCommand(type: CommandType, parameters: any[]): RouterCommand { + if (type === CommandType.V4_SWAP) { + return { type, encodedInput: parameters[0] } + } const encodedInput = defaultAbiCoder.encode(ABI_DEFINITION[type], parameters) return { type, encodedInput } } diff --git a/sdks/universal-router-sdk/test/forge/SwapERC20CallParameters.t.sol b/sdks/universal-router-sdk/test/forge/SwapERC20CallParameters.t.sol index e4ef24c63..a77aa2b2c 100644 --- a/sdks/universal-router-sdk/test/forge/SwapERC20CallParameters.t.sol +++ b/sdks/universal-router-sdk/test/forge/SwapERC20CallParameters.t.sol @@ -26,6 +26,8 @@ contract SwapERC20CallParametersTest is Test, Interop, DeployRouter { json = vm.readFile(string.concat(root, "/test/forge/interop.json")); vm.createSelectFork(vm.envString("FORK_URL"), 16075500); + deployV4Contracts(); + initializeV4Pools(WETH, USDC, DAI); vm.startPrank(from); deployRouterAndPermit2(); vm.deal(from, BALANCE); @@ -531,6 +533,96 @@ contract SwapERC20CallParametersTest is Test, Interop, DeployRouter { assertEq(address(router).balance, 0); } + function testV4ExactInputETH() public { + MethodParameters memory params = readFixture(json, "._UNISWAP_V4_1_ETH_FOR_USDC"); + assertEq(from.balance, BALANCE); + assertEq(USDC.balanceOf(RECIPIENT), 0); + assertEq(params.value, 1e18); + + (bool success,) = address(router).call{value: params.value}(params.data); + require(success, "call failed"); + + assertLe(from.balance, BALANCE - params.value); + assertGt(USDC.balanceOf(RECIPIENT), 2000 * ONE_USDC); + } + + // v4-sdk 1.6.3 allows this + // function testV4ExactInputEthWithWrap() public { + // MethodParameters memory params = readFixture(json, "._UNISWAP_V4_1_ETH_FOR_USDC_WITH_WRAP"); + // } + + function testV4ExactInWithFee() public { + MethodParameters memory params = readFixture(json, "._UNISWAP_V4_1_ETH_FOR_USDC_WITH_FEE"); + + assertEq(from.balance, BALANCE); + assertEq(USDC.balanceOf(RECIPIENT), 0); + assertEq(USDC.balanceOf(FEE_RECIPIENT), 0); + + (bool success,) = address(router).call{value: params.value}(params.data); + require(success, "call failed"); + assertLe(from.balance, BALANCE - params.value); + + uint256 recipientBalance = USDC.balanceOf(RECIPIENT); + uint256 feeRecipientBalance = USDC.balanceOf(FEE_RECIPIENT); + uint256 totalOut = recipientBalance + feeRecipientBalance; + uint256 expectedFee = totalOut * 500 / 10000; + assertEq(feeRecipientBalance, expectedFee, "Fee received"); + assertEq(recipientBalance, totalOut - expectedFee, "User output"); + assertGt(totalOut, 1000 * ONE_USDC, "Slippage"); + } + + function testV4ExactInWithFlatFee() public { + MethodParameters memory params = readFixture(json, "._UNISWAP_V4_1_ETH_FOR_USDC_WITH_FLAT_FEE"); + assertEq(from.balance, BALANCE); + assertEq(USDC.balanceOf(RECIPIENT), 0); + assertEq(USDC.balanceOf(FEE_RECIPIENT), 0); + + (bool success,) = address(router).call{value: params.value}(params.data); + require(success, "call failed"); + assertLe(from.balance, BALANCE - params.value); + + uint256 recipientBalance = USDC.balanceOf(RECIPIENT); + uint256 feeRecipientBalance = USDC.balanceOf(FEE_RECIPIENT); + uint256 totalOut = recipientBalance + feeRecipientBalance; + uint256 expectedFee = 50 * ONE_USDC; + assertEq(feeRecipientBalance, expectedFee); + assertEq(recipientBalance, totalOut - expectedFee); + assertGt(totalOut, 1000 * ONE_USDC); + } + + function testV4ExactInNativeOutput() public { + MethodParameters memory params = readFixture(json, "._UNISWAP_V4_1000_USDC_FOR_ETH"); + + deal(address(USDC), from, BALANCE); + USDC.approve(address(permit2), BALANCE); + permit2.approve(address(USDC), address(router), uint160(BALANCE), uint48(block.timestamp + 1000)); + assertEq(USDC.balanceOf(from), BALANCE); + uint256 startingRecipientBalance = from.balance; + + (bool success,) = address(router).call{value: params.value}(params.data); + require(success, "call failed"); + + assertEq(USDC.balanceOf(from), BALANCE - 1000 * ONE_USDC); + assertGe(from.balance, startingRecipientBalance); + } + + // v4-sdk 1.6.3 allows this + // function testV4ExactInNativeOutputWithUnwrap() public { + // MethodParameters memory params = readFixture(json, "._UNISWAP_V4_1000_USDC_FOR_ETH_WITH_UNWRAP"); + // } + + function testV4ExactInMultiHop() public { + MethodParameters memory params = readFixture(json, "._UNISWAP_V4_ETH_FOR_DAI"); + + assertEq(from.balance, BALANCE); + assertEq(DAI.balanceOf(RECIPIENT), 0); + + (bool success,) = address(router).call{value: params.value}(params.data); + require(success, "call failed"); + assertLe(from.balance, BALANCE - params.value); + assertGt(DAI.balanceOf(RECIPIENT), 9 * ONE_DAI / 10); + } + function testMixedExactInputNative() public { MethodParameters memory params = readFixture(json, "._UNISWAP_MIXED_1_ETH_FOR_DAI"); diff --git a/sdks/universal-router-sdk/test/forge/interop.json b/sdks/universal-router-sdk/test/forge/interop.json index 383729b4c..f0662e459 100644 --- a/sdks/universal-router-sdk/test/forge/interop.json +++ b/sdks/universal-router-sdk/test/forge/interop.json @@ -246,5 +246,29 @@ "_UNISWAP_V3_ETH_FOR_DAI_SAFE_MODE": { "calldata": "0x24856bc30000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000030b000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000120000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000003eb3459f0ce6ae000b00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480001f46b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000000000000000000", "value": "1000000000000000000" + }, + "_UNISWAP_V4_1_ETH_FOR_USDC": { + "calldata": "0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003050912000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000d2d62035241defd00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000000000000000000", + "value": "1000000000000000000" + }, + "_UNISWAP_V4_1_ETH_FOR_USDC_WITH_FEE": { + "calldata": "0x24856bc3000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003100604000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000044000000000000000000000000000000000000000000000000000000000000004c000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003050912000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000c940c1a716d6c2000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000000000000000000000000000000000000000000000000000000001f40000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000c940c1a716d6c20", + "value": "1000000000000000000" + }, + "_UNISWAP_V4_1_ETH_FOR_USDC_WITH_FLAT_FEE": { + "calldata": "0x24856bc3000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003100504000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000044000000000000000000000000000000000000000000000000000000000000004c000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003050912000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000d2d62035241defd00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb0000000000000000000000000000000000000000000000000000000002faf0800000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000d2d62035241defd", + "value": "1000000000000000000" + }, + "_UNISWAP_V4_1000_USDC_FOR_ETH": { + "calldata": "0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003050912000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000003b9aca0000000000000000000000000000000000000000000000000000000000389895600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000000000000000000", + "value": "0" + }, + "_UNISWAP_V4_ETH_FOR_DAI": { + "calldata": "0x24856bc3000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002100400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000004a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000030509120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000d2342662b64fb0c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000000000000000000", + "value": "1000000000000000000" + }, + "_UNISWAP_V4_DAI_FOR_1_ETH_2_HOP": { + "calldata": "0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004a00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000030709120000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000ea8d710c6670fe50000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000bb8000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000000000000000000", + "value": "0" } } diff --git a/sdks/universal-router-sdk/test/forge/utils/DeployRouter.sol b/sdks/universal-router-sdk/test/forge/utils/DeployRouter.sol index 409666a07..0d0e20aa9 100644 --- a/sdks/universal-router-sdk/test/forge/utils/DeployRouter.sol +++ b/sdks/universal-router-sdk/test/forge/utils/DeployRouter.sol @@ -3,11 +3,22 @@ pragma solidity ^0.8.26; import {console2} from "forge-std/console2.sol"; import {Test} from "forge-std/Test.sol"; +import {ERC20} from "solmate/src/tokens/ERC20.sol"; import {UniversalRouter} from "universal-router/UniversalRouter.sol"; +import {PoolManager} from "v4-core/PoolManager.sol"; +import {IERC20Minimal} from "v4-core/interfaces/external/IERC20Minimal.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "v4-core/types/PoolId.sol"; +import {Currency} from "v4-core/types/Currency.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {IHooks} from "v4-core/interfaces/IHooks.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; import {RouterParameters} from "universal-router/base/RouterImmutables.sol"; import {IPermit2} from "permit2/src/interfaces/IPermit2.sol"; contract DeployRouter is Test { + using PoolIdLibrary for PoolKey; + address public constant V2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; address public constant V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; bytes32 public constant PAIR_INIT_CODE_HASH = 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f; @@ -25,6 +36,7 @@ contract DeployRouter is Test { UniversalRouter public router; IPermit2 public permit2; + IPoolManager public poolManager; address from; uint256 fromPrivateKey; @@ -39,7 +51,7 @@ contract DeployRouter is Test { v3Factory: V3_FACTORY, pairInitCodeHash: PAIR_INIT_CODE_HASH, poolInitCodeHash: POOL_INIT_CODE_HASH, - v4PoolManager: V4_POOL_MANAGER_PLACEHOLDER, + v4PoolManager: address(poolManager), v3NFTPositionManager: V3_POSITION_MANAGER, v4PositionManager: V4_POSITION_MANAGER_PLACEHOLDER }) @@ -52,6 +64,77 @@ contract DeployRouter is Test { sstore(permit2.slot, create(0, add(bytecode, 0x20), mload(bytecode))) } deployRouter(address(permit2)); - require(FORGE_ROUTER_ADDRESS == address(router)); + require(FORGE_ROUTER_ADDRESS == address(router), "Invalid Router Address"); + } + + //////////////////////////////////////////////////////////////// + //////////////////////// V4 SETUP ////////////////////////////// + //////////////////////////////////////////////////////////////// + + function deployV4Contracts() public { + poolManager = new PoolManager(); + } + + function initializeV4Pools(ERC20 WETH, ERC20 USDC, ERC20 DAI) public { + Currency eth = Currency.wrap(address(0)); + Currency weth = Currency.wrap(address(WETH)); + Currency usdc = Currency.wrap(address(USDC)); + Currency dai = Currency.wrap(address(DAI)); + + uint256 amount = 10000 ether; + + deal(address(USDC), address(this), amount); + USDC.approve(address(poolManager), amount); + + deal(address(WETH), address(this), amount); + WETH.approve(address(poolManager), amount); + + deal(address(DAI), address(this), amount); + DAI.approve(address(poolManager), amount); + + vm.deal(address(this), amount * 2); + + poolManager.unlock( + abi.encode( + [ + PoolKey(eth, usdc, 3000, 60, IHooks(address(0))), + PoolKey(dai, usdc, 3000, 60, IHooks(address(0))), + PoolKey(usdc, weth, 3000, 60, IHooks(address(0))) + ] + ) + ); + } + + function unlockCallback(bytes calldata data) external returns (bytes memory) { + PoolKey[3] memory poolKeys = abi.decode(data, (PoolKey[3])); + + for (uint256 i = 0; i < poolKeys.length; i++) { + PoolKey memory poolKey = poolKeys[i]; + poolManager.initialize(poolKey, 79228162514264337593543950336, bytes("")); + + (BalanceDelta delta, BalanceDelta feesAccrued) = poolManager.modifyLiquidity( + poolKey, + IPoolManager.ModifyLiquidityParams({ + tickLower: -60, + tickUpper: 60, + liquidityDelta: 1000000 ether, + salt: 0 + }), + bytes("") + ); + + settle(poolKey.currency0, uint256((uint128(-delta.amount0())))); + settle(poolKey.currency1, uint256((uint128(-delta.amount1())))); + } + } + + function settle(Currency currency, uint256 amount) internal { + if (currency.isAddressZero()) { + poolManager.settle{value: amount}(); + } else { + poolManager.sync(currency); + IERC20Minimal(Currency.unwrap(currency)).transfer(address(poolManager), amount); + poolManager.settle(); + } } } diff --git a/sdks/universal-router-sdk/test/uniswapTrades.test.ts b/sdks/universal-router-sdk/test/uniswapTrades.test.ts index e637ee961..5aa8812e5 100644 --- a/sdks/universal-router-sdk/test/uniswapTrades.test.ts +++ b/sdks/universal-router-sdk/test/uniswapTrades.test.ts @@ -5,7 +5,16 @@ import { expandTo18Decimals } from '../src/utils/numbers' import { SwapRouter, UniswapTrade, FlatFeeOptions } from '../src' import { MixedRouteTrade, MixedRouteSDK } from '@uniswap/router-sdk' import { Trade as V2Trade, Pair, Route as RouteV2 } from '@uniswap/v2-sdk' -import { Trade as V3Trade, Route as RouteV3, Pool, FeeOptions } from '@uniswap/v3-sdk' +import { + Trade as V3Trade, + Route as V3Route, + Pool as V3Pool, + FeeOptions, + encodeSqrtRatioX96, + nearestUsableTick, + TickMath, +} from '@uniswap/v3-sdk' +import { Pool as V4Pool, Route as V4Route, Trade as V4Trade } from '@uniswap/v4-sdk' import { generatePermitSignature, toInputPermit, makePermit, generateEip2098PermitSignature } from './utils/permit2' import { CurrencyAmount, Ether, Percent, Token, TradeType } from '@uniswap/sdk-core' import { registerFixture } from './forge/writeInterop' @@ -19,7 +28,7 @@ import { V2PoolInRoute, V3PoolInRoute, } from '../src/utils/routerTradeAdapter' -import { E_ETH_ADDRESS, ETH_ADDRESS } from '../src/utils/constants' +import { E_ETH_ADDRESS, ETH_ADDRESS, ZERO_ADDRESS } from '../src/utils/constants' const FORK_BLOCK = 16075500 @@ -29,14 +38,68 @@ describe('Uniswap', () => { const wallet = new Wallet(utils.zeroPad('0x1234', 32)) let WETH_USDC_V2: Pair let USDC_DAI_V2: Pair - let WETH_USDC_V3: Pool - let WETH_USDC_V3_LOW_FEE: Pool - let USDC_DAI_V3: Pool + let WETH_USDC_V3: V3Pool + let WETH_USDC_V3_LOW_FEE: V3Pool + let USDC_DAI_V3: V3Pool + let ETH_USDC_V4: V4Pool + let WETH_USDC_V4: V4Pool + let USDC_DAI_V4: V4Pool before(async () => { ;({ WETH_USDC_V2, USDC_DAI_V2, WETH_USDC_V3, USDC_DAI_V3, WETH_USDC_V3_LOW_FEE } = await getUniswapPools( FORK_BLOCK )) + + let liquidity = JSBI.BigInt(utils.parseEther('1000000').toString()) + let tickSpacing = 60 + let tickProviderMock = [ + { + index: nearestUsableTick(TickMath.MIN_TICK, tickSpacing), + liquidityNet: liquidity, + liquidityGross: liquidity, + }, + { + index: nearestUsableTick(TickMath.MAX_TICK, tickSpacing), + liquidityNet: JSBI.multiply(liquidity, JSBI.BigInt('-1')), + liquidityGross: liquidity, + }, + ] + + WETH_USDC_V4 = new V4Pool( + WETH, + USDC, + 3_000, + tickSpacing, + ZERO_ADDRESS, + encodeSqrtRatioX96(1, 1), + liquidity, + 0, + tickProviderMock + ) + + ETH_USDC_V4 = new V4Pool( + ETHER, + USDC, + 3_000, + tickSpacing, + ZERO_ADDRESS, + encodeSqrtRatioX96(1, 1), + liquidity, + 0, + tickProviderMock + ) + + USDC_DAI_V4 = new V4Pool( + DAI, + USDC, + 3_000, + tickSpacing, + ZERO_ADDRESS, + encodeSqrtRatioX96(1, 1), + liquidity, + 0, + tickProviderMock + ) }) describe('v2', () => { @@ -270,7 +333,7 @@ describe('Uniswap', () => { it('encodes a single exactInput ETH->USDC swap', async () => { const inputEther = utils.parseEther('1').toString() const trade = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3], ETHER, USDC), + new V3Route([WETH_USDC_V3], ETHER, USDC), CurrencyAmount.fromRawAmount(ETHER, inputEther), TradeType.EXACT_INPUT ) @@ -283,7 +346,7 @@ describe('Uniswap', () => { it('encodes a single exactInput ETH->USDC swap, with a fee', async () => { const inputEther = utils.parseEther('1').toString() const trade = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3], ETHER, USDC), + new V3Route([WETH_USDC_V3], ETHER, USDC), CurrencyAmount.fromRawAmount(ETHER, inputEther), TradeType.EXACT_INPUT ) @@ -297,7 +360,7 @@ describe('Uniswap', () => { it('encodes a single exactInput ETH->USDC swap, with a flat fee', async () => { const inputEther = utils.parseEther('1').toString() const trade = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3], ETHER, USDC), + new V3Route([WETH_USDC_V3], ETHER, USDC), CurrencyAmount.fromRawAmount(ETHER, inputEther), TradeType.EXACT_INPUT ) @@ -311,7 +374,7 @@ describe('Uniswap', () => { it('encodes a single exactInput USDC->ETH swap', async () => { const inputUSDC = utils.parseUnits('1000', 6).toString() const trade = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3], USDC, ETHER), + new V3Route([WETH_USDC_V3], USDC, ETHER), CurrencyAmount.fromRawAmount(USDC, inputUSDC), TradeType.EXACT_INPUT ) @@ -324,7 +387,7 @@ describe('Uniswap', () => { it('encodes a single exactInput USDC->ETH swap, with WETH fee', async () => { const inputUSDC = utils.parseUnits('1000', 6).toString() const trade = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3], USDC, ETHER), + new V3Route([WETH_USDC_V3], USDC, ETHER), CurrencyAmount.fromRawAmount(USDC, inputUSDC), TradeType.EXACT_INPUT ) @@ -338,7 +401,7 @@ describe('Uniswap', () => { it('encodes a single exactInput USDC->ETH swap with permit', async () => { const inputUSDC = utils.parseUnits('1000', 6).toString() const trade = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3], USDC, ETHER), + new V3Route([WETH_USDC_V3], USDC, ETHER), CurrencyAmount.fromRawAmount(USDC, inputUSDC), TradeType.EXACT_INPUT ) @@ -358,7 +421,7 @@ describe('Uniswap', () => { it('encodes a single exactInput ETH->USDC->DAI swap', async () => { const inputEther = utils.parseEther('1').toString() const trade = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3, USDC_DAI_V3], ETHER, DAI), + new V3Route([WETH_USDC_V3, USDC_DAI_V3], ETHER, DAI), CurrencyAmount.fromRawAmount(ETHER, inputEther), TradeType.EXACT_INPUT ) @@ -371,7 +434,7 @@ describe('Uniswap', () => { it('encodes a single exactInput ETH->USDC->DAI swap in safemode, sends too much ETH', async () => { const inputEther = utils.parseEther('1').toString() const trade = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3, USDC_DAI_V3], ETHER, DAI), + new V3Route([WETH_USDC_V3, USDC_DAI_V3], ETHER, DAI), CurrencyAmount.fromRawAmount(ETHER, inputEther), TradeType.EXACT_INPUT ) @@ -384,7 +447,7 @@ describe('Uniswap', () => { it('encodes a single exactOutput ETH->USDC swap', async () => { const outputUSDC = utils.parseUnits('1000', 6).toString() const trade = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3], ETHER, USDC), + new V3Route([WETH_USDC_V3], ETHER, USDC), CurrencyAmount.fromRawAmount(USDC, outputUSDC), TradeType.EXACT_OUTPUT ) @@ -397,7 +460,7 @@ describe('Uniswap', () => { it('encodes a single exactOutput USDC->ETH swap', async () => { const outputEther = utils.parseEther('1').toString() const trade = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3], USDC, ETHER), + new V3Route([WETH_USDC_V3], USDC, ETHER), CurrencyAmount.fromRawAmount(ETHER, outputEther), TradeType.EXACT_OUTPUT ) @@ -410,7 +473,7 @@ describe('Uniswap', () => { it('encodes an exactOutput ETH->USDC->DAI swap', async () => { const outputDai = utils.parseEther('1000').toString() const trade = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3, USDC_DAI_V3], ETHER, DAI), + new V3Route([WETH_USDC_V3, USDC_DAI_V3], ETHER, DAI), CurrencyAmount.fromRawAmount(DAI, outputDai), TradeType.EXACT_OUTPUT ) @@ -423,7 +486,7 @@ describe('Uniswap', () => { it('encodes an exactOutput DAI->USDC->ETH swap', async () => { const outputEther = utils.parseEther('1').toString() const trade = await V3Trade.fromRoute( - new RouteV3([USDC_DAI_V3, WETH_USDC_V3], DAI, ETHER), + new V3Route([USDC_DAI_V3, WETH_USDC_V3], DAI, ETHER), CurrencyAmount.fromRawAmount(ETHER, outputEther), TradeType.EXACT_OUTPUT ) @@ -441,10 +504,11 @@ describe('Uniswap', () => { .div(10000 - 500) .toString() const trade = await V3Trade.fromRoute( - new RouteV3([USDC_DAI_V3, WETH_USDC_V3], DAI, ETHER), + new V3Route([USDC_DAI_V3, WETH_USDC_V3], DAI, ETHER), CurrencyAmount.fromRawAmount(ETHER, adjustedOutputEther), TradeType.EXACT_OUTPUT ) + const feeOptions: FeeOptions = { fee: new Percent(5, 100), recipient: TEST_FEE_RECIPIENT_ADDRESS } const opts = swapOptions({ fee: feeOptions }) const methodParameters = SwapRouter.swapCallParameters(buildTrade([trade]), opts) @@ -453,6 +517,123 @@ describe('Uniswap', () => { }) }) + describe('v4', () => { + it('encodes a single exactInput ETH->USDC swap', async () => { + const inputEther = utils.parseEther('1').toString() + const trade = await V4Trade.fromRoute( + new V4Route([ETH_USDC_V4], ETHER, USDC), + CurrencyAmount.fromRawAmount(ETHER, inputEther), + TradeType.EXACT_INPUT + ) + const opts = swapOptions({}) + const methodParameters = SwapRouter.swapCallParameters(buildTrade([trade]), opts) + registerFixture('_UNISWAP_V4_1_ETH_FOR_USDC', methodParameters) + expect(hexToDecimalString(methodParameters.value)).to.eq(inputEther) + }) + + // this test needs v4-sdk 1.6.3 to merge + // it('encodes a single exactInput ETH->USDC swap, via WETH', async () => { + // const inputEther = utils.parseEther('1').toString() + // const trade = await V4Trade.fromRoute( + // new V4Route([WETH_USDC_V4], ETHER, USDC), + // CurrencyAmount.fromRawAmount(ETHER, inputEther), + // TradeType.EXACT_INPUT + // ) + // const opts = swapOptions({}) + // const methodParameters = SwapRouter.swapCallParameters(buildTrade([trade]), opts) + // registerFixture('_UNISWAP_V4_1_ETH_FOR_USDC_WITH_WRAP', methodParameters) + // expect(hexToDecimalString(methodParameters.value)).to.eq(inputEther) + // }) + + it('encodes a single exactInput ETH->USDC swap, with a fee', async () => { + const inputEther = utils.parseEther('1').toString() + const trade = await V4Trade.fromRoute( + new V4Route([ETH_USDC_V4], ETHER, USDC), + CurrencyAmount.fromRawAmount(ETHER, inputEther), + TradeType.EXACT_INPUT + ) + const feeOptions: FeeOptions = { fee: new Percent(5, 100), recipient: TEST_FEE_RECIPIENT_ADDRESS } + const opts = swapOptions({ fee: feeOptions }) + const methodParameters = SwapRouter.swapCallParameters(buildTrade([trade]), opts) + registerFixture('_UNISWAP_V4_1_ETH_FOR_USDC_WITH_FEE', methodParameters) + expect(hexToDecimalString(methodParameters.value)).to.eq(inputEther) + }) + + it('encodes a single exactInput ETH->USDC swap, with a flat fee', async () => { + const inputEther = utils.parseEther('1').toString() + const trade = await V4Trade.fromRoute( + new V4Route([ETH_USDC_V4], ETHER, USDC), + CurrencyAmount.fromRawAmount(ETHER, inputEther), + TradeType.EXACT_INPUT + ) + const feeOptions: FlatFeeOptions = { amount: utils.parseUnits('50', 6), recipient: TEST_FEE_RECIPIENT_ADDRESS } + const opts = swapOptions({ flatFee: feeOptions }) + const methodParameters = SwapRouter.swapCallParameters(buildTrade([trade]), opts) + registerFixture('_UNISWAP_V4_1_ETH_FOR_USDC_WITH_FLAT_FEE', methodParameters) + expect(hexToDecimalString(methodParameters.value)).to.eq(inputEther) + }) + + it('encodes a single exactInput USDC->ETH swap', async () => { + const inputUSDC = utils.parseUnits('1000', 6).toString() + const trade = await V4Trade.fromRoute( + new V4Route([ETH_USDC_V4], USDC, ETHER), + CurrencyAmount.fromRawAmount(USDC, inputUSDC), + TradeType.EXACT_INPUT + ) + const opts = swapOptions({}) + const methodParameters = SwapRouter.swapCallParameters(buildTrade([trade]), opts) + registerFixture('_UNISWAP_V4_1000_USDC_FOR_ETH', methodParameters) + expect(hexToDecimalString(methodParameters.value)).to.eq('0') + }) + + it('encodes a single exactInput ETH->USDC->DAI swap', async () => { + const inputEther = utils.parseEther('1').toString() + const trade = await V4Trade.fromRoute( + new V4Route([ETH_USDC_V4, USDC_DAI_V4], ETHER, DAI), + CurrencyAmount.fromRawAmount(ETHER, inputEther), + TradeType.EXACT_INPUT + ) + const opts = swapOptions({ safeMode: true }) + const methodParameters = SwapRouter.swapCallParameters(buildTrade([trade]), opts) + registerFixture('_UNISWAP_V4_ETH_FOR_DAI', methodParameters) + expect(hexToDecimalString(methodParameters.value)).to.eq(inputEther) + }) + + it('encodes an exactOutput DAI->USDC->ETH swap', async () => { + const outputEther = utils.parseEther('1').toString() + const trade = await V4Trade.fromRoute( + new V4Route([USDC_DAI_V4, ETH_USDC_V4], DAI, ETHER), + CurrencyAmount.fromRawAmount(ETHER, outputEther), + TradeType.EXACT_OUTPUT + ) + const opts = swapOptions({}) + const methodParameters = SwapRouter.swapCallParameters(buildTrade([trade]), opts) + registerFixture('_UNISWAP_V4_DAI_FOR_1_ETH_2_HOP', methodParameters) + expect(hexToDecimalString(methodParameters.value)).to.equal('0') + }) + + it.skip('encodes an exactOutput DAI->USDC->ETH swap, with WETH fee', async () => { + // "exact output" of 1ETH. We must adjust for a 5% fee + // v4-sdk 1.6.3 needed + const outputEther = utils.parseEther('1') + const adjustedOutputEther = outputEther + .mul(10000) + .div(10000 - 500) + .toString() + const trade = await V4Trade.fromRoute( + new V4Route([USDC_DAI_V4, WETH_USDC_V4], DAI, ETHER), + CurrencyAmount.fromRawAmount(ETHER, adjustedOutputEther), + TradeType.EXACT_OUTPUT + ) + const feeOptions: FeeOptions = { fee: new Percent(5, 100), recipient: TEST_FEE_RECIPIENT_ADDRESS } + const opts = swapOptions({ fee: feeOptions }) + buildTrade([trade]) + const methodParameters = SwapRouter.swapCallParameters(buildTrade([trade]), opts) + // registerFixture('_UNISWAP_V4_DAI_FOR_1_ETH_2_HOP_WITH_WETH_FEE', methodParameters) + // expect(hexToDecimalString(methodParameters.value)).to.equal('0') + }) + }) + describe('mixed (interleaved)', async () => { it('encodes a mixed exactInput v3ETH->v2USDC->DAI swap', async () => { const inputEther = utils.parseEther('1').toString() @@ -529,7 +710,7 @@ describe('Uniswap', () => { TradeType.EXACT_INPUT ) const v3Trade = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3], ETHER, USDC), + new V3Route([WETH_USDC_V3], ETHER, USDC), CurrencyAmount.fromRawAmount(ETHER, inputEther), TradeType.EXACT_INPUT ) @@ -547,12 +728,12 @@ describe('Uniswap', () => { TradeType.EXACT_INPUT ) const v3Trade1 = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3], ETHER, USDC), + new V3Route([WETH_USDC_V3], ETHER, USDC), CurrencyAmount.fromRawAmount(ETHER, inputEther), TradeType.EXACT_INPUT ) const v3Trade2 = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3_LOW_FEE], ETHER, USDC), + new V3Route([WETH_USDC_V3_LOW_FEE], ETHER, USDC), CurrencyAmount.fromRawAmount(ETHER, inputEther), TradeType.EXACT_INPUT ) @@ -584,7 +765,7 @@ describe('Uniswap', () => { it('throws if flat fee amount is larger than minimumAmountOut', async () => { const inputEther = utils.parseEther('1').toString() const trade = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3], ETHER, USDC), + new V3Route([WETH_USDC_V3], ETHER, USDC), CurrencyAmount.fromRawAmount(ETHER, inputEther), TradeType.EXACT_INPUT ) @@ -645,7 +826,7 @@ describe('Uniswap', () => { } const mockV3PoolInRoute = ( - pool: Pool, + pool: V3Pool, tokenIn: Token, tokenOut: Token, amountIn: string, @@ -732,7 +913,7 @@ describe('Uniswap', () => { const rawInputAmount = getAmount(tokenIn, tokenOut, inputAmount, tradeType) const opts = swapOptions({}) - const trade = await V3Trade.fromRoute(new RouteV3([USDC_DAI_V3], tokenIn, tokenOut), rawInputAmount, tradeType) + const trade = await V3Trade.fromRoute(new V3Route([USDC_DAI_V3], tokenIn, tokenOut), rawInputAmount, tradeType) const classicQuote: PartialClassicQuote = { tokenIn: DAI.address, @@ -794,7 +975,7 @@ describe('Uniswap', () => { const rawInputAmount = getAmount(tokenIn, tokenOut, inputAmount, tradeType) const opts = swapOptions({}) - const trade = await V3Trade.fromRoute(new RouteV3([WETH_USDC_V3], WETH, USDC), rawInputAmount, tradeType) + const trade = await V3Trade.fromRoute(new V3Route([WETH_USDC_V3], WETH, USDC), rawInputAmount, tradeType) const classicQuote: PartialClassicQuote = { tokenIn: WETH.address, @@ -890,7 +1071,7 @@ describe('Uniswap', () => { const opts = swapOptions({}) const trade = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3], Ether.onChain(1), USDC), + new V3Route([WETH_USDC_V3], Ether.onChain(1), USDC), rawInputAmount, tradeType ) @@ -957,7 +1138,7 @@ describe('Uniswap', () => { const rawInputAmount = getAmount(tokenIn, tokenOut, inputAmount, tradeType) const opts = swapOptions({}) - const trade = await V3Trade.fromRoute(new RouteV3([WETH_USDC_V3], tokenIn, tokenOut), rawInputAmount, tradeType) + const trade = await V3Trade.fromRoute(new V3Route([WETH_USDC_V3], tokenIn, tokenOut), rawInputAmount, tradeType) const classicQuote: PartialClassicQuote = { tokenIn: USDC.address, @@ -990,7 +1171,7 @@ describe('Uniswap', () => { const opts = swapOptions({}) const trade = await V3Trade.fromRoute( - new RouteV3([USDC_DAI_V3, WETH_USDC_V3], tokenIn, tokenOut), + new V3Route([USDC_DAI_V3, WETH_USDC_V3], tokenIn, tokenOut), rawInputAmount, tradeType ) @@ -1077,12 +1258,12 @@ describe('Uniswap', () => { const opts = swapOptions({}) const trade1 = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3], tokenIn, tokenOut), + new V3Route([WETH_USDC_V3], tokenIn, tokenOut), rawInputAmount.divide(2), tradeType ) const trade2 = await V3Trade.fromRoute( - new RouteV3([WETH_USDC_V3_LOW_FEE], tokenIn, tokenOut), + new V3Route([WETH_USDC_V3_LOW_FEE], tokenIn, tokenOut), rawInputAmount.divide(2), tradeType ) diff --git a/sdks/universal-router-sdk/test/utils/uniswapData.ts b/sdks/universal-router-sdk/test/utils/uniswapData.ts index b0dcf0910..fc4b940ed 100644 --- a/sdks/universal-router-sdk/test/utils/uniswapData.ts +++ b/sdks/universal-router-sdk/test/utils/uniswapData.ts @@ -11,6 +11,7 @@ import { TICK_SPACINGS, FeeAmount, } from '@uniswap/v3-sdk' +import { Pool as V4Pool, Route as V4Route, Trade as V4Trade } from '@uniswap/v4-sdk' import { SwapOptions } from '../../src' import { CurrencyAmount, TradeType, Ether, Token, Percent, Currency } from '@uniswap/sdk-core' import IUniswapV3Pool from '@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json' @@ -133,6 +134,7 @@ export function buildTrade( trades: ( | V2Trade | V3Trade + | V4Trade | MixedRouteTrade )[] ): RouterTrade { @@ -151,8 +153,13 @@ export function buildTrade( inputAmount: trade.inputAmount, outputAmount: trade.outputAmount, })), - // TODO: ROUTE-219 - Support v4 trade in universal-router sdk - v4Routes: [], + v4Routes: trades + .filter((trade) => trade instanceof V4Trade) + .map((trade) => ({ + routev4: trade.route as RouteV4, + inputAmount: trade.inputAmount, + outputAmount: trade.outputAmount, + })), mixedRoutes: trades .filter((trade) => trade instanceof MixedRouteTrade) .map((trade) => ({ diff --git a/yarn.lock b/yarn.lock index c9cb2ba77..5b7144636 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4609,17 +4609,17 @@ __metadata: languageName: unknown linkType: soft -"@uniswap/router-sdk@npm:^1.11.0": - version: 1.11.1 - resolution: "@uniswap/router-sdk@npm:1.11.1" +"@uniswap/router-sdk@npm:^1.12.1": + version: 1.12.1 + resolution: "@uniswap/router-sdk@npm:1.12.1" dependencies: "@ethersproject/abi": ^5.5.0 "@uniswap/sdk-core": ^5.3.1 "@uniswap/swap-router-contracts": ^1.3.0 "@uniswap/v2-sdk": ^4.3.2 "@uniswap/v3-sdk": ^3.11.2 - "@uniswap/v4-sdk": ^1.0.0 - checksum: 14530401e29bef09de817f243cf73820d2b043b35da7f34f1a9e06d82ce924cb396d715ea4a9f137bb8e98d5e593254f6eb78b1008e2f84b9e8166feb0297014 + "@uniswap/v4-sdk": ^1.6.0 + checksum: d97732822cdd062349a42762c9c4c458e156bdcf436fc9ba8ee3a7fb4e2ade803a6b6305b4a59e869073436dd510ad0d229ec58aa56dddff60a3f06d3670a84c languageName: node linkType: hard @@ -4730,14 +4730,14 @@ __metadata: "@types/node": ^18.7.16 "@types/node-fetch": ^2.6.2 "@uniswap/permit2-sdk": ^1.3.0 - "@uniswap/router-sdk": ^1.11.0 + "@uniswap/router-sdk": ^1.12.1 "@uniswap/sdk-core": ^5.3.1 "@uniswap/universal-router": 2.0.0-beta.1 "@uniswap/v2-core": ^1.0.1 "@uniswap/v2-sdk": ^4.4.1 "@uniswap/v3-core": 1.0.0 "@uniswap/v3-sdk": ^3.13.1 - "@uniswap/v4-sdk": ^1.0.0 + "@uniswap/v4-sdk": ^1.6.3 bignumber.js: ^9.0.2 chai: ^4.3.6 dotenv: ^16.0.3 @@ -4890,16 +4890,16 @@ __metadata: languageName: node linkType: hard -"@uniswap/v4-sdk@npm:^1.0.0, @uniswap/v4-sdk@npm:^1.6.0": - version: 1.6.0 - resolution: "@uniswap/v4-sdk@npm:1.6.0" +"@uniswap/v4-sdk@npm:^1.6.0, @uniswap/v4-sdk@npm:^1.6.3": + version: 1.6.3 + resolution: "@uniswap/v4-sdk@npm:1.6.3" dependencies: "@ethersproject/solidity": ^5.0.9 "@uniswap/sdk-core": ^5.3.1 "@uniswap/v3-sdk": 3.12.0 tiny-invariant: ^1.1.0 tiny-warning: ^1.0.3 - checksum: 6ca999a8e519fe8b9c4337153ecfeec52051b8ad5adf18fb8bf05aed09b6853533081eeaa3409b027135032c2b4e5cece593088b7c6ff4fef1c3b5e9bceceb0a + checksum: b5137b2fe2cfc2940e986cd89458f56318bb9bec1899b511724462ee6956d6fa9f8a598c300d697339c4f0a51c45d4061a1afb2144c42798a5774c0e90bd89f6 languageName: node linkType: hard