switchboard_solana/oracle_program/accounts/
aggregator.rs

1use crate::prelude::*;
2use rust_decimal::Decimal;
3use std::cell::Ref;
4
5#[zero_copy(unsafe)]
6#[repr(packed)]
7#[derive(Default, Debug, PartialEq, Eq)]
8pub struct Hash {
9    /// The bytes used to derive the hash.
10    pub data: [u8; 32],
11}
12
13#[zero_copy(unsafe)]
14#[repr(packed)]
15#[derive(Default, PartialEq, Eq)]
16pub struct AggregatorRound {
17    /// Maintains the number of successful responses received from nodes.
18    /// Nodes can submit one successful response per round.
19    pub num_success: u32,
20    /// Number of error responses.
21    pub num_error: u32,
22    /// Whether an update request round has ended.
23    pub is_closed: bool,
24    /// Maintains the `solana_program::clock::Slot` that the round was opened at.
25    pub round_open_slot: u64,
26    /// Maintains the `solana_program::clock::UnixTimestamp;` the round was opened at.
27    pub round_open_timestamp: i64,
28    /// Maintains the current median of all successful round responses.
29    pub result: SwitchboardDecimal,
30    /// Standard deviation of the accepted results in the round.
31    pub std_deviation: SwitchboardDecimal,
32    /// Maintains the minimum node response this round.
33    pub min_response: SwitchboardDecimal,
34    /// Maintains the maximum node response this round.
35    pub max_response: SwitchboardDecimal,
36    /// Pubkeys of the oracles fulfilling this round.
37    pub oracle_pubkeys_data: [Pubkey; 16],
38    /// Represents all successful node responses this round. `NaN` if empty.
39    pub medians_data: [SwitchboardDecimal; 16],
40    /// Current rewards/slashes oracles have received this round.
41    pub current_payout: [i64; 16],
42    /// Keep track of which responses are fulfilled here.
43    pub medians_fulfilled: [bool; 16],
44    /// Keeps track of which errors are fulfilled here.
45    pub errors_fulfilled: [bool; 16],
46}
47
48#[derive(Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize, Eq, PartialEq)]
49#[repr(u8)]
50pub enum AggregatorResolutionMode {
51    ModeRoundResolution = 0,
52    ModeSlidingResolution = 1,
53}
54
55// #[zero_copy(unsafe)]
56#[account(zero_copy(unsafe))]
57#[repr(packed)]
58#[derive(PartialEq)]
59pub struct AggregatorAccountData {
60    /// Name of the aggregator to store on-chain.
61    pub name: [u8; 32],
62    /// Metadata of the aggregator to store on-chain.
63    pub metadata: [u8; 128],
64    /// Reserved.
65    pub _reserved1: [u8; 32],
66    /// Pubkey of the queue the aggregator belongs to.
67    pub queue_pubkey: Pubkey,
68    /// CONFIGS
69    /// Number of oracles assigned to an update request.
70    pub oracle_request_batch_size: u32,
71    /// Minimum number of oracle responses required before a round is validated.
72    pub min_oracle_results: u32,
73    /// Minimum number of job results before an oracle accepts a result.
74    pub min_job_results: u32,
75    /// Minimum number of seconds required between aggregator rounds.
76    pub min_update_delay_seconds: u32,
77    /// Unix timestamp for which no feed update will occur before.
78    pub start_after: i64,
79    /// Change percentage required between a previous round and the current round. If variance percentage is not met, reject new oracle responses.
80    pub variance_threshold: SwitchboardDecimal,
81    /// Number of seconds for which, even if the variance threshold is not passed, accept new responses from oracles.
82    pub force_report_period: i64,
83    /// Timestamp when the feed is no longer needed.
84    pub expiration: i64,
85    //
86    /// Counter for the number of consecutive failures before a feed is removed from a queue. If set to 0, failed feeds will remain on the queue.
87    pub consecutive_failure_count: u64,
88    /// Timestamp when the next update request will be available.
89    pub next_allowed_update_time: i64,
90    /// Flag for whether an aggregators configuration is locked for editing.
91    pub is_locked: bool,
92    /// Optional, public key of the crank the aggregator is currently using. Event based feeds do not need a crank.
93    pub crank_pubkey: Pubkey,
94    /// Latest confirmed update request result that has been accepted as valid.
95    pub latest_confirmed_round: AggregatorRound,
96    /// Oracle results from the current round of update request that has not been accepted as valid yet.
97    pub current_round: AggregatorRound,
98    /// List of public keys containing the job definitions for how data is sourced off-chain by oracles.
99    pub job_pubkeys_data: [Pubkey; 16],
100    /// Used to protect against malicious RPC nodes providing incorrect task definitions to oracles before fulfillment.
101    pub job_hashes: [Hash; 16],
102    /// Number of jobs assigned to an oracle.
103    pub job_pubkeys_size: u32,
104    /// Used to protect against malicious RPC nodes providing incorrect task definitions to oracles before fulfillment.
105    pub jobs_checksum: [u8; 32],
106    //
107    /// The account delegated as the authority for making account changes.
108    pub authority: Pubkey,
109    /// Optional, public key of a history buffer account storing the last N accepted results and their timestamps.
110    pub history_buffer: Pubkey,
111    /// The previous confirmed round result.
112    pub previous_confirmed_round_result: SwitchboardDecimal,
113    /// The slot when the previous confirmed round was opened.
114    pub previous_confirmed_round_slot: u64,
115    /// 	Whether an aggregator is permitted to join a crank.
116    pub disable_crank: bool,
117    /// Job weights used for the weighted median of the aggregator's assigned job accounts.
118    pub job_weights: [u8; 16],
119    /// Unix timestamp when the feed was created.
120    pub creation_timestamp: i64,
121    /// Use sliding window or round based resolution
122    /// NOTE: This changes result propogation in latest_round_result
123    pub resolution_mode: AggregatorResolutionMode,
124    pub base_priority_fee: u32,
125    pub priority_fee_bump: u32,
126    pub priority_fee_bump_period: u32,
127    pub max_priority_fee_multiplier: u32,
128    pub parent_function: Pubkey,
129    /// Reserved for future info.
130    pub _ebuf: [u8; 90],
131}
132impl Default for AggregatorAccountData {
133    fn default() -> Self {
134        unsafe { std::mem::zeroed() }
135    }
136}
137
138impl TryInto<AggregatorAccountData> for Option<Vec<u8>> {
139    type Error = SwitchboardError;
140
141    fn try_into(self) -> std::result::Result<AggregatorAccountData, Self::Error> {
142        if let Some(data) = self {
143            bytemuck::try_from_bytes(&data)
144                .map(|&x| x)
145                .map_err(|_| SwitchboardError::AccountDeserializationError)
146        } else {
147            Err(SwitchboardError::AccountDeserializationError)
148        }
149    }
150}
151
152impl AggregatorAccountData {
153    pub fn size() -> usize {
154        8 + std::mem::size_of::<AggregatorAccountData>()
155    }
156
157    /// Returns the deserialized Switchboard Aggregator account
158    ///
159    /// # Arguments
160    ///
161    /// * `switchboard_feed` - A Solana AccountInfo referencing an existing Switchboard Aggregator
162    ///
163    /// # Examples
164    ///
165    /// ```ignore
166    /// use switchboard_solana::AggregatorAccountData;
167    ///
168    /// let data_feed = AggregatorAccountData::new(feed_account_info)?;
169    /// ```
170    pub fn new<'info>(
171        switchboard_feed: &'info AccountInfo<'info>,
172    ) -> anchor_lang::Result<Ref<'info, AggregatorAccountData>> {
173        let data = switchboard_feed.try_borrow_data()?;
174        if data.len() < AggregatorAccountData::discriminator().len() {
175            return Err(ErrorCode::AccountDiscriminatorNotFound.into());
176        }
177
178        let mut disc_bytes = [0u8; 8];
179        disc_bytes.copy_from_slice(&data[..8]);
180        if disc_bytes != AggregatorAccountData::discriminator() {
181            return Err(ErrorCode::AccountDiscriminatorMismatch.into());
182        }
183
184        Ok(Ref::map(data, |data| {
185            bytemuck::from_bytes(&data[8..std::mem::size_of::<AggregatorAccountData>() + 8])
186        }))
187    }
188
189    /// Returns the deserialized Switchboard Aggregator account
190    ///
191    /// # Arguments
192    ///
193    /// * `data` - A Solana AccountInfo's data buffer
194    ///
195    /// # Examples
196    ///
197    /// ```ignore
198    /// use switchboard_solana::AggregatorAccountData;
199    ///
200    /// let data_feed = AggregatorAccountData::new_from_bytes(feed_account_info.try_borrow_data()?)?;
201    /// ```
202    pub fn new_from_bytes(data: &[u8]) -> anchor_lang::Result<&AggregatorAccountData> {
203        if data.len() < AggregatorAccountData::discriminator().len() {
204            return Err(ErrorCode::AccountDiscriminatorNotFound.into());
205        }
206
207        let mut disc_bytes = [0u8; 8];
208        disc_bytes.copy_from_slice(&data[..8]);
209        if disc_bytes != AggregatorAccountData::discriminator() {
210            return Err(ErrorCode::AccountDiscriminatorMismatch.into());
211        }
212
213        Ok(bytemuck::from_bytes(
214            &data[8..std::mem::size_of::<AggregatorAccountData>() + 8],
215        ))
216    }
217
218    /// If sufficient oracle responses, returns the latest on-chain result in SwitchboardDecimal format
219    ///
220    /// # Examples
221    ///
222    /// ```ignore
223    /// use switchboard_solana::AggregatorAccountData;
224    /// use std::convert::TryInto;
225    ///
226    /// let feed_result = AggregatorAccountData::new(feed_account_info)?.get_result()?;
227    /// let decimal: f64 = feed_result.try_into()?;
228    /// ```
229    pub fn get_result(&self) -> anchor_lang::Result<SwitchboardDecimal> {
230        if self.resolution_mode == AggregatorResolutionMode::ModeSlidingResolution {
231            return Ok(self.latest_confirmed_round.result);
232        }
233        let min_oracle_results = self.min_oracle_results;
234        let latest_confirmed_round_num_success = self.latest_confirmed_round.num_success;
235        if min_oracle_results > latest_confirmed_round_num_success {
236            return Err(SwitchboardError::InvalidAggregatorRound.into());
237        }
238        Ok(self.latest_confirmed_round.result)
239    }
240
241    /// Check whether the confidence interval exceeds a given threshold
242    ///
243    /// # Examples
244    ///
245    /// ```ignore
246    /// use switchboard_solana::{AggregatorAccountData, SwitchboardDecimal};
247    ///
248    /// let feed = AggregatorAccountData::new(feed_account_info)?;
249    /// feed.check_confidence_interval(SwitchboardDecimal::from_f64(0.80))?;
250    /// ```
251    pub fn check_confidence_interval(
252        &self,
253        max_confidence_interval: SwitchboardDecimal,
254    ) -> anchor_lang::Result<()> {
255        if self.latest_confirmed_round.std_deviation > max_confidence_interval {
256            return Err(SwitchboardError::ConfidenceIntervalExceeded.into());
257        }
258        Ok(())
259    }
260
261    /// Check the variance (as a percentage difference from the max delivered
262    /// oracle value) from all oracles.
263    pub fn check_variance(&self, max_variance: Decimal) -> anchor_lang::Result<()> {
264        if max_variance > Decimal::ONE {
265            return Err(SwitchboardError::InvalidFunctionInput.into());
266        }
267        let min: Decimal = self.latest_confirmed_round.min_response.try_into().unwrap();
268        let max: Decimal = self.latest_confirmed_round.max_response.try_into().unwrap();
269
270        if min < Decimal::ZERO || max < Decimal::ZERO || min > max {
271            return Err(SwitchboardError::AllowedVarianceExceeded.into());
272        }
273        if min / max > max_variance {
274            return Err(SwitchboardError::AllowedVarianceExceeded.into());
275        }
276        Ok(())
277    }
278
279    /// Check whether the feed has been updated in the last max_staleness seconds
280    ///
281    /// # Examples
282    ///
283    /// ```ignore
284    /// use switchboard_solana::AggregatorAccountData;
285    ///
286    /// let feed = AggregatorAccountData::new(feed_account_info)?;
287    /// feed.check_staleness(clock::Clock::get().unwrap().unix_timestamp, 300)?;
288    /// ```
289    pub fn check_staleness(
290        &self,
291        unix_timestamp: i64,
292        max_staleness: i64,
293    ) -> anchor_lang::Result<()> {
294        let staleness = unix_timestamp - self.latest_confirmed_round.round_open_timestamp;
295        if staleness > max_staleness {
296            msg!("Feed has not been updated in {} seconds!", staleness);
297            return Err(SwitchboardError::StaleFeed.into());
298        }
299        Ok(())
300    }
301
302    pub fn is_expired(&self) -> anchor_lang::Result<bool> {
303        if self.expiration == 0 {
304            return Ok(false);
305        }
306        Ok(Clock::get()?.unix_timestamp < self.expiration)
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    fn create_aggregator(lastest_round: AggregatorRound) -> AggregatorAccountData {
315        AggregatorAccountData {
316            min_update_delay_seconds: 10,
317            latest_confirmed_round: lastest_round,
318            min_job_results: 10,
319            min_oracle_results: 10,
320            ..Default::default()
321        }
322    }
323
324    fn create_round(value: f64, num_success: u32, num_error: u32) -> AggregatorRound {
325        AggregatorRound {
326            num_success,
327            num_error,
328            result: SwitchboardDecimal::from_f64(value),
329            ..Default::default()
330        }
331    }
332
333    #[test]
334    fn test_accept_current_on_sucess_count() {
335        let lastest_round = create_round(100.0, 30, 0); // num success 30 > 10 min oracle result
336
337        let aggregator = create_aggregator(lastest_round);
338        assert_eq!(
339            aggregator.get_result().unwrap(),
340            lastest_round.result.clone()
341        );
342    }
343
344    #[test]
345    fn test_reject_current_on_sucess_count() {
346        let lastest_round = create_round(100.0, 5, 0); // num success 30 < 10 min oracle result
347        let aggregator = create_aggregator(lastest_round);
348
349        assert!(
350            aggregator.get_result().is_err(),
351            "Aggregator is not currently populated with a valid round."
352        );
353    }
354
355    #[test]
356    fn test_no_valid_aggregator_result() {
357        let aggregator = create_aggregator(AggregatorRound::default());
358
359        assert!(
360            aggregator.get_result().is_err(),
361            "Aggregator is not currently populated with a valid round."
362        );
363    }
364}