From 7a96d40b7eeaf6d04f42dccd485796b71905d46a Mon Sep 17 00:00:00 2001 From: luchobonatti Date: Tue, 3 Jun 2025 15:51:15 -0300 Subject: [PATCH] fix: fix getQuote using Pool. Add SlippageTolerance in builsSwapCallData --- src/core/uniDevKitV4.ts | 2 +- src/{test => }/helpers/swap.ts | 0 src/test/utils/buildSwapCallData.test.ts | 33 ++++++++++++------- src/test/utils/getQuote.test.ts | 21 ++++++++---- src/types/utils/buildSwapCallData.ts | 4 +-- src/types/utils/getQuote.ts | 23 +++---------- src/utils/buildSwapCallData.ts | 26 +++++++++++++-- src/utils/getQuote.ts | 41 ++++++++---------------- 8 files changed, 80 insertions(+), 70 deletions(-) rename src/{test => }/helpers/swap.ts (100%) diff --git a/src/core/uniDevKitV4.ts b/src/core/uniDevKitV4.ts index 4e7dc28..20be7d7 100644 --- a/src/core/uniDevKitV4.ts +++ b/src/core/uniDevKitV4.ts @@ -188,6 +188,6 @@ export class UniDevKitV4 { } async buildSwapCallData(params: BuildSwapCallDataParams): Promise { - return buildSwapCallData(params); + return buildSwapCallData(params, this.instance); } } diff --git a/src/test/helpers/swap.ts b/src/helpers/swap.ts similarity index 100% rename from src/test/helpers/swap.ts rename to src/helpers/swap.ts diff --git a/src/test/utils/buildSwapCallData.test.ts b/src/test/utils/buildSwapCallData.test.ts index 84f6c73..377b049 100644 --- a/src/test/utils/buildSwapCallData.test.ts +++ b/src/test/utils/buildSwapCallData.test.ts @@ -1,8 +1,19 @@ +import { createMockSdkInstance } from "@/test/helpers/sdkInstance"; import { buildSwapCallData } from "@/utils/buildSwapCallData"; +import * as getQuoteModule from "@/utils/getQuote"; import { Token } from "@uniswap/sdk-core"; import { Pool } from "@uniswap/v4-sdk"; import { type Address, zeroAddress } from "viem"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; + +const sdkInstance = createMockSdkInstance(); + +// Mock getQuote to return a fixed amount +vi.spyOn(getQuoteModule, "getQuote").mockImplementation(async () => ({ + amountOut: BigInt(1000000000000000000), // 1 WETH + estimatedGasUsed: BigInt(100000), + timestamp: Date.now(), +})); describe("buildSwapCallData", () => { // USDC and WETH on Mainnet @@ -31,11 +42,11 @@ describe("buildSwapCallData", () => { const params = { tokenIn: mockTokens[0], amountIn: BigInt(1000000), // 1 USDC - amountOutMin: BigInt(0), + slippageTolerance: 50, pool: mockPool, }; - const calldata = await buildSwapCallData(params); + const calldata = await buildSwapCallData(params, sdkInstance); expect(calldata).toBeDefined(); expect(calldata).toMatch(/^0x/); // Should be a hex string }); @@ -44,11 +55,11 @@ describe("buildSwapCallData", () => { const params = { tokenIn: mockTokens[1], amountIn: BigInt(1000000000000000000), // 1 WETH - amountOutMin: BigInt(0), + slippageTolerance: 50, pool: mockPool, }; - const calldata = await buildSwapCallData(params); + const calldata = await buildSwapCallData(params, sdkInstance); expect(calldata).toBeDefined(); expect(calldata).toMatch(/^0x/); }); @@ -57,11 +68,11 @@ describe("buildSwapCallData", () => { const params = { tokenIn: mockTokens[0], amountIn: BigInt(1000000), - amountOutMin: BigInt(500000), // 0.5 WETH minimum + slippageTolerance: 50, pool: mockPool, }; - const calldata = await buildSwapCallData(params); + const calldata = await buildSwapCallData(params, sdkInstance); expect(calldata).toBeDefined(); expect(calldata).toMatch(/^0x/); }); @@ -70,11 +81,11 @@ describe("buildSwapCallData", () => { const params = { tokenIn: mockTokens[0], amountIn: BigInt(1000000), - amountOutMin: BigInt(0), + slippageTolerance: 50, pool: mockPool, }; - const calldata = await buildSwapCallData(params); + const calldata = await buildSwapCallData(params, sdkInstance); expect(calldata).toBeDefined(); expect(calldata).toMatch(/^0x/); }); @@ -83,11 +94,11 @@ describe("buildSwapCallData", () => { const params = { tokenIn: mockTokens[0], amountIn: BigInt(1000000), - amountOutMin: BigInt(0), + slippageTolerance: 50, pool: mockPool, }; - const calldata = await buildSwapCallData(params); + const calldata = await buildSwapCallData(params, sdkInstance); expect(calldata).toBeDefined(); expect(calldata).toMatch(/^0x/); }); diff --git a/src/test/utils/getQuote.test.ts b/src/test/utils/getQuote.test.ts index f0ffcad..558b8fd 100644 --- a/src/test/utils/getQuote.test.ts +++ b/src/test/utils/getQuote.test.ts @@ -1,9 +1,20 @@ import { createMockSdkInstance } from "@/test/helpers/sdkInstance"; import { getQuote } from "@/utils/getQuote"; +import type { Pool } from "@uniswap/v4-sdk"; import type { Abi } from "viem"; import type { SimulateContractReturnType } from "viem/actions"; import { describe, expect, it, vi } from "vitest"; +const mockPool: Pool = { + poolKey: { + currency0: "0x123", + currency1: "0x456", + fee: 3000, + tickSpacing: 10, + hooks: "0x", + }, +} as Pool; + describe("getQuote", () => { it("should throw error if SDK instance not found", async () => { const mockDeps = createMockSdkInstance(); @@ -14,10 +25,9 @@ describe("getQuote", () => { await expect( getQuote( { - tokens: ["0x123", "0x456"], - zeroForOne: true, + pool: mockPool, amountIn: BigInt(1000000), - feeTier: 3000, + zeroForOne: true, }, mockDeps, ), @@ -32,10 +42,9 @@ describe("getQuote", () => { const result = await getQuote( { - tokens: ["0x123", "0x456"], - zeroForOne: true, + pool: mockPool, amountIn: BigInt(1000000), - feeTier: 3000, + zeroForOne: true, }, mockDeps, ); diff --git a/src/types/utils/buildSwapCallData.ts b/src/types/utils/buildSwapCallData.ts index 1ad4b4a..9f85762 100644 --- a/src/types/utils/buildSwapCallData.ts +++ b/src/types/utils/buildSwapCallData.ts @@ -20,8 +20,8 @@ export type BuildSwapCallDataParams = { tokenIn: Address; /** Amount of input tokens to swap (in token's smallest unit) */ amountIn: bigint; - /** Minimum amount of output tokens to receive (in token's smallest unit) */ - amountOutMin?: bigint; /** Pool */ pool: Pool; + /** Slippage tolerance in basis points (e.g., 50 = 0.5%). Defaults to 50 (0.5%) */ + slippageTolerance?: number; }; diff --git a/src/types/utils/getQuote.ts b/src/types/utils/getQuote.ts index b96ac2e..71a7cb0 100644 --- a/src/types/utils/getQuote.ts +++ b/src/types/utils/getQuote.ts @@ -1,24 +1,14 @@ -import type { FeeTier } from "@/types/utils/getPool"; -import type { Address, Hex } from "viem"; +import type { Pool } from "@uniswap/v4-sdk"; +import type { Hex } from "viem"; /** * Parameters required for fetching a quote using the V4 Quoter contract. */ export interface QuoteParams { /** - * Array of two token addresses representing the pair. The order will be handled internally. + * The pool instance to quote from */ - tokens: [Address, Address]; - - /** - * The fee tier of the pool (e.g., FeeTier.MEDIUM). - */ - feeTier: FeeTier; - - /** - * The tick spacing for the pool. If not provided, it will be derived from the fee tier. - */ - tickSpacing?: number; + pool: Pool; /** * The amount of tokens being swapped, expressed as a bigint. @@ -30,11 +20,6 @@ export interface QuoteParams { */ zeroForOne: boolean; - /** - * Address of the hooks contract, if any. Defaults to zero address if not provided. - */ - hooks?: Address; - /** * Optional additional data for the hooks, if any. */ diff --git a/src/utils/buildSwapCallData.ts b/src/utils/buildSwapCallData.ts index 18d4c85..a13930d 100644 --- a/src/utils/buildSwapCallData.ts +++ b/src/utils/buildSwapCallData.ts @@ -1,5 +1,8 @@ +import { calculateMinimumOutput } from "@/helpers/swap"; import type { BuildSwapCallDataParams } from "@/types"; import { COMMANDS } from "@/types"; +import type { UniDevKitV4Instance } from "@/types/core"; +import { getQuote } from "@/utils/getQuote"; import { ethers } from "ethers"; import type { Hex } from "viem"; @@ -21,8 +24,8 @@ import type { Hex } from "viem"; * const swapParams = { * tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC * amountIn: parseUnits("100", 6), // 100 USDC - * amountOutMin: parseUnits("0.05", 18), // Min 0.05 WETH * pool: pool, + * slippageTolerance: 50, // 0.5% * }; * * const calldata = await buildSwapCallData(swapParams); @@ -37,13 +40,30 @@ import type { Hex } from "viem"; */ export async function buildSwapCallData( params: BuildSwapCallDataParams, + instance: UniDevKitV4Instance, ): Promise { // Extract and set default parameters - const { tokenIn, amountIn, amountOutMin = 0n, pool } = params; + const { tokenIn, amountIn, pool, slippageTolerance = 50 } = params; const zeroForOne = tokenIn.toLowerCase() === pool.poolKey.currency0.toLowerCase(); + // Get quote and calculate minimum output amount + const quote = await getQuote( + { + pool, + amountIn, + zeroForOne, + }, + instance, + ); + + // Calculate minimum output amount based on slippage + const amountOutMin = calculateMinimumOutput( + quote.amountOut, + slippageTolerance, + ); + // Encode Universal Router commands const commands = ethers.utils.solidityPack( ["uint8"], @@ -66,7 +86,7 @@ export async function buildSwapCallData( poolKey: pool.poolKey, zeroForOne, amountIn: ethers.BigNumber.from(amountIn.toString()), - amountOutMinimum: ethers.BigNumber.from(amountOutMin.toString()), + amountOutMinimum: amountOutMin, hookData: "0x", }, ], diff --git a/src/utils/getQuote.ts b/src/utils/getQuote.ts index ad56dfe..9b1aad2 100644 --- a/src/utils/getQuote.ts +++ b/src/utils/getQuote.ts @@ -1,20 +1,15 @@ import V4QuoterAbi from "@/constants/abis/V4Quoter"; -import { sortTokens } from "@/helpers/tokens"; import type { UniDevKitV4Instance } from "@/types/core"; -import { FeeTier, TICK_SPACING_BY_FEE } from "@/types/utils/getPool"; import type { QuoteParams, QuoteResponse } from "@/types/utils/getQuote"; -import { zeroAddress } from "viem"; /** * Fetches a quote for a token swap using the V4 Quoter contract. - * This function constructs the pool key from the given tokens and parameters, - * and then simulates the quote to estimate the output amount. + * This function uses the provided pool instance to simulate the quote. * - * @param params - The parameters required for the quote, including tokens, fee tier, tick spacing, and amount. - * @param chainId - (Optional) The chain ID to use. If only one instance is registered, this is not required. + * @param params - The parameters required for the quote, including pool and amount. + * @param instance - UniDevKitV4 instance for contract interaction * @returns A Promise that resolves to the quote result, including the amount out and gas estimate. * @throws Will throw an error if: - * - SDK instance is not found * - Simulation fails (e.g., insufficient liquidity, invalid parameters) * - Contract call reverts */ @@ -24,30 +19,20 @@ export async function getQuote( ): Promise { const { client, contracts } = instance; const { quoter } = contracts; + const { + pool: { poolKey }, + } = params; try { - // Sort tokens to ensure consistent pool key ordering - const [currency0, currency1] = sortTokens( - params.tokens[0], - params.tokens[1], - ); - - // Use provided tick spacing or derive from fee tier - const fee = (params.feeTier ?? FeeTier.MEDIUM) as FeeTier; - const tickSpacing = params.tickSpacing ?? TICK_SPACING_BY_FEE[fee]; - - // Construct the poolKey - const poolKey = { - currency0, - currency1, - fee, - tickSpacing, - hooks: params.hooks || zeroAddress, - }; - // Build the parameters for quoteExactInputSingle const quoteParams = { - poolKey, + poolKey: { + currency0: poolKey.currency0 as `0x${string}`, + currency1: poolKey.currency1 as `0x${string}`, + fee: poolKey.fee, + tickSpacing: poolKey.tickSpacing, + hooks: poolKey.hooks as `0x${string}`, + }, zeroForOne: params.zeroForOne, exactAmount: params.amountIn, hookData: params.hookData || "0x",