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
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
157 changes: 145 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 @@ -2464,7 +2500,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 +2510,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 +3074,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 +6021,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 +7732,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
53 changes: 2 additions & 51 deletions contracts/predictify-hybrid/src/market_id_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,57 +79,6 @@ impl MarketIdGenerator {
/// Maximum collision-retry attempts before giving up.
pub const MAX_RETRIES: u32 = 10;

// ── Seed sealing methods ───────────────────────────────────────────────────

/// Check if the seed has been sealed.
///
/// Returns `true` if the seed is sealed, preventing further regeneration.
///
/// Check if the seed has been sealed.
///
/// Returns `true` if the seed is sealed, preventing further regeneration.
///
/// # Returns
///
/// - `true` if the seed is sealed and cannot be regenerated
/// - `false` if the seed is still unsealed and can be regenerated
pub fn is_seed_sealed(env: &Env) -> bool {
env.storage()
.persistent()
.get(&Symbol::new(env, Self::SEED_SEALED_KEY))
.unwrap_or(false)
}

/// Ensure the seed is not sealed before regeneration.

///
/// This safety check prevents any seed regeneration after sealing.
/// It provides explicit validation before attempting to regenerate the seed.
///
/// # Panics
///
/// - [`Error::InvalidState`] if attempting to regenerate an already sealed seed
fn ensure_seed_not_sealed(env: &Env) {
if Self::is_seed_sealed(env) {
panic_with_error!(env, Error::InvalidState);
}
}

/// Bump TTL for seed-related storage to ensure long-term persistence.
///
/// This ensures the seed sealing flag persists for the contract's entire lifetime.
///
/// # Safety Note
///
/// Uses the maximum allowed TTL to ensure the seed flag remains valid even as
/// the contract matures and storage entries age.
fn bump_seed_storage_ttl(env: &Env) {
let key = Symbol::new(env, Self::SEED_SEALED_KEY);
env.storage()
.persistent()
.extend_ttl(&key, env.storage().max_ttl(), env.storage().max_ttl());
}

// ── Public API ───────────────────────────────────────────────────────────

/// Generate a unique, collision-resistant market ID for `admin`.
Expand Down Expand Up @@ -346,6 +295,8 @@ pub fn parse_market_id_components(
return Ok(MarketIdComponents { counter, is_legacy: false });
}
}
}


// ── Tests ─────────────────────────────────────────────────────────────────────

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ impl MetadataTest {
let contract_id = env.register(crate::PredictifyHybrid, ());
let client = PredictifyHybridClient::new(&env, &contract_id);

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

Self {
env,
Expand Down
Loading
Loading