Skip to content

Research notes

Uniswap v3 Fee Growth vs Tokens Owed

Follow a hypothetical v3 position through a partial collection, an upper-tick crossing and re-entry, with a ledger that reconciles 88 A and 142 B.

Ivory paper ribbon unwinds from a ceramic spool toward a lime pin and a tray of separate folded pieces.
Illustration for research notes; examples in this article are educational.

In Uniswap v3, fee growth measures accumulated fees per unit of liquidity, while tokens owed is a stored token balance from the position’s last accounting update. Current uncollected fees can include both stored owed fees and newer growth since that checkpoint. After liquidity is removed, tokens owed can also contain released principal, so the field is not always a fee-income total.

That distinction explains a common puzzle: swaps continue, but an NFT’s tokensOwed0 and tokensOwed1 stay unchanged. Reading those fields does not refresh them. A current estimate needs the pool’s range accounting as well as the NFT record.

The mechanics below follow Uniswap v3 core and NonfungiblePositionManager release v1.0.0. They describe that implementation; a vault, fork or different manager needs its own accounting rules.

Three records answer different questions

The pool maintains separate global fee-growth counters for token0 and token1. They measure growth per unit of active liquidity across the pool’s history, rather than the number of tokens earned by a particular NFT. In the v3 pool implementation, the swap step removes any configured protocol share before increasing LP fee growth.

A second calculation isolates the growth inside a position’s lower and upper ticks. The third record belongs to the position: its liquidity, last inside-growth checkpoint and stored owed balances. The manager interface explicitly describes those owed amounts as balances from the last computation.

For one token, calculate the new raw fee amount as floor(L × growth delta / 2^128). Here L is that position’s liquidity, and the delta is current inside growth minus its saved checkpoint. Add the result to previously stored owed fees when estimating the full fee claim. Repeat separately for the other token. The X128 scale is removed by dividing by 2^128; the resulting raw amount still needs that token’s decimal conversion.

The v3 Position library performs this checkpoint calculation with integer arithmetic. On a liquidity change, it credits the elapsed growth using the old L before storing the new liquidity. Multiplying today’s L by all historical inside growth would give newly added liquidity credit for a period when it did not exist.

L itself is not a token quantity or a dollar value. The separate liquidity units versus token amounts guide explains that conversion. Fee counters also differ from the reserve-product checkpoint discussed in Uniswap kLast versus current k; these are different records in different protocol versions.

Reconstruct the growth inside the range

For each token, call global growth G, the lower tick’s outside growth Oₗ and the upper tick’s outside growth Oᵤ. The v3 Tick library chooses the below-range and above-range amounts using the current pool tick:

Current tickGrowth below lower boundGrowth above upper bound
Below lowerG − OₗOᵤ
At or above lower, below upperOₗOᵤ
At or above upperOₗG − Oᵤ

Inside growth is G − below − above. The lower bound is inclusive and the upper bound is exclusive in these branches.

An outside accumulator is relative bookkeeping. It is not simply “fees earned before crossing this price.” When an initialized tick is crossed, its outside value becomes global growth minus its previous outside value. That reversal keeps the correct side of the boundary attached to the counter.

Fetch the current tick from slot0() alongside the growth values. The core state interface warns that the recorded tick can differ from a tick reconstructed from the square-root price at a boundary. Replacing it with a rounded price calculation can select the wrong branch.

A snapshot with deliberately simple units

Consider a hypothetical NFT with bounds −120 and 120, current tick 0 and unchanged L of 1,024,000,000. Token A is token0; token B is token1. Both have six decimals. Assume no released principal remains in its owed balances. Those opening balances are entirely fees: 8 A and 12 B.

Every growth coefficient in the following tables means that number multiplied by 2^128 / 1024. These are synthetic accounting inputs, not observations from a live pool. The chosen scale divides exactly into the contract’s fixed-point unit.

Growth recordA coefficientB coefficient
Global G1,000900
Lower outside Oₗ100120
Upper outside Oᵤ150300
NFT’s last inside checkpoint700400
Current inside growth750480
Growth since checkpoint5080

At tick 0, A’s inside coefficient is 1,000 − 100 − 150 = 750. B’s is 900 − 120 − 300 = 480.

For A, the newly accrued raw amount is 1,024,000,000 × 50 / 1024 = 50,000,000. With six decimals, that is 50 A. B similarly earns 80 B. Adding the opening stored balances gives a current claim of 58 A and 92 B.

A read of the unchanged NFT record can still show 8 A and 12 B owed. The additional 50 A and 80 B sit in the difference between current range growth and the saved checkpoints. Uniswap Labs’ fee-math primer identifies the pool and NFT inputs needed for this reconstruction.

