P0, the settlement price P_T, the range bounds Pa/Pb — must come from a source the user cannot choose. If any of them were caller-supplied, the protocol would be quoting and settling against a fiction. This page is the story of one specific hole in an early design, why it was a fund-loss vector, and exactly how the deployed code closes it: all four prices are pinned on-chain, derived from Chainlink and from the position’s own NFT, never from calldata.
This is the enforcement side of two invariants. The settlement-price pinning restores I2 and I4; the entry-price pinning feeds the I9 oracle band. It is also the mechanism behind the no-bad-debt guarantee being real rather than nominal — a cap on realized_IL means nothing if the settler gets to pick the price that feeds the IL formula.
The hole — caller-supplied prices
A review of an early build found thatcreateSwap and settle once accepted a caller-supplied sqrt price as a function argument. Two independent abuses fell straight out of that:
Settle-side: pick the payout
settle computed the payout from the caller’s sqrtP_T. Because settlement is permissionless (anyone can settle an expired swap), any settler could set P_T to whatever maximised — or zeroed — the payout, placing it anywhere in [0, MaxIL] regardless of the true price at expiry.Create-side: pick the entry
createSwap let the LP choose the entry sqrt price. Since P0 sets MaxIL, the premium, and V0, an LP could pick an entry that made the position cheap to hedge and rich to settle — choosing its own risk parameters.The fix — derive both prices on-chain from Chainlink
In the deployed contracts, both the entry priceP0 and the settlement price P_T are derived on-chain from the Chainlink feed via a single internal helper, _oracleSqrtPriceX96, which delegates to the SwapMath.oracleSqrtPriceX96 library:
sqrtPriceX96, accounting for token decimals and oracle orientation (covered below). It is the only way a price enters the pricing or settlement math.
- Settlement price P_T
- Entry price P0
At The settler chooses when to call
settle, the settlement price is pinned to the Chainlink round-at-T — the unique round bracketed by updatedAt ≤ expiry — and converted via _oracleSqrtPriceX96. realized_IL is then recomputed on-chain from that pinned price and the stored liquidity:settle, never what price it settles at. The round-at-T is deterministic given the expiry, so two honest settlers reach the same payout. This restores I2 and I4 and is the Fork-1 oracle pinning (the forks). The round-at-T machinery — the lone-spike sanity check and the 24h liveness backstop — lives in OracleManager.getSettlementPrice and is invariant I8.One read, two jobs. The live oracle read at
createSwap does double duty: it pins P0 and it is the g.livePrice the I9 band check compares against the MM’s quotePrice. There is no second oracle call, so the entry price the LP is charged against and the price the MM’s quote is validated against are guaranteed identical — they are literally the same variable.Decimals are read on-chain, not trusted from calldata
A sqrt-price conversion is only correct if the token decimals are correct. A market’sMarketConfig decimals are read on-chain at registerMarket by querying the token contracts directly — never accepted from the registrant’s calldata:
Orientation / token-order immutability
Chainlink feeds quote a fixed direction (e.g. ETH/USD), but a Uniswap pool has its owntoken0/token1 ordering. The conversion in _oracleSqrtPriceX96 must know which way to orient the feed relative to the pool. That orientation is fixed at first registration and immutable across re-registration:
The “LP lies about geometry” defense
Pinning the price is only half the job. The IL formula also needs the position’s range (Pa, Pb) and its liquidity L. If the LP could assert its own geometry, it could claim a wider or narrower range than its NFT actually holds and inflate the computed IL. Inflexion derives all of it from the position’s own NFT and cross-checks the market:
1
Range bounds come from the NFT ticks, via TickMath
sqrtPa / sqrtPb are derived on-chain from positions(tokenId).tickLower / tickUpper through Uniswap’s TickMath, never from an LP-supplied range:_sqrtBoundsFor. The LP cannot widen, narrow, or shift the range it actually holds.2
The position's tokens & fee are cross-checked against the market
The NFT’s actual So an LP cannot hedge an ETH/USDC position under a DAI/USDC market’s pricing, or otherwise smuggle a position into a market whose decimals/orientation/oracle don’t match it.
(token0, token1, fee) is read from the position and cross-checked against the registered marketId; a mismatch reverts:3
Liquidity L is snapshotted once at creation
L is read from the NFT exactly once at creation (on both Path A and Path B) and settlement uses the stored s.liquidity — never a re-read. This is invariant I6: an external increaseLiquidity on the custodied NFT cannot inflate the payout above MaxIL.Accepted residuals — by design, not oversight
Two residuals remain after pinning. Both are intentional design choices, framed here with their why and their bound — never exposed naked.In-range is defined at the ORACLE price, not the pool's slot0
In-range is defined at the ORACLE price, not the pool's slot0
Limit: the
Pa ≤ P0 ≤ Pb gate compares the Chainlink-derived P0 against the NFT-derived bounds, not the pool’s instantaneous slot0 tick.
Why: settlement is also oracle-pinned (round-at-T), so defining “in range” at the same oracle price keeps creation and settlement consistent. Using slot0 at create and the oracle at settle would introduce a basis between the two that the protocol would then have to reason about.
Bound: this is not a fund-loss vector — the worst case is a position whose pool tick is just outside the range while the oracle says it’s just inside (or vice versa), which only affects whether the swap is admitted, not how much it pays.Bounded entry-timing freedom
Bounded entry-timing freedom
Limit:
P0 is the latest Chainlink round at the LP’s chosen block, so the LP has a small amount of freedom over when — and therefore at what oracle print — it enters.
Why: the LP must be able to choose a moment to transact; this is intrinsic to any firm-quote design.
Bound: the freedom is clamped to within priceBandBps of the MM’s quotePrice over a 5–15s validity window (I9), and it is blunted by the premium scaling with MaxIL — picking a slightly better entry also raises the MaxIL the LP pays a premium against. Bounded and self-limiting; intended firm-quote behaviour, not a drain.Attack → defense at a glance
Where this is enforced — live
Network: Arbitrum Sepolia · chainId421614 · deploy block 274081134 (subgraph startBlock) · deployed 2026-06-05
Core protocol
Stylus & libraries
Oracles
Uniswap
Chainlink feeds
Demo tokens / pool / LP
Lifecycle swaps
Live create→settle lifecycle (2026-06-05, short 300s fee-500 market
0xb8bbd684f213d5833886ade7b531a6949d85522249881a2b5d46a5cc76e439c2):
- Path A (cvAMM convexity) — swapId 1, LP tokenId 3220 · V0 $270,531.28 · MaxIL $1,669.24 · premium $9.70 (0.58% of MaxIL) · settled payout $148.64 from ConvexityVault.
- Path B (MM routing) — swapId 2, LP tokenId 3221 · MM beat cvAMM ($8.93 vs $13.80) · MaxIL $3,215.65 · MM collateral locked $3,215.65 · settled payout $245.66 from MM collateral.
InflexionCore (entry/settle pinning, decimals, orientation, geometry cross-check) and OracleManager (the Chainlink round-at-T machinery the settlement price is pinned to). For the full registry and the live decimal/orientation values per market see deployments and decimals & units.
Why this matters for the headline claim
The no-bad-debt guarantee ispayout = min(realized_IL, MaxIL) ≤ collateral. The cap bounds the payout from above; price pinning is what makes realized_IL honest so the cap is binding on a real number rather than a settler-chosen one. Without pinning, I1 would still hold numerically (payout ≤ MaxIL) while I2 and I4 silently failed — the LP would be “always paid”, just an arbitrary amount. Pinning is the difference between a cap and a guarantee.
The two resolved forks
Fork-1 (round-at-T pin + lone-spike + 24h backstop, I8) and Fork-2 (oracle-anchored band, not last-look, I9) — the machinery the pinned prices flow through.
The ten invariants
I2 (cap correctness) and I4 (LP never profits) are the invariants this pinning restores; I6 (liquidity immutability) is the geometry companion.