switchboard_solana/oracle_program/accounts/
aggregator.rs1use 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 pub data: [u8; 32],
11}
12
13#[zero_copy(unsafe)]
14#[repr(packed)]
15#[derive(Default, PartialEq, Eq)]
16pub struct AggregatorRound {
17 pub num_success: u32,
20 pub num_error: u32,
22 pub is_closed: bool,
24 pub round_open_slot: u64,
26 pub round_open_timestamp: i64,
28 pub result: SwitchboardDecimal,
30 pub std_deviation: SwitchboardDecimal,
32 pub min_response: SwitchboardDecimal,
34 pub max_response: SwitchboardDecimal,
36 pub oracle_pubkeys_data: [Pubkey; 16],
38 pub medians_data: [SwitchboardDecimal; 16],
40 pub current_payout: [i64; 16],
42 pub medians_fulfilled: [bool; 16],
44 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#[account(zero_copy(unsafe))]
57#[repr(packed)]
58#[derive(PartialEq)]
59pub struct AggregatorAccountData {
60 pub name: [u8; 32],
62 pub metadata: [u8; 128],
64 pub _reserved1: [u8; 32],
66 pub queue_pubkey: Pubkey,
68 pub oracle_request_batch_size: u32,
71 pub min_oracle_results: u32,
73 pub min_job_results: u32,
75 pub min_update_delay_seconds: u32,
77 pub start_after: i64,
79 pub variance_threshold: SwitchboardDecimal,
81 pub force_report_period: i64,
83 pub expiration: i64,
85 pub consecutive_failure_count: u64,
88 pub next_allowed_update_time: i64,
90 pub is_locked: bool,
92 pub crank_pubkey: Pubkey,
94 pub latest_confirmed_round: AggregatorRound,
96 pub current_round: AggregatorRound,
98 pub job_pubkeys_data: [Pubkey; 16],
100 pub job_hashes: [Hash; 16],
102 pub job_pubkeys_size: u32,
104 pub jobs_checksum: [u8; 32],
106 pub authority: Pubkey,
109 pub history_buffer: Pubkey,
111 pub previous_confirmed_round_result: SwitchboardDecimal,
113 pub previous_confirmed_round_slot: u64,
115 pub disable_crank: bool,
117 pub job_weights: [u8; 16],
119 pub creation_timestamp: i64,
121 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 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 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 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 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 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 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 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); 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); 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}