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
65 changes: 60 additions & 5 deletions contracts/claims-processor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,12 @@ impl ClaimsProcessor {
/// Returns a Vec of (claim_id, result) pairs for the processed claims.
/// Skips any claim that is not in Pending status (idempotent).
///
/// FAIRNESS GUARANTEE: Claims are processed in FIFO (First-In-First-Out) order
/// based on their submission timestamp. This ensures older claims are settled
/// before newer ones, preventing unfair advantage to recently submitted claims.
/// The pending queue maintains insertion order, and this function processes
/// from index 0 (oldest) onwards.
///
/// `limit` is clamped to [`MAX_BATCH_SIZE`]. Passing a larger value (or
/// `u32::MAX`) is not an error — it simply settles the first
/// `MAX_BATCH_SIZE` pending claims, keeping the transaction inside
Expand All @@ -819,14 +825,53 @@ impl ClaimsProcessor {
if limit == 0 {
panic_with_error!(&env, Error::InvalidInput);
}
let pending: Vec<u128> = env.storage().instance()

// FAIRNESS FIX: Get pending claims list - maintained in FIFO order
let mut pending: Vec<u128> = env.storage().instance()
.get(&StorageKey::PendingClaims)
.unwrap_or_else(|| Vec::new(&env));


// FAIRNESS FIX: Sort claims by submission time to ensure true FIFO processing.
// Even if the pending queue gets slightly out of order (due to concurrent
// submissions or manual queue manipulation), this ensures oldest claims
// are always processed first.
let mut claim_timestamps: Vec<(u128, u64)> = Vec::new(&env);
for i in 0..pending.len() {
let claim_id = pending.get_unchecked(i);
if let Some(claim) = env.storage().persistent().get::<_, Claim>(&StorageKey::Claim(claim_id)) {
claim_timestamps.push_back((claim_id, claim.submitted_at));
}
}

// Sort by timestamp (ascending - oldest first) using bubble sort
// Bubble sort is used here because Soroban doesn't have built-in sorting
let len = claim_timestamps.len();
for i in 0..len {
for j in 0..(len - i - 1) {
let curr = claim_timestamps.get_unchecked(j);
let next = claim_timestamps.get_unchecked(j + 1);
if curr.1 > next.1 {
// Swap
claim_timestamps.set(j, next);
claim_timestamps.set(j + 1, curr);
}
}
}

// Rebuild pending queue in correct FIFO order
let mut sorted_pending: Vec<u128> = Vec::new(&env);
for i in 0..claim_timestamps.len() {
let (claim_id, _) = claim_timestamps.get_unchecked(i);
sorted_pending.push_back(claim_id);
}

// Update storage with sorted order
env.storage().instance().set(&StorageKey::PendingClaims, &sorted_pending);

let mut results: Vec<(u128, ClaimResult)> = Vec::new(&env);
let effective_limit = if limit > MAX_BATCH_SIZE { MAX_BATCH_SIZE } else { limit };
let process_count = if pending.len() < effective_limit {
pending.len()
let process_count = if sorted_pending.len() < effective_limit {
sorted_pending.len()
} else {
effective_limit
};
Expand All @@ -835,7 +880,7 @@ impl ClaimsProcessor {
let fn_name = Symbol::new(&env, "auto_process");

for i in 0..process_count {
let claim_id = pending.get_unchecked(i);
let claim_id = sorted_pending.get_unchecked(i);

// Pre-check: skip claims that are no longer Pending without
// spending a sub-invocation on them (fast path, no isolation cost).
Expand Down Expand Up @@ -1874,6 +1919,16 @@ impl ClaimsProcessor {
panic_with_error!(&env, Error::NotEscalatable);
}

// SECURITY FIX: Verify escalation is within the allowed time window from claim submission.
// Claims should only be escalatable within a reasonable time after submission.
// After the policy end_time + claim_deadline has passed, escalation is no longer meaningful
// because the policy period is over and the claim should have been processed already.
let claim_deadline = Self::claim_deadline(&env);
let escalation_cutoff = policy.end_time.saturating_add(claim_deadline);
if now > escalation_cutoff {
panic_with_error!(&env, Error::NotEscalatable);
}

claim.status = ClaimStatus::Escalated;
let policy_id = claim.policy_id;
let claimant = claim.claimant.clone();
Expand Down
107 changes: 105 additions & 2 deletions contracts/oracle-verifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ const DEFAULT_OUTLIER_MIN_SAMPLE_SIZE: u32 = 4;
/// can consume by flooding submissions.
const DEFAULT_MIN_SUBMIT_INTERVAL: u64 = 30;

/// Default per-data-type range limits for common oracle data types.
/// These prevent data type-specific overflow issues in downstream calculations.
/// Admin can override these via set_data_type_range.
const DEFAULT_WEATHER_MAX: i128 = 10_000_000_000i128; // 10,000 mm rainfall or 1000°C in 7-decimal
const DEFAULT_FLIGHT_MAX: i128 = 172_800_000_000i128; // 48 hours delay in seconds (7-decimal)
const DEFAULT_ONCHAIN_MAX: i128 = 1_000_000_000_000_000_000_000i128; // 10^21 for DeFi values
const DEFAULT_DEFI_MAX: i128 = 1_000_000_000_000_000_000_000i128; // 10^21 for DeFi values
const DEFAULT_DISASTER_MAX: i128 = 100_000_000i128; // Binary or magnitude values (7-decimal)
const DEFAULT_HEALTH_MAX: i128 = 1_000_000_000i128; // Health metrics (7-decimal)

// ─── Storage keys ─────────────────────────────────────────────────────────────

#[contracttype]
Expand Down Expand Up @@ -175,6 +185,10 @@ enum StorageKey {
/// Seconds an oracle may go without submitting before it is treated as
/// offline (u64). 0 or unset = offline detection disabled.
OracleInactivityLimit,
/// Per-data-type value range limits (min, max) as (i128, i128).
/// Validates submissions against data-type-specific bounds to prevent
/// overflow in calculations specific to that data type.
DataTypeRange(Symbol),
}

// ─── Errors ───────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -832,6 +846,58 @@ impl OracleVerifier {
Self::effective_outlier_config(&env, &data_type)
}

/// Admin-only: set per-data-type value range limits.
/// Extreme values can cause overflow in aggregation calculations.
/// Use this to enforce realistic bounds for specific data types.
///
/// `min` and `max` are absolute value bounds in 7-decimal fixed point.
/// Values submitted outside this range will be rejected with InvalidInput.
///
/// Clear a configured range by setting both to 0 (reverts to defaults).
pub fn set_data_type_range(
env: Env,
admin: Address,
data_type: Symbol,
min: i128,
max: i128,
) {
Self::require_admin(&env, &admin);

// Validate the range makes sense
if min < 0 || max < 0 {
panic_with_error!(&env, Error::InvalidInput);
}
if min > max {
panic_with_error!(&env, Error::InvalidInput);
}
if max > MAX_DATA_VALUE {
panic_with_error!(&env, Error::InvalidInput);
}

let range_key = StorageKey::DataTypeRange(data_type.clone());

// Special case: both zero means clear the override
if min == 0 && max == 0 {
env.storage().persistent().remove(&range_key);
} else {
env.storage().persistent().set(&range_key, &(min, max));
env.storage().persistent().extend_ttl(&range_key, TTL_THRESHOLD, TTL_EXTEND_TO);
}

env.events().publish(
(Symbol::new(&env, "data_type_range_updated"),),
(data_type, min, max),
);
}

/// Get the configured value range for a data type.
/// Returns (min, max) or None if using defaults.
pub fn get_data_type_range(env: Env, data_type: Symbol) -> Option<(i128, i128)> {
env.storage()
.persistent()
.get(&StorageKey::DataTypeRange(data_type))
}

/// Report whether the data for `(data_type, key)` is fresh enough to use,
/// without panicking.
///
Expand Down Expand Up @@ -2071,11 +2137,19 @@ impl OracleVerifier {
if confidence == 0 || confidence > 100 {
panic_with_error!(&env, Error::InvalidConfidence);
}
// Validate data_value is within a reasonable range to prevent overflow
// in downstream aggregation calculations (median, weighted average, etc.)

// SECURITY FIX: Validate data_value is within data-type-specific ranges
// to prevent overflow in downstream aggregation calculations.
// First check global bounds, then apply per-data-type limits.
if value > MAX_DATA_VALUE || value < -MAX_DATA_VALUE {
panic_with_error!(&env, Error::InvalidInput);
}

// Get data-type-specific range if configured, otherwise use defaults
let type_max = Self::get_data_type_max(&env, &data_type);
if value.abs() > type_max {
panic_with_error!(&env, Error::InvalidInput);
}

let now = env.ledger().timestamp();
let future_buffer: u64 = env
Expand Down Expand Up @@ -3333,6 +3407,35 @@ impl OracleVerifier {
})
}

/// Get the maximum allowed absolute value for a data type.
/// Returns the per-type override when set, otherwise returns a default
/// based on the data type name, falling back to MAX_DATA_VALUE.
fn get_data_type_max(env: &Env, data_type: &Symbol) -> i128 {
// Check for explicit admin-configured range override
if let Some((_, max)) = env.storage()
.persistent()
.get::<_, (i128, i128)>(&StorageKey::DataTypeRange(data_type.clone())) {
return max;
}

// Use defaults based on data type name
let sym_val = data_type.to_symbol_val();
if let Ok(sym_str) = SymbolStr::try_from_val(env, &sym_val) {
let s_str: &str = sym_str.as_ref();
match s_str {
"weather" => DEFAULT_WEATHER_MAX,
"flight" => DEFAULT_FLIGHT_MAX,
"onchain" => DEFAULT_ONCHAIN_MAX,
"defi" => DEFAULT_DEFI_MAX,
"disaster" => DEFAULT_DISASTER_MAX,
"health" => DEFAULT_HEALTH_MAX,
_ => MAX_DATA_VALUE,
}
} else {
MAX_DATA_VALUE
}
}

/// Drop statistical outliers from a batch of collected oracle values
/// ahead of aggregation, using a MAD-based (median absolute deviation)
/// modified z-score (issue #383). `values`/`timestamps` are the same
Expand Down
Loading