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 tier | Spacing |
|---|---|
0 | 200 |
500 | 10 |
3000 | 60 |
10000 | 200 |
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)is1.0001 ** tickas aBigNumber, 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 isNaNfor a price that is not positive and finite.tickToSqrtPrice(tick)andsqrtPriceToTick(sqrtPrice)are the square-root forms;sqrtPriceToTickis the greatest tick whose square-root price is no greater than the input (NaNfor one that is not positive and finite).isSqrtPriceInRangeandisTickInRangeare 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 answerfalse.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 isfalsefor 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.