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 tick | Growth below lower bound | Growth above upper bound |
|---|---|---|
| Below lower | G − Oₗ | Oᵤ |
| At or above lower, below upper | Oₗ | Oᵤ |
| At or above upper | Oₗ | 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 record | A coefficient | B coefficient |
|---|---|---|
| Global G | 1,000 | 900 |
| Lower outside Oₗ | 100 | 120 |
| Upper outside Oᵤ | 150 | 300 |
| NFT’s last inside checkpoint | 700 | 400 |
| Current inside growth | 750 | 480 |
| Growth since checkpoint | 50 | 80 |
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 point | Saved checkpoint A / B | Stored owed A / B | Paid at this step A / B |
|---|---|---|---|
| Before first collection | 700 / 400 | 8 / 12 | 0 / 0 |
| After capped collection | 750 / 480 | 18 / 32 | 40 / 60 |
| Outside range, unchanged NFT record | 750 / 480 | 18 / 32 | 0 / 0 |
| After return and full collection | 780 / 530 | 0 / 0 | 48 / 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 state | Check before accepting the result |
|---|---|
| Chain, pool, manager and NFT identifier | All records describe the intended implementation and position |
| Block number and hash | Pool, ticks and NFT inputs refer to the same state |
| Token order and decimals | Raw A and B amounts use their own conversions |
| NFT L, bounds and checkpoints | Individual NFT inputs are used throughout |
| Global and initialized boundary counters | Tick branches reproduce current inside growth |
| Stored owed balances and decrease history | Released principal is identified separately |
| Collection caps, recipient and confirmed transfers | Expected 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.
