Skip to main content
This page pairs with products/clmm/accounts (what the accounts are) and products/clmm/math (what the math is). It is authoritative for arguments and account ordering; specific byte layouts come from the IDL.The 2026-09 program upgrade rebuilt CLMM on Anchor 1.0.2 / Solana 3.1.10, added the admin instruction CollectExcessLamports, and changed what CreateAmmConfig writes into owner / fund_owner. No user-facing instruction changed its accounts, arguments or math. See the 2026-09-30 changelog entry.

Instruction inventory

There is no init-tick-array instruction, and none is needed. A tick array is created inside OpenPosition* / IncreaseLiquidity* by TickArrayState::get_or_create_tick_array, paid for by payer. Pass the (possibly still-uninitialized, system-owned) tick-array PDA as tick_array_lower / tick_array_upper and the program allocates it if it is missing. OpenLimitOrder does the same for its single tick_array.
Most admin-only instructions (CreateAmmConfig, UpdateAmmConfig, UpdatePoolStatus, CreateOperationAccount, UpdateOperationAccount, CloseProtocolPosition) are gated by the program’s hardcoded admin pubkey. CreatePermissionPda / ClosePermissionPda accept either the admin pubkey or a dedicated permission_pda_admin key; CreateSupportMintAssociated / CloseSupportMintAssociated accept either the admin pubkey or a dedicated support-mint owner key; CollectExcessLamports accepts either the admin pubkey or a dedicated collect-lamports wallet. Reward-stream admin instructions (TransferRewardOwner, CollectRemainingRewards) are gated by the reward funder, not the program admin. V2 suffix means “supports Token-2022 on vaults / NFT, requires bitmap-extension slot”. The SDK picks V2 by default for new pools.

CreatePool

Arguments
Accounts (abridged)
Slots 10 and 11 are per-mint, not “SPL Token then Token-2022”. Each is constrained by mint::token_program on the matching mint, so for a pool whose token_mint_0 is Token-2022 and token_mint_1 is classic SPL you must pass Token-2022 in slot 10 and SPL Token in slot 11. CreateCustomizablePool and CreatePermissionedPool use the same two slots.
Preconditions
  • token_mint_0 < token_mint_1 by byte order.
  • amm_config.disable_create_pool == false.
  • Mints are not rejected by the Token-2022 extension allow-list.
Postconditions
  • pool_state.sqrt_price_x64 = sqrt_price_x64, tick_current = floor(log_{1.0001}(price)).
  • pool_state.liquidity = 0 (no positions yet).
  • pool_state.fee_on = FromInput (legacy default).
  • pool_state.dynamic_fee_info is zeroed (dynamic fee disabled).

CreateCustomizablePool

Recommended for new pools. Same effect as CreatePool plus per-pool fee-collection mode and an optional dynamic-fee opt-in. Arguments
Accounts — exactly the 13 declared accounts of CreatePool. dynamic_fee_config is not a declared account.
When enable_dynamic_fee = true, the DynamicFeeConfig to snapshot must be supplied as the last remaining_account — the handler reads ctx.remaining_accounts.last(). Any SupportMintAssociated bypass PDAs must therefore come before it, or the program snapshots the wrong account and fails deserialization. Omitting it while the flag is set fails with AccountLack.
Preconditions — same as CreatePool. If enable_dynamic_fee = false, no remaining_account is required and any that are passed are only scanned for support-mint records. Postconditions
  • pool_state.fee_on set to the chosen CollectFeeOn variant.
  • If dynamic fee was enabled: pool_state.dynamic_fee_info is initialized from the supplied DynamicFeeConfig (five calibration parameters copied; state fields zeroed).
  • Otherwise: pool_state.dynamic_fee_info is zeroed (= dynamic fee inactive forever for this pool).
fee_on and the dynamic-fee enablement bit are set only at pool creation. There is no in-place upgrade — pools created via legacy CreatePool cannot retroactively gain dynamic fee or single-sided fee. New deployments should default to this instruction.

