Skip to main content

07 Position Fee Settlement

Overview

In the previous chapter, we derived:

finside=fgfb(il)fa(iu)f_{\text{inside}} = f_g - f_b(i_l) - f_a(i_u)

This gives us a unified expression for the accumulated fees within an interval. However, one key question remains: how are LP gains recorded and settled in the contract?

1. Core question: Why can’t fees be allocated in real time?

In V2:

  • All liquidity is global
  • Fees go directly into the pool reserves
  • LPs earn fees through their share of the pool

But in V3:

  • Liquidity is split across price intervals
  • Different LPs participate in swaps at different times
  • Fees cannot be allocated on a tranche-by-tranche basis

If every LP had to be updated for every swap, gas usage would explode.

2. The core idea of V3: Lazy Settlement

V3 does not allocate fees to LPs for each swap. Instead, it accumulates them first and settles them later.

Each position will be recorded:


feeGrowthInside0LastX128
feeGrowthInside1LastX128

tokensOwed0
tokensOwed1

When an LP creates or updates a position, it stores:

fentry=finside at entryf_{\text{entry}} = f_{\text{inside}} \ \text{at entry}

Stored as:

flastf_{\text{last}}

Current moment:

fnow=feeGrowth inside the current rangef_{\text{now}} = \text{feeGrowth inside the current range}

Revenue calculation:

Δf=fnowflast\Delta f = f_{\text{now}} - f_{\text{last}}

From feeGrowth to real income

fg=fiLif_g = \sum \frac{f_i}{L_i}

It represents the return per unit of liquidity. Therefore, the actual LP return is:

tokensOwed=L(fnowflast)\text{tokensOwed} = L \cdot (f_{\text{now}} - f_{\text{last}})

Where:

  • LL = LP liquidity
  • fnowf_{\text{now}} = accumulated fee within the current range
  • flastf_{\text{last}} = snapshot from the last settlement

Core formula:

tokensOwed+=L(finside, nowfinside, last)\text{tokensOwed} += L \cdot \left( f_{\text{inside, now}} - f_{\text{inside, last}} \right)

3. When is settlement triggered?

V3 does not automatically send funds. Settlement is triggered only when the following operations are performed:

1. mint (open position)
  • Initialize the position
  • Record the initial snapshot:
flast=finsidef_{\text{last}} = f_{\text{inside}}
2. increaseLiquidity (increase position)

Fees must be settled before adding liquidity:

tokensOwed+=L(finside, nowfinside, last)\text{tokensOwed} += L \cdot \left( f_{\text{inside, now}} - f_{\text{inside, last}} \right)

Then update:

flast=finsidef_{\text{last}} = f_{\text{inside}}

Then update f_last = f_inside. Newly added liquidity should not receive historical returns.

3. decreaseLiquidity (reduce position)

Same idea:

  • Settle the old income first
  • Then reduce liquidity
4. collect (receive proceeds)

collect does not calculate returns. It only does one thing: transfer(tokensOwed) and then set tokensOwed = 0.

Example:

Assumptions:

LP provides liquidity: S = 100 Initial:

flast=10f_{\text{last}} = 10

Unit: token / liquidity

After some time:

fnow=15f_{\text{now}} = 15

Then:

Δf=5\Delta f = 5

means that each unit of liquidity earns 5 more tokens, so the total income is:

tokensOwed=1005=500\text{tokensOwed} = 100 \cdot 5 = 500

Note: In the real Uniswap V3 contract, feeGrowthInsideX128 is actually:

feeGrowthInsideX128=f2128\text{feeGrowthInsideX128} = f \cdot 2^{128}

LP income calculation, using the real on-chain formula:

tokensOwed=Lfinside, nowfinside, last2128\text{tokensOwed} = L \cdot \frac{f_{\text{inside, now}} - f_{\text{inside, last}}}{2^{128}}

Note: f here represents the cumulative income per unit of liquidity (fee / liquidity), not the actual number of tokens.