From c824f8362d644885b9c035514fb7ce364e854689 Mon Sep 17 00:00:00 2001 From: Ebenezer199914 Date: Sat, 27 Jun 2026 12:49:11 +0000 Subject: [PATCH] test(#617): add deterministic event ordering test for resolve_market MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add resolution_event_ordering_tests.rs verifying that resolve_market emits events in the exact sequence: mkt_res → st_chng → idx_transition - Add negative test: no resolution events emitted when resolution fails early (missing oracle result) - Fix CI errors in require_auth_coverage_tests.rs: add missing max_deviation_bps argument to 4 calls of set_oracle_val_cfg_global and set_oracle_val_cfg_event - Register new test module in lib.rs under #[cfg(test)] Closes #617 --- contracts/predictify-hybrid/src/lib.rs | 2 + .../src/require_auth_coverage_tests.rs | 8 +- .../src/resolution_event_ordering_tests.rs | 198 ++++++++++++++++++ 3 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 contracts/predictify-hybrid/src/resolution_event_ordering_tests.rs diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index 96324a3f..0b944211 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -55,6 +55,8 @@ mod recovery; mod reentrancy_guard; #[cfg(test)] mod require_auth_coverage_tests; +#[cfg(test)] +mod resolution_event_ordering_tests; mod resolution; mod statistics; mod storage; diff --git a/contracts/predictify-hybrid/src/require_auth_coverage_tests.rs b/contracts/predictify-hybrid/src/require_auth_coverage_tests.rs index 47c5fcae..4cdb610b 100644 --- a/contracts/predictify-hybrid/src/require_auth_coverage_tests.rs +++ b/contracts/predictify-hybrid/src/require_auth_coverage_tests.rs @@ -1005,7 +1005,7 @@ fn test_set_event_bet_limits_forged_admin_rejected() { #[test] fn test_set_oracle_val_cfg_global_authorized_admin_succeeds() { let (env, cid, admin) = setup(); - let result = client(&env, &cid).try_set_oracle_val_cfg_global(&admin, &300u64, &9500u32); + let result = client(&env, &cid).try_set_oracle_val_cfg_global(&admin, &300u64, &9500u32, &None); assert_auth_ok_contract!(result, "set_oracle_val_cfg_global rejected authorized admin"); } @@ -1014,7 +1014,7 @@ fn test_set_oracle_val_cfg_global_authorized_admin_succeeds() { fn test_set_oracle_val_cfg_global_forged_admin_rejected() { let (env, cid, _admin) = setup(); let attacker = Address::generate(&env); - let result = client(&env, &cid).try_set_oracle_val_cfg_global(&attacker, &300u64, &9500u32); + let result = client(&env, &cid).try_set_oracle_val_cfg_global(&attacker, &300u64, &9500u32, &None); assert_unauthorized_contract!(result); } @@ -1026,7 +1026,7 @@ fn test_set_oracle_val_cfg_event_authorized_admin_succeeds() { let (env, cid, admin) = setup(); let market_id = make_market(&env, &cid, &admin); let result = client(&env, &cid).try_set_oracle_val_cfg_event( - &admin, &market_id, &300u64, &9500u32, + &admin, &market_id, &300u64, &9500u32, &None, ); assert_auth_ok_contract!(result, "set_oracle_val_cfg_event rejected authorized admin"); } @@ -1038,7 +1038,7 @@ fn test_set_oracle_val_cfg_event_forged_admin_rejected() { let market_id = make_market(&env, &cid, &admin); let attacker = Address::generate(&env); let result = client(&env, &cid).try_set_oracle_val_cfg_event( - &attacker, &market_id, &300u64, &9500u32, + &attacker, &market_id, &300u64, &9500u32, &None, ); assert_unauthorized_contract!(result); } diff --git a/contracts/predictify-hybrid/src/resolution_event_ordering_tests.rs b/contracts/predictify-hybrid/src/resolution_event_ordering_tests.rs new file mode 100644 index 00000000..5caad224 --- /dev/null +++ b/contracts/predictify-hybrid/src/resolution_event_ordering_tests.rs @@ -0,0 +1,198 @@ +//! Issue #617 – Deterministic event ordering test for `resolve_market`. +//! +//! Verifies that `MarketResolutionManager::resolve_market` emits the three +//! resolution-signalling events in the exact, deterministic sequence: +//! +//! 1. `mkt_res` – market resolved (`emit_market_resolved`) +//! 2. `st_chng` – state change (`emit_state_change_event`) +//! 3. `idx_transition` – indexer hook (`emit_resolution_transition_hook`) +//! +//! The full event stream from resolve_market also includes a `market_state_change` +//! event (emitted by `set_winning_outcomes`) and storage events from `store_event`. +//! This test verifies the relative ordering of the three resolution events only. + +#[cfg(test)] +mod resolution_event_ordering_tests { + use crate::config::ConfigManager; + use crate::resolution::MarketResolutionManager; + use crate::types::{Market, MarketState, OracleConfig, OracleProvider}; + use crate::PredictifyHybrid; + use soroban_sdk::testutils::{Address as _, Events, Ledger, LedgerInfo}; + use soroban_sdk::{symbol_short, xdr, Address, Env, String, Symbol, TryIntoVal, Vec}; + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + struct Setup { + env: Env, + contract_id: Address, + admin: Address, + } + + impl Setup { + fn new() -> Self { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let contract_id = env.register_contract(None, PredictifyHybrid); + env.as_contract(&contract_id, || { + let cfg = ConfigManager::get_development_config(&env); + ConfigManager::store_config(&env, &cfg).unwrap(); + }); + Self { env, contract_id, admin } + } + + /// Store a market in `Ended` state, with oracle result set and one vote. + fn store_ready_market(&self, market_id: &Symbol) { + let end_time = self.env.ledger().timestamp().saturating_sub(10); + let mut outcomes = Vec::new(&self.env); + outcomes.push_back(String::from_str(&self.env, "yes")); + outcomes.push_back(String::from_str(&self.env, "no")); + let oracle_cfg = OracleConfig::new( + OracleProvider::reflector(), + Address::from_str( + &self.env, + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + ), + String::from_str(&self.env, "BTC/USD"), + 50_000_00, + String::from_str(&self.env, "gt"), + ); + let mut market = Market::new( + &self.env, + self.admin.clone(), + String::from_str(&self.env, "Will BTC reach $50k?"), + outcomes, + end_time, + oracle_cfg, + None, + 86400, + MarketState::Ended, + ); + market.oracle_result = Some(String::from_str(&self.env, "yes")); + market.votes.set(self.admin.clone(), String::from_str(&self.env, "yes")); + market.stakes.set(self.admin.clone(), 1_000_000_i128); + market.total_staked = 1_000_000_i128; + self.env.storage().persistent().set(market_id, &market); + } + } + + /// Try to extract the first topic Symbol from an XDR ContractEvent. + /// Returns None if the first topic is not a Symbol (e.g. a String). + fn first_topic_sym(env: &Env, event: &xdr::ContractEvent) -> Option { + let v0 = match &event.body { + xdr::ContractEventBody::V0(v0) => v0, + }; + let scval = v0.topics.get(0)?; + scval.clone().try_into_val(env).ok() + } + + // --------------------------------------------------------------------------- + // Issue #617 – core deterministic ordering test + // --------------------------------------------------------------------------- + + /// Verifies that `resolve_market` emits `mkt_res`, `st_chng`, and + /// `idx_transition` in that exact order relative to one another. + #[test] + fn test_resolve_market_emits_events_in_deterministic_order() { + let setup = Setup::new(); + let market_id = Symbol::new(&setup.env, "mkt_617"); + + setup.env.as_contract(&setup.contract_id, || { + setup.store_ready_market(&market_id); + + let count_before = setup.env.events().all().events().len(); + + MarketResolutionManager::resolve_market(&setup.env, &market_id) + .expect("resolve_market should succeed"); + + let all = setup.env.events().all(); + let emitted = &all.events()[count_before..]; + + assert!(!emitted.is_empty(), "resolve_market must emit at least one event"); + + // Collect the indices (relative positions) of the three key events. + let mkt_res_sym = symbol_short!("mkt_res"); + let st_chng_sym = symbol_short!("st_chng"); + let idx_trans_sym = Symbol::new(&setup.env, "idx_transition"); + + let pos_mkt_res = emitted + .iter() + .position(|e| first_topic_sym(&setup.env, e) == Some(mkt_res_sym.clone())) + .expect("mkt_res event must be emitted by resolve_market"); + + let pos_st_chng = emitted + .iter() + .position(|e| first_topic_sym(&setup.env, e) == Some(st_chng_sym.clone())) + .expect("st_chng event must be emitted by resolve_market"); + + let pos_idx = emitted + .iter() + .position(|e| first_topic_sym(&setup.env, e) == Some(idx_trans_sym.clone())) + .expect("idx_transition event must be emitted by resolve_market"); + + // Deterministic ordering: mkt_res → st_chng → idx_transition + assert!( + pos_mkt_res < pos_st_chng, + "mkt_res (pos={}) must come before st_chng (pos={})", + pos_mkt_res, + pos_st_chng + ); + assert!( + pos_st_chng < pos_idx, + "st_chng (pos={}) must come before idx_transition (pos={})", + pos_st_chng, + pos_idx + ); + }); + } + + /// Sanity check: no `mkt_res`, `st_chng`, or `idx_transition` events are + /// emitted when resolution fails early (no oracle result available). + #[test] + fn test_no_resolution_events_emitted_when_resolution_fails() { + let setup = Setup::new(); + let market_id = Symbol::new(&setup.env, "mkt_no_res"); + + setup.env.as_contract(&setup.contract_id, || { + let end_time = setup.env.ledger().timestamp().saturating_sub(10); + let mut outcomes = Vec::new(&setup.env); + outcomes.push_back(String::from_str(&setup.env, "yes")); + outcomes.push_back(String::from_str(&setup.env, "no")); + let oracle_cfg = OracleConfig::new( + OracleProvider::reflector(), + Address::from_str( + &setup.env, + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + ), + String::from_str(&setup.env, "BTC/USD"), + 50_000_00, + String::from_str(&setup.env, "gt"), + ); + // No oracle_result — resolution must fail before emitting. + let market = Market::new( + &setup.env, + setup.admin.clone(), + String::from_str(&setup.env, "No oracle"), + outcomes, + end_time, + oracle_cfg, + None, + 86400, + MarketState::Ended, + ); + setup.env.storage().persistent().set(&market_id, &market); + + let count_before = setup.env.events().all().events().len(); + let result = MarketResolutionManager::resolve_market(&setup.env, &market_id); + assert!(result.is_err(), "should fail without oracle result"); + let count_after = setup.env.events().all().events().len(); + + assert_eq!( + count_before, count_after, + "no events should be emitted on failed resolution" + ); + }); + } +}