Skip to content

finance: reject oracle prices from before a cluster restart - #119

Open
mikemaccana wants to merge 3 commits into
mainfrom
claude/pdf-markdown-extraction-rqs594
Open

finance: reject oracle prices from before a cluster restart#119
mikemaccana wants to merge 3 commits into
mainfrom
claude/pdf-markdown-extraction-rqs594

Conversation

@mikemaccana

Copy link
Copy Markdown
Collaborator

What

A cluster halt stops the slot count but not the wall clock, so after a restart an oracle price can pass a slot-measured staleness bound while its value is hours old. The three oracle-priced examples — lending, prop-amm, and perpetual-futures, in both Anchor and Quasar variants — now also require the feed's slot to be after the LastRestartSlot sysvar's slot, failing with a dedicated error (PricePredatesRestart / PRICE_PREDATES_RESTART) until the publisher posts again. Zero means the cluster has never restarted, so the check is a no-op on a chain with no restart history.

Each check carries a comment stating the intention: the market pauses valuation after a restart rather than acting on a pre-halt price.

Tests

One new test per variant, each simulating a restart inside the staleness window (so only the restart check can catch it) and proving that a fresh publication reopens the market:

  • borrow_with_price_from_before_a_restart_is_rejected (lending, Anchor + Quasar)
  • test_swap_rejects_price_from_before_a_restart (prop-amm, Anchor) / swap_rejects_price_from_before_a_restart (Quasar)
  • test_open_rejects_price_from_before_a_restart (perps, Anchor) / open_rejects_price_from_before_a_restart (Quasar)

LiteSVM initializes the sysvar and set_sysvar overrides it; the Quasar tests pin the sysvar account directly, which quasar-svm's sysvar cache picks up.

Quasar sysvar declaration

quasar-lang ships only the Clock and Rent sysvars, so each Quasar variant declares the sysvar's 8-byte layout in src/last_restart.rs and reads it through the same sol_get_sysvar syscall quasar's own sysvars use. The Sysvar impl is written out by hand because quasar-lang's impl_sysvar_get! macro names private constants and cannot expand outside that crate.

Incidental fix: zeropod pin

The three touched Quasar variants pin zeropod = "=0.3.3". zeropod 0.3.4 moved to wincode 0.5 while quasar-lang's pinned rev stays on wincode 0.4, so any fresh resolve (these projects commit no lockfile) splits the graph across two wincode versions and fails every Pod* trait bound. The repository's other Quasar examples have the same latent break and will need the same pin (or an upstream quasar-lang fix) the next time their CI resolves fresh — happy to do that as a follow-up.

Verification

All six workspaces pass cargo check --workspace --all-targets locally. The LiteSVM/quasar-test suites need cargo build-sbf, whose platform-tools download is blocked in this environment, so CI is the test signal for the suites.

This change pairs with a book update in quicknode/solana-book#16, which teaches what a halt does to the two clocks and points at these oracle readers as the demonstration.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj


Generated by Claude Code

claude added 3 commits August 4, 2026 21:30
A cluster halt stops the slot count but not the wall clock, so after a
restart an oracle price can pass a slot-measured staleness bound while
its value is hours old. The three oracle-priced examples (lending,
prop-amm, perpetual-futures, both Anchor and Quasar variants) now also
require the feed's slot to be after the LastRestartSlot sysvar's slot,
failing with PricePredatesRestart until the publisher posts again. Each
variant gets a test that simulates a restart inside the staleness
window and proves a fresh publication reopens the market.

quasar-lang ships only the Clock and Rent sysvars, so each Quasar
variant declares the sysvar's 8-byte layout in src/last_restart.rs and
reads it through the same sol_get_sysvar syscall (hand-implemented:
quasar-lang's impl_sysvar_get! macro names private constants and cannot
expand outside that crate).

Also pins zeropod = "=0.3.3" in the three Quasar variants: zeropod
0.3.4 moved to wincode 0.5 while quasar-lang's pinned rev stays on
wincode 0.4, so any fresh resolve (no lockfile is committed) split the
graph across two wincode versions and failed every Pod* trait bound.
The other Quasar examples in this repository have the same latent break
and will need the same pin or an upstream fix.

All six workspaces pass cargo check --workspace --all-targets. The
LiteSVM/quasar-test suites need cargo build-sbf, whose platform-tools
download is blocked in this environment, so CI is the test signal for
this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj
All three restart tests asserted the rejection correctly and then failed
on the retry, for two reasons.

The retry transaction was byte-identical to the one that was rejected,
so it carried the same signature and LiteSVM dropped it as already
processed. Warping a slot before republishing the price expires the
blockhash, and matches what actually happens: the publisher posts again
after the restart.

The lending retry also passed the borrow reserve in the obligation's
existing-borrows refresh list. The first borrow was rejected, so it
recorded nothing and the obligation still had no borrows, which
refresh_obligation reported as InvalidObligationAccount (6015).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj
"Price feed was last updated before the most recent cluster restart"
states the condition that tripped and leaves the reader to work out why
anyone cares. Lead with the consequence instead, in the voice the
neighbouring staleness errors already use: the price is stale, and the
restart is why.

Anchor variants only. The Quasar variants raise bare numeric codes with
no message strings attached.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ABFpszmPxRorwWtWGR2jXj
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.

2 participants