UnderwriterVault, and
an LP fills it directly on-chain via createSwap. It is the
ceiling of price — an MM only ever wins flow by genuinely beating the always-on
cvAMM pool. It can never degrade the guarantee an LP
already has from the pool.
Two load-bearing reasons the MM rail exists at all:
- Hedged MMs export short-gamma risk OUT of the system to the global options market (Deribit / Panoptic). A closed pool can only relocate risk; it cannot shrink it. An MM that lays off its short-convexity exposure makes the whole system carry less risk.
- Forward-looking-vol MMs correct the pool’s structural backward-looking bias.
The cvAMM prices off realized
σ_ref; an MM can price off implied / forward vol and undercut the pool precisely when the pool is mispricing the future.
The two rails, one core
Routing in depth
createSwapRouted — the cheaper of {pool, MM}, never reverting on a bad quote.MaxIL — the unit of risk
Forks 1 & 2
createSwap — the full signed-quote entry point
createSwap(quote, signature, tokenId, maxPremium) is
the one entry point where Path-B state mutates. It follows the same four-phase
CEI structure as every create path: a READ prologue, a
block of CHECKS, atomic EFFECTS, then external INTERACTIONS last.
PHASE 1 — the shared READ prologue
createSwap begins with _prepareSwap, the identical
view prologue used by all three entry points. It establishes the facts of the
position on-chain, so neither the LP nor the MM can lie about geometry or price:
Ownership
ownerOf(tokenId) == msg.sender else NotPositionOwner.Position read + marketId cross-check
positions(tokenId); derive marketId from the live token0/token1/fee and
the market’s durationSeconds, require it equals the quote’s marketId else
MarketMismatch. The LP cannot point a quote at the
wrong market.Pa / Pb from ticks
sqrtPa / sqrtPb come from positions().tickLower/tickUpper via
TickMath.getSqrtRatioAtTick — the LP cannot lie
about the range.P0 pinned to Chainlink on-chain
oracle.getPrice(oracleToken) sets g.livePrice — never caller-supplied.
This single read both pins P0 and serves as the
anchor for the Path-B band check below, at zero extra oracle cost.In-range gate
require Pa ≤ P0 ≤ Pb else PositionOutOfRange —
the rule that makes this an in-range convexity hedge,
not insurance.MaxIL, entry snapshot, V0
maxIL = ilMath.computeMaxIL(...); entry amounts (a0, a1) via
SwapMath.entryAmounts; V0 = amount0InToken1(a0) + a1 in numeraire units.
L is captured here (g.liquidity) and stored at execution —
invariant I6._fairPremium: the
geometry is converted to ratios , , the VolOracle
is poked (permissionless, no-op if too soon), and the
Stylus FairValueOracle returns
(premium, fairRateWad, sigmaRefWad). This is the single source of fair
value for the rail. Both rails — and the router — price off this one number.
Path B then derives its premium as a load over the published fair value — the
quote can set the load, never the premium itself:
The round-up ceiling closes the
integer-division free-coverage edge.
The SignedQuote struct — all 13 fields
The quote is a flat EIP-712 struct. Read each field
together with what the contract does with it at fill — the struct is only meaningful
alongside its on-chain enforcement.
1. mm — the signer and collateral owner
1. mm — the signer and collateral owner
UnderwriterVault balance backs it. At
fill the contract requires underwriterVault.availableBalance(mm) ≥ maxIL else
MMUndercollateralised, and locks maxIL from this
MM’s pool. With EIP-1271 support, mm can be a contract signer — so a vault can
own its collateral directly with no keeper EOA.2. marketId — PER-MARKET, the key to fungibility
2. marketId — PER-MARKET, the key to fungibility
keccak256(abi.encodePacked(token0, token1, fee, durationSeconds)).
The quote binds to a market, not a position. For
ETH/USDC that is 3 fee tiers × 3 durations = 9 marketIds, all underwritten by
one pool — and any MM quote targets exactly one of them. At fill, the position’s
actual (token0, token1, fee) is cross-checked against this marketId
(MarketMismatch).3. loadBps — the only price lever the MM holds
3. loadBps — the only price lever the MM holds
FairPremium. The contract requires
loadBps ≤ maxLoadBps else LoadExceedsMax — this
is invariant I10 on Path B. The MM sets a load over the
published fair value; it can never stream a raw premium. Overcharge is impossible
by code, not by trust.4–5. minMaxILRatioBps / maxMaxILRatioBps — the per-market ratio band
4–5. minMaxILRatioBps / maxMaxILRatioBps — the per-market ratio band
ratioBps = maxIL·1e4/V0 and requires it lies in
[min, max] else RatioOutOfBand. This is a
convenience filter, never a pricing input — geometry is public, so it is not an
adverse-selection fix. See below.6–7. quotePrice / priceBandBps — the Fork-2 oracle band
6–7. quotePrice / priceBandBps — the Fork-2 oracle band
quotePrice is the oracle price the MM saw at signing; priceBandBps is the ±
tolerance. At fill, require absBps(P_live, quotePrice) ≤ priceBandBps else
PriceOutOfBand. priceBandBps must itself lie in
[PRICE_BAND_MIN_BPS = 25, PRICE_BAND_MAX_BPS = 500] (PriceBandOutOfProtocolRange).
This is invariant I9 — the
no-last-look protection.8–9. model / partialRatioBps — FULL only at launch
8–9. model / partialRatioBps — FULL only at launch
model must be CollateralModel.FULL else UnsupportedModel;
partialRatioBps is 0 in FULL. PARTIAL exists in the
enum but reverts — it is roadmap, gated on the quant.10. maxNotionalV0 — the capacity ceiling
10. maxNotionalV0 — the capacity ceiling
consumedNotional[quoteId] enforces it across every fill — see
capacity authority (I7).11. validUntil — the short validity window
11. validUntil — the short validity window
validUntil > now (QuoteExpired) and
secondsAhead ∈ [5s, 15s] (ValidityOutOfBand;
bounds VALIDITY_MIN_S=5/VALIDITY_MAX_S=15, default 8s). Bounds the
leakage interval — one of the three protections.12. quoteId — the capacity + replay key
12. quoteId — the capacity + replay key
consumedNotional[quoteId]) is tracked. One
quoteId can fill repeatedly up to maxNotionalV0, then no more.13. nonce — Permit2-style bitmap, selective cancel
13. nonce — Permit2-style bitmap, selective cancel
word << 8 | bit. The MM cancels exactly one quote by flipping exactly one
bit — never cancel-all. See bitmap-nonce replay defense.tokenId in the quote. This is not an omission — it is the entire
design. The quote is per-market; the fill binds it to a specific NFT. The
SIGNED_QUOTE_TYPEHASH was bumped for the loadBps
schema, and verification moved from ECDSA.recover to OZ
SignatureChecker.isValidSignatureNow to support EIP-1271 contract signers.Per-market, not per-NFT
The absence oftokenId is the direct mechanical consequence of MaxIL being the
fungible unit of risk. Because MaxIL is pure geometry, frozen
at creation, identical across durations, and L-independent in the fair-rate sense,
positions become interchangeable to an underwriter within a market. An MM does not
need to see your specific NFT to price you — it expresses a single appetite once and
the contract enforces it per-fill.
So an MM signs one quote per marketId carrying a triple:
and any LP position in that market whose ratio lies in the band can fill it. The
band check is the only place the LP’s specific geometry enters the Path-B gate, and
it is computed from the LP’s own position, not from the quote:
else RatioOutOfBand (mirrored non-reverting in the router).
The PHASE-2 checks, in order
After the READ prologue,createSwap runs the full revert path before any state
changes. In order:
MIN_POSITION_V0 = $100 and
MIN_PREMIUM = $1, blocking dust swaps that grief capacity
and closing the integer-division free-coverage edge.
The three deterministic protections — firm quotes, no last-look
A signed payload is a bearer instrument: once an MM signs it, the bytes survive in any hand that copied them, beyond the off-chain engine’s cancel. When vol gaps the MM is short convexity and a searcher can submit the leaked bytes on-chain. The naive defense — “last look”, where the MM rejects at settlement — undercuts trustlessness and is itself the abuse pattern auditors flag. So Inflexion has no last-look path. Instead, the MM is protected by three deterministic on-chain mechanisms at fill, with zero MM discretion:Oracle-anchored price band (I9 / Fork 2)
P0,
g.livePrice) and requires absBps(P_live, quote.quotePrice) ≤ priceBandBps, else
PriceOutOfBand. The quote auto-voids on-chain,
deterministically, if the oracle has drifted beyond the band the MM chose. This
kills the dominant pickoff (gap-on-stale-quote). Bounds: PRICE_BAND_MIN_BPS = 25
(0.25%), PRICE_BAND_MAX_BPS = 500 (5%), MM default 100 (1%). The MM is
exposed only to drifts within the band it priced for — never to gaps that blow
through it. See price pinning.Short validity window
validUntil must be [5s, 15s] ahead (default 8s) — VALIDITY_MIN_S/VALIDITY_MAX_S.
This bounds the leakage interval:
a leaked quote is only live for a handful of seconds. The band is the primary
defense; the clock is a secondary leakage-window control.On-chain selective nonce invalidation
cancelNonces, isNonceUsed,
_useNonce). See below.validUntil; within-band drift the MM bears (it chose
the band); and oracle lag if Chainlink has not ticked (majors tick on 0.05% deviation).
Capacity authority — I7
The on-chain ledger is authoritative over the off-chain engine. A signed quote can fill repeatedly only up to its declared capacity, and concurrent submissions cannot over-consume it. The rule (I7): elseCapacityExceeded. maxNotionalV0 is denominated
in V0 (position value), not collateral. The check-and-increment is
atomic in a single transaction, in PHASE-3 effects, before any external call —
consumedNotional[quote.quoteId] += g.V0 in _executePathB. Because the
increment happens in effects before the premium pull or NFT custody, two concurrent
fills cannot both pass the capacity check and over-consume.
Bitmap-nonce replay defense
A bearer quote must be revocable selectively — an MM that wants to pull one stale quote must not have to invalidate its entire book. Inflexion uses Permit2-style bitmap nonces: thenonce packs a word index and a bit position as word << 8 | bit.
- Check:
!isNonceUsed(mm, nonce)elseNonceAlreadyUsed. - Consume:
_useNonce(mm, nonce)flips the bit, atomically in PHASE-3 effects alongside the capacity increment. - Selective cancel:
cancelNonces(uint256[])flips a list of bits, so the MM revokes exactly the quotes it wants — never cancel-all. A cancelled bit can never fill, satisfying the second clause of I7.
PHASE 3 + 4 — execution (_executePathB)
_executePathB is the only place Path-B state
mutates. Strict CEI: all effects, then all interactions.
EFFECTS — state, no external calls
consumedNotional[quoteId] += V0 (I7) → _useNonce(mm, nonce) →
underwriterVault.lockCollateral(mm, maxIL) → write
SwapRecord{ mm: quote.mm, collateral: maxIL, liquidity: g.liquidity /* I6 */, ... }.
The counterparty stored is quote.mm, so settle later dispatches to the MM’s vault.INTERACTIONS — external, last
ILVault → premium split: 99% to the MM, 1%
to the treasury (no tranche split on Path B) → emit SwapCreated +
QuoteFilled + the SwapPriced event.UnderwriterVault,
where the standing invariant is I5: locked[mm] ≤ deposited[mm]
by construction. At settle the MM keeps
collateral − payout and the LP receives payout from the MM’s own pool — never
protocol funds.
settle
infers Path B purely from the stored counterparty (s.mm != address(convexityVault))
and dispatches the payout from UnderwriterVault.releaseAndDistribute. The
min(IL, MaxIL) cap, the MaxIL formula, and I1–I9 are identical to Path A. See
the lifecycle.How the router uses a Path-B quote without ever reverting
createSwapRouted (routing) does not call the reverting
createSwap gate. It calls _quoteUsableAndPremiumB —
a view mirror of every check above that returns (false, 0) instead of
reverting. It reads only (isNonceUsed, consumedNotional, availableBalance, the
signature via STATICCALL) and never mutates a nonce, capacity, or lock, so an
unchosen quote leaves zero trace (I7 preserved). An MM wins only if usableB && premiumB < premiumA — a strict beat; a tie resolves to the pool. A bad quote falls
back to the always-on pool and never reverts the LP’s transaction.
The trust model — what the engine cannot do
Path B needs an off-chain quote book (Path A does not). The engine’s power is strictly bounded (trust model):createSwap directly, bypassing the engine entirely. A
censoring operator simply loses that flow. A decentralization roadmap (Arbitrum Orbit
chain hosting the quote book on-chain) is the clean long-term answer.
Proven live on Path B
The 2026-06-05 redeploy ran a real routed fill that chose the MM:createSwapRouted(swap #2) sawpremiumA_cvamm $13.80vspremiumB_mm $8.93(MMloadBps1000) and chose Path B because it strictly beat the pool.- MaxIL $3,215.65 was locked from the MM’s own
UnderwriterVaultcollateral. - At expiry: realized IL $245.66, payout $245.66 — settled from the MM’s collateral, not protocol funds.
The two separate claims
The MM rail does not change which guarantees apply. The settlement core enforces the LP-facing guarantee identically for both rails: And the capital-at-risk disclosure applies to the underwriter — on Path B, the market maker:Where to go next
The cvAMM rail
Routing in depth
createSwapRouted and the non-reverting view mirror, step by step.Quote signing
SignedQuote.