From 94c6a667df655d56cecb15c84ec32f8a9a234875 Mon Sep 17 00:00:00 2001 From: edevlopa Date: Wed, 21 Oct 2026 00:42:52 +0100 Subject: [PATCH] fix: validate evidence hash, limit oracles per key, add delegation expiry, and update cumulative yield on withdrawal Closes #728 Closes #727 Closes #726 Closes #725 --- contracts/claims-processor/src/lib.rs | 45 +++++++++++++++++++++++++++ contracts/governance-dao/src/lib.rs | 38 +++++++++++++++++++++- contracts/oracle-verifier/src/lib.rs | 9 +++--- contracts/risk-pool/src/lib.rs | 9 +++++- 4 files changed, 95 insertions(+), 6 deletions(-) diff --git a/contracts/claims-processor/src/lib.rs b/contracts/claims-processor/src/lib.rs index df0deb6..e2b120c 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), + /// Submitted evidence hashes for a claim (Vec) (issue #728, #747). + ClaimEvidence(u128), } /// Maximum number of evidence files allowed per claim (issue #747). @@ -223,6 +225,10 @@ pub enum Error { DuplicateClaimReference = 37, /// Dispute deadline must be strictly in the future (issue #740). InvalidDisputeDeadline = 38, + /// Evidence file count exceeds maximum allowed limit (issue #747). + EvidenceLimitExceeded = 39, + /// Evidence hash cannot be zero or invalid format (issue #728). + InvalidEvidence = 40, } /// Approximate Stellar ledger close time in seconds, used to convert @@ -2016,6 +2022,45 @@ impl ClaimsProcessor { Self::claim_deadline(&env) } + /// Submit evidence files for a claim, rejecting zero hashes and enforcing max file count (issue #728, #747). + pub fn submit_claim_evidence(env: Env, claim_id: u128, claimant: Address, evidence_hashes: Vec) { + Self::require_not_paused(&env); + claimant.require_auth(); + let claim: Claim = env.storage().persistent() + .get(&StorageKey::Claim(claim_id)) + .unwrap_or_else(|| panic_with_error!(&env, Error::ClaimNotFound)); + if claim.claimant != claimant { + panic_with_error!(&env, Error::Unauthorized); + } + if evidence_hashes.len() > MAX_EVIDENCE_FILES { + panic_with_error!(&env, Error::EvidenceLimitExceeded); + } + for i in 0..evidence_hashes.len() { + let hash = evidence_hashes.get_unchecked(i); + if hash.is_empty() { + panic_with_error!(&env, Error::InvalidEvidence); + } + let mut all_zeros = true; + for b in hash.iter() { + if b != 0 { + all_zeros = false; + break; + } + } + if all_zeros { + panic_with_error!(&env, Error::InvalidEvidence); + } + } + let key = StorageKey::ClaimEvidence(claim_id); + env.storage().persistent().set(&key, &evidence_hashes); + env.storage().persistent().extend_ttl(&key, TTL_THRESHOLD, TTL_EXTEND_TO); + } + + /// Retrieve attached evidence hashes for a claim if available (issue #728, #747). + pub fn get_claim_evidence(env: Env, claim_id: u128) -> Vec { + env.storage().persistent().get(&StorageKey::ClaimEvidence(claim_id)).unwrap_or_else(|| Vec::new(&env)) + } + // ── Payout Delay (issue #432) ───────────────────────────────────────────── /// Set the delay in seconds between claim approval and actual payout. diff --git a/contracts/governance-dao/src/lib.rs b/contracts/governance-dao/src/lib.rs index c5858f0..1b9e048 100644 --- a/contracts/governance-dao/src/lib.rs +++ b/contracts/governance-dao/src/lib.rs @@ -126,6 +126,8 @@ enum StorageKey { Participation, /// Who an address has delegated its voting power to — Address → Address. DelegateOf(Address), + /// Expiration timestamp of a vote delegation — Address -> u64 (issue #726). + DelegationExpiry(Address), /// Addresses currently delegating to an account — Address → Vec
. Delegators(Address), /// Marks that a delegator's weight was already counted in a proposal, so @@ -677,6 +679,12 @@ impl GovernanceDao { .persistent() .set(&StorageKey::Delegators(delegate.clone()), &delegators); + // Set delegation expiry (default 180 days) (issue #726) + let expiry = env.ledger().timestamp().saturating_add(180 * 24 * 60 * 60); + env.storage() + .persistent() + .set(&StorageKey::DelegationExpiry(delegator.clone()), &expiry); + let config: DaoConfig = env.storage().instance().get(&StorageKey::Config).unwrap(); let weight = token::Client::new(&env, &config.gov_token).balance(&delegator); @@ -701,6 +709,28 @@ impl GovernanceDao { ); } + /// Delegate voting power with an explicit expiration duration in seconds (issue #726). + pub fn delegate_with_expiry(env: Env, delegator: Address, delegate: Address, duration_seconds: u64) { + if duration_seconds == 0 { + panic_with_error!(&env, Error::InvalidInput); + } + Self::delegate(env.clone(), delegator.clone(), delegate); + let expiry = env.ledger().timestamp().saturating_add(duration_seconds); + env.storage() + .persistent() + .set(&StorageKey::DelegationExpiry(delegator), &expiry); + } + + /// Check if a delegation is expired (issue #726). + pub fn is_delegation_expired(env: Env, delegator: Address) -> bool { + let now = env.ledger().timestamp(); + if let Some(expiry) = env.storage().persistent().get::<_, u64>(&StorageKey::DelegationExpiry(delegator)) { + now > expiry + } else { + false + } + } + /// Withdraw a delegation, returning voting authority to the delegator. /// /// Effective immediately for any proposal the delegate has not already @@ -719,6 +749,9 @@ impl GovernanceDao { env.storage() .persistent() .remove(&StorageKey::DelegateOf(delegator.clone())); + env.storage() + .persistent() + .remove(&StorageKey::DelegationExpiry(delegator.clone())); Self::remove_delegator(&env, &delegate, &delegator); env.events().publish( @@ -741,8 +774,11 @@ impl GovernanceDao { ); } - /// Who this address has delegated to, if anyone. + /// Who this address has delegated to, if anyone (respects expiration). pub fn get_delegate(env: Env, delegator: Address) -> Option
{ + if Self::is_delegation_expired(env.clone(), delegator.clone()) { + return None; + } env.storage() .persistent() .get(&StorageKey::DelegateOf(delegator)) diff --git a/contracts/oracle-verifier/src/lib.rs b/contracts/oracle-verifier/src/lib.rs index 415b04d..cc66297 100644 --- a/contracts/oracle-verifier/src/lib.rs +++ b/contracts/oracle-verifier/src/lib.rs @@ -37,6 +37,8 @@ use parashield_common::{TTL_THRESHOLD, TTL_EXTEND_TO, ADMIN_TRANSFER_TIMELOCK}; /// cannot overflow i128. #[allow(dead_code)] const MAX_ORACLES: u32 = 100; +/// Maximum number of registered oracles per key or data type (issue #727). +pub const MAX_ORACLES_PER_KEY: u32 = 10; /// Maximum number of data points stored per (data_type, key). const MAX_DATA_POINTS: u32 = 100; @@ -339,11 +341,10 @@ impl OracleVerifier { .get(&StorageKey::OracleList(data_type.clone())) .unwrap_or_else(|| Vec::new(&env)); - // Count how many oracles are already registered for this data_type - // If close to max, require admin to review (this is a simplified check). - // In a more sophisticated system, you'd track controller pubkeys. + // Count how many oracles are already registered for this data_type / key (issue #727). + // Limit to MAX_ORACLES_PER_KEY to prevent Sybil attacks and oracle domination. let oracle_count = list.len(); - if oracle_count >= MAX_ORACLES { + if oracle_count >= MAX_ORACLES_PER_KEY { panic_with_error!(&env, Error::TooManyOracles); } diff --git a/contracts/risk-pool/src/lib.rs b/contracts/risk-pool/src/lib.rs index 756fd91..8785704 100644 --- a/contracts/risk-pool/src/lib.rs +++ b/contracts/risk-pool/src/lib.rs @@ -660,10 +660,17 @@ impl RiskPool { if amount == 0 { panic_with_error!(&env, Error::ZeroAmount); } if amount > available_liquidity { panic_with_error!(&env, Error::Undercollateralized); } + // Issue #725: Update cumulative premium per share and settle yield during withdrawal let pending_yield = Self::settle_yield(&env, &mut position); + let acc_per_share: i128 = env + .storage() + .instance() + .get(&StorageKey::AccumulatedPerShare) + .unwrap_or(0); position.deposited = position.deposited.saturating_sub(amount); position.shares -= shares; - position.yield_debt = (env.storage().instance().get(&StorageKey::AccumulatedPerShare).unwrap_or(0) * position.shares) / 1_000_000_000_000; + position.yield_debt = (acc_per_share * position.shares) / 1_000_000_000_000; + env.storage().instance().set(&StorageKey::AccumulatedPerShare, &acc_per_share); env.storage().persistent().set(&lp_key, &position); Self::extend_to_max(&env, &lp_key); env.storage().instance().set(&StorageKey::TotalDeposited, &total_deposited.checked_sub(amount).unwrap_or_else(|| panic_with_error!(&env, Error::Overflow)));