VibeKast Mechanism Spec (v1)
Status: Live on Base Sepolia — weekly BTC markets trading, settling, and rolling autonomously · Scope: Maps the continuous-outcome market maker to the VibeKast product layer · Audience: Prospective market makers, technical reviewers, internal eng
This document describes how a Paradigm-style continuous (function-space) market maker maps onto VibeKast's "sculpt a distribution" product. It covers the invariant and pricing, how the UI projects onto the traded density, the on-chain discretization, the liquidity/tail policy, and the resolution spec. Open questions are flagged inline.
What's live (Aug 2026). Everything below is deployed and operating on Base Sepolia. The first weekly cycle settled trustlessly at its 2h-TWAP value with zero manual intervention; winners redeemed 1:1; the next market deployed itself via the automated Monday roll (settle previous → measure realized vol from the feed's own round history → deploy on a fresh spot-centered grid → record to the on-chain
MarketRegistry). The LMSR core is implemented twice and proven equivalent: a reference TypeScript engine (lib/mm/engine.ts, the readable version an MM runs) and a fixed-point Solidity suite (contracts/).testParityAllCasesproves the two agree on prices and trade costs to ~1e-6, and stateful invariant suites prove the bounded-loss / solvency properties over 128k adversarial calls. Current market address: readMarketRegistry.latest()on-chain, or the/tradepage footer. Testnet collateral (MockUSDCwith a public faucet); contracts are self-tested, not independently audited. See §9 for the as-built parameters and safety-proof inventory.
0. TL;DR
- The AMM's reserves are a probability density over a continuous outcome (e.g. "BTC price on date T"). Trading reshapes the density; the resulting curve is the market's implied belief.
- Users express views two ways: a two-slider mode (mean + confidence) for casual users, and a freeform sculpt for power users. Both project onto the same underlying density.
- On-chain, the continuous density is a piecewise-constant approximation over N discrete buckets — N = 26 as built. Gas scales with N, so the elegant "infinite support" is, in practice, a finite outcome-token set. The testnet build uses equal-width buckets (log-spacing is deferred; see §9).
- v1 resolves only against a scalar oracle, and the testnet market is now live-wired to the Chainlink BTC/USD feed on Base Sepolia (
0x0FB9…4298, 8-dp, 1200s heartbeat) viaChainlinkScalarOracle. Settlement is round-pinned: only the first feed round at/after the market's close time is accepted, proven by checking the predecessor round falls strictly before close.finalizeis permissionless and single-shot, so no caller — operator included — can shop for a favourable price.MockScalarOracleis now local/dev only. No arbitrary-event outcomes at launch. - Trading halts at close, not at resolution. A safety property, not a UX nicety: with a live feed the outcome becomes public the moment a post-close round publishes, which is strictly before anyone calls
resolve(). Leaving trading open in that gap lets anyone buy the known-winning bucket below its 1:1 redemption value and drain the subsidy risk-free. The market readscloseTime()off the oracle and rejects trades from that instant. - Settlement is a time-weighted average price (as built). The adapter settles on the time-weighted mean over the final 2 hours before close, not a single observation. Round-pinning is retained, but its job is now to prove the window is complete rather than to supply the price; the pinned round's own answer is deliberately excluded. Distorting the feed for one round no longer moves the outcome — an attacker must sustain the distortion across the window, and a distortion held for
dofwseconds moves the settled value by at mostd/wof its magnitude. See §5c. - Tails get non-degenerate liquidity via a curvature/temperature parameter + a small fixed prior density, with a capped treasury subsidy for tail depth.
1. Outcome space & market definition
A market is defined at creation by an immutable spec:
| Field | Description | Example |
|---|---|---|
outcomeScalar | The single real number the market resolves to | Final BTC/USD price |
support | Where the interior buckets sit. The market itself covers all of ℝ — see "Unbounded tails" below | Weekly, spot-relative: GridLib.gridFor(spot, vol) at deploy — ±4σ of spot, e.g. [$51k, $77k] at $64k spot (as built) |
bucketScheme | How support is partitioned into buckets | 26 buckets, equal-width = smallest nice increment ≥ 0.275σ (~$1k at typical BTC vol; log-spacing deferred) |
resolutionSource | Oracle feed identifier | Chainlink BTC/USD on Base Sepolia 0x0FB9…4298 (as built) |
resolutionWindow | TWAP averaging window | 2 h ending at closeTime (as built) |
maxSettlementDelay | How stale a settling round may be | 2 h (must exceed the feed's 1200s heartbeat) |
closeTime | Trading halts; settlement round pinned | Unix ts, Sun 23:59 UTC — [6d, 8d] after deploy, calendar-anchored (as built) |
collateral | Settlement token | USDC on Base |
b / curvature | Liquidity depth parameter | see §4 |
Everything about how the question resolves is fixed at creation and cannot be changed. Most real-world market disputes come from ambiguity in the question, not the oracle — so the spec is the primary risk-control surface.
Bucketing
- Buckets partition
supportintoNintervals. Each interval is an ERC-1155 outcome token (Gnosis conditional-token style): holding 1 unit of bucket i pays 1 USDC if the resolved scalar falls in interval i, else 0. - As built (testnet):
N = 26equal-width buckets on a weekly, spot-relative grid. The grid is computed per market at deploy time byGridLib.gridFor(spot, weeklyVol)from the live Chainlink spot: width is the smallest "nice" increment (1/2/2.5/5 × 10^k) ≥ 0.275σ_wk, spot centered in bucket 13, span ±~4σ — at typical BTC vol that is $1,000-wide buckets over e.g.[$51k, $77k], every edge a round number, which is how traders actually phrase beliefs ("BTC ends between $63k and $64k"). N = 26 itself was chosen over 24 and 32 by measurement, not taste — see §7.1 — and is cadence-independent; the grid geometry is the horizon-dependent part (full derivation indocs/weekly-grid-spec.md). Log-spacing near the expected value is designed but deferred — equal-width keeps the first on-chain version simple and the parity proof clean. Bucket scheme is per-market config, so log-spacing is a parameter change, not a redesign. - This is the crux of the UI-vs-gas tension: the demo sculpts ~90 points, but on-chain settlement is over N buckets. The mechanism claim must be "piecewise-constant density over N buckets," not "continuous."
Unbounded tails (as built)
support is not the market's domain, and describing the market as "ranged $51k–$77k" (or whatever the week's grid spans) undersells a correctness property. _bucketOf clamps rather than reverting:
if (v <= rangeLo) return 0;
if (v >= rangeHi) return n - 1;
So the true partition is two unbounded tail buckets plus N−2 interior intervals:
| bucket | interval | width |
|---|---|---|
| 0 | (−∞, $50,000] | unbounded |
| 1 … 24 | equal-width interior | $10,000 |
| 25 | [$290,000, +∞) | unbounded |
This makes the bucket set a true partition of ℝ, which is load-bearing in three places:
- Exactly one bucket always pays. Total payout is 1 USDC per complete set for every possible scalar, so the
b·ln Nloss bound in §2 holds unconditionally — not merely "provided BTC lands in range." - Settlement cannot brick. Had out-of-range reverted, a $500k print would leave the market permanently unresolvable with collateral trapped. Clamping means the tail holder simply wins.
- Manipulation incentive vanishes in the tails. Once the feed is past an outer edge, pushing it further changes no payout. All settlement-manipulation value concentrates near interior edges — which is exactly what §5c's cost argument assumes, so the two results agree without having been designed together.
The cost, stated plainly: clamping converts a misconfiguration into a silent wrong answer instead of a loud failure. This is not hypothetical — it already happened. When bucket edges were briefly declared unscaled (40_000 instead of 40_000e18), every 18-decimal oracle value exceeded rangeHi and clamped to the top bucket. Nothing reverted; the market simply settled every outcome there. MarketConfig.t.sol exists specifically to catch that class of error, because clamping guarantees the contract will not.
Known gap: the clamp itself is untested.Closed.MarketVoid.t.solnow drives settlements belowrangeLoand aboverangeHiand asserts bucket 0 / bucket N−1 wins and redeems 1:1 (test_SettlementBelowGridClampsToBucketZeroAndPays,test_SettlementAboveGridClampsToTopBucketAndPays). Before these, every assertion in the suite checked that values land inside the grid — the opposite of the black-swan path this section advertises.
2. Invariant & pricing
We use a scoring-rule market maker over the bucket set — concretely, LMSR (Hanson's Logarithmic Market Scoring Rule), which is the discrete, gas-tractable realization of the continuous L2/entropy-over-function-space idea.
Let be the net outstanding shares of bucket i. The LMSR cost function is:
The instantaneous price (implied probability) of bucket i is:
The cost to move the book from to is . Key properties:
- Prices always form a valid probability distribution (, sum to 1) — so the "density" is always well-formed, tails included.
- Max loss is bounded by — this is the market's liquidity budget and the number the treasury underwrites.
- Larger
b→ deeper liquidity, less price impact, higher subsidy cost. Smallerb→ snappier price discovery, more impact.
Why not a constant-product (x*y=k) AMM? Those price token swaps, not a normalized distribution. They can't guarantee the buckets sum to a probability, and they behave pathologically at the tails. LMSR is the correct primitive for a distribution market.
Continuous framing (for the advisor)
The continuous analog replaces the sum with an integral over the density and the per-bucket curvature with a functional. The L2-over-function-space invariant your advisor referenced is the continuum limit of the above as . We deliberately discretize for on-chain tractability; the discretization error is bounded by bucket width and is largest exactly where we widen buckets (the tails), which is acceptable because mass there is low.
3. UI → density projection
Two entry points, one underlying density:
3a. Two-slider mode (default)
The user sets mean (μ) and confidence (σ⁻¹). We render a Gaussian (or log-normal on log-spaced support) and project it onto the bucket probabilities by integrating the chosen parametric density over each bucket. The trade is then the share vector that moves current prices toward , scaled by the user's stake.
- This is what most users touch. It naturally produces smooth curves with non-zero tail mass, so the aggregate book never degenerates.
3b. Freeform sculpt (power users)
The 90-point drawn curve is downsampled/integrated onto the N buckets to produce . Same projection math, arbitrary shape (fat tails, bimodal, skew).
In both cases the on-chain action is identical: buy the bucket shares needed to push implied probabilities toward the target, spending at most the user's committed collateral. The UI shows the pre-trade price impact and the post-trade curve.
4. Liquidity & the thin-tail problem
The tails aren't "broken" — a thin tail is the book faithfully reporting low crowd probability. The real issue is price-impact asymmetry: small notional on a tail bucket moves its implied probability a lot. Levers, in order of preference:
- Non-uniform curvature. Allow a per-region
bso tail buckets get extra depth. Extreme bets face less impact without flattening central price discovery. Bounded, quantifiable treasury cost. - Fixed prior density (floor). Blend traded prices with a small fixed prior so no bucket collapses to ~0: with small and wide baseline . This is the continuous version of "always quote a spread."
- Two-slider smoothing. Because casual flow expresses μ+σ, the aggregate curve carries tail mass for free; sculptors trade against a non-degenerate book.
Guardrail: subsidized tail depth is capped per market and treated as a liquidity/marketing cost line item, because it's farmable (round-tripping tail positions to harvest subsidy). Cap + monitor.
Implementation note — a prior needs no new cost function. Lever 2 looks like it requires a prior-weighted LMSR,
C(q) = b·ln Σᵢ πᵢ·exp(qᵢ/b). It doesn't, because that is algebraically plain LMSR with seeded inventories:b·ln Σᵢ πᵢ·exp(qᵢ/b) ≡ b·ln Σᵢ exp((qᵢ + b·ln πᵢ)/b)So any prior shape — fat tails, skew, log-normal — is reachable by initialising
qᵢ⁰ = b·ln πᵢinstead ofq = 0. Zero changes toLMSRMath, so path independence and the parity proof survive untouched. The catch is the loss bound: it generalises fromb·ln Ntob·ln(1/π_min)— identical for a uniform prior, but larger for any skewed one. A prior that makes tails cheap to trade widens worst-case loss by exactlyb·ln(1/(N·π_min)), which must be funded deliberately rather than discovered.
Why not literally unbounded support?
Since the tails already extend to infinity (§1), why not N = ∞ and drop support altogether? Gas is the obvious objection but it is merely engineering, and the subsidy is not the obstacle either — b·ln N grows so slowly that a million buckets would cost only 800·ln(10⁶) ≈ 11,000 USDC.
The real obstruction is arithmetic: there is no uniform distribution over countably many buckets. At q = 0 the partition function Σᵢ exp(0/b) = Σᵢ 1 diverges, so C(0) is undefined and every price is 1/∞ = 0. This is the improper-prior problem from Bayesian statistics, and no implementation trick avoids it.
A prior-weighted maker fixes convergence (C(0) = b·ln 1 = 0) but relocates the cost onto solvency, which states the tradeoff cleanly:
- finite support →
π_min > 0→ bounded worst-case loss - infinite support →
π_minapproaches 0 → no a-priori bound
The N in b·ln N is the price of the solvency proof. Unbounded support is purchasable, and the currency is the one thing a subsidised maker cannot spend. The two unbounded tail buckets already capture the liveness benefit (nothing bricks, §1) at finite N; what they don't provide is tail resolution — and that is the thing genuinely worth wanting. The right lever for it is log-spaced buckets, not infinite ones: equal-width spacing is 25% wide at $40k but only 3.3% at $300k, systematically over-resolving the expensive end of a roughly log-normal variable. Log spacing also spans orders of magnitude for free, since log maps (0, ∞) onto (−∞, ∞) and a price series cannot go negative.
Note this conflicts with round-number bucket edges (§7.1): constant relative width and $10k edges are mutually exclusive. For a retail sculpting UI on a single asset, legible edges likely win; log spacing is the better default for a market spanning multiple orders of magnitude. Decide per listing, and record which property was traded away.
Who funds b?
Open question §7. Options: (a) treasury subsidy — simplest UX, bounded loss; (b) LP-funded — LPs earn fees, more contract/accounting complexity; (c) hybrid — treasury seeds, LPs top up. Recommendation for v1: treasury subsidy with a fixed per-market budget.
5. Resolution (the existential risk)
Continuous resolution needs a trusted scalar, and the entire payout curve pivots on it. A $1 disagreement reallocates real money across every bucket. Therefore v1 is deliberately narrow:
- Chainlink price feeds only. Launch outcomes must have a canonical on-chain scalar (crypto pairs, indices via feeds). No sports/politics/"vibe" outcomes on-chain until v2.
- Round-pinned settlement (as built).
ChainlinkScalarOracleaccepts exactly one round: the first round whoseupdatedAt >= closeTime. It proves the round is the first by requiring the predecessor round to be strictly before close, so a caller cannot skip ahead to a more favourable print.finalize(roundId)is permissionless, single-shot, and validated (positive answer, complete round, matching round id, no carried-over answer, withinmaxSettlementDelay). The feed's own decimals are read on-chain and rescaled to 18-dp WAD, so a feed with different precision cannot silently mis-scale settlement. With TWAP enabled the pinned round is a completeness proof, not a price source — see §5c. - Trading halts at
closeTime. The market gatesbuy/sell/sculpton the oracle's close time, not onresolved. See §0 — this closes a risk-free drain of the subsidy in the window between the outcome becoming public and someone callingresolve(). - Immutable resolution spec. Feed address, close time, max settlement delay, and TWAP window are set at oracle construction and cannot be changed afterwards (§1).
- Settlement: after close, anyone calls
finalizewith the pinned round, thenresolve()maps the settled scalar to the winning bucket i*. Each bucket-i* share redeems for 1 USDC; all other buckets → 0. Redemption is pull-based.
5b. Adverse selection before close (fee ramp, as built)
The closeTime halt above fixes the window after the outcome is public. The final hours before close are a separate, milder version of the same problem, and they are not fixed by halting trading.
LMSR is a passive market maker: it quotes a formula price and fills anyone, with no view on whether the counterparty knows more than the curve does. As close approaches, the outcome becomes progressively more knowable while the curve still quotes a spread-out distribution — so a late informed trader can lift the winning bucket below its eventual 1:1 redemption value. Loss stays bounded by b·ln N either way, but that bound should be a rare worst case, not the routine cost of running a market.
As built, the fee ramps linearly from 100 bps to 300 bps over the final 6 hours before closeTime (currentFeeBps()), which taxes the trade rather than blocking it. The market stays open and keeps publishing a live distribution, and the sniper's edge is redirected into feesEarned instead of eroding the subsidy.
Why a fee ramp and not decaying
b? Thinning liquidity toward close is the more common textbook answer, and it is the wrong tool here.bsits inside the cost functionC(q) = b·ln(Σexp(qᵢ/b))and also determines the subsidy already transferred at construction. Changing it mid-market would break path independence — a buy/sell round trip would stop netting to zero and become extractable by anyone, not just the snipers being targeted — and would invalidate theb·ln Nbound the treasury underwrites. The fee sits strictly outsideC, so every LMSR invariant survives untouched. (Liquidity-sensitive LMSR variants exist — Othman & Sandholm — but they are a redesign, not a parameter tweak.)
Two honest limits: the ramp is indiscriminate (it taxes genuine late information too — see §7.8), and it is a tax, not a wall (a trader whose edge exceeds the fee still trades; the profitable window is compressed, not closed).
5c. Feed manipulation at settlement (TWAP, as built)
§5b covers a trader exploiting information. This covers an attacker distorting the settlement input itself — a different attack with a different fix. The fee ramp does nothing here: the profit comes from redeeming at 1:1, not from a cheap fill, so there is no trade to tax.
Round-pinning removed caller discretion, but the settled value was still a single feed observation. An entity able to distort the aggregate for the duration of one round could push settlement across a bucket edge. Averaging is the standard mitigation, and it is now built:
As built, settlement is the time-weighted mean price over the final 2 hours before closeTime (twapWindow, in MarketConfig.sol). The security property: a distortion sustained for d seconds of a w-second window moves the settled value by at most d/w of its magnitude, so a one-round distortion is diluted by roughly the ratio of a round to the window, and the full effect requires holding the manipulation across the entire window.
Three design points worth stating explicitly:
- Time-weighted, not a simple mean of the last N rounds. Chainlink publishes irregularly — on a heartbeat or a deviation threshold. A simple mean over-weights the burst of closely spaced updates that volatility produces, which is exactly the regime an attacker creates. Weighting each price by how long it was actually in effect makes the result a property of the window, not of update count.
- The pinned round's own answer is excluded. Pinning guarantees
updatedAt >= closeTimeand the window ends atcloseTime, so the pinned round can never overlap it. Its role is now purely to prove the feed has moved past close, so no later-arriving round can still fall inside the window and change the result. Useful side effect: the first post-close round — the one an attacker has the most reason to distort, since by then the outcome is known — is structurally excluded from the price. - Window sizing came from measurement, not the datasheet. The feed's 1200s heartbeat implies ~6 rounds in 2h; the live feed actually publishes far more often (a fork run against Base Sepolia observed 39 rounds in 2h, ~156s apart, because deviation updates dominate). The round-walk cap is set to 128 for ~3x headroom, since gaps shrink during volatility ��� precisely when manipulation is most plausible.
The cost of averaging is settlement lag versus the close print — and at weekly grid resolution that cost is real, not negligible. Buckets are ~$1k wide (~0.31σ of a typical week — GridLib's documented sweet spot, §1), while a 2h BTC move is ~$200–400, so the TWAP and the last print differ by ~0.2 bucket widths on an ordinary week: roughly one settlement in five lands one bucket away from where the final tick printed. This is disclosed in the product UI and is the mechanism working as designed — the averaged value is the correct settlement variable, and a sim-vs-chain mismatch must never be "fixed" by moving the chain to the print (docs/weekly-grid-spec.md §5). The finer grid also cuts the boundary-flip manipulation threshold 10× versus the old $10k buckets ($450 sustained across the full window); the window length is what prices that attack, so it must never be shortened to reduce lag.
Remaining honest limits: averaging raises the cost of feed manipulation, it does not make it impossible, and the market still inherits the security of the underlying Chainlink aggregate. If the feed stalls for the entire window and no qualifying round ever publishes, finalize reverts and the oracle can never settle — deliberate on the oracle side, since a wrong price is unrecoverable and a missing one is not. The market-side recovery path is §5d.
5d. Settlement failure: the void escape hatch (as built)
The previous revision of this section ended with "the market stays pending rather than settling on bad data — a stuck market is recoverable." That sentence hid a total-loss failure mode: "recoverable" meant redeploying the contract, which did nothing for funds already in the old one. If the feed never produced a qualifying round, finalize reverted forever, resolve() could never run, redeem() required resolved — and every trader's collateral, not just the subsidy, was locked permanently. Bounded loss was proven everywhere; bounded lockup was not, and for a trader those are the same thing.
As built now:
voidMarket()— permissionless, callable only when both hold: the oracle reports it can never settle (isSettleable() == false— a provable condition: no round exists inside the settlement window and the window has elapsed, so no future round can be timestamped into it), andcloseTime + 3 dayshas passed. Voiding snapshots the final LMSR prices.redeemVoided()— every holder redeems every bucket at its snapshotted final price. Traders lose the winner-takes-all payoff (the outcome was never observed) but keep the market value of their position.
Three design decisions worth stating:
- Settleability, not mere non-finalization, gates the void. If "nobody has called
finalizeyet" were enough, anyone facing a winner-takes-all loss would prefer to not settle and void at price level instead. The oracle must prove settlement impossible, and itsisSettleableis conservative — every ambiguous case returns true. - Final LMSR prices, not pro-rata. Pro-rata pays every share equally, so cheap tail shares (bought at ~0) would redeem at the same rate as expensive consensus shares — the void itself would become the profit. Final prices pay each share what the market last said it was worth. Solvency is arithmetic, not hope: the void payout
Σ qᵢ·pᵢis a p-weighted mean ofq, hence ≤max qᵢ≤C(q), which is exactly the collateral on hand (excluding fees). - Prices are snapshotted at void time, because redemption burns shares; live prices would make payouts order-dependent (
test_VoidPayoutIsOrderIndependentpins this).
Honest residual, found by testing rather than analysis: a buyer's void payout exceeds their cost — a buy pays the price-path average while the void redeems at the final (higher) price, and for a routine trade that convexity gap beats the 1% fee (~5% net in tests; the original test asserted the opposite direction and failed). The excess is funded entirely by the operator's subsidy, never by other traders, and collecting it requires predicting days in advance that a Chainlink feed will permanently die. A void is the operator's insurance event; this is the deductible.
v2 (not in scope for launch)
Arbitrary events require an optimistic oracle (UMA-style) with a dispute window. Continuous outcomes make disputes worse (a continuum of contestable values, not yes/no), so this needs dedicated design — explicitly deferred.
6. Composability (north star, not a v1 constraint)
The "vibecast density feeds a lending protocol / structured product" vision is a real edge of being fully on-chain on Base — but it's v3. The moment an external protocol consumes our density as a price/risk input, our oracle-resolution risk becomes their solvency risk, and we inherit systemic exposure we don't control. Great direction; must not pull v1 design decisions.
7. Open questions (for the advisor + us)
-
Bucket count N.Resolved: N = 26, adopted after measurement. (This subsection is the "§7.1 benchmark" cited fromMarketConfig.sol.)The original N = 24 justification cited contract size ("15.6KB, well under the 24KB limit"). That is a non-sequitur:
nis animmutableconstructor argument, so bytecode is byte-identical at N = 24, 26 or 128, and the constructor already accepts any N in [2, 128]. Contract size never constrained this choice. N = 24 was a conservative default, not a derived optimum.Measured (b = 800, one buy touching every bucket):
N bucket width round edges full-grid buy subsidy 24 (previous) $10,833.33 no 1.84M gas 2,542 USDC 26 (as built) $10,000.00 yes 1.97M (+7%) 2,606 (+64) 32 $8,125.00 no 2.39M (+30%) 2,772 48 $5,416.67 no 3.54M (+94%) 3,097 64 $4,062.50 no 4.67M (+155%) 3,327 (
getPricestracks the same linear curve — 116k gas at N = 24 rising to 304k at N = 64 — so it is omitted as redundant.)The asymmetry is the whole result: because the subsidy is logarithmic and gas is linear, resolution is cheap in capital and expensive in execution. Anyone arguing from the subsidy column is arguing for more buckets; the binding constraint is the O(N) loop on every trade.
260,000 / 26 = 10,000 exactly, so on the then-yearly grid N = 26 put every edge on a round number ($40k, $50k … $300k) for +7% gas and +64 USDC. N = 32 costs four times that gas increase and still yields ugly edges ($48,125, $56,250). Legible edges matter more than raw resolution here: a trader thinks "BTC ends between $90k and $100k," and at N = 24 that had to be expressed as "$91,666.67 to $102,500" — the grid fought the mental model on every bucket. The gap between 10.8k and 8.1k buckets is not perceptually meaningful; the gap between round and non-round edges is. (Under the weekly spot-relative grid, round edges are guaranteed by
GridLib's nice-increment width and width-multiplelofor any N — so the edge argument no longer selects 26 specifically, but the gas/subsidy measurement above still does, and it is cadence-independent.)The switch was made exactly as predicted — one constant in
MarketConfig.solplus a mechanical sweep (test files now importMarketConfig.N_BUCKETSinstead of carrying their own copies,strategy.test.ts,engine.ts'sN_BUCKETS, regenerated parity fixtures, one UI string, andLMSRMath.t.sol's recomputedb·ln 26constant). No Solidity logic changed, so no new audit surface — and it was done pre-deploy, since bucket edges are immutable per market. -
Curvature policy. Single global
bvs. per-regionbfor tail depth. As built: single globalb = 800(equal-width buckets). Per-region curvature deferred with log-spacing. -
Prior floor λ. What baseline prior and blend weight keep tails tradable without distorting central price discovery? (Still open — the reference agent blends a small prior at the UI layer; not yet enforced on-chain.)
-
Liquidity funding.Resolved for v1: treasury/operator subsidy. The market constructor pulls the boundedb·ln Nsubsidy from the operator at deploy; LP-funded depth is a later iteration. -
Subsidy anti-gaming. Concrete cap + monitoring so tail subsidy can't be farmed via round-tripping. (Still open — the per-market loss is hard-bounded by
b·ln N, but round-trip monitoring is not yet built.) -
TWAP window.Resolved for v1: a 2-hour time-weighted window. Settlement now averages over the final 2 hours before close rather than reading one round, so feed manipulation must be sustained across the window instead of a single round (§5c). As predicted, this touched the adapter only —DistributionMarketandIScalarOraclewere unchanged. Still open at the margin: window length and granularity per asset class. The weekly grid has already collected on the warning this item used to carry: buckets are now ~$1k, the TWAP-vs-print gap is ~0.2 buckets (§5c), and the boundary-flip threshold dropped ~10×. The window survives that re-derivation in the lengthening direction only — attack cost scales ~linearly with window length while tracking error grows ~√length, so 2h is the floor, not a tuned optimum. A thinner feed than Chainlink BTC/USD would still want its own derivation. -
Fee model.Resolved for v1: a separate skim, ramped toward close. The trading fee is charged on top of the LMSR share cost and captured by the MM, so it does not touch theb·ln Nloss bound. As built it rises linearly from100 bpsto300 bpsover the final 6 hours beforecloseTime— see §5b. -
Fee-ramp calibration. (New, open.) The ramp mitigates adverse selection near close (§5b), but it is necessarily indiscriminate: it taxes genuine late information at the same rate as sniping, and late information is the most valuable kind.
300 bpsover 6 hours is a deliberately conservative first guess, not a calibrated number. Wants real flow data to tune, and is a pure parameter change.
8. Contract surface (as built)
Shipped in contracts/ (Foundry, Solidity 0.8.24, PRB-Math fixed point):
LMSRMath.sol— pure library:cost,prices,costDelta,maxLoss. Numerically stable log-sum-exp in signed 59.18 fixed point. This is the on-chain twin oflib/mm/engine.ts.DistributionMarket.sol— holds the cost function,b, collateral, and per-bucket inventory; prices and executes buy/sell; mints/burns outcome tokens; runs settlement + pull-based redemption. Bundles the outcome-token, market-maker, and settlement roles into one market contract (a factory splitting these out is a later iteration).- Outcome tokens (ERC-1155) — one token id per bucket, held inside
DistributionMarket. IScalarOracle/ChainlinkScalarOracle/MockScalarOracle— oracle interface (settled value, finalized flag,closeTime()for the trading halt, andisSettleable()for the void guard, §5d); the live Chainlink adapter that drives testnet settlement with round-pinning, a conveniencesettle()that finds the pinned round on-chain, and TWAP; and an operator-resolved mock now used only for local/dev runs (whoseisSettleableis always-true — a mock market can never be voided, which is one more reason it is dev-only).MarketConfig.sol— single source of truth for the deployed market's parameters (bucket edges in WAD,b, fee, feed address, close window). Consumed byscript/Deploy.s.soland asserted bytest/MarketConfig.t.solso a scale mistake fails CI instead of mis-settling.MockUSDC.sol— 18-decimal faucet collateral for testers (18-dp is a deliberate testnet choice for exact 1:1 parity with the WAD math).
Deploy + runbook: contracts/README.md (one command: ./deploy.sh). Full indexer + auth architecture still lives in the separate backend plan.
9. As-built parameters (testnet)
The numbers below are what the deployed BTC market actually uses — no longer placeholders. Keep lib/mm/agent.ts, contracts/script/Deploy.s.sol, and this table in sync.
| Parameter | Value | Notes |
|---|---|---|
Buckets N | 26 | gas/subsidy tradeoff, cadence-independent (§7.1) |
| Support | weekly, spot-relative — GridLib.gridFor(spot, vol), e.g. [$50k, $76k] at $63.3k spot | recomputed per market at deploy from the live Chainlink spot; not a fixed range — see §1, docs/weekly-grid-spec.md |
| Bucket width | smallest nice increment ≥ 0.275σ_wk — $1,000 at typical BTC vol | every edge a round number (§7.1) |
b (liquidity depth) | 800 (WAD 800e18) | single global curvature |
Max operator loss b·ln N | ≈ 2,606 USDC | seeded from operator at deploy |
| Trading fee (base) | 100 bps (1%) | separate skim, outside the loss bound |
| Trading fee at close | 300 bps (3%) | linear ramp over the final 6 h — adverse selection (§5b) |
| Fee ramp window | 6 h before closeTime | wider than the feed's 1200s heartbeat |
| Collateral | MockUSDC, 18-dp | open faucet on testnet |
| Oracle | ChainlinkScalarOracle | live BTC/USD feed 0x0FB9…4298, round-pinned, 2 h TWAP, permissionless finalize |
| Trading window | ~7 days, anchored to Sun 23:59 UTC | [6d, 8d] bound enforced at deploy; trades rejected at closeTime |
| Max settlement delay | 2 h | must exceed the feed's 1200s heartbeat |
| Chain | Base Sepolia | unaudited testnet code |
Parity + safety proofs (in contracts/test/): testParityAllCases (Solidity LMSR == TS engine, ~1e-6), plus stateful invariant tests run over 128,000 calls each — solvency across every outcome, prices-sum-to-one, an exact collateral-accounting identity, and the bounded-loss corollary (operator loss ≤ b·ln N). The handler now also carries an adversarial buyExtreme action that feeds full-uint256 share amounts biased into the wrap-to-negative region, so those same invariants are a permanent guard against the boundary bug found in the Round-2 review (see below). Rounded out by lifecycle tests (buy/sell/resolve/redeem, slippage guards), 17 revert-path tests, 3 share-overflow regression tests (AuditShareOverflow.t.sol), deployment-config tests that pin the WAD bucket scale, 7 fee-ramp tests (§5b — including a fuzzed monotonicity sweep, a check that the same trade costs strictly more late than early, and a round-trip value-neutrality test pinning the path independence that decaying b would have broken), 26 oracle tests (round-pinning, stale/incomplete rounds, round-id mismatch, carried-over answers, decimal rescaling, fuzzed "only the first post-close round is accepted", the on-chain settle() round search, and isSettleable()'s conservatism — the void guard of §5d), 12 TWAP tests (§5c — time-weighting arithmetic against hand-computed means, a one-round spike that would have flipped the winning bucket under spot settlement being diluted to harmless, time-weighting provably differing from a simple mean of the last N rounds, and exclusion of the pinned round), 16 void tests (§5d — the guard set, snapshotted-price payout arithmetic, order independence of multi-trader redemption, solvency across void, and the tail-clamp settlements that were §1's known gap), 8 live-settlement tests — including one that confirms the pre-close-guard attack would have been profitable and now reverts — and 5 fork tests against the real Base Sepolia aggregator (verifying its ABI, 8 decimals, "BTC / USD" description, an end-to-end settle, and TWAP settlement bounded by the real prices in the window). 117 tests: 112 pass locally, plus the 5 fork tests — those vm.skip unless actually run against Base Sepolia, so they report as skipped rather than passing and a green local run can never imply the live feed was checked. Line coverage is 100% on all three live contracts (LMSRMath, ChainlinkScalarOracle, DistributionMarket). Static analysis (Slither) was re-run after the adapter went live; remaining findings are documented as accepted. See contracts/AUDIT.md for the full Security & Testing Report, which now opens with a severity-ranked findings log. The latest internal round (Round 2 — a line-by-line adversarial re-review) found and fixed one HIGH-severity bug: an unbounded int256(shares[i]) cast in buy/sell/quoteBuy/quoteSell let a share amount ≥ 2**255 wrap to a negative delta, so a buy drove AMM inventory q[i] short while minting the full token amount — breaking the q == sharesOutstanding ≥ 0 invariant. It is now bounded by MAX_SHARES_PER_BUCKET at all four cast sites and regression-guarded (both directly and via the buyExtreme invariant action above). Notably, the pre-existing 128k-call invariant suite missed it because the handler bounded trade sizes to a "sane" range — a reminder that bounded fuzzing hides boundary bugs. Note all of this is in-house self-hardening; an independent audit has not yet been performed and is scheduled before mainnet.
10. Market-making interface
Because pricing is an LMSR cost function, the market contract itself is always the counterparty — there is no order book to post into. A "market maker" here is therefore not a quote-poster but an informed keeper:
- Role. The MM (or the protocol treasury) seeds the bounded
b·ln Nsubsidy that funds liquidity, then watches the implied distribution the contract exposes viagetPrices(). Whenever the market's implied belief drifts away from the MM's own fair-value belief by more than a spread band, the MM trades the book back toward its belief — buying underpriced buckets and selling ones it holds that are overpriced. Its edge is being better-calibrated than the crowd; its risk is bounded by inventory and the same solvency guarantees every trader relies on. - One structural caveat. A long-only keeper can lift underpriced buckets but cannot fully push down overpriced buckets it holds no inventory in — an honest limit of the single-agent reference design, not a bug.
- Reference tooling. A runnable reference bot ships in
contracts/bot/(a viem/TypeScript agent implementing exactly this read → compare-to-belief → trade-to-band loop, read-only by default viaDRY_RUN=true). The one-page operator guide isdocs/mm-quickstart.md— start there. The bot is unaudited example tooling an MM should review before pointing it at a funded wallet; it deliberately did not receive the contract-level hardening described in §9.
v1 as-built. Parameters in §9 reflect the deployed testnet market; items still marked open in §7 are the remaining design decisions before mainnet.