Skip to main content

devol_accounts_kit/accounts/oracles/
oracles_data.rs

1use pyth_sdk_solana::state::SolanaPriceAccount;
2use serde::{Deserialize, Serialize};
3use solana_program::account_info::AccountInfo;
4use solana_program::clock::Clock;
5use solana_program::sysvar::Sysvar;
6use crate::accounts::oracles::oracle_params::{DataLen, Endian, OracleParams};
7use crate::accounts::oracles::oracle_provider::OracleProvider;
8use crate::dvl_error::DvlError;
9use crate::errors::*;
10
11pub const ORACLE_PARAMS_QUANTITY: usize = 3;
12pub const ORACLES_DATA_SIZE: usize = 216;
13pub const ORACLES_DATA_COUNT: usize = 8;
14
15#[repr(C)]
16#[derive(Copy, Clone, Serialize, Deserialize)]
17pub struct OracleData {
18    pub base_ticker: [u8; 8],              // The base currency ticker, e.g., BTC in BTC/USD
19    pub relative_ticker: [u8; 8],          // The quote currency ticker, e.g., USD in BTC/USD
20    pub configured: bool,                  // Indicates if the oracle is configured and operational
21    pub use_relative_oracle: bool,         // Flag to indicate whether to use the relative_oracle_num
22    pub relative_oracle_num: u8,           // Index of the reference oracle for price recalculations
23    pub reserved: u8,                      // Reserved space for future use
24    pub max_price_deviation: u32,          // Maximum absolute deviation for the price
25    pub params: [OracleParams; ORACLE_PARAMS_QUANTITY], // Oracle parameters, 192 bytes, offset=24
26}
27
28impl OracleData {
29    /// Calculates the asset price based on oracle data
30    /// Returns Result<(f64, DvlError), ProgramError> where f64 is the average price
31    pub fn get_asset_price(&self, ext_oracles_accounts: &[AccountInfo]) -> Result<f64, DvlError> {
32        let current_time = Clock::get().
33            map_err(|_| DvlError::new(ContractError::TimeReadError))?
34            .unix_timestamp;
35        let mut prices = Vec::<f64>::new();
36
37        for account in ext_oracles_accounts {
38            for oracle_param in self.params.iter() {
39                if account.key == &oracle_param.account {
40                    if !oracle_param.enabled {
41                        continue;
42                    }
43                    match oracle_param.provider {
44                        OracleProvider::Custom => {
45                            let timestamp_offset = oracle_param.timestamp.offset as usize;
46                            let ext_oracle_data = account.try_borrow_data().
47                                map_err(|_| DvlError::new_with_account(AccountTag::ExternalOracle, ContractError::AccountSize))?;
48                            let timestamp = Self::read_value(&ext_oracle_data[timestamp_offset..], oracle_param.timestamp.data_len, oracle_param.timestamp.endian)
49                                .map_err(|_| DvlError::new_with_account(AccountTag::Oracle, ContractError::TimeReadError))? as i64;
50
51                            if (current_time - timestamp).abs() > oracle_param.max_timestamp_diff_sec as i64 {
52                                continue; // Skip this oracle due to time difference
53                            }
54
55                            let mantissa_offset = oracle_param.mantissa.offset as usize;
56                            let mantissa = Self::read_value(&ext_oracle_data[mantissa_offset..], oracle_param.mantissa.data_len, oracle_param.mantissa.endian)?;
57
58                            let exponent_offset = oracle_param.exponent.offset as usize;
59                            let exponent = Self::read_value(&ext_oracle_data[exponent_offset..], oracle_param.exponent.data_len, oracle_param.exponent.endian)? as i32;
60
61                            let price = mantissa as f64 * 10f64.powi(-exponent);
62                            prices.push(price);
63                        }
64                        OracleProvider::Switchboard => {
65                            continue;}
66                        OracleProvider::Pyth => {
67                            let price_feed = SolanaPriceAccount::account_info_to_feed( &account ).
68                                map_err(|_| {
69                                    DvlError::new_with_account(AccountTag::Oracle, ContractError::AssetPriceUnavailable)
70                                })?;
71                            let current_price = price_feed.get_price_no_older_than(current_time, oracle_param.max_timestamp_diff_sec as u64).unwrap();
72                            prices.push(current_price.price as f64 * 10f64.powi(current_price.expo));
73                        }
74                    }
75                }
76            }
77        }
78
79        return match prices.len() {
80            0 => Err(DvlError::new(ContractError::AssetPriceUnavailable)),
81            2 => {
82                if (prices[1] - prices[0]).abs() > self.max_price_deviation as f64 {
83                    return Err(DvlError::new(ContractError::PriceDiscrepancyError))
84                }
85                Ok(prices[0])
86            }
87            3 => {
88                let max_deviation = self.max_price_deviation as f64;
89                if (prices[1] - prices[0]).abs() > max_deviation || (prices[2] - prices[1]).abs() > max_deviation {
90                    return Err(DvlError::new(ContractError::PriceDiscrepancyError))
91                }
92                Ok(prices[0])
93            }
94            _ => Ok(prices[0])
95        };
96    }
97
98    /// Reads value from data slice according to DataLen and Endian
99    fn read_value(data: &[u8], data_len: DataLen, endian: Endian) -> Result<u128, DvlError> {
100        let val = match data_len {
101            DataLen::U8 => data[0] as u128,
102            DataLen::U32 => {
103                if endian == Endian::LE {
104                    u32::from_le_bytes(data[..4].try_into().unwrap()) as u128
105                } else {
106                    u32::from_be_bytes(data[..4].try_into().unwrap()) as u128
107                }
108            },
109            DataLen::U64 => {
110                if endian == Endian::LE {
111                    u64::from_le_bytes(data[..8].try_into().unwrap()) as u128
112                } else {
113                    u64::from_be_bytes(data[..8].try_into().unwrap()) as u128
114                }
115            },
116            DataLen::U128 => {
117                if endian == Endian::LE {
118                    u128::from_le_bytes(data[..16].try_into().unwrap())
119                } else {
120                    u128::from_be_bytes(data[..16].try_into().unwrap())
121                }
122            },
123            _ => return Err(DvlError::new(ContractError::ComputationError)), // Unsupported data length
124        };
125        Ok(val)
126    }
127
128}
129
130#[cfg(test)]
131impl Default for OracleData {
132    fn default() -> Self {
133        Self {
134            base_ticker: [0; 8],
135            relative_ticker: [0; 8],
136            configured: false,
137            use_relative_oracle: false,
138            relative_oracle_num: 0,
139            max_price_deviation: 0,
140            reserved: 0,
141            params: [OracleParams::default(); ORACLE_PARAMS_QUANTITY],
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use std::mem;
150
151    #[test]
152    fn test_instruments_account_offsets() {
153        // checking total size
154        assert_eq!(mem::size_of::<OracleData>(), ORACLES_DATA_SIZE);
155    }
156}