gmsol_store/states/oracle/
validator.rs

1use anchor_lang::prelude::*;
2use gmsol_utils::price::Price;
3
4use crate::{
5    states::{Amount, Store, TokenConfig},
6    CoreError,
7};
8
9use super::PriceProviderKind;
10
11/// Default timestamp adjustment.
12pub const DEFAULT_TIMESTAMP_ADJUSTMENT: u64 = 1;
13
14/// Price Validator.
15pub struct PriceValidator {
16    clock: Clock,
17    max_age: Amount,
18    max_oracle_timestamp_range: Amount,
19    max_future_timestamp_excess: Amount,
20    min_oracle_ts: i64,
21    max_oracle_ts: i64,
22    min_oracle_slot: Option<u64>,
23}
24
25impl PriceValidator {
26    pub(super) fn clock(&self) -> &Clock {
27        &self.clock
28    }
29
30    pub(super) fn validate_one(
31        &mut self,
32        token_config: &TokenConfig,
33        provider: &PriceProviderKind,
34        oracle_ts: i64,
35        oracle_slot: u64,
36        _price: &Price,
37    ) -> Result<()> {
38        let timestamp_adjustment = token_config
39            .timestamp_adjustment(provider)
40            .map_err(CoreError::from)?
41            .into();
42        let ts = oracle_ts
43            .checked_sub_unsigned(timestamp_adjustment)
44            .ok_or_else(|| error!(CoreError::TokenAmountOverflow))?;
45
46        let expiration_ts = ts
47            .checked_add_unsigned(self.max_age)
48            .ok_or_else(|| error!(CoreError::TokenAmountOverflow))?;
49
50        let current_ts = self.clock.unix_timestamp;
51
52        require_gte!(expiration_ts, current_ts, CoreError::MaxPriceAgeExceeded);
53        require_gte!(
54            current_ts.saturating_add_unsigned(self.max_future_timestamp_excess),
55            oracle_ts,
56            CoreError::MaxPriceTimestampExceeded
57        );
58
59        // Note: we may add ref price validation here in the future.
60
61        self.merge_range(Some(oracle_slot), ts, ts);
62
63        Ok(())
64    }
65
66    pub(super) fn merge_range(
67        &mut self,
68        min_oracle_slot: Option<u64>,
69        min_oracle_ts: i64,
70        max_oracle_ts: i64,
71    ) {
72        self.min_oracle_slot = match (self.min_oracle_slot, min_oracle_slot) {
73            (Some(current), Some(other)) => Some(current.min(other)),
74            (None, Some(slot)) | (Some(slot), None) => Some(slot),
75            (None, None) => None,
76        };
77        self.min_oracle_ts = self.min_oracle_ts.min(min_oracle_ts);
78        self.max_oracle_ts = self.max_oracle_ts.max(max_oracle_ts);
79    }
80
81    pub(super) fn finish(self) -> Result<Option<(u64, i64, i64)>> {
82        let range: u64 = self
83            .max_oracle_ts
84            .checked_sub(self.min_oracle_ts)
85            .ok_or_else(|| error!(CoreError::TokenAmountOverflow))?
86            .try_into()
87            .map_err(|_| error!(CoreError::InvalidOracleTimestampsRange))?;
88        require_gte!(
89            self.max_oracle_timestamp_range,
90            range,
91            CoreError::MaxOracleTimestampsRangeExceeded
92        );
93        Ok(self
94            .min_oracle_slot
95            .map(|slot| (slot, self.min_oracle_ts, self.max_oracle_ts)))
96    }
97}
98
99impl<'a> TryFrom<&'a Store> for PriceValidator {
100    type Error = anchor_lang::error::Error;
101
102    fn try_from(config: &'a Store) -> Result<Self> {
103        let max_age = config.amount.oracle_max_age;
104        // Note: Ref price validation is not implemented currently.
105        let _max_ref_price_deviation_factor = config.factor.oracle_ref_price_deviation;
106        let max_oracle_timestamp_range = config.amount.oracle_max_timestamp_range;
107        let max_future_timestamp_excess = config.amount.oracle_max_future_timestamp_excess;
108        Ok(Self {
109            clock: Clock::get()?,
110            max_age,
111            // max_ref_price_deviation_factor,
112            max_oracle_timestamp_range,
113            max_future_timestamp_excess,
114            min_oracle_ts: i64::MAX,
115            max_oracle_ts: i64::MIN,
116            min_oracle_slot: None,
117        })
118    }
119}