From 9ca469a0759b5bc385e942a504dad40e86a04917583fdd1cd5957554b394b9fd Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 10 Jun 2026 02:25:45 +0400 Subject: [PATCH] =?UTF-8?q?solana:=20=D1=83=D1=81=D0=B8=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D1=83=20Pyt?= =?UTF-8?q?h=20oracle=20=D0=B2=20shine=5Fpayments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.properties | 4 +- shine-solana/shine/Cargo.lock | 378 +++++++++++++++++- .../shine/doc/programs/shine_payments.md | 10 +- .../shine/programs/shine_payments/Cargo.toml | 2 + .../shine/programs/shine_payments/src/lib.rs | 76 ++-- .../programs/shine_payments/src/settings.rs | 3 + 6 files changed, 430 insertions(+), 43 deletions(-) diff --git a/VERSION.properties b/VERSION.properties index 606bca7..58bff52 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.149 -server.version=1.2.141 +client.version=1.2.150 +server.version=1.2.142 diff --git a/shine-solana/shine/Cargo.lock b/shine-solana/shine/Cargo.lock index 1397baf..f660f0e 100644 --- a/shine-solana/shine/Cargo.lock +++ b/shine-solana/shine/Cargo.lock @@ -14,6 +14,188 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "anchor-attribute-access-control" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f70fd141a4d18adf11253026b32504f885447048c7494faf5fa83b01af9c0cf" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-account" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715a261c57c7679581e06f07a74fa2af874ac30f86bd8ea07cca4a7e5388a064" +dependencies = [ + "anchor-syn", + "bs58", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-constant" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730d6df8ae120321c5c25e0779e61789e4b70dc8297102248902022f286102e4" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-error" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e6e449cc3a37b2880b74dcafb8e5a17b954c0e58e376432d7adc646fb333ef" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-event" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7710e4c54adf485affcd9be9adec5ef8846d9c71d7f31e16ba86ff9fc1dd49f" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-program" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ecfd49b2aeadeb32f35262230db402abed76ce87e27562b34f61318b2ec83c" +dependencies = [ + "anchor-lang-idl", + "anchor-syn", + "anyhow", + "bs58", + "heck", + "proc-macro2", + "quote", + "serde_json", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-accounts" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be89d160793a88495af462a7010b3978e48e30a630c91de47ce2c1d3cb7a6149" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-serde" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc6ee78acb7bfe0c2dd2abc677aaa4789c0281a0c0ef01dbf6fe85e0fd9e6e4" +dependencies = [ + "anchor-syn", + "borsh-derive-internal", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-space" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134a01c0703f6fd355a0e472c033f6f3e41fac1ef6e370b20c50f4c8d022cea7" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-lang" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6bab117055905e930f762c196e08f861f8dfe7241b92cee46677a3b15561a0a" +dependencies = [ + "anchor-attribute-access-control", + "anchor-attribute-account", + "anchor-attribute-constant", + "anchor-attribute-error", + "anchor-attribute-event", + "anchor-attribute-program", + "anchor-derive-accounts", + "anchor-derive-serde", + "anchor-derive-space", + "base64 0.21.7", + "bincode", + "borsh 0.10.4", + "bytemuck", + "solana-program", + "thiserror 1.0.69", +] + +[[package]] +name = "anchor-lang-idl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e8599d21995f68e296265aa5ab0c3cef582fd58afec014d01bd0bce18a4418" +dependencies = [ + "anchor-lang-idl-spec", + "anyhow", + "heck", + "serde", + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "anchor-lang-idl-spec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bdf143115440fe621bdac3a29a1f7472e09f6cd82b2aa569429a0c13f103838" +dependencies = [ + "anyhow", + "serde", +] + +[[package]] +name = "anchor-syn" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dc7a6d90cc643df0ed2744862cdf180587d1e5d28936538c18fc8908489ed67" +dependencies = [ + "anyhow", + "bs58", + "heck", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "syn 1.0.109", + "thiserror 1.0.69", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "arrayref" version = "0.3.9" @@ -38,6 +220,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -189,6 +377,9 @@ name = "bytemuck" version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" +dependencies = [ + "bytemuck_derive", +] [[package]] name = "bytemuck_derive" @@ -201,6 +392,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cc" version = "1.2.23" @@ -327,6 +524,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "fast-math" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2465292146cdfc2011350fe3b1c616ac83cf0faeedb33463ba1c332ed8948d66" +dependencies = [ + "ieee754", +] + [[package]] name = "feature-probe" version = "0.1.1" @@ -412,6 +618,30 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "ieee754" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9007da9cacbd3e6343da136e98b0d2df013f553d35bdec8b518f07bea768e19c" + [[package]] name = "indexmap" version = "2.9.0" @@ -422,6 +652,12 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "js-sys" version = "0.3.77" @@ -530,6 +766,20 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -540,6 +790,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.4.2" @@ -560,6 +819,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -640,6 +921,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pyth-solana-receiver-sdk" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f07e92abfc18154532ed3dabaa7dac8e693b9925bfe28b2915bc8f8c1540ca0" +dependencies = [ + "anchor-lang", + "bytemuck_derive", + "hex", + "pythnet-sdk", + "solana-program", +] + +[[package]] +name = "pythnet-sdk" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498d20fd330277697aaee92f341bdabdb4695b10e05f054157a18ad8b7746a17" +dependencies = [ + "anchor-lang", + "bincode", + "borsh 0.10.4", + "bytemuck", + "byteorder", + "fast-math", + "hex", + "rustc_version", + "serde", + "sha3", + "slow_primes", + "solana-program", + "thiserror 1.0.69", +] + [[package]] name = "quote" version = "1.0.40" @@ -744,6 +1059,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "scopeguard" version = "1.2.0" @@ -785,6 +1106,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "sha2" version = "0.9.9" @@ -830,6 +1163,8 @@ dependencies = [ name = "shine_payments" version = "0.2.0" dependencies = [ + "anchor-lang", + "pyth-solana-receiver-sdk", "solana-program", ] @@ -846,6 +1181,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "slow_primes" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58267dd2fbaa6dceecba9e3e106d2d90a2b02497c0e8b01b8759beccf5113938" +dependencies = [ + "num", +] + [[package]] name = "smallvec" version = "1.15.0" @@ -1035,7 +1379,7 @@ dependencies = [ "solana-pubkey", "solana-sdk-ids", "solana-system-interface", - "thiserror", + "thiserror 2.0.12", ] [[package]] @@ -1318,7 +1662,7 @@ dependencies = [ "solana-sysvar", "solana-sysvar-id", "solana-vote-interface", - "thiserror", + "thiserror 2.0.12", "wasm-bindgen", ] @@ -1449,7 +1793,7 @@ checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" dependencies = [ "libsecp256k1", "solana-define-syscall", - "thiserror", + "thiserror 2.0.12", ] [[package]] @@ -1674,13 +2018,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -1747,6 +2111,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "version_check" version = "0.9.5" diff --git a/shine-solana/shine/doc/programs/shine_payments.md b/shine-solana/shine/doc/programs/shine_payments.md index 912da5e..3c471a9 100644 --- a/shine-solana/shine/doc/programs/shine_payments.md +++ b/shine-solana/shine/doc/programs/shine_payments.md @@ -350,10 +350,18 @@ next_index = tickets_paid + 1 Проверки oracle: - передан именно тот oracle account, что указан в settings; -- feed id совпадает с ожидаемым; +- owner oracle-аккаунта совпадает с Pyth Solana Receiver program; +- feed id совпадает с ожидаемым `PYTH_SOL_USD_FEED_ID`; +- verification level должен быть `Full`; - цена не старше `ORACLE_MAX_AGE_SECS`; +- доверительный интервал (`conf`) не должен быть шире `ORACLE_MAX_CONFIDENCE_PPM`; - цена положительная и корректно переводима в ratio. +Реализация чтения: + +- для декодирования price update используется официальный open-source `pyth-solana-receiver-sdk`; +- ручной парсинг по фиксированным offset-ам не используется. + Внутренние преобразования: - `lamports -> usd_cents` делаются с округлением вниз; diff --git a/shine-solana/shine/programs/shine_payments/Cargo.toml b/shine-solana/shine/programs/shine_payments/Cargo.toml index 90ab3c4..6fc6a02 100644 --- a/shine-solana/shine/programs/shine_payments/Cargo.toml +++ b/shine-solana/shine/programs/shine_payments/Cargo.toml @@ -12,6 +12,8 @@ doctest = false bench = false [dependencies] +anchor-lang = "=0.31.1" +pyth-solana-receiver-sdk = "=0.6.0" solana-program = "2.1.21" [features] diff --git a/shine-solana/shine/programs/shine_payments/src/lib.rs b/shine-solana/shine/programs/shine_payments/src/lib.rs index da1532f..056e8ef 100644 --- a/shine-solana/shine/programs/shine_payments/src/lib.rs +++ b/shine-solana/shine/programs/shine_payments/src/lib.rs @@ -1,3 +1,6 @@ +use anchor_lang::AccountDeserialize; +use pyth_solana_receiver_sdk::error::GetPriceError; +use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, FeedId, PriceUpdateV2}; use solana_program::{ account_info::{next_account_info, AccountInfo}, clock::Clock, @@ -1062,21 +1065,50 @@ fn read_sol_usd_price(price_update: &AccountInfo, key: &Pubkey) -> Result 0, PaymentsError::InvalidOraclePrice); + require_oracle_confidence_ok(price.price, price.conf)?; + sol_usd_price_from_components(price.price, price.exponent) } -fn parse_pyth_price_update_v2(data: &[u8], clock: &Clock) -> Result { - require!(data.len() > 100, PaymentsError::InvalidOraclePrice); - let price = read_i64_at(data, 73)?; - let exponent = read_i32_at(data, 89)?; - let publish_time = read_i64_at(data, 93)?; +fn expected_sol_usd_feed_id() -> Result { + get_feed_id_from_hex(settings::PYTH_SOL_USD_FEED_ID) + .map_err(|_| ProgramError::from(PaymentsError::InvalidOracleFeedConfig)) +} + +fn map_pyth_price_error(err: GetPriceError) -> ProgramError { + match err { + GetPriceError::PriceTooOld => PaymentsError::OraclePriceTooOld.into(), + GetPriceError::MismatchedFeedId => PaymentsError::InvalidOracleFeed.into(), + GetPriceError::InsufficientVerificationLevel => PaymentsError::InvalidOraclePrice.into(), + GetPriceError::FeedIdMustBe32Bytes | GetPriceError::FeedIdNonHexCharacter => { + PaymentsError::InvalidOracleFeedConfig.into() + } + GetPriceError::InvalidWindowSize => PaymentsError::InvalidOraclePrice.into(), + } +} + +fn require_oracle_confidence_ok(price: i64, conf: u64) -> ProgramResult { + require!(price > 0, PaymentsError::InvalidOraclePrice); + let conf_ppm = checked_mul_u128(conf as u128, 1_000_000u128)? / (price as u128); require!( - publish_time.saturating_add(settings::ORACLE_MAX_AGE_SECS as i64) >= clock.unix_timestamp, - PaymentsError::OraclePriceTooOld + conf_ppm <= settings::ORACLE_MAX_CONFIDENCE_PPM as u128, + PaymentsError::InvalidOraclePrice ); + Ok(()) +} + +fn sol_usd_price_from_components(price: i64, exponent: i32) -> Result { require!(price > 0, PaymentsError::InvalidOraclePrice); let mut num = checked_mul_u128(price as u128, settings::USD_CENTS_SCALE as u128)?; let mut den: u128 = 1; @@ -1097,34 +1129,6 @@ fn parse_pyth_price_update_v2(data: &[u8], clock: &Clock) -> Result Result { - let end = offset - .checked_add(4) - .ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?; - let slice = data - .get(offset..end) - .ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?; - Ok(i32::from_le_bytes( - slice - .try_into() - .map_err(|_| ProgramError::from(PaymentsError::InvalidOraclePrice))?, - )) -} - -fn read_i64_at(data: &[u8], offset: usize) -> Result { - let end = offset - .checked_add(8) - .ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?; - let slice = data - .get(offset..end) - .ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?; - Ok(i64::from_le_bytes( - slice - .try_into() - .map_err(|_| ProgramError::from(PaymentsError::InvalidOraclePrice))?, - )) -} - fn lamports_to_usd_cents_floor(lamports: u64, price: &SolUsdPrice) -> Result { let numerator = checked_mul_u128(lamports as u128, price.price_num)?; let denominator = checked_mul_u128(settings::LAMPORTS_PER_SOL as u128, price.price_den)?; diff --git a/shine-solana/shine/programs/shine_payments/src/settings.rs b/shine-solana/shine/programs/shine_payments/src/settings.rs index 2e25faf..34f875a 100644 --- a/shine-solana/shine/programs/shine_payments/src/settings.rs +++ b/shine-solana/shine/programs/shine_payments/src/settings.rs @@ -45,6 +45,9 @@ pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000; /// `ORACLE_MAX_AGE_SECS` — максимальный возраст oracle-цены (в секундах), допустимый для расчетов. pub const ORACLE_MAX_AGE_SECS: u64 = 120; +/// `ORACLE_MAX_CONFIDENCE_PPM` — максимальная относительная ширина доверительного интервала oracle-цены. +/// Если `conf / price` выше этого порога, цена считается слишком неопределённой для покупки и payout. +pub const ORACLE_MAX_CONFIDENCE_PPM: u64 = 100_000; // 10% /// `PYTH_SOL_USD_FEED_ID` — feed id Pyth для пары SOL/USD. pub const PYTH_SOL_USD_FEED_ID: &str = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d";