> ## Documentation Index
> Fetch the complete documentation index at: https://docs.picon.finance/llms.txt
> Use this file to discover all available pages before exploring further.

# Fees

> Base rate, dynamic surcharge, composition fee, and how LPs get paid

## 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 per `bin_step` at pool creation and admin-overridable per pool:

| Field                        | Meaning                                                                                  |
| ---------------------------- | ---------------------------------------------------------------------------------------- |
| `filter_period`              | Swaps closer together than this don't move the reference bin                             |
| `decay_period`               | Window of inactivity after which the accumulator fully resets instead of merely decaying |
| `reduction_factor`           | Decay ratio applied to the volatility accumulator between those two windows              |
| `dynamic_fee_control`        | Scales squared volatility into an actual fee rate                                        |
| `max_volatility_accumulator` | Hard cap on the accumulator                                                              |

**On every swap, before pricing anything**, the pool decays its own state:

```
elapsed = now - last_update_timestamp
if elapsed < filter_period:      no-op — rapid, contained trading doesn't spike the fee
elif elapsed < decay_period:     partial decay by reduction_factor, anchored to today's active bin
else:                            full reset to zero — a genuinely idle pool returns to base rate
```

**On every bin actually crossed during traversal**, the accumulator grows:

```
volatility_accumulator = min(
  volatility_reference + |reference_bin_id - bin_id| * MAX_BASIS_POINTS,
  max_volatility_accumulator,
)
```

Because this runs per bin inside the same swap's fold, a single large swap crossing many bins
prices its own later bins at a higher rate than its earlier ones — the fee responds within one
swap, not just between swaps.

**From accumulator to rate**:

```
dynamic_fee_rate = ceil(dynamic_fee_control * (volatility_accumulator * bin_step)^2 / DYNAMIC_FEE_DIVISOR)
```

Rounds up (LP-favorable). Scaling by `bin_step` means the same accumulator value implies a
larger price move — and thus a larger fee — on a coarser pool.

<Tip>
  Any quoting logic that doesn't replicate this exact decay-then-accumulate state machine,
  bin by bin, will systematically misprice a multi-bin swap. This is precisely what
  `@picon-finance/dlmm-sdk`'s bundled quote engine does internally — see
  [SDK → Pools and swaps](/sdk/pools-and-swaps).
</Tip>

## 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 = floor(fee * protocol_share / 10_000)
lp_fee       = fee - protocol_fee
```

`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
  sweeps `fee_owed` for 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.
