Repository navigation
Self-serve Create Market wizard: planner → real transactor (hub-e19) - #93
Conversation
✅ Deploy Preview for futarchy ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
| category: 'crypto', | ||
| language: 'en', | ||
| minBond: form.minBondWei, | ||
| openingTime: new Date((Number(form.closeTimestamp) + 48 * 3600) * 1000).toISOString().slice(0, 16), |
There was a problem hiding this comment.
[correctness, confirmed] openingTime round-trips UTC → local and shifts by the operator's timezone. toISOString().slice(0,16) produces UTC wall-clock text without a Z; new Date(text) (here in simulate, and in useCreateProposal) parses it as local time. An operator in UTC+2 gets an on-chain openingTime 2h earlier than closeTimestamp + 48h (up to ±14h depending on timezone, plus dropped seconds). Fix: keep openingTime as a unix epoch end-to-end and only format for display.
Also on this line: clearing the close-date input stores closeTimestamp = NaN, and this render-body new Date(NaN).toISOString() throws RangeError — the whole page white-screens and form input is lost. Guard invalid dates before rendering ExecutePanel.
| // 5-day (120h) window, ending 48h before vote close so the msig can act on the | ||
| // signal while voting is still open. | ||
| const twapDurationHours = 120; | ||
| const twapStartTimestamp = closeTimestamp - (48 * 60 * 60) - (twapDurationHours * 60 * 60); |
There was a problem hiding this comment.
[correctness, confirmed] Default TWAP start is frozen at page-load time, before the pools exist. With closeTimestamp = now + 7d, this start lands exactly at page load (and startCandleUnix an hour before that). The user reviews for 10–20 min, broadcasts, and seeds pools later still — so the recorded window start predates pool initialization: Algebra getTimepoints reverts (OLD) for the window start and the candle indexer is queried for a pre-pool hour. Every wizard-default market gets a broken chart/TWAP read. Fix: anchor the window at broadcast time (or pool-seed time), not page-load, and keep the 48h-buffer relation to close as the invariant.
| const spot = ro / ri; // output per input at rest | ||
| const out = (ro * ai) / (ri + ai); // x*y=k output for amountIn | ||
| const exec = out / ai; // realized output per input | ||
| return Math.max(0, 1 - exec / spot); |
There was a problem hiding this comment.
[correctness, confirmed] Floor measures execution-price impact, ~half the real spot move. ai/(ri+ai) is the execution price impact; the post-trade spot price move in a constant-product pool is 1 − (ri/(ri+ai))² ≈ 2× larger. Example: $3,300 reserve → formula says 2.9% (LIVE), real spot move of the $100 trade is ≈5.8%. The gate passes pools at half the liquidity the R1 rule intends — exactly the thin-market state it exists to block. Fix: use the spot-move formula (and the UI copy then matches reality).
| for (const side of ['yes', 'no']) { | ||
| const token0 = (t0[side] || '').toLowerCase(); | ||
| if (!token0) continue; | ||
| const expected = token0 !== company; // raw price = token1/token0; invert when company isn't token0 |
There was a problem hiding this comment.
[correctness-when-wired, confirmed] Invert-flag rule compares the wrong token. The market page's authoritative rule (MarketPageShowcase.jsx:571) compares pool token0 against the conditional (wrapped YES/NO) company token — pools never hold the base token. This validator takes the base company token (ReadinessPanel wires selectedOrganization.companyToken.address). Once a real poolToken0 is supplied post-pool-creation, token0 can never equal the base address, so expected is always true: correct invert=false metadata gets flagged as wrong and 'corrected' to inverted — recreating the ~22,000× KIP-88/90 display bug this guard exists to prevent. The API needs per-pool conditional token addresses, not a single companyTokenAddress.
| // chunk 404'd, leaving /markets/new an empty shell in production (PR #81). | ||
| // CreateMarketFlow renders its static planner during SSG and mounts the | ||
| // wallet-connected panels client-side behind a mounted guard. | ||
| import CreateMarketFlow from '../../../components/futarchyFi/createMarket/CreateMarketFlow'; |
There was a problem hiding this comment.
[correctness, confirmed] Static import + Date.now() defaults = baked build-time timestamps. With output:'export', the page's HTML now freezes close/TWAP timestamps computed at build time (rendered in the close-date field and the MetadataPreview block); hydration recomputes them at visit time. React 18 throws hydration-mismatch errors in production and users briefly see build-date timestamps that can be days stale. The mounted guard covers ExecutePanel but not these. Fix: gate the whole timestamp-bearing UI on mounted (or compute defaults inside useEffect), keeping the static-export fix.
| const { ethers } = await import('ethers'); | ||
| const { CHAIN_CONFIG } = await import('../../debug/constants/chainConfig'); | ||
| const cfg = CHAIN_CONFIG[GNOSIS_CHAIN_ID]; | ||
| const provider = new ethers.providers.JsonRpcProvider( |
There was a problem hiding this comment.
[resilience, confirmed] Simulate rides one hardcoded RPC. NEXT_PUBLIC_GNOSIS_RPC || 'https://rpc.gnosischain.com' with no fallback/retry, against the repo's 6-endpoint RPC-resilience convention. When that endpoint rate-limits, the panel shows a transport error as if the proposal would fail — pushing the operator to skip simulate and broadcast blind. Reuse the multi-RPC provider the app already ships.
Related (altitude): this block re-implements the factory ABI, param tuple, and opening-time conversion that useCreateProposal owns — simulate and broadcast can silently diverge. Expose simulateProposal(formData) from the hook so both encode provably identical calldata.
| @@ -0,0 +1,73 @@ | |||
| import { test } from 'node:test'; | |||
There was a problem hiding this comment.
[cleanup, confirmed] Tests run nowhere. No npm script covers test/ (CI runs no tests), so floor/validator regressions ship silently; and the package is CJS-scoped, so on Node 18/20 the ESM import dies with SyntaxError. Copy the existing pattern: "test:unit": "node --test 'test/**/*.test.mjs'" like auto-qa:test:unit.
|
The following review findings could not be attached inline (line outside the diff); posting here instead:
[correctness, confirmed] updateCloseDate still uses the 24h-era TWAP formula. This handler sets |
|
Review pass (2026-08-06, Fable, deep multi-agent review at high effort): keep as draft — 7 confirmed correctness bugs need fixes before this is merge-ready. They cluster in the timing math, which is exactly the part that resolves markets:
Plus three cleanups (single-RPC simulate, duplicated factory ABI between simulate and broadcast, tests wired to no npm script). One candidate was refuted in verification (the The architecture is right — simulate-first transactor, floor gate, honesty copy, read-modify-write metadata guard all match the ratified design. The fixes are well-specified and mechanical; fix package to follow on this branch. |
|
Fix package landed as
Proof: Still deliberately NOT in this PR (per hub-e19 scope): metadata write + pool creation (wizard steps 2–3, next increment), FLM bootstrap (gated on hub-rslu), arb activation. Ready for Kelvin's review. |
d7ed549 to
4c068f9
Compare
…dator (hub-e19) - Wallet-connected createProposal (simulate-first) via existing useCreateProposal - R1 liquidity floor gate ($100 trade <3% else DRAFT); R3 zero-trade honesty - validateMetadata: invert-flag vs token0, ticker, JSON round-trip, read-modify-write - 0xAlex TWAP defaults (live-from-start, 120h window, 48h msig buffer) - Static-export fix (was PR #81): /markets/new rendered empty shell in prod - 9/9 node:test units
Timing correctness: - deriveTwapTiming() is the single source of the TWAP window; the close-date edit handler shared a stale 24h-era formula that published windows ending 72h AFTER vote close. - openingTime is epoch seconds end-to-end (openingTimeUnix); the ISO string round-trip shifted the on-chain value by the operator's UTC offset. REALITY_OPENING_BUFFER_SECONDS names the close→opening gap. - validateMetadata errors when the TWAP window starts >15min in the past (page-load-stale drafts predated pool creation → oracle reads revert). Gates: - Liquidity floor now measures the post-trade spot move 1-(ri/(ri+ai))^2, not execution impact (~2x smaller) — no more LIVE at half the depth. - Invert-flag validation compares pool token0 against the CONDITIONAL company token per pool (the market page's authoritative rule); checks defer with a warning until pools exist. Comparing the base token made the check always-wrong once wired — the KIP-88/90 bug reborn. Robustness: - Shared proposalCalldata.js builds the createProposal tuple for both simulate and broadcast — identical calldata by construction. - simulateProposal() iterates getRpcUrls(100); transport failures try the next endpoint, reverts are reported as verdicts. - Clearing the close date stores null and disables actions (no NaN white-screen); form defaults are client-mount-only so the static export bakes no build-time timestamps (no hydration mismatch). Tests: npm run test:unit wired, 13/13 pass. next build green; exported /markets/new keeps wizard markup with zero embedded timestamps.
The wizard looked for a ProposalCreated event the factory never emits, so it always fell back to the newest proposal on the shared factory. If anyone else's proposal landed first, the user was shown (and later steps would use) the wrong market. Now the address comes only from this transaction's NewProposal event, and the wizard stops with an error if it is missing.
- Send the full resolution question as marketName. It becomes the Reality.eth question on chain and cannot be changed; before, it was just "KIP-90". - Refuse to broadcast when the question is empty, lacks a question mark or has characters that break the Reality template, or when the close date is so soon that the TWAP window has already started. - Allow only one broadcast per page load, so a retry after a replaced or slow transaction cannot deploy a duplicate market.
4c068f9 to
30b96dd
Compare
|
Review (2026-09-26): APPROVE after the fixes pushed today. (Posted as a comment because GitHub does not let the author approve their own PR.) This PR sends a real transaction from the user's wallet: P1, fixed on the branch
Also fixed: only one broadcast is allowed per page load, so a retry after a slow or replaced transaction cannot deploy a duplicate market. I also re-authored the first commit, which credited an AI tool as author; the tree is unchanged. P2, still open (not blocking)
Tests: 15/15 unit, 695/695 deterministic auto-QA, and lint on the changed files all pass, and |
Self-serve Create Market wizard: planner → real transactor (hub-e19)
Turns
/markets/newfrom a read-only plan preview into a wallet-connected marketcreation wizard, with the ratified self-serve gates baked in. Part of the
futarchy.fi self-serve program (hub-ilc), Tier 1.
What's new
CreateMarketFlownow connects a wallet(RainbowKit) and creates the proposal via the existing, proven
useCreateProposalhook. Simulate-first: a static call against Gnosis shows"would succeed — new proposal 0x…" with no broadcast, so the flow is demoable
end-to-end without minting a market; "Sign & broadcast" sends the real tx.
$100 trade moves price < 3%; below the floor it reads DRAFT. Computed from
pool math (
src/features/marketCreation/liquidityFloor.js). With a pinhead seedthe gate correctly says DRAFT — "a wizard that mints dead markets would be worse
than no wizard."
validateMetadata.js) — catches the exact bugs thatshipped to production: TWAP invert flags disagreeing with on-chain
token0(the ~22,000× price-display incident), missing
coingecko_ticker(hidden spotprice), and JSON that won't round-trip (the escape-stripping incident). Plus a
read-modify-write helper so
updateExtendedMetadatanever drops keys.resolves by the default rule, not as a market verdict.
48h msig buffer before vote close.
/markets/newwas an empty shell inproduction because the
dynamic(ssr:false)chunk 404'd underoutput:'export'.Now a static import + a mounted-guard on the wallet panel, so the page renders
in SSG and the wallet-connected parts hydrate client-side.
Tests
node --test test/marketCreation.test.mjs— 9/9: wrong invert flag caught + autocorrected, missing ticker caught, invert deferred pre-pools, read-modify-write
preserves keys / refuses unparseable input, deep pool LIVE vs thin pool DRAFT,
zero liquidity → DRAFT.
Not in this PR (staged, shown as the plan below the wizard)
FLM bundle deploy + bootstrap, Snapshot link, and arb activation remain the
described plan steps. The FLM factory is deployed on Gnosis (
0x1814aDfE…) but ispre-audit (one medium open, hub-rslu), so wiring its bootstrap into a one-click
flow waits on that fix. Arb activation = writing
arbitrageContractAddress(shared GNO/sDAI
0xaC59…) into metadata, picked up automatically by the arbautopilot.