This document analyzes security threats specific to the Predictify Hybrid prediction market smart contracts and details the implemented mitigations. Each attack vector is mapped to specific code-level protections and validation mechanisms.
- Target: Predictify Hybrid Soroban smart contracts
- Assets: User funds, market integrity, oracle data, admin controls
- Actors: Malicious users, compromised oracles, insider threats, external attackers
- Confidentiality: Protect sensitive market and user data
- Integrity: Ensure market outcomes and transactions remain unaltered
- Availability: Maintain contract operation under adverse conditions
- Accountability: Track all administrative and critical actions
Threat: Malicious actors gain admin privileges to manipulate markets or steal funds.
Attack Scenarios:
- Direct admin function calls by non-admin users
- Privilege escalation through role manipulation
- Admin key compromise or theft
Mitigations:
// Multi-layered authorization checks
pub fn require_admin_role(env: &Env, caller: &Address, required_role: AdminRole) -> Result<(), Error> {
let admin_manager = AdminManager::from_env(env);
// Check if caller has required role
if !admin_manager.has_role(caller, required_role)? {
return Err(Error::Unauthorized);
}
// Additional permission validation
if !admin_manager.has_permission(caller, required_permission)? {
return Err(Error::Unauthorized);
}
Ok(())
}Code References:
admin.rs: Role-based access control systemvalidation.rs: Admin permission validation- Error code:
Error::Unauthorized (100)
Threat: Admin private keys are compromised, allowing complete contract control.
Mitigations:
- Multi-Admin System: Requires multiple admins for critical operations
- Time-Based Controls: Admin actions have time windows and rate limits
- Audit Trail: All admin actions are logged and auditable
- Recovery Mechanisms: Compromised admin keys can be revoked
// Multi-admin validation for critical operations
pub fn require_multi_admin_approval(env: &Env, action: AdminAction) -> Result<(), Error> {
let required_approvals = get_required_approvals(action);
let current_approvals = get_admin_approvals(env, action)?;
if current_approvals.len() < required_approvals {
return Err(Error::InsufficientApprovals);
}
Ok(())
}Code References:
admin.rs: Multi-admin approval systemaudit_trail.rs: Action logging and tracking- Error codes:
Error::InsufficientApprovals (418)
Threat: Attackers create confusing duplicate outcomes to manipulate betting behavior.
Attack Scenarios:
- "Yes" vs "yes " (with trailing space)
- "Yes" vs "YES" (case variation)
- "Yes" vs "yes!" (punctuation variation)
Mitigations:
// Outcome deduplication system
pub struct OutcomeDeduplicator;
impl OutcomeDeduplicator {
/// Normalizes outcomes for consistent comparison
pub fn normalize_outcome(outcome: &String) -> Result<String, ValidationError> {
// 1. Trim whitespace
let trimmed = outcome.trim();
if trimmed.is_empty() {
return Err(ValidationError::OutcomeNormalizationFailed);
}
// 2. Case normalization
let lowercased = trimmed.to_lowercase();
// 3. Whitespace compression
let compressed = lowercased.split_whitespace().collect::<Vec<&str>>().join(" ");
// 4. Punctuation removal
let cleaned = compressed.chars()
.filter(|c| !matches!(c, '!' | '?' | '.' | ',' | ';' | ':' | '"' | '\'' | '(' | ')' | '[' | ']' | '{' | '}'))
.collect::<String>();
if cleaned.is_empty() {
return Err(ValidationError::OutcomeNormalizationFailed);
}
Ok(String::from_str(&outcome.env(), &cleaned))
}
/// Validates outcomes for duplicates and ambiguities
pub fn validate_outcomes(outcomes: &Vec<String>) -> Result<(), ValidationError> {
let mut normalized_outcomes = Vec::new();
// Normalize and collect all outcomes
for outcome in outcomes {
let normalized = Self::normalize_outcome(outcome)?;
// Check for exact duplicates
if normalized_outcomes.contains(&normalized) {
return Err(ValidationError::DuplicateOutcome);
}
normalized_outcomes.push(normalized);
}
// Check for ambiguous outcomes (high similarity)
for i in 0..normalized_outcomes.len() {
for j in (i + 1)..normalized_outcomes.len() {
let similarity = Self::calculate_similarity(&normalized_outcomes[i], &normalized_outcomes[j]);
if similarity > 80 { // 80% similarity threshold
return Err(ValidationError::AmbiguousOutcome);
}
// Check for semantic duplicates
if Self::is_semantic_duplicate(&normalized_outcomes[i], &normalized_outcomes[j]) {
return Err(ValidationError::AmbiguousOutcome);
}
}
}
Ok(())
}
}Code References:
validation.rs: OutcomeDeduplicator implementation- Error codes:
ValidationError::DuplicateOutcome,ValidationError::AmbiguousOutcome - Test coverage:
metadata_validation_tests.rs
Threat: Manipulating oracle data or resolution process to favor specific outcomes.
Mitigations:
- Oracle Validation: Strict oracle provider validation and whitelisting
- Signature Verification: Cryptographic validation of oracle data
- Replay Protection: Timestamp and nonce validation
- Multi-Oracle Consensus: Require multiple oracle confirmations
// Oracle security validation
pub fn validate_oracle_signature(
env: &Env,
oracle_data: &OracleData,
expected_oracle: &Address
) -> Result<(), Error> {
// Verify oracle is whitelisted
let oracle_whitelist = OracleWhitelist::from_env(env);
if !oracle_whitelist.is_approved(expected_oracle)? {
return Err(Error::UnauthorizedOracle);
}
// Verify cryptographic signature
if !verify_signature(oracle_data, expected_oracle)? {
return Err(Error::InvalidOracleSignature);
}
// Check for replay attacks
if is_replay_attack(env, oracle_data)? {
return Err(Error::ReplayAttack);
}
Ok(())
}Code References:
oracles.rs: Oracle validation and managementtests/security/oracle_security_tests.rs: Oracle security tests- Error codes:
Error::UnauthorizedOracle (201),Error::InvalidOracleSignature (202)
Threat: Injecting malicious strings or data to cause unexpected behavior.
Attack Scenarios:
- Unicode manipulation attacks
- Zero-width character injection
- Buffer overflow attempts
- Format string attacks
Mitigations:
// Comprehensive input validation
pub struct InputValidator;
impl InputValidator {
/// Validates string input with security checks
pub fn validate_secure_string(input: &String, min_len: u32, max_len: u32) -> Result<(), ValidationError> {
// Length validation
if input.len() < min_len as usize || input.len() > max_len as usize {
return Err(ValidationError::InvalidStringLength);
}
// Unicode safety validation
if contains_malicious_unicode(input)? {
return Err(ValidationError::InvalidUnicode);
}
// Control character validation
if contains_control_characters(input)? {
return Err(ValidationError::InvalidCharacters);
}
// Pattern validation
if matches_dangerous_patterns(input)? {
return Err(ValidationError::InvalidFormat);
}
Ok(())
}
/// Checks for malicious Unicode sequences
fn contains_malicious_unicode(input: &String) -> Result<bool, ValidationError> {
// Check for zero-width characters
if input.chars().any(|c| c.is_control() && c != ' ' && c != '\t' && c != '\n' && c != '\r') {
return Ok(true);
}
// Check for suspicious Unicode normalization
let normalized = input.to_lowercase();
if normalized != input.to_lowercase() {
// Potential Unicode spoofing attempt
return Ok(true);
}
Ok(false)
}
}Code References:
validation.rs: InputValidator implementation- Error codes:
ValidationError::InvalidString,ValidationError::InvalidFormat
Threat: Overwhelming the contract with expensive operations to cause denial of service.
Attack Scenarios:
- Creating excessive numbers of markets
- Large input strings causing gas exhaustion
- Recursive call attempts
- Storage exhaustion attacks
Mitigations:
// Circuit breaker for DoS protection
pub struct CircuitBreaker;
impl CircuitBreaker {
/// Checks if operation should be allowed based on system state
pub fn check_operation_allowed(env: &Env, operation: OperationType) -> Result<(), Error> {
let breaker_state = Self::get_state(env);
match breaker_state {
BreakerState::Closed => {
// Check operation-specific limits
Self::check_operation_limits(env, operation)?;
},
BreakerState::Open => {
return Err(Error::CircuitBreakerOpen);
},
BreakerState::HalfOpen => {
// Limited operations allowed for testing
Self::check_half_open_limits(env, operation)?;
}
}
Ok(())
}
/// Rate limiting for expensive operations
pub fn check_rate_limits(env: &Env, caller: &Address, operation: OperationType) -> Result<(), Error> {
let rate_limiter = RateLimiter::from_env(env);
if rate_limiter.is_rate_limited(caller, operation)? {
return Err(Error::RateLimitExceeded);
}
Ok(())
}
}Code References:
circuit_breaker.rs: DoS protection mechanismsrate_limiter.rs: Rate limiting implementation- Error codes:
Error::CircuitBreakerOpen (500),Error::RateLimitExceeded (501)
Threat: Forcing users to spend excessive gas on operations.
Mitigations:
- Gas Limits: Maximum gas limits for operations
- Early Validation: Fail fast on invalid inputs
- Efficient Algorithms: Optimized for gas usage
- Batch Operation Limits: Limits on batch sizes
// Gas-efficient validation
pub fn validate_gas_efficient(outcomes: &Vec<String>) -> Result<(), ValidationError> {
// Early size check to prevent expensive operations
if outcomes.len() > MAX_MARKET_OUTCOMES as usize {
return Err(ValidationError::ArrayTooLarge);
}
// Batch validation for efficiency
for (i, outcome) in outcomes.iter().enumerate() {
// Quick length check first
if outcome.len() > MAX_OUTCOME_LENGTH as usize {
return Err(ValidationError::StringTooLong);
}
// Only then do more expensive validation
if i < outcomes.len() - 1 {
// Check for duplicates in remaining outcomes
for other in &outcomes[i+1..] {
if are_similar_fast(outcome, other)? {
return Err(ValidationError::AmbiguousOutcome);
}
}
}
}
Ok(())
}Threat: Manipulating market prices or odds for financial gain.
Mitigations:
- Price Validation: Oracle price validation and sanity checks
- Liquidity Requirements: Minimum liquidity thresholds
- Betting Limits: Maximum bet sizes and exposure limits
- Market Making: Automated market making to prevent manipulation
// Price manipulation protection
pub fn validate_price_data(env: &Env, price_data: &PriceData) -> Result<(), Error> {
// Sanity check on price values
if price_data.price <= 0 || price_data.price > MAX_REASONABLE_PRICE {
return Err(Error::InvalidPrice);
}
// Check for price deviation from previous
if let Some(prev_price) = get_previous_price(env, &price_data.asset)? {
let deviation = calculate_price_deviation(price_data.price, prev_price);
if deviation > MAX_PRICE_DEVIATION {
return Err(Error::ExcessivePriceDeviation);
}
}
// Validate timestamp freshness
if env.ledger().timestamp() - price_data.timestamp > MAX_PRICE_AGE {
return Err(Error::StalePriceData);
}
Ok(())
}Code References:
fees.rs: Fee validation and limitsmarkets.rs: Market integrity checks- Error codes:
Error::InvalidPrice (203),Error::ExcessivePriceDeviation (204)
Threat: Exploiting knowledge of pending transactions for profit.
Mitigations:
- Transaction Ordering: Fair transaction ordering mechanisms
- Commit-Reveal Schemes: Two-phase commitment for sensitive operations
- Time-Based Windows: Fixed time windows for operations
- Randomized Delays: Small randomized delays to prevent timing attacks
Threat: Compromised oracles providing incorrect data.
Mitigations:
- Multi-Oracle Consensus: Require multiple oracle confirmations
- Oracle Reputation: Reputation-based oracle selection
- Data Validation: Cross-validation of oracle data
- Fallback Mechanisms: Alternative oracle sources
// Multi-oracle validation
pub fn validate_multi_oracle_consensus(
env: &Env,
oracle_data: Vec<OracleData>
) -> Result<ConsensusResult, Error> {
// Require minimum number of oracle responses
if oracle_data.len() < MIN_ORACLE_RESPONSES {
return Err(Error::InsufficientOracleResponses);
}
// Check for consensus among oracles
let consensus_data = find_consensus(&oracle_data)?;
let agreement_percentage = calculate_agreement(&oracle_data, &consensus_data);
if agreement_percentage < MIN_CONSENSUS_THRESHOLD {
return Err(Error::NoOracleConsensus);
}
Ok(ConsensusResult {
data: consensus_data,
confidence: agreement_percentage,
participating_oracles: oracle_data.len(),
})
}Code References:
oracles.rs: Multi-oracle consensus systemtests/security/oracle_security_tests.rs: Oracle security tests- Error codes:
Error::InsufficientOracleResponses (205),Error::NoOracleConsensus (206)
Threat: Recursive calls that can drain contract funds.
Mitigations:
- Checks-Effects-Interactions Pattern: Proper ordering of operations
- Reentrancy Guards: Explicit reentrancy protection
- State Updates First: Update contract state before external calls
// Reentrancy protection
pub struct ReentrancyGuard;
impl ReentrancyGuard {
pub fn enter(env: &Env) -> Result<(), Error> {
let guard_key = StorageKey::ReentrancyGuard;
if env.storage().get(&guard_key).unwrap_or(false) {
return Err(Error::ReentrancyDetected);
}
env.storage().set(&guard_key, &true);
Ok(())
}
pub fn exit(env: &Env) {
let guard_key = StorageKey::ReentrancyGuard;
env.storage().set(&guard_key, &false);
}
}
// Usage pattern
pub fn place_bet(env: &Env, caller: Address, market_id: u64, outcome: u32, amount: i128) -> Result<(), Error> {
// Reentrancy protection
ReentrancyGuard::enter(env)?;
// Checks
validate_bet_params(env, &caller, market_id, outcome, amount)?;
// Effects (update state first)
update_bet_state(env, &caller, market_id, outcome, amount)?;
// Interactions (external calls last)
transfer_tokens(env, &caller, amount)?;
// Release guard
ReentrancyGuard::exit(env);
Ok(())
}Code References:
- Error code:
Error::ReentrancyDetected (417)
Threat: Arithmetic operations causing unexpected behavior.
Mitigations:
- Safe Math Operations: Use checked arithmetic
- Range Validation: Validate input ranges
- Type Safety: Use appropriate numeric types
// Safe arithmetic operations
pub fn safe_add(a: i128, b: i128) -> Result<i128, Error> {
a.checked_add(b).ok_or(Error::ArithmeticOverflow)
}
pub fn safe_subtract(a: i128, b: i128) -> Result<i128, Error> {
a.checked_sub(b).ok_or(Error::ArithmeticUnderflow)
}
pub fn safe_multiply(a: i128, b: i128) -> Result<i128, Error> {
a.checked_mul(b).ok_or(Error::ArithmeticOverflow)
}
// Usage in betting calculations
pub fn calculate_winnings(bet_amount: i128, odds: u32) -> Result<i128, Error> {
let odds_as_i128 = i128::from(odds);
let numerator = safe_multiply(bet_amount, odds_as_i128)?;
safe_divide(numerator, 10000i128) // Odds are in basis points
}Code References:
- Error codes:
Error::ArithmeticOverflow (415),Error::ArithmeticUnderflow (416)
- Unit Tests: Individual function security testing
- Integration Tests: Cross-module security validation
- Property-Based Tests: Fuzzing and edge case testing
- Security-Specific Tests: Attack simulation and mitigation verification
tests/security/oracle_security_tests.rs: Oracle security testingtests/metadata_validation_tests.rs: Input validation and deduplicationtests/property_based_tests.rs: Fuzzing and edge case testingtests/circuit_breaker_tests.rs: DoS protection testing
- โฅ95% line coverage on all security-critical modules
- 100% coverage on authentication and authorization logic
- Comprehensive edge case testing for all validation functions
- Failed Authentication Rate: Monitor unauthorized access attempts
- Validation Failure Rate: Track input validation failures
- Oracle Consensus Health: Monitor oracle agreement rates
- Circuit Breaker Triggers: Track DoS protection activation
All security-relevant actions are logged:
pub struct AuditEvent {
pub timestamp: u64,
pub actor: Address,
pub action: AuditAction,
pub target: Option<Address>,
pub metadata: String,
pub result: AuditResult,
}- Critical: Fund loss, admin compromise, oracle manipulation
- High: Market manipulation, DoS attacks, data corruption
- Medium: Unauthorized access attempts, validation failures
- Low: Suspicious activity, performance issues
- Detection: Automated monitoring and alerting
- Assessment: Impact analysis and threat classification
- Containment: Circuit breaker activation, access revocation
- Recovery: System restoration, fund recovery
- Post-Mortem: Analysis and improvement implementation
- No Unauthorized Admin Access: All admin actions require proper authorization
- No Duplicate Market Outcomes: All outcomes must be unique and unambiguous
- No Fund Drainage: Contract funds cannot be drained by attackers
- No Oracle Manipulation: Oracle data must be validated and consensus-based
- No DoS Vulnerabilities: System remains available under attack conditions
- All inputs must pass comprehensive validation
- All state changes must be authorized and audited
- All external calls must be protected against reentrancy
- All arithmetic operations must be safe against overflow/underflow
- All security tests pass with โฅ95% coverage
- Manual security review completed
- Third-party audit findings addressed
- Circuit breaker functionality verified
- Admin access controls tested
- Oracle security validated
- Continuous monitoring enabled
- Alert systems configured
- Incident response procedures tested
- Regular security reviews scheduled
- Audit trail integrity verified
- Zero-Knowledge Proofs: Enhanced privacy for sensitive operations
- Multi-Signature Wallets: Enhanced admin security
- Decentralized Oracle Networks: Improved oracle security and reliability
- Formal Verification: Mathematical proof of security properties
- Machine Learning Detection: AI-powered anomaly detection
- Quantum-resistant cryptography
- Advanced fraud detection algorithms
- Cross-chain security protocols
- Privacy-preserving market mechanisms
This document serves as the authoritative reference for Predictify Hybrid security architecture and should be updated whenever new threats are identified or mitigations are implemented.
Description: If fetch_oracle_result is called after end_time + resolution_timeout and a dispute is active, the naive implementation would cancel the market. This permanently locks dispute stakes and leaves the market unresolvable โ a deadlock.
Invariant: resolution_timeout must never cancel a market that has an active dispute (state == Disputed or total_dispute_stakes() > 0). When a dispute is active, the dispute process is the authoritative resolution path.
Mitigation (resolution.rs โ OracleResolutionManager::fetch_oracle_result):
if current_time > market.end_time + market.resolution_timeout {
if market.state == MarketState::Disputed || market.total_dispute_stakes() > 0 {
return Err(Error::ResolutionTimeoutReached); // dispute owns resolution
}
// No dispute: safe to cancel for refunds
market.state = MarketState::Cancelled;
...
}Description: Without enforcing dispute_window_seconds, a dispute could be filed long after the market ended, re-opening a market that participants already consider settled and payouts already expected.
Invariant: Disputes must be filed within [end_time, end_time + dispute_window_seconds). After the window closes, payouts are unambiguously allowed.
Mitigation (disputes.rs โ DisputeValidator::validate_market_for_dispute):
if market.dispute_window_seconds > 0
&& current_time >= market.end_time + market.dispute_window_seconds
{
return Err(Error::MarketResolved);
}Note: dispute_window_seconds == 0 disables the window check (no restriction), preserving backward compatibility for markets created before this field was introduced.
- This does not prevent an oracle from returning stale data within the timeout window; that is handled separately by
OracleStale/OracleConfidenceTooWide. - Admin override (
finalize_market) bypasses both checks by design โ it is an emergency escape hatch requiring explicit admin authentication.