# Math

> Ticks, prices, sqrtPriceX96, liquidity amounts and fee units, with tested TypeScript.

The formulas behind prices, ranges and amounts in a CL pool, as self-contained TypeScript. Everything that touches on-chain integers uses `bigint` and rounds the way the contracts do; a `number` appears only where the result is for display. Each block on this page runs as written and is tested against the Giwater app's own math.

## Ticks and Prices

A pool's price is the amount of `currency1` one unit of `currency0` buys, in raw units (each token's smallest unit). Ticks index that price on a geometric scale: `price = 1.0001 ^ tick`, so each tick is a 0.01% step. Tick 0 is a raw price of 1; above it one raw unit of `currency0` is worth more than one of `currency1`, below it less.

To show a price in whole tokens, scale by the decimals difference. In the BTC/USDC pools BTC (8 decimals) is `currency0` and USDC (6) is `currency1`, so a raw price is multiplied by `10 ^ (8 - 6)` to read as USDC per BTC. For the inverse quote, take `1 / price`.

Ticks run from `MIN_TICK = -887272` to `MAX_TICK = 887272`. A position's ticks must also be multiples of the pool's tick spacing, which the pool key carries in bits 16–39 of `parameters`. The seeded pools use 1 (0.01%), 10 (0.05%) and 60 (0.30%). When turning a price range into ticks, round the lower bound down and the upper bound up, so the range covers at least the prices asked for.

```ts
export const MIN_TICK = -887272;
export const MAX_TICK = 887272;

// Token1 per token0 in whole tokens.
export function priceAtTick(tick: number, decimals0: number, decimals1: number): number {
  return 1.0001 ** tick * 10 ** (decimals0 - decimals1);
}

// The fractional tick of a price; align it before using it as a position bound.
export function tickAtPrice(price: number, decimals0: number, decimals1: number): number {
  return Math.log(price / 10 ** (decimals0 - decimals1)) / Math.log(1.0001);
}

export function tickSpacing(parameters: `0x${string}`): number {
  return Number((BigInt(parameters) >> 16n) & 0xffffffn);
}

// Lower bounds round down, upper bounds round up.
export function alignTick(tick: number, spacing: number, direction: "down" | "up"): number {
  return (direction === "down" ? Math.floor(tick / spacing) : Math.ceil(tick / spacing)) * spacing;
}

// The widest range a position can take: the outermost multiples of the spacing.
export function usableTicks(spacing: number): { lower: number; upper: number } {
  return {
    lower: Math.ceil(MIN_TICK / spacing) * spacing,
    upper: Math.floor(MAX_TICK / spacing) * spacing,
  };
}
```

A full-range position uses `usableTicks(spacing)`, not `MIN_TICK` and `MAX_TICK`: with a spacing of 60 those are -887220 and 887220.

## sqrtPriceX96

The pool stores its price as `sqrtPriceX96`: the square root of the raw price as a Q64.96 fixed-point number, a `uint160` equal to `sqrt(price) * 2^96`. `CLPoolManager.getSlot0(poolId)` returns it together with the current tick, which is the largest tick whose square-root price does not exceed it.

`sqrtRatioAtTick` is the contract's `TickMath.getSqrtRatioAtTick`, bit for bit: use it whenever a result must match what the pool computes, such as position amounts. Its values at the tick bounds are `MIN_SQRT_RATIO = 4295128739` and `MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342`; a valid `sqrtPriceX96` lies in `[MIN_SQRT_RATIO, MAX_SQRT_RATIO)`.

```ts
export const Q96 = 1n << 96n;
export const MIN_SQRT_RATIO = 4295128739n;
export const MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342n;

// TickMath.getSqrtRatioAtTick: sqrt(1.0001 ^ tick) * 2^96, rounded up.
export function sqrtRatioAtTick(tick: number): bigint {
  const absTick = BigInt(Math.abs(tick));
  if (absTick > BigInt(MAX_TICK)) throw new RangeError("tick out of range");
  let ratio = absTick & 1n ? 0xfffcb933bd6fad37aa2d162d1a594001n : 1n << 128n;
  const factors = [
    0xfff97272373d413259a46990580e213an, 0xfff2e50f5f656932ef12357cf3c7fdccn,
    0xffe5caca7e10e4e61c3624eaa0941cd0n, 0xffcb9843d60f6159c9db58835c926644n,
    0xff973b41fa98c081472e6896dfb254c0n, 0xff2ea16466c96a3843ec78b326b52861n,
    0xfe5dee046a99a2a811c461f1969c3053n, 0xfcbe86c7900a88aedcffc83b479aa3a4n,
    0xf987a7253ac413176f2b074cf7815e54n, 0xf3392b0822b70005940c7a398e4b70f3n,
    0xe7159475a2c29b7443b29c7fa6e889d9n, 0xd097f3bdfd2022b8845ad8f792aa5825n,
    0xa9f746462d870fdf8a65dc1f90e061e5n, 0x70d869a156d2a1b890bb3df62baf32f7n,
    0x31be135f97d08fd981231505542fcfa6n, 0x9aa508b5b7a84e1c677de54f3e99bc9n,
    0x5d6af8dedb81196699c329225ee604n, 0x2216e584f5fa1ea926041bedfe98n,
    0x48a170391f7dc42444e8fa2n,
  ];
  factors.forEach((factor, i) => {
    if (absTick & (2n << BigInt(i))) ratio = (ratio * factor) >> 128n;
  });
  if (tick > 0) ratio = ((1n << 256n) - 1n) / ratio;
  return (ratio >> 32n) + (ratio % (1n << 32n) === 0n ? 0n : 1n);
}

// TickMath.getTickAtSqrtRatio: the largest tick whose sqrt ratio is <= sqrtPriceX96.
export function tickAtSqrtRatio(sqrtPriceX96: bigint): number {
  if (sqrtPriceX96 < MIN_SQRT_RATIO || sqrtPriceX96 >= MAX_SQRT_RATIO) {
    throw new RangeError("sqrtPriceX96 out of range");
  }
  // A float estimate, then corrected against the exact ratios.
  const estimate = Math.floor((2 * Math.log(Number(sqrtPriceX96) / 2 ** 96)) / Math.log(1.0001));
  let tick = Math.min(Math.max(estimate, MIN_TICK), MAX_TICK - 1);
  while (sqrtRatioAtTick(tick) > sqrtPriceX96) tick--;
  while (sqrtRatioAtTick(tick + 1) <= sqrtPriceX96) tick++;
  return tick;
}

// Token1 per token0 in whole tokens. Square in bigint; convert to a float last.
export function priceAtSqrtPriceX96(sqrtPriceX96: bigint, decimals0: number, decimals1: number): number {
  return (
    Number(sqrtPriceX96 * sqrtPriceX96 * 10n ** BigInt(decimals0)) /
    Number((1n << 192n) * 10n ** BigInt(decimals1))
  );
}

// Float precision: fine to initialize a pool, not for amounts.
export function sqrtPriceX96AtPrice(price: number, decimals0: number, decimals1: number): bigint {
  return BigInt(Math.floor(Math.sqrt(price * 10 ** (decimals1 - decimals0)) * 2 ** 96));
}
```

Precision:

- Never square `Number(sqrtPriceX96)`. A float keeps 53 bits of a 160-bit value; square the `bigint` and divide last, as `priceAtSqrtPriceX96` does.
- `1.0001 ** tick` and `sqrtRatioAtTick` agree to about twelve significant digits, which is fine for display but not for amounts. Anything that feeds a transaction uses `sqrtRatioAtTick`.
- `tickAtPrice` is a float: a price exactly on a tick boundary can land a hair either side. Always align it with an explicit rounding direction.

## Liquidity and Amounts

A position holds liquidity `L` between two ticks, not token amounts. What `L` is worth depends on where the current price sits relative to the range, using the square-root prices `sqrtLower` and `sqrtUpper` of the two ticks:

- **Below the range** (`tick < tickLower`): all `currency0`, `amount0 = L · 2^96 · (sqrtUpper − sqrtLower) / (sqrtUpper · sqrtLower)`.
- **In range** (`tickLower ≤ tick < tickUpper`): `amount0` from the current price up to `sqrtUpper`, and `amount1 = L · (sqrtPrice − sqrtLower) / 2^96`.
- **Above the range** (`tick ≥ tickUpper`): all `currency1`, `amount1 = L · (sqrtUpper − sqrtLower) / 2^96`.

The branch is chosen by the current tick, as the pool chooses it, not by comparing square-root prices. Rounding always favours the pool: amounts you pay in (a mint or an increase) round up, and amounts you take out (a decrease or a burn) round down.

```ts
const divide = (numerator: bigint, denominator: bigint, roundUp: boolean) =>
  roundUp ? (numerator + denominator - 1n) / denominator : numerator / denominator;

// SqrtPriceMath.getAmount0Delta
export function getAmount0Delta(sqrtA: bigint, sqrtB: bigint, liquidity: bigint, roundUp: boolean): bigint {
  const [lower, upper] = sqrtA < sqrtB ? [sqrtA, sqrtB] : [sqrtB, sqrtA];
  return divide((liquidity << 96n) * (upper - lower), upper * lower, roundUp);
}

// SqrtPriceMath.getAmount1Delta
export function getAmount1Delta(sqrtA: bigint, sqrtB: bigint, liquidity: bigint, roundUp: boolean): bigint {
  const [lower, upper] = sqrtA < sqrtB ? [sqrtA, sqrtB] : [sqrtB, sqrtA];
  return divide(liquidity * (upper - lower), Q96, roundUp);
}

// The tokens behind a position. roundUp: true for a deposit, false for a withdrawal.
export function getAmountsForLiquidity(
  tickLower: number,
  tickUpper: number,
  currentTick: number,
  liquidity: bigint,
  sqrtPriceX96: bigint,
  roundUp = false,
): [bigint, bigint] {
  const lower = sqrtRatioAtTick(tickLower);
  const upper = sqrtRatioAtTick(tickUpper);
  if (currentTick < tickLower) return [getAmount0Delta(lower, upper, liquidity, roundUp), 0n];
  if (currentTick >= tickUpper) return [0n, getAmount1Delta(lower, upper, liquidity, roundUp)];
  return [
    getAmount0Delta(sqrtPriceX96, upper, liquidity, roundUp),
    getAmount1Delta(lower, sqrtPriceX96, liquidity, roundUp),
  ];
}
```

The reverse — how much liquidity a token amount buys — is what a deposit form needs. Below the range only `amount0` counts, above it only `amount1`, and in range the smaller of the two liquidities is the one both amounts can fund. Each division rounds down, so the liquidity never needs more than the amount given.

```ts
export function getLiquidityForAmount0(sqrtA: bigint, sqrtB: bigint, amount0: bigint): bigint {
  const [lower, upper] = sqrtA < sqrtB ? [sqrtA, sqrtB] : [sqrtB, sqrtA];
  return (amount0 * lower * upper) / Q96 / (upper - lower);
}

export function getLiquidityForAmount1(sqrtA: bigint, sqrtB: bigint, amount1: bigint): bigint {
  const [lower, upper] = sqrtA < sqrtB ? [sqrtA, sqrtB] : [sqrtB, sqrtA];
  return (amount1 * Q96) / (upper - lower);
}

export function getLiquidityForAmounts(
  tickLower: number,
  tickUpper: number,
  currentTick: number,
  sqrtPriceX96: bigint,
  amount0: bigint,
  amount1: bigint,
): bigint {
  const lower = sqrtRatioAtTick(tickLower);
  const upper = sqrtRatioAtTick(tickUpper);
  if (currentTick < tickLower) return getLiquidityForAmount0(lower, upper, amount0);
  if (currentTick >= tickUpper) return getLiquidityForAmount1(lower, upper, amount1);
  const liquidity0 = getLiquidityForAmount0(sqrtPriceX96, upper, amount0);
  const liquidity1 = getLiquidityForAmount1(lower, sqrtPriceX96, amount1);
  return liquidity0 < liquidity1 ? liquidity0 : liquidity1;
}

// The cap for amount0Max / amount1Max: the rounded-up amount plus slippage.
export function maxAmountIn(amount: bigint, slippageBps: number): bigint {
  return (amount * BigInt(10_000 + slippageBps) + 9_999n) / 10_000n;
}
```

