gmsol_store/states/oracle/
mod.rs

1/// Price Map.
2pub mod price_map;
3
4/// Custom Price Feed.
5mod feed;
6
7/// Switchboard.
8pub mod switchboard;
9
10/// Chainlink.
11pub mod chainlink;
12
13/// Pyth.
14pub mod pyth;
15
16/// Price Validator.
17pub mod validator;
18
19/// Oracle time validation.
20pub mod time;
21
22use std::ops::Deref;
23
24use crate::{
25    states::{TokenMapAccess, TokenMapLoader},
26    CoreError, CoreResult,
27};
28use anchor_lang::prelude::*;
29use num_enum::{IntoPrimitive, TryFromPrimitive};
30
31use self::price_map::PriceMap;
32use super::{HasMarketMeta, Seed, Store, TokenConfig, TokenMapHeader, TokenMapRef};
33
34pub use self::{
35    chainlink::Chainlink,
36    feed::{PriceFeed, PriceFeedPrice},
37    pyth::Pyth,
38    switchboard::Switchboard,
39    time::{ValidateOracleTime, ValidateOracleTimeExt},
40    validator::PriceValidator,
41};
42
43const MAX_FLAGS: usize = 8;
44
45#[repr(u8)]
46#[non_exhaustive]
47#[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive)]
48enum OracleFlag {
49    /// Cleared.
50    Cleared,
51    // CHECK: should have no more than `MAX_FLAGS` of flags.
52}
53
54gmsol_utils::flags!(OracleFlag, MAX_FLAGS, u8);
55
56/// Oracle Account.
57#[account(zero_copy)]
58#[cfg_attr(feature = "debug", derive(derive_more::Debug))]
59pub struct Oracle {
60    version: u8,
61    #[cfg_attr(feature = "debug", debug(skip))]
62    padding_0: [u8; 7],
63    /// Store.
64    pub store: Pubkey,
65    /// This address is authorized to **directly** modify
66    /// the oracle through instrcutions.
67    pub(crate) authority: Pubkey,
68    min_oracle_ts: i64,
69    max_oracle_ts: i64,
70    min_oracle_slot: u64,
71    primary: PriceMap,
72    flags: OracleFlagContainer,
73    #[cfg_attr(feature = "debug", debug(skip))]
74    padding_1: [u8; 3],
75    #[cfg_attr(feature = "debug", debug(skip))]
76    reserved: [u8; 256],
77}
78
79impl gmsol_utils::InitSpace for Oracle {
80    const INIT_SPACE: usize = std::mem::size_of::<Self>();
81}
82
83impl Seed for Oracle {
84    const SEED: &'static [u8] = b"oracle";
85}
86
87impl Oracle {
88    /// Initialize the [`Oracle`].
89    pub(crate) fn init(&mut self, store: Pubkey, authority: Pubkey) {
90        self.clear_all_prices();
91        self.store = store;
92        self.authority = authority;
93    }
94
95    /// Return whether the oracle is cleared.
96    pub fn is_cleared(&self) -> bool {
97        self.flags.get_flag(OracleFlag::Cleared)
98    }
99
100    /// Set prices from remaining accounts.
101    pub(crate) fn set_prices_from_remaining_accounts<'info>(
102        &mut self,
103        mut validator: PriceValidator,
104        map: &TokenMapRef,
105        tokens: &[Pubkey],
106        remaining_accounts: &'info [AccountInfo<'info>],
107        chainlink: Option<&Program<'info, Chainlink>>,
108    ) -> Result<()> {
109        require!(self.is_cleared(), CoreError::PricesAreAlreadySet);
110        require!(self.primary.is_empty(), CoreError::PricesAreAlreadySet);
111        require!(
112            tokens.len() <= PriceMap::MAX_TOKENS,
113            CoreError::ExceedMaxLengthLimit
114        );
115        require!(
116            tokens.len() <= remaining_accounts.len(),
117            ErrorCode::AccountNotEnoughKeys
118        );
119        // Assume the remaining accounts are arranged in the following way:
120        // [token_config, feed; tokens.len()] [..remaining]
121        for (idx, token) in tokens.iter().enumerate() {
122            let feed = &remaining_accounts[idx];
123            let token_config = map.get(token).ok_or_else(|| error!(CoreError::NotFound))?;
124
125            require!(token_config.is_enabled(), CoreError::TokenConfigDisabled);
126
127            let oracle_price = OraclePrice::parse_from_feed_account(
128                validator.clock(),
129                token_config,
130                chainlink,
131                feed,
132            )?;
133
134            validator.validate_one(
135                token_config,
136                &oracle_price.provider,
137                oracle_price.oracle_ts,
138                oracle_price.oracle_slot,
139                &oracle_price.price,
140            )?;
141            self.primary
142                .set(token, oracle_price.price, token_config.is_synthetic())?;
143        }
144        self.update_oracle_ts_and_slot(validator)?;
145        Ok(())
146    }
147
148    /// Get min oracle slot.
149    pub fn min_oracle_slot(&self) -> Option<u64> {
150        if self.is_cleared() {
151            None
152        } else {
153            Some(self.min_oracle_slot)
154        }
155    }
156
157    /// Get min oracle ts.
158    pub fn min_oracle_ts(&self) -> i64 {
159        self.min_oracle_ts
160    }
161
162    /// Get max oracle ts.
163    pub fn max_oracle_ts(&self) -> i64 {
164        self.max_oracle_ts
165    }
166
167    fn update_oracle_ts_and_slot(&mut self, mut validator: PriceValidator) -> Result<()> {
168        validator.merge_range(
169            self.min_oracle_slot(),
170            self.min_oracle_ts,
171            self.max_oracle_ts,
172        );
173        if let Some((min_slot, min_ts, max_ts)) = validator.finish()? {
174            self.min_oracle_slot = min_slot;
175            self.min_oracle_ts = min_ts;
176            self.max_oracle_ts = max_ts;
177            self.flags.set_flag(OracleFlag::Cleared, false);
178        }
179        Ok(())
180    }
181
182    /// Clear all prices.
183    pub(crate) fn clear_all_prices(&mut self) {
184        self.primary.clear();
185        self.min_oracle_ts = i64::MAX;
186        self.max_oracle_ts = i64::MIN;
187        self.min_oracle_slot = u64::MAX;
188        self.flags.set_flag(OracleFlag::Cleared, true);
189    }
190
191    #[inline(never)]
192    pub(crate) fn with_prices<'info, T>(
193        &mut self,
194        store: &AccountLoader<'info, Store>,
195        token_map: &AccountLoader<'info, TokenMapHeader>,
196        tokens: &[Pubkey],
197        remaining_accounts: &'info [AccountInfo<'info>],
198        chainlink: Option<&Program<'info, Chainlink>>,
199        f: impl FnOnce(&mut Self, &'info [AccountInfo<'info>]) -> Result<T>,
200    ) -> Result<T> {
201        let validator = PriceValidator::try_from(store.load()?.deref())?;
202        require_gte!(
203            remaining_accounts.len(),
204            tokens.len(),
205            CoreError::NotEnoughTokenFeeds,
206        );
207        let feeds = &remaining_accounts[..tokens.len()];
208        let remaining_accounts = &remaining_accounts[tokens.len()..];
209        let res = {
210            let token_map = token_map.load_token_map()?;
211            self.set_prices_from_remaining_accounts(validator, &token_map, tokens, feeds, chainlink)
212        };
213        match res {
214            Ok(()) => {
215                let output = f(self, remaining_accounts);
216                self.clear_all_prices();
217                output
218            }
219            Err(err) => {
220                self.clear_all_prices();
221                Err(err)
222            }
223        }
224    }
225
226    /// Validate oracle time.
227    pub(crate) fn validate_time(&self, target: &impl ValidateOracleTime) -> CoreResult<()> {
228        if self.max_oracle_ts < self.min_oracle_ts {
229            msg!("min = {}, max = {}", self.min_oracle_ts, self.max_oracle_ts);
230            return Err(CoreError::InvalidOracleTimestampsRange);
231        }
232        target.validate_min_oracle_slot(self)?;
233        target.validate_min_oracle_ts(self)?;
234        target.validate_max_oracle_ts(self)?;
235        Ok(())
236    }
237
238    /// Get primary price for the given token.
239    pub fn get_primary_price(
240        &self,
241        token: &Pubkey,
242        allow_synthetic: bool,
243    ) -> Result<gmsol_model::price::Price<u128>> {
244        let price = self
245            .primary
246            .get(token)
247            .ok_or_else(|| error!(CoreError::MissingOraclePrice))?;
248
249        // The `is_synthetic` flag needs to be checked because, for pool tokens,
250        // we do not want their token decimals to be manually set (only synthetic
251        // tokens are allowed to have their decimals manually configured).
252        // This helps prevent the possibility of unit price using incorrect token
253        // decimals assumptions.
254        //
255        // Note: the SPL token mint cannot be closed, and the SPL token 2022 token mint
256        // also cannot be closed when there is a supply. Therefore, there is generally
257        // no need to worry about the decimals of non-synthetic tokens being changed.
258        if !allow_synthetic {
259            require!(
260                !price.is_synthetic(),
261                CoreError::SyntheticTokenPriceIsNotAllowed
262            );
263        }
264        Ok(gmsol_model::price::Price {
265            min: price.min().to_unit_price(),
266            max: price.max().to_unit_price(),
267        })
268    }
269
270    /// Get prices for the market
271    pub(crate) fn market_prices(
272        &self,
273        market: &impl HasMarketMeta,
274    ) -> Result<gmsol_model::price::Prices<u128>> {
275        let meta = market.market_meta();
276        let prices = gmsol_model::price::Prices {
277            index_token_price: self.get_primary_price(&meta.index_token_mint, true)?,
278            long_token_price: self.get_primary_price(&meta.long_token_mint, false)?,
279            short_token_price: self.get_primary_price(&meta.short_token_mint, false)?,
280        };
281        Ok(prices)
282    }
283}
284
285/// Supported Price Provider Kind.
286#[repr(u8)]
287#[derive(
288    Clone,
289    Copy,
290    Default,
291    TryFromPrimitive,
292    IntoPrimitive,
293    PartialEq,
294    Eq,
295    Hash,
296    strum::EnumString,
297    strum::Display,
298)]
299#[strum(serialize_all = "snake_case")]
300#[cfg_attr(feature = "enum-iter", derive(strum::EnumIter))]
301#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
302#[cfg_attr(feature = "clap", clap(rename_all = "snake_case"))]
303#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
304#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
305#[cfg_attr(feature = "debug", derive(Debug))]
306#[non_exhaustive]
307pub enum PriceProviderKind {
308    /// Chainlink Data Streams.
309    #[default]
310    ChainlinkDataStreams = 0,
311    /// Pyth Oracle V2.
312    Pyth = 1,
313    /// Chainlink Data Feed.
314    Chainlink = 2,
315    /// Switchboard On-Demand (V3) Data Feed.
316    Switchboard = 3,
317}
318
319impl PriceProviderKind {
320    /// Create from program id.
321    pub fn from_program_id(program_id: &Pubkey) -> Option<Self> {
322        if *program_id == Chainlink::id() {
323            Some(Self::Chainlink)
324        } else if *program_id == Pyth::id() {
325            Some(Self::Pyth)
326        } else if *program_id == Switchboard::id() {
327            Some(Self::Switchboard)
328        } else {
329            None
330        }
331    }
332}
333
334struct OraclePrice {
335    provider: PriceProviderKind,
336    oracle_slot: u64,
337    oracle_ts: i64,
338    price: gmsol_utils::Price,
339}
340
341impl OraclePrice {
342    fn parse_from_feed_account<'info>(
343        clock: &Clock,
344        token_config: &TokenConfig,
345        chainlink: Option<&Program<'info, Chainlink>>,
346        account: &'info AccountInfo<'info>,
347    ) -> Result<Self> {
348        let (provider, parsed) = match PriceProviderKind::from_program_id(account.owner) {
349            Some(provider) => (provider, None),
350            None if *account.owner == crate::ID => {
351                let loader = AccountLoader::<'info, PriceFeed>::try_from(account)?;
352                let feed = loader.load()?;
353                let kind = feed.provider()?;
354                (kind, Some(feed.check_and_get_price(clock, token_config)?))
355            }
356            None => return Err(error!(CoreError::InvalidPriceFeedAccount)),
357        };
358
359        require_eq!(token_config.expected_provider()?, provider);
360
361        let feed_id = token_config.get_feed(&provider)?;
362
363        let (oracle_slot, oracle_ts, price) = match provider {
364            PriceProviderKind::ChainlinkDataStreams => {
365                parsed.ok_or_else(|| error!(CoreError::Internal))?
366            }
367            PriceProviderKind::Pyth => {
368                let (oracle_slot, oracle_ts, price) =
369                    Pyth::check_and_get_price(clock, token_config, account, &feed_id)?;
370                (oracle_slot, oracle_ts, price)
371            }
372            PriceProviderKind::Chainlink => {
373                require_keys_eq!(feed_id, account.key(), CoreError::InvalidPriceFeedAccount);
374                let program =
375                    chainlink.ok_or_else(|| error!(CoreError::ChainlinkProgramIsRequired))?;
376                let (oracle_slot, oracle_ts, price) = Chainlink::check_and_get_chainlink_price(
377                    clock,
378                    program,
379                    token_config,
380                    account,
381                )?;
382                (oracle_slot, oracle_ts, price)
383            }
384            PriceProviderKind::Switchboard => {
385                require_keys_eq!(feed_id, account.key(), CoreError::InvalidPriceFeedAccount);
386                Switchboard::check_and_get_price(clock, token_config, account)?
387            }
388        };
389
390        Ok(Self {
391            provider,
392            oracle_slot,
393            oracle_ts,
394            price,
395        })
396    }
397}