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%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 ii and the price it represents:

price(i)=1.0001i\text{price}(i) = 1.0001^i

The inverse:

tick(price)=log(price)log(1.0001)\text{tick}(\text{price}) = \left\lfloor\frac{\log(\text{price})}{\log(1.0001)}\right\rfloor

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 2962^{96} as a uint160:

sqrtPriceX96=price296\text{sqrtPriceX96} = \sqrt{\text{price}} \cdot 2^{96}

The square root form is chosen because the core swap math operates on price\sqrt{\text{price}} directly:

Δy=L(PupperPlower)\Delta y = L\left(\sqrt{P_{\text{upper}}} - \sqrt{P_{\text{lower}}}\right) Δx=L(1Plower1Pupper)\Delta x = L\left(\frac{1}{\sqrt{P_{\text{lower}}}} - \frac{1}{\sqrt{P_{\text{upper}}}}\right)

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=(sqrtPriceX96296)2=sqrtPriceX9622192\text{price} = \left(\frac{\text{sqrtPriceX96}}{2^{96}}\right)^2 = \frac{\text{sqrtPriceX96}^2}{2^{192}}

price → sqrtPriceX96:

sqrtPriceX96=price296\text{sqrtPriceX96} = \sqrt{\text{price}} \cdot 2^{96}

tick → sqrtPriceX96:

sqrtPriceX96=1.0001tick296=1.0001tick/2296\text{sqrtPriceX96} = \sqrt{1.0001^{\text{tick}}}\cdot 2^{96} = 1.0001^{\text{tick}/2}\cdot 2^{96}

Worked example: CTN/USDC

Assume CTN is token0 and USDC is token1, with CTN priced at $2,000 USDC.

human_price=2000\text{human\_price} = 2000

Because tick/sqrtPriceX96 use smallest units, convert to raw price first (CTN: 18 decimals, USDC: 6 decimals):

raw_price=20001061018=2×109\text{raw\_price} = 2000 \cdot \frac{10^6}{10^{18}} = 2 \times 10^{-9} tick=log(2×109)log(1.0001)=200312\text{tick} = \left\lfloor\frac{\log(2 \times 10^{-9})}{\log(1.0001)}\right\rfloor = -200312 sqrtPriceX96=2×1092963.5432×1024\text{sqrtPriceX96} = \sqrt{2 \times 10^{-9}}\cdot 2^{96} \approx 3.5432 \times 10^{24}

Verification:

raw_price=(3.5432×1024296)22×109\text{raw\_price} = \left(\frac{3.5432 \times 10^{24}}{2^{96}}\right)^2 \approx 2 \times 10^{-9}

Convert back to human-readable units:

human_price=raw_price10(186)2000 USDC per CTN  \text{human\_price} = \text{raw\_price} \cdot 10^{(18-6)} \approx 2000\ \text{USDC per CTN} \;\checkmark

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=desired_lower_ticktick_spacingtick_spacing\text{lower\_tick} = \left\lfloor\frac{\text{desired\_lower\_tick}}{\text{tick\_spacing}}\right\rfloor \cdot \text{tick\_spacing} upper_tick=desired_upper_ticktick_spacingtick_spacing\text{upper\_tick} = \left\lceil\frac{\text{desired\_upper\_tick}}{\text{tick\_spacing}}\right\rceil \cdot \text{tick\_spacing}

Example: In a 0.30%0.30\% pool (tick spacing 60), targeting a range of $1,800–$2,200:

tick(1800)=log(1800)log(1.0001)=74959\text{tick}(1800) = \left\lfloor\frac{\log(1800)}{\log(1.0001)}\right\rfloor = 74959 Snapped lower=749596060=74940\text{Snapped lower} = \left\lfloor\frac{74959}{60}\right\rfloor \cdot 60 = 74940 tick(2200)=log(2200)log(1.0001)=76961\text{tick}(2200) = \left\lfloor\frac{\log(2200)}{\log(1.0001)}\right\rfloor = 76961 Snapped upper=769616060=76980\text{Snapped upper} = \left\lceil\frac{76961}{60}\right\rceil \cdot 60 = 76980

Actual price range of the position:

lower=1.000174940=$1,797.42\text{lower} = 1.0001^{74940} = \$1{,}797.42 upper=1.000176980=$2,203.81\text{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_price10(decimals_token0decimals_token1)\text{human\_price} = \text{raw\_price}\cdot 10^{(\text{decimals\_token0}-\text{decimals\_token1})}

Example: USDC (6 decimals) as token0, WCTN (18 decimals) as token1, raw price =0.0005= 0.0005:

human_price=0.000510(618)=5×1016 WCTN per USDC-unit\text{human\_price} = 0.0005 \cdot 10^{(6-18)} = 5 \times 10^{-16}\ \text{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_value2(fractional_bits)\text{decimal\_value} = \frac{\text{stored\_value}}{2^{(\text{fractional\_bits})}}

Example: A feeGrowthGlobal0X128 of 1039\approx 10^{39}:

fee_growth=10392128=10393.4×10382.94\text{fee\_growth} = \frac{10^{39}}{2^{128}} = \frac{10^{39}}{3.4 \times 10^{38}} \approx 2.94

This indicates 2.94 units of token0 in fees earned per unit of liquidity since pool creation.

Liquidity math

Liquidity LL represents the virtual reserves available at the current price:

L=xy(v2 equivalent)L = \sqrt{xy} \qquad (\text{v2 equivalent}) L=ΔyPbPa(from token1 deposits)L = \frac{\Delta y}{\sqrt{P_b} - \sqrt{P_a}} \qquad (\text{from token1 deposits}) L=Δx1Pa1Pb(from token0 deposits)L = \frac{\Delta x}{\frac{1}{\sqrt{P_a}} - \frac{1}{\sqrt{P_b}}} \qquad (\text{from token0 deposits})

Where PaP_a and PbP_b are the lower and upper price bounds.

Token composition of a position

Given liquidity LL in range [Pa,Pb][P_a, P_b] at current price PP:

If P<PaP < P_a (below range — 100%100\% token0):

x=L(1Pa1Pb)x = L\left(\frac{1}{\sqrt{P_a}} - \frac{1}{\sqrt{P_b}}\right) y=0y = 0

If P>PbP > P_b (above range — 100%100\% token1):

x=0x = 0 y=L(PbPa)y = L\left(\sqrt{P_b} - \sqrt{P_a}\right)

If PaPPbP_a \le P \le P_b (in range — both tokens):

x=L(1P1Pb)x = L\left(\frac{1}{\sqrt{P}} - \frac{1}{\sqrt{P_b}}\right) y=L(PPa)y = L\left(\sqrt{P} - \sqrt{P_a}\right)

Worked example

Position: L=100,000L = 100{,}000, CTN/USDC, range $1,800–$2,200, current price $2,000.

1800=42.426,2000=44.721,2200=46.904\sqrt{1800}=42.426,\quad \sqrt{2000}=44.721,\quad \sqrt{2200}=46.904 x (CTN)=100,000(144.721146.904)=100,0000.001039=103.9 CTN-unitsx\ (\text{CTN}) = 100{,}000\left(\frac{1}{44.721}-\frac{1}{46.904}\right) = 100{,}000\cdot 0.001039 = 103.9\ \text{CTN-units} y (USDC)=100,000(44.72142.426)=100,0002.295=229,500 USDC-unitsy\ (\text{USDC}) = 100{,}000(44.721-42.426) = 100{,}000\cdot 2.295 = 229{,}500\ \text{USDC-units}

(Values are in smallest units — divide by 10^decimals for human-readable amounts.)

Further reading