To deposit a fixed amount of one token, take the liquidity for that amount alone, then `getAmountsForLiquidity(…, roundUp = true)` for the other side. Pass the result to `CL_MINT_POSITION` with each `amountMax` from `maxAmountIn`; the [Integration](/docs/developers/integration#add-liquidity) example does exactly this.

## Fee Units

Fees are in pips: hundredths of a basis point, so `1_000_000` is 100% and `3_000` is 0.30%. The pool key's `fee` is the LP fee alone. The protocol fee comes from slot0 and packs two 12-bit pip values into one `uint24`, the low 12 bits for `zeroForOne` swaps and the high 12 for `oneForZero`. The two fees do not simply add: the protocol fee is taken first and the LP fee applies to what remains.

```ts
export const pipsToPercent = (pips: number) => pips / 10_000;

export function protocolFeeFor(packed: number, zeroForOne: boolean): number {
  return zeroForOne ? packed & 0xfff : packed >> 12;
}

// ProtocolFeeLibrary.calculateSwapFee: 2011 LP + 989 protocol = 2999 (0.2999%).
export function totalSwapFee(lpFee: number, protocolFee: number): number {
  return protocolFee + lpFee - Math.floor((protocolFee * lpFee) / 1_000_000);
}
```

Fees accrue as fee growth: the fees earned per unit of liquidity, a Q128.128 `uint256` per currency. The pool keeps a global counter, and each initialized tick keeps the growth on its far side ("outside"). A position's unclaimed fees are the growth inside its range since it last settled, times its liquidity. The counters are allowed to overflow, so every subtraction is modulo 2^256.

```ts
const Q128 = 1n << 128n;
const sub256 = (a: bigint, b: bigint) => (a - b) & ((1n << 256n) - 1n);

export function feeGrowthInside(
  currentTick: number,
  tickLower: number,
  tickUpper: number,
  feeGrowthGlobalX128: bigint,
  lowerOutsideX128: bigint,
  upperOutsideX128: bigint,
): bigint {
  if (currentTick < tickLower) return sub256(lowerOutsideX128, upperOutsideX128);
  if (currentTick >= tickUpper) return sub256(upperOutsideX128, lowerOutsideX128);
  return sub256(sub256(feeGrowthGlobalX128, lowerOutsideX128), upperOutsideX128);
}

// In the currency's smallest unit, rounded down.
export function unclaimedFees(insideX128: bigint, insideLastX128: bigint, liquidity: bigint): bigint {
  return (sub256(insideX128, insideLastX128) * liquidity) / Q128;
}
```

Run it once per currency. The inputs, all read at the same block:

- `CLPositionManager.positions(tokenId)` — the pool key, `tickLower`, `tickUpper`, `liquidity`, and `feeGrowthInside0LastX128` / `feeGrowthInside1LastX128`.
- `CLPoolManager.getFeeGrowthGlobals(poolId)` — `feeGrowthGlobal0x128`, `feeGrowthGlobal1x128`.
- `CLPoolManager.getPoolTickInfo(poolId, tick)` for `tickLower` and `tickUpper` — `feeGrowthOutside0X128`, `feeGrowthOutside1X128`.
- `CLPoolManager.getSlot0(poolId)` — the current `tick`.

Fees are paid out on the position's next liquidity change; a `CL_DECREASE_LIQUIDITY` of zero collects them without touching the position.
