Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions contracts/predictify-hybrid/src/bets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =====
Expand Down Expand Up @@ -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)?;
Expand Down
17 changes: 17 additions & 0 deletions contracts/predictify-hybrid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand All @@ -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,
Expand All @@ -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);

Expand Down
13 changes: 10 additions & 3 deletions contracts/predictify-hybrid/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Event, Error> {
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
Expand Down
220 changes: 220 additions & 0 deletions contracts/predictify-hybrid/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading