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
52 changes: 24 additions & 28 deletions contracts/predictify-hybrid/src/disputes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ pub struct DisputeStats {
/// - Timestamp for regulatory compliance
/// - Outcome justification for participants
#[contracttype]
#[derive(Debug, PartialEq)]
pub struct DisputeResolution {
pub market_id: Symbol,
pub final_outcome: String,
Expand Down Expand Up @@ -2353,7 +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 @@ -2367,14 +2368,14 @@ 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 All @@ -2401,7 +2402,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 @@ -2417,7 +2418,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 @@ -2442,17 +2443,17 @@ 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 @@ -2464,7 +2465,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 @@ -2492,7 +2493,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 @@ -2512,7 +2513,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 @@ -2523,7 +2524,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 All @@ -2539,13 +2540,11 @@ impl DisputeUtils {
env.storage().persistent().get(&key)
}

/// Returns `true` if `user` has already claimed winnings for `dispute_id`.
pub fn has_user_claimed_dispute(env: &Env, dispute_id: &Symbol, user: &Address) -> bool {
let key = (symbol_short!("d_clm"), dispute_id.clone(), user.clone());
env.storage().persistent().get(&key).unwrap_or(false)
}

/// Marks `user` as having claimed winnings for `dispute_id` to prevent double-claims.
pub fn set_user_claimed_dispute(env: &Env, dispute_id: &Symbol, user: &Address) {
let key = (symbol_short!("d_clm"), dispute_id.clone(), user.clone());
env.storage().persistent().set(&key, &true);
Expand Down Expand Up @@ -2573,7 +2572,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 @@ -2609,7 +2608,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 @@ -2620,7 +2619,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 @@ -2641,7 +2640,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 @@ -2652,14 +2651,13 @@ 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)
}

/// Emit dispute vote event

/// Records a vote event for `dispute_id` in persistent storage.
pub fn emit_dispute_vote_event(
env: &Env,
_dispute_id: &Symbol,
Expand All @@ -2676,7 +2674,6 @@ impl DisputeUtils {

/// Emit fee distribution event

/// Records a fee distribution event for `dispute_id` in persistent storage.
pub fn emit_fee_distribution_event(
env: &Env,
_dispute_id: &Symbol,
Expand All @@ -2689,7 +2686,6 @@ impl DisputeUtils {
}

/// Emit dispute escalation event
/// Records an escalation event for `dispute_id` in persistent storage.
pub fn emit_dispute_escalation_event(
env: &Env,
_dispute_id: &Symbol,
Expand All @@ -2708,7 +2704,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 @@ -2719,7 +2715,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 @@ -2728,27 +2724,27 @@ 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
6 changes: 6 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 checked platform-fee calculation.
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 Expand Up @@ -1584,6 +1588,8 @@ mod tests {
Error::DisputeCondNotMet,
Error::DisputeFeeFailed,
Error::DisputeError,
Error::SweepAlreadyDone,
Error::FeeArithmeticOverflow,
Error::FeeAlreadyCollected,
Error::NoFeesToCollect,
Error::InvalidExtensionDays,
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
36 changes: 20 additions & 16 deletions contracts/predictify-hybrid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,7 @@ mod monitoring;
#[cfg(test)]
mod multi_admin_multisig_tests;
#[cfg(test)]
mod admin_auth_audit_tests;
#[cfg(any())]
mod metadata_limits_tests;
mod monitoring;
#[cfg(any())]
mod multi_admin_multisig_tests;
mod require_auth_coverage_tests;
mod oracles;
mod performance_benchmarks;
mod queries;
Expand All @@ -63,7 +58,7 @@ mod reentrancy_guard;
mod resolution;
mod statistics;
mod storage;
#[cfg(any())]
#[cfg(test)]
mod storage_layout_tests;
pub mod tokens;
mod types;
Expand Down Expand Up @@ -311,21 +306,30 @@ impl PredictifyHybrid {
Err(e) => panic_with_error!(env, e),
}

// Initialize circuit breaker
match crate::circuit_breaker::CircuitBreaker::initialize(&env) {
Ok(_) => (),
Err(e) => panic_with_error!(env, e),
}
// Store platform fee configuration in persistent storage
env.storage()
.persistent()
.set(&Symbol::new(&env, "platform_fee"), &fee_percentage);

// Initialize circuit breaker
if let Err(e) = crate::circuit_breaker::CircuitBreaker::initialize(&env) {
// Store default contract configuration so validators have deterministic bounds
let mut default_config = crate::config::ConfigManager::get_development_config(&env);
default_config.fees.platform_fee_percentage = fee_percentage;
if let Err(e) = crate::config::ConfigManager::store_config(&env, &default_config) {
panic_with_error!(env, e);
}

// Store platform fee configuration in persistent storage
// Initialize rate limiter with permissive defaults (0 = no limit)
let rate_limit_config = crate::rate_limiter::RateLimitConfig {
voting_limit: 0,
dispute_limit: 0,
oracle_call_limit: 0,
bet_limit: 0,
events_per_admin_limit: 0,
time_window_seconds: 3600,
};
env.storage()
.persistent()
.set(&Symbol::new(&env, "platform_fee"), &fee_percentage);
.set(&crate::rate_limiter::RateLimiterData::Config, &rate_limit_config);

// Seed default runtime configuration so validators and query paths have
// deterministic bounds immediately after deployment.
Expand Down
38 changes: 20 additions & 18 deletions contracts/predictify-hybrid/src/market_id_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,12 @@ impl MarketIdGenerator {
/// unstructured.
#[cfg(not(target_family = "wasm"))]
pub fn validate_market_id_format(_env: &Env, market_id: &Symbol) -> bool {
market_id.to_string().starts_with("mkt_")
// Symbol::to_string() requires std/Display unavailable in WASM no_std.
// Use cfg guard: full logic in std, safe fallback in WASM.
#[cfg(not(target_family = "wasm"))]
{ use alloc::string::ToString; return market_id.to_string().starts_with("mkt_"); }
#[allow(unreachable_code)]
{ let _ = market_id; true }
}

#[cfg(target_family = "wasm")]
Expand All @@ -158,24 +163,21 @@ impl MarketIdGenerator {
_env: &Env,
market_id: &Symbol,
) -> Result<MarketIdComponents, Error> {
let s = market_id.to_string();
// Expected format: mkt_{hex}_{counter}
if !s.starts_with("mkt_") {
return Ok(MarketIdComponents {
counter: 0,
is_legacy: true,
});
}
// Split on '_': ["mkt", "{hex}", "{counter}"]
let parts: alloc::vec::Vec<&str> = s.splitn(3, '_').collect();
if parts.len() != 3 {
return Err(Error::InvalidInput);
// Symbol::to_string() requires std/Display unavailable in WASM no_std.
#[cfg(not(target_family = "wasm"))]
{
use alloc::string::ToString;
let s = market_id.to_string();
if !s.starts_with("mkt_") {
return Ok(MarketIdComponents { counter: 0, is_legacy: true });
}
let parts: alloc::vec::Vec<&str> = s.splitn(3, '_').collect();
if parts.len() != 3 { return Err(Error::InvalidInput); }
let counter = parts[2].parse::<u32>().map_err(|_| Error::InvalidInput)?;
return Ok(MarketIdComponents { counter, is_legacy: false });
}
let counter = parts[2].parse::<u32>().map_err(|_| Error::InvalidInput)?;
Ok(MarketIdComponents {
counter,
is_legacy: false,
})
#[allow(unreachable_code)]
{ let _ = market_id; Ok(MarketIdComponents { counter: 0, is_legacy: true }) }
}

#[cfg(target_family = "wasm")]
Expand Down
Loading
Loading