finance: reject oracle prices from before a cluster restart - #119
Open
mikemaccana wants to merge 2 commits into
Open
finance: reject oracle prices from before a cluster restart#119mikemaccana wants to merge 2 commits into
mikemaccana wants to merge 2 commits into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
LastRestartSlotsysvar'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_sysvaroverrides 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.rsand reads it through the samesol_get_sysvarsyscall quasar's own sysvars use. TheSysvarimpl is written out by hand because quasar-lang'simpl_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 everyPod*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-targetslocally. The LiteSVM/quasar-test suites needcargo 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