CreatePermissionedPool

Both CreatePool and CreateCustomizablePool derive the pool PDA from ["pool", amm_config, token_mint_0, token_mint_1], so there is exactly one canonical pool address per (config, mint0, mint1) triple — a second init at the same seeds fails. CreatePermissionedPool lifts that restriction by folding a client-supplied seed_index: u16 into the pool PDA seeds, allowing multiple pools for the same pair and fee tier — each at its own address. Because an arbitrary pool address is a privileged capability, the payer must hold a Permission PDA that authorizes it. Everything else about the pool is identical to CreateCustomizablePool: it takes the same CreateCustomizableParams and supports single-sided fee and the dynamic-fee opt-in. Arguments
Accounts (abridged) — same as CreateCustomizablePool plus, at the front: The pool_state PDA is derived from ["pool", amm_config, token_mint_0, token_mint_1, seed_index.to_le_bytes()]. Preconditions
  • seed_index != 0. A seed_index of 0 is reserved for legacy pools and is rejected here; the [0, 0] seed component is what makes a legacy pool address collapse to the classic four-seed form.
  • The permission PDA for payer exists (created by an admin via CreatePermissionPda).
  • Same mint / allow-list rules as CreatePool.
Postconditions
  • A new pool_state exists at the seed_index-derived address, with pool_state.seed_index = seed_index.
  • All other post-state matches CreateCustomizablePool (fee mode, optional dynamic fee).
This instruction does not widen general pool-creation access — permissionless creation continues through CreatePool / CreateCustomizablePool, which remain one-pool-per-pair. CreatePermissionedPool exists for the specific case where a whitelisted operator needs several pools for the same pair (e.g. differing initial prices or launch cohorts) and holds a Permission PDA granted by the admin.

OpenPositionV2 / OpenPositionWithToken22Nft

Create a new position inside an existing pool. Arguments
Accounts — the two variants have different account lists. OpenPositionV2 declares 22; OpenPositionWithToken22Nft declares 20. OpenPositionV2, in order: OpenPositionWithToken22Nft is the same list with metadata_account (5) and metadata_program (19) removed — it writes the position’s metadata through the Token-2022 metadata extension on the NFT mint instead — giving 20 accounts. Its position_nft_mint is a bare Signer and position_nft_account an UncheckedAccount.
tick_array_bitmap_extension is not a declared account on either variant. When the position’s range falls outside the pool’s inline tick_array_bitmap (±512 tick arrays), append the TickArrayBitmapExtension PDA at seeds ["pool_tick_array_bitmap_extension", pool_state] as remaining_accounts[0].
Math — see products/clmm/math. Given base_flag, the program resolves either liquidity or (amount_0_max, amount_1_max) into the actual L and the actual token amounts consumed. Preconditions
  • tick_lower < tick_upper, both multiples of pool.tick_spacing, within [MIN_TICK, MAX_TICK].
  • The two tick-array PDAs are passed. They need not already exist — get_or_create_tick_array allocates a missing one at payer’s expense inside this instruction. There is no separate init-tick-array instruction.
  • User has at least amount_0_max and amount_1_max in the source ATAs.
Postconditions
  • personal_position exists, liquidity set, fee_growth_inside_last snapshotted.
  • Tick-array entries at tick_lower and tick_upper updated (liquidity_gross += L, liquidity_net ± L, fee-growth snapshots maintained).
  • pool_state.liquidity += L if position is in range (tick_lower ≤ tick_current < tick_upper).
  • The position NFT mint records pool_state as freeze authority. Mint authority is removed after the single NFT is minted. Recording the freeze authority does not change the NFT token account’s state.
  • The NFT token account remains unfrozen unless the instruction is OpenPositionV2 or OpenPositionWithToken22Nft and either vault mint’s freeze authority matches CLMM’s restricted-issuer list. Only that matching V2 path freezes the account. OpenPosition V1 does not freeze.
