Uniswap v4

Liquidity Mechanics in Uniswap v4 Core

Explore Uniswap v4 core liquidity mechanics, including hooks, singleton design, flash accounting & gas optimizations for decentralized trading.

Last updated: 8/28/2025

To understand the significance of concentrated liquidity, we must first examine the limitations of Uniswap v2's approach. In v2, liquidity providers deposited their tokens into pools where liquidity was distributed uniformly across the entire price curve from zero to infinity.
 

Inefficiency Challenge

Infinite price curve approach created several critical problems:

  • Capital Inefficiency: Liquidity in v2 pools was distributed infinitely that allowing trades at any price from 0 to infinity. This made capital extremely inefficient. For example, the historical price range of ETH spans from $0.75 to $4,800, yet v2 provided liquidity for scenarios where ETH might trade at $50,000 or $0.01 price ranges that would never realistically be reached.

    image - 2025-08-27T131208.107.webp
     

  • Poor Stablecoin Trading: Pegged tokens like USDC and USDT require high liquidity to maintain their 1:1 peg, regardless of trade size. v2's general AMM algorithm wasn't well-suited for stablecoin trading, which is why alternative AMMs like Curve dominated this market.

     
  • Wasted Liquidity: Most of the liquidity sat unused in price ranges far from current market prices. If ETH was trading at $2,000, providing liquidity for ETH at $10,000 was essentially pointless yet that's exactly what V2 forced LPs to do.

     
  • Higher Slippage for Traders: With liquidity spread thinly across all price ranges, traders experienced higher slippage, especially for larger swaps. The lack of concentrated liquidity around current market prices meant that even moderate-sized trades could move prices significantly.
     

How Concentrated Liquidity Works?

image - 2025-08-27T131241.073.webp

Instead of spreading liquidity uniformly, v3 and v4 enable LPs to:

  • Select Custom Price Ranges: LPs can choose specific price boundaries (between points 'a' and 'b') where they want their liquidity to be active
  • Concentrate Capital Strategically: Multiple LPs can overlap their ranges, creating deep liquidity pools around expected trading ranges
  • Activate and Deactivate Automatically: When prices move outside an LP's range, their liquidity becomes inactive until prices return

In v3/v4, however, the liquidity is spread around more like this:

image - 2025-08-27T131249.079.webp

LPs choose a "price range" where they add their liquidity. These "liquidity buckets" can overlap with each other, increasing the total amount of liquidity available with a subsection of their price ranges - and can be non-overlapping as well.

In a real example from an ETH/USDC pool from Uniswap v3/v4, we can see the spread of liquidity looks like this:

image - 2025-08-27T131255.339.webp

Price Range Mechanics

When you provide liquidity to a specific price range, you're essentially creating a finite curve between two points rather than an infinite one. This has several important implications:

  • Range Depletion: When the price moves to either boundary of your range, the pool becomes depleted one of the token reserves becomes zero, and buying that token becomes impossible within that range. and there is liquidty available but there is ditamd between two liquidity that
     
  • Price Slippage Between Ranges: When the current price range gets depleted during a trade, the price slips into the next available price range. If no next range exists, the trade is only partially fulfilled.
     
  • Limit Order Functionality: An interesting side effect of this mechanism is that price ranges can function as limit orders, automatically executing when prices reach specific levels.
     

How to create new pool?

Uniswap V4 uses a Singleton pattern (all pools in one contract) and does not have a traditional factory like v3. Instead, pools are created directly in the PoolManager contract.

However, note that in v4, the PoolManager itself acts as a factory for pools. You create a new pool by initializing it with a unique set of parameters (token0, token1, fee, tickSpacing, hooks).

Steps to create a new pool in Uniswap v4:

  1. Define the pool parameters (PoolKey struct).
  2. Call initialize on the PoolManager with the initial sqrtPriceX96.

image - 2025-08-27T131301.269.webp

After initializing the pool, the pool is created but has zero liquidity. Now, the first liquidity provider must add liquidity.

However, note: the first liquidity provider must set the initial price correctly (which is already set by the initialize call). The liquidity is added by minting a position that covers the current price.
 

How modifyLiquidity Function Works?

