Uniswap v4 dynamic LP fees let a pool's hook change the liquidity-provider rate. The hook can update the stored fee or return a permitted override for one swap. An override alone does not replace the stored fee. To explain a swap's cost, identify the LP rate actually selected, combine it with the protocol rate for that direction, and reconcile the resulting token amounts. Separate hook charges and network costs require their own accounting.
Consider a hypothetical pool storing a 0.30% LP fee while applying 1.00% to one swap. Reading the stored value afterward can still show 0.30%. Follow the call to distinguish its selected rate from the default.
Read the rate separately from the dynamic marker
The mechanics below follow Uniswap v4-core's tagged v4.0.0 source, pinned to commit e50237c. Check the actual deployment before applying these rules to a fork.
Uniswap's dynamic-fee documentation, checked on 28 October 2026, distinguishes updates through PoolManager from per-swap hook overrides. Dynamic capability is fixed when the pool is created. The protocol supplies the mechanism; the particular hook supplies its pricing rule. A dynamic pool does not automatically follow a standard volatility formula.
Keep these records apart:
| Record | Meaning | What it cannot establish alone |
|---|---|---|
| Pool key's fee field | Static rate or dynamic marker | Current numerical LP rate for a dynamic pool |
| Stored LP fee | Default numerical rate in pool state | Whether a swap selected an override |
| Hook's flagged fee return | Candidate rate for this swap | A permanent change to the stored rate |
| Directional protocol fee | Separate core protocol rate | Hook charges, gas or LP net return |
In the pinned LP fee library, numerical rates use millionths: 3,000 means 0.30%, and 10,000 means 1.00%. One unit is 0.0001 percentage points, or one hundredth of a basis point.
The pool key value 0x800000 is the dynamic marker. It is not an exceptionally expensive numerical fee. The same library initializes a dynamic pool's stored LP rate to zero; a hook that wants another starting rate must arrange an update. A zero initial stored rate also says nothing about separate protocol or hook charges.
A stored update and an override have different lifetimes
PoolManager's dynamic-fee setter accepts an update only for a dynamic-fee pool and only from its configured hook address. A trader cannot call it directly to choose a cheaper rate. Who can make that hook request an update depends on the hook's own code and controls.
For a per-swap override, the hook needs its beforeSwap callback enabled. The pinned hook dispatch code reads the returned fee only for dynamic pools. The numerical rate must carry the separate 0x400000 override flag. Returning an unflagged 10,000 does not select a 1.00% override; the stored fee remains the fallback.
A flagged zero can deliberately select a zero LP rate. A flagged rate above 1,000,000, or 100%, reverts during validation. The permission to return a balance delta is separate from permission to run beforeSwap; changing the LP rate does not inherently require a custom balance charge. The core skips that callback when the hook is the direct caller of PoolManager's swap function. If a separate router calls PoolManager, the equality check uses that router's address.
Consider this hypothetical sequence, using ordinary external swap calls and a hook that returns no custom deltas:
| Event | LP rate used by the swap | Stored LP rate afterward |
|---|---|---|
| Hook sets the default to 3,000 | No swap | 3,000 |
| Swap receives a flagged 10,000 override | 10,000 | 3,000 |
| Next swap receives no override | 3,000 | 3,000 |
| Hook explicitly updates the default to 1,500 | No swap | 1,500 |
| Next swap receives no override | 1,500 | 1,500 |
The pool swap implementation selects either the validated override or the stored rate. The override is not a write to that stored field. A hook can also call the setter during its execution, so inspect both actions when reconstructing a real transaction.
Reconcile two hypothetical swaps from identical starting state
Suppose a constructed pool exchanges token A, its currency0, for token B, its currency1. Both tokens have six decimals. Compare two separate exact-input scenarios, each starting at 1 B per A with raw liquidity of 1,000,000,000,000. Liquidity spans ticks −600 to 600, with tick spacing 1.
Each scenario supplies exactly 10,000 A, including core swap fees. Assume the entire input is consumed, no initialized liquidity boundary is crossed, the price limit is not reached, transfers behave normally, and hook deltas are zero. Exclude router charges and gas. These are hypothetical inputs, not a live pool quote or fee policy.
Set the directional protocol rate to 1,000 millionths, or 0.10%. The pinned protocol fee library caps that rate at 1,000 for this implementation and keeps separate rates for the two swap directions. No current activation or rate is assumed here.
The fee composition follows the protocol charge first and the LP rate on the remaining input. Let p be the directional protocol rate and l the selected LP rate, both in millionths. Add p and l, then subtract floor(p × l / 1,000,000). This produces the integer combined rate; simply adding the percentages overstates it.
For the stored LP rate of 3,000, the combined rate is 3,997. For the override of 10,000, it is 10,990. In these two cases the multiplication divides exactly, so the rate calculation introduces no rounding difference.
| Amount or rate | Stored-rate scenario | Override scenario |
|---|---|---|
| Selected LP rate | 0.30% | 1.00% |
| Combined core rate | 0.3997% | 1.0990% |
| Gross A input | 10,000.000000 | 10,000.000000 |
| A entering the trading curve | 9,960.030000 | 9,890.100000 |
| Total core fee in A | 39.970000 | 109.900000 |
| Protocol allocation in A | 10.000000 | 10.000000 |
| LP allocation in A | 29.970000 | 99.900000 |
| B output after integer rounding | 9,861.806115 | 9,793.243839 |
| Stored LP rate after swap | 0.30% | 0.30% |
The input check is exact in each column; the output is measured separately in B. In the first, 9,960.03 A enters the curve, 10 A goes to protocol accounting and 29.97 A goes to LP fee accounting. Those amounts sum to 10,000 A. The second column sums to the same input with a larger LP allocation.
Protocol fees are set through a separate controller-authorized path. An LP fee override does not itself change that directional protocol rate.
Verify the output as well as the fee split
At this illustration's starting price, the liquidity parameter implies virtual balances of 1,000,000 A and 1,000,000 B. These virtual balances parameterize the curve; actual deposited balances and pool TVL require separate measurements. Within the unchanged liquidity range, input a produces continuous output of 1,000,000 × a / (1,000,000 + a) B.
Putting 9,960.03 A into that expression gives approximately 9,861.806115238 B. Putting 9,890.10 A in gives approximately 9,793.243839107 B. This checks the scale of both outputs independently of the fee ledger. The concentrated-liquidity execution guide explains why crossing a liquidity boundary would require another segment of the calculation.
For the table, the exact-input calculation follows the pinned SwapMath branch: remove the combined fee from available input, compute the ending square-root price and obtain B output. The square-root price library rounds that price upward for this direction and rounds output downward. Integer recomputation gives the six-decimal quantities shown above.
The override reduces B output by 68.562276 B, while increasing the LP allocation by 69.93 A. Those are different token units, and the curved exchange rate also affects the comparison. Treating the extra fee as an equal-sized output deduction would miss the swap's price movement.
When a real swap spans several steps, preserve the contract's integer operations and protocol allocation at each step. A single multiplication against a dashboard's rounded rate can miss the resulting units.
A valid rate can still prevent the requested swap. At a combined rate of 100%, the pinned pool code rejects exact-output swaps, because no input remains to obtain output. Check this boundary explicitly when a hook can select extreme rates.
Read the selected rate in the swap event
A successful core Swap event reports the combined swap rate selected by the PoolManager event path. Calling that number the LP-only rate would mislabel it. The event is emitted before the afterSwap hook, so it does not by itself settle every possible hook-adjusted cost.
An end-of-block state read can follow several earlier swaps and fee decisions. In the hypothetical override scenario, a stored reading of 3,000 and an event rate of 10,990 are consistent: one is the default LP rate and the other combines the selected override with the protocol rate.
Asset settlement adds another distinction. The native ETH versus WETH guide explains which asset is delivered and how native value is handled; that identity does not choose the LP rate. Likewise, a flash swap or flash loan can impose separate repayment and fee rules. Do not put a lender's premium into the LP field merely because the operations share a transaction.
The older Uniswap v2 kLast calculation measures reserve-product growth for protocol LP-token minting. That checkpoint is a different accounting mechanism from selecting a v4 swap rate.
For LPs, this table's allocation is fee income from the stipulated swap. It does not establish a position's return or justify projecting unchanged volume at the higher rate. The pool fee APR versus net return reconciliation accounts for inventory, costs and the holding comparison.
Build a record that explains one completed swap
Use this worksheet when a quote, stored rate and receipt disagree:
| Save | Check |
|---|---|
| Chain, PoolManager, full pool key and implementation | Same pool and code throughout |
| Block hash and transaction position | State corresponds to immediately before the relevant call |
| Stored LP rate and hook return | Distinguish fallback, flagged override and explicit stored updates |
| Swap direction and protocol rate | Decode the applicable direction |
| Sender, hook data and hook logic | Reproduce the actual fee-selection branch |
| Price, liquidity, ticks and limit | Replay each consumed segment and its rounding |
| Core event and final token deltas | Separate combined core fees from custom hook or router charges |
A simulation using another sender or different hook data may exercise a different pricing branch. A later execution can also encounter changed state. Save the exact simulated call and its block, then compare the completed transaction against it.
Start the reconciliation with three numbers: the stored LP rate, the selected LP rate and the combined core rate. Finish by explaining the actual input allocation and output. A fee label is useful only when that record shows which rate the swap used and where the tokens went.
