From b16c4783ba3950947a9c52d9f2771c96d9c4947c Mon Sep 17 00:00:00 2001 From: Baskarayelu Date: Thu, 26 Feb 2026 13:30:57 +0530 Subject: [PATCH] feat: implement global user and creator blacklists - Added functionality to manage global blacklists and whitelists for users and event creators. - Introduced new error types for blacklisted users and creators. - Implemented methods for adding and removing users from global blacklists and whitelists. - Updated event creation and betting logic to enforce blacklist checks. - Added tests to ensure correct behavior when users or creators are blacklisted. --- contracts/predictify-hybrid/src/errors.rs | 6 + .../src/event_creation_tests.rs | 39 +++++ contracts/predictify-hybrid/src/lib.rs | 146 ++++++++++++++++++ contracts/predictify-hybrid/src/lists.rs | 130 ++++++++++++++++ contracts/predictify-hybrid/src/test.rs | 24 +++ 5 files changed, 345 insertions(+) create mode 100644 contracts/predictify-hybrid/src/lists.rs diff --git a/contracts/predictify-hybrid/src/errors.rs b/contracts/predictify-hybrid/src/errors.rs index 839c809..45dc2da 100644 --- a/contracts/predictify-hybrid/src/errors.rs +++ b/contracts/predictify-hybrid/src/errors.rs @@ -36,6 +36,12 @@ pub enum Error { BetsAlreadyPlaced = 111, /// Insufficient balance InsufficientBalance = 112, + /// User is blocked by global blacklist + UserBlacklisted = 113, + /// User is not in the required global whitelist + UserNotWhitelisted = 114, + /// Event creator is blocked by global blacklist + CreatorBlacklisted = 115, // FundsLocked removed to save space // ===== ORACLE ERRORS ===== diff --git a/contracts/predictify-hybrid/src/event_creation_tests.rs b/contracts/predictify-hybrid/src/event_creation_tests.rs index 55a1361..19c6452 100644 --- a/contracts/predictify-hybrid/src/event_creation_tests.rs +++ b/contracts/predictify-hybrid/src/event_creation_tests.rs @@ -214,6 +214,45 @@ fn test_create_event_unauthorized() { ); } +#[test] +#[should_panic(expected = "HostError: Error(Contract, #115)")] // Error::CreatorBlacklisted = 115 +fn test_create_event_creator_blacklisted_globally() { + let setup = TestSetup::new(); + let client = PredictifyHybridClient::new(&setup.env, &setup.contract_id); + + // Blacklist the admin as a creator globally + let addrs = vec![&setup.env, setup.admin.clone()]; + setup.env.mock_all_auths(); + client.add_creators_to_global_blacklist(&setup.admin, &addrs); + + let description = String::from_str(&setup.env, "Blacklisted creator event?"); + let outcomes = vec![ + &setup.env, + String::from_str(&setup.env, "Yes"), + String::from_str(&setup.env, "No"), + ]; + let end_time = setup.env.ledger().timestamp() + 3600; + let oracle_config = OracleConfig { + provider: OracleProvider::Reflector, + oracle_address: Address::generate(&setup.env), + feed_id: String::from_str(&setup.env, "BTC/USD"), + threshold: 50000, + comparison: String::from_str(&setup.env, "gt"), + }; + + // Now event creation by this admin should fail due to creator blacklist + client.create_event( + &setup.admin, + &description, + &outcomes, + &end_time, + &oracle_config, + &None, + &0, + &EventVisibility::Public, + ); +} + #[test] #[should_panic(expected = "HostError: Error(Contract, #302)")] // Error::InvalidDuration = 302 fn test_create_event_invalid_end_time() { diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index 4609658..b0a0a60 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -42,6 +42,7 @@ mod reentrancy_guard; mod resolution; mod statistics; mod storage; +mod lists; mod types; mod upgrade_manager; mod utils; @@ -715,6 +716,11 @@ impl PredictifyHybrid { panic_with_error!(env, Error::Unauthorized); } + // Enforce global creator blacklist (if configured) + if let Err(e) = crate::lists::AccessLists::require_creator_can_create(&env, &admin) { + panic_with_error!(env, e); + } + // Get market configuration for limits let market_config = crate::config::ConfigManager::get_default_market_config(); @@ -975,6 +981,138 @@ impl PredictifyHybrid { Ok(()) } + /// Adds users to the global betting whitelist (admin only). + pub fn add_users_to_global_whitelist( + env: Env, + admin: Address, + addresses: Vec
, + ) -> Result<(), Error> { + admin.require_auth(); + + let stored_admin: Address = env + .storage() + .persistent() + .get(&Symbol::new(&env, "Admin")) + .ok_or(Error::Unauthorized)?; + + if admin != stored_admin { + return Err(Error::Unauthorized); + } + + crate::lists::AccessLists::add_to_user_whitelist(&env, &addresses); + Ok(()) + } + + /// Removes users from the global betting whitelist (admin only). + pub fn remove_users_from_global_whitelist( + env: Env, + admin: Address, + addresses: Vec
, + ) -> Result<(), Error> { + admin.require_auth(); + + let stored_admin: Address = env + .storage() + .persistent() + .get(&Symbol::new(&env, "Admin")) + .ok_or(Error::Unauthorized)?; + + if admin != stored_admin { + return Err(Error::Unauthorized); + } + + crate::lists::AccessLists::remove_from_user_whitelist(&env, &addresses); + Ok(()) + } + + /// Adds users to the global betting blacklist (admin only). + pub fn add_users_to_global_blacklist( + env: Env, + admin: Address, + addresses: Vec
, + ) -> Result<(), Error> { + admin.require_auth(); + + let stored_admin: Address = env + .storage() + .persistent() + .get(&Symbol::new(&env, "Admin")) + .ok_or(Error::Unauthorized)?; + + if admin != stored_admin { + return Err(Error::Unauthorized); + } + + crate::lists::AccessLists::add_to_user_blacklist(&env, &addresses); + Ok(()) + } + + /// Removes users from the global betting blacklist (admin only). + pub fn remove_users_from_global_blacklist( + env: Env, + admin: Address, + addresses: Vec
, + ) -> Result<(), Error> { + admin.require_auth(); + + let stored_admin: Address = env + .storage() + .persistent() + .get(&Symbol::new(&env, "Admin")) + .ok_or(Error::Unauthorized)?; + + if admin != stored_admin { + return Err(Error::Unauthorized); + } + + crate::lists::AccessLists::remove_from_user_blacklist(&env, &addresses); + Ok(()) + } + + /// Adds event creators to the global creator blacklist (admin only). + pub fn add_creators_to_global_blacklist( + env: Env, + admin: Address, + addresses: Vec
, + ) -> Result<(), Error> { + admin.require_auth(); + + let stored_admin: Address = env + .storage() + .persistent() + .get(&Symbol::new(&env, "Admin")) + .ok_or(Error::Unauthorized)?; + + if admin != stored_admin { + return Err(Error::Unauthorized); + } + + crate::lists::AccessLists::add_to_creator_blacklist(&env, &addresses); + Ok(()) + } + + /// Removes event creators from the global creator blacklist (admin only). + pub fn remove_creators_from_global_blacklist( + env: Env, + admin: Address, + addresses: Vec
, + ) -> Result<(), Error> { + admin.require_auth(); + + let stored_admin: Address = env + .storage() + .persistent() + .get(&Symbol::new(&env, "Admin")) + .ok_or(Error::Unauthorized)?; + + if admin != stored_admin { + return Err(Error::Unauthorized); + } + + crate::lists::AccessLists::remove_from_creator_blacklist(&env, &addresses); + Ok(()) + } + /// Allows users to vote on a market outcome by staking tokens. /// /// This function enables users to participate in prediction markets by voting @@ -1229,6 +1367,10 @@ impl PredictifyHybrid { } Err(_) => panic_with_error!(env, Error::InvalidInput), } + // Enforce global user whitelist/blacklist for betting + if let Err(e) = crate::lists::AccessLists::require_user_can_bet(&env, &user) { + 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) { Ok(bet) => { @@ -1311,6 +1453,10 @@ impl PredictifyHybrid { if ReentrancyGuard::check_reentrancy_state(&env).is_err() { panic_with_error!(env, Error::InvalidState); } + // Enforce global user whitelist/blacklist for betting + if let Err(e) = crate::lists::AccessLists::require_user_can_bet(&env, &user) { + panic_with_error!(env, e); + } match bets::BetManager::place_bets(&env, user, bets) { Ok(placed_bets) => { crate::gas::GasTracker::end_tracking( diff --git a/contracts/predictify-hybrid/src/lists.rs b/contracts/predictify-hybrid/src/lists.rs new file mode 100644 index 0000000..1f6ac50 --- /dev/null +++ b/contracts/predictify-hybrid/src/lists.rs @@ -0,0 +1,130 @@ +use soroban_sdk::{Address, Env, Symbol, Vec}; + +use crate::errors::Error; + +/// Simple global access control lists for users and event creators. +/// +/// Design: +/// - Empty whitelist means "whitelist disabled" (no restriction). +/// - Blacklist always applies when it contains an address. +/// - For betting, blacklist is checked first, then whitelist (if non-empty). +/// - For event creation, a global creator blacklist is enforced. +pub struct AccessLists; + +impl AccessLists { + const USER_WHITELIST_KEY: &'static str = "UserWhitelist"; + const USER_BLACKLIST_KEY: &'static str = "UserBlacklist"; + const CREATOR_BLACKLIST_KEY: &'static str = "CreatorBlacklist"; + + fn get_address_list(env: &Env, key: &'static str) -> Vec
{ + env.storage() + .persistent() + .get(&Symbol::new(env, key)) + .unwrap_or_else(|| Vec::new(env)) + } + + fn set_address_list(env: &Env, key: &'static str, list: &Vec
) { + env.storage() + .persistent() + .set(&Symbol::new(env, key), list); + } + + pub fn add_to_user_whitelist(env: &Env, addresses: &Vec
) { + let mut list = Self::get_address_list(env, Self::USER_WHITELIST_KEY); + for addr in addresses.iter() { + if !list.contains(&addr) { + list.push_back(addr); + } + } + Self::set_address_list(env, Self::USER_WHITELIST_KEY, &list); + } + + pub fn remove_from_user_whitelist(env: &Env, addresses: &Vec
) { + let current = Self::get_address_list(env, Self::USER_WHITELIST_KEY); + let mut filtered = Vec::new(env); + for addr in current.iter() { + if !addresses.contains(&addr) { + filtered.push_back(addr); + } + } + Self::set_address_list(env, Self::USER_WHITELIST_KEY, &filtered); + } + + pub fn add_to_user_blacklist(env: &Env, addresses: &Vec
) { + let mut list = Self::get_address_list(env, Self::USER_BLACKLIST_KEY); + for addr in addresses.iter() { + if !list.contains(&addr) { + list.push_back(addr); + } + } + Self::set_address_list(env, Self::USER_BLACKLIST_KEY, &list); + } + + pub fn remove_from_user_blacklist(env: &Env, addresses: &Vec
) { + let current = Self::get_address_list(env, Self::USER_BLACKLIST_KEY); + let mut filtered = Vec::new(env); + for addr in current.iter() { + if !addresses.contains(&addr) { + filtered.push_back(addr); + } + } + Self::set_address_list(env, Self::USER_BLACKLIST_KEY, &filtered); + } + + pub fn add_to_creator_blacklist(env: &Env, addresses: &Vec
) { + let mut list = Self::get_address_list(env, Self::CREATOR_BLACKLIST_KEY); + for addr in addresses.iter() { + if !list.contains(&addr) { + list.push_back(addr); + } + } + Self::set_address_list(env, Self::CREATOR_BLACKLIST_KEY, &list); + } + + pub fn remove_from_creator_blacklist(env: &Env, addresses: &Vec
) { + let current = Self::get_address_list(env, Self::CREATOR_BLACKLIST_KEY); + let mut filtered = Vec::new(env); + for addr in current.iter() { + if !addresses.contains(&addr) { + filtered.push_back(addr); + } + } + Self::set_address_list(env, Self::CREATOR_BLACKLIST_KEY, &filtered); + } + + /// Enforce global user whitelist/blacklist for betting. + /// + /// Logic: + /// - If user is in global blacklist → `Error::UserBlacklisted` + /// - Else if whitelist is empty → allowed + /// - Else if whitelist contains user → allowed + /// - Else → `Error::UserNotWhitelisted` + pub fn require_user_can_bet(env: &Env, user: &Address) -> Result<(), Error> { + let blacklist = Self::get_address_list(env, Self::USER_BLACKLIST_KEY); + if blacklist.contains(user) { + return Err(Error::UserBlacklisted); + } + + let whitelist = Self::get_address_list(env, Self::USER_WHITELIST_KEY); + if whitelist.is_empty() { + return Ok(()); + } + + if whitelist.contains(user) { + Ok(()) + } else { + Err(Error::UserNotWhitelisted) + } + } + + /// Enforce global creator blacklist for event creation. + pub fn require_creator_can_create(env: &Env, creator: &Address) -> Result<(), Error> { + let blacklist = Self::get_address_list(env, Self::CREATOR_BLACKLIST_KEY); + if blacklist.contains(creator) { + Err(Error::CreatorBlacklisted) + } else { + Ok(()) + } + } +} + diff --git a/contracts/predictify-hybrid/src/test.rs b/contracts/predictify-hybrid/src/test.rs index d197490..faa7517 100644 --- a/contracts/predictify-hybrid/src/test.rs +++ b/contracts/predictify-hybrid/src/test.rs @@ -226,6 +226,30 @@ fn test_public_event_allows_any_address_to_bet() { assert_eq!(event.visibility, EventVisibility::Public); } +#[test] +#[should_panic(expected = "Error(Contract, #113)")] // Error::UserBlacklisted = 113 +fn test_global_blacklist_blocks_bettor() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + + let event_id = test.create_test_event(EventVisibility::Public); + let blocked_user = test.create_funded_user(); + + // Add user to global betting blacklist + let addrs = vec![&test.env, blocked_user.clone()]; + test.env.mock_all_auths(); + client.add_users_to_global_blacklist(&test.admin, &addrs); + + // Betting from this user should now fail due to global blacklist + test.env.mock_all_auths(); + client.place_bet( + &blocked_user, + &event_id, + &String::from_str(&test.env, "yes"), + &10_000_000i128, + ); +} + #[test] #[should_panic(expected = "Error(Contract, #100)")] fn test_private_event_blocks_non_allowlisted_address_from_betting() {