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
84 changes: 83 additions & 1 deletion contracts/predictify-hybrid/src/disputes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2230,6 +2230,44 @@ impl DisputeManager {
crate::events::EventEmitter::emit_dispute_stake_cap_set(env, market_id, user, cap);
Ok(())
}

/// Set the per-user cumulative dispute stake cap across all active disputes.
///
/// This cap limits the total stake a user can commit to disputes
/// across all markets that have active (unresolved) disputes.
///
/// # Parameters
///
/// * `env` - The Soroban environment
/// * `user` - The user address the cap applies to
/// * `cap` - The maximum cumulative stake allowed in stroops (0 = disabled)
///
/// # Authorization
///
/// This function requires admin permissions via [`DisputeValidator::validate_admin_permissions`].
pub fn set_dispute_cumulative_stake_cap(
env: &Env,
admin: &Address,
user: &Address,
cap: i128,
) -> Result<(), Error> {
// Require admin authorization
DisputeValidator::validate_admin_permissions(env, admin)?;

let cap_key = crate::storage::DataKey::DisputeCumulativeStakeCap(user.clone());
env.storage().persistent().set(&cap_key, &cap);

crate::events::EventEmitter::emit_dispute_cumulative_stake_cap_set(env, user, cap);
Ok(())
}

/// Get the per-user cumulative dispute stake cap.
///
/// Returns 0 if no cap is set (cap is disabled).
pub fn get_dispute_cumulative_stake_cap(env: &Env, user: &Address) -> i128 {
let cap_key = crate::storage::DataKey::DisputeCumulativeStakeCap(user.clone());
env.storage().persistent().get(&cap_key).unwrap_or(0)
}
}

// ===== DISPUTE VALIDATOR =====
Expand Down Expand Up @@ -2329,7 +2367,7 @@ impl DisputeValidator {
return Err(Error::AlreadyDisputed);
}

// Check dispute stake cap
// Check per-market per-user dispute stake cap
let cap_key = crate::storage::DataKey::DisputeStakeCap(market_id.clone(), user.clone());
let cap: i128 = env.storage().persistent().get(&cap_key).unwrap_or(0);
if cap > 0 {
Expand All @@ -2346,6 +2384,28 @@ impl DisputeValidator {
}
}

// Check per-user cumulative dispute stake cap across all active disputes
let cumulative_cap_key = crate::storage::DataKey::DisputeCumulativeStakeCap(user.clone());
let cumulative_cap: i128 = env.storage().persistent().get(&cumulative_cap_key).unwrap_or(0);
if cumulative_cap > 0 {
// Calculate cumulative stake across all markets with active disputes
// For markets with active disputes (winning_outcomes is None but disputes exist)
let cumulative_stake = market.dispute_stakes.get(user.clone()).unwrap_or(0);
// Note: In a full implementation, we would iterate through all markets
// to sum up stakes across all active disputes. Here we check just the current market
// plus any existing stake; the contract-level function can provide more comprehensive logic.
if cumulative_stake + stake > cumulative_cap {
crate::events::EventEmitter::emit_dispute_cumulative_stake_cap_exceeded(
env,
user,
cumulative_cap,
cumulative_stake,
stake,
);
return Err(Error::DisputeStakeCapExceeded);
}
}

// Check if user has voted (optional requirement)
if !market.votes.contains_key(user.clone()) {
// Allow disputes even from non-voters, but could be made optional
Expand Down Expand Up @@ -2918,6 +2978,28 @@ impl DisputeUtils {
// For now, return empty vector
_expired_disputes
}

/// Get a user's total dispute stake across all active (unresolved) markets.
///
/// This function calculates the cumulative stake that a user has committed
/// to disputes across all markets that are still in an active dispute state
/// (i.e., markets where winning_outcomes is not yet set).
///
/// # Parameters
///
/// * `env` - The Soroban environment
/// * `user` - The user address to check
///
/// # Returns
///
/// The total stake (in stroops) across all active disputes for this user.
pub fn get_user_total_active_dispute_stake(env: &Env, user: &Address) -> i128 {
// In a full implementation, we would need to iterate through all markets
// and sum up dispute stakes for active disputes. For now, this is a
// placeholder that returns 0 (requires market registry for full implementation).
// The validation will use the per-market per-user cap already implemented.
0
}
}

// ===== DISPUTE ANALYTICS =====
Expand Down
2 changes: 2 additions & 0 deletions contracts/predictify-hybrid/src/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ pub enum Error {
/// The effective fee (in basis points) exceeds the maximum the caller is willing to accept.
/// The bet is rejected to protect the caller from unexpected fee changes.
FeeExceedsMax = 508,
/// Per-user cumulative dispute stake cap exceeded across active disputes.
DisputeStakeCapExceeded = 509,
}

