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

# LP fee entitlements

> How swap fees become range-specific, position-specific amounts owed.

An LP earns a share of the integer fee charged during a swap step only when its liquidity is active for that step. The entitlement is recorded through cumulative fee growth, not by iterating over positions during a swap.

The accounting path is:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
step fee
  -> protocol share and LP share
  -> global growth for the input token
  -> tick outside-growth checkpoints
  -> growth inside a position's range
  -> position settlement
  -> tokens owed
  -> collect
```

## Fee-bearing token

Fees accrue in the input token:

* `zero_for_one = true` advances token0 fee growth.
* `zero_for_one = false` advances token1 fee growth.

The output token's global accumulator does not advance merely because the pool paid output in that token.

For multi-hop swaps, the rule applies separately to every hop. A route can therefore generate fees in different token programs at different pools during one transaction.

## Trading fee and protocol share

The pool fee is expressed in fee pips, or parts per million. A step first calculates its integer total fee under the swap math.

The configured protocol fraction then splits it:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
protocol_fee = floor(total_fee * fee_protocol / 16)
lp_fee = total_fee - protocol_fee
```

`fee_protocol` may be zero or 4 through 10. The floor remainder belongs to `lp_fee`.

Protocol fees accumulate in separate slot balances. They do not appear in LP fee growth and cannot be claimed by positions.

## Global growth

For active liquidity `L` and the step's LP fee `F`:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
growth_delta = floor(F * 2^128 / L)
```

The input token's global growth becomes:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
global_next = global_previous + growth_delta mod 2^256
```

The implementation stores the value as `u256::U256`:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
U256 {
    hi: high 128 bits,
    lo: low 128 bits
}
```

The packed value is:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
(hi << 128) | lo
```

The accumulator wraps by design. A decrease in the packed raw value does not mean fees were removed.

Read [Q128.128 fee growth](../math/q128-fee-growth).

## Liquidity epochs

A liquidity epoch is a period during which one active-liquidity set earns fee growth.

Epoch boundaries include:

* Minting active liquidity
* Increasing or decreasing active liquidity
* Freezing a live position
* Crossing an initialized tick
* Reinitializing a removed boundary

The implementation stores no pool-level residual from fee-growth division. Each step records the floor at 128-bit fractional precision using the liquidity active in that step. The sub-Q128 remainder is discarded, so it cannot cross into a later epoch and be credited to a different active-liquidity set.

Discarding the sub-Q128 remainder prevents fee fractions from crossing liquidity epochs. It can also leave rounding surplus in the contract.

## Tick outside growth

Each initialized tick stores outside growth for token0 and token1. At a crossing:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
outside0_next = global0 - outside0_previous mod 2^256
outside1_next = global1 - outside1_previous mod 2^256
```

Both tokens flip, even though only the input token generated a fee in the current step. Crossing reverses which side of the tick is considered outside, so both reference frames must change.

The current step's fee is added before the flip. Its LP fee therefore belongs to the pre-crossing active-liquidity set.

A crossing also completes when the tick price equals the user's price limit. The slot finishes with post-crossing liquidity even if the swap stops at that exact price.

## Growth inside a range

For either token, define:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
below =
    current_tick >= lower_tick
      ? lower.outside
      : global - lower.outside

above =
    current_tick < upper_tick
      ? upper.outside
      : global - upper.outside

inside = global - below - above
```

Every subtraction wraps modulo `2^256`.

The branch conditions implement:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
lower_tick <= current_tick < upper_tick
```

At the lower tick, the range is active. At the upper tick, it is not.

## Position checkpoint

A public `Position` stores:

* Current liquidity
* `fee_growth_inside0_last_x_128`
* `fee_growth_inside1_last_x_128`
* Integer `tokens_owed0`
* Integer `tokens_owed1`

For each token:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
delta_inside =
    inside_now - inside_last mod 2^256

pending_fee =
    floor(delta_inside * position_liquidity / 2^128)
```

Pending fees are not continuously written to `tokens_owed`. They become settled integer amounts during increase, decrease, collect, or freeze.

## Settlement events

| Operation | Liquidity used for pending fees | State after settlement                                 |
| --------- | ------------------------------- | ------------------------------------------------------ |
| Mint      | None                            | New position snapshots current inside growth           |
| Increase  | Old position liquidity          | Checkpoint advances, then liquidity increases          |
| Decrease  | Full pre-decrease liquidity     | Checkpoint advances, then liquidity decreases          |
| Collect   | Current liquidity               | Checkpoint advances, requested amount leaves           |
| Freeze    | Full live liquidity             | Fees and principal become owed, liquidity becomes zero |

This ordering prevents new liquidity from sharing old growth and prevents removed liquidity from losing growth already earned.

Burn performs no fee settlement. It removes a position only after liquidity and both owed balances are zero.

## Worked example

Suppose one step has:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
total fee = 160 internal token0 units
fee_protocol = 4
active liquidity = 60
position liquidity = 15
```

The split is:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
protocol fee = floor(160 * 4 / 16) = 40
LP fee = 160 - 40 = 120
```

Global token0 growth advances by:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
floor(120 * 2^128 / 60) = 2 * 2^128
```

If the position was active for the step and no boundary adjustment removes the growth from its range:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
pending token0 fee =
    floor((2 * 2^128) * 15 / 2^128)
  = 30
```

The protocol records 40, the position earns 30, and the other active liquidity earns the remaining 90 in proportion to its liquidity, subject to settlement floors.

## Rounding and dust

Global growth floors once per fee-bearing step. The aggregate shortfall is less than one native token base unit because active liquidity is a `u128` and the fee-growth scale is `2^128`.

Position settlement floors again. Its shortfall is less than one native base unit for that position and token at that settlement.

Advancing the checkpoint discards that position fraction. It is not stored in a residual field and is not reassigned to another LP.

The contract has no generic sweep for the resulting surplus. `collect_protocol` can withdraw only recorded protocol fees.

Institutional reconciliation should use:

```text theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
gross fees
= recorded protocol fees
+ claimable or collected LP fees
+ rounding surplus retained by the contract
```

Read [Rounding, overflow, and invariants](../math/rounding-overflow-and-invariants).

## Public observability

Global growth, tick outside growth, position checkpoints, position liquidity, and settled owed amounts are public mapping state. Collection requests are also public.

The `PositionNFT` record authorizes the holder without storing the owner's address in `Position`. The position's range and fee economics remain public.

An observer with a token ID and a consistent state snapshot can estimate pending fees. The estimate does not identify the private immutable withdrawal address.

## Frozen positions

Freezing a live position removes its entire liquidity and settles fees plus principal into `tokens_owed`. The freeze then blocks collect. Unfreeze restores access to the owed value but does not restore active liquidity.

Fees stop accruing after liquidity becomes zero. The holder must increase or mint again after unfreeze to return capital to the active set.

## Off-chain calculation rules

An indexer or wallet should:

1. Read slot, lower tick, upper tick, and position at one state height.
2. Pack each `u256::U256` from its two limbs.
3. Calculate below, above, and inside separately for token0 and token1.
4. Use modulo `2^256` subtraction.
5. Multiply by position liquidity using arbitrary precision.
6. Shift right by 128 bits.
7. Reject a result above `u128` maximum, matching the contract.
8. Add stored `tokens_owed`.
9. Keep the result in the token's native base units and apply decimals only for display.

Do not use floating-point arithmetic or ordinary signed subtraction.
