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
10 changes: 10 additions & 0 deletions contracts/predictify-hybrid/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@ pub enum Error {
CircuitBreakerNotOpen = 502,
/// Circuit breaker is open (operations blocked)
CircuitBreakerOpen = 503,

// ===== REENTRANCY AND EXTERNAL CALL ERRORS =====
/// Reentrancy guard is active (operation blocked to prevent reentry)
ReentrancyGuardActive = 600,
/// External call failed (e.g., token transfer or oracle invocation)
ExternalCallFailed = 601,
}

// ===== ERROR CATEGORIZATION AND RECOVERY SYSTEM =====
Expand Down Expand Up @@ -677,6 +683,8 @@ impl Error {
Error::CircuitBreakerAlreadyOpen => "Circuit breaker is already open (paused)",
Error::CircuitBreakerNotOpen => "Circuit breaker is not open (cannot recover)",
Error::CircuitBreakerOpen => "Circuit breaker is open (operations blocked)",
Error::ReentrancyGuardActive => "Reentrancy guard active (operation blocked)",
Error::ExternalCallFailed => "External call failed",
}
}

Expand Down Expand Up @@ -792,6 +800,8 @@ impl Error {
Error::CircuitBreakerAlreadyOpen => "CIRCUIT_BREAKER_ALREADY_OPEN",
Error::CircuitBreakerNotOpen => "CIRCUIT_BREAKER_NOT_OPEN",
Error::CircuitBreakerOpen => "CIRCUIT_BREAKER_OPEN",
Error::ReentrancyGuardActive => "REENTRANCY_GUARD_ACTIVE",
Error::ExternalCallFailed => "EXTERNAL_CALL_FAILED",
}
}
}
Expand Down
10 changes: 8 additions & 2 deletions contracts/predictify-hybrid/src/fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ use soroban_sdk::{contracttype, symbol_short, vec, Address, Env, Map, String, Sy
use crate::errors::Error;
use crate::markets::{MarketStateManager, MarketUtils};
use crate::types::Market;
use crate::reentrancy_guard::ReentrancyGuard;
use crate::config::{ConfigManager, ConfigValidator, FeeConfig};
use crate::events::EventEmitter;


/// Fee management system for Predictify Hybrid contract
///
/// This module provides a comprehensive fee management system with:
Expand Down Expand Up @@ -713,10 +715,12 @@ impl FeeManager {
FeeValidator::validate_creation_fee_env(env, creation_fee)?;

// Get token client
ReentrancyGuard::before_external_call(env)?;
let token_client = MarketUtils::get_token_client(env)?;

// Transfer creation fee from admin to contract
token_client.transfer(admin, &env.current_contract_address(), &creation_fee);
token_client.transfer(admin, &env.current_contract_address(), &MARKET_CREATION_FEE);
ReentrancyGuard::after_external_call(env);


// Record creation fee
FeeTracker::record_creation_fee(env, admin, creation_fee)?;
Expand Down Expand Up @@ -1337,8 +1341,10 @@ pub struct FeeUtils;
impl FeeUtils {
/// Transfer fees to admin
pub fn transfer_fees_to_admin(env: &Env, admin: &Address, amount: i128) -> Result<(), Error> {
ReentrancyGuard::before_external_call(env)?;
let token_client = MarketUtils::get_token_client(env)?;
token_client.transfer(&env.current_contract_address(), admin, &amount);
ReentrancyGuard::after_external_call(env);
Ok(())
}

Expand Down
45 changes: 38 additions & 7 deletions contracts/predictify-hybrid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod fees;
mod markets;
mod oracles;
mod resolution;
mod reentrancy_guard;
mod storage;
mod types;
mod utils;
Expand All @@ -44,8 +45,10 @@ use alloc::format;
use soroban_sdk::{
contract, contractimpl, panic_with_error, Address, Env, Map, String, Symbol, Vec,
};
use crate::reentrancy_guard::ReentrancyGuard;
use crate::config::{ConfigManager, ContractConfig, ConfigChanges, MarketLimits, ConfigUpdateRecord};


#[contract]
pub struct PredictifyHybrid;

Expand Down Expand Up @@ -282,6 +285,9 @@ impl PredictifyHybrid {
/// - Current time must be before market end time
/// - Market must not be cancelled or resolved
pub fn vote(env: Env, user: Address, market_id: Symbol, outcome: String, stake: i128) {
if let Err(e) = ReentrancyGuard::check_reentrancy_state(&env) {
panic_with_error!(env, e);
}
user.require_auth();

let mut market: Market = env
Expand Down Expand Up @@ -372,6 +378,9 @@ impl PredictifyHybrid {
/// - User must have voted for the winning outcome
/// - User must not have previously claimed winnings
pub fn claim_winnings(env: Env, user: Address, market_id: Symbol) {
if let Err(e) = ReentrancyGuard::check_reentrancy_state(&env) {
panic_with_error!(env, e);
}
user.require_auth();

let mut market: Market = env
Expand Down Expand Up @@ -555,6 +564,9 @@ impl PredictifyHybrid {
market_id: Symbol,
winning_outcome: String,
) {
if let Err(e) = ReentrancyGuard::check_reentrancy_state(&env) {
panic_with_error!(env, e);
}
admin.require_auth();

// Verify admin
Expand Down Expand Up @@ -684,14 +696,15 @@ impl PredictifyHybrid {
return Err(Error::MarketClosed);
}

// Get oracle result using the resolution module
let oracle_resolution = resolution::OracleResolutionManager::fetch_oracle_result(
&env,
&market_id,
&oracle_contract,
)?;
// Guard external oracle invocation
ReentrancyGuard::check_reentrancy_state(&env)?;
ReentrancyGuard::before_external_call(&env)?;
let result = resolution::OracleResolutionManager::fetch_oracle_result(
&env, &market_id, &oracle_contract,
);
ReentrancyGuard::after_external_call(&env);

Ok(oracle_resolution.oracle_result)
result.map(|oracle_resolution| oracle_resolution.oracle_result)
}

/// Resolves a market automatically using oracle data and community consensus.
Expand Down Expand Up @@ -764,6 +777,9 @@ impl PredictifyHybrid {
/// - Users can claim winnings
/// - Market statistics are finalized
pub fn resolve_market(env: Env, market_id: Symbol) -> Result<(), Error> {
if let Err(e) = ReentrancyGuard::check_reentrancy_state(&env) {
return Err(e);
}
// Use the resolution module to resolve the market
let _resolution = resolution::MarketResolutionManager::resolve_market(&env, &market_id)?;
Ok(())
Expand Down Expand Up @@ -956,6 +972,9 @@ impl PredictifyHybrid {
stake: i128,
reason: Option<String>,
) -> Result<(), Error> {
if let Err(e) = ReentrancyGuard::check_reentrancy_state(&env) {
return Err(e);
}
user.require_auth();
disputes::DisputeManager::process_dispute(&env, user, market_id, stake, reason)
}
Expand All @@ -970,6 +989,9 @@ impl PredictifyHybrid {
stake: i128,
reason: Option<String>,
) -> Result<(), Error> {
if let Err(e) = ReentrancyGuard::check_reentrancy_state(&env) {
return Err(e);
}
user.require_auth();
disputes::DisputeManager::vote_on_dispute(
&env, user, market_id, dispute_id, vote, stake, reason,
Expand All @@ -982,6 +1004,9 @@ impl PredictifyHybrid {
admin: Address,
market_id: Symbol,
) -> Result<disputes::DisputeResolution, Error> {
if let Err(e) = ReentrancyGuard::check_reentrancy_state(&env) {
return Err(e);
}
admin.require_auth();

// Verify admin
Expand All @@ -1002,6 +1027,9 @@ impl PredictifyHybrid {

/// Collect fees from a market (admin only)
pub fn collect_fees(env: Env, admin: Address, market_id: Symbol) -> Result<i128, Error> {
if let Err(e) = ReentrancyGuard::check_reentrancy_state(&env) {
return Err(e);
}
admin.require_auth();

// Verify admin
Expand Down Expand Up @@ -1029,6 +1057,9 @@ impl PredictifyHybrid {
reason: String,
fee_amount: i128,
) -> Result<(), Error> {
if let Err(e) = ReentrancyGuard::check_reentrancy_state(&env) {
return Err(e);
}
admin.require_auth();

// Verify admin
Expand Down
11 changes: 3 additions & 8 deletions contracts/predictify-hybrid/src/markets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1321,17 +1321,10 @@ impl MarketAnalytics {
/// - Total amount staked across all participants
/// - Total dispute stakes (if any)
/// - Distribution of votes across different outcomes
///
/// # Example
///
/// ```rust
/// use soroban_sdk::{Env, Symbol};
/// use crate::markets::{MarketAnalytics, MarketStateManager};
///
/// let env = Env::default();
/// let market_id = Symbol::new(&env, "active_market");
/// let market = MarketStateManager::get_market(&env, &market_id)?;
///
///
/// let stats = MarketAnalytics::get_market_stats(&market);
///
/// println!("Total votes: {}", stats.total_votes);
Expand Down Expand Up @@ -2358,6 +2351,8 @@ impl MarketTestHelpers {
// Transfer stake
let token_client = MarketUtils::get_token_client(env)?;
token_client.transfer(&user, &env.current_contract_address(), &stake);
// Transfer stake via centralized, guarded utility
// VotingUtils::transfer_stake(env, &user, stake)?;

// Add vote
MarketStateManager::add_vote(&mut market, user, outcome, stake, None);
Expand Down
31 changes: 25 additions & 6 deletions contracts/predictify-hybrid/src/oracles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use soroban_sdk::{contracttype, symbol_short, vec, Address, Env, IntoVal, String

use crate::errors::Error;
use crate::types::*;
use crate::reentrancy_guard::ReentrancyGuard;

/// Oracle management system for Predictify Hybrid contract
///
Expand Down Expand Up @@ -604,8 +605,14 @@ impl<'a> ReflectorOracleClient<'a> {
/// Get the latest price for an asset
pub fn lastprice(&self, asset: ReflectorAsset) -> Option<ReflectorPriceData> {
let args = vec![self.env, asset.into_val(self.env)];
self.env
.invoke_contract(&self.contract_id, &symbol_short!("lastprice"), args)
if ReentrancyGuard::before_external_call(self.env).is_err() {
return None;
}
let res = self
.env
.invoke_contract(&self.contract_id, &symbol_short!("lastprice"), args);
ReentrancyGuard::after_external_call(self.env);
res
}

/// Get price for an asset at a specific timestamp
Expand All @@ -615,8 +622,14 @@ impl<'a> ReflectorOracleClient<'a> {
asset.into_val(self.env),
timestamp.into_val(self.env),
];
self.env
.invoke_contract(&self.contract_id, &symbol_short!("price"), args)
if ReentrancyGuard::before_external_call(self.env).is_err() {
return None;
}
let res = self
.env
.invoke_contract(&self.contract_id, &symbol_short!("price"), args);
ReentrancyGuard::after_external_call(self.env);
res
}

/// Get TWAP (Time-Weighted Average Price) for an asset
Expand All @@ -626,8 +639,14 @@ impl<'a> ReflectorOracleClient<'a> {
asset.into_val(self.env),
records.into_val(self.env),
];
self.env
.invoke_contract(&self.contract_id, &symbol_short!("twap"), args)
if ReentrancyGuard::before_external_call(self.env).is_err() {
return None;
}
let res = self
.env
.invoke_contract(&self.contract_id, &symbol_short!("twap"), args);
ReentrancyGuard::after_external_call(self.env);
res
}

/// Check if the Reflector oracle is healthy
Expand Down
Loading