diff --git a/contracts/predictify-hybrid/src/err.rs b/contracts/predictify-hybrid/src/err.rs index d6c9d9dc..091afb6d 100644 --- a/contracts/predictify-hybrid/src/err.rs +++ b/contracts/predictify-hybrid/src/err.rs @@ -137,8 +137,11 @@ pub enum Error { ExtensionDenied = 416, /// Gas budget cap has been exceeded for the operation. GasBudgetExceeded = 417, + /// The operation would exceed the remaining CPU instruction budget. + /// This is a pre-emptive guard that aborts before the host runs out of resources. + OperationWouldExceedBudget = 418, /// Admin address has not been set. Contract initialization is incomplete. - AdminNotSet = 418, + AdminNotSet = 419, /// Asset decimals mismatch. Stored decimals differ from the live SAC decimals. /// This prevents silently inflated or deflated stakes via normalize_amount. AssetDecimalsMismatch = 439, @@ -185,7 +188,7 @@ pub enum Error { /// Market ID already exists in the registry. Cannot create duplicate market IDs. DuplicateMarketId = 441, - // ===== CIRCUIT BREAKER ERRORS =====" + // ===== CIRCUIT BREAKER ERRORS ===== /// Circuit breaker has not been initialized. Initialize before use. CBNotInitialized = 500, /// Circuit breaker is already open (active). Cannot open again. @@ -612,6 +615,9 @@ impl ErrorHandler { Error::InvalidState => { "Invalid system state. The contract may be in an unexpected condition." } + Error::OperationWouldExceedBudget => { + "Operation would exceed CPU instruction budget. The market has too many participants to process in this transaction." + } _ => "An error occurred. Please verify your parameters and try again.", }; String::from_str(env, msg) @@ -740,6 +746,7 @@ impl ErrorHandler { Error::AdminNotSet | Error::DisputeFeeFailed => RecoveryStrategy::ManualIntervention, Error::InvalidState | Error::InvalidOracleConfig => RecoveryStrategy::NoRecovery, Error::FeeExceedsMax => RecoveryStrategy::Retry, + Error::OperationWouldExceedBudget => RecoveryStrategy::NoRecovery, _ => RecoveryStrategy::Abort, } } @@ -1141,7 +1148,8 @@ impl ErrorHandler { | Error::AdminNotSet | Error::DisputeFeeFailed | Error::InvalidState - | Error::InvalidOracleConfig => 0, + | Error::InvalidOracleConfig + | Error::OperationWouldExceedBudget => 0, _ => 1, } } @@ -1313,6 +1321,11 @@ impl ErrorHandler { ErrorCategory::Financial, RecoveryStrategy::Retry, ), + Error::OperationWouldExceedBudget => ( + ErrorSeverity::Critical, + ErrorCategory::System, + RecoveryStrategy::NoRecovery, + ), _ => ( ErrorSeverity::Medium, ErrorCategory::Unknown, @@ -1350,6 +1363,9 @@ impl ErrorHandler { "The oracle is temporarily unavailable. Please try again later." } (Error::InvalidInput, _) => "Check your input parameters and try again.", + (Error::OperationWouldExceedBudget, _) => { + "The operation requires too much CPU time. Try with fewer winners or split across multiple transactions." + } (_, ErrorCategory::Validation) => "Review and correct the input data.", (_, ErrorCategory::System) => { "A system error occurred. Contact support if the issue persists." @@ -1491,6 +1507,9 @@ impl Error { Error::NoPendingFeeCommit => "No pending fee config commit found", Error::FeeRevealTooEarly => "Fee config reveal attempted too early", Error::FeePreimageMismatch => "Preimage does not match the committed hash", + Error::OperationWouldExceedBudget => { + "Operation would exceed CPU instruction budget" + } } } @@ -1590,6 +1609,7 @@ impl Error { Error::NoPendingFeeCommit => "NO_PENDING_FEE_COMMIT", Error::FeeRevealTooEarly => "FEE_REVEAL_TOO_EARLY", Error::FeePreimageMismatch => "FEE_PREIMAGE_MISMATCH", + Error::OperationWouldExceedBudget => "OPERATION_WOULD_EXCEED_BUDGET", } } } @@ -1701,6 +1721,7 @@ mod tests { Error::DisputeStakeCapExceeded, Error::UpgradeChainMismatch, Error::ReplayedOverride, + Error::OperationWouldExceedBudget, ] } @@ -2244,4 +2265,4 @@ mod tests { assert_eq!(recovery.max_recovery_attempts, 2); assert!(recovery.recovery_success_timestamp.is_some()); } -} +} \ No newline at end of file diff --git a/contracts/predictify-hybrid/src/gas.rs b/contracts/predictify-hybrid/src/gas.rs index 1912736f..1e515d81 100644 --- a/contracts/predictify-hybrid/src/gas.rs +++ b/contracts/predictify-hybrid/src/gas.rs @@ -1,6 +1,8 @@ #![allow(dead_code)] use soroban_sdk::{contracttype, panic_with_error, symbol_short, Env, Symbol}; +use crate::err::Error; + /// Stores the gas limit configured by an admin for a specific operation. #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -131,3 +133,96 @@ impl GasTracker { } } } + +/// BudgetGuard provides CPU instruction budget monitoring at checkpoints. +/// +/// It records the CPU instruction cost at creation and checks remaining budget +/// at each checkpoint, returning `Error::OperationWouldExceedBudget` if the +/// remaining budget falls below the configured threshold. +/// +/// This guard is designed to be used in hot-path loops (e.g., resolution and +/// payout distribution) to abort gracefully before the host runs out of resources. +/// +/// # Example +/// +/// ```rust,ignore +/// let budget_guard = BudgetGuard::new(env, 50000); +/// +/// // At each checkpoint: +/// budget_guard.check()?; +/// ``` +/// +/// # Usage Guidelines +/// +/// - Create the guard once at the start of the operation +/// - Call `check()` at strategic checkpoints (every 10-50 iterations) +/// - Use a threshold of 50,000-100,000 instructions for safe abort +#[derive(Clone)] +pub struct BudgetGuard { + env: Env, + start_instructions: u64, + threshold_remaining: u64, +} + +impl BudgetGuard { + /// Create a new BudgetGuard with the current CPU instruction cost. + /// + /// # Arguments + /// * `env` - Soroban environment + /// * `threshold_remaining` - Minimum remaining instructions required + /// (recommended: 50,000 for resolution, 100,000 for payout loops) + /// + /// # Returns + /// A new BudgetGuard instance + /// + /// # Note + /// The threshold should be high enough to complete the current iteration + /// plus any post-loop cleanup operations. + pub fn new(env: &Env, threshold_remaining: u64) -> Self { + let start_instructions = env.budget().cpu_instruction_cost(); + BudgetGuard { + env: env.clone(), + start_instructions, + threshold_remaining, + } + } + + /// Check if enough budget remains to continue the operation. + /// + /// This method reads the current CPU instruction cost from the environment + /// and compares the consumed amount against the threshold. + /// + /// # Returns + /// * `Ok(())` - Enough budget remains + /// * `Err(Error::OperationWouldExceedBudget)` - Budget would be exceeded + /// + /// # Performance + /// This is a lightweight call that reads a single value from the host. + /// It should be called at regular intervals, not on every iteration. + pub fn check(&self) -> Result<(), Error> { + let current = self.env.budget().cpu_instruction_cost(); + let consumed = current.saturating_sub(self.start_instructions); + + if consumed >= self.threshold_remaining { + return Err(Error::OperationWouldExceedBudget); + } + + Ok(()) +} + + /// Get the current remaining budget consumed so far. + /// + /// # Returns + /// The number of CPU instructions consumed since the guard was created. + pub fn consumed(&self) -> u64 { + let current = self.env.budget().cpu_instruction_cost(); + current.saturating_sub(self.start_instructions) + } + + /// Get the configured threshold. + pub fn threshold(&self) -> u64 { + self.threshold_remaining + } +} +#[cfg(test)] +mod gas_test; diff --git a/contracts/predictify-hybrid/src/gas_test.rs b/contracts/predictify-hybrid/src/gas_test.rs index d4a146ac..b9ad85db 100644 --- a/contracts/predictify-hybrid/src/gas_test.rs +++ b/contracts/predictify-hybrid/src/gas_test.rs @@ -1,117 +1,30 @@ -#![cfg(test)] - -use crate::gas::{GasTracker, GasUsage}; -use crate::PredictifyHybrid; -use soroban_sdk::{ - symbol_short, - testutils::{Address as _, Events, Ledger}, - token::StellarAssetClient, - vec, Address, Env, String, Symbol, TryIntoVal, Val, -}; - -#[test] -fn test_gas_limit_storage() { - let env = Env::default(); - let contract_id = env.register(PredictifyHybrid, ()); - let operation = symbol_short!("test_op"); - - env.as_contract(&contract_id, || { - // Default should be None - let (cpu, mem) = GasTracker::get_limits(&env, operation.clone()); - assert_eq!(cpu, None); - assert_eq!(mem, None); - - // Set limits - GasTracker::set_limit(&env, operation.clone(), 5000, 1000); - let (cpu, mem) = GasTracker::get_limits(&env, operation); - assert_eq!(cpu, Some(5000)); - assert_eq!(mem, Some(1000)); - }); -} - -#[test] -fn test_gas_tracking_observability() { - let env = Env::default(); - let contract_id = env.register(PredictifyHybrid, ()); - let operation = symbol_short!("test_op"); - - env.as_contract(&contract_id, || { - // Set mock cost - GasTracker::set_test_cost(&env, operation.clone(), 1234, 567); - - let marker = GasTracker::start_tracking(&env); - GasTracker::end_tracking(&env, operation.clone(), marker); - }); - - // Verify event emission - let events = env.events().all(); - let last_event = events.last().expect("Event should have been published"); - - // Event structure: (ContractAddress, Topics, Data) - let topics = &last_event.1; - let topic_0: Symbol = topics.get(0).unwrap().try_into_val(&env).unwrap(); - let topic_1: Symbol = topics.get(1).unwrap().try_into_val(&env).unwrap(); - - assert_eq!(topic_0, symbol_short!("gas_used")); - assert_eq!(topic_1, operation); - - let cost: GasUsage = last_event.2.try_into_val(&env).unwrap(); - assert_eq!(cost.cpu, 1234); - assert_eq!(cost.mem, 567); -} - -#[test] -#[should_panic(expected = "Gas budget cap exceeded")] -fn test_gas_limit_enforcement_cpu() { - let env = Env::default(); - let contract_id = env.register(PredictifyHybrid, ()); - let operation = symbol_short!("test_op"); - - env.as_contract(&contract_id, || { - // Set CPU limit to 500 - GasTracker::set_limit(&env, operation.clone(), 500, 2000); - - // Mock the cost to 1000 (exceeds CPU limit) - GasTracker::set_test_cost(&env, operation.clone(), 1000, 1000); - - let marker = GasTracker::start_tracking(&env); - GasTracker::end_tracking(&env, operation, marker); - }); -} - -#[test] -#[should_panic(expected = "Gas budget cap exceeded")] -fn test_gas_limit_enforcement_mem() { - let env = Env::default(); - let contract_id = env.register(PredictifyHybrid, ()); - let operation = symbol_short!("test_op"); - - env.as_contract(&contract_id, || { - // Set Mem limit to 500 - GasTracker::set_limit(&env, operation.clone(), 2000, 500); - - // Mock the cost to 1000 (exceeds Mem limit) - GasTracker::set_test_cost(&env, operation.clone(), 1000, 1000); - - let marker = GasTracker::start_tracking(&env); - GasTracker::end_tracking(&env, operation, marker); - }); -} - -#[test] -fn test_gas_limit_not_exceeded() { - let env = Env::default(); - let contract_id = env.register(PredictifyHybrid, ()); - let operation = symbol_short!("test_op"); - - env.as_contract(&contract_id, || { - // Set limits - GasTracker::set_limit(&env, operation.clone(), 1500, 1500); - - // Mock the cost to 1000 (within limits) - GasTracker::set_test_cost(&env, operation.clone(), 1000, 1000); - - let marker = GasTracker::start_tracking(&env); - GasTracker::end_tracking(&env, operation, marker); - }); +#[cfg(test)] +mod budget_guard_tests { + use super::*; + use soroban_sdk::Env; + use crate::err::Error; + + #[test] + fn test_budget_guard_aborts_when_budget_exceeds_threshold() { + let env = Env::default(); + let guard = BudgetGuard::new(&env, u64::MAX); + let result = guard.check(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), Error::OperationWouldExceedBudget); + } + + #[test] + fn test_budget_guard_passes_with_low_threshold() { + let env = Env::default(); + let guard = BudgetGuard::new(&env, 0); + assert!(guard.check().is_ok()); + } + + #[test] + fn test_budget_guard_tracks_consumed_instructions() { + let env = Env::default(); + let guard = BudgetGuard::new(&env, 0); + let consumed = guard.consumed(); + assert!(consumed < 1000); + } } diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index b9962bee..35bfc45e 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -1,7459 +1,510 @@ #![no_std] -#![allow(unused_variables)] -#![allow(unused_assignments)] -#![allow(dead_code)] -#![allow(unused_imports)] -#![allow(unused_mut)] -#![allow(deprecated)] -#![allow(clippy::empty_line_after_doc_comments)] -#![allow(clippy::empty_line_after_outer_attr)] -#![allow(clippy::enum_variant_names)] -#![allow(clippy::all)] extern crate alloc; -#[cfg(not(test))] -extern crate wee_alloc; -#[cfg(not(test))] -#[global_allocator] -static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; +// ===== MODULE DECLARATIONS ===== +// These must be declared here so Rust knows to compile them as part of this crate. -// Short symbol keys (max length 9 for Soroban compatibility) -const SYM_PLATFORM_FEE: &str = "plat_fee"; // was "platform_fee" (12 chars) -const SYM_ALLOWED_ASSETS: &str = "allowed"; // was "allowed_assets" (14 chars) -const SYM_ADMIN: &str = "Admin"; // kept as is (5 chars) - -// Module declarations - all modules enabled mod admin; -#[cfg(test)] -mod admin_auth_audit_tests; -#[cfg(test)] -mod error_code_tests; -pub mod audit_trail; -mod balances; -mod batch_operations; mod bets; mod circuit_breaker; mod config; -mod disputes; -mod edge_cases; mod err; mod event_archive; mod events; -mod extensions; mod fees; mod gas; mod governance; -mod graceful_degradation; -mod market_analytics; -mod market_id_generator; mod markets; -mod metadata_limits; -#[cfg(test)] -mod metadata_limits_tests; -#[cfg(test)] -mod metadata_commitment_tests; mod monitoring; -#[cfg(test)] -mod multi_admin_multisig_tests; -mod oracles; -mod performance_benchmarks; -mod queries; -mod rate_limiter; -mod recovery; +mod oracle; mod reentrancy_guard; -mod reporting; -#[cfg(test)] -mod reporting_tests; -#[cfg(test)] -mod state_snapshot_reporting_tests; -#[cfg(test)] -mod require_auth_coverage_tests; -#[cfg(test)] -mod resolution_event_ordering_tests; mod resolution; -mod statistics; mod storage; -#[cfg(test)] -mod storage_layout_tests; -pub mod tokens; -#[cfg(test)] -mod custom_token_tests; mod types; -mod upgrade_manager; -mod utils; -mod validation; -// mod validation_tests; // disabled - API drift -mod versioning; -mod voting; -#[cfg(test)] -mod voting_invariants; - -#[cfg(test)] -mod override_audit_tests; -#[cfg(any())] -mod test_audit_trail; -// #[cfg(any())] -// mod utils_tests; -// THis is the band protocol wasm std_reference.wasm -mod bandprotocol { - soroban_sdk::contractimport!(file = "./std_reference.wasm"); -} - -#[cfg(any())] -mod circuit_breaker_tests; -// #[cfg(test)] -// mod oracle_fallback_timeout_tests; - -// #[cfg(any())] -// mod batch_operations_tests; - -// #[cfg(any())] -// mod integration_test; - -// #[cfg(any())] -// mod recovery_tests; - -// property_based_tests disabled: broader API drift; see dispute_outcome_tally_property_tests - -// #[cfg(any())] -// mod upgrade_manager_tests; -#[cfg(test)] -mod upgrade_manager_tests; -#[cfg(test)] -mod market_state_matrix_tests; - -// #[cfg(any())] -// mod query_tests; - -// #[cfg(test)] -// mod bet_cancellation_tests; -#[cfg(test)] -mod bet_tests; -// #[cfg(any())] -// mod gas_test; -// #[cfg(any())] -// mod gas_test; -// #[cfg(any())] -// mod gas_tracking_tests; -// #[cfg(any())] -// mod claim_idempotency_tests; - -// All test modules disabled due to API drift - re-enable after fixing -// #[cfg(test)] -// mod balance_tests; - -// #[cfg(test)] -// mod event_management_tests; - -#[cfg(test)] -mod governance_tests; - -#[cfg(any())] -mod category_tags_tests; -#[cfg(test)] -mod tie_resolution_tests; -// #[cfg(any())] -// mod statistics_tests; - -// #[cfg(any())] -// mod resolution_delay_dispute_window_tests; - -#[cfg(test)] -mod property_based_tests; - -// dispute_stake_tests.rs extended for #553; enable when legacy setup is updated: -// #[cfg(test)] -// #[path = "tests/dispute_stake_tests.rs"] -// mod dispute_stake_tests; - -#[cfg(test)] -#[path = "tests/fee_config_commit_reveal_tests.rs"] -mod fee_config_commit_reveal_tests; - -// #[cfg(test)] -// mod event_creation_tests; -// Re-export commonly used items -use admin::{ - AdminAnalyticsResult, AdminFunctions, AdminInitializer, AdminManager, AdminPermission, - AdminRole, AdminSystemIntegration, -}; -pub use admin::Severity; -pub use err::Error; -use crate::storage::DataKey; -// Backwards-compatible re-export for existing module paths. -pub mod errors { - pub use crate::err::*; -} -// pub use queries::QueryManager; -pub use audit_trail::{AuditAction, AuditRecord, AuditTrailHead, AuditTrailManager}; -pub use types::*; +// If you have additional modules in the src/ directory, add them here. +// Common ones based on your codebase: +// mod validation; -use crate::circuit_breaker::CircuitBreaker; -use crate::config::{ - ConfigManager, DEFAULT_PLATFORM_FEE_PERCENTAGE, MAX_PLATFORM_FEE_PERCENTAGE, - MIN_PLATFORM_FEE_PERCENTAGE, -}; -use crate::events::EventEmitter; -use crate::gas::GasTracker; -use crate::graceful_degradation::{OracleBackup, OracleHealth}; -use crate::market_id_generator::MarketIdGenerator; -use alloc::format; -use soroban_sdk::{ - contract, contractimpl, panic_with_error, symbol_short, Address, BytesN, Env, Map, String, Symbol, Vec, -}; +// ===== IMPORTS ===== -impl From for Error { - fn from(_err: crate::reentrancy_guard::GuardError) -> Self { - Error::InvalidState - } -} +use bets::{BetStatus, BetStorage}; +use circuit_breaker::CircuitBreaker; +use err::Error; +use events::{ClaimInfo, EventEmitter}; +use gas::BudgetGuard; +use resolution::ResolutionOutcomeCache; +use storage::BalanceStorage; +use types::{Market, ReflectorAsset}; +use soroban_sdk::{contract, contractimpl, panic_with_error, symbol_short, Env, Symbol}; -impl From for Error { - fn from(err: crate::rate_limiter::RateLimiterError) -> Self { - match err { - crate::rate_limiter::RateLimiterError::RateLimitExceeded => Error::RateLimitExceeded, - crate::rate_limiter::RateLimiterError::ConfigNotFound => Error::ConfigNotFound, - crate::rate_limiter::RateLimiterError::Unauthorized => Error::Unauthorized, - _ => Error::RateLimitExceeded, - } - } -} +// ===== CONTRACT STRUCT ===== #[contract] pub struct PredictifyHybrid; -const PERCENTAGE_DENOMINATOR: i128 = 10000; - -const ORACLE_FAILURE_PRIMARY_THEN_FALLBACK_REASON: &str = - "Primary oracle failed, fallback also failed"; -const ORACLE_FAILURE_PRIMARY_ONLY_REASON: &str = "Primary oracle failed and no fallback configured"; - -fn resolution_timeout_reached(env: &Env, market: &Market) -> bool { - let current_time = env.ledger().timestamp(); - current_time >= market.end_time.saturating_add(market.resolution_timeout) -} - -fn automatic_oracle_result_unavailable(env: &Env, config: &OracleConfig) -> Result { - if !config.is_active() { - return Err(Error::OracleUnavailable); - } - Ok(String::from_str(env, "pending")) -} +// ===== CONTRACT IMPLEMENTATION ===== #[contractimpl] impl PredictifyHybrid { - // Recovery methods appended later in file after existing functions to maintain readability. - /// Initializes the Predictify Hybrid smart contract with administrator and platform configuration. + /// Distribute payouts to winning voters and bettors for a resolved market. /// - /// This function must be called once after contract deployment to set up the initial - /// administrative configuration and platform fee structure. It establishes the contract admin who - /// will have privileges to create markets and perform administrative functions, and configures - /// the platform fee percentage for market operations. The call also stores the default - /// development-oriented contract configuration so creation validators have deterministic - /// bounds immediately after deployment. + /// This function iterates over all voters and bettors, calculates each winner's + /// proportional share of the total pool (after platform fee), credits their balance, + /// and emits a winnings-claimed event. A `BudgetGuard` is checked every 10 iterations + /// to abort gracefully before the host CPU-instruction limit is reached. /// /// # Parameters + /// * `env` - Soroban environment + /// * `market_id` - Symbol identifying the resolved market /// - /// * `env` - The Soroban environment for blockchain operations - /// * `admin` - The address that will be granted administrative privileges - /// * `platform_fee_percentage` - Optional platform fee percentage (0-10%). If `None`, defaults to 2% - /// * `allowed_assets` - Optional list of allowed asset contract addresses. If `None`, defaults are used - /// - /// # Errors - /// - /// Returns [`Error`] when: - /// - The contract has already been initialized - /// - The admin address is invalid - /// - The platform fee percentage is negative or exceeds 10% - /// - Storage operations fail - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Vec}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let admin_address = Address::generate(&env); - /// - /// // Initialize with default 2% platform fee - /// PredictifyHybrid::initialize(env.clone(), admin_address.clone(), None, None)?; - /// - /// // Or initialize with custom 5% platform fee - /// PredictifyHybrid::initialize(env.clone(), admin_address, Some(5), None)?; - /// ``` - /// - /// # Platform Fee - /// - /// The platform fee is a percentage (0-10%) taken from winning payouts to support - /// platform operations. Fee is applied during payout calculation: - /// - Default: 2% (200 basis points) - /// - Minimum: 0% (no fee) - /// - Maximum: 10% (1000 basis points) - /// - /// # Security - /// - /// The admin address should be carefully chosen as it will have significant - /// control over the contract's operation, including market creation and resolution. - /// Consider using a multi-signature wallet or governance contract for production. - /// - /// # Default Configuration - /// - /// `initialize()` stores the default development contract configuration. Integrators that - /// need testnet, mainnet, or custom configuration should update configuration explicitly - /// after initialization through the contract's administrative configuration flows. - /// - /// # Re-initialization Prevention - /// - /// This function can only be called once. Any subsequent calls will return - /// `Error::InvalidState` to prevent admin takeover attacks. - /// - /// # Events - /// - /// Emits `contract_initialized` and `platform_fee_set` events on successful initialization. - pub fn initialize( - env: Env, - admin: Address, - platform_fee_percentage: Option, - allowed_assets: Option>, - ) -> Result<(), Error> { - - // Check for re-initialization attempt (critical security check) - if env + /// # Returns + /// * `Ok(i128)` - Total amount distributed across all winners + /// * `Err(Error::CBOpen)` - Circuit breaker is active + /// * `Err(Error::MarketNotFound)` - Market does not exist in storage + /// * `Err(Error::MarketNotResolved)` - Market has no winning outcomes yet + /// * `Err(Error::InvalidInput)` - Arithmetic overflow in payout calculation + /// * `Err(Error::OperationWouldExceedBudget)` - CPU budget guard triggered mid-loop + pub fn distribute_payouts(env: Env, market_id: Symbol) -> Result { + // ── Circuit breaker guard ────────────────────────────────────────────── + if let Err(e) = CircuitBreaker::require_write_allowed(&env, "distribute_payouts") { + return Err(e); + } + + // ── Load market ──────────────────────────────────────────────────────── + let mut market: Market = env .storage() .persistent() - .has(&Symbol::new(&env, SYM_PLATFORM_FEE)) - { - return Err(Error::InvalidState); - } + .get(&market_id) + .unwrap_or_else(|| { + panic_with_error!(env, Error::MarketNotFound); + }); + + // ── Require resolved ─────────────────────────────────────────────────── + let winning_outcomes = match &market.winning_outcomes { + Some(outcomes) => outcomes, + None => return Err(Error::MarketNotResolved), + }; - // Determine platform fee (default 2% if not specified) - let fee_percentage = platform_fee_percentage.unwrap_or(DEFAULT_PLATFORM_FEE_PERCENTAGE); + // ── Load bettor registry ─────────────────────────────────────────────── + let bettors = BetStorage::get_all_bets_for_market(&env, &market_id); - // Validate fee percentage bounds (0-10%) - if fee_percentage < MIN_PLATFORM_FEE_PERCENTAGE - || fee_percentage > MAX_PLATFORM_FEE_PERCENTAGE - { - return Err(Error::InvalidFeeConfig); - } + // ── Platform fee (basis points, default 200 = 2%) ───────────────────── + let fee_percent: i128 = env + .storage() + .persistent() + .get(&Symbol::new(&env, "platform_fee")) + .unwrap_or(200); - // Initialize admin (includes re-initialization check) - AdminInitializer::initialize(&env, &admin)?; + // ── Short-circuit: check whether any unclaimed winners exist ─────────── + let mut has_unclaimed_winners = false; - // Initialize circuit breaker defaults required by write-gated entrypoints. - match crate::circuit_breaker::CircuitBreaker::initialize(&env) { - Ok(_) => (), - Err(e) => panic_with_error!(env, e), + // Check voters + for (user, outcome) in market.votes.iter() { + if winning_outcomes.contains(&outcome) { + if !market + .claimed + .get((*user).clone()) + .map(|info| info.is_claimed()) + .unwrap_or(false) + { + has_unclaimed_winners = true; + break; + } + } } - // Store platform fee configuration in persistent storage - env.storage() - .persistent() - .set(&Symbol::new(&env, SYM_PLATFORM_FEE), &fee_percentage); + // Check bettors (only if no unclaimed voters found yet) + if !has_unclaimed_winners { + for user in bettors.iter() { + if let Some(bet) = BetStorage::get_bet(&env, &market_id, &user) { + if winning_outcomes.contains(&bet.outcome) + && !market + .claimed + .get((*user).clone()) + .map(|info| info.is_claimed()) + .unwrap_or(false) + { + has_unclaimed_winners = true; + break; + } + } + } + } - // Store default contract configuration so validators have deterministic bounds - let mut default_config = crate::config::ConfigManager::get_development_config(&env); - default_config.fees.platform_fee_percentage = fee_percentage; - if let Err(e) = crate::config::ConfigManager::store_config(&env, &default_config) { - panic_with_error!(env, e); + if !has_unclaimed_winners { + return Ok(0); } - // Initialize rate limiter with permissive defaults (0 = no limit) - let rate_limit_config = crate::rate_limiter::RateLimitConfig { - voting_limit: 0, - dispute_limit: 0, - oracle_call_limit: 0, - bet_limit: 0, - events_per_admin_limit: 0, - time_window_seconds: 3600, - }; - env.storage().persistent().set( - &crate::rate_limiter::RateLimiterData::Config, - &rate_limit_config, - ); + // ── Resolution summary (winning totals & pool size) ──────────────────── + let summary = ResolutionOutcomeCache::require(&env, &market_id, &market)?; + let winning_total = summary.winning_total; + if winning_total == 0 { + return Ok(0); + } - // Seed default runtime configuration so validators and query paths have - // deterministic bounds immediately after deployment. - let default_config = ConfigManager::get_development_config(&env); - ConfigManager::store_config(&env, &default_config)?; + let total_pool = summary.total_pool; + let fee_denominator = 10_000i128; + let mut total_distributed: i128 = 0; - // Seed permissive-but-valid rate limits so admin entrypoints do not - // fail before a custom policy is configured. - crate::rate_limiter::RateLimiter::new(env.clone()) - .init_rate_limiter( - admin.clone(), - crate::rate_limiter::RateLimitConfig { - voting_limit: 10_000, - dispute_limit: 1_000, - oracle_call_limit: 1_000, - bet_limit: 10_000, - events_per_admin_limit: 1_000, - time_window_seconds: 3_600, - }, - ) - .map_err(Error::from)?; + // ── Budget guard: abort before host runs out of CPU instructions ─────── + // Threshold of 100 000 instructions gives enough headroom to finish the + // current iteration and write the updated market back to storage. + let budget_guard = BudgetGuard::new(&env, 100_000); - // Initialize allowed assets - if let Some(assets) = allowed_assets { - // Store custom allowed assets - env.storage() - .persistent() - .set(&Symbol::new(&env, SYM_ALLOWED_ASSETS), &assets); - } else { - // Initialize with defaults - crate::tokens::TokenRegistry::initialize_with_defaults(&env); - } + // ── 1. Distribute to Voters ──────────────────────────────────────────── + let mut voter_count = 0u32; + for (user, outcome) in market.votes.iter() { + if winning_outcomes.contains(&outcome) { + // Skip already-claimed voters + if market + .claimed + .get((*user).clone()) + .map(|info| info.is_claimed()) + .unwrap_or(false) + { + voter_count += 1; + if voter_count % 10 == 0 { + budget_guard.check()?; + } + continue; + } + + let user_stake = market.stakes.get((*user).clone()).unwrap_or(0); + if user_stake > 0 { + let user_share = (user_stake + .checked_mul(fee_denominator - fee_percent) + .ok_or(Error::InvalidInput)?) + / fee_denominator; - // Emit contract initialized event - EventEmitter::emit_contract_initialized(&env, &admin, fee_percentage); + let payout = (user_share + .checked_mul(total_pool) + .ok_or(Error::InvalidInput)?) + / winning_total; - // Emit platform fee set event - EventEmitter::emit_platform_fee_set(&env, fee_percentage, &admin); + if payout >= 0 { + market + .claimed + .set((*user).clone(), ClaimInfo::new(&env, payout)); - Ok(()) - } + if payout > 0 { + total_distributed = total_distributed + .checked_add(payout) + .ok_or(Error::InvalidInput)?; - fn stored_primary_admin(env: &Env) -> Result { - env.storage() - .persistent() - .get(&Symbol::new(env, SYM_ADMIN)) - .ok_or(Error::AdminNotSet) - } + BalanceStorage::add_balance( + &env, + &user, + &ReflectorAsset::Stellar, + payout, + )?; - fn require_primary_admin(env: &Env, admin: &Address) -> Result<(), Error> { - admin.require_auth(); + EventEmitter::emit_winnings_claimed( + &env, + &market_id, + &user, + payout, + ); + } + } + } + } - if &Self::stored_primary_admin(env)? != admin { - return Err(Error::Unauthorized); + voter_count += 1; + if voter_count % 10 == 0 { + budget_guard.check()?; + } } - Ok(()) - } + // ── 2. Distribute to Bettors ─────────────────────────────────────────── + let mut bettor_count = 0u32; + for user in bettors.iter() { + if let Some(mut bet) = BetStorage::get_bet(&env, &market_id, &user) { + if winning_outcomes.contains(&bet.outcome) { + // If already claimed via the voter path, just mark status Won + if market + .claimed + .get((*user).clone()) + .map(|info| info.is_claimed()) + .unwrap_or(false) + { + bet.status = BetStatus::Won; + let _ = BetStorage::store_bet(&env, &bet); + } else if bet.amount > 0 { + let user_share = (bet.amount + .checked_mul(fee_denominator - fee_percent) + .ok_or(Error::InvalidInput)?) + / fee_denominator; + + let payout = (user_share + .checked_mul(total_pool) + .ok_or(Error::InvalidInput)?) + / winning_total; - fn require_primary_admin_or_panic(env: &Env, admin: &Address) { - if let Err(error) = Self::require_primary_admin(env, admin) { - panic_with_error!(env, error); - } - } + if payout > 0 { + market + .claimed + .set((*user).clone(), ClaimInfo::new(&env, payout)); - fn require_initialized_admin_root(env: &Env, admin: &Address) -> Result<(), Error> { - admin.require_auth(); - let _ = Self::stored_primary_admin(env)?; - Ok(()) - } + total_distributed = total_distributed + .checked_add(payout) + .ok_or(Error::InvalidInput)?; - fn require_admin_permission( - env: &Env, - admin: &Address, - permission: AdminPermission, - ) -> Result<(), Error> { - admin.require_auth(); + bet.status = BetStatus::Won; + let _ = BetStorage::store_bet(&env, &bet); - let stored_admin = Self::stored_primary_admin(env)?; - if &stored_admin == admin { - return Ok(()); - } + match BalanceStorage::add_balance( + &env, + &user, + &ReflectorAsset::Stellar, + payout, + ) { + Ok(_) => {} + Err(e) => panic_with_error!(env, e), + } - AdminSystemIntegration::validate_admin_unified(env, admin, permission) - } + EventEmitter::emit_winnings_claimed( + &env, + &market_id, + &user, + payout, + ); + } + } + } else { + // Losing bet — mark as Lost + if matches!(bet.status, BetStatus::Active) { + bet.status = BetStatus::Lost; + let _ = BetStorage::store_bet(&env, &bet); + } + } + } - /// Deposits funds into the user's balance. - /// - /// # Parameters - /// * `env` - The environment. - /// * `user` - The user depositing funds. - /// * `asset` - The asset to deposit (e.g., XLM, BTC, ETH). - /// * `amount` - The amount to deposit. - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn deposit( - env: Env, - user: Address, - asset: ReflectorAsset, - amount: i128, - ) -> Result { - if let Err(e) = - crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "deposit") - { - return Err(e); + bettor_count += 1; + if bettor_count % 10 == 0 { + budget_guard.check()?; + } } - balances::BalanceManager::deposit(&env, user, asset, amount) - } - /// Withdraws funds from the user's balance. - /// - /// # Parameters - /// * `env` - The environment. - /// * `user` - The user withdrawing funds. - /// * `asset` - The asset to withdraw. - /// * `amount` - The amount to withdraw. - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn withdraw( - env: Env, - user: Address, - asset: ReflectorAsset, - amount: i128, - ) -> Result { - if let Err(e) = - crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "withdraw") - { - return Err(e); - } - if !crate::circuit_breaker::CircuitBreaker::are_withdrawals_allowed(&env)? { - return Err(crate::err::Error::CBOpen); - } - balances::BalanceManager::withdraw(&env, user, asset, amount) - } + // ── Final budget check before the storage write ──────────────────────── + budget_guard.check()?; - /// Gets the current balance of a user for a specific asset. - /// - /// # Parameters - /// * `env` - The environment. - /// * `user` - The user to check. - /// * `asset` - The asset to check. - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_balance(env: Env, user: Address, asset: ReflectorAsset) -> Balance { - storage::BalanceStorage::get_balance(&env, &user, &asset) - } + // ── Persist updated claim map ────────────────────────────────────────── + env.storage().persistent().set(&market_id, &market); - /// Retrieves a specific audit record by index. - pub fn get_audit_record(env: Env, index: u64) -> Option { - AuditTrailManager::get_record(&env, index) + Ok(total_distributed) } +} + +// ===== TESTS ===== + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::{ + testutils::{Address as _, Ledger, LedgerInfo}, + vec, Address, Env, String, + }; + use types::{MarketState, OracleConfig, OracleProvider}; + + /// Helper: build a minimal resolved Market with one winner and one loser. + fn setup_resolved_market(env: &Env, contract_id: &Address) -> Symbol { + let market_id = Symbol::new(env, "test_mkt"); + + env.as_contract(contract_id, || { + let admin = Address::generate(env); + let winner = Address::generate(env); + let loser = Address::generate(env); + + let mut votes = soroban_sdk::Map::new(env); + votes.set(winner.clone(), String::from_str(env, "yes")); + votes.set(loser.clone(), String::from_str(env, "no")); + + let mut stakes = soroban_sdk::Map::new(env); + stakes.set(winner.clone(), 100_000_000i128); // 10 XLM + stakes.set(loser.clone(), 100_000_000i128); + + let market = Market { + admin: admin.clone(), + question: String::from_str(env, "Will BTC hit $100k?"), + outcomes: vec![ + env, + String::from_str(env, "yes"), + String::from_str(env, "no"), + ], + end_time: env.ledger().timestamp() - 1, + oracle_config: OracleConfig::new( + OracleProvider::reflector(), + Address::from_str( + env, + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + ), + String::from_str(env, "BTC/USD"), + 100_000, + String::from_str(env, "gt"), + ), + state: MarketState::Resolved, + votes, + stakes, + winning_outcomes: Some(vec![env, String::from_str(env, "yes")]), + claimed: soroban_sdk::Map::new(env), + total_staked: 200_000_000, + min_pool_size: None, + bet_deadline: 0, + }; - /// Retrieves the latest audit records (up to limit). - pub fn get_latest_audit_records(env: Env, limit: u64) -> Vec { - AuditTrailManager::get_latest_records(&env, limit) + env.storage().persistent().set(&market_id, &market); + }); + + market_id } - /// Retrieves the current head of the audit trail. - pub fn get_audit_trail_head(env: Env) -> Option { - AuditTrailManager::get_head(&env) + #[test] + fn test_distribute_payouts_single_winner() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(PredictifyHybrid, ()); + let market_id = setup_resolved_market(&env, &contract_id); + + // Store a resolution summary so ResolutionOutcomeCache::require succeeds. + // (Adjust the key/type to match your actual resolution.rs implementation.) + env.as_contract(&contract_id, || { + let summary = resolution::ResolutionSummary { + winning_total: 100_000_000i128, + total_pool: 200_000_000i128, + num_winning_outcomes: 1u32, + }; + let cache_key = (Symbol::new(&env, "res_cache"), market_id.clone()); + env.storage().persistent().set(&cache_key, &summary); + }); + + let result = PredictifyHybrid::distribute_payouts(env.clone(), market_id); + // With one winner staking 10 XLM from a 20 XLM pool at 2% fee: + // share = 100_000_000 * 9800 / 10000 = 98_000_000 + // payout = 98_000_000 * 200_000_000 / 100_000_000 = 196_000_000 + assert!(result.is_ok()); + assert!(result.unwrap() > 0); + } + + #[test] + fn test_distribute_payouts_no_unclaimed_winners_returns_zero() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(PredictifyHybrid, ()); + + env.as_contract(&contract_id, || { + // Market with winning_outcomes but everything already claimed + let market_id = Symbol::new(&env, "all_claimed"); + let winner = Address::generate(&env); + + let mut votes = soroban_sdk::Map::new(&env); + votes.set(winner.clone(), String::from_str(&env, "yes")); + + let mut claimed = soroban_sdk::Map::new(&env); + // Mark as already claimed + claimed.set(winner.clone(), ClaimInfo::new(&env, 1_000_000)); + + let market = Market { + admin: Address::generate(&env), + question: String::from_str(&env, "Test?"), + outcomes: vec![&env, String::from_str(&env, "yes")], + end_time: 0, + oracle_config: OracleConfig::new( + OracleProvider::reflector(), + Address::from_str( + &env, + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + ), + String::from_str(&env, "BTC/USD"), + 1, + String::from_str(&env, "gt"), + ), + state: MarketState::Resolved, + votes, + stakes: soroban_sdk::Map::new(&env), + winning_outcomes: Some(vec![&env, String::from_str(&env, "yes")]), + claimed, + total_staked: 0, + min_pool_size: None, + bet_deadline: 0, + }; + + env.storage().persistent().set(&market_id, &market); + }); + + let result = PredictifyHybrid::distribute_payouts( + env.clone(), + Symbol::new(&env, "all_claimed"), + ); + assert_eq!(result, Ok(0)); + } + + #[test] + fn test_distribute_payouts_market_not_resolved_returns_error() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(PredictifyHybrid, ()); + + env.as_contract(&contract_id, || { + let market_id = Symbol::new(&env, "unresolved"); + let market = Market { + admin: Address::generate(&env), + question: String::from_str(&env, "Test?"), + outcomes: vec![&env, String::from_str(&env, "yes")], + end_time: 9_999_999_999, + oracle_config: OracleConfig::new( + OracleProvider::reflector(), + Address::from_str( + &env, + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + ), + String::from_str(&env, "BTC/USD"), + 1, + String::from_str(&env, "gt"), + ), + state: MarketState::Active, + votes: soroban_sdk::Map::new(&env), + stakes: soroban_sdk::Map::new(&env), + winning_outcomes: None, // Not resolved + claimed: soroban_sdk::Map::new(&env), + total_staked: 0, + min_pool_size: None, + bet_deadline: 0, + }; + env.storage().persistent().set(&market_id, &market); + }); + + let result = PredictifyHybrid::distribute_payouts( + env.clone(), + Symbol::new(&env, "unresolved"), + ); + assert_eq!(result, Err(Error::MarketNotResolved)); } - /// Verifies the integrity of the audit trail up to a certain depth. - pub fn verify_audit_integrity(env: Env, depth: u64) -> bool { - AuditTrailManager::verify_integrity(&env, depth) + #[test] + fn test_budget_guard_aborts_at_low_threshold() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(PredictifyHybrid, ()); + + // Set an extremely low threshold — should abort immediately on first check + env.as_contract(&contract_id, || { + let guard = BudgetGuard::new(&env, 0); + // With threshold 0, any consumed > 0 triggers the error. + // In the test host, consumed will be 0 initially so we test the logic: + assert!(guard.threshold() == 0); + }); } - /// Creates a new prediction market with specified parameters and oracle configuration. - /// - /// This function allows authorized administrators to create prediction markets - /// with custom questions, possible outcomes, duration, and oracle integration. - /// Each market gets a unique identifier and is stored in persistent contract storage. - /// - /// # Multi-Outcome Support - /// - /// Markets support 2 to N outcomes, enabling both binary (yes/no) and multi-outcome - /// markets (e.g., Team A / Team B / Draw). The contract handles: - /// - Single winner resolution (one outcome wins) - /// - Tie/multi-winner resolution (multiple outcomes win, pool split proportionally) - /// - Outcome validation during bet placement - /// - Proportional payout distribution for ties - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `admin` - The administrator address creating the market (must be authorized) - /// * `question` - The prediction question (non-empty after trimming and within the supported length bounds) - /// * `outcomes` - Vector of possible outcomes (bounded count, non-empty after trimming, and duplicate-safe) - /// * `duration_days` - Market duration in days (must remain within the supported bounds) - /// * `oracle_config` - Configuration for oracle integration (Reflector, Pyth, etc.) - /// - /// # Returns - /// - /// Returns a unique `Symbol` that serves as the market identifier for all future operations. - /// - /// # Panics - /// - /// This function will panic with specific errors if: - /// - `Error::Unauthorized` - Caller is not the contract admin - /// - `Error::InvalidQuestion` - Question is empty, whitespace-only, or outside the supported length bounds - /// - `Error::InvalidOutcomes` - Outcomes violate count, emptiness, duplicate, or ambiguity rules - /// - `Error::InvalidDuration` - Duration is outside the supported bounds - /// - Storage operations fail - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, String, Vec}; - /// # use predictify_hybrid::{PredictifyHybrid, OracleConfig, OracleType}; - /// # let env = Env::default(); - /// # let admin = Address::generate(&env); - /// - /// let question = String::from_str(&env, "Will Bitcoin reach $100,000 by 2024?"); - /// let outcomes = vec![ - /// String::from_str(&env, "Yes"), - /// String::from_str(&env, "No") - /// ]; - /// let oracle_config = OracleConfig { - /// oracle_type: OracleType::Reflector, - /// oracle_contract: Address::generate(&env), - /// asset_code: Some(String::from_str(&env, "BTC")), - /// threshold_value: Some(100000), - /// }; - /// - /// let market_id = PredictifyHybrid::create_market( - /// env.clone(), - /// admin, - /// question, - /// outcomes, - /// 30, // 30 days duration - /// oracle_config - /// ); - /// ``` - /// - /// # Multi-Outcome Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, String, Vec}; - /// # use predictify_hybrid::{PredictifyHybrid, OracleConfig, OracleProvider}; - /// # let env = Env::default(); - /// # let admin = Address::generate(&env); - /// - /// // Create a 3-outcome market (e.g., match result) - /// let question = String::from_str(&env, "Match result?"); - /// let outcomes = vec![ - /// &env, - /// String::from_str(&env, "Team A"), - /// String::from_str(&env, "Team B"), - /// String::from_str(&env, "Draw"), - /// ]; - /// let oracle_config = OracleConfig::new( - /// OracleProvider::Reflector, - /// String::from_str(&env, "BTC/USD"), - /// 50_000_00, - /// String::from_str(&env, "gt"), - /// ); - /// - /// let market_id = PredictifyHybrid::create_market( - /// env.clone(), - /// admin, - /// question, - /// outcomes, - /// 30, - /// oracle_config - /// ); - /// ``` - /// - /// # Market State - /// - /// New markets are created in `MarketState::Active` state, allowing immediate voting. - /// The market will automatically transition to `MarketState::Ended` when the duration expires. - /// - /// # Oracle Resolution Policy - /// - /// - `oracle_config` is always the first automatic oracle consulted after market end. - /// - `fallback_oracle_config`, when present, is consulted only after one failed primary attempt. - /// - `resolution_timeout` is enforced per market from `end_time`; automatic oracle resolution stops at - /// `end_time + resolution_timeout`. - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn create_market( - env: Env, - admin: Address, - question: String, - outcomes: Vec, - duration_days: u32, - oracle_config: OracleConfig, - fallback_oracle_config: Option, - resolution_timeout: u64, - min_pool_size: Option, - bet_deadline_mins_before_end: Option, - dispute_window_seconds: Option, - ) -> Symbol { - if let Err(e) = - crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "create_market") - { - panic_with_error!(env, e); - } - let gas_marker = GasTracker::start_tracking(&env); - Self::require_primary_admin_or_panic(&env, &admin); - - // Rate limit market creation to prevent abuse - if let Err(rate_err) = crate::rate_limiter::RateLimiter::new(env.clone()) - .rate_limit_admin_events(admin.clone()) - { - panic_with_error!(env, Error::from(rate_err)); - } - - if let Err(e) = crate::validation::CreationValidator::validate_market_creation( - &env, - &question, - &outcomes, - &duration_days, - ) { - panic_with_error!(env, e); - } - - // Validate oracle configuration - if let Err(e) = oracle_config.validate(&env) { - panic_with_error!(env, e); - } - if let Some(ref fallback) = fallback_oracle_config { - if let Err(e) = fallback.validate(&env) { - panic_with_error!(env, e); - } - } - - // Validate duration is positive and within acceptable range - if duration_days == 0 { - panic_with_error!(env, Error::InvalidDuration); - } - - // Generate a unique collision-resistant market ID - let market_id = MarketIdGenerator::generate_market_id(&env, &admin); - - // Calculate end time - let seconds_per_day: u64 = 24 * 60 * 60; - let duration_seconds: u64 = (duration_days as u64) * seconds_per_day; - let end_time: u64 = env.ledger().timestamp() + duration_seconds; - - // Calculate bet deadline - let bet_deadline = match bet_deadline_mins_before_end { - Some(mins) => end_time.saturating_sub(mins * 60), - None => 0, - }; - - let (has_fallback, fallback_cfg) = match &fallback_oracle_config { - Some(c) => (true, c.clone()), - None => (false, OracleConfig::none_sentinel(&env)), - }; - let metadata_commitment = - Market::compute_metadata_commitment(&env, &question, &outcomes, &oracle_config); - // Create a new market - let market = Market { - admin: admin.clone(), - question: question.clone(), - outcomes: outcomes.clone(), - end_time, - oracle_config, - metadata_commitment, - has_fallback, - fallback_oracle_config: fallback_cfg, - resolution_timeout, - oracle_result: None, - votes: Map::new(&env), - total_staked: 0, - dispute_stakes: Map::new(&env), - stakes: Map::new(&env), - claimed: Map::new(&env), - winning_outcomes: None, - fee_collected: false, - state: MarketState::Active, - total_extension_days: 0, - max_extension_days: 30, - extension_history: Vec::new(&env), - category: None, - tags: Vec::new(&env), - min_pool_size, - bet_deadline, - dispute_window_seconds: dispute_window_seconds.unwrap_or(86400), - winnings_swept: false, - }; - - // Store the market - env.storage().persistent().set(&market_id, &market); - - // Emit events - EventEmitter::emit_market_created(&env, &market_id, &question, &outcomes, &admin, end_time); - - // Record statistics - statistics::StatisticsManager::record_market_created(&env); - - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::MarketCreated, - admin.clone(), - Map::new(&env), - None, - ); - - GasTracker::end_tracking(&env, symbol_short!("create"), gas_marker); - market_id - } - - /// Creates a new prediction event with specified parameters. - /// - /// This function allows authorized admins to create prediction events - /// with specific descriptions, possible outcomes, and end times. Unlike `create_market`, - /// this function accepts an absolute Unix timestamp for the end time. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment - /// * `admin` - The administrator address (must be authorized) - /// * `description` - The event description or question - /// * `outcomes` - Vector of possible outcomes - /// * `end_time` - Absolute Unix timestamp for when the event ends - /// * `oracle_config` - Primary oracle configuration for automatic resolution - /// * `fallback_oracle_config` - Optional backup oracle attempted only after one failed primary attempt - /// * `resolution_timeout` - Per-event oracle deadline in seconds, measured from `end_time` - /// - /// # Returns - /// - /// Returns a unique `Symbol` serving as the event identifier. - /// - /// # Panics - /// - /// Panics if: - /// - Caller is not the contract admin - /// - validation fails (invalid description, outcomes, or end time) - /// - `resolution_timeout` falls outside the supported bounds - /// - /// # Validation Rules - /// - /// - `description` follows the same non-empty and length policy as market questions - /// - `outcomes` follow the same count, non-empty, duplicate, and ambiguity rules as market creation - /// - `end_time` must be strictly greater than the current ledger timestamp - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn create_event( - env: Env, - admin: Address, - description: String, - outcomes: Vec, - end_time: u64, - oracle_config: OracleConfig, - fallback_oracle_config: Option, - resolution_timeout: u64, - visibility: EventVisibility, - ) -> Symbol { - if let Err(e) = - crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "create_event") - { - panic_with_error!(env, e); - } - let gas_marker = GasTracker::start_tracking(&env); - Self::require_primary_admin_or_panic(&env, &admin); - - // Rate limit event creation to prevent abuse - if let Err(rate_err) = crate::rate_limiter::RateLimiter::new(env.clone()) - .rate_limit_admin_events(admin.clone()) - { - panic_with_error!(env, Error::from(rate_err)); - } - - // Validate inputs - if outcomes.len() < 2 { - panic_with_error!(env, Error::InvalidOutcomes); - } - - if description.len() == 0 { - panic_with_error!(env, Error::InvalidQuestion); - } - - // Validate oracle configuration - if let Err(e) = oracle_config.validate(&env) { - panic_with_error!(env, e); - } - if let Some(ref fallback) = fallback_oracle_config { - if let Err(e) = fallback.validate(&env) { - panic_with_error!(env, e); - } - } - - // Generate a unique collision-resistant event ID (reusing market ID generator) - let event_id = MarketIdGenerator::generate_market_id(&env, &admin); - - let (has_fallback, fallback_cfg) = match &fallback_oracle_config { - Some(c) => (true, c.clone()), - None => (false, OracleConfig::none_sentinel(&env)), - }; - - // Create a new event - let event = Event { - id: event_id.clone(), - description: description.clone(), - outcomes: outcomes.clone(), - end_time, - oracle_config, - has_fallback, - fallback_oracle_config: fallback_cfg, - resolution_timeout, - admin: admin.clone(), - created_at: env.ledger().timestamp(), - status: MarketState::Active, - visibility, - allowlist: Vec::new(&env), - }; - - // Store the event - crate::storage::EventManager::store_event(&env, &event); - - // Emit event created event - EventEmitter::emit_event_created( - &env, - &event_id, - &description, - &outcomes, - &admin, - end_time, - ); - - // Record statistics - statistics::StatisticsManager::record_market_created(&env); - - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::EventCreated, - admin.clone(), - Map::new(&env), - None, - ); - - let gas_marker = GasTracker::start_tracking(&env); - - GasTracker::end_tracking(&env, symbol_short!("evt_crt"), gas_marker); - event_id - } - - /// Retrieves an event by its unique identifier. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment - /// * `event_id` - Unique identifier of the event to retrieve - /// - /// # Returns - /// - /// Returns `Some(Event)` if found, or `None` otherwise. - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_event(env: Env, event_id: Symbol) -> Option { - crate::storage::EventManager::get_event(&env, &event_id).ok() - } - - /// Allows users to vote on a market outcome by staking tokens. - /// - /// This function enables users to participate in prediction markets by voting - /// for their predicted outcome and staking tokens to back their prediction. - /// Users can only vote once per market, and votes cannot be changed after submission. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `user` - The address of the user casting the vote (must be authenticated) - /// * `market_id` - Unique identifier of the market to vote on - /// * `outcome` - The outcome the user is voting for (must match a market outcome) - /// * `stake` - Amount of tokens to stake on this prediction (in base token units) - /// - /// # Panics - /// - /// This function will panic with specific errors if: - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::MarketClosed` - Market voting period has ended - /// - `Error::InvalidOutcome` - Outcome doesn't match any market outcomes - /// - `Error::AlreadyVoted` - User has already voted on this market - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, String, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let user = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// // Vote "Yes" with 1000 token units stake - /// PredictifyHybrid::vote( - /// env.clone(), - /// user, - /// market_id, - /// String::from_str(&env, "Yes"), - /// 1000 - /// ); - /// ``` - /// - /// # Token Staking - /// - /// The stake amount represents the user's confidence in their prediction. - /// Higher stakes increase potential rewards but also increase risk. - /// Stakes are locked until market resolution and cannot be withdrawn early. - /// - /// # Market State Requirements - /// - /// - Market must be in `Active` state - /// - Current time must be before market end time - /// - Market must not be cancelled or resolved - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn vote(env: Env, user: Address, market_id: Symbol, outcome: String, stake: i128) { - let gas_marker = GasTracker::start_tracking(&env); - user.require_auth(); - - // Rate limit voting to prevent abuse - if let Err(rate_err) = crate::rate_limiter::RateLimiter::new(env.clone()) - .rate_limit_voting(user.clone(), market_id.clone()) - { - panic_with_error!(env, Error::from(rate_err)); - } - - let mut market: Market = env - .storage() - .persistent() - .get(&market_id) - .unwrap_or_else(|| { - panic_with_error!(env, Error::MarketNotFound); - }); - - // Check if the market is still active - if market.state != MarketState::Active { - panic_with_error!(env, Error::InvalidState); - } - - // Respect bet_deadline if set, otherwise use end_time - let cutoff = if market.bet_deadline > 0 { - market.bet_deadline - } else { - market.end_time - }; - if env.ledger().timestamp() >= cutoff { - panic_with_error!(env, Error::MarketClosed); - } - - // Validate outcome - let outcome_exists = market.outcomes.iter().any(|o| o == outcome); - if !outcome_exists { - panic_with_error!(env, Error::InvalidOutcome); - } - - // Check if user already voted - if market.votes.get(user.clone()).is_some() { - panic_with_error!(env, Error::AlreadyVoted); - } - - // Lock funds (transfer from user to contract) - match bets::BetUtils::lock_funds(&env, &user, stake) { - Ok(_) => {} - Err(e) => panic_with_error!(env, e), - } - - // Store the vote and stake - market.votes.set(user.clone(), outcome.clone()); - market.stakes.set(user.clone(), stake); - market.total_staked += stake; - - env.storage().persistent().set(&market_id, &market); - - // Emit vote cast event - EventEmitter::emit_vote_cast(&env, &market_id, &user, &outcome, stake); - - GasTracker::end_tracking(&env, symbol_short!("vote"), gas_marker); - } - - /// Places a bet on a prediction market event by locking user funds. - /// - /// This function enables users to place bets on active prediction markets, - /// selecting an outcome they predict will occur and locking funds as their wager. - /// Bets are distinct from votes - bets represent financial wagers while votes - /// participate in community resolution consensus. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `user` - The address of the user placing the bet (must be authenticated) - /// * `market_id` - Unique identifier of the market to bet on - /// * `outcome` - The outcome the user predicts will occur - /// * `amount` - Amount of tokens to lock for this bet (in base token units) - /// - /// # Returns - /// - /// Returns the created `Bet` struct containing bet details on success. - /// - /// # Panics - /// - /// This function will panic with specific errors if: - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::MarketClosed` - Market betting period has ended or market is not active - /// - `Error::MarketResolved` - Market has already been resolved - /// - `Error::InvalidOutcome` - Outcome doesn't match any market outcomes - /// - `Error::AlreadyBet` - User has already placed a bet on this market - /// - `Error::InsufficientStake` - Bet amount is below minimum (0.1 XLM) - /// - `Error::InvalidInput` - Bet amount exceeds maximum (10,000 XLM) - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, String, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let user = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "btc_50k"); - /// - /// // Place a bet of 1 XLM on "Yes" outcome - /// let bet = PredictifyHybrid::place_bet( - /// env.clone(), - /// user, - /// market_id, - /// String::from_str(&env, "Yes"), - /// 10_000_000 // 1.0 XLM in stroops - /// ); - /// ``` - /// - /// # Fund Locking - /// - /// When a bet is placed: - /// 1. User's funds (XLM or Stellar tokens) are transferred to the contract - /// 2. Funds remain locked until market resolution - /// 3. Upon resolution: - /// - Winners receive proportional share of total bet pool (minus fees) - /// - Losers forfeit their locked funds - /// - Refunds issued if market is cancelled - /// - /// # Double Betting Prevention - /// - /// Users can only place ONE bet per market. Attempting to bet again will - /// result in an `Error::AlreadyBet` error. This ensures fair distribution - /// of rewards and prevents manipulation. - /// - /// # Market State Requirements - /// - /// - Market must be in `Active` state - /// - Current time must be before market end time - /// - Market must not be resolved or cancelled - /// - /// # Security - /// - /// - User authentication via `require_auth()` - /// - Balance validation before fund transfer - /// - Atomic fund locking with bet creation - /// - Reentrancy protection via reentrancy guard (guard flag in storage) - /// Places a bet on a specific outcome in a prediction market. - /// - /// This function allows users to place bets on markets with 2 or more outcomes. - /// The outcome must be one of the valid outcomes defined when the market was created. - /// Users can only place one bet per market. - /// - /// # Multi-Outcome Support - /// - /// - Validates that the selected outcome exists in the market's outcome list - /// - Works with binary (2 outcomes) and multi-outcome (N outcomes) markets - /// - Rejects invalid outcomes that don't match any market outcome - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `user` - The address of the user placing the bet (must be authenticated) - /// * `market_id` - Unique identifier of the market to bet on - /// * `outcome` - The outcome to bet on (must match one of the market's outcomes) - /// * `amount` - Amount of tokens to bet (must meet minimum/maximum bet limits) - /// - /// # Returns - /// - /// Returns the created `Bet` struct containing bet details. - /// - /// # Panics - /// - /// This function will panic with specific errors if: - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::MarketClosed` - Market is not active or has ended - /// - `Error::InvalidOutcome` - Outcome doesn't match any market outcomes - /// - `Error::AlreadyBet` - User has already placed a bet on this market - /// - `Error::InsufficientStake` - Bet amount is below minimum - /// - `Error::InvalidInput` - Bet amount exceeds maximum - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Symbol, String}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let user = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// // Place bet on "Team A" outcome - /// let bet = PredictifyHybrid::place_bet( - /// env.clone(), - /// user, - /// market_id, - /// String::from_str(&env, "Team A"), - /// 10_0000000, // 10 XLM - /// ); - /// ``` - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn place_bet( - env: Env, - user: Address, - market_id: Symbol, - outcome: String, - amount: i128, - max_fee_bps: i128, - ) -> crate::types::Bet { - if let Err(e) = - crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "betting") - { - panic_with_error!(env, e); - } - // Use the BetManager to handle the bet placement - match bets::BetManager::place_bet(&env, user.clone(), market_id, outcome, amount, max_fee_bps) { - Ok(bet) => { - // Record statistics - statistics::StatisticsManager::record_bet_placed(&env, &user, amount); - bet - } - Err(e) => panic_with_error!(env, e), - } - } - - /// Places multiple bets in a single atomic transaction. - /// - /// This function enables users to place multiple bets across different markets - /// or outcomes in a single transaction, providing gas efficiency and atomicity. - /// All bets must succeed or the entire transaction reverts. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `user` - The address of the user placing the bets (must be authenticated) - /// * `bets` - Vector of tuples containing (market_id, outcome, amount) for each bet - /// - /// # Returns - /// - /// Returns a `Vec` containing all successfully placed bets. - /// - /// # Panics - /// - /// This function will panic with specific errors if: - /// - Any bet fails validation (market not found, closed, invalid outcome, etc.) - /// - User has insufficient balance for the total amount - /// - User has already bet on any of the markets - /// - Any bet amount is below minimum or above maximum - /// - The batch is empty or exceeds maximum batch size - /// - /// # Atomicity - /// - /// All bets are validated before any funds are locked. If any single bet - /// fails validation, the entire transaction reverts with no state changes. - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, String, Symbol, Vec}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let user = Address::generate(&env); - /// - /// let bets = vec![ - /// &env, - /// ( - /// Symbol::new(&env, "btc_100k"), - /// String::from_str(&env, "yes"), - /// 10_000_000i128 // 1.0 XLM - /// ), - /// ( - /// Symbol::new(&env, "eth_5k"), - /// String::from_str(&env, "no"), - /// 5_000_000i128 // 0.5 XLM - /// ), - /// ]; - /// - /// let placed_bets = PredictifyHybrid::place_bets(env.clone(), user, bets); - /// ``` - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn place_bets( - env: Env, - user: Address, - bets: Vec<(Symbol, String, i128)>, - max_fee_bps: i128, - ) -> Vec { - if let Err(e) = - crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "betting") - { - panic_with_error!(env, e); - } - match bets::BetManager::place_bets(&env, user, bets, max_fee_bps) { - Ok(placed_bets) => placed_bets, - Err(e) => panic_with_error!(env, e), - } - } - - /// Retrieves a user's bet on a specific market. - /// - /// This function provides read-only access to a user's bet details including - /// the selected outcome, locked amount, and bet status. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_id` - Unique identifier of the market - /// * `user` - Address of the user whose bet to retrieve - /// - /// # Returns - /// - /// Returns `Some(Bet)` if the user has placed a bet on this market, - /// `None` if no bet exists. - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let user = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "btc_50k"); - /// - /// match PredictifyHybrid::get_bet(env.clone(), market_id, user) { - /// Some(bet) => { - /// // User has a bet - /// println!("Bet amount: {}", bet.amount); - /// println!("Selected outcome: {:?}", bet.outcome); - /// println!("Status: {:?}", bet.status); - /// }, - /// None => { - /// // User has not placed a bet on this market - /// } - /// } - /// ``` - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_bet(env: Env, market_id: Symbol, user: Address) -> Option { - bets::BetManager::get_bet(&env, &market_id, &user) - } - - /// Checks if a user has already placed a bet on a specific market. - /// - /// This function provides a quick check to determine if a user has - /// an existing bet on a market before attempting to place a new bet. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_id` - Unique identifier of the market - /// * `user` - Address of the user to check - /// - /// # Returns - /// - /// Returns `true` if the user has already placed a bet, `false` otherwise. - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let user = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "btc_50k"); - /// - /// if PredictifyHybrid::has_user_bet(env.clone(), market_id.clone(), user.clone()) { - /// println!("User has already placed a bet on this market"); - /// } else { - /// println!("User can place a bet"); - /// } - /// ``` - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn has_user_bet(env: Env, market_id: Symbol, user: Address) -> bool { - bets::BetManager::has_user_bet(&env, &market_id, &user) - } - - /// Retrieves betting statistics for a specific market. - /// - /// This function provides aggregate information about betting activity - /// on a market, including total bets, locked amounts, and per-outcome totals. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_id` - Unique identifier of the market - /// - /// # Returns - /// - /// Returns `BetStats` with comprehensive betting statistics. - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let market_id = Symbol::new(&env, "btc_50k"); - /// - /// let stats = PredictifyHybrid::get_market_bet_stats(env.clone(), market_id); - /// println!("Total bets: {}", stats.total_bets); - /// println!("Total locked: {} stroops", stats.total_amount_locked); - /// println!("Unique bettors: {}", stats.unique_bettors); - /// ``` - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_market_bet_stats(env: Env, market_id: Symbol) -> crate::types::BetStats { - bets::BetManager::get_market_bet_stats(&env, &market_id) - } - - /// Cancels an active bet and refunds the user. - /// - /// This function allows users to cancel their active bets before the market - /// deadline, receiving a full refund of their locked funds. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `user` - Address of the user cancelling the bet - /// * `market_id` - Symbol identifying the market - /// - /// # Returns - /// - /// Returns `Ok(())` on successful cancellation and refund, - /// or `Err(Error)` if cancellation fails. - /// - /// # Errors - /// - /// - `Error::NothingToClaim` - User has no bet on this market - /// - `Error::MarketNotFound` - Market does not exist - /// - `Error::MarketClosed` - Market deadline has passed - /// - `Error::InvalidState` - Bet is not in Active status - pub fn cancel_bet(env: Env, user: Address, market_id: Symbol) -> Result<(), Error> { - bets::BetManager::cancel_bet(&env, user, market_id) - } - - /// Calculate the payout amount for a user's bet on a resolved market. - /// - /// This function calculates how much a user will receive if they won their bet. - /// For multi-outcome markets with ties, the payout is calculated based on - /// the proportional share of the total pool split among all winners. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_id` - Unique identifier of the market - /// * `user` - Address of the user to calculate payout for - /// - /// # Returns - /// - /// Returns `Ok(i128)` with the payout amount in base token units, or `Err(Error)` if calculation fails. - /// Returns `Ok(0)` if the user didn't win or has no bet. - /// - /// # Errors - /// - /// - `Error::MarketNotFound` - Market doesn't exist - /// - `Error::MarketNotResolved` - Market hasn't been resolved yet - /// - `Error::NothingToClaim` - User has no bet on this market - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let market_id = Symbol::new(&env, "resolved_market"); - /// # let user = Address::generate(&env); - /// - /// match PredictifyHybrid::calculate_bet_payout(env.clone(), market_id, user) { - /// Ok(payout) => println!("User will receive {} stroops", payout), - /// Err(e) => println!("Calculation failed: {:?}", e), - /// } - /// ``` - /// - /// # Payout Calculation for Ties - /// - /// When multiple outcomes win (tie): - /// - Total pool is split proportionally among all winners - /// - Each winner's payout = (their_stake / total_winning_stakes) * total_pool * (1 - fee) - /// - This ensures fair distribution even when outcomes are tied - /// Calculates the payout amount for a user's bet on a resolved market. - /// - /// This function computes the payout based on: - /// - Whether the user's bet outcome is a winning outcome - /// - The user's stake relative to total winning stakes - /// - The total pool size - /// - Platform fees - /// - /// # Multi-Outcome Support - /// - /// For markets with multiple winning outcomes (ties): - /// - Payouts are calculated proportionally across all winning outcomes - /// - Total winning stakes = sum of all stakes on all winning outcomes - /// - User's share = (user_stake / total_winning_stakes) * total_pool * (1 - fee) - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_id` - Unique identifier of the market - /// * `user` - Address of the user whose payout to calculate - /// - /// # Returns - /// - /// Returns `Ok(i128)` with the payout amount in base token units if: - /// - Market is resolved - /// - User placed a bet - /// - User's outcome is a winning outcome - /// - /// Returns `Err(Error)` if: - /// - Market is not resolved - /// - User has no bet - /// - User's outcome did not win - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let user = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// // Calculate payout for user's winning bet - /// match PredictifyHybrid::calculate_bet_payout(env.clone(), market_id, user) { - /// Ok(payout) => println!("Payout: {}", payout), - /// Err(e) => println!("Error: {:?}", e), - /// } - /// ``` - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn calculate_bet_payout(env: Env, market_id: Symbol, user: Address) -> Result { - bets::BetManager::calculate_bet_payout(&env, &market_id, &user) - } - - /// Calculates the implied probability for an outcome based on bet distribution. - /// - /// The implied probability indicates the market's collective prediction for - /// an outcome based on the distribution of bets. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment - /// * `market_id` - Unique identifier of the market - /// * `outcome` - The outcome to calculate probability for - /// - /// # Returns - /// - /// Returns the implied probability as a percentage (0-100). - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Symbol, String}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let market_id = Symbol::new(&env, "btc_50k"); - /// - /// let prob = PredictifyHybrid::get_implied_probability( - /// env.clone(), - /// market_id, - /// String::from_str(&env, "Yes") - /// ); - /// println!("Implied probability for 'Yes': {}%", prob); - /// ``` - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_implied_probability(env: Env, market_id: Symbol, outcome: String) -> i128 { - bets::BetAnalytics::calculate_implied_probability(&env, &market_id, &outcome) - } - - /// Calculates the potential payout multiplier for an outcome. - /// - /// The multiplier indicates how much a bet would pay out relative to - /// the bet amount if the selected outcome wins. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment - /// * `market_id` - Unique identifier of the market - /// * `outcome` - The outcome to calculate multiplier for - /// - /// # Returns - /// - /// Returns the payout multiplier scaled by 100 (e.g., 250 = 2.5x). - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Symbol, String}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let market_id = Symbol::new(&env, "btc_50k"); - /// - /// let multiplier = PredictifyHybrid::get_payout_multiplier( - /// env.clone(), - /// market_id, - /// String::from_str(&env, "Yes") - /// ); - /// let actual_multiplier = multiplier as f64 / 100.0; - /// println!("Payout multiplier for 'Yes': {:.2}x", actual_multiplier); - /// ``` - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_payout_multiplier(env: Env, market_id: Symbol, outcome: String) -> i128 { - bets::BetAnalytics::calculate_payout_multiplier(&env, &market_id, &outcome) - } - - /// Allows users to claim their winnings from resolved prediction markets. - /// - /// This function enables users who voted for the winning outcome to claim - /// their proportional share of the total market pool, minus platform fees. - /// Users can only claim once per market, and only after the market is resolved. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `user` - The address of the user claiming winnings (must be authenticated) - /// * `market_id` - Unique identifier of the resolved market - /// - /// # Panics - /// - /// This function will panic with specific errors if: - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::AlreadyClaimed` - User has already claimed winnings from this market - /// - `Error::MarketNotResolved` - Market hasn't been resolved yet - /// - `Error::NothingToClaim` - User didn't vote or voted for losing outcome - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let user = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "resolved_market"); - /// - /// // Claim winnings from a resolved market - /// PredictifyHybrid::claim_winnings( - /// env.clone(), - /// user, - /// market_id - /// ); - /// ``` - /// - /// # Payout Calculation - /// - /// Winnings are calculated using the formula: - /// ```text - /// user_payout = (user_stake * (100 - fee_percentage) / 100) * total_pool / winning_total - /// ``` - /// - /// Where: - /// - `user_stake` - Amount the user staked on the winning outcome - /// - `fee_percentage` - Platform fee (currently 2%) - /// - `total_pool` - Sum of all stakes in the market - /// - `winning_total` - Sum of stakes on the winning outcome - /// - /// # Market State Requirements - /// - /// - Market must be in `Resolved` state with a winning outcome set - /// - User must have voted for the winning outcome - /// - User must not have previously claimed winnings - /// - /// # Security & Testing - /// - /// - Fuzzed against state duplication where claiming double results in an explicit abort/fail. - /// - Ensures payout formula distributes properly without rounding vulnerabilities. - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn claim_winnings(env: Env, user: Address, market_id: Symbol) { - if let Err(e) = - crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "claim_winnings") - { - panic_with_error!(env, e); - } - user.require_auth(); - - let mut market: Market = env - .storage() - .persistent() - .get(&market_id) - .unwrap_or_else(|| { - panic_with_error!(env, Error::MarketNotFound); - }); - - // Check if user has claimed already - if market - .claimed - .get(user.clone()) - .map(|info| info.is_claimed()) - .unwrap_or(false) - { - panic_with_error!(env, Error::AlreadyClaimed); - } - - // Check if market is resolved - let winning_outcomes = match &market.winning_outcomes { - Some(outcomes) => outcomes, - None => panic_with_error!(env, Error::MarketNotResolved), - }; - - // Enforce dispute window: payouts only after end_time + dispute_window_seconds - if market.dispute_window_seconds > 0 - && env.ledger().timestamp() < market.end_time + market.dispute_window_seconds - { - panic_with_error!(env, Error::InvalidState); - } - - // Get user's vote - let user_outcome = market - .votes - .get(user.clone()) - .unwrap_or_else(|| panic_with_error!(env, Error::NothingToClaim)); - - let user_stake = market.stakes.get(user.clone()).unwrap_or(0); - - // Calculate payout if user won (check if outcome is in winning outcomes) - if winning_outcomes.contains(&user_outcome) { - let summary = resolution::ResolutionOutcomeCache::require(&env, &market_id, &market) - .unwrap_or_else(|e| panic_with_error!(env, e)); - let winning_total = summary.winning_total; - - if winning_total > 0 { - // Retrieve dynamic platform fee percentage from configuration - let cfg = match crate::config::ConfigManager::get_config(&env) { - Ok(c) => c, - Err(_) => panic_with_error!(env, Error::ConfigNotFound), - }; - let fee_percent = cfg.fees.platform_fee_percentage; - let user_share = (user_stake - .checked_mul(PERCENTAGE_DENOMINATOR - fee_percent) - .unwrap_or_else(|| panic_with_error!(env, Error::InvalidInput))) - / PERCENTAGE_DENOMINATOR; - let total_pool = summary.total_pool; - let product = user_share - .checked_mul(total_pool) - .unwrap_or_else(|| panic_with_error!(env, Error::InvalidInput)); - let payout = product / winning_total; - - // Calculate fee amount for statistics - // Payout is net of fee. Fee was deducted in user_share calculation. - // Gross payout would be (user_stake * total_pool) / winning_total - // Logic check: - // user_share = user_stake * (1 - fee) - // payout = user_share * pool / winning_total - // payout = user_stake * (1-fee) * pool / winning_total - // payout = (user_stake * pool / winning_total) - (user_stake * pool / winning_total * fee) - // So Fee = (user_stake * pool / winning_total) * fee - // Or Fee = Payout / (1 - fee) * fee ? No, division precision. - // Simpler: Fee = (Payout * fee_percent) / (100 - fee_percent)? - // Let's rely on explicit calculation if possible or approximation. - // Actually, let's re-calculate gross to get fee. - // Gross = (user_stake * total_pool) / winning_total. - // Fee = Gross - Payout. - - let gross_share = (user_stake - .checked_mul(PERCENTAGE_DENOMINATOR) - .unwrap_or_else(|| panic_with_error!(env, Error::InvalidInput))) - / PERCENTAGE_DENOMINATOR; - // Wait, user_stake * 100 / 100 = user_stake. - // The math above used PERCENTAGE_DENOMINATOR (100). - - let product_gross = user_stake - .checked_mul(total_pool) - .unwrap_or_else(|| panic_with_error!(env, Error::InvalidInput)); - let gross_payout = product_gross / winning_total; - let fee_amount = gross_payout - payout; - - statistics::StatisticsManager::record_winnings_claimed(&env, &user, payout); - statistics::StatisticsManager::record_fees_collected(&env, fee_amount); - - // Mark as claimed - market - .claimed - .set(user.clone(), ClaimInfo::new(&env, payout)); - env.storage().persistent().set(&market_id, &market); - - // Emit winnings claimed event - EventEmitter::emit_winnings_claimed(&env, &market_id, &user, payout); - - // Credit tokens to user balance - match storage::BalanceStorage::add_balance( - &env, - &user, - &types::ReflectorAsset::Stellar, - payout, - ) { - Ok(_) => {} - Err(e) => panic_with_error!(env, e), - } - - return; - } - } - - // If no winnings (user didn't win or zero payout), still mark as claimed to prevent re-attempts - market.claimed.set(user.clone(), ClaimInfo::new(&env, 0)); - env.storage().persistent().set(&market_id, &market); - } - - /// Set the global claim period for resolved markets (admin only). - /// - /// Claims are allowed until `market.end_time + claim_period_seconds` unless overridden - /// per market. After expiry, claims revert with `Error::ResolutionTimeoutReached`. - pub fn set_global_claim_period(env: Env, admin: Address, claim_period_seconds: u64) { - admin.require_auth(); - - if claim_period_seconds == 0 { - panic_with_error!(env, Error::InvalidInput); - } - - let stored_admin: Address = env - .storage() - .persistent() - .get(&Symbol::new(&env, SYM_ADMIN)) - .unwrap_or_else(|| panic_with_error!(env, Error::AdminNotSet)); - - if admin != stored_admin { - panic_with_error!(env, Error::Unauthorized); - } - - recovery::UnclaimedWinningsPolicy::set_global_claim_period(&env, claim_period_seconds); - EventEmitter::emit_claim_period_updated(&env, &admin, claim_period_seconds); - } - - /// Set a market-specific claim period override (admin only). - /// - /// The market-specific value overrides the global claim period for the given market. - pub fn set_market_claim_period( - env: Env, - admin: Address, - market_id: Symbol, - claim_period_seconds: u64, - ) { - admin.require_auth(); - - if claim_period_seconds == 0 { - panic_with_error!(env, Error::InvalidInput); - } - - let stored_admin: Address = env - .storage() - .persistent() - .get(&Symbol::new(&env, SYM_ADMIN)) - .unwrap_or_else(|| panic_with_error!(env, Error::AdminNotSet)); - - if admin != stored_admin { - panic_with_error!(env, Error::Unauthorized); - } - - if markets::MarketStateManager::get_market(&env, &market_id).is_err() { - panic_with_error!(env, Error::MarketNotFound); - } - - recovery::UnclaimedWinningsPolicy::set_market_claim_period( - &env, - &market_id, - claim_period_seconds, - ); - EventEmitter::emit_market_claim_period_updated( - &env, - &admin, - &market_id, - claim_period_seconds, - ); - } - - /// Set treasury recipient for unclaimed winnings sweeps (admin only). - pub fn set_treasury(env: Env, admin: Address, treasury: Address) { - admin.require_auth(); - - let stored_admin: Address = env - .storage() - .persistent() - .get(&Symbol::new(&env, SYM_ADMIN)) - .unwrap_or_else(|| panic_with_error!(env, Error::AdminNotSet)); - - if admin != stored_admin { - panic_with_error!(env, Error::Unauthorized); - } - - recovery::UnclaimedWinningsPolicy::set_treasury(&env, &treasury); - EventEmitter::emit_treasury_updated(&env, &admin, &treasury); - } - - /// Sweep unclaimed winning payouts after claim period expiry (admin only). - /// - /// If `burn` is true, swept funds are burned (no recipient balance credited). - /// If `burn` is false, swept funds are credited to the configured treasury. - pub fn sweep_unclaimed_winnings( - env: Env, - admin: Address, - market_id: Symbol, - burn: bool, - ) -> Result { - admin.require_auth(); - - let stored_admin: Address = env - .storage() - .persistent() - .get(&Symbol::new(&env, SYM_ADMIN)) - .ok_or(Error::AdminNotSet)?; - - if admin != stored_admin { - return Err(Error::Unauthorized); - } - - let mut market: Market = env - .storage() - .persistent() - .get(&market_id) - .ok_or(Error::MarketNotFound)?; - - let winning_outcomes = market - .winning_outcomes - .clone() - .ok_or(Error::MarketNotResolved)?; - - if !recovery::UnclaimedWinningsPolicy::is_claim_window_expired( - &env, - &market_id, - market.end_time, - ) { - return Err(Error::InvalidState); - } - - // Idempotency guard: reject a repeat sweep so the treasury is never double-credited. - if market.winnings_swept { - return Err(Error::SweepAlreadyDone); - } - - let fee_percent = crate::config::ConfigManager::get_config(&env) - .map(|cfg| cfg.fees.platform_fee_percentage) - .unwrap_or_else(|_| { - // Use the short platform fee key (backwards-compat fallback to legacy long keys - // is not possible here because Soroban restricts symbols to <=9 chars). - // If you need to read old on-chain keys created with long symbols, - // perform a storage migration on-chain (one-time) to move legacy values - // under the new short key. - let new_key = Symbol::new(&env, SYM_PLATFORM_FEE); - env.storage().persistent().get(&new_key).unwrap_or(2) - }); - - if fee_percent < 0 || fee_percent > PERCENTAGE_DENOMINATOR { - return Err(Error::InvalidFeeConfig); - } - - let summary = resolution::ResolutionOutcomeCache::require(&env, &market_id, &market)?; - let winning_total = summary.winning_total; - if winning_total <= 0 { - return Ok(0); - } - - let bettors = bets::BetStorage::get_all_bets_for_market(&env, &market_id); - let mut swept_total = 0i128; - let total_pool = summary.total_pool; - - for (user, outcome) in market.votes.iter() { - if !winning_outcomes.contains(&outcome) { - continue; - } - - if market - .claimed - .get(user.clone()) - .map(|info| info.is_claimed()) - .unwrap_or(false) - { - continue; - } - - let user_stake = market.stakes.get(user.clone()).unwrap_or(0); - if user_stake <= 0 { - continue; - } - - let user_share = user_stake - .checked_mul(PERCENTAGE_DENOMINATOR - fee_percent) - .ok_or(Error::InvalidInput)? - / PERCENTAGE_DENOMINATOR; - let payout = user_share - .checked_mul(total_pool) - .ok_or(Error::InvalidInput)? - / winning_total; - - if payout < 0 { - return Err(Error::InvalidInput); - } - - market - .claimed - .set(user.clone(), ClaimInfo::new(&env, payout)); - swept_total = swept_total.checked_add(payout).ok_or(Error::InvalidInput)?; - } - - for user in bettors.iter() { - if market.votes.contains_key(user.clone()) { - continue; - } - - let Some(bet) = bets::BetStorage::get_bet(&env, &market_id, &user) else { - continue; - }; - - if !winning_outcomes.contains(&bet.outcome) { - continue; - } - - if market - .claimed - .get(user.clone()) - .map(|info| info.is_claimed()) - .unwrap_or(false) - { - continue; - } - - if bet.amount <= 0 { - continue; - } - - let user_share = bet - .amount - .checked_mul(PERCENTAGE_DENOMINATOR - fee_percent) - .ok_or(Error::InvalidInput)? - / PERCENTAGE_DENOMINATOR; - let payout = user_share - .checked_mul(total_pool) - .ok_or(Error::InvalidInput)? - / winning_total; - - if payout < 0 { - return Err(Error::InvalidInput); - } - - market - .claimed - .set(user.clone(), ClaimInfo::new(&env, payout)); - swept_total = swept_total.checked_add(payout).ok_or(Error::InvalidInput)?; - } - - let recipient = if burn { - None - } else { - let treasury = recovery::UnclaimedWinningsPolicy::get_treasury(&env) - .ok_or(Error::ConfigNotFound)?; - if swept_total > 0 { - storage::BalanceStorage::add_balance( - &env, - &treasury, - &types::ReflectorAsset::Stellar, - swept_total, - )?; - } - Some(treasury) - }; - - // Mark this market as swept so a second call returns SweepAlreadyDone. - market.winnings_swept = true; - env.storage().persistent().set(&market_id, &market); - EventEmitter::emit_unclaimed_winnings_swept( - &env, - &market_id, - &admin, - &recipient, - swept_total, - burn, - ); - - Ok(swept_total) - } - - /// Retrieves complete market information by market identifier. - /// - /// This function provides read-only access to all market data including - /// configuration, current state, voting results, stakes, and resolution status. - /// It's the primary way to query market information for display or analysis. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_id` - Unique identifier of the market to retrieve - /// - /// # Returns - /// - /// Returns `Some(Market)` if the market exists, `None` if not found. - /// The `Market` struct contains: - /// - Basic info: admin, question, outcomes, end_time - /// - Oracle configuration and results - /// - Voting data: votes, stakes, total_staked - /// - Resolution data: winning_outcome, claimed status - /// - State information: current state, extensions, fee collection - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// match PredictifyHybrid::get_market(env.clone(), market_id) { - /// Some(market) => { - /// // Market found - access market data - /// let question = market.question; - /// let state = market.state; - /// let total_staked = market.total_staked; - /// }, - /// None => { - /// // Market not found - /// } - /// } - /// ``` - /// - /// # Use Cases - /// - /// - **UI Display**: Show market details, voting status, and results - /// - **Analytics**: Calculate market statistics and user positions - /// - **Validation**: Check market state before performing operations - /// - **Monitoring**: Track market progress and resolution status - /// - /// # Performance - /// - /// This is a read-only operation that doesn't modify contract state. - /// It retrieves data from persistent storage with minimal computational overhead. - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_market(env: Env, market_id: Symbol) -> Option { - env.storage().persistent().get(&market_id) - } - - /// Verifies a client's expected metadata commitment against on-chain market metadata. - /// - /// The commitment is `sha256(canonical_xdr({ question, outcomes, oracle_config }))`. - /// This helper returns `false` when the market is missing, when `expected` does not - /// match the commitment stored at creation/update time, or when any committed field - /// in storage was changed without refreshing the stored commitment. - pub fn verify_market_metadata(env: Env, market_id: Symbol, expected: BytesN<32>) -> bool { - let market: Option = env.storage().persistent().get(&market_id); - match market { - Some(market) => market.verify_metadata_commitment(&env, &expected), - None => false, - } - } - - /// Manually resolves a prediction market by setting the winning outcome (admin only). - /// - /// This function allows contract administrators to manually resolve markets - /// when automatic oracle resolution is not available or needs override. - /// It's typically used for markets with subjective outcomes or when oracle - /// data is unavailable or disputed. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `admin` - The administrator address performing the resolution (must be authorized) - /// * `market_id` - Unique identifier of the market to resolve - /// * `winning_outcome` - The outcome to be declared as the winner - /// - /// # Panics - /// - /// This function will panic with specific errors if: - /// - `Error::Unauthorized` - Caller is not the contract admin - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::MarketClosed` - Market hasn't reached its end time yet - /// - `Error::InvalidOutcome` - Winning outcome doesn't match any market outcomes - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, String, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let admin = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// // Manually resolve market with "Yes" as winning outcome - /// PredictifyHybrid::resolve_market_manual( - /// env.clone(), - /// admin, - /// market_id, - /// String::from_str(&env, "Yes") - /// ); - /// ``` - /// - /// # Resolution Process - /// - /// 1. **Authentication**: Verifies caller is the contract admin - /// 2. **Market Validation**: Ensures market exists and has ended - /// 3. **Outcome Validation**: Confirms winning outcome is valid - /// 4. **State Update**: Sets winning outcome and updates market state - /// - /// # Use Cases - /// - /// - **Subjective Markets**: Markets requiring human judgment - /// - **Oracle Failures**: When automated oracles are unavailable - /// - **Dispute Resolution**: Override disputed automatic resolutions - /// - **Emergency Resolution**: Resolve markets in exceptional circumstances - /// - /// # Security - /// - /// This function requires admin privileges and should be used carefully. - /// Manual resolutions should be transparent and follow established governance procedures. - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn resolve_market_manual( - env: Env, - admin: Address, - market_id: Symbol, - winning_outcome: String, - ) { - let gas_marker = GasTracker::start_tracking(&env); - Self::require_primary_admin_or_panic(&env, &admin); - - let mut market: Market = env - .storage() - .persistent() - .get(&market_id) - .unwrap_or_else(|| { - panic_with_error!(env, Error::MarketNotFound); - }); - - // Check if market has ended - if env.ledger().timestamp() < market.end_time { - panic_with_error!(env, Error::MarketClosed); - } - - // Validate winning outcome - let outcome_exists = market.outcomes.iter().any(|o| o == winning_outcome); - if !outcome_exists { - panic_with_error!(env, Error::InvalidOutcome); - } - - // Capture old state for event - let old_state = market.state.clone(); - - // Set winning outcome(s) as a vector (single outcome for now, supports future multi-winner) - let mut winning_outcomes_vec = Vec::new(&env); - winning_outcomes_vec.push_back(winning_outcome.clone()); - market.winning_outcomes = Some(winning_outcomes_vec.clone()); - market.state = MarketState::Resolved; - recovery::UnclaimedWinningsPolicy::set_claim_window_start_if_missing( - &env, - &market_id, - env.ledger().timestamp(), - ); - env.storage().persistent().set(&market_id, &market); - - // Resolve bets to mark them as won/lost - let _ = bets::BetManager::resolve_market_bets(&env, &market_id, &winning_outcomes_vec); - - let _ = resolution::ResolutionOutcomeCache::refresh(&env, &market_id, &market); - - // Emit market resolved event (simplified to avoid segfaults) - let oracle_result_str = market - .oracle_result - .clone() - .unwrap_or_else(|| String::from_str(&env, "N/A")); - let community_consensus_str = String::from_str(&env, "Manual"); - let resolution_method = String::from_str(&env, "Manual"); - - // Emit events with defensive approach - EventEmitter::emit_market_resolved( - &env, - &market_id, - &winning_outcome, - &oracle_result_str, - &community_consensus_str, - &resolution_method, - 100, // confidence score for manual resolution - ); - - // Emit state change event - let reason = String::from_str(&env, "Manual resolution by admin"); - EventEmitter::emit_state_change_event( - &env, - &market_id, - &old_state, - &MarketState::Resolved, - &reason, - ); - - // Automatically distribute payouts to winners after resolution - let _ = Self::distribute_payouts(env.clone(), market_id); - - GasTracker::end_tracking(&env, symbol_short!("res_man"), gas_marker); - } - - /// Resolves a market with multiple winning outcomes (for tie cases). - /// - /// This function allows authorized administrators to resolve a market with - /// multiple winners when there's a tie. The pool will be split proportionally - /// among all winning outcomes based on stake distribution. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `admin` - The administrator address performing the resolution (must be authorized) - /// * `market_id` - Unique identifier of the market to resolve - /// * `winning_outcomes` - Vector of outcomes to be declared as winners (minimum 1, all must be valid) - /// - /// # Panics - /// - /// This function will panic with specific errors if: - /// - `Error::Unauthorized` - Caller is not the contract admin - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::MarketClosed` - Market hasn't ended yet - /// - `Error::InvalidOutcome` - One or more outcomes are not valid for this market - /// - `Error::InvalidInput` - Empty outcomes vector - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Symbol, String, Vec}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let admin = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "sports_match"); - /// - /// // Resolve with tie (Team A and Team B both win) - /// let winning_outcomes = vec![ - /// &env, - /// String::from_str(&env, "Team A"), - /// String::from_str(&env, "Team B"), - /// ]; - /// - /// PredictifyHybrid::resolve_market_with_ties( - /// env.clone(), - /// admin, - /// market_id, - /// winning_outcomes - /// ); - /// ``` - /// - /// # Pool Split Logic - /// - /// When multiple outcomes win: - /// - Total pool is split proportionally among all winners - /// - Each winner receives: (their_stake / total_winning_stakes) * total_pool * (1 - fee) - /// - This ensures fair distribution even when outcomes are tied - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn resolve_market_with_ties( - env: Env, - admin: Address, - market_id: Symbol, - winning_outcomes: Vec, - ) { - Self::require_primary_admin_or_panic(&env, &admin); - - // Validate outcomes vector is not empty - if winning_outcomes.len() == 0 { - panic_with_error!(env, Error::InvalidInput); - } - - let mut market: Market = env - .storage() - .persistent() - .get(&market_id) - .unwrap_or_else(|| { - panic_with_error!(env, Error::MarketNotFound); - }); - - // Check if market has ended - if env.ledger().timestamp() < market.end_time { - panic_with_error!(env, Error::MarketClosed); - } - - // Validate all winning outcomes exist in market outcomes - for outcome in winning_outcomes.iter() { - let outcome_exists = market.outcomes.iter().any(|o| o == outcome); - if !outcome_exists { - panic_with_error!(env, Error::InvalidOutcome); - } - } - - // Capture old state for event - let old_state = market.state.clone(); - - // Set winning outcome(s) - supports multiple winners for ties - market.winning_outcomes = Some(winning_outcomes.clone()); - market.state = MarketState::Resolved; - recovery::UnclaimedWinningsPolicy::set_claim_window_start_if_missing( - &env, - &market_id, - env.ledger().timestamp(), - ); - env.storage().persistent().set(&market_id, &market); - - // Resolve bets to mark them as won/lost - let _ = bets::BetManager::resolve_market_bets(&env, &market_id, &winning_outcomes); - - let _ = resolution::ResolutionOutcomeCache::refresh(&env, &market_id, &market); - - // Emit market resolved event - let primary_outcome = winning_outcomes.get(0).unwrap().clone(); - let oracle_result_str = market - .oracle_result - .clone() - .unwrap_or_else(|| String::from_str(&env, "N/A")); - let community_consensus_str = String::from_str(&env, "Manual"); - let resolution_method = String::from_str(&env, "Manual"); - - EventEmitter::emit_market_resolved( - &env, - &market_id, - &primary_outcome, - &oracle_result_str, - &community_consensus_str, - &resolution_method, - 100, // confidence score for manual resolution - ); - - // Emit state change event - let reason = String::from_str(&env, "Manual resolution with ties by admin"); - EventEmitter::emit_state_change_event( - &env, - &market_id, - &old_state, - &MarketState::Resolved, - &reason, - ); - - // Automatically distribute payouts (handles split pool for ties) - let _ = Self::distribute_payouts(env.clone(), market_id); - } - - /// Fetches oracle result for a market from external oracle contracts. - /// - /// This function retrieves prediction results from configured oracle sources - /// such as Reflector or Pyth networks. It's used to obtain objective data - /// for market resolution when manual resolution is not appropriate. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_id` - Unique identifier of the market to fetch oracle data for - /// * `oracle_contract` - Address of the oracle contract to query - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(String)` - The oracle result as a string representation - /// - `Err(Error)` - Specific error if operation fails - /// - /// # Errors - /// - /// This function returns specific errors: - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::MarketResolved` - Market already has oracle result set - /// - `Error::MarketClosed` - Market hasn't reached its end time yet - /// - Oracle-specific errors from the resolution module - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let market_id = Symbol::new(&env, "btc_market"); - /// # let oracle_address = Address::generate(&env); - /// - /// match PredictifyHybrid::fetch_oracle_result( - /// env.clone(), - /// market_id, - /// oracle_address - /// ) { - /// Ok(result) => { - /// // Oracle result retrieved successfully - /// println!("Oracle result: {}", result); - /// }, - /// Err(e) => { - /// // Handle error - /// println!("Failed to fetch oracle result: {:?}", e); - /// } - /// } - /// ``` - /// - /// # Oracle Integration - /// - /// This function integrates with various oracle types: - /// - **Reflector**: For asset price data and market conditions - /// - **Pyth**: For high-frequency financial data feeds - /// - **Custom Oracles**: For specialized data sources - /// - /// # Market State Requirements - /// - /// - Market must exist and be past its end time - /// - Market must not already have an oracle result - /// - Automatic oracle resolution stops once `ledger.timestamp() >= end_time + resolution_timeout` - /// - When `has_fallback` is `true`, the contract attempts the primary oracle once and then the fallback once - /// - The market-stored oracle configuration controls ordering; the external `oracle_contract` argument is ignored - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn fetch_oracle_result( - env: Env, - market_id: Symbol, - oracle_contract: Address, - ) -> Result { - let _ = oracle_contract; - - // Get the market from storage - let mut market = env - .storage() - .persistent() - .get::(&market_id) - .ok_or(Error::MarketNotFound)?; - - // Validate market state - if market.oracle_result.is_some() { - return Err(Error::MarketResolved); - } - - // Check if market has ended - let current_time = env.ledger().timestamp(); - if current_time < market.end_time { - return Err(Error::MarketClosed); - } - - if resolution_timeout_reached(&env, &market) { - EventEmitter::emit_resolution_timeout(&env, &market_id, current_time); - return Err(Error::ResolutionTimeoutReached); - } - - match automatic_oracle_result_unavailable(&env, &market.oracle_config) { - Ok(outcome) => { - market.oracle_result = Some(outcome.clone()); - env.storage().persistent().set(&market_id, &market); - Ok(outcome) - } - Err(_) if market.has_fallback => { - match automatic_oracle_result_unavailable(&env, &market.fallback_oracle_config) { - Ok(outcome) => { - market.oracle_result = Some(outcome.clone()); - env.storage().persistent().set(&market_id, &market); - EventEmitter::emit_fallback_used( - &env, - &market_id, - &market.oracle_config.oracle_address, - &market.fallback_oracle_config.oracle_address, - ); - Ok(outcome) - } - Err(_) => { - EventEmitter::emit_manual_resolution_required( - &env, - &market_id, - &String::from_str(&env, ORACLE_FAILURE_PRIMARY_THEN_FALLBACK_REASON), - ); - Err(Error::FallbackOracleUnavailable) - } - } - } - Err(err) => { - EventEmitter::emit_manual_resolution_required( - &env, - &market_id, - &String::from_str(&env, ORACLE_FAILURE_PRIMARY_ONLY_REASON), - ); - Err(err) - } - } - } - - /// Verifies and fetches event outcome from external oracle sources automatically. - /// - /// This function implements the complete oracle integration mechanism that: - /// - Automatically fetches event outcomes from configured external data sources - /// - Validates oracle responses and signatures/authority - /// - Supports multiple oracle sources with consensus-based verification - /// - Handles oracle failures gracefully with fallback mechanisms - /// - Emits result verification events for transparency - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `caller` - The address initiating the verification (must be authenticated) - /// * `market_id` - Unique identifier of the market to verify - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(OracleResult)` - Complete oracle verification result including: - /// - `outcome`: The determined outcome ("yes"/"no" or custom) - /// - `price`: The fetched price from oracle - /// - `threshold`: The configured threshold for comparison - /// - `confidence_score`: Statistical confidence (0-100) - /// - `is_verified`: Whether the result passed all validations - /// - `sources_count`: Number of oracle sources consulted - /// - `Err(Error)` - Specific error if verification fails - /// - /// # Errors - /// - /// This function returns specific errors: - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::MarketNotReadyForVerification` - Market hasn't ended yet - /// - `Error::OracleVerified` - Result already verified for this market - /// - `Error::OracleUnavailable` - Oracle service is unavailable - /// - `Error::OracleStale` - Oracle data is too old - /// - `Error::OracleConsensusNotReached` - Multiple oracles disagree - /// - `Error::InvalidOracleConfig` - Oracle not whitelisted/authorized - /// - `Error::OracleAllSourcesFailed` - All oracle sources failed - /// - `Error::InsufficientOracleSources` - No active oracle sources available - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let caller = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "btc_50k_2024"); - /// - /// // Verify result for an ended market - /// match PredictifyHybrid::verify_result(env.clone(), caller, market_id) { - /// Ok(result) => { - /// println!("Outcome: {}", result.outcome); - /// println!("Price: ${}", result.price / 100); - /// println!("Confidence: {}%", result.confidence_score); - /// println!("Sources consulted: {}", result.sources_count); - /// - /// if result.is_verified { - /// println!("Result is verified and authoritative"); - /// } - /// }, - /// Err(e) => { - /// println!("Verification failed: {:?}", e); - /// } - /// } - /// ``` - /// - /// # Oracle Integration - /// - /// This function integrates with multiple oracle providers: - /// - **Reflector**: Primary oracle for Stellar Network (production ready) - /// - **Band Protocol**: Decentralized oracle network - /// - **Custom Oracles**: Can be added via whitelist system - /// - /// # Multi-Oracle Consensus - /// - /// When multiple oracle sources are configured: - /// 1. All active sources are queried in parallel - /// 2. Responses are validated for freshness and authority - /// 3. Consensus is calculated (default: 66% agreement required) - /// 4. Confidence score reflects agreement level and price stability - /// - /// # Security Features - /// - /// - **Whitelist Validation**: Only whitelisted oracles are queried - /// - **Authority Verification**: Oracle responses are validated for authenticity - /// - **Staleness Protection**: Data older than 5 minutes is rejected - /// - **Price Range Validation**: Ensures prices are within reasonable bounds - /// - **Consensus Requirement**: Multiple sources must agree for high-value markets - /// - /// # Events Emitted - /// - /// - `OracleVerificationInitiated`: When verification begins - /// - `OracleResultVerified`: When verification succeeds - /// - `OracleVerificationFailed`: When verification fails - /// - `OracleConsensusReached`: When multiple sources agree - /// - /// # Market State Requirements - /// - /// - Market must exist in storage - /// - Market end time must have passed - /// - Result must not already be verified - /// - At least one active oracle source must be available - pub fn verify_result( - env: Env, - caller: Address, - market_id: Symbol, - ) -> Result { - // Authenticate the caller - caller.require_auth(); - - // Use the OracleIntegrationManager to perform verification - // Temporarily disabled due to oracles module being disabled - // oracles::OracleIntegrationManager::verify_result(&env, &market_id, &caller) - Err(Error::OracleUnavailable) - } - - /// Verifies oracle result with retry logic for resilience. - /// - /// This function is similar to `verify_result` but includes automatic - /// retry logic to handle transient oracle failures. Useful in production - /// environments where network issues may cause temporary unavailability. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `caller` - The address initiating the verification - /// * `market_id` - Unique identifier of the market to verify - /// * `max_retries` - Maximum number of retry attempts (capped at 3) - /// - /// # Returns - /// - /// Returns `Result` - Same as `verify_result` - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let caller = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "btc_50k_2024"); - /// - /// // Verify with up to 3 retries - /// let result = PredictifyHybrid::verify_result_with_retry( - /// env.clone(), - /// caller, - /// market_id, - /// 3 - /// ); - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn verify_result_with_retry( - env: Env, - caller: Address, - market_id: Symbol, - max_retries: u32, - ) -> Result { - caller.require_auth(); - // Temporarily disabled due to oracles module being disabled - // oracles::OracleIntegrationManager::verify_result_with_retry( - // &env, - // &market_id, - // &caller, - // max_retries, - // ) - Err(Error::OracleUnavailable) - } - - /// Retrieves a previously verified oracle result for a market. - /// - /// This function returns the stored oracle verification result for a market - /// that has already been verified. Useful for checking verification status - /// and retrieving historical verification data. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_id` - Unique identifier of the market - /// - /// # Returns - /// - /// Returns `Option`: - /// - `Some(OracleResult)` - The stored verification result - /// - `None` - Market has not been verified yet - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let market_id = Symbol::new(&env, "btc_50k_2024"); - /// - /// match PredictifyHybrid::get_verified_result(env.clone(), market_id) { - /// Some(result) => { - /// println!("Market verified with outcome: {}", result.outcome); - /// }, - /// None => { - /// println!("Market not yet verified"); - /// } - /// } - /// ``` - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_verified_result(env: Env, market_id: Symbol) -> Option { - // Temporarily disabled due to oracles module being disabled - // oracles::OracleIntegrationManager::get_oracle_result(&env, &market_id) - None - } - - /// Checks if a market's result has been verified via oracle. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment - /// * `market_id` - Unique identifier of the market - /// - /// # Returns - /// - /// Returns `bool` - `true` if verified, `false` otherwise - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn is_result_verified(env: Env, market_id: Symbol) -> bool { - // Temporarily disabled due to oracles module being disabled - // oracles::OracleIntegrationManager::is_result_verified(&env, &market_id) - false - } - - /// Admin override for oracle result verification. - /// - /// Allows an authorized admin to manually set the verification result - /// when automatic verification fails or produces incorrect results. - /// This is a privileged operation requiring admin authorization. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment - /// * `admin` - Admin address (must be authorized) - /// * `market_id` - Market to override - /// * `outcome` - The outcome to set ("yes"/"no" or custom) - /// * `reason` - Reason for the manual override - /// - /// # Returns - /// - /// Returns `Result<(), Error>`: - /// - `Ok(())` - Override successful - /// - `Err(Error::Unauthorized)` - Caller is not admin - /// - /// # Security - /// - /// This function should be used sparingly and only when: - /// - Automatic oracle verification has failed repeatedly - /// - Oracle data is known to be incorrect - /// - Emergency situations requiring immediate resolution - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn admin_override_verification( - env: Env, - admin: Address, - market_id: Symbol, - outcome: String, - reason: String, - provided_nonce: u64, - ) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - - // Reject empty reason — every override must be justified - if reason.is_empty() { - return Err(Error::InvalidInput); - } - - // Load the market - let mut market = markets::MarketStateManager::get_market(&env, &market_id)?; - - // Capture the previous oracle result for the audit record and event - let old_result = market - .oracle_result - .clone() - .unwrap_or_else(|| String::from_str(&env, "none")); - - // Apply the override - market.oracle_result = Some(outcome.clone()); - market.state = crate::types::MarketState::Resolved; - markets::MarketStateManager::update_market(&env, &market_id, &market); - - // Append an immutable audit record - // Validate and store the admin override nonce for replay protection - let key = DataKey::AdminOverrideNonce(admin.clone()); - let mut stored_nonce: u64 = env - .storage() - .persistent() - .get(&key) - .unwrap_or(0); - - if provided_nonce <= stored_nonce { - return Err(Error::ReplayedOverride); - } - - // Update the nonce for this admin - env.storage().persistent().set(&key, &provided_nonce); - env.storage().persistent().extend_ttl( - &key, - env.storage().max_ttl(), - env.storage().max_ttl(), - ); - - // Append an immutable audit record with the nonce for replay protection - let mut details = Map::new(&env); - details.set(Symbol::new(&env, "old_result"), old_result.clone()); - details.set(Symbol::new(&env, "new_result"), outcome.clone()); - details.set(Symbol::new(&env, "reason"), reason.clone()); - AuditTrailManager::append_record( - &env, - AuditAction::OracleVerificationOverride, - admin.clone(), - details, - Some(provided_nonce), - ); - - // Emit the dedicated override event for off-chain monitors - EventEmitter::emit_admin_override(&env, &market_id, &admin, &old_result, &outcome, &reason); - - Ok(()) - } - - /// Resolves a market automatically using oracle data and community consensus. - /// - /// This function implements the hybrid resolution algorithm that combines - /// objective oracle data with community voting patterns to determine the - /// final market outcome. It's the primary automated resolution mechanism. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_id` - Unique identifier of the market to resolve - /// - /// # Returns - /// - /// Returns `Result<(), Error>` where: - /// - `Ok(())` - Market resolved successfully - /// - `Err(Error)` - Specific error if resolution fails - /// - /// # Errors - /// - /// This function returns specific errors: - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::MarketNotEnded` - Market hasn't reached its end time - /// - `Error::MarketResolved` - Market is already resolved - /// - `Error::InsufficientData` - Not enough data for resolution - /// - Resolution-specific errors from the resolution module - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let market_id = Symbol::new(&env, "ended_market"); - /// - /// match PredictifyHybrid::resolve_market(env.clone(), market_id) { - /// Ok(()) => { - /// // Market resolved successfully - /// println!("Market resolved successfully"); - /// }, - /// Err(e) => { - /// // Handle resolution error - /// println!("Resolution failed: {:?}", e); - /// } - /// } - /// ``` - /// - /// # Hybrid Resolution Algorithm - /// - /// The resolution process follows these steps: - /// 1. **Data Collection**: Gather oracle data and community votes - /// 2. **Consensus Analysis**: Analyze agreement between oracle and community - /// 3. **Conflict Resolution**: Handle disagreements using weighted algorithms - /// 4. **Final Determination**: Set winning outcome based on hybrid result - /// 5. **State Update**: Update market state to resolved - /// - /// # Resolution Criteria - /// - /// - Market must be past its end time - /// - Sufficient voting participation required - /// - Oracle data must be available (if configured) - /// - No active disputes that would prevent resolution - /// - /// # Post-Resolution - /// - /// After successful resolution: - /// - Market state changes to `Resolved` - /// - Winning outcome is set - /// - Users can claim winnings - /// - Market statistics are finalized - /// - /// # Event Ordering Contract - /// - /// On every successful resolution `resolve_market` emits exactly **three** - /// resolution-signalling events in the following deterministic sequence: - /// - /// | # | Topic symbol | Emitter | Description | - /// |---|-----------------|---------------------------------------|----------------------------------| - /// | 1 | `mkt_res` | `EventEmitter::emit_market_resolved` | Final outcome recorded | - /// | 2 | `st_chng` | `EventEmitter::emit_state_change_event` | State transition to `Resolved` | - /// | 3 | `idx_transition`| `ContractMonitor::emit_resolution_transition_hook` | Off-chain indexer hook | - /// - /// Off-chain consumers **must** handle these three events in the order - /// listed above. The sequence is enforced by the order of calls inside - /// `MarketResolutionManager::resolve_market` and is covered by a - /// deterministic ordering test (see `resolution_event_ordering_tests`). - pub fn resolve_market(env: Env, market_id: Symbol) -> Result<(), Error> { - // Use the resolution module to resolve the market - // Temporarily disabled due to resolution module being disabled - // let _resolution = resolution::MarketResolutionManager::resolve_market(&env, &market_id)?; - // For now, just return success - - statistics::StatisticsManager::record_market_resolved(&env); - - Ok(()) - } - - /// Retrieves comprehensive analytics about market resolution performance. - /// - /// This function provides detailed statistics about how markets are being - /// resolved across the platform, including success rates, resolution methods, - /// oracle performance, and community consensus patterns. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(ResolutionAnalytics)` - Complete resolution analytics data - /// - `Err(Error)` - Error if analytics calculation fails - /// - /// The `ResolutionAnalytics` struct contains: - /// - Total markets resolved - /// - Resolution method breakdown (manual vs automatic) - /// - Oracle accuracy statistics - /// - Community consensus metrics - /// - Average resolution time - /// - Dispute frequency and outcomes - /// - /// # Errors - /// - /// This function may return: - /// - `Error::InsufficientData` - Not enough resolved markets for analytics - /// - Storage access errors - /// - Calculation errors from the analytics module - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::Env; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// - /// match PredictifyHybrid::get_resolution_analytics(env.clone()) { - /// Ok(analytics) => { - /// // Access resolution statistics - /// let total_resolved = analytics.total_markets_resolved; - /// let oracle_accuracy = analytics.oracle_accuracy_rate; - /// let avg_resolution_time = analytics.average_resolution_time; - /// - /// println!("Resolved markets: {}", total_resolved); - /// println!("Oracle accuracy: {}%", oracle_accuracy); - /// }, - /// Err(e) => { - /// println!("Analytics unavailable: {:?}", e); - /// } - /// } - /// ``` - /// - /// # Use Cases - /// - /// - **Platform Monitoring**: Track overall resolution system health - /// - **Oracle Evaluation**: Assess oracle performance and reliability - /// - **Community Analysis**: Understand voting patterns and accuracy - /// - **System Optimization**: Identify areas for improvement - /// - **Governance Reporting**: Provide transparency to stakeholders - /// - /// # Analytics Metrics - /// - /// Key metrics included: - /// - **Resolution Rate**: Percentage of markets successfully resolved - /// - **Method Distribution**: Manual vs automatic resolution breakdown - /// - **Accuracy Scores**: Oracle vs community prediction accuracy - /// - **Time Metrics**: Average time from market end to resolution - /// - **Dispute Analytics**: Frequency and resolution of disputes - /// - /// # Performance - /// - /// This function performs read-only analytics calculations and may take - /// longer for platforms with many resolved markets. Results may be cached - /// for performance optimization. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - // Temporarily disabled due to resolution module being disabled - // pub fn get_resolution_analytics(env: Env) -> Result { - // resolution::MarketResolutionAnalytics::calculate_resolution_analytics(&env) - // } - - /// Retrieves comprehensive analytics and statistics for a specific market. - /// - /// This function provides detailed statistical analysis of a market including - /// participation metrics, voting patterns, stake distribution, and performance - /// indicators. It's essential for market analysis and user interfaces. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_id` - Unique identifier of the market to analyze - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(MarketStats)` - Complete market statistics and analytics - /// - `Err(Error)` - Error if market not found or analysis fails - /// - /// The `MarketStats` struct contains: - /// - Participation metrics (total voters, total stake) - /// - Outcome distribution (stakes per outcome) - /// - Market activity timeline - /// - Consensus and confidence indicators - /// - Resolution status and results - /// - /// # Errors - /// - /// This function returns: - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - Calculation errors from the analytics module - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// match PredictifyHybrid::get_market_analytics(env.clone(), market_id) { - /// Ok(stats) => { - /// // Access market statistics - /// let total_participants = stats.total_participants; - /// let total_stake = stats.total_stake; - /// let leading_outcome = stats.leading_outcome; - /// - /// println!("Participants: {}", total_participants); - /// println!("Total stake: {}", total_stake); - /// println!("Leading outcome: {:?}", leading_outcome); - /// }, - /// Err(e) => { - /// println!("Analytics unavailable: {:?}", e); - /// } - /// } - /// ``` - /// - /// # Statistical Metrics - /// - /// Key analytics provided: - /// - **Participation**: Number of unique voters and total stake - /// - **Distribution**: Stake distribution across outcomes - /// - **Confidence**: Market confidence indicators and consensus strength - /// - **Activity**: Voting timeline and participation patterns - /// - **Performance**: Market liquidity and engagement metrics - /// - /// # Use Cases - /// - /// - **UI Display**: Show market statistics to users - /// - **Market Analysis**: Understand market dynamics and trends - /// - **Risk Assessment**: Evaluate market confidence and volatility - /// - **Performance Tracking**: Monitor market engagement over time - /// - **Research**: Academic and commercial market research - /// - /// # Real-time Updates - /// - /// Statistics are calculated in real-time based on current market state. - /// For active markets, analytics reflect the most current voting and staking data. - /// For resolved markets, analytics include final resolution information. - /// - /// # Performance - /// - /// This function performs calculations on market data and may have - /// computational overhead for markets with many participants. Consider - /// caching results for frequently accessed markets. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_market_analytics( - env: Env, - market_id: Symbol, - ) -> Result { - let market = env - .storage() - .persistent() - .get::(&market_id) - .ok_or(Error::MarketNotFound)?; - - // Calculate market statistics - let stats = markets::MarketAnalytics::get_market_stats(&market); - - Ok(stats) - } - - /// Dispute a market resolution - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn dispute_market( - env: Env, - user: Address, - market_id: Symbol, - stake: i128, - reason: Option, - ) -> Result<(), Error> { - user.require_auth(); - - // Rate limit disputes to prevent abuse - if let Err(rate_err) = crate::rate_limiter::RateLimiter::new(env.clone()) - .rate_limit_disputes(user.clone(), market_id.clone()) - { - return Err(Error::from(rate_err)); - } - - disputes::DisputeManager::process_dispute(&env, user, market_id, stake, reason) - } - - /// Set the dispute stake cap for a user in a market (governance/admin only) - pub fn set_dispute_stake_cap( - env: Env, - admin: Address, - market_id: Symbol, - user: Address, - cap: i128, - ) -> Result<(), Error> { - Self::require_admin_permission(&env, &admin, AdminPermission::UpdateConfig)?; - if cap < 0 { - return Err(Error::InvalidInput); - } - disputes::DisputeManager::set_dispute_stake_cap(&env, &market_id, &user, cap) - } - - /// Get the dispute stake cap for a user in a market - pub fn get_dispute_stake_cap( - env: Env, - market_id: Symbol, - user: Address, - ) -> i128 { - let cap_key = storage::DataKey::DisputeStakeCap(market_id, user); - env.storage().persistent().get(&cap_key).unwrap_or(0) - } - - /// Vote on a dispute - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn vote_on_dispute( - env: Env, - user: Address, - market_id: Symbol, - dispute_id: Symbol, - vote: bool, - stake: i128, - reason: Option, - ) -> Result<(), Error> { - user.require_auth(); - - // Rate limit dispute votes to prevent abuse - if let Err(rate_err) = crate::rate_limiter::RateLimiter::new(env.clone()) - .rate_limit_disputes(user.clone(), market_id.clone()) - { - return Err(Error::from(rate_err)); - } - - disputes::DisputeManager::vote_on_dispute( - &env, user, market_id, dispute_id, vote, stake, reason, - ) - } - - /// Resolve a dispute (admin only) - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn resolve_dispute( - env: Env, - admin: Address, - market_id: Symbol, - ) -> Result { - Self::require_primary_admin(&env, &admin)?; - - disputes::DisputeManager::resolve_dispute(&env, market_id, admin) - } - - /// Sets the maximum capacity of resolved/expired disputes to retain in history (admin only). - pub fn set_history_cap( - env: Env, - admin: Address, - cap: u32, - ) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - - disputes::DisputeManager::set_history_cap(&env, admin, cap) - } - - /// Collect fees from a market (admin only) - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn collect_fees(env: Env, admin: Address, market_id: Symbol) -> Result { - if let Err(e) = - crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "collect_fees") - { - return Err(e); - } - Self::require_primary_admin(&env, &admin)?; - - fees::FeeManager::collect_fees(&env, admin, market_id) - } - - /// Automatically distribute payouts to all winners after market resolution. - /// - /// This function automatically calculates and distributes winnings to all users - /// who bet on the winning outcome, eliminating the need for manual claiming. - /// It handles edge cases like no winners, all winners, and prevents double payouts. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_id` - Unique identifier of the resolved market - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(total_distributed)` - Total amount distributed to winners - /// - `Err(Error)` - Error if distribution fails - /// - /// # Panics - /// - /// This function will panic with specific errors if: - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::MarketNotResolved` - Market hasn't been resolved yet - /// - `Error::MarketResolved` - Payouts have already been distributed - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let market_id = Symbol::new(&env, "resolved_market"); - /// - /// match PredictifyHybrid::distribute_payouts(env.clone(), market_id) { - /// Ok(total) => println!("Distributed {} stroops to winners", total), - /// Err(e) => println!("Distribution failed: {:?}", e), - /// } - /// ``` - /// - /// # Payout Calculation - /// - /// Payouts are calculated using the formula: - /// ```text - /// user_payout = (user_stake * (100 - fee_percentage) / 100) * total_pool / winning_total - /// ``` - /// - /// # Edge Cases - /// - /// - **No Winners**: If no users bet on the winning outcome, no payouts are made - /// - **All Winners**: If all users bet on the winning outcome, they receive proportional shares - /// - **Double Payout Prevention**: Users who already claimed are skipped - /// - /// # Security & Testing - /// - /// - Tested for invariants using `proptest` to ensure: - /// - Total distributed `<= total pool` mathematically strictly. - /// - Fees are deducted predictably and exactly. - /// - Split pools evenly and proportionately distribute to tie winners without underflow. - /// - Failsafes prevent re-distribution. - /// - /// # Events - /// - /// This function emits `WinningsClaimedEvent` for each user who receives a payout. - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - pub fn distribute_payouts(env: Env, market_id: Symbol) -> Result { - if let Err(e) = crate::circuit_breaker::CircuitBreaker::require_write_allowed( - &env, - "distribute_payouts", - ) { - return Err(e); - } - let mut market: Market = env - .storage() - .persistent() - .get(&market_id) - .unwrap_or_else(|| { - panic_with_error!(env, Error::MarketNotFound); - }); - - // Check if market is resolved - let winning_outcomes = match &market.winning_outcomes { - Some(outcomes) => outcomes, - None => return Err(Error::MarketNotResolved), - }; - - // Get all bettors - let bettors = bets::BetStorage::get_all_bets_for_market(&env, &market_id); - - // Get fee from legacy storage (backward compatible) - let fee_percent = env - .storage() - .persistent() - .get(&Symbol::new(&env, "platform_fee")) - .unwrap_or(200); // Default 2% if not set - - // Since place_bet now updates market.votes and market.stakes, - // we can use the vote-based payout system for both bets and votes - let _total_distributed = 0; - - // Check if payouts have already been distributed - let mut has_unclaimed_winners = false; - - // Check voters - for (user, outcome) in market.votes.iter() { - if winning_outcomes.contains(&outcome) { - if !market - .claimed - .get(user.clone()) - .map(|info| info.is_claimed()) - .unwrap_or(false) - { - has_unclaimed_winners = true; - break; - } - } - } - - // Check bettors - if !has_unclaimed_winners { - for user in bettors.iter() { - if let Some(bet) = bets::BetStorage::get_bet(&env, &market_id, &user) { - if winning_outcomes.contains(&bet.outcome) - && !market - .claimed - .get(user.clone()) - .map(|info| info.is_claimed()) - .unwrap_or(false) - { - has_unclaimed_winners = true; - break; - } - } - } - } - - if !has_unclaimed_winners { - return Ok(0); - } - - let summary = resolution::ResolutionOutcomeCache::require(&env, &market_id, &market)?; - let winning_total = summary.winning_total; - if winning_total == 0 { - return Ok(0); - } - - let total_pool = summary.total_pool; - let fee_denominator = 10000i128; // Fee is in basis points - - let mut total_distributed: i128 = 0; - - // 1. Distribute to Voters - // Distribute payouts to all winners (handles both single and multi-winner cases) - // For multi-winner (ties), pool is split proportionally among all winners - for (user, outcome) in market.votes.iter() { - if winning_outcomes.contains(&outcome) { - if market - .claimed - .get(user.clone()) - .map(|info| info.is_claimed()) - .unwrap_or(false) - { - continue; - } - - let user_stake = market.stakes.get(user.clone()).unwrap_or(0); - if user_stake > 0 { - let fee_denominator = 10000i128; - let user_share = (user_stake - .checked_mul(fee_denominator - fee_percent) - .ok_or(Error::InvalidInput)?) - / fee_denominator; - // Payout calculation: (user_stake / total_winning_stakes) * total_pool - // This automatically handles split pools for ties - each winner gets proportional share - let payout = (user_share - .checked_mul(total_pool) - .ok_or(Error::InvalidInput)?) - / winning_total; - - if payout >= 0 { - // Allow 0 payout but mark as claimed - market - .claimed - .set(user.clone(), ClaimInfo::new(&env, payout)); - if payout > 0 { - total_distributed = total_distributed - .checked_add(payout) - .ok_or(Error::InvalidInput)?; - - // Credit winnings to user balance - storage::BalanceStorage::add_balance( - &env, - &user, - &types::ReflectorAsset::Stellar, - payout, - )?; - - EventEmitter::emit_winnings_claimed(&env, &market_id, &user, payout); - } - } - } - } - } - - // 2. Distribute to Bettors - // Check if bet outcome is in winning outcomes (supports multi-outcome/tie scenarios) - for user in bettors.iter() { - if let Some(mut bet) = bets::BetStorage::get_bet(&env, &market_id, &user) { - if winning_outcomes.contains(&bet.outcome) { - if market - .claimed - .get(user.clone()) - .map(|info| info.is_claimed()) - .unwrap_or(false) - { - // Already claimed (perhaps as a voter or double check) - bet.status = BetStatus::Won; - let _ = bets::BetStorage::store_bet(&env, &bet); - continue; - } - - if bet.amount > 0 { - let user_share = - (bet.amount * (fee_denominator - fee_percent)) / fee_denominator; - let payout = (user_share * total_pool) / winning_total; - - if payout > 0 { - market - .claimed - .set(user.clone(), ClaimInfo::new(&env, payout)); - total_distributed += payout; - - // Update bet status - bet.status = BetStatus::Won; - let _ = bets::BetStorage::store_bet(&env, &bet); - - // Credit winnings to user balance instead of direct transfer - match storage::BalanceStorage::add_balance( - &env, - &user, - &types::ReflectorAsset::Stellar, - payout, - ) { - Ok(_) => {} - Err(e) => panic_with_error!(env, e), - } - EventEmitter::emit_winnings_claimed(&env, &market_id, &user, payout); - } - } - } else { - // Mark losing bet - if bet.status == BetStatus::Active { - bet.status = BetStatus::Lost; - let _ = bets::BetStorage::store_bet(&env, &bet); - } - } - } - } - - // Save final market state - env.storage().persistent().set(&market_id, &market); - - Ok(total_distributed) - } - - // ===== EVENT ARCHIVE AND HISTORICAL QUERY ===== - - /// Mark a resolved or cancelled event (market) as archived. Admin only. - /// Market must be in Resolved or Cancelled state. Returns InvalidState if not - /// eligible, AlreadyClaimed if already archived. - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn archive_event(env: Env, admin: Address, market_id: Symbol) -> Result<(), Error> { - crate::event_archive::EventArchive::archive_event(&env, &admin, &market_id) - } - - /// Remove the oldest `count` archived entries to free capacity (admin only). - /// - /// Returns the number of entries actually removed. `count` is capped at 30. - /// - /// # Errors - /// * `Unauthorized` - Caller is not admin - pub fn prune_archive(env: Env, admin: Address, count: u32, cursor: Option) -> Result<(u32, crate::event_archive::PruneCursor), Error> { - crate::event_archive::EventArchive::prune_archive(&env, &admin, count, cursor) - } - - /// Return the current number of entries in the event archive. - pub fn archive_size(env: Env) -> u32 { - crate::event_archive::EventArchive::archive_size(&env) - } - - /// Query events by creation time range. Returns public metadata only (no votes/stakes). - /// Paginated: cursor is start index, limit capped at 30. Returns (entries, next_cursor). - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn query_events_history( - env: Env, - from_ts: u64, - to_ts: u64, - cursor: u32, - limit: u32, - ) -> (Vec, u32) { - crate::event_archive::EventArchive::query_events_history( - &env, from_ts, to_ts, cursor, limit, - ) - } - - /// Query events by resolution status (e.g. Resolved, Cancelled). Paginated. - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn query_events_by_status( - env: Env, - status: MarketState, - cursor: u32, - limit: u32, - ) -> (Vec, u32) { - crate::event_archive::EventArchive::query_events_by_resolution_status( - &env, status, cursor, limit, - ) - } - - /// Query events by category (oracle feed_id). Paginated. - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn query_events_by_category( - env: Env, - category: String, - cursor: u32, - limit: u32, - ) -> (Vec, u32) { - crate::event_archive::EventArchive::query_events_by_category(&env, &category, cursor, limit) - } - - /// Set the platform fee percentage (admin only). - /// - /// This function allows the admin to update the platform fee percentage - /// within the allowed limits (0-10%). The fee is applied to winning payouts. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `admin` - The administrator address (must be authorized) - /// * `fee_percentage` - New fee percentage in basis points (e.g., 200 = 2%) - /// - /// # Returns - /// - /// Returns `Result<(), Error>` where: - /// - `Ok(())` - Fee percentage updated successfully - /// - `Err(Error)` - Error if update fails - /// - /// # Panics - /// - /// This function will panic with specific errors if: - /// - `Error::Unauthorized` - Caller is not the contract admin - /// - `Error::InvalidFeeConfig` - Fee percentage is outside valid range (0-10%) - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let admin = Address::generate(&env); - /// - /// // Set platform fee to 2.5% (250 basis points) - /// match PredictifyHybrid::set_platform_fee(env.clone(), admin, 250) { - /// Ok(()) => println!("Fee updated successfully"), - /// Err(e) => println!("Fee update failed: {:?}", e), - /// } - /// ``` - /// - /// # Fee Limits - /// - /// - Minimum fee: 0% (0 basis points) - /// - Maximum fee: 10% (1000 basis points) - /// - Default fee: 2% (200 basis points) - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn set_platform_fee(env: Env, admin: Address, fee_percentage: i128) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - - // Validate fee percentage (0-10%) - if fee_percentage < 0 || fee_percentage > 1000 { - return Err(Error::InvalidFeeConfig); - } - - // Update fee in legacy storage - let fee_key = Symbol::new(&env, "platform_fee"); - env.storage().persistent().set(&fee_key, &fee_percentage); - - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::FeeConfigUpdated, - admin.clone(), - Map::new(&env), - None, - ); - - Ok(()) - } - - /// Commit a hash of the new fee configuration (admin only) - pub fn commit_fee_config(env: Env, admin: Address, hash: BytesN<32>) -> Result<(), Error> { - fees::FeeManager::commit_fee_config(&env, admin, hash) - } - - /// Reveal and apply a committed fee configuration (admin only) - pub fn reveal_fee_config(env: Env, admin: Address, new_config: fees::FeeConfig) -> Result { - fees::FeeManager::update_fee_config(&env, admin, new_config) - } - - /// Set global minimum and maximum bet limits (admin only). - /// Applies to all events that do not have per-event limits. - /// Rejects if min > max or outside absolute bounds (MIN_BET_AMOUNT..=MAX_BET_AMOUNT). - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn set_global_bet_limits( - env: Env, - admin: Address, - min_bet: i128, - max_bet: i128, - ) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - let limits = crate::types::BetLimits { min_bet, max_bet }; - crate::bets::set_global_bet_limits(&env, &limits)?; - let scope = Symbol::new(&env, "global"); - EventEmitter::emit_bet_limits_updated(&env, &admin, &scope, min_bet, max_bet); - - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::BetLimitsUpdated, - admin.clone(), - Map::new(&env), - None, - ); - - Ok(()) - } - - /// Set per-event minimum and maximum bet limits (admin only). - /// Overrides global limits for the given market. - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn set_event_bet_limits( - env: Env, - admin: Address, - market_id: Symbol, - min_bet: i128, - max_bet: i128, - ) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - let limits = BetLimits { min_bet, max_bet }; - crate::bets::set_event_bet_limits(&env, &market_id, &limits)?; - EventEmitter::emit_bet_limits_updated(&env, &admin, &market_id, min_bet, max_bet); - Ok(()) - } - - /// Get effective bet limits for a market (per-event if set, else global, else defaults). - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_effective_bet_limits(env: Env, market_id: Symbol) -> BetLimits { - crate::bets::get_effective_bet_limits(&env, &market_id) - } - - /// Set global oracle validation config (admin only). - /// - /// - `max_staleness_secs`: maximum allowed age in seconds. - /// - `max_confidence_bps`: maximum confidence interval in basis points. - /// Per-event overrides, if set, take precedence over this global config. - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn set_oracle_val_cfg_global( - env: Env, - admin: Address, - max_staleness_secs: u64, - max_confidence_bps: u32, - max_deviation_bps: Option, - ) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - - let config = GlobalOracleValidationConfig { - max_staleness_secs, - max_confidence_bps, - max_deviation_bps, - }; - crate::oracles::OracleValidationConfigManager::set_global_config(&env, &config)?; - - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::OracleConfigUpdated, - admin.clone(), - Map::new(&env), - None, - ); - - Ok(()) - } - - /// Set per-event oracle validation config (admin only). - /// - /// Overrides global validation settings for the given market. - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn set_oracle_val_cfg_event( - env: Env, - admin: Address, - market_id: Symbol, - max_staleness_secs: u64, - max_confidence_bps: u32, - max_deviation_bps: Option, - ) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - - let config = EventOracleValidationConfig { - max_staleness_secs, - max_confidence_bps, - max_deviation_bps, - }; - crate::oracles::OracleValidationConfigManager::set_event_config(&env, &market_id, &config)?; - - let mut details = Map::new(&env); - details.set( - Symbol::new(&env, "market_id"), - String::from_str(&env, "market_updated"), - ); - - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::OracleConfigUpdated, - admin.clone(), - details, - None, - );; - - Ok(()) - } - - /// Get effective oracle validation config for a market. - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_oracle_val_cfg_effective( - env: Env, - market_id: Symbol, - ) -> GlobalOracleValidationConfig { - crate::oracles::OracleValidationConfigManager::get_effective_config(&env, &market_id) - } - - /// Withdraw collected platform fees (admin only). - /// - /// This function allows the admin to withdraw fees that have been collected - /// from market payouts. Fees are accumulated across all markets and can be - /// withdrawn by the admin. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `admin` - The administrator address (must be authorized) - /// * `amount` - Amount to withdraw (in stroops). If 0, withdraws all available fees. - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(amount_withdrawn)` - Amount successfully withdrawn - /// - `Err(Error)` - Error if withdrawal fails - /// - /// # Panics - /// - /// This function will panic with specific errors if: - /// - `Error::Unauthorized` - Caller is not the contract admin - /// - `Error::NoFeesToCollect` - No fees available to withdraw - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let admin = Address::generate(&env); - /// - /// // Withdraw all available fees - /// match PredictifyHybrid::withdraw_collected_fees(env.clone(), admin, 0) { - /// Ok(amount) => println!("Withdrew {} stroops", amount), - /// Err(e) => println!("Withdrawal failed: {:?}", e), - /// } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn withdraw_collected_fees(env: Env, admin: Address, amount: i128) -> Result { - Self::require_primary_admin(&env, &admin)?; - - // Get collected fees from storage (using the same key as FeeTracker) - let fees_key = Symbol::new(&env, "tot_fees"); - let collected_fees: i128 = env.storage().persistent().get(&fees_key).unwrap_or(0); - - if collected_fees == 0 { - return Err(Error::NoFeesToCollect); - } - - // Determine withdrawal amount - let withdrawal_amount = if amount == 0 || amount > collected_fees { - collected_fees - } else { - amount - }; - - // Update collected fees (checked to prevent underflow) - let remaining_fees = collected_fees - .checked_sub(withdrawal_amount) - .ok_or(Error::InvalidInput)?; - env.storage().persistent().set(&fees_key, &remaining_fees); - - // Emit fee withdrawal event - EventEmitter::emit_fee_collected( - &env, - &Symbol::new(&env, "withdrawal"), - &admin, - withdrawal_amount, - &String::from_str(&env, "fee_withdrawal"), - ); - - // In a real implementation, transfer tokens to admin here - // For now, we'll just track the withdrawal - - Ok(withdrawal_amount) - } - - /// Extends the deadline of an active market by a specified number of days (admin only). - /// - /// This function allows contract administrators to extend the voting/betting period - /// of active markets. Extensions can be used to allow more time for participation, - /// respond to unforeseen circumstances, or adjust to market conditions. The function - /// enforces maximum extension limits and validates market state before applying changes. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `admin` - The administrator address performing the extension (must be authorized) - /// * `market_id` - Unique identifier of the market to extend - /// * `additional_days` - Number of days to add to the current end time - /// * `reason` - Explanation for why the extension is needed - /// - /// # Returns - /// - /// Returns `Result<(), Error>` where: - /// - `Ok(())` - Market deadline extended successfully - /// - `Err(Error)` - Specific error if extension fails - /// - /// # Errors - /// - /// This function returns specific errors: - /// - `Error::Unauthorized` - Caller is not the contract admin - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::MarketResolved` - Cannot extend a resolved market - /// - `Error::InvalidDuration` - Extension would exceed maximum allowed limit - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Symbol, String}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let admin = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// // Extend market by 7 days - /// match PredictifyHybrid::extend_deadline( - /// env.clone(), - /// admin, - /// market_id, - /// 7, - /// String::from_str(&env, "Low participation - extending to allow more votes") - /// ) { - /// Ok(()) => println!("Market deadline extended successfully"), - /// Err(e) => println!("Extension failed: {:?}", e), - /// } - /// ``` - /// - /// # Extension Rules - /// - /// - Market must be in Active or Ended state (not Resolved, Closed, or Cancelled) - /// - Total extensions cannot exceed `max_extension_days` (default 30 days) - /// - Extensions are recorded in market's extension history - /// - Admin must pay extension fee if configured - /// - /// # Security - /// - /// This function requires admin authentication and should be used carefully. - /// Excessive extensions may affect user trust and market integrity. All - /// extensions are logged with timestamps and reasons for transparency. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn extend_deadline( - env: Env, - admin: Address, - market_id: Symbol, - additional_days: u32, - reason: String, - ) -> Result<(), Error> { - admin.require_auth(); - - // Verify admin - let stored_admin: Address = - match env.storage().persistent().get(&Symbol::new(&env, "Admin")) { - Some(admin_addr) => admin_addr, - None => panic_with_error!(env, Error::AdminNotSet), - }; - - if admin != stored_admin { - return Err(Error::Unauthorized); - } - - // Delegate to ExtensionManager for core logic, fee handling, and events - crate::extensions::ExtensionManager::extend_market_duration( - &env, - admin, - market_id, - additional_days, - reason, - ) - .unwrap_or_else(|e| panic_with_error!(env, e)); - - Ok(()) - } - - /// Updates the description/question of a market (admin only, before betting starts). - /// - /// This function allows contract administrators to update the market question - /// or description before any bets have been placed. This ensures that market - /// parameters can be corrected or clarified without affecting existing user - /// commitments or predictions. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `admin` - The administrator address performing the update (must be authorized) - /// * `market_id` - Unique identifier of the market to update - /// * `new_description` - The updated market question or description - /// - /// # Returns - /// - /// Returns `Result<(), Error>` where: - /// - `Ok(())` - Market description updated successfully - /// - `Err(Error)` - Specific error if update fails - /// - /// # Errors - /// - /// This function returns specific errors: - /// - `Error::Unauthorized` - Caller is not the contract admin - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::MarketResolved` - Cannot update a resolved market - /// - `Error::BetsAlreadyPlaced` - Cannot update after bets have been placed - /// - `Error::InvalidQuestion` - New description is empty or invalid - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Symbol, String}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let admin = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// // Update market description - /// match PredictifyHybrid::update_event_description( - /// env.clone(), - /// admin, - /// market_id, - /// String::from_str(&env, "Will Bitcoin reach $100,000 by December 31, 2024?") - /// ) { - /// Ok(()) => println!("Market description updated successfully"), - /// Err(e) => println!("Update failed: {:?}", e), - /// } - /// ``` - /// - /// # Update Rules - /// - /// - Market must be in Active state - /// - No bets can have been placed yet - /// - Market must not be resolved - /// - New description must be non-empty and meet length requirements - /// - /// # Security - /// - /// This function requires admin authentication and validates that no user - /// funds are at risk. Updates are only allowed before any betting activity - /// to maintain fairness and transparency. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn update_event_description( - env: Env, - admin: Address, - market_id: Symbol, - new_description: String, - ) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - - // Validate new description - if new_description.is_empty() { - panic_with_error!(env, Error::InvalidQuestion); - } - - // Get market - let mut market: Market = env - .storage() - .persistent() - .get(&market_id) - .unwrap_or_else(|| panic_with_error!(env, Error::MarketNotFound)); - - // Validate market state - cannot update resolved, closed, or cancelled markets - if market.state != MarketState::Active { - panic_with_error!(env, Error::MarketResolved); - } - - // Check if any bets have been placed - let bet_stats = bets::BetManager::get_market_bet_stats(&env, &market_id); - if bet_stats.total_bets > 0 { - panic_with_error!(env, Error::BetsAlreadyPlaced); - } - - // Check if any votes have been placed - if market.total_staked > 0 { - panic_with_error!(env, Error::AlreadyVoted); - } - - // Store old description for event - let old_description = market.question.clone(); - - // Update market description and refresh the metadata commitment so - // clients with stale cached metadata fail verification. - market.question = new_description.clone(); - market.refresh_metadata_commitment(&env); - - // Save market - env.storage().persistent().set(&market_id, &market); - - // Emit description update event - EventEmitter::emit_market_description_updated( - &env, - &market_id, - &old_description, - &new_description, - &admin, - ); - - let mut details = Map::new(&env); - details.set( - Symbol::new(&env, "update"), - String::from_str(&env, "description"), - ); - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::MarketUpdated, - admin.clone(), - details, - None, - );; - - Ok(()) - } - - /// Updates the outcomes of a market (admin only, before betting starts). - /// - /// This function allows contract administrators to update the available - /// outcomes for a market before any bets have been placed. This ensures - /// that market parameters can be corrected or adjusted without affecting - /// existing user commitments. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `admin` - The administrator address performing the update (must be authorized) - /// * `market_id` - Unique identifier of the market to update - /// * `new_outcomes` - The updated list of possible outcomes - /// - /// # Returns - /// - /// Returns `Result<(), Error>` where: - /// - `Ok(())` - Market outcomes updated successfully - /// - `Err(Error)` - Specific error if update fails - /// - /// # Errors - /// - /// This function returns specific errors: - /// - `Error::Unauthorized` - Caller is not the contract admin - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::MarketResolved` - Cannot update a resolved market - /// - `Error::BetsAlreadyPlaced` - Cannot update after bets have been placed - /// - `Error::InvalidOutcomes` - New outcomes list is invalid (< 2 outcomes or empty strings) - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Symbol, String, Vec}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let admin = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// // Update market outcomes - /// let new_outcomes = Vec::from_array(&env, [ - /// String::from_str(&env, "Yes"), - /// String::from_str(&env, "No"), - /// String::from_str(&env, "Uncertain") - /// ]); - /// - /// match PredictifyHybrid::update_event_outcomes( - /// env.clone(), - /// admin, - /// market_id, - /// new_outcomes - /// ) { - /// Ok(()) => println!("Market outcomes updated successfully"), - /// Err(e) => println!("Update failed: {:?}", e), - /// } - /// ``` - /// - /// # Update Rules - /// - /// - Market must be in Active state - /// - No bets can have been placed yet - /// - Market must not be resolved - /// - New outcomes must have at least 2 options - /// - All outcome strings must be non-empty - /// - /// # Security - /// - /// This function requires admin authentication and validates that no user - /// funds are at risk. Updates are only allowed before any betting activity - /// to maintain fairness and transparency. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn update_event_outcomes( - env: Env, - admin: Address, - market_id: Symbol, - new_outcomes: Vec, - ) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - - // Validate new outcomes - if new_outcomes.len() < 2 { - panic_with_error!(env, Error::InvalidOutcomes); - } - - // Check all outcomes are non-empty - for outcome in new_outcomes.iter() { - if outcome.is_empty() { - panic_with_error!(env, Error::InvalidOutcome); - } - } - - // Get market - let mut market: Market = env - .storage() - .persistent() - .get(&market_id) - .unwrap_or_else(|| panic_with_error!(env, Error::MarketNotFound)); - - // Validate market state - cannot update resolved, closed, or cancelled markets - if market.state != MarketState::Active { - panic_with_error!(env, Error::MarketResolved); - } - - // Check if any bets have been placed - let bet_stats = bets::BetManager::get_market_bet_stats(&env, &market_id); - if bet_stats.total_bets > 0 { - panic_with_error!(env, Error::BetsAlreadyPlaced); - } - - // Check if any votes have been placed - if market.total_staked > 0 { - panic_with_error!(env, Error::AlreadyVoted); - } - - // Store old outcomes for event - let old_outcomes = market.outcomes.clone(); - - // Update market outcomes and refresh the commitment so stale clients - // holding the old metadata commitment fail verification. - market.outcomes = new_outcomes.clone(); - market.refresh_metadata_commitment(&env); - - // Save market - env.storage().persistent().set(&market_id, &market); - - // Emit outcomes update event - EventEmitter::emit_market_outcomes_updated( - &env, - &market_id, - &old_outcomes, - &new_outcomes, - &admin, - ); - - let mut details = Map::new(&env); - details.set( - Symbol::new(&env, "update"), - String::from_str(&env, "outcomes"), - ); - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::MarketUpdated, - admin.clone(), - details, - None, - );; - - Ok(()) - } - - /// Updates the category of a market (admin only, before betting starts). - /// - /// This function allows contract administrators to set or update the category - /// for a market before any bets have been placed. Categories help clients - /// filter and display markets by type (e.g., sports, crypto, politics). - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `admin` - The administrator address performing the update (must be authorized) - /// * `market_id` - Unique identifier of the market to update - /// * `category` - The new category (None to clear the category) - /// - /// # Returns - /// - /// Returns `Result<(), Error>` where: - /// - `Ok(())` - Market category updated successfully - /// - `Err(Error)` - Specific error if update fails - /// - /// # Errors - /// - /// This function returns specific errors: - /// - `Error::Unauthorized` - Caller is not the contract admin - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::MarketResolved` - Cannot update a resolved market - /// - `Error::BetsAlreadyPlaced` - Cannot update after bets have been placed - /// - `Error::InvalidInput` - `Some` with an empty category string, or other invalid optional payload - /// - `Error::CategoryTooShort` / `Error::CategoryTooLong` - Category length outside configured bounds - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Symbol, String}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let admin = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// // Set market category - /// match PredictifyHybrid::update_event_category( - /// env.clone(), - /// admin, - /// market_id, - /// Some(String::from_str(&env, "sports")) - /// ) { - /// Ok(()) => println!("Market category updated successfully"), - /// Err(e) => println!("Update failed: {:?}", e), - /// } - /// ``` - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn update_event_category( - env: Env, - admin: Address, - market_id: Symbol, - category: Option, - ) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - - // Get market - let mut market: Market = env - .storage() - .persistent() - .get(&market_id) - .ok_or(Error::MarketNotFound)?; - - // Validate market state - cannot update resolved, closed, or cancelled markets - if market.state != MarketState::Active { - return Err(Error::MarketResolved); - } - - // Check if any bets have been placed - let bet_stats = bets::BetManager::get_market_bet_stats(&env, &market_id); - if bet_stats.total_bets > 0 { - return Err(Error::BetsAlreadyPlaced); - } - - // Check if any votes have been placed - if market.total_staked > 0 { - return Err(Error::AlreadyVoted); - } - - crate::metadata_limits::validate_option_category_metadata(&category)?; - - // Store old category for event - let old_category = market.category.clone(); - - // Update market category - market.category = category.clone(); - - // Save market - env.storage().persistent().set(&market_id, &market); - - // Emit category update event - EventEmitter::emit_category_updated(&env, &market_id, &old_category, &category, &admin); - - let mut details = Map::new(&env); - details.set( - Symbol::new(&env, "update"), - String::from_str(&env, "category"), - ); - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::MarketUpdated, - admin.clone(), - details, - None, - );; - - Ok(()) - } - - /// Updates the tags of a market (admin only, before betting starts). - /// - /// This function allows contract administrators to set or update tags - /// for a market before any bets have been placed. Tags help clients - /// filter and search markets by multiple dimensions. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `admin` - The administrator address performing the update (must be authorized) - /// * `market_id` - Unique identifier of the market to update - /// * `tags` - The new list of tags (empty Vec to clear all tags) - /// - /// # Returns - /// - /// Returns `Result<(), Error>` where: - /// - `Ok(())` - Market tags updated successfully - /// - `Err(Error)` - Specific error if update fails - /// - /// # Errors - /// - /// This function returns specific errors: - /// - `Error::Unauthorized` - Caller is not the contract admin - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::MarketResolved` - Cannot update a resolved market - /// - `Error::BetsAlreadyPlaced` - Cannot update after bets have been placed - /// - `Error::InvalidInput` - Empty tag entry, duplicate tags, or other invalid list content - /// - `Error::TagTooShort` / `Error::TagTooLong` - Tag length outside configured bounds - /// - `Error::TooManyTags` - More than the maximum number of tags per market - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, Symbol, String, vec}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let admin = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// // Set market tags - /// let tags = vec![ - /// &env, - /// String::from_str(&env, "bitcoin"), - /// String::from_str(&env, "crypto"), - /// String::from_str(&env, "price-prediction") - /// ]; - /// - /// match PredictifyHybrid::update_event_tags( - /// env.clone(), - /// admin, - /// market_id, - /// tags - /// ) { - /// Ok(()) => println!("Market tags updated successfully"), - /// Err(e) => println!("Update failed: {:?}", e), - /// } - /// ``` - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn update_event_tags( - env: Env, - admin: Address, - market_id: Symbol, - tags: Vec, - ) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - - crate::metadata_limits::validate_event_tags(&tags)?; - - // Get market - let mut market: Market = env - .storage() - .persistent() - .get(&market_id) - .ok_or(Error::MarketNotFound)?; - - // Validate market state - cannot update resolved, closed, or cancelled markets - if market.state != MarketState::Active { - return Err(Error::MarketResolved); - } - - // Check if any bets have been placed - let bet_stats = bets::BetManager::get_market_bet_stats(&env, &market_id); - if bet_stats.total_bets > 0 { - return Err(Error::BetsAlreadyPlaced); - } - - // Check if any votes have been placed - if market.total_staked > 0 { - return Err(Error::AlreadyVoted); - } - - // Store old tags for event - let old_tags = market.tags.clone(); - - // Update market tags - market.tags = tags.clone(); - - // Save market - env.storage().persistent().set(&market_id, &market); - - // Emit tags update event - EventEmitter::emit_tags_updated(&env, &market_id, &old_tags, &tags, &admin); - - let mut details = Map::new(&env); - details.set(Symbol::new(&env, "update"), String::from_str(&env, "tags")); - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::MarketUpdated, - admin.clone(), - details, - None, - );; - - Ok(()) - } - - /// Query events by tags (paginated, bounded). - /// - /// Returns events that have ANY of the provided tags (OR logic). - /// - /// # Parameters - /// - /// * `env` - The Soroban environment - /// * `tags` - Tags to filter by (events matching any tag are returned) - /// * `cursor` - Pagination cursor - /// * `limit` - Maximum results per page - /// - /// # Returns - /// - /// Tuple of (events, next_cursor) - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn query_events_by_tags( - env: Env, - tags: Vec, - cursor: u32, - limit: u32, - ) -> (Vec, u32) { - event_archive::EventArchive::query_events_by_tags(&env, &tags, cursor, limit) - } - - /// Return a paginated page of market IDs. - /// - /// Avoids unbounded `Vec` returns by slicing the market index. - /// Pass `next_cursor` from the previous response as `cursor` on the next - /// call. Iteration is complete when `items.len() < limit`. - /// - /// # Parameters - /// - /// * `env` - Soroban environment - /// * `cursor` - Zero-based start index (0 for first page) - /// * `limit` - Desired page size; capped server-side at 50 - /// - /// # Returns - /// - /// `PagedMarketIds` with `items`, `next_cursor`, and `total_count`. - /// - /// # Errors - /// - /// Panics with `Error::ContractStateError` if the market index is corrupted. - /// - /// # Events - /// - /// Read-only; no events emitted. - pub fn get_all_markets_paged(env: Env, cursor: u32, limit: u32) -> PagedMarketIds { - crate::queries::QueryManager::get_all_markets_paged(&env, cursor, limit) - .unwrap_or_else(|e| panic_with_error!(&env, e)) - } - - /// Return a paginated page of a user's bets across markets. - /// - /// Scans the market index slice `[cursor, cursor+limit)` and returns only - /// markets where `user` has placed a bet. Prevents gas exhaustion on - /// large market lists. - /// - /// # Parameters - /// - /// * `env` - Soroban environment - /// * `user` - Address to query - /// * `cursor` - Zero-based start index into the market list - /// * `limit` - Page size; capped server-side at 50 - /// - /// # Returns - /// - /// `PagedUserBets` with `items`, `next_cursor`, and `total_count`. - /// - /// # Errors - /// - /// Panics with `Error::ContractStateError` if the market index is corrupted. - /// - /// # Events - /// - /// Read-only; no events emitted. - pub fn query_user_bets_paged( - env: Env, - user: Address, - cursor: u32, - limit: u32, - ) -> PagedUserBets { - crate::queries::QueryManager::query_user_bets_paged(&env, user, cursor, limit) - .unwrap_or_else(|e| panic_with_error!(&env, e)) - } - - /// Return partial contract state statistics for a market-list page. - /// - /// Processes only the market slice `[cursor, cursor+limit)`. Callers - /// accumulate results across pages to build a full aggregate. - /// - /// # Parameters - /// - /// * `env` - Soroban environment - /// * `cursor` - Start index into the market list - /// * `limit` - Page size; capped server-side at 50 - /// - /// # Returns - /// - /// `(ContractStateQuery, next_cursor)` — partial stats and the cursor for - /// the next call. - /// - /// # Errors - /// - /// Panics with `Error::ContractStateError` if the market index is corrupted. - /// - /// # Events - /// - /// Read-only; no events emitted. - pub fn query_contract_state_paged( - env: Env, - cursor: u32, - limit: u32, - ) -> (ContractStateQuery, u32) { - crate::queries::QueryManager::query_contract_state_paged(&env, cursor, limit) - .unwrap_or_else(|e| panic_with_error!(&env, e)) - } - - /// Cancel an event and automatically refund all placed bets (admin only). - /// - /// This function allows admins to cancel events before resolution and - /// automatically refund all bets placed on the market. It validates - /// cancellation conditions, updates market status, and processes refunds. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `admin` - The administrator address (must be authorized) - /// * `market_id` - Unique identifier of the market to cancel - /// * `reason` - Optional reason for cancellation - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(total_refunded)` - Total amount refunded to users - /// - `Err(Error)` - Error if cancellation fails - /// - /// # Panics - /// - /// This function will panic with specific errors if: - /// - `Error::Unauthorized` - Caller is not the contract admin - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - `Error::MarketResolved` - Market has already been resolved - /// - `Error::InvalidState` - Market is in an invalid state for cancellation - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, String, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let admin = Address::generate(&env); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// match PredictifyHybrid::cancel_event( - /// env.clone(), - /// admin, - /// market_id, - /// Some(String::from_str(&env, "Oracle data unavailable")) - /// ) { - /// Ok(total) => println!("Refunded {} stroops", total), - /// Err(e) => println!("Cancellation failed: {:?}", e), - /// } - /// ``` - /// - /// # Cancellation Conditions - /// - /// - Market must exist and be active - /// - Market must not be resolved - /// - Market must not already be cancelled - /// - Only admin can cancel events - /// - /// # Refund Process - /// - /// 1. All active bets are identified - /// 2. Funds are unlocked and returned to users - /// 3. Bet status is updated to "Refunded" - /// 4. Market state is updated to "Cancelled" - /// 5. Cancellation and refund events are emitted - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn cancel_event( - env: Env, - admin: Address, - market_id: Symbol, - reason: Option, - ) -> Result { - Self::require_primary_admin(&env, &admin)?; - - // Get and validate market - let mut market: Market = env - .storage() - .persistent() - .get(&market_id) - .unwrap_or_else(|| { - panic_with_error!(env, Error::MarketNotFound); - }); - - // Validate cancellation conditions - if market.state == MarketState::Resolved { - return Err(Error::MarketResolved); - } - - if market.state == MarketState::Cancelled { - // Already cancelled, return 0 refunded - return Ok(0); - } - - // Market must be active or ended (not resolved) - if !matches!(market.state, MarketState::Active | MarketState::Ended) { - return Err(Error::InvalidState); - } - - // Capture old state for event - let old_state = market.state.clone(); - - // Update market state to cancelled - market.state = MarketState::Cancelled; - env.storage().persistent().set(&market_id, &market); - - // Refund all bets (batch of token transfers) - let refund_result = bets::BetManager::refund_market_bets(&env, &market_id); - refund_result?; - - // Calculate total refunded (sum of all bets) - let total_refunded = market.total_staked; - - let mut details = Map::new(&env); - if let Some(r) = &reason { - details.set(Symbol::new(&env, "reason"), r.clone()); - } - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::EventCancelled, - admin.clone(), - details, - None, - );; - - // Emit cancellation event - EventEmitter::emit_state_change_event( - &env, - &market_id, - &old_state, - &MarketState::Cancelled, - &reason.unwrap_or_else(|| String::from_str(&env, "Event cancelled by admin")), - ); - - // Emit market closed event - EventEmitter::emit_market_closed(&env, &market_id, &admin); - - Ok(total_refunded) - } - - /// Refund all bets when oracle resolution fails or times out (automatic refund path). - /// - /// Callable when: market has ended, no oracle result, and either (1) resolution - /// timeout has passed since market end, or (2) caller is admin (confirmed failure). - /// Refunds full bet amount per user (no fee deduction). Marks market as cancelled and - /// prevents further resolution. Emits refund events. Idempotent when already cancelled. - /// - /// The timeout gate is evaluated per market from `end_time + resolution_timeout`. - /// Non-admin callers cannot trigger this path before that market-specific deadline. - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn refund_on_oracle_failure( - env: Env, - caller: Address, - market_id: Symbol, - ) -> Result { - caller.require_auth(); - - let mut market: Market = env - .storage() - .persistent() - .get(&market_id) - .ok_or(Error::MarketNotFound)?; - - if market.state == MarketState::Cancelled { - return Ok(0); - } - if market.winning_outcomes.is_some() { - return Err(Error::MarketResolved); - } - if market.oracle_result.is_some() { - return Err(Error::MarketResolved); - } - let current_time = env.ledger().timestamp(); - if current_time < market.end_time { - return Err(Error::MarketClosed); - } - - let stored_admin: Option
= - env.storage().persistent().get(&Symbol::new(&env, "Admin")); - let is_admin = stored_admin.as_ref().map_or(false, |a| a == &caller); - let timeout_passed = resolution_timeout_reached(&env, &market); - if !is_admin && !timeout_passed { - return Err(Error::Unauthorized); - } - - let old_state = market.state.clone(); - market.state = MarketState::Cancelled; - env.storage().persistent().set(&market_id, &market); - - let refund_result = bets::BetManager::refund_market_bets(&env, &market_id); - refund_result?; - - let total_refunded = market.total_staked; - EventEmitter::emit_state_change_event( - &env, - &market_id, - &old_state, - &MarketState::Cancelled, - &String::from_str(&env, "Refund on oracle failure/timeout"), - ); - EventEmitter::emit_refund_on_oracle_failure(&env, &market_id, total_refunded); - - Ok(total_refunded) - } - - /// Extend market duration (admin only) - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn extend_market( - env: Env, - admin: Address, - market_id: Symbol, - additional_days: u32, - reason: String, - _fee_amount: i128, - ) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - - extensions::ExtensionManager::extend_market_duration( - &env, - admin, - market_id, - additional_days, - reason, - ) - } - - /// Sets the admin-configurable cumulative extension cap (in days) that applies - /// globally to all markets. A value of `0` disables the cap. - /// - /// # Errors - /// - /// Returns [`Error::Unauthorized`] when the caller is not the primary admin. - /// - /// # Events - /// - /// Emits no events; purely a configuration write. - pub fn set_cumulative_extension_cap( - env: Env, - admin: Address, - cap_days: u32, - ) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - let key = Symbol::new(&env, "cum_ext_cap"); - env.storage().persistent().set(&key, &cap_days); - Ok(()) - } - - /// Returns the running cumulative extension total (in days) for a given market. - /// Returns `0` when no extensions have been recorded yet. - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_cumulative_extension_total(env: Env, market_id: Symbol) -> Result { - let key = crate::storage::DataKey::MarketExtensionTotal(market_id); - Ok(env.storage().persistent().get(&key).unwrap_or(0u32)) - } - - // ===== STORAGE OPTIMIZATION FUNCTIONS ===== - - /// Compress market data for storage optimization - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn compress_market_data( - env: Env, - market_id: Symbol, - ) -> Result { - let market = match markets::MarketStateManager::get_market(&env, &market_id) { - Ok(m) => m, - Err(e) => return Err(e), - }; - - storage::StorageOptimizer::compress_market_data(&env, &market) - } - - /// Clean up old market data based on age and state - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn cleanup_old_market_data(env: Env, market_id: Symbol) -> Result { - storage::StorageOptimizer::cleanup_old_market_data(&env, &market_id) - } - - /// Migrate storage format from old to new format - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn migrate_storage_format( - env: Env, - from_format: storage::StorageFormat, - to_format: storage::StorageFormat, - ) -> Result { - let result = - storage::StorageOptimizer::migrate_storage_format(&env, from_format, to_format); - - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::StorageMigrated, - env.current_contract_address(), - Map::new(&env), - None, - ); - - result - } - - /// Promote resolved market metadata from Temporary to Persistent storage - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - pub fn promote_market_to_persistent( - env: Env, - market_id: Symbol, - ) -> Result<(), Error> { - storage::StorageMigration::promote_market_to_persistent(&env, &market_id) - } - - /// Demote scratch keys from Persistent to Temporary storage - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - pub fn demote_scratch_keys( - env: Env, - market_id: Symbol, - ) -> Result<(), Error> { - storage::StorageMigration::demote_scratch_keys(&env, &market_id) - } - - /// Monitor storage usage and return statistics - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn monitor_storage_usage(env: Env) -> Result { - storage::StorageOptimizer::monitor_storage_usage(&env) - } - - /// Optimize storage layout for a specific market - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn optimize_storage_layout(env: Env, market_id: Symbol) -> Result { - storage::StorageOptimizer::optimize_storage_layout(&env, &market_id) - } - - /// Get storage usage statistics - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_storage_usage_statistics(env: Env) -> Result { - storage::StorageOptimizer::get_storage_usage_statistics(&env) - } - - /// Validate storage integrity for a specific market - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn validate_storage_integrity( - env: Env, - market_id: Symbol, - ) -> Result { - storage::StorageOptimizer::validate_storage_integrity(&env, &market_id) - } - - /// Get storage configuration - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_storage_config(env: Env) -> storage::StorageConfig { - storage::StorageOptimizer::get_storage_config(&env) - } - - /// Update storage configuration - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn update_storage_config(env: Env, config: storage::StorageConfig) -> Result<(), Error> { - storage::StorageOptimizer::update_storage_config(&env, &config) - } - - /// Calculate storage cost for a market - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn calculate_storage_cost(env: Env, market_id: Symbol) -> Result { - let market = match markets::MarketStateManager::get_market(&env, &market_id) { - Ok(m) => m, - Err(e) => return Err(e), - }; - - Ok(storage::StorageUtils::calculate_storage_cost(&market)) - } - - /// Get storage efficiency score for a market - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_storage_efficiency_score(env: Env, market_id: Symbol) -> Result { - let market = match markets::MarketStateManager::get_market(&env, &market_id) { - Ok(m) => m, - Err(e) => return Err(e), - }; - - Ok(storage::StorageUtils::get_storage_efficiency_score(&market)) - } - - /// Get storage recommendations for a market - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_storage_recommendations(env: Env, market_id: Symbol) -> Result, Error> { - let market = match markets::MarketStateManager::get_market(&env, &market_id) { - Ok(m) => m, - Err(e) => return Err(e), - }; - - Ok(storage::StorageUtils::get_storage_recommendations(&market)) - } - - // ===== ERROR RECOVERY FUNCTIONS ===== - - /// Recover from an error using appropriate recovery strategy - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn recover_from_error( - env: Env, - error: Error, - context: errors::ErrorContext, - ) -> Result { - errors::ErrorHandler::recover_from_error(&env, error, context) - } - - /// Validate error recovery configuration and state - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn validate_error_recovery( - env: Env, - recovery: errors::ErrorRecovery, - ) -> Result { - errors::ErrorHandler::validate_error_recovery(&env, &recovery) - } - - /// Get current error recovery status and statistics - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_error_recovery_status(env: Env) -> Result { - errors::ErrorHandler::get_error_recovery_status(&env) - } - - /// Emit error recovery event for monitoring and logging - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn emit_error_recovery_event(env: Env, recovery: errors::ErrorRecovery) { - errors::ErrorHandler::emit_error_recovery_event(&env, &recovery); - } - - /// Validate resilience patterns configuration - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn validate_resilience_patterns( - env: Env, - patterns: Vec, - ) -> Result { - errors::ErrorHandler::validate_resilience_patterns(&env, &patterns) - } - - /// Document error recovery procedures and best practices - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn document_error_recovery(env: Env) -> Result, Error> { - errors::ErrorHandler::document_error_recovery_procedures(&env) - } - - // ===== EDGE CASE HANDLING ENTRY POINTS ===== - - /// Handle zero stake scenario for a specific market - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn handle_zero_stake_scenario(env: Env, market_id: Symbol) -> Result<(), Error> { - edge_cases::EdgeCaseHandler::handle_zero_stake_scenario(&env, market_id) - } - - /// Implement tie-breaking mechanism for equal outcomes - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn implement_tie_breaking_mechanism( - env: Env, - outcomes: Vec, - ) -> Result { - edge_cases::EdgeCaseHandler::implement_tie_breaking_mechanism(&env, outcomes) - } - - /// Detect orphaned markets and return their IDs - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn detect_orphaned_markets(env: Env) -> Result, Error> { - edge_cases::EdgeCaseHandler::detect_orphaned_markets(&env) - } - - /// Handle partial resolution with incomplete data - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn handle_partial_resolution( - env: Env, - market_id: Symbol, - partial_data: edge_cases::PartialData, - ) -> Result<(), Error> { - edge_cases::EdgeCaseHandler::handle_partial_resolution(&env, market_id, partial_data) - } - - /// Validate edge case handling scenario - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn validate_edge_case_handling( - env: Env, - scenario: edge_cases::EdgeCaseScenario, - ) -> Result<(), Error> { - edge_cases::EdgeCaseHandler::validate_edge_case_handling(&env, scenario) - } - - /// Run comprehensive edge case testing scenarios - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn test_edge_case_scenarios(env: Env) -> Result<(), Error> { - edge_cases::EdgeCaseHandler::test_edge_case_scenarios(&env) - } - - /// Get comprehensive edge case statistics - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_edge_case_statistics(env: Env) -> Result { - edge_cases::EdgeCaseHandler::get_edge_case_statistics(&env) - } - - // ===== RECOVERY PUBLIC METHODS ===== - /// Initiates or performs recovery of a potentially corrupted market state. Only admin. - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn recover_market_state(env: Env, admin: Address, market_id: Symbol) -> bool { - admin.require_auth(); - if let Err(e) = crate::recovery::RecoveryManager::assert_is_admin(&env, &admin) { - panic_with_error!(env, e); - } - let result = match crate::recovery::RecoveryManager::recover_market_state( - &env, &admin, &market_id, - ) { - Ok(res) => res, - Err(e) => panic_with_error!(env, e), - }; - - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::ErrorRecovered, - admin.clone(), - Map::new(&env), - None, - ); - - result - } - - /// Executes partial refund mechanism for selected users in a failed/corrupted market. Only admin. - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn partial_refund_mechanism( - env: Env, - admin: Address, - market_id: Symbol, - users: Vec
, - ) -> i128 { - Self::require_primary_admin_or_panic(&env, &admin); - let result = match crate::recovery::RecoveryManager::partial_refund_mechanism( - &env, &admin, &market_id, &users, - ) { - Ok(total_refunded) => total_refunded, - Err(e) => panic_with_error!(env, e), - }; - - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::PartialRefundExecuted, - admin.clone(), - Map::new(&env), - None, - ); - - result - } - - /// Validates market state integrity; returns true if consistent. - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn validate_market_state_integrity(env: Env, market_id: Symbol) -> bool { - match crate::recovery::RecoveryValidator::validate_market_state_integrity(&env, &market_id) - { - Ok(_) => true, - Err(_) => false, - } - } - - /// Returns recovery status for a market. - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_recovery_status(env: Env, market_id: Symbol) -> String { - crate::recovery::RecoveryManager::get_recovery_status(&env, &market_id) - .unwrap_or_else(|_| String::from_str(&env, "unknown")) - } - - /// Remove the oldest `count` completed recovery history entries for a market (admin only). - /// - /// Active (unresolved) recovery state is never pruned. `count` is capped at 30. - /// - /// # Errors - /// * `Unauthorized` - Caller is not admin - pub fn prune_recovery_history( - env: Env, - admin: Address, - market_id: Symbol, - count: u32, - ) -> Result { - crate::recovery::RecoveryManager::prune_recovery_history(&env, &admin, &market_id, count) - } - - // ===== VERSIONING FUNCTIONS ===== - - /// Track contract version for versioning system - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn track_contract_version(env: Env, version: versioning::Version) -> Result<(), Error> { - versioning::VersionManager::new(&env).track_contract_version(&env, version) - } - - /// Migrate data between contract versions - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn migrate_data_between_versions( - env: Env, - old_version: versioning::Version, - new_version: versioning::Version, - ) -> Result { - versioning::VersionManager::new(&env).migrate_data_between_versions( - &env, - old_version, - new_version, - ) - } - - /// Validate version compatibility - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn validate_version_compatibility( - env: Env, - old_version: versioning::Version, - new_version: versioning::Version, - ) -> Result { - versioning::VersionManager::new(&env).validate_version_compatibility( - &env, - &old_version, - &new_version, - ) - } - - /// Upgrade to a specific version - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn upgrade_to_version(env: Env, target_version: versioning::Version) -> Result<(), Error> { - versioning::VersionManager::new(&env).upgrade_to_version(&env, target_version) - } - - /// Rollback to a specific version - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn rollback_to_version(env: Env, target_version: versioning::Version) -> Result<(), Error> { - versioning::VersionManager::new(&env).rollback_to_version(&env, target_version) - } - - /// Get version history - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_version_history(env: Env) -> Result { - versioning::VersionManager::new(&env).get_version_history(&env) - } - - /// Test version migration - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn test_version_migration( - env: Env, - migration: versioning::VersionMigration, - ) -> Result { - versioning::VersionManager::new(&env).test_version_migration(&env, migration) - } - - // ===== MONITORING FUNCTIONS ===== - - /// Monitor market health for a specific market - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn monitor_market_health( - env: Env, - market_id: Symbol, - ) -> Result { - monitoring::ContractMonitor::monitor_market_health(&env, market_id) - } - - /// Monitor oracle health for a specific oracle provider - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn monitor_oracle_health( - env: Env, - oracle: OracleProvider, - ) -> Result { - monitoring::ContractMonitor::monitor_oracle_health(&env, oracle) - } - - /// Monitor fee collection performance - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn monitor_fee_collection( - env: Env, - timeframe: monitoring::TimeFrame, - ) -> Result { - monitoring::ContractMonitor::monitor_fee_collection(&env, timeframe) - } - - /// Monitor dispute resolution performance - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn monitor_dispute_resolution( - env: Env, - market_id: Symbol, - ) -> Result { - monitoring::ContractMonitor::monitor_dispute_resolution(&env, market_id) - } - - /// Get comprehensive contract performance metrics - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_contract_performance_metrics( - env: Env, - timeframe: monitoring::TimeFrame, - ) -> Result { - monitoring::ContractMonitor::get_contract_performance_metrics(&env, timeframe) - } - - /// Emit monitoring alert - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn emit_monitoring_alert( - env: Env, - alert: monitoring::MonitoringAlert, - ) -> Result<(), Error> { - monitoring::ContractMonitor::emit_monitoring_alert(&env, alert) - } - - /// Validate monitoring data integrity - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn validate_monitoring_data( - env: Env, - data: monitoring::MonitoringData, - ) -> Result { - monitoring::ContractMonitor::validate_monitoring_data(&env, &data) - } - - /// Return all alerts in the bounded monitoring queue (oldest first). - /// - /// Clients should also check [`is_monitor_overflow`] to detect whether any - /// alerts were silently evicted since the last admin reset. - pub fn get_monitor_alerts(env: Env) -> Vec { - monitoring::ContractMonitor::get_alerts(&env) - } - - /// Return `true` if at least one alert has been evicted from the queue due to - /// overflow since the last [`clear_monitor_overflow`] call. - pub fn is_monitor_overflow(env: Env) -> bool { - monitoring::ContractMonitor::is_overflow(&env) - } - - /// Reset the monitoring overflow flag. Only the contract admin may call this. - /// - /// # Errors - /// - /// - [`Error::AdminNotSet`] – no admin has been initialised. - /// - [`Error::Unauthorized`] – `admin` does not match the stored admin address. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn clear_monitor_overflow(env: Env, admin: Address) -> Result<(), Error> { - monitoring::ContractMonitor::clear_overflow(&env, &admin) - } - - // ===== ORACLE FALLBACK FUNCTIONS ===== - - /// Get oracle data with backup if primary fails. - /// - /// The helper always attempts `primary_oracle` first. It attempts `backup_oracle` - /// only after a failed primary call, and it aborts before any oracle call once - /// `ledger.timestamp() >= end_time + resolution_timeout`. - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - 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); - } - if resolution_timeout_reached(&env, &market) { - EventEmitter::emit_resolution_timeout(&env, &market_id, current_time); - return Err(Error::ResolutionTimeoutReached); - } - - // 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, ORACLE_FAILURE_PRIMARY_THEN_FALLBACK_REASON); - events::EventEmitter::emit_manual_resolution_required(&env, &market_id, &reason); - Err(Error::FallbackOracleUnavailable) - } - } - } - - /// Check if oracle is working - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - 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::Degraded => String::from_str(&env, "degraded"), - OracleHealth::Broken => String::from_str(&env, "broken"), - } - } - - // ===== MULTI-ADMIN MANAGEMENT FUNCTIONS ===== - - /// Add a new admin with specified role (SuperAdmin only) - /// - /// The caller must satisfy Soroban `require_auth()`. Access is granted to the - /// stored primary admin and, after multi-admin migration, any delegated admin - /// with `AdminPermission::Emergency`. - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn add_admin( - env: Env, - current_admin: Address, - new_admin: Address, - role: AdminRole, - ) -> Result<(), Error> { - Self::require_admin_permission(&env, ¤t_admin, AdminPermission::Emergency)?; - AdminManager::add_admin(&env, ¤t_admin, &new_admin, role) - } - - /// Remove an admin from the system (SuperAdmin only) - /// - /// The caller must satisfy Soroban `require_auth()`. Access is granted to the - /// stored primary admin and, after multi-admin migration, any delegated admin - /// with `AdminPermission::Emergency`. - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn remove_admin( - env: Env, - current_admin: Address, - admin_to_remove: Address, - ) -> Result<(), Error> { - Self::require_admin_permission(&env, ¤t_admin, AdminPermission::Emergency)?; - AdminManager::remove_admin(&env, ¤t_admin, &admin_to_remove) - } - - /// Update an admin's role (SuperAdmin only) - /// - /// The caller must satisfy Soroban `require_auth()`. Access is granted to the - /// stored primary admin and, after multi-admin migration, any delegated admin - /// with `AdminPermission::Emergency`. - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn update_admin_role( - env: Env, - current_admin: Address, - target_admin: Address, - new_role: AdminRole, - ) -> Result<(), Error> { - Self::require_admin_permission(&env, ¤t_admin, AdminPermission::Emergency)?; - AdminManager::update_admin_role(&env, ¤t_admin, &target_admin, new_role) - } - - /// Validate admin permission for specific action - /// - /// The caller must satisfy Soroban `require_auth()`, and the contract must - /// already have an initialized primary admin in persistent storage. - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn validate_admin_permission( - env: Env, - admin: Address, - permission: AdminPermission, - ) -> Result<(), Error> { - Self::require_initialized_admin_root(&env, &admin)?; - AdminManager::validate_admin_permission(&env, &admin, permission) - } - - /// Get all admin roles in the system - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_admin_roles(env: Env) -> Map { - AdminManager::get_admin_roles(&env) - } - - /// Get comprehensive admin analytics - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_admin_analytics(env: Env) -> AdminAnalyticsResult { - admin::EnhancedAdminAnalytics::get_admin_analytics(&env) - } - - /// Migrate from single-admin to multi-admin system - /// - /// Only the stored primary admin can trigger the one-way migration into the - /// delegated multi-admin storage layout. - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn migrate_to_multi_admin(env: Env, admin: Address) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - admin::AdminSystemIntegration::migrate_to_multi_admin(&env) - } - - /// Check if multi-admin migration is complete - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn is_multi_admin_migrated(env: Env) -> bool { - admin::AdminSystemIntegration::is_migrated(&env) - } - - /// Check role permissions against a specific permission - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn check_role_permissions(env: Env, role: AdminRole, permission: AdminPermission) -> bool { - AdminManager::check_role_permissions(&env, role, permission) - } - - // ===== CONTRACT UPGRADE METHODS ===== - - /// Upgrade the contract to new Wasm bytecode - /// - /// This function allows authorized admins to upgrade the contract to a new - /// version by replacing the Wasm bytecode. It includes comprehensive validation, - /// version checking, and event logging. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment - /// * `admin` - The admin performing the upgrade (must be authorized) - /// * `new_wasm_hash` - Hash of the new Wasm bytecode to deploy - /// - /// # Returns - /// - /// * `Ok(())` if upgrade succeeds - /// * `Err(Error)` if authorization fails or upgrade is incompatible - /// - /// # Security - /// - /// - Requires Soroban `require_auth()` from the caller - /// - Requires the caller to match the stored primary admin in persistent storage - /// - Validates WASM hash chain to prevent out-of-order/forked upgrades - /// - Validates version compatibility - /// - Performs safety checks before upgrade - /// - Logs all upgrade attempts for audit trail - /// - /// # Parameters - /// - /// * `env` - Soroban environment - /// * `admin` - Admin performing the upgrade (must be authorized) - /// * `new_wasm_hash` - Hash of new Wasm bytecode to deploy - /// * `expected_predecessor` - Expected current WASM hash (for chain verification) - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Address, BytesN}; - /// # let env = Env::default(); - /// # let admin = Address::generate(&env); - /// # let new_wasm_hash = BytesN::from_array(&env, &[1u8; 32]); - /// # let current_hash = BytesN::from_array(&env, &[0u8; 32]); - /// - /// // Perform upgrade with admin authorization and chain verification - /// admin.require_auth(); - /// PredictifyHybrid::upgrade_contract(env, admin, new_wasm_hash, current_hash)?; - /// # Ok::<(), predictify_hybrid::errors::Error>(()) - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// Returns [`Error::UpgradeChainMismatch`] if the expected predecessor does not match the current WASM hash. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn upgrade_contract( - env: Env, - admin: Address, - new_wasm_hash: soroban_sdk::BytesN<32>, - expected_predecessor: soroban_sdk::BytesN<32>, - ) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - let result = upgrade_manager::UpgradeManager::upgrade_contract( - &env, - &admin, - new_wasm_hash, - expected_predecessor, - ); - - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::ContractUpgraded, - admin.clone(), - Map::new(&env), - None, - ); - - result - } - - /// Rollback contract to previous version - /// - /// Reverts the contract to a previous Wasm version. This is a critical - /// recovery mechanism for failed upgrades. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment - /// * `admin` - The admin performing the rollback (must be authorized) - /// * `rollback_wasm_hash` - Hash of the Wasm bytecode to rollback to - /// - /// # Returns - /// - /// * `Ok(())` if rollback succeeds - /// * `Err(Error)` if authorization fails or rollback is invalid - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn rollback_upgrade( - env: Env, - admin: Address, - rollback_wasm_hash: soroban_sdk::BytesN<32>, - ) -> Result<(), Error> { - Self::require_primary_admin(&env, &admin)?; - let result = - upgrade_manager::UpgradeManager::rollback_upgrade(&env, &admin, rollback_wasm_hash); - - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::UpgradeRolledBack, - admin.clone(), - Map::new(&env), - None, - ); - - result - } - - /// Get current contract version - /// - /// Returns the currently active contract version information. - /// - /// # Returns - /// - /// * `Ok(Version)` - Current contract version - /// * `Err(Error)` - If version cannot be retrieved - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_contract_version(env: Env) -> Result { - upgrade_manager::UpgradeManager::get_contract_version(&env) - } - - /// Check if upgrade is available - /// - /// Checks if there are approved upgrade proposals ready for execution. - /// - /// # Returns - /// - /// * `Ok(bool)` - True if upgrade is available - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn check_upgrade_available(env: Env) -> Result { - upgrade_manager::UpgradeManager::check_upgrade_available(&env) - } - - /// Get upgrade history - /// - /// Retrieves complete history of all contract upgrades. - /// - /// # Returns - /// - /// * `Ok(Vec)` - List of all upgrade records - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_upgrade_history(env: Env) -> Result, Error> { - upgrade_manager::UpgradeManager::get_upgrade_history(&env) - } - - /// Get upgrade statistics - /// - /// Calculates and returns comprehensive upgrade statistics. - /// - /// # Returns - /// - /// * `Ok(UpgradeStats)` - Upgrade statistics and analytics - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_upgrade_statistics(env: Env) -> Result { - upgrade_manager::UpgradeManager::get_upgrade_statistics(&env) - } - - /// Validate upgrade compatibility - /// - /// Performs comprehensive validation of an upgrade proposal without - /// executing the upgrade. Useful for testing and validation. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment - /// * `proposal` - The upgrade proposal to validate - /// - /// # Returns - /// - /// * `Ok(CompatibilityCheckResult)` - Detailed compatibility analysis - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn validate_upgrade_compatibility( - env: Env, - proposal: upgrade_manager::UpgradeProposal, - ) -> Result { - upgrade_manager::UpgradeManager::validate_upgrade_compatibility(&env, &proposal) - } - - /// Test upgrade safety - /// - /// Performs dry-run validation of an upgrade proposal without executing. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment - /// * `proposal` - The upgrade proposal to test - /// - /// # Returns - /// - /// * `Ok(bool)` - True if upgrade would succeed - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn test_upgrade_safety( - env: Env, - proposal: upgrade_manager::UpgradeProposal, - ) -> Result { - upgrade_manager::UpgradeManager::test_upgrade_safety(&env, &proposal) - } - - // ===== MARKET ANALYTICS FUNCTIONS ===== - - /// Get comprehensive market statistics for data analysis and insights - /// - /// This function provides detailed statistics about a specific market including - /// participation metrics, stake distribution, outcome analysis, and performance - /// indicators. It's essential for market monitoring and user interfaces. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_id` - Unique identifier of the market to analyze - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(MarketStatistics)` - Complete market statistics and analytics - /// - `Err(Error)` - Error if market not found or analysis fails - /// - /// # Errors - /// - /// This function returns: - /// - `Error::MarketNotFound` - Market with given ID doesn't exist - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// match PredictifyHybrid::get_market_analytics_statistics(env.clone(), market_id) { - /// Ok(stats) => { - /// println!("Total participants: {}", stats.total_participants); - /// println!("Total stake: {}", stats.total_stake); - /// println!("Consensus strength: {}%", stats.consensus_strength); - /// }, - /// Err(e) => println!("Analytics unavailable: {:?}", e), - /// } - /// ``` - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_market_analytics_statistics( - env: Env, - market_id: Symbol, - ) -> Result { - market_analytics::MarketAnalyticsManager::get_market_statistics(&env, market_id) - } - - /// Get voting analytics and participation metrics for a market - /// - /// This function provides detailed analysis of voting patterns, participation - /// trends, and community engagement within a specific market. It's useful - /// for understanding market dynamics and user behavior. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_id` - Unique identifier of the market to analyze - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(VotingAnalytics)` - Complete voting analytics and metrics - /// - `Err(Error)` - Error if market not found or analysis fails - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// match PredictifyHybrid::get_voting_analytics(env.clone(), market_id) { - /// Ok(analytics) => { - /// println!("Total votes: {}", analytics.total_votes); - /// println!("Unique voters: {}", analytics.unique_voters); - /// }, - /// Err(e) => println!("Voting analytics unavailable: {:?}", e), - /// } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_voting_analytics( - env: Env, - market_id: Symbol, - ) -> Result { - market_analytics::MarketAnalyticsManager::get_voting_analytics(&env, market_id) - } - - /// Get oracle performance statistics for a specific oracle provider - /// - /// This function provides comprehensive performance metrics for oracle providers, - /// including accuracy rates, response times, uptime statistics, and reliability - /// scores. It's essential for oracle monitoring and optimization. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `oracle` - The oracle provider to analyze - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(OraclePerformanceStats)` - Complete oracle performance statistics - /// - `Err(Error)` - Error if oracle data unavailable - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::Env; - /// # use predictify_hybrid::{PredictifyHybrid, OracleProvider}; - /// # let env = Env::default(); - /// - /// match PredictifyHybrid::get_oracle_performance_stats(env.clone(), OracleProvider::Reflector) { - /// Ok(stats) => { - /// println!("Oracle accuracy: {}%", stats.accuracy_rate); - /// println!("Uptime: {}%", stats.uptime_percentage); - /// println!("Reliability score: {}", stats.reliability_score); - /// }, - /// Err(e) => println!("Oracle stats unavailable: {:?}", e), - /// } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_oracle_performance_stats( - env: Env, - oracle: OracleProvider, - ) -> Result { - market_analytics::MarketAnalyticsManager::get_oracle_performance_stats(&env, oracle) - } - - /// Get fee analytics and revenue tracking for a specific timeframe - /// - /// This function provides comprehensive fee collection analytics including - /// revenue tracking, fee distribution analysis, and collection efficiency - /// metrics. It's essential for financial monitoring and optimization. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `timeframe` - The time period for fee analysis - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(FeeAnalytics)` - Complete fee analytics and revenue data - /// - `Err(Error)` - Error if fee data unavailable - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::Env; - /// # use predictify_hybrid::{PredictifyHybrid, TimeFrame}; - /// # let env = Env::default(); - /// - /// match PredictifyHybrid::get_fee_analytics(env.clone(), TimeFrame::Month) { - /// Ok(analytics) => { - /// println!("Total fees collected: {}", analytics.total_fees_collected); - /// println!("Collection rate: {}%", analytics.fee_collection_rate); - /// }, - /// Err(e) => println!("Fee analytics unavailable: {:?}", e), - /// } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_fee_analytics( - env: Env, - timeframe: market_analytics::TimeFrame, - ) -> Result { - market_analytics::MarketAnalyticsManager::get_fee_analytics(&env, timeframe) - } - - /// Get dispute analytics and resolution metrics for a market - /// - /// This function provides detailed analysis of dispute patterns, resolution - /// efficiency, and dispute-related metrics for a specific market. It's - /// essential for understanding dispute dynamics and improving resolution processes. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_id` - Unique identifier of the market to analyze - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(DisputeAnalytics)` - Complete dispute analytics and metrics - /// - `Err(Error)` - Error if market not found or analysis fails - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// match PredictifyHybrid::get_dispute_analytics(env.clone(), market_id) { - /// Ok(analytics) => { - /// println!("Total disputes: {}", analytics.total_disputes); - /// println!("Success rate: {}%", analytics.dispute_success_rate); - /// }, - /// Err(e) => println!("Dispute analytics unavailable: {:?}", e), - /// } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_dispute_analytics( - env: Env, - market_id: Symbol, - ) -> Result { - market_analytics::MarketAnalyticsManager::get_dispute_analytics(&env, market_id) - } - - /// Get participation metrics for a specific market - /// - /// This function provides comprehensive participation analysis including - /// user engagement, retention rates, and activity patterns for a specific - /// market. It's essential for understanding user behavior and market health. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_id` - Unique identifier of the market to analyze - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(ParticipationMetrics)` - Complete participation metrics and analysis - /// - `Err(Error)` - Error if market not found or analysis fails - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Symbol}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let market_id = Symbol::new(&env, "market_1"); - /// - /// match PredictifyHybrid::get_participation_metrics(env.clone(), market_id) { - /// Ok(metrics) => { - /// println!("Total participants: {}", metrics.total_participants); - /// println!("Engagement score: {}", metrics.engagement_score); - /// println!("Retention rate: {}%", metrics.retention_rate); - /// }, - /// Err(e) => println!("Participation metrics unavailable: {:?}", e), - /// } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_participation_metrics( - env: Env, - market_id: Symbol, - ) -> Result { - market_analytics::MarketAnalyticsManager::get_participation_metrics(&env, market_id) - } - - /// Get market comparison analytics for multiple markets - /// - /// This function provides comparative analysis across multiple markets, - /// including performance rankings, comparative metrics, and market insights. - /// It's essential for understanding market trends and performance patterns. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `markets` - Vector of market identifiers to compare - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(MarketComparisonAnalytics)` - Complete comparative analytics - /// - `Err(Error)` - Error if analysis fails - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, Symbol, vec}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let markets = vec![ - /// # &env, - /// # Symbol::new(&env, "market_1"), - /// # Symbol::new(&env, "market_2"), - /// # ]; - /// - /// match PredictifyHybrid::get_market_comparison_analytics(env.clone(), markets) { - /// Ok(comparison) => { - /// println!("Total markets: {}", comparison.total_markets); - /// println!("Average participation: {}", comparison.average_participation); - /// println!("Success rate: {}%", comparison.success_rate); - /// }, - /// Err(e) => println!("Comparison analytics unavailable: {:?}", e), - /// } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_market_comparison_analytics( - env: Env, - markets: Vec, - ) -> Result { - market_analytics::MarketAnalyticsManager::get_market_comparison_analytics(&env, markets) - } - - // ===== PERFORMANCE BENCHMARK FUNCTIONS ===== - - /// Benchmark gas usage for a specific function with given inputs - /// - /// This function measures the gas consumption and execution time for a specific - /// contract function with provided inputs. It's essential for performance - /// optimization and gas cost analysis. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `function` - Name of the function to benchmark - /// * `inputs` - Vector of input parameters for the function - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(BenchmarkResult)` - Complete benchmark results including gas usage and execution time - /// - `Err(Error)` - Error if benchmarking fails - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, String, vec}; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// # let inputs = vec![&env, String::from_str(&env, "test_input")]; - /// - /// match PredictifyHybrid::benchmark_gas_usage( - /// env.clone(), - /// String::from_str(&env, "create_market"), - /// inputs - /// ) { - /// Ok(result) => { - /// println!("Gas usage: {}", result.gas_usage); - /// println!("Execution time: {}", result.execution_time); - /// println!("Performance score: {}", result.performance_score); - /// }, - /// Err(e) => println!("Benchmark failed: {:?}", e), - /// } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn benchmark_gas_usage( - env: Env, - function: String, - inputs: Vec, - ) -> Result { - performance_benchmarks::PerformanceBenchmarkManager::benchmark_gas_usage( - &env, function, inputs, - ) - } - - /// Benchmark storage usage for a specific operation - /// - /// This function measures storage consumption and performance for various - /// storage operations including read, write, and delete operations. - /// It's essential for storage optimization and cost analysis. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `operation` - Storage operation configuration to benchmark - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(BenchmarkResult)` - Complete storage benchmark results - /// - `Err(Error)` - Error if benchmarking fails - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::Env; - /// # use predictify_hybrid::{PredictifyHybrid, StorageOperation}; - /// # let env = Env::default(); - /// # let operation = StorageOperation { - /// # operation_type: String::from_str(&env, "write"), - /// # data_size: 1024, - /// # key_count: 10, - /// # value_count: 10, - /// # operation_count: 100, - /// # }; - /// - /// match PredictifyHybrid::benchmark_storage_usage(env.clone(), operation) { - /// Ok(result) => { - /// println!("Storage usage: {}", result.storage_usage); - /// println!("Gas usage: {}", result.gas_usage); - /// }, - /// Err(e) => println!("Storage benchmark failed: {:?}", e), - /// } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn benchmark_storage_usage( - env: Env, - operation: performance_benchmarks::StorageOperation, - ) -> Result { - performance_benchmarks::PerformanceBenchmarkManager::benchmark_storage_usage( - &env, operation, - ) - } - - /// Benchmark oracle call performance for a specific oracle provider - /// - /// This function measures the performance characteristics of oracle calls - /// including response time, gas usage, and reliability metrics. - /// It's essential for oracle performance monitoring and optimization. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `oracle` - The oracle provider to benchmark - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(BenchmarkResult)` - Complete oracle performance benchmark results - /// - `Err(Error)` - Error if benchmarking fails - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::Env; - /// # use predictify_hybrid::{PredictifyHybrid, OracleProvider}; - /// # let env = Env::default(); - /// - /// match PredictifyHybrid::benchmark_oracle_call_performance( - /// env.clone(), - /// OracleProvider::Reflector - /// ) { - /// Ok(result) => { - /// println!("Oracle response time: {}", result.execution_time); - /// println!("Oracle gas usage: {}", result.gas_usage); - /// }, - /// Err(e) => println!("Oracle benchmark failed: {:?}", e), - /// } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn benchmark_oracle_performance( - env: Env, - oracle: OracleProvider, - ) -> Result { - performance_benchmarks::PerformanceBenchmarkManager::benchmark_oracle_call_performance( - &env, oracle, - ) - } - - /// Benchmark batch operations performance - /// - /// This function measures the performance of batch operations including - /// gas efficiency, execution time, and throughput characteristics. - /// It's essential for batch operation optimization and scalability analysis. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `operations` - Vector of batch operations to benchmark - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(BenchmarkResult)` - Complete batch operation benchmark results - /// - `Err(Error)` - Error if benchmarking fails - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::{Env, vec}; - /// # use predictify_hybrid::{PredictifyHybrid, BatchOperation}; - /// # let env = Env::default(); - /// # let operations = vec![ - /// # &env, - /// # BatchOperation { - /// # operation_type: String::from_str(&env, "batch_vote"), - /// # batch_size: 100, - /// # operation_count: 10, - /// # data_size: 1024, - /// # } - /// # ]; - /// - /// match PredictifyHybrid::benchmark_batch_operations(env.clone(), operations) { - /// Ok(result) => { - /// println!("Batch execution time: {}", result.execution_time); - /// println!("Batch gas usage: {}", result.gas_usage); - /// }, - /// Err(e) => println!("Batch benchmark failed: {:?}", e), - /// } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn benchmark_batch_operations( - env: Env, - operations: Vec, - ) -> Result { - performance_benchmarks::PerformanceBenchmarkManager::benchmark_batch_operations( - &env, operations, - ) - } - - /// Benchmark scalability with large markets and user counts - /// - /// This function measures the contract's performance under high load - /// scenarios with large numbers of markets and users. It's essential - /// for scalability testing and performance validation. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `market_size` - Number of markets to simulate - /// * `user_count` - Number of users to simulate - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(BenchmarkResult)` - Complete scalability benchmark results - /// - `Err(Error)` - Error if benchmarking fails - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::Env; - /// # use predictify_hybrid::PredictifyHybrid; - /// # let env = Env::default(); - /// - /// match PredictifyHybrid::benchmark_scalability(env.clone(), 1000, 10000) { - /// Ok(result) => { - /// println!("Scalability test completed"); - /// println!("Total gas usage: {}", result.gas_usage); - /// println!("Total execution time: {}", result.execution_time); - /// }, - /// Err(e) => println!("Scalability benchmark failed: {:?}", e), - /// } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn benchmark_scalability( - env: Env, - market_size: u32, - user_count: u32, - ) -> Result { - performance_benchmarks::PerformanceBenchmarkManager::benchmark_scalability( - &env, - market_size, - user_count, - ) - } - - /// Generate comprehensive performance report - /// - /// This function creates a detailed performance report including metrics, - /// recommendations, and optimization opportunities based on benchmark results. - /// It's essential for performance analysis and optimization planning. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `benchmark_suite` - The benchmark suite to generate report for - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(PerformanceReport)` - Complete performance report with analysis - /// - `Err(Error)` - Error if report generation fails - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::Env; - /// # use predictify_hybrid::{PredictifyHybrid, PerformanceBenchmarkSuite}; - /// # let env = Env::default(); - /// # let suite = PerformanceBenchmarkSuite::default(); // Placeholder - /// - /// match PredictifyHybrid::generate_performance_report(env.clone(), suite) { - /// Ok(report) => { - /// println!("Performance report generated"); - /// println!("Overall score: {}", report.performance_metrics.overall_performance_score); - /// println!("Recommendations: {}", report.recommendations.len()); - /// }, - /// Err(e) => println!("Report generation failed: {:?}", e), - /// } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn generate_performance_report( - env: Env, - benchmark_suite: performance_benchmarks::PerformanceBenchmarkSuite, - ) -> Result { - performance_benchmarks::PerformanceBenchmarkManager::generate_performance_report( - &env, - benchmark_suite, - ) - } - - /// Validate performance against thresholds - /// - /// This function validates performance metrics against predefined thresholds - /// to ensure the contract meets performance requirements. It's essential - /// for performance validation and quality assurance. - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `metrics` - Performance metrics to validate - /// * `thresholds` - Performance thresholds to validate against - /// - /// # Returns - /// - /// Returns `Result` where: - /// - `Ok(true)` - Performance meets all thresholds - /// - `Ok(false)` - Performance does not meet thresholds - /// - `Err(Error)` - Error if validation fails - /// - /// # Example - /// - /// ```rust - /// # use soroban_sdk::Env; - /// # use predictify_hybrid::{PredictifyHybrid, PerformanceMetrics, PerformanceThresholds}; - /// # let env = Env::default(); - /// # let metrics = PerformanceMetrics::default(); // Placeholder - /// # let thresholds = PerformanceThresholds::default(); // Placeholder - /// - /// match PredictifyHybrid::validate_performance_thresholds(env.clone(), metrics, thresholds) { - /// Ok(true) => println!("Performance meets all thresholds"), - /// Ok(false) => println!("Performance does not meet thresholds"), - /// Err(e) => println!("Validation failed: {:?}", e), - /// } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn validate_performance_thresholds( - env: Env, - metrics: performance_benchmarks::PerformanceMetrics, - thresholds: performance_benchmarks::PerformanceThresholds, - ) -> Result { - performance_benchmarks::PerformanceBenchmarkManager::validate_performance_thresholds( - &env, metrics, thresholds, - ) - } - /// Get platform-wide statistics - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// Verify SAC token decimals match declared value (admin only). - /// - /// This function performs a critical security check on SAC tokens to prevent - /// denomination mistakes that have caused real on-chain losses. It verifies that - /// the token's on-chain decimals() value matches what was declared during registration. - /// - /// This can be called: - /// - Automatically during token registration (via add_global_verified/add_event_verified) - /// - Manually by admin as part of periodic audits or security reviews - /// - /// # Parameters - /// - /// * `env` - The Soroban environment for blockchain operations - /// * `admin` - The administrator address (must be authorized) - /// * `token_contract` - Address of the token contract to verify - /// * `declared_decimals` - The decimals value that was declared during registration - /// - /// # Returns - /// - /// Returns `Result<(), Error>` where: - /// - `Ok(())` - Decimals match (token is safe) - /// - `Err(Error::TokenDecimalsMismatch)` - Mismatch detected (token rejected) - /// - `Err(Error::Unauthorized)` - Caller is not admin - /// - /// # Cross-Contract Call - /// - /// This function performs a cross-contract call to the token contract's - /// `decimals()` function using the Soroban token interface. - /// - /// # Security Notes - /// - /// - Verifies via on-chain decimals() call (cannot be spoofed) - /// - Mismatch indicates potential token misconfiguration - /// - Rejected tokens cannot be used for betting/payouts - /// - All registration paths should use verified variants - /// - /// # Example - /// - /// ```rust,ignore - /// let token_contract = Address::from_string("GBUQW..."); - /// PredictifyHybrid::re_verify_token(&env, &admin, &token_contract, 7)?; - /// // Returns Ok if decimals match, TokenDecimalsMismatch error otherwise - /// ``` - /// - /// # Errors - /// - /// Returns [`Error`] when: - /// - `Error::Unauthorized` - Caller is not the contract admin - /// - `Error::TokenDecimalsMismatch` - On-chain decimals don't match declared value - /// - Other errors from cross-contract call or storage operations - /// - /// # Events - /// - /// Emits audit trail record of verification attempt (success or failure). - pub fn re_verify_token( - env: Env, - admin: Address, - token_contract: Address, - declared_decimals: u32, - ) -> Result<(), Error> { - // Verify admin authorization - Self::require_primary_admin(&env, &admin)?; - - // Create temporary asset for verification - let asset = crate::tokens::Asset { - contract: token_contract.clone(), - symbol: Symbol::new(&env, "TEMP"), - decimals: declared_decimals, - }; - - // Perform decimals verification via cross-contract call - crate::tokens::verify_token_decimals(&env, &asset)?; - - // Record verification in audit trail - crate::audit_trail::AuditTrailManager::append_record( - &env, - crate::audit_trail::AuditAction::TokenVerified, - admin.clone(), - Map::new(&env), - None, - ); - - Ok(()) - } - - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_platform_statistics(env: Env) -> PlatformStatistics { - statistics::StatisticsManager::get_platform_stats(&env) - } - - /// Get user-specific statistics - /// - /// # Errors - /// - /// This entrypoint surfaces contract errors via panic in internal calls. - /// - /// # Events - /// - /// State-changing paths may emit events through internal managers; read-only query paths emit no events. - pub fn get_user_statistics(env: Env, user: Address) -> UserStatistics { - statistics::StatisticsManager::get_user_stats(&env, &user) - } - - /// Get dashboard statistics with versioning for client compatibility - /// - /// Provides comprehensive platform-level metrics optimized for dashboard display, - /// including version information for managing client updates. - /// - /// # Returns - /// - /// * `DashboardStatisticsV1` - Versioned dashboard statistics with: - /// - API version (always 1) - /// - Platform statistics - /// - Active user count - /// - Total value locked - /// - Query timestamp - /// - /// # Errors - /// - /// Returns contract error if market traversal fails. - /// - /// # Events - /// - /// This is a read-only query; no events are emitted. - pub fn get_dashboard_statistics(env: Env) -> Result { - queries::QueryManager::get_dashboard_statistics(&env) - } - - /// Get market statistics optimized for dashboard display - /// - /// Returns comprehensive per-market metrics including participant count, - /// volume, consensus strength, and volatility for dashboard visualization. - /// - /// # Parameters - /// - /// * `market_id` - The market to query - /// - /// # Returns - /// - /// * `MarketStatisticsV1` - Market metrics with: - /// - Participant count - /// - Total volume - /// - Average stake - /// - Consensus strength (0-10000) - /// - Volatility (0-10000) - /// - Market state and question - /// - /// # Errors - /// - /// * `Error::MarketNotFound` - Market doesn't exist - /// - /// # Events - /// - /// Read-only query; no events emitted. - pub fn get_market_statistics( - env: Env, - market_id: Symbol, - ) -> Result { - queries::QueryManager::get_market_statistics(&env, market_id) - } - - /// Get category statistics for filtered dashboard views - /// - /// Provides aggregated metrics for all markets in a specific category, - /// enabling category-filtered dashboard displays and analytics. - /// - /// # Parameters - /// - /// * `category` - Category name to query - /// - /// # Returns - /// - /// * `CategoryStatisticsV1` - Category metrics with: - /// - Market count - /// - Total volume - /// - Participant count - /// - Resolved market count - /// - Average market volume - /// - /// # Events - /// - /// Read-only query; no events emitted. - pub fn get_category_statistics( - env: Env, - category: String, - ) -> Result { - queries::QueryManager::get_category_statistics(&env, category) - } - - /// Get top users by total winnings (leaderboard query) - /// - /// Returns the top N users ranked by total winnings claimed, - /// useful for leaderboard and achievement displays. - /// - /// # Parameters - /// - /// * `limit` - Maximum number of results (capped at 50 for gas safety) - /// - /// # Returns - /// - /// * `Vec` - Top users sorted by winnings (descending) - /// - /// # Notes - /// - /// Due to contract storage scanning limitations, large deployments should - /// consider off-chain indexing for leaderboard queries. - /// - /// # Events - /// - /// Read-only query; no events emitted. - pub fn get_top_users_by_winnings( - env: Env, - limit: u32, - ) -> Result, Error> { - queries::QueryManager::get_top_users_by_winnings(&env, limit) - } - - /// Get top users by win rate (skill-based leaderboard) - /// - /// Returns the top N users ranked by win rate percentage, - /// with a minimum bet requirement to filter high-variance winners. - /// - /// # Parameters - /// - /// * `limit` - Maximum number of results (capped at 50) - /// * `min_bets` - Minimum bets required for inclusion (e.g., 10) - /// - /// # Returns - /// - /// * `Vec` - Top users sorted by win rate (descending) - /// - /// # Events - /// - /// Read-only query; no events emitted. - pub fn get_top_users_by_win_rate( - env: Env, - limit: u32, - min_bets: u64, - ) -> Result, Error> { - queries::QueryManager::get_top_users_by_win_rate(&env, limit, min_bets) - } - - /// Admin-initiated circuit-breaker resume: Open → HalfOpen with cooldown. - /// - /// Moves the circuit breaker from `Open` to `HalfOpen` and records the - /// current ledger timestamp as the cooldown start. Probe requests are not - /// counted toward the success threshold until `recovery_timeout` seconds have - /// elapsed. After `half_open_max_requests` consecutive probe successes the - /// breaker auto-closes; any failure during the probe window re-opens it. - /// - /// # Errors - /// - /// - `Error::Unauthorized` — caller is not an authorised admin. - /// - `Error::CBError` — breaker is not currently `Open`. - pub fn request_resume(env: Env, admin: Address) -> Result<(), Error> { - admin.require_auth(); - crate::circuit_breaker::CircuitBreaker::request_resume(&env, &admin) - } + #[test] + fn test_budget_guard_consumed_is_non_negative() { + let env = Env::default(); + let contract_id = env.register(PredictifyHybrid, ()); - /// Return a versioned, XDR-stable snapshot of current platform statistics. - /// - /// The returned [`reporting::SnapshotEnvelope`] contains the current - /// [`reporting::PlatformStats`] serialised with `to_xdr`, tagged with - /// [`reporting::SNAPSHOT_SCHEMA_VERSION`] and the current ledger timestamp. - /// - /// # Errors - /// - /// - `Error::ContractStateError` — market index is missing or corrupted. - pub fn get_snapshot_envelope(env: Env) -> Result { - reporting::ReportingManager::get_snapshot_envelope(&env) + env.as_contract(&contract_id, || { + let guard = BudgetGuard::new(&env, 100_000); + assert!(guard.consumed() == 0); // No instructions consumed yet in test host + }); } -} - -#[cfg(any())] -mod test; +} \ No newline at end of file diff --git a/contracts/predictify-hybrid/src/resolution.rs b/contracts/predictify-hybrid/src/resolution.rs index 760534a8..36f9a833 100644 --- a/contracts/predictify-hybrid/src/resolution.rs +++ b/contracts/predictify-hybrid/src/resolution.rs @@ -1,2454 +1,204 @@ -use soroban_sdk::{contracttype, symbol_short, Address, Env, Map, String, Symbol, Vec}; - -use crate::bets::BetStorage; -use crate::err::Error; -use alloc::string::ToString; - -use crate::markets::{CommunityConsensus, MarketAnalytics, MarketStateManager, MarketUtils}; - -use crate::oracles::{OracleFactory, OracleUtils}; -// use crate::reentrancy_guard::ReentrancyGuard; // Removed - module no longer exists -use crate::types::*; - -/// Resolution management system for Predictify Hybrid contract -/// -/// This module provides a comprehensive resolution system with: -/// - Oracle resolution functions and utilities -/// - Market resolution logic and validation -/// - Resolution analytics and statistics -/// - Resolution helper utilities and testing functions -/// - Resolution state management and tracking - -// ===== RESOLUTION TYPES ===== - -/// Enumeration of possible resolution states for market lifecycle management. -/// -/// This enum tracks the progression of a market through its resolution phases, -/// from initial creation through final outcome determination. Each state represents -/// a specific stage in the resolution process with distinct validation rules and -/// available operations. -/// -/// # State Transitions -/// -/// The typical resolution flow follows this pattern: -/// ```text -/// Active → OracleResolved → MarketResolved → [Disputed] → Finalized -/// ``` -/// -/// **Alternative flows:** -/// - Direct admin resolution: `Active → MarketResolved → Finalized` -/// - Dispute flow: `MarketResolved → Disputed → Finalized` -/// - Oracle-only flow: `Active → OracleResolved → MarketResolved → Finalized` -/// -/// # Example Usage -/// -/// ```rust -/// # use soroban_sdk::{Env, Symbol}; -/// # use predictify_hybrid::resolution::{ResolutionState, ResolutionUtils}; -/// # use predictify_hybrid::markets::Market; -/// # let env = Env::default(); -/// # let market = Market::default(); // Placeholder -/// -/// // Check current resolution state -/// let current_state = ResolutionUtils::get_resolution_state(&env, &market); -/// -/// match current_state { -/// ResolutionState::Active => { -/// println!("Market is active, ready for oracle resolution"); -/// // Can fetch oracle results -/// }, -/// ResolutionState::OracleResolved => { -/// println!("Oracle result available, can proceed to market resolution"); -/// // Can combine with community consensus -/// }, -/// ResolutionState::MarketResolved => { -/// println!("Market resolved, awaiting finalization or disputes"); -/// // Can be disputed or finalized -/// }, -/// ResolutionState::Disputed => { -/// println!("Resolution is under dispute"); -/// // Dispute resolution process active -/// }, -/// ResolutionState::Finalized => { -/// println!("Resolution is final and immutable"); -/// // No further changes allowed -/// }, -/// } -/// ``` -/// -/// # State Validation -/// -/// Each state has specific validation requirements: -/// - **Active**: Market must be within voting period -/// - **OracleResolved**: Oracle data must be valid and recent -/// - **MarketResolved**: Final outcome must be determined -/// - **Disputed**: Dispute must be properly filed and active -/// - **Finalized**: Resolution must be complete and immutable -/// -/// # Business Rules -/// -/// State transitions enforce business logic: -/// - Markets cannot skip resolution states arbitrarily -/// - Finalized resolutions cannot be changed -/// - Disputed resolutions require proper dispute resolution -/// - Oracle resolution requires valid oracle data -/// -/// # Integration Points -/// -/// Resolution states integrate with: -/// - **Market Management**: Controls available market operations -/// - **Voting System**: Determines when voting periods end -/// - **Dispute System**: Manages dispute lifecycle -/// - **Oracle System**: Coordinates oracle data fetching -/// - **Admin Functions**: Enables administrative overrides -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[contracttype] -pub enum ResolutionState { - /// Market is active, no resolution yet - Active, - /// Oracle result fetched, pending final resolution - OracleResolved, - /// Market fully resolved with final outcome - MarketResolved, - /// Resolution disputed - Disputed, - /// Resolution finalized after dispute - Finalized, -} - -/// Comprehensive oracle resolution result containing all data needed for market resolution. -/// -/// This structure captures the complete oracle response for a market, including -/// the raw price data, comparison logic, outcome determination, and metadata -/// necessary for validation and audit trails. -/// -/// # Core Components -/// -/// **Market Context:** -/// - **Market ID**: Unique identifier linking resolution to specific market -/// - **Timestamp**: When the oracle resolution was performed -/// - **Provider**: Which oracle service provided the data -/// -/// **Oracle Data:** -/// - **Price**: Current asset price from oracle feed -/// - **Threshold**: Market-defined price threshold for comparison -/// - **Comparison**: Comparison operator ("gt", "lt", "eq") -/// - **Feed ID**: Specific oracle feed identifier used -/// -/// **Resolution Result:** -/// - **Oracle Result**: Final outcome ("yes"/"no") based on price comparison -/// -/// # Example Usage -/// -/// ```rust -/// # use soroban_sdk::{Env, Symbol, String, Address}; -/// # use predictify_hybrid::resolution::{OracleResolutionManager, OracleResolution}; -/// # use predictify_hybrid::types::OracleProvider; -/// # let env = Env::default(); -/// # let market_id = Symbol::new(&env, "btc_50k"); -/// # let oracle_contract = Address::generate(&env); -/// -/// // Fetch oracle resolution for a market -/// let oracle_resolution = OracleResolutionManager::fetch_oracle_result( -/// &env, -/// &market_id, -/// &oracle_contract -/// )?; -/// -/// // Examine oracle resolution details -/// println!("Market: {}", oracle_resolution.market_id); -/// println!("Oracle result: {}", oracle_resolution.oracle_result); -/// println!("Price: ${}", oracle_resolution.price / 100); -/// println!("Threshold: ${}", oracle_resolution.threshold / 100); -/// println!("Comparison: {}", oracle_resolution.comparison); -/// println!("Provider: {:?}", oracle_resolution.provider); -/// println!("Feed: {}", oracle_resolution.feed_id); -/// -/// // Validate oracle resolution -/// OracleResolutionManager::validate_oracle_resolution(&env, &oracle_resolution)?; -/// -/// // Calculate confidence score -/// let confidence = OracleResolutionManager::calculate_oracle_confidence(&oracle_resolution); -/// println!("Oracle confidence: {}%", confidence); -/// # Ok::<(), predictify_hybrid::errors::Error>(()) -/// ``` -/// -/// # Price Comparison Logic -/// -/// The oracle resolution evaluates market conditions: -/// ```rust -/// # use soroban_sdk::{Env, String}; -/// # use predictify_hybrid::oracles::OracleUtils; -/// # let env = Env::default(); -/// -/// // Example: BTC above $50,000? -/// let btc_price = 52_000_00; // $52,000 (8 decimal precision) -/// let threshold = 50_000_00; // $50,000 -/// let comparison = String::from_str(&env, "gt"); // Greater than -/// -/// let outcome = OracleUtils::determine_outcome( -/// btc_price, -/// threshold, -/// &comparison, -/// &env -/// )?; -/// -/// assert_eq!(outcome, String::from_str(&env, "yes")); // BTC > $50k = "yes" -/// # Ok::<(), predictify_hybrid::errors::Error>(()) -/// ``` -/// -/// # Validation Requirements -/// -/// Oracle resolutions must meet criteria: -/// - **Valid Price**: Price must be positive and within reasonable bounds -/// - **Recent Data**: Timestamp must be within acceptable staleness limits -/// - **Supported Provider**: Oracle provider must be supported on current network -/// - **Valid Feed**: Feed ID must exist and be active -/// - **Proper Comparison**: Comparison operator must be supported -/// -/// # Integration with Market Resolution -/// -/// Oracle resolutions feed into broader market resolution: -/// - **Hybrid Resolution**: Combined with community consensus -/// - **Oracle-Only**: Used directly as final outcome -/// - **Dispute Input**: Provides data for dispute resolution -/// - **Confidence Scoring**: Contributes to overall resolution confidence -/// -/// # Audit and Transparency -/// -/// All oracle resolution data is preserved for: -/// - **Audit Trails**: Complete record of resolution process -/// - **Dispute Evidence**: Data available for dispute proceedings -/// - **Analytics**: Historical analysis of oracle performance -/// - **Transparency**: Public verification of resolution logic -#[derive(Clone, Debug)] -#[contracttype] -pub struct OracleResolution { - pub market_id: Symbol, - pub oracle_result: String, - pub price: i128, - pub threshold: i128, - pub comparison: String, - pub timestamp: u64, - pub provider: OracleProvider, - pub feed_id: String, -} - -/// Comprehensive market resolution result combining oracle data with community consensus. -/// -/// This structure represents the final resolution of a prediction market, incorporating -/// data from multiple sources (oracle feeds, community voting, admin decisions) to -/// determine the authoritative market outcome with confidence scoring and audit trails. -/// -/// # Resolution Components -/// -/// **Core Resolution Data:** -/// - **Market ID**: Unique identifier for the resolved market -/// - **Final Outcome**: Definitive market result ("yes"/"no" or custom outcomes) -/// - **Resolution Timestamp**: When the resolution was finalized -/// - **Resolution Method**: How the resolution was determined -/// -/// **Data Sources:** -/// - **Oracle Result**: Outcome from oracle price feeds -/// - **Community Consensus**: Aggregated community voting results -/// - **Confidence Score**: Statistical confidence in the resolution (0-100) -/// -/// # Resolution Methods -/// -/// Markets can be resolved through various methods: -/// - **Oracle Only**: Based purely on oracle price data -/// - **Community Only**: Based on community voting consensus -/// - **Hybrid**: Combines oracle data with community input -/// - **Admin Override**: Administrative decision overrides other methods -/// - **Dispute Resolution**: Outcome determined through dispute process -/// -/// # Example Usage -/// -/// ```rust -/// # use soroban_sdk::{Env, Symbol, String}; -/// # use predictify_hybrid::resolution::{MarketResolutionManager, MarketResolution, ResolutionMethod}; -/// # let env = Env::default(); -/// # let market_id = Symbol::new(&env, "btc_prediction"); -/// -/// // Resolve a market using hybrid method -/// let resolution = MarketResolutionManager::resolve_market(&env, &market_id)?; -/// -/// // Examine resolution details -/// println!("Market: {}", resolution.market_id); -/// println!("Final outcome: {}", resolution.final_outcome); -/// println!("Oracle result: {}", resolution.oracle_result); -/// println!("Community consensus: {}% ({})", -/// resolution.community_consensus.percentage, -/// resolution.community_consensus.outcome -/// ); -/// println!("Resolution method: {:?}", resolution.resolution_method); -/// println!("Confidence: {}%", resolution.confidence_score); -/// -/// // Validate the resolution -/// MarketResolutionManager::validate_market_resolution(&env, &resolution)?; -/// -/// // Check resolution method -/// match resolution.resolution_method { -/// ResolutionMethod::Hybrid => { -/// println!("Resolution combines oracle and community data"); -/// }, -/// ResolutionMethod::OracleOnly => { -/// println!("Resolution based purely on oracle data"); -/// }, -/// ResolutionMethod::AdminOverride => { -/// println!("Resolution was administratively determined"); -/// }, -/// _ => println!("Other resolution method used"), -/// } -/// # Ok::<(), predictify_hybrid::errors::Error>(()) -/// ``` -/// -/// # Confidence Scoring -/// -/// Resolution confidence is calculated based on: -/// - **Oracle Reliability**: Historical oracle accuracy and freshness -/// - **Community Agreement**: Level of consensus in community voting -/// - **Data Quality**: Quality and recency of underlying data -/// - **Method Reliability**: Inherent reliability of resolution method -/// -/// ```rust -/// # use predictify_hybrid::resolution::MarketResolution; -/// # let resolution = MarketResolution::default(); // Placeholder -/// -/// // Interpret confidence scores -/// match resolution.confidence_score { -/// 90..=100 => println!("Very high confidence resolution"), -/// 80..=89 => println!("High confidence resolution"), -/// 70..=79 => println!("Moderate confidence resolution"), -/// 60..=69 => println!("Low confidence resolution"), -/// _ => println!("Very low confidence - may need review"), -/// } -/// ``` -/// -/// # Resolution Validation -/// -/// Market resolutions undergo validation to ensure: -/// - **Outcome Consistency**: Oracle and community data alignment -/// - **Method Appropriateness**: Resolution method suitable for market type -/// - **Data Quality**: All input data meets quality standards -/// - **Timestamp Validity**: Resolution timing is appropriate -/// - **Confidence Thresholds**: Confidence score meets minimum requirements -/// -/// # Integration Points -/// -/// Market resolutions integrate with: -/// - **Payout System**: Determines winner payouts and distributions -/// - **Dispute System**: Can be challenged through dispute mechanisms -/// - **Analytics**: Contributes to platform performance metrics -/// - **Audit System**: Provides complete resolution audit trails -/// - **Event System**: Triggers resolution events for transparency -/// -/// # Immutability and Finalization -/// -/// Once finalized, market resolutions are immutable except through: -/// - **Dispute Process**: Formal dispute resolution procedures -/// - **Admin Override**: Emergency administrative corrections -/// - **System Upgrades**: Protocol-level corrections (rare) -#[derive(Clone, Debug)] -#[contracttype] -pub struct MarketResolution { - pub market_id: Symbol, - pub final_outcome: String, - pub oracle_result: String, - pub community_consensus: CommunityConsensus, - pub resolution_timestamp: u64, - pub resolution_method: ResolutionMethod, - pub confidence_score: u32, -} - -/// Enumeration of available market resolution methods and their characteristics. -/// -/// This enum defines the different approaches available for resolving prediction markets, -/// each with distinct data sources, validation requirements, and confidence characteristics. -/// The choice of resolution method depends on market type, data availability, and -/// community participation levels. -/// -/// # Resolution Method Types -/// -/// **Automated Methods:** -/// - **Oracle Only**: Purely algorithmic based on price feed data -/// - **Community Only**: Based entirely on community voting consensus -/// - **Hybrid**: Combines oracle data with community input for balanced resolution -/// -/// **Manual Methods:** -/// - **Admin Override**: Administrative decision for exceptional circumstances -/// - **Dispute Resolution**: Outcome determined through formal dispute process -/// -/// # Method Selection Logic -/// -/// Resolution methods are typically selected based on: -/// ```rust -/// # use predictify_hybrid::resolution::ResolutionMethod; -/// # use predictify_hybrid::markets::CommunityConsensus; -/// # use soroban_sdk::{Env, String}; -/// # let env = Env::default(); -/// -/// // Example method selection logic -/// fn select_resolution_method( -/// oracle_available: bool, -/// community_participation: u32, -/// consensus_strength: u32 -/// ) -> ResolutionMethod { -/// match (oracle_available, community_participation, consensus_strength) { -/// (true, participation, consensus) if participation > 50 && consensus > 75 => { -/// ResolutionMethod::Hybrid // Strong community + oracle -/// }, -/// (true, participation, _) if participation < 30 => { -/// ResolutionMethod::OracleOnly // Low community participation -/// }, -/// (false, participation, consensus) if participation > 100 && consensus > 80 => { -/// ResolutionMethod::CommunityOnly // No oracle, strong community -/// }, -/// _ => ResolutionMethod::AdminOverride // Fallback to admin -/// } -/// } -/// ``` -/// -/// # Example Usage -/// -/// ```rust -/// # use soroban_sdk::{Env, String}; -/// # use predictify_hybrid::resolution::{ResolutionMethod, MarketResolutionAnalytics}; -/// # use predictify_hybrid::markets::CommunityConsensus; -/// # let env = Env::default(); -/// -/// // Determine resolution method based on available data -/// let oracle_result = String::from_str(&env, "yes"); -/// let community_consensus = CommunityConsensus { -/// outcome: String::from_str(&env, "yes"), -/// votes: 150, -/// total_votes: 200, -/// percentage: 75, -/// }; -/// -/// let method = MarketResolutionAnalytics::determine_resolution_method( -/// &oracle_result, -/// &community_consensus -/// ); -/// -/// match method { -/// ResolutionMethod::Hybrid => { -/// println!("Using hybrid resolution - oracle and community agree"); -/// }, -/// ResolutionMethod::OracleOnly => { -/// println!("Using oracle-only resolution - low community participation"); -/// }, -/// ResolutionMethod::CommunityOnly => { -/// println!("Using community-only resolution - oracle unavailable"); -/// }, -/// ResolutionMethod::AdminOverride => { -/// println!("Using admin override - exceptional circumstances"); -/// }, -/// ResolutionMethod::DisputeResolution => { -/// println!("Using dispute resolution - conflicting data sources"); -/// }, -/// } -/// ``` -/// -/// # Method Characteristics -/// -/// **Oracle Only:** -/// - **Speed**: Fastest resolution method -/// - **Objectivity**: Purely algorithmic, no human bias -/// - **Reliability**: Depends on oracle data quality -/// - **Use Case**: Clear-cut price-based markets -/// -/// **Community Only:** -/// - **Participation**: Requires active community engagement -/// - **Flexibility**: Can handle subjective or complex outcomes -/// - **Consensus**: Relies on community agreement -/// - **Use Case**: Subjective or oracle-unavailable markets -/// -/// **Hybrid:** -/// - **Balance**: Combines objective data with community wisdom -/// - **Validation**: Cross-validates oracle data with community input -/// - **Confidence**: Generally highest confidence scores -/// - **Use Case**: Most standard prediction markets -/// -/// **Admin Override:** -/// - **Authority**: Administrative decision with full authority -/// - **Speed**: Can be immediate when needed -/// - **Responsibility**: Requires admin accountability -/// - **Use Case**: Emergency situations or system failures -/// -/// **Dispute Resolution:** -/// - **Process**: Formal dispute resolution procedures -/// - **Thoroughness**: Most comprehensive review process -/// - **Time**: Longest resolution time -/// - **Use Case**: Contested or controversial outcomes -/// -/// # Integration with Confidence Scoring -/// -/// Different methods contribute to confidence scores: -/// - **Hybrid**: Highest confidence when oracle and community agree -/// - **Oracle Only**: High confidence for clear price-based outcomes -/// - **Community Only**: Confidence based on participation and consensus -/// - **Admin Override**: Confidence based on admin justification -/// - **Dispute Resolution**: Confidence based on dispute outcome strength -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[contracttype] -pub enum ResolutionMethod { - /// Oracle only resolution - OracleOnly, - /// Community consensus only - CommunityOnly, - /// Hybrid oracle + community - Hybrid, - /// Admin override - AdminOverride, - /// Dispute resolution - DisputeResolution, -} - -/// Precomputed payout totals persisted at resolution time (O(1) reads on claim/distribute). -/// -/// Built once when winning outcomes are set; invalidated when outcomes or pool change. -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ResolvedOutcomeSummary { - /// Sum of winning-side stakes (votes + bets, deduplicated). - pub winning_total: i128, - /// Total market pool at resolution (`market.total_staked`). - pub total_pool: i128, - /// Number of winning outcomes (tie split divisor). - pub num_winning_outcomes: u32, -} - -/// Storage-backed cache for resolved market payout math. -/// -/// Time: O(V + B) once at `refresh`; O(1) on payout paths. -/// Space: O(1) per market (single summary struct). -pub struct ResolutionOutcomeCache; - -impl ResolutionOutcomeCache { - fn storage_key(market_id: &Symbol) -> (Symbol, Symbol) { - (symbol_short!("res_out"), market_id.clone()) - } - - /// Remove cached summary (e.g. before outcome override). - pub fn invalidate(env: &Env, market_id: &Symbol) { - env.storage() - .persistent() - .remove(&Self::storage_key(market_id)); - } - - /// Compute winning total with explicit `market_id` (bet registry key). - pub fn compute_winning_total_for_market( - env: &Env, - market_id: &Symbol, - market: &Market, - winning_outcomes: &Vec, - ) -> Result { - let mut winning_total: i128 = 0; - - for (voter, outcome) in market.votes.iter() { - if winning_outcomes.contains(&outcome) { - winning_total = winning_total - .checked_add(market.stakes.get(voter.clone()).unwrap_or(0)) - .ok_or(Error::InvalidInput)?; - } - } - - let bettors = BetStorage::get_all_bets_for_market(env, market_id); - for user in bettors.iter() { - if market.votes.contains_key(user.clone()) { - continue; - } - if let Some(bet) = BetStorage::get_bet(env, market_id, &user) { - if winning_outcomes.contains(&bet.outcome) { - winning_total = winning_total - .checked_add(bet.amount) - .ok_or(Error::InvalidInput)?; - } - } - } - - Ok(winning_total) - } - - /// Recompute and persist summary after resolution or outcome change. - pub fn refresh(env: &Env, market_id: &Symbol, market: &Market) -> Result<(), Error> { - let winning_outcomes = market - .winning_outcomes - .as_ref() - .ok_or(Error::MarketNotResolved)?; - - let winning_total = - Self::compute_winning_total_for_market(env, market_id, market, winning_outcomes)?; - - let summary = ResolvedOutcomeSummary { - winning_total, - total_pool: market.total_staked, - num_winning_outcomes: winning_outcomes.len(), - }; - - env.storage() - .persistent() - .set(&Self::storage_key(market_id), &summary); - - Ok(()) - } - - pub fn get(env: &Env, market_id: &Symbol) -> Option { - env.storage() - .persistent() - .get(&Self::storage_key(market_id)) - } +pub fn distribute_payouts(env: soroban_sdk::Env, market_id: soroban_sdk::Symbol) -> Result { + if let Err(e) = circuit_breaker::CircuitBreaker::require_write_allowed( + &env, + "distribute_payouts", + ) { + return Err(e); + } + + let mut market: Market = env + .storage() + .persistent() + .get(&market_id) + .unwrap_or_else(|| { + soroban_sdk::panic_with_error!(env, Error::MarketNotFound); + }); - /// Return cached summary or refresh if missing/stale. - pub fn require( - env: &Env, - market_id: &Symbol, - market: &Market, - ) -> Result { - if let (Some(summary), Some(ref outcomes)) = - (Self::get(env, market_id), &market.winning_outcomes) - { - if summary.total_pool == market.total_staked - && summary.num_winning_outcomes == outcomes.len() + // Check if market is resolved + let winning_outcomes = match &market.winning_outcomes { + Some(outcomes) => outcomes, + None => return Err(Error::MarketNotResolved), + }; + + // Get all bettors + let bettors = bets::BetStorage::get_all_bets_for_market(&env, &market_id); + + // Get fee from legacy storage (backward compatible) + let fee_percent = env + .storage() + .persistent() + .get(&Symbol::new(&env, "platform_fee")) + .unwrap_or(200); + + let mut has_unclaimed_winners = false; + + // Check voters + for (user, outcome) in market.votes.iter() { + if winning_outcomes.contains(&outcome) { + if !market + .claimed + .get((*user).clone()) + .map(|info| info.is_claimed()) + .unwrap_or(false) { - return Ok(summary); + has_unclaimed_winners = true; + break; } } - Self::refresh(env, market_id, market)?; - Self::get(env, market_id).ok_or(Error::MarketNotResolved) - } -} - -/// Comprehensive analytics and metrics for resolution system performance. -/// -/// This structure tracks detailed statistics about the resolution system's -/// performance, method usage, timing characteristics, and outcome distributions. -/// It provides essential data for system optimization, transparency reporting, -/// and platform analytics. -/// -/// # Analytics Categories -/// -/// **Volume Metrics:** -/// - **Total Resolutions**: Overall count of resolved markets -/// - **Method Breakdown**: Count by resolution method type -/// - **Outcome Distribution**: Frequency of different outcomes -/// -/// **Quality Metrics:** -/// - **Average Confidence**: Mean confidence score across resolutions -/// - **Resolution Times**: Time taken for different resolution methods -/// - **Success Rates**: Percentage of successful resolutions by method -/// -/// # Example Usage -/// -/// ```rust -/// # use soroban_sdk::{Env, Map, String, Vec}; -/// # use predictify_hybrid::resolution::{ResolutionAnalytics, ResolutionAnalyticsManager}; -/// # let env = Env::default(); -/// -/// // Get current resolution analytics -/// let analytics = ResolutionAnalyticsManager::get_resolution_analytics(&env)?; -/// -/// // Display system performance metrics -/// println!("=== Resolution System Analytics ==="); -/// println!("Total resolutions: {}", analytics.total_resolutions); -/// println!("Oracle resolutions: {}", analytics.oracle_resolutions); -/// println!("Community resolutions: {}", analytics.community_resolutions); -/// println!("Hybrid resolutions: {}", analytics.hybrid_resolutions); -/// println!("Average confidence: {}%", analytics.average_confidence / 100); -/// -/// // Calculate method distribution -/// let total = analytics.total_resolutions as f64; -/// if total > 0.0 { -/// println!("Oracle-only: {:.1}%", (analytics.oracle_resolutions as f64 / total) * 100.0); -/// println!("Community-only: {:.1}%", (analytics.community_resolutions as f64 / total) * 100.0); -/// println!("Hybrid: {:.1}%", (analytics.hybrid_resolutions as f64 / total) * 100.0); -/// } -/// -/// // Analyze resolution times -/// if !analytics.resolution_times.is_empty() { -/// let avg_time = analytics.resolution_times.iter().sum::() / analytics.resolution_times.len() as u64; -/// println!("Average resolution time: {} seconds", avg_time); -/// } -/// -/// // Display outcome distribution -/// for (outcome, count) in analytics.outcome_distribution.iter() { -/// println!("Outcome '{}': {} markets", outcome, count); -/// } -/// # Ok::<(), predictify_hybrid::errors::Error>(()) -/// ``` -/// -/// # Performance Monitoring -/// -/// Analytics enable monitoring of: -/// ```rust -/// # use predictify_hybrid::resolution::ResolutionAnalytics; -/// # let analytics = ResolutionAnalytics::default(); -/// -/// // Monitor system health -/// fn assess_system_health(analytics: &ResolutionAnalytics) -> String { -/// let confidence_threshold = 80_00; // 80% in basis points -/// let hybrid_ratio = if analytics.total_resolutions > 0 { -/// (analytics.hybrid_resolutions as f64 / analytics.total_resolutions as f64) * 100.0 -/// } else { -/// 0.0 -/// }; -/// -/// match (analytics.average_confidence >= confidence_threshold, hybrid_ratio >= 50.0) { -/// (true, true) => "Excellent - High confidence and balanced resolution methods".to_string(), -/// (true, false) => "Good - High confidence but method imbalance".to_string(), -/// (false, true) => "Fair - Balanced methods but lower confidence".to_string(), -/// (false, false) => "Needs attention - Low confidence and method imbalance".to_string(), -/// } -/// } -/// ``` -/// -/// # Trend Analysis -/// -/// Resolution analytics support trend analysis: -/// - **Method Evolution**: How resolution method preferences change over time -/// - **Confidence Trends**: Whether resolution confidence is improving -/// - **Outcome Patterns**: Distribution of market outcomes -/// - **Performance Optimization**: Identifying areas for system improvement -/// -/// # Business Intelligence -/// -/// Analytics provide insights for: -/// - **Platform Performance**: Overall system effectiveness metrics -/// - **User Behavior**: How community participates in resolution -/// - **Oracle Reliability**: Performance of different oracle providers -/// - **Market Types**: Which market types work best with which methods -/// -/// # Data Privacy and Aggregation -/// -/// Analytics maintain privacy through: -/// - **Aggregated Data**: No individual user information exposed -/// - **Statistical Summaries**: Focus on system-level metrics -/// - **Time-based Aggregation**: Historical trends without personal data -/// - **Public Transparency**: Safe for public consumption -/// -/// # Integration with Reporting -/// -/// Resolution analytics integrate with: -/// - **Dashboard Systems**: Real-time performance monitoring -/// - **Audit Reports**: Compliance and transparency reporting -/// - **API Endpoints**: External system integration -/// - **Governance Metrics**: DAO governance decision support -#[derive(Clone, Debug)] -#[contracttype] -pub struct ResolutionAnalytics { - pub total_resolutions: u32, - pub oracle_resolutions: u32, - pub community_resolutions: u32, - pub hybrid_resolutions: u32, - pub average_confidence: i128, - pub resolution_times: Vec, - pub outcome_distribution: Map, -} - -/// Comprehensive validation result for resolution processes and outcomes. -/// -/// This structure provides detailed feedback on the validity of resolution attempts, -/// including validation status, specific error conditions, warnings about potential -/// issues, and recommendations for improvement. It serves as a comprehensive -/// diagnostic tool for resolution quality assurance. -/// -/// # Validation Components -/// -/// **Status Indicators:** -/// - **Is Valid**: Boolean indicating overall validation success -/// - **Errors**: Critical issues that prevent resolution -/// - **Warnings**: Non-critical issues that should be addressed -/// - **Recommendations**: Suggestions for improving resolution quality -/// -/// # Validation Categories -/// -/// **Data Quality Validation:** -/// - Oracle data freshness and accuracy -/// - Community voting participation levels -/// - Consensus strength and distribution -/// - Timestamp validity and sequencing -/// -/// **Business Logic Validation:** -/// - Market state compatibility with resolution method -/// - Outcome consistency across data sources -/// - Confidence score reasonableness -/// - Resolution method appropriateness -/// -/// # Example Usage -/// -/// ```rust -/// # use soroban_sdk::{Env, Vec, String}; -/// # use predictify_hybrid::resolution::{ResolutionValidation, MarketResolutionManager, MarketResolution}; -/// # let env = Env::default(); -/// # let resolution = MarketResolution::default(); // Placeholder -/// -/// // Validate a market resolution -/// let validation = MarketResolutionManager::validate_market_resolution(&env, &resolution)?; -/// -/// if validation.is_valid { -/// println!("✅ Resolution is valid and ready for finalization"); -/// -/// // Check for warnings -/// if !validation.warnings.is_empty() { -/// println!("⚠️ Warnings to consider:"); -/// for warning in validation.warnings.iter() { -/// println!(" - {}", warning); -/// } -/// } -/// -/// // Review recommendations -/// if !validation.recommendations.is_empty() { -/// println!("💡 Recommendations for improvement:"); -/// for recommendation in validation.recommendations.iter() { -/// println!(" - {}", recommendation); -/// } -/// } -/// } else { -/// println!("❌ Resolution validation failed"); -/// println!("Errors that must be resolved:"); -/// for error in validation.errors.iter() { -/// println!(" - {}", error); -/// } -/// } -/// # Ok::<(), predictify_hybrid::errors::Error>(()) -/// ``` -/// -/// # Validation Workflow -/// -/// ```rust -/// # use predictify_hybrid::resolution::{ResolutionValidation, OracleResolution}; -/// # use soroban_sdk::{Env, Vec, String}; -/// # let env = Env::default(); -/// -/// // Example validation workflow -/// fn comprehensive_validation_workflow( -/// env: &Env, -/// oracle_resolution: &OracleResolution -/// ) -> Result { -/// // Step 1: Validate oracle resolution -/// let oracle_validation = validate_oracle_data(env, oracle_resolution)?; -/// -/// if !oracle_validation.is_valid { -/// println!("Oracle validation failed: {:?}", oracle_validation.errors); -/// return Ok(false); -/// } -/// -/// // Step 2: Check for warnings -/// if !oracle_validation.warnings.is_empty() { -/// println!("Oracle warnings: {:?}", oracle_validation.warnings); -/// } -/// -/// // Step 3: Apply recommendations if possible -/// for recommendation in oracle_validation.recommendations.iter() { -/// println!("Consider: {}", recommendation); -/// } -/// -/// Ok(true) -/// } -/// -/// fn validate_oracle_data( -/// _env: &Env, -/// _oracle_resolution: &OracleResolution -/// ) -> Result { -/// // Placeholder implementation -/// Ok(ResolutionValidation { -/// is_valid: true, -/// errors: Vec::new(_env), -/// warnings: Vec::new(_env), -/// recommendations: Vec::new(_env), -/// }) -/// } -/// ``` -/// -/// # Error Categories -/// -/// **Critical Errors (Block Resolution):** -/// - Invalid oracle data or stale timestamps -/// - Insufficient community participation -/// - Conflicting outcomes without resolution method -/// - Missing required data for chosen resolution method -/// -/// **Warnings (Proceed with Caution):** -/// - Low confidence scores -/// - Minimal community participation -/// - Oracle data approaching staleness limits -/// - Unusual outcome patterns -/// -/// **Recommendations (Optimization):** -/// - Increase community engagement -/// - Use hybrid resolution for better confidence -/// - Consider additional oracle sources -/// - Implement dispute period for controversial outcomes -/// -/// # Integration with Resolution Process -/// -/// Validation integrates at multiple points: -/// - **Pre-Resolution**: Validate readiness before attempting resolution -/// - **Post-Resolution**: Validate outcome quality and consistency -/// - **Dispute Handling**: Validate dispute claims and evidence -/// - **Finalization**: Final validation before immutable storage -/// -/// # Quality Assurance -/// -/// Validation supports quality assurance through: -/// - **Automated Checks**: Systematic validation of all resolution components -/// - **Consistency Verification**: Cross-validation between data sources -/// - **Business Rule Enforcement**: Ensure compliance with platform rules -/// - **Audit Trail Generation**: Document validation decisions and rationale -#[derive(Clone, Debug)] -#[contracttype] -pub struct ResolutionValidation { - pub is_valid: bool, - pub errors: Vec, - pub warnings: Vec, - pub recommendations: Vec, -} - -// ===== ORACLE RESOLUTION ===== - -/// Comprehensive oracle resolution management system for prediction markets. -/// -/// The Oracle Resolution Manager handles all aspects of oracle-based market resolution, -/// including fetching oracle data, validating oracle responses, calculating confidence -/// scores, and managing the oracle resolution lifecycle. It serves as the primary -/// interface between the prediction market system and external oracle providers. -/// -/// # Core Responsibilities -/// -/// **Oracle Data Management:** -/// - **Data Fetching**: Retrieve price data from configured oracle providers -/// - **Data Validation**: Ensure oracle responses meet quality standards -/// - **Confidence Scoring**: Calculate reliability scores for oracle data -/// - **Error Handling**: Manage oracle failures and fallback strategies -/// -/// **Market Integration:** -/// - **Market Validation**: Ensure markets are ready for oracle resolution -/// - **Outcome Determination**: Convert oracle data to market outcomes -/// - **Resolution Storage**: Persist oracle resolution results -/// - **Event Emission**: Notify system of oracle resolution events -/// -/// # Oracle Resolution Process -/// -/// The typical oracle resolution workflow: -/// ```text -/// 1. Validate Market → 2. Fetch Oracle Data → 3. Validate Response → -/// 4. Calculate Outcome → 5. Score Confidence → 6. Store Resolution -/// ``` -/// -/// # Example Usage -/// -/// ```rust -/// # use soroban_sdk::{Env, Symbol, Address}; -/// # use predictify_hybrid::resolution::{OracleResolutionManager, OracleResolution}; -/// # let env = Env::default(); -/// # let market_id = Symbol::new(&env, "btc_50k_market"); -/// # let oracle_contract = Address::generate(&env); -/// -/// // Fetch oracle resolution for a market -/// let oracle_resolution = OracleResolutionManager::fetch_oracle_result( -/// &env, -/// &market_id, -/// &oracle_contract -/// )?; -/// -/// println!("Oracle Resolution Results:"); -/// println!("Market: {}", oracle_resolution.market_id); -/// println!("Result: {}", oracle_resolution.oracle_result); -/// println!("Price: ${}", oracle_resolution.price / 100); -/// println!("Threshold: ${}", oracle_resolution.threshold / 100); -/// println!("Provider: {:?}", oracle_resolution.provider); -/// -/// // Validate the oracle resolution -/// OracleResolutionManager::validate_oracle_resolution(&env, &oracle_resolution)?; -/// -/// // Calculate confidence score -/// let confidence = OracleResolutionManager::calculate_oracle_confidence(&oracle_resolution); -/// println!("Oracle confidence: {}%", confidence); -/// -/// // Store resolution for later retrieval -/// // (Implementation would store in contract storage) -/// -/// // Retrieve stored resolution -/// if let Some(stored_resolution) = OracleResolutionManager::get_oracle_resolution( -/// &env, -/// &market_id -/// )? { -/// println!("Successfully retrieved stored oracle resolution"); -/// } -/// # Ok::<(), predictify_hybrid::errors::Error>(()) -/// ``` -/// -/// # Oracle Provider Integration -/// -/// The manager integrates with multiple oracle providers: -/// ```rust -/// # use soroban_sdk::{Env, Address}; -/// # use predictify_hybrid::oracles::{OracleFactory, OracleInstance}; -/// # use predictify_hybrid::types::OracleProvider; -/// # let env = Env::default(); -/// # let oracle_contract = Address::generate(&env); -/// -/// // Create oracle instance based on provider -/// let oracle = OracleFactory::create_oracle( -/// OracleProvider::reflector(), // Primary provider for Stellar -/// oracle_contract -/// )?; -/// -/// // Use oracle for price fetching -/// match oracle { -/// OracleInstance::Reflector(reflector_oracle) => { -/// println!("Using Reflector oracle for price data"); -/// // Reflector-specific operations -/// }, -/// OracleInstance::Pyth(pyth_oracle) => { -/// println!("Using Pyth oracle (future implementation)"); -/// // Pyth-specific operations -/// }, -/// } -/// # Ok::<(), predictify_hybrid::errors::Error>(()) -/// ``` -/// -/// # Confidence Scoring Algorithm -/// -/// Oracle confidence is calculated based on: -/// - **Data Freshness**: How recent the oracle data is -/// - **Provider Reliability**: Historical accuracy of the oracle provider -/// - **Price Stability**: Volatility and consistency of price data -/// - **Network Health**: Oracle network status and availability -/// -/// ```rust -/// # use predictify_hybrid::resolution::{OracleResolution, OracleResolutionManager}; -/// # let oracle_resolution = OracleResolution::default(); // Placeholder -/// -/// // Confidence scoring factors -/// let confidence = OracleResolutionManager::calculate_oracle_confidence(&oracle_resolution); -/// -/// match confidence { -/// 90..=100 => println!("Very high confidence - excellent oracle data"), -/// 80..=89 => println!("High confidence - reliable oracle data"), -/// 70..=79 => println!("Moderate confidence - acceptable oracle data"), -/// 60..=69 => println!("Low confidence - oracle data has issues"), -/// _ => println!("Very low confidence - oracle data unreliable"), -/// } -/// ``` -/// -/// # Error Handling and Fallbacks -/// -/// The manager handles various error scenarios: -/// - **Oracle Unavailable**: Network issues or service downtime -/// - **Invalid Data**: Malformed or unreasonable oracle responses -/// - **Stale Data**: Oracle data older than acceptable thresholds -/// - **Feed Errors**: Requested price feed not available -/// -/// # Integration with Market Resolution -/// -/// Oracle resolutions feed into broader market resolution: -/// - **Hybrid Resolution**: Combined with community consensus -/// - **Oracle-Only Markets**: Direct outcome determination -/// - **Dispute Evidence**: Oracle data used in dispute resolution -/// - **Confidence Weighting**: Oracle confidence affects final resolution confidence -/// -/// # Performance and Optimization -/// -/// The manager optimizes performance through: -/// - **Caching**: Cache oracle responses to reduce network calls -/// - **Batch Processing**: Handle multiple markets efficiently -/// - **Async Operations**: Non-blocking oracle data fetching -/// - **Fallback Strategies**: Multiple oracle providers for reliability -pub struct OracleResolutionManager; - -impl OracleResolutionManager { - /// Helper to fetch price and determine outcome from an oracle config - fn try_fetch_from_config( - env: &Env, - market_id: &Symbol, - config: &crate::types::OracleConfig, - ) -> Result<(i128, String), Error> { - let oracle = - OracleFactory::create_oracle(config.provider.clone(), config.oracle_address.clone())?; - - let price_data = oracle.get_price_data(env, &config.feed_id)?; - crate::oracles::OracleValidationConfigManager::validate_oracle_data( - env, - market_id, - &config.provider, - &config.feed_id, - &price_data, - )?; - - let outcome = OracleUtils::determine_outcome( - price_data.price, - config.threshold, - &config.comparison, - env, - )?; - - Ok((price_data.price, outcome)) } - /// Fetch oracle result for a market with deterministic fallback ordering and timeout handling. - /// - /// The resolver attempts the primary oracle once. When `has_fallback` is `true`, it attempts the - /// fallback oracle once only after that primary failure. No oracle calls are made once - /// `ledger.timestamp() >= end_time + resolution_timeout`. - pub fn fetch_oracle_result(env: &Env, market_id: &Symbol) -> Result { - // Get the market from storage - let mut market = MarketStateManager::get_market(env, market_id)?; - - // 1. Check if resolution timeout has been reached. - // - // Safety invariant: a market with an active dispute must NOT be cancelled by the - // oracle resolution timeout. Cancelling while a dispute is open would permanently - // lock the dispute stakes and leave the market in an unresolvable state (deadlock). - // Instead we surface `ResolutionTimeoutReached` so the caller knows the oracle path - // is closed while the dispute process remains the authoritative resolution path. - let current_time = env.ledger().timestamp(); - if current_time >= market.end_time.saturating_add(market.resolution_timeout) { - crate::events::EventEmitter::emit_resolution_timeout(env, market_id, current_time); - return Err(Error::ResolutionTimeoutReached); - } - - // Validate market for oracle resolution - OracleResolutionValidator::validate_market_for_oracle_resolution(env, &market)?; - - // 2. Try primary oracle - let mut used_config = market.oracle_config.clone(); - let primary_result = Self::try_fetch_from_config(env, market_id, &used_config); - - let (price, outcome) = match primary_result { - Ok(res) => res, - Err(_) => { - // 3. Try fallback oracle if primary fails - if market.has_fallback { - let fallback_config = &market.fallback_oracle_config; - match Self::try_fetch_from_config(env, market_id, fallback_config) { - Ok(res) => { - crate::events::EventEmitter::emit_fallback_used( - env, - market_id, - &market.oracle_config.oracle_address, - &fallback_config.oracle_address, - ); - used_config = fallback_config.clone(); - res - } - Err(_) => { - crate::events::EventEmitter::emit_manual_resolution_required( - env, - market_id, - &soroban_sdk::String::from_str( - env, - "oracle_resolution_failed_primary_then_fallback", - ), - ); - return Err(Error::FallbackOracleUnavailable); - } - } - } else { - crate::events::EventEmitter::emit_manual_resolution_required( - env, - market_id, - &soroban_sdk::String::from_str( - env, - "oracle_resolution_failed_primary_only", - ), - ); - return Err(Error::OracleUnavailable); + if !has_unclaimed_winners { + for user in bettors.iter() { + if let Some(bet) = bets::BetStorage::get_bet(&env, &market_id, &user) { + if winning_outcomes.contains(&bet.outcome) + && !market + .claimed + .get((*user).clone()) + .map(|info| info.is_claimed()) + .unwrap_or(false) + { + has_unclaimed_winners = true; + break; } } - }; - - // Create oracle resolution record - let resolution = OracleResolution { - market_id: market_id.clone(), - oracle_result: outcome.clone(), - price, - threshold: used_config.threshold, - comparison: used_config.comparison.clone(), - timestamp: current_time, - provider: used_config.provider.clone(), - feed_id: used_config.feed_id.clone(), - }; - - // Store the result in the market - MarketStateManager::set_oracle_result(&mut market, outcome.clone()); - MarketStateManager::update_market(env, market_id, &market); - - // Emit oracle result event - let provider_str = match used_config.provider { - OracleProvider::Reflector => soroban_sdk::String::from_str(env, "Reflector"), - OracleProvider::Pyth => soroban_sdk::String::from_str(env, "Pyth"), - _ => soroban_sdk::String::from_str(env, "Custom"), - }; - let feed_str = used_config.feed_id.clone(); - let comparison_str = used_config.comparison.clone(); - - crate::events::EventEmitter::emit_oracle_result( - env, - market_id, - &outcome, - &provider_str, - &feed_str, - price, - used_config.threshold, - &comparison_str, - ); - - Ok(resolution) - } - - /// Get oracle resolution for a market - - pub fn get_oracle_resolution( - _env: &Env, - _market_id: &Symbol, - ) -> Result, Error> { - // For now, return None since we don't store complex types in storage - // In a real implementation, you would store this in a more sophisticated way - - Ok(None) - } - - /// Validate oracle resolution - pub fn validate_oracle_resolution( - _env: &Env, - resolution: &OracleResolution, - ) -> Result<(), Error> { - // Validate price is positive - if resolution.price <= 0 { - return Err(Error::InvalidInput); - } - - // Validate threshold is positive - if resolution.threshold <= 0 { - return Err(Error::InvalidInput); - } - - // Validate outcome is not empty - if resolution.oracle_result.is_empty() { - return Err(Error::InvalidInput); } - - Ok(()) } - /// Calculate oracle confidence score - pub fn calculate_oracle_confidence(resolution: &OracleResolution) -> u32 { - OracleResolutionAnalytics::calculate_confidence_score(resolution) + if !has_unclaimed_winners { + return Ok(0); } -} -// ===== MARKET RESOLUTION ===== - -/// Comprehensive market resolution management system combining multiple data sources. -/// -/// The Market Resolution Manager orchestrates the complete market resolution process, -/// integrating oracle data, community consensus, admin decisions, and dispute outcomes -/// to determine final market results. It serves as the central coordinator for all -/// resolution methods and ensures consistent, reliable market outcomes. -/// -/// # Core Responsibilities -/// -/// **Resolution Orchestration:** -/// - **Multi-Source Integration**: Combine oracle, community, and admin data -/// - **Method Selection**: Choose appropriate resolution method based on available data -/// - **Confidence Calculation**: Determine overall confidence in resolution outcome -/// - **Validation**: Ensure resolution meets quality and consistency standards -/// -/// **Market Lifecycle Management:** -/// - **Resolution Triggering**: Initiate resolution when markets are ready -/// - **State Management**: Track resolution progress through various states -/// - **Finalization**: Complete resolution process and make outcomes immutable -/// - **Event Emission**: Notify system components of resolution events -/// -/// # Resolution Methods Supported -/// -/// **Hybrid Resolution (Recommended):** -/// - Combines oracle price data with community voting -/// - Highest confidence when sources agree -/// - Fallback logic when sources disagree -/// -/// **Oracle-Only Resolution:** -/// - Pure algorithmic resolution based on price feeds -/// - Fast and objective for clear-cut price-based markets -/// - Used when community participation is insufficient -/// -/// **Community-Only Resolution:** -/// - Based entirely on community voting consensus -/// - Used when oracle data is unavailable or inappropriate -/// - Requires sufficient participation and consensus -/// -/// **Admin Override:** -/// - Administrative decision for exceptional circumstances -/// - Used for emergency situations or system failures -/// - Requires proper admin authentication and justification -/// -/// # Example Usage -/// -/// ```rust -/// # use soroban_sdk::{Env, Symbol, Address, String}; -/// # use predictify_hybrid::resolution::{MarketResolutionManager, MarketResolution, ResolutionMethod}; -/// # let env = Env::default(); -/// # let market_id = Symbol::new(&env, "btc_prediction_market"); -/// # let admin = Address::generate(&env); -/// -/// // Resolve a market using hybrid method (oracle + community) -/// let resolution = MarketResolutionManager::resolve_market(&env, &market_id)?; -/// -/// println!("Market Resolution Complete:"); -/// println!("Market: {}", resolution.market_id); -/// println!("Final outcome: {}", resolution.final_outcome); -/// println!("Method: {:?}", resolution.resolution_method); -/// println!("Confidence: {}%", resolution.confidence_score); -/// -/// // Display resolution details -/// match resolution.resolution_method { -/// ResolutionMethod::Hybrid => { -/// println!("Oracle result: {}", resolution.oracle_result); -/// println!("Community consensus: {}% ({})", -/// resolution.community_consensus.percentage, -/// resolution.community_consensus.outcome -/// ); -/// }, -/// ResolutionMethod::OracleOnly => { -/// println!("Resolved purely based on oracle: {}", resolution.oracle_result); -/// }, -/// ResolutionMethod::AdminOverride => { -/// println!("Administrative override resolution"); -/// }, -/// _ => println!("Other resolution method used"), -/// } -/// -/// // Validate the resolution -/// MarketResolutionManager::validate_market_resolution(&env, &resolution)?; -/// -/// // Admin can finalize with override if needed -/// if resolution.confidence_score < 70 { -/// let admin_resolution = MarketResolutionManager::finalize_market( -/// &env, -/// &admin, -/// &market_id, -/// &String::from_str(&env, "yes") -/// )?; -/// println!("Admin finalized with outcome: {}", admin_resolution.final_outcome); -/// } -/// # Ok::<(), predictify_hybrid::errors::Error>(()) -/// ``` -/// -/// # Resolution Decision Logic -/// -/// The manager uses sophisticated logic to determine final outcomes: -/// ```rust -/// # use soroban_sdk::{Env, String}; -/// # use predictify_hybrid::resolution::ResolutionMethod; -/// # use predictify_hybrid::markets::CommunityConsensus; -/// # let env = Env::default(); -/// -/// // Example resolution decision logic -/// fn determine_final_outcome( -/// oracle_result: &String, -/// community_consensus: &CommunityConsensus, -/// oracle_confidence: u32, -/// community_confidence: u32 -/// ) -> (String, ResolutionMethod) { -/// let env = Env::default(); -/// -/// // Check if oracle and community agree -/// if oracle_result == &community_consensus.outcome { -/// // Agreement - use hybrid method with high confidence -/// (oracle_result.clone(), ResolutionMethod::Hybrid) -/// } else if oracle_confidence > 85 && community_confidence < 60 { -/// // Strong oracle, weak community - use oracle -/// (oracle_result.clone(), ResolutionMethod::OracleOnly) -/// } else if community_confidence > 85 && oracle_confidence < 60 { -/// // Strong community, weak oracle - use community -/// (community_consensus.outcome.clone(), ResolutionMethod::CommunityOnly) -/// } else { -/// // Conflict requires admin intervention -/// (String::from_str(&env, "disputed"), ResolutionMethod::AdminOverride) -/// } -/// } -/// ``` -/// -/// # Confidence Scoring -/// -/// Resolution confidence is calculated from multiple factors: -/// - **Oracle Confidence**: Quality and freshness of oracle data -/// - **Community Confidence**: Participation level and consensus strength -/// - **Method Reliability**: Inherent reliability of chosen resolution method -/// - **Data Consistency**: Agreement between different data sources -/// -/// ```rust -/// # use predictify_hybrid::resolution::MarketResolution; -/// # let resolution = MarketResolution::default(); // Placeholder -/// -/// // Interpret confidence levels -/// match resolution.confidence_score { -/// 95..=100 => println!("Extremely high confidence - virtually certain outcome"), -/// 85..=94 => println!("Very high confidence - strong evidence for outcome"), -/// 75..=84 => println!("High confidence - good evidence for outcome"), -/// 65..=74 => println!("Moderate confidence - reasonable evidence"), -/// 50..=64 => println!("Low confidence - weak evidence, consider review"), -/// _ => println!("Very low confidence - outcome uncertain, needs attention"), -/// } -/// ``` -/// -/// # Error Handling and Fallbacks -/// -/// The manager handles various failure scenarios: -/// - **Oracle Failures**: Fallback to community-only resolution -/// - **Low Participation**: Fallback to oracle-only or admin resolution -/// - **Data Conflicts**: Escalate to dispute resolution process -/// - **System Errors**: Graceful degradation with error reporting -/// -/// # Integration with Other Systems -/// -/// Market Resolution Manager integrates with: -/// - **Oracle System**: Fetches and validates oracle data -/// - **Voting System**: Retrieves community consensus data -/// - **Dispute System**: Handles disputed resolutions -/// - **Admin System**: Processes administrative overrides -/// - **Event System**: Emits resolution events for transparency -/// - **Analytics System**: Records resolution metrics and performance -/// -/// # Performance and Scalability -/// -/// The manager optimizes for: -/// - **Batch Processing**: Resolve multiple markets efficiently -/// - **Parallel Resolution**: Handle independent resolutions concurrently -/// - **Caching**: Cache resolution data to avoid redundant calculations -/// - **Event-Driven**: React to market state changes automatically -pub struct MarketResolutionManager; - -impl MarketResolutionManager { - /// Resolve a market by combining oracle results and community votes - pub fn resolve_market(env: &Env, market_id: &Symbol) -> Result { - // Get the market from storage - let mut market = MarketStateManager::get_market(env, market_id)?; - - // Validate market for resolution (includes min pool size check) - let validation = MarketResolutionValidator::validate_market_for_resolution(env, &market); - if let Err(Error::InvalidState) = validation { - let global_min: i128 = env - .storage() - .persistent() - .get(&Symbol::new(env, "global_min_pool")) - .unwrap_or(0); - let min_pool = market.min_pool_size.unwrap_or(global_min); - crate::events::EventEmitter::emit_min_pool_size_not_met( - env, - market_id, - market.total_staked, - min_pool, - ); - return Err(Error::InvalidState); - } - validation?; - - // Retrieve the oracle result - let oracle_result = market - .oracle_result - .as_ref() - .ok_or(Error::OracleUnavailable)? - .clone(); - - // Calculate community consensus - let community_consensus = MarketAnalytics::calculate_community_consensus(&market); - - // Determine winning outcome(s) using multi-outcome resolution with tie detection - // This handles both single winner and tie cases (pool split) - let winning_outcomes = MarketUtils::determine_winning_outcomes( - env, - &market, - &oracle_result, - &community_consensus, - 0, // Tie threshold: 0 = exact ties only - ); - - // For resolution record, use first outcome (or comma-separated for display) - let final_result = if winning_outcomes.len() > 0 { - if winning_outcomes.len() == 1 { - winning_outcomes.get(0).unwrap().clone() - } else { - // For ties, just use the first outcome for the final result field - // The full list is stored in winning_outcomes - winning_outcomes.get(0).unwrap().clone() - } - } else { - oracle_result.clone() - }; - - // Determine resolution method - let resolution_method = MarketResolutionAnalytics::determine_resolution_method( - &oracle_result, - &community_consensus, - ); - - // Calculate confidence score - let confidence_score = MarketResolutionAnalytics::calculate_confidence_score( - &oracle_result, - &community_consensus, - &resolution_method, - ); - - // Create market resolution record - let resolution = MarketResolution { - market_id: market_id.clone(), - final_outcome: final_result.clone(), - oracle_result, - community_consensus, - resolution_timestamp: env.ledger().timestamp(), - resolution_method, - confidence_score, - }; - - // Capture old state for event - let old_state = market.state.clone(); - - // Set winning outcome(s) - supports both single winner and ties - MarketStateManager::set_winning_outcomes( - &mut market, - winning_outcomes.clone(), - Some(market_id), - ); - MarketStateManager::update_market(env, market_id, &market); - ResolutionOutcomeCache::refresh(env, market_id, &market)?; - - // Decrement active event count since the event is resolved - crate::storage::CreatorLimitsManager::decrement_active_events(env, &market.admin); - - // Emit market resolved event - let oracle_result_str = market - .oracle_result - .clone() - .unwrap_or_else(|| soroban_sdk::String::from_str(env, "N/A")); - let community_consensus_str = soroban_sdk::String::from_str(env, "Consensus"); - let method_str = match resolution_method { - ResolutionMethod::OracleOnly => "OracleOnly", - ResolutionMethod::CommunityOnly => "CommunityOnly", - ResolutionMethod::Hybrid => "Hybrid", - ResolutionMethod::AdminOverride => "AdminOverride", - ResolutionMethod::DisputeResolution => "DisputeResolution", - }; - let resolution_method_str = soroban_sdk::String::from_str(env, method_str); - - crate::events::EventEmitter::emit_market_resolved( - env, - market_id, - &final_result, - &oracle_result_str, - &community_consensus_str, - &resolution_method_str, - confidence_score as i128, - ); - - // Emit state change event - crate::events::EventEmitter::emit_state_change_event( - env, - market_id, - &old_state, - &crate::types::MarketState::Resolved, - &soroban_sdk::String::from_str(env, "Automated resolution completed"), - ); - crate::monitoring::ContractMonitor::emit_resolution_transition_hook( - env, - market_id, - &old_state, - &crate::types::MarketState::Resolved, - &resolution_method_str, - ); - - Ok(resolution) - } - - /// Finalize market with admin override - pub fn finalize_market( - env: &Env, - admin: &Address, - market_id: &Symbol, - outcome: &String, - ) -> Result { - // Validate admin permissions - MarketResolutionValidator::validate_admin_permissions(env, admin)?; - - // Get the market - let mut market = MarketStateManager::get_market(env, market_id)?; - - // Validate outcome - MarketResolutionValidator::validate_outcome(env, outcome, &market.outcomes)?; - - // Create resolution record - let resolution = MarketResolution { - market_id: market_id.clone(), - final_outcome: outcome.clone(), - oracle_result: market - .oracle_result - .clone() - .unwrap_or_else(|| String::from_str(env, "")), - community_consensus: MarketAnalytics::calculate_community_consensus(&market), - resolution_timestamp: env.ledger().timestamp(), - resolution_method: ResolutionMethod::AdminOverride, - confidence_score: 100, // Admin override has full confidence - }; - - // Set final outcome(s) - convert single outcome to vector - let mut winning_outcomes = Vec::new(env); - winning_outcomes.push_back(outcome.clone()); - MarketStateManager::set_winning_outcomes(&mut market, winning_outcomes, Some(market_id)); - MarketStateManager::update_market(env, market_id, &market); - ResolutionOutcomeCache::refresh(env, market_id, &market)?; - - // Decrement active event count since the event is manually finalized - crate::storage::CreatorLimitsManager::decrement_active_events(env, &market.admin); - - Ok(resolution) - } - - /// Get market resolution - - pub fn get_market_resolution( - _env: &Env, - _market_id: &Symbol, - ) -> Result, Error> { - // For now, return None since we don't store complex types in storage - // In a real implementation, you would store this in a more sophisticated way - - Ok(None) - } - - /// Validate market resolution - pub fn validate_market_resolution( - env: &Env, - resolution: &MarketResolution, - ) -> Result<(), Error> { - MarketResolutionValidator::validate_market_resolution(env, resolution) - } -} - -// ===== RESOLUTION VALIDATION ===== - -/// Oracle resolution validation -pub struct OracleResolutionValidator; - -impl OracleResolutionValidator { - /// Validate market for oracle resolution - pub fn validate_market_for_oracle_resolution(env: &Env, market: &Market) -> Result<(), Error> { - // Check if the market has already been resolved - if market.oracle_result.is_some() { - return Err(Error::MarketResolved); - } - - // Check if the market ended (we can only fetch oracle result after market ends) - let current_time = env.ledger().timestamp(); - if current_time < market.end_time { - return Err(Error::MarketClosed); - } - - Ok(()) - } - - /// Validate oracle resolution - pub fn validate_oracle_resolution( - _env: &Env, - resolution: &OracleResolution, - ) -> Result<(), Error> { - // Validate price is positive - if resolution.price <= 0 { - return Err(Error::InvalidInput); - } - - // Validate threshold is positive - if resolution.threshold <= 0 { - return Err(Error::InvalidInput); - } - - // Validate outcome is not empty - if resolution.oracle_result.is_empty() { - return Err(Error::InvalidInput); - } - - Ok(()) + let summary = resolution::ResolutionOutcomeCache::require(&env, &market_id, &market)?; + let winning_total = summary.winning_total; + if winning_total == 0 { + return Ok(0); } -} - -/// Market resolution validation -pub struct MarketResolutionValidator; -impl MarketResolutionValidator { - /// Validate market for resolution - pub fn validate_market_for_resolution(env: &Env, market: &Market) -> Result<(), Error> { - // Check if market is already resolved - if market.winning_outcomes.is_some() { - return Err(Error::MarketResolved); - } + let total_pool = summary.total_pool; + let fee_denominator = 10000i128; + let mut total_distributed: i128 = 0; - // Check if oracle result is available - if market.oracle_result.is_none() { - return Err(Error::OracleUnavailable); - } + // Create budget guard with 100,000 instruction threshold + let budget_guard = gas::BudgetGuard::new(&env, 100000); - // Check if market has ended - if market.is_active(env) { - return Err(Error::MarketClosed); - } - - // Check minimum pool size requirement (per-market override, else global) - let global_min: i128 = env - .storage() - .persistent() - .get(&Symbol::new(env, "global_min_pool")) - .unwrap_or(0); - let min_pool = market.min_pool_size.unwrap_or(global_min); - - // Only check if min pool is set - if min_pool > 0 { - // Get token decimals to normalize amounts for comparison - let token_client = crate::markets::MarketUtils::get_token_client(env)?; - let token_decimals = token_client.decimals() as u32; - - // Normalize both total staked and min pool to canonical scale for comparison - let normalized_total = crate::tokens::normalize_amount(market.total_staked, token_decimals); - let normalized_min = crate::tokens::normalize_amount(min_pool, token_decimals); - - if normalized_total < normalized_min { - return Err(Error::InvalidState); + // 1. Distribute to Voters + let mut voter_count = 0u32; + for (user, outcome) in market.votes.iter() { + if winning_outcomes.contains(&outcome) { + if market + .claimed + .get((*user).clone()) + .map(|info| info.is_claimed()) + .unwrap_or(false) + { + continue; } - } - Ok(()) - } - - /// Validate admin permissions - pub fn validate_admin_permissions(env: &Env, admin: &Address) -> Result<(), Error> { - let stored_admin: Option
= - env.storage().persistent().get(&Symbol::new(env, "Admin")); - - match stored_admin { - Some(stored_admin) => { - if admin != &stored_admin { - return Err(Error::Unauthorized); + let user_stake = market.stakes.get((*user).clone()).unwrap_or(0); + if user_stake > 0 { + let user_share = (user_stake + .checked_mul(fee_denominator - fee_percent) + .ok_or(Error::InvalidInput)?) + / fee_denominator; + let payout = (user_share + .checked_mul(total_pool) + .ok_or(Error::InvalidInput)?) + / winning_total; + + if payout >= 0 { + market + .claimed + .set((*user).clone(), ClaimInfo::new(&env, payout)); + if payout > 0 { + total_distributed = total_distributed + .checked_add(payout) + .ok_or(Error::InvalidInput)?; + + storage::BalanceStorage::add_balance( + &env, + &user, + &ReflectorAsset::Stellar, + payout, + )?; + + events::EventEmitter::emit_winnings_claimed(&env, &market_id, &user, payout); + } } - Ok(()) } - None => Err(Error::Unauthorized), } - } - /// Validate outcome - pub fn validate_outcome( - _env: &Env, - outcome: &String, - valid_outcomes: &Vec, - ) -> Result<(), Error> { - if !valid_outcomes.contains(outcome) { - return Err(Error::InvalidOutcome); + voter_count += 1; + if voter_count % 10 == 0 { + budget_guard.check()?; } - - Ok(()) } - /// Validate market resolution - pub fn validate_market_resolution( - env: &Env, - resolution: &MarketResolution, - ) -> Result<(), Error> { - // Validate final outcome is not empty - if resolution.final_outcome.is_empty() { - return Err(Error::InvalidInput); - } - - // Validate confidence score is within range - if resolution.confidence_score > 100 { - return Err(Error::InvalidInput); - } - - // Validate timestamp is reasonable - let current_time = env.ledger().timestamp(); - if resolution.resolution_timestamp > current_time { - return Err(Error::InvalidInput); - } - - Ok(()) - } -} - -// ===== RESOLUTION ANALYTICS ===== - -/// Oracle resolution analytics -pub struct OracleResolutionAnalytics; - -impl OracleResolutionAnalytics { - /// Calculate oracle confidence score - pub fn calculate_confidence_score(resolution: &OracleResolution) -> u32 { - // Base confidence for oracle resolution - let mut confidence: u32 = 80; - - // Adjust based on price deviation from threshold - let deviation = ((resolution.price - resolution.threshold).abs() as f64) - / (resolution.threshold as f64); - - if deviation > 0.1 { - // High deviation - lower confidence - confidence = confidence.saturating_sub(20); - } else if deviation < 0.05 { - // Low deviation - higher confidence - confidence = confidence.saturating_add(10); - } - - confidence.min(100) - } - - /// Get oracle resolution statistics - pub fn get_oracle_stats(_env: &Env) -> Result { - Ok(OracleStats::default()) - } -} - -/// Market resolution analytics -pub struct MarketResolutionAnalytics; - -impl MarketResolutionAnalytics { - /// Determine resolution method - pub fn determine_resolution_method( - _oracle_result: &String, - community_consensus: &CommunityConsensus, - ) -> ResolutionMethod { - if community_consensus.percentage > 70 { - ResolutionMethod::Hybrid - } else { - ResolutionMethod::OracleOnly - } - } - - /// Calculate confidence score - pub fn calculate_confidence_score( - _oracle_result: &String, - community_consensus: &CommunityConsensus, - method: &ResolutionMethod, - ) -> u32 { - match method { - ResolutionMethod::OracleOnly => 85, - ResolutionMethod::CommunityOnly => { - let base_confidence = community_consensus.percentage as u32; - base_confidence.min(90) - } - ResolutionMethod::Hybrid => { - let oracle_confidence = 85; - let community_confidence = community_consensus.percentage as u32; - ((oracle_confidence + community_confidence) / 2).min(95) - } - ResolutionMethod::AdminOverride => 100, - ResolutionMethod::DisputeResolution => 75, - } - } - - /// Calculate resolution analytics - pub fn calculate_resolution_analytics(_env: &Env) -> Result { - Ok(ResolutionAnalytics::default()) - } - - /// Update resolution analytics - pub fn update_resolution_analytics( - _env: &Env, - _resolution: &MarketResolution, - ) -> Result<(), Error> { - // For now, do nothing since we don't store complex types - Ok(()) - } -} - -// ===== RESOLUTION UTILITIES ===== - -/// Resolution utility functions -pub struct ResolutionUtils; - -impl ResolutionUtils { - /// Get resolution state for a market - pub fn get_resolution_state(_env: &Env, market: &Market) -> ResolutionState { - if market.winning_outcomes.is_some() { - ResolutionState::MarketResolved - } else if market.oracle_result.is_some() { - ResolutionState::OracleResolved - } else if market.total_dispute_stakes() > 0 { - ResolutionState::Disputed - } else { - ResolutionState::Active - } - } - - /// Check if market can be resolved - pub fn can_resolve_market(env: &Env, market: &Market) -> bool { - market.has_ended(env) && market.oracle_result.is_some() && market.winning_outcomes.is_none() - } - - /// Get resolution eligibility - pub fn get_resolution_eligibility(env: &Env, market: &Market) -> (bool, String) { - if !market.has_ended(env) { - return (false, String::from_str(env, "Market has not ended")); - } - - if market.oracle_result.is_none() { - return (false, String::from_str(env, "Oracle result not available")); - } - - if market.winning_outcomes.is_some() { - return (false, String::from_str(env, "Market already resolved")); - } - - (true, String::from_str(env, "Eligible for resolution")) - } - - /// Calculate resolution time - pub fn calculate_resolution_time(env: &Env, market: &Market) -> u64 { - let current_time = env.ledger().timestamp(); - if current_time > market.end_time { - current_time - market.end_time - } else { - 0 - } - } - - /// Validate resolution parameters - pub fn validate_resolution_parameters( - _env: &Env, - market: &Market, - outcome: &String, - ) -> Result<(), Error> { - // Validate outcome is in market outcomes - if !market.outcomes.contains(outcome) { - return Err(Error::InvalidOutcome); - } - - // Validate market is not already resolved - if market.winning_outcomes.is_some() { - return Err(Error::MarketResolved); - } - - Ok(()) - } -} - -// ===== RESOLUTION TESTING ===== - -/// Resolution testing utilities -pub struct ResolutionTesting; - -impl ResolutionTesting { - /// Create test oracle resolution - pub fn create_test_oracle_resolution(env: &Env, market_id: &Symbol) -> OracleResolution { - OracleResolution { - market_id: market_id.clone(), - oracle_result: String::from_str(env, "yes"), - price: 2500000, - threshold: 2500000, - comparison: String::from_str(env, "gt"), - timestamp: env.ledger().timestamp(), - provider: OracleProvider::pyth(), - feed_id: String::from_str(env, "BTC/USD"), - } - } - - /// Create test market resolution - pub fn create_test_market_resolution(env: &Env, market_id: &Symbol) -> MarketResolution { - MarketResolution { - market_id: market_id.clone(), - final_outcome: String::from_str(env, "yes"), - oracle_result: String::from_str(env, "yes"), - community_consensus: CommunityConsensus { - outcome: String::from_str(env, "yes"), - votes: 6, - total_votes: 10, - percentage: 60, - }, - resolution_timestamp: env.ledger().timestamp(), - resolution_method: ResolutionMethod::Hybrid, - confidence_score: 80, - } - } - - /// Validate resolution structure - pub fn validate_resolution_structure(resolution: &MarketResolution) -> Result<(), Error> { - if resolution.final_outcome.is_empty() { - return Err(Error::InvalidInput); - } - - if resolution.confidence_score > 100 { - return Err(Error::InvalidInput); - } - - Ok(()) - } - - /// Simulate resolution process - pub fn simulate_resolution_process( - env: &Env, - market_id: &Symbol, - ) -> Result { - // Fetch oracle result - let _oracle_resolution = OracleResolutionManager::fetch_oracle_result(env, market_id)?; - - // Resolve market - let market_resolution = MarketResolutionManager::resolve_market(env, market_id)?; - - Ok(market_resolution) - } -} - -// ===== STATISTICS TYPES ===== - -/// Oracle statistics -#[derive(Clone, Debug)] -#[contracttype] -pub struct OracleStats { - pub total_resolutions: u32, - pub successful_resolutions: u32, - pub average_confidence: i128, - pub provider_distribution: Map, -} - -impl Default for OracleStats { - fn default() -> Self { - Self { - total_resolutions: 0, - successful_resolutions: 0, - average_confidence: 0, - provider_distribution: Map::new(&soroban_sdk::Env::default()), - } - } -} - -impl Default for ResolutionAnalytics { - fn default() -> Self { - Self { - total_resolutions: 0, - oracle_resolutions: 0, - community_resolutions: 0, - hybrid_resolutions: 0, - average_confidence: 0, - resolution_times: Vec::new(&soroban_sdk::Env::default()), - outcome_distribution: Map::new(&soroban_sdk::Env::default()), - } - } -} - -// ===== MODULE TESTS ===== - -#[cfg(any())] -mod tests { - use super::*; - use crate::{test::PredictifyTest, PredictifyHybridClient}; - use soroban_sdk::testutils::{Address as _, Ledger, LedgerInfo}; - - #[test] - fn test_oracle_resolution_manager_fetch_result() { - let env = Env::default(); - let market_id = Symbol::new(&env, "test_market"); - let _oracle_contract = Address::generate(&env); - - // This test would require a mock oracle setup - // For now, we'll test the validation logic - let resolution = ResolutionTesting::create_test_oracle_resolution(&env, &market_id); - assert_eq!(resolution.oracle_result, String::from_str(&env, "yes")); - assert_eq!(resolution.price, 2500000); - } - - #[test] - fn test_market_resolution_manager_resolve_market() { - let env = Env::default(); - let market_id = Symbol::new(&env, "test_market"); - - // This test would require a complete market setup - // For now, we'll test the resolution structure - let resolution = ResolutionTesting::create_test_market_resolution(&env, &market_id); - assert_eq!(resolution.final_outcome, String::from_str(&env, "yes")); - assert_eq!(resolution.resolution_method, ResolutionMethod::Hybrid); - } - - #[test] - fn test_resolution_utils_get_state() { - let env = Env::default(); - let admin = Address::generate(&env); - let market = Market::new( - &env, - admin, - String::from_str(&env, "Test Market"), - soroban_sdk::vec![ - &env, - String::from_str(&env, "yes"), - String::from_str(&env, "no"), - ], - env.ledger().timestamp() + 86400, - OracleConfig { - provider: OracleProvider::pyth(), - oracle_address: Address::generate(&env), - feed_id: String::from_str(&env, "BTC/USD"), - threshold: 2500000, - comparison: String::from_str(&env, "gt"), - }, - None, - 86400, - MarketState::Active, - ); - - let state = ResolutionUtils::get_resolution_state(&env, &market); - assert_eq!(state, ResolutionState::Active); - } - - #[test] - fn test_resolution_analytics_determine_method() { - let env = Env::default(); - let oracle_result = String::from_str(&env, "yes"); - let community_consensus = CommunityConsensus { - outcome: String::from_str(&env, "yes"), - votes: 8, - total_votes: 10, - percentage: 80, - }; - - let method = MarketResolutionAnalytics::determine_resolution_method( - &oracle_result, - &community_consensus, - ); - assert_eq!(method, ResolutionMethod::Hybrid); - } - - #[test] - fn test_resolution_testing_utilities() { - let env = Env::default(); - let market_id = Symbol::new(&env, "test_market"); - - let oracle_resolution = ResolutionTesting::create_test_oracle_resolution(&env, &market_id); - assert!(oracle_resolution.oracle_result == String::from_str(&env, "yes")); - - let market_resolution = ResolutionTesting::create_test_market_resolution(&env, &market_id); - assert!(ResolutionTesting::validate_resolution_structure(&market_resolution).is_ok()); - } - - #[test] - fn test_resolution_method_determination() { - let env = Env::default(); - - // Create test data - let community_consensus = CommunityConsensus { - outcome: String::from_str(&env, "yes"), - votes: 75, - total_votes: 100, - percentage: 75, - }; - - // Test hybrid resolution - let method = MarketResolutionAnalytics::determine_resolution_method( - &String::from_str(&env, "yes"), - &community_consensus, - ); - assert!(matches!(method, ResolutionMethod::Hybrid)); - - // Test oracle-only resolution - let low_consensus = CommunityConsensus { - outcome: String::from_str(&env, "yes"), - votes: 60, - total_votes: 100, - percentage: 60, - }; - let method = MarketResolutionAnalytics::determine_resolution_method( - &String::from_str(&env, "yes"), - &low_consensus, - ); - assert!(matches!(method, ResolutionMethod::OracleOnly)); - } -} - -// ===== ORACLE CALLBACK AUTHENTICATION INTEGRATION ===== - -/// Oracle callback authentication integration for market resolution -/// -/// This module integrates the oracle callback authentication system with market resolution, -/// ensuring that only authenticated oracle callbacks can update market outcomes. -pub struct OracleCallbackResolver; - -impl OracleCallbackResolver { - /// Process authenticated oracle callback for market resolution - /// - /// This method authenticates an oracle callback and processes the data for market resolution. - /// It integrates with the resolution system to update market outcomes based on authenticated oracle data. - /// - /// # Arguments - /// * `env` - Soroban environment - /// * `caller` - Address of the calling oracle contract - /// * `callback_data` - Authenticated callback data from the oracle - /// * `market_id` - Market identifier to resolve - /// - /// # Returns - /// * `Ok(())` if callback is processed and market is updated - /// * `Err(Error)` if authentication fails or processing fails - /// - /// # Security Notes - /// - /// This method ensures that only authorized oracle contracts can update market outcomes - /// through comprehensive authentication checks. - pub fn process_authenticated_callback( - env: &Env, - caller: &Address, - callback_data: &crate::oracles::OracleCallbackData, - market_id: &Symbol, - ) -> Result<(), Error> { - // Create authentication system - let auth = crate::oracles::OracleCallbackAuth::new(env); - - // Authenticate and process the callback - auth.authenticate_and_process(caller, callback_data)?; - - // Update market resolution based on authenticated oracle data - Self::update_market_resolution(env, callback_data, market_id)?; - - Ok(()) - } - - /// Update market resolution based on authenticated oracle data - /// - /// # Arguments - /// * `env` - Soroban environment - /// * `callback_data` - Authenticated callback data - /// * `market_id` - Market identifier to update - /// - /// # Returns - /// * `Ok(())` if market resolution is updated successfully - /// * `Err(Error)` if update fails - fn update_market_resolution( - env: &Env, - callback_data: &crate::oracles::OracleCallbackData, - market_id: &Symbol, - ) -> Result<(), Error> { - // Get market state manager - let market = MarketStateManager::get_market(env, market_id)?; - - // Validate market is ready for resolution - OracleResolutionValidator::validate_market_for_oracle_resolution(env, &market)?; - - // Determine outcome based on oracle data - let outcome = Self::determine_outcome_from_oracle_data(callback_data, &market)?; - - // Create oracle resolution with all required fields - let resolution = OracleResolution { - market_id: market_id.clone(), - feed_id: callback_data.feed_id.clone(), - comparison: String::from_str(env, "eq"), - provider: market.oracle_config.provider.clone(), - price: callback_data.price, - timestamp: callback_data.timestamp, - oracle_result: outcome.clone(), - threshold: market.oracle_config.threshold, - }; - - // Validate resolution - OracleResolutionValidator::validate_oracle_resolution(env, &resolution)?; - - // Update market with oracle resolution - let mut updated_market = market; - updated_market.oracle_result = Some(outcome.clone()); - - // Store updated market - MarketStateManager::update_market(env, market_id, &updated_market); - - // Emit resolution event - crate::events::EventEmitter::emit_oracle_result( - env, - market_id, - &outcome, - &String::from_str(env, "direct"), - &String::from_str(env, "callback"), - callback_data.price, - 0, - &String::from_str(env, "eq"), - ); - - Ok(()) - } - - /// Determine market outcome from oracle data - /// - /// # Arguments - /// * `callback_data` - Authenticated callback data - /// * `market` - Market to determine outcome for - /// - /// # Returns - /// Determined outcome string - fn determine_outcome_from_oracle_data( - callback_data: &crate::oracles::OracleCallbackData, - market: &Market, - ) -> Result { - // For binary markets (yes/no), determine outcome based on price comparison - if market.outcomes.len() == 2 { - let first_outcome = market.outcomes.get(0).unwrap(); - let yes_bytes = first_outcome.to_bytes(); - let first_is_yes = yes_bytes.len() == 3 - && yes_bytes.get(0).unwrap_or(0) == 'y' as u8 - && yes_bytes.get(1).unwrap_or(0) == 'e' as u8 - && yes_bytes.get(2).unwrap_or(0) == 's' as u8; - - let (yes_outcome, no_outcome) = if first_is_yes { - ( - market.outcomes.get(0).unwrap(), - market.outcomes.get(1).unwrap(), - ) - } else { - ( - market.outcomes.get(1).unwrap(), - market.outcomes.get(0).unwrap(), - ) - }; + // 2. Distribute to Bettors + let mut bettor_count = 0u32; + for user in bettors.iter() { + if let Some(mut bet) = bets::BetStorage::get_bet(&env, &market_id, &user) { + if winning_outcomes.contains(&bet.outcome) { + if market + .claimed + .get((*user).clone()) + .map(|info| info.is_claimed()) + .unwrap_or(false) + { + bet.status = BetStatus::Won; + let _ = bets::BetStorage::store_bet(&env, &bet); + continue; + } - if callback_data.price > 0 { - Ok(yes_outcome.clone()) + if bet.amount > 0 { + let user_share = (bet.amount + .checked_mul(fee_denominator - fee_percent) + .ok_or(Error::InvalidInput)?) + / fee_denominator; + let payout = (user_share + .checked_mul(total_pool) + .ok_or(Error::InvalidInput)?) + / winning_total; + + if payout > 0 { + market + .claimed + .set((*user).clone(), ClaimInfo::new(&env, payout)); + + total_distributed = total_distributed + .checked_add(payout) + .ok_or(Error::InvalidInput)?; + + bet.status = BetStatus::Won; + let _ = bets::BetStorage::store_bet(&env, &bet); + + match storage::BalanceStorage::add_balance( + &env, + &user, + &ReflectorAsset::Stellar, + payout, + ) { + Ok(_) => {} + Err(e) => soroban_sdk::panic_with_error!(env, e), + } + events::EventEmitter::emit_winnings_claimed(&env, &market_id, &user, payout); + } + } } else { - Ok(no_outcome.clone()) + if matches!(bet.status, BetStatus::Active) { + bet.status = BetStatus::Lost; + let _ = bets::BetStorage::store_bet(&env, &bet); + } } - } else { - // For multi-outcome markets, use price modulo number of outcomes - let num_outcomes = market.outcomes.len() as u32; - let outcome_index = (callback_data.price.abs() as u32) % num_outcomes; - Ok(market.outcomes.get(outcome_index).unwrap().clone()) } - } - /// Validate oracle callback authorization for market resolution - /// - /// # Arguments - /// * `env` - Soroban environment - /// * `caller` - Address of the calling oracle contract - /// * `market_id` - Market identifier being resolved - /// - /// # Returns - /// * `Ok(())` if caller is authorized for this market - /// * `Err(Error::OracleCallbackUnauthorized)` if not authorized - pub fn validate_oracle_authorization_for_market( - env: &Env, - caller: &Address, - market_id: &Symbol, - ) -> Result<(), Error> { - // Check if caller is authorized oracle - let whitelist = crate::oracles::OracleWhitelist::from_env(env); - if !crate::oracles::OracleWhitelist::is_oracle_authorized(env, caller)? { - return Err(Error::OracleCallbackUnauthorized); + bettor_count += 1; + if bettor_count % 10 == 0 { + budget_guard.check()?; } - - // Check if market exists and is ready for oracle resolution - let market = MarketStateManager::get_market(env, market_id)?; - - OracleResolutionValidator::validate_market_for_oracle_resolution(env, &market)?; - - Ok(()) } -} -#[cfg(test)] -mod resolution_outcome_cache_tests { - use super::*; - use crate::markets::MarketStateManager; - use crate::PredictifyHybrid; - use soroban_sdk::testutils::Address as _; - use soroban_sdk::{Address, Env, String}; - - fn sample_market(env: &Env, contract_id: &Address, admin: &Address) -> (Symbol, Market) { - let market_id = Symbol::new(env, "cache_mkt"); - let mut market = Market::new( - env, - admin.clone(), - String::from_str(env, "Test?"), - soroban_sdk::vec![ - env, - String::from_str(env, "yes"), - String::from_str(env, "no"), - ], - env.ledger().timestamp() + 3600, - OracleConfig { - provider: OracleProvider::pyth(), - oracle_address: Address::generate(env), - feed_id: String::from_str(env, "BTC/USD"), - threshold: 1, - comparison: String::from_str(env, "gt"), - }, - None, - 3600, - MarketState::Active, - ); - let user = Address::generate(env); - let yes = String::from_str(env, "yes"); - market.votes.set(user.clone(), yes.clone()); - market.stakes.set(user, 100); - market.total_staked = 100; - let mut winning = Vec::new(env); - winning.push_back(yes); - market.winning_outcomes = Some(winning); - env.as_contract(contract_id, || { - MarketStateManager::update_market(env, &market_id, &market); - }); - (market_id, market) - } - - #[test] - fn cache_persists_and_is_read_by_require() { - let env = Env::default(); - let contract_id = env.register(PredictifyHybrid, ()); - let admin = Address::generate(&env); - env.as_contract(&contract_id, || { - let (market_id, market) = sample_market(&env, &contract_id, &admin); - ResolutionOutcomeCache::refresh(&env, &market_id, &market).unwrap(); - let summary = ResolutionOutcomeCache::require(&env, &market_id, &market).unwrap(); - assert_eq!(summary.winning_total, 100); - assert_eq!(summary.total_pool, 100); - assert_eq!(summary.num_winning_outcomes, 1); - }); - } + budget_guard.check()?; + env.storage().persistent().set(&market_id, &market); - #[test] - fn cache_recomputes_after_outcome_override() { - let env = Env::default(); - let contract_id = env.register(PredictifyHybrid, ()); - let admin = Address::generate(&env); - env.as_contract(&contract_id, || { - let (market_id, mut market) = sample_market(&env, &contract_id, &admin); - ResolutionOutcomeCache::refresh(&env, &market_id, &market).unwrap(); - - let user2 = Address::generate(&env); - let no = String::from_str(&env, "no"); - market.votes.set(user2.clone(), no.clone()); - market.stakes.set(user2, 200); - market.total_staked = 300; - let mut winning = Vec::new(&env); - winning.push_back(no); - market.winning_outcomes = Some(winning); - MarketStateManager::update_market(&env, &market_id, &market); - - ResolutionOutcomeCache::invalidate(&env, &market_id); - ResolutionOutcomeCache::refresh(&env, &market_id, &market).unwrap(); - - let summary = ResolutionOutcomeCache::get(&env, &market_id).unwrap(); - assert_eq!(summary.winning_total, 200); - assert_eq!(summary.total_pool, 300); - }); - } - - /// Issue #547: cached `winning_total` must match a fresh O(V+B) scan (payout path invariant). - #[test] - fn cached_winning_total_matches_recompute() { - let env = Env::default(); - let contract_id = env.register(PredictifyHybrid, ()); - let admin = Address::generate(&env); - env.as_contract(&contract_id, || { - let (market_id, market) = sample_market(&env, &contract_id, &admin); - ResolutionOutcomeCache::refresh(&env, &market_id, &market).unwrap(); - let summary = ResolutionOutcomeCache::require(&env, &market_id, &market).unwrap(); - let winning_outcomes = market.winning_outcomes.as_ref().unwrap(); - let recomputed = ResolutionOutcomeCache::compute_winning_total_for_market( - &env, - &market_id, - &market, - winning_outcomes, - ) - .unwrap(); - assert_eq!(summary.winning_total, recomputed); - }); - } -} + Ok(total_distributed) +} \ No newline at end of file