Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion api/_dexes/cross-swap-service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BigNumber } from "ethers";
import { TradeType } from "@uniswap/sdk-core";

import {
Expand Down Expand Up @@ -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%

Expand Down Expand Up @@ -708,6 +709,13 @@ function _prepCrossSwapQuotesRetrievalB2A(
chainId: destinationSwapChainId,
};

validateDestinationSwapSlippage({
tokenIn: bridgeableOutputToken,
tokenOut: crossSwap.outputToken,
slippageTolerance: crossSwap.slippageTolerance,
splitSlippage: false,
});

const destinationSwap = {
chainId: destinationSwapChainId,
tokenIn: bridgeableOutputToken,
Expand Down Expand Up @@ -1640,6 +1648,13 @@ function _prepCrossSwapQuotesRetrievalA2A(params: {
chainId: bridgeRoute.toChain,
};

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,
Expand Down
11 changes: 11 additions & 0 deletions api/_errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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: {
Expand Down
43 changes: 43 additions & 0 deletions api/_slippage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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%
Expand Down Expand Up @@ -147,3 +148,45 @@ 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 tolerance. Minimum recommended slippage is ${(
autoSlippage / 100
).toFixed(4)} for this token pair.`,
});
}
}
139 changes: 139 additions & 0 deletions test/api/_slippage.test.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -166,4 +168,141 @@ 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);

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", () => {
// 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);

expect(() =>
validateDestinationSwapSlippage({
tokenIn: wethMainnet,
tokenOut: usdcMainnet,
slippageTolerance: 1.0,
splitSlippage: false,
})
).toThrow(/Minimum recommended slippage is 0\.0150/);
});

test("should throw SwapSlippageInsufficientError when user slippage < auto for long tail pair", () => {
// Long tail pair (PEPE/USDT) destination slippage = 5.0%, user provides 3.0%
expect(() =>
validateDestinationSwapSlippage({
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", () => {
// 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);

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", () => {
// 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();
});
});
});
Loading