Summary
The /my-markets page currently treats creator ownership discovery as complete as soon as general market discovery finishes, even though useCreatedMarkets() still performs a separate asynchronous creator ownership scan through connection.getProgramAccounts().
Because this ownership scan is not represented by a wallet-scoped loading, error, or verified-result lifecycle, the page can render three incorrect states:
- A false empty state while the creator ownership scan is still pending.
- A confirmed-looking empty state after the creator ownership RPC scan fails.
- Markets belonging to the previously connected wallet after the active wallet has already changed.
This causes /my-markets to present pending, failed, or stale creator ownership data as if it were a final and successfully verified result.
Severity
Medium
Category
- UI/UX
- Asynchronous state lifecycle
- Wallet-scoped data correctness
- RPC error handling
- Stale state isolation
Affected Branch
playground
Validated Baseline
Commit: c626d4e
Branch: upstream/playground
Behind/Ahead: 0 / 0
The issue was reproduced against the latest synchronized playground baseline available during validation.
Affected Components
app/hooks/useCreatedMarkets.ts
app/app/my-markets/page.tsx
app/__tests__/components/MyMarkets.test.tsx
Technical Background
Completed markets cannot always be identified by comparing the connected wallet with the market authority field.
During market launch, the original market authority may be rotated away from the creator wallet to a program-derived address. To continue identifying completed markets created by the connected wallet, useCreatedMarkets() performs an additional creator ownership scan using LP portfolio accounts returned by:
connection.getProgramAccounts(...)
This LP portfolio scan is therefore an essential part of creator market discovery for completed markets.
However, the hook currently exposes only the general market discovery lifecycle:
return {
myMarkets: enrichedMarkets,
loading: discoveryLoading,
error,
connected: !!publicKey,
refetch: discoveryRefetch,
};
The asynchronous creator ownership scan is not represented in the returned loading, error, or wallet identity state.
Root Cause
1. Creator ownership scanning is not included in the loading lifecycle
After general market discovery completes, the hook starts a separate asynchronous creator ownership scan.
This process includes asynchronous operations such as:
connection.getProgramAccounts(...)
resolveLabel(...)
Despite those operations still being pending, the hook returns:
loading: discoveryLoading
This allows loading to become false before creator ownership has been verified.
The current inline comment also states that loading is based only on discovery and that no second owner RPC scan is present, even though getProgramAccounts() is still executed by the implementation.
The effective lifecycle is therefore:
General market discovery completes
→ discoveryLoading becomes false
→ creator ownership scan is still pending
→ useCreatedMarkets.loading becomes false
→ createdMarkets may still be empty or stale
2. Creator ownership RPC failures are silently swallowed
The owner scan catches RPC failures without propagating an error:
try {
const owned = await connection.getProgramAccounts(...);
// Process creator ownership accounts.
} catch {
// Failure is ignored.
}
A failed RPC request is therefore indistinguishable from a successful scan that found no creator portfolios.
The hook continues returning only the error provided by general market discovery:
As a result, an ownership verification failure can be rendered as a valid empty result.
For completed markets, this is particularly important because the authority-derived result may no longer recognize the original creator after the authority has been rotated.
The effective failure path becomes:
Authority comparison does not identify the completed market
→ LP creator ownership scan fails
→ RPC error is swallowed
→ error remains null
→ myMarkets remains empty
→ page renders a confirmed-looking empty state
3. Creator results are not scoped to the wallet that produced them
Creator markets are stored in a shared state array:
const [createdMarkets, setCreatedMarkets] = useState<CreatedMarket[]>([]);
The stored result does not retain the wallet identity or scan identity that produced it.
When the connected wallet changes:
the previously committed createdMarkets state remains available while the ownership scan for wallet B is running.
The existing cancellation guard helps prevent an older asynchronous completion from overwriting a newer result after an effect cleanup. However, it does not prevent already committed wallet A results from being rendered during wallet B's pending scan.
This is a stale state retention issue, not a late-response overwrite race.
The effective wallet-switch lifecycle is:
Wallet A creator markets are loaded
→ user switches to wallet B
→ wallet B ownership scan starts
→ wallet B scan remains pending
→ wallet A markets remain exposed through myMarkets
4. /my-markets treats unresolved results as final
The page currently renders its states in this order:
if (loading) return <LoadingSkeleton />;
if (error) {
return /* error state */;
}
if (myMarkets.length === 0) {
return /* empty state */;
}
Because the creator ownership scan is not included in loading or error, the page can display:
you haven't created a market with this wallet yet.
while creator ownership verification is still pending or has failed.
The page has no way to distinguish among:
The scan succeeded and found zero markets
The scan has not finished
The scan failed
The displayed markets belong to the previous wallet
User Impact
Scenario 1: False empty state while creator scanning is pending
A creator opens /my-markets.
General market discovery completes before the LP creator ownership scan.
The hook returns:
loading = false
error = null
myMarkets = []
even though getProgramAccounts() is still pending.
The page displays an empty-state message indicating that the connected wallet has never created a market.
This can cause the user to incorrectly conclude that:
- their launched market disappeared;
- the launch process did not complete;
- they connected the wrong wallet;
- creator ownership information was lost;
/my-markets no longer recognizes their completed market.
Scenario 2: RPC failure is presented as a successful empty result
The creator ownership RPC scan rejects because of a transient RPC, provider, or network failure.
The error is caught and ignored.
The hook still returns:
loading = false
error = null
myMarkets = []
The UI therefore presents an infrastructure failure as a valid and confirmed empty result.
This state may remain visible until a later scan succeeds.
The user is not given:
- a retryable error;
- a degraded-state warning;
- an indication that ownership verification failed;
- a distinction between failed verification and a valid empty result.
Scenario 3: Wallet A markets remain visible after switching to wallet B
- Wallet A opens
/my-markets.
- Wallet A's creator markets are successfully loaded.
- The user switches to wallet B.
- The creator ownership scan for wallet B starts.
- The wallet B scan remains pending.
- The hook continues exposing wallet A's previously verified markets.
The page temporarily attributes wallet A's markets to the wallet B session.
On-chain authorization should still reject unauthorized administrative transactions, so this is not an authorization bypass. However, the dashboard displays an incorrect wallet ownership context and may direct the user toward controls for markets that do not belong to the active wallet.
Proof of Concept
Three regression tests were added without modifying production source code.
PoC 1: Pending creator scan does not preserve loading state
it('keeps loading while creator ownership scan is pending', async () => {
const market = createMockV12Market(pk(108), pk(224));
mockMarkets.push(market);
mockUseMarketDiscovery.mockReturnValue({
markets: [market],
loading: false,
error: null,
refetch: vi.fn(),
});
mockConnection.getProgramAccounts.mockImplementationOnce(
() => new Promise<never[]>(() => {}),
);
const { result } = renderHook(() => useCreatedMarkets());
await waitFor(() => {
expect(mockConnection.getProgramAccounts).toHaveBeenCalledTimes(1);
});
expect(result.current.loading).toBe(true);
});
Baseline Result
AssertionError: expected false to be true
Expected: true
Received: false
This proves that creator ownership verification is still pending while the hook already reports loading === false.
PoC 2: Ownership RPC failure is not exposed
it('surfaces creator ownership scan failures instead of confirming an empty result', async () => {
const market = createMockV12Market(pk(109), pk(225));
mockMarkets.push(market);
mockUseMarketDiscovery.mockReturnValue({
markets: [market],
loading: false,
error: null,
refetch: vi.fn(),
});
mockConnection.getProgramAccounts.mockRejectedValueOnce(
new Error('creator ownership RPC scan failed'),
);
const { result } = renderHook(() => useCreatedMarkets());
await waitFor(() => {
expect(mockConnection.getProgramAccounts).toHaveBeenCalledTimes(1);
});
await waitFor(
() => {
expect(result.current.error).not.toBeNull();
},
{ timeout: 500 },
);
expect(result.current.myMarkets).toEqual([]);
});
Baseline Result
AssertionError: expected null not to be null
Expected: a non-null error
Received: null
This proves that an ownership scan failure is silently converted into a confirmed-looking empty result.
PoC 3: Previous-wallet markets remain exposed during the new wallet scan
it('withholds wallet A markets while wallet B ownership scan is pending', async () => {
const walletA = mockPublicKey;
const walletB = pk(226);
const marketA = createMockV12Market(pk(110), walletA);
mockMarkets.push(marketA);
mockUseMarketDiscovery.mockReturnValue({
markets: [marketA],
loading: false,
error: null,
refetch: vi.fn(),
});
mockConnection.getProgramAccounts
.mockResolvedValueOnce([])
.mockImplementationOnce(() => new Promise<never[]>(() => {}));
const { result, rerender } = renderHook(() => useCreatedMarkets());
await waitFor(() => {
expect(result.current.myMarkets).toHaveLength(1);
});
expect(mockConnection.getProgramAccounts).toHaveBeenCalledTimes(1);
mockUseWallet.mockReturnValue({
publicKey: walletB,
connected: true,
});
rerender();
await waitFor(() => {
expect(mockConnection.getProgramAccounts).toHaveBeenCalledTimes(2);
});
expect(result.current.myMarkets).toEqual([]);
expect(result.current.loading).toBe(true);
});
Baseline Result
Expected: []
Received: [marketA]
This proves that wallet A's creator markets remain exposed after wallet B becomes active and its ownership scan has already started.
Test Results
Before adding the PoCs, the existing test file passed completely:
Test Files: 1 passed
Tests: 25 passed
After adding the three test-only PoCs:
Test Files: 1 failed
Tests: 3 failed | 25 passed
Total: 28 tests
Only the three newly added PoCs failed.
All 25 pre-existing tests continued to pass, demonstrating that the failures are isolated to the missing creator ownership scan lifecycle.
No production source file was modified during reproduction.
The only modified file was:
app/__tests__/components/MyMarkets.test.tsx
Reproduction Command
cd app
pnpm exec vitest run __tests__/components/MyMarkets.test.tsx \
--reporter=verbose
Reproduced Result
Test Files 1 failed
Tests 3 failed | 25 passed (28)
Actual Behavior
loading becomes false before creator ownership discovery finishes.
- Creator ownership RPC failures are swallowed.
error remains null after the ownership scan fails.
- Pending and failed scans are treated as verified results.
- An empty array from a failed scan is indistinguishable from a successful empty scan.
- Markets from the previous wallet remain visible while the next wallet's ownership scan is pending.
/my-markets may display a false “no markets created” message.
- Wallet-scoped creator result isolation is missing.
Expected Behavior
- Creator ownership discovery must be included in the hook's loading lifecycle.
/my-markets must not render its empty state before creator verification succeeds.
- Creator ownership RPC failures must produce a recoverable error or explicitly degraded state.
- A failed scan must not be treated as a successful empty scan.
- Creator results must be associated with the wallet and scan identity that produced them.
- Results from wallet A must not be rendered after wallet B becomes active.
- Stale asynchronous completions must be ignored.
- Empty state must appear only after all required ownership scans succeed and return no creator markets.
- Manual refresh must rerun both general market discovery and creator ownership discovery.
Suggested Fix Direction
Introduce a wallet-scoped creator ownership scan state instead of storing only a shared market array.
For example:
type CreatorScanState = {
scanKey: string | null;
walletKey: string | null;
status: 'idle' | 'loading' | 'success' | 'error';
markets: CreatedMarket[];
error: string | null;
};
The scan identity should include at least:
Connected wallet
Discovered slab set
Program ID set
Manual refresh generation
Recommended lifecycle:
- Derive a creator scan key for the currently connected wallet and discovered market set.
- Treat a missing or mismatched scan key as pending immediately during render.
- Do not expose creator results produced for another wallet.
- Set creator scan status to
loading before starting ownership RPC calls.
- Commit scan results only if the completion still matches the active scan key.
- Surface complete or partial RPC failures instead of converting them into verified-empty results.
- Preserve the last successful snapshot only when it belongs to the same wallet and the intended UX explicitly supports stale-but-verified data.
- Keep
/my-markets in a loading or retryable-error state until creator ownership verification completes.
- Make
refetch() rerun both general discovery and creator ownership scanning.
Using only setLoading(true) inside the effect may still allow a transient incorrect render because React effects execute after render.
The visible result should therefore be gated by the current wallet and scan identity, not only by an effect-controlled boolean.
Acceptance Criteria
Non-Duplication Review
This issue is distinct from earlier changes that introduced LP portfolio scanning to recover completed markets whose authority had been rotated away during launch.
Those earlier changes addressed:
Completed creator markets were not detected through market authority alone.
This issue addresses:
The asynchronous LP creator scan has no wallet-scoped loading, error, or verified-result lifecycle.
It is also distinct from portfolio owner-scan fixes related to cached active-position snapshots.
Those changes concern trading portfolio visibility and shared scan-cache behavior, while this issue concerns creator market discovery in:
useCreatedMarkets()
/my-markets
PRs related to logo-upload authorization are also not duplicates. Those changes evaluate whether LP portfolio signals are suitable for a server-side authorization boundary.
This report does not claim an authorization bypass and focuses exclusively on client-side creator dashboard correctness.
No open or closed issue or PR with the same affected hook, user-facing symptoms, and root cause was identified during the duplication review.
Security Scope Clarification
This report does not claim:
- an on-chain authorization bypass;
- unauthorized transaction execution;
- private data disclosure;
- direct fund loss;
- ownership transfer;
- administrative privilege escalation.
On-chain authorization remains the final enforcement boundary.
The impact is a wallet-scoped UI and data-integrity failure that can:
- display stale creator ownership information;
- temporarily attribute wallet A markets to wallet B;
- hide valid completed creator markets;
- represent an RPC verification failure as a successful empty result;
- mislead users about whether their markets still exist or are associated with the connected wallet.
Summary
The
/my-marketspage currently treats creator ownership discovery as complete as soon as general market discovery finishes, even thoughuseCreatedMarkets()still performs a separate asynchronous creator ownership scan throughconnection.getProgramAccounts().Because this ownership scan is not represented by a wallet-scoped loading, error, or verified-result lifecycle, the page can render three incorrect states:
This causes
/my-marketsto present pending, failed, or stale creator ownership data as if it were a final and successfully verified result.Severity
Medium
Category
Affected Branch
playgroundValidated Baseline
The issue was reproduced against the latest synchronized
playgroundbaseline available during validation.Affected Components
Technical Background
Completed markets cannot always be identified by comparing the connected wallet with the market authority field.
During market launch, the original market authority may be rotated away from the creator wallet to a program-derived address. To continue identifying completed markets created by the connected wallet,
useCreatedMarkets()performs an additional creator ownership scan using LP portfolio accounts returned by:This LP portfolio scan is therefore an essential part of creator market discovery for completed markets.
However, the hook currently exposes only the general market discovery lifecycle:
The asynchronous creator ownership scan is not represented in the returned
loading,error, or wallet identity state.Root Cause
1. Creator ownership scanning is not included in the loading lifecycle
After general market discovery completes, the hook starts a separate asynchronous creator ownership scan.
This process includes asynchronous operations such as:
Despite those operations still being pending, the hook returns:
loading: discoveryLoadingThis allows
loadingto becomefalsebefore creator ownership has been verified.The current inline comment also states that loading is based only on discovery and that no second owner RPC scan is present, even though
getProgramAccounts()is still executed by the implementation.The effective lifecycle is therefore:
2. Creator ownership RPC failures are silently swallowed
The owner scan catches RPC failures without propagating an error:
A failed RPC request is therefore indistinguishable from a successful scan that found no creator portfolios.
The hook continues returning only the error provided by general market discovery:
errorAs a result, an ownership verification failure can be rendered as a valid empty result.
For completed markets, this is particularly important because the authority-derived result may no longer recognize the original creator after the authority has been rotated.
The effective failure path becomes:
3. Creator results are not scoped to the wallet that produced them
Creator markets are stored in a shared state array:
The stored result does not retain the wallet identity or scan identity that produced it.
When the connected wallet changes:
the previously committed
createdMarketsstate remains available while the ownership scan for wallet B is running.The existing cancellation guard helps prevent an older asynchronous completion from overwriting a newer result after an effect cleanup. However, it does not prevent already committed wallet A results from being rendered during wallet B's pending scan.
This is a stale state retention issue, not a late-response overwrite race.
The effective wallet-switch lifecycle is:
4.
/my-marketstreats unresolved results as finalThe page currently renders its states in this order:
Because the creator ownership scan is not included in
loadingorerror, the page can display:while creator ownership verification is still pending or has failed.
The page has no way to distinguish among:
User Impact
Scenario 1: False empty state while creator scanning is pending
A creator opens
/my-markets.General market discovery completes before the LP creator ownership scan.
The hook returns:
even though
getProgramAccounts()is still pending.The page displays an empty-state message indicating that the connected wallet has never created a market.
This can cause the user to incorrectly conclude that:
/my-marketsno longer recognizes their completed market.Scenario 2: RPC failure is presented as a successful empty result
The creator ownership RPC scan rejects because of a transient RPC, provider, or network failure.
The error is caught and ignored.
The hook still returns:
The UI therefore presents an infrastructure failure as a valid and confirmed empty result.
This state may remain visible until a later scan succeeds.
The user is not given:
Scenario 3: Wallet A markets remain visible after switching to wallet B
/my-markets.The page temporarily attributes wallet A's markets to the wallet B session.
On-chain authorization should still reject unauthorized administrative transactions, so this is not an authorization bypass. However, the dashboard displays an incorrect wallet ownership context and may direct the user toward controls for markets that do not belong to the active wallet.
Proof of Concept
Three regression tests were added without modifying production source code.
PoC 1: Pending creator scan does not preserve loading state
Baseline Result
This proves that creator ownership verification is still pending while the hook already reports
loading === false.PoC 2: Ownership RPC failure is not exposed
Baseline Result
This proves that an ownership scan failure is silently converted into a confirmed-looking empty result.
PoC 3: Previous-wallet markets remain exposed during the new wallet scan
Baseline Result
This proves that wallet A's creator markets remain exposed after wallet B becomes active and its ownership scan has already started.
Test Results
Before adding the PoCs, the existing test file passed completely:
After adding the three test-only PoCs:
Only the three newly added PoCs failed.
All 25 pre-existing tests continued to pass, demonstrating that the failures are isolated to the missing creator ownership scan lifecycle.
No production source file was modified during reproduction.
The only modified file was:
Reproduction Command
Reproduced Result
Actual Behavior
loadingbecomesfalsebefore creator ownership discovery finishes.errorremainsnullafter the ownership scan fails./my-marketsmay display a false “no markets created” message.Expected Behavior
/my-marketsmust not render its empty state before creator verification succeeds.Suggested Fix Direction
Introduce a wallet-scoped creator ownership scan state instead of storing only a shared market array.
For example:
The scan identity should include at least:
Recommended lifecycle:
loadingbefore starting ownership RPC calls./my-marketsin a loading or retryable-error state until creator ownership verification completes.refetch()rerun both general discovery and creator ownership scanning.Using only
setLoading(true)inside the effect may still allow a transient incorrect render because React effects execute after render.The visible result should therefore be gated by the current wallet and scan identity, not only by an effect-controlled boolean.
Acceptance Criteria
loadingremainstruewhile creator ownership RPC scanning is pending./my-marketsdoes not render its empty state before creator verification succeeds.Non-Duplication Review
This issue is distinct from earlier changes that introduced LP portfolio scanning to recover completed markets whose authority had been rotated away during launch.
Those earlier changes addressed:
This issue addresses:
It is also distinct from portfolio owner-scan fixes related to cached active-position snapshots.
Those changes concern trading portfolio visibility and shared scan-cache behavior, while this issue concerns creator market discovery in:
PRs related to logo-upload authorization are also not duplicates. Those changes evaluate whether LP portfolio signals are suitable for a server-side authorization boundary.
This report does not claim an authorization bypass and focuses exclusively on client-side creator dashboard correctness.
No open or closed issue or PR with the same affected hook, user-facing symptoms, and root cause was identified during the duplication review.
Security Scope Clarification
This report does not claim:
On-chain authorization remains the final enforcement boundary.
The impact is a wallet-scoped UI and data-integrity failure that can: