diff --git a/contracts/claims-processor/src/lib.rs b/contracts/claims-processor/src/lib.rs index df0deb6..dc679fe 100644 --- a/contracts/claims-processor/src/lib.rs +++ b/contracts/claims-processor/src/lib.rs @@ -148,6 +148,8 @@ enum StorageKey { ClaimDisputeDeadline(u128), /// Per-user last claim submission timestamp: claimant (Address) -> timestamp (u64) (issue #737). UserLastClaim(Address), + /// Registered valid claim type (Symbol) -> bool (issue #734). + RegisteredClaimType(Symbol), } /// Maximum number of evidence files allowed per claim (issue #747). @@ -223,6 +225,8 @@ pub enum Error { DuplicateClaimReference = 37, /// Dispute deadline must be strictly in the future (issue #740). InvalidDisputeDeadline = 38, + /// Claim type must be registered and non-empty (issue #734). + InvalidClaimType = 39, } /// Approximate Stellar ledger close time in seconds, used to convert @@ -467,6 +471,43 @@ impl ClaimsProcessor { claim_id } + /// Register a valid claim type / product category (issue #734). + pub fn register_claim_type(env: Env, admin: Address, claim_type: Symbol) { + admin.require_auth(); + Self::require_admin(&env, &admin); + if claim_type == Symbol::new(&env, "") { + panic_with_error!(&env, Error::InvalidClaimType); + } + let key = StorageKey::RegisteredClaimType(claim_type); + env.storage().persistent().set(&key, &true); + env.storage().persistent().extend_ttl(&key, TTL_THRESHOLD, TTL_EXTEND_TO); + } + + /// Check if a claim type is registered (issue #734). + pub fn is_claim_type_registered(env: Env, claim_type: Symbol) -> bool { + let key = StorageKey::RegisteredClaimType(claim_type); + env.storage().persistent().has(&key) + } + + /// Submit a claim with an explicit claim type (issue #734). + pub fn submit_claim_with_type( + env: Env, + claimant: Address, + policy_id: u128, + claim_type: Symbol, + ) -> u128 { + claimant.require_auth(); + Self::require_not_paused(&env); + Self::require_not_admin(&env, &claimant); + Self::require_not_zero_address(&env, &claimant); + + if claim_type == Symbol::new(&env, "") || !Self::is_claim_type_registered(env.clone(), claim_type) { + panic_with_error!(&env, Error::InvalidClaimType); + } + + Self::file_claim(&env, &claimant, policy_id) + } + /// The claim-filing body shared by `submit_claim` and /// `batch_submit_claims`. The caller has already authorized `claimant`, /// checked the pause flag and rejected the admin — deliberately @@ -489,6 +530,13 @@ impl ClaimsProcessor { let policy = PolicyEngineClient::new(env, &policy_engine) .get_policy(&policy_id); + // Validate claim type / product category is non-empty (issue #734) + let product = PolicyEngineClient::new(env, &policy_engine) + .get_product(&policy.product_id); + if product.category == Symbol::new(env, "") { + panic_with_error!(env, Error::InvalidClaimType); + } + if policy.coverage_amount <= 0 { panic_with_error!(env, Error::InvalidClaimAmount); } diff --git a/contracts/oracle-verifier/src/lib.rs b/contracts/oracle-verifier/src/lib.rs index 415b04d..c3ca5fb 100644 --- a/contracts/oracle-verifier/src/lib.rs +++ b/contracts/oracle-verifier/src/lib.rs @@ -2341,6 +2341,11 @@ impl OracleVerifier { panic_with_error!(&env, Error::InvalidInput); } + // Zero-timestamp oracle readings are rejected (issue #735) + if timestamp == 0 { + panic_with_error!(&env, Error::InvalidTimestamp); + } + let now = env.ledger().timestamp(); let future_buffer: u64 = env .storage() @@ -2525,6 +2530,10 @@ impl OracleVerifier { Self::validate_symbol_len(&env, &data_type); Self::validate_key_len(&env, &key); + if timestamp == 0 { + panic_with_error!(&env, Error::InvalidTimestamp); + } + let now = env.ledger().timestamp(); if timestamp > now { panic_with_error!(&env, Error::InvalidTimestamp); @@ -3222,6 +3231,9 @@ impl OracleVerifier { if sub.confidence == 0 || sub.confidence > 100 { panic_with_error!(&env, Error::InvalidConfidence); } + if sub.timestamp == 0 { + panic_with_error!(&env, Error::InvalidTimestamp); + } let now = env.ledger().timestamp(); let future_buffer: u64 = env .storage() diff --git a/contracts/policy-engine/src/lib.rs b/contracts/policy-engine/src/lib.rs index bbdb973..8bad5ce 100644 --- a/contracts/policy-engine/src/lib.rs +++ b/contracts/policy-engine/src/lib.rs @@ -540,12 +540,9 @@ impl PolicyEngine { { panic_with_error!(&env, Error::InvalidTriggerThreshold); } - // Coverage bounds must form a valid, positive range: 0 < min < max. - // Rejects free coverage (min <= 0) and inverted or zero-width ranges - // (min >= max), which `buy_policy` could never satisfy (issue #567). - // A non-positive `max` cannot satisfy the same inequality, so it - // needs no separate branch. - if params.coverage_min <= 0 || params.coverage_min >= params.coverage_max { + // Coverage bounds must form a valid, positive range: 0 < min < max (issue #736, #567). + // Rejects free coverage (min <= 0) and inverted or zero-width ranges (coverage_max < coverage_min). + if params.coverage_min <= 0 || params.coverage_max < params.coverage_min || params.coverage_min >= params.coverage_max { panic_with_error!(&env, Error::InvalidCoverageRange); } // Coverage ceiling must be reasonable (issue #616). An unbounded diff --git a/contracts/risk-pool/src/lib.rs b/contracts/risk-pool/src/lib.rs index 756fd91..531349e 100644 --- a/contracts/risk-pool/src/lib.rs +++ b/contracts/risk-pool/src/lib.rs @@ -167,6 +167,8 @@ pub(crate) enum StorageKey { ReentrancyGuard, /// Human-readable pool name (issue #748). PoolName, + /// Approvals count for pending admin withdrawal (issue #733). + AdminWithdrawalApprovals, } /// Maximum pool name length in bytes/characters (issue #748). @@ -2245,6 +2247,21 @@ impl RiskPool { panic_with_error!(&env, Error::TimelockPending); } + let guardians: Vec
= env + .storage() + .instance() + .get(&StorageKey::Guardians) + .unwrap_or_else(|| Vec::new(&env)); + let threshold: u32 = env + .storage() + .instance() + .get(&StorageKey::GuardianThreshold) + .unwrap_or(0); + if !guardians.is_empty() && threshold == 0 { + panic_with_error!(&env, Error::InvalidThreshold); + } + env.storage().persistent().set(&StorageKey::AdminWithdrawalApprovals, &0u32); + let now = env.ledger().timestamp(); // Freeze the deadline now. Recomputing it at execution time would let a // contract upgrade that shortens TIMELOCK_SECONDS cut the wait on a @@ -2271,6 +2288,34 @@ impl RiskPool { ); } + /// Guardian approval for an admin withdrawal request (issue #733). + pub fn approve_admin_withdrawal(env: Env, guardian: Address) { + guardian.require_auth(); + let guardians: Vec
= env + .storage() + .instance() + .get(&StorageKey::Guardians) + .unwrap_or_else(|| Vec::new(&env)); + let mut is_guardian = false; + for g in guardians.iter() { + if g == guardian { + is_guardian = true; + break; + } + } + if !is_guardian { + panic_with_error!(&env, Error::NotGuardian); + } + let approvals: u32 = env + .storage() + .persistent() + .get(&StorageKey::AdminWithdrawalApprovals) + .unwrap_or(0); + env.storage() + .persistent() + .set(&StorageKey::AdminWithdrawalApprovals, &(approvals + 1)); + } + /// Execute a previously requested admin withdrawal after the 7-day timelock. /// Funds are transferred to the treasury address. pub fn execute_admin_withdrawal(env: Env, admin: Address) { @@ -2282,6 +2327,22 @@ impl RiskPool { if req.executed { panic_with_error!(&env, Error::AlreadyReleased); } + let threshold: u32 = env + .storage() + .instance() + .get(&StorageKey::GuardianThreshold) + .unwrap_or(0); + if threshold > 0 { + let approvals: u32 = env + .storage() + .persistent() + .get(&StorageKey::AdminWithdrawalApprovals) + .unwrap_or(0); + if approvals < threshold { + panic_with_error!(&env, Error::Unauthorized); + } + } + let now = env.ledger().timestamp(); if now < req.execute_after { panic_with_error!(&env, Error::TimelockNotReady);