diff --git a/contracts/oracle-verifier/src/lib.rs b/contracts/oracle-verifier/src/lib.rs index 467d29b..7e1dd10 100644 --- a/contracts/oracle-verifier/src/lib.rs +++ b/contracts/oracle-verifier/src/lib.rs @@ -1277,6 +1277,11 @@ impl OracleVerifier { /// consume storage/instruction budget (griefing). pub fn set_min_submit_interval(env: Env, admin: Address, seconds: u64) { Self::require_admin(&env, &admin); + // Issue #624: reject zero -- a zero interval removes the per-oracle + // rate-limit guard and allows unlimited oracle submissions per block. + if seconds == 0 { + panic_with_error!(&env, Error::InvalidInput); + } env.storage() .instance() .set(&StorageKey::MinSubmitInterval, &seconds); diff --git a/contracts/policy-engine/src/lib.rs b/contracts/policy-engine/src/lib.rs index cdde810..c8b1c32 100644 --- a/contracts/policy-engine/src/lib.rs +++ b/contracts/policy-engine/src/lib.rs @@ -647,6 +647,11 @@ impl PolicyEngine { if duration_days == 0 { panic_with_error!(env, Error::InvalidDurationRange); } + // Issue #623: validate duration_days against the product ceiling + // explicitly. The computed (end_time - start_time) must never + // exceed max_duration_days * 86_400 seconds. Checking the + // parameter here (before any arithmetic) ensures this holds even + // for scheduled-start policies where start_time != now. if duration_days > product.max_duration_days { panic_with_error!(env, Error::DurationTooLong); } @@ -683,6 +688,15 @@ impl PolicyEngine { if end_time <= start_time { panic_with_error!(env, Error::InvalidDurationRange); } + // Issue #623: defense-in-depth -- confirm the computed span does + // not exceed max_duration_days * 86_400 seconds. This catches any + // future code path that might compute duration_secs independently. + let max_duration_secs = (product.max_duration_days as u64) + .checked_mul(86_400) + .unwrap_or(u64::MAX); + if end_time.saturating_sub(start_time) > max_duration_secs { + panic_with_error!(env, Error::DurationTooLong); + } let policy_id = Self::next_policy_id(env); let policy = Policy { diff --git a/contracts/risk-pool/src/lib.rs b/contracts/risk-pool/src/lib.rs index dfbc61b..38c91e6 100644 --- a/contracts/risk-pool/src/lib.rs +++ b/contracts/risk-pool/src/lib.rs @@ -390,6 +390,8 @@ impl RiskPool { } minted - burn_shares } else { + // Issue #621: integer division truncates toward zero; the + // MIN_SHARES guard below catches the resulting 0 case. amount.checked_mul(total_shares) .and_then(|v| v.checked_div(total_deposited)) .unwrap_or_else(|| panic_with_error!(&env, Error::Overflow)) @@ -539,6 +541,11 @@ impl RiskPool { let available_liquidity = total_deposited.saturating_sub(total_locked); if available_liquidity <= 0 { panic_with_error!(&env, Error::Undercollateralized); } + // Issue #621: shares * total_deposited / total_shares truncates + // toward zero; if the pool has absorbed losses total_deposited + // may be tiny relative to total_shares and a small redemption + // rounds to 0. The ZeroAmount guard below rejects such cases so + // no shares are ever burned without returning capital. let amount = shares.checked_mul(total_deposited) .and_then(|v| v.checked_div(total_shares)) .unwrap_or_else(|| panic_with_error!(&env, Error::Overflow)); @@ -692,9 +699,14 @@ impl RiskPool { panic_with_error!(&env, Error::InsufficientShares); } + // Issue #621: share-to-amount calculation can underflow to 0 when + // from_pos.deposited has been eroded by losses while shares stayed + // constant. A zero-amount transfer burns the shares without moving + // any capital, so reject it explicitly. let amount = shares.checked_mul(from_pos.deposited) .and_then(|v| v.checked_div(from_pos.shares)) .unwrap_or_else(|| panic_with_error!(&env, Error::Overflow)); + if amount == 0 { panic_with_error!(&env, Error::ZeroAmount); } let pending_yield_from = Self::settle_yield(&env, &mut from_pos); from_pos.deposited = from_pos.deposited.saturating_sub(amount);