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: 48 additions & 4 deletions contracts/predictify-hybrid/src/disputes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -905,9 +905,12 @@ impl DisputeManager {
let mut market = MarketStateManager::get_market(env, &market_id)?;
DisputeValidator::validate_market_for_dispute(env, &market)?;

// Enforce anti-grief floor
let anti_grief_floor = Self::get_anti_grief_floor(env).unwrap_or(0);
if stake < anti_grief_floor {
// Enforce anti-grief floor (market specific or global)
let global_anti_grief_floor = Self::get_anti_grief_floor(env).unwrap_or(0);
let market_floor = market.dispute_stake_floor.unwrap_or(0);
let effective_floor = global_anti_grief_floor.max(market_floor);

if stake < effective_floor {
return Err(Error::InvalidStakeAmount);
}

Expand Down Expand Up @@ -3585,5 +3588,46 @@ mod tests {
DisputeManager::apply_eviction(&env, &market_id, &mut history2).unwrap();
assert_eq!(history2.len(), 2); // No eviction because cap is 0
}
#[test]
fn test_market_specific_anti_grief_floor() {
let env = Env::default();
let admin = Address::generate(&env);
let user = Address::generate(&env);
let contract_id = env.register(crate::PredictifyHybrid, ());

let market_id = Symbol::new(&env, "market_floor_test");
let mut market = create_test_market(&env, env.ledger().timestamp().saturating_sub(1));
market.oracle_result = Some(String::from_str(&env, "yes"));
market.state = crate::types::MarketState::Resolved;
market.dispute_stake_floor = Some(1500);

env.as_contract(&contract_id, || {
crate::markets::MarketStateManager::update_market(&env, &market_id, &market);
DisputeManager::set_anti_grief_floor(&env, admin.clone(), 1000).unwrap();

// Stake 1200: higher than global (1000) but lower than market (1500). Should fail.
assert_eq!(
DisputeManager::process_dispute(&env, user.clone(), market_id.clone(), 1200, None).unwrap_err(),
Error::InvalidStakeAmount
);

// Stake 2000: higher than both.
// process_dispute doesn't check if user has balance in tests because the contract doesn't have the mock balance set up
// Wait, process_dispute internally locks funds, which will fail if there's no balance.
// I should use DisputeValidator directly to just check the dispute stake check.
assert_eq!(
DisputeValidator::validate_dispute_parameters(&env, &market_id, &user, &market, 1200).unwrap_err(),
Error::InvalidStakeAmount
);
assert!(DisputeValidator::validate_dispute_parameters(&env, &market_id, &user, &market, 2000).is_ok());

// Test when market_floor < global_anti_grief_floor
market.dispute_stake_floor = Some(500);
assert_eq!(
DisputeValidator::validate_dispute_parameters(&env, &market_id, &user, &market, 800).unwrap_err(),
Error::InvalidStakeAmount
);
assert!(DisputeValidator::validate_dispute_parameters(&env, &market_id, &user, &market, 1200).is_ok());
});
}
}

5 changes: 3 additions & 2 deletions contracts/predictify-hybrid/src/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use soroban_sdk::{contracterror, contracttype, Address, Env, Map, String, Symbol
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
IdempotentBatchAlreadyApplied = 660,
// ===== USER OPERATION ERRORS (100-112) =====
/// User is not authorized to perform the requested action. Typically returned when
/// a non-admin attempts to call admin-only functions.
Expand Down Expand Up @@ -145,6 +146,8 @@ pub enum Error {
/// Asset decimals mismatch. Stored decimals differ from the live SAC decimals.
/// This prevents silently inflated or deflated stakes via normalize_amount.
AssetDecimalsMismatch = 439,
/// A per-market admin action was attempted before the configured timelock period elapsed.
AdminActionTimelocked = 443,

// ===== METADATA LENGTH LIMIT ERRORS (420-434) =====
/// Market question exceeds maximum allowed length.
Expand Down Expand Up @@ -193,8 +196,6 @@ pub enum Error {
// ===== VALIDATION ERRORS (435-437) =====
/// Market ID already exists in the registry. Cannot create duplicate market IDs.
DuplicateMarketId = 441,
/// Override replay detected. Nonce has already been used.
ReplayedOverride = 442,

// ===== CIRCUIT BREAKER ERRORS =====
/// Circuit breaker has not been initialized. Initialize before use.
Expand Down
3 changes: 3 additions & 0 deletions contracts/predictify-hybrid/src/event_archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,7 @@ mod tests {
bet_deadline: 0,
dispute_window_seconds: 3600,
winnings_swept: false,
timelock_config: crate::timelock::MarketTimelockConfig::default(),
};

let res =
Expand Down Expand Up @@ -1161,6 +1162,7 @@ mod tests {
bet_deadline: 0,
dispute_window_seconds: 3600,
winnings_swept: false,
timelock_config: crate::timelock::MarketTimelockConfig::default(),
};

let res1 =
Expand Down Expand Up @@ -1428,6 +1430,7 @@ mod tests {
bet_deadline: 0,
dispute_window_seconds: 3600,
winnings_swept: false,
timelock_config: crate::timelock::MarketTimelockConfig::default(),
};

let res =
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(&\1, &None, &None);
client.initialize(&admin, &None, &None);

// Configure token used for fees and staking
env.as_contract(&contract_id, || {
Expand Down
2 changes: 1 addition & 1 deletion contracts/predictify-hybrid/src/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ impl GasTracker {
};

env.events().publish(
(symbol_short!("performance_metric"), operation.clone()),
(symbol_short!("perf_metr"), operation.clone()),
event,
);
}
Expand Down
160 changes: 148 additions & 12 deletions contracts/predictify-hybrid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ extern crate alloc;
// ===== MODULE DECLARATIONS =====
// These must be declared here so Rust knows to compile them as part of this crate.


const SYM_ADMIN: &str = "Admin";
const SYM_PLATFORM_FEE: &str = "platform_fee";
pub use config::PERCENTAGE_DENOMINATOR;
mod admin;
// #[cfg(any())]
// mod admin_auth_audit_tests;
Expand Down Expand Up @@ -61,15 +65,20 @@ mod bandprotocol {
soroban_sdk::contractimport!(file = "./std_reference.wasm");
}

mod performance_benchmarks;
mod market_analytics;
pub mod timelock;

// #[cfg(any())]
// mod circuit_breaker_tests;
// #[cfg(test)]
// mod oracle_fallback_timeout_tests;

use bets::{BetStatus, BetStorage};
use types::BetStatus;
use bets::BetStorage;
use circuit_breaker::CircuitBreaker;
use err::Error;
use events::{ClaimInfo, EventEmitter};

use events::EventEmitter;
use gas::BudgetGuard;
use resolution::ResolutionOutcomeCache;
use storage::BalanceStorage;
Expand All @@ -89,9 +98,11 @@ use soroban_sdk::{contract, contractimpl, panic_with_error, symbol_short, Env, S
// #[cfg(any())]
// mod upgrade_manager_tests;
#[cfg(test)]
mod capability_bitmap_tests;

#[cfg(test)]
mod market_state_matrix_tests;
#[cfg(test)]
mod timelock_tests;

// #[cfg(any())]
// mod query_tests;
Expand Down Expand Up @@ -162,18 +173,18 @@ pub mod errors {
pub use audit_trail::{AuditAction, AuditRecord, AuditTrailHead, AuditTrailManager};
pub use types::*;

use crate::circuit_breaker::CircuitBreaker;

use crate::config::{
ConfigManager, DEFAULT_PLATFORM_FEE_PERCENTAGE, MAX_PLATFORM_FEE_PERCENTAGE,
MIN_PLATFORM_FEE_PERCENTAGE,
};
use crate::events::{emit_deprecated, EventEmitter};
use crate::events::emit_deprecated;
use crate::gas::GasTracker;
use crate::graceful_degradation::{OracleBackup, OracleHealth};
use crate::market_id_generator::MarketIdGenerator;
use alloc::format;
use soroban_sdk::{
contract, contractimpl, panic_with_error, symbol_short, Address, BytesN, Env, Map, String, Symbol, Vec,
Address, BytesN, Map, String, Vec,
};

impl From<crate::reentrancy_guard::GuardError> for Error {
Expand All @@ -200,6 +211,31 @@ pub struct PredictifyHybrid;

#[contractimpl]
impl PredictifyHybrid {
pub fn initialize(env: Env, admin: Address, platform_fee_percentage: Option<i128>, _allowed_assets: Option<soroban_sdk::Vec<Address>>) -> Result<(), Error> {
if env.storage().persistent().has(&Symbol::new(&env, SYM_PLATFORM_FEE)) {
return Err(Error::InvalidState);
}
let fee_percentage = platform_fee_percentage.unwrap_or(crate::config::DEFAULT_PLATFORM_FEE_PERCENTAGE);
if fee_percentage < crate::config::MIN_PLATFORM_FEE_PERCENTAGE || fee_percentage > crate::config::MAX_PLATFORM_FEE_PERCENTAGE {
panic_with_error!(env, Error::InvalidFeeConfig);
}
env.storage().persistent().set(&Symbol::new(&env, SYM_ADMIN), &admin);
env.storage().persistent().set(&Symbol::new(&env, SYM_PLATFORM_FEE), &fee_percentage);
Ok(())
}

pub fn deposit(env: Env, user: Address, asset: ReflectorAsset, amount: i128) -> Result<types::Balance, Error> {
crate::balances::BalanceManager::deposit(&env, user, asset, amount)
}

pub fn withdraw(env: Env, user: Address, asset: ReflectorAsset, amount: i128) -> Result<types::Balance, Error> {
crate::balances::BalanceManager::withdraw(&env, user, asset, amount)
}

pub fn get_balance(env: Env, user: Address, asset: ReflectorAsset) -> types::Balance {
crate::balances::BalanceManager::get_balance(&env, user, asset)
}

/// Distribute payouts to winning voters and bettors for a resolved market.
///
/// This function iterates over all voters and bettors, calculates each winner's
Expand Down Expand Up @@ -318,6 +354,7 @@ impl PredictifyHybrid {
min_pool_size: Option<i128>,
bet_deadline_mins_before_end: Option<u64>,
dispute_window_seconds: Option<u64>,
dispute_stake_floor: Option<i128>,
) -> Symbol {
if let Err(e) =
crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "create_market")
Expand Down Expand Up @@ -407,6 +444,8 @@ impl PredictifyHybrid {
bet_deadline,
dispute_window_seconds: dispute_window_seconds.unwrap_or(86400),
winnings_swept: false,
timelock_config: timelock::MarketTimelockConfig::default(),
dispute_stake_floor,
};

// Pre-flight check: ensure sufficient storage rent budget
Expand Down Expand Up @@ -2464,7 +2503,7 @@ impl PredictifyHybrid {
EventEmitter::emit_manual_resolution_required(
&env,
&market_id,
&String::from_str(&env, ORACLE_FAILURE_PRIMARY_THEN_FALLBACK_REASON),
&String::from_str(&env, "primary_and_fallback_failed"),
);
Err(Error::FallbackOracleUnavailable)
}
Expand All @@ -2474,7 +2513,7 @@ impl PredictifyHybrid {
EventEmitter::emit_manual_resolution_required(
&env,
&market_id,
&String::from_str(&env, ORACLE_FAILURE_PRIMARY_ONLY_REASON),
&String::from_str(&env, "primary_failed_no_fallback"),
);
Err(err)
}
Expand Down Expand Up @@ -3038,7 +3077,7 @@ impl PredictifyHybrid {
/// # Events
///
/// State-changing paths may emit events through internal managers; read-only query paths emit no events.
pub fn get_resolution_analytics(env: Env) -> Result<resolution::ResolutionAnalytics, Error> {
pub fn get_resolution_analytics(env: Env) -> Result<resolution::MarketResolutionAnalytics, Error> {
resolution::MarketResolutionAnalytics::calculate_resolution_analytics(&env)
}

Expand Down Expand Up @@ -5985,7 +6024,7 @@ impl PredictifyHybrid {
}
Err(_) => {
// Both oracles failed
let reason = String::from_str(&env, ORACLE_FAILURE_PRIMARY_THEN_FALLBACK_REASON);
let reason = String::from_str(&env, "primary_and_fallback_failed");
events::EventEmitter::emit_manual_resolution_required(&env, &market_id, &reason);
Err(Error::FallbackOracleUnavailable)
}
Expand Down Expand Up @@ -7696,5 +7735,102 @@ mod tests {
let guard = BudgetGuard::new(&env, 100_000);
assert!(guard.consumed() == 0); // No instructions consumed yet in test host
});

pub fn get_fee_withdrawal_schedule(env: Env) -> crate::fees::FeeWithdrawalSchedule {
crate::fees::FeeWithdrawalManager::get_schedule(&env)
}

pub fn set_fee_withdrawal_schedule(
env: Env,
admin: Address,
timelock_seconds: u64,
max_withdrawal_bps: u32,
) -> Result<(), Error> {
crate::admin::AdminManager::assert_is_admin(&env, &admin)?;
let schedule = crate::fees::FeeWithdrawalSchedule {
timelock_seconds,
max_withdrawal_bps,
};
crate::fees::FeeWithdrawalManager::set_schedule(&env, &schedule)
}

pub fn pause(env: Env, admin: Address) -> Result<(), Error> {
crate::admin::ContractPauseManager::pause(&env, &admin)
}
}mod dispute_multisig;

pub fn unpause(env: Env, admin: Address) -> Result<(), Error> {
crate::admin::ContractPauseManager::unpause(&env, &admin)
}


}}mod dispute_multisig;
mod disputes;
mod edge_cases;
mod extensions;
mod graceful_degradation;
mod queries;
mod recovery;
mod statistics;

mod audit;

#[cfg(test)]
mod audit_tests;

#[cfg(test)]
mod batch_operations_tests;

#[cfg(test)]
mod custom_token_tests;

mod event_topic_catalog;

#[cfg(test)]
mod event_visibility_test;

#[cfg(test)]
mod extensions_cumulative_cap_tests;

mod leaderboard;

mod lists;

#[cfg(test)]
mod market_creation_validation_tests;

mod market_id_generator;

#[cfg(test)]
mod metadata_commitment_tests;

mod metadata_limits;

#[cfg(test)]
mod metadata_limits_tests;

#[cfg(test)]
mod metadata_validation_tests;

#[cfg(test)]
mod multi_admin_multisig_tests;

#[cfg(test)]
mod place_bets_idempotency_tests;

mod rate_limiter;

#[cfg(test)]
mod storage_layout_tests;

mod storage_tier_audit;

#[cfg(test)]
mod test;

mod tokens;

#[cfg(test)]
mod unclaimed_winnings_timeout_tests;

#[cfg(test)]
mod voting_tests;
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ impl TestSetup {
});

let client = PredictifyHybridClient::new(&env, &contract_id);
client.initialize(&\1, &None, &None);
client.initialize(&admin, &None, &None);
env.as_contract(&contract_id, || {
crate::circuit_breaker::CircuitBreaker::initialize(&env)
.expect("circuit breaker should initialize in tests");
Expand Down
Loading
Loading