Skip to main content

precolator/
oracle.rs

1// Oracle module - Price feed management and aggregation
2
3use serde::{Deserialize, Serialize};
4use solana_program::pubkey::Pubkey;
5use crate::errors::{ProgramError, Result};
6use crate::constants::*;
7
8#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
9pub struct PriceData {
10    pub price: u64,
11    pub confidence: u64,
12    pub last_update: i64,
13    pub slot: u64,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct OracleConfig {
18    pub oracle_id: Pubkey,
19    pub feed_id: Pubkey,
20    pub circuit_breaker_active: bool,
21    pub max_price_change_bps: u16,
22    pub staleness_threshold: i64,
23}
24
25pub struct OracleManager;
26
27impl OracleManager {
28    /// Validate oracle price and confidence
29    pub fn validate_price(price_data: &PriceData, current_slot: u64) -> Result<()> {
30        if price_data.price == 0 {
31            return Err(ProgramError::InvalidOraclePrice);
32        }
33
34        let slot_diff = current_slot.saturating_sub(price_data.slot) as i64;
35        if slot_diff > MAX_ORACLE_STALENESS_SLOTS {
36            return Err(ProgramError::StalePriceData);
37        }
38
39        Ok(())
40    }
41
42    /// Aggregate prices from multiple oracle sources
43    pub fn aggregate_prices(prices: &[PriceData], config: &OracleConfig) -> Result<PriceData> {
44        if prices.is_empty() {
45            return Err(ProgramError::InvalidOraclePrice);
46        }
47
48        let mut total_price: u128 = 0;
49        let mut total_confidence: u128 = 0;
50        let mut latest_update: i64 = 0;
51        let mut latest_slot: u64 = 0;
52
53        for price in prices {
54            total_price += price.price as u128;
55            total_confidence += price.confidence as u128;
56            if price.last_update > latest_update {
57                latest_update = price.last_update;
58            }
59            if price.slot > latest_slot {
60                latest_slot = price.slot;
61            }
62        }
63
64        let count = prices.len() as u128;
65        let avg_price = (total_price / count) as u64;
66        let avg_confidence = (total_confidence / count) as u64;
67
68        Ok(PriceData {
69            price: avg_price,
70            confidence: avg_confidence,
71            last_update: latest_update,
72            slot: latest_slot,
73        })
74    }
75
76    /// Check if price movement exceeds circuit breaker threshold
77    pub fn check_circuit_breaker(
78        previous_price: u64,
79        new_price: u64,
80        max_change_bps: u16,
81    ) -> Result<bool> {
82        if previous_price == 0 {
83            return Ok(true);
84        }
85
86        let change_bps = if new_price >= previous_price {
87            ((new_price - previous_price) as u128 * 10_000) / previous_price as u128
88        } else {
89            ((previous_price - new_price) as u128 * 10_000) / previous_price as u128
90        } as u16;
91
92        Ok(change_bps <= max_change_bps)
93    }
94
95    /// Calculate normalized price with specified decimals
96    pub fn normalize_price(price: u64, from_decimals: u8, to_decimals: u8) -> Result<u64> {
97        if from_decimals >= to_decimals {
98            Ok(price / 10u64.pow((from_decimals - to_decimals) as u32))
99        } else {
100            let multiplier = 10u64.pow((to_decimals - from_decimals) as u32);
101            price.checked_mul(multiplier).ok_or(ProgramError::Overflow)
102        }
103    }
104
105    /// Calculate price impact for large orders
106    pub fn calculate_price_impact(
107        order_size: u64,
108        available_liquidity: u64,
109        base_price: u64,
110    ) -> Result<u64> {
111        if available_liquidity == 0 {
112            return Err(ProgramError::InsufficientLiquidity);
113        }
114
115        let utilization_pct = (order_size as u128 * 100) / available_liquidity as u128;
116        let impact_bps = (utilization_pct as u16).min(500); // Max 5% impact
117
118        let impact = (base_price as u128 * impact_bps as u128) / 10_000;
119        Ok((base_price as u128 + impact) as u64)
120    }
121
122    /// Detect potential oracle manipulation
123    pub fn detect_manipulation(
124        prices: &[PriceData],
125        volatility_threshold: u16,
126    ) -> Result<bool> {
127        if prices.len() < 2 {
128            return Ok(false);
129        }
130
131        let mut min_price: u64 = u64::MAX;
132        let mut max_price: u64 = 0;
133
134        for price in prices {
135            min_price = min_price.min(price.price);
136            max_price = max_price.max(price.price);
137        }
138
139        let avg_price = prices.iter().map(|p| p.price as u128).sum::<u128>() / prices.len() as u128;
140        let volatility = if avg_price > 0 {
141            ((max_price as u128 - min_price as u128) * 10_000) / avg_price
142        } else {
143            0
144        } as u16;
145
146        Ok(volatility > volatility_threshold)
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_validate_price() {
156        let price_data = PriceData {
157            price: 1000,
158            confidence: 100,
159            last_update: 0,
160            slot: 100,
161        };
162
163        assert!(OracleManager::validate_price(&price_data, 200).is_ok());
164        assert!(OracleManager::validate_price(&price_data, 1000).is_err());
165    }
166
167    #[test]
168    fn test_aggregate_prices() {
169        let config = OracleConfig {
170            oracle_id: Pubkey::new_unique(),
171            feed_id: Pubkey::new_unique(),
172            circuit_breaker_active: false,
173            max_price_change_bps: 500,
174            staleness_threshold: 300,
175        };
176
177        let prices = vec![
178            PriceData {
179                price: 100,
180                confidence: 10,
181                last_update: 0,
182                slot: 100,
183            },
184            PriceData {
185                price: 110,
186                confidence: 10,
187                last_update: 1,
188                slot: 101,
189            },
190        ];
191
192        let result = OracleManager::aggregate_prices(&prices, &config);
193        assert!(result.is_ok());
194    }
195
196    #[test]
197    fn test_circuit_breaker() {
198        assert!(OracleManager::check_circuit_breaker(100, 105, 1000).is_ok());
199        assert!(OracleManager::check_circuit_breaker(100, 150, 100).is_ok());
200    }
201}