solana: усилить проверку Pyth oracle в shine_payments

This commit is contained in:
AidarKC 2026-06-10 02:25:45 +04:00
parent 5981d3f871
commit 9ca469a075
6 changed files with 430 additions and 43 deletions

View File

@ -1,2 +1,2 @@
client.version=1.2.149
server.version=1.2.141
client.version=1.2.150
server.version=1.2.142

View File

@ -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"

View File

@ -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` делаются с округлением вниз;

View File

@ -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]

View File

@ -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<SolUsd
let expected_oracle = Pubkey::from_str(settings::PYTH_SOL_USD_ACCOUNT)
.map_err(|_| ProgramError::from(PaymentsError::InvalidOracleFeedConfig))?;
require_keys_eq!(expected_oracle, *key, PaymentsError::InvalidOracleAccount);
require_keys_eq!(*price_update.owner, pyth_solana_receiver_sdk::ID, PaymentsError::InvalidOracleAccount);
let data = price_update.try_borrow_data()?;
let clock = Clock::get()?;
parse_pyth_price_update_v2(&data, &clock)
let mut data_slice: &[u8] = &data;
let price_update_state = PriceUpdateV2::try_deserialize(&mut data_slice)
.map_err(|_| ProgramError::from(PaymentsError::InvalidOraclePrice))?;
let feed_id = expected_sol_usd_feed_id()?;
let price = price_update_state
.get_price_no_older_than(&clock, settings::ORACLE_MAX_AGE_SECS, &feed_id)
.map_err(map_pyth_price_error)?;
require!(price.price > 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<SolUsdPrice, ProgramError> {
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<FeedId, ProgramError> {
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<SolUsdPrice, ProgramError> {
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<SolUsdPrice,
Ok(SolUsdPrice { price_num: num, price_den: den })
}
fn read_i32_at(data: &[u8], offset: usize) -> Result<i32, ProgramError> {
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<i64, ProgramError> {
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<u64, ProgramError> {
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)?;

View File

@ -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";