From 065c0a31036bae7052f3471a6d7fa47736bf198e Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Wed, 23 Sep 2026 01:37:39 +0000 Subject: [PATCH] Point the finance examples' oracle path at Pyth and rename the mock feed The oracle network the lending, perpetual futures and prop AMM examples modeled their price feeds on has shut down: every implementation is deprecated and support ends on 25 September 2026. The examples still named it throughout, and their production-path comments told readers to build on it. The perpetual futures and prop AMM mock oracle program is now mock-price-feed (library mock_price_feed) in both Anchor variants. Its program ID, instructions, account name and layout are unchanged, so the programs' offset readers and every test keep working as before; only the crate, directory, Anchor.toml key and .so name move. The root workspace exclusion and lockfile entries follow the rename. The production path in every oracle reader, README and doc comment now reads a Pyth PriceUpdateV2 account, owned by the Pyth Receiver program, as basics/pyth already does. Lending's README no longer says it chose the old network over Pyth for compute cost. Changelog entries added at the root and in all nine example variants. Claude-Session: https://claude.ai/code/session_01Bb9YgXxuvSJThr3TjtDoca --- CHANGELOG.md | 10 +++++ Cargo.lock | 8 ++-- Cargo.toml | 10 ++--- finance/lending/anchor-v1/CHANGELOG.md | 10 ++++- finance/lending/anchor-v1/README.md | 13 ++++--- .../src/instructions/admin/set_price.rs | 4 +- .../programs/lending/src/state/price_feed.rs | 13 ++++--- .../programs/lending/tests/common/mod.rs | 2 +- finance/lending/anchor/CHANGELOG.md | 10 ++++- finance/lending/anchor/README.md | 13 ++++--- .../src/instructions/admin/set_price.rs | 4 +- .../programs/lending/src/state/price_feed.rs | 13 ++++--- .../programs/lending/tests/common/mod.rs | 2 +- finance/lending/quasar/CHANGELOG.md | 9 ++++- finance/lending/quasar/README.md | 6 +-- .../lending/quasar/src/instructions/admin.rs | 2 +- finance/lending/quasar/src/state.rs | 9 +++-- finance/lending/quasar/src/tests.rs | 2 +- .../perpetual-futures/anchor-v1/Anchor.toml | 2 +- .../perpetual-futures/anchor-v1/CHANGELOG.md | 8 ++++ finance/perpetual-futures/anchor-v1/README.md | 2 +- .../anchor-v1/TERMINOLOGY.md | 3 +- .../programs/mock-price-feed}/Cargo.toml | 6 +-- .../src/lib.rs | 38 ++++++++++--------- .../programs/perpetual-futures/Cargo.toml | 2 +- .../src/instructions/initialize_pool.rs | 2 +- .../perpetual-futures/src/state/oracle.rs | 19 ++++++---- .../tests/test_perpetual_futures.rs | 24 ++++++------ finance/perpetual-futures/anchor/Anchor.toml | 2 +- finance/perpetual-futures/anchor/CHANGELOG.md | 8 ++++ finance/perpetual-futures/anchor/README.md | 2 +- .../perpetual-futures/anchor/TERMINOLOGY.md | 3 +- .../Cargo.toml | 6 +-- .../src/lib.rs | 38 ++++++++++--------- .../programs/perpetual-futures/Cargo.toml | 2 +- .../src/instructions/initialize_pool.rs | 2 +- .../perpetual-futures/src/state/oracle.rs | 19 ++++++---- .../tests/test_perpetual_futures.rs | 24 ++++++------ finance/perpetual-futures/quasar/CHANGELOG.md | 6 +++ finance/perpetual-futures/quasar/README.md | 2 +- .../quasar/src/instructions/shared.rs | 4 +- finance/prop-amm/anchor-v1/Anchor.toml | 2 +- finance/prop-amm/anchor-v1/CHANGELOG.md | 10 ++++- finance/prop-amm/anchor-v1/README.md | 6 +-- .../programs/mock-price-feed}/Cargo.toml | 6 +-- .../src/lib.rs | 34 +++++++++-------- .../anchor-v1/programs/prop-amm/Cargo.toml | 2 +- .../src/instructions/initialize_market.rs | 2 +- .../programs/prop-amm/src/state/oracle.rs | 18 +++++---- .../programs/prop-amm/tests/test_prop_amm.rs | 24 ++++++------ finance/prop-amm/anchor/Anchor.toml | 2 +- finance/prop-amm/anchor/CHANGELOG.md | 10 ++++- finance/prop-amm/anchor/README.md | 6 +-- .../Cargo.toml | 8 ++-- .../src/lib.rs | 34 +++++++++-------- .../anchor/programs/prop-amm/Cargo.toml | 2 +- .../src/instructions/initialize_market.rs | 2 +- .../programs/prop-amm/src/state/oracle.rs | 18 +++++---- .../programs/prop-amm/tests/test_prop_amm.rs | 24 ++++++------ finance/prop-amm/quasar/CHANGELOG.md | 6 +++ finance/prop-amm/quasar/README.md | 2 +- .../quasar/src/instructions/shared.rs | 11 +++--- 62 files changed, 352 insertions(+), 241 deletions(-) rename finance/{prop-amm/anchor-v1/programs/mock-switchboard => perpetual-futures/anchor-v1/programs/mock-price-feed}/Cargo.toml (75%) rename finance/perpetual-futures/anchor-v1/programs/{mock-switchboard => mock-price-feed}/src/lib.rs (66%) rename finance/perpetual-futures/anchor/programs/{mock-switchboard => mock-price-feed}/Cargo.toml (86%) rename finance/perpetual-futures/anchor/programs/{mock-switchboard => mock-price-feed}/src/lib.rs (66%) rename finance/{perpetual-futures/anchor-v1/programs/mock-switchboard => prop-amm/anchor-v1/programs/mock-price-feed}/Cargo.toml (74%) rename finance/prop-amm/anchor-v1/programs/{mock-switchboard => mock-price-feed}/src/lib.rs (67%) rename finance/prop-amm/anchor/programs/{mock-switchboard => mock-price-feed}/Cargo.toml (84%) rename finance/prop-amm/anchor/programs/{mock-switchboard => mock-price-feed}/src/lib.rs (67%) diff --git a/CHANGELOG.md b/CHANGELOG.md index d46a8663e..aade9498c 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-22] - Lending interest and perpetual futures funding accrue by the wall clock Both programs accrued over elapsed slots, so a rate quoted per year or per 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 481fe877f..be63529b7 100644 --- a/finance/lending/anchor-v1/CHANGELOG.md +++ b/finance/lending/anchor-v1/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2026-09-23 + +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 @@ -49,7 +57,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 cefc669a6..ef101553c 100644 --- a/finance/lending/anchor-v1/README.md +++ b/finance/lending/anchor-v1/README.md @@ -127,7 +127,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 @@ -140,11 +140,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 39756bd95..f0a4d319b 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 481fe877f..be63529b7 100644 --- a/finance/lending/anchor/CHANGELOG.md +++ b/finance/lending/anchor/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2026-09-23 + +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 @@ -49,7 +57,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 40713099f..1fdf8f894 100644 --- a/finance/lending/anchor/README.md +++ b/finance/lending/anchor/README.md @@ -127,7 +127,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 @@ -140,11 +140,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 d369b8df1..bbe766d0b 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 fa17e700b..cfc8620d7 100644 --- a/finance/lending/quasar/CHANGELOG.md +++ b/finance/lending/quasar/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [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. + ## [2026-09-22] ### Changed @@ -93,7 +100,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 65b29de9a..0a5f91713 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 0c1b729c1..d462f20ed 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;