Skip to content
Open
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
10 changes: 7 additions & 3 deletions contracts/predictify-hybrid/src/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,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 Expand Up @@ -227,6 +225,12 @@ 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,
/// A place_bets batch with this idempotency key has already been successfully applied.
IdempotentBatchAlreadyApplied = 509,
/// Force-resolve idempotency key has already been used. Use a new unique key.
ForceResolveReplayed = 517,
/// Force-resolve reason is empty. Every force-resolve must be justified.
ForceResolveReasonEmpty = 518,
/// No pending fee config commit was found for reveal or apply.
NoPendingFeeCommit = 519,
/// Fee config reveal was attempted too early (before timelock expiry).
Expand Down Expand Up @@ -1478,7 +1482,7 @@ impl Error {
"Bets have already been placed on this market (cannot update)"
}
Error::InsufficientBalance => "Insufficient balance for operation",
Error::InsufficientStorageRent => "Insufficient storage rent for persistent key allocation",
Error::InsufficientStorageRentBudget => "Insufficient storage rent for persistent key allocation",
Error::OracleUnavailable => "Oracle is unavailable",
Error::InvalidOracleConfig => "Invalid oracle configuration",
Error::GasBudgetExceeded => "Gas budget exceeded",
Expand Down
2 changes: 1 addition & 1 deletion contracts/predictify-hybrid/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5142,7 +5142,7 @@ mod focused_dispute_tests {
// topic2 = 1 (schema version)

let mut found = false;
for event in events.iter() {
for event in events.events().iter() {
if event.2.len() == 3 {
let topic0: Symbol = event.2.get(0).unwrap().try_into_val(&env).unwrap();
let topic1: Symbol = event.2.get(1).unwrap().try_into_val(&env).unwrap();
Expand Down
4 changes: 2 additions & 2 deletions contracts/predictify-hybrid/src/fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1044,7 +1044,7 @@ impl FeeCalculator {
}

fn checked_bps_floor(amount: i128, bps: i128) -> Result<i128, Error> {
Self::checked_mul_div_floor(amount, bps, crate::PERCENTAGE_DENOMINATOR)
Self::checked_mul_div_floor(amount, bps, crate::config::PERCENTAGE_DENOMINATOR)
}

/// Calculate platform fee for a market
Expand Down Expand Up @@ -1155,7 +1155,7 @@ impl FeeCalculator {

let fee_percentage = PLATFORM_FEE_PERCENTAGE;
let user_share =
Self::checked_bps_floor(user_stake, crate::PERCENTAGE_DENOMINATOR - fee_percentage)?;
Self::checked_bps_floor(user_stake, crate::config::PERCENTAGE_DENOMINATOR - fee_percentage)?;
let payout = Self::checked_mul_div_floor(user_share, total_pool, winning_total)?;

Ok(payout)
Expand Down
20 changes: 15 additions & 5 deletions contracts/predictify-hybrid/src/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub enum GasConfigKey {

/// Represents consumed resources for an operation.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq, Default)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GasUsage {
pub cpu: u64,
pub mem: u64,
Expand Down Expand Up @@ -60,6 +60,15 @@ pub struct GasUsage {
pub struct GasTracker;

impl GasUsage {
pub fn new(env: &Env) -> Self {
Self {
cpu: 0,
mem: 0,
cpu_history: Vec::new(env),
history_index: 0,
history_count: 0,
}
}
/// Adds a new CPU usage value to the rolling window buffer.
/// Uses ring buffer semantics for O(1) insertion.
/// Returns the moving average of the buffer contents.
Expand Down Expand Up @@ -210,17 +219,18 @@ impl GasTracker {

// Check if we've crossed the threshold
if used > threshold {
use alloc::string::ToString;
// Emit performance metric event
let event = PerformanceMetricEvent {
metric_name: Symbol::new(env, "gas_low_water").into(),
metric_name: soroban_sdk::String::from_str(env, "gas_low_water"),
value: used as i128,
unit: Symbol::new(env, "cpu").into(),
context: operation.into(),
unit: soroban_sdk::String::from_str(env, "cpu"),
context: soroban_sdk::String::from_str(env, &operation.to_string()),
timestamp: env.ledger().timestamp(),
};

env.events().publish(
(symbol_short!("performance_metric"), operation.clone()),
(Symbol::new(env, "performance_metric"), operation.clone()),
event,
);
}
Expand Down
14 changes: 7 additions & 7 deletions contracts/predictify-hybrid/src/gas_tracking_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ fn test_gas_operations_within_expected_ranges() {
fn test_gas_usage_ring_buffer_initialization() {
// Test: Ring buffer initializes correctly with empty state
let env = Env::default();
let mut usage = GasUsage::default();
let mut usage = GasUsage::new(&env);

assert_eq!(usage.history_count, 0);
assert_eq!(usage.history_index, 0);
Expand All @@ -510,7 +510,7 @@ fn test_gas_usage_ring_buffer_initialization() {
fn test_gas_usage_add_to_history() {
// Test: Adding values to ring buffer works correctly
let env = Env::default();
let mut usage = GasUsage::default();
let mut usage = GasUsage::new(&env);

// Add first value
let avg1 = usage.add_to_history(&env, 100);
Expand All @@ -534,7 +534,7 @@ fn test_gas_usage_add_to_history() {
fn test_gas_usage_ring_buffer_wrap_around() {
// Test: Ring buffer wraps around correctly when full
let env = Env::default();
let mut usage = GasUsage::default();
let mut usage = GasUsage::new(&env);

// Fill buffer to capacity (GAS_TRACKING_WINDOW_SIZE = 10)
for i in 1..=10 {
Expand All @@ -557,7 +557,7 @@ fn test_gas_usage_ring_buffer_wrap_around() {
fn test_gas_usage_moving_average_empty_buffer() {
// Test: Moving average returns 0 for empty buffer
let env = Env::default();
let usage = GasUsage::default();
let usage = GasUsage::new(&env);

// Empty buffer should return 0
let avg = usage.calculate_moving_average(&env);
Expand Down Expand Up @@ -727,7 +727,7 @@ fn test_gas_usage_ring_buffer_o1_insertion() {
// Test: Verify ring buffer insertion is O(1) by checking it doesn't
// depend on buffer size for insertion time
let env = Env::default();
let mut usage = GasUsage::default();
let mut usage = GasUsage::new(&env);

// Fill buffer
for i in 0..10 {
Expand All @@ -744,8 +744,8 @@ fn test_gas_usage_ring_buffer_o1_insertion() {

#[test]
fn test_gas_usage_default_fields() {
// Test: Default GasUsage has all new fields initialized
let usage = GasUsage::default();
let env = Env::default();
let usage = GasUsage::new(&env);

assert_eq!(usage.cpu, 0);
assert_eq!(usage.mem, 0);
Expand Down
166 changes: 153 additions & 13 deletions contracts/predictify-hybrid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ mod validation;
// mod validation_tests; // disabled - API drift
mod versioning;
mod voting;
mod market_analytics;
mod performance_benchmarks;
mod disputes;
mod edge_cases;
mod extensions;
mod graceful_degradation;
mod market_id_generator;
mod metadata_limits;
mod queries;
mod recovery;
mod statistics;
mod tokens;
mod rate_limiter;
mod dispute_multisig;
mod event_topic_catalog;
mod storage_tier_audit;
mod leaderboard;
mod lists;
// #[cfg(any())]
// mod voting_invariants;

Expand All @@ -66,15 +84,17 @@ mod bandprotocol {
// #[cfg(test)]
// mod oracle_fallback_timeout_tests;

use bets::{BetStatus, BetStorage};
use bets::BetStorage;
use circuit_breaker::CircuitBreaker;
use err::Error;
use events::{ClaimInfo, EventEmitter};
use crate::config::PERCENTAGE_DENOMINATOR;
use events::{EventEmitter, emit_deprecated};
use gas::BudgetGuard;
use resolution::ResolutionOutcomeCache;
use storage::BalanceStorage;
use types::{Market, ReflectorAsset};
use soroban_sdk::{contract, contractimpl, panic_with_error, symbol_short, Env, Symbol};
use soroban_sdk::{
contract, contractimpl, panic_with_error, symbol_short, Address, BytesN, Env, Map, String, Symbol, Vec,
};

// #[cfg(any())]
// mod integration_test;
Expand All @@ -89,7 +109,7 @@ use soroban_sdk::{contract, contractimpl, panic_with_error, symbol_short, Env, S
// #[cfg(any())]
// mod upgrade_manager_tests;
#[cfg(test)]
mod capability_bitmap_tests;
// mod capability_bitmap_tests;
#[cfg(test)]
mod market_state_matrix_tests;

Expand Down Expand Up @@ -162,19 +182,25 @@ pub mod errors {
pub use audit_trail::{AuditAction, AuditRecord, AuditTrailHead, AuditTrailManager};
pub use types::*;

use crate::circuit_breaker::CircuitBreaker;
const SYM_PLATFORM_FEE: &str = "plat_fee";
const SYM_ALLOWED_ASSETS: &str = "allowed";
const SYM_ADMIN: &str = "Admin";

// Legacy symbol keys for backwards compatibility
const SYM_PLATFORM_FEE_LEGACY: &str = "platform_fee";
const SYM_ALLOWED_ASSETS_LEGACY: &str = "allowed_assets";

const ORACLE_FAILURE_PRIMARY_ONLY_REASON: &str = "oracle_resolution_failed_primary_only";
const ORACLE_FAILURE_PRIMARY_THEN_FALLBACK_REASON: &str = "oracle_resolution_failed_primary_then_fallback";

use crate::config::{
ConfigManager, DEFAULT_PLATFORM_FEE_PERCENTAGE, MAX_PLATFORM_FEE_PERCENTAGE,
MIN_PLATFORM_FEE_PERCENTAGE,
};
use crate::events::{emit_deprecated, EventEmitter};
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,
};

impl From<crate::reentrancy_guard::GuardError> for Error {
fn from(_err: crate::reentrancy_guard::GuardError) -> Self {
Expand All @@ -200,6 +226,120 @@ pub struct PredictifyHybrid;

#[contractimpl]
impl PredictifyHybrid {
pub fn initialize(
env: Env,
admin: Address,
platform_fee_percentage: Option<i128>,
allowed_assets: Option<Vec<Address>>,
) -> Result<(), Error> {
// Check for re-initialization attempt (critical security check)
if env
.storage()
.persistent()
.has(&Symbol::new(&env, SYM_PLATFORM_FEE))
{
return Err(Error::InvalidState);
}

// Determine platform fee (default 2% if not specified)
let fee_percentage = platform_fee_percentage.unwrap_or(DEFAULT_PLATFORM_FEE_PERCENTAGE);

// Validate fee percentage bounds (0-10%)
if fee_percentage < MIN_PLATFORM_FEE_PERCENTAGE
|| fee_percentage > MAX_PLATFORM_FEE_PERCENTAGE
{
return Err(Error::InvalidFeeConfig);
}

// Initialize admin (includes re-initialization check)
admin::AdminInitializer::initialize(&env, &admin)?;

// Initialize circuit breaker defaults required by write-gated entrypoints.
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, SYM_PLATFORM_FEE), &fee_percentage);

// Initialize rate limiter config
let rate_limit_config = crate::rate_limiter::RateLimitConfig {
voting_limit: 10_000,
dispute_limit: 1_000,
oracle_call_limit: 1_000,
bet_limit: 10_000,
events_per_admin_limit: 1_000,
time_window_seconds: 3_600,
};
env.storage().persistent().set(
&crate::rate_limiter::RateLimiterData::Config,
&rate_limit_config,
);

// 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);
}

// Initialize allowed assets
if let Some(assets) = allowed_assets {
// Store custom allowed assets
env.storage()
.persistent()
.set(&Symbol::new(&env, SYM_ALLOWED_ASSETS), &assets);
} else {
// Initialize with defaults
crate::tokens::TokenRegistry::initialize_with_defaults(&env);
}

// Emit contract initialized event
EventEmitter::emit_contract_initialized(&env, &admin, fee_percentage);

// Emit platform fee set event
EventEmitter::emit_platform_fee_set(&env, fee_percentage, &admin);

Ok(())
}

pub fn deposit(
env: Env,
user: Address,
asset: ReflectorAsset,
amount: i128,
) -> Result<Balance, Error> {
if let Err(e) =
crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "deposit")
{
return Err(e);
}
balances::BalanceManager::deposit(&env, user, asset, amount)
}

pub fn withdraw(
env: Env,
user: Address,
asset: ReflectorAsset,
amount: i128,
) -> Result<Balance, Error> {
if let Err(e) =
crate::circuit_breaker::CircuitBreaker::require_write_allowed(&env, "withdraw")
{
return Err(e);
}
if !crate::circuit_breaker::CircuitBreaker::are_withdrawals_allowed(&env)? {
return Err(crate::err::Error::CBOpen);
}
balances::BalanceManager::withdraw(&env, user, asset, amount)
}

pub fn get_balance(env: Env, user: Address, asset: ReflectorAsset) -> Balance {
storage::BalanceStorage::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 @@ -7560,12 +7700,12 @@ mod tests {
// Store a resolution summary so ResolutionOutcomeCache::require succeeds.
// (Adjust the key/type to match your actual resolution.rs implementation.)
env.as_contract(&contract_id, || {
let summary = resolution::ResolutionSummary {
let summary = resolution::ResolvedOutcomeSummary {
winning_total: 100_000_000i128,
total_pool: 200_000_000i128,
num_winning_outcomes: 1u32,
};
let cache_key = (Symbol::new(&env, "res_cache"), market_id.clone());
let cache_key = (symbol_short!("res_out"), market_id.clone());
env.storage().persistent().set(&cache_key, &summary);
});

Expand Down Expand Up @@ -7697,4 +7837,4 @@ mod tests {
assert!(guard.consumed() == 0); // No instructions consumed yet in test host
});
}
}mod dispute_multisig;
}
Loading