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
@@ -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]
@@ -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)?;
@@ -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";