Skip to content

fix(trade): block stale oracles for every mode, not an allowlist (GH#2484) - #2485

Open
dcccrypto wants to merge 1 commit into
playgroundfrom
fix/2484-oracle-stale-gate-all-modes
Open

fix(trade): block stale oracles for every mode, not an allowlist (GH#2484)#2485
dcccrypto wants to merge 1 commit into
playgroundfrom
fix/2484-oracle-stale-gate-all-modes

Conversation

@dcccrypto

@dcccrypto dcccrypto commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Fixes #2484.

The reported bug is real, and wider than reported

Verified the premise on playground@f2a3bbe5 before touching anything:
OracleMode is a four-member union (oraclePrice.ts:40) and the gate at
OrderTicket.tsx:307 listed three of them. Confirmed.

The part the issue doesn't cover: the same expression is duplicated in four
components, not one.

File Gate
components/trade/OrderTicket.tsx:307 opening a trade — the reported one
components/trade/PositionPanel.tsx:243 managing an open position
components/trade/PositionsDock.tsx:141 the positions dock
components/trade/OtherMarketPositions.tsx:92 cross-market positions

All four carried the identical admin || hyperp || keeper allowlist, so a stale
Pyth market also bypassed the gates on the close/manage surfaces. Fixing only
the order ticket would have left three copies to drift again.

The fix

Took the issue's preferred option (fail-safe default) rather than the minimal
one, since the allowlist has now leaked twice — keeper was missing before (the
H7 comment is still in the file), then pyth-pinned.

lib/oracle-stale-gate.ts holds one predicate. Stale blocks every recognised
mode; exemptions must be declared:

export const STALE_EXEMPT_MODES: Record<OracleMode, boolean> = {
  "pyth-pinned": false, hyperp: false, admin: false, keeper: false,
};

Record<OracleMode, boolean> rather than the issue's suggested array is the one
deliberate deviation, and it's the point of the change: adding a member to
OracleMode is a compile error until it's classified here.
An array-based
!STALE_EXEMPT_MODES.includes(mode) is fail-safe at runtime but still lets a new
mode ship unconsidered. This makes that impossible rather than merely unlikely.

Callers keep their own unavailable handling, which differs on purpose — the
order ticket excludes it (it has its own message), the position panels fold it
in. Behaviour for admin/hyperp/keeper is unchanged.

Tests, and what actually binds them

Two halves, because the first alone would not have caught this bug:

  1. Exhaustiveness driven off the union. The mode list comes from
    Object.keys(STALE_EXEMPT_MODES), not a hand-written array, so a new mode
    fails the test rather than quietly passing it.
  2. Each of the four components is asserted to call the shared gate and to
    contain no inline allowlist.
    This is what binds the call sites. A unit test
    of the predicate on its own stays green if a component reverts to its own
    copy — which is precisely how this defect spread to four files.

Mutation-verified both ways:

Mutation Result
flip "pyth-pinned" back to exempt (restores #2484) 2 tests fail
re-inline the allowlist in PositionsDock only 2 tests fail
cd app && npx vitest run
Test Files  282 passed | 1 skipped (283)
     Tests  2939 passed | 16 skipped (2955)

npx tsc --noEmit    # clean

Severity

Agree with the issue's Medium, and for its stated reason: the on-chain Pyth
staleness filter is still the fund-safety backstop, so this is a UI safety-gate
gap rather than a direct loss-of-funds path. What it costs users is signing
transactions that are expected to fail, and a worst-fill bound anchored to a
stale mark in the window before the on-chain filter rejects it. The close/manage
surfaces being affected too is why I'd not leave it sitting.

Summary by CodeRabbit

  • Bug Fixes
    • Improved protection against trading with stale market pricing.
    • Buying, opening, and closing positions are now consistently blocked when pricing data is stale and ready for use.
    • Markets with unavailable or unresolved pricing data continue to follow their existing handling.
    • Mock markets remain available for testing and development.
  • Tests
    • Added coverage across all supported pricing modes and freshness states.

…2484)

The stale-oracle trading gate was an inline ALLOWLIST of oracle modes,
duplicated verbatim across four trade components:

    oracleLevel === "stale" && (mode === "admin" || "hyperp" || "keeper")

`OracleMode` has four members. "pyth-pinned" was missing from all four
copies, so a Pyth market whose price stopped advancing stayed tradeable
while admin/hyperp/keeper markets in the identical stale state were
blocked — the user signs a transaction the on-chain Pyth staleness filter
is expected to reject, and any worst-fill bound is derived from the stale
mark.

The allowlist has now leaked twice: "keeper" was missing before (H7), and
"pyth-pinned" after that. GH#2484 reports the order ticket; the same
expression had spread to the three position surfaces, which gate managing
and closing positions.

Extract the predicate to lib/oracle-stale-gate and invert the default:
stale blocks every recognised mode, and exemptions must be declared.
STALE_EXEMPT_MODES is a Record<OracleMode, boolean> rather than an array,
so adding a member to the union is a compile error until it is classified
— a new mode cannot reach production unclassified, which is the failure
this keeps having. Every entry is false today.

Callers keep their own "unavailable" handling, which differs deliberately:
the order ticket excludes it (it has its own message), the position panels
fold it in. Behaviour for admin/hyperp/keeper is unchanged.

Tests assert both halves. The predicate is driven off STALE_EXEMPT_MODES
rather than a hand-written mode list, so a new mode fails the test instead
of silently passing; and each of the four components is asserted to call
the shared gate and to carry no inline allowlist. That second half is what
binds the call sites — a unit test of the predicate alone still passes if
a component reverts to its own copy, which is exactly how this spread.

Mutation-verified: flipping "pyth-pinned" back to exempt fails 2 tests;
re-inlining the allowlist in one component fails 2 more.

Suite: 2939 passed / 0 failed. tsc --noEmit clean.
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
percolator-launch Ready Ready Preview Aug 5, 2026 11:56am
percolator-mainnet Ready Ready Preview Aug 5, 2026 11:56am
percolator-playground Ready Ready Preview Aug 5, 2026 11:56am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR adds a shared isOracleStaleBlocking predicate, applies it to four trade surfaces, and adds regression tests for oracle modes and freshness states.

Oracle stale gating

Layer / File(s) Summary
Shared stale-gate predicate
app/lib/oracle-stale-gate.ts
Defines per-mode exemptions and blocks ready markets with stale oracle data.
Trade-surface integration
app/components/trade/OrderTicket.tsx, app/components/trade/OtherMarketPositions.tsx, app/components/trade/PositionPanel.tsx, app/components/trade/PositionsDock.tsx
Replaces inline oracle-mode checks with the shared predicate while preserving unavailable and mock-oracle handling.
Stale-gate regression coverage
app/__tests__/lib/oracle-stale-gate.test.ts
Tests all oracle modes, freshness states, and shared-gate usage across trade components.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: bug

Suggested reviewers: 0x-squidsol, v1ktorrr0x

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states that stale-oracle blocking now applies to every oracle mode.
Linked Issues check ✅ Passed The changes block stale markets for all supported modes, preserve unavailable handling, update all trade surfaces, and add the required regression coverage for issue [#2484].
Out of Scope Changes check ✅ Passed All code changes support the linked issue by centralizing stale-oracle gating, updating trade surfaces, and adding focused regression tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/2484-oracle-stale-gate-all-modes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/components/trade/OrderTicket.tsx`:
- Around line 305-310: Update handleTrade to revalidate blockingIssue
immediately before calling trade(...), rejecting the submission when an oracle
becomes stale after the confirmation modal opens. Preserve the existing modal
flow for submissions without a blocking issue, and add a regression test
covering an oracle becoming stale while the confirmation modal remains open.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e9d05515-6d84-4be0-8c5a-e3e06ece04ed

📥 Commits

Reviewing files that changed from the base of the PR and between f2a3bbe and 9943e53.

📒 Files selected for processing (6)
  • app/__tests__/lib/oracle-stale-gate.test.ts
  • app/components/trade/OrderTicket.tsx
  • app/components/trade/OtherMarketPositions.tsx
  • app/components/trade/PositionPanel.tsx
  • app/components/trade/PositionsDock.tsx
  • app/lib/oracle-stale-gate.ts

Comment on lines +305 to +310
// GH#2484: this was an inline ALLOWLIST of oracle modes, and it leaked twice —
// first "keeper" (H7: a stale keeper-priced market never blocked trading,
// firing for 0/5 live markets), then "pyth-pinned". The predicate now lives in
// lib/oracle-stale-gate and blocks every recognised mode by default, so the
// next mode added to the union cannot silently trade on a stale price.
const oracleStale = !oracleUnavailable && isOracleStaleBlocking(oracleLevel, oracleMode, oracleReady);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Revalidate blocking state in handleTrade.

If the oracle becomes stale after the confirmation modal opens, submitDisabled no longer controls submission. The modal calls handleTrade directly, and handleTrade does not reject blockingIssue before it calls trade(...).

Reject the submission when blockingIssue is present. Add a regression test for an oracle that becomes stale while the confirmation modal is open.

Proposed fix
-    if (!marginInput || !userAccount || effectiveSize <= 0n || exceedsBalance) return;
+    if (!marginInput || !userAccount || effectiveSize <= 0n || exceedsBalance || blockingIssue) return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/trade/OrderTicket.tsx` around lines 305 - 310, Update
handleTrade to revalidate blockingIssue immediately before calling trade(...),
rejecting the submission when an oracle becomes stale after the confirmation
modal opens. Preserve the existing modal flow for submissions without a blocking
issue, and add a regression test covering an oracle becoming stale while the
confirmation modal remains open.

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