Skip to main content

Tick and Price Math

The SDK exports the contract's tick arithmetic, so an app does not carry its own copy. Everything on this page is exact to 60 significant digits (decimal.js) and, unless it says otherwise, returns NaN or false for an input it cannot use instead of throwing.

Bounds, fee tiers and spacing​

import {
ALL_FEE_TIERS,
GC_MAX_TICK,
GC_MIN_TICK,
TICK_SPACING_BY_FEE,
} from '@gala-chain/gswap-sdk';

console.log(GC_MIN_TICK, GC_MAX_TICK); // -887272 887272
console.log(ALL_FEE_TIERS); // [0, 500, 3000, 10000]
console.log(TICK_SPACING_BY_FEE[3000]); // 60
Fee tierSpacing
0200
50010
300060
10000200

A position's ticks must be integers, tickLower < tickUpper, inside the bounds and multiples of the pool's spacing. assertTickRange(lower, upper, fee) checks all of that and throws a coded error (TICKS_NOT_INTEGERS, INVALID_TICK_RANGE, TICKS_OUT_OF_BOUNDS, TICKS_NOT_ALIGNED, INVALID_FEE).

Ticks and prices​

import { tickFromPrice, tickToPrice, tickToSqrtPrice, sqrtPriceToTick } from '@gala-chain/gswap-sdk';

const price = tickToPrice(6000); // BigNumber: 1.0001 ** 6000 (token1 per token0)
const display = price.toNumber();

const floor = tickFromPrice('0.5'); // the greatest tick whose price is no greater than 0.5
const snapped = tickFromPrice('0.5', { rounding: 'nearest' }); // what a price input wants

const sqrt = tickToSqrtPrice(6000);
const back = sqrtPriceToTick(sqrt); // 6000
  • tickToPrice(tick) is 1.0001 ** tick as a BigNumber, the lower edge of the tick's price range. Use .toNumber() for display.
  • tickFromPrice(price, options?) floors by default, which is how the contract places a price. { rounding: 'nearest' } rounds half away from zero, which is what a price field that snaps to a tick wants. The result is NaN for a price that is not positive and finite.
  • tickToSqrtPrice(tick) and sqrtPriceToTick(sqrtPrice) are the square-root forms; sqrtPriceToTick is the greatest tick whose square-root price is no greater than the input (NaN for one that is not positive and finite).
  • isSqrtPriceInRange and isTickInRange are below.

tickToPrice agrees with the common float formula (Math.exp(tick * ln 1.0001)) to 8 significant digits for every tick and differs only in the last digits of a double, because the SDK's value is exact. tickFromPrice(price, { rounding: 'nearest' }) returns the same tick as a float priceToTick helper.

Aligning to the spacing​

import { alignTickDown, alignTickDownOrNaN, alignTickUp } from '@gala-chain/gswap-sdk';

alignTickDown(-61, 60); // -120: it floors, it does not truncate toward zero
alignTickUp(-61, 60); // -60
alignTickDownOrNaN(-61, 0); // NaN: a spacing that has not loaded yet

alignTickDown and alignTickUp throw INVALID_TICK_SPACING for a spacing that is not a positive integer. alignTickDownOrNaN and alignTickUpOrNaN return the same value and answer NaN instead, for a drag handler or a price field that runs before the pool has loaded.

In range: lower inclusive, upper exclusive​

A position is in range when the pool's current tick is at or above tickLower and strictly below tickUpper. That is the contract's own test (tickCurrent >= tickLower && tickCurrent < tickUpper): a pool sitting exactly on tickUpper is already out of range, and the position is entirely token1.

import { isSqrtPriceInRange, isTickInRange } from '@gala-chain/gswap-sdk';

isTickInRange(-60, -60, 60); // true: the lower tick is inclusive
isTickInRange(60, -60, 60); // false: the upper tick is exclusive
isTickInRange('59', '-60', '60'); // true: ticks may be the decimal strings a backend returns

// A position row carries the pool's sqrtPrice (when the backend sent it), not its tick:
const inRange =
position.sqrtPrice === undefined
? undefined // not loaded: show a neutral state
: isSqrtPriceInRange(position.sqrtPrice, position.tickLower, position.tickUpper);
  • isTickInRange(currentTick, tickLower, tickUpper) takes numbers or decimal strings. Anything that is not an integer (NaN, an empty string, a fraction) makes the answer false.
  • isSqrtPriceInRange(sqrtPrice, tickLower, tickUpper) derives the tick from the price exactly as the contract does (sqrtPriceToTick) and then applies the same rule, so no float rounding can flip a pool that sits exactly on a boundary. It is false for a price that is not positive and finite. An app that needs a neutral "not loaded" state checks for the missing price first.

Do not decide range by comparing a price with tickToPrice(tickUpper) using <=, or by mixing a square-root price with a price: at an exact boundary that reports the upper edge as in range, and two representations of the same boundary can round to opposite sides.

Slot0 in the requested frame​

pools.getSlot0(tokenA, tokenB, fee) answers in the pool's canonical order with flippedFromRequest. frameSlot0(slot0) puts it in the order that was requested: when flipped, the square-root price is inverted at 60 digits and the tick is re-derived from it with the contract's greatest-tick rule (negating the tick is wrong for a price between ticks). It throws INVALID_SQRT_PRICE for a price that cannot be inverted.

Which of these are safe in an app shell​

isTickInRange, alignTickDownOrNaN, alignTickUpOrNaN, ALL_FEE_TIERS, TICK_SPACING_BY_FEE cost a few hundred bytes. tickToPrice, tickFromPrice and isSqrtPriceInRange cost the two number libraries they compute with (decimal.js and bignumber.js, about 53 kB minified together), and nothing of the error catalog. The functions that throw (alignTickDown, alignTickUp, assertTickRange, frameSlot0) are not shell-safe; use the ...OrNaN siblings there. See Bundle size.