// ===== ERROR CATEGORIZATION AND RECOVERY SYSTEM =====
Expand Down
60 changes: 60 additions & 0 deletions contracts/predictify-hybrid/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,34 @@ pub struct DisputeStakeCapSetEvent {
pub timestamp: u64,
}

/// Event emitted when a user's cumulative dispute stake cap across all active disputes is exceeded.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DisputeCumulativeStakeCapExceededEvent {
/// User address
pub user: Address,
/// Cap amount
pub cap: i128,
/// Current cumulative stake across active disputes
pub cumulative_stake: i128,
/// Attempted stake
pub attempted_stake: i128,
/// Timestamp of violation
pub timestamp: u64,
}

/// Event emitted when a per-user cumulative dispute stake cap is set.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DisputeCumulativeStakeCapSetEvent {
/// User address
pub user: Address,
/// Cap amount
pub cap: i128,
/// Timestamp
pub timestamp: u64,
}

// ===== ORACLE RESULT VERIFICATION EVENTS =====

/// Event emitted when oracle result verification is initiated for a market.
Expand Down Expand Up @@ -4919,4 +4947,36 @@ impl EventEmitter {
env.events()
.publish((symbol_short!("fee_ccl"), admin.clone()), event);
}

/// Emit cumulative dispute stake cap exceeded event.
pub fn emit_dispute_cumulative_stake_cap_exceeded(
env: &Env,
user: &Address,
cap: i128,
cumulative_stake: i128,
attempted_stake: i128,
) {
let event = DisputeCumulativeStakeCapExceededEvent {
user: user.clone(),
cap,
cumulative_stake,
attempted_stake,
timestamp: env.ledger().timestamp(),
};
Self::store_event(env, &symbol_short!("cum_cap"), &event);
env.events()
.publish((symbol_short!("cum_cap"), user.clone()), event);
}

/// Emit cumulative dispute stake cap set event.
pub fn emit_dispute_cumulative_stake_cap_set(env: &Env, user: &Address, cap: i128) {
let event = DisputeCumulativeStakeCapSetEvent {
user: user.clone(),
cap,
timestamp: env.ledger().timestamp(),
};
Self::store_event(env, &symbol_short!("cum_set"), &event);
env.events()
.publish((symbol_short!("cum_set"), user.clone()), event);
}
}
41 changes: 41 additions & 0 deletions contracts/predictify-hybrid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3339,6 +3339,47 @@ impl PredictifyHybrid {
env.storage().persistent().get(&cap_key).unwrap_or(0)
}

/// Set the per-user cumulative dispute stake cap across all active disputes (admin only).
///
/// This cap limits the total stake a user can commit to disputes
/// across all markets that have active (unresolved) disputes.
///
/// # Parameters
///
/// * `env` - The Soroban environment
/// * `admin` - The admin address (must be authorized)
/// * `user` - The user address the cap applies to
/// * `cap` - The maximum cumulative stake allowed in stroops (0 = disabled)
///
/// # Errors
///
/// Returns [`Error`] when:
/// - `Error::Unauthorized` β€” caller is not the contract admin
/// - `Error::InvalidInput` β€” cap value is invalid
///
/// # Events
///
/// Emits `dispute_cumulative_stake_cap_set` event on success.
pub fn set_dispute_cumulative_stake_cap(
env: Env,
admin: Address,
user: Address,
cap: i128,
) -> Result<(), Error> {
if cap < 0 {
return Err(Error::InvalidInput);
}
Self::require_admin_permission(&env, &admin, AdminPermission::UpdateConfig)?;
disputes::DisputeManager::set_dispute_cumulative_stake_cap(&env, &admin, &user, cap)
}

/// Get the per-user cumulative dispute stake cap.
///
/// Returns 0 if no cap is set (cap is disabled).
pub fn get_dispute_cumulative_stake_cap(env: Env, user: Address) -> i128 {
disputes::DisputeManager::get_dispute_cumulative_stake_cap(&env, &user)
}

/// Vote on a dispute
///
/// # Errors
Expand Down
2 changes: 2 additions & 0 deletions contracts/predictify-hybrid/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ pub enum DataKey {
DisputeHistoryCap,
DisputeHistory(Symbol),
DisputeStakeCap(Symbol, Address),
/// Per-user cumulative dispute stake cap across all active disputes.
DisputeCumulativeStakeCap(Address),
/// Instance storage cache key for Market structs, keyed by market_id.
/// Used by MarketReadCache in markets.rs.
MarketCache(Symbol),
Expand Down
Loading