SHA256
solana: усилить проверку Pyth oracle в shine_payments
This commit is contained in:
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user