From 5f497198ab8ff0397ef3413669980ac63f6bd910 Mon Sep 17 00:00:00 2001 From: Melisa Guevara Date: Mon, 15 Dec 2025 18:59:28 -0300 Subject: [PATCH 1/4] chore: throw for slippage lower than recommended values --- api/_dexes/cross-swap-service.ts | 19 +++++- api/_errors.ts | 11 ++++ api/_slippage.ts | 41 ++++++++++++ test/api/_slippage.test.ts | 103 +++++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 1 deletion(-) diff --git a/api/_dexes/cross-swap-service.ts b/api/_dexes/cross-swap-service.ts index 6c203c388..fc6d3462c 100644 --- a/api/_dexes/cross-swap-service.ts +++ b/api/_dexes/cross-swap-service.ts @@ -1,3 +1,4 @@ +import { BigNumber } from "ethers"; import { TradeType } from "@uniswap/sdk-core"; import { @@ -49,7 +50,7 @@ import { BridgeStrategy } from "../_bridges/types"; import { getSpokePoolPeripheryAddress } from "../_spoke-pool-periphery"; import { accountExistsOnHyperCore } from "../_hypercore"; import { CHAIN_IDs } from "../_constants"; -import { BigNumber } from "ethers"; +import { validateDestinationSwapSlippage } from "../_slippage"; const QUOTE_BUFFER = 0.005; // 0.5% @@ -708,6 +709,14 @@ function _prepCrossSwapQuotesRetrievalB2A( chainId: destinationSwapChainId, }; + // Validate slippage for destination swap + validateDestinationSwapSlippage({ + tokenIn: bridgeableOutputToken, + tokenOut: crossSwap.outputToken, + slippageTolerance: crossSwap.slippageTolerance, + splitSlippage: false, + }); + const destinationSwap = { chainId: destinationSwapChainId, tokenIn: bridgeableOutputToken, @@ -1640,6 +1649,14 @@ function _prepCrossSwapQuotesRetrievalA2A(params: { chainId: bridgeRoute.toChain, }; + // Validate slippage for destination swap + validateDestinationSwapSlippage({ + tokenIn: bridgeableOutputToken, + tokenOut: crossSwap.outputToken, + slippageTolerance: crossSwap.slippageTolerance, + splitSlippage: true, // A2A splits slippage between origin and destination swaps + }); + const originStrategies = getQuoteFetchStrategies( originSwapChainId, crossSwap.inputToken.symbol, diff --git a/api/_errors.ts b/api/_errors.ts index 79321ab47..5ac493488 100644 --- a/api/_errors.ts +++ b/api/_errors.ts @@ -45,6 +45,7 @@ export const AcrossErrorCode = { ROUTE_NOT_ENABLED: "ROUTE_NOT_ENABLED", SWAP_LIQUIDITY_INSUFFICIENT: "SWAP_LIQUIDITY_INSUFFICIENT", SWAP_QUOTE_UNAVAILABLE: "SWAP_QUOTE_UNAVAILABLE", + SWAP_SLIPPAGE_INSUFFICIENT: "SWAP_SLIPPAGE_INSUFFICIENT", SWAP_TYPE_NOT_GUARANTEED: "SWAP_TYPE_NOT_GUARANTEED", ABI_ENCODING_ERROR: "ABI_ENCODING_ERROR", SPONSORED_SWAP_SLIPPAGE_TOO_HIGH: "SPONSORED_SWAP_SLIPPAGE_TOO_HIGH", @@ -250,6 +251,16 @@ export class AmountTooHighError extends InputError { } } +export class SwapSlippageInsufficientError extends InputError { + constructor(args: { message: string }) { + super({ + message: args.message, + code: AcrossErrorCode.SWAP_SLIPPAGE_INSUFFICIENT, + param: "slippage", + }); + } +} + export class SwapQuoteUnavailableError extends AcrossApiError { constructor( args: { diff --git a/api/_slippage.ts b/api/_slippage.ts index 9f8ecbbdc..d4945e639 100644 --- a/api/_slippage.ts +++ b/api/_slippage.ts @@ -5,6 +5,7 @@ import { CHAIN_IDs, } from "./_constants"; import { OriginOrDestination, Token } from "./_dexes/types"; +import { SwapSlippageInsufficientError } from "./_errors"; export const STABLE_COIN_SWAP_SLIPPAGE = { origin: 0.25, // 0.25% @@ -147,3 +148,43 @@ function isMajorTokenSymbol(symbol: string, chainId: number) { majorTokenSymbol.toUpperCase() === symbol.toUpperCase() ); } + +/** + * Validates that user-provided slippage is sufficient for destination swaps. + * Throws an error if the user slippage is less than the auto (recommended) slippage. + * Only validates when slippage is explicitly set by the user. + * + * @param params.tokenIn - The input token for the destination swap + * @param params.tokenOut - The output token for the destination swap + * @param params.slippageTolerance - The user-provided slippage tolerance + * @param params.splitSlippage - Whether slippage is being split between origin and destination + * @throws {InputError} When user slippage is less than auto slippage + */ +export function validateDestinationSwapSlippage(params: { + tokenIn: Token; + tokenOut: Token; + slippageTolerance: number | "auto"; + splitSlippage?: boolean; +}) { + // Only validate if user explicitly provided a slippage value + if (params.slippageTolerance === "auto") { + return; + } + + const autoSlippage = resolveAutoSlippage({ + tokenIn: params.tokenIn, + tokenOut: params.tokenOut, + originOrDestination: "destination", + }); + + // Calculate the effective user slippage (accounting for split if applicable) + const userSlippage = params.splitSlippage + ? params.slippageTolerance / 2 + : params.slippageTolerance; + + if (userSlippage < autoSlippage) { + throw new SwapSlippageInsufficientError({ + message: `Insufficient slippage.`, + }); + } +} diff --git a/test/api/_slippage.test.ts b/test/api/_slippage.test.ts index 6c77689c9..a521da1a6 100644 --- a/test/api/_slippage.test.ts +++ b/test/api/_slippage.test.ts @@ -1,10 +1,12 @@ import { getSlippage, + validateDestinationSwapSlippage, STABLE_COIN_SWAP_SLIPPAGE, MAJOR_PAIR_SLIPPAGE, LONG_TAIL_SLIPPAGE, } from "../../api/_slippage"; import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "../../api/_constants"; +import { SwapSlippageInsufficientError } from "../../api/_errors"; describe("api/_slippage", () => { const wethMainnet = { @@ -166,4 +168,105 @@ describe("api/_slippage", () => { ).toThrow("Can't resolve auto slippage for tokens on different chains"); }); }); + + describe("#validateDestinationSwapSlippage()", () => { + test("should not throw when slippage is 'auto'", () => { + expect(() => + validateDestinationSwapSlippage({ + tokenIn: usdcMainnet, + tokenOut: usdtMainnet, + slippageTolerance: "auto", + splitSlippage: false, + }) + ).not.toThrow(); + }); + + test("should not throw when user slippage equals auto slippage", () => { + // Stable coin pair destination slippage = 0.5% + expect(() => + validateDestinationSwapSlippage({ + tokenIn: usdcMainnet, + tokenOut: usdtMainnet, + slippageTolerance: STABLE_COIN_SWAP_SLIPPAGE.destination, + splitSlippage: false, + }) + ).not.toThrow(); + }); + + test("should not throw when user slippage is greater than auto slippage", () => { + // Stable coin pair destination slippage = 0.5%, user provides 1.0% + expect(() => + validateDestinationSwapSlippage({ + tokenIn: usdcMainnet, + tokenOut: usdtMainnet, + slippageTolerance: 1.0, + splitSlippage: false, + }) + ).not.toThrow(); + }); + + test("should throw SwapSlippageInsufficientError when user slippage < auto for stable coins", () => { + // Stable coin pair destination slippage = 0.5%, user provides 0.3% + expect(() => + validateDestinationSwapSlippage({ + tokenIn: usdcMainnet, + tokenOut: usdtMainnet, + slippageTolerance: 0.3, + splitSlippage: false, + }) + ).toThrow(SwapSlippageInsufficientError); + }); + + test("should throw SwapSlippageInsufficientError when user slippage < auto for major pair", () => { + // Major pair (WETH/USDC) destination slippage = 1.5%, user provides 1.0% + expect(() => + validateDestinationSwapSlippage({ + tokenIn: wethMainnet, + tokenOut: usdcMainnet, + slippageTolerance: 1.0, + splitSlippage: false, + }) + ).toThrow(SwapSlippageInsufficientError); + }); + + test("should throw SwapSlippageInsufficientError when user slippage < auto for long tail pair", () => { + // Long tail pair (PEPE/USDC) destination slippage = 5.0%, user provides 3.0% + expect(() => + validateDestinationSwapSlippage({ + tokenIn: usdcMainnet, + tokenOut: pepeMainnet, + slippageTolerance: 3.0, + splitSlippage: false, + }) + ).toThrow(SwapSlippageInsufficientError); + }); + + test("should account for split slippage in validation", () => { + // User provides 2.0% with splitSlippage: true + // Effective slippage = 1.0% + // Major pair auto = 1.5%, so should throw + expect(() => + validateDestinationSwapSlippage({ + tokenIn: wethMainnet, + tokenOut: usdcMainnet, + slippageTolerance: 2.0, + splitSlippage: true, + }) + ).toThrow(SwapSlippageInsufficientError); + }); + + test("should pass validation with split slippage when sufficient", () => { + // User provides 4.0% with splitSlippage: true + // Effective slippage = 2.0% (4.0% / 2) + // Major pair auto = 1.5%, so should pass + expect(() => + validateDestinationSwapSlippage({ + tokenIn: wethMainnet, + tokenOut: usdcMainnet, + slippageTolerance: 4.0, + splitSlippage: true, + }) + ).not.toThrow(); + }); + }); }); From c37ba34f30de72eff3b7260794cf933e62f1fc1d Mon Sep 17 00:00:00 2001 From: Melisa Guevara Date: Mon, 15 Dec 2025 19:27:44 -0300 Subject: [PATCH 2/4] include minimun recommended value in error message --- api/_slippage.ts | 4 +++- test/api/_slippage.test.ts | 40 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/api/_slippage.ts b/api/_slippage.ts index d4945e639..c56639c50 100644 --- a/api/_slippage.ts +++ b/api/_slippage.ts @@ -184,7 +184,9 @@ export function validateDestinationSwapSlippage(params: { if (userSlippage < autoSlippage) { throw new SwapSlippageInsufficientError({ - message: `Insufficient slippage.`, + message: `Insufficient slippage tolerance. Minimum recommended slippage is ${( + autoSlippage / 100 + ).toFixed(4)} for this token pair.`, }); } } diff --git a/test/api/_slippage.test.ts b/test/api/_slippage.test.ts index a521da1a6..902f667d6 100644 --- a/test/api/_slippage.test.ts +++ b/test/api/_slippage.test.ts @@ -215,6 +215,15 @@ describe("api/_slippage", () => { splitSlippage: false, }) ).toThrow(SwapSlippageInsufficientError); + + expect(() => + validateDestinationSwapSlippage({ + tokenIn: usdcMainnet, + tokenOut: usdtMainnet, + slippageTolerance: 0.3, + splitSlippage: false, + }) + ).toThrow(/Minimum recommended slippage is 0\.0050/); }); test("should throw SwapSlippageInsufficientError when user slippage < auto for major pair", () => { @@ -227,18 +236,36 @@ describe("api/_slippage", () => { splitSlippage: false, }) ).toThrow(SwapSlippageInsufficientError); + + expect(() => + validateDestinationSwapSlippage({ + tokenIn: wethMainnet, + tokenOut: usdcMainnet, + slippageTolerance: 1.0, + splitSlippage: false, + }) + ).toThrow(/Minimum recommended slippage is 0\.0500/); }); test("should throw SwapSlippageInsufficientError when user slippage < auto for long tail pair", () => { - // Long tail pair (PEPE/USDC) destination slippage = 5.0%, user provides 3.0% + // Long tail pair (PEPE/USDT) destination slippage = 5.0%, user provides 3.0% expect(() => validateDestinationSwapSlippage({ - tokenIn: usdcMainnet, + tokenIn: usdtMainnet, tokenOut: pepeMainnet, slippageTolerance: 3.0, splitSlippage: false, }) ).toThrow(SwapSlippageInsufficientError); + + expect(() => + validateDestinationSwapSlippage({ + tokenIn: usdtMainnet, + tokenOut: pepeMainnet, + slippageTolerance: 3.0, + splitSlippage: false, + }) + ).toThrow(/Minimum recommended slippage is 0\.0500/); }); test("should account for split slippage in validation", () => { @@ -253,6 +280,15 @@ describe("api/_slippage", () => { splitSlippage: true, }) ).toThrow(SwapSlippageInsufficientError); + + expect(() => + validateDestinationSwapSlippage({ + tokenIn: wethMainnet, + tokenOut: usdcMainnet, + slippageTolerance: 2.0, + splitSlippage: true, + }) + ).toThrow(/Minimum recommended slippage is 0\.0150/); }); test("should pass validation with split slippage when sufficient", () => { From 4f8290639d686aa72e10368bbbd4663da592f066 Mon Sep 17 00:00:00 2001 From: Melisa Guevara Date: Mon, 15 Dec 2025 22:49:34 -0300 Subject: [PATCH 3/4] fix test --- test/api/_slippage.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/api/_slippage.test.ts b/test/api/_slippage.test.ts index 902f667d6..272cff612 100644 --- a/test/api/_slippage.test.ts +++ b/test/api/_slippage.test.ts @@ -244,7 +244,7 @@ describe("api/_slippage", () => { slippageTolerance: 1.0, splitSlippage: false, }) - ).toThrow(/Minimum recommended slippage is 0\.0500/); + ).toThrow(/Minimum recommended slippage is 0\.0150/); }); test("should throw SwapSlippageInsufficientError when user slippage < auto for long tail pair", () => { From 97b4f95cdea4f21887232bf0b9be84bf44b963b5 Mon Sep 17 00:00:00 2001 From: Melisa Guevara Date: Tue, 16 Dec 2025 10:00:02 -0300 Subject: [PATCH 4/4] remove comments --- api/_dexes/cross-swap-service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/_dexes/cross-swap-service.ts b/api/_dexes/cross-swap-service.ts index fc6d3462c..d3a050433 100644 --- a/api/_dexes/cross-swap-service.ts +++ b/api/_dexes/cross-swap-service.ts @@ -709,7 +709,6 @@ function _prepCrossSwapQuotesRetrievalB2A( chainId: destinationSwapChainId, }; - // Validate slippage for destination swap validateDestinationSwapSlippage({ tokenIn: bridgeableOutputToken, tokenOut: crossSwap.outputToken, @@ -1649,7 +1648,6 @@ function _prepCrossSwapQuotesRetrievalA2A(params: { chainId: bridgeRoute.toChain, }; - // Validate slippage for destination swap validateDestinationSwapSlippage({ tokenIn: bridgeableOutputToken, tokenOut: crossSwap.outputToken,