diff --git a/contracts/predictify-hybrid/src/oracles.rs b/contracts/predictify-hybrid/src/oracles.rs index 46ef07e1..0696cd5e 100644 --- a/contracts/predictify-hybrid/src/oracles.rs +++ b/contracts/predictify-hybrid/src/oracles.rs @@ -1072,6 +1072,10 @@ impl OracleFactory { oracle_config: &OracleConfig, contract_id: Address, ) -> Result { + if oracle_config.is_none_sentinel() { + return Err(Error::InvalidOracleConfig); + } + Self::create_oracle(oracle_config.provider.clone(), contract_id) } @@ -1178,6 +1182,10 @@ impl OracleFactory { /// # Returns /// Result indicating if the configuration is valid for Stellar pub fn validate_stellar_compatibility(oracle_config: &OracleConfig) -> Result<(), Error> { + if oracle_config.is_none_sentinel() { + return Err(Error::InvalidOracleConfig); + } + match oracle_config.provider { OracleProvider::Reflector => { // Reflector is fully supported @@ -2293,10 +2301,7 @@ impl OracleValidationConfigManager { } /// Get per-event validation config override. - pub fn get_event_config( - env: &Env, - market_id: &Symbol, - ) -> Option { + pub fn get_event_config(env: &Env, market_id: &Symbol) -> Option { let per_event: soroban_sdk::Map = env .storage() .persistent() @@ -2325,10 +2330,7 @@ impl OracleValidationConfigManager { } /// Resolve effective validation config for a market. - pub fn get_effective_config( - env: &Env, - market_id: &Symbol, - ) -> GlobalOracleValidationConfig { + pub fn get_effective_config(env: &Env, market_id: &Symbol) -> GlobalOracleValidationConfig { if let Some(event_cfg) = Self::get_event_config(env, market_id) { GlobalOracleValidationConfig { max_staleness_secs: event_cfg.max_staleness_secs, @@ -2375,11 +2377,19 @@ impl OracleValidationConfigManager { if *provider == OracleProvider::Pyth { if let Some(confidence) = data.confidence { - let price_abs = if data.price < 0 { -data.price } else { data.price }; + let price_abs = if data.price < 0 { + -data.price + } else { + data.price + }; if price_abs == 0 { return Err(Error::InvalidInput); } - let conf_abs = if confidence < 0 { -confidence } else { confidence }; + let conf_abs = if confidence < 0 { + -confidence + } else { + confidence + }; let confidence_bps = ((conf_abs * 10_000) / price_abs).min(Self::MAX_CONFIDENCE_BPS as i128); let confidence_bps_u32 = confidence_bps as u32; diff --git a/contracts/predictify-hybrid/src/types.rs b/contracts/predictify-hybrid/src/types.rs index f2fb37a4..21975e40 100644 --- a/contracts/predictify-hybrid/src/types.rs +++ b/contracts/predictify-hybrid/src/types.rs @@ -491,7 +491,17 @@ impl OracleConfig { } } - /// Sentinel value for "no fallback" (used when has_fallback is false). Do not use for resolution. + /// Returns the reserved sentinel used to encode "no fallback oracle" in storage. + /// + /// The contracts persist fallback-oracle state as a `(has_fallback, fallback_oracle_config)` + /// pair instead of `Option` for Soroban contract-type compatibility. When + /// `has_fallback` is `false`, this sentinel is written into `fallback_oracle_config`. + /// + /// Sentinel semantics are reserved by the tuple `(provider=Reflector, feed_id="", threshold=0, + /// comparison="")`. Those values are intentionally outside the valid oracle-config domain, so + /// the sentinel cannot collide with any configuration that passes validation. + /// + /// This value must never be used for live oracle creation or resolution. pub fn none_sentinel(env: &Env) -> Self { Self { provider: OracleProvider::Reflector, @@ -504,11 +514,27 @@ impl OracleConfig { comparison: String::from_str(env, ""), } } + + /// Returns `true` when this configuration is the reserved "no fallback" sentinel. + /// + /// The sentinel identity intentionally ignores `oracle_address`: the reserved semantics are + /// carried by the invalid `feed_id` / `threshold` / `comparison` tuple, which keeps the + /// sentinel unambiguous even if a caller reuses the placeholder address elsewhere. + pub fn is_none_sentinel(&self) -> bool { + self.provider == OracleProvider::Reflector + && self.feed_id.is_empty() + && self.threshold == 0 + && self.comparison.is_empty() + } } impl OracleConfig { /// Validate the oracle configuration pub fn validate(&self, env: &Env) -> Result<(), crate::Error> { + if self.is_none_sentinel() || self.feed_id.is_empty() { + return Err(crate::Error::InvalidOracleConfig); + } + // Validate threshold if self.threshold <= 0 { return Err(crate::Error::InvalidThreshold); @@ -732,7 +758,10 @@ pub struct Market { pub oracle_config: OracleConfig, /// Whether a fallback oracle is configured (avoids Option in contract type for SDK compatibility) pub has_fallback: bool, - /// Fallback oracle configuration (only valid when has_fallback is true) + /// Fallback oracle configuration. + /// + /// When `has_fallback` is `false`, this field is populated with `OracleConfig::none_sentinel()` + /// so the stored representation stays collision-free without using `Option`. pub fallback_oracle_config: OracleConfig, /// Resolution timeout in seconds after end_time pub resolution_timeout: u64, @@ -3169,7 +3198,10 @@ pub struct Event { pub oracle_config: OracleConfig, /// Whether a fallback oracle is configured pub has_fallback: bool, - /// Fallback oracle configuration (only valid when has_fallback is true) + /// Fallback oracle configuration. + /// + /// When `has_fallback` is `false`, this field is populated with `OracleConfig::none_sentinel()` + /// so the stored representation stays collision-free without using `Option`. pub fallback_oracle_config: OracleConfig, /// Resolution timeout in seconds after end_time pub resolution_timeout: u64, diff --git a/contracts/predictify-hybrid/src/validation.rs b/contracts/predictify-hybrid/src/validation.rs index 66cabeef..e627df07 100644 --- a/contracts/predictify-hybrid/src/validation.rs +++ b/contracts/predictify-hybrid/src/validation.rs @@ -7,7 +7,6 @@ use crate::{ errors::Error, types::{BetLimits, Market, OracleConfig, OracleProvider}, }; -// use alloc::string::ToString; // Removed to fix Display/ToString trait errors use soroban_sdk::{contracttype, vec, Address, Env, Map, String, Symbol, Vec}; // ===== VALIDATION ERROR TYPES ===== @@ -1391,16 +1390,16 @@ impl InputValidator { /// ``` pub fn validate_description_length(description: &String) -> Result<(), ValidationError> { let length = description.len() as u32; - + // Description is optional, so empty is allowed if length == 0 { return Ok(()); } - + if length > config::MAX_DESCRIPTION_LENGTH { return Err(ValidationError::StringTooLong); } - + Ok(()) } @@ -1426,11 +1425,7 @@ impl InputValidator { /// assert!(InputValidator::validate_tag_length(&tag).is_ok()); /// ``` pub fn validate_tag_length(tag: &String) -> Result<(), ValidationError> { - Self::validate_string_length_range( - tag, - config::MIN_TAG_LENGTH, - config::MAX_TAG_LENGTH, - ) + Self::validate_string_length_range(tag, config::MIN_TAG_LENGTH, config::MAX_TAG_LENGTH) } /// Validate market category with length limits @@ -1489,17 +1484,17 @@ impl InputValidator { pub fn validate_outcomes(outcomes: &Vec) -> Result<(), ValidationError> { // Validate array size Self::validate_array_size(outcomes, config::MAX_MARKET_OUTCOMES)?; - + // Validate minimum number of outcomes if (outcomes.len() as u32) < config::MIN_MARKET_OUTCOMES { return Err(ValidationError::ArrayTooSmall); } - + // Validate each outcome length for outcome in outcomes.iter() { Self::validate_outcome_length(&outcome)?; } - + Ok(()) } @@ -1532,17 +1527,17 @@ impl InputValidator { if tags.is_empty() { return Ok(()); } - + // Validate maximum number of tags if (tags.len() as u32) > config::MAX_TAGS_PER_MARKET { return Err(ValidationError::ArrayTooLarge); } - + // Validate each tag length for tag in tags.iter() { Self::validate_tag_length(&tag)?; } - + Ok(()) } @@ -1572,7 +1567,7 @@ impl InputValidator { /// let description = String::from_str(&env, "Market about Bitcoin price prediction"); /// let category = String::from_str(&env, "Cryptocurrency"); /// let tags = vec![&env, String::from_str(&env, "crypto"), String::from_str(&env, "bitcoin")]; - /// + /// /// assert!(InputValidator::validate_market_metadata( /// &question, /// &outcomes, @@ -1590,23 +1585,23 @@ impl InputValidator { ) -> Result<(), ValidationError> { // Validate question Self::validate_question_length(question)?; - + // Validate outcomes Self::validate_outcomes(outcomes)?; - + // Validate description if provided if let Some(desc) = description { Self::validate_description_length(desc)?; } - + // Validate category if provided if let Some(cat) = category { Self::validate_category_length(cat)?; } - + // Validate tags Self::validate_tags(tags)?; - + Ok(()) } } @@ -4880,7 +4875,7 @@ impl OracleConfigValidator { let supported_operators = Self::get_supported_operators_for_provider(&config.provider); // Validate comparison operator - Self::validate_comparison_operator(&config.comparison, &supported_operators)?; + Self::validate_comparison_operator_literals(&config.comparison, supported_operators)?; // Additional consistency checks match config.provider { @@ -5070,6 +5065,11 @@ impl OracleConfigValidator { pub fn validate_oracle_config_all_together( config: &OracleConfig, ) -> Result<(), ValidationError> { + // Reserve the storage-only sentinel so it can never become a live oracle config. + if config.is_none_sentinel() { + return Err(ValidationError::InvalidConfig); + } + // Step 1: Validate provider support Self::validate_oracle_provider(&config.provider)?; @@ -5081,7 +5081,7 @@ impl OracleConfigValidator { // Step 4: Get supported operators and validate comparison let supported_operators = Self::get_supported_operators_for_provider(&config.provider); - Self::validate_comparison_operator(&config.comparison, &supported_operators)?; + Self::validate_comparison_operator_literals(&config.comparison, supported_operators)?; // Step 5: Validate configuration consistency Self::validate_config_consistency(config)?; @@ -5095,7 +5095,7 @@ impl OracleConfigValidator { /// * `provider` - The oracle provider to get operators for /// /// # Returns - /// * `Vec` - Vector of supported comparison operators + /// * `&[&str]` - Provider-specific supported comparison operators /// /// # Provider-Specific Operators /// @@ -5113,29 +5113,29 @@ impl OracleConfigValidator { /// /// **Band Protocol & DIA:** /// - Empty vector (not supported) - fn get_supported_operators_for_provider(provider: &OracleProvider) -> Vec { + fn validate_comparison_operator_literals( + comparison: &String, + supported_operators: &[&str], + ) -> Result<(), ValidationError> { + if comparison.is_empty() { + return Err(ValidationError::InvalidOracle); + } + + if supported_operators + .iter() + .any(|operator| comparison == &String::from_str(comparison.env(), operator)) + { + return Ok(()); + } + + Err(ValidationError::InvalidOracle) + } + + fn get_supported_operators_for_provider(provider: &OracleProvider) -> &'static [&'static str] { match provider { - OracleProvider::Reflector => { - vec![ - &soroban_sdk::Env::default(), - String::from_str(&soroban_sdk::Env::default(), "gt"), - String::from_str(&soroban_sdk::Env::default(), "lt"), - String::from_str(&soroban_sdk::Env::default(), "eq"), - ] - } - OracleProvider::Pyth => { - vec![ - &soroban_sdk::Env::default(), - String::from_str(&soroban_sdk::Env::default(), "gt"), - String::from_str(&soroban_sdk::Env::default(), "gte"), - String::from_str(&soroban_sdk::Env::default(), "lt"), - String::from_str(&soroban_sdk::Env::default(), "lte"), - String::from_str(&soroban_sdk::Env::default(), "eq"), - ] - } - OracleProvider::BandProtocol | OracleProvider::DIA => { - vec![&soroban_sdk::Env::default()] - } + OracleProvider::Reflector => &["gt", "lt", "eq"], + OracleProvider::Pyth => &["gt", "gte", "lt", "lte", "eq"], + OracleProvider::BandProtocol | OracleProvider::DIA => &[], } } } diff --git a/contracts/predictify-hybrid/src/validation_tests.rs b/contracts/predictify-hybrid/src/validation_tests.rs index aaf5507d..daae5fbf 100644 --- a/contracts/predictify-hybrid/src/validation_tests.rs +++ b/contracts/predictify-hybrid/src/validation_tests.rs @@ -918,6 +918,7 @@ fn test_validation_error_messages() { #[cfg(test)] mod oracle_config_validator_tests { use super::*; + use crate::oracles::OracleFactory; use crate::types::{OracleConfig, OracleProvider}; use crate::validation::OracleConfigValidator; @@ -1435,6 +1436,40 @@ mod oracle_config_validator_tests { // Overall validation should fail due to provider not being supported assert!(OracleConfigValidator::validate_oracle_config_all_together(&pyth_config).is_err()); } + + #[test] + fn test_none_sentinel_is_reserved_and_invalid() { + let env = soroban_sdk::Env::default(); + let sentinel = OracleConfig::none_sentinel(&env); + + assert!(sentinel.is_none_sentinel()); + assert_eq!(sentinel.validate(&env), Err(Error::InvalidOracleConfig)); + assert!(OracleConfigValidator::validate_oracle_config_all_together(&sentinel).is_err()); + assert!( + OracleFactory::create_from_config(&sentinel, sentinel.oracle_address.clone()).is_err() + ); + assert!(OracleFactory::validate_stellar_compatibility(&sentinel).is_err()); + } + + #[test] + fn test_valid_config_does_not_collide_with_none_sentinel() { + let env = soroban_sdk::Env::default(); + let sentinel = OracleConfig::none_sentinel(&env); + let valid_with_placeholder_address = OracleConfig::new( + OracleProvider::Reflector, + sentinel.oracle_address.clone(), + String::from_str(&env, "BTC/USD"), + 50_000_00, + String::from_str(&env, "gt"), + ); + + assert!(!valid_with_placeholder_address.is_none_sentinel()); + assert!(valid_with_placeholder_address.validate(&env).is_ok()); + assert!(OracleConfigValidator::validate_oracle_config_all_together( + &valid_with_placeholder_address + ) + .is_ok()); + } } // ===== COMPREHENSIVE INPUT VALIDATION TESTS ===== diff --git a/docs/contracts/TYPES_SYSTEM.md b/docs/contracts/TYPES_SYSTEM.md index f081de4d..2038773e 100644 --- a/docs/contracts/TYPES_SYSTEM.md +++ b/docs/contracts/TYPES_SYSTEM.md @@ -1,374 +1,568 @@ -# Predictify Hybrid Types System (Code-Synced) - -This document is the canonical field/variant reference for every `#[contracttype]` defined in `/contracts/predictify-hybrid/src/types.rs`. - -## Scope - -- Source of truth: `/contracts/predictify-hybrid/src/types.rs` -- Included here: all `#[contracttype]` enums and structs in that file -- Not included: non-`contracttype` helper structs (for example, `OraclePriceData`) - -## Enums - -### `MarketState` -- `Active` -- `Ended` -- `Disputed` -- `Resolved` -- `Closed` -- `Cancelled` - -### `OracleProvider` -- `Reflector` -- `Pyth` -- `BandProtocol` -- `DIA` - -### `ReflectorAsset` -- `Stellar` -- `BTC` -- `ETH` -- `Other(Symbol)` - -### `OracleVerificationStatus` -- `Pending` -- `InProgress` -- `Verified` -- `InvalidSignature` -- `StaleData` -- `OracleUnavailable` -- `ThresholdNotMet` -- `NoConsensus` - -### `MarketStatus` -- `Active` -- `Ended` -- `Disputed` -- `Resolved` -- `Closed` -- `Cancelled` - -### `BetStatus` -- `Active` -- `Won` -- `Lost` -- `Refunded` -- `Cancelled` - -### `EventVisibility` -- `Public` -- `Private` - -## Structs - -### `OracleConfig` -| Field | Type | -|---|---| -| `provider` | `OracleProvider` | -| `oracle_address` | `Address` | -| `feed_id` | `String` | -| `threshold` | `i128` | -| `comparison` | `String` | - -### `Market` -| Field | Type | -|---|---| -| `admin` | `Address` | -| `question` | `String` | -| `outcomes` | `Vec` | -| `end_time` | `u64` | -| `oracle_config` | `OracleConfig` | -| `has_fallback` | `bool` | -| `fallback_oracle_config` | `OracleConfig` | -| `resolution_timeout` | `u64` | -| `oracle_result` | `Option` | -| `votes` | `Map` | -| `stakes` | `Map` | -| `claimed` | `Map` | -| `total_staked` | `i128` | -| `dispute_stakes` | `Map` | -| `winning_outcomes` | `Option>` | -| `fee_collected` | `bool` | -| `state` | `MarketState` | -| `total_extension_days` | `u32` | -| `max_extension_days` | `u32` | -| `extension_history` | `Vec` | -| `category` | `Option` | -| `tags` | `Vec` | -| `min_pool_size` | `Option` | -| `bet_deadline` | `u64` | -| `dispute_window_seconds` | `u64` | - -### `BetLimits` -| Field | Type | -|---|---| -| `min_bet` | `i128` | -| `max_bet` | `i128` | - -### `EventHistoryEntry` -| Field | Type | -|---|---| -| `market_id` | `Symbol` | -| `question` | `String` | -| `outcomes` | `Vec` | -| `end_time` | `u64` | -| `created_at` | `u64` | -| `state` | `MarketState` | -| `winning_outcome` | `Option` | -| `total_staked` | `i128` | -| `archived_at` | `Option` | -| `category` | `String` | -| `tags` | `Vec` | - -### `PlatformStatistics` -| Field | Type | -|---|---| -| `total_events_created` | `u64` | -| `total_bets_placed` | `u64` | -| `total_volume` | `i128` | -| `total_fees_collected` | `i128` | -| `active_events_count` | `u32` | - -### `UserStatistics` -| Field | Type | -|---|---| -| `total_bets_placed` | `u64` | -| `total_amount_wagered` | `i128` | -| `total_winnings` | `i128` | -| `total_bets_won` | `u64` | -| `win_rate` | `u32` | -| `last_activity_ts` | `u64` | - -### `OracleResult` -| Field | Type | -|---|---| -| `market_id` | `Symbol` | -| `outcome` | `String` | -| `price` | `i128` | -| `threshold` | `i128` | -| `comparison` | `String` | -| `provider` | `OracleProvider` | -| `feed_id` | `String` | -| `timestamp` | `u64` | -| `block_number` | `u32` | -| `is_verified` | `bool` | -| `confidence_score` | `u32` | -| `sources_count` | `u32` | -| `signature` | `Option` | -| `error_message` | `Option` | - -### `GlobalOracleValidationConfig` -| Field | Type | -|---|---| -| `max_staleness_secs` | `u64` | -| `max_confidence_bps` | `u32` | - -### `EventOracleValidationConfig` -| Field | Type | -|---|---| -| `max_staleness_secs` | `u64` | -| `max_confidence_bps` | `u32` | - -### `MultiOracleResult` -| Field | Type | -|---|---| -| `market_id` | `Symbol` | -| `final_outcome` | `String` | -| `individual_results` | `Vec` | -| `consensus_reached` | `bool` | -| `consensus_threshold` | `u32` | -| `agreement_percentage` | `u32` | -| `timestamp` | `u64` | - -### `OracleSource` -| Field | Type | -|---|---| -| `source_id` | `Symbol` | -| `provider` | `OracleProvider` | -| `contract_address` | `Address` | -| `weight` | `u32` | -| `is_active` | `bool` | -| `priority` | `u32` | -| `last_success` | `u64` | -| `failure_count` | `u32` | - -### `OracleFetchRequest` -| Field | Type | -|---|---| -| `market_id` | `Symbol` | -| `feed_id` | `String` | -| `max_data_age` | `u64` | -| `required_confirmations` | `u32` | -| `use_fallback` | `bool` | -| `min_confidence` | `u32` | - -### `ReflectorPriceData` -| Field | Type | -|---|---| -| `price` | `i128` | -| `timestamp` | `u64` | -| `source` | `String` | - -### `MarketExtension` -| Field | Type | -|---|---| -| `additional_days` | `u32` | -| `admin` | `Address` | -| `reason` | `String` | -| `fee_amount` | `i128` | -| `timestamp` | `u64` | - -### `ExtensionStats` -| Field | Type | -|---|---| -| `total_extensions` | `u32` | -| `total_extension_days` | `u32` | -| `max_extension_days` | `u32` | -| `can_extend` | `bool` | -| `extension_fee_per_day` | `i128` | - -### `MarketCreationParams` -| Field | Type | -|---|---| -| `admin` | `Address` | -| `question` | `String` | -| `outcomes` | `Vec` | -| `duration_days` | `u32` | -| `oracle_config` | `OracleConfig` | -| `creation_fee` | `i128` | - -### `CommunityConsensus` -| Field | Type | -|---|---| -| `outcome` | `String` | -| `votes` | `u32` | -| `total_votes` | `u32` | -| `percentage` | `i128` | - -### `MarketPauseInfo` -| Field | Type | -|---|---| -| `is_paused` | `bool` | -| `paused_at` | `u64` | -| `pause_duration_hours` | `u32` | -| `paused_by` | `Address` | -| `pause_end_time` | `u64` | -| `original_state` | `MarketState` | - -### `EventDetailsQuery` -| Field | Type | -|---|---| -| `market_id` | `Symbol` | -| `question` | `String` | -| `outcomes` | `Vec` | -| `created_at` | `u64` | -| `end_time` | `u64` | -| `status` | `MarketStatus` | -| `oracle_provider` | `String` | -| `feed_id` | `String` | -| `total_staked` | `i128` | -| `winning_outcome` | `Option` | -| `oracle_result` | `Option` | -| `participant_count` | `u32` | -| `vote_count` | `u32` | -| `admin` | `Address` | - -### `UserBetQuery` -| Field | Type | -|---|---| -| `user` | `Address` | -| `market_id` | `Symbol` | -| `outcome` | `String` | -| `stake_amount` | `i128` | -| `voted_at` | `u64` | -| `is_winning` | `bool` | -| `has_claimed` | `bool` | -| `potential_payout` | `i128` | -| `dispute_stake` | `i128` | - -### `UserBalanceQuery` -| Field | Type | -|---|---| -| `user` | `Address` | -| `available_balance` | `i128` | -| `total_staked` | `i128` | -| `total_winnings` | `i128` | -| `active_bet_count` | `u32` | -| `resolved_market_count` | `u32` | -| `unclaimed_balance` | `i128` | - -### `MarketPoolQuery` -| Field | Type | -|---|---| -| `market_id` | `Symbol` | -| `total_pool` | `i128` | -| `outcome_pools` | `Map` | -| `platform_fees` | `i128` | -| `implied_probability_yes` | `u32` | -| `implied_probability_no` | `u32` | - -### `ContractStateQuery` -| Field | Type | -|---|---| -| `total_markets` | `u32` | -| `active_markets` | `u32` | -| `resolved_markets` | `u32` | -| `total_value_locked` | `i128` | -| `total_fees_collected` | `i128` | -| `unique_users` | `u32` | -| `contract_version` | `String` | -| `last_update` | `u64` | - -### `MultipleBetsQuery` -| Field | Type | -|---|---| -| `bets` | `Vec` | -| `total_stake` | `i128` | -| `total_potential_payout` | `i128` | -| `winning_bets` | `u32` | - -### `Bet` -| Field | Type | -|---|---| -| `user` | `Address` | -| `market_id` | `Symbol` | -| `outcome` | `String` | -| `amount` | `i128` | -| `timestamp` | `u64` | -| `status` | `BetStatus` | - -### `BetStats` -| Field | Type | -|---|---| -| `total_bets` | `u32` | -| `total_amount_locked` | `i128` | -| `unique_bettors` | `u32` | -| `outcome_totals` | `Map` | - -### `Event` -| Field | Type | -|---|---| -| `id` | `Symbol` | -| `description` | `String` | -| `outcomes` | `Vec` | -| `end_time` | `u64` | -| `oracle_config` | `OracleConfig` | -| `has_fallback` | `bool` | -| `fallback_oracle_config` | `OracleConfig` | -| `resolution_timeout` | `u64` | -| `admin` | `Address` | -| `created_at` | `u64` | -| `status` | `MarketState` | -| `visibility` | `EventVisibility` | -| `allowlist` | `Vec
` | - -### `Balance` -| Field | Type | -|---|---| -| `user` | `Address` | -| `asset` | `ReflectorAsset` | -| `amount` | `i128` | - -## Maintenance Rule - -When adding, removing, or renaming any field or variant in `types.rs`, update this document in the same PR. +# Predictify Hybrid Types System + +## Overview + +The Predictify Hybrid contract now features a comprehensive, organized type system that centralizes all data structures and provides better organization, validation, and maintainability. This document outlines the architecture, usage patterns, and best practices for working with the types system. + +## Architecture + +### Type Categories + +Types are organized into logical categories for better understanding and maintenance: + +1. **Oracle Types** - Oracle providers, configurations, and data structures +2. **Market Types** - Market data structures and state management +3. **Price Types** - Price data and validation structures +4. **Validation Types** - Input validation and business logic types +5. **Utility Types** - Helper types and conversion utilities + +### Core Components + +#### 1. Oracle Types + +**OracleProvider Enum** +```rust +pub enum OracleProvider { + BandProtocol, + DIA, + Reflector, + Pyth, +} +``` + +**OracleConfig Struct** +```rust +pub struct OracleConfig { + pub provider: OracleProvider, + pub oracle_address: Address, + pub feed_id: String, + pub threshold: i128, + pub comparison: String, +} +``` + +**Fallback Sentinel Encoding** + +Optional fallback oracles are stored as a `(has_fallback, fallback_oracle_config)` pair rather +than `Option`. When `has_fallback` is `false`, the contracts write +`OracleConfig::none_sentinel()` into `fallback_oracle_config`. + +The reserved sentinel semantics are: + +- `provider == OracleProvider::Reflector` +- `feed_id == ""` +- `threshold == 0` +- `comparison == ""` + +That tuple is intentionally outside the valid oracle-config domain: + +- valid configs require a non-empty `feed_id` +- valid configs require `threshold > 0` +- valid configs require a supported comparison operator such as `gt`, `lt`, or `eq` + +Because of those invariants, the sentinel cannot collide with any configuration that is valid for +live oracle creation or resolution, even if the placeholder `oracle_address` is reused elsewhere. + +#### 2. Market Types + +**Market Struct** +```rust +pub struct Market { + pub admin: Address, + pub question: String, + pub outcomes: Vec, + pub end_time: u64, + pub oracle_config: OracleConfig, + // ... other fields +} +``` + +#### 3. Price Types + +**PythPrice Struct** +```rust +pub struct PythPrice { + pub price: i128, + pub conf: u64, + pub expo: i32, + pub publish_time: u64, +} +``` + +**ReflectorPriceData Struct** +```rust +pub struct ReflectorPriceData { + pub price: i128, + pub timestamp: u64, +} +``` + +## Usage Patterns + +### 1. Creating Oracle Configurations + +```rust +use soroban_sdk::{Address, String}; +use types::{OracleConfig, OracleProvider}; + +let oracle_address = Address::generate(&env); + +let oracle_config = OracleConfig::new( + OracleProvider::Pyth, + oracle_address, + String::from_str(&env, "BTC/USD"), + 2500000, // $25,000 threshold + String::from_str(&env, "gt"), // greater than +); + +// Validate the configuration +oracle_config.validate(&env)?; +``` + +### 2. Creating Markets + +```rust +use types::{Market, OracleConfig, OracleProvider}; + +let market = Market::new( + &env, + admin, + question, + outcomes, + end_time, + oracle_config, +); + +// Validate market parameters +market.validate(&env)?; +``` + +### 3. Market State Management + +```rust +use types::MarketState; + +let state = MarketState::from_market(&market, &env); + +if state.is_active() { + // Market is accepting votes +} else if state.has_ended() { + // Market has ended +} else if state.is_resolved() { + // Market is resolved +} +``` + +### 4. Oracle Result Handling + +```rust +use types::OracleResult; + +let result = OracleResult::price(2500000); + +if result.is_available() { + if let Some(price) = result.get_price() { + // Use the price + } +} +``` + +## Type Validation + +### Built-in Validation + +All types include built-in validation methods: + +```rust +// Oracle configuration validation +oracle_config.validate(&env)?; + +// Market validation +market.validate(&env)?; + +// Price validation +pyth_price.validate()?; +``` + +### Validation Helpers + +The types module provides validation helper functions: + +```rust +use types::validation; + +// Validate oracle provider +validation::validate_oracle_provider(&OracleProvider::Pyth)?; + +// Validate price +validation::validate_price(2500000)?; + +// Validate stake +validation::validate_stake(stake, min_stake)?; + +// Validate duration +validation::validate_duration(30)?; +``` + +## Type Conversion + +### Conversion Helpers + +```rust +use types::conversion; + +// Convert string to oracle provider +let provider = conversion::string_to_oracle_provider("pyth") + .ok_or(Error::InvalidOracleConfig)?; + +// Convert oracle provider to string +let provider_name = conversion::oracle_provider_to_string(&provider); + +// Validate comparison operator +conversion::validate_comparison(&comparison, &env)?; +``` + +## Market Operations + +### Market State Queries + +```rust +// Check if market is active +if market.is_active(&env) { + // Accept votes +} + +// Check if market has ended +if market.has_ended(&env) { + // Resolve market +} + +// Check if market is resolved +if market.is_resolved() { + // Allow claims +} +``` + +### User Operations + +```rust +// Get user's vote +let user_vote = market.get_user_vote(&user); + +// Get user's stake +let user_stake = market.get_user_stake(&user); + +// Check if user has claimed +let has_claimed = market.has_user_claimed(&user); + +// Get user's dispute stake +let dispute_stake = market.get_user_dispute_stake(&user); +``` + +### Market Modifications + +```rust +// Add vote and stake +market.add_vote(user, outcome, stake); + +// Add dispute stake +market.add_dispute_stake(user, stake); + +// Mark user as claimed +market.mark_claimed(user); + +// Set oracle result +market.set_oracle_result(result); + +// Set winning outcome +market.set_winning_outcome(outcome); + +// Mark fees as collected +market.mark_fees_collected(); +``` + +### Market Calculations + +```rust +// Get total dispute stakes +let total_disputes = market.total_dispute_stakes(); + +// Get winning stake total +let winning_total = market.winning_stake_total(); +``` + +## Oracle Integration + +### Oracle Provider Support + +```rust +// Check if provider is supported +if oracle_provider.is_supported() { + // Use the provider +} + +// Get provider name +let name = oracle_provider.name(); + +// Get default feed format +let format = oracle_provider.default_feed_format(); +``` + +### Oracle Configuration + +```rust +// Check comparison operators +if oracle_config.is_greater_than(&env) { + // Handle greater than comparison +} else if oracle_config.is_less_than(&env) { + // Handle less than comparison +} else if oracle_config.is_equal_to(&env) { + // Handle equal to comparison +} +``` + +## Price Data Handling + +### Pyth Price Data + +```rust +let pyth_price = PythPrice::new(2500000, 1000, -2, timestamp); + +// Get price in cents +let price_cents = pyth_price.price_in_cents(); + +// Check if price is stale (manual check) +if env.ledger().timestamp() > pyth_price.publish_time + max_age { + // Handle stale price +} + +// Validate price data +pyth_price.validate()?; +``` + +### Reflector Price Data + +```rust +let reflector_price = ReflectorPriceData::new(2500000, timestamp); + +// Get price in cents +let price_cents = reflector_price.price_in_cents(); + +// Check if price is stale (manual check) +if env.ledger().timestamp() > reflector_price.timestamp + max_age { + // Handle stale price +} + +// Validate price data +reflector_price.validate()?; +``` + +## Validation Types + +### Market Creation Parameters + +```rust +let params = MarketCreationParams::new( + admin, + question, + outcomes, + duration_days, + oracle_config, +); + +// Validate all parameters +params.validate(&env)?; + +// Calculate end time +let end_time = params.calculate_end_time(&env); +``` + +### Vote Parameters + +```rust +let vote_params = VoteParams::new(user, outcome, stake); + +// Validate vote parameters +vote_params.validate(&env, &market)?; +``` + +## Best Practices + +### 1. Always Validate Types + +```rust +// ❌ Don't skip validation +let market = Market::new(&env, admin, question, outcomes, end_time, oracle_config); + +// ✅ Always validate +let market = Market::new(&env, admin, question, outcomes, end_time, oracle_config); +market.validate(&env)?; +``` + +### 2. Use Type-Safe Operations + +```rust +// ❌ Manual state checking +if current_time < market.end_time && market.winning_outcome.is_none() { + // Market is active +} + +// ✅ Use type-safe methods +if market.is_active(current_time) { + // Market is active +} +``` + +### 3. Leverage Built-in Methods + +```rust +// ❌ Manual calculations +let mut total = 0; +for (user, outcome) in market.votes.iter() { + if &outcome == winning_outcome { + total += market.stakes.get(user.clone()).unwrap_or(0); + } +} + +// ✅ Use built-in methods +let total = market.winning_stake_total(); +``` + +### 4. Use Validation Helpers + +```rust +// ❌ Manual validation +if stake < min_stake { + return Err(Error::InsufficientStake); +} + +// ✅ Use validation helpers +validation::validate_stake(stake, min_stake)?; +``` + +### 5. Handle Oracle Results Safely + +```rust +// ❌ Direct access +let price = oracle_result.price; + +// ✅ Safe access +if let Some(price) = oracle_result.get_price() { + // Use the price +} +``` + +## Testing + +### Type Testing + +The types module includes comprehensive tests: + +```rust +#[test] +fn test_oracle_provider() { + let provider = OracleProvider::Pyth; + assert_eq!(provider.name(), "Pyth Network"); + assert!(provider.is_supported()); +} + +#[test] +fn test_market_creation() { + let market = Market::new(&env, admin, question, outcomes, end_time, oracle_config); + assert!(market.is_active(&env)); + assert!(!market.is_resolved()); +} + +#[test] +fn test_validation_helpers() { + assert!(validation::validate_oracle_provider(&OracleProvider::Pyth).is_ok()); + assert!(validation::validate_price(2500000).is_ok()); +} +``` + +## Migration Guide + +### From Direct Type Usage + +1. **Replace direct struct creation**: + ```rust + // Old + let market = Market { /* fields */ }; + + // New + let market = Market::new(&env, admin, question, outcomes, end_time, oracle_config); + ``` + +2. **Use validation methods**: + ```rust + // Old + if threshold <= 0 { return Err(Error::InvalidThreshold); } + + // New + oracle_config.validate(&env)?; + ``` + +3. **Use type-safe operations**: + ```rust + // Old + if current_time < market.end_time { /* active */ } + + // New + if market.is_active(&env) { /* active */ } + ``` + +## Type Reference + +### Oracle Types + +| Type | Purpose | Key Methods | +|------|---------|-------------| +| `OracleProvider` | Oracle service enumeration | `name()`, `is_supported()`, `default_feed_format()` | +| `OracleConfig` | Oracle configuration | `new()`, `validate()`, `is_supported()`, `is_greater_than()` | +| `PythPrice` | Pyth price data | `new()`, `price_in_cents()`, `is_stale()`, `validate()` | +| `ReflectorPriceData` | Reflector price data | `new()`, `price_in_cents()`, `is_stale()`, `validate()` | + +### Market Types + +| Type | Purpose | Key Methods | +|------|---------|-------------| +| `Market` | Market data structure | `new()`, `validate()`, `is_active()`, `add_vote()` | +| `MarketState` | Market state enumeration | `from_market()`, `is_active()`, `has_ended()` | +| `MarketCreationParams` | Market creation parameters | `new()`, `validate()`, `calculate_end_time()` | +| `VoteParams` | Vote parameters | `new()`, `validate()` | + +### Utility Types + +| Type | Purpose | Key Methods | +|------|---------|-------------| +| `OracleResult` | Oracle result wrapper | `price()`, `unavailable()`, `is_available()`, `get_price()` | +| `ReflectorAsset` | Reflector asset types | `stellar()`, `other()`, `is_stellar()`, `is_other()` | + +### Validation Functions + +| Function | Purpose | Parameters | +|----------|---------|------------| +| `validate_oracle_provider()` | Validate oracle provider | `provider: &OracleProvider` | +| `validate_price()` | Validate price value | `price: i128` | +| `validate_stake()` | Validate stake amount | `stake: i128, min_stake: i128` | +| `validate_duration()` | Validate duration | `duration_days: u32` | + +### Conversion Functions + +| Function | Purpose | Parameters | +|----------|---------|------------| +| `string_to_oracle_provider()` | Convert string to provider | `s: &str` | +| `oracle_provider_to_string()` | Convert provider to string | `provider: &OracleProvider` | +| `validate_comparison()` | Validate comparison operator | `comparison: &String, env: &Env` | + +## Future Enhancements + +1. **Type Serialization**: Proper serialization/deserialization support +2. **Type Metrics**: Collection and reporting of type usage statistics +3. **Type Validation**: Enhanced validation with custom rules +4. **Type Events**: Event emission for type state changes +5. **Type Localization**: Support for multiple languages in type messages + +## Conclusion + +The new types system provides a robust foundation for managing data structures in the Predictify Hybrid contract. By following the patterns and best practices outlined in this document, developers can create more maintainable, type-safe, and well-organized code.