Common errors — TickInvalidOrder (tick_lower >= tick_upper), TickAndSpacingNotMatch (an endpoint is not a multiple of tick_spacing), InvalidTickIndex (outside [MIN_TICK, MAX_TICK]), MissingTickArrayBitmapExtensionAccount (range outside the inline bitmap and the extension was not appended), NotApproved (pool_state.status blocks opening), ZeroAmountSpecified.
Position freezing does not add declared instruction accounts or arguments. Clients can open these positions with the existing V2 layouts. The behavior is selected on-chain from vault_0_mint and vault_1_mint.

IncreaseLiquidityV2

Add liquidity to an already-open position. Arguments
Accounts — 15, and not derivable from OpenPosition: there is no rent, no system_program, no associated_token_program and no metadata account. Prepend the TickArrayBitmapExtension PDA as remaining_accounts[0] when the position’s range is outside the inline bitmap. Effect
  • Transfers amount_0_actual / amount_1_actual from user → vaults.
  • Increments personal_position.liquidity and pool_state.liquidity (if in range), and the endpoint-tick liquidity_gross / liquidity_net accordingly.
  • Collects fees and rewards owed since last touch and credits them to token_fees_owed_{0,1} / reward_amount_owed. Those are paid out only on DecreaseLiquidity / DecreaseLiquidityV2, not on increase — there is no standalone collect instruction.

DecreaseLiquidityV2

Remove liquidity from a position. Arguments
Accounts — 16, and not the same shape or order as IncreaseLiquidityV2: personal_position and pool_state are swapped, the vaults come before the tick arrays, the user-side accounts are named recipient_token_account_*, and there is an extra memo_program. Remaining accounts — three per active reward being collected, in the order reward_token_vault(W), recipient_token_account(W), reward_vault_mint. Prepend the TickArrayBitmapExtension PDA when the position’s range is outside the inline bitmap.
This is also the only way to collect fees and rewards. To collect without changing the position, call it with liquidity = 0, amount_0_min = 0, amount_1_min = 0.
Effect
  • Computes (amount_0, amount_1) for the removed L given current sqrt_price_x64.
  • Settles fees/rewards accrued since the last touch, same as IncreaseLiquidity.
  • Transfers amount_0 + fees_owed_0 and amount_1 + fees_owed_1 out of vaults to the user.
  • Decrements liquidity counters; if the new personal_position.liquidity == 0, the position is eligible for ClosePosition.
Slippage — amount_0_min and amount_1_min are the minimums the user accepts net of Token-2022 transfer fees on the output side.

ClosePosition

Burn the position NFT and close PersonalPositionState. Declared accounts Remaining accounts
  • Unfrozen NFT: none required; an extra pool account is harmless because the handler does not read it.
  • Frozen NFT: append personal_position.pool_id as the first remaining account. The program loads it as PoolState and uses its PDA seeds to sign the thaw.
Preconditions
  • personal_position.liquidity == 0.
  • tokens_fees_owed_{0,1} == 0.
  • All reward counters reward_amount_owed == 0.
(I.e., collect everything and decrease-to-zero first.) Effect
  • If the NFT token account is frozen, verifies the first remaining account equals personal_position.pool_id, then thaws it with the pool PDA.
  • Burns the NFT.
  • Closes the NFT token account and personal_position, refunding rent to nft_owner. If the position NFT uses Token-2022, it also closes the NFT mint; classic SPL Token mints cannot be closed and remain with supply zero.
The thaw, burn, and close are atomic. The NFT cannot become transferable between those steps. Conditional client break — the declared IDL layout is unchanged, so legacy clients continue to close existing and unfrozen positions. A legacy builder that omits the pool remaining account fails with AccountLack when closing a frozen position. Passing the pool for every close is the simplest compatible strategy.

SwapV2

Walk the liquidity curve; exact input or exact output depending on is_base_input. Arguments
Accounts (abridged) Callers pass a ranked list of tick arrays covering the expected swap walk; the program uses as many as it needs. The SDK computes this list via PoolUtils.computeAmountOutFormat or the API’s quote endpoint. Preconditions
  • pool_state.status allows swap.
  • now >= open_time.
  • sqrt_price_limit_x64 is on the correct side of sqrt_price_x64 for the direction.
