Skip to content

[MEDIUM] Batched launch can publish and keeper-register before mandatory insurance verification #2464

Description

@Bayyan16

Summary

The fresh batched market-launch flow can start two externally visible registration side effects before the mandatory insurance seed has been successfully verified:

  1. registration in the markets database;
  2. 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:

6 passed
2 failed

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:

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 current playground is 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 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions