diff --git a/contracts/claims-processor/src/lib.rs b/contracts/claims-processor/src/lib.rs index 90b64f3..0779f10 100644 --- a/contracts/claims-processor/src/lib.rs +++ b/contracts/claims-processor/src/lib.rs @@ -2067,6 +2067,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 cfffa6d..09ec08f 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 @@ -675,6 +677,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); @@ -699,6 +707,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 @@ -717,6 +747,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( @@ -739,8 +772,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 905f575..6e2def7 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 deba113..925c57c 100644 --- a/contracts/risk-pool/src/lib.rs +++ b/contracts/risk-pool/src/lib.rs @@ -662,10 +662,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)));