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
48 changes: 48 additions & 0 deletions contracts/claims-processor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
Expand Down
12 changes: 12 additions & 0 deletions contracts/oracle-verifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()
Expand Down
9 changes: 3 additions & 6 deletions contracts/policy-engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions contracts/risk-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -2245,6 +2247,21 @@ impl RiskPool {
panic_with_error!(&env, Error::TimelockPending);
}

let guardians: Vec<Address> = 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
Expand All @@ -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<Address> = 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) {
Expand All @@ -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);
Expand Down
Loading