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