How to read this ledger. Every row follows the same shape: vector → mitigation → where enforced. “Where enforced” is a Foundry-fuzzed invariant (
I1–I10). Nothing here is “trust us” — each mitigation is code, and most are also driven by a fuzz test on the invariants page.The master ledger
The fifteen vectors below are grouped by what they attack: LP payout integrity, oracle/settlement, quote & capacity integrity, economic griefing, depositor/custody safety, and reentrancy. Audit-finding tags (F-#N) reference the external audit summarized on the audits page.
| Attack vector | Mitigation | Where enforced |
|---|---|---|
| MM / underwriter bad debt under a large price move | FULL collateral locked at MaxIL at creation + payout capped at min(realized_IL, MaxIL). The cap is the min; structural, not heuristic. Holds for any price path. | I1 + I2 — payout cap and collateral locked at creation (Path A / Path B) |
| Lone-spike Chainlink glitch at expiry (a single bad print pins a wrong settlement price) | Settlement price is the Chainlink round-at-T; a “lone spike” (a round differing from both neighbours by ≥ LONE_SPIKE_BPS = 5%) is deferred. Real fast moves persist across rounds and pass. | I8 — enforced in OracleManager (Fork-1) |
| Permanent fund lockup if an oracle deferral never resolves | A 24h LIVENESS_WINDOW backstop accepts the round unconditionally once the window passes; a missing neighbour across a Chainlink phaseId boundary is read tolerantly (try/catch) and degrades to the backstop, never a revert. | I8 — enforced in OracleManager |
Stale-quote pickoff — a leaked bearer-instrument SignedQuote replayed on-chain when vol gaps (F-#7, Fork-2) | Oracle-anchored band: the quote carries quotePrice + priceBandBps; createSwap auto-voids it on-chain, deterministically, if absBps(P_live, quotePrice) > priceBandBps. No MM discretion at fill ⇒ not last-look. | I9 — enforced in InflexionCore (Fork-2) |
| LP lies about the position’s range geometry (to inflate MaxIL/premium/V0) | sqrtPa/sqrtPb are derived on-chain from positions().tickLower/tickUpper via TickMath; the position’s actual (token0, token1, fee) is cross-checked against the registered marketId. The LP can lie about neither the range nor the entry. | Enforced in InflexionCore (MarketMismatch) |
Settler chooses the settlement price (a permissionless settler sets payout anywhere in [0, MaxIL]) | P_T is derived on-chain from the Chainlink round-at-T via _oracleSqrtPriceX96, never from calldata. Restores I2 and I4. | I2 / I4 — enforced in InflexionCore |
| LP picks a favorable entry price (to set MaxIL/premium/V0 in their favor) | P0 is the on-chain Chainlink round at the LP’s block, clamped to within priceBandBps of the MM quotePrice. Bounded entry-timing freedom, blunted by premium scaling with MaxIL. | Enforced in InflexionCore (I9 clamp) |
External increaseLiquidity on the custodied NFT (inflate computed IL post-creation, F-#2) | L is snapshotted once at creation; settle passes the stored s.liquidity to computeIL, never a re-read of the NFT. An attacker can grow on-chain L between create and settle with zero effect on payout. | I6 — enforced in InflexionCore |
Bearer-quote replay after off-chain cancel (F-#7) | Permit2-style bitmap nonces (nonce = word<<8 | bit) + selective cancelNonces([...]): an MM voids exactly one quote by flipping one bit. A cancelled bit can never fill. | I7 — enforced in InflexionCore |
Capacity over-consumption from concurrent fills (F-#6) | Atomic check-and-increment of consumedNotional[quoteId] in PHASE 3 effects, before any external call, guarded by the + g.V0 > maxNotionalV0 revert in PHASE 2. Capacity is denominated in V0, not collateral. | I7 — enforced in InflexionCore |
Dust-swap MM griefing (spam sub-economic positions, F-#8/F-#13) | Floors: MIN_POSITION_V0 = $100, MIN_PREMIUM = $1. Positions and premiums below the floor are rejected at the PHASE-2 dust gate. | Enforced in InflexionCore |
Premium under-charge from integer division (F-#8) | premium = ceilDiv(...) — rounds up, never down. An attacker cannot shave a fractional unit off the premium across many fills. | Enforced in CvammPricing and InflexionCore |
| Overcharge the LP (push premium above the fair value + max load) | By construction: Path A clamps total = min(baseLoad + skews, maxLoad); Path B requires loadBps ≤ maxLoadBps. Both also cap premium at MaxIL. No MM discretion, no graceful-degradation path. | I10 — enforced in CvammPricing and InflexionCore |
| Sequencer-downtime exploitation (settle/create against a stale L2 state) | _requireSequencerHealthy enforces sequencer uptime + a 1h grace period and fails closed on a future updatedAt. | Enforced in OracleManager |
| NFT custody hijack (deposit a fake NFT, or redirect fee claims) | ILVault.onERC721Received accepts NFTs only from the canonical NonfungiblePositionManager configured at deploy; claimFees recipient is forced to the registered LP. | ILVault (custody model) |
Locked-collateral utilization-gated withdraw failure (rehypothecation breach, F-#3) | A yield adapter must be isInstantlyRedeemable(); locked collateral is never routed to Aave/Compound. Only the idle slice may go to instantly-redeemable wrappers (sDAI / tokenized T-bills). | IYieldAdapter (vault rules) |
| Pool run on the cvAMM (mass withdrawal drains collateral mid-swap) | Withdrawals are cooldown-gated (requestWithdrawal → withdraw after withdrawalCooldown); junior cannot be drawn below totalLocked. The same locked/free accounting also drives util_skew up before the pool over-commits. | Enforced in ConvexityVault (JuniorBelowLocked) |
| Underwriting loss hits senior depositors | totalLocked ≤ juniorAssets is enforced at every lockCollateral; the waterfall takes juniorLoss first, seniorLoss only after — and by the invariant senior is never reached. Junior-first-loss. | Enforced in ConvexityVault (SeniorProtectionBreached) |
| Forged MM quote (fabricate a SignedQuote) | Every Path-B quote is EIP-712 signed and verified via OZ SignatureChecker.isValidSignatureNow (ECDSA + EIP-1271 contract signers). An unsigned or mis-signed quote is rejected. | Enforced in QuoteVerification |
Re-entrant settle via a malicious oracle/ilMath | Strict CEI with status-flip-first: s.status = SETTLED is hoisted to immediately after the validity checks, before any external call, so re-entry on the same swapId hits the Status.ACTIVE precondition and reverts. (The one Slither medium that was fixed.) | Enforced in InflexionCore |
The vectors, in a little more depth
The table is the canonical artifact; the accordions below add the why for the rows that most reward it.MM bad debt, settler price choice, and entry-price gaming — the LP-payout cluster
MM bad debt, settler price choice, and entry-price gaming — the LP-payout cluster
These three rows share one root defense: the LP can supply neither the geometry, the entry price, nor the settlement price.
- MM bad debt is closed structurally — collateral is
MaxILand the payout ismin(realized_IL, MaxIL), sopayout ≤ collateralfor any price path (the no-bad-debt math). The cap is the load-bearing piece; remove it and beyond-PbIL is unbounded. - Settler price choice was a real hole: a permissionless
settleonce computed payout from a caller-suppliedsqrtP_T, so a settler could set payout anywhere in[0, MaxIL]. The fix derivesP_Ton-chain from the Chainlink round-at-T, restoring I2 and I4 (an LP who would have profited vs hold gets exactly zero). - Entry-price gaming is bounded the same way:
P0is the on-chain round at the LP’s chosen block, clamped to within the MM’spriceBandBps. The residual entry-timing freedom is bounded and blunted because premium scales with MaxIL.
Lone-spike, permanent lockup, and sequencer downtime — the oracle/liveness cluster
Lone-spike, permanent lockup, and sequencer downtime — the oracle/liveness cluster
The settlement oracle is the most subtle surface, because it must be both correct (no glitched price pins a wrong payout) and live (funds never lock permanently). Fork-1 resolves the tension:
- A glitched lone spike — a round differing from both neighbours by ≥ 5% — is deferred, but only until the 24h
LIVENESS_WINDOWelapses, after which the backstop accepts the round unconditionally. - A missing neighbour round across a Chainlink
phaseIdboundary is read tolerantly via_tryRound(try/catch returningok=false), so it degrades to the backstop rather than reverting forever. - Sequencer downtime is gated separately by
_requireSequencerHealthy(uptime + 1h grace, fails-closed on a futureupdatedAt).
Stale-quote pickoff, forged quote, replay, and capacity — the quote-integrity cluster
Stale-quote pickoff, forged quote, replay, and capacity — the quote-integrity cluster
Path-B quotes are bearer instruments — signed bytes that survive in any hand that copied them, beyond the off-chain engine’s cancel. Four mechanisms, all on-chain, bound what a leaked or forged quote can do:
- Forgery is impossible — every quote is EIP-712 signed and verified via
SignatureChecker.isValidSignatureNow(ECDSA + EIP-1271). - Stale-quote pickoff (the dominant pickoff, Fork-2) is killed by the oracle-anchored band:
createSwapauto-voids the quote if the live oracle drifted beyondpriceBandBps. Deterministic, no MM discretion ⇒ not last-look (a hard protocol rule). - Replay after cancel is blocked by Permit2-style bitmap nonces — an MM flips one bit to void one quote; a cancelled bit can never fill.
- Capacity over-consumption from concurrent fills is blocked by atomic check-and-increment of
consumedNotional[quoteId]before any external call.
createSwapRouted, only the executed rail mutates nonce/capacity/lock — a quote that loses the route consumes nothing.Dust grief, under-charge, and overcharge — the economic cluster
Dust grief, under-charge, and overcharge — the economic cluster
Three economic-edge attacks, three deterministic clamps:
- Dust griefing — sub-economic spam — is floored at
MIN_POSITION_V0 = $100andMIN_PREMIUM = $1. - Under-charge from integer division rounds the wrong way only if you let it; Inflexion uses
ceilDivso the premium always rounds up. - Overcharge is impossible by construction — the load is clamped at
maxLoad(Path A) orloadBps ≤ maxLoadBps(Path B), and premium is additionally capped at MaxIL. This is I10, lives upstream of settle, and never touches the no-bad-debt proof. See the load stack and I10 on the no-bad-debt concept page.
Pool run, senior loss, NFT hijack, rehypothecation, and reentrant settle — the systemic/custody cluster
Pool run, senior loss, NFT hijack, rehypothecation, and reentrant settle — the systemic/custody cluster
- Pool run: withdrawals are cooldown-gated and junior cannot be drawn below
totalLocked. The same locked/free accounting raisesutil_skewbefore the pool over-commits. - Senior loss from underwriting:
totalLocked ≤ juniorAssetsis enforced at every lock; the junior-first waterfall absorbs all underwriting loss before senior is touched. This is senior protection, not a capital guarantee — see tranches. - NFT hijack:
ILVault.onERC721Receivedaccepts NFTs only from the canonical NonfungiblePositionManager;claimFeesis forced to the registered LP. - Rehypothecation breach: locked collateral never goes to a utilization-gated venue — a yield adapter must be
isInstantlyRedeemable()(idle-only, sDAI/T-bills). See collateral models. - Reentrant settle: status flips to
SETTLEDbefore any external call, so a maliciousoracle/ilMathre-enteringsettlereverts on theStatus.ACTIVEprecondition. Full CEI on the reentrancy page.
What the matcher cannot do — the structural backstop
Several rows above (forged quote, stale-quote pickoff, capacity) are also covered by a more general fact: the off-chain matching engine exists for Path B only, and its power is strictly bounded. It can censor, reorder, or drop Path-B quotes; it cannot steal funds, forge quotes, force a stale quote past the band, or touch Path A at all (which needs no engine). Settlement is non-custodial and on-chain, against the counterparty’s own collateral. This is the structural reason the table holds even against a malicious operator: the mitigations are enforced by the contracts, not by the relayer. Read the full trust model for the can/cannot grid and the direct-to-contract fallback.Two things the ledger does not claim
Two caveats, each with its why and its mitigation:Where every mitigation lives — on-chain, today
The entire ledger is enforced on the live Arbitrum Sepolia deployment.InflexionCore carries the I1–I10 enforcement and CEI settlement; OracleManager carries I8 and the sequencer gate; the vaults carry I5 and the senior-protection waterfall.
Network: Arbitrum Sepolia · chainId 421614 · 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.
The ten invariants
Each invariant’s precise statement, why it holds, and the Foundry fuzz test that verifies it.
The two resolved forks
Fork-1 (oracle liveness backstop, I8) and Fork-2 (oracle-anchored band, I9) — the two open audit items, now in code.
Trust model
What the off-chain matcher can and cannot do, and why the table holds against a malicious operator.
Audit history
The external audit (12 fixes + 2 forks), the quant audit, and the two Slither passes.