image - 2025-08-27T131308.461.webp

  1. User uses a periphery contract to initiate the liquidity modification.
  2. Periphery contract unlocks the PoolManager which then does a callback to unlockCallback
  3. Inside the callback, the periphery contract triggers modifyLiquidity on the manager.
  4. The manager ensures the pool is valid and is initialized.
  5. Manager figures out if the modification is an addition or removal of liquidity.
  6. Manager checks if the associated before hook needs to be called based on the hook's address - and calls it if necessary.
  7. The actual liquidity modification logic runs and the balance delta is calculated
  8. The ModifyLiquidity event is emitted.
  9. The manager checks if the associated after hook needs to be called, and calls it if necessary.
  10. The balance delta is returned to the periphery contract.
  11. The periphery contract settles all balances (by transferring tokens or minting/burning 6909 claims)
  12. unlockCallback finishes execution, Manager ensures no pending non-zero deltas, and locks itself.
  13. Transaction is complete.

Now, providing initial liquidity: We must mint a position that includes the current price. The current price is the one we set during initialization.
 

Core Math Behind Liquidity Modification

In Uniswap v3/v4, liquidity providers (LPs) must define a price range (tickLower and tickUpper) where their capital is active. Let’s break down how to calculate these ticks for an ETH/USDC pool where:

  • Current price: 1 ETH = $5000 USDC
  • Liquidity range: $4545 (lower) to $5500 (upper)
     

Step 1: Understand the Tick-Price Relationship

Uniswap v3 uses the formula:

1Price = 1.0001^tick

Each tick represents a 0.01% change in price.

To convert between price ↔ tick, use logarithms:

1tick = log₁.₀₀₀₁(price)

 

Step 2: Calculate Ticks for Your Price Range

  • Current Price: $5000/ETH
1tickCurrent = log₁.₀₀₀₁(5000) ≈ 85176
  • Lower Price Bound: $4545/ETH
1tickLower = log₁.₀₀₀₁(4545) ≈ 84222
  • Upper Price Bound: $5500/ETH
1tickUpper = log₁.₀₀₀₁(5500) ≈ 8602

Q64.96 Number Format

Last thing to note here is that Uniswap uses Q64.96 number to store P. This is a fixed-point number that has 64 bits for the integer part and 96 bits for the fractional part.

In our above calculations, prices are floating point numbers: 70.71, 67.42, and 74.16. We need to convert them to Q64.96.

$$\sqrt{P_c} = 5602277097478614198912276234240 $$

$$ \sqrt{P_l} = 5314786713428871004159001755648 $$

$$ \sqrt{P_u} = 5875717789736564987741329162240 $$

 

Step 3: Adjust for Tick Spacing

Uniswap pools have a tickSpacing depending on the fee tier.

For example, a 0.3% fee tier pool has tickSpacing = 60.

  • Rounded Lower Tick:
184222 ÷ 60 = 1403.7 → floor(1403.7) = 1403
21403 × 60 = 84180
  • Rounded Upper Tick:
186029 ÷ 60 = 1433.8 → ceil(1433.8) = 1434
21434 × 60 = 86040

✅ Final Tick Range:

1tickLower = 84180  (~$4543)
2tickUpper = 86040  (~$5502)

 

Step 4: Verify Price Range

Convert ticks back to price to confirm:

  • Lower Price ≈ 1.0001^84180 ≈ $4543
  • Upper Price ≈ 1.0001^86040 ≈ $5502

This confirms your liquidity will only be active between ~$4543 and ~$5502.

 

Now Lp provider call the modifyLiquidity function in PoolManager that check some condition and before and after here the hooks to build we talk in next blog no it, and then call internal function modifyLiquidity of pool.

image - 2025-08-27T131318.638.webp

Modifyliquidity in the pool is the bigger function; let's understand it by breaking it down into several parts.

image - 2025-08-27T131326.655.webp

If the user adds liquidity more than 0, this condition applies, and the following state should be updated.

image - 2025-08-27T131332.198.webp

  • first the updateTick internal function that
  • liquidityDelta != 0: Only execute if there's an actual change in liquidity (positive for adding, negative for removing)
  • updateTick(self, tickLower, liquidityDelta, false):
    • Updates the lower tick of your position
    • false indicates this is the lower bound
    • Returns whether the tick "flipped" (changed from initialized to uninitialized or vice versa)
    • Returns the new gross liquidity at this tick
  • updateTick(self, tickUpper, liquidityDelta, true):
    • Updates the upper tick of your position
    • true indicates this is the upper bound
    • Same return values as lower tick
       

