Skip to main content

The invariant

CPMM maintains the classic constant-product invariant on its two vaults: x⋅y=kx \cdot y = k where x 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: LP price in token0=xlpSupply,LP price in token1=ylpSupply\text{LP price in token0} = \frac{x}{\text{lpSupply}}, \qquad \text{LP price in token1} = \frac{y}{\text{lpSupply}} 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 grows k; the protocol and fund shares are extracted from vault accounting).
  • The creator fee (active only when enable_creator_fee == true) is charged at AmmConfig.creator_fee_rate. It is taken on the input side or the output side depending on PoolState.creator_fee_on and the swap direction (see products/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.
Let:
  • FEE_RATE_DENOMINATOR = 1_000_000
  • trade_fee_rate — from AmmConfig, e.g., 2500 = 0.25% of the relevant volume side
  • creator_fee_rate — from AmmConfig, e.g., 1000 = 0.10% of the relevant volume side
  • protocol_fee_rate, fund_fee_rate — denominated in units of 1/FEE_RATE_DENOMINATOR of the trade fee, not of volume
When the creator fee is on the input side:
When the creator fee is on the output side:
In both cases the trade fee is split the same way:
The 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 exactly amount_in of the input mint and receives at least minimum_amount_out of the output mint.” Ignoring Token-2022 for a moment:
By algebra: amount_out=y⋅Δxnetx+Δxnet\text{amount\_out} = \frac{y \cdot \Delta x_{\text{net}}}{x + \Delta x_{\text{net}}} where Δ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 receives amount_in − transfer_fee_in. The CPMM program therefore computes:
and runs the curve against 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 sends amount_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 computing amount_out:
If the output mint charges a transfer fee, the SDK applies the transfer fee before setting 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 exactly amount_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: Δxnet=⌈x⋅amount_outy−amount_out⌉\Delta x_{\text{net}} = \left\lceil \frac{x \cdot \text{amount\_out}}{y - \text{amount\_out}} \right\rceil The ceiling is important — it guarantees k' ≥ k after integer truncation. Then:
On Token-2022 input, wrap with:
so the user pays enough that after the mint’s transfer-fee deduction the pool still receives 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
User: SwapBaseInput with amount_in = 1_000_000_000 (1,000.000000 of token0). Creator fee is disabled (enable_creator_fee = false).
If the same pool had 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:
Two properties:
  • Cumulative price, not spot price. A single observation is not a price. To get a TWAP from time t0 to t1, 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.
More in 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 — see products/cpmm/fees). A concrete picture:
Consequences for integrators:
  • Do not quote from raw balances. Subtract the accrued-fee fields first, or call SwapBaseInput as a simulation and take its return.
  • CollectProtocolFee moves tokens out of the vault. After collection, raw_vault_balance drops but curve_balance is unchanged; the pool’s price does not move. This is deliberate.

Precision and overflow

  • All curve arithmetic uses u128 intermediates to prevent overflow on x * y.
  • Division rounds toward zero except for SwapBaseOutput’s Δx_net, which rounds up, and fee computation, which rounds up on the trade_fee and 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 ZeroTradingTokens in that case. See reference/error-codes.

Where to go next

Sources: