Skip to main content
This page describes the layout and role of each account. Seeds are canonical and listed in reference/program-addresses. A CLMM pool is more account-heavy than a CPMM pool because liquidity is stored sparsely across the tick range; understanding that sparsity is the bulk of this page.

Account inventory

A live CLMM pool is described by the following account families. All are owned by the CLMM program except the two mints and their vaults.

PoolState

The pool’s live state, read on every swap and every position change.
Fields you will actually touch:
  • sqrt_price_x64 and tick_current are the pool’s price state. They are updated together on every swap. tick_current is the floor of log_{1.0001}(price).
  • liquidity is the active liquidity — the sum of L values for all positions whose range contains tick_current. It changes every time a swap crosses a tick and every time a position is opened/closed/resized.
  • fee_growth_global_{0,1}_x64 are the cumulative fees earned per unit of liquidity across the entire pool history. Positions read this to compute what’s owed to them.
  • tick_spacing is locked to the AmmConfig at initialization and never changes. It determines which tick indices are even allowed to be position endpoints.
  • tick_array_bitmap is an inline bitmap covering the commonly used tick range around spot price. For pools whose positions reach far out, overflow tracking lives in the separate TickArrayBitmapExtension.
  • fee_on is fixed at pool creation. 0 (FromInput) reproduces classic Uniswap-V3 behavior. 1 and 2 route the swap fee to a single side of the book — see products/clmm/fees for trade-offs.
  • dynamic_fee_info carries volatility state for the dynamic-fee surcharge. When enabled, every swap recomputes a dynamic_fee_component on top of AmmConfig.trade_fee_rate. Layout is documented under DynamicFeeInfo below; pools without dynamic fee leave the entire struct zero.

AmmConfig

