The exact Uniswap v3 impermanent-loss math the protocol settles against: hold value vs LP value across the three price regimes, the guarded realized-IL subtraction (I3), entry-snapshot semantics, and the Q64.96 fixed-point implementation in the production ILMath contract.
This is the settle-path math — the exact quantity the protocol computes at expiry to decide what the LP is owed. Everything else in the pricing stack (MaxIL, fairRate, the load stack) is layered on top of this formula, and the closed-form Φ-sum pricer is verified back against it so it can never drift from the number that actually settles.Impermanent loss is the gap between two valuations of the same entry basket: what you would have had if you simply held the tokens, versus what your LP position is actually worth at the settlement price. Inflexion settles the capped version of that gap.
The one-line definition. Let P be the price of token0 in token1 (e.g. ETH measured in USDC). For a position of liquidity L over a tick range [Pa,Pb], snapshotted at entry price P0:realized_IL=max(0,Vhold(PT)−Vlp(PT))where PT is the price pinned at expiry. The payout is then min(realized_IL,MaxIL) — the cap is what makes the product trustless and fully collateralized.
The production IL kernel is the Solidity ILMath contract, deployed on Arbitrum Sepolia — see the deployments page.
The entry price — the price of token0 measured in token1 at the instant the swap is created. Pinned to Chainlink on-chain via oracle.getPrice, never caller-supplied.
The lower and upper bounds of the position’s tick range, with Pa≤Pb. Derived on-chain from the NFT’s tickLower/tickUpper via TickMath.getSqrtRatioAtTick — the LP cannot lie about the range.
The position’s liquidity. Read once at creation and stored in the SwapRecord. Settlement re-reads this stored value, never the live NFT (invariant I6).
The settlement price, pinned on-chain to the Chainlink round active at expiry T via getSettlementPrice. Also never caller-supplied — round-at-T pinning kills the settle-timing game (see oracles).
The numeraire is token1 — on the live deploy that is dUSDC (6 decimals). Every value below is denominated in token1 wei.
The in-range requirement is enforced at creation: Pa≤P0≤Pb.
Limit: an out-of-range entry is rejected — on-chain the in-range gate reverts PositionOutOfRange (equivalently the Stylus gate a ≥ 1e18 || b ≤ 1e18 reverts OutOfRange).
Why: beyond the range the position has fully rotated into one asset and the loss it bears is directional (foregone spot upside), not the impermanent loss this product hedges — and that tail is unbounded (see the three regimes below), so it could not be fully collateralized.
Mitigation: by construction every covered swap starts in range, where IL is bounded and the cap at MaxIL is load-bearing. This is precisely why Inflexion is an in-range convexity hedge, not “IL insurance.”
With P0 in range, a v3 position holds a specific mix of both tokens. The standard v3 amounts at entry are:amount0_entry=L(P01−Pb1)amount1_entry=L(P0−Pa)amount0_entry is the token0 leg (e.g. ETH); amount1_entry is the token1 leg (e.g. USDC). These two numbers are the entry basket — everything downstream is a comparison against holding exactly this basket. On-chain they are snapshotted by SwapMath.entryAmounts and stored in the SwapRecord.
Vhold is the value of simply holding the entry basket to expiry — the benchmark the LP is measured against. It is a straight line in PT because the basket is fixed and only token0 is re-priced:Vhold(PT)=amount0_entry⋅PT+amount1_entryThis is what the LP would have had if they had never provided liquidity. It is the line every regime of the LP value is compared against.
The actual position value at settlement, Vlp(PT), is piecewise in the settlement price, split by the range bounds. As price moves, a v3 position rebalances — selling the appreciating asset and buying the depreciating one — and that rebalancing is exactly what creates the loss. The three arms:
In range — Pa ≤ P_T ≤ Pb
Below Pa — P_T < Pa
Above Pb — P_T > Pb
Vlp(PT)=L(2PT−PbPT−Pa)Strictly concave in PT. The position holds a price-dependent mix of both tokens; the concavity is the LP’s short-gamma exposure. The gap from the affine hold line opens up here — this is where impermanent loss lives. On-chain: Vlp=amount0(PT)⋅PT+amount1(PT).
Vlp(PT)=L(Pa1−Pb1)⋅PTLinear in PT. Price has fallen out the bottom; the position has rotated entirely into token0. Value now scales linearly with price, just like holding pure token0. On-chain: Vlp=amount0(Pa)⋅PT.
Vlp(PT)=L(Pb−Pa)Constant in PT. Price has run out the top; the position has rotated entirely into token1 (the numeraire). Value is now flat — completely insensitive to further price moves. On-chain: Vlp=amount1(Pb).
The intuition end to end: inside the range the position holds both tokens (concave value, the LP is short gamma); belowPa it has rotated fully into token0 so value scales linearly with price; abovePb it has rotated fully into token1 so value is constant — and that flat arm against an ever-rising hold line is what makes the beyond-range tail unbounded.
Subtract the (piecewise concave/linear/flat) LP value from the (affine) hold value, and floor at zero:realized_IL=max(0,Vhold(PT)−Vlp(PT))[token1 wei]The on-chain form is written as a guarded subtraction, not an unchecked one:
// invariant I3: guarded, never an unchecked underflowrealisedIL = vHold > vLp ? vHold - vLp : 0;
Two of the protocol’s invariants are encoded right here:
I3 — non-negativity / no underflow
The subtraction is guarded: vHold > vLp ? vHold - vLp : 0. It is never an unchecked subtraction, so it can never underflow into a wrapped, enormous “loss.”
I4 — LP never profits from the swap
If Vlp≥Vhold then realized IL is 0 and so is the payout. The product is a hedge, not a lottery — the LP cannot extract a positive payment from the swap itself.
At PT=P0, IL is exactly zero. No drift ⇒ no loss. Vhold(P0)=Vlp(P0) by construction, so realized IL is 0. The on-chain test il_in_range_at_entry_is_zero asserts this holds within ≤100 wei. Because the swap is snapshotted at P0, the protocol only ever owes the LP for drift that happens after creation.
MaxIL is the maximum value realized_IL can take while price stays in range — pure geometry, max(IL(Pa),IL(Pb)), derived in the MaxIL derivation. Because collateral is locked exactly equal to MaxIL and payout ≤ MaxIL by construction of the cap, FULL mode cannot produce bad debt under any price path. See the no-bad-debt math.
LPs are always paid with no bad debt in FULL mode — qualified: capped payoff + solvent USDC + oracle/settlement liveness.The no-bad-debt guarantee is exact only under the full clause:
FULL collateralization — the underwriter has posted exactly MaxIL per swap, locked for the life of the position.
Capped payoff — the payout is min(realized_IL, MaxIL). The cap is load-bearing: it is what makes payout ≤ collateral hold by construction.
Solvent USDC — the collateral numeraire (dUSDC) holds its peg and remains redeemable.
Oracle / settlement liveness — the price oracle stays live and settle() runs within the protocol’s settlement window.
No rehypothecation breach — locked collateral stays instantly liquid; it is never routed to utilization-gated venues.
Stated without these qualifiers, the guarantee is not correct.
Everything is snapshotted at swap creation, not at the LP's original mint
P0 is the oracle price at createSwap, and the entry token amounts are the position’s current amounts at that instant — not whatever they were when the LP first minted the NFT.
2
Coverage runs from creation onward
The swap covers IL accruing from creation onward. Any IL the LP already bore between their original mint and the moment they buy coverage stays the LP’s own — the protocol does not retroactively pay for drift that already happened.
3
L is read once and stored; settlement re-reads the stored value
Liquidity L is captured at creation and written into the SwapRecord. Settlement uses that storedL, never the live NFT — this is invariant I6.
Why I6 matters in practice. Anyone can call increaseLiquidity on a v3 NFT — it is not owner-gated, and the NFT is in protocol custody while the swap is active. If settlement re-read L from the NFT, a third party could inflate the payout above MaxIL by adding liquidity. Because settlement uses the storedL, the extra liquidity is simply returned with the NFT at expiry and cannot inflate the payout. See the invariants.
The float formulas above are the Monte-Carlo reference. On-chain, everything is integer math in Uniswap’s Q64.96 fixed-point format.
The encoding — sqrtPriceX96 and Q96
Prices are carried as square roots in Q64.96:sqrtPriceX96=⌊P⋅296⌋Q96=296Working in P matches Uniswap’s own tick math (TickMath.getSqrtRatioAtTick) so Pa, Pb, P0, and PT all arrive in the same representation the AMM uses — no lossy round-trip through a decimal price.
The arithmetic — mulDiv with a 512-bit intermediate
Every product is taken throughmulDiv(a,b,d)=⌊da⋅b⌋with a 512-bit intermediate, because L⋅P can reach 2288 and would overflow a 256-bit multiply. Square roots use integer_sqrt via Newton’s method. Floor rounding everywhere makes the result fully deterministic and reproducible across implementations.
Why floor rounding and integer math, specifically
Floored integer math is what lets two independent implementations agree to the wei by construction — there is no floating-point nondeterminism to reconcile. The Solidity ILMath and the (benchmark-only) Stylus ILMath port share the same mulDiv chain, so they produce bit-identical results. It also means the on-node integer MaxIL sits a handful of wei from the Python float anchor (1,691 wei on the canonical fixture, well inside the 10,000-wei float-vs-integer tolerance — see the worked fixture below).
For the IL kernel, Solidity ILMath is production — the Stylus ILMath port is a rejected benchmark artifact.
Limit: the headline “Stylus ~10× cheaper than Solidity” intuition was inverted for this specific kernel — the Stylus ILMath measured ~5.3× more expensive cached.
Why:computeMaxIL is too little compute to amortize Stylus’s fixed per-call overhead — there simply is not enough arithmetic to pay back the fixed cost.
Mitigation: ship the Solidity ILMath as the production IL/MaxIL contract; keep the Stylus port only as a benchmark. This is separate from the Stylus FairValueOracle, which IS production — there the Φ-sum has enough compute (and a hard floating-point ban) to make Stylus the right home.
The two ILMath implementations are not just “close” — they are exact to the wei by construction because they share the mulDiv chain. That is what lets the protocol settle on the cheaper Solidity contract while keeping a high-precision benchmark for verification.
Take the canonical fixture: range [Pa,P0,Pb]=[80,100,125] with L=1018. This is a −20% / +25% range around an entry of 100. From the entry amounts:amount0_entry=L(1001−1251)≈0.01055728amount1_entry=L(100−80)≈1.05572809so Vhold(PT)=0.01055728PT+1.05572809. Now walk PT across the three regimes:
PT
regime
Vhold
Vlp
IL=max(0,Vhold−Vlp)
note
100
in range
2.11146
2.11146
0
at entry: no drift ⇒ no loss (I4)
110
in range
2.21703
2.21646
≈ 0.00057
small concave gap opens
125 (=Pb)
upper edge
2.31537
2.17605
0.13932
the upper-boundary maximum
80 (=Pa)
lower edge
1.90031
1.78885
≈ 0.11146
the lower-boundary value
160
above Pb
2.74489
2.17605
0.56884
Vlp now flat — IL grows without bound
Two things to read off the table:
The maximum in-range IL is at an endpoint, and it is the upper edge.IL(Pb=125)=0.13932>IL(Pa=80)≈0.11146, so MaxIL=IL(Pb)=0.13932. The IL curve is convex on [Pa,Pb], and even for a ratio-symmetric range it leans toward the upper boundary — this is the asymmetry proved in the MaxIL derivation.
Beyond Pb, IL is unbounded. At PT=160 the raw IL is already 0.569 — over 4× MaxIL — and it keeps growing linearly with price because Vlp is flat above Pb. The protocol caps the payout at MaxIL = 0.13932, so even at PT=160 the LP receives only 0.13932 and the rest is uncovered directional loss. This is the cap doing its load-bearing job.
The exact integer values for [80,100,125], L=1018, asserted by amounts_fixture_matches_python / maxil_fixture_matches_python and mirrored in ILMath.t.sol:
Quantity
Value (token1 wei, integer)
≈ (× 1e18)
amount0_entry
10_557_280_900_008_417
0.01055728
amount1_entry
1_055_728_090_000_841_200
1.05572809
V_hold(P0)
2_111_456_180_001_682_900
2.11145618
IL(Pa = 80)
≈ 1.11456e17
0.111456
IL(Pb = 125)
139_320_225_002_101_320
0.13932023
MaxIL
139_320_225_002_101_320
0.13932023
MaxIL=IL(Pb)>IL(Pa) confirms the upper-boundary asymmetry, and MaxIL/Vhold(P0)≈6.6%. The on-node integer-floor MaxIL (139_320_225_002_103_011) sits 1,691 wei from the Python float anchor — inside the 10,000-wei float-vs-integer tolerance; Stylus ≡ Solidity is exact to the wei.
The 2026-06-05 create→settle lifecycle on the fresh Arbitrum Sepolia stack ran this exact settle-path math on both rails:
Path A (cvAMM): V0 = $270,531.28, MaxIL = $1,669.24, realized IL at settle = $148.64, payout = $148.64 ≤ MaxIL — under the cap, paid from the ConvexityVault.
Path B (MM): MaxIL = $3,215.65, realized IL = $245.66, payout = $245.66 ≤ MaxIL — under the cap, paid from UnderwriterVault.
Both payouts came in below their respective MaxIL caps — realized_IL, not the cap, was the binding quantity. The settle-path math (computeIL with the stored L) is identical regardless of which rail opened the swap.Network: Arbitrum Sepolia · chainId421614 · deploy block274081134 (subgraph startBlock) · deployed 2026-06-05
Is realized IL measured from when the LP minted the position, or from when they bought coverage?
From when they bought coverage. Every entry quantity — P0, the token amounts, L — is snapshotted at swap creation, and coverage runs from there onward. IL the LP already bore before buying coverage stays theirs.
What happens if the price ends up below the range instead of above it?
The IL formula handles both edges symmetrically in form: below Pa, Vlp is linear in price (fully token0); above Pb, it is constant (fully token1). The realized IL is max(0,Vhold−Vlp) in either case, capped at MaxIL. Note the magnitudes are not symmetric — the convex IL curve leans toward the upper boundary, so for the same width IL(Pb)>IL(Pa) typically.
Why is the subtraction written as a ternary instead of just vHold − vLp?
To guarantee invariant I3. In Solidity an unchecked subtraction of unsigned integers underflows and wraps to a near-2^256 value when Vlp>Vhold. Writing it as vHold > vLp ? vHold - vLp : 0 makes the floor-at-zero explicit and impossible to skip — which is also exactly invariant I4 (LP never profits from the swap).
Can adding liquidity to the NFT after creation change the realized IL?
No. Settlement uses the liquidity Lstored at creation in the SwapRecord, not the live NFT (invariant I6). Anyone can increaseLiquidity on the custodied NFT, but the extra liquidity is returned with the NFT at expiry and cannot inflate the payout above MaxIL.
Why fixed-point Q64.96 instead of just using decimals?
Determinism. Floored integer math in Q64.96 lets independent implementations (Solidity vs the Stylus benchmark) agree to the wei by construction, with no floating-point nondeterminism to reconcile. It also matches Uniswap’s own sqrtPriceX96 representation, so the range bounds arrive in the exact format the AMM uses.