(token0, token1, fee, durationSeconds) tuple — a
specific Uniswap v3 pair, a specific fee tier, and a specific contract maturity. The
deploy registers 9 markets for one pair: 3 fee tiers crossed with 3 durations, all
underwritten by a single ConvexityVault with fungible USDC
capital. Each market has a deterministic identity — its marketId, a tight-packed
keccak256 hash that is byte-for-byte identical across the contract, the SDK, and
the subgraph.
The market grid is intentionally small and dense: 9 product lines, one treasury.
The duration is baked into the
marketId, so the same position geometry prices
differently in the 7d, 30d and 90d markets — the
S-curve in does the work, while
MaxIL (pure geometry) is identical across all three. One pair, one
pool of collateral, three prices for the same risk.Why 9 markets over one vault
The two axes are the two things that change the risk of underwriting a position without changing its geometry:- Fee tier selects which Uniswap v3 pool the LP’s position lives in. Different fee tiers have different tick spacing and different liquidity profiles, so they are genuinely different markets even for the same token pair.
- Duration is the contract maturity. A longer maturity gives the price more time to diffuse, so the same range is more likely to realize a larger fraction of its MaxIL. This is the entire reason the cvAMM publishes three different prices for the same position — see the grid.
ConvexityVault with fungible
capital. One vault per pair acts like a single insurer’s
treasury backing nine product lines — capital flows to wherever the demand is, and
intra-pair diversification (different fees, different durations) lowers the pool’s
aggregate tail. The per-market locked amount is tracked in
lockedByMarket[marketId], but the free capital that backs new fills is shared.
The 9 markets grid
The deploy registers 9 markets = 3 fee tiers x 3 durations for the dWETH/dUSDC pair. The axes:- Fee tiers (
uint24):500(0.05%),3000(0.30%),10000(1.00%). - Durations:
7d(604,800 s),30d(2,592,000 s),90d(7,776,000 s).
FEES.flatMap(fee => DURATIONS.map(...)), iterating
fee-outer, duration-inner, which fixes the index
layout of MARKETS[0..8]:
MARKETS[0] is the live, three-way-parity-pinned market:
0x67c4bee1ee037851fbe2a8ecfdd0b8ae3d358283e940750c268621f776479d69 (dWETH, dUSDC,
fee 500, 604800 s = 7d) — it matches the on-chain address registry’s
marketId_fee500_7d, and DEMO_MARKET_ID = demo.marketId_fee500_7d sanity-checks
against MARKETS[0].
Same geometry, three prices
Because the duration is part of themarketId, a single LP position fills against a
different marketId per duration and is priced per duration — while its
MaxIL is identical across all three (MaxIL is pure geometry, frozen
at creation, L-independent in the fair-rate sense). All three
prices share the same MaxIL but a different fairRate:
fairRate is an S-curve in — it carries all of the
volatility and time dependence, and for a calm/short market, for a
violent/long one. MaxIL carries none of it. The
7d / 30d / 90d markets are exactly the three points
on that one curve. The duration axis of this grid is the
convexity term structure the data moat publishes as Signal 3.
The marketId derivation
marketId is the join key that ties the contract, the SDK, and the
subgraph together, so its byte layout is load-bearing — a single field-width
mismatch produces a different, non-matching hash and silently breaks every cross-system
lookup.
with token0 = dWETH, token1 = dUSDC (ordered so token0 < token1 by address). The
ABI types are (address, address, uint24, uint32).
Where it lives in code
marketId is derived, never trusted from calldata: registerMarket
hardens it three ways:
- The oracle token must be one of the pair.
- Decimals are read on-chain, never trusted from calldata — one wrong digit would mis-scale every price ~10x.
- Price-config immutability — the oracle orientation is frozen once set, so re-registering a market can never re-price an active swap.
setCvammEnabled.
The subgraph’s hand-rolled replication
The subgraph cannot import the contract, so it re-derives themarketId in AssemblyScript
inside deriveMarketId: it concatenates the raw big-endian bytes
of each field at its on-chain width — address → 20 bytes, uint24 fee → 3 bytes,
uint32 durationSeconds → 4 bytes — then crypto.keccak256.
Why a hand-rolled uintBytesBE, not ethereum.encode
Why a hand-rolled uintBytesBE, not ethereum.encode
The subgraph deliberately uses a hand-rolled
uintBytesBE,
not ethereum.encode, because ethereum.encode ABI-pads every operand to 32
bytes and would produce a different, non-matching hash.
This is the same abi.encodePacked vs padded-ABI trap, flagged at the
implementation level.The uint32 sign-safety subtlety (durationSeconds ≥ 2³¹)
The uint32 sign-safety subtlety (durationSeconds ≥ 2³¹)
durationSeconds is a uint32 on-chain but arrives in AssemblyScript as an i32.
A value ≥ 2³¹ arrives negative after BigInt.toI32() and sign-extends to
i64. Because uintBytesBE emits only the low width bytes and & 0xff masks
each one, the sign bits (which live above width) are never written — so the
uint32 preimage stays bit-for-bit correct across the full on-chain domain.
None of the live durations approach 2³¹ s, but the encoder
is correct across the entire type domain regardless.How the mapping recovers the marketId from a SwapCreated event
How the mapping recovers the marketId from a SwapCreated event
A
SwapRecord carries no marketId, so the subgraph
reconstructs it: (1) it reads token0/token1/fee from the NPM positions()
enrichment call, (2) reads expiry/createdAt from InflexionCore.swaps() to
compute durationSeconds, then (3) calls deriveMarketId — reusing the NPM bind,
not re-binding. It needs all four fields, so both
the geometry decode and the swap-record read must succeed.Three-way parity
The 47-byte tight-packed preimage must stay identical across all three systems, or the join key breaks:
All three are pinned by two parity tests to the live market
marketId
0x67c4bee1ee037851fbe2a8ecfdd0b8ae3d358283e940750c268621f776479d69
(dWETH, dUSDC, fee 500, 604800 s = 7d):
- An on-chain parity test covers the contract side.
- An SDK parity test covers the SDK / width-contract side.
Why the duration is inside the id, not a side parameter. Folding
durationSeconds into the hash makes each (geometry, duration) pair its own first-class
market with its own price, its own capacity, and its own row in every aggregate. It is
what lets the data moat read a clean term structure: the 7d / 30d
/ 90d rows of one geometry are three distinct marketIds that the
subgraph keys on directly.How a market is used downstream
ThemarketId threads through the whole protocol as the single coordinate that names a
product line:
1
Quote — Path B
A SignedQuote carries
marketId (never a tokenId) —
an MM quotes a (load, MaxIL-ratio band, capacity) for an entire market, and any
in-band position in that market fills against it. This
is the mechanical consequence of MaxIL making positions fungible.2
Create — cross-check
Every
create* entry point re-derives the marketId from the live position
geometry and requires it equals the requested one,
so the rail can only fill the market the LP actually owns a position in.3
Lock — per-market accounting
On Path A,
ConvexityVault.lockCollateral(marketId, maxIL) increments
lockedByMarket[marketId] and the O(1) HHI accumulator, while the free capital
that backs the lock is shared across all 9 markets.4
Index — the join key
The subgraph keys
Market, MarketStateSnapshot, and the
per-fill Swap rows on marketId, and the API joins the
on-chain facts to off-chain telemetry on that same key.Live registry
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.
Structures ship from day one; the full DYNAMICS (pool-vs-MM spread as a forward-vol signal) mature with multiple competing MMs and volume. The dataset begins at the single redeploy; the off-chain telemetry halves begin at the first interaction logged.What Inflexion offers today is the architecture and the first public view into the microstructure of the DeFi LP volatility-risk premium — not yet a mature dataset:
- Structures are present day-one. All five non-circular behavioral signals are defined and queryable from launch.
- Dynamics mature with volume. The richer signal — the pool-vs-MM spread read as a forward-vol indicator — only sharpens once multiple market makers compete and real flow accumulates.
- The dataset begins at the single redeploy, and the off-chain telemetry halves begin at the first interaction logged. History is served live from the subgraph via the API; if a backing (subgraph URL / RPC / telemetry) is absent, surfaces degrade to a typed
pendingstate — never an error.
Where to go next
The markets grid (reference)
The full 9-cell grid with every marketId, fee, duration, and index laid out.
marketId parity (integrators)
The integrator checklist for reproducing the 47-byte preimage in any language.
MaxIL — the fungible unit
Why MaxIL frozen-at-creation is what makes a per-market (not per-NFT) quote possible.
The vaults
One ConvexityVault per pair, fungible capital across all 9 markets, dual-tranche.
Fair value & the S-curve
Why the same geometry prices differently per duration — the S-curve in σ²·T.
The subgraph
How marketId becomes the join key that ties the indexer to the chain.