The invariant
CPMM maintains the classic constant-product invariant on its two vaults: wherex is the vault0 balance after any Token-2022 transfer fees on receipt, and similarly for y. Every swap must leave k' ≥ k after accounting for trade fees credited to the LP (the protocol, fund, and creator buckets are not counted toward k — they sit in the vault but are excluded from the curve view, see Fees on the curve below). k therefore grows monotonically over time as LPs accrue fees.
LP shares are priced by the pool’s reserves, not by k:
Burning ΔLP LP tokens returns exactly ΔLP × x / lpSupply of token0 and ΔLP × y / lpSupply of token1. Neither the curve nor k moves on deposit or withdrawal — only swaps change the price.
Fee model on the swap path
CPMM applies two independently-rated fees on every swap:- The trade fee is taken on the input side, charged at
AmmConfig.trade_fee_rate. It is then split into LP, protocol, and fund shares (the LP share stays in the vault and growsk; the protocol and fund shares are extracted from vault accounting). - The creator fee (active only when
enable_creator_fee == true) is charged atAmmConfig.creator_fee_rate. It is taken on the input side or the output side depending onPoolState.creator_fee_onand the swap direction (seeproducts/cpmm/fees). It is its own bucket — never a slice of the trade fee.
The protocol’s share of the creator fee does not appear anywhere on this page, and that is deliberate. It is applied when
CollectCreatorFee moves accrued fees between two counters that the curve already excludes — not during a swap. Swap math, quotes and k are identical with and without it. See products/cpmm/fees.FEE_RATE_DENOMINATOR = 1_000_000trade_fee_rate— fromAmmConfig, e.g.,2500= 0.25% of the relevant volume sidecreator_fee_rate— fromAmmConfig, e.g.,1000= 0.10% of the relevant volume sideprotocol_fee_rate,fund_fee_rate— denominated in units of1/FEE_RATE_DENOMINATORof the trade fee, not of volume
protocol_fee + fund_fee + creator_fee amount is held in the vaults but tracked separately on the pool state (protocol_fees_token*, fund_fees_token*, creator_fees_token*). When the constant-product invariant checks k' ≥ k, it uses vault balances minus all three accrued-but-unswept fees — so LPs capture only lp_fee.
See products/cpmm/fees for the collection instructions and the worked numerical examples.
SwapBaseInput (input-exact)
“The user gives us exactlyamount_in of the input mint and receives at least minimum_amount_out of the output mint.”
Ignoring Token-2022 for a moment:
Δx_net = amount_in_after_trade_fee.
The program then updates the vault accounting such that the portion of trade_fee owed to protocol/fund/creator sits in “accrued” buckets (not included in the curve’s next x), while the LP share does join x for the next swap.
Token-2022 on the input side
If the input mint has a transfer-fee extension, the mint deducts its fee on the transfer from user → vault. So the vault actually receivesamount_in − transfer_fee_in. The CPMM program therefore computes:
amount_in_after_trade_fee. This matters because the curve price is computed off the net amount that landed in the vault, not off the user’s headline amount.
Token-2022 on the output side
If the output mint has a transfer fee, the pool sendsamount_out from its vault to the user. The mint will then skim its fee on the way out, so the user receives amount_out − transfer_fee_out(amount_out). The program computes amount_out from the curve as usual, but it is the integrator’s responsibility to convert the pool’s “vault send” number into a “user receive” number when showing quotes.
Slippage check
After computingamount_out:
minimum_amount_out so the slippage constant is denominated in what the user will actually receive, not in what the vault sends.
SwapBaseOutput (output-exact)
“The user will receive exactlyamount_out of the output mint and is willing to pay up to maximum_amount_in of the input mint.”
Inverting the curve for Δx_net:
The ceiling is important — it guarantees k' ≥ k after integer truncation. Then:
gross_needed.
Slippage check
Worked example
Pool state, ignoring Token-2022:x = 1_000_000_000_000(1,000,000.000000 of token0, 6 decimals)y = 2_000_000_000_000(2,000,000.000000 of token1, 6 decimals)AmmConfig:trade_fee_rate = 2500,protocol_fee_rate = 120_000,fund_fee_rate = 40_000,creator_fee_rate = 0
SwapBaseInput with amount_in = 1_000_000_000 (1,000.000000 of token0). Creator fee is disabled (enable_creator_fee = false).
enable_creator_fee = true with creator_fee_rate = 1000 (0.10%) on the input side, the program would charge total_input_fee = ceil(1_000_000_000 * 3500 / 1_000_000) = 3_500_000, then split it as creator_fee = 1_000_000 and trade_fee = 2_500_000. The protocol/fund/LP arithmetic on trade_fee is unchanged from the example above — the creator fee is its own bucket, accrued to creator_fees_token0 and excluded from curve_x along with the protocol and fund buckets.
If the input mint has a 1% Token-2022 transfer fee, the vault receives 990_000_000 tokens instead of 1_000_000_000, and every subsequent calculation uses that net amount.
Observation update rule
On every swap, the program evaluates whether to push a new observation into the ring buffer:- Cumulative price, not spot price. A single observation is not a price. To get a TWAP from time
t0tot1, read the observations closest to each end and compute(cumulative(t1) − cumulative(t0)) / (t1 − t0). - Samples are rate-limited. Back-to-back swaps in the same slot may share one observation. Reading an observation immediately after a swap can therefore look stale by one slot — this is normal.
products/clmm/accounts.
Fees on the curve
This is the subtle part and worth calling out. The curve arithmetic works against the net vault balances — i.e., raw SPL balance minus accrued protocol, fund, and creator fees (all three are independent buckets — seeproducts/cpmm/fees). A concrete picture:
- Do not quote from raw balances. Subtract the accrued-fee fields first, or call
SwapBaseInputas a simulation and take its return. CollectProtocolFeemoves tokens out of the vault. After collection,raw_vault_balancedrops butcurve_balanceis unchanged; the pool’s price does not move. This is deliberate.
Precision and overflow
- All curve arithmetic uses
u128intermediates to prevent overflow onx * y. - Division rounds toward zero except for
SwapBaseOutput’sΔx_net, which rounds up, and fee computation, which rounds up on thetrade_feeand down on the sub-splits. These rounding directions are chosen so the invariant never decreases due to integer truncation. - Pools with extreme vault ratios (billions : 1) may hit precision floors on small trades; the program returns
ZeroTradingTokensin that case. Seereference/error-codes.
Where to go next
products/cpmm/fees— the full fee tier and collection semantics.products/cpmm/instructions— the instructions that invoke this math.algorithms/constant-product— the derivation and edge cases ofx · y = kshared across AMM v4 and CPMM.
raydium-io/raydium-cp-swap— swap math instates/curve.rs- Raydium audit reports linked in
security/audits

