gmsol_store/states/oracle/
validator.rs1use anchor_lang::prelude::*;
2use gmsol_utils::price::Price;
3
4use crate::{
5 states::{Amount, Store, TokenConfig},
6 CoreError,
7};
8
9use super::PriceProviderKind;
10
11pub const DEFAULT_TIMESTAMP_ADJUSTMENT: u64 = 1;
13
14pub 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 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 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_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}