Collect part, leave the range, then return

Suppose the owner submits a manager collection capped at 40 A and 60 B, with no intervening growth. For a position with positive L, the v3 manager’s collection code first refreshes core fee accounting with a pool burn call for zero liquidity. It then credits the NFT’s growth delta, updates its checkpoints and requests the capped amounts.

In this exact-value example, the recipient receives 40 A and 60 B. The remaining owed balances become 18 A and 32 B; checkpoints advance to coefficients 750 and 480. Collection has moved tokens to the recipient without changing L.

Follow the upper-tick crossing

Next the pool crosses the upper bound. At global coefficients 1,000 and 900, the upper outside coefficients flip from 150/300 to 850/600. Assume no additional growth between the snapshot and that crossing.

Other liquidity remains active above this NFT’s range. While the current tick is 180, global coefficients rise to 1,100 and 1,000. Lower outside stays 100/120 and upper outside stays 850/600. For A, above-range growth is 1,100 − 850 = 250, leaving inside growth 1,100 − 100 − 250 = 750. For B, inside remains 480.

Global growth increased, but this position gained no new fee claim during that outside interval. Its owed 18 A and 32 B remain intact. Leaving the range does not erase previously earned fees.

When the pool crosses back below the upper tick at those same global values, upper outside flips again, becoming 250/400. Later, at tick 0, globals reach 1,130/1,050. Inside coefficients are now 780/530, giving new deltas of 30/50 since the collection checkpoint. At the stipulated L and decimals, those deltas credit 30 A and 50 B.

Confirmed pointSaved checkpoint A / BStored owed A / BPaid at this step A / B
Before first collection700 / 4008 / 120 / 0
After capped collection750 / 48018 / 3240 / 60
Outside range, unchanged NFT record750 / 48018 / 320 / 0
After return and full collection780 / 5300 / 048 / 82

Cumulative receipts are 88 A and 142 B. Independently, opening stored fees plus the two earning intervals give 8 + 50 + 30 = 88 A and 12 + 80 + 50 = 142 B. Partial collection neither loses the remainder nor restarts the pool’s global counter.

Check the contract layer and the receipt

Core pool collect and NFT manager collect have different preparation steps. The core actions interface says pool collection itself does not recompute earned fees. The manager refresh described above is additional logic. A custom integration that calls core directly must handle its own update sequence.

Also keep the position identity consistent. The core position key combines owner and tick bounds. The standard manager can hold an aggregate core position for several NFTs sharing a pool and range, while each NFT retains its own L and checkpoint. Combining the aggregate liquidity with one NFT’s historical checkpoint produces an invalid claim. The LP tokens versus position NFTs guide explains the surrounding ownership distinction.

After a liquidity decrease, owed balances can contain released principal. Preserve decrease records so a subsequent collection can be separated into principal and fees. Calling its whole receipt “fee income” corrupts the LP fees versus net return reconciliation.

Real amounts also round. The manager interface cautions that its collection event can differ slightly from transferred amounts. Our chosen coefficients avoid that residual; a live ledger should compare core collection, token transfers and the recipient’s resulting balance at the correct transaction boundary.

A worksheet for a current fee estimate

Save at the chosen stateCheck before accepting the result
Chain, pool, manager and NFT identifierAll records describe the intended implementation and position
Block number and hashPool, ticks and NFT inputs refer to the same state
Token order and decimalsRaw A and B amounts use their own conversions
NFT L, bounds and checkpointsIndividual NFT inputs are used throughout
Global and initialized boundary countersTick branches reproduce current inside growth
Stored owed balances and decrease historyReleased principal is identified separately
Collection caps, recipient and confirmed transfersExpected payout reconciles with actual delivery

Use arbitrary-precision integers and the contract’s unsigned 256-bit subtraction semantics for every growth subtraction. The state interface explicitly allows global growth counters to overflow; replacing a wrapped difference with zero can discard valid growth. Apply integer flooring before converting raw amounts to display units.

Keep the block reference on every read. Mixing a pre-crossing outside counter with a post-crossing global value can manufacture fees that never belonged to the position. For a fully withdrawn NFT, do not derive new fees from cleared boundary tick records. Reconcile its stored claim and prior decreases instead; current L cannot reconstruct what it earned while funded.

For the next reconciliation, save three separate lines per token: stored owed fees, growth accrued since the checkpoint, and confirmed collection receipts. Keep released principal on its own line. That makes a stale-looking balance, a partial payout and an out-of-range interval explainable from the same ledger.

Keep the context with the example.

Each worked example is illustrative, with its sources and assumptions in the article. Read our editorial standards for authorship, verification and corrections.