Tick Price
Ticki has price 1.0001^i, so each tick is a 0.01% (1 bp) step. Prices are in atoms of token Y per atom of token X, before adjusting for decimals. The program stores the square root of the price as a Q64.64 fixed-point number, and the current tick is the highest tick whose price is at or below the pool price. When the price sits exactly on a tick that a swap has just crossed downward, or a swap buying X stops on a tick that still has asks, the pool records the tick below.
Ticks run from -443636 to 443636, which covers prices from about 2^-64 to 2^64.
To show a price in whole tokens, multiply by 10^(decimals_x - decimals_y). For SOL (9 decimals) against USDC (6 decimals), tick -50000 is 1.0001^-50000 × 10^3, about 6.74 USDC per SOL. The TypeScript SDK does this with tickUiPrice.
Converting Between Price and Tick
To turn a price you read on a screen into a tick:- Multiply the price by
10^(decimals_y - decimals_x)to get atoms of token Y per atom of token X. - Take the square root and scale it by
2^64to get the Q64.64 square-root price. - Find the tick with
tickIdFromSqrtPrice, which returns the highest tick whose price is at or below the input. - To place a range edge or an order, round that tick down to a multiple of the pool’s tick spacing.
MIN_SQRT_PRICE (4295048016) to MAX_SQRT_PRICE (79226673515401279992447579055). These are the square-root prices of the lowest and highest tick. The current tick can be any integer in the tick range, not only a multiple of the tick spacing.
Both directions with SOL (9 decimals) and USDC (6 decimals), at a price of 150 USDC per SOL:
Swapping which token is X negates the exact tick. Because the result is rounded down, the two ticks differ by one:
-18973 and 18972, not -18973 and 18973.
Tick Spacing
A pool only uses ticks that are multiples of its tick spacing. Range edges and limit orders must sit on these usable ticks. The spacing is fixed when the pool is created and can be 1 to 256.
A smaller spacing lets LPs and makers place liquidity closer to the price. A larger spacing means fewer ticks to cross, so swaps that move the price far cost less compute.
Usable Ticks
A tick is usable when it is a multiple of the tick spacing and inside-443636 to 443636. Anything else is rejected with InvalidParameters. The highest usable tick is ⌊443636 / spacing⌋ × spacing, and the lowest is its negative.
The usable tick count also sets a cap on liquidity. The total liquidity of all range edges on one tick cannot exceed
2^128 - 1 divided by the usable tick count, for example 383514844834609487117690504987493 at spacing 1. A deposit that would pass the cap fails with LiquidityIsTooHigh.
Ranges
LP liquidity is placed in ranges[tick_lower, tick_upper). Both edges are usable ticks and the lower edge is below the upper edge. A range is active, and earns fees, while the current tick is inside it.
What a range holds depends on where it sits relative to the current tick:
This is why you can deposit a single token: a range entirely above the price needs only token X, and a range entirely below it needs only token Y. Inside a range the pool behaves like a constant-product curve, so the price moves continuously as a swap trades against it. The formulas page gives the amounts for a given liquidity.
Range Rules and Example
- Both edges must be usable ticks and the lower edge must be below the upper edge, or the instruction fails with
InvalidParameters. - A range has no maximum width, and ranges of one position can overlap. A position holds 1 to 45 ranges.
- Liquidity in a range is a positive integer. Zero fails with
LiquidityIsZero, and a value above2^127 - 1fails withLiquidityIsTooHigh. - The pool’s active liquidity changes only when the current tick is inside the range, from the lower edge up to but not including the upper edge.
1,000,000,000,000 need these amounts:
A deposit rounds each amount up and a withdrawal rounds down, so the pool keeps the difference.
Ticks and Tick Arrays
A tick only exists on-chain once a range edge or a limit order uses it. Each tick records:liquidity_net: the LP liquidity added when the price crosses the tick upward (and removed when it crosses downward).- Fee growth and, on liquidity mining pools, reward growth on the far side of the tick, used to work out what each range earned.
- The limit orders resting at the tick, all on one side (asks or bids), and what has been filled.
n covers the 128 usable ticks starting at tick n × 128 × tick_spacing, and a tick array account only stores the ticks in use. Each range edge and each order tick charges the payer one tick’s worth of rent, whether or not the tick already exists, and creates the tick (and its tick array) if needed. Removing a slot, closing a position or closing an order refunds that rent to the instruction’s rent receiver, and a tick is dropped once nothing references it. Whoever creates a tick array also pays the rent of its empty account, which is not refunded because tick arrays are never closed.
The pool keeps a bitmap of which tick arrays hold liquidity or orders. The bitmap inside the pool account covers tick array indexes -512 to 511. A pool with liquidity outside that window needs the tick array bitmap extension account; when the initial price is already outside the window, the creator passes it at pool creation and the program initializes it with the pool. Anyone can create it later.
Each tick also stores its own square-root price, its liquidity_gross (the sum of the liquidity of every range edge sitting on it) and a count of the range edges and order ticks that reference it. A range [a, b) with liquidity L adds L to liquidity_gross at both edges, adds +L to liquidity_net at a and -L at b. A tick is dropped when its reference count reaches zero. A tick is 160 bytes, or 192 bytes on pools with the LiquidityMining extension.
Tick Array Index
The array that holds a tick is⌊tick_id / tick_spacing⌋ divided by 128, rounded toward negative infinity. Array n covers tick ids n × 128 × tick_spacing to (n × 128 + 127) × tick_spacing. Array -1 always holds the 128 usable ticks just below zero.
The array account lives at the address derived from the pool and the index. Anyone can create an empty array with
initialize_tick_array, and the index must cover at least one tick inside the tick range. The position and limit order instructions create the arrays they need and the payer covers the rent. If the array already exists, creating it again does nothing.
Tick Array Bitmap Arithmetic
The bitmap has one bit per tick array index. A bit is set while any tick in the array has liquidity or a resting order, and cleared when the array holds neither. The inline bitmap in the pool account has 1,024 bits for indexes-512 to 511. The bit for an index is index + 512:
In tick ids the inline window is
-65536 × spacing to 65535 × spacing. At spacing 1 that is -65536 to 65535. At spacing 7 and above it is -458752 to 458745 or wider, which covers every possible tick, so those pools never need the extension to hold liquidity. If someone has created it anyway, swaps that search past the inline bitmap still have to pass it.
The extension account adds six chunks of 512 bits on each side, covering indexes 512 to 3583 and -513 to -3584. Every pool can hold any tick with it, because spacing 1 needs indexes -3466 to 3465. The chunk and bit for an index outside the inline window:
- For a positive index, the chunk is
⌊index / 512⌋ - 1and the bit isindex mod 512. - For a negative index, let
m = -(index + 1). The chunk is⌊m / 512⌋ - 1and the bit ism mod 512.
To find the next array to swap through, the program scans the bitmap from the current array index in the swap direction for the nearest set bit, starting in the bitmap that holds the current array index and continuing into the other one, with the extension used only if it is initialized. A swap that has to look outside the inline window, on a pool whose extension is initialized, fails with
BitmapExtensionAccountIsNotProvided when the account is not passed. A deposit, order or cancel that has to flip a bit outside the window without it fails with TickArrayBitmapExtensionAccountIsNotProvided. Anyone can create the extension with initialize_tick_bitmap_extension, and creating it again does nothing.
Tick Array Rent
Rent figures use the current Solana rent-exempt rate of 6,960 lamports per byte, charged on the account size plus 128 bytes of account overhead. The rate is a cluster setting, so the lamport amounts change if it does. The byte sizes do not.
Each edge pays the rent of one tick even when the tick already exists, and gets it back when that edge is removed. A single instruction can grow a tick array by at most 10,240 bytes. That is 64 new ticks, or 53 on
LiquidityMining pools. When the same instruction also creates the array on a pool without LiquidityMining, the limit is 63, because the new account’s 64 bytes count too. More fails with ReallocExceedMaxLengthPerInstruction.
How a Swap Moves
Swaps are exact-in. A swap fills against the pool step by step until the input is used up. The fee rate is set at the start of the swap, and each step takes its fee from that step’s output, or from its input when anOnlyY pool is buying X (see Fee Token).
Only orders on the opposite side of the trade fill. A swap selling X for Y fills bids (buy orders for X) at ticks at or below the current tick; a swap buying X fills asks at ticks above the current tick.
The transaction lists the tick arrays the swap may touch. If those arrays run out of liquidity before the input is used, the swap fails with NotConsumingFullAmountIn. The SDK’s quote returns the tick arrays to pass, and reports the input it could not fill as amountLeft.
The swap fails in these cases:
Pass the tick arrays in the order the swap visits them: descending index when selling X for Y, ascending when buying X. Every account passed must be a tick array. The program skips arrays that are not the next one it expects.
Worked Swap
Pool with tick spacing 10, price exactly on tick 0, a fixed fee of 0.30% (3,000,000 over 1,000,000,000), the OutputToken fee mode, no referral and no partner. LPs hold liquidity 1,000,000,000,000 in [-100, 100) and 2,000,000,000,000 in [100, 200). A maker has an ask of 5,000,000 X at tick 100. A swapper buys X with 5,200,000,000 Y.
The fee on step 1 is
⌈4,992,272,070 × 0.003⌉ = 14,976,817. It is split by output, so LP liquidity gets ⌈14,976,817 × 4,987,272,070 / 4,992,272,070⌉ = 14,961,817 and the ask gets the remaining 15,000. The protocol takes 10% of the LP part and 50% of the order part, both rounded down.
Totals: the swapper pays 5,200,000,000 Y and receives 5,157,598,797 X. The pool ends at square-root price 18540889055449466691, tick 101, with liquidity 2,000,000,000,000. The SDK’s quoteExactIn returns the same figures. When the maker cancels, the ask pays out ⌊5,000,000 × 1.0001^100⌋ = 5,050,248 Y plus a fee share of 7,500 X. The swapper paid one Y more than the maker receives, and that remainder stays in the pool.
Oracle
Each pool has an oracle account that accumulates the pool’s tick over time. Every swap addstick × seconds elapsed to the latest observation, using the tick before the swap, and starts a new observation when at least 15 seconds have passed since the previous one was created.
To get a time-weighted average price, read two observations and divide the change in tick_cumulative by the change in timestamp. The result is an average tick; 1.0001^average_tick is the geometric mean price over that window. The oracle only moves on swaps, so to average up to the present, extend the latest observation yourself by adding current tick × seconds since its last_updated_timestamp.
An oracle starts with one observation. Anyone can extend it, up to 500 observations, with increase_oracle_length, paying the extra rent. Integrators that need history should store observations off-chain rather than rely on the ring buffer.
Oracle Walk-Through
The oracle account holds a ring buffer of observations, each a pair oflast_updated_timestamp and tick_cumulative. Until the first swap it holds one empty observation with timestamp 0. The first swap stamps it with the swap time and a cumulative of 0, so there is no history from before the first swap. After that, every swap works out the seconds since the latest observation was last updated, multiplies by the tick before the swap, and adds the result to the cumulative. If at least 15 seconds have passed since the latest observation was created, the result is written to the next slot of the ring. Otherwise it overwrites the latest observation.
With a ring of 3 observations:
The next new observation wraps around and overwrites slot 0.
To read an average between slot 0 and slot 1 after the swap at time 1031, divide the change in cumulative by the change in time:
(3930 - 700) / (1031 - 1007) = 134.58. The geometric mean price over that window is 1.0001^134.58 = 1.01355. To average up to time 1036 with the current tick at 145, extend slot 1 yourself: 3930 + 5 × 145 = 4655, then (4655 - 700) / (1036 - 1007) = 136.38, a mean price of 1.0001^136.38 = 1.01373.
Extending the oracle costs 16 bytes per observation, 111,360 lamports each at the current rent rate, and the new account size is topped up to the rent-exempt minimum from the payer. The oracle starts at 72 bytes (1,392,000 lamports). At the 500 observation maximum it is 8,056 bytes and 56,960,640 lamports, so growing it from 1 to 500 costs 55,568,640. Adding zero observations, or growing past 500, fails with InvalidParameters. If the ring has already wrapped around when it is extended, the program first rotates it so the newest observation is last and the new empty slots follow it, which keeps existing observations.
Related
Positions
How ranges are grouped into one NFT position and shaped with weights.
Formulas
Price, liquidity and swap math in full.

