diff --git a/contracts/predictify-hybrid/src/bets.rs b/contracts/predictify-hybrid/src/bets.rs index 2fab4bac..ec25ae43 100644 --- a/contracts/predictify-hybrid/src/bets.rs +++ b/contracts/predictify-hybrid/src/bets.rs @@ -25,7 +25,7 @@ use crate::errors::Error; use crate::events::EventEmitter; use crate::markets::{MarketStateManager, MarketUtils, MarketValidator}; use crate::reentrancy_guard::ReentrancyGuard; -use crate::types::{Bet, BetLimits, BetStats, BetStatus, Market, MarketState}; +use crate::types::{Bet, BetLimits, BetStats, BetStatus, EventVisibility, Market, MarketState}; use crate::validation; // ===== CONSTANTS ===== @@ -249,9 +249,12 @@ impl BetManager { // Require authentication from the user user.require_auth(); - // Note: Event visibility checking is disabled to avoid deserialization issues - // when markets and events share the same ID space. Events should use a different - // ID prefix (e.g., "evt_") to enable visibility checks. + if crate::storage::EventManager::has_event(env, &market_id) { + let event = crate::storage::EventManager::get_event(env, &market_id)?; + if event.visibility == EventVisibility::Private && !event.allowlist.contains(&user) { + return Err(Error::Unauthorized); + } + } // Get and validate market let mut market = MarketStateManager::get_market(env, &market_id)?; diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index ff500a47..d38f1dec 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -725,6 +725,9 @@ impl PredictifyHybrid { // Generate a unique collision-resistant event ID (reusing market ID generator) let event_id = MarketIdGenerator::generate_market_id(&env, &admin); + let oracle_config_for_market = oracle_config.clone(); + let fallback_oracle_config_for_market = fallback_oracle_config.clone(); + let (has_fallback, fallback_cfg) = match &fallback_oracle_config { Some(c) => (true, c.clone()), None => (false, OracleConfig::none_sentinel(&env)), @@ -746,6 +749,17 @@ impl PredictifyHybrid { allowlist: Vec::new(&env), }; + let market = Market::new( + &env, + admin.clone(), + description.clone(), + outcomes.clone(), + end_time, + oracle_config_for_market, + fallback_oracle_config_for_market, + resolution_timeout, + MarketState::Active, + ); // Collect creation fee before persisting the event so failed payments abort creation. let creation_fee = match crate::markets::MarketUtils::process_creation_fee(&env, &admin) { Ok(amount) => amount, @@ -755,6 +769,9 @@ impl PredictifyHybrid { // Store the event crate::storage::EventManager::store_event(&env, &event); + // Store a corresponding market for betting paths + env.storage().persistent().set(&event_id, &market); + // Increment active event count for this creator crate::storage::CreatorLimitsManager::increment_active_events(&env, &admin); diff --git a/contracts/predictify-hybrid/src/storage.rs b/contracts/predictify-hybrid/src/storage.rs index 0df70f2d..4c491d89 100644 --- a/contracts/predictify-hybrid/src/storage.rs +++ b/contracts/predictify-hybrid/src/storage.rs @@ -650,22 +650,29 @@ impl StorageOptimizer { pub struct EventManager; impl EventManager { + fn event_storage_key(env: &Env, event_id: &Symbol) -> (Symbol, Symbol) { + (Symbol::new(env, "Event"), event_id.clone()) + } + /// Store a new event in persistent storage pub fn store_event(env: &Env, event: &Event) { - env.storage().persistent().set(&event.id, event); + let key = Self::event_storage_key(env, &event.id); + env.storage().persistent().set(&key, event); } /// Retrieve an event from persistent storage pub fn get_event(env: &Env, event_id: &Symbol) -> Result { + let key = Self::event_storage_key(env, event_id); env.storage() .persistent() - .get(event_id) + .get(&key) .ok_or(Error::MarketNotFound) } /// Check if an event exists pub fn has_event(env: &Env, event_id: &Symbol) -> bool { - env.storage().persistent().has(event_id) + let key = Self::event_storage_key(env, event_id); + env.storage().persistent().has(&key) } /// Update an existing event diff --git a/contracts/predictify-hybrid/src/test.rs b/contracts/predictify-hybrid/src/test.rs index 9bc65db1..81e639fb 100644 --- a/contracts/predictify-hybrid/src/test.rs +++ b/contracts/predictify-hybrid/src/test.rs @@ -167,6 +167,226 @@ impl PredictifyTest { &None, ) } + + pub fn create_test_event(&self, visibility: EventVisibility) -> Symbol { + let client = PredictifyHybridClient::new(&self.env, &self.contract_id); + + let outcomes = vec![ + &self.env, + String::from_str(&self.env, "yes"), + String::from_str(&self.env, "no"), + ]; + + self.env.mock_all_auths(); + client.create_event( + &self.admin, + &String::from_str(&self.env, "Will BTC reach $50k?"), + &outcomes, + &(self.env.ledger().timestamp() + 86400), + &OracleConfig { + provider: OracleProvider::Reflector, + oracle_address: Address::generate(&self.env), + feed_id: String::from_str(&self.env, "BTC"), + threshold: 50_000_00, + comparison: String::from_str(&self.env, "gt"), + }, + &None, + &3600, + &visibility, + ) + } +} + +#[test] +fn test_public_event_allows_any_address_to_bet() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + + let event_id = test.create_test_event(EventVisibility::Public); + let user1 = test.create_funded_user(); + let user2 = test.create_funded_user(); + + test.env.mock_all_auths(); + client.place_bet( + &user1, + &event_id, + &String::from_str(&test.env, "yes"), + &10_000_000i128, + ); + + test.env.mock_all_auths(); + client.place_bet( + &user2, + &event_id, + &String::from_str(&test.env, "no"), + &10_000_000i128, + ); + + let event = client.get_event(&event_id).unwrap(); + assert_eq!(event.visibility, EventVisibility::Public); +} + +#[test] +#[should_panic(expected = "Error(Contract, #100)")] +fn test_private_event_blocks_non_allowlisted_address_from_betting() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + + let event_id = test.create_test_event(EventVisibility::Private); + let allowlisted = test.create_funded_user(); + let non_allowlisted = test.create_funded_user(); + + let addresses = vec![&test.env, allowlisted.clone()]; + test.env.mock_all_auths(); + client.add_to_allowlist(&test.admin, &event_id, &addresses); + + test.env.mock_all_auths(); + client.place_bet( + &non_allowlisted, + &event_id, + &String::from_str(&test.env, "yes"), + &10_000_000i128, + ); +} + +#[test] +fn test_private_event_allows_allowlisted_address_to_bet() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + + let event_id = test.create_test_event(EventVisibility::Private); + let allowlisted = test.create_funded_user(); + + let addresses = vec![&test.env, allowlisted.clone()]; + test.env.mock_all_auths(); + client.add_to_allowlist(&test.admin, &event_id, &addresses); + + test.env.mock_all_auths(); + client.place_bet( + &allowlisted, + &event_id, + &String::from_str(&test.env, "yes"), + &10_000_000i128, + ); + + let event = client.get_event(&event_id).unwrap(); + assert_eq!(event.visibility, EventVisibility::Private); + assert!(event.allowlist.contains(&allowlisted)); +} + +#[test] +#[should_panic(expected = "Error(Contract, #100)")] +fn test_private_event_empty_allowlist_blocks_all_bettors() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + + let event_id = test.create_test_event(EventVisibility::Private); + let user = test.create_funded_user(); + + test.env.mock_all_auths(); + client.place_bet( + &user, + &event_id, + &String::from_str(&test.env, "yes"), + &10_000_000i128, + ); +} + +#[test] +fn test_allowlist_add_remove_and_query_exposure() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + + let event_id = test.create_test_event(EventVisibility::Private); + let user1 = test.create_funded_user(); + let user2 = test.create_funded_user(); + + let addrs = vec![&test.env, user1.clone(), user2.clone()]; + test.env.mock_all_auths(); + client.add_to_allowlist(&test.admin, &event_id, &addrs); + + let event = client.get_event(&event_id).unwrap(); + assert_eq!(event.visibility, EventVisibility::Private); + assert!(event.allowlist.contains(&user1)); + assert!(event.allowlist.contains(&user2)); + + let remove_addrs = vec![&test.env, user1.clone()]; + test.env.mock_all_auths(); + client.remove_from_allowlist(&test.admin, &event_id, &remove_addrs); + + let event = client.get_event(&event_id).unwrap(); + assert!(!event.allowlist.contains(&user1)); + assert!(event.allowlist.contains(&user2)); +} + +#[test] +fn test_switch_visibility_before_first_bet_enforced() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + + let event_id = test.create_test_event(EventVisibility::Public); + + test.env.mock_all_auths(); + client.set_event_visibility(&test.admin, &event_id, &EventVisibility::Private); + + let allowlisted = test.create_funded_user(); + let non_allowlisted = test.create_funded_user(); + + let addrs = vec![&test.env, allowlisted.clone()]; + test.env.mock_all_auths(); + client.add_to_allowlist(&test.admin, &event_id, &addrs); + + test.env.mock_all_auths(); + client.place_bet( + &allowlisted, + &event_id, + &String::from_str(&test.env, "yes"), + &10_000_000i128, + ); + + let event = client.get_event(&event_id).unwrap(); + assert_eq!(event.visibility, EventVisibility::Private); + assert!(event.allowlist.contains(&allowlisted)); + + let unauthorized_err = test.env.as_contract(&test.contract_id, || { + crate::bets::BetManager::place_bet( + &test.env, + non_allowlisted.clone(), + event_id.clone(), + String::from_str(&test.env, "no"), + 10_000_000i128, + ) + }); + assert!(unauthorized_err.is_err()); + assert_eq!(unauthorized_err.unwrap_err(), Error::Unauthorized); +} + +#[test] +fn test_cannot_switch_visibility_after_first_bet() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + + let event_id = test.create_test_event(EventVisibility::Public); + let bettor = test.create_funded_user(); + + test.env.mock_all_auths(); + client.place_bet( + &bettor, + &event_id, + &String::from_str(&test.env, "yes"), + &10_000_000i128, + ); + + let result = test.env.as_contract(&test.contract_id, || { + PredictifyHybrid::set_event_visibility( + test.env.clone(), + test.admin.clone(), + event_id.clone(), + EventVisibility::Private, + ) + }); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), Error::BetsAlreadyPlaced); } // Core functionality tests