diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dd0c17d6..72e9e880d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this repository are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [2026-09-23] - Finance examples point their production oracle path at Pyth + +The oracle network that the lending, perpetual futures and prop AMM examples +modeled their price feeds on has shut down. The perpetual futures and prop AMM +mock oracle program is now `mock-price-feed` in both Anchor variants, with the +same program ID, instructions and account layout. Every production-path comment +and README in the three examples, across Anchor v2, Anchor v1 and Quasar, now +points at a Pyth `PriceUpdateV2` account, which `basics/pyth` reads. No program +behavior changes. + ## [2026-09-23] - The token fundraiser and order book Anchor v1 copies catch up Under the old rule that `anchor-v1/` copies were frozen, two v1 copies were diff --git a/Cargo.lock b/Cargo.lock index 724b0bfea..35a92d8aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2648,7 +2648,7 @@ dependencies = [ ] [[package]] -name = "mock_switchboard" +name = "mock_price_feed" version = "0.1.0" dependencies = [ "anchor-lang", @@ -2657,7 +2657,7 @@ dependencies = [ ] [[package]] -name = "mock_switchboard_prop_amm" +name = "mock_price_feed_prop_amm" version = "0.1.0" dependencies = [ "anchor-lang", @@ -3083,7 +3083,7 @@ dependencies = [ "anchor-spl", "anchor-v2-testing", "borsh 1.7.0", - "mock_switchboard", + "mock_price_feed", "pinocchio 0.11.2", "solana-address 2.6.1", "solana-clock 3.1.1", @@ -3373,7 +3373,7 @@ dependencies = [ "anchor-spl", "anchor-v2-testing", "borsh 1.7.0", - "mock_switchboard_prop_amm", + "mock_price_feed_prop_amm", "pinocchio 0.11.2", "solana-address 2.6.1", "solana-clock 3.1.1", diff --git a/Cargo.toml b/Cargo.toml index 5424047f9..c1fd19028 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ members = [ # finance # - # The two `mock-switchboard` crates are excluded below: they share a package + # The two `mock-price-feed` crates are excluded below: they share a package # name, and one workspace cannot hold two packages called the same thing. # They stay path dependencies of the programs that test against them. "finance/betting-market/anchor/programs/betting-market", @@ -127,11 +127,11 @@ members = [ "tokens/token-extensions/transfer-hook/whitelist/anchor/programs/transfer-hook", ] # A path dependency inside the workspace directory joins the workspace unless it -# is excluded. Both of these are named `mock_switchboard`, so leaving them in -# fails with "two packages named `mock_switchboard` in this workspace". +# is excluded. Both of these are named `mock_price_feed`, so leaving them in +# fails with "two packages named `mock_price_feed` in this workspace". exclude = [ - "finance/perpetual-futures/anchor/programs/mock-switchboard", - "finance/prop-amm/anchor/programs/mock-switchboard", + "finance/perpetual-futures/anchor/programs/mock-price-feed", + "finance/prop-amm/anchor/programs/mock-price-feed", ] resolver = "2" diff --git a/finance/lending/anchor-v1/CHANGELOG.md b/finance/lending/anchor-v1/CHANGELOG.md index c1fc61662..b4eec5747 100644 --- a/finance/lending/anchor-v1/CHANGELOG.md +++ b/finance/lending/anchor-v1/CHANGELOG.md @@ -27,6 +27,12 @@ previous program, and by `first_deposit_must_exceed_the_minimum` and opens each reserve with a deposit from the market owner; `add_empty_reserve` leaves it empty. +The price feed's production path now points at a Pyth `PriceUpdateV2` account +(`price_mantissa = price_message.price`, `exponent = price_message.exponent`, +`last_updated_slot = posted_slot`), since the oracle network the feed was +modeled on has shut down. Documentation only: the account layout and handlers +are unchanged. + ## 2026-09-22 Accrue interest by the wall clock instead of by slots. The reserve's @@ -76,7 +82,7 @@ Initial lending program: a Kamino/Solend-style borrow/lend market. borrow-rate index; per-obligation scaled debt. - Oracle-priced obligation health with loan-to-value and liquidation-threshold limits, and close-factor-capped liquidation with a seize bonus. -- Switchboard-On-Demand-shaped price feed with a `set_price` test writer. +- Mantissa-and-exponent price feed with a `set_price` test writer. - Rust + LiteSVM integration tests covering supply/redeem, borrow/repay, withdraw, interest accrual, liquidation, the share-inflation guard, and rounding/stale-input edge cases. diff --git a/finance/lending/anchor-v1/README.md b/finance/lending/anchor-v1/README.md index 0063d4f01..9569a34d4 100644 --- a/finance/lending/anchor-v1/README.md +++ b/finance/lending/anchor-v1/README.md @@ -143,7 +143,7 @@ round-trips. ### Oracle -`PriceFeed` mirrors a Switchboard On-Demand pull feed: a signed mantissa, an +`PriceFeed` mirrors an oracle price feed such as Pyth's: a signed mantissa, an exponent (`price = mantissa * 10^exponent`), and the slot the price was written. Freshness is checked in **slots** (`MAX_PRICE_STALENESS_SLOTS`), not wall-clock time, plus one check slots alone cannot make: a cluster restart passes hours of @@ -156,11 +156,12 @@ trusts exactly its own market's feed for the mint, and isolated markets can price the same asset independently. The `set_price` handler writes the feed directly so the LiteSVM tests are -deterministic; in production a reserve points at the real Switchboard feed and the -program decodes `PullFeedAccountData` (`price_mantissa = current_result.value`, -`exponent = -18`, `last_updated_slot = current_result.slot`) instead, and should -also reject results whose confidence interval is too wide. Switchboard is used -rather than Pyth here for its lower compute cost. +deterministic; in production a reserve points at a Pyth price feed and the +program reads its `PriceUpdateV2` account instead, as +[`basics/pyth`](../../../basics/pyth/) does, after checking the update's +`feed_id`: `price_mantissa` is `price_message.price`, `exponent` is +`price_message.exponent`, and `last_updated_slot` is `posted_slot`. It should +also reject results whose confidence interval is too wide. ### Custody diff --git a/finance/lending/anchor-v1/programs/lending/src/instructions/admin/set_price.rs b/finance/lending/anchor-v1/programs/lending/src/instructions/admin/set_price.rs index 4044c255b..8140aaaf9 100644 --- a/finance/lending/anchor-v1/programs/lending/src/instructions/admin/set_price.rs +++ b/finance/lending/anchor-v1/programs/lending/src/instructions/admin/set_price.rs @@ -4,9 +4,9 @@ use anchor_spl::token_interface::Mint; use crate::constants::PRICE_FEED_SEED; use crate::state::{LendingMarket, PriceFeed}; -/// Test stand-in for a Switchboard On-Demand feed: writes a price directly so +/// Test stand-in for an oracle price feed: writes a price directly so /// LiteSVM tests are deterministic. In production the reserve points at a real -/// Switchboard feed instead and this handler is unused. +/// Pyth price feed instead and this handler is unused. /// /// The feed PDA is seeded by `[b"price_feed", market, mint]` and writing it /// requires the market's `owner` to sign, so a market's prices can only be set diff --git a/finance/lending/anchor-v1/programs/lending/src/state/price_feed.rs b/finance/lending/anchor-v1/programs/lending/src/state/price_feed.rs index 6d7cc8b82..cedfa148f 100644 --- a/finance/lending/anchor-v1/programs/lending/src/state/price_feed.rs +++ b/finance/lending/anchor-v1/programs/lending/src/state/price_feed.rs @@ -10,12 +10,13 @@ use crate::math::price_mantissa_to_scaled; /// individual), so each market prices its own assets and one market can never /// write another's feed. Only the market's `owner` may write it (`set_price`). /// -/// The layout mirrors a Switchboard On-Demand pull feed: a signed mantissa plus -/// an exponent (`price = price_mantissa * 10^exponent`) and the slot the value -/// was written. In production this account would be the real Switchboard feed -/// and the program would decode it with the `switchboard-on-demand` crate -/// (`PullFeedAccountData`): `price_mantissa = current_result.value`, -/// `exponent = -18`, `last_updated_slot = current_result.slot`. Here the +/// The layout mirrors an oracle price feed such as Pyth's: a signed mantissa +/// plus an exponent (`price = price_mantissa * 10^exponent`) and the slot the +/// value was written. In production this account would be a Pyth +/// `PriceUpdateV2` owned by the Pyth Receiver program (`basics/pyth` reads +/// one), mapped as `price_mantissa = price_message.price`, +/// `exponent = price_message.exponent` and `last_updated_slot = posted_slot`, +/// after checking the update's `feed_id`. Here the /// `set_price` handler writes it directly so LiteSVM tests are deterministic. /// A production read should also reject results whose confidence interval is /// too wide; this stand-in has no confidence field to check. diff --git a/finance/lending/anchor-v1/programs/lending/tests/common/mod.rs b/finance/lending/anchor-v1/programs/lending/tests/common/mod.rs index ec4bb9962..b396c8474 100644 --- a/finance/lending/anchor-v1/programs/lending/tests/common/mod.rs +++ b/finance/lending/anchor-v1/programs/lending/tests/common/mod.rs @@ -31,7 +31,7 @@ use lending::state::{Obligation, Reserve, ReserveConfig}; pub use anchor_lang::prelude::Pubkey; /// A FIXED_POINT_SCALE-scaled price exponent: prices are passed as -/// `mantissa * 10^-18`, matching a Switchboard On-Demand feed's 1e18 result. +/// `mantissa * 10^-18`, the same 18 decimals as `FIXED_POINT_SCALE`. pub const PRICE_EXPONENT: i32 = -18; pub fn dollars(whole: u64) -> i128 { diff --git a/finance/lending/anchor/CHANGELOG.md b/finance/lending/anchor/CHANGELOG.md index c1fc61662..b4eec5747 100644 --- a/finance/lending/anchor/CHANGELOG.md +++ b/finance/lending/anchor/CHANGELOG.md @@ -27,6 +27,12 @@ previous program, and by `first_deposit_must_exceed_the_minimum` and opens each reserve with a deposit from the market owner; `add_empty_reserve` leaves it empty. +The price feed's production path now points at a Pyth `PriceUpdateV2` account +(`price_mantissa = price_message.price`, `exponent = price_message.exponent`, +`last_updated_slot = posted_slot`), since the oracle network the feed was +modeled on has shut down. Documentation only: the account layout and handlers +are unchanged. + ## 2026-09-22 Accrue interest by the wall clock instead of by slots. The reserve's @@ -76,7 +82,7 @@ Initial lending program: a Kamino/Solend-style borrow/lend market. borrow-rate index; per-obligation scaled debt. - Oracle-priced obligation health with loan-to-value and liquidation-threshold limits, and close-factor-capped liquidation with a seize bonus. -- Switchboard-On-Demand-shaped price feed with a `set_price` test writer. +- Mantissa-and-exponent price feed with a `set_price` test writer. - Rust + LiteSVM integration tests covering supply/redeem, borrow/repay, withdraw, interest accrual, liquidation, the share-inflation guard, and rounding/stale-input edge cases. diff --git a/finance/lending/anchor/README.md b/finance/lending/anchor/README.md index 9d43d292f..b076bd9c0 100644 --- a/finance/lending/anchor/README.md +++ b/finance/lending/anchor/README.md @@ -143,7 +143,7 @@ round-trips. ### Oracle -`PriceFeed` mirrors a Switchboard On-Demand pull feed: a signed mantissa, an +`PriceFeed` mirrors an oracle price feed such as Pyth's: a signed mantissa, an exponent (`price = mantissa * 10^exponent`), and the slot the price was written. Freshness is checked in **slots** (`MAX_PRICE_STALENESS_SLOTS`), not wall-clock time, plus one check slots alone cannot make: a cluster restart passes hours of @@ -156,11 +156,12 @@ trusts exactly its own market's feed for the mint, and isolated markets can price the same asset independently. The `set_price` handler writes the feed directly so the LiteSVM tests are -deterministic; in production a reserve points at the real Switchboard feed and the -program decodes `PullFeedAccountData` (`price_mantissa = current_result.value`, -`exponent = -18`, `last_updated_slot = current_result.slot`) instead, and should -also reject results whose confidence interval is too wide. Switchboard is used -rather than Pyth here for its lower compute cost. +deterministic; in production a reserve points at a Pyth price feed and the +program reads its `PriceUpdateV2` account instead, as +[`basics/pyth`](../../../basics/pyth/) does, after checking the update's +`feed_id`: `price_mantissa` is `price_message.price`, `exponent` is +`price_message.exponent`, and `last_updated_slot` is `posted_slot`. It should +also reject results whose confidence interval is too wide. ### Custody diff --git a/finance/lending/anchor/programs/lending/src/instructions/admin/set_price.rs b/finance/lending/anchor/programs/lending/src/instructions/admin/set_price.rs index 7918e0193..02e3fac70 100644 --- a/finance/lending/anchor/programs/lending/src/instructions/admin/set_price.rs +++ b/finance/lending/anchor/programs/lending/src/instructions/admin/set_price.rs @@ -4,9 +4,9 @@ use anchor_spl::token_interface::Mint; use crate::constants::PRICE_FEED_SEED; use crate::state::{LendingMarket, PriceFeed}; -/// Test stand-in for a Switchboard On-Demand feed: writes a price directly so +/// Test stand-in for an oracle price feed: writes a price directly so /// LiteSVM tests are deterministic. In production the reserve points at a real -/// Switchboard feed instead and this handler is unused. +/// Pyth price feed instead and this handler is unused. /// /// The feed PDA is seeded by `[b"price_feed", market, mint]` and writing it /// requires the market's `owner` to sign, so a market's prices can only be set diff --git a/finance/lending/anchor/programs/lending/src/state/price_feed.rs b/finance/lending/anchor/programs/lending/src/state/price_feed.rs index dd2747849..719aaea91 100644 --- a/finance/lending/anchor/programs/lending/src/state/price_feed.rs +++ b/finance/lending/anchor/programs/lending/src/state/price_feed.rs @@ -9,12 +9,13 @@ use crate::math::price_mantissa_to_scaled; /// individual), so each market prices its own assets and one market can never /// write another's feed. Only the market's `owner` may write it (`set_price`). /// -/// The layout mirrors a Switchboard On-Demand pull feed: a signed mantissa plus -/// an exponent (`price = price_mantissa * 10^exponent`) and the slot the value -/// was written. In production this account would be the real Switchboard feed -/// and the program would decode it with the `switchboard-on-demand` crate -/// (`PullFeedAccountData`): `price_mantissa = current_result.value`, -/// `exponent = -18`, `last_updated_slot = current_result.slot`. Here the +/// The layout mirrors an oracle price feed such as Pyth's: a signed mantissa +/// plus an exponent (`price = price_mantissa * 10^exponent`) and the slot the +/// value was written. In production this account would be a Pyth +/// `PriceUpdateV2` owned by the Pyth Receiver program (`basics/pyth` reads +/// one), mapped as `price_mantissa = price_message.price`, +/// `exponent = price_message.exponent` and `last_updated_slot = posted_slot`, +/// after checking the update's `feed_id`. Here the /// `set_price` handler writes it directly so LiteSVM tests are deterministic. /// A production read should also reject results whose confidence interval is /// too wide; this stand-in has no confidence field to check. diff --git a/finance/lending/anchor/programs/lending/tests/common/mod.rs b/finance/lending/anchor/programs/lending/tests/common/mod.rs index 4f456692a..b8b05f832 100644 --- a/finance/lending/anchor/programs/lending/tests/common/mod.rs +++ b/finance/lending/anchor/programs/lending/tests/common/mod.rs @@ -26,7 +26,7 @@ use lending::state::{Obligation, Reserve, ReserveConfig}; pub use anchor_lang::prelude::Address; /// A FIXED_POINT_SCALE-scaled price exponent: prices are passed as -/// `mantissa * 10^-18`, matching a Switchboard On-Demand feed's 1e18 result. +/// `mantissa * 10^-18`, the same 18 decimals as `FIXED_POINT_SCALE`. pub const PRICE_EXPONENT: i32 = -18; pub fn dollars(whole: u64) -> i128 { diff --git a/finance/lending/quasar/CHANGELOG.md b/finance/lending/quasar/CHANGELOG.md index dc6a19ebb..a57ed3d67 100644 --- a/finance/lending/quasar/CHANGELOG.md +++ b/finance/lending/quasar/CHANGELOG.md @@ -2,6 +2,11 @@ ## [2026-09-23] +### Changed + +- Documentation only: the price feed's production path now points at a Pyth + price feed, since the oracle network it was modeled on has shut down. + ### Fixed - Lock a minimum number of reserve shares. The first deposit now mints @@ -113,7 +118,7 @@ Initial Quasar port of the Kamino/Solend-style borrow/lend program. borrow-rate index, accrued inline per instruction. - Oracle-priced health with loan-to-value and liquidation-threshold limits, and close-factor-capped liquidation with a seize bonus. -- Switchboard-On-Demand-shaped price feed with a `set_price` test writer. +- Mantissa-and-exponent price feed with a `set_price` test writer. - quasar-svm integration tests covering supply/redeem, borrow/repay, interest accrual, and liquidation (including the healthy-rejection path). - Price feed PDAs are seeded by their authority, so no signer can write or diff --git a/finance/lending/quasar/README.md b/finance/lending/quasar/README.md index 107c4b478..2080d47ba 100644 --- a/finance/lending/quasar/README.md +++ b/finance/lending/quasar/README.md @@ -47,12 +47,12 @@ Everything else mirrors the Anchor version. - **`Obligation`**: a borrower's isolated position: the collateral reserve and deposited share amount, plus the borrow reserve and scaled debt. PDA: `["obligation", market, owner]`. -- **`PriceFeed`**: a Switchboard-On-Demand-shaped price (`mantissa * 10^exponent` +- **`PriceFeed`**: an oracle-shaped price (`mantissa * 10^exponent` + slot). PDA: `["price_feed", market, mint]`: scoped to a market, not to any individual; only the market's `owner` may write it, so prices can't be squatted and each market prices its own assets. `set_price` writes it directly for - deterministic tests; in production a reserve points at the real Switchboard - feed. Freshness is checked in slots. + deterministic tests; in production a reserve points at a real Pyth + price feed. Freshness is checked in slots. - **Liquidation**: the close factor (max fraction of the debt one call repays) comes from the borrow reserve; the bonus from the collateral reserve. A repayment whose seizure would exceed the posted collateral fails with diff --git a/finance/lending/quasar/src/instructions/admin.rs b/finance/lending/quasar/src/instructions/admin.rs index fd973b369..811bfffe2 100644 --- a/finance/lending/quasar/src/instructions/admin.rs +++ b/finance/lending/quasar/src/instructions/admin.rs @@ -181,7 +181,7 @@ impl InitializeReserve { } // --------------------------------------------------------------------------- -// set_price (Switchboard stand-in for tests) +// set_price (oracle stand-in for tests) // --------------------------------------------------------------------------- #[derive(Accounts)] diff --git a/finance/lending/quasar/src/state.rs b/finance/lending/quasar/src/state.rs index 8296271fa..70d2ad369 100644 --- a/finance/lending/quasar/src/state.rs +++ b/finance/lending/quasar/src/state.rs @@ -72,11 +72,12 @@ pub struct Obligation { pub bump: u8, } -/// Switchboard-On-Demand-shaped price feed. PDA: `["price_feed", market, mint]` -/// — scoped to a market (not to any individual); only the market's `owner` may -/// write it, so prices can't be squatted and each market prices its own assets. +/// Oracle-shaped price feed, a mantissa and exponent like Pyth's. +/// PDA: `["price_feed", market, mint]` — scoped to a market (not to any +/// individual); only the market's `owner` may write it, so prices can't be +/// squatted and each market prices its own assets. /// `price = price_mantissa * 10^exponent`; freshness is checked in slots. In -/// production this account would be the real Switchboard feed. +/// production this account would be a real Pyth price feed. #[account(discriminator = 4, set_inner)] #[seeds(b"price_feed", market: Address, mint: Address)] pub struct PriceFeed { diff --git a/finance/lending/quasar/src/tests.rs b/finance/lending/quasar/src/tests.rs index 31bf1db1d..20812e9b4 100644 --- a/finance/lending/quasar/src/tests.rs +++ b/finance/lending/quasar/src/tests.rs @@ -18,7 +18,7 @@ use { quasar_test::prelude::*, }; -// Prices are passed as `mantissa * 10^-18` (Switchboard-shaped). +// Prices are passed as `mantissa * 10^-18`, the program's fixed-point scale. const EXP: i32 = -18; fn dollars(whole: u64) -> i128 { (whole as i128) * 1_000_000_000_000_000_000 diff --git a/finance/perpetual-futures/anchor-v1/Anchor.toml b/finance/perpetual-futures/anchor-v1/Anchor.toml index e16a2097e..22c7dc54f 100644 --- a/finance/perpetual-futures/anchor-v1/Anchor.toml +++ b/finance/perpetual-futures/anchor-v1/Anchor.toml @@ -9,7 +9,7 @@ skip-lint = false [programs.localnet] perpetual_futures = "3uCm8Jep469pHUpYQCh6eA6dpYV3ogvTvaRDZBPtw5So" -mock_switchboard = "FnisQqhF56BxVYh5Wt8xW8wuTVN6STAGnk13MM5SRM7b" +mock_price_feed = "FnisQqhF56BxVYh5Wt8xW8wuTVN6STAGnk13MM5SRM7b" [provider] cluster = "localnet" diff --git a/finance/perpetual-futures/anchor-v1/CHANGELOG.md b/finance/perpetual-futures/anchor-v1/CHANGELOG.md index 99e195a24..ce12abd8a 100644 --- a/finance/perpetual-futures/anchor-v1/CHANGELOG.md +++ b/finance/perpetual-futures/anchor-v1/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2026-09-23 + +The mock oracle program is now `mock-price-feed` (library and program +`mock_price_feed`), with the same program ID, instructions and +account layout. The oracle network it was modeled on has shut down, so the +production path described in `state/oracle.rs` now reads a Pyth +`PriceUpdateV2` account, as `basics/pyth` does. No behavior changes. + ## 2026-09-22 Accrue funding by the wall clock instead of by slots. The rate was quoted per diff --git a/finance/perpetual-futures/anchor-v1/README.md b/finance/perpetual-futures/anchor-v1/README.md index 9ab0b31e7..159e2bc4f 100644 --- a/finance/perpetual-futures/anchor-v1/README.md +++ b/finance/perpetual-futures/anchor-v1/README.md @@ -19,7 +19,7 @@ A [perpetual future](https://www.investopedia.com/terms/f/futurescontract.asp) ( ## Programs - `perpetual-futures`: The exchange: pool creation, liquidity provision, opening/closing leveraged positions, funding, liquidation, and fee collection. -- `mock-switchboard`: Test-only price feed. Stores a price, scale, last-update slot, and confidence band that tests write directly. Replaced by a real [Switchboard](https://docs.switchboard.xyz/) On-Demand feed in production. +- `mock-price-feed`: Test-only price feed. Stores a price, scale, last-update slot, and confidence band that tests write directly. Replaced in production by a Pyth `PriceUpdateV2` account, as read in [`basics/pyth`](../../../basics/pyth/). All money math is integer `u128` with `checked_*` operations, multiplying before dividing and rounding in the pool's favour: no floats, no fixed-point library. diff --git a/finance/perpetual-futures/anchor-v1/TERMINOLOGY.md b/finance/perpetual-futures/anchor-v1/TERMINOLOGY.md index 9ef81ba16..26c7bd5e0 100644 --- a/finance/perpetual-futures/anchor-v1/TERMINOLOGY.md +++ b/finance/perpetual-futures/anchor-v1/TERMINOLOGY.md @@ -31,6 +31,7 @@ Terms used in this example, in the sense they carry here. - **Liquidity-provider share** — a token representing a pro-rata claim on assets-under-management. - **Oracle feed** — the account the pool reads its price from. This example uses - a mock Switchboard On-Demand feed; production points at a real one. + a mock oracle price feed; production points at a real one, such as + a Pyth price feed. - **Mark price** — the price positions are valued at. Here it is the oracle price directly, with no separate mark/index distinction. diff --git a/finance/prop-amm/anchor-v1/programs/mock-switchboard/Cargo.toml b/finance/perpetual-futures/anchor-v1/programs/mock-price-feed/Cargo.toml similarity index 75% rename from finance/prop-amm/anchor-v1/programs/mock-switchboard/Cargo.toml rename to finance/perpetual-futures/anchor-v1/programs/mock-price-feed/Cargo.toml index 9b5e6da53..bc8254df6 100644 --- a/finance/prop-amm/anchor-v1/programs/mock-switchboard/Cargo.toml +++ b/finance/perpetual-futures/anchor-v1/programs/mock-price-feed/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "mock_switchboard" +name = "mock_price_feed" version = "0.1.0" -description = "Mock Switchboard On-Demand feed for testing the prop-amm program" +description = "Mock oracle price feed for testing the perpetual-futures program" edition = "2021" [lib] crate-type = ["cdylib", "lib"] -name = "mock_switchboard" +name = "mock_price_feed" [features] default = [] diff --git a/finance/perpetual-futures/anchor-v1/programs/mock-switchboard/src/lib.rs b/finance/perpetual-futures/anchor-v1/programs/mock-price-feed/src/lib.rs similarity index 66% rename from finance/perpetual-futures/anchor-v1/programs/mock-switchboard/src/lib.rs rename to finance/perpetual-futures/anchor-v1/programs/mock-price-feed/src/lib.rs index 2a3f79232..87a1350cf 100644 --- a/finance/perpetual-futures/anchor-v1/programs/mock-switchboard/src/lib.rs +++ b/finance/perpetual-futures/anchor-v1/programs/mock-price-feed/src/lib.rs @@ -1,16 +1,17 @@ -//! Mock Switchboard On-Demand feed for testing the perpetual-futures program. +//! Mock oracle price feed for testing the perpetual-futures program. //! -//! Real Switchboard On-Demand feeds are program-owned accounts whose data is -//! produced by an offchain oracle network and verified onchain via Ed25519 -//! signatures over the latest price update. That verification path is -//! out-of-scope for this teaching example, so this mock stores a single price -//! the test harness writes directly, plus the slot the update happened in. +//! A real price feed account is written by an oracle network's receiver +//! program, which verifies the network's signatures over each price update +//! before recording it. That verification path is out of scope for this +//! teaching example, so this mock stores a single price the test harness +//! writes directly, plus the slot the update happened in. //! -//! The perpetual-futures program reads this feed the same way it would read a -//! real feed: load the account, decode the layout, read `price`, `scale`, and -//! `last_update_slot` (see `perpetual_futures::state::oracle`). Swap this -//! program ID for `SBondMDrcV3K4kxZR1HNVT7osZxAHVHgYXL5Ze1oMUv` (Switchboard -//! On-Demand) and adapt the layout to consume real feeds in production. +//! The perpetual-futures program reads this feed the same way it would read a real +//! feed: load the account, decode the layout, read `price`, `scale`, and +//! `last_update_slot` (see `perpetual_futures::state::oracle`). In production the +//! program reads a Pyth `PriceUpdateV2` account instead, owned by the Pyth +//! Receiver program `rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ`. The oracle +//! module describes that change, and `basics/pyth` reads one. //! //! NOT FOR PRODUCTION. use anchor_lang::prelude::*; @@ -18,7 +19,7 @@ use anchor_lang::prelude::*; declare_id!("FnisQqhF56BxVYh5Wt8xW8wuTVN6STAGnk13MM5SRM7b"); #[program] -pub mod mock_switchboard { +pub mod mock_price_feed { use super::*; /// Initialize the mock feed with an initial price. The signer becomes the @@ -38,9 +39,9 @@ pub mod mock_switchboard { Ok(()) } - /// Push a new price (and confidence band) to the mock feed. In real - /// Switchboard this would be a signed update from the oracle network; here it - /// is an authority-gated write, because the goal is to drive deterministic + /// Push a new price (and confidence band) to the mock feed. For a real + /// feed this would be a signed update from the oracle network; here it is + /// an authority-gated write, because the goal is to drive deterministic /// test scenarios. pub fn set_price( context: Context, @@ -81,15 +82,16 @@ pub struct SetPriceAccountConstraints<'info> { pub authority: Signer<'info>, } -/// Mock of a Switchboard On-Demand feed. Real feeds carry many more fields -/// (median, range, sample window, signatures) — this is the bare minimum the +/// Mock of an oracle price feed. Real feeds carry many more fields (feed ID, +/// publish time, EMA price, verification level) — this is the bare minimum the /// perpetual-futures program needs to do a price comparison. #[derive(InitSpace)] #[account] pub struct MockFeed { pub authority: Pubkey, - /// Signed 128-bit fixed-point price. Real Switchboard prices are also i128. + /// Signed 128-bit fixed-point price, wide enough for any feed's price + /// (Pyth's is an i64 with a separate exponent). pub price: i128, /// Number of decimal places implied by `price`. E.g. `scale = 8` means diff --git a/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/Cargo.toml b/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/Cargo.toml index 16fba932e..dd3247f6a 100644 --- a/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/Cargo.toml +++ b/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/Cargo.toml @@ -44,7 +44,7 @@ solana-kite = "0.5.0" borsh = "1.6.1" # The LiteSVM tests load the compiled mock oracle program; depending on the # crate here lets the tests reuse its instruction-argument types and program ID. -mock_switchboard = { path = "../mock-switchboard", features = ["no-entrypoint"] } +mock_price_feed = { path = "../mock-price-feed", features = ["no-entrypoint"] } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] } diff --git a/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/src/instructions/initialize_pool.rs b/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/src/instructions/initialize_pool.rs index 7b597e0c6..e104f0a5d 100644 --- a/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/src/instructions/initialize_pool.rs +++ b/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/src/instructions/initialize_pool.rs @@ -121,7 +121,7 @@ pub struct InitializePoolAccountConstraints<'info> { /// CHECK: The oracle feed account. Its key is stored on the pool and every /// read validates the layout, scale, and freshness; it is never trusted by - /// type. Swap for a real Switchboard feed in production. + /// type. Swap for a real Pyth price feed in production. pub oracle_feed: UncheckedAccount<'info>, /// Liquidity-provider share mint. The pool account is its mint authority diff --git a/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/src/state/oracle.rs b/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/src/state/oracle.rs index e05b7ce32..6fc998bbc 100644 --- a/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/src/state/oracle.rs +++ b/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/src/state/oracle.rs @@ -5,16 +5,20 @@ use crate::constants::{BASIS_POINTS_DENOMINATOR, MAX_PRICE_STALENESS_SLOTS}; use crate::errors::PerpError; // Byte layout of the feed account this program reads. It matches the -// `mock_switchboard::MockFeed` account: an 8-byte Anchor discriminator followed +// `mock_price_feed::MockFeed` account: an 8-byte Anchor discriminator followed // by `authority: Pubkey (32)`, `price: i128 (16)`, `scale: u32 (4)`, // `last_update_slot: u64 (8)`, `confidence: u64 (8)`. // // We read the raw bytes rather than deserializing the mock account type so this -// program stays decoupled from the mock. To consume a real Switchboard -// On-Demand feed, replace the offsets below with a call to -// `switchboard_on_demand::PullFeedAccountData::parse_and_verify(...)`, which -// also checks the Ed25519 signatures over the price update — the only other -// change is the feed account's owning program ID. +// program stays decoupled from the mock. To consume a real Pyth feed, take the +// account as a `PriceUpdateV2` instead of reading offsets: from the +// `pyth-solana-receiver-sdk` crate, or the vendored copy in `basics/pyth` on +// Anchor 2. The Pyth Receiver program writes that account only after checking +// the Wormhole guardian signatures over the update, and the account type's +// owner check rejects any account that program does not own. Map +// `price_message.price`, `exponent` (the scale is its negation), `conf`, and +// `publish_time` or `posted_slot` onto the checks below, and also check +// `feed_id` and `verification_level`. // // A real feed reports a value plus a `confidence` band (a standard-deviation-like // uncertainty). This reader rejects a price whose band is too wide relative to @@ -26,7 +30,8 @@ use crate::errors::PerpError; // The feed account's owning program is NOT checked here: the pool trusts // whatever feed address its creator configured, which is inside the trust model // (the creator picks the oracle). A production reader must also verify the -// account owner is the oracle program, which `parse_and_verify` does. +// account owner is the oracle program, which the `PriceUpdateV2` account type +// does. const PRICE_OFFSET: usize = 8 + 32; const SCALE_OFFSET: usize = PRICE_OFFSET + 16; const LAST_UPDATE_SLOT_OFFSET: usize = SCALE_OFFSET + 4; diff --git a/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/tests/test_perpetual_futures.rs b/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/tests/test_perpetual_futures.rs index cd1ea07ef..cde376ec1 100644 --- a/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/tests/test_perpetual_futures.rs +++ b/finance/perpetual-futures/anchor-v1/programs/perpetual-futures/tests/test_perpetual_futures.rs @@ -87,16 +87,16 @@ impl Market { include_bytes!("../../../target/deploy/perpetual_futures.so"), ) .unwrap(); - // Use std::fs::read() instead of include_bytes!() for the switchboard program because + // Use std::fs::read() instead of include_bytes!() for the mock feed program because // include_bytes!() runs at compile time, and during `anchor build` the IDL generation // step compiles tests before the .so files exist. Since this is a cross-program - // dependency (not our own program), mock_switchboard.so may not be built yet at compile time. - let switchboard_bytes = std::fs::read(concat!( + // dependency (not our own program), mock_price_feed.so may not be built yet at compile time. + let mock_feed_bytes = std::fs::read(concat!( env!("CARGO_MANIFEST_DIR"), - "/../../target/deploy/mock_switchboard.so" + "/../../target/deploy/mock_price_feed.so" )) - .expect("mock_switchboard.so not found - run `anchor build` first"); - svm.add_program(mock_switchboard::id(), &switchboard_bytes) + .expect("mock_price_feed.so not found - run `anchor build` first"); + svm.add_program(mock_price_feed::id(), &mock_feed_bytes) .unwrap(); let payer = create_wallet(&mut svm, 100_000_000_000).unwrap(); @@ -107,14 +107,14 @@ impl Market { // program; the admin is its update authority. let feed_keypair = Keypair::new(); let initialize_feed = Instruction::new_with_bytes( - mock_switchboard::id(), - &mock_switchboard::instruction::InitializeFeed { + mock_price_feed::id(), + &mock_price_feed::instruction::InitializeFeed { price: initial_price, scale: ORACLE_SCALE, confidence: 0, } .data(), - mock_switchboard::accounts::InitializeFeedAccountConstraints { + mock_price_feed::accounts::InitializeFeedAccountConstraints { feed: feed_keypair.pubkey(), authority: admin.pubkey(), system_program: system_program::id(), @@ -192,9 +192,9 @@ impl Market { fn set_price_with_confidence(&mut self, price: i128, confidence: u64) { let set_price = Instruction::new_with_bytes( - mock_switchboard::id(), - &mock_switchboard::instruction::SetPrice { price, confidence }.data(), - mock_switchboard::accounts::SetPriceAccountConstraints { + mock_price_feed::id(), + &mock_price_feed::instruction::SetPrice { price, confidence }.data(), + mock_price_feed::accounts::SetPriceAccountConstraints { feed: self.feed, authority: self.admin.pubkey(), } diff --git a/finance/perpetual-futures/anchor/Anchor.toml b/finance/perpetual-futures/anchor/Anchor.toml index e16a2097e..22c7dc54f 100644 --- a/finance/perpetual-futures/anchor/Anchor.toml +++ b/finance/perpetual-futures/anchor/Anchor.toml @@ -9,7 +9,7 @@ skip-lint = false [programs.localnet] perpetual_futures = "3uCm8Jep469pHUpYQCh6eA6dpYV3ogvTvaRDZBPtw5So" -mock_switchboard = "FnisQqhF56BxVYh5Wt8xW8wuTVN6STAGnk13MM5SRM7b" +mock_price_feed = "FnisQqhF56BxVYh5Wt8xW8wuTVN6STAGnk13MM5SRM7b" [provider] cluster = "localnet" diff --git a/finance/perpetual-futures/anchor/CHANGELOG.md b/finance/perpetual-futures/anchor/CHANGELOG.md index 023feeaad..109eb9cc8 100644 --- a/finance/perpetual-futures/anchor/CHANGELOG.md +++ b/finance/perpetual-futures/anchor/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2026-09-23 + +The mock oracle program is now `mock-price-feed` (library and program +`mock_price_feed`), with the same program ID, instructions and +account layout. The oracle network it was modeled on has shut down, so the +production path described in `state/oracle.rs` now reads a Pyth +`PriceUpdateV2` account, as `basics/pyth` does. No behavior changes. + ## 2026-09-22 Accrue funding by the wall clock instead of by slots. The rate was quoted per diff --git a/finance/perpetual-futures/anchor/README.md b/finance/perpetual-futures/anchor/README.md index d4b0f374a..92ea43dd4 100644 --- a/finance/perpetual-futures/anchor/README.md +++ b/finance/perpetual-futures/anchor/README.md @@ -19,7 +19,7 @@ A [perpetual future](https://www.investopedia.com/terms/f/futurescontract.asp) ( ## Programs - `perpetual-futures`: The exchange: pool creation, liquidity provision, opening/closing leveraged positions, funding, liquidation, and fee collection. -- `mock-switchboard`: Test-only price feed. Stores a price, scale, last-update slot, and confidence band that tests write directly. Replaced by a real [Switchboard](https://docs.switchboard.xyz/) On-Demand feed in production. +- `mock-price-feed`: Test-only price feed. Stores a price, scale, last-update slot, and confidence band that tests write directly. Replaced in production by a Pyth `PriceUpdateV2` account, as read in [`basics/pyth`](../../../basics/pyth/). All money math is integer `u128` with `checked_*` operations, multiplying before dividing and rounding in the pool's favour: no floats, no fixed-point library. diff --git a/finance/perpetual-futures/anchor/TERMINOLOGY.md b/finance/perpetual-futures/anchor/TERMINOLOGY.md index 9ef81ba16..26c7bd5e0 100644 --- a/finance/perpetual-futures/anchor/TERMINOLOGY.md +++ b/finance/perpetual-futures/anchor/TERMINOLOGY.md @@ -31,6 +31,7 @@ Terms used in this example, in the sense they carry here. - **Liquidity-provider share** — a token representing a pro-rata claim on assets-under-management. - **Oracle feed** — the account the pool reads its price from. This example uses - a mock Switchboard On-Demand feed; production points at a real one. + a mock oracle price feed; production points at a real one, such as + a Pyth price feed. - **Mark price** — the price positions are valued at. Here it is the oracle price directly, with no separate mark/index distinction. diff --git a/finance/perpetual-futures/anchor/programs/mock-switchboard/Cargo.toml b/finance/perpetual-futures/anchor/programs/mock-price-feed/Cargo.toml similarity index 86% rename from finance/perpetual-futures/anchor/programs/mock-switchboard/Cargo.toml rename to finance/perpetual-futures/anchor/programs/mock-price-feed/Cargo.toml index f199b8d30..9093d6c60 100644 --- a/finance/perpetual-futures/anchor/programs/mock-switchboard/Cargo.toml +++ b/finance/perpetual-futures/anchor/programs/mock-price-feed/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "mock_switchboard" +name = "mock_price_feed" version = "0.1.0" -description = "Mock Switchboard On-Demand feed for testing the perpetual-futures program" +description = "Mock oracle price feed for testing the perpetual-futures program" edition = "2021" [lib] crate-type = ["cdylib", "lib"] -name = "mock_switchboard" +name = "mock_price_feed" [features] default = [] diff --git a/finance/perpetual-futures/anchor/programs/mock-switchboard/src/lib.rs b/finance/perpetual-futures/anchor/programs/mock-price-feed/src/lib.rs similarity index 66% rename from finance/perpetual-futures/anchor/programs/mock-switchboard/src/lib.rs rename to finance/perpetual-futures/anchor/programs/mock-price-feed/src/lib.rs index f2f638fc5..5f28a1667 100644 --- a/finance/perpetual-futures/anchor/programs/mock-switchboard/src/lib.rs +++ b/finance/perpetual-futures/anchor/programs/mock-price-feed/src/lib.rs @@ -1,16 +1,17 @@ -//! Mock Switchboard On-Demand feed for testing the perpetual-futures program. +//! Mock oracle price feed for testing the perpetual-futures program. //! -//! Real Switchboard On-Demand feeds are program-owned accounts whose data is -//! produced by an offchain oracle network and verified onchain via Ed25519 -//! signatures over the latest price update. That verification path is -//! out-of-scope for this teaching example, so this mock stores a single price -//! the test harness writes directly, plus the slot the update happened in. +//! A real price feed account is written by an oracle network's receiver +//! program, which verifies the network's signatures over each price update +//! before recording it. That verification path is out of scope for this +//! teaching example, so this mock stores a single price the test harness +//! writes directly, plus the slot the update happened in. //! -//! The perpetual-futures program reads this feed the same way it would read a -//! real feed: load the account, decode the layout, read `price`, `scale`, and -//! `last_update_slot` (see `perpetual_futures::state::oracle`). Swap this -//! program ID for `SBondMDrcV3K4kxZR1HNVT7osZxAHVHgYXL5Ze1oMUv` (Switchboard -//! On-Demand) and adapt the layout to consume real feeds in production. +//! The perpetual-futures program reads this feed the same way it would read a real +//! feed: load the account, decode the layout, read `price`, `scale`, and +//! `last_update_slot` (see `perpetual_futures::state::oracle`). In production the +//! program reads a Pyth `PriceUpdateV2` account instead, owned by the Pyth +//! Receiver program `rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ`. The oracle +//! module describes that change, and `basics/pyth` reads one. //! //! NOT FOR PRODUCTION. use anchor_lang::prelude::*; @@ -18,7 +19,7 @@ use anchor_lang::prelude::*; declare_id!("FnisQqhF56BxVYh5Wt8xW8wuTVN6STAGnk13MM5SRM7b"); #[program] -pub mod mock_switchboard { +pub mod mock_price_feed { use super::*; /// Initialize the mock feed with an initial price. The signer becomes the @@ -38,9 +39,9 @@ pub mod mock_switchboard { Ok(()) } - /// Push a new price (and confidence band) to the mock feed. In real - /// Switchboard this would be a signed update from the oracle network; here it - /// is an authority-gated write, because the goal is to drive deterministic + /// Push a new price (and confidence band) to the mock feed. For a real + /// feed this would be a signed update from the oracle network; here it is + /// an authority-gated write, because the goal is to drive deterministic /// test scenarios. pub fn set_price( context: &mut Context, @@ -79,15 +80,16 @@ pub struct SetPriceAccountConstraints { pub authority: Signer, } -/// Mock of a Switchboard On-Demand feed. Real feeds carry many more fields -/// (median, range, sample window, signatures) — this is the bare minimum the +/// Mock of an oracle price feed. Real feeds carry many more fields (feed ID, +/// publish time, EMA price, verification level) — this is the bare minimum the /// perpetual-futures program needs to do a price comparison. #[derive(InitSpace)] #[account(borsh)] pub struct MockFeed { pub authority: Address, - /// Signed 128-bit fixed-point price. Real Switchboard prices are also i128. + /// Signed 128-bit fixed-point price, wide enough for any feed's price + /// (Pyth's is an i64 with a separate exponent). pub price: i128, /// Number of decimal places implied by `price`. E.g. `scale = 8` means diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/Cargo.toml b/finance/perpetual-futures/anchor/programs/perpetual-futures/Cargo.toml index 8052c1659..42ad14526 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/Cargo.toml +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/Cargo.toml @@ -71,7 +71,7 @@ solana-kite = "0.4.0" borsh = "1.6.1" # The LiteSVM tests load the compiled mock oracle program; depending on the # crate here lets the tests reuse its instruction-argument types and program ID. -mock_switchboard = { path = "../mock-switchboard", features = ["no-entrypoint"] } +mock_price_feed = { path = "../mock-price-feed", features = ["no-entrypoint"] } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] } diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/initialize_pool.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/initialize_pool.rs index 6fd9d3edc..558723e31 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/initialize_pool.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/instructions/initialize_pool.rs @@ -123,7 +123,7 @@ pub struct InitializePoolAccountConstraints { /// CHECK: The oracle feed account. Its key is stored on the pool and every /// read validates the layout, scale, and freshness; it is never trusted by - /// type. Swap for a real Switchboard feed in production. + /// type. Swap for a real Pyth price feed in production. pub oracle_feed: UncheckedAccount, /// Liquidity-provider share mint. The pool account is its mint authority diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/oracle.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/oracle.rs index 04b0b5057..9b9a19749 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/oracle.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/src/state/oracle.rs @@ -5,16 +5,20 @@ use crate::constants::{BASIS_POINTS_DENOMINATOR, MAX_PRICE_STALENESS_SLOTS}; use crate::errors::PerpError; // Byte layout of the feed account this program reads. It matches the -// `mock_switchboard::MockFeed` account: an 8-byte Anchor discriminator followed +// `mock_price_feed::MockFeed` account: an 8-byte Anchor discriminator followed // by `authority: Address (32)`, `price: i128 (16)`, `scale: u32 (4)`, // `last_update_slot: u64 (8)`, `confidence: u64 (8)`. // // We read the raw bytes rather than deserializing the mock account type so this -// program stays decoupled from the mock. To consume a real Switchboard -// On-Demand feed, replace the offsets below with a call to -// `switchboard_on_demand::PullFeedAccountData::parse_and_verify(...)`, which -// also checks the Ed25519 signatures over the price update — the only other -// change is the feed account's owning program ID. +// program stays decoupled from the mock. To consume a real Pyth feed, take the +// account as a `PriceUpdateV2` instead of reading offsets: from the +// `pyth-solana-receiver-sdk` crate, or the vendored copy in `basics/pyth` on +// Anchor 2. The Pyth Receiver program writes that account only after checking +// the Wormhole guardian signatures over the update, and the account type's +// owner check rejects any account that program does not own. Map +// `price_message.price`, `exponent` (the scale is its negation), `conf`, and +// `publish_time` or `posted_slot` onto the checks below, and also check +// `feed_id` and `verification_level`. // // A real feed reports a value plus a `confidence` band (a standard-deviation-like // uncertainty). This reader rejects a price whose band is too wide relative to @@ -26,7 +30,8 @@ use crate::errors::PerpError; // The feed account's owning program is NOT checked here: the pool trusts // whatever feed address its creator configured, which is inside the trust model // (the creator picks the oracle). A production reader must also verify the -// account owner is the oracle program, which `parse_and_verify` does. +// account owner is the oracle program, which the `PriceUpdateV2` account type +// does. const PRICE_OFFSET: usize = 8 + 32; const SCALE_OFFSET: usize = PRICE_OFFSET + 16; const LAST_UPDATE_SLOT_OFFSET: usize = SCALE_OFFSET + 4; diff --git a/finance/perpetual-futures/anchor/programs/perpetual-futures/tests/test_perpetual_futures.rs b/finance/perpetual-futures/anchor/programs/perpetual-futures/tests/test_perpetual_futures.rs index b01f1a98a..7ec642eeb 100644 --- a/finance/perpetual-futures/anchor/programs/perpetual-futures/tests/test_perpetual_futures.rs +++ b/finance/perpetual-futures/anchor/programs/perpetual-futures/tests/test_perpetual_futures.rs @@ -85,16 +85,16 @@ impl Market { include_bytes!("../../../target/deploy/perpetual_futures.so"), ) .unwrap(); - // Use std::fs::read() instead of include_bytes!() for the switchboard program because + // Use std::fs::read() instead of include_bytes!() for the mock feed program because // include_bytes!() runs at compile time, and during `anchor build` the IDL generation // step compiles tests before the .so files exist. Since this is a cross-program - // dependency (not our own program), mock_switchboard.so may not be built yet at compile time. - let switchboard_bytes = std::fs::read(concat!( + // dependency (not our own program), mock_price_feed.so may not be built yet at compile time. + let mock_feed_bytes = std::fs::read(concat!( env!("CARGO_MANIFEST_DIR"), - "/../../target/deploy/mock_switchboard.so" + "/../../target/deploy/mock_price_feed.so" )) - .expect("mock_switchboard.so not found - run `anchor build` first"); - svm.add_program(mock_switchboard::id(), &switchboard_bytes) + .expect("mock_price_feed.so not found - run `anchor build` first"); + svm.add_program(mock_price_feed::id(), &mock_feed_bytes) .unwrap(); let payer = create_wallet(&mut svm, 100_000_000_000).unwrap(); @@ -105,14 +105,14 @@ impl Market { // program; the admin is its update authority. let feed_keypair = Keypair::new(); let initialize_feed = Instruction::new_with_bytes( - mock_switchboard::id(), - &mock_switchboard::instruction::InitializeFeed { + mock_price_feed::id(), + &mock_price_feed::instruction::InitializeFeed { price: initial_price, scale: ORACLE_SCALE, confidence: 0, } .data(), - mock_switchboard::accounts::InitializeFeedAccountConstraints { + mock_price_feed::accounts::InitializeFeedAccountConstraints { feed: feed_keypair.pubkey(), authority: admin.pubkey(), system_program: system_program::ID, @@ -190,9 +190,9 @@ impl Market { fn set_price_with_confidence(&mut self, price: i128, confidence: u64) { let set_price = Instruction::new_with_bytes( - mock_switchboard::id(), - &mock_switchboard::instruction::SetPrice { price, confidence }.data(), - mock_switchboard::accounts::SetPriceAccountConstraints { + mock_price_feed::id(), + &mock_price_feed::instruction::SetPrice { price, confidence }.data(), + mock_price_feed::accounts::SetPriceAccountConstraints { feed: self.feed, authority: self.admin.pubkey(), } diff --git a/finance/perpetual-futures/quasar/CHANGELOG.md b/finance/perpetual-futures/quasar/CHANGELOG.md index e2d92aa31..a9b23791a 100644 --- a/finance/perpetual-futures/quasar/CHANGELOG.md +++ b/finance/perpetual-futures/quasar/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2026-09-23 + +Documentation only: a production feed is now described as a Pyth +`PriceUpdateV2` account, since the oracle network the test feed was modeled on +has shut down. + ## 2026-09-22 Accrue funding by the wall clock instead of by slots. The rate was quoted per diff --git a/finance/perpetual-futures/quasar/README.md b/finance/perpetual-futures/quasar/README.md index 0ad945d47..53493c13d 100644 --- a/finance/perpetual-futures/quasar/README.md +++ b/finance/perpetual-futures/quasar/README.md @@ -22,7 +22,7 @@ math. This page only covers what differs in the Quasar version. of wall-clock time in zero slots). - **Oracle feed in tests.** Rather than a separate mock-oracle program, the tests write the feed account's bytes directly (price, scale, last-update slot) - and the program reads them the same way it would read a real Switchboard feed. + and the program reads them the same way it would read a real oracle feed. - **State writes** use Quasar's zero-copy field accessors (`field.get()` / `field.set()`) and `set_inner`, rather than Anchor's `Account` mutation. diff --git a/finance/perpetual-futures/quasar/src/instructions/shared.rs b/finance/perpetual-futures/quasar/src/instructions/shared.rs index 9e14cb46c..8dccb52ce 100644 --- a/finance/perpetual-futures/quasar/src/instructions/shared.rs +++ b/finance/perpetual-futures/quasar/src/instructions/shared.rs @@ -45,8 +45,8 @@ fn overflow() -> ProgramError { // Byte layout of the oracle feed account: price (i128), scale (u32), // last_update_slot (u64), confidence (u64). The tests craft this directly; in -// production it would be a real Switchboard On-Demand feed parsed with signature -// verification. +// production it would be a Pyth `PriceUpdateV2` account, which the Pyth +// Receiver program writes only after verifying the update's signatures. // // Like the Anchor sibling, this validates freshness, positivity, and the // confidence band (`confidence / price`), rejecting a price whose band is too diff --git a/finance/prop-amm/anchor-v1/Anchor.toml b/finance/prop-amm/anchor-v1/Anchor.toml index 4284d903d..f6eba6d4e 100644 --- a/finance/prop-amm/anchor-v1/Anchor.toml +++ b/finance/prop-amm/anchor-v1/Anchor.toml @@ -9,7 +9,7 @@ skip-lint = false [programs.localnet] prop_amm = "9ZMtJFtn5n4wwpEeXXG5paFQakcDtrd3ova5ptJL4VT1" -mock_switchboard = "BdFYarCfgMJhC26dFN6JXKVx4kUEM3k2wYaNjyjFrvAD" +mock_price_feed = "BdFYarCfgMJhC26dFN6JXKVx4kUEM3k2wYaNjyjFrvAD" [provider] cluster = "localnet" diff --git a/finance/prop-amm/anchor-v1/CHANGELOG.md b/finance/prop-amm/anchor-v1/CHANGELOG.md index d8f033115..d1c91e84d 100644 --- a/finance/prop-amm/anchor-v1/CHANGELOG.md +++ b/finance/prop-amm/anchor-v1/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2026-09-23 + +The mock oracle program is now `mock-price-feed` (library and program +`mock_price_feed`), with the same program ID, instructions and +account layout. The oracle network it was modeled on has shut down, so the +production path described in `state/oracle.rs` now reads a Pyth +`PriceUpdateV2` account, as `basics/pyth` does. No behavior changes. + ## 2026-09-10 The `Market` account now owns both vaults and signs their outgoing transfers @@ -38,5 +46,5 @@ same gates; only the amounts changed. Initial version: an oracle-quoted proprietary AMM. One operator funds the market's inventory and quotes both sides of it at the oracle price plus a -spread; anyone can swap against the quotes. Includes the `mock-switchboard` +spread; anyone can swap against the quotes. Includes the `mock-price-feed` oracle program for deterministic tests. diff --git a/finance/prop-amm/anchor-v1/README.md b/finance/prop-amm/anchor-v1/README.md index 034fe9dee..236e528bd 100644 --- a/finance/prop-amm/anchor-v1/README.md +++ b/finance/prop-amm/anchor-v1/README.md @@ -20,7 +20,7 @@ via Jupiter routing rather than their own user interfaces. - **`prop-amm`**: the market. One operator, one base/quote pair, one oracle feed, two vaults, five instruction handlers. -- **`mock-switchboard`**: a minimal stand-in for a Switchboard On-Demand +- **`mock-price-feed`**: a minimal stand-in for an oracle's price feed, so tests can drive deterministic price scenarios. Not for production. @@ -138,8 +138,8 @@ misprices. - Lifinity's public design notes and the Helius write-up "Solana's Proprietary AMM Revolution" are good next reads. - The oracle reader deliberately reads raw bytes at fixed offsets and - documents how to swap in `switchboard_on_demand::PullFeedAccountData:: - parse_and_verify(...)` for production. + documents how to read a Pyth `PriceUpdateV2` account + instead in production. ## Limitations diff --git a/finance/perpetual-futures/anchor-v1/programs/mock-switchboard/Cargo.toml b/finance/prop-amm/anchor-v1/programs/mock-price-feed/Cargo.toml similarity index 74% rename from finance/perpetual-futures/anchor-v1/programs/mock-switchboard/Cargo.toml rename to finance/prop-amm/anchor-v1/programs/mock-price-feed/Cargo.toml index f06b0eba1..9d91fc558 100644 --- a/finance/perpetual-futures/anchor-v1/programs/mock-switchboard/Cargo.toml +++ b/finance/prop-amm/anchor-v1/programs/mock-price-feed/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "mock_switchboard" +name = "mock_price_feed" version = "0.1.0" -description = "Mock Switchboard On-Demand feed for testing the perpetual-futures program" +description = "Mock oracle price feed for testing the prop-amm program" edition = "2021" [lib] crate-type = ["cdylib", "lib"] -name = "mock_switchboard" +name = "mock_price_feed" [features] default = [] diff --git a/finance/prop-amm/anchor-v1/programs/mock-switchboard/src/lib.rs b/finance/prop-amm/anchor-v1/programs/mock-price-feed/src/lib.rs similarity index 67% rename from finance/prop-amm/anchor-v1/programs/mock-switchboard/src/lib.rs rename to finance/prop-amm/anchor-v1/programs/mock-price-feed/src/lib.rs index 66d29a659..8256241e6 100644 --- a/finance/prop-amm/anchor-v1/programs/mock-switchboard/src/lib.rs +++ b/finance/prop-amm/anchor-v1/programs/mock-price-feed/src/lib.rs @@ -1,16 +1,17 @@ -//! Mock Switchboard On-Demand feed for testing the prop-amm program. +//! Mock oracle price feed for testing the prop-amm program. //! -//! Real Switchboard On-Demand feeds are program-owned accounts whose data is -//! produced by an offchain oracle network and verified onchain via Ed25519 -//! signatures over the latest price update. That verification path is -//! out-of-scope for this teaching example, so this mock stores a single price -//! the test harness writes directly, plus the slot the update happened in. +//! A real price feed account is written by an oracle network's receiver +//! program, which verifies the network's signatures over each price update +//! before recording it. That verification path is out of scope for this +//! teaching example, so this mock stores a single price the test harness +//! writes directly, plus the slot the update happened in. //! //! The prop-amm program reads this feed the same way it would read a real //! feed: load the account, decode the layout, read `price`, `scale`, and -//! `last_update_slot` (see `prop_amm::state::oracle`). Swap this program ID -//! for `SBondMDrcV3K4kxZR1HNVT7osZxAHVHgYXL5Ze1oMUv` (Switchboard On-Demand) -//! and adapt the layout to consume real feeds in production. +//! `last_update_slot` (see `prop_amm::state::oracle`). In production the +//! program reads a Pyth `PriceUpdateV2` account instead, owned by the Pyth +//! Receiver program `rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ`. The oracle +//! module describes that change, and `basics/pyth` reads one. //! //! NOT FOR PRODUCTION. use anchor_lang::prelude::*; @@ -18,7 +19,7 @@ use anchor_lang::prelude::*; declare_id!("BdFYarCfgMJhC26dFN6JXKVx4kUEM3k2wYaNjyjFrvAD"); #[program] -pub mod mock_switchboard { +pub mod mock_price_feed { use super::*; /// Initialize the mock feed with an initial price. The signer becomes the @@ -38,9 +39,9 @@ pub mod mock_switchboard { Ok(()) } - /// Push a new price (and confidence band) to the mock feed. In real - /// Switchboard this would be a signed update from the oracle network; here it - /// is an authority-gated write, because the goal is to drive deterministic + /// Push a new price (and confidence band) to the mock feed. For a real + /// feed this would be a signed update from the oracle network; here it is + /// an authority-gated write, because the goal is to drive deterministic /// test scenarios. pub fn set_price( context: Context, @@ -81,15 +82,16 @@ pub struct SetPriceAccountConstraints<'info> { pub authority: Signer<'info>, } -/// Mock of a Switchboard On-Demand feed. Real feeds carry many more fields -/// (median, range, sample window, signatures) — this is the bare minimum the +/// Mock of an oracle price feed. Real feeds carry many more fields (feed ID, +/// publish time, EMA price, verification level) — this is the bare minimum the /// prop-amm program needs to price a quote. #[derive(InitSpace)] #[account] pub struct MockFeed { pub authority: Pubkey, - /// Signed 128-bit fixed-point price. Real Switchboard prices are also i128. + /// Signed 128-bit fixed-point price, wide enough for any feed's price + /// (Pyth's is an i64 with a separate exponent). pub price: i128, /// Number of decimal places implied by `price`. E.g. `scale = 8` means diff --git a/finance/prop-amm/anchor-v1/programs/prop-amm/Cargo.toml b/finance/prop-amm/anchor-v1/programs/prop-amm/Cargo.toml index 3e10ff424..24c49330c 100644 --- a/finance/prop-amm/anchor-v1/programs/prop-amm/Cargo.toml +++ b/finance/prop-amm/anchor-v1/programs/prop-amm/Cargo.toml @@ -41,7 +41,7 @@ solana-kite = "0.5.0" borsh = "1.6.1" # The LiteSVM tests load the compiled mock oracle program; depending on the # crate here lets the tests reuse its instruction-argument types and program ID. -mock_switchboard = { path = "../mock-switchboard", features = ["no-entrypoint"] } +mock_price_feed = { path = "../mock-price-feed", features = ["no-entrypoint"] } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] } diff --git a/finance/prop-amm/anchor-v1/programs/prop-amm/src/instructions/initialize_market.rs b/finance/prop-amm/anchor-v1/programs/prop-amm/src/instructions/initialize_market.rs index 3445b70e0..bf7df72e9 100644 --- a/finance/prop-amm/anchor-v1/programs/prop-amm/src/instructions/initialize_market.rs +++ b/finance/prop-amm/anchor-v1/programs/prop-amm/src/instructions/initialize_market.rs @@ -89,7 +89,7 @@ pub struct InitializeMarketAccountConstraints<'info> { /// CHECK: The oracle feed account. Its key is stored on the market and /// every read validates the layout, scale, and freshness; it is never - /// trusted by type. Swap for a real Switchboard feed in production. + /// trusted by type. Swap for a real Pyth price feed in production. pub oracle_feed: UncheckedAccount<'info>, // The market account itself is the token authority of both vaults and diff --git a/finance/prop-amm/anchor-v1/programs/prop-amm/src/state/oracle.rs b/finance/prop-amm/anchor-v1/programs/prop-amm/src/state/oracle.rs index 83479816a..f4a834016 100644 --- a/finance/prop-amm/anchor-v1/programs/prop-amm/src/state/oracle.rs +++ b/finance/prop-amm/anchor-v1/programs/prop-amm/src/state/oracle.rs @@ -5,16 +5,20 @@ use crate::constants::{BASIS_POINTS_DENOMINATOR, MAX_PRICE_STALENESS_SLOTS}; use crate::errors::PropAmmError; // Byte layout of the feed account this program reads. It matches the -// `mock_switchboard::MockFeed` account: an 8-byte Anchor discriminator followed +// `mock_price_feed::MockFeed` account: an 8-byte Anchor discriminator followed // by `authority: Pubkey (32)`, `price: i128 (16)`, `scale: u32 (4)`, // `last_update_slot: u64 (8)`, `confidence: u64 (8)`. // // We read the raw bytes rather than deserializing the mock account type so this -// program stays decoupled from the mock. To consume a real Switchboard -// On-Demand feed, replace the offsets below with a call to -// `switchboard_on_demand::PullFeedAccountData::parse_and_verify(...)`, which -// also checks the Ed25519 signatures over the price update — the only other -// change is the feed account's owning program ID. +// program stays decoupled from the mock. To consume a real Pyth feed, take the +// account as a `PriceUpdateV2` instead of reading offsets: from the +// `pyth-solana-receiver-sdk` crate, or the vendored copy in `basics/pyth` on +// Anchor 2. The Pyth Receiver program writes that account only after checking +// the Wormhole guardian signatures over the update, and the account type's +// owner check rejects any account that program does not own. Map +// `price_message.price`, `exponent` (the scale is its negation), `conf`, and +// `publish_time` or `posted_slot` onto the checks below, and also check +// `feed_id` and `verification_level`. // // A real feed reports a value plus a `confidence` band (a standard-deviation-like // uncertainty). This reader rejects a price whose band is too wide relative to @@ -27,7 +31,7 @@ use crate::errors::PropAmmError; // model (the operator quotes its own capital against its own oracle choice; a // bad feed loses the operator's money, not the traders'). A production reader // must still verify the account owner is the oracle program, which -// `parse_and_verify` does. +// the `PriceUpdateV2` account type does. const PRICE_OFFSET: usize = 8 + 32; const SCALE_OFFSET: usize = PRICE_OFFSET + 16; const LAST_UPDATE_SLOT_OFFSET: usize = SCALE_OFFSET + 4; diff --git a/finance/prop-amm/anchor-v1/programs/prop-amm/tests/test_prop_amm.rs b/finance/prop-amm/anchor-v1/programs/prop-amm/tests/test_prop_amm.rs index 526e72e4f..b098949ab 100644 --- a/finance/prop-amm/anchor-v1/programs/prop-amm/tests/test_prop_amm.rs +++ b/finance/prop-amm/anchor-v1/programs/prop-amm/tests/test_prop_amm.rs @@ -91,16 +91,16 @@ impl Market { include_bytes!("../../../target/deploy/prop_amm.so"), ) .unwrap(); - // Use std::fs::read() instead of include_bytes!() for the switchboard program because + // Use std::fs::read() instead of include_bytes!() for the mock feed program because // include_bytes!() runs at compile time, and during `anchor build` the IDL generation // step compiles tests before the .so files exist. Since this is a cross-program - // dependency (not our own program), mock_switchboard.so may not be built yet at compile time. - let switchboard_bytes = std::fs::read(concat!( + // dependency (not our own program), mock_price_feed.so may not be built yet at compile time. + let mock_feed_bytes = std::fs::read(concat!( env!("CARGO_MANIFEST_DIR"), - "/../../target/deploy/mock_switchboard.so" + "/../../target/deploy/mock_price_feed.so" )) - .expect("mock_switchboard.so not found - run `anchor build` first"); - svm.add_program(mock_switchboard::id(), &switchboard_bytes) + .expect("mock_price_feed.so not found - run `anchor build` first"); + svm.add_program(mock_price_feed::id(), &mock_feed_bytes) .unwrap(); let payer = create_wallet(&mut svm, 100_000_000_000).unwrap(); @@ -112,14 +112,14 @@ impl Market { // program; the operator is its update authority. let feed_keypair = Keypair::new(); let initialize_feed = Instruction::new_with_bytes( - mock_switchboard::id(), - &mock_switchboard::instruction::InitializeFeed { + mock_price_feed::id(), + &mock_price_feed::instruction::InitializeFeed { price: initial_price, scale: ORACLE_SCALE, confidence: 0, } .data(), - mock_switchboard::accounts::InitializeFeedAccountConstraints { + mock_price_feed::accounts::InitializeFeedAccountConstraints { feed: feed_keypair.pubkey(), authority: operator.pubkey(), system_program: system_program::id(), @@ -229,9 +229,9 @@ impl Market { fn set_price_with_confidence(&mut self, price: i128, confidence: u64) { let set_price = Instruction::new_with_bytes( - mock_switchboard::id(), - &mock_switchboard::instruction::SetPrice { price, confidence }.data(), - mock_switchboard::accounts::SetPriceAccountConstraints { + mock_price_feed::id(), + &mock_price_feed::instruction::SetPrice { price, confidence }.data(), + mock_price_feed::accounts::SetPriceAccountConstraints { feed: self.feed, authority: self.operator.pubkey(), } diff --git a/finance/prop-amm/anchor/Anchor.toml b/finance/prop-amm/anchor/Anchor.toml index 4284d903d..f6eba6d4e 100644 --- a/finance/prop-amm/anchor/Anchor.toml +++ b/finance/prop-amm/anchor/Anchor.toml @@ -9,7 +9,7 @@ skip-lint = false [programs.localnet] prop_amm = "9ZMtJFtn5n4wwpEeXXG5paFQakcDtrd3ova5ptJL4VT1" -mock_switchboard = "BdFYarCfgMJhC26dFN6JXKVx4kUEM3k2wYaNjyjFrvAD" +mock_price_feed = "BdFYarCfgMJhC26dFN6JXKVx4kUEM3k2wYaNjyjFrvAD" [provider] cluster = "localnet" diff --git a/finance/prop-amm/anchor/CHANGELOG.md b/finance/prop-amm/anchor/CHANGELOG.md index 94faa1295..a829f2c15 100644 --- a/finance/prop-amm/anchor/CHANGELOG.md +++ b/finance/prop-amm/anchor/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2026-09-23 + +The mock oracle program is now `mock-price-feed` (library and program +`mock_price_feed`, package `mock_price_feed_prop_amm`), with the same program ID, instructions and +account layout. The oracle network it was modeled on has shut down, so the +production path described in `state/oracle.rs` now reads a Pyth +`PriceUpdateV2` account, as `basics/pyth` does. No behavior changes. + ## 2026-09-10 The `Market` account now owns both vaults and signs their outgoing transfers @@ -30,5 +38,5 @@ same gates; only the amounts changed. Initial version: an oracle-quoted proprietary AMM. One operator funds the market's inventory and quotes both sides of it at the oracle price plus a -spread; anyone can swap against the quotes. Includes the `mock-switchboard` +spread; anyone can swap against the quotes. Includes the `mock-price-feed` oracle program for deterministic tests. diff --git a/finance/prop-amm/anchor/README.md b/finance/prop-amm/anchor/README.md index cbc16938c..0a209c117 100644 --- a/finance/prop-amm/anchor/README.md +++ b/finance/prop-amm/anchor/README.md @@ -20,7 +20,7 @@ via Jupiter routing rather than their own user interfaces. - **`prop-amm`**: the market. One operator, one base/quote pair, one oracle feed, two vaults, five instruction handlers. -- **`mock-switchboard`**: a minimal stand-in for a Switchboard On-Demand +- **`mock-price-feed`**: a minimal stand-in for an oracle's price feed, so tests can drive deterministic price scenarios. Not for production. @@ -138,8 +138,8 @@ misprices. - Lifinity's public design notes and the Helius write-up "Solana's Proprietary AMM Revolution" are good next reads. - The oracle reader deliberately reads raw bytes at fixed offsets and - documents how to swap in `switchboard_on_demand::PullFeedAccountData:: - parse_and_verify(...)` for production. + documents how to read a Pyth `PriceUpdateV2` account + instead in production. ## Limitations diff --git a/finance/prop-amm/anchor/programs/mock-switchboard/Cargo.toml b/finance/prop-amm/anchor/programs/mock-price-feed/Cargo.toml similarity index 84% rename from finance/prop-amm/anchor/programs/mock-switchboard/Cargo.toml rename to finance/prop-amm/anchor/programs/mock-price-feed/Cargo.toml index a5a34cd76..f01220b60 100644 --- a/finance/prop-amm/anchor/programs/mock-switchboard/Cargo.toml +++ b/finance/prop-amm/anchor/programs/mock-price-feed/Cargo.toml @@ -2,16 +2,16 @@ # The perpetual-futures project has a mock of the same feed. Two path packages # with one name cannot both appear in a lockfile, and the repository-root # workspace pulls both in, so the package names differ. `[lib] name` stays -# `mock_switchboard`, which is what Anchor.toml, the IDL and the .so are keyed +# `mock_price_feed`, which is what Anchor.toml, the IDL and the .so are keyed # on, and the dependent renames it back with `package = `. -name = "mock_switchboard_prop_amm" +name = "mock_price_feed_prop_amm" version = "0.1.0" -description = "Mock Switchboard On-Demand feed for testing the prop-amm program" +description = "Mock oracle price feed for testing the prop-amm program" edition = "2021" [lib] crate-type = ["cdylib", "lib"] -name = "mock_switchboard" +name = "mock_price_feed" [features] default = [] diff --git a/finance/prop-amm/anchor/programs/mock-switchboard/src/lib.rs b/finance/prop-amm/anchor/programs/mock-price-feed/src/lib.rs similarity index 67% rename from finance/prop-amm/anchor/programs/mock-switchboard/src/lib.rs rename to finance/prop-amm/anchor/programs/mock-price-feed/src/lib.rs index 03ae2006a..423f16521 100644 --- a/finance/prop-amm/anchor/programs/mock-switchboard/src/lib.rs +++ b/finance/prop-amm/anchor/programs/mock-price-feed/src/lib.rs @@ -1,16 +1,17 @@ -//! Mock Switchboard On-Demand feed for testing the prop-amm program. +//! Mock oracle price feed for testing the prop-amm program. //! -//! Real Switchboard On-Demand feeds are program-owned accounts whose data is -//! produced by an offchain oracle network and verified onchain via Ed25519 -//! signatures over the latest price update. That verification path is -//! out-of-scope for this teaching example, so this mock stores a single price -//! the test harness writes directly, plus the slot the update happened in. +//! A real price feed account is written by an oracle network's receiver +//! program, which verifies the network's signatures over each price update +//! before recording it. That verification path is out of scope for this +//! teaching example, so this mock stores a single price the test harness +//! writes directly, plus the slot the update happened in. //! //! The prop-amm program reads this feed the same way it would read a real //! feed: load the account, decode the layout, read `price`, `scale`, and -//! `last_update_slot` (see `prop_amm::state::oracle`). Swap this program ID -//! for `SBondMDrcV3K4kxZR1HNVT7osZxAHVHgYXL5Ze1oMUv` (Switchboard On-Demand) -//! and adapt the layout to consume real feeds in production. +//! `last_update_slot` (see `prop_amm::state::oracle`). In production the +//! program reads a Pyth `PriceUpdateV2` account instead, owned by the Pyth +//! Receiver program `rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ`. The oracle +//! module describes that change, and `basics/pyth` reads one. //! //! NOT FOR PRODUCTION. use anchor_lang::prelude::*; @@ -18,7 +19,7 @@ use anchor_lang::prelude::*; declare_id!("BdFYarCfgMJhC26dFN6JXKVx4kUEM3k2wYaNjyjFrvAD"); #[program] -pub mod mock_switchboard { +pub mod mock_price_feed { use super::*; /// Initialize the mock feed with an initial price. The signer becomes the @@ -38,9 +39,9 @@ pub mod mock_switchboard { Ok(()) } - /// Push a new price (and confidence band) to the mock feed. In real - /// Switchboard this would be a signed update from the oracle network; here it - /// is an authority-gated write, because the goal is to drive deterministic + /// Push a new price (and confidence band) to the mock feed. For a real + /// feed this would be a signed update from the oracle network; here it is + /// an authority-gated write, because the goal is to drive deterministic /// test scenarios. pub fn set_price( context: &mut Context, @@ -79,15 +80,16 @@ pub struct SetPriceAccountConstraints { pub authority: Signer, } -/// Mock of a Switchboard On-Demand feed. Real feeds carry many more fields -/// (median, range, sample window, signatures) — this is the bare minimum the +/// Mock of an oracle price feed. Real feeds carry many more fields (feed ID, +/// publish time, EMA price, verification level) — this is the bare minimum the /// prop-amm program needs to price a quote. #[derive(InitSpace)] #[account(borsh)] pub struct MockFeed { pub authority: Address, - /// Signed 128-bit fixed-point price. Real Switchboard prices are also i128. + /// Signed 128-bit fixed-point price, wide enough for any feed's price + /// (Pyth's is an i64 with a separate exponent). pub price: i128, /// Number of decimal places implied by `price`. E.g. `scale = 8` means diff --git a/finance/prop-amm/anchor/programs/prop-amm/Cargo.toml b/finance/prop-amm/anchor/programs/prop-amm/Cargo.toml index 78071caca..448706098 100644 --- a/finance/prop-amm/anchor/programs/prop-amm/Cargo.toml +++ b/finance/prop-amm/anchor/programs/prop-amm/Cargo.toml @@ -68,7 +68,7 @@ solana-kite = "0.4.0" borsh = "1.6.1" # The LiteSVM tests load the compiled mock oracle program; depending on the # crate here lets the tests reuse its instruction-argument types and program ID. -mock_switchboard = { package = "mock_switchboard_prop_amm", path = "../mock-switchboard", features = ["no-entrypoint"] } +mock_price_feed = { package = "mock_price_feed_prop_amm", path = "../mock-price-feed", features = ["no-entrypoint"] } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] } diff --git a/finance/prop-amm/anchor/programs/prop-amm/src/instructions/initialize_market.rs b/finance/prop-amm/anchor/programs/prop-amm/src/instructions/initialize_market.rs index 40049bf83..f385e70d1 100644 --- a/finance/prop-amm/anchor/programs/prop-amm/src/instructions/initialize_market.rs +++ b/finance/prop-amm/anchor/programs/prop-amm/src/instructions/initialize_market.rs @@ -90,7 +90,7 @@ pub struct InitializeMarketAccountConstraints { /// CHECK: The oracle feed account. Its key is stored on the market and /// every read validates the layout, scale, and freshness; it is never - /// trusted by type. Swap for a real Switchboard feed in production. + /// trusted by type. Swap for a real Pyth price feed in production. pub oracle_feed: UncheckedAccount, // The market account itself is the token authority of both vaults and diff --git a/finance/prop-amm/anchor/programs/prop-amm/src/state/oracle.rs b/finance/prop-amm/anchor/programs/prop-amm/src/state/oracle.rs index f7aba055b..fb6be7a35 100644 --- a/finance/prop-amm/anchor/programs/prop-amm/src/state/oracle.rs +++ b/finance/prop-amm/anchor/programs/prop-amm/src/state/oracle.rs @@ -5,16 +5,20 @@ use crate::constants::{BASIS_POINTS_DENOMINATOR, MAX_PRICE_STALENESS_SLOTS}; use crate::errors::PropAmmError; // Byte layout of the feed account this program reads. It matches the -// `mock_switchboard::MockFeed` account: an 8-byte Anchor discriminator followed +// `mock_price_feed::MockFeed` account: an 8-byte Anchor discriminator followed // by `authority: Address (32)`, `price: i128 (16)`, `scale: u32 (4)`, // `last_update_slot: u64 (8)`, `confidence: u64 (8)`. // // We read the raw bytes rather than deserializing the mock account type so this -// program stays decoupled from the mock. To consume a real Switchboard -// On-Demand feed, replace the offsets below with a call to -// `switchboard_on_demand::PullFeedAccountData::parse_and_verify(...)`, which -// also checks the Ed25519 signatures over the price update — the only other -// change is the feed account's owning program ID. +// program stays decoupled from the mock. To consume a real Pyth feed, take the +// account as a `PriceUpdateV2` instead of reading offsets: from the +// `pyth-solana-receiver-sdk` crate, or the vendored copy in `basics/pyth` on +// Anchor 2. The Pyth Receiver program writes that account only after checking +// the Wormhole guardian signatures over the update, and the account type's +// owner check rejects any account that program does not own. Map +// `price_message.price`, `exponent` (the scale is its negation), `conf`, and +// `publish_time` or `posted_slot` onto the checks below, and also check +// `feed_id` and `verification_level`. // // A real feed reports a value plus a `confidence` band (a standard-deviation-like // uncertainty). This reader rejects a price whose band is too wide relative to @@ -27,7 +31,7 @@ use crate::errors::PropAmmError; // model (the operator quotes its own capital against its own oracle choice; a // bad feed loses the operator's money, not the traders'). A production reader // must still verify the account owner is the oracle program, which -// `parse_and_verify` does. +// the `PriceUpdateV2` account type does. const PRICE_OFFSET: usize = 8 + 32; const SCALE_OFFSET: usize = PRICE_OFFSET + 16; const LAST_UPDATE_SLOT_OFFSET: usize = SCALE_OFFSET + 4; diff --git a/finance/prop-amm/anchor/programs/prop-amm/tests/test_prop_amm.rs b/finance/prop-amm/anchor/programs/prop-amm/tests/test_prop_amm.rs index e7dd3bd2a..bd5d456f5 100644 --- a/finance/prop-amm/anchor/programs/prop-amm/tests/test_prop_amm.rs +++ b/finance/prop-amm/anchor/programs/prop-amm/tests/test_prop_amm.rs @@ -89,16 +89,16 @@ impl Market { include_bytes!("../../../target/deploy/prop_amm.so"), ) .unwrap(); - // Use std::fs::read() instead of include_bytes!() for the switchboard program because + // Use std::fs::read() instead of include_bytes!() for the mock feed program because // include_bytes!() runs at compile time, and during `anchor build` the IDL generation // step compiles tests before the .so files exist. Since this is a cross-program - // dependency (not our own program), mock_switchboard.so may not be built yet at compile time. - let switchboard_bytes = std::fs::read(concat!( + // dependency (not our own program), mock_price_feed.so may not be built yet at compile time. + let mock_feed_bytes = std::fs::read(concat!( env!("CARGO_MANIFEST_DIR"), - "/../../target/deploy/mock_switchboard.so" + "/../../target/deploy/mock_price_feed.so" )) - .expect("mock_switchboard.so not found - run `anchor build` first"); - svm.add_program(mock_switchboard::id(), &switchboard_bytes) + .expect("mock_price_feed.so not found - run `anchor build` first"); + svm.add_program(mock_price_feed::id(), &mock_feed_bytes) .unwrap(); let payer = create_wallet(&mut svm, 100_000_000_000).unwrap(); @@ -110,14 +110,14 @@ impl Market { // program; the operator is its update authority. let feed_keypair = Keypair::new(); let initialize_feed = Instruction::new_with_bytes( - mock_switchboard::id(), - &mock_switchboard::instruction::InitializeFeed { + mock_price_feed::id(), + &mock_price_feed::instruction::InitializeFeed { price: initial_price, scale: ORACLE_SCALE, confidence: 0, } .data(), - mock_switchboard::accounts::InitializeFeedAccountConstraints { + mock_price_feed::accounts::InitializeFeedAccountConstraints { feed: feed_keypair.pubkey(), authority: operator.pubkey(), system_program: system_program::ID, @@ -227,9 +227,9 @@ impl Market { fn set_price_with_confidence(&mut self, price: i128, confidence: u64) { let set_price = Instruction::new_with_bytes( - mock_switchboard::id(), - &mock_switchboard::instruction::SetPrice { price, confidence }.data(), - mock_switchboard::accounts::SetPriceAccountConstraints { + mock_price_feed::id(), + &mock_price_feed::instruction::SetPrice { price, confidence }.data(), + mock_price_feed::accounts::SetPriceAccountConstraints { feed: self.feed, authority: self.operator.pubkey(), } diff --git a/finance/prop-amm/quasar/CHANGELOG.md b/finance/prop-amm/quasar/CHANGELOG.md index 925913392..b8eef425d 100644 --- a/finance/prop-amm/quasar/CHANGELOG.md +++ b/finance/prop-amm/quasar/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2026-09-23 + +Documentation only: a production feed is now described as a Pyth +`PriceUpdateV2` account, since the oracle network the test feed was modeled on +has shut down. + ## 2026-09-10 The `Market` account now owns both vaults and signs their outgoing transfers diff --git a/finance/prop-amm/quasar/README.md b/finance/prop-amm/quasar/README.md index c377fff55..32d1ce54d 100644 --- a/finance/prop-amm/quasar/README.md +++ b/finance/prop-amm/quasar/README.md @@ -25,7 +25,7 @@ Quasar version. - **Oracle feed in tests.** Rather than a separate mock-oracle program, the tests write the feed account's bytes directly (price, scale, last-update slot, confidence) and the program reads them the same way it would read a - real Switchboard feed. + real oracle feed. - **State writes** use Quasar's zero-copy field accessors (`field.get()` / `field.set()`) and `set_inner`, rather than Anchor's `Account` mutation. diff --git a/finance/prop-amm/quasar/src/instructions/shared.rs b/finance/prop-amm/quasar/src/instructions/shared.rs index cd34a30c3..0d7e0ec4d 100644 --- a/finance/prop-amm/quasar/src/instructions/shared.rs +++ b/finance/prop-amm/quasar/src/instructions/shared.rs @@ -38,11 +38,12 @@ fn overflow() -> ProgramError { // Byte layout of the oracle feed account: price (i128), scale (u32), // last_update_slot (u64), confidence (u64). The tests craft this directly; in -// production it would be a real Switchboard On-Demand feed parsed with -// signature verification. Like the Anchor sibling, this validates freshness, -// positivity, scale, and the confidence band; the feed account's owning -// program is NOT checked — the operator picks the oracle, and a bad choice -// loses the operator's money, not the traders'. +// production it would be a Pyth `PriceUpdateV2` account, which the Pyth +// Receiver program writes only after verifying the update's signatures. Like +// the Anchor sibling, this validates freshness, positivity, scale, and the +// confidence band; the feed account's owning program is NOT checked — the +// operator picks the oracle, and a bad choice loses the operator's money, not +// the traders'. const PRICE_OFFSET: usize = 0; const SCALE_OFFSET: usize = PRICE_OFFSET + 16; const LAST_UPDATE_SLOT_OFFSET: usize = SCALE_OFFSET + 4;