Fee rate representation
Pool.fee_rate is stored in native units, where FEE_PRECISION = 1_000_000_000 represents
100% — finer resolution than plain basis points, leaving headroom for very low fee tiers. It’s
validated at pool creation against a fixed table of 23 supported tiers, 0.01% up to 10.00%.
total_fee_rate = min(fee_rate + dynamic_fee_rate, MAX_FEE_RATE) — MAX_FEE_RATE is a hard
10% ceiling on the combined rate, so a highly volatile pool can never charge more than a
static-fee pool is bound by. Every downstream fee calculation — the swap fee, the composition
fee — reads through this one combined rate; neither knows or cares that it has two components.
Pool.protocol_share is a separate rate, in ordinary basis points, capped at 25%. It’s the
protocol’s cut of the swap fee specifically — it scales with volatility exactly as the LP
share does, since both are computed from the same combined rate. It does not apply to the
composition fee (below), which exists purely to compensate existing LPs.
Dynamic fee
A volatility-based surcharge on top of the static base rate, in the Trader Joe/Meteora lineage (a decaying reference bin plus a squared-volatility fee curve — not Raydium/Orca-style tick-crossing counters). It exists so a pool’s price can move sharply — a real trade, not routed through many small swaps to avoid it — without LPs being underpriced for the risk that represents, and decays back toward zero once the pool goes quiet. Configuration, seeded perbin_step at pool creation and admin-overridable per pool:
On every swap, before pricing anything, the pool decays its own state:
bin_step means the same accumulator value implies a
larger price move — and thus a larger fee — on a coarser pool.
Composition fee
Charged only on the active bin, only when a deposit’s ratio differs from the bin’s current composition. The intuition: a share is a claim locked to the bin’s composition, not to the depositor’s deposit ratio, so an off-ratio deposit is effectively an implicit swap against existing LPs — and that implicit swap gets charged the same way a real swap would. The amount that “crosses” is measured by simulating a deposit-then-immediate-redeem against the bin, not by comparing to a static target ratio — this correctly accounts for the deposit’s own effect on the bin’s composition (a large off-ratio deposit shifts the ratio itself, so less crosses than a naive pre-deposit-ratio estimate would predict). The fee is carved directly out of the deposit before shares are minted, and compounds into reserves before the depositor’s own shares exist — so a deposit-then-immediate-withdraw round trip cannot extract value from existing LPs; the depositor can only ever lose the fee, never recapture it. This fee is not protocol-split — 100% of it goes to existing LPs. Balanced deposits and deposits into an empty bin cross nothing (0 fee); a deposit entirely to
one side of the active bin’s existing composition is bounded by the opposite reserve — it
can never cross more value than actually exists on the other side.
Swap fee split and accrual
For each bin a swap touches:fee = gross_amount_in − net_amount_in for that bin. Split:
protocol_fee rounds down (favoring LPs, consistent with every other rounding decision in the
system). The protocol slice accumulates pool-wide (Pool.protocol_fee_x/y); the LP slice folds
into that specific bin’s per-share accumulator — a masterchef-style pattern where each
position’s FeeState lazily materializes its share of newly-accrued fee on its next
deposit/withdraw/claim, computed from the delta against its own last-seen checkpoint.
Claiming
claim_fee(any position owner, permissionless for their own position): materializes and sweepsfee_owedfor every non-empty bin in the requested range.claim_protocol_fee(admin-only): transfers the pool’s full accumulated protocol fee to admin-controlled accounts and resets the accumulator to zero.