UNINITIALIZED → ACTIVE → SETTLED (the Status enum) — recorded
in a single SwapRecord struct. This page traces that
journey end to end: the one-time market setup, the shared read prologue every create
path runs, the three create entry points (each obeying strict Checks-Effects-
Interactions), the ACTIVE custody phase, and the settlement core that is identical
regardless of which rail opened the swap.
The shape of the lifecycle is the protocol’s whole safety argument in miniature: the
price is pinned on-chain at both ends (entry P0 and settlement round-at-T), the
position’s liquidity L is frozen at creation and never re-read, and the payout is
capped at MaxIL — so payout ≤ collateral holds by construction.
createSwap / createSwapPathA / createSwapRouted) differ only in who underwrites
and how the price is discovered — strictly upstream of settlement. The settlement
core (settle, the min(IL, MaxIL) cap, the MaxIL formula, and
invariants I1–I9) does not branch on the rail. See
the two rails for the upstream split; this page is the timeline.The lifecycle at a glance
SwapRecord that threads through all of this carries the fields settlement will
need, written once at creation:
mm and
has no path field — settle infers the rail from
s.mm == address(convexityVault). The
SwapRouted / SwapPriced events carry the path for the data moat.Phase 0 — registerMarket (the prerequisite)
Before any swap can be created, the owner registers each market once via
registerMarket. A market is a (pair, fee, duration)
triple, identified by:
For ETH/USDC that is 3 fee tiers × 3 durations = 9
marketIds — all underwritten by one ConvexityVault with fungible capital.
Registration is the place three pieces of hardening get baked in, each closing a way a
mis-configured market could later mis-price or be re-priced:
Oracle token must be one of the pair
token0 or token1, so the on-chain price
always refers to the actual traded asset.Decimals are read on-chain, never trusted from calldata
Price-config immutability
setCvammEnabled before the cvAMM
will underwrite them — see Path A.
Phase 1 — CREATE
Creation is the only phase that differs by rail, and even there the first two steps are shared. All three entry points run the same read prologue and compute the same fair value before any rail-specific pricing or locking happens.1a. The shared read prologue (_prepareSwap)
_prepareSwap is identical for createSwap,
createSwapPathA, and createSwapRouted. It is a view function — it touches no
state (the only non-view step in creation, the VolOracle poke, lives in
_fairPremium below). Its job is to read the truth about the position from the chain and
reject anything that is not a valid in-range entry.
Ownership
ownerOf(tokenId) == msg.sender, else NotPositionOwner. Only the LP who owns the
NFT can hedge it.Position read + marketId cross-check
positions(tokenId); derive a marketId from the live
token0 / token1 / fee plus the market’s durationSeconds, and require it equals
the requested marketId, else MarketMismatch. The
LP cannot route a position into the wrong market.Pa / Pb from ticks
TickMath.getSqrtRatioAtTick. The range is read from
the NFT — the LP cannot lie about it.P0 pinned to Chainlink, on-chain
oracle.getPrice(oracleToken) → _oracleSqrtPriceX96; the entry price is never
caller-supplied. The same read (g.livePrice) is
reused for the Path-B band check at zero extra oracle cost.In-range gate — entry must be in range
require Pa ≤ P0 ≤ Pb, else PositionOutOfRange.
This is the rule that makes this an
in-range convexity hedge, not insurance —
out-of-range entries are rejected at creation.MaxIL
maxIL = ilMath.computeMaxIL(...) — the pure-geometry
cap and unit of risk, .Entry-amount snapshot
(a0, a1) = SwapMath.entryAmounts(...) — the token0 / token1
amounts the position holds at P0.V0 in the numeraire
V0 = amount0InToken1(a0) + a1, the position value in
token1 units (typically dUSDC, 6 decimals).L is captured here as g.liquidity and stored at
execution — this is the I6 freeze that settlement will depend on.
1b. Fair value (_fairPremium)
With geometry in hand, every create path computes the same on-chain published fair
value via _fairPremium:
Geometry → ratios
a, b, σ_ref, and T.Poke the VolOracle
σ_ref permissionlessly — a no-op if too soon via VolOracle.poke.
Folding the poke into create keeps the vol
estimate fresh without a dedicated keeper. This is the only non-view step in
creation’s read phase.Read the FairValueOracle
FairValueOracle.fairPremium(oracleToken, a, b, durationSeconds, maxIL) returns
(premium, fairRateWad, sigmaRefWad). This is the single source of fair value for
both rails — the router pokes once and prices both rails off the same FairPremium.fairRate is an exact closed-form Phi-sum, evaluated live per quote by the Stylus
FairValueOracle — never a fitted surface and never reimplemented off-chain. See
the Phi-sum and the pricing stack.
1c. The three entry points (strict CEI)
All three enforce the dust floorsMIN_POSITION_V0 = $100 and MIN_PREMIUM = $1 —
blocking dust swaps that grief capacity and closing the
integer-division free-coverage edge — plus a final maxPremium LP slippage guard. From
there each rail differs only in pricing and locking, and each obeys strict
Checks-Effects-Interactions: all reverting checks first, then state effects, then
external calls.
- Path A — createSwapPathA
- Path B — createSwap
- Routed — createSwapRouted
createSwapPathA is the signature-free cvAMM rail.- Checks: the cvAMM must be wired (
convexityVault != 0) and the market must beactive+cvammEnabled. - Price:
_pricePathA= the on-chainFairPremiumplus the I10-clamped load stack, then capped at MaxIL. - Effects + Interactions (
_executePathA), in order:convexityVault.lockCollateral(marketId, maxIL)— checks free capital and senior protection.nextSwapId++.- Write
SwapRecord{ mm: address(convexityVault), collateral: maxIL, liquidity: L, ... }. - Pull premium USDC from the LP.
- Take the NFT into the
ILVault. - Premium split:
poolCut = 99%accrued into the vault viaaccruePremium,treasuryCut = 1%to the treasury.
Phase 2 — ACTIVE
Once created, the swap isACTIVE and the NFT sits in custody until expiry. This phase
has no on-chain action in FULL mode — by design.
NFT in custody, fees still the LP's
ILVault; the LP keeps fee accrual and may
claimFees(tokenId) at any time. There is no rehypothecation — locked
collateral is never lent out.The position is frozen
increaseLiquidity cannot inflate the payout (F-#2 / I6)
increaseLiquidity on a v3 NFT (it is not owner-gated), but
settlement uses the stored L captured at creation. Extra liquidity is simply
returned with the NFT and can never inflate the payout above MaxIL.No monitoring, no liquidation (FULL)
fees − IL + payout − premium — can
be positive. That is correct, not a leak. Invariant
I4 (the LP never profits from the swap) is about the payout,
which is still 0 whenever IL is 0.L is stored at creation and
read only from storage at settlement — the NFT’s live liquidity is never the input to the
payout. This is what makes the ungated increaseLiquidity harmless.
Phase 3 — SETTLE
Atblock.timestamp ≥ expiry, anyone can call settle(swapId, hintRoundId) — the
untouched settlement core, with math identical
regardless of which rail opened the swap.
Status + timing checks
status == ACTIVE and now ≥ expiry, else revert.status = SETTLED FIRST (strict CEI)
status = SETTLED before any external call. A
malicious oracle or ilMath re-entering settle on the same swapId would hit the
ACTIVE check and revert. See CEI & reentrancy.Pin the price at expiry (round-at-T)
oracle.getSettlementPrice(oracleToken, expiry, hintRoundId) pins the price to the
Chainlink round active at expiry T. The settlement
sqrt price is derived on-chain, never caller-supplied. The keeper
supplies hintRoundId; the oracle verifies it — round-at-T pinning kills the
settle-timing game. It reverts on sequencer-down / grace / staleness / lone-spike /
wrong-round (see oracles and price pinning).IL with the STORED L (I6)
ilMath.computeIL(sqrtP_T, sqrtPa, sqrtPb, s.liquidity, amount0Entry, amount1Entry)
— using s.liquidity from storage, never a re-read of the NFT.Apply the cap (I1 + I2)
payout = realisedIL > maxIL ? maxIL : realisedIL. This
single line is invariants I1 (no bad debt: payout ≤ collateral == MaxIL) and I2
(cap correctness: payout == min(realized_IL, MaxIL)).Vault dispatch by rail
s.mm == address(convexityVault) →
ConvexityVault.releaseAndDistribute(marketId, lp, payout, collateral); else
UnderwriterVault.releaseAndDistribute(mm, lp, payout, collateral). The LP
receives payout; the counterparty keeps collateral − payout (the residual MaxIL).Return the NFT
ilVault.returnNFT(...) hands the position (and any extra liquidity added during
ACTIVE) back to the LP.Emit SwapSettled
SwapSettled(id, realisedIL, payout, settlementPrice).(\cdot)^{+} is invariant
I3 — realized_IL = V_hold > V_lp ? V_hold − V_lp : 0, never an
unchecked subtraction. See the IL formula for the
three price regimes of .
settle reverts and is retryable — neither party can
exploit downtime, since the price can only be pinned to the verified round-at-T. The
liveness backstop (Fork 1 / I8) guarantees eventual success: past LIVENESS_WINDOW,
the Chainlink-at-T price is accepted unconditionally, so settle always succeeds within
expiry + LIVENESS_WINDOW + MAX_STALENESS + GRACE_PERIOD. See
the forks.Worked lifecycle — live on both rails
The 2026-06-05 redeploy ran a real create → settle on both rails against one 300-second market — the full lifecycle, end to end, on-chain:settle pins the round-at-T → payout = min(IL, MaxIL) → NFT returned. The
only difference was the rail that opened them and the vault that paid — exactly as the
“one core, two rails” design specifies.
The two separate claims
The lifecycle enforces two separate guarantees — never merge them. The LP-facing guarantee is enforced at settlement, identically for both rails (the cap at step 5, the FULL collateralization at lock): The capital-at-risk disclosure applies to whoever underwrote — the pool’s depositors on Path A, the market maker on Path B:Where to go next
The two rails
Routing in depth
createSwapRouted and the non-reverting mirror, step by step.