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
49 changes: 26 additions & 23 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 @@ -644,12 +642,6 @@ impl ErrorHandler {
Error::ForceResolveAlreadyUsed => {
"Force-resolve idempotency key already used. The operation is a safe no-op."
}
Error::ForceResolveReplayed => {
"Force-resolve idempotency key already used. Use a new unique key."
}
Error::ForceResolveReasonEmpty => {
"Force-resolve reason is empty. Provide a non-empty reason string."
}
_ => "An error occurred. Please verify your parameters and try again.",
};
String::from_str(env, msg)
Expand Down Expand Up @@ -773,9 +765,7 @@ impl ErrorHandler {
| Error::AlreadyClaimed
| Error::FeeAlreadyCollected
| Error::ForceResolveAlreadyUsed => RecoveryStrategy::Skip,
Error::ForceResolveReplayed | Error::ForceResolveReasonEmpty => {
RecoveryStrategy::Retry
}

Error::Unauthorized | Error::MarketClosed | Error::MarketResolved => {
RecoveryStrategy::Abort
}
Expand Down Expand Up @@ -1279,7 +1269,9 @@ impl ErrorHandler {
/// # Returns
///
/// A tuple of (severity, category, recovery_strategy) for the error.
pub(crate) fn get_error_classification(error: &Error) -> (ErrorSeverity, ErrorCategory, RecoveryStrategy) {
pub(crate) fn get_error_classification(
error: &Error,
) -> (ErrorSeverity, ErrorCategory, RecoveryStrategy) {
match error {
// Critical
Error::AdminNotSet => (
Expand Down Expand Up @@ -1349,11 +1341,7 @@ impl ErrorHandler {
ErrorCategory::UserOperation,
RecoveryStrategy::Skip,
),
Error::ForceResolveReplayed | Error::ForceResolveReasonEmpty => (
ErrorSeverity::Low,
ErrorCategory::UserOperation,
RecoveryStrategy::Retry,
),

Error::FeeAlreadyCollected => (
ErrorSeverity::Low,
ErrorCategory::Financial,
Expand Down Expand Up @@ -1478,7 +1466,9 @@ 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::OperationWouldExceedBudget => {
"Operation would exceed the available CPU instruction budget"
}
Error::OracleUnavailable => "Oracle is unavailable",
Error::InvalidOracleConfig => "Invalid oracle configuration",
Error::GasBudgetExceeded => "Gas budget exceeded",
Expand All @@ -1503,6 +1493,9 @@ impl Error {
Error::FeeArithmeticOverflow => "Fee arithmetic overflowed",
Error::FeeAlreadyCollected => "Platform fee already collected",
Error::NoFeesToCollect => "No fees available to collect",
Error::ForceResolveAlreadyUsed => {
"Force-resolve idempotency key already used for this market"
}
Error::InvalidExtensionDays => "Invalid extension days value",
Error::ExtensionDenied => "Market extension not allowed",
Error::AdminNotSet => "Admin address not set",
Expand Down Expand Up @@ -1555,14 +1548,22 @@ impl Error {
Error::InsufficientStorageRentBudget => {
"Insufficient storage rent budget for operation"
}
Error::ExtensionCapExceeded => "Cumulative extension cap for this market has been reached",
Error::ExtensionCapExceeded => {
"Cumulative extension cap for this market has been reached"
}
Error::UpgradeChainMismatch => "Upgrade chain predecessor hash mismatch",
Error::ReplayedOverride => "Admin override nonce replayed; rejected",
Error::AssetDecimalsMismatch => "Asset decimals mismatch between stored and SAC decimals",
Error::AssetDecimalsMismatch => {
"Asset decimals mismatch between stored and SAC decimals"
}
Error::DuplicateMarketId => "Market ID already exists in the registry",
Error::CumulativeExtensionCapHit => "Cumulative extension cap reached; no further extensions allowed",
Error::CumulativeExtensionCapHit => {
"Cumulative extension cap reached; no further extensions allowed"
}
Error::IllegalMarketStateTransition => "Illegal market state transition attempted",
Error::OracleQuoteOutlier => "Oracle quote is an outlier relative to the rolling median",
Error::OracleQuoteOutlier => {
"Oracle quote is an outlier relative to the rolling median"
}
}
}

Expand Down Expand Up @@ -1593,6 +1594,7 @@ impl Error {
Error::OracleUnavailable => "ORACLE_UNAVAILABLE",
Error::InvalidOracleConfig => "INVALID_ORACLE_CONFIG",
Error::GasBudgetExceeded => "GAS_BUDGET_EXCEEDED",
Error::OperationWouldExceedBudget => "OPERATION_WOULD_EXCEED_BUDGET",
Error::InvalidQuestion => "INVALID_QUESTION",
Error::InvalidOutcomes => "INVALID_OUTCOMES",
Error::InvalidDuration => "INVALID_DURATION",
Expand All @@ -1617,6 +1619,7 @@ impl Error {
Error::InvalidExtensionDays => "INVALID_EXTENSION_DAYS",
Error::ExtensionDenied => "EXTENSION_DENIED",
Error::AdminNotSet => "ADMIN_NOT_SET",
Error::ForceResolveAlreadyUsed => "FORCE_RESOLVE_ALREADY_USED",
Error::FeeExceedsMax => "FEE_ABOVE_ACCEPTABLE",
Error::OracleStale => "ORACLE_STALE",
Error::OracleNoConsensus => "ORACLE_NO_CONSENSUS",
Expand Down Expand Up @@ -2332,4 +2335,4 @@ mod tests {
assert_eq!(recovery.max_recovery_attempts, 2);
assert!(recovery.recovery_success_timestamp.is_some());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ fn create_market_rejects_overflow_ledger_sequence() {
&None,
&86_400u64,
);
assert_contract_error(result, Error::InsufficientStorageRent);
assert_contract_error(result, Error::InsufficientStorageRentBudget);
}

#[test]
Expand Down
39 changes: 24 additions & 15 deletions contracts/predictify-hybrid/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use super::*;
use crate::markets::{MarketStateLogic, MarketStateManager};
use crate::types::{Balance, ReflectorAsset, Market, MarketState, OracleConfig};
use crate::types::{Balance, Market, MarketState, OracleConfig, ReflectorAsset};
use soroban_sdk::{contracttype, Address, Env, IntoVal, Map, Symbol, Val, Vec};

const STORAGE_CONFIG_KEY: &str = "storage_config";
Expand Down Expand Up @@ -37,13 +37,13 @@ pub const MARKET_CREATION_PERSISTENT_KEYS: u32 = 1;
///
/// # Errors
///
/// Returns [`Error::InsufficientStorageRent`] if the sequence would overflow.
/// Returns [`Error::InsufficientStorageRentBudget`] if the sequence would overflow.
pub fn check_market_creation_rent(env: &Env) -> Result<(), Error> {
let effective_ttl = MARKET_TTL_LEDGERS.min(env.storage().max_ttl());
let current_seq = env.ledger().sequence();

if current_seq.checked_add(effective_ttl).is_none() {
return Err(Error::InsufficientStorageRent);
return Err(Error::InsufficientStorageRentBudget);
}

Ok(())
Expand Down Expand Up @@ -202,7 +202,7 @@ impl StorageMigration {
metadata.admin.require_auth();

let config = StorageOptimizer::get_storage_config(env);

StorageOptimizer::set_persistent_with_ttl(
env,
&persistent_key,
Expand Down Expand Up @@ -253,7 +253,10 @@ impl StorageMigration {

market.admin.require_auth();

let scratch_opt = env.storage().persistent().get::<_, Vec<i128>>(&persistent_key);
let scratch_opt = env
.storage()
.persistent()
.get::<_, Vec<i128>>(&persistent_key);

if let Some(scratch_data) = scratch_opt {
let config = StorageOptimizer::get_storage_config(env);
Expand Down Expand Up @@ -360,12 +363,8 @@ impl StorageOptimizer {
.extend_ttl(key, effective_ttl, effective_ttl);
}

fn set_persistent_with_ttl<K, V>(
env: &Env,
key: &K,
value: &V,
desired_ttl_ledgers: u32,
) where
fn set_persistent_with_ttl<K, V>(env: &Env, key: &K, value: &V, desired_ttl_ledgers: u32)
where
K: IntoVal<Env, Val>,
V: IntoVal<Env, Val>,
{
Expand Down Expand Up @@ -851,7 +850,11 @@ impl StorageOptimizer {
}

/// Archive market data before deletion
pub(crate) fn archive_market_data(env: &Env, market_id: &Symbol, market: &Market) -> Result<(), Error> {
pub(crate) fn archive_market_data(
env: &Env,
market_id: &Symbol,
market: &Market,
) -> Result<(), Error> {
// Store archived version with timestamp
let archive_key = DataKey::ArchivedMarket(market_id.clone(), env.ledger().timestamp());
Self::set_persistent_with_ttl(
Expand Down Expand Up @@ -915,7 +918,11 @@ impl StorageOptimizer {
env: &Env,
compressed_market: &CompressedMarket,
) -> Result<(), Error> {
let key = crate::event_archive::derive_archive_key(env, &compressed_market.market_id, "compressed");
let key = crate::event_archive::derive_archive_key(
env,
&compressed_market.market_id,
"compressed",
);
Self::set_persistent_with_ttl(
env,
&key,
Expand Down Expand Up @@ -1263,7 +1270,8 @@ mod tests {
BalanceStorage::set_balance(&env, &balance);

let key = BalanceStorage::get_key(&env, &user, &asset);
let expected_ttl = StorageOptimizer::persistent_ttl_for_tier(&env, StorageTtlTier::Balance);
let expected_ttl =
StorageOptimizer::persistent_ttl_for_tier(&env, StorageTtlTier::Balance);
assert_eq!(env.storage().persistent().get_ttl(&key), expected_ttl);

env.ledger().with_mut(|li| {
Expand All @@ -1288,7 +1296,8 @@ mod tests {
env.as_contract(&contract_id, || {
EventManager::store_event(&env, &event);
let key = EventManager::event_storage_key(&env, &event.id);
let expected_ttl = StorageOptimizer::persistent_ttl_for_tier(&env, StorageTtlTier::Event);
let expected_ttl =
StorageOptimizer::persistent_ttl_for_tier(&env, StorageTtlTier::Event);
assert_eq!(env.storage().persistent().get_ttl(&key), expected_ttl);
});
}
Expand Down
23 changes: 20 additions & 3 deletions contracts/predictify-hybrid/tests/err_stability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ fn general_errors() {
assert_eq!(Error::InvalidExtensionDays as u32, 415);
assert_eq!(Error::ExtensionDenied as u32, 416);
assert_eq!(Error::GasBudgetExceeded as u32, 417);
assert_eq!(Error::AdminNotSet as u32, 418);
assert_eq!(Error::OperationWouldExceedBudget as u32, 418);
assert_eq!(Error::AdminNotSet as u32, 419);
assert_eq!(Error::QuestionTooLong as u32, 420);
assert_eq!(Error::OutcomeTooLong as u32, 421);
assert_eq!(Error::TooManyOutcomes as u32, 422);
Expand All @@ -104,14 +105,22 @@ fn general_errors() {
assert_eq!(Error::TooManyExtensions as u32, 432);
assert_eq!(Error::TooManyOracleResults as u32, 433);
assert_eq!(Error::TooManyWinningOutcomes as u32, 434);
assert_eq!(Error::ForceResolveAlreadyUsed as u32, 435);
assert_eq!(Error::CategoryTooShort as u32, 436);
assert_eq!(Error::TagTooShort as u32, 437);
assert_eq!(Error::DisputerCannotVote as u32, 438);
assert_eq!(Error::ArchiveFull as u32, 440);
assert_eq!(Error::DuplicateMarketId as u32, 441);
}

// ===== Circuit Breaker Errors (500-508) =====
// ===== Override / Replay Error =====

#[test]
fn override_errors() {
assert_eq!(Error::ReplayedOverride as u32, 526);
}

// ===== Circuit Breaker Errors (500-527) =====

#[test]
fn circuit_breaker_errors() {
Expand All @@ -124,6 +133,14 @@ fn circuit_breaker_errors() {
assert_eq!(Error::CumulativeExtensionCapHit as u32, 506);
assert_eq!(Error::IllegalMarketStateTransition as u32, 507);
assert_eq!(Error::FeeExceedsMax as u32, 508);
assert_eq!(Error::NoPendingFeeCommit as u32, 519);
assert_eq!(Error::FeeRevealTooEarly as u32, 520);
assert_eq!(Error::FeePreimageMismatch as u32, 521);
assert_eq!(Error::DisputeStakeCapExceeded as u32, 522);
assert_eq!(Error::InsufficientStorageRentBudget as u32, 523);
assert_eq!(Error::ExtensionCapExceeded as u32, 524);
assert_eq!(Error::UpgradeChainMismatch as u32, 525);
assert_eq!(Error::OracleQuoteOutlier as u32, 527);
}

// ===== Asset decimals =====
Expand All @@ -146,4 +163,4 @@ fn total_variant_count() {
// update this comment when updating the count.
let expected = 93;
assert_eq!(std::mem::variant_count::<Error>(), expected);
}
}
Loading