Common errors — TooLittleOutputReceived (exact-in slippage), TooMuchInputPaid (exact-out slippage), SqrtPriceLimitOverflow, NotEnoughTickArrayAccount, InvalidFirstTickArrayAccount, MissingTickArrayBitmapExtensionAccount, LiquidityInsufficient, NotApproved (swap bit set on pool_state.status). CLMM has no ExceededSlippage variant — that name is CPMM’s — and no TickArrayNotFound. What SwapV2 does internally that callers should know about (post-2025 release):
  1. Dynamic fee surcharge — if pool.dynamic_fee_info is non-zero, the program updates the volatility accumulator using the tick distance traversed since the last swap (with the filter/decay rules from products/clmm/fees) and adds a dynamic_fee_component on top of AmmConfig.trade_fee_rate. Total fee is capped at 10% (MAX_FEE_RATE_NUMERATOR / 1_000_000).
  2. Limit-order matching — when the price walk crosses a tick that holds open limit orders, the program first fills available limit-order liquidity at that tick (FIFO by order_phase), then proceeds along the LP liquidity curve. Filled amounts update tick.unfilled_ratio_x64 and tick.part_filled_orders_remaining for later settlement; orders themselves remain unspent until their owner calls SettleLimitOrder.
  3. Single-sided fee routing — when pool.fee_on = Token0Only or Token1Only, the swap step still computes the same input-output trade; the fee is then routed to the configured side. For directions where the configured fee side is the output, the fee is deducted from the swap output (the user receives out − fee); for directions where it is the input, behavior matches FromInput. See is_fee_on_input(zero_for_one) and is_fee_on_token0(zero_for_one) on PoolState.
Swap (V1) implements the same dynamic fee, single-sided fee routing, and limit-order matching as SwapV2; the only feature it lacks is Token-2022 support — both vaults must be classic SPL Token. Pools with any Token-2022 mint must be swapped via SwapV2. The aggregator and SDK already prefer V2 for every CLMM leg so callers don’t have to branch on mint type.

OpenLimitOrder

Place a sell order at a specific tick. The order sits in a per-tick FIFO cohort and fills as price walks past. Arguments
Accounts (abridged) Remaining accounts — [0] tick_array_bitmap_extension, required only when initializing a tick array whose start index falls outside the pool’s inline bitmap. Pass nothing otherwise.
There is no rent account: the struct ends at system_program, 13 declared accounts. A stray rent in position 14 lands exactly where the optional bitmap extension is read, so the order appears to work until the first time a tick array outside the inline bitmap has to be created — and then fails.
Account-list change (2026-07 release). OpenLimitOrder now also takes the output-side accounts — output_token_account, output_vault, and output_vault_mint — in addition to the input side. They are used only for validation: the program rejects the order if the owner’s input or output token account is frozen. This guarantees that a fill can actually be settled to the owner’s output ATA, which matters for allow-list / default-frozen Token-2022 mints (e.g. permissioned tokens) where an account may not yet be thawed. Clients built against the older single-sided account list must add the three output accounts.
Preconditions
  • Neither input_token_account nor output_token_account is frozen (else NotApproved).
  • pool_state.status allows both the swap (bit 4) and limit-order (bit 5) operations (else NotApproved).
  • tick_index % pool.tick_spacing == 0 and within [MIN_TICK, MAX_TICK].
  • tick_index is on the right side of pool.tick_current for the chosen direction (selling token0 → tick must be above current, and vice versa). Selling at a tick already crossed would be matched immediately and is rejected.
Postconditions
  • limit_order exists, snapshotting tick.order_phase and tick.unfilled_ratio_x64 at open time.
  • tick.orders_amount += amount (in the current cohort).
  • limit_order_nonce.order_nonce += 1.
  • OpenLimitOrderEvent emitted.
