Vanguard

First Flight #56
Beginner FriendlyDeFiFoundry
0 EXP
Submission Details
Impact: high
Likelihood: high

Initial Liquidity Sandwich Attack Permanently Griefs All Users - maxSwapAmount Based on Manipulated Value

Author Revealed upon completion

## Root + Impact

In Uniswap V4, pool initialization and liquidity provision are separate operations. When `_afterInitialize` runs, there is typically NO liquidity in the pool yet, so `initialLiquidity = 0`. The re-read in `_beforeSwap` can be sandwiched by an attacker to permanently set `initialLiquidity` to an artificially low value, causing `maxSwapAmount` to be extremely small forever.

## Description

The `initialLiquidity` value determines `maxSwapAmount = (initialLiquidity * limitBps) / 10000`. An attacker can sandwich the first swap to manipulate this value permanently:

```solidity

function _afterInitialize(...) internal override returns (bytes4) {

// ...

uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());

initialLiquidity = uint256(liquidity); // @> This is 0 - no liquidity at init time!

// ...

}

function _beforeSwap(...) {

// ...

if (initialLiquidity == 0) {

uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());

initialLiquidity = uint256(liquidity); // @> Can be sandwiched!

}

// ...

}

```

## Risk

**Likelihood: HIGH**

- Pool initialization flow in Uniswap V4 separates init from liquidity provision

- First swap ALWAYS triggers the re-read when initialLiquidity is 0

- MEV bots constantly monitor for sandwich opportunities

**Impact: HIGH**

- All users permanently limited to tiny swap amounts

- Token launch is completely griefed

- No mechanism to fix - initialLiquidity never recalculated

## Proof of Concept

```solidity

function testInitialLiquiditySandwich() public {

// 1. Initialize pool - initialLiquidity = 0

poolManager.initialize(poolKey, sqrtPriceX96);

// 2. LP adds 1,000,000 liquidity

poolManager.modifyLiquidity(..., liquidityDelta: 1_000_000e18);

// 3. Attacker removes 99.99% before first swap

vm.prank(attacker);

poolManager.modifyLiquidity(..., liquidityDelta: -999_999e18);

// 4. First swap triggers re-read with tiny liquidity

poolManager.swap(poolKey, swapParams);

// initialLiquidity now = 1_000e18 (0.1% of actual)

// 5. Attacker re-adds liquidity

poolManager.modifyLiquidity(..., liquidityDelta: 999_999e18);

// 6. maxSwapAmount permanently griefed

// With limitBps=100, max swap = 10 tokens instead of 10,000!

}

```

## Recommended Mitigation

```diff

function _beforeSwap(...) {

- if (initialLiquidity == 0) {

- uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());

- initialLiquidity = uint256(liquidity);

- }

+ uint128 currentLiquidity = StateLibrary.getLiquidity(poolManager, key.toId());

+ if (currentLiquidity > initialLiquidity) {

+ initialLiquidity = currentLiquidity; // Only increase, never decrease

+ }

+ require(initialLiquidity > 0, "No liquidity");

}

```Root + Impact

Description

  • Describe the normal behavior in one or more sentences

  • Explain the specific issue or problem in one or more sentences

// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

  • Reason 2

Impact:

  • Impact 1

  • Impact 2

Proof of Concept

Recommended Mitigation

- remove this code
+ add this code

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!