diff --git a/contracts/claims-processor/src/lib.rs b/contracts/claims-processor/src/lib.rs index fe89991..df0deb6 100644 --- a/contracts/claims-processor/src/lib.rs +++ b/contracts/claims-processor/src/lib.rs @@ -144,6 +144,10 @@ enum StorageKey { DisputeWindow, /// Claim reference number mapping: reference_number (Symbol) -> claim_id (u128) (issue #754). ClaimReference(Symbol), + /// Custom dispute deadline for a claim: claim_id (u128) -> deadline (u64) (issue #740). + ClaimDisputeDeadline(u128), + /// Per-user last claim submission timestamp: claimant (Address) -> timestamp (u64) (issue #737). + UserLastClaim(Address), } /// Maximum number of evidence files allowed per claim (issue #747). @@ -217,6 +221,8 @@ pub enum Error { DisputeWindowExpired = 36, /// Duplicate claim reference numbers are not permitted (issue #754). DuplicateClaimReference = 37, + /// Dispute deadline must be strictly in the future (issue #740). + InvalidDisputeDeadline = 38, } /// Approximate Stellar ledger close time in seconds, used to convert @@ -540,6 +546,18 @@ impl ClaimsProcessor { panic_with_error!(env, Error::ClaimOutsideCoveragePeriod); } + // SECURITY FIX (issue #737): Rate limit claim submissions per user to prevent claim flooding. + // Users must wait at least 60 seconds between successive claim submissions. + const USER_CLAIM_COOLDOWN_SECONDS: u64 = 60; + let last_claim_key = StorageKey::UserLastClaim(claimant.clone()); + if let Some(last_time) = env.storage().persistent().get::<_, u64>(&last_claim_key) { + if now < last_time.saturating_add(USER_CLAIM_COOLDOWN_SECONDS) { + panic_with_error!(env, Error::RateLimitExceeded); + } + } + env.storage().persistent().set(&last_claim_key, &now); + env.storage().persistent().extend_ttl(&last_claim_key, TTL_THRESHOLD, TTL_EXTEND_TO); + let claim_id = Self::next_claim_id(env); // Issue #437: score this submission against the configured fraud @@ -1140,6 +1158,35 @@ impl ClaimsProcessor { .unwrap_or(DEFAULT_DISPUTE_WINDOW) } + /// Admin sets an explicit dispute deadline for a claim, validating it is strictly in the future (issue #740). + pub fn set_dispute_deadline(env: Env, admin: Address, claim_id: u128, deadline: u64) { + Self::require_admin(&env, &admin); + let now = env.ledger().timestamp(); + if deadline <= now { + panic_with_error!(&env, Error::InvalidDisputeDeadline); + } + let _claim: Claim = env.storage().persistent() + .get(&StorageKey::Claim(claim_id)) + .unwrap_or_else(|| panic_with_error!(&env, Error::ClaimNotFound)); + let key = StorageKey::ClaimDisputeDeadline(claim_id); + env.storage().persistent().set(&key, &deadline); + env.storage().persistent().extend_ttl(&key, TTL_THRESHOLD, TTL_EXTEND_TO); + env.events().publish( + (Symbol::new(&env, "dispute_deadline_set"),), + (claim_id, deadline), + ); + } + + /// Retrieve the explicit dispute deadline for a claim if configured (issue #740). + pub fn get_dispute_deadline(env: Env, claim_id: u128) -> Option { + env.storage().persistent().get(&StorageKey::ClaimDisputeDeadline(claim_id)) + } + + /// Return the timestamp of a claimant's last submitted claim (issue #737). + pub fn get_user_last_claim(env: Env, claimant: Address) -> u64 { + env.storage().persistent().get(&StorageKey::UserLastClaim(claimant)).unwrap_or(0) + } + /// Escalate a Pending or Rejected claim to Disputed status. /// SECURITY FIX: Only the original claimant or the policy holder may dispute. /// This restricts dispute initiation to parties with legitimate interest in the claim. @@ -1172,6 +1219,15 @@ impl ClaimsProcessor { { panic_with_error!(&env, Error::AlreadyProcessed); } + + // Validate custom dispute deadline if set (issue #740) + let now = env.ledger().timestamp(); + if let Some(deadline) = env.storage().persistent().get::<_, u64>(&StorageKey::ClaimDisputeDeadline(claim_id)) { + if now > deadline { + panic_with_error!(&env, Error::DisputeWindowExpired); + } + } + if claim.status != ClaimStatus::Pending { if let Some(processed_at) = claim.processed_at { let dispute_window = Self::get_dispute_window(env.clone()); diff --git a/contracts/governance-dao/src/lib.rs b/contracts/governance-dao/src/lib.rs index 520e7a8..c5858f0 100644 --- a/contracts/governance-dao/src/lib.rs +++ b/contracts/governance-dao/src/lib.rs @@ -226,6 +226,8 @@ pub enum Error { CircularDelegation = 51, /// A proposal template with this name already exists (issue #746). DuplicateTemplateName = 52, + /// Vote weight must be strictly positive (> 0) (issue #739). + ZeroVoteWeight = 53, } #[contract] @@ -835,10 +837,10 @@ impl GovernanceDao { panic_with_error!(&env, Error::WeightAlreadyCounted); } - // 1. Capture voting balance weight + // 1. Capture voting balance weight - strictly positive weight required (issue #739) let own_weight = gov_token.balance(&voter); if own_weight <= 0 { - panic_with_error!(&env, Error::InsufficientWeight); + panic_with_error!(&env, Error::ZeroVoteWeight); } // Apply vote weight cap to prevent whale dominance (issue #431). @@ -854,7 +856,7 @@ impl GovernanceDao { let delegated_weight = Self::collect_delegated_weight(&env, &voter, proposal_id, &gov_token); let weight = own_tally_weight.saturating_add(delegated_weight); if weight <= 0 { - panic_with_error!(&env, Error::InsufficientWeight); + panic_with_error!(&env, Error::ZeroVoteWeight); } // 3. Lock tokens in the DAO contract to prevent token cycling / double-voting @@ -981,7 +983,7 @@ impl GovernanceDao { let gov_token = token::Client::new(&env, &config.gov_token); let own_weight = gov_token.balance(&voter); if own_weight <= 0 { - panic_with_error!(&env, Error::InsufficientWeight); + panic_with_error!(&env, Error::ZeroVoteWeight); } // Apply vote weight cap to prevent whale dominance (issue #431). @@ -1000,6 +1002,9 @@ impl GovernanceDao { let delegated_weight = Self::collect_delegated_weight(&env, &voter, proposal_id, &gov_token); let weight = own_tally_weight.saturating_add(delegated_weight); + if weight <= 0 { + panic_with_error!(&env, Error::ZeroVoteWeight); + } match choice { VoteChoice::For => proposal.votes_for += weight, diff --git a/contracts/risk-pool/src/lib.rs b/contracts/risk-pool/src/lib.rs index dd3613e..756fd91 100644 --- a/contracts/risk-pool/src/lib.rs +++ b/contracts/risk-pool/src/lib.rs @@ -379,6 +379,24 @@ impl RiskPool { /// capacity change lands on their own deposit. The separation costs /// nothing here — the pool already requires third-party liquidity to /// function — so the conflict is refused outright rather than bounded. + /// Deposit liquidity specifying the deposit token address, verifying it matches the pool's token (issue #738). + pub fn deposit_with_token( + env: Env, + provider: Address, + deposit_token: Address, + amount: i128, + min_shares: i128, + compound_enabled: bool, + ) -> i128 { + let usdc: Address = env.storage().instance() + .get(&StorageKey::UsdcToken) + .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)); + if deposit_token != usdc { + panic_with_error!(&env, Error::InvalidToken); + } + Self::deposit(env, provider, amount, min_shares, compound_enabled) + } + pub fn deposit( env: Env, provider: Address,