diff --git a/contracts/predictify-hybrid/src/events.rs b/contracts/predictify-hybrid/src/events.rs index 65c09d8f..e4723aef 100644 --- a/contracts/predictify-hybrid/src/events.rs +++ b/contracts/predictify-hybrid/src/events.rs @@ -5,6 +5,7 @@ use soroban_sdk::{contracttype, symbol_short, vec, Address, Env, Map, String, Sy use crate::config::Environment; use crate::errors::Error; +use crate::types::OracleProvider; // Define AdminRole locally since it's not available in the crate root #[derive(Clone, Debug, Eq, PartialEq)] @@ -767,6 +768,36 @@ pub struct DisputeAutoResolvedEvent { pub timestamp: u64, } +/// Governance proposal created event +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GovernanceProposalCreatedEvent { + pub proposal_id: Symbol, + pub proposer: Address, + pub title: String, + pub description: String, + pub timestamp: u64, +} + +/// Governance vote cast event +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GovernanceVoteCastEvent { + pub proposal_id: Symbol, + pub voter: Address, + pub support: bool, + pub timestamp: u64, +} + +/// Governance proposal executed event +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GovernanceProposalExecutedEvent { + pub proposal_id: Symbol, + pub executor: Address, + pub timestamp: u64, +} + /// Config initialized event #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -819,6 +850,42 @@ pub struct StorageMigrationEvent { pub timestamp: u64, } +/// Oracle degradation event - emitted when oracle service fails or becomes unavailable +#[contracttype] +#[derive(Clone, Debug)] +pub struct OracleDegradationEvent { + /// Oracle provider that failed + pub oracle: OracleProvider, + /// Reason for degradation + pub reason: String, + /// Degradation timestamp + pub timestamp: u64, +} + +/// Oracle recovery event - emitted when oracle service recovers +#[contracttype] +#[derive(Clone, Debug)] +pub struct OracleRecoveryEvent { + /// Oracle provider that recovered + pub oracle: OracleProvider, + /// Recovery message + pub recovery_message: String, + /// Recovery timestamp + pub timestamp: u64, +} + +/// Manual resolution required event - emitted when automatic resolution fails +#[contracttype] +#[derive(Clone, Debug)] +pub struct ManualResolutionRequiredEvent { + /// Market that requires manual resolution + pub market_id: Symbol, + /// Reason manual resolution is needed + pub reason: String, + /// Event timestamp + pub timestamp: u64, +} + /// Event emitted when circuit breaker state changes /// /// This event provides comprehensive information about circuit breaker @@ -1376,6 +1443,101 @@ impl EventEmitter { Self::store_event(env, &symbol_short!("cb_event"), event); } + /// Emit oracle degradation event when oracle service fails + pub fn emit_oracle_degradation( + env: &Env, + oracle: &OracleProvider, + reason: &String, + ) { + let event = OracleDegradationEvent { + oracle: oracle.clone(), + reason: reason.clone(), + timestamp: env.ledger().timestamp(), + }; + Self::store_event(env, &symbol_short!("ora_deg"), &event); + } + + /// Emit oracle recovery event when oracle service recovers + pub fn emit_oracle_recovery( + env: &Env, + oracle: &OracleProvider, + message: &String, + ) { + let event = OracleRecoveryEvent { + oracle: oracle.clone(), + recovery_message: message.clone(), + timestamp: env.ledger().timestamp(), + }; + Self::store_event(env, &symbol_short!("ora_rec"), &event); + } + + /// Emit manual resolution required event when automatic resolution fails + pub fn emit_manual_resolution_required( + env: &Env, + market_id: &Symbol, + reason: &String, + ) { + let event = ManualResolutionRequiredEvent { + market_id: market_id.clone(), + reason: reason.clone(), + timestamp: env.ledger().timestamp(), + }; + Self::store_event(env, &symbol_short!("man_res"), &event); + } + + /// Emit governance proposal created event + pub fn emit_governance_proposal_created( + env: &Env, + proposal_id: &Symbol, + proposer: &Address, + title: &String, + description: &String, + ) { + let event = GovernanceProposalCreatedEvent { + proposal_id: proposal_id.clone(), + proposer: proposer.clone(), + title: title.clone(), + description: description.clone(), + timestamp: env.ledger().timestamp(), + }; + + Self::store_event(env, &symbol_short!("gov_prop"), &event); + } + + /// Emit governance vote cast event + pub fn emit_governance_vote_cast( + env: &Env, + proposal_id: &Symbol, + voter: &Address, + support: bool, + ) { + let timestamp = env.ledger().timestamp(); + let event = GovernanceVoteCastEvent { + proposal_id: proposal_id.clone(), + voter: voter.clone(), + support, + timestamp, + }; + + Self::store_event(env, &symbol_short!("gov_vote"), &event); + } + + /// Emit governance proposal executed event + pub fn emit_governance_proposal_executed( + env: &Env, + proposal_id: &Symbol, + executor: &Address, + ) { + let timestamp = env.ledger().timestamp(); + let event = GovernanceProposalExecutedEvent { + proposal_id: proposal_id.clone(), + executor: executor.clone(), + timestamp, + }; + + Self::store_event(env, &symbol_short!("gov_exec"), &event); + } + /// Store event in persistent storage fn store_event(env: &Env, event_key: &Symbol, event_data: &T) where diff --git a/contracts/predictify-hybrid/src/governance.rs b/contracts/predictify-hybrid/src/governance.rs index e0291038..d28608d4 100644 --- a/contracts/predictify-hybrid/src/governance.rs +++ b/contracts/predictify-hybrid/src/governance.rs @@ -195,7 +195,6 @@ impl GovernanceContract { &proposal_id, &voter, support, - env.ledger().timestamp(), ); Ok(()) @@ -293,7 +292,6 @@ impl GovernanceContract { &env, &proposal_id, &caller, - env.ledger().timestamp(), ); Ok(()) diff --git a/contracts/predictify-hybrid/src/graceful_degradation.rs b/contracts/predictify-hybrid/src/graceful_degradation.rs new file mode 100644 index 00000000..220e653f --- /dev/null +++ b/contracts/predictify-hybrid/src/graceful_degradation.rs @@ -0,0 +1,170 @@ +#![allow(dead_code)] + +use crate::errors::Error; +use crate::events::EventEmitter; +use crate::oracles::{OracleInterface, ReflectorOracle}; +use crate::types::OracleProvider; +use soroban_sdk::{contracttype, Address, Env, String, Symbol}; + +// Basic oracle backup system +pub struct OracleBackup { + primary: OracleProvider, + backup: OracleProvider, +} + +impl OracleBackup { + pub fn new(primary: OracleProvider, backup: OracleProvider) -> Self { + Self { primary, backup } + } + + // Get price, try backup if primary fails + pub fn get_price(&self, env: &Env, oracle_address: &Address, feed_id: &String) -> Result { + // Try primary oracle + if let Ok(price) = self.call_oracle(env, &self.primary, oracle_address, feed_id) { + return Ok(price); + } + + // Primary failed, notify and try backup + let msg = String::from_str(env, "Primary oracle failed"); + EventEmitter::emit_oracle_degradation(env, &self.primary, &msg); + + self.call_oracle(env, &self.backup, oracle_address, feed_id) + } + + // Call a single oracle + fn call_oracle(&self, env: &Env, oracle: &OracleProvider, address: &Address, feed_id: &String) -> Result { + match oracle { + OracleProvider::Reflector => { + let reflector = ReflectorOracle::new(address.clone()); + reflector.get_price(env, feed_id) + } + _ => Err(Error::OracleUnavailable), + } + } + + // Is oracle working? + pub fn is_working(&self, env: &Env, oracle_address: &Address) -> bool { + let test_feed = String::from_str(env, "BTC/USD"); + self.call_oracle(env, &self.primary, oracle_address, &test_feed).is_ok() + } +} + +// Required functions to match original spec +pub fn fallback_oracle_call( + env: &Env, + primary_oracle: OracleProvider, + fallback_oracle: OracleProvider, + oracle_address: &Address, + feed_id: &String, +) -> Result { + let backup = OracleBackup::new(primary_oracle, fallback_oracle); + backup.get_price(env, oracle_address, feed_id) +} + +pub fn handle_oracle_timeout(oracle: OracleProvider, timeout_seconds: u32, env: &Env) { + if timeout_seconds > 60 { + let msg = String::from_str(env, "Oracle timeout"); + EventEmitter::emit_oracle_degradation(env, &oracle, &msg); + } +} + +pub fn partial_resolution_mechanism( + env: &Env, + market_id: Symbol, + available_data: PartialData, +) -> Result { + // Good enough confidence? Use the data + if available_data.confidence >= 70 && available_data.price.is_some() { + return Ok(String::from_str(env, "resolved")); + } + + // Not good enough, need human + let msg = String::from_str(env, "Need manual resolution"); + EventEmitter::emit_manual_resolution_required(env, &market_id, &msg); + Err(Error::OracleUnavailable) +} + +pub fn emit_degradation_event(env: &Env, oracle: OracleProvider, reason: String) { + EventEmitter::emit_oracle_degradation(env, &oracle, &reason); +} + +pub fn monitor_oracle_health(env: &Env, oracle: OracleProvider, oracle_address: &Address) -> OracleHealth { + let backup = OracleBackup::new(oracle.clone(), oracle); + if backup.is_working(env, oracle_address) { + OracleHealth::Working + } else { + OracleHealth::Broken + } +} + +pub fn get_degradation_status(oracle: OracleProvider, env: &Env, oracle_address: &Address) -> OracleHealth { + monitor_oracle_health(env, oracle, oracle_address) +} + +pub fn validate_degradation_strategy(_strategy: DegradationStrategy) -> Result<(), Error> { + Ok(()) // All strategies are fine +} + +// Simple data types +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DegradationStrategy { + UseBackup, + ManualFix, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum OracleHealth { + Working, + Broken, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PartialData { + pub price: Option, + pub confidence: i128, + pub timestamp: u64, +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::testutils::Address as _; + use soroban_sdk::Env; + + #[test] + fn can_create_backup() { + let backup = OracleBackup::new(OracleProvider::Reflector, OracleProvider::Pyth); + assert_eq!(backup.primary, OracleProvider::Reflector); + assert_eq!(backup.backup, OracleProvider::Pyth); + } + + #[test] + fn can_check_health() { + let env = Env::default(); + let addr = Address::generate(&env); + let health = monitor_oracle_health(&env, OracleProvider::Reflector, &addr); + assert!(matches!(health, OracleHealth::Working | OracleHealth::Broken)); + } + + #[test] + fn strategy_works() { + let result = validate_degradation_strategy(DegradationStrategy::UseBackup); + assert!(result.is_ok()); + } + + #[test] + fn partial_data_works() { + let env = Env::default(); + let market = Symbol::new(&env, "test"); + let data = PartialData { + price: Some(100), + confidence: 80, + timestamp: env.ledger().timestamp(), + }; + let result = partial_resolution_mechanism(&env, market, data); + assert!(result.is_ok()); + } +} \ No newline at end of file diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index 968176cb..c89c9260 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -17,6 +17,8 @@ mod errors; mod events; mod extensions; mod fees; +mod governance; +mod graceful_degradation; mod markets; mod monitoring; mod oracles; @@ -52,6 +54,11 @@ use admin::{AdminInitializer, AdminManager, AdminRole, AdminPermission, AdminAna pub use errors::Error; pub use types::*; +use crate::config::{ + ConfigChanges, ConfigManager, ConfigUpdateRecord, ContractConfig, MarketLimits, +}; +use crate::graceful_degradation::{OracleBackup, OracleHealth}; +use crate::reentrancy_guard::ReentrancyGuard; use alloc::format; use soroban_sdk::{ contract, contractimpl, panic_with_error, Address, Env, Map, String, Symbol, Vec, @@ -1400,7 +1407,70 @@ impl PredictifyHybrid { monitoring::ContractMonitor::validate_monitoring_data(&env, &data) } - // ===== MULTI-ADMIN MANAGEMENT FUNCTIONS ===== + // ===== ORACLE FALLBACK FUNCTIONS ===== + + /// Get oracle data with backup if primary fails + pub fn get_oracle_with_backup( + env: Env, + market_id: Symbol, + oracle_contract: Address, + primary_oracle: OracleProvider, + backup_oracle: OracleProvider, + ) -> Result { + // Get market info + let market = env + .storage() + .persistent() + .get::(&market_id) + .ok_or(Error::MarketNotFound)?; + + // Check if market ended + let current_time = env.ledger().timestamp(); + if current_time < market.end_time { + return Err(Error::MarketClosed); + } + + // Try to get price with backup + let backup = OracleBackup::new(primary_oracle, backup_oracle); + match backup.get_price(&env, &oracle_contract, &market.oracle_config.feed_id) { + Ok(price) => { + // Simple comparison logic + let threshold = market.oracle_config.threshold; + let comparison = &market.oracle_config.comparison; + + let result = if comparison == &String::from_str(&env, "gt") { + if price > threshold { "yes" } else { "no" } + } else if comparison == &String::from_str(&env, "lt") { + if price < threshold { "yes" } else { "no" } + } else { + if price == threshold { "yes" } else { "no" } + }; + + Ok(String::from_str(&env, result)) + } + Err(_) => { + // Both oracles failed + let reason = String::from_str(&env, "All oracles failed"); + events::EventEmitter::emit_manual_resolution_required(&env, &market_id, &reason); + Err(Error::OracleUnavailable) + } + } + } + + /// Check if oracle is working + pub fn check_oracle_status( + env: Env, + oracle: OracleProvider, + oracle_contract: Address, + ) -> String { + let health = graceful_degradation::monitor_oracle_health(&env, oracle, &oracle_contract); + match health { + OracleHealth::Working => String::from_str(&env, "working"), + OracleHealth::Broken => String::from_str(&env, "broken"), + } + } + + // ===== MULTI-ADMIN MANAGEMENT FUNCTIONS ===== /// Add a new admin with specified role (SuperAdmin only) pub fn add_admin(