updateTick function is responsible for:

  1. Adjusting liquidity at specific price ticks when positions are modified
  2. Tracking fee accumulations around tick boundaries
  3. Managing tick initialization state changes
  4. Calculating net liquidity changes for efficient swap routing

When positions are changed, this internal function manages modifying liquidity at particular price ticks while preserving precise fee tracking and effective storage operations.
 

UpdateTick carefully updates the tick's liquidity values whenever liquidity providers add or remove funds. It also checks to see if the initialization state of the tick has changed, which is crucial for accurate fee accounting and swap routing.

In order to prevent underflows, the function first loads the current tick data and uses safe mathematical operations to determine the new liquidity amount. Its capacity to identify tick transitions between initialized and uninitialized states, which influences the tracking of fees at these price points, is a crucial feature. To guarantee appropriate fee distribution for newly initialized ticks, the function logs the current global fee accumulations. Additionally, it manages the intricate computation of net liquidity differently for upper and lower ticks, which is necessary for effective swap execution.
 

What makes updateTick particularly innovative is its optimized storage approach. Using low-level assembly, it efficiently packs both gross and net liquidity values into a single storage slot, significantly reducing gas costs. This optimization is especially valuable given how frequently these operations occur during normal pool activity. The function's careful design ensures liquidity providers get accurate fee accounting while maintaining the protocol's high performance standards, making it a cornerstone of Uniswap V4's improved concentrated liquidity model.

image - 2025-08-27T131341.758.webp

Actually a liquidity change (liquidityDelta >= 0), and if so, it calculates the maximum liquidity that can be safely stored at any single tick based on the pool's tick spacing. This maximum is determined by the tickSpacingToMaxLiquidityPerTick function, which ensures that even with the densest possible tick spacing, the liquidity values won't overflow the uint128 storage type.

The protocol then performs two critical checks on the gross liquidity after the proposed update. First, it verifies that the lower tick (tickLower) won't exceed the maximum liquidity limit by comparing state.liquidityGrossAfterLower against maxLiquidityPerTick. If this check fails, the transaction reverts with a TickLiquidityOverflow error, preventing the liquidity addition. Similarly, it performs the same verification for the upper tick (tickUpper) by checking state.liquidityGrossAfterUpper.

This overflow protection is essential because Uniswap uses fixed-point arithmetic with uint128 for liquidity values, and allowing overflow could lead to catastrophic failures where liquidity calculations become incorrect, potentially allowing users to drain the pool or causing other serious vulnerabilities.

image - 2025-08-27T131350.183.webp

This code snippet manages the tick bitmap in Uniswap, which is a critical gas optimization mechanism. When liquidity positions are created or removed, certain ticks may "flip" their state - transitioning from having no liquidity to having liquidity, or vice versa. The bitmap efficiently tracks which ticks are initialized (have liquidity) using individual bits rather than expensive storage slots.
 

The conditional checks if (state.flippedLower) and if (state.flippedUpper) ensure that the bitmap is only updated when necessary. When a tick flips, the flipTick function toggles the corresponding bit in the bitmap, which helps the protocol quickly identify which ticks are active during price movements and swaps.
 

In your example as the first depositor, both your lower tick (4545) and upper tick (5500) will flip from uninitialized to initialized, causing both conditions to be true. This means the bitmap will be updated to mark both ticks as active, which costs additional gas for the initialization but creates the infrastructure for efficient future operations.

image - 2025-08-27T131355.922.webp

  1. Fee Growth Tracking:
    • The contract tracks fees accumulated by liquidity providers (LPs) using feeGrowthInside values
    • These are stored as Q128.128 fixed-point numbers (hence the X128 suffix)
    • There are separate values for token0 (feeGrowthInside0X128) and token1 (feeGrowthInside1X128)
       
  2. Position Management:
    • self.positions.get() retrieves a specific liquidity position using:
      • Owner address
      • Lower tick bound
      • Upper tick bound
      • Salt (for position differentiation)
         
  3. Fee Calculation:
    • position.update() calculates the fees owed to this position based on:
      • liquidityDelta - Change in liquidity (can be positive or negative)
      • The fee growth values inside the position's tick range
         
  4. Return Value:
    • The owed fees are converted to a balance delta (change in token balances)
    • toBalanceDelta() packages the fee amounts into a structured return value

image - 2025-08-27T131402.817.webp

This would typically appear in functions that modify liquidity positions (like adding/removing liquidity), where:

  • The contract first updates tick states
  • Then cleans up unused ticks to save storage space
  • This pattern helps reduce the blockchain storage burden and gas costs for users

image - 2025-08-27T131410.466.webp

  1. Price-Position Relationship:
    • Below range → Only token0 required
    • Within range → Both tokens required
    • Above range → Only token1 required
       
  2. Token Amount Calculation:
    • Uses SqrtPriceMath for precision (Q64.96)
    • getAmount0Delta(): Computes token0 amount
    • getAmount1Delta(): Computes token1 amount
       
  3. Liquidity Update:
    • Uses LiquidityMath.addDelta() for safe signed/unsigned conversion
    • Prevents overflow/underflow vulnerabilities
       
  4. Gas Optimization:
    • Skips calculation if liquidityDelta == 0
    • Uses tick-based math instead of full price recalculation
       

Similar to Uniswap v2 - v3 still required that each pool was its own smart contract deployed through the UniswapV3Factory contract. This made certain actions such as creating a new pool, or doing a multi-hop swap, expensive actions.

image - 2025-08-27T131416.454.webp

Uniswap v4 Disign:

image - 2025-08-27T131421.584.webp

As shown in the image above, there now exists a single PoolManager smart contract. This PoolManager contains the mapping to identify all pools in existence - but they all also live within the PoolManager contract itself instead of being their own contracts.

This design is possible because pools now are implemented as Solidity libraries. This is a bit abstract - so let's see how it is in code

1// Pool Library
2library Pool {
3	function initialize(State storage self, ...) {
4      ...
5    }
6  
7  	function swap(State storage self, ...) {
8      ...
9    }
10  
11  	function modifyPosition(State storage self, ...) {
12      ...
13    }
14}
15
16// PoolManager
17contract PoolManager {
18
19	using Pools for *;
20	mapping (PoolId id => Pool.State) internal pools;
21
22	function swap(PoolId id, ...) {
23		pools[id].swap(...); // Library Call
24	}
25}

Unlike v3 where the pools mapping final nested value pointed to a contract address - in v4 the pools mapping points to a Pool.State struct. Using the Pool library on this struct, all the AMM logic gets encapsulated within the PoolManager contract directly requiring no external function calls.

The Pool.State struct itself contains mostly the same information as a Uniswap v3 Pool would contain. In v3, the pool state was stored per-contract as storage values and accessible through various view functions - now they're just part of the struct.

For example, in Uniswap v3 - view functions like slot0() existed on each Pool contract to return some crucial information

1interface IUniswapV3PoolState {
2	function slot0() external view returns (
3		uint160 sqrtPriceX96,
4		int24 tick,
5		uint16 observationIndex,
6		uint16 observationCardinality,
7		uint16 observationCardinalityNext,
8		uint8 feeProtocol,
9		bool unlocked
10	);
11
12	// Other view functions for a few other things
13}
14

The same exists in Uniswap v4, just as getter/setter functions you access differently from the PoolManager.

So - to conclude - v4 has a singleton design where all Pools and their logic becomes encapsulated within a single PoolManager contract, made possible by offloading most of the logic to libraries and calling functions through those libraries rather than deploying new contracts entirely for each pool.

Having explored how Uniswap v4 handles InitializePool and ModifyLiquidity, along with key concepts like tick math, we’ll next dive into the swap function. Stay tuned for the upcoming blog!

Previous
High Level Understanding of Un...
Next
Swap Mechanics in Uniswap v4 C...

STAY IN THE LOOP

Get updates on our community, partners, events, and everything happening across the ecosystem — delivered straight to your inbox.

Subscribe Now!

newsletter
DeFi SecurityplumeUniswap FoundationAethiropt-collectivePolygon SPNBNB Chain Kickstart

Office 104/105 Level 1, Emaar Square, Building 4 Sheikh Mohammed Bin Rashid Boulevard Downtown Dubai, United Arab Emirates P.O box: 416654

audits@quillaudits.com

All Rights Reserved. © 2025. QuillAudits - LLC

Privacy Policy