A typical published set of CLMM fee tiers (confirm against GET https://api-v3.raydium.io/main/clmm-config): protocol_fee_rate and fund_fee_rate are fractions of the trade fee; same convention as CPMM. See products/clmm/fees.

TickArrayState

CLMM does not store a single record per tick. That would be billions of accounts. Instead it groups TICK_ARRAY_SIZE adjacent initialized-or-not ticks (typically 60 or 88 depending on program version) into a TickArrayState that is lazily created on first use.
The four limit-order fields are zero on any tick that has never been used for a limit order. When orders are opened on a tick, the program tracks them as a sequence of cohorts:
  • order_phase is the cohort id. It increments every time a cohort transitions from “all unfilled” to “partially filled.”
  • orders_amount is the input-token total of the current (newest) cohort.
  • part_filled_orders_remaining tracks the previous cohort that is currently being filled by ongoing swaps.
  • unfilled_ratio_x64 is a Q64.64 multiplier carried on the cohort: when a swap fills X% of the cohort, the ratio is multiplied by (1 − X). Each open order stores its own (order_phase, unfilled_ratio_x64) snapshot at open time, so settle math reduces to comparing snapshots.
Rules:
  • A position endpoint tick t must satisfy t % tick_spacing == 0. The program rejects off-spacing positions.
  • The tick’s array is located at floor(t / (TICK_ARRAY_SIZE * tick_spacing)) * (TICK_ARRAY_SIZE * tick_spacing).
  • A tick array is initialized lazily: the first position or swap that touches an uninitialized array creates it, paying the rent.
  • A tick array is never closed by the program. Once allocated it persists for the life of the pool, even after every tick inside it returns to liquidity_gross == 0. Subsequent positions and swaps reuse the existing account at no extra rent. There is no ClosePosition-driven cleanup path for tick arrays.

TickArrayBitmapExtension

PoolState.tick_array_bitmap (inline) covers the “close to spot” range — ±1,024 tick arrays. Outside that range (for extreme tick values), the program maintains an extension account:
If your position’s range is “normal”, you never think about the extension account. Full-range positions (e.g., (MIN_TICK, MAX_TICK)) require it; the SDK resolves it for you.

Positions

A CLMM position is a bundle of three accounts plus a mint:

Position NFT mint

An SPL Token mint with supply 1. The mint’s address is a deterministic PDA; the position NFT in the owner’s wallet is just an ATA holding that single token. Transferring the NFT is how a position changes hands — the program keys authorization to the current holder of the NFT’s ATA balance, not to a Pubkey stored in state.

PersonalPositionState

One per open position. Keyed off the NFT mint.

ProtocolPositionState (deprecated)

Older CLMM releases stored aggregate per-(pool, tick_lower, tick_upper) bookkeeping in a ProtocolPositionState PDA. Newer releases no longer create or read this account. The slot still appears on the OpenPosition / IncreaseLiquidity / DecreaseLiquidity account lists as an UncheckedAccount for ABI compatibility, but the program does not write to it. Existing accounts on-chain are vestigial; the admin can call CloseProtocolPosition to reclaim rent for them.Aggregate range bookkeeping is now derived directly from the two endpoint ticks (liquidity_gross, liquidity_net, and the per-tick fee_growth_outside_* / reward_growths_outside_x64) in TickArrayState. The fee-growth-inside formula fee_growth_inside = global − outside_lower − outside_upper continues to work without an aggregate position account.

Observation

CLMM’s observation buffer stores a cumulative tick, not a cumulative price. External consumers compute the geometric-mean price over an interval from (tick_cumulative[t1] − tick_cumulative[t0]) / (t1 − t0) and then price = 1.0001 ** tick. See algorithms/clmm-math.

DynamicFeeConfig and DynamicFeeInfo

Dynamic fee parameters live in two places. The reusable template — DynamicFeeConfig — is admin-managed and shared across pools that opt in. The per-pool runtime state — DynamicFeeInfo — is embedded in PoolState and updated by every swap.

DynamicFeeConfig

PDA seed: ["dynamic_fee_config", index.to_be_bytes()]. Created via create_dynamic_fee_config (admin-gated) and modified via update_dynamic_fee_config. A pool created with enable_dynamic_fee = true snapshots the config’s five calibration parameters (filter_period, decay_period, reduction_factor, dynamic_fee_control, max_volatility_accumulator) into its own DynamicFeeInfo at creation time; later edits to the DynamicFeeConfig do not retroactively affect existing pools.

DynamicFeeInfo (embedded in PoolState)

The bottom four fields are state; the top five are calibration copied from DynamicFeeConfig. The fee math and the decay rules are documented under products/clmm/math and products/clmm/fees. Constants used by the formula:

LimitOrderState

One account per open limit order.
Lifecycle:
  1. Open — user calls open_limit_order, deposits total_amount of the input token, the order is bound to a TickState cohort.
  2. (optional) Increase / Decreaseincrease_limit_order adds to total_amount; decrease_limit_order returns unfilled tokens (and any settled output up to that point).
  3. Settle — when the cohort is fully or partially filled, the owner or the operational keeper calls settle_limit_order to push output tokens to the owner’s ATA.
  4. Close — once unfilled_amount == 0, the account is closeable. Rent always returns to owner.
PDA seed: [owner.as_ref(), limit_order_nonce.key().as_ref(), limit_order_nonce.order_nonce.to_be_bytes().as_ref()]. The order PDA is therefore unique per (owner, nonce_index, order_nonce).

LimitOrderNonce

Per-(wallet, nonce_index) counter that lets a single user run multiple parallel pipelines of limit orders without colliding on PDAs.
PDA seed: [user_wallet.as_ref(), &[nonce_index]]. Most clients use nonce_index = 0 and let order_nonce carry the cardinality.

Deriving the key accounts

The exact seed strings should always be double-checked against the on-chain IDL and reference/program-addresses.

Lifecycle quick reference

TickArrayState accounts are never closed by the program — they persist for the life of the pool. Once a tick array has been initialised it remains on-chain even when every tick inside it returns to liquidity_gross == 0. Re-using an existing tick array is free; only the first position to touch a never-initialised array pays its rent.

What to read where

Sources: