From b724de922b48e9fc25ad5e6bb304ec4eb76225a1 Mon Sep 17 00:00:00 2001 From: DavisVT Date: Sun, 31 May 2026 12:47:44 +0100 Subject: [PATCH] Add circuit breaker checks to all fund-moving entrypoints --- .../predictify-hybrid/src/circuit_breaker.rs | 13 ++- contracts/predictify-hybrid/src/lib.rs | 90 ++++++++++++------- 2 files changed, 70 insertions(+), 33 deletions(-) diff --git a/contracts/predictify-hybrid/src/circuit_breaker.rs b/contracts/predictify-hybrid/src/circuit_breaker.rs index 6ee379bc..1826aa2e 100644 --- a/contracts/predictify-hybrid/src/circuit_breaker.rs +++ b/contracts/predictify-hybrid/src/circuit_breaker.rs @@ -70,7 +70,11 @@ pub struct CircuitBreakerState { #[derive(Clone, Debug, PartialEq, Eq)] #[contracttype] pub enum PauseScope { + /// Pauses only betting operations: place_bet, place_bets BettingOnly, + /// Pauses all fund movement operations: deposit, withdraw, place_bet, claim_winnings, distribute_payouts, collect_fees + FundsOnly, + /// Pauses all operations Full, } @@ -288,7 +292,7 @@ impl CircuitBreaker { } /// Check whether a specific operation is allowed under current pause scope. - /// `op` examples: "betting", "create_event", "withdraw", etc. + /// Supported `op` values: "deposit", "withdraw", "place_bet", "claim_winnings", "distribute_payouts", "collect_fees", "betting", "create_event", etc. pub fn is_operation_allowed(env: &Env, op: &str) -> Result { let state = Self::get_state(env)?; @@ -297,12 +301,17 @@ impl CircuitBreaker { BreakerState::Open => match state.pause_scope { PauseScope::Full => Ok(false), PauseScope::BettingOnly => { - if op == "betting" { + if op == "betting" || op == "place_bet" { Ok(false) } else { Ok(true) } } + PauseScope::FundsOnly => match op { + "deposit" | "withdraw" | "place_bet" | "claim_winnings" + | "distribute_payouts" | "collect_fees" | "betting" => Ok(false), + _ => Ok(true), + }, }, BreakerState::HalfOpen => { let config = Self::get_config(env)?; diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index 8b06f402..a65bd27f 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -47,14 +47,14 @@ mod metadata_limits_tests; mod monitoring; #[cfg(test)] mod multi_admin_multisig_tests; -#[cfg(test)] -mod require_auth_coverage_tests; mod oracles; mod performance_benchmarks; mod queries; mod rate_limiter; mod recovery; mod reentrancy_guard; +#[cfg(test)] +mod require_auth_coverage_tests; mod resolution; mod statistics; mod storage; @@ -69,10 +69,10 @@ mod validation; mod versioning; mod voting; -#[cfg(any())] -mod test_audit_trail; #[cfg(test)] mod override_audit_tests; +#[cfg(any())] +mod test_audit_trail; // #[cfg(any())] // mod utils_tests; // THis is the band protocol wasm std_reference.wasm @@ -333,9 +333,10 @@ impl PredictifyHybrid { events_per_admin_limit: 0, time_window_seconds: 3600, }; - env.storage() - .persistent() - .set(&crate::rate_limiter::RateLimiterData::Config, &rate_limit_config); + env.storage().persistent().set( + &crate::rate_limiter::RateLimiterData::Config, + &rate_limit_config, + ); // Seed default runtime configuration so validators and query paths have // deterministic bounds immediately after deployment. @@ -344,17 +345,19 @@ impl PredictifyHybrid { // Seed permissive-but-valid rate limits so admin entrypoints do not // fail before a custom policy is configured. - crate::rate_limiter::RateLimiter::new(env.clone()).init_rate_limiter( - admin.clone(), - crate::rate_limiter::RateLimitConfig { - voting_limit: 10_000, - dispute_limit: 1_000, - oracle_call_limit: 1_000, - bet_limit: 10_000, - events_per_admin_limit: 1_000, - time_window_seconds: 3_600, - }, - ).map_err(Error::from)?; + crate::rate_limiter::RateLimiter::new(env.clone()) + .init_rate_limiter( + admin.clone(), + crate::rate_limiter::RateLimitConfig { + voting_limit: 10_000, + dispute_limit: 1_000, + oracle_call_limit: 1_000, + bet_limit: 10_000, + events_per_admin_limit: 1_000, + time_window_seconds: 3_600, + }, + ) + .map_err(Error::from)?; // Initialize allowed assets if let Some(assets) = allowed_assets { @@ -441,6 +444,11 @@ impl PredictifyHybrid { asset: ReflectorAsset, amount: i128, ) -> Result { + if let Err(e) = + crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "deposit") + { + return Err(e); + } balances::BalanceManager::deposit(&env, user, asset, amount) } @@ -465,6 +473,11 @@ impl PredictifyHybrid { asset: ReflectorAsset, amount: i128, ) -> Result { + if let Err(e) = + crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "withdraw") + { + return Err(e); + } balances::BalanceManager::withdraw(&env, user, asset, amount) } @@ -1175,6 +1188,11 @@ impl PredictifyHybrid { outcome: String, amount: i128, ) -> crate::types::Bet { + if let Err(e) = + crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "place_bet") + { + 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) => { @@ -1253,6 +1271,11 @@ impl PredictifyHybrid { user: Address, bets: Vec<(Symbol, String, i128)>, ) -> Vec { + if let Err(e) = + crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "place_bet") + { + panic_with_error!(env, e); + } match bets::BetManager::place_bets(&env, user, bets) { Ok(placed_bets) => placed_bets, Err(e) => panic_with_error!(env, e), @@ -1669,6 +1692,11 @@ impl PredictifyHybrid { /// /// State-changing paths may emit events through internal managers; read-only query paths emit no events. pub fn claim_winnings(env: Env, user: Address, market_id: Symbol) { + if let Err(e) = + crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "claim_winnings") + { + panic_with_error!(env, e); + } user.require_auth(); let mut market: Market = env @@ -2884,14 +2912,7 @@ impl PredictifyHybrid { ); // Emit the dedicated override event for off-chain monitors - EventEmitter::emit_admin_override( - &env, - &market_id, - &admin, - &old_result, - &outcome, - &reason, - ); + EventEmitter::emit_admin_override(&env, &market_id, &admin, &old_result, &outcome, &reason); Ok(()) } @@ -3257,6 +3278,11 @@ impl PredictifyHybrid { /// /// State-changing paths may emit events through internal managers; read-only query paths emit no events. pub fn collect_fees(env: Env, admin: Address, market_id: Symbol) -> Result { + if let Err(e) = + crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "collect_fees") + { + return Err(e); + } Self::require_primary_admin(&env, &admin)?; fees::FeeManager::collect_fees(&env, admin, market_id) @@ -3329,6 +3355,12 @@ impl PredictifyHybrid { /// /// Returns [`Error`] when validation, authorization, storage, or subsystem checks fail. pub fn distribute_payouts(env: Env, market_id: Symbol) -> Result { + if let Err(e) = crate::circuit_breaker::CircuitBreaker::require_write_allowed( + &env, + "distribute_payouts", + ) { + return Err(e); + } let mut market: Market = env .storage() .persistent() @@ -3822,11 +3854,7 @@ impl PredictifyHybrid { max_confidence_bps, max_deviation_bps, }; - crate::oracles::OracleValidationConfigManager::set_event_config( - &env, - &market_id, - &config, - )?; + crate::oracles::OracleValidationConfigManager::set_event_config(&env, &market_id, &config)?; let mut details = Map::new(&env); details.set(