Skip to main content
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:

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:
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:
The input token’s global growth becomes:
The implementation stores the value as u256::U256:
The packed value is:
The accumulator wraps by design. A decrease in the packed raw value does not mean fees were removed. Read Q128.128 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:
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:
Every subtraction wraps modulo 2^256. The branch conditions implement:
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:
Pending fees are not continuously written to tokens_owed. They become settled integer amounts during increase, decrease, collect, or freeze.

Settlement events

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:
The split is:
Global token0 growth advances by:
If the position was active for the step and no boundary adjustment removes the growth from its range:
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:
Read 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.