Introduction
Many people, including myself, felt overwhelmed when first encountering the concept of
Uniswap V3. Even if you're familiar with Uniswap V2, there are many new concepts and
features in Uniswap V3 to understand. One of them is ticks.
You've probably seen descriptions like this: Uniswap V3 revolutionized automated market making (AMM) by introducing concentrated liquidity. Unlike
Uniswap V2, where liquidity is spread across the entire price curve (0 to ∞), V3 allows liquidity
providers (LPs) to concentrate their capital within specific price ranges.
This definition wasn't very clear to me at first. I tried to understand it better by reading the Uniswap V3 contract code, but quickly felt overwhelmed. The code related to ticks is quite complex. I wanted to find a well-balanced explanation—not too general, but not too complex either.
In this article, I'll explain what ticks are from a conceptual level. I hope this article can help people like my past self understand the concept of ticks easily without feeling intimidated. For implementation details, I'll cover those in a separate article.
One-Dimensional Difference Array Technique
Before diving into how Uniswap V3 uses ticks, let's understand a fundamental algorithmic technique that makes it all possible: the 1D Difference Array. This classic optimization technique efficiently handles range updates on arrays.
The Problem: Imagine you have an array of values (all starting at 0), and you need to add or subtract values across ranges [L, R) many times. Calculate the value of the array after all updates.
Naive Solution: Iterates through every element in each range and updates it, resulting in O(n) time complexity per update. With many updates, this becomes very inefficient.
- Mark
+Vat index L (where the range begins) - Mark
-Vat index R (where the range ends)
To get the actual value at any index, you accumulate all changes from the beginning. This reduces each range update from O(n) to O(1).
Operations:
- Add 100 to range [2, 5)
- Add 75 to range [1, 6)
- Subtract 50 from range [3, 5)
Delta Array (boundary changes):
Only store what changes at each boundary:
Each operation only modifies 2 positions: the start and end boundaries.
For example, value at index 5 is: -100 + 50 = -50
Final Array (accumulated values):
Accumulate deltas from left to right to get actual values:
Understanding Ticks the Easy Way
At its core, ticks are represented as an array.
Each element in this array contains information about a specific price point. The most important
piece of information stored at each tick is liquidityNet—the change in liquidity at that price point.
To start, you can think of ticks as an array where each index mapping to a discrete price point, and the value at that index tells you how liquidity changes when the price crosses that point. It's similar to the difference array technique we discussed earlier.
Basic Terminology
An Uniswap pool contains two tokens (like ETH/USDC or ETH/BTC). Throughout this article, we'll use ETH/USDC as our example, where:
ETHis token X (the base token)USDCis token Y (the quote token)Pricemeans how manyUSDCper 1ETH(e.g., price = 2000 means 1ETH= 2000USDC)Price range[2000, 3000) means a range of price of the pool is from 2000 to 3000, excluding 3000
Each Tick Represents a Price Point
In Uniswap V3, each tick index
corresponds to a specific price. The relationship between a tick index and its price is
defined by a mathematical formula. Therefore, some price range can also be represented
by a tick range, with each tick corresponding to a specific price.
For this article, we'll use a simple linear formula to make the concepts easier to follow:
With this formula, assuming ETH price ranges from $4 to $4,000:
- Tick 1 = $4
- Tick 500 = $2,000
- Tick 750 = $3,000
- Tick 1,000 = $4,000
In the actual Uniswap V3 implementation, the price-to-tick relationship uses an exponential formula:
This exponential spacing allows the protocol to support an enormous price range (from nearly 0 to nearly infinity) with consistent percentage-based tick spacing. We use a simplified linear formula in this article to make the core concepts easier to understand.
Concentrated Liquidity with a Price Range
Reminder: The key innovation in Uniswap V3 is concentrated liquidity. Instead of spreading capital across all possible prices, liquidity providers can concentrate it within a specific price range where they expect trading to occur.
When providing liquidity in V3, liquidity providers specify:
- Liquidity amount - How much capital to provide
- Price range - The minimum and maximum prices for the position
Liquidity providers choose specific price ranges. Notice how different providers' ranges can overlap, creating varying liquidity depths at different price points. The prices in these ranges are not arbitrary—they correspond to specific tick indices.
Operations:
- Adds 1,000 liquidity in range [$2,000, $3,000) → ticks [500, 750)
- Adds 2,000 liquidity in range [$2,500, $3,500) → ticks [625, 875)
- Removes 500 liquidity from range [$2,000, $2,500) → ticks [500, 625)
Resulting Liquidity Array:
The liquidity at each tick index (showing key ticks in the array):
The array shows liquidity at each tick. Ticks with green backgrounds and bold borders (500, 625, 750, 875) are boundary ticks where liquidity changes. Notice how liquidity remains constant between boundaries: 0 before tick 500, then 500 from ticks 500-624, then 2,500 from ticks 625-749, then 2,000 from ticks 750-874, and back to 0 from tick 875 onwards.
Contrast with Uniswap V2: In V2, there is no concept of price ranges. Every liquidity provider's capital is automatically spread across the entire price curve, from 0 to infinity. This means your liquidity is available at all prices, even extremely unlikely ones.
Consider the same operations in Uniswap V2:
- Adds 1000 liquidity (spread across all prices)
- Adds 2000 liquidity (spread across all prices)
- Removes 500 liquidity (spread across all prices)
Result: Total liquidity = 2,500 (1000 + 2000 - 500) uniformly distributed across the entire price range [$4, $4,000).
Resulting Liquidity Array:
In V2, liquidity is spread uniformly across all ticks (every tick has the same liquidity):
Unlike V3's concentrated liquidity, V2 spreads the same 2,500 liquidity uniformly across every single tick from 0 to 1000. There are no boundaries or variations—every price point has identical liquidity depth, resulting in capital inefficiency.
Why this matters: By concentrating liquidity where trading actually
happens, Uniswap V3 allows liquidity providers
to offer deeper liquidity with the same amount of capital as Uniswap V2. This means liquidity providers can earn more fees with less capital. However, if the
price moves outside the chosen range, the position stops earning fees until the price
returns to that range.
How Swaps Change Current Liquidity
In the previous section, we somehow saw how liquidity is stored at different ticks. But which liquidity value does the pool actually use during a swap? The pool tracks two critical state variables:
liquidity- The current active liquidity available for tradingtick(or price) - The current price point of the pool
When a swap happens, the price moves up or down depending on the swap direction (buying
or selling). As the price crosses tick boundaries (a tick that is tick start or tick end
when providing liquidity), the liquidity value
must be updated to reflect the new liquidity depth at the new price level.
Initial State:
- We have the following liquidity distribution
- Current tick: 600 (price = $2,400)
- Current liquidity: 500
The array shows liquidity at each tick. Ticks with green backgrounds and bold borders (500, 625, 750, 875) are boundary ticks where liquidity changes. Notice how liquidity remains constant between boundaries: 0 before tick 500, then 500 from ticks 500-624, then 2,500 from ticks 625-749, then 2,000 from ticks 750-874, and back to 0 from tick 875 onwards.
Scenario: Let's trace how liquidity changes during swap
- Swap USDC for ETH. Price increases from tick 600 → 624
- No boundary crossed
- Liquidity remains: 500
- Swap USDC for ETH. Price increases from tick 624 → 700
- Crossed boundary at tick 625
- New liquidity: 2500
- Swap ETH for USDC. Price decreases from tick 700 → 600
- Crossed boundary at tick 625
- New liquidity: 500
- Swap ETH for USDC. Price decreases from tick 600 → 500
- No boundary crossed
- Liquidity remains: 500
- Swap ETH for USDC. Price decreases from tick 500 → 499
- Crossed boundary at tick 500
- New liquidity: 0, swap failed
Tick Management in Action
Now that we understand how ticks work conceptually, let's walk through a realistic
sequence of operations in an ETH/USDC pool. We'll
examine exactly how adding liquidity, removing liquidity, and swapping affect the pool's current
tick and current liquidity. For clarity, we'll continue using our simplified model where price = 4 × tick.
Let's start with an empty ETH/USDC pool and build it up step by step:
- Current tick: 500 (price = $2,000)
- No liquidity positions exist yet
- Current liquidity: 0 (pool is empty)
Operation 1: Add Liquidity
Action:
Adds 1,000 liquidity in range [$2,000, $3,000) → ticks [500, 750)
What happens:
- Update tick 500: liquidityNet += 1,000
- Update tick 750: liquidityNet -= 1,000
- Since current tick (500) is within range, update current liquidity
Pool state after:
- Current tick: 500 (unchanged - adding liquidity doesn't move price)
- Current liquidity: 0 + 1,000 = 1,000
liquidityNet Array (shows deltas at boundaries):
Only boundaries have non-zero values: tick 500 gets +1,000 (range start) and tick 750 gets -1,000 (range end).
Operation 2: Swap USDC for ETH
Action:
Swap USDC for ETH, pushing price from $2,000 to $2,600
What happens:
- Price moves from tick 500 → tick 650
- No tick boundaries crossed (still within [500, 750))
- Current liquidity stays the same
Pool state after:
- Current tick: 500 → 650 (price moved)
- Current liquidity: 1,000 (unchanged - no boundary crossed)
Operation 3: Add More Liquidity
Action:
Add 2,000 liquidity in range [$2,500, $3,500) → ticks [625, 875)
What happens:
- Update tick 625: liquidityNet += 2,000
- Update tick 875: liquidityNet -= 2,000
- Current tick (650) is within the new range [625, 875), so update current liquidity
Pool state after:
- Current tick: 650 (unchanged)
- Current liquidity: 1,000 + 2,000 = 3,000
liquidityNet Array (shows deltas at boundaries):
First position: +1,000 at tick 500, -1,000 at tick 750. Second position: +2,000 at tick 625, -2,000 at tick 875. All non-boundary ticks remain 0.
Operation 4: Swap ETH for USDC (Crosses Boundary)
Action:
Swap ETH for USDC, pushing price from $2,600 down to $2,400
What happens:
- Price moves from tick 650 → tick 600
- Crosses tick boundary 625 (going left/down)
- When crossing tick 625 going left, apply liquidityNet[625] = +2,000
- Current liquidity: 3,000 - 2,000 = 1,000
Pool state after:
- Current tick: 650 → 600 (price decreased)
- Current liquidity: 3,000 → 1,000 (crossed boundary at 625)
Key insight: When price crosses a boundary moving left (price decreasing), we subtract the liquidityNet value because we're leaving that range.
Operation 5: Add Liquidity Out of Range
Action:
Add 1,500 liquidity in range [$3,600, $4,000) → ticks [900, 1000)
What happens:
- Update tick 900: liquidityNet += 1,500
- Update tick 1000: liquidityNet -= 1,500
- Current tick (600) is outside this range, so current liquidity stays unchanged
Pool state after:
- Current tick: 600 (unchanged)
- Current liquidity: 1,000 (unchanged - position is out of range)
Key insight: Adding liquidity outside the current price range doesn't affect current liquidity. This liquidity will only become active when the price moves into that range.
liquidityNet Array (shows deltas at boundaries):
Now we have three positions: +1,000 at tick 500/-1,000 at tick 750, +2,000 at tick 625/-2,000 at tick 875, and +1,500 at tick 900/-1,500 at tick 1000. The new position at [900, 1000) doesn't affect current liquidity since current tick is 600.
Operation 6: Remove Partial Liquidity
Action:
Remove 400 liquidity from range [$2,000, $3,000) → ticks [500, 750)
What happens:
- Update tick 500: liquidityNet -= 400 (from +1,000 to +600)
- Update tick 750: liquidityNet += 400 (from -1,000 to -600)
- Current tick (600) is within this range, so update current liquidity
Pool state after:
- Current tick: 600 (unchanged)
- Current liquidity: 1,000 - 400 = 600
Final liquidityNet Array (shows deltas at boundaries):
After partial removal, the first position has reduced deltas: +600 at tick 500 and -600 at tick 750. The other positions remain unchanged. At current tick 600, the active liquidity is 600 from the partially removed position.
Operation 7: Swap Crossing Boundary (Price Increase)
Action:
Swap USDC for ETH, pushing price from $2,400 to $2,800
What happens:
- Price moves from tick 600 → tick 700
- Crosses tick boundary 625 (going right/up)
- When crossing tick 625 going right, apply liquidityNet[625] = +2,000
- Current liquidity: 600 + 2,000 = 2,600
Pool state after:
- Current tick: 600 → 700 (price increased)
- Current liquidity: 600 → 2,600 (crossed boundary at 625)
Key insight: When price crosses a boundary moving right (price increasing), we add the liquidityNet value because we're entering that range. Now at tick 700, both the first position [500, 750) and second position [625, 875) are active, providing combined liquidity of 600 + 2,000 = 2,600.
Summary of Operations
- Adding liquidity: Updates liquidityNet at boundaries, increases current liquidity if position includes current tick
- Removing liquidity: Updates liquidityNet at boundaries, decreases current liquidity if position includes current tick
- Swapping: Moves current tick (price), updates current liquidity only when crossing tick boundaries
Important Notes:
- Add/Remove operations are immediate: When adding or removing liquidity, the liquidityNet updates at boundaries take effect immediately, and current liquidity is updated if the current tick falls within the position's range.
- Swap optimization in practice: In the examples above, we showed swaps crossing boundaries tick by tick for educational clarity. However, in the actual Uniswap V3 implementation, the swap algorithm is optimized—it doesn't iterate through every tick. Instead, it finds the next initialized tick (a tick with non-zero liquidityNet), performs the swap calculation between those initialized ticks, then updates the current liquidity only when crossing an initialized boundary. This makes swaps extremely gas-efficient even with thousands of potential ticks.
Conclusion
In this article, we explored Uniswap V3's tick system from a conceptual perspective, breaking down what seemed like a complex topic into understandable pieces. By using a simplified model (price = 4 × tick) and relating the tick system to the familiar difference array technique, we've seen how Uniswap V3 achieves efficient liquidity management.
The key concepts we covered:
- The difference array technique as the algorithmic foundation for efficient range updates
- Ticks as discrete price points where liquidity changes occur
- How concentrated liquidity allows capital to be focused within specific price ranges
- How swaps interact with ticks and update current liquidity when crossing boundaries
- The mechanics of adding, removing liquidity and how it affects the tick system
Understanding these fundamentals provides a solid foundation for working with Uniswap V3. However, this conceptual overview is just the beginning. In a follow-up article, we'll dive into the implementation details—exploring the actual mathematical formulas for tick-to-price conversion, the data structures used in the smart contracts, gas optimization techniques, and how to interact with the protocol programmatically.
For now, I hope this gentle introduction has demystified the tick system and made Uniswap V3 more approachable. The conceptual understanding you've gained here will serve as a strong foundation when you're ready to explore the technical implementation.