Skip to content
Merged
2 changes: 1 addition & 1 deletion contracts/predictify-hybrid/src/balance_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ impl BalanceTestSetup {
// Register and initialize contract
let contract_id = env.register(crate::PredictifyHybrid, ());
let client = crate::PredictifyHybridClient::new(&env, &contract_id);
client.initialize(&admin, &None);
client.initialize(&\1, &None, &None);

// Set token for the contract (simulate what PredictifyTest::setup does)
env.as_contract(&contract_id, || {
Expand Down
8 changes: 7 additions & 1 deletion contracts/predictify-hybrid/src/balances.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//! - **Security**: Implements the checks-effects-interactions pattern to prevent reentrancy and double-spending.

use crate::errors::Error;
use crate::reentrancy_guard::{ReentrancyGuard, GuardError as ReentrancyError};
use crate::events::EventEmitter;
use crate::markets::MarketUtils;
use crate::storage::BalanceStorage;
Expand Down Expand Up @@ -82,7 +83,12 @@ impl BalanceManager {
// Transfer funds from user to contract
// In Soroban, if this fails it will panic, rolling back the transaction.
// This ensures the balance is NOT credited unless the transfer succeeds.
token_client.transfer(&user, &env.current_contract_address(), &amount);
// Guard the external transfer from user -> contract
ReentrancyGuard::with_external_call(env, || {
token_client.transfer(&user, &env.current_contract_address(), &amount);
Ok::<(), ReentrancyError>(())
})
.map_err(|_| Error::InvalidState)?;

// Update balance - occurs only if transfer succeeded
let balance = BalanceStorage::add_balance(env, &user, &asset, amount)?;
Expand Down
2 changes: 1 addition & 1 deletion contracts/predictify-hybrid/src/bet_cancellation_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl BetCancellationTestSetup {

let contract_id = env.register(PredictifyHybrid, ());
let client = PredictifyHybridClient::new(&env, &contract_id);
client.initialize(&admin, &None);
client.initialize(&\1, &None, &None);

let token_admin = Address::generate(&env);
let token_contract = env.register_stellar_asset_contract_v2(token_admin.clone());
Expand Down
2 changes: 1 addition & 1 deletion contracts/predictify-hybrid/src/bet_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ impl BetTestSetup {
// Register and initialize the contract
let contract_id = env.register(PredictifyHybrid, ());
let client = PredictifyHybridClient::new(&env, &contract_id);
client.initialize(&admin, &None);
client.initialize(&\1, &None, &None);

// Setup token for staking
let token_admin = Address::generate(&env);
Expand Down
17 changes: 13 additions & 4 deletions contracts/predictify-hybrid/src/bets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use soroban_sdk::{contracttype, symbol_short, Address, Env, Map, String, Symbol, Vec};

use crate::errors::Error;
use crate::reentrancy_guard::{ReentrancyGuard, GuardError as ReentrancyError};
use crate::events::EventEmitter;
use crate::markets::{MarketStateManager, MarketUtils, MarketValidator};
use crate::types::{Bet, BetLimits, BetStats, BetStatus, Market, MarketState};
Expand Down Expand Up @@ -1067,8 +1068,13 @@ impl BetUtils {
/// Returns `Ok(())` if transfer succeeds, `Err(Error)` otherwise.
pub fn lock_funds(env: &Env, user: &Address, amount: i128) -> Result<(), Error> {
let token_client = MarketUtils::get_token_client(env)?;
token_client.transfer(user, &env.current_contract_address(), &amount);
Ok(())
// Protect the external transfer with the reentrancy guard. If the
// guard cannot be acquired the call fails with `InvalidState`.
ReentrancyGuard::with_external_call(env, || {
token_client.transfer(user, &env.current_contract_address(), &amount);
Ok::<(), ReentrancyError>(())
})
.map_err(|_| Error::InvalidState)
}

/// Unlock funds by transferring from contract to user.
Expand All @@ -1091,8 +1097,11 @@ impl BetUtils {
/// before_external_call/after_external_call here to allow batch refunds.
pub fn unlock_funds(env: &Env, user: &Address, amount: i128) -> Result<(), Error> {
let token_client = MarketUtils::get_token_client(env)?;
token_client.transfer(&env.current_contract_address(), user, &amount);
Ok(())
ReentrancyGuard::with_external_call(env, || {
token_client.transfer(&env.current_contract_address(), user, &amount);
Ok::<(), ReentrancyError>(())
})
.map_err(|_| Error::InvalidState)
}

/// Get the contract's locked funds balance.
Expand Down
2 changes: 1 addition & 1 deletion contracts/predictify-hybrid/src/claim_idempotency_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ impl ClaimIdempotencyTestSetup {
// Register and initialize the contract
let contract_id = env.register(PredictifyHybrid, ());
let client = PredictifyHybridClient::new(&env, &contract_id);
client.initialize(&admin, &None);
client.initialize(&\1, &None, &None);

// Setup token for staking
let token_admin = Address::generate(&env);
Expand Down
2 changes: 1 addition & 1 deletion contracts/predictify-hybrid/src/custom_token_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ impl CustomTokenTestSetup {
// Register contract
let contract_id = env.register(PredictifyHybrid, ());
let client = PredictifyHybridClient::new(&env, &contract_id);
client.initialize(&admin, &None);
client.initialize(&\1, &None, &None);

// Setup custom token
let token_admin = Address::generate(&env);
Expand Down
23 changes: 23 additions & 0 deletions contracts/predictify-hybrid/src/disputes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2354,6 +2354,7 @@ pub struct DisputeUtils;
impl DisputeUtils {
/// Add dispute to market
/// Records `dispute.stake` in `market.dispute_stakes` for the disputing user.
pub fn add_dispute_to_market(market: &mut Market, dispute: Dispute) -> Result<(), Error> {
// Add dispute stake to market
let current_stake = market.dispute_stakes.get(dispute.user.clone()).unwrap_or(0);
market
Expand All @@ -2368,13 +2369,15 @@ impl DisputeUtils {

/// Extend market for dispute period
/// Extends `market.end_time` by [`DISPUTE_EXTENSION_HOURS`] to allow voting.
pub fn extend_market_for_dispute(market: &mut Market, _env: &Env) -> Result<(), Error> {
let extension_seconds = (DISPUTE_EXTENSION_HOURS as u64) * 3600;
market.end_time += extension_seconds;
Ok(())
}

/// Determine final outcome considering disputes
/// Picks the final outcome, deferring to community consensus when dispute impact > 30%.
pub fn determine_final_outcome_with_disputes(
env: &Env,
market: &Market,
) -> Result<String, Error> {
Expand Down Expand Up @@ -2402,6 +2405,7 @@ impl DisputeUtils {

/// Finalize market with resolution
/// Sets `market.winning_outcomes` to `[final_outcome]` after validating it is a known outcome.
pub fn finalize_market_with_resolution(
market: &mut Market,
final_outcome: String,
) -> Result<(), Error> {
Expand All @@ -2418,6 +2422,7 @@ impl DisputeUtils {

/// Extract disputes from market
/// Builds a `Vec<Dispute>` from `market.dispute_stakes` entries with stake > 0.
pub fn extract_disputes_from_market(
env: &Env,
market: &Market,
market_id: Symbol,
Expand All @@ -2443,16 +2448,19 @@ impl DisputeUtils {

/// Check if user has disputed
/// Returns `true` if `user` has a non-zero stake in `market.dispute_stakes`.
pub fn has_user_disputed(market: &Market, user: &Address) -> bool {
market.dispute_stakes.get(user.clone()).unwrap_or(0) > 0
}

/// Get user's dispute stake
/// Returns the dispute stake for `user`, or `0` if they have not disputed.
pub fn get_user_dispute_stake(market: &Market, user: &Address) -> i128 {
market.dispute_stakes.get(user.clone()).unwrap_or(0)
}

/// Calculate dispute impact on market resolution
/// Returns `total_dispute_stakes / total_staked` as a float, or `0.0` when `total_staked == 0`.
pub fn calculate_dispute_impact(market: &Market) -> f64 {
let total_staked = market.total_staked;
let total_disputes = market.total_dispute_stakes();

Expand All @@ -2465,6 +2473,7 @@ impl DisputeUtils {

/// Add vote to dispute
/// Appends `vote` to the dispute's voting record and updates aggregate stake counters.
pub fn add_vote_to_dispute(
env: &Env,
dispute_id: &Symbol,
vote: DisputeVote,
Expand Down Expand Up @@ -2493,6 +2502,7 @@ impl DisputeUtils {

/// Get dispute voting data
/// Loads the [`DisputeVoting`] record for `dispute_id`, creating a default if absent.
pub fn get_dispute_voting(env: &Env, dispute_id: &Symbol) -> Result<DisputeVoting, Error> {
let key = (symbol_short!("dispute_v"), dispute_id.clone());
Ok(env
.storage()
Expand All @@ -2513,6 +2523,7 @@ impl DisputeUtils {

/// Store dispute voting data
/// Persists `voting` under the `dispute_v` storage key for `dispute_id`.
pub fn store_dispute_voting(
env: &Env,
dispute_id: &Symbol,
voting: &DisputeVoting,
Expand All @@ -2524,6 +2535,7 @@ impl DisputeUtils {

/// Store dispute vote
/// Persists an individual `vote` keyed by `(dispute_id, user)`.
pub fn store_dispute_vote(
env: &Env,
dispute_id: &Symbol,
vote: &DisputeVote,
Expand Down Expand Up @@ -2574,6 +2586,7 @@ impl DisputeUtils {

/// Distribute fees based on outcome
/// Builds and stores a [`DisputeFeeDistribution`] record based on `outcome`.
pub fn distribute_fees_based_on_outcome(
env: &Env,
dispute_id: &Symbol,
voting_data: &DisputeVoting,
Expand Down Expand Up @@ -2610,6 +2623,7 @@ impl DisputeUtils {

/// Store dispute fee distribution
/// Persists `distribution` under the `dispute_f` storage key for `dispute_id`.
pub fn store_dispute_fee_distribution(
env: &Env,
dispute_id: &Symbol,
distribution: &DisputeFeeDistribution,
Expand All @@ -2621,6 +2635,7 @@ impl DisputeUtils {

/// Get dispute fee distribution
/// Loads the [`DisputeFeeDistribution`] for `dispute_id`, returning a zeroed default if absent.
pub fn get_dispute_fee_distribution(
env: &Env,
dispute_id: &Symbol,
) -> Result<DisputeFeeDistribution, Error> {
Expand All @@ -2642,6 +2657,7 @@ impl DisputeUtils {

/// Store dispute escalation
/// Persists `escalation` under the `dispute_e` storage key for `dispute_id`.
pub fn store_dispute_escalation(
env: &Env,
dispute_id: &Symbol,
escalation: &DisputeEscalation,
Expand All @@ -2653,6 +2669,7 @@ impl DisputeUtils {

/// Get dispute escalation
/// Returns the [`DisputeEscalation`] for `dispute_id`, or `None` if not escalated.
pub fn get_dispute_escalation(env: &Env, dispute_id: &Symbol) -> Option<DisputeEscalation> {
let key = (symbol_short!("dispute_e"), dispute_id.clone());
env.storage().persistent().get(&key)
}
Expand Down Expand Up @@ -2709,6 +2726,7 @@ impl DisputeUtils {

/// Store dispute timeout
/// Persists `timeout` under the `timeout` storage key for `dispute_id`.
pub fn store_dispute_timeout(
env: &Env,
dispute_id: &Symbol,
timeout: &DisputeTimeout,
Expand All @@ -2720,6 +2738,7 @@ impl DisputeUtils {

/// Get dispute timeout
/// Loads the [`DisputeTimeout`] for `dispute_id`.
pub fn get_dispute_timeout(env: &Env, dispute_id: &Symbol) -> Result<DisputeTimeout, Error> {
let key = (symbol_short!("timeout"), dispute_id.clone());
env.storage()
.persistent()
Expand All @@ -2729,26 +2748,30 @@ impl DisputeUtils {

/// Check if dispute timeout exists
/// Returns `true` if a timeout has been configured for `dispute_id`.
pub fn has_dispute_timeout(env: &Env, dispute_id: &Symbol) -> bool {
let key = (symbol_short!("timeout"), dispute_id.clone());
env.storage().persistent().has(&key)
}

/// Remove dispute timeout
/// Removes the timeout record for `dispute_id` from persistent storage.
pub fn remove_dispute_timeout(env: &Env, dispute_id: &Symbol) -> Result<(), Error> {
let key = (symbol_short!("timeout"), dispute_id.clone());
env.storage().persistent().remove(&key);
Ok(())
}

/// Get all active timeouts
/// Returns all active [`DisputeTimeout`] records (currently returns empty — index not yet implemented).
pub fn get_active_timeouts(env: &Env) -> Vec<DisputeTimeout> {
// This is a simplified implementation
// In a real system, you would maintain an index of active timeouts
Vec::new(env)
}

/// Check for expired timeouts
/// Returns IDs of disputes whose timeout has expired (currently returns empty — index not yet implemented).
pub fn check_expired_timeouts(env: &Env) -> Vec<Symbol> {
let _expired_disputes = Vec::new(env);
let _current_time = env.ledger().timestamp();

Expand Down
4 changes: 4 additions & 0 deletions contracts/predictify-hybrid/src/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ pub enum Error {
DisputeError = 410,
/// Unclaimed winnings have already been swept for this market. Repeat sweeps are not allowed.
SweepAlreadyDone = 411,
/// Fee arithmetic overflowed during calculation. No fee state is updated.
FeeArithmeticOverflow = 412,
/// Platform fee has already been collected from this market.
FeeAlreadyCollected = 413,
/// No fees are available to collect from this market.
Expand Down Expand Up @@ -1378,6 +1380,7 @@ impl Error {
Error::DisputeCondNotMet => "Dispute resolution conditions not met",
Error::DisputeFeeFailed => "Dispute fee distribution failed",
Error::DisputeError => "Generic dispute subsystem error",
Error::SweepAlreadyDone => "Unclaimed winnings already swept for this market",
Error::FeeArithmeticOverflow => "Fee arithmetic overflowed",
Error::FeeAlreadyCollected => "Platform fee already collected",
Error::NoFeesToCollect => "No fees available to collect",
Expand Down Expand Up @@ -1471,6 +1474,7 @@ impl Error {
Error::DisputeCondNotMet => "DISPUTE_RESOLUTION_CONDITIONS_NOT_MET",
Error::DisputeFeeFailed => "DISPUTE_FEE_DISTRIBUTION_FAILED",
Error::DisputeError => "DISPUTE_ERROR",
Error::SweepAlreadyDone => "SWEEP_ALREADY_DONE",
Error::FeeArithmeticOverflow => "FEE_ARITHMETIC_OVERFLOW",
Error::FeeAlreadyCollected => "FEE_ALREADY_COLLECTED",
Error::NoFeesToCollect => "NO_FEES_TO_COLLECT",
Expand Down
2 changes: 1 addition & 1 deletion contracts/predictify-hybrid/src/event_creation_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl TestSetup {

// Initialize the contract
let client = PredictifyHybridClient::new(&env, &contract_id);
client.initialize(&admin, &None);
client.initialize(&\1, &None, &None);

// Configure token used for creation fee collection and fund admin balance.
env.as_contract(&contract_id, || {
Expand Down
2 changes: 1 addition & 1 deletion contracts/predictify-hybrid/src/event_management_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ impl TestSetup {

// Initialize the contract
let client = PredictifyHybridClient::new(&env, &contract_id);
client.initialize(&admin, &None);
client.initialize(&\1, &None, &None);
env.as_contract(&contract_id, || {
crate::circuit_breaker::CircuitBreaker::initialize(&env)
.expect("circuit breaker should initialize in tests");
Expand Down
2 changes: 1 addition & 1 deletion contracts/predictify-hybrid/src/event_visibility_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ mod event_visibility_tests {
let contract_id = env.register(PredictifyHybrid, ());

let client = PredictifyHybridClient::new(&env, &contract_id);
client.initialize(&admin, &None);
client.initialize(&\1, &None, &None);

// Configure token used for fees and staking
env.as_contract(&contract_id, || {
Expand Down
16 changes: 12 additions & 4 deletions contracts/predictify-hybrid/src/fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::errors::Error;
use crate::markets::{MarketStateManager, MarketUtils};
use crate::reentrancy_guard::ReentrancyGuard;
use crate::types::Market;
use crate::reentrancy_guard::GuardError as ReentrancyError;

/// Fee management system for Predictify Hybrid contract
///
Expand Down Expand Up @@ -793,8 +794,12 @@ impl FeeManager {
// Get token client
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);
// Transfer creation fee from admin to contract under the reentrancy guard.
ReentrancyGuard::with_external_call(env, || {
token_client.transfer(admin, &env.current_contract_address(), &creation_fee);
Ok::<(), ReentrancyError>(())
})
.map_err(|_| Error::InvalidState)?;

// Record creation fee
FeeTracker::record_creation_fee(env, admin, creation_fee)?;
Expand Down Expand Up @@ -1349,8 +1354,11 @@ impl FeeUtils {
/// Transfer fees to admin
pub fn transfer_fees_to_admin(env: &Env, admin: &Address, amount: i128) -> Result<(), Error> {
let token_client = MarketUtils::get_token_client(env)?;
token_client.transfer(&env.current_contract_address(), admin, &amount);
Ok(())
ReentrancyGuard::with_external_call(env, || {
token_client.transfer(&env.current_contract_address(), admin, &amount);
Ok::<(), ReentrancyError>(())
})
.map_err(|_| Error::InvalidState)
}

/// Get fee statistics for a market
Expand Down
4 changes: 2 additions & 2 deletions contracts/predictify-hybrid/src/gas_tracking_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ impl GasTestContext {

let contract_id = env.register(PredictifyHybrid, ());
let client = PredictifyHybridClient::new(&env, &contract_id);
client.initialize(&admin, &None);
client.initialize(&\1, &None, &None);

// Initialize configuration
env.as_contract(&contract_id, || {
Expand Down Expand Up @@ -179,7 +179,7 @@ fn test_gas_initialize_baseline() {
let contract_id = env.register(PredictifyHybrid, ());
let client = PredictifyHybridClient::new(&env, &contract_id);

client.initialize(&admin, &None);
client.initialize(&\1, &None, &None);

// Verify: Admin stored correctly
let stored_admin = env.as_contract(&contract_id, || {
Expand Down
1 change: 1 addition & 0 deletions contracts/predictify-hybrid/src/graceful_degradation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ impl OracleBackup {
if backup_result.is_err() {
let backup_msg = String::from_str(env, "Backup oracle failed");
EventEmitter::emit_oracle_degradation(env, &self.backup, &backup_msg);
return Err(Error::FallbackOracleUnavailable);
}
backup_result
}
Expand Down
Loading
Loading