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
51 changes: 23 additions & 28 deletions contracts/predictify-hybrid/src/disputes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2353,7 +2353,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 +2367,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 +2401,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 +2417,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 +2442,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 +2464,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 +2492,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 +2512,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 +2523,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 +2539,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 +2571,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 +2607,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 +2618,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 +2639,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 +2650,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 +2673,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 +2685,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 +2703,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 +2714,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 +2723,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 or otherwise failed during checked 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,9 @@ 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 have already been 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 +1476,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/graceful_degradation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ impl OracleBackup {
let msg = String::from_str(env, "Primary oracle failed");
EventEmitter::emit_oracle_degradation(env, &self.primary, &msg);

// capture backup result to ensure we don't fial silently if the fallback drops
let backup_result = self.call_oracle(env, &self.backup, oracle_address, feed_id);
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
9 changes: 2 additions & 7 deletions contracts/predictify-hybrid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,6 @@ mod metadata_limits_tests;
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 oracles;
mod performance_benchmarks;
mod queries;
Expand Down Expand Up @@ -130,6 +123,8 @@ mod circuit_breaker_tests;

#[cfg(any())]
mod category_tags_tests;
#[cfg(test)]
mod tie_resolution_tests;
// #[cfg(any())]
// mod statistics_tests;

Expand Down
2 changes: 0 additions & 2 deletions contracts/predictify-hybrid/src/oracles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ use alloc::format;
use alloc::string::ToString;
use crate::bandprotocol;
use crate::errors::Error;
use alloc::format;
use alloc::string::ToString;
use soroban_sdk::{
contracttype, symbol_short, vec, Address, Bytes, Env, IntoVal, String, Symbol, Vec,
};
Expand Down
6 changes: 1 addition & 5 deletions contracts/predictify-hybrid/src/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,8 @@
use alloc::string::ToString;

use crate::{
errors::Error,
markets::{MarketAnalytics, MarketStateManager, MarketValidator},
types::{Market, MarketState, PagedMarketIds, PagedUserBets},
voting::VotingStats,
admin::{AdminManager, AdminPermission, AdminRole, MultisigConfig},
oracles::{OracleMetadata, OracleWhitelist},
bets::BetManager,
disputes::{Dispute, DisputeManager, DisputeStats, DisputeVote},
errors::Error,
governance::{GovernanceContract, GovernanceProposal},
Expand Down
3 changes: 3 additions & 0 deletions contracts/predictify-hybrid/src/recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,9 @@ mod tests {
#[test]
fn test_recovery_history_capped_per_market() {
let (env, _admin, contract_id, market_id) = setup_admin_env();
// Large history vectors exceed default test budgets at MAX=100; unlimited here
// only exercises cap logic, not production metering.
env.cost_estimate().budget().reset_unlimited();
let cap = MAX_RECOVERY_HISTORY_PER_MARKET as usize + 5;
env.as_contract(&contract_id, || {
for i in 0..cap {
Expand Down
4 changes: 3 additions & 1 deletion contracts/predictify-hybrid/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -954,7 +954,9 @@ impl StorageUtils {
#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::testutils::{Address as _, EnvTestConfig};
use soroban_sdk::testutils::{
storage::Persistent as _, Address as _, EnvTestConfig, Ledger as _,
};

#[test]
fn test_sub_balance_rejects_overdraw_without_mutation() {
Expand Down
Loading
Loading