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
56 changes: 56 additions & 0 deletions contracts/claims-processor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<u64> {
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.
Expand Down Expand Up @@ -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());
Expand Down
13 changes: 9 additions & 4 deletions contracts/governance-dao/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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).
Expand All @@ -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
Expand Down Expand Up @@ -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).
Expand All @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions contracts/risk-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down