Skip to content

[Security] Slippage Protection Significantly Weakened in swapAndAdd for Certain Price Ranges #514

@hyk51594176

Description

@hyk51594176

[Security] Slippage Protection Significantly Weakened in swapAndAdd for Certain Price Ranges

Summary

The swapAndAddCallParameters method in @uniswap/router-sdk has a critical design flaw that can significantly weaken slippage protection from the intended 3% to as much as 30-40% in certain scenarios, particularly when the position's price range causes mintAmountsWithSlippage to return substantially reduced amounts. This creates a large MEV attack surface.

Environment

  • Package: @uniswap/router-sdk
  • Version: [Current version]
  • Affected Method: SwapRouter.swapAndAddCallParameters

Problem Description

When using swapAndAddCallParameters , the swap slippage protection ( minimumAmountOut ) can be overridden by a much weaker protection value from mintAmountsWithSlippage , especially when:

  1. The position's price range is wide or near boundaries
  2. Multiple trades trigger aggregated slippage check (>2 trades)
  3. The current pool price is close to the position's range boundaries

Root Cause

The issue occurs in the interaction between three components:

1. Aggregated Slippage Check (swapRouter.ts:432)

const performAggregatedSlippageCheck = 
  sampleTrade.tradeType === TradeType.EXACT_INPUT && numberOfTrades > 2

// When triggered, individual swaps have amountOutMinimum = 0

2. Minimal Position Construction (swapRouter.ts:618-625)

const minimalPosition = Position.fromAmounts({
  pool: position.pool,
  tickLower: position.tickLower,
  tickUpper: position.tickUpper,
  amount0: zeroForOne ? position.amount0 : minimumAmountOut,  // Uses swap's minimumAmountOut
  amount1: zeroForOne ? minimumAmountOut : position.amount1,  // Uses swap's minimumAmountOut
  useFullPrecision: false,
})

3. Min Value Selection (approveAndCall.ts:82-87)

let { amount0: amount0Min, amount1: amount1Min } = 
  position.mintAmountsWithSlippage(slippageTolerance)

// Takes the SMALLER value between two protections
if (JSBI.lessThan(minimalPosition.amount0.quotient, amount0Min)) {
  amount0Min = minimalPosition.amount0.quotient
}
if (JSBI.lessThan(minimalPosition.amount1.quotient, amount1Min)) {
  amount1Min = minimalPosition.amount1.quotient
}

The problem: mintAmountsWithSlippage can return values much smaller than minimalPosition amounts due to Uniswap V3's concentrated liquidity mechanics. When this happens, the weaker protection overwrites the stronger one.

Reproduction Steps

Scenario Setup

// User wants to swap USDC → cbBTC and add liquidity
const slippageTolerance = new Percent(300, 10000)  // 3%

// Position with relatively wide price range
const position = new Position({
  pool: cbBTC_USDC_POOL,
  tickLower: 62082,   // Lower price bound
  tickUpper: 75878,   // Upper price bound (22% width)
  liquidity: DESIRED_LIQUIDITY
})

// Swap trade (3 routes, triggers aggregated slippage check)
const trades = [trade1, trade2, trade3]  // numberOfTrades > 2

const { calldata, value } = SwapRouter.swapAndAddCallParameters(
  trades,
  { slippageTolerance, ... },
  position,
  addLiquidityOptions,
  ApprovalTypes.MAX,
  ApprovalTypes.MAX
)

Expected Protection

Swap slippage: 3%
minimumAmountOut for cbBTC: ~97% of expected output
Final amount1Min should be: ~97% of expected cbBTC

Actual Behavior

Calculation flow:
1. minimumAmountOut (from swap): 1.455 cbBTC (3% slippage)
2. minimalPosition.amount1: 1.455 cbBTC
3. position.mintAmountsWithSlippage(3%): 1.019 cbBTC (30% reduction due to price range)
4. Final amount1Min = min(1.019, 1.455) = 1.019 cbBTC (30% slippage!)

Result: Protection degraded from 3% to 30%

Real-World Example

From an actual on-chain transaction:

User input: 188,088 USDC
Swap amount: 88,740 USDC
Expected remaining for LP: ~99,348 USDC

After mintAmountsWithSlippage:
- amount0Min: 70,609 USDC
- Reduction: (99,348 - 70,609) / 99,348 ≈ 29%

This means:
- User expected: 3% slippage protection
- Actual protection: ~30% slippage (10x weaker!)

Security Impact

Attack Scenario

1. User submits swapAndAdd transaction with 3% slippage
2. MEV bot front-runs: Buys cbBTC to pump price
3. User's swap executes at inflated price:
   - Expected: 1.5 cbBTC
   - Gets: 1.05 cbBTC (30% loss)
4. Validation:
   - Swap check: amountOutMinimum = 0  (passes)
   - LP check: 1.05 >= 1.019  (passes)
5. MEV bot back-runs: Sells cbBTC for profit
6. Transaction succeeds, user loses 30% instead of max 3%

Risk Assessment

Condition Risk Level Potential Loss
Normal price range (< 10% reduction) 🟢 Low < 5%
Medium price range (10-20% reduction) 🟡 Medium 5-15%
Wide/boundary range (> 20% reduction) 🔴 High 15-40%

Additional Context

Why mintAmountsWithSlippage Can Reduce Amounts Significantly

The method calculates amounts needed for adding liquidity at price boundaries:

// position.ts:157-205
public mintAmountsWithSlippage(slippageTolerance: Percent) {
  const { sqrtRatioX96Upper, sqrtRatioX96Lower } = 
    this.ratiosAfterSlippage(slippageTolerance)
  
  // Creates positions at price boundaries
  // When position is near range boundaries, the amount change is non-linear
  // A 3% price change can result in 30%+ amount reduction
}

This is by design for Uniswap V3's concentrated liquidity, but creates a security issue when used to override swap slippage protection.

Observed in Practice

Some implementations appear to add an additional sweepToken validation after swaps:

calldatas = [
  swap1, swap2, swap3,
  sweepToken(USDC, minimumRemaining),  // ← Extra validation step observed
  pullTokens,
  approve,
  mintLP,
  sweep
]

This suggests the issue may be known, but the standard SDK implementation does not include this protection.

References

  • Code: sdks/router-sdk/src/swapRouter.ts:563-653
  • Related: sdks/router-sdk/src/approveAndCall.ts:71-113
  • V3 SDK Position: sdks/v3-sdk/src/entities/position.ts:157-205

Thank you for your attention to this security concern. I'm happy to provide more details if needed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions