Summary
The fresh batched market-launch flow can start two externally visible registration side effects before the mandatory insurance seed has been successfully verified:
- registration in the markets database;
- keeper registration for keeper-oracle markets.
If M3b fails, or the resulting on-chain insuranceBalance remains below the requested amount, the launch correctly throws an insurance-seed error. However, the database and keeper requests may already be in flight or may already have completed.
This can leave a market visible or keeper-registered even though the launch is reported as failed because its mandatory insurance invariant was not satisfied.
Severity
Medium
This report does not demonstrate direct fund theft, unauthorized signing, an on-chain authorization bypass, or protocol insolvency.
The impact is an inconsistent launch state across the on-chain market, application registry, keeper service, and user-visible launch result.
Affected baseline
Repository: dcccrypto/percolator-launch
Branch: playground
Commit: ce15a20a039ded7248204bed30f45952fb69a954
Affected path
app/hooks/useCreateMarket.ts
Affected function:
attemptFreshBatchedLaunch()
Root cause
The fresh batched flow correctly treats the requested insurance seed as mandatory:
if (insuranceOnChain < params.insuranceAmount) {
throw new Error(
`Insurance fund was not seeded ...`,
);
}
However, both external registration operations are initiated before that invariant has passed.
The affected ordering is effectively:
create keeperRegisterPromise
...
broadcast M3a
start marketsRegisterPromise
broadcast M3b
read insuranceBalance
throw if insuranceBalance < requested amount
await keeperRegisterPromise
broadcast M4
await marketsRegisterPromise
Promises begin executing when they are created. Awaiting them later does not defer or cancel the underlying registration request.
Therefore, an M3b failure or an under-seeded insurance balance can occur after one or both registrations have already started.
Expected invariant
No externally visible registration should begin until the required insurance outcome has been verified.
The intended ordering is:
M3a
M3b
read and verify insuranceBalance
start markets database registration
start keeper registration
await keeper registration
M4
await markets database registration
Keeper registration must remain before M4 because StakeInitPool rotates marketauth, while keeper registration requires the deployer authority to remain valid.
Impact
A failed mandatory insurance seed can produce conflicting state:
launch result:
failed — insurance was not seeded
on-chain market:
created but under-insured
markets database:
registration may already have succeeded
keeper service:
registration may already have succeeded
Consequences may include:
- an under-insured market becoming visible in market discovery;
- keeper infrastructure accepting a market whose launch was reported as failed;
- retries operating against partially published external state;
- confusing recovery behavior for creators and operators;
- application state claiming that a market is available before all mandatory launch invariants have passed.
This report is intentionally not classified as High or Critical because no direct loss-of-funds path has been demonstrated.
Deterministic reproduction
A non-network regression test was added to the existing fresh-batched source-ordering suite:
app/__tests__/hooks/useCreateMarket-fresh-batched-registration.test.ts
The new assertions require:
expect(register).toBeGreaterThan(insuranceFailure);
expect(keeper).toBeGreaterThan(insuranceFailure);
expect(keeper).toBeLessThan(m4);
Before the fix
Command:
cd app
pnpm exec vitest run \
__tests__/hooks/useCreateMarket-fresh-batched-registration.test.ts
Result:
Test Files 1 failed
Tests 2 failed | 6 passed
Observed source positions:
keeper registration : 32745
market registration : 33973
insurance failure : 35413
Both registrations were located before the mandatory insurance-failure boundary.
All six pre-existing tests in the file remained green, confirming that the reproduction was isolated to the new ordering invariants.
Proposed fix
Move the invocation of:
const marketsRegisterPromise = registerMarketInDb();
and creation of:
const keeperRegisterPromise = ...
to immediately after the mandatory insurance-verification block and before M4.
Do not move or modify:
- signed keeper-cosign handling;
- M1, M2, M3a, M3b, or M4 transaction construction;
- sequential/recovery flow;
- registration-payload construction;
- API route behavior;
- the
insuranceAmount === 0n best-effort crank behavior.
Regression result after the fix
The focused test changes from:
to:
Test Files 1 passed
Tests 8 passed
Additional adjacent tests:
insurance-idempotency.test.ts
useCreateMarket-keeper-register.test.ts
useCreateMarket.v17-matcher-recovery.test.ts
useCreateMarket.v17-matcher-resume.test.ts
useCreateMarket.feesplit.test.ts
Result:
Test Files 5 passed
Tests 19 passed
TypeScript:
pnpm exec tsc --noEmit
PASS
Production build:
pnpm build
PASS
Exit code: 0
Full-suite comparison
The repository currently contains unrelated pre-existing wallet/UI test failures.
A clean detached baseline worktree at commit:
ce15a20a039ded7248204bed30f45952fb69a954
was tested separately using the same dependency and environment setup.
Clean baseline
Test Files 4 failed | 260 passed | 1 skipped
Tests 14 failed | 2822 passed | 16 skipped
Patched worktree
Test Files 4 failed | 260 passed | 1 skipped
Tests 14 failed | 2824 passed | 16 skipped
The additional two passing tests are the two new ordering regressions.
The complete set of 14 failures was identical between baseline and patched worktrees and remained limited to:
__tests__/components/ConnectButton.test.tsx
__tests__/components/Header.test.tsx
__tests__/components/Portfolio.test.tsx
__tests__/hooks/useWallet.test.ts
No new failing test was introduced by this patch.
Duplicate and active-PR analysis
Searches across open and closed issues and pull requests were performed using:
insurance registration M3b
registerMarketInDb insurance
publish before insurance
under-insured registration
keeper registration insurance verification
No report addressing this exact ordering invariant was found.
Two open pull requests touched one of the same file paths:
This is a wallet/test-suite refactor. Its historical changed-file list included the fresh-batched registration test and useCreateMarket.ts, but its remaining delta against current playground is unrelated to the insurance-registration ordering.
It does not implement this fix.
This fixes mainnet Hyperp oracle-mode resolution in a different hunk of useCreateMarket.ts.
It does not modify the M3b insurance-registration ordering and is not a duplicate.
A temporary combined worktree was created by applying PR #2434 to current playground and then applying this insurance patch.
Compatibility result:
PR #2434 applies cleanly to current playground
insurance patch applies cleanly on top of PR #2434
no conflict markers
unique oracle and registration call sites
Combined validation:
Targeted tests : 52 passed
TypeScript : PASS
Production build: PASS
Diff checks : PASS
Scope of the proposed patch
Files changed:
app/hooks/useCreateMarket.ts
app/__tests__/hooks/useCreateMarket-fresh-batched-registration.test.ts
The patch does not modify:
- API routes;
- database schema;
- keeper-route authentication;
- transaction signing;
- wallet authorization;
- Solana instruction encoding;
- on-chain program code;
- oracle-price handling;
- sequential launch recovery;
- registration-payload format.
Suggested acceptance criteria
- Markets-database registration does not begin before mandatory insurance verification passes.
- Keeper registration does not begin before mandatory insurance verification passes.
- Keeper registration still completes before M4.
- Insurance amount
0 retains its current best-effort crank behavior.
- Existing M3a collateral and zombie-market guards remain intact.
- Focused regression suite passes.
- TypeScript and production build pass.
Summary
The fresh batched market-launch flow can start two externally visible registration side effects before the mandatory insurance seed has been successfully verified:
If M3b fails, or the resulting on-chain
insuranceBalanceremains below the requested amount, the launch correctly throws an insurance-seed error. However, the database and keeper requests may already be in flight or may already have completed.This can leave a market visible or keeper-registered even though the launch is reported as failed because its mandatory insurance invariant was not satisfied.
Severity
Medium
This report does not demonstrate direct fund theft, unauthorized signing, an on-chain authorization bypass, or protocol insolvency.
The impact is an inconsistent launch state across the on-chain market, application registry, keeper service, and user-visible launch result.
Affected baseline
Affected path
Affected function:
Root cause
The fresh batched flow correctly treats the requested insurance seed as mandatory:
However, both external registration operations are initiated before that invariant has passed.
The affected ordering is effectively:
Promises begin executing when they are created. Awaiting them later does not defer or cancel the underlying registration request.
Therefore, an M3b failure or an under-seeded insurance balance can occur after one or both registrations have already started.
Expected invariant
No externally visible registration should begin until the required insurance outcome has been verified.
The intended ordering is:
Keeper registration must remain before M4 because
StakeInitPoolrotatesmarketauth, while keeper registration requires the deployer authority to remain valid.Impact
A failed mandatory insurance seed can produce conflicting state:
Consequences may include:
This report is intentionally not classified as High or Critical because no direct loss-of-funds path has been demonstrated.
Deterministic reproduction
A non-network regression test was added to the existing fresh-batched source-ordering suite:
The new assertions require:
Before the fix
Command:
Result:
Observed source positions:
Both registrations were located before the mandatory insurance-failure boundary.
All six pre-existing tests in the file remained green, confirming that the reproduction was isolated to the new ordering invariants.
Proposed fix
Move the invocation of:
and creation of:
to immediately after the mandatory insurance-verification block and before M4.
Do not move or modify:
insuranceAmount === 0nbest-effort crank behavior.Regression result after the fix
The focused test changes from:
to:
Additional adjacent tests:
Result:
TypeScript:
Production build:
Full-suite comparison
The repository currently contains unrelated pre-existing wallet/UI test failures.
A clean detached baseline worktree at commit:
was tested separately using the same dependency and environment setup.
Clean baseline
Patched worktree
The additional two passing tests are the two new ordering regressions.
The complete set of 14 failures was identical between baseline and patched worktrees and remained limited to:
No new failing test was introduced by this patch.
Duplicate and active-PR analysis
Searches across open and closed issues and pull requests were performed using:
No report addressing this exact ordering invariant was found.
Two open pull requests touched one of the same file paths:
PR #2451
This is a wallet/test-suite refactor. Its historical changed-file list included the fresh-batched registration test and
useCreateMarket.ts, but its remaining delta against currentplaygroundis unrelated to the insurance-registration ordering.It does not implement this fix.
PR #2434
This fixes mainnet Hyperp oracle-mode resolution in a different hunk of
useCreateMarket.ts.It does not modify the M3b insurance-registration ordering and is not a duplicate.
A temporary combined worktree was created by applying PR #2434 to current
playgroundand then applying this insurance patch.Compatibility result:
Combined validation:
Scope of the proposed patch
Files changed:
The patch does not modify:
Suggested acceptance criteria
0retains its current best-effort crank behavior.