Skip to content

Wizard steps 2–3: metadata write + pools + invert verification (hub-e19 increment C) - #94

Merged
krandder merged 9 commits into
mainfrom
selfserve-wizard-c
Sep 26, 2026
Merged

krandder merged 9 commits into
mainfrom
selfserve-wizard-c

Conversation

@krandder

@krandder krandder commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Stacked on #93 (base: selfserve-wizard) so review units stay small — merge #93 first, then rebase this onto main.

Implements the frozen spec hub/docs/specs/2026-08-06-selfserve-wizard-increment-C.md:

  • Step 2 — metadata write: createAndAddProposalMetadata via the org owner/editor key. Role gate read on-chain, simulate-first, TWAP timing recomputed at write time (a page-load-stale draft can never be written — the validator's past-start gate backs this), shared args tuple with OrganizationManagerModal.
  • Step 3 — pools: conditional tokens from the existing fetchProposalFromChain (wrappedOutcome 0–3), YES/NO pools via the existing useCreatePool, initial price prefilled from the org's spot ticker and operator-confirmed (a wrong initial price permanently distorts quotes — the RAGE-pool lesson).
  • Invert verification (the point of the increment): after both pools exist, read each pool's token0(), validate the on-chain metadata's invert flags against the conditional company token (the market page's authoritative rule), and offer a single flags-only updateExtendedMetadata built with mergeMetadataForUpdate — read-modify-write, nothing else touched. KIP-88/90 becomes unrepresentable through this flow.
  • Steps unlock from the step-1 result or ?proposal=0x… (refresh-safe resume).

Proof: npm run test:unit 17/17 (new: args tuple, invert context/patch, end-to-end wrong-flag → flags-only merge) · next build green · exported HTML still timestamp-free.

Not in scope (per spec): FLM bootstrap (hub-rslu gate), liquidity amounts (deferred decision), arb activation, Snapshot vote-timing autofill (increment D). Known ceiling: anvil-fork E2E scenario to land via the auto-qa harness before merge.

@krandder

krandder commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

E2E fork demonstration added and passing (test/e2e-fork-demo.mjs, run node test/e2e-fork-demo.mjs — spawns its own anvil fork of Gnosis):

✓ org.setEditor(client)                       — one-time onboarding grant (org owner's only appearance)
✓ factory.createProposal → 0xeE88…BadeF       — client-signed, 1.87M gas
✓ org.createAndAddProposalMetadata            — client as editor, timing recomputed at write
✓ create YES pool / create NO pool            — Algebra NFPM, 6.77M gas each
✓ invert flags wrong on-chain ({invertTwapPoolNo:true}) — client fixes with one flags-only tx
PASS — 6 txs total, 0 from Kelvin's EOA, TWAP 120h ending close−48h, invert flags verified

Note what happened on the first run: the NO pool's real token ordering required invertTwapPoolNo: true — the wizard's verification pass caught and corrected a genuine inversion, the exact KIP-88/90 failure mode this increment exists to kill. The demo asserts zero Kelvin-signed transactions and that the org owner appears exactly once (the mixed-mode setEditor grant).

This is the success-criterion mechanics (create → metadata → pools → verified) proven on a fork through the same modules the UI calls. The browser-level twin lands as an auto-qa harness scenario before merge.

@krandder

krandder commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Increment D (wizard side) added in fe61325: pasting a Snapshot proposal hash autofills the close time from the vote's real end (hub.snapshot.org GraphQL), and deriveTwapTiming anchors the whole 0xAlex window to it — 5-day window, 48h buffer before close, openingTime 48h after close. Fails closed on bad id / unknown proposal / hub outage (manual entry stands). 19/19 unit tests, build green. With this, every timing field in the wizard derives from the vote itself — the hand-aligned windows behind both production TWAP incidents can no longer be typed in.

@krandder

krandder commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Full-journey demonstration now passing (node test/e2e-fork-demo.mjs --full, commit 840c743) — the goal's complete success-criterion mechanics on an anvil Gnosis fork:

create proposal → metadata (editor key) → both pools → invert-verified
→ split collateral (FutarchyRouter) → seed both pools (~$10.8k currency side)
→ LIVE: floor gate passes (a $100 trade moves price 1.83% < 3%)
→ warp past openingTime → Reality answer (1 xDAI bond, client-posted)
→ 3.5d timeout → realityProxy.resolve(proposal) — permissionless
→ CTF payout vector read → winning side redeemed: 1,200 sDAI returned
PASS FULL — 21 txs, 0 from Kelvin's EOA, org owner only the setEditor grant

Demo liquidity amounts decide nothing about the deferred product defaults (§5a) — they exist to exercise the mechanics. What remains for the criterion is exactly the review/merge/deploy gates: this same journey through the deployed wizard UI is what the browser-level auto-qa scenario will replay once #93/#94 land.

@krandder

krandder commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Browser-level E2E now passing (node test/e2e-ui-demo.mjs, commit 4724665) — the wizard driven entirely through the real UI with Chromium against an anvil fork:

wallet connected through RainbowKit (EIP-6963-announced test wallet)
STEP 1 through UI — proposal created: 0x4120…1677 (broadcast from the page)
STEP 2 through UI — metadata written by the client as org editor
STEP 3 through UI — both conditional pools exist
invert flags VERIFIED through the UI
PASS UI — 0 Kelvin-signed txs; org owner only the pre-UI setEditor grant

Two gates proved themselves during test development: the metadata validator refused to write without a snapshot_id (the test had to fill it), and in the earlier scripted run the invert verification caught a real NO-pool inversion. Combined coverage: this UI run (steps 1–3) + e2e-fork-demo.mjs --full (LIVE seeding → Reality resolution → redeem) = the goal's full journey, demonstrated.

krandder added a commit that referenced this pull request Aug 14, 2026
@krandder
krandder marked this pull request as ready for review August 14, 2026 13:07
@krandder
krandder requested a review from nicscl August 14, 2026 13:17
@krandder

Copy link
Copy Markdown
Contributor Author

Review (2026-09-26): APPROVE after the fixes pushed today, merge after #93. (Posted as a comment because GitHub does not let the author approve their own PR.)

Today's changes:

This PR sends up to four wallet transactions: the org metadata write, the YES and NO pool creations, and a flags-only metadata update. None of them are token approvals.

P1, fixed on the branch

  1. Metadata could be attached to someone else's market. After the write, step 2 took "the last entry in the org's proposal list" as its metadata contract. A concurrent write by another editor would make step 3's "Fix flags" call updateExtendedMetadata on a different live market and invert its price. The address now comes from this transaction's ProposalCreatedAndAdded event, matched to our proposal. (c81e548)
  2. Steps 2–3 could use the wrong proposal. The step 1 fix is in Self-serve Create Market wizard: planner → real transactor (hub-e19) #93 (NewProposal event).
  3. Metadata could be written from stale form values. After a refresh the form resets to defaults, and the org, close date, Snapshot autofill and question stayed editable after step 1. Step 2 wrote those live values, so the TWAP window or org could disagree with the proposal on chain. Now:
    • the org is locked and kept in the URL (c81e548);
    • step 2 reads the proposal back from chain (question, token pair, Reality opening time);
    • it refuses to write if the tokens are not the org's;
    • it derives the close time from the on-chain opening time (143dd8f).
  4. Render loop after the first pool was created. It made step 3 unreachable. Fixed with stable callbacks and a publish-once guard (c81e548, 1647944).

Also fixed: only one metadata transaction is allowed per page load. Metadata and pool writes now switch to Gnosis and verify the chain before sending.

P2, still open (not blocking a first supervised launch)

  • Existing pools are not price-checked. If a pool already exists (for example one someone else created at a bad price), it is accepted without checking its price. createAndInitializePoolIfNecessary also succeeds silently in that case. Suggested fix: read globalState().price and block on a mismatch.
  • Snapshot lookup failures are silent. If the lookup fails or finds nothing, the typed close date is kept with no warning. Step 1 now blocks too-soon dates, and step 2 uses the on-chain time.
  • A refresh after step 2 offers the metadata write again. The org would get a duplicate entry. Suggested fix: look up existing metadata for this proposal on load.
  • Pool creation signs through window.ethereum, not the connected wagmi wallet. This is the same WalletConnect caveat as Self-serve Create Market wizard: planner → real transactor (hub-e19) #93.

P3

  • The spot-price prefill reads fetchSpotCandles as an array, but it returns {candles, price}, so the prefill never fills. This fails safe (the field is left blank).
  • Price math assumes 18/18 decimals.
  • CI runs only auto-qa/tests. The wizard's test/*.test.mjs suite needs dependencies installed and does not run in CI.

Tests: 26/26 unit, 695/695 deterministic auto-QA, and lint on the changed files all pass, and next build succeeds. The fork end-to-end demos (test/e2e-*.mjs) were not run, because anvil is not installed here.

@krandder
krandder changed the base branch from selfserve-wizard to main September 26, 2026 23:50
…19 increment C)

Step 2 (MetadataWritePanel): createAndAddProposalMetadata via the org
owner/editor key — role gate read on-chain, simulate-first, TWAP timing
recomputed at write time (never a page-load-stale draft), invert flags
written false/false pending step-3 verification. Args assembled by the
shared buildProposalMetadataArgs tuple.

Step 3 (PoolsPanel): conditional tokens read via the existing
fetchProposalFromChain (wrappedOutcome 0-3), YES/NO pools created through
useCreatePool with a spot-prefilled, operator-confirmed initial price
(wrong initial price = the seeded-1000x-off bug), then the verification
pass: read each pool's token0, validate invert flags against the
CONDITIONAL company token, and offer a single flags-only
updateExtendedMetadata built with mergeMetadataForUpdate — the KIP-88/90
inverted-price bug class dies here.

Steps unlock from ?proposal=0x… (resume after refresh) or the step-1
transactor result. Pure helpers in orgMetadataWrite.js; 17/17 unit tests.
Spec: hub docs/specs/2026-08-06-selfserve-wizard-increment-C.md.

ponytail: per-action stepper UI and the anvil-fork E2E scenario are the
known ceiling — E2E lands with the auto-qa harness before merge.
node test/e2e-fork-demo.mjs spawns an anvil fork of Gnosis and drives the
complete self-serve journey through the same modules the wizard UI calls:
createProposal (shared calldata builder) -> org metadata write by the
client as editor (timing recomputed, past-start gate asserted) -> both
conditional pools (Algebra NFPM, wrappedOutcome tokens) -> invert
verification against real token0 with a flags-only corrective
updateExtendedMetadata.

First run PASSED and caught a live inversion: the NO pool's token order
required invertTwapPoolNo=true; the verification pass fixed it in one tx.
6 txs total, 0 from Kelvin's EOA (asserted), org owner appears exactly
once (the mixed-mode setEditor grant, asserted).
…(hub-e19 increment D)

Pasting a Snapshot proposal hash fetches the vote's end from
hub.snapshot.org and autofills closeTimestamp; deriveTwapTiming anchors
the 0xAlex window to it and openingTime lands 48h after close. Fails
closed (bad id / unknown proposal / hub down -> no change, manual entry
stands). With this, the wizard computes every timing field from the vote
itself — the hand-aligned windows that shipped twice can't recur.

19/19 unit tests (new: autofill epoch fetch, fail-closed cases).
… (hub-e19)

Extends the E2E demo through the goal's full success-criterion mechanics:
whale-funded client splits collateral via FutarchyRouter, seeds both
conditional pools full-range (~$10.8k currency side), floor gate reads
LIVE (1.83% < 3%); then warps past openingTime, posts the Reality answer
with a 1 xDAI bond, warps the 3.5d timeout, resolves permissionlessly via
the factory's realityProxy, reads the CTF payout vector, and redeems the
winning side back to collateral (1,200 sDAI returned).

21 txs total, 0 from Kelvin's EOA (asserted), org owner only the one-time
setEditor grant (asserted). Demo amounts decide nothing about the
deferred product liquidity defaults (dossier 5a).
Run: node test/e2e-fork-demo.mjs --full
…e19)

node test/e2e-ui-demo.mjs serves the built static export, routes every
production Gnosis RPC to an anvil fork, injects an EIP-6963-announced
EIP-1193 wallet backed by anvil's unlocked client account, and drives the
real interface with Chromium: RainbowKit connect -> broadcast
createProposal -> metadata write as org editor (the validator correctly
blocked a missing snapshot_id until filled) -> both conditional pools ->
invert-flag verification. Zero Kelvin-signed txs; org owner only the
pre-UI setEditor grant.

Reuses the auto-qa harness's playwright install read-only. LIVE seeding +
resolution mechanics remain covered by e2e-fork-demo.mjs --full.
- Step 2 now takes the metadata contract from this transaction's
  ProposalCreatedAndAdded event (matched to our proposal) instead of the last
  entry in the organization's list, which another write could have changed.
- The organization is locked once a proposal exists and is kept in the URL,
  so a resumed wizard cannot write metadata to a different organization.
- Pool creation reports each new pool once through stable callbacks; before,
  it re-rendered forever after the first pool was created.
- Metadata and pool writes switch to Gnosis and check the chain first.
After a refresh the form resets to defaults, and the close date, Snapshot
autofill and question stay editable after step 1. Step 2 used those live
values, so it could write a TWAP window or question that did not match the
proposal. Step 2 now reads the proposal back from chain (question, token
pair, Reality opening time), refuses to write if the tokens are not the
selected organization's, and derives the close time from the opening time.
It also allows only one metadata transaction per page load, so a retry
cannot add a duplicate entry to the organization.
@netlify

netlify Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for futarchy ready!

Name Link
🔨 Latest commit cc3be7d
🔍 Latest deploy log https://app.netlify.com/projects/futarchy/deploys/6ab85acff8cdc900081f7c8c
😎 Deploy Preview https://deploy-preview-94--futarchy.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@krandder
krandder merged commit c90df73 into main Sep 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant