Tick Math
Last modified:
Why ticks?
v3 does not store prices directly. Instead, prices are encoded as ticks — integer indices where each tick represents a 0.01 % (1 basis point) price change. This encoding lets the contract efficiently track which liquidity positions are active at any given price.
Tick-to-price conversion
The relationship between tick i and the price it represents:
price(i) = 1.0001 ^ iThe inverse:
tick(price) = floor(log(price) / log(1.0001))Worked examples
| Tick | Price (token1 per token0) | Context |
|---|---|---|
| 0 | 1.0001⁰ = 1.0000 | Tokens at parity |
| 100 | 1.0001¹⁰⁰ = 1.01005 | ~1 % above parity |
| 1,000 | 1.0001¹⁰⁰⁰ = 1.10517 | ~10.5 % above parity |
| 10,000 | 1.0001¹⁰⁰⁰⁰ = 2.71828 | ≈ e (Euler's number) |
| 100,000 | 1.0001¹⁰⁰⁰⁰⁰ = 22,015 | Typical CTN/USDC tick |
| −100,000 | 1.0001⁻¹⁰⁰⁰⁰⁰ = 0.0000454 | Inverse of above |
| 887,272 | MAX_TICK | Upper bound ≈ 3.4 × 10³⁸ |
| −887,272 | MIN_TICK | Lower bound ≈ 2.9 × 10⁻³⁹ |
Tick 10,000 yields exactly e ≈ 2.71828 because (1 + 1/10000)^10000 is the classic limit definition of e.
sqrtPriceX96: the on-chain representation
Solidity lacks floating-point arithmetic, so v3 stores the square root of the price scaled by 2⁹⁶ as a uint160:
sqrtPriceX96 = sqrt(price) * 2^96The square root form is chosen because the core swap math operates on sqrt(price) directly:
Δy = L * (sqrt(P_upper) - sqrt(P_lower))
Δx = L * (1/sqrt(P_lower) - 1/sqrt(P_upper))Storing the value pre-rooted avoids an on-chain square-root computation on every swap — a meaningful gas saving.
Converting between representations
sqrtPriceX96 → price:
price = (sqrtPriceX96 / 2^96) ^ 2
= sqrtPriceX96^2 / 2^192price → sqrtPriceX96:
sqrtPriceX96 = sqrt(price) * 2^96tick → sqrtPriceX96:
sqrtPriceX96 = sqrt(1.0001^tick) * 2^96
= 1.0001^(tick/2) * 2^96Worked example: CTN/USDC
Assume CTN is token0 and USDC is token1, with CTN priced at $2,000 USDC.
price = 2000
tick = floor(log(2000) / log(1.0001)) = floor(76013.9) = 76013
sqrtPriceX96 = sqrt(2000) * 2^96 = 44.721 * 79,228,162,514,264,337,593,543,950,336
≈ 3,543,191,142,285,914,205,922,034,000,000Verification:
price = (3,543,191,142,285,914,205,922,034,000,000 / 2^96)^2
= 44.721^2
= 1999.97 ≈ 2000 ✓Tick spacing
Not every tick is usable. Each fee tier defines a tick spacing — only multiples of the spacing can serve as position boundaries:
| Fee tier | Tick spacing | Min price granularity |
|---|---|---|
| 0.01 % (100) | 1 | 0.01 % per tick |
| 0.05 % (500) | 10 | 0.10 % per usable tick |
| 0.30 % (3000) | 60 | 0.60 % per usable tick |
| 1.00 % (10000) | 200 | 2.00 % per usable tick |
The contract snaps requested bounds to the nearest valid tick:
lower_tick = floor(desired_lower_tick / tick_spacing) * tick_spacing
upper_tick = ceil(desired_upper_tick / tick_spacing) * tick_spacingExample: In a 0.30 % pool (tick spacing 60), targeting a range of $1,800–$2,200:
tick($1800) = floor(log(1800) / log(1.0001)) = 74959
Snapped lower = floor(74959 / 60) * 60 = 74940
tick($2200) = floor(log(2200) / log(1.0001)) = 76961
Snapped upper = ceil(76961 / 60) * 60 = 76980Actual price range of the position:
lower = 1.0001^74940 = $1,797.42
upper = 1.0001^76980 = $2,203.81Decimal adjustment for different token pairs
The raw tick-derived price gives token1-per-token0 in the tokens' smallest units (wei, etc.). For human-readable prices, adjust for decimals:
human_price = raw_price * 10^(decimals_token0 - decimals_token1)Example: USDC (6 decimals) as token0, WCTN (18 decimals) as token1, raw price = 0.0005:
human_price = 0.0005 * 10^(6 - 18) = 5 × 10^-16 WCTN per USDC-unitMost integrations compute the inverse — 1 / human_price — to display the familiar "USDC per CTN" format.
Q notation
v3 uses fixed-point arithmetic throughout. The two key formats:
| Format | Meaning | Used for |
|---|---|---|
| Q64.96 | Value × 2⁹⁶, stored as uint160 | sqrtPriceX96 |
| Q128.128 | Value × 2¹²⁸, stored as uint256 | feeGrowthGlobal0X128, feeGrowthGlobal1X128 |
To convert from Q notation:
decimal_value = stored_value / 2^(fractional_bits)Example: A feeGrowthGlobal0X128 of ≈ 10³⁹:
fee_growth = 10^39 / 2^128 = 10^39 / 3.4 × 10^38 ≈ 2.94This indicates 2.94 units of token0 in fees earned per unit of liquidity since pool creation.
Liquidity math
Liquidity L represents the virtual reserves available at the current price:
L = sqrt(x * y) (v2 equivalent)
L = Δy / (sqrt(P_b) - sqrt(P_a)) (from token1 deposits)
L = Δx / (1/sqrt(P_a) - 1/sqrt(P_b)) (from token0 deposits)Where P_a and P_b are the lower and upper price bounds.
Token composition of a position
Given liquidity L in range [P_a, P_b] at current price P:
If P < P_a (below range — 100 % token0):
x = L * (1/sqrt(P_a) - 1/sqrt(P_b))
y = 0If P > P_b (above range — 100 % token1):
x = 0
y = L * (sqrt(P_b) - sqrt(P_a))If P_a ≤ P ≤ P_b (in range — both tokens):
x = L * (1/sqrt(P) - 1/sqrt(P_b))
y = L * (sqrt(P) - sqrt(P_a))Worked example
Position: L = 100,000, CTN/USDC, range $1,800–$2,200, current price $2,000.
sqrt(1800) = 42.426
sqrt(2000) = 44.721
sqrt(2200) = 46.904
x (CTN) = 100,000 * (1/44.721 - 1/46.904) = 100,000 * 0.001039 = 103.9 CTN-units
y (USDC) = 100,000 * (44.721 - 42.426) = 100,000 * 2.295 = 229,500 USDC-units(Values are in smallest units — divide by 10^decimals for human-readable amounts.)
Further reading
- Concentrated Liquidity — conceptual overview
- Quick Reference — constants and conversion formulas
- Fees — fee-growth accumulator mechanics
- Impermanent Loss — how IL interacts with concentration