Common errors — NotApproved (input or output token account frozen, or the pool has swap / limit-order disabled), ZeroAmountSpecified (amount == 0 after the input-side transfer fee), InvalidLimitOrderAmount (the amount would produce an output below 1 base unit at that tick, or overflow u64), InvalidTickIndex (out of [MIN_TICK, MAX_TICK], or on the wrong side of tick_current for the chosen direction), TickAndSpacingNotMatch (tick_index % pool.tick_spacing != 0), OrderPhaseSaturated.

IncreaseLimitOrder

Add to an existing open order. Only callable by the order’s owner. Arguments
Accounts — 8, input side only. It drops the nonce account, all three output-side accounts and system_program. Preconditions
  • limit_order.owner == signer.
  • The order is still in the same cohort (tick.order_phase == limit_order.order_phase). If the cohort has already begun filling, the order is partially settled — the caller should call DecreaseLimitOrder or SettleLimitOrder first to roll forward.
Effect
  • Transfers amount from owner ATA to input_vault.
  • limit_order.total_amount += amount; tick.orders_amount += amount.

DecreaseLimitOrder

Reduce or fully cancel an open order. Pays the unfilled remainder back to the owner, plus any output already settled by past partial fills. Arguments
Accounts — both input and output token sides: Effect
  • Recomputes the order’s filled amount from the cohort’s unfilled_ratio_x64 since open.
  • Sends filled output to output_token_account.
  • Sends amount of unfilled input back to input_token_account.
  • Updates limit_order accordingly. If the new unfilled remainder is zero, the program closes the account and refunds rent to owner.

SettleLimitOrder

Push filled output tokens to the owner without changing the order’s unfilled remainder. Useful when auto_withdraw keepers want to drip-pay long-running partial fills. Caller — either the order’s owner, or the program’s limit_order_admin (an off-chain operational hot wallet that runs an automated keeper loop). The keeper has no other authority — it cannot move user funds outside of pushing filled output to the order’s owner ATA. Accounts Effect
  • Computes the cumulative output owed using (limit_order.unfilled_ratio_x64, tick.unfilled_ratio_x64).
  • Transfers the delta to output_token_account.
  • Updates limit_order.settled_output.
  • Does not close the order; it is still open against any remaining input.

CloseLimitOrder

Close a fully-consumed order account. Rent is always returned to limit_order.owner regardless of who signs. Caller — either owner or limit_order_admin. Preconditions
  • The order has zero unfilled remainder (either amount == total_amount was filled and settled, or the owner previously decreased the order to zero and forgot to close).
Effect
  • Closes limit_order; rent is sent to limit_order.owner.

CreateDynamicFeeConfig (admin)

Create a reusable parameter set under a u16 index. Arguments
Accounts Common errors — InvalidDynamicFeeConfigParams if decay_period <= filter_period or any 0-valued field is out of bounds.

UpdateDynamicFeeConfig (admin)

Modify an existing DynamicFeeConfig. Pools that already snapshotted the config at creation time are not retroactively updated; only newly-created pools that reference this config will pick up the new values. Arguments — same five calibration fields as CreateDynamicFeeConfig (filter_period, decay_period, reduction_factor, dynamic_fee_control, max_volatility_accumulator); index is fixed at creation and not re-passed here.

CollectProtocolFee / CollectFundFee

Sweep accrued protocol/fund fees from the pool’s vaults to a recipient, zeroing the corresponding PoolState.protocol_fees_* / fund_fees_* fields. This is not CPMM’s layout — CLMM has no authority account, and the recipient fields are named recipient_token_account_{0,1} rather than recipient_token_{0,1}_account. Arguments — amount_0_requested: u64, amount_1_requested: u64.
Changed in 2026-09: new configs no longer take their fee owners from the signer. create_amm_config now writes the program’s hardcoded protocol_fee_owner::ID into owner and fund_fee_owner::ID into fund_owner. Before, it copied the admin signer’s key into both. On mainnet these are the same keys already stored on all 21 existing configs. Addresses are in reference/program-addresses.The accepted signers above are unchanged. The admin can still collect from any config, and existing AmmConfig accounts are not rewritten. Read owner / fund_owner off the account instead of assuming either value. UpdateAmmConfig params 3 and 4 still rotate them.

CollectExcessLamports

Admin sweep of lamports above the rent-exempt minimum on accounts CLMM controls. It was added in the 2026-09 upgrade to reclaim the over-funding that the SIMD-0437 rent reduction leaves on accounts created before each step. Only the excess moves. Token balances, account data, owners, pool state and the price curve are untouched. An account already at its minimum is left as is, so the instruction can be re-run safely after each rollout step. Arguments: none. Accounts Scope: one pool per call. CLMM has no program-wide vault authority. Each pool’s vaults are owned by its own PoolState. Every token-program source must therefore have the pool_state in slot 2 as its authority: token_vault_0, token_vault_1, or one of that pool’s reward vaults. If you pass another pool’s vault or a user’s token account, the token program’s owner check fails and the whole instruction reverts. It is not skipped. Position NFT mints cannot be swept, because their mint authority is revoked when the position is opened. How each source account is handled The program makes two passes over remaining_accounts: every token-program CPI first, then the direct debits. Interleaving the two aborts with the runtime’s UnbalancedInstruction error, so the order is fixed in the program and you don’t have to sort the list yourself.
Pass 2 covers accounts whose rent a user paid. A PersonalPositionState or LimitOrderState passed here gives up its excess like any other CLMM-owned account. It stays rent-exempt and keeps its data. When it is later closed, the owner gets back whatever the account holds at that point, which after a sweep is the current rent minimum.
Transaction size is the real limit on how many sources fit in one call. It is the same constraint as the wallet-side sweep described in solana-fundamentals/rent-and-reclaimable-rent. Common errors:
  • NotApproved (6000): wrong signer.
  • LamportsCalculateError (6052): the wSOL round-trip did not net to zero.
  • A token-program owner-mismatch error: a token source is not owned by pool_state.
  • InsufficientFunds: a program-owned account holds less than its own rent minimum.
No SDK builder. @raydium-io/raydium-sdk-v2 does not ship a builder for this admin path. Encode it by hand from the IDL.

InitializeReward

Add a new reward stream to a pool. Up to 3 streams may be active at once. Arguments — a single struct, param: InitializeRewardParam:
The wire encoding is the three fields back to back, so a hand-built instruction is unaffected — but an IDL-driven client must pass one param object rather than three positional arguments. Accounts Preconditions
  • Less than 3 streams currently active on the pool.
  • Funder deposits total_emission = emissions_per_second × (end_time − open_time) worth of reward token into the vault as part of this instruction.
  • Whitelisted reward mint per operation_state.

SetRewardParams

Extend, top up, or change emission rate on an existing reward stream. Typically called by a pool creator or the Raydium multisig. Constraints live on-chain: you can usually extend end_time or increase emissions, not shrink them retroactively. Check operation_state’s owner list.

UpdateRewardInfos

Pure bookkeeping — settles reward_growth_global_x64 to the current time by multiplying emissions_per_second × Δt / liquidity. Called internally by every liquidity-touching instruction. Exposed as a standalone instruction because external actors (UIs, cranks) sometimes want to trigger it.

Collecting rewards

There is no standalone collect-reward instruction. The program exposes no CollectReward entrypoint, and the published IDL has none either.
Rewards owed to a position are paid out by DecreaseLiquidity / DecreaseLiquidityV2. To collect without changing the position, call it with liquidity = 0, amount_0_min = 0, amount_1_min = 0. The reward vaults and recipient accounts go in remaining_accounts in groups of three per active reward, in the order reward_token_vault(W), recipient_token_account(W), reward_vault_mint. CollectRemainingRewards is a different thing: it lets the reward funder, after a stream’s end_time, sweep tokens that were never allocated to any position.

State-change matrix

Where to go next

Sources: