06 Fees and Fund Settlement
Overview
In the previous chapter, we learned that LPs participate in swaps by providing liquidity. In this chapter, we explain a core question in V3: how are LP returns calculated? In Uniswap, LP income mainly comes from swap fees, but the mechanism is very different between V2 and V3.
In V2, all liquidity is globally shared, there is no price range, and all LP funds participate in every transaction. Therefore, in each swap, the trader pays a fee (such as 0.3%), and those fees are added directly to the pool's reserves. LPs do not receive fees directly, but instead own part of the pool's assets through their LP token share.
Its essence is:
LP income ∝ LP holding shares × accumulated fees of the pool
Therefore, in V2, there is no need to calculate fees transaction by transaction, distinguish between intervals, or track historical state.
However, the problem becomes more complicated in V3. Liquidity is distributed across different price ranges, and only active liquidity participates in swaps. Because liquidity differs across ranges, different LPs participate in transactions to varying degrees at different times and in different intervals.
1. Single swap
Let’s not look at the contract first. Let’s start with the theoretical calculation.

- Yellow column: total liquidity (L) in different tick intervals
- Pink horizontal bar: liquidity (S) provided by Alice
- Dashed line position: price is moving (swap process)
- f₀, f₁: fees generated in different ranges
Assumption: the user exchanges token Y for token X, so the price moves to the right.
Step 1: Transaction within the first interval (L₀)
The price first moves within the range L₀
→ Generate fee f₀
→ All LPs in this range participate in the distribution
Step 2: Price enters the next range (L₁)
Price continues to move to the next range L₁
→ There is also a fee f₁
→ LP in this range re-participates in distribution
How much fee can Alice get?
In the interval :
Alice share = S / L0
Fee received = f0 × (S / L0)
In the interval :
Alice share = S / L1
Fee received = f1 × (S / L1)
Total revenue:
Therefore, each price range is an independent fee allocation pool. In each interval, LPs share the fees generated in that interval in proportion to their liquidity relative to the total liquidity in that interval.
2. Multiple intervals (introduction time)
However, V3 is not a simple single-swap system. Swaps can span multiple intervals, occur at different times, and generate fee allocations in different intervals.

- Yellow column: total liquidity at a given time within a given tick interval
- Pink horizontal bar
S: liquidity provided by Alice - Purple mark
L_{i,t}: total liquidity in thetth moment andith interval - Orange mark
f_{i,t}: fees generated at timetin intervali
Here we record the "interval" as i and the "time" as t.
Case 1: t = 0
At the first moment, the price moves to the right and crosses two ranges.
The corresponding figure is:
- In the interval
i = 0, the feef_{0,0}was generated - In the interval
i = 1, the feef_{1,0}was generated
Alice provides liquidity S in both ranges, so she can share the fees in these two ranges respectively.
In the interval i = 0, Alice's proportion is: , so the fee she gets in this interval is:
Similarly, in the interval i = 1, she gets:
Therefore, the total revenue corresponding to the first graph is:
Case 2: t = 1
At the second moment, the price moves to the left.
At this moment, the price moves to the left, corresponding to the swap direction X → Y. Since we are currently only concerned with fees in token Y:
- There will be no Y fee in this direction
- Therefore, there is no contribution to
feeGrowth(Y)
Therefore:
Therefore, Alice does not earn distributable fees at every moment or in every interval.
Case 3: t = 2
At the third moment, the price moves to the right again and crosses more ranges.
The corresponding figure is:
- In the interval
i = -2, the feef_{-2,3}was generated - In the interval
i = -1, the feef_{-1,3}was generated - In the interval
i = 0, the feef_{0,3}was generated
The current total liquidity in these intervals is:
L_{-2,3}L_{-1,3}L_{0,3}
Therefore, the total fee Alice receives at this moment is:
The key point is this: LP fees are not uniformly distributed across the entire pool. They are calculated separately in each interval based on liquidity proportion. If we ignore time for the moment and look only at the case where a single price movement spans multiple intervals, then Alice's total income can be written as:
f_i: fee generated in stepiL_i: total liquidity in intervaliS: Alice’s liquidity
What this formula means:
For each range that the price passes through, Alice will share the fees generated in that range according to her proportion of liquidity in that range.
If we factor out S, it can also be written as:
This is already close to the idea behind feeGrowth.
We added the time dimension earlier, because in reality, swap does not happen only once. Prices will continue to change at different times, and liquidity in different intervals may also change. Therefore, fee distribution is not only "across intervals", but also "across time."
So at the tth moment, Alice’s income can be written as:
This represents the sum of Alice’s fee income in all relevant intervals at time t.
If we add up all the time periods, Alice's total income from the starting moment to the final moment is:
Combining the above two formulas, we can get the final general form:
Therefore, LP income in V3 is a double weighted summation across time and across intervals. At each point in time and in each price range, the fee is allocated according to the LP's liquidity proportion. So the question is: Why can’t it be calculated directly on-chain according to this formula?
Because doing so means that we must traverse: all time points, all intervals in which swap has occurred, and liquidity changes corresponding to all intervals. This is obviously not feasible on-chain because the gas cost would explode.
Therefore, this formula is "theoretically the most intuitive definition of fees", but it is not a direct implementation method in the contract. It is precisely because of this that V3 must design a set of compressed accounting methods to compress this complex sum into the feeGrowth mechanism to be discussed later.
3. From tick-by-tick calculation to feeGrowth
The above formula gives a theoretical definition of LP returns, but it is not feasible to calculate these values on-chain on a per-transaction basis. Therefore, we need a method: not to record every transaction, but to record the "accumulated unit liquidity return", which is the core design of V3: feeGrowth.
We define feeGrowth as "the cumulative return per unit of liquidity":
It can also be expanded and written as:
In other words, feeGrowth represents "the fees earned so far per unit of liquidity." Note: What is recorded here is not the total fee, but fee per unit of liquidity.
So how did feeGrowth change? Take the example in the picture below:

Yellow bars represent total liquidity in different tick intervals
L₂,L₃,L₄represent the effective liquidity in the corresponding range- The green polyline represents the process of accumulation of fees when the price crosses multiple intervals during the swap process.
f₀, f₁, f₂, f₃, f₄represents the fee fragments generated in different steps/different intervals
The most important thing here is to understand that f0, f1, f2, f3, f4 are not repeated fees in the same range, but incremental fees collected by swaps in different steps.
f0
The price has just started to move, and a transaction occurs in the first step. A fee of f0 is charged.
The corresponding total liquidity at this time is L0, so its contribution to fee growth is:
f1
The price continues to move along the next short path, and another transaction occurs. A fee of f1 is charged.
At this time, the effective liquidity of the corresponding interval is L1, so the contribution is:
f2
The price further enters the liquidity range marked L₂ in the figure, and a transaction occurs within this range. A fee of f2 is charged.
Therefore the contribution of this step is:
f3
The price continues to advance to the next step, and the total liquidity in the corresponding range is L₃. A fee of f3 is charged.
The contribution is therefore:
f4
Finally, the price enters the liquidity range corresponding to L₄, another transaction occurs, and the fee f4 is charged.
The contribution is therefore:
Because feeGrowth is cumulative, it does not record the total fee from a single swap. Instead, it records the cumulative sum of the contributions of all steps to the "income per unit of liquidity" so far.
So the green polyline in the picture actually expresses:
- Add
f0 / L0first - plus
f1 / L1 - plus
f2 / L2 - plus
f3 / L3 - plus
f4 / L4
Every time a new step occurs, feeGrowth is incremented.
If this example is generalized, then in any swap process, feeGrowth can be written as:
f_i: The fee charged in theistepL_i: The effective liquidity in the corresponding interval of theistep
Combined with the above figure, we can use "rules" to summarize the changes in fee growth during the swap process:
Rule 1: When fee collection occurs, fee growth increases
During the swap process:
- In each step, if a transaction occurs and a fee of is charged
- And the liquidity in the current range is
Then fee growth will increase:
Correspondence in the figure:
- In the tick 3 interval: is generated -> fee growth increases by
- In the tick 4 interval: is generated -> fee growth increases by
- In the tick 2 interval: is generated -> fee growth increases by
- In the tick 3 interval: is generated -> fee growth increases by
- In the tick 4 interval: is generated -> fee growth increases by
Rule 2: fee growth is cumulative and will not decrease
- fee growth only increases
- It is not reduced when price reverses
Therefore:
- The dotted line (fee growth) in the graph is always upward
- There will be no decline
Rule 3: Swaps in different directions have different impacts on fee growth.
For a certain token (e.g. token Y):
-
when swap is Y → X (input Y)
-
A fee of Y will be charged
-
fee growth increases
-
when swap is X → Y (input X)
-
No fee of Y
-
fee growth remains unchanged
In summary, fee growth only increases when the corresponding token fee is charged, and remains unchanged in other cases.
4. From feeGrowth to feeGrowthInside
In the previous section, we have reached a key conclusion:
And further abstracted:
We can use feeGrowth to represent the accumulated fees per unit of liquidity.
But this is not enough. Although feeGrowth has compressed the complex sum of "across time + across ranges" into a cumulative amount, one key problem remains: an LP should only receive fees within its own price range. feeGrowth is a global accumulation, but an LP only cares about the part inside the [i_lower, i_upper] interval.
If we calculate by traversing all historical steps, this is completely infeasible on the chain (gas explosion). Therefore, the optimization method of V3 is to use "subtraction" instead of "traversal". It will not "find the fee within the range", but use global fee - fee outside the range.
4.1 Three key quantities

Based on the picture, we now derive the fee inside the range as global fee minus fee outside the range. First, we must understand three key variables:
1️⃣ Global fee growth
Indicates the cumulative fee per unit of liquidity across all steps from the beginning of the system to the present
2️⃣ Outside the interval (below / above)
For any tick :
Total below:
Total above:
3️⃣ fee growth within the range
For an LP interval:
definition:
Core formula:
🟥 Red on the left (below)
- means
- That is: fee generated on the left side of lower tick
These fees do not belong to the current LP
🟥 Red on the right (above)
- means
- That is: fee generated on the right side of upper tick
Also not part of the current LP
🟢 Middle green (inside)
overall situation
- I don’t want the one on the left.
- Don’t want the one on the right
The rest is the fee you should really get within the LP range.
formula:
Namely: feeGrowthInside = global cumulative growth - outside-range cumulative growth
This design brings three benefits:
- There is no need to traverse the history
O(N) → O(1), only the status of the current global and two boundary ticks is needed; - Supports any LP range. No matter when LP enters the market, what range is chosen, or how the price moves back and forth, it can be calculated using the same set of formulas;
- There will be no gas explosion on the chain, and you only need to deposit
feeGrowthGlobalandfeeGrowthOutsidefor each tick to complete the settlement of all LP.
5. Fee Growth Below (cumulative below)
Let’s first focus on a fixed tick: . We define: , which represents the cumulative fee growth that occurred below tick in all historical swaps.

As can be seen from the picture:
- The green polyline represents the change of global fee growth over time
- The red area indicates the fee generated below tick
As time progresses:
- When the price is to the left of tick , the new fee will be calculated "below"
- When the price is to the right of tick , the new fee does not count as "below"
Therefore, is essentially the accumulation of all fees that occur below tick . However, there is a core problem here: the price moves back and forth on either side of tick .
This means:
- Some fees are classified as below at one point in time
- But if the price crosses the tick, the attribution of those fees changes
Therefore, is not a quantity that can be simply "linearly accumulated".
If we expand this over time, at different time points:
You can get:
🔹 t₀ (price is to the left of i)
- All fees occur to the left of i, no crossing
- so:
🔹 t₁ (price crosses i from left → right)
- The part that used to be "left" now becomes "right"
- The area below has changed sides.
Therefore, we cannot simply use below = previous cumulative growth to calculate, but must use below = current global fee growth - cumulative growth already attributed to the right side
Right now:
🔹 t₂ (price crosses i from right → left)
- When crossing occurs, below / above are exchanged again
- Current price returns to the left
- The newly generated fee (fg2) belongs below
- Expression returns to "normal accumulation form"
express:
- crossing
- Just add the new paragraph to the expression
🔹 t₃ (price crosses i from left → right)
- Again the below and above exchange occurs
- fb = fg - the accumulated other side fee
Right now:
🔹 t4 (price crosses i again from right -> left)
- crossing occurs again
- below / above swap again
- the expression is flipped again
Right now:
Summarize:
-
One tick per crossing: will become:
-
No crossing: Just continue to accumulate new fee growth (± )
So the overall pattern is:
That is, the sign flips at every tick crossing. In actual implementation, V3 does not calculate this formula directly; it maintains the result through state updates.
Because calculating fee_below on-chain from this definition would require traversing all historical swaps, which is not feasible in practice because gas costs would explode.
To avoid calculating above / below directly, V3 introduces a key variable:
Define:
Here, represents the tick at the current price.
then:
Therefore, V3 does not directly store fee growth below; instead, it stores feeGrowthOutside on each tick, which is easier to update. Later, based on the current price position relative to tick , it recovers and . Next, we continue with .
6. Fee Growth Above
We also focus on a fixed tick: . We define:

As can be seen from the picture:
- The green polyline represents the global fee growth
- The red area indicates the fee generated above tick
As time progresses:
- When the price is to the right of tick , the new fee will be calculated "above"
- When the price is to the left of tick , the new fee does not belong to the above
Therefore, is essentially the accumulation of all fees that occurred above tick .
As with below:
- price crossing tick i
- The ownership of "above/below" will be flipped
Therefore, is not a quantity that can be simply accumulated linearly.
Observe the pattern over time (for intuition only)
Define:
You can get:
🔹 t₀ (price is to the right of i)
- All fees are above i
- Calculate the fee above
🔹 t₁ (price crosses i from right → left)
- exchange occurs above / below
🔹 t₂(price left → right through i)
- The newly generated fee belongs to the above
🔹 t₃ (price crosses i from right → left)
- flipping happens again
🔹 t₄ (price left → right through i)
From these expansions it can be observed:
- Same as below, a sign flip occurs every tick crossing
- When not crossing, just continue to accumulate
These expansions only help explain the flip mechanism. The actual calculation does not use these expressions; it uses feeGrowthOutside to represent the above side.
We have defined:
It represents feeGrowthOutside recorded on the tick.
Define:
Here, represents the tick at the current price.
then:
Therefore, the above is not an independently designed quantity, but is restored by feeGrowthOutside plus the current price position. Below / above are essentially two perspectives of the same thing, and when crossing occurs, the attribution flips. In addition, V3 does not store separate below / above values; it only stores , and recovers them from the current price position as and .
7. Initialization and update of feeGrowthOutside
In the previous section, we saw:
- Neither
feeGrowthBelownorfeeGrowthAboveare simple linear accumulators - They undergo flips as the price crosses ticks
- If maintained directly according to historical definitions, all historical swaps need to be traversed on the chain, and the gas cost is unacceptable.
Therefore, V3 does not store directly:
f_b(i): cumulative fee growth below tickif_a(i): cumulative fee growth above ticki
Instead, for each initialized tick, store a state variable that is easier to maintain:
It represents feeGrowthOutside recorded at tick i. The core function of this variable is to use a state quantity to compress and encode the "flip history" after the price crosses the tick multiple times. Therefore, we do not directly calculate later.
7.1 Initialization rules
When tick is first initialized, we need to decide which fees are currently "outside".
Assume that the current price is at tick , then:
- If (tick is to the left of current price)
- outside = the left side
- so:
- If (tick is to the right of current price)
- outside has not accumulated anything yet
- so:
When price crosses tick i:
- The original outside area becomes inside
- The original inside area becomes outside
The outside definition flips when the price crosses the tick:
Therefore:
This means already implicitly records:
- Fee accumulation on the left/right side of the tick
- and the "flip result" after multiple crossings
Next, we use to recover:
To further calculate:
7.2 Update rules
For an LP interval , the fee within the interval is defined as:
in:
- : global fee growth
- : fee below the lower boundary
- : fee above the upper boundary
Assume that the current price tick is :
Fee below
Fee above
You can see the calculation of fee inside, depending on:
- Current price position
- Position relative to interval
Therefore, it needs to be discussed in three situations:
- The current price is on the left side of the range:
- The current price is within the range:
- The current price is on the right side of the range:
Case 1: (price is on the left side of the range)

By definition:
Substitute:
- Because :
get:
Case 2: (price is within the range)

By definition:
Substitute:
- Because
- Because
get:
Case 3: (price is on the right side of the range)

By definition:
Substitute:
- Because
get:
The three situations are unified as follows:
8. Accumulated fee calculation under uninitialized tick
In the previous section, we have obtained a unified interval fee expression:
Next we consider a special but very important case:
The interval boundaries were not initialized when position was created.
This means that at creation time :
Also define:
The goal is to calculate:
Case 1: (price is always on the left side of the range)

Use the formula at this time:
Initial time
At some point
Assuming that price first crosses at , then:
And remains untouched:
Therefore:
get:
Case 2: (price enters the range)

Use the formula:
Initial time
Since both ticks are uninitialized:
At some point
Assumptions:
- has been crossed (in )
- has not been crossed yet
then:
Therefore:
get:
Case 3: (price crosses the entire range)

Use the formula:
Initial time
At some point
Assumptions:
- is initialized before
- is crossed at
then:
Therefore:
get:
Regardless of the current price's position relative to the range (left/inside/right), in the case where the boundary tick is not initialized initially, there is:
- Before the tick is initialized, the interval boundary does not form a division
- So fee growth will not be assigned to outside
- Fee accumulation in the entire interval is equivalent to changes in global fee growth
9. lower has been initialized, upper has not been initialized
In the previous section, we analyzed that both ticks were not initialized, and now we enter a more critical intermediate state:
has been initialized, has not been initialized
Initial state (when position is created)
Assume position is created at and the current price is
because:
- initialized
- not initialized
Therefore:
in:
definition:
The goal remains:
Case 1: (price is on the left side of the range)

at this time:
Initial time
At some point
When the price crosses at , a flip occurs:
Right now:
and:
Therefore:
Substitute:
Difference
Case 2: (price enters the range)

at this time:
Initial time
At some point
at this time:
- has been passed through (or is already on the left)
- has not been crossed yet
Therefore:
Difference
Case 3: (price crosses upper)

at this time:
Initial time
At some point
Assumptions:
- is first crossed (initialized) at
- flip occurs at the same time:
Therefore:
Substitute:
Difference
This section explains: As long as upper has not been initialized, the "right boundary" of the interval will not truncate "fee".
Therefore:
- fee is still only truncated once by lower
- The overall effect is still equivalent to:
10. upper has been initialized, lower has not been initialized
In the previous section, we analyzed that ** is initialized and is not initialized. Now we consider the completely symmetric case:
has been initialized, has not been initialized
Initial state (when position is created)
Assume position is created at and the current price is
because:
- not initialized
- initialized
Therefore:
in:
definition:
The goal remains:
Case 1: (price is on the left side of the range)

at this time:
Initial time
At some point
Price crosses at (first initialization):
and:
Therefore:
Substitute:
Difference
Case 2: (price is within the range)

at this time:
Initial time
At some point
at this time:
- has not been initialized yet
- already exists
Therefore:
Difference
Case 3: (price is on the right side of the range)

at this time:
Initial time
At some point
The price crosses at , and a flip occurs:
Right now:
Therefore:
Substitute:
Difference
This section forms a completely symmetrical relationship with the previous section:
- lower initialization → truncate left fee
- upper initialization → truncate the right fee
But as long as the other side is uninitialized: the interval still does not form a "complete boundary".
Therefore:
- fee is truncated on one side only
- Overall still equivalent to global fee growth
11. lower and upper are both initialized (complete range)
In the previous sections we have shown that:
- uninitialized on both sides → equivalent to global
- only lower → equivalent to global
- only upper → equivalent to global
Now onto the last case:
and have been initialized
Initial state (when position is created)
Assume position is created at and the current price is
at this time:
in:
definition:
Target:
Case 1: (price is on the left side of the range)

at this time:
Initial time
At some point
The price crosses at , and a flip occurs:
and:
Substitute:
Difference
Case 2: (price is within the range)

at this time:
Initial time
At some point
at this time:
- lower on the left
- upper on the right
Therefore:
Difference
Case 3: (price is on the right side of the range)

at this time:
Initial time
At some point
The price crosses at , and a flip occurs:
Substitute:
Difference