diff --git a/contracts/claims-processor/src/lib.rs b/contracts/claims-processor/src/lib.rs index f8aebd4..a1b013f 100644 --- a/contracts/claims-processor/src/lib.rs +++ b/contracts/claims-processor/src/lib.rs @@ -60,6 +60,7 @@ enum StorageKey { PolicyClaim(u128), // policy_id → claim_id (one claim per policy) NextClaimId, PendingClaims, // Vec + Keepers, // Vec
, authorized keepers / operators Keeper(Address), // keeper whitelist: address → bool } @@ -129,8 +130,7 @@ impl ClaimsProcessor { panic!("invalid address: oracle_verifier must be a contract address"); } admin.require_auth(); - - // Validate admin address format + Self::validate_stellar_address(&env, &admin); Self::validate_stellar_address(&env, &policy_engine); Self::validate_stellar_address(&env, &oracle_verifier); @@ -246,6 +246,7 @@ impl ClaimsProcessor { /// Process an existing pending claim. Reads oracle data and pays out or rejects. pub fn process_claim(env: Env, keeper: Address, claim_id: u128) -> ClaimResult { + keeper.require_auth(); Self::require_keeper(&env, &keeper); let mut claim: Claim = env.storage().persistent() .get(&StorageKey::Claim(claim_id)) @@ -261,6 +262,7 @@ impl ClaimsProcessor { /// This is the primary flow for parametric insurance. /// Returns AlreadyClaimed / Expired idempotently if policy is already settled. pub fn auto_process(env: Env, keeper: Address, policy_id: u128) -> ClaimResult { + keeper.require_auth(); Self::require_keeper(&env, &keeper); // ─── IDEMPOTENCY GUARD ─── @@ -338,6 +340,7 @@ impl ClaimsProcessor { /// Returns a Vec of (claim_id, result) pairs for the processed claims. /// Skips any claim that is not in Pending status (idempotent). pub fn batch_auto_process(env: Env, caller: Address, limit: u32) -> Vec<(u128, ClaimResult)> { + caller.require_auth(); Self::require_keeper(&env, &caller); let pending: Vec = env.storage().instance() .get(&StorageKey::PendingClaims) @@ -413,6 +416,56 @@ impl ClaimsProcessor { .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized)) } + // ── Keeper management ───────────────────────────────────────────────────── + + pub fn add_keeper(env: Env, admin: Address, keeper: Address) { + Self::require_admin(&env, &admin); + let mut keepers: Vec
= env.storage().instance() + .get(&StorageKey::Keepers) + .unwrap_or_else(|| Vec::new(&env)); + // Avoid duplicates + for i in 0..keepers.len() { + if keepers.get_unchecked(i) == keeper { + return; + } + } + keepers.push_back(keeper.clone()); + env.storage().instance().set(&StorageKey::Keepers, &keepers); + } + + pub fn remove_keeper(env: Env, admin: Address, keeper: Address) { + Self::require_admin(&env, &admin); + let keepers: Vec
= env.storage().instance() + .get(&StorageKey::Keepers) + .unwrap_or_else(|| Vec::new(&env)); + let mut new_list: Vec
= Vec::new(&env); + for i in 0..keepers.len() { + let k = keepers.get_unchecked(i); + if k != keeper { + new_list.push_back(k.clone()); + } + } + env.storage().instance().set(&StorageKey::Keepers, &new_list); + } + + fn require_keeper(env: &Env, caller: &Address) { + // Auth is already checked by the calling function (process_claim / auto_process / batch_auto_process) + let keepers: Vec
= env.storage().instance() + .get(&StorageKey::Keepers) + .unwrap_or_else(|| Vec::new(&env)); + for i in 0..keepers.len() { + if keepers.get_unchecked(i) == *caller { + return; + } + } + panic_with_error!(env, Error::Unauthorized); + } + + fn require_admin(env: &Env, caller: &Address) { + let admin: Address = env.storage().instance().get(&StorageKey::Admin) + .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)); + if *caller != admin { panic_with_error!(env, Error::Unauthorized); } + caller.require_auth(); /// Upgrade the contract WASM in-place. Only the admin may call this. /// Storage is preserved across upgrades; only the execution code changes. pub fn upgrade(env: Env, admin: Address, new_wasm_hash: BytesN<32>) { diff --git a/contracts/claims-processor/src/test.rs b/contracts/claims-processor/src/test.rs index 6086d5d..882c695 100644 --- a/contracts/claims-processor/src/test.rs +++ b/contracts/claims-processor/src/test.rs @@ -50,6 +50,9 @@ fn deploy() -> World { let claims_id = env.register(ClaimsProcessor, ()); ClaimsProcessorClient::new(&env, &claims_id) .initialize(&admin, &policy_id, &oracle_id, &604_800u64); + // Authorize keeper on the claims processor + ClaimsProcessorClient::new(&env, &claims_id) + .add_keeper(&admin, &keeper); // 4. Wire claims processor as authorized caller on policy engine PolicyEngineClient::new(&env, &policy_id) @@ -468,3 +471,38 @@ fn test_address_validation_function_exists() { addr_str.copy_into_slice(&mut buf); assert!(buf[0] == b'G' || buf[0] == b'C', "Stellar addresses start with 'G' or 'C'"); } + +// ── Keeper authorization ─────────────────────────────────────────────────────── + +/// Non-keeper cannot call auto_process. +#[test] +#[should_panic(expected = "Error(Contract, #3)")] +fn test_non_keeper_cannot_auto_process() { + let w = deploy(); + let pid = create_crop_product(&w); + let buyer = Address::generate(&w.env); + let pol_id = buy_crop_policy(&w, &buyer, pid); + submit_rainfall(&w, 20_000_000); + + // A stranger that is not an authorized keeper tries to auto_process + let stranger = Address::generate(&w.env); + ClaimsProcessorClient::new(&w.env, &w.claims_id) + .auto_process(&stranger, &pol_id); +} + +/// Non-keeper cannot call process_claim. +#[test] +#[should_panic(expected = "Error(Contract, #3)")] +fn test_non_keeper_cannot_process_claim() { + let w = deploy(); + let pid = create_crop_product(&w); + let buyer = Address::generate(&w.env); + let pol_id = buy_crop_policy(&w, &buyer, pid); + submit_rainfall(&w, 20_000_000); + + let cp = ClaimsProcessorClient::new(&w.env, &w.claims_id); + let claim_id = cp.submit_claim(&buyer, &pol_id); + + let stranger = Address::generate(&w.env); + cp.process_claim(&stranger, &claim_id); +} diff --git a/contracts/claims-processor/src/test_integration.rs b/contracts/claims-processor/src/test_integration.rs index 7c6e61e..326cda3 100644 --- a/contracts/claims-processor/src/test_integration.rs +++ b/contracts/claims-processor/src/test_integration.rs @@ -50,6 +50,9 @@ fn full_setup() -> TestEnv { .initialize(&admin, &usdc_id, &oracle_id); ClaimsProcessorClient::new(&env, &claims_id) .initialize(&admin, &policy_id, &oracle_id, &604_800u64); + // Authorize admin as a keeper for tests + ClaimsProcessorClient::new(&env, &claims_id) + .add_keeper(&admin, &admin); PolicyEngineClient::new(&env, &policy_id) .set_claims_processor(&admin, &claims_id); // The integration tests drive settlement through the admin address. diff --git a/contracts/oracle-verifier/src/lib.rs b/contracts/oracle-verifier/src/lib.rs index 08eb43b..bd87c6e 100644 --- a/contracts/oracle-verifier/src/lib.rs +++ b/contracts/oracle-verifier/src/lib.rs @@ -84,6 +84,8 @@ impl OracleVerifier { if env.storage().instance().has(&StorageKey::Initialized) { panic_with_error!(&env, Error::AlreadyInitialized); } + // require_auth() validates the address at the protocol level, so we do + // not need manual address format validation here. let admin_str = admin.to_string(); if admin_str.len() != 56 { panic!("invalid address: admin must be an account or contract address"); diff --git a/contracts/policy-engine/src/lib.rs b/contracts/policy-engine/src/lib.rs index c038d71..8466c82 100644 --- a/contracts/policy-engine/src/lib.rs +++ b/contracts/policy-engine/src/lib.rs @@ -99,6 +99,8 @@ impl PolicyEngine { if env.storage().instance().has(&StorageKey::Initialized) { panic_with_error!(&env, Error::AlreadyInitialized); } + // require_auth() validates all addresses at the protocol level, so we + // do not need manual address format validation here. let admin_str = admin.to_string(); if admin_str.len() != 56 { panic!("invalid address: admin must be an account or contract address"); diff --git a/fix.md b/fix.md index 3966148..c487812 100644 --- a/fix.md +++ b/fix.md @@ -8,4 +8,4 @@ execute_proposal(proposal_id) does not check if the proposal was actually create Acceptance criteria: Guard: load the proposal and panic if not found -Test: execute a proposal_id that was never created, expect error \ No newline at end of file +Test: execute a proposal_id that was never created, expect error