diff --git a/contracts/predictify-hybrid/src/err.rs b/contracts/predictify-hybrid/src/err.rs index d6c9d9dc..55f46ac2 100644 --- a/contracts/predictify-hybrid/src/err.rs +++ b/contracts/predictify-hybrid/src/err.rs @@ -184,6 +184,8 @@ pub enum Error { // ===== VALIDATION ERRORS (435-437) ===== /// Market ID already exists in the registry. Cannot create duplicate market IDs. DuplicateMarketId = 441, + /// Override replay detected. Nonce has already been used. + ReplayedOverride = 442, // ===== CIRCUIT BREAKER ERRORS =====" /// Circuit breaker has not been initialized. Initialize before use. diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index b9962bee..6a6ab4e4 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -3960,6 +3960,8 @@ impl PredictifyHybrid { max_staleness_secs, max_confidence_bps, max_deviation_bps, + max_deviation_z_multiple: None, + history_size: None, }; crate::oracles::OracleValidationConfigManager::set_global_config(&env, &config)?; @@ -3999,6 +4001,8 @@ impl PredictifyHybrid { max_staleness_secs, max_confidence_bps, max_deviation_bps, + max_deviation_z_multiple: None, + history_size: None, }; crate::oracles::OracleValidationConfigManager::set_event_config(&env, &market_id, &config)?; diff --git a/contracts/predictify-hybrid/src/market_id_generator.rs b/contracts/predictify-hybrid/src/market_id_generator.rs index 0b653b81..ddd8ed38 100644 --- a/contracts/predictify-hybrid/src/market_id_generator.rs +++ b/contracts/predictify-hybrid/src/market_id_generator.rs @@ -520,7 +520,7 @@ pub struct MarketIdGenerator; counters.set(admin.clone(), counter); env.storage().persistent().set(&key, &counters); } - +} // ── Tests ───────────────────────────────────────────────────────────────────── diff --git a/contracts/predictify-hybrid/src/storage.rs b/contracts/predictify-hybrid/src/storage.rs index 200b4462..177365a4 100644 --- a/contracts/predictify-hybrid/src/storage.rs +++ b/contracts/predictify-hybrid/src/storage.rs @@ -40,6 +40,7 @@ enum StorageTtlTier { pub enum DataKey { Whitelisted(Address), Blacklisted(Address), + AdminOverrideNonce(Address), ArchivedMarket(Symbol, u64), /// Cumulative days extended for a given market (u32). MarketExtensionTotal(Symbol), diff --git a/contracts/predictify-hybrid/src/tests/oracle_differential_fuzz.rs b/contracts/predictify-hybrid/src/tests/oracle_differential_fuzz.rs new file mode 100644 index 00000000..3cd4e66f --- /dev/null +++ b/contracts/predictify-hybrid/src/tests/oracle_differential_fuzz.rs @@ -0,0 +1,57 @@ +#![cfg(test)] + +use proptest::prelude::*; +use soroban_sdk::{testutils::Address as _, Address, Env, String, Symbol}; +use crate::{ + oracles::{OracleInterface, PythOracle, PythFeedConfig, ReflectorOracle}, + types::Error, +}; + +#[derive(Debug, Clone, PartialEq)] +enum OracleOutcome { + Healthy(i128), + Unsupported, + Stale, + Scaled(i128), + UnknownError, +} + +fn categorize_outcome(result: Result) -> OracleOutcome { + match result { + Ok(price) => OracleOutcome::Healthy(price), + Err(Error::OracleUnavailable) => OracleOutcome::Unsupported, + Err(Error::OracleStale) => OracleOutcome::Stale, + Err(_) => OracleOutcome::UnknownError, + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(100))] + + #[test] + fn test_oracle_differential( + price in 0..i128::MAX, + ) { + let env = Env::default(); + let admin = Address::generate(&env); + + let pyth = PythOracle::new(admin.clone()); + let reflector = ReflectorOracle::new(admin.clone()); + + let feed_id = String::from_str(&env, "BTC/USD"); + + // Pyth is unsupported on Stellar right now + let pyth_res = categorize_outcome(pyth.get_price(&env, &feed_id)); + assert_eq!(pyth_res, OracleOutcome::Unsupported); + + // Reflector is the primary oracle but without a mock it will fail + // The differential check here ensures we handle unsupported explicitly + let reflector_res = categorize_outcome(reflector.get_price(&env, &feed_id)); + + // Either Reflector works (with mock) or returns an error, but Pyth MUST be unsupported + // Let's assert they don't unexpectedly succeed with different valid prices without mock + if let (OracleOutcome::Healthy(p1), OracleOutcome::Healthy(p2)) = (&pyth_res, &reflector_res) { + assert_eq!(p1, p2, "Oracles should not return mismatched healthy prices"); + } + } +} diff --git a/contracts/predictify-hybrid/src/upgrade_manager.rs b/contracts/predictify-hybrid/src/upgrade_manager.rs index 68c36271..01fe1a07 100644 --- a/contracts/predictify-hybrid/src/upgrade_manager.rs +++ b/contracts/predictify-hybrid/src/upgrade_manager.rs @@ -640,10 +640,10 @@ impl UpgradeManager { return Ok(true); } - let verify_count = if depth == 0 || depth > chain.len() { + let verify_count = if depth == 0 || depth > chain.len() as u64 { chain.len() } else { - depth + depth as u32 }; let zero_hash = BytesN::from_array(env, &[0u8; 32]); @@ -1146,7 +1146,7 @@ mod tests { fn test_upgrade_proposal_validation() { let env = Env::default(); let new_wasm_hash = BytesN::from_array(&env, &[1u8; 32]); - let target_version = Version::new(&env, 1, 1, 0, String::from_str(&env, "Upgrade"), false); + let target_version = Version::new(&env, 1, 1, 0, String::from_str(&env, "Upgrade"), false, 0); let mut proposal = UpgradeProposal::new( &env, @@ -1179,15 +1179,14 @@ mod tests { // Initialize version let version_manager = VersionManager::new(&env); let current_version = - Version::new(&env, 1, 0, 0, String::from_str(&env, "Current"), false); + Version::new(&env, 1, 0, 0, String::from_str(&env, "Current"), false, 0); version_manager .track_contract_version(&env, current_version) .unwrap(); // Create upgrade proposal let new_wasm_hash = BytesN::from_array(&env, &[1u8; 32]); - let target_version = - Version::new(&env, 1, 1, 0, String::from_str(&env, "Upgrade"), false); + let target_version = Version::new(&env, 1, 1, 0, String::from_str(&env, "Upgrade"), false, 0); let proposal = UpgradeProposal::new( &env,