CenturionDEX
Launch App

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 ^ i

The inverse:

tick(price) = floor(log(price) / log(1.0001))

Worked examples

TickPrice (token1 per token0)Context
01.0001⁰ = 1.0000Tokens at parity
1001.0001¹⁰⁰ = 1.01005~1 % above parity
1,0001.0001¹⁰⁰⁰ = 1.10517~10.5 % above parity
10,0001.0001¹⁰⁰⁰⁰ = 2.71828e (Euler's number)
100,0001.0001¹⁰⁰⁰⁰⁰ = 22,015Typical CTN/USDC tick
−100,0001.0001⁻¹⁰⁰⁰⁰⁰ = 0.0000454Inverse of above
887,272MAX_TICKUpper bound ≈ 3.4 × 10³⁸
−887,272MIN_TICKLower 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^96

The 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^192

price → sqrtPriceX96:

sqrtPriceX96 = sqrt(price) * 2^96

tick → sqrtPriceX96:

sqrtPriceX96 = sqrt(1.0001^tick) * 2^96
             = 1.0001^(tick/2) * 2^96

Worked 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,000

Verification:

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 tierTick spacingMin price granularity
0.01 % (100)10.01 % per tick
0.05 % (500)100.10 % per usable tick
0.30 % (3000)600.60 % per usable tick
1.00 % (10000)2002.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_spacing

Example: 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 = 76980

Actual price range of the position:

lower = 1.0001^74940 = $1,797.42
upper = 1.0001^76980 = $2,203.81

Decimal 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-unit

Most 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:

FormatMeaningUsed for
Q64.96Value × 2⁹⁶, stored as uint160sqrtPriceX96
Q128.128Value × 2¹²⁸, stored as uint256feeGrowthGlobal0X128, 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.94

This 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 = 0

If 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