Smile Continuation Track: Implementation Reference

Tracks two plans on branch EthOnline2026_continuation_track: Parts A/B from the Aqua plan (two opt-in sibling vaults) and Part C from The Graph plan (a subgraph indexer + Copilot integration, The Graph feature). Continuation Track, Sept 5–13 2026. Execution order, as decided on Sept 10: Part A (SpreadVault) → Part C (subgraph) → Arc (tracked in the Arc plan) → Part B (MarginVault, stretch — cut first if the clock runs out). Outcome, Sept 10 evening: A1–A4, B1–B8 and C1–C5 all landed, MarginVault included; the RFQ tier first scoped as the Arc plan's stretch X7 was built too and is tracked here as A6 — it is a third Aqua app, so it belongs with the 1inch work, not with Arc. Rows flip from "Not started" to "Done" as each task's commit lands; the overview of all three features is in the EthOnline 2026 plan. The main Reference Table still lists only implemented/designed protocol items. The MarginVault plan defines a v1 scope only (puts only, no close()); there is no v2 — calls are mentioned only as a later follow-on, not a scoped phase.
Filter by category:
Code Category Description Status Target References
A1 Part A SpreadVault scaffold, ranges, own settlement — the foundation everything else in Part A builds on: a new `AquaApp` (`SpreadVault`) that opens two/three-leg structures (call credit, put credit, iron condor) as its own authorization type, with its own `AquaOptionSettlement` instance so the main vault's settlement is never touched.
Filessrc/periphery/SpreadVault.sol src/periphery/SpreadToken.sol test/SpreadVault.t.sol
Done 2026-09-10commit 2b060d2 — a $3,000/$3,200 call credit spread ships exactly 0.0625 WETH to Aqua (the S12 true max loss, not a full WETH), the vault's own balance stays 0 until a real pull, and setSettlement is one-time. Self-hosted AquaApp, same pattern as the main vault's put leg — no SwapVM dispatch (that's the optional call-credit opcode, later).
Done Day 3 M1
A2 Part A Shared premium library, two-leg quote — a spread's price is just its long leg's Ask minus its short leg's Bid; this task ports the vault's existing per-unit premium math into a standalone library both legs can call, so a spread quote provably agrees with the main vault's own quote to the wei.
Filessrc/periphery/SmilePremiumLib.sol SpreadVault.quote()
Done 2026-09-10the library reproduces vault.putQuote to the wei with the R3 staleness slope and the fee gross-up both live (test_quote_putLegMatchesVaultPutQuote); SpreadVault.quote returns Ask(long leg) − Bid(short leg), the fee grossed up on that net, and the S12 escrow for the size (test_quote_spreadIsAskMinusBid). Call credit and put credit both priced; iron condor still reverts UnsupportedKind. Adds MIN_QUOTE_SIGMA (0.2), inert at the default σ.
Done Day 3 A1M1
A3 Part A buy() pulls exactly max loss, mints one series token — the actual capital-efficiency payoff of Part A: a call credit spread (short K1, long K2) currently would lock collateral as if K1 were naked; this task makes it pull only the structure's true worst case, and mints one ERC-20 series token for the combined position instead of two.
FilesSpreadVault.buy() SpreadVault.execPull() src/periphery/SpreadToken.sol
Done 2026-09-10a 3000/3200 call credit spread pulls 0.0625 WETH from the writer, not the 1 WETH the main vault locks for a naked short leg (test_buy_callCreditPullsTrueMaxLossNotFullWeth); a 3000/3200 put credit spread pulls 200 USDC, not 3,200 (test_buy_putCreditPulls200not3200). Premium + fee in, JIT pull of exactly the netted escrow through this vault's own Aqua strategy under the official reentrancy guard, one SpreadToken series per structure registered with its own settlement. Reverts loudly on expiry, slippage, or exhausted Aqua capacity — no firmness bond, so never a silent (0, 0).
Done Day 3 A2M1
A4 Part A Settle, redeem, reclaim — wires the spread through to expiry: one settlement price, one floored payout formula per the netting table (credit spreads, condors), holder redemption, and the LP reclaiming whatever's left. Also the deploy-script wiring so `local.sh` and the frontend pick up the new vault's address.
FilesSpreadVault.netPayout / redeem / reclaim test/SpreadSettlement.t.sol (deploy wiring landed with A3's UI commit)
Done 2026-09-10one settlement price, one floored payout expression per structure — call credit units·(clamp(S,K1,K2)−K1)/S WETH, put credit units·(K2−clamp(S,K1,K2))/1e30 USDC — whose maximum over S is exactly the escrow, so the cap never binds. 10 tests: pin at K2 pays the holder exactly the escrow with zero shortfall; holder payout + writer reclaim == escrow to the wei in both orders; a 256-run fuzz across OTM / between / far-ITM matches the formula; OTM burns for zero and the writer reclaims everything; the put-credit twin pays the full 200 USDC below K1. Settlement reuses AquaOptionSettlement unchanged (the spread's own instance, registrar = SpreadVault). script/spread-lifecycle.sh replays the whole thing as real transactions on the ./local.sh Anvil — open → ship → buy → expiry → oracle round → permissionless settleWithChainlinkRound → redeem → reclaim — and checks conservation: at $3,100 the holder received 32,258,064,516,129,032 wei and the writer reclaimed 30,241,935,483,870,968 wei, summing to the 62,500,000,000,000,000 wei escrow exactly, vault left holding 0.
Done Day 6 A3M2
A5 Part A Long OptionToken as collateral (debit spreads, optional) — the dominance result the plan's design work already proved: if the writer already owns the long leg as an actual OptionToken, a debit spread needs zero extra collateral beyond that token. Optional/cut-first if time runs short — everything else in Part A works without it.
Planned files modifies SpreadVault.sol
Not started Day 6 A4M2
A6 Part A Hybrid RFQ tier (R6) — Derive-inspired signed quotes — the limitations doc's Phase-3 recommendation (first scoped as the Arc plan's stretch item X7, but nothing in it is Arc-specific — it is a third Aqua app, so it lives with the 1inch work): tier 1 stays the formula surface (permissionless, always live, the fallback); tier 2 lets an LP sign EIP-712 quotes off-chain from any pricing model and a taker fill them, settling through the identical Aqua JIT pull. Tradfi's "NBBO + price improvement".
Filessrc/periphery/RfqVault.sol test/RfqVault.t.sol frontend/components/RfqDesk.tsx script/rfq-lifecycle.sh; script/Deploy.s.sol (_deployRfq), local.sh, wagmi.ts
Done 2026-09-10Built as a sibling AquaApp, not a SwapVM instruction (nonces need state; the custody model, not the pricing path, is what Aqua contributes). openRange ships calls (WETH) or puts (USDC), fully collateralized; the LP signs Quote(authId, strike, maxAmount, premiumPerUnit, ttl, nonce) under domain "Smile RFQ" v1; fill recovers the signer, checks ttl / size / nonce, takes premium + 1 % fee, pulls collateral JIT under nonReentrantStrategy, mints one OptionToken per (range, strike), registers with its own settlement; formulaQuote exposes the tier-1 Ask and QuoteFilled emits both prices. Single-use nonces, cancelQuote, no close(). 8 tests (wrong signer / oversize / off-range / cancelled / expired / tampered all revert; ITM call settles with conservation). The RFQ tab signs with useSignTypedData (no gas) and shows quote vs formula; script/rfq-lifecycle.sh on Anvil: formula Ask 691.93 USDC, signed quote 685.01, taker paid 691.93 incl. fee vs 698.92 on tier 1, 1 WETH pulled JIT at the fill, replay rejected QuoteUsed. Runtime 12,714 bytes; 200 Foundry tests. On Arc testnet (0x269E7008…1879) with a real-USDC signed fill — quote 0.688860 vs formula 0.695819, 0.001 WETH pulled JIT (see the Arc deployment notes); not on Sepolia.
Done Stretch · built A3B3
B1 Part B MarginVault scaffold, ranges, own settlement, size gate — the v1-scoped (puts-only, USDC-only) margin tier's foundation: its own `AquaApp`, its own settlement instance, and a timelocked vol-buffer schedule (margin requirements can only ratchet up gradually, never jump).
Filessrc/periphery/MarginVault.sol src/periphery/MarginBackstop.sol (stub) test/MarginVault.t.sol
Done 2026-09-10MarginVault is AquaApp, Ownable, ReentrancyGuard: openRange(strikeMin, strikeMax, expiry, maxCapacity, lpMarginBps, autoTopUp, sigmaMulBps) ships to Aqua under this vault's own strategy hash (asserted against aqua.rawBalances); setSettlement / setBackstop are one-time; scheduleVolBuffer(im, mm) only tightens — IM raise applies at once, MM raise lands after 24 h via applyVolBuffer(), at most +1000 bps a step, MM ≤ IM. MarginBackstop stub: totalAssets() and a vault-only draw(). Runtime 4,905 bytes.
Done Day 9 M3
B2 Part B Worst-of mark and margin rule — what "properly margined" actually means here: the mark price is the lowest Chainlink answer in the last hour (deliberately conservative, and it never reads the vault's own σ hook — margin can't be gamed by moving vol). Initial/maintenance margin are 50%/30% of that mark, capped at the strike.
FilesMarginVault.markSpot / isMarkStale / marginRequirement test/MarginVault.t.sol
Done 2026-09-10markSpot() walks getRoundData back from the latest round until the first round updated before the hour (bounded at 64 rounds, never reverts on staleness); isMarkStale() past 90 min. marginRequirement(K, u, S, initial) = min(K·u, intrinsic + u·S·buffer) in USDC — K 3000, 1 unit: S 3000 → IM 1500 / MM 900; S 2000 → 2000 / 1600; S 0 → both capped at 3000. Tests: 3000 → 2700 → 2950 in an hour marks 2700; a 2500 two hours ago is ignored.
Done Day 9 B1M3
B3 Part B buy() locks only initial margin — the actual capital-efficiency payoff of Part B: an ATM put currently locks the full strike value; this task makes it lock only the 50% IM computed by B2, with a naked-notional ceiling tied to the backstop pool's size so total exposure can never outrun what could actually absorb a default.
FilesMarginVault.quote / initialMargin / effectiveCeiling / buy / execPull test/MarginVault.t.sol test/GasProbe.t.sol
Done 2026-09-10buy(authId, strike, units, maxPremium): the taker pays the same SmilePremiumLib Ask the main vault charges; the writer locks only IM — free balance first, the rest pulled JIT via execPull under nonReentrantStrategy. An ATM 3000 put locks 1500 USDC, not 3000 (asserted on the LP wallet). One OptionToken per (strike, expiry) shared by every writer; series registered with the vault's own settlement. Ceiling at fill: nakedNotional + (K·u − IM) ≤ min(notionalCeiling, 10 × backstop.totalAssets()) — shrinking the backstop stops new fills. Fee split 50 % insurance / 30 % backstop / 20 % DAO (claim()); fundInsurance open. Reverts on a stale mark or under 3 h to expiry. Repeat fill with a 12-round mark walk: 119,698 gas. Not done: per-range maxBlockNotional (the global ceiling bounds exposure instead). Runtime 13,257 bytes.
Done Day 9 B2M3
B4 Part B Margin calls, covered immunity, withdrawals — what happens when a writer's margin falls below maintenance: free balance gets swept first, then an opt-in Aqua credit line attempts a bounded auto top-up, and only if that's not enough does the position actually flag for liquidation (with a 1-hour grace period). A writer who's fully hedged (holds the matching long position) is immune to being flagged at all.
FilesMarginVault.deposit / withdraw / health / isCovered / flag / topUp / startAuction / coverShort test/MarginCall.t.sol
Done 2026-09-10flag(sid, writer): under MM at the worst-of-hour mark, the vault first cures toward IM on the writer's behalf — free balance, then (range opted in) a pull from the same Aqua allowance the fill used, bounded by shipped / wallet / approval — and flags only if still under MM. Test: a crash to $2,000 auto-tops the ATM put from 1,500 to 2,000 USDC out of the writer's own shipped allowance, no flag. topUp (anyone) clears the flag at IM, excess past full cover lands in free; startAuction after the 1 h grace judges health on rounds posted after the flag only (a dip that recovered unflags instead); covered puts are Covered()-immune; withdraw refused while flagged / in debt / stale and never below IM across every open position; coverShort burns own longs and frees the margin pro rata. 11 tests.
Done Day 11 B3M4
B5 Part B Writer-takeover auction and backstop pool — what happens after the grace period expires with no cure: a 30-minute auction lets another writer take over the flagged position (rising bonus, 1%→10% of notional) for a discount; if nobody bids, the pre-funded backstop pool absorbs it instead. Either way the holder's option token is untouched — only who's on the hook for it changes.
FilesMarginVault.takeoverBonusBps / takeOver / absorb / hasExpiredUnfinalized src/periphery/MarginBackstop.sol test/MarginAuction.t.sol
Done 2026-09-10takeOver(sid, writer): bonus 1 % → 10 % of notional over the 30 min window; min(locked, MM + bonus + penalty) travels with the position — bonus to the bidder, 2 % penalty to insurance (0.25 % to the flagger), the rest seeds the bidder who posts only IM − seed; the holder's OptionToken is untouched; whatever exceeded the liability is the old writer's again as free balance; no self-takeover. Worked numbers at a $2,000 mark: 1,500 moves, bidder posts 725, holds 2,000. absorb after the window: the backstop adopts the position and draws only MM − seed (175 USDC in the test), keeper tip 0.5 %. MarginBackstop: share-based deposit with dead shares, 24 h requestWithdraw, withdraw floored at max(poolRequirement, nakedNotional / 10) and frozen while any expired series is unfinalized, vault-only draw, epoch roll voiding every share on a full draw. Whole-unit takeover only (partial-unit is on the cut list). 7 tests, plus a book-balance check: vault USDC == Σ locked + free + insurance + claimable.
Done Day 11 B4M4
B6 Part B Settlement waterfall, haircut, ratchet — the actual solvency guarantee gets proven here: at expiry, each writer settles (locked margin → free balance → penalty, in that order), the series finalizes by drawing backstop then insurance for any remaining shortfall, and only as an explicit last resort do holders take a haircut (paid less than owed). A test in this task fuzzes a 40% crash and asserts holders stay whole.
FilesMarginVault.intrinsicPerUnit / settlePosition / finalizeSeries / redeem test/MarginSettlement.t.sol
Done 2026-09-10settlePosition: locked, then free; what is still short is bad debt plus the 2 % penalty, junior to the holder; a backstop-held position draws the rest from the pool; a late settler (after finalization) repays the backstop first, then insurance, then keeps the rest. finalizeSeries once every writer settled or 6 h after expiry: remaining shortfall from the backstop, then insurance, only then a haircut — HolderHaircut emitted, IM buffer +500 bps. redeem needs finalization. Gap-40 holds after lowering the backstop multiple 10 → 7 (a writer exactly at MM gapping 40 % leaves a 0.1·S shortfall against 0.7·S naked): holders get 1,714.285714 USDC to the unit with insurance untouched. Conservation: two writers, crash to $1,200, every USDC booked, total supply unchanged. 5 tests.
Done Day 13 B5M5
B7 Part B Invariants — no new production code; a pure test task asserting margin math is completely independent of σ (bump the demand-feedback hook 400 times, margin numbers must stay bit-identical) and that the main vault's bytecode is provably untouched by this whole plan (`git diff main -- AquaCollateralVault.sol` empty).
Filestest/MarginInvariants.t.sol
Done 2026-09-10400 bumpSigma calls through the hook, exactly as fills do: the premium rises, marginRequirement / initialMargin / health / markSpot stay bit-identical. Bytecode guards: AquaCollateralVault runtime still 24,364 bytes; MarginVault 23,463 and MarginBackstop 3,148 — under EIP-170 without the auctioneer split. No production code changed.
Done Day 13 B6M5
B8 Part B Deploy, keeper, demo, docs — makes the whole thing runnable: deploy wiring (seeded backstop + insurance on Anvil so the first buy doesn't immediately hit the notional ceiling), a keeper script that polls positions and drives the margin-call → auction → settle → finalize lifecycle, and an end-to-end demo test that walks and logs the entire flow.
Filesscript/Deploy.s.sol (_deployMargin) local.sh frontend/config/wagmi.ts frontend/components/MarginDesk.tsx script/margin-lifecycle.sh keeper/margin.mjs
Done 2026-09-10Deploy wires MarginVault + its own AquaOptionSettlement + MarginBackstop, 1 % fee split 50/30/20, 250k ceiling; on a blank Anvil the backstop is seeded with 25k and insurance with 5k so the first fill clears the ceiling (175k). ./local.sh prints NEXT_PUBLIC_MARGIN_VAULT / _BACKSTOP / _SETTLEMENT. The app gets a Margin · Opt-in Puts (S13) tab: write a margined range, buy a put with "writer locks IM vs the 3,000 the main vault would" shown live, and a health card (mark, locked vs MM/IM, state, naked notional vs ceiling, backstop and insurance). script/margin-lifecycle.sh replays fill → crash → flag → grace → auction → absorb (or MODE=takeover) → expiry → permissionless settle → settlePosition → finalize → redeem as real cast transactions: holder receives exactly 1,000 USDC of intrinsic, backstop drew 175. keeper/margin.mjs (viem) discovers every series from events and drives flag / startAuction / absorb / settle / finalize permissionlessly. Cut per the plan: test/MarginDemo.t.sol (the shell script is the demo), .env.example (never had a SpreadVault line either).
Done Day 13 B7M5
C1 Part C Local subgraph scaffold — Authorization entity — the foundation of the indexer: a schema + AssemblyScript mappings turning `RangeAuthorized`/`AuthorizationRevoked` events into a queryable `Authorization` entity, replacing the capped, brute-force `eth_getLogs`/RPC scans currently backing `LPDashboard.tsx` and the copilot's `chain.ts`.
Filessubgraph/subgraph.yaml subgraph/schema.graphql subgraph/src/vault.ts subgraph/tests/vault.test.ts the subgraph notes
Done 2026-09-10Authorization (keyed by authId, LP indexed) and Fill (one per OptionBought) entities; handlers for RangeAuthorized, AuthorizationRevoked, OptionBought, OptionClosed, CollateralReleased, PullFailed. graph codegen + graph build clean. Four matchstick tests written, including "an older authorization stays visible after a newer one appears" — the LPDashboard bug this replaces; matchstick has no arm64 binary, so they run via graph test -d (Docker + qemu) or on x86 CI. Package joined the pnpm workspace.
Done Day 2 M6
C2 Part C Keep usedCollateral in sync via bound contract calls — rather than reimplementing the vault's JIT-pull collateral accounting in AssemblyScript (drift risk), the mappings for `OptionBought`/`CollateralReleased`/`PullFailed` each make one bound `authorizations(authId)` contract call and overwrite `usedCollateral`/`active` directly from the live value.
Filessubgraph/src/vault.ts (refreshFromChain, refreshBySeries)
Done 2026-09-10one bound authorizations(authId) call per fill / sellback / reclaim / pull failure overwrites maxCollateral, usedCollateral, collateralToken, active from chain state; sellbacks and reclaims resolve the series token back to its authorization through the vault's seriesOf(token) getter. The matchstick fill test asserts usedCollateral comes from the mocked call, not from arithmetic in the mapping.
Done Day 2 C1M6
C3 Part C Local graph-node against Anvil, docker-compose — a 3-container stack (graph-node, IPFS, Postgres) pointed at this VPS's own Anvil RPC, for fast dev iteration before the real Sepolia deploy. Verified by querying it directly and matching a `cast call` on the same authorization.
Filessubgraph/docker-compose.yml the subgraph notes local.sh (Anvil now also binds the docker bridge)
Blocked on this host, 2026-09-10the compose stack (graph-node, IPFS, Postgres; ports remapped off 8000) and the Anvil bridge binding are in place, but graphprotocol/graph-node ships amd64 images only (checked v0.36–v0.38 and latest) and crashes under qemu emulation on this arm64 VPS. Runs as written on an x86-64 host; on arm64 the path that counts is Graph Studio (C5), which is the one that matters for the feature anyway.
Blocked: arm64 host Day 2 C2C5M6
C4 Part C Wire the frontend to the subgraph (Sepolia + local dual-mode) — `LPDashboard.tsx` and the copilot's `readAuths` query the subgraph via GraphQL when `NEXT_PUBLIC_SUBGRAPH_URL` is set; the existing `eth_getLogs`/RPC-scan paths stay as the fallback for local Anvil dev with no indexer running — never deleted, per the plan's ground rules.
Filesfrontend/lib/subgraph.ts; LPDashboard.tsx, lib/copilot/chain.ts, .env.example
Done 2026-09-10 (code)lib/subgraph.ts is a thin GraphQL fetch client with typed Authorization queries (by LP, all active). LPDashboard and the copilot's readAuths take the indexed path when NEXT_PUBLIC_SUBGRAPH_URL is set — no MAX_AUTHS cap on that path — and fall back to the existing getLogs / RPC scan on any error or when unset. Type-checks clean; exercised for real once the Studio endpoint (C5) exists, since no local graph-node runs on this host (C3).
Done Day 3 C3M7
C5 Part C Deploy contracts + subgraph to Sepolia (the live deployment) — no Sepolia deployment exists yet for this repo at all (only a local Anvil broadcast history), so this task is a real first deploy, not a re-point. The one task in the whole plan marked "cannot be cut" — an Anvil-only subgraph doesn't demonstrate the feature.
Filessubgraph/networks.json; runs forge script script/Deploy.s.sol --rpc-url sepolia --broadcast
Done 2026-09-10Contracts: the full current stack (main vault, SpreadVault, MarginVault + backstop, router, hook, lens, three settlements, fresh official Aqua) deployed to Sepolia with real Circle USDC, canonical WETH and the Chainlink ETH/USD feed — 31 txs, 28.0M gas at 1.4 gwei ≈ 0.034 ETH, addresses and hashes in the Sepolia deployment notes; vault 0x82AcBBFE5E03510d5407d8C50435B08e6d2d0a4D. Then a real authorizeRange (calls $2,300–$2,800 around the live $2,462 spot) + Aqua.ship. Subgraph: smile-sepolia on Graph Studio — v0.0.1 proved the pipeline against the old vault (no data), v0.0.2 indexes the new vault from block 11,677,088 and returned Authorization #0 within a minute of the ship: https://api.studio.thegraph.com/query/44448/smile-sepolia/v0.0.2, no indexing errors. subgraph/networks.json carries the per-network address + startBlock. Then a real buy of 0.01 units of the $2,500 call (0x505285ff…, 0.01 WETH pulled JIT from the LP wallet through Aqua, premium 5.636405 USDC): the subgraph returned the Fill and the refreshed authorization (usedCollateral 0.01 WETH, fillCount 1) one block later.
Done Day 4 C4M8
C6 Part C Long-option positions via dynamic data sources (stretch) — indexes each lazily-deployed `OptionToken`'s own Transfer events (a subgraph "data source template," instantiated the first time `OptionBought` reveals a new token address), replacing `chain.ts`'s `MAX_STRIKES_PER_AUTH`-bounded strike-grid scan entirely.
Planned filessubgraph/src/optionToken.ts; modifies subgraph/subgraph.yaml, subgraph/schema.graphql
Done 2026-09-11, differentlya Position entity (holder, instrument, balance) is kept from the vault's own events — OptionBought adds, sellback and redemption subtract — so no data-source template and no per-token Transfer indexing was needed. Consequence: an OptionToken transferred wallet-to-wallet is not seen by the subgraph (noted in the mapping). subgraph/src/vault.ts, lib/subgraph.ts positions(where: holder).
Done Day 5 C5M9
C7 Part C Subgraph MCP for local dev (stretch, AI-tooling half of the feature) — documents pointing Claude Code/Cursor/ChatGPT at The Graph's own hosted Subgraph MCP server so a contributor can query `smile-sepolia` in natural language during development. Opt-in tooling doc, not app code — the feature's other path alongside C1-C5's "AI agent using live chain data."
Planned filesthe subgraph notes (MCP section); possibly repo-root .mcp.json
Done 2026-09-12repo-root .mcp.json.example (The Graph's hosted Subgraph MCP with a Gateway key header), the subgraph notes' MCP section, and the app copilot's own "Add The Graph Subgraph MCP" preset (lib/copilot/mcp.ts); the Overview's Continuation Track table and The Graph page describe the setup.
Done Day 5 C5M10
M1 Milestone A call credit spread pulls 200 USDC, not 3200 — first proof the netting math actually works end to end, on Anvil.
Done 2026-09-10real broadcast transactions on the local Anvil via script/SpreadDemo.s.sol: a 3000/3200 call credit spread opened and shipped, one unit bought, 0.0625 WETH pulled from the writer where the main vault would lock 1 WETH; taker paid 45.50 USDC; SpreadToken minted. The put-credit twin (200 USDC, not 3,200) is asserted in the test suite. A "Spreads · Defined Risk (S12)" tab in the app writes and buys spreads with the netted-vs-naked escrow shown live.
Done Day 3 A1A2A3
M2 Milestone Pin settlement conserves escrow to the wei; Deploy prints the SpreadVault address — Part A is demoable end to end on a fresh ./local.sh.
Done 2026-09-10test_redeem_pinAtK2_holderGetsExactlyTheEscrow + the fuzz; ./local.sh prints NEXT_PUBLIC_SPREAD_VAULT; script/spread-lifecycle.sh checks payout + reclaim == escrow on real Anvil transactions. A5 (debit-spread collateral) was optional and not built.
Done Day 6 A4A5
M3 Milestone An ATM put fills with 1500 USDC pulled, not 3000 — first proof MarginVault's IM-only locking works end to end.
Done 2026-09-10test_buy_pullsOnlyInitialMargin: writer wallet drops 1,500 USDC of margin (net of the premium received), positions[sid][lp].locked == 1500e6, nakedNotional == 1500e6, OptionToken minted, series registered. 167 Foundry tests passing.
Done Day 9 B1B2B3
M4 Milestone Crash, margin call, takeover, absorb into the backstop — the liquidation path runs end to end, not just in isolation.
Done 2026-09-10test/MarginAuction.t.sol: ATM put filled at 1,500 locked → crash to $2,000 → flag → 1 h grace → post-flag round → startAuction → either takeOver (bidder posts 725, holds at IM 2,000) or, 30 min later, absorb (backstop draws 175, holds at MM 1,600, poolRequirement = 3,000). Holder's token unchanged throughout. 185 Foundry tests passing.
Done Day 11 B4B5
M5 Milestone Holders whole after a 40% gap; keeper demo; docs updated — the plan's actual definition of done: solvency proven under stress, the whole lifecycle runnable by a keeper, not just by a human calling functions by hand.
Done 2026-09-10test_gap40_holdersWhole green (backstop multiple 7); keeper/margin.mjs ran one pass against the Anvil stack and settled + finalized the open series on its own; README ladder rung 3, Solutions S13, Limitations L13, the reference table's S12/S13/L13 rows, the copilot's doc ranges, and the help site all updated. 192 Foundry tests passing.
Done Day 13 B6B7B8
M6 Milestone Local subgraph answers "LP X's active ranges" against Anvil, matches cast call — first proof the indexer's data is actually correct, not just running.
Gated: arm64 hostNeeds a local graph-node, which has no arm64 image and crashes under emulation (C3). The correctness proof happened on Sepolia instead: v0.0.2's Authorization #0 and its Fill match the vault's authorizations(0) (usedCollateral 0.01 WETH, fillCount 1) — see M8.
Gated Day 2 C1C2C3
M7 Milestone LPDashboard + copilot get_positions read from the local subgraph, getLogs still works with it stopped — proves the fallback path is real, not just a plan-doc promise.
Done 2026-09-10Both readers take NEXT_PUBLIC_SUBGRAPH_URL when set (the Studio URL, since no local node runs here) and fall through to getLogs / per-id RPC calls when it is unset or the query fails — frontend/lib/subgraph.ts, LPDashboard.tsx, lib/copilot/chain.ts (C4).
Done Day 3 C4
M8 Milestone Same data, live on Sepolia via Graph Studio — the live artifact for the feature.
Done 2026-09-10smile-sepolia v0.0.2 — https://api.studio.thegraph.com/query/44448/smile-sepolia/v0.0.2 — indexing the fresh Sepolia deployment: Authorization #0 within a minute of Aqua.ship, the Fill one block after the buy, hasIndexingErrors: false (C5).
Done Day 4 C5
M9 Milestone Copilot answers "what options do I hold" without the strike-grid scan (stretch) — proves the dynamic data-source pattern actually replaces the bounded RPC scan, not just supplements it.
Done 2026-09-11on Sepolia and Arc the copilot's get_positions and the LP dashboard read holder positions from the subgraph (readWalletPositions reports source: "subgraph"); the bounded strike-grid scan survives only for Anvil and as the fallback when the tape read fails.
Done Day 5 C6
M10 Milestone README documents Subgraph MCP setup for contributors (stretch) — a natural-language query against the Subgraph MCP correctly resolves `smile-sepolia` and returns M8's live data.
Done 2026-09-12documented in the Overview (Continuation Track table) and on The Graph page: copy .mcp.json.example, add a Gateway key, ask the agent for smile-sepolia.
Done Day 5 C7
Legend
Code: Unique identifier (A = Part A/SpreadVault task, B = Part B/MarginVault task, C = Part C/Subgraph task, M = Milestone)
Status: "Done" rows carry a dated note with the commit or test that proves it; "Gated" means blocked by the environment (arm64 host); the one remaining "Not started" (A5, long OptionToken as collateral) was cut on purpose
Target: the day target from each plan's own milestone table, assuming work starts day 0 (Parts A/B and Part C run on independent day-counts from their own plan docs, not one shared calendar)
References: the task/milestone each row depends on or feeds into — click any to scroll to it
Cut order if time compresses — Parts A/B: A5 first, then keeper/margin.mjs (demo via the script instead), partial-unit takeover, lpMarginBps. Never cut: single-price settlement in Part A, the backstop-coupled ceiling, the two-step waterfall, the sigma-independence test (B7).
Cut order if time compresses — Part C: C6 and C7 first (both stretch). C5 is the one task that cannot be cut — an Anvil-only subgraph doesn't demonstrate the feature; if C5 itself is at risk, ship C1-C4 and say so explicitly.
Full task detail (exact signatures, test names, commit messages) lives in the Aqua plan (Parts A/B) and The Graph plan (Part C).