From 5239ab7df6352b9b233db8d34348429404a6463d Mon Sep 17 00:00:00 2001 From: dcccrypto Date: Thu, 3 Sep 2026 10:05:21 +0100 Subject: [PATCH] fix(#2464): don't register the market until insurance has verified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The batched launch STARTED the keeper-register call, then ran the M3b insurance broadcast and its on-chain verification, then awaited the call. So the registration request was in flight — and could already have completed — while the check was still deciding whether the launch had failed. A launch that threw "Insurance fund was not seeded" could therefore leave the market listed and keeper-registered anyway: on-chain state, app registry, keeper service and the user-visible result all disagreeing. Fixed by making it a thunk invoked AFTER the insurance gate rather than a promise started before it. THE WINDOW IS UNCHANGED AND STILL SATISFIED, which is what made this fixable at all. The source documents both bounds: * after M3a — so a market is listed only once it actually holds collateral. This is the ANSEM zombie: registration used to fire right after M1, and since "create the market" always succeeds, EVERY failed launch left a listed, unfunded, untradeable market behind. * before M4 — StakeInitPool rotates marketauth away from the deployer, and keeper-register's H1 check requires marketauth to still equal it. The insurance check sits inside that window, so registration only had to move later within it, not out of it. THE COST IS LATENCY, stated rather than hidden: the round-trip no longer overlaps M3b's broadcast, so the tail sits longer against the shared blockhash. That is the right trade — the blockhash-expiry path already REBUILDS a not-yet-landed tail tx against a fresh blockhash (TailTxDescriptor), whereas an incorrectly published market has no equivalent undo. Two existing source-position tests pinned the OLD ordering and are updated to assert the invocation rather than the declaration — and TIGHTENED: the zombie guard now also requires registration to follow the insurance gate, which is the #2464 property itself. Negative control: restoring the eager start fails both. Launch suite: 3149 passed / 16 skipped / 0 failed. Refs: dcccrypto/percolator-launch#2464 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NgoNgagkvw7i5SSRC3FJ8D --- ...eMarket-fresh-batched-registration.test.ts | 20 ++++++++++++++-- app/hooks/useCreateMarket.ts | 24 +++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/app/__tests__/hooks/useCreateMarket-fresh-batched-registration.test.ts b/app/__tests__/hooks/useCreateMarket-fresh-batched-registration.test.ts index 37c27c91..2e64e4c9 100644 --- a/app/__tests__/hooks/useCreateMarket-fresh-batched-registration.test.ts +++ b/app/__tests__/hooks/useCreateMarket-fresh-batched-registration.test.ts @@ -57,10 +57,22 @@ describe("useCreateMarket fresh batched registration", () => { // keeper-register now writes the markets row, so THE REGISTRATION CALL is // what must come after M3a. It used to be created before M2, which was safe // only while it wrote nothing but the keeper's blob. + // + // #2464 TIGHTENED THIS. The call used to be STARTED here and awaited later, + // so it was in flight — and could have completed — while the insurance check + // was still deciding whether the launch had failed. It is now a thunk + // (`startKeeperRegister`) INVOKED after that check, so assert on the + // invocation, which is the moment the request actually leaves. const m3a = freshBatchSource.indexOf("const m3aSig = await broadcastTailTx(2)"); - const register = freshBatchSource.indexOf("const keeperRegisterPromise"); + const register = freshBatchSource.indexOf("await startKeeperRegister()"); expect(m3a).toBeGreaterThanOrEqual(0); expect(register).toBeGreaterThan(m3a); + + // And the point of #2464: it must also come after the insurance verification, + // so a launch that throws there publishes nothing. + const insuranceGate = freshBatchSource.indexOf("Insurance fund was not seeded"); + expect(insuranceGate).toBeGreaterThanOrEqual(0); + expect(register).toBeGreaterThan(insuranceGate); }); it("keeps keeper-registration before M4, where marketauth still works", () => { @@ -68,7 +80,11 @@ describe("useCreateMarket fresh batched registration", () => { // rotates marketauth away from the deployer, and keeper-register's H1 check // requires marketauth to still equal the deployer. Pinning the order stops // someone "fixing" the asymmetry and silently breaking keeper registration. - const keeper = freshBatchSource.indexOf("const keeperRegisterPromise"); + // + // #2464 narrowed the window from "after M3a" to "after the insurance check", + // but this upper bound is unchanged and is the reason it could not move any + // later. Asserting on the INVOCATION, not the thunk's declaration. + const keeper = freshBatchSource.indexOf("await startKeeperRegister()"); const m4 = freshBatchSource.indexOf("const m4Sig = await broadcastTailTx(4)"); expect(keeper).toBeGreaterThanOrEqual(0); expect(m4).toBeGreaterThan(keeper); diff --git a/app/hooks/useCreateMarket.ts b/app/hooks/useCreateMarket.ts index d57925ae..52ef472c 100644 --- a/app/hooks/useCreateMarket.ts +++ b/app/hooks/useCreateMarket.ts @@ -1434,7 +1434,24 @@ async function attemptFreshBatchedLaunch(ctx: FreshBatchContext): Promise = + // #2464: this used to be STARTED here and awaited after the insurance check, + // so the registration request was already in flight — and could already have + // completed — while that check was still deciding whether the launch had + // failed. A launch that threw on an unseeded insurance fund could therefore + // leave the market listed and keeper-registered anyway. + // + // It is now a THUNK, invoked below only once insurance has verified. The + // window it must run in is unchanged and still satisfied: after M3a (so a + // market is only listed once it holds collateral) and before M4 (whose + // StakeInitPool rotates marketauth away from the deployer, which + // keeper-register's H1 check requires). + // + // The cost is latency: the round-trip no longer overlaps M3b's broadcast, so + // the tail sits a little longer against the shared blockhash. That is the + // right trade — the blockhash-expiry path already REBUILDS a not-yet-landed + // tail tx (see TailTxDescriptor), whereas an incorrectly published market has + // no equivalent undo. + const startKeeperRegister = (): Promise => (isKeeperOracle && params.dexPoolAddress && keeperProofSignature) ? registerMarketWithKeeper( { publicKey: walletPk, signMessage: wallet.signMessage }, @@ -1504,7 +1521,10 @@ async function attemptFreshBatchedLaunch(ctx: FreshBatchContext): Promise